[
  {
    "path": ".VERSION",
    "content": "$Format:%d$\n"
  },
  {
    "path": ".dockerignore",
    "content": "bin\ncoverage.txt\n*.test\n*.out\n.travis-releases\n"
  },
  {
    "path": ".gitattributes",
    "content": ".VERSION export-subst\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug-report.yml",
    "content": "name: Bug Report\ndescription: File a bug report\ntitle: \"[Bug]: \"\nlabels: [\"bug\", \"needs triage\"]\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        Thanks for taking the time to fill out this bug report!\n  - type: textarea\n    id: steps\n    attributes:\n      label: Steps to Reproduce\n      description: Tell us how to reproduce this issue.\n      placeholder: These are the steps!\n    validations:\n      required: true\n  - type: textarea\n    id: your-env\n    attributes:\n      label: Your Environment\n      value: |-\n             * OS -\n             * `step-ca` Version -\n    validations:\n      required: true\n  - type: textarea\n    id: expected-behavior\n    attributes:\n      label: Expected Behavior\n      description: What did you expect to happen?\n    validations:\n      required: true\n  - type: textarea\n    id: actual-behavior\n    attributes:\n      label: Actual Behavior\n      description: What happens instead?\n    validations:\n      required: true\n  - type: textarea\n    id: context\n    attributes:\n      label: Additional Context\n      description: Add any other context about the problem here.\n    validations:\n      required: false\n  - type: textarea\n    id: contributing\n    attributes:\n      label: Contributing\n      value: |\n             Vote on this issue by adding a 👍 reaction.\n             To contribute a fix for this issue, leave a comment (and link to your pull request, if you've opened one already).\n    validations:\n      required: false\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: true\ncontact_links:\n  - name: Ask on Discord\n    url: https://discord.gg/7xgjhVAg6g\n    about: You can ask for help here!\n  - name: Want to contribute to step certificates?\n    url: https://github.com/smallstep/certificates/blob/master/CONTRIBUTING.md\n    about: Be sure to read contributing guidelines!\n\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/documentation-request.md",
    "content": "---\nname: Documentation Request\nabout: Request documentation for a feature\ntitle: '[Docs]:'\nlabels: docs, needs triage\nassignees: ''\n\n---\n\n## Hello!\n<!-- Please leave this section as-is, it's designed to help others in the community know how to interact with our GitHub issues. -->\n\n- Vote on this issue by adding a 👍 reaction\n- If you want to document this feature, comment to let us know (we'll work with you on design, scheduling, etc.)\n\n## Affected area/feature\n\n<!---\nTell us which feature you'd like to see documented. \n - Where would you like that documentation to live (command line usage output, website, github markdown on the repo)? \n- If there are specific attributes or options you'd like to see documented, please include those in the request.\n-->\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/enhancement.md",
    "content": "---\nname: Enhancement\nabout: Suggest an enhancement to step-ca\ntitle: ''\nlabels: enhancement, needs triage\nassignees: ''\n\n---\n\n## Hello!\n<!-- Please leave this section as-is, \nit's designed to help others in the community know how to interact with our GitHub issues. -->\n\n- Vote on this issue by adding a 👍 reaction\n- If you want to implement this feature, comment to let us know (we'll work with you on design, scheduling, etc.)\n\n## Issue details\n\n<!-- Enhancement requests are most helpful when they describe the problem you're having \nas well as articulating the potential solution you'd like to see built. -->\n\n## Why is this needed?\n\n<!-- Let us know why you think this enhancement would be good for the project or community. -->\n"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE",
    "content": "<!---\nPlease provide answers in the spaces below each prompt, where applicable.\nNot every PR requires responses for each prompt.\nUse your discretion.\n-->\n#### Name of feature:\n\n#### Pain or issue this feature alleviates:\n\n#### Why is this important to the project (if not answered above):\n\n#### Is there documentation on how to use this feature? If so, where?\n\n#### In what environments or workflows is this feature supported?\n\n#### In what environments or workflows is this feature explicitly NOT supported (if any)?\n\n#### Supporting links/other PRs/issues:\n\n💔Thank you!\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "# To get started with Dependabot version updates, you'll need to specify which\n# package ecosystems to update and where the package manifests are located.\n# Please see the documentation for all configuration options:\n# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates\n\nversion: 2\nupdates:\n  - package-ecosystem: \"gomod\" # See documentation for possible values\n    directory: \"/\" # Location of package manifests\n    schedule:\n      interval: \"weekly\"\n  - package-ecosystem: \"github-actions\"\n    directory: \"/\"\n    schedule:\n      interval: \"weekly\"\n"
  },
  {
    "path": ".github/workflows/actionci.yml",
    "content": "name: Action CI\n\non:\n  push:\n    tags-ignore:\n    - 'v*'\n    branches:\n    - \"master\"\n  pull_request:\n  workflow_call:\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}\n  cancel-in-progress: true\n\njobs:\n  actionci:\n    permissions:\n      contents: read\n      security-events: write\n    uses: smallstep/workflows/.github/workflows/actionci.yml@main\n    with:\n      zizmor-advanced-security: true\n    secrets: inherit\n"
  },
  {
    "path": ".github/workflows/ci.yml",
    "content": "name: CI\n\non:\n  push:\n    tags-ignore:\n    - 'v*'\n    branches:\n    - \"master\"\n  pull_request:\n  workflow_call:\n    secrets:\n      CODECOV_TOKEN:\n        required: true\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}\n  cancel-in-progress: true\n\npermissions:\n  contents: read\n\njobs:\n  ci:\n    permissions:\n      actions: read\n      contents: read\n      security-events: write\n    uses: smallstep/workflows/.github/workflows/goCI.yml@main\n    with:\n      only-latest-golang: false\n      os-dependencies: 'libpcsclite-dev'\n      run-codeql: true\n      test-command: 'V=1 make test'\n    secrets: inherit\n"
  },
  {
    "path": ".github/workflows/code-scan-cron.yml",
    "content": "on:\n  schedule:\n    - cron: '0 0 * * *'\n\npermissions:\n  actions: read\n  contents: read\n  security-events: write\n\njobs:\n  code-scan:\n    uses: smallstep/workflows/.github/workflows/code-scan.yml@main\n"
  },
  {
    "path": ".github/workflows/dependabot-auto-merge.yml",
    "content": "name: Dependabot auto-merge\non: pull_request\n\npermissions:\n  contents: write\n  pull-requests: write\n\njobs:\n  dependabot-auto-merge:\n    uses: smallstep/workflows/.github/workflows/dependabot-auto-merge.yml@main\n    secrets: inherit\n"
  },
  {
    "path": ".github/workflows/publish-packages.yml",
    "content": "name: Publish to packages.smallstep.com\n\n# Independently publish packages to Red Hat (RPM) and Debian (DEB) repositories\n# without running a full release. Downloads packages from GitHub releases,\n# uploads to GCS, and imports to Artifact Registry.\n#\n# Usage (CLI):\n#   gh workflow run publish-packages.yml -f tag=v0.28.0\n\non:\n  workflow_dispatch:\n    inputs:\n      tag:\n        description: 'Git tag to publish (e.g., v0.28.0)'\n        required: true\n        type: string\n\njobs:\n  publish:\n    runs-on: ubuntu-latest\n    permissions:\n      id-token: write\n      contents: read\n    steps:\n      - name: Checkout\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n        with:\n          ref: ${{ inputs.tag }}\n          fetch-depth: 0\n\n      - name: Extract version\n        id: version\n        run: echo \"version=${TAG#v}\" >> \"$GITHUB_OUTPUT\"\n        env:\n          TAG: ${{ inputs.tag }}\n\n      - name: Is Pre-release\n        id: is_prerelease\n        run: |\n          if [[ \"$TAG\" == *\"-rc\"* ]]; then\n            echo \"is_prerelease=true\" >> \"$GITHUB_OUTPUT\"\n          else\n            echo \"is_prerelease=false\" >> \"$GITHUB_OUTPUT\"\n          fi\n        env:\n          TAG: ${{ inputs.tag }}\n\n      - name: Authenticate to Google Cloud\n        uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 # v3.0.0\n        with:\n          workload_identity_provider: ${{ secrets.GOOGLE_CLOUD_WORKLOAD_IDENTITY_PROVIDER }}\n          service_account: ${{ secrets.GOOGLE_CLOUD_GITHUB_SERVICE_ACCOUNT }}\n\n      - name: Set up Cloud SDK\n        uses: google-github-actions/setup-gcloud@aa5489c8933f4cc7a4f7d45035b3b1440c9c10db # v3.0.1\n        with:\n          project_id: ${{ secrets.GOOGLE_CLOUD_PACKAGES_PROJECT_ID }}\n\n      - name: Download packages from GitHub release\n        run: |\n          mkdir -p dist\n          gh release download \"$TAG\" --pattern \"*${VERSION}*.deb\" --pattern \"*${VERSION}*.rpm\" --dir dist\n        env:\n          TAG: ${{ inputs.tag }}\n          VERSION: ${{ steps.version.outputs.version }}\n          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Upload packages to GCS\n        run: |\n          for pkg in dist/*.deb dist/*.rpm; do\n            ./scripts/package-upload.sh \"$pkg\" step-ca ${{ steps.version.outputs.version }}\n          done\n\n      - name: Import packages to Artifact Registry\n        run: ./scripts/package-repo-import.sh step-ca ${{ steps.version.outputs.version }}\n        env:\n          IS_PRERELEASE: ${{ steps.is_prerelease.outputs.is_prerelease }}\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: Create Release & Upload Assets\n\non:\n  push:\n    # Sequence of patterns matched against refs/tags\n    tags:\n    - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10\n\npermissions:\n  contents: write\n\njobs:\n  ci:\n    permissions:\n      contents: read\n      actions: read\n      security-events: write\n    uses: smallstep/certificates/.github/workflows/ci.yml@master\n    secrets: inherit\n\n  create_release:\n    name: Create Release\n    permissions:\n      contents: write\n    needs: ci\n    runs-on: ubuntu-latest\n    env:\n      DOCKER_IMAGE: smallstep/step-ca\n    outputs:\n      version: ${{ steps.extract-tag.outputs.VERSION }}\n      is_prerelease: ${{ steps.is_prerelease.outputs.IS_PRERELEASE }}\n      docker_tags: ${{ env.DOCKER_TAGS }}\n      docker_tags_hsm: ${{ env.DOCKER_TAGS_HSM }}\n    steps:\n      - name: Is Pre-release\n        id: is_prerelease\n        env:\n          REF: ${{ github.ref }}\n        run: |\n          set +e\n          echo \"${REF}\" | grep \"\\-rc.*\"\n          OUT=$?\n          if [ $OUT -eq 0 ]; then IS_PRERELEASE=true; else IS_PRERELEASE=false; fi\n          echo \"IS_PRERELEASE=${IS_PRERELEASE}\" >> \"${GITHUB_OUTPUT}\"\n      - name: Extract Tag Names\n        id: extract-tag\n        run: |\n          VERSION=${GITHUB_REF#refs/tags/v}\n          echo \"VERSION=${VERSION}\" >> \"${GITHUB_OUTPUT}\"\n          echo \"DOCKER_TAGS=${{ env.DOCKER_IMAGE }}:${VERSION}\" >> \"${GITHUB_ENV}\"\n          echo \"DOCKER_TAGS_HSM=${{ env.DOCKER_IMAGE }}:${VERSION}-hsm\" >> \"${GITHUB_ENV}\"\n      - name: Add Latest Tag\n        if: steps.is_prerelease.outputs.IS_PRERELEASE == 'false'\n        run: |\n          echo \"DOCKER_TAGS=${{ env.DOCKER_TAGS }},${{ env.DOCKER_IMAGE }}:latest\" >> \"${GITHUB_ENV}\"\n          echo \"DOCKER_TAGS_HSM=${{ env.DOCKER_TAGS_HSM }},${{ env.DOCKER_IMAGE }}:hsm\" >> \"${GITHUB_ENV}\"\n      - name: Create Release\n        id: create_release\n        uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2.6.1\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        with:\n          tag_name: ${{ github.ref_name }}\n          name: Release ${{ github.ref_name }}\n          draft: false\n          prerelease: ${{ steps.is_prerelease.outputs.IS_PRERELEASE }}\n\n  goreleaser:\n    needs: create_release\n    permissions:\n      id-token: write\n      contents: write\n      packages: write\n    uses: smallstep/workflows/.github/workflows/goreleaser.yml@main\n    with:\n      enable-packages-upload: true\n      is-prerelease: ${{ needs.create_release.outputs.is_prerelease == 'true' }}\n    secrets: inherit\n\n  build_upload_docker:\n    name: Build & Upload Docker Images\n    needs: create_release\n    permissions:\n      id-token: write\n      contents: write\n    uses: smallstep/workflows/.github/workflows/docker-buildx-push.yml@main\n    with:\n      platforms: linux/amd64,linux/386,linux/arm,linux/arm64\n      tags: ${{ needs.create_release.outputs.docker_tags }}\n      docker_image: smallstep/step-ca\n      docker_file: docker/Dockerfile\n    secrets: inherit\n\n  build_upload_docker_hsm:\n    name: Build & Upload HSM Enabled Docker Images\n    needs: create_release\n    permissions:\n      id-token: write\n      contents: write\n    uses: smallstep/workflows/.github/workflows/docker-buildx-push.yml@main\n    with:\n      platforms: linux/amd64,linux/386,linux/arm,linux/arm64\n      tags: ${{ needs.create_release.outputs.docker_tags_hsm }}\n      docker_image: smallstep/step-ca\n      docker_file: docker/Dockerfile.hsm\n    secrets: inherit\n"
  },
  {
    "path": ".github/workflows/triage.yml",
    "content": "name: Add Issues and PRs to Triage\n\non:\n  issues:\n    types:\n      - opened\n      - reopened\n  pull_request_target:\n    types:\n      - opened\n      - reopened\n\npermissions:\n  pull-requests: write\n  issues: write\n\njobs:\n  triage:\n    uses: smallstep/workflows/.github/workflows/triage.yml@main\n    secrets: inherit\n"
  },
  {
    "path": ".github/zizmor.yml",
    "content": "rules:\n  unpinned-uses:\n    config:\n      policies:\n        \"smallstep/*\": ref-pin\n  secrets-inherit:\n    disable: true\n  ref-confusion:\n    disable: true\n  dangerous-triggers:\n    ignore:\n      - triage.yml\n"
  },
  {
    "path": ".gitignore",
    "content": "# Binaries for programs and plugins\n/bin\n*.exe\n*.exe~\n*.dll\n*.so\n*.dylib\n\n# Go Workspaces\ngo.work\ngo.work.sum\n\n# Test binary, build with `go test -c`\n*.test\n\n# Output of the go coverage tool, specifically when used with LiteIDE\n*.out\n\n# Others\n*.swp\n.releases\ncoverage.txt\noutput\nvendor\ndist/\n.idea\n.envrc\n\n# Packages files\n0x889B19391F774443-Certify.key\ngha-creds-*.json\n"
  },
  {
    "path": ".goreleaser.yml",
    "content": "# Documentation: https://goreleaser.com/customization/\n# yaml-language-server: $schema=https://goreleaser.com/static/schema-pro.json\nproject_name: step-ca\nversion: 2\n\nvariables:\n  packageName: step-ca\n  packageRelease: 1 # Manually update release: in the nfpm section to match this value if you change this\n\nbefore:\n  hooks:\n    # You may remove this if you don't use go modules.\n    - go mod download\n\nafter:\n  hooks:\n    # This script depends on IS_PRERELEASE env being set. This is set by CI in the Is Pre-release step.\n    - cmd: bash scripts/package-repo-import.sh {{ .Var.packageName }} {{ .Version }}\n      output: true\n\nbuilds:\n  -\n    id: step-ca\n    env:\n      - CGO_ENABLED=0\n    targets:\n      - darwin_amd64\n      - darwin_arm64\n      - freebsd_amd64\n      - linux_386\n      - linux_amd64\n      - linux_arm64\n      - linux_arm_5\n      - linux_arm_6\n      - linux_arm_7\n      - windows_amd64\n    flags:\n      - -trimpath\n    main: ./cmd/step-ca/main.go\n    binary: step-ca\n    ldflags:\n      - -w -X main.Version={{.Version}} -X main.BuildTime={{.Date}}\n\narchives:\n  - &ARCHIVE\n    # Can be used to change the archive formats for specific GOOSs.\n    # Most common use case is to archive as zip on Windows.\n    # Default is empty.\n    name_template: \"{{ .ProjectName }}_{{ .Os }}_{{ .Version }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}{{ if .Mips }}_{{ .Mips }}{{ end }}\"\n    format_overrides:\n      - goos: windows\n        format: zip\n    files:\n      - README.md\n      - LICENSE\n    allow_different_binary_count: true\n  -\n    << : *ARCHIVE\n    id: unversioned\n    name_template: \"{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}{{ if .Mips }}_{{ .Mips }}{{ end }}\"\n    wrap_in_directory: \"{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}{{ if .Mips }}_{{ .Mips }}{{ end }}\"\n\n\nnfpms:\n  # Configure nFPM for .deb and .rpm releases\n  #\n  # See https://nfpm.goreleaser.com/configuration/\n  # and https://goreleaser.com/customization/nfpm/\n  #\n  # Useful tools for debugging .debs:\n  # List file contents: dpkg -c dist/step_...deb\n  # Package metadata: dpkg --info dist/step_....deb\n  #\n  - &NFPM\n    id: packages\n    builds:\n      - step-ca\n    package_name: \"{{ .Var.packageName }}\"\n    release: \"1\"\n    file_name_template: >-\n      {{- trimsuffix .ConventionalFileName .ConventionalExtension -}}\n      {{- if and (eq .Arm \"6\") (eq .ConventionalExtension \".deb\") }}6{{ end -}}\n      {{- if not (eq .Amd64 \"v1\")}}{{ .Amd64 }}{{ end -}}\n      {{- .ConventionalExtension -}}\n    vendor: Smallstep Labs\n    homepage: https://github.com/smallstep/certificates\n    maintainer: Smallstep <techadmin@smallstep.com>\n    description: >\n      step-ca is an online certificate authority for secure, automated certificate management.\n    license: Apache 2.0\n    section: utils\n    formats:\n      - deb\n      - rpm\n    priority: optional\n    bindir: /usr/bin\n    contents:\n      - src: debian/copyright\n        dst: /usr/share/doc/step-ca/copyright\n    rpm:\n      signature:\n          key_file: \"{{ .Env.GPG_PRIVATE_KEY_FILE }}\"\n    deb:\n      signature:\n          key_file: \"{{ .Env.GPG_PRIVATE_KEY_FILE }}\"\n          type: origin\n  -\n    << : *NFPM\n    id: unversioned\n    file_name_template: \"{{ .PackageName }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}{{ if .Mips }}_{{ .Mips }}{{ end }}\"\n\nsource:\n  enabled: true\n  name_template: '{{ .ProjectName }}_{{ .Version }}'\n\nchecksum:\n  name_template: 'checksums.txt'\n  extra_files:\n    - glob: ./.releases/*\n\nsigns:\n- cmd: cosign\n  signature: \"${artifact}.sigstore.json\"\n  args:\n    - \"sign-blob\"\n    - \"--bundle=${signature}\"\n    - \"${artifact}\"\n    - \"--yes\"\n  artifacts: all\n\npublishers:\n- name: Google Cloud Artifact Registry\n  ids:\n  - packages\n  cmd: ./scripts/package-upload.sh {{ abs .ArtifactPath }} {{ .Var.packageName }} {{ .Version }} {{ .Var.packageRelease }}\n\nsnapshot:\n  name_template: \"{{ .Tag }}-next\"\n\nrelease:\n  # Repo in which the release will be created.\n  # Default is extracted from the origin remote URL or empty if its private hosted.\n  # Note: it can only be one: either github, gitlab or gitea\n  github:\n    owner: smallstep\n    name: certificates\n\n  # IDs of the archives to use.\n  # Defaults to all.\n  #ids:\n  #  - foo\n  #  - bar\n\n  # If set to true, will not auto-publish the release.\n  # Default is false.\n  draft: false\n\n  # If set to auto, will mark the release as not ready for production\n  # in case there is an indicator for this in the tag e.g. v1.0.0-rc1\n  # If set to true, will mark the release as not ready for production.\n  # Default is false.\n  prerelease: auto\n\n  # You can change the name of the release.\n  # Default is `{{.Tag}}`\n  name_template: \"Step CA {{ .Tag }} ({{ .Env.RELEASE_DATE }})\"\n\n  # Header template for the release body.\n  # Defaults to empty.\n  header: |\n    ## Official Release Artifacts\n\n    #### Linux\n\n    - 📦 [step-ca_linux_{{ .Version }}_amd64.tar.gz](https://dl.smallstep.com/gh-release/certificates/gh-release-header/{{ .Tag }}/step-ca_linux_{{ .Version }}_amd64.tar.gz)\n    - 📦 [step-ca_{{ replace .Version \"-\" \".\" }}-{{ .Var.packageRelease }}_amd64.deb](https://dl.smallstep.com/gh-release/certificates/gh-release-header/{{ .Tag }}/step-ca_{{ replace .Version \"-\" \".\" }}-{{ .Var.packageRelease }}_amd64.deb)\n    - 📦 [step-ca-{{ replace .Version \"-\" \".\" }}-{{ .Var.packageRelease }}.x86_64.rpm](https://dl.smallstep.com/gh-release/certificates/gh-release-header/{{ .Tag }}/step-ca-{{ replace .Version \"-\" \".\" }}-{{ .Var.packageRelease }}.x86_64.rpm)\n    - 📦 [step-ca_{{ replace .Version \"-\" \".\" }}-{{ .Var.packageRelease }}_arm64.deb](https://dl.smallstep.com/gh-release/certificates/gh-release-header/{{ .Tag }}/step-ca_{{ replace .Version \"-\" \".\" }}-{{ .Var.packageRelease }}_arm64.deb)\n    - 📦 [step-ca-{{ replace .Version \"-\" \".\" }}-{{ .Var.packageRelease }}.aarch64.rpm](https://dl.smallstep.com/gh-release/certificates/gh-release-header/{{ .Tag }}/step-ca-{{ replace .Version \"-\" \".\" }}-{{ .Var.packageRelease }}.aarch64.rpm)\n\n    #### OSX Darwin\n\n    - 📦 [step-ca_darwin_{{ .Version }}_amd64.tar.gz](https://dl.smallstep.com/gh-release/certificates/gh-release-header/{{ .Tag }}/step-ca_darwin_{{ .Version }}_amd64.tar.gz)\n    - 📦 [step-ca_darwin_{{ .Version }}_arm64.tar.gz](https://dl.smallstep.com/gh-release/certificates/gh-release-header/{{ .Tag }}/step-ca_darwin_{{ .Version }}_arm64.tar.gz)\n\n    #### Windows\n\n    - 📦 [step-ca_windows_{{ .Version }}_amd64.zip](https://dl.smallstep.com/gh-release/certificates/gh-release-header/{{ .Tag }}/step-ca_windows_{{ .Version }}_amd64.zip)\n\n    For more builds across platforms and architectures, see the `Assets` section below.\n    And for packaged versions (Docker, k8s, Homebrew), see our [installation docs](https://smallstep.com/docs/step-ca/installation).\n\n    Don't see the artifact you need? Open an issue [here](https://github.com/smallstep/certificates/issues/new/choose).\n\n    ## Signatures and Checksums\n\n    `step-ca` uses [sigstore/cosign](https://github.com/sigstore/cosign) for signing and verifying release artifacts.\n\n    Below is an example using `cosign` to verify a release artifact:\n\n    ```\n    cosign verify-blob \\\n      --bundle step-ca_darwin_{{ .Version }}_amd64.tar.gz.sigstore.json \\\n      --certificate-identity-regexp \"https://github\\.com/smallstep/workflows/.*\" \\\n      --certificate-oidc-issuer https://token.actions.githubusercontent.com \\\n      step-ca_darwin_{{ .Version }}_amd64.tar.gz\n    ```\n\n    The `checksums.txt` file (in the `Assets` section below) contains a checksum for every artifact in the release.\n\n  # Footer template for the release body.\n  # Defaults to empty.\n  footer: |\n    ## Thanks!\n\n    Those were the changes on {{ .Tag }}!\n\n    Come join us on [Discord](https://discord.gg/X2RKGwEbV9) to ask questions, chat about PKI, or get a sneak peek at the freshest PKI memes.\n\n  # You can disable this pipe in order to not upload any artifacts.\n  # Defaults to false.\n  #disable: true\n\n  # You can add extra pre-existing files to the release.\n  # The filename on the release will be the last part of the path (base). If\n  # another file with the same name exists, the latest one found will be used.\n  # Defaults to empty.\n  extra_files:\n    - glob: ./.releases/*\n  #extra_files:\n  #  - glob: ./path/to/file.txt\n  #  - glob: ./glob/**/to/**/file/**/*\n  #  - glob: ./glob/foo/to/bar/file/foobar/override_from_previous\n\nwinget:\n  -\n    # IDs of the archives to use.\n    # Empty means all IDs.\n    ids: [ default ]\n\n    #\n    # Default: ProjectName\n    # Templates: allowed\n    name: step-ca\n\n    # Publisher name.\n    #\n    # Templates: allowed\n    # Required.\n    publisher: Smallstep\n\n    # Your app's description.\n    #\n    # Templates: allowed\n    # Required.\n    short_description: \"A private certificate authority (X.509 & SSH) & ACME server for secure automated certificate management.\"\n\n    # Package identifier.\n    #\n    # Default: Publisher.ProjectName\n    # Templates: allowed\n    package_identifier: Smallstep.step-ca\n\n    # License name.\n    #\n    # Templates: allowed\n    # Required.\n    license: \"Apache-2.0\"\n\n    # Publisher URL.\n    #\n    # Templates: allowed\n    publisher_url: \"https://smallstep.com\"\n\n    # Publisher support URL.\n    #\n    # Templates: allowed\n    publisher_support_url: \"https://github.com/smallstep/certificates/discussions\"\n\n    # Privacy URL.\n    #\n    # Templates: allowed\n    privacy_url: \"https://smallstep.com/privacy-policy/\"\n\n    # URL which is determined by the given Token (github, gitlab or gitea).\n    #\n    # Default depends on the client.\n    # Templates: allowed\n    url_template: \"https://github.com/smallstep/certificates/releases/download/{{ .Tag }}/{{ .ArtifactName }}\"\n\n    # Git author used to commit to the repository.\n    commit_author:\n      name: goreleaserbot\n      email: goreleaser@smallstep.com\n\n    # The project name and current git tag are used in the format string.\n    #\n    # Templates: allowed\n    commit_msg_template: \"{{ .PackageIdentifier }}: {{ .Tag }}\"\n\n    # Your app's homepage.\n    homepage: \"https://github.com/smallstep/certificates\"\n\n    # Your app's long description.\n    #\n    # Templates: allowed\n    description: \"step-ca is an online certificate authority for secure, automated certificate management. It issues X.509 and SSH certificates using protocols like ACME, OIDC, and SCEP.\"\n\n    # License URL.\n    #\n    # Templates: allowed\n    license_url: \"https://github.com/smallstep/certificates/blob/master/LICENSE\"\n\n    # Release notes.\n    #\n    # Templates: allowed\n    release_notes: \"{{.Changelog}}\"\n\n    # Release notes URL.\n    #\n    # Templates: allowed\n    release_notes_url: \"https://github.com/smallstep/certificates/releases/tag/{{ .Tag }}\"\n\n    # Installation notes.\n    #\n    # Templates: allowed\n    installation_notes: \"After installation, run 'step-ca --help' to get started. Documentation: https://smallstep.com/docs/step-ca\"\n\n    # Create the PR - for testing\n    skip_upload: auto\n\n    # Tags.\n    tags:\n      - certificates\n      - smallstep\n      - tls\n\n    # Repository to push the generated files to.\n    repository:\n      owner: smallstep\n      name: winget-pkgs\n      branch: \"step-ca-{{.Version}}\"\n\n      # Optionally a token can be provided, if it differs from the token\n      # provided to GoReleaser\n      # Templates: allowed\n      #token: \"{{ .Env.GITHUB_PERSONAL_AUTH_TOKEN }}\"\n\n      # Sets up pull request creation instead of just pushing to the given branch.\n      # Make sure the 'branch' property is different from base before enabling\n      # it.\n      #\n      # Since: v1.17\n      pull_request:\n        # Whether to enable it or not.\n        enabled: true\n        check_boxes: true\n        # Whether to open the PR as a draft or not.\n        #\n        # Default: false\n        # Since: v1.19\n        # draft: true\n\n        # Base can also be another repository, in which case the owner and name\n        # above will be used as HEAD, allowing cross-repository pull requests.\n        #\n        # Since: v1.19\n        base:\n          owner: microsoft\n          name: winget-pkgs\n          branch: master\n\n\nscoops:\n  -\n    ids: [ default ]\n    # Template for the url which is determined by the given Token (github or gitlab)\n    # Default for github is \"https://github.com/<repo_owner>/<repo_name>/releases/download/{{ .Tag }}/{{ .ArtifactName }}\"\n    # Default for gitlab is \"https://gitlab.com/<repo_owner>/<repo_name>/uploads/{{ .ArtifactUploadHash }}/{{ .ArtifactName }}\"\n    # Default for gitea is \"https://gitea.com/<repo_owner>/<repo_name>/releases/download/{{ .Tag }}/{{ .ArtifactName }}\"\n    url_template: \"http://github.com/smallstep/certificates/releases/download/{{ .Tag }}/{{ .ArtifactName }}\"\n    # Repository to push the app manifest to.\n    repository:\n      owner: smallstep\n      name: scoop-bucket\n      branch: main\n\n    # Git author used to commit to the repository.\n    # Defaults are shown.\n    commit_author:\n      name: goreleaserbot\n      email: goreleaser@smallstep.com\n\n    # The project name and current git tag are used in the format string.\n    commit_msg_template: \"Scoop update for {{ .ProjectName }} version {{ .Tag }}\"\n\n    # Your app's homepage.\n    # Default is empty.\n    homepage: \"https://smallstep.com/docs/step-ca\"\n\n    # Skip uploads for prerelease.\n    skip_upload: auto\n\n    # Your app's description.\n    # Default is empty.\n    description: \"A private certificate authority (X.509 & SSH) & ACME server for secure automated certificate management, so you can use TLS everywhere & SSO for SSH.\"\n\n    # Your app's license\n    # Default is empty.\n    license: \"Apache-2.0\"\n\n"
  },
  {
    "path": ".version.sh",
    "content": "#!/usr/bin/env sh\nread -r firstline < .VERSION\nlast_half=\"${firstline##*tag: }\"\ncase \"$last_half\" in\n    v*)\n        version_string=\"${last_half%%[,)]*}\"\n        ;;\nesac\necho \"${version_string:-v0.0.0}\"\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Changelog\n\nAll notable changes to this project will be documented in this file.\n\nThe format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)\nand this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).\n\n## TEMPLATE -- do not alter or remove\n\n---\n\n## [x.y.z] - aaaa-bb-cc\n\n### Added\n\n### Changed\n\n### Deprecated\n\n### Removed\n\n### Fixed\n\n### Security\n\n---\n\n### [0.30.1] - 2026-03-18\n - Fix release issue\n\n\n### [0.30.0] - 2026-03-18\n\n### Added\n\n- Warn when ACME provisioner is configured without a database (smallstep/certificates#2526)\n- Validate webhooks configured on the ca.json (smallstep/certificates#2570)\n- Add HTTP transport decorator (smallstep/certificates#2533)\n\n### Changed\n\n- Upgrade HSM-enabled Docker images from Debian Bookworm (12) to Debian Trixie\n  (13) (smallstep/certificates#2493)\n- Use JSON array format for Dockerfile's `CMD` instruction. This prevents shell\n  interpolation of environment variables like `CONFIGPATH` and `PWDPATH`,\n  ensuring consistent command execution. Commands can still be overridden via\n  Kubernetes or Docker configuration when needed (smallstep/certificates#2493)\n\n### Fixed\n\n- Fix CRL IssuingDistributionPoint marshaling to correctly unset `OnlyContainsUserCerts` and `OnlyContainsCACerts` flags (smallstep/certificates#2511)\n- Fix CRL DER download content-disposition filename extension from `.der` to `.crl` (smallstep/certificates#2537)\n- Fix SSH agent KMS when CA is configured with Prometheus instrumented signer (smallstep/certificates#2379)\n- Return helpful error message when root certificate is not found (smallstep/certificates#1893)\n- Fix missing version number when building step-ca from source archive (smallstep/certificates#2513)\n- Fix potential panic if a certificate had an empty tcg-kp-AIKCertificate extended key usage (smallstep/certificates#2569)\n- Fix CA startup when configured with SCEP and Google Cloud CAS (smallstep/certificates#2517)\n- Close idle connections on client certificate renew (smallstep/certificates#2515)\n\n\n## [0.29.0] - 2025-12-03\n\n### Added\n\n- Add support for YubiKeys 5.7.4+ (smallstep/certificates#2370)\n- Support managed device ID OID for step attestation format (smallstep/certificates#2382)\n- Add support for remote configuration of GCP Organization-Id (smallstep/certificates#2408)\n- Add additional DOCKER_STEPCA_INIT_* envs for docker/entrypoint.sh (smallstep/certificates#2461)\n- Add sd_notify support (smallstep/certificates#2463)\n\n### Changed\n\n- Use errgroup to shutdown services concurrently (smallstep/certificates#2343)\n\n### Fixed\n\n- Fix process hanging after SIGTERM (smallstep/certificates#2338)\n- Disable execute permission on a few policy/engine source files (smallstep/certificates#2435)\n- Fix backdate support for ACME provisioner (smallstep/certificates#2444)\n\n### Security\n\n- Authorization Bypass in ACME and SCEP Provisioners (smallstep/certificates#2491)\n- Improper Authorization Check for SSH Certificate Revocation (smallstep/certificates#2491)\n\n\n## [0.28.4] - 2025-07-13\n\n### Added\n\n- Add support for using key usage, extended key usage, and basic constraints\n  from certificate requests in certificate templates (smallstep/crypto#767)\n- Allow to specify audience when generating JWK provisioner tokens (smallstep/certificates#2326)\n- Add SSH certificate type to exposed metrics (smallstep/certificates#2290)\n- Enable dynamic validation of project ownership within a GCP organization\n  when using the GCP Cloud Instance Identity provisioner (smallstep/certificates#2133)\n\n### Changed\n\n- Introduce poolhttp package for improved memory performance of Authority\n  httpClients (smallstep/certificates#2325)\n\n\n## [0.28.3] - 2025-03-17\n\n- dependabot updates\n\n\n## [0.28.2] - 2025-02-20\n\n### Added\n\n- Added support for imported keys on YubiKey (smallstep/certificates#2113)\n- Enable storing ACME attestation payload (smallstep/certificates#2114)\n- Add ACME attestation format field to ACME challenge (smallstep/certificates#2124)\n\n### Changed\n\n- Added internal httptransport package to replace cloning of http.DefaultTransport (smallstep/certificates#2098, smallstep/certificates#2103, smallstep/certificates#2104)\n  - For example, replacing http.DefaultTransport clone in provisioner webhook business logic.\n\n\n## [0.28.1] - 2024-11-19\n\n### Added\n\n- Support for using template data from SCEPCHALLENGE webhooks (smallstep/certificates#2065)\n- New field to Webhook response that allows for propagation of human readable errors to the client (smallstep/certificates#2066, smallstep/certificates#2069)\n- CICD for pushing DEB and RPM packages to packages.smallstep.com on releases (smallstep/certificates#2076)\n- PKCS11 utilities in HSM container image (smallstep/certificates#2077)\n\n### Changed\n\n- Artifact names for RPM and DEB packages in conformance with standards (smallstep/certificates#2076)\n\n\n## [0.28.0] - 2024-10-29\n\n### Added\n\n- Add options to GCP IID provisioner to enable or disable signing of SSH user and host certificates (smallstep/certificates#2045)\n\n### Changed\n\n- For IID provisioners with disableCustomSANs set to true, validate that the\n  requested DNS names are a subset of the allowed DNS names (based on the IID token),\n  rather than requiring an exact match to the entire list of allowed DNS names. (smallstep/certificates#2044)\n\n\n## [0.27.5] - 2024-10-17\n\n### Added\n\n- Option to log real IP (x-forwarded-for) in logging middleware (smallstep/certificates#2002)\n\n### Fixed\n\n- Pulled in updates to smallstep/pkcs7 to fix failing Windows SCEP enrollment certificates (smallstep/certificates#1994)\n\n\n## [0.27.4] - 2024-09-13\n\n### Fixed\n\n- Release worfklow\n\n## [0.27.3] - 2024-09-13\n\n### Added\n\n- AWS auth method for Vault RA mode (smallstep/certificates#1976)\n- API endpoints for retrieving Intermediate certificates (smallstep/certificates#1962)\n- Enable use of OIDC provisioner with private identity providers and a certificate from step-ca (smallstep/certificates#1940)\n- Support for verifying `cnf` and `x5rt#S256` claim when provided in token (smallstep/certificates#1660)\n- Add Wire integration to ACME provisioner (smallstep/certificates#1666)\n\n### Changed\n\n- Clarified SSH certificate policy errors (smallstep/certificates#1951)\n\n### Fixed\n\n- Nebula ECDSA P-256 support (smallstep/certificates#1662)\n\n## [0.27.2] - 2024-07-18\n\n### Added\n\n- `--console` option to default step-ssh config (smallstep/certificates#1931)\n\n\n## [0.27.1] - 2024-07-12\n\n### Changed\n\n- Enable use of strict FQDN with a flag (smallstep/certificates#1926)\n    - This reverses a change in 0.27.0 that required the use of strict FQDNs (smallstep/certificate#1910)\n\n\n## [0.27.0] - 2024-07-11\n\n### Added\n\n- Support for validity windows in templates (smallstep/certificates#1903)\n- Create identity certificate with host URI when using any provisioner (smallstep/certificates#1922)\n\n### Changed\n\n- Do strict DNS lookup on ACME (smallstep/certificates#1910)\n\n### Fixed\n\n- Handle bad attestation object in deviceAttest01 validation (smallstep/certificates#1913)\n\n\n## [0.26.2] - 2024-06-13\n\n### Added\n\n- Add provisionerID to ACME accounts (smallstep/certificates#1830)\n- Enable verifying ACME provisioner using provisionerID if available (smallstep/certificates#1844)\n- Add methods to Authority to get intermediate certificates (smallstep/certificates#1848)\n- Add GetX509Signer method (smallstep/certificates#1850)\n\n### Changed\n\n- Make ISErrNotFound more flexible (smallstep/certificates#1819)\n- Log errors using slog.Logger (smallstep/certificates#1849)\n- Update hardcoded AWS certificates (smallstep/certificates#1881)\n\n\n## [0.26.1] - 2024-04-22\n\n### Added\n\n- Allow configuration of a custom SCEP key manager (smallstep/certificates#1797)\n\n### Fixed\n\n- id-scep-failInfoText OID (smallstep/certificates#1794)\n- CA startup with Vault RA configuration (smallstep/certificates#1803)\n\n\n## [0.26.0] - 2024-03-28\n\n### Added\n\n- [TPM KMS](https://github.com/smallstep/crypto/tree/master/kms/tpmkms) support for CA keys (smallstep/certificates#1772)\n- Propagation of HTTP request identifier using X-Request-Id header (smallstep/certificates#1743, smallstep/certificates#1542)\n- Expires header in CRL response (smallstep/certificates#1708)\n- Support for providing TLS configuration programmatically (smallstep/certificates#1685)\n- Support for providing external CAS implementation (smallstep/certificates#1684)\n- AWS `ca-west-1` identity document root certificate (smallstep/certificates#1715)\n- [COSE RS1](https://www.rfc-editor.org/rfc/rfc8812.html#section-2) as a supported algorithm with ACME `device-attest-01` challenge (smallstep/certificates#1663)\n\n### Changed\n\n- In an RA setup, let the CA decide the RA certificate lifetime (smallstep/certificates#1764)\n- Use Debian Bookworm in Docker containers (smallstep/certificates#1615)\n- Error message for CSR validation (smallstep/certificates#1665)\n- Updated dependencies\n\n### Fixed\n\n- Stop CA when any of the required servers fails to start (smallstep/certificates#1751). Before the fix, the CA would continue running and only log the server failure when stopped.\n- Configuration loading errors when not using context were not returned. Fixed in [cli-utils/109](https://github.com/smallstep/cli-utils/pull/109).\n- HTTP_PROXY and HTTPS_PROXY support for ACME validation client (smallstep/certificates#1658).\n\n### Security\n\n- Upgrade to using cosign v2 for signing artifacts\n\n## [0.25.1] - 2023-11-28\n\n### Added\n\n- Provisioner name in SCEP webhook request body in (smallstep/certificates#1617)\n- Support for ASN1 boolean encoding in (smallstep/certificates#1590)\n\n### Changed\n\n- Generation of first provisioner name on `step ca init` in (smallstep/certificates#1566)\n- Processing of SCEP Get PKIOperation requests in (smallstep/certificates#1570)\n- Support for signing identity certificate during SSH sign by skipping URI validation in (smallstep/certificates#1572)\n- Dependency on `micromdm/scep` and `go.mozilla.org/pkcs7` to use Smallstep forks in (smallstep/certificates#1600)\n- Make the Common Name validator for JWK provisioners accept values from SANs too in (smallstep/certificates#1609)\n\n### Fixed\n\n- Registration Authority token creation relied on values from CSR. Fixed to rely on template in (smallstep/certificates#1608)\n- Use same glibc version for running the CA when built using CGo in (smallstep/certificates#1616)\n\n## [0.25.0] - 2023-09-26\n\n### Added\n\n- Added support for configuring SCEP decrypters in the provisioner (smallstep/certificates#1414)\n- Added support for TPM KMS (smallstep/crypto#253)\n- Added support for disableSmallstepExtensions provisioner claim\n  (smallstep/certificates#1484)\n- Added script to migrate a badger DB to MySQL or PostgreSQL\n  (smallstep/certificates#1477)\n- Added AWS public certificates for me-central-1 and ap-southeast-3\n  (smallstep/certificates#1404)\n- Added namespace field to VaultCAS JSON config (smallstep/certificates#1424)\n- Added AWS public certificates for me-central-1 and ap-southeast-3\n  (smallstep/certificates#1404)\n- Added unversioned filenames to Github release assets\n  (smallstep/certificates#1435)\n- Send X5C leaf certificate to webhooks (smallstep/certificates#1485)\n- Added support for disableSmallstepExtensions claim (smallstep/certificates#1484)\n- Added all AWS Identity Document Certificates (smallstep/certificates#1404, smallstep/certificates#1510)\n- Added Winget release automation (smallstep/certificates#1519)\n- Added CSR to SCEPCHALLENGE webhook request body (smallstep/certificates#1523)\n- Added SCEP issuance notification webhook (smallstep/certificates#1544)\n- Added ability to disable color in the log text formatter\n  (smallstep/certificates(#1559)\n\n### Changed\n\n- Changed the Makefile to produce cgo-enabled builds running\n  `make build GO_ENVS=\"CGO_ENABLED=1\"` (smallstep/certificates#1446)\n- Return more detailed errors to ACME clients using device-attest-01\n  (smallstep/certificates#1495)\n- Change SCEP password type to string (smallstep/certificates#1555)\n\n### Removed\n\n- Removed OIDC user regexp check (smallstep/certificates#1481)\n- Removed automatic initialization of $STEPPATH (smallstep/certificates#1493)\n- Removed db datasource from error msg to prevent leaking of secrets to logs\n  (smallstep/certificates#1528)\n\n### Fixed\n\n- Improved authentication for ACME requests using kid and provisioner name\n  (smallstep/certificates#1386).\n- Fixed indentation of KMS configuration in helm charts\n  (smallstep/certificates#1405)\n- Fixed simultaneous sign or decrypt operation on a YubiKey\n  (smallstep/certificates#1476, smallstep/crypto#288)\n- Fixed adding certificate templates with ASN.1 functions\n  (smallstep/certificates#1500, smallstep/crypto#302)\n- Fixed a problem when the ca.json is truncated if the encoding of the\n  configuration fails (e.g., new provisioner with bad template data)\n  (smallstep/cli#994, smallstep/certificates#1501)\n- Fixed provisionerOptionsToLinkedCA missing template and templateData\n  (smallstep/certificates#1520)\n- Fix calculation of webhook signature (smallstep/certificates#1546)\n\n## [v0.24.2] - 2023-05-11\n\n### Added\n\n- Log SSH certificates (smallstep/certificates#1374)\n- CRL endpoints on the HTTP server (smallstep/certificates#1372)\n- Dynamic SCEP challenge validation using webhooks (smallstep/certificates#1366)\n- For Docker deployments, added DOCKER_STEPCA_INIT_PASSWORD_FILE. Useful for pointing to a Docker Secret in the container (smallstep/certificates#1384)\n\n### Changed\n\n- Depend on [smallstep/go-attestation](https://github.com/smallstep/go-attestation) instead of [google/go-attestation](https://github.com/google/go-attestation)\n- Render CRLs into http.ResponseWriter instead of memory (smallstep/certificates#1373)\n- Redaction of SCEP static challenge when listing provisioners (smallstep/certificates#1204)\n\n### Fixed\n\n- VaultCAS certificate lifetime (smallstep/certificates#1376)\n\n## [v0.24.1] - 2023-04-14\n\n### Fixed\n\n- Docker image name for HSM support (smallstep/certificates#1348)\n\n## [v0.24.0] - 2023-04-12\n\n### Added\n\n- Add ACME `device-attest-01` support with TPM 2.0\n  (smallstep/certificates#1063).\n- Add support for new Azure SDK, sovereign clouds, and HSM keys on Azure KMS\n  (smallstep/crypto#192, smallstep/crypto#197, smallstep/crypto#198,\n  smallstep/certificates#1323, smallstep/certificates#1309).\n- Add support for ASN.1 functions on certificate templates\n  (smallstep/crypto#208, smallstep/certificates#1345)\n- Add `DOCKER_STEPCA_INIT_ADDRESS` to configure the address to use in a docker\n  container (smallstep/certificates#1262).\n- Make sure that the CSR used matches the attested key when using AME\n  `device-attest-01` challenge (smallstep/certificates#1265).\n- Add support for compacting the Badger DB (smallstep/certificates#1298).\n- Build and release cleanups (smallstep/certificates#1322,\n  smallstep/certificates#1329, smallstep/certificates#1340).\n\n### Fixed\n\n- Fix support for PKCS #7 RSA-OAEP decryption through\n  [smallstep/pkcs7#4](https://github.com/smallstep/pkcs7/pull/4), as used in\n  SCEP.\n- Fix RA installation using `scripts/install-step-ra.sh`\n  (smallstep/certificates#1255).\n- Clarify error messages on policy errors (smallstep/certificates#1287,\n  smallstep/certificates#1278).\n- Clarify error message on OIDC email validation (smallstep/certificates#1290).\n- Mark the IDP critical in the generated CRL data (smallstep/certificates#1293).\n- Disable database if CA is initialized with the `--no-db` flag\n  (smallstep/certificates#1294).\n\n## [v0.23.2] - 2023-02-02\n\n### Added\n\n- Added [`step-kms-plugin`](https://github.com/smallstep/step-kms-plugin) to\n  docker images, and a new image, `smallstep/step-ca-hsm`, compiled with cgo\n  (smallstep/certificates#1243).\n- Added [`scoop`](https://scoop.sh) packages back to the release\n  (smallstep/certificates#1250).\n- Added optional flag `--pidfile` which allows passing a filename where step-ca\n  will write its process id (smallstep/certificates#1251).\n- Added helpful message on CA startup when config can't be opened\n  (smallstep/certificates#1252).\n- Improved validation and error messages on `device-attest-01` orders\n  (smallstep/certificates#1235).\n\n### Removed\n\n- The deprecated CLI utils `step-awskms-init`, `step-cloudkms-init`,\n  `step-pkcs11-init`, `step-yubikey-init` have been removed.\n  [`step`](https://github.com/smallstep/cli) and\n  [`step-kms-plugin`](https://github.com/smallstep/step-kms-plugin) should be\n  used instead (smallstep/certificates#1240).\n\n### Fixed\n\n- Fixed remote management flags in docker images (smallstep/certificates#1228).\n\n## [v0.23.1] - 2023-01-10\n\n### Added\n\n- Added configuration property `.crl.idpURL`  to be able to set a custom Issuing\n  Distribution Point in the CRL (smallstep/certificates#1178).\n- Added WithContext methods to the CA client (smallstep/certificates#1211).\n- Docker: Added environment variables for enabling Remote Management and ACME\n  provisioner (smallstep/certificates#1201).\n- Docker: The entrypoint script now generates and displays an initial JWK\n  provisioner password by default when the CA is being initialized\n  (smallstep/certificates#1223).\n\n### Changed\n\n- Ignore SSH principals validation when using an OIDC provisioner. The\n  provisioner will ignore the principals passed and set the defaults or the ones\n  including using WebHooks or templates (smallstep/certificates#1206).\n\n## [v0.23.0] - 2022-11-11\n\n### Added\n\n- Added support for ACME device-attest-01 challenge on iOS, iPadOS, tvOS and\n  YubiKey.\n- Ability to disable ACME challenges and attestation formats.\n- Added flags to change ACME challenge ports for testing purposes.\n- Added name constraints evaluation and enforcement when issuing or renewing\n  X.509 certificates.\n- Added provisioner webhooks for augmenting template data and authorizing\n  certificate requests before signing.\n- Added automatic migration of provisioners when enabling remote management.\n- Added experimental support for CRLs.\n- Add certificate renewal support on RA mode. The `step ca renew` command must\n  use the flag `--mtls=false` to use the token renewal flow.\n- Added support for initializing remote management using `step ca init`.\n- Added support for renewing X.509 certificates on RAs.\n- Added support for using SCEP with keys in a KMS.\n- Added client support to set the dialer's local address with the environment variable\n  `STEP_CLIENT_ADDR`.\n\n### Changed\n\n- Remove the email requirement for issuing SSH certificates with an OIDC\n  provisioner.\n- Root files can contain more than one certificate.\n\n### Fixed\n\n- Fixed MySQL DSN parsing issues with an upgrade to\n  [smallstep/nosql@v0.5.0](https://github.com/smallstep/nosql/releases/tag/v0.5.0).\n- Fixed renewal of certificates with missing subject attributes.\n- Fixed ACME support with [ejabberd](https://github.com/processone/ejabberd).\n\n### Deprecated\n\n- The CLIs `step-awskms-init`, `step-cloudkms-init`, `step-pkcs11-init`,\n  `step-yubikey-init` are deprecated. Now you can use\n  [`step-kms-plugin`](https://github.com/smallstep/step-kms-plugin) in\n  combination with `step certificates create` to initialize your PKI.\n\n## [0.22.1] - 2022-08-31\n\n### Fixed\n\n- Fixed signature algorithm on EC (root) + RSA (intermediate) PKIs.\n\n## [0.22.0] - 2022-08-26\n\n### Added\n\n- Added automatic configuration of Linked RAs.\n- Send provisioner configuration on Linked RAs.\n\n### Changed\n\n- Certificates signed by an issuer using an RSA key will be signed using the\n  same algorithm used to sign the issuer certificate. The signature will no\n  longer default to PKCS #1. For example, if the issuer certificate was signed\n  using RSA-PSS with SHA-256, a new certificate will also be signed using\n  RSA-PSS with SHA-256.\n- Support two latest versions of Go (1.18, 1.19).\n- Validate revocation serial number (either base 10 or prefixed with an\n  appropriate base).\n- Sanitize TLS options.\n\n## [0.20.0] - 2022-05-26\n\n### Added\n\n- Added Kubernetes auth method for Vault RAs.\n- Added support for reporting provisioners to linkedca.\n- Added support for certificate policies on authority level.\n- Added a Dockerfile with a step-ca build with HSM support.\n- A few new WithXX methods for instantiating authorities\n\n### Changed\n\n- Context usage in HTTP APIs.\n- Changed authentication for Vault RAs.\n- Error message returned to client when authenticating with expired certificate.\n- Strip padding from ACME CSRs.\n\n### Deprecated\n\n- HTTP API handler types.\n\n### Fixed\n\n- Fixed SSH revocation.\n- CA client dial context for js/wasm target.\n- Incomplete `extraNames` support in templates.\n- SCEP GET request support.\n- Large SCEP request handling.\n\n## [0.19.0] - 2022-04-19\n\n### Added\n\n- Added support for certificate renewals after expiry using the claim `allowRenewalAfterExpiry`.\n- Added support for `extraNames` in X.509 templates.\n- Added `armv5` builds.\n- Added RA support using a Vault instance as the CA.\n- Added `WithX509SignerFunc` authority option.\n- Added a new `/roots.pem` endpoint to download the CA roots in PEM format.\n- Added support for Azure `Managed Identity` tokens.\n- Added support for automatic configuration of linked RAs.\n- Added support for the `--context` flag. It's now possible to start the\n  CA with `step-ca --context=abc` to use the configuration from context `abc`.\n  When a context has been configured and no configuration file is provided\n  on startup, the configuration for the current context is used.\n- Added startup info logging and option to skip it (`--quiet`).\n- Added support for renaming the CA (Common Name).\n\n### Changed\n\n- Made SCEP CA URL paths dynamic.\n- Support two latest versions of Go (1.17, 1.18).\n- Upgrade go.step.sm/crypto to v0.16.1.\n- Upgrade go.step.sm/linkedca to v0.15.0.\n\n### Deprecated\n\n- Go 1.16 support.\n\n### Removed\n\n### Fixed\n\n- Fixed admin credentials on RAs.\n- Fixed ACME HTTP-01 challenges for IPv6 identifiers.\n- Various improvements under the hood.\n\n### Security\n\n## [0.18.2] - 2022-03-01\n\n### Added\n\n- Added `subscriptionIDs` and `objectIDs` filters to the Azure provisioner.\n- [NoSQL](https://github.com/smallstep/nosql/pull/21) package allows filtering\n  out database drivers using Go tags. For example, using the Go flag\n  `--tags=nobadger,nobbolt,nomysql` will only compile `step-ca` with the pgx\n  driver for PostgreSQL.\n\n### Changed\n\n- IPv6 addresses are normalized as IP addresses instead of hostnames.\n- More descriptive JWK decryption error message.\n- Make the X5C leaf certificate available to the templates using `{{ .AuthorizationCrt }}`.\n\n### Fixed\n\n- During provisioner add - validate provisioner configuration before storing to DB.\n\n## [0.18.1] - 2022-02-03\n\n### Added\n\n- Support for ACME revocation.\n- Replace hash function with an RSA SSH CA to \"rsa-sha2-256\".\n- Support Nebula provisioners.\n- Example Ansible configurations.\n- Support PKCS#11 as a decrypter, as used by SCEP.\n\n### Changed\n\n- Automatically create database directory on `step ca init`.\n- Slightly improve errors reported when a template has invalid content.\n- Error reporting in logs and to clients.\n\n### Fixed\n\n- SCEP renewal using HTTPS on macOS.\n\n## [0.18.0] - 2021-11-17\n\n### Added\n\n- Support for multiple certificate authority contexts.\n- Support for generating extractable keys and certificates on a pkcs#11 module.\n\n### Changed\n\n- Support two latest versions of Go (1.16, 1.17)\n\n### Deprecated\n\n- go 1.15 support\n\n## [0.17.6] - 2021-10-20\n\n### Notes\n\n- 0.17.5 failed in CI/CD\n\n## [0.17.5] - 2021-10-20\n\n### Added\n\n- Support for Azure Key Vault as a KMS.\n- Adapt `pki` package to support key managers.\n- gocritic linter\n\n### Fixed\n\n- gocritic warnings\n\n## [0.17.4] - 2021-09-28\n\n### Fixed\n\n- Support host-only or user-only SSH CA.\n\n## [0.17.3] - 2021-09-24\n\n### Added\n\n- go 1.17 to github action test matrix\n- Support for CloudKMS RSA-PSS signers without using templates.\n- Add flags to support individual passwords for the intermediate and SSH keys.\n- Global support for group admins in the OIDC provisioner.\n\n### Changed\n\n- Using go 1.17 for binaries\n\n### Fixed\n\n- Upgrade go-jose.v2 to fix a bug in the JWK fingerprint of Ed25519 keys.\n\n### Security\n\n- Use cosign to sign and upload signatures for multi-arch Docker container.\n- Add debian checksum\n\n## [0.17.2] - 2021-08-30\n\n### Added\n\n- Additional way to distinguish Azure IID and Azure OIDC tokens.\n\n### Security\n\n- Sign over all goreleaser github artifacts using cosign\n\n## [0.17.1] - 2021-08-26\n\n## [0.17.0] - 2021-08-25\n\n### Added\n\n- Add support for Linked CAs using protocol buffers and gRPC\n- `step-ca init` adds support for\n  - configuring a StepCAS RA\n  - configuring a Linked CA\n  - congifuring a `step-ca` using Helm\n\n### Changed\n\n- Update badger driver to use v2 by default\n- Update TLS cipher suites to include 1.3\n\n### Security\n\n- Fix key version when SHA512WithRSA is used. There was a typo creating RSA keys with SHA256 digests instead of SHA512.\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing to `step certificates`\n\nWe welcome contributions to `step certificates` of any kind including\ndocumentation, themes, organization, tutorials, blog posts, bug reports,\nissues, feature requests, feature implementations, pull requests, helping\nto manage issues, etc.\n\n## Table of Contents\n\n- [Contributing to `step certificates`](#contributing-to-step-certificates)\n  - [Table of Contents](#table-of-contents)\n  - [Building From Source](#building-from-source)\n    - [Build a standard `step-ca`](#build-a-standard-step-ca)\n    - [Build `step-ca` using CGO](#build-step-ca-using-cgo)\n      - [The CGO build enables PKCS #11 and YubiKey PIV support](#the-cgo-build-enables-pkcs-11-and-yubikey-piv-support)\n      - [1. Install PCSC support](#1-install-pcsc-support)\n      - [2. Build `step-ca`](#2-build-step-ca)\n  - [Asking Support Questions](#asking-support-questions)\n  - [Reporting Issues](#reporting-issues)\n  - [Code Contribution](#code-contribution)\n  - [Submitting Patches](#submitting-patches)\n    - [Code Contribution Guidelines](#code-contribution-guidelines)\n    - [Git Commit Message Guidelines](#git-commit-message-guidelines)\n\n## Building From Source\n\nClone this repository to get a bleeding-edge build, \nor download the source archive for [the latest stable release](https://github.com/smallstep/certificates/releases/latest).\n\n### Build a standard `step-ca`\n\nThe only prerequisites are [`go`](https://golang.org/) and make.\n\nTo build from source:\n\n    make bootstrap && make\n\nFind your binaries in `bin/`.\n\n### Build `step-ca` using CGO\n\n#### The CGO build enables PKCS #11 and YubiKey PIV support\n\nTo build the CGO version of `step-ca`, you will need [`go`](https://golang.org/), make, and a C compiler.\n\nYou'll also need PCSC support on your operating system, as required by the `go-piv` module.\nOn Linux, the [`libpcsclite-dev`](https://pcsclite.apdu.fr/) package provides PCSC support.\nOn macOS and Windows, PCSC support is built into the OS.\n\n#### 1. Install PCSC support\n\nOn Debian-based distributions, run:\n\n```shell\nsudo apt-get install libpcsclite-dev\n```\n\nOn Fedora:\n\n```shell\nsudo yum install pcsc-lite-devel\n```\n\nOn CentOS:\n\n```\nsudo yum install 'dnf-command(config-manager)'\nsudo yum config-manager --set-enabled PowerTools\nsudo yum install pcsc-lite-devel\n```\n\n#### 2. Build `step-ca`\n\nTo build `step-ca`, clone this repository and run the following:\n\n```shell\nmake bootstrap && make build GO_ENVS=\"CGO_ENABLED=1\"\n```\n\nWhen the build is complete, you will find binaries in `bin/`.\n\n## Asking Support Questions\n\nFeel free to post a question on our [GitHub Discussions](https://github.com/smallstep/certificates/discussions) page, or find us on [Discord](https://bit.ly/step-discord).\n\n## Reporting Issues\n\nIf you believe you have found a defect in `step certificates` or its\ndocumentation, use the GitHub [issue\ntracker](https://github.com/smallstep/certificates/issues) to report the\nproblem. When reporting the issue, please provide the version of `step\ncertificates` in use (`step-ca version`) and your operating system.\n\n## Code Contribution\n\n`step certificates` aims to become a fully featured online Certificate\nAuthority. We encourage all contributions that meet the following criteria:\n\n* fit naturally into a Certificate Authority.\n* strive not to break existing functionality.\n* close or update an open [`step certificates`\nissue](https://github.com/smallstep/certificates/issues)\n\n**Bug fixes are, of course, always welcome.**\n\n## Submitting Patches\n\n`step certificates` welcomes all contributors and contributions. If you are\ninterested in helping with the project, please reach out to us or, better yet,\nsubmit a PR :).\n\n### Code Contribution Guidelines\n\nBecause we want to create the best possible product for our users and the best\ncontribution experience for our developers, we have a set of guidelines which\nensure that all contributions are acceptable. The guidelines are not intended\nas a filter or barrier to participation. If you are unfamiliar with the\ncontribution process, the Smallstep team will guide you in order to get your\ncontribution in accordance with the guidelines.\n\nTo make the contribution process as seamless as possible, we ask for the following:\n\n* Go ahead and fork the project and make your changes. We encourage pull\nrequests to allow for review and discussion of code changes.\n* When you’re ready to create a pull request, be sure to:\n    * Sign the [CLA](https://cla-assistant.io/smallstep/certificates).\n    * Have test cases for the new code. If you have questions about how to do\n    this, please ask in your pull request.\n    * Run `go fmt`.\n    * Add documentation if you are adding new features or changing\n    functionality.\n    * Squash your commits into a single commit. `git rebase -i`. It’s okay to\n    force update your pull request with `git push -f`.\n    * Follow the **Git Commit Message Guidelines** below.\n\n### Git Commit Message Guidelines\n\nThis [blog article](http://chris.beams.io/posts/git-commit/) is a good resource\nfor learning how to write good commit messages, the most important part being\nthat each commit message should have a title/subject in imperative mood\nstarting with a capital letter and no trailing period: *\"Return error on wrong\nuse of the Paginator\"*, **NOT** *\"returning some error.\"*\n\nAlso, if your commit references one or more GitHub issues, always end your\ncommit message body with *See #1234* or *Fixes #1234*.  Replace *1234* with the\nGitHub issue ID. The last example will close the issue when the commit is\nmerged into *master*.\n\nPlease use a short and descriptive branch name, e.g. **NOT** \"patch-1\". It's\nvery common but creates a naming conflict each time when a submission is pulled\nfor a review.\n\nAn example:\n\n```text\nAdd step certificate install\n\nAdd a command line utility for installing (and uninstalling) certificates to the\nlocal system truststores. This should help developers with local development\nflows.\n\nFixes #75\n```\n"
  },
  {
    "path": "LICENSE",
    "content": "\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright 2020 Smallstep Labs, Inc.\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "Makefile",
    "content": "PKG?=github.com/smallstep/certificates/cmd/step-ca\nBINNAME?=step-ca\n\n# Set V to 1 for verbose output from the Makefile\nQ=$(if $V,,@)\nPREFIX?=\nSRC=$(shell find . -type f -name '*.go' -not -path \"./vendor/*\")\nGOOS_OVERRIDE ?=\n\nall: lint test build\n\nci: testcgo build\n\n.PHONY: all ci\n\n#########################################\n# Bootstrapping\n#########################################\n\nbootstra%:\n\t$Q curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $$(go env GOPATH)/bin latest\n\t$Q go install golang.org/x/vuln/cmd/govulncheck@latest\n\t$Q go install gotest.tools/gotestsum@latest\n\t$Q go install github.com/goreleaser/goreleaser/v2@latest\n\t$Q go install github.com/sigstore/cosign/v2/cmd/cosign@latest\n\n.PHONY: bootstra%\n\n#################################################\n# Determine the type of `push` and `version`\n#################################################\n\n# GITHUB Actions\nifdef GITHUB_REF\nVERSION ?= $(shell echo $(GITHUB_REF) | sed 's/^refs\\/tags\\///')\nNOT_RC  := $(shell echo $(VERSION) | grep -v -e -rc)\n\tifeq ($(NOT_RC),)\nPUSHTYPE := release-candidate\n\telse\nPUSHTYPE := release\n\tendif\nelse\nVERSION ?= $(shell [ -d .git ] && git describe --tags --always --dirty=\"-dev\")\n# If we are not in an active git dir then try reading the version from .VERSION.\n# .VERSION contains a slug populated by `git archive`.\nVERSION := $(or $(VERSION),$(shell ./.version.sh .VERSION))\nPUSHTYPE := branch\nendif\n\nVERSION := $(shell echo $(VERSION) | sed 's/^v//')\n\nifdef V\n$(info    GITHUB_REF is $(GITHUB_REF))\n$(info    VERSION is $(VERSION))\n$(info    PUSHTYPE is $(PUSHTYPE))\nendif\n\n#########################################\n# Build\n#########################################\n\nDATE    := $(shell date -u '+%Y-%m-%d %H:%M UTC')\nLDFLAGS := -ldflags='-w -X \"main.Version=$(VERSION)\" -X \"main.BuildTime=$(DATE)\"'\n\n# Always explicitly enable or disable cgo,\n# so that go doesn't silently fall back on\n# non-cgo when gcc is not found.\nifeq (,$(findstring CGO_ENABLED,$(GO_ENVS)))\n\tifneq ($(origin GOFLAGS),undefined)\n\t\t# This section is for backward compatibility with \n\t\t# \n\t\t# $ make build GOFLAGS=\"\"\n\t\t#\n\t\t# which is how we recommended building step-ca with cgo support\n\t\t# until June 2023.\n\t\tGO_ENVS := $(GO_ENVS) CGO_ENABLED=1\n\telse\n\t\tGO_ENVS := $(GO_ENVS) CGO_ENABLED=0\n\tendif\nendif\n\ndownload:\n\t$Q go mod download\n\nbuild: $(PREFIX)bin/$(BINNAME)\n\t@echo \"Build Complete!\"\n\n$(PREFIX)bin/$(BINNAME): download $(call rwildcard,*.go)\n\t$Q mkdir -p $(@D)\n\t$Q $(GOOS_OVERRIDE) GOFLAGS=\"$(GOFLAGS)\" $(GO_ENVS) go build -v -o $(PREFIX)bin/$(BINNAME) $(LDFLAGS) $(PKG)\n\n# Target to force a build of step-ca without running tests\nsimple: build\n\n.PHONY: download build simple\n\n#########################################\n# Go generate\n#########################################\n\ngenerate:\n\t$Q go generate ./...\n\n.PHONY: generate\n\n#########################################\n# Test\n#########################################\ntest: testdefault testtpmsimulator combinecoverage\n\ntestdefault:\n\t$Q $(GO_ENVS) gotestsum -- -coverprofile=defaultcoverage.out -short -covermode=atomic ./...\n\ntesttpmsimulator:\n\t$Q CGO_ENABLED=1 gotestsum -- -coverprofile=tpmsimulatorcoverage.out -short -covermode=atomic -tags tpmsimulator ./acme \n\ntestcgo:\n\t$Q gotestsum -- -coverprofile=coverage.out -short -covermode=atomic ./...\n\ncombinecoverage:\n\tcat defaultcoverage.out tpmsimulatorcoverage.out > coverage.out\n\n.PHONY: test testdefault testtpmsimulator testcgo combinecoverage\n\nintegrate: integration\n\nintegration: bin/$(BINNAME)\n\t$Q $(GO_ENVS) gotestsum -- -tags=integration ./integration/...\n\n.PHONY: integrate integration\n\n#########################################\n# Linting\n#########################################\n\nfmt:\n\t$Q goimports -l -w $(SRC)\n\nlint: SHELL:=/bin/bash\nlint:\n\t$Q LOG_LEVEL=error golangci-lint run --config <(curl -s https://raw.githubusercontent.com/smallstep/workflows/master/.golangci.yml) --timeout=30m\n\t$Q govulncheck ./...\n\n.PHONY: fmt lint\n\n#########################################\n# Install\n#########################################\n\nINSTALL_PREFIX?=/usr/local/\n\ninstall: $(PREFIX)bin/$(BINNAME)\n\t$Q install -D $(PREFIX)bin/$(BINNAME) $(DESTDIR)$(INSTALL_PREFIX)bin/$(BINNAME)\n\nuninstall:\n\t$Q rm -f $(DESTDIR)$(INSTALL_PREFIX)/bin/$(BINNAME)\n\n.PHONY: install uninstall\n\n#########################################\n# Clean\n#########################################\n\nclean:\nifneq ($(BINNAME),\"\")\n\t$Q rm -f bin/$(BINNAME)\nendif\n\n.PHONY: clean\n\n#########################################\n# Dev\n#########################################\n\nrun:\n\t$Q go run cmd/step-ca/main.go $(shell step path)/config/ca.json\n\n.PHONY: run\n\n"
  },
  {
    "path": "README.md",
    "content": "# step-ca\n\n[![GitHub release](https://img.shields.io/github/release/smallstep/certificates.svg)](https://github.com/smallstep/certificates/releases/latest)\n[![Go Report Card](https://goreportcard.com/badge/github.com/smallstep/certificates)](https://goreportcard.com/report/github.com/smallstep/certificates)\n[![Build Status](https://github.com/smallstep/certificates/actions/workflows/test.yml/badge.svg)](https://github.com/smallstep/certificates)\n[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)\n[![CLA assistant](https://cla-assistant.io/readme/badge/smallstep/certificates)](https://cla-assistant.io/smallstep/certificates)\n\n`step-ca` is an online certificate authority for secure, automated certificate management for DevOps.\nIt's the server counterpart to the [`step` CLI tool](https://github.com/smallstep/cli) for working with certificates and keys.\nBoth projects are maintained by [Smallstep Labs](https://smallstep.com).\n\nYou can use `step-ca` to:\n- Issue HTTPS server and client certificates that [work in browsers](https://smallstep.com/blog/step-v0-8-6-valid-HTTPS-certificates-for-dev-pre-prod.html) ([RFC5280](https://tools.ietf.org/html/rfc5280) and [CA/Browser Forum](https://cabforum.org/baseline-requirements-documents/) compliance)\n- Issue TLS certificates for DevOps: VMs, containers, APIs, database connections, Kubernetes pods...\n- Issue SSH certificates:\n  - For people, in exchange for single sign-on identity tokens\n  - For hosts, in exchange for cloud instance identity documents\n- Easily automate certificate management:\n  - It's an [ACME server](https://smallstep.com/docs/step-ca/acme-basics/) that supports all [popular ACME challenge types](https://smallstep.com/docs/step-ca/acme-basics/#acme-challenge-types)\n  - It comes with a [Go wrapper](./examples#user-content-basic-client-usage)\n  - ... and there's a [command-line client](https://github.com/smallstep/cli) you can use in scripts!\n\n---\n\n### Comparison with Smallstep's commercial product\n\n`step-ca` is optimized for a two-tier PKI serving common DevOps use cases.\n\nAs you design your PKI, if you need any of the following, [consider our commerical CA](http://smallstep.com):\n- Multiple certificate authorities\n- Active revocation (CRL, OCSP)\n- Turnkey high-volume, high availability CA\n- An API for seamless IaC management of your PKI\n- Integrated support for SCEP & NDES, for migrating from legacy Active Directory Certificate Services deployments\n- Device identity — cross-platform device inventory and attestation using Secure Enclave & TPM 2.0\n- Highly automated PKI — managed certificate renewal, monitoring, TPM-based attested enrollment\n- Seamless client deployments of EAP-TLS Wi-Fi, VPN, SSH, and browser certificates\n- Jamf, Intune, or other MDM for root distribution and client enrollment\n- Web Admin UI — history, issuance, and metrics\n- ACME External Account Binding (EAB)\n- Deep integration with an identity provider\n- Fine-grained, role-based access control\n- FIPS-compliant software\n- HSM-bound private keys\n\nSee our [full feature comparison](https://smallstep.com/step-ca-vs-smallstep-certificate-manager/) for more.\n\nYou can [start a free trial](https://smallstep.com/signup) or [set up a call with us](https://go.smallstep.com/request-demo) to learn more.\n\n---\n\n**Questions? Find us in [Discussions](https://github.com/smallstep/certificates/discussions) or [Join our Discord](https://u.step.sm/discord).**\n\n[Website](https://smallstep.com/certificates) |\n[Documentation](https://smallstep.com/docs/step-ca) |\n[Installation](https://smallstep.com/docs/step-ca/installation) |\n[Contributor's Guide](./CONTRIBUTING.md)\n\n## Features\n\n### 🦾 A fast, stable, flexible private CA\n\nSetting up a *public key infrastructure* (PKI) is out of reach for many small teams. `step-ca` makes it easier.\n\n- Choose key types (RSA, ECDSA, EdDSA) and lifetimes to suit your needs\n- [Short-lived certificates](https://smallstep.com/blog/passive-revocation.html) with automated enrollment, renewal, and passive revocation\n- Can operate as [an online intermediate CA for an existing root CA](https://smallstep.com/docs/tutorials/intermediate-ca-new-ca)\n- [Badger, BoltDB, Postgres, and MySQL database backends](https://smallstep.com/docs/step-ca/configuration#databases)\n\n### ⚙️ Many ways to automate\n\nThere are several ways to authorize a request with the CA and establish a chain of trust that suits your flow.\n\nYou can issue certificates in exchange for:\n- [ACME challenge responses](#your-own-private-acme-server) from any ACMEv2 client\n- [OAuth OIDC single sign-on tokens](https://smallstep.com/blog/easily-curl-services-secured-by-https-tls.html), eg:\n  - ID tokens from Okta, GSuite, Azure AD, Auth0.\n  - ID tokens from an OAuth OIDC service that you host, like [Keycloak](https://www.keycloak.org/) or [Dex](https://github.com/dexidp/dex)\n- [Cloud instance identity documents](https://smallstep.com/blog/embarrassingly-easy-certificates-on-aws-azure-gcp/), for VMs on AWS, GCP, and Azure\n- [Single-use, short-lived JWK tokens](https://smallstep.com/docs/step-ca/provisioners#jwk) issued by your CD tool — Puppet, Chef, Ansible, Terraform, etc.\n- A trusted X.509 certificate (X5C provisioner)\n- A host certificate from your Nebula network\n- A SCEP challenge (SCEP provisioner)\n- An SSH host certificates needing renewal (the SSHPOP provisioner)\n- Learn more in our [provisioner documentation](https://smallstep.com/docs/step-ca/provisioners)\n\n### 🏔 Your own private ACME server\n\nACME is the protocol used by Let's Encrypt to automate the issuance of HTTPS certificates. It's _super easy_ to issue certificates to any ACMEv2 ([RFC8555](https://tools.ietf.org/html/rfc8555)) client.\n\n- [Use ACME in development & pre-production](https://smallstep.com/blog/private-acme-server/#local-development--pre-production)\n- Supports the most popular [ACME challenge types](https://letsencrypt.org/docs/challenge-types/):\n  - For `http-01`, place a token at a well-known URL to prove that you control the web server\n  - For `dns-01`, add a `TXT` record to prove that you control the DNS record set\n  - For `tls-alpn-01`, respond to the challenge at the TLS layer ([as Caddy does](https://caddy.community/t/caddy-supports-the-acme-tls-alpn-challenge/4860)) to prove that you control the web server\n\n- Works with any ACME client. We've written examples for:\n  - [certbot](https://smallstep.com/docs/tutorials/acme-protocol-acme-clients#certbot)\n  - [acme.sh](https://smallstep.com/docs/tutorials/acme-protocol-acme-clients#acmesh)\n  - [win-acme](https://smallstep.com/docs/tutorials/acme-protocol-acme-clients#win-acme)\n  - [Caddy](https://smallstep.com/docs/tutorials/acme-protocol-acme-clients#caddy-v2)\n  - [Traefik](https://smallstep.com/docs/tutorials/acme-protocol-acme-clients#traefik)\n  - [Apache](https://smallstep.com/docs/tutorials/acme-protocol-acme-clients#apache)\n  - [nginx](https://smallstep.com/docs/tutorials/acme-protocol-acme-clients#nginx)\n- Get certificates programmatically using ACME, using these libraries:\n  - [`lego`](https://github.com/go-acme/lego) for Golang ([example usage](https://smallstep.com/docs/tutorials/acme-protocol-acme-clients#golang))\n  - certbot's [`acme` module](https://github.com/certbot/certbot/tree/master/acme) for Python ([example usage](https://smallstep.com/docs/tutorials/acme-protocol-acme-clients#python))\n  - [`acme-client`](https://github.com/publishlab/node-acme-client) for Node.js ([example usage](https://smallstep.com/docs/tutorials/acme-protocol-acme-clients#node))\n- Our own [`step` CLI tool](https://github.com/smallstep/cli) is also an ACME client!\n- See our [ACME tutorial](https://smallstep.com/docs/tutorials/acme-challenge) for more\n\n### 👩🏽‍💻 An online SSH Certificate Authority\n\n- Delegate SSH authentication to `step-ca` by using [SSH certificates](https://smallstep.com/blog/use-ssh-certificates/) instead of public keys and `authorized_keys` files\n- For user certificates, [connect SSH to your single sign-on provider](https://smallstep.com/blog/diy-single-sign-on-for-ssh/), to improve security with short-lived certificates and MFA (or other security policies) via any OAuth OIDC provider.\n- For host certificates, improve security, [eliminate TOFU warnings](https://smallstep.com/blog/use-ssh-certificates/), and set up automated host certificate renewal.\n\n### 🤓 A general purpose PKI tool, via [`step` CLI](https://github.com/smallstep/cli) [integration](https://smallstep.com/docs/step-cli/reference/ca/)\n\n- Generate key pairs where they're needed so private keys are never transmitted across the network\n- [Authenticate and obtain a certificate](https://smallstep.com/docs/step-cli/reference/ca/certificate/) using any provisioner supported by `step-ca`\n- Securely [distribute root certificates](https://smallstep.com/docs/step-cli/reference/ca/root/) and [bootstrap](https://smallstep.com/docs/step-cli/reference/ca/bootstrap/) PKI relying parties\n- [Renew](https://smallstep.com/docs/step-cli/reference/ca/renew/) and [revoke](https://smallstep.com/docs/step-cli/reference/ca/revoke/) certificates issued by `step-ca`\n- [Install root certificates](https://smallstep.com/docs/step-cli/reference/certificate/install/) on your machine and browsers, so your CA is trusted\n- [Inspect](https://smallstep.com/docs/step-cli/reference/certificate/inspect/) and [lint](https://smallstep.com/docs/step-cli/reference/certificate/lint/) certificates\n\n## Installation\n\nSee our installation docs [here](https://smallstep.com/docs/step-ca/installation).\n\n## Documentation\n\n* [Official documentation](https://smallstep.com/docs/step-ca) is on smallstep.com\n* The `step` command reference is available via `step help`,\n[on smallstep.com](https://smallstep.com/docs/step-cli/reference/),\nor by running `step help --http=:8080` from the command line\nand visiting http://localhost:8080.\n\n## Feedback?\n\n* Tell us what you like and don't like about managing your PKI - we're eager to help solve problems in this space. [Join our Discord](https://u.step.sm/discord) or [GitHub Discussions](https://github.com/smallstep/certificates/discussions)\n* Tell us about a feature you'd like to see! [Request a Feature](https://github.com/smallstep/certificates/issues/new?assignees=&labels=enhancement%2C+needs+triage&template=enhancement.md&title=)\n"
  },
  {
    "path": "SECURITY.md",
    "content": "We appreciate any effort to discover and disclose security vulnerabilities responsibly.\n\nIf you would like to report a vulnerability in one of our projects, or have security concerns regarding Smallstep software, please email security@smallstep.com.\n\nIn order for us to best respond to your report, please include any of the following:\n * Steps to reproduce or proof-of-concept\n * Any relevant tools, including versions used\n * Tool output\n"
  },
  {
    "path": "acme/account.go",
    "content": "package acme\n\nimport (\n\t\"crypto\"\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"time\"\n\n\t\"go.step.sm/crypto/jose\"\n\n\t\"github.com/smallstep/certificates/authority/policy\"\n)\n\n// Account is a subset of the internal account type containing only those\n// attributes required for responses in the ACME protocol.\ntype Account struct {\n\tID                     string           `json:\"-\"`\n\tKey                    *jose.JSONWebKey `json:\"-\"`\n\tContact                []string         `json:\"contact,omitempty\"`\n\tStatus                 Status           `json:\"status\"`\n\tOrdersURL              string           `json:\"orders\"`\n\tExternalAccountBinding interface{}      `json:\"externalAccountBinding,omitempty\"`\n\tLocationPrefix         string           `json:\"-\"`\n\tProvisionerID          string           `json:\"-\"`\n\tProvisionerName        string           `json:\"-\"`\n}\n\n// GetLocation returns the URL location of the given account.\nfunc (a *Account) GetLocation() string {\n\tif a.LocationPrefix == \"\" {\n\t\treturn \"\"\n\t}\n\treturn a.LocationPrefix + a.ID\n}\n\n// ToLog enables response logging.\nfunc (a *Account) ToLog() (interface{}, error) {\n\tb, err := json.Marshal(a)\n\tif err != nil {\n\t\treturn nil, WrapErrorISE(err, \"error marshaling account for logging\")\n\t}\n\treturn string(b), nil\n}\n\n// IsValid returns true if the Account is valid.\nfunc (a *Account) IsValid() bool {\n\treturn a.Status == StatusValid\n}\n\n// KeyToID converts a JWK to a thumbprint.\nfunc KeyToID(jwk *jose.JSONWebKey) (string, error) {\n\tkid, err := jwk.Thumbprint(crypto.SHA256)\n\tif err != nil {\n\t\treturn \"\", WrapErrorISE(err, \"error generating jwk thumbprint\")\n\t}\n\treturn base64.RawURLEncoding.EncodeToString(kid), nil\n}\n\n// PolicyNames contains ACME account level policy names\ntype PolicyNames struct {\n\tDNSNames []string `json:\"dns\"`\n\tIPRanges []string `json:\"ips\"`\n}\n\n// X509Policy contains ACME account level X.509 policy\ntype X509Policy struct {\n\tAllowed            PolicyNames `json:\"allow\"`\n\tDenied             PolicyNames `json:\"deny\"`\n\tAllowWildcardNames bool        `json:\"allowWildcardNames\"`\n}\n\n// Policy is an ACME Account level policy\ntype Policy struct {\n\tX509 X509Policy `json:\"x509\"`\n}\n\nfunc (p *Policy) GetAllowedNameOptions() *policy.X509NameOptions {\n\tif p == nil {\n\t\treturn nil\n\t}\n\treturn &policy.X509NameOptions{\n\t\tDNSDomains: p.X509.Allowed.DNSNames,\n\t\tIPRanges:   p.X509.Allowed.IPRanges,\n\t}\n}\n\nfunc (p *Policy) GetDeniedNameOptions() *policy.X509NameOptions {\n\tif p == nil {\n\t\treturn nil\n\t}\n\treturn &policy.X509NameOptions{\n\t\tDNSDomains: p.X509.Denied.DNSNames,\n\t\tIPRanges:   p.X509.Denied.IPRanges,\n\t}\n}\n\n// AreWildcardNamesAllowed returns if wildcard names\n// like *.example.com are allowed to be signed.\n// Defaults to false.\nfunc (p *Policy) AreWildcardNamesAllowed() bool {\n\tif p == nil {\n\t\treturn false\n\t}\n\treturn p.X509.AllowWildcardNames\n}\n\n// ExternalAccountKey is an ACME External Account Binding key.\ntype ExternalAccountKey struct {\n\tID            string    `json:\"id\"`\n\tProvisionerID string    `json:\"provisionerID\"`\n\tReference     string    `json:\"reference\"`\n\tAccountID     string    `json:\"-\"`\n\tHmacKey       []byte    `json:\"-\"`\n\tCreatedAt     time.Time `json:\"createdAt\"`\n\tBoundAt       time.Time `json:\"boundAt,omitempty\"`\n\tPolicy        *Policy   `json:\"policy,omitempty\"`\n}\n\n// AlreadyBound returns whether this EAK is already bound to\n// an ACME Account or not.\nfunc (eak *ExternalAccountKey) AlreadyBound() bool {\n\treturn !eak.BoundAt.IsZero()\n}\n\n// BindTo binds the EAK to an Account.\n// It returns an error if it's already bound.\nfunc (eak *ExternalAccountKey) BindTo(account *Account) error {\n\tif eak.AlreadyBound() {\n\t\treturn NewError(ErrorUnauthorizedType, \"external account binding key with id '%s' was already bound to account '%s' on %s\", eak.ID, eak.AccountID, eak.BoundAt)\n\t}\n\teak.AccountID = account.ID\n\teak.BoundAt = time.Now()\n\teak.HmacKey = []byte{} // clearing the key bytes; can only be used once\n\treturn nil\n}\n"
  },
  {
    "path": "acme/account_test.go",
    "content": "package acme\n\nimport (\n\t\"crypto\"\n\t\"encoding/base64\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n\t\"go.step.sm/crypto/jose\"\n\n\t\"github.com/smallstep/assert\"\n)\n\nfunc TestKeyToID(t *testing.T) {\n\ttype test struct {\n\t\tjwk *jose.JSONWebKey\n\t\texp string\n\t\terr *Error\n\t}\n\ttests := map[string]func(t *testing.T) test{\n\t\t\"fail/error-generating-thumbprint\": func(t *testing.T) test {\n\t\t\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\tassert.FatalError(t, err)\n\t\t\tjwk.Key = \"foo\"\n\t\t\treturn test{\n\t\t\t\tjwk: jwk,\n\t\t\t\terr: NewErrorISE(\"error generating jwk thumbprint: go-jose/go-jose: unknown key type 'string'\"),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\tassert.FatalError(t, err)\n\n\t\t\tkid, err := jwk.Thumbprint(crypto.SHA256)\n\t\t\tassert.FatalError(t, err)\n\n\t\t\treturn test{\n\t\t\t\tjwk: jwk,\n\t\t\t\texp: base64.RawURLEncoding.EncodeToString(kid),\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ttc := run(t)\n\t\t\tif id, err := KeyToID(tc.jwk); err != nil {\n\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\tvar k *Error\n\t\t\t\t\tif errors.As(err, &k) {\n\t\t\t\t\t\tassert.Equals(t, k.Type, tc.err.Type)\n\t\t\t\t\t\tassert.Equals(t, k.Detail, tc.err.Detail)\n\t\t\t\t\t\tassert.Equals(t, k.Status, tc.err.Status)\n\t\t\t\t\t\tassert.Equals(t, k.Err.Error(), tc.err.Err.Error())\n\t\t\t\t\t\tassert.Equals(t, k.Detail, tc.err.Detail)\n\t\t\t\t\t} else {\n\t\t\t\t\t\tassert.FatalError(t, errors.New(\"unexpected error type\"))\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif assert.Nil(t, tc.err) {\n\t\t\t\t\tassert.Equals(t, id, tc.exp)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestAccount_GetLocation(t *testing.T) {\n\tlocationPrefix := \"https://test.ca.smallstep.com/acme/foo/account/\"\n\ttype test struct {\n\t\tacc *Account\n\t\texp string\n\t}\n\ttests := map[string]test{\n\t\t\"empty\":     {acc: &Account{LocationPrefix: \"\"}, exp: \"\"},\n\t\t\"not-empty\": {acc: &Account{ID: \"bar\", LocationPrefix: locationPrefix}, exp: locationPrefix + \"bar\"},\n\t}\n\tfor name, tc := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tassert.Equals(t, tc.acc.GetLocation(), tc.exp)\n\t\t})\n\t}\n}\n\nfunc TestAccount_IsValid(t *testing.T) {\n\ttype test struct {\n\t\tacc *Account\n\t\texp bool\n\t}\n\ttests := map[string]test{\n\t\t\"valid\":   {acc: &Account{Status: StatusValid}, exp: true},\n\t\t\"invalid\": {acc: &Account{Status: StatusInvalid}, exp: false},\n\t}\n\tfor name, tc := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tassert.Equals(t, tc.acc.IsValid(), tc.exp)\n\t\t})\n\t}\n}\n\nfunc TestExternalAccountKey_BindTo(t *testing.T) {\n\tboundAt := time.Now()\n\ttests := []struct {\n\t\tname string\n\t\teak  *ExternalAccountKey\n\t\tacct *Account\n\t\terr  *Error\n\t}{\n\t\t{\n\t\t\tname: \"ok\",\n\t\t\teak: &ExternalAccountKey{\n\t\t\t\tID:            \"eakID\",\n\t\t\t\tProvisionerID: \"provID\",\n\t\t\t\tReference:     \"ref\",\n\t\t\t\tHmacKey:       []byte{1, 3, 3, 7},\n\t\t\t},\n\t\t\tacct: &Account{\n\t\t\t\tID: \"accountID\",\n\t\t\t},\n\t\t\terr: nil,\n\t\t},\n\t\t{\n\t\t\tname: \"fail/already-bound\",\n\t\t\teak: &ExternalAccountKey{\n\t\t\t\tID:            \"eakID\",\n\t\t\t\tProvisionerID: \"provID\",\n\t\t\t\tReference:     \"ref\",\n\t\t\t\tHmacKey:       []byte{1, 3, 3, 7},\n\t\t\t\tAccountID:     \"someAccountID\",\n\t\t\t\tBoundAt:       boundAt,\n\t\t\t},\n\t\t\tacct: &Account{\n\t\t\t\tID: \"accountID\",\n\t\t\t},\n\t\t\terr: NewError(ErrorUnauthorizedType, \"external account binding key with id '%s' was already bound to account '%s' on %s\", \"eakID\", \"someAccountID\", boundAt),\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\teak := tt.eak\n\t\t\tacct := tt.acct\n\t\t\terr := eak.BindTo(acct)\n\t\t\twantErr := tt.err != nil\n\t\t\tgotErr := err != nil\n\t\t\tif wantErr != gotErr {\n\t\t\t\tt.Errorf(\"ExternalAccountKey.BindTo() error = %v, wantErr %v\", err, tt.err)\n\t\t\t}\n\t\t\tif wantErr {\n\t\t\t\tassert.NotNil(t, err)\n\t\t\t\tvar ae *Error\n\t\t\t\tif assert.True(t, errors.As(err, &ae)) {\n\t\t\t\t\tassert.Equals(t, ae.Type, tt.err.Type)\n\t\t\t\t\tassert.Equals(t, ae.Detail, tt.err.Detail)\n\t\t\t\t\tassert.Equals(t, ae.Subproblems, tt.err.Subproblems)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tassert.Equals(t, eak.AccountID, acct.ID)\n\t\t\t\tassert.Equals(t, eak.HmacKey, []byte{})\n\t\t\t\tassert.NotNil(t, eak.BoundAt)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "acme/api/account.go",
    "content": "package api\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"net/http\"\n\n\t\"github.com/go-chi/chi/v5\"\n\n\t\"github.com/smallstep/certificates/acme\"\n\t\"github.com/smallstep/certificates/api/render\"\n\t\"github.com/smallstep/certificates/logging\"\n)\n\n// NewAccountRequest represents the payload for a new account request.\ntype NewAccountRequest struct {\n\tContact                []string                `json:\"contact\"`\n\tOnlyReturnExisting     bool                    `json:\"onlyReturnExisting\"`\n\tTermsOfServiceAgreed   bool                    `json:\"termsOfServiceAgreed\"`\n\tExternalAccountBinding *ExternalAccountBinding `json:\"externalAccountBinding,omitempty\"`\n}\n\nfunc validateContacts(cs []string) error {\n\tfor _, c := range cs {\n\t\tif c == \"\" {\n\t\t\treturn acme.NewError(acme.ErrorMalformedType, \"contact cannot be empty string\")\n\t\t}\n\t}\n\treturn nil\n}\n\n// Validate validates a new-account request body.\nfunc (n *NewAccountRequest) Validate() error {\n\tif n.OnlyReturnExisting && len(n.Contact) > 0 {\n\t\treturn acme.NewError(acme.ErrorMalformedType, \"incompatible input; onlyReturnExisting must be alone\")\n\t}\n\treturn validateContacts(n.Contact)\n}\n\n// UpdateAccountRequest represents an update-account request.\ntype UpdateAccountRequest struct {\n\tContact []string    `json:\"contact\"`\n\tStatus  acme.Status `json:\"status\"`\n}\n\n// Validate validates a update-account request body.\nfunc (u *UpdateAccountRequest) Validate() error {\n\tswitch {\n\tcase len(u.Status) > 0 && len(u.Contact) > 0:\n\t\treturn acme.NewError(acme.ErrorMalformedType, \"incompatible input; contact and \"+\n\t\t\t\"status updates are mutually exclusive\")\n\tcase len(u.Contact) > 0:\n\t\tif err := validateContacts(u.Contact); err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn nil\n\tcase len(u.Status) > 0:\n\t\tif u.Status != acme.StatusDeactivated {\n\t\t\treturn acme.NewError(acme.ErrorMalformedType, \"cannot update account \"+\n\t\t\t\t\"status to %s, only deactivated\", u.Status)\n\t\t}\n\t\treturn nil\n\tdefault:\n\t\t// According to the ACME spec (https://tools.ietf.org/html/rfc8555#section-7.3.2)\n\t\t// accountUpdate should ignore any fields not recognized by the server.\n\t\treturn nil\n\t}\n}\n\n// getAccountLocationPath returns the current account URL location.\n// Returned location will be of the form: https://<ca-url>/acme/<provisioner>/account/<accID>\nfunc getAccountLocationPath(ctx context.Context, linker acme.Linker, accID string) string {\n\treturn linker.GetLink(ctx, acme.AccountLinkType, accID)\n}\n\n// NewAccount is the handler resource for creating new ACME accounts.\nfunc NewAccount(w http.ResponseWriter, r *http.Request) {\n\tctx := r.Context()\n\tdb := acme.MustDatabaseFromContext(ctx)\n\tlinker := acme.MustLinkerFromContext(ctx)\n\n\tpayload, err := payloadFromContext(ctx)\n\tif err != nil {\n\t\trender.Error(w, r, err)\n\t\treturn\n\t}\n\tvar nar NewAccountRequest\n\tif err := json.Unmarshal(payload.value, &nar); err != nil {\n\t\trender.Error(w, r, acme.WrapError(acme.ErrorMalformedType, err,\n\t\t\t\"failed to unmarshal new-account request payload\"))\n\t\treturn\n\t}\n\tif err := nar.Validate(); err != nil {\n\t\trender.Error(w, r, err)\n\t\treturn\n\t}\n\n\tprov, err := acmeProvisionerFromContext(ctx)\n\tif err != nil {\n\t\trender.Error(w, r, err)\n\t\treturn\n\t}\n\n\thttpStatus := http.StatusCreated\n\tacc, err := accountFromContext(ctx)\n\tif err != nil {\n\t\tvar acmeErr *acme.Error\n\t\tif !errors.As(err, &acmeErr) || acmeErr.Status != http.StatusBadRequest {\n\t\t\t// Something went wrong ...\n\t\t\trender.Error(w, r, err)\n\t\t\treturn\n\t\t}\n\n\t\t// Account does not exist //\n\t\tif nar.OnlyReturnExisting {\n\t\t\trender.Error(w, r, acme.NewError(acme.ErrorAccountDoesNotExistType,\n\t\t\t\t\"account does not exist\"))\n\t\t\treturn\n\t\t}\n\n\t\tjwk, err := jwkFromContext(ctx)\n\t\tif err != nil {\n\t\t\trender.Error(w, r, err)\n\t\t\treturn\n\t\t}\n\n\t\teak, err := validateExternalAccountBinding(ctx, &nar)\n\t\tif err != nil {\n\t\t\trender.Error(w, r, err)\n\t\t\treturn\n\t\t}\n\n\t\tacc = &acme.Account{\n\t\t\tKey:             jwk,\n\t\t\tContact:         nar.Contact,\n\t\t\tStatus:          acme.StatusValid,\n\t\t\tLocationPrefix:  getAccountLocationPath(ctx, linker, \"\"),\n\t\t\tProvisionerID:   prov.ID,\n\t\t\tProvisionerName: prov.Name,\n\t\t}\n\t\tif err := db.CreateAccount(ctx, acc); err != nil {\n\t\t\trender.Error(w, r, acme.WrapErrorISE(err, \"error creating account\"))\n\t\t\treturn\n\t\t}\n\n\t\tif eak != nil { // means that we have a (valid) External Account Binding key that should be bound, updated and sent in the response\n\t\t\tif err := eak.BindTo(acc); err != nil {\n\t\t\t\trender.Error(w, r, err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif err := db.UpdateExternalAccountKey(ctx, prov.ID, eak); err != nil {\n\t\t\t\trender.Error(w, r, acme.WrapErrorISE(err, \"error updating external account binding key\"))\n\t\t\t\treturn\n\t\t\t}\n\t\t\tacc.ExternalAccountBinding = nar.ExternalAccountBinding\n\t\t}\n\t} else {\n\t\t// Account exists\n\t\thttpStatus = http.StatusOK\n\t}\n\n\tlinker.LinkAccount(ctx, acc)\n\n\tw.Header().Set(\"Location\", getAccountLocationPath(ctx, linker, acc.ID))\n\trender.JSONStatus(w, r, acc, httpStatus)\n}\n\n// GetOrUpdateAccount is the api for updating an ACME account.\nfunc GetOrUpdateAccount(w http.ResponseWriter, r *http.Request) {\n\tctx := r.Context()\n\tdb := acme.MustDatabaseFromContext(ctx)\n\tlinker := acme.MustLinkerFromContext(ctx)\n\n\tacc, err := accountFromContext(ctx)\n\tif err != nil {\n\t\trender.Error(w, r, err)\n\t\treturn\n\t}\n\tpayload, err := payloadFromContext(ctx)\n\tif err != nil {\n\t\trender.Error(w, r, err)\n\t\treturn\n\t}\n\n\t// If PostAsGet just respond with the account, otherwise process like a\n\t// normal Post request.\n\tif !payload.isPostAsGet {\n\t\tvar uar UpdateAccountRequest\n\t\tif err := json.Unmarshal(payload.value, &uar); err != nil {\n\t\t\trender.Error(w, r, acme.WrapError(acme.ErrorMalformedType, err,\n\t\t\t\t\"failed to unmarshal new-account request payload\"))\n\t\t\treturn\n\t\t}\n\t\tif err := uar.Validate(); err != nil {\n\t\t\trender.Error(w, r, err)\n\t\t\treturn\n\t\t}\n\t\tif len(uar.Status) > 0 || len(uar.Contact) > 0 {\n\t\t\tif len(uar.Status) > 0 {\n\t\t\t\tacc.Status = uar.Status\n\t\t\t} else if len(uar.Contact) > 0 {\n\t\t\t\tacc.Contact = uar.Contact\n\t\t\t}\n\n\t\t\tif err := db.UpdateAccount(ctx, acc); err != nil {\n\t\t\t\trender.Error(w, r, acme.WrapErrorISE(err, \"error updating account\"))\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}\n\n\tlinker.LinkAccount(ctx, acc)\n\n\tw.Header().Set(\"Location\", linker.GetLink(ctx, acme.AccountLinkType, acc.ID))\n\trender.JSON(w, r, acc)\n}\n\nfunc logOrdersByAccount(w http.ResponseWriter, oids []string) {\n\tif rl, ok := w.(logging.ResponseLogger); ok {\n\t\tm := map[string]interface{}{\n\t\t\t\"orders\": oids,\n\t\t}\n\t\trl.WithFields(m)\n\t}\n}\n\n// GetOrdersByAccountID ACME api for retrieving the list of order urls belonging to an account.\nfunc GetOrdersByAccountID(w http.ResponseWriter, r *http.Request) {\n\tctx := r.Context()\n\tdb := acme.MustDatabaseFromContext(ctx)\n\tlinker := acme.MustLinkerFromContext(ctx)\n\n\tacc, err := accountFromContext(ctx)\n\tif err != nil {\n\t\trender.Error(w, r, err)\n\t\treturn\n\t}\n\taccID := chi.URLParam(r, \"accID\")\n\tif acc.ID != accID {\n\t\trender.Error(w, r, acme.NewError(acme.ErrorUnauthorizedType, \"account ID '%s' does not match url param '%s'\", acc.ID, accID))\n\t\treturn\n\t}\n\n\torders, err := db.GetOrdersByAccountID(ctx, acc.ID)\n\tif err != nil {\n\t\trender.Error(w, r, err)\n\t\treturn\n\t}\n\n\tlinker.LinkOrdersByAccountID(ctx, orders)\n\n\trender.JSON(w, r, orders)\n\tlogOrdersByAccount(w, orders)\n}\n"
  },
  {
    "path": "acme/api/account_test.go",
    "content": "package api\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/x509\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-chi/chi/v5\"\n\t\"github.com/google/uuid\"\n\t\"github.com/pkg/errors\"\n\n\t\"go.step.sm/crypto/jose\"\n\n\t\"github.com/smallstep/assert\"\n\t\"github.com/smallstep/certificates/acme\"\n\t\"github.com/smallstep/certificates/authority/provisioner\"\n)\n\nvar (\n\tdefaultDisableRenewal             = false\n\tdefaultDisableSmallstepExtensions = false\n\tglobalProvisionerClaims           = provisioner.Claims{\n\t\tMinTLSDur:                  &provisioner.Duration{Duration: 5 * time.Minute},\n\t\tMaxTLSDur:                  &provisioner.Duration{Duration: 24 * time.Hour},\n\t\tDefaultTLSDur:              &provisioner.Duration{Duration: 24 * time.Hour},\n\t\tDisableRenewal:             &defaultDisableRenewal,\n\t\tDisableSmallstepExtensions: &defaultDisableSmallstepExtensions,\n\t}\n)\n\ntype fakeProvisioner struct{}\n\nfunc (*fakeProvisioner) AuthorizeOrderIdentifier(context.Context, provisioner.ACMEIdentifier) error {\n\treturn nil\n}\nfunc (*fakeProvisioner) AuthorizeSign(context.Context, string) ([]provisioner.SignOption, error) {\n\treturn nil, nil\n}\nfunc (*fakeProvisioner) IsChallengeEnabled(context.Context, provisioner.ACMEChallenge) bool {\n\treturn true\n}\nfunc (*fakeProvisioner) IsAttestationFormatEnabled(context.Context, provisioner.ACMEAttestationFormat) bool {\n\treturn true\n}\nfunc (*fakeProvisioner) GetAttestationRoots() (*x509.CertPool, bool)   { return nil, false }\nfunc (*fakeProvisioner) AuthorizeRevoke(context.Context, string) error { return nil }\nfunc (*fakeProvisioner) GetID() string                                 { return \"\" }\nfunc (*fakeProvisioner) GetName() string                               { return \"\" }\nfunc (*fakeProvisioner) DefaultTLSCertDuration() time.Duration         { return 0 }\nfunc (*fakeProvisioner) GetOptions() *provisioner.Options              { return nil }\n\nfunc newProv() acme.Provisioner {\n\t// Initialize provisioners\n\tp := &provisioner.ACME{\n\t\tType: \"ACME\",\n\t\tName: \"test@acme-<test>provisioner.com\",\n\t}\n\tif err := p.Init(provisioner.Config{Claims: globalProvisionerClaims}); err != nil {\n\t\tfmt.Printf(\"%v\", err)\n\t}\n\treturn p\n}\n\nfunc newProvWithID() acme.Provisioner {\n\t// Initialize provisioners\n\tp := &provisioner.ACME{\n\t\tID:   uuid.NewString(),\n\t\tType: \"ACME\",\n\t\tName: \"test@acme-<test>provisioner.com\",\n\t}\n\tif err := p.Init(provisioner.Config{Claims: globalProvisionerClaims}); err != nil {\n\t\tfmt.Printf(\"%v\", err)\n\t}\n\treturn p\n}\n\nfunc newProvWithOptions(options *provisioner.Options) acme.Provisioner {\n\t// Initialize provisioners\n\tp := &provisioner.ACME{\n\t\tType:    \"ACME\",\n\t\tName:    \"test@acme-<test>provisioner.com\",\n\t\tOptions: options,\n\t}\n\tif err := p.Init(provisioner.Config{Claims: globalProvisionerClaims}); err != nil {\n\t\tfmt.Printf(\"%v\", err)\n\t}\n\treturn p\n}\n\nfunc newACMEProv(t *testing.T) *provisioner.ACME {\n\tp := newProv()\n\ta, ok := p.(*provisioner.ACME)\n\tif !ok {\n\t\tt.Fatal(\"not a valid ACME provisioner\")\n\t}\n\treturn a\n}\n\nfunc newACMEProvWithOptions(t *testing.T, options *provisioner.Options) *provisioner.ACME {\n\tp := newProvWithOptions(options)\n\ta, ok := p.(*provisioner.ACME)\n\tif !ok {\n\t\tt.Fatal(\"not a valid ACME provisioner\")\n\t}\n\treturn a\n}\n\nfunc createEABJWS(jwk *jose.JSONWebKey, hmacKey []byte, keyID, u string) (*jose.JSONWebSignature, error) {\n\tsigner, err := jose.NewSigner(\n\t\tjose.SigningKey{\n\t\t\tAlgorithm: jose.SignatureAlgorithm(\"HS256\"),\n\t\t\tKey:       hmacKey,\n\t\t},\n\t\t&jose.SignerOptions{\n\t\t\tExtraHeaders: map[jose.HeaderKey]interface{}{\n\t\t\t\t\"kid\": keyID,\n\t\t\t\t\"url\": u,\n\t\t\t},\n\t\t\tEmbedJWK: false,\n\t\t},\n\t)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tjwkJSONBytes, err := jwk.Public().MarshalJSON()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tjws, err := signer.Sign(jwkJSONBytes)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\traw, err := jws.CompactSerialize()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tparsedJWS, err := jose.ParseJWS(raw)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn parsedJWS, nil\n}\n\nfunc createRawEABJWS(jwk *jose.JSONWebKey, hmacKey []byte, keyID, u string) ([]byte, error) {\n\tjws, err := createEABJWS(jwk, hmacKey, keyID, u)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\trawJWS := jws.FullSerialize()\n\treturn []byte(rawJWS), nil\n}\n\nfunc TestNewAccountRequest_Validate(t *testing.T) {\n\ttype test struct {\n\t\tnar *NewAccountRequest\n\t\terr *acme.Error\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/incompatible-input\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tnar: &NewAccountRequest{\n\t\t\t\t\tOnlyReturnExisting: true,\n\t\t\t\t\tContact:            []string{\"foo\", \"bar\"},\n\t\t\t\t},\n\t\t\t\terr: acme.NewError(acme.ErrorMalformedType, \"incompatible input; onlyReturnExisting must be alone\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/bad-contact\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tnar: &NewAccountRequest{\n\t\t\t\t\tContact: []string{\"foo\", \"\"},\n\t\t\t\t},\n\t\t\t\terr: acme.NewError(acme.ErrorMalformedType, \"contact cannot be empty string\"),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tnar: &NewAccountRequest{\n\t\t\t\t\tContact: []string{\"foo\", \"bar\"},\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"ok/onlyReturnExisting\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tnar: &NewAccountRequest{\n\t\t\t\t\tOnlyReturnExisting: true,\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\ttc := run(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tif err := tc.nar.Validate(); err != nil {\n\t\t\t\tif assert.NotNil(t, err) {\n\t\t\t\t\tvar ae *acme.Error\n\t\t\t\t\tif assert.True(t, errors.As(err, &ae)) {\n\t\t\t\t\t\tassert.HasPrefix(t, ae.Error(), tc.err.Error())\n\t\t\t\t\t\tassert.Equals(t, ae.StatusCode(), tc.err.StatusCode())\n\t\t\t\t\t\tassert.Equals(t, ae.Type, tc.err.Type)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tassert.Nil(t, tc.err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestUpdateAccountRequest_Validate(t *testing.T) {\n\ttype test struct {\n\t\tuar *UpdateAccountRequest\n\t\terr *acme.Error\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/incompatible-input\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tuar: &UpdateAccountRequest{\n\t\t\t\t\tContact: []string{\"foo\", \"bar\"},\n\t\t\t\t\tStatus:  \"foo\",\n\t\t\t\t},\n\t\t\t\terr: acme.NewError(acme.ErrorMalformedType, \"incompatible input; \"+\n\t\t\t\t\t\"contact and status updates are mutually exclusive\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/bad-contact\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tuar: &UpdateAccountRequest{\n\t\t\t\t\tContact: []string{\"foo\", \"\"},\n\t\t\t\t},\n\t\t\t\terr: acme.NewError(acme.ErrorMalformedType, \"contact cannot be empty string\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/bad-status\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tuar: &UpdateAccountRequest{\n\t\t\t\t\tStatus: \"foo\",\n\t\t\t\t},\n\t\t\t\terr: acme.NewError(acme.ErrorMalformedType, \"cannot update account \"+\n\t\t\t\t\t\"status to foo, only deactivated\"),\n\t\t\t}\n\t\t},\n\t\t\"ok/contact\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tuar: &UpdateAccountRequest{\n\t\t\t\t\tContact: []string{\"foo\", \"bar\"},\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"ok/status\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tuar: &UpdateAccountRequest{\n\t\t\t\t\tStatus: \"deactivated\",\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"ok/accept-empty\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tuar: &UpdateAccountRequest{},\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\ttc := run(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tif err := tc.uar.Validate(); err != nil {\n\t\t\t\tif assert.NotNil(t, err) {\n\t\t\t\t\tvar ae *acme.Error\n\t\t\t\t\tif assert.True(t, errors.As(err, &ae)) {\n\t\t\t\t\t\tassert.HasPrefix(t, ae.Error(), tc.err.Error())\n\t\t\t\t\t\tassert.Equals(t, ae.StatusCode(), tc.err.StatusCode())\n\t\t\t\t\t\tassert.Equals(t, ae.Type, tc.err.Type)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tassert.Nil(t, tc.err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestHandler_GetOrdersByAccountID(t *testing.T) {\n\taccID := \"account-id\"\n\n\t// Request with chi context\n\tchiCtx := chi.NewRouteContext()\n\tchiCtx.URLParams.Add(\"accID\", accID)\n\n\tprov := newProv()\n\tprovName := url.PathEscape(prov.GetName())\n\tbaseURL := &url.URL{Scheme: \"https\", Host: \"test.ca.smallstep.com\"}\n\n\tu := fmt.Sprintf(\"http://ca.smallstep.com/acme/%s/account/%s/orders\", provName, accID)\n\n\toids := []string{\"foo\", \"bar\"}\n\toidURLs := []string{\n\t\tfmt.Sprintf(\"%s/acme/%s/order/foo\", baseURL.String(), provName),\n\t\tfmt.Sprintf(\"%s/acme/%s/order/bar\", baseURL.String(), provName),\n\t}\n\n\ttype test struct {\n\t\tdb         acme.DB\n\t\tctx        context.Context\n\t\tstatusCode int\n\t\terr        *acme.Error\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/no-account\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tdb:         &acme.MockDB{},\n\t\t\t\tctx:        context.Background(),\n\t\t\t\tstatusCode: 400,\n\t\t\t\terr:        acme.NewError(acme.ErrorAccountDoesNotExistType, \"account does not exist\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/nil-account\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tdb:         &acme.MockDB{},\n\t\t\t\tctx:        context.WithValue(context.Background(), accContextKey, http.NoBody),\n\t\t\t\tstatusCode: 400,\n\t\t\t\terr:        acme.NewError(acme.ErrorAccountDoesNotExistType, \"account does not exist\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/account-id-mismatch\": func(t *testing.T) test {\n\t\t\tacc := &acme.Account{ID: \"foo\"}\n\t\t\tctx := context.WithValue(context.Background(), accContextKey, acc)\n\t\t\tctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx)\n\t\t\treturn test{\n\t\t\t\tdb:         &acme.MockDB{},\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 401,\n\t\t\t\terr:        acme.NewError(acme.ErrorUnauthorizedType, \"account ID does not match url param\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/db.GetOrdersByAccountID-error\": func(t *testing.T) test {\n\t\t\tacc := &acme.Account{ID: accID}\n\t\t\tctx := context.WithValue(context.Background(), accContextKey, acc)\n\t\t\tctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx)\n\t\t\treturn test{\n\t\t\t\tdb: &acme.MockDB{\n\t\t\t\t\tMockError: acme.NewErrorISE(\"force\"),\n\t\t\t\t},\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 500,\n\t\t\t\terr:        acme.NewErrorISE(\"force\"),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tacc := &acme.Account{ID: accID}\n\t\t\tctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)\n\t\t\tctx = acme.NewProvisionerContext(ctx, prov)\n\t\t\tctx = context.WithValue(ctx, accContextKey, acc)\n\t\t\treturn test{\n\t\t\t\tdb: &acme.MockDB{\n\t\t\t\t\tMockGetOrdersByAccountID: func(ctx context.Context, id string) ([]string, error) {\n\t\t\t\t\t\tassert.Equals(t, id, acc.ID)\n\t\t\t\t\t\treturn oids, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 200,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\ttc := run(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tctx := acme.NewContext(tc.ctx, tc.db, nil, acme.NewLinker(\"test.ca.smallstep.com\", \"acme\"), nil)\n\t\t\treq := httptest.NewRequest(\"GET\", u, http.NoBody)\n\t\t\treq = req.WithContext(ctx)\n\t\t\tw := httptest.NewRecorder()\n\t\t\tGetOrdersByAccountID(w, req)\n\t\t\tres := w.Result()\n\n\t\t\tassert.Equals(t, res.StatusCode, tc.statusCode)\n\n\t\t\tbody, err := io.ReadAll(res.Body)\n\t\t\tres.Body.Close()\n\t\t\tassert.FatalError(t, err)\n\n\t\t\tif res.StatusCode >= 400 && assert.NotNil(t, tc.err) {\n\t\t\t\tvar ae acme.Error\n\t\t\t\tassert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &ae))\n\n\t\t\t\tassert.Equals(t, ae.Type, tc.err.Type)\n\t\t\t\tassert.Equals(t, ae.Detail, tc.err.Detail)\n\t\t\t\tassert.Equals(t, ae.Subproblems, tc.err.Subproblems)\n\t\t\t\tassert.Equals(t, res.Header[\"Content-Type\"], []string{\"application/problem+json\"})\n\t\t\t} else {\n\t\t\t\texpB, err := json.Marshal(oidURLs)\n\t\t\t\tassert.FatalError(t, err)\n\t\t\t\tassert.Equals(t, bytes.TrimSpace(body), expB)\n\t\t\t\tassert.Equals(t, res.Header[\"Content-Type\"], []string{\"application/json\"})\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestHandler_NewAccount(t *testing.T) {\n\tprov := newProv()\n\tescProvName := url.PathEscape(prov.GetName())\n\tbaseURL := &url.URL{Scheme: \"https\", Host: \"test.ca.smallstep.com\"}\n\tprovID := prov.GetID()\n\n\ttype test struct {\n\t\tdb         acme.DB\n\t\tacc        *acme.Account\n\t\tctx        context.Context\n\t\tstatusCode int\n\t\terr        *acme.Error\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/no-payload\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tdb:         &acme.MockDB{},\n\t\t\t\tctx:        context.Background(),\n\t\t\t\tstatusCode: 500,\n\t\t\t\terr:        acme.NewErrorISE(\"payload expected in request context\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/nil-payload\": func(t *testing.T) test {\n\t\t\tctx := context.WithValue(context.Background(), payloadContextKey, nil)\n\t\t\treturn test{\n\t\t\t\tdb:         &acme.MockDB{},\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 500,\n\t\t\t\terr:        acme.NewErrorISE(\"payload expected in request context\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/unmarshal-payload-error\": func(t *testing.T) test {\n\t\t\tctx := context.WithValue(context.Background(), payloadContextKey, &payloadInfo{})\n\t\t\treturn test{\n\t\t\t\tdb:         &acme.MockDB{},\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 400,\n\t\t\t\terr: acme.NewError(acme.ErrorMalformedType, \"failed to \"+\n\t\t\t\t\t\"unmarshal new-account request payload: unexpected end of JSON input\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/malformed-payload-error\": func(t *testing.T) test {\n\t\t\tnar := &NewAccountRequest{\n\t\t\t\tContact: []string{\"foo\", \"\"},\n\t\t\t}\n\t\t\tb, err := json.Marshal(nar)\n\t\t\tassert.FatalError(t, err)\n\t\t\tctx := context.WithValue(context.Background(), payloadContextKey, &payloadInfo{value: b})\n\t\t\treturn test{\n\t\t\t\tdb:         &acme.MockDB{},\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 400,\n\t\t\t\terr:        acme.NewError(acme.ErrorMalformedType, \"contact cannot be empty string\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/no-existing-account\": func(t *testing.T) test {\n\t\t\tnar := &NewAccountRequest{\n\t\t\t\tOnlyReturnExisting: true,\n\t\t\t}\n\t\t\tb, err := json.Marshal(nar)\n\t\t\tassert.FatalError(t, err)\n\t\t\tctx := context.WithValue(context.Background(), payloadContextKey, &payloadInfo{value: b})\n\t\t\tctx = acme.NewProvisionerContext(ctx, prov)\n\t\t\treturn test{\n\t\t\t\tdb:         &acme.MockDB{},\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 400,\n\t\t\t\terr:        acme.NewError(acme.ErrorAccountDoesNotExistType, \"account does not exist\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/no-jwk\": func(t *testing.T) test {\n\t\t\tnar := &NewAccountRequest{\n\t\t\t\tContact: []string{\"foo\", \"bar\"},\n\t\t\t}\n\t\t\tb, err := json.Marshal(nar)\n\t\t\tassert.FatalError(t, err)\n\t\t\tctx := acme.NewProvisionerContext(context.Background(), prov)\n\t\t\tctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b})\n\t\t\treturn test{\n\t\t\t\tdb:         &acme.MockDB{},\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 500,\n\t\t\t\terr:        acme.NewErrorISE(\"jwk expected in request context\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/nil-jwk\": func(t *testing.T) test {\n\t\t\tnar := &NewAccountRequest{\n\t\t\t\tContact: []string{\"foo\", \"bar\"},\n\t\t\t}\n\t\t\tb, err := json.Marshal(nar)\n\t\t\tassert.FatalError(t, err)\n\t\t\tctx := acme.NewProvisionerContext(context.Background(), prov)\n\t\t\tctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b})\n\t\t\tctx = context.WithValue(ctx, jwkContextKey, nil)\n\t\t\treturn test{\n\t\t\t\tdb:         &acme.MockDB{},\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 500,\n\t\t\t\terr:        acme.NewErrorISE(\"jwk expected in request context\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/new-account-no-eab-provided\": func(t *testing.T) test {\n\t\t\tnar := &NewAccountRequest{\n\t\t\t\tContact:                []string{\"foo\", \"bar\"},\n\t\t\t\tExternalAccountBinding: nil,\n\t\t\t}\n\t\t\tb, err := json.Marshal(nar)\n\t\t\tassert.FatalError(t, err)\n\t\t\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\tassert.FatalError(t, err)\n\t\t\tprov := newACMEProv(t)\n\t\t\tprov.RequireEAB = true\n\t\t\tctx := context.WithValue(context.Background(), payloadContextKey, &payloadInfo{value: b})\n\t\t\tctx = context.WithValue(ctx, jwkContextKey, jwk)\n\t\t\tctx = acme.NewProvisionerContext(ctx, prov)\n\t\t\treturn test{\n\t\t\t\tdb:         &acme.MockDB{},\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 400,\n\t\t\t\terr:        acme.NewError(acme.ErrorExternalAccountRequiredType, \"no external account binding provided\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/db.CreateAccount-error\": func(t *testing.T) test {\n\t\t\tnar := &NewAccountRequest{\n\t\t\t\tContact: []string{\"foo\", \"bar\"},\n\t\t\t}\n\t\t\tb, err := json.Marshal(nar)\n\t\t\tassert.FatalError(t, err)\n\t\t\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\tassert.FatalError(t, err)\n\t\t\tctx := context.WithValue(context.Background(), payloadContextKey, &payloadInfo{value: b})\n\t\t\tctx = acme.NewProvisionerContext(ctx, prov)\n\t\t\tctx = context.WithValue(ctx, jwkContextKey, jwk)\n\t\t\treturn test{\n\t\t\t\tdb: &acme.MockDB{\n\t\t\t\t\tMockCreateAccount: func(ctx context.Context, acc *acme.Account) error {\n\t\t\t\t\t\tassert.Equals(t, acc.Contact, nar.Contact)\n\t\t\t\t\t\tassert.Equals(t, acc.Key, jwk)\n\t\t\t\t\t\treturn acme.NewErrorISE(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 500,\n\t\t\t\terr:        acme.NewErrorISE(\"force\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/acmeProvisionerFromContext\": func(t *testing.T) test {\n\t\t\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\tassert.FatalError(t, err)\n\t\t\turl := fmt.Sprintf(\"%s/acme/%s/account/new-account\", baseURL.String(), escProvName)\n\t\t\trawEABJWS, err := createRawEABJWS(jwk, []byte{1, 3, 3, 7}, \"eakID\", url)\n\t\t\tassert.FatalError(t, err)\n\t\t\teab := &ExternalAccountBinding{}\n\t\t\terr = json.Unmarshal(rawEABJWS, &eab)\n\t\t\tassert.FatalError(t, err)\n\t\t\tnar := &NewAccountRequest{\n\t\t\t\tContact:                []string{\"foo\", \"bar\"},\n\t\t\t\tExternalAccountBinding: eab,\n\t\t\t}\n\t\t\tb, err := json.Marshal(nar)\n\t\t\tassert.FatalError(t, err)\n\t\t\tctx := context.WithValue(context.Background(), payloadContextKey, &payloadInfo{value: b})\n\t\t\tctx = context.WithValue(ctx, jwkContextKey, jwk)\n\t\t\tctx = acme.NewProvisionerContext(ctx, &fakeProvisioner{})\n\t\t\treturn test{\n\t\t\t\tdb:         &acme.MockDB{},\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 500,\n\t\t\t\terr:        acme.NewError(acme.ErrorServerInternalType, \"provisioner in context is not an ACME provisioner\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/db.UpdateExternalAccountKey-error\": func(t *testing.T) test {\n\t\t\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\tassert.FatalError(t, err)\n\t\t\turl := fmt.Sprintf(\"%s/acme/%s/account/new-account\", baseURL.String(), escProvName)\n\t\t\trawEABJWS, err := createRawEABJWS(jwk, []byte{1, 3, 3, 7}, \"eakID\", url)\n\t\t\tassert.FatalError(t, err)\n\t\t\teab := &ExternalAccountBinding{}\n\t\t\terr = json.Unmarshal(rawEABJWS, &eab)\n\t\t\tassert.FatalError(t, err)\n\t\t\tnar := &NewAccountRequest{\n\t\t\t\tContact:                []string{\"foo\", \"bar\"},\n\t\t\t\tExternalAccountBinding: eab,\n\t\t\t}\n\t\t\tpayloadBytes, err := json.Marshal(nar)\n\t\t\tassert.FatalError(t, err)\n\t\t\tso := new(jose.SignerOptions)\n\t\t\tso.WithHeader(\"alg\", jose.SignatureAlgorithm(jwk.Algorithm))\n\t\t\tso.WithHeader(\"url\", url)\n\t\t\tsigner, err := jose.NewSigner(jose.SigningKey{\n\t\t\t\tAlgorithm: jose.SignatureAlgorithm(jwk.Algorithm),\n\t\t\t\tKey:       jwk.Key,\n\t\t\t}, so)\n\t\t\tassert.FatalError(t, err)\n\t\t\tjws, err := signer.Sign(payloadBytes)\n\t\t\tassert.FatalError(t, err)\n\t\t\traw, err := jws.CompactSerialize()\n\t\t\tassert.FatalError(t, err)\n\t\t\tparsedJWS, err := jose.ParseJWS(raw)\n\t\t\tassert.FatalError(t, err)\n\t\t\tprov := newACMEProv(t)\n\t\t\tprov.RequireEAB = true\n\t\t\tctx := context.WithValue(context.Background(), payloadContextKey, &payloadInfo{value: payloadBytes})\n\t\t\tctx = context.WithValue(ctx, jwkContextKey, jwk)\n\t\t\tctx = acme.NewProvisionerContext(ctx, prov)\n\t\t\tctx = context.WithValue(ctx, jwsContextKey, parsedJWS)\n\t\t\teak := &acme.ExternalAccountKey{\n\t\t\t\tID:            \"eakID\",\n\t\t\t\tProvisionerID: provID,\n\t\t\t\tReference:     \"testeak\",\n\t\t\t\tHmacKey:       []byte{1, 3, 3, 7},\n\t\t\t\tCreatedAt:     time.Now(),\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tdb: &acme.MockDB{\n\t\t\t\t\tMockCreateAccount: func(ctx context.Context, acc *acme.Account) error {\n\t\t\t\t\t\tacc.ID = \"accountID\"\n\t\t\t\t\t\tassert.Equals(t, acc.Contact, nar.Contact)\n\t\t\t\t\t\tassert.Equals(t, acc.Key, jwk)\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t\tMockGetExternalAccountKey: func(ctx context.Context, provisionerName, keyID string) (*acme.ExternalAccountKey, error) {\n\t\t\t\t\t\treturn eak, nil\n\t\t\t\t\t},\n\t\t\t\t\tMockUpdateExternalAccountKey: func(ctx context.Context, provisionerName string, eak *acme.ExternalAccountKey) error {\n\t\t\t\t\t\treturn errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tacc: &acme.Account{\n\t\t\t\t\tID:                     \"accountID\",\n\t\t\t\t\tKey:                    jwk,\n\t\t\t\t\tStatus:                 acme.StatusValid,\n\t\t\t\t\tContact:                []string{\"foo\", \"bar\"},\n\t\t\t\t\tOrdersURL:              fmt.Sprintf(\"%s/acme/%s/account/accountID/orders\", baseURL.String(), escProvName),\n\t\t\t\t\tExternalAccountBinding: eab,\n\t\t\t\t},\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 500,\n\t\t\t\terr:        acme.NewError(acme.ErrorServerInternalType, \"error updating external account binding key\"),\n\t\t\t}\n\t\t},\n\t\t\"ok/new-account\": func(t *testing.T) test {\n\t\t\tnar := &NewAccountRequest{\n\t\t\t\tContact: []string{\"foo\", \"bar\"},\n\t\t\t}\n\t\t\tb, err := json.Marshal(nar)\n\t\t\tassert.FatalError(t, err)\n\t\t\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\tassert.FatalError(t, err)\n\t\t\tctx := context.WithValue(context.Background(), payloadContextKey, &payloadInfo{value: b})\n\t\t\tctx = context.WithValue(ctx, jwkContextKey, jwk)\n\t\t\tctx = acme.NewProvisionerContext(ctx, prov)\n\t\t\treturn test{\n\t\t\t\tdb: &acme.MockDB{\n\t\t\t\t\tMockCreateAccount: func(ctx context.Context, acc *acme.Account) error {\n\t\t\t\t\t\tacc.ID = \"accountID\"\n\t\t\t\t\t\tassert.Equals(t, acc.Contact, nar.Contact)\n\t\t\t\t\t\tassert.Equals(t, acc.Key, jwk)\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tacc: &acme.Account{\n\t\t\t\t\tID:        \"accountID\",\n\t\t\t\t\tKey:       jwk,\n\t\t\t\t\tStatus:    acme.StatusValid,\n\t\t\t\t\tContact:   []string{\"foo\", \"bar\"},\n\t\t\t\t\tOrdersURL: fmt.Sprintf(\"%s/acme/%s/account/accountID/orders\", baseURL.String(), escProvName),\n\t\t\t\t},\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 201,\n\t\t\t}\n\t\t},\n\t\t\"ok/return-existing\": func(t *testing.T) test {\n\t\t\tnar := &NewAccountRequest{\n\t\t\t\tOnlyReturnExisting: true,\n\t\t\t}\n\t\t\tb, err := json.Marshal(nar)\n\t\t\tassert.FatalError(t, err)\n\t\t\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\tassert.FatalError(t, err)\n\t\t\tacc := &acme.Account{\n\t\t\t\tID:      \"accountID\",\n\t\t\t\tKey:     jwk,\n\t\t\t\tStatus:  acme.StatusValid,\n\t\t\t\tContact: []string{\"foo\", \"bar\"},\n\t\t\t}\n\t\t\tctx := acme.NewProvisionerContext(context.Background(), prov)\n\t\t\tctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b})\n\t\t\tctx = context.WithValue(ctx, accContextKey, acc)\n\t\t\treturn test{\n\t\t\t\tdb:         &acme.MockDB{},\n\t\t\t\tctx:        ctx,\n\t\t\t\tacc:        acc,\n\t\t\t\tstatusCode: 200,\n\t\t\t}\n\t\t},\n\t\t\"ok/new-account-no-eab-required\": func(t *testing.T) test {\n\t\t\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\tassert.FatalError(t, err)\n\t\t\turl := fmt.Sprintf(\"%s/acme/%s/account/new-account\", baseURL.String(), escProvName)\n\t\t\trawEABJWS, err := createRawEABJWS(jwk, []byte{1, 3, 3, 7}, \"eakID\", url)\n\t\t\tassert.FatalError(t, err)\n\t\t\teab := &ExternalAccountBinding{}\n\t\t\terr = json.Unmarshal(rawEABJWS, &eab)\n\t\t\tassert.FatalError(t, err)\n\t\t\tnar := &NewAccountRequest{\n\t\t\t\tContact:                []string{\"foo\", \"bar\"},\n\t\t\t\tExternalAccountBinding: eab,\n\t\t\t}\n\t\t\tb, err := json.Marshal(nar)\n\t\t\tassert.FatalError(t, err)\n\t\t\tprov := newACMEProv(t)\n\t\t\tprov.RequireEAB = false\n\t\t\tctx := context.WithValue(context.Background(), payloadContextKey, &payloadInfo{value: b})\n\t\t\tctx = context.WithValue(ctx, jwkContextKey, jwk)\n\t\t\tctx = acme.NewProvisionerContext(ctx, prov)\n\t\t\treturn test{\n\t\t\t\tdb: &acme.MockDB{\n\t\t\t\t\tMockCreateAccount: func(ctx context.Context, acc *acme.Account) error {\n\t\t\t\t\t\tacc.ID = \"accountID\"\n\t\t\t\t\t\tassert.Equals(t, acc.Contact, nar.Contact)\n\t\t\t\t\t\tassert.Equals(t, acc.Key, jwk)\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tacc: &acme.Account{\n\t\t\t\t\tID:        \"accountID\",\n\t\t\t\t\tKey:       jwk,\n\t\t\t\t\tStatus:    acme.StatusValid,\n\t\t\t\t\tContact:   []string{\"foo\", \"bar\"},\n\t\t\t\t\tOrdersURL: fmt.Sprintf(\"%s/acme/%s/account/accountID/orders\", baseURL.String(), escProvName),\n\t\t\t\t},\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 201,\n\t\t\t}\n\t\t},\n\t\t\"ok/new-account-with-eab\": func(t *testing.T) test {\n\t\t\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\tassert.FatalError(t, err)\n\t\t\turl := fmt.Sprintf(\"%s/acme/%s/account/new-account\", baseURL.String(), escProvName)\n\t\t\trawEABJWS, err := createRawEABJWS(jwk, []byte{1, 3, 3, 7}, \"eakID\", url)\n\t\t\tassert.FatalError(t, err)\n\t\t\teab := &ExternalAccountBinding{}\n\t\t\terr = json.Unmarshal(rawEABJWS, &eab)\n\t\t\tassert.FatalError(t, err)\n\t\t\tnar := &NewAccountRequest{\n\t\t\t\tContact:                []string{\"foo\", \"bar\"},\n\t\t\t\tExternalAccountBinding: eab,\n\t\t\t}\n\t\t\tpayloadBytes, err := json.Marshal(nar)\n\t\t\tassert.FatalError(t, err)\n\t\t\tso := new(jose.SignerOptions)\n\t\t\tso.WithHeader(\"alg\", jose.SignatureAlgorithm(jwk.Algorithm))\n\t\t\tso.WithHeader(\"url\", url)\n\t\t\tsigner, err := jose.NewSigner(jose.SigningKey{\n\t\t\t\tAlgorithm: jose.SignatureAlgorithm(jwk.Algorithm),\n\t\t\t\tKey:       jwk.Key,\n\t\t\t}, so)\n\t\t\tassert.FatalError(t, err)\n\t\t\tjws, err := signer.Sign(payloadBytes)\n\t\t\tassert.FatalError(t, err)\n\t\t\traw, err := jws.CompactSerialize()\n\t\t\tassert.FatalError(t, err)\n\t\t\tparsedJWS, err := jose.ParseJWS(raw)\n\t\t\tassert.FatalError(t, err)\n\t\t\tprov := newACMEProv(t)\n\t\t\tprov.RequireEAB = true\n\t\t\tctx := context.WithValue(context.Background(), payloadContextKey, &payloadInfo{value: payloadBytes})\n\t\t\tctx = context.WithValue(ctx, jwkContextKey, jwk)\n\t\t\tctx = acme.NewProvisionerContext(ctx, prov)\n\t\t\tctx = context.WithValue(ctx, jwsContextKey, parsedJWS)\n\t\t\treturn test{\n\t\t\t\tdb: &acme.MockDB{\n\t\t\t\t\tMockCreateAccount: func(ctx context.Context, acc *acme.Account) error {\n\t\t\t\t\t\tacc.ID = \"accountID\"\n\t\t\t\t\t\tassert.Equals(t, acc.Contact, nar.Contact)\n\t\t\t\t\t\tassert.Equals(t, acc.Key, jwk)\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t\tMockGetExternalAccountKey: func(ctx context.Context, provisionerName, keyID string) (*acme.ExternalAccountKey, error) {\n\t\t\t\t\t\treturn &acme.ExternalAccountKey{\n\t\t\t\t\t\t\tID:            \"eakID\",\n\t\t\t\t\t\t\tProvisionerID: provID,\n\t\t\t\t\t\t\tReference:     \"testeak\",\n\t\t\t\t\t\t\tHmacKey:       []byte{1, 3, 3, 7},\n\t\t\t\t\t\t\tCreatedAt:     time.Now(),\n\t\t\t\t\t\t}, nil\n\t\t\t\t\t},\n\t\t\t\t\tMockUpdateExternalAccountKey: func(ctx context.Context, provisionerName string, eak *acme.ExternalAccountKey) error {\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tacc: &acme.Account{\n\t\t\t\t\tID:                     \"accountID\",\n\t\t\t\t\tKey:                    jwk,\n\t\t\t\t\tStatus:                 acme.StatusValid,\n\t\t\t\t\tContact:                []string{\"foo\", \"bar\"},\n\t\t\t\t\tOrdersURL:              fmt.Sprintf(\"%s/acme/%s/account/accountID/orders\", baseURL.String(), escProvName),\n\t\t\t\t\tExternalAccountBinding: eab,\n\t\t\t\t},\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 201,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\ttc := run(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tctx := acme.NewContext(tc.ctx, tc.db, nil, acme.NewLinker(\"test.ca.smallstep.com\", \"acme\"), nil)\n\t\t\treq := httptest.NewRequest(\"GET\", \"/foo/bar\", http.NoBody)\n\t\t\treq = req.WithContext(ctx)\n\t\t\tw := httptest.NewRecorder()\n\t\t\tNewAccount(w, req)\n\t\t\tres := w.Result()\n\n\t\t\tassert.Equals(t, res.StatusCode, tc.statusCode)\n\n\t\t\tbody, err := io.ReadAll(res.Body)\n\t\t\tres.Body.Close()\n\t\t\tassert.FatalError(t, err)\n\n\t\t\tif res.StatusCode >= 400 && assert.NotNil(t, tc.err) {\n\t\t\t\tvar ae acme.Error\n\t\t\t\tassert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &ae))\n\n\t\t\t\tassert.Equals(t, ae.Type, tc.err.Type)\n\t\t\t\tassert.Equals(t, ae.Detail, tc.err.Detail)\n\t\t\t\tassert.Equals(t, ae.Subproblems, tc.err.Subproblems)\n\t\t\t\tassert.Equals(t, res.Header[\"Content-Type\"], []string{\"application/problem+json\"})\n\t\t\t} else {\n\t\t\t\texpB, err := json.Marshal(tc.acc)\n\t\t\t\tassert.FatalError(t, err)\n\t\t\t\tassert.Equals(t, bytes.TrimSpace(body), expB)\n\t\t\t\tassert.Equals(t, res.Header[\"Location\"],\n\t\t\t\t\t[]string{fmt.Sprintf(\"%s/acme/%s/account/%s\", baseURL.String(),\n\t\t\t\t\t\tescProvName, \"accountID\")})\n\t\t\t\tassert.Equals(t, res.Header[\"Content-Type\"], []string{\"application/json\"})\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestHandler_GetOrUpdateAccount(t *testing.T) {\n\taccID := \"accountID\"\n\tacc := acme.Account{\n\t\tID:        accID,\n\t\tStatus:    \"valid\",\n\t\tOrdersURL: fmt.Sprintf(\"https://ca.smallstep.com/acme/account/%s/orders\", accID),\n\t}\n\tprov := newProv()\n\tescProvName := url.PathEscape(prov.GetName())\n\tbaseURL := &url.URL{Scheme: \"https\", Host: \"test.ca.smallstep.com\"}\n\n\ttype test struct {\n\t\tdb         acme.DB\n\t\tctx        context.Context\n\t\tstatusCode int\n\t\terr        *acme.Error\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/no-account\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tdb:         &acme.MockDB{},\n\t\t\t\tctx:        context.Background(),\n\t\t\t\tstatusCode: 400,\n\t\t\t\terr:        acme.NewError(acme.ErrorAccountDoesNotExistType, \"account does not exist\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/nil-account\": func(t *testing.T) test {\n\t\t\tctx := context.WithValue(context.Background(), accContextKey, nil)\n\t\t\treturn test{\n\t\t\t\tdb:         &acme.MockDB{},\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 400,\n\t\t\t\terr:        acme.NewError(acme.ErrorAccountDoesNotExistType, \"account does not exist\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/no-payload\": func(t *testing.T) test {\n\t\t\tctx := context.WithValue(context.Background(), accContextKey, &acc)\n\t\t\treturn test{\n\t\t\t\tdb:         &acme.MockDB{},\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 500,\n\t\t\t\terr:        acme.NewErrorISE(\"payload expected in request context\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/nil-payload\": func(t *testing.T) test {\n\t\t\tctx := context.WithValue(context.Background(), accContextKey, &acc)\n\t\t\tctx = context.WithValue(ctx, payloadContextKey, nil)\n\t\t\treturn test{\n\t\t\t\tdb:         &acme.MockDB{},\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 500,\n\t\t\t\terr:        acme.NewErrorISE(\"payload expected in request context\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/unmarshal-payload-error\": func(t *testing.T) test {\n\t\t\tctx := context.WithValue(context.Background(), accContextKey, &acc)\n\t\t\tctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{})\n\t\t\treturn test{\n\t\t\t\tdb:         &acme.MockDB{},\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 400,\n\t\t\t\terr:        acme.NewError(acme.ErrorMalformedType, \"failed to unmarshal new-account request payload: unexpected end of JSON input\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/malformed-payload-error\": func(t *testing.T) test {\n\t\t\tuar := &UpdateAccountRequest{\n\t\t\t\tContact: []string{\"foo\", \"\"},\n\t\t\t}\n\t\t\tb, err := json.Marshal(uar)\n\t\t\tassert.FatalError(t, err)\n\t\t\tctx := context.WithValue(context.Background(), accContextKey, &acc)\n\t\t\tctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b})\n\t\t\treturn test{\n\t\t\t\tdb:         &acme.MockDB{},\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 400,\n\t\t\t\terr:        acme.NewError(acme.ErrorMalformedType, \"contact cannot be empty string\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/db.UpdateAccount-error\": func(t *testing.T) test {\n\t\t\tuar := &UpdateAccountRequest{\n\t\t\t\tStatus: \"deactivated\",\n\t\t\t}\n\t\t\tb, err := json.Marshal(uar)\n\t\t\tassert.FatalError(t, err)\n\t\t\tctx := context.WithValue(context.Background(), accContextKey, &acc)\n\t\t\tctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b})\n\t\t\treturn test{\n\t\t\t\tdb: &acme.MockDB{\n\t\t\t\t\tMockUpdateAccount: func(ctx context.Context, upd *acme.Account) error {\n\t\t\t\t\t\tassert.Equals(t, upd.Status, acme.StatusDeactivated)\n\t\t\t\t\t\tassert.Equals(t, upd.ID, acc.ID)\n\t\t\t\t\t\treturn acme.NewErrorISE(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 500,\n\t\t\t\terr:        acme.NewErrorISE(\"force\"),\n\t\t\t}\n\t\t},\n\t\t\"ok/deactivate\": func(t *testing.T) test {\n\t\t\tuar := &UpdateAccountRequest{\n\t\t\t\tStatus: \"deactivated\",\n\t\t\t}\n\t\t\tb, err := json.Marshal(uar)\n\t\t\tassert.FatalError(t, err)\n\t\t\tctx := acme.NewProvisionerContext(context.Background(), prov)\n\t\t\tctx = context.WithValue(ctx, accContextKey, &acc)\n\t\t\tctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b})\n\t\t\treturn test{\n\t\t\t\tdb: &acme.MockDB{\n\t\t\t\t\tMockUpdateAccount: func(ctx context.Context, upd *acme.Account) error {\n\t\t\t\t\t\tassert.Equals(t, upd.Status, acme.StatusDeactivated)\n\t\t\t\t\t\tassert.Equals(t, upd.ID, acc.ID)\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 200,\n\t\t\t}\n\t\t},\n\t\t\"ok/update-empty\": func(t *testing.T) test {\n\t\t\tuar := &UpdateAccountRequest{}\n\t\t\tb, err := json.Marshal(uar)\n\t\t\tassert.FatalError(t, err)\n\t\t\tctx := acme.NewProvisionerContext(context.Background(), prov)\n\t\t\tctx = context.WithValue(ctx, accContextKey, &acc)\n\t\t\tctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b})\n\t\t\treturn test{\n\t\t\t\tdb:         &acme.MockDB{},\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 200,\n\t\t\t}\n\t\t},\n\t\t\"ok/update-contacts\": func(t *testing.T) test {\n\t\t\tuar := &UpdateAccountRequest{\n\t\t\t\tContact: []string{\"foo\", \"bar\"},\n\t\t\t}\n\t\t\tb, err := json.Marshal(uar)\n\t\t\tassert.FatalError(t, err)\n\t\t\tctx := acme.NewProvisionerContext(context.Background(), prov)\n\t\t\tctx = context.WithValue(ctx, accContextKey, &acc)\n\t\t\tctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b})\n\t\t\treturn test{\n\t\t\t\tdb: &acme.MockDB{\n\t\t\t\t\tMockUpdateAccount: func(ctx context.Context, upd *acme.Account) error {\n\t\t\t\t\t\tassert.Equals(t, upd.Contact, uar.Contact)\n\t\t\t\t\t\tassert.Equals(t, upd.ID, acc.ID)\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 200,\n\t\t\t}\n\t\t},\n\t\t\"ok/post-as-get\": func(t *testing.T) test {\n\t\t\tctx := acme.NewProvisionerContext(context.Background(), prov)\n\t\t\tctx = context.WithValue(ctx, accContextKey, &acc)\n\t\t\tctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{isPostAsGet: true})\n\t\t\treturn test{\n\t\t\t\tdb:         &acme.MockDB{},\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 200,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\ttc := run(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tctx := acme.NewContext(tc.ctx, tc.db, nil, acme.NewLinker(\"test.ca.smallstep.com\", \"acme\"), nil)\n\t\t\treq := httptest.NewRequest(\"GET\", \"/foo/bar\", http.NoBody)\n\t\t\treq = req.WithContext(ctx)\n\t\t\tw := httptest.NewRecorder()\n\t\t\tGetOrUpdateAccount(w, req)\n\t\t\tres := w.Result()\n\n\t\t\tassert.Equals(t, res.StatusCode, tc.statusCode)\n\n\t\t\tbody, err := io.ReadAll(res.Body)\n\t\t\tres.Body.Close()\n\t\t\tassert.FatalError(t, err)\n\n\t\t\tif res.StatusCode >= 400 && assert.NotNil(t, tc.err) {\n\t\t\t\tvar ae acme.Error\n\t\t\t\tassert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &ae))\n\n\t\t\t\tassert.Equals(t, ae.Type, tc.err.Type)\n\t\t\t\tassert.Equals(t, ae.Detail, tc.err.Detail)\n\t\t\t\tassert.Equals(t, ae.Subproblems, tc.err.Subproblems)\n\t\t\t\tassert.Equals(t, res.Header[\"Content-Type\"], []string{\"application/problem+json\"})\n\t\t\t} else {\n\t\t\t\texpB, err := json.Marshal(acc)\n\t\t\t\tassert.FatalError(t, err)\n\t\t\t\tassert.Equals(t, bytes.TrimSpace(body), expB)\n\t\t\t\tassert.Equals(t, res.Header[\"Location\"],\n\t\t\t\t\t[]string{fmt.Sprintf(\"%s/acme/%s/account/%s\", baseURL.String(),\n\t\t\t\t\t\tescProvName, accID)})\n\t\t\t\tassert.Equals(t, res.Header[\"Content-Type\"], []string{\"application/json\"})\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "acme/api/eab.go",
    "content": "package api\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\n\t\"go.step.sm/crypto/jose\"\n\n\t\"github.com/smallstep/certificates/acme\"\n)\n\n// ExternalAccountBinding represents the ACME externalAccountBinding JWS\ntype ExternalAccountBinding struct {\n\tProtected string `json:\"protected\"`\n\tPayload   string `json:\"payload\"`\n\tSig       string `json:\"signature\"`\n}\n\n// validateExternalAccountBinding validates the externalAccountBinding property in a call to new-account.\nfunc validateExternalAccountBinding(ctx context.Context, nar *NewAccountRequest) (*acme.ExternalAccountKey, error) {\n\tacmeProv, err := acmeProvisionerFromContext(ctx)\n\tif err != nil {\n\t\treturn nil, acme.WrapErrorISE(err, \"could not load ACME provisioner from context\")\n\t}\n\n\tif !acmeProv.RequireEAB {\n\t\t//nolint:nilnil // legacy\n\t\treturn nil, nil\n\t}\n\n\tif nar.ExternalAccountBinding == nil {\n\t\treturn nil, acme.NewError(acme.ErrorExternalAccountRequiredType, \"no external account binding provided\")\n\t}\n\n\teabJSONBytes, err := json.Marshal(nar.ExternalAccountBinding)\n\tif err != nil {\n\t\treturn nil, acme.WrapErrorISE(err, \"error marshaling externalAccountBinding into bytes\")\n\t}\n\n\teabJWS, err := jose.ParseJWS(string(eabJSONBytes))\n\tif err != nil {\n\t\treturn nil, acme.WrapErrorISE(err, \"error parsing externalAccountBinding jws\")\n\t}\n\n\t// TODO(hs): implement strategy pattern to allow for different ways of verification (i.e. webhook call) based on configuration?\n\n\tkeyID, acmeErr := validateEABJWS(ctx, eabJWS)\n\tif acmeErr != nil {\n\t\treturn nil, acmeErr\n\t}\n\n\tdb := acme.MustDatabaseFromContext(ctx)\n\texternalAccountKey, err := db.GetExternalAccountKey(ctx, acmeProv.ID, keyID)\n\tif err != nil {\n\t\tvar ae *acme.Error\n\t\tif errors.As(err, &ae) {\n\t\t\treturn nil, acme.WrapError(acme.ErrorUnauthorizedType, err, \"the field 'kid' references an unknown key\")\n\t\t}\n\t\treturn nil, acme.WrapErrorISE(err, \"error retrieving external account key\")\n\t}\n\n\tif externalAccountKey == nil {\n\t\treturn nil, acme.NewError(acme.ErrorUnauthorizedType, \"the field 'kid' references an unknown key\")\n\t}\n\n\tif len(externalAccountKey.HmacKey) == 0 {\n\t\treturn nil, acme.NewError(acme.ErrorServerInternalType, \"external account binding key with id '%s' does not have secret bytes\", keyID)\n\t}\n\n\tif externalAccountKey.AlreadyBound() {\n\t\treturn nil, acme.NewError(acme.ErrorUnauthorizedType, \"external account binding key with id '%s' was already bound to account '%s' on %s\", keyID, externalAccountKey.AccountID, externalAccountKey.BoundAt)\n\t}\n\n\tpayload, err := eabJWS.Verify(externalAccountKey.HmacKey)\n\tif err != nil {\n\t\treturn nil, acme.WrapErrorISE(err, \"error verifying externalAccountBinding signature\")\n\t}\n\n\tjwk, err := jwkFromContext(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar payloadJWK *jose.JSONWebKey\n\tif err = json.Unmarshal(payload, &payloadJWK); err != nil {\n\t\treturn nil, acme.WrapError(acme.ErrorMalformedType, err, \"error unmarshaling payload into jwk\")\n\t}\n\n\tif !keysAreEqual(jwk, payloadJWK) {\n\t\treturn nil, acme.NewError(acme.ErrorUnauthorizedType, \"keys in jws and eab payload do not match\")\n\t}\n\n\treturn externalAccountKey, nil\n}\n\n// keysAreEqual performs an equality check on two JWKs by comparing\n// the (base64 encoding) of the Key IDs.\nfunc keysAreEqual(x, y *jose.JSONWebKey) bool {\n\tif x == nil || y == nil {\n\t\treturn false\n\t}\n\tdigestX, errX := acme.KeyToID(x)\n\tdigestY, errY := acme.KeyToID(y)\n\tif errX != nil || errY != nil {\n\t\treturn false\n\t}\n\treturn digestX == digestY\n}\n\n// validateEABJWS verifies the contents of the External Account Binding JWS.\n// The protected header of the JWS MUST meet the following criteria:\n//\n//   - The \"alg\" field MUST indicate a MAC-based algorithm\n//   - The \"kid\" field MUST contain the key identifier provided by the CA\n//   - The \"nonce\" field MUST NOT be present\n//   - The \"url\" field MUST be set to the same value as the outer JWS\nfunc validateEABJWS(ctx context.Context, jws *jose.JSONWebSignature) (string, *acme.Error) {\n\tif jws == nil {\n\t\treturn \"\", acme.NewErrorISE(\"no JWS provided\")\n\t}\n\n\tif len(jws.Signatures) != 1 {\n\t\treturn \"\", acme.NewError(acme.ErrorMalformedType, \"JWS must have one signature\")\n\t}\n\n\theader := jws.Signatures[0].Protected\n\talgorithm := header.Algorithm\n\tkeyID := header.KeyID\n\tnonce := header.Nonce\n\n\tif algorithm != jose.HS256 && algorithm != jose.HS384 && algorithm != jose.HS512 {\n\t\treturn \"\", acme.NewError(acme.ErrorMalformedType, \"'alg' field set to invalid algorithm '%s'\", algorithm)\n\t}\n\n\tif keyID == \"\" {\n\t\treturn \"\", acme.NewError(acme.ErrorMalformedType, \"'kid' field is required\")\n\t}\n\n\tif nonce != \"\" {\n\t\treturn \"\", acme.NewError(acme.ErrorMalformedType, \"'nonce' must not be present\")\n\t}\n\n\tjwsURL, ok := header.ExtraHeaders[\"url\"]\n\tif !ok {\n\t\treturn \"\", acme.NewError(acme.ErrorMalformedType, \"'url' field is required\")\n\t}\n\n\touterJWS, err := jwsFromContext(ctx)\n\tif err != nil {\n\t\treturn \"\", acme.WrapErrorISE(err, \"could not retrieve outer JWS from context\")\n\t}\n\n\tif len(outerJWS.Signatures) != 1 {\n\t\treturn \"\", acme.NewError(acme.ErrorMalformedType, \"outer JWS must have one signature\")\n\t}\n\n\touterJWSURL, ok := outerJWS.Signatures[0].Protected.ExtraHeaders[\"url\"]\n\tif !ok {\n\t\treturn \"\", acme.NewError(acme.ErrorMalformedType, \"'url' field must be set in outer JWS\")\n\t}\n\n\tif jwsURL != outerJWSURL {\n\t\treturn \"\", acme.NewError(acme.ErrorMalformedType, \"'url' field is not the same value as the outer JWS\")\n\t}\n\n\treturn keyID, nil\n}\n"
  },
  {
    "path": "acme/api/eab_test.go",
    "content": "package api\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/url\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n\n\t\"go.step.sm/crypto/jose\"\n\n\t\"github.com/smallstep/assert\"\n\t\"github.com/smallstep/certificates/acme\"\n)\n\nfunc Test_keysAreEqual(t *testing.T) {\n\tjwkX, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\tassert.FatalError(t, err)\n\tjwkY, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\tassert.FatalError(t, err)\n\twrongJWK, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\tassert.FatalError(t, err)\n\twrongJWK.Key = struct{}{}\n\ttype args struct {\n\t\tx *jose.JSONWebKey\n\t\ty *jose.JSONWebKey\n\t}\n\ttests := []struct {\n\t\tname string\n\t\targs args\n\t\twant bool\n\t}{\n\t\t{\n\t\t\tname: \"ok/nil\",\n\t\t\targs: args{\n\t\t\t\tx: jwkX,\n\t\t\t\ty: nil,\n\t\t\t},\n\t\t\twant: false,\n\t\t},\n\t\t{\n\t\t\tname: \"ok/equal\",\n\t\t\targs: args{\n\t\t\t\tx: jwkX,\n\t\t\t\ty: jwkX,\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"ok/not-equal\",\n\t\t\targs: args{\n\t\t\t\tx: jwkX,\n\t\t\t\ty: jwkY,\n\t\t\t},\n\t\t\twant: false,\n\t\t},\n\t\t{\n\t\t\tname: \"ok/wrong-key-type\",\n\t\t\targs: args{\n\t\t\t\tx: wrongJWK,\n\t\t\t\ty: jwkY,\n\t\t\t},\n\t\t\twant: false,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif got := keysAreEqual(tt.args.x, tt.args.y); got != tt.want {\n\t\t\t\tt.Errorf(\"keysAreEqual() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestHandler_validateExternalAccountBinding(t *testing.T) {\n\tacmeProv := newACMEProv(t)\n\tescProvName := url.PathEscape(acmeProv.GetName())\n\tbaseURL := &url.URL{Scheme: \"https\", Host: \"test.ca.smallstep.com\"}\n\tprovID := acmeProv.GetID()\n\ttype test struct {\n\t\tdb  acme.DB\n\t\tctx context.Context\n\t\tnar *NewAccountRequest\n\t\teak *acme.ExternalAccountKey\n\t\terr *acme.Error\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"ok/no-eab-required-but-provided\": func(t *testing.T) test {\n\t\t\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\tassert.FatalError(t, err)\n\t\t\turl := fmt.Sprintf(\"%s/acme/%s/account/new-account\", baseURL.String(), escProvName)\n\t\t\trawEABJWS, err := createRawEABJWS(jwk, []byte{1, 3, 3, 7}, \"eakID\", url)\n\t\t\tassert.FatalError(t, err)\n\t\t\teab := &ExternalAccountBinding{}\n\t\t\terr = json.Unmarshal(rawEABJWS, &eab)\n\t\t\tassert.FatalError(t, err)\n\t\t\tprov := newACMEProv(t)\n\t\t\tctx := context.WithValue(context.Background(), jwkContextKey, jwk)\n\t\t\tctx = acme.NewProvisionerContext(ctx, prov)\n\t\t\treturn test{\n\t\t\t\tdb:  &acme.MockDB{},\n\t\t\t\tctx: ctx,\n\t\t\t\tnar: &NewAccountRequest{\n\t\t\t\t\tContact:                []string{\"foo\", \"bar\"},\n\t\t\t\t\tExternalAccountBinding: eab,\n\t\t\t\t},\n\t\t\t\teak: nil,\n\t\t\t\terr: nil,\n\t\t\t}\n\t\t},\n\t\t\"ok/eab\": func(t *testing.T) test {\n\t\t\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\tassert.FatalError(t, err)\n\t\t\turl := fmt.Sprintf(\"%s/acme/%s/account/new-account\", baseURL.String(), escProvName)\n\t\t\trawEABJWS, err := createRawEABJWS(jwk, []byte{1, 3, 3, 7}, \"eakID\", url)\n\t\t\tassert.FatalError(t, err)\n\t\t\teab := &ExternalAccountBinding{}\n\t\t\terr = json.Unmarshal(rawEABJWS, &eab)\n\t\t\tassert.FatalError(t, err)\n\t\t\tnar := &NewAccountRequest{\n\t\t\t\tContact:                []string{\"foo\", \"bar\"},\n\t\t\t\tExternalAccountBinding: eab,\n\t\t\t}\n\t\t\tpayloadBytes, err := json.Marshal(nar)\n\t\t\tassert.FatalError(t, err)\n\t\t\tso := new(jose.SignerOptions)\n\t\t\tso.WithHeader(\"alg\", jose.SignatureAlgorithm(jwk.Algorithm))\n\t\t\tso.WithHeader(\"url\", url)\n\t\t\tsigner, err := jose.NewSigner(jose.SigningKey{\n\t\t\t\tAlgorithm: jose.SignatureAlgorithm(jwk.Algorithm),\n\t\t\t\tKey:       jwk.Key,\n\t\t\t}, so)\n\t\t\tassert.FatalError(t, err)\n\t\t\tjws, err := signer.Sign(payloadBytes)\n\t\t\tassert.FatalError(t, err)\n\t\t\traw, err := jws.CompactSerialize()\n\t\t\tassert.FatalError(t, err)\n\t\t\tparsedJWS, err := jose.ParseJWS(raw)\n\t\t\tassert.FatalError(t, err)\n\t\t\tprov := newACMEProv(t)\n\t\t\tprov.RequireEAB = true\n\t\t\tctx := context.WithValue(context.Background(), jwkContextKey, jwk)\n\t\t\tctx = acme.NewProvisionerContext(ctx, prov)\n\t\t\tctx = context.WithValue(ctx, jwsContextKey, parsedJWS)\n\t\t\tcreatedAt := time.Now()\n\t\t\treturn test{\n\t\t\t\tdb: &acme.MockDB{\n\t\t\t\t\tMockGetExternalAccountKey: func(ctx context.Context, provisionerName, keyID string) (*acme.ExternalAccountKey, error) {\n\t\t\t\t\t\treturn &acme.ExternalAccountKey{\n\t\t\t\t\t\t\tID:            \"eakID\",\n\t\t\t\t\t\t\tProvisionerID: provID,\n\t\t\t\t\t\t\tReference:     \"testeak\",\n\t\t\t\t\t\t\tHmacKey:       []byte{1, 3, 3, 7},\n\t\t\t\t\t\t\tCreatedAt:     createdAt,\n\t\t\t\t\t\t}, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tctx: ctx,\n\t\t\t\tnar: &NewAccountRequest{\n\t\t\t\t\tContact:                []string{\"foo\", \"bar\"},\n\t\t\t\t\tExternalAccountBinding: eab,\n\t\t\t\t},\n\t\t\t\teak: &acme.ExternalAccountKey{\n\t\t\t\t\tID:            \"eakID\",\n\t\t\t\t\tProvisionerID: provID,\n\t\t\t\t\tReference:     \"testeak\",\n\t\t\t\t\tHmacKey:       []byte{1, 3, 3, 7},\n\t\t\t\t\tCreatedAt:     createdAt,\n\t\t\t\t},\n\t\t\t\terr: nil,\n\t\t\t}\n\t\t},\n\t\t\"fail/acmeProvisionerFromContext\": func(t *testing.T) test {\n\t\t\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\tassert.FatalError(t, err)\n\t\t\turl := fmt.Sprintf(\"%s/acme/%s/account/new-account\", baseURL.String(), escProvName)\n\t\t\trawEABJWS, err := createRawEABJWS(jwk, []byte{1, 3, 3, 7}, \"eakID\", url)\n\t\t\tassert.FatalError(t, err)\n\t\t\teab := &ExternalAccountBinding{}\n\t\t\terr = json.Unmarshal(rawEABJWS, &eab)\n\t\t\tassert.FatalError(t, err)\n\t\t\tnar := &NewAccountRequest{\n\t\t\t\tContact:                []string{\"foo\", \"bar\"},\n\t\t\t\tExternalAccountBinding: eab,\n\t\t\t}\n\t\t\tb, err := json.Marshal(nar)\n\t\t\tassert.FatalError(t, err)\n\n\t\t\tctx := context.WithValue(context.Background(), payloadContextKey, &payloadInfo{value: b})\n\t\t\tctx = context.WithValue(ctx, jwkContextKey, jwk)\n\t\t\tctx = acme.NewProvisionerContext(ctx, &fakeProvisioner{})\n\t\t\treturn test{\n\t\t\t\tctx: ctx,\n\t\t\t\terr: acme.NewError(acme.ErrorServerInternalType, \"could not load ACME provisioner from context: provisioner in context is not an ACME provisioner\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/parse-eab-jose\": func(t *testing.T) test {\n\t\t\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\tassert.FatalError(t, err)\n\t\t\turl := fmt.Sprintf(\"%s/acme/%s/account/new-account\", baseURL.String(), escProvName)\n\t\t\trawEABJWS, err := createRawEABJWS(jwk, []byte{1, 3, 3, 7}, \"eakID\", url)\n\t\t\tassert.FatalError(t, err)\n\t\t\teab := &ExternalAccountBinding{}\n\t\t\terr = json.Unmarshal(rawEABJWS, &eab)\n\t\t\tassert.FatalError(t, err)\n\t\t\teab.Payload += \"{}\"\n\t\t\tprov := newACMEProv(t)\n\t\t\tprov.RequireEAB = true\n\t\t\tctx := context.WithValue(context.Background(), jwkContextKey, jwk)\n\t\t\tctx = acme.NewProvisionerContext(ctx, prov)\n\t\t\treturn test{\n\t\t\t\tdb:  &acme.MockDB{},\n\t\t\t\tctx: ctx,\n\t\t\t\tnar: &NewAccountRequest{\n\t\t\t\t\tContact:                []string{\"foo\", \"bar\"},\n\t\t\t\t\tExternalAccountBinding: eab,\n\t\t\t\t},\n\t\t\t\teak: nil,\n\t\t\t\terr: acme.NewErrorISE(\"error parsing externalAccountBinding jws\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/validate-eab-jws-no-signatures\": func(t *testing.T) test {\n\t\t\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\tassert.FatalError(t, err)\n\t\t\turl := fmt.Sprintf(\"%s/acme/%s/account/new-account\", baseURL.String(), escProvName)\n\t\t\trawEABJWS, err := createRawEABJWS(jwk, []byte{1, 3, 3, 7}, \"eakID\", url)\n\t\t\tassert.FatalError(t, err)\n\t\t\teab := &ExternalAccountBinding{}\n\t\t\terr = json.Unmarshal(rawEABJWS, &eab)\n\t\t\tassert.FatalError(t, err)\n\t\t\tnar := &NewAccountRequest{\n\t\t\t\tContact:                []string{\"foo\", \"bar\"},\n\t\t\t\tExternalAccountBinding: eab,\n\t\t\t}\n\t\t\tpayloadBytes, err := json.Marshal(nar)\n\t\t\tassert.FatalError(t, err)\n\t\t\tso := new(jose.SignerOptions)\n\t\t\tso.WithHeader(\"alg\", jose.SignatureAlgorithm(jwk.Algorithm))\n\t\t\tso.WithHeader(\"url\", url)\n\t\t\tsigner, err := jose.NewSigner(jose.SigningKey{\n\t\t\t\tAlgorithm: jose.SignatureAlgorithm(jwk.Algorithm),\n\t\t\t\tKey:       jwk.Key,\n\t\t\t}, so)\n\t\t\tassert.FatalError(t, err)\n\t\t\tjws, err := signer.Sign(payloadBytes)\n\t\t\tassert.FatalError(t, err)\n\t\t\traw, err := jws.CompactSerialize()\n\t\t\tassert.FatalError(t, err)\n\t\t\tparsedJWS, err := jose.ParseJWS(raw)\n\t\t\tassert.FatalError(t, err)\n\t\t\tparsedJWS.Signatures = []jose.Signature{}\n\t\t\tprov := newACMEProv(t)\n\t\t\tprov.RequireEAB = true\n\t\t\tctx := context.WithValue(context.Background(), jwkContextKey, jwk)\n\t\t\tctx = acme.NewProvisionerContext(ctx, prov)\n\t\t\tctx = context.WithValue(ctx, jwsContextKey, parsedJWS)\n\t\t\treturn test{\n\t\t\t\tdb:  &acme.MockDB{},\n\t\t\t\tctx: ctx,\n\t\t\t\tnar: &NewAccountRequest{\n\t\t\t\t\tContact:                []string{\"foo\", \"bar\"},\n\t\t\t\t\tExternalAccountBinding: eab,\n\t\t\t\t},\n\t\t\t\teak: nil,\n\t\t\t\terr: acme.NewError(acme.ErrorMalformedType, \"outer JWS must have one signature\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/retrieve-eab-key-db-failure\": func(t *testing.T) test {\n\t\t\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\tassert.FatalError(t, err)\n\t\t\turl := fmt.Sprintf(\"%s/acme/%s/account/new-account\", baseURL.String(), escProvName)\n\t\t\trawEABJWS, err := createRawEABJWS(jwk, []byte{1, 3, 3, 7}, \"eakID\", url)\n\t\t\tassert.FatalError(t, err)\n\t\t\teab := &ExternalAccountBinding{}\n\t\t\terr = json.Unmarshal(rawEABJWS, &eab)\n\t\t\tassert.FatalError(t, err)\n\t\t\tnar := &NewAccountRequest{\n\t\t\t\tContact:                []string{\"foo\", \"bar\"},\n\t\t\t\tExternalAccountBinding: eab,\n\t\t\t}\n\t\t\tpayloadBytes, err := json.Marshal(nar)\n\t\t\tassert.FatalError(t, err)\n\t\t\tso := new(jose.SignerOptions)\n\t\t\tso.WithHeader(\"alg\", jose.SignatureAlgorithm(jwk.Algorithm))\n\t\t\tso.WithHeader(\"url\", url)\n\t\t\tsigner, err := jose.NewSigner(jose.SigningKey{\n\t\t\t\tAlgorithm: jose.SignatureAlgorithm(jwk.Algorithm),\n\t\t\t\tKey:       jwk.Key,\n\t\t\t}, so)\n\t\t\tassert.FatalError(t, err)\n\t\t\tjws, err := signer.Sign(payloadBytes)\n\t\t\tassert.FatalError(t, err)\n\t\t\traw, err := jws.CompactSerialize()\n\t\t\tassert.FatalError(t, err)\n\t\t\tparsedJWS, err := jose.ParseJWS(raw)\n\t\t\tassert.FatalError(t, err)\n\t\t\tprov := newACMEProv(t)\n\t\t\tprov.RequireEAB = true\n\t\t\tctx := context.WithValue(context.Background(), jwkContextKey, jwk)\n\t\t\tctx = acme.NewProvisionerContext(ctx, prov)\n\t\t\tctx = context.WithValue(ctx, jwsContextKey, parsedJWS)\n\t\t\treturn test{\n\t\t\t\tdb: &acme.MockDB{\n\t\t\t\t\tMockError: errors.New(\"db failure\"),\n\t\t\t\t},\n\t\t\t\tctx: ctx,\n\t\t\t\tnar: &NewAccountRequest{\n\t\t\t\t\tContact:                []string{\"foo\", \"bar\"},\n\t\t\t\t\tExternalAccountBinding: eab,\n\t\t\t\t},\n\t\t\t\teak: nil,\n\t\t\t\terr: acme.NewErrorISE(\"error retrieving external account key\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/db.GetExternalAccountKey-not-found\": func(t *testing.T) test {\n\t\t\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\tassert.FatalError(t, err)\n\t\t\turl := fmt.Sprintf(\"%s/acme/%s/account/new-account\", baseURL.String(), escProvName)\n\t\t\trawEABJWS, err := createRawEABJWS(jwk, []byte{1, 3, 3, 7}, \"eakID\", url)\n\t\t\tassert.FatalError(t, err)\n\t\t\teab := &ExternalAccountBinding{}\n\t\t\terr = json.Unmarshal(rawEABJWS, &eab)\n\t\t\tassert.FatalError(t, err)\n\t\t\tnar := &NewAccountRequest{\n\t\t\t\tContact:                []string{\"foo\", \"bar\"},\n\t\t\t\tExternalAccountBinding: eab,\n\t\t\t}\n\t\t\tpayloadBytes, err := json.Marshal(nar)\n\t\t\tassert.FatalError(t, err)\n\t\t\tso := new(jose.SignerOptions)\n\t\t\tso.WithHeader(\"alg\", jose.SignatureAlgorithm(jwk.Algorithm))\n\t\t\tso.WithHeader(\"url\", url)\n\t\t\tsigner, err := jose.NewSigner(jose.SigningKey{\n\t\t\t\tAlgorithm: jose.SignatureAlgorithm(jwk.Algorithm),\n\t\t\t\tKey:       jwk.Key,\n\t\t\t}, so)\n\t\t\tassert.FatalError(t, err)\n\t\t\tjws, err := signer.Sign(payloadBytes)\n\t\t\tassert.FatalError(t, err)\n\t\t\traw, err := jws.CompactSerialize()\n\t\t\tassert.FatalError(t, err)\n\t\t\tparsedJWS, err := jose.ParseJWS(raw)\n\t\t\tassert.FatalError(t, err)\n\t\t\tprov := newACMEProv(t)\n\t\t\tprov.RequireEAB = true\n\t\t\tctx := context.WithValue(context.Background(), jwkContextKey, jwk)\n\t\t\tctx = acme.NewProvisionerContext(ctx, prov)\n\t\t\tctx = context.WithValue(ctx, jwsContextKey, parsedJWS)\n\t\t\treturn test{\n\t\t\t\tdb: &acme.MockDB{\n\t\t\t\t\tMockGetExternalAccountKey: func(ctx context.Context, provisionerName, keyID string) (*acme.ExternalAccountKey, error) {\n\t\t\t\t\t\treturn nil, acme.ErrNotFound\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tctx: ctx,\n\t\t\t\tnar: &NewAccountRequest{\n\t\t\t\t\tContact:                []string{\"foo\", \"bar\"},\n\t\t\t\t\tExternalAccountBinding: eab,\n\t\t\t\t},\n\t\t\t\teak: nil,\n\t\t\t\terr: acme.NewErrorISE(\"error retrieving external account key\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/db.GetExternalAccountKey-error\": func(t *testing.T) test {\n\t\t\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\tassert.FatalError(t, err)\n\t\t\turl := fmt.Sprintf(\"%s/acme/%s/account/new-account\", baseURL.String(), escProvName)\n\t\t\trawEABJWS, err := createRawEABJWS(jwk, []byte{1, 3, 3, 7}, \"eakID\", url)\n\t\t\tassert.FatalError(t, err)\n\t\t\teab := &ExternalAccountBinding{}\n\t\t\terr = json.Unmarshal(rawEABJWS, &eab)\n\t\t\tassert.FatalError(t, err)\n\t\t\tnar := &NewAccountRequest{\n\t\t\t\tContact:                []string{\"foo\", \"bar\"},\n\t\t\t\tExternalAccountBinding: eab,\n\t\t\t}\n\t\t\tpayloadBytes, err := json.Marshal(nar)\n\t\t\tassert.FatalError(t, err)\n\t\t\tso := new(jose.SignerOptions)\n\t\t\tso.WithHeader(\"alg\", jose.SignatureAlgorithm(jwk.Algorithm))\n\t\t\tso.WithHeader(\"url\", url)\n\t\t\tsigner, err := jose.NewSigner(jose.SigningKey{\n\t\t\t\tAlgorithm: jose.SignatureAlgorithm(jwk.Algorithm),\n\t\t\t\tKey:       jwk.Key,\n\t\t\t}, so)\n\t\t\tassert.FatalError(t, err)\n\t\t\tjws, err := signer.Sign(payloadBytes)\n\t\t\tassert.FatalError(t, err)\n\t\t\traw, err := jws.CompactSerialize()\n\t\t\tassert.FatalError(t, err)\n\t\t\tparsedJWS, err := jose.ParseJWS(raw)\n\t\t\tassert.FatalError(t, err)\n\t\t\tprov := newACMEProv(t)\n\t\t\tprov.RequireEAB = true\n\t\t\tctx := context.WithValue(context.Background(), jwkContextKey, jwk)\n\t\t\tctx = acme.NewProvisionerContext(ctx, prov)\n\t\t\tctx = context.WithValue(ctx, jwsContextKey, parsedJWS)\n\t\t\treturn test{\n\t\t\t\tdb: &acme.MockDB{\n\t\t\t\t\tMockGetExternalAccountKey: func(ctx context.Context, provisionerName, keyID string) (*acme.ExternalAccountKey, error) {\n\t\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tctx: ctx,\n\t\t\t\tnar: &NewAccountRequest{\n\t\t\t\t\tContact:                []string{\"foo\", \"bar\"},\n\t\t\t\t\tExternalAccountBinding: eab,\n\t\t\t\t},\n\t\t\t\teak: nil,\n\t\t\t\terr: acme.NewErrorISE(\"error retrieving external account key\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/db.GetExternalAccountKey-nil\": func(t *testing.T) test {\n\t\t\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\tassert.FatalError(t, err)\n\t\t\turl := fmt.Sprintf(\"%s/acme/%s/account/new-account\", baseURL.String(), escProvName)\n\t\t\trawEABJWS, err := createRawEABJWS(jwk, []byte{1, 3, 3, 7}, \"eakID\", url)\n\t\t\tassert.FatalError(t, err)\n\t\t\teab := &ExternalAccountBinding{}\n\t\t\terr = json.Unmarshal(rawEABJWS, &eab)\n\t\t\tassert.FatalError(t, err)\n\t\t\tnar := &NewAccountRequest{\n\t\t\t\tContact:                []string{\"foo\", \"bar\"},\n\t\t\t\tExternalAccountBinding: eab,\n\t\t\t}\n\t\t\tpayloadBytes, err := json.Marshal(nar)\n\t\t\tassert.FatalError(t, err)\n\t\t\tso := new(jose.SignerOptions)\n\t\t\tso.WithHeader(\"alg\", jose.SignatureAlgorithm(jwk.Algorithm))\n\t\t\tso.WithHeader(\"url\", url)\n\t\t\tsigner, err := jose.NewSigner(jose.SigningKey{\n\t\t\t\tAlgorithm: jose.SignatureAlgorithm(jwk.Algorithm),\n\t\t\t\tKey:       jwk.Key,\n\t\t\t}, so)\n\t\t\tassert.FatalError(t, err)\n\t\t\tjws, err := signer.Sign(payloadBytes)\n\t\t\tassert.FatalError(t, err)\n\t\t\traw, err := jws.CompactSerialize()\n\t\t\tassert.FatalError(t, err)\n\t\t\tparsedJWS, err := jose.ParseJWS(raw)\n\t\t\tassert.FatalError(t, err)\n\t\t\tprov := newACMEProv(t)\n\t\t\tprov.RequireEAB = true\n\t\t\tctx := context.WithValue(context.Background(), jwkContextKey, jwk)\n\t\t\tctx = acme.NewProvisionerContext(ctx, prov)\n\t\t\tctx = context.WithValue(ctx, jwsContextKey, parsedJWS)\n\t\t\treturn test{\n\t\t\t\tdb: &acme.MockDB{\n\t\t\t\t\tMockGetExternalAccountKey: func(ctx context.Context, provisionerName, keyID string) (*acme.ExternalAccountKey, error) {\n\t\t\t\t\t\treturn nil, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tctx: ctx,\n\t\t\t\tnar: &NewAccountRequest{\n\t\t\t\t\tContact:                []string{\"foo\", \"bar\"},\n\t\t\t\t\tExternalAccountBinding: eab,\n\t\t\t\t},\n\t\t\t\teak: nil,\n\t\t\t\terr: acme.NewError(acme.ErrorUnauthorizedType, \"the field 'kid' references an unknown key\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/db.GetExternalAccountKey-no-keybytes\": func(t *testing.T) test {\n\t\t\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\tassert.FatalError(t, err)\n\t\t\turl := fmt.Sprintf(\"%s/acme/%s/account/new-account\", baseURL.String(), escProvName)\n\t\t\trawEABJWS, err := createRawEABJWS(jwk, []byte{1, 3, 3, 7}, \"eakID\", url)\n\t\t\tassert.FatalError(t, err)\n\t\t\teab := &ExternalAccountBinding{}\n\t\t\terr = json.Unmarshal(rawEABJWS, &eab)\n\t\t\tassert.FatalError(t, err)\n\t\t\tnar := &NewAccountRequest{\n\t\t\t\tContact:                []string{\"foo\", \"bar\"},\n\t\t\t\tExternalAccountBinding: eab,\n\t\t\t}\n\t\t\tpayloadBytes, err := json.Marshal(nar)\n\t\t\tassert.FatalError(t, err)\n\t\t\tso := new(jose.SignerOptions)\n\t\t\tso.WithHeader(\"alg\", jose.SignatureAlgorithm(jwk.Algorithm))\n\t\t\tso.WithHeader(\"url\", url)\n\t\t\tsigner, err := jose.NewSigner(jose.SigningKey{\n\t\t\t\tAlgorithm: jose.SignatureAlgorithm(jwk.Algorithm),\n\t\t\t\tKey:       jwk.Key,\n\t\t\t}, so)\n\t\t\tassert.FatalError(t, err)\n\t\t\tjws, err := signer.Sign(payloadBytes)\n\t\t\tassert.FatalError(t, err)\n\t\t\traw, err := jws.CompactSerialize()\n\t\t\tassert.FatalError(t, err)\n\t\t\tparsedJWS, err := jose.ParseJWS(raw)\n\t\t\tassert.FatalError(t, err)\n\t\t\tprov := newACMEProv(t)\n\t\t\tprov.RequireEAB = true\n\t\t\tctx := context.WithValue(context.Background(), jwkContextKey, jwk)\n\t\t\tctx = acme.NewProvisionerContext(ctx, prov)\n\t\t\tctx = context.WithValue(ctx, jwsContextKey, parsedJWS)\n\t\t\tcreatedAt := time.Now()\n\t\t\treturn test{\n\t\t\t\tdb: &acme.MockDB{\n\t\t\t\t\tMockGetExternalAccountKey: func(ctx context.Context, provisionerName, keyID string) (*acme.ExternalAccountKey, error) {\n\t\t\t\t\t\treturn &acme.ExternalAccountKey{\n\t\t\t\t\t\t\tID:            \"eakID\",\n\t\t\t\t\t\t\tProvisionerID: provID,\n\t\t\t\t\t\t\tReference:     \"testeak\",\n\t\t\t\t\t\t\tCreatedAt:     createdAt,\n\t\t\t\t\t\t\tAccountID:     \"some-account-id\",\n\t\t\t\t\t\t\tHmacKey:       []byte{},\n\t\t\t\t\t\t}, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tctx: ctx,\n\t\t\t\tnar: &NewAccountRequest{\n\t\t\t\t\tContact:                []string{\"foo\", \"bar\"},\n\t\t\t\t\tExternalAccountBinding: eab,\n\t\t\t\t},\n\t\t\t\teak: nil,\n\t\t\t\terr: acme.NewError(acme.ErrorServerInternalType, \"external account binding key with id 'eakID' does not have secret bytes\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/db.GetExternalAccountKey-wrong-provisioner\": func(t *testing.T) test {\n\t\t\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\tassert.FatalError(t, err)\n\t\t\turl := fmt.Sprintf(\"%s/acme/%s/account/new-account\", baseURL.String(), escProvName)\n\t\t\trawEABJWS, err := createRawEABJWS(jwk, []byte{1, 3, 3, 7}, \"eakID\", url)\n\t\t\tassert.FatalError(t, err)\n\t\t\teab := &ExternalAccountBinding{}\n\t\t\terr = json.Unmarshal(rawEABJWS, &eab)\n\t\t\tassert.FatalError(t, err)\n\t\t\tnar := &NewAccountRequest{\n\t\t\t\tContact:                []string{\"foo\", \"bar\"},\n\t\t\t\tExternalAccountBinding: eab,\n\t\t\t}\n\t\t\tpayloadBytes, err := json.Marshal(nar)\n\t\t\tassert.FatalError(t, err)\n\t\t\tso := new(jose.SignerOptions)\n\t\t\tso.WithHeader(\"alg\", jose.SignatureAlgorithm(jwk.Algorithm))\n\t\t\tso.WithHeader(\"url\", url)\n\t\t\tsigner, err := jose.NewSigner(jose.SigningKey{\n\t\t\t\tAlgorithm: jose.SignatureAlgorithm(jwk.Algorithm),\n\t\t\t\tKey:       jwk.Key,\n\t\t\t}, so)\n\t\t\tassert.FatalError(t, err)\n\t\t\tjws, err := signer.Sign(payloadBytes)\n\t\t\tassert.FatalError(t, err)\n\t\t\traw, err := jws.CompactSerialize()\n\t\t\tassert.FatalError(t, err)\n\t\t\tparsedJWS, err := jose.ParseJWS(raw)\n\t\t\tassert.FatalError(t, err)\n\t\t\tprov := newACMEProv(t)\n\t\t\tprov.RequireEAB = true\n\t\t\tctx := context.WithValue(context.Background(), jwkContextKey, jwk)\n\t\t\tctx = acme.NewProvisionerContext(ctx, prov)\n\t\t\tctx = context.WithValue(ctx, jwsContextKey, parsedJWS)\n\t\t\treturn test{\n\t\t\t\tdb: &acme.MockDB{\n\t\t\t\t\tMockError: acme.NewError(acme.ErrorUnauthorizedType, \"name of provisioner does not match provisioner for which the EAB key was created\"),\n\t\t\t\t},\n\t\t\t\tctx: ctx,\n\t\t\t\tnar: &NewAccountRequest{\n\t\t\t\t\tContact:                []string{\"foo\", \"bar\"},\n\t\t\t\t\tExternalAccountBinding: eab,\n\t\t\t\t},\n\t\t\t\teak: nil,\n\t\t\t\terr: acme.NewError(acme.ErrorUnauthorizedType, \"the field 'kid' references an unknown key: name of provisioner does not match provisioner for which the EAB key was created\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/eab-already-bound\": func(t *testing.T) test {\n\t\t\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\tassert.FatalError(t, err)\n\t\t\turl := fmt.Sprintf(\"%s/acme/%s/account/new-account\", baseURL.String(), escProvName)\n\t\t\trawEABJWS, err := createRawEABJWS(jwk, []byte{1, 3, 3, 7}, \"eakID\", url)\n\t\t\tassert.FatalError(t, err)\n\t\t\teab := &ExternalAccountBinding{}\n\t\t\terr = json.Unmarshal(rawEABJWS, &eab)\n\t\t\tassert.FatalError(t, err)\n\t\t\tnar := &NewAccountRequest{\n\t\t\t\tContact:                []string{\"foo\", \"bar\"},\n\t\t\t\tExternalAccountBinding: eab,\n\t\t\t}\n\t\t\tpayloadBytes, err := json.Marshal(nar)\n\t\t\tassert.FatalError(t, err)\n\t\t\tso := new(jose.SignerOptions)\n\t\t\tso.WithHeader(\"alg\", jose.SignatureAlgorithm(jwk.Algorithm))\n\t\t\tso.WithHeader(\"url\", url)\n\t\t\tsigner, err := jose.NewSigner(jose.SigningKey{\n\t\t\t\tAlgorithm: jose.SignatureAlgorithm(jwk.Algorithm),\n\t\t\t\tKey:       jwk.Key,\n\t\t\t}, so)\n\t\t\tassert.FatalError(t, err)\n\t\t\tjws, err := signer.Sign(payloadBytes)\n\t\t\tassert.FatalError(t, err)\n\t\t\traw, err := jws.CompactSerialize()\n\t\t\tassert.FatalError(t, err)\n\t\t\tparsedJWS, err := jose.ParseJWS(raw)\n\t\t\tassert.FatalError(t, err)\n\t\t\tprov := newACMEProv(t)\n\t\t\tprov.RequireEAB = true\n\t\t\tctx := context.WithValue(context.Background(), jwkContextKey, jwk)\n\t\t\tctx = acme.NewProvisionerContext(ctx, prov)\n\t\t\tctx = context.WithValue(ctx, jwsContextKey, parsedJWS)\n\t\t\tcreatedAt := time.Now()\n\t\t\tboundAt := time.Now().Add(1 * time.Second)\n\t\t\treturn test{\n\t\t\t\tdb: &acme.MockDB{\n\t\t\t\t\tMockGetExternalAccountKey: func(ctx context.Context, provisionerName, keyID string) (*acme.ExternalAccountKey, error) {\n\t\t\t\t\t\treturn &acme.ExternalAccountKey{\n\t\t\t\t\t\t\tID:            \"eakID\",\n\t\t\t\t\t\t\tProvisionerID: provID,\n\t\t\t\t\t\t\tReference:     \"testeak\",\n\t\t\t\t\t\t\tCreatedAt:     createdAt,\n\t\t\t\t\t\t\tAccountID:     \"some-account-id\",\n\t\t\t\t\t\t\tHmacKey:       []byte{1, 3, 3, 7},\n\t\t\t\t\t\t\tBoundAt:       boundAt,\n\t\t\t\t\t\t}, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tctx: ctx,\n\t\t\t\tnar: &NewAccountRequest{\n\t\t\t\t\tContact:                []string{\"foo\", \"bar\"},\n\t\t\t\t\tExternalAccountBinding: eab,\n\t\t\t\t},\n\t\t\t\teak: nil,\n\t\t\t\terr: acme.NewError(acme.ErrorUnauthorizedType, \"external account binding key with id '%s' was already bound to account '%s' on %s\", \"eakID\", \"some-account-id\", boundAt),\n\t\t\t}\n\t\t},\n\t\t\"fail/eab-verify\": func(t *testing.T) test {\n\t\t\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\tassert.FatalError(t, err)\n\t\t\turl := fmt.Sprintf(\"%s/acme/%s/account/new-account\", baseURL.String(), escProvName)\n\t\t\trawEABJWS, err := createRawEABJWS(jwk, []byte{1, 3, 3, 7}, \"eakID\", url)\n\t\t\tassert.FatalError(t, err)\n\t\t\teab := &ExternalAccountBinding{}\n\t\t\terr = json.Unmarshal(rawEABJWS, &eab)\n\t\t\tassert.FatalError(t, err)\n\t\t\tnar := &NewAccountRequest{\n\t\t\t\tContact:                []string{\"foo\", \"bar\"},\n\t\t\t\tExternalAccountBinding: eab,\n\t\t\t}\n\t\t\tpayloadBytes, err := json.Marshal(nar)\n\t\t\tassert.FatalError(t, err)\n\t\t\tso := new(jose.SignerOptions)\n\t\t\tso.WithHeader(\"alg\", jose.SignatureAlgorithm(jwk.Algorithm))\n\t\t\tso.WithHeader(\"url\", url)\n\t\t\tsigner, err := jose.NewSigner(jose.SigningKey{\n\t\t\t\tAlgorithm: jose.SignatureAlgorithm(jwk.Algorithm),\n\t\t\t\tKey:       jwk.Key,\n\t\t\t}, so)\n\t\t\tassert.FatalError(t, err)\n\t\t\tjws, err := signer.Sign(payloadBytes)\n\t\t\tassert.FatalError(t, err)\n\t\t\traw, err := jws.CompactSerialize()\n\t\t\tassert.FatalError(t, err)\n\t\t\tparsedJWS, err := jose.ParseJWS(raw)\n\t\t\tassert.FatalError(t, err)\n\t\t\tprov := newACMEProv(t)\n\t\t\tprov.RequireEAB = true\n\t\t\tctx := context.WithValue(context.Background(), jwkContextKey, jwk)\n\t\t\tctx = acme.NewProvisionerContext(ctx, prov)\n\t\t\tctx = context.WithValue(ctx, jwsContextKey, parsedJWS)\n\t\t\treturn test{\n\t\t\t\tdb: &acme.MockDB{\n\t\t\t\t\tMockGetExternalAccountKey: func(ctx context.Context, provisionerName, keyID string) (*acme.ExternalAccountKey, error) {\n\t\t\t\t\t\treturn &acme.ExternalAccountKey{\n\t\t\t\t\t\t\tID:            \"eakID\",\n\t\t\t\t\t\t\tProvisionerID: provID,\n\t\t\t\t\t\t\tReference:     \"testeak\",\n\t\t\t\t\t\t\tHmacKey:       []byte{1, 2, 3, 4},\n\t\t\t\t\t\t\tCreatedAt:     time.Now(),\n\t\t\t\t\t\t}, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tctx: ctx,\n\t\t\t\tnar: &NewAccountRequest{\n\t\t\t\t\tContact:                []string{\"foo\", \"bar\"},\n\t\t\t\t\tExternalAccountBinding: eab,\n\t\t\t\t},\n\t\t\t\teak: nil,\n\t\t\t\terr: acme.NewErrorISE(\"error verifying externalAccountBinding signature\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/eab-non-matching-keys\": func(t *testing.T) test {\n\t\t\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\tassert.FatalError(t, err)\n\t\t\tdifferentJWK, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\tassert.FatalError(t, err)\n\t\t\turl := fmt.Sprintf(\"%s/acme/%s/account/new-account\", baseURL.String(), escProvName)\n\t\t\trawEABJWS, err := createRawEABJWS(differentJWK, []byte{1, 3, 3, 7}, \"eakID\", url)\n\t\t\tassert.FatalError(t, err)\n\t\t\teab := &ExternalAccountBinding{}\n\t\t\terr = json.Unmarshal(rawEABJWS, &eab)\n\t\t\tassert.FatalError(t, err)\n\t\t\tnar := &NewAccountRequest{\n\t\t\t\tContact:                []string{\"foo\", \"bar\"},\n\t\t\t\tExternalAccountBinding: eab,\n\t\t\t}\n\t\t\tpayloadBytes, err := json.Marshal(nar)\n\t\t\tassert.FatalError(t, err)\n\t\t\tso := new(jose.SignerOptions)\n\t\t\tso.WithHeader(\"alg\", jose.SignatureAlgorithm(jwk.Algorithm))\n\t\t\tso.WithHeader(\"url\", url)\n\t\t\tsigner, err := jose.NewSigner(jose.SigningKey{\n\t\t\t\tAlgorithm: jose.SignatureAlgorithm(jwk.Algorithm),\n\t\t\t\tKey:       jwk.Key,\n\t\t\t}, so)\n\t\t\tassert.FatalError(t, err)\n\t\t\tjws, err := signer.Sign(payloadBytes)\n\t\t\tassert.FatalError(t, err)\n\t\t\traw, err := jws.CompactSerialize()\n\t\t\tassert.FatalError(t, err)\n\t\t\tparsedJWS, err := jose.ParseJWS(raw)\n\t\t\tassert.FatalError(t, err)\n\t\t\tprov := newACMEProv(t)\n\t\t\tprov.RequireEAB = true\n\t\t\tctx := context.WithValue(context.Background(), jwkContextKey, jwk)\n\t\t\tctx = acme.NewProvisionerContext(ctx, prov)\n\t\t\tctx = context.WithValue(ctx, jwsContextKey, parsedJWS)\n\t\t\treturn test{\n\t\t\t\tdb: &acme.MockDB{\n\t\t\t\t\tMockGetExternalAccountKey: func(ctx context.Context, provisionerName, keyID string) (*acme.ExternalAccountKey, error) {\n\t\t\t\t\t\treturn &acme.ExternalAccountKey{\n\t\t\t\t\t\t\tID:            \"eakID\",\n\t\t\t\t\t\t\tProvisionerID: provID,\n\t\t\t\t\t\t\tReference:     \"testeak\",\n\t\t\t\t\t\t\tHmacKey:       []byte{1, 3, 3, 7},\n\t\t\t\t\t\t\tCreatedAt:     time.Now(),\n\t\t\t\t\t\t}, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tctx: ctx,\n\t\t\t\tnar: &NewAccountRequest{\n\t\t\t\t\tContact:                []string{\"foo\", \"bar\"},\n\t\t\t\t\tExternalAccountBinding: eab,\n\t\t\t\t},\n\t\t\t\teak: nil,\n\t\t\t\terr: acme.NewError(acme.ErrorUnauthorizedType, \"keys in jws and eab payload do not match\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/no-jwk\": func(t *testing.T) test {\n\t\t\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\tassert.FatalError(t, err)\n\t\t\turl := fmt.Sprintf(\"%s/acme/%s/account/new-account\", baseURL.String(), escProvName)\n\t\t\trawEABJWS, err := createRawEABJWS(jwk, []byte{1, 3, 3, 7}, \"eakID\", url)\n\t\t\tassert.FatalError(t, err)\n\t\t\teab := &ExternalAccountBinding{}\n\t\t\terr = json.Unmarshal(rawEABJWS, &eab)\n\t\t\tassert.FatalError(t, err)\n\t\t\tnar := &NewAccountRequest{\n\t\t\t\tContact:                []string{\"foo\", \"bar\"},\n\t\t\t\tExternalAccountBinding: eab,\n\t\t\t}\n\t\t\tpayloadBytes, err := json.Marshal(nar)\n\t\t\tassert.FatalError(t, err)\n\t\t\tso := new(jose.SignerOptions)\n\t\t\tso.WithHeader(\"alg\", jose.SignatureAlgorithm(jwk.Algorithm))\n\t\t\tso.WithHeader(\"url\", url)\n\t\t\tsigner, err := jose.NewSigner(jose.SigningKey{\n\t\t\t\tAlgorithm: jose.SignatureAlgorithm(jwk.Algorithm),\n\t\t\t\tKey:       jwk.Key,\n\t\t\t}, so)\n\t\t\tassert.FatalError(t, err)\n\t\t\tjws, err := signer.Sign(payloadBytes)\n\t\t\tassert.FatalError(t, err)\n\t\t\traw, err := jws.CompactSerialize()\n\t\t\tassert.FatalError(t, err)\n\t\t\tparsedJWS, err := jose.ParseJWS(raw)\n\t\t\tassert.FatalError(t, err)\n\t\t\tprov := newACMEProv(t)\n\t\t\tprov.RequireEAB = true\n\t\t\tctx := acme.NewProvisionerContext(context.Background(), prov)\n\t\t\tctx = context.WithValue(ctx, jwsContextKey, parsedJWS)\n\t\t\treturn test{\n\t\t\t\tdb: &acme.MockDB{\n\t\t\t\t\tMockGetExternalAccountKey: func(ctx context.Context, provisionerName, keyID string) (*acme.ExternalAccountKey, error) {\n\t\t\t\t\t\treturn &acme.ExternalAccountKey{\n\t\t\t\t\t\t\tID:            \"eakID\",\n\t\t\t\t\t\t\tProvisionerID: provID,\n\t\t\t\t\t\t\tReference:     \"testeak\",\n\t\t\t\t\t\t\tHmacKey:       []byte{1, 3, 3, 7},\n\t\t\t\t\t\t\tCreatedAt:     time.Now(),\n\t\t\t\t\t\t}, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tctx: ctx,\n\t\t\t\tnar: &NewAccountRequest{\n\t\t\t\t\tContact:                []string{\"foo\", \"bar\"},\n\t\t\t\t\tExternalAccountBinding: eab,\n\t\t\t\t},\n\t\t\t\teak: nil,\n\t\t\t\terr: acme.NewError(acme.ErrorServerInternalType, \"jwk expected in request context\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/nil-jwk\": func(t *testing.T) test {\n\t\t\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\tassert.FatalError(t, err)\n\t\t\turl := fmt.Sprintf(\"%s/acme/%s/account/new-account\", baseURL.String(), escProvName)\n\t\t\trawEABJWS, err := createRawEABJWS(jwk, []byte{1, 3, 3, 7}, \"eakID\", url)\n\t\t\tassert.FatalError(t, err)\n\t\t\teab := &ExternalAccountBinding{}\n\t\t\terr = json.Unmarshal(rawEABJWS, &eab)\n\t\t\tassert.FatalError(t, err)\n\t\t\tnar := &NewAccountRequest{\n\t\t\t\tContact:                []string{\"foo\", \"bar\"},\n\t\t\t\tExternalAccountBinding: eab,\n\t\t\t}\n\t\t\tpayloadBytes, err := json.Marshal(nar)\n\t\t\tassert.FatalError(t, err)\n\t\t\tso := new(jose.SignerOptions)\n\t\t\tso.WithHeader(\"alg\", jose.SignatureAlgorithm(jwk.Algorithm))\n\t\t\tso.WithHeader(\"url\", url)\n\t\t\tsigner, err := jose.NewSigner(jose.SigningKey{\n\t\t\t\tAlgorithm: jose.SignatureAlgorithm(jwk.Algorithm),\n\t\t\t\tKey:       jwk.Key,\n\t\t\t}, so)\n\t\t\tassert.FatalError(t, err)\n\t\t\tjws, err := signer.Sign(payloadBytes)\n\t\t\tassert.FatalError(t, err)\n\t\t\traw, err := jws.CompactSerialize()\n\t\t\tassert.FatalError(t, err)\n\t\t\tparsedJWS, err := jose.ParseJWS(raw)\n\t\t\tassert.FatalError(t, err)\n\t\t\tprov := newACMEProv(t)\n\t\t\tprov.RequireEAB = true\n\t\t\tctx := context.WithValue(context.Background(), jwkContextKey, nil)\n\t\t\tctx = acme.NewProvisionerContext(ctx, prov)\n\t\t\tctx = context.WithValue(ctx, jwsContextKey, parsedJWS)\n\t\t\treturn test{\n\t\t\t\tdb: &acme.MockDB{\n\t\t\t\t\tMockGetExternalAccountKey: func(ctx context.Context, provisionerName, keyID string) (*acme.ExternalAccountKey, error) {\n\t\t\t\t\t\treturn &acme.ExternalAccountKey{\n\t\t\t\t\t\t\tID:            \"eakID\",\n\t\t\t\t\t\t\tProvisionerID: provID,\n\t\t\t\t\t\t\tReference:     \"testeak\",\n\t\t\t\t\t\t\tHmacKey:       []byte{1, 3, 3, 7},\n\t\t\t\t\t\t\tCreatedAt:     time.Now(),\n\t\t\t\t\t\t}, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tctx: ctx,\n\t\t\t\tnar: &NewAccountRequest{\n\t\t\t\t\tContact:                []string{\"foo\", \"bar\"},\n\t\t\t\t\tExternalAccountBinding: eab,\n\t\t\t\t},\n\t\t\t\teak: nil,\n\t\t\t\terr: acme.NewError(acme.ErrorServerInternalType, \"jwk expected in request context\"),\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\ttc := run(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tctx := acme.NewDatabaseContext(tc.ctx, tc.db)\n\t\t\tgot, err := validateExternalAccountBinding(ctx, tc.nar)\n\t\t\twantErr := tc.err != nil\n\t\t\tgotErr := err != nil\n\t\t\tif wantErr != gotErr {\n\t\t\t\tt.Errorf(\"Handler.validateExternalAccountBinding() error = %v, want %v\", err, tc.err)\n\t\t\t}\n\t\t\tif wantErr {\n\t\t\t\tassert.NotNil(t, err)\n\t\t\t\tassert.Type(t, &acme.Error{}, err)\n\t\t\t\tvar ae *acme.Error\n\t\t\t\tif assert.True(t, errors.As(err, &ae)) {\n\t\t\t\t\tassert.Equals(t, ae.Type, tc.err.Type)\n\t\t\t\t\tassert.Equals(t, ae.Status, tc.err.Status)\n\t\t\t\t\tassert.HasPrefix(t, ae.Err.Error(), tc.err.Err.Error())\n\t\t\t\t\tassert.Equals(t, ae.Detail, tc.err.Detail)\n\t\t\t\t\tassert.Equals(t, ae.Subproblems, tc.err.Subproblems)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif got == nil {\n\t\t\t\t\tassert.Nil(t, tc.eak)\n\t\t\t\t} else {\n\t\t\t\t\tassert.NotNil(t, tc.eak)\n\t\t\t\t\tassert.Equals(t, got.ID, tc.eak.ID)\n\t\t\t\t\tassert.Equals(t, got.HmacKey, tc.eak.HmacKey)\n\t\t\t\t\tassert.Equals(t, got.ProvisionerID, tc.eak.ProvisionerID)\n\t\t\t\t\tassert.Equals(t, got.Reference, tc.eak.Reference)\n\t\t\t\t\tassert.Equals(t, got.CreatedAt, tc.eak.CreatedAt)\n\t\t\t\t\tassert.Equals(t, got.AccountID, tc.eak.AccountID)\n\t\t\t\t\tassert.Equals(t, got.BoundAt, tc.eak.BoundAt)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_validateEABJWS(t *testing.T) {\n\tacmeProv := newACMEProv(t)\n\tescProvName := url.PathEscape(acmeProv.GetName())\n\tbaseURL := &url.URL{Scheme: \"https\", Host: \"test.ca.smallstep.com\"}\n\ttype test struct {\n\t\tctx   context.Context\n\t\tjws   *jose.JSONWebSignature\n\t\tkeyID string\n\t\terr   *acme.Error\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/nil-jws\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tjws: nil,\n\t\t\t\terr: acme.NewErrorISE(\"no JWS provided\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/invalid-number-of-signatures\": func(t *testing.T) test {\n\t\t\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\tassert.FatalError(t, err)\n\t\t\turl := fmt.Sprintf(\"%s/acme/%s/account/new-account\", baseURL.String(), escProvName)\n\t\t\teabJWS, err := createEABJWS(jwk, []byte{1, 3, 3, 7}, \"eakID\", url)\n\t\t\tassert.FatalError(t, err)\n\t\t\teabJWS.Signatures = append(eabJWS.Signatures, jose.Signature{})\n\t\t\treturn test{\n\t\t\t\tjws: eabJWS,\n\t\t\t\terr: acme.NewError(acme.ErrorMalformedType, \"JWS must have one signature\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/invalid-algorithm\": func(t *testing.T) test {\n\t\t\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\tassert.FatalError(t, err)\n\t\t\turl := fmt.Sprintf(\"%s/acme/%s/account/new-account\", baseURL.String(), escProvName)\n\t\t\teabJWS, err := createEABJWS(jwk, []byte{1, 3, 3, 7}, \"eakID\", url)\n\t\t\tassert.FatalError(t, err)\n\t\t\teabJWS.Signatures[0].Protected.Algorithm = \"HS42\"\n\t\t\treturn test{\n\t\t\t\tjws: eabJWS,\n\t\t\t\terr: acme.NewError(acme.ErrorMalformedType, \"'alg' field set to invalid algorithm 'HS42'\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/kid-not-set\": func(t *testing.T) test {\n\t\t\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\tassert.FatalError(t, err)\n\t\t\turl := fmt.Sprintf(\"%s/acme/%s/account/new-account\", baseURL.String(), escProvName)\n\t\t\teabJWS, err := createEABJWS(jwk, []byte{1, 3, 3, 7}, \"eakID\", url)\n\t\t\tassert.FatalError(t, err)\n\t\t\teabJWS.Signatures[0].Protected.KeyID = \"\"\n\t\t\treturn test{\n\t\t\t\tjws: eabJWS,\n\t\t\t\terr: acme.NewError(acme.ErrorMalformedType, \"'kid' field is required\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/nonce-not-empty\": func(t *testing.T) test {\n\t\t\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\tassert.FatalError(t, err)\n\t\t\turl := fmt.Sprintf(\"%s/acme/%s/account/new-account\", baseURL.String(), escProvName)\n\t\t\teabJWS, err := createEABJWS(jwk, []byte{1, 3, 3, 7}, \"eakID\", url)\n\t\t\tassert.FatalError(t, err)\n\t\t\teabJWS.Signatures[0].Protected.Nonce = \"some-bogus-nonce\"\n\t\t\treturn test{\n\t\t\t\tjws: eabJWS,\n\t\t\t\terr: acme.NewError(acme.ErrorMalformedType, \"'nonce' must not be present\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/url-not-set\": func(t *testing.T) test {\n\t\t\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\tassert.FatalError(t, err)\n\t\t\turl := fmt.Sprintf(\"%s/acme/%s/account/new-account\", baseURL.String(), escProvName)\n\t\t\teabJWS, err := createEABJWS(jwk, []byte{1, 3, 3, 7}, \"eakID\", url)\n\t\t\tassert.FatalError(t, err)\n\t\t\tdelete(eabJWS.Signatures[0].Protected.ExtraHeaders, \"url\")\n\t\t\treturn test{\n\t\t\t\tjws: eabJWS,\n\t\t\t\terr: acme.NewError(acme.ErrorMalformedType, \"'url' field is required\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/no-outer-jws\": func(t *testing.T) test {\n\t\t\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\tassert.FatalError(t, err)\n\t\t\turl := fmt.Sprintf(\"%s/acme/%s/account/new-account\", baseURL.String(), escProvName)\n\t\t\teabJWS, err := createEABJWS(jwk, []byte{1, 3, 3, 7}, \"eakID\", url)\n\t\t\tassert.FatalError(t, err)\n\t\t\tctx := context.WithValue(context.TODO(), jwsContextKey, nil)\n\t\t\treturn test{\n\t\t\t\tctx: ctx,\n\t\t\t\tjws: eabJWS,\n\t\t\t\terr: acme.NewErrorISE(\"could not retrieve outer JWS from context\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/outer-jws-multiple-signatures\": func(t *testing.T) test {\n\t\t\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\tassert.FatalError(t, err)\n\t\t\turl := fmt.Sprintf(\"%s/acme/%s/account/new-account\", baseURL.String(), escProvName)\n\t\t\teabJWS, err := createEABJWS(jwk, []byte{1, 3, 3, 7}, \"eakID\", url)\n\t\t\tassert.FatalError(t, err)\n\t\t\trawEABJWS := eabJWS.FullSerialize()\n\t\t\tassert.FatalError(t, err)\n\t\t\teab := &ExternalAccountBinding{}\n\t\t\terr = json.Unmarshal([]byte(rawEABJWS), &eab)\n\t\t\tassert.FatalError(t, err)\n\t\t\tnar := &NewAccountRequest{\n\t\t\t\tContact:                []string{\"foo\", \"bar\"},\n\t\t\t\tExternalAccountBinding: eab,\n\t\t\t}\n\t\t\tpayloadBytes, err := json.Marshal(nar)\n\t\t\tassert.FatalError(t, err)\n\t\t\tso := new(jose.SignerOptions)\n\t\t\tso.WithHeader(\"alg\", jose.SignatureAlgorithm(jwk.Algorithm))\n\t\t\tsigner, err := jose.NewSigner(jose.SigningKey{\n\t\t\t\tAlgorithm: jose.SignatureAlgorithm(jwk.Algorithm),\n\t\t\t\tKey:       jwk.Key,\n\t\t\t}, so)\n\t\t\tassert.FatalError(t, err)\n\t\t\tjws, err := signer.Sign(payloadBytes)\n\t\t\tassert.FatalError(t, err)\n\t\t\traw, err := jws.CompactSerialize()\n\t\t\tassert.FatalError(t, err)\n\t\t\touterJWS, err := jose.ParseJWS(raw)\n\t\t\tassert.FatalError(t, err)\n\t\t\touterJWS.Signatures = append(outerJWS.Signatures, jose.Signature{})\n\t\t\tctx := context.WithValue(context.TODO(), jwsContextKey, outerJWS)\n\t\t\treturn test{\n\t\t\t\tctx: ctx,\n\t\t\t\tjws: eabJWS,\n\t\t\t\terr: acme.NewError(acme.ErrorMalformedType, \"outer JWS must have one signature\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/outer-jws-no-url\": func(t *testing.T) test {\n\t\t\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\tassert.FatalError(t, err)\n\t\t\turl := fmt.Sprintf(\"%s/acme/%s/account/new-account\", baseURL.String(), escProvName)\n\t\t\teabJWS, err := createEABJWS(jwk, []byte{1, 3, 3, 7}, \"eakID\", url)\n\t\t\tassert.FatalError(t, err)\n\t\t\trawEABJWS := eabJWS.FullSerialize()\n\t\t\tassert.FatalError(t, err)\n\t\t\teab := &ExternalAccountBinding{}\n\t\t\terr = json.Unmarshal([]byte(rawEABJWS), &eab)\n\t\t\tassert.FatalError(t, err)\n\t\t\tnar := &NewAccountRequest{\n\t\t\t\tContact:                []string{\"foo\", \"bar\"},\n\t\t\t\tExternalAccountBinding: eab,\n\t\t\t}\n\t\t\tpayloadBytes, err := json.Marshal(nar)\n\t\t\tassert.FatalError(t, err)\n\t\t\tso := new(jose.SignerOptions)\n\t\t\tso.WithHeader(\"alg\", jose.SignatureAlgorithm(jwk.Algorithm))\n\t\t\tsigner, err := jose.NewSigner(jose.SigningKey{\n\t\t\t\tAlgorithm: jose.SignatureAlgorithm(jwk.Algorithm),\n\t\t\t\tKey:       jwk.Key,\n\t\t\t}, so)\n\t\t\tassert.FatalError(t, err)\n\t\t\tjws, err := signer.Sign(payloadBytes)\n\t\t\tassert.FatalError(t, err)\n\t\t\traw, err := jws.CompactSerialize()\n\t\t\tassert.FatalError(t, err)\n\t\t\touterJWS, err := jose.ParseJWS(raw)\n\t\t\tassert.FatalError(t, err)\n\t\t\tctx := context.WithValue(context.TODO(), jwsContextKey, outerJWS)\n\t\t\treturn test{\n\t\t\t\tctx: ctx,\n\t\t\t\tjws: eabJWS,\n\t\t\t\terr: acme.NewError(acme.ErrorMalformedType, \"'url' field must be set in outer JWS\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/outer-jws-with-different-url\": func(t *testing.T) test {\n\t\t\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\tassert.FatalError(t, err)\n\t\t\turl := fmt.Sprintf(\"%s/acme/%s/account/new-account\", baseURL.String(), escProvName)\n\t\t\teabJWS, err := createEABJWS(jwk, []byte{1, 3, 3, 7}, \"eakID\", url)\n\t\t\tassert.FatalError(t, err)\n\t\t\trawEABJWS := eabJWS.FullSerialize()\n\t\t\tassert.FatalError(t, err)\n\t\t\teab := &ExternalAccountBinding{}\n\t\t\terr = json.Unmarshal([]byte(rawEABJWS), &eab)\n\t\t\tassert.FatalError(t, err)\n\t\t\tnar := &NewAccountRequest{\n\t\t\t\tContact:                []string{\"foo\", \"bar\"},\n\t\t\t\tExternalAccountBinding: eab,\n\t\t\t}\n\t\t\tpayloadBytes, err := json.Marshal(nar)\n\t\t\tassert.FatalError(t, err)\n\t\t\tso := new(jose.SignerOptions)\n\t\t\tso.WithHeader(\"alg\", jose.SignatureAlgorithm(jwk.Algorithm))\n\t\t\tso.WithHeader(\"url\", \"this-is-not-the-same-url-as-in-the-eab-jws\")\n\t\t\tsigner, err := jose.NewSigner(jose.SigningKey{\n\t\t\t\tAlgorithm: jose.SignatureAlgorithm(jwk.Algorithm),\n\t\t\t\tKey:       jwk.Key,\n\t\t\t}, so)\n\t\t\tassert.FatalError(t, err)\n\t\t\tjws, err := signer.Sign(payloadBytes)\n\t\t\tassert.FatalError(t, err)\n\t\t\traw, err := jws.CompactSerialize()\n\t\t\tassert.FatalError(t, err)\n\t\t\touterJWS, err := jose.ParseJWS(raw)\n\t\t\tassert.FatalError(t, err)\n\t\t\tctx := context.WithValue(context.TODO(), jwsContextKey, outerJWS)\n\t\t\treturn test{\n\t\t\t\tctx: ctx,\n\t\t\t\tjws: eabJWS,\n\t\t\t\terr: acme.NewError(acme.ErrorMalformedType, \"'url' field is not the same value as the outer JWS\"),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\tassert.FatalError(t, err)\n\t\t\turl := fmt.Sprintf(\"%s/acme/%s/account/new-account\", baseURL.String(), escProvName)\n\t\t\teabJWS, err := createEABJWS(jwk, []byte{1, 3, 3, 7}, \"eakID\", url)\n\t\t\tassert.FatalError(t, err)\n\t\t\trawEABJWS := eabJWS.FullSerialize()\n\t\t\tassert.FatalError(t, err)\n\t\t\teab := &ExternalAccountBinding{}\n\t\t\terr = json.Unmarshal([]byte(rawEABJWS), &eab)\n\t\t\tassert.FatalError(t, err)\n\t\t\tnar := &NewAccountRequest{\n\t\t\t\tContact:                []string{\"foo\", \"bar\"},\n\t\t\t\tExternalAccountBinding: eab,\n\t\t\t}\n\t\t\tpayloadBytes, err := json.Marshal(nar)\n\t\t\tassert.FatalError(t, err)\n\t\t\tso := new(jose.SignerOptions)\n\t\t\tso.WithHeader(\"alg\", jose.SignatureAlgorithm(jwk.Algorithm))\n\t\t\tso.WithHeader(\"url\", url)\n\t\t\tsigner, err := jose.NewSigner(jose.SigningKey{\n\t\t\t\tAlgorithm: jose.SignatureAlgorithm(jwk.Algorithm),\n\t\t\t\tKey:       jwk.Key,\n\t\t\t}, so)\n\t\t\tassert.FatalError(t, err)\n\t\t\tjws, err := signer.Sign(payloadBytes)\n\t\t\tassert.FatalError(t, err)\n\t\t\traw, err := jws.CompactSerialize()\n\t\t\tassert.FatalError(t, err)\n\t\t\touterJWS, err := jose.ParseJWS(raw)\n\t\t\tassert.FatalError(t, err)\n\t\t\tctx := context.WithValue(context.TODO(), jwsContextKey, outerJWS)\n\t\t\treturn test{\n\t\t\t\tctx:   ctx,\n\t\t\t\tjws:   eabJWS,\n\t\t\t\tkeyID: \"eakID\",\n\t\t\t\terr:   nil,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, prep := range tests {\n\t\ttc := prep(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tkeyID, err := validateEABJWS(tc.ctx, tc.jws)\n\t\t\twantErr := tc.err != nil\n\t\t\tgotErr := err != nil\n\t\t\tif wantErr != gotErr {\n\t\t\t\tt.Errorf(\"validateEABJWS() error = %v, want %v\", err, tc.err)\n\t\t\t}\n\t\t\tif wantErr {\n\t\t\t\tassert.NotNil(t, err)\n\t\t\t\tassert.Equals(t, tc.err.Type, err.Type)\n\t\t\t\tassert.Equals(t, tc.err.Status, err.Status)\n\t\t\t\tassert.HasPrefix(t, err.Err.Error(), tc.err.Err.Error())\n\t\t\t\tassert.Equals(t, tc.err.Detail, err.Detail)\n\t\t\t\tassert.Equals(t, tc.err.Subproblems, err.Subproblems)\n\t\t\t} else {\n\t\t\t\tassert.Nil(t, err)\n\t\t\t\tassert.Equals(t, tc.keyID, keyID)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "acme/api/handler.go",
    "content": "package api\n\nimport (\n\t\"context\"\n\t\"crypto/x509\"\n\t\"encoding/json\"\n\t\"encoding/pem\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/go-chi/chi/v5\"\n\n\t\"github.com/smallstep/certificates/acme\"\n\t\"github.com/smallstep/certificates/api\"\n\t\"github.com/smallstep/certificates/api/render\"\n\t\"github.com/smallstep/certificates/authority\"\n\t\"github.com/smallstep/certificates/authority/provisioner\"\n)\n\nfunc link(url, typ string) string {\n\treturn fmt.Sprintf(\"<%s>;rel=%q\", url, typ)\n}\n\n// Clock that returns time in UTC rounded to seconds.\ntype Clock struct{}\n\n// Now returns the UTC time rounded to seconds.\nfunc (c *Clock) Now() time.Time {\n\treturn time.Now().UTC().Truncate(time.Second)\n}\n\nvar clock Clock\n\ntype payloadInfo struct {\n\tvalue       []byte\n\tisPostAsGet bool\n\tisEmptyJSON bool\n}\n\n// HandlerOptions required to create a new ACME API request handler.\ntype HandlerOptions struct {\n\t// DB storage backend that implements the acme.DB interface.\n\t//\n\t// Deprecated: use acme.NewContex(context.Context, acme.DB)\n\tDB acme.DB\n\n\t// CA is the certificate authority interface.\n\t//\n\t// Deprecated: use authority.NewContext(context.Context, *authority.Authority)\n\tCA acme.CertificateAuthority\n\n\t// Backdate is the duration that the CA will subtract from the current time\n\t// to set the NotBefore in the certificate.\n\tBackdate provisioner.Duration\n\n\t// DNS the host used to generate accurate ACME links. By default the authority\n\t// will use the Host from the request, so this value will only be used if\n\t// request.Host is empty.\n\tDNS string\n\n\t// Prefix is a URL path prefix under which the ACME api is served. This\n\t// prefix is required to generate accurate ACME links.\n\t// E.g. https://ca.smallstep.com/acme/my-acme-provisioner/new-account --\n\t// \"acme\" is the prefix from which the ACME api is accessed.\n\tPrefix string\n\n\t// PrerequisitesChecker checks if all prerequisites for serving ACME are\n\t// met by the CA configuration.\n\tPrerequisitesChecker func(ctx context.Context) (bool, error)\n}\n\nvar mustAuthority = func(ctx context.Context) acme.CertificateAuthority {\n\treturn authority.MustFromContext(ctx)\n}\n\n// handler is the ACME API request handler.\ntype handler struct {\n\topts *HandlerOptions\n}\n\n// Route traffic and implement the Router interface. For backward compatibility\n// this route adds will add a new middleware that will set the ACME components\n// on the context.\n//\n// Note: this method is deprecated in step-ca, other applications can still use\n// this to support ACME, but the recommendation is to use use\n// api.Route(api.Router) and acme.NewContext() instead.\nfunc (h *handler) Route(r api.Router) {\n\tclient := acme.NewClient()\n\tlinker := acme.NewLinker(h.opts.DNS, h.opts.Prefix)\n\troute(r, func(next nextHTTP) nextHTTP {\n\t\treturn func(w http.ResponseWriter, r *http.Request) {\n\t\t\tctx := r.Context()\n\t\t\tif ca, ok := h.opts.CA.(*authority.Authority); ok && ca != nil {\n\t\t\t\tctx = authority.NewContext(ctx, ca)\n\t\t\t}\n\t\t\tctx = acme.NewContext(ctx, h.opts.DB, client, linker, h.opts.PrerequisitesChecker)\n\t\t\tnext(w, r.WithContext(ctx))\n\t\t}\n\t})\n}\n\n// NewHandler returns a new ACME API handler.\n//\n// Note: this method is deprecated in step-ca, other applications can still use\n// this to support ACME, but the recommendation is to use use\n// api.Route(api.Router) and acme.NewContext() instead.\nfunc NewHandler(opts HandlerOptions) api.RouterHandler {\n\treturn &handler{\n\t\topts: &opts,\n\t}\n}\n\n// Route traffic and implement the Router interface. This method requires that\n// all the acme components, authority, db, client, linker, and prerequisite\n// checker to be present in the context.\nfunc Route(r api.Router) {\n\troute(r, nil)\n}\n\nfunc route(r api.Router, middleware func(next nextHTTP) nextHTTP) {\n\tcommonMiddleware := func(next nextHTTP) nextHTTP {\n\t\thandler := func(w http.ResponseWriter, r *http.Request) {\n\t\t\t// Linker middleware gets the provisioner and current url from the\n\t\t\t// request and sets them in the context.\n\t\t\tlinker := acme.MustLinkerFromContext(r.Context())\n\t\t\tlinker.Middleware(http.HandlerFunc(checkPrerequisites(next))).ServeHTTP(w, r)\n\t\t}\n\t\tif middleware != nil {\n\t\t\thandler = middleware(handler)\n\t\t}\n\t\treturn handler\n\t}\n\tvalidatingMiddleware := func(next nextHTTP) nextHTTP {\n\t\treturn commonMiddleware(addNonce(addDirLink(verifyContentType(parseJWS(validateJWS(next))))))\n\t}\n\textractPayloadByJWK := func(next nextHTTP) nextHTTP {\n\t\treturn validatingMiddleware(extractJWK(verifyAndExtractJWSPayload(next)))\n\t}\n\textractPayloadByKid := func(next nextHTTP) nextHTTP {\n\t\treturn validatingMiddleware(lookupJWK(verifyAndExtractJWSPayload(next)))\n\t}\n\textractPayloadByKidOrJWK := func(next nextHTTP) nextHTTP {\n\t\treturn validatingMiddleware(extractOrLookupJWK(verifyAndExtractJWSPayload(next)))\n\t}\n\n\tgetPath := acme.GetUnescapedPathSuffix\n\n\t// Standard ACME API\n\tr.MethodFunc(\"GET\", getPath(acme.NewNonceLinkType, \"{provisionerID}\"),\n\t\tcommonMiddleware(addNonce(addDirLink(GetNonce))))\n\tr.MethodFunc(\"HEAD\", getPath(acme.NewNonceLinkType, \"{provisionerID}\"),\n\t\tcommonMiddleware(addNonce(addDirLink(GetNonce))))\n\tr.MethodFunc(\"GET\", getPath(acme.DirectoryLinkType, \"{provisionerID}\"),\n\t\tcommonMiddleware(GetDirectory))\n\tr.MethodFunc(\"HEAD\", getPath(acme.DirectoryLinkType, \"{provisionerID}\"),\n\t\tcommonMiddleware(GetDirectory))\n\n\tr.MethodFunc(\"POST\", getPath(acme.NewAccountLinkType, \"{provisionerID}\"),\n\t\textractPayloadByJWK(NewAccount))\n\tr.MethodFunc(\"POST\", getPath(acme.AccountLinkType, \"{provisionerID}\", \"{accID}\"),\n\t\textractPayloadByKid(GetOrUpdateAccount))\n\tr.MethodFunc(\"POST\", getPath(acme.KeyChangeLinkType, \"{provisionerID}\", \"{accID}\"),\n\t\textractPayloadByKid(NotImplemented))\n\tr.MethodFunc(\"POST\", getPath(acme.NewOrderLinkType, \"{provisionerID}\"),\n\t\textractPayloadByKid(NewOrder))\n\tr.MethodFunc(\"POST\", getPath(acme.OrderLinkType, \"{provisionerID}\", \"{ordID}\"),\n\t\textractPayloadByKid(isPostAsGet(GetOrder)))\n\tr.MethodFunc(\"POST\", getPath(acme.OrdersByAccountLinkType, \"{provisionerID}\", \"{accID}\"),\n\t\textractPayloadByKid(isPostAsGet(GetOrdersByAccountID)))\n\tr.MethodFunc(\"POST\", getPath(acme.FinalizeLinkType, \"{provisionerID}\", \"{ordID}\"),\n\t\textractPayloadByKid(FinalizeOrder))\n\tr.MethodFunc(\"POST\", getPath(acme.AuthzLinkType, \"{provisionerID}\", \"{authzID}\"),\n\t\textractPayloadByKid(isPostAsGet(GetAuthorization)))\n\tr.MethodFunc(\"POST\", getPath(acme.ChallengeLinkType, \"{provisionerID}\", \"{authzID}\", \"{chID}\"),\n\t\textractPayloadByKid(GetChallenge))\n\tr.MethodFunc(\"POST\", getPath(acme.CertificateLinkType, \"{provisionerID}\", \"{certID}\"),\n\t\textractPayloadByKid(isPostAsGet(GetCertificate)))\n\tr.MethodFunc(\"POST\", getPath(acme.RevokeCertLinkType, \"{provisionerID}\"),\n\t\textractPayloadByKidOrJWK(RevokeCert))\n}\n\n// GetNonce just sets the right header since a Nonce is added to each response\n// by middleware by default.\nfunc GetNonce(w http.ResponseWriter, r *http.Request) {\n\tif r.Method == \"HEAD\" {\n\t\tw.WriteHeader(http.StatusOK)\n\t} else {\n\t\tw.WriteHeader(http.StatusNoContent)\n\t}\n}\n\ntype Meta struct {\n\tTermsOfService          string   `json:\"termsOfService,omitempty\"`\n\tWebsite                 string   `json:\"website,omitempty\"`\n\tCaaIdentities           []string `json:\"caaIdentities,omitempty\"`\n\tExternalAccountRequired bool     `json:\"externalAccountRequired,omitempty\"`\n}\n\n// Directory represents an ACME directory for configuring clients.\ntype Directory struct {\n\tNewNonce   string `json:\"newNonce\"`\n\tNewAccount string `json:\"newAccount\"`\n\tNewOrder   string `json:\"newOrder\"`\n\tRevokeCert string `json:\"revokeCert\"`\n\tKeyChange  string `json:\"keyChange\"`\n\tMeta       *Meta  `json:\"meta,omitempty\"`\n}\n\n// ToLog enables response logging for the Directory type.\nfunc (d *Directory) ToLog() (interface{}, error) {\n\tb, err := json.Marshal(d)\n\tif err != nil {\n\t\treturn nil, acme.WrapErrorISE(err, \"error marshaling directory for logging\")\n\t}\n\treturn string(b), nil\n}\n\n// GetDirectory is the ACME resource for returning a directory configuration\n// for client configuration.\nfunc GetDirectory(w http.ResponseWriter, r *http.Request) {\n\tctx := r.Context()\n\tacmeProv, err := acmeProvisionerFromContext(ctx)\n\tif err != nil {\n\t\trender.Error(w, r, err)\n\t\treturn\n\t}\n\n\tlinker := acme.MustLinkerFromContext(ctx)\n\n\trender.JSON(w, r, &Directory{\n\t\tNewNonce:   linker.GetLink(ctx, acme.NewNonceLinkType),\n\t\tNewAccount: linker.GetLink(ctx, acme.NewAccountLinkType),\n\t\tNewOrder:   linker.GetLink(ctx, acme.NewOrderLinkType),\n\t\tRevokeCert: linker.GetLink(ctx, acme.RevokeCertLinkType),\n\t\tKeyChange:  linker.GetLink(ctx, acme.KeyChangeLinkType),\n\t\tMeta:       createMetaObject(acmeProv),\n\t})\n}\n\n// createMetaObject creates a Meta object if the ACME provisioner\n// has one or more properties that are written in the ACME directory output.\n// It returns nil if none of the properties are set.\nfunc createMetaObject(p *provisioner.ACME) *Meta {\n\tif shouldAddMetaObject(p) {\n\t\treturn &Meta{\n\t\t\tTermsOfService:          p.TermsOfService,\n\t\t\tWebsite:                 p.Website,\n\t\t\tCaaIdentities:           p.CaaIdentities,\n\t\t\tExternalAccountRequired: p.RequireEAB,\n\t\t}\n\t}\n\treturn nil\n}\n\n// shouldAddMetaObject returns whether or not the ACME provisioner\n// has properties configured that must be added to the ACME directory object.\nfunc shouldAddMetaObject(p *provisioner.ACME) bool {\n\tswitch {\n\tcase p.TermsOfService != \"\":\n\t\treturn true\n\tcase p.Website != \"\":\n\t\treturn true\n\tcase len(p.CaaIdentities) > 0:\n\t\treturn true\n\tcase p.RequireEAB:\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\n// NotImplemented returns a 501 and is generally a placeholder for functionality which\n// MAY be added at some point in the future but is not in any way a guarantee of such.\nfunc NotImplemented(w http.ResponseWriter, r *http.Request) {\n\trender.Error(w, r, acme.NewError(acme.ErrorNotImplementedType, \"this API is not implemented\"))\n}\n\n// GetAuthorization ACME api for retrieving an Authz.\nfunc GetAuthorization(w http.ResponseWriter, r *http.Request) {\n\tctx := r.Context()\n\tdb := acme.MustDatabaseFromContext(ctx)\n\tlinker := acme.MustLinkerFromContext(ctx)\n\n\tacc, err := accountFromContext(ctx)\n\tif err != nil {\n\t\trender.Error(w, r, err)\n\t\treturn\n\t}\n\taz, err := db.GetAuthorization(ctx, chi.URLParam(r, \"authzID\"))\n\tif err != nil {\n\t\trender.Error(w, r, acme.WrapErrorISE(err, \"error retrieving authorization\"))\n\t\treturn\n\t}\n\tif acc.ID != az.AccountID {\n\t\trender.Error(w, r, acme.NewError(acme.ErrorUnauthorizedType,\n\t\t\t\"account '%s' does not own authorization '%s'\", acc.ID, az.ID))\n\t\treturn\n\t}\n\tif err = az.UpdateStatus(ctx, db); err != nil {\n\t\trender.Error(w, r, acme.WrapErrorISE(err, \"error updating authorization status\"))\n\t\treturn\n\t}\n\n\tlinker.LinkAuthorization(ctx, az)\n\n\tw.Header().Set(\"Location\", linker.GetLink(ctx, acme.AuthzLinkType, az.ID))\n\trender.JSON(w, r, az)\n}\n\n// GetChallenge ACME api for retrieving a Challenge.\nfunc GetChallenge(w http.ResponseWriter, r *http.Request) {\n\tctx := r.Context()\n\tdb := acme.MustDatabaseFromContext(ctx)\n\tlinker := acme.MustLinkerFromContext(ctx)\n\n\tacc, err := accountFromContext(ctx)\n\tif err != nil {\n\t\trender.Error(w, r, err)\n\t\treturn\n\t}\n\n\tpayload, err := payloadFromContext(ctx)\n\tif err != nil {\n\t\trender.Error(w, r, err)\n\t\treturn\n\t}\n\n\t// NOTE: We should be checking that the request is either a POST-as-GET, or\n\t// that for all challenges except for device-attest-01, the payload is an\n\t// empty JSON block ({}). However, older ACME clients still send a vestigial\n\t// body (rather than an empty JSON block) and strict enforcement would\n\t// render these clients broken.\n\n\tazID := chi.URLParam(r, \"authzID\")\n\tch, err := db.GetChallenge(ctx, chi.URLParam(r, \"chID\"), azID)\n\tif err != nil {\n\t\trender.Error(w, r, acme.WrapErrorISE(err, \"error retrieving challenge\"))\n\t\treturn\n\t}\n\tch.AuthorizationID = azID\n\tif acc.ID != ch.AccountID {\n\t\trender.Error(w, r, acme.NewError(acme.ErrorUnauthorizedType,\n\t\t\t\"account '%s' does not own challenge '%s'\", acc.ID, ch.ID))\n\t\treturn\n\t}\n\tjwk, err := jwkFromContext(ctx)\n\tif err != nil {\n\t\trender.Error(w, r, err)\n\t\treturn\n\t}\n\tif err = ch.Validate(ctx, db, jwk, payload.value); err != nil {\n\t\trender.Error(w, r, acme.WrapErrorISE(err, \"error validating challenge\"))\n\t\treturn\n\t}\n\n\tlinker.LinkChallenge(ctx, ch, azID)\n\n\tw.Header().Add(\"Link\", link(linker.GetLink(ctx, acme.AuthzLinkType, azID), \"up\"))\n\tw.Header().Set(\"Location\", linker.GetLink(ctx, acme.ChallengeLinkType, azID, ch.ID))\n\trender.JSON(w, r, ch)\n}\n\n// GetCertificate ACME api for retrieving a Certificate.\nfunc GetCertificate(w http.ResponseWriter, r *http.Request) {\n\tctx := r.Context()\n\tdb := acme.MustDatabaseFromContext(ctx)\n\n\tacc, err := accountFromContext(ctx)\n\tif err != nil {\n\t\trender.Error(w, r, err)\n\t\treturn\n\t}\n\n\tcertID := chi.URLParam(r, \"certID\")\n\tcert, err := db.GetCertificate(ctx, certID)\n\tif err != nil {\n\t\trender.Error(w, r, acme.WrapErrorISE(err, \"error retrieving certificate\"))\n\t\treturn\n\t}\n\tif cert.AccountID != acc.ID {\n\t\trender.Error(w, r, acme.NewError(acme.ErrorUnauthorizedType,\n\t\t\t\"account '%s' does not own certificate '%s'\", acc.ID, certID))\n\t\treturn\n\t}\n\n\tvar certBytes []byte\n\tfor _, c := range append([]*x509.Certificate{cert.Leaf}, cert.Intermediates...) {\n\t\tcertBytes = append(certBytes, pem.EncodeToMemory(&pem.Block{\n\t\t\tType:  \"CERTIFICATE\",\n\t\t\tBytes: c.Raw,\n\t\t})...)\n\t}\n\n\tapi.LogCertificate(w, cert.Leaf)\n\tw.Header().Set(\"Content-Type\", \"application/pem-certificate-chain\")\n\tw.Write(certBytes)\n}\n"
  },
  {
    "path": "acme/api/handler_test.go",
    "content": "package api\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"encoding/json\"\n\t\"encoding/pem\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-chi/chi/v5\"\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/pkg/errors\"\n\n\t\"go.step.sm/crypto/jose\"\n\t\"go.step.sm/crypto/pemutil\"\n\n\t\"github.com/smallstep/assert\"\n\t\"github.com/smallstep/certificates/acme\"\n\t\"github.com/smallstep/certificates/authority/provisioner\"\n)\n\ntype mockClient struct {\n\tget       func(url string) (*http.Response, error)\n\tlookupTxt func(name string) ([]string, error)\n\ttlsDial   func(network, addr string, config *tls.Config) (*tls.Conn, error)\n}\n\nfunc (m *mockClient) Get(u string) (*http.Response, error)    { return m.get(u) }\nfunc (m *mockClient) LookupTxt(name string) ([]string, error) { return m.lookupTxt(name) }\nfunc (m *mockClient) TLSDial(network, addr string, config *tls.Config) (*tls.Conn, error) {\n\treturn m.tlsDial(network, addr, config)\n}\n\nfunc mockMustAuthority(t *testing.T, a acme.CertificateAuthority) {\n\tt.Helper()\n\tfn := mustAuthority\n\tt.Cleanup(func() {\n\t\tmustAuthority = fn\n\t})\n\tmustAuthority = func(ctx context.Context) acme.CertificateAuthority {\n\t\treturn a\n\t}\n}\n\nfunc TestHandler_GetNonce(t *testing.T) {\n\ttests := []struct {\n\t\tname       string\n\t\tstatusCode int\n\t}{\n\t\t{\"GET\", 204},\n\t\t{\"HEAD\", 200},\n\t}\n\n\t// Request with chi context\n\treq := httptest.NewRequest(\"GET\", \"http://ca.smallstep.com/nonce\", http.NoBody)\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\t// h := &Handler{}\n\t\t\tw := httptest.NewRecorder()\n\t\t\treq.Method = tt.name\n\t\t\tGetNonce(w, req)\n\t\t\tres := w.Result()\n\n\t\t\tif res.StatusCode != tt.statusCode {\n\t\t\t\tt.Errorf(\"Handler.GetNonce StatusCode = %d, wants %d\", res.StatusCode, tt.statusCode)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestHandler_GetDirectory(t *testing.T) {\n\tlinker := acme.NewLinker(\"ca.smallstep.com\", \"acme\")\n\t_ = linker\n\ttype test struct {\n\t\tctx        context.Context\n\t\tstatusCode int\n\t\tdir        Directory\n\t\terr        *acme.Error\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/no-provisioner\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tctx:        context.Background(),\n\t\t\t\tstatusCode: 500,\n\t\t\t\terr:        acme.NewErrorISE(\"provisioner is not in context\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/different-provisioner\": func(t *testing.T) test {\n\t\t\tctx := acme.NewProvisionerContext(context.Background(), &fakeProvisioner{})\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 500,\n\t\t\t\terr:        acme.NewErrorISE(\"provisioner in context is not an ACME provisioner\"),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tprov := newProv()\n\t\t\tprovName := url.PathEscape(prov.GetName())\n\t\t\tbaseURL := &url.URL{Scheme: \"https\", Host: \"test.ca.smallstep.com\"}\n\t\t\tctx := acme.NewProvisionerContext(context.Background(), prov)\n\t\t\texpDir := Directory{\n\t\t\t\tNewNonce:   fmt.Sprintf(\"%s/acme/%s/new-nonce\", baseURL.String(), provName),\n\t\t\t\tNewAccount: fmt.Sprintf(\"%s/acme/%s/new-account\", baseURL.String(), provName),\n\t\t\t\tNewOrder:   fmt.Sprintf(\"%s/acme/%s/new-order\", baseURL.String(), provName),\n\t\t\t\tRevokeCert: fmt.Sprintf(\"%s/acme/%s/revoke-cert\", baseURL.String(), provName),\n\t\t\t\tKeyChange:  fmt.Sprintf(\"%s/acme/%s/key-change\", baseURL.String(), provName),\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\tdir:        expDir,\n\t\t\t\tstatusCode: 200,\n\t\t\t}\n\t\t},\n\t\t\"ok/eab-required\": func(t *testing.T) test {\n\t\t\tprov := newACMEProv(t)\n\t\t\tprov.RequireEAB = true\n\t\t\tprovName := url.PathEscape(prov.GetName())\n\t\t\tbaseURL := &url.URL{Scheme: \"https\", Host: \"test.ca.smallstep.com\"}\n\t\t\tctx := acme.NewProvisionerContext(context.Background(), prov)\n\t\t\texpDir := Directory{\n\t\t\t\tNewNonce:   fmt.Sprintf(\"%s/acme/%s/new-nonce\", baseURL.String(), provName),\n\t\t\t\tNewAccount: fmt.Sprintf(\"%s/acme/%s/new-account\", baseURL.String(), provName),\n\t\t\t\tNewOrder:   fmt.Sprintf(\"%s/acme/%s/new-order\", baseURL.String(), provName),\n\t\t\t\tRevokeCert: fmt.Sprintf(\"%s/acme/%s/revoke-cert\", baseURL.String(), provName),\n\t\t\t\tKeyChange:  fmt.Sprintf(\"%s/acme/%s/key-change\", baseURL.String(), provName),\n\t\t\t\tMeta: &Meta{\n\t\t\t\t\tExternalAccountRequired: true,\n\t\t\t\t},\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\tdir:        expDir,\n\t\t\t\tstatusCode: 200,\n\t\t\t}\n\t\t},\n\t\t\"ok/full-meta\": func(t *testing.T) test {\n\t\t\tprov := newACMEProv(t)\n\t\t\tprov.TermsOfService = \"https://terms.ca.local/\"\n\t\t\tprov.Website = \"https://ca.local/\"\n\t\t\tprov.CaaIdentities = []string{\"ca.local\"}\n\t\t\tprov.RequireEAB = true\n\t\t\tprovName := url.PathEscape(prov.GetName())\n\t\t\tbaseURL := &url.URL{Scheme: \"https\", Host: \"test.ca.smallstep.com\"}\n\t\t\tctx := acme.NewProvisionerContext(context.Background(), prov)\n\t\t\texpDir := Directory{\n\t\t\t\tNewNonce:   fmt.Sprintf(\"%s/acme/%s/new-nonce\", baseURL.String(), provName),\n\t\t\t\tNewAccount: fmt.Sprintf(\"%s/acme/%s/new-account\", baseURL.String(), provName),\n\t\t\t\tNewOrder:   fmt.Sprintf(\"%s/acme/%s/new-order\", baseURL.String(), provName),\n\t\t\t\tRevokeCert: fmt.Sprintf(\"%s/acme/%s/revoke-cert\", baseURL.String(), provName),\n\t\t\t\tKeyChange:  fmt.Sprintf(\"%s/acme/%s/key-change\", baseURL.String(), provName),\n\t\t\t\tMeta: &Meta{\n\t\t\t\t\tTermsOfService:          \"https://terms.ca.local/\",\n\t\t\t\t\tWebsite:                 \"https://ca.local/\",\n\t\t\t\t\tCaaIdentities:           []string{\"ca.local\"},\n\t\t\t\t\tExternalAccountRequired: true,\n\t\t\t\t},\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\tdir:        expDir,\n\t\t\t\tstatusCode: 200,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\ttc := run(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tctx := acme.NewLinkerContext(tc.ctx, acme.NewLinker(\"test.ca.smallstep.com\", \"acme\"))\n\t\t\treq := httptest.NewRequest(\"GET\", \"/foo/bar\", http.NoBody)\n\t\t\treq = req.WithContext(ctx)\n\t\t\tw := httptest.NewRecorder()\n\t\t\tGetDirectory(w, req)\n\t\t\tres := w.Result()\n\n\t\t\tassert.Equals(t, res.StatusCode, tc.statusCode)\n\n\t\t\tbody, err := io.ReadAll(res.Body)\n\t\t\tres.Body.Close()\n\t\t\tassert.FatalError(t, err)\n\n\t\t\tif res.StatusCode >= 400 && assert.NotNil(t, tc.err) {\n\t\t\t\tvar ae acme.Error\n\t\t\t\tassert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &ae))\n\n\t\t\t\tassert.Equals(t, ae.Type, tc.err.Type)\n\t\t\t\tassert.Equals(t, ae.Detail, tc.err.Detail)\n\t\t\t\tassert.Equals(t, ae.Subproblems, tc.err.Subproblems)\n\t\t\t\tassert.Equals(t, res.Header[\"Content-Type\"], []string{\"application/problem+json\"})\n\t\t\t} else {\n\t\t\t\tvar dir Directory\n\t\t\t\tjson.Unmarshal(bytes.TrimSpace(body), &dir)\n\t\t\t\tif !cmp.Equal(tc.dir, dir) {\n\t\t\t\t\tt.Errorf(\"GetDirectory() diff =\\n%s\", cmp.Diff(tc.dir, dir))\n\t\t\t\t}\n\t\t\t\tassert.Equals(t, res.Header[\"Content-Type\"], []string{\"application/json\"})\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestHandler_GetAuthorization(t *testing.T) {\n\texpiry := time.Now().UTC().Add(6 * time.Hour)\n\taz := acme.Authorization{\n\t\tID:        \"authzID\",\n\t\tAccountID: \"accID\",\n\t\tIdentifier: acme.Identifier{\n\t\t\tType:  \"dns\",\n\t\t\tValue: \"example.com\",\n\t\t},\n\t\tStatus:    \"pending\",\n\t\tExpiresAt: expiry,\n\t\tWildcard:  false,\n\t\tChallenges: []*acme.Challenge{\n\t\t\t{\n\t\t\t\tType:   \"http-01\",\n\t\t\t\tStatus: \"pending\",\n\t\t\t\tToken:  \"tok2\",\n\t\t\t\tURL:    \"https://ca.smallstep.com/acme/challenge/chHTTPID\",\n\t\t\t\tID:     \"chHTTP01ID\",\n\t\t\t},\n\t\t\t{\n\t\t\t\tType:   \"dns-01\",\n\t\t\t\tStatus: \"pending\",\n\t\t\t\tToken:  \"tok2\",\n\t\t\t\tURL:    \"https://ca.smallstep.com/acme/challenge/chDNSID\",\n\t\t\t\tID:     \"chDNSID\",\n\t\t\t},\n\t\t},\n\t}\n\tprov := newProv()\n\tprovName := url.PathEscape(prov.GetName())\n\tbaseURL := &url.URL{Scheme: \"https\", Host: \"test.ca.smallstep.com\"}\n\n\t// Request with chi context\n\tchiCtx := chi.NewRouteContext()\n\tchiCtx.URLParams.Add(\"authzID\", az.ID)\n\tu := fmt.Sprintf(\"%s/acme/%s/authz/%s\",\n\t\tbaseURL.String(), provName, az.ID)\n\n\ttype test struct {\n\t\tdb         acme.DB\n\t\tctx        context.Context\n\t\tstatusCode int\n\t\terr        *acme.Error\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/no-account\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tdb:         &acme.MockDB{},\n\t\t\t\tctx:        context.Background(),\n\t\t\t\tstatusCode: 400,\n\t\t\t\terr:        acme.NewError(acme.ErrorAccountDoesNotExistType, \"account does not exist\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/nil-account\": func(t *testing.T) test {\n\t\t\tctx := acme.NewProvisionerContext(context.Background(), prov)\n\t\t\tctx = context.WithValue(ctx, accContextKey, nil)\n\t\t\treturn test{\n\t\t\t\tdb:         &acme.MockDB{},\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 400,\n\t\t\t\terr:        acme.NewError(acme.ErrorAccountDoesNotExistType, \"account does not exist\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/db.GetAuthorization-error\": func(t *testing.T) test {\n\t\t\tacc := &acme.Account{ID: \"accID\"}\n\t\t\tctx := context.WithValue(context.Background(), accContextKey, acc)\n\t\t\tctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx)\n\t\t\treturn test{\n\t\t\t\tdb: &acme.MockDB{\n\t\t\t\t\tMockError: acme.NewErrorISE(\"force\"),\n\t\t\t\t},\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 500,\n\t\t\t\terr:        acme.NewErrorISE(\"force\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/account-id-mismatch\": func(t *testing.T) test {\n\t\t\tacc := &acme.Account{ID: \"accID\"}\n\t\t\tctx := context.WithValue(context.Background(), accContextKey, acc)\n\t\t\tctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx)\n\t\t\treturn test{\n\t\t\t\tdb: &acme.MockDB{\n\t\t\t\t\tMockGetAuthorization: func(ctx context.Context, id string) (*acme.Authorization, error) {\n\t\t\t\t\t\tassert.Equals(t, id, az.ID)\n\t\t\t\t\t\treturn &acme.Authorization{\n\t\t\t\t\t\t\tAccountID: \"foo\",\n\t\t\t\t\t\t}, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 401,\n\t\t\t\terr:        acme.NewError(acme.ErrorUnauthorizedType, \"account id mismatch\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/db.UpdateAuthorization-error\": func(t *testing.T) test {\n\t\t\tacc := &acme.Account{ID: \"accID\"}\n\t\t\tctx := context.WithValue(context.Background(), accContextKey, acc)\n\t\t\tctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx)\n\t\t\treturn test{\n\t\t\t\tdb: &acme.MockDB{\n\t\t\t\t\tMockGetAuthorization: func(ctx context.Context, id string) (*acme.Authorization, error) {\n\t\t\t\t\t\tassert.Equals(t, id, az.ID)\n\t\t\t\t\t\treturn &acme.Authorization{\n\t\t\t\t\t\t\tAccountID: \"accID\",\n\t\t\t\t\t\t\tStatus:    acme.StatusPending,\n\t\t\t\t\t\t\tExpiresAt: time.Now().Add(-1 * time.Hour),\n\t\t\t\t\t\t}, nil\n\t\t\t\t\t},\n\t\t\t\t\tMockUpdateAuthorization: func(ctx context.Context, az *acme.Authorization) error {\n\t\t\t\t\t\tassert.Equals(t, az.Status, acme.StatusInvalid)\n\t\t\t\t\t\treturn acme.NewErrorISE(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 500,\n\t\t\t\terr:        acme.NewErrorISE(\"force\"),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tacc := &acme.Account{ID: \"accID\"}\n\t\t\tctx := acme.NewProvisionerContext(context.Background(), prov)\n\t\t\tctx = context.WithValue(ctx, accContextKey, acc)\n\t\t\tctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx)\n\t\t\treturn test{\n\t\t\t\tdb: &acme.MockDB{\n\t\t\t\t\tMockGetAuthorization: func(ctx context.Context, id string) (*acme.Authorization, error) {\n\t\t\t\t\t\tassert.Equals(t, id, az.ID)\n\t\t\t\t\t\treturn &az, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 200,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\ttc := run(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tctx := acme.NewContext(tc.ctx, tc.db, nil, acme.NewLinker(\"test.ca.smallstep.com\", \"acme\"), nil)\n\t\t\treq := httptest.NewRequest(\"GET\", \"/foo/bar\", http.NoBody)\n\t\t\treq = req.WithContext(ctx)\n\t\t\tw := httptest.NewRecorder()\n\t\t\tGetAuthorization(w, req)\n\t\t\tres := w.Result()\n\n\t\t\tassert.Equals(t, res.StatusCode, tc.statusCode)\n\n\t\t\tbody, err := io.ReadAll(res.Body)\n\t\t\tres.Body.Close()\n\t\t\tassert.FatalError(t, err)\n\n\t\t\tif res.StatusCode >= 400 && assert.NotNil(t, tc.err) {\n\t\t\t\tvar ae acme.Error\n\t\t\t\tassert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &ae))\n\n\t\t\t\tassert.Equals(t, ae.Type, tc.err.Type)\n\t\t\t\tassert.Equals(t, ae.Detail, tc.err.Detail)\n\t\t\t\tassert.Equals(t, ae.Subproblems, tc.err.Subproblems)\n\t\t\t\tassert.Equals(t, res.Header[\"Content-Type\"], []string{\"application/problem+json\"})\n\t\t\t} else {\n\t\t\t\t//var gotAz acme.Authz\n\t\t\t\t//assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &gotAz))\n\t\t\t\texpB, err := json.Marshal(az)\n\t\t\t\tassert.FatalError(t, err)\n\t\t\t\tassert.Equals(t, bytes.TrimSpace(body), expB)\n\t\t\t\tassert.Equals(t, res.Header[\"Location\"], []string{u})\n\t\t\t\tassert.Equals(t, res.Header[\"Content-Type\"], []string{\"application/json\"})\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestHandler_GetCertificate(t *testing.T) {\n\tleaf, err := pemutil.ReadCertificate(\"../../authority/testdata/certs/foo.crt\")\n\tassert.FatalError(t, err)\n\tinter, err := pemutil.ReadCertificate(\"../../authority/testdata/certs/intermediate_ca.crt\")\n\tassert.FatalError(t, err)\n\troot, err := pemutil.ReadCertificate(\"../../authority/testdata/certs/root_ca.crt\")\n\tassert.FatalError(t, err)\n\n\tcertBytes := append(pem.EncodeToMemory(&pem.Block{\n\t\tType:  \"CERTIFICATE\",\n\t\tBytes: leaf.Raw,\n\t}), pem.EncodeToMemory(&pem.Block{\n\t\tType:  \"CERTIFICATE\",\n\t\tBytes: inter.Raw,\n\t})...)\n\tcertBytes = append(certBytes, pem.EncodeToMemory(&pem.Block{\n\t\tType:  \"CERTIFICATE\",\n\t\tBytes: root.Raw,\n\t})...)\n\tcertID := \"certID\"\n\n\tprov := newProv()\n\tprovName := url.PathEscape(prov.GetName())\n\tbaseURL := &url.URL{Scheme: \"https\", Host: \"test.ca.smallstep.com\"}\n\t// Request with chi context\n\tchiCtx := chi.NewRouteContext()\n\tchiCtx.URLParams.Add(\"certID\", certID)\n\tu := fmt.Sprintf(\"%s/acme/%s/certificate/%s\",\n\t\tbaseURL.String(), provName, certID)\n\n\ttype test struct {\n\t\tdb         acme.DB\n\t\tctx        context.Context\n\t\tstatusCode int\n\t\terr        *acme.Error\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/no-account\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tdb:         &acme.MockDB{},\n\t\t\t\tctx:        context.Background(),\n\t\t\t\tstatusCode: 400,\n\t\t\t\terr:        acme.NewError(acme.ErrorAccountDoesNotExistType, \"account does not exist\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/nil-account\": func(t *testing.T) test {\n\t\t\tctx := context.WithValue(context.Background(), accContextKey, nil)\n\t\t\treturn test{\n\t\t\t\tdb:         &acme.MockDB{},\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 400,\n\t\t\t\terr:        acme.NewError(acme.ErrorAccountDoesNotExistType, \"account does not exist\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/db.GetCertificate-error\": func(t *testing.T) test {\n\t\t\tacc := &acme.Account{ID: \"accID\"}\n\t\t\tctx := context.WithValue(context.Background(), accContextKey, acc)\n\t\t\tctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx)\n\t\t\treturn test{\n\t\t\t\tdb: &acme.MockDB{\n\t\t\t\t\tMockError: acme.NewErrorISE(\"force\"),\n\t\t\t\t},\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 500,\n\t\t\t\terr:        acme.NewErrorISE(\"force\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/account-id-mismatch\": func(t *testing.T) test {\n\t\t\tacc := &acme.Account{ID: \"accID\"}\n\t\t\tctx := context.WithValue(context.Background(), accContextKey, acc)\n\t\t\tctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx)\n\t\t\treturn test{\n\t\t\t\tdb: &acme.MockDB{\n\t\t\t\t\tMockGetCertificate: func(ctx context.Context, id string) (*acme.Certificate, error) {\n\t\t\t\t\t\tassert.Equals(t, id, certID)\n\t\t\t\t\t\treturn &acme.Certificate{AccountID: \"foo\"}, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 401,\n\t\t\t\terr:        acme.NewError(acme.ErrorUnauthorizedType, \"account id mismatch\"),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tacc := &acme.Account{ID: \"accID\"}\n\t\t\tctx := context.WithValue(context.Background(), accContextKey, acc)\n\t\t\tctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx)\n\t\t\treturn test{\n\t\t\t\tdb: &acme.MockDB{\n\t\t\t\t\tMockGetCertificate: func(ctx context.Context, id string) (*acme.Certificate, error) {\n\t\t\t\t\t\tassert.Equals(t, id, certID)\n\t\t\t\t\t\treturn &acme.Certificate{\n\t\t\t\t\t\t\tAccountID:     \"accID\",\n\t\t\t\t\t\t\tOrderID:       \"ordID\",\n\t\t\t\t\t\t\tLeaf:          leaf,\n\t\t\t\t\t\t\tIntermediates: []*x509.Certificate{inter, root},\n\t\t\t\t\t\t\tID:            id,\n\t\t\t\t\t\t}, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 200,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\ttc := run(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tctx := acme.NewDatabaseContext(tc.ctx, tc.db)\n\t\t\treq := httptest.NewRequest(\"GET\", u, http.NoBody)\n\t\t\treq = req.WithContext(ctx)\n\t\t\tw := httptest.NewRecorder()\n\t\t\tGetCertificate(w, req)\n\t\t\tres := w.Result()\n\n\t\t\tassert.Equals(t, res.StatusCode, tc.statusCode)\n\n\t\t\tbody, err := io.ReadAll(res.Body)\n\t\t\tres.Body.Close()\n\t\t\tassert.FatalError(t, err)\n\n\t\t\tif res.StatusCode >= 400 && assert.NotNil(t, tc.err) {\n\t\t\t\tvar ae acme.Error\n\t\t\t\tassert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &ae))\n\n\t\t\t\tassert.Equals(t, ae.Type, tc.err.Type)\n\t\t\t\tassert.HasPrefix(t, ae.Detail, tc.err.Detail)\n\t\t\t\tassert.Equals(t, ae.Subproblems, tc.err.Subproblems)\n\t\t\t\tassert.Equals(t, res.Header[\"Content-Type\"], []string{\"application/problem+json\"})\n\t\t\t} else {\n\t\t\t\tassert.Equals(t, bytes.TrimSpace(body), bytes.TrimSpace(certBytes))\n\t\t\t\tassert.Equals(t, res.Header[\"Content-Type\"], []string{\"application/pem-certificate-chain\"})\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestHandler_GetChallenge(t *testing.T) {\n\tchiCtx := chi.NewRouteContext()\n\tchiCtx.URLParams.Add(\"chID\", \"chID\")\n\tchiCtx.URLParams.Add(\"authzID\", \"authzID\")\n\tprov := newProv()\n\tprovName := url.PathEscape(prov.GetName())\n\n\tbaseURL := &url.URL{Scheme: \"https\", Host: \"test.ca.smallstep.com\"}\n\n\tu := fmt.Sprintf(\"%s/acme/%s/challenge/%s/%s\",\n\t\tbaseURL.String(), provName, \"authzID\", \"chID\")\n\n\ttype test struct {\n\t\tdb         acme.DB\n\t\tvc         acme.Client\n\t\tctx        context.Context\n\t\tstatusCode int\n\t\tch         *acme.Challenge\n\t\terr        *acme.Error\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/no-account\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tdb:         &acme.MockDB{},\n\t\t\t\tctx:        context.Background(),\n\t\t\t\tstatusCode: 400,\n\t\t\t\terr:        acme.NewError(acme.ErrorAccountDoesNotExistType, \"account does not exist\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/nil-account\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tdb:         &acme.MockDB{},\n\t\t\t\tctx:        context.WithValue(context.Background(), accContextKey, nil),\n\t\t\t\tstatusCode: 400,\n\t\t\t\terr:        acme.NewError(acme.ErrorAccountDoesNotExistType, \"account does not exist\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/no-payload\": func(t *testing.T) test {\n\t\t\tacc := &acme.Account{ID: \"accID\"}\n\t\t\tctx := context.WithValue(context.Background(), accContextKey, acc)\n\t\t\treturn test{\n\t\t\t\tdb:         &acme.MockDB{},\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 500,\n\t\t\t\terr:        acme.NewErrorISE(\"payload expected in request context\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/nil-payload\": func(t *testing.T) test {\n\t\t\tacc := &acme.Account{ID: \"accID\"}\n\t\t\tctx := acme.NewProvisionerContext(context.Background(), prov)\n\t\t\tctx = context.WithValue(ctx, accContextKey, acc)\n\t\t\tctx = context.WithValue(ctx, payloadContextKey, nil)\n\t\t\treturn test{\n\t\t\t\tdb:         &acme.MockDB{},\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 500,\n\t\t\t\terr:        acme.NewErrorISE(\"payload expected in request context\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/db.GetChallenge-error\": func(t *testing.T) test {\n\t\t\tacc := &acme.Account{ID: \"accID\"}\n\t\t\tctx := acme.NewProvisionerContext(context.Background(), prov)\n\t\t\tctx = context.WithValue(ctx, accContextKey, acc)\n\t\t\tctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{isEmptyJSON: true})\n\t\t\tctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx)\n\t\t\treturn test{\n\t\t\t\tdb: &acme.MockDB{\n\t\t\t\t\tMockGetChallenge: func(ctx context.Context, chID, azID string) (*acme.Challenge, error) {\n\t\t\t\t\t\tassert.Equals(t, chID, \"chID\")\n\t\t\t\t\t\tassert.Equals(t, azID, \"authzID\")\n\t\t\t\t\t\treturn nil, acme.NewErrorISE(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 500,\n\t\t\t\terr:        acme.NewErrorISE(\"force\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/account-id-mismatch\": func(t *testing.T) test {\n\t\t\tacc := &acme.Account{ID: \"accID\"}\n\t\t\tctx := acme.NewProvisionerContext(context.Background(), prov)\n\t\t\tctx = context.WithValue(ctx, accContextKey, acc)\n\t\t\tctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{isEmptyJSON: true})\n\t\t\tctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx)\n\t\t\treturn test{\n\t\t\t\tdb: &acme.MockDB{\n\t\t\t\t\tMockGetChallenge: func(ctx context.Context, chID, azID string) (*acme.Challenge, error) {\n\t\t\t\t\t\tassert.Equals(t, chID, \"chID\")\n\t\t\t\t\t\tassert.Equals(t, azID, \"authzID\")\n\t\t\t\t\t\treturn &acme.Challenge{AccountID: \"foo\"}, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 401,\n\t\t\t\terr:        acme.NewError(acme.ErrorUnauthorizedType, \"accout id mismatch\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/no-jwk\": func(t *testing.T) test {\n\t\t\tacc := &acme.Account{ID: \"accID\"}\n\t\t\tctx := acme.NewProvisionerContext(context.Background(), prov)\n\t\t\tctx = context.WithValue(ctx, accContextKey, acc)\n\t\t\tctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{isEmptyJSON: true})\n\t\t\tctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx)\n\t\t\treturn test{\n\t\t\t\tdb: &acme.MockDB{\n\t\t\t\t\tMockGetChallenge: func(ctx context.Context, chID, azID string) (*acme.Challenge, error) {\n\t\t\t\t\t\tassert.Equals(t, chID, \"chID\")\n\t\t\t\t\t\tassert.Equals(t, azID, \"authzID\")\n\t\t\t\t\t\treturn &acme.Challenge{AccountID: \"accID\"}, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 500,\n\t\t\t\terr:        acme.NewErrorISE(\"missing jwk\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/nil-jwk\": func(t *testing.T) test {\n\t\t\tacc := &acme.Account{ID: \"accID\"}\n\t\t\tctx := acme.NewProvisionerContext(context.Background(), prov)\n\t\t\tctx = context.WithValue(ctx, accContextKey, acc)\n\t\t\tctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{isEmptyJSON: true})\n\t\t\tctx = context.WithValue(ctx, jwkContextKey, nil)\n\t\t\tctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx)\n\t\t\treturn test{\n\t\t\t\tdb: &acme.MockDB{\n\t\t\t\t\tMockGetChallenge: func(ctx context.Context, chID, azID string) (*acme.Challenge, error) {\n\t\t\t\t\t\tassert.Equals(t, chID, \"chID\")\n\t\t\t\t\t\tassert.Equals(t, azID, \"authzID\")\n\t\t\t\t\t\treturn &acme.Challenge{AccountID: \"accID\"}, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 500,\n\t\t\t\terr:        acme.NewErrorISE(\"nil jwk\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/validate-challenge-error\": func(t *testing.T) test {\n\t\t\tacc := &acme.Account{ID: \"accID\"}\n\t\t\tctx := acme.NewProvisionerContext(context.Background(), prov)\n\t\t\tctx = context.WithValue(ctx, accContextKey, acc)\n\t\t\tctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{isEmptyJSON: true})\n\t\t\t_jwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\tassert.FatalError(t, err)\n\t\t\t_pub := _jwk.Public()\n\t\t\tctx = context.WithValue(ctx, jwkContextKey, &_pub)\n\t\t\tctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx)\n\t\t\treturn test{\n\t\t\t\tdb: &acme.MockDB{\n\t\t\t\t\tMockGetChallenge: func(ctx context.Context, chID, azID string) (*acme.Challenge, error) {\n\t\t\t\t\t\tassert.Equals(t, chID, \"chID\")\n\t\t\t\t\t\tassert.Equals(t, azID, \"authzID\")\n\t\t\t\t\t\treturn &acme.Challenge{\n\t\t\t\t\t\t\tStatus:    acme.StatusPending,\n\t\t\t\t\t\t\tType:      acme.HTTP01,\n\t\t\t\t\t\t\tAccountID: \"accID\",\n\t\t\t\t\t\t}, nil\n\t\t\t\t\t},\n\t\t\t\t\tMockUpdateChallenge: func(ctx context.Context, ch *acme.Challenge) error {\n\t\t\t\t\t\tassert.Equals(t, ch.Status, acme.StatusPending)\n\t\t\t\t\t\tassert.Equals(t, ch.Type, acme.HTTP01)\n\t\t\t\t\t\tassert.Equals(t, ch.AccountID, \"accID\")\n\t\t\t\t\t\tassert.Equals(t, ch.AuthorizationID, \"authzID\")\n\t\t\t\t\t\tassert.HasSuffix(t, ch.Error.Type, acme.ErrorConnectionType.String())\n\t\t\t\t\t\treturn acme.NewErrorISE(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tvc: &mockClient{\n\t\t\t\t\tget: func(string) (*http.Response, error) {\n\t\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 500,\n\t\t\t\terr:        acme.NewErrorISE(\"force\"),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tacc := &acme.Account{ID: \"accID\"}\n\t\t\tctx := acme.NewProvisionerContext(context.Background(), prov)\n\t\t\tctx = context.WithValue(ctx, accContextKey, acc)\n\t\t\tctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{isEmptyJSON: true})\n\t\t\t_jwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\tassert.FatalError(t, err)\n\t\t\t_pub := _jwk.Public()\n\t\t\tctx = context.WithValue(ctx, jwkContextKey, &_pub)\n\t\t\tctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx)\n\t\t\treturn test{\n\t\t\t\tdb: &acme.MockDB{\n\t\t\t\t\tMockGetChallenge: func(ctx context.Context, chID, azID string) (*acme.Challenge, error) {\n\t\t\t\t\t\tassert.Equals(t, chID, \"chID\")\n\t\t\t\t\t\tassert.Equals(t, azID, \"authzID\")\n\t\t\t\t\t\treturn &acme.Challenge{\n\t\t\t\t\t\t\tID:        \"chID\",\n\t\t\t\t\t\t\tStatus:    acme.StatusPending,\n\t\t\t\t\t\t\tType:      acme.HTTP01,\n\t\t\t\t\t\t\tAccountID: \"accID\",\n\t\t\t\t\t\t}, nil\n\t\t\t\t\t},\n\t\t\t\t\tMockUpdateChallenge: func(ctx context.Context, ch *acme.Challenge) error {\n\t\t\t\t\t\tassert.Equals(t, ch.Status, acme.StatusPending)\n\t\t\t\t\t\tassert.Equals(t, ch.Type, acme.HTTP01)\n\t\t\t\t\t\tassert.Equals(t, ch.AccountID, \"accID\")\n\t\t\t\t\t\tassert.Equals(t, ch.AuthorizationID, \"authzID\")\n\t\t\t\t\t\tassert.HasSuffix(t, ch.Error.Type, acme.ErrorConnectionType.String())\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tch: &acme.Challenge{\n\t\t\t\t\tID:              \"chID\",\n\t\t\t\t\tStatus:          acme.StatusPending,\n\t\t\t\t\tAuthorizationID: \"authzID\",\n\t\t\t\t\tType:            acme.HTTP01,\n\t\t\t\t\tAccountID:       \"accID\",\n\t\t\t\t\tURL:             u,\n\t\t\t\t\tError:           acme.NewError(acme.ErrorConnectionType, \"force\"),\n\t\t\t\t},\n\t\t\t\tvc: &mockClient{\n\t\t\t\t\tget: func(string) (*http.Response, error) {\n\t\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 200,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\ttc := run(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tctx := acme.NewContext(tc.ctx, tc.db, nil, acme.NewLinker(\"test.ca.smallstep.com\", \"acme\"), nil)\n\t\t\treq := httptest.NewRequest(\"GET\", u, http.NoBody)\n\t\t\treq = req.WithContext(ctx)\n\t\t\tw := httptest.NewRecorder()\n\t\t\tGetChallenge(w, req)\n\t\t\tres := w.Result()\n\n\t\t\tassert.Equals(t, res.StatusCode, tc.statusCode)\n\n\t\t\tbody, err := io.ReadAll(res.Body)\n\t\t\tres.Body.Close()\n\t\t\tassert.FatalError(t, err)\n\n\t\t\tif res.StatusCode >= 400 && assert.NotNil(t, tc.err) {\n\t\t\t\tvar ae acme.Error\n\t\t\t\tassert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &ae))\n\n\t\t\t\tassert.Equals(t, ae.Type, tc.err.Type)\n\t\t\t\tassert.Equals(t, ae.Detail, tc.err.Detail)\n\t\t\t\tassert.Equals(t, ae.Subproblems, tc.err.Subproblems)\n\t\t\t\tassert.Equals(t, res.Header[\"Content-Type\"], []string{\"application/problem+json\"})\n\t\t\t} else {\n\t\t\t\texpB, err := json.Marshal(tc.ch)\n\t\t\t\tassert.FatalError(t, err)\n\t\t\t\tassert.Equals(t, bytes.TrimSpace(body), expB)\n\t\t\t\tassert.Equals(t, res.Header[\"Link\"], []string{fmt.Sprintf(\"<%s/acme/%s/authz/%s>;rel=\\\"up\\\"\", baseURL, provName, \"authzID\")})\n\t\t\t\tassert.Equals(t, res.Header[\"Location\"], []string{u})\n\t\t\t\tassert.Equals(t, res.Header[\"Content-Type\"], []string{\"application/json\"})\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_createMetaObject(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tp    *provisioner.ACME\n\t\twant *Meta\n\t}{\n\t\t{\n\t\t\tname: \"no-meta\",\n\t\t\tp: &provisioner.ACME{\n\t\t\t\tType: \"ACME\",\n\t\t\t\tName: \"acme\",\n\t\t\t},\n\t\t\twant: nil,\n\t\t},\n\t\t{\n\t\t\tname: \"terms-of-service\",\n\t\t\tp: &provisioner.ACME{\n\t\t\t\tType:           \"ACME\",\n\t\t\t\tName:           \"acme\",\n\t\t\t\tTermsOfService: \"https://terms.ca.local\",\n\t\t\t},\n\t\t\twant: &Meta{\n\t\t\t\tTermsOfService: \"https://terms.ca.local\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"website\",\n\t\t\tp: &provisioner.ACME{\n\t\t\t\tType:    \"ACME\",\n\t\t\t\tName:    \"acme\",\n\t\t\t\tWebsite: \"https://ca.local\",\n\t\t\t},\n\t\t\twant: &Meta{\n\t\t\t\tWebsite: \"https://ca.local\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"caa\",\n\t\t\tp: &provisioner.ACME{\n\t\t\t\tType:          \"ACME\",\n\t\t\t\tName:          \"acme\",\n\t\t\t\tCaaIdentities: []string{\"ca.local\", \"ca.remote\"},\n\t\t\t},\n\t\t\twant: &Meta{\n\t\t\t\tCaaIdentities: []string{\"ca.local\", \"ca.remote\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"require-eab\",\n\t\t\tp: &provisioner.ACME{\n\t\t\t\tType:       \"ACME\",\n\t\t\t\tName:       \"acme\",\n\t\t\t\tRequireEAB: true,\n\t\t\t},\n\t\t\twant: &Meta{\n\t\t\t\tExternalAccountRequired: true,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"full-meta\",\n\t\t\tp: &provisioner.ACME{\n\t\t\t\tType:           \"ACME\",\n\t\t\t\tName:           \"acme\",\n\t\t\t\tTermsOfService: \"https://terms.ca.local\",\n\t\t\t\tWebsite:        \"https://ca.local\",\n\t\t\t\tCaaIdentities:  []string{\"ca.local\", \"ca.remote\"},\n\t\t\t\tRequireEAB:     true,\n\t\t\t},\n\t\t\twant: &Meta{\n\t\t\t\tTermsOfService:          \"https://terms.ca.local\",\n\t\t\t\tWebsite:                 \"https://ca.local\",\n\t\t\t\tCaaIdentities:           []string{\"ca.local\", \"ca.remote\"},\n\t\t\t\tExternalAccountRequired: true,\n\t\t\t},\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot := createMetaObject(tt.p)\n\t\t\tif !cmp.Equal(tt.want, got) {\n\t\t\t\tt.Errorf(\"createMetaObject() diff =\\n%s\", cmp.Diff(tt.want, got))\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "acme/api/middleware.go",
    "content": "package api\n\nimport (\n\t\"context\"\n\t\"crypto/rsa\"\n\t\"errors\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"path\"\n\t\"strings\"\n\n\t\"go.step.sm/crypto/jose\"\n\t\"go.step.sm/crypto/keyutil\"\n\n\t\"github.com/smallstep/certificates/acme\"\n\t\"github.com/smallstep/certificates/api/render\"\n\t\"github.com/smallstep/certificates/authority/provisioner\"\n\t\"github.com/smallstep/certificates/logging\"\n)\n\ntype nextHTTP = func(http.ResponseWriter, *http.Request)\n\nfunc logNonce(w http.ResponseWriter, nonce string) {\n\tif rl, ok := w.(logging.ResponseLogger); ok {\n\t\tm := map[string]interface{}{\n\t\t\t\"nonce\": nonce,\n\t\t}\n\t\trl.WithFields(m)\n\t}\n}\n\n// addNonce is a middleware that adds a nonce to the response header.\nfunc addNonce(next nextHTTP) nextHTTP {\n\treturn func(w http.ResponseWriter, r *http.Request) {\n\t\tdb := acme.MustDatabaseFromContext(r.Context())\n\t\tnonce, err := db.CreateNonce(r.Context())\n\t\tif err != nil {\n\t\t\trender.Error(w, r, err)\n\t\t\treturn\n\t\t}\n\t\tw.Header().Set(\"Replay-Nonce\", string(nonce))\n\t\tw.Header().Set(\"Cache-Control\", \"no-store\")\n\t\tlogNonce(w, string(nonce))\n\t\tnext(w, r)\n\t}\n}\n\n// addDirLink is a middleware that adds a 'Link' response reader with the\n// directory index url.\nfunc addDirLink(next nextHTTP) nextHTTP {\n\treturn func(w http.ResponseWriter, r *http.Request) {\n\t\tctx := r.Context()\n\t\tlinker := acme.MustLinkerFromContext(ctx)\n\n\t\tw.Header().Add(\"Link\", link(linker.GetLink(ctx, acme.DirectoryLinkType), \"index\"))\n\t\tnext(w, r)\n\t}\n}\n\n// verifyContentType is a middleware that verifies that content type is\n// application/jose+json.\nfunc verifyContentType(next nextHTTP) nextHTTP {\n\treturn func(w http.ResponseWriter, r *http.Request) {\n\t\tp, err := provisionerFromContext(r.Context())\n\t\tif err != nil {\n\t\t\trender.Error(w, r, err)\n\t\t\treturn\n\t\t}\n\n\t\tu := &url.URL{\n\t\t\tPath: acme.GetUnescapedPathSuffix(acme.CertificateLinkType, p.GetName(), \"\"),\n\t\t}\n\n\t\tvar expected []string\n\t\tif strings.Contains(r.URL.String(), u.EscapedPath()) {\n\t\t\t// GET /certificate requests allow a greater range of content types.\n\t\t\texpected = []string{\"application/jose+json\", \"application/pkix-cert\", \"application/pkcs7-mime\"}\n\t\t} else {\n\t\t\t// By default every request should have content-type applictaion/jose+json.\n\t\t\texpected = []string{\"application/jose+json\"}\n\t\t}\n\n\t\tct := r.Header.Get(\"Content-Type\")\n\t\tfor _, e := range expected {\n\t\t\tif ct == e {\n\t\t\t\tnext(w, r)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t\trender.Error(w, r, acme.NewError(acme.ErrorMalformedType,\n\t\t\t\"expected content-type to be in %s, but got %s\", expected, ct))\n\t}\n}\n\n// parseJWS is a middleware that parses a request body into a JSONWebSignature struct.\nfunc parseJWS(next nextHTTP) nextHTTP {\n\treturn func(w http.ResponseWriter, r *http.Request) {\n\t\tbody, err := io.ReadAll(r.Body)\n\t\tif err != nil {\n\t\t\trender.Error(w, r, acme.WrapErrorISE(err, \"failed to read request body\"))\n\t\t\treturn\n\t\t}\n\t\tjws, err := jose.ParseJWS(string(body))\n\t\tif err != nil {\n\t\t\trender.Error(w, r, acme.WrapError(acme.ErrorMalformedType, err, \"failed to parse JWS from request body\"))\n\t\t\treturn\n\t\t}\n\t\tctx := context.WithValue(r.Context(), jwsContextKey, jws)\n\t\tnext(w, r.WithContext(ctx))\n\t}\n}\n\n// validateJWS checks the request body for to verify that it meets ACME\n// requirements for a JWS.\n//\n// The JWS MUST NOT have multiple signatures\n// The JWS Unencoded Payload Option [RFC7797] MUST NOT be used\n// The JWS Unprotected Header [RFC7515] MUST NOT be used\n// The JWS Payload MUST NOT be detached\n// The JWS Protected Header MUST include the following fields:\n//   - “alg” (Algorithm).\n//     This field MUST NOT contain “none” or a Message Authentication Code\n//     (MAC) algorithm (e.g. one in which the algorithm registry description\n//     mentions MAC/HMAC).\n//   - “nonce” (defined in Section 6.5)\n//   - “url” (defined in Section 6.4)\n//   - Either “jwk” (JSON Web Key) or “kid” (Key ID) as specified below<Paste>\nfunc validateJWS(next nextHTTP) nextHTTP {\n\treturn func(w http.ResponseWriter, r *http.Request) {\n\t\tctx := r.Context()\n\t\tdb := acme.MustDatabaseFromContext(ctx)\n\n\t\tjws, err := jwsFromContext(ctx)\n\t\tif err != nil {\n\t\t\trender.Error(w, r, err)\n\t\t\treturn\n\t\t}\n\t\tif len(jws.Signatures) == 0 {\n\t\t\trender.Error(w, r, acme.NewError(acme.ErrorMalformedType, \"request body does not contain a signature\"))\n\t\t\treturn\n\t\t}\n\t\tif len(jws.Signatures) > 1 {\n\t\t\trender.Error(w, r, acme.NewError(acme.ErrorMalformedType, \"request body contains more than one signature\"))\n\t\t\treturn\n\t\t}\n\n\t\tsig := jws.Signatures[0]\n\t\tuh := sig.Unprotected\n\t\tif uh.KeyID != \"\" ||\n\t\t\tuh.JSONWebKey != nil ||\n\t\t\tuh.Algorithm != \"\" ||\n\t\t\tuh.Nonce != \"\" ||\n\t\t\tlen(uh.ExtraHeaders) > 0 {\n\t\t\trender.Error(w, r, acme.NewError(acme.ErrorMalformedType, \"unprotected header must not be used\"))\n\t\t\treturn\n\t\t}\n\t\thdr := sig.Protected\n\t\tswitch hdr.Algorithm {\n\t\tcase jose.RS256, jose.RS384, jose.RS512, jose.PS256, jose.PS384, jose.PS512:\n\t\t\tif hdr.JSONWebKey != nil {\n\t\t\t\tswitch k := hdr.JSONWebKey.Key.(type) {\n\t\t\t\tcase *rsa.PublicKey:\n\t\t\t\t\tif k.Size() < keyutil.MinRSAKeyBytes {\n\t\t\t\t\t\trender.Error(w, r, acme.NewError(acme.ErrorMalformedType,\n\t\t\t\t\t\t\t\"rsa keys must be at least %d bits (%d bytes) in size\",\n\t\t\t\t\t\t\t8*keyutil.MinRSAKeyBytes, keyutil.MinRSAKeyBytes))\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\tdefault:\n\t\t\t\t\trender.Error(w, r, acme.NewError(acme.ErrorMalformedType,\n\t\t\t\t\t\t\"jws key type and algorithm do not match\"))\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\t\tcase jose.ES256, jose.ES384, jose.ES512, jose.EdDSA:\n\t\t\t// we good\n\t\tdefault:\n\t\t\trender.Error(w, r, acme.NewError(acme.ErrorBadSignatureAlgorithmType, \"unsuitable algorithm: %s\", hdr.Algorithm))\n\t\t\treturn\n\t\t}\n\n\t\t// Check the validity/freshness of the Nonce.\n\t\tif err := db.DeleteNonce(ctx, acme.Nonce(hdr.Nonce)); err != nil {\n\t\t\trender.Error(w, r, err)\n\t\t\treturn\n\t\t}\n\n\t\t// Check that the JWS url matches the requested url.\n\t\tjwsURL, ok := hdr.ExtraHeaders[\"url\"].(string)\n\t\tif !ok {\n\t\t\trender.Error(w, r, acme.NewError(acme.ErrorMalformedType, \"jws missing url protected header\"))\n\t\t\treturn\n\t\t}\n\t\treqURL := &url.URL{Scheme: \"https\", Host: r.Host, Path: r.URL.Path}\n\t\tif jwsURL != reqURL.String() {\n\t\t\trender.Error(w, r, acme.NewError(acme.ErrorMalformedType,\n\t\t\t\t\"url header in JWS (%s) does not match request url (%s)\", jwsURL, reqURL))\n\t\t\treturn\n\t\t}\n\n\t\tif hdr.JSONWebKey != nil && hdr.KeyID != \"\" {\n\t\t\trender.Error(w, r, acme.NewError(acme.ErrorMalformedType, \"jwk and kid are mutually exclusive\"))\n\t\t\treturn\n\t\t}\n\t\tif hdr.JSONWebKey == nil && hdr.KeyID == \"\" {\n\t\t\trender.Error(w, r, acme.NewError(acme.ErrorMalformedType, \"either jwk or kid must be defined in jws protected header\"))\n\t\t\treturn\n\t\t}\n\t\tnext(w, r)\n\t}\n}\n\n// extractJWK is a middleware that extracts the JWK from the JWS and saves it\n// in the context. Make sure to parse and validate the JWS before running this\n// middleware.\nfunc extractJWK(next nextHTTP) nextHTTP {\n\treturn func(w http.ResponseWriter, r *http.Request) {\n\t\tctx := r.Context()\n\t\tdb := acme.MustDatabaseFromContext(ctx)\n\n\t\tjws, err := jwsFromContext(ctx)\n\t\tif err != nil {\n\t\t\trender.Error(w, r, err)\n\t\t\treturn\n\t\t}\n\t\tjwk := jws.Signatures[0].Protected.JSONWebKey\n\t\tif jwk == nil {\n\t\t\trender.Error(w, r, acme.NewError(acme.ErrorMalformedType, \"jwk expected in protected header\"))\n\t\t\treturn\n\t\t}\n\t\tif !jwk.Valid() {\n\t\t\trender.Error(w, r, acme.NewError(acme.ErrorMalformedType, \"invalid jwk in protected header\"))\n\t\t\treturn\n\t\t}\n\n\t\t// Overwrite KeyID with the JWK thumbprint.\n\t\tjwk.KeyID, err = acme.KeyToID(jwk)\n\t\tif err != nil {\n\t\t\trender.Error(w, r, acme.WrapErrorISE(err, \"error getting KeyID from JWK\"))\n\t\t\treturn\n\t\t}\n\n\t\t// Store the JWK in the context.\n\t\tctx = context.WithValue(ctx, jwkContextKey, jwk)\n\n\t\t// Get Account OR continue to generate a new one OR continue Revoke with certificate private key\n\t\tacc, err := db.GetAccountByKeyID(ctx, jwk.KeyID)\n\t\tswitch {\n\t\tcase acme.IsErrNotFound(err):\n\t\t\t// For NewAccount and Revoke requests ...\n\t\t\tbreak\n\t\tcase err != nil:\n\t\t\trender.Error(w, r, err)\n\t\t\treturn\n\t\tdefault:\n\t\t\tif !acc.IsValid() {\n\t\t\t\trender.Error(w, r, acme.NewError(acme.ErrorUnauthorizedType, \"account is not active\"))\n\t\t\t\treturn\n\t\t\t}\n\t\t\tctx = context.WithValue(ctx, accContextKey, acc)\n\t\t}\n\t\tnext(w, r.WithContext(ctx))\n\t}\n}\n\n// checkPrerequisites checks if all prerequisites for serving ACME\n// are met by the CA configuration.\nfunc checkPrerequisites(next nextHTTP) nextHTTP {\n\treturn func(w http.ResponseWriter, r *http.Request) {\n\t\tctx := r.Context()\n\t\t// If the function is not set assume that all prerequisites are met.\n\t\tcheckFunc, ok := acme.PrerequisitesCheckerFromContext(ctx)\n\t\tif ok {\n\t\t\tok, err := checkFunc(ctx)\n\t\t\tif err != nil {\n\t\t\t\trender.Error(w, r, acme.WrapErrorISE(err, \"error checking acme provisioner prerequisites\"))\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !ok {\n\t\t\t\trender.Error(w, r, acme.NewError(acme.ErrorNotImplementedType, \"acme provisioner configuration lacks prerequisites\"))\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t\tnext(w, r)\n\t}\n}\n\n// lookupJWK loads the JWK associated with the acme account referenced by the\n// kid parameter of the signed payload.\n// Make sure to parse and validate the JWS before running this middleware.\nfunc lookupJWK(next nextHTTP) nextHTTP {\n\treturn func(w http.ResponseWriter, r *http.Request) {\n\t\tctx := r.Context()\n\t\tdb := acme.MustDatabaseFromContext(ctx)\n\n\t\tjws, err := jwsFromContext(ctx)\n\t\tif err != nil {\n\t\t\trender.Error(w, r, err)\n\t\t\treturn\n\t\t}\n\n\t\tkid := jws.Signatures[0].Protected.KeyID\n\t\tif kid == \"\" {\n\t\t\trender.Error(w, r, acme.NewError(acme.ErrorMalformedType, \"signature missing 'kid'\"))\n\t\t\treturn\n\t\t}\n\n\t\taccID := path.Base(kid)\n\t\tacc, err := db.GetAccount(ctx, accID)\n\t\tswitch {\n\t\tcase acme.IsErrNotFound(err):\n\t\t\trender.Error(w, r, acme.NewError(acme.ErrorAccountDoesNotExistType, \"account with ID '%s' not found\", accID))\n\t\t\treturn\n\t\tcase err != nil:\n\t\t\trender.Error(w, r, err)\n\t\t\treturn\n\t\tdefault:\n\t\t\tif !acc.IsValid() {\n\t\t\t\trender.Error(w, r, acme.NewError(acme.ErrorUnauthorizedType, \"account is not active\"))\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif storedLocation := acc.GetLocation(); storedLocation != \"\" {\n\t\t\t\tif kid != storedLocation {\n\t\t\t\t\t// ACME accounts should have a stored location equivalent to the\n\t\t\t\t\t// kid in the ACME request.\n\t\t\t\t\trender.Error(w, r, acme.NewError(acme.ErrorUnauthorizedType,\n\t\t\t\t\t\t\"kid does not match stored account location; expected %s, but got %s\",\n\t\t\t\t\t\tstoredLocation, kid))\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\t// Verify that the provisioner with which the account was created\n\t\t\t\t// matches the provisioner in the request URL.\n\t\t\t\treqProv := acme.MustProvisionerFromContext(ctx)\n\t\t\t\tswitch {\n\t\t\t\tcase acc.ProvisionerID == \"\" && acc.ProvisionerName != reqProv.GetName():\n\t\t\t\t\trender.Error(w, r, acme.NewError(acme.ErrorUnauthorizedType,\n\t\t\t\t\t\t\"account provisioner does not match requested provisioner; account provisioner = %s, requested provisioner = %s\",\n\t\t\t\t\t\tacc.ProvisionerName, reqProv.GetName()))\n\t\t\t\t\treturn\n\t\t\t\tcase acc.ProvisionerID != \"\" && acc.ProvisionerID != reqProv.GetID():\n\t\t\t\t\trender.Error(w, r, acme.NewError(acme.ErrorUnauthorizedType,\n\t\t\t\t\t\t\"account provisioner does not match requested provisioner; account provisioner = %s, requested provisioner = %s\",\n\t\t\t\t\t\tacc.ProvisionerID, reqProv.GetID()))\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// This code will only execute for old ACME accounts that do\n\t\t\t\t// not have a cached location. The following validation was\n\t\t\t\t// the original implementation of the `kid` check which has\n\t\t\t\t// since been deprecated. However, the code will remain to\n\t\t\t\t// ensure consistent behavior for old ACME accounts.\n\t\t\t\tlinker := acme.MustLinkerFromContext(ctx)\n\t\t\t\tkidPrefix := linker.GetLink(ctx, acme.AccountLinkType, \"\")\n\t\t\t\tif !strings.HasPrefix(kid, kidPrefix) {\n\t\t\t\t\trender.Error(w, r, acme.NewError(acme.ErrorMalformedType,\n\t\t\t\t\t\t\"kid does not have required prefix; expected %s, but got %s\",\n\t\t\t\t\t\tkidPrefix, kid))\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\t\t\tctx = context.WithValue(ctx, accContextKey, acc)\n\t\t\tctx = context.WithValue(ctx, jwkContextKey, acc.Key)\n\t\t\tnext(w, r.WithContext(ctx))\n\t\t\treturn\n\t\t}\n\t}\n}\n\n// extractOrLookupJWK forwards handling to either extractJWK or\n// lookupJWK based on the presence of a JWK or a KID, respectively.\nfunc extractOrLookupJWK(next nextHTTP) nextHTTP {\n\treturn func(w http.ResponseWriter, r *http.Request) {\n\t\tctx := r.Context()\n\t\tjws, err := jwsFromContext(ctx)\n\t\tif err != nil {\n\t\t\trender.Error(w, r, err)\n\t\t\treturn\n\t\t}\n\n\t\t// at this point the JWS has already been verified (if correctly configured in middleware),\n\t\t// and it can be used to check if a JWK exists. This flow is used when the ACME client\n\t\t// signed the payload with a certificate private key.\n\t\tif canExtractJWKFrom(jws) {\n\t\t\textractJWK(next)(w, r)\n\t\t\treturn\n\t\t}\n\n\t\t// default to looking up the JWK based on KeyID. This flow is used when the ACME client\n\t\t// signed the payload with an account private key.\n\t\tlookupJWK(next)(w, r)\n\t}\n}\n\n// canExtractJWKFrom checks if the JWS has a JWK that can be extracted\nfunc canExtractJWKFrom(jws *jose.JSONWebSignature) bool {\n\tif jws == nil {\n\t\treturn false\n\t}\n\tif len(jws.Signatures) == 0 {\n\t\treturn false\n\t}\n\treturn jws.Signatures[0].Protected.JSONWebKey != nil\n}\n\n// verifyAndExtractJWSPayload extracts the JWK from the JWS and saves it in the context.\n// Make sure to parse and validate the JWS before running this middleware.\nfunc verifyAndExtractJWSPayload(next nextHTTP) nextHTTP {\n\treturn func(w http.ResponseWriter, r *http.Request) {\n\t\tctx := r.Context()\n\t\tjws, err := jwsFromContext(ctx)\n\t\tif err != nil {\n\t\t\trender.Error(w, r, err)\n\t\t\treturn\n\t\t}\n\t\tjwk, err := jwkFromContext(ctx)\n\t\tif err != nil {\n\t\t\trender.Error(w, r, err)\n\t\t\treturn\n\t\t}\n\t\tif jwk.Algorithm != \"\" && jwk.Algorithm != jws.Signatures[0].Protected.Algorithm {\n\t\t\trender.Error(w, r, acme.NewError(acme.ErrorMalformedType, \"verifier and signature algorithm do not match\"))\n\t\t\treturn\n\t\t}\n\n\t\tpayload, err := jws.Verify(jwk)\n\t\tswitch {\n\t\tcase errors.Is(err, jose.ErrCryptoFailure):\n\t\t\tpayload, err = retryVerificationWithPatchedSignatures(jws, jwk)\n\t\t\tif err != nil {\n\t\t\t\trender.Error(w, r, acme.WrapError(acme.ErrorMalformedType, err, \"error verifying jws with patched signature(s)\"))\n\t\t\t\treturn\n\t\t\t}\n\t\tcase err != nil:\n\t\t\trender.Error(w, r, acme.WrapError(acme.ErrorMalformedType, err, \"error verifying jws\"))\n\t\t\treturn\n\t\t}\n\n\t\tctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{\n\t\t\tvalue:       payload,\n\t\t\tisPostAsGet: len(payload) == 0,\n\t\t\tisEmptyJSON: string(payload) == \"{}\",\n\t\t})\n\t\tnext(w, r.WithContext(ctx))\n\t}\n}\n\n// retryVerificationWithPatchedSignatures retries verification of the JWS using\n// the JWK by patching the JWS signatures if they're determined to be too short.\n//\n// Generally this shouldn't happen, but we've observed this to be the case with\n// the macOS ACME client, which seems to omit (at least one) leading null\n// byte(s). The error returned is `go-jose/go-jose: error in cryptographic\n// primitive`, which is a sentinel error that hides the details of the actual\n// underlying error, which is as follows: `go-jose/go-jose: invalid signature\n// size, have 63 bytes, wanted 64`, for ES256.\nfunc retryVerificationWithPatchedSignatures(jws *jose.JSONWebSignature, jwk *jose.JSONWebKey) (data []byte, err error) {\n\toriginalSignatureValues := make([][]byte, len(jws.Signatures))\n\tpatched := false\n\tdefer func() {\n\t\tif patched && err != nil {\n\t\t\tfor i, sig := range jws.Signatures {\n\t\t\t\tsig.Signature = originalSignatureValues[i]\n\t\t\t\tjws.Signatures[i] = sig\n\t\t\t}\n\t\t}\n\t}()\n\tfor i, sig := range jws.Signatures {\n\t\tvar expectedSize int\n\t\talg := strings.ToUpper(sig.Header.Algorithm)\n\t\tswitch alg {\n\t\tcase jose.ES256:\n\t\t\texpectedSize = 64\n\t\tcase jose.ES384:\n\t\t\texpectedSize = 96\n\t\tcase jose.ES512:\n\t\t\texpectedSize = 132\n\t\tdefault:\n\t\t\t// other cases are (currently) ignored\n\t\t\tcontinue\n\t\t}\n\n\t\tswitch diff := expectedSize - len(sig.Signature); diff {\n\t\tcase 0:\n\t\t\t// expected length; nothing to do; will result in just doing the\n\t\t\t// same verification (as done before calling this function) again,\n\t\t\t// and thus an error will be returned.\n\t\t\tcontinue\n\t\tcase 1:\n\t\t\tpatched = true\n\t\t\toriginal := make([]byte, expectedSize-diff)\n\t\t\tcopy(original, sig.Signature)\n\t\t\toriginalSignatureValues[i] = original\n\n\t\t\tpatchedR := make([]byte, expectedSize)\n\t\t\tcopy(patchedR[1:], original) // [0x00, R.0:31, S.0:32], for expectedSize 64\n\t\t\tsig.Signature = patchedR\n\t\t\tjws.Signatures[i] = sig\n\n\t\t\t// verify it with a patched R; return early if successful; continue\n\t\t\t// with patching S if not.\n\t\t\tdata, err = jws.Verify(jwk)\n\t\t\tif err == nil {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tpatchedS := make([]byte, expectedSize)\n\t\t\thalfSize := expectedSize / 2\n\t\t\tcopy(patchedS, original[:halfSize])              // [R.0:32], for expectedSize 64\n\t\t\tcopy(patchedS[halfSize+1:], original[halfSize:]) // [R.0:32, 0x00, S.0:31]\n\t\t\tsig.Signature = patchedS\n\t\t\tjws.Signatures[i] = sig\n\t\tcase 2:\n\t\t\t// assumption is currently the Apple case, in which only the\n\t\t\t// first null byte of R and/or S are removed, and thus not a case in\n\t\t\t// which two first bytes of either R or S are removed.\n\t\t\tpatched = true\n\t\t\toriginal := make([]byte, expectedSize-diff)\n\t\t\tcopy(original, sig.Signature)\n\t\t\toriginalSignatureValues[i] = original\n\n\t\t\tpatchedRS := make([]byte, expectedSize)\n\t\t\thalfSize := expectedSize / 2\n\t\t\tcopy(patchedRS[1:], original[:halfSize-1])          // [0x00, R.0:31], for expectedSize 64\n\t\t\tcopy(patchedRS[halfSize+1:], original[halfSize-1:]) // [0x00, R.0:31, 0x00, S.0:31]\n\t\t\tsig.Signature = patchedRS\n\t\t\tjws.Signatures[i] = sig\n\t\tdefault:\n\t\t\t// Technically, there can be multiple null bytes in either R or S,\n\t\t\t// so when the difference is larger than 2, there is more than one\n\t\t\t// option to pick. Apple's ACME client seems to only cut off the\n\t\t\t// first null byte of either R or S, so we don't do anything in this\n\t\t\t// case. Will result in just doing the same verification (as done\n\t\t\t// before calling this function) again, and thus an error will be\n\t\t\t// returned.\n\t\t\t// TODO(hs): log this specific case? It might mean some other ACME\n\t\t\t// client is doing weird things.\n\t\t\tcontinue\n\t\t}\n\t}\n\n\tdata, err = jws.Verify(jwk)\n\n\treturn\n}\n\n// isPostAsGet asserts that the request is a PostAsGet (empty JWS payload).\nfunc isPostAsGet(next nextHTTP) nextHTTP {\n\treturn func(w http.ResponseWriter, r *http.Request) {\n\t\tpayload, err := payloadFromContext(r.Context())\n\t\tif err != nil {\n\t\t\trender.Error(w, r, err)\n\t\t\treturn\n\t\t}\n\t\tif !payload.isPostAsGet {\n\t\t\trender.Error(w, r, acme.NewError(acme.ErrorMalformedType, \"expected POST-as-GET\"))\n\t\t\treturn\n\t\t}\n\t\tnext(w, r)\n\t}\n}\n\n// ContextKey is the key type for storing and searching for ACME request\n// essentials in the context of a request.\ntype ContextKey string\n\nconst (\n\t// accContextKey account key\n\taccContextKey = ContextKey(\"acc\")\n\t// jwsContextKey jws key\n\tjwsContextKey = ContextKey(\"jws\")\n\t// jwkContextKey jwk key\n\tjwkContextKey = ContextKey(\"jwk\")\n\t// payloadContextKey payload key\n\tpayloadContextKey = ContextKey(\"payload\")\n)\n\n// accountFromContext searches the context for an ACME account. Returns the\n// account or an error.\nfunc accountFromContext(ctx context.Context) (*acme.Account, error) {\n\tval, ok := ctx.Value(accContextKey).(*acme.Account)\n\tif !ok || val == nil {\n\t\treturn nil, acme.NewError(acme.ErrorAccountDoesNotExistType, \"account not in context\")\n\t}\n\treturn val, nil\n}\n\n// jwkFromContext searches the context for a JWK. Returns the JWK or an error.\nfunc jwkFromContext(ctx context.Context) (*jose.JSONWebKey, error) {\n\tval, ok := ctx.Value(jwkContextKey).(*jose.JSONWebKey)\n\tif !ok || val == nil {\n\t\treturn nil, acme.NewErrorISE(\"jwk expected in request context\")\n\t}\n\treturn val, nil\n}\n\n// jwsFromContext searches the context for a JWS. Returns the JWS or an error.\nfunc jwsFromContext(ctx context.Context) (*jose.JSONWebSignature, error) {\n\tval, ok := ctx.Value(jwsContextKey).(*jose.JSONWebSignature)\n\tif !ok || val == nil {\n\t\treturn nil, acme.NewErrorISE(\"jws expected in request context\")\n\t}\n\treturn val, nil\n}\n\n// provisionerFromContext searches the context for a provisioner. Returns the\n// provisioner or an error.\nfunc provisionerFromContext(ctx context.Context) (acme.Provisioner, error) {\n\tp, ok := acme.ProvisionerFromContext(ctx)\n\tif !ok || p == nil {\n\t\treturn nil, acme.NewErrorISE(\"provisioner expected in request context\")\n\t}\n\treturn p, nil\n}\n\n// acmeProvisionerFromContext searches the context for an ACME provisioner. Returns\n// pointer to an ACME provisioner or an error.\nfunc acmeProvisionerFromContext(ctx context.Context) (*provisioner.ACME, error) {\n\tp, err := provisionerFromContext(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tap, ok := p.(*provisioner.ACME)\n\tif !ok {\n\t\treturn nil, acme.NewErrorISE(\"provisioner in context is not an ACME provisioner\")\n\t}\n\n\treturn ap, nil\n}\n\n// payloadFromContext searches the context for a payload. Returns the payload\n// or an error.\nfunc payloadFromContext(ctx context.Context) (*payloadInfo, error) {\n\tval, ok := ctx.Value(payloadContextKey).(*payloadInfo)\n\tif !ok || val == nil {\n\t\treturn nil, acme.NewErrorISE(\"payload expected in request context\")\n\t}\n\treturn val, nil\n}\n"
  },
  {
    "path": "acme/api/middleware_test.go",
    "content": "package api\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto\"\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/google/uuid\"\n\t\"github.com/smallstep/assert\"\n\t\"github.com/smallstep/certificates/acme\"\n\ttassert \"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"go.step.sm/crypto/jose\"\n\t\"go.step.sm/crypto/keyutil\"\n)\n\nvar testBody = []byte(\"foo\")\n\nfunc testNext(w http.ResponseWriter, _ *http.Request) {\n\tw.Write(testBody)\n}\n\nfunc newBaseContext(ctx context.Context, args ...interface{}) context.Context {\n\tfor _, a := range args {\n\t\tswitch v := a.(type) {\n\t\tcase acme.DB:\n\t\t\tctx = acme.NewDatabaseContext(ctx, v)\n\t\tcase acme.Linker:\n\t\t\tctx = acme.NewLinkerContext(ctx, v)\n\t\tcase acme.PrerequisitesChecker:\n\t\t\tctx = acme.NewPrerequisitesCheckerContext(ctx, v)\n\t\t}\n\t}\n\treturn ctx\n}\n\nfunc TestHandler_addNonce(t *testing.T) {\n\tu := \"https://ca.smallstep.com/acme/new-nonce\"\n\ttype test struct {\n\t\tdb         acme.DB\n\t\terr        *acme.Error\n\t\tstatusCode int\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/AddNonce-error\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tdb: &acme.MockDB{\n\t\t\t\t\tMockCreateNonce: func(ctx context.Context) (acme.Nonce, error) {\n\t\t\t\t\t\treturn acme.Nonce(\"\"), acme.NewErrorISE(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tstatusCode: 500,\n\t\t\t\terr:        acme.NewErrorISE(\"force\"),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tdb: &acme.MockDB{\n\t\t\t\t\tMockCreateNonce: func(ctx context.Context) (acme.Nonce, error) {\n\t\t\t\t\t\treturn \"bar\", nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tstatusCode: 200,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\ttc := run(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tctx := newBaseContext(context.Background(), tc.db)\n\t\t\treq := httptest.NewRequest(\"GET\", u, http.NoBody).WithContext(ctx)\n\t\t\tw := httptest.NewRecorder()\n\t\t\taddNonce(testNext)(w, req)\n\t\t\tres := w.Result()\n\n\t\t\tassert.Equals(t, res.StatusCode, tc.statusCode)\n\n\t\t\tbody, err := io.ReadAll(res.Body)\n\t\t\tres.Body.Close()\n\t\t\tassert.FatalError(t, err)\n\n\t\t\tif res.StatusCode >= 400 && assert.NotNil(t, tc.err) {\n\t\t\t\tvar ae acme.Error\n\t\t\t\tassert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &ae))\n\n\t\t\t\tassert.Equals(t, ae.Type, tc.err.Type)\n\t\t\t\tassert.Equals(t, ae.Detail, tc.err.Detail)\n\t\t\t\tassert.Equals(t, ae.Subproblems, tc.err.Subproblems)\n\t\t\t\tassert.Equals(t, res.Header[\"Content-Type\"], []string{\"application/problem+json\"})\n\t\t\t} else {\n\t\t\t\tassert.Equals(t, res.Header[\"Replay-Nonce\"], []string{\"bar\"})\n\t\t\t\tassert.Equals(t, res.Header[\"Cache-Control\"], []string{\"no-store\"})\n\t\t\t\tassert.Equals(t, bytes.TrimSpace(body), testBody)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestHandler_addDirLink(t *testing.T) {\n\tprov := newProv()\n\tprovName := url.PathEscape(prov.GetName())\n\tbaseURL := &url.URL{Scheme: \"https\", Host: \"test.ca.smallstep.com\"}\n\ttype test struct {\n\t\tlink       string\n\t\tstatusCode int\n\t\tctx        context.Context\n\t\terr        *acme.Error\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tctx := acme.NewProvisionerContext(context.Background(), prov)\n\t\t\tctx = acme.NewLinkerContext(ctx, acme.NewLinker(\"test.ca.smallstep.com\", \"acme\"))\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\tlink:       fmt.Sprintf(\"%s/acme/%s/directory\", baseURL.String(), provName),\n\t\t\t\tstatusCode: 200,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\ttc := run(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\treq := httptest.NewRequest(\"GET\", \"/foo\", http.NoBody)\n\t\t\treq = req.WithContext(tc.ctx)\n\t\t\tw := httptest.NewRecorder()\n\t\t\taddDirLink(testNext)(w, req)\n\t\t\tres := w.Result()\n\n\t\t\tassert.Equals(t, res.StatusCode, tc.statusCode)\n\n\t\t\tbody, err := io.ReadAll(res.Body)\n\t\t\tres.Body.Close()\n\t\t\tassert.FatalError(t, err)\n\n\t\t\tif res.StatusCode >= 400 && assert.NotNil(t, tc.err) {\n\t\t\t\tvar ae acme.Error\n\t\t\t\tassert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &ae))\n\n\t\t\t\tassert.Equals(t, ae.Type, tc.err.Type)\n\t\t\t\tassert.Equals(t, ae.Detail, tc.err.Detail)\n\t\t\t\tassert.Equals(t, ae.Subproblems, tc.err.Subproblems)\n\t\t\t\tassert.Equals(t, res.Header[\"Content-Type\"], []string{\"application/problem+json\"})\n\t\t\t} else {\n\t\t\t\tassert.Equals(t, res.Header[\"Link\"], []string{fmt.Sprintf(\"<%s>;rel=\\\"index\\\"\", tc.link)})\n\t\t\t\tassert.Equals(t, bytes.TrimSpace(body), testBody)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestHandler_verifyContentType(t *testing.T) {\n\tprov := newProv()\n\tescProvName := url.PathEscape(prov.GetName())\n\tbaseURL := &url.URL{Scheme: \"https\", Host: \"test.ca.smallstep.com\"}\n\tu := fmt.Sprintf(\"%s/acme/%s/certificate/abc123\", baseURL.String(), escProvName)\n\ttype test struct {\n\t\tctx         context.Context\n\t\tcontentType string\n\t\terr         *acme.Error\n\t\tstatusCode  int\n\t\turl         string\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/provisioner-not-set\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\turl:         u,\n\t\t\t\tctx:         context.Background(),\n\t\t\t\tcontentType: \"foo\",\n\t\t\t\tstatusCode:  500,\n\t\t\t\terr:         acme.NewErrorISE(\"provisioner expected in request context\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/general-bad-content-type\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\turl:         u,\n\t\t\t\tctx:         acme.NewProvisionerContext(context.Background(), prov),\n\t\t\t\tcontentType: \"foo\",\n\t\t\t\tstatusCode:  400,\n\t\t\t\terr:         acme.NewError(acme.ErrorMalformedType, \"expected content-type to be in [application/jose+json], but got foo\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/certificate-bad-content-type\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tctx:         acme.NewProvisionerContext(context.Background(), prov),\n\t\t\t\tcontentType: \"foo\",\n\t\t\t\tstatusCode:  400,\n\t\t\t\terr:         acme.NewError(acme.ErrorMalformedType, \"expected content-type to be in [application/jose+json application/pkix-cert application/pkcs7-mime], but got foo\"),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tctx:         acme.NewProvisionerContext(context.Background(), prov),\n\t\t\t\tcontentType: \"application/jose+json\",\n\t\t\t\tstatusCode:  200,\n\t\t\t}\n\t\t},\n\t\t\"ok/certificate/pkix-cert\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tctx:         acme.NewProvisionerContext(context.Background(), prov),\n\t\t\t\tcontentType: \"application/pkix-cert\",\n\t\t\t\tstatusCode:  200,\n\t\t\t}\n\t\t},\n\t\t\"ok/certificate/jose+json\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tctx:         acme.NewProvisionerContext(context.Background(), prov),\n\t\t\t\tcontentType: \"application/jose+json\",\n\t\t\t\tstatusCode:  200,\n\t\t\t}\n\t\t},\n\t\t\"ok/certificate/pkcs7-mime\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tctx:         acme.NewProvisionerContext(context.Background(), prov),\n\t\t\t\tcontentType: \"application/pkcs7-mime\",\n\t\t\t\tstatusCode:  200,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\ttc := run(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\t_u := u\n\t\t\tif tc.url != \"\" {\n\t\t\t\t_u = tc.url\n\t\t\t}\n\t\t\treq := httptest.NewRequest(\"GET\", _u, http.NoBody)\n\t\t\treq = req.WithContext(tc.ctx)\n\t\t\treq.Header.Add(\"Content-Type\", tc.contentType)\n\t\t\tw := httptest.NewRecorder()\n\t\t\tverifyContentType(testNext)(w, req)\n\t\t\tres := w.Result()\n\n\t\t\tassert.Equals(t, res.StatusCode, tc.statusCode)\n\n\t\t\tbody, err := io.ReadAll(res.Body)\n\t\t\tres.Body.Close()\n\t\t\tassert.FatalError(t, err)\n\n\t\t\tif res.StatusCode >= 400 && assert.NotNil(t, tc.err) {\n\t\t\t\tvar ae acme.Error\n\t\t\t\tassert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &ae))\n\n\t\t\t\tassert.Equals(t, ae.Type, tc.err.Type)\n\t\t\t\tassert.Equals(t, ae.Detail, tc.err.Detail)\n\t\t\t\tassert.Equals(t, ae.Subproblems, tc.err.Subproblems)\n\t\t\t\tassert.Equals(t, res.Header[\"Content-Type\"], []string{\"application/problem+json\"})\n\t\t\t} else {\n\t\t\t\tassert.Equals(t, bytes.TrimSpace(body), testBody)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestHandler_isPostAsGet(t *testing.T) {\n\tu := \"https://ca.smallstep.com/acme/new-account\"\n\ttype test struct {\n\t\tctx        context.Context\n\t\terr        *acme.Error\n\t\tstatusCode int\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/no-payload\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tctx:        context.Background(),\n\t\t\t\tstatusCode: 500,\n\t\t\t\terr:        acme.NewErrorISE(\"payload expected in request context\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/nil-payload\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tctx:        context.WithValue(context.Background(), payloadContextKey, nil),\n\t\t\t\tstatusCode: 500,\n\t\t\t\terr:        acme.NewErrorISE(\"payload expected in request context\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/not-post-as-get\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tctx:        context.WithValue(context.Background(), payloadContextKey, &payloadInfo{}),\n\t\t\t\tstatusCode: 400,\n\t\t\t\terr:        acme.NewError(acme.ErrorMalformedType, \"expected POST-as-GET\"),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tctx:        context.WithValue(context.Background(), payloadContextKey, &payloadInfo{isPostAsGet: true}),\n\t\t\t\tstatusCode: 200,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\ttc := run(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\t// h := &Handler{}\n\t\t\treq := httptest.NewRequest(\"GET\", u, http.NoBody)\n\t\t\treq = req.WithContext(tc.ctx)\n\t\t\tw := httptest.NewRecorder()\n\t\t\tisPostAsGet(testNext)(w, req)\n\t\t\tres := w.Result()\n\n\t\t\tassert.Equals(t, res.StatusCode, tc.statusCode)\n\n\t\t\tbody, err := io.ReadAll(res.Body)\n\t\t\tres.Body.Close()\n\t\t\tassert.FatalError(t, err)\n\n\t\t\tif res.StatusCode >= 400 && assert.NotNil(t, tc.err) {\n\t\t\t\tvar ae acme.Error\n\t\t\t\tassert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &ae))\n\n\t\t\t\tassert.Equals(t, ae.Type, tc.err.Type)\n\t\t\t\tassert.Equals(t, ae.Detail, tc.err.Detail)\n\t\t\t\tassert.Equals(t, ae.Subproblems, tc.err.Subproblems)\n\t\t\t\tassert.Equals(t, res.Header[\"Content-Type\"], []string{\"application/problem+json\"})\n\t\t\t} else {\n\t\t\t\tassert.Equals(t, bytes.TrimSpace(body), testBody)\n\t\t\t}\n\t\t})\n\t}\n}\n\ntype errReader int\n\nfunc (errReader) Read([]byte) (int, error) {\n\treturn 0, errors.New(\"force\")\n}\nfunc (errReader) Close() error {\n\treturn nil\n}\n\nfunc TestHandler_parseJWS(t *testing.T) {\n\tu := \"https://ca.smallstep.com/acme/new-account\"\n\ttype test struct {\n\t\tnext       nextHTTP\n\t\tbody       io.Reader\n\t\terr        *acme.Error\n\t\tstatusCode int\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/read-body-error\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tbody:       errReader(0),\n\t\t\t\tstatusCode: 500,\n\t\t\t\terr:        acme.NewErrorISE(\"failed to read request body: force\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/parse-jws-error\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tbody:       strings.NewReader(\"foo\"),\n\t\t\t\tstatusCode: 400,\n\t\t\t\terr:        acme.NewError(acme.ErrorMalformedType, \"failed to parse JWS from request body: go-jose/go-jose: compact JWS format must have three parts\"),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\tassert.FatalError(t, err)\n\t\t\tsigner, err := jose.NewSigner(jose.SigningKey{\n\t\t\t\tAlgorithm: jose.SignatureAlgorithm(jwk.Algorithm),\n\t\t\t\tKey:       jwk.Key,\n\t\t\t}, new(jose.SignerOptions))\n\t\t\tassert.FatalError(t, err)\n\t\t\tsigned, err := signer.Sign([]byte(\"baz\"))\n\t\t\tassert.FatalError(t, err)\n\t\t\texpRaw, err := signed.CompactSerialize()\n\t\t\tassert.FatalError(t, err)\n\n\t\t\treturn test{\n\t\t\t\tbody: strings.NewReader(expRaw),\n\t\t\t\tnext: func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\t\tjws, err := jwsFromContext(r.Context())\n\t\t\t\t\tassert.FatalError(t, err)\n\t\t\t\t\tgotRaw, err := jws.CompactSerialize()\n\t\t\t\t\tassert.FatalError(t, err)\n\t\t\t\t\tassert.Equals(t, gotRaw, expRaw)\n\t\t\t\t\tw.Write(testBody)\n\t\t\t\t},\n\t\t\t\tstatusCode: 200,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\ttc := run(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\t// h := &Handler{}\n\t\t\treq := httptest.NewRequest(\"GET\", u, tc.body)\n\t\t\tw := httptest.NewRecorder()\n\t\t\tparseJWS(tc.next)(w, req)\n\t\t\tres := w.Result()\n\n\t\t\tassert.Equals(t, res.StatusCode, tc.statusCode)\n\n\t\t\tbody, err := io.ReadAll(res.Body)\n\t\t\tres.Body.Close()\n\t\t\tassert.FatalError(t, err)\n\n\t\t\tif res.StatusCode >= 400 && assert.NotNil(t, tc.err) {\n\t\t\t\tvar ae acme.Error\n\t\t\t\tassert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &ae))\n\n\t\t\t\tassert.Equals(t, ae.Type, tc.err.Type)\n\t\t\t\tassert.Equals(t, ae.Detail, tc.err.Detail)\n\t\t\t\tassert.Equals(t, ae.Subproblems, tc.err.Subproblems)\n\t\t\t\tassert.Equals(t, res.Header[\"Content-Type\"], []string{\"application/problem+json\"})\n\t\t\t} else {\n\t\t\t\tassert.Equals(t, bytes.TrimSpace(body), testBody)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestHandler_verifyAndExtractJWSPayload(t *testing.T) {\n\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\tassert.FatalError(t, err)\n\t_pub := jwk.Public()\n\tpub := &_pub\n\tso := new(jose.SignerOptions)\n\tso.WithHeader(\"alg\", jose.SignatureAlgorithm(jwk.Algorithm))\n\tsigner, err := jose.NewSigner(jose.SigningKey{\n\t\tAlgorithm: jose.SignatureAlgorithm(jwk.Algorithm),\n\t\tKey:       jwk.Key,\n\t}, so)\n\tassert.FatalError(t, err)\n\tjws, err := signer.Sign([]byte(\"baz\"))\n\tassert.FatalError(t, err)\n\traw, err := jws.CompactSerialize()\n\tassert.FatalError(t, err)\n\tparsedJWS, err := jose.ParseJWS(raw)\n\tassert.FatalError(t, err)\n\tu := \"https://ca.smallstep.com/acme/account/1234\"\n\ttype test struct {\n\t\tctx        context.Context\n\t\tnext       func(http.ResponseWriter, *http.Request)\n\t\terr        *acme.Error\n\t\tstatusCode int\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/no-jws\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tctx:        context.Background(),\n\t\t\t\tstatusCode: 500,\n\t\t\t\terr:        acme.NewErrorISE(\"jws expected in request context\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/nil-jws\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tctx:        context.WithValue(context.Background(), jwsContextKey, nil),\n\t\t\t\tstatusCode: 500,\n\t\t\t\terr:        acme.NewErrorISE(\"jws expected in request context\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/no-jwk\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tctx:        context.WithValue(context.Background(), jwsContextKey, jws),\n\t\t\t\tstatusCode: 500,\n\t\t\t\terr:        acme.NewErrorISE(\"jwk expected in request context\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/nil-jwk\": func(t *testing.T) test {\n\t\t\tctx := context.WithValue(context.Background(), jwsContextKey, parsedJWS)\n\t\t\treturn test{\n\t\t\t\tctx:        context.WithValue(ctx, jwsContextKey, nil),\n\t\t\t\tstatusCode: 500,\n\t\t\t\terr:        acme.NewErrorISE(\"jwk expected in request context\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/verify-jws-failure-wrong-jwk\": func(t *testing.T) test {\n\t\t\t_jwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\tassert.FatalError(t, err)\n\t\t\t_pub := _jwk.Public()\n\t\t\tctx := context.WithValue(context.Background(), jwsContextKey, parsedJWS)\n\t\t\tctx = context.WithValue(ctx, jwkContextKey, &_pub)\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 400,\n\t\t\t\terr:        acme.NewError(acme.ErrorMalformedType, \"error verifying jws: go-jose/go-jose: error in cryptographic primitive\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/verify-jws-failure-too-many-signatures\": func(t *testing.T) test {\n\t\t\tnewParsedJWS, err := jose.ParseJWS(raw)\n\t\t\tassert.FatalError(t, err)\n\t\t\tnewParsedJWS.Signatures = append(newParsedJWS.Signatures, newParsedJWS.Signatures...)\n\t\t\tctx := context.WithValue(context.Background(), jwsContextKey, newParsedJWS)\n\t\t\tctx = context.WithValue(ctx, jwkContextKey, pub)\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 400,\n\t\t\t\terr:        acme.NewError(acme.ErrorMalformedType, \"error verifying jws: go-jose/go-jose: too many signatures in payload; expecting only one\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/apple-acmeclient-omitting-leading-null-byte-in-signature-with-wrong-jwk\": func(t *testing.T) test {\n\t\t\t_jwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\tassert.FatalError(t, err)\n\t\t\t_pub := _jwk.Public()\n\t\t\tappleNullByteCaseBody := `{\"payload\":\"dGVzdC0xMTA1\",\"protected\":\"eyJhbGciOiJFUzI1NiJ9\",\"signature\":\"rQPYKYflfKnlgBKqDeWsJH2TJ6iHAnou7sFzXlmYD4ArXqLfYuqotWERKrna2wfzh0pu7USWO2gzlOqRK9qq\"}`\n\t\t\tappleNullByteCaseJWS, err := jose.ParseJWS(appleNullByteCaseBody)\n\t\t\trequire.NoError(t, err)\n\t\t\tctx := context.WithValue(context.Background(), jwsContextKey, appleNullByteCaseJWS)\n\t\t\tctx = context.WithValue(ctx, jwkContextKey, &_pub)\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 400,\n\t\t\t\terr:        acme.NewError(acme.ErrorMalformedType, \"error verifying jws: go-jose/go-jose: error in cryptographic primitive\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/algorithm-mismatch\": func(t *testing.T) test {\n\t\t\t_pub := *pub\n\t\t\tclone := &_pub\n\t\t\tclone.Algorithm = jose.HS256\n\t\t\tctx := context.WithValue(context.Background(), jwsContextKey, parsedJWS)\n\t\t\tctx = context.WithValue(ctx, jwkContextKey, clone)\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 400,\n\t\t\t\terr:        acme.NewError(acme.ErrorMalformedType, \"verifier and signature algorithm do not match\"),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tctx := context.WithValue(context.Background(), jwsContextKey, parsedJWS)\n\t\t\tctx = context.WithValue(ctx, jwkContextKey, pub)\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 200,\n\t\t\t\tnext: func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\t\tp, err := payloadFromContext(r.Context())\n\t\t\t\t\tassert.FatalError(t, err)\n\t\t\t\t\tif assert.NotNil(t, p) {\n\t\t\t\t\t\tassert.Equals(t, p.value, []byte(\"baz\"))\n\t\t\t\t\t\tassert.False(t, p.isPostAsGet)\n\t\t\t\t\t\tassert.False(t, p.isEmptyJSON)\n\t\t\t\t\t}\n\t\t\t\t\tw.Write(testBody)\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"ok/empty-algorithm-in-jwk\": func(t *testing.T) test {\n\t\t\tctx := context.WithValue(context.Background(), jwsContextKey, parsedJWS)\n\t\t\tctx = context.WithValue(ctx, jwkContextKey, pub)\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 200,\n\t\t\t\tnext: func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\t\tp, err := payloadFromContext(r.Context())\n\t\t\t\t\tassert.FatalError(t, err)\n\t\t\t\t\tif assert.NotNil(t, p) {\n\t\t\t\t\t\tassert.Equals(t, p.value, []byte(\"baz\"))\n\t\t\t\t\t\tassert.False(t, p.isPostAsGet)\n\t\t\t\t\t\tassert.False(t, p.isEmptyJSON)\n\t\t\t\t\t}\n\t\t\t\t\tw.Write(testBody)\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"ok/post-as-get\": func(t *testing.T) test {\n\t\t\t_jws, err := signer.Sign([]byte(\"\"))\n\t\t\tassert.FatalError(t, err)\n\t\t\t_raw, err := _jws.CompactSerialize()\n\t\t\tassert.FatalError(t, err)\n\t\t\t_parsed, err := jose.ParseJWS(_raw)\n\t\t\tassert.FatalError(t, err)\n\t\t\tctx := context.WithValue(context.Background(), jwsContextKey, _parsed)\n\t\t\tctx = context.WithValue(ctx, jwkContextKey, pub)\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 200,\n\t\t\t\tnext: func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\t\tp, err := payloadFromContext(r.Context())\n\t\t\t\t\tassert.FatalError(t, err)\n\t\t\t\t\tif assert.NotNil(t, p) {\n\t\t\t\t\t\tassert.Equals(t, p.value, []byte{})\n\t\t\t\t\t\tassert.True(t, p.isPostAsGet)\n\t\t\t\t\t\tassert.False(t, p.isEmptyJSON)\n\t\t\t\t\t}\n\t\t\t\t\tw.Write(testBody)\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"ok/empty-json\": func(t *testing.T) test {\n\t\t\t_jws, err := signer.Sign([]byte(\"{}\"))\n\t\t\tassert.FatalError(t, err)\n\t\t\t_raw, err := _jws.CompactSerialize()\n\t\t\tassert.FatalError(t, err)\n\t\t\t_parsed, err := jose.ParseJWS(_raw)\n\t\t\tassert.FatalError(t, err)\n\t\t\tctx := context.WithValue(context.Background(), jwsContextKey, _parsed)\n\t\t\tctx = context.WithValue(ctx, jwkContextKey, pub)\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 200,\n\t\t\t\tnext: func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\t\tp, err := payloadFromContext(r.Context())\n\t\t\t\t\tassert.FatalError(t, err)\n\t\t\t\t\tif assert.NotNil(t, p) {\n\t\t\t\t\t\tassert.Equals(t, p.value, []byte(\"{}\"))\n\t\t\t\t\t\tassert.False(t, p.isPostAsGet)\n\t\t\t\t\t\tassert.True(t, p.isEmptyJSON)\n\t\t\t\t\t}\n\t\t\t\t\tw.Write(testBody)\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"ok/apple-acmeclient-omitting-leading-null-byte-in-signature\": func(t *testing.T) test {\n\t\t\tappleNullByteCaseKey := []byte(`{\n\t\t\t\t\"kid\": \"uioinbiTlJICL0MYsb6ar1totfRA2tiPqWgntF8xUdo\",\n\t\t\t\t\"crv\": \"P-256\",\n\t\t\t\t\"alg\": \"ES256\",\n\t\t\t\t\"kty\": \"EC\",\n\t\t\t\t\"x\": \"wlz-Kv9X0h32fzLq-cogls9HxoZQqV-GuWxdb2MCeUY\",\n\t\t\t\t\"y\": \"xzP6zRrg_jynYljZTxfJuql_QWtdQR6lpJ52q_6Vavg\"\n\t\t\t}`)\n\t\t\tappleNullByteCaseJWK := &jose.JSONWebKey{}\n\t\t\terr = json.Unmarshal(appleNullByteCaseKey, appleNullByteCaseJWK)\n\t\t\trequire.NoError(t, err)\n\t\t\tappleNullByteCaseBody := `{\"payload\":\"dGVzdC0xMTA1\",\"protected\":\"eyJhbGciOiJFUzI1NiJ9\",\"signature\":\"rQPYKYflfKnlgBKqDeWsJH2TJ6iHAnou7sFzXlmYD4ArXqLfYuqotWERKrna2wfzh0pu7USWO2gzlOqRK9qq\"}`\n\t\t\tappleNullByteCaseJWS, err := jose.ParseJWS(appleNullByteCaseBody)\n\t\t\trequire.NoError(t, err)\n\t\t\tctx := context.WithValue(context.Background(), jwsContextKey, appleNullByteCaseJWS)\n\t\t\tctx = context.WithValue(ctx, jwkContextKey, appleNullByteCaseJWK)\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 200,\n\t\t\t\tnext: func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\t\tp, err := payloadFromContext(r.Context())\n\t\t\t\t\ttassert.NoError(t, err)\n\t\t\t\t\tif tassert.NotNil(t, p) {\n\t\t\t\t\t\ttassert.Equal(t, []byte(`test-1105`), p.value)\n\t\t\t\t\t\ttassert.False(t, p.isPostAsGet)\n\t\t\t\t\t\ttassert.False(t, p.isEmptyJSON)\n\t\t\t\t\t}\n\t\t\t\t\tw.Write(testBody)\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\ttc := run(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\t// h := &Handler{}\n\t\t\treq := httptest.NewRequest(\"GET\", u, http.NoBody)\n\t\t\treq = req.WithContext(tc.ctx)\n\t\t\tw := httptest.NewRecorder()\n\t\t\tverifyAndExtractJWSPayload(tc.next)(w, req)\n\t\t\tres := w.Result()\n\n\t\t\tassert.Equals(t, res.StatusCode, tc.statusCode)\n\n\t\t\tbody, err := io.ReadAll(res.Body)\n\t\t\tres.Body.Close()\n\t\t\tassert.FatalError(t, err)\n\n\t\t\tif res.StatusCode >= 400 && assert.NotNil(t, tc.err) {\n\t\t\t\tvar ae acme.Error\n\t\t\t\tassert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &ae))\n\n\t\t\t\tassert.Equals(t, ae.Type, tc.err.Type)\n\t\t\t\tassert.Equals(t, ae.Detail, tc.err.Detail)\n\t\t\t\tassert.Equals(t, ae.Subproblems, tc.err.Subproblems)\n\t\t\t\tassert.Equals(t, res.Header[\"Content-Type\"], []string{\"application/problem+json\"})\n\t\t\t} else {\n\t\t\t\tassert.Equals(t, bytes.TrimSpace(body), testBody)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestHandler_lookupJWK(t *testing.T) {\n\tprov := newProv()\n\tprovName := url.PathEscape(prov.GetName())\n\tbaseURL := &url.URL{Scheme: \"https\", Host: \"test.ca.smallstep.com\"}\n\tu := fmt.Sprintf(\"%s/acme/%s/account/1234\",\n\t\tbaseURL, provName)\n\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\tassert.FatalError(t, err)\n\taccID := \"account-id\"\n\tprefix := fmt.Sprintf(\"%s/acme/%s/account/\",\n\t\tbaseURL, provName)\n\tso := new(jose.SignerOptions)\n\tso.WithHeader(\"kid\", fmt.Sprintf(\"%s%s\", prefix, accID))\n\tsigner, err := jose.NewSigner(jose.SigningKey{\n\t\tAlgorithm: jose.SignatureAlgorithm(jwk.Algorithm),\n\t\tKey:       jwk.Key,\n\t}, so)\n\tassert.FatalError(t, err)\n\tjws, err := signer.Sign([]byte(\"baz\"))\n\tassert.FatalError(t, err)\n\traw, err := jws.CompactSerialize()\n\tassert.FatalError(t, err)\n\tparsedJWS, err := jose.ParseJWS(raw)\n\tassert.FatalError(t, err)\n\ttype test struct {\n\t\tlinker     acme.Linker\n\t\tdb         acme.DB\n\t\tctx        context.Context\n\t\tnext       func(http.ResponseWriter, *http.Request)\n\t\terr        *acme.Error\n\t\tstatusCode int\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/no-jws\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tdb:         &acme.MockDB{},\n\t\t\t\tlinker:     acme.NewLinker(\"test.ca.smallstep.com\", \"acme\"),\n\t\t\t\tctx:        acme.NewProvisionerContext(context.Background(), prov),\n\t\t\t\tstatusCode: 500,\n\t\t\t\terr:        acme.NewErrorISE(\"jws expected in request context\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/nil-jws\": func(t *testing.T) test {\n\t\t\tctx := acme.NewProvisionerContext(context.Background(), prov)\n\t\t\tctx = context.WithValue(ctx, jwsContextKey, nil)\n\t\t\treturn test{\n\t\t\t\tdb:         &acme.MockDB{},\n\t\t\t\tlinker:     acme.NewLinker(\"test.ca.smallstep.com\", \"acme\"),\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 500,\n\t\t\t\terr:        acme.NewErrorISE(\"jws expected in request context\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/no-kid\": func(t *testing.T) test {\n\t\t\t_signer, err := jose.NewSigner(jose.SigningKey{\n\t\t\t\tAlgorithm: jose.SignatureAlgorithm(jwk.Algorithm),\n\t\t\t\tKey:       jwk.Key,\n\t\t\t}, new(jose.SignerOptions))\n\t\t\tassert.FatalError(t, err)\n\t\t\t_jws, err := _signer.Sign([]byte(\"baz\"))\n\t\t\tassert.FatalError(t, err)\n\t\t\tctx := acme.NewProvisionerContext(context.Background(), prov)\n\t\t\tctx = context.WithValue(ctx, jwsContextKey, _jws)\n\t\t\treturn test{\n\t\t\t\tdb:         &acme.MockDB{},\n\t\t\t\tlinker:     acme.NewLinker(\"test.ca.smallstep.com\", \"acme\"),\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 400,\n\t\t\t\terr:        acme.NewError(acme.ErrorMalformedType, \"signature missing 'kid'\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/account-not-found\": func(t *testing.T) test {\n\t\t\tctx := acme.NewProvisionerContext(context.Background(), prov)\n\t\t\tctx = context.WithValue(ctx, jwsContextKey, parsedJWS)\n\t\t\treturn test{\n\t\t\t\tlinker: acme.NewLinker(\"test.ca.smallstep.com\", \"acme\"),\n\t\t\t\tdb: &acme.MockDB{\n\t\t\t\t\tMockGetAccount: func(ctx context.Context, accID string) (*acme.Account, error) {\n\t\t\t\t\t\tassert.Equals(t, accID, accID)\n\t\t\t\t\t\treturn nil, acme.ErrNotFound\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 400,\n\t\t\t\terr:        acme.NewError(acme.ErrorAccountDoesNotExistType, \"account does not exist\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/GetAccount-error\": func(t *testing.T) test {\n\t\t\tctx := acme.NewProvisionerContext(context.Background(), prov)\n\t\t\tctx = context.WithValue(ctx, jwsContextKey, parsedJWS)\n\t\t\treturn test{\n\t\t\t\tlinker: acme.NewLinker(\"test.ca.smallstep.com\", \"acme\"),\n\t\t\t\tdb: &acme.MockDB{\n\t\t\t\t\tMockGetAccount: func(ctx context.Context, id string) (*acme.Account, error) {\n\t\t\t\t\t\tassert.Equals(t, id, accID)\n\t\t\t\t\t\treturn nil, acme.NewErrorISE(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 500,\n\t\t\t\terr:        acme.NewErrorISE(\"force\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/account-not-valid\": func(t *testing.T) test {\n\t\t\tacc := &acme.Account{Status: \"deactivated\"}\n\t\t\tctx := acme.NewProvisionerContext(context.Background(), prov)\n\t\t\tctx = context.WithValue(ctx, jwsContextKey, parsedJWS)\n\t\t\treturn test{\n\t\t\t\tlinker: acme.NewLinker(\"test.ca.smallstep.com\", \"acme\"),\n\t\t\t\tdb: &acme.MockDB{\n\t\t\t\t\tMockGetAccount: func(ctx context.Context, id string) (*acme.Account, error) {\n\t\t\t\t\t\tassert.Equals(t, id, accID)\n\t\t\t\t\t\treturn acc, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 401,\n\t\t\t\terr:        acme.NewError(acme.ErrorUnauthorizedType, \"account is not active\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/account-with-location-prefix/bad-kid\": func(t *testing.T) test {\n\t\t\tacc := &acme.Account{LocationPrefix: \"foobar\", Status: \"valid\"}\n\t\t\tctx := acme.NewProvisionerContext(context.Background(), prov)\n\t\t\tctx = context.WithValue(ctx, jwsContextKey, parsedJWS)\n\t\t\treturn test{\n\t\t\t\tlinker: acme.NewLinker(\"test.ca.smallstep.com\", \"acme\"),\n\t\t\t\tdb: &acme.MockDB{\n\t\t\t\t\tMockGetAccount: func(ctx context.Context, id string) (*acme.Account, error) {\n\t\t\t\t\t\tassert.Equals(t, id, accID)\n\t\t\t\t\t\treturn acc, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: http.StatusUnauthorized,\n\t\t\t\terr:        acme.NewError(acme.ErrorUnauthorizedType, \"kid does not match stored account location; expected foobar, but %q\", prefix+accID),\n\t\t\t}\n\t\t},\n\t\t\"fail/account-with-location-prefix/bad-provisioner\": func(t *testing.T) test {\n\t\t\tacc := &acme.Account{LocationPrefix: prefix + accID, Status: \"valid\", Key: jwk, ProvisionerName: \"other\"}\n\t\t\tctx := acme.NewProvisionerContext(context.Background(), prov)\n\t\t\tctx = context.WithValue(ctx, jwsContextKey, parsedJWS)\n\t\t\treturn test{\n\t\t\t\tlinker: acme.NewLinker(\"test.ca.smallstep.com\", \"acme\"),\n\t\t\t\tdb: &acme.MockDB{\n\t\t\t\t\tMockGetAccount: func(ctx context.Context, id string) (*acme.Account, error) {\n\t\t\t\t\t\tassert.Equals(t, id, accID)\n\t\t\t\t\t\treturn acc, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tctx: ctx,\n\t\t\t\tnext: func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\t\t_acc, err := accountFromContext(r.Context())\n\t\t\t\t\tassert.FatalError(t, err)\n\t\t\t\t\tassert.Equals(t, _acc, acc)\n\t\t\t\t\t_jwk, err := jwkFromContext(r.Context())\n\t\t\t\t\tassert.FatalError(t, err)\n\t\t\t\t\tassert.Equals(t, _jwk, jwk)\n\t\t\t\t\tw.Write(testBody)\n\t\t\t\t},\n\t\t\t\tstatusCode: http.StatusUnauthorized,\n\t\t\t\terr: acme.NewError(acme.ErrorUnauthorizedType,\n\t\t\t\t\t\"account provisioner does not match requested provisioner; account provisioner = %s, requested provisioner = %s\",\n\t\t\t\t\t\"other\", prov.GetName()),\n\t\t\t}\n\t\t},\n\t\t\"fail/account-with-location-prefix/bad-provisioner-id\": func(t *testing.T) test {\n\t\t\tp := newProvWithID()\n\t\t\tacc := &acme.Account{LocationPrefix: prefix + accID, Status: \"valid\", Key: jwk, ProvisionerID: uuid.NewString()}\n\t\t\tctx := acme.NewProvisionerContext(context.Background(), p)\n\t\t\tctx = context.WithValue(ctx, jwsContextKey, parsedJWS)\n\t\t\treturn test{\n\t\t\t\tlinker: acme.NewLinker(\"test.ca.smallstep.com\", \"acme\"),\n\t\t\t\tdb: &acme.MockDB{\n\t\t\t\t\tMockGetAccount: func(ctx context.Context, id string) (*acme.Account, error) {\n\t\t\t\t\t\tassert.Equals(t, id, accID)\n\t\t\t\t\t\treturn acc, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tctx: ctx,\n\t\t\t\tnext: func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\t\t_acc, err := accountFromContext(r.Context())\n\t\t\t\t\tassert.FatalError(t, err)\n\t\t\t\t\tassert.Equals(t, _acc, acc)\n\t\t\t\t\t_jwk, err := jwkFromContext(r.Context())\n\t\t\t\t\tassert.FatalError(t, err)\n\t\t\t\t\tassert.Equals(t, _jwk, jwk)\n\t\t\t\t\tw.Write(testBody)\n\t\t\t\t},\n\t\t\t\tstatusCode: http.StatusUnauthorized,\n\t\t\t\terr: acme.NewError(acme.ErrorUnauthorizedType,\n\t\t\t\t\t\"account provisioner does not match requested provisioner; account provisioner = %s, requested provisioner = %s\",\n\t\t\t\t\tacc.ProvisionerID, p.GetID()),\n\t\t\t}\n\t\t},\n\t\t\"ok/account-with-location-prefix\": func(t *testing.T) test {\n\t\t\tacc := &acme.Account{LocationPrefix: prefix + accID, Status: \"valid\", Key: jwk, ProvisionerName: prov.GetName()}\n\t\t\tctx := acme.NewProvisionerContext(context.Background(), prov)\n\t\t\tctx = context.WithValue(ctx, jwsContextKey, parsedJWS)\n\t\t\treturn test{\n\t\t\t\tlinker: acme.NewLinker(\"test.ca.smallstep.com\", \"acme\"),\n\t\t\t\tdb: &acme.MockDB{\n\t\t\t\t\tMockGetAccount: func(ctx context.Context, id string) (*acme.Account, error) {\n\t\t\t\t\t\tassert.Equals(t, id, accID)\n\t\t\t\t\t\treturn acc, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tctx: ctx,\n\t\t\t\tnext: func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\t\t_acc, err := accountFromContext(r.Context())\n\t\t\t\t\tassert.FatalError(t, err)\n\t\t\t\t\tassert.Equals(t, _acc, acc)\n\t\t\t\t\t_jwk, err := jwkFromContext(r.Context())\n\t\t\t\t\tassert.FatalError(t, err)\n\t\t\t\t\tassert.Equals(t, _jwk, jwk)\n\t\t\t\t\tw.Write(testBody)\n\t\t\t\t},\n\t\t\t\tstatusCode: http.StatusOK,\n\t\t\t}\n\t\t},\n\t\t\"ok/account-without-location-prefix\": func(t *testing.T) test {\n\t\t\tacc := &acme.Account{Status: \"valid\", Key: jwk}\n\t\t\tctx := acme.NewProvisionerContext(context.Background(), prov)\n\t\t\tctx = context.WithValue(ctx, jwsContextKey, parsedJWS)\n\t\t\treturn test{\n\t\t\t\tlinker: acme.NewLinker(\"test.ca.smallstep.com\", \"acme\"),\n\t\t\t\tdb: &acme.MockDB{\n\t\t\t\t\tMockGetAccount: func(ctx context.Context, id string) (*acme.Account, error) {\n\t\t\t\t\t\tassert.Equals(t, id, accID)\n\t\t\t\t\t\treturn acc, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tctx: ctx,\n\t\t\t\tnext: func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\t\t_acc, err := accountFromContext(r.Context())\n\t\t\t\t\tassert.FatalError(t, err)\n\t\t\t\t\tassert.Equals(t, _acc, acc)\n\t\t\t\t\t_jwk, err := jwkFromContext(r.Context())\n\t\t\t\t\tassert.FatalError(t, err)\n\t\t\t\t\tassert.Equals(t, _jwk, jwk)\n\t\t\t\t\tw.Write(testBody)\n\t\t\t\t},\n\t\t\t\tstatusCode: 200,\n\t\t\t}\n\t\t},\n\t\t\"ok/account-with-provisioner-id\": func(t *testing.T) test {\n\t\t\tp := newProvWithID()\n\t\t\tacc := &acme.Account{LocationPrefix: prefix + accID, Status: \"valid\", Key: jwk, ProvisionerID: p.GetID()}\n\t\t\tctx := acme.NewProvisionerContext(context.Background(), p)\n\t\t\tctx = context.WithValue(ctx, jwsContextKey, parsedJWS)\n\t\t\treturn test{\n\t\t\t\tlinker: acme.NewLinker(\"test.ca.smallstep.com\", \"acme\"),\n\t\t\t\tdb: &acme.MockDB{\n\t\t\t\t\tMockGetAccount: func(ctx context.Context, id string) (*acme.Account, error) {\n\t\t\t\t\t\tassert.Equals(t, id, accID)\n\t\t\t\t\t\treturn acc, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tctx: ctx,\n\t\t\t\tnext: func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\t\t_acc, err := accountFromContext(r.Context())\n\t\t\t\t\tassert.FatalError(t, err)\n\t\t\t\t\tassert.Equals(t, _acc, acc)\n\t\t\t\t\t_jwk, err := jwkFromContext(r.Context())\n\t\t\t\t\tassert.FatalError(t, err)\n\t\t\t\t\tassert.Equals(t, _jwk, jwk)\n\t\t\t\t\tw.Write(testBody)\n\t\t\t\t},\n\t\t\t\tstatusCode: 200,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\ttc := run(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tctx := newBaseContext(tc.ctx, tc.db, tc.linker)\n\t\t\treq := httptest.NewRequest(\"GET\", u, http.NoBody)\n\t\t\treq = req.WithContext(ctx)\n\t\t\tw := httptest.NewRecorder()\n\t\t\tlookupJWK(tc.next)(w, req)\n\t\t\tres := w.Result()\n\n\t\t\tassert.Equals(t, res.StatusCode, tc.statusCode)\n\n\t\t\tbody, err := io.ReadAll(res.Body)\n\t\t\tres.Body.Close()\n\t\t\tassert.FatalError(t, err)\n\n\t\t\tif res.StatusCode >= 400 && assert.NotNil(t, tc.err) {\n\t\t\t\tvar ae acme.Error\n\t\t\t\tassert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &ae))\n\n\t\t\t\tassert.Equals(t, ae.Type, tc.err.Type)\n\t\t\t\tassert.Equals(t, ae.Detail, tc.err.Detail)\n\t\t\t\tassert.Equals(t, ae.Subproblems, tc.err.Subproblems)\n\t\t\t\tassert.Equals(t, res.Header[\"Content-Type\"], []string{\"application/problem+json\"})\n\t\t\t} else {\n\t\t\t\tassert.Equals(t, bytes.TrimSpace(body), testBody)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestHandler_extractJWK(t *testing.T) {\n\tprov := newProv()\n\tprovName := url.PathEscape(prov.GetName())\n\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\tassert.FatalError(t, err)\n\tkid, err := jwk.Thumbprint(crypto.SHA256)\n\tassert.FatalError(t, err)\n\tpub := jwk.Public()\n\tpub.KeyID = base64.RawURLEncoding.EncodeToString(kid)\n\n\tso := new(jose.SignerOptions)\n\tso.WithHeader(\"jwk\", pub)\n\tsigner, err := jose.NewSigner(jose.SigningKey{\n\t\tAlgorithm: jose.SignatureAlgorithm(jwk.Algorithm),\n\t\tKey:       jwk.Key,\n\t}, so)\n\tassert.FatalError(t, err)\n\tjws, err := signer.Sign([]byte(\"baz\"))\n\tassert.FatalError(t, err)\n\traw, err := jws.CompactSerialize()\n\tassert.FatalError(t, err)\n\tparsedJWS, err := jose.ParseJWS(raw)\n\tassert.FatalError(t, err)\n\tu := fmt.Sprintf(\"https://ca.smallstep.com/acme/%s/account/1234\",\n\t\tprovName)\n\ttype test struct {\n\t\tdb         acme.DB\n\t\tctx        context.Context\n\t\tnext       func(http.ResponseWriter, *http.Request)\n\t\terr        *acme.Error\n\t\tstatusCode int\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/no-jws\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tdb:         &acme.MockDB{},\n\t\t\t\tctx:        acme.NewProvisionerContext(context.Background(), prov),\n\t\t\t\tstatusCode: 500,\n\t\t\t\terr:        acme.NewErrorISE(\"jws expected in request context\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/nil-jws\": func(t *testing.T) test {\n\t\t\tctx := acme.NewProvisionerContext(context.Background(), prov)\n\t\t\tctx = context.WithValue(ctx, jwsContextKey, nil)\n\t\t\treturn test{\n\t\t\t\tdb:         &acme.MockDB{},\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 500,\n\t\t\t\terr:        acme.NewErrorISE(\"jws expected in request context\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/nil-jwk\": func(t *testing.T) test {\n\t\t\t_jws := &jose.JSONWebSignature{\n\t\t\t\tSignatures: []jose.Signature{\n\t\t\t\t\t{\n\t\t\t\t\t\tProtected: jose.Header{\n\t\t\t\t\t\t\tJSONWebKey: nil,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t\tctx := acme.NewProvisionerContext(context.Background(), prov)\n\t\t\tctx = context.WithValue(ctx, jwsContextKey, _jws)\n\t\t\treturn test{\n\t\t\t\tdb:         &acme.MockDB{},\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 400,\n\t\t\t\terr:        acme.NewError(acme.ErrorMalformedType, \"jwk expected in protected header\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/invalid-jwk\": func(t *testing.T) test {\n\t\t\t_jws := &jose.JSONWebSignature{\n\t\t\t\tSignatures: []jose.Signature{\n\t\t\t\t\t{\n\t\t\t\t\t\tProtected: jose.Header{\n\t\t\t\t\t\t\tJSONWebKey: &jose.JSONWebKey{Key: \"foo\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t\tctx := acme.NewProvisionerContext(context.Background(), prov)\n\t\t\tctx = context.WithValue(ctx, jwsContextKey, _jws)\n\t\t\treturn test{\n\t\t\t\tdb:         &acme.MockDB{},\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 400,\n\t\t\t\terr:        acme.NewError(acme.ErrorMalformedType, \"invalid jwk in protected header\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/GetAccountByKey-error\": func(t *testing.T) test {\n\t\t\tctx := acme.NewProvisionerContext(context.Background(), prov)\n\t\t\tctx = context.WithValue(ctx, jwsContextKey, parsedJWS)\n\t\t\treturn test{\n\t\t\t\tctx: ctx,\n\t\t\t\tdb: &acme.MockDB{\n\t\t\t\t\tMockGetAccountByKeyID: func(ctx context.Context, kid string) (*acme.Account, error) {\n\t\t\t\t\t\tassert.Equals(t, kid, pub.KeyID)\n\t\t\t\t\t\treturn nil, acme.NewErrorISE(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tstatusCode: 500,\n\t\t\t\terr:        acme.NewErrorISE(\"force\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/account-not-valid\": func(t *testing.T) test {\n\t\t\tacc := &acme.Account{Status: \"deactivated\"}\n\t\t\tctx := acme.NewProvisionerContext(context.Background(), prov)\n\t\t\tctx = context.WithValue(ctx, jwsContextKey, parsedJWS)\n\t\t\treturn test{\n\t\t\t\tctx: ctx,\n\t\t\t\tdb: &acme.MockDB{\n\t\t\t\t\tMockGetAccountByKeyID: func(ctx context.Context, kid string) (*acme.Account, error) {\n\t\t\t\t\t\tassert.Equals(t, kid, pub.KeyID)\n\t\t\t\t\t\treturn acc, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tstatusCode: 401,\n\t\t\t\terr:        acme.NewError(acme.ErrorUnauthorizedType, \"account is not active\"),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tacc := &acme.Account{Status: \"valid\"}\n\t\t\tctx := acme.NewProvisionerContext(context.Background(), prov)\n\t\t\tctx = context.WithValue(ctx, jwsContextKey, parsedJWS)\n\t\t\treturn test{\n\t\t\t\tctx: ctx,\n\t\t\t\tdb: &acme.MockDB{\n\t\t\t\t\tMockGetAccountByKeyID: func(ctx context.Context, kid string) (*acme.Account, error) {\n\t\t\t\t\t\tassert.Equals(t, kid, pub.KeyID)\n\t\t\t\t\t\treturn acc, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tnext: func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\t\t_acc, err := accountFromContext(r.Context())\n\t\t\t\t\tassert.FatalError(t, err)\n\t\t\t\t\tassert.Equals(t, _acc, acc)\n\t\t\t\t\t_jwk, err := jwkFromContext(r.Context())\n\t\t\t\t\tassert.FatalError(t, err)\n\t\t\t\t\tassert.Equals(t, _jwk.KeyID, pub.KeyID)\n\t\t\t\t\tw.Write(testBody)\n\t\t\t\t},\n\t\t\t\tstatusCode: 200,\n\t\t\t}\n\t\t},\n\t\t\"ok/no-account\": func(t *testing.T) test {\n\t\t\tctx := acme.NewProvisionerContext(context.Background(), prov)\n\t\t\tctx = context.WithValue(ctx, jwsContextKey, parsedJWS)\n\t\t\treturn test{\n\t\t\t\tctx: ctx,\n\t\t\t\tdb: &acme.MockDB{\n\t\t\t\t\tMockGetAccountByKeyID: func(ctx context.Context, kid string) (*acme.Account, error) {\n\t\t\t\t\t\tassert.Equals(t, kid, pub.KeyID)\n\t\t\t\t\t\treturn nil, acme.ErrNotFound\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tnext: func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\t\t_acc, err := accountFromContext(r.Context())\n\t\t\t\t\tassert.NotNil(t, err)\n\t\t\t\t\tassert.Nil(t, _acc)\n\t\t\t\t\t_jwk, err := jwkFromContext(r.Context())\n\t\t\t\t\tassert.FatalError(t, err)\n\t\t\t\t\tassert.Equals(t, _jwk.KeyID, pub.KeyID)\n\t\t\t\t\tw.Write(testBody)\n\t\t\t\t},\n\t\t\t\tstatusCode: 200,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\ttc := run(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tctx := newBaseContext(tc.ctx, tc.db)\n\t\t\treq := httptest.NewRequest(\"GET\", u, http.NoBody)\n\t\t\treq = req.WithContext(ctx)\n\t\t\tw := httptest.NewRecorder()\n\t\t\textractJWK(tc.next)(w, req)\n\t\t\tres := w.Result()\n\n\t\t\tassert.Equals(t, res.StatusCode, tc.statusCode)\n\n\t\t\tbody, err := io.ReadAll(res.Body)\n\t\t\tres.Body.Close()\n\t\t\tassert.FatalError(t, err)\n\n\t\t\tif res.StatusCode >= 400 && assert.NotNil(t, tc.err) {\n\t\t\t\tvar ae acme.Error\n\t\t\t\tassert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &ae))\n\n\t\t\t\tassert.Equals(t, ae.Type, tc.err.Type)\n\t\t\t\tassert.Equals(t, ae.Detail, tc.err.Detail)\n\t\t\t\tassert.Equals(t, ae.Subproblems, tc.err.Subproblems)\n\t\t\t\tassert.Equals(t, res.Header[\"Content-Type\"], []string{\"application/problem+json\"})\n\t\t\t} else {\n\t\t\t\tassert.Equals(t, bytes.TrimSpace(body), testBody)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestHandler_validateJWS(t *testing.T) {\n\tu := \"https://ca.smallstep.com/acme/account/1234\"\n\ttype test struct {\n\t\tdb         acme.DB\n\t\tctx        context.Context\n\t\tnext       func(http.ResponseWriter, *http.Request)\n\t\terr        *acme.Error\n\t\tstatusCode int\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/no-jws\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tdb:         &acme.MockDB{},\n\t\t\t\tctx:        context.Background(),\n\t\t\t\tstatusCode: 500,\n\t\t\t\terr:        acme.NewErrorISE(\"jws expected in request context\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/nil-jws\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tdb:         &acme.MockDB{},\n\t\t\t\tctx:        context.WithValue(context.Background(), jwsContextKey, nil),\n\t\t\t\tstatusCode: 500,\n\t\t\t\terr:        acme.NewErrorISE(\"jws expected in request context\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/no-signature\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tdb:         &acme.MockDB{},\n\t\t\t\tctx:        context.WithValue(context.Background(), jwsContextKey, &jose.JSONWebSignature{}),\n\t\t\t\tstatusCode: 400,\n\t\t\t\terr:        acme.NewError(acme.ErrorMalformedType, \"request body does not contain a signature\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/more-than-one-signature\": func(t *testing.T) test {\n\t\t\tjws := &jose.JSONWebSignature{\n\t\t\t\tSignatures: []jose.Signature{\n\t\t\t\t\t{},\n\t\t\t\t\t{},\n\t\t\t\t},\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tdb:         &acme.MockDB{},\n\t\t\t\tctx:        context.WithValue(context.Background(), jwsContextKey, jws),\n\t\t\t\tstatusCode: 400,\n\t\t\t\terr:        acme.NewError(acme.ErrorMalformedType, \"request body contains more than one signature\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/unprotected-header-not-empty\": func(t *testing.T) test {\n\t\t\tjws := &jose.JSONWebSignature{\n\t\t\t\tSignatures: []jose.Signature{\n\t\t\t\t\t{Unprotected: jose.Header{Nonce: \"abc\"}},\n\t\t\t\t},\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tdb:         &acme.MockDB{},\n\t\t\t\tctx:        context.WithValue(context.Background(), jwsContextKey, jws),\n\t\t\t\tstatusCode: 400,\n\t\t\t\terr:        acme.NewError(acme.ErrorMalformedType, \"unprotected header must not be used\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/unsuitable-algorithm-none\": func(t *testing.T) test {\n\t\t\tjws := &jose.JSONWebSignature{\n\t\t\t\tSignatures: []jose.Signature{\n\t\t\t\t\t{Protected: jose.Header{Algorithm: \"none\"}},\n\t\t\t\t},\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tdb:         &acme.MockDB{},\n\t\t\t\tctx:        context.WithValue(context.Background(), jwsContextKey, jws),\n\t\t\t\tstatusCode: 400,\n\t\t\t\terr:        acme.NewError(acme.ErrorBadSignatureAlgorithmType, \"unsuitable algorithm: none\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/unsuitable-algorithm-mac\": func(t *testing.T) test {\n\t\t\tjws := &jose.JSONWebSignature{\n\t\t\t\tSignatures: []jose.Signature{\n\t\t\t\t\t{Protected: jose.Header{Algorithm: jose.HS256}},\n\t\t\t\t},\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tdb:         &acme.MockDB{},\n\t\t\t\tctx:        context.WithValue(context.Background(), jwsContextKey, jws),\n\t\t\t\tstatusCode: 400,\n\t\t\t\terr:        acme.NewError(acme.ErrorBadSignatureAlgorithmType, \"unsuitable algorithm: %s\", jose.HS256),\n\t\t\t}\n\t\t},\n\t\t\"fail/rsa-key-&-alg-mismatch\": func(t *testing.T) test {\n\t\t\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\tassert.FatalError(t, err)\n\t\t\tpub := jwk.Public()\n\t\t\tjws := &jose.JSONWebSignature{\n\t\t\t\tSignatures: []jose.Signature{\n\t\t\t\t\t{\n\t\t\t\t\t\tProtected: jose.Header{\n\t\t\t\t\t\t\tAlgorithm:  jose.RS256,\n\t\t\t\t\t\t\tJSONWebKey: &pub,\n\t\t\t\t\t\t\tExtraHeaders: map[jose.HeaderKey]interface{}{\n\t\t\t\t\t\t\t\t\"url\": u,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tdb: &acme.MockDB{\n\t\t\t\t\tMockDeleteNonce: func(ctx context.Context, n acme.Nonce) error {\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tctx:        context.WithValue(context.Background(), jwsContextKey, jws),\n\t\t\t\tstatusCode: 400,\n\t\t\t\terr:        acme.NewError(acme.ErrorMalformedType, \"jws key type and algorithm do not match\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/rsa-key-too-small\": func(t *testing.T) test {\n\t\t\trevert := keyutil.Insecure()\n\t\t\tdefer revert()\n\t\t\tjwk, err := jose.GenerateJWK(\"RSA\", \"\", \"\", \"sig\", \"\", 1024)\n\t\t\tassert.FatalError(t, err)\n\t\t\tpub := jwk.Public()\n\t\t\tjws := &jose.JSONWebSignature{\n\t\t\t\tSignatures: []jose.Signature{\n\t\t\t\t\t{\n\t\t\t\t\t\tProtected: jose.Header{\n\t\t\t\t\t\t\tAlgorithm:  jose.RS256,\n\t\t\t\t\t\t\tJSONWebKey: &pub,\n\t\t\t\t\t\t\tExtraHeaders: map[jose.HeaderKey]interface{}{\n\t\t\t\t\t\t\t\t\"url\": u,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tdb: &acme.MockDB{\n\t\t\t\t\tMockDeleteNonce: func(ctx context.Context, n acme.Nonce) error {\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tctx:        context.WithValue(context.Background(), jwsContextKey, jws),\n\t\t\t\tstatusCode: 400,\n\t\t\t\terr:        acme.NewError(acme.ErrorMalformedType, \"rsa keys must be at least 2048 bits (256 bytes) in size\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/UseNonce-error\": func(t *testing.T) test {\n\t\t\tjws := &jose.JSONWebSignature{\n\t\t\t\tSignatures: []jose.Signature{\n\t\t\t\t\t{Protected: jose.Header{Algorithm: jose.ES256}},\n\t\t\t\t},\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tdb: &acme.MockDB{\n\t\t\t\t\tMockDeleteNonce: func(ctx context.Context, n acme.Nonce) error {\n\t\t\t\t\t\treturn acme.NewErrorISE(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tctx:        context.WithValue(context.Background(), jwsContextKey, jws),\n\t\t\t\tstatusCode: 500,\n\t\t\t\terr:        acme.NewErrorISE(\"force\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/no-url-header\": func(t *testing.T) test {\n\t\t\tjws := &jose.JSONWebSignature{\n\t\t\t\tSignatures: []jose.Signature{\n\t\t\t\t\t{Protected: jose.Header{Algorithm: jose.ES256}},\n\t\t\t\t},\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tdb: &acme.MockDB{\n\t\t\t\t\tMockDeleteNonce: func(ctx context.Context, n acme.Nonce) error {\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tctx:        context.WithValue(context.Background(), jwsContextKey, jws),\n\t\t\t\tstatusCode: 400,\n\t\t\t\terr:        acme.NewError(acme.ErrorMalformedType, \"jws missing url protected header\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/url-mismatch\": func(t *testing.T) test {\n\t\t\tjws := &jose.JSONWebSignature{\n\t\t\t\tSignatures: []jose.Signature{\n\t\t\t\t\t{\n\t\t\t\t\t\tProtected: jose.Header{\n\t\t\t\t\t\t\tAlgorithm: jose.ES256,\n\t\t\t\t\t\t\tExtraHeaders: map[jose.HeaderKey]interface{}{\n\t\t\t\t\t\t\t\t\"url\": \"foo\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tdb: &acme.MockDB{\n\t\t\t\t\tMockDeleteNonce: func(ctx context.Context, n acme.Nonce) error {\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tctx:        context.WithValue(context.Background(), jwsContextKey, jws),\n\t\t\t\tstatusCode: 400,\n\t\t\t\terr:        acme.NewError(acme.ErrorMalformedType, \"url header in JWS (foo) does not match request url (%s)\", u),\n\t\t\t}\n\t\t},\n\t\t\"fail/both-jwk-kid\": func(t *testing.T) test {\n\t\t\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\tassert.FatalError(t, err)\n\t\t\tpub := jwk.Public()\n\t\t\tjws := &jose.JSONWebSignature{\n\t\t\t\tSignatures: []jose.Signature{\n\t\t\t\t\t{\n\t\t\t\t\t\tProtected: jose.Header{\n\t\t\t\t\t\t\tAlgorithm:  jose.ES256,\n\t\t\t\t\t\t\tKeyID:      \"bar\",\n\t\t\t\t\t\t\tJSONWebKey: &pub,\n\t\t\t\t\t\t\tExtraHeaders: map[jose.HeaderKey]interface{}{\n\t\t\t\t\t\t\t\t\"url\": u,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tdb: &acme.MockDB{\n\t\t\t\t\tMockDeleteNonce: func(ctx context.Context, n acme.Nonce) error {\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tctx:        context.WithValue(context.Background(), jwsContextKey, jws),\n\t\t\t\tstatusCode: 400,\n\t\t\t\terr:        acme.NewError(acme.ErrorMalformedType, \"jwk and kid are mutually exclusive\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/no-jwk-kid\": func(t *testing.T) test {\n\t\t\tjws := &jose.JSONWebSignature{\n\t\t\t\tSignatures: []jose.Signature{\n\t\t\t\t\t{\n\t\t\t\t\t\tProtected: jose.Header{\n\t\t\t\t\t\t\tAlgorithm: jose.ES256,\n\t\t\t\t\t\t\tExtraHeaders: map[jose.HeaderKey]interface{}{\n\t\t\t\t\t\t\t\t\"url\": u,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tdb: &acme.MockDB{\n\t\t\t\t\tMockDeleteNonce: func(ctx context.Context, n acme.Nonce) error {\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tctx:        context.WithValue(context.Background(), jwsContextKey, jws),\n\t\t\t\tstatusCode: 400,\n\t\t\t\terr:        acme.NewError(acme.ErrorMalformedType, \"either jwk or kid must be defined in jws protected header\"),\n\t\t\t}\n\t\t},\n\t\t\"ok/kid\": func(t *testing.T) test {\n\t\t\tjws := &jose.JSONWebSignature{\n\t\t\t\tSignatures: []jose.Signature{\n\t\t\t\t\t{\n\t\t\t\t\t\tProtected: jose.Header{\n\t\t\t\t\t\t\tAlgorithm: jose.ES256,\n\t\t\t\t\t\t\tKeyID:     \"bar\",\n\t\t\t\t\t\t\tExtraHeaders: map[jose.HeaderKey]interface{}{\n\t\t\t\t\t\t\t\t\"url\": u,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tdb: &acme.MockDB{\n\t\t\t\t\tMockDeleteNonce: func(ctx context.Context, n acme.Nonce) error {\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tctx: context.WithValue(context.Background(), jwsContextKey, jws),\n\t\t\t\tnext: func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\t\tw.Write(testBody)\n\t\t\t\t},\n\t\t\t\tstatusCode: 200,\n\t\t\t}\n\t\t},\n\t\t\"ok/jwk/ecdsa\": func(t *testing.T) test {\n\t\t\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\tassert.FatalError(t, err)\n\t\t\tpub := jwk.Public()\n\t\t\tjws := &jose.JSONWebSignature{\n\t\t\t\tSignatures: []jose.Signature{\n\t\t\t\t\t{\n\t\t\t\t\t\tProtected: jose.Header{\n\t\t\t\t\t\t\tAlgorithm:  jose.ES256,\n\t\t\t\t\t\t\tJSONWebKey: &pub,\n\t\t\t\t\t\t\tExtraHeaders: map[jose.HeaderKey]interface{}{\n\t\t\t\t\t\t\t\t\"url\": u,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tdb: &acme.MockDB{\n\t\t\t\t\tMockDeleteNonce: func(ctx context.Context, n acme.Nonce) error {\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tctx: context.WithValue(context.Background(), jwsContextKey, jws),\n\t\t\t\tnext: func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\t\tw.Write(testBody)\n\t\t\t\t},\n\t\t\t\tstatusCode: 200,\n\t\t\t}\n\t\t},\n\t\t\"ok/jwk/rsa\": func(t *testing.T) test {\n\t\t\tjwk, err := jose.GenerateJWK(\"RSA\", \"\", \"\", \"sig\", \"\", 2048)\n\t\t\tassert.FatalError(t, err)\n\t\t\tpub := jwk.Public()\n\t\t\tjws := &jose.JSONWebSignature{\n\t\t\t\tSignatures: []jose.Signature{\n\t\t\t\t\t{\n\t\t\t\t\t\tProtected: jose.Header{\n\t\t\t\t\t\t\tAlgorithm:  jose.RS256,\n\t\t\t\t\t\t\tJSONWebKey: &pub,\n\t\t\t\t\t\t\tExtraHeaders: map[jose.HeaderKey]interface{}{\n\t\t\t\t\t\t\t\t\"url\": u,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tdb: &acme.MockDB{\n\t\t\t\t\tMockDeleteNonce: func(ctx context.Context, n acme.Nonce) error {\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tctx: context.WithValue(context.Background(), jwsContextKey, jws),\n\t\t\t\tnext: func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\t\tw.Write(testBody)\n\t\t\t\t},\n\t\t\t\tstatusCode: 200,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\ttc := run(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tctx := newBaseContext(tc.ctx, tc.db)\n\t\t\treq := httptest.NewRequest(\"GET\", u, http.NoBody)\n\t\t\treq = req.WithContext(ctx)\n\t\t\tw := httptest.NewRecorder()\n\t\t\tvalidateJWS(tc.next)(w, req)\n\t\t\tres := w.Result()\n\n\t\t\tassert.Equals(t, res.StatusCode, tc.statusCode)\n\n\t\t\tbody, err := io.ReadAll(res.Body)\n\t\t\tres.Body.Close()\n\t\t\tassert.FatalError(t, err)\n\n\t\t\tif res.StatusCode >= 400 && assert.NotNil(t, tc.err) {\n\t\t\t\tvar ae acme.Error\n\t\t\t\tassert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &ae))\n\n\t\t\t\tassert.Equals(t, ae.Type, tc.err.Type)\n\t\t\t\tassert.Equals(t, ae.Detail, tc.err.Detail)\n\t\t\t\tassert.Equals(t, ae.Subproblems, tc.err.Subproblems)\n\t\t\t\tassert.Equals(t, res.Header[\"Content-Type\"], []string{\"application/problem+json\"})\n\t\t\t} else {\n\t\t\t\tassert.Equals(t, bytes.TrimSpace(body), testBody)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_canExtractJWKFrom(t *testing.T) {\n\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\tassert.FatalError(t, err)\n\ttype args struct {\n\t\tjws *jose.JSONWebSignature\n\t}\n\ttests := []struct {\n\t\tname string\n\t\targs args\n\t\twant bool\n\t}{\n\t\t{\n\t\t\tname: \"no-jws\",\n\t\t\targs: args{\n\t\t\t\tjws: nil,\n\t\t\t},\n\t\t\twant: false,\n\t\t},\n\t\t{\n\t\t\tname: \"no-signatures\",\n\t\t\targs: args{\n\t\t\t\tjws: &jose.JSONWebSignature{\n\t\t\t\t\tSignatures: []jose.Signature{},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: false,\n\t\t},\n\t\t{\n\t\t\tname: \"no-jwk\",\n\t\t\targs: args{\n\t\t\t\tjws: &jose.JSONWebSignature{\n\t\t\t\t\tSignatures: []jose.Signature{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tProtected: jose.Header{},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: false,\n\t\t},\n\t\t{\n\t\t\tname: \"ok\",\n\t\t\targs: args{\n\t\t\t\tjws: &jose.JSONWebSignature{\n\t\t\t\t\tSignatures: []jose.Signature{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tProtected: jose.Header{\n\t\t\t\t\t\t\t\tJSONWebKey: jwk,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif got := canExtractJWKFrom(tt.args.jws); got != tt.want {\n\t\t\t\tt.Errorf(\"canExtractJWKFrom() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestHandler_extractOrLookupJWK(t *testing.T) {\n\tu := \"https://ca.smallstep.com/acme/account\"\n\ttype test struct {\n\t\tdb         acme.DB\n\t\tlinker     acme.Linker\n\t\tstatusCode int\n\t\tctx        context.Context\n\t\terr        *acme.Error\n\t\tnext       func(w http.ResponseWriter, r *http.Request)\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"ok/extract\": func(t *testing.T) test {\n\t\t\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\tassert.FatalError(t, err)\n\t\t\tkid, err := jwk.Thumbprint(crypto.SHA256)\n\t\t\tassert.FatalError(t, err)\n\t\t\tpub := jwk.Public()\n\t\t\tpub.KeyID = base64.RawURLEncoding.EncodeToString(kid)\n\t\t\tso := new(jose.SignerOptions)\n\t\t\tso.WithHeader(\"jwk\", pub) // JWK for certificate private key flow\n\t\t\tsigner, err := jose.NewSigner(jose.SigningKey{\n\t\t\t\tAlgorithm: jose.SignatureAlgorithm(jwk.Algorithm),\n\t\t\t\tKey:       jwk.Key,\n\t\t\t}, so)\n\t\t\tassert.FatalError(t, err)\n\t\t\tsigned, err := signer.Sign([]byte(\"foo\"))\n\t\t\tassert.FatalError(t, err)\n\t\t\traw, err := signed.CompactSerialize()\n\t\t\tassert.FatalError(t, err)\n\t\t\tparsedJWS, err := jose.ParseJWS(raw)\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tlinker: acme.NewLinker(\"dns\", \"acme\"),\n\t\t\t\tdb: &acme.MockDB{\n\t\t\t\t\tMockGetAccountByKeyID: func(ctx context.Context, kid string) (*acme.Account, error) {\n\t\t\t\t\t\tassert.Equals(t, kid, pub.KeyID)\n\t\t\t\t\t\treturn nil, acme.ErrNotFound\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tctx:        context.WithValue(context.Background(), jwsContextKey, parsedJWS),\n\t\t\t\tstatusCode: 200,\n\t\t\t\tnext: func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\t\tw.Write(testBody)\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"ok/lookup\": func(t *testing.T) test {\n\t\t\tprov := newProv()\n\t\t\tprovName := url.PathEscape(prov.GetName())\n\t\t\tbaseURL := &url.URL{Scheme: \"https\", Host: \"test.ca.smallstep.com\"}\n\t\t\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\tassert.FatalError(t, err)\n\t\t\taccID := \"accID\"\n\t\t\tprefix := fmt.Sprintf(\"%s/acme/%s/account/\", baseURL, provName)\n\t\t\tso := new(jose.SignerOptions)\n\t\t\tso.WithHeader(\"kid\", fmt.Sprintf(\"%s%s\", prefix, accID)) // KID for account private key flow\n\t\t\tsigner, err := jose.NewSigner(jose.SigningKey{\n\t\t\t\tAlgorithm: jose.SignatureAlgorithm(jwk.Algorithm),\n\t\t\t\tKey:       jwk.Key,\n\t\t\t}, so)\n\t\t\tassert.FatalError(t, err)\n\t\t\tjws, err := signer.Sign([]byte(\"baz\"))\n\t\t\tassert.FatalError(t, err)\n\t\t\traw, err := jws.CompactSerialize()\n\t\t\tassert.FatalError(t, err)\n\t\t\tparsedJWS, err := jose.ParseJWS(raw)\n\t\t\tassert.FatalError(t, err)\n\t\t\tacc := &acme.Account{ID: \"accID\", Key: jwk, Status: \"valid\"}\n\t\t\tctx := acme.NewProvisionerContext(context.Background(), prov)\n\t\t\tctx = context.WithValue(ctx, jwsContextKey, parsedJWS)\n\t\t\treturn test{\n\t\t\t\tlinker: acme.NewLinker(\"test.ca.smallstep.com\", \"acme\"),\n\t\t\t\tdb: &acme.MockDB{\n\t\t\t\t\tMockGetAccount: func(ctx context.Context, accID string) (*acme.Account, error) {\n\t\t\t\t\t\tassert.Equals(t, accID, acc.ID)\n\t\t\t\t\t\treturn acc, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 200,\n\t\t\t\tnext: func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\t\tw.Write(testBody)\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t}\n\tfor name, prep := range tests {\n\t\ttc := prep(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tctx := newBaseContext(tc.ctx, tc.db, tc.linker)\n\t\t\treq := httptest.NewRequest(\"GET\", u, http.NoBody)\n\t\t\treq = req.WithContext(ctx)\n\t\t\tw := httptest.NewRecorder()\n\t\t\textractOrLookupJWK(tc.next)(w, req)\n\t\t\tres := w.Result()\n\n\t\t\tassert.Equals(t, res.StatusCode, tc.statusCode)\n\n\t\t\tbody, err := io.ReadAll(res.Body)\n\t\t\tres.Body.Close()\n\t\t\tassert.FatalError(t, err)\n\n\t\t\tif res.StatusCode >= 400 && assert.NotNil(t, tc.err) {\n\t\t\t\tvar ae acme.Error\n\t\t\t\tassert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &ae))\n\n\t\t\t\tassert.Equals(t, ae.Type, tc.err.Type)\n\t\t\t\tassert.Equals(t, ae.Detail, tc.err.Detail)\n\t\t\t\tassert.Equals(t, ae.Subproblems, tc.err.Subproblems)\n\t\t\t\tassert.Equals(t, res.Header[\"Content-Type\"], []string{\"application/problem+json\"})\n\t\t\t} else {\n\t\t\t\tassert.Equals(t, bytes.TrimSpace(body), testBody)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestHandler_checkPrerequisites(t *testing.T) {\n\tprov := newProv()\n\tprovName := url.PathEscape(prov.GetName())\n\tbaseURL := &url.URL{Scheme: \"https\", Host: \"test.ca.smallstep.com\"}\n\tu := fmt.Sprintf(\"%s/acme/%s/account/1234\",\n\t\tbaseURL, provName)\n\ttype test struct {\n\t\tlinker               acme.Linker\n\t\tctx                  context.Context\n\t\tprerequisitesChecker func(context.Context) (bool, error)\n\t\tnext                 func(http.ResponseWriter, *http.Request)\n\t\terr                  *acme.Error\n\t\tstatusCode           int\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/error\": func(t *testing.T) test {\n\t\t\tctx := acme.NewProvisionerContext(context.Background(), prov)\n\t\t\treturn test{\n\t\t\t\tlinker:               acme.NewLinker(\"dns\", \"acme\"),\n\t\t\t\tctx:                  ctx,\n\t\t\t\tprerequisitesChecker: func(context.Context) (bool, error) { return false, errors.New(\"force\") },\n\t\t\t\tnext: func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\t\tw.Write(testBody)\n\t\t\t\t},\n\t\t\t\terr:        acme.WrapErrorISE(errors.New(\"force\"), \"error checking acme provisioner prerequisites\"),\n\t\t\t\tstatusCode: 500,\n\t\t\t}\n\t\t},\n\t\t\"fail/prerequisites-nok\": func(t *testing.T) test {\n\t\t\tctx := acme.NewProvisionerContext(context.Background(), prov)\n\t\t\treturn test{\n\t\t\t\tlinker:               acme.NewLinker(\"dns\", \"acme\"),\n\t\t\t\tctx:                  ctx,\n\t\t\t\tprerequisitesChecker: func(context.Context) (bool, error) { return false, nil },\n\t\t\t\tnext: func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\t\tw.Write(testBody)\n\t\t\t\t},\n\t\t\t\terr:        acme.NewError(acme.ErrorNotImplementedType, \"acme provisioner configuration lacks prerequisites\"),\n\t\t\t\tstatusCode: 501,\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tctx := acme.NewProvisionerContext(context.Background(), prov)\n\t\t\treturn test{\n\t\t\t\tlinker:               acme.NewLinker(\"dns\", \"acme\"),\n\t\t\t\tctx:                  ctx,\n\t\t\t\tprerequisitesChecker: func(context.Context) (bool, error) { return true, nil },\n\t\t\t\tnext: func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\t\tw.Write(testBody)\n\t\t\t\t},\n\t\t\t\tstatusCode: 200,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\ttc := run(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tctx := acme.NewPrerequisitesCheckerContext(tc.ctx, tc.prerequisitesChecker)\n\t\t\treq := httptest.NewRequest(\"GET\", u, http.NoBody)\n\t\t\treq = req.WithContext(ctx)\n\t\t\tw := httptest.NewRecorder()\n\t\t\tcheckPrerequisites(tc.next)(w, req)\n\t\t\tres := w.Result()\n\n\t\t\tassert.Equals(t, res.StatusCode, tc.statusCode)\n\n\t\t\tbody, err := io.ReadAll(res.Body)\n\t\t\tres.Body.Close()\n\t\t\tassert.FatalError(t, err)\n\n\t\t\tif res.StatusCode >= 400 && assert.NotNil(t, tc.err) {\n\t\t\t\tvar ae acme.Error\n\t\t\t\tassert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &ae))\n\t\t\t\tassert.Equals(t, ae.Type, tc.err.Type)\n\t\t\t\tassert.Equals(t, ae.Detail, tc.err.Detail)\n\t\t\t\tassert.Equals(t, ae.Subproblems, tc.err.Subproblems)\n\t\t\t\tassert.Equals(t, res.Header[\"Content-Type\"], []string{\"application/problem+json\"})\n\t\t\t} else {\n\t\t\t\tassert.Equals(t, bytes.TrimSpace(body), testBody)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_retryVerificationWithPatchedSignatures(t *testing.T) {\n\tpatchedRKey := []byte(`{\n\t\t\"kid\": \"uioinbiTlJICL0MYsb6ar1totfRA2tiPqWgntF8xUdo\",\n\t\t\"crv\": \"P-256\",\n\t\t\"alg\": \"ES256\",\n\t\t\"kty\": \"EC\",\n\t\t\"x\": \"wlz-Kv9X0h32fzLq-cogls9HxoZQqV-GuWxdb2MCeUY\",\n\t\t\"y\": \"xzP6zRrg_jynYljZTxfJuql_QWtdQR6lpJ52q_6Vavg\"\n\t}`)\n\tpatchedRJWK := &jose.JSONWebKey{}\n\terr := json.Unmarshal(patchedRKey, patchedRJWK)\n\trequire.NoError(t, err)\n\tpatchedRBody := `{\"payload\":\"dGVzdC0xMTA1\",\"protected\":\"eyJhbGciOiJFUzI1NiJ9\",\"signature\":\"rQPYKYflfKnlgBKqDeWsJH2TJ6iHAnou7sFzXlmYD4ArXqLfYuqotWERKrna2wfzh0pu7USWO2gzlOqRK9qq\"}`\n\tpatchedR, err := jose.ParseJWS(patchedRBody)\n\trequire.NoError(t, err)\n\n\tpatchedSKey := []byte(`{\n\t\t\"kid\": \"PblXsnK59uTiF5k3mmAN2B6HDPPxqBL_4UGhEG8ZO6g\",\n\t\t\"crv\": \"P-256\",\n\t\t\"alg\": \"ES256\",\n\t\t\"kty\": \"EC\",\n\t\t\"x\": \"T5aM_TOSattXNeUkH1VHZXh8URzdjZTI2zLvVgI0cy0\",\n\t\t\"y\": \"Lf8h8qZnURXIxm6OnQ69kxGC91YtTZRD2GAroEf1UA8\"\n\t}`)\n\tpatchedSJWK := &jose.JSONWebKey{}\n\terr = json.Unmarshal(patchedSKey, patchedSJWK)\n\trequire.NoError(t, err)\n\tpatchedSBody := `{\"payload\":\"dGVzdC02Ng\",\"protected\":\"eyJhbGciOiJFUzI1NiJ9\",\"signature\":\"krtSKSgVB04oqx6i9QLeal_wZSnjV1_PSIM3AubT0WRIxnhl_yYbVpa3i53p3dUW56TtP6_SUZboH6SvLHMz\"}`\n\tpatchedS, err := jose.ParseJWS(patchedSBody)\n\trequire.NoError(t, err)\n\n\tpatchedRSKey := []byte(`{\n\t\t\"kid\": \"U8BmBVbZsNUawvhOomJQPa6uYj1rdxCPQWF_nOLVsc4\",\n\t\t\"crv\": \"P-256\",\n\t\t\"alg\": \"ES256\",\n\t\t\"kty\": \"EC\",\n\t\t\"x\": \"Ym0l3GMS6aHBLo-xe73Kub4kafnOBu_QAfOsx5y-bV0\",\n\t\t\"y\": \"wKijX9Cu67HbK94StPcI18WulgRfIMbP2ZU7gQuf3-M\"\n\t}`)\n\tpatchedRSJWK := &jose.JSONWebKey{}\n\terr = json.Unmarshal(patchedRSKey, patchedRSJWK)\n\trequire.NoError(t, err)\n\tpatchedRSBody := `{\"payload\":\"dGVzdC05MDY3\",\"protected\":\"eyJhbGciOiJFUzI1NiJ9\",\"signature\":\"2r_My19oRg7mWf9I5JTkNYp8otfEMz-yXRA8ltZTAKZxyJLurpVEgicmNItu7lfcCrGrTgI3Obye_gSaIyc\"}`\n\tpatchedRS, err := jose.ParseJWS(patchedRSBody)\n\trequire.NoError(t, err)\n\n\tpatchedRWithWrongJWK, err := jose.ParseJWS(patchedRBody)\n\trequire.NoError(t, err)\n\n\ttests := []struct {\n\t\tname              string\n\t\tjws               *jose.JSONWebSignature\n\t\tjwk               *jose.JSONWebKey\n\t\texpectedData      []byte\n\t\texpectedSignature string\n\t\texpectedError     error\n\t}{\n\t\t{\"ok/patched-r\", patchedR, patchedRJWK, []byte(`test-1105`), `AK0D2CmH5Xyp5YASqg3lrCR9kyeohwJ6Lu7Bc15ZmA-AK16i32LqqLVhESq52tsH84dKbu1EljtoM5TqkSvaqg`, nil},\n\t\t{\"ok/patched-s\", patchedS, patchedSJWK, []byte(`test-66`), `krtSKSgVB04oqx6i9QLeal_wZSnjV1_PSIM3AubT0WQASMZ4Zf8mG1aWt4ud6d3VFuek7T-v0lGW6B-kryxzMw`, nil},\n\t\t{\"ok/patched-rs\", patchedRS, patchedRSJWK, []byte(`test-9067`), `ANq_zMtfaEYO5ln_SOSU5DWKfKLXxDM_sl0QPJbWUwAApnHIku6ulUSCJyY0i27uV9wKsatOAjc5vJ7-BJojJw`, nil},\n\t\t{\"fail/patched-r-wrong-jwk\", patchedRWithWrongJWK, patchedRSJWK, nil, `rQPYKYflfKnlgBKqDeWsJH2TJ6iHAnou7sFzXlmYD4ArXqLfYuqotWERKrna2wfzh0pu7USWO2gzlOqRK9qq`, errors.New(\"go-jose/go-jose: error in cryptographic primitive\")},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\texpectedSignature, decodeErr := base64.RawURLEncoding.DecodeString(tt.expectedSignature)\n\t\t\trequire.NoError(t, decodeErr)\n\n\t\t\tdata, err := retryVerificationWithPatchedSignatures(tt.jws, tt.jwk)\n\t\t\tif tt.expectedError != nil {\n\t\t\t\ttassert.EqualError(t, err, tt.expectedError.Error())\n\t\t\t\ttassert.Equal(t, expectedSignature, tt.jws.Signatures[0].Signature)\n\t\t\t\ttassert.Empty(t, data)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\ttassert.NoError(t, err)\n\t\t\ttassert.Len(t, tt.jws.Signatures[0].Signature, 64)\n\t\t\ttassert.Equal(t, expectedSignature, tt.jws.Signatures[0].Signature)\n\t\t\ttassert.Equal(t, tt.expectedData, data)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "acme/api/order.go",
    "content": "package api\n\nimport (\n\t\"context\"\n\t\"crypto/x509\"\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/http\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go-chi/chi/v5\"\n\n\t\"go.step.sm/crypto/randutil\"\n\t\"go.step.sm/crypto/x509util\"\n\n\t\"github.com/smallstep/certificates/acme\"\n\t\"github.com/smallstep/certificates/acme/wire\"\n\t\"github.com/smallstep/certificates/api/render\"\n\t\"github.com/smallstep/certificates/authority/policy\"\n\t\"github.com/smallstep/certificates/authority/provisioner\"\n)\n\n// NewOrderRequest represents the body for a NewOrder request.\ntype NewOrderRequest struct {\n\tIdentifiers []acme.Identifier `json:\"identifiers\"`\n\tNotBefore   time.Time         `json:\"notBefore,omitempty\"`\n\tNotAfter    time.Time         `json:\"notAfter,omitempty\"`\n}\n\n// Validate validates a new-order request body.\nfunc (n *NewOrderRequest) Validate() error {\n\tif len(n.Identifiers) == 0 {\n\t\treturn acme.NewError(acme.ErrorMalformedType, \"identifiers list cannot be empty\")\n\t}\n\tfor _, id := range n.Identifiers {\n\t\tswitch id.Type {\n\t\tcase acme.IP:\n\t\t\tif net.ParseIP(id.Value) == nil {\n\t\t\t\treturn acme.NewError(acme.ErrorMalformedType, \"invalid IP address: %s\", id.Value)\n\t\t\t}\n\t\tcase acme.DNS:\n\t\t\tvalue, _ := trimIfWildcard(id.Value)\n\t\t\tif _, err := x509util.SanitizeName(value); err != nil {\n\t\t\t\treturn acme.NewError(acme.ErrorMalformedType, \"invalid DNS name: %s\", id.Value)\n\t\t\t}\n\t\tcase acme.PermanentIdentifier:\n\t\t\tif id.Value == \"\" {\n\t\t\t\treturn acme.NewError(acme.ErrorMalformedType, \"permanent identifier cannot be empty\")\n\t\t\t}\n\t\tcase acme.WireUser, acme.WireDevice:\n\t\t\t// validation of Wire identifiers is performed in `validateWireIdentifiers`, but\n\t\t\t// marked here as known and supported types.\n\t\t\tcontinue\n\t\tdefault:\n\t\t\treturn acme.NewError(acme.ErrorMalformedType, \"identifier type unsupported: %s\", id.Type)\n\t\t}\n\t}\n\n\tif err := n.validateWireIdentifiers(); err != nil {\n\t\treturn acme.WrapError(acme.ErrorMalformedType, err, \"failed validating Wire identifiers\")\n\t}\n\n\t// TODO(hs): add some validations for DNS domains?\n\t// TODO(hs): combine the errors from this with allow/deny policy, like example error in https://datatracker.ietf.org/doc/html/rfc8555#section-6.7.1\n\n\treturn nil\n}\n\nfunc (n *NewOrderRequest) validateWireIdentifiers() error {\n\tif !n.hasWireIdentifiers() {\n\t\treturn nil\n\t}\n\n\tuserIdentifiers := identifiersOfType(acme.WireUser, n.Identifiers)\n\tdeviceIdentifiers := identifiersOfType(acme.WireDevice, n.Identifiers)\n\n\tif len(userIdentifiers) != 1 {\n\t\treturn fmt.Errorf(\"expected exactly one Wire UserID identifier; got %d\", len(userIdentifiers))\n\t}\n\tif len(deviceIdentifiers) != 1 {\n\t\treturn fmt.Errorf(\"expected exactly one Wire DeviceID identifier, got %d\", len(deviceIdentifiers))\n\t}\n\n\twireUserID, err := wire.ParseUserID(userIdentifiers[0].Value)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed parsing Wire UserID: %w\", err)\n\t}\n\n\twireDeviceID, err := wire.ParseDeviceID(deviceIdentifiers[0].Value)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed parsing Wire DeviceID: %w\", err)\n\t}\n\tif _, err := wire.ParseClientID(wireDeviceID.ClientID); err != nil {\n\t\treturn fmt.Errorf(\"invalid Wire client ID %q: %w\", wireDeviceID.ClientID, err)\n\t}\n\n\tswitch {\n\tcase wireUserID.Domain != wireDeviceID.Domain:\n\t\treturn fmt.Errorf(\"UserID domain %q does not match DeviceID domain %q\", wireUserID.Domain, wireDeviceID.Domain)\n\tcase wireUserID.Name != wireDeviceID.Name:\n\t\treturn fmt.Errorf(\"UserID name %q does not match DeviceID name %q\", wireUserID.Name, wireDeviceID.Name)\n\tcase wireUserID.Handle != wireDeviceID.Handle:\n\t\treturn fmt.Errorf(\"UserID handle %q does not match DeviceID handle %q\", wireUserID.Handle, wireDeviceID.Handle)\n\t}\n\n\treturn nil\n}\n\n// hasWireIdentifiers returns whether the [NewOrderRequest] contains\n// Wire identifiers.\nfunc (n *NewOrderRequest) hasWireIdentifiers() bool {\n\tfor _, i := range n.Identifiers {\n\t\tif i.Type == acme.WireUser || i.Type == acme.WireDevice {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// identifiersOfType returns the Identifiers that are of type typ.\nfunc identifiersOfType(typ acme.IdentifierType, ids []acme.Identifier) (result []acme.Identifier) {\n\tfor _, id := range ids {\n\t\tif id.Type == typ {\n\t\t\tresult = append(result, id)\n\t\t}\n\t}\n\treturn\n}\n\n// FinalizeRequest captures the body for a Finalize order request.\ntype FinalizeRequest struct {\n\tCSR string `json:\"csr\"`\n\tcsr *x509.CertificateRequest\n}\n\n// Validate validates a finalize request body.\nfunc (f *FinalizeRequest) Validate() error {\n\tvar err error\n\t// RFC 8555 isn't 100% conclusive about using raw base64-url encoding for the\n\t// CSR specifically, instead of \"normal\" base64-url encoding (incl. padding).\n\t// By trimming the padding from CSRs submitted by ACME clients that use\n\t// base64-url encoding instead of raw base64-url encoding, these are also\n\t// supported. This was reported in https://github.com/smallstep/certificates/issues/939\n\t// to be the case for a Synology DSM NAS system.\n\tcsrBytes, err := base64.RawURLEncoding.DecodeString(strings.TrimRight(f.CSR, \"=\"))\n\tif err != nil {\n\t\treturn acme.WrapError(acme.ErrorMalformedType, err, \"error base64url decoding csr\")\n\t}\n\tf.csr, err = x509.ParseCertificateRequest(csrBytes)\n\tif err != nil {\n\t\treturn acme.WrapError(acme.ErrorMalformedType, err, \"unable to parse csr\")\n\t}\n\tif err = f.csr.CheckSignature(); err != nil {\n\t\treturn acme.WrapError(acme.ErrorMalformedType, err, \"csr failed signature check\")\n\t}\n\treturn nil\n}\n\nvar defaultOrderExpiry = time.Hour * 24\nvar defaultOrderBackdate = time.Minute\n\n// NewOrder ACME api for creating a new order.\nfunc NewOrder(w http.ResponseWriter, r *http.Request) {\n\tctx := r.Context()\n\tca := mustAuthority(ctx)\n\tdb := acme.MustDatabaseFromContext(ctx)\n\tlinker := acme.MustLinkerFromContext(ctx)\n\n\tacc, err := accountFromContext(ctx)\n\tif err != nil {\n\t\trender.Error(w, r, err)\n\t\treturn\n\t}\n\tprov, err := provisionerFromContext(ctx)\n\tif err != nil {\n\t\trender.Error(w, r, err)\n\t\treturn\n\t}\n\tpayload, err := payloadFromContext(ctx)\n\tif err != nil {\n\t\trender.Error(w, r, err)\n\t\treturn\n\t}\n\n\tvar nor NewOrderRequest\n\tif err := json.Unmarshal(payload.value, &nor); err != nil {\n\t\trender.Error(w, r, acme.WrapError(acme.ErrorMalformedType, err,\n\t\t\t\"failed to unmarshal new-order request payload\"))\n\t\treturn\n\t}\n\n\tif err := nor.Validate(); err != nil {\n\t\trender.Error(w, r, err)\n\t\treturn\n\t}\n\n\t// TODO(hs): gather all errors, so that we can build one response with ACME subproblems\n\t// include the nor.Validate() error here too, like in the example in the ACME RFC?\n\n\tacmeProv, err := acmeProvisionerFromContext(ctx)\n\tif err != nil {\n\t\trender.Error(w, r, err)\n\t\treturn\n\t}\n\n\tvar eak *acme.ExternalAccountKey\n\tif acmeProv.RequireEAB {\n\t\tif eak, err = db.GetExternalAccountKeyByAccountID(ctx, prov.GetID(), acc.ID); err != nil {\n\t\t\trender.Error(w, r, acme.WrapErrorISE(err, \"error retrieving external account binding key\"))\n\t\t\treturn\n\t\t}\n\t}\n\n\tacmePolicy, err := newACMEPolicyEngine(eak)\n\tif err != nil {\n\t\trender.Error(w, r, acme.WrapErrorISE(err, \"error creating ACME policy engine\"))\n\t\treturn\n\t}\n\n\tfor _, identifier := range nor.Identifiers {\n\t\t// evaluate the ACME account level policy\n\t\tif err = isIdentifierAllowed(acmePolicy, identifier); err != nil {\n\t\t\trender.Error(w, r, acme.WrapError(acme.ErrorRejectedIdentifierType, err, \"not authorized\"))\n\t\t\treturn\n\t\t}\n\t\t// evaluate the provisioner level policy\n\t\torderIdentifier := provisioner.ACMEIdentifier{Type: provisioner.ACMEIdentifierType(identifier.Type), Value: identifier.Value}\n\t\tif err = prov.AuthorizeOrderIdentifier(ctx, orderIdentifier); err != nil {\n\t\t\trender.Error(w, r, acme.WrapError(acme.ErrorRejectedIdentifierType, err, \"not authorized\"))\n\t\t\treturn\n\t\t}\n\t\t// evaluate the authority level policy\n\t\tif err = ca.AreSANsAllowed(ctx, []string{identifier.Value}); err != nil {\n\t\t\trender.Error(w, r, acme.WrapError(acme.ErrorRejectedIdentifierType, err, \"not authorized\"))\n\t\t\treturn\n\t\t}\n\t}\n\n\tnow := clock.Now()\n\t// New order.\n\to := &acme.Order{\n\t\tAccountID:        acc.ID,\n\t\tProvisionerID:    prov.GetID(),\n\t\tStatus:           acme.StatusPending,\n\t\tIdentifiers:      nor.Identifiers,\n\t\tExpiresAt:        now.Add(defaultOrderExpiry),\n\t\tAuthorizationIDs: make([]string, len(nor.Identifiers)),\n\t\tNotBefore:        nor.NotBefore,\n\t\tNotAfter:         nor.NotAfter,\n\t}\n\n\tfor i, identifier := range o.Identifiers {\n\t\taz := &acme.Authorization{\n\t\t\tAccountID:  acc.ID,\n\t\t\tIdentifier: identifier,\n\t\t\tExpiresAt:  o.ExpiresAt,\n\t\t\tStatus:     acme.StatusPending,\n\t\t}\n\t\tif err := newAuthorization(ctx, az); err != nil {\n\t\t\trender.Error(w, r, err)\n\t\t\treturn\n\t\t}\n\t\to.AuthorizationIDs[i] = az.ID\n\t}\n\n\tif o.NotBefore.IsZero() {\n\t\to.NotBefore = now\n\t}\n\tif o.NotAfter.IsZero() {\n\t\to.NotAfter = o.NotBefore.Add(prov.DefaultTLSCertDuration())\n\t}\n\n\t// if request NotBefore was empty, then backdate the order.NotBefore (now)\n\t// to avoid timing issues.\n\tif nor.NotBefore.IsZero() {\n\t\tbackdate := defaultOrderBackdate\n\t\tif bd := ca.GetBackdate(); bd != nil {\n\t\t\tbackdate = *bd\n\t\t}\n\t\to.NotBefore = o.NotBefore.Add(-backdate)\n\t}\n\n\tif err := db.CreateOrder(ctx, o); err != nil {\n\t\trender.Error(w, r, acme.WrapErrorISE(err, \"error creating order\"))\n\t\treturn\n\t}\n\n\tlinker.LinkOrder(ctx, o)\n\n\tw.Header().Set(\"Location\", linker.GetLink(ctx, acme.OrderLinkType, o.ID))\n\trender.JSONStatus(w, r, o, http.StatusCreated)\n}\n\nfunc isIdentifierAllowed(acmePolicy policy.X509Policy, identifier acme.Identifier) error {\n\tif acmePolicy == nil {\n\t\treturn nil\n\t}\n\treturn acmePolicy.AreSANsAllowed([]string{identifier.Value})\n}\n\nfunc newACMEPolicyEngine(eak *acme.ExternalAccountKey) (policy.X509Policy, error) {\n\tif eak == nil {\n\t\t//nolint:nilnil,nolintlint // expected values\n\t\treturn nil, nil\n\t}\n\treturn policy.NewX509PolicyEngine(eak.Policy)\n}\n\nfunc trimIfWildcard(value string) (string, bool) {\n\tif strings.HasPrefix(value, \"*.\") {\n\t\treturn strings.TrimPrefix(value, \"*.\"), true\n\t}\n\treturn value, false\n}\n\nfunc newAuthorization(ctx context.Context, az *acme.Authorization) error {\n\tvalue, isWildcard := trimIfWildcard(az.Identifier.Value)\n\taz.Wildcard = isWildcard\n\taz.Identifier = acme.Identifier{\n\t\tValue: value,\n\t\tType:  az.Identifier.Type,\n\t}\n\n\tchTypes := challengeTypes(az)\n\n\tvar err error\n\taz.Token, err = randutil.Alphanumeric(32)\n\tif err != nil {\n\t\treturn acme.WrapErrorISE(err, \"error generating random alphanumeric ID\")\n\t}\n\n\tdb := acme.MustDatabaseFromContext(ctx)\n\tprov := acme.MustProvisionerFromContext(ctx)\n\taz.Challenges = make([]*acme.Challenge, 0, len(chTypes))\n\tfor _, typ := range chTypes {\n\t\tif !prov.IsChallengeEnabled(ctx, provisioner.ACMEChallenge(typ)) {\n\t\t\tcontinue\n\t\t}\n\n\t\tvar target string\n\t\tswitch az.Identifier.Type {\n\t\tcase acme.WireUser:\n\t\t\twireOptions, err := prov.GetOptions().GetWireOptions()\n\t\t\tif err != nil {\n\t\t\t\treturn acme.WrapErrorISE(err, \"failed getting Wire options\")\n\t\t\t}\n\t\t\ttarget, err = wireOptions.GetOIDCOptions().EvaluateTarget(\"\") // TODO(hs): determine if required by Wire\n\t\t\tif err != nil {\n\t\t\t\treturn acme.WrapError(acme.ErrorMalformedType, err, \"invalid Go template registered for 'target'\")\n\t\t\t}\n\t\tcase acme.WireDevice:\n\t\t\twireID, err := wire.ParseDeviceID(az.Identifier.Value)\n\t\t\tif err != nil {\n\t\t\t\treturn acme.WrapError(acme.ErrorMalformedType, err, \"failed parsing WireDevice\")\n\t\t\t}\n\t\t\tclientID, err := wire.ParseClientID(wireID.ClientID)\n\t\t\tif err != nil {\n\t\t\t\treturn acme.WrapError(acme.ErrorMalformedType, err, \"failed parsing ClientID\")\n\t\t\t}\n\t\t\twireOptions, err := prov.GetOptions().GetWireOptions()\n\t\t\tif err != nil {\n\t\t\t\treturn acme.WrapErrorISE(err, \"failed getting Wire options\")\n\t\t\t}\n\t\t\ttarget, err = wireOptions.GetDPOPOptions().EvaluateTarget(clientID.DeviceID)\n\t\t\tif err != nil {\n\t\t\t\treturn acme.WrapError(acme.ErrorMalformedType, err, \"invalid Go template registered for 'target'\")\n\t\t\t}\n\t\t}\n\n\t\tch := &acme.Challenge{\n\t\t\tAccountID: az.AccountID,\n\t\t\tValue:     az.Identifier.Value,\n\t\t\tType:      typ,\n\t\t\tToken:     az.Token,\n\t\t\tStatus:    acme.StatusPending,\n\t\t\tTarget:    target,\n\t\t}\n\t\tif err := db.CreateChallenge(ctx, ch); err != nil {\n\t\t\treturn acme.WrapErrorISE(err, \"error creating challenge\")\n\t\t}\n\t\taz.Challenges = append(az.Challenges, ch)\n\t}\n\tif err = db.CreateAuthorization(ctx, az); err != nil {\n\t\treturn acme.WrapErrorISE(err, \"error creating authorization\")\n\t}\n\treturn nil\n}\n\n// GetOrder ACME api for retrieving an order.\nfunc GetOrder(w http.ResponseWriter, r *http.Request) {\n\tctx := r.Context()\n\tdb := acme.MustDatabaseFromContext(ctx)\n\tlinker := acme.MustLinkerFromContext(ctx)\n\n\tacc, err := accountFromContext(ctx)\n\tif err != nil {\n\t\trender.Error(w, r, err)\n\t\treturn\n\t}\n\tprov, err := provisionerFromContext(ctx)\n\tif err != nil {\n\t\trender.Error(w, r, err)\n\t\treturn\n\t}\n\n\to, err := db.GetOrder(ctx, chi.URLParam(r, \"ordID\"))\n\tif err != nil {\n\t\trender.Error(w, r, acme.WrapErrorISE(err, \"error retrieving order\"))\n\t\treturn\n\t}\n\tif acc.ID != o.AccountID {\n\t\trender.Error(w, r, acme.NewError(acme.ErrorUnauthorizedType,\n\t\t\t\"account '%s' does not own order '%s'\", acc.ID, o.ID))\n\t\treturn\n\t}\n\tif prov.GetID() != o.ProvisionerID {\n\t\trender.Error(w, r, acme.NewError(acme.ErrorUnauthorizedType,\n\t\t\t\"provisioner '%s' does not own order '%s'\", prov.GetID(), o.ID))\n\t\treturn\n\t}\n\tif err = o.UpdateStatus(ctx, db); err != nil {\n\t\trender.Error(w, r, acme.WrapErrorISE(err, \"error updating order status\"))\n\t\treturn\n\t}\n\n\tlinker.LinkOrder(ctx, o)\n\n\tw.Header().Set(\"Location\", linker.GetLink(ctx, acme.OrderLinkType, o.ID))\n\trender.JSON(w, r, o)\n}\n\n// FinalizeOrder attempts to finalize an order and create a certificate.\nfunc FinalizeOrder(w http.ResponseWriter, r *http.Request) {\n\tctx := r.Context()\n\tdb := acme.MustDatabaseFromContext(ctx)\n\tlinker := acme.MustLinkerFromContext(ctx)\n\n\tacc, err := accountFromContext(ctx)\n\tif err != nil {\n\t\trender.Error(w, r, err)\n\t\treturn\n\t}\n\tprov, err := provisionerFromContext(ctx)\n\tif err != nil {\n\t\trender.Error(w, r, err)\n\t\treturn\n\t}\n\tpayload, err := payloadFromContext(ctx)\n\tif err != nil {\n\t\trender.Error(w, r, err)\n\t\treturn\n\t}\n\tvar fr FinalizeRequest\n\tif err := json.Unmarshal(payload.value, &fr); err != nil {\n\t\trender.Error(w, r, acme.WrapError(acme.ErrorMalformedType, err,\n\t\t\t\"failed to unmarshal finalize-order request payload\"))\n\t\treturn\n\t}\n\tif err := fr.Validate(); err != nil {\n\t\trender.Error(w, r, err)\n\t\treturn\n\t}\n\n\to, err := db.GetOrder(ctx, chi.URLParam(r, \"ordID\"))\n\tif err != nil {\n\t\trender.Error(w, r, acme.WrapErrorISE(err, \"error retrieving order\"))\n\t\treturn\n\t}\n\tif acc.ID != o.AccountID {\n\t\trender.Error(w, r, acme.NewError(acme.ErrorUnauthorizedType,\n\t\t\t\"account '%s' does not own order '%s'\", acc.ID, o.ID))\n\t\treturn\n\t}\n\tif prov.GetID() != o.ProvisionerID {\n\t\trender.Error(w, r, acme.NewError(acme.ErrorUnauthorizedType,\n\t\t\t\"provisioner '%s' does not own order '%s'\", prov.GetID(), o.ID))\n\t\treturn\n\t}\n\n\tca := mustAuthority(ctx)\n\tif err = o.Finalize(ctx, db, fr.csr, ca, prov); err != nil {\n\t\trender.Error(w, r, acme.WrapErrorISE(err, \"error finalizing order\"))\n\t\treturn\n\t}\n\n\tlinker.LinkOrder(ctx, o)\n\n\tw.Header().Set(\"Location\", linker.GetLink(ctx, acme.OrderLinkType, o.ID))\n\trender.JSON(w, r, o)\n}\n\n// challengeTypes determines the types of challenges that should be used\n// for the ACME authorization request.\nfunc challengeTypes(az *acme.Authorization) []acme.ChallengeType {\n\tvar chTypes []acme.ChallengeType\n\n\tswitch az.Identifier.Type {\n\tcase acme.IP:\n\t\tchTypes = []acme.ChallengeType{acme.HTTP01, acme.TLSALPN01}\n\tcase acme.DNS:\n\t\tchTypes = []acme.ChallengeType{acme.DNS01}\n\t\t// HTTP and TLS challenges can only be used for identifiers without wildcards.\n\t\tif !az.Wildcard {\n\t\t\tchTypes = append(chTypes, []acme.ChallengeType{acme.HTTP01, acme.TLSALPN01}...)\n\t\t}\n\tcase acme.PermanentIdentifier:\n\t\tchTypes = []acme.ChallengeType{acme.DEVICEATTEST01}\n\tcase acme.WireUser:\n\t\tchTypes = []acme.ChallengeType{acme.WIREOIDC01}\n\tcase acme.WireDevice:\n\t\tchTypes = []acme.ChallengeType{acme.WIREDPOP01}\n\tdefault:\n\t\tchTypes = []acme.ChallengeType{}\n\t}\n\n\treturn chTypes\n}\n"
  },
  {
    "path": "acme/api/order_test.go",
    "content": "package api\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/x509\"\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"reflect\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-chi/chi/v5\"\n\t\"github.com/pkg/errors\"\n\n\t\"go.step.sm/crypto/pemutil\"\n\n\t\"github.com/smallstep/assert\"\n\t\"github.com/smallstep/certificates/acme\"\n\t\"github.com/smallstep/certificates/authority/policy\"\n\t\"github.com/smallstep/certificates/authority/provisioner\"\n\t\"github.com/smallstep/certificates/authority/provisioner/wire\"\n\n\tsassert \"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestNewOrderRequest_Validate(t *testing.T) {\n\ttype test struct {\n\t\tnor      *NewOrderRequest\n\t\tnbf, naf time.Time\n\t\terr      *acme.Error\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/no-identifiers\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tnor: &NewOrderRequest{},\n\t\t\t\terr: acme.NewError(acme.ErrorMalformedType, \"identifiers list cannot be empty\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/bad-identifier\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tnor: &NewOrderRequest{\n\t\t\t\t\tIdentifiers: []acme.Identifier{\n\t\t\t\t\t\t{Type: \"dns\", Value: \"example.com\"},\n\t\t\t\t\t\t{Type: \"foo\", Value: \"bar.com\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: acme.NewError(acme.ErrorMalformedType, \"identifier type unsupported: foo\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/bad-identifier/bad-dns\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tnor: &NewOrderRequest{\n\t\t\t\t\tIdentifiers: []acme.Identifier{\n\t\t\t\t\t\t{Type: \"dns\", Value: \"xn--bücher.example.com\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: acme.NewError(acme.ErrorMalformedType, \"invalid DNS name: xn--bücher.example.com\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/bad-identifier/dns-port\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tnor: &NewOrderRequest{\n\t\t\t\t\tIdentifiers: []acme.Identifier{\n\t\t\t\t\t\t{Type: \"dns\", Value: \"example.com:8080\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: acme.NewError(acme.ErrorMalformedType, \"invalid DNS name: example.com:8080\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/bad-identifier/dns-wildcard-port\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tnor: &NewOrderRequest{\n\t\t\t\t\tIdentifiers: []acme.Identifier{\n\t\t\t\t\t\t{Type: \"dns\", Value: \"*.example.com:8080\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: acme.NewError(acme.ErrorMalformedType, \"invalid DNS name: *.example.com:8080\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/bad-identifier/ip\": func(t *testing.T) test {\n\t\t\tnbf := time.Now().UTC().Add(time.Minute)\n\t\t\tnaf := time.Now().UTC().Add(5 * time.Minute)\n\t\t\treturn test{\n\t\t\t\tnor: &NewOrderRequest{\n\t\t\t\t\tIdentifiers: []acme.Identifier{\n\t\t\t\t\t\t{Type: \"ip\", Value: \"192.168.42.1000\"},\n\t\t\t\t\t},\n\t\t\t\t\tNotAfter:  naf,\n\t\t\t\t\tNotBefore: nbf,\n\t\t\t\t},\n\t\t\t\tnbf: nbf,\n\t\t\t\tnaf: naf,\n\t\t\t\terr: acme.NewError(acme.ErrorMalformedType, \"invalid IP address: %s\", \"192.168.42.1000\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/bad-identifier/wireapp-invalid-uri\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tnor: &NewOrderRequest{\n\t\t\t\t\tIdentifiers: []acme.Identifier{\n\t\t\t\t\t\t{Type: \"wireapp-user\", Value: `{\"name\": \"Alice Smith\", \"domain\": \"wire.com\", \"handle\": \"wireapp://%40alice_wire@wire.com\"}`},\n\t\t\t\t\t\t{Type: \"wireapp-device\", Value: `{\"name\": \"Smith, Alice M (QA)\", \"domain\": \"example.com\", \"client-id\": \"example.com\", \"handle\": \"wireapp://%40alice.smith.qa@example.com\"}`},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: acme.NewError(acme.ErrorMalformedType, `failed validating Wire identifiers: invalid Wire client ID \"example.com\": invalid Wire client ID scheme \"\"; expected \"wireapp\"`),\n\t\t\t}\n\t\t},\n\t\t\"fail/bad-identifier/wireapp-wrong-scheme\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tnor: &NewOrderRequest{\n\t\t\t\t\tIdentifiers: []acme.Identifier{\n\t\t\t\t\t\t{Type: \"wireapp-user\", Value: `{\"name\": \"Alice Smith\", \"domain\": \"wire.com\", \"handle\": \"wireapp://%40alice_wire@wire.com\"}`},\n\t\t\t\t\t\t{Type: \"wireapp-device\", Value: `{\"name\": \"Smith, Alice M (QA)\", \"domain\": \"example.com\", \"client-id\": \"nowireapp://example.com\", \"handle\": \"wireapp://%40alice.smith.qa@example.com\"}`},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: acme.NewError(acme.ErrorMalformedType, `failed validating Wire identifiers: invalid Wire client ID \"nowireapp://example.com\": invalid Wire client ID scheme \"nowireapp\"; expected \"wireapp\"`),\n\t\t\t}\n\t\t},\n\t\t\"fail/bad-identifier/wireapp-invalid-user-parts\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tnor: &NewOrderRequest{\n\t\t\t\t\tIdentifiers: []acme.Identifier{\n\t\t\t\t\t\t{Type: \"wireapp-user\", Value: `{\"name\": \"Alice Smith\", \"domain\": \"wire.com\", \"handle\": \"wireapp://%40alice_wire@wire.com\"}`},\n\t\t\t\t\t\t{Type: \"wireapp-device\", Value: `{\"name\": \"Smith, Alice M (QA)\", \"domain\": \"example.com\", \"client-id\": \"wireapp://user-device@example.com\", \"handle\": \"wireapp://%40alice.smith.qa@example.com\"}`},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: acme.NewError(acme.ErrorMalformedType, `failed validating Wire identifiers: invalid Wire client ID \"wireapp://user-device@example.com\": invalid Wire client ID username \"user-device\"`),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tnbf := time.Now().UTC().Add(time.Minute)\n\t\t\tnaf := time.Now().UTC().Add(5 * time.Minute)\n\t\t\treturn test{\n\t\t\t\tnor: &NewOrderRequest{\n\t\t\t\t\tIdentifiers: []acme.Identifier{\n\t\t\t\t\t\t{Type: \"dns\", Value: \"example.com\"},\n\t\t\t\t\t\t{Type: \"dns\", Value: \"*.bar.com\"},\n\t\t\t\t\t},\n\t\t\t\t\tNotAfter:  naf,\n\t\t\t\t\tNotBefore: nbf,\n\t\t\t\t},\n\t\t\t\tnbf: nbf,\n\t\t\t\tnaf: naf,\n\t\t\t}\n\t\t},\n\t\t\"ok/ipv4\": func(t *testing.T) test {\n\t\t\tnbf := time.Now().UTC().Add(time.Minute)\n\t\t\tnaf := time.Now().UTC().Add(5 * time.Minute)\n\t\t\treturn test{\n\t\t\t\tnor: &NewOrderRequest{\n\t\t\t\t\tIdentifiers: []acme.Identifier{\n\t\t\t\t\t\t{Type: \"ip\", Value: \"192.168.42.42\"},\n\t\t\t\t\t},\n\t\t\t\t\tNotAfter:  naf,\n\t\t\t\t\tNotBefore: nbf,\n\t\t\t\t},\n\t\t\t\tnbf: nbf,\n\t\t\t\tnaf: naf,\n\t\t\t}\n\t\t},\n\t\t\"ok/ipv6\": func(t *testing.T) test {\n\t\t\tnbf := time.Now().UTC().Add(time.Minute)\n\t\t\tnaf := time.Now().UTC().Add(5 * time.Minute)\n\t\t\treturn test{\n\t\t\t\tnor: &NewOrderRequest{\n\t\t\t\t\tIdentifiers: []acme.Identifier{\n\t\t\t\t\t\t{Type: \"ip\", Value: \"2001:db8::1\"},\n\t\t\t\t\t},\n\t\t\t\t\tNotAfter:  naf,\n\t\t\t\t\tNotBefore: nbf,\n\t\t\t\t},\n\t\t\t\tnbf: nbf,\n\t\t\t\tnaf: naf,\n\t\t\t}\n\t\t},\n\t\t\"ok/mixed-dns-and-ipv4\": func(t *testing.T) test {\n\t\t\tnbf := time.Now().UTC().Add(time.Minute)\n\t\t\tnaf := time.Now().UTC().Add(5 * time.Minute)\n\t\t\treturn test{\n\t\t\t\tnor: &NewOrderRequest{\n\t\t\t\t\tIdentifiers: []acme.Identifier{\n\t\t\t\t\t\t{Type: \"dns\", Value: \"example.com\"},\n\t\t\t\t\t\t{Type: \"ip\", Value: \"192.168.42.42\"},\n\t\t\t\t\t},\n\t\t\t\t\tNotAfter:  naf,\n\t\t\t\t\tNotBefore: nbf,\n\t\t\t\t},\n\t\t\t\tnbf: nbf,\n\t\t\t\tnaf: naf,\n\t\t\t}\n\t\t},\n\t\t\"ok/mixed-ipv4-and-ipv6\": func(t *testing.T) test {\n\t\t\tnbf := time.Now().UTC().Add(time.Minute)\n\t\t\tnaf := time.Now().UTC().Add(5 * time.Minute)\n\t\t\treturn test{\n\t\t\t\tnor: &NewOrderRequest{\n\t\t\t\t\tIdentifiers: []acme.Identifier{\n\t\t\t\t\t\t{Type: \"ip\", Value: \"192.168.42.42\"},\n\t\t\t\t\t\t{Type: \"ip\", Value: \"2001:db8::1\"},\n\t\t\t\t\t},\n\t\t\t\t\tNotAfter:  naf,\n\t\t\t\t\tNotBefore: nbf,\n\t\t\t\t},\n\t\t\t\tnbf: nbf,\n\t\t\t\tnaf: naf,\n\t\t\t}\n\t\t},\n\t\t\"ok/wireapp\": func(t *testing.T) test {\n\t\t\tnbf := time.Now().UTC().Add(time.Minute)\n\t\t\tnaf := time.Now().UTC().Add(5 * time.Minute)\n\t\t\treturn test{\n\t\t\t\tnor: &NewOrderRequest{\n\t\t\t\t\tIdentifiers: []acme.Identifier{\n\t\t\t\t\t\t{Type: \"wireapp-user\", Value: `{\"name\": \"Smith, Alice M (QA)\", \"domain\": \"example.com\", \"handle\": \"wireapp://%40alice.smith.qa@example.com\"}`},\n\t\t\t\t\t\t{Type: \"wireapp-device\", Value: `{\"name\": \"Smith, Alice M (QA)\", \"domain\": \"example.com\", \"client-id\": \"wireapp://lJGYPz0ZRq2kvc_XpdaDlA!ed416ce8ecdd9fad@example.com\", \"handle\": \"wireapp://%40alice.smith.qa@example.com\"}`},\n\t\t\t\t\t},\n\t\t\t\t\tNotAfter:  naf,\n\t\t\t\t\tNotBefore: nbf,\n\t\t\t\t},\n\t\t\t\tnbf: nbf,\n\t\t\t\tnaf: naf,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\ttc := run(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\terr := tc.nor.Validate()\n\t\t\tif tc.err != nil {\n\t\t\t\tassert.Error(t, err)\n\t\t\t\tvar ae *acme.Error\n\t\t\t\tif assert.True(t, errors.As(err, &ae)) {\n\t\t\t\t\tassert.HasPrefix(t, ae.Error(), tc.err.Error())\n\t\t\t\t\tassert.Equals(t, ae.StatusCode(), tc.err.StatusCode())\n\t\t\t\t\tassert.Equals(t, ae.Type, tc.err.Type)\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.NoError(t, err)\n\t\t\tif tc.nbf.IsZero() {\n\t\t\t\tassert.True(t, tc.nor.NotBefore.Before(time.Now().Add(time.Minute)))\n\t\t\t\tassert.True(t, tc.nor.NotBefore.After(time.Now().Add(-time.Minute)))\n\t\t\t} else {\n\t\t\t\tassert.Equals(t, tc.nor.NotBefore, tc.nbf)\n\t\t\t}\n\t\t\tif tc.naf.IsZero() {\n\t\t\t\tassert.True(t, tc.nor.NotAfter.Before(time.Now().Add(24*time.Hour)))\n\t\t\t\tassert.True(t, tc.nor.NotAfter.After(time.Now().Add(24*time.Hour-time.Minute)))\n\t\t\t} else {\n\t\t\t\tassert.Equals(t, tc.nor.NotAfter, tc.naf)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestFinalizeRequestValidate(t *testing.T) {\n\t_csr, err := pemutil.Read(\"../../authority/testdata/certs/foo.csr\")\n\tassert.FatalError(t, err)\n\tcsr, ok := _csr.(*x509.CertificateRequest)\n\tassert.Fatal(t, ok)\n\ttype test struct {\n\t\tfr  *FinalizeRequest\n\t\terr *acme.Error\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/parse-csr-error\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tfr:  &FinalizeRequest{},\n\t\t\t\terr: acme.NewError(acme.ErrorMalformedType, \"unable to parse csr: asn1: syntax error: sequence truncated\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/invalid-csr-signature\": func(t *testing.T) test {\n\t\t\tb, err := pemutil.Read(\"../../authority/testdata/certs/badsig.csr\")\n\t\t\tassert.FatalError(t, err)\n\t\t\tc, ok := b.(*x509.CertificateRequest)\n\t\t\tassert.Fatal(t, ok)\n\t\t\treturn test{\n\t\t\t\tfr: &FinalizeRequest{\n\t\t\t\t\tCSR: base64.RawURLEncoding.EncodeToString(c.Raw),\n\t\t\t\t},\n\t\t\t\terr: acme.NewError(acme.ErrorMalformedType, \"csr failed signature check: x509: ECDSA verification failure\"),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tfr: &FinalizeRequest{\n\t\t\t\t\tCSR: base64.RawURLEncoding.EncodeToString(csr.Raw),\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"ok/padding\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tfr: &FinalizeRequest{\n\t\t\t\t\tCSR: base64.RawURLEncoding.EncodeToString(csr.Raw) + \"==\", // add intentional padding\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\ttc := run(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tif err := tc.fr.Validate(); err != nil {\n\t\t\t\tif assert.NotNil(t, err) {\n\t\t\t\t\tvar ae *acme.Error\n\t\t\t\t\tif assert.True(t, errors.As(err, &ae)) {\n\t\t\t\t\t\tassert.HasPrefix(t, ae.Error(), tc.err.Error())\n\t\t\t\t\t\tassert.Equals(t, ae.StatusCode(), tc.err.StatusCode())\n\t\t\t\t\t\tassert.Equals(t, ae.Type, tc.err.Type)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif assert.Nil(t, tc.err) {\n\t\t\t\t\tassert.Equals(t, tc.fr.csr.Raw, csr.Raw)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestHandler_GetOrder(t *testing.T) {\n\tprov := newProv()\n\tescProvName := url.PathEscape(prov.GetName())\n\tbaseURL := &url.URL{Scheme: \"https\", Host: \"test.ca.smallstep.com\"}\n\n\tnow := clock.Now()\n\tnbf := now\n\tnaf := now.Add(24 * time.Hour)\n\texpiry := now.Add(-time.Hour)\n\to := acme.Order{\n\t\tID:        \"orderID\",\n\t\tNotBefore: nbf,\n\t\tNotAfter:  naf,\n\t\tIdentifiers: []acme.Identifier{\n\t\t\t{\n\t\t\t\tType:  \"dns\",\n\t\t\t\tValue: \"example.com\",\n\t\t\t},\n\t\t\t{\n\t\t\t\tType:  \"dns\",\n\t\t\t\tValue: \"*.smallstep.com\",\n\t\t\t},\n\t\t},\n\t\tExpiresAt: expiry,\n\t\tStatus:    acme.StatusInvalid,\n\t\tError:     acme.NewError(acme.ErrorMalformedType, \"order has expired\"),\n\t\tAuthorizationURLs: []string{\n\t\t\tfmt.Sprintf(\"%s/acme/%s/authz/foo\", baseURL.String(), escProvName),\n\t\t\tfmt.Sprintf(\"%s/acme/%s/authz/bar\", baseURL.String(), escProvName),\n\t\t\tfmt.Sprintf(\"%s/acme/%s/authz/baz\", baseURL.String(), escProvName),\n\t\t},\n\t\tFinalizeURL: fmt.Sprintf(\"%s/acme/%s/order/orderID/finalize\", baseURL.String(), escProvName),\n\t}\n\n\t// Request with chi context\n\tchiCtx := chi.NewRouteContext()\n\tchiCtx.URLParams.Add(\"ordID\", o.ID)\n\tu := fmt.Sprintf(\"%s/acme/%s/order/%s\",\n\t\tbaseURL.String(), escProvName, o.ID)\n\n\ttype test struct {\n\t\tdb         acme.DB\n\t\tctx        context.Context\n\t\tstatusCode int\n\t\terr        *acme.Error\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/no-account\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tdb:         &acme.MockDB{},\n\t\t\t\tctx:        acme.NewProvisionerContext(context.Background(), prov),\n\t\t\t\tstatusCode: 400,\n\t\t\t\terr:        acme.NewError(acme.ErrorAccountDoesNotExistType, \"account does not exist\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/nil-account\": func(t *testing.T) test {\n\t\t\tctx := acme.NewProvisionerContext(context.Background(), prov)\n\t\t\tctx = context.WithValue(ctx, accContextKey, nil)\n\t\t\treturn test{\n\t\t\t\tdb:         &acme.MockDB{},\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 400,\n\t\t\t\terr:        acme.NewError(acme.ErrorAccountDoesNotExistType, \"account does not exist\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/no-provisioner\": func(t *testing.T) test {\n\t\t\tacc := &acme.Account{ID: \"accountID\"}\n\t\t\tctx := context.WithValue(context.Background(), accContextKey, acc)\n\t\t\treturn test{\n\t\t\t\tdb:         &acme.MockDB{},\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 500,\n\t\t\t\terr:        acme.NewErrorISE(\"provisioner does not exist\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/nil-provisioner\": func(t *testing.T) test {\n\t\t\tacc := &acme.Account{ID: \"accountID\"}\n\t\t\tctx := acme.NewProvisionerContext(context.Background(), nil)\n\t\t\tctx = context.WithValue(ctx, accContextKey, acc)\n\t\t\treturn test{\n\t\t\t\tdb:         &acme.MockDB{},\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 500,\n\t\t\t\terr:        acme.NewErrorISE(\"provisioner does not exist\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/db.GetOrder-error\": func(t *testing.T) test {\n\t\t\tacc := &acme.Account{ID: \"accountID\"}\n\t\t\tctx := acme.NewProvisionerContext(context.Background(), prov)\n\t\t\tctx = context.WithValue(ctx, accContextKey, acc)\n\t\t\tctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx)\n\t\t\treturn test{\n\t\t\t\tdb: &acme.MockDB{\n\t\t\t\t\tMockError: acme.NewErrorISE(\"force\"),\n\t\t\t\t},\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 500,\n\t\t\t\terr:        acme.NewErrorISE(\"force\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/account-id-mismatch\": func(t *testing.T) test {\n\t\t\tacc := &acme.Account{ID: \"accountID\"}\n\t\t\tctx := acme.NewProvisionerContext(context.Background(), prov)\n\t\t\tctx = context.WithValue(ctx, accContextKey, acc)\n\t\t\tctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx)\n\t\t\treturn test{\n\t\t\t\tdb: &acme.MockDB{\n\t\t\t\t\tMockGetOrder: func(ctx context.Context, id string) (*acme.Order, error) {\n\t\t\t\t\t\treturn &acme.Order{AccountID: \"foo\"}, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 401,\n\t\t\t\terr:        acme.NewError(acme.ErrorUnauthorizedType, \"account id mismatch\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/provisioner-id-mismatch\": func(t *testing.T) test {\n\t\t\tacc := &acme.Account{ID: \"accountID\"}\n\t\t\tctx := acme.NewProvisionerContext(context.Background(), prov)\n\t\t\tctx = context.WithValue(ctx, accContextKey, acc)\n\t\t\tctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx)\n\t\t\treturn test{\n\t\t\t\tdb: &acme.MockDB{\n\t\t\t\t\tMockGetOrder: func(ctx context.Context, id string) (*acme.Order, error) {\n\t\t\t\t\t\treturn &acme.Order{AccountID: \"accountID\", ProvisionerID: \"bar\"}, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 401,\n\t\t\t\terr:        acme.NewError(acme.ErrorUnauthorizedType, \"provisioner id mismatch\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/order-update-error\": func(t *testing.T) test {\n\t\t\tacc := &acme.Account{ID: \"accountID\"}\n\t\t\tctx := acme.NewProvisionerContext(context.Background(), prov)\n\t\t\tctx = context.WithValue(ctx, accContextKey, acc)\n\t\t\tctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx)\n\t\t\treturn test{\n\t\t\t\tdb: &acme.MockDB{\n\t\t\t\t\tMockGetOrder: func(ctx context.Context, id string) (*acme.Order, error) {\n\t\t\t\t\t\treturn &acme.Order{\n\t\t\t\t\t\t\tAccountID:     \"accountID\",\n\t\t\t\t\t\t\tProvisionerID: fmt.Sprintf(\"acme/%s\", prov.GetName()),\n\t\t\t\t\t\t\tExpiresAt:     clock.Now().Add(-time.Hour),\n\t\t\t\t\t\t\tStatus:        acme.StatusReady,\n\t\t\t\t\t\t}, nil\n\t\t\t\t\t},\n\t\t\t\t\tMockUpdateOrder: func(ctx context.Context, o *acme.Order) error {\n\t\t\t\t\t\treturn acme.NewErrorISE(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 500,\n\t\t\t\terr:        acme.NewErrorISE(\"force\"),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tacc := &acme.Account{ID: \"accountID\"}\n\t\t\tctx := acme.NewProvisionerContext(context.Background(), prov)\n\t\t\tctx = context.WithValue(ctx, accContextKey, acc)\n\t\t\tctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx)\n\t\t\treturn test{\n\t\t\t\tdb: &acme.MockDB{\n\t\t\t\t\tMockGetOrder: func(ctx context.Context, id string) (*acme.Order, error) {\n\t\t\t\t\t\treturn &acme.Order{\n\t\t\t\t\t\t\tID:               \"orderID\",\n\t\t\t\t\t\t\tAccountID:        \"accountID\",\n\t\t\t\t\t\t\tProvisionerID:    fmt.Sprintf(\"acme/%s\", prov.GetName()),\n\t\t\t\t\t\t\tExpiresAt:        expiry,\n\t\t\t\t\t\t\tStatus:           acme.StatusReady,\n\t\t\t\t\t\t\tAuthorizationIDs: []string{\"foo\", \"bar\", \"baz\"},\n\t\t\t\t\t\t\tNotBefore:        nbf,\n\t\t\t\t\t\t\tNotAfter:         naf,\n\t\t\t\t\t\t\tIdentifiers: []acme.Identifier{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tType:  \"dns\",\n\t\t\t\t\t\t\t\t\tValue: \"example.com\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tType:  \"dns\",\n\t\t\t\t\t\t\t\t\tValue: \"*.smallstep.com\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t}, nil\n\t\t\t\t\t},\n\t\t\t\t\tMockUpdateOrder: func(ctx context.Context, o *acme.Order) error {\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 200,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\ttc := run(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tctx := newBaseContext(tc.ctx, tc.db, acme.NewLinker(\"test.ca.smallstep.com\", \"acme\"))\n\t\t\treq := httptest.NewRequest(\"GET\", u, http.NoBody)\n\t\t\treq = req.WithContext(ctx)\n\t\t\tw := httptest.NewRecorder()\n\t\t\tGetOrder(w, req)\n\t\t\tres := w.Result()\n\n\t\t\tassert.Equals(t, res.StatusCode, tc.statusCode)\n\n\t\t\tbody, err := io.ReadAll(res.Body)\n\t\t\tres.Body.Close()\n\t\t\tassert.FatalError(t, err)\n\n\t\t\tif res.StatusCode >= 400 && assert.NotNil(t, tc.err) {\n\t\t\t\tvar ae acme.Error\n\t\t\t\tassert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &ae))\n\n\t\t\t\tassert.Equals(t, ae.Type, tc.err.Type)\n\t\t\t\tassert.Equals(t, ae.Detail, tc.err.Detail)\n\t\t\t\tassert.Equals(t, ae.Subproblems, tc.err.Subproblems)\n\t\t\t\tassert.Equals(t, res.Header[\"Content-Type\"], []string{\"application/problem+json\"})\n\t\t\t} else {\n\t\t\t\texpB, err := json.Marshal(o)\n\t\t\t\tassert.FatalError(t, err)\n\n\t\t\t\tassert.Equals(t, bytes.TrimSpace(body), expB)\n\t\t\t\tassert.Equals(t, res.Header[\"Location\"], []string{u})\n\t\t\t\tassert.Equals(t, res.Header[\"Content-Type\"], []string{\"application/json\"})\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestHandler_newAuthorization(t *testing.T) {\n\tdefaultProvisioner := newProv()\n\tfakeKey := `-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEA5c+4NKZSNQcR1T8qN6SjwgdPZQ0Ge12Ylx/YeGAJ35k=\n-----END PUBLIC KEY-----`\n\twireProvisioner := newWireProvisionerWithOptions(t, &provisioner.Options{\n\t\tWire: &wire.Options{\n\t\t\tOIDC: &wire.OIDCOptions{\n\t\t\t\tProvider: &wire.Provider{\n\t\t\t\t\tIssuerURL:  \"https://issuer.example.com\",\n\t\t\t\t\tAlgorithms: []string{\"ES256\"},\n\t\t\t\t},\n\t\t\t\tConfig: &wire.Config{\n\t\t\t\t\tClientID:            \"test\",\n\t\t\t\t\tSignatureAlgorithms: []string{\"ES256\"},\n\t\t\t\t\tNow:                 time.Now,\n\t\t\t\t},\n\t\t\t\tTransformTemplate: \"\",\n\t\t\t},\n\t\t\tDPOP: &wire.DPOPOptions{\n\t\t\t\tSigningKey: []byte(fakeKey),\n\t\t\t},\n\t\t},\n\t})\n\twireProvisionerFailOptions := &provisioner.ACME{\n\t\tType:    \"ACME\",\n\t\tName:    \"test@acme-<test>provisioner.com\",\n\t\tOptions: &provisioner.Options{},\n\t\tChallenges: []provisioner.ACMEChallenge{\n\t\t\tprovisioner.WIREOIDC_01,\n\t\t\tprovisioner.WIREDPOP_01,\n\t\t},\n\t}\n\ttype test struct {\n\t\taz   *acme.Authorization\n\t\tprov acme.Provisioner\n\t\tdb   acme.DB\n\t\terr  *acme.Error\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/error-db.CreateChallenge\": func(t *testing.T) test {\n\t\t\taz := &acme.Authorization{\n\t\t\t\tAccountID: \"accID\",\n\t\t\t\tIdentifier: acme.Identifier{\n\t\t\t\t\tType:  \"dns\",\n\t\t\t\t\tValue: \"zap.internal\",\n\t\t\t\t},\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tprov: defaultProvisioner,\n\t\t\t\tdb: &acme.MockDB{\n\t\t\t\t\tMockCreateChallenge: func(ctx context.Context, ch *acme.Challenge) error {\n\t\t\t\t\t\tassert.Equals(t, ch.AccountID, az.AccountID)\n\t\t\t\t\t\tassert.Equals(t, ch.Type, acme.DNS01)\n\t\t\t\t\t\tassert.Equals(t, ch.Token, az.Token)\n\t\t\t\t\t\tassert.Equals(t, ch.Status, acme.StatusPending)\n\t\t\t\t\t\tassert.Equals(t, ch.Value, az.Identifier.Value)\n\t\t\t\t\t\treturn errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\taz: az,\n\t\t\t\terr: &acme.Error{\n\t\t\t\t\tType:   \"urn:ietf:params:acme:error:serverInternal\",\n\t\t\t\t\tErr:    errors.New(\"error creating challenge: force\"),\n\t\t\t\t\tDetail: \"The server experienced an internal error\",\n\t\t\t\t\tStatus: 500,\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"fail/error-db.CreateAuthorization\": func(t *testing.T) test {\n\t\t\taz := &acme.Authorization{\n\t\t\t\tAccountID: \"accID\",\n\t\t\t\tIdentifier: acme.Identifier{\n\t\t\t\t\tType:  \"dns\",\n\t\t\t\t\tValue: \"zap.internal\",\n\t\t\t\t},\n\t\t\t\tStatus:    acme.StatusPending,\n\t\t\t\tExpiresAt: clock.Now(),\n\t\t\t}\n\t\t\tcount := 0\n\t\t\tvar ch1, ch2, ch3 **acme.Challenge\n\t\t\treturn test{\n\t\t\t\tprov: defaultProvisioner,\n\t\t\t\tdb: &acme.MockDB{\n\t\t\t\t\tMockCreateChallenge: func(ctx context.Context, ch *acme.Challenge) error {\n\t\t\t\t\t\tswitch count {\n\t\t\t\t\t\tcase 0:\n\t\t\t\t\t\t\tch.ID = \"dns\"\n\t\t\t\t\t\t\tassert.Equals(t, ch.Type, acme.DNS01)\n\t\t\t\t\t\t\tch1 = &ch\n\t\t\t\t\t\tcase 1:\n\t\t\t\t\t\t\tch.ID = \"http\"\n\t\t\t\t\t\t\tassert.Equals(t, ch.Type, acme.HTTP01)\n\t\t\t\t\t\t\tch2 = &ch\n\t\t\t\t\t\tcase 2:\n\t\t\t\t\t\t\tch.ID = \"tls\"\n\t\t\t\t\t\t\tassert.Equals(t, ch.Type, acme.TLSALPN01)\n\t\t\t\t\t\t\tch3 = &ch\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\tassert.FatalError(t, errors.New(\"test logic error\"))\n\t\t\t\t\t\t\treturn errors.New(\"force\")\n\t\t\t\t\t\t}\n\t\t\t\t\t\tcount++\n\t\t\t\t\t\tassert.Equals(t, ch.AccountID, az.AccountID)\n\t\t\t\t\t\tassert.Equals(t, ch.Token, az.Token)\n\t\t\t\t\t\tassert.Equals(t, ch.Status, acme.StatusPending)\n\t\t\t\t\t\tassert.Equals(t, ch.Value, az.Identifier.Value)\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t\tMockCreateAuthorization: func(ctx context.Context, _az *acme.Authorization) error {\n\t\t\t\t\t\tassert.Equals(t, _az.AccountID, az.AccountID)\n\t\t\t\t\t\tassert.Equals(t, _az.Token, az.Token)\n\t\t\t\t\t\tassert.Equals(t, _az.Status, acme.StatusPending)\n\t\t\t\t\t\tassert.Equals(t, _az.Identifier, az.Identifier)\n\t\t\t\t\t\tassert.Equals(t, _az.ExpiresAt, az.ExpiresAt)\n\t\t\t\t\t\tassert.Equals(t, _az.Challenges, []*acme.Challenge{*ch1, *ch2, *ch3})\n\t\t\t\t\t\tassert.Equals(t, _az.Wildcard, false)\n\t\t\t\t\t\treturn errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\taz: az,\n\t\t\t\terr: &acme.Error{\n\t\t\t\t\tType:   \"urn:ietf:params:acme:error:serverInternal\",\n\t\t\t\t\tErr:    errors.New(\"error creating authorization: force\"),\n\t\t\t\t\tDetail: \"The server experienced an internal error\",\n\t\t\t\t\tStatus: 500,\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"fail/wireapp-user-options\": func(t *testing.T) test {\n\t\t\taz := &acme.Authorization{\n\t\t\t\tAccountID: \"accID\",\n\t\t\t\tIdentifier: acme.Identifier{\n\t\t\t\t\tType:  \"wireapp-user\",\n\t\t\t\t\tValue: \"wireapp://%40alice.smith.qa@example.com\",\n\t\t\t\t},\n\t\t\t\tStatus:    acme.StatusPending,\n\t\t\t\tExpiresAt: clock.Now(),\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tprov: wireProvisionerFailOptions,\n\t\t\t\tdb:   &acme.MockDB{},\n\t\t\t\taz:   az,\n\t\t\t\terr: &acme.Error{\n\t\t\t\t\tType:   \"urn:ietf:params:acme:error:serverInternal\",\n\t\t\t\t\tErr:    errors.New(\"failed getting Wire options: no Wire options available\"),\n\t\t\t\t\tDetail: \"The server experienced an internal error\",\n\t\t\t\t\tStatus: 500,\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"fail/wireapp-device-parse-id\": func(t *testing.T) test {\n\t\t\taz := &acme.Authorization{\n\t\t\t\tAccountID: \"accID\",\n\t\t\t\tIdentifier: acme.Identifier{\n\t\t\t\t\tType:  \"wireapp-device\",\n\t\t\t\t\tValue: `{\"name}`,\n\t\t\t\t},\n\t\t\t\tStatus:    acme.StatusPending,\n\t\t\t\tExpiresAt: clock.Now(),\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tprov: wireProvisioner,\n\t\t\t\tdb:   &acme.MockDB{},\n\t\t\t\taz:   az,\n\t\t\t\terr: &acme.Error{\n\t\t\t\t\tType:   \"urn:ietf:params:acme:error:malformed\",\n\t\t\t\t\tErr:    errors.New(\"failed parsing WireDevice: unexpected end of JSON input\"),\n\t\t\t\t\tDetail: \"The request message was malformed\",\n\t\t\t\t\tStatus: 400,\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"fail/wireapp-device-parse-client-id\": func(t *testing.T) test {\n\t\t\taz := &acme.Authorization{\n\t\t\t\tAccountID: \"accID\",\n\t\t\t\tIdentifier: acme.Identifier{\n\t\t\t\t\tType:  \"wireapp-device\",\n\t\t\t\t\tValue: `{\"name\": \"device\", \"domain\": \"wire.com\", \"client-id\": \"CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com\", \"handle\": \"wireapp://%40alice_wire@wire.com\"}`,\n\t\t\t\t},\n\t\t\t\tStatus:    acme.StatusPending,\n\t\t\t\tExpiresAt: clock.Now(),\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tprov: wireProvisioner,\n\t\t\t\tdb:   &acme.MockDB{},\n\t\t\t\taz:   az,\n\t\t\t\terr: &acme.Error{\n\t\t\t\t\tType:   \"urn:ietf:params:acme:error:malformed\",\n\t\t\t\t\tErr:    errors.New(`failed parsing ClientID: invalid Wire client ID scheme \"\"; expected \"wireapp\"`),\n\t\t\t\t\tDetail: \"The request message was malformed\",\n\t\t\t\t\tStatus: 400,\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"fail/wireapp-device-options\": func(t *testing.T) test {\n\t\t\taz := &acme.Authorization{\n\t\t\t\tAccountID: \"accID\",\n\t\t\t\tIdentifier: acme.Identifier{\n\t\t\t\t\tType:  \"wireapp-device\",\n\t\t\t\t\tValue: `{\"name\": \"device\", \"domain\": \"wire.com\", \"client-id\": \"wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com\", \"handle\": \"wireapp://%40alice_wire@wire.com\"}`,\n\t\t\t\t},\n\t\t\t\tStatus:    acme.StatusPending,\n\t\t\t\tExpiresAt: clock.Now(),\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tprov: wireProvisionerFailOptions,\n\t\t\t\tdb:   &acme.MockDB{},\n\t\t\t\taz:   az,\n\t\t\t\terr: &acme.Error{\n\t\t\t\t\tType:   \"urn:ietf:params:acme:error:serverInternal\",\n\t\t\t\t\tErr:    errors.New(\"failed getting Wire options: no Wire options available\"),\n\t\t\t\t\tDetail: \"The server experienced an internal error\",\n\t\t\t\t\tStatus: 500,\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"ok/no-wildcard\": func(t *testing.T) test {\n\t\t\taz := &acme.Authorization{\n\t\t\t\tAccountID: \"accID\",\n\t\t\t\tIdentifier: acme.Identifier{\n\t\t\t\t\tType:  \"dns\",\n\t\t\t\t\tValue: \"zap.internal\",\n\t\t\t\t},\n\t\t\t\tStatus:    acme.StatusPending,\n\t\t\t\tExpiresAt: clock.Now(),\n\t\t\t}\n\t\t\tcount := 0\n\t\t\tvar ch1, ch2, ch3 **acme.Challenge\n\t\t\treturn test{\n\t\t\t\tprov: defaultProvisioner,\n\t\t\t\tdb: &acme.MockDB{\n\t\t\t\t\tMockCreateChallenge: func(ctx context.Context, ch *acme.Challenge) error {\n\t\t\t\t\t\tswitch count {\n\t\t\t\t\t\tcase 0:\n\t\t\t\t\t\t\tch.ID = \"dns\"\n\t\t\t\t\t\t\tassert.Equals(t, ch.Type, acme.DNS01)\n\t\t\t\t\t\t\tch1 = &ch\n\t\t\t\t\t\tcase 1:\n\t\t\t\t\t\t\tch.ID = \"http\"\n\t\t\t\t\t\t\tassert.Equals(t, ch.Type, acme.HTTP01)\n\t\t\t\t\t\t\tch2 = &ch\n\t\t\t\t\t\tcase 2:\n\t\t\t\t\t\t\tch.ID = \"tls\"\n\t\t\t\t\t\t\tassert.Equals(t, ch.Type, acme.TLSALPN01)\n\t\t\t\t\t\t\tch3 = &ch\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\tassert.FatalError(t, errors.New(\"test logic error\"))\n\t\t\t\t\t\t\treturn errors.New(\"force\")\n\t\t\t\t\t\t}\n\t\t\t\t\t\tcount++\n\t\t\t\t\t\tassert.Equals(t, ch.AccountID, az.AccountID)\n\t\t\t\t\t\tassert.Equals(t, ch.Token, az.Token)\n\t\t\t\t\t\tassert.Equals(t, ch.Status, acme.StatusPending)\n\t\t\t\t\t\tassert.Equals(t, ch.Value, az.Identifier.Value)\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t\tMockCreateAuthorization: func(ctx context.Context, _az *acme.Authorization) error {\n\t\t\t\t\t\tassert.Equals(t, _az.AccountID, az.AccountID)\n\t\t\t\t\t\tassert.Equals(t, _az.Token, az.Token)\n\t\t\t\t\t\tassert.Equals(t, _az.Status, acme.StatusPending)\n\t\t\t\t\t\tassert.Equals(t, _az.Identifier, az.Identifier)\n\t\t\t\t\t\tassert.Equals(t, _az.ExpiresAt, az.ExpiresAt)\n\t\t\t\t\t\tassert.Equals(t, _az.Challenges, []*acme.Challenge{*ch1, *ch2, *ch3})\n\t\t\t\t\t\tassert.Equals(t, _az.Wildcard, false)\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\taz: az,\n\t\t\t}\n\t\t},\n\t\t\"ok/wildcard\": func(t *testing.T) test {\n\t\t\taz := &acme.Authorization{\n\t\t\t\tAccountID: \"accID\",\n\t\t\t\tIdentifier: acme.Identifier{\n\t\t\t\t\tType:  \"dns\",\n\t\t\t\t\tValue: \"*.zap.internal\",\n\t\t\t\t},\n\t\t\t\tStatus:    acme.StatusPending,\n\t\t\t\tExpiresAt: clock.Now(),\n\t\t\t}\n\t\t\tvar ch1 **acme.Challenge\n\t\t\treturn test{\n\t\t\t\tprov: defaultProvisioner,\n\t\t\t\tdb: &acme.MockDB{\n\t\t\t\t\tMockCreateChallenge: func(ctx context.Context, ch *acme.Challenge) error {\n\t\t\t\t\t\tch.ID = \"dns\"\n\t\t\t\t\t\tassert.Equals(t, ch.Type, acme.DNS01)\n\t\t\t\t\t\tassert.Equals(t, ch.AccountID, az.AccountID)\n\t\t\t\t\t\tassert.Equals(t, ch.Token, az.Token)\n\t\t\t\t\t\tassert.Equals(t, ch.Status, acme.StatusPending)\n\t\t\t\t\t\tassert.Equals(t, ch.Value, \"zap.internal\")\n\t\t\t\t\t\tch1 = &ch\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t\tMockCreateAuthorization: func(ctx context.Context, _az *acme.Authorization) error {\n\t\t\t\t\t\tassert.Equals(t, _az.AccountID, az.AccountID)\n\t\t\t\t\t\tassert.Equals(t, _az.Token, az.Token)\n\t\t\t\t\t\tassert.Equals(t, _az.Status, acme.StatusPending)\n\t\t\t\t\t\tassert.Equals(t, _az.Identifier, acme.Identifier{\n\t\t\t\t\t\t\tType:  \"dns\",\n\t\t\t\t\t\t\tValue: \"zap.internal\",\n\t\t\t\t\t\t})\n\t\t\t\t\t\tassert.Equals(t, _az.ExpiresAt, az.ExpiresAt)\n\t\t\t\t\t\tassert.Equals(t, _az.Challenges, []*acme.Challenge{*ch1})\n\t\t\t\t\t\tassert.Equals(t, _az.Wildcard, true)\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\taz: az,\n\t\t\t}\n\t\t},\n\t\t\"ok/permanent-identifier-disabled\": func(t *testing.T) test {\n\t\t\taz := &acme.Authorization{\n\t\t\t\tAccountID: \"accID\",\n\t\t\t\tIdentifier: acme.Identifier{\n\t\t\t\t\tType:  \"permanent-identifier\",\n\t\t\t\t\tValue: \"7b53aa19-26f7-4fac-824f-7a781de0dab0\",\n\t\t\t\t},\n\t\t\t\tStatus:    acme.StatusPending,\n\t\t\t\tExpiresAt: clock.Now(),\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tprov: defaultProvisioner,\n\t\t\t\tdb: &acme.MockDB{\n\t\t\t\t\tMockCreateChallenge: func(ctx context.Context, ch *acme.Challenge) error {\n\t\t\t\t\t\tt.Errorf(\"createChallenge should not be called\")\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t\tMockCreateAuthorization: func(ctx context.Context, _az *acme.Authorization) error {\n\t\t\t\t\t\tassert.Equals(t, _az.AccountID, az.AccountID)\n\t\t\t\t\t\tassert.Equals(t, _az.Token, az.Token)\n\t\t\t\t\t\tassert.Equals(t, _az.Status, acme.StatusPending)\n\t\t\t\t\t\tassert.Equals(t, _az.Identifier, az.Identifier)\n\t\t\t\t\t\tassert.Equals(t, _az.ExpiresAt, az.ExpiresAt)\n\t\t\t\t\t\tassert.Equals(t, _az.Challenges, []*acme.Challenge{})\n\t\t\t\t\t\tassert.Equals(t, _az.Wildcard, false)\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\taz: az,\n\t\t\t}\n\t\t},\n\t\t\"ok/permanent-identifier-enabled\": func(t *testing.T) test {\n\t\t\tvar ch1 *acme.Challenge\n\t\t\taz := &acme.Authorization{\n\t\t\t\tAccountID: \"accID\",\n\t\t\t\tIdentifier: acme.Identifier{\n\t\t\t\t\tType:  \"permanent-identifier\",\n\t\t\t\t\tValue: \"7b53aa19-26f7-4fac-824f-7a781de0dab0\",\n\t\t\t\t},\n\t\t\t\tStatus:    acme.StatusPending,\n\t\t\t\tExpiresAt: clock.Now(),\n\t\t\t}\n\t\t\tdeviceAttestProv := newProv()\n\t\t\tdeviceAttestProv.(*provisioner.ACME).Challenges = []provisioner.ACMEChallenge{provisioner.DEVICE_ATTEST_01}\n\t\t\treturn test{\n\t\t\t\tprov: deviceAttestProv,\n\t\t\t\tdb: &acme.MockDB{\n\t\t\t\t\tMockCreateChallenge: func(ctx context.Context, ch *acme.Challenge) error {\n\t\t\t\t\t\tch.ID = \"997bacc2-c175-4214-a3b4-a229ada5f671\"\n\t\t\t\t\t\tassert.Equals(t, ch.Type, acme.DEVICEATTEST01)\n\t\t\t\t\t\tassert.Equals(t, ch.AccountID, az.AccountID)\n\t\t\t\t\t\tassert.Equals(t, ch.Token, az.Token)\n\t\t\t\t\t\tassert.Equals(t, ch.Status, acme.StatusPending)\n\t\t\t\t\t\tassert.Equals(t, ch.Value, \"7b53aa19-26f7-4fac-824f-7a781de0dab0\")\n\t\t\t\t\t\tch1 = ch\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t\tMockCreateAuthorization: func(ctx context.Context, _az *acme.Authorization) error {\n\t\t\t\t\t\tassert.Equals(t, _az.AccountID, az.AccountID)\n\t\t\t\t\t\tassert.Equals(t, _az.Token, az.Token)\n\t\t\t\t\t\tassert.Equals(t, _az.Status, acme.StatusPending)\n\t\t\t\t\t\tassert.Equals(t, _az.Identifier, az.Identifier)\n\t\t\t\t\t\tassert.Equals(t, _az.ExpiresAt, az.ExpiresAt)\n\t\t\t\t\t\tassert.Equals(t, _az.Challenges, []*acme.Challenge{ch1})\n\t\t\t\t\t\tassert.Equals(t, _az.Wildcard, false)\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\taz: az,\n\t\t\t}\n\t\t},\n\t\t\"ok/wireapp-user\": func(t *testing.T) test {\n\t\t\taz := &acme.Authorization{\n\t\t\t\tAccountID: \"accID\",\n\t\t\t\tIdentifier: acme.Identifier{\n\t\t\t\t\tType:  \"wireapp-user\",\n\t\t\t\t\tValue: \"wireapp://%40alice.smith.qa@example.com\",\n\t\t\t\t},\n\t\t\t\tStatus:    acme.StatusPending,\n\t\t\t\tExpiresAt: clock.Now(),\n\t\t\t}\n\t\t\tcount := 0\n\t\t\tvar ch1 **acme.Challenge\n\t\t\treturn test{\n\t\t\t\tprov: wireProvisioner,\n\t\t\t\tdb: &acme.MockDB{\n\t\t\t\t\tMockCreateChallenge: func(ctx context.Context, ch *acme.Challenge) error {\n\t\t\t\t\t\tswitch count {\n\t\t\t\t\t\tcase 0:\n\t\t\t\t\t\t\tch.ID = \"wireapp-user\"\n\t\t\t\t\t\t\tassert.Equals(t, ch.Type, acme.WIREOIDC01)\n\t\t\t\t\t\t\tch1 = &ch\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\tassert.FatalError(t, errors.New(\"test logic error\"))\n\t\t\t\t\t\t\treturn errors.New(\"force\")\n\t\t\t\t\t\t}\n\t\t\t\t\t\tcount++\n\t\t\t\t\t\tassert.Equals(t, ch.AccountID, az.AccountID)\n\t\t\t\t\t\tassert.Equals(t, ch.Token, az.Token)\n\t\t\t\t\t\tassert.Equals(t, ch.Status, acme.StatusPending)\n\t\t\t\t\t\tassert.Equals(t, ch.Value, az.Identifier.Value)\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t\tMockCreateAuthorization: func(ctx context.Context, _az *acme.Authorization) error {\n\t\t\t\t\t\tassert.Equals(t, _az.AccountID, az.AccountID)\n\t\t\t\t\t\tassert.Equals(t, _az.Token, az.Token)\n\t\t\t\t\t\tassert.Equals(t, _az.Status, acme.StatusPending)\n\t\t\t\t\t\tassert.Equals(t, _az.Identifier, az.Identifier)\n\t\t\t\t\t\tassert.Equals(t, _az.ExpiresAt, az.ExpiresAt)\n\t\t\t\t\t\t_ = ch1\n\t\t\t\t\t\t// assert.Equals(t, _az.Challenges, []*acme.Challenge{*ch1})\n\t\t\t\t\t\tassert.Equals(t, _az.Wildcard, false)\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\taz: az,\n\t\t\t}\n\t\t},\n\t\t\"ok/wireapp-device\": func(t *testing.T) test {\n\t\t\taz := &acme.Authorization{\n\t\t\t\tAccountID: \"accID\",\n\t\t\t\tIdentifier: acme.Identifier{\n\t\t\t\t\tType:  \"wireapp-device\",\n\t\t\t\t\tValue: `{\"name\": \"device\", \"domain\": \"wire.com\", \"client-id\": \"wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com\", \"handle\": \"wireapp://%40alice_wire@wire.com\"}`,\n\t\t\t\t},\n\t\t\t\tStatus:    acme.StatusPending,\n\t\t\t\tExpiresAt: clock.Now(),\n\t\t\t}\n\t\t\tcount := 0\n\t\t\tvar ch1 **acme.Challenge\n\t\t\treturn test{\n\t\t\t\tprov: wireProvisioner,\n\t\t\t\tdb: &acme.MockDB{\n\t\t\t\t\tMockCreateChallenge: func(ctx context.Context, ch *acme.Challenge) error {\n\t\t\t\t\t\tswitch count {\n\t\t\t\t\t\tcase 0:\n\t\t\t\t\t\t\tch.ID = \"wireapp-device\"\n\t\t\t\t\t\t\tassert.Equals(t, ch.Type, acme.WIREDPOP01)\n\t\t\t\t\t\t\tch1 = &ch\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\tassert.FatalError(t, errors.New(\"test logic error\"))\n\t\t\t\t\t\t\treturn errors.New(\"force\")\n\t\t\t\t\t\t}\n\t\t\t\t\t\tcount++\n\t\t\t\t\t\tassert.Equals(t, ch.AccountID, az.AccountID)\n\t\t\t\t\t\tassert.Equals(t, ch.Token, az.Token)\n\t\t\t\t\t\tassert.Equals(t, ch.Status, acme.StatusPending)\n\t\t\t\t\t\tassert.Equals(t, ch.Value, az.Identifier.Value)\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t\tMockCreateAuthorization: func(ctx context.Context, _az *acme.Authorization) error {\n\t\t\t\t\t\tassert.Equals(t, _az.AccountID, az.AccountID)\n\t\t\t\t\t\tassert.Equals(t, _az.Token, az.Token)\n\t\t\t\t\t\tassert.Equals(t, _az.Status, acme.StatusPending)\n\t\t\t\t\t\tassert.Equals(t, _az.Identifier, az.Identifier)\n\t\t\t\t\t\tassert.Equals(t, _az.ExpiresAt, az.ExpiresAt)\n\t\t\t\t\t\t_ = ch1\n\t\t\t\t\t\t// assert.Equals(t, _az.Challenges, []*acme.Challenge{*ch1})\n\t\t\t\t\t\tassert.Equals(t, _az.Wildcard, false)\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\taz: az,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ttc := run(t)\n\t\t\tctx := newBaseContext(context.Background(), tc.db)\n\t\t\tctx = acme.NewProvisionerContext(ctx, tc.prov)\n\t\t\terr := newAuthorization(ctx, tc.az)\n\t\t\tif tc.err != nil {\n\t\t\t\tsassert.Error(t, err)\n\t\t\t\tvar k *acme.Error\n\t\t\t\tif sassert.True(t, errors.As(err, &k)) {\n\t\t\t\t\tsassert.Equal(t, tc.err.Type, k.Type)\n\t\t\t\t\tsassert.Equal(t, tc.err.Detail, k.Detail)\n\t\t\t\t\tsassert.Equal(t, tc.err.Status, k.Status)\n\t\t\t\t\tsassert.EqualError(t, k.Err, tc.err.Error())\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tsassert.NoError(t, err)\n\t\t})\n\t}\n}\n\nfunc TestHandler_NewOrder(t *testing.T) {\n\t// Request with chi context\n\tprov := newProv()\n\tescProvName := url.PathEscape(prov.GetName())\n\tbaseURL := &url.URL{Scheme: \"https\", Host: \"test.ca.smallstep.com\"}\n\tu := fmt.Sprintf(\"%s/acme/%s/order/ordID\",\n\t\tbaseURL.String(), escProvName)\n\n\tfakeWireSigningKey := `-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEA5c+4NKZSNQcR1T8qN6SjwgdPZQ0Ge12Ylx/YeGAJ35k=\n-----END PUBLIC KEY-----`\n\n\ttype test struct {\n\t\tca         acme.CertificateAuthority\n\t\tdb         acme.DB\n\t\tctx        context.Context\n\t\tnor        *NewOrderRequest\n\t\tstatusCode int\n\t\tvr         func(t *testing.T, o *acme.Order)\n\t\terr        *acme.Error\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/no-account\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tdb:         &acme.MockDB{},\n\t\t\t\tctx:        acme.NewProvisionerContext(context.Background(), prov),\n\t\t\t\tstatusCode: 400,\n\t\t\t\terr:        acme.NewError(acme.ErrorAccountDoesNotExistType, \"account does not exist\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/nil-account\": func(t *testing.T) test {\n\t\t\tctx := acme.NewProvisionerContext(context.Background(), prov)\n\t\t\tctx = context.WithValue(ctx, accContextKey, nil)\n\t\t\treturn test{\n\t\t\t\tdb:         &acme.MockDB{},\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 400,\n\t\t\t\terr:        acme.NewError(acme.ErrorAccountDoesNotExistType, \"account does not exist\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/no-provisioner\": func(t *testing.T) test {\n\t\t\tacc := &acme.Account{ID: \"accountID\"}\n\t\t\tctx := context.WithValue(context.Background(), accContextKey, acc)\n\t\t\treturn test{\n\t\t\t\tdb:         &acme.MockDB{},\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 500,\n\t\t\t\terr:        acme.NewErrorISE(\"provisioner does not exist\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/nil-provisioner\": func(t *testing.T) test {\n\t\t\tacc := &acme.Account{ID: \"accountID\"}\n\t\t\tctx := acme.NewProvisionerContext(context.Background(), prov)\n\t\t\tctx = context.WithValue(ctx, accContextKey, acc)\n\t\t\treturn test{\n\t\t\t\tdb:         &acme.MockDB{},\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 500,\n\t\t\t\terr:        acme.NewErrorISE(\"provisioner does not exist\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/no-payload\": func(t *testing.T) test {\n\t\t\tacc := &acme.Account{ID: \"accountID\"}\n\t\t\tctx := context.WithValue(context.Background(), accContextKey, acc)\n\t\t\tctx = acme.NewProvisionerContext(ctx, prov)\n\t\t\treturn test{\n\t\t\t\tdb:         &acme.MockDB{},\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 500,\n\t\t\t\terr:        acme.NewErrorISE(\"payload does not exist\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/nil-payload\": func(t *testing.T) test {\n\t\t\tacc := &acme.Account{ID: \"accountID\"}\n\t\t\tctx := acme.NewProvisionerContext(context.Background(), prov)\n\t\t\tctx = context.WithValue(ctx, accContextKey, acc)\n\t\t\tctx = context.WithValue(ctx, payloadContextKey, nil)\n\t\t\treturn test{\n\t\t\t\tdb:         &acme.MockDB{},\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 500,\n\t\t\t\terr:        acme.NewErrorISE(\"payload does not exist\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/unmarshal-payload-error\": func(t *testing.T) test {\n\t\t\tacc := &acme.Account{ID: \"accID\"}\n\t\t\tctx := acme.NewProvisionerContext(context.Background(), prov)\n\t\t\tctx = context.WithValue(ctx, accContextKey, acc)\n\t\t\tctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{})\n\t\t\treturn test{\n\t\t\t\tdb:         &acme.MockDB{},\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 400,\n\t\t\t\terr:        acme.NewError(acme.ErrorMalformedType, \"failed to unmarshal new-order request payload: unexpected end of JSON input\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/malformed-payload-error\": func(t *testing.T) test {\n\t\t\tacc := &acme.Account{ID: \"accID\"}\n\t\t\tfr := &NewOrderRequest{}\n\t\t\tb, err := json.Marshal(fr)\n\t\t\tassert.FatalError(t, err)\n\t\t\tctx := acme.NewProvisionerContext(context.Background(), prov)\n\t\t\tctx = context.WithValue(ctx, accContextKey, acc)\n\t\t\tctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b})\n\t\t\treturn test{\n\t\t\t\tdb:         &acme.MockDB{},\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 400,\n\t\t\t\terr:        acme.NewError(acme.ErrorMalformedType, \"identifiers list cannot be empty\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/acmeProvisionerFromContext-error\": func(t *testing.T) test {\n\t\t\tacc := &acme.Account{ID: \"accID\"}\n\t\t\tfr := &NewOrderRequest{\n\t\t\t\tIdentifiers: []acme.Identifier{\n\t\t\t\t\t{Type: \"dns\", Value: \"zap.internal\"},\n\t\t\t\t},\n\t\t\t}\n\t\t\tb, err := json.Marshal(fr)\n\t\t\tassert.FatalError(t, err)\n\t\t\tctx := acme.NewProvisionerContext(context.Background(), &acme.MockProvisioner{})\n\t\t\tctx = context.WithValue(ctx, accContextKey, acc)\n\t\t\tctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b})\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 500,\n\t\t\t\tca:         &mockCA{},\n\t\t\t\tdb: &acme.MockDB{\n\t\t\t\t\tMockGetExternalAccountKeyByAccountID: func(ctx context.Context, provisionerID, accountID string) (*acme.ExternalAccountKey, error) {\n\t\t\t\t\t\tassert.Equals(t, prov.GetID(), provisionerID)\n\t\t\t\t\t\tassert.Equals(t, \"accID\", accountID)\n\t\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: acme.NewErrorISE(\"error retrieving external account binding key: force\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/db.GetExternalAccountKeyByAccountID-error\": func(t *testing.T) test {\n\t\t\tacmeProv := newACMEProv(t)\n\t\t\tacmeProv.RequireEAB = true\n\t\t\tacc := &acme.Account{ID: \"accID\"}\n\t\t\tfr := &NewOrderRequest{\n\t\t\t\tIdentifiers: []acme.Identifier{\n\t\t\t\t\t{Type: \"dns\", Value: \"zap.internal\"},\n\t\t\t\t},\n\t\t\t}\n\t\t\tb, err := json.Marshal(fr)\n\t\t\tassert.FatalError(t, err)\n\t\t\tctx := acme.NewProvisionerContext(context.Background(), acmeProv)\n\t\t\tctx = context.WithValue(ctx, accContextKey, acc)\n\t\t\tctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b})\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 500,\n\t\t\t\tca:         &mockCA{},\n\t\t\t\tdb: &acme.MockDB{\n\t\t\t\t\tMockGetExternalAccountKeyByAccountID: func(ctx context.Context, provisionerID, accountID string) (*acme.ExternalAccountKey, error) {\n\t\t\t\t\t\tassert.Equals(t, prov.GetID(), provisionerID)\n\t\t\t\t\t\tassert.Equals(t, \"accID\", accountID)\n\t\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: acme.NewErrorISE(\"error retrieving external account binding key: force\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/newACMEPolicyEngine-error\": func(t *testing.T) test {\n\t\t\tacmeProv := newACMEProv(t)\n\t\t\tacmeProv.RequireEAB = true\n\t\t\tacc := &acme.Account{ID: \"accID\"}\n\t\t\tfr := &NewOrderRequest{\n\t\t\t\tIdentifiers: []acme.Identifier{\n\t\t\t\t\t{Type: \"dns\", Value: \"zap.internal\"},\n\t\t\t\t},\n\t\t\t}\n\t\t\tb, err := json.Marshal(fr)\n\t\t\tassert.FatalError(t, err)\n\t\t\tctx := acme.NewProvisionerContext(context.Background(), acmeProv)\n\t\t\tctx = context.WithValue(ctx, accContextKey, acc)\n\t\t\tctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b})\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 500,\n\t\t\t\tca:         &mockCA{},\n\t\t\t\tdb: &acme.MockDB{\n\t\t\t\t\tMockGetExternalAccountKeyByAccountID: func(ctx context.Context, provisionerID, accountID string) (*acme.ExternalAccountKey, error) {\n\t\t\t\t\t\tassert.Equals(t, prov.GetID(), provisionerID)\n\t\t\t\t\t\tassert.Equals(t, \"accID\", accountID)\n\t\t\t\t\t\treturn &acme.ExternalAccountKey{\n\t\t\t\t\t\t\tPolicy: &acme.Policy{\n\t\t\t\t\t\t\t\tX509: acme.X509Policy{\n\t\t\t\t\t\t\t\t\tAllowed: acme.PolicyNames{\n\t\t\t\t\t\t\t\t\t\tDNSNames: []string{\"**.local\"},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t}, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: acme.NewErrorISE(\"error creating ACME policy engine\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/isIdentifierAllowed-error\": func(t *testing.T) test {\n\t\t\tacmeProv := newACMEProv(t)\n\t\t\tacmeProv.RequireEAB = true\n\t\t\tacc := &acme.Account{ID: \"accID\"}\n\t\t\tfr := &NewOrderRequest{\n\t\t\t\tIdentifiers: []acme.Identifier{\n\t\t\t\t\t{Type: \"dns\", Value: \"zap.internal\"},\n\t\t\t\t},\n\t\t\t}\n\t\t\tb, err := json.Marshal(fr)\n\t\t\tassert.FatalError(t, err)\n\t\t\tctx := acme.NewProvisionerContext(context.Background(), acmeProv)\n\t\t\tctx = context.WithValue(ctx, accContextKey, acc)\n\t\t\tctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b})\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 400,\n\t\t\t\tca:         &mockCA{},\n\t\t\t\tdb: &acme.MockDB{\n\t\t\t\t\tMockGetExternalAccountKeyByAccountID: func(ctx context.Context, provisionerID, accountID string) (*acme.ExternalAccountKey, error) {\n\t\t\t\t\t\tassert.Equals(t, prov.GetID(), provisionerID)\n\t\t\t\t\t\tassert.Equals(t, \"accID\", accountID)\n\t\t\t\t\t\treturn &acme.ExternalAccountKey{\n\t\t\t\t\t\t\tPolicy: &acme.Policy{\n\t\t\t\t\t\t\t\tX509: acme.X509Policy{\n\t\t\t\t\t\t\t\t\tAllowed: acme.PolicyNames{\n\t\t\t\t\t\t\t\t\t\tDNSNames: []string{\"*.local\"},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t}, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: acme.NewError(acme.ErrorRejectedIdentifierType, \"not authorized\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/prov.AuthorizeOrderIdentifier-error\": func(t *testing.T) test {\n\t\t\toptions := &provisioner.Options{\n\t\t\t\tX509: &provisioner.X509Options{\n\t\t\t\t\tAllowedNames: &policy.X509NameOptions{\n\t\t\t\t\t\tDNSDomains: []string{\"*.local\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t\tprovWithPolicy := newACMEProvWithOptions(t, options)\n\t\t\tprovWithPolicy.RequireEAB = true\n\t\t\tacc := &acme.Account{ID: \"accID\"}\n\t\t\tfr := &NewOrderRequest{\n\t\t\t\tIdentifiers: []acme.Identifier{\n\t\t\t\t\t{Type: \"dns\", Value: \"zap.internal\"},\n\t\t\t\t},\n\t\t\t}\n\t\t\tb, err := json.Marshal(fr)\n\t\t\tassert.FatalError(t, err)\n\t\t\tctx := acme.NewProvisionerContext(context.Background(), provWithPolicy)\n\t\t\tctx = context.WithValue(ctx, accContextKey, acc)\n\t\t\tctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b})\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 400,\n\t\t\t\tca:         &mockCA{},\n\t\t\t\tdb: &acme.MockDB{\n\t\t\t\t\tMockGetExternalAccountKeyByAccountID: func(ctx context.Context, provisionerID, accountID string) (*acme.ExternalAccountKey, error) {\n\t\t\t\t\t\tassert.Equals(t, prov.GetID(), provisionerID)\n\t\t\t\t\t\tassert.Equals(t, \"accID\", accountID)\n\t\t\t\t\t\treturn &acme.ExternalAccountKey{\n\t\t\t\t\t\t\tPolicy: &acme.Policy{\n\t\t\t\t\t\t\t\tX509: acme.X509Policy{\n\t\t\t\t\t\t\t\t\tAllowed: acme.PolicyNames{\n\t\t\t\t\t\t\t\t\t\tDNSNames: []string{\"*.internal\"},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t}, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: acme.NewError(acme.ErrorRejectedIdentifierType, \"not authorized\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/ca.AreSANsAllowed-error\": func(t *testing.T) test {\n\t\t\toptions := &provisioner.Options{\n\t\t\t\tX509: &provisioner.X509Options{\n\t\t\t\t\tAllowedNames: &policy.X509NameOptions{\n\t\t\t\t\t\tDNSDomains: []string{\"*.internal\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t\tprovWithPolicy := newACMEProvWithOptions(t, options)\n\t\t\tprovWithPolicy.RequireEAB = true\n\t\t\tacc := &acme.Account{ID: \"accID\"}\n\t\t\tfr := &NewOrderRequest{\n\t\t\t\tIdentifiers: []acme.Identifier{\n\t\t\t\t\t{Type: \"dns\", Value: \"zap.internal\"},\n\t\t\t\t},\n\t\t\t}\n\t\t\tb, err := json.Marshal(fr)\n\t\t\tassert.FatalError(t, err)\n\t\t\tctx := acme.NewProvisionerContext(context.Background(), provWithPolicy)\n\t\t\tctx = context.WithValue(ctx, accContextKey, acc)\n\t\t\tctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b})\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 400,\n\t\t\t\tca: &mockCA{\n\t\t\t\t\tMockAreSANsallowed: func(ctx context.Context, sans []string) error {\n\t\t\t\t\t\treturn errors.New(\"force: not authorized by authority\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tdb: &acme.MockDB{\n\t\t\t\t\tMockGetExternalAccountKeyByAccountID: func(ctx context.Context, provisionerID, accountID string) (*acme.ExternalAccountKey, error) {\n\t\t\t\t\t\tassert.Equals(t, prov.GetID(), provisionerID)\n\t\t\t\t\t\tassert.Equals(t, \"accID\", accountID)\n\t\t\t\t\t\treturn &acme.ExternalAccountKey{\n\t\t\t\t\t\t\tPolicy: &acme.Policy{\n\t\t\t\t\t\t\t\tX509: acme.X509Policy{\n\t\t\t\t\t\t\t\t\tAllowed: acme.PolicyNames{\n\t\t\t\t\t\t\t\t\t\tDNSNames: []string{\"*.internal\"},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t}, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: acme.NewError(acme.ErrorRejectedIdentifierType, \"not authorized\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/error-h.newAuthorization\": func(t *testing.T) test {\n\t\t\tacc := &acme.Account{ID: \"accID\"}\n\t\t\tfr := &NewOrderRequest{\n\t\t\t\tIdentifiers: []acme.Identifier{\n\t\t\t\t\t{Type: \"dns\", Value: \"zap.internal\"},\n\t\t\t\t},\n\t\t\t}\n\t\t\tb, err := json.Marshal(fr)\n\t\t\tassert.FatalError(t, err)\n\t\t\tctx := acme.NewProvisionerContext(context.Background(), prov)\n\t\t\tctx = context.WithValue(ctx, accContextKey, acc)\n\t\t\tctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b})\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 500,\n\t\t\t\tca:         &mockCA{},\n\t\t\t\tdb: &acme.MockDB{\n\t\t\t\t\tMockCreateChallenge: func(ctx context.Context, ch *acme.Challenge) error {\n\t\t\t\t\t\tassert.Equals(t, ch.AccountID, \"accID\")\n\t\t\t\t\t\tassert.Equals(t, ch.Type, acme.DNS01)\n\t\t\t\t\t\tassert.NotEquals(t, ch.Token, \"\")\n\t\t\t\t\t\tassert.Equals(t, ch.Status, acme.StatusPending)\n\t\t\t\t\t\tassert.Equals(t, ch.Value, \"zap.internal\")\n\t\t\t\t\t\treturn errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t\tMockGetExternalAccountKeyByAccountID: func(ctx context.Context, provisionerID, accountID string) (*acme.ExternalAccountKey, error) {\n\t\t\t\t\t\tassert.Equals(t, prov.GetID(), provisionerID)\n\t\t\t\t\t\tassert.Equals(t, \"accID\", accountID)\n\t\t\t\t\t\treturn nil, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: acme.NewErrorISE(\"error creating challenge: force\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/error-db.CreateOrder\": func(t *testing.T) test {\n\t\t\tacc := &acme.Account{ID: \"accID\"}\n\t\t\tfr := &NewOrderRequest{\n\t\t\t\tIdentifiers: []acme.Identifier{\n\t\t\t\t\t{Type: \"dns\", Value: \"zap.internal\"},\n\t\t\t\t},\n\t\t\t}\n\t\t\tb, err := json.Marshal(fr)\n\t\t\tassert.FatalError(t, err)\n\t\t\tctx := acme.NewProvisionerContext(context.Background(), prov)\n\t\t\tctx = context.WithValue(ctx, accContextKey, acc)\n\t\t\tctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b})\n\t\t\tvar (\n\t\t\t\tch1, ch2, ch3 **acme.Challenge\n\t\t\t\taz1ID         *string\n\t\t\t\tcount         = 0\n\t\t\t)\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 500,\n\t\t\t\tca:         &mockCA{},\n\t\t\t\tdb: &acme.MockDB{\n\t\t\t\t\tMockCreateChallenge: func(ctx context.Context, ch *acme.Challenge) error {\n\t\t\t\t\t\tswitch count {\n\t\t\t\t\t\tcase 0:\n\t\t\t\t\t\t\tch.ID = \"dns\"\n\t\t\t\t\t\t\tassert.Equals(t, ch.Type, acme.DNS01)\n\t\t\t\t\t\t\tch1 = &ch\n\t\t\t\t\t\tcase 1:\n\t\t\t\t\t\t\tch.ID = \"http\"\n\t\t\t\t\t\t\tassert.Equals(t, ch.Type, acme.HTTP01)\n\t\t\t\t\t\t\tch2 = &ch\n\t\t\t\t\t\tcase 2:\n\t\t\t\t\t\t\tch.ID = \"tls\"\n\t\t\t\t\t\t\tassert.Equals(t, ch.Type, acme.TLSALPN01)\n\t\t\t\t\t\t\tch3 = &ch\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\tassert.FatalError(t, errors.New(\"test logic error\"))\n\t\t\t\t\t\t\treturn errors.New(\"force\")\n\t\t\t\t\t\t}\n\t\t\t\t\t\tcount++\n\t\t\t\t\t\tassert.Equals(t, ch.AccountID, \"accID\")\n\t\t\t\t\t\tassert.NotEquals(t, ch.Token, \"\")\n\t\t\t\t\t\tassert.Equals(t, ch.Status, acme.StatusPending)\n\t\t\t\t\t\tassert.Equals(t, ch.Value, \"zap.internal\")\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t\tMockCreateAuthorization: func(ctx context.Context, az *acme.Authorization) error {\n\t\t\t\t\t\taz.ID = \"az1ID\"\n\t\t\t\t\t\taz1ID = &az.ID\n\t\t\t\t\t\tassert.Equals(t, az.AccountID, \"accID\")\n\t\t\t\t\t\tassert.NotEquals(t, az.Token, \"\")\n\t\t\t\t\t\tassert.Equals(t, az.Status, acme.StatusPending)\n\t\t\t\t\t\tassert.Equals(t, az.Identifier, fr.Identifiers[0])\n\t\t\t\t\t\tassert.Equals(t, az.Challenges, []*acme.Challenge{*ch1, *ch2, *ch3})\n\t\t\t\t\t\tassert.Equals(t, az.Wildcard, false)\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t\tMockCreateOrder: func(ctx context.Context, o *acme.Order) error {\n\t\t\t\t\t\tassert.Equals(t, o.AccountID, \"accID\")\n\t\t\t\t\t\tassert.Equals(t, o.ProvisionerID, prov.GetID())\n\t\t\t\t\t\tassert.Equals(t, o.Status, acme.StatusPending)\n\t\t\t\t\t\tassert.Equals(t, o.Identifiers, fr.Identifiers)\n\t\t\t\t\t\tassert.Equals(t, o.AuthorizationIDs, []string{*az1ID})\n\t\t\t\t\t\treturn errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t\tMockGetExternalAccountKeyByAccountID: func(ctx context.Context, provisionerID, accountID string) (*acme.ExternalAccountKey, error) {\n\t\t\t\t\t\tassert.Equals(t, prov.GetID(), provisionerID)\n\t\t\t\t\t\tassert.Equals(t, \"accID\", accountID)\n\t\t\t\t\t\treturn nil, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: acme.NewErrorISE(\"error creating order: force\"),\n\t\t\t}\n\t\t},\n\t\t\"ok/multiple-authz\": func(t *testing.T) test {\n\t\t\tacc := &acme.Account{ID: \"accID\"}\n\t\t\tnor := &NewOrderRequest{\n\t\t\t\tIdentifiers: []acme.Identifier{\n\t\t\t\t\t{Type: \"dns\", Value: \"zap.internal\"},\n\t\t\t\t\t{Type: \"dns\", Value: \"*.zar.internal\"},\n\t\t\t\t},\n\t\t\t}\n\t\t\tb, err := json.Marshal(nor)\n\t\t\tassert.FatalError(t, err)\n\t\t\tctx := acme.NewProvisionerContext(context.Background(), prov)\n\t\t\tctx = context.WithValue(ctx, accContextKey, acc)\n\t\t\tctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b})\n\t\t\tvar (\n\t\t\t\tch1, ch2, ch3, ch4 **acme.Challenge\n\t\t\t\taz1ID, az2ID       *string\n\t\t\t\tchCount, azCount   = 0, 0\n\t\t\t)\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 201,\n\t\t\t\tnor:        nor,\n\t\t\t\tca:         &mockCA{},\n\t\t\t\tdb: &acme.MockDB{\n\t\t\t\t\tMockCreateChallenge: func(ctx context.Context, ch *acme.Challenge) error {\n\t\t\t\t\t\tswitch chCount {\n\t\t\t\t\t\tcase 0:\n\t\t\t\t\t\t\tch.ID = \"dns\"\n\t\t\t\t\t\t\tassert.Equals(t, ch.Type, acme.DNS01)\n\t\t\t\t\t\t\tassert.Equals(t, ch.Value, \"zap.internal\")\n\t\t\t\t\t\t\tch1 = &ch\n\t\t\t\t\t\tcase 1:\n\t\t\t\t\t\t\tch.ID = \"http\"\n\t\t\t\t\t\t\tassert.Equals(t, ch.Type, acme.HTTP01)\n\t\t\t\t\t\t\tassert.Equals(t, ch.Value, \"zap.internal\")\n\t\t\t\t\t\t\tch2 = &ch\n\t\t\t\t\t\tcase 2:\n\t\t\t\t\t\t\tch.ID = \"tls\"\n\t\t\t\t\t\t\tassert.Equals(t, ch.Type, acme.TLSALPN01)\n\t\t\t\t\t\t\tassert.Equals(t, ch.Value, \"zap.internal\")\n\t\t\t\t\t\t\tch3 = &ch\n\t\t\t\t\t\tcase 3:\n\t\t\t\t\t\t\tch.ID = \"dns\"\n\t\t\t\t\t\t\tassert.Equals(t, ch.Type, acme.DNS01)\n\t\t\t\t\t\t\tassert.Equals(t, ch.Value, \"zar.internal\")\n\t\t\t\t\t\t\tch4 = &ch\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\tassert.FatalError(t, errors.New(\"test logic error\"))\n\t\t\t\t\t\t\treturn errors.New(\"force\")\n\t\t\t\t\t\t}\n\t\t\t\t\t\tchCount++\n\t\t\t\t\t\tassert.Equals(t, ch.AccountID, \"accID\")\n\t\t\t\t\t\tassert.NotEquals(t, ch.Token, \"\")\n\t\t\t\t\t\tassert.Equals(t, ch.Status, acme.StatusPending)\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t\tMockCreateAuthorization: func(ctx context.Context, az *acme.Authorization) error {\n\t\t\t\t\t\tswitch azCount {\n\t\t\t\t\t\tcase 0:\n\t\t\t\t\t\t\taz.ID = \"az1ID\"\n\t\t\t\t\t\t\taz1ID = &az.ID\n\t\t\t\t\t\t\tassert.Equals(t, az.Identifier, nor.Identifiers[0])\n\t\t\t\t\t\t\tassert.Equals(t, az.Wildcard, false)\n\t\t\t\t\t\t\tassert.Equals(t, az.Challenges, []*acme.Challenge{*ch1, *ch2, *ch3})\n\t\t\t\t\t\tcase 1:\n\t\t\t\t\t\t\taz.ID = \"az2ID\"\n\t\t\t\t\t\t\taz2ID = &az.ID\n\t\t\t\t\t\t\tassert.Equals(t, az.Identifier, acme.Identifier{\n\t\t\t\t\t\t\t\tType:  acme.DNS,\n\t\t\t\t\t\t\t\tValue: \"zar.internal\",\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\tassert.Equals(t, az.Wildcard, true)\n\t\t\t\t\t\t\tassert.Equals(t, az.Challenges, []*acme.Challenge{*ch4})\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\tassert.FatalError(t, errors.New(\"test logic error\"))\n\t\t\t\t\t\t\treturn errors.New(\"force\")\n\t\t\t\t\t\t}\n\t\t\t\t\t\tazCount++\n\t\t\t\t\t\tassert.Equals(t, az.AccountID, \"accID\")\n\t\t\t\t\t\tassert.NotEquals(t, az.Token, \"\")\n\t\t\t\t\t\tassert.Equals(t, az.Status, acme.StatusPending)\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t\tMockCreateOrder: func(ctx context.Context, o *acme.Order) error {\n\t\t\t\t\t\to.ID = \"ordID\"\n\t\t\t\t\t\tassert.Equals(t, o.AccountID, \"accID\")\n\t\t\t\t\t\tassert.Equals(t, o.ProvisionerID, prov.GetID())\n\t\t\t\t\t\tassert.Equals(t, o.Status, acme.StatusPending)\n\t\t\t\t\t\tassert.Equals(t, o.Identifiers, nor.Identifiers)\n\t\t\t\t\t\tassert.Equals(t, o.AuthorizationIDs, []string{*az1ID, *az2ID})\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t\tMockGetExternalAccountKeyByAccountID: func(ctx context.Context, provisionerID, accountID string) (*acme.ExternalAccountKey, error) {\n\t\t\t\t\t\tassert.Equals(t, prov.GetID(), provisionerID)\n\t\t\t\t\t\tassert.Equals(t, \"accID\", accountID)\n\t\t\t\t\t\treturn nil, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tvr: func(t *testing.T, o *acme.Order) {\n\t\t\t\t\tnow := clock.Now()\n\t\t\t\t\ttestBufferDur := 5 * time.Second\n\t\t\t\t\torderExpiry := now.Add(defaultOrderExpiry)\n\t\t\t\t\texpNbf := now.Add(-defaultOrderBackdate)\n\t\t\t\t\texpNaf := now.Add(prov.DefaultTLSCertDuration())\n\n\t\t\t\t\tassert.Equals(t, o.ID, \"ordID\")\n\t\t\t\t\tassert.Equals(t, o.Status, acme.StatusPending)\n\t\t\t\t\tassert.Equals(t, o.Identifiers, nor.Identifiers)\n\t\t\t\t\tassert.Equals(t, o.AuthorizationURLs, []string{\n\t\t\t\t\t\tfmt.Sprintf(\"%s/acme/%s/authz/az1ID\", baseURL.String(), escProvName),\n\t\t\t\t\t\tfmt.Sprintf(\"%s/acme/%s/authz/az2ID\", baseURL.String(), escProvName),\n\t\t\t\t\t})\n\t\t\t\t\tassert.True(t, o.NotBefore.Add(-testBufferDur).Before(expNbf))\n\t\t\t\t\tassert.True(t, o.NotBefore.Add(testBufferDur).After(expNbf))\n\t\t\t\t\tassert.True(t, o.NotAfter.Add(-testBufferDur).Before(expNaf))\n\t\t\t\t\tassert.True(t, o.NotAfter.Add(testBufferDur).After(expNaf))\n\t\t\t\t\tassert.True(t, o.ExpiresAt.Add(-testBufferDur).Before(orderExpiry))\n\t\t\t\t\tassert.True(t, o.ExpiresAt.Add(testBufferDur).After(orderExpiry))\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"ok/default-naf-nbf\": func(t *testing.T) test {\n\t\t\tacc := &acme.Account{ID: \"accID\"}\n\t\t\tnor := &NewOrderRequest{\n\t\t\t\tIdentifiers: []acme.Identifier{\n\t\t\t\t\t{Type: \"dns\", Value: \"zap.internal\"},\n\t\t\t\t},\n\t\t\t}\n\t\t\tb, err := json.Marshal(nor)\n\t\t\tassert.FatalError(t, err)\n\t\t\tctx := acme.NewProvisionerContext(context.Background(), prov)\n\t\t\tctx = context.WithValue(ctx, accContextKey, acc)\n\t\t\tctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b})\n\t\t\tvar (\n\t\t\t\tch1, ch2, ch3 **acme.Challenge\n\t\t\t\taz1ID         *string\n\t\t\t\tcount         = 0\n\t\t\t)\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 201,\n\t\t\t\tnor:        nor,\n\t\t\t\tca:         &mockCA{},\n\t\t\t\tdb: &acme.MockDB{\n\t\t\t\t\tMockCreateChallenge: func(ctx context.Context, ch *acme.Challenge) error {\n\t\t\t\t\t\tswitch count {\n\t\t\t\t\t\tcase 0:\n\t\t\t\t\t\t\tch.ID = \"dns\"\n\t\t\t\t\t\t\tassert.Equals(t, ch.Type, acme.DNS01)\n\t\t\t\t\t\t\tch1 = &ch\n\t\t\t\t\t\tcase 1:\n\t\t\t\t\t\t\tch.ID = \"http\"\n\t\t\t\t\t\t\tassert.Equals(t, ch.Type, acme.HTTP01)\n\t\t\t\t\t\t\tch2 = &ch\n\t\t\t\t\t\tcase 2:\n\t\t\t\t\t\t\tch.ID = \"tls\"\n\t\t\t\t\t\t\tassert.Equals(t, ch.Type, acme.TLSALPN01)\n\t\t\t\t\t\t\tch3 = &ch\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\tassert.FatalError(t, errors.New(\"test logic error\"))\n\t\t\t\t\t\t\treturn errors.New(\"force\")\n\t\t\t\t\t\t}\n\t\t\t\t\t\tcount++\n\t\t\t\t\t\tassert.Equals(t, ch.AccountID, \"accID\")\n\t\t\t\t\t\tassert.NotEquals(t, ch.Token, \"\")\n\t\t\t\t\t\tassert.Equals(t, ch.Status, acme.StatusPending)\n\t\t\t\t\t\tassert.Equals(t, ch.Value, \"zap.internal\")\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t\tMockCreateAuthorization: func(ctx context.Context, az *acme.Authorization) error {\n\t\t\t\t\t\taz.ID = \"az1ID\"\n\t\t\t\t\t\taz1ID = &az.ID\n\t\t\t\t\t\tassert.Equals(t, az.AccountID, \"accID\")\n\t\t\t\t\t\tassert.NotEquals(t, az.Token, \"\")\n\t\t\t\t\t\tassert.Equals(t, az.Status, acme.StatusPending)\n\t\t\t\t\t\tassert.Equals(t, az.Identifier, nor.Identifiers[0])\n\t\t\t\t\t\tassert.Equals(t, az.Challenges, []*acme.Challenge{*ch1, *ch2, *ch3})\n\t\t\t\t\t\tassert.Equals(t, az.Wildcard, false)\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t\tMockCreateOrder: func(ctx context.Context, o *acme.Order) error {\n\t\t\t\t\t\to.ID = \"ordID\"\n\t\t\t\t\t\tassert.Equals(t, o.AccountID, \"accID\")\n\t\t\t\t\t\tassert.Equals(t, o.ProvisionerID, prov.GetID())\n\t\t\t\t\t\tassert.Equals(t, o.Status, acme.StatusPending)\n\t\t\t\t\t\tassert.Equals(t, o.Identifiers, nor.Identifiers)\n\t\t\t\t\t\tassert.Equals(t, o.AuthorizationIDs, []string{*az1ID})\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t\tMockGetExternalAccountKeyByAccountID: func(ctx context.Context, provisionerID, accountID string) (*acme.ExternalAccountKey, error) {\n\t\t\t\t\t\tassert.Equals(t, prov.GetID(), provisionerID)\n\t\t\t\t\t\tassert.Equals(t, \"accID\", accountID)\n\t\t\t\t\t\treturn nil, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tvr: func(t *testing.T, o *acme.Order) {\n\t\t\t\t\tnow := clock.Now()\n\t\t\t\t\ttestBufferDur := 5 * time.Second\n\t\t\t\t\torderExpiry := now.Add(defaultOrderExpiry)\n\t\t\t\t\texpNbf := now.Add(-defaultOrderBackdate)\n\t\t\t\t\texpNaf := now.Add(prov.DefaultTLSCertDuration())\n\n\t\t\t\t\tassert.Equals(t, o.ID, \"ordID\")\n\t\t\t\t\tassert.Equals(t, o.Status, acme.StatusPending)\n\t\t\t\t\tassert.Equals(t, o.Identifiers, nor.Identifiers)\n\t\t\t\t\tassert.Equals(t, o.AuthorizationURLs, []string{fmt.Sprintf(\"%s/acme/%s/authz/az1ID\", baseURL.String(), escProvName)})\n\t\t\t\t\tassert.True(t, o.NotBefore.Add(-testBufferDur).Before(expNbf))\n\t\t\t\t\tassert.True(t, o.NotBefore.Add(testBufferDur).After(expNbf))\n\t\t\t\t\tassert.True(t, o.NotAfter.Add(-testBufferDur).Before(expNaf))\n\t\t\t\t\tassert.True(t, o.NotAfter.Add(testBufferDur).After(expNaf))\n\t\t\t\t\tassert.True(t, o.ExpiresAt.Add(-testBufferDur).Before(orderExpiry))\n\t\t\t\t\tassert.True(t, o.ExpiresAt.Add(testBufferDur).After(orderExpiry))\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"ok/nbf-no-naf\": func(t *testing.T) test {\n\t\t\tnow := clock.Now()\n\t\t\texpNbf := now.Add(10 * time.Minute)\n\t\t\tacc := &acme.Account{ID: \"accID\"}\n\t\t\tnor := &NewOrderRequest{\n\t\t\t\tIdentifiers: []acme.Identifier{\n\t\t\t\t\t{Type: \"dns\", Value: \"zap.internal\"},\n\t\t\t\t},\n\t\t\t\tNotBefore: expNbf,\n\t\t\t}\n\t\t\tb, err := json.Marshal(nor)\n\t\t\tassert.FatalError(t, err)\n\t\t\tctx := acme.NewProvisionerContext(context.Background(), prov)\n\t\t\tctx = context.WithValue(ctx, accContextKey, acc)\n\t\t\tctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b})\n\t\t\tvar (\n\t\t\t\tch1, ch2, ch3 **acme.Challenge\n\t\t\t\taz1ID         *string\n\t\t\t\tcount         = 0\n\t\t\t)\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 201,\n\t\t\t\tnor:        nor,\n\t\t\t\tca:         &mockCA{},\n\t\t\t\tdb: &acme.MockDB{\n\t\t\t\t\tMockCreateChallenge: func(ctx context.Context, ch *acme.Challenge) error {\n\t\t\t\t\t\tswitch count {\n\t\t\t\t\t\tcase 0:\n\t\t\t\t\t\t\tch.ID = \"dns\"\n\t\t\t\t\t\t\tassert.Equals(t, ch.Type, acme.DNS01)\n\t\t\t\t\t\t\tch1 = &ch\n\t\t\t\t\t\tcase 1:\n\t\t\t\t\t\t\tch.ID = \"http\"\n\t\t\t\t\t\t\tassert.Equals(t, ch.Type, acme.HTTP01)\n\t\t\t\t\t\t\tch2 = &ch\n\t\t\t\t\t\tcase 2:\n\t\t\t\t\t\t\tch.ID = \"tls\"\n\t\t\t\t\t\t\tassert.Equals(t, ch.Type, acme.TLSALPN01)\n\t\t\t\t\t\t\tch3 = &ch\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\tassert.FatalError(t, errors.New(\"test logic error\"))\n\t\t\t\t\t\t\treturn errors.New(\"force\")\n\t\t\t\t\t\t}\n\t\t\t\t\t\tcount++\n\t\t\t\t\t\tassert.Equals(t, ch.AccountID, \"accID\")\n\t\t\t\t\t\tassert.NotEquals(t, ch.Token, \"\")\n\t\t\t\t\t\tassert.Equals(t, ch.Status, acme.StatusPending)\n\t\t\t\t\t\tassert.Equals(t, ch.Value, \"zap.internal\")\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t\tMockCreateAuthorization: func(ctx context.Context, az *acme.Authorization) error {\n\t\t\t\t\t\taz.ID = \"az1ID\"\n\t\t\t\t\t\taz1ID = &az.ID\n\t\t\t\t\t\tassert.Equals(t, az.AccountID, \"accID\")\n\t\t\t\t\t\tassert.NotEquals(t, az.Token, \"\")\n\t\t\t\t\t\tassert.Equals(t, az.Status, acme.StatusPending)\n\t\t\t\t\t\tassert.Equals(t, az.Identifier, nor.Identifiers[0])\n\t\t\t\t\t\tassert.Equals(t, az.Challenges, []*acme.Challenge{*ch1, *ch2, *ch3})\n\t\t\t\t\t\tassert.Equals(t, az.Wildcard, false)\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t\tMockCreateOrder: func(ctx context.Context, o *acme.Order) error {\n\t\t\t\t\t\to.ID = \"ordID\"\n\t\t\t\t\t\tassert.Equals(t, o.AccountID, \"accID\")\n\t\t\t\t\t\tassert.Equals(t, o.ProvisionerID, prov.GetID())\n\t\t\t\t\t\tassert.Equals(t, o.Status, acme.StatusPending)\n\t\t\t\t\t\tassert.Equals(t, o.Identifiers, nor.Identifiers)\n\t\t\t\t\t\tassert.Equals(t, o.AuthorizationIDs, []string{*az1ID})\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t\tMockGetExternalAccountKeyByAccountID: func(ctx context.Context, provisionerID, accountID string) (*acme.ExternalAccountKey, error) {\n\t\t\t\t\t\tassert.Equals(t, prov.GetID(), provisionerID)\n\t\t\t\t\t\tassert.Equals(t, \"accID\", accountID)\n\t\t\t\t\t\treturn nil, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tvr: func(t *testing.T, o *acme.Order) {\n\t\t\t\t\tnow := clock.Now()\n\t\t\t\t\ttestBufferDur := 5 * time.Second\n\t\t\t\t\torderExpiry := now.Add(defaultOrderExpiry)\n\t\t\t\t\texpNaf := expNbf.Add(prov.DefaultTLSCertDuration())\n\n\t\t\t\t\tassert.Equals(t, o.ID, \"ordID\")\n\t\t\t\t\tassert.Equals(t, o.Status, acme.StatusPending)\n\t\t\t\t\tassert.Equals(t, o.Identifiers, nor.Identifiers)\n\t\t\t\t\tassert.Equals(t, o.AuthorizationURLs, []string{fmt.Sprintf(\"%s/acme/%s/authz/az1ID\", baseURL.String(), escProvName)})\n\t\t\t\t\tassert.True(t, o.NotBefore.Add(-testBufferDur).Before(expNbf))\n\t\t\t\t\tassert.True(t, o.NotBefore.Add(testBufferDur).After(expNbf))\n\t\t\t\t\tassert.True(t, o.NotAfter.Add(-testBufferDur).Before(expNaf))\n\t\t\t\t\tassert.True(t, o.NotAfter.Add(testBufferDur).After(expNaf))\n\t\t\t\t\tassert.True(t, o.ExpiresAt.Add(-testBufferDur).Before(orderExpiry))\n\t\t\t\t\tassert.True(t, o.ExpiresAt.Add(testBufferDur).After(orderExpiry))\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"ok/naf-no-nbf\": func(t *testing.T) test {\n\t\t\tnow := clock.Now()\n\t\t\texpNaf := now.Add(15 * time.Minute)\n\t\t\tacc := &acme.Account{ID: \"accID\"}\n\t\t\tnor := &NewOrderRequest{\n\t\t\t\tIdentifiers: []acme.Identifier{\n\t\t\t\t\t{Type: \"dns\", Value: \"zap.internal\"},\n\t\t\t\t},\n\t\t\t\tNotAfter: expNaf,\n\t\t\t}\n\t\t\tb, err := json.Marshal(nor)\n\t\t\tassert.FatalError(t, err)\n\t\t\tctx := acme.NewProvisionerContext(context.Background(), prov)\n\t\t\tctx = context.WithValue(ctx, accContextKey, acc)\n\t\t\tctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b})\n\t\t\tvar (\n\t\t\t\tch1, ch2, ch3 **acme.Challenge\n\t\t\t\taz1ID         *string\n\t\t\t\tcount         = 0\n\t\t\t)\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 201,\n\t\t\t\tnor:        nor,\n\t\t\t\tca:         &mockCA{},\n\t\t\t\tdb: &acme.MockDB{\n\t\t\t\t\tMockCreateChallenge: func(ctx context.Context, ch *acme.Challenge) error {\n\t\t\t\t\t\tswitch count {\n\t\t\t\t\t\tcase 0:\n\t\t\t\t\t\t\tch.ID = \"dns\"\n\t\t\t\t\t\t\tassert.Equals(t, ch.Type, acme.DNS01)\n\t\t\t\t\t\t\tch1 = &ch\n\t\t\t\t\t\tcase 1:\n\t\t\t\t\t\t\tch.ID = \"http\"\n\t\t\t\t\t\t\tassert.Equals(t, ch.Type, acme.HTTP01)\n\t\t\t\t\t\t\tch2 = &ch\n\t\t\t\t\t\tcase 2:\n\t\t\t\t\t\t\tch.ID = \"tls\"\n\t\t\t\t\t\t\tassert.Equals(t, ch.Type, acme.TLSALPN01)\n\t\t\t\t\t\t\tch3 = &ch\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\tassert.FatalError(t, errors.New(\"test logic error\"))\n\t\t\t\t\t\t\treturn errors.New(\"force\")\n\t\t\t\t\t\t}\n\t\t\t\t\t\tcount++\n\t\t\t\t\t\tassert.Equals(t, ch.AccountID, \"accID\")\n\t\t\t\t\t\tassert.NotEquals(t, ch.Token, \"\")\n\t\t\t\t\t\tassert.Equals(t, ch.Status, acme.StatusPending)\n\t\t\t\t\t\tassert.Equals(t, ch.Value, \"zap.internal\")\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t\tMockCreateAuthorization: func(ctx context.Context, az *acme.Authorization) error {\n\t\t\t\t\t\taz.ID = \"az1ID\"\n\t\t\t\t\t\taz1ID = &az.ID\n\t\t\t\t\t\tassert.Equals(t, az.AccountID, \"accID\")\n\t\t\t\t\t\tassert.NotEquals(t, az.Token, \"\")\n\t\t\t\t\t\tassert.Equals(t, az.Status, acme.StatusPending)\n\t\t\t\t\t\tassert.Equals(t, az.Identifier, nor.Identifiers[0])\n\t\t\t\t\t\tassert.Equals(t, az.Challenges, []*acme.Challenge{*ch1, *ch2, *ch3})\n\t\t\t\t\t\tassert.Equals(t, az.Wildcard, false)\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t\tMockCreateOrder: func(ctx context.Context, o *acme.Order) error {\n\t\t\t\t\t\to.ID = \"ordID\"\n\t\t\t\t\t\tassert.Equals(t, o.AccountID, \"accID\")\n\t\t\t\t\t\tassert.Equals(t, o.ProvisionerID, prov.GetID())\n\t\t\t\t\t\tassert.Equals(t, o.Status, acme.StatusPending)\n\t\t\t\t\t\tassert.Equals(t, o.Identifiers, nor.Identifiers)\n\t\t\t\t\t\tassert.Equals(t, o.AuthorizationIDs, []string{*az1ID})\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t\tMockGetExternalAccountKeyByAccountID: func(ctx context.Context, provisionerID, accountID string) (*acme.ExternalAccountKey, error) {\n\t\t\t\t\t\tassert.Equals(t, prov.GetID(), provisionerID)\n\t\t\t\t\t\tassert.Equals(t, \"accID\", accountID)\n\t\t\t\t\t\treturn nil, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tvr: func(t *testing.T, o *acme.Order) {\n\t\t\t\t\ttestBufferDur := 5 * time.Second\n\t\t\t\t\torderExpiry := now.Add(defaultOrderExpiry)\n\t\t\t\t\texpNbf := now.Add(-defaultOrderBackdate)\n\n\t\t\t\t\tassert.Equals(t, o.ID, \"ordID\")\n\t\t\t\t\tassert.Equals(t, o.Status, acme.StatusPending)\n\t\t\t\t\tassert.Equals(t, o.Identifiers, nor.Identifiers)\n\t\t\t\t\tassert.Equals(t, o.AuthorizationURLs, []string{fmt.Sprintf(\"%s/acme/%s/authz/az1ID\", baseURL.String(), escProvName)})\n\t\t\t\t\tassert.True(t, o.NotBefore.Add(-testBufferDur).Before(expNbf))\n\t\t\t\t\tassert.True(t, o.NotBefore.Add(testBufferDur).After(expNbf))\n\t\t\t\t\tassert.True(t, o.NotAfter.Add(-testBufferDur).Before(expNaf))\n\t\t\t\t\tassert.True(t, o.NotAfter.Add(testBufferDur).After(expNaf))\n\t\t\t\t\tassert.True(t, o.ExpiresAt.Add(-testBufferDur).Before(orderExpiry))\n\t\t\t\t\tassert.True(t, o.ExpiresAt.Add(testBufferDur).After(orderExpiry))\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"ok/naf-nbf-from-ca\": func(t *testing.T) test {\n\t\t\tnow := clock.Now()\n\t\t\texpNaf := now.Add(15 * time.Minute)\n\t\t\tacc := &acme.Account{ID: \"accID\"}\n\t\t\tnor := &NewOrderRequest{\n\t\t\t\tIdentifiers: []acme.Identifier{\n\t\t\t\t\t{Type: \"dns\", Value: \"zap.internal\"},\n\t\t\t\t},\n\t\t\t\tNotAfter: expNaf,\n\t\t\t}\n\t\t\tb, err := json.Marshal(nor)\n\t\t\tassert.FatalError(t, err)\n\t\t\tctx := acme.NewProvisionerContext(context.Background(), prov)\n\t\t\tctx = context.WithValue(ctx, accContextKey, acc)\n\t\t\tctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b})\n\t\t\tvar (\n\t\t\t\tch1, ch2, ch3 **acme.Challenge\n\t\t\t\taz1ID         *string\n\t\t\t\tcount         = 0\n\t\t\t)\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 201,\n\t\t\t\tnor:        nor,\n\t\t\t\tca: &mockCA{\n\t\t\t\t\tMockGetBackdate: func() *time.Duration {\n\t\t\t\t\t\td, err := time.ParseDuration(\"1h\")\n\t\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t\t\treturn &d\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tdb: &acme.MockDB{\n\t\t\t\t\tMockCreateChallenge: func(ctx context.Context, ch *acme.Challenge) error {\n\t\t\t\t\t\tswitch count {\n\t\t\t\t\t\tcase 0:\n\t\t\t\t\t\t\tch.ID = \"dns\"\n\t\t\t\t\t\t\tassert.Equals(t, ch.Type, acme.DNS01)\n\t\t\t\t\t\t\tch1 = &ch\n\t\t\t\t\t\tcase 1:\n\t\t\t\t\t\t\tch.ID = \"http\"\n\t\t\t\t\t\t\tassert.Equals(t, ch.Type, acme.HTTP01)\n\t\t\t\t\t\t\tch2 = &ch\n\t\t\t\t\t\tcase 2:\n\t\t\t\t\t\t\tch.ID = \"tls\"\n\t\t\t\t\t\t\tassert.Equals(t, ch.Type, acme.TLSALPN01)\n\t\t\t\t\t\t\tch3 = &ch\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\tassert.FatalError(t, errors.New(\"test logic error\"))\n\t\t\t\t\t\t\treturn errors.New(\"force\")\n\t\t\t\t\t\t}\n\t\t\t\t\t\tcount++\n\t\t\t\t\t\tassert.Equals(t, ch.AccountID, \"accID\")\n\t\t\t\t\t\tassert.NotEquals(t, ch.Token, \"\")\n\t\t\t\t\t\tassert.Equals(t, ch.Status, acme.StatusPending)\n\t\t\t\t\t\tassert.Equals(t, ch.Value, \"zap.internal\")\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t\tMockCreateAuthorization: func(ctx context.Context, az *acme.Authorization) error {\n\t\t\t\t\t\taz.ID = \"az1ID\"\n\t\t\t\t\t\taz1ID = &az.ID\n\t\t\t\t\t\tassert.Equals(t, az.AccountID, \"accID\")\n\t\t\t\t\t\tassert.NotEquals(t, az.Token, \"\")\n\t\t\t\t\t\tassert.Equals(t, az.Status, acme.StatusPending)\n\t\t\t\t\t\tassert.Equals(t, az.Identifier, nor.Identifiers[0])\n\t\t\t\t\t\tassert.Equals(t, az.Challenges, []*acme.Challenge{*ch1, *ch2, *ch3})\n\t\t\t\t\t\tassert.Equals(t, az.Wildcard, false)\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t\tMockCreateOrder: func(ctx context.Context, o *acme.Order) error {\n\t\t\t\t\t\to.ID = \"ordID\"\n\t\t\t\t\t\tassert.Equals(t, o.AccountID, \"accID\")\n\t\t\t\t\t\tassert.Equals(t, o.ProvisionerID, prov.GetID())\n\t\t\t\t\t\tassert.Equals(t, o.Status, acme.StatusPending)\n\t\t\t\t\t\tassert.Equals(t, o.Identifiers, nor.Identifiers)\n\t\t\t\t\t\tassert.Equals(t, o.AuthorizationIDs, []string{*az1ID})\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t\tMockGetExternalAccountKeyByAccountID: func(ctx context.Context, provisionerID, accountID string) (*acme.ExternalAccountKey, error) {\n\t\t\t\t\t\tassert.Equals(t, prov.GetID(), provisionerID)\n\t\t\t\t\t\tassert.Equals(t, \"accID\", accountID)\n\t\t\t\t\t\treturn nil, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tvr: func(t *testing.T, o *acme.Order) {\n\t\t\t\t\ttestBufferDur := 5 * time.Second\n\t\t\t\t\torderExpiry := now.Add(defaultOrderExpiry)\n\t\t\t\t\texpNbf := now.Add(-1 * time.Hour)\n\n\t\t\t\t\tassert.Equals(t, o.ID, \"ordID\")\n\t\t\t\t\tassert.Equals(t, o.Status, acme.StatusPending)\n\t\t\t\t\tassert.Equals(t, o.Identifiers, nor.Identifiers)\n\t\t\t\t\tassert.Equals(t, o.AuthorizationURLs, []string{fmt.Sprintf(\"%s/acme/%s/authz/az1ID\", baseURL.String(), escProvName)})\n\t\t\t\t\tassert.True(t, o.NotBefore.Add(-testBufferDur).Before(expNbf))\n\t\t\t\t\tassert.True(t, o.NotBefore.Add(testBufferDur).After(expNbf))\n\t\t\t\t\tassert.True(t, o.NotAfter.Add(-testBufferDur).Before(expNaf))\n\t\t\t\t\tassert.True(t, o.NotAfter.Add(testBufferDur).After(expNaf))\n\t\t\t\t\tassert.True(t, o.ExpiresAt.Add(-testBufferDur).Before(orderExpiry))\n\t\t\t\t\tassert.True(t, o.ExpiresAt.Add(testBufferDur).After(orderExpiry))\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"ok/default-naf-nbf-wireapp\": func(t *testing.T) test {\n\t\t\tacmeWireProv := newWireProvisionerWithOptions(t, &provisioner.Options{\n\t\t\t\tWire: &wire.Options{\n\t\t\t\t\tOIDC: &wire.OIDCOptions{\n\t\t\t\t\t\tProvider: &wire.Provider{\n\t\t\t\t\t\t\tIssuerURL:   \"https://issuer.example.com\",\n\t\t\t\t\t\t\tAuthURL:     \"\",\n\t\t\t\t\t\t\tTokenURL:    \"\",\n\t\t\t\t\t\t\tJWKSURL:     \"\",\n\t\t\t\t\t\t\tUserInfoURL: \"\",\n\t\t\t\t\t\t\tAlgorithms:  []string{\"ES256\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tConfig: &wire.Config{\n\t\t\t\t\t\t\tClientID:                   \"integration test\",\n\t\t\t\t\t\t\tSignatureAlgorithms:        []string{\"ES256\"},\n\t\t\t\t\t\t\tSkipClientIDCheck:          true,\n\t\t\t\t\t\t\tSkipExpiryCheck:            true,\n\t\t\t\t\t\t\tSkipIssuerCheck:            true,\n\t\t\t\t\t\t\tInsecureSkipSignatureCheck: true,\n\t\t\t\t\t\t\tNow:                        time.Now,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tDPOP: &wire.DPOPOptions{\n\t\t\t\t\t\tSigningKey: []byte(fakeWireSigningKey),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t})\n\t\t\tacc := &acme.Account{ID: \"accID\"}\n\t\t\tnor := &NewOrderRequest{\n\t\t\t\tIdentifiers: []acme.Identifier{\n\t\t\t\t\t{Type: \"wireapp-user\", Value: `{\"name\": \"Smith, Alice M (QA)\", \"domain\": \"example.com\", \"handle\": \"wireapp://%40alice_wire@wire.com\"}`},\n\t\t\t\t\t{Type: \"wireapp-device\", Value: `{\"name\": \"Smith, Alice M (QA)\", \"domain\": \"example.com\", \"client-id\": \"wireapp://lJGYPz0ZRq2kvc_XpdaDlA!ed416ce8ecdd9fad@example.com\", \"handle\": \"wireapp://%40alice_wire@wire.com\"}`},\n\t\t\t\t},\n\t\t\t}\n\t\t\tb, err := json.Marshal(nor)\n\t\t\tassert.FatalError(t, err)\n\t\t\tctx := acme.NewProvisionerContext(context.Background(), acmeWireProv)\n\t\t\tctx = context.WithValue(ctx, accContextKey, acc)\n\t\t\tctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b})\n\t\t\tvar (\n\t\t\t\tch1, ch2         **acme.Challenge\n\t\t\t\taz1ID, az2ID     *string\n\t\t\t\tchCount, azCount = 0, 0\n\t\t\t)\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 201,\n\t\t\t\tnor:        nor,\n\t\t\t\tca:         &mockCA{},\n\t\t\t\tdb: &acme.MockDB{\n\t\t\t\t\tMockCreateChallenge: func(ctx context.Context, ch *acme.Challenge) error {\n\t\t\t\t\t\tswitch chCount {\n\t\t\t\t\t\tcase 0:\n\t\t\t\t\t\t\tassert.Equals(t, ch.Type, acme.WIREOIDC01)\n\t\t\t\t\t\t\tassert.Equals(t, ch.Value, `{\"name\": \"Smith, Alice M (QA)\", \"domain\": \"example.com\", \"handle\": \"wireapp://%40alice_wire@wire.com\"}`)\n\t\t\t\t\t\t\tch.ID = \"wireapp-oidc\"\n\t\t\t\t\t\t\tch1 = &ch\n\t\t\t\t\t\tcase 1:\n\t\t\t\t\t\t\tassert.Equals(t, ch.Type, acme.WIREDPOP01)\n\t\t\t\t\t\t\tassert.Equals(t, ch.Value, `{\"name\": \"Smith, Alice M (QA)\", \"domain\": \"example.com\", \"client-id\": \"wireapp://lJGYPz0ZRq2kvc_XpdaDlA!ed416ce8ecdd9fad@example.com\", \"handle\": \"wireapp://%40alice_wire@wire.com\"}`)\n\t\t\t\t\t\t\tch.ID = \"wireapp-dpop\"\n\t\t\t\t\t\t\tch2 = &ch\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\trequire.Fail(t, \"test logic error\")\n\t\t\t\t\t\t}\n\t\t\t\t\t\tchCount++\n\t\t\t\t\t\tassert.Equals(t, ch.AccountID, \"accID\")\n\t\t\t\t\t\tassert.NotEquals(t, ch.Token, \"\")\n\t\t\t\t\t\tassert.Equals(t, ch.Status, acme.StatusPending)\n\n\t\t\t\t\t\t_, _ = ch1, ch2\n\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t\tMockCreateAuthorization: func(ctx context.Context, az *acme.Authorization) error {\n\t\t\t\t\t\tswitch azCount {\n\t\t\t\t\t\tcase 0:\n\t\t\t\t\t\t\taz.ID = \"az1ID\"\n\t\t\t\t\t\t\taz1ID = &az.ID\n\t\t\t\t\t\t\tassert.Equals(t, az.Identifier, nor.Identifiers[0])\n\t\t\t\t\t\t\tassert.Equals(t, az.Challenges, []*acme.Challenge{*ch1})\n\t\t\t\t\t\tcase 1:\n\t\t\t\t\t\t\taz.ID = \"az2ID\"\n\t\t\t\t\t\t\taz2ID = &az.ID\n\t\t\t\t\t\t\tassert.Equals(t, az.Identifier, nor.Identifiers[1])\n\t\t\t\t\t\t\tassert.Equals(t, az.Challenges, []*acme.Challenge{*ch2})\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\trequire.Fail(t, \"test logic error\")\n\t\t\t\t\t\t}\n\t\t\t\t\t\tazCount++\n\n\t\t\t\t\t\tassert.Equals(t, az.AccountID, \"accID\")\n\t\t\t\t\t\tassert.NotEquals(t, az.Token, \"\")\n\t\t\t\t\t\tassert.Equals(t, az.Status, acme.StatusPending)\n\t\t\t\t\t\tassert.Equals(t, az.Wildcard, false)\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t\tMockCreateOrder: func(ctx context.Context, o *acme.Order) error {\n\t\t\t\t\t\to.ID = \"ordID\"\n\t\t\t\t\t\tassert.Equals(t, o.AccountID, \"accID\")\n\t\t\t\t\t\tassert.Equals(t, o.ProvisionerID, prov.GetID())\n\t\t\t\t\t\tassert.Equals(t, o.Status, acme.StatusPending)\n\t\t\t\t\t\tassert.Equals(t, o.Identifiers, nor.Identifiers)\n\t\t\t\t\t\tassert.Equals(t, o.AuthorizationIDs, []string{*az1ID, *az2ID})\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t\tMockGetExternalAccountKeyByAccountID: func(ctx context.Context, provisionerID, accountID string) (*acme.ExternalAccountKey, error) {\n\t\t\t\t\t\tassert.Equals(t, prov.GetID(), provisionerID)\n\t\t\t\t\t\tassert.Equals(t, \"accID\", accountID)\n\t\t\t\t\t\treturn nil, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tvr: func(t *testing.T, o *acme.Order) {\n\t\t\t\t\tnow := clock.Now()\n\t\t\t\t\ttestBufferDur := 5 * time.Second\n\t\t\t\t\torderExpiry := now.Add(defaultOrderExpiry)\n\t\t\t\t\texpNbf := now.Add(-defaultOrderBackdate)\n\t\t\t\t\texpNaf := now.Add(prov.DefaultTLSCertDuration())\n\n\t\t\t\t\tassert.Equals(t, o.ID, \"ordID\")\n\t\t\t\t\tassert.Equals(t, o.Status, acme.StatusPending)\n\t\t\t\t\tassert.Equals(t, o.Identifiers, nor.Identifiers)\n\t\t\t\t\tassert.Equals(t, o.AuthorizationURLs, []string{\n\t\t\t\t\t\tfmt.Sprintf(\"%s/acme/%s/authz/az1ID\", baseURL.String(), escProvName),\n\t\t\t\t\t\tfmt.Sprintf(\"%s/acme/%s/authz/az2ID\", baseURL.String(), escProvName),\n\t\t\t\t\t})\n\t\t\t\t\tassert.True(t, o.NotBefore.Add(-testBufferDur).Before(expNbf))\n\t\t\t\t\tassert.True(t, o.NotBefore.Add(testBufferDur).After(expNbf))\n\t\t\t\t\tassert.True(t, o.NotAfter.Add(-testBufferDur).Before(expNaf))\n\t\t\t\t\tassert.True(t, o.NotAfter.Add(testBufferDur).After(expNaf))\n\t\t\t\t\tassert.True(t, o.ExpiresAt.Add(-testBufferDur).Before(orderExpiry))\n\t\t\t\t\tassert.True(t, o.ExpiresAt.Add(testBufferDur).After(orderExpiry))\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"ok/naf-nbf\": func(t *testing.T) test {\n\t\t\tnow := clock.Now()\n\t\t\texpNbf := now.Add(5 * time.Minute)\n\t\t\texpNaf := now.Add(15 * time.Minute)\n\t\t\tacc := &acme.Account{ID: \"accID\"}\n\t\t\tnor := &NewOrderRequest{\n\t\t\t\tIdentifiers: []acme.Identifier{\n\t\t\t\t\t{Type: \"dns\", Value: \"zap.internal\"},\n\t\t\t\t},\n\t\t\t\tNotBefore: expNbf,\n\t\t\t\tNotAfter:  expNaf,\n\t\t\t}\n\t\t\tb, err := json.Marshal(nor)\n\t\t\tassert.FatalError(t, err)\n\t\t\tctx := acme.NewProvisionerContext(context.Background(), prov)\n\t\t\tctx = context.WithValue(ctx, accContextKey, acc)\n\t\t\tctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b})\n\t\t\tvar (\n\t\t\t\tch1, ch2, ch3 **acme.Challenge\n\t\t\t\taz1ID         *string\n\t\t\t\tcount         = 0\n\t\t\t)\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 201,\n\t\t\t\tnor:        nor,\n\t\t\t\tca:         &mockCA{},\n\t\t\t\tdb: &acme.MockDB{\n\t\t\t\t\tMockCreateChallenge: func(ctx context.Context, ch *acme.Challenge) error {\n\t\t\t\t\t\tswitch count {\n\t\t\t\t\t\tcase 0:\n\t\t\t\t\t\t\tch.ID = \"dns\"\n\t\t\t\t\t\t\tassert.Equals(t, ch.Type, acme.DNS01)\n\t\t\t\t\t\t\tch1 = &ch\n\t\t\t\t\t\tcase 1:\n\t\t\t\t\t\t\tch.ID = \"http\"\n\t\t\t\t\t\t\tassert.Equals(t, ch.Type, acme.HTTP01)\n\t\t\t\t\t\t\tch2 = &ch\n\t\t\t\t\t\tcase 2:\n\t\t\t\t\t\t\tch.ID = \"tls\"\n\t\t\t\t\t\t\tassert.Equals(t, ch.Type, acme.TLSALPN01)\n\t\t\t\t\t\t\tch3 = &ch\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\tassert.FatalError(t, errors.New(\"test logic error\"))\n\t\t\t\t\t\t\treturn errors.New(\"force\")\n\t\t\t\t\t\t}\n\t\t\t\t\t\tcount++\n\t\t\t\t\t\tassert.Equals(t, ch.AccountID, \"accID\")\n\t\t\t\t\t\tassert.NotEquals(t, ch.Token, \"\")\n\t\t\t\t\t\tassert.Equals(t, ch.Status, acme.StatusPending)\n\t\t\t\t\t\tassert.Equals(t, ch.Value, \"zap.internal\")\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t\tMockCreateAuthorization: func(ctx context.Context, az *acme.Authorization) error {\n\t\t\t\t\t\taz.ID = \"az1ID\"\n\t\t\t\t\t\taz1ID = &az.ID\n\t\t\t\t\t\tassert.Equals(t, az.AccountID, \"accID\")\n\t\t\t\t\t\tassert.NotEquals(t, az.Token, \"\")\n\t\t\t\t\t\tassert.Equals(t, az.Status, acme.StatusPending)\n\t\t\t\t\t\tassert.Equals(t, az.Identifier, nor.Identifiers[0])\n\t\t\t\t\t\tassert.Equals(t, az.Challenges, []*acme.Challenge{*ch1, *ch2, *ch3})\n\t\t\t\t\t\tassert.Equals(t, az.Wildcard, false)\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t\tMockCreateOrder: func(ctx context.Context, o *acme.Order) error {\n\t\t\t\t\t\to.ID = \"ordID\"\n\t\t\t\t\t\tassert.Equals(t, o.AccountID, \"accID\")\n\t\t\t\t\t\tassert.Equals(t, o.ProvisionerID, prov.GetID())\n\t\t\t\t\t\tassert.Equals(t, o.Status, acme.StatusPending)\n\t\t\t\t\t\tassert.Equals(t, o.Identifiers, nor.Identifiers)\n\t\t\t\t\t\tassert.Equals(t, o.AuthorizationIDs, []string{*az1ID})\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t\tMockGetExternalAccountKeyByAccountID: func(ctx context.Context, provisionerID, accountID string) (*acme.ExternalAccountKey, error) {\n\t\t\t\t\t\tassert.Equals(t, prov.GetID(), provisionerID)\n\t\t\t\t\t\tassert.Equals(t, \"accID\", accountID)\n\t\t\t\t\t\treturn nil, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tvr: func(t *testing.T, o *acme.Order) {\n\t\t\t\t\ttestBufferDur := 5 * time.Second\n\t\t\t\t\torderExpiry := now.Add(defaultOrderExpiry)\n\n\t\t\t\t\tassert.Equals(t, o.ID, \"ordID\")\n\t\t\t\t\tassert.Equals(t, o.Status, acme.StatusPending)\n\t\t\t\t\tassert.Equals(t, o.Identifiers, nor.Identifiers)\n\t\t\t\t\tassert.Equals(t, o.AuthorizationURLs, []string{fmt.Sprintf(\"%s/acme/%s/authz/az1ID\", baseURL.String(), escProvName)})\n\t\t\t\t\tassert.True(t, o.NotBefore.Add(-testBufferDur).Before(expNbf))\n\t\t\t\t\tassert.True(t, o.NotBefore.Add(testBufferDur).After(expNbf))\n\t\t\t\t\tassert.True(t, o.NotAfter.Add(-testBufferDur).Before(expNaf))\n\t\t\t\t\tassert.True(t, o.NotAfter.Add(testBufferDur).After(expNaf))\n\t\t\t\t\tassert.True(t, o.ExpiresAt.Add(-testBufferDur).Before(orderExpiry))\n\t\t\t\t\tassert.True(t, o.ExpiresAt.Add(testBufferDur).After(orderExpiry))\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"ok/default-naf-nbf-with-policy\": func(t *testing.T) test {\n\t\t\toptions := &provisioner.Options{\n\t\t\t\tX509: &provisioner.X509Options{\n\t\t\t\t\tAllowedNames: &policy.X509NameOptions{\n\t\t\t\t\t\tDNSDomains: []string{\"*.internal\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t\tprovWithPolicy := newACMEProvWithOptions(t, options)\n\t\t\tprovWithPolicy.RequireEAB = true\n\t\t\tacc := &acme.Account{ID: \"accID\"}\n\t\t\tnor := &NewOrderRequest{\n\t\t\t\tIdentifiers: []acme.Identifier{\n\t\t\t\t\t{Type: \"dns\", Value: \"zap.internal\"},\n\t\t\t\t},\n\t\t\t}\n\t\t\tb, err := json.Marshal(nor)\n\t\t\tassert.FatalError(t, err)\n\t\t\tctx := acme.NewProvisionerContext(context.Background(), provWithPolicy)\n\t\t\tctx = context.WithValue(ctx, accContextKey, acc)\n\t\t\tctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b})\n\t\t\tvar (\n\t\t\t\tch1, ch2, ch3 **acme.Challenge\n\t\t\t\taz1ID         *string\n\t\t\t\tcount         = 0\n\t\t\t)\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 201,\n\t\t\t\tnor:        nor,\n\t\t\t\tca:         &mockCA{},\n\t\t\t\tdb: &acme.MockDB{\n\t\t\t\t\tMockCreateChallenge: func(ctx context.Context, ch *acme.Challenge) error {\n\t\t\t\t\t\tswitch count {\n\t\t\t\t\t\tcase 0:\n\t\t\t\t\t\t\tch.ID = \"dns\"\n\t\t\t\t\t\t\tassert.Equals(t, ch.Type, acme.DNS01)\n\t\t\t\t\t\t\tch1 = &ch\n\t\t\t\t\t\tcase 1:\n\t\t\t\t\t\t\tch.ID = \"http\"\n\t\t\t\t\t\t\tassert.Equals(t, ch.Type, acme.HTTP01)\n\t\t\t\t\t\t\tch2 = &ch\n\t\t\t\t\t\tcase 2:\n\t\t\t\t\t\t\tch.ID = \"tls\"\n\t\t\t\t\t\t\tassert.Equals(t, ch.Type, acme.TLSALPN01)\n\t\t\t\t\t\t\tch3 = &ch\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\tassert.FatalError(t, errors.New(\"test logic error\"))\n\t\t\t\t\t\t\treturn errors.New(\"force\")\n\t\t\t\t\t\t}\n\t\t\t\t\t\tcount++\n\t\t\t\t\t\tassert.Equals(t, ch.AccountID, \"accID\")\n\t\t\t\t\t\tassert.NotEquals(t, ch.Token, \"\")\n\t\t\t\t\t\tassert.Equals(t, ch.Status, acme.StatusPending)\n\t\t\t\t\t\tassert.Equals(t, ch.Value, \"zap.internal\")\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t\tMockCreateAuthorization: func(ctx context.Context, az *acme.Authorization) error {\n\t\t\t\t\t\taz.ID = \"az1ID\"\n\t\t\t\t\t\taz1ID = &az.ID\n\t\t\t\t\t\tassert.Equals(t, az.AccountID, \"accID\")\n\t\t\t\t\t\tassert.NotEquals(t, az.Token, \"\")\n\t\t\t\t\t\tassert.Equals(t, az.Status, acme.StatusPending)\n\t\t\t\t\t\tassert.Equals(t, az.Identifier, nor.Identifiers[0])\n\t\t\t\t\t\tassert.Equals(t, az.Challenges, []*acme.Challenge{*ch1, *ch2, *ch3})\n\t\t\t\t\t\tassert.Equals(t, az.Wildcard, false)\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t\tMockCreateOrder: func(ctx context.Context, o *acme.Order) error {\n\t\t\t\t\t\to.ID = \"ordID\"\n\t\t\t\t\t\tassert.Equals(t, o.AccountID, \"accID\")\n\t\t\t\t\t\tassert.Equals(t, o.ProvisionerID, prov.GetID())\n\t\t\t\t\t\tassert.Equals(t, o.Status, acme.StatusPending)\n\t\t\t\t\t\tassert.Equals(t, o.Identifiers, nor.Identifiers)\n\t\t\t\t\t\tassert.Equals(t, o.AuthorizationIDs, []string{*az1ID})\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t\tMockGetExternalAccountKeyByAccountID: func(ctx context.Context, provisionerID, accountID string) (*acme.ExternalAccountKey, error) {\n\t\t\t\t\t\tassert.Equals(t, prov.GetID(), provisionerID)\n\t\t\t\t\t\tassert.Equals(t, \"accID\", accountID)\n\t\t\t\t\t\treturn nil, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tvr: func(t *testing.T, o *acme.Order) {\n\t\t\t\t\tnow := clock.Now()\n\t\t\t\t\ttestBufferDur := 5 * time.Second\n\t\t\t\t\torderExpiry := now.Add(defaultOrderExpiry)\n\t\t\t\t\texpNbf := now.Add(-defaultOrderBackdate)\n\t\t\t\t\texpNaf := now.Add(prov.DefaultTLSCertDuration())\n\n\t\t\t\t\tassert.Equals(t, o.ID, \"ordID\")\n\t\t\t\t\tassert.Equals(t, o.Status, acme.StatusPending)\n\t\t\t\t\tassert.Equals(t, o.Identifiers, nor.Identifiers)\n\t\t\t\t\tassert.Equals(t, o.AuthorizationURLs, []string{fmt.Sprintf(\"%s/acme/%s/authz/az1ID\", baseURL.String(), escProvName)})\n\t\t\t\t\tassert.True(t, o.NotBefore.Add(-testBufferDur).Before(expNbf))\n\t\t\t\t\tassert.True(t, o.NotBefore.Add(testBufferDur).After(expNbf))\n\t\t\t\t\tassert.True(t, o.NotAfter.Add(-testBufferDur).Before(expNaf))\n\t\t\t\t\tassert.True(t, o.NotAfter.Add(testBufferDur).After(expNaf))\n\t\t\t\t\tassert.True(t, o.ExpiresAt.Add(-testBufferDur).Before(orderExpiry))\n\t\t\t\t\tassert.True(t, o.ExpiresAt.Add(testBufferDur).After(orderExpiry))\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\ttc := run(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tmockMustAuthority(t, tc.ca)\n\t\t\tctx := newBaseContext(tc.ctx, tc.db, acme.NewLinker(\"test.ca.smallstep.com\", \"acme\"))\n\t\t\treq := httptest.NewRequest(\"GET\", u, http.NoBody)\n\t\t\treq = req.WithContext(ctx)\n\t\t\tw := httptest.NewRecorder()\n\t\t\tNewOrder(w, req)\n\t\t\tres := w.Result()\n\n\t\t\tassert.Equals(t, res.StatusCode, tc.statusCode)\n\n\t\t\tbody, err := io.ReadAll(res.Body)\n\t\t\tres.Body.Close()\n\t\t\tassert.FatalError(t, err)\n\n\t\t\tif res.StatusCode >= 400 && assert.NotNil(t, tc.err) {\n\t\t\t\tvar ae acme.Error\n\t\t\t\tassert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &ae))\n\n\t\t\t\tassert.Equals(t, ae.Type, tc.err.Type)\n\t\t\t\tassert.Equals(t, ae.Detail, tc.err.Detail)\n\t\t\t\tassert.Equals(t, ae.Subproblems, tc.err.Subproblems)\n\t\t\t\tassert.Equals(t, res.Header[\"Content-Type\"], []string{\"application/problem+json\"})\n\t\t\t} else {\n\t\t\t\tro := new(acme.Order)\n\t\t\t\tassert.FatalError(t, json.Unmarshal(body, ro))\n\t\t\t\tif tc.vr != nil {\n\t\t\t\t\ttc.vr(t, ro)\n\t\t\t\t}\n\n\t\t\t\tassert.Equals(t, res.Header[\"Location\"], []string{u})\n\t\t\t\tassert.Equals(t, res.Header[\"Content-Type\"], []string{\"application/json\"})\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestHandler_FinalizeOrder(t *testing.T) {\n\tmockMustAuthority(t, &mockCA{})\n\tprov := newProv()\n\tescProvName := url.PathEscape(prov.GetName())\n\tbaseURL := &url.URL{Scheme: \"https\", Host: \"test.ca.smallstep.com\"}\n\n\tnow := clock.Now()\n\tnbf := now\n\tnaf := now.Add(24 * time.Hour)\n\to := acme.Order{\n\t\tID:        \"orderID\",\n\t\tNotBefore: nbf,\n\t\tNotAfter:  naf,\n\t\tIdentifiers: []acme.Identifier{\n\t\t\t{\n\t\t\t\tType:  \"dns\",\n\t\t\t\tValue: \"example.com\",\n\t\t\t},\n\t\t\t{\n\t\t\t\tType:  \"dns\",\n\t\t\t\tValue: \"*.smallstep.com\",\n\t\t\t},\n\t\t},\n\t\tExpiresAt: naf,\n\t\tStatus:    acme.StatusValid,\n\t\tAuthorizationURLs: []string{\n\t\t\tfmt.Sprintf(\"%s/acme/%s/authz/foo\", baseURL.String(), escProvName),\n\t\t\tfmt.Sprintf(\"%s/acme/%s/authz/bar\", baseURL.String(), escProvName),\n\t\t\tfmt.Sprintf(\"%s/acme/%s/authz/baz\", baseURL.String(), escProvName),\n\t\t},\n\t\tFinalizeURL:    fmt.Sprintf(\"%s/acme/%s/order/orderID/finalize\", baseURL.String(), escProvName),\n\t\tCertificateURL: fmt.Sprintf(\"%s/acme/%s/certificate/certID\", baseURL.String(), escProvName),\n\t}\n\n\t// Request with chi context\n\tchiCtx := chi.NewRouteContext()\n\tchiCtx.URLParams.Add(\"ordID\", o.ID)\n\tu := fmt.Sprintf(\"%s/acme/%s/order/%s\",\n\t\tbaseURL.String(), escProvName, o.ID)\n\n\t_csr, err := pemutil.Read(\"../../authority/testdata/certs/foo.csr\")\n\tassert.FatalError(t, err)\n\tcsr, ok := _csr.(*x509.CertificateRequest)\n\tassert.Fatal(t, ok)\n\n\tnor := &FinalizeRequest{\n\t\tCSR: base64.RawURLEncoding.EncodeToString(csr.Raw),\n\t}\n\tpayloadBytes, err := json.Marshal(nor)\n\tassert.FatalError(t, err)\n\n\ttype test struct {\n\t\tdb         acme.DB\n\t\tctx        context.Context\n\t\tstatusCode int\n\t\terr        *acme.Error\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/no-account\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tdb:         &acme.MockDB{},\n\t\t\t\tctx:        acme.NewProvisionerContext(context.Background(), prov),\n\t\t\t\tstatusCode: 400,\n\t\t\t\terr:        acme.NewError(acme.ErrorAccountDoesNotExistType, \"account does not exist\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/nil-account\": func(t *testing.T) test {\n\t\t\tctx := acme.NewProvisionerContext(context.Background(), prov)\n\t\t\tctx = context.WithValue(ctx, accContextKey, nil)\n\t\t\treturn test{\n\t\t\t\tdb:         &acme.MockDB{},\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 400,\n\t\t\t\terr:        acme.NewError(acme.ErrorAccountDoesNotExistType, \"account does not exist\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/no-provisioner\": func(t *testing.T) test {\n\t\t\tacc := &acme.Account{ID: \"accountID\"}\n\t\t\tctx := context.WithValue(context.Background(), accContextKey, acc)\n\t\t\treturn test{\n\t\t\t\tdb:         &acme.MockDB{},\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 500,\n\t\t\t\terr:        acme.NewErrorISE(\"provisioner does not exist\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/nil-provisioner\": func(t *testing.T) test {\n\t\t\tacc := &acme.Account{ID: \"accountID\"}\n\t\t\tctx := acme.NewProvisionerContext(context.Background(), prov)\n\t\t\tctx = context.WithValue(ctx, accContextKey, acc)\n\t\t\treturn test{\n\t\t\t\tdb:         &acme.MockDB{},\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 500,\n\t\t\t\terr:        acme.NewErrorISE(\"provisioner does not exist\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/no-payload\": func(t *testing.T) test {\n\t\t\tacc := &acme.Account{ID: \"accountID\"}\n\t\t\tctx := context.WithValue(context.Background(), accContextKey, acc)\n\t\t\tctx = acme.NewProvisionerContext(ctx, prov)\n\t\t\treturn test{\n\t\t\t\tdb:         &acme.MockDB{},\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 500,\n\t\t\t\terr:        acme.NewErrorISE(\"payload does not exist\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/nil-payload\": func(t *testing.T) test {\n\t\t\tacc := &acme.Account{ID: \"accountID\"}\n\t\t\tctx := acme.NewProvisionerContext(context.Background(), prov)\n\t\t\tctx = context.WithValue(ctx, accContextKey, acc)\n\t\t\tctx = context.WithValue(ctx, payloadContextKey, nil)\n\t\t\treturn test{\n\t\t\t\tdb:         &acme.MockDB{},\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 500,\n\t\t\t\terr:        acme.NewErrorISE(\"payload does not exist\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/unmarshal-payload-error\": func(t *testing.T) test {\n\t\t\tacc := &acme.Account{ID: \"accID\"}\n\t\t\tctx := acme.NewProvisionerContext(context.Background(), prov)\n\t\t\tctx = context.WithValue(ctx, accContextKey, acc)\n\t\t\tctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{})\n\t\t\treturn test{\n\t\t\t\tdb:         &acme.MockDB{},\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 400,\n\t\t\t\terr:        acme.NewError(acme.ErrorMalformedType, \"failed to unmarshal finalize-order request payload: unexpected end of JSON input\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/malformed-payload-error\": func(t *testing.T) test {\n\t\t\tacc := &acme.Account{ID: \"accID\"}\n\t\t\tfr := &FinalizeRequest{}\n\t\t\tb, err := json.Marshal(fr)\n\t\t\tassert.FatalError(t, err)\n\t\t\tctx := acme.NewProvisionerContext(context.Background(), prov)\n\t\t\tctx = context.WithValue(ctx, accContextKey, acc)\n\t\t\tctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b})\n\t\t\treturn test{\n\t\t\t\tdb:         &acme.MockDB{},\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 400,\n\t\t\t\terr:        acme.NewError(acme.ErrorMalformedType, \"unable to parse csr: asn1: syntax error: sequence truncated\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/db.GetOrder-error\": func(t *testing.T) test {\n\n\t\t\tacc := &acme.Account{ID: \"accountID\"}\n\t\t\tctx := acme.NewProvisionerContext(context.Background(), prov)\n\t\t\tctx = context.WithValue(ctx, accContextKey, acc)\n\t\t\tctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: payloadBytes})\n\t\t\tctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx)\n\t\t\treturn test{\n\t\t\t\tdb: &acme.MockDB{\n\t\t\t\t\tMockError: acme.NewErrorISE(\"force\"),\n\t\t\t\t},\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 500,\n\t\t\t\terr:        acme.NewErrorISE(\"force\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/account-id-mismatch\": func(t *testing.T) test {\n\t\t\tacc := &acme.Account{ID: \"accountID\"}\n\t\t\tctx := acme.NewProvisionerContext(context.Background(), prov)\n\t\t\tctx = context.WithValue(ctx, accContextKey, acc)\n\t\t\tctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: payloadBytes})\n\t\t\tctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx)\n\t\t\treturn test{\n\t\t\t\tdb: &acme.MockDB{\n\t\t\t\t\tMockGetOrder: func(ctx context.Context, id string) (*acme.Order, error) {\n\t\t\t\t\t\treturn &acme.Order{AccountID: \"foo\"}, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 401,\n\t\t\t\terr:        acme.NewError(acme.ErrorUnauthorizedType, \"account id mismatch\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/provisioner-id-mismatch\": func(t *testing.T) test {\n\t\t\tacc := &acme.Account{ID: \"accountID\"}\n\t\t\tctx := acme.NewProvisionerContext(context.Background(), prov)\n\t\t\tctx = context.WithValue(ctx, accContextKey, acc)\n\t\t\tctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: payloadBytes})\n\t\t\tctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx)\n\t\t\treturn test{\n\t\t\t\tdb: &acme.MockDB{\n\t\t\t\t\tMockGetOrder: func(ctx context.Context, id string) (*acme.Order, error) {\n\t\t\t\t\t\treturn &acme.Order{AccountID: \"accountID\", ProvisionerID: \"bar\"}, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 401,\n\t\t\t\terr:        acme.NewError(acme.ErrorUnauthorizedType, \"provisioner id mismatch\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/order-finalize-error\": func(t *testing.T) test {\n\t\t\tacc := &acme.Account{ID: \"accountID\"}\n\t\t\tctx := acme.NewProvisionerContext(context.Background(), prov)\n\t\t\tctx = context.WithValue(ctx, accContextKey, acc)\n\t\t\tctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: payloadBytes})\n\t\t\tctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx)\n\t\t\treturn test{\n\t\t\t\tdb: &acme.MockDB{\n\t\t\t\t\tMockGetOrder: func(ctx context.Context, id string) (*acme.Order, error) {\n\t\t\t\t\t\treturn &acme.Order{\n\t\t\t\t\t\t\tAccountID:     \"accountID\",\n\t\t\t\t\t\t\tProvisionerID: fmt.Sprintf(\"acme/%s\", prov.GetName()),\n\t\t\t\t\t\t\tExpiresAt:     clock.Now().Add(-time.Hour),\n\t\t\t\t\t\t\tStatus:        acme.StatusReady,\n\t\t\t\t\t\t}, nil\n\t\t\t\t\t},\n\t\t\t\t\tMockUpdateOrder: func(ctx context.Context, o *acme.Order) error {\n\t\t\t\t\t\treturn acme.NewErrorISE(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 500,\n\t\t\t\terr:        acme.NewErrorISE(\"force\"),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tacc := &acme.Account{ID: \"accountID\"}\n\t\t\tctx := acme.NewProvisionerContext(context.Background(), prov)\n\t\t\tctx = context.WithValue(ctx, accContextKey, acc)\n\t\t\tctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: payloadBytes})\n\t\t\tctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx)\n\t\t\treturn test{\n\t\t\t\tdb: &acme.MockDB{\n\t\t\t\t\tMockGetOrder: func(ctx context.Context, id string) (*acme.Order, error) {\n\t\t\t\t\t\treturn &acme.Order{\n\t\t\t\t\t\t\tID:               \"orderID\",\n\t\t\t\t\t\t\tAccountID:        \"accountID\",\n\t\t\t\t\t\t\tProvisionerID:    fmt.Sprintf(\"acme/%s\", prov.GetName()),\n\t\t\t\t\t\t\tExpiresAt:        naf,\n\t\t\t\t\t\t\tStatus:           acme.StatusValid,\n\t\t\t\t\t\t\tAuthorizationIDs: []string{\"foo\", \"bar\", \"baz\"},\n\t\t\t\t\t\t\tNotBefore:        nbf,\n\t\t\t\t\t\t\tNotAfter:         naf,\n\t\t\t\t\t\t\tIdentifiers: []acme.Identifier{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tType:  \"dns\",\n\t\t\t\t\t\t\t\t\tValue: \"example.com\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tType:  \"dns\",\n\t\t\t\t\t\t\t\t\tValue: \"*.smallstep.com\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tCertificateID: \"certID\",\n\t\t\t\t\t\t}, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 200,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\ttc := run(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tctx := newBaseContext(tc.ctx, tc.db, acme.NewLinker(\"test.ca.smallstep.com\", \"acme\"))\n\t\t\treq := httptest.NewRequest(\"GET\", u, http.NoBody)\n\t\t\treq = req.WithContext(ctx)\n\t\t\tw := httptest.NewRecorder()\n\t\t\tFinalizeOrder(w, req)\n\t\t\tres := w.Result()\n\n\t\t\tassert.Equals(t, res.StatusCode, tc.statusCode)\n\n\t\t\tbody, err := io.ReadAll(res.Body)\n\t\t\tres.Body.Close()\n\t\t\tassert.FatalError(t, err)\n\n\t\t\tif res.StatusCode >= 400 && assert.NotNil(t, tc.err) {\n\t\t\t\tvar ae acme.Error\n\t\t\t\tassert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &ae))\n\n\t\t\t\tassert.Equals(t, ae.Type, tc.err.Type)\n\t\t\t\tassert.Equals(t, ae.Detail, tc.err.Detail)\n\t\t\t\tassert.Equals(t, ae.Subproblems, tc.err.Subproblems)\n\t\t\t\tassert.Equals(t, res.Header[\"Content-Type\"], []string{\"application/problem+json\"})\n\t\t\t} else {\n\t\t\t\texpB, err := json.Marshal(o)\n\t\t\t\tassert.FatalError(t, err)\n\n\t\t\t\tro := new(acme.Order)\n\t\t\t\tassert.FatalError(t, json.Unmarshal(body, ro))\n\n\t\t\t\tassert.Equals(t, bytes.TrimSpace(body), expB)\n\t\t\t\tassert.Equals(t, res.Header[\"Location\"], []string{u})\n\t\t\t\tassert.Equals(t, res.Header[\"Content-Type\"], []string{\"application/json\"})\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestHandler_challengeTypes(t *testing.T) {\n\ttype args struct {\n\t\taz *acme.Authorization\n\t}\n\ttests := []struct {\n\t\tname string\n\t\targs args\n\t\twant []acme.ChallengeType\n\t}{\n\t\t{\n\t\t\tname: \"ok/dns\",\n\t\t\targs: args{\n\t\t\t\taz: &acme.Authorization{\n\t\t\t\t\tIdentifier: acme.Identifier{Type: \"dns\", Value: \"example.com\"},\n\t\t\t\t\tWildcard:   false,\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: []acme.ChallengeType{acme.DNS01, acme.HTTP01, acme.TLSALPN01},\n\t\t},\n\t\t{\n\t\t\tname: \"ok/wildcard\",\n\t\t\targs: args{\n\t\t\t\taz: &acme.Authorization{\n\t\t\t\t\tIdentifier: acme.Identifier{Type: \"dns\", Value: \"*.example.com\"},\n\t\t\t\t\tWildcard:   true,\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: []acme.ChallengeType{acme.DNS01},\n\t\t},\n\t\t{\n\t\t\tname: \"ok/ip\",\n\t\t\targs: args{\n\t\t\t\taz: &acme.Authorization{\n\t\t\t\t\tIdentifier: acme.Identifier{Type: \"ip\", Value: \"192.168.42.42\"},\n\t\t\t\t\tWildcard:   false,\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: []acme.ChallengeType{acme.HTTP01, acme.TLSALPN01},\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif got := challengeTypes(tt.args.az); !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"Handler.challengeTypes() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestTrimIfWildcard(t *testing.T) {\n\ttests := []struct {\n\t\tname      string\n\t\targ       string\n\t\twantValue string\n\t\twantBool  bool\n\t}{\n\t\t{\n\t\t\tname:      \"no trim\",\n\t\t\targ:       \"smallstep.com\",\n\t\t\twantValue: \"smallstep.com\",\n\t\t\twantBool:  false,\n\t\t},\n\t\t{\n\t\t\tname:      \"trim\",\n\t\t\targ:       \"*.smallstep.com\",\n\t\t\twantValue: \"smallstep.com\",\n\t\t\twantBool:  true,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tv, ok := trimIfWildcard(tt.arg)\n\t\t\tassert.Equals(t, v, tt.wantValue)\n\t\t\tassert.Equals(t, ok, tt.wantBool)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "acme/api/revoke.go",
    "content": "package api\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/x509\"\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strings\"\n\n\t\"go.step.sm/crypto/jose\"\n\t\"golang.org/x/crypto/ocsp\"\n\n\t\"github.com/smallstep/certificates/acme\"\n\t\"github.com/smallstep/certificates/api/render\"\n\t\"github.com/smallstep/certificates/authority\"\n\t\"github.com/smallstep/certificates/authority/provisioner\"\n\t\"github.com/smallstep/certificates/logging\"\n)\n\ntype revokePayload struct {\n\tCertificate string `json:\"certificate\"`\n\tReasonCode  *int   `json:\"reason,omitempty\"`\n}\n\n// RevokeCert attempts to revoke a certificate.\nfunc RevokeCert(w http.ResponseWriter, r *http.Request) {\n\tctx := r.Context()\n\tdb := acme.MustDatabaseFromContext(ctx)\n\tlinker := acme.MustLinkerFromContext(ctx)\n\n\tjws, err := jwsFromContext(ctx)\n\tif err != nil {\n\t\trender.Error(w, r, err)\n\t\treturn\n\t}\n\n\tprov, err := provisionerFromContext(ctx)\n\tif err != nil {\n\t\trender.Error(w, r, err)\n\t\treturn\n\t}\n\n\tpayload, err := payloadFromContext(ctx)\n\tif err != nil {\n\t\trender.Error(w, r, err)\n\t\treturn\n\t}\n\n\tvar p revokePayload\n\terr = json.Unmarshal(payload.value, &p)\n\tif err != nil {\n\t\trender.Error(w, r, acme.WrapErrorISE(err, \"error unmarshaling payload\"))\n\t\treturn\n\t}\n\n\tcertBytes, err := base64.RawURLEncoding.DecodeString(p.Certificate)\n\tif err != nil {\n\t\t// in this case the most likely cause is a client that didn't properly encode the certificate\n\t\trender.Error(w, r, acme.WrapError(acme.ErrorMalformedType, err, \"error base64url decoding payload certificate property\"))\n\t\treturn\n\t}\n\n\tcertToBeRevoked, err := x509.ParseCertificate(certBytes)\n\tif err != nil {\n\t\t// in this case a client may have encoded something different than a certificate\n\t\trender.Error(w, r, acme.WrapError(acme.ErrorMalformedType, err, \"error parsing certificate\"))\n\t\treturn\n\t}\n\n\tserial := certToBeRevoked.SerialNumber.String()\n\tdbCert, err := db.GetCertificateBySerial(ctx, serial)\n\tif err != nil {\n\t\trender.Error(w, r, acme.WrapErrorISE(err, \"error retrieving certificate by serial\"))\n\t\treturn\n\t}\n\n\tif !bytes.Equal(dbCert.Leaf.Raw, certToBeRevoked.Raw) {\n\t\t// this should never happen\n\t\trender.Error(w, r, acme.NewErrorISE(\"certificate raw bytes are not equal\"))\n\t\treturn\n\t}\n\n\tif shouldCheckAccountFrom(jws) {\n\t\taccount, err := accountFromContext(ctx)\n\t\tif err != nil {\n\t\t\trender.Error(w, r, err)\n\t\t\treturn\n\t\t}\n\t\tacmeErr := isAccountAuthorized(ctx, dbCert, certToBeRevoked, account)\n\t\tif acmeErr != nil {\n\t\t\trender.Error(w, r, acmeErr)\n\t\t\treturn\n\t\t}\n\t} else {\n\t\t// if account doesn't need to be checked, the JWS should be verified to be signed by the\n\t\t// private key that belongs to the public key in the certificate to be revoked.\n\t\t_, err := jws.Verify(certToBeRevoked.PublicKey)\n\t\tif err != nil {\n\t\t\t// TODO(hs): possible to determine an error vs. unauthorized and thus provide an ISE vs. Unauthorized?\n\t\t\trender.Error(w, r, wrapUnauthorizedError(certToBeRevoked, nil, \"verification of jws using certificate public key failed\", err))\n\t\t\treturn\n\t\t}\n\t}\n\n\tca := mustAuthority(ctx)\n\thasBeenRevokedBefore, err := ca.IsRevoked(serial)\n\tif err != nil {\n\t\trender.Error(w, r, acme.WrapErrorISE(err, \"error retrieving revocation status of certificate\"))\n\t\treturn\n\t}\n\n\tif hasBeenRevokedBefore {\n\t\trender.Error(w, r, acme.NewError(acme.ErrorAlreadyRevokedType, \"certificate was already revoked\"))\n\t\treturn\n\t}\n\n\treasonCode := p.ReasonCode\n\tacmeErr := validateReasonCode(reasonCode)\n\tif acmeErr != nil {\n\t\trender.Error(w, r, acmeErr)\n\t\treturn\n\t}\n\n\t// Authorize revocation by ACME provisioner\n\tctx = provisioner.NewContextWithMethod(ctx, provisioner.RevokeMethod)\n\terr = prov.AuthorizeRevoke(ctx, \"\")\n\tif err != nil {\n\t\trender.Error(w, r, acme.WrapErrorISE(err, \"error authorizing revocation on provisioner\"))\n\t\treturn\n\t}\n\n\toptions := revokeOptions(serial, certToBeRevoked, reasonCode)\n\terr = ca.Revoke(ctx, options)\n\tif err != nil {\n\t\trender.Error(w, r, wrapRevokeErr(err))\n\t\treturn\n\t}\n\n\tlogRevoke(w, options)\n\tw.Header().Add(\"Link\", link(linker.GetLink(ctx, acme.DirectoryLinkType), \"index\"))\n\tw.Write(nil)\n}\n\n// isAccountAuthorized checks if an ACME account that was retrieved earlier is authorized\n// to revoke the certificate. An Account must always be valid in order to revoke a certificate.\n// In case the certificate retrieved from the database belongs to the Account, the Account is\n// authorized. If the certificate retrieved from the database doesn't belong to the Account,\n// the identifiers in the certificate are extracted and compared against the (valid) Authorizations\n// that are stored for the ACME Account. If these sets match, the Account is considered authorized\n// to revoke the certificate. If this check fails, the client will receive an unauthorized error.\nfunc isAccountAuthorized(_ context.Context, dbCert *acme.Certificate, certToBeRevoked *x509.Certificate, account *acme.Account) *acme.Error {\n\tif !account.IsValid() {\n\t\treturn wrapUnauthorizedError(certToBeRevoked, nil, fmt.Sprintf(\"account '%s' has status '%s'\", account.ID, account.Status), nil)\n\t}\n\tcertificateBelongsToAccount := dbCert.AccountID == account.ID\n\tif certificateBelongsToAccount {\n\t\treturn nil // return early\n\t}\n\n\t// TODO(hs): according to RFC8555: 7.6, a server MUST consider the following accounts authorized\n\t// to revoke a certificate:\n\t//\n\t//\to  the account that issued the certificate.\n\t//\to  an account that holds authorizations for all of the identifiers in the certificate.\n\t//\n\t// We currently only support the first case. The second might result in step going OOM when\n\t// large numbers of Authorizations are involved when the current nosql interface is in use.\n\t// We want to protect users from this failure scenario, so that's why it hasn't been added yet.\n\t// This issue is tracked in https://github.com/smallstep/certificates/issues/767\n\n\t// not authorized; fail closed.\n\treturn wrapUnauthorizedError(certToBeRevoked, nil, fmt.Sprintf(\"account '%s' is not authorized\", account.ID), nil)\n}\n\n// wrapRevokeErr is a best effort implementation to transform an error during\n// revocation into an ACME error, so that clients can understand the error.\nfunc wrapRevokeErr(err error) *acme.Error {\n\tt := err.Error()\n\tif strings.Contains(t, \"is already revoked\") {\n\t\treturn acme.NewError(acme.ErrorAlreadyRevokedType, \"%s\", t)\n\t}\n\treturn acme.WrapErrorISE(err, \"error when revoking certificate\")\n}\n\n// unauthorizedError returns an ACME error indicating the request was\n// not authorized to revoke the certificate.\nfunc wrapUnauthorizedError(cert *x509.Certificate, unauthorizedIdentifiers []acme.Identifier, msg string, err error) *acme.Error {\n\tvar acmeErr *acme.Error\n\tif err == nil {\n\t\tacmeErr = acme.NewError(acme.ErrorUnauthorizedType, \"%s\", msg)\n\t} else {\n\t\tacmeErr = acme.WrapError(acme.ErrorUnauthorizedType, err, \"%s\", msg)\n\t}\n\tacmeErr.Status = http.StatusForbidden // RFC8555 7.6 shows example with 403\n\n\tswitch {\n\tcase len(unauthorizedIdentifiers) > 0:\n\t\tidentifier := unauthorizedIdentifiers[0] // picking the first; compound may be an option too?\n\t\tacmeErr.Detail = fmt.Sprintf(\"No authorization provided for name %s\", identifier.Value)\n\tcase cert.Subject.String() != \"\":\n\t\tacmeErr.Detail = fmt.Sprintf(\"No authorization provided for name %s\", cert.Subject.CommonName)\n\tdefault:\n\t\tacmeErr.Detail = \"No authorization provided\"\n\t}\n\n\treturn acmeErr\n}\n\n// logRevoke logs successful revocation of certificate\nfunc logRevoke(w http.ResponseWriter, ri *authority.RevokeOptions) {\n\tif rl, ok := w.(logging.ResponseLogger); ok {\n\t\trl.WithFields(map[string]interface{}{\n\t\t\t\"serial\":      ri.Serial,\n\t\t\t\"reasonCode\":  ri.ReasonCode,\n\t\t\t\"reason\":      ri.Reason,\n\t\t\t\"passiveOnly\": ri.PassiveOnly,\n\t\t\t\"ACME\":        ri.ACME,\n\t\t})\n\t}\n}\n\n// validateReasonCode validates the revocation reason\nfunc validateReasonCode(reasonCode *int) *acme.Error {\n\tif reasonCode != nil && ((*reasonCode < ocsp.Unspecified || *reasonCode > ocsp.AACompromise) || *reasonCode == 7) {\n\t\treturn acme.NewError(acme.ErrorBadRevocationReasonType, \"reasonCode out of bounds\")\n\t}\n\t// NOTE: it's possible to add additional requirements to the reason code:\n\t//\t\tThe server MAY disallow a subset of reasonCodes from being\n\t//\t\tused by the user. If a request contains a disallowed reasonCode,\n\t//\t\tthen the server MUST reject it with the error type\n\t//\t\t\"urn:ietf:params:acme:error:badRevocationReason\"\n\t// No additional checks have been implemented so far.\n\treturn nil\n}\n\n// revokeOptions determines the RevokeOptions for the Authority to use in revocation\nfunc revokeOptions(serial string, certToBeRevoked *x509.Certificate, reasonCode *int) *authority.RevokeOptions {\n\topts := &authority.RevokeOptions{\n\t\tSerial: serial,\n\t\tACME:   true,\n\t\tCrt:    certToBeRevoked,\n\t}\n\tif reasonCode != nil { // NOTE: when implementing CRL and/or OCSP, and reason code is missing, CRL entry extension should be omitted\n\t\topts.Reason = reason(*reasonCode)\n\t\topts.ReasonCode = *reasonCode\n\t}\n\treturn opts\n}\n\n// reason transforms an integer reason code to a\n// textual description of the revocation reason.\nfunc reason(reasonCode int) string {\n\tswitch reasonCode {\n\tcase ocsp.Unspecified:\n\t\treturn \"unspecified reason\"\n\tcase ocsp.KeyCompromise:\n\t\treturn \"key compromised\"\n\tcase ocsp.CACompromise:\n\t\treturn \"ca compromised\"\n\tcase ocsp.AffiliationChanged:\n\t\treturn \"affiliation changed\"\n\tcase ocsp.Superseded:\n\t\treturn \"superseded\"\n\tcase ocsp.CessationOfOperation:\n\t\treturn \"cessation of operation\"\n\tcase ocsp.CertificateHold:\n\t\treturn \"certificate hold\"\n\tcase ocsp.RemoveFromCRL:\n\t\treturn \"remove from crl\"\n\tcase ocsp.PrivilegeWithdrawn:\n\t\treturn \"privilege withdrawn\"\n\tcase ocsp.AACompromise:\n\t\treturn \"aa compromised\"\n\tdefault:\n\t\treturn \"unspecified reason\"\n\t}\n}\n\n// shouldCheckAccountFrom indicates whether an account should be\n// retrieved from the context, so that it can be used for\n// additional checks. This should only be done when no JWK\n// can be extracted from the request, as that would indicate\n// that the revocation request was signed with a certificate\n// key pair (and not an account key pair). Looking up such\n// a JWK would result in no Account being found.\nfunc shouldCheckAccountFrom(jws *jose.JSONWebSignature) bool {\n\treturn !canExtractJWKFrom(jws)\n}\n"
  },
  {
    "path": "acme/api/revoke_test.go",
    "content": "package api\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto\"\n\t\"crypto/ecdsa\"\n\t\"crypto/rand\"\n\t\"crypto/rsa\"\n\t\"crypto/x509\"\n\t\"crypto/x509/pkix\"\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"math/big\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-chi/chi/v5\"\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/pkg/errors\"\n\t\"golang.org/x/crypto/ocsp\"\n\n\t\"go.step.sm/crypto/jose\"\n\t\"go.step.sm/crypto/keyutil\"\n\t\"go.step.sm/crypto/x509util\"\n\n\t\"github.com/smallstep/assert\"\n\t\"github.com/smallstep/certificates/acme\"\n\t\"github.com/smallstep/certificates/authority\"\n\t\"github.com/smallstep/certificates/authority/provisioner\"\n)\n\n// v is a utility function to return the pointer to an integer\nfunc v(v int) *int {\n\treturn &v\n}\n\nfunc generateSerial() (*big.Int, error) {\n\treturn rand.Int(rand.Reader, big.NewInt(1000000000000000000))\n}\n\n// generateCertKeyPair generates fresh x509 certificate/key pairs for testing\nfunc generateCertKeyPair() (*x509.Certificate, crypto.Signer, error) {\n\n\tpub, priv, err := keyutil.GenerateKeyPair(\"EC\", \"P-256\", 0)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\n\tserial, err := generateSerial()\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\n\tnow := time.Now()\n\ttemplate := &x509.Certificate{\n\t\tSubject:      pkix.Name{CommonName: \"127.0.0.1\"},\n\t\tIssuer:       pkix.Name{CommonName: \"Test ACME Revoke Certificate\"},\n\t\tIPAddresses:  []net.IP{net.ParseIP(\"127.0.0.1\")},\n\t\tIsCA:         false,\n\t\tMaxPathLen:   0,\n\t\tKeyUsage:     x509.KeyUsageCertSign | x509.KeyUsageCRLSign,\n\t\tNotBefore:    now,\n\t\tNotAfter:     now.Add(time.Hour),\n\t\tSerialNumber: serial,\n\t}\n\n\tsigner, ok := priv.(crypto.Signer)\n\tif !ok {\n\t\treturn nil, nil, errors.Errorf(\"result is not a crypto.Signer: type %T\", priv)\n\t}\n\n\tcert, err := x509util.CreateCertificate(template, template, pub, signer)\n\n\treturn cert, signer, err\n}\n\nvar errUnsupportedKey = fmt.Errorf(\"unknown key type; only RSA and ECDSA are supported\")\n\n// keyID is the account identity provided by a CA during registration.\ntype keyID string\n\n// noKeyID indicates that jwsEncodeJSON should compute and use JWK instead of a KID.\n// See jwsEncodeJSON for details.\nconst noKeyID = keyID(\"\")\n\n// jwsEncodeJSON signs claimset using provided key and a nonce.\n// The result is serialized in JSON format containing either kid or jwk\n// fields based on the provided keyID value.\n//\n// If kid is non-empty, its quoted value is inserted in the protected head\n// as \"kid\" field value. Otherwise, JWK is computed using jwkEncode and inserted\n// as \"jwk\" field value. The \"jwk\" and \"kid\" fields are mutually exclusive.\n//\n// See https://tools.ietf.org/html/rfc7515#section-7.\n//\n// If nonce is empty, it will not be encoded into the header.\n// Implementation taken from github.com/mholt/acmez, which seems to be based on\n// https://github.com/golang/crypto/blob/master/acme/jws.go.\nfunc jwsEncodeJSON(claimset interface{}, key crypto.Signer, kid keyID, nonce, u string) ([]byte, error) {\n\talg, sha := jwsHasher(key.Public())\n\tif alg == \"\" || !sha.Available() {\n\t\treturn nil, errUnsupportedKey\n\t}\n\n\tphead, err := jwsHead(alg, nonce, u, kid, key)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar payload string\n\tif claimset != nil {\n\t\tcs, err := json.Marshal(claimset)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tpayload = base64.RawURLEncoding.EncodeToString(cs)\n\t}\n\n\tpayloadToSign := []byte(phead + \".\" + payload)\n\thash := sha.New()\n\t_, _ = hash.Write(payloadToSign)\n\tdigest := hash.Sum(nil)\n\n\tsig, err := jwsSign(key, sha, digest)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn jwsFinal(sha, sig, phead, payload)\n}\n\n// jwsHasher indicates suitable JWS algorithm name and a hash function\n// to use for signing a digest with the provided key.\n// It returns (\"\", 0) if the key is not supported.\n// Implementation taken from github.com/mholt/acmez, which seems to be based on\n// https://github.com/golang/crypto/blob/master/acme/jws.go.\nfunc jwsHasher(pub crypto.PublicKey) (string, crypto.Hash) {\n\tswitch pub := pub.(type) {\n\tcase *rsa.PublicKey:\n\t\treturn \"RS256\", crypto.SHA256\n\tcase *ecdsa.PublicKey:\n\t\tswitch pub.Params().Name {\n\t\tcase \"P-256\":\n\t\t\treturn \"ES256\", crypto.SHA256\n\t\tcase \"P-384\":\n\t\t\treturn \"ES384\", crypto.SHA384\n\t\tcase \"P-521\":\n\t\t\treturn \"ES512\", crypto.SHA512\n\t\t}\n\t}\n\treturn \"\", 0\n}\n\n// jwsSign signs the digest using the given key.\n// The hash is unused for ECDSA keys.\n//\n// Note: non-stdlib crypto.Signer implementations are expected to return\n// the signature in the format as specified in RFC7518.\n// See https://tools.ietf.org/html/rfc7518 for more details.\n// Implementation taken from github.com/mholt/acmez, which seems to be based on\n// https://github.com/golang/crypto/blob/master/acme/jws.go.\nfunc jwsSign(key crypto.Signer, hash crypto.Hash, digest []byte) ([]byte, error) {\n\tif key, ok := key.(*ecdsa.PrivateKey); ok {\n\t\t// The key.Sign method of ecdsa returns ASN1-encoded signature.\n\t\t// So, we use the package Sign function instead\n\t\t// to get R and S values directly and format the result accordingly.\n\t\tr, s, err := ecdsa.Sign(rand.Reader, key, digest)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\trb, sb := r.Bytes(), s.Bytes()\n\t\tsize := key.Params().BitSize / 8\n\t\tif size%8 > 0 {\n\t\t\tsize++\n\t\t}\n\t\tsig := make([]byte, size*2)\n\t\tcopy(sig[size-len(rb):], rb)\n\t\tcopy(sig[size*2-len(sb):], sb)\n\t\treturn sig, nil\n\t}\n\treturn key.Sign(rand.Reader, digest, hash)\n}\n\n// jwsHead constructs the protected JWS header for the given fields.\n// Since jwk and kid are mutually-exclusive, the jwk will be encoded\n// only if kid is empty. If nonce is empty, it will not be encoded.\n// Implementation taken from github.com/mholt/acmez, which seems to be based on\n// https://github.com/golang/crypto/blob/master/acme/jws.go.\nfunc jwsHead(alg, nonce, u string, kid keyID, key crypto.Signer) (string, error) {\n\tphead := fmt.Sprintf(`{\"alg\":%q`, alg)\n\tif kid == noKeyID {\n\t\tjwk, err := jwkEncode(key.Public())\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\tphead += fmt.Sprintf(`,\"jwk\":%s`, jwk)\n\t} else {\n\t\tphead += fmt.Sprintf(`,\"kid\":%q`, kid)\n\t}\n\tif nonce != \"\" {\n\t\tphead += fmt.Sprintf(`,\"nonce\":%q`, nonce)\n\t}\n\tphead += fmt.Sprintf(`,\"url\":%q}`, u)\n\tphead = base64.RawURLEncoding.EncodeToString([]byte(phead))\n\treturn phead, nil\n}\n\n// jwkEncode encodes public part of an RSA or ECDSA key into a JWK.\n// The result is also suitable for creating a JWK thumbprint.\n// https://tools.ietf.org/html/rfc7517\n// Implementation taken from github.com/mholt/acmez, which seems to be based on\n// https://github.com/golang/crypto/blob/master/acme/jws.go.\nfunc jwkEncode(pub crypto.PublicKey) (string, error) {\n\tswitch pub := pub.(type) {\n\tcase *rsa.PublicKey:\n\t\t// https://tools.ietf.org/html/rfc7518#section-6.3.1\n\t\tn := pub.N\n\t\te := big.NewInt(int64(pub.E))\n\t\t// Field order is important.\n\t\t// See https://tools.ietf.org/html/rfc7638#section-3.3 for details.\n\t\treturn fmt.Sprintf(`{\"e\":%q,\"kty\":\"RSA\",\"n\":%q}`,\n\t\t\tbase64.RawURLEncoding.EncodeToString(e.Bytes()),\n\t\t\tbase64.RawURLEncoding.EncodeToString(n.Bytes()),\n\t\t), nil\n\tcase *ecdsa.PublicKey:\n\t\t// https://tools.ietf.org/html/rfc7518#section-6.2.1\n\t\tp := pub.Curve.Params()\n\t\tn := p.BitSize / 8\n\t\tif p.BitSize%8 != 0 {\n\t\t\tn++\n\t\t}\n\t\tx := pub.X.Bytes()\n\t\tif n > len(x) {\n\t\t\tx = append(make([]byte, n-len(x)), x...)\n\t\t}\n\t\ty := pub.Y.Bytes()\n\t\tif n > len(y) {\n\t\t\ty = append(make([]byte, n-len(y)), y...)\n\t\t}\n\t\t// Field order is important.\n\t\t// See https://tools.ietf.org/html/rfc7638#section-3.3 for details.\n\t\treturn fmt.Sprintf(`{\"crv\":%q,\"kty\":\"EC\",\"x\":%q,\"y\":%q}`,\n\t\t\tp.Name,\n\t\t\tbase64.RawURLEncoding.EncodeToString(x),\n\t\t\tbase64.RawURLEncoding.EncodeToString(y),\n\t\t), nil\n\t}\n\treturn \"\", errUnsupportedKey\n}\n\n// jwsFinal constructs the final JWS object.\n// Implementation taken from github.com/mholt/acmez, which seems to be based on\n// https://github.com/golang/crypto/blob/master/acme/jws.go.\nfunc jwsFinal(_ crypto.Hash, sig []byte, phead, payload string) ([]byte, error) {\n\tenc := struct {\n\t\tProtected string `json:\"protected\"`\n\t\tPayload   string `json:\"payload\"`\n\t\tSig       string `json:\"signature\"`\n\t}{\n\t\tProtected: phead,\n\t\tPayload:   payload,\n\t\tSig:       base64.RawURLEncoding.EncodeToString(sig),\n\t}\n\tresult, err := json.Marshal(&enc)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn result, nil\n}\n\ntype mockCA struct {\n\tMockIsRevoked      func(sn string) (bool, error)\n\tMockRevoke         func(ctx context.Context, opts *authority.RevokeOptions) error\n\tMockAreSANsallowed func(ctx context.Context, sans []string) error\n\tMockGetBackdate    func() *time.Duration\n}\n\nfunc (m *mockCA) SignWithContext(context.Context, *x509.CertificateRequest, provisioner.SignOptions, ...provisioner.SignOption) ([]*x509.Certificate, error) {\n\treturn nil, nil\n}\n\nfunc (m *mockCA) AreSANsAllowed(ctx context.Context, sans []string) error {\n\tif m.MockAreSANsallowed != nil {\n\t\treturn m.MockAreSANsallowed(ctx, sans)\n\t}\n\treturn nil\n}\n\nfunc (m *mockCA) IsRevoked(sn string) (bool, error) {\n\tif m.MockIsRevoked != nil {\n\t\treturn m.MockIsRevoked(sn)\n\t}\n\treturn false, nil\n}\n\nfunc (m *mockCA) Revoke(ctx context.Context, opts *authority.RevokeOptions) error {\n\tif m.MockRevoke != nil {\n\t\treturn m.MockRevoke(ctx, opts)\n\t}\n\treturn nil\n}\n\nfunc (m *mockCA) LoadProvisionerByName(string) (provisioner.Interface, error) {\n\treturn nil, nil\n}\n\nfunc (m *mockCA) GetBackdate() *time.Duration {\n\tif m.MockGetBackdate != nil {\n\t\treturn m.MockGetBackdate()\n\t}\n\treturn nil\n}\n\nfunc Test_validateReasonCode(t *testing.T) {\n\ttests := []struct {\n\t\tname       string\n\t\treasonCode *int\n\t\twant       *acme.Error\n\t}{\n\t\t{\n\t\t\tname:       \"ok\",\n\t\t\treasonCode: v(ocsp.Unspecified),\n\t\t\twant:       nil,\n\t\t},\n\t\t{\n\t\t\tname:       \"fail/too-low\",\n\t\t\treasonCode: v(-1),\n\t\t\twant:       acme.NewError(acme.ErrorBadRevocationReasonType, \"reasonCode out of bounds\"),\n\t\t},\n\t\t{\n\t\t\tname:       \"fail/too-high\",\n\t\t\treasonCode: v(11),\n\t\t\twant:       acme.NewError(acme.ErrorBadRevocationReasonType, \"reasonCode out of bounds\"),\n\t\t},\n\t\t{\n\t\t\tname:       \"fail/missing-7\",\n\t\t\treasonCode: v(7),\n\n\t\t\twant: acme.NewError(acme.ErrorBadRevocationReasonType, \"reasonCode out of bounds\"),\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\terr := validateReasonCode(tt.reasonCode)\n\t\t\tif (err != nil) != (tt.want != nil) {\n\t\t\t\tt.Errorf(\"validateReasonCode() = %v, want %v\", err, tt.want)\n\t\t\t}\n\t\t\tif err != nil {\n\t\t\t\tassert.Equals(t, err.Type, tt.want.Type)\n\t\t\t\tassert.Equals(t, err.Detail, tt.want.Detail)\n\t\t\t\tassert.Equals(t, err.Status, tt.want.Status)\n\t\t\t\tassert.Equals(t, err.Err.Error(), tt.want.Err.Error())\n\t\t\t\tassert.Equals(t, err.Detail, tt.want.Detail)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_reason(t *testing.T) {\n\ttests := []struct {\n\t\tname       string\n\t\treasonCode int\n\t\twant       string\n\t}{\n\t\t{\n\t\t\tname:       \"unspecified reason\",\n\t\t\treasonCode: ocsp.Unspecified,\n\t\t\twant:       \"unspecified reason\",\n\t\t},\n\t\t{\n\t\t\tname:       \"key compromised\",\n\t\t\treasonCode: ocsp.KeyCompromise,\n\t\t\twant:       \"key compromised\",\n\t\t},\n\t\t{\n\t\t\tname:       \"ca compromised\",\n\t\t\treasonCode: ocsp.CACompromise,\n\t\t\twant:       \"ca compromised\",\n\t\t},\n\t\t{\n\t\t\tname:       \"affiliation changed\",\n\t\t\treasonCode: ocsp.AffiliationChanged,\n\t\t\twant:       \"affiliation changed\",\n\t\t},\n\t\t{\n\t\t\tname:       \"superseded\",\n\t\t\treasonCode: ocsp.Superseded,\n\t\t\twant:       \"superseded\",\n\t\t},\n\t\t{\n\t\t\tname:       \"cessation of operation\",\n\t\t\treasonCode: ocsp.CessationOfOperation,\n\t\t\twant:       \"cessation of operation\",\n\t\t},\n\t\t{\n\t\t\tname:       \"certificate hold\",\n\t\t\treasonCode: ocsp.CertificateHold,\n\t\t\twant:       \"certificate hold\",\n\t\t},\n\t\t{\n\t\t\tname:       \"remove from crl\",\n\t\t\treasonCode: ocsp.RemoveFromCRL,\n\t\t\twant:       \"remove from crl\",\n\t\t},\n\t\t{\n\t\t\tname:       \"privilege withdrawn\",\n\t\t\treasonCode: ocsp.PrivilegeWithdrawn,\n\t\t\twant:       \"privilege withdrawn\",\n\t\t},\n\t\t{\n\t\t\tname:       \"aa compromised\",\n\t\t\treasonCode: ocsp.AACompromise,\n\t\t\twant:       \"aa compromised\",\n\t\t},\n\t\t{\n\t\t\tname:       \"default\",\n\t\t\treasonCode: -1,\n\t\t\twant:       \"unspecified reason\",\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif got := reason(tt.reasonCode); got != tt.want {\n\t\t\t\tt.Errorf(\"reason() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_revokeOptions(t *testing.T) {\n\tcert, _, err := generateCertKeyPair()\n\tassert.FatalError(t, err)\n\ttype args struct {\n\t\tserial          string\n\t\tcertToBeRevoked *x509.Certificate\n\t\treasonCode      *int\n\t}\n\ttests := []struct {\n\t\tname string\n\t\targs args\n\t\twant *authority.RevokeOptions\n\t}{\n\t\t{\n\t\t\tname: \"ok/no-reasoncode\",\n\t\t\targs: args{\n\t\t\t\tserial:          \"1234\",\n\t\t\t\tcertToBeRevoked: cert,\n\t\t\t},\n\t\t\twant: &authority.RevokeOptions{\n\t\t\t\tSerial: \"1234\",\n\t\t\t\tCrt:    cert,\n\t\t\t\tACME:   true,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"ok/including-reasoncode\",\n\t\t\targs: args{\n\t\t\t\tserial:          \"1234\",\n\t\t\t\tcertToBeRevoked: cert,\n\t\t\t\treasonCode:      v(ocsp.KeyCompromise),\n\t\t\t},\n\t\t\twant: &authority.RevokeOptions{\n\t\t\t\tSerial:     \"1234\",\n\t\t\t\tCrt:        cert,\n\t\t\t\tACME:       true,\n\t\t\t\tReasonCode: 1,\n\t\t\t\tReason:     \"key compromised\",\n\t\t\t},\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif got := revokeOptions(tt.args.serial, tt.args.certToBeRevoked, tt.args.reasonCode); !cmp.Equal(got, tt.want) {\n\t\t\t\tt.Errorf(\"revokeOptions() diff =\\n%s\", cmp.Diff(got, tt.want))\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestHandler_RevokeCert(t *testing.T) {\n\tprov := &provisioner.ACME{\n\t\tType: \"ACME\",\n\t\tName: \"testprov\",\n\t}\n\tescProvName := url.PathEscape(prov.GetName())\n\tbaseURL := &url.URL{Scheme: \"https\", Host: \"test.ca.smallstep.com\"}\n\n\tchiCtx := chi.NewRouteContext()\n\trevokeURL := fmt.Sprintf(\"%s/acme/%s/revoke-cert\", baseURL.String(), escProvName)\n\n\tcert, key, err := generateCertKeyPair()\n\tassert.FatalError(t, err)\n\trp := &revokePayload{\n\t\tCertificate: base64.RawURLEncoding.EncodeToString(cert.Raw),\n\t}\n\tpayloadBytes, err := json.Marshal(rp)\n\tassert.FatalError(t, err)\n\n\tjws := &jose.JSONWebSignature{\n\t\tSignatures: []jose.Signature{\n\t\t\t{\n\t\t\t\tProtected: jose.Header{\n\t\t\t\t\tAlgorithm: jose.ES256,\n\t\t\t\t\tKeyID:     \"bar\",\n\t\t\t\t\tExtraHeaders: map[jose.HeaderKey]interface{}{\n\t\t\t\t\t\t\"url\": revokeURL,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\ttype test struct {\n\t\tdb         acme.DB\n\t\tca         acme.CertificateAuthority\n\t\tctx        context.Context\n\t\tstatusCode int\n\t\terr        *acme.Error\n\t}\n\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/no-jws\": func(t *testing.T) test {\n\t\t\tctx := context.Background()\n\t\t\treturn test{\n\t\t\t\tdb:         &acme.MockDB{},\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 500,\n\t\t\t\terr:        acme.NewErrorISE(\"jws expected in request context\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/nil-jws\": func(t *testing.T) test {\n\t\t\tctx := context.WithValue(context.Background(), jwsContextKey, nil)\n\t\t\treturn test{\n\t\t\t\tdb:         &acme.MockDB{},\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 500,\n\t\t\t\terr:        acme.NewErrorISE(\"jws expected in request context\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/no-provisioner\": func(t *testing.T) test {\n\t\t\tctx := context.WithValue(context.Background(), jwsContextKey, jws)\n\t\t\treturn test{\n\t\t\t\tdb:         &acme.MockDB{},\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 500,\n\t\t\t\terr:        acme.NewErrorISE(\"provisioner does not exist\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/nil-provisioner\": func(t *testing.T) test {\n\t\t\tctx := context.WithValue(context.Background(), jwsContextKey, jws)\n\t\t\tctx = acme.NewProvisionerContext(ctx, nil)\n\t\t\treturn test{\n\t\t\t\tdb:         &acme.MockDB{},\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 500,\n\t\t\t\terr:        acme.NewErrorISE(\"provisioner does not exist\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/no-payload\": func(t *testing.T) test {\n\t\t\tctx := context.WithValue(context.Background(), jwsContextKey, jws)\n\t\t\tctx = acme.NewProvisionerContext(ctx, prov)\n\t\t\treturn test{\n\t\t\t\tdb:         &acme.MockDB{},\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 500,\n\t\t\t\terr:        acme.NewErrorISE(\"payload does not exist\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/nil-payload\": func(t *testing.T) test {\n\t\t\tctx := context.WithValue(context.Background(), jwsContextKey, jws)\n\t\t\tctx = acme.NewProvisionerContext(ctx, prov)\n\t\t\tctx = context.WithValue(ctx, payloadContextKey, nil)\n\t\t\treturn test{\n\t\t\t\tdb:         &acme.MockDB{},\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 500,\n\t\t\t\terr:        acme.NewErrorISE(\"payload does not exist\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/unmarshal-payload\": func(t *testing.T) test {\n\t\t\tmalformedPayload := []byte(`{\"payload\":malformed?}`)\n\t\t\tctx := context.WithValue(context.Background(), jwsContextKey, jws)\n\t\t\tctx = acme.NewProvisionerContext(ctx, prov)\n\t\t\tctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: malformedPayload})\n\t\t\treturn test{\n\t\t\t\tdb:         &acme.MockDB{},\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 500,\n\t\t\t\terr:        acme.NewErrorISE(\"error unmarshaling payload\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/wrong-certificate-encoding\": func(t *testing.T) test {\n\t\t\twrongPayload := &revokePayload{\n\t\t\t\tCertificate: base64.StdEncoding.EncodeToString(cert.Raw),\n\t\t\t}\n\t\t\twronglyEncodedPayloadBytes, err := json.Marshal(wrongPayload)\n\t\t\tassert.FatalError(t, err)\n\t\t\tctx := acme.NewProvisionerContext(context.Background(), prov)\n\t\t\tctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: wronglyEncodedPayloadBytes})\n\t\t\tctx = context.WithValue(ctx, jwsContextKey, jws)\n\t\t\treturn test{\n\t\t\t\tdb:         &acme.MockDB{},\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 400,\n\t\t\t\terr: &acme.Error{\n\t\t\t\t\tType:   \"urn:ietf:params:acme:error:malformed\",\n\t\t\t\t\tStatus: 400,\n\t\t\t\t\tDetail: \"The request message was malformed\",\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"fail/no-certificate-encoded\": func(t *testing.T) test {\n\t\t\temptyPayload := &revokePayload{\n\t\t\t\tCertificate: base64.RawURLEncoding.EncodeToString([]byte{}),\n\t\t\t}\n\t\t\temptyPayloadBytes, err := json.Marshal(emptyPayload)\n\t\t\tassert.FatalError(t, err)\n\t\t\tctx := acme.NewProvisionerContext(context.Background(), prov)\n\t\t\tctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: emptyPayloadBytes})\n\t\t\tctx = context.WithValue(ctx, jwsContextKey, jws)\n\t\t\treturn test{\n\t\t\t\tdb:         &acme.MockDB{},\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 400,\n\t\t\t\terr: &acme.Error{\n\t\t\t\t\tType:   \"urn:ietf:params:acme:error:malformed\",\n\t\t\t\t\tStatus: 400,\n\t\t\t\t\tDetail: \"The request message was malformed\",\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"fail/db.GetCertificateBySerial\": func(t *testing.T) test {\n\t\t\tctx := acme.NewProvisionerContext(context.Background(), prov)\n\t\t\tctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: payloadBytes})\n\t\t\tctx = context.WithValue(ctx, jwsContextKey, jws)\n\t\t\tdb := &acme.MockDB{\n\t\t\t\tMockGetCertificateBySerial: func(ctx context.Context, serial string) (*acme.Certificate, error) {\n\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t},\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tdb:         db,\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 500,\n\t\t\t\terr:        acme.NewErrorISE(\"error retrieving certificate by serial\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/different-certificate-contents\": func(t *testing.T) test {\n\t\t\taDifferentCert, _, err := generateCertKeyPair()\n\t\t\tassert.FatalError(t, err)\n\t\t\tctx := acme.NewProvisionerContext(context.Background(), prov)\n\t\t\tctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: payloadBytes})\n\t\t\tctx = context.WithValue(ctx, jwsContextKey, jws)\n\t\t\tdb := &acme.MockDB{\n\t\t\t\tMockGetCertificateBySerial: func(ctx context.Context, serial string) (*acme.Certificate, error) {\n\t\t\t\t\tassert.Equals(t, cert.SerialNumber.String(), serial)\n\t\t\t\t\treturn &acme.Certificate{\n\t\t\t\t\t\tLeaf: aDifferentCert,\n\t\t\t\t\t}, nil\n\t\t\t\t},\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tdb:         db,\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 500,\n\t\t\t\terr:        acme.NewErrorISE(\"certificate raw bytes are not equal\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/no-account\": func(t *testing.T) test {\n\t\t\tctx := acme.NewProvisionerContext(context.Background(), prov)\n\t\t\tctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: payloadBytes})\n\t\t\tctx = context.WithValue(ctx, jwsContextKey, jws)\n\t\t\tdb := &acme.MockDB{\n\t\t\t\tMockGetCertificateBySerial: func(ctx context.Context, serial string) (*acme.Certificate, error) {\n\t\t\t\t\tassert.Equals(t, cert.SerialNumber.String(), serial)\n\t\t\t\t\treturn &acme.Certificate{\n\t\t\t\t\t\tLeaf: cert,\n\t\t\t\t\t}, nil\n\t\t\t\t},\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tdb:         db,\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 400,\n\t\t\t\terr:        acme.NewError(acme.ErrorAccountDoesNotExistType, \"account not in context\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/nil-account\": func(t *testing.T) test {\n\t\t\tctx := acme.NewProvisionerContext(context.Background(), prov)\n\t\t\tctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: payloadBytes})\n\t\t\tctx = context.WithValue(ctx, jwsContextKey, jws)\n\t\t\tctx = context.WithValue(ctx, accContextKey, nil)\n\t\t\tdb := &acme.MockDB{\n\t\t\t\tMockGetCertificateBySerial: func(ctx context.Context, serial string) (*acme.Certificate, error) {\n\t\t\t\t\tassert.Equals(t, cert.SerialNumber.String(), serial)\n\t\t\t\t\treturn &acme.Certificate{\n\t\t\t\t\t\tLeaf: cert,\n\t\t\t\t\t}, nil\n\t\t\t\t},\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tdb:         db,\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 400,\n\t\t\t\terr:        acme.NewError(acme.ErrorAccountDoesNotExistType, \"account not in context\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/account-not-valid\": func(t *testing.T) test {\n\t\t\tacc := &acme.Account{ID: \"accountID\", Status: acme.StatusInvalid}\n\t\t\tctx := acme.NewProvisionerContext(context.Background(), prov)\n\t\t\tctx = context.WithValue(ctx, accContextKey, acc)\n\t\t\tctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: payloadBytes})\n\t\t\tctx = context.WithValue(ctx, jwsContextKey, jws)\n\t\t\tctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx)\n\t\t\tdb := &acme.MockDB{\n\t\t\t\tMockGetCertificateBySerial: func(ctx context.Context, serial string) (*acme.Certificate, error) {\n\t\t\t\t\tassert.Equals(t, cert.SerialNumber.String(), serial)\n\t\t\t\t\treturn &acme.Certificate{\n\t\t\t\t\t\tAccountID: \"accountID\",\n\t\t\t\t\t\tLeaf:      cert,\n\t\t\t\t\t}, nil\n\t\t\t\t},\n\t\t\t}\n\t\t\tca := &mockCA{}\n\t\t\treturn test{\n\t\t\t\tdb:         db,\n\t\t\t\tca:         ca,\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 403,\n\t\t\t\terr: &acme.Error{\n\t\t\t\t\tType:   \"urn:ietf:params:acme:error:unauthorized\",\n\t\t\t\t\tDetail: \"No authorization provided for name 127.0.0.1\",\n\t\t\t\t\tStatus: 403,\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"fail/account-not-authorized\": func(t *testing.T) test {\n\t\t\tacc := &acme.Account{ID: \"accountID\", Status: acme.StatusValid}\n\t\t\tctx := acme.NewProvisionerContext(context.Background(), prov)\n\t\t\tctx = context.WithValue(ctx, accContextKey, acc)\n\t\t\tctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: payloadBytes})\n\t\t\tctx = context.WithValue(ctx, jwsContextKey, jws)\n\t\t\tctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx)\n\t\t\tdb := &acme.MockDB{\n\t\t\t\tMockGetCertificateBySerial: func(ctx context.Context, serial string) (*acme.Certificate, error) {\n\t\t\t\t\tassert.Equals(t, cert.SerialNumber.String(), serial)\n\t\t\t\t\treturn &acme.Certificate{\n\t\t\t\t\t\tAccountID: \"differentAccountID\",\n\t\t\t\t\t\tLeaf:      cert,\n\t\t\t\t\t}, nil\n\t\t\t\t},\n\t\t\t\tMockGetAuthorizationsByAccountID: func(ctx context.Context, accountID string) ([]*acme.Authorization, error) {\n\t\t\t\t\tassert.Equals(t, \"accountID\", accountID)\n\t\t\t\t\treturn []*acme.Authorization{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tAccountID: \"accountID\",\n\t\t\t\t\t\t\tStatus:    acme.StatusValid,\n\t\t\t\t\t\t\tIdentifier: acme.Identifier{\n\t\t\t\t\t\t\t\tType:  acme.IP,\n\t\t\t\t\t\t\t\tValue: \"127.0.1.0\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t}, nil\n\t\t\t\t},\n\t\t\t}\n\t\t\tca := &mockCA{}\n\t\t\treturn test{\n\t\t\t\tdb:         db,\n\t\t\t\tca:         ca,\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 403,\n\t\t\t\terr: &acme.Error{\n\t\t\t\t\tType:   \"urn:ietf:params:acme:error:unauthorized\",\n\t\t\t\t\tDetail: \"No authorization provided for name 127.0.0.1\",\n\t\t\t\t\tStatus: 403,\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"fail/unauthorized-certificate-key\": func(t *testing.T) test {\n\t\t\t_, unauthorizedKey, err := generateCertKeyPair()\n\t\t\tassert.FatalError(t, err)\n\t\t\tjwsPayload := &revokePayload{\n\t\t\t\tCertificate: base64.RawURLEncoding.EncodeToString(cert.Raw),\n\t\t\t\tReasonCode:  v(2),\n\t\t\t}\n\t\t\tjwsBytes, err := jwsEncodeJSON(rp, unauthorizedKey, \"\", \"nonce\", revokeURL)\n\t\t\tassert.FatalError(t, err)\n\t\t\tparsedJWS, err := jose.ParseJWS(string(jwsBytes))\n\t\t\tassert.FatalError(t, err)\n\t\t\tunauthorizedPayloadBytes, err := json.Marshal(jwsPayload)\n\t\t\tassert.FatalError(t, err)\n\t\t\tctx := acme.NewProvisionerContext(context.Background(), prov)\n\t\t\tctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: unauthorizedPayloadBytes})\n\t\t\tctx = context.WithValue(ctx, jwsContextKey, parsedJWS)\n\t\t\tctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx)\n\t\t\tdb := &acme.MockDB{\n\t\t\t\tMockGetCertificateBySerial: func(ctx context.Context, serial string) (*acme.Certificate, error) {\n\t\t\t\t\tassert.Equals(t, cert.SerialNumber.String(), serial)\n\t\t\t\t\treturn &acme.Certificate{\n\t\t\t\t\t\tAccountID: \"accountID\",\n\t\t\t\t\t\tLeaf:      cert,\n\t\t\t\t\t}, nil\n\t\t\t\t},\n\t\t\t}\n\t\t\tca := &mockCA{}\n\t\t\tacmeErr := acme.NewError(acme.ErrorUnauthorizedType, \"verification of jws using certificate public key failed\")\n\t\t\tacmeErr.Detail = \"No authorization provided for name 127.0.0.1\"\n\t\t\treturn test{\n\t\t\t\tdb:         db,\n\t\t\t\tca:         ca,\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 403,\n\t\t\t\terr:        acmeErr,\n\t\t\t}\n\t\t},\n\t\t\"fail/certificate-revoked-check-fails\": func(t *testing.T) test {\n\t\t\tacc := &acme.Account{ID: \"accountID\", Status: acme.StatusValid}\n\t\t\tctx := acme.NewProvisionerContext(context.Background(), prov)\n\t\t\tctx = context.WithValue(ctx, accContextKey, acc)\n\t\t\tctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: payloadBytes})\n\t\t\tctx = context.WithValue(ctx, jwsContextKey, jws)\n\t\t\tctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx)\n\t\t\tdb := &acme.MockDB{\n\t\t\t\tMockGetCertificateBySerial: func(ctx context.Context, serial string) (*acme.Certificate, error) {\n\t\t\t\t\tassert.Equals(t, cert.SerialNumber.String(), serial)\n\t\t\t\t\treturn &acme.Certificate{\n\t\t\t\t\t\tAccountID: \"accountID\",\n\t\t\t\t\t\tLeaf:      cert,\n\t\t\t\t\t}, nil\n\t\t\t\t},\n\t\t\t}\n\t\t\tca := &mockCA{\n\t\t\t\tMockIsRevoked: func(sn string) (bool, error) {\n\t\t\t\t\treturn false, errors.New(\"force\")\n\t\t\t\t},\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tdb:         db,\n\t\t\t\tca:         ca,\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 500,\n\t\t\t\terr: &acme.Error{\n\t\t\t\t\tType:   \"urn:ietf:params:acme:error:serverInternal\",\n\t\t\t\t\tDetail: \"The server experienced an internal error\",\n\t\t\t\t\tStatus: 500,\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"fail/certificate-already-revoked\": func(t *testing.T) test {\n\t\t\tacc := &acme.Account{ID: \"accountID\", Status: acme.StatusValid}\n\t\t\tctx := acme.NewProvisionerContext(context.Background(), prov)\n\t\t\tctx = context.WithValue(ctx, accContextKey, acc)\n\t\t\tctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: payloadBytes})\n\t\t\tctx = context.WithValue(ctx, jwsContextKey, jws)\n\t\t\tdb := &acme.MockDB{\n\t\t\t\tMockGetCertificateBySerial: func(ctx context.Context, serial string) (*acme.Certificate, error) {\n\t\t\t\t\tassert.Equals(t, cert.SerialNumber.String(), serial)\n\t\t\t\t\treturn &acme.Certificate{\n\t\t\t\t\t\tAccountID: \"accountID\",\n\t\t\t\t\t\tLeaf:      cert,\n\t\t\t\t\t}, nil\n\t\t\t\t},\n\t\t\t}\n\t\t\tca := &mockCA{\n\t\t\t\tMockIsRevoked: func(sn string) (bool, error) {\n\t\t\t\t\treturn true, nil\n\t\t\t\t},\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tdb:         db,\n\t\t\t\tca:         ca,\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 400,\n\t\t\t\terr: &acme.Error{\n\t\t\t\t\tType:   \"urn:ietf:params:acme:error:alreadyRevoked\",\n\t\t\t\t\tDetail: \"Certificate already revoked\",\n\t\t\t\t\tStatus: 400,\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"fail/invalid-reasoncode\": func(t *testing.T) test {\n\t\t\tinvalidReasonPayload := &revokePayload{\n\t\t\t\tCertificate: base64.RawURLEncoding.EncodeToString(cert.Raw),\n\t\t\t\tReasonCode:  v(7),\n\t\t\t}\n\t\t\tinvalidReasonCodePayloadBytes, err := json.Marshal(invalidReasonPayload)\n\t\t\tassert.FatalError(t, err)\n\t\t\tacc := &acme.Account{ID: \"accountID\", Status: acme.StatusValid}\n\t\t\tctx := acme.NewProvisionerContext(context.Background(), prov)\n\t\t\tctx = context.WithValue(ctx, accContextKey, acc)\n\t\t\tctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: invalidReasonCodePayloadBytes})\n\t\t\tctx = context.WithValue(ctx, jwsContextKey, jws)\n\t\t\tdb := &acme.MockDB{\n\t\t\t\tMockGetCertificateBySerial: func(ctx context.Context, serial string) (*acme.Certificate, error) {\n\t\t\t\t\tassert.Equals(t, cert.SerialNumber.String(), serial)\n\t\t\t\t\treturn &acme.Certificate{\n\t\t\t\t\t\tAccountID: \"accountID\",\n\t\t\t\t\t\tLeaf:      cert,\n\t\t\t\t\t}, nil\n\t\t\t\t},\n\t\t\t}\n\t\t\tca := &mockCA{\n\t\t\t\tMockIsRevoked: func(sn string) (bool, error) {\n\t\t\t\t\treturn false, nil\n\t\t\t\t},\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tdb:         db,\n\t\t\t\tca:         ca,\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 400,\n\t\t\t\terr: &acme.Error{\n\t\t\t\t\tType:   \"urn:ietf:params:acme:error:badRevocationReason\",\n\t\t\t\t\tDetail: \"The revocation reason provided is not allowed by the server\",\n\t\t\t\t\tStatus: 400,\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"fail/prov.AuthorizeRevoke\": func(t *testing.T) test {\n\t\t\tassert.FatalError(t, err)\n\t\t\tmockACMEProv := &acme.MockProvisioner{\n\t\t\t\tMauthorizeRevoke: func(ctx context.Context, token string) error {\n\t\t\t\t\treturn errors.New(\"force\")\n\t\t\t\t},\n\t\t\t}\n\t\t\tacc := &acme.Account{ID: \"accountID\", Status: acme.StatusValid}\n\t\t\tctx := acme.NewProvisionerContext(context.Background(), mockACMEProv)\n\t\t\tctx = context.WithValue(ctx, accContextKey, acc)\n\t\t\tctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: payloadBytes})\n\t\t\tctx = context.WithValue(ctx, jwsContextKey, jws)\n\t\t\tdb := &acme.MockDB{\n\t\t\t\tMockGetCertificateBySerial: func(ctx context.Context, serial string) (*acme.Certificate, error) {\n\t\t\t\t\tassert.Equals(t, cert.SerialNumber.String(), serial)\n\t\t\t\t\treturn &acme.Certificate{\n\t\t\t\t\t\tAccountID: \"accountID\",\n\t\t\t\t\t\tLeaf:      cert,\n\t\t\t\t\t}, nil\n\t\t\t\t},\n\t\t\t}\n\t\t\tca := &mockCA{\n\t\t\t\tMockIsRevoked: func(sn string) (bool, error) {\n\t\t\t\t\treturn false, nil\n\t\t\t\t},\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tdb:         db,\n\t\t\t\tca:         ca,\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 500,\n\t\t\t\terr: &acme.Error{\n\t\t\t\t\tType:   \"urn:ietf:params:acme:error:serverInternal\",\n\t\t\t\t\tDetail: \"The server experienced an internal error\",\n\t\t\t\t\tStatus: 500,\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"fail/ca.Revoke\": func(t *testing.T) test {\n\t\t\tacc := &acme.Account{ID: \"accountID\", Status: acme.StatusValid}\n\t\t\tctx := acme.NewProvisionerContext(context.Background(), prov)\n\t\t\tctx = context.WithValue(ctx, accContextKey, acc)\n\t\t\tctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: payloadBytes})\n\t\t\tctx = context.WithValue(ctx, jwsContextKey, jws)\n\t\t\tdb := &acme.MockDB{\n\t\t\t\tMockGetCertificateBySerial: func(ctx context.Context, serial string) (*acme.Certificate, error) {\n\t\t\t\t\tassert.Equals(t, cert.SerialNumber.String(), serial)\n\t\t\t\t\treturn &acme.Certificate{\n\t\t\t\t\t\tAccountID: \"accountID\",\n\t\t\t\t\t\tLeaf:      cert,\n\t\t\t\t\t}, nil\n\t\t\t\t},\n\t\t\t}\n\t\t\tca := &mockCA{\n\t\t\t\tMockRevoke: func(ctx context.Context, opts *authority.RevokeOptions) error {\n\t\t\t\t\treturn errors.New(\"force\")\n\t\t\t\t},\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tdb:         db,\n\t\t\t\tca:         ca,\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 500,\n\t\t\t\terr: &acme.Error{\n\t\t\t\t\tType:   \"urn:ietf:params:acme:error:serverInternal\",\n\t\t\t\t\tDetail: \"The server experienced an internal error\",\n\t\t\t\t\tStatus: 500,\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"fail/ca.Revoke-already-revoked\": func(t *testing.T) test {\n\t\t\tacc := &acme.Account{ID: \"accountID\", Status: acme.StatusValid}\n\t\t\tctx := acme.NewProvisionerContext(context.Background(), prov)\n\t\t\tctx = context.WithValue(ctx, accContextKey, acc)\n\t\t\tctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: payloadBytes})\n\t\t\tctx = context.WithValue(ctx, jwsContextKey, jws)\n\t\t\tdb := &acme.MockDB{\n\t\t\t\tMockGetCertificateBySerial: func(ctx context.Context, serial string) (*acme.Certificate, error) {\n\t\t\t\t\tassert.Equals(t, cert.SerialNumber.String(), serial)\n\t\t\t\t\treturn &acme.Certificate{\n\t\t\t\t\t\tAccountID: \"accountID\",\n\t\t\t\t\t\tLeaf:      cert,\n\t\t\t\t\t}, nil\n\t\t\t\t},\n\t\t\t}\n\t\t\tca := &mockCA{\n\t\t\t\tMockIsRevoked: func(sn string) (bool, error) {\n\t\t\t\t\treturn false, nil\n\t\t\t\t},\n\t\t\t\tMockRevoke: func(ctx context.Context, opts *authority.RevokeOptions) error {\n\t\t\t\t\treturn fmt.Errorf(\"certificate with serial number '%s' is already revoked\", cert.SerialNumber.String())\n\t\t\t\t},\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tdb:         db,\n\t\t\t\tca:         ca,\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 400,\n\t\t\t\terr:        acme.NewError(acme.ErrorAlreadyRevokedType, \"certificate with serial number '%s' is already revoked\", cert.SerialNumber.String()),\n\t\t\t}\n\t\t},\n\t\t\"ok/using-account-key\": func(t *testing.T) test {\n\t\t\tacc := &acme.Account{ID: \"accountID\", Status: acme.StatusValid}\n\t\t\tctx := acme.NewProvisionerContext(context.Background(), prov)\n\t\t\tctx = context.WithValue(ctx, accContextKey, acc)\n\t\t\tctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: payloadBytes})\n\t\t\tctx = context.WithValue(ctx, jwsContextKey, jws)\n\t\t\tctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx)\n\t\t\tdb := &acme.MockDB{\n\t\t\t\tMockGetCertificateBySerial: func(ctx context.Context, serial string) (*acme.Certificate, error) {\n\t\t\t\t\tassert.Equals(t, cert.SerialNumber.String(), serial)\n\t\t\t\t\treturn &acme.Certificate{\n\t\t\t\t\t\tAccountID: \"accountID\",\n\t\t\t\t\t\tLeaf:      cert,\n\t\t\t\t\t}, nil\n\t\t\t\t},\n\t\t\t}\n\t\t\tca := &mockCA{}\n\t\t\treturn test{\n\t\t\t\tdb:         db,\n\t\t\t\tca:         ca,\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 200,\n\t\t\t}\n\t\t},\n\t\t\"ok/using-certificate-key\": func(t *testing.T) test {\n\t\t\tjwsBytes, err := jwsEncodeJSON(rp, key, \"\", \"nonce\", revokeURL)\n\t\t\tassert.FatalError(t, err)\n\t\t\tjws, err := jose.ParseJWS(string(jwsBytes))\n\t\t\tassert.FatalError(t, err)\n\t\t\tctx := acme.NewProvisionerContext(context.Background(), prov)\n\t\t\tctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: payloadBytes})\n\t\t\tctx = context.WithValue(ctx, jwsContextKey, jws)\n\t\t\tctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx)\n\t\t\tdb := &acme.MockDB{\n\t\t\t\tMockGetCertificateBySerial: func(ctx context.Context, serial string) (*acme.Certificate, error) {\n\t\t\t\t\tassert.Equals(t, cert.SerialNumber.String(), serial)\n\t\t\t\t\treturn &acme.Certificate{\n\t\t\t\t\t\tAccountID: \"someDifferentAccountID\",\n\t\t\t\t\t\tLeaf:      cert,\n\t\t\t\t\t}, nil\n\t\t\t\t},\n\t\t\t}\n\t\t\tca := &mockCA{}\n\t\t\treturn test{\n\t\t\t\tdb:         db,\n\t\t\t\tca:         ca,\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 200,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, setup := range tests {\n\t\ttc := setup(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tctx := newBaseContext(tc.ctx, tc.db, acme.NewLinker(\"test.ca.smallstep.com\", \"acme\"))\n\t\t\tmockMustAuthority(t, tc.ca)\n\t\t\treq := httptest.NewRequest(\"POST\", revokeURL, http.NoBody)\n\t\t\treq = req.WithContext(ctx)\n\t\t\tw := httptest.NewRecorder()\n\t\t\tRevokeCert(w, req)\n\t\t\tres := w.Result()\n\n\t\t\tassert.Equals(t, res.StatusCode, tc.statusCode)\n\n\t\t\tbody, err := io.ReadAll(res.Body)\n\t\t\tres.Body.Close()\n\t\t\tassert.FatalError(t, err)\n\n\t\t\tif res.StatusCode >= 400 && assert.NotNil(t, tc.err) {\n\t\t\t\tvar ae acme.Error\n\t\t\t\tassert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &ae))\n\n\t\t\t\tassert.Equals(t, ae.Type, tc.err.Type)\n\t\t\t\tassert.Equals(t, ae.Detail, tc.err.Detail)\n\t\t\t\tassert.Equals(t, ae.Subproblems, tc.err.Subproblems)\n\t\t\t\tassert.Equals(t, res.Header[\"Content-Type\"], []string{\"application/problem+json\"})\n\t\t\t} else {\n\t\t\t\tassert.True(t, bytes.Equal(bytes.TrimSpace(body), []byte{}))\n\t\t\t\tassert.Equals(t, []string{fmt.Sprintf(\"<%s/acme/%s/directory>;rel=\\\"index\\\"\", baseURL.String(), escProvName)}, res.Header[\"Link\"])\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestHandler_isAccountAuthorized(t *testing.T) {\n\ttype test struct {\n\t\tdb              acme.DB\n\t\tctx             context.Context\n\t\texistingCert    *acme.Certificate\n\t\tcertToBeRevoked *x509.Certificate\n\t\taccount         *acme.Account\n\t\terr             *acme.Error\n\t}\n\taccountID := \"accountID\"\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/account-invalid\": func(t *testing.T) test {\n\t\t\taccount := &acme.Account{\n\t\t\t\tID:     accountID,\n\t\t\t\tStatus: acme.StatusInvalid,\n\t\t\t}\n\t\t\tcertToBeRevoked := &x509.Certificate{\n\t\t\t\tSubject: pkix.Name{\n\t\t\t\t\tCommonName: \"127.0.0.1\",\n\t\t\t\t},\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tctx:             context.TODO(),\n\t\t\t\tcertToBeRevoked: certToBeRevoked,\n\t\t\t\taccount:         account,\n\t\t\t\terr: &acme.Error{\n\t\t\t\t\tType:   \"urn:ietf:params:acme:error:unauthorized\",\n\t\t\t\t\tStatus: http.StatusForbidden,\n\t\t\t\t\tDetail: \"No authorization provided for name 127.0.0.1\",\n\t\t\t\t\tErr:    errors.New(\"account 'accountID' has status 'invalid'\"),\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"fail/different-account\": func(t *testing.T) test {\n\t\t\taccount := &acme.Account{\n\t\t\t\tID:     accountID,\n\t\t\t\tStatus: acme.StatusValid,\n\t\t\t}\n\t\t\tcertToBeRevoked := &x509.Certificate{\n\t\t\t\tIPAddresses: []net.IP{net.ParseIP(\"127.0.0.1\")},\n\t\t\t}\n\t\t\texistingCert := &acme.Certificate{\n\t\t\t\tAccountID: \"differentAccountID\",\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tdb: &acme.MockDB{\n\t\t\t\t\tMockGetAuthorizationsByAccountID: func(ctx context.Context, accountID string) ([]*acme.Authorization, error) {\n\t\t\t\t\t\tassert.Equals(t, \"accountID\", accountID)\n\t\t\t\t\t\treturn []*acme.Authorization{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tAccountID: accountID,\n\t\t\t\t\t\t\t\tStatus:    acme.StatusValid,\n\t\t\t\t\t\t\t\tIdentifier: acme.Identifier{\n\t\t\t\t\t\t\t\t\tType:  acme.IP,\n\t\t\t\t\t\t\t\t\tValue: \"127.0.0.1\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t}, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tctx:             context.TODO(),\n\t\t\t\texistingCert:    existingCert,\n\t\t\t\tcertToBeRevoked: certToBeRevoked,\n\t\t\t\taccount:         account,\n\t\t\t\terr: &acme.Error{\n\t\t\t\t\tType:   \"urn:ietf:params:acme:error:unauthorized\",\n\t\t\t\t\tStatus: http.StatusForbidden,\n\t\t\t\t\tDetail: \"No authorization provided\",\n\t\t\t\t\tErr:    errors.New(\"account 'accountID' is not authorized\"),\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\taccount := &acme.Account{\n\t\t\t\tID:     accountID,\n\t\t\t\tStatus: acme.StatusValid,\n\t\t\t}\n\t\t\tcertToBeRevoked := &x509.Certificate{\n\t\t\t\tIPAddresses: []net.IP{net.ParseIP(\"127.0.0.1\")},\n\t\t\t}\n\t\t\texistingCert := &acme.Certificate{\n\t\t\t\tAccountID: \"accountID\",\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tdb: &acme.MockDB{\n\t\t\t\t\tMockGetAuthorizationsByAccountID: func(ctx context.Context, accountID string) ([]*acme.Authorization, error) {\n\t\t\t\t\t\tassert.Equals(t, \"accountID\", accountID)\n\t\t\t\t\t\treturn []*acme.Authorization{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tAccountID: accountID,\n\t\t\t\t\t\t\t\tStatus:    acme.StatusValid,\n\t\t\t\t\t\t\t\tIdentifier: acme.Identifier{\n\t\t\t\t\t\t\t\t\tType:  acme.IP,\n\t\t\t\t\t\t\t\t\tValue: \"127.0.0.1\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t}, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tctx:             context.TODO(),\n\t\t\t\texistingCert:    existingCert,\n\t\t\t\tcertToBeRevoked: certToBeRevoked,\n\t\t\t\taccount:         account,\n\t\t\t\terr:             nil,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, setup := range tests {\n\t\ttc := setup(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\t// h := &Handler{db: tc.db}\n\t\t\tacmeErr := isAccountAuthorized(tc.ctx, tc.existingCert, tc.certToBeRevoked, tc.account)\n\n\t\t\texpectError := tc.err != nil\n\t\t\tgotError := acmeErr != nil\n\t\t\tif expectError != gotError {\n\t\t\t\tt.Errorf(\"expected: %t, got: %t\", expectError, gotError)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif !gotError {\n\t\t\t\treturn // nothing to check; return early\n\t\t\t}\n\n\t\t\tassert.Equals(t, acmeErr.Err.Error(), tc.err.Err.Error())\n\t\t\tassert.Equals(t, acmeErr.Type, tc.err.Type)\n\t\t\tassert.Equals(t, acmeErr.Status, tc.err.Status)\n\t\t\tassert.Equals(t, acmeErr.Detail, tc.err.Detail)\n\t\t\tassert.Equals(t, acmeErr.Subproblems, tc.err.Subproblems)\n\n\t\t})\n\t}\n}\n\nfunc Test_wrapUnauthorizedError(t *testing.T) {\n\ttype test struct {\n\t\tcert                    *x509.Certificate\n\t\tunauthorizedIdentifiers []acme.Identifier\n\t\tmsg                     string\n\t\terr                     error\n\t\twant                    *acme.Error\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"unauthorizedIdentifiers\": func(t *testing.T) test {\n\t\t\tacmeErr := acme.NewError(acme.ErrorUnauthorizedType, \"account 'accountID' is not authorized\")\n\t\t\tacmeErr.Status = http.StatusForbidden\n\t\t\tacmeErr.Detail = \"No authorization provided for name 127.0.0.1\"\n\t\t\treturn test{\n\t\t\t\terr:  nil,\n\t\t\t\tcert: nil,\n\t\t\t\tunauthorizedIdentifiers: []acme.Identifier{\n\t\t\t\t\t{\n\t\t\t\t\t\tType:  acme.IP,\n\t\t\t\t\t\tValue: \"127.0.0.1\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tmsg:  \"account 'accountID' is not authorized\",\n\t\t\t\twant: acmeErr,\n\t\t\t}\n\t\t},\n\t\t\"subject\": func(t *testing.T) test {\n\t\t\tacmeErr := acme.NewError(acme.ErrorUnauthorizedType, \"account 'accountID' is not authorized\")\n\t\t\tacmeErr.Status = http.StatusForbidden\n\t\t\tacmeErr.Detail = \"No authorization provided for name test.example.com\"\n\t\t\tcert := &x509.Certificate{\n\t\t\t\tSubject: pkix.Name{\n\t\t\t\t\tCommonName: \"test.example.com\",\n\t\t\t\t},\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\terr:                     nil,\n\t\t\t\tcert:                    cert,\n\t\t\t\tunauthorizedIdentifiers: []acme.Identifier{},\n\t\t\t\tmsg:                     \"account 'accountID' is not authorized\",\n\t\t\t\twant:                    acmeErr,\n\t\t\t}\n\t\t},\n\t\t\"wrap-subject\": func(t *testing.T) test {\n\t\t\tacmeErr := acme.NewError(acme.ErrorUnauthorizedType, \"verification of jws using certificate public key failed: go-jose/go-jose: error in cryptographic primitive\")\n\t\t\tacmeErr.Status = http.StatusForbidden\n\t\t\tacmeErr.Detail = \"No authorization provided for name test.example.com\"\n\t\t\tcert := &x509.Certificate{\n\t\t\t\tSubject: pkix.Name{\n\t\t\t\t\tCommonName: \"test.example.com\",\n\t\t\t\t},\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\terr:                     errors.New(\"go-jose/go-jose: error in cryptographic primitive\"),\n\t\t\t\tcert:                    cert,\n\t\t\t\tunauthorizedIdentifiers: []acme.Identifier{},\n\t\t\t\tmsg:                     \"verification of jws using certificate public key failed\",\n\t\t\t\twant:                    acmeErr,\n\t\t\t}\n\t\t},\n\t\t\"default\": func(t *testing.T) test {\n\t\t\tacmeErr := acme.NewError(acme.ErrorUnauthorizedType, \"account 'accountID' is not authorized\")\n\t\t\tacmeErr.Status = http.StatusForbidden\n\t\t\tacmeErr.Detail = \"No authorization provided\"\n\t\t\tcert := &x509.Certificate{\n\t\t\t\tSubject: pkix.Name{\n\t\t\t\t\tCommonName: \"\",\n\t\t\t\t},\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\terr:                     nil,\n\t\t\t\tcert:                    cert,\n\t\t\t\tunauthorizedIdentifiers: []acme.Identifier{},\n\t\t\t\tmsg:                     \"account 'accountID' is not authorized\",\n\t\t\t\twant:                    acmeErr,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, prep := range tests {\n\t\ttc := prep(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tacmeErr := wrapUnauthorizedError(tc.cert, tc.unauthorizedIdentifiers, tc.msg, tc.err)\n\t\t\tassert.Equals(t, acmeErr.Err.Error(), tc.want.Err.Error())\n\t\t\tassert.Equals(t, acmeErr.Type, tc.want.Type)\n\t\t\tassert.Equals(t, acmeErr.Status, tc.want.Status)\n\t\t\tassert.Equals(t, acmeErr.Detail, tc.want.Detail)\n\t\t\tassert.Equals(t, acmeErr.Subproblems, tc.want.Subproblems)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "acme/api/wire_integration_test.go",
    "content": "package api\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/ed25519\"\n\t\"crypto/rand\"\n\t\"crypto/x509\"\n\t\"crypto/x509/pkix\"\n\t\"encoding/asn1\"\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"encoding/pem\"\n\t\"errors\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-chi/chi/v5\"\n\t\"github.com/smallstep/certificates/acme\"\n\t\"github.com/smallstep/certificates/acme/db/nosql\"\n\t\"github.com/smallstep/certificates/authority\"\n\t\"github.com/smallstep/certificates/authority/config\"\n\t\"github.com/smallstep/certificates/authority/provisioner\"\n\t\"github.com/smallstep/certificates/authority/provisioner/wire\"\n\tnosqlDB \"github.com/smallstep/nosql\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"go.step.sm/crypto/jose\"\n\t\"go.step.sm/crypto/minica\"\n\t\"go.step.sm/crypto/pemutil\"\n\t\"go.step.sm/crypto/x509util\"\n)\n\nconst (\n\tbaseURL      = \"test.ca.smallstep.com\"\n\tlinkerPrefix = \"acme\"\n)\n\nfunc newWireProvisionerWithOptions(t *testing.T, options *provisioner.Options) *provisioner.ACME {\n\tt.Helper()\n\tprov := &provisioner.ACME{\n\t\tType:    \"ACME\",\n\t\tName:    \"test@acme-<test>provisioner.com\",\n\t\tOptions: options,\n\t\tChallenges: []provisioner.ACMEChallenge{\n\t\t\tprovisioner.WIREOIDC_01,\n\t\t\tprovisioner.WIREDPOP_01,\n\t\t},\n\t}\n\n\terr := prov.Init(provisioner.Config{\n\t\tClaims: config.GlobalProvisionerClaims,\n\t})\n\trequire.NoError(t, err)\n\n\treturn prov\n}\n\n// TODO(hs): replace with test CA server + acmez based test client for\n// more realistic integration test?\nfunc TestWireIntegration(t *testing.T) {\n\taccessTokenSignerJWK, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\trequire.NoError(t, err)\n\n\taccessTokenSignerPEMBlock, err := pemutil.Serialize(accessTokenSignerJWK.Public().Key)\n\trequire.NoError(t, err)\n\taccessTokenSignerPEMBytes := pem.EncodeToMemory(accessTokenSignerPEMBlock)\n\n\taccessTokenSigner, err := jose.NewSigner(jose.SigningKey{\n\t\tAlgorithm: jose.SignatureAlgorithm(accessTokenSignerJWK.Algorithm),\n\t\tKey:       accessTokenSignerJWK,\n\t}, new(jose.SignerOptions))\n\trequire.NoError(t, err)\n\n\toidcTokenSignerJWK, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\trequire.NoError(t, err)\n\toidcTokenSigner, err := jose.NewSigner(jose.SigningKey{\n\t\tAlgorithm: jose.SignatureAlgorithm(oidcTokenSignerJWK.Algorithm),\n\t\tKey:       oidcTokenSignerJWK,\n\t}, new(jose.SignerOptions))\n\trequire.NoError(t, err)\n\n\tprov := newWireProvisionerWithOptions(t, &provisioner.Options{\n\t\tX509: &provisioner.X509Options{\n\t\t\tTemplate: `{\n\t\t\t\t\"subject\": {\n\t\t\t\t\t\"organization\": \"WireTest\",\n\t\t\t\t\t\"commonName\": {{ toJson .Oidc.name }}\n\t\t\t\t},\n\t\t\t\t\"uris\": [{{ toJson .Oidc.preferred_username }}, {{ toJson .Dpop.sub }}],\n\t\t\t\t\"keyUsage\": [\"digitalSignature\"],\n\t\t\t\t\"extKeyUsage\": [\"clientAuth\"]\n\t\t\t}`,\n\t\t},\n\t\tWire: &wire.Options{\n\t\t\tOIDC: &wire.OIDCOptions{\n\t\t\t\tProvider: &wire.Provider{\n\t\t\t\t\tIssuerURL:   \"https://issuer.example.com\",\n\t\t\t\t\tAuthURL:     \"\",\n\t\t\t\t\tTokenURL:    \"\",\n\t\t\t\t\tJWKSURL:     \"\",\n\t\t\t\t\tUserInfoURL: \"\",\n\t\t\t\t\tAlgorithms:  []string{\"ES256\"},\n\t\t\t\t},\n\t\t\t\tConfig: &wire.Config{\n\t\t\t\t\tClientID:                   \"integration test\",\n\t\t\t\t\tSignatureAlgorithms:        []string{\"ES256\"},\n\t\t\t\t\tSkipClientIDCheck:          true,\n\t\t\t\t\tSkipExpiryCheck:            true,\n\t\t\t\t\tSkipIssuerCheck:            true,\n\t\t\t\t\tInsecureSkipSignatureCheck: true, // NOTE: this skips actual token verification\n\t\t\t\t\tNow:                        time.Now,\n\t\t\t\t},\n\t\t\t\tTransformTemplate: \"\",\n\t\t\t},\n\t\t\tDPOP: &wire.DPOPOptions{\n\t\t\t\tSigningKey: accessTokenSignerPEMBytes,\n\t\t\t},\n\t\t},\n\t})\n\n\t// mock provisioner and linker\n\tctx := context.Background()\n\tctx = acme.NewProvisionerContext(ctx, prov)\n\tctx = acme.NewLinkerContext(ctx, acme.NewLinker(baseURL, linkerPrefix))\n\n\t// create temporary BoltDB file\n\tfile, err := os.CreateTemp(os.TempDir(), \"integration-db-\")\n\trequire.NoError(t, err)\n\n\tt.Log(\"database file name:\", file.Name())\n\tdbFn := file.Name()\n\terr = file.Close()\n\trequire.NoError(t, err)\n\n\t// open BoltDB\n\trawDB, err := nosqlDB.New(nosqlDB.BBoltDriver, dbFn)\n\trequire.NoError(t, err)\n\n\t// create tables\n\tdb, err := nosql.New(rawDB)\n\trequire.NoError(t, err)\n\n\t// make DB available to handlers\n\tctx = acme.NewDatabaseContext(ctx, db)\n\n\t// simulate signed payloads by making the signing key available in ctx\n\tjwk, err := jose.GenerateJWK(\"OKP\", \"\", \"EdDSA\", \"sig\", \"\", 0)\n\trequire.NoError(t, err)\n\n\ted25519PrivKey, ok := jwk.Key.(ed25519.PrivateKey)\n\trequire.True(t, ok)\n\n\tdpopSigner, err := jose.NewSigner(jose.SigningKey{\n\t\tAlgorithm: jose.SignatureAlgorithm(jwk.Algorithm),\n\t\tKey:       jwk,\n\t}, new(jose.SignerOptions))\n\trequire.NoError(t, err)\n\n\ted25519PubKey, ok := ed25519PrivKey.Public().(ed25519.PublicKey)\n\trequire.True(t, ok)\n\n\tjwk.Key = ed25519PubKey\n\tctx = context.WithValue(ctx, jwkContextKey, jwk)\n\n\t// get directory\n\tdir := func(ctx context.Context) (dir Directory) {\n\t\treq := httptest.NewRequest(http.MethodGet, \"/foo/bar\", http.NoBody)\n\t\treq = req.WithContext(ctx)\n\t\tw := httptest.NewRecorder()\n\n\t\tGetDirectory(w, req)\n\t\tres := w.Result()\n\t\trequire.Equal(t, http.StatusOK, res.StatusCode)\n\n\t\tbody, err := io.ReadAll(res.Body)\n\t\trequire.NoError(t, err)\n\n\t\terr = json.Unmarshal(bytes.TrimSpace(body), &dir)\n\t\trequire.NoError(t, err)\n\n\t\treturn\n\t}(ctx)\n\tt.Log(\"directory:\", dir)\n\n\t// get nonce\n\tnonce := func(ctx context.Context) (nonce string) {\n\t\treq := httptest.NewRequest(http.MethodGet, dir.NewNonce, http.NoBody).WithContext(ctx)\n\t\tw := httptest.NewRecorder()\n\t\taddNonce(GetNonce)(w, req)\n\t\tres := w.Result()\n\t\trequire.Equal(t, http.StatusNoContent, res.StatusCode)\n\n\t\tnonce = res.Header[\"Replay-Nonce\"][0]\n\t\treturn\n\t}(ctx)\n\tt.Log(\"nonce:\", nonce)\n\n\t// create new account\n\tacc := func(ctx context.Context) (acc *acme.Account) {\n\t\t// create payload\n\t\tnar := &NewAccountRequest{\n\t\t\tContact: []string{\"foo\", \"bar\"},\n\t\t}\n\t\trawNar, err := json.Marshal(nar)\n\t\trequire.NoError(t, err)\n\n\t\t// create account\n\t\tctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: rawNar})\n\t\treq := httptest.NewRequest(http.MethodGet, dir.NewAccount, http.NoBody).WithContext(ctx)\n\t\tw := httptest.NewRecorder()\n\t\tNewAccount(w, req)\n\n\t\tres := w.Result()\n\t\trequire.Equal(t, http.StatusCreated, res.StatusCode)\n\n\t\tbody, err := io.ReadAll(res.Body)\n\t\tdefer res.Body.Close()\n\t\trequire.NoError(t, err)\n\n\t\terr = json.Unmarshal(bytes.TrimSpace(body), &acc)\n\t\trequire.NoError(t, err)\n\n\t\tlocationParts := strings.Split(res.Header[\"Location\"][0], \"/\")\n\t\tacc, err = db.GetAccount(ctx, locationParts[len(locationParts)-1])\n\t\trequire.NoError(t, err)\n\n\t\treturn\n\t}(ctx)\n\tctx = context.WithValue(ctx, accContextKey, acc)\n\tt.Log(\"account ID:\", acc.ID)\n\n\t// new order\n\torder := func(ctx context.Context) (order *acme.Order) {\n\t\tmockMustAuthority(t, &mockCA{})\n\t\tnor := &NewOrderRequest{\n\t\t\tIdentifiers: []acme.Identifier{\n\t\t\t\t{\n\t\t\t\t\tType:  \"wireapp-user\",\n\t\t\t\t\tValue: `{\"name\": \"Smith, Alice M (QA)\", \"domain\": \"example.com\", \"handle\": \"wireapp://%40alice.smith.qa@example.com\"}`,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tType:  \"wireapp-device\",\n\t\t\t\t\tValue: `{\"name\": \"Smith, Alice M (QA)\", \"domain\": \"example.com\", \"client-id\": \"wireapp://lJGYPz0ZRq2kvc_XpdaDlA!ed416ce8ecdd9fad@example.com\", \"handle\": \"wireapp://%40alice.smith.qa@example.com\"}`,\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\t\tb, err := json.Marshal(nor)\n\t\trequire.NoError(t, err)\n\n\t\tctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b})\n\t\treq := httptest.NewRequest(\"POST\", \"https://random.local/\", http.NoBody)\n\t\treq = req.WithContext(ctx)\n\t\tw := httptest.NewRecorder()\n\t\tNewOrder(w, req)\n\n\t\tres := w.Result()\n\t\trequire.Equal(t, http.StatusCreated, res.StatusCode)\n\n\t\tbody, err := io.ReadAll(res.Body)\n\t\tdefer res.Body.Close()\n\t\trequire.NoError(t, err)\n\n\t\terr = json.Unmarshal(bytes.TrimSpace(body), &order)\n\t\trequire.NoError(t, err)\n\n\t\torder, err = db.GetOrder(ctx, order.ID)\n\t\trequire.NoError(t, err)\n\n\t\treturn\n\t}(ctx)\n\tt.Log(\"authzs IDs:\", order.AuthorizationIDs)\n\n\t// get authorization\n\tgetAuthz := func(ctx context.Context, authzID string) (az *acme.Authorization) {\n\t\tchiCtx := chi.NewRouteContext()\n\t\tchiCtx.URLParams.Add(\"authzID\", authzID)\n\t\tctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx)\n\n\t\treq := httptest.NewRequest(http.MethodGet, \"https://random.local/\", http.NoBody).WithContext(ctx)\n\t\tw := httptest.NewRecorder()\n\t\tGetAuthorization(w, req)\n\n\t\tres := w.Result()\n\t\trequire.Equal(t, http.StatusOK, res.StatusCode)\n\n\t\tbody, err := io.ReadAll(res.Body)\n\t\tdefer res.Body.Close()\n\t\trequire.NoError(t, err)\n\n\t\terr = json.Unmarshal(bytes.TrimSpace(body), &az)\n\t\trequire.NoError(t, err)\n\n\t\taz, err = db.GetAuthorization(ctx, authzID)\n\t\trequire.NoError(t, err)\n\n\t\treturn\n\t}\n\tvar azs []*acme.Authorization\n\tfor _, azID := range order.AuthorizationIDs {\n\t\taz := getAuthz(ctx, azID)\n\t\tazs = append(azs, az)\n\t\tfor _, challenge := range az.Challenges {\n\t\t\tchiCtx := chi.NewRouteContext()\n\t\t\tchiCtx.URLParams.Add(\"chID\", challenge.ID)\n\t\t\tctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx)\n\n\t\t\tvar payload []byte\n\t\t\tswitch challenge.Type {\n\t\t\tcase acme.WIREDPOP01:\n\t\t\t\tdpopBytes, err := json.Marshal(struct {\n\t\t\t\t\tjose.Claims\n\t\t\t\t\tChallenge string `json:\"chal,omitempty\"`\n\t\t\t\t\tHandle    string `json:\"handle,omitempty\"`\n\t\t\t\t\tNonce     string `json:\"nonce,omitempty\"`\n\t\t\t\t\tHTU       string `json:\"htu,omitempty\"`\n\t\t\t\t}{\n\t\t\t\t\tClaims: jose.Claims{\n\t\t\t\t\t\tSubject: \"wireapp://lJGYPz0ZRq2kvc_XpdaDlA!ed416ce8ecdd9fad@example.com\",\n\t\t\t\t\t},\n\t\t\t\t\tChallenge: \"token\",\n\t\t\t\t\tHandle:    \"wireapp://%40alice.smith.qa@example.com\",\n\t\t\t\t\tNonce:     \"nonce\",\n\t\t\t\t\tHTU:       \"http://issuer.example.com\",\n\t\t\t\t})\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tdpop, err := dpopSigner.Sign(dpopBytes)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tproof, err := dpop.CompactSerialize()\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\ttokenBytes, err := json.Marshal(struct {\n\t\t\t\t\tjose.Claims\n\t\t\t\t\tChallenge string `json:\"chal,omitempty\"`\n\t\t\t\t\tCnf       struct {\n\t\t\t\t\t\tKid string `json:\"kid,omitempty\"`\n\t\t\t\t\t} `json:\"cnf\"`\n\t\t\t\t\tProof      string `json:\"proof,omitempty\"`\n\t\t\t\t\tClientID   string `json:\"client_id\"`\n\t\t\t\t\tAPIVersion int    `json:\"api_version\"`\n\t\t\t\t\tScope      string `json:\"scope\"`\n\t\t\t\t}{\n\t\t\t\t\tClaims: jose.Claims{\n\t\t\t\t\t\tIssuer:   \"http://issuer.example.com\",\n\t\t\t\t\t\tAudience: []string{\"test\"},\n\t\t\t\t\t\tExpiry:   jose.NewNumericDate(time.Now().Add(1 * time.Minute)),\n\t\t\t\t\t},\n\t\t\t\t\tChallenge: \"token\",\n\t\t\t\t\tCnf: struct {\n\t\t\t\t\t\tKid string `json:\"kid,omitempty\"`\n\t\t\t\t\t}{\n\t\t\t\t\t\tKid: jwk.KeyID,\n\t\t\t\t\t},\n\t\t\t\t\tProof:      proof,\n\t\t\t\t\tClientID:   \"wireapp://lJGYPz0ZRq2kvc_XpdaDlA!ed416ce8ecdd9fad@example.com\",\n\t\t\t\t\tAPIVersion: 5,\n\t\t\t\t\tScope:      \"wire_client_id\",\n\t\t\t\t})\n\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tsigned, err := accessTokenSigner.Sign(tokenBytes)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\taccessToken, err := signed.CompactSerialize()\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\tp, err := json.Marshal(struct {\n\t\t\t\t\tAccessToken string `json:\"access_token\"`\n\t\t\t\t}{\n\t\t\t\t\tAccessToken: accessToken,\n\t\t\t\t})\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tpayload = p\n\t\t\tcase acme.WIREOIDC01:\n\t\t\t\tkeyAuth, err := acme.KeyAuthorization(\"token\", jwk)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\ttokenBytes, err := json.Marshal(struct {\n\t\t\t\t\tjose.Claims\n\t\t\t\t\tName              string `json:\"name,omitempty\"`\n\t\t\t\t\tPreferredUsername string `json:\"preferred_username,omitempty\"`\n\t\t\t\t\tKeyAuth           string `json:\"keyauth\"`\n\t\t\t\t}{\n\t\t\t\t\tClaims: jose.Claims{\n\t\t\t\t\t\tIssuer:   \"https://issuer.example.com\",\n\t\t\t\t\t\tAudience: []string{\"test\"},\n\t\t\t\t\t\tExpiry:   jose.NewNumericDate(time.Now().Add(1 * time.Minute)),\n\t\t\t\t\t},\n\t\t\t\t\tName:              \"Alice Smith\",\n\t\t\t\t\tPreferredUsername: \"wireapp://%40alice_wire@wire.com\",\n\t\t\t\t\tKeyAuth:           keyAuth,\n\t\t\t\t})\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tsigned, err := oidcTokenSigner.Sign(tokenBytes)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tidToken, err := signed.CompactSerialize()\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tp, err := json.Marshal(struct {\n\t\t\t\t\tIDToken string `json:\"id_token\"`\n\t\t\t\t}{\n\t\t\t\t\tIDToken: idToken,\n\t\t\t\t})\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tpayload = p\n\t\t\tdefault:\n\t\t\t\trequire.Fail(t, \"unexpected challenge payload type\")\n\t\t\t}\n\n\t\t\tctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: payload})\n\n\t\t\treq := httptest.NewRequest(http.MethodGet, \"https://random.local/\", http.NoBody).WithContext(ctx)\n\t\t\tw := httptest.NewRecorder()\n\t\t\tGetChallenge(w, req)\n\n\t\t\tres := w.Result()\n\t\t\trequire.Equal(t, http.StatusOK, res.StatusCode)\n\n\t\t\tbody, err := io.ReadAll(res.Body)\n\t\t\tdefer res.Body.Close() //nolint:gocritic // close the body\n\t\t\trequire.NoError(t, err)\n\n\t\t\terr = json.Unmarshal(bytes.TrimSpace(body), &challenge)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tt.Log(\"challenge:\", challenge.ID, challenge.Status)\n\t\t}\n\t}\n\n\t// get/validate challenge simulation\n\tupdateAz := func(ctx context.Context, az *acme.Authorization) (updatedAz *acme.Authorization) {\n\t\tnow := clock.Now().Format(time.RFC3339)\n\t\tfor _, challenge := range az.Challenges {\n\t\t\tchallenge.Status = acme.StatusValid\n\t\t\tchallenge.ValidatedAt = now\n\t\t\terr := db.UpdateChallenge(ctx, challenge)\n\t\t\tif err != nil {\n\t\t\t\tt.Error(\"updating challenge\", challenge.ID, \":\", err)\n\t\t\t}\n\t\t}\n\n\t\tupdatedAz, err = db.GetAuthorization(ctx, az.ID)\n\t\trequire.NoError(t, err)\n\n\t\treturn\n\t}\n\tfor _, az := range azs {\n\t\tupdatedAz := updateAz(ctx, az)\n\t\tfor _, challenge := range updatedAz.Challenges {\n\t\t\tt.Log(\"updated challenge:\", challenge.ID, challenge.Status)\n\t\t\tswitch challenge.Type {\n\t\t\tcase acme.WIREOIDC01:\n\t\t\t\terr = db.CreateOidcToken(ctx, order.ID, map[string]any{\"name\": \"Smith, Alice M (QA)\", \"preferred_username\": \"wireapp://%40alice.smith.qa@example.com\"})\n\t\t\t\trequire.NoError(t, err)\n\t\t\tcase acme.WIREDPOP01:\n\t\t\t\terr = db.CreateDpopToken(ctx, order.ID, map[string]any{\"sub\": \"wireapp://lJGYPz0ZRq2kvc_XpdaDlA!ed416ce8ecdd9fad@example.com\"})\n\t\t\t\trequire.NoError(t, err)\n\t\t\tdefault:\n\t\t\t\trequire.Fail(t, \"unexpected challenge type\")\n\t\t\t}\n\t\t}\n\t}\n\n\t// get order\n\tupdatedOrder := func(ctx context.Context) (updatedOrder *acme.Order) {\n\t\tchiCtx := chi.NewRouteContext()\n\t\tchiCtx.URLParams.Add(\"ordID\", order.ID)\n\t\tctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx)\n\n\t\treq := httptest.NewRequest(http.MethodGet, \"https://random.local/\", http.NoBody).WithContext(ctx)\n\t\tw := httptest.NewRecorder()\n\t\tGetOrder(w, req)\n\n\t\tres := w.Result()\n\t\trequire.Equal(t, http.StatusOK, res.StatusCode)\n\n\t\tbody, err := io.ReadAll(res.Body)\n\t\tdefer res.Body.Close()\n\t\trequire.NoError(t, err)\n\n\t\terr = json.Unmarshal(bytes.TrimSpace(body), &updatedOrder)\n\t\trequire.NoError(t, err)\n\n\t\trequire.Equal(t, acme.StatusReady, updatedOrder.Status)\n\n\t\treturn\n\t}(ctx)\n\tt.Log(\"updated order status:\", updatedOrder.Status)\n\n\t// finalize order\n\tfinalizedOrder := func(ctx context.Context) (finalizedOrder *acme.Order) {\n\t\tca, err := minica.New(minica.WithName(\"WireTestCA\"))\n\t\trequire.NoError(t, err)\n\t\tmockMustAuthority(t, &mockCASigner{\n\t\t\tsigner: func(csr *x509.CertificateRequest, signOpts provisioner.SignOptions, extraOpts ...provisioner.SignOption) ([]*x509.Certificate, error) {\n\t\t\t\tvar (\n\t\t\t\t\tcertOptions []x509util.Option\n\t\t\t\t)\n\t\t\t\tfor _, op := range extraOpts {\n\t\t\t\t\tif k, ok := op.(provisioner.CertificateOptions); ok {\n\t\t\t\t\t\tcertOptions = append(certOptions, k.Options(signOpts)...)\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tx509utilTemplate, err := x509util.NewCertificate(csr, certOptions...)\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\ttemplate := x509utilTemplate.GetCertificate()\n\t\t\t\trequire.NotNil(t, template)\n\n\t\t\t\tcert, err := ca.Sign(template)\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\tu1, err := url.Parse(\"wireapp://%40alice.smith.qa@example.com\")\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tu2, err := url.Parse(\"wireapp://lJGYPz0ZRq2kvc_XpdaDlA%21ed416ce8ecdd9fad@example.com\")\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tassert.Equal(t, []*url.URL{u1, u2}, cert.URIs)\n\t\t\t\tassert.Equal(t, \"Smith, Alice M (QA)\", cert.Subject.CommonName)\n\n\t\t\t\treturn []*x509.Certificate{cert, ca.Intermediate}, nil\n\t\t\t},\n\t\t})\n\n\t\tqUserID, err := url.Parse(\"wireapp://lJGYPz0ZRq2kvc_XpdaDlA!ed416ce8ecdd9fad@example.com\")\n\t\trequire.NoError(t, err)\n\n\t\tqUserName, err := url.Parse(\"wireapp://%40alice.smith.qa@example.com\")\n\t\trequire.NoError(t, err)\n\n\t\t_, priv, err := ed25519.GenerateKey(rand.Reader)\n\t\trequire.NoError(t, err)\n\n\t\tcsrTemplate := &x509.CertificateRequest{\n\t\t\tSubject: pkix.Name{\n\t\t\t\tOrganization: []string{\"example.com\"},\n\t\t\t\tExtraNames: []pkix.AttributeTypeAndValue{\n\t\t\t\t\t{\n\t\t\t\t\t\tType:  asn1.ObjectIdentifier{2, 16, 840, 1, 113730, 3, 1, 241},\n\t\t\t\t\t\tValue: \"Smith, Alice M (QA)\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tURIs: []*url.URL{\n\t\t\t\tqUserName,\n\t\t\t\tqUserID,\n\t\t\t},\n\t\t\tSignatureAlgorithm: x509.PureEd25519,\n\t\t}\n\n\t\tcsr, err := x509.CreateCertificateRequest(rand.Reader, csrTemplate, priv)\n\t\trequire.NoError(t, err)\n\n\t\tfr := FinalizeRequest{CSR: base64.RawURLEncoding.EncodeToString(csr)}\n\t\tfrRaw, err := json.Marshal(fr)\n\t\trequire.NoError(t, err)\n\n\t\tctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: frRaw})\n\n\t\tchiCtx := chi.NewRouteContext()\n\t\tchiCtx.URLParams.Add(\"ordID\", order.ID)\n\t\tctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx)\n\n\t\treq := httptest.NewRequest(http.MethodGet, \"https://random.local/\", http.NoBody).WithContext(ctx)\n\t\tw := httptest.NewRecorder()\n\t\tFinalizeOrder(w, req)\n\n\t\tres := w.Result()\n\t\trequire.Equal(t, http.StatusOK, res.StatusCode)\n\n\t\tbody, err := io.ReadAll(res.Body)\n\t\tdefer res.Body.Close()\n\t\trequire.NoError(t, err)\n\n\t\terr = json.Unmarshal(bytes.TrimSpace(body), &finalizedOrder)\n\t\trequire.NoError(t, err)\n\n\t\trequire.Equal(t, acme.StatusValid, finalizedOrder.Status)\n\n\t\tfinalizedOrder, err = db.GetOrder(ctx, order.ID)\n\t\trequire.NoError(t, err)\n\n\t\treturn\n\t}(ctx)\n\tt.Log(\"finalized order status:\", finalizedOrder.Status)\n}\n\ntype mockCASigner struct {\n\tsigner func(*x509.CertificateRequest, provisioner.SignOptions, ...provisioner.SignOption) ([]*x509.Certificate, error)\n}\n\nfunc (m *mockCASigner) SignWithContext(_ context.Context, cr *x509.CertificateRequest, opts provisioner.SignOptions, signOpts ...provisioner.SignOption) ([]*x509.Certificate, error) {\n\tif m.signer == nil {\n\t\treturn nil, errors.New(\"unimplemented\")\n\t}\n\treturn m.signer(cr, opts, signOpts...)\n}\n\nfunc (m *mockCASigner) AreSANsAllowed(ctx context.Context, sans []string) error {\n\treturn nil\n}\n\nfunc (m *mockCASigner) IsRevoked(sn string) (bool, error) {\n\treturn false, nil\n}\n\nfunc (m *mockCASigner) Revoke(ctx context.Context, opts *authority.RevokeOptions) error {\n\treturn nil\n}\n\nfunc (m *mockCASigner) LoadProvisionerByName(string) (provisioner.Interface, error) {\n\treturn nil, nil\n}\n\nfunc (m *mockCASigner) GetBackdate() *time.Duration {\n\treturn nil\n}\n"
  },
  {
    "path": "acme/authorization.go",
    "content": "package acme\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"time\"\n)\n\n// Authorization representst an ACME Authorization.\ntype Authorization struct {\n\tID          string       `json:\"-\"`\n\tAccountID   string       `json:\"-\"`\n\tToken       string       `json:\"-\"`\n\tFingerprint string       `json:\"-\"`\n\tIdentifier  Identifier   `json:\"identifier\"`\n\tStatus      Status       `json:\"status\"`\n\tChallenges  []*Challenge `json:\"challenges\"`\n\tWildcard    bool         `json:\"wildcard\"`\n\tExpiresAt   time.Time    `json:\"expires\"`\n\tError       *Error       `json:\"error,omitempty\"`\n}\n\n// ToLog enables response logging.\nfunc (az *Authorization) ToLog() (interface{}, error) {\n\tb, err := json.Marshal(az)\n\tif err != nil {\n\t\treturn nil, WrapErrorISE(err, \"error marshaling authz for logging\")\n\t}\n\treturn string(b), nil\n}\n\n// UpdateStatus updates the ACME Authorization Status if necessary.\n// Changes to the Authorization are saved using the database interface.\nfunc (az *Authorization) UpdateStatus(ctx context.Context, db DB) error {\n\tnow := clock.Now()\n\n\tswitch az.Status {\n\tcase StatusInvalid:\n\t\treturn nil\n\tcase StatusValid:\n\t\treturn nil\n\tcase StatusPending:\n\t\t// check expiry\n\t\tif now.After(az.ExpiresAt) {\n\t\t\taz.Status = StatusInvalid\n\t\t\tbreak\n\t\t}\n\n\t\tvar isValid = false\n\t\tfor _, ch := range az.Challenges {\n\t\t\tif ch.Status == StatusValid {\n\t\t\t\tisValid = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\tif !isValid {\n\t\t\treturn nil\n\t\t}\n\t\taz.Status = StatusValid\n\t\taz.Error = nil\n\tdefault:\n\t\treturn NewErrorISE(\"unrecognized authorization status: %s\", az.Status)\n\t}\n\n\tif err := db.UpdateAuthorization(ctx, az); err != nil {\n\t\treturn WrapErrorISE(err, \"error updating authorization\")\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "acme/authorization_test.go",
    "content": "package acme\n\nimport (\n\t\"context\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n\t\"github.com/smallstep/assert\"\n)\n\nfunc TestAuthorization_UpdateStatus(t *testing.T) {\n\ttype test struct {\n\t\taz  *Authorization\n\t\terr *Error\n\t\tdb  DB\n\t}\n\ttests := map[string]func(t *testing.T) test{\n\t\t\"ok/already-invalid\": func(t *testing.T) test {\n\t\t\taz := &Authorization{\n\t\t\t\tStatus: StatusInvalid,\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\taz: az,\n\t\t\t}\n\t\t},\n\t\t\"ok/already-valid\": func(t *testing.T) test {\n\t\t\taz := &Authorization{\n\t\t\t\tStatus: StatusInvalid,\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\taz: az,\n\t\t\t}\n\t\t},\n\t\t\"fail/error-unexpected-status\": func(t *testing.T) test {\n\t\t\taz := &Authorization{\n\t\t\t\tStatus: \"foo\",\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\taz:  az,\n\t\t\t\terr: NewErrorISE(\"unrecognized authorization status: %s\", az.Status),\n\t\t\t}\n\t\t},\n\t\t\"ok/expired\": func(t *testing.T) test {\n\t\t\tnow := clock.Now()\n\t\t\taz := &Authorization{\n\t\t\t\tID:        \"azID\",\n\t\t\t\tAccountID: \"accID\",\n\t\t\t\tStatus:    StatusPending,\n\t\t\t\tExpiresAt: now.Add(-5 * time.Minute),\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\taz: az,\n\t\t\t\tdb: &MockDB{\n\t\t\t\t\tMockUpdateAuthorization: func(ctx context.Context, updaz *Authorization) error {\n\t\t\t\t\t\tassert.Equals(t, updaz.ID, az.ID)\n\t\t\t\t\t\tassert.Equals(t, updaz.AccountID, az.AccountID)\n\t\t\t\t\t\tassert.Equals(t, updaz.Status, StatusInvalid)\n\t\t\t\t\t\tassert.Equals(t, updaz.ExpiresAt, az.ExpiresAt)\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"fail/db.UpdateAuthorization-error\": func(t *testing.T) test {\n\t\t\tnow := clock.Now()\n\t\t\taz := &Authorization{\n\t\t\t\tID:        \"azID\",\n\t\t\t\tAccountID: \"accID\",\n\t\t\t\tStatus:    StatusPending,\n\t\t\t\tExpiresAt: now.Add(-5 * time.Minute),\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\taz: az,\n\t\t\t\tdb: &MockDB{\n\t\t\t\t\tMockUpdateAuthorization: func(ctx context.Context, updaz *Authorization) error {\n\t\t\t\t\t\tassert.Equals(t, updaz.ID, az.ID)\n\t\t\t\t\t\tassert.Equals(t, updaz.AccountID, az.AccountID)\n\t\t\t\t\t\tassert.Equals(t, updaz.Status, StatusInvalid)\n\t\t\t\t\t\tassert.Equals(t, updaz.ExpiresAt, az.ExpiresAt)\n\t\t\t\t\t\treturn errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: NewErrorISE(\"error updating authorization: force\"),\n\t\t\t}\n\t\t},\n\t\t\"ok/no-valid-challenges\": func(t *testing.T) test {\n\t\t\tnow := clock.Now()\n\t\t\taz := &Authorization{\n\t\t\t\tID:        \"azID\",\n\t\t\t\tAccountID: \"accID\",\n\t\t\t\tStatus:    StatusPending,\n\t\t\t\tExpiresAt: now.Add(5 * time.Minute),\n\t\t\t\tChallenges: []*Challenge{\n\t\t\t\t\t{Status: StatusPending}, {Status: StatusPending}, {Status: StatusPending},\n\t\t\t\t},\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\taz: az,\n\t\t\t}\n\t\t},\n\t\t\"ok/valid\": func(t *testing.T) test {\n\t\t\tnow := clock.Now()\n\t\t\taz := &Authorization{\n\t\t\t\tID:        \"azID\",\n\t\t\t\tAccountID: \"accID\",\n\t\t\t\tStatus:    StatusPending,\n\t\t\t\tExpiresAt: now.Add(5 * time.Minute),\n\t\t\t\tChallenges: []*Challenge{\n\t\t\t\t\t{Status: StatusPending}, {Status: StatusPending}, {Status: StatusValid},\n\t\t\t\t},\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\taz: az,\n\t\t\t\tdb: &MockDB{\n\t\t\t\t\tMockUpdateAuthorization: func(ctx context.Context, updaz *Authorization) error {\n\t\t\t\t\t\tassert.Equals(t, updaz.ID, az.ID)\n\t\t\t\t\t\tassert.Equals(t, updaz.AccountID, az.AccountID)\n\t\t\t\t\t\tassert.Equals(t, updaz.Status, StatusValid)\n\t\t\t\t\t\tassert.Equals(t, updaz.ExpiresAt, az.ExpiresAt)\n\t\t\t\t\t\tassert.Equals(t, updaz.Error, nil)\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ttc := run(t)\n\t\t\tif err := tc.az.UpdateStatus(context.Background(), tc.db); err != nil {\n\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\tvar k *Error\n\t\t\t\t\tif errors.As(err, &k) {\n\t\t\t\t\t\tassert.Equals(t, k.Type, tc.err.Type)\n\t\t\t\t\t\tassert.Equals(t, k.Detail, tc.err.Detail)\n\t\t\t\t\t\tassert.Equals(t, k.Status, tc.err.Status)\n\t\t\t\t\t\tassert.Equals(t, k.Err.Error(), tc.err.Err.Error())\n\t\t\t\t\t\tassert.Equals(t, k.Detail, tc.err.Detail)\n\t\t\t\t\t} else {\n\t\t\t\t\t\tassert.FatalError(t, errors.New(\"unexpected error type\"))\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tassert.Nil(t, tc.err)\n\t\t\t}\n\t\t})\n\n\t}\n}\n"
  },
  {
    "path": "acme/certificate.go",
    "content": "package acme\n\nimport (\n\t\"crypto/x509\"\n)\n\n// Certificate options with which to create and store a cert object.\ntype Certificate struct {\n\tID            string\n\tAccountID     string\n\tOrderID       string\n\tLeaf          *x509.Certificate\n\tIntermediates []*x509.Certificate\n}\n"
  },
  {
    "path": "acme/challenge.go",
    "content": "package acme\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto\"\n\t\"crypto/ecdsa\"\n\t\"crypto/ed25519\"\n\t\"crypto/elliptic\"\n\t\"crypto/rsa\"\n\t\"crypto/sha256\"\n\t\"crypto/subtle\"\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"encoding/asn1\"\n\t\"encoding/base64\"\n\t\"encoding/hex\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"net/url\"\n\t\"reflect\"\n\t\"slices\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/coreos/go-oidc/v3/oidc\"\n\t\"github.com/fxamacker/cbor/v2\"\n\t\"github.com/google/go-tpm/legacy/tpm2\"\n\n\t\"github.com/smallstep/go-attestation/attest\"\n\t\"go.step.sm/crypto/jose\"\n\t\"go.step.sm/crypto/keyutil\"\n\t\"go.step.sm/crypto/pemutil\"\n\t\"go.step.sm/crypto/x509util\"\n\n\t\"github.com/smallstep/certificates/acme/wire\"\n\t\"github.com/smallstep/certificates/authority/provisioner\"\n\twireprovisioner \"github.com/smallstep/certificates/authority/provisioner/wire\"\n\t\"github.com/smallstep/certificates/internal/cast\"\n)\n\ntype ChallengeType string\n\nconst (\n\t// HTTP01 is the http-01 ACME challenge type\n\tHTTP01 ChallengeType = \"http-01\"\n\t// DNS01 is the dns-01 ACME challenge type\n\tDNS01 ChallengeType = \"dns-01\"\n\t// TLSALPN01 is the tls-alpn-01 ACME challenge type\n\tTLSALPN01 ChallengeType = \"tls-alpn-01\"\n\t// DEVICEATTEST01 is the device-attest-01 ACME challenge type\n\tDEVICEATTEST01 ChallengeType = \"device-attest-01\"\n\t// WIREOIDC01 is the Wire OIDC challenge type\n\tWIREOIDC01 ChallengeType = \"wire-oidc-01\"\n\t// WIREDPOP01 is the Wire DPoP challenge type\n\tWIREDPOP01 ChallengeType = \"wire-dpop-01\"\n)\n\nvar (\n\t// InsecurePortHTTP01 is the port used to verify http-01 challenges. If not set it\n\t// defaults to 80.\n\tInsecurePortHTTP01 int\n\n\t// InsecurePortTLSALPN01 is the port used to verify tls-alpn-01 challenges. If not\n\t// set it defaults to 443.\n\t//\n\t// This variable can be used for testing purposes.\n\tInsecurePortTLSALPN01 int\n\n\t// StrictFQDN allows to enforce a fully qualified domain name in the DNS\n\t// resolution. By default it allows domain resolution using a search list\n\t// defined in the resolv.conf or similar configuration.\n\tStrictFQDN bool\n)\n\n// Challenge represents an ACME response Challenge type.\ntype Challenge struct {\n\tID              string        `json:\"-\"`\n\tAccountID       string        `json:\"-\"`\n\tAuthorizationID string        `json:\"-\"`\n\tValue           string        `json:\"-\"`\n\tType            ChallengeType `json:\"type\"`\n\tStatus          Status        `json:\"status\"`\n\tToken           string        `json:\"token\"`\n\tValidatedAt     string        `json:\"validated,omitempty\"`\n\tURL             string        `json:\"url\"`\n\tTarget          string        `json:\"target,omitempty\"`\n\tError           *Error        `json:\"error,omitempty\"`\n\tPayload         []byte        `json:\"-\"`\n\tPayloadFormat   string        `json:\"-\"`\n}\n\n// ToLog enables response logging.\nfunc (ch *Challenge) ToLog() (interface{}, error) {\n\tb, err := json.Marshal(ch)\n\tif err != nil {\n\t\treturn nil, WrapErrorISE(err, \"error marshaling challenge for logging\")\n\t}\n\treturn string(b), nil\n}\n\n// Validate attempts to validate the Challenge. Stores changes to the Challenge\n// type using the DB interface. If the Challenge is validated, the 'status' and\n// 'validated' attributes are updated.\nfunc (ch *Challenge) Validate(ctx context.Context, db DB, jwk *jose.JSONWebKey, payload []byte) error {\n\t// If already valid or invalid then return without performing validation.\n\tif ch.Status != StatusPending {\n\t\treturn nil\n\t}\n\tswitch ch.Type {\n\tcase HTTP01:\n\t\treturn http01Validate(ctx, ch, db, jwk)\n\tcase DNS01:\n\t\treturn dns01Validate(ctx, ch, db, jwk)\n\tcase TLSALPN01:\n\t\treturn tlsalpn01Validate(ctx, ch, db, jwk)\n\tcase DEVICEATTEST01:\n\t\treturn deviceAttest01Validate(ctx, ch, db, jwk, payload)\n\tcase WIREOIDC01:\n\t\twireDB, ok := db.(WireDB)\n\t\tif !ok {\n\t\t\treturn NewErrorISE(\"db %T is not a WireDB\", db)\n\t\t}\n\t\treturn wireOIDC01Validate(ctx, ch, wireDB, jwk, payload)\n\tcase WIREDPOP01:\n\t\twireDB, ok := db.(WireDB)\n\t\tif !ok {\n\t\t\treturn NewErrorISE(\"db %T is not a WireDB\", db)\n\t\t}\n\t\treturn wireDPOP01Validate(ctx, ch, wireDB, jwk, payload)\n\tdefault:\n\t\treturn NewErrorISE(\"unexpected challenge type %q\", ch.Type)\n\t}\n}\n\nfunc http01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose.JSONWebKey) error {\n\tu := &url.URL{Scheme: \"http\", Host: ch.Value, Path: fmt.Sprintf(\"/.well-known/acme-challenge/%s\", ch.Token)}\n\tchallengeURL := &url.URL{Scheme: \"http\", Host: http01ChallengeHost(ch.Value), Path: fmt.Sprintf(\"/.well-known/acme-challenge/%s\", ch.Token)}\n\n\t// Append insecure port if set.\n\t// Only used for testing purposes.\n\tif InsecurePortHTTP01 != 0 {\n\t\tinsecurePort := strconv.Itoa(InsecurePortHTTP01)\n\t\tu.Host += \":\" + insecurePort\n\t\tchallengeURL.Host += \":\" + insecurePort\n\t}\n\n\tvc := MustClientFromContext(ctx)\n\tresp, err := vc.Get(challengeURL.String())\n\tif err != nil {\n\t\treturn storeError(ctx, db, ch, false, WrapError(ErrorConnectionType, err,\n\t\t\t\"error doing http GET for url %s\", u))\n\t}\n\tdefer resp.Body.Close()\n\tif resp.StatusCode >= 400 {\n\t\treturn storeError(ctx, db, ch, false, NewError(ErrorConnectionType,\n\t\t\t\"error doing http GET for url %s with status code %d\", u, resp.StatusCode))\n\t}\n\n\tbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn WrapErrorISE(err, \"error reading \"+\n\t\t\t\"response body for url %s\", u)\n\t}\n\tkeyAuth := strings.TrimSpace(string(body))\n\n\texpected, err := KeyAuthorization(ch.Token, jwk)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif keyAuth != expected {\n\t\treturn storeError(ctx, db, ch, true, NewError(ErrorRejectedIdentifierType,\n\t\t\t\"keyAuthorization does not match; expected %s, but got %s\", expected, keyAuth))\n\t}\n\n\t// Update and store the challenge.\n\tch.Status = StatusValid\n\tch.Error = nil\n\tch.ValidatedAt = clock.Now().Format(time.RFC3339)\n\n\tif err = db.UpdateChallenge(ctx, ch); err != nil {\n\t\treturn WrapErrorISE(err, \"error updating challenge\")\n\t}\n\treturn nil\n}\n\n// rootedName adds a trailing \".\" to a given domain name.\nfunc rootedName(name string) string {\n\tif StrictFQDN {\n\t\tif name == \"\" || name[len(name)-1] != '.' {\n\t\t\treturn name + \".\"\n\t\t}\n\t}\n\treturn name\n}\n\n// http01ChallengeHost checks if a Challenge value is an IPv6 address\n// and adds square brackets if that's the case, so that it can be used\n// as a hostname. Returns the original Challenge value as the host to\n// use in other cases.\nfunc http01ChallengeHost(value string) string {\n\tif ip := net.ParseIP(value); ip != nil {\n\t\tif ip.To4() == nil {\n\t\t\tvalue = \"[\" + value + \"]\"\n\t\t}\n\t\treturn value\n\t}\n\treturn rootedName(value)\n}\n\n// tlsAlpn01ChallengeHost returns the rooted DNS used on TLS-ALPN-01\n// validations.\nfunc tlsAlpn01ChallengeHost(name string) string {\n\tif ip := net.ParseIP(name); ip != nil {\n\t\treturn name\n\t}\n\treturn rootedName(name)\n}\n\n// dns01ChallengeHost returns the TXT record used in DNS-01 validations.\nfunc dns01ChallengeHost(domain string) string {\n\treturn \"_acme-challenge.\" + rootedName(domain)\n}\n\nfunc tlsAlert(err error) uint8 {\n\tvar opErr *net.OpError\n\tif errors.As(err, &opErr) {\n\t\tv := reflect.ValueOf(opErr.Err)\n\t\tif v.Kind() == reflect.Uint8 {\n\t\t\treturn cast.Uint8(v.Uint())\n\t\t}\n\t}\n\treturn 0\n}\n\nfunc tlsalpn01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose.JSONWebKey) error {\n\tconfig := &tls.Config{\n\t\tNextProtos: []string{\"acme-tls/1\"},\n\t\t// https://tools.ietf.org/html/rfc8737#section-4\n\t\t// ACME servers that implement \"acme-tls/1\" MUST only negotiate TLS 1.2\n\t\t// [RFC5246] or higher when connecting to clients for validation.\n\t\tMinVersion:         tls.VersionTLS12,\n\t\tServerName:         serverName(ch),\n\t\tInsecureSkipVerify: true, //nolint:gosec // we expect a self-signed challenge certificate\n\t}\n\n\t// Allow to change TLS port for testing purposes.\n\thostPort := tlsAlpn01ChallengeHost(ch.Value)\n\tif port := InsecurePortTLSALPN01; port == 0 {\n\t\thostPort = net.JoinHostPort(hostPort, \"443\")\n\t} else {\n\t\thostPort = net.JoinHostPort(hostPort, strconv.Itoa(port))\n\t}\n\n\tvc := MustClientFromContext(ctx)\n\tconn, err := vc.TLSDial(\"tcp\", hostPort, config)\n\tif err != nil {\n\t\t// With Go 1.17+ tls.Dial fails if there's no overlap between configured\n\t\t// client and server protocols. When this happens the connection is\n\t\t// closed with the error no_application_protocol(120) as required by\n\t\t// RFC7301. See https://golang.org/doc/go1.17#ALPN\n\t\tif tlsAlert(err) == 120 {\n\t\t\treturn storeError(ctx, db, ch, true, NewError(ErrorRejectedIdentifierType,\n\t\t\t\t\"cannot negotiate ALPN acme-tls/1 protocol for tls-alpn-01 challenge\"))\n\t\t}\n\t\treturn storeError(ctx, db, ch, false, WrapError(ErrorConnectionType, err,\n\t\t\t\"error doing TLS dial for %s\", ch.Value))\n\t}\n\tdefer conn.Close()\n\n\tcs := conn.ConnectionState()\n\tcerts := cs.PeerCertificates\n\n\tif len(certs) == 0 {\n\t\treturn storeError(ctx, db, ch, true, NewError(ErrorRejectedIdentifierType,\n\t\t\t\"%s challenge for %s resulted in no certificates\", ch.Type, ch.Value))\n\t}\n\n\tif cs.NegotiatedProtocol != \"acme-tls/1\" {\n\t\treturn storeError(ctx, db, ch, true, NewError(ErrorRejectedIdentifierType,\n\t\t\t\"cannot negotiate ALPN acme-tls/1 protocol for tls-alpn-01 challenge\"))\n\t}\n\n\tleafCert := certs[0]\n\n\t// if no DNS names present, look for IP address and verify that exactly one exists\n\tif len(leafCert.DNSNames) == 0 {\n\t\tif len(leafCert.IPAddresses) != 1 || !leafCert.IPAddresses[0].Equal(net.ParseIP(ch.Value)) {\n\t\t\treturn storeError(ctx, db, ch, true, NewError(ErrorRejectedIdentifierType,\n\t\t\t\t\"incorrect certificate for tls-alpn-01 challenge: leaf certificate must contain a single IP address or DNS name, %v\", ch.Value))\n\t\t}\n\t} else {\n\t\tif len(leafCert.DNSNames) != 1 || !strings.EqualFold(leafCert.DNSNames[0], ch.Value) {\n\t\t\treturn storeError(ctx, db, ch, true, NewError(ErrorRejectedIdentifierType,\n\t\t\t\t\"incorrect certificate for tls-alpn-01 challenge: leaf certificate must contain a single IP address or DNS name, %v\", ch.Value))\n\t\t}\n\t}\n\n\tidPeAcmeIdentifier := asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 31}\n\tidPeAcmeIdentifierV1Obsolete := asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 30, 1}\n\tfoundIDPeAcmeIdentifierV1Obsolete := false\n\n\tkeyAuth, err := KeyAuthorization(ch.Token, jwk)\n\tif err != nil {\n\t\treturn err\n\t}\n\thashedKeyAuth := sha256.Sum256([]byte(keyAuth))\n\n\tfor _, ext := range leafCert.Extensions {\n\t\tif idPeAcmeIdentifier.Equal(ext.Id) {\n\t\t\tif !ext.Critical {\n\t\t\t\treturn storeError(ctx, db, ch, true, NewError(ErrorRejectedIdentifierType,\n\t\t\t\t\t\"incorrect certificate for tls-alpn-01 challenge: acmeValidationV1 extension not critical\"))\n\t\t\t}\n\n\t\t\tvar extValue []byte\n\t\t\trest, err := asn1.Unmarshal(ext.Value, &extValue)\n\n\t\t\tif err != nil || len(rest) > 0 || len(hashedKeyAuth) != len(extValue) {\n\t\t\t\treturn storeError(ctx, db, ch, true, NewError(ErrorRejectedIdentifierType,\n\t\t\t\t\t\"incorrect certificate for tls-alpn-01 challenge: malformed acmeValidationV1 extension value\"))\n\t\t\t}\n\n\t\t\tif subtle.ConstantTimeCompare(hashedKeyAuth[:], extValue) != 1 {\n\t\t\t\treturn storeError(ctx, db, ch, true, NewError(ErrorRejectedIdentifierType,\n\t\t\t\t\t\"incorrect certificate for tls-alpn-01 challenge: \"+\n\t\t\t\t\t\t\"expected acmeValidationV1 extension value %s for this challenge but got %s\",\n\t\t\t\t\thex.EncodeToString(hashedKeyAuth[:]), hex.EncodeToString(extValue)))\n\t\t\t}\n\n\t\t\tch.Status = StatusValid\n\t\t\tch.Error = nil\n\t\t\tch.ValidatedAt = clock.Now().Format(time.RFC3339)\n\n\t\t\tif err = db.UpdateChallenge(ctx, ch); err != nil {\n\t\t\t\treturn WrapErrorISE(err, \"tlsalpn01ValidateChallenge - error updating challenge\")\n\t\t\t}\n\t\t\treturn nil\n\t\t}\n\n\t\tif idPeAcmeIdentifierV1Obsolete.Equal(ext.Id) {\n\t\t\tfoundIDPeAcmeIdentifierV1Obsolete = true\n\t\t}\n\t}\n\n\tif foundIDPeAcmeIdentifierV1Obsolete {\n\t\treturn storeError(ctx, db, ch, true, NewError(ErrorRejectedIdentifierType,\n\t\t\t\"incorrect certificate for tls-alpn-01 challenge: obsolete id-pe-acmeIdentifier in acmeValidationV1 extension\"))\n\t}\n\n\treturn storeError(ctx, db, ch, true, NewError(ErrorRejectedIdentifierType,\n\t\t\"incorrect certificate for tls-alpn-01 challenge: missing acmeValidationV1 extension\"))\n}\n\nfunc dns01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose.JSONWebKey) error {\n\t// Normalize domain for wildcard DNS names\n\t// This is done to avoid making TXT lookups for domains like\n\t// _acme-challenge.*.example.com\n\t// Instead perform txt lookup for _acme-challenge.example.com\n\tdomain := strings.TrimPrefix(ch.Value, \"*.\")\n\n\tvc := MustClientFromContext(ctx)\n\ttxtRecords, err := vc.LookupTxt(dns01ChallengeHost(domain))\n\tif err != nil {\n\t\treturn storeError(ctx, db, ch, false, WrapError(ErrorDNSType, err,\n\t\t\t\"error looking up TXT records for domain %s\", domain))\n\t}\n\n\texpectedKeyAuth, err := KeyAuthorization(ch.Token, jwk)\n\tif err != nil {\n\t\treturn err\n\t}\n\th := sha256.Sum256([]byte(expectedKeyAuth))\n\texpected := base64.RawURLEncoding.EncodeToString(h[:])\n\tvar found bool\n\tfor _, r := range txtRecords {\n\t\tif r == expected {\n\t\t\tfound = true\n\t\t\tbreak\n\t\t}\n\t}\n\tif !found {\n\t\treturn storeError(ctx, db, ch, false, NewError(ErrorRejectedIdentifierType,\n\t\t\t\"keyAuthorization does not match; expected %s, but got %s\", expectedKeyAuth, txtRecords))\n\t}\n\n\t// Update and store the challenge.\n\tch.Status = StatusValid\n\tch.Error = nil\n\tch.ValidatedAt = clock.Now().Format(time.RFC3339)\n\n\tif err = db.UpdateChallenge(ctx, ch); err != nil {\n\t\treturn WrapErrorISE(err, \"error updating challenge\")\n\t}\n\treturn nil\n}\n\ntype wireOidcPayload struct {\n\t// IDToken contains the OIDC identity token\n\tIDToken string `json:\"id_token\"`\n}\n\nfunc wireOIDC01Validate(ctx context.Context, ch *Challenge, db WireDB, jwk *jose.JSONWebKey, payload []byte) error {\n\tprov, ok := ProvisionerFromContext(ctx)\n\tif !ok {\n\t\treturn NewErrorISE(\"missing provisioner\")\n\t}\n\twireOptions, err := prov.GetOptions().GetWireOptions()\n\tif err != nil {\n\t\treturn WrapErrorISE(err, \"failed getting Wire options\")\n\t}\n\tlinker, ok := LinkerFromContext(ctx)\n\tif !ok {\n\t\treturn NewErrorISE(\"missing linker\")\n\t}\n\n\tvar oidcPayload wireOidcPayload\n\tif err := json.Unmarshal(payload, &oidcPayload); err != nil {\n\t\treturn WrapError(ErrorMalformedType, err, \"error unmarshalling Wire OIDC challenge payload\")\n\t}\n\n\twireID, err := wire.ParseUserID(ch.Value)\n\tif err != nil {\n\t\treturn WrapErrorISE(err, \"error unmarshalling challenge data\")\n\t}\n\n\toidcOptions := wireOptions.GetOIDCOptions()\n\tverifier, err := oidcOptions.GetVerifier(ctx)\n\tif err != nil {\n\t\treturn WrapErrorISE(err, \"no OIDC verifier available\")\n\t}\n\n\tidToken, err := verifier.Verify(ctx, oidcPayload.IDToken)\n\tif err != nil {\n\t\treturn storeError(ctx, db, ch, true, WrapError(ErrorRejectedIdentifierType, err,\n\t\t\t\"error verifying ID token signature\"))\n\t}\n\n\tvar claims struct {\n\t\tName         string `json:\"preferred_username,omitempty\"`\n\t\tHandle       string `json:\"name\"`\n\t\tIssuer       string `json:\"iss,omitempty\"`\n\t\tGivenName    string `json:\"given_name,omitempty\"`\n\t\tKeyAuth      string `json:\"keyauth\"`\n\t\tACMEAudience string `json:\"acme_aud,omitempty\"`\n\t}\n\tif err := idToken.Claims(&claims); err != nil {\n\t\treturn storeError(ctx, db, ch, true, WrapError(ErrorRejectedIdentifierType, err,\n\t\t\t\"error retrieving claims from ID token\"))\n\t}\n\n\t// TODO(hs): move this into validation below?\n\texpectedKeyAuth, err := KeyAuthorization(ch.Token, jwk)\n\tif err != nil {\n\t\treturn WrapErrorISE(err, \"error determining key authorization\")\n\t}\n\tif expectedKeyAuth != claims.KeyAuth {\n\t\treturn storeError(ctx, db, ch, true, NewError(ErrorRejectedIdentifierType,\n\t\t\t\"keyAuthorization does not match; expected %q, but got %q\", expectedKeyAuth, claims.KeyAuth))\n\t}\n\n\t// audience is the full URL to the challenge\n\tacmeAudience := linker.GetLink(ctx, ChallengeLinkType, ch.AuthorizationID, ch.ID)\n\tif claims.ACMEAudience != acmeAudience {\n\t\treturn storeError(ctx, db, ch, true, NewError(ErrorRejectedIdentifierType,\n\t\t\t\"invalid 'acme_aud' %q\", claims.ACMEAudience))\n\t}\n\n\ttransformedIDToken, err := validateWireOIDCClaims(oidcOptions, idToken, wireID)\n\tif err != nil {\n\t\treturn storeError(ctx, db, ch, true, WrapError(ErrorRejectedIdentifierType, err, \"claims in OIDC ID token don't match\"))\n\t}\n\n\t// Update and store the challenge.\n\tch.Status = StatusValid\n\tch.Error = nil\n\tch.ValidatedAt = clock.Now().Format(time.RFC3339)\n\n\tif err = db.UpdateChallenge(ctx, ch); err != nil {\n\t\treturn WrapErrorISE(err, \"error updating challenge\")\n\t}\n\n\torders, err := db.GetAllOrdersByAccountID(ctx, ch.AccountID)\n\tif err != nil {\n\t\treturn WrapErrorISE(err, \"could not retrieve current order by account id\")\n\t}\n\tif len(orders) == 0 {\n\t\treturn NewErrorISE(\"there are not enough orders for this account for this custom OIDC challenge\")\n\t}\n\n\torder := orders[len(orders)-1]\n\tif err := db.CreateOidcToken(ctx, order, transformedIDToken); err != nil {\n\t\treturn WrapErrorISE(err, \"failed storing OIDC id token\")\n\t}\n\n\treturn nil\n}\n\nfunc validateWireOIDCClaims(o *wireprovisioner.OIDCOptions, token *oidc.IDToken, wireID wire.UserID) (map[string]any, error) {\n\tvar m map[string]any\n\tif err := token.Claims(&m); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed extracting OIDC ID token claims: %w\", err)\n\t}\n\ttransformed, err := o.Transform(m)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed transforming OIDC ID token: %w\", err)\n\t}\n\n\tname, ok := transformed[\"name\"]\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"transformed OIDC ID token does not contain 'name'\")\n\t}\n\tif wireID.Name != name {\n\t\treturn nil, fmt.Errorf(\"invalid 'name' %q after transformation\", name)\n\t}\n\n\tpreferredUsername, ok := transformed[\"preferred_username\"]\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"transformed OIDC ID token does not contain 'preferred_username'\")\n\t}\n\tif wireID.Handle != preferredUsername {\n\t\treturn nil, fmt.Errorf(\"invalid 'preferred_username' %q after transformation\", preferredUsername)\n\t}\n\n\treturn transformed, nil\n}\n\ntype wireDpopPayload struct {\n\t// AccessToken is the token generated by wire-server\n\tAccessToken string `json:\"access_token\"`\n}\n\nfunc wireDPOP01Validate(ctx context.Context, ch *Challenge, db WireDB, accountJWK *jose.JSONWebKey, payload []byte) error {\n\tprov, ok := ProvisionerFromContext(ctx)\n\tif !ok {\n\t\treturn NewErrorISE(\"missing provisioner\")\n\t}\n\twireOptions, err := prov.GetOptions().GetWireOptions()\n\tif err != nil {\n\t\treturn WrapErrorISE(err, \"failed getting Wire options\")\n\t}\n\tlinker, ok := LinkerFromContext(ctx)\n\tif !ok {\n\t\treturn NewErrorISE(\"missing linker\")\n\t}\n\n\tvar dpopPayload wireDpopPayload\n\tif err := json.Unmarshal(payload, &dpopPayload); err != nil {\n\t\treturn WrapError(ErrorMalformedType, err, \"error unmarshalling Wire DPoP challenge payload\")\n\t}\n\n\twireID, err := wire.ParseDeviceID(ch.Value)\n\tif err != nil {\n\t\treturn WrapErrorISE(err, \"error unmarshalling challenge data\")\n\t}\n\n\tclientID, err := wire.ParseClientID(wireID.ClientID)\n\tif err != nil {\n\t\treturn WrapErrorISE(err, \"error parsing device id\")\n\t}\n\n\tdpopOptions := wireOptions.GetDPOPOptions()\n\tissuer, err := dpopOptions.EvaluateTarget(clientID.DeviceID)\n\tif err != nil {\n\t\treturn WrapErrorISE(err, \"invalid Go template registered for 'target'\")\n\t}\n\n\t// audience is the full URL to the challenge\n\taudience := linker.GetLink(ctx, ChallengeLinkType, ch.AuthorizationID, ch.ID)\n\n\tparams := wireVerifyParams{\n\t\ttoken:     dpopPayload.AccessToken,\n\t\ttokenKey:  dpopOptions.GetSigningKey(),\n\t\tdpopKey:   accountJWK.Public(),\n\t\tdpopKeyID: accountJWK.KeyID,\n\t\tissuer:    issuer,\n\t\taudience:  audience,\n\t\twireID:    wireID,\n\t\tchToken:   ch.Token,\n\t\tt:         clock.Now().UTC(),\n\t}\n\t_, dpop, err := parseAndVerifyWireAccessToken(params)\n\tif err != nil {\n\t\treturn storeError(ctx, db, ch, true, WrapError(ErrorRejectedIdentifierType, err,\n\t\t\t\"failed validating Wire access token\"))\n\t}\n\n\t// Update and store the challenge.\n\tch.Status = StatusValid\n\tch.Error = nil\n\tch.ValidatedAt = clock.Now().Format(time.RFC3339)\n\n\tif err = db.UpdateChallenge(ctx, ch); err != nil {\n\t\treturn WrapErrorISE(err, \"error updating challenge\")\n\t}\n\n\torders, err := db.GetAllOrdersByAccountID(ctx, ch.AccountID)\n\tif err != nil {\n\t\treturn WrapErrorISE(err, \"could not find current order by account id\")\n\t}\n\tif len(orders) == 0 {\n\t\treturn NewErrorISE(\"there are not enough orders for this account for this custom OIDC challenge\")\n\t}\n\n\torder := orders[len(orders)-1]\n\tif err := db.CreateDpopToken(ctx, order, map[string]any(*dpop)); err != nil {\n\t\treturn WrapErrorISE(err, \"failed storing DPoP token\")\n\t}\n\n\treturn nil\n}\n\ntype wireCnf struct {\n\tKid string `json:\"kid\"`\n}\n\ntype wireAccessToken struct {\n\tjose.Claims\n\tChallenge  string  `json:\"chal\"`\n\tNonce      string  `json:\"nonce\"`\n\tCnf        wireCnf `json:\"cnf\"`\n\tProof      string  `json:\"proof\"`\n\tClientID   string  `json:\"client_id\"`\n\tAPIVersion int     `json:\"api_version\"`\n\tScope      string  `json:\"scope\"`\n}\n\ntype wireDpopJwt struct {\n\tjose.Claims\n\tClientID  string `json:\"client_id\"`\n\tChallenge string `json:\"chal\"`\n\tNonce     string `json:\"nonce\"`\n\tHTU       string `json:\"htu\"`\n}\n\ntype wireDpopToken map[string]any\n\ntype wireVerifyParams struct {\n\ttoken     string\n\ttokenKey  crypto.PublicKey\n\tdpopKey   crypto.PublicKey\n\tdpopKeyID string\n\tissuer    string\n\taudience  string\n\twireID    wire.DeviceID\n\tchToken   string\n\tt         time.Time\n}\n\nfunc parseAndVerifyWireAccessToken(v wireVerifyParams) (*wireAccessToken, *wireDpopToken, error) {\n\tjwt, err := jose.ParseSigned(v.token)\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"failed parsing token: %w\", err)\n\t}\n\n\tif len(jwt.Headers) != 1 {\n\t\treturn nil, nil, fmt.Errorf(\"token has wrong number of headers %d\", len(jwt.Headers))\n\t}\n\tkeyID, err := KeyToID(&jose.JSONWebKey{Key: v.tokenKey})\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"failed calculating token key ID: %w\", err)\n\t}\n\tjwtKeyID := jwt.Headers[0].KeyID\n\tif jwtKeyID == \"\" {\n\t\tif jwtKeyID, err = KeyToID(jwt.Headers[0].JSONWebKey); err != nil {\n\t\t\treturn nil, nil, fmt.Errorf(\"failed extracting token key ID: %w\", err)\n\t\t}\n\t}\n\tif jwtKeyID != keyID {\n\t\treturn nil, nil, fmt.Errorf(\"invalid token key ID %q\", jwtKeyID)\n\t}\n\n\tvar accessToken wireAccessToken\n\tif err = jwt.Claims(v.tokenKey, &accessToken); err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"failed validating Wire DPoP token claims: %w\", err)\n\t}\n\n\tif err := accessToken.ValidateWithLeeway(jose.Expected{\n\t\tTime:     v.t,\n\t\tIssuer:   v.issuer,\n\t\tAudience: jose.Audience{v.audience},\n\t}, 1*time.Minute); err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"failed validation: %w\", err)\n\t}\n\n\tif accessToken.Challenge == \"\" {\n\t\treturn nil, nil, errors.New(\"access token challenge 'chal' must not be empty\")\n\t}\n\tif accessToken.Cnf.Kid == \"\" || accessToken.Cnf.Kid != v.dpopKeyID {\n\t\treturn nil, nil, fmt.Errorf(\"expected 'kid' %q; got %q\", v.dpopKeyID, accessToken.Cnf.Kid)\n\t}\n\tif accessToken.ClientID != v.wireID.ClientID {\n\t\treturn nil, nil, fmt.Errorf(\"invalid Wire 'client_id' %q\", accessToken.ClientID)\n\t}\n\tif accessToken.Expiry.Time().After(v.t.Add(time.Hour)) {\n\t\treturn nil, nil, fmt.Errorf(\"token expiry 'exp' %s is too far into the future\", accessToken.Expiry.Time().String())\n\t}\n\tif accessToken.Scope != \"wire_client_id\" {\n\t\treturn nil, nil, fmt.Errorf(\"invalid Wire 'scope' %q\", accessToken.Scope)\n\t}\n\n\tdpopJWT, err := jose.ParseSigned(accessToken.Proof)\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"invalid Wire DPoP token: %w\", err)\n\t}\n\tif len(dpopJWT.Headers) != 1 {\n\t\treturn nil, nil, fmt.Errorf(\"DPoP token has wrong number of headers %d\", len(jwt.Headers))\n\t}\n\tdpopJwtKeyID := dpopJWT.Headers[0].KeyID\n\tif dpopJwtKeyID == \"\" {\n\t\tif dpopJwtKeyID, err = KeyToID(dpopJWT.Headers[0].JSONWebKey); err != nil {\n\t\t\treturn nil, nil, fmt.Errorf(\"failed extracting DPoP token key ID: %w\", err)\n\t\t}\n\t}\n\tif dpopJwtKeyID != v.dpopKeyID {\n\t\treturn nil, nil, fmt.Errorf(\"invalid DPoP token key ID %q\", dpopJWT.Headers[0].KeyID)\n\t}\n\n\tvar wireDpop wireDpopJwt\n\tif err := dpopJWT.Claims(v.dpopKey, &wireDpop); err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"failed validating Wire DPoP token claims: %w\", err)\n\t}\n\n\tif err := wireDpop.ValidateWithLeeway(jose.Expected{\n\t\tTime:     v.t,\n\t\tAudience: jose.Audience{v.audience},\n\t}, 1*time.Minute); err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"failed DPoP validation: %w\", err)\n\t}\n\tif wireDpop.HTU == \"\" || wireDpop.HTU != v.issuer { // DPoP doesn't contains \"iss\" claim, but has it in the \"htu\" claim\n\t\treturn nil, nil, fmt.Errorf(\"DPoP contains invalid issuer 'htu' %q\", wireDpop.HTU)\n\t}\n\tif wireDpop.Expiry.Time().After(v.t.Add(time.Hour)) {\n\t\treturn nil, nil, fmt.Errorf(\"'exp' %s is too far into the future\", wireDpop.Expiry.Time().String())\n\t}\n\tif wireDpop.Subject != v.wireID.ClientID {\n\t\treturn nil, nil, fmt.Errorf(\"DPoP contains invalid Wire client ID %q\", wireDpop.ClientID)\n\t}\n\tif wireDpop.Nonce == \"\" || wireDpop.Nonce != accessToken.Nonce {\n\t\treturn nil, nil, fmt.Errorf(\"DPoP contains invalid 'nonce' %q\", wireDpop.Nonce)\n\t}\n\tif wireDpop.Challenge == \"\" || wireDpop.Challenge != accessToken.Challenge {\n\t\treturn nil, nil, fmt.Errorf(\"DPoP contains invalid challenge 'chal' %q\", wireDpop.Challenge)\n\t}\n\n\t// TODO(hs): can we use the wireDpopJwt and map that instead of doing Claims() twice?\n\tvar dpopToken wireDpopToken\n\tif err := dpopJWT.Claims(v.dpopKey, &dpopToken); err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"failed validating Wire DPoP token claims: %w\", err)\n\t}\n\n\tchallenge, ok := dpopToken[\"chal\"].(string)\n\tif !ok {\n\t\treturn nil, nil, fmt.Errorf(\"invalid challenge 'chal' in Wire DPoP token\")\n\t}\n\tif challenge == \"\" || challenge != v.chToken {\n\t\treturn nil, nil, fmt.Errorf(\"invalid Wire DPoP challenge 'chal' %q\", challenge)\n\t}\n\n\thandle, ok := dpopToken[\"handle\"].(string)\n\tif !ok {\n\t\treturn nil, nil, fmt.Errorf(\"invalid 'handle' in Wire DPoP token\")\n\t}\n\tif handle == \"\" || handle != v.wireID.Handle {\n\t\treturn nil, nil, fmt.Errorf(\"invalid Wire client 'handle' %q\", handle)\n\t}\n\n\tname, ok := dpopToken[\"name\"].(string)\n\tif !ok {\n\t\treturn nil, nil, fmt.Errorf(\"invalid display 'name' in Wire DPoP token\")\n\t}\n\tif name == \"\" || name != v.wireID.Name {\n\t\treturn nil, nil, fmt.Errorf(\"invalid Wire client display 'name' %q\", name)\n\t}\n\n\treturn &accessToken, &dpopToken, nil\n}\n\ntype payloadType struct {\n\tAttObj string `json:\"attObj\"`\n\tError  string `json:\"error\"`\n}\n\ntype attestationObject struct {\n\tFormat       string                 `json:\"fmt\"`\n\tAttStatement map[string]interface{} `json:\"attStmt,omitempty\"`\n}\n\n// TODO(bweeks): move attestation verification to a shared package.\nfunc deviceAttest01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose.JSONWebKey, payload []byte) error {\n\t// Update challenge with the payload\n\tch.Payload = payload\n\n\t// Load authorization to store the key fingerprint.\n\taz, err := db.GetAuthorization(ctx, ch.AuthorizationID)\n\tif err != nil {\n\t\treturn WrapErrorISE(err, \"error loading authorization\")\n\t}\n\n\t// Parse payload.\n\tvar p payloadType\n\tif err := json.Unmarshal(payload, &p); err != nil {\n\t\treturn WrapErrorISE(err, \"error unmarshalling JSON\")\n\t}\n\tif p.Error != \"\" {\n\t\treturn storeError(ctx, db, ch, true, NewError(ErrorRejectedIdentifierType,\n\t\t\t\"payload contained error: %v\", p.Error))\n\t}\n\n\tattObj, err := base64.RawURLEncoding.DecodeString(p.AttObj)\n\tif err != nil {\n\t\treturn storeError(ctx, db, ch, true, NewDetailedError(ErrorBadAttestationStatementType, \"failed base64 decoding attObj %q\", p.AttObj))\n\t}\n\n\tif len(attObj) == 0 || bytes.Equal(attObj, []byte(\"{}\")) {\n\t\treturn storeError(ctx, db, ch, true, NewDetailedError(ErrorBadAttestationStatementType, \"attObj must not be empty\"))\n\t}\n\n\tcborDecoderOptions := cbor.DecOptions{}\n\tcborDecoder, err := cborDecoderOptions.DecMode()\n\tif err != nil {\n\t\treturn WrapErrorISE(err, \"failed creating CBOR decoder\")\n\t}\n\n\tif err := cborDecoder.Wellformed(attObj); err != nil {\n\t\treturn storeError(ctx, db, ch, true, NewDetailedError(ErrorBadAttestationStatementType, \"attObj is not well formed CBOR: %v\", err))\n\t}\n\n\tatt := attestationObject{}\n\tif err := cborDecoder.Unmarshal(attObj, &att); err != nil {\n\t\treturn WrapErrorISE(err, \"failed unmarshalling CBOR\")\n\t}\n\n\tformat := att.Format\n\tprov := MustProvisionerFromContext(ctx)\n\tif !prov.IsAttestationFormatEnabled(ctx, provisioner.ACMEAttestationFormat(format)) {\n\t\tif format != \"apple\" && format != \"step\" && format != \"tpm\" {\n\t\t\treturn storeError(ctx, db, ch, true, NewDetailedError(ErrorBadAttestationStatementType, \"unsupported attestation object format %q\", format))\n\t\t}\n\n\t\treturn storeError(ctx, db, ch, true,\n\t\t\tNewError(ErrorBadAttestationStatementType, \"attestation format %q is not enabled\", format))\n\t}\n\n\tswitch format {\n\tcase \"apple\":\n\t\tdata, err := doAppleAttestationFormat(ctx, prov, ch, &att)\n\t\tif err != nil {\n\t\t\tvar acmeError *Error\n\t\t\tif errors.As(err, &acmeError) {\n\t\t\t\tif acmeError.Status == 500 {\n\t\t\t\t\treturn acmeError\n\t\t\t\t}\n\t\t\t\treturn storeError(ctx, db, ch, true, acmeError)\n\t\t\t}\n\t\t\treturn WrapErrorISE(err, \"error validating attestation\")\n\t\t}\n\n\t\t// Validate nonce with SHA-256 of the token.\n\t\tif len(data.Nonce) != 0 {\n\t\t\tsum := sha256.Sum256([]byte(ch.Token))\n\t\t\tif subtle.ConstantTimeCompare(data.Nonce, sum[:]) != 1 {\n\t\t\t\treturn storeError(ctx, db, ch, true, NewDetailedError(ErrorBadAttestationStatementType, \"challenge token does not match\"))\n\t\t\t}\n\t\t}\n\n\t\t// Validate Apple's ClientIdentifier (Identifier.Value) with device\n\t\t// identifiers.\n\t\t//\n\t\t// Note: We might want to use an external service for this.\n\t\tif data.UDID != ch.Value && data.SerialNumber != ch.Value {\n\t\t\tsubproblem := NewSubproblemWithIdentifier(\n\t\t\t\tErrorRejectedIdentifierType,\n\t\t\t\tIdentifier{Type: \"permanent-identifier\", Value: ch.Value},\n\t\t\t\t\"challenge identifier %q doesn't match any of the attested hardware identifiers %q\", ch.Value, []string{data.UDID, data.SerialNumber},\n\t\t\t)\n\t\t\treturn storeError(ctx, db, ch, true, NewDetailedError(ErrorBadAttestationStatementType, \"permanent identifier does not match\").AddSubproblems(subproblem))\n\t\t}\n\n\t\t// Update attestation key fingerprint to compare against the CSR\n\t\taz.Fingerprint = data.Fingerprint\n\tcase \"step\":\n\t\tdata, err := doStepAttestationFormat(ctx, prov, ch, jwk, &att)\n\t\tif err != nil {\n\t\t\tvar acmeError *Error\n\t\t\tif errors.As(err, &acmeError) {\n\t\t\t\tif acmeError.Status == 500 {\n\t\t\t\t\treturn acmeError\n\t\t\t\t}\n\t\t\t\treturn storeError(ctx, db, ch, true, acmeError)\n\t\t\t}\n\t\t\treturn WrapErrorISE(err, \"error validating attestation\")\n\t\t}\n\n\t\t// Validate the YubiKey serial number from the attestation\n\t\t// certificate with the challenged Order value.\n\t\t//\n\t\t// Note: We might want to use an external service for this.\n\t\tif data.SerialNumber != ch.Value {\n\t\t\tsubproblem := NewSubproblemWithIdentifier(\n\t\t\t\tErrorRejectedIdentifierType,\n\t\t\t\tIdentifier{Type: \"permanent-identifier\", Value: ch.Value},\n\t\t\t\t\"challenge identifier %q doesn't match the attested hardware identifier %q\", ch.Value, data.SerialNumber,\n\t\t\t)\n\t\t\treturn storeError(ctx, db, ch, true, NewDetailedError(ErrorBadAttestationStatementType, \"permanent identifier does not match\").AddSubproblems(subproblem))\n\t\t}\n\n\t\t// Update attestation key fingerprint to compare against the CSR\n\t\taz.Fingerprint = data.Fingerprint\n\n\tcase \"tpm\":\n\t\tdata, err := doTPMAttestationFormat(ctx, prov, ch, jwk, &att)\n\t\tif err != nil {\n\t\t\tvar acmeError *Error\n\t\t\tif errors.As(err, &acmeError) {\n\t\t\t\tif acmeError.Status == 500 {\n\t\t\t\t\treturn acmeError\n\t\t\t\t}\n\t\t\t\treturn storeError(ctx, db, ch, true, acmeError)\n\t\t\t}\n\t\t\treturn WrapErrorISE(err, \"error validating attestation\")\n\t\t}\n\n\t\t// TODO(hs): currently this will allow a request for which no PermanentIdentifiers have been\n\t\t// extracted from the AK certificate. This is currently the case for AK certs from the CLI, as we\n\t\t// haven't implemented a way for AK certs requested by the CLI to always contain the requested\n\t\t// PermanentIdentifier. Omitting the check below doesn't allow just any request, as the Order can\n\t\t// still fail if the challenge value isn't equal to the CSR subject.\n\t\tif len(data.PermanentIdentifiers) > 0 && !slices.Contains(data.PermanentIdentifiers, ch.Value) { // TODO(hs): add support for HardwareModuleName\n\t\t\tsubproblem := NewSubproblemWithIdentifier(\n\t\t\t\tErrorRejectedIdentifierType,\n\t\t\t\tIdentifier{Type: \"permanent-identifier\", Value: ch.Value},\n\t\t\t\t\"challenge identifier %q doesn't match any of the attested hardware identifiers %q\", ch.Value, data.PermanentIdentifiers,\n\t\t\t)\n\t\t\treturn storeError(ctx, db, ch, true, NewDetailedError(ErrorBadAttestationStatementType, \"permanent identifier does not match\").AddSubproblems(subproblem))\n\t\t}\n\n\t\t// Update attestation key fingerprint to compare against the CSR\n\t\taz.Fingerprint = data.Fingerprint\n\tdefault:\n\t\treturn storeError(ctx, db, ch, true, NewDetailedError(ErrorBadAttestationStatementType, \"unsupported attestation object format %q\", format))\n\t}\n\n\t// Update and store the challenge.\n\tch.Status = StatusValid\n\tch.Error = nil\n\tch.ValidatedAt = clock.Now().Format(time.RFC3339)\n\tch.PayloadFormat = format\n\n\t// Store the fingerprint in the authorization.\n\t//\n\t// TODO: add method to update authorization and challenge atomically.\n\tif az.Fingerprint != \"\" {\n\t\tif err := db.UpdateAuthorization(ctx, az); err != nil {\n\t\t\treturn WrapErrorISE(err, \"error updating authorization\")\n\t\t}\n\t}\n\n\tif err := db.UpdateChallenge(ctx, ch); err != nil {\n\t\treturn WrapErrorISE(err, \"error updating challenge\")\n\t}\n\treturn nil\n}\n\nvar (\n\toidSubjectAlternativeName = asn1.ObjectIdentifier{2, 5, 29, 17}\n)\n\ntype tpmAttestationData struct {\n\tCertificate          *x509.Certificate\n\tVerifiedChains       [][]*x509.Certificate\n\tPermanentIdentifiers []string\n\tFingerprint          string\n}\n\n// coseAlgorithmIdentifier models a COSEAlgorithmIdentifier.\n// Also see https://www.w3.org/TR/webauthn-2/#sctn-alg-identifier.\ntype coseAlgorithmIdentifier int32\n\nconst (\n\tcoseAlgES256 = coseAlgorithmIdentifier(-7)\n\tcoseAlgRS256 = coseAlgorithmIdentifier(-257)\n\tcoseAlgRS1   = coseAlgorithmIdentifier(-65535) // deprecated, but (still) often used in TPMs\n)\n\nfunc doTPMAttestationFormat(_ context.Context, prov Provisioner, ch *Challenge, jwk *jose.JSONWebKey, att *attestationObject) (*tpmAttestationData, error) {\n\tver, ok := att.AttStatement[\"ver\"].(string)\n\tif !ok {\n\t\treturn nil, NewDetailedError(ErrorBadAttestationStatementType, \"ver not present\")\n\t}\n\tif ver != \"2.0\" {\n\t\treturn nil, NewDetailedError(ErrorBadAttestationStatementType, \"version %q is not supported\", ver)\n\t}\n\n\tx5c, ok := att.AttStatement[\"x5c\"].([]interface{})\n\tif !ok {\n\t\treturn nil, NewDetailedError(ErrorBadAttestationStatementType, \"x5c not present\")\n\t}\n\tif len(x5c) == 0 {\n\t\treturn nil, NewDetailedError(ErrorBadAttestationStatementType, \"x5c is empty\")\n\t}\n\n\takCertBytes, ok := x5c[0].([]byte)\n\tif !ok {\n\t\treturn nil, NewDetailedError(ErrorBadAttestationStatementType, \"x5c is malformed\")\n\t}\n\takCert, err := x509.ParseCertificate(akCertBytes)\n\tif err != nil {\n\t\treturn nil, WrapDetailedError(ErrorBadAttestationStatementType, err, \"x5c is malformed\")\n\t}\n\n\tintermediates := x509.NewCertPool()\n\tfor _, v := range x5c[1:] {\n\t\tintCertBytes, vok := v.([]byte)\n\t\tif !vok {\n\t\t\treturn nil, NewDetailedError(ErrorBadAttestationStatementType, \"x5c is malformed\")\n\t\t}\n\t\tintCert, err := x509.ParseCertificate(intCertBytes)\n\t\tif err != nil {\n\t\t\treturn nil, WrapDetailedError(ErrorBadAttestationStatementType, err, \"x5c is malformed\")\n\t\t}\n\t\tintermediates.AddCert(intCert)\n\t}\n\n\t// TODO(hs): this can be removed when permanent-identifier/hardware-module-name are handled correctly in\n\t// the stdlib in https://cs.opensource.google/go/go/+/refs/tags/go1.19:src/crypto/x509/parser.go;drc=b5b2cf519fe332891c165077f3723ee74932a647;l=362,\n\t// but I doubt that will happen.\n\tif len(akCert.UnhandledCriticalExtensions) > 0 {\n\t\tunhandledCriticalExtensions := akCert.UnhandledCriticalExtensions[:0]\n\t\tfor _, extOID := range akCert.UnhandledCriticalExtensions {\n\t\t\tif !extOID.Equal(oidSubjectAlternativeName) {\n\t\t\t\t// critical extensions other than the Subject Alternative Name remain unhandled\n\t\t\t\tunhandledCriticalExtensions = append(unhandledCriticalExtensions, extOID)\n\t\t\t}\n\t\t}\n\t\takCert.UnhandledCriticalExtensions = unhandledCriticalExtensions\n\t}\n\n\troots, ok := prov.GetAttestationRoots()\n\tif !ok {\n\t\treturn nil, NewErrorISE(\"no root CA bundle available to verify the attestation certificate\")\n\t}\n\n\t// verify that the AK certificate was signed by a trusted root,\n\t// chained to by the intermediates provided by the client. As part\n\t// of building the verified certificate chain, the signature over the\n\t// AK certificate is checked to be a valid signature of one of the\n\t// provided intermediates. Signatures over the intermediates are in\n\t// turn also verified to be valid signatures from one of the trusted\n\t// roots.\n\tverifiedChains, err := akCert.Verify(x509.VerifyOptions{\n\t\tRoots:         roots,\n\t\tIntermediates: intermediates,\n\t\tCurrentTime:   time.Now().Truncate(time.Second),\n\t\tKeyUsages:     []x509.ExtKeyUsage{x509.ExtKeyUsageAny},\n\t})\n\tif err != nil {\n\t\treturn nil, WrapDetailedError(ErrorBadAttestationStatementType, err, \"x5c is not valid\")\n\t}\n\n\t// validate additional AK certificate requirements\n\tif err := validateAKCertificate(akCert); err != nil {\n\t\treturn nil, WrapDetailedError(ErrorBadAttestationStatementType, err, \"AK certificate is not valid\")\n\t}\n\n\t// TODO(hs): implement revocation check; Verify() doesn't perform CRL check nor OCSP lookup.\n\n\tsans, err := x509util.ParseSubjectAlternativeNames(akCert)\n\tif err != nil {\n\t\treturn nil, WrapDetailedError(ErrorBadAttestationStatementType, err, \"failed parsing AK certificate Subject Alternative Names\")\n\t}\n\n\tpermanentIdentifiers := make([]string, len(sans.PermanentIdentifiers))\n\tfor i, pi := range sans.PermanentIdentifiers {\n\t\tpermanentIdentifiers[i] = pi.Identifier\n\t}\n\n\t// extract and validate pubArea, sig, certInfo and alg properties from the request body\n\tpubArea, ok := att.AttStatement[\"pubArea\"].([]byte)\n\tif !ok {\n\t\treturn nil, NewDetailedError(ErrorBadAttestationStatementType, \"invalid pubArea in attestation statement\")\n\t}\n\tif len(pubArea) == 0 {\n\t\treturn nil, NewDetailedError(ErrorBadAttestationStatementType, \"pubArea is empty\")\n\t}\n\n\tsig, ok := att.AttStatement[\"sig\"].([]byte)\n\tif !ok {\n\t\treturn nil, NewDetailedError(ErrorBadAttestationStatementType, \"invalid sig in attestation statement\")\n\t}\n\tif len(sig) == 0 {\n\t\treturn nil, NewDetailedError(ErrorBadAttestationStatementType, \"sig is empty\")\n\t}\n\n\tcertInfo, ok := att.AttStatement[\"certInfo\"].([]byte)\n\tif !ok {\n\t\treturn nil, NewDetailedError(ErrorBadAttestationStatementType, \"invalid certInfo in attestation statement\")\n\t}\n\tif len(certInfo) == 0 {\n\t\treturn nil, NewDetailedError(ErrorBadAttestationStatementType, \"certInfo is empty\")\n\t}\n\n\talg, ok := att.AttStatement[\"alg\"].(int64)\n\tif !ok {\n\t\treturn nil, NewDetailedError(ErrorBadAttestationStatementType, \"invalid alg in attestation statement\")\n\t}\n\n\talgI32, err := cast.SafeInt32(alg)\n\tif err != nil {\n\t\treturn nil, WrapDetailedError(ErrorBadAttestationStatementType, err, \"invalid alg %d in attestation statement\", alg)\n\t}\n\n\tvar hash crypto.Hash\n\tswitch coseAlgorithmIdentifier(algI32) {\n\tcase coseAlgRS256, coseAlgES256:\n\t\thash = crypto.SHA256\n\tcase coseAlgRS1:\n\t\thash = crypto.SHA1\n\tdefault:\n\t\treturn nil, NewDetailedError(ErrorBadAttestationStatementType, \"invalid alg %d in attestation statement\", alg)\n\t}\n\n\t// recreate the generated key certification parameter values and verify\n\t// the attested key using the public key of the AK.\n\tcertificationParameters := &attest.CertificationParameters{\n\t\tPublic:            pubArea,  // the public key that was attested\n\t\tCreateAttestation: certInfo, // the attested properties of the key\n\t\tCreateSignature:   sig,      // signature over the attested properties\n\t}\n\tverifyOpts := attest.VerifyOpts{\n\t\tPublic: akCert.PublicKey, // public key of the AK that attested the key\n\t\tHash:   hash,\n\t}\n\tif err = certificationParameters.Verify(verifyOpts); err != nil {\n\t\treturn nil, WrapDetailedError(ErrorBadAttestationStatementType, err, \"invalid certification parameters\")\n\t}\n\n\t// decode the \"certInfo\" data. This won't fail, as it's also done as part of Verify().\n\ttpmCertInfo, err := tpm2.DecodeAttestationData(certInfo)\n\tif err != nil {\n\t\treturn nil, WrapDetailedError(ErrorBadAttestationStatementType, err, \"failed decoding attestation data\")\n\t}\n\n\tkeyAuth, err := KeyAuthorization(ch.Token, jwk)\n\tif err != nil {\n\t\treturn nil, WrapErrorISE(err, \"failed creating key auth digest\")\n\t}\n\thashedKeyAuth := sha256.Sum256([]byte(keyAuth))\n\n\t// verify the WebAuthn object contains the expect key authorization digest, which is carried\n\t// within the encoded `certInfo` property of the attestation statement.\n\tif subtle.ConstantTimeCompare(hashedKeyAuth[:], []byte(tpmCertInfo.ExtraData)) == 0 {\n\t\treturn nil, NewDetailedError(ErrorBadAttestationStatementType, \"key authorization invalid\")\n\t}\n\n\t// decode the (attested) public key and determine its fingerprint.  This won't fail, as it's also done as part of Verify().\n\tpub, err := tpm2.DecodePublic(pubArea)\n\tif err != nil {\n\t\treturn nil, WrapDetailedError(ErrorBadAttestationStatementType, err, \"failed decoding pubArea\")\n\t}\n\n\tpublicKey, err := pub.Key()\n\tif err != nil {\n\t\treturn nil, WrapDetailedError(ErrorBadAttestationStatementType, err, \"failed getting public key\")\n\t}\n\n\tdata := &tpmAttestationData{\n\t\tCertificate:          akCert,\n\t\tVerifiedChains:       verifiedChains,\n\t\tPermanentIdentifiers: permanentIdentifiers,\n\t}\n\n\tif data.Fingerprint, err = keyutil.Fingerprint(publicKey); err != nil {\n\t\treturn nil, WrapErrorISE(err, \"error calculating key fingerprint\")\n\t}\n\n\t// TODO(hs): pass more attestation data, so that that can be used/recorded too?\n\treturn data, nil\n}\n\nvar (\n\toidExtensionExtendedKeyUsage = asn1.ObjectIdentifier{2, 5, 29, 37}\n\toidTCGKpAIKCertificate       = asn1.ObjectIdentifier{2, 23, 133, 8, 3}\n)\n\n// validateAKCertificate validates the X.509 AK certificate to be\n// in accordance with the required properties. The requirements come from:\n// https://www.w3.org/TR/webauthn-2/#sctn-tpm-cert-requirements.\n//\n//   - Version MUST be set to 3.\n//   - Subject field MUST be set to empty.\n//   - The Subject Alternative Name extension MUST be set as defined\n//     in [TPMv2-EK-Profile] section 3.2.9.\n//   - The Extended Key Usage extension MUST contain the OID 2.23.133.8.3\n//     (\"joint-iso-itu-t(2) international-organizations(23) 133 tcg-kp(8) tcg-kp-AIKCertificate(3)\").\n//   - The Basic Constraints extension MUST have the CA component set to false.\n//   - An Authority Information Access (AIA) extension with entry id-ad-ocsp\n//     and a CRL Distribution Point extension [RFC5280] are both OPTIONAL as\n//     the status of many attestation certificates is available through metadata\n//     services. See, for example, the FIDO Metadata Service.\nfunc validateAKCertificate(c *x509.Certificate) error {\n\tif c.Version != 3 {\n\t\treturn fmt.Errorf(\"AK certificate has invalid version %d; only version 3 is allowed\", c.Version)\n\t}\n\tif c.Subject.String() != \"\" {\n\t\treturn fmt.Errorf(\"AK certificate subject must be empty; got %q\", c.Subject)\n\t}\n\tif c.IsCA {\n\t\treturn errors.New(\"AK certificate must not be a CA\")\n\t}\n\tif err := validateAKCertificateExtendedKeyUsage(c); err != nil {\n\t\treturn err\n\t}\n\treturn validateAKCertificateSubjectAlternativeNames(c)\n}\n\n// validateAKCertificateSubjectAlternativeNames checks if the AK certificate\n// has TPM hardware details set.\nfunc validateAKCertificateSubjectAlternativeNames(c *x509.Certificate) error {\n\tsans, err := x509util.ParseSubjectAlternativeNames(c)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed parsing AK certificate Subject Alternative Names: %w\", err)\n\t}\n\n\tdetails := sans.TPMHardwareDetails\n\tmanufacturer, model, version := details.Manufacturer, details.Model, details.Version\n\n\tswitch {\n\tcase manufacturer == \"\":\n\t\treturn errors.New(\"missing TPM manufacturer\")\n\tcase model == \"\":\n\t\treturn errors.New(\"missing TPM model\")\n\tcase version == \"\":\n\t\treturn errors.New(\"missing TPM version\")\n\t}\n\n\treturn nil\n}\n\n// validateAKCertificateExtendedKeyUsage checks if the AK certificate\n// has the \"tcg-kp-AIKCertificate\" Extended Key Usage set.\nfunc validateAKCertificateExtendedKeyUsage(c *x509.Certificate) error {\n\tvar (\n\t\tvalid = false\n\t\tekus  []asn1.ObjectIdentifier\n\t)\n\tfor _, ext := range c.Extensions {\n\t\tif ext.Id.Equal(oidExtensionExtendedKeyUsage) {\n\t\t\tif _, err := asn1.Unmarshal(ext.Value, &ekus); err != nil || len(ekus) == 0 || !ekus[0].Equal(oidTCGKpAIKCertificate) {\n\t\t\t\treturn errors.New(\"AK certificate is missing Extended Key Usage value tcg-kp-AIKCertificate (2.23.133.8.3)\")\n\t\t\t}\n\t\t\tvalid = true\n\t\t}\n\t}\n\n\tif !valid {\n\t\treturn errors.New(\"AK certificate is missing Extended Key Usage extension\")\n\t}\n\n\treturn nil\n}\n\n// Apple Enterprise Attestation Root CA from\n// https://www.apple.com/certificateauthority/private/\nconst appleEnterpriseAttestationRootCA = `-----BEGIN CERTIFICATE-----\nMIICJDCCAamgAwIBAgIUQsDCuyxyfFxeq/bxpm8frF15hzcwCgYIKoZIzj0EAwMw\nUTEtMCsGA1UEAwwkQXBwbGUgRW50ZXJwcmlzZSBBdHRlc3RhdGlvbiBSb290IENB\nMRMwEQYDVQQKDApBcHBsZSBJbmMuMQswCQYDVQQGEwJVUzAeFw0yMjAyMTYxOTAx\nMjRaFw00NzAyMjAwMDAwMDBaMFExLTArBgNVBAMMJEFwcGxlIEVudGVycHJpc2Ug\nQXR0ZXN0YXRpb24gUm9vdCBDQTETMBEGA1UECgwKQXBwbGUgSW5jLjELMAkGA1UE\nBhMCVVMwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAT6Jigq+Ps9Q4CoT8t8q+UnOe2p\noT9nRaUfGhBTbgvqSGXPjVkbYlIWYO+1zPk2Sz9hQ5ozzmLrPmTBgEWRcHjA2/y7\n7GEicps9wn2tj+G89l3INNDKETdxSPPIZpPj8VmjQjBAMA8GA1UdEwEB/wQFMAMB\nAf8wHQYDVR0OBBYEFPNqTQGd8muBpV5du+UIbVbi+d66MA4GA1UdDwEB/wQEAwIB\nBjAKBggqhkjOPQQDAwNpADBmAjEA1xpWmTLSpr1VH4f8Ypk8f3jMUKYz4QPG8mL5\n8m9sX/b2+eXpTv2pH4RZgJjucnbcAjEA4ZSB6S45FlPuS/u4pTnzoz632rA+xW/T\nZwFEh9bhKjJ+5VQ9/Do1os0u3LEkgN/r\n-----END CERTIFICATE-----`\n\nvar (\n\toidAppleSerialNumber                    = asn1.ObjectIdentifier{1, 2, 840, 113635, 100, 8, 9, 1}\n\toidAppleUniqueDeviceIdentifier          = asn1.ObjectIdentifier{1, 2, 840, 113635, 100, 8, 9, 2}\n\toidAppleSecureEnclaveProcessorOSVersion = asn1.ObjectIdentifier{1, 2, 840, 113635, 100, 8, 10, 2}\n\toidAppleNonce                           = asn1.ObjectIdentifier{1, 2, 840, 113635, 100, 8, 11, 1}\n)\n\ntype appleAttestationData struct {\n\tNonce        []byte\n\tSerialNumber string\n\tUDID         string\n\tSEPVersion   string\n\tCertificate  *x509.Certificate\n\tFingerprint  string\n}\n\nfunc doAppleAttestationFormat(_ context.Context, prov Provisioner, _ *Challenge, att *attestationObject) (*appleAttestationData, error) {\n\t// Use configured or default attestation roots if none is configured.\n\troots, ok := prov.GetAttestationRoots()\n\tif !ok {\n\t\troot, err := pemutil.ParseCertificate([]byte(appleEnterpriseAttestationRootCA))\n\t\tif err != nil {\n\t\t\treturn nil, WrapErrorISE(err, \"error parsing apple enterprise ca\")\n\t\t}\n\t\troots = x509.NewCertPool()\n\t\troots.AddCert(root)\n\t}\n\n\tx5c, ok := att.AttStatement[\"x5c\"].([]interface{})\n\tif !ok {\n\t\treturn nil, NewDetailedError(ErrorBadAttestationStatementType, \"x5c not present\")\n\t}\n\tif len(x5c) == 0 {\n\t\treturn nil, NewDetailedError(ErrorBadAttestationStatementType, \"x5c is empty\")\n\t}\n\n\tder, ok := x5c[0].([]byte)\n\tif !ok {\n\t\treturn nil, NewDetailedError(ErrorBadAttestationStatementType, \"x5c is malformed\")\n\t}\n\tleaf, err := x509.ParseCertificate(der)\n\tif err != nil {\n\t\treturn nil, WrapDetailedError(ErrorBadAttestationStatementType, err, \"x5c is malformed\")\n\t}\n\n\tintermediates := x509.NewCertPool()\n\tfor _, v := range x5c[1:] {\n\t\tder, ok = v.([]byte)\n\t\tif !ok {\n\t\t\treturn nil, NewDetailedError(ErrorBadAttestationStatementType, \"x5c is malformed\")\n\t\t}\n\t\tcert, err := x509.ParseCertificate(der)\n\t\tif err != nil {\n\t\t\treturn nil, WrapDetailedError(ErrorBadAttestationStatementType, err, \"x5c is malformed\")\n\t\t}\n\t\tintermediates.AddCert(cert)\n\t}\n\n\tif _, err := leaf.Verify(x509.VerifyOptions{\n\t\tIntermediates: intermediates,\n\t\tRoots:         roots,\n\t\tCurrentTime:   time.Now().Truncate(time.Second),\n\t\tKeyUsages:     []x509.ExtKeyUsage{x509.ExtKeyUsageAny},\n\t}); err != nil {\n\t\treturn nil, WrapDetailedError(ErrorBadAttestationStatementType, err, \"x5c is not valid\")\n\t}\n\n\tdata := &appleAttestationData{\n\t\tCertificate: leaf,\n\t}\n\tif data.Fingerprint, err = keyutil.Fingerprint(leaf.PublicKey); err != nil {\n\t\treturn nil, WrapErrorISE(err, \"error calculating key fingerprint\")\n\t}\n\tfor _, ext := range leaf.Extensions {\n\t\tswitch {\n\t\tcase ext.Id.Equal(oidAppleSerialNumber):\n\t\t\tdata.SerialNumber = string(ext.Value)\n\t\tcase ext.Id.Equal(oidAppleUniqueDeviceIdentifier):\n\t\t\tdata.UDID = string(ext.Value)\n\t\tcase ext.Id.Equal(oidAppleSecureEnclaveProcessorOSVersion):\n\t\t\tdata.SEPVersion = string(ext.Value)\n\t\tcase ext.Id.Equal(oidAppleNonce):\n\t\t\tdata.Nonce = ext.Value\n\t\t}\n\t}\n\n\treturn data, nil\n}\n\n// Yubico PIV Root CA Serial 263751\n// https://developers.yubico.com/PIV/Introduction/piv-attestation-ca.pem\nconst yubicoPIVRootCA = `-----BEGIN CERTIFICATE-----\nMIIDFzCCAf+gAwIBAgIDBAZHMA0GCSqGSIb3DQEBCwUAMCsxKTAnBgNVBAMMIFl1\nYmljbyBQSVYgUm9vdCBDQSBTZXJpYWwgMjYzNzUxMCAXDTE2MDMxNDAwMDAwMFoY\nDzIwNTIwNDE3MDAwMDAwWjArMSkwJwYDVQQDDCBZdWJpY28gUElWIFJvb3QgQ0Eg\nU2VyaWFsIDI2Mzc1MTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMN2\ncMTNR6YCdcTFRxuPy31PabRn5m6pJ+nSE0HRWpoaM8fc8wHC+Tmb98jmNvhWNE2E\nilU85uYKfEFP9d6Q2GmytqBnxZsAa3KqZiCCx2LwQ4iYEOb1llgotVr/whEpdVOq\njoU0P5e1j1y7OfwOvky/+AXIN/9Xp0VFlYRk2tQ9GcdYKDmqU+db9iKwpAzid4oH\nBVLIhmD3pvkWaRA2H3DA9t7H/HNq5v3OiO1jyLZeKqZoMbPObrxqDg+9fOdShzgf\nwCqgT3XVmTeiwvBSTctyi9mHQfYd2DwkaqxRnLbNVyK9zl+DzjSGp9IhVPiVtGet\nX02dxhQnGS7K6BO0Qe8CAwEAAaNCMEAwHQYDVR0OBBYEFMpfyvLEojGc6SJf8ez0\n1d8Cv4O/MA8GA1UdEwQIMAYBAf8CAQEwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3\nDQEBCwUAA4IBAQBc7Ih8Bc1fkC+FyN1fhjWioBCMr3vjneh7MLbA6kSoyWF70N3s\nXhbXvT4eRh0hvxqvMZNjPU/VlRn6gLVtoEikDLrYFXN6Hh6Wmyy1GTnspnOvMvz2\nlLKuym9KYdYLDgnj3BeAvzIhVzzYSeU77/Cupofj093OuAswW0jYvXsGTyix6B3d\nbW5yWvyS9zNXaqGaUmP3U9/b6DlHdDogMLu3VLpBB9bm5bjaKWWJYgWltCVgUbFq\nFqyi4+JE014cSgR57Jcu3dZiehB6UtAPgad9L5cNvua/IWRmm+ANy3O2LH++Pyl8\nSREzU8onbBsjMg9QDiSf5oJLKvd/Ren+zGY7\n-----END CERTIFICATE-----`\n\n// Yubico Attestation Root 1 (YubiKey 5.7.4+)\n// https://developers.yubico.com/PKI/yubico-ca-1.pem\nconst yubicoAttestationRootCA = `-----BEGIN CERTIFICATE-----\nMIIDPjCCAiagAwIBAgIUXzeiEDJEOTt14F5n0o6Zf/bBwiUwDQYJKoZIhvcNAQEN\nBQAwJDEiMCAGA1UEAwwZWXViaWNvIEF0dGVzdGF0aW9uIFJvb3QgMTAgFw0yNDEy\nMDEwMDAwMDBaGA85OTk5MTIzMTIzNTk1OVowJDEiMCAGA1UEAwwZWXViaWNvIEF0\ndGVzdGF0aW9uIFJvb3QgMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB\nAMZ6/TxM8rIT+EaoPvG81ontMOo/2mQ2RBwJHS0QZcxVaNXvl12LUhBZ5LmiBScI\nZd1Rnx1od585h+/dhK7hEm7JAALkKKts1fO53KGNLZujz5h3wGncr4hyKF0G74b/\nU3K9hE5mGND6zqYchCRAHfrYMYRDF4YL0X4D5nGdxvppAy6nkEmtWmMnwO3i0TAu\ncsrbE485HvGM4r0VpgVdJpvgQjiTJCTIq+D35hwtT8QDIv+nGvpcyi5wcIfCkzyC\nimJukhYy6KoqNMKQEdpNiSOvWyDMTMt1bwCvEzpw91u+msUt4rj0efnO9s0ZOwdw\nMRDnH4xgUl5ZLwrrPkfC1/0CAwEAAaNmMGQwHQYDVR0OBBYEFNLu71oijTptXCOX\nPfKF1SbxJXuSMB8GA1UdIwQYMBaAFNLu71oijTptXCOXPfKF1SbxJXuSMBIGA1Ud\nEwEB/wQIMAYBAf8CAQMwDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBDQUAA4IB\nAQC3IW/sgB9pZ8apJNjxuGoX+FkILks0wMNrdXL/coUvsrhzsvl6mePMrbGJByJ1\nXnquB5sgcRENFxdQFma3mio8Upf1owM1ZreXrJ0mADG2BplqbJnxiyYa+R11reIF\nTWeIhMNcZKsDZrFAyPuFjCWSQvJmNWe9mFRYFgNhXJKkXIb5H1XgEDlwiedYRM7V\nolBNlld6pRFKlX8ust6OTMOeADl2xNF0m1LThSdeuXvDyC1g9+ILfz3S6OIYgc3i\nroRcFD354g7rKfu67qFAw9gC4yi0xBTPrY95rh4/HqaUYCA/L8ldRk6H7Xk35D+W\nVpmq2Sh/xT5HiFuhf4wJb0bK\n-----END CERTIFICATE-----`\n\nvar (\n\t// serial number of the YubiKey, encoded as an integer.\n\t// https://developers.yubico.com/PIV/Introduction/PIV_attestation.html\n\toidYubicoSerialNumber = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 41482, 3, 7}\n\n\t// custom Smallstep managed device extension carrying a device ID or serial number\n\toidStepManagedDevice = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 37476, 9000, 64, 4}\n)\n\ntype stepAttestationData struct {\n\tCertificate  *x509.Certificate\n\tSerialNumber string\n\tFingerprint  string\n}\n\nfunc doStepAttestationFormat(_ context.Context, prov Provisioner, ch *Challenge, jwk *jose.JSONWebKey, att *attestationObject) (*stepAttestationData, error) {\n\t// Use configured or default attestation roots if none is configured.\n\troots, ok := prov.GetAttestationRoots()\n\tif !ok {\n\t\tpivRoot, err := pemutil.ParseCertificate([]byte(yubicoPIVRootCA))\n\t\tif err != nil {\n\t\t\treturn nil, WrapErrorISE(err, \"error parsing root ca\")\n\t\t}\n\t\tattRoot, err := pemutil.ParseCertificate([]byte(yubicoAttestationRootCA))\n\t\tif err != nil {\n\t\t\treturn nil, WrapErrorISE(err, \"error parsing root ca\")\n\t\t}\n\t\troots = x509.NewCertPool()\n\t\troots.AddCert(pivRoot)\n\t\troots.AddCert(attRoot)\n\t}\n\n\t// Extract x5c and verify certificate\n\tx5c, ok := att.AttStatement[\"x5c\"].([]interface{})\n\tif !ok {\n\t\treturn nil, NewDetailedError(ErrorBadAttestationStatementType, \"x5c not present\")\n\t}\n\tif len(x5c) == 0 {\n\t\treturn nil, NewDetailedError(ErrorRejectedIdentifierType, \"x5c is empty\")\n\t}\n\tder, ok := x5c[0].([]byte)\n\tif !ok {\n\t\treturn nil, NewDetailedError(ErrorBadAttestationStatementType, \"x5c is malformed\")\n\t}\n\tleaf, err := x509.ParseCertificate(der)\n\tif err != nil {\n\t\treturn nil, WrapDetailedError(ErrorBadAttestationStatementType, err, \"x5c is malformed\")\n\t}\n\tintermediates := x509.NewCertPool()\n\tfor _, v := range x5c[1:] {\n\t\tder, ok = v.([]byte)\n\t\tif !ok {\n\t\t\treturn nil, NewDetailedError(ErrorBadAttestationStatementType, \"x5c is malformed\")\n\t\t}\n\t\tcert, err := x509.ParseCertificate(der)\n\t\tif err != nil {\n\t\t\treturn nil, WrapDetailedError(ErrorBadAttestationStatementType, err, \"x5c is malformed\")\n\t\t}\n\t\tintermediates.AddCert(cert)\n\t}\n\tif _, err := leaf.Verify(x509.VerifyOptions{\n\t\tIntermediates: intermediates,\n\t\tRoots:         roots,\n\t\tCurrentTime:   time.Now().Truncate(time.Second),\n\t\tKeyUsages:     []x509.ExtKeyUsage{x509.ExtKeyUsageAny},\n\t}); err != nil {\n\t\treturn nil, WrapDetailedError(ErrorBadAttestationStatementType, err, \"x5c is not valid\")\n\t}\n\n\t// Verify proof of possession of private key validating the key\n\t// authorization. Per recommendation at\n\t// https://w3c.github.io/webauthn/#sctn-signature-attestation-types the\n\t// signature is CBOR-encoded.\n\tvar sig []byte\n\tcsig, ok := att.AttStatement[\"sig\"].([]byte)\n\tif !ok {\n\t\treturn nil, NewDetailedError(ErrorBadAttestationStatementType, \"sig not present\")\n\t}\n\tif err := cbor.Unmarshal(csig, &sig); err != nil {\n\t\treturn nil, NewDetailedError(ErrorBadAttestationStatementType, \"sig is malformed\")\n\t}\n\tkeyAuth, err := KeyAuthorization(ch.Token, jwk)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tswitch pub := leaf.PublicKey.(type) {\n\tcase *ecdsa.PublicKey:\n\t\tif pub.Curve != elliptic.P256() {\n\t\t\treturn nil, WrapDetailedError(ErrorBadAttestationStatementType, err, \"unsupported elliptic curve %s\", pub.Curve)\n\t\t}\n\t\tsum := sha256.Sum256([]byte(keyAuth))\n\t\tif !ecdsa.VerifyASN1(pub, sum[:], sig) {\n\t\t\treturn nil, NewDetailedError(ErrorBadAttestationStatementType, \"failed to validate signature\")\n\t\t}\n\tcase *rsa.PublicKey:\n\t\tsum := sha256.Sum256([]byte(keyAuth))\n\t\tif err := rsa.VerifyPKCS1v15(pub, crypto.SHA256, sum[:], sig); err != nil {\n\t\t\treturn nil, NewDetailedError(ErrorBadAttestationStatementType, \"failed to validate signature\")\n\t\t}\n\tcase ed25519.PublicKey:\n\t\tif !ed25519.Verify(pub, []byte(keyAuth), sig) {\n\t\t\treturn nil, NewDetailedError(ErrorBadAttestationStatementType, \"failed to validate signature\")\n\t\t}\n\tdefault:\n\t\treturn nil, NewDetailedError(ErrorBadAttestationStatementType, \"unsupported public key type %T\", pub)\n\t}\n\n\t// Parse attestation data:\n\t// TODO(mariano): add support for other extensions.\n\tdata := &stepAttestationData{\n\t\tCertificate: leaf,\n\t}\n\n\tif data.Fingerprint, err = keyutil.Fingerprint(leaf.PublicKey); err != nil {\n\t\treturn nil, WrapErrorISE(err, \"error calculating key fingerprint\")\n\t}\n\n\tif data.SerialNumber, err = searchSerialNumber(leaf); err != nil {\n\t\treturn nil, WrapErrorISE(err, \"error finding serial number\")\n\t}\n\n\treturn data, nil\n}\n\n// searchSerialNumber searches the certificate extensions, looking for a serial\n// number encoded in one of them. It is not guaranteed that a certificate contains\n// an extension carrying a serial number, so the result can be empty.\nfunc searchSerialNumber(cert *x509.Certificate) (string, error) {\n\tfor _, ext := range cert.Extensions {\n\t\tif ext.Id.Equal(oidYubicoSerialNumber) {\n\t\t\tvar serialNumber int\n\t\t\trest, err := asn1.Unmarshal(ext.Value, &serialNumber)\n\t\t\tif err != nil || len(rest) > 0 {\n\t\t\t\treturn \"\", WrapError(ErrorBadAttestationStatementType, err, \"error parsing serial number\")\n\t\t\t}\n\t\t\treturn strconv.Itoa(serialNumber), nil\n\t\t}\n\t\tif ext.Id.Equal(oidStepManagedDevice) {\n\t\t\ttype stepManagedDevice struct {\n\t\t\t\tDeviceID string\n\t\t\t}\n\t\t\tvar md stepManagedDevice\n\t\t\trest, err := asn1.Unmarshal(ext.Value, &md)\n\t\t\tif err != nil || len(rest) > 0 {\n\t\t\t\treturn \"\", WrapError(ErrorBadAttestationStatementType, err, \"error parsing serial number\")\n\t\t\t}\n\t\t\treturn md.DeviceID, nil\n\t\t}\n\t}\n\n\treturn \"\", nil\n}\n\n// serverName determines the SNI HostName to set based on an acme.Challenge\n// for TLS-ALPN-01 challenges RFC8738 states that, if HostName is an IP, it\n// should be the ARPA address https://datatracker.ietf.org/doc/html/rfc8738#section-6.\n// It also references TLS Extensions [RFC6066].\nfunc serverName(ch *Challenge) string {\n\tif ip := net.ParseIP(ch.Value); ip != nil {\n\t\treturn reverseAddr(ip)\n\t}\n\treturn ch.Value\n}\n\n// reverseaddr returns the in-addr.arpa. or ip6.arpa. hostname of the IP\n// address addr suitable for rDNS (PTR) record lookup or an error if it fails\n// to parse the IP address.\n// Implementation taken and adapted from https://golang.org/src/net/dnsclient.go?s=780:834#L20\nfunc reverseAddr(ip net.IP) (arpa string) {\n\tif ip.To4() != nil {\n\t\treturn uitoa(uint(ip[15])) + \".\" + uitoa(uint(ip[14])) + \".\" + uitoa(uint(ip[13])) + \".\" + uitoa(uint(ip[12])) + \".in-addr.arpa.\"\n\t}\n\t// Must be IPv6\n\tbuf := make([]byte, 0, len(ip)*4+len(\"ip6.arpa.\"))\n\t// Add it, in reverse, to the buffer\n\tfor i := len(ip) - 1; i >= 0; i-- {\n\t\tv := ip[i]\n\t\tbuf = append(buf, hexit[v&0xF],\n\t\t\t'.',\n\t\t\thexit[v>>4],\n\t\t\t'.')\n\t}\n\t// Append \"ip6.arpa.\" and return (buf already has the final .)\n\tbuf = append(buf, \"ip6.arpa.\"...)\n\treturn string(buf)\n}\n\n// Convert unsigned integer to decimal string.\n// Implementation taken from https://golang.org/src/net/parse.go\nfunc uitoa(val uint) string {\n\tif val == 0 { // avoid string allocation\n\t\treturn \"0\"\n\t}\n\tvar buf [20]byte // big enough for 64bit value base 10\n\ti := len(buf) - 1\n\tfor val >= 10 {\n\t\tv := val / 10\n\t\tbuf[i] = byte('0' + val - v*10) //nolint:gosec // val - v*10 is always 0-9\n\t\ti--\n\t\tval = v\n\t}\n\t// val < 10\n\tbuf[i] = byte('0' + val)\n\treturn string(buf[i:])\n}\n\nconst hexit = \"0123456789abcdef\"\n\n// KeyAuthorization creates the ACME key authorization value from a token\n// and a jwk.\nfunc KeyAuthorization(token string, jwk *jose.JSONWebKey) (string, error) {\n\tthumbprint, err := jwk.Thumbprint(crypto.SHA256)\n\tif err != nil {\n\t\treturn \"\", WrapErrorISE(err, \"error generating JWK thumbprint\")\n\t}\n\tencPrint := base64.RawURLEncoding.EncodeToString(thumbprint)\n\treturn fmt.Sprintf(\"%s.%s\", token, encPrint), nil\n}\n\n// storeError the given error to an ACME error and saves using the DB interface.\nfunc storeError(ctx context.Context, db DB, ch *Challenge, markInvalid bool, err *Error) error {\n\tch.Error = err\n\tif markInvalid {\n\t\tch.Status = StatusInvalid\n\t}\n\tif err := db.UpdateChallenge(ctx, ch); err != nil {\n\t\treturn WrapErrorISE(err, \"failure saving error to acme challenge\")\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "acme/challenge_test.go",
    "content": "package acme\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto\"\n\t\"crypto/ecdsa\"\n\t\"crypto/elliptic\"\n\t\"crypto/rand\"\n\t\"crypto/rsa\"\n\t\"crypto/sha256\"\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"crypto/x509/pkix\"\n\t\"encoding/asn1\"\n\t\"encoding/base64\"\n\t\"encoding/hex\"\n\t\"encoding/json\"\n\t\"encoding/pem\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"math/big\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"reflect\"\n\t\"strconv\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/fxamacker/cbor/v2\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"go.step.sm/crypto/jose\"\n\t\"go.step.sm/crypto/keyutil\"\n\t\"go.step.sm/crypto/minica\"\n\t\"go.step.sm/crypto/pemutil\"\n\t\"go.step.sm/crypto/x509util\"\n\n\t\"github.com/smallstep/certificates/authority/config\"\n\t\"github.com/smallstep/certificates/authority/provisioner\"\n\twireprovisioner \"github.com/smallstep/certificates/authority/provisioner/wire\"\n)\n\ntype mockClient struct {\n\tget       func(url string) (*http.Response, error)\n\tlookupTxt func(name string) ([]string, error)\n\ttlsDial   func(network, addr string, config *tls.Config) (*tls.Conn, error)\n}\n\nfunc (m *mockClient) Get(url string) (*http.Response, error)  { return m.get(url) }\nfunc (m *mockClient) LookupTxt(name string) ([]string, error) { return m.lookupTxt(name) }\nfunc (m *mockClient) TLSDial(network, addr string, tlsConfig *tls.Config) (*tls.Conn, error) {\n\treturn m.tlsDial(network, addr, tlsConfig)\n}\n\nfunc fatalError(t *testing.T, err error) {\n\tt.Helper()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n}\n\nfunc mustNonAttestationProvisioner(t *testing.T) Provisioner {\n\tt.Helper()\n\n\tprov := &provisioner.ACME{\n\t\tType:       \"ACME\",\n\t\tName:       \"acme\",\n\t\tChallenges: []provisioner.ACMEChallenge{provisioner.HTTP_01},\n\t}\n\tif err := prov.Init(provisioner.Config{\n\t\tClaims: config.GlobalProvisionerClaims,\n\t}); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tprov.AttestationFormats = []provisioner.ACMEAttestationFormat{\"bogus-format\"} // results in no attestation formats enabled\n\treturn prov\n}\n\nfunc mustAttestationProvisioner(t *testing.T, roots []byte) Provisioner {\n\tt.Helper()\n\n\tprov := &provisioner.ACME{\n\t\tType:             \"ACME\",\n\t\tName:             \"acme\",\n\t\tChallenges:       []provisioner.ACMEChallenge{provisioner.DEVICE_ATTEST_01},\n\t\tAttestationRoots: roots,\n\t}\n\tif err := prov.Init(provisioner.Config{\n\t\tClaims: config.GlobalProvisionerClaims,\n\t}); err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn prov\n}\n\nfunc mustAccountAndKeyAuthorization(t *testing.T, token string) (*jose.JSONWebKey, string) {\n\tt.Helper()\n\n\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\tfatalError(t, err)\n\n\tkeyAuth, err := KeyAuthorization(token, jwk)\n\tfatalError(t, err)\n\treturn jwk, keyAuth\n}\n\nfunc mustAttestApple(t *testing.T, nonce string) ([]byte, *x509.Certificate, *x509.Certificate) {\n\tt.Helper()\n\n\tca, err := minica.New()\n\tfatalError(t, err)\n\n\tsigner, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)\n\tfatalError(t, err)\n\n\tnonceSum := sha256.Sum256([]byte(nonce))\n\tleaf, err := ca.Sign(&x509.Certificate{\n\t\tSubject:   pkix.Name{CommonName: \"attestation cert\"},\n\t\tPublicKey: signer.Public(),\n\t\tExtraExtensions: []pkix.Extension{\n\t\t\t{Id: oidAppleSerialNumber, Value: []byte(\"serial-number\")},\n\t\t\t{Id: oidAppleUniqueDeviceIdentifier, Value: []byte(\"udid\")},\n\t\t\t{Id: oidAppleSecureEnclaveProcessorOSVersion, Value: []byte(\"16.0\")},\n\t\t\t{Id: oidAppleNonce, Value: nonceSum[:]},\n\t\t},\n\t})\n\tfatalError(t, err)\n\n\tattObj, err := cbor.Marshal(struct {\n\t\tFormat       string                 `json:\"fmt\"`\n\t\tAttStatement map[string]interface{} `json:\"attStmt,omitempty\"`\n\t}{\n\t\tFormat: \"apple\",\n\t\tAttStatement: map[string]interface{}{\n\t\t\t\"x5c\": []interface{}{leaf.Raw, ca.Intermediate.Raw},\n\t\t},\n\t})\n\tfatalError(t, err)\n\n\tpayload, err := json.Marshal(struct {\n\t\tAttObj string `json:\"attObj\"`\n\t}{\n\t\tAttObj: base64.RawURLEncoding.EncodeToString(attObj),\n\t})\n\tfatalError(t, err)\n\n\treturn payload, leaf, ca.Root\n}\n\nfunc mustAttestYubikey(t *testing.T, _, keyAuthorization string, serial int) ([]byte, *x509.Certificate, *x509.Certificate) {\n\tt.Helper()\n\n\tca, err := minica.New()\n\tfatalError(t, err)\n\n\tkeyAuthSum := sha256.Sum256([]byte(keyAuthorization))\n\n\tsigner, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)\n\tfatalError(t, err)\n\tsig, err := signer.Sign(rand.Reader, keyAuthSum[:], crypto.SHA256)\n\tfatalError(t, err)\n\tcborSig, err := cbor.Marshal(sig)\n\tfatalError(t, err)\n\n\tserialNumber, err := asn1.Marshal(serial)\n\tfatalError(t, err)\n\n\tleaf, err := ca.Sign(&x509.Certificate{\n\t\tSubject:   pkix.Name{CommonName: \"attestation cert\"},\n\t\tPublicKey: signer.Public(),\n\t\tExtraExtensions: []pkix.Extension{\n\t\t\t{Id: oidYubicoSerialNumber, Value: serialNumber},\n\t\t},\n\t})\n\tfatalError(t, err)\n\n\tattObj, err := cbor.Marshal(struct {\n\t\tFormat       string                 `json:\"fmt\"`\n\t\tAttStatement map[string]interface{} `json:\"attStmt,omitempty\"`\n\t}{\n\t\tFormat: \"step\",\n\t\tAttStatement: map[string]interface{}{\n\t\t\t\"x5c\": []interface{}{leaf.Raw, ca.Intermediate.Raw},\n\t\t\t\"alg\": -7,\n\t\t\t\"sig\": cborSig,\n\t\t},\n\t})\n\tfatalError(t, err)\n\n\tpayload, err := json.Marshal(struct {\n\t\tAttObj string `json:\"attObj\"`\n\t}{\n\t\tAttObj: base64.RawURLEncoding.EncodeToString(attObj),\n\t})\n\tfatalError(t, err)\n\n\treturn payload, leaf, ca.Root\n}\n\ntype stepManagedDevice struct {\n\tDeviceID string\n}\n\nfunc mustAttestStepManagedDeviceID(t *testing.T, _, keyAuthorization, serialNumber string) ([]byte, *x509.Certificate, *x509.Certificate) {\n\tt.Helper()\n\n\tca, err := minica.New()\n\trequire.NoError(t, err)\n\n\tkeyAuthSum := sha256.Sum256([]byte(keyAuthorization))\n\n\tsigner, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)\n\trequire.NoError(t, err)\n\tsig, err := signer.Sign(rand.Reader, keyAuthSum[:], crypto.SHA256)\n\trequire.NoError(t, err)\n\tcborSig, err := cbor.Marshal(sig)\n\trequire.NoError(t, err)\n\n\tv, err := asn1.Marshal(stepManagedDevice{DeviceID: serialNumber})\n\trequire.NoError(t, err)\n\n\tleaf, err := ca.Sign(&x509.Certificate{\n\t\tSubject:   pkix.Name{CommonName: \"attestation cert\"},\n\t\tPublicKey: signer.Public(),\n\t\tExtraExtensions: []pkix.Extension{\n\t\t\t{Id: oidStepManagedDevice, Value: v},\n\t\t},\n\t})\n\trequire.NoError(t, err)\n\n\tattObj, err := cbor.Marshal(struct {\n\t\tFormat       string                 `json:\"fmt\"`\n\t\tAttStatement map[string]interface{} `json:\"attStmt,omitempty\"`\n\t}{\n\t\tFormat: \"step\",\n\t\tAttStatement: map[string]interface{}{\n\t\t\t\"x5c\": []interface{}{leaf.Raw, ca.Intermediate.Raw},\n\t\t\t\"alg\": -7,\n\t\t\t\"sig\": cborSig,\n\t\t},\n\t})\n\trequire.NoError(t, err)\n\n\tpayload, err := json.Marshal(struct {\n\t\tAttObj string `json:\"attObj\"`\n\t}{\n\t\tAttObj: base64.RawURLEncoding.EncodeToString(attObj),\n\t})\n\trequire.NoError(t, err)\n\n\treturn payload, leaf, ca.Root\n}\n\nfunc newWireProvisionerWithOptions(t *testing.T, options *provisioner.Options) *provisioner.ACME {\n\tt.Helper()\n\tprov := &provisioner.ACME{\n\t\tType:    \"ACME\",\n\t\tName:    \"wire\",\n\t\tOptions: options,\n\t\tChallenges: []provisioner.ACMEChallenge{\n\t\t\tprovisioner.WIREOIDC_01,\n\t\t\tprovisioner.WIREDPOP_01,\n\t\t},\n\t}\n\tif err := prov.Init(provisioner.Config{\n\t\tClaims: config.GlobalProvisionerClaims,\n\t}); err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn prov\n}\n\nfunc Test_storeError(t *testing.T) {\n\ttype test struct {\n\t\tch          *Challenge\n\t\tdb          DB\n\t\tmarkInvalid bool\n\t\terr         *Error\n\t}\n\terr := NewError(ErrorMalformedType, \"foo\")\n\ttests := map[string]func(t *testing.T) test{\n\t\t\"fail/db.UpdateChallenge-error\": func(t *testing.T) test {\n\t\t\tch := &Challenge{\n\t\t\t\tID:     \"chID\",\n\t\t\t\tToken:  \"token\",\n\t\t\t\tValue:  \"zap.internal\",\n\t\t\t\tStatus: StatusValid,\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tch: ch,\n\t\t\t\tdb: &MockDB{\n\t\t\t\t\tMockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {\n\t\t\t\t\t\tassert.Equal(t, \"chID\", updch.ID)\n\t\t\t\t\t\tassert.Equal(t, \"token\", updch.Token)\n\t\t\t\t\t\tassert.Equal(t, \"zap.internal\", updch.Value)\n\t\t\t\t\t\tassert.Equal(t, StatusValid, updch.Status)\n\n\t\t\t\t\t\tassert.EqualError(t, updch.Error.Err, err.Err.Error())\n\t\t\t\t\t\tassert.Equal(t, err.Type, updch.Error.Type)\n\t\t\t\t\t\tassert.Equal(t, err.Detail, updch.Error.Detail)\n\t\t\t\t\t\tassert.Equal(t, err.Status, updch.Error.Status)\n\t\t\t\t\t\tassert.Equal(t, err.Subproblems, updch.Error.Subproblems)\n\n\t\t\t\t\t\treturn errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: NewErrorISE(\"failure saving error to acme challenge: force\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/db.UpdateChallenge-acme-error\": func(t *testing.T) test {\n\t\t\tch := &Challenge{\n\t\t\t\tID:     \"chID\",\n\t\t\t\tToken:  \"token\",\n\t\t\t\tValue:  \"zap.internal\",\n\t\t\t\tStatus: StatusValid,\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tch: ch,\n\t\t\t\tdb: &MockDB{\n\t\t\t\t\tMockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {\n\t\t\t\t\t\tassert.Equal(t, \"chID\", updch.ID)\n\t\t\t\t\t\tassert.Equal(t, \"token\", updch.Token)\n\t\t\t\t\t\tassert.Equal(t, \"zap.internal\", updch.Value)\n\t\t\t\t\t\tassert.Equal(t, StatusValid, updch.Status)\n\n\t\t\t\t\t\tassert.EqualError(t, updch.Error.Err, err.Err.Error())\n\t\t\t\t\t\tassert.Equal(t, err.Type, updch.Error.Type)\n\t\t\t\t\t\tassert.Equal(t, err.Detail, updch.Error.Detail)\n\t\t\t\t\t\tassert.Equal(t, err.Status, updch.Error.Status)\n\t\t\t\t\t\tassert.Equal(t, err.Subproblems, updch.Error.Subproblems)\n\n\t\t\t\t\t\treturn NewError(ErrorMalformedType, \"bar\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: NewError(ErrorMalformedType, \"failure saving error to acme challenge: bar\"),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tch := &Challenge{\n\t\t\t\tID:     \"chID\",\n\t\t\t\tToken:  \"token\",\n\t\t\t\tValue:  \"zap.internal\",\n\t\t\t\tStatus: StatusValid,\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tch: ch,\n\t\t\t\tdb: &MockDB{\n\t\t\t\t\tMockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {\n\t\t\t\t\t\tassert.Equal(t, \"chID\", updch.ID)\n\t\t\t\t\t\tassert.Equal(t, \"token\", updch.Token)\n\t\t\t\t\t\tassert.Equal(t, \"zap.internal\", updch.Value)\n\t\t\t\t\t\tassert.Equal(t, StatusValid, updch.Status)\n\n\t\t\t\t\t\tassert.EqualError(t, updch.Error.Err, err.Err.Error())\n\t\t\t\t\t\tassert.Equal(t, err.Type, updch.Error.Type)\n\t\t\t\t\t\tassert.Equal(t, err.Detail, updch.Error.Detail)\n\t\t\t\t\t\tassert.Equal(t, err.Status, updch.Error.Status)\n\t\t\t\t\t\tassert.Equal(t, err.Subproblems, updch.Error.Subproblems)\n\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"ok/mark-invalid\": func(t *testing.T) test {\n\t\t\tch := &Challenge{\n\t\t\t\tID:     \"chID\",\n\t\t\t\tToken:  \"token\",\n\t\t\t\tValue:  \"zap.internal\",\n\t\t\t\tStatus: StatusValid,\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tch: ch,\n\t\t\t\tdb: &MockDB{\n\t\t\t\t\tMockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {\n\t\t\t\t\t\tassert.Equal(t, \"chID\", updch.ID)\n\t\t\t\t\t\tassert.Equal(t, \"token\", updch.Token)\n\t\t\t\t\t\tassert.Equal(t, \"zap.internal\", updch.Value)\n\t\t\t\t\t\tassert.Equal(t, StatusInvalid, updch.Status)\n\n\t\t\t\t\t\tassert.EqualError(t, updch.Error.Err, err.Err.Error())\n\t\t\t\t\t\tassert.Equal(t, err.Type, updch.Error.Type)\n\t\t\t\t\t\tassert.Equal(t, err.Detail, updch.Error.Detail)\n\t\t\t\t\t\tassert.Equal(t, err.Status, updch.Error.Status)\n\t\t\t\t\t\tassert.Equal(t, err.Subproblems, updch.Error.Subproblems)\n\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tmarkInvalid: true,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ttc := run(t)\n\t\t\tif err := storeError(context.Background(), tc.db, tc.ch, tc.markInvalid, err); err != nil {\n\t\t\t\tif assert.Error(t, tc.err) {\n\t\t\t\t\tvar k *Error\n\t\t\t\t\tif errors.As(err, &k) {\n\t\t\t\t\t\tassert.Equal(t, tc.err.Type, k.Type)\n\t\t\t\t\t\tassert.Equal(t, tc.err.Detail, k.Detail)\n\t\t\t\t\t\tassert.Equal(t, tc.err.Status, k.Status)\n\t\t\t\t\t\tassert.Equal(t, tc.err.Err.Error(), k.Err.Error())\n\t\t\t\t\t} else {\n\t\t\t\t\t\tassert.Fail(t, \"unexpected error type\")\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tassert.Nil(t, tc.err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestKeyAuthorization(t *testing.T) {\n\ttype test struct {\n\t\ttoken string\n\t\tjwk   *jose.JSONWebKey\n\t\texp   string\n\t\terr   *Error\n\t}\n\ttests := map[string]func(t *testing.T) test{\n\t\t\"fail/jwk-thumbprint-error\": func(t *testing.T) test {\n\t\t\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\trequire.NoError(t, err)\n\t\t\tjwk.Key = \"foo\"\n\t\t\treturn test{\n\t\t\t\ttoken: \"1234\",\n\t\t\t\tjwk:   jwk,\n\t\t\t\terr:   NewErrorISE(\"error generating JWK thumbprint: go-jose/go-jose: unknown key type 'string'\"),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\ttoken := \"1234\"\n\t\t\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\trequire.NoError(t, err)\n\t\t\tthumbprint, err := jwk.Thumbprint(crypto.SHA256)\n\t\t\trequire.NoError(t, err)\n\t\t\tencPrint := base64.RawURLEncoding.EncodeToString(thumbprint)\n\t\t\treturn test{\n\t\t\t\ttoken: token,\n\t\t\t\tjwk:   jwk,\n\t\t\t\texp:   fmt.Sprintf(\"%s.%s\", token, encPrint),\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ttc := run(t)\n\t\t\tif ka, err := KeyAuthorization(tc.token, tc.jwk); err != nil {\n\t\t\t\tif assert.Error(t, tc.err) {\n\t\t\t\t\tvar k *Error\n\t\t\t\t\tif errors.As(err, &k) {\n\t\t\t\t\t\tassert.Equal(t, tc.err.Type, k.Type)\n\t\t\t\t\t\tassert.Equal(t, tc.err.Detail, k.Detail)\n\t\t\t\t\t\tassert.Equal(t, tc.err.Status, k.Status)\n\t\t\t\t\t\tassert.Equal(t, tc.err.Err.Error(), k.Err.Error())\n\t\t\t\t\t} else {\n\t\t\t\t\t\tassert.Fail(t, \"unexpected error type\")\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif assert.Nil(t, tc.err) {\n\t\t\t\t\tassert.Equal(t, tc.exp, ka)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestChallenge_Validate(t *testing.T) {\n\tfakeKey := `-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEA5c+4NKZSNQcR1T8qN6SjwgdPZQ0Ge12Ylx/YeGAJ35k=\n-----END PUBLIC KEY-----`\n\ttype test struct {\n\t\tch      *Challenge\n\t\tvc      Client\n\t\tjwk     *jose.JSONWebKey\n\t\tdb      DB\n\t\tsrv     *httptest.Server\n\t\tpayload []byte\n\t\tctx     context.Context\n\t\terr     *Error\n\t}\n\ttests := map[string]func(t *testing.T) test{\n\t\t\"ok/already-valid\": func(t *testing.T) test {\n\t\t\tch := &Challenge{\n\t\t\t\tStatus: StatusValid,\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tch: ch,\n\t\t\t}\n\t\t},\n\t\t\"fail/already-invalid\": func(t *testing.T) test {\n\t\t\tch := &Challenge{\n\t\t\t\tStatus: StatusInvalid,\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tch: ch,\n\t\t\t}\n\t\t},\n\t\t\"fail/unexpected-type\": func(t *testing.T) test {\n\t\t\tch := &Challenge{\n\t\t\t\tStatus: StatusPending,\n\t\t\t\tType:   \"foo\",\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tch:  ch,\n\t\t\t\terr: NewErrorISE(`unexpected challenge type \"foo\"`),\n\t\t\t}\n\t\t},\n\t\t\"fail/http-01\": func(t *testing.T) test {\n\t\t\tch := &Challenge{\n\t\t\t\tID:     \"chID\",\n\t\t\t\tStatus: StatusPending,\n\t\t\t\tType:   \"http-01\",\n\t\t\t\tToken:  \"token\",\n\t\t\t\tValue:  \"zap.internal\",\n\t\t\t}\n\n\t\t\treturn test{\n\t\t\t\tch: ch,\n\t\t\t\tvc: &mockClient{\n\t\t\t\t\tget: func(url string) (*http.Response, error) {\n\t\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tdb: &MockDB{\n\t\t\t\t\tMockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {\n\t\t\t\t\t\tassert.Equal(t, \"chID\", updch.ID)\n\t\t\t\t\t\tassert.Equal(t, \"token\", updch.Token)\n\t\t\t\t\t\tassert.Equal(t, ChallengeType(\"http-01\"), updch.Type)\n\t\t\t\t\t\tassert.Equal(t, \"zap.internal\", updch.Value)\n\t\t\t\t\t\tassert.Equal(t, StatusPending, updch.Status)\n\n\t\t\t\t\t\terr := NewError(ErrorConnectionType, \"error doing http GET for url http://zap.internal/.well-known/acme-challenge/%s: force\", ch.Token)\n\t\t\t\t\t\tassert.EqualError(t, updch.Error.Err, err.Err.Error())\n\t\t\t\t\t\tassert.Equal(t, err.Type, updch.Error.Type)\n\t\t\t\t\t\tassert.Equal(t, err.Detail, updch.Error.Detail)\n\t\t\t\t\t\tassert.Equal(t, err.Status, updch.Error.Status)\n\t\t\t\t\t\tassert.Equal(t, err.Subproblems, updch.Error.Subproblems)\n\n\t\t\t\t\t\treturn errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: NewErrorISE(\"failure saving error to acme challenge: force\"),\n\t\t\t}\n\t\t},\n\t\t\"ok/http-01\": func(t *testing.T) test {\n\t\t\tch := &Challenge{\n\t\t\t\tID:     \"chID\",\n\t\t\t\tStatus: StatusPending,\n\t\t\t\tType:   \"http-01\",\n\t\t\t\tToken:  \"token\",\n\t\t\t\tValue:  \"zap.internal\",\n\t\t\t}\n\n\t\t\treturn test{\n\t\t\t\tch: ch,\n\t\t\t\tvc: &mockClient{\n\t\t\t\t\tget: func(url string) (*http.Response, error) {\n\t\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tdb: &MockDB{\n\t\t\t\t\tMockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {\n\t\t\t\t\t\tassert.Equal(t, \"chID\", updch.ID)\n\t\t\t\t\t\tassert.Equal(t, \"token\", updch.Token)\n\t\t\t\t\t\tassert.Equal(t, ChallengeType(\"http-01\"), updch.Type)\n\t\t\t\t\t\tassert.Equal(t, \"zap.internal\", updch.Value)\n\t\t\t\t\t\tassert.Equal(t, StatusPending, updch.Status)\n\n\t\t\t\t\t\terr := NewError(ErrorConnectionType, \"error doing http GET for url http://zap.internal/.well-known/acme-challenge/%s: force\", ch.Token)\n\t\t\t\t\t\tassert.EqualError(t, updch.Error.Err, err.Err.Error())\n\t\t\t\t\t\tassert.Equal(t, err.Type, updch.Error.Type)\n\t\t\t\t\t\tassert.Equal(t, err.Detail, updch.Error.Detail)\n\t\t\t\t\t\tassert.Equal(t, err.Status, updch.Error.Status)\n\t\t\t\t\t\tassert.Equal(t, err.Subproblems, updch.Error.Subproblems)\n\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"ok/http-01-insecure\": func(t *testing.T) test {\n\t\t\tt.Cleanup(func() {\n\t\t\t\tInsecurePortHTTP01 = 0\n\t\t\t})\n\n\t\t\tch := &Challenge{\n\t\t\t\tID:     \"chID\",\n\t\t\t\tStatus: StatusPending,\n\t\t\t\tType:   \"http-01\",\n\t\t\t\tToken:  \"token\",\n\t\t\t\tValue:  \"zap.internal\",\n\t\t\t}\n\n\t\t\tInsecurePortHTTP01 = 8080\n\n\t\t\treturn test{\n\t\t\t\tch: ch,\n\t\t\t\tvc: &mockClient{\n\t\t\t\t\tget: func(url string) (*http.Response, error) {\n\t\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tdb: &MockDB{\n\t\t\t\t\tMockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {\n\t\t\t\t\t\tassert.Equal(t, \"chID\", updch.ID)\n\t\t\t\t\t\tassert.Equal(t, \"token\", updch.Token)\n\t\t\t\t\t\tassert.Equal(t, ChallengeType(\"http-01\"), updch.Type)\n\t\t\t\t\t\tassert.Equal(t, \"zap.internal\", updch.Value)\n\t\t\t\t\t\tassert.Equal(t, StatusPending, updch.Status)\n\n\t\t\t\t\t\terr := NewError(ErrorConnectionType, \"error doing http GET for url http://zap.internal:8080/.well-known/acme-challenge/%s: force\", ch.Token)\n\t\t\t\t\t\tassert.EqualError(t, updch.Error.Err, err.Err.Error())\n\t\t\t\t\t\tassert.Equal(t, err.Type, updch.Error.Type)\n\t\t\t\t\t\tassert.Equal(t, err.Detail, updch.Error.Detail)\n\t\t\t\t\t\tassert.Equal(t, err.Status, updch.Error.Status)\n\t\t\t\t\t\tassert.Equal(t, err.Subproblems, updch.Error.Subproblems)\n\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"fail/dns-01\": func(t *testing.T) test {\n\t\t\tch := &Challenge{\n\t\t\t\tID:     \"chID\",\n\t\t\t\tType:   \"dns-01\",\n\t\t\t\tStatus: StatusPending,\n\t\t\t\tToken:  \"token\",\n\t\t\t\tValue:  \"zap.internal\",\n\t\t\t}\n\n\t\t\treturn test{\n\t\t\t\tch: ch,\n\t\t\t\tvc: &mockClient{\n\t\t\t\t\tlookupTxt: func(url string) ([]string, error) {\n\t\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tdb: &MockDB{\n\t\t\t\t\tMockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {\n\t\t\t\t\t\tassert.Equal(t, \"chID\", updch.ID)\n\t\t\t\t\t\tassert.Equal(t, \"token\", updch.Token)\n\t\t\t\t\t\tassert.Equal(t, ChallengeType(\"dns-01\"), updch.Type)\n\t\t\t\t\t\tassert.Equal(t, \"zap.internal\", updch.Value)\n\t\t\t\t\t\tassert.Equal(t, StatusPending, updch.Status)\n\n\t\t\t\t\t\terr := NewError(ErrorDNSType, \"error looking up TXT records for domain %s: force\", ch.Value)\n\n\t\t\t\t\t\tassert.EqualError(t, updch.Error.Err, err.Err.Error())\n\t\t\t\t\t\tassert.Equal(t, err.Type, updch.Error.Type)\n\t\t\t\t\t\tassert.Equal(t, err.Detail, updch.Error.Detail)\n\t\t\t\t\t\tassert.Equal(t, err.Status, updch.Error.Status)\n\t\t\t\t\t\tassert.Equal(t, err.Subproblems, updch.Error.Subproblems)\n\n\t\t\t\t\t\treturn errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: NewErrorISE(\"failure saving error to acme challenge: force\"),\n\t\t\t}\n\t\t},\n\t\t\"ok/dns-01\": func(t *testing.T) test {\n\t\t\tch := &Challenge{\n\t\t\t\tID:     \"chID\",\n\t\t\t\tType:   \"dns-01\",\n\t\t\t\tStatus: StatusPending,\n\t\t\t\tToken:  \"token\",\n\t\t\t\tValue:  \"zap.internal\",\n\t\t\t}\n\n\t\t\treturn test{\n\t\t\t\tch: ch,\n\t\t\t\tvc: &mockClient{\n\t\t\t\t\tlookupTxt: func(url string) ([]string, error) {\n\t\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tdb: &MockDB{\n\t\t\t\t\tMockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {\n\t\t\t\t\t\tassert.Equal(t, \"chID\", updch.ID)\n\t\t\t\t\t\tassert.Equal(t, \"token\", updch.Token)\n\t\t\t\t\t\tassert.Equal(t, ChallengeType(\"dns-01\"), updch.Type)\n\t\t\t\t\t\tassert.Equal(t, \"zap.internal\", updch.Value)\n\t\t\t\t\t\tassert.Equal(t, StatusPending, updch.Status)\n\n\t\t\t\t\t\terr := NewError(ErrorDNSType, \"error looking up TXT records for domain %s: force\", ch.Value)\n\n\t\t\t\t\t\tassert.EqualError(t, updch.Error.Err, err.Err.Error())\n\t\t\t\t\t\tassert.Equal(t, err.Type, updch.Error.Type)\n\t\t\t\t\t\tassert.Equal(t, err.Detail, updch.Error.Detail)\n\t\t\t\t\t\tassert.Equal(t, err.Status, updch.Error.Status)\n\t\t\t\t\t\tassert.Equal(t, err.Subproblems, updch.Error.Subproblems)\n\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"fail/tls-alpn-01\": func(t *testing.T) test {\n\t\t\tch := &Challenge{\n\t\t\t\tID:     \"chID\",\n\t\t\t\tToken:  \"token\",\n\t\t\t\tType:   \"tls-alpn-01\",\n\t\t\t\tStatus: StatusPending,\n\t\t\t\tValue:  \"zap.internal\",\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tch: ch,\n\t\t\t\tvc: &mockClient{\n\t\t\t\t\ttlsDial: func(network, addr string, config *tls.Config) (*tls.Conn, error) {\n\t\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tdb: &MockDB{\n\t\t\t\t\tMockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {\n\t\t\t\t\t\tassert.Equal(t, \"chID\", updch.ID)\n\t\t\t\t\t\tassert.Equal(t, \"token\", updch.Token)\n\t\t\t\t\t\tassert.Equal(t, ChallengeType(\"tls-alpn-01\"), updch.Type)\n\t\t\t\t\t\tassert.Equal(t, \"zap.internal\", updch.Value)\n\t\t\t\t\t\tassert.Equal(t, StatusPending, updch.Status)\n\n\t\t\t\t\t\terr := NewError(ErrorConnectionType, \"error doing TLS dial for %v: force\", ch.Value)\n\n\t\t\t\t\t\tassert.EqualError(t, updch.Error.Err, err.Err.Error())\n\t\t\t\t\t\tassert.Equal(t, err.Type, updch.Error.Type)\n\t\t\t\t\t\tassert.Equal(t, err.Detail, updch.Error.Detail)\n\t\t\t\t\t\tassert.Equal(t, err.Status, updch.Error.Status)\n\t\t\t\t\t\tassert.Equal(t, err.Subproblems, updch.Error.Subproblems)\n\n\t\t\t\t\t\treturn errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: NewErrorISE(\"failure saving error to acme challenge: force\"),\n\t\t\t}\n\t\t},\n\t\t\"ok/tls-alpn-01\": func(t *testing.T) test {\n\t\t\tch := &Challenge{\n\t\t\t\tID:     \"chID\",\n\t\t\t\tToken:  \"token\",\n\t\t\t\tType:   \"tls-alpn-01\",\n\t\t\t\tStatus: StatusPending,\n\t\t\t\tValue:  \"zap.internal\",\n\t\t\t}\n\n\t\t\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\trequire.NoError(t, err)\n\n\t\t\texpKeyAuth, err := KeyAuthorization(ch.Token, jwk)\n\t\t\trequire.NoError(t, err)\n\t\t\texpKeyAuthHash := sha256.Sum256([]byte(expKeyAuth))\n\n\t\t\tcert, err := newTLSALPNValidationCert(expKeyAuthHash[:], false, true, ch.Value)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tsrv, tlsDial := newTestTLSALPNServer(cert)\n\t\t\tsrv.Start()\n\n\t\t\treturn test{\n\t\t\t\tch: ch,\n\t\t\t\tvc: &mockClient{\n\t\t\t\t\ttlsDial: tlsDial,\n\t\t\t\t},\n\t\t\t\tdb: &MockDB{\n\t\t\t\t\tMockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {\n\t\t\t\t\t\tassert.Equal(t, \"chID\", updch.ID)\n\t\t\t\t\t\tassert.Equal(t, \"token\", updch.Token)\n\t\t\t\t\t\tassert.Equal(t, ChallengeType(\"tls-alpn-01\"), updch.Type)\n\t\t\t\t\t\tassert.Equal(t, \"zap.internal\", updch.Value)\n\t\t\t\t\t\tassert.Equal(t, StatusValid, updch.Status)\n\t\t\t\t\t\tassert.Nil(t, updch.Error)\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tsrv: srv,\n\t\t\t\tjwk: jwk,\n\t\t\t}\n\t\t},\n\t\t\"ok/tls-alpn-01-insecure\": func(t *testing.T) test {\n\t\t\tt.Cleanup(func() {\n\t\t\t\tInsecurePortTLSALPN01 = 0\n\t\t\t})\n\n\t\t\tch := &Challenge{\n\t\t\t\tID:     \"chID\",\n\t\t\t\tToken:  \"token\",\n\t\t\t\tType:   \"tls-alpn-01\",\n\t\t\t\tStatus: StatusPending,\n\t\t\t\tValue:  \"zap.internal\",\n\t\t\t}\n\n\t\t\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\trequire.NoError(t, err)\n\n\t\t\texpKeyAuth, err := KeyAuthorization(ch.Token, jwk)\n\t\t\trequire.NoError(t, err)\n\t\t\texpKeyAuthHash := sha256.Sum256([]byte(expKeyAuth))\n\n\t\t\tcert, err := newTLSALPNValidationCert(expKeyAuthHash[:], false, true, ch.Value)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tl, err := net.Listen(\"tcp\", \"127.0.0.1:0\")\n\t\t\tif err != nil {\n\t\t\t\tif l, err = net.Listen(\"tcp6\", \"[::1]:0\"); err != nil {\n\t\t\t\t\tt.Fatalf(\"failed to listen on a port: %v\", err)\n\t\t\t\t}\n\t\t\t}\n\t\t\t_, port, err := net.SplitHostPort(l.Addr().String())\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"failed to split host port: %v\", err)\n\t\t\t}\n\n\t\t\t// Use an insecure port\n\t\t\tInsecurePortTLSALPN01, err = strconv.Atoi(port)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"failed to convert port to int: %v\", err)\n\t\t\t}\n\n\t\t\tsrv, tlsDial := newTestTLSALPNServer(cert, func(srv *httptest.Server) {\n\t\t\t\tsrv.Listener.Close()\n\t\t\t\tsrv.Listener = l\n\t\t\t})\n\t\t\tsrv.Start()\n\n\t\t\treturn test{\n\t\t\t\tch: ch,\n\t\t\t\tvc: &mockClient{\n\t\t\t\t\ttlsDial: tlsDial,\n\t\t\t\t},\n\t\t\t\tdb: &MockDB{\n\t\t\t\t\tMockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {\n\t\t\t\t\t\tassert.Equal(t, \"chID\", updch.ID)\n\t\t\t\t\t\tassert.Equal(t, \"token\", updch.Token)\n\t\t\t\t\t\tassert.Equal(t, ChallengeType(\"tls-alpn-01\"), updch.Type)\n\t\t\t\t\t\tassert.Equal(t, \"zap.internal\", updch.Value)\n\t\t\t\t\t\tassert.Equal(t, StatusValid, updch.Status)\n\t\t\t\t\t\tassert.Nil(t, updch.Error)\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tsrv: srv,\n\t\t\t\tjwk: jwk,\n\t\t\t}\n\t\t},\n\t\t\"fail/device-attest-01\": func(t *testing.T) test {\n\t\t\tpayload, err := json.Marshal(struct {\n\t\t\t\tError string `json:\"error\"`\n\t\t\t}{\n\t\t\t\tError: \"an error\",\n\t\t\t})\n\t\t\tassert.NoError(t, err)\n\t\t\treturn test{\n\t\t\t\tch: &Challenge{\n\t\t\t\t\tID:              \"chID\",\n\t\t\t\t\tAuthorizationID: \"azID\",\n\t\t\t\t\tToken:           \"token\",\n\t\t\t\t\tType:            \"device-attest-01\",\n\t\t\t\t\tStatus:          StatusPending,\n\t\t\t\t\tValue:           \"12345678\",\n\t\t\t\t},\n\t\t\t\tpayload: payload,\n\t\t\t\tdb: &MockDB{\n\t\t\t\t\tMockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {\n\t\t\t\t\t\tassert.Equal(t, \"azID\", id)\n\t\t\t\t\t\treturn &Authorization{ID: \"azID\"}, nil\n\t\t\t\t\t},\n\t\t\t\t\tMockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {\n\t\t\t\t\t\tassert.Equal(t, \"chID\", updch.ID)\n\t\t\t\t\t\tassert.Equal(t, \"token\", updch.Token)\n\t\t\t\t\t\tassert.Equal(t, StatusInvalid, updch.Status)\n\t\t\t\t\t\tassert.Equal(t, ChallengeType(\"device-attest-01\"), updch.Type)\n\t\t\t\t\t\tassert.Equal(t, \"12345678\", updch.Value)\n\t\t\t\t\t\tassert.Equal(t, payload, updch.Payload)\n\t\t\t\t\t\tassert.Empty(t, updch.PayloadFormat)\n\n\t\t\t\t\t\terr := NewError(ErrorRejectedIdentifierType, \"payload contained error: an error\")\n\n\t\t\t\t\t\tassert.EqualError(t, updch.Error.Err, err.Err.Error())\n\t\t\t\t\t\tassert.Equal(t, err.Type, updch.Error.Type)\n\t\t\t\t\t\tassert.Equal(t, err.Detail, updch.Error.Detail)\n\t\t\t\t\t\tassert.Equal(t, err.Status, updch.Error.Status)\n\n\t\t\t\t\t\treturn errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: NewError(ErrorServerInternalType, \"failure saving error to acme challenge: force\"),\n\t\t\t}\n\t\t},\n\t\t\"ok/device-attest-01\": func(t *testing.T) test {\n\t\t\tjwk, keyAuth := mustAccountAndKeyAuthorization(t, \"token\")\n\t\t\tpayload, leaf, root := mustAttestYubikey(t, \"nonce\", keyAuth, 1234)\n\n\t\t\tcaRoot := pem.EncodeToMemory(&pem.Block{Type: \"CERTIFICATE\", Bytes: root.Raw})\n\t\t\tctx := NewProvisionerContext(context.Background(), mustAttestationProvisioner(t, caRoot))\n\n\t\t\treturn test{\n\t\t\t\tch: &Challenge{\n\t\t\t\t\tID:              \"chID\",\n\t\t\t\t\tAuthorizationID: \"azID\",\n\t\t\t\t\tToken:           \"token\",\n\t\t\t\t\tType:            \"device-attest-01\",\n\t\t\t\t\tStatus:          StatusPending,\n\t\t\t\t\tValue:           \"1234\",\n\t\t\t\t},\n\t\t\t\tpayload: payload,\n\t\t\t\tctx:     ctx,\n\t\t\t\tjwk:     jwk,\n\t\t\t\tdb: &MockDB{\n\t\t\t\t\tMockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {\n\t\t\t\t\t\tassert.Equal(t, \"azID\", id)\n\t\t\t\t\t\treturn &Authorization{ID: \"azID\"}, nil\n\t\t\t\t\t},\n\t\t\t\t\tMockUpdateAuthorization: func(ctx context.Context, az *Authorization) error {\n\t\t\t\t\t\tfingerprint, err := keyutil.Fingerprint(leaf.PublicKey)\n\t\t\t\t\t\tassert.NoError(t, err)\n\t\t\t\t\t\tassert.Equal(t, \"azID\", az.ID)\n\t\t\t\t\t\tassert.Equal(t, fingerprint, az.Fingerprint)\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t\tMockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {\n\t\t\t\t\t\tassert.Equal(t, \"chID\", updch.ID)\n\t\t\t\t\t\tassert.Equal(t, \"token\", updch.Token)\n\t\t\t\t\t\tassert.Equal(t, StatusValid, updch.Status)\n\t\t\t\t\t\tassert.Equal(t, ChallengeType(\"device-attest-01\"), updch.Type)\n\t\t\t\t\t\tassert.Equal(t, \"1234\", updch.Value)\n\t\t\t\t\t\tassert.Equal(t, payload, updch.Payload)\n\t\t\t\t\t\tassert.Equal(t, \"step\", updch.PayloadFormat)\n\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"ok/wire-oidc-01\": func(t *testing.T) test {\n\t\t\tjwk, keyAuth := mustAccountAndKeyAuthorization(t, \"token\")\n\t\t\tsignerJWK, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\trequire.NoError(t, err)\n\t\t\tsigner, err := jose.NewSigner(jose.SigningKey{\n\t\t\t\tAlgorithm: jose.SignatureAlgorithm(signerJWK.Algorithm),\n\t\t\t\tKey:       signerJWK,\n\t\t\t}, new(jose.SignerOptions))\n\t\t\trequire.NoError(t, err)\n\t\t\tsrv := mustJWKServer(t, signerJWK.Public())\n\t\t\ttokenBytes, err := json.Marshal(struct {\n\t\t\t\tjose.Claims\n\t\t\t\tName              string `json:\"name,omitempty\"`\n\t\t\t\tPreferredUsername string `json:\"preferred_username,omitempty\"`\n\t\t\t\tKeyAuth           string `json:\"keyauth\"`\n\t\t\t\tACMEAudience      string `json:\"acme_aud\"`\n\t\t\t}{\n\t\t\t\tClaims: jose.Claims{\n\t\t\t\t\tIssuer:   srv.URL,\n\t\t\t\t\tAudience: []string{\"test\"},\n\t\t\t\t\tExpiry:   jose.NewNumericDate(time.Now().Add(1 * time.Minute)),\n\t\t\t\t},\n\t\t\t\tName:              \"Alice Smith\",\n\t\t\t\tPreferredUsername: \"wireapp://%40alice_wire@wire.com\",\n\t\t\t\tKeyAuth:           keyAuth,\n\t\t\t\tACMEAudience:      \"https://ca.example.com/acme/wire/challenge/azID/chID\",\n\t\t\t})\n\t\t\trequire.NoError(t, err)\n\t\t\tsigned, err := signer.Sign(tokenBytes)\n\t\t\trequire.NoError(t, err)\n\t\t\tidToken, err := signed.CompactSerialize()\n\t\t\trequire.NoError(t, err)\n\t\t\tpayload, err := json.Marshal(struct {\n\t\t\t\tIDToken string `json:\"id_token\"`\n\t\t\t}{\n\t\t\t\tIDToken: idToken,\n\t\t\t})\n\t\t\trequire.NoError(t, err)\n\t\t\tvalueBytes, err := json.Marshal(struct {\n\t\t\t\tName     string `json:\"name,omitempty\"`\n\t\t\t\tDomain   string `json:\"domain,omitempty\"`\n\t\t\t\tClientID string `json:\"client-id,omitempty\"`\n\t\t\t\tHandle   string `json:\"handle,omitempty\"`\n\t\t\t}{\n\t\t\t\tName:     \"Alice Smith\",\n\t\t\t\tDomain:   \"wire.com\",\n\t\t\t\tClientID: \"wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com\",\n\t\t\t\tHandle:   \"wireapp://%40alice_wire@wire.com\",\n\t\t\t})\n\t\t\trequire.NoError(t, err)\n\t\t\tctx := NewProvisionerContext(context.Background(), newWireProvisionerWithOptions(t, &provisioner.Options{\n\t\t\t\tWire: &wireprovisioner.Options{\n\t\t\t\t\tOIDC: &wireprovisioner.OIDCOptions{\n\t\t\t\t\t\tProvider: &wireprovisioner.Provider{\n\t\t\t\t\t\t\tIssuerURL:  srv.URL,\n\t\t\t\t\t\t\tJWKSURL:    srv.URL + \"/keys\",\n\t\t\t\t\t\t\tAlgorithms: []string{\"ES256\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tConfig: &wireprovisioner.Config{\n\t\t\t\t\t\t\tClientID:            \"test\",\n\t\t\t\t\t\t\tSignatureAlgorithms: []string{\"ES256\"},\n\t\t\t\t\t\t\tNow:                 time.Now,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tTransformTemplate: \"\",\n\t\t\t\t\t},\n\t\t\t\t\tDPOP: &wireprovisioner.DPOPOptions{\n\t\t\t\t\t\tSigningKey: []byte(fakeKey),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}))\n\t\t\tctx = NewLinkerContext(ctx, NewLinker(\"ca.example.com\", \"acme\"))\n\t\t\treturn test{\n\t\t\t\tch: &Challenge{\n\t\t\t\t\tID:              \"chID\",\n\t\t\t\t\tAuthorizationID: \"azID\",\n\t\t\t\t\tAccountID:       \"accID\",\n\t\t\t\t\tToken:           \"token\",\n\t\t\t\t\tType:            \"wire-oidc-01\",\n\t\t\t\t\tStatus:          StatusPending,\n\t\t\t\t\tValue:           string(valueBytes),\n\t\t\t\t},\n\t\t\t\tsrv:     srv,\n\t\t\t\tpayload: payload,\n\t\t\t\tctx:     ctx,\n\t\t\t\tjwk:     jwk,\n\t\t\t\tdb: &MockWireDB{\n\t\t\t\t\tMockDB: MockDB{\n\t\t\t\t\t\tMockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {\n\t\t\t\t\t\t\tassert.Equal(t, \"chID\", updch.ID)\n\t\t\t\t\t\t\tassert.Equal(t, \"token\", updch.Token)\n\t\t\t\t\t\t\tassert.Equal(t, StatusValid, updch.Status)\n\t\t\t\t\t\t\tassert.Equal(t, ChallengeType(\"wire-oidc-01\"), updch.Type)\n\t\t\t\t\t\t\tassert.Equal(t, string(valueBytes), updch.Value)\n\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tMockGetAllOrdersByAccountID: func(ctx context.Context, accountID string) ([]string, error) {\n\t\t\t\t\t\tassert.Equal(t, \"accID\", accountID)\n\t\t\t\t\t\treturn []string{\"orderID\"}, nil\n\t\t\t\t\t},\n\t\t\t\t\tMockCreateOidcToken: func(ctx context.Context, orderID string, idToken map[string]interface{}) error {\n\t\t\t\t\t\tassert.Equal(t, \"orderID\", orderID)\n\t\t\t\t\t\tassert.Equal(t, \"Alice Smith\", idToken[\"name\"].(string))\n\t\t\t\t\t\tassert.Equal(t, \"wireapp://%40alice_wire@wire.com\", idToken[\"preferred_username\"].(string))\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"fail/wire-oidc-01-no-wire-db\": func(t *testing.T) test {\n\t\t\tjwk, keyAuth := mustAccountAndKeyAuthorization(t, \"token\")\n\t\t\tsignerJWK, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\trequire.NoError(t, err)\n\t\t\tsigner, err := jose.NewSigner(jose.SigningKey{\n\t\t\t\tAlgorithm: jose.SignatureAlgorithm(signerJWK.Algorithm),\n\t\t\t\tKey:       signerJWK,\n\t\t\t}, new(jose.SignerOptions))\n\t\t\trequire.NoError(t, err)\n\t\t\tsrv := mustJWKServer(t, signerJWK.Public())\n\t\t\ttokenBytes, err := json.Marshal(struct {\n\t\t\t\tjose.Claims\n\t\t\t\tName              string `json:\"name,omitempty\"`\n\t\t\t\tPreferredUsername string `json:\"preferred_username,omitempty\"`\n\t\t\t\tKeyAuth           string `json:\"keyauth\"`\n\t\t\t\tACMEAudience      string `json:\"acme_aud\"`\n\t\t\t}{\n\t\t\t\tClaims: jose.Claims{\n\t\t\t\t\tIssuer:   srv.URL,\n\t\t\t\t\tAudience: []string{\"test\"},\n\t\t\t\t\tExpiry:   jose.NewNumericDate(time.Now().Add(1 * time.Minute)),\n\t\t\t\t},\n\t\t\t\tName:              \"Alice Smith\",\n\t\t\t\tPreferredUsername: \"wireapp://%40alice_wire@wire.com\",\n\t\t\t\tKeyAuth:           keyAuth,\n\t\t\t\tACMEAudience:      \"https://ca.example.com/acme/wire/challenge/azID/chID\",\n\t\t\t})\n\t\t\trequire.NoError(t, err)\n\t\t\tsigned, err := signer.Sign(tokenBytes)\n\t\t\trequire.NoError(t, err)\n\t\t\tidToken, err := signed.CompactSerialize()\n\t\t\trequire.NoError(t, err)\n\t\t\tpayload, err := json.Marshal(struct {\n\t\t\t\tIDToken string `json:\"id_token\"`\n\t\t\t}{\n\t\t\t\tIDToken: idToken,\n\t\t\t})\n\t\t\trequire.NoError(t, err)\n\t\t\tvalueBytes, err := json.Marshal(struct {\n\t\t\t\tName     string `json:\"name,omitempty\"`\n\t\t\t\tDomain   string `json:\"domain,omitempty\"`\n\t\t\t\tClientID string `json:\"client-id,omitempty\"`\n\t\t\t\tHandle   string `json:\"handle,omitempty\"`\n\t\t\t}{\n\t\t\t\tName:     \"Alice Smith\",\n\t\t\t\tDomain:   \"wire.com\",\n\t\t\t\tClientID: \"wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com\",\n\t\t\t\tHandle:   \"wireapp://%40alice_wire@wire.com\",\n\t\t\t})\n\t\t\trequire.NoError(t, err)\n\t\t\tctx := NewProvisionerContext(context.Background(), newWireProvisionerWithOptions(t, &provisioner.Options{\n\t\t\t\tWire: &wireprovisioner.Options{\n\t\t\t\t\tOIDC: &wireprovisioner.OIDCOptions{\n\t\t\t\t\t\tProvider: &wireprovisioner.Provider{\n\t\t\t\t\t\t\tIssuerURL:  srv.URL,\n\t\t\t\t\t\t\tJWKSURL:    srv.URL + \"/keys\",\n\t\t\t\t\t\t\tAlgorithms: []string{\"ES256\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tConfig: &wireprovisioner.Config{\n\t\t\t\t\t\t\tClientID:            \"test\",\n\t\t\t\t\t\t\tSignatureAlgorithms: []string{\"ES256\"},\n\t\t\t\t\t\t\tNow:                 time.Now,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tTransformTemplate: \"\",\n\t\t\t\t\t},\n\t\t\t\t\tDPOP: &wireprovisioner.DPOPOptions{\n\t\t\t\t\t\tSigningKey: []byte(fakeKey),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}))\n\t\t\tctx = NewLinkerContext(ctx, NewLinker(\"ca.example.com\", \"acme\"))\n\t\t\treturn test{\n\t\t\t\tch: &Challenge{\n\t\t\t\t\tID:              \"chID\",\n\t\t\t\t\tAuthorizationID: \"azID\",\n\t\t\t\t\tAccountID:       \"accID\",\n\t\t\t\t\tToken:           \"token\",\n\t\t\t\t\tType:            \"wire-oidc-01\",\n\t\t\t\t\tStatus:          StatusPending,\n\t\t\t\t\tValue:           string(valueBytes),\n\t\t\t\t},\n\t\t\t\tsrv:     srv,\n\t\t\t\tpayload: payload,\n\t\t\t\tctx:     ctx,\n\t\t\t\tjwk:     jwk,\n\t\t\t\tdb:      &MockDB{},\n\t\t\t\terr: &Error{\n\t\t\t\t\tType:   \"urn:ietf:params:acme:error:serverInternal\",\n\t\t\t\t\tDetail: \"The server experienced an internal error\",\n\t\t\t\t\tStatus: 500,\n\t\t\t\t\tErr:    errors.New(\"db *acme.MockDB is not a WireDB\"),\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"ok/wire-dpop-01\": func(t *testing.T) test {\n\t\t\tjwk, keyAuth := mustAccountAndKeyAuthorization(t, \"token\")\n\t\t\t_ = keyAuth // TODO(hs): keyAuth (not) required for DPoP? Or needs to be added to validation?\n\t\t\tdpopSigner, err := jose.NewSigner(jose.SigningKey{\n\t\t\t\tAlgorithm: jose.SignatureAlgorithm(jwk.Algorithm),\n\t\t\t\tKey:       jwk,\n\t\t\t}, new(jose.SignerOptions))\n\t\t\trequire.NoError(t, err)\n\t\t\tsignerJWK, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\trequire.NoError(t, err)\n\t\t\tsigner, err := jose.NewSigner(jose.SigningKey{\n\t\t\t\tAlgorithm: jose.SignatureAlgorithm(signerJWK.Algorithm),\n\t\t\t\tKey:       signerJWK,\n\t\t\t}, new(jose.SignerOptions))\n\t\t\trequire.NoError(t, err)\n\t\t\tsignerPEMBlock, err := pemutil.Serialize(signerJWK.Public().Key)\n\t\t\trequire.NoError(t, err)\n\t\t\tsignerPEMBytes := pem.EncodeToMemory(signerPEMBlock)\n\t\t\tdpopBytes, err := json.Marshal(struct {\n\t\t\t\tjose.Claims\n\t\t\t\tChallenge string `json:\"chal,omitempty\"`\n\t\t\t\tHandle    string `json:\"handle,omitempty\"`\n\t\t\t\tNonce     string `json:\"nonce,omitempty\"`\n\t\t\t\tHTU       string `json:\"htu,omitempty\"`\n\t\t\t\tName      string `json:\"name,omitempty\"`\n\t\t\t}{\n\t\t\t\tClaims: jose.Claims{\n\t\t\t\t\tSubject:  \"wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com\",\n\t\t\t\t\tAudience: jose.Audience{\"https://ca.example.com/acme/wire/challenge/azID/chID\"},\n\t\t\t\t},\n\t\t\t\tChallenge: \"token\",\n\t\t\t\tHandle:    \"wireapp://%40alice_wire@wire.com\",\n\t\t\t\tNonce:     \"nonce\",\n\t\t\t\tHTU:       \"http://issuer.example.com\",\n\t\t\t\tName:      \"Alice Smith\",\n\t\t\t})\n\t\t\trequire.NoError(t, err)\n\t\t\tdpop, err := dpopSigner.Sign(dpopBytes)\n\t\t\trequire.NoError(t, err)\n\t\t\tproof, err := dpop.CompactSerialize()\n\t\t\trequire.NoError(t, err)\n\t\t\ttokenBytes, err := json.Marshal(struct {\n\t\t\t\tjose.Claims\n\t\t\t\tChallenge string `json:\"chal,omitempty\"`\n\t\t\t\tNonce     string `json:\"nonce,omitempty\"`\n\t\t\t\tCnf       struct {\n\t\t\t\t\tKid string `json:\"kid,omitempty\"`\n\t\t\t\t} `json:\"cnf\"`\n\t\t\t\tProof      string `json:\"proof,omitempty\"`\n\t\t\t\tClientID   string `json:\"client_id\"`\n\t\t\t\tAPIVersion int    `json:\"api_version\"`\n\t\t\t\tScope      string `json:\"scope\"`\n\t\t\t}{\n\t\t\t\tClaims: jose.Claims{\n\t\t\t\t\tIssuer:   \"http://issuer.example.com\",\n\t\t\t\t\tAudience: jose.Audience{\"https://ca.example.com/acme/wire/challenge/azID/chID\"},\n\t\t\t\t\tExpiry:   jose.NewNumericDate(time.Now().Add(1 * time.Minute)),\n\t\t\t\t},\n\t\t\t\tChallenge: \"token\",\n\t\t\t\tNonce:     \"nonce\",\n\t\t\t\tCnf: struct {\n\t\t\t\t\tKid string `json:\"kid,omitempty\"`\n\t\t\t\t}{\n\t\t\t\t\tKid: jwk.KeyID,\n\t\t\t\t},\n\t\t\t\tProof:      proof,\n\t\t\t\tClientID:   \"wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com\",\n\t\t\t\tAPIVersion: 5,\n\t\t\t\tScope:      \"wire_client_id\",\n\t\t\t})\n\t\t\trequire.NoError(t, err)\n\t\t\tsigned, err := signer.Sign(tokenBytes)\n\t\t\trequire.NoError(t, err)\n\t\t\taccessToken, err := signed.CompactSerialize()\n\t\t\trequire.NoError(t, err)\n\t\t\tpayload, err := json.Marshal(struct {\n\t\t\t\tAccessToken string `json:\"access_token\"`\n\t\t\t}{\n\t\t\t\tAccessToken: accessToken,\n\t\t\t})\n\t\t\trequire.NoError(t, err)\n\t\t\tvalueBytes, err := json.Marshal(struct {\n\t\t\t\tName     string `json:\"name,omitempty\"`\n\t\t\t\tDomain   string `json:\"domain,omitempty\"`\n\t\t\t\tClientID string `json:\"client-id,omitempty\"`\n\t\t\t\tHandle   string `json:\"handle,omitempty\"`\n\t\t\t}{\n\t\t\t\tName:     \"Alice Smith\",\n\t\t\t\tDomain:   \"wire.com\",\n\t\t\t\tClientID: \"wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com\",\n\t\t\t\tHandle:   \"wireapp://%40alice_wire@wire.com\",\n\t\t\t})\n\t\t\trequire.NoError(t, err)\n\t\t\tctx := NewProvisionerContext(context.Background(), newWireProvisionerWithOptions(t, &provisioner.Options{\n\t\t\t\tWire: &wireprovisioner.Options{\n\t\t\t\t\tOIDC: &wireprovisioner.OIDCOptions{\n\t\t\t\t\t\tProvider: &wireprovisioner.Provider{\n\t\t\t\t\t\t\tIssuerURL:  \"http://issuerexample.com\",\n\t\t\t\t\t\t\tAlgorithms: []string{\"ES256\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tConfig: &wireprovisioner.Config{\n\t\t\t\t\t\t\tClientID:            \"test\",\n\t\t\t\t\t\t\tSignatureAlgorithms: []string{\"ES256\"},\n\t\t\t\t\t\t\tNow:                 time.Now,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tTransformTemplate: \"\",\n\t\t\t\t\t},\n\t\t\t\t\tDPOP: &wireprovisioner.DPOPOptions{\n\t\t\t\t\t\tTarget:     \"http://issuer.example.com\",\n\t\t\t\t\t\tSigningKey: signerPEMBytes,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}))\n\t\t\tctx = NewLinkerContext(ctx, NewLinker(\"ca.example.com\", \"acme\"))\n\t\t\treturn test{\n\t\t\t\tch: &Challenge{\n\t\t\t\t\tID:              \"chID\",\n\t\t\t\t\tAuthorizationID: \"azID\",\n\t\t\t\t\tAccountID:       \"accID\",\n\t\t\t\t\tToken:           \"token\",\n\t\t\t\t\tType:            \"wire-dpop-01\",\n\t\t\t\t\tStatus:          StatusPending,\n\t\t\t\t\tValue:           string(valueBytes),\n\t\t\t\t},\n\t\t\t\tpayload: payload,\n\t\t\t\tctx:     ctx,\n\t\t\t\tjwk:     jwk,\n\t\t\t\tdb: &MockWireDB{\n\t\t\t\t\tMockDB: MockDB{\n\t\t\t\t\t\tMockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {\n\t\t\t\t\t\t\tassert.Equal(t, \"chID\", updch.ID)\n\t\t\t\t\t\t\tassert.Equal(t, \"token\", updch.Token)\n\t\t\t\t\t\t\tassert.Equal(t, StatusValid, updch.Status)\n\t\t\t\t\t\t\tassert.Equal(t, ChallengeType(\"wire-dpop-01\"), updch.Type)\n\t\t\t\t\t\t\tassert.Equal(t, string(valueBytes), updch.Value)\n\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tMockGetAllOrdersByAccountID: func(ctx context.Context, accountID string) ([]string, error) {\n\t\t\t\t\t\tassert.Equal(t, \"accID\", accountID)\n\t\t\t\t\t\treturn []string{\"orderID\"}, nil\n\t\t\t\t\t},\n\t\t\t\t\tMockCreateDpopToken: func(ctx context.Context, orderID string, dpop map[string]interface{}) error {\n\t\t\t\t\t\tassert.Equal(t, \"orderID\", orderID)\n\t\t\t\t\t\tassert.Equal(t, \"token\", dpop[\"chal\"].(string))\n\t\t\t\t\t\tassert.Equal(t, \"wireapp://%40alice_wire@wire.com\", dpop[\"handle\"].(string))\n\t\t\t\t\t\tassert.Equal(t, \"wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com\", dpop[\"sub\"].(string))\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"fail/wire-dpop-01-no-wire-db\": func(t *testing.T) test {\n\t\t\tjwk, _ := mustAccountAndKeyAuthorization(t, \"token\")\n\t\t\tdpopSigner, err := jose.NewSigner(jose.SigningKey{\n\t\t\t\tAlgorithm: jose.SignatureAlgorithm(jwk.Algorithm),\n\t\t\t\tKey:       jwk,\n\t\t\t}, new(jose.SignerOptions))\n\t\t\trequire.NoError(t, err)\n\t\t\tsignerJWK, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\trequire.NoError(t, err)\n\t\t\tsigner, err := jose.NewSigner(jose.SigningKey{\n\t\t\t\tAlgorithm: jose.SignatureAlgorithm(signerJWK.Algorithm),\n\t\t\t\tKey:       signerJWK,\n\t\t\t}, new(jose.SignerOptions))\n\t\t\trequire.NoError(t, err)\n\t\t\tsignerPEMBlock, err := pemutil.Serialize(signerJWK.Public().Key)\n\t\t\trequire.NoError(t, err)\n\t\t\tsignerPEMBytes := pem.EncodeToMemory(signerPEMBlock)\n\t\t\tdpopBytes, err := json.Marshal(struct {\n\t\t\t\tjose.Claims\n\t\t\t\tChallenge string `json:\"chal,omitempty\"`\n\t\t\t\tHandle    string `json:\"handle,omitempty\"`\n\t\t\t\tNonce     string `json:\"nonce,omitempty\"`\n\t\t\t\tHTU       string `json:\"htu,omitempty\"`\n\t\t\t\tName      string `json:\"name,omitempty\"`\n\t\t\t}{\n\t\t\t\tClaims: jose.Claims{\n\t\t\t\t\tSubject:  \"wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com\",\n\t\t\t\t\tAudience: jose.Audience{\"https://ca.example.com/acme/wire/challenge/azID/chID\"},\n\t\t\t\t},\n\t\t\t\tChallenge: \"token\",\n\t\t\t\tHandle:    \"wireapp://%40alice_wire@wire.com\",\n\t\t\t\tNonce:     \"nonce\",\n\t\t\t\tHTU:       \"http://issuer.example.com\",\n\t\t\t\tName:      \"Alice Smith\",\n\t\t\t})\n\t\t\trequire.NoError(t, err)\n\t\t\tdpop, err := dpopSigner.Sign(dpopBytes)\n\t\t\trequire.NoError(t, err)\n\t\t\tproof, err := dpop.CompactSerialize()\n\t\t\trequire.NoError(t, err)\n\t\t\ttokenBytes, err := json.Marshal(struct {\n\t\t\t\tjose.Claims\n\t\t\t\tChallenge string `json:\"chal,omitempty\"`\n\t\t\t\tNonce     string `json:\"nonce,omitempty\"`\n\t\t\t\tCnf       struct {\n\t\t\t\t\tKid string `json:\"kid,omitempty\"`\n\t\t\t\t} `json:\"cnf\"`\n\t\t\t\tProof      string `json:\"proof,omitempty\"`\n\t\t\t\tClientID   string `json:\"client_id\"`\n\t\t\t\tAPIVersion int    `json:\"api_version\"`\n\t\t\t\tScope      string `json:\"scope\"`\n\t\t\t}{\n\t\t\t\tClaims: jose.Claims{\n\t\t\t\t\tIssuer:   \"http://issuer.example.com\",\n\t\t\t\t\tAudience: jose.Audience{\"https://ca.example.com/acme/wire/challenge/azID/chID\"},\n\t\t\t\t\tExpiry:   jose.NewNumericDate(time.Now().Add(1 * time.Minute)),\n\t\t\t\t},\n\t\t\t\tChallenge: \"token\",\n\t\t\t\tNonce:     \"nonce\",\n\t\t\t\tCnf: struct {\n\t\t\t\t\tKid string `json:\"kid,omitempty\"`\n\t\t\t\t}{\n\t\t\t\t\tKid: jwk.KeyID,\n\t\t\t\t},\n\t\t\t\tProof:      proof,\n\t\t\t\tClientID:   \"wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com\",\n\t\t\t\tAPIVersion: 5,\n\t\t\t\tScope:      \"wire_client_id\",\n\t\t\t})\n\t\t\trequire.NoError(t, err)\n\t\t\tsigned, err := signer.Sign(tokenBytes)\n\t\t\trequire.NoError(t, err)\n\t\t\taccessToken, err := signed.CompactSerialize()\n\t\t\trequire.NoError(t, err)\n\t\t\tpayload, err := json.Marshal(struct {\n\t\t\t\tAccessToken string `json:\"access_token\"`\n\t\t\t}{\n\t\t\t\tAccessToken: accessToken,\n\t\t\t})\n\t\t\trequire.NoError(t, err)\n\t\t\tvalueBytes, err := json.Marshal(struct {\n\t\t\t\tName     string `json:\"name,omitempty\"`\n\t\t\t\tDomain   string `json:\"domain,omitempty\"`\n\t\t\t\tClientID string `json:\"client-id,omitempty\"`\n\t\t\t\tHandle   string `json:\"handle,omitempty\"`\n\t\t\t}{\n\t\t\t\tName:     \"Alice Smith\",\n\t\t\t\tDomain:   \"wire.com\",\n\t\t\t\tClientID: \"wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com\",\n\t\t\t\tHandle:   \"wireapp://%40alice_wire@wire.com\",\n\t\t\t})\n\t\t\trequire.NoError(t, err)\n\t\t\tctx := NewProvisionerContext(context.Background(), newWireProvisionerWithOptions(t, &provisioner.Options{\n\t\t\t\tWire: &wireprovisioner.Options{\n\t\t\t\t\tOIDC: &wireprovisioner.OIDCOptions{\n\t\t\t\t\t\tProvider: &wireprovisioner.Provider{\n\t\t\t\t\t\t\tIssuerURL:  \"http://issuerexample.com\",\n\t\t\t\t\t\t\tAlgorithms: []string{\"ES256\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tConfig: &wireprovisioner.Config{\n\t\t\t\t\t\t\tClientID:            \"test\",\n\t\t\t\t\t\t\tSignatureAlgorithms: []string{\"ES256\"},\n\t\t\t\t\t\t\tNow:                 time.Now,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tTransformTemplate: \"\",\n\t\t\t\t\t},\n\t\t\t\t\tDPOP: &wireprovisioner.DPOPOptions{\n\t\t\t\t\t\tTarget:     \"http://issuer.example.com\",\n\t\t\t\t\t\tSigningKey: signerPEMBytes,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}))\n\t\t\tctx = NewLinkerContext(ctx, NewLinker(\"ca.example.com\", \"acme\"))\n\t\t\treturn test{\n\t\t\t\tch: &Challenge{\n\t\t\t\t\tID:              \"chID\",\n\t\t\t\t\tAuthorizationID: \"azID\",\n\t\t\t\t\tAccountID:       \"accID\",\n\t\t\t\t\tToken:           \"token\",\n\t\t\t\t\tType:            \"wire-dpop-01\",\n\t\t\t\t\tStatus:          StatusPending,\n\t\t\t\t\tValue:           string(valueBytes),\n\t\t\t\t},\n\t\t\t\tpayload: payload,\n\t\t\t\tctx:     ctx,\n\t\t\t\tjwk:     jwk,\n\t\t\t\tdb:      &MockDB{},\n\t\t\t\terr: &Error{\n\t\t\t\t\tType:   \"urn:ietf:params:acme:error:serverInternal\",\n\t\t\t\t\tDetail: \"The server experienced an internal error\",\n\t\t\t\t\tStatus: 500,\n\t\t\t\t\tErr:    errors.New(\"db *acme.MockDB is not a WireDB\"),\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ttc := run(t)\n\n\t\t\tif tc.srv != nil {\n\t\t\t\tdefer tc.srv.Close()\n\t\t\t}\n\n\t\t\tctx := tc.ctx\n\t\t\tif ctx == nil {\n\t\t\t\tctx = context.Background()\n\t\t\t}\n\t\t\tctx = NewClientContext(ctx, tc.vc)\n\t\t\terr := tc.ch.Validate(ctx, tc.db, tc.jwk, tc.payload)\n\t\t\tif tc.err != nil {\n\t\t\t\tvar k *Error\n\t\t\t\tif errors.As(err, &k) {\n\t\t\t\t\tassert.Equal(t, tc.err.Type, k.Type)\n\t\t\t\t\tassert.Equal(t, tc.err.Detail, k.Detail)\n\t\t\t\t\tassert.Equal(t, tc.err.Status, k.Status)\n\t\t\t\t\tassert.Equal(t, tc.err.Err.Error(), k.Err.Error())\n\t\t\t\t} else {\n\t\t\t\t\tassert.Fail(t, \"unexpected error type\")\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.NoError(t, err)\n\t\t})\n\t}\n}\n\nfunc mustJWKServer(t *testing.T, pub jose.JSONWebKey) *httptest.Server {\n\tt.Helper()\n\tmux := http.NewServeMux()\n\tserver := httptest.NewServer(mux)\n\tb, err := json.Marshal(struct {\n\t\tKeys []jose.JSONWebKey `json:\"keys,omitempty\"`\n\t}{\n\t\tKeys: []jose.JSONWebKey{pub},\n\t})\n\trequire.NoError(t, err)\n\tjwks := string(b)\n\n\twellKnown := fmt.Sprintf(`{\n\t\t\"issuer\": \"%[1]s\",\n\t\t\"authorization_endpoint\": \"%[1]s/auth\",\n\t\t\"token_endpoint\": \"%[1]s/token\",\n\t\t\"jwks_uri\": \"%[1]s/keys\",\n\t\t\"userinfo_endpoint\": \"%[1]s/userinfo\",\n\t\t\"id_token_signing_alg_values_supported\": [\"ES256\"]\n\t}`, server.URL)\n\n\tmux.HandleFunc(\"/.well-known/openid-configuration\", func(w http.ResponseWriter, req *http.Request) {\n\t\t_, err := io.WriteString(w, wellKnown)\n\t\tif err != nil {\n\t\t\tw.WriteHeader(500)\n\t\t}\n\t})\n\tmux.HandleFunc(\"/keys\", func(w http.ResponseWriter, req *http.Request) {\n\t\t_, err := io.WriteString(w, jwks)\n\t\tif err != nil {\n\t\t\tw.WriteHeader(500)\n\t\t}\n\t})\n\n\tt.Cleanup(server.Close)\n\treturn server\n}\n\ntype errReader int\n\nfunc (errReader) Read([]byte) (int, error) {\n\treturn 0, errors.New(\"force\")\n}\nfunc (errReader) Close() error {\n\treturn nil\n}\n\nfunc TestHTTP01Validate(t *testing.T) {\n\ttype test struct {\n\t\tvc  Client\n\t\tch  *Challenge\n\t\tjwk *jose.JSONWebKey\n\t\tdb  DB\n\t\terr *Error\n\t}\n\ttests := map[string]func(t *testing.T) test{\n\t\t\"fail/http-get-error-store-error\": func(t *testing.T) test {\n\t\t\tch := &Challenge{\n\t\t\t\tID:     \"chID\",\n\t\t\t\tToken:  \"token\",\n\t\t\t\tValue:  \"zap.internal\",\n\t\t\t\tStatus: StatusPending,\n\t\t\t}\n\n\t\t\treturn test{\n\t\t\t\tch: ch,\n\t\t\t\tvc: &mockClient{\n\t\t\t\t\tget: func(url string) (*http.Response, error) {\n\t\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tdb: &MockDB{\n\t\t\t\t\tMockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {\n\t\t\t\t\t\tassert.Equal(t, \"chID\", updch.ID)\n\t\t\t\t\t\tassert.Equal(t, \"token\", updch.Token)\n\t\t\t\t\t\tassert.Equal(t, \"zap.internal\", updch.Value)\n\t\t\t\t\t\tassert.Equal(t, StatusPending, updch.Status)\n\n\t\t\t\t\t\terr := NewError(ErrorConnectionType, \"error doing http GET for url http://zap.internal/.well-known/acme-challenge/%s: force\", ch.Token)\n\t\t\t\t\t\tassert.EqualError(t, updch.Error.Err, err.Err.Error())\n\t\t\t\t\t\tassert.Equal(t, err.Type, updch.Error.Type)\n\t\t\t\t\t\tassert.Equal(t, err.Detail, updch.Error.Detail)\n\t\t\t\t\t\tassert.Equal(t, err.Status, updch.Error.Status)\n\t\t\t\t\t\tassert.Equal(t, err.Subproblems, updch.Error.Subproblems)\n\n\t\t\t\t\t\treturn errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: NewErrorISE(\"failure saving error to acme challenge: force\"),\n\t\t\t}\n\t\t},\n\t\t\"ok/http-get-error\": func(t *testing.T) test {\n\t\t\tch := &Challenge{\n\t\t\t\tID:     \"chID\",\n\t\t\t\tToken:  \"token\",\n\t\t\t\tValue:  \"zap.internal\",\n\t\t\t\tStatus: StatusPending,\n\t\t\t}\n\n\t\t\treturn test{\n\t\t\t\tch: ch,\n\t\t\t\tvc: &mockClient{\n\t\t\t\t\tget: func(url string) (*http.Response, error) {\n\t\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tdb: &MockDB{\n\t\t\t\t\tMockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {\n\t\t\t\t\t\tassert.Equal(t, \"chID\", updch.ID)\n\t\t\t\t\t\tassert.Equal(t, \"token\", updch.Token)\n\t\t\t\t\t\tassert.Equal(t, \"zap.internal\", updch.Value)\n\t\t\t\t\t\tassert.Equal(t, StatusPending, updch.Status)\n\n\t\t\t\t\t\terr := NewError(ErrorConnectionType, \"error doing http GET for url http://zap.internal/.well-known/acme-challenge/%s: force\", ch.Token)\n\t\t\t\t\t\tassert.EqualError(t, updch.Error.Err, err.Err.Error())\n\t\t\t\t\t\tassert.Equal(t, err.Type, updch.Error.Type)\n\t\t\t\t\t\tassert.Equal(t, err.Detail, updch.Error.Detail)\n\t\t\t\t\t\tassert.Equal(t, err.Status, updch.Error.Status)\n\t\t\t\t\t\tassert.Equal(t, err.Subproblems, updch.Error.Subproblems)\n\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"fail/http-get->=400-store-error\": func(t *testing.T) test {\n\t\t\tch := &Challenge{\n\t\t\t\tID:     \"chID\",\n\t\t\t\tToken:  \"token\",\n\t\t\t\tValue:  \"zap.internal\",\n\t\t\t\tStatus: StatusPending,\n\t\t\t}\n\n\t\t\treturn test{\n\t\t\t\tch: ch,\n\t\t\t\tvc: &mockClient{\n\t\t\t\t\tget: func(url string) (*http.Response, error) {\n\t\t\t\t\t\treturn &http.Response{\n\t\t\t\t\t\t\tStatusCode: http.StatusBadRequest,\n\t\t\t\t\t\t\tBody:       errReader(0),\n\t\t\t\t\t\t}, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tdb: &MockDB{\n\t\t\t\t\tMockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {\n\t\t\t\t\t\tassert.Equal(t, \"chID\", updch.ID)\n\t\t\t\t\t\tassert.Equal(t, \"token\", updch.Token)\n\t\t\t\t\t\tassert.Equal(t, \"zap.internal\", updch.Value)\n\t\t\t\t\t\tassert.Equal(t, StatusPending, updch.Status)\n\n\t\t\t\t\t\terr := NewError(ErrorConnectionType, \"error doing http GET for url http://zap.internal/.well-known/acme-challenge/%s with status code 400\", ch.Token)\n\t\t\t\t\t\tassert.EqualError(t, updch.Error.Err, err.Err.Error())\n\t\t\t\t\t\tassert.Equal(t, err.Type, updch.Error.Type)\n\t\t\t\t\t\tassert.Equal(t, err.Detail, updch.Error.Detail)\n\t\t\t\t\t\tassert.Equal(t, err.Status, updch.Error.Status)\n\t\t\t\t\t\tassert.Equal(t, err.Subproblems, updch.Error.Subproblems)\n\n\t\t\t\t\t\treturn errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: NewErrorISE(\"failure saving error to acme challenge: force\"),\n\t\t\t}\n\t\t},\n\t\t\"ok/http-get->=400\": func(t *testing.T) test {\n\t\t\tch := &Challenge{\n\t\t\t\tID:     \"chID\",\n\t\t\t\tToken:  \"token\",\n\t\t\t\tValue:  \"zap.internal\",\n\t\t\t\tStatus: StatusPending,\n\t\t\t}\n\n\t\t\treturn test{\n\t\t\t\tch: ch,\n\t\t\t\tvc: &mockClient{\n\t\t\t\t\tget: func(url string) (*http.Response, error) {\n\t\t\t\t\t\treturn &http.Response{\n\t\t\t\t\t\t\tStatusCode: http.StatusBadRequest,\n\t\t\t\t\t\t\tBody:       errReader(0),\n\t\t\t\t\t\t}, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tdb: &MockDB{\n\t\t\t\t\tMockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {\n\t\t\t\t\t\tassert.Equal(t, \"chID\", updch.ID)\n\t\t\t\t\t\tassert.Equal(t, \"token\", updch.Token)\n\t\t\t\t\t\tassert.Equal(t, \"zap.internal\", updch.Value)\n\t\t\t\t\t\tassert.Equal(t, StatusPending, updch.Status)\n\n\t\t\t\t\t\terr := NewError(ErrorConnectionType, \"error doing http GET for url http://zap.internal/.well-known/acme-challenge/%s with status code 400\", ch.Token)\n\t\t\t\t\t\tassert.EqualError(t, updch.Error.Err, err.Err.Error())\n\t\t\t\t\t\tassert.Equal(t, err.Type, updch.Error.Type)\n\t\t\t\t\t\tassert.Equal(t, err.Detail, updch.Error.Detail)\n\t\t\t\t\t\tassert.Equal(t, err.Status, updch.Error.Status)\n\t\t\t\t\t\tassert.Equal(t, err.Subproblems, updch.Error.Subproblems)\n\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"fail/read-body\": func(t *testing.T) test {\n\t\t\tch := &Challenge{\n\t\t\t\tID:     \"chID\",\n\t\t\t\tToken:  \"token\",\n\t\t\t\tValue:  \"zap.internal\",\n\t\t\t\tStatus: StatusPending,\n\t\t\t}\n\n\t\t\treturn test{\n\t\t\t\tch: ch,\n\t\t\t\tvc: &mockClient{\n\t\t\t\t\tget: func(url string) (*http.Response, error) {\n\t\t\t\t\t\treturn &http.Response{\n\t\t\t\t\t\t\tBody: errReader(0),\n\t\t\t\t\t\t}, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: NewErrorISE(\"error reading response body for url http://zap.internal/.well-known/acme-challenge/%s: force\", ch.Token),\n\t\t\t}\n\t\t},\n\t\t\"fail/key-auth-gen-error\": func(t *testing.T) test {\n\t\t\tch := &Challenge{\n\t\t\t\tID:     \"chID\",\n\t\t\t\tToken:  \"token\",\n\t\t\t\tValue:  \"zap.internal\",\n\t\t\t\tStatus: StatusPending,\n\t\t\t}\n\n\t\t\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\trequire.NoError(t, err)\n\t\t\tjwk.Key = \"foo\"\n\t\t\treturn test{\n\t\t\t\tch: ch,\n\t\t\t\tvc: &mockClient{\n\t\t\t\t\tget: func(url string) (*http.Response, error) {\n\t\t\t\t\t\treturn &http.Response{\n\t\t\t\t\t\t\tBody: io.NopCloser(bytes.NewBufferString(\"foo\")),\n\t\t\t\t\t\t}, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tjwk: jwk,\n\t\t\t\terr: NewErrorISE(\"error generating JWK thumbprint: go-jose/go-jose: unknown key type 'string'\"),\n\t\t\t}\n\t\t},\n\t\t\"ok/key-auth-mismatch\": func(t *testing.T) test {\n\t\t\tch := &Challenge{\n\t\t\t\tID:     \"chID\",\n\t\t\t\tToken:  \"token\",\n\t\t\t\tValue:  \"zap.internal\",\n\t\t\t\tStatus: StatusPending,\n\t\t\t}\n\n\t\t\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\trequire.NoError(t, err)\n\n\t\t\texpKeyAuth, err := KeyAuthorization(ch.Token, jwk)\n\t\t\trequire.NoError(t, err)\n\t\t\treturn test{\n\t\t\t\tch: ch,\n\t\t\t\tvc: &mockClient{\n\t\t\t\t\tget: func(url string) (*http.Response, error) {\n\t\t\t\t\t\treturn &http.Response{\n\t\t\t\t\t\t\tBody: io.NopCloser(bytes.NewBufferString(\"foo\")),\n\t\t\t\t\t\t}, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tjwk: jwk,\n\t\t\t\tdb: &MockDB{\n\t\t\t\t\tMockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {\n\t\t\t\t\t\tassert.Equal(t, \"chID\", updch.ID)\n\t\t\t\t\t\tassert.Equal(t, \"token\", updch.Token)\n\t\t\t\t\t\tassert.Equal(t, \"zap.internal\", updch.Value)\n\t\t\t\t\t\tassert.Equal(t, StatusInvalid, updch.Status)\n\n\t\t\t\t\t\terr := NewError(ErrorRejectedIdentifierType,\n\t\t\t\t\t\t\t\"keyAuthorization does not match; expected %s, but got foo\", expKeyAuth)\n\t\t\t\t\t\tassert.EqualError(t, updch.Error.Err, err.Err.Error())\n\t\t\t\t\t\tassert.Equal(t, err.Type, updch.Error.Type)\n\t\t\t\t\t\tassert.Equal(t, err.Detail, updch.Error.Detail)\n\t\t\t\t\t\tassert.Equal(t, err.Status, updch.Error.Status)\n\t\t\t\t\t\tassert.Equal(t, err.Subproblems, updch.Error.Subproblems)\n\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"fail/key-auth-mismatch-store-error\": func(t *testing.T) test {\n\t\t\tch := &Challenge{\n\t\t\t\tID:     \"chID\",\n\t\t\t\tToken:  \"token\",\n\t\t\t\tValue:  \"zap.internal\",\n\t\t\t\tStatus: StatusPending,\n\t\t\t}\n\n\t\t\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\trequire.NoError(t, err)\n\n\t\t\texpKeyAuth, err := KeyAuthorization(ch.Token, jwk)\n\t\t\trequire.NoError(t, err)\n\t\t\treturn test{\n\t\t\t\tch: ch,\n\t\t\t\tvc: &mockClient{\n\t\t\t\t\tget: func(url string) (*http.Response, error) {\n\t\t\t\t\t\treturn &http.Response{\n\t\t\t\t\t\t\tBody: io.NopCloser(bytes.NewBufferString(\"foo\")),\n\t\t\t\t\t\t}, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tjwk: jwk,\n\t\t\t\tdb: &MockDB{\n\t\t\t\t\tMockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {\n\t\t\t\t\t\tassert.Equal(t, \"chID\", updch.ID)\n\t\t\t\t\t\tassert.Equal(t, \"token\", updch.Token)\n\t\t\t\t\t\tassert.Equal(t, \"zap.internal\", updch.Value)\n\t\t\t\t\t\tassert.Equal(t, StatusInvalid, updch.Status)\n\n\t\t\t\t\t\terr := NewError(ErrorRejectedIdentifierType,\n\t\t\t\t\t\t\t\"keyAuthorization does not match; expected %s, but got foo\", expKeyAuth)\n\t\t\t\t\t\tassert.EqualError(t, updch.Error.Err, err.Err.Error())\n\t\t\t\t\t\tassert.Equal(t, err.Type, updch.Error.Type)\n\t\t\t\t\t\tassert.Equal(t, err.Detail, updch.Error.Detail)\n\t\t\t\t\t\tassert.Equal(t, err.Status, updch.Error.Status)\n\t\t\t\t\t\tassert.Equal(t, err.Subproblems, updch.Error.Subproblems)\n\n\t\t\t\t\t\treturn errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: NewErrorISE(\"failure saving error to acme challenge: force\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/update-challenge-error\": func(t *testing.T) test {\n\t\t\tch := &Challenge{\n\t\t\t\tID:     \"chID\",\n\t\t\t\tToken:  \"token\",\n\t\t\t\tValue:  \"zap.internal\",\n\t\t\t\tStatus: StatusPending,\n\t\t\t}\n\n\t\t\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\trequire.NoError(t, err)\n\n\t\t\texpKeyAuth, err := KeyAuthorization(ch.Token, jwk)\n\t\t\trequire.NoError(t, err)\n\t\t\treturn test{\n\t\t\t\tch: ch,\n\t\t\t\tvc: &mockClient{\n\t\t\t\t\tget: func(url string) (*http.Response, error) {\n\t\t\t\t\t\treturn &http.Response{\n\t\t\t\t\t\t\tBody: io.NopCloser(bytes.NewBufferString(expKeyAuth)),\n\t\t\t\t\t\t}, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tjwk: jwk,\n\t\t\t\tdb: &MockDB{\n\t\t\t\t\tMockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {\n\t\t\t\t\t\tassert.Equal(t, \"chID\", updch.ID)\n\t\t\t\t\t\tassert.Equal(t, \"token\", updch.Token)\n\t\t\t\t\t\tassert.Equal(t, \"zap.internal\", updch.Value)\n\t\t\t\t\t\tassert.Equal(t, StatusValid, updch.Status)\n\t\t\t\t\t\tassert.Nil(t, updch.Error)\n\n\t\t\t\t\t\tva, err := time.Parse(time.RFC3339, updch.ValidatedAt)\n\t\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t\t\tnow := clock.Now()\n\t\t\t\t\t\tassert.True(t, va.Add(-time.Minute).Before(now))\n\t\t\t\t\t\tassert.True(t, va.Add(time.Minute).After(now))\n\n\t\t\t\t\t\treturn errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: NewErrorISE(\"error updating challenge: force\"),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tch := &Challenge{\n\t\t\t\tID:     \"chID\",\n\t\t\t\tToken:  \"token\",\n\t\t\t\tValue:  \"zap.internal\",\n\t\t\t\tStatus: StatusPending,\n\t\t\t}\n\n\t\t\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\trequire.NoError(t, err)\n\n\t\t\texpKeyAuth, err := KeyAuthorization(ch.Token, jwk)\n\t\t\trequire.NoError(t, err)\n\t\t\treturn test{\n\t\t\t\tch: ch,\n\t\t\t\tvc: &mockClient{\n\t\t\t\t\tget: func(url string) (*http.Response, error) {\n\t\t\t\t\t\treturn &http.Response{\n\t\t\t\t\t\t\tBody: io.NopCloser(bytes.NewBufferString(expKeyAuth)),\n\t\t\t\t\t\t}, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tjwk: jwk,\n\t\t\t\tdb: &MockDB{\n\t\t\t\t\tMockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {\n\t\t\t\t\t\tassert.Equal(t, \"chID\", updch.ID)\n\t\t\t\t\t\tassert.Equal(t, \"token\", updch.Token)\n\t\t\t\t\t\tassert.Equal(t, \"zap.internal\", updch.Value)\n\t\t\t\t\t\tassert.Equal(t, StatusValid, updch.Status)\n\t\t\t\t\t\tassert.Nil(t, updch.Error)\n\n\t\t\t\t\t\tva, err := time.Parse(time.RFC3339, updch.ValidatedAt)\n\t\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t\t\tnow := clock.Now()\n\t\t\t\t\t\tassert.True(t, va.Add(-time.Minute).Before(now))\n\t\t\t\t\t\tassert.True(t, va.Add(time.Minute).After(now))\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ttc := run(t)\n\t\t\tctx := NewClientContext(context.Background(), tc.vc)\n\t\t\tif err := http01Validate(ctx, tc.ch, tc.db, tc.jwk); err != nil {\n\t\t\t\tif assert.Error(t, tc.err) {\n\t\t\t\t\tvar k *Error\n\t\t\t\t\tif errors.As(err, &k) {\n\t\t\t\t\t\tassert.Equal(t, tc.err.Type, k.Type)\n\t\t\t\t\t\tassert.Equal(t, tc.err.Detail, k.Detail)\n\t\t\t\t\t\tassert.Equal(t, tc.err.Status, k.Status)\n\t\t\t\t\t\tassert.Equal(t, tc.err.Err.Error(), k.Err.Error())\n\t\t\t\t\t} else {\n\t\t\t\t\t\tassert.Fail(t, \"unexpected error type\")\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tassert.Nil(t, tc.err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDNS01Validate(t *testing.T) {\n\tfulldomain := \"*.zap.internal\"\n\tdomain := strings.TrimPrefix(fulldomain, \"*.\")\n\ttype test struct {\n\t\tvc  Client\n\t\tch  *Challenge\n\t\tjwk *jose.JSONWebKey\n\t\tdb  DB\n\t\terr *Error\n\t}\n\ttests := map[string]func(t *testing.T) test{\n\t\t\"fail/lookupTXT-store-error\": func(t *testing.T) test {\n\t\t\tch := &Challenge{\n\t\t\t\tID:     \"chID\",\n\t\t\t\tToken:  \"token\",\n\t\t\t\tValue:  fulldomain,\n\t\t\t\tStatus: StatusPending,\n\t\t\t}\n\n\t\t\treturn test{\n\t\t\t\tch: ch,\n\t\t\t\tvc: &mockClient{\n\t\t\t\t\tlookupTxt: func(url string) ([]string, error) {\n\t\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tdb: &MockDB{\n\t\t\t\t\tMockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {\n\t\t\t\t\t\tassert.Equal(t, \"chID\", updch.ID)\n\t\t\t\t\t\tassert.Equal(t, \"token\", updch.Token)\n\t\t\t\t\t\tassert.Equal(t, fulldomain, updch.Value)\n\t\t\t\t\t\tassert.Equal(t, StatusPending, updch.Status)\n\n\t\t\t\t\t\terr := NewError(ErrorDNSType, \"error looking up TXT records for domain %s: force\", domain)\n\n\t\t\t\t\t\tassert.EqualError(t, updch.Error.Err, err.Err.Error())\n\t\t\t\t\t\tassert.Equal(t, err.Type, updch.Error.Type)\n\t\t\t\t\t\tassert.Equal(t, err.Detail, updch.Error.Detail)\n\t\t\t\t\t\tassert.Equal(t, err.Status, updch.Error.Status)\n\t\t\t\t\t\tassert.Equal(t, err.Subproblems, updch.Error.Subproblems)\n\n\t\t\t\t\t\treturn errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: NewErrorISE(\"failure saving error to acme challenge: force\"),\n\t\t\t}\n\t\t},\n\t\t\"ok/lookupTXT-error\": func(t *testing.T) test {\n\t\t\tch := &Challenge{\n\t\t\t\tID:     \"chID\",\n\t\t\t\tToken:  \"token\",\n\t\t\t\tValue:  fulldomain,\n\t\t\t\tStatus: StatusPending,\n\t\t\t}\n\n\t\t\treturn test{\n\t\t\t\tch: ch,\n\t\t\t\tvc: &mockClient{\n\t\t\t\t\tlookupTxt: func(url string) ([]string, error) {\n\t\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tdb: &MockDB{\n\t\t\t\t\tMockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {\n\t\t\t\t\t\tassert.Equal(t, \"chID\", updch.ID)\n\t\t\t\t\t\tassert.Equal(t, \"token\", updch.Token)\n\t\t\t\t\t\tassert.Equal(t, fulldomain, updch.Value)\n\t\t\t\t\t\tassert.Equal(t, StatusPending, updch.Status)\n\n\t\t\t\t\t\terr := NewError(ErrorDNSType, \"error looking up TXT records for domain %s: force\", domain)\n\n\t\t\t\t\t\tassert.EqualError(t, updch.Error.Err, err.Err.Error())\n\t\t\t\t\t\tassert.Equal(t, err.Type, updch.Error.Type)\n\t\t\t\t\t\tassert.Equal(t, err.Detail, updch.Error.Detail)\n\t\t\t\t\t\tassert.Equal(t, err.Status, updch.Error.Status)\n\t\t\t\t\t\tassert.Equal(t, err.Subproblems, updch.Error.Subproblems)\n\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"fail/key-auth-gen-error\": func(t *testing.T) test {\n\t\t\tch := &Challenge{\n\t\t\t\tID:     \"chID\",\n\t\t\t\tToken:  \"token\",\n\t\t\t\tValue:  fulldomain,\n\t\t\t\tStatus: StatusPending,\n\t\t\t}\n\n\t\t\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\trequire.NoError(t, err)\n\t\t\tjwk.Key = \"foo\"\n\n\t\t\treturn test{\n\t\t\t\tch: ch,\n\t\t\t\tvc: &mockClient{\n\t\t\t\t\tlookupTxt: func(url string) ([]string, error) {\n\t\t\t\t\t\treturn []string{\"foo\"}, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tjwk: jwk,\n\t\t\t\terr: NewErrorISE(\"error generating JWK thumbprint: go-jose/go-jose: unknown key type 'string'\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/key-auth-mismatch-store-error\": func(t *testing.T) test {\n\t\t\tch := &Challenge{\n\t\t\t\tID:     \"chID\",\n\t\t\t\tToken:  \"token\",\n\t\t\t\tValue:  fulldomain,\n\t\t\t\tStatus: StatusPending,\n\t\t\t}\n\n\t\t\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\trequire.NoError(t, err)\n\n\t\t\texpKeyAuth, err := KeyAuthorization(ch.Token, jwk)\n\t\t\trequire.NoError(t, err)\n\n\t\t\treturn test{\n\t\t\t\tch: ch,\n\t\t\t\tvc: &mockClient{\n\t\t\t\t\tlookupTxt: func(url string) ([]string, error) {\n\t\t\t\t\t\treturn []string{\"foo\", \"bar\"}, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tdb: &MockDB{\n\t\t\t\t\tMockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {\n\t\t\t\t\t\tassert.Equal(t, \"chID\", updch.ID)\n\t\t\t\t\t\tassert.Equal(t, \"token\", updch.Token)\n\t\t\t\t\t\tassert.Equal(t, fulldomain, updch.Value)\n\t\t\t\t\t\tassert.Equal(t, StatusPending, updch.Status)\n\n\t\t\t\t\t\terr := NewError(ErrorRejectedIdentifierType, \"keyAuthorization does not match; expected %s, but got %s\", expKeyAuth, []string{\"foo\", \"bar\"})\n\n\t\t\t\t\t\tassert.EqualError(t, updch.Error.Err, err.Err.Error())\n\t\t\t\t\t\tassert.Equal(t, err.Type, updch.Error.Type)\n\t\t\t\t\t\tassert.Equal(t, err.Detail, updch.Error.Detail)\n\t\t\t\t\t\tassert.Equal(t, err.Status, updch.Error.Status)\n\t\t\t\t\t\tassert.Equal(t, err.Subproblems, updch.Error.Subproblems)\n\n\t\t\t\t\t\treturn errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tjwk: jwk,\n\t\t\t\terr: NewErrorISE(\"failure saving error to acme challenge: force\"),\n\t\t\t}\n\t\t},\n\t\t\"ok/key-auth-mismatch-store-error\": func(t *testing.T) test {\n\t\t\tch := &Challenge{\n\t\t\t\tID:     \"chID\",\n\t\t\t\tToken:  \"token\",\n\t\t\t\tValue:  fulldomain,\n\t\t\t\tStatus: StatusPending,\n\t\t\t}\n\n\t\t\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\trequire.NoError(t, err)\n\n\t\t\texpKeyAuth, err := KeyAuthorization(ch.Token, jwk)\n\t\t\trequire.NoError(t, err)\n\n\t\t\treturn test{\n\t\t\t\tch: ch,\n\t\t\t\tvc: &mockClient{\n\t\t\t\t\tlookupTxt: func(url string) ([]string, error) {\n\t\t\t\t\t\treturn []string{\"foo\", \"bar\"}, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tdb: &MockDB{\n\t\t\t\t\tMockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {\n\t\t\t\t\t\tassert.Equal(t, \"chID\", updch.ID)\n\t\t\t\t\t\tassert.Equal(t, \"token\", updch.Token)\n\t\t\t\t\t\tassert.Equal(t, fulldomain, updch.Value)\n\t\t\t\t\t\tassert.Equal(t, StatusPending, updch.Status)\n\n\t\t\t\t\t\terr := NewError(ErrorRejectedIdentifierType, \"keyAuthorization does not match; expected %s, but got %s\", expKeyAuth, []string{\"foo\", \"bar\"})\n\n\t\t\t\t\t\tassert.EqualError(t, updch.Error.Err, err.Err.Error())\n\t\t\t\t\t\tassert.Equal(t, err.Type, updch.Error.Type)\n\t\t\t\t\t\tassert.Equal(t, err.Detail, updch.Error.Detail)\n\t\t\t\t\t\tassert.Equal(t, err.Status, updch.Error.Status)\n\t\t\t\t\t\tassert.Equal(t, err.Subproblems, updch.Error.Subproblems)\n\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tjwk: jwk,\n\t\t\t}\n\t\t},\n\t\t\"fail/update-challenge-error\": func(t *testing.T) test {\n\t\t\tch := &Challenge{\n\t\t\t\tID:     \"chID\",\n\t\t\t\tToken:  \"token\",\n\t\t\t\tValue:  fulldomain,\n\t\t\t\tStatus: StatusPending,\n\t\t\t}\n\n\t\t\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\trequire.NoError(t, err)\n\n\t\t\texpKeyAuth, err := KeyAuthorization(ch.Token, jwk)\n\t\t\trequire.NoError(t, err)\n\t\t\th := sha256.Sum256([]byte(expKeyAuth))\n\t\t\texpected := base64.RawURLEncoding.EncodeToString(h[:])\n\n\t\t\treturn test{\n\t\t\t\tch: ch,\n\t\t\t\tvc: &mockClient{\n\t\t\t\t\tlookupTxt: func(url string) ([]string, error) {\n\t\t\t\t\t\treturn []string{\"foo\", expected}, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tdb: &MockDB{\n\t\t\t\t\tMockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {\n\t\t\t\t\t\tassert.Equal(t, \"chID\", updch.ID)\n\t\t\t\t\t\tassert.Equal(t, \"token\", updch.Token)\n\t\t\t\t\t\tassert.Equal(t, fulldomain, ch.Value)\n\t\t\t\t\t\tassert.Equal(t, StatusValid, updch.Status)\n\t\t\t\t\t\tassert.Nil(t, updch.Error)\n\n\t\t\t\t\t\tva, err := time.Parse(time.RFC3339, updch.ValidatedAt)\n\t\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t\t\tnow := clock.Now()\n\t\t\t\t\t\tassert.True(t, va.Add(-time.Minute).Before(now))\n\t\t\t\t\t\tassert.True(t, va.Add(time.Minute).After(now))\n\n\t\t\t\t\t\treturn errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tjwk: jwk,\n\t\t\t\terr: NewErrorISE(\"error updating challenge: force\"),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tch := &Challenge{\n\t\t\t\tID:     \"chID\",\n\t\t\t\tToken:  \"token\",\n\t\t\t\tValue:  fulldomain,\n\t\t\t\tStatus: StatusPending,\n\t\t\t}\n\n\t\t\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\trequire.NoError(t, err)\n\n\t\t\texpKeyAuth, err := KeyAuthorization(ch.Token, jwk)\n\t\t\trequire.NoError(t, err)\n\t\t\th := sha256.Sum256([]byte(expKeyAuth))\n\t\t\texpected := base64.RawURLEncoding.EncodeToString(h[:])\n\n\t\t\treturn test{\n\t\t\t\tch: ch,\n\t\t\t\tvc: &mockClient{\n\t\t\t\t\tlookupTxt: func(url string) ([]string, error) {\n\t\t\t\t\t\treturn []string{\"foo\", expected}, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tdb: &MockDB{\n\t\t\t\t\tMockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {\n\t\t\t\t\t\tassert.Equal(t, \"chID\", updch.ID)\n\t\t\t\t\t\tassert.Equal(t, \"token\", updch.Token)\n\t\t\t\t\t\tassert.Equal(t, fulldomain, updch.Value)\n\t\t\t\t\t\tassert.Equal(t, StatusValid, updch.Status)\n\t\t\t\t\t\tassert.Nil(t, updch.Error)\n\n\t\t\t\t\t\tva, err := time.Parse(time.RFC3339, updch.ValidatedAt)\n\t\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t\t\tnow := clock.Now()\n\t\t\t\t\t\tassert.True(t, va.Add(-time.Minute).Before(now))\n\t\t\t\t\t\tassert.True(t, va.Add(time.Minute).After(now))\n\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tjwk: jwk,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ttc := run(t)\n\t\t\tctx := NewClientContext(context.Background(), tc.vc)\n\t\t\tif err := dns01Validate(ctx, tc.ch, tc.db, tc.jwk); err != nil {\n\t\t\t\tif assert.Error(t, tc.err) {\n\t\t\t\t\tvar k *Error\n\t\t\t\t\tif errors.As(err, &k) {\n\t\t\t\t\t\tassert.Equal(t, tc.err.Type, k.Type)\n\t\t\t\t\t\tassert.Equal(t, tc.err.Detail, k.Detail)\n\t\t\t\t\t\tassert.Equal(t, tc.err.Status, k.Status)\n\t\t\t\t\t\tassert.Equal(t, tc.err.Err.Error(), k.Err.Error())\n\t\t\t\t\t} else {\n\t\t\t\t\t\tassert.Fail(t, \"unexpected error type\")\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tassert.Nil(t, tc.err)\n\t\t\t}\n\t\t})\n\t}\n}\n\ntype tlsDialer func(network, addr string, config *tls.Config) (conn *tls.Conn, err error)\n\nfunc newTestTLSALPNServer(validationCert *tls.Certificate, opts ...func(*httptest.Server)) (*httptest.Server, tlsDialer) {\n\tsrv := httptest.NewUnstartedServer(http.NewServeMux())\n\n\tsrv.Config.TLSNextProto = map[string]func(*http.Server, *tls.Conn, http.Handler){\n\t\t\"acme-tls/1\": func(_ *http.Server, conn *tls.Conn, _ http.Handler) {\n\t\t\t// no-op\n\t\t},\n\t\t\"http/1.1\": func(_ *http.Server, conn *tls.Conn, _ http.Handler) {\n\t\t\tpanic(\"unexpected http/1.1 next proto\")\n\t\t},\n\t}\n\n\tsrv.TLS = &tls.Config{\n\t\tGetCertificate: func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {\n\t\t\tif len(hello.SupportedProtos) == 1 && hello.SupportedProtos[0] == \"acme-tls/1\" {\n\t\t\t\treturn validationCert, nil\n\t\t\t}\n\t\t\treturn nil, nil\n\t\t},\n\t\tNextProtos: []string{\n\t\t\t\"acme-tls/1\",\n\t\t\t\"http/1.1\",\n\t\t},\n\t}\n\n\t// Apply options\n\tfor _, fn := range opts {\n\t\tfn(srv)\n\t}\n\n\tsrv.Listener = tls.NewListener(srv.Listener, srv.TLS)\n\t//srv.Config.ErrorLog = log.New(ioutil.Discard, \"\", 0) // hush\n\n\treturn srv, func(network, addr string, config *tls.Config) (conn *tls.Conn, err error) {\n\t\treturn tls.DialWithDialer(&net.Dialer{Timeout: time.Second}, \"tcp\", srv.Listener.Addr().String(), config)\n\t}\n}\n\n// noopConn is a mock net.Conn that does nothing.\ntype noopConn struct{}\n\nfunc (c *noopConn) Read(_ []byte) (n int, err error)  { return 0, io.EOF }\nfunc (c *noopConn) Write(_ []byte) (n int, err error) { return 0, io.EOF }\nfunc (c *noopConn) Close() error                      { return nil }\nfunc (c *noopConn) LocalAddr() net.Addr               { return &net.IPAddr{IP: net.IPv4zero, Zone: \"\"} }\nfunc (c *noopConn) RemoteAddr() net.Addr              { return &net.IPAddr{IP: net.IPv4zero, Zone: \"\"} }\nfunc (c *noopConn) SetDeadline(time.Time) error       { return nil }\nfunc (c *noopConn) SetReadDeadline(time.Time) error   { return nil }\nfunc (c *noopConn) SetWriteDeadline(time.Time) error  { return nil }\n\nfunc newTLSALPNValidationCert(keyAuthHash []byte, obsoleteOID, critical bool, names ...string) (*tls.Certificate, error) {\n\tprivateKey, err := rsa.GenerateKey(rand.Reader, 2048)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tcertTemplate := &x509.Certificate{\n\t\tSerialNumber: big.NewInt(1337),\n\t\tSubject: pkix.Name{\n\t\t\tOrganization: []string{\"Test\"},\n\t\t},\n\t\tNotBefore:             time.Now(),\n\t\tNotAfter:              time.Now().AddDate(0, 0, 1),\n\t\tKeyUsage:              x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,\n\t\tExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},\n\t\tBasicConstraintsValid: true,\n\t\tDNSNames:              names,\n\t}\n\n\tif keyAuthHash != nil {\n\t\toid := asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 31}\n\t\tif obsoleteOID {\n\t\t\toid = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 30, 1}\n\t\t}\n\n\t\tkeyAuthHashEnc, _ := asn1.Marshal(keyAuthHash)\n\n\t\tcertTemplate.ExtraExtensions = []pkix.Extension{\n\t\t\t{\n\t\t\t\tId:       oid,\n\t\t\t\tCritical: critical,\n\t\t\t\tValue:    keyAuthHashEnc,\n\t\t\t},\n\t\t}\n\t}\n\n\tcert, err := x509.CreateCertificate(rand.Reader, certTemplate, certTemplate, privateKey.Public(), privateKey)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &tls.Certificate{\n\t\tPrivateKey:  privateKey,\n\t\tCertificate: [][]byte{cert},\n\t}, nil\n}\n\nfunc TestTLSALPN01Validate(t *testing.T) {\n\tmakeTLSCh := func() *Challenge {\n\t\treturn &Challenge{\n\t\t\tID:     \"chID\",\n\t\t\tToken:  \"token\",\n\t\t\tType:   \"tls-alpn-01\",\n\t\t\tStatus: StatusPending,\n\t\t\tValue:  \"zap.internal\",\n\t\t}\n\t}\n\ttype test struct {\n\t\tvc  Client\n\t\tch  *Challenge\n\t\tjwk *jose.JSONWebKey\n\t\tdb  DB\n\t\tsrv *httptest.Server\n\t\terr *Error\n\t}\n\ttests := map[string]func(t *testing.T) test{\n\t\t\"fail/tlsDial-store-error\": func(t *testing.T) test {\n\t\t\tch := makeTLSCh()\n\t\t\treturn test{\n\t\t\t\tch: ch,\n\t\t\t\tvc: &mockClient{\n\t\t\t\t\ttlsDial: func(network, addr string, config *tls.Config) (*tls.Conn, error) {\n\t\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tdb: &MockDB{\n\t\t\t\t\tMockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {\n\t\t\t\t\t\tassert.Equal(t, \"chID\", updch.ID)\n\t\t\t\t\t\tassert.Equal(t, \"token\", updch.Token)\n\t\t\t\t\t\tassert.Equal(t, StatusPending, updch.Status)\n\t\t\t\t\t\tassert.Equal(t, ChallengeType(\"tls-alpn-01\"), updch.Type)\n\t\t\t\t\t\tassert.Equal(t, \"zap.internal\", updch.Value)\n\n\t\t\t\t\t\terr := NewError(ErrorConnectionType, \"error doing TLS dial for %v: force\", ch.Value)\n\n\t\t\t\t\t\tassert.EqualError(t, updch.Error.Err, err.Err.Error())\n\t\t\t\t\t\tassert.Equal(t, err.Type, updch.Error.Type)\n\t\t\t\t\t\tassert.Equal(t, err.Detail, updch.Error.Detail)\n\t\t\t\t\t\tassert.Equal(t, err.Status, updch.Error.Status)\n\t\t\t\t\t\tassert.Equal(t, err.Subproblems, updch.Error.Subproblems)\n\n\t\t\t\t\t\treturn errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: NewErrorISE(\"failure saving error to acme challenge: force\"),\n\t\t\t}\n\t\t},\n\t\t\"ok/tlsDial-error\": func(t *testing.T) test {\n\t\t\tch := makeTLSCh()\n\t\t\treturn test{\n\t\t\t\tch: ch,\n\t\t\t\tvc: &mockClient{\n\t\t\t\t\ttlsDial: func(network, addr string, config *tls.Config) (*tls.Conn, error) {\n\t\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tdb: &MockDB{\n\t\t\t\t\tMockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {\n\t\t\t\t\t\tassert.Equal(t, \"chID\", updch.ID)\n\t\t\t\t\t\tassert.Equal(t, \"token\", updch.Token)\n\t\t\t\t\t\tassert.Equal(t, StatusPending, updch.Status)\n\t\t\t\t\t\tassert.Equal(t, ChallengeType(\"tls-alpn-01\"), updch.Type)\n\t\t\t\t\t\tassert.Equal(t, \"zap.internal\", updch.Value)\n\n\t\t\t\t\t\terr := NewError(ErrorConnectionType, \"error doing TLS dial for %v: force\", ch.Value)\n\n\t\t\t\t\t\tassert.EqualError(t, updch.Error.Err, err.Err.Error())\n\t\t\t\t\t\tassert.Equal(t, err.Type, updch.Error.Type)\n\t\t\t\t\t\tassert.Equal(t, err.Detail, updch.Error.Detail)\n\t\t\t\t\t\tassert.Equal(t, err.Status, updch.Error.Status)\n\t\t\t\t\t\tassert.Equal(t, err.Subproblems, updch.Error.Subproblems)\n\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"ok/tlsDial-timeout\": func(t *testing.T) test {\n\t\t\tch := makeTLSCh()\n\n\t\t\tsrv, tlsDial := newTestTLSALPNServer(nil)\n\t\t\t// srv.Start() - do not start server to cause timeout\n\n\t\t\treturn test{\n\t\t\t\tch: ch,\n\t\t\t\tvc: &mockClient{\n\t\t\t\t\ttlsDial: tlsDial,\n\t\t\t\t},\n\t\t\t\tdb: &MockDB{\n\t\t\t\t\tMockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {\n\t\t\t\t\t\tassert.Equal(t, \"chID\", updch.ID)\n\t\t\t\t\t\tassert.Equal(t, \"token\", updch.Token)\n\t\t\t\t\t\tassert.Equal(t, StatusPending, updch.Status)\n\t\t\t\t\t\tassert.Equal(t, ChallengeType(\"tls-alpn-01\"), updch.Type)\n\t\t\t\t\t\tassert.Equal(t, \"zap.internal\", updch.Value)\n\n\t\t\t\t\t\terr := NewError(ErrorConnectionType, \"error doing TLS dial for %v: context deadline exceeded\", ch.Value)\n\n\t\t\t\t\t\tassert.EqualError(t, updch.Error.Err, err.Err.Error())\n\t\t\t\t\t\tassert.Equal(t, err.Type, updch.Error.Type)\n\t\t\t\t\t\tassert.Equal(t, err.Detail, updch.Error.Detail)\n\t\t\t\t\t\tassert.Equal(t, err.Status, updch.Error.Status)\n\t\t\t\t\t\tassert.Equal(t, err.Subproblems, updch.Error.Subproblems)\n\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tsrv: srv,\n\t\t\t}\n\t\t},\n\t\t\"ok/no-certificates-error\": func(t *testing.T) test {\n\t\t\tch := makeTLSCh()\n\n\t\t\treturn test{\n\t\t\t\tch: ch,\n\t\t\t\tvc: &mockClient{\n\t\t\t\t\ttlsDial: func(network, addr string, config *tls.Config) (*tls.Conn, error) {\n\t\t\t\t\t\treturn tls.Client(&noopConn{}, config), nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tdb: &MockDB{\n\t\t\t\t\tMockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {\n\t\t\t\t\t\tassert.Equal(t, \"chID\", updch.ID)\n\t\t\t\t\t\tassert.Equal(t, \"token\", updch.Token)\n\t\t\t\t\t\tassert.Equal(t, StatusInvalid, updch.Status)\n\t\t\t\t\t\tassert.Equal(t, ChallengeType(\"tls-alpn-01\"), updch.Type)\n\t\t\t\t\t\tassert.Equal(t, \"zap.internal\", updch.Value)\n\n\t\t\t\t\t\terr := NewError(ErrorRejectedIdentifierType, \"tls-alpn-01 challenge for %v resulted in no certificates\", ch.Value)\n\n\t\t\t\t\t\tassert.EqualError(t, updch.Error.Err, err.Err.Error())\n\t\t\t\t\t\tassert.Equal(t, err.Type, updch.Error.Type)\n\t\t\t\t\t\tassert.Equal(t, err.Detail, updch.Error.Detail)\n\t\t\t\t\t\tassert.Equal(t, err.Status, updch.Error.Status)\n\t\t\t\t\t\tassert.Equal(t, err.Subproblems, updch.Error.Subproblems)\n\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"fail/no-certificates-store-error\": func(t *testing.T) test {\n\t\t\tch := makeTLSCh()\n\n\t\t\treturn test{\n\t\t\t\tch: ch,\n\t\t\t\tvc: &mockClient{\n\t\t\t\t\ttlsDial: func(network, addr string, config *tls.Config) (*tls.Conn, error) {\n\t\t\t\t\t\treturn tls.Client(&noopConn{}, config), nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tdb: &MockDB{\n\t\t\t\t\tMockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {\n\t\t\t\t\t\tassert.Equal(t, \"chID\", updch.ID)\n\t\t\t\t\t\tassert.Equal(t, \"token\", updch.Token)\n\t\t\t\t\t\tassert.Equal(t, StatusInvalid, updch.Status)\n\t\t\t\t\t\tassert.Equal(t, ChallengeType(\"tls-alpn-01\"), updch.Type)\n\t\t\t\t\t\tassert.Equal(t, \"zap.internal\", updch.Value)\n\n\t\t\t\t\t\terr := NewError(ErrorRejectedIdentifierType, \"tls-alpn-01 challenge for %v resulted in no certificates\", ch.Value)\n\n\t\t\t\t\t\tassert.EqualError(t, updch.Error.Err, err.Err.Error())\n\t\t\t\t\t\tassert.Equal(t, err.Type, updch.Error.Type)\n\t\t\t\t\t\tassert.Equal(t, err.Detail, updch.Error.Detail)\n\t\t\t\t\t\tassert.Equal(t, err.Status, updch.Error.Status)\n\t\t\t\t\t\tassert.Equal(t, err.Subproblems, updch.Error.Subproblems)\n\n\t\t\t\t\t\treturn errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: NewErrorISE(\"failure saving error to acme challenge: force\"),\n\t\t\t}\n\t\t},\n\t\t\"ok/error-no-protocol\": func(t *testing.T) test {\n\t\t\tch := makeTLSCh()\n\n\t\t\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tsrv := httptest.NewTLSServer(nil)\n\n\t\t\treturn test{\n\t\t\t\tch: ch,\n\t\t\t\tvc: &mockClient{\n\t\t\t\t\ttlsDial: func(network, addr string, config *tls.Config) (*tls.Conn, error) {\n\t\t\t\t\t\treturn tls.DialWithDialer(&net.Dialer{Timeout: time.Second}, \"tcp\", srv.Listener.Addr().String(), config)\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tdb: &MockDB{\n\t\t\t\t\tMockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {\n\t\t\t\t\t\tassert.Equal(t, \"chID\", updch.ID)\n\t\t\t\t\t\tassert.Equal(t, \"token\", updch.Token)\n\t\t\t\t\t\tassert.Equal(t, StatusInvalid, updch.Status)\n\t\t\t\t\t\tassert.Equal(t, ChallengeType(\"tls-alpn-01\"), updch.Type)\n\t\t\t\t\t\tassert.Equal(t, \"zap.internal\", updch.Value)\n\n\t\t\t\t\t\terr := NewError(ErrorRejectedIdentifierType, \"cannot negotiate ALPN acme-tls/1 protocol for tls-alpn-01 challenge\")\n\n\t\t\t\t\t\tassert.EqualError(t, updch.Error.Err, err.Err.Error())\n\t\t\t\t\t\tassert.Equal(t, err.Type, updch.Error.Type)\n\t\t\t\t\t\tassert.Equal(t, err.Detail, updch.Error.Detail)\n\t\t\t\t\t\tassert.Equal(t, err.Status, updch.Error.Status)\n\t\t\t\t\t\tassert.Equal(t, err.Subproblems, updch.Error.Subproblems)\n\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tsrv: srv,\n\t\t\t\tjwk: jwk,\n\t\t\t}\n\t\t},\n\t\t\"fail/no-protocol-store-error\": func(t *testing.T) test {\n\t\t\tch := makeTLSCh()\n\n\t\t\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tsrv := httptest.NewTLSServer(nil)\n\n\t\t\treturn test{\n\t\t\t\tch: ch,\n\t\t\t\tvc: &mockClient{\n\t\t\t\t\ttlsDial: func(network, addr string, config *tls.Config) (*tls.Conn, error) {\n\t\t\t\t\t\treturn tls.DialWithDialer(&net.Dialer{Timeout: time.Second}, \"tcp\", srv.Listener.Addr().String(), config)\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tdb: &MockDB{\n\t\t\t\t\tMockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {\n\t\t\t\t\t\tassert.Equal(t, \"chID\", updch.ID)\n\t\t\t\t\t\tassert.Equal(t, \"token\", updch.Token)\n\t\t\t\t\t\tassert.Equal(t, StatusInvalid, updch.Status)\n\t\t\t\t\t\tassert.Equal(t, ChallengeType(\"tls-alpn-01\"), updch.Type)\n\t\t\t\t\t\tassert.Equal(t, \"zap.internal\", updch.Value)\n\n\t\t\t\t\t\terr := NewError(ErrorRejectedIdentifierType, \"cannot negotiate ALPN acme-tls/1 protocol for tls-alpn-01 challenge\")\n\n\t\t\t\t\t\tassert.EqualError(t, updch.Error.Err, err.Err.Error())\n\t\t\t\t\t\tassert.Equal(t, err.Type, updch.Error.Type)\n\t\t\t\t\t\tassert.Equal(t, err.Detail, updch.Error.Detail)\n\t\t\t\t\t\tassert.Equal(t, err.Status, updch.Error.Status)\n\t\t\t\t\t\tassert.Equal(t, err.Subproblems, updch.Error.Subproblems)\n\n\t\t\t\t\t\treturn errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tsrv: srv,\n\t\t\t\tjwk: jwk,\n\t\t\t\terr: NewErrorISE(\"failure saving error to acme challenge: force\"),\n\t\t\t}\n\t\t},\n\t\t\"ok/no-names-nor-ips-error\": func(t *testing.T) test {\n\t\t\tch := makeTLSCh()\n\n\t\t\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\trequire.NoError(t, err)\n\n\t\t\texpKeyAuth, err := KeyAuthorization(ch.Token, jwk)\n\t\t\trequire.NoError(t, err)\n\t\t\texpKeyAuthHash := sha256.Sum256([]byte(expKeyAuth))\n\n\t\t\tcert, err := newTLSALPNValidationCert(expKeyAuthHash[:], false, true)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tsrv, tlsDial := newTestTLSALPNServer(cert)\n\t\t\tsrv.Start()\n\n\t\t\treturn test{\n\t\t\t\tch: ch,\n\t\t\t\tvc: &mockClient{\n\t\t\t\t\ttlsDial: tlsDial,\n\t\t\t\t},\n\t\t\t\tdb: &MockDB{\n\t\t\t\t\tMockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {\n\t\t\t\t\t\tassert.Equal(t, \"chID\", updch.ID)\n\t\t\t\t\t\tassert.Equal(t, \"token\", updch.Token)\n\t\t\t\t\t\tassert.Equal(t, StatusInvalid, updch.Status)\n\t\t\t\t\t\tassert.Equal(t, ChallengeType(\"tls-alpn-01\"), updch.Type)\n\t\t\t\t\t\tassert.Equal(t, \"zap.internal\", updch.Value)\n\n\t\t\t\t\t\terr := NewError(ErrorRejectedIdentifierType, \"incorrect certificate for tls-alpn-01 challenge: leaf certificate must contain a single IP address or DNS name, %v\", ch.Value)\n\n\t\t\t\t\t\tassert.EqualError(t, updch.Error.Err, err.Err.Error())\n\t\t\t\t\t\tassert.Equal(t, err.Type, updch.Error.Type)\n\t\t\t\t\t\tassert.Equal(t, err.Detail, updch.Error.Detail)\n\t\t\t\t\t\tassert.Equal(t, err.Status, updch.Error.Status)\n\t\t\t\t\t\tassert.Equal(t, err.Subproblems, updch.Error.Subproblems)\n\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tsrv: srv,\n\t\t\t\tjwk: jwk,\n\t\t\t}\n\t\t},\n\t\t\"fail/no-names-store-error\": func(t *testing.T) test {\n\t\t\tch := makeTLSCh()\n\n\t\t\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\trequire.NoError(t, err)\n\n\t\t\texpKeyAuth, err := KeyAuthorization(ch.Token, jwk)\n\t\t\trequire.NoError(t, err)\n\t\t\texpKeyAuthHash := sha256.Sum256([]byte(expKeyAuth))\n\n\t\t\tcert, err := newTLSALPNValidationCert(expKeyAuthHash[:], false, true)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tsrv, tlsDial := newTestTLSALPNServer(cert)\n\t\t\tsrv.Start()\n\n\t\t\treturn test{\n\t\t\t\tch: ch,\n\t\t\t\tvc: &mockClient{\n\t\t\t\t\ttlsDial: tlsDial,\n\t\t\t\t},\n\t\t\t\tdb: &MockDB{\n\t\t\t\t\tMockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {\n\t\t\t\t\t\tassert.Equal(t, \"chID\", updch.ID)\n\t\t\t\t\t\tassert.Equal(t, \"token\", updch.Token)\n\t\t\t\t\t\tassert.Equal(t, StatusInvalid, updch.Status)\n\t\t\t\t\t\tassert.Equal(t, ChallengeType(\"tls-alpn-01\"), updch.Type)\n\t\t\t\t\t\tassert.Equal(t, \"zap.internal\", updch.Value)\n\n\t\t\t\t\t\terr := NewError(ErrorRejectedIdentifierType, \"incorrect certificate for tls-alpn-01 challenge: leaf certificate must contain a single IP address or DNS name, %v\", ch.Value)\n\n\t\t\t\t\t\tassert.EqualError(t, updch.Error.Err, err.Err.Error())\n\t\t\t\t\t\tassert.Equal(t, err.Type, updch.Error.Type)\n\t\t\t\t\t\tassert.Equal(t, err.Detail, updch.Error.Detail)\n\t\t\t\t\t\tassert.Equal(t, err.Status, updch.Error.Status)\n\t\t\t\t\t\tassert.Equal(t, err.Subproblems, updch.Error.Subproblems)\n\n\t\t\t\t\t\treturn errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tsrv: srv,\n\t\t\t\tjwk: jwk,\n\t\t\t\terr: NewErrorISE(\"failure saving error to acme challenge: force\"),\n\t\t\t}\n\t\t},\n\t\t\"ok/too-many-names-error\": func(t *testing.T) test {\n\t\t\tch := makeTLSCh()\n\n\t\t\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\trequire.NoError(t, err)\n\n\t\t\texpKeyAuth, err := KeyAuthorization(ch.Token, jwk)\n\t\t\trequire.NoError(t, err)\n\t\t\texpKeyAuthHash := sha256.Sum256([]byte(expKeyAuth))\n\n\t\t\tcert, err := newTLSALPNValidationCert(expKeyAuthHash[:], false, true, ch.Value, \"other.internal\")\n\t\t\trequire.NoError(t, err)\n\n\t\t\tsrv, tlsDial := newTestTLSALPNServer(cert)\n\t\t\tsrv.Start()\n\n\t\t\treturn test{\n\t\t\t\tch: ch,\n\t\t\t\tvc: &mockClient{\n\t\t\t\t\ttlsDial: tlsDial,\n\t\t\t\t},\n\t\t\t\tdb: &MockDB{\n\t\t\t\t\tMockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {\n\t\t\t\t\t\tassert.Equal(t, \"chID\", updch.ID)\n\t\t\t\t\t\tassert.Equal(t, \"token\", updch.Token)\n\t\t\t\t\t\tassert.Equal(t, StatusInvalid, updch.Status)\n\t\t\t\t\t\tassert.Equal(t, ChallengeType(\"tls-alpn-01\"), updch.Type)\n\t\t\t\t\t\tassert.Equal(t, \"zap.internal\", updch.Value)\n\n\t\t\t\t\t\terr := NewError(ErrorRejectedIdentifierType, \"incorrect certificate for tls-alpn-01 challenge: leaf certificate must contain a single IP address or DNS name, %v\", ch.Value)\n\n\t\t\t\t\t\tassert.EqualError(t, updch.Error.Err, err.Err.Error())\n\t\t\t\t\t\tassert.Equal(t, err.Type, updch.Error.Type)\n\t\t\t\t\t\tassert.Equal(t, err.Detail, updch.Error.Detail)\n\t\t\t\t\t\tassert.Equal(t, err.Status, updch.Error.Status)\n\t\t\t\t\t\tassert.Equal(t, err.Subproblems, updch.Error.Subproblems)\n\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tsrv: srv,\n\t\t\t\tjwk: jwk,\n\t\t\t}\n\t\t},\n\t\t\"ok/wrong-name\": func(t *testing.T) test {\n\t\t\tch := makeTLSCh()\n\n\t\t\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\trequire.NoError(t, err)\n\n\t\t\texpKeyAuth, err := KeyAuthorization(ch.Token, jwk)\n\t\t\trequire.NoError(t, err)\n\t\t\texpKeyAuthHash := sha256.Sum256([]byte(expKeyAuth))\n\n\t\t\tcert, err := newTLSALPNValidationCert(expKeyAuthHash[:], false, true, \"other.internal\")\n\t\t\trequire.NoError(t, err)\n\n\t\t\tsrv, tlsDial := newTestTLSALPNServer(cert)\n\t\t\tsrv.Start()\n\n\t\t\treturn test{\n\t\t\t\tch: ch,\n\t\t\t\tvc: &mockClient{\n\t\t\t\t\ttlsDial: tlsDial,\n\t\t\t\t},\n\t\t\t\tdb: &MockDB{\n\t\t\t\t\tMockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {\n\t\t\t\t\t\tassert.Equal(t, \"chID\", updch.ID)\n\t\t\t\t\t\tassert.Equal(t, \"token\", updch.Token)\n\t\t\t\t\t\tassert.Equal(t, StatusInvalid, updch.Status)\n\t\t\t\t\t\tassert.Equal(t, ChallengeType(\"tls-alpn-01\"), updch.Type)\n\t\t\t\t\t\tassert.Equal(t, \"zap.internal\", updch.Value)\n\n\t\t\t\t\t\terr := NewError(ErrorRejectedIdentifierType, \"incorrect certificate for tls-alpn-01 challenge: leaf certificate must contain a single IP address or DNS name, %v\", ch.Value)\n\n\t\t\t\t\t\tassert.EqualError(t, updch.Error.Err, err.Err.Error())\n\t\t\t\t\t\tassert.Equal(t, err.Type, updch.Error.Type)\n\t\t\t\t\t\tassert.Equal(t, err.Detail, updch.Error.Detail)\n\t\t\t\t\t\tassert.Equal(t, err.Status, updch.Error.Status)\n\t\t\t\t\t\tassert.Equal(t, err.Subproblems, updch.Error.Subproblems)\n\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tsrv: srv,\n\t\t\t\tjwk: jwk,\n\t\t\t}\n\t\t},\n\t\t\"fail/key-auth-gen-error\": func(t *testing.T) test {\n\t\t\tch := makeTLSCh()\n\n\t\t\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\trequire.NoError(t, err)\n\n\t\t\texpKeyAuth, err := KeyAuthorization(ch.Token, jwk)\n\t\t\trequire.NoError(t, err)\n\t\t\texpKeyAuthHash := sha256.Sum256([]byte(expKeyAuth))\n\t\t\tjwk.Key = \"foo\"\n\n\t\t\tcert, err := newTLSALPNValidationCert(expKeyAuthHash[:], false, true, ch.Value)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tsrv, tlsDial := newTestTLSALPNServer(cert)\n\t\t\tsrv.Start()\n\n\t\t\treturn test{\n\t\t\t\tch: ch,\n\t\t\t\tvc: &mockClient{\n\t\t\t\t\ttlsDial: tlsDial,\n\t\t\t\t},\n\t\t\t\tsrv: srv,\n\t\t\t\tjwk: jwk,\n\t\t\t\terr: NewErrorISE(\"error generating JWK thumbprint: go-jose/go-jose: unknown key type 'string'\"),\n\t\t\t}\n\t\t},\n\t\t\"ok/error-no-extension\": func(t *testing.T) test {\n\t\t\tch := makeTLSCh()\n\n\t\t\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tcert, err := newTLSALPNValidationCert(nil, false, true, ch.Value)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tsrv, tlsDial := newTestTLSALPNServer(cert)\n\t\t\tsrv.Start()\n\n\t\t\treturn test{\n\t\t\t\tch: ch,\n\t\t\t\tvc: &mockClient{\n\t\t\t\t\ttlsDial: tlsDial,\n\t\t\t\t},\n\t\t\t\tdb: &MockDB{\n\t\t\t\t\tMockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {\n\t\t\t\t\t\tassert.Equal(t, \"chID\", updch.ID)\n\t\t\t\t\t\tassert.Equal(t, \"token\", updch.Token)\n\t\t\t\t\t\tassert.Equal(t, StatusInvalid, updch.Status)\n\t\t\t\t\t\tassert.Equal(t, ChallengeType(\"tls-alpn-01\"), updch.Type)\n\t\t\t\t\t\tassert.Equal(t, \"zap.internal\", updch.Value)\n\n\t\t\t\t\t\terr := NewError(ErrorRejectedIdentifierType, \"incorrect certificate for tls-alpn-01 challenge: missing acmeValidationV1 extension\")\n\n\t\t\t\t\t\tassert.EqualError(t, updch.Error.Err, err.Err.Error())\n\t\t\t\t\t\tassert.Equal(t, err.Type, updch.Error.Type)\n\t\t\t\t\t\tassert.Equal(t, err.Detail, updch.Error.Detail)\n\t\t\t\t\t\tassert.Equal(t, err.Status, updch.Error.Status)\n\t\t\t\t\t\tassert.Equal(t, err.Subproblems, updch.Error.Subproblems)\n\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tsrv: srv,\n\t\t\t\tjwk: jwk,\n\t\t\t}\n\t\t},\n\t\t\"fail/no-extension-store-error\": func(t *testing.T) test {\n\t\t\tch := makeTLSCh()\n\n\t\t\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tcert, err := newTLSALPNValidationCert(nil, false, true, ch.Value)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tsrv, tlsDial := newTestTLSALPNServer(cert)\n\t\t\tsrv.Start()\n\n\t\t\treturn test{\n\t\t\t\tch: ch,\n\t\t\t\tvc: &mockClient{\n\t\t\t\t\ttlsDial: tlsDial,\n\t\t\t\t},\n\t\t\t\tdb: &MockDB{\n\t\t\t\t\tMockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {\n\t\t\t\t\t\tassert.Equal(t, \"chID\", updch.ID)\n\t\t\t\t\t\tassert.Equal(t, \"token\", updch.Token)\n\t\t\t\t\t\tassert.Equal(t, StatusInvalid, updch.Status)\n\t\t\t\t\t\tassert.Equal(t, ChallengeType(\"tls-alpn-01\"), updch.Type)\n\t\t\t\t\t\tassert.Equal(t, \"zap.internal\", updch.Value)\n\n\t\t\t\t\t\terr := NewError(ErrorRejectedIdentifierType, \"incorrect certificate for tls-alpn-01 challenge: missing acmeValidationV1 extension\")\n\n\t\t\t\t\t\tassert.EqualError(t, updch.Error.Err, err.Err.Error())\n\t\t\t\t\t\tassert.Equal(t, err.Type, updch.Error.Type)\n\t\t\t\t\t\tassert.Equal(t, err.Detail, updch.Error.Detail)\n\t\t\t\t\t\tassert.Equal(t, err.Status, updch.Error.Status)\n\t\t\t\t\t\tassert.Equal(t, err.Subproblems, updch.Error.Subproblems)\n\n\t\t\t\t\t\treturn errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tsrv: srv,\n\t\t\t\tjwk: jwk,\n\t\t\t\terr: NewErrorISE(\"failure saving error to acme challenge: force\"),\n\t\t\t}\n\t\t},\n\t\t\"ok/error-extension-not-critical\": func(t *testing.T) test {\n\t\t\tch := makeTLSCh()\n\n\t\t\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\trequire.NoError(t, err)\n\n\t\t\texpKeyAuth, err := KeyAuthorization(ch.Token, jwk)\n\t\t\trequire.NoError(t, err)\n\t\t\texpKeyAuthHash := sha256.Sum256([]byte(expKeyAuth))\n\n\t\t\tcert, err := newTLSALPNValidationCert(expKeyAuthHash[:], false, false, ch.Value)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tsrv, tlsDial := newTestTLSALPNServer(cert)\n\t\t\tsrv.Start()\n\n\t\t\treturn test{\n\t\t\t\tch: ch,\n\t\t\t\tvc: &mockClient{\n\t\t\t\t\ttlsDial: tlsDial,\n\t\t\t\t},\n\t\t\t\tdb: &MockDB{\n\t\t\t\t\tMockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {\n\t\t\t\t\t\tassert.Equal(t, \"chID\", updch.ID)\n\t\t\t\t\t\tassert.Equal(t, \"token\", updch.Token)\n\t\t\t\t\t\tassert.Equal(t, StatusInvalid, updch.Status)\n\t\t\t\t\t\tassert.Equal(t, ChallengeType(\"tls-alpn-01\"), updch.Type)\n\t\t\t\t\t\tassert.Equal(t, \"zap.internal\", updch.Value)\n\n\t\t\t\t\t\terr := NewError(ErrorRejectedIdentifierType, \"incorrect certificate for tls-alpn-01 challenge: acmeValidationV1 extension not critical\")\n\n\t\t\t\t\t\tassert.EqualError(t, updch.Error.Err, err.Err.Error())\n\t\t\t\t\t\tassert.Equal(t, err.Type, updch.Error.Type)\n\t\t\t\t\t\tassert.Equal(t, err.Detail, updch.Error.Detail)\n\t\t\t\t\t\tassert.Equal(t, err.Status, updch.Error.Status)\n\t\t\t\t\t\tassert.Equal(t, err.Subproblems, updch.Error.Subproblems)\n\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tsrv: srv,\n\t\t\t\tjwk: jwk,\n\t\t\t}\n\t\t},\n\t\t\"fail/extension-not-critical-store-error\": func(t *testing.T) test {\n\t\t\tch := makeTLSCh()\n\n\t\t\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\trequire.NoError(t, err)\n\n\t\t\texpKeyAuth, err := KeyAuthorization(ch.Token, jwk)\n\t\t\trequire.NoError(t, err)\n\t\t\texpKeyAuthHash := sha256.Sum256([]byte(expKeyAuth))\n\n\t\t\tcert, err := newTLSALPNValidationCert(expKeyAuthHash[:], false, false, ch.Value)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tsrv, tlsDial := newTestTLSALPNServer(cert)\n\t\t\tsrv.Start()\n\n\t\t\treturn test{\n\t\t\t\tch: ch,\n\t\t\t\tvc: &mockClient{\n\t\t\t\t\ttlsDial: tlsDial,\n\t\t\t\t},\n\t\t\t\tdb: &MockDB{\n\t\t\t\t\tMockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {\n\t\t\t\t\t\tassert.Equal(t, \"chID\", updch.ID)\n\t\t\t\t\t\tassert.Equal(t, \"token\", updch.Token)\n\t\t\t\t\t\tassert.Equal(t, StatusInvalid, updch.Status)\n\t\t\t\t\t\tassert.Equal(t, ChallengeType(\"tls-alpn-01\"), updch.Type)\n\t\t\t\t\t\tassert.Equal(t, \"zap.internal\", updch.Value)\n\n\t\t\t\t\t\terr := NewError(ErrorRejectedIdentifierType, \"incorrect certificate for tls-alpn-01 challenge: acmeValidationV1 extension not critical\")\n\n\t\t\t\t\t\tassert.EqualError(t, updch.Error.Err, err.Err.Error())\n\t\t\t\t\t\tassert.Equal(t, err.Type, updch.Error.Type)\n\t\t\t\t\t\tassert.Equal(t, err.Detail, updch.Error.Detail)\n\t\t\t\t\t\tassert.Equal(t, err.Status, updch.Error.Status)\n\t\t\t\t\t\tassert.Equal(t, err.Subproblems, updch.Error.Subproblems)\n\n\t\t\t\t\t\treturn errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tsrv: srv,\n\t\t\t\tjwk: jwk,\n\t\t\t\terr: NewErrorISE(\"failure saving error to acme challenge: force\"),\n\t\t\t}\n\t\t},\n\t\t\"ok/error-malformed-extension\": func(t *testing.T) test {\n\t\t\tch := makeTLSCh()\n\n\t\t\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tcert, err := newTLSALPNValidationCert([]byte{1, 2, 3}, false, true, ch.Value)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tsrv, tlsDial := newTestTLSALPNServer(cert)\n\t\t\tsrv.Start()\n\n\t\t\treturn test{\n\t\t\t\tch: ch,\n\t\t\t\tvc: &mockClient{\n\t\t\t\t\ttlsDial: tlsDial,\n\t\t\t\t},\n\t\t\t\tdb: &MockDB{\n\t\t\t\t\tMockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {\n\t\t\t\t\t\tassert.Equal(t, \"chID\", updch.ID)\n\t\t\t\t\t\tassert.Equal(t, \"token\", updch.Token)\n\t\t\t\t\t\tassert.Equal(t, StatusInvalid, updch.Status)\n\t\t\t\t\t\tassert.Equal(t, ChallengeType(\"tls-alpn-01\"), updch.Type)\n\t\t\t\t\t\tassert.Equal(t, \"zap.internal\", updch.Value)\n\n\t\t\t\t\t\terr := NewError(ErrorRejectedIdentifierType, \"incorrect certificate for tls-alpn-01 challenge: malformed acmeValidationV1 extension value\")\n\n\t\t\t\t\t\tassert.EqualError(t, updch.Error.Err, err.Err.Error())\n\t\t\t\t\t\tassert.Equal(t, err.Type, updch.Error.Type)\n\t\t\t\t\t\tassert.Equal(t, err.Detail, updch.Error.Detail)\n\t\t\t\t\t\tassert.Equal(t, err.Status, updch.Error.Status)\n\t\t\t\t\t\tassert.Equal(t, err.Subproblems, updch.Error.Subproblems)\n\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tsrv: srv,\n\t\t\t\tjwk: jwk,\n\t\t\t}\n\t\t},\n\t\t\"fail/malformed-extension-store-error\": func(t *testing.T) test {\n\t\t\tch := makeTLSCh()\n\n\t\t\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tcert, err := newTLSALPNValidationCert([]byte{1, 2, 3}, false, true, ch.Value)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tsrv, tlsDial := newTestTLSALPNServer(cert)\n\t\t\tsrv.Start()\n\n\t\t\treturn test{\n\t\t\t\tch: ch,\n\t\t\t\tvc: &mockClient{\n\t\t\t\t\ttlsDial: tlsDial,\n\t\t\t\t},\n\t\t\t\tdb: &MockDB{\n\t\t\t\t\tMockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {\n\t\t\t\t\t\tassert.Equal(t, \"chID\", updch.ID)\n\t\t\t\t\t\tassert.Equal(t, \"token\", updch.Token)\n\t\t\t\t\t\tassert.Equal(t, StatusInvalid, updch.Status)\n\t\t\t\t\t\tassert.Equal(t, ChallengeType(\"tls-alpn-01\"), updch.Type)\n\t\t\t\t\t\tassert.Equal(t, \"zap.internal\", updch.Value)\n\n\t\t\t\t\t\terr := NewError(ErrorRejectedIdentifierType, \"incorrect certificate for tls-alpn-01 challenge: malformed acmeValidationV1 extension value\")\n\n\t\t\t\t\t\tassert.EqualError(t, updch.Error.Err, err.Err.Error())\n\t\t\t\t\t\tassert.Equal(t, err.Type, updch.Error.Type)\n\t\t\t\t\t\tassert.Equal(t, err.Detail, updch.Error.Detail)\n\t\t\t\t\t\tassert.Equal(t, err.Status, updch.Error.Status)\n\t\t\t\t\t\tassert.Equal(t, err.Subproblems, updch.Error.Subproblems)\n\n\t\t\t\t\t\treturn errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tsrv: srv,\n\t\t\t\tjwk: jwk,\n\t\t\t\terr: NewErrorISE(\"failure saving error to acme challenge: force\"),\n\t\t\t}\n\t\t},\n\t\t\"ok/error-keyauth-mismatch\": func(t *testing.T) test {\n\t\t\tch := makeTLSCh()\n\n\t\t\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\trequire.NoError(t, err)\n\n\t\t\texpKeyAuth, err := KeyAuthorization(ch.Token, jwk)\n\t\t\trequire.NoError(t, err)\n\t\t\texpKeyAuthHash := sha256.Sum256([]byte(expKeyAuth))\n\t\t\tincorrectTokenHash := sha256.Sum256([]byte(\"mismatched\"))\n\n\t\t\tcert, err := newTLSALPNValidationCert(incorrectTokenHash[:], false, true, ch.Value)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tsrv, tlsDial := newTestTLSALPNServer(cert)\n\t\t\tsrv.Start()\n\n\t\t\treturn test{\n\t\t\t\tch: ch,\n\t\t\t\tvc: &mockClient{\n\t\t\t\t\ttlsDial: tlsDial,\n\t\t\t\t},\n\t\t\t\tdb: &MockDB{\n\t\t\t\t\tMockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {\n\t\t\t\t\t\tassert.Equal(t, \"chID\", updch.ID)\n\t\t\t\t\t\tassert.Equal(t, \"token\", updch.Token)\n\t\t\t\t\t\tassert.Equal(t, StatusInvalid, updch.Status)\n\t\t\t\t\t\tassert.Equal(t, ChallengeType(\"tls-alpn-01\"), updch.Type)\n\t\t\t\t\t\tassert.Equal(t, \"zap.internal\", updch.Value)\n\n\t\t\t\t\t\terr := NewError(ErrorRejectedIdentifierType, \"incorrect certificate for tls-alpn-01 challenge: \"+\n\t\t\t\t\t\t\t\"expected acmeValidationV1 extension value %s for this challenge but got %s\",\n\t\t\t\t\t\t\thex.EncodeToString(expKeyAuthHash[:]), hex.EncodeToString(incorrectTokenHash[:]))\n\n\t\t\t\t\t\tassert.EqualError(t, updch.Error.Err, err.Err.Error())\n\t\t\t\t\t\tassert.Equal(t, err.Type, updch.Error.Type)\n\t\t\t\t\t\tassert.Equal(t, err.Detail, updch.Error.Detail)\n\t\t\t\t\t\tassert.Equal(t, err.Status, updch.Error.Status)\n\t\t\t\t\t\tassert.Equal(t, err.Subproblems, updch.Error.Subproblems)\n\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tsrv: srv,\n\t\t\t\tjwk: jwk,\n\t\t\t}\n\t\t},\n\t\t\"fail/keyauth-mismatch-store-error\": func(t *testing.T) test {\n\t\t\tch := makeTLSCh()\n\n\t\t\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\trequire.NoError(t, err)\n\n\t\t\texpKeyAuth, err := KeyAuthorization(ch.Token, jwk)\n\t\t\trequire.NoError(t, err)\n\t\t\texpKeyAuthHash := sha256.Sum256([]byte(expKeyAuth))\n\t\t\tincorrectTokenHash := sha256.Sum256([]byte(\"mismatched\"))\n\n\t\t\tcert, err := newTLSALPNValidationCert(incorrectTokenHash[:], false, true, ch.Value)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tsrv, tlsDial := newTestTLSALPNServer(cert)\n\t\t\tsrv.Start()\n\n\t\t\treturn test{\n\t\t\t\tch: ch,\n\t\t\t\tvc: &mockClient{\n\t\t\t\t\ttlsDial: tlsDial,\n\t\t\t\t},\n\t\t\t\tdb: &MockDB{\n\t\t\t\t\tMockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {\n\t\t\t\t\t\tassert.Equal(t, \"chID\", updch.ID)\n\t\t\t\t\t\tassert.Equal(t, \"token\", updch.Token)\n\t\t\t\t\t\tassert.Equal(t, StatusInvalid, updch.Status)\n\t\t\t\t\t\tassert.Equal(t, ChallengeType(\"tls-alpn-01\"), updch.Type)\n\t\t\t\t\t\tassert.Equal(t, \"zap.internal\", updch.Value)\n\n\t\t\t\t\t\terr := NewError(ErrorRejectedIdentifierType, \"incorrect certificate for tls-alpn-01 challenge: \"+\n\t\t\t\t\t\t\t\"expected acmeValidationV1 extension value %s for this challenge but got %s\",\n\t\t\t\t\t\t\thex.EncodeToString(expKeyAuthHash[:]), hex.EncodeToString(incorrectTokenHash[:]))\n\n\t\t\t\t\t\tassert.EqualError(t, updch.Error.Err, err.Err.Error())\n\t\t\t\t\t\tassert.Equal(t, err.Type, updch.Error.Type)\n\t\t\t\t\t\tassert.Equal(t, err.Detail, updch.Error.Detail)\n\t\t\t\t\t\tassert.Equal(t, err.Status, updch.Error.Status)\n\t\t\t\t\t\tassert.Equal(t, err.Subproblems, updch.Error.Subproblems)\n\n\t\t\t\t\t\treturn errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tsrv: srv,\n\t\t\t\tjwk: jwk,\n\t\t\t\terr: NewErrorISE(\"failure saving error to acme challenge: force\"),\n\t\t\t}\n\t\t},\n\t\t\"ok/error-obsolete-oid\": func(t *testing.T) test {\n\t\t\tch := makeTLSCh()\n\n\t\t\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\trequire.NoError(t, err)\n\n\t\t\texpKeyAuth, err := KeyAuthorization(ch.Token, jwk)\n\t\t\trequire.NoError(t, err)\n\t\t\texpKeyAuthHash := sha256.Sum256([]byte(expKeyAuth))\n\n\t\t\tcert, err := newTLSALPNValidationCert(expKeyAuthHash[:], true, true, ch.Value)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tsrv, tlsDial := newTestTLSALPNServer(cert)\n\t\t\tsrv.Start()\n\n\t\t\treturn test{\n\t\t\t\tch: ch,\n\t\t\t\tvc: &mockClient{\n\t\t\t\t\ttlsDial: tlsDial,\n\t\t\t\t},\n\t\t\t\tdb: &MockDB{\n\t\t\t\t\tMockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {\n\t\t\t\t\t\tassert.Equal(t, \"chID\", updch.ID)\n\t\t\t\t\t\tassert.Equal(t, \"token\", updch.Token)\n\t\t\t\t\t\tassert.Equal(t, StatusInvalid, updch.Status)\n\t\t\t\t\t\tassert.Equal(t, ChallengeType(\"tls-alpn-01\"), updch.Type)\n\t\t\t\t\t\tassert.Equal(t, \"zap.internal\", updch.Value)\n\n\t\t\t\t\t\terr := NewError(ErrorRejectedIdentifierType, \"incorrect certificate for tls-alpn-01 challenge: \"+\n\t\t\t\t\t\t\t\"obsolete id-pe-acmeIdentifier in acmeValidationV1 extension\")\n\n\t\t\t\t\t\tassert.EqualError(t, updch.Error.Err, err.Err.Error())\n\t\t\t\t\t\tassert.Equal(t, err.Type, updch.Error.Type)\n\t\t\t\t\t\tassert.Equal(t, err.Detail, updch.Error.Detail)\n\t\t\t\t\t\tassert.Equal(t, err.Status, updch.Error.Status)\n\t\t\t\t\t\tassert.Equal(t, err.Subproblems, updch.Error.Subproblems)\n\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tsrv: srv,\n\t\t\t\tjwk: jwk,\n\t\t\t}\n\t\t},\n\t\t\"fail/obsolete-oid-store-error\": func(t *testing.T) test {\n\t\t\tch := makeTLSCh()\n\n\t\t\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\trequire.NoError(t, err)\n\n\t\t\texpKeyAuth, err := KeyAuthorization(ch.Token, jwk)\n\t\t\trequire.NoError(t, err)\n\t\t\texpKeyAuthHash := sha256.Sum256([]byte(expKeyAuth))\n\n\t\t\tcert, err := newTLSALPNValidationCert(expKeyAuthHash[:], true, true, ch.Value)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tsrv, tlsDial := newTestTLSALPNServer(cert)\n\t\t\tsrv.Start()\n\n\t\t\treturn test{\n\t\t\t\tch: ch,\n\t\t\t\tvc: &mockClient{\n\t\t\t\t\ttlsDial: tlsDial,\n\t\t\t\t},\n\t\t\t\tdb: &MockDB{\n\t\t\t\t\tMockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {\n\t\t\t\t\t\tassert.Equal(t, \"chID\", updch.ID)\n\t\t\t\t\t\tassert.Equal(t, \"token\", updch.Token)\n\t\t\t\t\t\tassert.Equal(t, StatusInvalid, updch.Status)\n\t\t\t\t\t\tassert.Equal(t, ChallengeType(\"tls-alpn-01\"), updch.Type)\n\t\t\t\t\t\tassert.Equal(t, \"zap.internal\", updch.Value)\n\n\t\t\t\t\t\terr := NewError(ErrorRejectedIdentifierType, \"incorrect certificate for tls-alpn-01 challenge: \"+\n\t\t\t\t\t\t\t\"obsolete id-pe-acmeIdentifier in acmeValidationV1 extension\")\n\n\t\t\t\t\t\tassert.EqualError(t, updch.Error.Err, err.Err.Error())\n\t\t\t\t\t\tassert.Equal(t, err.Type, updch.Error.Type)\n\t\t\t\t\t\tassert.Equal(t, err.Detail, updch.Error.Detail)\n\t\t\t\t\t\tassert.Equal(t, err.Status, updch.Error.Status)\n\t\t\t\t\t\tassert.Equal(t, err.Subproblems, updch.Error.Subproblems)\n\n\t\t\t\t\t\treturn errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tsrv: srv,\n\t\t\t\tjwk: jwk,\n\t\t\t\terr: NewErrorISE(\"failure saving error to acme challenge: force\"),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tch := makeTLSCh()\n\n\t\t\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\trequire.NoError(t, err)\n\n\t\t\texpKeyAuth, err := KeyAuthorization(ch.Token, jwk)\n\t\t\trequire.NoError(t, err)\n\t\t\texpKeyAuthHash := sha256.Sum256([]byte(expKeyAuth))\n\n\t\t\tcert, err := newTLSALPNValidationCert(expKeyAuthHash[:], false, true, ch.Value)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tsrv, tlsDial := newTestTLSALPNServer(cert)\n\t\t\tsrv.Start()\n\n\t\t\treturn test{\n\t\t\t\tch: ch,\n\t\t\t\tvc: &mockClient{\n\t\t\t\t\ttlsDial: tlsDial,\n\t\t\t\t},\n\t\t\t\tdb: &MockDB{\n\t\t\t\t\tMockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {\n\t\t\t\t\t\tassert.Equal(t, \"chID\", updch.ID)\n\t\t\t\t\t\tassert.Equal(t, \"token\", updch.Token)\n\t\t\t\t\t\tassert.Equal(t, StatusValid, updch.Status)\n\t\t\t\t\t\tassert.Equal(t, ChallengeType(\"tls-alpn-01\"), updch.Type)\n\t\t\t\t\t\tassert.Equal(t, \"zap.internal\", updch.Value)\n\t\t\t\t\t\tassert.Nil(t, updch.Error)\n\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tsrv: srv,\n\t\t\t\tjwk: jwk,\n\t\t\t}\n\t\t},\n\t\t\"ok/ip\": func(t *testing.T) test {\n\t\t\tch := makeTLSCh()\n\t\t\tch.Value = \"127.0.0.1\"\n\n\t\t\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\trequire.NoError(t, err)\n\n\t\t\texpKeyAuth, err := KeyAuthorization(ch.Token, jwk)\n\t\t\trequire.NoError(t, err)\n\t\t\texpKeyAuthHash := sha256.Sum256([]byte(expKeyAuth))\n\n\t\t\tcert, err := newTLSALPNValidationCert(expKeyAuthHash[:], false, true, ch.Value)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tsrv, tlsDial := newTestTLSALPNServer(cert)\n\t\t\tsrv.Start()\n\n\t\t\treturn test{\n\t\t\t\tch: ch,\n\t\t\t\tvc: &mockClient{\n\t\t\t\t\ttlsDial: tlsDial,\n\t\t\t\t},\n\t\t\t\tdb: &MockDB{\n\t\t\t\t\tMockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {\n\t\t\t\t\t\tassert.Equal(t, \"chID\", updch.ID)\n\t\t\t\t\t\tassert.Equal(t, \"token\", updch.Token)\n\t\t\t\t\t\tassert.Equal(t, StatusValid, updch.Status)\n\t\t\t\t\t\tassert.Equal(t, ChallengeType(\"tls-alpn-01\"), updch.Type)\n\t\t\t\t\t\tassert.Equal(t, \"127.0.0.1\", updch.Value)\n\t\t\t\t\t\tassert.Nil(t, updch.Error)\n\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tsrv: srv,\n\t\t\t\tjwk: jwk,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ttc := run(t)\n\n\t\t\tif tc.srv != nil {\n\t\t\t\tdefer tc.srv.Close()\n\t\t\t}\n\n\t\t\tctx := NewClientContext(context.Background(), tc.vc)\n\t\t\tif err := tlsalpn01Validate(ctx, tc.ch, tc.db, tc.jwk); err != nil {\n\t\t\t\tif assert.Error(t, tc.err) {\n\t\t\t\t\tvar k *Error\n\t\t\t\t\tif errors.As(err, &k) {\n\t\t\t\t\t\tassert.Equal(t, tc.err.Type, k.Type)\n\t\t\t\t\t\tassert.Equal(t, tc.err.Detail, k.Detail)\n\t\t\t\t\t\tassert.Equal(t, tc.err.Status, k.Status)\n\t\t\t\t\t\tassert.Equal(t, tc.err.Err.Error(), k.Err.Error())\n\t\t\t\t\t\tassert.Equal(t, tc.err.Subproblems, k.Subproblems)\n\t\t\t\t\t} else {\n\t\t\t\t\t\tassert.Fail(t, \"unexpected error type\")\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tassert.Nil(t, tc.err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_reverseAddr(t *testing.T) {\n\ttype args struct {\n\t\tip net.IP\n\t}\n\ttests := []struct {\n\t\tname     string\n\t\targs     args\n\t\twantArpa string\n\t}{\n\t\t{\n\t\t\tname: \"ok/ipv4\",\n\t\t\targs: args{\n\t\t\t\tip: net.ParseIP(\"127.0.0.1\"),\n\t\t\t},\n\t\t\twantArpa: \"1.0.0.127.in-addr.arpa.\",\n\t\t},\n\t\t{\n\t\t\tname: \"ok/ipv6\",\n\t\t\targs: args{\n\t\t\t\tip: net.ParseIP(\"2001:db8::567:89ab\"),\n\t\t\t},\n\t\t\twantArpa: \"b.a.9.8.7.6.5.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa.\",\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif gotArpa := reverseAddr(tt.args.ip); gotArpa != tt.wantArpa {\n\t\t\t\tt.Errorf(\"reverseAddr() = %v, want %v\", gotArpa, tt.wantArpa)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_serverName(t *testing.T) {\n\ttype args struct {\n\t\tch *Challenge\n\t}\n\ttests := []struct {\n\t\tname string\n\t\targs args\n\t\twant string\n\t}{\n\t\t{\n\t\t\tname: \"ok/dns\",\n\t\t\targs: args{\n\t\t\t\tch: &Challenge{\n\t\t\t\t\tValue: \"example.com\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: \"example.com\",\n\t\t},\n\t\t{\n\t\t\tname: \"ok/ipv4\",\n\t\t\targs: args{\n\t\t\t\tch: &Challenge{\n\t\t\t\t\tValue: \"127.0.0.1\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: \"1.0.0.127.in-addr.arpa.\",\n\t\t},\n\t\t{\n\t\t\tname: \"ok/ipv6\",\n\t\t\targs: args{\n\t\t\t\tch: &Challenge{\n\t\t\t\t\tValue: \"2001:db8::567:89ab\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: \"b.a.9.8.7.6.5.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa.\",\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif got := serverName(tt.args.ch); got != tt.want {\n\t\t\t\tt.Errorf(\"serverName() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_http01ChallengeHost(t *testing.T) {\n\ttests := []struct {\n\t\tname       string\n\t\tstrictFQDN bool\n\t\tvalue      string\n\t\twant       string\n\t}{\n\t\t{\n\t\t\tname:       \"dns\",\n\t\t\tstrictFQDN: false,\n\t\t\tvalue:      \"www.example.com\",\n\t\t\twant:       \"www.example.com\",\n\t\t},\n\t\t{\n\t\t\tname:       \"dns strict\",\n\t\t\tstrictFQDN: true,\n\t\t\tvalue:      \"www.example.com\",\n\t\t\twant:       \"www.example.com.\",\n\t\t},\n\t\t{\n\t\t\tname:       \"rooted dns\",\n\t\t\tstrictFQDN: false,\n\t\t\tvalue:      \"www.example.com.\",\n\t\t\twant:       \"www.example.com.\",\n\t\t},\n\t\t{\n\t\t\tname:       \"rooted dns strict\",\n\t\t\tstrictFQDN: true,\n\t\t\tvalue:      \"www.example.com.\",\n\t\t\twant:       \"www.example.com.\",\n\t\t},\n\t\t{\n\t\t\tname:  \"ipv4\",\n\t\t\tvalue: \"127.0.0.1\",\n\t\t\twant:  \"127.0.0.1\",\n\t\t},\n\t\t{\n\t\t\tname:  \"ipv6\",\n\t\t\tvalue: \"::1\",\n\t\t\twant:  \"[::1]\",\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ttmp := StrictFQDN\n\t\t\tt.Cleanup(func() {\n\t\t\t\tStrictFQDN = tmp\n\t\t\t})\n\t\t\tStrictFQDN = tt.strictFQDN\n\t\t\tif got := http01ChallengeHost(tt.value); got != tt.want {\n\t\t\t\tt.Errorf(\"http01ChallengeHost() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_doAppleAttestationFormat(t *testing.T) {\n\tctx := context.Background()\n\tca, err := minica.New()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tcaRoot := pem.EncodeToMemory(&pem.Block{Type: \"CERTIFICATE\", Bytes: ca.Root.Raw})\n\tsigner, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tleaf, err := ca.Sign(&x509.Certificate{\n\t\tSubject:   pkix.Name{CommonName: \"attestation cert\"},\n\t\tPublicKey: signer.Public(),\n\t\tExtraExtensions: []pkix.Extension{\n\t\t\t{Id: oidAppleSerialNumber, Value: []byte(\"serial-number\")},\n\t\t\t{Id: oidAppleUniqueDeviceIdentifier, Value: []byte(\"udid\")},\n\t\t\t{Id: oidAppleSecureEnclaveProcessorOSVersion, Value: []byte(\"16.0\")},\n\t\t\t{Id: oidAppleNonce, Value: []byte(\"nonce\")},\n\t\t},\n\t})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tfingerprint, err := keyutil.Fingerprint(signer.Public())\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\ttype args struct {\n\t\tctx  context.Context\n\t\tprov Provisioner\n\t\tch   *Challenge\n\t\tatt  *attestationObject\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\targs    args\n\t\twant    *appleAttestationData\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{}, &attestationObject{\n\t\t\tFormat: \"apple\",\n\t\t\tAttStatement: map[string]interface{}{\n\t\t\t\t\"x5c\": []interface{}{leaf.Raw, ca.Intermediate.Raw},\n\t\t\t},\n\t\t}}, &appleAttestationData{\n\t\t\tNonce:        []byte(\"nonce\"),\n\t\t\tSerialNumber: \"serial-number\",\n\t\t\tUDID:         \"udid\",\n\t\t\tSEPVersion:   \"16.0\",\n\t\t\tCertificate:  leaf,\n\t\t\tFingerprint:  fingerprint,\n\t\t}, false},\n\t\t{\"fail apple issuer\", args{ctx, mustAttestationProvisioner(t, nil), &Challenge{}, &attestationObject{\n\t\t\tFormat: \"apple\",\n\t\t\tAttStatement: map[string]interface{}{\n\t\t\t\t\"x5c\": []interface{}{leaf.Raw, ca.Intermediate.Raw},\n\t\t\t},\n\t\t}}, nil, true},\n\t\t{\"fail missing x5c\", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{}, &attestationObject{\n\t\t\tFormat: \"apple\",\n\t\t\tAttStatement: map[string]interface{}{\n\t\t\t\t\"foo\": \"bar\",\n\t\t\t},\n\t\t}}, nil, true},\n\t\t{\"fail empty issuer\", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{}, &attestationObject{\n\t\t\tFormat: \"apple\",\n\t\t\tAttStatement: map[string]interface{}{\n\t\t\t\t\"x5c\": []interface{}{},\n\t\t\t},\n\t\t}}, nil, true},\n\t\t{\"fail leaf type\", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{}, &attestationObject{\n\t\t\tFormat: \"apple\",\n\t\t\tAttStatement: map[string]interface{}{\n\t\t\t\t\"x5c\": []interface{}{\"leaf\", ca.Intermediate.Raw},\n\t\t\t},\n\t\t}}, nil, true},\n\t\t{\"fail leaf parse\", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{}, &attestationObject{\n\t\t\tFormat: \"apple\",\n\t\t\tAttStatement: map[string]interface{}{\n\t\t\t\t\"x5c\": []interface{}{leaf.Raw[:100], ca.Intermediate.Raw},\n\t\t\t},\n\t\t}}, nil, true},\n\t\t{\"fail intermediate type\", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{}, &attestationObject{\n\t\t\tFormat: \"apple\",\n\t\t\tAttStatement: map[string]interface{}{\n\t\t\t\t\"x5c\": []interface{}{leaf.Raw, \"intermediate\"},\n\t\t\t},\n\t\t}}, nil, true},\n\t\t{\"fail intermediate parse\", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{}, &attestationObject{\n\t\t\tFormat: \"apple\",\n\t\t\tAttStatement: map[string]interface{}{\n\t\t\t\t\"x5c\": []interface{}{leaf.Raw, ca.Intermediate.Raw[:100]},\n\t\t\t},\n\t\t}}, nil, true},\n\t\t{\"fail verify\", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{}, &attestationObject{\n\t\t\tFormat: \"apple\",\n\t\t\tAttStatement: map[string]interface{}{\n\t\t\t\t\"x5c\": []interface{}{leaf.Raw},\n\t\t\t},\n\t\t}}, nil, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := doAppleAttestationFormat(tt.args.ctx, tt.args.prov, tt.args.ch, tt.args.att)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"doAppleAttestationFormat() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"doAppleAttestationFormat() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_doStepAttestationFormat(t *testing.T) {\n\tctx := context.Background()\n\tca, err := minica.New()\n\trequire.NoError(t, err)\n\n\tcaRoot := pem.EncodeToMemory(&pem.Block{Type: \"CERTIFICATE\", Bytes: ca.Root.Raw})\n\n\tmakeLeaf := func(signer crypto.Signer, serialNumber []byte) *x509.Certificate {\n\t\tleaf, err := ca.Sign(&x509.Certificate{\n\t\t\tSubject:   pkix.Name{CommonName: \"attestation cert\"},\n\t\t\tPublicKey: signer.Public(),\n\t\t\tExtraExtensions: []pkix.Extension{\n\t\t\t\t{Id: oidYubicoSerialNumber, Value: serialNumber},\n\t\t\t},\n\t\t})\n\t\trequire.NoError(t, err)\n\t\treturn leaf\n\t}\n\n\tmakeLeafWithStepManagedDeviceID := func(signer crypto.Signer, serialNumber string) *x509.Certificate {\n\t\tv, err := asn1.Marshal(stepManagedDevice{DeviceID: serialNumber})\n\t\trequire.NoError(t, err)\n\t\tleaf, err := ca.Sign(&x509.Certificate{\n\t\t\tSubject:   pkix.Name{CommonName: \"attestation cert\"},\n\t\t\tPublicKey: signer.Public(),\n\t\t\tExtraExtensions: []pkix.Extension{\n\t\t\t\t{Id: oidStepManagedDevice, Value: v},\n\t\t\t},\n\t\t})\n\t\trequire.NoError(t, err)\n\t\treturn leaf\n\t}\n\n\tmustSigner := func(kty, crv string, size int) crypto.Signer {\n\t\ts, err := keyutil.GenerateSigner(kty, crv, size)\n\t\trequire.NoError(t, err)\n\t\treturn s\n\t}\n\n\tsigner, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)\n\trequire.NoError(t, err)\n\n\tfingerprint, err := keyutil.Fingerprint(signer.Public())\n\trequire.NoError(t, err)\n\n\tserialNumber, err := asn1.Marshal(1234)\n\trequire.NoError(t, err)\n\n\tleaf := makeLeaf(signer, serialNumber)\n\tleafWithStepManagedDeviceID := makeLeafWithStepManagedDeviceID(signer, \"1234\")\n\n\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\trequire.NoError(t, err)\n\n\tkeyAuth, err := KeyAuthorization(\"token\", jwk)\n\trequire.NoError(t, err)\n\n\tkeyAuthSum := sha256.Sum256([]byte(keyAuth))\n\tsig, err := signer.Sign(rand.Reader, keyAuthSum[:], crypto.SHA256)\n\trequire.NoError(t, err)\n\n\tcborSig, err := cbor.Marshal(sig)\n\trequire.NoError(t, err)\n\n\totherSigner, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)\n\trequire.NoError(t, err)\n\n\totherSig, err := otherSigner.Sign(rand.Reader, keyAuthSum[:], crypto.SHA256)\n\trequire.NoError(t, err)\n\n\totherCBORSig, err := cbor.Marshal(otherSig)\n\trequire.NoError(t, err)\n\n\ttype args struct {\n\t\tctx  context.Context\n\t\tprov Provisioner\n\t\tch   *Challenge\n\t\tjwk  *jose.JSONWebKey\n\t\tatt  *attestationObject\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\targs    args\n\t\twant    *stepAttestationData\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{Token: \"token\"}, jwk, &attestationObject{\n\t\t\tFormat: \"step\",\n\t\t\tAttStatement: map[string]interface{}{\n\t\t\t\t\"x5c\": []interface{}{leaf.Raw, ca.Intermediate.Raw},\n\t\t\t\t\"alg\": -7,\n\t\t\t\t\"sig\": cborSig,\n\t\t\t},\n\t\t}}, &stepAttestationData{\n\t\t\tSerialNumber: \"1234\",\n\t\t\tCertificate:  leaf,\n\t\t\tFingerprint:  fingerprint,\n\t\t}, false},\n\t\t{\"ok/step-managed-device-id\", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{Token: \"token\"}, jwk, &attestationObject{\n\t\t\tFormat: \"step\",\n\t\t\tAttStatement: map[string]interface{}{\n\t\t\t\t\"x5c\": []interface{}{leafWithStepManagedDeviceID.Raw, ca.Intermediate.Raw},\n\t\t\t\t\"alg\": -7,\n\t\t\t\t\"sig\": cborSig,\n\t\t\t},\n\t\t}}, &stepAttestationData{\n\t\t\tSerialNumber: \"1234\",\n\t\t\tCertificate:  leafWithStepManagedDeviceID,\n\t\t\tFingerprint:  fingerprint,\n\t\t}, false},\n\t\t{\"fail yubico issuer\", args{ctx, mustAttestationProvisioner(t, nil), &Challenge{Token: \"token\"}, jwk, &attestationObject{\n\t\t\tFormat: \"step\",\n\t\t\tAttStatement: map[string]interface{}{\n\t\t\t\t\"x5c\": []interface{}{leaf.Raw, ca.Intermediate.Raw},\n\t\t\t\t\"alg\": -7,\n\t\t\t\t\"sig\": cborSig,\n\t\t\t},\n\t\t}}, nil, true},\n\t\t{\"fail x5c type\", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{Token: \"token\"}, jwk, &attestationObject{\n\t\t\tFormat: \"step\",\n\t\t\tAttStatement: map[string]interface{}{\n\t\t\t\t\"x5c\": [][]byte{leaf.Raw, ca.Intermediate.Raw},\n\t\t\t\t\"alg\": -7,\n\t\t\t\t\"sig\": cborSig,\n\t\t\t},\n\t\t}}, nil, true},\n\t\t{\"fail x5c empty\", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{Token: \"token\"}, jwk, &attestationObject{\n\t\t\tFormat: \"step\",\n\t\t\tAttStatement: map[string]interface{}{\n\t\t\t\t\"x5c\": []interface{}{},\n\t\t\t\t\"alg\": -7,\n\t\t\t\t\"sig\": cborSig,\n\t\t\t},\n\t\t}}, nil, true},\n\t\t{\"fail leaf type\", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{Token: \"token\"}, jwk, &attestationObject{\n\t\t\tFormat: \"step\",\n\t\t\tAttStatement: map[string]interface{}{\n\t\t\t\t\"x5c\": []interface{}{\"leaf\", ca.Intermediate.Raw},\n\t\t\t\t\"alg\": -7,\n\t\t\t\t\"sig\": cborSig,\n\t\t\t},\n\t\t}}, nil, true},\n\t\t{\"fail leaf parse\", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{Token: \"token\"}, jwk, &attestationObject{\n\t\t\tFormat: \"step\",\n\t\t\tAttStatement: map[string]interface{}{\n\t\t\t\t\"x5c\": []interface{}{leaf.Raw[:100], ca.Intermediate.Raw},\n\t\t\t\t\"alg\": -7,\n\t\t\t\t\"sig\": cborSig,\n\t\t\t},\n\t\t}}, nil, true},\n\t\t{\"fail intermediate type\", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{Token: \"token\"}, jwk, &attestationObject{\n\t\t\tFormat: \"step\",\n\t\t\tAttStatement: map[string]interface{}{\n\t\t\t\t\"x5c\": []interface{}{leaf.Raw, \"intermediate\"},\n\t\t\t\t\"alg\": -7,\n\t\t\t\t\"sig\": cborSig,\n\t\t\t},\n\t\t}}, nil, true},\n\t\t{\"fail intermediate parse\", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{Token: \"token\"}, jwk, &attestationObject{\n\t\t\tFormat: \"step\",\n\t\t\tAttStatement: map[string]interface{}{\n\t\t\t\t\"x5c\": []interface{}{leaf.Raw, ca.Intermediate.Raw[:100]},\n\t\t\t\t\"alg\": -7,\n\t\t\t\t\"sig\": cborSig,\n\t\t\t},\n\t\t}}, nil, true},\n\t\t{\"fail verify\", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{Token: \"token\"}, jwk, &attestationObject{\n\t\t\tFormat: \"step\",\n\t\t\tAttStatement: map[string]interface{}{\n\t\t\t\t\"x5c\": []interface{}{leaf.Raw},\n\t\t\t\t\"alg\": -7,\n\t\t\t\t\"sig\": cborSig,\n\t\t\t},\n\t\t}}, nil, true},\n\t\t{\"fail sig type\", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{Token: \"token\"}, jwk, &attestationObject{\n\t\t\tFormat: \"step\",\n\t\t\tAttStatement: map[string]interface{}{\n\t\t\t\t\"x5c\": []interface{}{leaf.Raw, ca.Intermediate.Raw},\n\t\t\t\t\"alg\": -7,\n\t\t\t\t\"sig\": string(cborSig),\n\t\t\t},\n\t\t}}, nil, true},\n\t\t{\"fail sig unmarshal\", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{Token: \"token\"}, jwk, &attestationObject{\n\t\t\tFormat: \"step\",\n\t\t\tAttStatement: map[string]interface{}{\n\t\t\t\t\"x5c\": []interface{}{leaf.Raw, ca.Intermediate.Raw},\n\t\t\t\t\"alg\": -7,\n\t\t\t\t\"sig\": []byte(\"bad-sig\"),\n\t\t\t},\n\t\t}}, nil, true},\n\t\t{\"fail keyAuthorization\", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{Token: \"token\"}, &jose.JSONWebKey{Key: []byte(\"not an asymmetric key\")}, &attestationObject{\n\t\t\tFormat: \"step\",\n\t\t\tAttStatement: map[string]interface{}{\n\t\t\t\t\"x5c\": []interface{}{leaf.Raw, ca.Intermediate.Raw},\n\t\t\t\t\"alg\": -7,\n\t\t\t\t\"sig\": cborSig,\n\t\t\t},\n\t\t}}, nil, true},\n\t\t{\"fail sig verify P-256\", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{Token: \"token\"}, jwk, &attestationObject{\n\t\t\tFormat: \"step\",\n\t\t\tAttStatement: map[string]interface{}{\n\t\t\t\t\"x5c\": []interface{}{leaf.Raw, ca.Intermediate.Raw},\n\t\t\t\t\"alg\": -7,\n\t\t\t\t\"sig\": otherCBORSig,\n\t\t\t},\n\t\t}}, nil, true},\n\t\t{\"fail sig verify P-384\", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{Token: \"token\"}, jwk, &attestationObject{\n\t\t\tFormat: \"step\",\n\t\t\tAttStatement: map[string]interface{}{\n\t\t\t\t\"x5c\": []interface{}{makeLeaf(mustSigner(\"EC\", \"P-384\", 0), serialNumber).Raw, ca.Intermediate.Raw},\n\t\t\t\t\"alg\": -7,\n\t\t\t\t\"sig\": cborSig,\n\t\t\t},\n\t\t}}, nil, true},\n\t\t{\"fail sig verify RSA\", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{Token: \"token\"}, jwk, &attestationObject{\n\t\t\tFormat: \"step\",\n\t\t\tAttStatement: map[string]interface{}{\n\t\t\t\t\"x5c\": []interface{}{makeLeaf(mustSigner(\"RSA\", \"\", 2048), serialNumber).Raw, ca.Intermediate.Raw},\n\t\t\t\t\"alg\": -7,\n\t\t\t\t\"sig\": cborSig,\n\t\t\t},\n\t\t}}, nil, true},\n\t\t{\"fail sig verify Ed25519\", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{Token: \"token\"}, jwk, &attestationObject{\n\t\t\tFormat: \"step\",\n\t\t\tAttStatement: map[string]interface{}{\n\t\t\t\t\"x5c\": []interface{}{makeLeaf(mustSigner(\"OKP\", \"Ed25519\", 0), serialNumber).Raw, ca.Intermediate.Raw},\n\t\t\t\t\"alg\": -7,\n\t\t\t\t\"sig\": cborSig,\n\t\t\t},\n\t\t}}, nil, true},\n\t\t{\"fail unmarshal serial number\", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{Token: \"token\"}, jwk, &attestationObject{\n\t\t\tFormat: \"step\",\n\t\t\tAttStatement: map[string]interface{}{\n\t\t\t\t\"x5c\": []interface{}{makeLeaf(signer, []byte(\"bad-serial\")).Raw, ca.Intermediate.Raw},\n\t\t\t\t\"alg\": -7,\n\t\t\t\t\"sig\": cborSig,\n\t\t\t},\n\t\t}}, nil, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := doStepAttestationFormat(tt.args.ctx, tt.args.prov, tt.args.ch, tt.args.jwk, tt.args.att)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"doStepAttestationFormat() error = %#v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"doStepAttestationFormat() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_doStepAttestationFormat_noCAIntermediate(t *testing.T) {\n\tctx := context.Background()\n\n\t// This CA simulates a YubiKey v5.2.4, where the attestation intermediate in\n\t// the CA does not have the basic constraint extension. With the current\n\t// validation of the certificate the test case below returns an error. If\n\t// we change the validation to support this use case, the test case below\n\t// should change.\n\t//\n\t// See https://github.com/Yubico/yubikey-manager/issues/522\n\tca, err := minica.New(minica.WithIntermediateTemplate(`{\"subject\": {{ toJson .Subject }}}`))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tcaRoot := pem.EncodeToMemory(&pem.Block{Type: \"CERTIFICATE\", Bytes: ca.Root.Raw})\n\n\tmakeLeaf := func(signer crypto.Signer, serialNumber []byte) *x509.Certificate {\n\t\tleaf, err := ca.Sign(&x509.Certificate{\n\t\t\tSubject:   pkix.Name{CommonName: \"attestation cert\"},\n\t\t\tPublicKey: signer.Public(),\n\t\t\tExtraExtensions: []pkix.Extension{\n\t\t\t\t{Id: oidYubicoSerialNumber, Value: serialNumber},\n\t\t\t},\n\t\t})\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\treturn leaf\n\t}\n\n\tsigner, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tserialNumber, err := asn1.Marshal(1234)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tleaf := makeLeaf(signer, serialNumber)\n\n\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tkeyAuth, err := KeyAuthorization(\"token\", jwk)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tkeyAuthSum := sha256.Sum256([]byte(keyAuth))\n\tsig, err := signer.Sign(rand.Reader, keyAuthSum[:], crypto.SHA256)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tcborSig, err := cbor.Marshal(sig)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\ttype args struct {\n\t\tctx  context.Context\n\t\tprov Provisioner\n\t\tch   *Challenge\n\t\tjwk  *jose.JSONWebKey\n\t\tatt  *attestationObject\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\targs    args\n\t\twant    *stepAttestationData\n\t\twantErr bool\n\t}{\n\t\t{\"fail no intermediate\", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{Token: \"token\"}, jwk, &attestationObject{\n\t\t\tFormat: \"step\",\n\t\t\tAttStatement: map[string]interface{}{\n\t\t\t\t\"x5c\": []interface{}{leaf.Raw, ca.Intermediate.Raw},\n\t\t\t\t\"alg\": -7,\n\t\t\t\t\"sig\": cborSig,\n\t\t\t},\n\t\t}}, nil, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := doStepAttestationFormat(tt.args.ctx, tt.args.prov, tt.args.ch, tt.args.jwk, tt.args.att)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"doStepAttestationFormat() error = %#v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"doStepAttestationFormat() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_deviceAttest01Validate(t *testing.T) {\n\tinvalidPayload := \"!?\"\n\terrorPayload, err := json.Marshal(struct {\n\t\tError string `json:\"error\"`\n\t}{\n\t\tError: \"an error\",\n\t})\n\trequire.NoError(t, err)\n\terrorBase64Payload, err := json.Marshal(struct {\n\t\tAttObj string `json:\"attObj\"`\n\t}{\n\t\tAttObj: \"?!\",\n\t})\n\trequire.NoError(t, err)\n\temptyPayload, err := json.Marshal(struct {\n\t\tAttObj string `json:\"attObj\"`\n\t}{\n\t\tAttObj: base64.RawURLEncoding.EncodeToString([]byte(\"\")),\n\t})\n\trequire.NoError(t, err)\n\temptyObjectPayload, err := json.Marshal(struct {\n\t\tAttObj string `json:\"attObj\"`\n\t}{\n\t\tAttObj: base64.RawURLEncoding.EncodeToString([]byte(\"{}\")),\n\t})\n\trequire.NoError(t, err)\n\tattObj, err := cbor.Marshal(struct {\n\t\tFormat       string                 `json:\"fmt\"`\n\t\tAttStatement map[string]interface{} `json:\"attStmt,omitempty\"`\n\t}{\n\t\tFormat: \"step\",\n\t\tAttStatement: map[string]interface{}{\n\t\t\t\"alg\": -7,\n\t\t\t\"sig\": \"\",\n\t\t},\n\t})\n\trequire.NoError(t, err)\n\terrorNonWellformedCBORPayload, err := json.Marshal(struct {\n\t\tAttObj string `json:\"attObj\"`\n\t}{\n\t\tAttObj: base64.RawURLEncoding.EncodeToString(attObj[:len(attObj)-1]), // cut the CBOR encoded data off\n\t})\n\trequire.NoError(t, err)\n\tunsupportedFormatAttObj, err := cbor.Marshal(struct {\n\t\tFormat       string                 `json:\"fmt\"`\n\t\tAttStatement map[string]interface{} `json:\"attStmt,omitempty\"`\n\t}{\n\t\tFormat: \"unsupported-format\",\n\t\tAttStatement: map[string]interface{}{\n\t\t\t\"alg\": -7,\n\t\t\t\"sig\": \"\",\n\t\t},\n\t})\n\trequire.NoError(t, err)\n\terrorUnsupportedFormat, err := json.Marshal(struct {\n\t\tAttObj string `json:\"attObj\"`\n\t}{\n\t\tAttObj: base64.RawURLEncoding.EncodeToString(unsupportedFormatAttObj),\n\t})\n\trequire.NoError(t, err)\n\ttype args struct {\n\t\tctx     context.Context\n\t\tch      *Challenge\n\t\tdb      DB\n\t\tjwk     *jose.JSONWebKey\n\t\tpayload []byte\n\t}\n\ttype test struct {\n\t\targs    args\n\t\twantErr *Error\n\t}\n\ttests := map[string]func(t *testing.T) test{\n\t\t\"fail/getAuthorization\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\targs: args{\n\t\t\t\t\tch: &Challenge{\n\t\t\t\t\t\tID:              \"chID\",\n\t\t\t\t\t\tAuthorizationID: \"azID\",\n\t\t\t\t\t\tToken:           \"token\",\n\t\t\t\t\t\tType:            \"device-attest-01\",\n\t\t\t\t\t\tStatus:          StatusPending,\n\t\t\t\t\t\tValue:           \"12345678\",\n\t\t\t\t\t},\n\t\t\t\t\tdb: &MockDB{\n\t\t\t\t\t\tMockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {\n\t\t\t\t\t\t\treturn nil, errors.New(\"not found\")\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tpayload: []byte(invalidPayload),\n\t\t\t\t},\n\t\t\t\twantErr: NewErrorISE(\"error loading authorization: not found\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/json.Unmarshal\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\targs: args{\n\t\t\t\t\tch: &Challenge{\n\t\t\t\t\t\tID:              \"chID\",\n\t\t\t\t\t\tAuthorizationID: \"azID\",\n\t\t\t\t\t\tToken:           \"token\",\n\t\t\t\t\t\tType:            \"device-attest-01\",\n\t\t\t\t\t\tStatus:          StatusPending,\n\t\t\t\t\t\tValue:           \"12345678\",\n\t\t\t\t\t},\n\t\t\t\t\tdb: &MockDB{\n\t\t\t\t\t\tMockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {\n\t\t\t\t\t\t\tassert.Equal(t, \"azID\", id)\n\t\t\t\t\t\t\treturn &Authorization{ID: \"azID\"}, nil\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tpayload: []byte(invalidPayload),\n\t\t\t\t},\n\t\t\t\twantErr: NewErrorISE(\"error unmarshalling JSON: invalid character '!' looking for beginning of value\"),\n\t\t\t}\n\n\t\t},\n\t\t\"fail/storeError\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\targs: args{\n\t\t\t\t\tch: &Challenge{\n\t\t\t\t\t\tID:              \"chID\",\n\t\t\t\t\t\tAuthorizationID: \"azID\",\n\t\t\t\t\t\tToken:           \"token\",\n\t\t\t\t\t\tType:            \"device-attest-01\",\n\t\t\t\t\t\tStatus:          StatusPending,\n\t\t\t\t\t\tValue:           \"12345678\",\n\t\t\t\t\t},\n\t\t\t\t\tpayload: errorPayload,\n\t\t\t\t\tdb: &MockDB{\n\t\t\t\t\t\tMockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {\n\t\t\t\t\t\t\tassert.Equal(t, \"azID\", id)\n\t\t\t\t\t\t\treturn &Authorization{ID: \"azID\"}, nil\n\t\t\t\t\t\t},\n\t\t\t\t\t\tMockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {\n\t\t\t\t\t\t\tassert.Equal(t, \"chID\", updch.ID)\n\t\t\t\t\t\t\tassert.Equal(t, \"token\", updch.Token)\n\t\t\t\t\t\t\tassert.Equal(t, StatusInvalid, updch.Status)\n\t\t\t\t\t\t\tassert.Equal(t, ChallengeType(\"device-attest-01\"), updch.Type)\n\t\t\t\t\t\t\tassert.Equal(t, \"12345678\", updch.Value)\n\n\t\t\t\t\t\t\terr := NewError(ErrorRejectedIdentifierType, \"payload contained error: an error\")\n\n\t\t\t\t\t\t\tassert.EqualError(t, updch.Error.Err, err.Err.Error())\n\t\t\t\t\t\t\tassert.Equal(t, err.Type, updch.Error.Type)\n\t\t\t\t\t\t\tassert.Equal(t, err.Detail, updch.Error.Detail)\n\t\t\t\t\t\t\tassert.Equal(t, err.Status, updch.Error.Status)\n\t\t\t\t\t\t\tassert.Equal(t, err.Subproblems, updch.Error.Subproblems)\n\n\t\t\t\t\t\t\treturn errors.New(\"force\")\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\twantErr: NewErrorISE(\"failure saving error to acme challenge: force\"),\n\t\t\t}\n\t\t},\n\t\t\"ok/storeError-return-nil\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\targs: args{\n\t\t\t\t\tch: &Challenge{\n\t\t\t\t\t\tID:              \"chID\",\n\t\t\t\t\t\tAuthorizationID: \"azID\",\n\t\t\t\t\t\tToken:           \"token\",\n\t\t\t\t\t\tType:            \"device-attest-01\",\n\t\t\t\t\t\tStatus:          StatusPending,\n\t\t\t\t\t\tValue:           \"12345678\",\n\t\t\t\t\t},\n\t\t\t\t\tpayload: errorPayload,\n\t\t\t\t\tdb: &MockDB{\n\t\t\t\t\t\tMockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {\n\t\t\t\t\t\t\tassert.Equal(t, \"azID\", id)\n\t\t\t\t\t\t\treturn &Authorization{ID: \"azID\"}, nil\n\t\t\t\t\t\t},\n\t\t\t\t\t\tMockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {\n\t\t\t\t\t\t\tassert.Equal(t, \"chID\", updch.ID)\n\t\t\t\t\t\t\tassert.Equal(t, \"token\", updch.Token)\n\t\t\t\t\t\t\tassert.Equal(t, StatusInvalid, updch.Status)\n\t\t\t\t\t\t\tassert.Equal(t, ChallengeType(\"device-attest-01\"), updch.Type)\n\t\t\t\t\t\t\tassert.Equal(t, \"12345678\", updch.Value)\n\t\t\t\t\t\t\tassert.Equal(t, errorPayload, updch.Payload)\n\t\t\t\t\t\t\tassert.Empty(t, updch.PayloadFormat)\n\n\t\t\t\t\t\t\terr := NewError(ErrorRejectedIdentifierType, \"payload contained error: an error\")\n\n\t\t\t\t\t\t\tassert.EqualError(t, updch.Error.Err, err.Err.Error())\n\t\t\t\t\t\t\tassert.Equal(t, err.Type, updch.Error.Type)\n\t\t\t\t\t\t\tassert.Equal(t, err.Detail, updch.Error.Detail)\n\t\t\t\t\t\t\tassert.Equal(t, err.Status, updch.Error.Status)\n\t\t\t\t\t\t\tassert.Equal(t, err.Subproblems, updch.Error.Subproblems)\n\n\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\twantErr: nil,\n\t\t\t}\n\t\t},\n\t\t\"ok/base64-decode\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\targs: args{\n\t\t\t\t\tch: &Challenge{\n\t\t\t\t\t\tID:              \"chID\",\n\t\t\t\t\t\tAuthorizationID: \"azID\",\n\t\t\t\t\t\tToken:           \"token\",\n\t\t\t\t\t\tType:            \"device-attest-01\",\n\t\t\t\t\t\tStatus:          StatusPending,\n\t\t\t\t\t\tValue:           \"12345678\",\n\t\t\t\t\t},\n\t\t\t\t\tdb: &MockDB{\n\t\t\t\t\t\tMockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {\n\t\t\t\t\t\t\tassert.Equal(t, \"azID\", id)\n\t\t\t\t\t\t\treturn &Authorization{ID: \"azID\"}, nil\n\t\t\t\t\t\t},\n\t\t\t\t\t\tMockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {\n\t\t\t\t\t\t\tassert.Equal(t, \"chID\", updch.ID)\n\t\t\t\t\t\t\tassert.Equal(t, \"token\", updch.Token)\n\t\t\t\t\t\t\tassert.Equal(t, StatusInvalid, updch.Status)\n\t\t\t\t\t\t\tassert.Equal(t, ChallengeType(\"device-attest-01\"), updch.Type)\n\t\t\t\t\t\t\tassert.Equal(t, \"12345678\", updch.Value)\n\t\t\t\t\t\t\tassert.Equal(t, errorBase64Payload, updch.Payload)\n\t\t\t\t\t\t\tassert.Empty(t, updch.PayloadFormat)\n\n\t\t\t\t\t\t\terr := NewDetailedError(ErrorBadAttestationStatementType, \"failed base64 decoding attObj %q\", \"?!\")\n\n\t\t\t\t\t\t\tassert.EqualError(t, updch.Error.Err, err.Err.Error())\n\t\t\t\t\t\t\tassert.Equal(t, err.Type, updch.Error.Type)\n\t\t\t\t\t\t\tassert.Equal(t, err.Detail, updch.Error.Detail)\n\t\t\t\t\t\t\tassert.Equal(t, err.Status, updch.Error.Status)\n\t\t\t\t\t\t\tassert.Equal(t, err.Subproblems, updch.Error.Subproblems)\n\n\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tpayload: errorBase64Payload,\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"ok/empty-attobj\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\targs: args{\n\t\t\t\t\tch: &Challenge{\n\t\t\t\t\t\tID:              \"chID\",\n\t\t\t\t\t\tAuthorizationID: \"azID\",\n\t\t\t\t\t\tToken:           \"token\",\n\t\t\t\t\t\tType:            \"device-attest-01\",\n\t\t\t\t\t\tStatus:          StatusPending,\n\t\t\t\t\t\tValue:           \"12345678\",\n\t\t\t\t\t},\n\t\t\t\t\tdb: &MockDB{\n\t\t\t\t\t\tMockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {\n\t\t\t\t\t\t\tassert.Equal(t, \"azID\", id)\n\t\t\t\t\t\t\treturn &Authorization{ID: \"azID\"}, nil\n\t\t\t\t\t\t},\n\t\t\t\t\t\tMockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {\n\t\t\t\t\t\t\tassert.Equal(t, \"chID\", updch.ID)\n\t\t\t\t\t\t\tassert.Equal(t, \"token\", updch.Token)\n\t\t\t\t\t\t\tassert.Equal(t, StatusInvalid, updch.Status)\n\t\t\t\t\t\t\tassert.Equal(t, ChallengeType(\"device-attest-01\"), updch.Type)\n\t\t\t\t\t\t\tassert.Equal(t, \"12345678\", updch.Value)\n\t\t\t\t\t\t\tassert.Equal(t, emptyPayload, updch.Payload)\n\t\t\t\t\t\t\tassert.Empty(t, updch.PayloadFormat)\n\n\t\t\t\t\t\t\terr := NewDetailedError(ErrorBadAttestationStatementType, \"attObj must not be empty\")\n\n\t\t\t\t\t\t\tassert.EqualError(t, updch.Error.Err, err.Err.Error())\n\t\t\t\t\t\t\tassert.Equal(t, err.Type, updch.Error.Type)\n\t\t\t\t\t\t\tassert.Equal(t, err.Detail, updch.Error.Detail)\n\t\t\t\t\t\t\tassert.Equal(t, err.Status, updch.Error.Status)\n\t\t\t\t\t\t\tassert.Equal(t, err.Subproblems, updch.Error.Subproblems)\n\n\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tpayload: emptyPayload,\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"ok/empty-json-attobj\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\targs: args{\n\t\t\t\t\tch: &Challenge{\n\t\t\t\t\t\tID:              \"chID\",\n\t\t\t\t\t\tAuthorizationID: \"azID\",\n\t\t\t\t\t\tToken:           \"token\",\n\t\t\t\t\t\tType:            \"device-attest-01\",\n\t\t\t\t\t\tStatus:          StatusPending,\n\t\t\t\t\t\tValue:           \"12345678\",\n\t\t\t\t\t},\n\t\t\t\t\tdb: &MockDB{\n\t\t\t\t\t\tMockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {\n\t\t\t\t\t\t\tassert.Equal(t, \"azID\", id)\n\t\t\t\t\t\t\treturn &Authorization{ID: \"azID\"}, nil\n\t\t\t\t\t\t},\n\t\t\t\t\t\tMockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {\n\t\t\t\t\t\t\tassert.Equal(t, \"chID\", updch.ID)\n\t\t\t\t\t\t\tassert.Equal(t, \"token\", updch.Token)\n\t\t\t\t\t\t\tassert.Equal(t, StatusInvalid, updch.Status)\n\t\t\t\t\t\t\tassert.Equal(t, ChallengeType(\"device-attest-01\"), updch.Type)\n\t\t\t\t\t\t\tassert.Equal(t, \"12345678\", updch.Value)\n\t\t\t\t\t\t\tassert.Equal(t, emptyObjectPayload, updch.Payload)\n\t\t\t\t\t\t\tassert.Empty(t, updch.PayloadFormat)\n\n\t\t\t\t\t\t\terr := NewDetailedError(ErrorBadAttestationStatementType, \"attObj must not be empty\")\n\n\t\t\t\t\t\t\tassert.EqualError(t, updch.Error.Err, err.Err.Error())\n\t\t\t\t\t\t\tassert.Equal(t, err.Type, updch.Error.Type)\n\t\t\t\t\t\t\tassert.Equal(t, err.Detail, updch.Error.Detail)\n\t\t\t\t\t\t\tassert.Equal(t, err.Status, updch.Error.Status)\n\t\t\t\t\t\t\tassert.Equal(t, err.Subproblems, updch.Error.Subproblems)\n\n\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tpayload: emptyObjectPayload,\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"ok/cborDecoder.Wellformed\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\targs: args{\n\t\t\t\t\tch: &Challenge{\n\t\t\t\t\t\tID:              \"chID\",\n\t\t\t\t\t\tAuthorizationID: \"azID\",\n\t\t\t\t\t\tToken:           \"token\",\n\t\t\t\t\t\tType:            \"device-attest-01\",\n\t\t\t\t\t\tStatus:          StatusPending,\n\t\t\t\t\t\tValue:           \"12345678\",\n\t\t\t\t\t},\n\t\t\t\t\tdb: &MockDB{\n\t\t\t\t\t\tMockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {\n\t\t\t\t\t\t\tassert.Equal(t, \"azID\", id)\n\t\t\t\t\t\t\treturn &Authorization{ID: \"azID\"}, nil\n\t\t\t\t\t\t},\n\t\t\t\t\t\tMockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {\n\t\t\t\t\t\t\tassert.Equal(t, \"chID\", updch.ID)\n\t\t\t\t\t\t\tassert.Equal(t, \"token\", updch.Token)\n\t\t\t\t\t\t\tassert.Equal(t, StatusInvalid, updch.Status)\n\t\t\t\t\t\t\tassert.Equal(t, ChallengeType(\"device-attest-01\"), updch.Type)\n\t\t\t\t\t\t\tassert.Equal(t, \"12345678\", updch.Value)\n\t\t\t\t\t\t\tassert.Equal(t, errorNonWellformedCBORPayload, updch.Payload)\n\t\t\t\t\t\t\tassert.Empty(t, updch.PayloadFormat)\n\n\t\t\t\t\t\t\terr := NewDetailedError(ErrorBadAttestationStatementType, \"attObj is not well formed CBOR: unexpected EOF\")\n\n\t\t\t\t\t\t\tassert.EqualError(t, updch.Error.Err, err.Err.Error())\n\t\t\t\t\t\t\tassert.Equal(t, err.Type, updch.Error.Type)\n\t\t\t\t\t\t\tassert.Equal(t, err.Detail, updch.Error.Detail)\n\t\t\t\t\t\t\tassert.Equal(t, err.Status, updch.Error.Status)\n\t\t\t\t\t\t\tassert.Equal(t, err.Subproblems, updch.Error.Subproblems)\n\n\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tpayload: errorNonWellformedCBORPayload,\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"ok/unsupported-attestation-format\": func(t *testing.T) test {\n\t\t\tctx := NewProvisionerContext(context.Background(), mustNonAttestationProvisioner(t))\n\t\t\treturn test{\n\t\t\t\targs: args{\n\t\t\t\t\tctx: ctx,\n\t\t\t\t\tch: &Challenge{\n\t\t\t\t\t\tID:              \"chID\",\n\t\t\t\t\t\tAuthorizationID: \"azID\",\n\t\t\t\t\t\tToken:           \"token\",\n\t\t\t\t\t\tType:            \"device-attest-01\",\n\t\t\t\t\t\tStatus:          StatusPending,\n\t\t\t\t\t\tValue:           \"12345678\",\n\t\t\t\t\t},\n\t\t\t\t\tdb: &MockDB{\n\t\t\t\t\t\tMockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {\n\t\t\t\t\t\t\tassert.Equal(t, \"azID\", id)\n\t\t\t\t\t\t\treturn &Authorization{ID: \"azID\"}, nil\n\t\t\t\t\t\t},\n\t\t\t\t\t\tMockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {\n\t\t\t\t\t\t\tassert.Equal(t, \"chID\", updch.ID)\n\t\t\t\t\t\t\tassert.Equal(t, \"token\", updch.Token)\n\t\t\t\t\t\t\tassert.Equal(t, StatusInvalid, updch.Status)\n\t\t\t\t\t\t\tassert.Equal(t, ChallengeType(\"device-attest-01\"), updch.Type)\n\t\t\t\t\t\t\tassert.Equal(t, \"12345678\", updch.Value)\n\t\t\t\t\t\t\tassert.Equal(t, errorUnsupportedFormat, updch.Payload)\n\t\t\t\t\t\t\tassert.Empty(t, updch.PayloadFormat)\n\n\t\t\t\t\t\t\terr := NewDetailedError(ErrorBadAttestationStatementType, \"unsupported attestation object format %q\", \"unsupported-format\")\n\n\t\t\t\t\t\t\tassert.EqualError(t, updch.Error.Err, err.Err.Error())\n\t\t\t\t\t\t\tassert.Equal(t, err.Type, updch.Error.Type)\n\t\t\t\t\t\t\tassert.Equal(t, err.Detail, updch.Error.Detail)\n\t\t\t\t\t\t\tassert.Equal(t, err.Status, updch.Error.Status)\n\t\t\t\t\t\t\tassert.Equal(t, err.Subproblems, updch.Error.Subproblems)\n\n\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tpayload: errorUnsupportedFormat,\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"ok/prov.IsAttestationFormatEnabled\": func(t *testing.T) test {\n\t\t\tjwk, keyAuth := mustAccountAndKeyAuthorization(t, \"token\")\n\t\t\tpayload, _, _ := mustAttestYubikey(t, \"nonce\", keyAuth, 12345678)\n\t\t\tctx := NewProvisionerContext(context.Background(), mustNonAttestationProvisioner(t))\n\n\t\t\treturn test{\n\t\t\t\targs: args{\n\t\t\t\t\tctx: ctx,\n\t\t\t\t\tjwk: jwk,\n\t\t\t\t\tch: &Challenge{\n\t\t\t\t\t\tID:              \"chID\",\n\t\t\t\t\t\tAuthorizationID: \"azID\",\n\t\t\t\t\t\tToken:           \"token\",\n\t\t\t\t\t\tType:            \"device-attest-01\",\n\t\t\t\t\t\tStatus:          StatusPending,\n\t\t\t\t\t\tValue:           \"12345678\",\n\t\t\t\t\t},\n\t\t\t\t\tpayload: payload,\n\t\t\t\t\tdb: &MockDB{\n\t\t\t\t\t\tMockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {\n\t\t\t\t\t\t\tassert.Equal(t, \"azID\", id)\n\t\t\t\t\t\t\treturn &Authorization{ID: \"azID\"}, nil\n\t\t\t\t\t\t},\n\t\t\t\t\t\tMockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {\n\t\t\t\t\t\t\tassert.Equal(t, \"chID\", updch.ID)\n\t\t\t\t\t\t\tassert.Equal(t, \"token\", updch.Token)\n\t\t\t\t\t\t\tassert.Equal(t, StatusInvalid, updch.Status)\n\t\t\t\t\t\t\tassert.Equal(t, ChallengeType(\"device-attest-01\"), updch.Type)\n\t\t\t\t\t\t\tassert.Equal(t, \"12345678\", updch.Value)\n\t\t\t\t\t\t\tassert.Equal(t, payload, updch.Payload)\n\t\t\t\t\t\t\tassert.Empty(t, updch.PayloadFormat)\n\n\t\t\t\t\t\t\terr := NewError(ErrorBadAttestationStatementType, \"attestation format %q is not enabled\", \"step\")\n\n\t\t\t\t\t\t\tassert.EqualError(t, updch.Error.Err, err.Err.Error())\n\t\t\t\t\t\t\tassert.Equal(t, err.Type, updch.Error.Type)\n\t\t\t\t\t\t\tassert.Equal(t, err.Detail, updch.Error.Detail)\n\t\t\t\t\t\t\tassert.Equal(t, err.Status, updch.Error.Status)\n\t\t\t\t\t\t\tassert.Equal(t, err.Subproblems, updch.Error.Subproblems)\n\n\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\twantErr: nil,\n\t\t\t}\n\t\t},\n\t\t\"ok/doAppleAttestationFormat-storeError\": func(t *testing.T) test {\n\t\t\tctx := NewProvisionerContext(context.Background(), mustAttestationProvisioner(t, nil))\n\t\t\tattObj, err := cbor.Marshal(struct {\n\t\t\t\tFormat       string                 `json:\"fmt\"`\n\t\t\t\tAttStatement map[string]interface{} `json:\"attStmt,omitempty\"`\n\t\t\t}{\n\t\t\t\tFormat:       \"apple\",\n\t\t\t\tAttStatement: map[string]interface{}{},\n\t\t\t})\n\t\t\trequire.NoError(t, err)\n\t\t\tpayload, err := json.Marshal(struct {\n\t\t\t\tAttObj string `json:\"attObj\"`\n\t\t\t}{\n\t\t\t\tAttObj: base64.RawURLEncoding.EncodeToString(attObj),\n\t\t\t})\n\t\t\trequire.NoError(t, err)\n\t\t\treturn test{\n\t\t\t\targs: args{\n\t\t\t\t\tctx: ctx,\n\t\t\t\t\tch: &Challenge{\n\t\t\t\t\t\tID:              \"chID\",\n\t\t\t\t\t\tAuthorizationID: \"azID\",\n\t\t\t\t\t\tToken:           \"token\",\n\t\t\t\t\t\tType:            \"device-attest-01\",\n\t\t\t\t\t\tStatus:          StatusPending,\n\t\t\t\t\t\tValue:           \"12345678\",\n\t\t\t\t\t},\n\t\t\t\t\tpayload: payload,\n\t\t\t\t\tdb: &MockDB{\n\t\t\t\t\t\tMockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {\n\t\t\t\t\t\t\tassert.Equal(t, \"azID\", id)\n\t\t\t\t\t\t\treturn &Authorization{ID: \"azID\"}, nil\n\t\t\t\t\t\t},\n\t\t\t\t\t\tMockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {\n\t\t\t\t\t\t\tassert.Equal(t, \"chID\", updch.ID)\n\t\t\t\t\t\t\tassert.Equal(t, \"token\", updch.Token)\n\t\t\t\t\t\t\tassert.Equal(t, StatusInvalid, updch.Status)\n\t\t\t\t\t\t\tassert.Equal(t, ChallengeType(\"device-attest-01\"), updch.Type)\n\t\t\t\t\t\t\tassert.Equal(t, \"12345678\", updch.Value)\n\t\t\t\t\t\t\tassert.Equal(t, payload, updch.Payload)\n\t\t\t\t\t\t\tassert.Empty(t, updch.PayloadFormat)\n\n\t\t\t\t\t\t\terr := NewDetailedError(ErrorBadAttestationStatementType, \"x5c not present\")\n\n\t\t\t\t\t\t\tassert.EqualError(t, updch.Error.Err, err.Err.Error())\n\t\t\t\t\t\t\tassert.Equal(t, err.Type, updch.Error.Type)\n\t\t\t\t\t\t\tassert.Equal(t, err.Detail, updch.Error.Detail)\n\t\t\t\t\t\t\tassert.Equal(t, err.Status, updch.Error.Status)\n\t\t\t\t\t\t\tassert.Equal(t, err.Subproblems, updch.Error.Subproblems)\n\n\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\twantErr: nil,\n\t\t\t}\n\t\t},\n\t\t\"ok/doAppleAttestationFormat-non-matching-nonce\": func(t *testing.T) test {\n\t\t\tjwk, _ := mustAccountAndKeyAuthorization(t, \"token\")\n\t\t\tpayload, _, root := mustAttestApple(t, \"bad-nonce\")\n\n\t\t\tcaRoot := pem.EncodeToMemory(&pem.Block{Type: \"CERTIFICATE\", Bytes: root.Raw})\n\t\t\tctx := NewProvisionerContext(context.Background(), mustAttestationProvisioner(t, caRoot))\n\n\t\t\treturn test{\n\t\t\t\targs: args{\n\t\t\t\t\tctx: ctx,\n\t\t\t\t\tjwk: jwk,\n\t\t\t\t\tch: &Challenge{\n\t\t\t\t\t\tID:              \"chID\",\n\t\t\t\t\t\tAuthorizationID: \"azID\",\n\t\t\t\t\t\tToken:           \"token\",\n\t\t\t\t\t\tType:            \"device-attest-01\",\n\t\t\t\t\t\tStatus:          StatusPending,\n\t\t\t\t\t\tValue:           \"serial-number\",\n\t\t\t\t\t},\n\t\t\t\t\tpayload: payload,\n\t\t\t\t\tdb: &MockDB{\n\t\t\t\t\t\tMockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {\n\t\t\t\t\t\t\tassert.Equal(t, \"azID\", id)\n\t\t\t\t\t\t\treturn &Authorization{ID: \"azID\"}, nil\n\t\t\t\t\t\t},\n\t\t\t\t\t\tMockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {\n\t\t\t\t\t\t\tassert.Equal(t, \"chID\", updch.ID)\n\t\t\t\t\t\t\tassert.Equal(t, \"token\", updch.Token)\n\t\t\t\t\t\t\tassert.Equal(t, StatusInvalid, updch.Status)\n\t\t\t\t\t\t\tassert.Equal(t, ChallengeType(\"device-attest-01\"), updch.Type)\n\t\t\t\t\t\t\tassert.Equal(t, \"serial-number\", updch.Value)\n\t\t\t\t\t\t\tassert.Equal(t, payload, updch.Payload)\n\t\t\t\t\t\t\tassert.Empty(t, updch.PayloadFormat)\n\n\t\t\t\t\t\t\terr := NewDetailedError(ErrorBadAttestationStatementType, \"challenge token does not match\")\n\n\t\t\t\t\t\t\tassert.EqualError(t, updch.Error.Err, err.Err.Error())\n\t\t\t\t\t\t\tassert.Equal(t, err.Type, updch.Error.Type)\n\t\t\t\t\t\t\tassert.Equal(t, err.Detail, updch.Error.Detail)\n\t\t\t\t\t\t\tassert.Equal(t, err.Status, updch.Error.Status)\n\t\t\t\t\t\t\tassert.Equal(t, err.Subproblems, updch.Error.Subproblems)\n\n\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\twantErr: nil,\n\t\t\t}\n\t\t},\n\t\t\"ok/doAppleAttestationFormat-non-matching-challenge-value\": func(t *testing.T) test {\n\t\t\tjwk, _ := mustAccountAndKeyAuthorization(t, \"token\")\n\t\t\tpayload, _, root := mustAttestApple(t, \"nonce\")\n\n\t\t\tcaRoot := pem.EncodeToMemory(&pem.Block{Type: \"CERTIFICATE\", Bytes: root.Raw})\n\t\t\tctx := NewProvisionerContext(context.Background(), mustAttestationProvisioner(t, caRoot))\n\t\t\treturn test{\n\t\t\t\targs: args{\n\t\t\t\t\tctx: ctx,\n\t\t\t\t\tjwk: jwk,\n\t\t\t\t\tch: &Challenge{\n\t\t\t\t\t\tID:              \"chID\",\n\t\t\t\t\t\tAuthorizationID: \"azID\",\n\t\t\t\t\t\tToken:           \"nonce\",\n\t\t\t\t\t\tType:            \"device-attest-01\",\n\t\t\t\t\t\tStatus:          StatusPending,\n\t\t\t\t\t\tValue:           \"non-matching-value\",\n\t\t\t\t\t},\n\t\t\t\t\tpayload: payload,\n\t\t\t\t\tdb: &MockDB{\n\t\t\t\t\t\tMockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {\n\t\t\t\t\t\t\tassert.Equal(t, \"azID\", id)\n\t\t\t\t\t\t\treturn &Authorization{ID: \"azID\"}, nil\n\t\t\t\t\t\t},\n\t\t\t\t\t\tMockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {\n\t\t\t\t\t\t\tassert.Equal(t, \"chID\", updch.ID)\n\t\t\t\t\t\t\tassert.Equal(t, \"nonce\", updch.Token)\n\t\t\t\t\t\t\tassert.Equal(t, StatusInvalid, updch.Status)\n\t\t\t\t\t\t\tassert.Equal(t, ChallengeType(\"device-attest-01\"), updch.Type)\n\t\t\t\t\t\t\tassert.Equal(t, \"non-matching-value\", updch.Value)\n\t\t\t\t\t\t\tassert.Equal(t, payload, updch.Payload)\n\t\t\t\t\t\t\tassert.Empty(t, updch.PayloadFormat)\n\n\t\t\t\t\t\t\tsubproblem := NewSubproblemWithIdentifier(\n\t\t\t\t\t\t\t\tErrorRejectedIdentifierType,\n\t\t\t\t\t\t\t\tIdentifier{Type: \"permanent-identifier\", Value: \"non-matching-value\"},\n\t\t\t\t\t\t\t\t`challenge identifier \"non-matching-value\" doesn't match any of the attested hardware identifiers [\"udid\" \"serial-number\"]`,\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\terr := NewDetailedError(ErrorBadAttestationStatementType, \"permanent identifier does not match\").AddSubproblems(subproblem)\n\n\t\t\t\t\t\t\tassert.EqualError(t, updch.Error.Err, err.Err.Error())\n\t\t\t\t\t\t\tassert.Equal(t, err.Type, updch.Error.Type)\n\t\t\t\t\t\t\tassert.Equal(t, err.Detail, updch.Error.Detail)\n\t\t\t\t\t\t\tassert.Equal(t, err.Status, updch.Error.Status)\n\t\t\t\t\t\t\tassert.Equal(t, err.Subproblems, updch.Error.Subproblems)\n\n\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\twantErr: nil,\n\t\t\t}\n\t\t},\n\t\t\"ok/doStepAttestationFormat-storeError\": func(t *testing.T) test {\n\t\t\tca, err := minica.New()\n\t\t\trequire.NoError(t, err)\n\t\t\tcaRoot := pem.EncodeToMemory(&pem.Block{Type: \"CERTIFICATE\", Bytes: ca.Root.Raw})\n\t\t\tsigner, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)\n\t\t\trequire.NoError(t, err)\n\t\t\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\trequire.NoError(t, err)\n\t\t\ttoken := \"token\"\n\t\t\tkeyAuth, err := KeyAuthorization(token, jwk)\n\t\t\trequire.NoError(t, err)\n\t\t\tkeyAuthSum := sha256.Sum256([]byte(keyAuth))\n\t\t\tsig, err := signer.Sign(rand.Reader, keyAuthSum[:], crypto.SHA256)\n\t\t\trequire.NoError(t, err)\n\t\t\tcborSig, err := cbor.Marshal(sig)\n\t\t\trequire.NoError(t, err)\n\t\t\tctx := NewProvisionerContext(context.Background(), mustAttestationProvisioner(t, caRoot))\n\t\t\tattObj, err := cbor.Marshal(struct {\n\t\t\t\tFormat       string                 `json:\"fmt\"`\n\t\t\t\tAttStatement map[string]interface{} `json:\"attStmt,omitempty\"`\n\t\t\t}{\n\t\t\t\tFormat: \"step\",\n\t\t\t\tAttStatement: map[string]interface{}{\n\t\t\t\t\t\"alg\": -7,\n\t\t\t\t\t\"sig\": cborSig,\n\t\t\t\t},\n\t\t\t})\n\t\t\trequire.NoError(t, err)\n\t\t\tpayload, err := json.Marshal(struct {\n\t\t\t\tAttObj string `json:\"attObj\"`\n\t\t\t}{\n\t\t\t\tAttObj: base64.RawURLEncoding.EncodeToString(attObj),\n\t\t\t})\n\t\t\trequire.NoError(t, err)\n\t\t\treturn test{\n\t\t\t\targs: args{\n\t\t\t\t\tctx: ctx,\n\t\t\t\t\tch: &Challenge{\n\t\t\t\t\t\tID:              \"chID\",\n\t\t\t\t\t\tAuthorizationID: \"azID\",\n\t\t\t\t\t\tToken:           \"token\",\n\t\t\t\t\t\tType:            \"device-attest-01\",\n\t\t\t\t\t\tStatus:          StatusPending,\n\t\t\t\t\t\tValue:           \"12345678\",\n\t\t\t\t\t},\n\t\t\t\t\tpayload: payload,\n\t\t\t\t\tdb: &MockDB{\n\t\t\t\t\t\tMockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {\n\t\t\t\t\t\t\tassert.Equal(t, \"azID\", id)\n\t\t\t\t\t\t\treturn &Authorization{ID: \"azID\"}, nil\n\t\t\t\t\t\t},\n\t\t\t\t\t\tMockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {\n\t\t\t\t\t\t\tassert.Equal(t, \"chID\", updch.ID)\n\t\t\t\t\t\t\tassert.Equal(t, \"token\", updch.Token)\n\t\t\t\t\t\t\tassert.Equal(t, StatusInvalid, updch.Status)\n\t\t\t\t\t\t\tassert.Equal(t, ChallengeType(\"device-attest-01\"), updch.Type)\n\t\t\t\t\t\t\tassert.Equal(t, \"12345678\", updch.Value)\n\t\t\t\t\t\t\tassert.Equal(t, payload, updch.Payload)\n\t\t\t\t\t\t\tassert.Empty(t, updch.PayloadFormat)\n\n\t\t\t\t\t\t\terr := NewDetailedError(ErrorBadAttestationStatementType, \"x5c not present\")\n\n\t\t\t\t\t\t\tassert.EqualError(t, updch.Error.Err, err.Err.Error())\n\t\t\t\t\t\t\tassert.Equal(t, err.Type, updch.Error.Type)\n\t\t\t\t\t\t\tassert.Equal(t, err.Detail, updch.Error.Detail)\n\t\t\t\t\t\t\tassert.Equal(t, err.Status, updch.Error.Status)\n\t\t\t\t\t\t\tassert.Equal(t, err.Subproblems, updch.Error.Subproblems)\n\n\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\twantErr: nil,\n\t\t\t}\n\t\t},\n\t\t\"ok/doStepAttestationFormat-non-matching-identifier\": func(t *testing.T) test {\n\t\t\tjwk, keyAuth := mustAccountAndKeyAuthorization(t, \"token\")\n\t\t\tpayload, leaf, root := mustAttestYubikey(t, \"nonce\", keyAuth, 87654321)\n\n\t\t\tcaRoot := pem.EncodeToMemory(&pem.Block{Type: \"CERTIFICATE\", Bytes: root.Raw})\n\t\t\tctx := NewProvisionerContext(context.Background(), mustAttestationProvisioner(t, caRoot))\n\n\t\t\treturn test{\n\t\t\t\targs: args{\n\t\t\t\t\tctx: ctx,\n\t\t\t\t\tjwk: jwk,\n\t\t\t\t\tch: &Challenge{\n\t\t\t\t\t\tID:              \"chID\",\n\t\t\t\t\t\tAuthorizationID: \"azID\",\n\t\t\t\t\t\tToken:           \"token\",\n\t\t\t\t\t\tType:            \"device-attest-01\",\n\t\t\t\t\t\tStatus:          StatusPending,\n\t\t\t\t\t\tValue:           \"12345678\",\n\t\t\t\t\t},\n\t\t\t\t\tpayload: payload,\n\t\t\t\t\tdb: &MockDB{\n\t\t\t\t\t\tMockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {\n\t\t\t\t\t\t\tassert.Equal(t, \"azID\", id)\n\t\t\t\t\t\t\treturn &Authorization{ID: \"azID\"}, nil\n\t\t\t\t\t\t},\n\t\t\t\t\t\tMockUpdateAuthorization: func(ctx context.Context, az *Authorization) error {\n\t\t\t\t\t\t\tfingerprint, err := keyutil.Fingerprint(leaf.PublicKey)\n\t\t\t\t\t\t\tassert.NoError(t, err)\n\t\t\t\t\t\t\tassert.Equal(t, \"azID\", az.ID)\n\t\t\t\t\t\t\tassert.Equal(t, fingerprint, az.Fingerprint)\n\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t},\n\t\t\t\t\t\tMockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {\n\t\t\t\t\t\t\tassert.Equal(t, \"chID\", updch.ID)\n\t\t\t\t\t\t\tassert.Equal(t, \"token\", updch.Token)\n\t\t\t\t\t\t\tassert.Equal(t, StatusInvalid, updch.Status)\n\t\t\t\t\t\t\tassert.Equal(t, ChallengeType(\"device-attest-01\"), updch.Type)\n\t\t\t\t\t\t\tassert.Equal(t, \"12345678\", updch.Value)\n\t\t\t\t\t\t\tassert.Equal(t, payload, updch.Payload)\n\t\t\t\t\t\t\tassert.Empty(t, updch.PayloadFormat)\n\n\t\t\t\t\t\t\terr := NewDetailedError(ErrorBadAttestationStatementType, \"permanent identifier does not match\").\n\t\t\t\t\t\t\t\tAddSubproblems(NewSubproblemWithIdentifier(\n\t\t\t\t\t\t\t\t\tErrorRejectedIdentifierType,\n\t\t\t\t\t\t\t\t\tIdentifier{Type: \"permanent-identifier\", Value: \"12345678\"},\n\t\t\t\t\t\t\t\t\t\"challenge identifier \\\"12345678\\\" doesn't match the attested hardware identifier \\\"87654321\\\"\",\n\t\t\t\t\t\t\t\t))\n\n\t\t\t\t\t\t\tassert.EqualError(t, updch.Error.Err, err.Err.Error())\n\t\t\t\t\t\t\tassert.Equal(t, err.Type, updch.Error.Type)\n\t\t\t\t\t\t\tassert.Equal(t, err.Detail, updch.Error.Detail)\n\t\t\t\t\t\t\tassert.Equal(t, err.Status, updch.Error.Status)\n\t\t\t\t\t\t\tassert.Equal(t, err.Subproblems, updch.Error.Subproblems)\n\n\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\twantErr: nil,\n\t\t\t}\n\t\t},\n\t\t\"ok/unknown-attestation-format\": func(t *testing.T) test {\n\t\t\tca, err := minica.New()\n\t\t\trequire.NoError(t, err)\n\t\t\tsigner, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)\n\t\t\trequire.NoError(t, err)\n\t\t\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\trequire.NoError(t, err)\n\t\t\ttoken := \"token\"\n\t\t\tkeyAuth, err := KeyAuthorization(token, jwk)\n\t\t\trequire.NoError(t, err)\n\t\t\tkeyAuthSum := sha256.Sum256([]byte(keyAuth))\n\t\t\tsig, err := signer.Sign(rand.Reader, keyAuthSum[:], crypto.SHA256)\n\t\t\trequire.NoError(t, err)\n\t\t\tcborSig, err := cbor.Marshal(sig)\n\t\t\trequire.NoError(t, err)\n\t\t\tctx := NewProvisionerContext(context.Background(), mustNonAttestationProvisioner(t))\n\t\t\tmakeLeaf := func(signer crypto.Signer, serialNumber []byte) *x509.Certificate {\n\t\t\t\tleaf, err := ca.Sign(&x509.Certificate{\n\t\t\t\t\tSubject:   pkix.Name{CommonName: \"attestation cert\"},\n\t\t\t\t\tPublicKey: signer.Public(),\n\t\t\t\t\tExtraExtensions: []pkix.Extension{\n\t\t\t\t\t\t{Id: oidYubicoSerialNumber, Value: serialNumber},\n\t\t\t\t\t},\n\t\t\t\t})\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatal(err)\n\t\t\t\t}\n\t\t\t\treturn leaf\n\t\t\t}\n\t\t\trequire.NoError(t, err)\n\t\t\tserialNumber, err := asn1.Marshal(87654321)\n\t\t\trequire.NoError(t, err)\n\t\t\tleaf := makeLeaf(signer, serialNumber)\n\t\t\tattObj, err := cbor.Marshal(struct {\n\t\t\t\tFormat       string                 `json:\"fmt\"`\n\t\t\t\tAttStatement map[string]interface{} `json:\"attStmt,omitempty\"`\n\t\t\t}{\n\t\t\t\tFormat: \"bogus-format\",\n\t\t\t\tAttStatement: map[string]interface{}{\n\t\t\t\t\t\"x5c\": []interface{}{leaf.Raw, ca.Intermediate.Raw},\n\t\t\t\t\t\"alg\": -7,\n\t\t\t\t\t\"sig\": cborSig,\n\t\t\t\t},\n\t\t\t})\n\t\t\trequire.NoError(t, err)\n\t\t\tpayload, err := json.Marshal(struct {\n\t\t\t\tAttObj string `json:\"attObj\"`\n\t\t\t}{\n\t\t\t\tAttObj: base64.RawURLEncoding.EncodeToString(attObj),\n\t\t\t})\n\t\t\trequire.NoError(t, err)\n\t\t\treturn test{\n\t\t\t\targs: args{\n\t\t\t\t\tctx: ctx,\n\t\t\t\t\tch: &Challenge{\n\t\t\t\t\t\tID:              \"chID\",\n\t\t\t\t\t\tAuthorizationID: \"azID\",\n\t\t\t\t\t\tToken:           \"token\",\n\t\t\t\t\t\tType:            \"device-attest-01\",\n\t\t\t\t\t\tStatus:          StatusPending,\n\t\t\t\t\t\tValue:           \"12345678\",\n\t\t\t\t\t},\n\t\t\t\t\tpayload: payload,\n\t\t\t\t\tdb: &MockDB{\n\t\t\t\t\t\tMockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {\n\t\t\t\t\t\t\tassert.Equal(t, \"azID\", id)\n\t\t\t\t\t\t\treturn &Authorization{ID: \"azID\"}, nil\n\t\t\t\t\t\t},\n\t\t\t\t\t\tMockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {\n\t\t\t\t\t\t\tassert.Equal(t, \"chID\", updch.ID)\n\t\t\t\t\t\t\tassert.Equal(t, \"token\", updch.Token)\n\t\t\t\t\t\t\tassert.Equal(t, StatusInvalid, updch.Status)\n\t\t\t\t\t\t\tassert.Equal(t, ChallengeType(\"device-attest-01\"), updch.Type)\n\t\t\t\t\t\t\tassert.Equal(t, \"12345678\", updch.Value)\n\t\t\t\t\t\t\tassert.Equal(t, payload, updch.Payload)\n\t\t\t\t\t\t\tassert.Empty(t, updch.PayloadFormat)\n\n\t\t\t\t\t\t\terr := NewDetailedError(ErrorBadAttestationStatementType, `unsupported attestation object format \"bogus-format\"`)\n\n\t\t\t\t\t\t\tassert.EqualError(t, updch.Error.Err, err.Err.Error())\n\t\t\t\t\t\t\tassert.Equal(t, err.Type, updch.Error.Type)\n\t\t\t\t\t\t\tassert.Equal(t, err.Detail, updch.Error.Detail)\n\t\t\t\t\t\t\tassert.Equal(t, err.Status, updch.Error.Status)\n\t\t\t\t\t\t\tassert.Equal(t, err.Subproblems, updch.Error.Subproblems)\n\n\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tjwk: jwk,\n\t\t\t\t},\n\t\t\t\twantErr: nil,\n\t\t\t}\n\t\t},\n\t\t\"fail/db.UpdateAuthorization\": func(t *testing.T) test {\n\t\t\tjwk, keyAuth := mustAccountAndKeyAuthorization(t, \"token\")\n\t\t\tpayload, leaf, root := mustAttestYubikey(t, \"nonce\", keyAuth, 12345678)\n\n\t\t\tcaRoot := pem.EncodeToMemory(&pem.Block{Type: \"CERTIFICATE\", Bytes: root.Raw})\n\t\t\tctx := NewProvisionerContext(context.Background(), mustAttestationProvisioner(t, caRoot))\n\n\t\t\treturn test{\n\t\t\t\targs: args{\n\t\t\t\t\tctx: ctx,\n\t\t\t\t\tjwk: jwk,\n\t\t\t\t\tch: &Challenge{\n\t\t\t\t\t\tID:              \"chID\",\n\t\t\t\t\t\tAuthorizationID: \"azID\",\n\t\t\t\t\t\tToken:           \"token\",\n\t\t\t\t\t\tType:            \"device-attest-01\",\n\t\t\t\t\t\tStatus:          StatusPending,\n\t\t\t\t\t\tValue:           \"12345678\",\n\t\t\t\t\t},\n\t\t\t\t\tpayload: payload,\n\t\t\t\t\tdb: &MockDB{\n\t\t\t\t\t\tMockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {\n\t\t\t\t\t\t\tassert.Equal(t, \"azID\", id)\n\t\t\t\t\t\t\treturn &Authorization{ID: \"azID\"}, nil\n\t\t\t\t\t\t},\n\t\t\t\t\t\tMockUpdateAuthorization: func(ctx context.Context, az *Authorization) error {\n\t\t\t\t\t\t\tfingerprint, err := keyutil.Fingerprint(leaf.PublicKey)\n\t\t\t\t\t\t\tassert.NoError(t, err)\n\t\t\t\t\t\t\tassert.Equal(t, \"azID\", az.ID)\n\t\t\t\t\t\t\tassert.Equal(t, fingerprint, az.Fingerprint)\n\t\t\t\t\t\t\treturn errors.New(\"force\")\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\twantErr: NewError(ErrorServerInternalType, \"error updating authorization: force\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/db.UpdateChallenge\": func(t *testing.T) test {\n\t\t\tjwk, keyAuth := mustAccountAndKeyAuthorization(t, \"token\")\n\t\t\tpayload, leaf, root := mustAttestYubikey(t, \"nonce\", keyAuth, 12345678)\n\n\t\t\tcaRoot := pem.EncodeToMemory(&pem.Block{Type: \"CERTIFICATE\", Bytes: root.Raw})\n\t\t\tctx := NewProvisionerContext(context.Background(), mustAttestationProvisioner(t, caRoot))\n\n\t\t\treturn test{\n\t\t\t\targs: args{\n\t\t\t\t\tctx: ctx,\n\t\t\t\t\tjwk: jwk,\n\t\t\t\t\tch: &Challenge{\n\t\t\t\t\t\tID:              \"chID\",\n\t\t\t\t\t\tAuthorizationID: \"azID\",\n\t\t\t\t\t\tToken:           \"token\",\n\t\t\t\t\t\tType:            \"device-attest-01\",\n\t\t\t\t\t\tStatus:          StatusPending,\n\t\t\t\t\t\tValue:           \"12345678\",\n\t\t\t\t\t},\n\t\t\t\t\tpayload: payload,\n\t\t\t\t\tdb: &MockDB{\n\t\t\t\t\t\tMockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {\n\t\t\t\t\t\t\tassert.Equal(t, \"azID\", id)\n\t\t\t\t\t\t\treturn &Authorization{ID: \"azID\"}, nil\n\t\t\t\t\t\t},\n\t\t\t\t\t\tMockUpdateAuthorization: func(ctx context.Context, az *Authorization) error {\n\t\t\t\t\t\t\tfingerprint, err := keyutil.Fingerprint(leaf.PublicKey)\n\t\t\t\t\t\t\tassert.NoError(t, err)\n\t\t\t\t\t\t\tassert.Equal(t, \"azID\", az.ID)\n\t\t\t\t\t\t\tassert.Equal(t, fingerprint, az.Fingerprint)\n\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t},\n\t\t\t\t\t\tMockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {\n\t\t\t\t\t\t\tassert.Equal(t, \"chID\", updch.ID)\n\t\t\t\t\t\t\tassert.Equal(t, \"token\", updch.Token)\n\t\t\t\t\t\t\tassert.Equal(t, StatusValid, updch.Status)\n\t\t\t\t\t\t\tassert.Equal(t, ChallengeType(\"device-attest-01\"), updch.Type)\n\t\t\t\t\t\t\tassert.Equal(t, \"12345678\", updch.Value)\n\t\t\t\t\t\t\tassert.Equal(t, payload, updch.Payload)\n\t\t\t\t\t\t\tassert.Equal(t, \"step\", updch.PayloadFormat)\n\n\t\t\t\t\t\t\treturn errors.New(\"force\")\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\twantErr: NewError(ErrorServerInternalType, \"error updating challenge: force\"),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tjwk, keyAuth := mustAccountAndKeyAuthorization(t, \"token\")\n\t\t\tpayload, leaf, root := mustAttestYubikey(t, \"nonce\", keyAuth, 12345678)\n\n\t\t\tcaRoot := pem.EncodeToMemory(&pem.Block{Type: \"CERTIFICATE\", Bytes: root.Raw})\n\t\t\tctx := NewProvisionerContext(context.Background(), mustAttestationProvisioner(t, caRoot))\n\n\t\t\treturn test{\n\t\t\t\targs: args{\n\t\t\t\t\tctx: ctx,\n\t\t\t\t\tjwk: jwk,\n\t\t\t\t\tch: &Challenge{\n\t\t\t\t\t\tID:              \"chID\",\n\t\t\t\t\t\tAuthorizationID: \"azID\",\n\t\t\t\t\t\tToken:           \"token\",\n\t\t\t\t\t\tType:            \"device-attest-01\",\n\t\t\t\t\t\tStatus:          StatusPending,\n\t\t\t\t\t\tValue:           \"12345678\",\n\t\t\t\t\t},\n\t\t\t\t\tpayload: payload,\n\t\t\t\t\tdb: &MockDB{\n\t\t\t\t\t\tMockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {\n\t\t\t\t\t\t\tassert.Equal(t, \"azID\", id)\n\t\t\t\t\t\t\treturn &Authorization{ID: \"azID\"}, nil\n\t\t\t\t\t\t},\n\t\t\t\t\t\tMockUpdateAuthorization: func(ctx context.Context, az *Authorization) error {\n\t\t\t\t\t\t\tfingerprint, err := keyutil.Fingerprint(leaf.PublicKey)\n\t\t\t\t\t\t\tassert.NoError(t, err)\n\t\t\t\t\t\t\tassert.Equal(t, \"azID\", az.ID)\n\t\t\t\t\t\t\tassert.Equal(t, fingerprint, az.Fingerprint)\n\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t},\n\t\t\t\t\t\tMockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {\n\t\t\t\t\t\t\tassert.Equal(t, \"chID\", updch.ID)\n\t\t\t\t\t\t\tassert.Equal(t, \"token\", updch.Token)\n\t\t\t\t\t\t\tassert.Equal(t, StatusValid, updch.Status)\n\t\t\t\t\t\t\tassert.Equal(t, ChallengeType(\"device-attest-01\"), updch.Type)\n\t\t\t\t\t\t\tassert.Equal(t, \"12345678\", updch.Value)\n\t\t\t\t\t\t\tassert.Equal(t, payload, updch.Payload)\n\t\t\t\t\t\t\tassert.Equal(t, \"step\", updch.PayloadFormat)\n\n\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\twantErr: nil,\n\t\t\t}\n\t\t},\n\t\t\"ok/step-managed-device-id\": func(t *testing.T) test {\n\t\t\tjwk, keyAuth := mustAccountAndKeyAuthorization(t, \"token\")\n\t\t\tpayload, leaf, root := mustAttestStepManagedDeviceID(t, \"nonce\", keyAuth, \"12345678\")\n\n\t\t\tcaRoot := pem.EncodeToMemory(&pem.Block{Type: \"CERTIFICATE\", Bytes: root.Raw})\n\t\t\tctx := NewProvisionerContext(context.Background(), mustAttestationProvisioner(t, caRoot))\n\n\t\t\treturn test{\n\t\t\t\targs: args{\n\t\t\t\t\tctx: ctx,\n\t\t\t\t\tjwk: jwk,\n\t\t\t\t\tch: &Challenge{\n\t\t\t\t\t\tID:              \"chID\",\n\t\t\t\t\t\tAuthorizationID: \"azID\",\n\t\t\t\t\t\tToken:           \"token\",\n\t\t\t\t\t\tType:            \"device-attest-01\",\n\t\t\t\t\t\tStatus:          StatusPending,\n\t\t\t\t\t\tValue:           \"12345678\",\n\t\t\t\t\t},\n\t\t\t\t\tpayload: payload,\n\t\t\t\t\tdb: &MockDB{\n\t\t\t\t\t\tMockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {\n\t\t\t\t\t\t\tassert.Equal(t, \"azID\", id)\n\t\t\t\t\t\t\treturn &Authorization{ID: \"azID\"}, nil\n\t\t\t\t\t\t},\n\t\t\t\t\t\tMockUpdateAuthorization: func(ctx context.Context, az *Authorization) error {\n\t\t\t\t\t\t\tfingerprint, err := keyutil.Fingerprint(leaf.PublicKey)\n\t\t\t\t\t\t\tassert.NoError(t, err)\n\t\t\t\t\t\t\tassert.Equal(t, \"azID\", az.ID)\n\t\t\t\t\t\t\tassert.Equal(t, fingerprint, az.Fingerprint)\n\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t},\n\t\t\t\t\t\tMockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {\n\t\t\t\t\t\t\tassert.Equal(t, \"chID\", updch.ID)\n\t\t\t\t\t\t\tassert.Equal(t, \"token\", updch.Token)\n\t\t\t\t\t\t\tassert.Equal(t, StatusValid, updch.Status)\n\t\t\t\t\t\t\tassert.Equal(t, ChallengeType(\"device-attest-01\"), updch.Type)\n\t\t\t\t\t\t\tassert.Equal(t, \"12345678\", updch.Value)\n\t\t\t\t\t\t\tassert.Equal(t, payload, updch.Payload)\n\t\t\t\t\t\t\tassert.Equal(t, \"step\", updch.PayloadFormat)\n\n\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\twantErr: nil,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ttc := run(t)\n\n\t\t\tif err := deviceAttest01Validate(tc.args.ctx, tc.args.ch, tc.args.db, tc.args.jwk, tc.args.payload); err != nil {\n\t\t\t\tif assert.Error(t, tc.wantErr) {\n\t\t\t\t\tassert.ErrorContains(t, err, tc.wantErr.Error())\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.Nil(t, tc.wantErr)\n\t\t})\n\t}\n}\n\nvar (\n\toidTPMManufacturer = asn1.ObjectIdentifier{2, 23, 133, 2, 1}\n\toidTPMModel        = asn1.ObjectIdentifier{2, 23, 133, 2, 2}\n\toidTPMVersion      = asn1.ObjectIdentifier{2, 23, 133, 2, 3}\n)\n\nfunc generateValidAKCertificate(t *testing.T) *x509.Certificate {\n\tt.Helper()\n\tsigner, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)\n\trequire.NoError(t, err)\n\ttemplate := &x509.Certificate{\n\t\tPublicKey:          signer.Public(),\n\t\tVersion:            3,\n\t\tIsCA:               false,\n\t\tUnknownExtKeyUsage: []asn1.ObjectIdentifier{oidTCGKpAIKCertificate},\n\t}\n\tasn1Value := []byte(fmt.Sprintf(`{\"extraNames\":[{\"type\": %q, \"value\": %q},{\"type\": %q, \"value\": %q},{\"type\": %q, \"value\": %q}]}`, oidTPMManufacturer, \"1414747215\", oidTPMModel, \"SLB 9670 TPM2.0\", oidTPMVersion, \"7.55\"))\n\tsans := []x509util.SubjectAlternativeName{\n\t\t{Type: x509util.DirectoryNameType,\n\t\t\tASN1Value: asn1Value},\n\t}\n\text, err := createSubjectAltNameExtension(nil, nil, nil, nil, sans, true)\n\trequire.NoError(t, err)\n\text.Set(template)\n\tca, err := minica.New()\n\trequire.NoError(t, err)\n\tcert, err := ca.Sign(template)\n\trequire.NoError(t, err)\n\n\treturn cert\n}\n\nfunc Test_validateAKCertificate(t *testing.T) {\n\tcert := generateValidAKCertificate(t)\n\ttests := []struct {\n\t\tname   string\n\t\tc      *x509.Certificate\n\t\texpErr error\n\t}{\n\t\t{\n\t\t\tname:   \"ok\",\n\t\t\tc:      cert,\n\t\t\texpErr: nil,\n\t\t},\n\t\t{\n\t\t\tname: \"fail/version\",\n\t\t\tc: &x509.Certificate{\n\t\t\t\tVersion: 1,\n\t\t\t},\n\t\t\texpErr: errors.New(\"AK certificate has invalid version 1; only version 3 is allowed\"),\n\t\t},\n\t\t{\n\t\t\tname: \"fail/subject\",\n\t\t\tc: &x509.Certificate{\n\t\t\t\tVersion: 3,\n\t\t\t\tSubject: pkix.Name{CommonName: \"fail!\"},\n\t\t\t},\n\t\t\texpErr: errors.New(`AK certificate subject must be empty; got \"CN=fail!\"`),\n\t\t},\n\t\t{\n\t\t\tname: \"fail/isCA\",\n\t\t\tc: &x509.Certificate{\n\t\t\t\tVersion: 3,\n\t\t\t\tIsCA:    true,\n\t\t\t},\n\t\t\texpErr: errors.New(\"AK certificate must not be a CA\"),\n\t\t},\n\t\t{\n\t\t\tname: \"fail/extendedKeyUsage\",\n\t\t\tc: &x509.Certificate{\n\t\t\t\tVersion: 3,\n\t\t\t},\n\t\t\texpErr: errors.New(\"AK certificate is missing Extended Key Usage extension\"),\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\terr := validateAKCertificate(tt.c)\n\t\t\tif tt.expErr != nil {\n\t\t\t\tif assert.Error(t, err) {\n\t\t\t\t\tassert.EqualError(t, err, tt.expErr.Error())\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.NoError(t, err)\n\t\t})\n\t}\n}\n\nfunc Test_validateAKCertificateSubjectAlternativeNames(t *testing.T) {\n\tok := generateValidAKCertificate(t)\n\tt.Helper()\n\tsigner, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)\n\trequire.NoError(t, err)\n\n\tgetBase := func() *x509.Certificate {\n\t\treturn &x509.Certificate{\n\t\t\tPublicKey:          signer.Public(),\n\t\t\tVersion:            3,\n\t\t\tIsCA:               false,\n\t\t\tUnknownExtKeyUsage: []asn1.ObjectIdentifier{oidTCGKpAIKCertificate},\n\t\t}\n\t}\n\n\tca, err := minica.New()\n\trequire.NoError(t, err)\n\tmissingManufacturerASN1 := []byte(fmt.Sprintf(`{\"extraNames\":[{\"type\": %q, \"value\": %q},{\"type\": %q, \"value\": %q}]}`, oidTPMModel, \"SLB 9670 TPM2.0\", oidTPMVersion, \"7.55\"))\n\tsans := []x509util.SubjectAlternativeName{\n\t\t{Type: x509util.DirectoryNameType,\n\t\t\tASN1Value: missingManufacturerASN1},\n\t}\n\text, err := createSubjectAltNameExtension(nil, nil, nil, nil, sans, true)\n\trequire.NoError(t, err)\n\tmissingManufacturer := getBase()\n\text.Set(missingManufacturer)\n\n\tmissingManufacturer, err = ca.Sign(missingManufacturer)\n\trequire.NoError(t, err)\n\n\tmissingModelASN1 := []byte(fmt.Sprintf(`{\"extraNames\":[{\"type\": %q, \"value\": %q},{\"type\": %q, \"value\": %q}]}`, oidTPMManufacturer, \"1414747215\", oidTPMVersion, \"7.55\"))\n\tsans = []x509util.SubjectAlternativeName{\n\t\t{Type: x509util.DirectoryNameType,\n\t\t\tASN1Value: missingModelASN1},\n\t}\n\text, err = createSubjectAltNameExtension(nil, nil, nil, nil, sans, true)\n\trequire.NoError(t, err)\n\tmissingModel := getBase()\n\text.Set(missingModel)\n\n\tmissingModel, err = ca.Sign(missingModel)\n\trequire.NoError(t, err)\n\n\tmissingFirmwareVersionASN1 := []byte(fmt.Sprintf(`{\"extraNames\":[{\"type\": %q, \"value\": %q},{\"type\": %q, \"value\": %q}]}`, oidTPMManufacturer, \"1414747215\", oidTPMModel, \"SLB 9670 TPM2.0\"))\n\tsans = []x509util.SubjectAlternativeName{\n\t\t{Type: x509util.DirectoryNameType,\n\t\t\tASN1Value: missingFirmwareVersionASN1},\n\t}\n\text, err = createSubjectAltNameExtension(nil, nil, nil, nil, sans, true)\n\trequire.NoError(t, err)\n\tmissingFirmwareVersion := getBase()\n\text.Set(missingFirmwareVersion)\n\n\tmissingFirmwareVersion, err = ca.Sign(missingFirmwareVersion)\n\trequire.NoError(t, err)\n\n\ttests := []struct {\n\t\tname   string\n\t\tc      *x509.Certificate\n\t\texpErr error\n\t}{\n\t\t{\"ok\", ok, nil},\n\t\t{\"fail/missing-manufacturer\", missingManufacturer, errors.New(\"missing TPM manufacturer\")},\n\t\t{\"fail/missing-model\", missingModel, errors.New(\"missing TPM model\")},\n\t\t{\"fail/missing-firmware-version\", missingFirmwareVersion, errors.New(\"missing TPM version\")},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\terr := validateAKCertificateSubjectAlternativeNames(tt.c)\n\t\t\tif tt.expErr != nil {\n\t\t\t\tif assert.Error(t, err) {\n\t\t\t\t\tassert.EqualError(t, err, tt.expErr.Error())\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.NoError(t, err)\n\t\t})\n\t}\n}\n\nfunc Test_validateAKCertificateExtendedKeyUsage(t *testing.T) {\n\tok := generateValidAKCertificate(t)\n\tmissingEKU := &x509.Certificate{}\n\tt.Helper()\n\tsigner, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)\n\trequire.NoError(t, err)\n\ttemplate := &x509.Certificate{\n\t\tPublicKey:   signer.Public(),\n\t\tExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},\n\t}\n\tca, err := minica.New()\n\trequire.NoError(t, err)\n\twrongEKU, err := ca.Sign(template)\n\trequire.NoError(t, err)\n\temptyEKU, err := ca.Sign(&x509.Certificate{\n\t\tPublicKey: signer.Public(),\n\t\tExtraExtensions: []pkix.Extension{{\n\t\t\tId:    oidExtensionExtendedKeyUsage,\n\t\t\tValue: []byte{0x30, 0x00}, // DER: empty SEQUENCE\n\t\t}},\n\t})\n\trequire.NoError(t, err)\n\n\ttests := []struct {\n\t\tname   string\n\t\tc      *x509.Certificate\n\t\texpErr error\n\t}{\n\t\t{\"ok\", ok, nil},\n\t\t{\"fail/wrong-eku\", wrongEKU, errors.New(\"AK certificate is missing Extended Key Usage value tcg-kp-AIKCertificate (2.23.133.8.3)\")},\n\t\t{\"fail/empty-eku\", emptyEKU, errors.New(\"AK certificate is missing Extended Key Usage value tcg-kp-AIKCertificate (2.23.133.8.3)\")},\n\t\t{\"fail/missing-eku\", missingEKU, errors.New(\"AK certificate is missing Extended Key Usage extension\")},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\terr := validateAKCertificateExtendedKeyUsage(tt.c)\n\t\t\tif tt.expErr != nil {\n\t\t\t\tif assert.Error(t, err) {\n\t\t\t\t\tassert.EqualError(t, err, tt.expErr.Error())\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.NoError(t, err)\n\t\t})\n\t}\n}\n\n// createSubjectAltNameExtension will construct an Extension containing all\n// SubjectAlternativeNames held in a Certificate. It implements more types than\n// the golang x509 library, so it is used whenever OtherName or RegisteredID\n// type SANs are present in the certificate.\n//\n// See also https://datatracker.ietf.org/doc/html/rfc5280.html#section-4.2.1.6\n//\n// TODO(hs): this was copied from go.step.sm/crypto/x509util to make it easier\n// to create the SAN extension for testing purposes. Should it be exposed instead?\nfunc createSubjectAltNameExtension(dnsNames, emailAddresses x509util.MultiString, ipAddresses x509util.MultiIP, uris x509util.MultiURL, sans []x509util.SubjectAlternativeName, subjectIsEmpty bool) (x509util.Extension, error) {\n\tvar zero x509util.Extension\n\n\tvar rawValues []asn1.RawValue\n\tfor _, dnsName := range dnsNames {\n\t\trawValue, err := x509util.SubjectAlternativeName{\n\t\t\tType: x509util.DNSType, Value: dnsName,\n\t\t}.RawValue()\n\t\tif err != nil {\n\t\t\treturn zero, err\n\t\t}\n\n\t\trawValues = append(rawValues, rawValue)\n\t}\n\n\tfor _, emailAddress := range emailAddresses {\n\t\trawValue, err := x509util.SubjectAlternativeName{\n\t\t\tType: x509util.EmailType, Value: emailAddress,\n\t\t}.RawValue()\n\t\tif err != nil {\n\t\t\treturn zero, err\n\t\t}\n\n\t\trawValues = append(rawValues, rawValue)\n\t}\n\n\tfor _, ip := range ipAddresses {\n\t\trawValue, err := x509util.SubjectAlternativeName{\n\t\t\tType: x509util.IPType, Value: ip.String(),\n\t\t}.RawValue()\n\t\tif err != nil {\n\t\t\treturn zero, err\n\t\t}\n\n\t\trawValues = append(rawValues, rawValue)\n\t}\n\n\tfor _, uri := range uris {\n\t\trawValue, err := x509util.SubjectAlternativeName{\n\t\t\tType: x509util.URIType, Value: uri.String(),\n\t\t}.RawValue()\n\t\tif err != nil {\n\t\t\treturn zero, err\n\t\t}\n\n\t\trawValues = append(rawValues, rawValue)\n\t}\n\n\tfor _, san := range sans {\n\t\trawValue, err := san.RawValue()\n\t\tif err != nil {\n\t\t\treturn zero, err\n\t\t}\n\n\t\trawValues = append(rawValues, rawValue)\n\t}\n\n\t// Now marshal the rawValues into the ASN1 sequence, and create an Extension object to hold the extension\n\trawBytes, err := asn1.Marshal(rawValues)\n\tif err != nil {\n\t\treturn zero, fmt.Errorf(\"error marshaling SubjectAlternativeName extension to ASN1: %w\", err)\n\t}\n\n\treturn x509util.Extension{\n\t\tID:       x509util.ObjectIdentifier(oidSubjectAlternativeName),\n\t\tCritical: subjectIsEmpty,\n\t\tValue:    rawBytes,\n\t}, nil\n}\n\nfunc Test_tlsAlpn01ChallengeHost(t *testing.T) {\n\ttype args struct {\n\t\tname string\n\t}\n\ttests := []struct {\n\t\tname       string\n\t\tstrictFQDN bool\n\t\targs       args\n\t\twant       string\n\t}{\n\t\t{\"dns\", false, args{\"smallstep.com\"}, \"smallstep.com\"},\n\t\t{\"dns strict\", true, args{\"smallstep.com\"}, \"smallstep.com.\"},\n\t\t{\"rooted dns\", false, args{\"smallstep.com.\"}, \"smallstep.com.\"},\n\t\t{\"rooted dns strict\", true, args{\"smallstep.com.\"}, \"smallstep.com.\"},\n\t\t{\"ipv4\", true, args{\"1.2.3.4\"}, \"1.2.3.4\"},\n\t\t{\"ipv6\", true, args{\"2607:f8b0:4023:1009::71\"}, \"2607:f8b0:4023:1009::71\"},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ttmp := StrictFQDN\n\t\t\tt.Cleanup(func() {\n\t\t\t\tStrictFQDN = tmp\n\t\t\t})\n\t\t\tStrictFQDN = tt.strictFQDN\n\t\t\tassert.Equal(t, tt.want, tlsAlpn01ChallengeHost(tt.args.name))\n\t\t})\n\t}\n}\n\nfunc Test_dns01ChallengeHost(t *testing.T) {\n\ttype args struct {\n\t\tdomain string\n\t}\n\ttests := []struct {\n\t\tname       string\n\t\tstrictFQDN bool\n\t\targs       args\n\t\twant       string\n\t}{\n\t\t{\"dns\", false, args{\"smallstep.com\"}, \"_acme-challenge.smallstep.com\"},\n\t\t{\"dns strict\", true, args{\"smallstep.com\"}, \"_acme-challenge.smallstep.com.\"},\n\t\t{\"rooted dns\", false, args{\"smallstep.com.\"}, \"_acme-challenge.smallstep.com.\"},\n\t\t{\"rooted dns strict\", true, args{\"smallstep.com.\"}, \"_acme-challenge.smallstep.com.\"},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ttmp := StrictFQDN\n\t\t\tt.Cleanup(func() {\n\t\t\t\tStrictFQDN = tmp\n\t\t\t})\n\t\t\tStrictFQDN = tt.strictFQDN\n\t\t\tassert.Equal(t, tt.want, dns01ChallengeHost(tt.args.domain))\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "acme/challenge_tpmsimulator_test.go",
    "content": "//go:build tpmsimulator\n\npackage acme\n\nimport (\n\t\"context\"\n\t\"crypto\"\n\t\"crypto/sha256\"\n\t\"crypto/x509\"\n\t\"encoding/asn1\"\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"encoding/pem\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/fxamacker/cbor/v2\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/smallstep/go-attestation/attest\"\n\t\"go.step.sm/crypto/jose\"\n\t\"go.step.sm/crypto/keyutil\"\n\t\"go.step.sm/crypto/minica\"\n\t\"go.step.sm/crypto/tpm\"\n\t\"go.step.sm/crypto/tpm/simulator\"\n\ttpmstorage \"go.step.sm/crypto/tpm/storage\"\n\t\"go.step.sm/crypto/x509util\"\n)\n\nfunc newSimulatedTPM(t *testing.T) *tpm.TPM {\n\tt.Helper()\n\ttmpDir := t.TempDir()\n\ttpm, err := tpm.New(withSimulator(t), tpm.WithStore(tpmstorage.NewDirstore(tmpDir))) // TODO: provide in-memory storage implementation instead\n\trequire.NoError(t, err)\n\treturn tpm\n}\n\nfunc withSimulator(t *testing.T) tpm.NewTPMOption {\n\tt.Helper()\n\tvar sim simulator.Simulator\n\tt.Cleanup(func() {\n\t\tif sim == nil {\n\t\t\treturn\n\t\t}\n\t\terr := sim.Close()\n\t\trequire.NoError(t, err)\n\t})\n\tsim, err := simulator.New()\n\trequire.NoError(t, err)\n\terr = sim.Open()\n\trequire.NoError(t, err)\n\treturn tpm.WithSimulator(sim)\n}\n\nfunc generateKeyID(t *testing.T, pub crypto.PublicKey) []byte {\n\tt.Helper()\n\tb, err := x509.MarshalPKIXPublicKey(pub)\n\trequire.NoError(t, err)\n\thash := sha256.Sum256(b)\n\treturn hash[:]\n}\n\nfunc mustAttestTPM(t *testing.T, keyAuthorization string, permanentIdentifiers []string) ([]byte, crypto.Signer, *x509.Certificate) {\n\tt.Helper()\n\taca, err := minica.New(\n\t\tminica.WithName(\"TPM Testing\"),\n\t\tminica.WithGetSignerFunc(\n\t\t\tfunc() (crypto.Signer, error) {\n\t\t\t\treturn keyutil.GenerateSigner(\"RSA\", \"\", 2048)\n\t\t\t},\n\t\t),\n\t)\n\trequire.NoError(t, err)\n\n\t// prepare simulated TPM and create an AK\n\tstpm := newSimulatedTPM(t)\n\teks, err := stpm.GetEKs(context.Background())\n\trequire.NoError(t, err)\n\tak, err := stpm.CreateAK(context.Background(), \"first-ak\")\n\trequire.NoError(t, err)\n\trequire.NotNil(t, ak)\n\n\t// extract the AK public key // TODO(hs): replace this when there's a simpler method to get the AK public key (e.g. ak.Public())\n\tap, err := ak.AttestationParameters(context.Background())\n\trequire.NoError(t, err)\n\takp, err := attest.ParseAKPublic(attest.TPMVersion20, ap.Public)\n\trequire.NoError(t, err)\n\n\t// create template and sign certificate for the AK public key\n\tkeyID := generateKeyID(t, eks[0].Public())\n\ttemplate := &x509.Certificate{\n\t\tPublicKey:          akp.Public,\n\t\tIsCA:               false,\n\t\tUnknownExtKeyUsage: []asn1.ObjectIdentifier{oidTCGKpAIKCertificate},\n\t}\n\tsans := []x509util.SubjectAlternativeName{}\n\turis := []*url.URL{{Scheme: \"urn\", Opaque: \"ek:sha256:\" + base64.StdEncoding.EncodeToString(keyID)}}\n\tfor _, pi := range permanentIdentifiers {\n\t\tsans = append(sans, x509util.SubjectAlternativeName{\n\t\t\tType:  x509util.PermanentIdentifierType,\n\t\t\tValue: pi,\n\t\t})\n\t}\n\tasn1Value := []byte(fmt.Sprintf(`{\"extraNames\":[{\"type\": %q, \"value\": %q},{\"type\": %q, \"value\": %q},{\"type\": %q, \"value\": %q}]}`, oidTPMManufacturer, \"1414747215\", oidTPMModel, \"SLB 9670 TPM2.0\", oidTPMVersion, \"7.55\"))\n\tsans = append(sans, x509util.SubjectAlternativeName{\n\t\tType:      x509util.DirectoryNameType,\n\t\tASN1Value: asn1Value,\n\t})\n\text, err := createSubjectAltNameExtension(nil, nil, nil, uris, sans, true)\n\trequire.NoError(t, err)\n\text.Set(template)\n\takCert, err := aca.Sign(template)\n\trequire.NoError(t, err)\n\trequire.NotNil(t, akCert)\n\n\t// create a new key attested by the AK, while including\n\t// the key authorization bytes as qualifying data.\n\tkeyAuthSum := sha256.Sum256([]byte(keyAuthorization))\n\tconfig := tpm.AttestKeyConfig{\n\t\tAlgorithm:      \"RSA\",\n\t\tSize:           2048,\n\t\tQualifyingData: keyAuthSum[:],\n\t}\n\tkey, err := stpm.AttestKey(context.Background(), \"first-ak\", \"first-key\", config)\n\trequire.NoError(t, err)\n\trequire.NotNil(t, key)\n\trequire.Equal(t, \"first-key\", key.Name())\n\trequire.NotEqual(t, 0, len(key.Data()))\n\trequire.Equal(t, \"first-ak\", key.AttestedBy())\n\trequire.True(t, key.WasAttested())\n\trequire.True(t, key.WasAttestedBy(ak))\n\n\tsigner, err := key.Signer(context.Background())\n\trequire.NoError(t, err)\n\n\t// prepare the attestation object with the AK certificate chain,\n\t// the attested key, its metadata and the signature signed by the\n\t// AK.\n\tparams, err := key.CertificationParameters(context.Background())\n\trequire.NoError(t, err)\n\tattObj, err := cbor.Marshal(struct {\n\t\tFormat       string                 `json:\"fmt\"`\n\t\tAttStatement map[string]interface{} `json:\"attStmt,omitempty\"`\n\t}{\n\t\tFormat: \"tpm\",\n\t\tAttStatement: map[string]interface{}{\n\t\t\t\"ver\":      \"2.0\",\n\t\t\t\"x5c\":      []interface{}{akCert.Raw, aca.Intermediate.Raw},\n\t\t\t\"alg\":      int64(-257), // RS256\n\t\t\t\"sig\":      params.CreateSignature,\n\t\t\t\"certInfo\": params.CreateAttestation,\n\t\t\t\"pubArea\":  params.Public,\n\t\t},\n\t})\n\trequire.NoError(t, err)\n\n\t// marshal the ACME payload\n\tpayload, err := json.Marshal(struct {\n\t\tAttObj string `json:\"attObj\"`\n\t}{\n\t\tAttObj: base64.RawURLEncoding.EncodeToString(attObj),\n\t})\n\trequire.NoError(t, err)\n\n\treturn payload, signer, aca.Root\n}\n\nfunc Test_deviceAttest01ValidateWithTPMSimulator(t *testing.T) {\n\ttype args struct {\n\t\tctx     context.Context\n\t\tch      *Challenge\n\t\tdb      DB\n\t\tjwk     *jose.JSONWebKey\n\t\tpayload []byte\n\t}\n\ttype test struct {\n\t\targs    args\n\t\twantErr *Error\n\t}\n\ttests := map[string]func(t *testing.T) test{\n\t\t\"ok/doTPMAttestationFormat-storeError\": func(t *testing.T) test {\n\t\t\tjwk, keyAuth := mustAccountAndKeyAuthorization(t, \"token\")\n\t\t\tpayload, _, root := mustAttestTPM(t, keyAuth, nil) // TODO: value(s) for AK cert?\n\t\t\tcaRoot := pem.EncodeToMemory(&pem.Block{Type: \"CERTIFICATE\", Bytes: root.Raw})\n\t\t\tctx := NewProvisionerContext(context.Background(), mustAttestationProvisioner(t, caRoot))\n\n\t\t\t// parse payload, set invalid \"ver\", remarshal\n\t\t\tvar p payloadType\n\t\t\terr := json.Unmarshal(payload, &p)\n\t\t\trequire.NoError(t, err)\n\t\t\tattObj, err := base64.RawURLEncoding.DecodeString(p.AttObj)\n\t\t\trequire.NoError(t, err)\n\t\t\tatt := attestationObject{}\n\t\t\terr = cbor.Unmarshal(attObj, &att)\n\t\t\trequire.NoError(t, err)\n\t\t\tatt.AttStatement[\"ver\"] = \"bogus\"\n\t\t\tattObj, err = cbor.Marshal(struct {\n\t\t\t\tFormat       string                 `json:\"fmt\"`\n\t\t\t\tAttStatement map[string]interface{} `json:\"attStmt,omitempty\"`\n\t\t\t}{\n\t\t\t\tFormat:       \"tpm\",\n\t\t\t\tAttStatement: att.AttStatement,\n\t\t\t})\n\t\t\trequire.NoError(t, err)\n\t\t\tpayload, err = json.Marshal(struct {\n\t\t\t\tAttObj string `json:\"attObj\"`\n\t\t\t}{\n\t\t\t\tAttObj: base64.RawURLEncoding.EncodeToString(attObj),\n\t\t\t})\n\t\t\trequire.NoError(t, err)\n\t\t\treturn test{\n\t\t\t\targs: args{\n\t\t\t\t\tctx: ctx,\n\t\t\t\t\tjwk: jwk,\n\t\t\t\t\tch: &Challenge{\n\t\t\t\t\t\tID:              \"chID\",\n\t\t\t\t\t\tAuthorizationID: \"azID\",\n\t\t\t\t\t\tToken:           \"token\",\n\t\t\t\t\t\tType:            \"device-attest-01\",\n\t\t\t\t\t\tStatus:          StatusPending,\n\t\t\t\t\t\tValue:           \"device.id.12345678\",\n\t\t\t\t\t},\n\t\t\t\t\tpayload: payload,\n\t\t\t\t\tdb: &MockDB{\n\t\t\t\t\t\tMockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {\n\t\t\t\t\t\t\tassert.Equal(t, \"azID\", id)\n\t\t\t\t\t\t\treturn &Authorization{ID: \"azID\"}, nil\n\t\t\t\t\t\t},\n\t\t\t\t\t\tMockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {\n\t\t\t\t\t\t\tassert.Equal(t, \"chID\", updch.ID)\n\t\t\t\t\t\t\tassert.Equal(t, \"token\", updch.Token)\n\t\t\t\t\t\t\tassert.Equal(t, StatusInvalid, updch.Status)\n\t\t\t\t\t\t\tassert.Equal(t, ChallengeType(\"device-attest-01\"), updch.Type)\n\t\t\t\t\t\t\tassert.Equal(t, \"device.id.12345678\", updch.Value)\n\n\t\t\t\t\t\t\terr := NewDetailedError(ErrorBadAttestationStatementType, `version \"bogus\" is not supported`)\n\n\t\t\t\t\t\t\tassert.EqualError(t, updch.Error.Err, err.Err.Error())\n\t\t\t\t\t\t\tassert.Equal(t, err.Type, updch.Error.Type)\n\t\t\t\t\t\t\tassert.Equal(t, err.Detail, updch.Error.Detail)\n\t\t\t\t\t\t\tassert.Equal(t, err.Status, updch.Error.Status)\n\t\t\t\t\t\t\tassert.Equal(t, err.Subproblems, updch.Error.Subproblems)\n\n\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\twantErr: nil,\n\t\t\t}\n\t\t},\n\t\t\"ok with invalid PermanentIdentifier SAN\": func(t *testing.T) test {\n\t\t\tjwk, keyAuth := mustAccountAndKeyAuthorization(t, \"token\")\n\t\t\tpayload, _, root := mustAttestTPM(t, keyAuth, []string{\"device.id.12345678\"}) // TODO: value(s) for AK cert?\n\t\t\tcaRoot := pem.EncodeToMemory(&pem.Block{Type: \"CERTIFICATE\", Bytes: root.Raw})\n\t\t\tctx := NewProvisionerContext(context.Background(), mustAttestationProvisioner(t, caRoot))\n\t\t\treturn test{\n\t\t\t\targs: args{\n\t\t\t\t\tctx: ctx,\n\t\t\t\t\tjwk: jwk,\n\t\t\t\t\tch: &Challenge{\n\t\t\t\t\t\tID:              \"chID\",\n\t\t\t\t\t\tAuthorizationID: \"azID\",\n\t\t\t\t\t\tToken:           \"token\",\n\t\t\t\t\t\tType:            \"device-attest-01\",\n\t\t\t\t\t\tStatus:          StatusPending,\n\t\t\t\t\t\tValue:           \"device.id.99999999\",\n\t\t\t\t\t},\n\t\t\t\t\tpayload: payload,\n\t\t\t\t\tdb: &MockDB{\n\t\t\t\t\t\tMockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {\n\t\t\t\t\t\t\tassert.Equal(t, \"azID\", id)\n\t\t\t\t\t\t\treturn &Authorization{ID: \"azID\"}, nil\n\t\t\t\t\t\t},\n\t\t\t\t\t\tMockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {\n\t\t\t\t\t\t\tassert.Equal(t, \"chID\", updch.ID)\n\t\t\t\t\t\t\tassert.Equal(t, \"token\", updch.Token)\n\t\t\t\t\t\t\tassert.Equal(t, StatusInvalid, updch.Status)\n\t\t\t\t\t\t\tassert.Equal(t, ChallengeType(\"device-attest-01\"), updch.Type)\n\t\t\t\t\t\t\tassert.Equal(t, \"device.id.99999999\", updch.Value)\n\n\t\t\t\t\t\t\terr := NewDetailedError(ErrorBadAttestationStatementType, `permanent identifier does not match`).\n\t\t\t\t\t\t\t\tAddSubproblems(NewSubproblemWithIdentifier(\n\t\t\t\t\t\t\t\t\tErrorRejectedIdentifierType,\n\t\t\t\t\t\t\t\t\tIdentifier{Type: \"permanent-identifier\", Value: \"device.id.99999999\"},\n\t\t\t\t\t\t\t\t\t`challenge identifier \"device.id.99999999\" doesn't match any of the attested hardware identifiers [\"device.id.12345678\"]`,\n\t\t\t\t\t\t\t\t))\n\n\t\t\t\t\t\t\tassert.EqualError(t, updch.Error.Err, err.Err.Error())\n\t\t\t\t\t\t\tassert.Equal(t, err.Type, updch.Error.Type)\n\t\t\t\t\t\t\tassert.Equal(t, err.Detail, updch.Error.Detail)\n\t\t\t\t\t\t\tassert.Equal(t, err.Status, updch.Error.Status)\n\t\t\t\t\t\t\tassert.Equal(t, err.Subproblems, updch.Error.Subproblems)\n\n\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\twantErr: nil,\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tjwk, keyAuth := mustAccountAndKeyAuthorization(t, \"token\")\n\t\t\tpayload, signer, root := mustAttestTPM(t, keyAuth, nil) // TODO: value(s) for AK cert?\n\t\t\tcaRoot := pem.EncodeToMemory(&pem.Block{Type: \"CERTIFICATE\", Bytes: root.Raw})\n\t\t\tctx := NewProvisionerContext(context.Background(), mustAttestationProvisioner(t, caRoot))\n\t\t\treturn test{\n\t\t\t\targs: args{\n\t\t\t\t\tctx: ctx,\n\t\t\t\t\tjwk: jwk,\n\t\t\t\t\tch: &Challenge{\n\t\t\t\t\t\tID:              \"chID\",\n\t\t\t\t\t\tAuthorizationID: \"azID\",\n\t\t\t\t\t\tToken:           \"token\",\n\t\t\t\t\t\tType:            \"device-attest-01\",\n\t\t\t\t\t\tStatus:          StatusPending,\n\t\t\t\t\t\tValue:           \"device.id.12345678\",\n\t\t\t\t\t},\n\t\t\t\t\tpayload: payload,\n\t\t\t\t\tdb: &MockDB{\n\t\t\t\t\t\tMockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {\n\t\t\t\t\t\t\tassert.Equal(t, \"azID\", id)\n\t\t\t\t\t\t\treturn &Authorization{ID: \"azID\"}, nil\n\t\t\t\t\t\t},\n\t\t\t\t\t\tMockUpdateAuthorization: func(ctx context.Context, az *Authorization) error {\n\t\t\t\t\t\t\tfingerprint, err := keyutil.Fingerprint(signer.Public())\n\t\t\t\t\t\t\tassert.NoError(t, err)\n\t\t\t\t\t\t\tassert.Equal(t, \"azID\", az.ID)\n\t\t\t\t\t\t\tassert.Equal(t, fingerprint, az.Fingerprint)\n\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t},\n\t\t\t\t\t\tMockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {\n\t\t\t\t\t\t\tassert.Equal(t, \"chID\", updch.ID)\n\t\t\t\t\t\t\tassert.Equal(t, \"token\", updch.Token)\n\t\t\t\t\t\t\tassert.Equal(t, StatusValid, updch.Status)\n\t\t\t\t\t\t\tassert.Equal(t, ChallengeType(\"device-attest-01\"), updch.Type)\n\t\t\t\t\t\t\tassert.Equal(t, \"device.id.12345678\", updch.Value)\n\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\twantErr: nil,\n\t\t\t}\n\t\t},\n\t\t\"ok with PermanentIdentifier SAN\": func(t *testing.T) test {\n\t\t\tjwk, keyAuth := mustAccountAndKeyAuthorization(t, \"token\")\n\t\t\tpayload, signer, root := mustAttestTPM(t, keyAuth, []string{\"device.id.12345678\"}) // TODO: value(s) for AK cert?\n\t\t\tcaRoot := pem.EncodeToMemory(&pem.Block{Type: \"CERTIFICATE\", Bytes: root.Raw})\n\t\t\tctx := NewProvisionerContext(context.Background(), mustAttestationProvisioner(t, caRoot))\n\t\t\treturn test{\n\t\t\t\targs: args{\n\t\t\t\t\tctx: ctx,\n\t\t\t\t\tjwk: jwk,\n\t\t\t\t\tch: &Challenge{\n\t\t\t\t\t\tID:              \"chID\",\n\t\t\t\t\t\tAuthorizationID: \"azID\",\n\t\t\t\t\t\tToken:           \"token\",\n\t\t\t\t\t\tType:            \"device-attest-01\",\n\t\t\t\t\t\tStatus:          StatusPending,\n\t\t\t\t\t\tValue:           \"device.id.12345678\",\n\t\t\t\t\t},\n\t\t\t\t\tpayload: payload,\n\t\t\t\t\tdb: &MockDB{\n\t\t\t\t\t\tMockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {\n\t\t\t\t\t\t\tassert.Equal(t, \"azID\", id)\n\t\t\t\t\t\t\treturn &Authorization{ID: \"azID\"}, nil\n\t\t\t\t\t\t},\n\t\t\t\t\t\tMockUpdateAuthorization: func(ctx context.Context, az *Authorization) error {\n\t\t\t\t\t\t\tfingerprint, err := keyutil.Fingerprint(signer.Public())\n\t\t\t\t\t\t\tassert.NoError(t, err)\n\t\t\t\t\t\t\tassert.Equal(t, \"azID\", az.ID)\n\t\t\t\t\t\t\tassert.Equal(t, fingerprint, az.Fingerprint)\n\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t},\n\t\t\t\t\t\tMockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {\n\t\t\t\t\t\t\tassert.Equal(t, \"chID\", updch.ID)\n\t\t\t\t\t\t\tassert.Equal(t, \"token\", updch.Token)\n\t\t\t\t\t\t\tassert.Equal(t, StatusValid, updch.Status)\n\t\t\t\t\t\t\tassert.Equal(t, ChallengeType(\"device-attest-01\"), updch.Type)\n\t\t\t\t\t\t\tassert.Equal(t, \"device.id.12345678\", updch.Value)\n\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\twantErr: nil,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ttc := run(t)\n\n\t\t\tif err := deviceAttest01Validate(tc.args.ctx, tc.args.ch, tc.args.db, tc.args.jwk, tc.args.payload); err != nil {\n\t\t\t\tassert.Error(t, tc.wantErr)\n\t\t\t\tassert.EqualError(t, err, tc.wantErr.Error())\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.Nil(t, tc.wantErr)\n\t\t})\n\t}\n}\n\nfunc newBadAttestationStatementError(msg string) *Error {\n\treturn &Error{\n\t\tType:   \"urn:ietf:params:acme:error:badAttestationStatement\",\n\t\tStatus: 400,\n\t\tErr:    errors.New(msg),\n\t}\n}\n\nfunc newInternalServerError(msg string) *Error {\n\treturn &Error{\n\t\tType:   \"urn:ietf:params:acme:error:serverInternal\",\n\t\tStatus: 500,\n\t\tErr:    errors.New(msg),\n\t}\n}\n\nvar (\n\toidPermanentIdentifier          = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 8, 3}\n\toidHardwareModuleNameIdentifier = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 8, 4}\n)\n\nfunc Test_doTPMAttestationFormat(t *testing.T) {\n\tctx := context.Background()\n\taca, err := minica.New(\n\t\tminica.WithName(\"TPM Testing\"),\n\t\tminica.WithGetSignerFunc(\n\t\t\tfunc() (crypto.Signer, error) {\n\t\t\t\treturn keyutil.GenerateSigner(\"RSA\", \"\", 2048)\n\t\t\t},\n\t\t),\n\t)\n\trequire.NoError(t, err)\n\tacaRoot := pem.EncodeToMemory(&pem.Block{Type: \"CERTIFICATE\", Bytes: aca.Root.Raw})\n\n\t// prepare simulated TPM and create an AK\n\tstpm := newSimulatedTPM(t)\n\teks, err := stpm.GetEKs(context.Background())\n\trequire.NoError(t, err)\n\tak, err := stpm.CreateAK(context.Background(), \"first-ak\")\n\trequire.NoError(t, err)\n\trequire.NotNil(t, ak)\n\n\t// extract the AK public key // TODO(hs): replace this when there's a simpler method to get the AK public key (e.g. ak.Public())\n\tap, err := ak.AttestationParameters(context.Background())\n\trequire.NoError(t, err)\n\takp, err := attest.ParseAKPublic(attest.TPMVersion20, ap.Public)\n\trequire.NoError(t, err)\n\n\t// create template and sign certificate for the AK public key\n\tkeyID := generateKeyID(t, eks[0].Public())\n\ttemplate := &x509.Certificate{\n\t\tPublicKey:          akp.Public,\n\t\tIsCA:               false,\n\t\tUnknownExtKeyUsage: []asn1.ObjectIdentifier{oidTCGKpAIKCertificate},\n\t}\n\tsans := []x509util.SubjectAlternativeName{}\n\turis := []*url.URL{{Scheme: \"urn\", Opaque: \"ek:sha256:\" + base64.StdEncoding.EncodeToString(keyID)}}\n\tasn1Value := []byte(fmt.Sprintf(`{\"extraNames\":[{\"type\": %q, \"value\": %q},{\"type\": %q, \"value\": %q},{\"type\": %q, \"value\": %q}]}`, oidTPMManufacturer, \"1414747215\", oidTPMModel, \"SLB 9670 TPM2.0\", oidTPMVersion, \"7.55\"))\n\tsans = append(sans, x509util.SubjectAlternativeName{\n\t\tType:      x509util.DirectoryNameType,\n\t\tASN1Value: asn1Value,\n\t})\n\text, err := createSubjectAltNameExtension(nil, nil, nil, uris, sans, true)\n\trequire.NoError(t, err)\n\text.Set(template)\n\takCert, err := aca.Sign(template)\n\trequire.NoError(t, err)\n\trequire.NotNil(t, akCert)\n\n\tinvalidTemplate := &x509.Certificate{\n\t\tPublicKey:          akp.Public,\n\t\tIsCA:               false,\n\t\tUnknownExtKeyUsage: []asn1.ObjectIdentifier{oidTCGKpAIKCertificate},\n\t}\n\tinvalidAKCert, err := aca.Sign(invalidTemplate)\n\trequire.NoError(t, err)\n\trequire.NotNil(t, invalidAKCert)\n\n\t// generate a JWK and the key authorization value\n\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\trequire.NoError(t, err)\n\tkeyAuthorization, err := KeyAuthorization(\"token\", jwk)\n\trequire.NoError(t, err)\n\n\t// create a new key attested by the AK, while including\n\t// the key authorization bytes as qualifying data.\n\tkeyAuthSum := sha256.Sum256([]byte(keyAuthorization))\n\tconfig := tpm.AttestKeyConfig{\n\t\tAlgorithm:      \"RSA\",\n\t\tSize:           2048,\n\t\tQualifyingData: keyAuthSum[:],\n\t}\n\tkey, err := stpm.AttestKey(context.Background(), \"first-ak\", \"first-key\", config)\n\trequire.NoError(t, err)\n\trequire.NotNil(t, key)\n\tparams, err := key.CertificationParameters(context.Background())\n\trequire.NoError(t, err)\n\n\tsigner, err := key.Signer(context.Background())\n\trequire.NoError(t, err)\n\tfingerprint, err := keyutil.Fingerprint(signer.Public())\n\trequire.NoError(t, err)\n\n\t// attest another key and get its certification parameters\n\tanotherKey, err := stpm.AttestKey(context.Background(), \"first-ak\", \"another-key\", config)\n\trequire.NoError(t, err)\n\trequire.NotNil(t, key)\n\tanotherKeyParams, err := anotherKey.CertificationParameters(context.Background())\n\trequire.NoError(t, err)\n\n\ttype args struct {\n\t\tctx  context.Context\n\t\tprov Provisioner\n\t\tch   *Challenge\n\t\tjwk  *jose.JSONWebKey\n\t\tatt  *attestationObject\n\t}\n\ttests := []struct {\n\t\tname   string\n\t\targs   args\n\t\twant   *tpmAttestationData\n\t\texpErr *Error\n\t}{\n\t\t{\"ok\", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: \"token\"}, jwk, &attestationObject{\n\t\t\tFormat: \"tpm\",\n\t\t\tAttStatement: map[string]interface{}{\n\t\t\t\t\"ver\":      \"2.0\",\n\t\t\t\t\"x5c\":      []interface{}{akCert.Raw, aca.Intermediate.Raw},\n\t\t\t\t\"alg\":      int64(-257), // RS256\n\t\t\t\t\"sig\":      params.CreateSignature,\n\t\t\t\t\"certInfo\": params.CreateAttestation,\n\t\t\t\t\"pubArea\":  params.Public,\n\t\t\t},\n\t\t}}, nil, nil},\n\t\t{\"fail ver not present\", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: \"token\"}, jwk, &attestationObject{\n\t\t\tFormat: \"tpm\",\n\t\t\tAttStatement: map[string]interface{}{\n\t\t\t\t\"x5c\":      []interface{}{akCert.Raw, aca.Intermediate.Raw},\n\t\t\t\t\"alg\":      int64(-257), // RS256\n\t\t\t\t\"sig\":      params.CreateSignature,\n\t\t\t\t\"certInfo\": params.CreateAttestation,\n\t\t\t\t\"pubArea\":  params.Public,\n\t\t\t},\n\t\t}}, nil, newBadAttestationStatementError(\"ver not present\")},\n\t\t{\"fail ver type\", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: \"token\"}, jwk, &attestationObject{\n\t\t\tFormat: \"tpm\",\n\t\t\tAttStatement: map[string]interface{}{\n\t\t\t\t\"ver\":      []interface{}{},\n\t\t\t\t\"x5c\":      []interface{}{akCert.Raw, aca.Intermediate.Raw},\n\t\t\t\t\"alg\":      int64(-257), // RS256\n\t\t\t\t\"sig\":      params.CreateSignature,\n\t\t\t\t\"certInfo\": params.CreateAttestation,\n\t\t\t\t\"pubArea\":  params.Public,\n\t\t\t},\n\t\t}}, nil, newBadAttestationStatementError(\"ver not present\")},\n\t\t{\"fail bogus ver\", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: \"token\"}, jwk, &attestationObject{\n\t\t\tFormat: \"tpm\",\n\t\t\tAttStatement: map[string]interface{}{\n\t\t\t\t\"ver\":      \"bogus\",\n\t\t\t\t\"x5c\":      []interface{}{akCert.Raw, aca.Intermediate.Raw},\n\t\t\t\t\"alg\":      int64(-257), // RS256\n\t\t\t\t\"sig\":      params.CreateSignature,\n\t\t\t\t\"certInfo\": params.CreateAttestation,\n\t\t\t\t\"pubArea\":  params.Public,\n\t\t\t},\n\t\t}}, nil, newBadAttestationStatementError(`version \"bogus\" is not supported`)},\n\t\t{\"fail x5c not present\", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: \"token\"}, jwk, &attestationObject{\n\t\t\tFormat: \"tpm\",\n\t\t\tAttStatement: map[string]interface{}{\n\t\t\t\t\"ver\":      \"2.0\",\n\t\t\t\t\"alg\":      int64(-257), // RS256\n\t\t\t\t\"sig\":      params.CreateSignature,\n\t\t\t\t\"certInfo\": params.CreateAttestation,\n\t\t\t\t\"pubArea\":  params.Public,\n\t\t\t},\n\t\t}}, nil, newBadAttestationStatementError(\"x5c not present\")},\n\t\t{\"fail x5c type\", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: \"token\"}, jwk, &attestationObject{\n\t\t\tFormat: \"tpm\",\n\t\t\tAttStatement: map[string]interface{}{\n\t\t\t\t\"ver\":      \"2.0\",\n\t\t\t\t\"x5c\":      [][]byte{akCert.Raw, aca.Intermediate.Raw},\n\t\t\t\t\"alg\":      int64(-257), // RS256\n\t\t\t\t\"sig\":      params.CreateSignature,\n\t\t\t\t\"certInfo\": params.CreateAttestation,\n\t\t\t\t\"pubArea\":  params.Public,\n\t\t\t},\n\t\t}}, nil, newBadAttestationStatementError(\"x5c not present\")},\n\t\t{\"fail x5c empty\", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: \"token\"}, jwk, &attestationObject{\n\t\t\tFormat: \"tpm\",\n\t\t\tAttStatement: map[string]interface{}{\n\t\t\t\t\"ver\":      \"2.0\",\n\t\t\t\t\"x5c\":      []interface{}{},\n\t\t\t\t\"alg\":      int64(-257), // RS256\n\t\t\t\t\"sig\":      params.CreateSignature,\n\t\t\t\t\"certInfo\": params.CreateAttestation,\n\t\t\t\t\"pubArea\":  params.Public,\n\t\t\t},\n\t\t}}, nil, newBadAttestationStatementError(\"x5c is empty\")},\n\t\t{\"fail leaf type\", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: \"token\"}, jwk, &attestationObject{\n\t\t\tFormat: \"step\",\n\t\t\tAttStatement: map[string]interface{}{\n\t\t\t\t\"ver\":      \"2.0\",\n\t\t\t\t\"x5c\":      []interface{}{\"leaf\", aca.Intermediate.Raw},\n\t\t\t\t\"alg\":      int64(-257), // RS256\n\t\t\t\t\"sig\":      params.CreateSignature,\n\t\t\t\t\"certInfo\": params.CreateAttestation,\n\t\t\t\t\"pubArea\":  params.Public,\n\t\t\t},\n\t\t}}, nil, newBadAttestationStatementError(\"x5c is malformed\")},\n\t\t{\"fail leaf parse\", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: \"token\"}, jwk, &attestationObject{\n\t\t\tFormat: \"step\",\n\t\t\tAttStatement: map[string]interface{}{\n\t\t\t\t\"ver\":      \"2.0\",\n\t\t\t\t\"x5c\":      []interface{}{akCert.Raw[:100], aca.Intermediate.Raw},\n\t\t\t\t\"alg\":      int64(-257), // RS256\n\t\t\t\t\"sig\":      params.CreateSignature,\n\t\t\t\t\"certInfo\": params.CreateAttestation,\n\t\t\t\t\"pubArea\":  params.Public,\n\t\t\t},\n\t\t}}, nil, newBadAttestationStatementError(\"x5c is malformed: x509: malformed certificate\")},\n\t\t{\"fail intermediate type\", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: \"token\"}, jwk, &attestationObject{\n\t\t\tFormat: \"step\",\n\t\t\tAttStatement: map[string]interface{}{\n\t\t\t\t\"ver\":      \"2.0\",\n\t\t\t\t\"x5c\":      []interface{}{akCert.Raw, \"intermediate\"},\n\t\t\t\t\"alg\":      int64(-257), // RS256\n\t\t\t\t\"sig\":      params.CreateSignature,\n\t\t\t\t\"certInfo\": params.CreateAttestation,\n\t\t\t\t\"pubArea\":  params.Public,\n\t\t\t},\n\t\t}}, nil, newBadAttestationStatementError(\"x5c is malformed\")},\n\t\t{\"fail intermediate parse\", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: \"token\"}, jwk, &attestationObject{\n\t\t\tFormat: \"step\",\n\t\t\tAttStatement: map[string]interface{}{\n\t\t\t\t\"ver\":      \"2.0\",\n\t\t\t\t\"x5c\":      []interface{}{akCert.Raw, aca.Intermediate.Raw[:100]},\n\t\t\t\t\"alg\":      int64(-257), // RS256\n\t\t\t\t\"sig\":      params.CreateSignature,\n\t\t\t\t\"certInfo\": params.CreateAttestation,\n\t\t\t\t\"pubArea\":  params.Public,\n\t\t\t},\n\t\t}}, nil, newBadAttestationStatementError(\"x5c is malformed: x509: malformed certificate\")},\n\t\t{\"fail roots\", args{ctx, mustAttestationProvisioner(t, nil), &Challenge{Token: \"token\"}, jwk, &attestationObject{\n\t\t\tFormat: \"tpm\",\n\t\t\tAttStatement: map[string]interface{}{\n\t\t\t\t\"ver\":      \"2.0\",\n\t\t\t\t\"x5c\":      []interface{}{akCert.Raw, aca.Intermediate.Raw},\n\t\t\t\t\"alg\":      int64(-257), // RS256\n\t\t\t\t\"sig\":      params.CreateSignature,\n\t\t\t\t\"certInfo\": params.CreateAttestation,\n\t\t\t\t\"pubArea\":  params.Public,\n\t\t\t},\n\t\t}}, nil, newInternalServerError(\"no root CA bundle available to verify the attestation certificate\")},\n\t\t{\"fail verify\", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: \"token\"}, jwk, &attestationObject{\n\t\t\tFormat: \"step\",\n\t\t\tAttStatement: map[string]interface{}{\n\t\t\t\t\"ver\":      \"2.0\",\n\t\t\t\t\"x5c\":      []interface{}{akCert.Raw},\n\t\t\t\t\"alg\":      int64(-257), // RS256\n\t\t\t\t\"sig\":      params.CreateSignature,\n\t\t\t\t\"certInfo\": params.CreateAttestation,\n\t\t\t\t\"pubArea\":  params.Public,\n\t\t\t},\n\t\t}}, nil, newBadAttestationStatementError(\"x5c is not valid: x509: certificate signed by unknown authority\")},\n\t\t{\"fail validateAKCertificate\", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: \"token\"}, jwk, &attestationObject{\n\t\t\tFormat: \"tpm\",\n\t\t\tAttStatement: map[string]interface{}{\n\t\t\t\t\"ver\":      \"2.0\",\n\t\t\t\t\"x5c\":      []interface{}{invalidAKCert.Raw, aca.Intermediate.Raw},\n\t\t\t\t\"alg\":      int64(-257), // RS256\n\t\t\t\t\"sig\":      params.CreateSignature,\n\t\t\t\t\"certInfo\": params.CreateAttestation,\n\t\t\t\t\"pubArea\":  params.Public,\n\t\t\t},\n\t\t}}, nil, newBadAttestationStatementError(\"AK certificate is not valid: missing TPM manufacturer\")},\n\t\t{\"fail pubArea not present\", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: \"token\"}, jwk, &attestationObject{\n\t\t\tFormat: \"tpm\",\n\t\t\tAttStatement: map[string]interface{}{\n\t\t\t\t\"ver\":      \"2.0\",\n\t\t\t\t\"x5c\":      []interface{}{akCert.Raw, aca.Intermediate.Raw},\n\t\t\t\t\"alg\":      int64(-257), // RS256\n\t\t\t\t\"sig\":      params.CreateSignature,\n\t\t\t\t\"certInfo\": params.CreateAttestation,\n\t\t\t},\n\t\t}}, nil, newBadAttestationStatementError(\"invalid pubArea in attestation statement\")},\n\t\t{\"fail pubArea type\", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: \"token\"}, jwk, &attestationObject{\n\t\t\tFormat: \"tpm\",\n\t\t\tAttStatement: map[string]interface{}{\n\t\t\t\t\"ver\":      \"2.0\",\n\t\t\t\t\"x5c\":      []interface{}{akCert.Raw, aca.Intermediate.Raw},\n\t\t\t\t\"alg\":      int64(-257), // RS256\n\t\t\t\t\"sig\":      params.CreateSignature,\n\t\t\t\t\"certInfo\": params.CreateAttestation,\n\t\t\t\t\"pubArea\":  []interface{}{},\n\t\t\t},\n\t\t}}, nil, newBadAttestationStatementError(\"invalid pubArea in attestation statement\")},\n\t\t{\"fail pubArea empty\", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: \"token\"}, jwk, &attestationObject{\n\t\t\tFormat: \"tpm\",\n\t\t\tAttStatement: map[string]interface{}{\n\t\t\t\t\"ver\":      \"2.0\",\n\t\t\t\t\"x5c\":      []interface{}{akCert.Raw, aca.Intermediate.Raw},\n\t\t\t\t\"alg\":      int64(-257), // RS256\n\t\t\t\t\"sig\":      params.CreateSignature,\n\t\t\t\t\"certInfo\": params.CreateAttestation,\n\t\t\t\t\"pubArea\":  []byte{},\n\t\t\t},\n\t\t}}, nil, newBadAttestationStatementError(\"pubArea is empty\")},\n\t\t{\"fail sig not present\", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: \"token\"}, jwk, &attestationObject{\n\t\t\tFormat: \"tpm\",\n\t\t\tAttStatement: map[string]interface{}{\n\t\t\t\t\"ver\":      \"2.0\",\n\t\t\t\t\"x5c\":      []interface{}{akCert.Raw, aca.Intermediate.Raw},\n\t\t\t\t\"alg\":      int64(-257), // RS256\n\t\t\t\t\"certInfo\": params.CreateAttestation,\n\t\t\t\t\"pubArea\":  params.Public,\n\t\t\t},\n\t\t}}, nil, newBadAttestationStatementError(\"invalid sig in attestation statement\")},\n\t\t{\"fail sig type\", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: \"token\"}, jwk, &attestationObject{\n\t\t\tFormat: \"tpm\",\n\t\t\tAttStatement: map[string]interface{}{\n\t\t\t\t\"ver\":      \"2.0\",\n\t\t\t\t\"x5c\":      []interface{}{akCert.Raw, aca.Intermediate.Raw},\n\t\t\t\t\"alg\":      int64(-257), // RS256\n\t\t\t\t\"sig\":      []interface{}{},\n\t\t\t\t\"certInfo\": params.CreateAttestation,\n\t\t\t\t\"pubArea\":  params.Public,\n\t\t\t},\n\t\t}}, nil, newBadAttestationStatementError(\"invalid sig in attestation statement\")},\n\t\t{\"fail sig empty\", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: \"token\"}, jwk, &attestationObject{\n\t\t\tFormat: \"tpm\",\n\t\t\tAttStatement: map[string]interface{}{\n\t\t\t\t\"ver\":      \"2.0\",\n\t\t\t\t\"x5c\":      []interface{}{akCert.Raw, aca.Intermediate.Raw},\n\t\t\t\t\"alg\":      int64(-257), // RS256\n\t\t\t\t\"sig\":      []byte{},\n\t\t\t\t\"certInfo\": params.CreateAttestation,\n\t\t\t\t\"pubArea\":  params.Public,\n\t\t\t},\n\t\t}}, nil, newBadAttestationStatementError(\"sig is empty\")},\n\t\t{\"fail certInfo not present\", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: \"token\"}, jwk, &attestationObject{\n\t\t\tFormat: \"tpm\",\n\t\t\tAttStatement: map[string]interface{}{\n\t\t\t\t\"ver\":     \"2.0\",\n\t\t\t\t\"x5c\":     []interface{}{akCert.Raw, aca.Intermediate.Raw},\n\t\t\t\t\"alg\":     int64(-257), // RS256\n\t\t\t\t\"sig\":     params.CreateSignature,\n\t\t\t\t\"pubArea\": params.Public,\n\t\t\t},\n\t\t}}, nil, newBadAttestationStatementError(\"invalid certInfo in attestation statement\")},\n\t\t{\"fail certInfo type\", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: \"token\"}, jwk, &attestationObject{\n\t\t\tFormat: \"tpm\",\n\t\t\tAttStatement: map[string]interface{}{\n\t\t\t\t\"ver\":      \"2.0\",\n\t\t\t\t\"x5c\":      []interface{}{akCert.Raw, aca.Intermediate.Raw},\n\t\t\t\t\"alg\":      int64(-257), // RS256\n\t\t\t\t\"sig\":      params.CreateSignature,\n\t\t\t\t\"certInfo\": []interface{}{},\n\t\t\t\t\"pubArea\":  params.Public,\n\t\t\t},\n\t\t}}, nil, newBadAttestationStatementError(\"invalid certInfo in attestation statement\")},\n\t\t{\"fail certInfo empty\", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: \"token\"}, jwk, &attestationObject{\n\t\t\tFormat: \"tpm\",\n\t\t\tAttStatement: map[string]interface{}{\n\t\t\t\t\"ver\":      \"2.0\",\n\t\t\t\t\"x5c\":      []interface{}{akCert.Raw, aca.Intermediate.Raw},\n\t\t\t\t\"alg\":      int64(-257), // RS256\n\t\t\t\t\"sig\":      params.CreateSignature,\n\t\t\t\t\"certInfo\": []byte{},\n\t\t\t\t\"pubArea\":  params.Public,\n\t\t\t},\n\t\t}}, nil, newBadAttestationStatementError(\"certInfo is empty\")},\n\t\t{\"fail alg not present\", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: \"token\"}, jwk, &attestationObject{\n\t\t\tFormat: \"tpm\",\n\t\t\tAttStatement: map[string]interface{}{\n\t\t\t\t\"ver\":      \"2.0\",\n\t\t\t\t\"x5c\":      []interface{}{akCert.Raw, aca.Intermediate.Raw},\n\t\t\t\t\"sig\":      params.CreateSignature,\n\t\t\t\t\"certInfo\": params.CreateAttestation,\n\t\t\t\t\"pubArea\":  params.Public,\n\t\t\t},\n\t\t}}, nil, newBadAttestationStatementError(\"invalid alg in attestation statement\")},\n\t\t{\"fail alg type\", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: \"token\"}, jwk, &attestationObject{\n\t\t\tFormat: \"tpm\",\n\t\t\tAttStatement: map[string]interface{}{\n\t\t\t\t\"ver\":      \"2.0\",\n\t\t\t\t\"x5c\":      []interface{}{akCert.Raw, aca.Intermediate.Raw},\n\t\t\t\t\"alg\":      int64(0), // invalid alg\n\t\t\t\t\"sig\":      params.CreateSignature,\n\t\t\t\t\"certInfo\": params.CreateAttestation,\n\t\t\t\t\"pubArea\":  params.Public,\n\t\t\t},\n\t\t}}, nil, newBadAttestationStatementError(\"invalid alg 0 in attestation statement\")},\n\t\t{\"fail attestation verification\", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: \"token\"}, jwk, &attestationObject{\n\t\t\tFormat: \"tpm\",\n\t\t\tAttStatement: map[string]interface{}{\n\t\t\t\t\"ver\":      \"2.0\",\n\t\t\t\t\"x5c\":      []interface{}{akCert.Raw, aca.Intermediate.Raw},\n\t\t\t\t\"alg\":      int64(-257), // RS256\n\t\t\t\t\"sig\":      params.CreateSignature,\n\t\t\t\t\"certInfo\": params.CreateAttestation,\n\t\t\t\t\"pubArea\":  anotherKeyParams.Public,\n\t\t\t},\n\t\t}}, nil, newBadAttestationStatementError(\"invalid certification parameters: certification refers to a different key\")},\n\t\t{\"fail keyAuthorization\", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: \"token\"}, &jose.JSONWebKey{Key: []byte(\"not an asymmetric key\")}, &attestationObject{\n\t\t\tFormat: \"tpm\",\n\t\t\tAttStatement: map[string]interface{}{\n\t\t\t\t\"ver\":      \"2.0\",\n\t\t\t\t\"x5c\":      []interface{}{akCert.Raw, aca.Intermediate.Raw},\n\t\t\t\t\"alg\":      int64(-257), // RS256\n\t\t\t\t\"sig\":      params.CreateSignature,\n\t\t\t\t\"certInfo\": params.CreateAttestation,\n\t\t\t\t\"pubArea\":  params.Public,\n\t\t\t},\n\t\t}}, nil, newInternalServerError(\"failed creating key auth digest: error generating JWK thumbprint: go-jose/go-jose: unknown key type '[]uint8'\")},\n\t\t{\"fail different keyAuthorization\", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: \"aDifferentToken\"}, jwk, &attestationObject{\n\t\t\tFormat: \"tpm\",\n\t\t\tAttStatement: map[string]interface{}{\n\t\t\t\t\"ver\":      \"2.0\",\n\t\t\t\t\"x5c\":      []interface{}{akCert.Raw, aca.Intermediate.Raw},\n\t\t\t\t\"alg\":      int64(-257), //\n\t\t\t\t\"sig\":      params.CreateSignature,\n\t\t\t\t\"certInfo\": params.CreateAttestation,\n\t\t\t\t\"pubArea\":  params.Public,\n\t\t\t},\n\t\t}}, nil, newBadAttestationStatementError(\"key authorization invalid\")},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := doTPMAttestationFormat(tt.args.ctx, tt.args.prov, tt.args.ch, tt.args.jwk, tt.args.att)\n\t\t\tif tt.expErr != nil {\n\t\t\t\tvar ae *Error\n\t\t\t\tif assert.True(t, errors.As(err, &ae)) {\n\t\t\t\t\tassert.EqualError(t, err, tt.expErr.Error())\n\t\t\t\t\tassert.Equal(t, ae.StatusCode(), tt.expErr.StatusCode())\n\t\t\t\t\tassert.Equal(t, ae.Type, tt.expErr.Type)\n\t\t\t\t}\n\t\t\t\tassert.Nil(t, got)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.NoError(t, err)\n\t\t\tif assert.NotNil(t, got) {\n\t\t\t\tassert.Equal(t, akCert, got.Certificate)\n\t\t\t\tassert.Equal(t, [][]*x509.Certificate{\n\t\t\t\t\t{\n\t\t\t\t\t\takCert, aca.Intermediate, aca.Root,\n\t\t\t\t\t},\n\t\t\t\t}, got.VerifiedChains)\n\t\t\t\tassert.Equal(t, fingerprint, got.Fingerprint)\n\t\t\t\tassert.Empty(t, got.PermanentIdentifiers) // currently expected to be always empty\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "acme/challenge_wire_test.go",
    "content": "package acme\n\nimport (\n\t\"context\"\n\t\"crypto\"\n\t\"crypto/ed25519\"\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"encoding/pem\"\n\t\"errors\"\n\t\"net/http/httptest\"\n\t\"strconv\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/smallstep/certificates/acme/wire\"\n\t\"github.com/smallstep/certificates/authority/provisioner\"\n\twireprovisioner \"github.com/smallstep/certificates/authority/provisioner/wire\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"go.step.sm/crypto/jose\"\n\t\"go.step.sm/crypto/pemutil\"\n)\n\nfunc Test_wireDPOP01Validate(t *testing.T) {\n\tfakeKey := `-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEA5c+4NKZSNQcR1T8qN6SjwgdPZQ0Ge12Ylx/YeGAJ35k=\n-----END PUBLIC KEY-----`\n\ttype test struct {\n\t\tch          *Challenge\n\t\tjwk         *jose.JSONWebKey\n\t\tdb          WireDB\n\t\tpayload     []byte\n\t\tctx         context.Context\n\t\texpectedErr *Error\n\t}\n\ttests := map[string]func(t *testing.T) test{\n\t\t\"fail/no-provisioner\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tctx: context.Background(),\n\t\t\t\tdb:  &MockWireDB{},\n\t\t\t\texpectedErr: &Error{\n\t\t\t\t\tType:   \"urn:ietf:params:acme:error:serverInternal\",\n\t\t\t\t\tDetail: \"The server experienced an internal error\",\n\t\t\t\t\tStatus: 500,\n\t\t\t\t\tErr:    errors.New(\"missing provisioner\"),\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"fail/no-linker\": func(t *testing.T) test {\n\t\t\tctx := NewProvisionerContext(context.Background(), newWireProvisionerWithOptions(t, &provisioner.Options{\n\t\t\t\tWire: &wireprovisioner.Options{\n\t\t\t\t\tOIDC: &wireprovisioner.OIDCOptions{\n\t\t\t\t\t\tProvider: &wireprovisioner.Provider{\n\t\t\t\t\t\t\tIssuerURL:  \"https://issuer.example.com\",\n\t\t\t\t\t\t\tAlgorithms: []string{\"ES256\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tConfig: &wireprovisioner.Config{\n\t\t\t\t\t\t\tClientID:            \"test\",\n\t\t\t\t\t\t\tSignatureAlgorithms: []string{\"ES256\"},\n\t\t\t\t\t\t\tNow:                 time.Now,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tTransformTemplate: \"\",\n\t\t\t\t\t},\n\t\t\t\t\tDPOP: &wireprovisioner.DPOPOptions{\n\t\t\t\t\t\tSigningKey: []byte(fakeKey),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}))\n\t\t\treturn test{\n\t\t\t\tctx: ctx,\n\t\t\t\tdb:  &MockWireDB{},\n\t\t\t\texpectedErr: &Error{\n\t\t\t\t\tType:   \"urn:ietf:params:acme:error:serverInternal\",\n\t\t\t\t\tDetail: \"The server experienced an internal error\",\n\t\t\t\t\tStatus: 500,\n\t\t\t\t\tErr:    errors.New(\"missing linker\"),\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"fail/unmarshal\": func(t *testing.T) test {\n\t\t\tctx := NewProvisionerContext(context.Background(), newWireProvisionerWithOptions(t, &provisioner.Options{\n\t\t\t\tWire: &wireprovisioner.Options{\n\t\t\t\t\tOIDC: &wireprovisioner.OIDCOptions{\n\t\t\t\t\t\tProvider: &wireprovisioner.Provider{\n\t\t\t\t\t\t\tIssuerURL:  \"https://issuer.example.com\",\n\t\t\t\t\t\t\tAlgorithms: []string{\"ES256\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tConfig: &wireprovisioner.Config{\n\t\t\t\t\t\t\tClientID:            \"test\",\n\t\t\t\t\t\t\tSignatureAlgorithms: []string{\"ES256\"},\n\t\t\t\t\t\t\tNow:                 time.Now,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tTransformTemplate: \"\",\n\t\t\t\t\t},\n\t\t\t\t\tDPOP: &wireprovisioner.DPOPOptions{\n\t\t\t\t\t\tSigningKey: []byte(fakeKey),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}))\n\t\t\tctx = NewLinkerContext(ctx, NewLinker(\"ca.example.com\", \"acme\"))\n\t\t\treturn test{\n\t\t\t\tctx:     ctx,\n\t\t\t\tpayload: []byte(\"?!\"),\n\t\t\t\tch: &Challenge{\n\t\t\t\t\tID:              \"chID\",\n\t\t\t\t\tAuthorizationID: \"azID\",\n\t\t\t\t\tAccountID:       \"accID\",\n\t\t\t\t\tToken:           \"token\",\n\t\t\t\t\tType:            \"wire-dpop-01\",\n\t\t\t\t\tStatus:          StatusPending,\n\t\t\t\t\tValue:           \"1234\",\n\t\t\t\t},\n\t\t\t\tdb: &MockWireDB{},\n\t\t\t\texpectedErr: &Error{\n\t\t\t\t\tType:   \"urn:ietf:params:acme:error:malformed\",\n\t\t\t\t\tDetail: \"The request message was malformed\",\n\t\t\t\t\tStatus: 400,\n\t\t\t\t\tErr:    errors.New(`error unmarshalling Wire DPoP challenge payload: invalid character '?' looking for beginning of value`),\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"fail/wire-parse-id\": func(t *testing.T) test {\n\t\t\tctx := NewProvisionerContext(context.Background(), newWireProvisionerWithOptions(t, &provisioner.Options{\n\t\t\t\tWire: &wireprovisioner.Options{\n\t\t\t\t\tOIDC: &wireprovisioner.OIDCOptions{\n\t\t\t\t\t\tProvider: &wireprovisioner.Provider{\n\t\t\t\t\t\t\tIssuerURL:  \"https://issuer.example.com\",\n\t\t\t\t\t\t\tAlgorithms: []string{\"ES256\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tConfig: &wireprovisioner.Config{\n\t\t\t\t\t\t\tClientID:            \"test\",\n\t\t\t\t\t\t\tSignatureAlgorithms: []string{\"ES256\"},\n\t\t\t\t\t\t\tNow:                 time.Now,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tTransformTemplate: \"\",\n\t\t\t\t\t},\n\t\t\t\t\tDPOP: &wireprovisioner.DPOPOptions{\n\t\t\t\t\t\tSigningKey: []byte(fakeKey),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}))\n\t\t\tctx = NewLinkerContext(ctx, NewLinker(\"ca.example.com\", \"acme\"))\n\t\t\treturn test{\n\t\t\t\tctx:     ctx,\n\t\t\t\tpayload: []byte(\"{}\"),\n\t\t\t\tch: &Challenge{\n\t\t\t\t\tID:              \"chID\",\n\t\t\t\t\tAuthorizationID: \"azID\",\n\t\t\t\t\tAccountID:       \"accID\",\n\t\t\t\t\tToken:           \"token\",\n\t\t\t\t\tType:            \"wire-dpop-01\",\n\t\t\t\t\tStatus:          StatusPending,\n\t\t\t\t\tValue:           \"1234\",\n\t\t\t\t},\n\t\t\t\tdb: &MockWireDB{},\n\t\t\t\texpectedErr: &Error{\n\t\t\t\t\tType:   \"urn:ietf:params:acme:error:serverInternal\",\n\t\t\t\t\tDetail: \"The server experienced an internal error\",\n\t\t\t\t\tStatus: 500,\n\t\t\t\t\tErr:    errors.New(`error unmarshalling challenge data: json: cannot unmarshal number into Go value of type wire.DeviceID`),\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"fail/wire-parse-client-id\": func(t *testing.T) test {\n\t\t\tctx := NewProvisionerContext(context.Background(), newWireProvisionerWithOptions(t, &provisioner.Options{\n\t\t\t\tWire: &wireprovisioner.Options{\n\t\t\t\t\tOIDC: &wireprovisioner.OIDCOptions{\n\t\t\t\t\t\tProvider: &wireprovisioner.Provider{\n\t\t\t\t\t\t\tIssuerURL:  \"https://issuer.example.com\",\n\t\t\t\t\t\t\tAlgorithms: []string{\"ES256\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tConfig: &wireprovisioner.Config{\n\t\t\t\t\t\t\tClientID:            \"test\",\n\t\t\t\t\t\t\tSignatureAlgorithms: []string{\"ES256\"},\n\t\t\t\t\t\t\tNow:                 time.Now,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tTransformTemplate: \"\",\n\t\t\t\t\t},\n\t\t\t\t\tDPOP: &wireprovisioner.DPOPOptions{\n\t\t\t\t\t\tSigningKey: []byte(fakeKey),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}))\n\t\t\tctx = NewLinkerContext(ctx, NewLinker(\"ca.example.com\", \"acme\"))\n\t\t\tvalueBytes, err := json.Marshal(struct {\n\t\t\t\tName     string `json:\"name,omitempty\"`\n\t\t\t\tDomain   string `json:\"domain,omitempty\"`\n\t\t\t\tClientID string `json:\"client-id,omitempty\"`\n\t\t\t\tHandle   string `json:\"handle,omitempty\"`\n\t\t\t}{\n\t\t\t\tName:     \"Alice Smith\",\n\t\t\t\tDomain:   \"wire.com\",\n\t\t\t\tClientID: \"wireapp://594930e9d50bb175@wire.com\",\n\t\t\t\tHandle:   \"wireapp://%40alice_wire@wire.com\",\n\t\t\t})\n\t\t\trequire.NoError(t, err)\n\t\t\treturn test{\n\t\t\t\tctx:     ctx,\n\t\t\t\tpayload: []byte(\"{}\"),\n\t\t\t\tch: &Challenge{\n\t\t\t\t\tID:              \"chID\",\n\t\t\t\t\tAuthorizationID: \"azID\",\n\t\t\t\t\tAccountID:       \"accID\",\n\t\t\t\t\tToken:           \"token\",\n\t\t\t\t\tType:            \"wire-dpop-01\",\n\t\t\t\t\tStatus:          StatusPending,\n\t\t\t\t\tValue:           string(valueBytes),\n\t\t\t\t},\n\t\t\t\tdb: &MockWireDB{},\n\t\t\t\texpectedErr: &Error{\n\t\t\t\t\tType:   \"urn:ietf:params:acme:error:serverInternal\",\n\t\t\t\t\tDetail: \"The server experienced an internal error\",\n\t\t\t\t\tStatus: 500,\n\t\t\t\t\tErr:    errors.New(`error parsing device id: invalid Wire client ID username \"594930e9d50bb175\"`),\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"fail/parse-and-verify\": func(t *testing.T) test {\n\t\t\tctx := NewProvisionerContext(context.Background(), newWireProvisionerWithOptions(t, &provisioner.Options{\n\t\t\t\tWire: &wireprovisioner.Options{\n\t\t\t\t\tOIDC: &wireprovisioner.OIDCOptions{\n\t\t\t\t\t\tProvider: &wireprovisioner.Provider{\n\t\t\t\t\t\t\tIssuerURL:  \"http://issuer.example.com\",\n\t\t\t\t\t\t\tAlgorithms: []string{\"ES256\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tConfig: &wireprovisioner.Config{\n\t\t\t\t\t\t\tClientID:            \"test\",\n\t\t\t\t\t\t\tSignatureAlgorithms: []string{\"ES256\"},\n\t\t\t\t\t\t\tNow:                 time.Now,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tTransformTemplate: \"\",\n\t\t\t\t\t},\n\t\t\t\t\tDPOP: &wireprovisioner.DPOPOptions{\n\t\t\t\t\t\tTarget:     \"{{ .DeviceID }}\",\n\t\t\t\t\t\tSigningKey: []byte(fakeKey),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}))\n\t\t\tctx = NewLinkerContext(ctx, NewLinker(\"ca.example.com\", \"acme\"))\n\t\t\tvalueBytes, err := json.Marshal(struct {\n\t\t\t\tName     string `json:\"name,omitempty\"`\n\t\t\t\tDomain   string `json:\"domain,omitempty\"`\n\t\t\t\tClientID string `json:\"client-id,omitempty\"`\n\t\t\t\tHandle   string `json:\"handle,omitempty\"`\n\t\t\t}{\n\t\t\t\tName:     \"Alice Smith\",\n\t\t\t\tDomain:   \"wire.com\",\n\t\t\t\tClientID: \"wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com\",\n\t\t\t\tHandle:   \"wireapp://%40alice_wire@wire.com\",\n\t\t\t})\n\t\t\trequire.NoError(t, err)\n\t\t\tjwk, _ := mustAccountAndKeyAuthorization(t, \"token\")\n\t\t\treturn test{\n\t\t\t\tctx:     ctx,\n\t\t\t\tpayload: []byte(\"{}\"),\n\t\t\t\tjwk:     jwk,\n\t\t\t\tch: &Challenge{\n\t\t\t\t\tID:              \"chID\",\n\t\t\t\t\tAuthorizationID: \"azID\",\n\t\t\t\t\tAccountID:       \"accID\",\n\t\t\t\t\tToken:           \"token\",\n\t\t\t\t\tType:            \"wire-dpop-01\",\n\t\t\t\t\tStatus:          StatusPending,\n\t\t\t\t\tValue:           string(valueBytes),\n\t\t\t\t},\n\t\t\t\tdb: &MockWireDB{\n\t\t\t\t\tMockDB: MockDB{\n\t\t\t\t\t\tMockUpdateChallenge: func(ctx context.Context, ch *Challenge) error {\n\t\t\t\t\t\t\tassert.Equal(t, \"chID\", ch.ID)\n\t\t\t\t\t\t\tassert.Equal(t, \"azID\", ch.AuthorizationID)\n\t\t\t\t\t\t\tassert.Equal(t, \"accID\", ch.AccountID)\n\t\t\t\t\t\t\tassert.Equal(t, \"token\", ch.Token)\n\t\t\t\t\t\t\tassert.Equal(t, ChallengeType(\"wire-dpop-01\"), ch.Type)\n\t\t\t\t\t\t\tassert.Equal(t, StatusInvalid, ch.Status)\n\t\t\t\t\t\t\tassert.Equal(t, string(valueBytes), ch.Value)\n\t\t\t\t\t\t\tif assert.NotNil(t, ch.Error) {\n\t\t\t\t\t\t\t\tvar k *Error // NOTE: the error is not returned up, but stored with the challenge instead\n\t\t\t\t\t\t\t\tif errors.As(ch.Error, &k) {\n\t\t\t\t\t\t\t\t\tassert.Equal(t, \"urn:ietf:params:acme:error:rejectedIdentifier\", k.Type)\n\t\t\t\t\t\t\t\t\tassert.Equal(t, \"The server will not issue certificates for the identifier\", k.Detail)\n\t\t\t\t\t\t\t\t\tassert.Equal(t, 400, k.Status)\n\t\t\t\t\t\t\t\t\tassert.Equal(t, `failed validating Wire access token: failed parsing token: go-jose/go-jose: compact JWS format must have three parts`, k.Err.Error())\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"fail/db.UpdateChallenge\": func(t *testing.T) test {\n\t\t\tjwk, keyAuth := mustAccountAndKeyAuthorization(t, \"token\")\n\t\t\t_ = keyAuth // TODO(hs): keyAuth (not) required for DPoP? Or needs to be added to validation?\n\t\t\tdpopSigner, err := jose.NewSigner(jose.SigningKey{\n\t\t\t\tAlgorithm: jose.SignatureAlgorithm(jwk.Algorithm),\n\t\t\t\tKey:       jwk,\n\t\t\t}, new(jose.SignerOptions))\n\t\t\trequire.NoError(t, err)\n\t\t\tsignerJWK, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\trequire.NoError(t, err)\n\t\t\tsigner, err := jose.NewSigner(jose.SigningKey{\n\t\t\t\tAlgorithm: jose.SignatureAlgorithm(signerJWK.Algorithm),\n\t\t\t\tKey:       signerJWK,\n\t\t\t}, new(jose.SignerOptions))\n\t\t\trequire.NoError(t, err)\n\t\t\tsignerPEMBlock, err := pemutil.Serialize(signerJWK.Public().Key)\n\t\t\trequire.NoError(t, err)\n\t\t\tsignerPEMBytes := pem.EncodeToMemory(signerPEMBlock)\n\t\t\tdpopBytes, err := json.Marshal(struct {\n\t\t\t\tjose.Claims\n\t\t\t\tChallenge string `json:\"chal,omitempty\"`\n\t\t\t\tHandle    string `json:\"handle,omitempty\"`\n\t\t\t\tNonce     string `json:\"nonce,omitempty\"`\n\t\t\t\tHTU       string `json:\"htu,omitempty\"`\n\t\t\t\tName      string `json:\"name,omitempty\"`\n\t\t\t}{\n\t\t\t\tClaims: jose.Claims{\n\t\t\t\t\tSubject:  \"wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com\",\n\t\t\t\t\tAudience: jose.Audience{\"https://ca.example.com/acme/wire/challenge/azID/chID\"},\n\t\t\t\t},\n\t\t\t\tChallenge: \"token\",\n\t\t\t\tHandle:    \"wireapp://%40alice_wire@wire.com\",\n\t\t\t\tNonce:     \"nonce\",\n\t\t\t\tHTU:       \"http://issuer.example.com\",\n\t\t\t\tName:      \"Alice Smith\",\n\t\t\t})\n\t\t\trequire.NoError(t, err)\n\t\t\tdpop, err := dpopSigner.Sign(dpopBytes)\n\t\t\trequire.NoError(t, err)\n\t\t\tproof, err := dpop.CompactSerialize()\n\t\t\trequire.NoError(t, err)\n\t\t\ttokenBytes, err := json.Marshal(struct {\n\t\t\t\tjose.Claims\n\t\t\t\tChallenge string `json:\"chal,omitempty\"`\n\t\t\t\tNonce     string `json:\"nonce,omitempty\"`\n\t\t\t\tCnf       struct {\n\t\t\t\t\tKid string `json:\"kid,omitempty\"`\n\t\t\t\t} `json:\"cnf\"`\n\t\t\t\tProof      string `json:\"proof,omitempty\"`\n\t\t\t\tClientID   string `json:\"client_id\"`\n\t\t\t\tAPIVersion int    `json:\"api_version\"`\n\t\t\t\tScope      string `json:\"scope\"`\n\t\t\t}{\n\t\t\t\tClaims: jose.Claims{\n\t\t\t\t\tIssuer:   \"http://issuer.example.com\",\n\t\t\t\t\tAudience: jose.Audience{\"https://ca.example.com/acme/wire/challenge/azID/chID\"},\n\t\t\t\t\tExpiry:   jose.NewNumericDate(time.Now().Add(1 * time.Minute)),\n\t\t\t\t},\n\t\t\t\tChallenge: \"token\",\n\t\t\t\tNonce:     \"nonce\",\n\t\t\t\tCnf: struct {\n\t\t\t\t\tKid string `json:\"kid,omitempty\"`\n\t\t\t\t}{\n\t\t\t\t\tKid: jwk.KeyID,\n\t\t\t\t},\n\t\t\t\tProof:      proof,\n\t\t\t\tClientID:   \"wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com\",\n\t\t\t\tAPIVersion: 5,\n\t\t\t\tScope:      \"wire_client_id\",\n\t\t\t})\n\n\t\t\trequire.NoError(t, err)\n\t\t\tsigned, err := signer.Sign(tokenBytes)\n\t\t\trequire.NoError(t, err)\n\t\t\taccessToken, err := signed.CompactSerialize()\n\t\t\trequire.NoError(t, err)\n\t\t\tpayload, err := json.Marshal(struct {\n\t\t\t\tAccessToken string `json:\"access_token\"`\n\t\t\t}{\n\t\t\t\tAccessToken: accessToken,\n\t\t\t})\n\t\t\trequire.NoError(t, err)\n\t\t\tvalueBytes, err := json.Marshal(struct {\n\t\t\t\tName     string `json:\"name,omitempty\"`\n\t\t\t\tDomain   string `json:\"domain,omitempty\"`\n\t\t\t\tClientID string `json:\"client-id,omitempty\"`\n\t\t\t\tHandle   string `json:\"handle,omitempty\"`\n\t\t\t}{\n\t\t\t\tName:     \"Alice Smith\",\n\t\t\t\tDomain:   \"wire.com\",\n\t\t\t\tClientID: \"wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com\",\n\t\t\t\tHandle:   \"wireapp://%40alice_wire@wire.com\",\n\t\t\t})\n\t\t\trequire.NoError(t, err)\n\t\t\tctx := NewProvisionerContext(context.Background(), newWireProvisionerWithOptions(t, &provisioner.Options{\n\t\t\t\tWire: &wireprovisioner.Options{\n\t\t\t\t\tOIDC: &wireprovisioner.OIDCOptions{\n\t\t\t\t\t\tProvider: &wireprovisioner.Provider{\n\t\t\t\t\t\t\tIssuerURL:  \"http://issuer.example.com\",\n\t\t\t\t\t\t\tAlgorithms: []string{\"ES256\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tConfig: &wireprovisioner.Config{\n\t\t\t\t\t\t\tClientID:            \"test\",\n\t\t\t\t\t\t\tSignatureAlgorithms: []string{\"ES256\"},\n\t\t\t\t\t\t\tNow:                 time.Now,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tTransformTemplate: \"\",\n\t\t\t\t\t},\n\t\t\t\t\tDPOP: &wireprovisioner.DPOPOptions{\n\t\t\t\t\t\tTarget:     \"http://issuer.example.com\",\n\t\t\t\t\t\tSigningKey: signerPEMBytes,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}))\n\t\t\tctx = NewLinkerContext(ctx, NewLinker(\"ca.example.com\", \"acme\"))\n\t\t\treturn test{\n\t\t\t\tch: &Challenge{\n\t\t\t\t\tID:              \"chID\",\n\t\t\t\t\tAuthorizationID: \"azID\",\n\t\t\t\t\tAccountID:       \"accID\",\n\t\t\t\t\tToken:           \"token\",\n\t\t\t\t\tType:            \"wire-dpop-01\",\n\t\t\t\t\tStatus:          StatusPending,\n\t\t\t\t\tValue:           string(valueBytes),\n\t\t\t\t},\n\t\t\t\tpayload: payload,\n\t\t\t\tctx:     ctx,\n\t\t\t\tjwk:     jwk,\n\t\t\t\tdb: &MockWireDB{\n\t\t\t\t\tMockDB: MockDB{\n\t\t\t\t\t\tMockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {\n\t\t\t\t\t\t\tassert.Equal(t, \"chID\", updch.ID)\n\t\t\t\t\t\t\tassert.Equal(t, \"token\", updch.Token)\n\t\t\t\t\t\t\tassert.Equal(t, StatusValid, updch.Status)\n\t\t\t\t\t\t\tassert.Equal(t, ChallengeType(\"wire-dpop-01\"), updch.Type)\n\t\t\t\t\t\t\tassert.Equal(t, string(valueBytes), updch.Value)\n\t\t\t\t\t\t\treturn errors.New(\"fail\")\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\texpectedErr: &Error{\n\t\t\t\t\tType:   \"urn:ietf:params:acme:error:serverInternal\",\n\t\t\t\t\tDetail: \"The server experienced an internal error\",\n\t\t\t\t\tStatus: 500,\n\t\t\t\t\tErr:    errors.New(`error updating challenge: fail`),\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"fail/db.GetAllOrdersByAccountID\": func(t *testing.T) test {\n\t\t\tjwk, keyAuth := mustAccountAndKeyAuthorization(t, \"token\")\n\t\t\t_ = keyAuth // TODO(hs): keyAuth (not) required for DPoP? Or needs to be added to validation?\n\t\t\tdpopSigner, err := jose.NewSigner(jose.SigningKey{\n\t\t\t\tAlgorithm: jose.SignatureAlgorithm(jwk.Algorithm),\n\t\t\t\tKey:       jwk,\n\t\t\t}, new(jose.SignerOptions))\n\t\t\trequire.NoError(t, err)\n\t\t\tsignerJWK, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\trequire.NoError(t, err)\n\t\t\tsigner, err := jose.NewSigner(jose.SigningKey{\n\t\t\t\tAlgorithm: jose.SignatureAlgorithm(signerJWK.Algorithm),\n\t\t\t\tKey:       signerJWK,\n\t\t\t}, new(jose.SignerOptions))\n\t\t\trequire.NoError(t, err)\n\t\t\tsignerPEMBlock, err := pemutil.Serialize(signerJWK.Public().Key)\n\t\t\trequire.NoError(t, err)\n\t\t\tsignerPEMBytes := pem.EncodeToMemory(signerPEMBlock)\n\t\t\tdpopBytes, err := json.Marshal(struct {\n\t\t\t\tjose.Claims\n\t\t\t\tChallenge string `json:\"chal,omitempty\"`\n\t\t\t\tHandle    string `json:\"handle,omitempty\"`\n\t\t\t\tNonce     string `json:\"nonce,omitempty\"`\n\t\t\t\tHTU       string `json:\"htu,omitempty\"`\n\t\t\t\tName      string `json:\"name,omitempty\"`\n\t\t\t}{\n\t\t\t\tClaims: jose.Claims{\n\t\t\t\t\tSubject:  \"wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com\",\n\t\t\t\t\tAudience: jose.Audience{\"https://ca.example.com/acme/wire/challenge/azID/chID\"},\n\t\t\t\t},\n\t\t\t\tChallenge: \"token\",\n\t\t\t\tHandle:    \"wireapp://%40alice_wire@wire.com\",\n\t\t\t\tNonce:     \"nonce\",\n\t\t\t\tHTU:       \"http://issuer.example.com\",\n\t\t\t\tName:      \"Alice Smith\",\n\t\t\t})\n\t\t\trequire.NoError(t, err)\n\t\t\tdpop, err := dpopSigner.Sign(dpopBytes)\n\t\t\trequire.NoError(t, err)\n\t\t\tproof, err := dpop.CompactSerialize()\n\t\t\trequire.NoError(t, err)\n\t\t\ttokenBytes, err := json.Marshal(struct {\n\t\t\t\tjose.Claims\n\t\t\t\tChallenge string `json:\"chal,omitempty\"`\n\t\t\t\tNonce     string `json:\"nonce,omitempty\"`\n\t\t\t\tCnf       struct {\n\t\t\t\t\tKid string `json:\"kid,omitempty\"`\n\t\t\t\t} `json:\"cnf\"`\n\t\t\t\tProof      string `json:\"proof,omitempty\"`\n\t\t\t\tClientID   string `json:\"client_id\"`\n\t\t\t\tAPIVersion int    `json:\"api_version\"`\n\t\t\t\tScope      string `json:\"scope\"`\n\t\t\t}{\n\t\t\t\tClaims: jose.Claims{\n\t\t\t\t\tIssuer:   \"http://issuer.example.com\",\n\t\t\t\t\tAudience: jose.Audience{\"https://ca.example.com/acme/wire/challenge/azID/chID\"},\n\t\t\t\t\tExpiry:   jose.NewNumericDate(time.Now().Add(1 * time.Minute)),\n\t\t\t\t},\n\t\t\t\tChallenge: \"token\",\n\t\t\t\tNonce:     \"nonce\",\n\t\t\t\tCnf: struct {\n\t\t\t\t\tKid string `json:\"kid,omitempty\"`\n\t\t\t\t}{\n\t\t\t\t\tKid: jwk.KeyID,\n\t\t\t\t},\n\t\t\t\tProof:      proof,\n\t\t\t\tClientID:   \"wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com\",\n\t\t\t\tAPIVersion: 5,\n\t\t\t\tScope:      \"wire_client_id\",\n\t\t\t})\n\n\t\t\trequire.NoError(t, err)\n\t\t\tsigned, err := signer.Sign(tokenBytes)\n\t\t\trequire.NoError(t, err)\n\t\t\taccessToken, err := signed.CompactSerialize()\n\t\t\trequire.NoError(t, err)\n\t\t\tpayload, err := json.Marshal(struct {\n\t\t\t\tAccessToken string `json:\"access_token\"`\n\t\t\t}{\n\t\t\t\tAccessToken: accessToken,\n\t\t\t})\n\t\t\trequire.NoError(t, err)\n\t\t\tvalueBytes, err := json.Marshal(struct {\n\t\t\t\tName     string `json:\"name,omitempty\"`\n\t\t\t\tDomain   string `json:\"domain,omitempty\"`\n\t\t\t\tClientID string `json:\"client-id,omitempty\"`\n\t\t\t\tHandle   string `json:\"handle,omitempty\"`\n\t\t\t}{\n\t\t\t\tName:     \"Alice Smith\",\n\t\t\t\tDomain:   \"wire.com\",\n\t\t\t\tClientID: \"wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com\",\n\t\t\t\tHandle:   \"wireapp://%40alice_wire@wire.com\",\n\t\t\t})\n\t\t\trequire.NoError(t, err)\n\t\t\tctx := NewProvisionerContext(context.Background(), newWireProvisionerWithOptions(t, &provisioner.Options{\n\t\t\t\tWire: &wireprovisioner.Options{\n\t\t\t\t\tOIDC: &wireprovisioner.OIDCOptions{\n\t\t\t\t\t\tProvider: &wireprovisioner.Provider{\n\t\t\t\t\t\t\tIssuerURL:  \"http://issuer.example.com\",\n\t\t\t\t\t\t\tAlgorithms: []string{\"ES256\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tConfig: &wireprovisioner.Config{\n\t\t\t\t\t\t\tClientID:            \"test\",\n\t\t\t\t\t\t\tSignatureAlgorithms: []string{\"ES256\"},\n\t\t\t\t\t\t\tNow:                 time.Now,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tTransformTemplate: \"\",\n\t\t\t\t\t},\n\t\t\t\t\tDPOP: &wireprovisioner.DPOPOptions{\n\t\t\t\t\t\tTarget:     \"http://issuer.example.com\",\n\t\t\t\t\t\tSigningKey: signerPEMBytes,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}))\n\t\t\tctx = NewLinkerContext(ctx, NewLinker(\"ca.example.com\", \"acme\"))\n\t\t\treturn test{\n\t\t\t\tch: &Challenge{\n\t\t\t\t\tID:              \"chID\",\n\t\t\t\t\tAuthorizationID: \"azID\",\n\t\t\t\t\tAccountID:       \"accID\",\n\t\t\t\t\tToken:           \"token\",\n\t\t\t\t\tType:            \"wire-dpop-01\",\n\t\t\t\t\tStatus:          StatusPending,\n\t\t\t\t\tValue:           string(valueBytes),\n\t\t\t\t},\n\t\t\t\tpayload: payload,\n\t\t\t\tctx:     ctx,\n\t\t\t\tjwk:     jwk,\n\t\t\t\tdb: &MockWireDB{\n\t\t\t\t\tMockDB: MockDB{\n\t\t\t\t\t\tMockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {\n\t\t\t\t\t\t\tassert.Equal(t, \"chID\", updch.ID)\n\t\t\t\t\t\t\tassert.Equal(t, \"token\", updch.Token)\n\t\t\t\t\t\t\tassert.Equal(t, StatusValid, updch.Status)\n\t\t\t\t\t\t\tassert.Equal(t, ChallengeType(\"wire-dpop-01\"), updch.Type)\n\t\t\t\t\t\t\tassert.Equal(t, string(valueBytes), updch.Value)\n\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tMockGetAllOrdersByAccountID: func(ctx context.Context, accountID string) ([]string, error) {\n\t\t\t\t\t\tassert.Equal(t, \"accID\", accountID)\n\t\t\t\t\t\treturn nil, errors.New(\"fail\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\texpectedErr: &Error{\n\t\t\t\t\tType:   \"urn:ietf:params:acme:error:serverInternal\",\n\t\t\t\t\tDetail: \"The server experienced an internal error\",\n\t\t\t\t\tStatus: 500,\n\t\t\t\t\tErr:    errors.New(`could not find current order by account id: fail`),\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"fail/db.GetAllOrdersByAccountID-zero\": func(t *testing.T) test {\n\t\t\tjwk, keyAuth := mustAccountAndKeyAuthorization(t, \"token\")\n\t\t\t_ = keyAuth // TODO(hs): keyAuth (not) required for DPoP? Or needs to be added to validation?\n\t\t\tdpopSigner, err := jose.NewSigner(jose.SigningKey{\n\t\t\t\tAlgorithm: jose.SignatureAlgorithm(jwk.Algorithm),\n\t\t\t\tKey:       jwk,\n\t\t\t}, new(jose.SignerOptions))\n\t\t\trequire.NoError(t, err)\n\t\t\tsignerJWK, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\trequire.NoError(t, err)\n\t\t\tsigner, err := jose.NewSigner(jose.SigningKey{\n\t\t\t\tAlgorithm: jose.SignatureAlgorithm(signerJWK.Algorithm),\n\t\t\t\tKey:       signerJWK,\n\t\t\t}, new(jose.SignerOptions))\n\t\t\trequire.NoError(t, err)\n\t\t\tsignerPEMBlock, err := pemutil.Serialize(signerJWK.Public().Key)\n\t\t\trequire.NoError(t, err)\n\t\t\tsignerPEMBytes := pem.EncodeToMemory(signerPEMBlock)\n\t\t\tdpopBytes, err := json.Marshal(struct {\n\t\t\t\tjose.Claims\n\t\t\t\tChallenge string `json:\"chal,omitempty\"`\n\t\t\t\tHandle    string `json:\"handle,omitempty\"`\n\t\t\t\tNonce     string `json:\"nonce,omitempty\"`\n\t\t\t\tHTU       string `json:\"htu,omitempty\"`\n\t\t\t\tName      string `json:\"name,omitempty\"`\n\t\t\t}{\n\t\t\t\tClaims: jose.Claims{\n\t\t\t\t\tSubject:  \"wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com\",\n\t\t\t\t\tAudience: jose.Audience{\"https://ca.example.com/acme/wire/challenge/azID/chID\"},\n\t\t\t\t},\n\t\t\t\tChallenge: \"token\",\n\t\t\t\tHandle:    \"wireapp://%40alice_wire@wire.com\",\n\t\t\t\tNonce:     \"nonce\",\n\t\t\t\tHTU:       \"http://issuer.example.com\",\n\t\t\t\tName:      \"Alice Smith\",\n\t\t\t})\n\t\t\trequire.NoError(t, err)\n\t\t\tdpop, err := dpopSigner.Sign(dpopBytes)\n\t\t\trequire.NoError(t, err)\n\t\t\tproof, err := dpop.CompactSerialize()\n\t\t\trequire.NoError(t, err)\n\t\t\ttokenBytes, err := json.Marshal(struct {\n\t\t\t\tjose.Claims\n\t\t\t\tChallenge string `json:\"chal,omitempty\"`\n\t\t\t\tNonce     string `json:\"nonce,omitempty\"`\n\t\t\t\tCnf       struct {\n\t\t\t\t\tKid string `json:\"kid,omitempty\"`\n\t\t\t\t} `json:\"cnf\"`\n\t\t\t\tProof      string `json:\"proof,omitempty\"`\n\t\t\t\tClientID   string `json:\"client_id\"`\n\t\t\t\tAPIVersion int    `json:\"api_version\"`\n\t\t\t\tScope      string `json:\"scope\"`\n\t\t\t}{\n\t\t\t\tClaims: jose.Claims{\n\t\t\t\t\tIssuer:   \"http://issuer.example.com\",\n\t\t\t\t\tAudience: jose.Audience{\"https://ca.example.com/acme/wire/challenge/azID/chID\"},\n\t\t\t\t\tExpiry:   jose.NewNumericDate(time.Now().Add(1 * time.Minute)),\n\t\t\t\t},\n\t\t\t\tChallenge: \"token\",\n\t\t\t\tNonce:     \"nonce\",\n\t\t\t\tCnf: struct {\n\t\t\t\t\tKid string `json:\"kid,omitempty\"`\n\t\t\t\t}{\n\t\t\t\t\tKid: jwk.KeyID,\n\t\t\t\t},\n\t\t\t\tProof:      proof,\n\t\t\t\tClientID:   \"wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com\",\n\t\t\t\tAPIVersion: 5,\n\t\t\t\tScope:      \"wire_client_id\",\n\t\t\t})\n\n\t\t\trequire.NoError(t, err)\n\t\t\tsigned, err := signer.Sign(tokenBytes)\n\t\t\trequire.NoError(t, err)\n\t\t\taccessToken, err := signed.CompactSerialize()\n\t\t\trequire.NoError(t, err)\n\t\t\tpayload, err := json.Marshal(struct {\n\t\t\t\tAccessToken string `json:\"access_token\"`\n\t\t\t}{\n\t\t\t\tAccessToken: accessToken,\n\t\t\t})\n\t\t\trequire.NoError(t, err)\n\t\t\tvalueBytes, err := json.Marshal(struct {\n\t\t\t\tName     string `json:\"name,omitempty\"`\n\t\t\t\tDomain   string `json:\"domain,omitempty\"`\n\t\t\t\tClientID string `json:\"client-id,omitempty\"`\n\t\t\t\tHandle   string `json:\"handle,omitempty\"`\n\t\t\t}{\n\t\t\t\tName:     \"Alice Smith\",\n\t\t\t\tDomain:   \"wire.com\",\n\t\t\t\tClientID: \"wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com\",\n\t\t\t\tHandle:   \"wireapp://%40alice_wire@wire.com\",\n\t\t\t})\n\t\t\trequire.NoError(t, err)\n\t\t\tctx := NewProvisionerContext(context.Background(), newWireProvisionerWithOptions(t, &provisioner.Options{\n\t\t\t\tWire: &wireprovisioner.Options{\n\t\t\t\t\tOIDC: &wireprovisioner.OIDCOptions{\n\t\t\t\t\t\tProvider: &wireprovisioner.Provider{\n\t\t\t\t\t\t\tIssuerURL:  \"http://issuer.example.com\",\n\t\t\t\t\t\t\tAlgorithms: []string{\"ES256\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tConfig: &wireprovisioner.Config{\n\t\t\t\t\t\t\tClientID:            \"test\",\n\t\t\t\t\t\t\tSignatureAlgorithms: []string{\"ES256\"},\n\t\t\t\t\t\t\tNow:                 time.Now,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tTransformTemplate: \"\",\n\t\t\t\t\t},\n\t\t\t\t\tDPOP: &wireprovisioner.DPOPOptions{\n\t\t\t\t\t\tTarget:     \"http://issuer.example.com\",\n\t\t\t\t\t\tSigningKey: signerPEMBytes,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}))\n\t\t\tctx = NewLinkerContext(ctx, NewLinker(\"ca.example.com\", \"acme\"))\n\t\t\treturn test{\n\t\t\t\tch: &Challenge{\n\t\t\t\t\tID:              \"chID\",\n\t\t\t\t\tAuthorizationID: \"azID\",\n\t\t\t\t\tAccountID:       \"accID\",\n\t\t\t\t\tToken:           \"token\",\n\t\t\t\t\tType:            \"wire-dpop-01\",\n\t\t\t\t\tStatus:          StatusPending,\n\t\t\t\t\tValue:           string(valueBytes),\n\t\t\t\t},\n\t\t\t\tpayload: payload,\n\t\t\t\tctx:     ctx,\n\t\t\t\tjwk:     jwk,\n\t\t\t\tdb: &MockWireDB{\n\t\t\t\t\tMockDB: MockDB{\n\t\t\t\t\t\tMockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {\n\t\t\t\t\t\t\tassert.Equal(t, \"chID\", updch.ID)\n\t\t\t\t\t\t\tassert.Equal(t, \"token\", updch.Token)\n\t\t\t\t\t\t\tassert.Equal(t, StatusValid, updch.Status)\n\t\t\t\t\t\t\tassert.Equal(t, ChallengeType(\"wire-dpop-01\"), updch.Type)\n\t\t\t\t\t\t\tassert.Equal(t, string(valueBytes), updch.Value)\n\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tMockGetAllOrdersByAccountID: func(ctx context.Context, accountID string) ([]string, error) {\n\t\t\t\t\t\tassert.Equal(t, \"accID\", accountID)\n\t\t\t\t\t\treturn []string{}, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\texpectedErr: &Error{\n\t\t\t\t\tType:   \"urn:ietf:params:acme:error:serverInternal\",\n\t\t\t\t\tDetail: \"The server experienced an internal error\",\n\t\t\t\t\tStatus: 500,\n\t\t\t\t\tErr:    errors.New(`there are not enough orders for this account for this custom OIDC challenge`),\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"fail/db.CreateDpopToken\": func(t *testing.T) test {\n\t\t\tjwk, keyAuth := mustAccountAndKeyAuthorization(t, \"token\")\n\t\t\t_ = keyAuth // TODO(hs): keyAuth (not) required for DPoP? Or needs to be added to validation?\n\t\t\tdpopSigner, err := jose.NewSigner(jose.SigningKey{\n\t\t\t\tAlgorithm: jose.SignatureAlgorithm(jwk.Algorithm),\n\t\t\t\tKey:       jwk,\n\t\t\t}, new(jose.SignerOptions))\n\t\t\trequire.NoError(t, err)\n\t\t\tsignerJWK, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\trequire.NoError(t, err)\n\t\t\tsigner, err := jose.NewSigner(jose.SigningKey{\n\t\t\t\tAlgorithm: jose.SignatureAlgorithm(signerJWK.Algorithm),\n\t\t\t\tKey:       signerJWK,\n\t\t\t}, new(jose.SignerOptions))\n\t\t\trequire.NoError(t, err)\n\t\t\tsignerPEMBlock, err := pemutil.Serialize(signerJWK.Public().Key)\n\t\t\trequire.NoError(t, err)\n\t\t\tsignerPEMBytes := pem.EncodeToMemory(signerPEMBlock)\n\t\t\tdpopBytes, err := json.Marshal(struct {\n\t\t\t\tjose.Claims\n\t\t\t\tChallenge string `json:\"chal,omitempty\"`\n\t\t\t\tHandle    string `json:\"handle,omitempty\"`\n\t\t\t\tNonce     string `json:\"nonce,omitempty\"`\n\t\t\t\tHTU       string `json:\"htu,omitempty\"`\n\t\t\t\tName      string `json:\"name,omitempty\"`\n\t\t\t}{\n\t\t\t\tClaims: jose.Claims{\n\t\t\t\t\tSubject:  \"wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com\",\n\t\t\t\t\tAudience: jose.Audience{\"https://ca.example.com/acme/wire/challenge/azID/chID\"},\n\t\t\t\t},\n\t\t\t\tChallenge: \"token\",\n\t\t\t\tHandle:    \"wireapp://%40alice_wire@wire.com\",\n\t\t\t\tNonce:     \"nonce\",\n\t\t\t\tHTU:       \"http://issuer.example.com\",\n\t\t\t\tName:      \"Alice Smith\",\n\t\t\t})\n\t\t\trequire.NoError(t, err)\n\t\t\tdpop, err := dpopSigner.Sign(dpopBytes)\n\t\t\trequire.NoError(t, err)\n\t\t\tproof, err := dpop.CompactSerialize()\n\t\t\trequire.NoError(t, err)\n\t\t\ttokenBytes, err := json.Marshal(struct {\n\t\t\t\tjose.Claims\n\t\t\t\tChallenge string `json:\"chal,omitempty\"`\n\t\t\t\tNonce     string `json:\"nonce,omitempty\"`\n\t\t\t\tCnf       struct {\n\t\t\t\t\tKid string `json:\"kid,omitempty\"`\n\t\t\t\t} `json:\"cnf\"`\n\t\t\t\tProof      string `json:\"proof,omitempty\"`\n\t\t\t\tClientID   string `json:\"client_id\"`\n\t\t\t\tAPIVersion int    `json:\"api_version\"`\n\t\t\t\tScope      string `json:\"scope\"`\n\t\t\t}{\n\t\t\t\tClaims: jose.Claims{\n\t\t\t\t\tIssuer:   \"http://issuer.example.com\",\n\t\t\t\t\tAudience: jose.Audience{\"https://ca.example.com/acme/wire/challenge/azID/chID\"},\n\t\t\t\t\tExpiry:   jose.NewNumericDate(time.Now().Add(1 * time.Minute)),\n\t\t\t\t},\n\t\t\t\tChallenge: \"token\",\n\t\t\t\tNonce:     \"nonce\",\n\t\t\t\tCnf: struct {\n\t\t\t\t\tKid string `json:\"kid,omitempty\"`\n\t\t\t\t}{\n\t\t\t\t\tKid: jwk.KeyID,\n\t\t\t\t},\n\t\t\t\tProof:      proof,\n\t\t\t\tClientID:   \"wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com\",\n\t\t\t\tAPIVersion: 5,\n\t\t\t\tScope:      \"wire_client_id\",\n\t\t\t})\n\n\t\t\trequire.NoError(t, err)\n\t\t\tsigned, err := signer.Sign(tokenBytes)\n\t\t\trequire.NoError(t, err)\n\t\t\taccessToken, err := signed.CompactSerialize()\n\t\t\trequire.NoError(t, err)\n\t\t\tpayload, err := json.Marshal(struct {\n\t\t\t\tAccessToken string `json:\"access_token\"`\n\t\t\t}{\n\t\t\t\tAccessToken: accessToken,\n\t\t\t})\n\t\t\trequire.NoError(t, err)\n\t\t\tvalueBytes, err := json.Marshal(struct {\n\t\t\t\tName     string `json:\"name,omitempty\"`\n\t\t\t\tDomain   string `json:\"domain,omitempty\"`\n\t\t\t\tClientID string `json:\"client-id,omitempty\"`\n\t\t\t\tHandle   string `json:\"handle,omitempty\"`\n\t\t\t}{\n\t\t\t\tName:     \"Alice Smith\",\n\t\t\t\tDomain:   \"wire.com\",\n\t\t\t\tClientID: \"wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com\",\n\t\t\t\tHandle:   \"wireapp://%40alice_wire@wire.com\",\n\t\t\t})\n\t\t\trequire.NoError(t, err)\n\t\t\tctx := NewProvisionerContext(context.Background(), newWireProvisionerWithOptions(t, &provisioner.Options{\n\t\t\t\tWire: &wireprovisioner.Options{\n\t\t\t\t\tOIDC: &wireprovisioner.OIDCOptions{\n\t\t\t\t\t\tProvider: &wireprovisioner.Provider{\n\t\t\t\t\t\t\tIssuerURL:  \"http://issuer.example.com\",\n\t\t\t\t\t\t\tAlgorithms: []string{\"ES256\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tConfig: &wireprovisioner.Config{\n\t\t\t\t\t\t\tClientID:            \"test\",\n\t\t\t\t\t\t\tSignatureAlgorithms: []string{\"ES256\"},\n\t\t\t\t\t\t\tNow:                 time.Now,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tTransformTemplate: \"\",\n\t\t\t\t\t},\n\t\t\t\t\tDPOP: &wireprovisioner.DPOPOptions{\n\t\t\t\t\t\tTarget:     \"http://issuer.example.com\",\n\t\t\t\t\t\tSigningKey: signerPEMBytes,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}))\n\t\t\tctx = NewLinkerContext(ctx, NewLinker(\"ca.example.com\", \"acme\"))\n\t\t\treturn test{\n\t\t\t\tch: &Challenge{\n\t\t\t\t\tID:              \"chID\",\n\t\t\t\t\tAuthorizationID: \"azID\",\n\t\t\t\t\tAccountID:       \"accID\",\n\t\t\t\t\tToken:           \"token\",\n\t\t\t\t\tType:            \"wire-dpop-01\",\n\t\t\t\t\tStatus:          StatusPending,\n\t\t\t\t\tValue:           string(valueBytes),\n\t\t\t\t},\n\t\t\t\tpayload: payload,\n\t\t\t\tctx:     ctx,\n\t\t\t\tjwk:     jwk,\n\t\t\t\tdb: &MockWireDB{\n\t\t\t\t\tMockDB: MockDB{\n\t\t\t\t\t\tMockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {\n\t\t\t\t\t\t\tassert.Equal(t, \"chID\", updch.ID)\n\t\t\t\t\t\t\tassert.Equal(t, \"token\", updch.Token)\n\t\t\t\t\t\t\tassert.Equal(t, StatusValid, updch.Status)\n\t\t\t\t\t\t\tassert.Equal(t, ChallengeType(\"wire-dpop-01\"), updch.Type)\n\t\t\t\t\t\t\tassert.Equal(t, string(valueBytes), updch.Value)\n\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tMockGetAllOrdersByAccountID: func(ctx context.Context, accountID string) ([]string, error) {\n\t\t\t\t\t\tassert.Equal(t, \"accID\", accountID)\n\t\t\t\t\t\treturn []string{\"orderID\"}, nil\n\t\t\t\t\t},\n\t\t\t\t\tMockCreateDpopToken: func(ctx context.Context, orderID string, dpop map[string]interface{}) error {\n\t\t\t\t\t\tassert.Equal(t, \"orderID\", orderID)\n\t\t\t\t\t\tassert.Equal(t, \"token\", dpop[\"chal\"].(string))\n\t\t\t\t\t\tassert.Equal(t, \"wireapp://%40alice_wire@wire.com\", dpop[\"handle\"].(string))\n\t\t\t\t\t\tassert.Equal(t, \"wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com\", dpop[\"sub\"].(string))\n\t\t\t\t\t\treturn errors.New(\"fail\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\texpectedErr: &Error{\n\t\t\t\t\tType:   \"urn:ietf:params:acme:error:serverInternal\",\n\t\t\t\t\tDetail: \"The server experienced an internal error\",\n\t\t\t\t\tStatus: 500,\n\t\t\t\t\tErr:    errors.New(`failed storing DPoP token: fail`),\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tjwk, keyAuth := mustAccountAndKeyAuthorization(t, \"token\")\n\t\t\t_ = keyAuth // TODO(hs): keyAuth (not) required for DPoP? Or needs to be added to validation?\n\t\t\tdpopSigner, err := jose.NewSigner(jose.SigningKey{\n\t\t\t\tAlgorithm: jose.SignatureAlgorithm(jwk.Algorithm),\n\t\t\t\tKey:       jwk,\n\t\t\t}, new(jose.SignerOptions))\n\t\t\trequire.NoError(t, err)\n\t\t\tsignerJWK, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\trequire.NoError(t, err)\n\t\t\tsigner, err := jose.NewSigner(jose.SigningKey{\n\t\t\t\tAlgorithm: jose.SignatureAlgorithm(signerJWK.Algorithm),\n\t\t\t\tKey:       signerJWK,\n\t\t\t}, new(jose.SignerOptions))\n\t\t\trequire.NoError(t, err)\n\t\t\tsignerPEMBlock, err := pemutil.Serialize(signerJWK.Public().Key)\n\t\t\trequire.NoError(t, err)\n\t\t\tsignerPEMBytes := pem.EncodeToMemory(signerPEMBlock)\n\t\t\tdpopBytes, err := json.Marshal(struct {\n\t\t\t\tjose.Claims\n\t\t\t\tChallenge string `json:\"chal,omitempty\"`\n\t\t\t\tHandle    string `json:\"handle,omitempty\"`\n\t\t\t\tNonce     string `json:\"nonce,omitempty\"`\n\t\t\t\tHTU       string `json:\"htu,omitempty\"`\n\t\t\t\tName      string `json:\"name,omitempty\"`\n\t\t\t}{\n\t\t\t\tClaims: jose.Claims{\n\t\t\t\t\tSubject:  \"wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com\",\n\t\t\t\t\tAudience: jose.Audience{\"https://ca.example.com/acme/wire/challenge/azID/chID\"},\n\t\t\t\t},\n\t\t\t\tChallenge: \"token\",\n\t\t\t\tHandle:    \"wireapp://%40alice_wire@wire.com\",\n\t\t\t\tNonce:     \"nonce\",\n\t\t\t\tHTU:       \"http://issuer.example.com\",\n\t\t\t\tName:      \"Alice Smith\",\n\t\t\t})\n\t\t\trequire.NoError(t, err)\n\t\t\tdpop, err := dpopSigner.Sign(dpopBytes)\n\t\t\trequire.NoError(t, err)\n\t\t\tproof, err := dpop.CompactSerialize()\n\t\t\trequire.NoError(t, err)\n\t\t\ttokenBytes, err := json.Marshal(struct {\n\t\t\t\tjose.Claims\n\t\t\t\tChallenge string `json:\"chal,omitempty\"`\n\t\t\t\tNonce     string `json:\"nonce,omitempty\"`\n\t\t\t\tCnf       struct {\n\t\t\t\t\tKid string `json:\"kid,omitempty\"`\n\t\t\t\t} `json:\"cnf\"`\n\t\t\t\tProof      string `json:\"proof,omitempty\"`\n\t\t\t\tClientID   string `json:\"client_id\"`\n\t\t\t\tAPIVersion int    `json:\"api_version\"`\n\t\t\t\tScope      string `json:\"scope\"`\n\t\t\t}{\n\t\t\t\tClaims: jose.Claims{\n\t\t\t\t\tIssuer:   \"http://issuer.example.com\",\n\t\t\t\t\tAudience: jose.Audience{\"https://ca.example.com/acme/wire/challenge/azID/chID\"},\n\t\t\t\t\tExpiry:   jose.NewNumericDate(time.Now().Add(1 * time.Minute)),\n\t\t\t\t},\n\t\t\t\tChallenge: \"token\",\n\t\t\t\tNonce:     \"nonce\",\n\t\t\t\tCnf: struct {\n\t\t\t\t\tKid string `json:\"kid,omitempty\"`\n\t\t\t\t}{\n\t\t\t\t\tKid: jwk.KeyID,\n\t\t\t\t},\n\t\t\t\tProof:      proof,\n\t\t\t\tClientID:   \"wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com\",\n\t\t\t\tAPIVersion: 5,\n\t\t\t\tScope:      \"wire_client_id\",\n\t\t\t})\n\t\t\trequire.NoError(t, err)\n\t\t\tsigned, err := signer.Sign(tokenBytes)\n\t\t\trequire.NoError(t, err)\n\t\t\taccessToken, err := signed.CompactSerialize()\n\t\t\trequire.NoError(t, err)\n\t\t\tpayload, err := json.Marshal(struct {\n\t\t\t\tAccessToken string `json:\"access_token\"`\n\t\t\t}{\n\t\t\t\tAccessToken: accessToken,\n\t\t\t})\n\t\t\trequire.NoError(t, err)\n\t\t\tvalueBytes, err := json.Marshal(struct {\n\t\t\t\tName     string `json:\"name,omitempty\"`\n\t\t\t\tDomain   string `json:\"domain,omitempty\"`\n\t\t\t\tClientID string `json:\"client-id,omitempty\"`\n\t\t\t\tHandle   string `json:\"handle,omitempty\"`\n\t\t\t}{\n\t\t\t\tName:     \"Alice Smith\",\n\t\t\t\tDomain:   \"wire.com\",\n\t\t\t\tClientID: \"wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com\",\n\t\t\t\tHandle:   \"wireapp://%40alice_wire@wire.com\",\n\t\t\t})\n\t\t\trequire.NoError(t, err)\n\t\t\tctx := NewProvisionerContext(context.Background(), newWireProvisionerWithOptions(t, &provisioner.Options{\n\t\t\t\tWire: &wireprovisioner.Options{\n\t\t\t\t\tOIDC: &wireprovisioner.OIDCOptions{\n\t\t\t\t\t\tProvider: &wireprovisioner.Provider{\n\t\t\t\t\t\t\tIssuerURL:  \"http://issuer.example.com\",\n\t\t\t\t\t\t\tAlgorithms: []string{\"ES256\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tConfig: &wireprovisioner.Config{\n\t\t\t\t\t\t\tClientID:            \"test\",\n\t\t\t\t\t\t\tSignatureAlgorithms: []string{\"ES256\"},\n\t\t\t\t\t\t\tNow:                 time.Now,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tTransformTemplate: \"\",\n\t\t\t\t\t},\n\t\t\t\t\tDPOP: &wireprovisioner.DPOPOptions{\n\t\t\t\t\t\tTarget:     \"http://issuer.example.com\",\n\t\t\t\t\t\tSigningKey: signerPEMBytes,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}))\n\t\t\tctx = NewLinkerContext(ctx, NewLinker(\"ca.example.com\", \"acme\"))\n\t\t\treturn test{\n\t\t\t\tch: &Challenge{\n\t\t\t\t\tID:              \"chID\",\n\t\t\t\t\tAuthorizationID: \"azID\",\n\t\t\t\t\tAccountID:       \"accID\",\n\t\t\t\t\tToken:           \"token\",\n\t\t\t\t\tType:            \"wire-dpop-01\",\n\t\t\t\t\tStatus:          StatusPending,\n\t\t\t\t\tValue:           string(valueBytes),\n\t\t\t\t},\n\t\t\t\tpayload: payload,\n\t\t\t\tctx:     ctx,\n\t\t\t\tjwk:     jwk,\n\t\t\t\tdb: &MockWireDB{\n\t\t\t\t\tMockDB: MockDB{\n\t\t\t\t\t\tMockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {\n\t\t\t\t\t\t\tassert.Equal(t, \"chID\", updch.ID)\n\t\t\t\t\t\t\tassert.Equal(t, \"token\", updch.Token)\n\t\t\t\t\t\t\tassert.Equal(t, StatusValid, updch.Status)\n\t\t\t\t\t\t\tassert.Equal(t, ChallengeType(\"wire-dpop-01\"), updch.Type)\n\t\t\t\t\t\t\tassert.Equal(t, string(valueBytes), updch.Value)\n\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tMockGetAllOrdersByAccountID: func(ctx context.Context, accountID string) ([]string, error) {\n\t\t\t\t\t\tassert.Equal(t, \"accID\", accountID)\n\t\t\t\t\t\treturn []string{\"orderID\"}, nil\n\t\t\t\t\t},\n\t\t\t\t\tMockCreateDpopToken: func(ctx context.Context, orderID string, dpop map[string]interface{}) error {\n\t\t\t\t\t\tassert.Equal(t, \"orderID\", orderID)\n\t\t\t\t\t\tassert.Equal(t, \"token\", dpop[\"chal\"].(string))\n\t\t\t\t\t\tassert.Equal(t, \"wireapp://%40alice_wire@wire.com\", dpop[\"handle\"].(string))\n\t\t\t\t\t\tassert.Equal(t, \"wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com\", dpop[\"sub\"].(string))\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ttc := run(t)\n\t\t\terr := wireDPOP01Validate(tc.ctx, tc.ch, tc.db, tc.jwk, tc.payload)\n\t\t\tif tc.expectedErr != nil {\n\t\t\t\tvar k *Error\n\t\t\t\tif errors.As(err, &k) {\n\t\t\t\t\tassert.Equal(t, tc.expectedErr.Type, k.Type)\n\t\t\t\t\tassert.Equal(t, tc.expectedErr.Detail, k.Detail)\n\t\t\t\t\tassert.Equal(t, tc.expectedErr.Status, k.Status)\n\t\t\t\t\tassert.Equal(t, tc.expectedErr.Err.Error(), k.Err.Error())\n\t\t\t\t} else {\n\t\t\t\t\tassert.Fail(t, \"unexpected error type\")\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.NoError(t, err)\n\t\t})\n\t}\n}\n\nfunc Test_wireOIDC01Validate(t *testing.T) {\n\tfakeKey := `-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEA5c+4NKZSNQcR1T8qN6SjwgdPZQ0Ge12Ylx/YeGAJ35k=\n-----END PUBLIC KEY-----`\n\ttype test struct {\n\t\tch          *Challenge\n\t\tjwk         *jose.JSONWebKey\n\t\tdb          WireDB\n\t\tpayload     []byte\n\t\tsrv         *httptest.Server\n\t\tctx         context.Context\n\t\texpectedErr *Error\n\t}\n\ttests := map[string]func(t *testing.T) test{\n\t\t\"fail/no-provisioner\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tctx: context.Background(),\n\t\t\t\tdb:  &MockWireDB{},\n\t\t\t\texpectedErr: &Error{\n\t\t\t\t\tType:   \"urn:ietf:params:acme:error:serverInternal\",\n\t\t\t\t\tDetail: \"The server experienced an internal error\",\n\t\t\t\t\tStatus: 500,\n\t\t\t\t\tErr:    errors.New(\"missing provisioner\"),\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"fail/no-linker\": func(t *testing.T) test {\n\t\t\tctx := NewProvisionerContext(context.Background(), newWireProvisionerWithOptions(t, &provisioner.Options{\n\t\t\t\tWire: &wireprovisioner.Options{\n\t\t\t\t\tOIDC: &wireprovisioner.OIDCOptions{\n\t\t\t\t\t\tProvider: &wireprovisioner.Provider{\n\t\t\t\t\t\t\tIssuerURL:  \"https://issuer.example.com\",\n\t\t\t\t\t\t\tAlgorithms: []string{\"ES256\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tConfig: &wireprovisioner.Config{\n\t\t\t\t\t\t\tClientID:            \"test\",\n\t\t\t\t\t\t\tSignatureAlgorithms: []string{\"ES256\"},\n\t\t\t\t\t\t\tNow:                 time.Now,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tTransformTemplate: \"\",\n\t\t\t\t\t},\n\t\t\t\t\tDPOP: &wireprovisioner.DPOPOptions{\n\t\t\t\t\t\tSigningKey: []byte(fakeKey),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}))\n\t\t\treturn test{\n\t\t\t\tctx: ctx,\n\t\t\t\tdb:  &MockWireDB{},\n\t\t\t\texpectedErr: &Error{\n\t\t\t\t\tType:   \"urn:ietf:params:acme:error:serverInternal\",\n\t\t\t\t\tDetail: \"The server experienced an internal error\",\n\t\t\t\t\tStatus: 500,\n\t\t\t\t\tErr:    errors.New(\"missing linker\"),\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"fail/unmarshal\": func(t *testing.T) test {\n\t\t\tctx := NewProvisionerContext(context.Background(), newWireProvisionerWithOptions(t, &provisioner.Options{\n\t\t\t\tWire: &wireprovisioner.Options{\n\t\t\t\t\tOIDC: &wireprovisioner.OIDCOptions{\n\t\t\t\t\t\tProvider: &wireprovisioner.Provider{\n\t\t\t\t\t\t\tIssuerURL:  \"https://issuer.example.com\",\n\t\t\t\t\t\t\tAlgorithms: []string{\"ES256\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tConfig: &wireprovisioner.Config{\n\t\t\t\t\t\t\tClientID:            \"test\",\n\t\t\t\t\t\t\tSignatureAlgorithms: []string{\"ES256\"},\n\t\t\t\t\t\t\tNow:                 time.Now,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tTransformTemplate: \"\",\n\t\t\t\t\t},\n\t\t\t\t\tDPOP: &wireprovisioner.DPOPOptions{\n\t\t\t\t\t\tSigningKey: []byte(fakeKey),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}))\n\t\t\tctx = NewLinkerContext(ctx, NewLinker(\"ca.example.com\", \"acme\"))\n\t\t\treturn test{\n\t\t\t\tctx:     ctx,\n\t\t\t\tpayload: []byte(\"?!\"),\n\t\t\t\tch: &Challenge{\n\t\t\t\t\tID:              \"chID\",\n\t\t\t\t\tAuthorizationID: \"azID\",\n\t\t\t\t\tAccountID:       \"accID\",\n\t\t\t\t\tToken:           \"token\",\n\t\t\t\t\tType:            \"wire-oidc-01\",\n\t\t\t\t\tStatus:          StatusPending,\n\t\t\t\t\tValue:           \"1234\",\n\t\t\t\t},\n\t\t\t\tdb: &MockWireDB{\n\t\t\t\t\tMockDB: MockDB{\n\t\t\t\t\t\tMockUpdateChallenge: func(ctx context.Context, ch *Challenge) error {\n\t\t\t\t\t\t\tassert.Equal(t, \"chID\", ch.ID)\n\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\texpectedErr: &Error{\n\t\t\t\t\tType:   \"urn:ietf:params:acme:error:malformed\",\n\t\t\t\t\tDetail: \"The request message was malformed\",\n\t\t\t\t\tStatus: 400,\n\t\t\t\t\tErr:    errors.New(`error unmarshalling Wire OIDC challenge payload: invalid character '?' looking for beginning of value`),\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"fail/wire-parse-id\": func(t *testing.T) test {\n\t\t\tctx := NewProvisionerContext(context.Background(), newWireProvisionerWithOptions(t, &provisioner.Options{\n\t\t\t\tWire: &wireprovisioner.Options{\n\t\t\t\t\tOIDC: &wireprovisioner.OIDCOptions{\n\t\t\t\t\t\tProvider: &wireprovisioner.Provider{\n\t\t\t\t\t\t\tIssuerURL:  \"https://issuer.example.com\",\n\t\t\t\t\t\t\tAlgorithms: []string{\"ES256\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tConfig: &wireprovisioner.Config{\n\t\t\t\t\t\t\tClientID:            \"test\",\n\t\t\t\t\t\t\tSignatureAlgorithms: []string{\"ES256\"},\n\t\t\t\t\t\t\tNow:                 time.Now,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tTransformTemplate: \"\",\n\t\t\t\t\t},\n\t\t\t\t\tDPOP: &wireprovisioner.DPOPOptions{\n\t\t\t\t\t\tSigningKey: []byte(fakeKey),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}))\n\t\t\tctx = NewLinkerContext(ctx, NewLinker(\"ca.example.com\", \"acme\"))\n\t\t\treturn test{\n\t\t\t\tctx:     ctx,\n\t\t\t\tpayload: []byte(\"{}\"),\n\t\t\t\tch: &Challenge{\n\t\t\t\t\tID:              \"chID\",\n\t\t\t\t\tAuthorizationID: \"azID\",\n\t\t\t\t\tAccountID:       \"accID\",\n\t\t\t\t\tToken:           \"token\",\n\t\t\t\t\tType:            \"wire-oidc-01\",\n\t\t\t\t\tStatus:          StatusPending,\n\t\t\t\t\tValue:           \"1234\",\n\t\t\t\t},\n\t\t\t\tdb: &MockWireDB{},\n\t\t\t\texpectedErr: &Error{\n\t\t\t\t\tType:   \"urn:ietf:params:acme:error:serverInternal\",\n\t\t\t\t\tDetail: \"The server experienced an internal error\",\n\t\t\t\t\tStatus: 500,\n\t\t\t\t\tErr:    errors.New(`error unmarshalling challenge data: json: cannot unmarshal number into Go value of type wire.UserID`),\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"fail/verify\": func(t *testing.T) test {\n\t\t\tjwk, keyAuth := mustAccountAndKeyAuthorization(t, \"token\")\n\t\t\tsignerJWK, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\trequire.NoError(t, err)\n\t\t\tsigner, err := jose.NewSigner(jose.SigningKey{\n\t\t\t\tAlgorithm: jose.SignatureAlgorithm(signerJWK.Algorithm),\n\t\t\t\tKey:       signerJWK,\n\t\t\t}, new(jose.SignerOptions))\n\t\t\trequire.NoError(t, err)\n\t\t\tanotherSignerJWK, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\trequire.NoError(t, err)\n\t\t\tsrv := mustJWKServer(t, anotherSignerJWK.Public())\n\t\t\ttokenBytes, err := json.Marshal(struct {\n\t\t\t\tjose.Claims\n\t\t\t\tName              string `json:\"name,omitempty\"`\n\t\t\t\tPreferredUsername string `json:\"preferred_username,omitempty\"`\n\t\t\t\tKeyAuth           string `json:\"keyauth\"`\n\t\t\t\tACMEAudience      string `json:\"acme_aud\"`\n\t\t\t}{\n\t\t\t\tClaims: jose.Claims{\n\t\t\t\t\tIssuer:   srv.URL,\n\t\t\t\t\tAudience: []string{\"test\"},\n\t\t\t\t\tExpiry:   jose.NewNumericDate(time.Now().Add(1 * time.Minute)),\n\t\t\t\t},\n\t\t\t\tName:              \"Alice Smith\",\n\t\t\t\tPreferredUsername: \"wireapp://%40alice_wire@wire.com\",\n\t\t\t\tKeyAuth:           keyAuth,\n\t\t\t\tACMEAudience:      \"https://ca.example.com/acme/wire/challenge/azID/chID\",\n\t\t\t})\n\t\t\trequire.NoError(t, err)\n\t\t\tsigned, err := signer.Sign(tokenBytes)\n\t\t\trequire.NoError(t, err)\n\t\t\tidToken, err := signed.CompactSerialize()\n\t\t\trequire.NoError(t, err)\n\t\t\tpayload, err := json.Marshal(struct {\n\t\t\t\tIDToken string `json:\"id_token\"`\n\t\t\t}{\n\t\t\t\tIDToken: idToken,\n\t\t\t})\n\t\t\trequire.NoError(t, err)\n\t\t\tvalueBytes, err := json.Marshal(struct {\n\t\t\t\tName     string `json:\"name,omitempty\"`\n\t\t\t\tDomain   string `json:\"domain,omitempty\"`\n\t\t\t\tClientID string `json:\"client-id,omitempty\"`\n\t\t\t\tHandle   string `json:\"handle,omitempty\"`\n\t\t\t}{\n\t\t\t\tName:     \"Alice Smith\",\n\t\t\t\tDomain:   \"wire.com\",\n\t\t\t\tClientID: \"wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com\",\n\t\t\t\tHandle:   \"wireapp://%40alice_wire@wire.com\",\n\t\t\t})\n\t\t\trequire.NoError(t, err)\n\t\t\tctx := NewProvisionerContext(context.Background(), newWireProvisionerWithOptions(t, &provisioner.Options{\n\t\t\t\tWire: &wireprovisioner.Options{\n\t\t\t\t\tOIDC: &wireprovisioner.OIDCOptions{\n\t\t\t\t\t\tProvider: &wireprovisioner.Provider{\n\t\t\t\t\t\t\tIssuerURL:  srv.URL,\n\t\t\t\t\t\t\tJWKSURL:    srv.URL + \"/keys\",\n\t\t\t\t\t\t\tAlgorithms: []string{\"ES256\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tConfig: &wireprovisioner.Config{\n\t\t\t\t\t\t\tClientID:            \"test\",\n\t\t\t\t\t\t\tSignatureAlgorithms: []string{\"ES256\"},\n\t\t\t\t\t\t\tNow:                 time.Now,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tTransformTemplate: \"\",\n\t\t\t\t\t},\n\t\t\t\t\tDPOP: &wireprovisioner.DPOPOptions{\n\t\t\t\t\t\tSigningKey: []byte(fakeKey),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}))\n\t\t\tctx = NewLinkerContext(ctx, NewLinker(\"ca.example.com\", \"acme\"))\n\t\t\treturn test{\n\t\t\t\tch: &Challenge{\n\t\t\t\t\tID:              \"chID\",\n\t\t\t\t\tAuthorizationID: \"azID\",\n\t\t\t\t\tAccountID:       \"accID\",\n\t\t\t\t\tToken:           \"token\",\n\t\t\t\t\tType:            \"wire-oidc-01\",\n\t\t\t\t\tStatus:          StatusPending,\n\t\t\t\t\tValue:           string(valueBytes),\n\t\t\t\t},\n\t\t\t\tsrv:     srv,\n\t\t\t\tpayload: payload,\n\t\t\t\tctx:     ctx,\n\t\t\t\tjwk:     jwk,\n\t\t\t\tdb: &MockWireDB{\n\t\t\t\t\tMockDB: MockDB{\n\t\t\t\t\t\tMockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {\n\t\t\t\t\t\t\tassert.Equal(t, \"chID\", updch.ID)\n\t\t\t\t\t\t\tassert.Equal(t, \"token\", updch.Token)\n\t\t\t\t\t\t\tassert.Equal(t, StatusInvalid, updch.Status)\n\t\t\t\t\t\t\tassert.Equal(t, ChallengeType(\"wire-oidc-01\"), updch.Type)\n\t\t\t\t\t\t\tassert.Equal(t, string(valueBytes), updch.Value)\n\t\t\t\t\t\t\tif assert.NotNil(t, updch.Error) {\n\t\t\t\t\t\t\t\tvar k *Error // NOTE: the error is not returned up, but stored with the challenge instead\n\t\t\t\t\t\t\t\tif errors.As(updch.Error, &k) {\n\t\t\t\t\t\t\t\t\tassert.Equal(t, \"urn:ietf:params:acme:error:rejectedIdentifier\", k.Type)\n\t\t\t\t\t\t\t\t\tassert.Equal(t, \"The server will not issue certificates for the identifier\", k.Detail)\n\t\t\t\t\t\t\t\t\tassert.Equal(t, 400, k.Status)\n\t\t\t\t\t\t\t\t\tassert.Equal(t, `error verifying ID token signature: failed to verify signature: failed to verify id token signature`, k.Err.Error())\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"fail/keyauth-mismatch\": func(t *testing.T) test {\n\t\t\tjwk, _ := mustAccountAndKeyAuthorization(t, \"token\")\n\t\t\tsignerJWK, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\trequire.NoError(t, err)\n\t\t\tsigner, err := jose.NewSigner(jose.SigningKey{\n\t\t\t\tAlgorithm: jose.SignatureAlgorithm(signerJWK.Algorithm),\n\t\t\t\tKey:       signerJWK,\n\t\t\t}, new(jose.SignerOptions))\n\t\t\trequire.NoError(t, err)\n\t\t\tsrv := mustJWKServer(t, signerJWK.Public())\n\t\t\ttokenBytes, err := json.Marshal(struct {\n\t\t\t\tjose.Claims\n\t\t\t\tName              string `json:\"name,omitempty\"`\n\t\t\t\tPreferredUsername string `json:\"preferred_username,omitempty\"`\n\t\t\t\tKeyAuth           string `json:\"keyauth\"`\n\t\t\t\tACMEAudience      string `json:\"acme_aud\"`\n\t\t\t}{\n\t\t\t\tClaims: jose.Claims{\n\t\t\t\t\tIssuer:   srv.URL,\n\t\t\t\t\tAudience: []string{\"test\"},\n\t\t\t\t\tExpiry:   jose.NewNumericDate(time.Now().Add(1 * time.Minute)),\n\t\t\t\t},\n\t\t\t\tName:              \"Alice Smith\",\n\t\t\t\tPreferredUsername: \"wireapp://%40alice_wire@wire.com\",\n\t\t\t\tKeyAuth:           \"wrong-keyauth\",\n\t\t\t\tACMEAudience:      \"https://ca.example.com/acme/wire/challenge/azID/chID\",\n\t\t\t})\n\t\t\trequire.NoError(t, err)\n\t\t\tsigned, err := signer.Sign(tokenBytes)\n\t\t\trequire.NoError(t, err)\n\t\t\tidToken, err := signed.CompactSerialize()\n\t\t\trequire.NoError(t, err)\n\t\t\tpayload, err := json.Marshal(struct {\n\t\t\t\tIDToken string `json:\"id_token\"`\n\t\t\t}{\n\t\t\t\tIDToken: idToken,\n\t\t\t})\n\t\t\trequire.NoError(t, err)\n\t\t\tvalueBytes, err := json.Marshal(struct {\n\t\t\t\tName     string `json:\"name,omitempty\"`\n\t\t\t\tDomain   string `json:\"domain,omitempty\"`\n\t\t\t\tClientID string `json:\"client-id,omitempty\"`\n\t\t\t\tHandle   string `json:\"handle,omitempty\"`\n\t\t\t}{\n\t\t\t\tName:     \"Alice Smith\",\n\t\t\t\tDomain:   \"wire.com\",\n\t\t\t\tClientID: \"wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com\",\n\t\t\t\tHandle:   \"wireapp://%40alice_wire@wire.com\",\n\t\t\t})\n\t\t\trequire.NoError(t, err)\n\t\t\tctx := NewProvisionerContext(context.Background(), newWireProvisionerWithOptions(t, &provisioner.Options{\n\t\t\t\tWire: &wireprovisioner.Options{\n\t\t\t\t\tOIDC: &wireprovisioner.OIDCOptions{\n\t\t\t\t\t\tProvider: &wireprovisioner.Provider{\n\t\t\t\t\t\t\tIssuerURL:  srv.URL,\n\t\t\t\t\t\t\tJWKSURL:    srv.URL + \"/keys\",\n\t\t\t\t\t\t\tAlgorithms: []string{\"ES256\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tConfig: &wireprovisioner.Config{\n\t\t\t\t\t\t\tClientID:            \"test\",\n\t\t\t\t\t\t\tSignatureAlgorithms: []string{\"ES256\"},\n\t\t\t\t\t\t\tNow:                 time.Now,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tTransformTemplate: \"\",\n\t\t\t\t\t},\n\t\t\t\t\tDPOP: &wireprovisioner.DPOPOptions{\n\t\t\t\t\t\tSigningKey: []byte(fakeKey),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}))\n\t\t\tctx = NewLinkerContext(ctx, NewLinker(\"ca.example.com\", \"acme\"))\n\t\t\treturn test{\n\t\t\t\tch: &Challenge{\n\t\t\t\t\tID:              \"chID\",\n\t\t\t\t\tAuthorizationID: \"azID\",\n\t\t\t\t\tAccountID:       \"accID\",\n\t\t\t\t\tToken:           \"token\",\n\t\t\t\t\tType:            \"wire-oidc-01\",\n\t\t\t\t\tStatus:          StatusPending,\n\t\t\t\t\tValue:           string(valueBytes),\n\t\t\t\t},\n\t\t\t\tsrv:     srv,\n\t\t\t\tpayload: payload,\n\t\t\t\tctx:     ctx,\n\t\t\t\tjwk:     jwk,\n\t\t\t\tdb: &MockWireDB{\n\t\t\t\t\tMockDB: MockDB{\n\t\t\t\t\t\tMockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {\n\t\t\t\t\t\t\tassert.Equal(t, \"chID\", updch.ID)\n\t\t\t\t\t\t\tassert.Equal(t, \"token\", updch.Token)\n\t\t\t\t\t\t\tassert.Equal(t, StatusInvalid, updch.Status)\n\t\t\t\t\t\t\tassert.Equal(t, ChallengeType(\"wire-oidc-01\"), updch.Type)\n\t\t\t\t\t\t\tassert.Equal(t, string(valueBytes), updch.Value)\n\t\t\t\t\t\t\tif assert.NotNil(t, updch.Error) {\n\t\t\t\t\t\t\t\tvar k *Error // NOTE: the error is not returned up, but stored with the challenge instead\n\t\t\t\t\t\t\t\tif errors.As(updch.Error, &k) {\n\t\t\t\t\t\t\t\t\tassert.Equal(t, \"urn:ietf:params:acme:error:rejectedIdentifier\", k.Type)\n\t\t\t\t\t\t\t\t\tassert.Equal(t, \"The server will not issue certificates for the identifier\", k.Detail)\n\t\t\t\t\t\t\t\t\tassert.Equal(t, 400, k.Status)\n\t\t\t\t\t\t\t\t\tassert.Contains(t, k.Err.Error(), \"keyAuthorization does not match\")\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"fail/validateWireOIDCClaims\": func(t *testing.T) test {\n\t\t\tjwk, keyAuth := mustAccountAndKeyAuthorization(t, \"token\")\n\t\t\tsignerJWK, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\trequire.NoError(t, err)\n\t\t\tsigner, err := jose.NewSigner(jose.SigningKey{\n\t\t\t\tAlgorithm: jose.SignatureAlgorithm(signerJWK.Algorithm),\n\t\t\t\tKey:       signerJWK,\n\t\t\t}, new(jose.SignerOptions))\n\t\t\trequire.NoError(t, err)\n\t\t\tsrv := mustJWKServer(t, signerJWK.Public())\n\t\t\ttokenBytes, err := json.Marshal(struct {\n\t\t\t\tjose.Claims\n\t\t\t\tName              string `json:\"name,omitempty\"`\n\t\t\t\tPreferredUsername string `json:\"preferred_username,omitempty\"`\n\t\t\t\tKeyAuth           string `json:\"keyauth\"`\n\t\t\t\tACMEAudience      string `json:\"acme_aud\"`\n\t\t\t}{\n\t\t\t\tClaims: jose.Claims{\n\t\t\t\t\tIssuer:   srv.URL,\n\t\t\t\t\tAudience: []string{\"test\"},\n\t\t\t\t\tExpiry:   jose.NewNumericDate(time.Now().Add(1 * time.Minute)),\n\t\t\t\t},\n\t\t\t\tName:              \"Alice Smith\",\n\t\t\t\tPreferredUsername: \"wireapp://%40bob@wire.com\",\n\t\t\t\tKeyAuth:           keyAuth,\n\t\t\t\tACMEAudience:      \"https://ca.example.com/acme/wire/challenge/azID/chID\",\n\t\t\t})\n\t\t\trequire.NoError(t, err)\n\t\t\tsigned, err := signer.Sign(tokenBytes)\n\t\t\trequire.NoError(t, err)\n\t\t\tidToken, err := signed.CompactSerialize()\n\t\t\trequire.NoError(t, err)\n\t\t\tpayload, err := json.Marshal(struct {\n\t\t\t\tIDToken string `json:\"id_token\"`\n\t\t\t}{\n\t\t\t\tIDToken: idToken,\n\t\t\t})\n\t\t\trequire.NoError(t, err)\n\t\t\tvalueBytes, err := json.Marshal(struct {\n\t\t\t\tName     string `json:\"name,omitempty\"`\n\t\t\t\tDomain   string `json:\"domain,omitempty\"`\n\t\t\t\tClientID string `json:\"client-id,omitempty\"`\n\t\t\t\tHandle   string `json:\"handle,omitempty\"`\n\t\t\t}{\n\t\t\t\tName:     \"Alice Smith\",\n\t\t\t\tDomain:   \"wire.com\",\n\t\t\t\tClientID: \"wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com\",\n\t\t\t\tHandle:   \"wireapp://%40alice_wire@wire.com\",\n\t\t\t})\n\t\t\trequire.NoError(t, err)\n\t\t\tctx := NewProvisionerContext(context.Background(), newWireProvisionerWithOptions(t, &provisioner.Options{\n\t\t\t\tWire: &wireprovisioner.Options{\n\t\t\t\t\tOIDC: &wireprovisioner.OIDCOptions{\n\t\t\t\t\t\tProvider: &wireprovisioner.Provider{\n\t\t\t\t\t\t\tIssuerURL:  srv.URL,\n\t\t\t\t\t\t\tJWKSURL:    srv.URL + \"/keys\",\n\t\t\t\t\t\t\tAlgorithms: []string{\"ES256\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tConfig: &wireprovisioner.Config{\n\t\t\t\t\t\t\tClientID:            \"test\",\n\t\t\t\t\t\t\tSignatureAlgorithms: []string{\"ES256\"},\n\t\t\t\t\t\t\tNow:                 time.Now,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tTransformTemplate: \"\",\n\t\t\t\t\t},\n\t\t\t\t\tDPOP: &wireprovisioner.DPOPOptions{\n\t\t\t\t\t\tSigningKey: []byte(fakeKey),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}))\n\t\t\tctx = NewLinkerContext(ctx, NewLinker(\"ca.example.com\", \"acme\"))\n\t\t\treturn test{\n\t\t\t\tch: &Challenge{\n\t\t\t\t\tID:              \"chID\",\n\t\t\t\t\tAuthorizationID: \"azID\",\n\t\t\t\t\tAccountID:       \"accID\",\n\t\t\t\t\tToken:           \"token\",\n\t\t\t\t\tType:            \"wire-oidc-01\",\n\t\t\t\t\tStatus:          StatusPending,\n\t\t\t\t\tValue:           string(valueBytes),\n\t\t\t\t},\n\t\t\t\tsrv:     srv,\n\t\t\t\tpayload: payload,\n\t\t\t\tctx:     ctx,\n\t\t\t\tjwk:     jwk,\n\t\t\t\tdb: &MockWireDB{\n\t\t\t\t\tMockDB: MockDB{\n\t\t\t\t\t\tMockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {\n\t\t\t\t\t\t\tassert.Equal(t, \"chID\", updch.ID)\n\t\t\t\t\t\t\tassert.Equal(t, \"token\", updch.Token)\n\t\t\t\t\t\t\tassert.Equal(t, StatusInvalid, updch.Status)\n\t\t\t\t\t\t\tassert.Equal(t, ChallengeType(\"wire-oidc-01\"), updch.Type)\n\t\t\t\t\t\t\tassert.Equal(t, string(valueBytes), updch.Value)\n\t\t\t\t\t\t\tif assert.NotNil(t, updch.Error) {\n\t\t\t\t\t\t\t\tvar k *Error // NOTE: the error is not returned up, but stored with the challenge instead\n\t\t\t\t\t\t\t\tif errors.As(updch.Error, &k) {\n\t\t\t\t\t\t\t\t\tassert.Equal(t, \"urn:ietf:params:acme:error:rejectedIdentifier\", k.Type)\n\t\t\t\t\t\t\t\t\tassert.Equal(t, \"The server will not issue certificates for the identifier\", k.Detail)\n\t\t\t\t\t\t\t\t\tassert.Equal(t, 400, k.Status)\n\t\t\t\t\t\t\t\t\tassert.Equal(t, `claims in OIDC ID token don't match: invalid 'preferred_username' \"wireapp://%40bob@wire.com\" after transformation`, k.Err.Error())\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"fail/db.UpdateChallenge\": func(t *testing.T) test {\n\t\t\tjwk, keyAuth := mustAccountAndKeyAuthorization(t, \"token\")\n\t\t\tsignerJWK, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\trequire.NoError(t, err)\n\t\t\tsigner, err := jose.NewSigner(jose.SigningKey{\n\t\t\t\tAlgorithm: jose.SignatureAlgorithm(signerJWK.Algorithm),\n\t\t\t\tKey:       signerJWK,\n\t\t\t}, new(jose.SignerOptions))\n\t\t\trequire.NoError(t, err)\n\t\t\tsrv := mustJWKServer(t, signerJWK.Public())\n\t\t\ttokenBytes, err := json.Marshal(struct {\n\t\t\t\tjose.Claims\n\t\t\t\tName              string `json:\"name,omitempty\"`\n\t\t\t\tPreferredUsername string `json:\"preferred_username,omitempty\"`\n\t\t\t\tKeyAuth           string `json:\"keyauth\"`\n\t\t\t\tACMEAudience      string `json:\"acme_aud\"`\n\t\t\t}{\n\t\t\t\tClaims: jose.Claims{\n\t\t\t\t\tIssuer:   srv.URL,\n\t\t\t\t\tAudience: []string{\"test\"},\n\t\t\t\t\tExpiry:   jose.NewNumericDate(time.Now().Add(1 * time.Minute)),\n\t\t\t\t},\n\t\t\t\tName:              \"Alice Smith\",\n\t\t\t\tPreferredUsername: \"wireapp://%40alice_wire@wire.com\",\n\t\t\t\tKeyAuth:           keyAuth,\n\t\t\t\tACMEAudience:      \"https://ca.example.com/acme/wire/challenge/azID/chID\",\n\t\t\t})\n\t\t\trequire.NoError(t, err)\n\t\t\tsigned, err := signer.Sign(tokenBytes)\n\t\t\trequire.NoError(t, err)\n\t\t\tidToken, err := signed.CompactSerialize()\n\t\t\trequire.NoError(t, err)\n\t\t\tpayload, err := json.Marshal(struct {\n\t\t\t\tIDToken string `json:\"id_token\"`\n\t\t\t}{\n\t\t\t\tIDToken: idToken,\n\t\t\t})\n\t\t\trequire.NoError(t, err)\n\t\t\tvalueBytes, err := json.Marshal(struct {\n\t\t\t\tName     string `json:\"name,omitempty\"`\n\t\t\t\tDomain   string `json:\"domain,omitempty\"`\n\t\t\t\tClientID string `json:\"client-id,omitempty\"`\n\t\t\t\tHandle   string `json:\"handle,omitempty\"`\n\t\t\t}{\n\t\t\t\tName:     \"Alice Smith\",\n\t\t\t\tDomain:   \"wire.com\",\n\t\t\t\tClientID: \"wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com\",\n\t\t\t\tHandle:   \"wireapp://%40alice_wire@wire.com\",\n\t\t\t})\n\t\t\trequire.NoError(t, err)\n\t\t\tctx := NewProvisionerContext(context.Background(), newWireProvisionerWithOptions(t, &provisioner.Options{\n\t\t\t\tWire: &wireprovisioner.Options{\n\t\t\t\t\tOIDC: &wireprovisioner.OIDCOptions{\n\t\t\t\t\t\tProvider: &wireprovisioner.Provider{\n\t\t\t\t\t\t\tIssuerURL:  srv.URL,\n\t\t\t\t\t\t\tJWKSURL:    srv.URL + \"/keys\",\n\t\t\t\t\t\t\tAlgorithms: []string{\"ES256\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tConfig: &wireprovisioner.Config{\n\t\t\t\t\t\t\tClientID:            \"test\",\n\t\t\t\t\t\t\tSignatureAlgorithms: []string{\"ES256\"},\n\t\t\t\t\t\t\tNow:                 time.Now,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tTransformTemplate: \"\",\n\t\t\t\t\t},\n\t\t\t\t\tDPOP: &wireprovisioner.DPOPOptions{\n\t\t\t\t\t\tSigningKey: []byte(fakeKey),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}))\n\t\t\tctx = NewLinkerContext(ctx, NewLinker(\"ca.example.com\", \"acme\"))\n\t\t\treturn test{\n\t\t\t\tch: &Challenge{\n\t\t\t\t\tID:              \"chID\",\n\t\t\t\t\tAuthorizationID: \"azID\",\n\t\t\t\t\tAccountID:       \"accID\",\n\t\t\t\t\tToken:           \"token\",\n\t\t\t\t\tType:            \"wire-oidc-01\",\n\t\t\t\t\tStatus:          StatusPending,\n\t\t\t\t\tValue:           string(valueBytes),\n\t\t\t\t},\n\t\t\t\tsrv:     srv,\n\t\t\t\tpayload: payload,\n\t\t\t\tctx:     ctx,\n\t\t\t\tjwk:     jwk,\n\t\t\t\tdb: &MockWireDB{\n\t\t\t\t\tMockDB: MockDB{\n\t\t\t\t\t\tMockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {\n\t\t\t\t\t\t\tassert.Equal(t, \"chID\", updch.ID)\n\t\t\t\t\t\t\tassert.Equal(t, \"token\", updch.Token)\n\t\t\t\t\t\t\tassert.Equal(t, StatusValid, updch.Status)\n\t\t\t\t\t\t\tassert.Equal(t, ChallengeType(\"wire-oidc-01\"), updch.Type)\n\t\t\t\t\t\t\tassert.Equal(t, string(valueBytes), updch.Value)\n\t\t\t\t\t\t\treturn errors.New(\"fail\")\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\texpectedErr: &Error{\n\t\t\t\t\tType:   \"urn:ietf:params:acme:error:serverInternal\",\n\t\t\t\t\tDetail: \"The server experienced an internal error\",\n\t\t\t\t\tStatus: 500,\n\t\t\t\t\tErr:    errors.New(`error updating challenge: fail`),\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"fail/db.GetAllOrdersByAccountID\": func(t *testing.T) test {\n\t\t\tjwk, keyAuth := mustAccountAndKeyAuthorization(t, \"token\")\n\t\t\tsignerJWK, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\trequire.NoError(t, err)\n\t\t\tsigner, err := jose.NewSigner(jose.SigningKey{\n\t\t\t\tAlgorithm: jose.SignatureAlgorithm(signerJWK.Algorithm),\n\t\t\t\tKey:       signerJWK,\n\t\t\t}, new(jose.SignerOptions))\n\t\t\trequire.NoError(t, err)\n\t\t\tsrv := mustJWKServer(t, signerJWK.Public())\n\t\t\ttokenBytes, err := json.Marshal(struct {\n\t\t\t\tjose.Claims\n\t\t\t\tName              string `json:\"name,omitempty\"`\n\t\t\t\tPreferredUsername string `json:\"preferred_username,omitempty\"`\n\t\t\t\tKeyAuth           string `json:\"keyauth\"`\n\t\t\t\tACMEAudience      string `json:\"acme_aud\"`\n\t\t\t}{\n\t\t\t\tClaims: jose.Claims{\n\t\t\t\t\tIssuer:   srv.URL,\n\t\t\t\t\tAudience: []string{\"test\"},\n\t\t\t\t\tExpiry:   jose.NewNumericDate(time.Now().Add(1 * time.Minute)),\n\t\t\t\t},\n\t\t\t\tName:              \"Alice Smith\",\n\t\t\t\tPreferredUsername: \"wireapp://%40alice_wire@wire.com\",\n\t\t\t\tKeyAuth:           keyAuth,\n\t\t\t\tACMEAudience:      \"https://ca.example.com/acme/wire/challenge/azID/chID\",\n\t\t\t})\n\t\t\trequire.NoError(t, err)\n\t\t\tsigned, err := signer.Sign(tokenBytes)\n\t\t\trequire.NoError(t, err)\n\t\t\tidToken, err := signed.CompactSerialize()\n\t\t\trequire.NoError(t, err)\n\t\t\tpayload, err := json.Marshal(struct {\n\t\t\t\tIDToken string `json:\"id_token\"`\n\t\t\t}{\n\t\t\t\tIDToken: idToken,\n\t\t\t})\n\t\t\trequire.NoError(t, err)\n\t\t\tvalueBytes, err := json.Marshal(struct {\n\t\t\t\tName     string `json:\"name,omitempty\"`\n\t\t\t\tDomain   string `json:\"domain,omitempty\"`\n\t\t\t\tClientID string `json:\"client-id,omitempty\"`\n\t\t\t\tHandle   string `json:\"handle,omitempty\"`\n\t\t\t}{\n\t\t\t\tName:     \"Alice Smith\",\n\t\t\t\tDomain:   \"wire.com\",\n\t\t\t\tClientID: \"wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com\",\n\t\t\t\tHandle:   \"wireapp://%40alice_wire@wire.com\",\n\t\t\t})\n\t\t\trequire.NoError(t, err)\n\t\t\tctx := NewProvisionerContext(context.Background(), newWireProvisionerWithOptions(t, &provisioner.Options{\n\t\t\t\tWire: &wireprovisioner.Options{\n\t\t\t\t\tOIDC: &wireprovisioner.OIDCOptions{\n\t\t\t\t\t\tProvider: &wireprovisioner.Provider{\n\t\t\t\t\t\t\tIssuerURL:  srv.URL,\n\t\t\t\t\t\t\tJWKSURL:    srv.URL + \"/keys\",\n\t\t\t\t\t\t\tAlgorithms: []string{\"ES256\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tConfig: &wireprovisioner.Config{\n\t\t\t\t\t\t\tClientID:            \"test\",\n\t\t\t\t\t\t\tSignatureAlgorithms: []string{\"ES256\"},\n\t\t\t\t\t\t\tNow:                 time.Now,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tTransformTemplate: \"\",\n\t\t\t\t\t},\n\t\t\t\t\tDPOP: &wireprovisioner.DPOPOptions{\n\t\t\t\t\t\tSigningKey: []byte(fakeKey),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}))\n\t\t\tctx = NewLinkerContext(ctx, NewLinker(\"ca.example.com\", \"acme\"))\n\t\t\treturn test{\n\t\t\t\tch: &Challenge{\n\t\t\t\t\tID:              \"chID\",\n\t\t\t\t\tAuthorizationID: \"azID\",\n\t\t\t\t\tAccountID:       \"accID\",\n\t\t\t\t\tToken:           \"token\",\n\t\t\t\t\tType:            \"wire-oidc-01\",\n\t\t\t\t\tStatus:          StatusPending,\n\t\t\t\t\tValue:           string(valueBytes),\n\t\t\t\t},\n\t\t\t\tsrv:     srv,\n\t\t\t\tpayload: payload,\n\t\t\t\tctx:     ctx,\n\t\t\t\tjwk:     jwk,\n\t\t\t\tdb: &MockWireDB{\n\t\t\t\t\tMockDB: MockDB{\n\t\t\t\t\t\tMockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {\n\t\t\t\t\t\t\tassert.Equal(t, \"chID\", updch.ID)\n\t\t\t\t\t\t\tassert.Equal(t, \"token\", updch.Token)\n\t\t\t\t\t\t\tassert.Equal(t, StatusValid, updch.Status)\n\t\t\t\t\t\t\tassert.Equal(t, ChallengeType(\"wire-oidc-01\"), updch.Type)\n\t\t\t\t\t\t\tassert.Equal(t, string(valueBytes), updch.Value)\n\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tMockGetAllOrdersByAccountID: func(ctx context.Context, accountID string) ([]string, error) {\n\t\t\t\t\t\tassert.Equal(t, \"accID\", accountID)\n\t\t\t\t\t\treturn nil, errors.New(\"fail\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\texpectedErr: &Error{\n\t\t\t\t\tType:   \"urn:ietf:params:acme:error:serverInternal\",\n\t\t\t\t\tDetail: \"The server experienced an internal error\",\n\t\t\t\t\tStatus: 500,\n\t\t\t\t\tErr:    errors.New(`could not retrieve current order by account id: fail`),\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"fail/db.GetAllOrdersByAccountID-zero\": func(t *testing.T) test {\n\t\t\tjwk, keyAuth := mustAccountAndKeyAuthorization(t, \"token\")\n\t\t\tsignerJWK, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\trequire.NoError(t, err)\n\t\t\tsigner, err := jose.NewSigner(jose.SigningKey{\n\t\t\t\tAlgorithm: jose.SignatureAlgorithm(signerJWK.Algorithm),\n\t\t\t\tKey:       signerJWK,\n\t\t\t}, new(jose.SignerOptions))\n\t\t\trequire.NoError(t, err)\n\t\t\tsrv := mustJWKServer(t, signerJWK.Public())\n\t\t\ttokenBytes, err := json.Marshal(struct {\n\t\t\t\tjose.Claims\n\t\t\t\tName              string `json:\"name,omitempty\"`\n\t\t\t\tPreferredUsername string `json:\"preferred_username,omitempty\"`\n\t\t\t\tKeyAuth           string `json:\"keyauth\"`\n\t\t\t\tACMEAudience      string `json:\"acme_aud\"`\n\t\t\t}{\n\t\t\t\tClaims: jose.Claims{\n\t\t\t\t\tIssuer:   srv.URL,\n\t\t\t\t\tAudience: []string{\"test\"},\n\t\t\t\t\tExpiry:   jose.NewNumericDate(time.Now().Add(1 * time.Minute)),\n\t\t\t\t},\n\t\t\t\tName:              \"Alice Smith\",\n\t\t\t\tPreferredUsername: \"wireapp://%40alice_wire@wire.com\",\n\t\t\t\tKeyAuth:           keyAuth,\n\t\t\t\tACMEAudience:      \"https://ca.example.com/acme/wire/challenge/azID/chID\",\n\t\t\t})\n\t\t\trequire.NoError(t, err)\n\t\t\tsigned, err := signer.Sign(tokenBytes)\n\t\t\trequire.NoError(t, err)\n\t\t\tidToken, err := signed.CompactSerialize()\n\t\t\trequire.NoError(t, err)\n\t\t\tpayload, err := json.Marshal(struct {\n\t\t\t\tIDToken string `json:\"id_token\"`\n\t\t\t}{\n\t\t\t\tIDToken: idToken,\n\t\t\t})\n\t\t\trequire.NoError(t, err)\n\t\t\tvalueBytes, err := json.Marshal(struct {\n\t\t\t\tName     string `json:\"name,omitempty\"`\n\t\t\t\tDomain   string `json:\"domain,omitempty\"`\n\t\t\t\tClientID string `json:\"client-id,omitempty\"`\n\t\t\t\tHandle   string `json:\"handle,omitempty\"`\n\t\t\t}{\n\t\t\t\tName:     \"Alice Smith\",\n\t\t\t\tDomain:   \"wire.com\",\n\t\t\t\tClientID: \"wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com\",\n\t\t\t\tHandle:   \"wireapp://%40alice_wire@wire.com\",\n\t\t\t})\n\t\t\trequire.NoError(t, err)\n\t\t\tctx := NewProvisionerContext(context.Background(), newWireProvisionerWithOptions(t, &provisioner.Options{\n\t\t\t\tWire: &wireprovisioner.Options{\n\t\t\t\t\tOIDC: &wireprovisioner.OIDCOptions{\n\t\t\t\t\t\tProvider: &wireprovisioner.Provider{\n\t\t\t\t\t\t\tIssuerURL:  srv.URL,\n\t\t\t\t\t\t\tJWKSURL:    srv.URL + \"/keys\",\n\t\t\t\t\t\t\tAlgorithms: []string{\"ES256\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tConfig: &wireprovisioner.Config{\n\t\t\t\t\t\t\tClientID:            \"test\",\n\t\t\t\t\t\t\tSignatureAlgorithms: []string{\"ES256\"},\n\t\t\t\t\t\t\tNow:                 time.Now,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tTransformTemplate: \"\",\n\t\t\t\t\t},\n\t\t\t\t\tDPOP: &wireprovisioner.DPOPOptions{\n\t\t\t\t\t\tSigningKey: []byte(fakeKey),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}))\n\t\t\tctx = NewLinkerContext(ctx, NewLinker(\"ca.example.com\", \"acme\"))\n\t\t\treturn test{\n\t\t\t\tch: &Challenge{\n\t\t\t\t\tID:              \"chID\",\n\t\t\t\t\tAuthorizationID: \"azID\",\n\t\t\t\t\tAccountID:       \"accID\",\n\t\t\t\t\tToken:           \"token\",\n\t\t\t\t\tType:            \"wire-oidc-01\",\n\t\t\t\t\tStatus:          StatusPending,\n\t\t\t\t\tValue:           string(valueBytes),\n\t\t\t\t},\n\t\t\t\tsrv:     srv,\n\t\t\t\tpayload: payload,\n\t\t\t\tctx:     ctx,\n\t\t\t\tjwk:     jwk,\n\t\t\t\tdb: &MockWireDB{\n\t\t\t\t\tMockDB: MockDB{\n\t\t\t\t\t\tMockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {\n\t\t\t\t\t\t\tassert.Equal(t, \"chID\", updch.ID)\n\t\t\t\t\t\t\tassert.Equal(t, \"token\", updch.Token)\n\t\t\t\t\t\t\tassert.Equal(t, StatusValid, updch.Status)\n\t\t\t\t\t\t\tassert.Equal(t, ChallengeType(\"wire-oidc-01\"), updch.Type)\n\t\t\t\t\t\t\tassert.Equal(t, string(valueBytes), updch.Value)\n\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tMockGetAllOrdersByAccountID: func(ctx context.Context, accountID string) ([]string, error) {\n\t\t\t\t\t\tassert.Equal(t, \"accID\", accountID)\n\t\t\t\t\t\treturn []string{}, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\texpectedErr: &Error{\n\t\t\t\t\tType:   \"urn:ietf:params:acme:error:serverInternal\",\n\t\t\t\t\tDetail: \"The server experienced an internal error\",\n\t\t\t\t\tStatus: 500,\n\t\t\t\t\tErr:    errors.New(`there are not enough orders for this account for this custom OIDC challenge`),\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"fail/db.CreateOidcToken\": func(t *testing.T) test {\n\t\t\tjwk, keyAuth := mustAccountAndKeyAuthorization(t, \"token\")\n\t\t\tsignerJWK, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\trequire.NoError(t, err)\n\t\t\tsigner, err := jose.NewSigner(jose.SigningKey{\n\t\t\t\tAlgorithm: jose.SignatureAlgorithm(signerJWK.Algorithm),\n\t\t\t\tKey:       signerJWK,\n\t\t\t}, new(jose.SignerOptions))\n\t\t\trequire.NoError(t, err)\n\t\t\tsrv := mustJWKServer(t, signerJWK.Public())\n\t\t\ttokenBytes, err := json.Marshal(struct {\n\t\t\t\tjose.Claims\n\t\t\t\tName              string `json:\"name,omitempty\"`\n\t\t\t\tPreferredUsername string `json:\"preferred_username,omitempty\"`\n\t\t\t\tKeyAuth           string `json:\"keyauth\"`\n\t\t\t\tACMEAudience      string `json:\"acme_aud\"`\n\t\t\t}{\n\t\t\t\tClaims: jose.Claims{\n\t\t\t\t\tIssuer:   srv.URL,\n\t\t\t\t\tAudience: []string{\"test\"},\n\t\t\t\t\tExpiry:   jose.NewNumericDate(time.Now().Add(1 * time.Minute)),\n\t\t\t\t},\n\t\t\t\tName:              \"Alice Smith\",\n\t\t\t\tPreferredUsername: \"wireapp://%40alice_wire@wire.com\",\n\t\t\t\tKeyAuth:           keyAuth,\n\t\t\t\tACMEAudience:      \"https://ca.example.com/acme/wire/challenge/azID/chID\",\n\t\t\t})\n\t\t\trequire.NoError(t, err)\n\t\t\tsigned, err := signer.Sign(tokenBytes)\n\t\t\trequire.NoError(t, err)\n\t\t\tidToken, err := signed.CompactSerialize()\n\t\t\trequire.NoError(t, err)\n\t\t\tpayload, err := json.Marshal(struct {\n\t\t\t\tIDToken string `json:\"id_token\"`\n\t\t\t}{\n\t\t\t\tIDToken: idToken,\n\t\t\t})\n\t\t\trequire.NoError(t, err)\n\t\t\tvalueBytes, err := json.Marshal(struct {\n\t\t\t\tName     string `json:\"name,omitempty\"`\n\t\t\t\tDomain   string `json:\"domain,omitempty\"`\n\t\t\t\tClientID string `json:\"client-id,omitempty\"`\n\t\t\t\tHandle   string `json:\"handle,omitempty\"`\n\t\t\t}{\n\t\t\t\tName:     \"Alice Smith\",\n\t\t\t\tDomain:   \"wire.com\",\n\t\t\t\tClientID: \"wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com\",\n\t\t\t\tHandle:   \"wireapp://%40alice_wire@wire.com\",\n\t\t\t})\n\t\t\trequire.NoError(t, err)\n\t\t\tctx := NewProvisionerContext(context.Background(), newWireProvisionerWithOptions(t, &provisioner.Options{\n\t\t\t\tWire: &wireprovisioner.Options{\n\t\t\t\t\tOIDC: &wireprovisioner.OIDCOptions{\n\t\t\t\t\t\tProvider: &wireprovisioner.Provider{\n\t\t\t\t\t\t\tIssuerURL:  srv.URL,\n\t\t\t\t\t\t\tJWKSURL:    srv.URL + \"/keys\",\n\t\t\t\t\t\t\tAlgorithms: []string{\"ES256\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tConfig: &wireprovisioner.Config{\n\t\t\t\t\t\t\tClientID:            \"test\",\n\t\t\t\t\t\t\tSignatureAlgorithms: []string{\"ES256\"},\n\t\t\t\t\t\t\tNow:                 time.Now,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tTransformTemplate: \"\",\n\t\t\t\t\t},\n\t\t\t\t\tDPOP: &wireprovisioner.DPOPOptions{\n\t\t\t\t\t\tSigningKey: []byte(fakeKey),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}))\n\t\t\tctx = NewLinkerContext(ctx, NewLinker(\"ca.example.com\", \"acme\"))\n\t\t\treturn test{\n\t\t\t\tch: &Challenge{\n\t\t\t\t\tID:              \"chID\",\n\t\t\t\t\tAuthorizationID: \"azID\",\n\t\t\t\t\tAccountID:       \"accID\",\n\t\t\t\t\tToken:           \"token\",\n\t\t\t\t\tType:            \"wire-oidc-01\",\n\t\t\t\t\tStatus:          StatusPending,\n\t\t\t\t\tValue:           string(valueBytes),\n\t\t\t\t},\n\t\t\t\tsrv:     srv,\n\t\t\t\tpayload: payload,\n\t\t\t\tctx:     ctx,\n\t\t\t\tjwk:     jwk,\n\t\t\t\tdb: &MockWireDB{\n\t\t\t\t\tMockDB: MockDB{\n\t\t\t\t\t\tMockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {\n\t\t\t\t\t\t\tassert.Equal(t, \"chID\", updch.ID)\n\t\t\t\t\t\t\tassert.Equal(t, \"token\", updch.Token)\n\t\t\t\t\t\t\tassert.Equal(t, StatusValid, updch.Status)\n\t\t\t\t\t\t\tassert.Equal(t, ChallengeType(\"wire-oidc-01\"), updch.Type)\n\t\t\t\t\t\t\tassert.Equal(t, string(valueBytes), updch.Value)\n\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tMockGetAllOrdersByAccountID: func(ctx context.Context, accountID string) ([]string, error) {\n\t\t\t\t\t\tassert.Equal(t, \"accID\", accountID)\n\t\t\t\t\t\treturn []string{\"orderID\"}, nil\n\t\t\t\t\t},\n\t\t\t\t\tMockCreateOidcToken: func(ctx context.Context, orderID string, idToken map[string]interface{}) error {\n\t\t\t\t\t\tassert.Equal(t, \"orderID\", orderID)\n\t\t\t\t\t\tassert.Equal(t, \"Alice Smith\", idToken[\"name\"].(string))\n\t\t\t\t\t\tassert.Equal(t, \"wireapp://%40alice_wire@wire.com\", idToken[\"preferred_username\"].(string))\n\t\t\t\t\t\treturn errors.New(\"fail\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\texpectedErr: &Error{\n\t\t\t\t\tType:   \"urn:ietf:params:acme:error:serverInternal\",\n\t\t\t\t\tDetail: \"The server experienced an internal error\",\n\t\t\t\t\tStatus: 500,\n\t\t\t\t\tErr:    errors.New(`failed storing OIDC id token: fail`),\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"ok/wire-oidc-01\": func(t *testing.T) test {\n\t\t\tjwk, keyAuth := mustAccountAndKeyAuthorization(t, \"token\")\n\t\t\tsignerJWK, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\trequire.NoError(t, err)\n\t\t\tsigner, err := jose.NewSigner(jose.SigningKey{\n\t\t\t\tAlgorithm: jose.SignatureAlgorithm(signerJWK.Algorithm),\n\t\t\t\tKey:       signerJWK,\n\t\t\t}, new(jose.SignerOptions))\n\t\t\trequire.NoError(t, err)\n\t\t\tsrv := mustJWKServer(t, signerJWK.Public())\n\t\t\ttokenBytes, err := json.Marshal(struct {\n\t\t\t\tjose.Claims\n\t\t\t\tName              string `json:\"name,omitempty\"`\n\t\t\t\tPreferredUsername string `json:\"preferred_username,omitempty\"`\n\t\t\t\tKeyAuth           string `json:\"keyauth\"`\n\t\t\t\tACMEAudience      string `json:\"acme_aud\"`\n\t\t\t}{\n\t\t\t\tClaims: jose.Claims{\n\t\t\t\t\tIssuer:   srv.URL,\n\t\t\t\t\tAudience: []string{\"test\"},\n\t\t\t\t\tExpiry:   jose.NewNumericDate(time.Now().Add(1 * time.Minute)),\n\t\t\t\t},\n\t\t\t\tName:              \"Alice Smith\",\n\t\t\t\tPreferredUsername: \"wireapp://%40alice_wire@wire.com\",\n\t\t\t\tKeyAuth:           keyAuth,\n\t\t\t\tACMEAudience:      \"https://ca.example.com/acme/wire/challenge/azID/chID\",\n\t\t\t})\n\t\t\trequire.NoError(t, err)\n\t\t\tsigned, err := signer.Sign(tokenBytes)\n\t\t\trequire.NoError(t, err)\n\t\t\tidToken, err := signed.CompactSerialize()\n\t\t\trequire.NoError(t, err)\n\t\t\tpayload, err := json.Marshal(struct {\n\t\t\t\tIDToken string `json:\"id_token\"`\n\t\t\t}{\n\t\t\t\tIDToken: idToken,\n\t\t\t})\n\t\t\trequire.NoError(t, err)\n\t\t\tvalueBytes, err := json.Marshal(struct {\n\t\t\t\tName     string `json:\"name,omitempty\"`\n\t\t\t\tDomain   string `json:\"domain,omitempty\"`\n\t\t\t\tClientID string `json:\"client-id,omitempty\"`\n\t\t\t\tHandle   string `json:\"handle,omitempty\"`\n\t\t\t}{\n\t\t\t\tName:     \"Alice Smith\",\n\t\t\t\tDomain:   \"wire.com\",\n\t\t\t\tClientID: \"wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com\",\n\t\t\t\tHandle:   \"wireapp://%40alice_wire@wire.com\",\n\t\t\t})\n\t\t\trequire.NoError(t, err)\n\t\t\tctx := NewProvisionerContext(context.Background(), newWireProvisionerWithOptions(t, &provisioner.Options{\n\t\t\t\tWire: &wireprovisioner.Options{\n\t\t\t\t\tOIDC: &wireprovisioner.OIDCOptions{\n\t\t\t\t\t\tProvider: &wireprovisioner.Provider{\n\t\t\t\t\t\t\tIssuerURL:  srv.URL,\n\t\t\t\t\t\t\tJWKSURL:    srv.URL + \"/keys\",\n\t\t\t\t\t\t\tAlgorithms: []string{\"ES256\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tConfig: &wireprovisioner.Config{\n\t\t\t\t\t\t\tClientID:            \"test\",\n\t\t\t\t\t\t\tSignatureAlgorithms: []string{\"ES256\"},\n\t\t\t\t\t\t\tNow:                 time.Now,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tTransformTemplate: \"\",\n\t\t\t\t\t},\n\t\t\t\t\tDPOP: &wireprovisioner.DPOPOptions{\n\t\t\t\t\t\tSigningKey: []byte(fakeKey),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}))\n\t\t\tctx = NewLinkerContext(ctx, NewLinker(\"ca.example.com\", \"acme\"))\n\t\t\treturn test{\n\t\t\t\tch: &Challenge{\n\t\t\t\t\tID:              \"chID\",\n\t\t\t\t\tAuthorizationID: \"azID\",\n\t\t\t\t\tAccountID:       \"accID\",\n\t\t\t\t\tToken:           \"token\",\n\t\t\t\t\tType:            \"wire-oidc-01\",\n\t\t\t\t\tStatus:          StatusPending,\n\t\t\t\t\tValue:           string(valueBytes),\n\t\t\t\t},\n\t\t\t\tsrv:     srv,\n\t\t\t\tpayload: payload,\n\t\t\t\tctx:     ctx,\n\t\t\t\tjwk:     jwk,\n\t\t\t\tdb: &MockWireDB{\n\t\t\t\t\tMockDB: MockDB{\n\t\t\t\t\t\tMockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {\n\t\t\t\t\t\t\tassert.Equal(t, \"chID\", updch.ID)\n\t\t\t\t\t\t\tassert.Equal(t, \"token\", updch.Token)\n\t\t\t\t\t\t\tassert.Equal(t, StatusValid, updch.Status)\n\t\t\t\t\t\t\tassert.Equal(t, ChallengeType(\"wire-oidc-01\"), updch.Type)\n\t\t\t\t\t\t\tassert.Equal(t, string(valueBytes), updch.Value)\n\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tMockGetAllOrdersByAccountID: func(ctx context.Context, accountID string) ([]string, error) {\n\t\t\t\t\t\tassert.Equal(t, \"accID\", accountID)\n\t\t\t\t\t\treturn []string{\"orderID\"}, nil\n\t\t\t\t\t},\n\t\t\t\t\tMockCreateOidcToken: func(ctx context.Context, orderID string, idToken map[string]interface{}) error {\n\t\t\t\t\t\tassert.Equal(t, \"orderID\", orderID)\n\t\t\t\t\t\tassert.Equal(t, \"Alice Smith\", idToken[\"name\"].(string))\n\t\t\t\t\t\tassert.Equal(t, \"wireapp://%40alice_wire@wire.com\", idToken[\"preferred_username\"].(string))\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ttc := run(t)\n\t\t\tif tc.srv != nil {\n\t\t\t\tdefer tc.srv.Close()\n\t\t\t}\n\t\t\terr := wireOIDC01Validate(tc.ctx, tc.ch, tc.db, tc.jwk, tc.payload)\n\t\t\tif tc.expectedErr != nil {\n\t\t\t\tvar k *Error\n\t\t\t\tif errors.As(err, &k) {\n\t\t\t\t\tassert.Equal(t, tc.expectedErr.Type, k.Type)\n\t\t\t\t\tassert.Equal(t, tc.expectedErr.Detail, k.Detail)\n\t\t\t\t\tassert.Equal(t, tc.expectedErr.Status, k.Status)\n\t\t\t\t\tassert.Equal(t, tc.expectedErr.Err.Error(), k.Err.Error())\n\t\t\t\t} else {\n\t\t\t\t\tassert.Fail(t, \"unexpected error type\")\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.NoError(t, err)\n\t\t})\n\t}\n}\n\nfunc Test_parseAndVerifyWireAccessToken(t *testing.T) {\n\tt.Skip(\"skip until we can retrieve public key from e2e test, so that we can actually verify the token\")\n\tkey := `\n-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAB2IYqBWXAouDt3WcCZgCM3t9gumMEKMlgMsGenSu+fA=\n-----END PUBLIC KEY-----`\n\tpublicKey, err := pemutil.Parse([]byte(key))\n\trequire.NoError(t, err)\n\tpk, ok := publicKey.(ed25519.PublicKey)\n\trequire.True(t, ok)\n\n\tissuer := \"http://wire.com:19983/clients/7a41cf5b79683410/access-token\"\n\twireID := wire.DeviceID{\n\t\tClientID: \"wireapp://guVX5xeFS3eTatmXBIyA4A!7a41cf5b79683410@wire.com\",\n\t\tHandle:   \"wireapp://%40alice_wire@wire.com\",\n\t}\n\n\ttoken := `eyJhbGciOiJFZERTQSIsInR5cCI6ImF0K2p3dCIsImp3ayI6eyJrdHkiOiJPS1AiLCJjcnYiOiJFZDI1NTE5IiwieCI6Im8zcWZhQ045a2FzSnZJRlhPdFNMTGhlYW0wTE5jcVF5MHdBMk9PeFRRNW8ifX0.eyJpYXQiOjE3MDU0OTc3MzksImV4cCI6MTcwNTUwMTY5OSwibmJmIjoxNzA1NDk3NzM5LCJpc3MiOiJodHRwOi8vd2lyZS5jb206MTY4MjQvY2xpZW50cy8zN2ZlOThiZDQwZDBkZmUvYWNjZXNzLXRva2VuIiwic3ViIjoid2lyZWFwcDovLzE4NXdIUmtRVHdTOTVGODhaZTQ1SlEhMzdmZTk4YmQ0MGQwZGZlQHdpcmUuY29tIiwiYXVkIjoiaHR0cHM6Ly9zdGVwY2E6NTUwMjMvYWNtZS93aXJlL2NoYWxsZW5nZS9SeEdSWGVoRGxCcHcxNTJQTVUzem0xY2M0cEtGcHVWRi9RWnRFazdQNUVFRXhadHBSYngydjVoYlc3QXB1S2NOSSIsImp0aSI6ImU1MzllODYzLTRkNTgtNGMwMS1iYjk3LTYwODdiNTEzOWIyMCIsIm5vbmNlIjoiUzJKYWVWcExkV28wUkZKaFFrWndXR0ZKY0VoVlFrNUxXVGd4WkhkRFVqQSIsImNoYWwiOiIyaDFPdUdxbTBKUXd6bHVsWGtLSTJEMGZiRDgzRUIxdyIsImNuZiI6eyJraWQiOiJhSEY3MVhYeG0tTWE5Q05zSjNaU1RKTjlYS0ZxOFFmOGh2UTJLN3NLQmQ4In0sInByb29mIjoiZXlKaGJHY2lPaUpGWkVSVFFTSXNJblI1Y0NJNkltUndiM0FyYW5kMElpd2lhbmRySWpwN0ltdDBlU0k2SWs5TFVDSXNJbU55ZGlJNklrVmtNalUxTVRraUxDSjRJam9pWVVsaVMwcFBha0poWXpZeVF6TnRhVmhHVjAxb09ITTJkRXQzUkROaGNHRnVSMHBQZURaVVFYVklRU0o5ZlEuZXlKcFlYUWlPakUzTURVME9UYzNNemtzSW1WNGNDSTZNVGN3TlRVd05Ea3pPU3dpYm1KbUlqb3hOekExTkRrM056TTVMQ0p6ZFdJaU9pSjNhWEpsWVhCd09pOHZNVGcxZDBoU2ExRlVkMU01TlVZNE9GcGxORFZLVVNFek4yWmxPVGhpWkRRd1pEQmtabVZBZDJseVpTNWpiMjBpTENKaGRXUWlPaUpvZEhSd2N6b3ZMM04wWlhCallUbzFOVEF5TXk5aFkyMWxMM2RwY21VdlkyaGhiR3hsYm1kbEwxSjRSMUpZWldoRWJFSndkekUxTWxCTlZUTjZiVEZqWXpSd1MwWndkVlpHTDFGYWRFVnJOMUExUlVWRmVGcDBjRkppZURKMk5XaGlWemRCY0hWTFkwNUpJaXdpYW5ScElqb2lNV1kxTUdRM1lUQXRaamt6WmkwME5XWXdMV0V3TWpBdE1ETm1NREJpTlRreVlUUmtJaXdpYm05dVkyVWlPaUpUTWtwaFpWWndUR1JYYnpCU1JrcG9VV3RhZDFkSFJrcGpSV2hXVVdzMVRGZFVaM2hhU0dSRVZXcEJJaXdpYUhSdElqb2lVRTlUVkNJc0ltaDBkU0k2SW1oMGRIQTZMeTkzYVhKbExtTnZiVG94TmpneU5DOWpiR2xsYm5Sekx6TTNabVU1T0dKa05EQmtNR1JtWlM5aFkyTmxjM010ZEc5clpXNGlMQ0pqYUdGc0lqb2lNbWd4VDNWSGNXMHdTbEYzZW14MWJGaHJTMGt5UkRCbVlrUTRNMFZDTVhjaUxDSm9ZVzVrYkdVaU9pSjNhWEpsWVhCd09pOHZKVFF3WVd4cFkyVmZkMmx5WlVCM2FYSmxMbU52YlNJc0luUmxZVzBpT2lKM2FYSmxJbjAuZlNmQnFuWWlfMTRhZEc5MDAyZ0RJdEgybXNyYW55eXVnR0g5bHpFcmprdmRGbkRPOFRVWWRYUXJKUzdlX3BlU0lzcGxlRUVkaGhzc0gwM3FBWHY2QXciLCJjbGllbnRfaWQiOiJ3aXJlYXBwOi8vMTg1d0hSa1FUd1M5NUY4OFplNDVKUSEzN2ZlOThiZDQwZDBkZmVAd2lyZS5jb20iLCJhcGlfdmVyc2lvbiI6NSwic2NvcGUiOiJ3aXJlX2NsaWVudF9pZCJ9.GKK7ZsJ8EWJjeaHqf8P48H9mluJhxyXUmI0FO3xstda3XDJIK7Z5Ur4hi1OIJB0ZsS5BqRVT2q5whL4KP9hZCA`\n\tch := &Challenge{\n\t\tToken: \"bXUGNpUfcRx3EhB34xP3y62aQZoGZS6j\",\n\t}\n\n\tissuedAtUnix, err := strconv.ParseInt(\"1704985205\", 10, 64)\n\trequire.NoError(t, err)\n\tissuedAt := time.Unix(issuedAtUnix, 0)\n\n\tjwkBytes := []byte(`{\"crv\": \"Ed25519\", \"kty\": \"OKP\", \"x\": \"1L1eH2a6AgVvzTp5ZalKRfq6pVPOtEjI7h8TPzBYFgM\"}`)\n\tvar accountJWK jose.JSONWebKey\n\tjson.Unmarshal(jwkBytes, &accountJWK)\n\n\trawKid, err := accountJWK.Thumbprint(crypto.SHA256)\n\trequire.NoError(t, err)\n\taccountJWK.KeyID = base64.RawURLEncoding.EncodeToString(rawKid)\n\n\tat, dpop, err := parseAndVerifyWireAccessToken(wireVerifyParams{\n\t\ttoken:     token,\n\t\ttokenKey:  pk,\n\t\tdpopKey:   accountJWK.Public(),\n\t\tdpopKeyID: accountJWK.KeyID,\n\t\tissuer:    issuer,\n\t\twireID:    wireID,\n\t\tchToken:   ch.Token,\n\t\tt:         issuedAt.Add(1 * time.Minute), // set validation time to be one minute after issuance\n\t})\n\n\tif assert.NoError(t, err) {\n\t\t// token assertions\n\t\tassert.Equal(t, \"42c46d4c-e510-4175-9fb5-d055e125a49d\", at.ID)\n\t\tassert.Equal(t, \"http://wire.com:19983/clients/7a41cf5b79683410/access-token\", at.Issuer)\n\t\tassert.Equal(t, \"wireapp://guVX5xeFS3eTatmXBIyA4A!7a41cf5b79683410@wire.com\", at.Subject)\n\t\tassert.Contains(t, at.Audience, \"http://wire.com:19983/clients/7a41cf5b79683410/access-token\")\n\t\tassert.Equal(t, \"bXUGNpUfcRx3EhB34xP3y62aQZoGZS6j\", at.Challenge)\n\t\tassert.Equal(t, \"wireapp://guVX5xeFS3eTatmXBIyA4A!7a41cf5b79683410@wire.com\", at.ClientID)\n\t\tassert.Equal(t, 5, at.APIVersion)\n\t\tassert.Equal(t, \"wire_client_id\", at.Scope)\n\t\tif assert.NotNil(t, at.Cnf) {\n\t\t\tassert.Equal(t, \"oMWfNDJQsI5cPlXN5UoBNncKtc4f2dq2vwCjjXsqw7Q\", at.Cnf.Kid)\n\t\t}\n\n\t\t// dpop proof assertions\n\t\tdt := *dpop\n\t\tassert.Equal(t, \"bXUGNpUfcRx3EhB34xP3y62aQZoGZS6j\", dt[\"chal\"].(string))\n\t\tassert.Equal(t, \"wireapp://%40alice_wire@wire.com\", dt[\"handle\"].(string))\n\t\tassert.Equal(t, \"POST\", dt[\"htm\"].(string))\n\t\tassert.Equal(t, \"http://wire.com:19983/clients/7a41cf5b79683410/access-token\", dt[\"htu\"].(string))\n\t\tassert.Equal(t, \"5e6684cb-6b48-468d-b091-ff04bed6ec2e\", dt[\"jti\"].(string))\n\t\tassert.Equal(t, \"UEJyR2dqOEhzZFJEYWJBaTkyODNEYTE2aEs0dHIxcEc\", dt[\"nonce\"].(string))\n\t\tassert.Equal(t, \"wireapp://guVX5xeFS3eTatmXBIyA4A!7a41cf5b79683410@wire.com\", dt[\"sub\"].(string))\n\t\tassert.Equal(t, \"wire\", dt[\"team\"].(string))\n\t}\n}\n\nfunc Test_validateWireOIDCClaims(t *testing.T) {\n\tfakeKey := `\n-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEA5c+4NKZSNQcR1T8qN6SjwgdPZQ0Ge12Ylx/YeGAJ35k=\n-----END PUBLIC KEY-----`\n\topts := &wireprovisioner.Options{\n\t\tOIDC: &wireprovisioner.OIDCOptions{\n\t\t\tProvider: &wireprovisioner.Provider{\n\t\t\t\tIssuerURL:  \"http://dex:15818/dex\",\n\t\t\t\tAlgorithms: []string{\"ES256\"},\n\t\t\t},\n\t\t\tConfig: &wireprovisioner.Config{\n\t\t\t\tClientID:            \"wireapp\",\n\t\t\t\tSignatureAlgorithms: []string{\"RS256\"},\n\t\t\t\tNow: func() time.Time {\n\t\t\t\t\treturn time.Date(2024, 1, 12, 18, 32, 41, 0, time.UTC) // (Token Expiry: 2024-01-12 21:32:42 +0100 CET)\n\t\t\t\t},\n\t\t\t\tInsecureSkipSignatureCheck: true, // skipping signature check for this specific test\n\t\t\t},\n\t\t\tTransformTemplate: `{\"name\": \"{{ .preferred_username }}\", \"preferred_username\": \"{{ .name }}\"}`,\n\t\t},\n\t\tDPOP: &wireprovisioner.DPOPOptions{\n\t\t\tSigningKey: []byte(fakeKey),\n\t\t},\n\t}\n\n\terr := opts.Validate()\n\trequire.NoError(t, err)\n\n\tidTokenString := `eyJhbGciOiJSUzI1NiIsImtpZCI6IjZhNDZlYzQ3YTQzYWI1ZTc4NzU3MzM5NWY1MGY4ZGQ5MWI2OTM5MzcifQ.eyJpc3MiOiJodHRwOi8vZGV4OjE1ODE4L2RleCIsInN1YiI6IkNqcDNhWEpsWVhCd09pOHZTMmh0VjBOTFpFTlRXakoyT1dWTWFHRk9XVlp6WnlFeU5UZzFNVEpoT0RRek5qTXhaV1V6UUhkcGNtVXVZMjl0RWdSc1pHRnciLCJhdWQiOiJ3aXJlYXBwIiwiZXhwIjoxNzA1MDkxNTYyLCJpYXQiOjE3MDUwMDUxNjIsIm5vbmNlIjoib0VjUzBRQUNXLVIyZWkxS09wUmZ2QSIsImF0X2hhc2giOiJoYzk0NmFwS25FeEV5TDVlSzJZMzdRIiwiY19oYXNoIjoidmRubFp2V1d1bVd1Z2NYR1JpOU5FUSIsIm5hbWUiOiJ3aXJlYXBwOi8vJTQwYWxpY2Vfd2lyZUB3aXJlLmNvbSIsInByZWZlcnJlZF91c2VybmFtZSI6IkFsaWNlIFNtaXRoIn0.aEBhWJugBJ9J_0L_4odUCg8SR8HMXVjd__X8uZRo42BSJQQO7-wdpy0jU3S4FOX9fQKr68wD61gS_QsnhfiT7w9U36mLpxaYlNVDCYfpa-gklVFit_0mjUOukXajTLK6H527TGiSss8z22utc40ckS1SbZa2BzKu3yOcqnFHUQwQc5sLYfpRABTB6WBoYFtnWDzdpyWJDaOzz7lfKYv2JBnf9vV8u8SYm-6gNKgtiQ3UUnjhIVUjdfHet2BMvmV2ooZ8V441RULCzKKG_sWZba-D_k_TOnSholGobtUOcKHlmVlmfUe8v7kuyBdhbPcembfgViaNldLQGKZjZfgvLg`\n\tctx := context.Background()\n\to := opts.GetOIDCOptions()\n\tverifier, err := o.GetVerifier(ctx)\n\trequire.NoError(t, err)\n\tidToken, err := verifier.Verify(ctx, idTokenString)\n\trequire.NoError(t, err)\n\n\twireID := wire.UserID{\n\t\tName:   \"Alice Smith\",\n\t\tHandle: \"wireapp://%40alice_wire@wire.com\",\n\t}\n\n\tgot, err := validateWireOIDCClaims(o, idToken, wireID)\n\tassert.NoError(t, err)\n\n\tassert.Equal(t, \"wireapp://%40alice_wire@wire.com\", got[\"preferred_username\"].(string))\n\tassert.Equal(t, \"Alice Smith\", got[\"name\"].(string))\n\tassert.Equal(t, \"http://dex:15818/dex\", got[\"iss\"].(string))\n}\n\nfunc createWireOptions(t *testing.T, transformTemplate string) *wireprovisioner.Options {\n\tt.Helper()\n\tfakeKey := `\n-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEA5c+4NKZSNQcR1T8qN6SjwgdPZQ0Ge12Ylx/YeGAJ35k=\n-----END PUBLIC KEY-----`\n\topts := &wireprovisioner.Options{\n\t\tOIDC: &wireprovisioner.OIDCOptions{\n\t\t\tProvider: &wireprovisioner.Provider{\n\t\t\t\tIssuerURL:  \"https://issuer.example.com\",\n\t\t\t\tAlgorithms: []string{\"ES256\"},\n\t\t\t},\n\t\t\tConfig: &wireprovisioner.Config{\n\t\t\t\tClientID:            \"unit test\",\n\t\t\t\tSignatureAlgorithms: []string{\"ES256\"},\n\t\t\t\tNow:                 time.Now,\n\t\t\t},\n\t\t\tTransformTemplate: transformTemplate,\n\t\t},\n\t\tDPOP: &wireprovisioner.DPOPOptions{\n\t\t\tSigningKey: []byte(fakeKey),\n\t\t},\n\t}\n\n\terr := opts.Validate()\n\trequire.NoError(t, err)\n\n\treturn opts\n}\n\nfunc Test_idTokenTransformation(t *testing.T) {\n\t// {\"name\": \"wireapp://%40alice_wire@wire.com\", \"preferred_username\": \"Alice Smith\", \"iss\": \"http://dex:15818/dex\", ...}\n\tidTokenString := `eyJhbGciOiJSUzI1NiIsImtpZCI6IjZhNDZlYzQ3YTQzYWI1ZTc4NzU3MzM5NWY1MGY4ZGQ5MWI2OTM5MzcifQ.eyJpc3MiOiJodHRwOi8vZGV4OjE1ODE4L2RleCIsInN1YiI6IkNqcDNhWEpsWVhCd09pOHZTMmh0VjBOTFpFTlRXakoyT1dWTWFHRk9XVlp6WnlFeU5UZzFNVEpoT0RRek5qTXhaV1V6UUhkcGNtVXVZMjl0RWdSc1pHRnciLCJhdWQiOiJ3aXJlYXBwIiwiZXhwIjoxNzA1MDkxNTYyLCJpYXQiOjE3MDUwMDUxNjIsIm5vbmNlIjoib0VjUzBRQUNXLVIyZWkxS09wUmZ2QSIsImF0X2hhc2giOiJoYzk0NmFwS25FeEV5TDVlSzJZMzdRIiwiY19oYXNoIjoidmRubFp2V1d1bVd1Z2NYR1JpOU5FUSIsIm5hbWUiOiJ3aXJlYXBwOi8vJTQwYWxpY2Vfd2lyZUB3aXJlLmNvbSIsInByZWZlcnJlZF91c2VybmFtZSI6IkFsaWNlIFNtaXRoIn0.aEBhWJugBJ9J_0L_4odUCg8SR8HMXVjd__X8uZRo42BSJQQO7-wdpy0jU3S4FOX9fQKr68wD61gS_QsnhfiT7w9U36mLpxaYlNVDCYfpa-gklVFit_0mjUOukXajTLK6H527TGiSss8z22utc40ckS1SbZa2BzKu3yOcqnFHUQwQc5sLYfpRABTB6WBoYFtnWDzdpyWJDaOzz7lfKYv2JBnf9vV8u8SYm-6gNKgtiQ3UUnjhIVUjdfHet2BMvmV2ooZ8V441RULCzKKG_sWZba-D_k_TOnSholGobtUOcKHlmVlmfUe8v7kuyBdhbPcembfgViaNldLQGKZjZfgvLg`\n\tvar claims struct {\n\t\tName   string `json:\"name,omitempty\"`\n\t\tHandle string `json:\"preferred_username,omitempty\"`\n\t\tIssuer string `json:\"iss,omitempty\"`\n\t}\n\n\tidToken, err := jose.ParseSigned(idTokenString)\n\trequire.NoError(t, err)\n\terr = idToken.UnsafeClaimsWithoutVerification(&claims)\n\trequire.NoError(t, err)\n\n\t// original token contains \"Alice Smith\" as handle, and name as \"wireapp://%40alice_wire@wire.com\"\n\tassert.Equal(t, \"Alice Smith\", claims.Handle)\n\tassert.Equal(t, \"wireapp://%40alice_wire@wire.com\", claims.Name)\n\tassert.Equal(t, \"http://dex:15818/dex\", claims.Issuer)\n\n\tvar m map[string]any\n\terr = idToken.UnsafeClaimsWithoutVerification(&m)\n\trequire.NoError(t, err)\n\n\topts := createWireOptions(t, \"\") // uses default transformation template\n\tresult, err := opts.GetOIDCOptions().Transform(m)\n\trequire.NoError(t, err)\n\n\t// default transformation sets preferred username to handle; name as name\n\tassert.Equal(t, \"Alice Smith\", result[\"preferred_username\"].(string))\n\tassert.Equal(t, \"wireapp://%40alice_wire@wire.com\", result[\"name\"].(string))\n\tassert.Equal(t, \"http://dex:15818/dex\", result[\"iss\"].(string))\n\n\t// swap the preferred_name and the name\n\tswap := `{\"name\": \"{{ .preferred_username }}\", \"preferred_username\": \"{{ .name }}\"}`\n\topts = createWireOptions(t, swap)\n\tresult, err = opts.GetOIDCOptions().Transform(m)\n\trequire.NoError(t, err)\n\n\t// with the transformation, handle now contains wireapp://%40alice_wire@wire.com, name contains Alice Smith\n\tassert.Equal(t, \"wireapp://%40alice_wire@wire.com\", result[\"preferred_username\"].(string))\n\tassert.Equal(t, \"Alice Smith\", result[\"name\"].(string))\n\tassert.Equal(t, \"http://dex:15818/dex\", result[\"iss\"].(string))\n}\n"
  },
  {
    "path": "acme/client.go",
    "content": "package acme\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"net\"\n\t\"net/http\"\n\t\"time\"\n)\n\n// Client is the interface used to verify ACME challenges.\ntype Client interface {\n\t// Get issues an HTTP GET to the specified URL.\n\tGet(url string) (*http.Response, error)\n\n\t// LookupTXT returns the DNS TXT records for the given domain name.\n\tLookupTxt(name string) ([]string, error)\n\n\t// TLSDial connects to the given network address using net.Dialer and then\n\t// initiates a TLS handshake, returning the resulting TLS connection.\n\tTLSDial(network, addr string, config *tls.Config) (*tls.Conn, error)\n}\n\ntype clientKey struct{}\n\n// NewClientContext adds the given client to the context.\nfunc NewClientContext(ctx context.Context, c Client) context.Context {\n\treturn context.WithValue(ctx, clientKey{}, c)\n}\n\n// ClientFromContext returns the current client from the given context.\nfunc ClientFromContext(ctx context.Context) (c Client, ok bool) {\n\tc, ok = ctx.Value(clientKey{}).(Client)\n\treturn\n}\n\n// MustClientFromContext returns the current client from the given context. It will\n// return a new instance of the client if it does not exist.\nfunc MustClientFromContext(ctx context.Context) Client {\n\tc, ok := ClientFromContext(ctx)\n\tif !ok {\n\t\treturn NewClient()\n\t}\n\treturn c\n}\n\ntype client struct {\n\thttp   *http.Client\n\tdialer *net.Dialer\n}\n\n// NewClient returns an implementation of Client for verifying ACME challenges.\nfunc NewClient() Client {\n\treturn &client{\n\t\thttp: &http.Client{\n\t\t\tTimeout: 30 * time.Second,\n\t\t\tTransport: &http.Transport{\n\t\t\t\tProxy: http.ProxyFromEnvironment,\n\t\t\t\tTLSClientConfig: &tls.Config{\n\t\t\t\t\t//nolint:gosec // used on tls-alpn-01 challenge\n\t\t\t\t\tInsecureSkipVerify: true, // lgtm[go/disabled-certificate-check]\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tdialer: &net.Dialer{\n\t\t\tTimeout: 30 * time.Second,\n\t\t},\n\t}\n}\n\nfunc (c *client) Get(url string) (*http.Response, error) {\n\treturn c.http.Get(url)\n}\n\nfunc (c *client) LookupTxt(name string) ([]string, error) {\n\treturn net.LookupTXT(name)\n}\n\nfunc (c *client) TLSDial(network, addr string, config *tls.Config) (*tls.Conn, error) {\n\treturn tls.DialWithDialer(c.dialer, network, addr, config)\n}\n"
  },
  {
    "path": "acme/common.go",
    "content": "package acme\n\nimport (\n\t\"context\"\n\t\"crypto/x509\"\n\t\"time\"\n\n\t\"github.com/smallstep/certificates/authority\"\n\t\"github.com/smallstep/certificates/authority/provisioner\"\n)\n\n// Clock that returns time in UTC rounded to seconds.\ntype Clock struct{}\n\n// Now returns the UTC time rounded to seconds.\nfunc (c *Clock) Now() time.Time {\n\treturn time.Now().UTC().Truncate(time.Second)\n}\n\nvar clock Clock\n\n// CertificateAuthority is the interface implemented by a CA authority.\ntype CertificateAuthority interface {\n\tSignWithContext(ctx context.Context, cr *x509.CertificateRequest, opts provisioner.SignOptions, signOpts ...provisioner.SignOption) ([]*x509.Certificate, error)\n\tAreSANsAllowed(ctx context.Context, sans []string) error\n\tIsRevoked(sn string) (bool, error)\n\tRevoke(context.Context, *authority.RevokeOptions) error\n\tLoadProvisionerByName(string) (provisioner.Interface, error)\n\tGetBackdate() *time.Duration\n}\n\n// NewContext adds the given acme components to the context.\nfunc NewContext(ctx context.Context, db DB, client Client, linker Linker, fn PrerequisitesChecker) context.Context {\n\tctx = NewDatabaseContext(ctx, db)\n\tctx = NewClientContext(ctx, client)\n\tctx = NewLinkerContext(ctx, linker)\n\t// Prerequisite checker is optional.\n\tif fn != nil {\n\t\tctx = NewPrerequisitesCheckerContext(ctx, fn)\n\t}\n\treturn ctx\n}\n\n// PrerequisitesChecker is a function that checks if all prerequisites for\n// serving ACME are met by the CA configuration.\ntype PrerequisitesChecker func(ctx context.Context) (bool, error)\n\n// DefaultPrerequisitesChecker is the default PrerequisiteChecker and returns\n// always true.\nfunc DefaultPrerequisitesChecker(context.Context) (bool, error) {\n\treturn true, nil\n}\n\ntype prerequisitesKey struct{}\n\n// NewPrerequisitesCheckerContext adds the given PrerequisitesChecker to the\n// context.\nfunc NewPrerequisitesCheckerContext(ctx context.Context, fn PrerequisitesChecker) context.Context {\n\treturn context.WithValue(ctx, prerequisitesKey{}, fn)\n}\n\n// PrerequisitesCheckerFromContext returns the PrerequisitesChecker in the\n// context.\nfunc PrerequisitesCheckerFromContext(ctx context.Context) (PrerequisitesChecker, bool) {\n\tfn, ok := ctx.Value(prerequisitesKey{}).(PrerequisitesChecker)\n\treturn fn, ok && fn != nil\n}\n\n// Provisioner is an interface that implements a subset of the provisioner.Interface --\n// only those methods required by the ACME api/authority.\ntype Provisioner interface {\n\tAuthorizeOrderIdentifier(ctx context.Context, identifier provisioner.ACMEIdentifier) error\n\tAuthorizeSign(ctx context.Context, token string) ([]provisioner.SignOption, error)\n\tAuthorizeRevoke(ctx context.Context, token string) error\n\tIsChallengeEnabled(ctx context.Context, challenge provisioner.ACMEChallenge) bool\n\tIsAttestationFormatEnabled(ctx context.Context, format provisioner.ACMEAttestationFormat) bool\n\tGetAttestationRoots() (*x509.CertPool, bool)\n\tGetID() string\n\tGetName() string\n\tDefaultTLSCertDuration() time.Duration\n\tGetOptions() *provisioner.Options\n}\n\ntype provisionerKey struct{}\n\n// NewProvisionerContext adds the given provisioner to the context.\nfunc NewProvisionerContext(ctx context.Context, v Provisioner) context.Context {\n\treturn context.WithValue(ctx, provisionerKey{}, v)\n}\n\n// ProvisionerFromContext returns the current provisioner from the given context.\nfunc ProvisionerFromContext(ctx context.Context) (v Provisioner, ok bool) {\n\tv, ok = ctx.Value(provisionerKey{}).(Provisioner)\n\treturn\n}\n\n// MustProvisionerFromContext returns the current provisioner from the given context.\n// It will panic if it's not in the context.\nfunc MustProvisionerFromContext(ctx context.Context) Provisioner {\n\tvar (\n\t\tv  Provisioner\n\t\tok bool\n\t)\n\tif v, ok = ProvisionerFromContext(ctx); !ok {\n\t\tpanic(\"acme provisioner is not the context\")\n\t}\n\treturn v\n}\n\n// MockProvisioner for testing\ntype MockProvisioner struct {\n\tMret1                     interface{}\n\tMerr                      error\n\tMgetID                    func() string\n\tMgetName                  func() string\n\tMauthorizeOrderIdentifier func(ctx context.Context, identifier provisioner.ACMEIdentifier) error\n\tMauthorizeSign            func(ctx context.Context, ott string) ([]provisioner.SignOption, error)\n\tMauthorizeRevoke          func(ctx context.Context, token string) error\n\tMisChallengeEnabled       func(ctx context.Context, challenge provisioner.ACMEChallenge) bool\n\tMisAttFormatEnabled       func(ctx context.Context, format provisioner.ACMEAttestationFormat) bool\n\tMgetAttestationRoots      func() (*x509.CertPool, bool)\n\tMdefaultTLSCertDuration   func() time.Duration\n\tMgetOptions               func() *provisioner.Options\n}\n\n// GetName mock\nfunc (m *MockProvisioner) GetName() string {\n\tif m.MgetName != nil {\n\t\treturn m.MgetName()\n\t}\n\treturn m.Mret1.(string)\n}\n\n// AuthorizeOrderIdentifier mock\nfunc (m *MockProvisioner) AuthorizeOrderIdentifier(ctx context.Context, identifier provisioner.ACMEIdentifier) error {\n\tif m.MauthorizeOrderIdentifier != nil {\n\t\treturn m.MauthorizeOrderIdentifier(ctx, identifier)\n\t}\n\treturn m.Merr\n}\n\n// AuthorizeSign mock\nfunc (m *MockProvisioner) AuthorizeSign(ctx context.Context, ott string) ([]provisioner.SignOption, error) {\n\tif m.MauthorizeSign != nil {\n\t\treturn m.MauthorizeSign(ctx, ott)\n\t}\n\treturn m.Mret1.([]provisioner.SignOption), m.Merr\n}\n\n// AuthorizeRevoke mock\nfunc (m *MockProvisioner) AuthorizeRevoke(ctx context.Context, token string) error {\n\tif m.MauthorizeRevoke != nil {\n\t\treturn m.MauthorizeRevoke(ctx, token)\n\t}\n\treturn m.Merr\n}\n\n// IsChallengeEnabled mock\nfunc (m *MockProvisioner) IsChallengeEnabled(ctx context.Context, challenge provisioner.ACMEChallenge) bool {\n\tif m.MisChallengeEnabled != nil {\n\t\treturn m.MisChallengeEnabled(ctx, challenge)\n\t}\n\treturn m.Merr == nil\n}\n\n// IsAttestationFormatEnabled mock\nfunc (m *MockProvisioner) IsAttestationFormatEnabled(ctx context.Context, format provisioner.ACMEAttestationFormat) bool {\n\tif m.MisAttFormatEnabled != nil {\n\t\treturn m.MisAttFormatEnabled(ctx, format)\n\t}\n\treturn m.Merr == nil\n}\n\nfunc (m *MockProvisioner) GetAttestationRoots() (*x509.CertPool, bool) {\n\tif m.MgetAttestationRoots != nil {\n\t\treturn m.MgetAttestationRoots()\n\t}\n\treturn m.Mret1.(*x509.CertPool), m.Mret1 != nil\n}\n\n// DefaultTLSCertDuration mock\nfunc (m *MockProvisioner) DefaultTLSCertDuration() time.Duration {\n\tif m.MdefaultTLSCertDuration != nil {\n\t\treturn m.MdefaultTLSCertDuration()\n\t}\n\treturn m.Mret1.(time.Duration)\n}\n\n// GetOptions mock\nfunc (m *MockProvisioner) GetOptions() *provisioner.Options {\n\tif m.MgetOptions != nil {\n\t\treturn m.MgetOptions()\n\t}\n\treturn m.Mret1.(*provisioner.Options)\n}\n\n// GetID mock\nfunc (m *MockProvisioner) GetID() string {\n\tif m.MgetID != nil {\n\t\treturn m.MgetID()\n\t}\n\treturn m.Mret1.(string)\n}\n"
  },
  {
    "path": "acme/db/nosql/account.go",
    "content": "package nosql\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n\t\"github.com/smallstep/certificates/acme\"\n\tnosqlDB \"github.com/smallstep/nosql\"\n\t\"go.step.sm/crypto/jose\"\n)\n\n// dbAccount represents an ACME account.\ntype dbAccount struct {\n\tID              string           `json:\"id\"`\n\tKey             *jose.JSONWebKey `json:\"key\"`\n\tContact         []string         `json:\"contact,omitempty\"`\n\tStatus          acme.Status      `json:\"status\"`\n\tLocationPrefix  string           `json:\"locationPrefix\"`\n\tProvisionerID   string           `json:\"provisionerID,omitempty\"`\n\tProvisionerName string           `json:\"provisionerName\"`\n\tCreatedAt       time.Time        `json:\"createdAt\"`\n\tDeactivatedAt   time.Time        `json:\"deactivatedAt\"`\n}\n\nfunc (dba *dbAccount) clone() *dbAccount {\n\tnu := *dba\n\treturn &nu\n}\n\nfunc (db *DB) getAccountIDByKeyID(_ context.Context, kid string) (string, error) {\n\tid, err := db.db.Get(accountByKeyIDTable, []byte(kid))\n\tif err != nil {\n\t\tif nosqlDB.IsErrNotFound(err) {\n\t\t\treturn \"\", acme.ErrNotFound\n\t\t}\n\t\treturn \"\", errors.Wrapf(err, \"error loading key-account index for key %s\", kid)\n\t}\n\treturn string(id), nil\n}\n\n// getDBAccount retrieves and unmarshals dbAccount.\nfunc (db *DB) getDBAccount(_ context.Context, id string) (*dbAccount, error) {\n\tdata, err := db.db.Get(accountTable, []byte(id))\n\tif err != nil {\n\t\tif nosqlDB.IsErrNotFound(err) {\n\t\t\treturn nil, acme.ErrNotFound\n\t\t}\n\t\treturn nil, errors.Wrapf(err, \"error loading account %s\", id)\n\t}\n\n\tdbacc := new(dbAccount)\n\tif err = json.Unmarshal(data, dbacc); err != nil {\n\t\treturn nil, errors.Wrapf(err, \"error unmarshaling account %s into dbAccount\", id)\n\t}\n\treturn dbacc, nil\n}\n\n// GetAccount retrieves an ACME account by ID.\nfunc (db *DB) GetAccount(ctx context.Context, id string) (*acme.Account, error) {\n\tdbacc, err := db.getDBAccount(ctx, id)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &acme.Account{\n\t\tStatus:          dbacc.Status,\n\t\tContact:         dbacc.Contact,\n\t\tKey:             dbacc.Key,\n\t\tID:              dbacc.ID,\n\t\tLocationPrefix:  dbacc.LocationPrefix,\n\t\tProvisionerID:   dbacc.ProvisionerID,\n\t\tProvisionerName: dbacc.ProvisionerName,\n\t}, nil\n}\n\n// GetAccountByKeyID retrieves an ACME account by KeyID (thumbprint of the Account Key -- JWK).\nfunc (db *DB) GetAccountByKeyID(ctx context.Context, kid string) (*acme.Account, error) {\n\tid, err := db.getAccountIDByKeyID(ctx, kid)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn db.GetAccount(ctx, id)\n}\n\n// CreateAccount imlements the AcmeDB.CreateAccount interface.\nfunc (db *DB) CreateAccount(ctx context.Context, acc *acme.Account) error {\n\tvar err error\n\tacc.ID, err = randID()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tdba := &dbAccount{\n\t\tID:              acc.ID,\n\t\tKey:             acc.Key,\n\t\tContact:         acc.Contact,\n\t\tStatus:          acc.Status,\n\t\tCreatedAt:       clock.Now(),\n\t\tLocationPrefix:  acc.LocationPrefix,\n\t\tProvisionerID:   acc.ProvisionerID,\n\t\tProvisionerName: acc.ProvisionerName,\n\t}\n\n\tkid, err := acme.KeyToID(dba.Key)\n\tif err != nil {\n\t\treturn err\n\t}\n\tkidB := []byte(kid)\n\n\t// Set the jwkID -> acme account ID index\n\t_, swapped, err := db.db.CmpAndSwap(accountByKeyIDTable, kidB, nil, []byte(acc.ID))\n\tswitch {\n\tcase err != nil:\n\t\treturn errors.Wrap(err, \"error storing keyID to accountID index\")\n\tcase !swapped:\n\t\treturn errors.Errorf(\"key-id to account-id index already exists\")\n\tdefault:\n\t\tif err = db.save(ctx, acc.ID, dba, nil, \"account\", accountTable); err != nil {\n\t\t\tdb.db.Del(accountByKeyIDTable, kidB)\n\t\t\treturn err\n\t\t}\n\t\treturn nil\n\t}\n}\n\n// UpdateAccount imlements the AcmeDB.UpdateAccount interface.\nfunc (db *DB) UpdateAccount(ctx context.Context, acc *acme.Account) error {\n\told, err := db.getDBAccount(ctx, acc.ID)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tnu := old.clone()\n\tnu.Contact = acc.Contact\n\tnu.Status = acc.Status\n\n\t// If the status has changed to 'deactivated', then set deactivatedAt timestamp.\n\tif acc.Status == acme.StatusDeactivated && old.Status != acme.StatusDeactivated {\n\t\tnu.DeactivatedAt = clock.Now()\n\t}\n\n\treturn db.save(ctx, old.ID, nu, old, \"account\", accountTable)\n}\n"
  },
  {
    "path": "acme/db/nosql/account_test.go",
    "content": "package nosql\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n\t\"github.com/smallstep/assert\"\n\t\"github.com/smallstep/certificates/acme\"\n\t\"github.com/smallstep/certificates/db\"\n\t\"github.com/smallstep/nosql\"\n\tnosqldb \"github.com/smallstep/nosql/database\"\n\t\"go.step.sm/crypto/jose\"\n)\n\nfunc TestDB_getDBAccount(t *testing.T) {\n\taccID := \"accID\"\n\ttype test struct {\n\t\tdb      nosql.DB\n\t\terr     error\n\t\tacmeErr *acme.Error\n\t\tdbacc   *dbAccount\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/not-found\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, accountTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), accID)\n\n\t\t\t\t\t\treturn nil, nosqldb.ErrNotFound\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: acme.ErrNotFound,\n\t\t\t}\n\t\t},\n\t\t\"fail/db.Get-error\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, accountTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), accID)\n\n\t\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"error loading account accID: force\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/unmarshal-error\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, accountTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), accID)\n\n\t\t\t\t\t\treturn []byte(\"foo\"), nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"error unmarshaling account accID into dbAccount\"),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tnow := clock.Now()\n\t\t\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\tassert.FatalError(t, err)\n\t\t\tdbacc := &dbAccount{\n\t\t\t\tID:              accID,\n\t\t\t\tStatus:          acme.StatusDeactivated,\n\t\t\t\tCreatedAt:       now,\n\t\t\t\tDeactivatedAt:   now,\n\t\t\t\tContact:         []string{\"foo\", \"bar\"},\n\t\t\t\tKey:             jwk,\n\t\t\t\tProvisionerID:   \"73d2c0f1-9753-448b-9b48-bf00fe434681\",\n\t\t\t\tProvisionerName: \"acme\",\n\t\t\t}\n\t\t\tb, err := json.Marshal(dbacc)\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, accountTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), accID)\n\n\t\t\t\t\t\treturn b, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tdbacc: dbacc,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\ttc := run(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\td := DB{db: tc.db}\n\t\t\tif dbacc, err := d.getDBAccount(context.Background(), accID); err != nil {\n\t\t\t\tvar acmeErr *acme.Error\n\t\t\t\tif errors.As(err, &acmeErr) {\n\t\t\t\t\tif assert.NotNil(t, tc.acmeErr) {\n\t\t\t\t\t\tassert.Equals(t, acmeErr.Type, tc.acmeErr.Type)\n\t\t\t\t\t\tassert.Equals(t, acmeErr.Detail, tc.acmeErr.Detail)\n\t\t\t\t\t\tassert.Equals(t, acmeErr.Status, tc.acmeErr.Status)\n\t\t\t\t\t\tassert.Equals(t, acmeErr.Err.Error(), tc.acmeErr.Err.Error())\n\t\t\t\t\t\tassert.Equals(t, acmeErr.Detail, tc.acmeErr.Detail)\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.err.Error())\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else if assert.Nil(t, tc.err) {\n\t\t\t\tassert.Equals(t, dbacc.ID, tc.dbacc.ID)\n\t\t\t\tassert.Equals(t, dbacc.Status, tc.dbacc.Status)\n\t\t\t\tassert.Equals(t, dbacc.CreatedAt, tc.dbacc.CreatedAt)\n\t\t\t\tassert.Equals(t, dbacc.DeactivatedAt, tc.dbacc.DeactivatedAt)\n\t\t\t\tassert.Equals(t, dbacc.Contact, tc.dbacc.Contact)\n\t\t\t\tassert.Equals(t, dbacc.Key.KeyID, tc.dbacc.Key.KeyID)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDB_getAccountIDByKeyID(t *testing.T) {\n\taccID := \"accID\"\n\tkid := \"kid\"\n\ttype test struct {\n\t\tdb      nosql.DB\n\t\terr     error\n\t\tacmeErr *acme.Error\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/not-found\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, accountByKeyIDTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), kid)\n\n\t\t\t\t\t\treturn nil, nosqldb.ErrNotFound\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: acme.ErrNotFound,\n\t\t\t}\n\t\t},\n\t\t\"fail/db.Get-error\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, accountByKeyIDTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), kid)\n\n\t\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"error loading key-account index for key kid: force\"),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, accountByKeyIDTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), kid)\n\n\t\t\t\t\t\treturn []byte(accID), nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\ttc := run(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\td := DB{db: tc.db}\n\t\t\tif retAccID, err := d.getAccountIDByKeyID(context.Background(), kid); err != nil {\n\t\t\t\tvar acmeErr *acme.Error\n\t\t\t\tif errors.As(err, &acmeErr) {\n\t\t\t\t\tif assert.NotNil(t, tc.acmeErr) {\n\t\t\t\t\t\tassert.Equals(t, acmeErr.Type, tc.acmeErr.Type)\n\t\t\t\t\t\tassert.Equals(t, acmeErr.Detail, tc.acmeErr.Detail)\n\t\t\t\t\t\tassert.Equals(t, acmeErr.Status, tc.acmeErr.Status)\n\t\t\t\t\t\tassert.Equals(t, acmeErr.Err.Error(), tc.acmeErr.Err.Error())\n\t\t\t\t\t\tassert.Equals(t, acmeErr.Detail, tc.acmeErr.Detail)\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.err.Error())\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else if assert.Nil(t, tc.err) {\n\t\t\t\tassert.Equals(t, retAccID, accID)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDB_GetAccount(t *testing.T) {\n\taccID := \"accID\"\n\tlocationPrefix := \"https://test.ca.smallstep.com/acme/foo/account/\"\n\tprovisionerName := \"foo\"\n\ttype test struct {\n\t\tdb      nosql.DB\n\t\terr     error\n\t\tacmeErr *acme.Error\n\t\tdbacc   *dbAccount\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/db.Get-error\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, accountTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), accID)\n\n\t\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"error loading account accID: force\"),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tnow := clock.Now()\n\t\t\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\tassert.FatalError(t, err)\n\t\t\tdbacc := &dbAccount{\n\t\t\t\tID:              accID,\n\t\t\t\tStatus:          acme.StatusDeactivated,\n\t\t\t\tCreatedAt:       now,\n\t\t\t\tDeactivatedAt:   now,\n\t\t\t\tContact:         []string{\"foo\", \"bar\"},\n\t\t\t\tKey:             jwk,\n\t\t\t\tLocationPrefix:  locationPrefix,\n\t\t\t\tProvisionerName: provisionerName,\n\t\t\t}\n\t\t\tb, err := json.Marshal(dbacc)\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, accountTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), accID)\n\t\t\t\t\t\treturn b, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tdbacc: dbacc,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\ttc := run(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\td := DB{db: tc.db}\n\t\t\tif acc, err := d.GetAccount(context.Background(), accID); err != nil {\n\t\t\t\tvar acmeErr *acme.Error\n\t\t\t\tif errors.As(err, &acmeErr) {\n\t\t\t\t\tif assert.NotNil(t, tc.acmeErr) {\n\t\t\t\t\t\tassert.Equals(t, acmeErr.Type, tc.acmeErr.Type)\n\t\t\t\t\t\tassert.Equals(t, acmeErr.Detail, tc.acmeErr.Detail)\n\t\t\t\t\t\tassert.Equals(t, acmeErr.Status, tc.acmeErr.Status)\n\t\t\t\t\t\tassert.Equals(t, acmeErr.Err.Error(), tc.acmeErr.Err.Error())\n\t\t\t\t\t\tassert.Equals(t, acmeErr.Detail, tc.acmeErr.Detail)\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.err.Error())\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else if assert.Nil(t, tc.err) {\n\t\t\t\tassert.Equals(t, acc.ID, tc.dbacc.ID)\n\t\t\t\tassert.Equals(t, acc.Status, tc.dbacc.Status)\n\t\t\t\tassert.Equals(t, acc.Contact, tc.dbacc.Contact)\n\t\t\t\tassert.Equals(t, acc.LocationPrefix, tc.dbacc.LocationPrefix)\n\t\t\t\tassert.Equals(t, acc.ProvisionerName, tc.dbacc.ProvisionerName)\n\t\t\t\tassert.Equals(t, acc.Key.KeyID, tc.dbacc.Key.KeyID)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDB_GetAccountByKeyID(t *testing.T) {\n\taccID := \"accID\"\n\tkid := \"kid\"\n\ttype test struct {\n\t\tdb      nosql.DB\n\t\terr     error\n\t\tacmeErr *acme.Error\n\t\tdbacc   *dbAccount\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/db.getAccountIDByKeyID-error\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, string(bucket), string(accountByKeyIDTable))\n\t\t\t\t\t\tassert.Equals(t, string(key), kid)\n\n\t\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"error loading key-account index for key kid: force\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/db.GetAccount-error\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tswitch string(bucket) {\n\t\t\t\t\t\tcase string(accountByKeyIDTable):\n\t\t\t\t\t\t\tassert.Equals(t, string(key), kid)\n\t\t\t\t\t\t\treturn []byte(accID), nil\n\t\t\t\t\t\tcase string(accountTable):\n\t\t\t\t\t\t\tassert.Equals(t, string(key), accID)\n\t\t\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\tassert.FatalError(t, errors.Errorf(\"unexpected bucket %s\", string(bucket)))\n\t\t\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"error loading account accID: force\"),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tnow := clock.Now()\n\t\t\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\tassert.FatalError(t, err)\n\t\t\tdbacc := &dbAccount{\n\t\t\t\tID:            accID,\n\t\t\t\tStatus:        acme.StatusDeactivated,\n\t\t\t\tCreatedAt:     now,\n\t\t\t\tDeactivatedAt: now,\n\t\t\t\tContact:       []string{\"foo\", \"bar\"},\n\t\t\t\tKey:           jwk,\n\t\t\t}\n\t\t\tb, err := json.Marshal(dbacc)\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tswitch string(bucket) {\n\t\t\t\t\t\tcase string(accountByKeyIDTable):\n\t\t\t\t\t\t\tassert.Equals(t, string(key), kid)\n\t\t\t\t\t\t\treturn []byte(accID), nil\n\t\t\t\t\t\tcase string(accountTable):\n\t\t\t\t\t\t\tassert.Equals(t, string(key), accID)\n\t\t\t\t\t\t\treturn b, nil\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\tassert.FatalError(t, errors.Errorf(\"unexpected bucket %s\", string(bucket)))\n\t\t\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tdbacc: dbacc,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\ttc := run(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\td := DB{db: tc.db}\n\t\t\tif acc, err := d.GetAccountByKeyID(context.Background(), kid); err != nil {\n\t\t\t\tvar acmeErr *acme.Error\n\t\t\t\tif errors.As(err, &acmeErr) {\n\t\t\t\t\tif assert.NotNil(t, tc.acmeErr) {\n\t\t\t\t\t\tassert.Equals(t, acmeErr.Type, tc.acmeErr.Type)\n\t\t\t\t\t\tassert.Equals(t, acmeErr.Detail, tc.acmeErr.Detail)\n\t\t\t\t\t\tassert.Equals(t, acmeErr.Status, tc.acmeErr.Status)\n\t\t\t\t\t\tassert.Equals(t, acmeErr.Err.Error(), tc.acmeErr.Err.Error())\n\t\t\t\t\t\tassert.Equals(t, acmeErr.Detail, tc.acmeErr.Detail)\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.err.Error())\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else if assert.Nil(t, tc.err) {\n\t\t\t\tassert.Equals(t, acc.ID, tc.dbacc.ID)\n\t\t\t\tassert.Equals(t, acc.Status, tc.dbacc.Status)\n\t\t\t\tassert.Equals(t, acc.Contact, tc.dbacc.Contact)\n\t\t\t\tassert.Equals(t, acc.Key.KeyID, tc.dbacc.Key.KeyID)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDB_CreateAccount(t *testing.T) {\n\tlocationPrefix := \"https://test.ca.smallstep.com/acme/foo/account/\"\n\ttype test struct {\n\t\tdb  nosql.DB\n\t\tacc *acme.Account\n\t\terr error\n\t\t_id *string\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/keyID-cmpAndSwap-error\": func(t *testing.T) test {\n\t\t\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\tassert.FatalError(t, err)\n\t\t\tacc := &acme.Account{\n\t\t\t\tStatus:         acme.StatusValid,\n\t\t\t\tContact:        []string{\"foo\", \"bar\"},\n\t\t\t\tKey:            jwk,\n\t\t\t\tLocationPrefix: locationPrefix,\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, accountByKeyIDTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), jwk.KeyID)\n\t\t\t\t\t\tassert.Equals(t, old, nil)\n\n\t\t\t\t\t\tassert.Equals(t, nu, []byte(acc.ID))\n\t\t\t\t\t\treturn nil, false, errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tacc: acc,\n\t\t\t\terr: errors.New(\"error storing keyID to accountID index: force\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/keyID-cmpAndSwap-false\": func(t *testing.T) test {\n\t\t\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\tassert.FatalError(t, err)\n\t\t\tacc := &acme.Account{\n\t\t\t\tStatus:         acme.StatusValid,\n\t\t\t\tContact:        []string{\"foo\", \"bar\"},\n\t\t\t\tKey:            jwk,\n\t\t\t\tLocationPrefix: locationPrefix,\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, accountByKeyIDTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), jwk.KeyID)\n\t\t\t\t\t\tassert.Equals(t, old, nil)\n\n\t\t\t\t\t\tassert.Equals(t, nu, []byte(acc.ID))\n\t\t\t\t\t\treturn nil, false, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tacc: acc,\n\t\t\t\terr: errors.New(\"key-id to account-id index already exists\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/account-save-error\": func(t *testing.T) test {\n\t\t\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\tassert.FatalError(t, err)\n\t\t\tacc := &acme.Account{\n\t\t\t\tStatus:         acme.StatusValid,\n\t\t\t\tContact:        []string{\"foo\", \"bar\"},\n\t\t\t\tKey:            jwk,\n\t\t\t\tLocationPrefix: locationPrefix,\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {\n\t\t\t\t\t\tswitch string(bucket) {\n\t\t\t\t\t\tcase string(accountByKeyIDTable):\n\t\t\t\t\t\t\tassert.Equals(t, string(key), jwk.KeyID)\n\t\t\t\t\t\t\tassert.Equals(t, old, nil)\n\t\t\t\t\t\t\treturn nu, true, nil\n\t\t\t\t\t\tcase string(accountTable):\n\t\t\t\t\t\t\tassert.Equals(t, string(key), acc.ID)\n\t\t\t\t\t\t\tassert.Equals(t, old, nil)\n\n\t\t\t\t\t\t\tdbacc := new(dbAccount)\n\t\t\t\t\t\t\tassert.FatalError(t, json.Unmarshal(nu, dbacc))\n\t\t\t\t\t\t\tassert.Equals(t, dbacc.ID, string(key))\n\t\t\t\t\t\t\tassert.Equals(t, dbacc.Contact, acc.Contact)\n\t\t\t\t\t\t\tassert.Equals(t, dbacc.LocationPrefix, acc.LocationPrefix)\n\t\t\t\t\t\t\tassert.Equals(t, dbacc.ProvisionerName, acc.ProvisionerName)\n\t\t\t\t\t\t\tassert.Equals(t, dbacc.Key.KeyID, acc.Key.KeyID)\n\t\t\t\t\t\t\tassert.True(t, clock.Now().Add(-time.Minute).Before(dbacc.CreatedAt))\n\t\t\t\t\t\t\tassert.True(t, clock.Now().Add(time.Minute).After(dbacc.CreatedAt))\n\t\t\t\t\t\t\tassert.True(t, dbacc.DeactivatedAt.IsZero())\n\t\t\t\t\t\t\treturn nil, false, errors.New(\"force\")\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\tassert.FatalError(t, errors.Errorf(\"unexpected bucket %s\", string(bucket)))\n\t\t\t\t\t\t\treturn nil, false, errors.New(\"force\")\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tacc: acc,\n\t\t\t\terr: errors.New(\"error saving acme account: force\"),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tvar (\n\t\t\t\tid    string\n\t\t\t\tidPtr = &id\n\t\t\t)\n\t\t\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\tassert.FatalError(t, err)\n\t\t\tacc := &acme.Account{\n\t\t\t\tStatus:         acme.StatusValid,\n\t\t\t\tContact:        []string{\"foo\", \"bar\"},\n\t\t\t\tKey:            jwk,\n\t\t\t\tLocationPrefix: locationPrefix,\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {\n\t\t\t\t\t\tid = string(key)\n\t\t\t\t\t\tswitch string(bucket) {\n\t\t\t\t\t\tcase string(accountByKeyIDTable):\n\t\t\t\t\t\t\tassert.Equals(t, string(key), jwk.KeyID)\n\t\t\t\t\t\t\tassert.Equals(t, old, nil)\n\t\t\t\t\t\t\treturn nu, true, nil\n\t\t\t\t\t\tcase string(accountTable):\n\t\t\t\t\t\t\tassert.Equals(t, string(key), acc.ID)\n\t\t\t\t\t\t\tassert.Equals(t, old, nil)\n\n\t\t\t\t\t\t\tdbacc := new(dbAccount)\n\t\t\t\t\t\t\tassert.FatalError(t, json.Unmarshal(nu, dbacc))\n\t\t\t\t\t\t\tassert.Equals(t, dbacc.ID, string(key))\n\t\t\t\t\t\t\tassert.Equals(t, dbacc.Contact, acc.Contact)\n\t\t\t\t\t\t\tassert.Equals(t, dbacc.LocationPrefix, acc.LocationPrefix)\n\t\t\t\t\t\t\tassert.Equals(t, dbacc.ProvisionerName, acc.ProvisionerName)\n\t\t\t\t\t\t\tassert.Equals(t, dbacc.Key.KeyID, acc.Key.KeyID)\n\t\t\t\t\t\t\tassert.True(t, clock.Now().Add(-time.Minute).Before(dbacc.CreatedAt))\n\t\t\t\t\t\t\tassert.True(t, clock.Now().Add(time.Minute).After(dbacc.CreatedAt))\n\t\t\t\t\t\t\tassert.True(t, dbacc.DeactivatedAt.IsZero())\n\t\t\t\t\t\t\treturn nu, true, nil\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\tassert.FatalError(t, errors.Errorf(\"unexpected bucket %s\", string(bucket)))\n\t\t\t\t\t\t\treturn nil, false, errors.New(\"force\")\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tacc: acc,\n\t\t\t\t_id: idPtr,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\ttc := run(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\td := DB{db: tc.db}\n\t\t\tif err := d.CreateAccount(context.Background(), tc.acc); err != nil {\n\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.err.Error())\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif assert.Nil(t, tc.err) {\n\t\t\t\t\tassert.Equals(t, tc.acc.ID, *tc._id)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDB_UpdateAccount(t *testing.T) {\n\taccID := \"accID\"\n\tnow := clock.Now()\n\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\tassert.FatalError(t, err)\n\tdbacc := &dbAccount{\n\t\tID:              accID,\n\t\tStatus:          acme.StatusDeactivated,\n\t\tCreatedAt:       now,\n\t\tDeactivatedAt:   now,\n\t\tContact:         []string{\"foo\", \"bar\"},\n\t\tLocationPrefix:  \"foo\",\n\t\tProvisionerName: \"alpha\",\n\t\tKey:             jwk,\n\t}\n\tb, err := json.Marshal(dbacc)\n\tassert.FatalError(t, err)\n\ttype test struct {\n\t\tdb  nosql.DB\n\t\tacc *acme.Account\n\t\terr error\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/db.Get-error\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tacc: &acme.Account{\n\t\t\t\t\tID: accID,\n\t\t\t\t},\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, accountTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), accID)\n\n\t\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"error loading account accID: force\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/already-deactivated\": func(t *testing.T) test {\n\t\t\tclone := dbacc.clone()\n\t\t\tclone.Status = acme.StatusDeactivated\n\t\t\tclone.DeactivatedAt = now\n\t\t\tdbaccb, err := json.Marshal(clone)\n\t\t\tassert.FatalError(t, err)\n\t\t\tacc := &acme.Account{\n\t\t\t\tID:      accID,\n\t\t\t\tStatus:  acme.StatusDeactivated,\n\t\t\t\tContact: []string{\"foo\", \"bar\"},\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tacc: acc,\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, accountTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), accID)\n\n\t\t\t\t\t\treturn dbaccb, nil\n\t\t\t\t\t},\n\t\t\t\t\tMCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, accountTable)\n\t\t\t\t\t\tassert.Equals(t, old, b)\n\n\t\t\t\t\t\tdbNew := new(dbAccount)\n\t\t\t\t\t\tassert.FatalError(t, json.Unmarshal(nu, dbNew))\n\t\t\t\t\t\tassert.Equals(t, dbNew.ID, clone.ID)\n\t\t\t\t\t\tassert.Equals(t, dbNew.Status, clone.Status)\n\t\t\t\t\t\tassert.Equals(t, dbNew.Contact, clone.Contact)\n\t\t\t\t\t\tassert.Equals(t, dbNew.Key.KeyID, clone.Key.KeyID)\n\t\t\t\t\t\tassert.Equals(t, dbNew.CreatedAt, clone.CreatedAt)\n\t\t\t\t\t\tassert.Equals(t, dbNew.DeactivatedAt, clone.DeactivatedAt)\n\t\t\t\t\t\treturn nil, false, errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"error saving acme account: force\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/db.CmpAndSwap-error\": func(t *testing.T) test {\n\t\t\tacc := &acme.Account{\n\t\t\t\tID:      accID,\n\t\t\t\tStatus:  acme.StatusDeactivated,\n\t\t\t\tContact: []string{\"foo\", \"bar\"},\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tacc: acc,\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, accountTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), accID)\n\n\t\t\t\t\t\treturn b, nil\n\t\t\t\t\t},\n\t\t\t\t\tMCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, accountTable)\n\t\t\t\t\t\tassert.Equals(t, old, b)\n\n\t\t\t\t\t\tdbNew := new(dbAccount)\n\t\t\t\t\t\tassert.FatalError(t, json.Unmarshal(nu, dbNew))\n\t\t\t\t\t\tassert.Equals(t, dbNew.ID, dbacc.ID)\n\t\t\t\t\t\tassert.Equals(t, dbNew.Status, acc.Status)\n\t\t\t\t\t\tassert.Equals(t, dbNew.Contact, dbacc.Contact)\n\t\t\t\t\t\tassert.Equals(t, dbNew.Key.KeyID, dbacc.Key.KeyID)\n\t\t\t\t\t\tassert.Equals(t, dbNew.CreatedAt, dbacc.CreatedAt)\n\t\t\t\t\t\tassert.True(t, dbNew.DeactivatedAt.Add(-time.Minute).Before(now))\n\t\t\t\t\t\tassert.True(t, dbNew.DeactivatedAt.Add(time.Minute).After(now))\n\t\t\t\t\t\treturn nil, false, errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"error saving acme account: force\"),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tacc := &acme.Account{\n\t\t\t\tID:              accID,\n\t\t\t\tStatus:          acme.StatusDeactivated,\n\t\t\t\tContact:         []string{\"baz\", \"zap\"},\n\t\t\t\tLocationPrefix:  \"bar\",\n\t\t\t\tProvisionerName: \"beta\",\n\t\t\t\tKey:             jwk,\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tacc: acc,\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, accountTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), accID)\n\n\t\t\t\t\t\treturn b, nil\n\t\t\t\t\t},\n\t\t\t\t\tMCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, accountTable)\n\t\t\t\t\t\tassert.Equals(t, old, b)\n\n\t\t\t\t\t\tdbNew := new(dbAccount)\n\t\t\t\t\t\tassert.FatalError(t, json.Unmarshal(nu, dbNew))\n\t\t\t\t\t\tassert.Equals(t, dbNew.ID, dbacc.ID)\n\t\t\t\t\t\tassert.Equals(t, dbNew.Status, acc.Status)\n\t\t\t\t\t\tassert.Equals(t, dbNew.Contact, acc.Contact)\n\t\t\t\t\t\t// LocationPrefix should not change.\n\t\t\t\t\t\tassert.Equals(t, dbNew.LocationPrefix, dbacc.LocationPrefix)\n\t\t\t\t\t\tassert.Equals(t, dbNew.ProvisionerName, dbacc.ProvisionerName)\n\t\t\t\t\t\tassert.Equals(t, dbNew.Key.KeyID, dbacc.Key.KeyID)\n\t\t\t\t\t\tassert.Equals(t, dbNew.CreatedAt, dbacc.CreatedAt)\n\t\t\t\t\t\tassert.True(t, dbNew.DeactivatedAt.Add(-time.Minute).Before(now))\n\t\t\t\t\t\tassert.True(t, dbNew.DeactivatedAt.Add(time.Minute).After(now))\n\t\t\t\t\t\treturn nu, true, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\ttc := run(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\td := DB{db: tc.db}\n\t\t\tif err := d.UpdateAccount(context.Background(), tc.acc); err != nil {\n\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.err.Error())\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tassert.Nil(t, tc.err)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "acme/db/nosql/authz.go",
    "content": "package nosql\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n\t\"github.com/smallstep/certificates/acme\"\n\t\"github.com/smallstep/nosql\"\n)\n\n// dbAuthz is the base authz type that others build from.\ntype dbAuthz struct {\n\tID           string          `json:\"id\"`\n\tAccountID    string          `json:\"accountID\"`\n\tIdentifier   acme.Identifier `json:\"identifier\"`\n\tStatus       acme.Status     `json:\"status\"`\n\tToken        string          `json:\"token\"`\n\tFingerprint  string          `json:\"fingerprint,omitempty\"`\n\tChallengeIDs []string        `json:\"challengeIDs\"`\n\tWildcard     bool            `json:\"wildcard\"`\n\tCreatedAt    time.Time       `json:\"createdAt\"`\n\tExpiresAt    time.Time       `json:\"expiresAt\"`\n\tError        *acme.Error     `json:\"error\"`\n}\n\nfunc (ba *dbAuthz) clone() *dbAuthz {\n\tu := *ba\n\treturn &u\n}\n\n// getDBAuthz retrieves and unmarshals a database representation of the\n// ACME Authorization type.\nfunc (db *DB) getDBAuthz(_ context.Context, id string) (*dbAuthz, error) {\n\tdata, err := db.db.Get(authzTable, []byte(id))\n\tif nosql.IsErrNotFound(err) {\n\t\treturn nil, acme.NewError(acme.ErrorMalformedType, \"authz %s not found\", id)\n\t} else if err != nil {\n\t\treturn nil, errors.Wrapf(err, \"error loading authz %s\", id)\n\t}\n\n\tvar dbaz dbAuthz\n\tif err = json.Unmarshal(data, &dbaz); err != nil {\n\t\treturn nil, errors.Wrapf(err, \"error unmarshaling authz %s into dbAuthz\", id)\n\t}\n\treturn &dbaz, nil\n}\n\n// GetAuthorization retrieves and unmarshals an ACME authz type from the database.\n// Implements acme.DB GetAuthorization interface.\nfunc (db *DB) GetAuthorization(ctx context.Context, id string) (*acme.Authorization, error) {\n\tdbaz, err := db.getDBAuthz(ctx, id)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvar chs = make([]*acme.Challenge, len(dbaz.ChallengeIDs))\n\tfor i, chID := range dbaz.ChallengeIDs {\n\t\tchs[i], err = db.GetChallenge(ctx, chID, id)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\treturn &acme.Authorization{\n\t\tID:          dbaz.ID,\n\t\tAccountID:   dbaz.AccountID,\n\t\tIdentifier:  dbaz.Identifier,\n\t\tStatus:      dbaz.Status,\n\t\tChallenges:  chs,\n\t\tWildcard:    dbaz.Wildcard,\n\t\tExpiresAt:   dbaz.ExpiresAt,\n\t\tToken:       dbaz.Token,\n\t\tFingerprint: dbaz.Fingerprint,\n\t\tError:       dbaz.Error,\n\t}, nil\n}\n\n// CreateAuthorization creates an entry in the database for the Authorization.\n// Implements the acme.DB.CreateAuthorization interface.\nfunc (db *DB) CreateAuthorization(ctx context.Context, az *acme.Authorization) error {\n\tvar err error\n\taz.ID, err = randID()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tchIDs := make([]string, len(az.Challenges))\n\tfor i, ch := range az.Challenges {\n\t\tchIDs[i] = ch.ID\n\t}\n\n\tnow := clock.Now()\n\tdbaz := &dbAuthz{\n\t\tID:           az.ID,\n\t\tAccountID:    az.AccountID,\n\t\tStatus:       az.Status,\n\t\tCreatedAt:    now,\n\t\tExpiresAt:    az.ExpiresAt,\n\t\tIdentifier:   az.Identifier,\n\t\tChallengeIDs: chIDs,\n\t\tToken:        az.Token,\n\t\tFingerprint:  az.Fingerprint,\n\t\tWildcard:     az.Wildcard,\n\t}\n\n\treturn db.save(ctx, az.ID, dbaz, nil, \"authz\", authzTable)\n}\n\n// UpdateAuthorization saves an updated ACME Authorization to the database.\nfunc (db *DB) UpdateAuthorization(ctx context.Context, az *acme.Authorization) error {\n\told, err := db.getDBAuthz(ctx, az.ID)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tnu := old.clone()\n\tnu.Status = az.Status\n\tnu.Fingerprint = az.Fingerprint\n\tnu.Error = az.Error\n\treturn db.save(ctx, old.ID, nu, old, \"authz\", authzTable)\n}\n\n// GetAuthorizationsByAccountID retrieves and unmarshals ACME authz types from the database.\nfunc (db *DB) GetAuthorizationsByAccountID(_ context.Context, accountID string) ([]*acme.Authorization, error) {\n\tentries, err := db.db.List(authzTable)\n\tif err != nil {\n\t\treturn nil, errors.Wrapf(err, \"error listing authz\")\n\t}\n\tauthzs := []*acme.Authorization{}\n\tfor _, entry := range entries {\n\t\tdbaz := new(dbAuthz)\n\t\tif err = json.Unmarshal(entry.Value, dbaz); err != nil {\n\t\t\treturn nil, errors.Wrapf(err, \"error unmarshaling dbAuthz key '%s' into dbAuthz struct\", string(entry.Key))\n\t\t}\n\t\t// Filter out all dbAuthzs that don't belong to the accountID. This\n\t\t// could be made more efficient with additional data structures mapping the\n\t\t// Account ID to authorizations. Not trivial to do, though.\n\t\tif dbaz.AccountID != accountID {\n\t\t\tcontinue\n\t\t}\n\t\tauthzs = append(authzs, &acme.Authorization{\n\t\t\tID:          dbaz.ID,\n\t\t\tAccountID:   dbaz.AccountID,\n\t\t\tIdentifier:  dbaz.Identifier,\n\t\t\tStatus:      dbaz.Status,\n\t\t\tChallenges:  nil, // challenges not required for current use case\n\t\t\tWildcard:    dbaz.Wildcard,\n\t\t\tExpiresAt:   dbaz.ExpiresAt,\n\t\t\tToken:       dbaz.Token,\n\t\t\tFingerprint: dbaz.Fingerprint,\n\t\t\tError:       dbaz.Error,\n\t\t})\n\t}\n\n\treturn authzs, nil\n}\n"
  },
  {
    "path": "acme/db/nosql/authz_test.go",
    "content": "package nosql\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/pkg/errors\"\n\t\"github.com/smallstep/assert\"\n\t\"github.com/smallstep/certificates/acme\"\n\t\"github.com/smallstep/certificates/db\"\n\t\"github.com/smallstep/nosql\"\n\tnosqldb \"github.com/smallstep/nosql/database\"\n)\n\nfunc TestDB_getDBAuthz(t *testing.T) {\n\tazID := \"azID\"\n\ttype test struct {\n\t\tdb      nosql.DB\n\t\terr     error\n\t\tacmeErr *acme.Error\n\t\tdbaz    *dbAuthz\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/not-found\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, authzTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), azID)\n\n\t\t\t\t\t\treturn nil, nosqldb.ErrNotFound\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tacmeErr: acme.NewError(acme.ErrorMalformedType, \"authz azID not found\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/db.Get-error\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, authzTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), azID)\n\n\t\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"error loading authz azID: force\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/unmarshal-error\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, authzTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), azID)\n\n\t\t\t\t\t\treturn []byte(\"foo\"), nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"error unmarshaling authz azID into dbAuthz\"),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tnow := clock.Now()\n\t\t\tdbaz := &dbAuthz{\n\t\t\t\tID:        azID,\n\t\t\t\tAccountID: \"accountID\",\n\t\t\t\tIdentifier: acme.Identifier{\n\t\t\t\t\tType:  \"dns\",\n\t\t\t\t\tValue: \"test.ca.smallstep.com\",\n\t\t\t\t},\n\t\t\t\tStatus:       acme.StatusPending,\n\t\t\t\tToken:        \"token\",\n\t\t\t\tCreatedAt:    now,\n\t\t\t\tExpiresAt:    now.Add(5 * time.Minute),\n\t\t\t\tError:        acme.NewErrorISE(\"The server experienced an internal error\"),\n\t\t\t\tChallengeIDs: []string{\"foo\", \"bar\"},\n\t\t\t\tWildcard:     true,\n\t\t\t}\n\t\t\tb, err := json.Marshal(dbaz)\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, authzTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), azID)\n\n\t\t\t\t\t\treturn b, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tdbaz: dbaz,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\ttc := run(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\td := DB{db: tc.db}\n\t\t\tif dbaz, err := d.getDBAuthz(context.Background(), azID); err != nil {\n\t\t\t\tvar acmeErr *acme.Error\n\t\t\t\tif errors.As(err, &acmeErr) {\n\t\t\t\t\tif assert.NotNil(t, tc.acmeErr) {\n\t\t\t\t\t\tassert.Equals(t, acmeErr.Type, tc.acmeErr.Type)\n\t\t\t\t\t\tassert.Equals(t, acmeErr.Detail, tc.acmeErr.Detail)\n\t\t\t\t\t\tassert.Equals(t, acmeErr.Status, tc.acmeErr.Status)\n\t\t\t\t\t\tassert.Equals(t, acmeErr.Err.Error(), tc.acmeErr.Err.Error())\n\t\t\t\t\t\tassert.Equals(t, acmeErr.Detail, tc.acmeErr.Detail)\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.err.Error())\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else if assert.Nil(t, tc.err) {\n\t\t\t\tassert.Equals(t, dbaz.ID, tc.dbaz.ID)\n\t\t\t\tassert.Equals(t, dbaz.AccountID, tc.dbaz.AccountID)\n\t\t\t\tassert.Equals(t, dbaz.Identifier, tc.dbaz.Identifier)\n\t\t\t\tassert.Equals(t, dbaz.Status, tc.dbaz.Status)\n\t\t\t\tassert.Equals(t, dbaz.Token, tc.dbaz.Token)\n\t\t\t\tassert.Equals(t, dbaz.CreatedAt, tc.dbaz.CreatedAt)\n\t\t\t\tassert.Equals(t, dbaz.ExpiresAt, tc.dbaz.ExpiresAt)\n\t\t\t\tassert.Equals(t, dbaz.Error.Error(), tc.dbaz.Error.Error())\n\t\t\t\tassert.Equals(t, dbaz.Wildcard, tc.dbaz.Wildcard)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDB_GetAuthorization(t *testing.T) {\n\tazID := \"azID\"\n\ttype test struct {\n\t\tdb      nosql.DB\n\t\terr     error\n\t\tacmeErr *acme.Error\n\t\tdbaz    *dbAuthz\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/db.Get-error\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, authzTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), azID)\n\n\t\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"error loading authz azID: force\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/forward-acme-error\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, authzTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), azID)\n\n\t\t\t\t\t\treturn nil, nosqldb.ErrNotFound\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tacmeErr: acme.NewError(acme.ErrorMalformedType, \"authz azID not found\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/db.GetChallenge-error\": func(t *testing.T) test {\n\t\t\tnow := clock.Now()\n\t\t\tdbaz := &dbAuthz{\n\t\t\t\tID:        azID,\n\t\t\t\tAccountID: \"accountID\",\n\t\t\t\tIdentifier: acme.Identifier{\n\t\t\t\t\tType:  \"dns\",\n\t\t\t\t\tValue: \"test.ca.smallstep.com\",\n\t\t\t\t},\n\t\t\t\tStatus:       acme.StatusPending,\n\t\t\t\tToken:        \"token\",\n\t\t\t\tCreatedAt:    now,\n\t\t\t\tExpiresAt:    now.Add(5 * time.Minute),\n\t\t\t\tError:        acme.NewErrorISE(\"force\"),\n\t\t\t\tChallengeIDs: []string{\"foo\", \"bar\"},\n\t\t\t\tWildcard:     true,\n\t\t\t}\n\t\t\tb, err := json.Marshal(dbaz)\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tswitch string(bucket) {\n\t\t\t\t\t\tcase string(authzTable):\n\t\t\t\t\t\t\tassert.Equals(t, string(key), azID)\n\t\t\t\t\t\t\treturn b, nil\n\t\t\t\t\t\tcase string(challengeTable):\n\t\t\t\t\t\t\tassert.Equals(t, string(key), \"foo\")\n\t\t\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\tassert.FatalError(t, errors.Errorf(\"unexpected bucket '%s'\", string(bucket)))\n\t\t\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"error loading acme challenge foo: force\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/db.GetChallenge-not-found\": func(t *testing.T) test {\n\t\t\tnow := clock.Now()\n\t\t\tdbaz := &dbAuthz{\n\t\t\t\tID:        azID,\n\t\t\t\tAccountID: \"accountID\",\n\t\t\t\tIdentifier: acme.Identifier{\n\t\t\t\t\tType:  \"dns\",\n\t\t\t\t\tValue: \"test.ca.smallstep.com\",\n\t\t\t\t},\n\t\t\t\tStatus:       acme.StatusPending,\n\t\t\t\tToken:        \"token\",\n\t\t\t\tCreatedAt:    now,\n\t\t\t\tExpiresAt:    now.Add(5 * time.Minute),\n\t\t\t\tError:        acme.NewErrorISE(\"force\"),\n\t\t\t\tChallengeIDs: []string{\"foo\", \"bar\"},\n\t\t\t\tWildcard:     true,\n\t\t\t}\n\t\t\tb, err := json.Marshal(dbaz)\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tswitch string(bucket) {\n\t\t\t\t\t\tcase string(authzTable):\n\t\t\t\t\t\t\tassert.Equals(t, string(key), azID)\n\t\t\t\t\t\t\treturn b, nil\n\t\t\t\t\t\tcase string(challengeTable):\n\t\t\t\t\t\t\tassert.Equals(t, string(key), \"foo\")\n\t\t\t\t\t\t\treturn nil, nosqldb.ErrNotFound\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\tassert.FatalError(t, errors.Errorf(\"unexpected bucket '%s'\", string(bucket)))\n\t\t\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tacmeErr: acme.NewError(acme.ErrorMalformedType, \"challenge foo not found\"),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tnow := clock.Now()\n\t\t\tdbaz := &dbAuthz{\n\t\t\t\tID:        azID,\n\t\t\t\tAccountID: \"accountID\",\n\t\t\t\tIdentifier: acme.Identifier{\n\t\t\t\t\tType:  \"dns\",\n\t\t\t\t\tValue: \"test.ca.smallstep.com\",\n\t\t\t\t},\n\t\t\t\tStatus:       acme.StatusPending,\n\t\t\t\tToken:        \"token\",\n\t\t\t\tCreatedAt:    now,\n\t\t\t\tExpiresAt:    now.Add(5 * time.Minute),\n\t\t\t\tError:        acme.NewErrorISE(\"The server experienced an internal error\"),\n\t\t\t\tChallengeIDs: []string{\"foo\", \"bar\"},\n\t\t\t\tWildcard:     true,\n\t\t\t}\n\t\t\tb, err := json.Marshal(dbaz)\n\t\t\tassert.FatalError(t, err)\n\t\t\tchCount := 0\n\t\t\tfooChb, err := json.Marshal(&dbChallenge{ID: \"foo\"})\n\t\t\tassert.FatalError(t, err)\n\t\t\tbarChb, err := json.Marshal(&dbChallenge{ID: \"bar\"})\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tswitch string(bucket) {\n\t\t\t\t\t\tcase string(authzTable):\n\t\t\t\t\t\t\tassert.Equals(t, string(key), azID)\n\t\t\t\t\t\t\treturn b, nil\n\t\t\t\t\t\tcase string(challengeTable):\n\t\t\t\t\t\t\tif chCount == 0 {\n\t\t\t\t\t\t\t\tchCount++\n\t\t\t\t\t\t\t\tassert.Equals(t, string(key), \"foo\")\n\t\t\t\t\t\t\t\treturn fooChb, nil\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tassert.Equals(t, string(key), \"bar\")\n\t\t\t\t\t\t\treturn barChb, nil\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\tassert.FatalError(t, errors.Errorf(\"unexpected bucket '%s'\", string(bucket)))\n\t\t\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tdbaz: dbaz,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\ttc := run(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\td := DB{db: tc.db}\n\t\t\tif az, err := d.GetAuthorization(context.Background(), azID); err != nil {\n\t\t\t\tvar acmeErr *acme.Error\n\t\t\t\tif errors.As(err, &acmeErr) {\n\t\t\t\t\tif assert.NotNil(t, tc.acmeErr) {\n\t\t\t\t\t\tassert.Equals(t, acmeErr.Type, tc.acmeErr.Type)\n\t\t\t\t\t\tassert.Equals(t, acmeErr.Detail, tc.acmeErr.Detail)\n\t\t\t\t\t\tassert.Equals(t, acmeErr.Status, tc.acmeErr.Status)\n\t\t\t\t\t\tassert.Equals(t, acmeErr.Err.Error(), tc.acmeErr.Err.Error())\n\t\t\t\t\t\tassert.Equals(t, acmeErr.Detail, tc.acmeErr.Detail)\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.err.Error())\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else if assert.Nil(t, tc.err) {\n\t\t\t\tassert.Equals(t, az.ID, tc.dbaz.ID)\n\t\t\t\tassert.Equals(t, az.AccountID, tc.dbaz.AccountID)\n\t\t\t\tassert.Equals(t, az.Identifier, tc.dbaz.Identifier)\n\t\t\t\tassert.Equals(t, az.Status, tc.dbaz.Status)\n\t\t\t\tassert.Equals(t, az.Token, tc.dbaz.Token)\n\t\t\t\tassert.Equals(t, az.Wildcard, tc.dbaz.Wildcard)\n\t\t\t\tassert.Equals(t, az.ExpiresAt, tc.dbaz.ExpiresAt)\n\t\t\t\tassert.Equals(t, az.Challenges, []*acme.Challenge{\n\t\t\t\t\t{ID: \"foo\"},\n\t\t\t\t\t{ID: \"bar\"},\n\t\t\t\t})\n\t\t\t\tassert.Equals(t, az.Error.Error(), tc.dbaz.Error.Error())\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDB_CreateAuthorization(t *testing.T) {\n\tazID := \"azID\"\n\ttype test struct {\n\t\tdb  nosql.DB\n\t\taz  *acme.Authorization\n\t\terr error\n\t\t_id *string\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/cmpAndSwap-error\": func(t *testing.T) test {\n\t\t\tnow := clock.Now()\n\t\t\taz := &acme.Authorization{\n\t\t\t\tID:        azID,\n\t\t\t\tAccountID: \"accountID\",\n\t\t\t\tIdentifier: acme.Identifier{\n\t\t\t\t\tType:  \"dns\",\n\t\t\t\t\tValue: \"test.ca.smallstep.com\",\n\t\t\t\t},\n\t\t\t\tStatus:    acme.StatusPending,\n\t\t\t\tToken:     \"token\",\n\t\t\t\tExpiresAt: now.Add(5 * time.Minute),\n\t\t\t\tChallenges: []*acme.Challenge{\n\t\t\t\t\t{ID: \"foo\"},\n\t\t\t\t\t{ID: \"bar\"},\n\t\t\t\t},\n\t\t\t\tWildcard: true,\n\t\t\t\tError:    acme.NewErrorISE(\"force\"),\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, authzTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), az.ID)\n\t\t\t\t\t\tassert.Equals(t, old, nil)\n\n\t\t\t\t\t\tdbaz := new(dbAuthz)\n\t\t\t\t\t\tassert.FatalError(t, json.Unmarshal(nu, dbaz))\n\t\t\t\t\t\tassert.Equals(t, dbaz.ID, string(key))\n\t\t\t\t\t\tassert.Equals(t, dbaz.AccountID, az.AccountID)\n\t\t\t\t\t\tassert.Equals(t, dbaz.Identifier, acme.Identifier{\n\t\t\t\t\t\t\tType:  \"dns\",\n\t\t\t\t\t\t\tValue: \"test.ca.smallstep.com\",\n\t\t\t\t\t\t})\n\t\t\t\t\t\tassert.Equals(t, dbaz.Status, az.Status)\n\t\t\t\t\t\tassert.Equals(t, dbaz.Token, az.Token)\n\t\t\t\t\t\tassert.Equals(t, dbaz.ChallengeIDs, []string{\"foo\", \"bar\"})\n\t\t\t\t\t\tassert.Equals(t, dbaz.Wildcard, az.Wildcard)\n\t\t\t\t\t\tassert.Equals(t, dbaz.ExpiresAt, az.ExpiresAt)\n\t\t\t\t\t\tassert.Nil(t, dbaz.Error)\n\t\t\t\t\t\tassert.True(t, clock.Now().Add(-time.Minute).Before(dbaz.CreatedAt))\n\t\t\t\t\t\tassert.True(t, clock.Now().Add(time.Minute).After(dbaz.CreatedAt))\n\t\t\t\t\t\treturn nil, false, errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\taz:  az,\n\t\t\t\terr: errors.New(\"error saving acme authz: force\"),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tvar (\n\t\t\t\tid    string\n\t\t\t\tidPtr = &id\n\t\t\t\tnow   = clock.Now()\n\t\t\t\taz    = &acme.Authorization{\n\t\t\t\t\tID:        azID,\n\t\t\t\t\tAccountID: \"accountID\",\n\t\t\t\t\tIdentifier: acme.Identifier{\n\t\t\t\t\t\tType:  \"dns\",\n\t\t\t\t\t\tValue: \"test.ca.smallstep.com\",\n\t\t\t\t\t},\n\t\t\t\t\tStatus:    acme.StatusPending,\n\t\t\t\t\tToken:     \"token\",\n\t\t\t\t\tExpiresAt: now.Add(5 * time.Minute),\n\t\t\t\t\tChallenges: []*acme.Challenge{\n\t\t\t\t\t\t{ID: \"foo\"},\n\t\t\t\t\t\t{ID: \"bar\"},\n\t\t\t\t\t},\n\t\t\t\t\tWildcard: true,\n\t\t\t\t\tError:    acme.NewErrorISE(\"force\"),\n\t\t\t\t}\n\t\t\t)\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {\n\t\t\t\t\t\t*idPtr = string(key)\n\t\t\t\t\t\tassert.Equals(t, bucket, authzTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), az.ID)\n\t\t\t\t\t\tassert.Equals(t, old, nil)\n\n\t\t\t\t\t\tdbaz := new(dbAuthz)\n\t\t\t\t\t\tassert.FatalError(t, json.Unmarshal(nu, dbaz))\n\t\t\t\t\t\tassert.Equals(t, dbaz.ID, string(key))\n\t\t\t\t\t\tassert.Equals(t, dbaz.AccountID, az.AccountID)\n\t\t\t\t\t\tassert.Equals(t, dbaz.Identifier, acme.Identifier{\n\t\t\t\t\t\t\tType:  \"dns\",\n\t\t\t\t\t\t\tValue: \"test.ca.smallstep.com\",\n\t\t\t\t\t\t})\n\t\t\t\t\t\tassert.Equals(t, dbaz.Status, az.Status)\n\t\t\t\t\t\tassert.Equals(t, dbaz.Token, az.Token)\n\t\t\t\t\t\tassert.Equals(t, dbaz.ChallengeIDs, []string{\"foo\", \"bar\"})\n\t\t\t\t\t\tassert.Equals(t, dbaz.Wildcard, az.Wildcard)\n\t\t\t\t\t\tassert.Equals(t, dbaz.ExpiresAt, az.ExpiresAt)\n\t\t\t\t\t\tassert.Nil(t, dbaz.Error)\n\t\t\t\t\t\tassert.True(t, clock.Now().Add(-time.Minute).Before(dbaz.CreatedAt))\n\t\t\t\t\t\tassert.True(t, clock.Now().Add(time.Minute).After(dbaz.CreatedAt))\n\t\t\t\t\t\treturn nu, true, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\taz:  az,\n\t\t\t\t_id: idPtr,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\ttc := run(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\td := DB{db: tc.db}\n\t\t\tif err := d.CreateAuthorization(context.Background(), tc.az); err != nil {\n\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.err.Error())\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif assert.Nil(t, tc.err) {\n\t\t\t\t\tassert.Equals(t, tc.az.ID, *tc._id)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDB_UpdateAuthorization(t *testing.T) {\n\tazID := \"azID\"\n\tnow := clock.Now()\n\tdbaz := &dbAuthz{\n\t\tID:        azID,\n\t\tAccountID: \"accountID\",\n\t\tIdentifier: acme.Identifier{\n\t\t\tType:  \"dns\",\n\t\t\tValue: \"test.ca.smallstep.com\",\n\t\t},\n\t\tStatus:       acme.StatusPending,\n\t\tToken:        \"token\",\n\t\tCreatedAt:    now,\n\t\tExpiresAt:    now.Add(5 * time.Minute),\n\t\tChallengeIDs: []string{\"foo\", \"bar\"},\n\t\tWildcard:     true,\n\t\tFingerprint:  \"fingerprint\",\n\t}\n\tb, err := json.Marshal(dbaz)\n\tassert.FatalError(t, err)\n\ttype test struct {\n\t\tdb  nosql.DB\n\t\taz  *acme.Authorization\n\t\terr error\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/db.Get-error\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\taz: &acme.Authorization{\n\t\t\t\t\tID: azID,\n\t\t\t\t},\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, authzTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), azID)\n\n\t\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"error loading authz azID: force\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/db.CmpAndSwap-error\": func(t *testing.T) test {\n\t\t\tupdAz := &acme.Authorization{\n\t\t\t\tID:     azID,\n\t\t\t\tStatus: acme.StatusValid,\n\t\t\t\tError:  acme.NewError(acme.ErrorMalformedType, \"malformed\"),\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\taz: updAz,\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, authzTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), azID)\n\n\t\t\t\t\t\treturn b, nil\n\t\t\t\t\t},\n\t\t\t\t\tMCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, authzTable)\n\t\t\t\t\t\tassert.Equals(t, old, b)\n\n\t\t\t\t\t\tdbOld := new(dbAuthz)\n\t\t\t\t\t\tassert.FatalError(t, json.Unmarshal(old, dbOld))\n\t\t\t\t\t\tassert.Equals(t, dbaz, dbOld)\n\n\t\t\t\t\t\tdbNew := new(dbAuthz)\n\t\t\t\t\t\tassert.FatalError(t, json.Unmarshal(nu, dbNew))\n\t\t\t\t\t\tassert.Equals(t, dbNew.ID, dbaz.ID)\n\t\t\t\t\t\tassert.Equals(t, dbNew.AccountID, dbaz.AccountID)\n\t\t\t\t\t\tassert.Equals(t, dbNew.Identifier, dbaz.Identifier)\n\t\t\t\t\t\tassert.Equals(t, dbNew.Status, acme.StatusValid)\n\t\t\t\t\t\tassert.Equals(t, dbNew.Token, dbaz.Token)\n\t\t\t\t\t\tassert.Equals(t, dbNew.ChallengeIDs, dbaz.ChallengeIDs)\n\t\t\t\t\t\tassert.Equals(t, dbNew.Wildcard, dbaz.Wildcard)\n\t\t\t\t\t\tassert.Equals(t, dbNew.CreatedAt, dbaz.CreatedAt)\n\t\t\t\t\t\tassert.Equals(t, dbNew.ExpiresAt, dbaz.ExpiresAt)\n\t\t\t\t\t\tassert.Equals(t, dbNew.Error.Error(), acme.NewError(acme.ErrorMalformedType, \"The request message was malformed\").Error())\n\t\t\t\t\t\treturn nil, false, errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"error saving acme authz: force\"),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tupdAz := &acme.Authorization{\n\t\t\t\tID:         azID,\n\t\t\t\tAccountID:  dbaz.AccountID,\n\t\t\t\tStatus:     acme.StatusValid,\n\t\t\t\tIdentifier: dbaz.Identifier,\n\t\t\t\tChallenges: []*acme.Challenge{\n\t\t\t\t\t{ID: \"foo\"},\n\t\t\t\t\t{ID: \"bar\"},\n\t\t\t\t},\n\t\t\t\tToken:       dbaz.Token,\n\t\t\t\tWildcard:    dbaz.Wildcard,\n\t\t\t\tExpiresAt:   dbaz.ExpiresAt,\n\t\t\t\tFingerprint: \"fingerprint\",\n\t\t\t\tError:       acme.NewError(acme.ErrorMalformedType, \"malformed\"),\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\taz: updAz,\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, authzTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), azID)\n\n\t\t\t\t\t\treturn b, nil\n\t\t\t\t\t},\n\t\t\t\t\tMCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, authzTable)\n\t\t\t\t\t\tassert.Equals(t, old, b)\n\n\t\t\t\t\t\tdbOld := new(dbAuthz)\n\t\t\t\t\t\tassert.FatalError(t, json.Unmarshal(old, dbOld))\n\t\t\t\t\t\tassert.Equals(t, dbaz, dbOld)\n\n\t\t\t\t\t\tdbNew := new(dbAuthz)\n\t\t\t\t\t\tassert.FatalError(t, json.Unmarshal(nu, dbNew))\n\t\t\t\t\t\tassert.Equals(t, dbNew.ID, dbaz.ID)\n\t\t\t\t\t\tassert.Equals(t, dbNew.AccountID, dbaz.AccountID)\n\t\t\t\t\t\tassert.Equals(t, dbNew.Identifier, dbaz.Identifier)\n\t\t\t\t\t\tassert.Equals(t, dbNew.Status, acme.StatusValid)\n\t\t\t\t\t\tassert.Equals(t, dbNew.Token, dbaz.Token)\n\t\t\t\t\t\tassert.Equals(t, dbNew.ChallengeIDs, dbaz.ChallengeIDs)\n\t\t\t\t\t\tassert.Equals(t, dbNew.Wildcard, dbaz.Wildcard)\n\t\t\t\t\t\tassert.Equals(t, dbNew.CreatedAt, dbaz.CreatedAt)\n\t\t\t\t\t\tassert.Equals(t, dbNew.ExpiresAt, dbaz.ExpiresAt)\n\t\t\t\t\t\tassert.Equals(t, dbNew.Fingerprint, dbaz.Fingerprint)\n\t\t\t\t\t\tassert.Equals(t, dbNew.Error.Error(), acme.NewError(acme.ErrorMalformedType, \"The request message was malformed\").Error())\n\t\t\t\t\t\treturn nu, true, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\ttc := run(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\td := DB{db: tc.db}\n\t\t\tif err := d.UpdateAuthorization(context.Background(), tc.az); err != nil {\n\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.err.Error())\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif assert.Nil(t, tc.err) {\n\t\t\t\t\tassert.Equals(t, tc.az.ID, dbaz.ID)\n\t\t\t\t\tassert.Equals(t, tc.az.AccountID, dbaz.AccountID)\n\t\t\t\t\tassert.Equals(t, tc.az.Identifier, dbaz.Identifier)\n\t\t\t\t\tassert.Equals(t, tc.az.Status, acme.StatusValid)\n\t\t\t\t\tassert.Equals(t, tc.az.Wildcard, dbaz.Wildcard)\n\t\t\t\t\tassert.Equals(t, tc.az.Token, dbaz.Token)\n\t\t\t\t\tassert.Equals(t, tc.az.ExpiresAt, dbaz.ExpiresAt)\n\t\t\t\t\tassert.Equals(t, tc.az.Challenges, []*acme.Challenge{\n\t\t\t\t\t\t{ID: \"foo\"},\n\t\t\t\t\t\t{ID: \"bar\"},\n\t\t\t\t\t})\n\t\t\t\t\tassert.Equals(t, tc.az.Error.Error(), acme.NewError(acme.ErrorMalformedType, \"malformed\").Error())\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDB_GetAuthorizationsByAccountID(t *testing.T) {\n\tazID := \"azID\"\n\taccountID := \"accountID\"\n\ttype test struct {\n\t\tdb      nosql.DB\n\t\terr     error\n\t\tacmeErr *acme.Error\n\t\tauthzs  []*acme.Authorization\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/db.List-error\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMList: func(bucket []byte) ([]*nosqldb.Entry, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, authzTable)\n\t\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"error listing authz: force\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/unmarshal\": func(t *testing.T) test {\n\t\t\tb := []byte(`{malformed}`)\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMList: func(bucket []byte) ([]*nosqldb.Entry, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, authzTable)\n\t\t\t\t\t\treturn []*nosqldb.Entry{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tBucket: bucket,\n\t\t\t\t\t\t\t\tKey:    []byte(azID),\n\t\t\t\t\t\t\t\tValue:  b,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t}, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tauthzs: nil,\n\t\t\t\terr:    fmt.Errorf(\"error unmarshaling dbAuthz key '%s' into dbAuthz struct\", azID),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tnow := clock.Now()\n\t\t\tdbaz := &dbAuthz{\n\t\t\t\tID:        azID,\n\t\t\t\tAccountID: accountID,\n\t\t\t\tIdentifier: acme.Identifier{\n\t\t\t\t\tType:  \"dns\",\n\t\t\t\t\tValue: \"test.ca.smallstep.com\",\n\t\t\t\t},\n\t\t\t\tStatus:       acme.StatusValid,\n\t\t\t\tToken:        \"token\",\n\t\t\t\tCreatedAt:    now,\n\t\t\t\tExpiresAt:    now.Add(5 * time.Minute),\n\t\t\t\tChallengeIDs: []string{\"foo\", \"bar\"},\n\t\t\t\tWildcard:     true,\n\t\t\t}\n\t\t\tb, err := json.Marshal(dbaz)\n\t\t\tassert.FatalError(t, err)\n\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMList: func(bucket []byte) ([]*nosqldb.Entry, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, authzTable)\n\t\t\t\t\t\treturn []*nosqldb.Entry{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tBucket: bucket,\n\t\t\t\t\t\t\t\tKey:    []byte(azID),\n\t\t\t\t\t\t\t\tValue:  b,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t}, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tauthzs: []*acme.Authorization{\n\t\t\t\t\t{\n\t\t\t\t\t\tID:         dbaz.ID,\n\t\t\t\t\t\tAccountID:  dbaz.AccountID,\n\t\t\t\t\t\tToken:      dbaz.Token,\n\t\t\t\t\t\tIdentifier: dbaz.Identifier,\n\t\t\t\t\t\tStatus:     dbaz.Status,\n\t\t\t\t\t\tChallenges: nil,\n\t\t\t\t\t\tWildcard:   dbaz.Wildcard,\n\t\t\t\t\t\tExpiresAt:  dbaz.ExpiresAt,\n\t\t\t\t\t\tError:      dbaz.Error,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"ok/skip-different-account\": func(t *testing.T) test {\n\t\t\tnow := clock.Now()\n\t\t\tdbaz := &dbAuthz{\n\t\t\t\tID:        azID,\n\t\t\t\tAccountID: \"differentAccountID\",\n\t\t\t\tIdentifier: acme.Identifier{\n\t\t\t\t\tType:  \"dns\",\n\t\t\t\t\tValue: \"test.ca.smallstep.com\",\n\t\t\t\t},\n\t\t\t\tStatus:       acme.StatusValid,\n\t\t\t\tToken:        \"token\",\n\t\t\t\tCreatedAt:    now,\n\t\t\t\tExpiresAt:    now.Add(5 * time.Minute),\n\t\t\t\tChallengeIDs: []string{\"foo\", \"bar\"},\n\t\t\t\tWildcard:     true,\n\t\t\t}\n\t\t\tb, err := json.Marshal(dbaz)\n\t\t\tassert.FatalError(t, err)\n\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMList: func(bucket []byte) ([]*nosqldb.Entry, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, authzTable)\n\t\t\t\t\t\treturn []*nosqldb.Entry{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tBucket: bucket,\n\t\t\t\t\t\t\t\tKey:    []byte(azID),\n\t\t\t\t\t\t\t\tValue:  b,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t}, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tauthzs: []*acme.Authorization{},\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\ttc := run(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\td := DB{db: tc.db}\n\t\t\tif azs, err := d.GetAuthorizationsByAccountID(context.Background(), accountID); err != nil {\n\t\t\t\tvar acmeErr *acme.Error\n\t\t\t\tif errors.As(err, &acmeErr) {\n\t\t\t\t\tif assert.NotNil(t, tc.acmeErr) {\n\t\t\t\t\t\tassert.Equals(t, acmeErr.Type, tc.acmeErr.Type)\n\t\t\t\t\t\tassert.Equals(t, acmeErr.Detail, tc.acmeErr.Detail)\n\t\t\t\t\t\tassert.Equals(t, acmeErr.Status, tc.acmeErr.Status)\n\t\t\t\t\t\tassert.Equals(t, acmeErr.Err.Error(), tc.acmeErr.Err.Error())\n\t\t\t\t\t\tassert.Equals(t, acmeErr.Detail, tc.acmeErr.Detail)\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.err.Error())\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else if assert.Nil(t, tc.err) {\n\t\t\t\tif !cmp.Equal(azs, tc.authzs) {\n\t\t\t\t\tt.Errorf(\"db.GetAuthorizationsByAccountID() diff =\\n%s\", cmp.Diff(azs, tc.authzs))\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "acme/db/nosql/certificate.go",
    "content": "package nosql\n\nimport (\n\t\"context\"\n\t\"crypto/x509\"\n\t\"encoding/json\"\n\t\"encoding/pem\"\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n\t\"github.com/smallstep/certificates/acme\"\n\t\"github.com/smallstep/nosql\"\n)\n\ntype dbCert struct {\n\tID            string    `json:\"id\"`\n\tCreatedAt     time.Time `json:\"createdAt\"`\n\tAccountID     string    `json:\"accountID\"`\n\tOrderID       string    `json:\"orderID\"`\n\tLeaf          []byte    `json:\"leaf\"`\n\tIntermediates []byte    `json:\"intermediates\"`\n}\n\ntype dbSerial struct {\n\tSerial        string `json:\"serial\"`\n\tCertificateID string `json:\"certificateID\"`\n}\n\n// CreateCertificate creates and stores an ACME certificate type.\nfunc (db *DB) CreateCertificate(ctx context.Context, cert *acme.Certificate) error {\n\tvar err error\n\tcert.ID, err = randID()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tleaf := pem.EncodeToMemory(&pem.Block{\n\t\tType:  \"CERTIFICATE\",\n\t\tBytes: cert.Leaf.Raw,\n\t})\n\tvar intermediates []byte\n\tfor _, cert := range cert.Intermediates {\n\t\tintermediates = append(intermediates, pem.EncodeToMemory(&pem.Block{\n\t\t\tType:  \"CERTIFICATE\",\n\t\t\tBytes: cert.Raw,\n\t\t})...)\n\t}\n\n\tdbch := &dbCert{\n\t\tID:            cert.ID,\n\t\tAccountID:     cert.AccountID,\n\t\tOrderID:       cert.OrderID,\n\t\tLeaf:          leaf,\n\t\tIntermediates: intermediates,\n\t\tCreatedAt:     time.Now().UTC(),\n\t}\n\terr = db.save(ctx, cert.ID, dbch, nil, \"certificate\", certTable)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tserial := cert.Leaf.SerialNumber.String()\n\tdbSerial := &dbSerial{\n\t\tSerial:        serial,\n\t\tCertificateID: cert.ID,\n\t}\n\treturn db.save(ctx, serial, dbSerial, nil, \"serial\", certBySerialTable)\n}\n\n// GetCertificate retrieves and unmarshals an ACME certificate type from the\n// datastore.\nfunc (db *DB) GetCertificate(_ context.Context, id string) (*acme.Certificate, error) {\n\tb, err := db.db.Get(certTable, []byte(id))\n\tif nosql.IsErrNotFound(err) {\n\t\treturn nil, acme.NewError(acme.ErrorMalformedType, \"certificate %s not found\", id)\n\t} else if err != nil {\n\t\treturn nil, errors.Wrapf(err, \"error loading certificate %s\", id)\n\t}\n\tdbC := new(dbCert)\n\tif err := json.Unmarshal(b, dbC); err != nil {\n\t\treturn nil, errors.Wrapf(err, \"error unmarshaling certificate %s\", id)\n\t}\n\n\tcerts, err := parseBundle(append(dbC.Leaf, dbC.Intermediates...))\n\tif err != nil {\n\t\treturn nil, errors.Wrapf(err, \"error parsing certificate chain for ACME certificate with ID %s\", id)\n\t}\n\n\treturn &acme.Certificate{\n\t\tID:            dbC.ID,\n\t\tAccountID:     dbC.AccountID,\n\t\tOrderID:       dbC.OrderID,\n\t\tLeaf:          certs[0],\n\t\tIntermediates: certs[1:],\n\t}, nil\n}\n\n// GetCertificateBySerial retrieves and unmarshals an ACME certificate type from the\n// datastore based on a certificate serial number.\nfunc (db *DB) GetCertificateBySerial(ctx context.Context, serial string) (*acme.Certificate, error) {\n\tb, err := db.db.Get(certBySerialTable, []byte(serial))\n\tif nosql.IsErrNotFound(err) {\n\t\treturn nil, acme.NewError(acme.ErrorMalformedType, \"certificate with serial %s not found\", serial)\n\t} else if err != nil {\n\t\treturn nil, errors.Wrapf(err, \"error loading certificate ID for serial %s\", serial)\n\t}\n\n\tdbSerial := new(dbSerial)\n\tif err := json.Unmarshal(b, dbSerial); err != nil {\n\t\treturn nil, errors.Wrapf(err, \"error unmarshaling certificate with serial %s\", serial)\n\t}\n\n\treturn db.GetCertificate(ctx, dbSerial.CertificateID)\n}\n\nfunc parseBundle(b []byte) ([]*x509.Certificate, error) {\n\tvar (\n\t\terr    error\n\t\tblock  *pem.Block\n\t\tbundle []*x509.Certificate\n\t)\n\tfor len(b) > 0 {\n\t\tblock, b = pem.Decode(b)\n\t\tif block == nil {\n\t\t\tbreak\n\t\t}\n\t\tif block.Type != \"CERTIFICATE\" {\n\t\t\treturn nil, errors.New(\"error decoding PEM: data contains block that is not a certificate\")\n\t\t}\n\t\tvar crt *x509.Certificate\n\t\tcrt, err = x509.ParseCertificate(block.Bytes)\n\t\tif err != nil {\n\t\t\treturn nil, errors.Wrapf(err, \"error parsing x509 certificate\")\n\t\t}\n\t\tbundle = append(bundle, crt)\n\t}\n\tif len(b) > 0 {\n\t\treturn nil, errors.New(\"error decoding PEM: unexpected data\")\n\t}\n\treturn bundle, nil\n}\n"
  },
  {
    "path": "acme/db/nosql/certificate_test.go",
    "content": "package nosql\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/x509\"\n\t\"encoding/json\"\n\t\"encoding/pem\"\n\t\"fmt\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n\t\"github.com/smallstep/assert\"\n\t\"github.com/smallstep/certificates/acme\"\n\t\"github.com/smallstep/certificates/db\"\n\t\"github.com/smallstep/nosql\"\n\tnosqldb \"github.com/smallstep/nosql/database\"\n\t\"go.step.sm/crypto/pemutil\"\n)\n\nfunc TestDB_CreateCertificate(t *testing.T) {\n\tleaf, err := pemutil.ReadCertificate(\"../../../authority/testdata/certs/foo.crt\")\n\tassert.FatalError(t, err)\n\tinter, err := pemutil.ReadCertificate(\"../../../authority/testdata/certs/intermediate_ca.crt\")\n\tassert.FatalError(t, err)\n\troot, err := pemutil.ReadCertificate(\"../../../authority/testdata/certs/root_ca.crt\")\n\tassert.FatalError(t, err)\n\ttype test struct {\n\t\tdb   nosql.DB\n\t\tcert *acme.Certificate\n\t\terr  error\n\t\t_id  *string\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/cmpAndSwap-error\": func(t *testing.T) test {\n\t\t\tcert := &acme.Certificate{\n\t\t\t\tAccountID:     \"accountID\",\n\t\t\t\tOrderID:       \"orderID\",\n\t\t\t\tLeaf:          leaf,\n\t\t\t\tIntermediates: []*x509.Certificate{inter, root},\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, certTable)\n\t\t\t\t\t\tassert.Equals(t, key, []byte(cert.ID))\n\t\t\t\t\t\tassert.Equals(t, old, nil)\n\n\t\t\t\t\t\tdbc := new(dbCert)\n\t\t\t\t\t\tassert.FatalError(t, json.Unmarshal(nu, dbc))\n\t\t\t\t\t\tassert.Equals(t, dbc.ID, string(key))\n\t\t\t\t\t\tassert.Equals(t, dbc.ID, cert.ID)\n\t\t\t\t\t\tassert.Equals(t, dbc.AccountID, cert.AccountID)\n\t\t\t\t\t\tassert.True(t, clock.Now().Add(-time.Minute).Before(dbc.CreatedAt))\n\t\t\t\t\t\tassert.True(t, clock.Now().Add(time.Minute).After(dbc.CreatedAt))\n\t\t\t\t\t\treturn nil, false, errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tcert: cert,\n\t\t\t\terr:  errors.New(\"error saving acme certificate: force\"),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tcert := &acme.Certificate{\n\t\t\t\tAccountID:     \"accountID\",\n\t\t\t\tOrderID:       \"orderID\",\n\t\t\t\tLeaf:          leaf,\n\t\t\t\tIntermediates: []*x509.Certificate{inter, root},\n\t\t\t}\n\t\t\tvar (\n\t\t\t\tid    string\n\t\t\t\tidPtr = &id\n\t\t\t)\n\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {\n\t\t\t\t\t\tif !bytes.Equal(bucket, certTable) && !bytes.Equal(bucket, certBySerialTable) {\n\t\t\t\t\t\t\tt.Fail()\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif bytes.Equal(bucket, certTable) {\n\t\t\t\t\t\t\t*idPtr = string(key)\n\t\t\t\t\t\t\tassert.Equals(t, bucket, certTable)\n\t\t\t\t\t\t\tassert.Equals(t, key, []byte(cert.ID))\n\t\t\t\t\t\t\tassert.Equals(t, old, nil)\n\n\t\t\t\t\t\t\tdbc := new(dbCert)\n\t\t\t\t\t\t\tassert.FatalError(t, json.Unmarshal(nu, dbc))\n\t\t\t\t\t\t\tassert.Equals(t, dbc.ID, string(key))\n\t\t\t\t\t\t\tassert.Equals(t, dbc.ID, cert.ID)\n\t\t\t\t\t\t\tassert.Equals(t, dbc.AccountID, cert.AccountID)\n\t\t\t\t\t\t\tassert.True(t, clock.Now().Add(-time.Minute).Before(dbc.CreatedAt))\n\t\t\t\t\t\t\tassert.True(t, clock.Now().Add(time.Minute).After(dbc.CreatedAt))\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif bytes.Equal(bucket, certBySerialTable) {\n\t\t\t\t\t\t\tassert.Equals(t, bucket, certBySerialTable)\n\t\t\t\t\t\t\tassert.Equals(t, key, []byte(cert.Leaf.SerialNumber.String()))\n\t\t\t\t\t\t\tassert.Equals(t, old, nil)\n\n\t\t\t\t\t\t\tdbs := new(dbSerial)\n\t\t\t\t\t\t\tassert.FatalError(t, json.Unmarshal(nu, dbs))\n\t\t\t\t\t\t\tassert.Equals(t, dbs.Serial, string(key))\n\t\t\t\t\t\t\tassert.Equals(t, dbs.CertificateID, cert.ID)\n\n\t\t\t\t\t\t\t*idPtr = cert.ID\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\treturn nil, true, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t_id:  idPtr,\n\t\t\t\tcert: cert,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\ttc := run(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\td := DB{db: tc.db}\n\t\t\tif err := d.CreateCertificate(context.Background(), tc.cert); err != nil {\n\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.err.Error())\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif assert.Nil(t, tc.err) {\n\t\t\t\t\tassert.Equals(t, tc.cert.ID, *tc._id)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDB_GetCertificate(t *testing.T) {\n\tleaf, err := pemutil.ReadCertificate(\"../../../authority/testdata/certs/foo.crt\")\n\tassert.FatalError(t, err)\n\tinter, err := pemutil.ReadCertificate(\"../../../authority/testdata/certs/intermediate_ca.crt\")\n\tassert.FatalError(t, err)\n\troot, err := pemutil.ReadCertificate(\"../../../authority/testdata/certs/root_ca.crt\")\n\tassert.FatalError(t, err)\n\n\tcertID := \"certID\"\n\ttype test struct {\n\t\tdb      nosql.DB\n\t\terr     error\n\t\tacmeErr *acme.Error\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/not-found\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, certTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), certID)\n\n\t\t\t\t\t\treturn nil, nosqldb.ErrNotFound\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tacmeErr: acme.NewError(acme.ErrorMalformedType, \"certificate certID not found\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/db.Get-error\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, certTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), certID)\n\n\t\t\t\t\t\treturn nil, errors.Errorf(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"error loading certificate certID: force\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/unmarshal-error\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, certTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), certID)\n\n\t\t\t\t\t\treturn []byte(\"foobar\"), nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"error unmarshaling certificate certID\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/parseBundle-error\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, certTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), certID)\n\n\t\t\t\t\t\tcert := dbCert{\n\t\t\t\t\t\t\tID:        certID,\n\t\t\t\t\t\t\tAccountID: \"accountID\",\n\t\t\t\t\t\t\tOrderID:   \"orderID\",\n\t\t\t\t\t\t\tLeaf: pem.EncodeToMemory(&pem.Block{\n\t\t\t\t\t\t\t\tType:  \"Public Key\",\n\t\t\t\t\t\t\t\tBytes: leaf.Raw,\n\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\tCreatedAt: clock.Now(),\n\t\t\t\t\t\t}\n\t\t\t\t\t\tb, err := json.Marshal(cert)\n\t\t\t\t\t\tassert.FatalError(t, err)\n\n\t\t\t\t\t\treturn b, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: errors.Errorf(\"error parsing certificate chain for ACME certificate with ID certID\"),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, certTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), certID)\n\n\t\t\t\t\t\tcert := dbCert{\n\t\t\t\t\t\t\tID:        certID,\n\t\t\t\t\t\t\tAccountID: \"accountID\",\n\t\t\t\t\t\t\tOrderID:   \"orderID\",\n\t\t\t\t\t\t\tLeaf: pem.EncodeToMemory(&pem.Block{\n\t\t\t\t\t\t\t\tType:  \"CERTIFICATE\",\n\t\t\t\t\t\t\t\tBytes: leaf.Raw,\n\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\tIntermediates: append(pem.EncodeToMemory(&pem.Block{\n\t\t\t\t\t\t\t\tType:  \"CERTIFICATE\",\n\t\t\t\t\t\t\t\tBytes: inter.Raw,\n\t\t\t\t\t\t\t}), pem.EncodeToMemory(&pem.Block{\n\t\t\t\t\t\t\t\tType:  \"CERTIFICATE\",\n\t\t\t\t\t\t\t\tBytes: root.Raw,\n\t\t\t\t\t\t\t})...),\n\t\t\t\t\t\t\tCreatedAt: clock.Now(),\n\t\t\t\t\t\t}\n\t\t\t\t\t\tb, err := json.Marshal(cert)\n\t\t\t\t\t\tassert.FatalError(t, err)\n\n\t\t\t\t\t\treturn b, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\ttc := run(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\td := DB{db: tc.db}\n\t\t\tcert, err := d.GetCertificate(context.Background(), certID)\n\t\t\tif err != nil {\n\t\t\t\tvar acmeErr *acme.Error\n\t\t\t\tif errors.As(err, &acmeErr) {\n\t\t\t\t\tif assert.NotNil(t, tc.acmeErr) {\n\t\t\t\t\t\tassert.Equals(t, acmeErr.Type, tc.acmeErr.Type)\n\t\t\t\t\t\tassert.Equals(t, acmeErr.Detail, tc.acmeErr.Detail)\n\t\t\t\t\t\tassert.Equals(t, acmeErr.Status, tc.acmeErr.Status)\n\t\t\t\t\t\tassert.Equals(t, acmeErr.Err.Error(), tc.acmeErr.Err.Error())\n\t\t\t\t\t\tassert.Equals(t, acmeErr.Detail, tc.acmeErr.Detail)\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.err.Error())\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else if assert.Nil(t, tc.err) {\n\t\t\t\tassert.Equals(t, cert.ID, certID)\n\t\t\t\tassert.Equals(t, cert.AccountID, \"accountID\")\n\t\t\t\tassert.Equals(t, cert.OrderID, \"orderID\")\n\t\t\t\tassert.Equals(t, cert.Leaf, leaf)\n\t\t\t\tassert.Equals(t, cert.Intermediates, []*x509.Certificate{inter, root})\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_parseBundle(t *testing.T) {\n\tleaf, err := pemutil.ReadCertificate(\"../../../authority/testdata/certs/foo.crt\")\n\tassert.FatalError(t, err)\n\tinter, err := pemutil.ReadCertificate(\"../../../authority/testdata/certs/intermediate_ca.crt\")\n\tassert.FatalError(t, err)\n\troot, err := pemutil.ReadCertificate(\"../../../authority/testdata/certs/root_ca.crt\")\n\tassert.FatalError(t, err)\n\n\tvar certs []byte\n\tfor _, cert := range []*x509.Certificate{leaf, inter, root} {\n\t\tcerts = append(certs, pem.EncodeToMemory(&pem.Block{\n\t\t\tType:  \"CERTIFICATE\",\n\t\t\tBytes: cert.Raw,\n\t\t})...)\n\t}\n\n\ttype test struct {\n\t\tb   []byte\n\t\terr error\n\t}\n\tvar tests = map[string]test{\n\t\t\"fail/bad-type-error\": {\n\t\t\tb: pem.EncodeToMemory(&pem.Block{\n\t\t\t\tType:  \"Public Key\",\n\t\t\t\tBytes: leaf.Raw,\n\t\t\t}),\n\t\t\terr: errors.Errorf(\"error decoding PEM: data contains block that is not a certificate\"),\n\t\t},\n\t\t\"fail/bad-pem-error\": {\n\t\t\tb: pem.EncodeToMemory(&pem.Block{\n\t\t\t\tType:  \"CERTIFICATE\",\n\t\t\t\tBytes: []byte(\"foo\"),\n\t\t\t}),\n\t\t\terr: errors.Errorf(\"error parsing x509 certificate\"),\n\t\t},\n\t\t\"fail/unexpected-data\": {\n\t\t\tb: append(pem.EncodeToMemory(&pem.Block{\n\t\t\t\tType:  \"CERTIFICATE\",\n\t\t\t\tBytes: leaf.Raw,\n\t\t\t}), []byte(\"foo\")...),\n\t\t\terr: errors.Errorf(\"error decoding PEM: unexpected data\"),\n\t\t},\n\t\t\"ok\": {\n\t\t\tb: certs,\n\t\t},\n\t}\n\tfor name, tc := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tret, err := parseBundle(tc.b)\n\t\t\tif err != nil {\n\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.err.Error())\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif assert.Nil(t, tc.err) {\n\t\t\t\t\tassert.Equals(t, ret, []*x509.Certificate{leaf, inter, root})\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDB_GetCertificateBySerial(t *testing.T) {\n\tleaf, err := pemutil.ReadCertificate(\"../../../authority/testdata/certs/foo.crt\")\n\tassert.FatalError(t, err)\n\tinter, err := pemutil.ReadCertificate(\"../../../authority/testdata/certs/intermediate_ca.crt\")\n\tassert.FatalError(t, err)\n\troot, err := pemutil.ReadCertificate(\"../../../authority/testdata/certs/root_ca.crt\")\n\tassert.FatalError(t, err)\n\n\tcertID := \"certID\"\n\tserial := \"\"\n\ttype test struct {\n\t\tdb      nosql.DB\n\t\terr     error\n\t\tacmeErr *acme.Error\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/not-found\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tif bytes.Equal(bucket, certBySerialTable) {\n\t\t\t\t\t\t\treturn nil, nosqldb.ErrNotFound\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn nil, errors.New(\"wrong table\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tacmeErr: acme.NewError(acme.ErrorMalformedType, \"certificate with serial %s not found\", serial),\n\t\t\t}\n\t\t},\n\t\t\"fail/db-error\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tif bytes.Equal(bucket, certBySerialTable) {\n\t\t\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn nil, errors.New(\"wrong table\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: fmt.Errorf(\"error loading certificate ID for serial %s\", serial),\n\t\t\t}\n\t\t},\n\t\t\"fail/unmarshal-dbSerial\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tif bytes.Equal(bucket, certBySerialTable) {\n\t\t\t\t\t\t\treturn []byte(`{\"serial\":malformed!}`), nil\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn nil, errors.New(\"wrong table\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: fmt.Errorf(\"error unmarshaling certificate with serial %s\", serial),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\n\t\t\t\t\t\tif bytes.Equal(bucket, certBySerialTable) {\n\t\t\t\t\t\t\tcertSerial := dbSerial{\n\t\t\t\t\t\t\t\tSerial:        serial,\n\t\t\t\t\t\t\t\tCertificateID: certID,\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tb, err := json.Marshal(certSerial)\n\t\t\t\t\t\t\tassert.FatalError(t, err)\n\n\t\t\t\t\t\t\treturn b, nil\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif bytes.Equal(bucket, certTable) {\n\t\t\t\t\t\t\tcert := dbCert{\n\t\t\t\t\t\t\t\tID:        certID,\n\t\t\t\t\t\t\t\tAccountID: \"accountID\",\n\t\t\t\t\t\t\t\tOrderID:   \"orderID\",\n\t\t\t\t\t\t\t\tLeaf: pem.EncodeToMemory(&pem.Block{\n\t\t\t\t\t\t\t\t\tType:  \"CERTIFICATE\",\n\t\t\t\t\t\t\t\t\tBytes: leaf.Raw,\n\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t\tIntermediates: append(pem.EncodeToMemory(&pem.Block{\n\t\t\t\t\t\t\t\t\tType:  \"CERTIFICATE\",\n\t\t\t\t\t\t\t\t\tBytes: inter.Raw,\n\t\t\t\t\t\t\t\t}), pem.EncodeToMemory(&pem.Block{\n\t\t\t\t\t\t\t\t\tType:  \"CERTIFICATE\",\n\t\t\t\t\t\t\t\t\tBytes: root.Raw,\n\t\t\t\t\t\t\t\t})...),\n\t\t\t\t\t\t\t\tCreatedAt: clock.Now(),\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tb, err := json.Marshal(cert)\n\t\t\t\t\t\t\tassert.FatalError(t, err)\n\n\t\t\t\t\t\t\treturn b, nil\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn nil, errors.New(\"wrong table\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t}\n\tfor name, prep := range tests {\n\t\ttc := prep(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\td := DB{db: tc.db}\n\t\t\tcert, err := d.GetCertificateBySerial(context.Background(), serial)\n\t\t\tif err != nil {\n\t\t\t\tvar ae *acme.Error\n\t\t\t\tif errors.As(err, &ae) {\n\t\t\t\t\tif assert.NotNil(t, tc.acmeErr) {\n\t\t\t\t\t\tassert.Equals(t, ae.Type, tc.acmeErr.Type)\n\t\t\t\t\t\tassert.Equals(t, ae.Detail, tc.acmeErr.Detail)\n\t\t\t\t\t\tassert.Equals(t, ae.Status, tc.acmeErr.Status)\n\t\t\t\t\t\tassert.Equals(t, ae.Err.Error(), tc.acmeErr.Err.Error())\n\t\t\t\t\t\tassert.Equals(t, ae.Detail, tc.acmeErr.Detail)\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.err.Error())\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else if assert.Nil(t, tc.err) {\n\t\t\t\tassert.Equals(t, cert.ID, certID)\n\t\t\t\tassert.Equals(t, cert.AccountID, \"accountID\")\n\t\t\t\tassert.Equals(t, cert.OrderID, \"orderID\")\n\t\t\t\tassert.Equals(t, cert.Leaf, leaf)\n\t\t\t\tassert.Equals(t, cert.Intermediates, []*x509.Certificate{inter, root})\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "acme/db/nosql/challenge.go",
    "content": "package nosql\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n\n\t\"github.com/smallstep/nosql\"\n\n\t\"github.com/smallstep/certificates/acme\"\n)\n\ntype dbChallenge struct {\n\tID          string             `json:\"id\"`\n\tAccountID   string             `json:\"accountID\"`\n\tType        acme.ChallengeType `json:\"type\"`\n\tStatus      acme.Status        `json:\"status\"`\n\tToken       string             `json:\"token\"`\n\tValue       string             `json:\"value\"`\n\tTarget      string             `json:\"target,omitempty\"`\n\tValidatedAt string             `json:\"validatedAt\"`\n\tCreatedAt   time.Time          `json:\"createdAt\"`\n\tError       *acme.Error        `json:\"error\"` // TODO(hs): a bit dangerous; should become db-specific type\n}\n\nfunc (dbc *dbChallenge) clone() *dbChallenge {\n\tu := *dbc\n\treturn &u\n}\n\nfunc (db *DB) getDBChallenge(_ context.Context, id string) (*dbChallenge, error) {\n\tdata, err := db.db.Get(challengeTable, []byte(id))\n\tif nosql.IsErrNotFound(err) {\n\t\treturn nil, acme.NewError(acme.ErrorMalformedType, \"challenge %s not found\", id)\n\t} else if err != nil {\n\t\treturn nil, errors.Wrapf(err, \"error loading acme challenge %s\", id)\n\t}\n\n\tdbch := new(dbChallenge)\n\tif err := json.Unmarshal(data, dbch); err != nil {\n\t\treturn nil, errors.Wrap(err, \"error unmarshaling dbChallenge\")\n\t}\n\treturn dbch, nil\n}\n\n// CreateChallenge creates a new ACME challenge data structure in the database.\n// Implements acme.DB.CreateChallenge interface.\nfunc (db *DB) CreateChallenge(ctx context.Context, ch *acme.Challenge) error {\n\tvar err error\n\tch.ID, err = randID()\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"error generating random id for ACME challenge\")\n\t}\n\n\tdbch := &dbChallenge{\n\t\tID:        ch.ID,\n\t\tAccountID: ch.AccountID,\n\t\tValue:     ch.Value,\n\t\tStatus:    acme.StatusPending,\n\t\tToken:     ch.Token,\n\t\tCreatedAt: clock.Now(),\n\t\tType:      ch.Type,\n\t\tTarget:    ch.Target,\n\t}\n\n\treturn db.save(ctx, ch.ID, dbch, nil, \"challenge\", challengeTable)\n}\n\n// GetChallenge retrieves and unmarshals an ACME challenge type from the database.\n// Implements the acme.DB GetChallenge interface.\nfunc (db *DB) GetChallenge(ctx context.Context, id, authzID string) (*acme.Challenge, error) {\n\t_ = authzID // unused input\n\tdbch, err := db.getDBChallenge(ctx, id)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tch := &acme.Challenge{\n\t\tID:          dbch.ID,\n\t\tAccountID:   dbch.AccountID,\n\t\tType:        dbch.Type,\n\t\tValue:       dbch.Value,\n\t\tStatus:      dbch.Status,\n\t\tToken:       dbch.Token,\n\t\tError:       dbch.Error,\n\t\tValidatedAt: dbch.ValidatedAt,\n\t\tTarget:      dbch.Target,\n\t}\n\treturn ch, nil\n}\n\n// UpdateChallenge updates an ACME challenge type in the database.\nfunc (db *DB) UpdateChallenge(ctx context.Context, ch *acme.Challenge) error {\n\told, err := db.getDBChallenge(ctx, ch.ID)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tnu := old.clone()\n\n\t// These should be the only values changing in an Update request.\n\tnu.Status = ch.Status\n\tnu.Error = ch.Error\n\tnu.ValidatedAt = ch.ValidatedAt\n\n\treturn db.save(ctx, old.ID, nu, old, \"challenge\", challengeTable)\n}\n"
  },
  {
    "path": "acme/db/nosql/challenge_test.go",
    "content": "package nosql\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n\t\"github.com/smallstep/assert\"\n\t\"github.com/smallstep/certificates/acme\"\n\t\"github.com/smallstep/certificates/db\"\n\t\"github.com/smallstep/nosql\"\n\tnosqldb \"github.com/smallstep/nosql/database\"\n)\n\nfunc TestDB_getDBChallenge(t *testing.T) {\n\tchID := \"chID\"\n\ttype test struct {\n\t\tdb      nosql.DB\n\t\terr     error\n\t\tacmeErr *acme.Error\n\t\tdbc     *dbChallenge\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/not-found\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, challengeTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), chID)\n\n\t\t\t\t\t\treturn nil, nosqldb.ErrNotFound\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tacmeErr: acme.NewError(acme.ErrorMalformedType, \"challenge chID not found\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/db.Get-error\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, challengeTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), chID)\n\n\t\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"error loading acme challenge chID: force\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/unmarshal-error\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, challengeTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), chID)\n\n\t\t\t\t\t\treturn []byte(\"foo\"), nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"error unmarshaling dbChallenge\"),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tdbc := &dbChallenge{\n\t\t\t\tID:          chID,\n\t\t\t\tAccountID:   \"accountID\",\n\t\t\t\tType:        \"dns-01\",\n\t\t\t\tStatus:      acme.StatusPending,\n\t\t\t\tToken:       \"token\",\n\t\t\t\tValue:       \"test.ca.smallstep.com\",\n\t\t\t\tCreatedAt:   clock.Now(),\n\t\t\t\tValidatedAt: \"foobar\",\n\t\t\t\tError:       acme.NewErrorISE(\"The server experienced an internal error\"),\n\t\t\t}\n\t\t\tb, err := json.Marshal(dbc)\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, challengeTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), chID)\n\n\t\t\t\t\t\treturn b, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tdbc: dbc,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\ttc := run(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\td := DB{db: tc.db}\n\t\t\tif ch, err := d.getDBChallenge(context.Background(), chID); err != nil {\n\t\t\t\tvar ae *acme.Error\n\t\t\t\tif errors.As(err, &ae) {\n\t\t\t\t\tif assert.NotNil(t, tc.acmeErr) {\n\t\t\t\t\t\tassert.Equals(t, ae.Type, tc.acmeErr.Type)\n\t\t\t\t\t\tassert.Equals(t, ae.Detail, tc.acmeErr.Detail)\n\t\t\t\t\t\tassert.Equals(t, ae.Status, tc.acmeErr.Status)\n\t\t\t\t\t\tassert.Equals(t, ae.Err.Error(), tc.acmeErr.Err.Error())\n\t\t\t\t\t\tassert.Equals(t, ae.Detail, tc.acmeErr.Detail)\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.err.Error())\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else if assert.Nil(t, tc.err) {\n\t\t\t\tassert.Equals(t, ch.ID, tc.dbc.ID)\n\t\t\t\tassert.Equals(t, ch.AccountID, tc.dbc.AccountID)\n\t\t\t\tassert.Equals(t, ch.Type, tc.dbc.Type)\n\t\t\t\tassert.Equals(t, ch.Status, tc.dbc.Status)\n\t\t\t\tassert.Equals(t, ch.Token, tc.dbc.Token)\n\t\t\t\tassert.Equals(t, ch.Value, tc.dbc.Value)\n\t\t\t\tassert.Equals(t, ch.ValidatedAt, tc.dbc.ValidatedAt)\n\t\t\t\tassert.Equals(t, ch.Error.Error(), tc.dbc.Error.Error())\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDB_CreateChallenge(t *testing.T) {\n\ttype test struct {\n\t\tdb  nosql.DB\n\t\tch  *acme.Challenge\n\t\terr error\n\t\t_id *string\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/cmpAndSwap-error\": func(t *testing.T) test {\n\t\t\tch := &acme.Challenge{\n\t\t\t\tAccountID: \"accountID\",\n\t\t\t\tType:      \"dns-01\",\n\t\t\t\tStatus:    acme.StatusPending,\n\t\t\t\tToken:     \"token\",\n\t\t\t\tValue:     \"test.ca.smallstep.com\",\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, challengeTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), ch.ID)\n\t\t\t\t\t\tassert.Equals(t, old, nil)\n\n\t\t\t\t\t\tdbc := new(dbChallenge)\n\t\t\t\t\t\tassert.FatalError(t, json.Unmarshal(nu, dbc))\n\t\t\t\t\t\tassert.Equals(t, dbc.ID, string(key))\n\t\t\t\t\t\tassert.Equals(t, dbc.AccountID, ch.AccountID)\n\t\t\t\t\t\tassert.Equals(t, dbc.Type, ch.Type)\n\t\t\t\t\t\tassert.Equals(t, dbc.Status, ch.Status)\n\t\t\t\t\t\tassert.Equals(t, dbc.Token, ch.Token)\n\t\t\t\t\t\tassert.Equals(t, dbc.Value, ch.Value)\n\t\t\t\t\t\tassert.True(t, clock.Now().Add(-time.Minute).Before(dbc.CreatedAt))\n\t\t\t\t\t\tassert.True(t, clock.Now().Add(time.Minute).After(dbc.CreatedAt))\n\t\t\t\t\t\treturn nil, false, errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tch:  ch,\n\t\t\t\terr: errors.New(\"error saving acme challenge: force\"),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tvar (\n\t\t\t\tid    string\n\t\t\t\tidPtr = &id\n\t\t\t\tch    = &acme.Challenge{\n\t\t\t\t\tAccountID: \"accountID\",\n\t\t\t\t\tType:      \"dns-01\",\n\t\t\t\t\tStatus:    acme.StatusPending,\n\t\t\t\t\tToken:     \"token\",\n\t\t\t\t\tValue:     \"test.ca.smallstep.com\",\n\t\t\t\t}\n\t\t\t)\n\n\t\t\treturn test{\n\t\t\t\tch: ch,\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {\n\t\t\t\t\t\t*idPtr = string(key)\n\t\t\t\t\t\tassert.Equals(t, bucket, challengeTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), ch.ID)\n\t\t\t\t\t\tassert.Equals(t, old, nil)\n\n\t\t\t\t\t\tdbc := new(dbChallenge)\n\t\t\t\t\t\tassert.FatalError(t, json.Unmarshal(nu, dbc))\n\t\t\t\t\t\tassert.Equals(t, dbc.ID, string(key))\n\t\t\t\t\t\tassert.Equals(t, dbc.AccountID, ch.AccountID)\n\t\t\t\t\t\tassert.Equals(t, dbc.Type, ch.Type)\n\t\t\t\t\t\tassert.Equals(t, dbc.Status, ch.Status)\n\t\t\t\t\t\tassert.Equals(t, dbc.Token, ch.Token)\n\t\t\t\t\t\tassert.Equals(t, dbc.Value, ch.Value)\n\t\t\t\t\t\tassert.True(t, clock.Now().Add(-time.Minute).Before(dbc.CreatedAt))\n\t\t\t\t\t\tassert.True(t, clock.Now().Add(time.Minute).After(dbc.CreatedAt))\n\t\t\t\t\t\treturn nil, true, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t_id: idPtr,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\ttc := run(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\td := DB{db: tc.db}\n\t\t\tif err := d.CreateChallenge(context.Background(), tc.ch); err != nil {\n\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.err.Error())\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif assert.Nil(t, tc.err) {\n\t\t\t\t\tassert.Equals(t, tc.ch.ID, *tc._id)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDB_GetChallenge(t *testing.T) {\n\tchID := \"chID\"\n\tazID := \"azID\"\n\ttype test struct {\n\t\tdb      nosql.DB\n\t\terr     error\n\t\tacmeErr *acme.Error\n\t\tdbc     *dbChallenge\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/db.Get-error\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, challengeTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), chID)\n\n\t\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"error loading acme challenge chID: force\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/forward-acme-error\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, challengeTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), chID)\n\n\t\t\t\t\t\treturn nil, nosqldb.ErrNotFound\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tacmeErr: acme.NewError(acme.ErrorMalformedType, \"challenge chID not found\"),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tdbc := &dbChallenge{\n\t\t\t\tID:          chID,\n\t\t\t\tAccountID:   \"accountID\",\n\t\t\t\tType:        \"dns-01\",\n\t\t\t\tStatus:      acme.StatusPending,\n\t\t\t\tToken:       \"token\",\n\t\t\t\tValue:       \"test.ca.smallstep.com\",\n\t\t\t\tCreatedAt:   clock.Now(),\n\t\t\t\tValidatedAt: \"foobar\",\n\t\t\t\tError:       acme.NewErrorISE(\"The server experienced an internal error\"),\n\t\t\t}\n\t\t\tb, err := json.Marshal(dbc)\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, challengeTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), chID)\n\n\t\t\t\t\t\treturn b, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tdbc: dbc,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\ttc := run(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\td := DB{db: tc.db}\n\t\t\tif ch, err := d.GetChallenge(context.Background(), chID, azID); err != nil {\n\t\t\t\tvar ae *acme.Error\n\t\t\t\tif errors.As(err, &ae) {\n\t\t\t\t\tif assert.NotNil(t, tc.acmeErr) {\n\t\t\t\t\t\tassert.Equals(t, ae.Type, tc.acmeErr.Type)\n\t\t\t\t\t\tassert.Equals(t, ae.Detail, tc.acmeErr.Detail)\n\t\t\t\t\t\tassert.Equals(t, ae.Status, tc.acmeErr.Status)\n\t\t\t\t\t\tassert.Equals(t, ae.Err.Error(), tc.acmeErr.Err.Error())\n\t\t\t\t\t\tassert.Equals(t, ae.Detail, tc.acmeErr.Detail)\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.err.Error())\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else if assert.Nil(t, tc.err) {\n\t\t\t\tassert.Equals(t, ch.ID, tc.dbc.ID)\n\t\t\t\tassert.Equals(t, ch.AccountID, tc.dbc.AccountID)\n\t\t\t\tassert.Equals(t, ch.Type, tc.dbc.Type)\n\t\t\t\tassert.Equals(t, ch.Status, tc.dbc.Status)\n\t\t\t\tassert.Equals(t, ch.Token, tc.dbc.Token)\n\t\t\t\tassert.Equals(t, ch.Value, tc.dbc.Value)\n\t\t\t\tassert.Equals(t, ch.ValidatedAt, tc.dbc.ValidatedAt)\n\t\t\t\tassert.Equals(t, ch.Error.Error(), tc.dbc.Error.Error())\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDB_UpdateChallenge(t *testing.T) {\n\tchID := \"chID\"\n\tdbc := &dbChallenge{\n\t\tID:        chID,\n\t\tAccountID: \"accountID\",\n\t\tType:      \"dns-01\",\n\t\tStatus:    acme.StatusPending,\n\t\tToken:     \"token\",\n\t\tValue:     \"test.ca.smallstep.com\",\n\t\tCreatedAt: clock.Now(),\n\t}\n\tb, err := json.Marshal(dbc)\n\tassert.FatalError(t, err)\n\ttype test struct {\n\t\tdb  nosql.DB\n\t\tch  *acme.Challenge\n\t\terr error\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/db.Get-error\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tch: &acme.Challenge{\n\t\t\t\t\tID: chID,\n\t\t\t\t},\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, challengeTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), chID)\n\n\t\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"error loading acme challenge chID: force\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/db.CmpAndSwap-error\": func(t *testing.T) test {\n\t\t\tupdCh := &acme.Challenge{\n\t\t\t\tID:          chID,\n\t\t\t\tStatus:      acme.StatusValid,\n\t\t\t\tValidatedAt: \"foobar\",\n\t\t\t\tError:       acme.NewError(acme.ErrorMalformedType, \"The request message was malformed\"),\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tch: updCh,\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, challengeTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), chID)\n\n\t\t\t\t\t\treturn b, nil\n\t\t\t\t\t},\n\t\t\t\t\tMCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, challengeTable)\n\t\t\t\t\t\tassert.Equals(t, old, b)\n\n\t\t\t\t\t\tdbOld := new(dbChallenge)\n\t\t\t\t\t\tassert.FatalError(t, json.Unmarshal(old, dbOld))\n\t\t\t\t\t\tassert.Equals(t, dbc, dbOld)\n\n\t\t\t\t\t\tdbNew := new(dbChallenge)\n\t\t\t\t\t\tassert.FatalError(t, json.Unmarshal(nu, dbNew))\n\t\t\t\t\t\tassert.Equals(t, dbNew.ID, dbc.ID)\n\t\t\t\t\t\tassert.Equals(t, dbNew.AccountID, dbc.AccountID)\n\t\t\t\t\t\tassert.Equals(t, dbNew.Type, dbc.Type)\n\t\t\t\t\t\tassert.Equals(t, dbNew.Status, updCh.Status)\n\t\t\t\t\t\tassert.Equals(t, dbNew.Token, dbc.Token)\n\t\t\t\t\t\tassert.Equals(t, dbNew.Value, dbc.Value)\n\t\t\t\t\t\tassert.Equals(t, dbNew.Error.Error(), updCh.Error.Error())\n\t\t\t\t\t\tassert.Equals(t, dbNew.CreatedAt, dbc.CreatedAt)\n\t\t\t\t\t\tassert.Equals(t, dbNew.ValidatedAt, updCh.ValidatedAt)\n\t\t\t\t\t\treturn nil, false, errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"error saving acme challenge: force\"),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tupdCh := &acme.Challenge{\n\t\t\t\tID:          dbc.ID,\n\t\t\t\tAccountID:   dbc.AccountID,\n\t\t\t\tType:        dbc.Type,\n\t\t\t\tToken:       dbc.Token,\n\t\t\t\tValue:       dbc.Value,\n\t\t\t\tStatus:      acme.StatusValid,\n\t\t\t\tValidatedAt: \"foobar\",\n\t\t\t\tError:       acme.NewError(acme.ErrorMalformedType, \"malformed\"),\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tch: updCh,\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, challengeTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), chID)\n\n\t\t\t\t\t\treturn b, nil\n\t\t\t\t\t},\n\t\t\t\t\tMCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, challengeTable)\n\t\t\t\t\t\tassert.Equals(t, old, b)\n\n\t\t\t\t\t\tdbOld := new(dbChallenge)\n\t\t\t\t\t\tassert.FatalError(t, json.Unmarshal(old, dbOld))\n\t\t\t\t\t\tassert.Equals(t, dbc, dbOld)\n\n\t\t\t\t\t\tdbNew := new(dbChallenge)\n\t\t\t\t\t\tassert.FatalError(t, json.Unmarshal(nu, dbNew))\n\t\t\t\t\t\tassert.Equals(t, dbNew.ID, dbc.ID)\n\t\t\t\t\t\tassert.Equals(t, dbNew.AccountID, dbc.AccountID)\n\t\t\t\t\t\tassert.Equals(t, dbNew.Type, dbc.Type)\n\t\t\t\t\t\tassert.Equals(t, dbNew.Token, dbc.Token)\n\t\t\t\t\t\tassert.Equals(t, dbNew.Value, dbc.Value)\n\t\t\t\t\t\tassert.Equals(t, dbNew.CreatedAt, dbc.CreatedAt)\n\t\t\t\t\t\tassert.Equals(t, dbNew.Status, acme.StatusValid)\n\t\t\t\t\t\tassert.Equals(t, dbNew.ValidatedAt, \"foobar\")\n\t\t\t\t\t\tassert.Equals(t, dbNew.Error.Error(), acme.NewError(acme.ErrorMalformedType, \"The request message was malformed\").Error())\n\t\t\t\t\t\treturn nu, true, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\ttc := run(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\td := DB{db: tc.db}\n\t\t\tif err := d.UpdateChallenge(context.Background(), tc.ch); err != nil {\n\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.err.Error())\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif assert.Nil(t, tc.err) {\n\t\t\t\t\tassert.Equals(t, tc.ch.ID, dbc.ID)\n\t\t\t\t\tassert.Equals(t, tc.ch.AccountID, dbc.AccountID)\n\t\t\t\t\tassert.Equals(t, tc.ch.Type, dbc.Type)\n\t\t\t\t\tassert.Equals(t, tc.ch.Token, dbc.Token)\n\t\t\t\t\tassert.Equals(t, tc.ch.Value, dbc.Value)\n\t\t\t\t\tassert.Equals(t, tc.ch.ValidatedAt, \"foobar\")\n\t\t\t\t\tassert.Equals(t, tc.ch.Status, acme.StatusValid)\n\t\t\t\t\tassert.Equals(t, tc.ch.Error.Error(), acme.NewError(acme.ErrorMalformedType, \"malformed\").Error())\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "acme/db/nosql/eab.go",
    "content": "package nosql\n\nimport (\n\t\"context\"\n\t\"crypto/rand\"\n\t\"encoding/json\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n\n\t\"github.com/smallstep/certificates/acme\"\n\tnosqlDB \"github.com/smallstep/nosql\"\n)\n\n// externalAccountKeyMutex for read/write locking of EAK operations.\nvar externalAccountKeyMutex sync.RWMutex\n\n// referencesByProvisionerIndexMutex for locking referencesByProvisioner index operations.\nvar referencesByProvisionerIndexMutex sync.Mutex\n\ntype dbExternalAccountKey struct {\n\tID            string    `json:\"id\"`\n\tProvisionerID string    `json:\"provisionerID\"`\n\tReference     string    `json:\"reference\"`\n\tAccountID     string    `json:\"accountID,omitempty\"`\n\tHmacKey       []byte    `json:\"key\"`\n\tCreatedAt     time.Time `json:\"createdAt\"`\n\tBoundAt       time.Time `json:\"boundAt\"`\n}\n\ntype dbExternalAccountKeyReference struct {\n\tReference            string `json:\"reference\"`\n\tExternalAccountKeyID string `json:\"externalAccountKeyID\"`\n}\n\n// getDBExternalAccountKey retrieves and unmarshals dbExternalAccountKey.\nfunc (db *DB) getDBExternalAccountKey(_ context.Context, id string) (*dbExternalAccountKey, error) {\n\tdata, err := db.db.Get(externalAccountKeyTable, []byte(id))\n\tif err != nil {\n\t\tif nosqlDB.IsErrNotFound(err) {\n\t\t\treturn nil, acme.ErrNotFound\n\t\t}\n\t\treturn nil, errors.Wrapf(err, \"error loading external account key %s\", id)\n\t}\n\n\tdbeak := new(dbExternalAccountKey)\n\tif err = json.Unmarshal(data, dbeak); err != nil {\n\t\treturn nil, errors.Wrapf(err, \"error unmarshaling external account key %s into dbExternalAccountKey\", id)\n\t}\n\n\treturn dbeak, nil\n}\n\n// CreateExternalAccountKey creates a new External Account Binding key with a name\nfunc (db *DB) CreateExternalAccountKey(ctx context.Context, provisionerID, reference string) (*acme.ExternalAccountKey, error) {\n\texternalAccountKeyMutex.Lock()\n\tdefer externalAccountKeyMutex.Unlock()\n\n\tkeyID, err := randID()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\trandom := make([]byte, 32)\n\t_, err = rand.Read(random)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tdbeak := &dbExternalAccountKey{\n\t\tID:            keyID,\n\t\tProvisionerID: provisionerID,\n\t\tReference:     reference,\n\t\tHmacKey:       random,\n\t\tCreatedAt:     clock.Now(),\n\t}\n\n\tif err := db.save(ctx, keyID, dbeak, nil, \"external_account_key\", externalAccountKeyTable); err != nil {\n\t\treturn nil, err\n\t}\n\n\tif err := db.addEAKID(ctx, provisionerID, dbeak.ID); err != nil {\n\t\treturn nil, err\n\t}\n\n\tif dbeak.Reference != \"\" {\n\t\tdbExternalAccountKeyReference := &dbExternalAccountKeyReference{\n\t\t\tReference:            dbeak.Reference,\n\t\t\tExternalAccountKeyID: dbeak.ID,\n\t\t}\n\t\tif err := db.save(ctx, referenceKey(provisionerID, dbeak.Reference), dbExternalAccountKeyReference, nil, \"external_account_key_reference\", externalAccountKeyIDsByReferenceTable); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn &acme.ExternalAccountKey{\n\t\tID:            dbeak.ID,\n\t\tProvisionerID: dbeak.ProvisionerID,\n\t\tReference:     dbeak.Reference,\n\t\tAccountID:     dbeak.AccountID,\n\t\tHmacKey:       dbeak.HmacKey,\n\t\tCreatedAt:     dbeak.CreatedAt,\n\t\tBoundAt:       dbeak.BoundAt,\n\t}, nil\n}\n\n// GetExternalAccountKey retrieves an External Account Binding key by KeyID\nfunc (db *DB) GetExternalAccountKey(ctx context.Context, provisionerID, keyID string) (*acme.ExternalAccountKey, error) {\n\texternalAccountKeyMutex.RLock()\n\tdefer externalAccountKeyMutex.RUnlock()\n\n\tdbeak, err := db.getDBExternalAccountKey(ctx, keyID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif dbeak.ProvisionerID != provisionerID {\n\t\treturn nil, acme.NewError(acme.ErrorUnauthorizedType, \"provisioner does not match provisioner for which the EAB key was created\")\n\t}\n\n\treturn &acme.ExternalAccountKey{\n\t\tID:            dbeak.ID,\n\t\tProvisionerID: dbeak.ProvisionerID,\n\t\tReference:     dbeak.Reference,\n\t\tAccountID:     dbeak.AccountID,\n\t\tHmacKey:       dbeak.HmacKey,\n\t\tCreatedAt:     dbeak.CreatedAt,\n\t\tBoundAt:       dbeak.BoundAt,\n\t}, nil\n}\n\nfunc (db *DB) DeleteExternalAccountKey(ctx context.Context, provisionerID, keyID string) error {\n\texternalAccountKeyMutex.Lock()\n\tdefer externalAccountKeyMutex.Unlock()\n\n\tdbeak, err := db.getDBExternalAccountKey(ctx, keyID)\n\tif err != nil {\n\t\treturn errors.Wrapf(err, \"error loading ACME EAB Key with Key ID %s\", keyID)\n\t}\n\n\tif dbeak.ProvisionerID != provisionerID {\n\t\treturn errors.New(\"provisioner does not match provisioner for which the EAB key was created\")\n\t}\n\n\tif dbeak.Reference != \"\" {\n\t\tif err := db.db.Del(externalAccountKeyIDsByReferenceTable, []byte(referenceKey(provisionerID, dbeak.Reference))); err != nil {\n\t\t\treturn errors.Wrapf(err, \"error deleting ACME EAB Key reference with Key ID %s and reference %s\", keyID, dbeak.Reference)\n\t\t}\n\t}\n\tif err := db.db.Del(externalAccountKeyTable, []byte(keyID)); err != nil {\n\t\treturn errors.Wrapf(err, \"error deleting ACME EAB Key with Key ID %s\", keyID)\n\t}\n\tif err := db.deleteEAKID(ctx, provisionerID, keyID); err != nil {\n\t\treturn errors.Wrapf(err, \"error removing ACME EAB Key ID %s\", keyID)\n\t}\n\n\treturn nil\n}\n\n// GetExternalAccountKeys retrieves all External Account Binding keys for a provisioner\nfunc (db *DB) GetExternalAccountKeys(ctx context.Context, provisionerID, cursor string, limit int) ([]*acme.ExternalAccountKey, string, error) {\n\t_, _ = cursor, limit // unused input\n\n\texternalAccountKeyMutex.RLock()\n\tdefer externalAccountKeyMutex.RUnlock()\n\n\t// cursor and limit are ignored in open source, at least for now.\n\n\tvar eakIDs []string\n\tr, err := db.db.Get(externalAccountKeyIDsByProvisionerIDTable, []byte(provisionerID))\n\tif err != nil {\n\t\tif !nosqlDB.IsErrNotFound(err) {\n\t\t\treturn nil, \"\", errors.Wrapf(err, \"error loading ACME EAB Key IDs for provisioner %s\", provisionerID)\n\t\t}\n\t\t// it may happen that no record is found; we'll continue with an empty slice\n\t} else {\n\t\tif err := json.Unmarshal(r, &eakIDs); err != nil {\n\t\t\treturn nil, \"\", errors.Wrapf(err, \"error unmarshaling ACME EAB Key IDs for provisioner %s\", provisionerID)\n\t\t}\n\t}\n\n\tkeys := []*acme.ExternalAccountKey{}\n\tfor _, eakID := range eakIDs {\n\t\tif eakID == \"\" {\n\t\t\tcontinue // shouldn't happen; just in case\n\t\t}\n\t\teak, err := db.getDBExternalAccountKey(ctx, eakID)\n\t\tif err != nil {\n\t\t\tif !nosqlDB.IsErrNotFound(err) {\n\t\t\t\treturn nil, \"\", errors.Wrapf(err, \"error retrieving ACME EAB Key for provisioner %s and keyID %s\", provisionerID, eakID)\n\t\t\t}\n\t\t}\n\t\tkeys = append(keys, &acme.ExternalAccountKey{\n\t\t\tID:            eak.ID,\n\t\t\tHmacKey:       eak.HmacKey,\n\t\t\tProvisionerID: eak.ProvisionerID,\n\t\t\tReference:     eak.Reference,\n\t\t\tAccountID:     eak.AccountID,\n\t\t\tCreatedAt:     eak.CreatedAt,\n\t\t\tBoundAt:       eak.BoundAt,\n\t\t})\n\t}\n\n\treturn keys, \"\", nil\n}\n\n// GetExternalAccountKeyByReference retrieves an External Account Binding key with unique reference\nfunc (db *DB) GetExternalAccountKeyByReference(ctx context.Context, provisionerID, reference string) (*acme.ExternalAccountKey, error) {\n\texternalAccountKeyMutex.RLock()\n\tdefer externalAccountKeyMutex.RUnlock()\n\n\tif reference == \"\" {\n\t\t//nolint:nilnil // legacy\n\t\treturn nil, nil\n\t}\n\n\tk, err := db.db.Get(externalAccountKeyIDsByReferenceTable, []byte(referenceKey(provisionerID, reference)))\n\tif nosqlDB.IsErrNotFound(err) {\n\t\treturn nil, acme.ErrNotFound\n\t} else if err != nil {\n\t\treturn nil, errors.Wrapf(err, \"error loading ACME EAB key for reference %s\", reference)\n\t}\n\tdbExternalAccountKeyReference := new(dbExternalAccountKeyReference)\n\tif err := json.Unmarshal(k, dbExternalAccountKeyReference); err != nil {\n\t\treturn nil, errors.Wrapf(err, \"error unmarshaling ACME EAB key for reference %s\", reference)\n\t}\n\n\treturn db.GetExternalAccountKey(ctx, provisionerID, dbExternalAccountKeyReference.ExternalAccountKeyID)\n}\n\nfunc (db *DB) GetExternalAccountKeyByAccountID(context.Context, string, string) (*acme.ExternalAccountKey, error) {\n\t//nolint:nilnil // legacy\n\treturn nil, nil\n}\n\nfunc (db *DB) UpdateExternalAccountKey(ctx context.Context, provisionerID string, eak *acme.ExternalAccountKey) error {\n\texternalAccountKeyMutex.Lock()\n\tdefer externalAccountKeyMutex.Unlock()\n\n\told, err := db.getDBExternalAccountKey(ctx, eak.ID)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif old.ProvisionerID != provisionerID {\n\t\treturn errors.New(\"provisioner does not match provisioner for which the EAB key was created\")\n\t}\n\n\tif old.ProvisionerID != eak.ProvisionerID {\n\t\treturn errors.New(\"cannot change provisioner for an existing ACME EAB Key\")\n\t}\n\n\tif old.Reference != eak.Reference {\n\t\treturn errors.New(\"cannot change reference for an existing ACME EAB Key\")\n\t}\n\n\tnu := dbExternalAccountKey{\n\t\tID:            eak.ID,\n\t\tProvisionerID: eak.ProvisionerID,\n\t\tReference:     eak.Reference,\n\t\tAccountID:     eak.AccountID,\n\t\tHmacKey:       eak.HmacKey,\n\t\tCreatedAt:     eak.CreatedAt,\n\t\tBoundAt:       eak.BoundAt,\n\t}\n\n\treturn db.save(ctx, nu.ID, nu, old, \"external_account_key\", externalAccountKeyTable)\n}\n\nfunc (db *DB) addEAKID(ctx context.Context, provisionerID, eakID string) error {\n\treferencesByProvisionerIndexMutex.Lock()\n\tdefer referencesByProvisionerIndexMutex.Unlock()\n\n\tif eakID == \"\" {\n\t\treturn errors.Errorf(\"can't add empty eakID for provisioner %s\", provisionerID)\n\t}\n\n\tvar eakIDs []string\n\tb, err := db.db.Get(externalAccountKeyIDsByProvisionerIDTable, []byte(provisionerID))\n\tif err != nil {\n\t\tif !nosqlDB.IsErrNotFound(err) {\n\t\t\treturn errors.Wrapf(err, \"error loading eakIDs for provisioner %s\", provisionerID)\n\t\t}\n\t\t// it may happen that no record is found; we'll continue with an empty slice\n\t} else {\n\t\tif err := json.Unmarshal(b, &eakIDs); err != nil {\n\t\t\treturn errors.Wrapf(err, \"error unmarshaling eakIDs for provisioner %s\", provisionerID)\n\t\t}\n\t}\n\n\tfor _, id := range eakIDs {\n\t\tif id == eakID {\n\t\t\t// return an error when a duplicate ID is found\n\t\t\treturn errors.Errorf(\"eakID %s already exists for provisioner %s\", eakID, provisionerID)\n\t\t}\n\t}\n\n\tvar newEAKIDs []string\n\tnewEAKIDs = append(newEAKIDs, eakIDs...)\n\tnewEAKIDs = append(newEAKIDs, eakID)\n\n\tvar (\n\t\t_old interface{} = eakIDs\n\t\t_new interface{} = newEAKIDs\n\t)\n\n\t// ensure that the DB gets the expected value when the slice is empty; otherwise\n\t// it'll return with an error that indicates that the DBs view of the data is\n\t// different from the last read (i.e. _old is different from what the DB has).\n\tif len(eakIDs) == 0 {\n\t\t_old = nil\n\t}\n\n\tif err = db.save(ctx, provisionerID, _new, _old, \"externalAccountKeyIDsByProvisionerID\", externalAccountKeyIDsByProvisionerIDTable); err != nil {\n\t\treturn errors.Wrapf(err, \"error saving eakIDs index for provisioner %s\", provisionerID)\n\t}\n\n\treturn nil\n}\n\nfunc (db *DB) deleteEAKID(ctx context.Context, provisionerID, eakID string) error {\n\treferencesByProvisionerIndexMutex.Lock()\n\tdefer referencesByProvisionerIndexMutex.Unlock()\n\n\tvar eakIDs []string\n\tb, err := db.db.Get(externalAccountKeyIDsByProvisionerIDTable, []byte(provisionerID))\n\tif err != nil {\n\t\tif !nosqlDB.IsErrNotFound(err) {\n\t\t\treturn errors.Wrapf(err, \"error loading eakIDs for provisioner %s\", provisionerID)\n\t\t}\n\t\t// it may happen that no record is found; we'll continue with an empty slice\n\t} else {\n\t\tif err := json.Unmarshal(b, &eakIDs); err != nil {\n\t\t\treturn errors.Wrapf(err, \"error unmarshaling eakIDs for provisioner %s\", provisionerID)\n\t\t}\n\t}\n\n\tnewEAKIDs := removeElement(eakIDs, eakID)\n\tvar (\n\t\t_old interface{} = eakIDs\n\t\t_new interface{} = newEAKIDs\n\t)\n\n\t// ensure that the DB gets the expected value when the slice is empty; otherwise\n\t// it'll return with an error that indicates that the DBs view of the data is\n\t// different from the last read (i.e. _old is different from what the DB has).\n\tif len(eakIDs) == 0 {\n\t\t_old = nil\n\t}\n\n\tif err = db.save(ctx, provisionerID, _new, _old, \"externalAccountKeyIDsByProvisionerID\", externalAccountKeyIDsByProvisionerIDTable); err != nil {\n\t\treturn errors.Wrapf(err, \"error saving eakIDs index for provisioner %s\", provisionerID)\n\t}\n\n\treturn nil\n}\n\n// referenceKey returns a unique key for a reference per provisioner\nfunc referenceKey(provisionerID, reference string) string {\n\treturn provisionerID + \".\" + reference\n}\n\n// sliceIndex finds the index of item in slice\nfunc sliceIndex(slice []string, item string) int {\n\tfor i := range slice {\n\t\tif slice[i] == item {\n\t\t\treturn i\n\t\t}\n\t}\n\treturn -1\n}\n\n// removeElement deletes the item if it exists in the\n// slice. It returns a new slice, keeping the old one intact.\nfunc removeElement(slice []string, item string) []string {\n\tnewSlice := make([]string, 0)\n\tindex := sliceIndex(slice, item)\n\tif index < 0 {\n\t\tnewSlice = append(newSlice, slice...)\n\t\treturn newSlice\n\t}\n\n\tnewSlice = append(newSlice, slice[:index]...)\n\n\treturn append(newSlice, slice[index+1:]...)\n}\n"
  },
  {
    "path": "acme/db/nosql/eab_test.go",
    "content": "package nosql\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/pkg/errors\"\n\n\t\"github.com/smallstep/assert\"\n\t\"github.com/smallstep/certificates/acme\"\n\tcertdb \"github.com/smallstep/certificates/db\"\n\t\"github.com/smallstep/nosql\"\n\tnosqldb \"github.com/smallstep/nosql/database\"\n)\n\nfunc TestDB_getDBExternalAccountKey(t *testing.T) {\n\tkeyID := \"keyID\"\n\tprovID := \"provID\"\n\ttype test struct {\n\t\tdb      nosql.DB\n\t\terr     error\n\t\tacmeErr *acme.Error\n\t\tdbeak   *dbExternalAccountKey\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tnow := clock.Now()\n\t\t\tdbeak := &dbExternalAccountKey{\n\t\t\t\tID:            keyID,\n\t\t\t\tProvisionerID: provID,\n\t\t\t\tReference:     \"ref\",\n\t\t\t\tAccountID:     \"\",\n\t\t\t\tHmacKey:       []byte{1, 3, 3, 7},\n\t\t\t\tCreatedAt:     now,\n\t\t\t}\n\t\t\tb, err := json.Marshal(dbeak)\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tdb: &certdb.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, externalAccountKeyTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), keyID)\n\t\t\t\t\t\treturn b, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr:   nil,\n\t\t\t\tdbeak: dbeak,\n\t\t\t}\n\t\t},\n\t\t\"fail/not-found\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tdb: &certdb.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, externalAccountKeyTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), keyID)\n\t\t\t\t\t\treturn nil, nosqldb.ErrNotFound\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: acme.ErrNotFound,\n\t\t\t}\n\t\t},\n\t\t\"fail/db.Get-error\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tdb: &certdb.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, externalAccountKeyTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), keyID)\n\t\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"error loading external account key keyID: force\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/unmarshal-error\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tdb: &certdb.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, externalAccountKeyTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), keyID)\n\n\t\t\t\t\t\treturn []byte(\"foo\"), nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"error unmarshaling external account key keyID into dbExternalAccountKey\"),\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\ttc := run(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\td := DB{db: tc.db}\n\t\t\tif dbeak, err := d.getDBExternalAccountKey(context.Background(), keyID); err != nil {\n\t\t\t\tvar ae *acme.Error\n\t\t\t\tif errors.As(err, &ae) {\n\t\t\t\t\tif assert.NotNil(t, tc.acmeErr) {\n\t\t\t\t\t\tassert.Equals(t, ae.Type, tc.acmeErr.Type)\n\t\t\t\t\t\tassert.Equals(t, ae.Detail, tc.acmeErr.Detail)\n\t\t\t\t\t\tassert.Equals(t, ae.Status, tc.acmeErr.Status)\n\t\t\t\t\t\tassert.Equals(t, ae.Err.Error(), tc.acmeErr.Err.Error())\n\t\t\t\t\t\tassert.Equals(t, ae.Detail, tc.acmeErr.Detail)\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.err.Error())\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else if assert.Nil(t, tc.err) {\n\t\t\t\tassert.Equals(t, dbeak.ID, tc.dbeak.ID)\n\t\t\t\tassert.Equals(t, dbeak.HmacKey, tc.dbeak.HmacKey)\n\t\t\t\tassert.Equals(t, dbeak.ProvisionerID, tc.dbeak.ProvisionerID)\n\t\t\t\tassert.Equals(t, dbeak.Reference, tc.dbeak.Reference)\n\t\t\t\tassert.Equals(t, dbeak.CreatedAt, tc.dbeak.CreatedAt)\n\t\t\t\tassert.Equals(t, dbeak.AccountID, tc.dbeak.AccountID)\n\t\t\t\tassert.Equals(t, dbeak.BoundAt, tc.dbeak.BoundAt)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDB_GetExternalAccountKey(t *testing.T) {\n\tkeyID := \"keyID\"\n\tprovID := \"provID\"\n\ttype test struct {\n\t\tdb      nosql.DB\n\t\terr     error\n\t\tacmeErr *acme.Error\n\t\teak     *acme.ExternalAccountKey\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tnow := clock.Now()\n\t\t\tdbeak := &dbExternalAccountKey{\n\t\t\t\tID:            keyID,\n\t\t\t\tProvisionerID: provID,\n\t\t\t\tReference:     \"ref\",\n\t\t\t\tAccountID:     \"\",\n\t\t\t\tHmacKey:       []byte{1, 3, 3, 7},\n\t\t\t\tCreatedAt:     now,\n\t\t\t}\n\t\t\tb, err := json.Marshal(dbeak)\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tdb: &certdb.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, externalAccountKeyTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), keyID)\n\t\t\t\t\t\treturn b, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\teak: &acme.ExternalAccountKey{\n\t\t\t\t\tID:            keyID,\n\t\t\t\t\tProvisionerID: provID,\n\t\t\t\t\tReference:     \"ref\",\n\t\t\t\t\tAccountID:     \"\",\n\t\t\t\t\tHmacKey:       []byte{1, 3, 3, 7},\n\t\t\t\t\tCreatedAt:     now,\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"fail/db.Get-error\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tdb: &certdb.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, externalAccountKeyTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), keyID)\n\n\t\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"error loading external account key keyID: force\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/non-matching-provisioner\": func(t *testing.T) test {\n\t\t\tnow := clock.Now()\n\t\t\tdbeak := &dbExternalAccountKey{\n\t\t\t\tID:            keyID,\n\t\t\t\tProvisionerID: \"aDifferentProvID\",\n\t\t\t\tReference:     \"ref\",\n\t\t\t\tAccountID:     \"\",\n\t\t\t\tHmacKey:       []byte{1, 3, 3, 7},\n\t\t\t\tCreatedAt:     now,\n\t\t\t}\n\t\t\tb, err := json.Marshal(dbeak)\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tdb: &certdb.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, externalAccountKeyTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), keyID)\n\t\t\t\t\t\treturn b, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\teak: &acme.ExternalAccountKey{\n\t\t\t\t\tID:            keyID,\n\t\t\t\t\tProvisionerID: provID,\n\t\t\t\t\tReference:     \"ref\",\n\t\t\t\t\tAccountID:     \"\",\n\t\t\t\t\tHmacKey:       []byte{1, 3, 3, 7},\n\t\t\t\t\tCreatedAt:     now,\n\t\t\t\t},\n\t\t\t\tacmeErr: acme.NewError(acme.ErrorUnauthorizedType, \"provisioner does not match provisioner for which the EAB key was created\"),\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\ttc := run(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\td := DB{db: tc.db}\n\t\t\tif eak, err := d.GetExternalAccountKey(context.Background(), provID, keyID); err != nil {\n\t\t\t\tvar ae *acme.Error\n\t\t\t\tif errors.As(err, &ae) {\n\t\t\t\t\tif assert.NotNil(t, tc.acmeErr) {\n\t\t\t\t\t\tassert.Equals(t, ae.Type, tc.acmeErr.Type)\n\t\t\t\t\t\tassert.Equals(t, ae.Detail, tc.acmeErr.Detail)\n\t\t\t\t\t\tassert.Equals(t, ae.Status, tc.acmeErr.Status)\n\t\t\t\t\t\tassert.Equals(t, ae.Err.Error(), tc.acmeErr.Err.Error())\n\t\t\t\t\t\tassert.Equals(t, ae.Detail, tc.acmeErr.Detail)\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.err.Error())\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else if assert.Nil(t, tc.err) {\n\t\t\t\tassert.Equals(t, eak.ID, tc.eak.ID)\n\t\t\t\tassert.Equals(t, eak.HmacKey, tc.eak.HmacKey)\n\t\t\t\tassert.Equals(t, eak.ProvisionerID, tc.eak.ProvisionerID)\n\t\t\t\tassert.Equals(t, eak.Reference, tc.eak.Reference)\n\t\t\t\tassert.Equals(t, eak.CreatedAt, tc.eak.CreatedAt)\n\t\t\t\tassert.Equals(t, eak.AccountID, tc.eak.AccountID)\n\t\t\t\tassert.Equals(t, eak.BoundAt, tc.eak.BoundAt)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDB_GetExternalAccountKeyByReference(t *testing.T) {\n\tkeyID := \"keyID\"\n\tprovID := \"provID\"\n\tref := \"ref\"\n\ttype test struct {\n\t\tdb      nosql.DB\n\t\terr     error\n\t\tref     string\n\t\tacmeErr *acme.Error\n\t\teak     *acme.ExternalAccountKey\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tnow := clock.Now()\n\t\t\tdbeak := &dbExternalAccountKey{\n\t\t\t\tID:            keyID,\n\t\t\t\tProvisionerID: provID,\n\t\t\t\tReference:     ref,\n\t\t\t\tAccountID:     \"\",\n\t\t\t\tHmacKey:       []byte{1, 3, 3, 7},\n\t\t\t\tCreatedAt:     now,\n\t\t\t}\n\t\t\tdbref := &dbExternalAccountKeyReference{\n\t\t\t\tReference:            ref,\n\t\t\t\tExternalAccountKeyID: keyID,\n\t\t\t}\n\t\t\tb, err := json.Marshal(dbeak)\n\t\t\tassert.FatalError(t, err)\n\t\t\tdbrefBytes, err := json.Marshal(dbref)\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tref: ref,\n\t\t\t\tdb: &certdb.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tswitch string(bucket) {\n\t\t\t\t\t\tcase string(externalAccountKeyIDsByReferenceTable):\n\t\t\t\t\t\t\tassert.Equals(t, string(key), provID+\".\"+ref)\n\t\t\t\t\t\t\treturn dbrefBytes, nil\n\t\t\t\t\t\tcase string(externalAccountKeyTable):\n\t\t\t\t\t\t\tassert.Equals(t, string(key), keyID)\n\t\t\t\t\t\t\treturn b, nil\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\tassert.FatalError(t, errors.Errorf(\"unexpected bucket %s\", string(bucket)))\n\t\t\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\teak: &acme.ExternalAccountKey{\n\t\t\t\t\tID:            keyID,\n\t\t\t\t\tProvisionerID: provID,\n\t\t\t\t\tReference:     ref,\n\t\t\t\t\tAccountID:     \"\",\n\t\t\t\t\tHmacKey:       []byte{1, 3, 3, 7},\n\t\t\t\t\tCreatedAt:     now,\n\t\t\t\t},\n\t\t\t\terr: nil,\n\t\t\t}\n\t\t},\n\t\t\"ok/no-reference\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tref: \"\",\n\t\t\t\teak: nil,\n\t\t\t\terr: nil,\n\t\t\t}\n\t\t},\n\t\t\"fail/reference-not-found\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tref: ref,\n\t\t\t\tdb: &certdb.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, string(bucket), string(externalAccountKeyIDsByReferenceTable))\n\t\t\t\t\t\tassert.Equals(t, string(key), provID+\".\"+ref)\n\t\t\t\t\t\treturn nil, nosqldb.ErrNotFound\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"not found\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/reference-load-error\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tref: ref,\n\t\t\t\tdb: &certdb.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, string(bucket), string(externalAccountKeyIDsByReferenceTable))\n\t\t\t\t\t\tassert.Equals(t, string(key), provID+\".\"+ref)\n\t\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"error loading ACME EAB key for reference ref: force\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/reference-unmarshal-error\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tref: ref,\n\t\t\t\tdb: &certdb.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, string(bucket), string(externalAccountKeyIDsByReferenceTable))\n\t\t\t\t\t\tassert.Equals(t, string(key), provID+\".\"+ref)\n\t\t\t\t\t\treturn []byte{0}, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"error unmarshaling ACME EAB key for reference ref\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/db.GetExternalAccountKey-error\": func(t *testing.T) test {\n\t\t\tdbref := &dbExternalAccountKeyReference{\n\t\t\t\tReference:            ref,\n\t\t\t\tExternalAccountKeyID: keyID,\n\t\t\t}\n\t\t\tdbrefBytes, err := json.Marshal(dbref)\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tref: ref,\n\t\t\t\tdb: &certdb.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tswitch string(bucket) {\n\t\t\t\t\t\tcase string(externalAccountKeyIDsByReferenceTable):\n\t\t\t\t\t\t\tassert.Equals(t, string(key), provID+\".\"+ref)\n\t\t\t\t\t\t\treturn dbrefBytes, nil\n\t\t\t\t\t\tcase string(externalAccountKeyTable):\n\t\t\t\t\t\t\tassert.Equals(t, string(key), keyID)\n\t\t\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\tassert.FatalError(t, errors.Errorf(\"unexpected bucket %s\", string(bucket)))\n\t\t\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"error loading external account key keyID: force\"),\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\ttc := run(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\td := DB{db: tc.db}\n\t\t\tif eak, err := d.GetExternalAccountKeyByReference(context.Background(), provID, tc.ref); err != nil {\n\t\t\t\tvar ae *acme.Error\n\t\t\t\tif errors.As(err, &ae) {\n\t\t\t\t\tif assert.NotNil(t, tc.acmeErr) {\n\t\t\t\t\t\tassert.Equals(t, ae.Type, tc.acmeErr.Type)\n\t\t\t\t\t\tassert.Equals(t, ae.Detail, tc.acmeErr.Detail)\n\t\t\t\t\t\tassert.Equals(t, ae.Status, tc.acmeErr.Status)\n\t\t\t\t\t\tassert.Equals(t, ae.Err.Error(), tc.acmeErr.Err.Error())\n\t\t\t\t\t\tassert.Equals(t, ae.Detail, tc.acmeErr.Detail)\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.err.Error())\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else if assert.Nil(t, tc.err) && tc.eak != nil {\n\t\t\t\tassert.Equals(t, eak.ID, tc.eak.ID)\n\t\t\t\tassert.Equals(t, eak.AccountID, tc.eak.AccountID)\n\t\t\t\tassert.Equals(t, eak.BoundAt, tc.eak.BoundAt)\n\t\t\t\tassert.Equals(t, eak.CreatedAt, tc.eak.CreatedAt)\n\t\t\t\tassert.Equals(t, eak.HmacKey, tc.eak.HmacKey)\n\t\t\t\tassert.Equals(t, eak.ProvisionerID, tc.eak.ProvisionerID)\n\t\t\t\tassert.Equals(t, eak.Reference, tc.eak.Reference)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDB_GetExternalAccountKeys(t *testing.T) {\n\tkeyID1 := \"keyID1\"\n\tkeyID2 := \"keyID2\"\n\tkeyID3 := \"keyID3\"\n\tprovID := \"provID\"\n\tref := \"ref\"\n\ttype test struct {\n\t\tdb      nosql.DB\n\t\terr     error\n\t\tacmeErr *acme.Error\n\t\teaks    []*acme.ExternalAccountKey\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tnow := clock.Now()\n\t\t\tdbeak1 := &dbExternalAccountKey{\n\t\t\t\tID:            keyID1,\n\t\t\t\tProvisionerID: provID,\n\t\t\t\tReference:     ref,\n\t\t\t\tAccountID:     \"\",\n\t\t\t\tHmacKey:       []byte{1, 3, 3, 7},\n\t\t\t\tCreatedAt:     now,\n\t\t\t}\n\t\t\tb1, err := json.Marshal(dbeak1)\n\t\t\tassert.FatalError(t, err)\n\t\t\tdbeak2 := &dbExternalAccountKey{\n\t\t\t\tID:            keyID2,\n\t\t\t\tProvisionerID: provID,\n\t\t\t\tReference:     ref,\n\t\t\t\tAccountID:     \"\",\n\t\t\t\tHmacKey:       []byte{1, 3, 3, 7},\n\t\t\t\tCreatedAt:     now,\n\t\t\t}\n\t\t\tb2, err := json.Marshal(dbeak2)\n\t\t\tassert.FatalError(t, err)\n\t\t\tdbeak3 := &dbExternalAccountKey{\n\t\t\t\tID:            keyID3,\n\t\t\t\tProvisionerID: \"aDifferentProvID\",\n\t\t\t\tReference:     ref,\n\t\t\t\tAccountID:     \"\",\n\t\t\t\tHmacKey:       []byte{1, 3, 3, 7},\n\t\t\t\tCreatedAt:     now,\n\t\t\t}\n\t\t\tb3, err := json.Marshal(dbeak3)\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tdb: &certdb.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tswitch string(bucket) {\n\t\t\t\t\t\tcase string(externalAccountKeyIDsByProvisionerIDTable):\n\t\t\t\t\t\t\tkeys := []string{\"\", keyID1, keyID2} // includes an empty keyID\n\t\t\t\t\t\t\tb, err := json.Marshal(keys)\n\t\t\t\t\t\t\tassert.FatalError(t, err)\n\t\t\t\t\t\t\treturn b, nil\n\t\t\t\t\t\tcase string(externalAccountKeyTable):\n\t\t\t\t\t\t\tswitch string(key) {\n\t\t\t\t\t\t\tcase keyID1:\n\t\t\t\t\t\t\t\treturn b1, nil\n\t\t\t\t\t\t\tcase keyID2:\n\t\t\t\t\t\t\t\treturn b2, nil\n\t\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\t\tassert.FatalError(t, errors.Errorf(\"unexpected key %s\", string(key)))\n\t\t\t\t\t\t\t\treturn nil, errors.New(\"force default\")\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\tassert.FatalError(t, errors.Errorf(\"unexpected bucket %s\", string(bucket)))\n\t\t\t\t\t\t\treturn nil, errors.New(\"force default\")\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t// TODO: remove the MList\n\t\t\t\t\tMList: func(bucket []byte) ([]*nosqldb.Entry, error) {\n\t\t\t\t\t\tswitch string(bucket) {\n\t\t\t\t\t\tcase string(externalAccountKeyTable):\n\t\t\t\t\t\t\treturn []*nosqldb.Entry{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tBucket: bucket,\n\t\t\t\t\t\t\t\t\tKey:    []byte(keyID1),\n\t\t\t\t\t\t\t\t\tValue:  b1,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tBucket: bucket,\n\t\t\t\t\t\t\t\t\tKey:    []byte(keyID2),\n\t\t\t\t\t\t\t\t\tValue:  b2,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tBucket: bucket,\n\t\t\t\t\t\t\t\t\tKey:    []byte(keyID3),\n\t\t\t\t\t\t\t\t\tValue:  b3,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t}, nil\n\t\t\t\t\t\tcase string(externalAccountKeyIDsByProvisionerIDTable):\n\t\t\t\t\t\t\tkeys := []string{keyID1, keyID2}\n\t\t\t\t\t\t\tb, err := json.Marshal(keys)\n\t\t\t\t\t\t\tassert.FatalError(t, err)\n\t\t\t\t\t\t\treturn []*nosqldb.Entry{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tBucket: bucket,\n\t\t\t\t\t\t\t\t\tKey:    []byte(provID),\n\t\t\t\t\t\t\t\t\tValue:  b,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t}, nil\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\tassert.FatalError(t, errors.Errorf(\"unexpected bucket %s\", string(bucket)))\n\t\t\t\t\t\t\treturn nil, errors.New(\"force default\")\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\teaks: []*acme.ExternalAccountKey{\n\t\t\t\t\t{\n\t\t\t\t\t\tID:            keyID1,\n\t\t\t\t\t\tProvisionerID: provID,\n\t\t\t\t\t\tReference:     ref,\n\t\t\t\t\t\tAccountID:     \"\",\n\t\t\t\t\t\tHmacKey:       []byte{1, 3, 3, 7},\n\t\t\t\t\t\tCreatedAt:     now,\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tID:            keyID2,\n\t\t\t\t\t\tProvisionerID: provID,\n\t\t\t\t\t\tReference:     ref,\n\t\t\t\t\t\tAccountID:     \"\",\n\t\t\t\t\t\tHmacKey:       []byte{1, 3, 3, 7},\n\t\t\t\t\t\tCreatedAt:     now,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"fail/db.Get-externalAccountKeysByProvisionerIDTable\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tdb: &certdb.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, string(bucket), string(externalAccountKeyIDsByProvisionerIDTable))\n\t\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"error loading ACME EAB Key IDs for provisioner provID: force\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/db.Get-externalAccountKeysByProvisionerIDTable-unmarshal\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tdb: &certdb.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, string(bucket), string(externalAccountKeyIDsByProvisionerIDTable))\n\t\t\t\t\t\tb, _ := json.Marshal(1)\n\t\t\t\t\t\treturn b, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"error unmarshaling ACME EAB Key IDs for provisioner provID: json: cannot unmarshal number into Go value of type []string\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/db.getDBExternalAccountKey\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tdb: &certdb.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tswitch string(bucket) {\n\t\t\t\t\t\tcase string(externalAccountKeyIDsByProvisionerIDTable):\n\t\t\t\t\t\t\tkeys := []string{keyID1, keyID2}\n\t\t\t\t\t\t\tb, err := json.Marshal(keys)\n\t\t\t\t\t\t\tassert.FatalError(t, err)\n\t\t\t\t\t\t\treturn b, nil\n\t\t\t\t\t\tcase string(externalAccountKeyTable):\n\t\t\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\tassert.FatalError(t, errors.Errorf(\"unexpected bucket %s\", string(bucket)))\n\t\t\t\t\t\t\treturn nil, errors.New(\"force bucket\")\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"error retrieving ACME EAB Key for provisioner provID and keyID keyID1: error loading external account key keyID1: force\"),\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\ttc := run(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\td := DB{db: tc.db}\n\t\t\tcursor, limit := \"\", 0\n\t\t\tif eaks, nextCursor, err := d.GetExternalAccountKeys(context.Background(), provID, cursor, limit); err != nil {\n\t\t\t\tassert.Equals(t, \"\", nextCursor)\n\t\t\t\tvar ae *acme.Error\n\t\t\t\tif errors.As(err, &ae) {\n\t\t\t\t\tif assert.NotNil(t, tc.acmeErr) {\n\t\t\t\t\t\tassert.Equals(t, ae.Type, tc.acmeErr.Type)\n\t\t\t\t\t\tassert.Equals(t, ae.Detail, tc.acmeErr.Detail)\n\t\t\t\t\t\tassert.Equals(t, ae.Status, tc.acmeErr.Status)\n\t\t\t\t\t\tassert.Equals(t, ae.Err.Error(), tc.acmeErr.Err.Error())\n\t\t\t\t\t\tassert.Equals(t, ae.Detail, tc.acmeErr.Detail)\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\t\tassert.Equals(t, tc.err.Error(), err.Error())\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else if assert.Nil(t, tc.err) {\n\t\t\t\tassert.Equals(t, len(eaks), len(tc.eaks))\n\t\t\t\tassert.Equals(t, \"\", nextCursor)\n\t\t\t\tfor i, eak := range eaks {\n\t\t\t\t\tassert.Equals(t, eak.ID, tc.eaks[i].ID)\n\t\t\t\t\tassert.Equals(t, eak.HmacKey, tc.eaks[i].HmacKey)\n\t\t\t\t\tassert.Equals(t, eak.ProvisionerID, tc.eaks[i].ProvisionerID)\n\t\t\t\t\tassert.Equals(t, eak.Reference, tc.eaks[i].Reference)\n\t\t\t\t\tassert.Equals(t, eak.CreatedAt, tc.eaks[i].CreatedAt)\n\t\t\t\t\tassert.Equals(t, eak.AccountID, tc.eaks[i].AccountID)\n\t\t\t\t\tassert.Equals(t, eak.BoundAt, tc.eaks[i].BoundAt)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDB_DeleteExternalAccountKey(t *testing.T) {\n\tkeyID := \"keyID\"\n\tprovID := \"provID\"\n\tref := \"ref\"\n\ttype test struct {\n\t\tdb      nosql.DB\n\t\terr     error\n\t\tacmeErr *acme.Error\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tnow := clock.Now()\n\t\t\tdbeak := &dbExternalAccountKey{\n\t\t\t\tID:            keyID,\n\t\t\t\tProvisionerID: provID,\n\t\t\t\tReference:     ref,\n\t\t\t\tAccountID:     \"\",\n\t\t\t\tHmacKey:       []byte{1, 3, 3, 7},\n\t\t\t\tCreatedAt:     now,\n\t\t\t}\n\t\t\tdbref := &dbExternalAccountKeyReference{\n\t\t\t\tReference:            ref,\n\t\t\t\tExternalAccountKeyID: keyID,\n\t\t\t}\n\t\t\tb, err := json.Marshal(dbeak)\n\t\t\tassert.FatalError(t, err)\n\t\t\tdbrefBytes, err := json.Marshal(dbref)\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tdb: &certdb.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tswitch string(bucket) {\n\t\t\t\t\t\tcase string(externalAccountKeyIDsByReferenceTable):\n\t\t\t\t\t\t\tassert.Equals(t, string(key), provID+\".\"+ref)\n\t\t\t\t\t\t\treturn dbrefBytes, nil\n\t\t\t\t\t\tcase string(externalAccountKeyTable):\n\t\t\t\t\t\t\tassert.Equals(t, string(key), keyID)\n\t\t\t\t\t\t\treturn b, nil\n\t\t\t\t\t\tcase string(externalAccountKeyIDsByProvisionerIDTable):\n\t\t\t\t\t\t\tassert.Equals(t, provID, string(key))\n\t\t\t\t\t\t\tb, err := json.Marshal([]string{keyID})\n\t\t\t\t\t\t\tassert.FatalError(t, err)\n\t\t\t\t\t\t\treturn b, nil\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\tassert.FatalError(t, errors.Errorf(\"unexpected bucket %s\", string(bucket)))\n\t\t\t\t\t\t\treturn nil, errors.New(\"force default\")\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\tMDel: func(bucket, key []byte) error {\n\t\t\t\t\t\tswitch string(bucket) {\n\t\t\t\t\t\tcase string(externalAccountKeyIDsByReferenceTable):\n\t\t\t\t\t\t\tassert.Equals(t, string(key), provID+\".\"+ref)\n\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\tcase string(externalAccountKeyTable):\n\t\t\t\t\t\t\tassert.Equals(t, string(key), keyID)\n\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\tassert.FatalError(t, errors.Errorf(\"unexpected bucket %s\", string(bucket)))\n\t\t\t\t\t\t\treturn errors.New(\"force default\")\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\tMCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {\n\t\t\t\t\t\tfmt.Println(string(bucket))\n\t\t\t\t\t\tswitch string(bucket) {\n\t\t\t\t\t\tcase string(externalAccountKeyIDsByReferenceTable):\n\t\t\t\t\t\t\tassert.Equals(t, provID+\".\"+ref, string(key))\n\t\t\t\t\t\t\treturn nil, true, nil\n\t\t\t\t\t\tcase string(externalAccountKeyIDsByProvisionerIDTable):\n\t\t\t\t\t\t\tassert.Equals(t, provID, string(key))\n\t\t\t\t\t\t\treturn nil, true, nil\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\tassert.FatalError(t, errors.Errorf(\"unexpected bucket %s\", string(bucket)))\n\t\t\t\t\t\t\treturn nil, false, errors.New(\"force default\")\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"fail/not-found\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tdb: &certdb.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, string(bucket), string(externalAccountKeyTable))\n\t\t\t\t\t\tassert.Equals(t, string(key), keyID)\n\t\t\t\t\t\treturn nil, nosqldb.ErrNotFound\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"error loading ACME EAB Key with Key ID keyID: not found\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/non-matching-provisioner\": func(t *testing.T) test {\n\t\t\tnow := clock.Now()\n\t\t\tdbeak := &dbExternalAccountKey{\n\t\t\t\tID:            keyID,\n\t\t\t\tProvisionerID: \"aDifferentProvID\",\n\t\t\t\tReference:     ref,\n\t\t\t\tAccountID:     \"\",\n\t\t\t\tHmacKey:       []byte{1, 3, 3, 7},\n\t\t\t\tCreatedAt:     now,\n\t\t\t}\n\t\t\tb, err := json.Marshal(dbeak)\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tdb: &certdb.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, string(bucket), string(externalAccountKeyTable))\n\t\t\t\t\t\tassert.Equals(t, string(key), keyID)\n\t\t\t\t\t\treturn b, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"provisioner does not match provisioner for which the EAB key was created\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/delete-reference\": func(t *testing.T) test {\n\t\t\tnow := clock.Now()\n\t\t\tdbeak := &dbExternalAccountKey{\n\t\t\t\tID:            keyID,\n\t\t\t\tProvisionerID: provID,\n\t\t\t\tReference:     ref,\n\t\t\t\tAccountID:     \"\",\n\t\t\t\tHmacKey:       []byte{1, 3, 3, 7},\n\t\t\t\tCreatedAt:     now,\n\t\t\t}\n\t\t\tdbref := &dbExternalAccountKeyReference{\n\t\t\t\tReference:            ref,\n\t\t\t\tExternalAccountKeyID: keyID,\n\t\t\t}\n\t\t\tb, err := json.Marshal(dbeak)\n\t\t\tassert.FatalError(t, err)\n\t\t\tdbrefBytes, err := json.Marshal(dbref)\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tdb: &certdb.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tswitch string(bucket) {\n\t\t\t\t\t\tcase string(externalAccountKeyIDsByReferenceTable):\n\t\t\t\t\t\t\tassert.Equals(t, string(key), ref)\n\t\t\t\t\t\t\treturn dbrefBytes, nil\n\t\t\t\t\t\tcase string(externalAccountKeyTable):\n\t\t\t\t\t\t\tassert.Equals(t, string(key), keyID)\n\t\t\t\t\t\t\treturn b, nil\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\tassert.FatalError(t, errors.Errorf(\"unexpected bucket %s\", string(bucket)))\n\t\t\t\t\t\t\treturn nil, errors.New(\"force default\")\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\tMDel: func(bucket, key []byte) error {\n\t\t\t\t\t\tswitch string(bucket) {\n\t\t\t\t\t\tcase string(externalAccountKeyIDsByReferenceTable):\n\t\t\t\t\t\t\tassert.Equals(t, string(key), provID+\".\"+ref)\n\t\t\t\t\t\t\treturn errors.New(\"force\")\n\t\t\t\t\t\tcase string(externalAccountKeyTable):\n\t\t\t\t\t\t\tassert.Equals(t, string(key), keyID)\n\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\tassert.FatalError(t, errors.Errorf(\"unexpected bucket %s\", string(bucket)))\n\t\t\t\t\t\t\treturn errors.New(\"force default\")\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"error deleting ACME EAB Key reference with Key ID keyID and reference ref: force\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/delete-eak\": func(t *testing.T) test {\n\t\t\tnow := clock.Now()\n\t\t\tdbeak := &dbExternalAccountKey{\n\t\t\t\tID:            keyID,\n\t\t\t\tProvisionerID: provID,\n\t\t\t\tReference:     ref,\n\t\t\t\tAccountID:     \"\",\n\t\t\t\tHmacKey:       []byte{1, 3, 3, 7},\n\t\t\t\tCreatedAt:     now,\n\t\t\t}\n\t\t\tdbref := &dbExternalAccountKeyReference{\n\t\t\t\tReference:            ref,\n\t\t\t\tExternalAccountKeyID: keyID,\n\t\t\t}\n\t\t\tb, err := json.Marshal(dbeak)\n\t\t\tassert.FatalError(t, err)\n\t\t\tdbrefBytes, err := json.Marshal(dbref)\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tdb: &certdb.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tswitch string(bucket) {\n\t\t\t\t\t\tcase string(externalAccountKeyIDsByReferenceTable):\n\t\t\t\t\t\t\tassert.Equals(t, string(key), ref)\n\t\t\t\t\t\t\treturn dbrefBytes, nil\n\t\t\t\t\t\tcase string(externalAccountKeyTable):\n\t\t\t\t\t\t\tassert.Equals(t, string(key), keyID)\n\t\t\t\t\t\t\treturn b, nil\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\tassert.FatalError(t, errors.Errorf(\"unexpected bucket %s\", string(bucket)))\n\t\t\t\t\t\t\treturn nil, errors.New(\"force default\")\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\tMDel: func(bucket, key []byte) error {\n\t\t\t\t\t\tswitch string(bucket) {\n\t\t\t\t\t\tcase string(externalAccountKeyIDsByReferenceTable):\n\t\t\t\t\t\t\tassert.Equals(t, string(key), provID+\".\"+ref)\n\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\tcase string(externalAccountKeyTable):\n\t\t\t\t\t\t\tassert.Equals(t, string(key), keyID)\n\t\t\t\t\t\t\treturn errors.New(\"force\")\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\tassert.FatalError(t, errors.Errorf(\"unexpected bucket %s\", string(bucket)))\n\t\t\t\t\t\t\treturn errors.New(\"force default\")\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"error deleting ACME EAB Key with Key ID keyID: force\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/delete-eakID\": func(t *testing.T) test {\n\t\t\tnow := clock.Now()\n\t\t\tdbeak := &dbExternalAccountKey{\n\t\t\t\tID:            keyID,\n\t\t\t\tProvisionerID: provID,\n\t\t\t\tReference:     ref,\n\t\t\t\tAccountID:     \"\",\n\t\t\t\tHmacKey:       []byte{1, 3, 3, 7},\n\t\t\t\tCreatedAt:     now,\n\t\t\t}\n\t\t\tdbref := &dbExternalAccountKeyReference{\n\t\t\t\tReference:            ref,\n\t\t\t\tExternalAccountKeyID: keyID,\n\t\t\t}\n\t\t\tb, err := json.Marshal(dbeak)\n\t\t\tassert.FatalError(t, err)\n\t\t\tdbrefBytes, err := json.Marshal(dbref)\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tdb: &certdb.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tswitch string(bucket) {\n\t\t\t\t\t\tcase string(externalAccountKeyIDsByReferenceTable):\n\t\t\t\t\t\t\tassert.Equals(t, string(key), ref)\n\t\t\t\t\t\t\treturn dbrefBytes, nil\n\t\t\t\t\t\tcase string(externalAccountKeyTable):\n\t\t\t\t\t\t\tassert.Equals(t, string(key), keyID)\n\t\t\t\t\t\t\treturn b, nil\n\t\t\t\t\t\tcase string(externalAccountKeyIDsByProvisionerIDTable):\n\t\t\t\t\t\t\treturn b, errors.New(\"force\")\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\tassert.FatalError(t, errors.Errorf(\"unexpected bucket %s\", string(bucket)))\n\t\t\t\t\t\t\treturn nil, errors.New(\"force default\")\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\tMDel: func(bucket, key []byte) error {\n\t\t\t\t\t\tswitch string(bucket) {\n\t\t\t\t\t\tcase string(externalAccountKeyIDsByReferenceTable):\n\t\t\t\t\t\t\tassert.Equals(t, string(key), provID+\".\"+ref)\n\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\tcase string(externalAccountKeyTable):\n\t\t\t\t\t\t\tassert.Equals(t, string(key), keyID)\n\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\tassert.FatalError(t, errors.Errorf(\"unexpected bucket %s\", string(bucket)))\n\t\t\t\t\t\t\treturn errors.New(\"force default\")\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"error removing ACME EAB Key ID keyID: error loading eakIDs for provisioner provID: force\"),\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\ttc := run(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\td := DB{db: tc.db}\n\t\t\tif err := d.DeleteExternalAccountKey(context.Background(), provID, keyID); err != nil {\n\t\t\t\tvar ae *acme.Error\n\t\t\t\tif errors.As(err, &ae) {\n\t\t\t\t\tif assert.NotNil(t, tc.acmeErr) {\n\t\t\t\t\t\tassert.Equals(t, ae.Type, tc.acmeErr.Type)\n\t\t\t\t\t\tassert.Equals(t, ae.Detail, tc.acmeErr.Detail)\n\t\t\t\t\t\tassert.Equals(t, ae.Status, tc.acmeErr.Status)\n\t\t\t\t\t\tassert.Equals(t, ae.Err.Error(), tc.acmeErr.Err.Error())\n\t\t\t\t\t\tassert.Equals(t, ae.Detail, tc.acmeErr.Detail)\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\t\tassert.Equals(t, err.Error(), tc.err.Error())\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tassert.Nil(t, tc.err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDB_CreateExternalAccountKey(t *testing.T) {\n\tkeyID := \"keyID\"\n\tprovID := \"provID\"\n\tref := \"ref\"\n\ttype test struct {\n\t\tdb  nosql.DB\n\t\terr error\n\t\t_id *string\n\t\teak *acme.ExternalAccountKey\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tvar (\n\t\t\t\tid    string\n\t\t\t\tidPtr = &id\n\t\t\t)\n\t\t\tnow := clock.Now()\n\t\t\teak := &acme.ExternalAccountKey{\n\t\t\t\tID:            keyID,\n\t\t\t\tProvisionerID: provID,\n\t\t\t\tReference:     \"ref\",\n\t\t\t\tAccountID:     \"\",\n\t\t\t\tCreatedAt:     now,\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tdb: &certdb.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, string(bucket), string(externalAccountKeyIDsByProvisionerIDTable))\n\t\t\t\t\t\tassert.Equals(t, provID, string(key))\n\t\t\t\t\t\tb, _ := json.Marshal([]string{})\n\t\t\t\t\t\treturn b, nil\n\t\t\t\t\t},\n\t\t\t\t\tMCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {\n\t\t\t\t\t\tswitch string(bucket) {\n\t\t\t\t\t\tcase string(externalAccountKeyIDsByProvisionerIDTable):\n\t\t\t\t\t\t\tassert.Equals(t, provID, string(key))\n\t\t\t\t\t\t\treturn nu, true, nil\n\t\t\t\t\t\tcase string(externalAccountKeyIDsByReferenceTable):\n\t\t\t\t\t\t\tassert.Equals(t, provID+\".\"+ref, string(key))\n\t\t\t\t\t\t\tassert.Equals(t, nil, old)\n\t\t\t\t\t\t\treturn nu, true, nil\n\t\t\t\t\t\tcase string(externalAccountKeyTable):\n\t\t\t\t\t\t\tassert.Equals(t, nil, old)\n\n\t\t\t\t\t\t\tid = string(key)\n\n\t\t\t\t\t\t\tdbeak := new(dbExternalAccountKey)\n\t\t\t\t\t\t\tassert.FatalError(t, json.Unmarshal(nu, dbeak))\n\t\t\t\t\t\t\tassert.Equals(t, string(key), dbeak.ID)\n\t\t\t\t\t\t\tassert.Equals(t, eak.ProvisionerID, dbeak.ProvisionerID)\n\t\t\t\t\t\t\tassert.Equals(t, eak.Reference, dbeak.Reference)\n\t\t\t\t\t\t\tassert.Equals(t, 32, len(dbeak.HmacKey))\n\t\t\t\t\t\t\tassert.False(t, dbeak.CreatedAt.IsZero())\n\t\t\t\t\t\t\tassert.Equals(t, dbeak.AccountID, eak.AccountID)\n\t\t\t\t\t\t\tassert.True(t, dbeak.BoundAt.IsZero())\n\t\t\t\t\t\t\treturn nu, true, nil\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\tassert.FatalError(t, errors.Errorf(\"unexpected bucket %s\", string(bucket)))\n\t\t\t\t\t\t\treturn nil, false, errors.New(\"force default\")\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\teak: eak,\n\t\t\t\t_id: idPtr,\n\t\t\t}\n\t\t},\n\t\t\"fail/externalAccountKeyID-cmpAndSwap-error\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tdb: &certdb.MockNoSQLDB{\n\t\t\t\t\tMCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {\n\t\t\t\t\t\tswitch string(bucket) {\n\t\t\t\t\t\tcase string(externalAccountKeyIDsByReferenceTable):\n\t\t\t\t\t\t\tassert.Equals(t, string(key), ref)\n\t\t\t\t\t\t\tassert.Equals(t, old, nil)\n\t\t\t\t\t\t\treturn nu, true, nil\n\t\t\t\t\t\tcase string(externalAccountKeyTable):\n\t\t\t\t\t\t\tassert.Equals(t, old, nil)\n\t\t\t\t\t\t\treturn nu, true, errors.New(\"force\")\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\tassert.FatalError(t, errors.Errorf(\"unexpected bucket %s\", string(bucket)))\n\t\t\t\t\t\t\treturn nil, false, errors.New(\"force default\")\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"error saving acme external_account_key: force\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/addEAKID-error\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tdb: &certdb.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, string(bucket), string(externalAccountKeyIDsByProvisionerIDTable))\n\t\t\t\t\t\tassert.Equals(t, provID, string(key))\n\t\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t\tMCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {\n\t\t\t\t\t\tswitch string(bucket) {\n\t\t\t\t\t\tcase string(externalAccountKeyIDsByReferenceTable):\n\t\t\t\t\t\t\tassert.Equals(t, string(key), ref)\n\t\t\t\t\t\t\tassert.Equals(t, old, nil)\n\t\t\t\t\t\t\treturn nu, true, nil\n\t\t\t\t\t\tcase string(externalAccountKeyTable):\n\t\t\t\t\t\t\tassert.Equals(t, old, nil)\n\t\t\t\t\t\t\treturn nu, true, nil\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\tassert.FatalError(t, errors.Errorf(\"unexpected bucket %s\", string(bucket)))\n\t\t\t\t\t\t\treturn nil, false, errors.New(\"force default\")\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"error loading eakIDs for provisioner provID: force\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/externalAccountKeyReference-cmpAndSwap-error\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tdb: &certdb.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, string(bucket), string(externalAccountKeyIDsByProvisionerIDTable))\n\t\t\t\t\t\tassert.Equals(t, provID, string(key))\n\t\t\t\t\t\tb, _ := json.Marshal([]string{})\n\t\t\t\t\t\treturn b, nil\n\t\t\t\t\t},\n\t\t\t\t\tMCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {\n\t\t\t\t\t\tswitch string(bucket) {\n\t\t\t\t\t\tcase string(externalAccountKeyIDsByProvisionerIDTable):\n\t\t\t\t\t\t\tassert.Equals(t, provID, string(key))\n\t\t\t\t\t\t\treturn nu, true, nil\n\t\t\t\t\t\tcase string(externalAccountKeyIDsByReferenceTable):\n\t\t\t\t\t\t\tassert.Equals(t, provID+\".\"+ref, string(key))\n\t\t\t\t\t\t\tassert.Equals(t, old, nil)\n\t\t\t\t\t\t\treturn nu, true, errors.New(\"force\")\n\t\t\t\t\t\tcase string(externalAccountKeyTable):\n\t\t\t\t\t\t\tassert.Equals(t, old, nil)\n\t\t\t\t\t\t\treturn nu, true, nil\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\tassert.FatalError(t, errors.Errorf(\"unexpected bucket %s\", string(bucket)))\n\t\t\t\t\t\t\treturn nil, false, errors.New(\"force default\")\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"error saving acme external_account_key_reference: force\"),\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\ttc := run(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\td := DB{db: tc.db}\n\t\t\teak, err := d.CreateExternalAccountKey(context.Background(), provID, ref)\n\t\t\tif err != nil {\n\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\tassert.Equals(t, err.Error(), tc.err.Error())\n\t\t\t\t}\n\t\t\t} else if assert.Nil(t, tc.err) {\n\t\t\t\tassert.Equals(t, *tc._id, eak.ID)\n\t\t\t\tassert.Equals(t, provID, eak.ProvisionerID)\n\t\t\t\tassert.Equals(t, ref, eak.Reference)\n\t\t\t\tassert.Equals(t, \"\", eak.AccountID)\n\t\t\t\tassert.False(t, eak.CreatedAt.IsZero())\n\t\t\t\tassert.False(t, eak.AlreadyBound())\n\t\t\t\tassert.True(t, eak.BoundAt.IsZero())\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDB_UpdateExternalAccountKey(t *testing.T) {\n\tkeyID := \"keyID\"\n\tprovID := \"provID\"\n\tref := \"ref\"\n\tnow := clock.Now()\n\tdbeak := &dbExternalAccountKey{\n\t\tID:            keyID,\n\t\tProvisionerID: provID,\n\t\tReference:     ref,\n\t\tAccountID:     \"\",\n\t\tHmacKey:       []byte{1, 3, 3, 7},\n\t\tCreatedAt:     now,\n\t}\n\tb, err := json.Marshal(dbeak)\n\tassert.FatalError(t, err)\n\ttype test struct {\n\t\tdb  nosql.DB\n\t\teak *acme.ExternalAccountKey\n\t\terr error\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\teak := &acme.ExternalAccountKey{\n\t\t\t\tID:            keyID,\n\t\t\t\tProvisionerID: provID,\n\t\t\t\tReference:     ref,\n\t\t\t\tAccountID:     \"\",\n\t\t\t\tHmacKey:       []byte{1, 3, 3, 7},\n\t\t\t\tCreatedAt:     now,\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\teak: eak,\n\t\t\t\tdb: &certdb.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, externalAccountKeyTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), keyID)\n\n\t\t\t\t\t\treturn b, nil\n\t\t\t\t\t},\n\t\t\t\t\tMCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, externalAccountKeyTable)\n\t\t\t\t\t\tassert.Equals(t, old, b)\n\n\t\t\t\t\t\tdbNew := new(dbExternalAccountKey)\n\t\t\t\t\t\tassert.FatalError(t, json.Unmarshal(nu, dbNew))\n\t\t\t\t\t\tassert.Equals(t, dbNew.ID, dbeak.ID)\n\t\t\t\t\t\tassert.Equals(t, dbNew.ProvisionerID, dbeak.ProvisionerID)\n\t\t\t\t\t\tassert.Equals(t, dbNew.Reference, dbeak.Reference)\n\t\t\t\t\t\tassert.Equals(t, dbNew.AccountID, dbeak.AccountID)\n\t\t\t\t\t\tassert.Equals(t, dbNew.CreatedAt, dbeak.CreatedAt)\n\t\t\t\t\t\tassert.Equals(t, dbNew.BoundAt, dbeak.BoundAt)\n\t\t\t\t\t\tassert.Equals(t, dbNew.HmacKey, dbeak.HmacKey)\n\t\t\t\t\t\treturn nu, true, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"fail/db.Get-error\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\teak: &acme.ExternalAccountKey{\n\t\t\t\t\tID: keyID,\n\t\t\t\t},\n\t\t\t\tdb: &certdb.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, externalAccountKeyTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), keyID)\n\n\t\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"error loading external account key keyID: force\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/provisioner-mismatch\": func(t *testing.T) test {\n\t\t\tnewDBEAK := &dbExternalAccountKey{\n\t\t\t\tID:            keyID,\n\t\t\t\tProvisionerID: \"aDifferentProvID\",\n\t\t\t\tReference:     ref,\n\t\t\t\tAccountID:     \"\",\n\t\t\t\tHmacKey:       []byte{1, 3, 3, 7},\n\t\t\t\tCreatedAt:     now,\n\t\t\t}\n\t\t\tb, err := json.Marshal(newDBEAK)\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\teak: &acme.ExternalAccountKey{\n\t\t\t\t\tID: keyID,\n\t\t\t\t},\n\t\t\t\tdb: &certdb.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, externalAccountKeyTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), keyID)\n\n\t\t\t\t\t\treturn b, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"provisioner does not match provisioner for which the EAB key was created\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/provisioner-change\": func(t *testing.T) test {\n\t\t\tnewDBEAK := &dbExternalAccountKey{\n\t\t\t\tID:            keyID,\n\t\t\t\tProvisionerID: provID,\n\t\t\t\tReference:     ref,\n\t\t\t\tAccountID:     \"\",\n\t\t\t\tHmacKey:       []byte{1, 3, 3, 7},\n\t\t\t\tCreatedAt:     now,\n\t\t\t}\n\t\t\tb, err := json.Marshal(newDBEAK)\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\teak: &acme.ExternalAccountKey{\n\t\t\t\t\tID:            keyID,\n\t\t\t\t\tProvisionerID: \"aDifferentProvisionerID\",\n\t\t\t\t},\n\t\t\t\tdb: &certdb.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, externalAccountKeyTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), keyID)\n\t\t\t\t\t\treturn b, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"cannot change provisioner for an existing ACME EAB Key\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/reference-change\": func(t *testing.T) test {\n\t\t\tnewDBEAK := &dbExternalAccountKey{\n\t\t\t\tID:            keyID,\n\t\t\t\tProvisionerID: provID,\n\t\t\t\tReference:     ref,\n\t\t\t\tAccountID:     \"\",\n\t\t\t\tHmacKey:       []byte{1, 3, 3, 7},\n\t\t\t\tCreatedAt:     now,\n\t\t\t}\n\t\t\tb, err := json.Marshal(newDBEAK)\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\teak: &acme.ExternalAccountKey{\n\t\t\t\t\tID:            keyID,\n\t\t\t\t\tProvisionerID: provID,\n\t\t\t\t\tReference:     \"aDifferentReference\",\n\t\t\t\t},\n\t\t\t\tdb: &certdb.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, externalAccountKeyTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), keyID)\n\t\t\t\t\t\treturn b, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"cannot change reference for an existing ACME EAB Key\"),\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\ttc := run(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\td := DB{db: tc.db}\n\t\t\tif err := d.UpdateExternalAccountKey(context.Background(), provID, tc.eak); err != nil {\n\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.err.Error())\n\t\t\t\t}\n\t\t\t} else if assert.Nil(t, tc.err) {\n\t\t\t\tassert.Equals(t, dbeak.ID, tc.eak.ID)\n\t\t\t\tassert.Equals(t, dbeak.ProvisionerID, tc.eak.ProvisionerID)\n\t\t\t\tassert.Equals(t, dbeak.Reference, tc.eak.Reference)\n\t\t\t\tassert.Equals(t, dbeak.AccountID, tc.eak.AccountID)\n\t\t\t\tassert.Equals(t, dbeak.CreatedAt, tc.eak.CreatedAt)\n\t\t\t\tassert.Equals(t, dbeak.BoundAt, tc.eak.BoundAt)\n\t\t\t\tassert.Equals(t, dbeak.HmacKey, tc.eak.HmacKey)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDB_addEAKID(t *testing.T) {\n\tprovID := \"provID\"\n\teakID := \"eakID\"\n\ttype test struct {\n\t\tctx           context.Context\n\t\tprovisionerID string\n\t\teakID         string\n\t\tdb            nosql.DB\n\t\terr           error\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/empty-eakID\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tctx:           context.Background(),\n\t\t\t\tprovisionerID: provID,\n\t\t\t\teakID:         \"\",\n\t\t\t\terr:           errors.New(\"can't add empty eakID for provisioner provID\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/db.Get\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tctx:           context.Background(),\n\t\t\t\tprovisionerID: provID,\n\t\t\t\teakID:         eakID,\n\t\t\t\tdb: &certdb.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, externalAccountKeyIDsByProvisionerIDTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), provID)\n\t\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"error loading eakIDs for provisioner provID: force\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/unmarshal\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tctx:           context.Background(),\n\t\t\t\tprovisionerID: provID,\n\t\t\t\teakID:         eakID,\n\t\t\t\tdb: &certdb.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, externalAccountKeyIDsByProvisionerIDTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), provID)\n\t\t\t\t\t\tb, _ := json.Marshal(1)\n\t\t\t\t\t\treturn b, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"error unmarshaling eakIDs for provisioner provID: json: cannot unmarshal number into Go value of type []string\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/eakID-already-exists\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tctx:           context.Background(),\n\t\t\t\tprovisionerID: provID,\n\t\t\t\teakID:         eakID,\n\t\t\t\tdb: &certdb.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, externalAccountKeyIDsByProvisionerIDTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), provID)\n\t\t\t\t\t\tb, _ := json.Marshal([]string{eakID})\n\t\t\t\t\t\treturn b, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"eakID eakID already exists for provisioner provID\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/db.save\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tctx:           context.Background(),\n\t\t\t\tprovisionerID: provID,\n\t\t\t\teakID:         eakID,\n\t\t\t\tdb: &certdb.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, externalAccountKeyIDsByProvisionerIDTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), provID)\n\t\t\t\t\t\tb, _ := json.Marshal([]string{\"id1\"})\n\t\t\t\t\t\treturn b, nil\n\t\t\t\t\t},\n\t\t\t\t\tMCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, externalAccountKeyIDsByProvisionerIDTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), provID)\n\t\t\t\t\t\toldB, _ := json.Marshal([]string{\"id1\"})\n\t\t\t\t\t\tassert.Equals(t, old, oldB)\n\t\t\t\t\t\tnewB, _ := json.Marshal([]string{\"id1\", eakID})\n\t\t\t\t\t\tassert.Equals(t, nu, newB)\n\t\t\t\t\t\treturn newB, true, errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"error saving eakIDs index for provisioner provID: error saving acme externalAccountKeyIDsByProvisionerID: force\"),\n\t\t\t}\n\t\t},\n\t\t\"ok/db.Get-not-found\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tctx:           context.Background(),\n\t\t\t\tprovisionerID: provID,\n\t\t\t\teakID:         eakID,\n\t\t\t\tdb: &certdb.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, externalAccountKeyIDsByProvisionerIDTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), provID)\n\t\t\t\t\t\treturn nil, nosqldb.ErrNotFound\n\t\t\t\t\t},\n\t\t\t\t\tMCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, externalAccountKeyIDsByProvisionerIDTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), provID)\n\t\t\t\t\t\tassert.Equals(t, old, nil)\n\t\t\t\t\t\tb, _ := json.Marshal([]string{eakID})\n\t\t\t\t\t\tassert.Equals(t, nu, b)\n\t\t\t\t\t\treturn b, true, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: nil,\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tctx:           context.Background(),\n\t\t\t\tprovisionerID: provID,\n\t\t\t\teakID:         eakID,\n\t\t\t\tdb: &certdb.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, externalAccountKeyIDsByProvisionerIDTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), provID)\n\t\t\t\t\t\tb, _ := json.Marshal([]string{\"id1\", \"id2\"})\n\t\t\t\t\t\treturn b, nil\n\t\t\t\t\t},\n\t\t\t\t\tMCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, externalAccountKeyIDsByProvisionerIDTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), provID)\n\t\t\t\t\t\toldB, _ := json.Marshal([]string{\"id1\", \"id2\"})\n\t\t\t\t\t\tassert.Equals(t, old, oldB)\n\t\t\t\t\t\tnewB, _ := json.Marshal([]string{\"id1\", \"id2\", eakID})\n\t\t\t\t\t\tassert.Equals(t, nu, newB)\n\t\t\t\t\t\treturn newB, true, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: nil,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\ttc := run(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tdb := &DB{\n\t\t\t\tdb: tc.db,\n\t\t\t}\n\t\t\twantErr := tc.err != nil\n\t\t\terr := db.addEAKID(tc.ctx, tc.provisionerID, tc.eakID)\n\t\t\tif (err != nil) != wantErr {\n\t\t\t\tt.Errorf(\"DB.addEAKID() error = %v, wantErr %v\", err, wantErr)\n\t\t\t}\n\t\t\tif err != nil {\n\t\t\t\tassert.Equals(t, tc.err.Error(), err.Error())\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDB_deleteEAKID(t *testing.T) {\n\tprovID := \"provID\"\n\teakID := \"eakID\"\n\ttype test struct {\n\t\tctx           context.Context\n\t\tprovisionerID string\n\t\teakID         string\n\t\tdb            nosql.DB\n\t\terr           error\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/db.Get\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tctx:           context.Background(),\n\t\t\t\tprovisionerID: provID,\n\t\t\t\teakID:         eakID,\n\t\t\t\tdb: &certdb.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, externalAccountKeyIDsByProvisionerIDTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), provID)\n\t\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"error loading eakIDs for provisioner provID: force\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/unmarshal\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tctx:           context.Background(),\n\t\t\t\tprovisionerID: provID,\n\t\t\t\teakID:         eakID,\n\t\t\t\tdb: &certdb.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, externalAccountKeyIDsByProvisionerIDTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), provID)\n\t\t\t\t\t\tb, _ := json.Marshal(1)\n\t\t\t\t\t\treturn b, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"error unmarshaling eakIDs for provisioner provID: json: cannot unmarshal number into Go value of type []string\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/db.save\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tctx:           context.Background(),\n\t\t\t\tprovisionerID: provID,\n\t\t\t\teakID:         eakID,\n\t\t\t\tdb: &certdb.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, externalAccountKeyIDsByProvisionerIDTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), provID)\n\t\t\t\t\t\tb, _ := json.Marshal([]string{\"id1\", eakID})\n\t\t\t\t\t\treturn b, nil\n\t\t\t\t\t},\n\t\t\t\t\tMCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, externalAccountKeyIDsByProvisionerIDTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), provID)\n\t\t\t\t\t\toldB, _ := json.Marshal([]string{\"id1\", eakID})\n\t\t\t\t\t\tassert.Equals(t, old, oldB)\n\t\t\t\t\t\tnewB, _ := json.Marshal([]string{\"id1\"})\n\t\t\t\t\t\tassert.Equals(t, nu, newB)\n\t\t\t\t\t\treturn newB, true, errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"error saving eakIDs index for provisioner provID: error saving acme externalAccountKeyIDsByProvisionerID: force\"),\n\t\t\t}\n\t\t},\n\t\t\"ok/db.Get-not-found\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tctx:           context.Background(),\n\t\t\t\tprovisionerID: provID,\n\t\t\t\teakID:         eakID,\n\t\t\t\tdb: &certdb.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, externalAccountKeyIDsByProvisionerIDTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), provID)\n\t\t\t\t\t\treturn nil, nosqldb.ErrNotFound\n\t\t\t\t\t},\n\t\t\t\t\tMCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, externalAccountKeyIDsByProvisionerIDTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), provID)\n\t\t\t\t\t\tassert.Equals(t, old, nil)\n\t\t\t\t\t\tb, _ := json.Marshal([]string{})\n\t\t\t\t\t\tassert.Equals(t, nu, b)\n\t\t\t\t\t\treturn b, true, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: nil,\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tctx:           context.Background(),\n\t\t\t\tprovisionerID: provID,\n\t\t\t\teakID:         eakID,\n\t\t\t\tdb: &certdb.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, externalAccountKeyIDsByProvisionerIDTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), provID)\n\t\t\t\t\t\tb, _ := json.Marshal([]string{\"id1\", eakID, \"id2\"})\n\t\t\t\t\t\treturn b, nil\n\t\t\t\t\t},\n\t\t\t\t\tMCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, externalAccountKeyIDsByProvisionerIDTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), provID)\n\t\t\t\t\t\toldB, _ := json.Marshal([]string{\"id1\", eakID, \"id2\"})\n\t\t\t\t\t\tassert.Equals(t, old, oldB)\n\t\t\t\t\t\tnewB, _ := json.Marshal([]string{\"id1\", \"id2\"})\n\t\t\t\t\t\tassert.Equals(t, nu, newB)\n\t\t\t\t\t\treturn newB, true, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: nil,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\ttc := run(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tdb := &DB{\n\t\t\t\tdb: tc.db,\n\t\t\t}\n\t\t\twantErr := tc.err != nil\n\t\t\terr := db.deleteEAKID(tc.ctx, tc.provisionerID, tc.eakID)\n\t\t\tif (err != nil) != wantErr {\n\t\t\t\tt.Errorf(\"DB.deleteEAKID() error = %v, wantErr %v\", err, wantErr)\n\t\t\t}\n\t\t\tif err != nil {\n\t\t\t\tassert.Equals(t, tc.err.Error(), err.Error())\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDB_addAndDeleteEAKID(t *testing.T) {\n\tprovID := \"provID\"\n\tcallCounter := 0\n\ttype test struct {\n\t\tctx context.Context\n\t\tdb  nosql.DB\n\t\terr error\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"ok/multi\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tctx: context.Background(),\n\t\t\t\tdb: &certdb.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, externalAccountKeyIDsByProvisionerIDTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), provID)\n\t\t\t\t\t\tswitch callCounter {\n\t\t\t\t\t\tcase 0:\n\t\t\t\t\t\t\treturn nil, nosqldb.ErrNotFound\n\t\t\t\t\t\tcase 1:\n\t\t\t\t\t\t\tb, _ := json.Marshal([]string{\"eakID\"})\n\t\t\t\t\t\t\treturn b, nil\n\t\t\t\t\t\tcase 2:\n\t\t\t\t\t\t\tb, _ := json.Marshal([]string{})\n\t\t\t\t\t\t\treturn b, nil\n\t\t\t\t\t\tcase 3:\n\t\t\t\t\t\t\tb, _ := json.Marshal([]string{\"eakID1\"})\n\t\t\t\t\t\t\treturn b, nil\n\t\t\t\t\t\tcase 4:\n\t\t\t\t\t\t\tb, _ := json.Marshal([]string{\"eakID1\", \"eakID2\"})\n\t\t\t\t\t\t\treturn b, nil\n\t\t\t\t\t\tcase 5:\n\t\t\t\t\t\t\tb, _ := json.Marshal([]string{\"eakID2\"})\n\t\t\t\t\t\t\treturn b, nil\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\tassert.FatalError(t, errors.New(\"unexpected get iteration\"))\n\t\t\t\t\t\t\treturn nil, errors.New(\"force get default\")\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\tMCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, externalAccountKeyIDsByProvisionerIDTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), provID)\n\t\t\t\t\t\tswitch callCounter {\n\t\t\t\t\t\tcase 0:\n\t\t\t\t\t\t\tassert.Equals(t, old, nil)\n\t\t\t\t\t\t\tnewB, _ := json.Marshal([]string{\"eakID\"})\n\t\t\t\t\t\t\tassert.Equals(t, nu, newB)\n\t\t\t\t\t\t\treturn newB, true, nil\n\t\t\t\t\t\tcase 1:\n\t\t\t\t\t\t\toldB, _ := json.Marshal([]string{\"eakID\"})\n\t\t\t\t\t\t\tassert.Equals(t, old, oldB)\n\t\t\t\t\t\t\tnewB, _ := json.Marshal([]string{})\n\t\t\t\t\t\t\treturn newB, true, nil\n\t\t\t\t\t\tcase 2:\n\t\t\t\t\t\t\tassert.Equals(t, old, nil)\n\t\t\t\t\t\t\tnewB, _ := json.Marshal([]string{\"eakID1\"})\n\t\t\t\t\t\t\tassert.Equals(t, nu, newB)\n\t\t\t\t\t\t\treturn newB, true, nil\n\t\t\t\t\t\tcase 3:\n\t\t\t\t\t\t\toldB, _ := json.Marshal([]string{\"eakID1\"})\n\t\t\t\t\t\t\tassert.Equals(t, old, oldB)\n\t\t\t\t\t\t\tnewB, _ := json.Marshal([]string{\"eakID1\", \"eakID2\"})\n\t\t\t\t\t\t\tassert.Equals(t, nu, newB)\n\t\t\t\t\t\t\treturn newB, true, nil\n\t\t\t\t\t\tcase 4:\n\t\t\t\t\t\t\toldB, _ := json.Marshal([]string{\"eakID1\", \"eakID2\"})\n\t\t\t\t\t\t\tassert.Equals(t, old, oldB)\n\t\t\t\t\t\t\tnewB, _ := json.Marshal([]string{\"eakID2\"})\n\t\t\t\t\t\t\tassert.Equals(t, nu, newB)\n\t\t\t\t\t\t\treturn newB, true, nil\n\t\t\t\t\t\tcase 5:\n\t\t\t\t\t\t\toldB, _ := json.Marshal([]string{\"eakID2\"})\n\t\t\t\t\t\t\tassert.Equals(t, old, oldB)\n\t\t\t\t\t\t\tnewB, _ := json.Marshal([]string{})\n\t\t\t\t\t\t\tassert.Equals(t, nu, newB)\n\t\t\t\t\t\t\treturn newB, true, nil\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\tassert.FatalError(t, errors.New(\"unexpected get iteration\"))\n\t\t\t\t\t\t\treturn nil, true, errors.New(\"force save default\")\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: nil,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\ttc := run(t)\n\t\tt.Run(name, func(t *testing.T) {\n\n\t\t\t// goal of this test is to simulate multiple calls; no errors expected.\n\n\t\t\tdb := &DB{\n\t\t\t\tdb: tc.db,\n\t\t\t}\n\n\t\t\terr := db.addEAKID(tc.ctx, provID, \"eakID\")\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"DB.addEAKID() error = %v\", err)\n\t\t\t}\n\n\t\t\tcallCounter++\n\t\t\terr = db.deleteEAKID(tc.ctx, provID, \"eakID\")\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"DB.deleteEAKID() error = %v\", err)\n\t\t\t}\n\n\t\t\tcallCounter++\n\t\t\terr = db.addEAKID(tc.ctx, provID, \"eakID1\")\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"DB.addEAKID() error = %v\", err)\n\t\t\t}\n\n\t\t\tcallCounter++\n\t\t\terr = db.addEAKID(tc.ctx, provID, \"eakID2\")\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"DB.addEAKID() error = %v\", err)\n\t\t\t}\n\n\t\t\tcallCounter++\n\t\t\terr = db.deleteEAKID(tc.ctx, provID, \"eakID1\")\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"DB.deleteEAKID() error = %v\", err)\n\t\t\t}\n\n\t\t\tcallCounter++\n\t\t\terr = db.deleteEAKID(tc.ctx, provID, \"eakID2\")\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"DB.deleteAKID() error = %v\", err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_removeElement(t *testing.T) {\n\ttests := []struct {\n\t\tname  string\n\t\tslice []string\n\t\titem  string\n\t\twant  []string\n\t}{\n\t\t{\n\t\t\tname:  \"remove-first\",\n\t\t\tslice: []string{\"id1\", \"id2\", \"id3\"},\n\t\t\titem:  \"id1\",\n\t\t\twant:  []string{\"id2\", \"id3\"},\n\t\t},\n\t\t{\n\t\t\tname:  \"remove-last\",\n\t\t\tslice: []string{\"id1\", \"id2\", \"id3\"},\n\t\t\titem:  \"id3\",\n\t\t\twant:  []string{\"id1\", \"id2\"},\n\t\t},\n\t\t{\n\t\t\tname:  \"remove-middle\",\n\t\t\tslice: []string{\"id1\", \"id2\", \"id3\"},\n\t\t\titem:  \"id2\",\n\t\t\twant:  []string{\"id1\", \"id3\"},\n\t\t},\n\t\t{\n\t\t\tname:  \"remove-non-existing\",\n\t\t\tslice: []string{\"id1\", \"id2\", \"id3\"},\n\t\t\titem:  \"none\",\n\t\t\twant:  []string{\"id1\", \"id2\", \"id3\"},\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot := removeElement(tt.slice, tt.item)\n\t\t\tif !cmp.Equal(tt.want, got) {\n\t\t\t\tt.Errorf(\"removeElement() diff =\\n %s\", cmp.Diff(tt.want, got))\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "acme/db/nosql/nonce.go",
    "content": "package nosql\n\nimport (\n\t\"context\"\n\t\"encoding/base64\"\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n\t\"github.com/smallstep/certificates/acme\"\n\t\"github.com/smallstep/nosql\"\n\t\"github.com/smallstep/nosql/database\"\n)\n\n// dbNonce contains nonce metadata used in the ACME protocol.\ntype dbNonce struct {\n\tID        string\n\tCreatedAt time.Time\n\tDeletedAt time.Time\n}\n\n// CreateNonce creates, stores, and returns an ACME replay-nonce.\n// Implements the acme.DB interface.\nfunc (db *DB) CreateNonce(ctx context.Context) (acme.Nonce, error) {\n\t_id, err := randID()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tid := base64.RawURLEncoding.EncodeToString([]byte(_id))\n\tn := &dbNonce{\n\t\tID:        id,\n\t\tCreatedAt: clock.Now(),\n\t}\n\tif err := db.save(ctx, id, n, nil, \"nonce\", nonceTable); err != nil {\n\t\treturn \"\", err\n\t}\n\treturn acme.Nonce(id), nil\n}\n\n// DeleteNonce verifies that the nonce is valid (by checking if it exists),\n// and if so, consumes the nonce resource by deleting it from the database.\nfunc (db *DB) DeleteNonce(_ context.Context, nonce acme.Nonce) error {\n\terr := db.db.Update(&database.Tx{\n\t\tOperations: []*database.TxEntry{\n\t\t\t{\n\t\t\t\tBucket: nonceTable,\n\t\t\t\tKey:    []byte(nonce),\n\t\t\t\tCmd:    database.Get,\n\t\t\t},\n\t\t\t{\n\t\t\t\tBucket: nonceTable,\n\t\t\t\tKey:    []byte(nonce),\n\t\t\t\tCmd:    database.Delete,\n\t\t\t},\n\t\t},\n\t})\n\n\tswitch {\n\tcase nosql.IsErrNotFound(err):\n\t\treturn acme.NewError(acme.ErrorBadNonceType, \"nonce %s not found\", string(nonce))\n\tcase err != nil:\n\t\treturn errors.Wrapf(err, \"error deleting nonce %s\", string(nonce))\n\tdefault:\n\t\treturn nil\n\t}\n}\n"
  },
  {
    "path": "acme/db/nosql/nonce_test.go",
    "content": "package nosql\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n\t\"github.com/smallstep/assert\"\n\t\"github.com/smallstep/certificates/acme\"\n\t\"github.com/smallstep/certificates/db\"\n\t\"github.com/smallstep/nosql\"\n\t\"github.com/smallstep/nosql/database\"\n)\n\nfunc TestDB_CreateNonce(t *testing.T) {\n\ttype test struct {\n\t\tdb  nosql.DB\n\t\terr error\n\t\t_id *string\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/cmpAndSwap-error\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, nonceTable)\n\t\t\t\t\t\tassert.Equals(t, old, nil)\n\n\t\t\t\t\t\tdbn := new(dbNonce)\n\t\t\t\t\t\tassert.FatalError(t, json.Unmarshal(nu, dbn))\n\t\t\t\t\t\tassert.Equals(t, dbn.ID, string(key))\n\t\t\t\t\t\tassert.True(t, clock.Now().Add(-time.Minute).Before(dbn.CreatedAt))\n\t\t\t\t\t\tassert.True(t, clock.Now().Add(time.Minute).After(dbn.CreatedAt))\n\t\t\t\t\t\treturn nil, false, errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"error saving acme nonce: force\"),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tvar (\n\t\t\t\tid    string\n\t\t\t\tidPtr = &id\n\t\t\t)\n\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {\n\t\t\t\t\t\t*idPtr = string(key)\n\t\t\t\t\t\tassert.Equals(t, bucket, nonceTable)\n\t\t\t\t\t\tassert.Equals(t, old, nil)\n\n\t\t\t\t\t\tdbn := new(dbNonce)\n\t\t\t\t\t\tassert.FatalError(t, json.Unmarshal(nu, dbn))\n\t\t\t\t\t\tassert.Equals(t, dbn.ID, string(key))\n\t\t\t\t\t\tassert.True(t, clock.Now().Add(-time.Minute).Before(dbn.CreatedAt))\n\t\t\t\t\t\tassert.True(t, clock.Now().Add(time.Minute).After(dbn.CreatedAt))\n\t\t\t\t\t\treturn nil, true, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t_id: idPtr,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\ttc := run(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\td := DB{db: tc.db}\n\t\t\tif n, err := d.CreateNonce(context.Background()); err != nil {\n\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.err.Error())\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif assert.Nil(t, tc.err) {\n\t\t\t\t\tassert.Equals(t, string(n), *tc._id)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDB_DeleteNonce(t *testing.T) {\n\n\tnonceID := \"nonceID\"\n\ttype test struct {\n\t\tdb      nosql.DB\n\t\terr     error\n\t\tacmeErr *acme.Error\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/not-found\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMUpdate: func(tx *database.Tx) error {\n\t\t\t\t\t\tassert.Equals(t, tx.Operations[0].Bucket, nonceTable)\n\t\t\t\t\t\tassert.Equals(t, tx.Operations[0].Key, []byte(nonceID))\n\t\t\t\t\t\tassert.Equals(t, tx.Operations[0].Cmd, database.Get)\n\n\t\t\t\t\t\tassert.Equals(t, tx.Operations[1].Bucket, nonceTable)\n\t\t\t\t\t\tassert.Equals(t, tx.Operations[1].Key, []byte(nonceID))\n\t\t\t\t\t\tassert.Equals(t, tx.Operations[1].Cmd, database.Delete)\n\t\t\t\t\t\treturn database.ErrNotFound\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tacmeErr: acme.NewError(acme.ErrorBadNonceType, \"nonce %s not found\", nonceID),\n\t\t\t}\n\t\t},\n\t\t\"fail/db.Update-error\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMUpdate: func(tx *database.Tx) error {\n\t\t\t\t\t\tassert.Equals(t, tx.Operations[0].Bucket, nonceTable)\n\t\t\t\t\t\tassert.Equals(t, tx.Operations[0].Key, []byte(nonceID))\n\t\t\t\t\t\tassert.Equals(t, tx.Operations[0].Cmd, database.Get)\n\n\t\t\t\t\t\tassert.Equals(t, tx.Operations[1].Bucket, nonceTable)\n\t\t\t\t\t\tassert.Equals(t, tx.Operations[1].Key, []byte(nonceID))\n\t\t\t\t\t\tassert.Equals(t, tx.Operations[1].Cmd, database.Delete)\n\t\t\t\t\t\treturn errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"error deleting nonce nonceID: force\"),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMUpdate: func(tx *database.Tx) error {\n\t\t\t\t\t\tassert.Equals(t, tx.Operations[0].Bucket, nonceTable)\n\t\t\t\t\t\tassert.Equals(t, tx.Operations[0].Key, []byte(nonceID))\n\t\t\t\t\t\tassert.Equals(t, tx.Operations[0].Cmd, database.Get)\n\n\t\t\t\t\t\tassert.Equals(t, tx.Operations[1].Bucket, nonceTable)\n\t\t\t\t\t\tassert.Equals(t, tx.Operations[1].Key, []byte(nonceID))\n\t\t\t\t\t\tassert.Equals(t, tx.Operations[1].Cmd, database.Delete)\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\ttc := run(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\td := DB{db: tc.db}\n\t\t\tif err := d.DeleteNonce(context.Background(), acme.Nonce(nonceID)); err != nil {\n\t\t\t\tvar ae *acme.Error\n\t\t\t\tif errors.As(err, &ae) {\n\t\t\t\t\tif assert.NotNil(t, tc.acmeErr) {\n\t\t\t\t\t\tassert.Equals(t, ae.Type, tc.acmeErr.Type)\n\t\t\t\t\t\tassert.Equals(t, ae.Detail, tc.acmeErr.Detail)\n\t\t\t\t\t\tassert.Equals(t, ae.Status, tc.acmeErr.Status)\n\t\t\t\t\t\tassert.Equals(t, ae.Err.Error(), tc.acmeErr.Err.Error())\n\t\t\t\t\t\tassert.Equals(t, ae.Detail, tc.acmeErr.Detail)\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.err.Error())\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tassert.Nil(t, tc.err)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "acme/db/nosql/nosql.go",
    "content": "package nosql\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n\tnosqlDB \"github.com/smallstep/nosql\"\n\t\"go.step.sm/crypto/randutil\"\n)\n\nvar (\n\taccountTable                              = []byte(\"acme_accounts\")\n\taccountByKeyIDTable                       = []byte(\"acme_keyID_accountID_index\")\n\tauthzTable                                = []byte(\"acme_authzs\")\n\tchallengeTable                            = []byte(\"acme_challenges\")\n\tnonceTable                                = []byte(\"nonces\")\n\torderTable                                = []byte(\"acme_orders\")\n\tordersByAccountIDTable                    = []byte(\"acme_account_orders_index\")\n\tcertTable                                 = []byte(\"acme_certs\")\n\tcertBySerialTable                         = []byte(\"acme_serial_certs_index\")\n\texternalAccountKeyTable                   = []byte(\"acme_external_account_keys\")\n\texternalAccountKeyIDsByReferenceTable     = []byte(\"acme_external_account_keyID_reference_index\")\n\texternalAccountKeyIDsByProvisionerIDTable = []byte(\"acme_external_account_keyID_provisionerID_index\")\n\twireDpopTokenTable                        = []byte(\"wire_acme_dpop_token\")\n\twireOidcTokenTable                        = []byte(\"wire_acme_oidc_token\")\n)\n\n// DB is a struct that implements the AcmeDB interface.\ntype DB struct {\n\tdb nosqlDB.DB\n}\n\n// New configures and returns a new ACME DB backend implemented using a nosql DB.\nfunc New(db nosqlDB.DB) (*DB, error) {\n\ttables := [][]byte{accountTable, accountByKeyIDTable, authzTable,\n\t\tchallengeTable, nonceTable, orderTable, ordersByAccountIDTable,\n\t\tcertTable, certBySerialTable, externalAccountKeyTable,\n\t\texternalAccountKeyIDsByReferenceTable, externalAccountKeyIDsByProvisionerIDTable,\n\t\twireDpopTokenTable, wireOidcTokenTable,\n\t}\n\tfor _, b := range tables {\n\t\tif err := db.CreateTable(b); err != nil {\n\t\t\treturn nil, errors.Wrapf(err, \"error creating table %s\", string(b))\n\t\t}\n\t}\n\treturn &DB{db}, nil\n}\n\n// save writes the new data to the database, overwriting the old data if it\n// existed.\nfunc (db *DB) save(_ context.Context, id string, nu, old interface{}, typ string, table []byte) error {\n\tvar (\n\t\terr  error\n\t\tnewB []byte\n\t)\n\tif nu == nil {\n\t\tnewB = nil\n\t} else {\n\t\tnewB, err = json.Marshal(nu)\n\t\tif err != nil {\n\t\t\treturn errors.Wrapf(err, \"error marshaling acme type: %s, value: %v\", typ, nu)\n\t\t}\n\t}\n\tvar oldB []byte\n\tif old == nil {\n\t\toldB = nil\n\t} else {\n\t\toldB, err = json.Marshal(old)\n\t\tif err != nil {\n\t\t\treturn errors.Wrapf(err, \"error marshaling acme type: %s, value: %v\", typ, old)\n\t\t}\n\t}\n\n\t_, swapped, err := db.db.CmpAndSwap(table, []byte(id), oldB, newB)\n\tswitch {\n\tcase err != nil:\n\t\treturn errors.Wrapf(err, \"error saving acme %s\", typ)\n\tcase !swapped:\n\t\treturn errors.Errorf(\"error saving acme %s; changed since last read\", typ)\n\tdefault:\n\t\treturn nil\n\t}\n}\n\nvar idLen = 32\n\nfunc randID() (val string, err error) {\n\tval, err = randutil.Alphanumeric(idLen)\n\tif err != nil {\n\t\treturn \"\", errors.Wrap(err, \"error generating random alphanumeric ID\")\n\t}\n\treturn val, nil\n}\n\n// Clock that returns time in UTC rounded to seconds.\ntype Clock struct{}\n\n// Now returns the UTC time rounded to seconds.\nfunc (c *Clock) Now() time.Time {\n\treturn time.Now().UTC().Truncate(time.Second)\n}\n\nvar clock = new(Clock)\n"
  },
  {
    "path": "acme/db/nosql/nosql_test.go",
    "content": "package nosql\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/pkg/errors\"\n\t\"github.com/smallstep/assert\"\n\t\"github.com/smallstep/certificates/db\"\n\t\"github.com/smallstep/nosql\"\n)\n\nfunc TestNew(t *testing.T) {\n\ttype test struct {\n\t\tdb  nosql.DB\n\t\terr error\n\t}\n\tvar tests = map[string]test{\n\t\t\"fail/db.CreateTable-error\": {\n\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\tMCreateTable: func(bucket []byte) error {\n\t\t\t\t\tassert.Equals(t, string(bucket), string(accountTable))\n\t\t\t\t\treturn errors.New(\"force\")\n\t\t\t\t},\n\t\t\t},\n\t\t\terr: errors.Errorf(\"error creating table %s: force\", string(accountTable)),\n\t\t},\n\t\t\"ok\": {\n\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\tMCreateTable: func(bucket []byte) error {\n\t\t\t\t\treturn nil\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\tfor name, tc := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tif _, err := New(tc.db); err != nil {\n\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.err.Error())\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tassert.Nil(t, tc.err)\n\t\t\t}\n\t\t})\n\t}\n}\n\ntype errorThrower string\n\nfunc (et errorThrower) MarshalJSON() ([]byte, error) {\n\treturn nil, errors.New(\"force\")\n}\n\nfunc TestDB_save(t *testing.T) {\n\ttype test struct {\n\t\tdb  nosql.DB\n\t\tnu  interface{}\n\t\told interface{}\n\t\terr error\n\t}\n\tvar tests = map[string]test{\n\t\t\"fail/error-marshaling-new\": {\n\t\t\tnu:  errorThrower(\"foo\"),\n\t\t\terr: errors.New(\"error marshaling acme type: challenge\"),\n\t\t},\n\t\t\"fail/error-marshaling-old\": {\n\t\t\tnu:  \"new\",\n\t\t\told: errorThrower(\"foo\"),\n\t\t\terr: errors.New(\"error marshaling acme type: challenge\"),\n\t\t},\n\t\t\"fail/db.CmpAndSwap-error\": {\n\t\t\tnu:  \"new\",\n\t\t\told: \"old\",\n\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\tMCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {\n\t\t\t\t\tassert.Equals(t, bucket, challengeTable)\n\t\t\t\t\tassert.Equals(t, string(key), \"id\")\n\t\t\t\t\tassert.Equals(t, string(old), \"\\\"old\\\"\")\n\t\t\t\t\tassert.Equals(t, string(nu), \"\\\"new\\\"\")\n\t\t\t\t\treturn nil, false, errors.New(\"force\")\n\t\t\t\t},\n\t\t\t},\n\t\t\terr: errors.New(\"error saving acme challenge: force\"),\n\t\t},\n\t\t\"fail/db.CmpAndSwap-false-marshaling-old\": {\n\t\t\tnu:  \"new\",\n\t\t\told: \"old\",\n\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\tMCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {\n\t\t\t\t\tassert.Equals(t, bucket, challengeTable)\n\t\t\t\t\tassert.Equals(t, string(key), \"id\")\n\t\t\t\t\tassert.Equals(t, string(old), \"\\\"old\\\"\")\n\t\t\t\t\tassert.Equals(t, string(nu), \"\\\"new\\\"\")\n\t\t\t\t\treturn nil, false, nil\n\t\t\t\t},\n\t\t\t},\n\t\t\terr: errors.New(\"error saving acme challenge; changed since last read\"),\n\t\t},\n\t\t\"ok\": {\n\t\t\tnu:  \"new\",\n\t\t\told: \"old\",\n\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\tMCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {\n\t\t\t\t\tassert.Equals(t, bucket, challengeTable)\n\t\t\t\t\tassert.Equals(t, string(key), \"id\")\n\t\t\t\t\tassert.Equals(t, string(old), \"\\\"old\\\"\")\n\t\t\t\t\tassert.Equals(t, string(nu), \"\\\"new\\\"\")\n\t\t\t\t\treturn nu, true, nil\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t\"ok/nils\": {\n\t\t\tnu:  nil,\n\t\t\told: nil,\n\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\tMCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {\n\t\t\t\t\tassert.Equals(t, bucket, challengeTable)\n\t\t\t\t\tassert.Equals(t, string(key), \"id\")\n\t\t\t\t\tassert.Equals(t, old, nil)\n\t\t\t\t\tassert.Equals(t, nu, nil)\n\t\t\t\t\treturn nu, true, nil\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\tfor name, tc := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\td := &DB{db: tc.db}\n\t\t\tif err := d.save(context.Background(), \"id\", tc.nu, tc.old, \"challenge\", challengeTable); err != nil {\n\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.err.Error())\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tassert.Nil(t, tc.err)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "acme/db/nosql/order.go",
    "content": "package nosql\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n\t\"github.com/smallstep/certificates/acme\"\n\t\"github.com/smallstep/nosql\"\n)\n\n// Mutex for locking ordersByAccount index operations.\nvar ordersByAccountMux sync.Mutex\n\ntype dbOrder struct {\n\tID               string            `json:\"id\"`\n\tAccountID        string            `json:\"accountID\"`\n\tProvisionerID    string            `json:\"provisionerID\"`\n\tIdentifiers      []acme.Identifier `json:\"identifiers\"`\n\tAuthorizationIDs []string          `json:\"authorizationIDs\"`\n\tStatus           acme.Status       `json:\"status\"`\n\tNotBefore        time.Time         `json:\"notBefore,omitempty\"`\n\tNotAfter         time.Time         `json:\"notAfter,omitempty\"`\n\tCreatedAt        time.Time         `json:\"createdAt\"`\n\tExpiresAt        time.Time         `json:\"expiresAt,omitempty\"`\n\tCertificateID    string            `json:\"certificate,omitempty\"`\n\tError            *acme.Error       `json:\"error,omitempty\"`\n}\n\nfunc (a *dbOrder) clone() *dbOrder {\n\tb := *a\n\treturn &b\n}\n\n// getDBOrder retrieves and unmarshals an ACME Order type from the database.\nfunc (db *DB) getDBOrder(_ context.Context, id string) (*dbOrder, error) {\n\tb, err := db.db.Get(orderTable, []byte(id))\n\tif nosql.IsErrNotFound(err) {\n\t\treturn nil, acme.NewError(acme.ErrorMalformedType, \"order %s not found\", id)\n\t} else if err != nil {\n\t\treturn nil, errors.Wrapf(err, \"error loading order %s\", id)\n\t}\n\to := new(dbOrder)\n\tif err := json.Unmarshal(b, &o); err != nil {\n\t\treturn nil, errors.Wrapf(err, \"error unmarshaling order %s into dbOrder\", id)\n\t}\n\treturn o, nil\n}\n\n// GetOrder retrieves an ACME Order from the database.\nfunc (db *DB) GetOrder(ctx context.Context, id string) (*acme.Order, error) {\n\tdbo, err := db.getDBOrder(ctx, id)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\to := &acme.Order{\n\t\tID:               dbo.ID,\n\t\tAccountID:        dbo.AccountID,\n\t\tProvisionerID:    dbo.ProvisionerID,\n\t\tCertificateID:    dbo.CertificateID,\n\t\tStatus:           dbo.Status,\n\t\tExpiresAt:        dbo.ExpiresAt,\n\t\tIdentifiers:      dbo.Identifiers,\n\t\tNotBefore:        dbo.NotBefore,\n\t\tNotAfter:         dbo.NotAfter,\n\t\tAuthorizationIDs: dbo.AuthorizationIDs,\n\t\tError:            dbo.Error,\n\t}\n\n\treturn o, nil\n}\n\n// CreateOrder creates ACME Order resources and saves them to the DB.\nfunc (db *DB) CreateOrder(ctx context.Context, o *acme.Order) error {\n\tvar err error\n\to.ID, err = randID()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tnow := clock.Now()\n\tdbo := &dbOrder{\n\t\tID:               o.ID,\n\t\tAccountID:        o.AccountID,\n\t\tProvisionerID:    o.ProvisionerID,\n\t\tStatus:           o.Status,\n\t\tCreatedAt:        now,\n\t\tExpiresAt:        o.ExpiresAt,\n\t\tIdentifiers:      o.Identifiers,\n\t\tNotBefore:        o.NotBefore,\n\t\tNotAfter:         o.NotAfter,\n\t\tAuthorizationIDs: o.AuthorizationIDs,\n\t}\n\tif err := db.save(ctx, o.ID, dbo, nil, \"order\", orderTable); err != nil {\n\t\treturn err\n\t}\n\n\t_, err = db.updateAddOrderIDs(ctx, o.AccountID, false, o.ID)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\n// UpdateOrder saves an updated ACME Order to the database.\nfunc (db *DB) UpdateOrder(ctx context.Context, o *acme.Order) error {\n\told, err := db.getDBOrder(ctx, o.ID)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tnu := old.clone()\n\n\tnu.Status = o.Status\n\tnu.Error = o.Error\n\tnu.CertificateID = o.CertificateID\n\n\treturn db.save(ctx, old.ID, nu, old, \"order\", orderTable)\n}\n\nfunc (db *DB) updateAddOrderIDs(ctx context.Context, accID string, includeReadyOrders bool, addOids ...string) ([]string, error) {\n\tordersByAccountMux.Lock()\n\tdefer ordersByAccountMux.Unlock()\n\n\tvar oldOids []string\n\tb, err := db.db.Get(ordersByAccountIDTable, []byte(accID))\n\tif err != nil {\n\t\tif !nosql.IsErrNotFound(err) {\n\t\t\treturn nil, errors.Wrapf(err, \"error loading orderIDs for account %s\", accID)\n\t\t}\n\t} else {\n\t\tif err := json.Unmarshal(b, &oldOids); err != nil {\n\t\t\treturn nil, errors.Wrapf(err, \"error unmarshaling orderIDs for account %s\", accID)\n\t\t}\n\t}\n\n\t// Remove any order that is not in PENDING state and update the stored list\n\t// before returning.\n\t//\n\t// According to RFC 8555:\n\t// The server SHOULD include pending orders and SHOULD NOT include orders\n\t// that are invalid in the array of URLs.\n\tpendOids := []string{}\n\tfor _, oid := range oldOids {\n\t\to, err := db.GetOrder(ctx, oid)\n\t\tif err != nil {\n\t\t\treturn nil, acme.WrapErrorISE(err, \"error loading order %s for account %s\", oid, accID)\n\t\t}\n\t\tif err = o.UpdateStatus(ctx, db); err != nil {\n\t\t\treturn nil, acme.WrapErrorISE(err, \"error updating order %s for account %s\", oid, accID)\n\t\t}\n\n\t\tif o.Status == acme.StatusPending || (o.Status == acme.StatusReady && includeReadyOrders) {\n\t\t\tpendOids = append(pendOids, oid)\n\t\t}\n\t}\n\tpendOids = append(pendOids, addOids...)\n\tvar (\n\t\t_old interface{} = oldOids\n\t\t_new interface{} = pendOids\n\t)\n\tswitch {\n\tcase len(oldOids) == 0 && len(pendOids) == 0:\n\t\t// If list has not changed from empty, then no need to write the DB.\n\t\treturn []string{}, nil\n\tcase len(oldOids) == 0:\n\t\t_old = nil\n\tcase len(pendOids) == 0:\n\t\t_new = nil\n\t}\n\tif err = db.save(ctx, accID, _new, _old, \"orderIDsByAccountID\", ordersByAccountIDTable); err != nil {\n\t\t// Delete all orders that may have been previously stored if orderIDsByAccountID update fails.\n\t\tfor _, oid := range addOids {\n\t\t\t// Ignore error from delete -- we tried our best.\n\t\t\t// TODO when we have logging w/ request ID tracking, logging this error.\n\t\t\tdb.db.Del(orderTable, []byte(oid))\n\t\t}\n\t\treturn nil, errors.Wrapf(err, \"error saving orderIDs index for account %s\", accID)\n\t}\n\treturn pendOids, nil\n}\n\n// GetOrdersByAccountID returns a list of order IDs owned by the account.\nfunc (db *DB) GetOrdersByAccountID(ctx context.Context, accID string) ([]string, error) {\n\treturn db.updateAddOrderIDs(ctx, accID, false)\n}\n\n// GetAllOrdersByAccountID returns a list of any order IDs owned by the account.\nfunc (db *DB) GetAllOrdersByAccountID(ctx context.Context, accID string) ([]string, error) {\n\treturn db.updateAddOrderIDs(ctx, accID, true)\n}\n"
  },
  {
    "path": "acme/db/nosql/order_test.go",
    "content": "package nosql\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"reflect\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n\t\"github.com/smallstep/assert\"\n\t\"github.com/smallstep/certificates/acme\"\n\t\"github.com/smallstep/certificates/db\"\n\t\"github.com/smallstep/nosql\"\n\t\"github.com/smallstep/nosql/database\"\n)\n\nfunc TestDB_getDBOrder(t *testing.T) {\n\torderID := \"orderID\"\n\ttype test struct {\n\t\tdb      nosql.DB\n\t\terr     error\n\t\tacmeErr *acme.Error\n\t\tdbo     *dbOrder\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/not-found\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, orderTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), orderID)\n\n\t\t\t\t\t\treturn nil, database.ErrNotFound\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tacmeErr: acme.NewError(acme.ErrorMalformedType, \"order orderID not found\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/db.Get-error\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, orderTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), orderID)\n\n\t\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"error loading order orderID: force\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/unmarshal-error\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, orderTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), orderID)\n\n\t\t\t\t\t\treturn []byte(\"foo\"), nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"error unmarshaling order orderID into dbOrder\"),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tnow := clock.Now()\n\t\t\tdbo := &dbOrder{\n\t\t\t\tID:            orderID,\n\t\t\t\tAccountID:     \"accID\",\n\t\t\t\tProvisionerID: \"provID\",\n\t\t\t\tCertificateID: \"certID\",\n\t\t\t\tStatus:        acme.StatusValid,\n\t\t\t\tExpiresAt:     now,\n\t\t\t\tCreatedAt:     now,\n\t\t\t\tNotBefore:     now,\n\t\t\t\tNotAfter:      now,\n\t\t\t\tIdentifiers: []acme.Identifier{\n\t\t\t\t\t{Type: \"dns\", Value: \"test.ca.smallstep.com\"},\n\t\t\t\t\t{Type: \"dns\", Value: \"example.foo.com\"},\n\t\t\t\t},\n\t\t\t\tAuthorizationIDs: []string{\"foo\", \"bar\"},\n\t\t\t\tError:            acme.NewError(acme.ErrorMalformedType, \"The request message was malformed\"),\n\t\t\t}\n\t\t\tb, err := json.Marshal(dbo)\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, orderTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), orderID)\n\n\t\t\t\t\t\treturn b, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tdbo: dbo,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\ttc := run(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\td := DB{db: tc.db}\n\t\t\tif dbo, err := d.getDBOrder(context.Background(), orderID); err != nil {\n\t\t\t\tvar ae *acme.Error\n\t\t\t\tif errors.As(err, &ae) {\n\t\t\t\t\tif assert.NotNil(t, tc.acmeErr) {\n\t\t\t\t\t\tassert.Equals(t, ae.Type, tc.acmeErr.Type)\n\t\t\t\t\t\tassert.Equals(t, ae.Detail, tc.acmeErr.Detail)\n\t\t\t\t\t\tassert.Equals(t, ae.Status, tc.acmeErr.Status)\n\t\t\t\t\t\tassert.Equals(t, ae.Err.Error(), tc.acmeErr.Err.Error())\n\t\t\t\t\t\tassert.Equals(t, ae.Detail, tc.acmeErr.Detail)\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.err.Error())\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else if assert.Nil(t, tc.err) {\n\t\t\t\tassert.Equals(t, dbo.ID, tc.dbo.ID)\n\t\t\t\tassert.Equals(t, dbo.ProvisionerID, tc.dbo.ProvisionerID)\n\t\t\t\tassert.Equals(t, dbo.CertificateID, tc.dbo.CertificateID)\n\t\t\t\tassert.Equals(t, dbo.Status, tc.dbo.Status)\n\t\t\t\tassert.Equals(t, dbo.CreatedAt, tc.dbo.CreatedAt)\n\t\t\t\tassert.Equals(t, dbo.ExpiresAt, tc.dbo.ExpiresAt)\n\t\t\t\tassert.Equals(t, dbo.NotBefore, tc.dbo.NotBefore)\n\t\t\t\tassert.Equals(t, dbo.NotAfter, tc.dbo.NotAfter)\n\t\t\t\tassert.Equals(t, dbo.Identifiers, tc.dbo.Identifiers)\n\t\t\t\tassert.Equals(t, dbo.AuthorizationIDs, tc.dbo.AuthorizationIDs)\n\t\t\t\tassert.Equals(t, dbo.Error.Error(), tc.dbo.Error.Error())\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDB_GetOrder(t *testing.T) {\n\torderID := \"orderID\"\n\ttype test struct {\n\t\tdb      nosql.DB\n\t\terr     error\n\t\tacmeErr *acme.Error\n\t\tdbo     *dbOrder\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/db.Get-error\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, orderTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), orderID)\n\n\t\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"error loading order orderID: force\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/forward-acme-error\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, orderTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), orderID)\n\n\t\t\t\t\t\treturn nil, database.ErrNotFound\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tacmeErr: acme.NewError(acme.ErrorMalformedType, \"order orderID not found\"),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tnow := clock.Now()\n\t\t\tdbo := &dbOrder{\n\t\t\t\tID:            orderID,\n\t\t\t\tAccountID:     \"accID\",\n\t\t\t\tProvisionerID: \"provID\",\n\t\t\t\tCertificateID: \"certID\",\n\t\t\t\tStatus:        acme.StatusValid,\n\t\t\t\tExpiresAt:     now,\n\t\t\t\tCreatedAt:     now,\n\t\t\t\tNotBefore:     now,\n\t\t\t\tNotAfter:      now,\n\t\t\t\tIdentifiers: []acme.Identifier{\n\t\t\t\t\t{Type: \"dns\", Value: \"test.ca.smallstep.com\"},\n\t\t\t\t\t{Type: \"dns\", Value: \"example.foo.com\"},\n\t\t\t\t},\n\t\t\t\tAuthorizationIDs: []string{\"foo\", \"bar\"},\n\t\t\t\tError:            acme.NewError(acme.ErrorMalformedType, \"The request message was malformed\"),\n\t\t\t}\n\t\t\tb, err := json.Marshal(dbo)\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, orderTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), orderID)\n\t\t\t\t\t\treturn b, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tdbo: dbo,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\ttc := run(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\td := DB{db: tc.db}\n\t\t\tif o, err := d.GetOrder(context.Background(), orderID); err != nil {\n\t\t\t\tvar ae *acme.Error\n\t\t\t\tif errors.As(err, &ae) {\n\t\t\t\t\tif assert.NotNil(t, tc.acmeErr) {\n\t\t\t\t\t\tassert.Equals(t, ae.Type, tc.acmeErr.Type)\n\t\t\t\t\t\tassert.Equals(t, ae.Detail, tc.acmeErr.Detail)\n\t\t\t\t\t\tassert.Equals(t, ae.Status, tc.acmeErr.Status)\n\t\t\t\t\t\tassert.Equals(t, ae.Err.Error(), tc.acmeErr.Err.Error())\n\t\t\t\t\t\tassert.Equals(t, ae.Detail, tc.acmeErr.Detail)\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.err.Error())\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else if assert.Nil(t, tc.err) {\n\t\t\t\tassert.Equals(t, o.ID, tc.dbo.ID)\n\t\t\t\tassert.Equals(t, o.AccountID, tc.dbo.AccountID)\n\t\t\t\tassert.Equals(t, o.ProvisionerID, tc.dbo.ProvisionerID)\n\t\t\t\tassert.Equals(t, o.CertificateID, tc.dbo.CertificateID)\n\t\t\t\tassert.Equals(t, o.Status, tc.dbo.Status)\n\t\t\t\tassert.Equals(t, o.ExpiresAt, tc.dbo.ExpiresAt)\n\t\t\t\tassert.Equals(t, o.NotBefore, tc.dbo.NotBefore)\n\t\t\t\tassert.Equals(t, o.NotAfter, tc.dbo.NotAfter)\n\t\t\t\tassert.Equals(t, o.Identifiers, tc.dbo.Identifiers)\n\t\t\t\tassert.Equals(t, o.AuthorizationIDs, tc.dbo.AuthorizationIDs)\n\t\t\t\tassert.Equals(t, o.Error.Error(), tc.dbo.Error.Error())\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDB_UpdateOrder(t *testing.T) {\n\torderID := \"orderID\"\n\tnow := clock.Now()\n\tdbo := &dbOrder{\n\t\tID:            orderID,\n\t\tAccountID:     \"accID\",\n\t\tProvisionerID: \"provID\",\n\t\tStatus:        acme.StatusPending,\n\t\tExpiresAt:     now,\n\t\tCreatedAt:     now,\n\t\tNotBefore:     now,\n\t\tNotAfter:      now,\n\t\tIdentifiers: []acme.Identifier{\n\t\t\t{Type: \"dns\", Value: \"test.ca.smallstep.com\"},\n\t\t\t{Type: \"dns\", Value: \"example.foo.com\"},\n\t\t},\n\t\tAuthorizationIDs: []string{\"foo\", \"bar\"},\n\t}\n\tb, err := json.Marshal(dbo)\n\tassert.FatalError(t, err)\n\ttype test struct {\n\t\tdb  nosql.DB\n\t\to   *acme.Order\n\t\terr error\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/db.Get-error\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\to: &acme.Order{\n\t\t\t\t\tID: orderID,\n\t\t\t\t},\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, orderTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), orderID)\n\n\t\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"error loading order orderID: force\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/save-error\": func(t *testing.T) test {\n\t\t\to := &acme.Order{\n\t\t\t\tID:            orderID,\n\t\t\t\tStatus:        acme.StatusValid,\n\t\t\t\tCertificateID: \"certID\",\n\t\t\t\tError:         acme.NewError(acme.ErrorMalformedType, \"The request message was malformed\"),\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\to: o,\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, orderTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), orderID)\n\n\t\t\t\t\t\treturn b, nil\n\t\t\t\t\t},\n\t\t\t\t\tMCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, orderTable)\n\t\t\t\t\t\tassert.Equals(t, old, b)\n\n\t\t\t\t\t\tdbNew := new(dbOrder)\n\t\t\t\t\t\tassert.FatalError(t, json.Unmarshal(nu, dbNew))\n\t\t\t\t\t\tassert.Equals(t, dbNew.ID, dbo.ID)\n\t\t\t\t\t\tassert.Equals(t, dbNew.AccountID, dbo.AccountID)\n\t\t\t\t\t\tassert.Equals(t, dbNew.ProvisionerID, dbo.ProvisionerID)\n\t\t\t\t\t\tassert.Equals(t, dbNew.CertificateID, o.CertificateID)\n\t\t\t\t\t\tassert.Equals(t, dbNew.Status, o.Status)\n\t\t\t\t\t\tassert.Equals(t, dbNew.CreatedAt, dbo.CreatedAt)\n\t\t\t\t\t\tassert.Equals(t, dbNew.ExpiresAt, dbo.ExpiresAt)\n\t\t\t\t\t\tassert.Equals(t, dbNew.NotBefore, dbo.NotBefore)\n\t\t\t\t\t\tassert.Equals(t, dbNew.NotAfter, dbo.NotAfter)\n\t\t\t\t\t\tassert.Equals(t, dbNew.AuthorizationIDs, dbo.AuthorizationIDs)\n\t\t\t\t\t\tassert.Equals(t, dbNew.Identifiers, dbo.Identifiers)\n\t\t\t\t\t\tassert.Equals(t, dbNew.Error.Error(), o.Error.Error())\n\t\t\t\t\t\treturn nil, false, errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"error saving acme order: force\"),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\to := &acme.Order{\n\t\t\t\tID:            orderID,\n\t\t\t\tStatus:        acme.StatusValid,\n\t\t\t\tCertificateID: \"certID\",\n\t\t\t\tError:         acme.NewError(acme.ErrorMalformedType, \"The request message was malformed\"),\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\to: o,\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, orderTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), orderID)\n\n\t\t\t\t\t\treturn b, nil\n\t\t\t\t\t},\n\t\t\t\t\tMCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, orderTable)\n\t\t\t\t\t\tassert.Equals(t, old, b)\n\n\t\t\t\t\t\tdbNew := new(dbOrder)\n\t\t\t\t\t\tassert.FatalError(t, json.Unmarshal(nu, dbNew))\n\t\t\t\t\t\tassert.Equals(t, dbNew.ID, dbo.ID)\n\t\t\t\t\t\tassert.Equals(t, dbNew.AccountID, dbo.AccountID)\n\t\t\t\t\t\tassert.Equals(t, dbNew.ProvisionerID, dbo.ProvisionerID)\n\t\t\t\t\t\tassert.Equals(t, dbNew.CertificateID, o.CertificateID)\n\t\t\t\t\t\tassert.Equals(t, dbNew.Status, o.Status)\n\t\t\t\t\t\tassert.Equals(t, dbNew.CreatedAt, dbo.CreatedAt)\n\t\t\t\t\t\tassert.Equals(t, dbNew.ExpiresAt, dbo.ExpiresAt)\n\t\t\t\t\t\tassert.Equals(t, dbNew.NotBefore, dbo.NotBefore)\n\t\t\t\t\t\tassert.Equals(t, dbNew.NotAfter, dbo.NotAfter)\n\t\t\t\t\t\tassert.Equals(t, dbNew.AuthorizationIDs, dbo.AuthorizationIDs)\n\t\t\t\t\t\tassert.Equals(t, dbNew.Identifiers, dbo.Identifiers)\n\t\t\t\t\t\tassert.Equals(t, dbNew.Error.Error(), o.Error.Error())\n\t\t\t\t\t\treturn nu, true, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\ttc := run(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\td := DB{db: tc.db}\n\t\t\tif err := d.UpdateOrder(context.Background(), tc.o); err != nil {\n\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.err.Error())\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif assert.Nil(t, tc.err) {\n\t\t\t\t\tassert.Equals(t, tc.o.ID, dbo.ID)\n\t\t\t\t\tassert.Equals(t, tc.o.CertificateID, \"certID\")\n\t\t\t\t\tassert.Equals(t, tc.o.Status, acme.StatusValid)\n\t\t\t\t\tassert.Equals(t, tc.o.Error.Error(), acme.NewError(acme.ErrorMalformedType, \"The request message was malformed\").Error())\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDB_CreateOrder(t *testing.T) {\n\tnow := clock.Now()\n\tnbf := now.Add(5 * time.Minute)\n\tnaf := now.Add(15 * time.Minute)\n\ttype test struct {\n\t\tdb  nosql.DB\n\t\to   *acme.Order\n\t\terr error\n\t\t_id *string\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/order-save-error\": func(t *testing.T) test {\n\t\t\to := &acme.Order{\n\t\t\t\tAccountID:     \"accID\",\n\t\t\t\tProvisionerID: \"provID\",\n\t\t\t\tCertificateID: \"certID\",\n\t\t\t\tStatus:        acme.StatusValid,\n\t\t\t\tExpiresAt:     now,\n\t\t\t\tNotBefore:     nbf,\n\t\t\t\tNotAfter:      naf,\n\t\t\t\tIdentifiers: []acme.Identifier{\n\t\t\t\t\t{Type: \"dns\", Value: \"test.ca.smallstep.com\"},\n\t\t\t\t\t{Type: \"dns\", Value: \"example.foo.com\"},\n\t\t\t\t},\n\t\t\t\tAuthorizationIDs: []string{\"foo\", \"bar\"},\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {\n\t\t\t\t\t\tassert.Equals(t, string(bucket), string(orderTable))\n\t\t\t\t\t\tassert.Equals(t, string(key), o.ID)\n\t\t\t\t\t\tassert.Equals(t, old, nil)\n\n\t\t\t\t\t\tdbo := new(dbOrder)\n\t\t\t\t\t\tassert.FatalError(t, json.Unmarshal(nu, dbo))\n\t\t\t\t\t\tassert.Equals(t, dbo.ID, o.ID)\n\t\t\t\t\t\tassert.Equals(t, dbo.AccountID, o.AccountID)\n\t\t\t\t\t\tassert.Equals(t, dbo.ProvisionerID, o.ProvisionerID)\n\t\t\t\t\t\tassert.Equals(t, dbo.CertificateID, \"\")\n\t\t\t\t\t\tassert.Equals(t, dbo.Status, o.Status)\n\t\t\t\t\t\tassert.True(t, dbo.CreatedAt.Add(-time.Minute).Before(now))\n\t\t\t\t\t\tassert.True(t, dbo.CreatedAt.Add(time.Minute).After(now))\n\t\t\t\t\t\tassert.Equals(t, dbo.ExpiresAt, o.ExpiresAt)\n\t\t\t\t\t\tassert.Equals(t, dbo.NotBefore, o.NotBefore)\n\t\t\t\t\t\tassert.Equals(t, dbo.NotAfter, o.NotAfter)\n\t\t\t\t\t\tassert.Equals(t, dbo.AuthorizationIDs, o.AuthorizationIDs)\n\t\t\t\t\t\tassert.Equals(t, dbo.Identifiers, o.Identifiers)\n\t\t\t\t\t\tassert.Equals(t, dbo.Error, nil)\n\t\t\t\t\t\treturn nil, false, errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\to:   o,\n\t\t\t\terr: errors.New(\"error saving acme order: force\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/orderIDsByOrderUpdate-error\": func(t *testing.T) test {\n\t\t\to := &acme.Order{\n\t\t\t\tAccountID:     \"accID\",\n\t\t\t\tProvisionerID: \"provID\",\n\t\t\t\tCertificateID: \"certID\",\n\t\t\t\tStatus:        acme.StatusValid,\n\t\t\t\tExpiresAt:     now,\n\t\t\t\tNotBefore:     nbf,\n\t\t\t\tNotAfter:      naf,\n\t\t\t\tIdentifiers: []acme.Identifier{\n\t\t\t\t\t{Type: \"dns\", Value: \"test.ca.smallstep.com\"},\n\t\t\t\t\t{Type: \"dns\", Value: \"example.foo.com\"},\n\t\t\t\t},\n\t\t\t\tAuthorizationIDs: []string{\"foo\", \"bar\"},\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, string(bucket), string(ordersByAccountIDTable))\n\t\t\t\t\t\tassert.Equals(t, string(key), o.AccountID)\n\t\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t\tMCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {\n\t\t\t\t\t\tassert.Equals(t, string(bucket), string(orderTable))\n\t\t\t\t\t\tassert.Equals(t, string(key), o.ID)\n\t\t\t\t\t\tassert.Equals(t, old, nil)\n\n\t\t\t\t\t\tdbo := new(dbOrder)\n\t\t\t\t\t\tassert.FatalError(t, json.Unmarshal(nu, dbo))\n\t\t\t\t\t\tassert.Equals(t, dbo.ID, o.ID)\n\t\t\t\t\t\tassert.Equals(t, dbo.AccountID, o.AccountID)\n\t\t\t\t\t\tassert.Equals(t, dbo.ProvisionerID, o.ProvisionerID)\n\t\t\t\t\t\tassert.Equals(t, dbo.CertificateID, \"\")\n\t\t\t\t\t\tassert.Equals(t, dbo.Status, o.Status)\n\t\t\t\t\t\tassert.True(t, dbo.CreatedAt.Add(-time.Minute).Before(now))\n\t\t\t\t\t\tassert.True(t, dbo.CreatedAt.Add(time.Minute).After(now))\n\t\t\t\t\t\tassert.Equals(t, dbo.ExpiresAt, o.ExpiresAt)\n\t\t\t\t\t\tassert.Equals(t, dbo.NotBefore, o.NotBefore)\n\t\t\t\t\t\tassert.Equals(t, dbo.NotAfter, o.NotAfter)\n\t\t\t\t\t\tassert.Equals(t, dbo.AuthorizationIDs, o.AuthorizationIDs)\n\t\t\t\t\t\tassert.Equals(t, dbo.Identifiers, o.Identifiers)\n\t\t\t\t\t\tassert.Equals(t, dbo.Error, nil)\n\t\t\t\t\t\treturn nu, true, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\to:   o,\n\t\t\t\terr: errors.New(\"error loading orderIDs for account accID: force\"),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tvar (\n\t\t\t\tid    string\n\t\t\t\tidptr = &id\n\t\t\t)\n\n\t\t\to := &acme.Order{\n\t\t\t\tAccountID:     \"accID\",\n\t\t\t\tProvisionerID: \"provID\",\n\t\t\t\tStatus:        acme.StatusValid,\n\t\t\t\tExpiresAt:     now,\n\t\t\t\tNotBefore:     nbf,\n\t\t\t\tNotAfter:      naf,\n\t\t\t\tIdentifiers: []acme.Identifier{\n\t\t\t\t\t{Type: \"dns\", Value: \"test.ca.smallstep.com\"},\n\t\t\t\t\t{Type: \"dns\", Value: \"example.foo.com\"},\n\t\t\t\t},\n\t\t\t\tAuthorizationIDs: []string{\"foo\", \"bar\"},\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, string(bucket), string(ordersByAccountIDTable))\n\t\t\t\t\t\tassert.Equals(t, string(key), o.AccountID)\n\t\t\t\t\t\treturn nil, database.ErrNotFound\n\t\t\t\t\t},\n\t\t\t\t\tMCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {\n\t\t\t\t\t\tswitch string(bucket) {\n\t\t\t\t\t\tcase string(ordersByAccountIDTable):\n\t\t\t\t\t\t\tb, err := json.Marshal([]string{o.ID})\n\t\t\t\t\t\t\tassert.FatalError(t, err)\n\t\t\t\t\t\t\tassert.Equals(t, string(key), \"accID\")\n\t\t\t\t\t\t\tassert.Equals(t, old, nil)\n\t\t\t\t\t\t\tassert.Equals(t, nu, b)\n\t\t\t\t\t\t\treturn nu, true, nil\n\t\t\t\t\t\tcase string(orderTable):\n\t\t\t\t\t\t\t*idptr = string(key)\n\t\t\t\t\t\t\tassert.Equals(t, string(key), o.ID)\n\t\t\t\t\t\t\tassert.Equals(t, old, nil)\n\n\t\t\t\t\t\t\tdbo := new(dbOrder)\n\t\t\t\t\t\t\tassert.FatalError(t, json.Unmarshal(nu, dbo))\n\t\t\t\t\t\t\tassert.Equals(t, dbo.ID, o.ID)\n\t\t\t\t\t\t\tassert.Equals(t, dbo.AccountID, o.AccountID)\n\t\t\t\t\t\t\tassert.Equals(t, dbo.ProvisionerID, o.ProvisionerID)\n\t\t\t\t\t\t\tassert.Equals(t, dbo.CertificateID, \"\")\n\t\t\t\t\t\t\tassert.Equals(t, dbo.Status, o.Status)\n\t\t\t\t\t\t\tassert.True(t, dbo.CreatedAt.Add(-time.Minute).Before(now))\n\t\t\t\t\t\t\tassert.True(t, dbo.CreatedAt.Add(time.Minute).After(now))\n\t\t\t\t\t\t\tassert.Equals(t, dbo.ExpiresAt, o.ExpiresAt)\n\t\t\t\t\t\t\tassert.Equals(t, dbo.NotBefore, o.NotBefore)\n\t\t\t\t\t\t\tassert.Equals(t, dbo.NotAfter, o.NotAfter)\n\t\t\t\t\t\t\tassert.Equals(t, dbo.AuthorizationIDs, o.AuthorizationIDs)\n\t\t\t\t\t\t\tassert.Equals(t, dbo.Identifiers, o.Identifiers)\n\t\t\t\t\t\t\tassert.Equals(t, dbo.Error, nil)\n\t\t\t\t\t\t\treturn nu, true, nil\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\tassert.FatalError(t, errors.Errorf(\"unexpected bucket %s\", string(bucket)))\n\t\t\t\t\t\t\treturn nil, false, errors.New(\"force\")\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\to:   o,\n\t\t\t\t_id: idptr,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\ttc := run(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\td := DB{db: tc.db}\n\t\t\tif err := d.CreateOrder(context.Background(), tc.o); err != nil {\n\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.err.Error())\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif assert.Nil(t, tc.err) {\n\t\t\t\t\tassert.Equals(t, tc.o.ID, *tc._id)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDB_updateAddOrderIDs(t *testing.T) {\n\taccID := \"accID\"\n\ttype test struct {\n\t\tdb      nosql.DB\n\t\terr     error\n\t\tacmeErr *acme.Error\n\t\taddOids []string\n\t\tres     []string\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/db.Get-error\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, ordersByAccountIDTable)\n\t\t\t\t\t\tassert.Equals(t, key, []byte(accID))\n\t\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: errors.Errorf(\"error loading orderIDs for account %s\", accID),\n\t\t\t}\n\t\t},\n\t\t\"fail/unmarshal-error\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, ordersByAccountIDTable)\n\t\t\t\t\t\tassert.Equals(t, key, []byte(accID))\n\t\t\t\t\t\treturn []byte(\"foo\"), nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: errors.Errorf(\"error unmarshaling orderIDs for account %s\", accID),\n\t\t\t}\n\t\t},\n\t\t\"fail/db.Get-order-error\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tswitch string(bucket) {\n\t\t\t\t\t\tcase string(ordersByAccountIDTable):\n\t\t\t\t\t\t\tassert.Equals(t, key, []byte(accID))\n\t\t\t\t\t\t\tb, err := json.Marshal([]string{\"foo\", \"bar\"})\n\t\t\t\t\t\t\tassert.FatalError(t, err)\n\t\t\t\t\t\t\treturn b, nil\n\t\t\t\t\t\tcase string(orderTable):\n\t\t\t\t\t\t\tassert.Equals(t, key, []byte(\"foo\"))\n\t\t\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\tassert.FatalError(t, errors.Errorf(\"unexpected bucket %s\", string(bucket)))\n\t\t\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tacmeErr: acme.NewErrorISE(\"error loading order foo for account accID: error loading order foo: force\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/update-order-status-error\": func(t *testing.T) test {\n\t\t\texpiry := clock.Now().Add(-5 * time.Minute)\n\t\t\tofoo := &dbOrder{\n\t\t\t\tID:        \"foo\",\n\t\t\t\tStatus:    acme.StatusPending,\n\t\t\t\tExpiresAt: expiry,\n\t\t\t}\n\t\t\tbfoo, err := json.Marshal(ofoo)\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tswitch string(bucket) {\n\t\t\t\t\t\tcase string(ordersByAccountIDTable):\n\t\t\t\t\t\t\tassert.Equals(t, key, []byte(accID))\n\t\t\t\t\t\t\tb, err := json.Marshal([]string{\"foo\", \"bar\"})\n\t\t\t\t\t\t\tassert.FatalError(t, err)\n\t\t\t\t\t\t\treturn b, nil\n\t\t\t\t\t\tcase string(orderTable):\n\t\t\t\t\t\t\tassert.Equals(t, key, []byte(\"foo\"))\n\t\t\t\t\t\t\treturn bfoo, nil\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\tassert.FatalError(t, errors.Errorf(\"unexpected bucket %s\", string(bucket)))\n\t\t\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\tMCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, orderTable)\n\t\t\t\t\t\tassert.Equals(t, key, []byte(\"foo\"))\n\t\t\t\t\t\tassert.Equals(t, old, bfoo)\n\n\t\t\t\t\t\tnewdbo := new(dbOrder)\n\t\t\t\t\t\tassert.FatalError(t, json.Unmarshal(nu, newdbo))\n\t\t\t\t\t\tassert.Equals(t, newdbo.ID, \"foo\")\n\t\t\t\t\t\tassert.Equals(t, newdbo.Status, acme.StatusInvalid)\n\t\t\t\t\t\tassert.Equals(t, newdbo.ExpiresAt, expiry)\n\t\t\t\t\t\tassert.Equals(t, newdbo.Error.Error(), acme.NewError(acme.ErrorMalformedType, \"The request message was malformed\").Error())\n\t\t\t\t\t\treturn nil, false, errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tacmeErr: acme.NewErrorISE(\"error updating order foo for account accID: error updating order: error saving acme order: force\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/db.save-order-error\": func(t *testing.T) test {\n\t\t\taddOids := []string{\"foo\", \"bar\"}\n\t\t\tb, err := json.Marshal(addOids)\n\t\t\tassert.FatalError(t, err)\n\t\t\tdelCount := 0\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, ordersByAccountIDTable)\n\t\t\t\t\t\tassert.Equals(t, key, []byte(accID))\n\t\t\t\t\t\treturn nil, database.ErrNotFound\n\t\t\t\t\t},\n\t\t\t\t\tMCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, ordersByAccountIDTable)\n\t\t\t\t\t\tassert.Equals(t, key, []byte(accID))\n\t\t\t\t\t\tassert.Equals(t, old, nil)\n\t\t\t\t\t\tassert.Equals(t, nu, b)\n\t\t\t\t\t\treturn nil, false, errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t\tMDel: func(bucket, key []byte) error {\n\t\t\t\t\t\tdelCount++\n\t\t\t\t\t\tswitch delCount {\n\t\t\t\t\t\tcase 1:\n\t\t\t\t\t\t\tassert.Equals(t, bucket, orderTable)\n\t\t\t\t\t\t\tassert.Equals(t, key, []byte(\"foo\"))\n\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\tcase 2:\n\t\t\t\t\t\t\tassert.Equals(t, bucket, orderTable)\n\t\t\t\t\t\t\tassert.Equals(t, key, []byte(\"bar\"))\n\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\tassert.FatalError(t, errors.New(\"delete should only be called twice\"))\n\t\t\t\t\t\t\treturn errors.New(\"force\")\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\taddOids: addOids,\n\t\t\t\terr:     errors.Errorf(\"error saving orderIDs index for account %s\", accID),\n\t\t\t}\n\t\t},\n\t\t\"ok/no-old\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tswitch string(bucket) {\n\t\t\t\t\t\tcase string(ordersByAccountIDTable):\n\t\t\t\t\t\t\treturn nil, database.ErrNotFound\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\tassert.FatalError(t, errors.Errorf(\"unexpected bucket %s\", string(bucket)))\n\t\t\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\tMCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {\n\t\t\t\t\t\tswitch string(bucket) {\n\t\t\t\t\t\tcase string(ordersByAccountIDTable):\n\t\t\t\t\t\t\tassert.Equals(t, key, []byte(accID))\n\t\t\t\t\t\t\tassert.Equals(t, old, nil)\n\t\t\t\t\t\t\tassert.Equals(t, nu, nil)\n\t\t\t\t\t\t\treturn nil, true, nil\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\tassert.FatalError(t, errors.Errorf(\"unexpected bucket %s\", string(bucket)))\n\t\t\t\t\t\t\treturn nil, false, errors.New(\"force\")\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tres: []string{},\n\t\t\t}\n\t\t},\n\t\t\"ok/all-old-not-pending\": func(t *testing.T) test {\n\t\t\toldOids := []string{\"foo\", \"bar\"}\n\t\t\tbOldOids, err := json.Marshal(oldOids)\n\t\t\tassert.FatalError(t, err)\n\t\t\texpiry := clock.Now().Add(-5 * time.Minute)\n\t\t\tofoo := &dbOrder{\n\t\t\t\tID:        \"foo\",\n\t\t\t\tStatus:    acme.StatusPending,\n\t\t\t\tExpiresAt: expiry,\n\t\t\t}\n\t\t\tbfoo, err := json.Marshal(ofoo)\n\t\t\tassert.FatalError(t, err)\n\t\t\tobar := &dbOrder{\n\t\t\t\tID:        \"bar\",\n\t\t\t\tStatus:    acme.StatusPending,\n\t\t\t\tExpiresAt: expiry,\n\t\t\t}\n\t\t\tbbar, err := json.Marshal(obar)\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tswitch string(bucket) {\n\t\t\t\t\t\tcase string(ordersByAccountIDTable):\n\t\t\t\t\t\t\treturn bOldOids, nil\n\t\t\t\t\t\tcase string(orderTable):\n\t\t\t\t\t\t\tswitch string(key) {\n\t\t\t\t\t\t\tcase \"foo\":\n\t\t\t\t\t\t\t\tassert.Equals(t, key, []byte(\"foo\"))\n\t\t\t\t\t\t\t\treturn bfoo, nil\n\t\t\t\t\t\t\tcase \"bar\":\n\t\t\t\t\t\t\t\tassert.Equals(t, key, []byte(\"bar\"))\n\t\t\t\t\t\t\t\treturn bbar, nil\n\t\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\t\tassert.FatalError(t, errors.Errorf(\"unexpected key %s\", string(key)))\n\t\t\t\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\tassert.FatalError(t, errors.Errorf(\"unexpected bucket %s\", string(bucket)))\n\t\t\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\tMCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {\n\t\t\t\t\t\tswitch string(bucket) {\n\t\t\t\t\t\tcase string(orderTable):\n\t\t\t\t\t\t\treturn nil, true, nil\n\t\t\t\t\t\tcase string(ordersByAccountIDTable):\n\t\t\t\t\t\t\tassert.Equals(t, key, []byte(accID))\n\t\t\t\t\t\t\tassert.Equals(t, old, bOldOids)\n\t\t\t\t\t\t\tassert.Equals(t, nu, nil)\n\t\t\t\t\t\t\treturn nil, true, nil\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\tassert.FatalError(t, errors.Errorf(\"unexpected bucket %s\", string(bucket)))\n\t\t\t\t\t\t\treturn nil, false, errors.New(\"force\")\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tres: []string{},\n\t\t\t}\n\t\t},\n\t\t\"ok/old-and-new\": func(t *testing.T) test {\n\t\t\toldOids := []string{\"foo\", \"bar\"}\n\t\t\tbOldOids, err := json.Marshal(oldOids)\n\t\t\tassert.FatalError(t, err)\n\t\t\taddOids := []string{\"zap\", \"zar\"}\n\t\t\tbAddOids, err := json.Marshal(addOids)\n\t\t\tassert.FatalError(t, err)\n\t\t\texpiry := clock.Now().Add(-5 * time.Minute)\n\t\t\tofoo := &dbOrder{\n\t\t\t\tID:        \"foo\",\n\t\t\t\tStatus:    acme.StatusPending,\n\t\t\t\tExpiresAt: expiry,\n\t\t\t}\n\t\t\tbfoo, err := json.Marshal(ofoo)\n\t\t\tassert.FatalError(t, err)\n\t\t\tobar := &dbOrder{\n\t\t\t\tID:        \"bar\",\n\t\t\t\tStatus:    acme.StatusPending,\n\t\t\t\tExpiresAt: expiry,\n\t\t\t}\n\t\t\tbbar, err := json.Marshal(obar)\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tswitch string(bucket) {\n\t\t\t\t\t\tcase string(ordersByAccountIDTable):\n\t\t\t\t\t\t\treturn bOldOids, nil\n\t\t\t\t\t\tcase string(orderTable):\n\t\t\t\t\t\t\tswitch string(key) {\n\t\t\t\t\t\t\tcase \"foo\":\n\t\t\t\t\t\t\t\tassert.Equals(t, key, []byte(\"foo\"))\n\t\t\t\t\t\t\t\treturn bfoo, nil\n\t\t\t\t\t\t\tcase \"bar\":\n\t\t\t\t\t\t\t\tassert.Equals(t, key, []byte(\"bar\"))\n\t\t\t\t\t\t\t\treturn bbar, nil\n\t\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\t\tassert.FatalError(t, errors.Errorf(\"unexpected key %s\", string(key)))\n\t\t\t\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\tassert.FatalError(t, errors.Errorf(\"unexpected bucket %s\", string(bucket)))\n\t\t\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\tMCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {\n\t\t\t\t\t\tswitch string(bucket) {\n\t\t\t\t\t\tcase string(orderTable):\n\t\t\t\t\t\t\treturn nil, true, nil\n\t\t\t\t\t\tcase string(ordersByAccountIDTable):\n\t\t\t\t\t\t\tassert.Equals(t, key, []byte(accID))\n\t\t\t\t\t\t\tassert.Equals(t, old, bOldOids)\n\t\t\t\t\t\t\tassert.Equals(t, nu, bAddOids)\n\t\t\t\t\t\t\treturn nil, true, nil\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\tassert.FatalError(t, errors.Errorf(\"unexpected bucket %s\", string(bucket)))\n\t\t\t\t\t\t\treturn nil, false, errors.New(\"force\")\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\taddOids: addOids,\n\t\t\t\tres:     addOids,\n\t\t\t}\n\t\t},\n\t\t\"ok/old-and-new-2\": func(t *testing.T) test {\n\t\t\toldOids := []string{\"foo\", \"bar\", \"baz\"}\n\t\t\tbOldOids, err := json.Marshal(oldOids)\n\t\t\tassert.FatalError(t, err)\n\t\t\taddOids := []string{\"zap\", \"zar\"}\n\t\t\tnow := clock.Now()\n\t\t\tmin5 := now.Add(5 * time.Minute)\n\t\t\texpiry := now.Add(-5 * time.Minute)\n\n\t\t\to1 := &dbOrder{\n\t\t\t\tID:               \"foo\",\n\t\t\t\tStatus:           acme.StatusPending,\n\t\t\t\tExpiresAt:        min5,\n\t\t\t\tAuthorizationIDs: []string{\"a\"},\n\t\t\t}\n\t\t\tbo1, err := json.Marshal(o1)\n\t\t\tassert.FatalError(t, err)\n\t\t\to2 := &dbOrder{\n\t\t\t\tID:        \"bar\",\n\t\t\t\tStatus:    acme.StatusPending,\n\t\t\t\tExpiresAt: expiry,\n\t\t\t}\n\t\t\tbo2, err := json.Marshal(o2)\n\t\t\tassert.FatalError(t, err)\n\t\t\to3 := &dbOrder{\n\t\t\t\tID:               \"baz\",\n\t\t\t\tStatus:           acme.StatusPending,\n\t\t\t\tExpiresAt:        min5,\n\t\t\t\tAuthorizationIDs: []string{\"b\"},\n\t\t\t}\n\t\t\tbo3, err := json.Marshal(o3)\n\t\t\tassert.FatalError(t, err)\n\n\t\t\taz1 := &dbAuthz{\n\t\t\t\tID:           \"a\",\n\t\t\t\tStatus:       acme.StatusPending,\n\t\t\t\tExpiresAt:    min5,\n\t\t\t\tChallengeIDs: []string{\"aa\"},\n\t\t\t}\n\t\t\tbaz1, err := json.Marshal(az1)\n\t\t\tassert.FatalError(t, err)\n\t\t\taz2 := &dbAuthz{\n\t\t\t\tID:           \"b\",\n\t\t\t\tStatus:       acme.StatusPending,\n\t\t\t\tExpiresAt:    min5,\n\t\t\t\tChallengeIDs: []string{\"bb\"},\n\t\t\t}\n\t\t\tbaz2, err := json.Marshal(az2)\n\t\t\tassert.FatalError(t, err)\n\n\t\t\tch1 := &dbChallenge{\n\t\t\t\tID:     \"aa\",\n\t\t\t\tStatus: acme.StatusPending,\n\t\t\t}\n\t\t\tbch1, err := json.Marshal(ch1)\n\t\t\tassert.FatalError(t, err)\n\t\t\tch2 := &dbChallenge{\n\t\t\t\tID:     \"bb\",\n\t\t\t\tStatus: acme.StatusPending,\n\t\t\t}\n\t\t\tbch2, err := json.Marshal(ch2)\n\t\t\tassert.FatalError(t, err)\n\n\t\t\tnewOids := append([]string{\"foo\", \"baz\"}, addOids...)\n\t\t\tbNewOids, err := json.Marshal(newOids)\n\t\t\tassert.FatalError(t, err)\n\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tswitch string(bucket) {\n\t\t\t\t\t\tcase string(authzTable):\n\t\t\t\t\t\t\tswitch string(key) {\n\t\t\t\t\t\t\tcase \"a\":\n\t\t\t\t\t\t\t\treturn baz1, nil\n\t\t\t\t\t\t\tcase \"b\":\n\t\t\t\t\t\t\t\treturn baz2, nil\n\t\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\t\tassert.FatalError(t, errors.Errorf(\"unexpected authz key %s\", string(key)))\n\t\t\t\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\tcase string(challengeTable):\n\t\t\t\t\t\t\tswitch string(key) {\n\t\t\t\t\t\t\tcase \"aa\":\n\t\t\t\t\t\t\t\treturn bch1, nil\n\t\t\t\t\t\t\tcase \"bb\":\n\t\t\t\t\t\t\t\treturn bch2, nil\n\t\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\t\tassert.FatalError(t, errors.Errorf(\"unexpected challenge key %s\", string(key)))\n\t\t\t\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\tcase string(ordersByAccountIDTable):\n\t\t\t\t\t\t\treturn bOldOids, nil\n\t\t\t\t\t\tcase string(orderTable):\n\t\t\t\t\t\t\tswitch string(key) {\n\t\t\t\t\t\t\tcase \"foo\":\n\t\t\t\t\t\t\t\treturn bo1, nil\n\t\t\t\t\t\t\tcase \"bar\":\n\t\t\t\t\t\t\t\treturn bo2, nil\n\t\t\t\t\t\t\tcase \"baz\":\n\t\t\t\t\t\t\t\treturn bo3, nil\n\t\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\t\tassert.FatalError(t, errors.Errorf(\"unexpected key %s\", string(key)))\n\t\t\t\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\tassert.FatalError(t, errors.Errorf(\"unexpected bucket %s\", string(bucket)))\n\t\t\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\tMCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {\n\t\t\t\t\t\tswitch string(bucket) {\n\t\t\t\t\t\tcase string(orderTable):\n\t\t\t\t\t\t\treturn nil, true, nil\n\t\t\t\t\t\tcase string(ordersByAccountIDTable):\n\t\t\t\t\t\t\tassert.Equals(t, key, []byte(accID))\n\t\t\t\t\t\t\tassert.Equals(t, old, bOldOids)\n\t\t\t\t\t\t\tassert.Equals(t, nu, bNewOids)\n\t\t\t\t\t\t\treturn nil, true, nil\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\tassert.FatalError(t, errors.Errorf(\"unexpected bucket %s\", string(bucket)))\n\t\t\t\t\t\t\treturn nil, false, errors.New(\"force\")\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\taddOids: addOids,\n\t\t\t\tres:     newOids,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\ttc := run(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\td := DB{db: tc.db}\n\t\t\tvar (\n\t\t\t\tres []string\n\t\t\t\terr error\n\t\t\t)\n\t\t\tif tc.addOids == nil {\n\t\t\t\tres, err = d.updateAddOrderIDs(context.Background(), accID, false)\n\t\t\t} else {\n\t\t\t\tres, err = d.updateAddOrderIDs(context.Background(), accID, false, tc.addOids...)\n\t\t\t}\n\n\t\t\tif err != nil {\n\t\t\t\tvar ae *acme.Error\n\t\t\t\tif errors.As(err, &ae) {\n\t\t\t\t\tif assert.NotNil(t, tc.acmeErr) {\n\t\t\t\t\t\tassert.Equals(t, ae.Type, tc.acmeErr.Type)\n\t\t\t\t\t\tassert.Equals(t, ae.Detail, tc.acmeErr.Detail)\n\t\t\t\t\t\tassert.Equals(t, ae.Status, tc.acmeErr.Status)\n\t\t\t\t\t\tassert.Equals(t, ae.Err.Error(), tc.acmeErr.Err.Error())\n\t\t\t\t\t\tassert.Equals(t, ae.Detail, tc.acmeErr.Detail)\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.err.Error())\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else if assert.Nil(t, tc.err) {\n\t\t\t\tassert.True(t, reflect.DeepEqual(res, tc.res))\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "acme/db/nosql/wire.go",
    "content": "package nosql\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/smallstep/certificates/acme\"\n\t\"github.com/smallstep/nosql\"\n)\n\ntype dbDpopToken struct {\n\tID        string    `json:\"id\"`\n\tContent   []byte    `json:\"content\"`\n\tCreatedAt time.Time `json:\"createdAt\"`\n}\n\n// getDBDpopToken retrieves and unmarshals an DPoP type from the database.\nfunc (db *DB) getDBDpopToken(_ context.Context, orderID string) (*dbDpopToken, error) {\n\tb, err := db.db.Get(wireDpopTokenTable, []byte(orderID))\n\tif err != nil {\n\t\tif nosql.IsErrNotFound(err) {\n\t\t\treturn nil, acme.NewError(acme.ErrorMalformedType, \"dpop token %q not found\", orderID)\n\t\t}\n\t\treturn nil, fmt.Errorf(\"failed loading dpop token %q: %w\", orderID, err)\n\t}\n\n\td := new(dbDpopToken)\n\tif err := json.Unmarshal(b, d); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed unmarshaling dpop token %q into dbDpopToken: %w\", orderID, err)\n\t}\n\treturn d, nil\n}\n\n// GetDpopToken retrieves an DPoP from the database.\nfunc (db *DB) GetDpopToken(ctx context.Context, orderID string) (map[string]any, error) {\n\tdbDpop, err := db.getDBDpopToken(ctx, orderID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tdpop := make(map[string]any)\n\terr = json.Unmarshal(dbDpop.Content, &dpop)\n\n\treturn dpop, err\n}\n\n// CreateDpopToken creates DPoP resources and saves them to the DB.\nfunc (db *DB) CreateDpopToken(ctx context.Context, orderID string, dpop map[string]any) error {\n\tcontent, err := json.Marshal(dpop)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed marshaling dpop token: %w\", err)\n\t}\n\n\tnow := clock.Now()\n\tdbDpop := &dbDpopToken{\n\t\tID:        orderID,\n\t\tContent:   content,\n\t\tCreatedAt: now,\n\t}\n\tif err := db.save(ctx, orderID, dbDpop, nil, \"dpop\", wireDpopTokenTable); err != nil {\n\t\treturn fmt.Errorf(\"failed saving dpop token: %w\", err)\n\t}\n\treturn nil\n}\n\ntype dbOidcToken struct {\n\tID        string    `json:\"id\"`\n\tContent   []byte    `json:\"content\"`\n\tCreatedAt time.Time `json:\"createdAt\"`\n}\n\n// getDBOidcToken retrieves and unmarshals an OIDC id token type from the database.\nfunc (db *DB) getDBOidcToken(_ context.Context, orderID string) (*dbOidcToken, error) {\n\tb, err := db.db.Get(wireOidcTokenTable, []byte(orderID))\n\tif err != nil {\n\t\tif nosql.IsErrNotFound(err) {\n\t\t\treturn nil, acme.NewError(acme.ErrorMalformedType, \"oidc token %q not found\", orderID)\n\t\t}\n\t\treturn nil, fmt.Errorf(\"failed loading oidc token %q: %w\", orderID, err)\n\t}\n\n\to := new(dbOidcToken)\n\tif err := json.Unmarshal(b, o); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed unmarshaling oidc token %q into dbOidcToken: %w\", orderID, err)\n\t}\n\treturn o, nil\n}\n\n// GetOidcToken retrieves an oidc token from the database.\nfunc (db *DB) GetOidcToken(ctx context.Context, orderID string) (map[string]any, error) {\n\tdbOidc, err := db.getDBOidcToken(ctx, orderID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tidToken := make(map[string]any)\n\terr = json.Unmarshal(dbOidc.Content, &idToken)\n\n\treturn idToken, err\n}\n\n// CreateOidcToken creates oidc token resources and saves them to the DB.\nfunc (db *DB) CreateOidcToken(ctx context.Context, orderID string, idToken map[string]any) error {\n\tcontent, err := json.Marshal(idToken)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed marshaling oidc token: %w\", err)\n\t}\n\n\tnow := clock.Now()\n\tdbOidc := &dbOidcToken{\n\t\tID:        orderID,\n\t\tContent:   content,\n\t\tCreatedAt: now,\n\t}\n\tif err := db.save(ctx, orderID, dbOidc, nil, \"oidc\", wireOidcTokenTable); err != nil {\n\t\treturn fmt.Errorf(\"failed saving oidc token: %w\", err)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "acme/db/nosql/wire_test.go",
    "content": "package nosql\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/smallstep/certificates/acme\"\n\tcertificatesdb \"github.com/smallstep/certificates/db\"\n\t\"github.com/smallstep/nosql\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestDB_GetDpopToken(t *testing.T) {\n\ttype test struct {\n\t\tdb          *DB\n\t\torderID     string\n\t\texpected    map[string]any\n\t\texpectedErr error\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/acme-not-found\": func(t *testing.T) test {\n\t\t\tdir := t.TempDir()\n\t\t\tdb, err := nosql.New(\"badgerv2\", dir)\n\t\t\trequire.NoError(t, err)\n\t\t\treturn test{\n\t\t\t\tdb: &DB{\n\t\t\t\t\tdb: db,\n\t\t\t\t},\n\t\t\t\torderID: \"orderID\",\n\t\t\t\texpectedErr: &acme.Error{\n\t\t\t\t\tType:   \"urn:ietf:params:acme:error:malformed\",\n\t\t\t\t\tStatus: 400,\n\t\t\t\t\tDetail: \"The request message was malformed\",\n\t\t\t\t\tErr:    errors.New(`dpop token \"orderID\" not found`),\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"fail/unmarshal-error\": func(t *testing.T) test {\n\t\t\tdir := t.TempDir()\n\t\t\tdb, err := nosql.New(\"badgerv2\", dir)\n\t\t\trequire.NoError(t, err)\n\t\t\ttoken := dbDpopToken{\n\t\t\t\tID:        \"orderID\",\n\t\t\t\tContent:   []byte(\"{}\"),\n\t\t\t\tCreatedAt: time.Now(),\n\t\t\t}\n\t\t\tb, err := json.Marshal(token)\n\t\t\trequire.NoError(t, err)\n\t\t\terr = db.Set(wireDpopTokenTable, []byte(\"orderID\"), b[1:]) // start at index 1; corrupt JSON data\n\t\t\trequire.NoError(t, err)\n\t\t\treturn test{\n\t\t\t\tdb: &DB{\n\t\t\t\t\tdb: db,\n\t\t\t\t},\n\t\t\t\torderID:     \"orderID\",\n\t\t\t\texpectedErr: errors.New(`failed unmarshaling dpop token \"orderID\" into dbDpopToken: invalid character ':' after top-level value`),\n\t\t\t}\n\t\t},\n\t\t\"fail/db.Get\": func(t *testing.T) test {\n\t\t\tdb := &certificatesdb.MockNoSQLDB{\n\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\tassert.Equal(t, wireDpopTokenTable, bucket)\n\t\t\t\t\tassert.Equal(t, []byte(\"orderID\"), key)\n\t\t\t\t\treturn nil, errors.New(\"fail\")\n\t\t\t\t},\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tdb: &DB{\n\t\t\t\t\tdb: db,\n\t\t\t\t},\n\t\t\t\torderID:     \"orderID\",\n\t\t\t\texpectedErr: errors.New(`failed loading dpop token \"orderID\": fail`),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tdir := t.TempDir()\n\t\t\tdb, err := nosql.New(\"badgerv2\", dir)\n\t\t\trequire.NoError(t, err)\n\t\t\ttoken := dbDpopToken{\n\t\t\t\tID:        \"orderID\",\n\t\t\t\tContent:   []byte(`{\"sub\": \"wireapp://guVX5xeFS3eTatmXBIyA4A!7a41cf5b79683410@wire.com\"}`),\n\t\t\t\tCreatedAt: time.Now(),\n\t\t\t}\n\t\t\tb, err := json.Marshal(token)\n\t\t\trequire.NoError(t, err)\n\t\t\terr = db.Set(wireDpopTokenTable, []byte(\"orderID\"), b)\n\t\t\trequire.NoError(t, err)\n\t\t\treturn test{\n\t\t\t\tdb: &DB{\n\t\t\t\t\tdb: db,\n\t\t\t\t},\n\t\t\t\torderID: \"orderID\",\n\t\t\t\texpected: map[string]any{\n\t\t\t\t\t\"sub\": \"wireapp://guVX5xeFS3eTatmXBIyA4A!7a41cf5b79683410@wire.com\",\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\ttc := run(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tgot, err := tc.db.GetDpopToken(context.Background(), tc.orderID)\n\t\t\tif tc.expectedErr != nil {\n\t\t\t\tassert.EqualError(t, err, tc.expectedErr.Error())\n\t\t\t\tae := &acme.Error{}\n\t\t\t\tif errors.As(err, &ae) {\n\t\t\t\t\tee := &acme.Error{}\n\t\t\t\t\trequire.True(t, errors.As(tc.expectedErr, &ee))\n\t\t\t\t\tassert.Equal(t, ee.Detail, ae.Detail)\n\t\t\t\t\tassert.Equal(t, ee.Type, ae.Type)\n\t\t\t\t\tassert.Equal(t, ee.Status, ae.Status)\n\t\t\t\t}\n\t\t\t\tassert.Nil(t, got)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, tc.expected, got)\n\t\t})\n\t}\n}\n\nfunc TestDB_CreateDpopToken(t *testing.T) {\n\ttype test struct {\n\t\tdb          *DB\n\t\torderID     string\n\t\tdpop        map[string]any\n\t\texpectedErr error\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/db.Save\": func(t *testing.T) test {\n\t\t\tdb := &certificatesdb.MockNoSQLDB{\n\t\t\t\tMCmpAndSwap: func(bucket, key, old, newval []byte) ([]byte, bool, error) {\n\t\t\t\t\tassert.Equal(t, wireDpopTokenTable, bucket)\n\t\t\t\t\tassert.Equal(t, []byte(\"orderID\"), key)\n\t\t\t\t\treturn nil, false, errors.New(\"fail\")\n\t\t\t\t},\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tdb: &DB{\n\t\t\t\t\tdb: db,\n\t\t\t\t},\n\t\t\t\torderID: \"orderID\",\n\t\t\t\tdpop: map[string]any{\n\t\t\t\t\t\"sub\": \"wireapp://guVX5xeFS3eTatmXBIyA4A!7a41cf5b79683410@wire.com\",\n\t\t\t\t},\n\t\t\t\texpectedErr: errors.New(\"failed saving dpop token: error saving acme dpop: fail\"),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tdir := t.TempDir()\n\t\t\tdb, err := nosql.New(\"badgerv2\", dir)\n\t\t\trequire.NoError(t, err)\n\t\t\treturn test{\n\t\t\t\tdb: &DB{\n\t\t\t\t\tdb: db,\n\t\t\t\t},\n\t\t\t\torderID: \"orderID\",\n\t\t\t\tdpop: map[string]any{\n\t\t\t\t\t\"sub\": \"wireapp://guVX5xeFS3eTatmXBIyA4A!7a41cf5b79683410@wire.com\",\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"ok/nil\": func(t *testing.T) test {\n\t\t\tdir := t.TempDir()\n\t\t\tdb, err := nosql.New(\"badgerv2\", dir)\n\t\t\trequire.NoError(t, err)\n\t\t\treturn test{\n\t\t\t\tdb: &DB{\n\t\t\t\t\tdb: db,\n\t\t\t\t},\n\t\t\t\torderID: \"orderID\",\n\t\t\t\tdpop:    nil,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\ttc := run(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\terr := tc.db.CreateDpopToken(context.Background(), tc.orderID, tc.dpop)\n\t\t\tif tc.expectedErr != nil {\n\t\t\t\tassert.EqualError(t, err, tc.expectedErr.Error())\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.NoError(t, err)\n\n\t\t\tdpop, err := tc.db.getDBDpopToken(context.Background(), tc.orderID)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tassert.Equal(t, tc.orderID, dpop.ID)\n\t\t\tvar m map[string]any\n\t\t\terr = json.Unmarshal(dpop.Content, &m)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tassert.Equal(t, tc.dpop, m)\n\t\t})\n\t}\n}\n\nfunc TestDB_GetOidcToken(t *testing.T) {\n\ttype test struct {\n\t\tdb          *DB\n\t\torderID     string\n\t\texpected    map[string]any\n\t\texpectedErr error\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/acme-not-found\": func(t *testing.T) test {\n\t\t\tdir := t.TempDir()\n\t\t\tdb, err := nosql.New(\"badgerv2\", dir)\n\t\t\trequire.NoError(t, err)\n\t\t\treturn test{\n\t\t\t\tdb: &DB{\n\t\t\t\t\tdb: db,\n\t\t\t\t},\n\t\t\t\torderID: \"orderID\",\n\t\t\t\texpectedErr: &acme.Error{\n\t\t\t\t\tType:   \"urn:ietf:params:acme:error:malformed\",\n\t\t\t\t\tStatus: 400,\n\t\t\t\t\tDetail: \"The request message was malformed\",\n\t\t\t\t\tErr:    errors.New(`oidc token \"orderID\" not found`),\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"fail/unmarshal-error\": func(t *testing.T) test {\n\t\t\tdir := t.TempDir()\n\t\t\tdb, err := nosql.New(\"badgerv2\", dir)\n\t\t\trequire.NoError(t, err)\n\t\t\ttoken := dbOidcToken{\n\t\t\t\tID:        \"orderID\",\n\t\t\t\tContent:   []byte(\"{}\"),\n\t\t\t\tCreatedAt: time.Now(),\n\t\t\t}\n\t\t\tb, err := json.Marshal(token)\n\t\t\trequire.NoError(t, err)\n\t\t\terr = db.Set(wireOidcTokenTable, []byte(\"orderID\"), b[1:]) // start at index 1; corrupt JSON data\n\t\t\trequire.NoError(t, err)\n\t\t\treturn test{\n\t\t\t\tdb: &DB{\n\t\t\t\t\tdb: db,\n\t\t\t\t},\n\t\t\t\torderID:     \"orderID\",\n\t\t\t\texpectedErr: errors.New(`failed unmarshaling oidc token \"orderID\" into dbOidcToken: invalid character ':' after top-level value`),\n\t\t\t}\n\t\t},\n\t\t\"fail/db.Get\": func(t *testing.T) test {\n\t\t\tdb := &certificatesdb.MockNoSQLDB{\n\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\tassert.Equal(t, wireOidcTokenTable, bucket)\n\t\t\t\t\tassert.Equal(t, []byte(\"orderID\"), key)\n\t\t\t\t\treturn nil, errors.New(\"fail\")\n\t\t\t\t},\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tdb: &DB{\n\t\t\t\t\tdb: db,\n\t\t\t\t},\n\t\t\t\torderID:     \"orderID\",\n\t\t\t\texpectedErr: errors.New(`failed loading oidc token \"orderID\": fail`),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tdir := t.TempDir()\n\t\t\tdb, err := nosql.New(\"badgerv2\", dir)\n\t\t\trequire.NoError(t, err)\n\t\t\ttoken := dbOidcToken{\n\t\t\t\tID:        \"orderID\",\n\t\t\t\tContent:   []byte(`{\"name\": \"Alice Smith\", \"preferred_username\": \"@alice.smith\"}`),\n\t\t\t\tCreatedAt: time.Now(),\n\t\t\t}\n\t\t\tb, err := json.Marshal(token)\n\t\t\trequire.NoError(t, err)\n\t\t\terr = db.Set(wireOidcTokenTable, []byte(\"orderID\"), b)\n\t\t\trequire.NoError(t, err)\n\t\t\treturn test{\n\t\t\t\tdb: &DB{\n\t\t\t\t\tdb: db,\n\t\t\t\t},\n\t\t\t\torderID: \"orderID\",\n\t\t\t\texpected: map[string]any{\n\t\t\t\t\t\"name\":               \"Alice Smith\",\n\t\t\t\t\t\"preferred_username\": \"@alice.smith\",\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\ttc := run(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tgot, err := tc.db.GetOidcToken(context.Background(), tc.orderID)\n\t\t\tif tc.expectedErr != nil {\n\t\t\t\tassert.EqualError(t, err, tc.expectedErr.Error())\n\t\t\t\tae := &acme.Error{}\n\t\t\t\tif errors.As(err, &ae) {\n\t\t\t\t\tee := &acme.Error{}\n\t\t\t\t\trequire.True(t, errors.As(tc.expectedErr, &ee))\n\t\t\t\t\tassert.Equal(t, ee.Detail, ae.Detail)\n\t\t\t\t\tassert.Equal(t, ee.Type, ae.Type)\n\t\t\t\t\tassert.Equal(t, ee.Status, ae.Status)\n\t\t\t\t}\n\t\t\t\tassert.Nil(t, got)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, tc.expected, got)\n\t\t})\n\t}\n}\n\nfunc TestDB_CreateOidcToken(t *testing.T) {\n\ttype test struct {\n\t\tdb          *DB\n\t\torderID     string\n\t\toidc        map[string]any\n\t\texpectedErr error\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/db.Save\": func(t *testing.T) test {\n\t\t\tdb := &certificatesdb.MockNoSQLDB{\n\t\t\t\tMCmpAndSwap: func(bucket, key, old, newval []byte) ([]byte, bool, error) {\n\t\t\t\t\tassert.Equal(t, wireOidcTokenTable, bucket)\n\t\t\t\t\tassert.Equal(t, []byte(\"orderID\"), key)\n\t\t\t\t\treturn nil, false, errors.New(\"fail\")\n\t\t\t\t},\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tdb: &DB{\n\t\t\t\t\tdb: db,\n\t\t\t\t},\n\t\t\t\torderID: \"orderID\",\n\t\t\t\toidc: map[string]any{\n\t\t\t\t\t\"name\":               \"Alice Smith\",\n\t\t\t\t\t\"preferred_username\": \"@alice.smith\",\n\t\t\t\t},\n\t\t\t\texpectedErr: errors.New(\"failed saving oidc token: error saving acme oidc: fail\"),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tdir := t.TempDir()\n\t\t\tdb, err := nosql.New(\"badgerv2\", dir)\n\t\t\trequire.NoError(t, err)\n\t\t\treturn test{\n\t\t\t\tdb: &DB{\n\t\t\t\t\tdb: db,\n\t\t\t\t},\n\t\t\t\torderID: \"orderID\",\n\t\t\t\toidc: map[string]any{\n\t\t\t\t\t\"name\":               \"Alice Smith\",\n\t\t\t\t\t\"preferred_username\": \"@alice.smith\",\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"ok/nil\": func(t *testing.T) test {\n\t\t\tdir := t.TempDir()\n\t\t\tdb, err := nosql.New(\"badgerv2\", dir)\n\t\t\trequire.NoError(t, err)\n\t\t\treturn test{\n\t\t\t\tdb: &DB{\n\t\t\t\t\tdb: db,\n\t\t\t\t},\n\t\t\t\torderID: \"orderID\",\n\t\t\t\toidc:    nil,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\ttc := run(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\terr := tc.db.CreateOidcToken(context.Background(), tc.orderID, tc.oidc)\n\t\t\tif tc.expectedErr != nil {\n\t\t\t\tassert.EqualError(t, err, tc.expectedErr.Error())\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.NoError(t, err)\n\n\t\t\toidc, err := tc.db.getDBOidcToken(context.Background(), tc.orderID)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tassert.Equal(t, tc.orderID, oidc.ID)\n\t\t\tvar m map[string]any\n\t\t\terr = json.Unmarshal(oidc.Content, &m)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tassert.Equal(t, tc.oidc, m)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "acme/db.go",
    "content": "package acme\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\n\t\"github.com/pkg/errors\"\n)\n\n// ErrNotFound is an error that should be used by the acme.DB interface to\n// indicate that an entity does not exist. For example, in the new-account\n// endpoint, if GetAccountByKeyID returns ErrNotFound we will create the new\n// account.\nvar ErrNotFound = errors.New(\"not found\")\n\n// IsErrNotFound returns true if the error is a \"not found\" error. Returns false\n// otherwise.\nfunc IsErrNotFound(err error) bool {\n\treturn errors.Is(err, ErrNotFound) || errors.Is(err, sql.ErrNoRows)\n}\n\n// DB is the DB interface expected by the step-ca ACME API.\ntype DB interface {\n\tCreateAccount(ctx context.Context, acc *Account) error\n\tGetAccount(ctx context.Context, id string) (*Account, error)\n\tGetAccountByKeyID(ctx context.Context, kid string) (*Account, error)\n\tUpdateAccount(ctx context.Context, acc *Account) error\n\n\tCreateExternalAccountKey(ctx context.Context, provisionerID, reference string) (*ExternalAccountKey, error)\n\tGetExternalAccountKey(ctx context.Context, provisionerID, keyID string) (*ExternalAccountKey, error)\n\tGetExternalAccountKeys(ctx context.Context, provisionerID, cursor string, limit int) ([]*ExternalAccountKey, string, error)\n\tGetExternalAccountKeyByReference(ctx context.Context, provisionerID, reference string) (*ExternalAccountKey, error)\n\tGetExternalAccountKeyByAccountID(ctx context.Context, provisionerID, accountID string) (*ExternalAccountKey, error)\n\tDeleteExternalAccountKey(ctx context.Context, provisionerID, keyID string) error\n\tUpdateExternalAccountKey(ctx context.Context, provisionerID string, eak *ExternalAccountKey) error\n\n\tCreateNonce(ctx context.Context) (Nonce, error)\n\tDeleteNonce(ctx context.Context, nonce Nonce) error\n\n\tCreateAuthorization(ctx context.Context, az *Authorization) error\n\tGetAuthorization(ctx context.Context, id string) (*Authorization, error)\n\tUpdateAuthorization(ctx context.Context, az *Authorization) error\n\tGetAuthorizationsByAccountID(ctx context.Context, accountID string) ([]*Authorization, error)\n\n\tCreateCertificate(ctx context.Context, cert *Certificate) error\n\tGetCertificate(ctx context.Context, id string) (*Certificate, error)\n\tGetCertificateBySerial(ctx context.Context, serial string) (*Certificate, error)\n\n\tCreateChallenge(ctx context.Context, ch *Challenge) error\n\tGetChallenge(ctx context.Context, id, authzID string) (*Challenge, error)\n\tUpdateChallenge(ctx context.Context, ch *Challenge) error\n\n\tCreateOrder(ctx context.Context, o *Order) error\n\tGetOrder(ctx context.Context, id string) (*Order, error)\n\tGetOrdersByAccountID(ctx context.Context, accountID string) ([]string, error)\n\tUpdateOrder(ctx context.Context, o *Order) error\n}\n\n// WireDB is the interface used for operations on ACME Orders for Wire identifiers. This\n// is not a general purpose interface, and it should only be used when Wire identifiers\n// are enabled in the CA configuration. Currently it provides a runtime assertion only;\n// not at compile time.\ntype WireDB interface {\n\tDB\n\tGetAllOrdersByAccountID(ctx context.Context, accountID string) ([]string, error)\n\tCreateDpopToken(ctx context.Context, orderID string, dpop map[string]interface{}) error\n\tGetDpopToken(ctx context.Context, orderID string) (map[string]interface{}, error)\n\tCreateOidcToken(ctx context.Context, orderID string, idToken map[string]interface{}) error\n\tGetOidcToken(ctx context.Context, orderID string) (map[string]interface{}, error)\n}\n\ntype dbKey struct{}\n\n// NewDatabaseContext adds the given acme database to the context.\nfunc NewDatabaseContext(ctx context.Context, db DB) context.Context {\n\treturn context.WithValue(ctx, dbKey{}, db)\n}\n\n// DatabaseFromContext returns the current acme database from the given context.\nfunc DatabaseFromContext(ctx context.Context) (db DB, ok bool) {\n\tdb, ok = ctx.Value(dbKey{}).(DB)\n\treturn\n}\n\n// MustDatabaseFromContext returns the current database from the given context.\n// It will panic if it's not in the context.\nfunc MustDatabaseFromContext(ctx context.Context) DB {\n\tvar (\n\t\tdb DB\n\t\tok bool\n\t)\n\tif db, ok = DatabaseFromContext(ctx); !ok {\n\t\tpanic(\"acme database is not in the context\")\n\t}\n\treturn db\n}\n\n// MockDB is an implementation of the DB interface that should only be used as\n// a mock in tests.\ntype MockDB struct {\n\tMockCreateAccount     func(ctx context.Context, acc *Account) error\n\tMockGetAccount        func(ctx context.Context, id string) (*Account, error)\n\tMockGetAccountByKeyID func(ctx context.Context, kid string) (*Account, error)\n\tMockUpdateAccount     func(ctx context.Context, acc *Account) error\n\n\tMockCreateExternalAccountKey         func(ctx context.Context, provisionerID, reference string) (*ExternalAccountKey, error)\n\tMockGetExternalAccountKey            func(ctx context.Context, provisionerID, keyID string) (*ExternalAccountKey, error)\n\tMockGetExternalAccountKeys           func(ctx context.Context, provisionerID, cursor string, limit int) ([]*ExternalAccountKey, string, error)\n\tMockGetExternalAccountKeyByReference func(ctx context.Context, provisionerID, reference string) (*ExternalAccountKey, error)\n\tMockGetExternalAccountKeyByAccountID func(ctx context.Context, provisionerID, accountID string) (*ExternalAccountKey, error)\n\tMockDeleteExternalAccountKey         func(ctx context.Context, provisionerID, keyID string) error\n\tMockUpdateExternalAccountKey         func(ctx context.Context, provisionerID string, eak *ExternalAccountKey) error\n\n\tMockCreateNonce func(ctx context.Context) (Nonce, error)\n\tMockDeleteNonce func(ctx context.Context, nonce Nonce) error\n\n\tMockCreateAuthorization          func(ctx context.Context, az *Authorization) error\n\tMockGetAuthorization             func(ctx context.Context, id string) (*Authorization, error)\n\tMockUpdateAuthorization          func(ctx context.Context, az *Authorization) error\n\tMockGetAuthorizationsByAccountID func(ctx context.Context, accountID string) ([]*Authorization, error)\n\n\tMockCreateCertificate      func(ctx context.Context, cert *Certificate) error\n\tMockGetCertificate         func(ctx context.Context, id string) (*Certificate, error)\n\tMockGetCertificateBySerial func(ctx context.Context, serial string) (*Certificate, error)\n\n\tMockCreateChallenge func(ctx context.Context, ch *Challenge) error\n\tMockGetChallenge    func(ctx context.Context, id, authzID string) (*Challenge, error)\n\tMockUpdateChallenge func(ctx context.Context, ch *Challenge) error\n\n\tMockCreateOrder          func(ctx context.Context, o *Order) error\n\tMockGetOrder             func(ctx context.Context, id string) (*Order, error)\n\tMockGetOrdersByAccountID func(ctx context.Context, accountID string) ([]string, error)\n\tMockUpdateOrder          func(ctx context.Context, o *Order) error\n\n\tMockRet1  interface{}\n\tMockError error\n}\n\n// MockWireDB is an implementation of the WireDB interface that should only be used as\n// a mock in tests. It embeds the MockDB, as it is an extension of the existing database\n// methods.\ntype MockWireDB struct {\n\tMockDB\n\tMockGetAllOrdersByAccountID func(ctx context.Context, accountID string) ([]string, error)\n\tMockGetDpopToken            func(ctx context.Context, orderID string) (map[string]interface{}, error)\n\tMockCreateDpopToken         func(ctx context.Context, orderID string, dpop map[string]interface{}) error\n\tMockGetOidcToken            func(ctx context.Context, orderID string) (map[string]interface{}, error)\n\tMockCreateOidcToken         func(ctx context.Context, orderID string, idToken map[string]interface{}) error\n}\n\n// CreateAccount mock.\nfunc (m *MockDB) CreateAccount(ctx context.Context, acc *Account) error {\n\tif m.MockCreateAccount != nil {\n\t\treturn m.MockCreateAccount(ctx, acc)\n\t} else if m.MockError != nil {\n\t\treturn m.MockError\n\t}\n\treturn m.MockError\n}\n\n// GetAccount mock.\nfunc (m *MockDB) GetAccount(ctx context.Context, id string) (*Account, error) {\n\tif m.MockGetAccount != nil {\n\t\treturn m.MockGetAccount(ctx, id)\n\t} else if m.MockError != nil {\n\t\treturn nil, m.MockError\n\t}\n\treturn m.MockRet1.(*Account), m.MockError\n}\n\n// GetAccountByKeyID mock\nfunc (m *MockDB) GetAccountByKeyID(ctx context.Context, kid string) (*Account, error) {\n\tif m.MockGetAccountByKeyID != nil {\n\t\treturn m.MockGetAccountByKeyID(ctx, kid)\n\t} else if m.MockError != nil {\n\t\treturn nil, m.MockError\n\t}\n\treturn m.MockRet1.(*Account), m.MockError\n}\n\n// UpdateAccount mock\nfunc (m *MockDB) UpdateAccount(ctx context.Context, acc *Account) error {\n\tif m.MockUpdateAccount != nil {\n\t\treturn m.MockUpdateAccount(ctx, acc)\n\t} else if m.MockError != nil {\n\t\treturn m.MockError\n\t}\n\treturn m.MockError\n}\n\n// CreateExternalAccountKey mock\nfunc (m *MockDB) CreateExternalAccountKey(ctx context.Context, provisionerID, reference string) (*ExternalAccountKey, error) {\n\tif m.MockCreateExternalAccountKey != nil {\n\t\treturn m.MockCreateExternalAccountKey(ctx, provisionerID, reference)\n\t} else if m.MockError != nil {\n\t\treturn nil, m.MockError\n\t}\n\treturn m.MockRet1.(*ExternalAccountKey), m.MockError\n}\n\n// GetExternalAccountKey mock\nfunc (m *MockDB) GetExternalAccountKey(ctx context.Context, provisionerID, keyID string) (*ExternalAccountKey, error) {\n\tif m.MockGetExternalAccountKey != nil {\n\t\treturn m.MockGetExternalAccountKey(ctx, provisionerID, keyID)\n\t} else if m.MockError != nil {\n\t\treturn nil, m.MockError\n\t}\n\treturn m.MockRet1.(*ExternalAccountKey), m.MockError\n}\n\n// GetExternalAccountKeys mock\nfunc (m *MockDB) GetExternalAccountKeys(ctx context.Context, provisionerID, cursor string, limit int) ([]*ExternalAccountKey, string, error) {\n\tif m.MockGetExternalAccountKeys != nil {\n\t\treturn m.MockGetExternalAccountKeys(ctx, provisionerID, cursor, limit)\n\t} else if m.MockError != nil {\n\t\treturn nil, \"\", m.MockError\n\t}\n\treturn m.MockRet1.([]*ExternalAccountKey), \"\", m.MockError\n}\n\n// GetExternalAccountKeyByReference mock\nfunc (m *MockDB) GetExternalAccountKeyByReference(ctx context.Context, provisionerID, reference string) (*ExternalAccountKey, error) {\n\tif m.MockGetExternalAccountKeyByReference != nil {\n\t\treturn m.MockGetExternalAccountKeyByReference(ctx, provisionerID, reference)\n\t} else if m.MockError != nil {\n\t\treturn nil, m.MockError\n\t}\n\treturn m.MockRet1.(*ExternalAccountKey), m.MockError\n}\n\n// GetExternalAccountKeyByAccountID mock\nfunc (m *MockDB) GetExternalAccountKeyByAccountID(ctx context.Context, provisionerID, accountID string) (*ExternalAccountKey, error) {\n\tif m.MockGetExternalAccountKeyByAccountID != nil {\n\t\treturn m.MockGetExternalAccountKeyByAccountID(ctx, provisionerID, accountID)\n\t} else if m.MockError != nil {\n\t\treturn nil, m.MockError\n\t}\n\treturn m.MockRet1.(*ExternalAccountKey), m.MockError\n}\n\n// DeleteExternalAccountKey mock\nfunc (m *MockDB) DeleteExternalAccountKey(ctx context.Context, provisionerID, keyID string) error {\n\tif m.MockDeleteExternalAccountKey != nil {\n\t\treturn m.MockDeleteExternalAccountKey(ctx, provisionerID, keyID)\n\t} else if m.MockError != nil {\n\t\treturn m.MockError\n\t}\n\treturn m.MockError\n}\n\n// UpdateExternalAccountKey mock\nfunc (m *MockDB) UpdateExternalAccountKey(ctx context.Context, provisionerID string, eak *ExternalAccountKey) error {\n\tif m.MockUpdateExternalAccountKey != nil {\n\t\treturn m.MockUpdateExternalAccountKey(ctx, provisionerID, eak)\n\t} else if m.MockError != nil {\n\t\treturn m.MockError\n\t}\n\treturn m.MockError\n}\n\n// CreateNonce mock\nfunc (m *MockDB) CreateNonce(ctx context.Context) (Nonce, error) {\n\tif m.MockCreateNonce != nil {\n\t\treturn m.MockCreateNonce(ctx)\n\t} else if m.MockError != nil {\n\t\treturn Nonce(\"\"), m.MockError\n\t}\n\treturn m.MockRet1.(Nonce), m.MockError\n}\n\n// DeleteNonce mock\nfunc (m *MockDB) DeleteNonce(ctx context.Context, nonce Nonce) error {\n\tif m.MockDeleteNonce != nil {\n\t\treturn m.MockDeleteNonce(ctx, nonce)\n\t} else if m.MockError != nil {\n\t\treturn m.MockError\n\t}\n\treturn m.MockError\n}\n\n// CreateAuthorization mock\nfunc (m *MockDB) CreateAuthorization(ctx context.Context, az *Authorization) error {\n\tif m.MockCreateAuthorization != nil {\n\t\treturn m.MockCreateAuthorization(ctx, az)\n\t} else if m.MockError != nil {\n\t\treturn m.MockError\n\t}\n\treturn m.MockError\n}\n\n// GetAuthorization mock\nfunc (m *MockDB) GetAuthorization(ctx context.Context, id string) (*Authorization, error) {\n\tif m.MockGetAuthorization != nil {\n\t\treturn m.MockGetAuthorization(ctx, id)\n\t} else if m.MockError != nil {\n\t\treturn nil, m.MockError\n\t}\n\treturn m.MockRet1.(*Authorization), m.MockError\n}\n\n// UpdateAuthorization mock\nfunc (m *MockDB) UpdateAuthorization(ctx context.Context, az *Authorization) error {\n\tif m.MockUpdateAuthorization != nil {\n\t\treturn m.MockUpdateAuthorization(ctx, az)\n\t} else if m.MockError != nil {\n\t\treturn m.MockError\n\t}\n\treturn m.MockError\n}\n\n// GetAuthorizationsByAccountID mock\nfunc (m *MockDB) GetAuthorizationsByAccountID(ctx context.Context, accountID string) ([]*Authorization, error) {\n\tif m.MockGetAuthorizationsByAccountID != nil {\n\t\treturn m.MockGetAuthorizationsByAccountID(ctx, accountID)\n\t} else if m.MockError != nil {\n\t\treturn nil, m.MockError\n\t}\n\treturn nil, m.MockError\n}\n\n// CreateCertificate mock\nfunc (m *MockDB) CreateCertificate(ctx context.Context, cert *Certificate) error {\n\tif m.MockCreateCertificate != nil {\n\t\treturn m.MockCreateCertificate(ctx, cert)\n\t} else if m.MockError != nil {\n\t\treturn m.MockError\n\t}\n\treturn m.MockError\n}\n\n// GetCertificate mock\nfunc (m *MockDB) GetCertificate(ctx context.Context, id string) (*Certificate, error) {\n\tif m.MockGetCertificate != nil {\n\t\treturn m.MockGetCertificate(ctx, id)\n\t} else if m.MockError != nil {\n\t\treturn nil, m.MockError\n\t}\n\treturn m.MockRet1.(*Certificate), m.MockError\n}\n\n// GetCertificateBySerial mock\nfunc (m *MockDB) GetCertificateBySerial(ctx context.Context, serial string) (*Certificate, error) {\n\tif m.MockGetCertificateBySerial != nil {\n\t\treturn m.MockGetCertificateBySerial(ctx, serial)\n\t} else if m.MockError != nil {\n\t\treturn nil, m.MockError\n\t}\n\treturn m.MockRet1.(*Certificate), m.MockError\n}\n\n// CreateChallenge mock\nfunc (m *MockDB) CreateChallenge(ctx context.Context, ch *Challenge) error {\n\tif m.MockCreateChallenge != nil {\n\t\treturn m.MockCreateChallenge(ctx, ch)\n\t} else if m.MockError != nil {\n\t\treturn m.MockError\n\t}\n\treturn m.MockError\n}\n\n// GetChallenge mock\nfunc (m *MockDB) GetChallenge(ctx context.Context, chID, azID string) (*Challenge, error) {\n\tif m.MockGetChallenge != nil {\n\t\treturn m.MockGetChallenge(ctx, chID, azID)\n\t} else if m.MockError != nil {\n\t\treturn nil, m.MockError\n\t}\n\treturn m.MockRet1.(*Challenge), m.MockError\n}\n\n// UpdateChallenge mock\nfunc (m *MockDB) UpdateChallenge(ctx context.Context, ch *Challenge) error {\n\tif m.MockUpdateChallenge != nil {\n\t\treturn m.MockUpdateChallenge(ctx, ch)\n\t} else if m.MockError != nil {\n\t\treturn m.MockError\n\t}\n\treturn m.MockError\n}\n\n// CreateOrder mock\nfunc (m *MockDB) CreateOrder(ctx context.Context, o *Order) error {\n\tif m.MockCreateOrder != nil {\n\t\treturn m.MockCreateOrder(ctx, o)\n\t} else if m.MockError != nil {\n\t\treturn m.MockError\n\t}\n\treturn m.MockError\n}\n\n// GetOrder mock\nfunc (m *MockDB) GetOrder(ctx context.Context, id string) (*Order, error) {\n\tif m.MockGetOrder != nil {\n\t\treturn m.MockGetOrder(ctx, id)\n\t} else if m.MockError != nil {\n\t\treturn nil, m.MockError\n\t}\n\treturn m.MockRet1.(*Order), m.MockError\n}\n\n// UpdateOrder mock\nfunc (m *MockDB) UpdateOrder(ctx context.Context, o *Order) error {\n\tif m.MockUpdateOrder != nil {\n\t\treturn m.MockUpdateOrder(ctx, o)\n\t} else if m.MockError != nil {\n\t\treturn m.MockError\n\t}\n\treturn m.MockError\n}\n\n// GetOrdersByAccountID mock\nfunc (m *MockDB) GetOrdersByAccountID(ctx context.Context, accID string) ([]string, error) {\n\tif m.MockGetOrdersByAccountID != nil {\n\t\treturn m.MockGetOrdersByAccountID(ctx, accID)\n\t} else if m.MockError != nil {\n\t\treturn nil, m.MockError\n\t}\n\treturn m.MockRet1.([]string), m.MockError\n}\n\n// GetAllOrdersByAccountID returns a list of any order IDs owned by the account.\nfunc (m *MockWireDB) GetAllOrdersByAccountID(ctx context.Context, accountID string) ([]string, error) {\n\tif m.MockGetAllOrdersByAccountID != nil {\n\t\treturn m.MockGetAllOrdersByAccountID(ctx, accountID)\n\t} else if m.MockError != nil {\n\t\treturn nil, m.MockError\n\t}\n\treturn m.MockRet1.([]string), m.MockError\n}\n\n// GetDpop retrieves a DPoP from the database.\nfunc (m *MockWireDB) GetDpopToken(ctx context.Context, orderID string) (map[string]any, error) {\n\tif m.MockGetDpopToken != nil {\n\t\treturn m.MockGetDpopToken(ctx, orderID)\n\t} else if m.MockError != nil {\n\t\treturn nil, m.MockError\n\t}\n\treturn m.MockRet1.(map[string]any), m.MockError\n}\n\n// CreateDpop creates DPoP resources and saves them to the DB.\nfunc (m *MockWireDB) CreateDpopToken(ctx context.Context, orderID string, dpop map[string]any) error {\n\tif m.MockCreateDpopToken != nil {\n\t\treturn m.MockCreateDpopToken(ctx, orderID, dpop)\n\t}\n\treturn m.MockError\n}\n\n// GetOidcToken retrieves an oidc token from the database.\nfunc (m *MockWireDB) GetOidcToken(ctx context.Context, orderID string) (map[string]any, error) {\n\tif m.MockGetOidcToken != nil {\n\t\treturn m.MockGetOidcToken(ctx, orderID)\n\t} else if m.MockError != nil {\n\t\treturn nil, m.MockError\n\t}\n\treturn m.MockRet1.(map[string]any), m.MockError\n}\n\n// CreateOidcToken creates oidc token resources and saves them to the DB.\nfunc (m *MockWireDB) CreateOidcToken(ctx context.Context, orderID string, idToken map[string]any) error {\n\tif m.MockCreateOidcToken != nil {\n\t\treturn m.MockCreateOidcToken(ctx, orderID, idToken)\n\t}\n\treturn m.MockError\n}\n"
  },
  {
    "path": "acme/db_test.go",
    "content": "package acme\n\nimport (\n\t\"database/sql\"\n\t\"errors\"\n\t\"fmt\"\n\t\"testing\"\n)\n\nfunc TestIsErrNotFound(t *testing.T) {\n\ttype args struct {\n\t\terr error\n\t}\n\ttests := []struct {\n\t\tname string\n\t\targs args\n\t\twant bool\n\t}{\n\t\t{\"true ErrNotFound\", args{ErrNotFound}, true},\n\t\t{\"true sql.ErrNoRows\", args{sql.ErrNoRows}, true},\n\t\t{\"true wrapped ErrNotFound\", args{fmt.Errorf(\"something failed: %w\", ErrNotFound)}, true},\n\t\t{\"true wrapped sql.ErrNoRows\", args{fmt.Errorf(\"something failed: %w\", sql.ErrNoRows)}, true},\n\t\t{\"false other\", args{errors.New(\"not found\")}, false},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif got := IsErrNotFound(tt.args.err); got != tt.want {\n\t\t\t\tt.Errorf(\"IsErrNotFound() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "acme/errors.go",
    "content": "package acme\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\n\t\"github.com/pkg/errors\"\n\t\"github.com/smallstep/certificates/api/render\"\n)\n\n// ProblemType is the type of the ACME problem.\ntype ProblemType int\n\nconst (\n\t// ErrorAccountDoesNotExistType request specified an account that does not exist\n\tErrorAccountDoesNotExistType ProblemType = iota\n\t// ErrorAlreadyRevokedType request specified a certificate to be revoked that has already been revoked\n\tErrorAlreadyRevokedType\n\t// ErrorBadAttestationStatementType WebAuthn attestation statement could not be verified\n\tErrorBadAttestationStatementType\n\t// ErrorBadCSRType CSR is unacceptable (e.g., due to a short key)\n\tErrorBadCSRType\n\t// ErrorBadNonceType client sent an unacceptable anti-replay nonce\n\tErrorBadNonceType\n\t// ErrorBadPublicKeyType JWS was signed by a public key the server does not support\n\tErrorBadPublicKeyType\n\t// ErrorBadRevocationReasonType revocation reason provided is not allowed by the server\n\tErrorBadRevocationReasonType\n\t// ErrorBadSignatureAlgorithmType JWS was signed with an algorithm the server does not support\n\tErrorBadSignatureAlgorithmType\n\t// ErrorCaaType Authority Authorization (CAA) records forbid the CA from issuing a certificate\n\tErrorCaaType\n\t// ErrorCompoundType error conditions are indicated in the “subproblems” array.\n\tErrorCompoundType\n\t// ErrorConnectionType server could not connect to validation target\n\tErrorConnectionType\n\t// ErrorDNSType was a problem with a DNS query during identifier validation\n\tErrorDNSType\n\t// ErrorExternalAccountRequiredType request must include a value for the “externalAccountBinding” field\n\tErrorExternalAccountRequiredType\n\t// ErrorIncorrectResponseType received didn’t match the challenge’s requirements\n\tErrorIncorrectResponseType\n\t// ErrorInvalidContactType URL for an account was invalid\n\tErrorInvalidContactType\n\t// ErrorMalformedType request message was malformed\n\tErrorMalformedType\n\t// ErrorOrderNotReadyType request attempted to finalize an order that is not ready to be finalized\n\tErrorOrderNotReadyType\n\t// ErrorRateLimitedType request exceeds a rate limit\n\tErrorRateLimitedType\n\t// ErrorRejectedIdentifierType server will not issue certificates for the identifier\n\tErrorRejectedIdentifierType\n\t// ErrorServerInternalType server experienced an internal error\n\tErrorServerInternalType\n\t// ErrorTLSType server received a TLS error during validation\n\tErrorTLSType\n\t// ErrorUnauthorizedType client lacks sufficient authorization\n\tErrorUnauthorizedType\n\t// ErrorUnsupportedContactType URL for an account used an unsupported protocol scheme\n\tErrorUnsupportedContactType\n\t// ErrorUnsupportedIdentifierType identifier is of an unsupported type\n\tErrorUnsupportedIdentifierType\n\t// ErrorUserActionRequiredType the “instance” URL and take actions specified there\n\tErrorUserActionRequiredType\n\t// ErrorNotImplementedType operation is not implemented\n\tErrorNotImplementedType\n)\n\n// String returns the string representation of the acme problem type,\n// fulfilling the Stringer interface.\nfunc (ap ProblemType) String() string {\n\tswitch ap {\n\tcase ErrorAccountDoesNotExistType:\n\t\treturn \"accountDoesNotExist\"\n\tcase ErrorAlreadyRevokedType:\n\t\treturn \"alreadyRevoked\"\n\tcase ErrorBadAttestationStatementType:\n\t\treturn \"badAttestationStatement\"\n\tcase ErrorBadCSRType:\n\t\treturn \"badCSR\"\n\tcase ErrorBadNonceType:\n\t\treturn \"badNonce\"\n\tcase ErrorBadPublicKeyType:\n\t\treturn \"badPublicKey\"\n\tcase ErrorBadRevocationReasonType:\n\t\treturn \"badRevocationReason\"\n\tcase ErrorBadSignatureAlgorithmType:\n\t\treturn \"badSignatureAlgorithm\"\n\tcase ErrorCaaType:\n\t\treturn \"caa\"\n\tcase ErrorCompoundType:\n\t\treturn \"compound\"\n\tcase ErrorConnectionType:\n\t\treturn \"connection\"\n\tcase ErrorDNSType:\n\t\treturn \"dns\"\n\tcase ErrorExternalAccountRequiredType:\n\t\treturn \"externalAccountRequired\"\n\tcase ErrorInvalidContactType:\n\t\treturn \"incorrectResponse\"\n\tcase ErrorMalformedType:\n\t\treturn \"malformed\"\n\tcase ErrorOrderNotReadyType:\n\t\treturn \"orderNotReady\"\n\tcase ErrorRateLimitedType:\n\t\treturn \"rateLimited\"\n\tcase ErrorRejectedIdentifierType:\n\t\treturn \"rejectedIdentifier\"\n\tcase ErrorServerInternalType:\n\t\treturn \"serverInternal\"\n\tcase ErrorTLSType:\n\t\treturn \"tls\"\n\tcase ErrorUnauthorizedType:\n\t\treturn \"unauthorized\"\n\tcase ErrorUnsupportedContactType:\n\t\treturn \"unsupportedContact\"\n\tcase ErrorUnsupportedIdentifierType:\n\t\treturn \"unsupportedIdentifier\"\n\tcase ErrorUserActionRequiredType:\n\t\treturn \"userActionRequired\"\n\tcase ErrorNotImplementedType:\n\t\treturn \"notImplemented\"\n\tdefault:\n\t\treturn fmt.Sprintf(\"unsupported type ACME error type '%d'\", int(ap))\n\t}\n}\n\ntype errorMetadata struct {\n\tdetails string\n\tstatus  int\n\ttyp     string\n\tString  string\n}\n\nvar (\n\tofficialACMEPrefix          = \"urn:ietf:params:acme:error:\"\n\terrorServerInternalMetadata = errorMetadata{\n\t\ttyp:     officialACMEPrefix + ErrorServerInternalType.String(),\n\t\tdetails: \"The server experienced an internal error\",\n\t\tstatus:  500,\n\t}\n\terrorMap = map[ProblemType]errorMetadata{\n\t\tErrorAccountDoesNotExistType: {\n\t\t\ttyp:     officialACMEPrefix + ErrorAccountDoesNotExistType.String(),\n\t\t\tdetails: \"Account does not exist\",\n\t\t\tstatus:  400,\n\t\t},\n\t\tErrorAlreadyRevokedType: {\n\t\t\ttyp:     officialACMEPrefix + ErrorAlreadyRevokedType.String(),\n\t\t\tdetails: \"Certificate already revoked\",\n\t\t\tstatus:  400,\n\t\t},\n\t\tErrorBadCSRType: {\n\t\t\ttyp:     officialACMEPrefix + ErrorBadCSRType.String(),\n\t\t\tdetails: \"The CSR is unacceptable\",\n\t\t\tstatus:  400,\n\t\t},\n\t\tErrorBadNonceType: {\n\t\t\ttyp:     officialACMEPrefix + ErrorBadNonceType.String(),\n\t\t\tdetails: \"Unacceptable anti-replay nonce\",\n\t\t\tstatus:  400,\n\t\t},\n\t\tErrorBadPublicKeyType: {\n\t\t\ttyp:     officialACMEPrefix + ErrorBadPublicKeyType.String(),\n\t\t\tdetails: \"The jws was signed by a public key the server does not support\",\n\t\t\tstatus:  400,\n\t\t},\n\t\tErrorBadRevocationReasonType: {\n\t\t\ttyp:     officialACMEPrefix + ErrorBadRevocationReasonType.String(),\n\t\t\tdetails: \"The revocation reason provided is not allowed by the server\",\n\t\t\tstatus:  400,\n\t\t},\n\t\tErrorBadSignatureAlgorithmType: {\n\t\t\ttyp:     officialACMEPrefix + ErrorBadSignatureAlgorithmType.String(),\n\t\t\tdetails: \"The JWS was signed with an algorithm the server does not support\",\n\t\t\tstatus:  400,\n\t\t},\n\t\tErrorBadAttestationStatementType: {\n\t\t\ttyp:     officialACMEPrefix + ErrorBadAttestationStatementType.String(),\n\t\t\tdetails: \"Attestation statement cannot be verified\",\n\t\t\tstatus:  400,\n\t\t},\n\t\tErrorCaaType: {\n\t\t\ttyp:     officialACMEPrefix + ErrorCaaType.String(),\n\t\t\tdetails: \"Certification Authority Authorization (CAA) records forbid the CA from issuing a certificate\",\n\t\t\tstatus:  400,\n\t\t},\n\t\tErrorCompoundType: {\n\t\t\ttyp:     officialACMEPrefix + ErrorCompoundType.String(),\n\t\t\tdetails: \"Specific error conditions are indicated in the “subproblems” array\",\n\t\t\tstatus:  400,\n\t\t},\n\t\tErrorConnectionType: {\n\t\t\ttyp:     officialACMEPrefix + ErrorConnectionType.String(),\n\t\t\tdetails: \"The server could not connect to validation target\",\n\t\t\tstatus:  400,\n\t\t},\n\t\tErrorDNSType: {\n\t\t\ttyp:     officialACMEPrefix + ErrorDNSType.String(),\n\t\t\tdetails: \"There was a problem with a DNS query during identifier validation\",\n\t\t\tstatus:  400,\n\t\t},\n\t\tErrorExternalAccountRequiredType: {\n\t\t\ttyp:     officialACMEPrefix + ErrorExternalAccountRequiredType.String(),\n\t\t\tdetails: \"The request must include a value for the \\\"externalAccountBinding\\\" field\",\n\t\t\tstatus:  400,\n\t\t},\n\t\tErrorIncorrectResponseType: {\n\t\t\ttyp:     officialACMEPrefix + ErrorIncorrectResponseType.String(),\n\t\t\tdetails: \"Response received didn't match the challenge's requirements\",\n\t\t\tstatus:  400,\n\t\t},\n\t\tErrorInvalidContactType: {\n\t\t\ttyp:     officialACMEPrefix + ErrorInvalidContactType.String(),\n\t\t\tdetails: \"A contact URL for an account was invalid\",\n\t\t\tstatus:  400,\n\t\t},\n\t\tErrorMalformedType: {\n\t\t\ttyp:     officialACMEPrefix + ErrorMalformedType.String(),\n\t\t\tdetails: \"The request message was malformed\",\n\t\t\tstatus:  400,\n\t\t},\n\t\tErrorOrderNotReadyType: {\n\t\t\ttyp:     officialACMEPrefix + ErrorOrderNotReadyType.String(),\n\t\t\tdetails: \"The request attempted to finalize an order that is not ready to be finalized\",\n\t\t\tstatus:  400,\n\t\t},\n\t\tErrorRateLimitedType: {\n\t\t\ttyp:     officialACMEPrefix + ErrorRateLimitedType.String(),\n\t\t\tdetails: \"The request exceeds a rate limit\",\n\t\t\tstatus:  400,\n\t\t},\n\t\tErrorRejectedIdentifierType: {\n\t\t\ttyp:     officialACMEPrefix + ErrorRejectedIdentifierType.String(),\n\t\t\tdetails: \"The server will not issue certificates for the identifier\",\n\t\t\tstatus:  400,\n\t\t},\n\t\tErrorNotImplementedType: {\n\t\t\ttyp:     officialACMEPrefix + ErrorRejectedIdentifierType.String(),\n\t\t\tdetails: \"The requested operation is not implemented\",\n\t\t\tstatus:  501,\n\t\t},\n\t\tErrorTLSType: {\n\t\t\ttyp:     officialACMEPrefix + ErrorTLSType.String(),\n\t\t\tdetails: \"The server received a TLS error during validation\",\n\t\t\tstatus:  400,\n\t\t},\n\t\tErrorUnauthorizedType: {\n\t\t\ttyp:     officialACMEPrefix + ErrorUnauthorizedType.String(),\n\t\t\tdetails: \"The client lacks sufficient authorization\",\n\t\t\tstatus:  401,\n\t\t},\n\t\tErrorUnsupportedContactType: {\n\t\t\ttyp:     officialACMEPrefix + ErrorUnsupportedContactType.String(),\n\t\t\tdetails: \"A contact URL for an account used an unsupported protocol scheme\",\n\t\t\tstatus:  400,\n\t\t},\n\t\tErrorUnsupportedIdentifierType: {\n\t\t\ttyp:     officialACMEPrefix + ErrorUnsupportedIdentifierType.String(),\n\t\t\tdetails: \"An identifier is of an unsupported type\",\n\t\t\tstatus:  400,\n\t\t},\n\t\tErrorUserActionRequiredType: {\n\t\t\ttyp:     officialACMEPrefix + ErrorUserActionRequiredType.String(),\n\t\t\tdetails: \"Visit the “instance” URL and take actions specified there\",\n\t\t\tstatus:  400,\n\t\t},\n\t\tErrorServerInternalType: errorServerInternalMetadata,\n\t}\n)\n\n// Error represents an ACME Error\ntype Error struct {\n\tType        string       `json:\"type\"`\n\tDetail      string       `json:\"detail\"`\n\tSubproblems []Subproblem `json:\"subproblems,omitempty\"`\n\tErr         error        `json:\"-\"`\n\tStatus      int          `json:\"-\"`\n}\n\n// Subproblem represents an ACME subproblem. It's fairly\n// similar to an ACME error, but differs in that it can't\n// include subproblems itself, the error is reflected\n// in the Detail property and doesn't have a Status.\ntype Subproblem struct {\n\tType   string `json:\"type\"`\n\tDetail string `json:\"detail\"`\n\t// The \"identifier\" field MUST NOT be present at the top level in ACME\n\t// problem documents.  It can only be present in subproblems.\n\t// Subproblems need not all have the same type, and they do not need to\n\t// match the top level type.\n\tIdentifier *Identifier `json:\"identifier,omitempty\"`\n}\n\n// NewError creates a new Error.\nfunc NewError(pt ProblemType, msg string, args ...any) *Error {\n\treturn newError(pt, errors.Errorf(msg, args...))\n}\n\n// NewDetailedError creates a new Error that includes the error\n// message in the details, providing more information to the\n// ACME client.\nfunc NewDetailedError(pt ProblemType, msg string, args ...any) *Error {\n\treturn NewError(pt, msg, args...).withDetail()\n}\n\nfunc (e *Error) withDetail() *Error {\n\tif e == nil || e.Status >= 500 || e.Err == nil {\n\t\treturn e\n\t}\n\n\te.Detail = fmt.Sprintf(\"%s: %s\", e.Detail, e.Err)\n\treturn e\n}\n\n// AddSubproblems adds the Subproblems to Error. It\n// returns the Error, allowing for fluent addition.\nfunc (e *Error) AddSubproblems(subproblems ...Subproblem) *Error {\n\te.Subproblems = append(e.Subproblems, subproblems...)\n\treturn e\n}\n\n// NewSubproblem creates a new Subproblem. The msg and args\n// are used to create a new error, which is set as the Detail, allowing\n// for more detailed error messages to be returned to the ACME client.\nfunc NewSubproblem(pt ProblemType, msg string, args ...any) Subproblem {\n\te := newError(pt, fmt.Errorf(msg, args...))\n\ts := Subproblem{\n\t\tType:   e.Type,\n\t\tDetail: e.Err.Error(),\n\t}\n\treturn s\n}\n\n// NewSubproblemWithIdentifier creates a new Subproblem with a specific ACME\n// Identifier. It calls NewSubproblem and sets the Identifier.\nfunc NewSubproblemWithIdentifier(pt ProblemType, identifier Identifier, msg string, args ...any) Subproblem {\n\ts := NewSubproblem(pt, msg, args...)\n\ts.Identifier = &identifier\n\treturn s\n}\n\nfunc newError(pt ProblemType, err error) *Error {\n\tmeta, ok := errorMap[pt]\n\tif !ok {\n\t\tmeta = errorServerInternalMetadata\n\t\treturn &Error{\n\t\t\tType:   meta.typ,\n\t\t\tDetail: meta.details,\n\t\t\tStatus: meta.status,\n\t\t\tErr:    err,\n\t\t}\n\t}\n\n\treturn &Error{\n\t\tType:   meta.typ,\n\t\tDetail: meta.details,\n\t\tStatus: meta.status,\n\t\tErr:    err,\n\t}\n}\n\n// NewErrorISE creates a new ErrorServerInternalType Error.\nfunc NewErrorISE(msg string, args ...any) *Error {\n\treturn NewError(ErrorServerInternalType, msg, args...)\n}\n\n// WrapError attempts to wrap the internal error.\nfunc WrapError(typ ProblemType, err error, msg string, args ...any) *Error {\n\tvar e *Error\n\tswitch {\n\tcase err == nil:\n\t\treturn nil\n\tcase errors.As(err, &e):\n\t\tif e.Err == nil {\n\t\t\te.Err = errors.Errorf(msg+\"; \"+e.Detail, args...)\n\t\t} else {\n\t\t\te.Err = errors.Wrapf(e.Err, msg, args...)\n\t\t}\n\t\treturn e\n\tdefault:\n\t\treturn newError(typ, errors.Wrapf(err, msg, args...))\n\t}\n}\n\nfunc WrapDetailedError(typ ProblemType, err error, msg string, args ...any) *Error {\n\treturn WrapError(typ, err, msg, args...).withDetail()\n}\n\n// WrapErrorISE shortcut to wrap an internal server error type.\nfunc WrapErrorISE(err error, msg string, args ...any) *Error {\n\treturn WrapError(ErrorServerInternalType, err, msg, args...)\n}\n\n// StatusCode returns the status code and implements the StatusCoder interface.\nfunc (e *Error) StatusCode() int {\n\treturn e.Status\n}\n\n// Error implements the error interface.\nfunc (e *Error) Error() string {\n\tif e.Err == nil {\n\t\treturn e.Detail\n\t}\n\treturn e.Err.Error()\n}\n\n// Cause returns the internal error and implements the Causer interface.\nfunc (e *Error) Cause() error {\n\tif e.Err == nil {\n\t\treturn errors.New(e.Detail)\n\t}\n\treturn e.Err\n}\n\n// ToLog implements the EnableLogger interface.\nfunc (e *Error) ToLog() (any, error) {\n\tb, err := json.Marshal(e)\n\tif err != nil {\n\t\treturn nil, WrapErrorISE(err, \"error marshaling acme.Error for logging\")\n\t}\n\treturn string(b), nil\n}\n\n// Render implements render.RenderableError for Error.\nfunc (e *Error) Render(w http.ResponseWriter, r *http.Request) {\n\tw.Header().Set(\"Content-Type\", \"application/problem+json\")\n\trender.JSONStatus(w, r, e, e.StatusCode())\n}\n"
  },
  {
    "path": "acme/errors_test.go",
    "content": "package acme\n\nimport (\n\t\"encoding/json\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc mustJSON(t *testing.T, m map[string]interface{}) string {\n\tt.Helper()\n\n\tb, err := json.Marshal(m)\n\trequire.NoError(t, err)\n\n\treturn string(b)\n}\n\nfunc TestError_WithAdditionalErrorDetail(t *testing.T) {\n\tinternalJSON := mustJSON(t, map[string]interface{}{\n\t\t\"detail\": \"The server experienced an internal error\",\n\t\t\"type\":   \"urn:ietf:params:acme:error:serverInternal\",\n\t})\n\tmalformedErr := NewError(ErrorMalformedType, \"malformed error\") // will result in Err == nil behavior\n\tmalformedJSON := mustJSON(t, map[string]interface{}{\n\t\t\"detail\": \"The request message was malformed\",\n\t\t\"type\":   \"urn:ietf:params:acme:error:malformed\",\n\t})\n\twithDetailJSON := mustJSON(t, map[string]interface{}{\n\t\t\"detail\": \"Attestation statement cannot be verified: invalid property\",\n\t\t\"type\":   \"urn:ietf:params:acme:error:badAttestationStatement\",\n\t})\n\ttests := []struct {\n\t\tname string\n\t\terr  *Error\n\t\twant string\n\t}{\n\t\t{\"internal\", NewDetailedError(ErrorServerInternalType, \"\"), internalJSON},\n\t\t{\"nil err\", malformedErr, malformedJSON},\n\t\t{\"detailed\", NewDetailedError(ErrorBadAttestationStatementType, \"invalid property\"), withDetailJSON},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tb, err := json.Marshal(tt.err)\n\t\t\trequire.NoError(t, err)\n\n\t\t\t// tests if the additional error detail is included in the JSON representation\n\t\t\t// of the ACME error. This is what is returned to ACME clients and being logged\n\t\t\t// by the CA.\n\t\t\tassert.JSONEq(t, tt.want, string(b))\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "acme/linker.go",
    "content": "package acme\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n\n\t\"github.com/go-chi/chi/v5\"\n\t\"github.com/smallstep/certificates/api/render\"\n\t\"github.com/smallstep/certificates/authority\"\n\t\"github.com/smallstep/certificates/authority/provisioner\"\n)\n\n// LinkType captures the link type.\ntype LinkType int\n\nconst (\n\t// NewNonceLinkType new-nonce\n\tNewNonceLinkType LinkType = iota\n\t// NewAccountLinkType new-account\n\tNewAccountLinkType\n\t// AccountLinkType account\n\tAccountLinkType\n\t// OrderLinkType order\n\tOrderLinkType\n\t// NewOrderLinkType new-order\n\tNewOrderLinkType\n\t// OrdersByAccountLinkType list of orders owned by account\n\tOrdersByAccountLinkType\n\t// FinalizeLinkType finalize order\n\tFinalizeLinkType\n\t// NewAuthzLinkType authz\n\tNewAuthzLinkType\n\t// AuthzLinkType new-authz\n\tAuthzLinkType\n\t// ChallengeLinkType challenge\n\tChallengeLinkType\n\t// CertificateLinkType certificate\n\tCertificateLinkType\n\t// DirectoryLinkType directory\n\tDirectoryLinkType\n\t// RevokeCertLinkType revoke certificate\n\tRevokeCertLinkType\n\t// KeyChangeLinkType key rollover\n\tKeyChangeLinkType\n)\n\nfunc (l LinkType) String() string {\n\tswitch l {\n\tcase NewNonceLinkType:\n\t\treturn \"new-nonce\"\n\tcase NewAccountLinkType:\n\t\treturn \"new-account\"\n\tcase AccountLinkType:\n\t\treturn \"account\"\n\tcase NewOrderLinkType:\n\t\treturn \"new-order\"\n\tcase OrderLinkType:\n\t\treturn \"order\"\n\tcase NewAuthzLinkType:\n\t\treturn \"new-authz\"\n\tcase AuthzLinkType:\n\t\treturn \"authz\"\n\tcase ChallengeLinkType:\n\t\treturn \"challenge\"\n\tcase CertificateLinkType:\n\t\treturn \"certificate\"\n\tcase DirectoryLinkType:\n\t\treturn \"directory\"\n\tcase RevokeCertLinkType:\n\t\treturn \"revoke-cert\"\n\tcase KeyChangeLinkType:\n\t\treturn \"key-change\"\n\tdefault:\n\t\treturn fmt.Sprintf(\"unexpected LinkType '%d'\", int(l))\n\t}\n}\n\nfunc GetUnescapedPathSuffix(typ LinkType, provisionerName string, inputs ...string) string {\n\tswitch typ {\n\tcase NewNonceLinkType, NewAccountLinkType, NewOrderLinkType, NewAuthzLinkType, DirectoryLinkType, KeyChangeLinkType, RevokeCertLinkType:\n\t\treturn fmt.Sprintf(\"/%s/%s\", provisionerName, typ)\n\tcase AccountLinkType, OrderLinkType, AuthzLinkType, CertificateLinkType:\n\t\treturn fmt.Sprintf(\"/%s/%s/%s\", provisionerName, typ, inputs[0])\n\tcase ChallengeLinkType:\n\t\treturn fmt.Sprintf(\"/%s/%s/%s/%s\", provisionerName, typ, inputs[0], inputs[1]) //nolint:gosec // operating on internally defined inputs\n\tcase OrdersByAccountLinkType:\n\t\treturn fmt.Sprintf(\"/%s/%s/%s/orders\", provisionerName, AccountLinkType, inputs[0])\n\tcase FinalizeLinkType:\n\t\treturn fmt.Sprintf(\"/%s/%s/%s/finalize\", provisionerName, OrderLinkType, inputs[0])\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\n// NewLinker returns a new Directory type.\nfunc NewLinker(dns, prefix string) Linker {\n\t_, _, err := net.SplitHostPort(dns)\n\tif err != nil && strings.Contains(err.Error(), \"too many colons in address\") {\n\t\t// this is most probably an IPv6 without brackets, e.g. ::1, 2001:0db8:85a3:0000:0000:8a2e:0370:7334\n\t\t// in case a port was appended to this wrong format, we try to extract the port, then check if it's\n\t\t// still a valid IPv6: 2001:0db8:85a3:0000:0000:8a2e:0370:7334:8443 (8443 is the port). If none of\n\t\t// these cases, then the input dns is not changed.\n\t\tlastIndex := strings.LastIndex(dns, \":\")\n\t\thostPart, portPart := dns[:lastIndex], dns[lastIndex+1:]\n\t\tif ip := net.ParseIP(hostPart); ip != nil {\n\t\t\tdns = \"[\" + hostPart + \"]:\" + portPart\n\t\t} else if ip := net.ParseIP(dns); ip != nil {\n\t\t\tdns = \"[\" + dns + \"]\"\n\t\t}\n\t}\n\treturn &linker{prefix: prefix, dns: dns}\n}\n\n// Linker interface for generating links for ACME resources.\ntype Linker interface {\n\tGetLink(ctx context.Context, typ LinkType, inputs ...string) string\n\tMiddleware(http.Handler) http.Handler\n\tLinkOrder(ctx context.Context, o *Order)\n\tLinkAccount(ctx context.Context, o *Account)\n\tLinkChallenge(ctx context.Context, o *Challenge, azID string)\n\tLinkAuthorization(ctx context.Context, o *Authorization)\n\tLinkOrdersByAccountID(ctx context.Context, orders []string)\n}\n\ntype linkerKey struct{}\n\n// NewLinkerContext adds the given linker to the context.\nfunc NewLinkerContext(ctx context.Context, v Linker) context.Context {\n\treturn context.WithValue(ctx, linkerKey{}, v)\n}\n\n// LinkerFromContext returns the current linker from the given context.\nfunc LinkerFromContext(ctx context.Context) (v Linker, ok bool) {\n\tv, ok = ctx.Value(linkerKey{}).(Linker)\n\treturn\n}\n\n// MustLinkerFromContext returns the current linker from the given context. It\n// will panic if it's not in the context.\nfunc MustLinkerFromContext(ctx context.Context) Linker {\n\tvar (\n\t\tv  Linker\n\t\tok bool\n\t)\n\tif v, ok = LinkerFromContext(ctx); !ok {\n\t\tpanic(\"acme linker is not the context\")\n\t}\n\treturn v\n}\n\ntype baseURLKey struct{}\n\nfunc newBaseURLContext(ctx context.Context, r *http.Request) context.Context {\n\tvar u *url.URL\n\tif r.Host != \"\" {\n\t\tu = &url.URL{Scheme: \"https\", Host: r.Host}\n\t}\n\treturn context.WithValue(ctx, baseURLKey{}, u)\n}\n\nfunc baseURLFromContext(ctx context.Context) *url.URL {\n\tif u, ok := ctx.Value(baseURLKey{}).(*url.URL); ok {\n\t\treturn u\n\t}\n\treturn nil\n}\n\n// linker generates ACME links.\ntype linker struct {\n\tprefix string\n\tdns    string\n}\n\n// Middleware gets the provisioner and current url from the request and sets\n// them in the context so we can use the linker to create ACME links.\nfunc (l *linker) Middleware(next http.Handler) http.Handler {\n\treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t// Add base url to the context.\n\t\tctx := newBaseURLContext(r.Context(), r)\n\n\t\t// Add provisioner to the context.\n\t\tnameEscaped := chi.URLParam(r, \"provisionerID\")\n\t\tname, err := url.PathUnescape(nameEscaped)\n\t\tif err != nil {\n\t\t\trender.Error(w, r, WrapErrorISE(err, \"error url unescaping provisioner name '%s'\", nameEscaped))\n\t\t\treturn\n\t\t}\n\n\t\tp, err := authority.MustFromContext(ctx).LoadProvisionerByName(name)\n\t\tif err != nil {\n\t\t\trender.Error(w, r, err)\n\t\t\treturn\n\t\t}\n\n\t\tvar acmeProv *provisioner.ACME\n\t\tswitch prov := p.(type) {\n\t\tcase *provisioner.ACME:\n\t\t\tacmeProv = prov\n\t\tcase provisioner.Uninitialized:\n\t\t\trender.Error(w, r, NewDetailedError(ErrorUnauthorizedType, \"provisioner is disabled due to an initialization error\"))\n\t\t\treturn\n\t\tdefault:\n\t\t\trender.Error(w, r, NewDetailedError(ErrorUnauthorizedType, \"provisioner must be of type ACME\"))\n\t\t\treturn\n\t\t}\n\n\t\tctx = NewProvisionerContext(ctx, Provisioner(acmeProv))\n\t\tnext.ServeHTTP(w, r.WithContext(ctx))\n\t})\n}\n\n// GetLink is a helper for GetLinkExplicit.\nfunc (l *linker) GetLink(ctx context.Context, typ LinkType, inputs ...string) string {\n\tvar name string\n\tif p, ok := ProvisionerFromContext(ctx); ok {\n\t\tname = p.GetName()\n\t}\n\n\tvar u url.URL\n\tif baseURL := baseURLFromContext(ctx); baseURL != nil {\n\t\tu = *baseURL\n\t}\n\tif u.Scheme == \"\" {\n\t\tu.Scheme = \"https\"\n\t}\n\tif u.Host == \"\" {\n\t\tu.Host = l.dns\n\t}\n\n\tu.Path = l.prefix + GetUnescapedPathSuffix(typ, name, inputs...)\n\treturn u.String()\n}\n\n// LinkOrder sets the ACME links required by an ACME order.\nfunc (l *linker) LinkOrder(ctx context.Context, o *Order) {\n\to.AuthorizationURLs = make([]string, len(o.AuthorizationIDs))\n\tfor i, azID := range o.AuthorizationIDs {\n\t\to.AuthorizationURLs[i] = l.GetLink(ctx, AuthzLinkType, azID)\n\t}\n\to.FinalizeURL = l.GetLink(ctx, FinalizeLinkType, o.ID)\n\tif o.CertificateID != \"\" {\n\t\to.CertificateURL = l.GetLink(ctx, CertificateLinkType, o.CertificateID)\n\t}\n}\n\n// LinkAccount sets the ACME links required by an ACME account.\nfunc (l *linker) LinkAccount(ctx context.Context, acc *Account) {\n\tacc.OrdersURL = l.GetLink(ctx, OrdersByAccountLinkType, acc.ID)\n}\n\n// LinkChallenge sets the ACME links required by an ACME challenge.\nfunc (l *linker) LinkChallenge(ctx context.Context, ch *Challenge, azID string) {\n\tch.URL = l.GetLink(ctx, ChallengeLinkType, azID, ch.ID)\n}\n\n// LinkAuthorization sets the ACME links required by an ACME authorization.\nfunc (l *linker) LinkAuthorization(ctx context.Context, az *Authorization) {\n\tfor _, ch := range az.Challenges {\n\t\tl.LinkChallenge(ctx, ch, az.ID)\n\t}\n}\n\n// LinkOrdersByAccountID converts each order ID to an ACME link.\nfunc (l *linker) LinkOrdersByAccountID(ctx context.Context, orders []string) {\n\tfor i, id := range orders {\n\t\torders[i] = l.GetLink(ctx, OrderLinkType, id)\n\t}\n}\n"
  },
  {
    "path": "acme/linker_test.go",
    "content": "package acme\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/url\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/smallstep/assert\"\n\t\"github.com/smallstep/certificates/authority/provisioner\"\n)\n\nfunc mockProvisioner(t *testing.T) Provisioner {\n\tt.Helper()\n\tvar defaultDisableRenewal = false\n\n\t// Initialize provisioners\n\tp := &provisioner.ACME{\n\t\tType: \"ACME\",\n\t\tName: \"test@acme-<test>provisioner.com\",\n\t}\n\tif err := p.Init(provisioner.Config{Claims: provisioner.Claims{\n\t\tMinTLSDur:      &provisioner.Duration{Duration: 5 * time.Minute},\n\t\tMaxTLSDur:      &provisioner.Duration{Duration: 24 * time.Hour},\n\t\tDefaultTLSDur:  &provisioner.Duration{Duration: 24 * time.Hour},\n\t\tDisableRenewal: &defaultDisableRenewal,\n\t}}); err != nil {\n\t\tfmt.Printf(\"%v\", err)\n\t}\n\treturn p\n}\n\nfunc TestGetUnescapedPathSuffix(t *testing.T) {\n\tgetPath := GetUnescapedPathSuffix\n\n\tassert.Equals(t, getPath(NewNonceLinkType, \"{provisionerID}\"), \"/{provisionerID}/new-nonce\")\n\tassert.Equals(t, getPath(DirectoryLinkType, \"{provisionerID}\"), \"/{provisionerID}/directory\")\n\tassert.Equals(t, getPath(NewAccountLinkType, \"{provisionerID}\"), \"/{provisionerID}/new-account\")\n\tassert.Equals(t, getPath(AccountLinkType, \"{provisionerID}\", \"{accID}\"), \"/{provisionerID}/account/{accID}\")\n\tassert.Equals(t, getPath(KeyChangeLinkType, \"{provisionerID}\"), \"/{provisionerID}/key-change\")\n\tassert.Equals(t, getPath(NewOrderLinkType, \"{provisionerID}\"), \"/{provisionerID}/new-order\")\n\tassert.Equals(t, getPath(OrderLinkType, \"{provisionerID}\", \"{ordID}\"), \"/{provisionerID}/order/{ordID}\")\n\tassert.Equals(t, getPath(OrdersByAccountLinkType, \"{provisionerID}\", \"{accID}\"), \"/{provisionerID}/account/{accID}/orders\")\n\tassert.Equals(t, getPath(FinalizeLinkType, \"{provisionerID}\", \"{ordID}\"), \"/{provisionerID}/order/{ordID}/finalize\")\n\tassert.Equals(t, getPath(AuthzLinkType, \"{provisionerID}\", \"{authzID}\"), \"/{provisionerID}/authz/{authzID}\")\n\tassert.Equals(t, getPath(ChallengeLinkType, \"{provisionerID}\", \"{authzID}\", \"{chID}\"), \"/{provisionerID}/challenge/{authzID}/{chID}\")\n\tassert.Equals(t, getPath(CertificateLinkType, \"{provisionerID}\", \"{certID}\"), \"/{provisionerID}/certificate/{certID}\")\n}\n\nfunc TestLinker_DNS(t *testing.T) {\n\tprov := mockProvisioner(t)\n\tescProvName := url.PathEscape(prov.GetName())\n\tctx := NewProvisionerContext(context.Background(), prov)\n\ttype test struct {\n\t\tname                  string\n\t\tdns                   string\n\t\tprefix                string\n\t\texpectedDirectoryLink string\n\t}\n\ttests := []test{\n\t\t{\n\t\t\tname:                  \"domain\",\n\t\t\tdns:                   \"ca.smallstep.com\",\n\t\t\tprefix:                \"acme\",\n\t\t\texpectedDirectoryLink: fmt.Sprintf(\"https://ca.smallstep.com/acme/%s/directory\", escProvName),\n\t\t},\n\t\t{\n\t\t\tname:                  \"domain-port\",\n\t\t\tdns:                   \"ca.smallstep.com:8443\",\n\t\t\tprefix:                \"acme\",\n\t\t\texpectedDirectoryLink: fmt.Sprintf(\"https://ca.smallstep.com:8443/acme/%s/directory\", escProvName),\n\t\t},\n\t\t{\n\t\t\tname:                  \"ipv4\",\n\t\t\tdns:                   \"127.0.0.1\",\n\t\t\tprefix:                \"acme\",\n\t\t\texpectedDirectoryLink: fmt.Sprintf(\"https://127.0.0.1/acme/%s/directory\", escProvName),\n\t\t},\n\t\t{\n\t\t\tname:                  \"ipv4-port\",\n\t\t\tdns:                   \"127.0.0.1:8443\",\n\t\t\tprefix:                \"acme\",\n\t\t\texpectedDirectoryLink: fmt.Sprintf(\"https://127.0.0.1:8443/acme/%s/directory\", escProvName),\n\t\t},\n\t\t{\n\t\t\tname:                  \"ipv6\",\n\t\t\tdns:                   \"[::1]\",\n\t\t\tprefix:                \"acme\",\n\t\t\texpectedDirectoryLink: fmt.Sprintf(\"https://[::1]/acme/%s/directory\", escProvName),\n\t\t},\n\t\t{\n\t\t\tname:                  \"ipv6-port\",\n\t\t\tdns:                   \"[::1]:8443\",\n\t\t\tprefix:                \"acme\",\n\t\t\texpectedDirectoryLink: fmt.Sprintf(\"https://[::1]:8443/acme/%s/directory\", escProvName),\n\t\t},\n\t\t{\n\t\t\tname:                  \"ipv6-no-brackets\",\n\t\t\tdns:                   \"::1\",\n\t\t\tprefix:                \"acme\",\n\t\t\texpectedDirectoryLink: fmt.Sprintf(\"https://[::1]/acme/%s/directory\", escProvName),\n\t\t},\n\t\t{\n\t\t\tname:                  \"ipv6-port-no-brackets\",\n\t\t\tdns:                   \"::1:8443\",\n\t\t\tprefix:                \"acme\",\n\t\t\texpectedDirectoryLink: fmt.Sprintf(\"https://[::1]:8443/acme/%s/directory\", escProvName),\n\t\t},\n\t\t{\n\t\t\tname:                  \"ipv6-long-no-brackets\",\n\t\t\tdns:                   \"2001:0db8:85a3:0000:0000:8a2e:0370:7334\",\n\t\t\tprefix:                \"acme\",\n\t\t\texpectedDirectoryLink: fmt.Sprintf(\"https://[2001:0db8:85a3:0000:0000:8a2e:0370:7334]/acme/%s/directory\", escProvName),\n\t\t},\n\t\t{\n\t\t\tname:                  \"ipv6-long-port-no-brackets\",\n\t\t\tdns:                   \"2001:0db8:85a3:0000:0000:8a2e:0370:7334:8443\",\n\t\t\tprefix:                \"acme\",\n\t\t\texpectedDirectoryLink: fmt.Sprintf(\"https://[2001:0db8:85a3:0000:0000:8a2e:0370:7334]:8443/acme/%s/directory\", escProvName),\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tlinker := NewLinker(tt.dns, tt.prefix)\n\t\t\tassert.Equals(t, tt.expectedDirectoryLink, linker.GetLink(ctx, DirectoryLinkType))\n\t\t})\n\t}\n}\n\nfunc TestLinker_GetLink(t *testing.T) {\n\tdns := \"ca.smallstep.com\"\n\tprefix := \"acme\"\n\tlinker := NewLinker(dns, prefix)\n\tid := \"1234\"\n\n\tprov := mockProvisioner(t)\n\tescProvName := url.PathEscape(prov.GetName())\n\tbaseURL := &url.URL{Scheme: \"https\", Host: \"test.ca.smallstep.com\"}\n\tctx := NewProvisionerContext(context.Background(), prov)\n\tctx = context.WithValue(ctx, baseURLKey{}, baseURL)\n\n\t// No provisioner and no BaseURL from request\n\tassert.Equals(t, linker.GetLink(context.Background(), NewNonceLinkType), fmt.Sprintf(\"%s/acme/%s/new-nonce\", \"https://ca.smallstep.com\", \"\"))\n\t// Provisioner: yes, BaseURL: no\n\tassert.Equals(t, linker.GetLink(context.WithValue(context.Background(), provisionerKey{}, prov), NewNonceLinkType), fmt.Sprintf(\"%s/acme/%s/new-nonce\", \"https://ca.smallstep.com\", escProvName))\n\n\t// Provisioner: no, BaseURL: yes\n\tassert.Equals(t, linker.GetLink(context.WithValue(context.Background(), baseURLKey{}, baseURL), NewNonceLinkType), fmt.Sprintf(\"%s/acme/%s/new-nonce\", \"https://test.ca.smallstep.com\", \"\"))\n\n\tassert.Equals(t, linker.GetLink(ctx, NewNonceLinkType), fmt.Sprintf(\"%s/acme/%s/new-nonce\", baseURL, escProvName))\n\tassert.Equals(t, linker.GetLink(ctx, NewNonceLinkType), fmt.Sprintf(\"%s/acme/%s/new-nonce\", baseURL, escProvName))\n\n\tassert.Equals(t, linker.GetLink(ctx, NewAccountLinkType), fmt.Sprintf(\"%s/acme/%s/new-account\", baseURL, escProvName))\n\n\tassert.Equals(t, linker.GetLink(ctx, AccountLinkType, id), fmt.Sprintf(\"%s/acme/%s/account/1234\", baseURL, escProvName))\n\n\tassert.Equals(t, linker.GetLink(ctx, NewOrderLinkType), fmt.Sprintf(\"%s/acme/%s/new-order\", baseURL, escProvName))\n\n\tassert.Equals(t, linker.GetLink(ctx, OrderLinkType, id), fmt.Sprintf(\"%s/acme/%s/order/1234\", baseURL, escProvName))\n\n\tassert.Equals(t, linker.GetLink(ctx, OrdersByAccountLinkType, id), fmt.Sprintf(\"%s/acme/%s/account/1234/orders\", baseURL, escProvName))\n\n\tassert.Equals(t, linker.GetLink(ctx, FinalizeLinkType, id), fmt.Sprintf(\"%s/acme/%s/order/1234/finalize\", baseURL, escProvName))\n\n\tassert.Equals(t, linker.GetLink(ctx, NewAuthzLinkType), fmt.Sprintf(\"%s/acme/%s/new-authz\", baseURL, escProvName))\n\n\tassert.Equals(t, linker.GetLink(ctx, AuthzLinkType, id), fmt.Sprintf(\"%s/acme/%s/authz/1234\", baseURL, escProvName))\n\n\tassert.Equals(t, linker.GetLink(ctx, DirectoryLinkType), fmt.Sprintf(\"%s/acme/%s/directory\", baseURL, escProvName))\n\n\tassert.Equals(t, linker.GetLink(ctx, RevokeCertLinkType, id), fmt.Sprintf(\"%s/acme/%s/revoke-cert\", baseURL, escProvName))\n\n\tassert.Equals(t, linker.GetLink(ctx, KeyChangeLinkType), fmt.Sprintf(\"%s/acme/%s/key-change\", baseURL, escProvName))\n\n\tassert.Equals(t, linker.GetLink(ctx, ChallengeLinkType, id, id), fmt.Sprintf(\"%s/acme/%s/challenge/%s/%s\", baseURL, escProvName, id, id))\n\n\tassert.Equals(t, linker.GetLink(ctx, CertificateLinkType, id), fmt.Sprintf(\"%s/acme/%s/certificate/1234\", baseURL, escProvName))\n}\n\nfunc TestLinker_LinkOrder(t *testing.T) {\n\tbaseURL := &url.URL{Scheme: \"https\", Host: \"test.ca.smallstep.com\"}\n\tprov := mockProvisioner(t)\n\tprovName := url.PathEscape(prov.GetName())\n\tctx := NewProvisionerContext(context.Background(), prov)\n\tctx = context.WithValue(ctx, baseURLKey{}, baseURL)\n\n\toid := \"orderID\"\n\tcertID := \"certID\"\n\tlinkerPrefix := \"acme\"\n\tl := NewLinker(\"dns\", linkerPrefix)\n\ttype test struct {\n\t\to        *Order\n\t\tvalidate func(o *Order)\n\t}\n\tvar tests = map[string]test{\n\t\t\"no-authz-and-no-cert\": {\n\t\t\to: &Order{\n\t\t\t\tID: oid,\n\t\t\t},\n\t\t\tvalidate: func(o *Order) {\n\t\t\t\tassert.Equals(t, o.FinalizeURL, fmt.Sprintf(\"%s/%s/%s/order/%s/finalize\", baseURL, linkerPrefix, provName, oid))\n\t\t\t\tassert.Equals(t, o.AuthorizationURLs, []string{})\n\t\t\t\tassert.Equals(t, o.CertificateURL, \"\")\n\t\t\t},\n\t\t},\n\t\t\"one-authz-and-cert\": {\n\t\t\to: &Order{\n\t\t\t\tID:               oid,\n\t\t\t\tCertificateID:    certID,\n\t\t\t\tAuthorizationIDs: []string{\"foo\"},\n\t\t\t},\n\t\t\tvalidate: func(o *Order) {\n\t\t\t\tassert.Equals(t, o.FinalizeURL, fmt.Sprintf(\"%s/%s/%s/order/%s/finalize\", baseURL, linkerPrefix, provName, oid))\n\t\t\t\tassert.Equals(t, o.AuthorizationURLs, []string{\n\t\t\t\t\tfmt.Sprintf(\"%s/%s/%s/authz/%s\", baseURL, linkerPrefix, provName, \"foo\"),\n\t\t\t\t})\n\t\t\t\tassert.Equals(t, o.CertificateURL, fmt.Sprintf(\"%s/%s/%s/certificate/%s\", baseURL, linkerPrefix, provName, certID))\n\t\t\t},\n\t\t},\n\t\t\"many-authz\": {\n\t\t\to: &Order{\n\t\t\t\tID:               oid,\n\t\t\t\tCertificateID:    certID,\n\t\t\t\tAuthorizationIDs: []string{\"foo\", \"bar\", \"zap\"},\n\t\t\t},\n\t\t\tvalidate: func(o *Order) {\n\t\t\t\tassert.Equals(t, o.FinalizeURL, fmt.Sprintf(\"%s/%s/%s/order/%s/finalize\", baseURL, linkerPrefix, provName, oid))\n\t\t\t\tassert.Equals(t, o.AuthorizationURLs, []string{\n\t\t\t\t\tfmt.Sprintf(\"%s/%s/%s/authz/%s\", baseURL, linkerPrefix, provName, \"foo\"),\n\t\t\t\t\tfmt.Sprintf(\"%s/%s/%s/authz/%s\", baseURL, linkerPrefix, provName, \"bar\"),\n\t\t\t\t\tfmt.Sprintf(\"%s/%s/%s/authz/%s\", baseURL, linkerPrefix, provName, \"zap\"),\n\t\t\t\t})\n\t\t\t\tassert.Equals(t, o.CertificateURL, fmt.Sprintf(\"%s/%s/%s/certificate/%s\", baseURL, linkerPrefix, provName, certID))\n\t\t\t},\n\t\t},\n\t}\n\tfor name, tc := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tl.LinkOrder(ctx, tc.o)\n\t\t\ttc.validate(tc.o)\n\t\t})\n\t}\n}\n\nfunc TestLinker_LinkAccount(t *testing.T) {\n\tbaseURL := &url.URL{Scheme: \"https\", Host: \"test.ca.smallstep.com\"}\n\tprov := mockProvisioner(t)\n\tprovName := url.PathEscape(prov.GetName())\n\tctx := NewProvisionerContext(context.Background(), prov)\n\tctx = context.WithValue(ctx, baseURLKey{}, baseURL)\n\n\taccID := \"accountID\"\n\tlinkerPrefix := \"acme\"\n\tl := NewLinker(\"dns\", linkerPrefix)\n\ttype test struct {\n\t\ta        *Account\n\t\tvalidate func(o *Account)\n\t}\n\tvar tests = map[string]test{\n\t\t\"ok\": {\n\t\t\ta: &Account{\n\t\t\t\tID: accID,\n\t\t\t},\n\t\t\tvalidate: func(a *Account) {\n\t\t\t\tassert.Equals(t, a.OrdersURL, fmt.Sprintf(\"%s/%s/%s/account/%s/orders\", baseURL, linkerPrefix, provName, accID))\n\t\t\t},\n\t\t},\n\t}\n\tfor name, tc := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tl.LinkAccount(ctx, tc.a)\n\t\t\ttc.validate(tc.a)\n\t\t})\n\t}\n}\n\nfunc TestLinker_LinkChallenge(t *testing.T) {\n\tbaseURL := &url.URL{Scheme: \"https\", Host: \"test.ca.smallstep.com\"}\n\tprov := mockProvisioner(t)\n\tprovName := url.PathEscape(prov.GetName())\n\tctx := NewProvisionerContext(context.Background(), prov)\n\tctx = context.WithValue(ctx, baseURLKey{}, baseURL)\n\n\tchID := \"chID\"\n\tazID := \"azID\"\n\tlinkerPrefix := \"acme\"\n\tl := NewLinker(\"dns\", linkerPrefix)\n\ttype test struct {\n\t\tch       *Challenge\n\t\tvalidate func(o *Challenge)\n\t}\n\tvar tests = map[string]test{\n\t\t\"ok\": {\n\t\t\tch: &Challenge{\n\t\t\t\tID: chID,\n\t\t\t},\n\t\t\tvalidate: func(ch *Challenge) {\n\t\t\t\tassert.Equals(t, ch.URL, fmt.Sprintf(\"%s/%s/%s/challenge/%s/%s\", baseURL, linkerPrefix, provName, azID, ch.ID))\n\t\t\t},\n\t\t},\n\t}\n\tfor name, tc := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tl.LinkChallenge(ctx, tc.ch, azID)\n\t\t\ttc.validate(tc.ch)\n\t\t})\n\t}\n}\n\nfunc TestLinker_LinkAuthorization(t *testing.T) {\n\tbaseURL := &url.URL{Scheme: \"https\", Host: \"test.ca.smallstep.com\"}\n\tprov := mockProvisioner(t)\n\tprovName := url.PathEscape(prov.GetName())\n\tctx := NewProvisionerContext(context.Background(), prov)\n\tctx = context.WithValue(ctx, baseURLKey{}, baseURL)\n\n\tchID0 := \"chID-0\"\n\tchID1 := \"chID-1\"\n\tchID2 := \"chID-2\"\n\tazID := \"azID\"\n\tlinkerPrefix := \"acme\"\n\tl := NewLinker(\"dns\", linkerPrefix)\n\ttype test struct {\n\t\taz       *Authorization\n\t\tvalidate func(o *Authorization)\n\t}\n\tvar tests = map[string]test{\n\t\t\"ok\": {\n\t\t\taz: &Authorization{\n\t\t\t\tID: azID,\n\t\t\t\tChallenges: []*Challenge{\n\t\t\t\t\t{ID: chID0},\n\t\t\t\t\t{ID: chID1},\n\t\t\t\t\t{ID: chID2},\n\t\t\t\t},\n\t\t\t},\n\t\t\tvalidate: func(az *Authorization) {\n\t\t\t\tassert.Equals(t, az.Challenges[0].URL, fmt.Sprintf(\"%s/%s/%s/challenge/%s/%s\", baseURL, linkerPrefix, provName, az.ID, chID0))\n\t\t\t\tassert.Equals(t, az.Challenges[1].URL, fmt.Sprintf(\"%s/%s/%s/challenge/%s/%s\", baseURL, linkerPrefix, provName, az.ID, chID1))\n\t\t\t\tassert.Equals(t, az.Challenges[2].URL, fmt.Sprintf(\"%s/%s/%s/challenge/%s/%s\", baseURL, linkerPrefix, provName, az.ID, chID2))\n\t\t\t},\n\t\t},\n\t}\n\tfor name, tc := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tl.LinkAuthorization(ctx, tc.az)\n\t\t\ttc.validate(tc.az)\n\t\t})\n\t}\n}\n\nfunc TestLinker_LinkOrdersByAccountID(t *testing.T) {\n\tbaseURL := &url.URL{Scheme: \"https\", Host: \"test.ca.smallstep.com\"}\n\tprov := mockProvisioner(t)\n\tprovName := url.PathEscape(prov.GetName())\n\tctx := NewProvisionerContext(context.Background(), prov)\n\tctx = context.WithValue(ctx, baseURLKey{}, baseURL)\n\n\tlinkerPrefix := \"acme\"\n\tl := NewLinker(\"dns\", linkerPrefix)\n\ttype test struct {\n\t\toids []string\n\t}\n\tvar tests = map[string]test{\n\t\t\"ok\": {\n\t\t\toids: []string{\"foo\", \"bar\", \"baz\"},\n\t\t},\n\t}\n\tfor name, tc := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tl.LinkOrdersByAccountID(ctx, tc.oids)\n\t\t\tassert.Equals(t, tc.oids, []string{\n\t\t\t\tfmt.Sprintf(\"%s/%s/%s/order/%s\", baseURL, linkerPrefix, provName, \"foo\"),\n\t\t\t\tfmt.Sprintf(\"%s/%s/%s/order/%s\", baseURL, linkerPrefix, provName, \"bar\"),\n\t\t\t\tfmt.Sprintf(\"%s/%s/%s/order/%s\", baseURL, linkerPrefix, provName, \"baz\"),\n\t\t\t})\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "acme/nonce.go",
    "content": "package acme\n\n// Nonce represents an ACME nonce type.\ntype Nonce string\n\n// String implements the ToString interface.\nfunc (n Nonce) String() string {\n\treturn string(n)\n}\n"
  },
  {
    "path": "acme/order.go",
    "content": "package acme\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/subtle\"\n\t\"crypto/x509\"\n\t\"encoding/asn1\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/url\"\n\t\"sort\"\n\t\"strings\"\n\t\"time\"\n\n\t\"go.step.sm/crypto/keyutil\"\n\t\"go.step.sm/crypto/x509util\"\n\n\t\"github.com/smallstep/certificates/acme/wire\"\n\t\"github.com/smallstep/certificates/authority/provisioner\"\n\t\"github.com/smallstep/certificates/webhook\"\n)\n\ntype IdentifierType string\n\nconst (\n\t// IP is the ACME ip identifier type\n\tIP IdentifierType = \"ip\"\n\t// DNS is the ACME dns identifier type\n\tDNS IdentifierType = \"dns\"\n\t// PermanentIdentifier is the ACME permanent-identifier identifier type\n\t// defined in https://datatracker.ietf.org/doc/html/draft-bweeks-acme-device-attest-00\n\tPermanentIdentifier IdentifierType = \"permanent-identifier\"\n\t// WireUser is the Wire user identifier type\n\tWireUser IdentifierType = \"wireapp-user\"\n\t// WireDevice is the Wire device identifier type\n\tWireDevice IdentifierType = \"wireapp-device\"\n)\n\n// Identifier encodes the type that an order pertains to.\ntype Identifier struct {\n\tType  IdentifierType `json:\"type\"`\n\tValue string         `json:\"value\"`\n}\n\n// Order contains order metadata for the ACME protocol order type.\ntype Order struct {\n\tID                string       `json:\"id\"`\n\tAccountID         string       `json:\"-\"`\n\tProvisionerID     string       `json:\"-\"`\n\tStatus            Status       `json:\"status\"`\n\tExpiresAt         time.Time    `json:\"expires\"`\n\tIdentifiers       []Identifier `json:\"identifiers\"`\n\tNotBefore         time.Time    `json:\"notBefore\"`\n\tNotAfter          time.Time    `json:\"notAfter\"`\n\tError             *Error       `json:\"error,omitempty\"`\n\tAuthorizationIDs  []string     `json:\"-\"`\n\tAuthorizationURLs []string     `json:\"authorizations\"`\n\tFinalizeURL       string       `json:\"finalize\"`\n\tCertificateID     string       `json:\"-\"`\n\tCertificateURL    string       `json:\"certificate,omitempty\"`\n}\n\n// ToLog enables response logging.\nfunc (o *Order) ToLog() (interface{}, error) {\n\tb, err := json.Marshal(o)\n\tif err != nil {\n\t\treturn nil, WrapErrorISE(err, \"error marshaling order for logging\")\n\t}\n\treturn string(b), nil\n}\n\n// UpdateStatus updates the ACME Order Status if necessary.\n// Changes to the order are saved using the database interface.\nfunc (o *Order) UpdateStatus(ctx context.Context, db DB) error {\n\tnow := clock.Now()\n\n\tswitch o.Status {\n\tcase StatusInvalid:\n\t\treturn nil\n\tcase StatusValid:\n\t\treturn nil\n\tcase StatusReady:\n\t\t// Check expiry\n\t\tif now.After(o.ExpiresAt) {\n\t\t\to.Status = StatusInvalid\n\t\t\to.Error = NewError(ErrorMalformedType, \"order has expired\")\n\t\t\tbreak\n\t\t}\n\t\treturn nil\n\tcase StatusPending:\n\t\t// Check expiry\n\t\tif now.After(o.ExpiresAt) {\n\t\t\to.Status = StatusInvalid\n\t\t\to.Error = NewError(ErrorMalformedType, \"order has expired\")\n\t\t\tbreak\n\t\t}\n\n\t\tvar count = map[Status]int{\n\t\t\tStatusValid:   0,\n\t\t\tStatusInvalid: 0,\n\t\t\tStatusPending: 0,\n\t\t}\n\t\tfor _, azID := range o.AuthorizationIDs {\n\t\t\taz, err := db.GetAuthorization(ctx, azID)\n\t\t\tif err != nil {\n\t\t\t\treturn WrapErrorISE(err, \"error getting authorization ID %s\", azID)\n\t\t\t}\n\t\t\tif err = az.UpdateStatus(ctx, db); err != nil {\n\t\t\t\treturn WrapErrorISE(err, \"error updating authorization ID %s\", azID)\n\t\t\t}\n\t\t\tst := az.Status\n\t\t\tcount[st]++\n\t\t}\n\t\tswitch {\n\t\tcase count[StatusInvalid] > 0:\n\t\t\to.Status = StatusInvalid\n\n\t\t// No change in the order status, so just return the order as is -\n\t\t// without writing any changes.\n\t\tcase count[StatusPending] > 0:\n\t\t\treturn nil\n\n\t\tcase count[StatusValid] == len(o.AuthorizationIDs):\n\t\t\to.Status = StatusReady\n\n\t\tdefault:\n\t\t\treturn NewErrorISE(\"unexpected authz status\")\n\t\t}\n\tdefault:\n\t\treturn NewErrorISE(\"unrecognized order status: %s\", o.Status)\n\t}\n\n\tif err := db.UpdateOrder(ctx, o); err != nil {\n\t\treturn WrapErrorISE(err, \"error updating order\")\n\t}\n\n\treturn nil\n}\n\n// getAuthorizationFingerprint returns a fingerprint from the list of authorizations. This\n// fingerprint is used on the device-attest-01 flow to verify the attestation\n// certificate public key with the CSR public key.\n//\n// There's no point on reading all the authorizations as there will be only one\n// for a permanent identifier.\nfunc (o *Order) getAuthorizationFingerprint(ctx context.Context, db DB) (string, error) {\n\tfor _, azID := range o.AuthorizationIDs {\n\t\taz, err := db.GetAuthorization(ctx, azID)\n\t\tif err != nil {\n\t\t\treturn \"\", WrapErrorISE(err, \"error getting authorization %q\", azID)\n\t\t}\n\t\t// There's no point on reading all the authorizations as there will\n\t\t// be only one for a permanent identifier.\n\t\tif az.Fingerprint != \"\" {\n\t\t\treturn az.Fingerprint, nil\n\t\t}\n\t}\n\treturn \"\", nil\n}\n\n// Finalize signs a certificate if the necessary conditions for Order completion\n// have been met.\n//\n// TODO(mariano): Here or in the challenge validation we should perform some\n// external validation using the identifier value and the attestation data. From\n// a validation service we can get the list of SANs to set in the final\n// certificate.\nfunc (o *Order) Finalize(ctx context.Context, db DB, csr *x509.CertificateRequest, auth CertificateAuthority, p Provisioner) error {\n\tif err := o.UpdateStatus(ctx, db); err != nil {\n\t\treturn err\n\t}\n\n\tswitch o.Status {\n\tcase StatusInvalid:\n\t\treturn NewError(ErrorOrderNotReadyType, \"order %s has been abandoned\", o.ID)\n\tcase StatusValid:\n\t\treturn nil\n\tcase StatusPending:\n\t\treturn NewError(ErrorOrderNotReadyType, \"order %s is not ready\", o.ID)\n\tcase StatusReady:\n\t\tbreak\n\tdefault:\n\t\treturn NewErrorISE(\"unexpected status %s for order %s\", o.Status, o.ID)\n\t}\n\n\t// Get key fingerprint if any. And then compare it with the CSR fingerprint.\n\t//\n\t// In device-attest-01 challenges we should check that the keys in the CSR\n\t// and the attestation certificate are the same.\n\tfingerprint, err := o.getAuthorizationFingerprint(ctx, db)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif fingerprint != \"\" {\n\t\tfp, err := keyutil.Fingerprint(csr.PublicKey)\n\t\tif err != nil {\n\t\t\treturn WrapErrorISE(err, \"error calculating key fingerprint\")\n\t\t}\n\t\tif subtle.ConstantTimeCompare([]byte(fingerprint), []byte(fp)) == 0 {\n\t\t\treturn NewError(ErrorUnauthorizedType, \"order %s csr does not match the attested key\", o.ID)\n\t\t}\n\t}\n\n\t// canonicalize the CSR to allow for comparison\n\tcsr = canonicalize(csr)\n\n\t// Template data\n\tdata := x509util.NewTemplateData()\n\tif o.containsWireIdentifiers() {\n\t\twireDB, ok := db.(WireDB)\n\t\tif !ok {\n\t\t\treturn fmt.Errorf(\"db %T is not a WireDB\", db)\n\t\t}\n\t\tsubject, err := createWireSubject(o, csr)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed creating Wire subject: %w\", err)\n\t\t}\n\t\tdata.SetSubject(subject)\n\n\t\t// Inject Wire's custom challenges into the template once they have been validated\n\t\tdpop, err := wireDB.GetDpopToken(ctx, o.ID)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed getting Wire DPoP token: %w\", err)\n\t\t}\n\t\tdata.Set(\"Dpop\", dpop)\n\n\t\toidc, err := wireDB.GetOidcToken(ctx, o.ID)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed getting Wire OIDC token: %w\", err)\n\t\t}\n\t\tdata.Set(\"Oidc\", oidc)\n\t} else {\n\t\tdata.SetCommonName(csr.Subject.CommonName)\n\t}\n\n\t// Custom sign options passed to authority.Sign\n\tvar extraOptions []provisioner.SignOption\n\n\t// TODO: support for multiple identifiers?\n\tvar permanentIdentifier string\n\tfor i := range o.Identifiers {\n\t\tif o.Identifiers[i].Type == PermanentIdentifier {\n\t\t\tpermanentIdentifier = o.Identifiers[i].Value\n\t\t\t// the first (and only) Permanent Identifier that gets added to the certificate\n\t\t\t// should be equal to the Subject Common Name if it's set. If not equal, the CSR\n\t\t\t// is rejected, because the Common Name hasn't been challenged in that case. This\n\t\t\t// could result in unauthorized access if a relying system relies on the Common\n\t\t\t// Name in its authorization logic.\n\t\t\tif csr.Subject.CommonName != \"\" && csr.Subject.CommonName != permanentIdentifier {\n\t\t\t\treturn NewError(ErrorBadCSRType, \"CSR Subject Common Name does not match identifiers exactly: \"+\n\t\t\t\t\t\"CSR Subject Common Name = %s, Order Permanent Identifier = %s\", csr.Subject.CommonName, permanentIdentifier)\n\t\t\t}\n\t\t\tbreak\n\t\t}\n\t}\n\n\tvar defaultTemplate string\n\tif permanentIdentifier != \"\" {\n\t\tdefaultTemplate = x509util.DefaultAttestedLeafTemplate\n\t\tdata.SetSubjectAlternativeNames(x509util.SubjectAlternativeName{\n\t\t\tType:  x509util.PermanentIdentifierType,\n\t\t\tValue: permanentIdentifier,\n\t\t})\n\t\textraOptions = append(extraOptions, provisioner.AttestationData{\n\t\t\tPermanentIdentifier: permanentIdentifier,\n\t\t})\n\t} else {\n\t\tdefaultTemplate = x509util.DefaultLeafTemplate\n\t\tsans, err := o.sans(csr)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tdata.SetSubjectAlternativeNames(sans...)\n\t}\n\n\t// Get authorizations from the ACME provisioner.\n\tctx = provisioner.NewContextWithMethod(ctx, provisioner.SignMethod)\n\tsignOps, err := p.AuthorizeSign(ctx, \"\")\n\tif err != nil {\n\t\treturn WrapErrorISE(err, \"error retrieving authorization options from ACME provisioner\")\n\t}\n\t// Unlike most of the provisioners, ACME's AuthorizeSign method doesn't\n\t// define the templates, and the template data used in WebHooks is not\n\t// available.\n\tfor _, signOp := range signOps {\n\t\tif wc, ok := signOp.(*provisioner.WebhookController); ok {\n\t\t\twc.TemplateData = data\n\t\t}\n\t}\n\n\ttemplateOptions, err := provisioner.CustomTemplateOptions(p.GetOptions(), data, defaultTemplate)\n\tif err != nil {\n\t\treturn WrapErrorISE(err, \"error creating template options from ACME provisioner\")\n\t}\n\n\t// Build extra signing options.\n\tsignOps = append(signOps, templateOptions)\n\tsignOps = append(signOps, extraOptions...)\n\n\t// Sign a new certificate.\n\tcertChain, err := auth.SignWithContext(ctx, csr, provisioner.SignOptions{\n\t\tNotBefore: provisioner.NewTimeDuration(o.NotBefore),\n\t\tNotAfter:  provisioner.NewTimeDuration(o.NotAfter),\n\t}, signOps...)\n\tif err != nil {\n\t\t// Add subproblem for webhook errors, others can be added later.\n\t\tvar webhookErr *webhook.Error\n\t\tif errors.As(err, &webhookErr) {\n\t\t\tacmeError := NewDetailedError(ErrorUnauthorizedType, \"%s\", webhookErr.Error())\n\t\t\tacmeError.AddSubproblems(Subproblem{\n\t\t\t\tType:   fmt.Sprintf(\"urn:smallstep:acme:error:%s\", webhookErr.Code),\n\t\t\t\tDetail: webhookErr.Message,\n\t\t\t})\n\t\t\treturn acmeError\n\t\t}\n\n\t\treturn WrapErrorISE(err, \"error signing certificate for order %s\", o.ID)\n\t}\n\n\tcert := &Certificate{\n\t\tAccountID:     o.AccountID,\n\t\tOrderID:       o.ID,\n\t\tLeaf:          certChain[0],\n\t\tIntermediates: certChain[1:],\n\t}\n\tif err := db.CreateCertificate(ctx, cert); err != nil {\n\t\treturn WrapErrorISE(err, \"error creating certificate for order %s\", o.ID)\n\t}\n\n\to.CertificateID = cert.ID\n\to.Status = StatusValid\n\n\tif err = db.UpdateOrder(ctx, o); err != nil {\n\t\treturn WrapErrorISE(err, \"error updating order %s\", o.ID)\n\t}\n\n\treturn nil\n}\n\n// containsWireIdentifiers checks if [Order] contains ACME\n// identifiers for the WireUser or WireDevice types.\nfunc (o *Order) containsWireIdentifiers() bool {\n\tfor _, i := range o.Identifiers {\n\t\tif i.Type == WireUser || i.Type == WireDevice {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// createWireSubject creates the subject for an [Order] with WireUser identifiers.\nfunc createWireSubject(o *Order, csr *x509.CertificateRequest) (subject x509util.Subject, err error) {\n\twireUserIDs, wireDeviceIDs, otherIDs := 0, 0, 0\n\tfor _, identifier := range o.Identifiers {\n\t\tswitch identifier.Type {\n\t\tcase WireUser:\n\t\t\twireID, err := wire.ParseUserID(identifier.Value)\n\t\t\tif err != nil {\n\t\t\t\treturn subject, NewErrorISE(\"unmarshal wireID: %s\", err)\n\t\t\t}\n\n\t\t\t// TODO: temporarily using a custom OIDC for carrying the display name without having it listed as a DNS SAN.\n\t\t\t// reusing LDAP's OID for diplay name see http://oid-info.com/get/2.16.840.1.113730.3.1.241\n\t\t\tdisplayNameOid := asn1.ObjectIdentifier{2, 16, 840, 1, 113730, 3, 1, 241}\n\t\t\tvar foundDisplayName = false\n\t\t\tfor _, entry := range csr.Subject.Names {\n\t\t\t\tif entry.Type.Equal(displayNameOid) {\n\t\t\t\t\tfoundDisplayName = true\n\t\t\t\t\tdisplayName := entry.Value.(string)\n\t\t\t\t\tif displayName != wireID.Name {\n\t\t\t\t\t\treturn subject, NewErrorISE(\"expected displayName %v, found %v\", wireID.Name, displayName)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tif !foundDisplayName {\n\t\t\t\treturn subject, NewErrorISE(\"CSR must contain the display name in '2.16.840.1.113730.3.1.241' OID\")\n\t\t\t}\n\n\t\t\tif len(csr.Subject.Organization) == 0 || !strings.EqualFold(csr.Subject.Organization[0], wireID.Domain) {\n\t\t\t\treturn subject, NewErrorISE(\"expected Organization [%s], found %v\", wireID.Domain, csr.Subject.Organization)\n\t\t\t}\n\t\t\tsubject.CommonName = wireID.Name\n\t\t\tsubject.Organization = []string{wireID.Domain}\n\t\t\twireUserIDs++\n\t\tcase WireDevice:\n\t\t\twireDeviceIDs++\n\t\tdefault:\n\t\t\totherIDs++\n\t\t}\n\t}\n\n\tif otherIDs > 0 || wireUserIDs != 1 && wireDeviceIDs != 1 {\n\t\treturn subject, NewErrorISE(\"order must have exactly one WireUser and WireDevice identifier\")\n\t}\n\n\treturn\n}\n\nfunc (o *Order) sans(csr *x509.CertificateRequest) ([]x509util.SubjectAlternativeName, error) {\n\tvar sans []x509util.SubjectAlternativeName\n\tif len(csr.EmailAddresses) > 0 {\n\t\treturn sans, NewError(ErrorBadCSRType, \"Only DNS names and IP addresses are allowed\")\n\t}\n\n\t// order the DNS names and IP addresses, so that they can be compared against the canonicalized CSR\n\torderNames := make([]string, numberOfIdentifierType(DNS, o.Identifiers))\n\torderIPs := make([]net.IP, numberOfIdentifierType(IP, o.Identifiers))\n\torderPIDs := make([]string, numberOfIdentifierType(PermanentIdentifier, o.Identifiers))\n\ttmpOrderURIs := make([]*url.URL, numberOfIdentifierType(WireUser, o.Identifiers)+numberOfIdentifierType(WireDevice, o.Identifiers))\n\tindexDNS, indexIP, indexPID, indexURI := 0, 0, 0, 0\n\tfor _, n := range o.Identifiers {\n\t\tswitch n.Type {\n\t\tcase DNS:\n\t\t\torderNames[indexDNS] = n.Value\n\t\t\tindexDNS++\n\t\tcase IP:\n\t\t\torderIPs[indexIP] = net.ParseIP(n.Value) // NOTE: this assumes are all valid IPs at this time; or will result in nil entries\n\t\t\tindexIP++\n\t\tcase PermanentIdentifier:\n\t\t\torderPIDs[indexPID] = n.Value\n\t\t\tindexPID++\n\t\tcase WireUser:\n\t\t\twireID, err := wire.ParseUserID(n.Value)\n\t\t\tif err != nil {\n\t\t\t\treturn sans, NewErrorISE(\"unsupported identifier value in order: %s\", n.Value)\n\t\t\t}\n\t\t\thandle, err := url.Parse(wireID.Handle)\n\t\t\tif err != nil {\n\t\t\t\treturn sans, NewErrorISE(\"handle must be a URI: %s\", wireID.Handle)\n\t\t\t}\n\t\t\ttmpOrderURIs[indexURI] = handle\n\t\t\tindexURI++\n\t\tcase WireDevice:\n\t\t\twireID, err := wire.ParseDeviceID(n.Value)\n\t\t\tif err != nil {\n\t\t\t\treturn sans, NewErrorISE(\"unsupported identifier value in order: %s\", n.Value)\n\t\t\t}\n\t\t\tclientID, err := url.Parse(wireID.ClientID)\n\t\t\tif err != nil {\n\t\t\t\treturn sans, NewErrorISE(\"clientId must be a URI: %s\", wireID.ClientID)\n\t\t\t}\n\t\t\ttmpOrderURIs[indexURI] = clientID\n\t\t\tindexURI++\n\t\tdefault:\n\t\t\treturn sans, NewErrorISE(\"unsupported identifier type in order: %s\", n.Type)\n\t\t}\n\t}\n\torderNames = uniqueSortedLowerNames(orderNames)\n\torderIPs = uniqueSortedIPs(orderIPs)\n\torderURIs := uniqueSortedURIStrings(tmpOrderURIs)\n\n\ttotalNumberOfSANs := len(csr.DNSNames) + len(csr.IPAddresses) + len(csr.URIs)\n\tsans = make([]x509util.SubjectAlternativeName, totalNumberOfSANs)\n\tindex := 0\n\n\t// Validate identifier names against CSR alternative names.\n\t//\n\t// Note that with certificate templates we are not going to check for the\n\t// absence of other SANs as they will only be set if the template allows\n\t// them.\n\tif len(csr.DNSNames) != len(orderNames) {\n\t\treturn sans, NewError(ErrorBadCSRType, \"CSR names do not match identifiers exactly: \"+\n\t\t\t\"CSR names = %v, Order names = %v\", csr.DNSNames, orderNames)\n\t}\n\n\tfor i := range csr.DNSNames {\n\t\tif csr.DNSNames[i] != orderNames[i] {\n\t\t\treturn sans, NewError(ErrorBadCSRType, \"CSR names do not match identifiers exactly: \"+\n\t\t\t\t\"CSR names = %v, Order names = %v\", csr.DNSNames, orderNames)\n\t\t}\n\t\tsans[index] = x509util.SubjectAlternativeName{\n\t\t\tType:  x509util.DNSType,\n\t\t\tValue: csr.DNSNames[i],\n\t\t}\n\t\tindex++\n\t}\n\n\tif len(csr.IPAddresses) != len(orderIPs) {\n\t\treturn sans, NewError(ErrorBadCSRType, \"CSR IPs do not match identifiers exactly: \"+\n\t\t\t\"CSR IPs = %v, Order IPs = %v\", csr.IPAddresses, orderIPs)\n\t}\n\n\tfor i := range csr.IPAddresses {\n\t\tif !ipsAreEqual(csr.IPAddresses[i], orderIPs[i]) {\n\t\t\treturn sans, NewError(ErrorBadCSRType, \"CSR IPs do not match identifiers exactly: \"+\n\t\t\t\t\"CSR IPs = %v, Order IPs = %v\", csr.IPAddresses, orderIPs)\n\t\t}\n\t\tsans[index] = x509util.SubjectAlternativeName{\n\t\t\tType:  x509util.IPType,\n\t\t\tValue: csr.IPAddresses[i].String(),\n\t\t}\n\t\tindex++\n\t}\n\n\tif len(csr.URIs) != len(tmpOrderURIs) {\n\t\treturn sans, NewError(ErrorBadCSRType, \"CSR URIs do not match identifiers exactly: \"+\n\t\t\t\"CSR URIs = %v, Order URIs = %v\", csr.URIs, tmpOrderURIs)\n\t}\n\n\t// sort URI list\n\tcsrURIs := uniqueSortedURIStrings(csr.URIs)\n\n\tfor i := range csrURIs {\n\t\tif csrURIs[i] != orderURIs[i] {\n\t\t\treturn sans, NewError(ErrorBadCSRType, \"CSR URIs do not match identifiers exactly: \"+\n\t\t\t\t\"CSR URIs = %v, Order URIs = %v\", csr.URIs, tmpOrderURIs)\n\t\t}\n\t\tsans[index] = x509util.SubjectAlternativeName{\n\t\t\tType:  x509util.URIType,\n\t\t\tValue: orderURIs[i],\n\t\t}\n\t\tindex++\n\t}\n\n\treturn sans, nil\n}\n\n// numberOfIdentifierType returns the number of Identifiers that\n// are of type typ.\nfunc numberOfIdentifierType(typ IdentifierType, ids []Identifier) int {\n\tc := 0\n\tfor _, id := range ids {\n\t\tif id.Type == typ {\n\t\t\tc++\n\t\t}\n\t}\n\treturn c\n}\n\n// canonicalize canonicalizes a CSR so that it can be compared against an Order\n// NOTE: this effectively changes the order of SANs in the CSR, which may be OK,\n// but may not be expected. It also adds a Subject Common Name to either the IP\n// addresses or DNS names slice, depending on whether it can be parsed as an IP\n// or not. This might result in an additional SAN in the final certificate.\nfunc canonicalize(csr *x509.CertificateRequest) (canonicalized *x509.CertificateRequest) {\n\t// for clarity only; we're operating on the same object by pointer\n\tcanonicalized = csr\n\n\t// RFC8555: The CSR MUST indicate the exact same set of requested\n\t// identifiers as the initial newOrder request. Identifiers of type \"dns\"\n\t// MUST appear either in the commonName portion of the requested subject\n\t// name or in an extensionRequest attribute [RFC2985] requesting a\n\t// subjectAltName extension, or both. Subject Common Names that can be\n\t// parsed as an IP are included as an IP address for the equality check.\n\t// If these were excluded, a certificate could contain an IP as the\n\t// common name without having been challenged.\n\tif csr.Subject.CommonName != \"\" {\n\t\tif ip := net.ParseIP(csr.Subject.CommonName); ip != nil {\n\t\t\tcanonicalized.IPAddresses = append(canonicalized.IPAddresses, ip)\n\t\t} else {\n\t\t\tcanonicalized.DNSNames = append(canonicalized.DNSNames, csr.Subject.CommonName)\n\t\t}\n\t}\n\n\tcanonicalized.DNSNames = uniqueSortedLowerNames(canonicalized.DNSNames)\n\tcanonicalized.IPAddresses = uniqueSortedIPs(canonicalized.IPAddresses)\n\n\treturn canonicalized\n}\n\n// ipsAreEqual compares IPs to be equal. Nil values (i.e. invalid IPs) are\n// not considered equal. IPv6 representations of IPv4 addresses are\n// considered equal to the IPv4 address in this implementation, which is\n// standard Go behavior. An example is \"::ffff:192.168.42.42\", which\n// is equal to \"192.168.42.42\". This is considered a known issue within\n// step and is tracked here too: https://github.com/golang/go/issues/37921.\nfunc ipsAreEqual(x, y net.IP) bool {\n\tif x == nil || y == nil {\n\t\treturn false\n\t}\n\treturn x.Equal(y)\n}\n\n// uniqueSortedLowerNames returns the set of all unique names in the input after all\n// of them are lowercased. The returned names will be in their lowercased form\n// and sorted alphabetically.\nfunc uniqueSortedLowerNames(names []string) (unique []string) {\n\tnameMap := make(map[string]int, len(names))\n\tfor _, name := range names {\n\t\tnameMap[strings.ToLower(name)] = 1\n\t}\n\tunique = make([]string, 0, len(nameMap))\n\tfor name := range nameMap {\n\t\tif name != \"\" {\n\t\t\tunique = append(unique, name)\n\t\t}\n\t}\n\tsort.Strings(unique)\n\treturn\n}\n\nfunc uniqueSortedURIStrings(uris []*url.URL) (unique []string) {\n\turiMap := make(map[string]struct{}, len(uris))\n\tfor _, name := range uris {\n\t\turiMap[name.String()] = struct{}{}\n\t}\n\tunique = make([]string, 0, len(uriMap))\n\tfor name := range uriMap {\n\t\tunique = append(unique, name)\n\t}\n\tsort.Strings(unique)\n\treturn\n}\n\n// uniqueSortedIPs returns the set of all unique net.IPs in the input. They\n// are sorted by their bytes (octet) representation.\nfunc uniqueSortedIPs(ips []net.IP) (unique []net.IP) {\n\ttype entry struct {\n\t\tip net.IP\n\t}\n\tipEntryMap := make(map[string]entry, len(ips))\n\tfor _, ip := range ips {\n\t\t// reparsing the IP results in the IP being represented using 16 bytes\n\t\t// for both IPv4 as well as IPv6, even when the ips slice contains IPs that\n\t\t// are represented by 4 bytes. This ensures a fair comparison and thus ordering.\n\t\tipEntryMap[ip.String()] = entry{ip: net.ParseIP(ip.String())}\n\t}\n\tunique = make([]net.IP, 0, len(ipEntryMap))\n\tfor _, entry := range ipEntryMap {\n\t\tunique = append(unique, entry.ip)\n\t}\n\tsort.Slice(unique, func(i, j int) bool {\n\t\treturn bytes.Compare(unique[i], unique[j]) < 0\n\t})\n\treturn\n}\n"
  },
  {
    "path": "acme/order_test.go",
    "content": "package acme\n\nimport (\n\t\"context\"\n\t\"crypto\"\n\t\"crypto/x509\"\n\t\"crypto/x509/pkix\"\n\t\"encoding/asn1\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/url\"\n\t\"reflect\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/pkg/errors\"\n\t\"github.com/smallstep/assert\"\n\t\"github.com/smallstep/certificates/authority\"\n\t\"github.com/smallstep/certificates/authority/provisioner\"\n\t\"github.com/smallstep/certificates/errs\"\n\t\"github.com/smallstep/certificates/webhook\"\n\t\"go.step.sm/crypto/keyutil\"\n\t\"go.step.sm/crypto/x509util\"\n)\n\nfunc TestOrder_UpdateStatus(t *testing.T) {\n\ttype test struct {\n\t\to   *Order\n\t\terr *Error\n\t\tdb  DB\n\t}\n\ttests := map[string]func(t *testing.T) test{\n\t\t\"ok/already-invalid\": func(t *testing.T) test {\n\t\t\to := &Order{\n\t\t\t\tStatus: StatusInvalid,\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\to: o,\n\t\t\t}\n\t\t},\n\t\t\"ok/already-valid\": func(t *testing.T) test {\n\t\t\to := &Order{\n\t\t\t\tStatus: StatusInvalid,\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\to: o,\n\t\t\t}\n\t\t},\n\t\t\"fail/error-unexpected-status\": func(t *testing.T) test {\n\t\t\to := &Order{\n\t\t\t\tStatus: \"foo\",\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\to:   o,\n\t\t\t\terr: NewErrorISE(\"unrecognized order status: %s\", o.Status),\n\t\t\t}\n\t\t},\n\t\t\"ok/ready-expired\": func(t *testing.T) test {\n\t\t\tnow := clock.Now()\n\t\t\to := &Order{\n\t\t\t\tID:        \"oID\",\n\t\t\t\tAccountID: \"accID\",\n\t\t\t\tStatus:    StatusReady,\n\t\t\t\tExpiresAt: now.Add(-5 * time.Minute),\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\to: o,\n\t\t\t\tdb: &MockDB{\n\t\t\t\t\tMockUpdateOrder: func(ctx context.Context, updo *Order) error {\n\t\t\t\t\t\tassert.Equals(t, updo.ID, o.ID)\n\t\t\t\t\t\tassert.Equals(t, updo.AccountID, o.AccountID)\n\t\t\t\t\t\tassert.Equals(t, updo.Status, StatusInvalid)\n\t\t\t\t\t\tassert.Equals(t, updo.ExpiresAt, o.ExpiresAt)\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"fail/ready-expired-db.UpdateOrder-error\": func(t *testing.T) test {\n\t\t\tnow := clock.Now()\n\t\t\to := &Order{\n\t\t\t\tID:        \"oID\",\n\t\t\t\tAccountID: \"accID\",\n\t\t\t\tStatus:    StatusReady,\n\t\t\t\tExpiresAt: now.Add(-5 * time.Minute),\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\to: o,\n\t\t\t\tdb: &MockDB{\n\t\t\t\t\tMockUpdateOrder: func(ctx context.Context, updo *Order) error {\n\t\t\t\t\t\tassert.Equals(t, updo.ID, o.ID)\n\t\t\t\t\t\tassert.Equals(t, updo.AccountID, o.AccountID)\n\t\t\t\t\t\tassert.Equals(t, updo.Status, StatusInvalid)\n\t\t\t\t\t\tassert.Equals(t, updo.ExpiresAt, o.ExpiresAt)\n\t\t\t\t\t\treturn errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: NewErrorISE(\"error updating order: force\"),\n\t\t\t}\n\t\t},\n\t\t\"ok/pending-expired\": func(t *testing.T) test {\n\t\t\tnow := clock.Now()\n\t\t\to := &Order{\n\t\t\t\tID:        \"oID\",\n\t\t\t\tAccountID: \"accID\",\n\t\t\t\tStatus:    StatusPending,\n\t\t\t\tExpiresAt: now.Add(-5 * time.Minute),\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\to: o,\n\t\t\t\tdb: &MockDB{\n\t\t\t\t\tMockUpdateOrder: func(ctx context.Context, updo *Order) error {\n\t\t\t\t\t\tassert.Equals(t, updo.ID, o.ID)\n\t\t\t\t\t\tassert.Equals(t, updo.AccountID, o.AccountID)\n\t\t\t\t\t\tassert.Equals(t, updo.Status, StatusInvalid)\n\t\t\t\t\t\tassert.Equals(t, updo.ExpiresAt, o.ExpiresAt)\n\n\t\t\t\t\t\terr := NewError(ErrorMalformedType, \"order has expired\")\n\t\t\t\t\t\tassert.HasPrefix(t, updo.Error.Err.Error(), err.Err.Error())\n\t\t\t\t\t\tassert.Equals(t, updo.Error.Type, err.Type)\n\t\t\t\t\t\tassert.Equals(t, updo.Error.Detail, err.Detail)\n\t\t\t\t\t\tassert.Equals(t, updo.Error.Status, err.Status)\n\t\t\t\t\t\tassert.Equals(t, updo.Error.Detail, err.Detail)\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"ok/invalid\": func(t *testing.T) test {\n\t\t\tnow := clock.Now()\n\t\t\to := &Order{\n\t\t\t\tID:               \"oID\",\n\t\t\t\tAccountID:        \"accID\",\n\t\t\t\tStatus:           StatusPending,\n\t\t\t\tExpiresAt:        now.Add(5 * time.Minute),\n\t\t\t\tAuthorizationIDs: []string{\"a\", \"b\"},\n\t\t\t}\n\t\t\taz1 := &Authorization{\n\t\t\t\tID:     \"a\",\n\t\t\t\tStatus: StatusValid,\n\t\t\t}\n\t\t\taz2 := &Authorization{\n\t\t\t\tID:     \"b\",\n\t\t\t\tStatus: StatusInvalid,\n\t\t\t}\n\n\t\t\treturn test{\n\t\t\t\to: o,\n\t\t\t\tdb: &MockDB{\n\t\t\t\t\tMockUpdateOrder: func(ctx context.Context, updo *Order) error {\n\t\t\t\t\t\tassert.Equals(t, updo.ID, o.ID)\n\t\t\t\t\t\tassert.Equals(t, updo.AccountID, o.AccountID)\n\t\t\t\t\t\tassert.Equals(t, updo.Status, StatusInvalid)\n\t\t\t\t\t\tassert.Equals(t, updo.ExpiresAt, o.ExpiresAt)\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t\tMockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {\n\t\t\t\t\t\tswitch id {\n\t\t\t\t\t\tcase az1.ID:\n\t\t\t\t\t\t\treturn az1, nil\n\t\t\t\t\t\tcase az2.ID:\n\t\t\t\t\t\t\treturn az2, nil\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\tassert.FatalError(t, errors.Errorf(\"unexpected authz key %s\", id))\n\t\t\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"ok/still-pending\": func(t *testing.T) test {\n\t\t\tnow := clock.Now()\n\t\t\to := &Order{\n\t\t\t\tID:               \"oID\",\n\t\t\t\tAccountID:        \"accID\",\n\t\t\t\tStatus:           StatusPending,\n\t\t\t\tExpiresAt:        now.Add(5 * time.Minute),\n\t\t\t\tAuthorizationIDs: []string{\"a\", \"b\"},\n\t\t\t}\n\t\t\taz1 := &Authorization{\n\t\t\t\tID:     \"a\",\n\t\t\t\tStatus: StatusValid,\n\t\t\t}\n\t\t\taz2 := &Authorization{\n\t\t\t\tID:     \"b\",\n\t\t\t\tStatus: StatusPending,\n\t\t\t}\n\n\t\t\treturn test{\n\t\t\t\to: o,\n\t\t\t\tdb: &MockDB{\n\t\t\t\t\tMockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {\n\t\t\t\t\t\tswitch id {\n\t\t\t\t\t\tcase az1.ID:\n\t\t\t\t\t\t\treturn az1, nil\n\t\t\t\t\t\tcase az2.ID:\n\t\t\t\t\t\t\treturn az2, nil\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\tassert.FatalError(t, errors.Errorf(\"unexpected authz key %s\", id))\n\t\t\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"ok/valid\": func(t *testing.T) test {\n\t\t\tnow := clock.Now()\n\t\t\to := &Order{\n\t\t\t\tID:               \"oID\",\n\t\t\t\tAccountID:        \"accID\",\n\t\t\t\tStatus:           StatusPending,\n\t\t\t\tExpiresAt:        now.Add(5 * time.Minute),\n\t\t\t\tAuthorizationIDs: []string{\"a\", \"b\"},\n\t\t\t}\n\t\t\taz1 := &Authorization{\n\t\t\t\tID:     \"a\",\n\t\t\t\tStatus: StatusValid,\n\t\t\t}\n\t\t\taz2 := &Authorization{\n\t\t\t\tID:     \"b\",\n\t\t\t\tStatus: StatusValid,\n\t\t\t}\n\n\t\t\treturn test{\n\t\t\t\to: o,\n\t\t\t\tdb: &MockDB{\n\t\t\t\t\tMockUpdateOrder: func(ctx context.Context, updo *Order) error {\n\t\t\t\t\t\tassert.Equals(t, updo.ID, o.ID)\n\t\t\t\t\t\tassert.Equals(t, updo.AccountID, o.AccountID)\n\t\t\t\t\t\tassert.Equals(t, updo.Status, StatusReady)\n\t\t\t\t\t\tassert.Equals(t, updo.ExpiresAt, o.ExpiresAt)\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t\tMockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {\n\t\t\t\t\t\tswitch id {\n\t\t\t\t\t\tcase az1.ID:\n\t\t\t\t\t\t\treturn az1, nil\n\t\t\t\t\t\tcase az2.ID:\n\t\t\t\t\t\t\treturn az2, nil\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\tassert.FatalError(t, errors.Errorf(\"unexpected authz key %s\", id))\n\t\t\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ttc := run(t)\n\t\t\tif err := tc.o.UpdateStatus(context.Background(), tc.db); err != nil {\n\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\tvar k *Error\n\t\t\t\t\tif errors.As(err, &k) {\n\t\t\t\t\t\tassert.Equals(t, k.Type, tc.err.Type)\n\t\t\t\t\t\tassert.Equals(t, k.Detail, tc.err.Detail)\n\t\t\t\t\t\tassert.Equals(t, k.Status, tc.err.Status)\n\t\t\t\t\t\tassert.Equals(t, k.Err.Error(), tc.err.Err.Error())\n\t\t\t\t\t\tassert.Equals(t, k.Detail, tc.err.Detail)\n\t\t\t\t\t} else {\n\t\t\t\t\t\tassert.FatalError(t, errors.New(\"unexpected error type\"))\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tassert.Nil(t, tc.err)\n\t\t\t}\n\t\t})\n\n\t}\n}\n\ntype mockSignAuth struct {\n\tsignWithContext       func(ctx context.Context, csr *x509.CertificateRequest, signOpts provisioner.SignOptions, extraOpts ...provisioner.SignOption) ([]*x509.Certificate, error)\n\tareSANsAllowed        func(ctx context.Context, sans []string) error\n\tloadProvisionerByName func(string) (provisioner.Interface, error)\n\tret1, ret2            interface{}\n\terr                   error\n}\n\nfunc (m *mockSignAuth) SignWithContext(ctx context.Context, csr *x509.CertificateRequest, signOpts provisioner.SignOptions, extraOpts ...provisioner.SignOption) ([]*x509.Certificate, error) {\n\tif m.signWithContext != nil {\n\t\treturn m.signWithContext(ctx, csr, signOpts, extraOpts...)\n\t} else if m.err != nil {\n\t\treturn nil, m.err\n\t}\n\treturn []*x509.Certificate{m.ret1.(*x509.Certificate), m.ret2.(*x509.Certificate)}, m.err\n}\n\nfunc (m *mockSignAuth) AreSANsAllowed(ctx context.Context, sans []string) error {\n\tif m.areSANsAllowed != nil {\n\t\treturn m.areSANsAllowed(ctx, sans)\n\t}\n\treturn m.err\n}\n\nfunc (m *mockSignAuth) LoadProvisionerByName(name string) (provisioner.Interface, error) {\n\tif m.loadProvisionerByName != nil {\n\t\treturn m.loadProvisionerByName(name)\n\t}\n\treturn m.ret1.(provisioner.Interface), m.err\n}\n\nfunc (m *mockSignAuth) IsRevoked(string) (bool, error) {\n\treturn false, nil\n}\n\nfunc (m *mockSignAuth) Revoke(context.Context, *authority.RevokeOptions) error {\n\treturn nil\n}\n\nfunc (m *mockSignAuth) GetBackdate() *time.Duration {\n\treturn nil\n}\n\nfunc TestOrder_Finalize(t *testing.T) {\n\tmustSigner := func(kty, crv string, size int) crypto.Signer {\n\t\ts, err := keyutil.GenerateSigner(kty, crv, size)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\treturn s\n\t}\n\n\ttype test struct {\n\t\to    *Order\n\t\terr  *Error\n\t\tdb   DB\n\t\tca   CertificateAuthority\n\t\tcsr  *x509.CertificateRequest\n\t\tprov Provisioner\n\t}\n\ttests := map[string]func(t *testing.T) test{\n\t\t\"fail/invalid\": func(t *testing.T) test {\n\t\t\to := &Order{\n\t\t\t\tID:     \"oid\",\n\t\t\t\tStatus: StatusInvalid,\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\to:   o,\n\t\t\t\terr: NewError(ErrorOrderNotReadyType, \"order %s has been abandoned\", o.ID),\n\t\t\t}\n\t\t},\n\t\t\"fail/pending\": func(t *testing.T) test {\n\t\t\tnow := clock.Now()\n\t\t\to := &Order{\n\t\t\t\tID:               \"oID\",\n\t\t\t\tAccountID:        \"accID\",\n\t\t\t\tStatus:           StatusPending,\n\t\t\t\tExpiresAt:        now.Add(5 * time.Minute),\n\t\t\t\tAuthorizationIDs: []string{\"a\", \"b\"},\n\t\t\t}\n\t\t\taz1 := &Authorization{\n\t\t\t\tID:     \"a\",\n\t\t\t\tStatus: StatusValid,\n\t\t\t}\n\t\t\taz2 := &Authorization{\n\t\t\t\tID:        \"b\",\n\t\t\t\tStatus:    StatusPending,\n\t\t\t\tExpiresAt: now.Add(5 * time.Minute),\n\t\t\t}\n\n\t\t\treturn test{\n\t\t\t\to: o,\n\t\t\t\tdb: &MockDB{\n\t\t\t\t\tMockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {\n\t\t\t\t\t\tswitch id {\n\t\t\t\t\t\tcase az1.ID:\n\t\t\t\t\t\t\treturn az1, nil\n\t\t\t\t\t\tcase az2.ID:\n\t\t\t\t\t\t\treturn az2, nil\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\tassert.FatalError(t, errors.Errorf(\"unexpected authz key %s\", id))\n\t\t\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: NewError(ErrorOrderNotReadyType, \"order %s is not ready\", o.ID),\n\t\t\t}\n\t\t},\n\t\t\"ok/already-valid\": func(t *testing.T) test {\n\t\t\to := &Order{\n\t\t\t\tID:     \"oid\",\n\t\t\t\tStatus: StatusValid,\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\to: o,\n\t\t\t}\n\t\t},\n\t\t\"fail/error-unexpected-status\": func(t *testing.T) test {\n\t\t\tnow := clock.Now()\n\t\t\to := &Order{\n\t\t\t\tID:               \"oID\",\n\t\t\t\tAccountID:        \"accID\",\n\t\t\t\tStatus:           \"foo\",\n\t\t\t\tExpiresAt:        now.Add(5 * time.Minute),\n\t\t\t\tAuthorizationIDs: []string{\"a\", \"b\"},\n\t\t\t}\n\n\t\t\treturn test{\n\t\t\t\to:   o,\n\t\t\t\terr: NewErrorISE(\"unrecognized order status: %s\", o.Status),\n\t\t\t}\n\t\t},\n\t\t\"fail/non-matching-permanent-identifier-common-name\": func(t *testing.T) test {\n\t\t\tnow := clock.Now()\n\t\t\to := &Order{\n\t\t\t\tID:               \"oID\",\n\t\t\t\tAccountID:        \"accID\",\n\t\t\t\tStatus:           StatusReady,\n\t\t\t\tExpiresAt:        now.Add(5 * time.Minute),\n\t\t\t\tAuthorizationIDs: []string{\"a\", \"b\"},\n\t\t\t\tIdentifiers: []Identifier{\n\t\t\t\t\t{Type: \"permanent-identifier\", Value: \"a-permanent-identifier\"},\n\t\t\t\t},\n\t\t\t}\n\n\t\t\tsigner := mustSigner(\"EC\", \"P-256\", 0)\n\t\t\tfingerprint, err := keyutil.Fingerprint(signer.Public())\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\tcsr := &x509.CertificateRequest{\n\t\t\t\tSubject: pkix.Name{\n\t\t\t\t\tCommonName: \"a-different-identifier\",\n\t\t\t\t},\n\t\t\t\tPublicKey: signer.Public(),\n\t\t\t\tExtraExtensions: []pkix.Extension{\n\t\t\t\t\t{\n\t\t\t\t\t\tId:    asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 8, 3},\n\t\t\t\t\t\tValue: []byte(\"a-permanent-identifier\"),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\to:   o,\n\t\t\t\tcsr: csr,\n\t\t\t\tdb: &MockDB{\n\t\t\t\t\tMockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {\n\t\t\t\t\t\tswitch id {\n\t\t\t\t\t\tcase \"a\":\n\t\t\t\t\t\t\treturn &Authorization{\n\t\t\t\t\t\t\t\tID:     id,\n\t\t\t\t\t\t\t\tStatus: StatusValid,\n\t\t\t\t\t\t\t}, nil\n\t\t\t\t\t\tcase \"b\":\n\t\t\t\t\t\t\treturn &Authorization{\n\t\t\t\t\t\t\t\tID:          id,\n\t\t\t\t\t\t\t\tFingerprint: fingerprint,\n\t\t\t\t\t\t\t\tStatus:      StatusValid,\n\t\t\t\t\t\t\t}, nil\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\tassert.FatalError(t, errors.Errorf(\"unexpected authorization %s\", id))\n\t\t\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\tMockUpdateOrder: func(ctx context.Context, o *Order) error {\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: &Error{\n\t\t\t\t\tType:   \"urn:ietf:params:acme:error:badCSR\",\n\t\t\t\t\tDetail: \"The CSR is unacceptable\",\n\t\t\t\t\tStatus: 400,\n\t\t\t\t\tErr: fmt.Errorf(\"CSR Subject Common Name does not match identifiers exactly: \"+\n\t\t\t\t\t\t\"CSR Subject Common Name = %s, Order Permanent Identifier = %s\", csr.Subject.CommonName, \"a-permanent-identifier\"),\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"fail/error-provisioner-auth\": func(t *testing.T) test {\n\t\t\tnow := clock.Now()\n\t\t\to := &Order{\n\t\t\t\tID:               \"oID\",\n\t\t\t\tAccountID:        \"accID\",\n\t\t\t\tStatus:           StatusReady,\n\t\t\t\tExpiresAt:        now.Add(5 * time.Minute),\n\t\t\t\tAuthorizationIDs: []string{\"a\", \"b\"},\n\t\t\t\tIdentifiers: []Identifier{\n\t\t\t\t\t{Type: \"dns\", Value: \"foo.internal\"},\n\t\t\t\t\t{Type: \"dns\", Value: \"bar.internal\"},\n\t\t\t\t},\n\t\t\t}\n\t\t\tcsr := &x509.CertificateRequest{\n\t\t\t\tSubject: pkix.Name{\n\t\t\t\t\tCommonName: \"foo.internal\",\n\t\t\t\t},\n\t\t\t\tDNSNames: []string{\"bar.internal\"},\n\t\t\t}\n\n\t\t\treturn test{\n\t\t\t\to:   o,\n\t\t\t\tcsr: csr,\n\t\t\t\tprov: &MockProvisioner{\n\t\t\t\t\tMauthorizeSign: func(ctx context.Context, token string) ([]provisioner.SignOption, error) {\n\t\t\t\t\t\tassert.Equals(t, token, \"\")\n\t\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tdb: &MockDB{\n\t\t\t\t\tMockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {\n\t\t\t\t\t\treturn &Authorization{ID: id, Status: StatusValid}, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: NewErrorISE(\"error retrieving authorization options from ACME provisioner: force\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/error-template-options\": func(t *testing.T) test {\n\t\t\tnow := clock.Now()\n\t\t\to := &Order{\n\t\t\t\tID:               \"oID\",\n\t\t\t\tAccountID:        \"accID\",\n\t\t\t\tStatus:           StatusReady,\n\t\t\t\tExpiresAt:        now.Add(5 * time.Minute),\n\t\t\t\tAuthorizationIDs: []string{\"a\", \"b\"},\n\t\t\t\tIdentifiers: []Identifier{\n\t\t\t\t\t{Type: \"dns\", Value: \"foo.internal\"},\n\t\t\t\t\t{Type: \"dns\", Value: \"bar.internal\"},\n\t\t\t\t},\n\t\t\t}\n\t\t\tcsr := &x509.CertificateRequest{\n\t\t\t\tSubject: pkix.Name{\n\t\t\t\t\tCommonName: \"foo.internal\",\n\t\t\t\t},\n\t\t\t\tDNSNames: []string{\"bar.internal\"},\n\t\t\t}\n\n\t\t\treturn test{\n\t\t\t\to:   o,\n\t\t\t\tcsr: csr,\n\t\t\t\tprov: &MockProvisioner{\n\t\t\t\t\tMauthorizeSign: func(ctx context.Context, token string) ([]provisioner.SignOption, error) {\n\t\t\t\t\t\tassert.Equals(t, token, \"\")\n\t\t\t\t\t\treturn nil, nil\n\t\t\t\t\t},\n\t\t\t\t\tMgetOptions: func() *provisioner.Options {\n\t\t\t\t\t\treturn &provisioner.Options{\n\t\t\t\t\t\t\tX509: &provisioner.X509Options{\n\t\t\t\t\t\t\t\tTemplateData: json.RawMessage([]byte(\"fo{o\")),\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tdb: &MockDB{\n\t\t\t\t\tMockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {\n\t\t\t\t\t\treturn &Authorization{ID: id, Status: StatusValid}, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: NewErrorISE(\"error creating template options from ACME provisioner: error unmarshaling template data: invalid character 'o' in literal false (expecting 'a')\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/error-ca-sign\": func(t *testing.T) test {\n\t\t\tnow := clock.Now()\n\t\t\to := &Order{\n\t\t\t\tID:               \"oID\",\n\t\t\t\tAccountID:        \"accID\",\n\t\t\t\tStatus:           StatusReady,\n\t\t\t\tExpiresAt:        now.Add(5 * time.Minute),\n\t\t\t\tAuthorizationIDs: []string{\"a\", \"b\"},\n\t\t\t\tIdentifiers: []Identifier{\n\t\t\t\t\t{Type: \"dns\", Value: \"foo.internal\"},\n\t\t\t\t\t{Type: \"dns\", Value: \"bar.internal\"},\n\t\t\t\t},\n\t\t\t}\n\t\t\tcsr := &x509.CertificateRequest{\n\t\t\t\tSubject: pkix.Name{\n\t\t\t\t\tCommonName: \"foo.internal\",\n\t\t\t\t},\n\t\t\t\tDNSNames: []string{\"bar.internal\"},\n\t\t\t}\n\n\t\t\treturn test{\n\t\t\t\to:   o,\n\t\t\t\tcsr: csr,\n\t\t\t\tprov: &MockProvisioner{\n\t\t\t\t\tMauthorizeSign: func(ctx context.Context, token string) ([]provisioner.SignOption, error) {\n\t\t\t\t\t\tassert.Equals(t, token, \"\")\n\t\t\t\t\t\treturn nil, nil\n\t\t\t\t\t},\n\t\t\t\t\tMgetOptions: func() *provisioner.Options {\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tca: &mockSignAuth{\n\t\t\t\t\tsignWithContext: func(_ context.Context, _csr *x509.CertificateRequest, signOpts provisioner.SignOptions, extraOpts ...provisioner.SignOption) ([]*x509.Certificate, error) {\n\t\t\t\t\t\tassert.Equals(t, _csr, csr)\n\t\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tdb: &MockDB{\n\t\t\t\t\tMockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {\n\t\t\t\t\t\treturn &Authorization{ID: id, Status: StatusValid}, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: NewErrorISE(\"error signing certificate for order oID: force\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/webhook-error\": func(t *testing.T) test {\n\t\t\tnow := clock.Now()\n\t\t\to := &Order{\n\t\t\t\tID:               \"oID\",\n\t\t\t\tAccountID:        \"accID\",\n\t\t\t\tStatus:           StatusReady,\n\t\t\t\tExpiresAt:        now.Add(5 * time.Minute),\n\t\t\t\tAuthorizationIDs: []string{\"a\", \"b\"},\n\t\t\t\tIdentifiers: []Identifier{\n\t\t\t\t\t{Type: \"dns\", Value: \"foo.internal\"},\n\t\t\t\t\t{Type: \"dns\", Value: \"bar.internal\"},\n\t\t\t\t},\n\t\t\t}\n\t\t\tcsr := &x509.CertificateRequest{\n\t\t\t\tSubject: pkix.Name{\n\t\t\t\t\tCommonName: \"foo.internal\",\n\t\t\t\t},\n\t\t\t\tDNSNames: []string{\"bar.internal\"},\n\t\t\t}\n\n\t\t\treturn test{\n\t\t\t\to:   o,\n\t\t\t\tcsr: csr,\n\t\t\t\tprov: &MockProvisioner{\n\t\t\t\t\tMauthorizeSign: func(ctx context.Context, token string) ([]provisioner.SignOption, error) {\n\t\t\t\t\t\tassert.Equals(t, token, \"\")\n\t\t\t\t\t\treturn nil, nil\n\t\t\t\t\t},\n\t\t\t\t\tMgetOptions: func() *provisioner.Options {\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tca: &mockSignAuth{\n\t\t\t\t\tsignWithContext: func(_ context.Context, _csr *x509.CertificateRequest, signOpts provisioner.SignOptions, extraOpts ...provisioner.SignOption) ([]*x509.Certificate, error) {\n\t\t\t\t\t\tassert.Equals(t, _csr, csr)\n\t\t\t\t\t\treturn nil, errs.ForbiddenErr(&webhook.Error{Code: \"theCode\", Message: \"The message\"}, \"forbidden error\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tdb: &MockDB{\n\t\t\t\t\tMockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {\n\t\t\t\t\t\treturn &Authorization{ID: id, Status: StatusValid}, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: NewDetailedError(ErrorUnauthorizedType, \"The message (theCode)\").AddSubproblems(Subproblem{\n\t\t\t\t\tType:   \"urn:smallstep:acme:error:theCode\",\n\t\t\t\t\tDetail: \"The message\",\n\t\t\t\t}),\n\t\t\t}\n\t\t},\n\t\t\"fail/error-db.CreateCertificate\": func(t *testing.T) test {\n\t\t\tnow := clock.Now()\n\t\t\to := &Order{\n\t\t\t\tID:               \"oID\",\n\t\t\t\tAccountID:        \"accID\",\n\t\t\t\tStatus:           StatusReady,\n\t\t\t\tExpiresAt:        now.Add(5 * time.Minute),\n\t\t\t\tAuthorizationIDs: []string{\"a\", \"b\"},\n\t\t\t\tIdentifiers: []Identifier{\n\t\t\t\t\t{Type: \"dns\", Value: \"foo.internal\"},\n\t\t\t\t\t{Type: \"dns\", Value: \"bar.internal\"},\n\t\t\t\t},\n\t\t\t}\n\t\t\tcsr := &x509.CertificateRequest{\n\t\t\t\tSubject: pkix.Name{\n\t\t\t\t\tCommonName: \"foo.internal\",\n\t\t\t\t},\n\t\t\t\tDNSNames: []string{\"bar.internal\"},\n\t\t\t}\n\n\t\t\tfoo := &x509.Certificate{Subject: pkix.Name{CommonName: \"foo\"}}\n\t\t\tbar := &x509.Certificate{Subject: pkix.Name{CommonName: \"bar\"}}\n\t\t\tbaz := &x509.Certificate{Subject: pkix.Name{CommonName: \"baz\"}}\n\n\t\t\treturn test{\n\t\t\t\to:   o,\n\t\t\t\tcsr: csr,\n\t\t\t\tprov: &MockProvisioner{\n\t\t\t\t\tMauthorizeSign: func(ctx context.Context, token string) ([]provisioner.SignOption, error) {\n\t\t\t\t\t\tassert.Equals(t, token, \"\")\n\t\t\t\t\t\treturn nil, nil\n\t\t\t\t\t},\n\t\t\t\t\tMgetOptions: func() *provisioner.Options {\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tca: &mockSignAuth{\n\t\t\t\t\tsignWithContext: func(_ context.Context, _csr *x509.CertificateRequest, signOpts provisioner.SignOptions, extraOpts ...provisioner.SignOption) ([]*x509.Certificate, error) {\n\t\t\t\t\t\tassert.Equals(t, _csr, csr)\n\t\t\t\t\t\treturn []*x509.Certificate{foo, bar, baz}, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tdb: &MockDB{\n\t\t\t\t\tMockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {\n\t\t\t\t\t\treturn &Authorization{ID: id, Status: StatusValid}, nil\n\t\t\t\t\t},\n\t\t\t\t\tMockCreateCertificate: func(ctx context.Context, cert *Certificate) error {\n\t\t\t\t\t\tassert.Equals(t, cert.AccountID, o.AccountID)\n\t\t\t\t\t\tassert.Equals(t, cert.OrderID, o.ID)\n\t\t\t\t\t\tassert.Equals(t, cert.Leaf, foo)\n\t\t\t\t\t\tassert.Equals(t, cert.Intermediates, []*x509.Certificate{bar, baz})\n\t\t\t\t\t\treturn errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: NewErrorISE(\"error creating certificate for order oID: force\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/error-db.UpdateOrder\": func(t *testing.T) test {\n\t\t\tnow := clock.Now()\n\t\t\to := &Order{\n\t\t\t\tID:               \"oID\",\n\t\t\t\tAccountID:        \"accID\",\n\t\t\t\tStatus:           StatusReady,\n\t\t\t\tExpiresAt:        now.Add(5 * time.Minute),\n\t\t\t\tAuthorizationIDs: []string{\"a\", \"b\"},\n\t\t\t\tIdentifiers: []Identifier{\n\t\t\t\t\t{Type: \"dns\", Value: \"foo.internal\"},\n\t\t\t\t\t{Type: \"dns\", Value: \"bar.internal\"},\n\t\t\t\t},\n\t\t\t}\n\t\t\tcsr := &x509.CertificateRequest{\n\t\t\t\tSubject: pkix.Name{\n\t\t\t\t\tCommonName: \"foo.internal\",\n\t\t\t\t},\n\t\t\t\tDNSNames: []string{\"bar.internal\"},\n\t\t\t}\n\n\t\t\tfoo := &x509.Certificate{Subject: pkix.Name{CommonName: \"foo\"}}\n\t\t\tbar := &x509.Certificate{Subject: pkix.Name{CommonName: \"bar\"}}\n\t\t\tbaz := &x509.Certificate{Subject: pkix.Name{CommonName: \"baz\"}}\n\n\t\t\treturn test{\n\t\t\t\to:   o,\n\t\t\t\tcsr: csr,\n\t\t\t\tprov: &MockProvisioner{\n\t\t\t\t\tMauthorizeSign: func(ctx context.Context, token string) ([]provisioner.SignOption, error) {\n\t\t\t\t\t\tassert.Equals(t, token, \"\")\n\t\t\t\t\t\treturn nil, nil\n\t\t\t\t\t},\n\t\t\t\t\tMgetOptions: func() *provisioner.Options {\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tca: &mockSignAuth{\n\t\t\t\t\tsignWithContext: func(_ context.Context, _csr *x509.CertificateRequest, signOpts provisioner.SignOptions, extraOpts ...provisioner.SignOption) ([]*x509.Certificate, error) {\n\t\t\t\t\t\tassert.Equals(t, _csr, csr)\n\t\t\t\t\t\treturn []*x509.Certificate{foo, bar, baz}, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tdb: &MockDB{\n\t\t\t\t\tMockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {\n\t\t\t\t\t\treturn &Authorization{ID: id, Status: StatusValid}, nil\n\t\t\t\t\t},\n\t\t\t\t\tMockCreateCertificate: func(ctx context.Context, cert *Certificate) error {\n\t\t\t\t\t\tcert.ID = \"certID\"\n\t\t\t\t\t\tassert.Equals(t, cert.AccountID, o.AccountID)\n\t\t\t\t\t\tassert.Equals(t, cert.OrderID, o.ID)\n\t\t\t\t\t\tassert.Equals(t, cert.Leaf, foo)\n\t\t\t\t\t\tassert.Equals(t, cert.Intermediates, []*x509.Certificate{bar, baz})\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t\tMockUpdateOrder: func(ctx context.Context, updo *Order) error {\n\t\t\t\t\t\tassert.Equals(t, updo.CertificateID, \"certID\")\n\t\t\t\t\t\tassert.Equals(t, updo.Status, StatusValid)\n\t\t\t\t\t\tassert.Equals(t, updo.ID, o.ID)\n\t\t\t\t\t\tassert.Equals(t, updo.AccountID, o.AccountID)\n\t\t\t\t\t\tassert.Equals(t, updo.ExpiresAt, o.ExpiresAt)\n\t\t\t\t\t\tassert.Equals(t, updo.AuthorizationIDs, o.AuthorizationIDs)\n\t\t\t\t\t\tassert.Equals(t, updo.Identifiers, o.Identifiers)\n\t\t\t\t\t\treturn errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: NewErrorISE(\"error updating order oID: force\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/csr-fingerprint\": func(t *testing.T) test {\n\t\t\tnow := clock.Now()\n\t\t\to := &Order{\n\t\t\t\tID:               \"oID\",\n\t\t\t\tAccountID:        \"accID\",\n\t\t\t\tStatus:           StatusReady,\n\t\t\t\tExpiresAt:        now.Add(5 * time.Minute),\n\t\t\t\tAuthorizationIDs: []string{\"a\", \"b\"},\n\t\t\t\tIdentifiers: []Identifier{\n\t\t\t\t\t{Type: \"permanent-identifier\", Value: \"a-permanent-identifier\"},\n\t\t\t\t},\n\t\t\t}\n\n\t\t\tsigner := mustSigner(\"EC\", \"P-256\", 0)\n\n\t\t\tcsr := &x509.CertificateRequest{\n\t\t\t\tSubject: pkix.Name{\n\t\t\t\t\tCommonName: \"a-permanent-identifier\",\n\t\t\t\t},\n\t\t\t\tPublicKey: signer.Public(),\n\t\t\t\tExtraExtensions: []pkix.Extension{\n\t\t\t\t\t{\n\t\t\t\t\t\tId:    asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 8, 3},\n\t\t\t\t\t\tValue: []byte(\"a-permanent-identifier\"),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\n\t\t\tleaf := &x509.Certificate{\n\t\t\t\tSubject:   pkix.Name{CommonName: \"a-permanent-identifier\"},\n\t\t\t\tPublicKey: signer.Public(),\n\t\t\t\tExtraExtensions: []pkix.Extension{\n\t\t\t\t\t{\n\t\t\t\t\t\tId:    asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 8, 3},\n\t\t\t\t\t\tValue: []byte(\"a-permanent-identifier\"),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t\tinter := &x509.Certificate{Subject: pkix.Name{CommonName: \"inter\"}}\n\t\t\troot := &x509.Certificate{Subject: pkix.Name{CommonName: \"root\"}}\n\n\t\t\treturn test{\n\t\t\t\to:   o,\n\t\t\t\tcsr: csr,\n\t\t\t\tprov: &MockProvisioner{\n\t\t\t\t\tMauthorizeSign: func(ctx context.Context, token string) ([]provisioner.SignOption, error) {\n\t\t\t\t\t\tassert.Equals(t, token, \"\")\n\t\t\t\t\t\treturn nil, nil\n\t\t\t\t\t},\n\t\t\t\t\tMgetOptions: func() *provisioner.Options {\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tca: &mockSignAuth{\n\t\t\t\t\tsignWithContext: func(_ context.Context, _csr *x509.CertificateRequest, signOpts provisioner.SignOptions, extraOpts ...provisioner.SignOption) ([]*x509.Certificate, error) {\n\t\t\t\t\t\tassert.Equals(t, _csr, csr)\n\t\t\t\t\t\treturn []*x509.Certificate{leaf, inter, root}, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tdb: &MockDB{\n\t\t\t\t\tMockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {\n\t\t\t\t\t\treturn &Authorization{\n\t\t\t\t\t\t\tID:          id,\n\t\t\t\t\t\t\tFingerprint: \"other-fingerprint\",\n\t\t\t\t\t\t\tStatus:      StatusValid,\n\t\t\t\t\t\t}, nil\n\t\t\t\t\t},\n\t\t\t\t\tMockCreateCertificate: func(ctx context.Context, cert *Certificate) error {\n\t\t\t\t\t\tcert.ID = \"certID\"\n\t\t\t\t\t\tassert.Equals(t, cert.AccountID, o.AccountID)\n\t\t\t\t\t\tassert.Equals(t, cert.OrderID, o.ID)\n\t\t\t\t\t\tassert.Equals(t, cert.Leaf, leaf)\n\t\t\t\t\t\tassert.Equals(t, cert.Intermediates, []*x509.Certificate{inter, root})\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t\tMockUpdateOrder: func(ctx context.Context, updo *Order) error {\n\t\t\t\t\t\tassert.Equals(t, updo.CertificateID, \"certID\")\n\t\t\t\t\t\tassert.Equals(t, updo.Status, StatusValid)\n\t\t\t\t\t\tassert.Equals(t, updo.ID, o.ID)\n\t\t\t\t\t\tassert.Equals(t, updo.AccountID, o.AccountID)\n\t\t\t\t\t\tassert.Equals(t, updo.ExpiresAt, o.ExpiresAt)\n\t\t\t\t\t\tassert.Equals(t, updo.AuthorizationIDs, o.AuthorizationIDs)\n\t\t\t\t\t\tassert.Equals(t, updo.Identifiers, o.Identifiers)\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: NewError(ErrorUnauthorizedType, \"order oID csr does not match the attested key\"),\n\t\t\t}\n\t\t},\n\t\t\"ok/permanent-identifier\": func(t *testing.T) test {\n\t\t\tnow := clock.Now()\n\t\t\to := &Order{\n\t\t\t\tID:               \"oID\",\n\t\t\t\tAccountID:        \"accID\",\n\t\t\t\tStatus:           StatusReady,\n\t\t\t\tExpiresAt:        now.Add(5 * time.Minute),\n\t\t\t\tAuthorizationIDs: []string{\"a\", \"b\"},\n\t\t\t\tIdentifiers: []Identifier{\n\t\t\t\t\t{Type: \"permanent-identifier\", Value: \"a-permanent-identifier\"},\n\t\t\t\t},\n\t\t\t}\n\n\t\t\tsigner := mustSigner(\"EC\", \"P-256\", 0)\n\t\t\tfingerprint, err := keyutil.Fingerprint(signer.Public())\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\tcsr := &x509.CertificateRequest{\n\t\t\t\tSubject: pkix.Name{\n\t\t\t\t\tCommonName: \"a-permanent-identifier\",\n\t\t\t\t},\n\t\t\t\tPublicKey: signer.Public(),\n\t\t\t\tExtraExtensions: []pkix.Extension{\n\t\t\t\t\t{\n\t\t\t\t\t\tId:    asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 8, 3},\n\t\t\t\t\t\tValue: []byte(\"a-permanent-identifier\"),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\n\t\t\tleaf := &x509.Certificate{\n\t\t\t\tSubject:   pkix.Name{CommonName: \"a-permanent-identifier\"},\n\t\t\t\tPublicKey: signer.Public(),\n\t\t\t\tExtraExtensions: []pkix.Extension{\n\t\t\t\t\t{\n\t\t\t\t\t\tId:    asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 8, 3},\n\t\t\t\t\t\tValue: []byte(\"a-permanent-identifier\"),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t\tinter := &x509.Certificate{Subject: pkix.Name{CommonName: \"inter\"}}\n\t\t\troot := &x509.Certificate{Subject: pkix.Name{CommonName: \"root\"}}\n\n\t\t\treturn test{\n\t\t\t\to:   o,\n\t\t\t\tcsr: csr,\n\t\t\t\tprov: &MockProvisioner{\n\t\t\t\t\tMauthorizeSign: func(ctx context.Context, token string) ([]provisioner.SignOption, error) {\n\t\t\t\t\t\tassert.Equals(t, token, \"\")\n\t\t\t\t\t\treturn nil, nil\n\t\t\t\t\t},\n\t\t\t\t\tMgetOptions: func() *provisioner.Options {\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tca: &mockSignAuth{\n\t\t\t\t\tsignWithContext: func(_ context.Context, _csr *x509.CertificateRequest, signOpts provisioner.SignOptions, extraOpts ...provisioner.SignOption) ([]*x509.Certificate, error) {\n\t\t\t\t\t\tassert.Equals(t, _csr, csr)\n\t\t\t\t\t\treturn []*x509.Certificate{leaf, inter, root}, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tdb: &MockDB{\n\t\t\t\t\tMockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {\n\t\t\t\t\t\tswitch id {\n\t\t\t\t\t\tcase \"a\":\n\t\t\t\t\t\t\treturn &Authorization{\n\t\t\t\t\t\t\t\tID:     id,\n\t\t\t\t\t\t\t\tStatus: StatusValid,\n\t\t\t\t\t\t\t}, nil\n\t\t\t\t\t\tcase \"b\":\n\t\t\t\t\t\t\treturn &Authorization{\n\t\t\t\t\t\t\t\tID:          id,\n\t\t\t\t\t\t\t\tFingerprint: fingerprint,\n\t\t\t\t\t\t\t\tStatus:      StatusValid,\n\t\t\t\t\t\t\t}, nil\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\tassert.FatalError(t, errors.Errorf(\"unexpected authorization %s\", id))\n\t\t\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\tMockCreateCertificate: func(ctx context.Context, cert *Certificate) error {\n\t\t\t\t\t\tcert.ID = \"certID\"\n\t\t\t\t\t\tassert.Equals(t, cert.AccountID, o.AccountID)\n\t\t\t\t\t\tassert.Equals(t, cert.OrderID, o.ID)\n\t\t\t\t\t\tassert.Equals(t, cert.Leaf, leaf)\n\t\t\t\t\t\tassert.Equals(t, cert.Intermediates, []*x509.Certificate{inter, root})\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t\tMockUpdateOrder: func(ctx context.Context, updo *Order) error {\n\t\t\t\t\t\tassert.Equals(t, updo.CertificateID, \"certID\")\n\t\t\t\t\t\tassert.Equals(t, updo.Status, StatusValid)\n\t\t\t\t\t\tassert.Equals(t, updo.ID, o.ID)\n\t\t\t\t\t\tassert.Equals(t, updo.AccountID, o.AccountID)\n\t\t\t\t\t\tassert.Equals(t, updo.ExpiresAt, o.ExpiresAt)\n\t\t\t\t\t\tassert.Equals(t, updo.AuthorizationIDs, o.AuthorizationIDs)\n\t\t\t\t\t\tassert.Equals(t, updo.Identifiers, o.Identifiers)\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"ok/permanent-identifier-only\": func(t *testing.T) test {\n\t\t\tnow := clock.Now()\n\t\t\to := &Order{\n\t\t\t\tID:               \"oID\",\n\t\t\t\tAccountID:        \"accID\",\n\t\t\t\tStatus:           StatusReady,\n\t\t\t\tExpiresAt:        now.Add(5 * time.Minute),\n\t\t\t\tAuthorizationIDs: []string{\"a\", \"b\"},\n\t\t\t\tIdentifiers: []Identifier{\n\t\t\t\t\t{Type: \"dns\", Value: \"foo.internal\"},\n\t\t\t\t\t{Type: \"permanent-identifier\", Value: \"a-permanent-identifier\"},\n\t\t\t\t},\n\t\t\t}\n\n\t\t\tsigner := mustSigner(\"EC\", \"P-256\", 0)\n\t\t\tfingerprint, err := keyutil.Fingerprint(signer.Public())\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\tcsr := &x509.CertificateRequest{\n\t\t\t\tSubject: pkix.Name{\n\t\t\t\t\tCommonName: \"a-permanent-identifier\",\n\t\t\t\t},\n\t\t\t\tDNSNames:  []string{\"foo.internal\"},\n\t\t\t\tPublicKey: signer.Public(),\n\t\t\t\tExtraExtensions: []pkix.Extension{\n\t\t\t\t\t{\n\t\t\t\t\t\tId:    asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 8, 3},\n\t\t\t\t\t\tValue: []byte(\"a-permanent-identifier\"),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\n\t\t\tleaf := &x509.Certificate{\n\t\t\t\tSubject:   pkix.Name{CommonName: \"a-permanent-identifier\"},\n\t\t\t\tPublicKey: signer.Public(),\n\t\t\t\tExtraExtensions: []pkix.Extension{\n\t\t\t\t\t{\n\t\t\t\t\t\tId:    asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 8, 3},\n\t\t\t\t\t\tValue: []byte(\"a-permanent-identifier\"),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t\tinter := &x509.Certificate{Subject: pkix.Name{CommonName: \"inter\"}}\n\t\t\troot := &x509.Certificate{Subject: pkix.Name{CommonName: \"root\"}}\n\n\t\t\treturn test{\n\t\t\t\to:   o,\n\t\t\t\tcsr: csr,\n\t\t\t\tprov: &MockProvisioner{\n\t\t\t\t\tMauthorizeSign: func(ctx context.Context, token string) ([]provisioner.SignOption, error) {\n\t\t\t\t\t\tassert.Equals(t, token, \"\")\n\t\t\t\t\t\treturn nil, nil\n\t\t\t\t\t},\n\t\t\t\t\tMgetOptions: func() *provisioner.Options {\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t// TODO(hs): we should work on making the mocks more realistic. Ideally, we should get rid of\n\t\t\t\t// the mock entirely, relying on an instances of provisioner, authority and DB (possibly hardest), so\n\t\t\t\t// that behavior of the tests is what an actual CA would do. We could gradually phase them out by\n\t\t\t\t// using the mocking functions as a wrapper for actual test helpers generated per test case or per\n\t\t\t\t// function that's tested.\n\t\t\t\tca: &mockSignAuth{\n\t\t\t\t\tsignWithContext: func(_ context.Context, _csr *x509.CertificateRequest, signOpts provisioner.SignOptions, extraOpts ...provisioner.SignOption) ([]*x509.Certificate, error) {\n\t\t\t\t\t\tassert.Equals(t, _csr, csr)\n\t\t\t\t\t\treturn []*x509.Certificate{leaf, inter, root}, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tdb: &MockDB{\n\t\t\t\t\tMockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {\n\t\t\t\t\t\treturn &Authorization{\n\t\t\t\t\t\t\tID:          id,\n\t\t\t\t\t\t\tFingerprint: fingerprint,\n\t\t\t\t\t\t\tStatus:      StatusValid,\n\t\t\t\t\t\t}, nil\n\t\t\t\t\t},\n\t\t\t\t\tMockCreateCertificate: func(ctx context.Context, cert *Certificate) error {\n\t\t\t\t\t\tcert.ID = \"certID\"\n\t\t\t\t\t\tassert.Equals(t, cert.AccountID, o.AccountID)\n\t\t\t\t\t\tassert.Equals(t, cert.OrderID, o.ID)\n\t\t\t\t\t\tassert.Equals(t, cert.Leaf, leaf)\n\t\t\t\t\t\tassert.Equals(t, cert.Intermediates, []*x509.Certificate{inter, root})\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t\tMockUpdateOrder: func(ctx context.Context, updo *Order) error {\n\t\t\t\t\t\tassert.Equals(t, updo.CertificateID, \"certID\")\n\t\t\t\t\t\tassert.Equals(t, updo.Status, StatusValid)\n\t\t\t\t\t\tassert.Equals(t, updo.ID, o.ID)\n\t\t\t\t\t\tassert.Equals(t, updo.AccountID, o.AccountID)\n\t\t\t\t\t\tassert.Equals(t, updo.ExpiresAt, o.ExpiresAt)\n\t\t\t\t\t\tassert.Equals(t, updo.AuthorizationIDs, o.AuthorizationIDs)\n\t\t\t\t\t\tassert.Equals(t, updo.Identifiers, o.Identifiers)\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"fail/csr-wire-id-csr-uri-missing\": func(t *testing.T) test {\n\t\t\tnow := clock.Now()\n\t\t\to := &Order{\n\t\t\t\tID:               \"oID\",\n\t\t\t\tAccountID:        \"accID\",\n\t\t\t\tStatus:           StatusReady,\n\t\t\t\tExpiresAt:        now.Add(5 * time.Minute),\n\t\t\t\tAuthorizationIDs: []string{\"a\", \"b\"},\n\t\t\t\tIdentifiers: []Identifier{\n\t\t\t\t\t{Type: \"wireapp-device\", Value: \"{\\\"name\\\": \\\"device\\\", \\\"domain\\\": \\\"wire.com\\\", \\\"client-id\\\": \\\"wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com\\\", \\\"handle\\\": \\\"wireapp://%40alice_wire@wire.com\\\"}\"},\n\t\t\t\t},\n\t\t\t}\n\n\t\t\tsigner := mustSigner(\"EC\", \"P-256\", 0)\n\t\t\t_, err := keyutil.Fingerprint(signer.Public())\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\tcsr := &x509.CertificateRequest{\n\t\t\t\tSubject: pkix.Name{\n\t\t\t\t\tNames: []pkix.AttributeTypeAndValue{\n\t\t\t\t\t\t{Type: asn1.ObjectIdentifier{2, 16, 840, 1, 113730, 3, 1, 241}, Value: \"device\"},\n\t\t\t\t\t},\n\t\t\t\t\tOrganization: []string{\"wire.com\"},\n\t\t\t\t},\n\t\t\t\tPublicKey: signer.Public(),\n\t\t\t\tExtraExtensions: []pkix.Extension{\n\t\t\t\t\t{\n\t\t\t\t\t\tId:    asn1.ObjectIdentifier{2, 16, 840, 1, 113730, 3, 1, 241},\n\t\t\t\t\t\tValue: []byte(\"a-wireapp-user\"),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\n\t\t\tleaf := &x509.Certificate{\n\t\t\t\tSubject:   pkix.Name{CommonName: \"a-wireapp-user\"},\n\t\t\t\tPublicKey: signer.Public(),\n\t\t\t\tExtraExtensions: []pkix.Extension{\n\t\t\t\t\t{\n\t\t\t\t\t\tId:    asn1.ObjectIdentifier{2, 16, 840, 1, 113730, 3, 1, 241},\n\t\t\t\t\t\tValue: []byte(\"a-wireapp-user\"),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t\tinter := &x509.Certificate{Subject: pkix.Name{CommonName: \"inter\"}}\n\t\t\troot := &x509.Certificate{Subject: pkix.Name{CommonName: \"root\"}}\n\n\t\t\treturn test{\n\t\t\t\to:   o,\n\t\t\t\tcsr: csr,\n\t\t\t\tprov: &MockProvisioner{\n\t\t\t\t\tMauthorizeSign: func(ctx context.Context, token string) ([]provisioner.SignOption, error) {\n\t\t\t\t\t\tassert.Equals(t, token, \"\")\n\t\t\t\t\t\treturn nil, nil\n\t\t\t\t\t},\n\t\t\t\t\tMgetOptions: func() *provisioner.Options {\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tca: &mockSignAuth{\n\t\t\t\t\tsignWithContext: func(_ context.Context, _csr *x509.CertificateRequest, signOpts provisioner.SignOptions, extraOpts ...provisioner.SignOption) ([]*x509.Certificate, error) {\n\t\t\t\t\t\tassert.Equals(t, _csr, csr)\n\t\t\t\t\t\treturn []*x509.Certificate{leaf, inter, root}, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tdb: &MockWireDB{\n\t\t\t\t\tMockDB: MockDB{\n\t\t\t\t\t\tMockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {\n\t\t\t\t\t\t\treturn &Authorization{ID: id, Status: StatusValid}, nil\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tMockGetDpopToken: func(ctx context.Context, orderID string) (map[string]interface{}, error) {\n\t\t\t\t\t\tassert.Equals(t, orderID, o.ID)\n\t\t\t\t\t\tdpopMap := map[string]interface{}{\n\t\t\t\t\t\t\t\"dpop\": \"a-dpop-token\",\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn dpopMap, nil\n\t\t\t\t\t},\n\t\t\t\t\tMockGetOidcToken: func(ctx context.Context, orderID string) (map[string]interface{}, error) {\n\t\t\t\t\t\tassert.Equals(t, orderID, o.ID)\n\t\t\t\t\t\toidcMap := map[string]interface{}{\n\t\t\t\t\t\t\t\"oidc\": \"a-oidc-token\",\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn oidcMap, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: NewError(ErrorBadCSRType, \"CSR URIs do not match identifiers exactly: CSR URIs = [], Order URIs = [wireapp://CzbfFjDOQrenCbDxVmgnFw%%21594930e9d50bb175@wire.com]\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/csr-wire-id-csr-uri-mismatch\": func(t *testing.T) test {\n\t\t\tnow := clock.Now()\n\t\t\to := &Order{\n\t\t\t\tID:               \"oID\",\n\t\t\t\tAccountID:        \"accID\",\n\t\t\t\tStatus:           StatusReady,\n\t\t\t\tExpiresAt:        now.Add(5 * time.Minute),\n\t\t\t\tAuthorizationIDs: []string{\"a\", \"b\"},\n\t\t\t\tIdentifiers: []Identifier{\n\t\t\t\t\t{Type: \"wireapp-device\", Value: \"{\\\"name\\\": \\\"device\\\", \\\"domain\\\": \\\"wire.com\\\", \\\"client-id\\\": \\\"wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com\\\", \\\"handle\\\": \\\"wireapp://%40alice_wire@wire.com\\\"}\"},\n\t\t\t\t},\n\t\t\t}\n\n\t\t\tsigner := mustSigner(\"EC\", \"P-256\", 0)\n\t\t\t_, err := keyutil.Fingerprint(signer.Public())\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\twireURL, _ := url.Parse(\"someurl.com\")\n\t\t\tcsr := &x509.CertificateRequest{\n\t\t\t\tSubject: pkix.Name{\n\t\t\t\t\tNames: []pkix.AttributeTypeAndValue{\n\t\t\t\t\t\t{Type: asn1.ObjectIdentifier{2, 16, 840, 1, 113730, 3, 1, 241}, Value: \"device\"},\n\t\t\t\t\t},\n\t\t\t\t\tOrganization: []string{\"wire.com\"},\n\t\t\t\t},\n\t\t\t\tURIs: []*url.URL{\n\t\t\t\t\twireURL,\n\t\t\t\t},\n\t\t\t\tPublicKey: signer.Public(),\n\t\t\t\tExtraExtensions: []pkix.Extension{\n\t\t\t\t\t{\n\t\t\t\t\t\tId:    asn1.ObjectIdentifier{2, 16, 840, 1, 113730, 3, 1, 241},\n\t\t\t\t\t\tValue: []byte(\"a-wireapp-user\"),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\n\t\t\tleaf := &x509.Certificate{\n\t\t\t\tSubject:   pkix.Name{CommonName: \"a-wireapp-user\"},\n\t\t\t\tPublicKey: signer.Public(),\n\t\t\t\tExtraExtensions: []pkix.Extension{\n\t\t\t\t\t{\n\t\t\t\t\t\tId:    asn1.ObjectIdentifier{2, 16, 840, 1, 113730, 3, 1, 241},\n\t\t\t\t\t\tValue: []byte(\"a-wireapp-user\"),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t\tinter := &x509.Certificate{Subject: pkix.Name{CommonName: \"inter\"}}\n\t\t\troot := &x509.Certificate{Subject: pkix.Name{CommonName: \"root\"}}\n\n\t\t\treturn test{\n\t\t\t\to:   o,\n\t\t\t\tcsr: csr,\n\t\t\t\tprov: &MockProvisioner{\n\t\t\t\t\tMauthorizeSign: func(ctx context.Context, token string) ([]provisioner.SignOption, error) {\n\t\t\t\t\t\tassert.Equals(t, token, \"\")\n\t\t\t\t\t\treturn nil, nil\n\t\t\t\t\t},\n\t\t\t\t\tMgetOptions: func() *provisioner.Options {\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tca: &mockSignAuth{\n\t\t\t\t\tsignWithContext: func(_ context.Context, _csr *x509.CertificateRequest, signOpts provisioner.SignOptions, extraOpts ...provisioner.SignOption) ([]*x509.Certificate, error) {\n\t\t\t\t\t\tassert.Equals(t, _csr, csr)\n\t\t\t\t\t\treturn []*x509.Certificate{leaf, inter, root}, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tdb: &MockWireDB{\n\t\t\t\t\tMockDB: MockDB{\n\t\t\t\t\t\tMockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {\n\t\t\t\t\t\t\treturn &Authorization{ID: id, Status: StatusValid}, nil\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tMockGetDpopToken: func(ctx context.Context, orderID string) (map[string]interface{}, error) {\n\t\t\t\t\t\tassert.Equals(t, orderID, o.ID)\n\t\t\t\t\t\tdpopMap := map[string]interface{}{\n\t\t\t\t\t\t\t\"dpop\": \"a-dpop-token\",\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn dpopMap, nil\n\t\t\t\t\t},\n\t\t\t\t\tMockGetOidcToken: func(ctx context.Context, orderID string) (map[string]interface{}, error) {\n\t\t\t\t\t\tassert.Equals(t, orderID, o.ID)\n\t\t\t\t\t\toidcMap := map[string]interface{}{\n\t\t\t\t\t\t\t\"oidc\": \"a-oidc-token\",\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn oidcMap, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: NewError(ErrorBadCSRType, \"CSR URIs do not match identifiers exactly: CSR URIs = [someurl.com], Order URIs = [wireapp://CzbfFjDOQrenCbDxVmgnFw%%21594930e9d50bb175@wire.com]\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/other-than-wire-ids-present\": func(t *testing.T) test {\n\t\t\tnow := clock.Now()\n\t\t\to := &Order{\n\t\t\t\tID:               \"oID\",\n\t\t\t\tAccountID:        \"accID\",\n\t\t\t\tStatus:           StatusReady,\n\t\t\t\tExpiresAt:        now.Add(5 * time.Minute),\n\t\t\t\tAuthorizationIDs: []string{\"a\", \"b\"},\n\t\t\t\tIdentifiers: []Identifier{\n\t\t\t\t\t{Type: \"wireapp-device\", Value: \"{\\\"name\\\": \\\"device\\\", \\\"domain\\\": \\\"wire.com\\\", \\\"client-id\\\": \\\"wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com\\\", \\\"handle\\\": \\\"wireapp://%40alice_wire@wire.com\\\"}\"},\n\t\t\t\t\t{Type: \"permanent-identifier\", Value: \"a-permanent-identifier\"},\n\t\t\t\t},\n\t\t\t}\n\n\t\t\tsigner := mustSigner(\"EC\", \"P-256\", 0)\n\t\t\t_, err := keyutil.Fingerprint(signer.Public())\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\twireURL, _ := url.Parse(\"wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com\")\n\t\t\tcsr := &x509.CertificateRequest{\n\t\t\t\tSubject: pkix.Name{\n\t\t\t\t\tNames: []pkix.AttributeTypeAndValue{\n\t\t\t\t\t\t{Type: asn1.ObjectIdentifier{2, 16, 840, 1, 113730, 3, 1, 241}, Value: \"device\"},\n\t\t\t\t\t},\n\t\t\t\t\tOrganization: []string{\"wire.com\"},\n\t\t\t\t},\n\t\t\t\tURIs: []*url.URL{\n\t\t\t\t\twireURL,\n\t\t\t\t},\n\t\t\t\tPublicKey: signer.Public(),\n\t\t\t\tExtraExtensions: []pkix.Extension{\n\t\t\t\t\t{\n\t\t\t\t\t\tId:    asn1.ObjectIdentifier{2, 16, 840, 1, 113730, 3, 1, 241},\n\t\t\t\t\t\tValue: []byte(\"a-wireapp-user\"),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\n\t\t\tleaf := &x509.Certificate{\n\t\t\t\tSubject:   pkix.Name{CommonName: \"a-wireapp-user\"},\n\t\t\t\tPublicKey: signer.Public(),\n\t\t\t\tExtraExtensions: []pkix.Extension{\n\t\t\t\t\t{\n\t\t\t\t\t\tId:    asn1.ObjectIdentifier{2, 16, 840, 1, 113730, 3, 1, 241},\n\t\t\t\t\t\tValue: []byte(\"a-wireapp-user\"),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t\tinter := &x509.Certificate{Subject: pkix.Name{CommonName: \"inter\"}}\n\t\t\troot := &x509.Certificate{Subject: pkix.Name{CommonName: \"root\"}}\n\n\t\t\treturn test{\n\t\t\t\to:   o,\n\t\t\t\tcsr: csr,\n\t\t\t\tprov: &MockProvisioner{\n\t\t\t\t\tMauthorizeSign: func(ctx context.Context, token string) ([]provisioner.SignOption, error) {\n\t\t\t\t\t\tassert.Equals(t, token, \"\")\n\t\t\t\t\t\treturn nil, nil\n\t\t\t\t\t},\n\t\t\t\t\tMgetOptions: func() *provisioner.Options {\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tca: &mockSignAuth{\n\t\t\t\t\tsignWithContext: func(_ context.Context, _csr *x509.CertificateRequest, signOpts provisioner.SignOptions, extraOpts ...provisioner.SignOption) ([]*x509.Certificate, error) {\n\t\t\t\t\t\tassert.Equals(t, _csr, csr)\n\t\t\t\t\t\treturn []*x509.Certificate{leaf, inter, root}, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tdb: &MockWireDB{\n\t\t\t\t\tMockDB: MockDB{\n\t\t\t\t\t\tMockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {\n\t\t\t\t\t\t\treturn &Authorization{ID: id, Status: StatusValid}, nil\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: NewError(ErrorServerInternalType, \"order must have exactly one WireUser and WireDevice identifier\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/wire-id-org-missing\": func(t *testing.T) test {\n\t\t\tnow := clock.Now()\n\t\t\to := &Order{\n\t\t\t\tID:               \"oID\",\n\t\t\t\tAccountID:        \"accID\",\n\t\t\t\tStatus:           StatusReady,\n\t\t\t\tExpiresAt:        now.Add(5 * time.Minute),\n\t\t\t\tAuthorizationIDs: []string{\"a\", \"b\"},\n\t\t\t\tIdentifiers: []Identifier{\n\t\t\t\t\t{Type: \"wireapp-user\", Value: \"{\\\"name\\\": \\\"Alice Smith\\\", \\\"domain\\\": \\\"wire.com\\\", \\\"handle\\\": \\\"wireapp://%40alice_wire@wire.com\\\"}\"},\n\t\t\t\t},\n\t\t\t}\n\n\t\t\tsigner := mustSigner(\"EC\", \"P-256\", 0)\n\t\t\t_, err := keyutil.Fingerprint(signer.Public())\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\twireURL, _ := url.Parse(\"wireapp://%40alice_wire@wire.com\")\n\t\t\tcsr := &x509.CertificateRequest{\n\t\t\t\tSubject: pkix.Name{\n\t\t\t\t\tNames: []pkix.AttributeTypeAndValue{\n\t\t\t\t\t\t{Type: asn1.ObjectIdentifier{2, 16, 840, 1, 113730, 3, 1, 241}, Value: \"Alice Smith\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tURIs: []*url.URL{\n\t\t\t\t\twireURL,\n\t\t\t\t},\n\t\t\t\tPublicKey: signer.Public(),\n\t\t\t}\n\n\t\t\tleaf := &x509.Certificate{\n\t\t\t\tSubject:   pkix.Name{CommonName: \"a-wireapp-user\"},\n\t\t\t\tPublicKey: signer.Public(),\n\t\t\t\tExtraExtensions: []pkix.Extension{\n\t\t\t\t\t{\n\t\t\t\t\t\tId:    asn1.ObjectIdentifier{2, 16, 840, 1, 113730, 3, 1, 241},\n\t\t\t\t\t\tValue: []byte(\"a-wireapp-user\"),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t\tinter := &x509.Certificate{Subject: pkix.Name{CommonName: \"inter\"}}\n\t\t\troot := &x509.Certificate{Subject: pkix.Name{CommonName: \"root\"}}\n\n\t\t\treturn test{\n\t\t\t\to:   o,\n\t\t\t\tcsr: csr,\n\t\t\t\tprov: &MockProvisioner{\n\t\t\t\t\tMauthorizeSign: func(ctx context.Context, token string) ([]provisioner.SignOption, error) {\n\t\t\t\t\t\tassert.Equals(t, token, \"\")\n\t\t\t\t\t\treturn nil, nil\n\t\t\t\t\t},\n\t\t\t\t\tMgetOptions: func() *provisioner.Options {\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tca: &mockSignAuth{\n\t\t\t\t\tsignWithContext: func(_ context.Context, _csr *x509.CertificateRequest, signOpts provisioner.SignOptions, extraOpts ...provisioner.SignOption) ([]*x509.Certificate, error) {\n\t\t\t\t\t\tassert.Equals(t, _csr, csr)\n\t\t\t\t\t\treturn []*x509.Certificate{leaf, inter, root}, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tdb: &MockWireDB{\n\t\t\t\t\tMockDB: MockDB{\n\t\t\t\t\t\tMockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {\n\t\t\t\t\t\t\treturn &Authorization{ID: id, Status: StatusValid}, nil\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: NewError(ErrorServerInternalType, \"expected Organization [wire.com], found []\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/wire-id-display-name-missing\": func(t *testing.T) test {\n\t\t\tnow := clock.Now()\n\t\t\to := &Order{\n\t\t\t\tID:               \"oID\",\n\t\t\t\tAccountID:        \"accID\",\n\t\t\t\tStatus:           StatusReady,\n\t\t\t\tExpiresAt:        now.Add(5 * time.Minute),\n\t\t\t\tAuthorizationIDs: []string{\"a\", \"b\"},\n\t\t\t\tIdentifiers: []Identifier{\n\t\t\t\t\t{Type: \"wireapp-user\", Value: \"{\\\"name\\\": \\\"Alice Smith\\\", \\\"domain\\\": \\\"wire.com\\\", \\\"handle\\\": \\\"wireapp://%40alice_wire@wire.com\\\"}\"},\n\t\t\t\t},\n\t\t\t}\n\n\t\t\tsigner := mustSigner(\"EC\", \"P-256\", 0)\n\t\t\t_, err := keyutil.Fingerprint(signer.Public())\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\twireURL, _ := url.Parse(\"wireapp://%40alice_wire@wire.com\")\n\t\t\tcsr := &x509.CertificateRequest{\n\t\t\t\tSubject: pkix.Name{\n\t\t\t\t\tOrganization: []string{\"wire.com\"},\n\t\t\t\t},\n\t\t\t\tURIs: []*url.URL{\n\t\t\t\t\twireURL,\n\t\t\t\t},\n\t\t\t\tPublicKey: signer.Public(),\n\t\t\t}\n\n\t\t\tleaf := &x509.Certificate{\n\t\t\t\tSubject:   pkix.Name{CommonName: \"a-wireapp-user\"},\n\t\t\t\tPublicKey: signer.Public(),\n\t\t\t\tExtraExtensions: []pkix.Extension{\n\t\t\t\t\t{\n\t\t\t\t\t\tId:    asn1.ObjectIdentifier{2, 16, 840, 1, 113730, 3, 1, 241},\n\t\t\t\t\t\tValue: []byte(\"a-wireapp-user\"),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t\tinter := &x509.Certificate{Subject: pkix.Name{CommonName: \"inter\"}}\n\t\t\troot := &x509.Certificate{Subject: pkix.Name{CommonName: \"root\"}}\n\n\t\t\treturn test{\n\t\t\t\to:   o,\n\t\t\t\tcsr: csr,\n\t\t\t\tprov: &MockProvisioner{\n\t\t\t\t\tMauthorizeSign: func(ctx context.Context, token string) ([]provisioner.SignOption, error) {\n\t\t\t\t\t\tassert.Equals(t, token, \"\")\n\t\t\t\t\t\treturn nil, nil\n\t\t\t\t\t},\n\t\t\t\t\tMgetOptions: func() *provisioner.Options {\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tca: &mockSignAuth{\n\t\t\t\t\tsignWithContext: func(_ context.Context, _csr *x509.CertificateRequest, signOpts provisioner.SignOptions, extraOpts ...provisioner.SignOption) ([]*x509.Certificate, error) {\n\t\t\t\t\t\tassert.Equals(t, _csr, csr)\n\t\t\t\t\t\treturn []*x509.Certificate{leaf, inter, root}, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tdb: &MockWireDB{\n\t\t\t\t\tMockDB: MockDB{\n\t\t\t\t\t\tMockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {\n\t\t\t\t\t\t\treturn &Authorization{ID: id, Status: StatusValid}, nil\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: NewError(ErrorServerInternalType, \"CSR must contain the display name in '2.16.840.1.113730.3.1.241' OID\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/wire-id-display-name-mismatch\": func(t *testing.T) test {\n\t\t\tnow := clock.Now()\n\t\t\to := &Order{\n\t\t\t\tID:               \"oID\",\n\t\t\t\tAccountID:        \"accID\",\n\t\t\t\tStatus:           StatusReady,\n\t\t\t\tExpiresAt:        now.Add(5 * time.Minute),\n\t\t\t\tAuthorizationIDs: []string{\"a\", \"b\"},\n\t\t\t\tIdentifiers: []Identifier{\n\t\t\t\t\t{Type: \"wireapp-user\", Value: \"{\\\"name\\\": \\\"Alice Smith\\\", \\\"domain\\\": \\\"wire.com\\\", \\\"handle\\\": \\\"wireapp://%40alice_wire@wire.com\\\"}\"},\n\t\t\t\t},\n\t\t\t}\n\n\t\t\tsigner := mustSigner(\"EC\", \"P-256\", 0)\n\t\t\t_, err := keyutil.Fingerprint(signer.Public())\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\twireURL, _ := url.Parse(\"wireapp://%40alice_wire@wire.com\")\n\t\t\tcsr := &x509.CertificateRequest{\n\t\t\t\tSubject: pkix.Name{\n\t\t\t\t\tNames: []pkix.AttributeTypeAndValue{\n\t\t\t\t\t\t{Type: asn1.ObjectIdentifier{2, 16, 840, 1, 113730, 3, 1, 241}, Value: \"Someone else\"},\n\t\t\t\t\t},\n\t\t\t\t\tOrganization: []string{\"wire.com\"},\n\t\t\t\t},\n\t\t\t\tURIs: []*url.URL{\n\t\t\t\t\twireURL,\n\t\t\t\t},\n\t\t\t\tPublicKey: signer.Public(),\n\t\t\t}\n\n\t\t\tleaf := &x509.Certificate{\n\t\t\t\tSubject:   pkix.Name{CommonName: \"a-wireapp-user\"},\n\t\t\t\tPublicKey: signer.Public(),\n\t\t\t\tExtraExtensions: []pkix.Extension{\n\t\t\t\t\t{\n\t\t\t\t\t\tId:    asn1.ObjectIdentifier{2, 16, 840, 1, 113730, 3, 1, 241},\n\t\t\t\t\t\tValue: []byte(\"a-wireapp-user\"),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t\tinter := &x509.Certificate{Subject: pkix.Name{CommonName: \"inter\"}}\n\t\t\troot := &x509.Certificate{Subject: pkix.Name{CommonName: \"root\"}}\n\n\t\t\treturn test{\n\t\t\t\to:   o,\n\t\t\t\tcsr: csr,\n\t\t\t\tprov: &MockProvisioner{\n\t\t\t\t\tMauthorizeSign: func(ctx context.Context, token string) ([]provisioner.SignOption, error) {\n\t\t\t\t\t\tassert.Equals(t, token, \"\")\n\t\t\t\t\t\treturn nil, nil\n\t\t\t\t\t},\n\t\t\t\t\tMgetOptions: func() *provisioner.Options {\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tca: &mockSignAuth{\n\t\t\t\t\tsignWithContext: func(_ context.Context, _csr *x509.CertificateRequest, signOpts provisioner.SignOptions, extraOpts ...provisioner.SignOption) ([]*x509.Certificate, error) {\n\t\t\t\t\t\tassert.Equals(t, _csr, csr)\n\t\t\t\t\t\treturn []*x509.Certificate{leaf, inter, root}, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tdb: &MockWireDB{\n\t\t\t\t\tMockDB: MockDB{\n\t\t\t\t\t\tMockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {\n\t\t\t\t\t\t\treturn &Authorization{ID: id, Status: StatusValid}, nil\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: NewError(ErrorServerInternalType, \"expected displayName Alice Smith, found Someone else\"),\n\t\t\t}\n\t\t},\n\t\t\"ok/wire-id-user\": func(t *testing.T) test {\n\t\t\tnow := clock.Now()\n\t\t\to := &Order{\n\t\t\t\tID:               \"oID\",\n\t\t\t\tAccountID:        \"accID\",\n\t\t\t\tStatus:           StatusReady,\n\t\t\t\tExpiresAt:        now.Add(5 * time.Minute),\n\t\t\t\tAuthorizationIDs: []string{\"a\", \"b\"},\n\t\t\t\tIdentifiers: []Identifier{\n\t\t\t\t\t{Type: \"wireapp-user\", Value: \"{\\\"name\\\": \\\"Alice Smith\\\", \\\"domain\\\": \\\"wire.com\\\", \\\"handle\\\": \\\"wireapp://%40alice_wire@wire.com\\\"}\"},\n\t\t\t\t},\n\t\t\t}\n\n\t\t\tsigner := mustSigner(\"EC\", \"P-256\", 0)\n\t\t\t_, err := keyutil.Fingerprint(signer.Public())\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\twireURL, _ := url.Parse(\"wireapp://%40alice_wire@wire.com\")\n\t\t\tcsr := &x509.CertificateRequest{\n\t\t\t\tSubject: pkix.Name{\n\t\t\t\t\tNames: []pkix.AttributeTypeAndValue{\n\t\t\t\t\t\t{Type: asn1.ObjectIdentifier{2, 16, 840, 1, 113730, 3, 1, 241}, Value: \"Alice Smith\"},\n\t\t\t\t\t},\n\t\t\t\t\tOrganization: []string{\"wire.com\"},\n\t\t\t\t},\n\t\t\t\tURIs: []*url.URL{\n\t\t\t\t\twireURL,\n\t\t\t\t},\n\t\t\t\tPublicKey: signer.Public(),\n\t\t\t\tExtraExtensions: []pkix.Extension{\n\t\t\t\t\t{\n\t\t\t\t\t\tId:    asn1.ObjectIdentifier{2, 16, 840, 1, 113730, 3, 1, 241},\n\t\t\t\t\t\tValue: []byte(\"a-wireapp-user\"),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\n\t\t\tleaf := &x509.Certificate{\n\t\t\t\tSubject:   pkix.Name{CommonName: \"a-wireapp-user\"},\n\t\t\t\tPublicKey: signer.Public(),\n\t\t\t\tExtraExtensions: []pkix.Extension{\n\t\t\t\t\t{\n\t\t\t\t\t\tId:    asn1.ObjectIdentifier{2, 16, 840, 1, 113730, 3, 1, 241},\n\t\t\t\t\t\tValue: []byte(\"a-wireapp-user\"),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t\tinter := &x509.Certificate{Subject: pkix.Name{CommonName: \"inter\"}}\n\t\t\troot := &x509.Certificate{Subject: pkix.Name{CommonName: \"root\"}}\n\n\t\t\treturn test{\n\t\t\t\to:   o,\n\t\t\t\tcsr: csr,\n\t\t\t\tprov: &MockProvisioner{\n\t\t\t\t\tMauthorizeSign: func(ctx context.Context, token string) ([]provisioner.SignOption, error) {\n\t\t\t\t\t\tassert.Equals(t, token, \"\")\n\t\t\t\t\t\treturn nil, nil\n\t\t\t\t\t},\n\t\t\t\t\tMgetOptions: func() *provisioner.Options {\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tca: &mockSignAuth{\n\t\t\t\t\tsignWithContext: func(_ context.Context, _csr *x509.CertificateRequest, signOpts provisioner.SignOptions, extraOpts ...provisioner.SignOption) ([]*x509.Certificate, error) {\n\t\t\t\t\t\tassert.Equals(t, _csr, csr)\n\t\t\t\t\t\treturn []*x509.Certificate{leaf, inter, root}, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tdb: &MockWireDB{\n\t\t\t\t\tMockDB: MockDB{\n\t\t\t\t\t\tMockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {\n\t\t\t\t\t\t\treturn &Authorization{ID: id, Status: StatusValid}, nil\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tMockGetDpopToken: func(ctx context.Context, orderID string) (map[string]interface{}, error) {\n\t\t\t\t\t\tassert.Equals(t, orderID, o.ID)\n\t\t\t\t\t\tdpopMap := map[string]interface{}{\n\t\t\t\t\t\t\t\"dpop\": \"a-dpop-token\",\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn dpopMap, nil\n\t\t\t\t\t},\n\t\t\t\t\tMockGetOidcToken: func(ctx context.Context, orderID string) (map[string]interface{}, error) {\n\t\t\t\t\t\tassert.Equals(t, orderID, o.ID)\n\t\t\t\t\t\toidcMap := map[string]interface{}{\n\t\t\t\t\t\t\t\"oidc\": \"a-oidc-token\",\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn oidcMap, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"ok/wire-id-device\": func(t *testing.T) test {\n\t\t\tnow := clock.Now()\n\t\t\to := &Order{\n\t\t\t\tID:               \"oID\",\n\t\t\t\tAccountID:        \"accID\",\n\t\t\t\tStatus:           StatusReady,\n\t\t\t\tExpiresAt:        now.Add(5 * time.Minute),\n\t\t\t\tAuthorizationIDs: []string{\"a\", \"b\"},\n\t\t\t\tIdentifiers: []Identifier{\n\t\t\t\t\t{Type: \"wireapp-device\", Value: \"{\\\"name\\\": \\\"device\\\", \\\"domain\\\": \\\"wire.com\\\", \\\"client-id\\\": \\\"wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com\\\", \\\"handle\\\": \\\"wireapp://%40alice_wire@wire.com\\\"}\"},\n\t\t\t\t},\n\t\t\t}\n\n\t\t\tsigner := mustSigner(\"EC\", \"P-256\", 0)\n\t\t\t_, err := keyutil.Fingerprint(signer.Public())\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\twireURL, _ := url.Parse(\"wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com\")\n\t\t\tcsr := &x509.CertificateRequest{\n\t\t\t\tSubject: pkix.Name{\n\t\t\t\t\tNames: []pkix.AttributeTypeAndValue{\n\t\t\t\t\t\t{Type: asn1.ObjectIdentifier{2, 16, 840, 1, 113730, 3, 1, 241}, Value: \"device\"},\n\t\t\t\t\t},\n\t\t\t\t\tOrganization: []string{\"wire.com\"},\n\t\t\t\t},\n\t\t\t\tURIs: []*url.URL{\n\t\t\t\t\twireURL,\n\t\t\t\t},\n\t\t\t\tPublicKey: signer.Public(),\n\t\t\t\tExtraExtensions: []pkix.Extension{\n\t\t\t\t\t{\n\t\t\t\t\t\tId:    asn1.ObjectIdentifier{2, 16, 840, 1, 113730, 3, 1, 241},\n\t\t\t\t\t\tValue: []byte(\"a-wireapp-user\"),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\n\t\t\tleaf := &x509.Certificate{\n\t\t\t\tSubject:   pkix.Name{CommonName: \"a-wireapp-user\"},\n\t\t\t\tPublicKey: signer.Public(),\n\t\t\t\tExtraExtensions: []pkix.Extension{\n\t\t\t\t\t{\n\t\t\t\t\t\tId:    asn1.ObjectIdentifier{2, 16, 840, 1, 113730, 3, 1, 241},\n\t\t\t\t\t\tValue: []byte(\"a-wireapp-user\"),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t\tinter := &x509.Certificate{Subject: pkix.Name{CommonName: \"inter\"}}\n\t\t\troot := &x509.Certificate{Subject: pkix.Name{CommonName: \"root\"}}\n\n\t\t\treturn test{\n\t\t\t\to:   o,\n\t\t\t\tcsr: csr,\n\t\t\t\tprov: &MockProvisioner{\n\t\t\t\t\tMauthorizeSign: func(ctx context.Context, token string) ([]provisioner.SignOption, error) {\n\t\t\t\t\t\tassert.Equals(t, token, \"\")\n\t\t\t\t\t\treturn nil, nil\n\t\t\t\t\t},\n\t\t\t\t\tMgetOptions: func() *provisioner.Options {\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tca: &mockSignAuth{\n\t\t\t\t\tsignWithContext: func(_ context.Context, _csr *x509.CertificateRequest, signOpts provisioner.SignOptions, extraOpts ...provisioner.SignOption) ([]*x509.Certificate, error) {\n\t\t\t\t\t\tassert.Equals(t, _csr, csr)\n\t\t\t\t\t\treturn []*x509.Certificate{leaf, inter, root}, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tdb: &MockWireDB{\n\t\t\t\t\tMockDB: MockDB{\n\t\t\t\t\t\tMockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {\n\t\t\t\t\t\t\treturn &Authorization{ID: id, Status: StatusValid}, nil\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tMockGetDpopToken: func(ctx context.Context, orderID string) (map[string]interface{}, error) {\n\t\t\t\t\t\tassert.Equals(t, orderID, o.ID)\n\t\t\t\t\t\tdpopMap := map[string]interface{}{\n\t\t\t\t\t\t\t\"dpop\": \"a-dpop-token\",\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn dpopMap, nil\n\t\t\t\t\t},\n\t\t\t\t\tMockGetOidcToken: func(ctx context.Context, orderID string) (map[string]interface{}, error) {\n\t\t\t\t\t\tassert.Equals(t, orderID, o.ID)\n\t\t\t\t\t\toidcMap := map[string]interface{}{\n\t\t\t\t\t\t\t\"oidc\": \"a-oidc-token\",\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn oidcMap, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"ok/new-cert-dns\": func(t *testing.T) test {\n\t\t\tnow := clock.Now()\n\t\t\to := &Order{\n\t\t\t\tID:               \"oID\",\n\t\t\t\tAccountID:        \"accID\",\n\t\t\t\tStatus:           StatusReady,\n\t\t\t\tExpiresAt:        now.Add(5 * time.Minute),\n\t\t\t\tAuthorizationIDs: []string{\"a\", \"b\"},\n\t\t\t\tIdentifiers: []Identifier{\n\t\t\t\t\t{Type: \"dns\", Value: \"foo.internal\"},\n\t\t\t\t\t{Type: \"dns\", Value: \"bar.internal\"},\n\t\t\t\t},\n\t\t\t}\n\t\t\tcsr := &x509.CertificateRequest{\n\t\t\t\tSubject: pkix.Name{\n\t\t\t\t\tCommonName: \"foo.internal\",\n\t\t\t\t},\n\t\t\t\tDNSNames: []string{\"bar.internal\"},\n\t\t\t}\n\n\t\t\tfoo := &x509.Certificate{Subject: pkix.Name{CommonName: \"foo\"}}\n\t\t\tbar := &x509.Certificate{Subject: pkix.Name{CommonName: \"bar\"}}\n\t\t\tbaz := &x509.Certificate{Subject: pkix.Name{CommonName: \"baz\"}}\n\n\t\t\treturn test{\n\t\t\t\to:   o,\n\t\t\t\tcsr: csr,\n\t\t\t\tprov: &MockProvisioner{\n\t\t\t\t\tMauthorizeSign: func(ctx context.Context, token string) ([]provisioner.SignOption, error) {\n\t\t\t\t\t\tassert.Equals(t, token, \"\")\n\t\t\t\t\t\treturn nil, nil\n\t\t\t\t\t},\n\t\t\t\t\tMgetOptions: func() *provisioner.Options {\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tca: &mockSignAuth{\n\t\t\t\t\tsignWithContext: func(_ context.Context, _csr *x509.CertificateRequest, signOpts provisioner.SignOptions, extraOpts ...provisioner.SignOption) ([]*x509.Certificate, error) {\n\t\t\t\t\t\tassert.Equals(t, _csr, csr)\n\t\t\t\t\t\treturn []*x509.Certificate{foo, bar, baz}, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tdb: &MockDB{\n\t\t\t\t\tMockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {\n\t\t\t\t\t\treturn &Authorization{ID: id, Status: StatusValid}, nil\n\t\t\t\t\t},\n\t\t\t\t\tMockCreateCertificate: func(ctx context.Context, cert *Certificate) error {\n\t\t\t\t\t\tcert.ID = \"certID\"\n\t\t\t\t\t\tassert.Equals(t, cert.AccountID, o.AccountID)\n\t\t\t\t\t\tassert.Equals(t, cert.OrderID, o.ID)\n\t\t\t\t\t\tassert.Equals(t, cert.Leaf, foo)\n\t\t\t\t\t\tassert.Equals(t, cert.Intermediates, []*x509.Certificate{bar, baz})\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t\tMockUpdateOrder: func(ctx context.Context, updo *Order) error {\n\t\t\t\t\t\tassert.Equals(t, updo.CertificateID, \"certID\")\n\t\t\t\t\t\tassert.Equals(t, updo.Status, StatusValid)\n\t\t\t\t\t\tassert.Equals(t, updo.ID, o.ID)\n\t\t\t\t\t\tassert.Equals(t, updo.AccountID, o.AccountID)\n\t\t\t\t\t\tassert.Equals(t, updo.ExpiresAt, o.ExpiresAt)\n\t\t\t\t\t\tassert.Equals(t, updo.AuthorizationIDs, o.AuthorizationIDs)\n\t\t\t\t\t\tassert.Equals(t, updo.Identifiers, o.Identifiers)\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"ok/new-cert-ip\": func(t *testing.T) test {\n\t\t\tnow := clock.Now()\n\t\t\to := &Order{\n\t\t\t\tID:               \"oID\",\n\t\t\t\tAccountID:        \"accID\",\n\t\t\t\tStatus:           StatusReady,\n\t\t\t\tExpiresAt:        now.Add(5 * time.Minute),\n\t\t\t\tAuthorizationIDs: []string{\"a\", \"b\"},\n\t\t\t\tIdentifiers: []Identifier{\n\t\t\t\t\t{Type: \"ip\", Value: \"192.168.42.42\"},\n\t\t\t\t\t{Type: \"ip\", Value: \"192.168.43.42\"},\n\t\t\t\t},\n\t\t\t}\n\t\t\tcsr := &x509.CertificateRequest{\n\t\t\t\tIPAddresses: []net.IP{net.ParseIP(\"192.168.42.42\"), net.ParseIP(\"192.168.43.42\")}, // in case of IPs, no Common Name\n\t\t\t}\n\n\t\t\tfoo := &x509.Certificate{Subject: pkix.Name{CommonName: \"foo\"}}\n\t\t\tbar := &x509.Certificate{Subject: pkix.Name{CommonName: \"bar\"}}\n\t\t\tbaz := &x509.Certificate{Subject: pkix.Name{CommonName: \"baz\"}}\n\n\t\t\treturn test{\n\t\t\t\to:   o,\n\t\t\t\tcsr: csr,\n\t\t\t\tprov: &MockProvisioner{\n\t\t\t\t\tMauthorizeSign: func(ctx context.Context, token string) ([]provisioner.SignOption, error) {\n\t\t\t\t\t\tassert.Equals(t, token, \"\")\n\t\t\t\t\t\treturn nil, nil\n\t\t\t\t\t},\n\t\t\t\t\tMgetOptions: func() *provisioner.Options {\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tca: &mockSignAuth{\n\t\t\t\t\tsignWithContext: func(_ context.Context, _csr *x509.CertificateRequest, signOpts provisioner.SignOptions, extraOpts ...provisioner.SignOption) ([]*x509.Certificate, error) {\n\t\t\t\t\t\tassert.Equals(t, _csr, csr)\n\t\t\t\t\t\treturn []*x509.Certificate{foo, bar, baz}, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tdb: &MockDB{\n\t\t\t\t\tMockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {\n\t\t\t\t\t\treturn &Authorization{ID: id, Status: StatusValid}, nil\n\t\t\t\t\t},\n\t\t\t\t\tMockCreateCertificate: func(ctx context.Context, cert *Certificate) error {\n\t\t\t\t\t\tcert.ID = \"certID\"\n\t\t\t\t\t\tassert.Equals(t, cert.AccountID, o.AccountID)\n\t\t\t\t\t\tassert.Equals(t, cert.OrderID, o.ID)\n\t\t\t\t\t\tassert.Equals(t, cert.Leaf, foo)\n\t\t\t\t\t\tassert.Equals(t, cert.Intermediates, []*x509.Certificate{bar, baz})\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t\tMockUpdateOrder: func(ctx context.Context, updo *Order) error {\n\t\t\t\t\t\tassert.Equals(t, updo.CertificateID, \"certID\")\n\t\t\t\t\t\tassert.Equals(t, updo.Status, StatusValid)\n\t\t\t\t\t\tassert.Equals(t, updo.ID, o.ID)\n\t\t\t\t\t\tassert.Equals(t, updo.AccountID, o.AccountID)\n\t\t\t\t\t\tassert.Equals(t, updo.ExpiresAt, o.ExpiresAt)\n\t\t\t\t\t\tassert.Equals(t, updo.AuthorizationIDs, o.AuthorizationIDs)\n\t\t\t\t\t\tassert.Equals(t, updo.Identifiers, o.Identifiers)\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"ok/new-cert-dns-and-ip\": func(t *testing.T) test {\n\t\t\tnow := clock.Now()\n\t\t\to := &Order{\n\t\t\t\tID:               \"oID\",\n\t\t\t\tAccountID:        \"accID\",\n\t\t\t\tStatus:           StatusReady,\n\t\t\t\tExpiresAt:        now.Add(5 * time.Minute),\n\t\t\t\tAuthorizationIDs: []string{\"a\", \"b\"},\n\t\t\t\tIdentifiers: []Identifier{\n\t\t\t\t\t{Type: \"dns\", Value: \"foo.internal\"},\n\t\t\t\t\t{Type: \"ip\", Value: \"192.168.42.42\"},\n\t\t\t\t},\n\t\t\t}\n\t\t\tcsr := &x509.CertificateRequest{\n\t\t\t\tSubject: pkix.Name{\n\t\t\t\t\tCommonName: \"foo.internal\",\n\t\t\t\t},\n\t\t\t\tIPAddresses: []net.IP{net.ParseIP(\"192.168.42.42\")},\n\t\t\t}\n\n\t\t\tfoo := &x509.Certificate{Subject: pkix.Name{CommonName: \"foo\"}}\n\t\t\tbar := &x509.Certificate{Subject: pkix.Name{CommonName: \"bar\"}}\n\t\t\tbaz := &x509.Certificate{Subject: pkix.Name{CommonName: \"baz\"}}\n\n\t\t\treturn test{\n\t\t\t\to:   o,\n\t\t\t\tcsr: csr,\n\t\t\t\tprov: &MockProvisioner{\n\t\t\t\t\tMauthorizeSign: func(ctx context.Context, token string) ([]provisioner.SignOption, error) {\n\t\t\t\t\t\tassert.Equals(t, token, \"\")\n\t\t\t\t\t\treturn nil, nil\n\t\t\t\t\t},\n\t\t\t\t\tMgetOptions: func() *provisioner.Options {\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tca: &mockSignAuth{\n\t\t\t\t\tsignWithContext: func(_ context.Context, _csr *x509.CertificateRequest, signOpts provisioner.SignOptions, extraOpts ...provisioner.SignOption) ([]*x509.Certificate, error) {\n\t\t\t\t\t\tassert.Equals(t, _csr, csr)\n\t\t\t\t\t\treturn []*x509.Certificate{foo, bar, baz}, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tdb: &MockDB{\n\t\t\t\t\tMockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {\n\t\t\t\t\t\treturn &Authorization{ID: id, Status: StatusValid}, nil\n\t\t\t\t\t},\n\t\t\t\t\tMockCreateCertificate: func(ctx context.Context, cert *Certificate) error {\n\t\t\t\t\t\tcert.ID = \"certID\"\n\t\t\t\t\t\tassert.Equals(t, cert.AccountID, o.AccountID)\n\t\t\t\t\t\tassert.Equals(t, cert.OrderID, o.ID)\n\t\t\t\t\t\tassert.Equals(t, cert.Leaf, foo)\n\t\t\t\t\t\tassert.Equals(t, cert.Intermediates, []*x509.Certificate{bar, baz})\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t\tMockUpdateOrder: func(ctx context.Context, updo *Order) error {\n\t\t\t\t\t\tassert.Equals(t, updo.CertificateID, \"certID\")\n\t\t\t\t\t\tassert.Equals(t, updo.Status, StatusValid)\n\t\t\t\t\t\tassert.Equals(t, updo.ID, o.ID)\n\t\t\t\t\t\tassert.Equals(t, updo.AccountID, o.AccountID)\n\t\t\t\t\t\tassert.Equals(t, updo.ExpiresAt, o.ExpiresAt)\n\t\t\t\t\t\tassert.Equals(t, updo.AuthorizationIDs, o.AuthorizationIDs)\n\t\t\t\t\t\tassert.Equals(t, updo.Identifiers, o.Identifiers)\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ttc := run(t)\n\t\t\tif err := tc.o.Finalize(context.Background(), tc.db, tc.csr, tc.ca, tc.prov); err != nil {\n\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\tvar k *Error\n\t\t\t\t\tif errors.As(err, &k) {\n\t\t\t\t\t\tassert.Equals(t, k.Type, tc.err.Type)\n\t\t\t\t\t\tassert.Equals(t, k.Detail, tc.err.Detail)\n\t\t\t\t\t\tassert.Equals(t, k.Status, tc.err.Status)\n\t\t\t\t\t\tassert.Equals(t, k.Err.Error(), tc.err.Err.Error())\n\t\t\t\t\t\tassert.Equals(t, k.Detail, tc.err.Detail)\n\t\t\t\t\t\tassert.Equals(t, k.Subproblems, tc.err.Subproblems)\n\t\t\t\t\t} else {\n\t\t\t\t\t\tassert.FatalError(t, errors.New(\"unexpected error type\"))\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tassert.Nil(t, tc.err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_uniqueSortedIPs(t *testing.T) {\n\ttype args struct {\n\t\tips []net.IP\n\t}\n\ttests := []struct {\n\t\tname string\n\t\targs args\n\t\twant []net.IP\n\t}{\n\t\t{\n\t\t\tname: \"ok/empty\",\n\t\t\targs: args{\n\t\t\t\tips: []net.IP{},\n\t\t\t},\n\t\t\twant: []net.IP{},\n\t\t},\n\t\t{\n\t\t\tname: \"ok/single-ipv4\",\n\t\t\targs: args{\n\t\t\t\tips: []net.IP{net.ParseIP(\"192.168.42.42\")},\n\t\t\t},\n\t\t\twant: []net.IP{net.ParseIP(\"192.168.42.42\")},\n\t\t},\n\t\t{\n\t\t\tname: \"ok/multiple-ipv4\",\n\t\t\targs: args{\n\t\t\t\tips: []net.IP{net.ParseIP(\"192.168.42.42\"), net.ParseIP(\"192.168.42.10\"), net.ParseIP(\"192.168.42.1\"), net.ParseIP(\"127.0.0.1\")},\n\t\t\t},\n\t\t\twant: []net.IP{net.ParseIP(\"127.0.0.1\"), net.ParseIP(\"192.168.42.1\"), net.ParseIP(\"192.168.42.10\"), net.ParseIP(\"192.168.42.42\")},\n\t\t}, {\n\t\t\tname: \"ok/multiple-ipv4-with-varying-byte-representations\",\n\t\t\targs: args{\n\t\t\t\tips: []net.IP{net.ParseIP(\"192.168.42.42\"), net.ParseIP(\"192.168.42.10\"), net.ParseIP(\"192.168.42.1\"), []byte{0x7f, 0x0, 0x0, 0x1}},\n\t\t\t},\n\t\t\twant: []net.IP{net.ParseIP(\"127.0.0.1\"), net.ParseIP(\"192.168.42.1\"), net.ParseIP(\"192.168.42.10\"), net.ParseIP(\"192.168.42.42\")},\n\t\t},\n\t\t{\n\t\t\tname: \"ok/unique-ipv4\",\n\t\t\targs: args{\n\t\t\t\tips: []net.IP{net.ParseIP(\"192.168.42.42\"), net.ParseIP(\"192.168.42.42\")},\n\t\t\t},\n\t\t\twant: []net.IP{net.ParseIP(\"192.168.42.42\")},\n\t\t},\n\t\t{\n\t\t\tname: \"ok/single-ipv6\",\n\t\t\targs: args{\n\t\t\t\tips: []net.IP{net.ParseIP(\"2001:db8::30\")},\n\t\t\t},\n\t\t\twant: []net.IP{net.ParseIP(\"2001:db8::30\")},\n\t\t},\n\t\t{\n\t\t\tname: \"ok/multiple-ipv6\",\n\t\t\targs: args{\n\t\t\t\tips: []net.IP{net.ParseIP(\"2001:db8::30\"), net.ParseIP(\"2001:db8::20\"), net.ParseIP(\"2001:db8::10\")},\n\t\t\t},\n\t\t\twant: []net.IP{net.ParseIP(\"2001:db8::10\"), net.ParseIP(\"2001:db8::20\"), net.ParseIP(\"2001:db8::30\")},\n\t\t},\n\t\t{\n\t\t\tname: \"ok/unique-ipv6\",\n\t\t\targs: args{\n\t\t\t\tips: []net.IP{net.ParseIP(\"2001:db8::1\"), net.ParseIP(\"2001:db8::1\")},\n\t\t\t},\n\t\t\twant: []net.IP{net.ParseIP(\"2001:db8::1\")},\n\t\t},\n\t\t{\n\t\t\tname: \"ok/mixed-ipv4-and-ipv6\",\n\t\t\targs: args{\n\t\t\t\tips: []net.IP{net.ParseIP(\"2001:db8::1\"), net.ParseIP(\"2001:db8::1\"), net.ParseIP(\"192.168.42.42\"), net.ParseIP(\"192.168.42.42\")},\n\t\t\t},\n\t\t\twant: []net.IP{net.ParseIP(\"192.168.42.42\"), net.ParseIP(\"2001:db8::1\")},\n\t\t},\n\t\t{\n\t\t\tname: \"ok/mixed-ipv4-and-ipv6-and-varying-byte-representations\",\n\t\t\targs: args{\n\t\t\t\tips: []net.IP{net.ParseIP(\"2001:db8::1\"), net.ParseIP(\"2001:db8::1\"), net.ParseIP(\"192.168.42.42\"), net.ParseIP(\"192.168.42.42\"), []byte{0x7f, 0x0, 0x0, 0x1}},\n\t\t\t},\n\t\t\twant: []net.IP{net.ParseIP(\"127.0.0.1\"), net.ParseIP(\"192.168.42.42\"), net.ParseIP(\"2001:db8::1\")},\n\t\t},\n\t\t{\n\t\t\tname: \"ok/mixed-ipv4-and-ipv6-and-more-varying-byte-representations\",\n\t\t\targs: args{\n\t\t\t\tips: []net.IP{net.ParseIP(\"2001:db8::1\"), net.ParseIP(\"2001:db8::1\"), net.ParseIP(\"192.168.42.42\"), net.ParseIP(\"2001:db8::2\"), net.ParseIP(\"192.168.42.42\"), []byte{0x7f, 0x0, 0x0, 0x1}, []byte{0x7f, 0x0, 0x0, 0x1}, []byte{0x7f, 0x0, 0x0, 0x2}},\n\t\t\t},\n\t\t\twant: []net.IP{net.ParseIP(\"127.0.0.1\"), net.ParseIP(\"127.0.0.2\"), net.ParseIP(\"192.168.42.42\"), net.ParseIP(\"2001:db8::1\"), net.ParseIP(\"2001:db8::2\")},\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot := uniqueSortedIPs(tt.args.ips)\n\t\t\tif !cmp.Equal(tt.want, got) {\n\t\t\t\tt.Errorf(\"uniqueSortedIPs() diff =\\n%s\", cmp.Diff(tt.want, got))\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_numberOfIdentifierType(t *testing.T) {\n\ttype args struct {\n\t\ttyp IdentifierType\n\t\tids []Identifier\n\t}\n\ttests := []struct {\n\t\tname string\n\t\targs args\n\t\twant int\n\t}{\n\t\t{\n\t\t\tname: \"ok/no-identifiers\",\n\t\t\targs: args{\n\t\t\t\ttyp: DNS,\n\t\t\t\tids: []Identifier{},\n\t\t\t},\n\t\t\twant: 0,\n\t\t},\n\t\t{\n\t\t\tname: \"ok/no-dns\",\n\t\t\targs: args{\n\t\t\t\ttyp: DNS,\n\t\t\t\tids: []Identifier{\n\t\t\t\t\t{\n\t\t\t\t\t\tType:  IP,\n\t\t\t\t\t\tValue: \"192.168.42.42\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: 0,\n\t\t},\n\t\t{\n\t\t\tname: \"ok/no-ips\",\n\t\t\targs: args{\n\t\t\t\ttyp: IP,\n\t\t\t\tids: []Identifier{\n\t\t\t\t\t{\n\t\t\t\t\t\tType:  DNS,\n\t\t\t\t\t\tValue: \"example.com\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: 0,\n\t\t},\n\t\t{\n\t\t\tname: \"ok/one-dns\",\n\t\t\targs: args{\n\t\t\t\ttyp: DNS,\n\t\t\t\tids: []Identifier{\n\t\t\t\t\t{\n\t\t\t\t\t\tType:  DNS,\n\t\t\t\t\t\tValue: \"example.com\",\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tType:  IP,\n\t\t\t\t\t\tValue: \"192.168.42.42\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: 1,\n\t\t},\n\t\t{\n\t\t\tname: \"ok/one-ip\",\n\t\t\targs: args{\n\t\t\t\ttyp: IP,\n\t\t\t\tids: []Identifier{\n\t\t\t\t\t{\n\t\t\t\t\t\tType:  DNS,\n\t\t\t\t\t\tValue: \"example.com\",\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tType:  IP,\n\t\t\t\t\t\tValue: \"192.168.42.42\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: 1,\n\t\t},\n\t\t{\n\t\t\tname: \"ok/more-dns\",\n\t\t\targs: args{\n\t\t\t\ttyp: DNS,\n\t\t\t\tids: []Identifier{\n\t\t\t\t\t{\n\t\t\t\t\t\tType:  DNS,\n\t\t\t\t\t\tValue: \"example.com\",\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tType:  DNS,\n\t\t\t\t\t\tValue: \"*.example.com\",\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tType:  IP,\n\t\t\t\t\t\tValue: \"192.168.42.42\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: 2,\n\t\t},\n\t\t{\n\t\t\tname: \"ok/more-ips\",\n\t\t\targs: args{\n\t\t\t\ttyp: IP,\n\t\t\t\tids: []Identifier{\n\t\t\t\t\t{\n\t\t\t\t\t\tType:  DNS,\n\t\t\t\t\t\tValue: \"example.com\",\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tType:  IP,\n\t\t\t\t\t\tValue: \"192.168.42.42\",\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tType:  IP,\n\t\t\t\t\t\tValue: \"192.168.42.43\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: 2,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif got := numberOfIdentifierType(tt.args.typ, tt.args.ids); got != tt.want {\n\t\t\t\tt.Errorf(\"numberOfIdentifierType() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_ipsAreEqual(t *testing.T) {\n\ttype args struct {\n\t\tx net.IP\n\t\ty net.IP\n\t}\n\ttests := []struct {\n\t\tname string\n\t\targs args\n\t\twant bool\n\t}{\n\t\t{\n\t\t\tname: \"ok/ipv4\",\n\t\t\targs: args{\n\t\t\t\tx: net.ParseIP(\"192.168.42.42\"),\n\t\t\t\ty: net.ParseIP(\"192.168.42.42\"),\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"fail/ipv4\",\n\t\t\targs: args{\n\t\t\t\tx: net.ParseIP(\"192.168.42.42\"),\n\t\t\t\ty: net.ParseIP(\"192.168.42.43\"),\n\t\t\t},\n\t\t\twant: false,\n\t\t},\n\t\t{\n\t\t\tname: \"ok/ipv6\",\n\t\t\targs: args{\n\t\t\t\tx: net.ParseIP(\"2001:0db8:85a3:0000:0000:8a2e:0370:7334\"),\n\t\t\t\ty: net.ParseIP(\"2001:0db8:85a3:0000:0000:8a2e:0370:7334\"),\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"fail/ipv6\",\n\t\t\targs: args{\n\t\t\t\tx: net.ParseIP(\"2001:0db8:85a3:0000:0000:8a2e:0370:7334\"),\n\t\t\t\ty: net.ParseIP(\"2001:0db8:85a3:0000:0000:8a2e:0370:7335\"),\n\t\t\t},\n\t\t\twant: false,\n\t\t},\n\t\t{\n\t\t\tname: \"fail/ipv4-and-ipv6\",\n\t\t\targs: args{\n\t\t\t\tx: net.ParseIP(\"192.168.42.42\"),\n\t\t\t\ty: net.ParseIP(\"2001:0db8:85a3:0000:0000:8a2e:0370:7334\"),\n\t\t\t},\n\t\t\twant: false,\n\t\t},\n\t\t{\n\t\t\tname: \"ok/ipv4-mapped-to-ipv6\",\n\t\t\targs: args{\n\t\t\t\tx: net.ParseIP(\"192.168.42.42\"),\n\t\t\t\ty: net.ParseIP(\"::ffff:192.168.42.42\"), // parsed to the same IPv4 by Go\n\t\t\t},\n\t\t\twant: true, // we expect this to happen; a known issue in which ipv4 mapped ipv6 addresses are considered the same as their ipv4 counterpart\n\t\t},\n\t\t{\n\t\t\tname: \"fail/invalid-ipv4-and-valid-ipv6\",\n\t\t\targs: args{\n\t\t\t\tx: net.ParseIP(\"192.168.42.1000\"),\n\t\t\t\ty: net.ParseIP(\"2001:0db8:85a3:0000:0000:8a2e:0370:7334\"),\n\t\t\t},\n\t\t\twant: false,\n\t\t},\n\t\t{\n\t\t\tname: \"fail/valid-ipv4-and-invalid-ipv6\",\n\t\t\targs: args{\n\t\t\t\tx: net.ParseIP(\"192.168.42.42\"),\n\t\t\t\ty: net.ParseIP(\"2001:0db8:85a3:0000:0000:8a2e:0370:733400\"),\n\t\t\t},\n\t\t\twant: false,\n\t\t},\n\t\t{\n\t\t\tname: \"fail/invalid-ipv4-and-invalid-ipv6\",\n\t\t\targs: args{\n\t\t\t\tx: net.ParseIP(\"192.168.42.1000\"),\n\t\t\t\ty: net.ParseIP(\"2001:0db8:85a3:0000:0000:8a2e:0370:1000000\"),\n\t\t\t},\n\t\t\twant: false,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif got := ipsAreEqual(tt.args.x, tt.args.y); got != tt.want {\n\t\t\t\tt.Errorf(\"ipsAreEqual() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_canonicalize(t *testing.T) {\n\ttype args struct {\n\t\tcsr *x509.CertificateRequest\n\t}\n\ttests := []struct {\n\t\tname string\n\t\targs args\n\t\twant *x509.CertificateRequest\n\t}{\n\t\t{\n\t\t\tname: \"ok/dns\",\n\t\t\targs: args{\n\t\t\t\tcsr: &x509.CertificateRequest{\n\t\t\t\t\tDNSNames: []string{\"www.example.com\", \"example.com\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: &x509.CertificateRequest{\n\t\t\t\tDNSNames:    []string{\"example.com\", \"www.example.com\"},\n\t\t\t\tIPAddresses: []net.IP{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"ok/common-name\",\n\t\t\targs: args{\n\t\t\t\tcsr: &x509.CertificateRequest{\n\t\t\t\t\tSubject: pkix.Name{\n\t\t\t\t\t\tCommonName: \"example.com\",\n\t\t\t\t\t},\n\t\t\t\t\tDNSNames: []string{\"www.example.com\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: &x509.CertificateRequest{\n\t\t\t\tSubject: pkix.Name{\n\t\t\t\t\tCommonName: \"example.com\",\n\t\t\t\t},\n\t\t\t\tDNSNames:    []string{\"example.com\", \"www.example.com\"},\n\t\t\t\tIPAddresses: []net.IP{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"ok/ipv4\",\n\t\t\targs: args{\n\t\t\t\tcsr: &x509.CertificateRequest{\n\t\t\t\t\tIPAddresses: []net.IP{net.ParseIP(\"192.168.43.42\"), net.ParseIP(\"192.168.42.42\")},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: &x509.CertificateRequest{\n\t\t\t\tDNSNames:    []string{},\n\t\t\t\tIPAddresses: []net.IP{net.ParseIP(\"192.168.42.42\"), net.ParseIP(\"192.168.43.42\")},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"ok/mixed\",\n\t\t\targs: args{\n\t\t\t\tcsr: &x509.CertificateRequest{\n\t\t\t\t\tDNSNames:    []string{\"www.example.com\", \"example.com\"},\n\t\t\t\t\tIPAddresses: []net.IP{net.ParseIP(\"192.168.43.42\"), net.ParseIP(\"192.168.42.42\")},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: &x509.CertificateRequest{\n\t\t\t\tDNSNames:    []string{\"example.com\", \"www.example.com\"},\n\t\t\t\tIPAddresses: []net.IP{net.ParseIP(\"192.168.42.42\"), net.ParseIP(\"192.168.43.42\")},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"ok/mixed-common-name\",\n\t\t\targs: args{\n\t\t\t\tcsr: &x509.CertificateRequest{\n\t\t\t\t\tSubject: pkix.Name{\n\t\t\t\t\t\tCommonName: \"example.com\",\n\t\t\t\t\t},\n\t\t\t\t\tDNSNames:    []string{\"www.example.com\"},\n\t\t\t\t\tIPAddresses: []net.IP{net.ParseIP(\"192.168.43.42\"), net.ParseIP(\"192.168.42.42\")},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: &x509.CertificateRequest{\n\t\t\t\tSubject: pkix.Name{\n\t\t\t\t\tCommonName: \"example.com\",\n\t\t\t\t},\n\t\t\t\tDNSNames:    []string{\"example.com\", \"www.example.com\"},\n\t\t\t\tIPAddresses: []net.IP{net.ParseIP(\"192.168.42.42\"), net.ParseIP(\"192.168.43.42\")},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"ok/ip-common-name\",\n\t\t\targs: args{\n\t\t\t\tcsr: &x509.CertificateRequest{\n\t\t\t\t\tSubject: pkix.Name{\n\t\t\t\t\t\tCommonName: \"127.0.0.1\",\n\t\t\t\t\t},\n\t\t\t\t\tDNSNames:    []string{\"example.com\"},\n\t\t\t\t\tIPAddresses: []net.IP{net.ParseIP(\"192.168.43.42\"), net.ParseIP(\"192.168.42.42\")},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: &x509.CertificateRequest{\n\t\t\t\tSubject: pkix.Name{\n\t\t\t\t\tCommonName: \"127.0.0.1\",\n\t\t\t\t},\n\t\t\t\tDNSNames:    []string{\"example.com\"},\n\t\t\t\tIPAddresses: []net.IP{net.ParseIP(\"127.0.0.1\"), net.ParseIP(\"192.168.42.42\"), net.ParseIP(\"192.168.43.42\")},\n\t\t\t},\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot := canonicalize(tt.args.csr)\n\t\t\tif !cmp.Equal(tt.want, got) {\n\t\t\t\tt.Errorf(\"canonicalize() diff =\\n%s\", cmp.Diff(tt.want, got))\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestOrder_sans(t *testing.T) {\n\ttype fields struct {\n\t\tIdentifiers []Identifier\n\t}\n\ttests := []struct {\n\t\tname   string\n\t\tfields fields\n\t\tcsr    *x509.CertificateRequest\n\t\twant   []x509util.SubjectAlternativeName\n\t\terr    *Error\n\t}{\n\t\t{\n\t\t\tname: \"ok/dns\",\n\t\t\tfields: fields{\n\t\t\t\tIdentifiers: []Identifier{\n\t\t\t\t\t{Type: \"dns\", Value: \"example.com\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tcsr: &x509.CertificateRequest{\n\t\t\t\tSubject: pkix.Name{\n\t\t\t\t\tCommonName: \"example.com\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: []x509util.SubjectAlternativeName{\n\t\t\t\t{Type: \"dns\", Value: \"example.com\"},\n\t\t\t},\n\t\t\terr: nil,\n\t\t},\n\t\t{\n\t\t\tname: \"fail/invalid-alternative-name-email\",\n\t\t\tfields: fields{\n\t\t\t\tIdentifiers: []Identifier{},\n\t\t\t},\n\t\t\tcsr: &x509.CertificateRequest{\n\t\t\t\tSubject: pkix.Name{\n\t\t\t\t\tCommonName: \"foo.internal\",\n\t\t\t\t},\n\t\t\t\tEmailAddresses: []string{\"test@example.com\"},\n\t\t\t},\n\t\t\twant: []x509util.SubjectAlternativeName{},\n\t\t\terr:  NewError(ErrorBadCSRType, \"Only DNS names and IP addresses are allowed\"),\n\t\t},\n\t\t{\n\t\t\tname: \"fail/error-names-length-mismatch\",\n\t\t\tfields: fields{\n\t\t\t\tIdentifiers: []Identifier{\n\t\t\t\t\t{Type: \"dns\", Value: \"foo.internal\"},\n\t\t\t\t\t{Type: \"dns\", Value: \"bar.internal\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tcsr: &x509.CertificateRequest{\n\t\t\t\tSubject: pkix.Name{\n\t\t\t\t\tCommonName: \"foo.internal\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: []x509util.SubjectAlternativeName{},\n\t\t\terr: NewError(ErrorBadCSRType, \"CSR names do not match identifiers exactly: \"+\n\t\t\t\t\"CSR names = %v, Order names = %v\", []string{\"foo.internal\"}, []string{\"bar.internal\", \"foo.internal\"}),\n\t\t},\n\t\t{\n\t\t\tname: \"fail/error-names-mismatch\",\n\t\t\tfields: fields{\n\t\t\t\tIdentifiers: []Identifier{\n\t\t\t\t\t{Type: \"dns\", Value: \"foo.internal\"},\n\t\t\t\t\t{Type: \"dns\", Value: \"bar.internal\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tcsr: &x509.CertificateRequest{\n\t\t\t\tSubject: pkix.Name{\n\t\t\t\t\tCommonName: \"foo.internal\",\n\t\t\t\t},\n\t\t\t\tDNSNames: []string{\"zap.internal\"},\n\t\t\t},\n\t\t\twant: []x509util.SubjectAlternativeName{},\n\t\t\terr: NewError(ErrorBadCSRType, \"CSR names do not match identifiers exactly: \"+\n\t\t\t\t\"CSR names = %v, Order names = %v\", []string{\"foo.internal\", \"zap.internal\"}, []string{\"bar.internal\", \"foo.internal\"}),\n\t\t},\n\t\t{\n\t\t\tname: \"ok/ipv4\",\n\t\t\tfields: fields{\n\t\t\t\tIdentifiers: []Identifier{\n\t\t\t\t\t{Type: \"ip\", Value: \"192.168.43.42\"},\n\t\t\t\t\t{Type: \"ip\", Value: \"192.168.42.42\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tcsr: &x509.CertificateRequest{\n\t\t\t\tIPAddresses: []net.IP{net.ParseIP(\"192.168.43.42\"), net.ParseIP(\"192.168.42.42\")},\n\t\t\t},\n\t\t\twant: []x509util.SubjectAlternativeName{\n\t\t\t\t{Type: \"ip\", Value: \"192.168.42.42\"},\n\t\t\t\t{Type: \"ip\", Value: \"192.168.43.42\"},\n\t\t\t},\n\t\t\terr: nil,\n\t\t},\n\t\t{\n\t\t\tname: \"ok/ipv6\",\n\t\t\tfields: fields{\n\t\t\t\tIdentifiers: []Identifier{\n\t\t\t\t\t{Type: \"ip\", Value: \"2001:0db8:85a3::8a2e:0370:7335\"},\n\t\t\t\t\t{Type: \"ip\", Value: \"2001:0db8:85a3::8a2e:0370:7334\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tcsr: &x509.CertificateRequest{\n\t\t\t\tIPAddresses: []net.IP{net.ParseIP(\"2001:0db8:85a3:0000:0000:8a2e:0370:7335\"), net.ParseIP(\"2001:0db8:85a3:0000:0000:8a2e:0370:7334\")},\n\t\t\t},\n\t\t\twant: []x509util.SubjectAlternativeName{\n\t\t\t\t{Type: \"ip\", Value: \"2001:db8:85a3::8a2e:370:7334\"},\n\t\t\t\t{Type: \"ip\", Value: \"2001:db8:85a3::8a2e:370:7335\"},\n\t\t\t},\n\t\t\terr: nil,\n\t\t},\n\t\t{\n\t\t\tname: \"fail/error-ips-length-mismatch\",\n\t\t\tfields: fields{\n\t\t\t\tIdentifiers: []Identifier{\n\t\t\t\t\t{Type: \"ip\", Value: \"192.168.42.42\"},\n\t\t\t\t\t{Type: \"ip\", Value: \"192.168.43.42\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tcsr: &x509.CertificateRequest{\n\t\t\t\tIPAddresses: []net.IP{net.ParseIP(\"192.168.42.42\")},\n\t\t\t},\n\t\t\twant: []x509util.SubjectAlternativeName{},\n\t\t\terr: NewError(ErrorBadCSRType, \"CSR IPs do not match identifiers exactly: \"+\n\t\t\t\t\"CSR IPs = %v, Order IPs = %v\", []net.IP{net.ParseIP(\"192.168.42.42\")}, []net.IP{net.ParseIP(\"192.168.42.42\"), net.ParseIP(\"192.168.43.42\")}),\n\t\t},\n\t\t{\n\t\t\tname: \"fail/error-ips-mismatch\",\n\t\t\tfields: fields{\n\t\t\t\tIdentifiers: []Identifier{\n\t\t\t\t\t{Type: \"ip\", Value: \"192.168.42.42\"},\n\t\t\t\t\t{Type: \"ip\", Value: \"192.168.43.42\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tcsr: &x509.CertificateRequest{\n\t\t\t\tIPAddresses: []net.IP{net.ParseIP(\"192.168.42.42\"), net.ParseIP(\"192.168.42.32\")},\n\t\t\t},\n\t\t\twant: []x509util.SubjectAlternativeName{},\n\t\t\terr: NewError(ErrorBadCSRType, \"CSR IPs do not match identifiers exactly: \"+\n\t\t\t\t\"CSR IPs = %v, Order IPs = %v\", []net.IP{net.ParseIP(\"192.168.42.32\"), net.ParseIP(\"192.168.42.42\")}, []net.IP{net.ParseIP(\"192.168.42.42\"), net.ParseIP(\"192.168.43.42\")}),\n\t\t},\n\t\t{\n\t\t\tname: \"ok/mixed\",\n\t\t\tfields: fields{\n\t\t\t\tIdentifiers: []Identifier{\n\t\t\t\t\t{Type: \"dns\", Value: \"foo.internal\"},\n\t\t\t\t\t{Type: \"dns\", Value: \"bar.internal\"},\n\t\t\t\t\t{Type: \"ip\", Value: \"192.168.43.42\"},\n\t\t\t\t\t{Type: \"ip\", Value: \"192.168.42.42\"},\n\t\t\t\t\t{Type: \"ip\", Value: \"2001:0db8:85a3:0000:0000:8a2e:0370:7334\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tcsr: &x509.CertificateRequest{\n\t\t\t\tSubject: pkix.Name{\n\t\t\t\t\tCommonName: \"bar.internal\",\n\t\t\t\t},\n\t\t\t\tDNSNames:    []string{\"foo.internal\"},\n\t\t\t\tIPAddresses: []net.IP{net.ParseIP(\"192.168.43.42\"), net.ParseIP(\"192.168.42.42\"), net.ParseIP(\"2001:0db8:85a3:0000:0000:8a2e:0370:7334\")},\n\t\t\t},\n\t\t\twant: []x509util.SubjectAlternativeName{\n\t\t\t\t{Type: \"dns\", Value: \"bar.internal\"},\n\t\t\t\t{Type: \"dns\", Value: \"foo.internal\"},\n\t\t\t\t{Type: \"ip\", Value: \"192.168.42.42\"},\n\t\t\t\t{Type: \"ip\", Value: \"192.168.43.42\"},\n\t\t\t\t{Type: \"ip\", Value: \"2001:db8:85a3::8a2e:370:7334\"},\n\t\t\t},\n\t\t\terr: nil,\n\t\t},\n\t\t{\n\t\t\tname: \"fail/unsupported-identifier-type\",\n\t\t\tfields: fields{\n\t\t\t\tIdentifiers: []Identifier{\n\t\t\t\t\t{Type: \"ipv4\", Value: \"192.168.42.42\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tcsr: &x509.CertificateRequest{\n\t\t\t\tIPAddresses: []net.IP{net.ParseIP(\"192.168.42.42\")},\n\t\t\t},\n\t\t\twant: []x509util.SubjectAlternativeName{},\n\t\t\terr:  NewError(ErrorServerInternalType, \"unsupported identifier type in order: ipv4\"),\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\to := &Order{\n\t\t\t\tIdentifiers: tt.fields.Identifiers,\n\t\t\t}\n\t\t\tcanonicalizedCSR := canonicalize(tt.csr)\n\t\t\tgot, err := o.sans(canonicalizedCSR)\n\t\t\tif tt.err != nil {\n\t\t\t\tif err == nil {\n\t\t\t\t\tt.Errorf(\"Order.sans() = %v, want error; got none\", got)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tvar k *Error\n\t\t\t\tif errors.As(err, &k) {\n\t\t\t\t\tassert.Equals(t, k.Type, tt.err.Type)\n\t\t\t\t\tassert.Equals(t, k.Detail, tt.err.Detail)\n\t\t\t\t\tassert.Equals(t, k.Status, tt.err.Status)\n\t\t\t\t\tassert.Equals(t, k.Err.Error(), tt.err.Err.Error())\n\t\t\t\t\tassert.Equals(t, k.Detail, tt.err.Detail)\n\t\t\t\t} else {\n\t\t\t\t\tassert.FatalError(t, errors.New(\"unexpected error type\"))\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"Order.sans() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestOrder_getAuthorizationFingerprint(t *testing.T) {\n\tctx := context.Background()\n\ttype fields struct {\n\t\tAuthorizationIDs []string\n\t}\n\ttype args struct {\n\t\tctx context.Context\n\t\tdb  DB\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tfields  fields\n\t\targs    args\n\t\twant    string\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", fields{[]string{\"az1\", \"az2\"}}, args{ctx, &MockDB{\n\t\t\tMockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {\n\t\t\t\treturn &Authorization{ID: id, Status: StatusValid}, nil\n\t\t\t},\n\t\t}}, \"\", false},\n\t\t{\"ok fingerprint\", fields{[]string{\"az1\", \"az2\"}}, args{ctx, &MockDB{\n\t\t\tMockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {\n\t\t\t\tif id == \"az1\" {\n\t\t\t\t\treturn &Authorization{ID: id, Status: StatusValid}, nil\n\t\t\t\t}\n\t\t\t\treturn &Authorization{ID: id, Fingerprint: \"fingerprint\", Status: StatusValid}, nil\n\t\t\t},\n\t\t}}, \"fingerprint\", false},\n\t\t{\"fail\", fields{[]string{\"az1\", \"az2\"}}, args{ctx, &MockDB{\n\t\t\tMockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {\n\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t},\n\t\t}}, \"\", true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\to := &Order{\n\t\t\t\tAuthorizationIDs: tt.fields.AuthorizationIDs,\n\t\t\t}\n\t\t\tgot, err := o.getAuthorizationFingerprint(tt.args.ctx, tt.args.db)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"Order.getAuthorizationFingerprint() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif got != tt.want {\n\t\t\t\tt.Errorf(\"Order.getAuthorizationFingerprint() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "acme/status.go",
    "content": "package acme\n\n// Status represents an ACME status.\ntype Status string\n\nvar (\n\t// StatusValid -- valid\n\tStatusValid = Status(\"valid\")\n\t// StatusInvalid -- invalid\n\tStatusInvalid = Status(\"invalid\")\n\t// StatusPending -- pending; e.g. an Order that is not ready to be finalized.\n\tStatusPending = Status(\"pending\")\n\t// StatusDeactivated -- deactivated; e.g. for an Account that is not longer valid.\n\tStatusDeactivated = Status(\"deactivated\")\n\t// StatusReady -- ready; e.g. for an Order that is ready to be finalized.\n\tStatusReady = Status(\"ready\")\n\t//statusExpired     = \"expired\"\n\t//statusActive      = \"active\"\n\t//statusProcessing  = \"processing\"\n)\n"
  },
  {
    "path": "acme/wire/id.go",
    "content": "package wire\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/url\"\n\t\"strings\"\n)\n\ntype UserID struct {\n\tName   string `json:\"name,omitempty\"`\n\tDomain string `json:\"domain,omitempty\"`\n\tHandle string `json:\"handle,omitempty\"`\n}\n\ntype DeviceID struct {\n\tName     string `json:\"name,omitempty\"`\n\tDomain   string `json:\"domain,omitempty\"`\n\tClientID string `json:\"client-id,omitempty\"`\n\tHandle   string `json:\"handle,omitempty\"`\n}\n\nfunc ParseUserID(value string) (id UserID, err error) {\n\tif err = json.Unmarshal([]byte(value), &id); err != nil {\n\t\treturn\n\t}\n\n\tswitch {\n\tcase id.Handle == \"\":\n\t\terr = errors.New(\"handle must not be empty\")\n\tcase id.Name == \"\":\n\t\terr = errors.New(\"name must not be empty\")\n\tcase id.Domain == \"\":\n\t\terr = errors.New(\"domain must not be empty\")\n\t}\n\n\treturn\n}\n\nfunc ParseDeviceID(value string) (id DeviceID, err error) {\n\tif err = json.Unmarshal([]byte(value), &id); err != nil {\n\t\treturn\n\t}\n\n\tswitch {\n\tcase id.Handle == \"\":\n\t\terr = errors.New(\"handle must not be empty\")\n\tcase id.Name == \"\":\n\t\terr = errors.New(\"name must not be empty\")\n\tcase id.Domain == \"\":\n\t\terr = errors.New(\"domain must not be empty\")\n\tcase id.ClientID == \"\":\n\t\terr = errors.New(\"client-id must not be empty\")\n\t}\n\n\treturn\n}\n\ntype ClientID struct {\n\tScheme   string\n\tUsername string\n\tDeviceID string\n\tDomain   string\n}\n\n// ParseClientID parses a Wire clientID. The ClientID format is as follows:\n//\n//\t\"wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com\",\n//\n// where '!' is used as a separator between the user id & device id.\nfunc ParseClientID(clientID string) (ClientID, error) {\n\tclientIDURI, err := url.Parse(clientID)\n\tif err != nil {\n\t\treturn ClientID{}, fmt.Errorf(\"invalid Wire client ID URI %q: %w\", clientID, err)\n\t}\n\tif clientIDURI.Scheme != \"wireapp\" {\n\t\treturn ClientID{}, fmt.Errorf(\"invalid Wire client ID scheme %q; expected \\\"wireapp\\\"\", clientIDURI.Scheme)\n\t}\n\tfullUsername := clientIDURI.User.Username()\n\tparts := strings.SplitN(fullUsername, \"!\", 2)\n\tif len(parts) != 2 {\n\t\treturn ClientID{}, fmt.Errorf(\"invalid Wire client ID username %q\", fullUsername)\n\t}\n\treturn ClientID{\n\t\tScheme:   clientIDURI.Scheme,\n\t\tUsername: parts[0],\n\t\tDeviceID: parts[1],\n\t\tDomain:   clientIDURI.Host,\n\t}, nil\n}\n"
  },
  {
    "path": "acme/wire/id_test.go",
    "content": "package wire\n\nimport (\n\t\"errors\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestParseUserID(t *testing.T) {\n\tok := `{\"name\": \"Alice Smith\", \"domain\": \"wire.com\", \"handle\": \"wireapp://%40alice_wire@wire.com\"}`\n\tfailJSON := `{\"name\": }`\n\temptyHandle := `{\"name\": \"Alice Smith\", \"domain\": \"wire.com\", \"handle\": \"\"}`\n\temptyName := `{\"name\": \"\", \"domain\": \"wire.com\", \"handle\": \"wireapp://%40alice_wire@wire.com\"}`\n\temptyDomain := `{\"name\": \"Alice Smith\", \"domain\": \"\", \"handle\": \"wireapp://%40alice_wire@wire.com\"}`\n\ttests := []struct {\n\t\tname       string\n\t\tvalue      string\n\t\twantWireID UserID\n\t\twantErr    bool\n\t}{\n\t\t{name: \"ok\", value: ok, wantWireID: UserID{Name: \"Alice Smith\", Domain: \"wire.com\", Handle: \"wireapp://%40alice_wire@wire.com\"}},\n\t\t{name: \"fail/json\", value: failJSON, wantErr: true},\n\t\t{name: \"fail/empty-handle\", value: emptyHandle, wantErr: true},\n\t\t{name: \"fail/empty-name\", value: emptyName, wantErr: true},\n\t\t{name: \"fail/empty-domain\", value: emptyDomain, wantErr: true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgotWireID, err := ParseUserID(tt.value)\n\t\t\tif tt.wantErr {\n\t\t\t\tassert.Error(t, err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, tt.wantWireID, gotWireID)\n\t\t})\n\t}\n}\n\nfunc TestParseDeviceID(t *testing.T) {\n\tok := `{\"name\": \"device\", \"domain\": \"wire.com\", \"client-id\": \"wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com\", \"handle\": \"wireapp://%40alice_wire@wire.com\"}`\n\tfailJSON := `{\"name\": }`\n\temptyHandle := `{\"name\": \"device\", \"domain\": \"wire.com\", \"client-id\": \"wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com\", \"handle\": \"\"}`\n\temptyName := `{\"name\": \"\", \"domain\": \"wire.com\", \"client-id\": \"wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com\", \"handle\": \"wireapp://%40alice_wire@wire.com\"}`\n\temptyDomain := `{\"name\": \"device\", \"domain\": \"\", \"client-id\": \"wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com\", \"handle\": \"wireapp://%40alice_wire@wire.com\"}`\n\temptyClientID := `{\"name\": \"device\", \"domain\": \"wire.com\", \"client-id\": \"\", \"handle\": \"wireapp://%40alice_wire@wire.com\"}`\n\ttests := []struct {\n\t\tname       string\n\t\tvalue      string\n\t\twantWireID DeviceID\n\t\twantErr    bool\n\t}{\n\t\t{name: \"ok\", value: ok, wantWireID: DeviceID{Name: \"device\", Domain: \"wire.com\", ClientID: \"wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com\", Handle: \"wireapp://%40alice_wire@wire.com\"}},\n\t\t{name: \"fail/json\", value: failJSON, wantErr: true},\n\t\t{name: \"fail/empty-handle\", value: emptyHandle, wantErr: true},\n\t\t{name: \"fail/empty-name\", value: emptyName, wantErr: true},\n\t\t{name: \"fail/empty-domain\", value: emptyDomain, wantErr: true},\n\t\t{name: \"fail/empty-client-id\", value: emptyClientID, wantErr: true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgotWireID, err := ParseDeviceID(tt.value)\n\t\t\tif tt.wantErr {\n\t\t\t\tassert.Error(t, err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, tt.wantWireID, gotWireID)\n\t\t})\n\t}\n}\n\nfunc TestParseClientID(t *testing.T) {\n\ttests := []struct {\n\t\tname        string\n\t\tclientID    string\n\t\twant        ClientID\n\t\texpectedErr error\n\t}{\n\t\t{name: \"ok\", clientID: \"wireapp://CzbfFjDOQrenCbDxVmgnFw!594930e9d50bb175@wire.com\", want: ClientID{Scheme: \"wireapp\", Username: \"CzbfFjDOQrenCbDxVmgnFw\", DeviceID: \"594930e9d50bb175\", Domain: \"wire.com\"}},\n\t\t{name: \"fail/uri\", clientID: \"bla\", expectedErr: errors.New(`invalid Wire client ID scheme \"\"; expected \"wireapp\"`)},\n\t\t{name: \"fail/scheme\", clientID: \"not-wireapp://bla.com\", expectedErr: errors.New(`invalid Wire client ID scheme \"not-wireapp\"; expected \"wireapp\"`)},\n\t\t{name: \"fail/username\", clientID: \"wireapp://user@wire.com\", expectedErr: errors.New(`invalid Wire client ID username \"user\"`)},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := ParseClientID(tt.clientID)\n\t\t\tif tt.expectedErr != nil {\n\t\t\t\tassert.EqualError(t, err, tt.expectedErr.Error())\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, tt.want, got)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "api/api.go",
    "content": "package api\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto\"\n\t\"crypto/dsa\" //nolint:staticcheck // support legacy algorithms\n\t\"crypto/ecdsa\"\n\t\"crypto/ed25519\"\n\t\"crypto/rsa\"\n\t\"crypto/x509\"\n\t\"encoding/asn1\"\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"encoding/pem\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go-chi/chi/v5\"\n\t\"github.com/pkg/errors\"\n\t\"go.step.sm/crypto/sshutil\"\n\t\"golang.org/x/crypto/ssh\"\n\n\t\"github.com/smallstep/certificates/api/log\"\n\t\"github.com/smallstep/certificates/api/models\"\n\t\"github.com/smallstep/certificates/api/render\"\n\t\"github.com/smallstep/certificates/authority\"\n\t\"github.com/smallstep/certificates/authority/config\"\n\t\"github.com/smallstep/certificates/authority/provisioner\"\n\t\"github.com/smallstep/certificates/errs\"\n\t\"github.com/smallstep/certificates/internal/cast\"\n\t\"github.com/smallstep/certificates/logging\"\n)\n\n// Authority is the interface implemented by a CA authority.\ntype Authority interface {\n\tSSHAuthority\n\t// context specifies the Authorize[Sign|Revoke|etc.] method.\n\tAuthorize(ctx context.Context, ott string) ([]provisioner.SignOption, error)\n\tAuthorizeRenewToken(ctx context.Context, ott string) (*x509.Certificate, error)\n\tGetTLSOptions() *config.TLSOptions\n\tRoot(shasum string) (*x509.Certificate, error)\n\tSignWithContext(ctx context.Context, cr *x509.CertificateRequest, opts provisioner.SignOptions, signOpts ...provisioner.SignOption) ([]*x509.Certificate, error)\n\tRenew(peer *x509.Certificate) ([]*x509.Certificate, error)\n\tRenewContext(ctx context.Context, peer *x509.Certificate, pk crypto.PublicKey) ([]*x509.Certificate, error)\n\tRekey(peer *x509.Certificate, pk crypto.PublicKey) ([]*x509.Certificate, error)\n\tLoadProvisionerByCertificate(*x509.Certificate) (provisioner.Interface, error)\n\tLoadProvisionerByName(string) (provisioner.Interface, error)\n\tGetProvisioners(cursor string, limit int) (provisioner.List, string, error)\n\tRevoke(context.Context, *authority.RevokeOptions) error\n\tGetEncryptedKey(kid string) (string, error)\n\tGetRoots() ([]*x509.Certificate, error)\n\tGetIntermediateCertificates() []*x509.Certificate\n\tGetFederation() ([]*x509.Certificate, error)\n\tVersion() authority.Version\n\tGetCertificateRevocationList() (*authority.CertificateRevocationListInfo, error)\n}\n\n// mustAuthority will be replaced on unit tests.\nvar mustAuthority = func(ctx context.Context) Authority {\n\treturn authority.MustFromContext(ctx)\n}\n\n// TimeDuration is an alias of provisioner.TimeDuration\ntype TimeDuration = provisioner.TimeDuration\n\n// NewTimeDuration returns a TimeDuration with the defined time.\nfunc NewTimeDuration(t time.Time) TimeDuration {\n\treturn provisioner.NewTimeDuration(t)\n}\n\n// ParseTimeDuration returns a new TimeDuration parsing the RFC 3339 time or\n// time.Duration string.\nfunc ParseTimeDuration(s string) (TimeDuration, error) {\n\treturn provisioner.ParseTimeDuration(s)\n}\n\n// Certificate wraps a *x509.Certificate and adds the json.Marshaler interface.\ntype Certificate struct {\n\t*x509.Certificate\n}\n\n// NewCertificate is a helper method that returns a Certificate from a\n// *x509.Certificate.\nfunc NewCertificate(cr *x509.Certificate) Certificate {\n\treturn Certificate{\n\t\tCertificate: cr,\n\t}\n}\n\n// reset sets the inner x509.CertificateRequest to nil\nfunc (c *Certificate) reset() {\n\tif c != nil {\n\t\tc.Certificate = nil\n\t}\n}\n\n// MarshalJSON implements the json.Marshaler interface. The certificate is\n// quoted string using the PEM encoding.\nfunc (c Certificate) MarshalJSON() ([]byte, error) {\n\tif c.Certificate == nil {\n\t\treturn []byte(\"null\"), nil\n\t}\n\tblock := pem.EncodeToMemory(&pem.Block{\n\t\tType:  \"CERTIFICATE\",\n\t\tBytes: c.Raw,\n\t})\n\treturn json.Marshal(string(block))\n}\n\n// UnmarshalJSON implements the json.Unmarshaler interface. The certificate is\n// expected to be a quoted string using the PEM encoding.\nfunc (c *Certificate) UnmarshalJSON(data []byte) error {\n\tvar s string\n\tif err := json.Unmarshal(data, &s); err != nil {\n\t\treturn errors.Wrap(err, \"error decoding certificate\")\n\t}\n\n\t// Make sure the inner x509.Certificate is nil\n\tif s == \"null\" || s == \"\" {\n\t\tc.reset()\n\t\treturn nil\n\t}\n\n\tblock, _ := pem.Decode([]byte(s))\n\tif block == nil {\n\t\treturn errors.New(\"error decoding certificate\")\n\t}\n\tcert, err := x509.ParseCertificate(block.Bytes)\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"error decoding certificate\")\n\t}\n\tc.Certificate = cert\n\treturn nil\n}\n\n// CertificateRequest wraps a *x509.CertificateRequest and adds the\n// json.Unmarshaler interface.\ntype CertificateRequest struct {\n\t*x509.CertificateRequest\n}\n\n// NewCertificateRequest is a helper method that returns a CertificateRequest\n// from a *x509.CertificateRequest.\nfunc NewCertificateRequest(cr *x509.CertificateRequest) CertificateRequest {\n\treturn CertificateRequest{\n\t\tCertificateRequest: cr,\n\t}\n}\n\n// reset sets the inner x509.CertificateRequest to nil\nfunc (c *CertificateRequest) reset() {\n\tif c != nil {\n\t\tc.CertificateRequest = nil\n\t}\n}\n\n// MarshalJSON implements the json.Marshaler interface. The certificate request\n// is a quoted string using the PEM encoding.\nfunc (c CertificateRequest) MarshalJSON() ([]byte, error) {\n\tif c.CertificateRequest == nil {\n\t\treturn []byte(\"null\"), nil\n\t}\n\tblock := pem.EncodeToMemory(&pem.Block{\n\t\tType:  \"CERTIFICATE REQUEST\",\n\t\tBytes: c.Raw,\n\t})\n\treturn json.Marshal(string(block))\n}\n\n// UnmarshalJSON implements the json.Unmarshaler interface. The certificate\n// request is expected to be a quoted string using the PEM encoding.\nfunc (c *CertificateRequest) UnmarshalJSON(data []byte) error {\n\tvar s string\n\tif err := json.Unmarshal(data, &s); err != nil {\n\t\treturn errors.Wrap(err, \"error decoding csr\")\n\t}\n\n\t// Make sure the inner x509.CertificateRequest is nil\n\tif s == \"null\" || s == \"\" {\n\t\tc.reset()\n\t\treturn nil\n\t}\n\n\tblock, _ := pem.Decode([]byte(s))\n\tif block == nil {\n\t\treturn errors.New(\"error decoding csr\")\n\t}\n\tcr, err := x509.ParseCertificateRequest(block.Bytes)\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"error decoding csr\")\n\t}\n\tc.CertificateRequest = cr\n\treturn nil\n}\n\n// Router defines a common router interface.\ntype Router interface {\n\t// MethodFunc adds routes for `pattern` that matches\n\t// the `method` HTTP method.\n\tMethodFunc(method, pattern string, h http.HandlerFunc)\n}\n\n// RouterHandler is the interface that a HTTP handler that manages multiple\n// endpoints will implement.\ntype RouterHandler interface {\n\tRoute(r Router)\n}\n\n// VersionResponse is the response object that returns the version of the\n// server.\ntype VersionResponse struct {\n\tVersion                     string `json:\"version\"`\n\tRequireClientAuthentication bool   `json:\"requireClientAuthentication,omitempty\"`\n}\n\n// HealthResponse is the response object that returns the health of the server.\ntype HealthResponse struct {\n\tStatus string `json:\"status\"`\n}\n\n// RootResponse is the response object that returns the PEM of a root certificate.\ntype RootResponse struct {\n\tRootPEM Certificate `json:\"ca\"`\n}\n\n// ProvisionersResponse is the response object that returns the list of\n// provisioners.\ntype ProvisionersResponse struct {\n\tProvisioners provisioner.List\n\tNextCursor   string\n}\n\nconst redacted = \"*** REDACTED ***\"\n\nfunc scepFromProvisioner(p *provisioner.SCEP) *models.SCEP {\n\treturn &models.SCEP{\n\t\tID:                            p.ID,\n\t\tType:                          p.Type,\n\t\tName:                          p.Name,\n\t\tForceCN:                       p.ForceCN,\n\t\tChallengePassword:             redacted,\n\t\tCapabilities:                  p.Capabilities,\n\t\tIncludeRoot:                   p.IncludeRoot,\n\t\tExcludeIntermediate:           p.ExcludeIntermediate,\n\t\tMinimumPublicKeyLength:        p.MinimumPublicKeyLength,\n\t\tDecrypterCertificate:          []byte(redacted),\n\t\tDecrypterKeyPEM:               []byte(redacted),\n\t\tDecrypterKeyURI:               redacted,\n\t\tDecrypterKeyPassword:          redacted,\n\t\tEncryptionAlgorithmIdentifier: p.EncryptionAlgorithmIdentifier,\n\t\tOptions:                       p.Options,\n\t\tClaims:                        p.Claims,\n\t}\n}\n\n// MarshalJSON implements json.Marshaler. It marshals the ProvisionersResponse\n// into a byte slice.\n//\n// Special treatment is given to the SCEP provisioner, as it contains a\n// challenge secret that MUST NOT be leaked in (public) HTTP responses. The\n// challenge value is thus redacted in HTTP responses.\nfunc (p ProvisionersResponse) MarshalJSON() ([]byte, error) {\n\tvar responseProvisioners provisioner.List\n\tfor _, item := range p.Provisioners {\n\t\tscepProv, ok := item.(*provisioner.SCEP)\n\t\tif !ok {\n\t\t\tresponseProvisioners = append(responseProvisioners, item)\n\t\t\tcontinue\n\t\t}\n\n\t\tresponseProvisioners = append(responseProvisioners, scepFromProvisioner(scepProv))\n\t}\n\n\tvar list = struct {\n\t\tProvisioners []provisioner.Interface `json:\"provisioners\"`\n\t\tNextCursor   string                  `json:\"nextCursor\"`\n\t}{\n\t\tProvisioners: []provisioner.Interface(responseProvisioners),\n\t\tNextCursor:   p.NextCursor,\n\t}\n\n\treturn json.Marshal(list)\n}\n\n// ProvisionerKeyResponse is the response object that returns the encrypted key\n// of a provisioner.\ntype ProvisionerKeyResponse struct {\n\tKey string `json:\"key\"`\n}\n\n// RootsResponse is the response object of the roots request.\ntype RootsResponse struct {\n\tCertificates []Certificate `json:\"crts\"`\n}\n\n// IntermediatesResponse is the response object of the intermediates request.\ntype IntermediatesResponse struct {\n\tCertificates []Certificate `json:\"crts\"`\n}\n\n// FederationResponse is the response object of the federation request.\ntype FederationResponse struct {\n\tCertificates []Certificate `json:\"crts\"`\n}\n\n// caHandler is the type used to implement the different CA HTTP endpoints.\ntype caHandler struct {\n\tAuthority Authority\n}\n\n// Route configures the http request router.\nfunc (h *caHandler) Route(r Router) {\n\tRoute(r)\n}\n\n// New creates a new RouterHandler with the CA endpoints.\n//\n// Deprecated: Use api.Route(r Router)\nfunc New(Authority) RouterHandler {\n\treturn &caHandler{}\n}\n\nfunc Route(r Router) {\n\tr.MethodFunc(\"GET\", \"/version\", Version)\n\tr.MethodFunc(\"GET\", \"/health\", Health)\n\tr.MethodFunc(\"GET\", \"/root/{sha}\", Root)\n\tr.MethodFunc(\"POST\", \"/sign\", Sign)\n\tr.MethodFunc(\"POST\", \"/renew\", Renew)\n\tr.MethodFunc(\"POST\", \"/rekey\", Rekey)\n\tr.MethodFunc(\"POST\", \"/revoke\", Revoke)\n\tr.MethodFunc(\"GET\", \"/crl\", CRL)\n\tr.MethodFunc(\"GET\", \"/provisioners\", Provisioners)\n\tr.MethodFunc(\"GET\", \"/provisioners/{kid}/encrypted-key\", ProvisionerKey)\n\tr.MethodFunc(\"GET\", \"/roots\", Roots)\n\tr.MethodFunc(\"GET\", \"/roots.pem\", RootsPEM)\n\tr.MethodFunc(\"GET\", \"/intermediates\", Intermediates)\n\tr.MethodFunc(\"GET\", \"/intermediates.pem\", IntermediatesPEM)\n\tr.MethodFunc(\"GET\", \"/federation\", Federation)\n\n\t// SSH CA\n\tr.MethodFunc(\"POST\", \"/ssh/sign\", SSHSign)\n\tr.MethodFunc(\"POST\", \"/ssh/renew\", SSHRenew)\n\tr.MethodFunc(\"POST\", \"/ssh/revoke\", SSHRevoke)\n\tr.MethodFunc(\"POST\", \"/ssh/rekey\", SSHRekey)\n\tr.MethodFunc(\"GET\", \"/ssh/roots\", SSHRoots)\n\tr.MethodFunc(\"GET\", \"/ssh/federation\", SSHFederation)\n\tr.MethodFunc(\"POST\", \"/ssh/config\", SSHConfig)\n\tr.MethodFunc(\"POST\", \"/ssh/config/{type}\", SSHConfig)\n\tr.MethodFunc(\"POST\", \"/ssh/check-host\", SSHCheckHost)\n\tr.MethodFunc(\"GET\", \"/ssh/hosts\", SSHGetHosts)\n\tr.MethodFunc(\"POST\", \"/ssh/bastion\", SSHBastion)\n\n\t// For compatibility with old code:\n\tr.MethodFunc(\"POST\", \"/re-sign\", Renew)\n\tr.MethodFunc(\"POST\", \"/sign-ssh\", SSHSign)\n\tr.MethodFunc(\"GET\", \"/ssh/get-hosts\", SSHGetHosts)\n}\n\n// Version is an HTTP handler that returns the version of the server.\nfunc Version(w http.ResponseWriter, r *http.Request) {\n\tv := mustAuthority(r.Context()).Version()\n\trender.JSON(w, r, VersionResponse{\n\t\tVersion:                     v.Version,\n\t\tRequireClientAuthentication: v.RequireClientAuthentication,\n\t})\n}\n\n// Health is an HTTP handler that returns the status of the server.\nfunc Health(w http.ResponseWriter, r *http.Request) {\n\trender.JSON(w, r, HealthResponse{Status: \"ok\"})\n}\n\n// Root is an HTTP handler that using the SHA256 from the URL, returns the root\n// certificate for the given SHA256.\nfunc Root(w http.ResponseWriter, r *http.Request) {\n\tsha := chi.URLParam(r, \"sha\")\n\tsum := strings.ToLower(strings.ReplaceAll(sha, \"-\", \"\"))\n\t// Load root certificate with the\n\tcert, err := mustAuthority(r.Context()).Root(sum)\n\tif err != nil {\n\t\trender.Error(w, r, errs.NotFoundErr(err, errs.WithMessage(\"root certificate with fingerprint %q was not found\", sum)))\n\t\treturn\n\t}\n\n\trender.JSON(w, r, &RootResponse{RootPEM: Certificate{cert}})\n}\n\nfunc certChainToPEM(certChain []*x509.Certificate) []Certificate {\n\tcertChainPEM := make([]Certificate, 0, len(certChain))\n\tfor _, c := range certChain {\n\t\tcertChainPEM = append(certChainPEM, Certificate{c})\n\t}\n\treturn certChainPEM\n}\n\n// Provisioners returns the list of provisioners configured in the authority.\nfunc Provisioners(w http.ResponseWriter, r *http.Request) {\n\tcursor, limit, err := ParseCursor(r)\n\tif err != nil {\n\t\trender.Error(w, r, err)\n\t\treturn\n\t}\n\n\tp, next, err := mustAuthority(r.Context()).GetProvisioners(cursor, limit)\n\tif err != nil {\n\t\trender.Error(w, r, errs.InternalServerErr(err))\n\t\treturn\n\t}\n\n\trender.JSON(w, r, &ProvisionersResponse{\n\t\tProvisioners: p,\n\t\tNextCursor:   next,\n\t})\n}\n\n// ProvisionerKey returns the encrypted key of a provisioner by it's key id.\nfunc ProvisionerKey(w http.ResponseWriter, r *http.Request) {\n\tkid := chi.URLParam(r, \"kid\")\n\tkey, err := mustAuthority(r.Context()).GetEncryptedKey(kid)\n\tif err != nil {\n\t\trender.Error(w, r, errs.NotFoundErr(err))\n\t\treturn\n\t}\n\n\trender.JSON(w, r, &ProvisionerKeyResponse{key})\n}\n\n// Roots returns all the root certificates for the CA.\nfunc Roots(w http.ResponseWriter, r *http.Request) {\n\troots, err := mustAuthority(r.Context()).GetRoots()\n\tif err != nil {\n\t\trender.Error(w, r, errs.ForbiddenErr(err, \"error getting roots\"))\n\t\treturn\n\t}\n\n\tcerts := make([]Certificate, len(roots))\n\tfor i := range roots {\n\t\tcerts[i] = Certificate{roots[i]}\n\t}\n\n\trender.JSONStatus(w, r, &RootsResponse{\n\t\tCertificates: certs,\n\t}, http.StatusCreated)\n}\n\n// RootsPEM returns all the root certificates for the CA in PEM format.\nfunc RootsPEM(w http.ResponseWriter, r *http.Request) {\n\troots, err := mustAuthority(r.Context()).GetRoots()\n\tif err != nil {\n\t\trender.Error(w, r, errs.InternalServerErr(err))\n\t\treturn\n\t}\n\n\tw.Header().Set(\"Content-Type\", \"application/x-pem-file\")\n\n\tfor _, root := range roots {\n\t\tblock := pem.EncodeToMemory(&pem.Block{\n\t\t\tType:  \"CERTIFICATE\",\n\t\t\tBytes: root.Raw,\n\t\t})\n\n\t\tif _, err := w.Write(block); err != nil {\n\t\t\tlog.Error(w, r, err)\n\t\t\treturn\n\t\t}\n\t}\n}\n\n// Intermediates returns all the intermediate certificates of the CA.\nfunc Intermediates(w http.ResponseWriter, r *http.Request) {\n\tintermediates := mustAuthority(r.Context()).GetIntermediateCertificates()\n\tif len(intermediates) == 0 {\n\t\trender.Error(w, r, errs.NotImplemented(\"error getting intermediates: method not implemented\"))\n\t\treturn\n\t}\n\n\tcerts := make([]Certificate, len(intermediates))\n\tfor i := range intermediates {\n\t\tcerts[i] = Certificate{intermediates[i]}\n\t}\n\n\trender.JSONStatus(w, r, &IntermediatesResponse{\n\t\tCertificates: certs,\n\t}, http.StatusCreated)\n}\n\n// IntermediatesPEM returns all the intermediate certificates for the CA in PEM format.\nfunc IntermediatesPEM(w http.ResponseWriter, r *http.Request) {\n\tintermediates := mustAuthority(r.Context()).GetIntermediateCertificates()\n\tif len(intermediates) == 0 {\n\t\trender.Error(w, r, errs.NotImplemented(\"error getting intermediates: method not implemented\"))\n\t\treturn\n\t}\n\n\tw.Header().Set(\"Content-Type\", \"application/x-pem-file\")\n\n\tfor _, crt := range intermediates {\n\t\tblock := pem.EncodeToMemory(&pem.Block{\n\t\t\tType:  \"CERTIFICATE\",\n\t\t\tBytes: crt.Raw,\n\t\t})\n\n\t\tif _, err := w.Write(block); err != nil {\n\t\t\tlog.Error(w, r, err)\n\t\t\treturn\n\t\t}\n\t}\n}\n\n// Federation returns all the public certificates in the federation.\nfunc Federation(w http.ResponseWriter, r *http.Request) {\n\tfederated, err := mustAuthority(r.Context()).GetFederation()\n\tif err != nil {\n\t\trender.Error(w, r, errs.ForbiddenErr(err, \"error getting federated roots\"))\n\t\treturn\n\t}\n\n\tcerts := make([]Certificate, len(federated))\n\tfor i := range federated {\n\t\tcerts[i] = Certificate{federated[i]}\n\t}\n\n\trender.JSONStatus(w, r, &FederationResponse{\n\t\tCertificates: certs,\n\t}, http.StatusCreated)\n}\n\nvar oidStepProvisioner = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 37476, 9000, 64, 1}\n\ntype stepProvisioner struct {\n\tType         int\n\tName         []byte\n\tCredentialID []byte\n}\n\nfunc logOtt(w http.ResponseWriter, token string) {\n\tif rl, ok := w.(logging.ResponseLogger); ok {\n\t\trl.WithFields(map[string]interface{}{\n\t\t\t\"ott\": token,\n\t\t})\n\t}\n}\n\n// LogCertificate adds certificate fields to the log message.\nfunc LogCertificate(w http.ResponseWriter, cert *x509.Certificate) {\n\tif rl, ok := w.(logging.ResponseLogger); ok {\n\t\tm := map[string]interface{}{\n\t\t\t\"serial\":      cert.SerialNumber.String(),\n\t\t\t\"subject\":     cert.Subject.CommonName,\n\t\t\t\"issuer\":      cert.Issuer.CommonName,\n\t\t\t\"sans\":        fmtSans(cert),\n\t\t\t\"valid-from\":  cert.NotBefore.Format(time.RFC3339),\n\t\t\t\"valid-to\":    cert.NotAfter.Format(time.RFC3339),\n\t\t\t\"public-key\":  fmtPublicKey(cert),\n\t\t\t\"certificate\": base64.StdEncoding.EncodeToString(cert.Raw),\n\t\t}\n\t\tfor _, ext := range cert.Extensions {\n\t\t\tif !ext.Id.Equal(oidStepProvisioner) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tval := &stepProvisioner{}\n\t\t\trest, err := asn1.Unmarshal(ext.Value, val)\n\t\t\tif err != nil || len(rest) > 0 {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tif len(val.CredentialID) > 0 {\n\t\t\t\tm[\"provisioner\"] = fmt.Sprintf(\"%s (%s)\", val.Name, val.CredentialID)\n\t\t\t} else {\n\t\t\t\tm[\"provisioner\"] = string(val.Name)\n\t\t\t}\n\t\t\tbreak\n\t\t}\n\t\trl.WithFields(m)\n\t}\n}\n\n// LogSSHCertificate adds SSH certificate fields to the log message.\nfunc LogSSHCertificate(w http.ResponseWriter, cert *ssh.Certificate) {\n\tif rl, ok := w.(logging.ResponseLogger); ok {\n\t\tmak := bytes.TrimSpace(ssh.MarshalAuthorizedKey(cert))\n\t\tvar certificate string\n\t\tparts := strings.Split(string(mak), \" \")\n\t\tif len(parts) > 1 {\n\t\t\tcertificate = parts[1]\n\t\t}\n\t\tvar userOrHost string\n\t\tif cert.CertType == ssh.HostCert {\n\t\t\tuserOrHost = \"host\"\n\t\t} else {\n\t\t\tuserOrHost = \"user\"\n\t\t}\n\t\tcertificateType := fmt.Sprintf(\"%s %s certificate\", parts[0], userOrHost) // e.g. ecdsa-sha2-nistp256-cert-v01@openssh.com user certificate\n\t\tm := map[string]interface{}{\n\t\t\t\"serial\":           cert.Serial,\n\t\t\t\"principals\":       cert.ValidPrincipals,\n\t\t\t\"valid-from\":       time.Unix(cast.Int64(cert.ValidAfter), 0).Format(time.RFC3339),\n\t\t\t\"valid-to\":         time.Unix(cast.Int64(cert.ValidBefore), 0).Format(time.RFC3339),\n\t\t\t\"certificate\":      certificate,\n\t\t\t\"certificate-type\": certificateType,\n\t\t}\n\t\tfingerprint, err := sshutil.FormatFingerprint(mak, sshutil.DefaultFingerprint)\n\t\tif err == nil {\n\t\t\tfpParts := strings.Split(fingerprint, \" \")\n\t\t\tif len(fpParts) > 3 {\n\t\t\t\tm[\"public-key\"] = fmt.Sprintf(\"%s %s\", fpParts[1], fpParts[len(fpParts)-1])\n\t\t\t}\n\t\t}\n\t\trl.WithFields(m)\n\t}\n}\n\n// ParseCursor parses the cursor and limit from the request query params.\nfunc ParseCursor(r *http.Request) (cursor string, limit int, err error) {\n\tq := r.URL.Query()\n\tcursor = q.Get(\"cursor\")\n\tif v := q.Get(\"limit\"); v != \"\" {\n\t\tlimit, err = strconv.Atoi(v)\n\t\tif err != nil {\n\t\t\treturn \"\", 0, errs.BadRequestErr(err, \"limit '%s' is not an integer\", v)\n\t\t}\n\t}\n\treturn\n}\n\nfunc fmtSans(cert *x509.Certificate) map[string][]string {\n\tsans := make(map[string][]string)\n\tif len(cert.DNSNames) > 0 {\n\t\tsans[\"dns\"] = cert.DNSNames\n\t}\n\tif len(cert.EmailAddresses) > 0 {\n\t\tsans[\"email\"] = cert.EmailAddresses\n\t}\n\tif size := len(cert.IPAddresses); size > 0 {\n\t\tips := make([]string, size)\n\t\tfor i, ip := range cert.IPAddresses {\n\t\t\tips[i] = ip.String()\n\t\t}\n\t\tsans[\"ip\"] = ips\n\t}\n\tif size := len(cert.URIs); size > 0 {\n\t\turis := make([]string, size)\n\t\tfor i, u := range cert.URIs {\n\t\t\turis[i] = u.String()\n\t\t}\n\t\tsans[\"uri\"] = uris\n\t}\n\treturn sans\n}\n\nfunc fmtPublicKey(cert *x509.Certificate) string {\n\tvar params string\n\tswitch pk := cert.PublicKey.(type) {\n\tcase *ecdsa.PublicKey:\n\t\tparams = pk.Curve.Params().Name\n\tcase *rsa.PublicKey:\n\t\tparams = strconv.Itoa(pk.Size() * 8)\n\tcase ed25519.PublicKey:\n\t\treturn cert.PublicKeyAlgorithm.String()\n\tcase *dsa.PublicKey:\n\t\tparams = strconv.Itoa(pk.Q.BitLen() * 8)\n\tdefault:\n\t\tparams = \"unknown\"\n\t}\n\treturn fmt.Sprintf(\"%s %s\", cert.PublicKeyAlgorithm, params)\n}\n"
  },
  {
    "path": "api/api_test.go",
    "content": "package api\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto\"\n\t\"crypto/dsa\" //nolint:staticcheck // support legacy algorithms\n\t\"crypto/ecdsa\"\n\t\"crypto/ed25519\"\n\t\"crypto/elliptic\"\n\t\"crypto/rand\"\n\t\"crypto/rsa\"\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"crypto/x509/pkix\"\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"encoding/pem\"\n\t\"fmt\"\n\t\"io\"\n\t\"math/big\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"reflect\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-chi/chi/v5\"\n\t\"github.com/pkg/errors\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"go.step.sm/crypto/jose\"\n\t\"go.step.sm/crypto/minica\"\n\t\"go.step.sm/crypto/x509util\"\n\t\"golang.org/x/crypto/ssh\"\n\n\t\"github.com/smallstep/certificates/authority\"\n\t\"github.com/smallstep/certificates/authority/provisioner\"\n\t\"github.com/smallstep/certificates/errs\"\n\t\"github.com/smallstep/certificates/logging\"\n\t\"github.com/smallstep/certificates/templates\"\n)\n\nconst (\n\trootPEM = `-----BEGIN CERTIFICATE-----\nMIIEBDCCAuygAwIBAgIDAjppMA0GCSqGSIb3DQEBBQUAMEIxCzAJBgNVBAYTAlVT\nMRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMRswGQYDVQQDExJHZW9UcnVzdCBHbG9i\nYWwgQ0EwHhcNMTMwNDA1MTUxNTU1WhcNMTUwNDA0MTUxNTU1WjBJMQswCQYDVQQG\nEwJVUzETMBEGA1UEChMKR29vZ2xlIEluYzElMCMGA1UEAxMcR29vZ2xlIEludGVy\nbmV0IEF1dGhvcml0eSBHMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB\nAJwqBHdc2FCROgajguDYUEi8iT/xGXAaiEZ+4I/F8YnOIe5a/mENtzJEiaB0C1NP\nVaTOgmKV7utZX8bhBYASxF6UP7xbSDj0U/ck5vuR6RXEz/RTDfRK/J9U3n2+oGtv\nh8DQUB8oMANA2ghzUWx//zo8pzcGjr1LEQTrfSTe5vn8MXH7lNVg8y5Kr0LSy+rE\nahqyzFPdFUuLH8gZYR/Nnag+YyuENWllhMgZxUYi+FOVvuOAShDGKuy6lyARxzmZ\nEASg8GF6lSWMTlJ14rbtCMoU/M4iarNOz0YDl5cDfsCx3nuvRTPPuj5xt970JSXC\nDTWJnZ37DhF5iR43xa+OcmkCAwEAAaOB+zCB+DAfBgNVHSMEGDAWgBTAephojYn7\nqwVkDBF9qn1luMrMTjAdBgNVHQ4EFgQUSt0GFhu89mi1dvWBtrtiGrpagS8wEgYD\nVR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAQYwOgYDVR0fBDMwMTAvoC2g\nK4YpaHR0cDovL2NybC5nZW90cnVzdC5jb20vY3Jscy9ndGdsb2JhbC5jcmwwPQYI\nKwYBBQUHAQEEMTAvMC0GCCsGAQUFBzABhiFodHRwOi8vZ3RnbG9iYWwtb2NzcC5n\nZW90cnVzdC5jb20wFwYDVR0gBBAwDjAMBgorBgEEAdZ5AgUBMA0GCSqGSIb3DQEB\nBQUAA4IBAQA21waAESetKhSbOHezI6B1WLuxfoNCunLaHtiONgaX4PCVOzf9G0JY\n/iLIa704XtE7JW4S615ndkZAkNoUyHgN7ZVm2o6Gb4ChulYylYbc3GrKBIxbf/a/\nzG+FA1jDaFETzf3I93k9mTXwVqO94FntT0QJo544evZG0R0SnU++0ED8Vf4GXjza\nHFa9llF7b1cq26KqltyMdMKVvvBulRP/F/A8rLIQjcxz++iPAsbw+zOzlTvjwsto\nWHPbqCRiOwY1nQ2pM714A5AuTHhdUDqB1O6gyHA43LL5Z/qHQF1hwFGPa4NrzQU6\nyuGnBXj8ytqU0CwIPX4WecigUCAkVDNx\n-----END CERTIFICATE-----`\n\n\tcertPEM = `-----BEGIN CERTIFICATE-----\nMIIDujCCAqKgAwIBAgIIE31FZVaPXTUwDQYJKoZIhvcNAQEFBQAwSTELMAkGA1UE\nBhMCVVMxEzARBgNVBAoTCkdvb2dsZSBJbmMxJTAjBgNVBAMTHEdvb2dsZSBJbnRl\ncm5ldCBBdXRob3JpdHkgRzIwHhcNMTQwMTI5MTMyNzQzWhcNMTQwNTI5MDAwMDAw\nWjBpMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwN\nTW91bnRhaW4gVmlldzETMBEGA1UECgwKR29vZ2xlIEluYzEYMBYGA1UEAwwPbWFp\nbC5nb29nbGUuY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEfRrObuSW5T7q\n5CnSEqefEmtH4CCv6+5EckuriNr1CjfVvqzwfAhopXkLrq45EQm8vkmf7W96XJhC\n7ZM0dYi1/qOCAU8wggFLMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAa\nBgNVHREEEzARgg9tYWlsLmdvb2dsZS5jb20wCwYDVR0PBAQDAgeAMGgGCCsGAQUF\nBwEBBFwwWjArBggrBgEFBQcwAoYfaHR0cDovL3BraS5nb29nbGUuY29tL0dJQUcy\nLmNydDArBggrBgEFBQcwAYYfaHR0cDovL2NsaWVudHMxLmdvb2dsZS5jb20vb2Nz\ncDAdBgNVHQ4EFgQUiJxtimAuTfwb+aUtBn5UYKreKvMwDAYDVR0TAQH/BAIwADAf\nBgNVHSMEGDAWgBRK3QYWG7z2aLV29YG2u2IaulqBLzAXBgNVHSAEEDAOMAwGCisG\nAQQB1nkCBQEwMAYDVR0fBCkwJzAloCOgIYYfaHR0cDovL3BraS5nb29nbGUuY29t\nL0dJQUcyLmNybDANBgkqhkiG9w0BAQUFAAOCAQEAH6RYHxHdcGpMpFE3oxDoFnP+\ngtuBCHan2yE2GRbJ2Cw8Lw0MmuKqHlf9RSeYfd3BXeKkj1qO6TVKwCh+0HdZk283\nTZZyzmEOyclm3UGFYe82P/iDFt+CeQ3NpmBg+GoaVCuWAARJN/KfglbLyyYygcQq\n0SgeDh8dRKUiaW3HQSoYvTvdTuqzwK4CXsr3b5/dAOY8uMuG/IAR3FgwTbZ1dtoW\nRvOTa8hYiU6A475WuZKyEHcwnGYe57u2I2KbMgcKjPniocj4QzgYsVAVKW3IwaOh\nyE+vPxsiUkvQHdO2fojCkY8jg70jxM+gu59tPDNbw3Uh/2Ij310FgTHsnGQMyA==\n-----END CERTIFICATE-----`\n\n\tcsrPEM = `-----BEGIN CERTIFICATE REQUEST-----\nMIIEYjCCAkoCAQAwHTEbMBkGA1UEAxMSdGVzdC5zbWFsbHN0ZXAuY29tMIICIjAN\nBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAuCpifZfoZhYNywfpnPa21NezXgtn\nwrWBFE6xhVzE7YDSIqtIsj8aR7R8zwEymxfv5j5298LUy/XSmItVH31CsKyfcGqN\nQM0PZr9XY3z5V6qchGMqjzt/jqlYMBHujcxIFBfz4HATxSgKyvHqvw14ESsS2huu\n7jowx+XTKbFYgKcXrjBkvOej5FXD3ehkg0jDA2UAJNdfKmrc1BBEaaqOtfh7eyU2\nHU7+5gxH8C27IiCAmNj719E0B99Nu2MUw6aLFIM4xAcRga33Avevx6UuXZZIEepe\nV1sihrkcnDK9Vsxkme5erXzvAoOiRusiC2iIomJHJrdRM5ReEU+N+Tl1Kxq+rk7H\n/qAq78wVm07M1/GGi9SUMObZS4WuJpM6whlikIAEbv9iV+CK0sv/Jr/AADdGMmQU\nlwk+Q0ZNE8p4ZuWILv/dtLDtDVBpnrrJ9e8duBtB0lGcG8MdaUCQ346EI4T0Sgx0\nhJ+wMq8zYYFfPIZEHC8o9p1ywWN9ySpJ8Zj/5ubmx9v2bY67GbuVFEa8iAp+S00x\n/Z8nD6/JsoKtexuHyGr3ixWFzlBqXDuugukIDFUOVDCbuGw4Io4/hEMu4Zz0TIFk\nUu/wf2z75Tt8EkosKLu2wieKcY7n7Vhog/0tqexqWlWtJH0tvq4djsGoSvA62WPs\n0iXXj+aZIARPNhECAwEAAaAAMA0GCSqGSIb3DQEBCwUAA4ICAQA0vyHIndAkIs/I\nNnz5yZWCokRjokoKv3Aj4VilyjncL+W0UIPULLU/47ZyoHVSUj2t8gknr9xu/Kd+\ng/2z0RiF3CIp8IUH49w/HYWaR95glzVNAAzr8qD9UbUqloLVQW3lObSRGtezhdZO\nsspw5dC+inhAb1LZhx8PVxB3SAeJ8h11IEBr0s2Hxt9viKKd7YPtIFZkZdOkVx4R\nif1DMawj1P6fEomf8z7m+dmbUYTqqosbCbRL01mzEga/kF6JyH/OzpNlcsAiyM8e\nBxPWH6TtPqwmyy4y7j1outmM0RnyUw5A0HmIbWh+rHpXiHVsnNqse0XfzmaxM8+z\ndxYeDax8aMWZKfvY1Zew+xIxl7DtEy1BpxrZcawumJYt5+LL+bwF/OtL0inQLnw8\nzyqydsXNdrpIQJnfmWPld7ThWbQw2FBE70+nFSxHeG2ULnpF3M9xf6ZNAF4gqaNE\nQ7vMNPBWrJWu+A++vHY61WGET+h4lY3GFr2I8OE4IiHPQi1D7Y0+fwOmStwuRPM4\n2rARcJChNdiYBkkuvs4kixKTTjdXhB8RQtuBSrJ0M1tzq2qMbm7F8G01rOg4KlXU\n58jHzJwr1K7cx0lpWfGTtc5bseCGtTKmDBXTziw04yl8eE1+ZFOganixGwCtl4Tt\nDCbKzWTW8lqVdp9Kyf7XEhhc2R8C5w==\n-----END CERTIFICATE REQUEST-----`\n\n\tstepCertPEM = `-----BEGIN CERTIFICATE-----\nMIIChTCCAiugAwIBAgIRAJ3O5T28Rdj2lr/UPjf+GAUwCgYIKoZIzj0EAwIwJDEi\nMCAGA1UEAxMZU21hbGxzdGVwIEludGVybWVkaWF0ZSBDQTAeFw0xOTAyMjAyMDE1\nNDNaFw0xOTAyMjEyMDE1NDNaMHExCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJDQTEW\nMBQGA1UEBxMNU2FuIEZyYW5jaXNjbzEcMBoGA1UEChMTU21hbGxzdGVwIExhYnMg\nSW5jLjEfMB0GA1UEAxMWaW50ZXJuYWwuc21hbGxzdGVwLmNvbTBZMBMGByqGSM49\nAgEGCCqGSM49AwEHA0IABC0aKrTNl+gXFuNkXisqX4/foLO3VMt+Kphngziim+fz\naJhiS9JU+oFYLTNW6HWGUD8CNzfwrmWlVsAmiJwHKlKjgfAwge0wDgYDVR0PAQH/\nBAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAdBgNVHQ4EFgQU\nJheKvlZqNv1IcgaC8WOS1Zg0i1QwHwYDVR0jBBgwFoAUu97PaFQPfuyKOeew7Hg4\n5WFIAVMwIQYDVR0RBBowGIIWaW50ZXJuYWwuc21hbGxzdGVwLmNvbTBZBgwrBgEE\nAYKkZMYoQAEESTBHAgEBBBVtYXJpYW5vQHNtYWxsc3RlcC5jb20EK2pPMzdkdERi\na3UtUW5hYnM1VlIwWXc2WUZGdjl3ZUExOGRwM2h0dmRFanMwCgYIKoZIzj0EAwID\nSAAwRQIhAIrn17fP5CBrGtKuhyPiq6eSwryBCf8ki+k17u5a+E/LAiB24Y2E0Put\nnIHOI54lAqDeF7A0y73fPRVCiJEWmuxz0g==\n-----END CERTIFICATE-----`\n\n\tpubKey = `{\n\t\"use\": \"sig\",\n\t\"kty\": \"EC\",\n\t\"kid\": \"oV1p0MJeGQ7qBlK6B-oyfVdBRjh_e7VSK_YSEEqgW00\",\n\t\"crv\": \"P-256\",\n\t\"alg\": \"ES256\",\n\t\"x\": \"p9QX4tzjxUrB0fgqRWLKUuPolDtBW681f2Qyh-uVNhk\",\n\t\"y\": \"CNSEloc4oLDFTX0Vywj0WiqOlh516sFQwCj6WtM8LT8\"\n}`\n\n\tprivKey = \"eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJjdHkiOiJqd2sranNvbiIsImVuYyI6IkEyNTZHQ00iLCJwMmMiOjEwMDAwMCwicDJzIjoiNEhBYjE0WDQ5OFM4LWxSb29JTnpqZyJ9.RbkJXGzI3kOsaP20KmZs0ELFLgpRddAE49AJHlEblw-uH_gg6SV3QA.M3MArEpHgI171lhm.gBlFySpzK9F7riBJbtLSNkb4nAw_gWokqs1jS-ZK1qxuqTK-9mtX5yILjRnftx9P9uFp5xt7rvv4Mgom1Ed4V9WtIyfNP_Cz3Pme1Eanp5nY68WCe_yG6iSB1RJdMDBUb2qBDZiBdhJim1DRXsOfgedOrNi7GGbppMlD77DEpId118owR5izA-c6Q_hg08hIE3tnMAnebDNQoF9jfEY99_AReVRH8G4hgwZEPCfXMTb3J-lowKGG4vXIbK5knFLh47SgOqG4M2M51SMS-XJ7oBz1Vjoamc90QIqKV51rvZ5m0N_sPFtxzcfV4E9yYH3XVd4O-CG4ydVKfKVyMtQ.mcKFZqBHp_n7Ytj2jz9rvw\"\n)\n\nfunc mustJSON(t *testing.T, v any) []byte {\n\tt.Helper()\n\tvar buf bytes.Buffer\n\trequire.NoError(t, json.NewEncoder(&buf).Encode(v))\n\treturn buf.Bytes()\n}\n\nfunc parseCertificate(data string) *x509.Certificate {\n\tblock, _ := pem.Decode([]byte(data))\n\tif block == nil {\n\t\tpanic(\"failed to parse certificate PEM\")\n\t}\n\tcert, err := x509.ParseCertificate(block.Bytes)\n\tif err != nil {\n\t\tpanic(\"failed to parse certificate: \" + err.Error())\n\t}\n\treturn cert\n}\n\nfunc parseCertificateRequest(data string) *x509.CertificateRequest {\n\tblock, _ := pem.Decode([]byte(data))\n\tif block == nil {\n\t\tpanic(\"failed to parse certificate request PEM\")\n\t}\n\tcsr, err := x509.ParseCertificateRequest(block.Bytes)\n\tif err != nil {\n\t\tpanic(\"failed to parse certificate request: \" + err.Error())\n\t}\n\treturn csr\n}\n\nfunc mockMustAuthority(t *testing.T, a Authority) {\n\tt.Helper()\n\tfn := mustAuthority\n\tt.Cleanup(func() {\n\t\tmustAuthority = fn\n\t})\n\tmustAuthority = func(ctx context.Context) Authority {\n\t\treturn a\n\t}\n}\n\ntype mockAuthority struct {\n\tret1, ret2                   interface{}\n\terr                          error\n\tauthorize                    func(ctx context.Context, ott string) ([]provisioner.SignOption, error)\n\tauthorizeRenewToken          func(ctx context.Context, ott string) (*x509.Certificate, error)\n\tgetTLSOptions                func() *authority.TLSOptions\n\troot                         func(shasum string) (*x509.Certificate, error)\n\tsignWithContext              func(ctx context.Context, cr *x509.CertificateRequest, opts provisioner.SignOptions, signOpts ...provisioner.SignOption) ([]*x509.Certificate, error)\n\trenew                        func(cert *x509.Certificate) ([]*x509.Certificate, error)\n\trekey                        func(oldCert *x509.Certificate, pk crypto.PublicKey) ([]*x509.Certificate, error)\n\trenewContext                 func(ctx context.Context, oldCert *x509.Certificate, pk crypto.PublicKey) ([]*x509.Certificate, error)\n\tloadProvisionerByCertificate func(cert *x509.Certificate) (provisioner.Interface, error)\n\tloadProvisionerByName        func(name string) (provisioner.Interface, error)\n\tgetProvisioners              func(nextCursor string, limit int) (provisioner.List, string, error)\n\trevoke                       func(context.Context, *authority.RevokeOptions) error\n\tgetEncryptedKey              func(kid string) (string, error)\n\tgetRoots                     func() ([]*x509.Certificate, error)\n\tgetIntermediateCertificates  func() []*x509.Certificate\n\tgetFederation                func() ([]*x509.Certificate, error)\n\tgetCRL                       func() (*authority.CertificateRevocationListInfo, error)\n\tsignSSH                      func(ctx context.Context, key ssh.PublicKey, opts provisioner.SignSSHOptions, signOpts ...provisioner.SignOption) (*ssh.Certificate, error)\n\tsignSSHAddUser               func(ctx context.Context, key ssh.PublicKey, cert *ssh.Certificate) (*ssh.Certificate, error)\n\trenewSSH                     func(ctx context.Context, cert *ssh.Certificate) (*ssh.Certificate, error)\n\trekeySSH                     func(ctx context.Context, cert *ssh.Certificate, key ssh.PublicKey, signOpts ...provisioner.SignOption) (*ssh.Certificate, error)\n\tgetSSHHosts                  func(ctx context.Context, cert *x509.Certificate) ([]authority.Host, error)\n\tgetSSHRoots                  func(ctx context.Context) (*authority.SSHKeys, error)\n\tgetSSHFederation             func(ctx context.Context) (*authority.SSHKeys, error)\n\tgetSSHConfig                 func(ctx context.Context, typ string, data map[string]string) ([]templates.Output, error)\n\tcheckSSHHost                 func(ctx context.Context, principal, token string) (bool, error)\n\tgetSSHBastion                func(ctx context.Context, user string, hostname string) (*authority.Bastion, error)\n\tversion                      func() authority.Version\n}\n\nfunc (m *mockAuthority) GetCertificateRevocationList() (*authority.CertificateRevocationListInfo, error) {\n\tif m.getCRL != nil {\n\t\treturn m.getCRL()\n\t}\n\n\treturn m.ret1.(*authority.CertificateRevocationListInfo), m.err\n}\n\n// TODO: remove once Authorize is deprecated.\nfunc (m *mockAuthority) Authorize(ctx context.Context, ott string) ([]provisioner.SignOption, error) {\n\tif m.authorize != nil {\n\t\treturn m.authorize(ctx, ott)\n\t}\n\treturn m.ret1.([]provisioner.SignOption), m.err\n}\n\nfunc (m *mockAuthority) AuthorizeRenewToken(ctx context.Context, ott string) (*x509.Certificate, error) {\n\tif m.authorizeRenewToken != nil {\n\t\treturn m.authorizeRenewToken(ctx, ott)\n\t}\n\treturn m.ret1.(*x509.Certificate), m.err\n}\n\nfunc (m *mockAuthority) GetTLSOptions() *authority.TLSOptions {\n\tif m.getTLSOptions != nil {\n\t\treturn m.getTLSOptions()\n\t}\n\treturn m.ret1.(*authority.TLSOptions)\n}\n\nfunc (m *mockAuthority) Root(shasum string) (*x509.Certificate, error) {\n\tif m.root != nil {\n\t\treturn m.root(shasum)\n\t}\n\treturn m.ret1.(*x509.Certificate), m.err\n}\n\nfunc (m *mockAuthority) SignWithContext(ctx context.Context, cr *x509.CertificateRequest, opts provisioner.SignOptions, signOpts ...provisioner.SignOption) ([]*x509.Certificate, error) {\n\tif m.signWithContext != nil {\n\t\treturn m.signWithContext(ctx, cr, opts, signOpts...)\n\t}\n\treturn []*x509.Certificate{m.ret1.(*x509.Certificate), m.ret2.(*x509.Certificate)}, m.err\n}\n\nfunc (m *mockAuthority) Renew(cert *x509.Certificate) ([]*x509.Certificate, error) {\n\tif m.renew != nil {\n\t\treturn m.renew(cert)\n\t}\n\treturn []*x509.Certificate{m.ret1.(*x509.Certificate), m.ret2.(*x509.Certificate)}, m.err\n}\n\nfunc (m *mockAuthority) RenewContext(ctx context.Context, oldcert *x509.Certificate, pk crypto.PublicKey) ([]*x509.Certificate, error) {\n\tif m.renewContext != nil {\n\t\treturn m.renewContext(ctx, oldcert, pk)\n\t}\n\treturn []*x509.Certificate{m.ret1.(*x509.Certificate), m.ret2.(*x509.Certificate)}, m.err\n}\n\nfunc (m *mockAuthority) Rekey(oldcert *x509.Certificate, pk crypto.PublicKey) ([]*x509.Certificate, error) {\n\tif m.rekey != nil {\n\t\treturn m.rekey(oldcert, pk)\n\t}\n\treturn []*x509.Certificate{m.ret1.(*x509.Certificate), m.ret2.(*x509.Certificate)}, m.err\n}\n\nfunc (m *mockAuthority) GetProvisioners(nextCursor string, limit int) (provisioner.List, string, error) {\n\tif m.getProvisioners != nil {\n\t\treturn m.getProvisioners(nextCursor, limit)\n\t}\n\treturn m.ret1.(provisioner.List), m.ret2.(string), m.err\n}\n\nfunc (m *mockAuthority) LoadProvisionerByCertificate(cert *x509.Certificate) (provisioner.Interface, error) {\n\tif m.loadProvisionerByCertificate != nil {\n\t\treturn m.loadProvisionerByCertificate(cert)\n\t}\n\treturn m.ret1.(provisioner.Interface), m.err\n}\n\nfunc (m *mockAuthority) LoadProvisionerByName(name string) (provisioner.Interface, error) {\n\tif m.loadProvisionerByName != nil {\n\t\treturn m.loadProvisionerByName(name)\n\t}\n\treturn m.ret1.(provisioner.Interface), m.err\n}\n\nfunc (m *mockAuthority) Revoke(ctx context.Context, opts *authority.RevokeOptions) error {\n\tif m.revoke != nil {\n\t\treturn m.revoke(ctx, opts)\n\t}\n\treturn m.err\n}\n\nfunc (m *mockAuthority) GetEncryptedKey(kid string) (string, error) {\n\tif m.getEncryptedKey != nil {\n\t\treturn m.getEncryptedKey(kid)\n\t}\n\treturn m.ret1.(string), m.err\n}\n\nfunc (m *mockAuthority) GetRoots() ([]*x509.Certificate, error) {\n\tif m.getRoots != nil {\n\t\treturn m.getRoots()\n\t}\n\treturn m.ret1.([]*x509.Certificate), m.err\n}\n\nfunc (m *mockAuthority) GetIntermediateCertificates() []*x509.Certificate {\n\tif m.getIntermediateCertificates != nil {\n\t\treturn m.getIntermediateCertificates()\n\t}\n\treturn m.ret1.([]*x509.Certificate)\n}\n\nfunc (m *mockAuthority) GetFederation() ([]*x509.Certificate, error) {\n\tif m.getFederation != nil {\n\t\treturn m.getFederation()\n\t}\n\treturn m.ret1.([]*x509.Certificate), m.err\n}\n\nfunc (m *mockAuthority) SignSSH(ctx context.Context, key ssh.PublicKey, opts provisioner.SignSSHOptions, signOpts ...provisioner.SignOption) (*ssh.Certificate, error) {\n\tif m.signSSH != nil {\n\t\treturn m.signSSH(ctx, key, opts, signOpts...)\n\t}\n\treturn m.ret1.(*ssh.Certificate), m.err\n}\n\nfunc (m *mockAuthority) SignSSHAddUser(ctx context.Context, key ssh.PublicKey, cert *ssh.Certificate) (*ssh.Certificate, error) {\n\tif m.signSSHAddUser != nil {\n\t\treturn m.signSSHAddUser(ctx, key, cert)\n\t}\n\treturn m.ret1.(*ssh.Certificate), m.err\n}\n\nfunc (m *mockAuthority) RenewSSH(ctx context.Context, cert *ssh.Certificate) (*ssh.Certificate, error) {\n\tif m.renewSSH != nil {\n\t\treturn m.renewSSH(ctx, cert)\n\t}\n\treturn m.ret1.(*ssh.Certificate), m.err\n}\n\nfunc (m *mockAuthority) RekeySSH(ctx context.Context, cert *ssh.Certificate, key ssh.PublicKey, signOpts ...provisioner.SignOption) (*ssh.Certificate, error) {\n\tif m.rekeySSH != nil {\n\t\treturn m.rekeySSH(ctx, cert, key, signOpts...)\n\t}\n\treturn m.ret1.(*ssh.Certificate), m.err\n}\n\nfunc (m *mockAuthority) GetSSHHosts(ctx context.Context, cert *x509.Certificate) ([]authority.Host, error) {\n\tif m.getSSHHosts != nil {\n\t\treturn m.getSSHHosts(ctx, cert)\n\t}\n\treturn m.ret1.([]authority.Host), m.err\n}\n\nfunc (m *mockAuthority) GetSSHRoots(ctx context.Context) (*authority.SSHKeys, error) {\n\tif m.getSSHRoots != nil {\n\t\treturn m.getSSHRoots(ctx)\n\t}\n\treturn m.ret1.(*authority.SSHKeys), m.err\n}\n\nfunc (m *mockAuthority) GetSSHFederation(ctx context.Context) (*authority.SSHKeys, error) {\n\tif m.getSSHFederation != nil {\n\t\treturn m.getSSHFederation(ctx)\n\t}\n\treturn m.ret1.(*authority.SSHKeys), m.err\n}\n\nfunc (m *mockAuthority) GetSSHConfig(ctx context.Context, typ string, data map[string]string) ([]templates.Output, error) {\n\tif m.getSSHConfig != nil {\n\t\treturn m.getSSHConfig(ctx, typ, data)\n\t}\n\treturn m.ret1.([]templates.Output), m.err\n}\n\nfunc (m *mockAuthority) CheckSSHHost(ctx context.Context, principal, token string) (bool, error) {\n\tif m.checkSSHHost != nil {\n\t\treturn m.checkSSHHost(ctx, principal, token)\n\t}\n\treturn m.ret1.(bool), m.err\n}\n\nfunc (m *mockAuthority) GetSSHBastion(ctx context.Context, user, hostname string) (*authority.Bastion, error) {\n\tif m.getSSHBastion != nil {\n\t\treturn m.getSSHBastion(ctx, user, hostname)\n\t}\n\treturn m.ret1.(*authority.Bastion), m.err\n}\n\nfunc (m *mockAuthority) Version() authority.Version {\n\tif m.version != nil {\n\t\treturn m.version()\n\t}\n\treturn m.ret1.(authority.Version)\n}\n\nfunc TestNewCertificate(t *testing.T) {\n\tcert := parseCertificate(rootPEM)\n\tif !reflect.DeepEqual(Certificate{Certificate: cert}, NewCertificate(cert)) {\n\t\tt.Errorf(\"NewCertificate failed, got %v, wants %v\", NewCertificate(cert), Certificate{Certificate: cert})\n\t}\n}\n\nfunc TestCertificate_MarshalJSON(t *testing.T) {\n\ttype fields struct {\n\t\tCertificate *x509.Certificate\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tfields  fields\n\t\twant    []byte\n\t\twantErr bool\n\t}{\n\t\t{\"nil\", fields{Certificate: nil}, []byte(\"null\"), false},\n\t\t{\"empty\", fields{Certificate: &x509.Certificate{Raw: nil}}, []byte(`\"-----BEGIN CERTIFICATE-----\\n-----END CERTIFICATE-----\\n\"`), false},\n\t\t{\"root\", fields{Certificate: parseCertificate(rootPEM)}, []byte(`\"` + strings.ReplaceAll(rootPEM, \"\\n\", `\\n`) + `\\n\"`), false},\n\t\t{\"cert\", fields{Certificate: parseCertificate(certPEM)}, []byte(`\"` + strings.ReplaceAll(certPEM, \"\\n\", `\\n`) + `\\n\"`), false},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tc := Certificate{\n\t\t\t\tCertificate: tt.fields.Certificate,\n\t\t\t}\n\t\t\tgot, err := c.MarshalJSON()\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"Certificate.MarshalJSON() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"Certificate.MarshalJSON() = %s, want %s\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestCertificate_UnmarshalJSON(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tdata     []byte\n\t\twantCert bool\n\t\twantErr  bool\n\t}{\n\t\t{\"no data\", nil, false, true},\n\t\t{\"incomplete string 1\", []byte(`\"foobar`), false, true}, {\"incomplete string 2\", []byte(`foobar\"`), false, true},\n\t\t{\"invalid string\", []byte(`\"foobar\"`), false, true},\n\t\t{\"invalid bytes 0\", []byte{}, false, true}, {\"invalid bytes 1\", []byte{1}, false, true},\n\t\t{\"empty csr\", []byte(`\"-----BEGIN CERTIFICATE-----\\n-----END CERTIFICATE----\\n\"`), false, true},\n\t\t{\"invalid type\", []byte(`\"` + strings.ReplaceAll(csrPEM, \"\\n\", `\\n`) + `\"`), false, true},\n\t\t{\"empty string\", []byte(`\"\"`), false, false},\n\t\t{\"json null\", []byte(`null`), false, false},\n\t\t{\"valid root\", []byte(`\"` + strings.ReplaceAll(rootPEM, \"\\n\", `\\n`) + `\"`), true, false},\n\t\t{\"valid cert\", []byte(`\"` + strings.ReplaceAll(certPEM, \"\\n\", `\\n`) + `\"`), true, false},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tvar c Certificate\n\t\t\tif err := c.UnmarshalJSON(tt.data); (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"Certificate.UnmarshalJSON() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\t\t\tif tt.wantCert && c.Certificate == nil {\n\t\t\t\tt.Error(\"Certificate.UnmarshalJSON() failed, Certificate is nil\")\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestCertificate_UnmarshalJSON_json(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tdata     string\n\t\twantCert bool\n\t\twantErr  bool\n\t}{\n\t\t{\"invalid type (bool)\", `{\"crt\":true}`, false, true},\n\t\t{\"invalid type (number)\", `{\"crt\":123}`, false, true},\n\t\t{\"invalid type (object)\", `{\"crt\":{}}`, false, true},\n\t\t{\"empty crt (null)\", `{\"crt\":null}`, false, false},\n\t\t{\"empty crt (string)\", `{\"crt\":\"\"}`, false, false},\n\t\t{\"empty crt\", `{\"crt\":\"-----BEGIN CERTIFICATE-----\\n-----END CERTIFICATE----\\n\"}`, false, true},\n\t\t{\"valid crt\", `{\"crt\":\"` + strings.ReplaceAll(certPEM, \"\\n\", `\\n`) + `\"}`, true, false},\n\t}\n\n\ttype request struct {\n\t\tCert Certificate `json:\"crt\"`\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tvar body request\n\t\t\tif err := json.Unmarshal([]byte(tt.data), &body); (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"json.Unmarshal() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\n\t\t\tswitch tt.wantCert {\n\t\t\tcase true:\n\t\t\t\tif body.Cert.Certificate == nil {\n\t\t\t\t\tt.Error(\"json.Unmarshal() failed, Certificate is nil\")\n\t\t\t\t}\n\t\t\tcase false:\n\t\t\t\tif body.Cert.Certificate != nil {\n\t\t\t\t\tt.Error(\"json.Unmarshal() failed, Certificate is not nil\")\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\nfunc TestNewCertificateRequest(t *testing.T) {\n\tcsr := parseCertificateRequest(csrPEM)\n\tif !reflect.DeepEqual(CertificateRequest{CertificateRequest: csr}, NewCertificateRequest(csr)) {\n\t\tt.Errorf(\"NewCertificateRequest failed, got %v, wants %v\", NewCertificateRequest(csr), CertificateRequest{CertificateRequest: csr})\n\t}\n}\n\nfunc TestCertificateRequest_MarshalJSON(t *testing.T) {\n\ttype fields struct {\n\t\tCertificateRequest *x509.CertificateRequest\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tfields  fields\n\t\twant    []byte\n\t\twantErr bool\n\t}{\n\t\t{\"nil\", fields{CertificateRequest: nil}, []byte(\"null\"), false},\n\t\t{\"empty\", fields{CertificateRequest: &x509.CertificateRequest{}}, []byte(`\"-----BEGIN CERTIFICATE REQUEST-----\\n-----END CERTIFICATE REQUEST-----\\n\"`), false},\n\t\t{\"csr\", fields{CertificateRequest: parseCertificateRequest(csrPEM)}, []byte(`\"` + strings.ReplaceAll(csrPEM, \"\\n\", `\\n`) + `\\n\"`), false},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tc := CertificateRequest{\n\t\t\t\tCertificateRequest: tt.fields.CertificateRequest,\n\t\t\t}\n\t\t\tgot, err := c.MarshalJSON()\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"CertificateRequest.MarshalJSON() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"CertificateRequest.MarshalJSON() = %s, want %s\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestCertificateRequest_UnmarshalJSON(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tdata     []byte\n\t\twantCert bool\n\t\twantErr  bool\n\t}{\n\t\t{\"no data\", nil, false, true},\n\t\t{\"incomplete string 1\", []byte(`\"foobar`), false, true}, {\"incomplete string 2\", []byte(`foobar\"`), false, true},\n\t\t{\"invalid string\", []byte(`\"foobar\"`), false, true},\n\t\t{\"invalid bytes 0\", []byte{}, false, true}, {\"invalid bytes 1\", []byte{1}, false, true},\n\t\t{\"empty csr\", []byte(`\"-----BEGIN CERTIFICATE REQUEST-----\\n-----END CERTIFICATE REQUEST----\\n\"`), false, true},\n\t\t{\"invalid type\", []byte(`\"` + strings.ReplaceAll(rootPEM, \"\\n\", `\\n`) + `\"`), false, true},\n\t\t{\"empty string\", []byte(`\"\"`), false, false},\n\t\t{\"json null\", []byte(`null`), false, false},\n\t\t{\"valid csr\", []byte(`\"` + strings.ReplaceAll(csrPEM, \"\\n\", `\\n`) + `\"`), true, false},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tvar c CertificateRequest\n\t\t\tif err := c.UnmarshalJSON(tt.data); (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"CertificateRequest.UnmarshalJSON() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\t\t\tif tt.wantCert && c.CertificateRequest == nil {\n\t\t\t\tt.Error(\"CertificateRequest.UnmarshalJSON() failed, CertificateRequet is nil\")\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestCertificateRequest_UnmarshalJSON_json(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tdata     string\n\t\twantCert bool\n\t\twantErr  bool\n\t}{\n\t\t{\"invalid type (bool)\", `{\"csr\":true}`, false, true},\n\t\t{\"invalid type (number)\", `{\"csr\":123}`, false, true},\n\t\t{\"invalid type (object)\", `{\"csr\":{}}`, false, true},\n\t\t{\"empty csr (null)\", `{\"csr\":null}`, false, false},\n\t\t{\"empty csr (string)\", `{\"csr\":\"\"}`, false, false},\n\t\t{\"empty csr\", `{\"csr\":\"-----BEGIN CERTIFICATE REQUEST-----\\n-----END CERTIFICATE REQUEST----\\n\"}`, false, true},\n\t\t{\"valid csr\", `{\"csr\":\"` + strings.ReplaceAll(csrPEM, \"\\n\", `\\n`) + `\"}`, true, false},\n\t}\n\n\ttype request struct {\n\t\tCSR CertificateRequest `json:\"csr\"`\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tvar body request\n\t\t\tif err := json.Unmarshal([]byte(tt.data), &body); (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"json.Unmarshal() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\n\t\t\tswitch tt.wantCert {\n\t\t\tcase true:\n\t\t\t\tif body.CSR.CertificateRequest == nil {\n\t\t\t\t\tt.Error(\"json.Unmarshal() failed, CertificateRequest is nil\")\n\t\t\t\t}\n\t\t\tcase false:\n\t\t\t\tif body.CSR.CertificateRequest != nil {\n\t\t\t\t\tt.Error(\"json.Unmarshal() failed, CertificateRequest is not nil\")\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSignRequest_Validate(t *testing.T) {\n\tcsr := parseCertificateRequest(csrPEM)\n\tbad := parseCertificateRequest(csrPEM)\n\tbad.Signature[0]++\n\ttype fields struct {\n\t\tCsrPEM    CertificateRequest\n\t\tOTT       string\n\t\tNotBefore time.Time\n\t\tNotAfter  time.Time\n\t}\n\ttests := []struct {\n\t\tname   string\n\t\tfields fields\n\t\terr    error\n\t}{\n\t\t{\"missing csr\", fields{CertificateRequest{}, \"foobarzar\", time.Time{}, time.Time{}}, errors.New(\"missing csr\")},\n\t\t{\"invalid csr\", fields{CertificateRequest{bad}, \"foobarzar\", time.Time{}, time.Time{}}, errors.New(\"invalid csr\")},\n\t\t{\"missing ott\", fields{CertificateRequest{csr}, \"\", time.Time{}, time.Time{}}, errors.New(\"missing ott\")},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ts := &SignRequest{\n\t\t\t\tCsrPEM:    tt.fields.CsrPEM,\n\t\t\t\tOTT:       tt.fields.OTT,\n\t\t\t\tNotAfter:  NewTimeDuration(tt.fields.NotAfter),\n\t\t\t\tNotBefore: NewTimeDuration(tt.fields.NotBefore),\n\t\t\t}\n\t\t\tif err := s.Validate(); err != nil {\n\t\t\t\tif assert.NotNil(t, tt.err) {\n\t\t\t\t\tassert.True(t, strings.HasPrefix(err.Error(), tt.err.Error()))\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tassert.Nil(t, tt.err)\n\t\t\t}\n\t\t})\n\t}\n}\n\ntype mockProvisioner struct {\n\tret1, ret2, ret3   interface{}\n\terr                error\n\tgetID              func() string\n\tgetIDForToken      func() string\n\tgetTokenID         func(string) (string, error)\n\tgetName            func() string\n\tgetType            func() provisioner.Type\n\tgetEncryptedKey    func() (string, string, bool)\n\tinit               func(provisioner.Config) error\n\tauthorizeRenew     func(ctx context.Context, cert *x509.Certificate) error\n\tauthorizeRevoke    func(ctx context.Context, token string) error\n\tauthorizeSign      func(ctx context.Context, ott string) ([]provisioner.SignOption, error)\n\tauthorizeRenewal   func(*x509.Certificate) error\n\tauthorizeSSHSign   func(ctx context.Context, token string) ([]provisioner.SignOption, error)\n\tauthorizeSSHRevoke func(ctx context.Context, token string) error\n\tauthorizeSSHRenew  func(ctx context.Context, token string) (*ssh.Certificate, error)\n\tauthorizeSSHRekey  func(ctx context.Context, token string) (*ssh.Certificate, []provisioner.SignOption, error)\n}\n\nfunc (m *mockProvisioner) GetID() string {\n\tif m.getID != nil {\n\t\treturn m.getID()\n\t}\n\treturn m.ret1.(string)\n}\n\nfunc (m *mockProvisioner) GetIDForToken() string {\n\tif m.getIDForToken != nil {\n\t\treturn m.getIDForToken()\n\t}\n\treturn m.ret1.(string)\n}\n\nfunc (m *mockProvisioner) GetTokenID(token string) (string, error) {\n\tif m.getTokenID != nil {\n\t\treturn m.getTokenID(token)\n\t}\n\tif m.ret1 == nil {\n\t\treturn \"\", m.err\n\t}\n\treturn m.ret1.(string), m.err\n}\n\nfunc (m *mockProvisioner) GetName() string {\n\tif m.getName != nil {\n\t\treturn m.getName()\n\t}\n\treturn m.ret1.(string)\n}\n\nfunc (m *mockProvisioner) GetType() provisioner.Type {\n\tif m.getType != nil {\n\t\treturn m.getType()\n\t}\n\treturn m.ret1.(provisioner.Type)\n}\n\nfunc (m *mockProvisioner) GetEncryptedKey() (string, string, bool) {\n\tif m.getEncryptedKey != nil {\n\t\treturn m.getEncryptedKey()\n\t}\n\treturn m.ret1.(string), m.ret2.(string), m.ret3.(bool)\n}\n\nfunc (m *mockProvisioner) Init(c provisioner.Config) error {\n\tif m.init != nil {\n\t\treturn m.init(c)\n\t}\n\treturn m.err\n}\n\nfunc (m *mockProvisioner) AuthorizeRenew(ctx context.Context, cert *x509.Certificate) error {\n\tif m.authorizeRenew != nil {\n\t\treturn m.authorizeRenew(ctx, cert)\n\t}\n\treturn m.err\n}\n\nfunc (m *mockProvisioner) AuthorizeRevoke(ctx context.Context, token string) error {\n\tif m.authorizeRevoke != nil {\n\t\treturn m.authorizeRevoke(ctx, token)\n\t}\n\treturn m.err\n}\n\nfunc (m *mockProvisioner) AuthorizeSign(ctx context.Context, ott string) ([]provisioner.SignOption, error) {\n\tif m.authorizeSign != nil {\n\t\treturn m.authorizeSign(ctx, ott)\n\t}\n\treturn m.ret1.([]provisioner.SignOption), m.err\n}\n\nfunc (m *mockProvisioner) AuthorizeRenewal(c *x509.Certificate) error {\n\tif m.authorizeRenewal != nil {\n\t\treturn m.authorizeRenewal(c)\n\t}\n\treturn m.err\n}\n\nfunc (m *mockProvisioner) AuthorizeSSHSign(ctx context.Context, token string) ([]provisioner.SignOption, error) {\n\tif m.authorizeSSHSign != nil {\n\t\treturn m.authorizeSSHSign(ctx, token)\n\t}\n\treturn m.ret1.([]provisioner.SignOption), m.err\n}\nfunc (m *mockProvisioner) AuthorizeSSHRevoke(ctx context.Context, token string) error {\n\tif m.authorizeSSHRevoke != nil {\n\t\treturn m.authorizeSSHRevoke(ctx, token)\n\t}\n\treturn m.err\n}\nfunc (m *mockProvisioner) AuthorizeSSHRenew(ctx context.Context, token string) (*ssh.Certificate, error) {\n\tif m.authorizeSSHRenew != nil {\n\t\treturn m.authorizeSSHRenew(ctx, token)\n\t}\n\treturn m.ret1.(*ssh.Certificate), m.err\n}\nfunc (m *mockProvisioner) AuthorizeSSHRekey(ctx context.Context, token string) (*ssh.Certificate, []provisioner.SignOption, error) {\n\tif m.authorizeSSHRekey != nil {\n\t\treturn m.authorizeSSHRekey(ctx, token)\n\t}\n\treturn m.ret1.(*ssh.Certificate), m.ret2.([]provisioner.SignOption), m.err\n}\n\nfunc Test_caHandler_Route(t *testing.T) {\n\ttype fields struct {\n\t\tAuthority Authority\n\t}\n\ttype args struct {\n\t\tr Router\n\t}\n\ttests := []struct {\n\t\tname   string\n\t\tfields fields\n\t\targs   args\n\t}{\n\t\t{\"ok\", fields{&mockAuthority{}}, args{chi.NewRouter()}},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\th := &caHandler{\n\t\t\t\tAuthority: tt.fields.Authority,\n\t\t\t}\n\t\t\th.Route(tt.args.r)\n\t\t})\n\t}\n}\n\nfunc Test_Health(t *testing.T) {\n\treq := httptest.NewRequest(\"GET\", \"http://example.com/health\", http.NoBody)\n\tw := httptest.NewRecorder()\n\tHealth(w, req)\n\n\tres := w.Result()\n\tif res.StatusCode != 200 {\n\t\tt.Errorf(\"caHandler.Health StatusCode = %d, wants 200\", res.StatusCode)\n\t}\n\n\tbody, err := io.ReadAll(res.Body)\n\tres.Body.Close()\n\tif err != nil {\n\t\tt.Errorf(\"caHandler.Health unexpected error = %v\", err)\n\t}\n\texpected := []byte(\"{\\\"status\\\":\\\"ok\\\"}\\n\")\n\tif !bytes.Equal(body, expected) {\n\t\tt.Errorf(\"caHandler.Health Body = %s, wants %s\", body, expected)\n\t}\n}\n\nfunc Test_Root(t *testing.T) {\n\tconst sha = \"efc7d6b475a56fe587650bcdb999a4a308f815ba44db4bf0371ea68a786ccd36\"\n\ttests := []struct {\n\t\tname        string\n\t\troot        *x509.Certificate\n\t\terr         error\n\t\texpectedMsg string\n\t\tstatusCode  int\n\t}{\n\t\t{\"ok\", parseCertificate(rootPEM), nil, \"\", 200},\n\t\t{\"fail\", nil, fmt.Errorf(\"not found\"), fmt.Sprintf(`root certificate with fingerprint \\\"%s\\\" was not found`, sha), 404},\n\t}\n\n\t// Request with chi context\n\tchiCtx := chi.NewRouteContext()\n\tchiCtx.URLParams.Add(\"sha\", sha)\n\treq := httptest.NewRequest(\"GET\", \"http://example.com/root/\"+sha, http.NoBody)\n\treq = req.WithContext(context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx))\n\n\texpected := []byte(`{\"ca\":\"` + strings.ReplaceAll(rootPEM, \"\\n\", `\\n`) + `\\n\"}`)\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tmockMustAuthority(t, &mockAuthority{ret1: tt.root, err: tt.err})\n\t\t\tw := httptest.NewRecorder()\n\t\t\tRoot(w, req)\n\t\t\tres := w.Result()\n\n\t\t\tif res.StatusCode != tt.statusCode {\n\t\t\t\tt.Errorf(\"caHandler.Root StatusCode = %d, wants %d\", res.StatusCode, tt.statusCode)\n\t\t\t}\n\n\t\t\tbody, err := io.ReadAll(res.Body)\n\t\t\tres.Body.Close()\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"caHandler.Root unexpected error = %v\", err)\n\t\t\t}\n\t\t\tif tt.statusCode == 200 {\n\t\t\t\tif !bytes.Equal(bytes.TrimSpace(body), expected) {\n\t\t\t\t\tt.Errorf(\"caHandler.Root Body = %s, wants %s\", body, expected)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\trequire.Contains(t, string(body), tt.expectedMsg)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_Sign(t *testing.T) {\n\tcsr := parseCertificateRequest(csrPEM)\n\tvalid, err := json.Marshal(SignRequest{\n\t\tCsrPEM: CertificateRequest{csr},\n\t\tOTT:    \"foobarzar\",\n\t})\n\trequire.NoError(t, err)\n\tinvalid, err := json.Marshal(SignRequest{\n\t\tCsrPEM: CertificateRequest{csr},\n\t\tOTT:    \"\",\n\t})\n\trequire.NoError(t, err)\n\n\texpected1 := []byte(`{\"crt\":\"` + strings.ReplaceAll(certPEM, \"\\n\", `\\n`) + `\\n\",\"ca\":\"` + strings.ReplaceAll(rootPEM, \"\\n\", `\\n`) + `\\n\",\"certChain\":[\"` + strings.ReplaceAll(certPEM, \"\\n\", `\\n`) + `\\n\",\"` + strings.ReplaceAll(rootPEM, \"\\n\", `\\n`) + `\\n\"]}`)\n\texpected2 := []byte(`{\"crt\":\"` + strings.ReplaceAll(stepCertPEM, \"\\n\", `\\n`) + `\\n\",\"ca\":\"` + strings.ReplaceAll(rootPEM, \"\\n\", `\\n`) + `\\n\",\"certChain\":[\"` + strings.ReplaceAll(stepCertPEM, \"\\n\", `\\n`) + `\\n\",\"` + strings.ReplaceAll(rootPEM, \"\\n\", `\\n`) + `\\n\"]}`)\n\n\ttests := []struct {\n\t\tname         string\n\t\tinput        string\n\t\tcertAttrOpts []provisioner.SignOption\n\t\tautherr      error\n\t\tcert         *x509.Certificate\n\t\troot         *x509.Certificate\n\t\tsignErr      error\n\t\tstatusCode   int\n\t\texpected     []byte\n\t}{\n\t\t{\"ok\", string(valid), nil, nil, parseCertificate(certPEM), parseCertificate(rootPEM), nil, http.StatusCreated, expected1},\n\t\t{\"ok with Provisioner\", string(valid), nil, nil, parseCertificate(stepCertPEM), parseCertificate(rootPEM), nil, http.StatusCreated, expected2},\n\t\t{\"json read error\", \"{\", nil, nil, nil, nil, nil, http.StatusBadRequest, nil},\n\t\t{\"validate error\", string(invalid), nil, nil, nil, nil, nil, http.StatusBadRequest, nil},\n\t\t{\"authorize error\", string(valid), nil, fmt.Errorf(\"an error\"), nil, nil, nil, http.StatusUnauthorized, nil},\n\t\t{\"sign error\", string(valid), nil, nil, nil, nil, fmt.Errorf(\"an error\"), http.StatusForbidden, nil},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tmockMustAuthority(t, &mockAuthority{\n\t\t\t\tret1: tt.cert, ret2: tt.root, err: tt.signErr,\n\t\t\t\tauthorize: func(ctx context.Context, ott string) ([]provisioner.SignOption, error) {\n\t\t\t\t\treturn tt.certAttrOpts, tt.autherr\n\t\t\t\t},\n\t\t\t\tgetTLSOptions: func() *authority.TLSOptions {\n\t\t\t\t\treturn nil\n\t\t\t\t},\n\t\t\t})\n\t\t\treq := httptest.NewRequest(\"POST\", \"http://example.com/sign\", strings.NewReader(tt.input))\n\t\t\tw := httptest.NewRecorder()\n\t\t\tSign(logging.NewResponseLogger(w), req)\n\t\t\tres := w.Result()\n\n\t\t\tif res.StatusCode != tt.statusCode {\n\t\t\t\tt.Errorf(\"caHandler.Root StatusCode = %d, wants %d\", res.StatusCode, tt.statusCode)\n\t\t\t}\n\n\t\t\tbody, err := io.ReadAll(res.Body)\n\t\t\tres.Body.Close()\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"caHandler.Root unexpected error = %v\", err)\n\t\t\t}\n\t\t\tif tt.statusCode < http.StatusBadRequest {\n\t\t\t\tif !bytes.Equal(bytes.TrimSpace(body), tt.expected) {\n\t\t\t\t\tt.Errorf(\"caHandler.Root Body = %s, wants %s\", body, tt.expected)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_Renew(t *testing.T) {\n\tcs := &tls.ConnectionState{\n\t\tPeerCertificates: []*x509.Certificate{parseCertificate(certPEM)},\n\t}\n\n\t// Prepare root and leaf for renew after expiry test.\n\tnow := time.Now()\n\trootPub, rootPriv, err := ed25519.GenerateKey(rand.Reader)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tleafPub, leafPriv, err := ed25519.GenerateKey(rand.Reader)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\troot := &x509.Certificate{\n\t\tSubject:               pkix.Name{CommonName: \"Test Root CA\"},\n\t\tPublicKey:             rootPub,\n\t\tKeyUsage:              x509.KeyUsageCertSign,\n\t\tBasicConstraintsValid: true,\n\t\tIsCA:                  true,\n\t\tNotBefore:             now.Add(-2 * time.Hour),\n\t\tNotAfter:              now.Add(time.Hour),\n\t}\n\troot, err = x509util.CreateCertificate(root, root, rootPub, rootPriv)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\texpiredLeaf := &x509.Certificate{\n\t\tSubject:        pkix.Name{CommonName: \"Leaf certificate\"},\n\t\tPublicKey:      leafPub,\n\t\tKeyUsage:       x509.KeyUsageDigitalSignature,\n\t\tExtKeyUsage:    []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},\n\t\tNotBefore:      now.Add(-time.Hour),\n\t\tNotAfter:       now.Add(-time.Minute),\n\t\tEmailAddresses: []string{\"test@example.org\"},\n\t}\n\texpiredLeaf, err = x509util.CreateCertificate(expiredLeaf, root, leafPub, rootPriv)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\t// Generate renew after expiry token\n\tso := new(jose.SignerOptions)\n\tso.WithType(\"JWT\")\n\tso.WithHeader(\"x5cInsecure\", []string{base64.StdEncoding.EncodeToString(expiredLeaf.Raw)})\n\tsig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.EdDSA, Key: leafPriv}, so)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tgenerateX5cToken := func(claims jose.Claims) string {\n\t\ts, err := jose.Signed(sig).Claims(claims).CompactSerialize()\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\treturn s\n\t}\n\n\ttests := []struct {\n\t\tname       string\n\t\ttls        *tls.ConnectionState\n\t\theader     http.Header\n\t\tcert       *x509.Certificate\n\t\troot       *x509.Certificate\n\t\terr        error\n\t\tstatusCode int\n\t}{\n\t\t{\"ok\", cs, nil, parseCertificate(certPEM), parseCertificate(rootPEM), nil, http.StatusCreated},\n\t\t{\"ok renew after expiry\", &tls.ConnectionState{}, http.Header{\n\t\t\t\"Authorization\": []string{\"Bearer \" + generateX5cToken(jose.Claims{\n\t\t\t\tNotBefore: jose.NewNumericDate(now), Expiry: jose.NewNumericDate(now.Add(5 * time.Minute)),\n\t\t\t})},\n\t\t}, expiredLeaf, root, nil, http.StatusCreated},\n\t\t{\"no tls\", nil, nil, nil, nil, nil, http.StatusBadRequest},\n\t\t{\"no peer certificates\", &tls.ConnectionState{}, nil, nil, nil, nil, http.StatusBadRequest},\n\t\t{\"renew error\", cs, nil, nil, nil, errs.Forbidden(\"an error\"), http.StatusForbidden},\n\t\t{\"fail expired token\", &tls.ConnectionState{}, http.Header{\n\t\t\t\"Authorization\": []string{\"Bearer \" + generateX5cToken(jose.Claims{\n\t\t\t\tNotBefore: jose.NewNumericDate(now.Add(-time.Hour)), Expiry: jose.NewNumericDate(now.Add(-time.Minute)),\n\t\t\t})},\n\t\t}, expiredLeaf, root, errs.Forbidden(\"an error\"), http.StatusUnauthorized},\n\t\t{\"fail invalid root\", &tls.ConnectionState{}, http.Header{\n\t\t\t\"Authorization\": []string{\"Bearer \" + generateX5cToken(jose.Claims{\n\t\t\t\tNotBefore: jose.NewNumericDate(now.Add(-time.Hour)), Expiry: jose.NewNumericDate(now.Add(-time.Minute)),\n\t\t\t})},\n\t\t}, expiredLeaf, parseCertificate(rootPEM), errs.Forbidden(\"an error\"), http.StatusUnauthorized},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tmockMustAuthority(t, &mockAuthority{\n\t\t\t\tret1: tt.cert, ret2: tt.root, err: tt.err,\n\t\t\t\tauthorizeRenewToken: func(ctx context.Context, ott string) (*x509.Certificate, error) {\n\t\t\t\t\tjwt, chain, err := jose.ParseX5cInsecure(ott, []*x509.Certificate{tt.root})\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn nil, errs.Unauthorized(err.Error())\n\t\t\t\t\t}\n\t\t\t\t\tvar claims jose.Claims\n\t\t\t\t\tif err := jwt.Claims(chain[0][0].PublicKey, &claims); err != nil {\n\t\t\t\t\t\treturn nil, errs.Unauthorized(err.Error())\n\t\t\t\t\t}\n\t\t\t\t\tif err := claims.ValidateWithLeeway(jose.Expected{\n\t\t\t\t\t\tTime: now,\n\t\t\t\t\t}, time.Minute); err != nil {\n\t\t\t\t\t\treturn nil, errs.Unauthorized(err.Error())\n\t\t\t\t\t}\n\t\t\t\t\treturn chain[0][0], nil\n\t\t\t\t},\n\t\t\t\tgetTLSOptions: func() *authority.TLSOptions {\n\t\t\t\t\treturn nil\n\t\t\t\t},\n\t\t\t})\n\t\t\treq := httptest.NewRequest(\"POST\", \"http://example.com/renew\", http.NoBody)\n\t\t\treq.TLS = tt.tls\n\t\t\treq.Header = tt.header\n\t\t\tw := httptest.NewRecorder()\n\t\t\tRenew(logging.NewResponseLogger(w), req)\n\n\t\t\tres := w.Result()\n\t\t\tdefer res.Body.Close()\n\n\t\t\tbody, err := io.ReadAll(res.Body)\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"caHandler.Renew unexpected error = %v\", err)\n\t\t\t}\n\t\t\tif res.StatusCode != tt.statusCode {\n\t\t\t\tt.Errorf(\"caHandler.Renew StatusCode = %d, wants %d\", res.StatusCode, tt.statusCode)\n\t\t\t\tt.Errorf(\"%s\", body)\n\t\t\t}\n\n\t\t\tif tt.statusCode < http.StatusBadRequest {\n\t\t\t\texpected := []byte(`{\"crt\":\"` + strings.ReplaceAll(string(pem.EncodeToMemory(&pem.Block{Type: \"CERTIFICATE\", Bytes: tt.cert.Raw})), \"\\n\", `\\n`) + `\",` +\n\t\t\t\t\t`\"ca\":\"` + strings.ReplaceAll(string(pem.EncodeToMemory(&pem.Block{Type: \"CERTIFICATE\", Bytes: tt.root.Raw})), \"\\n\", `\\n`) + `\",` +\n\t\t\t\t\t`\"certChain\":[\"` +\n\t\t\t\t\tstrings.ReplaceAll(string(pem.EncodeToMemory(&pem.Block{Type: \"CERTIFICATE\", Bytes: tt.cert.Raw})), \"\\n\", `\\n`) + `\",\"` +\n\t\t\t\t\tstrings.ReplaceAll(string(pem.EncodeToMemory(&pem.Block{Type: \"CERTIFICATE\", Bytes: tt.root.Raw})), \"\\n\", `\\n`) + `\"]}`)\n\n\t\t\t\tif !bytes.Equal(bytes.TrimSpace(body), expected) {\n\t\t\t\t\tt.Errorf(\"caHandler.Root Body = \\n%s, wants \\n%s\", body, expected)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_Rekey(t *testing.T) {\n\tcs := &tls.ConnectionState{\n\t\tPeerCertificates: []*x509.Certificate{parseCertificate(certPEM)},\n\t}\n\tcsr := parseCertificateRequest(csrPEM)\n\tvalid, err := json.Marshal(RekeyRequest{\n\t\tCsrPEM: CertificateRequest{csr},\n\t})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\ttests := []struct {\n\t\tname       string\n\t\tinput      string\n\t\ttls        *tls.ConnectionState\n\t\tcert       *x509.Certificate\n\t\troot       *x509.Certificate\n\t\terr        error\n\t\tstatusCode int\n\t}{\n\t\t{\"ok\", string(valid), cs, parseCertificate(certPEM), parseCertificate(rootPEM), nil, http.StatusCreated},\n\t\t{\"no tls\", string(valid), nil, nil, nil, nil, http.StatusBadRequest},\n\t\t{\"no peer certificates\", string(valid), &tls.ConnectionState{}, nil, nil, nil, http.StatusBadRequest},\n\t\t{\"rekey error\", string(valid), cs, nil, nil, errs.Forbidden(\"an error\"), http.StatusForbidden},\n\t\t{\"json read error\", \"{\", cs, nil, nil, nil, http.StatusBadRequest},\n\t}\n\n\texpected := []byte(`{\"crt\":\"` + strings.ReplaceAll(certPEM, \"\\n\", `\\n`) + `\\n\",\"ca\":\"` + strings.ReplaceAll(rootPEM, \"\\n\", `\\n`) + `\\n\",\"certChain\":[\"` + strings.ReplaceAll(certPEM, \"\\n\", `\\n`) + `\\n\",\"` + strings.ReplaceAll(rootPEM, \"\\n\", `\\n`) + `\\n\"]}`)\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tmockMustAuthority(t, &mockAuthority{\n\t\t\t\tret1: tt.cert, ret2: tt.root, err: tt.err,\n\t\t\t\tgetTLSOptions: func() *authority.TLSOptions {\n\t\t\t\t\treturn nil\n\t\t\t\t},\n\t\t\t})\n\t\t\treq := httptest.NewRequest(\"POST\", \"http://example.com/rekey\", strings.NewReader(tt.input))\n\t\t\treq.TLS = tt.tls\n\t\t\tw := httptest.NewRecorder()\n\t\t\tRekey(logging.NewResponseLogger(w), req)\n\t\t\tres := w.Result()\n\n\t\t\tif res.StatusCode != tt.statusCode {\n\t\t\t\tt.Errorf(\"caHandler.Rekey StatusCode = %d, wants %d\", res.StatusCode, tt.statusCode)\n\t\t\t}\n\n\t\t\tbody, err := io.ReadAll(res.Body)\n\t\t\tres.Body.Close()\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"caHandler.Rekey unexpected error = %v\", err)\n\t\t\t}\n\t\t\tif tt.statusCode < http.StatusBadRequest {\n\t\t\t\tif !bytes.Equal(bytes.TrimSpace(body), expected) {\n\t\t\t\t\tt.Errorf(\"caHandler.Rekey Body = %s, wants %s\", body, expected)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_Provisioners(t *testing.T) {\n\ttype fields struct {\n\t\tAuthority Authority\n\t}\n\ttype args struct {\n\t\tw http.ResponseWriter\n\t\tr *http.Request\n\t}\n\n\treq, err := http.NewRequest(\"GET\", \"http://example.com/provisioners?cursor=foo&limit=20\", http.NoBody)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\treqLimitFail, err := http.NewRequest(\"GET\", \"http://example.com/provisioners?limit=abc\", http.NoBody)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tvar key jose.JSONWebKey\n\tif err := json.Unmarshal([]byte(pubKey), &key); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tp := provisioner.List{\n\t\t&provisioner.JWK{\n\t\t\tType:         \"JWK\",\n\t\t\tName:         \"max\",\n\t\t\tEncryptedKey: \"abc\",\n\t\t\tKey:          &key,\n\t\t},\n\t\t&provisioner.JWK{\n\t\t\tType:         \"JWK\",\n\t\t\tName:         \"mariano\",\n\t\t\tEncryptedKey: \"def\",\n\t\t\tKey:          &key,\n\t\t},\n\t}\n\tpr := ProvisionersResponse{\n\t\tProvisioners: p,\n\t}\n\n\ttests := []struct {\n\t\tname       string\n\t\tfields     fields\n\t\targs       args\n\t\tstatusCode int\n\t}{\n\t\t{\"ok\", fields{&mockAuthority{ret1: p, ret2: \"\"}}, args{httptest.NewRecorder(), req}, 200},\n\t\t{\"fail\", fields{&mockAuthority{ret1: p, ret2: \"\", err: fmt.Errorf(\"the error\")}}, args{httptest.NewRecorder(), req}, 500},\n\t\t{\"limit fail\", fields{&mockAuthority{ret1: p, ret2: \"\"}}, args{httptest.NewRecorder(), reqLimitFail}, 400},\n\t}\n\n\texpected, err := json.Marshal(pr)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\texpectedError400 := errs.BadRequest(\"limit 'abc' is not an integer\")\n\texpectedError400Bytes, err := json.Marshal(expectedError400)\n\trequire.NoError(t, err)\n\texpectedError500 := errs.InternalServer(\"force\")\n\texpectedError500Bytes, err := json.Marshal(expectedError500)\n\trequire.NoError(t, err)\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tmockMustAuthority(t, tt.fields.Authority)\n\t\t\tProvisioners(tt.args.w, tt.args.r)\n\n\t\t\trec := tt.args.w.(*httptest.ResponseRecorder)\n\t\t\tres := rec.Result()\n\t\t\tif res.StatusCode != tt.statusCode {\n\t\t\t\tt.Errorf(\"caHandler.Provisioners StatusCode = %d, wants %d\", res.StatusCode, tt.statusCode)\n\t\t\t}\n\t\t\tbody, err := io.ReadAll(res.Body)\n\t\t\tres.Body.Close()\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"caHandler.Provisioners unexpected error = %v\", err)\n\t\t\t}\n\t\t\tif tt.statusCode < http.StatusBadRequest {\n\t\t\t\tif !bytes.Equal(bytes.TrimSpace(body), expected) {\n\t\t\t\t\tt.Errorf(\"caHandler.Provisioners Body = %s, wants %s\", body, expected)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tswitch tt.statusCode {\n\t\t\t\tcase 400:\n\t\t\t\t\tif !bytes.Equal(bytes.TrimSpace(body), expectedError400Bytes) {\n\t\t\t\t\t\tt.Errorf(\"caHandler.Provisioners Body = %s, wants %s\", body, expectedError400Bytes)\n\t\t\t\t\t}\n\t\t\t\tcase 500:\n\t\t\t\t\tif !bytes.Equal(bytes.TrimSpace(body), expectedError500Bytes) {\n\t\t\t\t\t\tt.Errorf(\"caHandler.Provisioners Body = %s, wants %s\", body, expectedError500Bytes)\n\t\t\t\t\t}\n\t\t\t\tdefault:\n\t\t\t\t\tt.Errorf(\"caHandler.Provisioner unexpected status code = %d\", tt.statusCode)\n\t\t\t\t}\n\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_ProvisionerKey(t *testing.T) {\n\ttype fields struct {\n\t\tAuthority Authority\n\t}\n\ttype args struct {\n\t\tw http.ResponseWriter\n\t\tr *http.Request\n\t}\n\n\t// Request with chi context\n\tchiCtx := chi.NewRouteContext()\n\tchiCtx.URLParams.Add(\"kid\", \"oV1p0MJeGQ7qBlK6B-oyfVdBRjh_e7VSK_YSEEqgW00\")\n\treq := httptest.NewRequest(\"GET\", \"http://example.com/provisioners/oV1p0MJeGQ7qBlK6B-oyfVdBRjh_e7VSK_YSEEqgW00/encrypted-key\", http.NoBody)\n\treq = req.WithContext(context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx))\n\n\ttests := []struct {\n\t\tname       string\n\t\tfields     fields\n\t\targs       args\n\t\tstatusCode int\n\t}{\n\t\t{\"ok\", fields{&mockAuthority{ret1: privKey}}, args{httptest.NewRecorder(), req}, 200},\n\t\t{\"fail\", fields{&mockAuthority{ret1: \"\", err: fmt.Errorf(\"not found\")}}, args{httptest.NewRecorder(), req}, 404},\n\t}\n\n\texpected := []byte(`{\"key\":\"` + privKey + `\"}`)\n\texpectedError404 := errs.NotFound(\"force\")\n\texpectedError404Bytes, err := json.Marshal(expectedError404)\n\trequire.NoError(t, err)\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tmockMustAuthority(t, tt.fields.Authority)\n\t\t\tProvisionerKey(tt.args.w, tt.args.r)\n\n\t\t\trec := tt.args.w.(*httptest.ResponseRecorder)\n\t\t\tres := rec.Result()\n\t\t\tif res.StatusCode != tt.statusCode {\n\t\t\t\tt.Errorf(\"caHandler.Provisioners StatusCode = %d, wants %d\", res.StatusCode, tt.statusCode)\n\t\t\t}\n\t\t\tbody, err := io.ReadAll(res.Body)\n\t\t\tres.Body.Close()\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"caHandler.Provisioners unexpected error = %v\", err)\n\t\t\t}\n\t\t\tif tt.statusCode < http.StatusBadRequest {\n\t\t\t\tif !bytes.Equal(bytes.TrimSpace(body), expected) {\n\t\t\t\t\tt.Errorf(\"caHandler.Provisioners Body = %s, wants %s\", body, expected)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif !bytes.Equal(bytes.TrimSpace(body), expectedError404Bytes) {\n\t\t\t\t\tt.Errorf(\"caHandler.Provisioners Body = %s, wants %s\", body, expectedError404Bytes)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_Roots(t *testing.T) {\n\tcs := &tls.ConnectionState{\n\t\tPeerCertificates: []*x509.Certificate{parseCertificate(certPEM)},\n\t}\n\ttests := []struct {\n\t\tname       string\n\t\ttls        *tls.ConnectionState\n\t\tcert       *x509.Certificate\n\t\troot       *x509.Certificate\n\t\terr        error\n\t\tstatusCode int\n\t}{\n\t\t{\"ok\", cs, parseCertificate(certPEM), parseCertificate(rootPEM), nil, http.StatusCreated},\n\t\t{\"no peer certificates\", &tls.ConnectionState{}, parseCertificate(certPEM), parseCertificate(rootPEM), nil, http.StatusCreated},\n\t\t{\"fail\", cs, nil, nil, fmt.Errorf(\"an error\"), http.StatusForbidden},\n\t}\n\n\texpected := []byte(`{\"crts\":[\"` + strings.ReplaceAll(rootPEM, \"\\n\", `\\n`) + `\\n\"]}`)\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tmockMustAuthority(t, &mockAuthority{ret1: []*x509.Certificate{tt.root}, err: tt.err})\n\t\t\treq := httptest.NewRequest(\"GET\", \"http://example.com/roots\", http.NoBody)\n\t\t\treq.TLS = tt.tls\n\t\t\tw := httptest.NewRecorder()\n\t\t\tRoots(w, req)\n\t\t\tres := w.Result()\n\n\t\t\tif res.StatusCode != tt.statusCode {\n\t\t\t\tt.Errorf(\"caHandler.Roots StatusCode = %d, wants %d\", res.StatusCode, tt.statusCode)\n\t\t\t}\n\n\t\t\tbody, err := io.ReadAll(res.Body)\n\t\t\tres.Body.Close()\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"caHandler.Roots unexpected error = %v\", err)\n\t\t\t}\n\t\t\tif tt.statusCode < http.StatusBadRequest {\n\t\t\t\tif !bytes.Equal(bytes.TrimSpace(body), expected) {\n\t\t\t\t\tt.Errorf(\"caHandler.Roots Body = %s, wants %s\", body, expected)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_caHandler_RootsPEM(t *testing.T) {\n\tparsedRoot := parseCertificate(rootPEM)\n\ttests := []struct {\n\t\tname       string\n\t\troots      []*x509.Certificate\n\t\terr        error\n\t\tstatusCode int\n\t\texpect     string\n\t}{\n\t\t{\"one root\", []*x509.Certificate{parsedRoot}, nil, http.StatusOK, rootPEM},\n\t\t{\"two roots\", []*x509.Certificate{parsedRoot, parsedRoot}, nil, http.StatusOK, rootPEM + \"\\n\" + rootPEM},\n\t\t{\"fail\", nil, errors.New(\"an error\"), http.StatusInternalServerError, \"\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tmockMustAuthority(t, &mockAuthority{ret1: tt.roots, err: tt.err})\n\t\t\treq := httptest.NewRequest(\"GET\", \"https://example.com/roots\", http.NoBody)\n\t\t\tw := httptest.NewRecorder()\n\t\t\tRootsPEM(w, req)\n\t\t\tres := w.Result()\n\n\t\t\tif res.StatusCode != tt.statusCode {\n\t\t\t\tt.Errorf(\"caHandler.RootsPEM StatusCode = %d, wants %d\", res.StatusCode, tt.statusCode)\n\t\t\t}\n\n\t\t\tbody, err := io.ReadAll(res.Body)\n\t\t\tres.Body.Close()\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"caHandler.RootsPEM unexpected error = %v\", err)\n\t\t\t}\n\t\t\tif tt.statusCode < http.StatusBadRequest {\n\t\t\t\tif !bytes.Equal(bytes.TrimSpace(body), []byte(tt.expect)) {\n\t\t\t\t\tt.Errorf(\"caHandler.RootsPEM Body = %s, wants %s\", body, tt.expect)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_Federation(t *testing.T) {\n\tcs := &tls.ConnectionState{\n\t\tPeerCertificates: []*x509.Certificate{parseCertificate(certPEM)},\n\t}\n\ttests := []struct {\n\t\tname       string\n\t\ttls        *tls.ConnectionState\n\t\tcert       *x509.Certificate\n\t\troot       *x509.Certificate\n\t\terr        error\n\t\tstatusCode int\n\t}{\n\t\t{\"ok\", cs, parseCertificate(certPEM), parseCertificate(rootPEM), nil, http.StatusCreated},\n\t\t{\"no peer certificates\", &tls.ConnectionState{}, parseCertificate(certPEM), parseCertificate(rootPEM), nil, http.StatusCreated},\n\t\t{\"fail\", cs, nil, nil, fmt.Errorf(\"an error\"), http.StatusForbidden},\n\t}\n\n\texpected := []byte(`{\"crts\":[\"` + strings.ReplaceAll(rootPEM, \"\\n\", `\\n`) + `\\n\"]}`)\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tmockMustAuthority(t, &mockAuthority{ret1: []*x509.Certificate{tt.root}, err: tt.err})\n\t\t\treq := httptest.NewRequest(\"GET\", \"http://example.com/federation\", http.NoBody)\n\t\t\treq.TLS = tt.tls\n\t\t\tw := httptest.NewRecorder()\n\t\t\tFederation(w, req)\n\t\t\tres := w.Result()\n\n\t\t\tif res.StatusCode != tt.statusCode {\n\t\t\t\tt.Errorf(\"caHandler.Federation StatusCode = %d, wants %d\", res.StatusCode, tt.statusCode)\n\t\t\t}\n\n\t\t\tbody, err := io.ReadAll(res.Body)\n\t\t\tres.Body.Close()\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"caHandler.Federation unexpected error = %v\", err)\n\t\t\t}\n\t\t\tif tt.statusCode < http.StatusBadRequest {\n\t\t\t\tif !bytes.Equal(bytes.TrimSpace(body), expected) {\n\t\t\t\t\tt.Errorf(\"caHandler.Federation Body = %s, wants %s\", body, expected)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_fmtPublicKey(t *testing.T) {\n\tp256, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\trsa2048, err := rsa.GenerateKey(rand.Reader, 2048)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tedPub, edPriv, err := ed25519.GenerateKey(rand.Reader)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tvar dsa2048 dsa.PrivateKey\n\tif err := dsa.GenerateParameters(&dsa2048.Parameters, rand.Reader, dsa.L2048N256); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := dsa.GenerateKey(&dsa2048, rand.Reader); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\ttype args struct {\n\t\tpub, priv interface{}\n\t\tcert      *x509.Certificate\n\t}\n\ttests := []struct {\n\t\tname string\n\t\targs args\n\t\twant string\n\t}{\n\t\t{\"p256\", args{p256.Public(), p256, nil}, \"ECDSA P-256\"},\n\t\t{\"rsa2048\", args{rsa2048.Public(), rsa2048, nil}, \"RSA 2048\"},\n\t\t{\"ed25519\", args{edPub, edPriv, nil}, \"Ed25519\"},\n\t\t{\"dsa2048\", args{cert: &x509.Certificate{PublicKeyAlgorithm: x509.DSA, PublicKey: &dsa2048.PublicKey}}, \"DSA 2048\"},\n\t\t{\"unknown\", args{cert: &x509.Certificate{PublicKeyAlgorithm: x509.ECDSA, PublicKey: []byte(\"12345678\")}}, \"ECDSA unknown\"},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tvar cert *x509.Certificate\n\t\t\tif tt.args.cert != nil {\n\t\t\t\tcert = tt.args.cert\n\t\t\t} else {\n\t\t\t\tcert = mustCertificate(t, tt.args.pub, tt.args.priv)\n\t\t\t}\n\t\t\tif got := fmtPublicKey(cert); got != tt.want {\n\t\t\t\tt.Errorf(\"fmtPublicKey() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc mustCertificate(t *testing.T, pub, priv interface{}) *x509.Certificate {\n\ttemplate := x509.Certificate{\n\t\tSerialNumber: big.NewInt(1),\n\t\tSubject: pkix.Name{\n\t\t\tOrganization: []string{\"Acme Co\"},\n\t\t},\n\t\tNotBefore: time.Now(),\n\t\tNotAfter:  time.Now().Add(24 * time.Hour),\n\n\t\tKeyUsage:              x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,\n\t\tExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},\n\t\tBasicConstraintsValid: true,\n\t}\n\n\tder, err := x509.CreateCertificate(rand.Reader, &template, &template, pub, priv)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tcert, err := x509.ParseCertificate(der)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn cert\n}\n\nfunc TestProvisionersResponse_MarshalJSON(t *testing.T) {\n\tk := map[string]any{\n\t\t\"use\": \"sig\",\n\t\t\"kty\": \"EC\",\n\t\t\"kid\": \"4UELJx8e0aS9m0CH3fZ0EB7D5aUPICb759zALHFejvc\",\n\t\t\"crv\": \"P-256\",\n\t\t\"alg\": \"ES256\",\n\t\t\"x\":   \"7ZdAAMZCFU4XwgblI5RfZouBi8lYmF6DlZusNNnsbm8\",\n\t\t\"y\":   \"sQr2JdzwD2fgyrymBEXWsxDxFNjjqN64qLLSbLdLZ9Y\",\n\t}\n\tkey := jose.JSONWebKey{}\n\tb, err := json.Marshal(k)\n\trequire.NoError(t, err)\n\terr = json.Unmarshal(b, &key)\n\trequire.NoError(t, err)\n\n\tr := ProvisionersResponse{\n\t\tProvisioners: provisioner.List{\n\t\t\t&provisioner.SCEP{\n\t\t\t\tName:                          \"scep\",\n\t\t\t\tType:                          \"scep\",\n\t\t\t\tChallengePassword:             \"not-so-secret\",\n\t\t\t\tMinimumPublicKeyLength:        2048,\n\t\t\t\tEncryptionAlgorithmIdentifier: 2,\n\t\t\t\tIncludeRoot:                   true,\n\t\t\t\tExcludeIntermediate:           true,\n\t\t\t\tDecrypterCertificate:          []byte{1, 2, 3, 4},\n\t\t\t\tDecrypterKeyPEM:               []byte{5, 6, 7, 8},\n\t\t\t\tDecrypterKeyURI:               \"softkms:path=/path/to/private.key\",\n\t\t\t\tDecrypterKeyPassword:          \"super-secret-password\",\n\t\t\t},\n\t\t\t&provisioner.JWK{\n\t\t\t\tEncryptedKey: \"eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJlbmMiOiJBMTI4R0NNIiwicDJjIjoxMDAwMDAsInAycyI6IlhOdmYxQjgxSUlLMFA2NUkwcmtGTGcifQ.XaN9zcPQeWt49zchUDm34FECUTHfQTn_.tmNHPQDqR3ebsWfd.9WZr3YVdeOyJh36vvx0VlRtluhvYp4K7jJ1KGDr1qypwZ3ziBVSNbYYQ71du7fTtrnfG1wgGTVR39tWSzBU-zwQ5hdV3rpMAaEbod5zeW6SHd95H3Bvcb43YiiqJFNL5sGZzFb7FqzVmpsZ1efiv6sZaGDHtnCAL6r12UG5EZuqGfM0jGCZitUz2m9TUKXJL5DJ7MOYbFfkCEsUBPDm_TInliSVn2kMJhFa0VOe5wZk5YOuYM3lNYW64HGtbf-llN2Xk-4O9TfeSPizBx9ZqGpeu8pz13efUDT2WL9tWo6-0UE-CrG0bScm8lFTncTkHcu49_a5NaUBkYlBjEiw.thPcx3t1AUcWuEygXIY3Fg\",\n\t\t\t\tKey:          &key,\n\t\t\t\tName:         \"step-cli\",\n\t\t\t\tType:         \"JWK\",\n\t\t\t},\n\t\t},\n\t\tNextCursor: \"next\",\n\t}\n\n\texpected := map[string]any{\n\t\t\"provisioners\": []map[string]any{\n\t\t\t{\n\t\t\t\t\"type\":                          \"scep\",\n\t\t\t\t\"name\":                          \"scep\",\n\t\t\t\t\"forceCN\":                       false,\n\t\t\t\t\"includeRoot\":                   true,\n\t\t\t\t\"excludeIntermediate\":           true,\n\t\t\t\t\"challenge\":                     \"*** REDACTED ***\",\n\t\t\t\t\"decrypterCertificate\":          []byte(\"*** REDACTED ***\"),\n\t\t\t\t\"decrypterKey\":                  \"*** REDACTED ***\",\n\t\t\t\t\"decrypterKeyPEM\":               []byte(\"*** REDACTED ***\"),\n\t\t\t\t\"decrypterKeyPassword\":          \"*** REDACTED ***\",\n\t\t\t\t\"minimumPublicKeyLength\":        2048,\n\t\t\t\t\"encryptionAlgorithmIdentifier\": 2,\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"type\": \"JWK\",\n\t\t\t\t\"name\": \"step-cli\",\n\t\t\t\t\"key\": map[string]any{\n\t\t\t\t\t\"use\": \"sig\",\n\t\t\t\t\t\"kty\": \"EC\",\n\t\t\t\t\t\"kid\": \"4UELJx8e0aS9m0CH3fZ0EB7D5aUPICb759zALHFejvc\",\n\t\t\t\t\t\"crv\": \"P-256\",\n\t\t\t\t\t\"alg\": \"ES256\",\n\t\t\t\t\t\"x\":   \"7ZdAAMZCFU4XwgblI5RfZouBi8lYmF6DlZusNNnsbm8\",\n\t\t\t\t\t\"y\":   \"sQr2JdzwD2fgyrymBEXWsxDxFNjjqN64qLLSbLdLZ9Y\",\n\t\t\t\t},\n\t\t\t\t\"encryptedKey\": \"eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJlbmMiOiJBMTI4R0NNIiwicDJjIjoxMDAwMDAsInAycyI6IlhOdmYxQjgxSUlLMFA2NUkwcmtGTGcifQ.XaN9zcPQeWt49zchUDm34FECUTHfQTn_.tmNHPQDqR3ebsWfd.9WZr3YVdeOyJh36vvx0VlRtluhvYp4K7jJ1KGDr1qypwZ3ziBVSNbYYQ71du7fTtrnfG1wgGTVR39tWSzBU-zwQ5hdV3rpMAaEbod5zeW6SHd95H3Bvcb43YiiqJFNL5sGZzFb7FqzVmpsZ1efiv6sZaGDHtnCAL6r12UG5EZuqGfM0jGCZitUz2m9TUKXJL5DJ7MOYbFfkCEsUBPDm_TInliSVn2kMJhFa0VOe5wZk5YOuYM3lNYW64HGtbf-llN2Xk-4O9TfeSPizBx9ZqGpeu8pz13efUDT2WL9tWo6-0UE-CrG0bScm8lFTncTkHcu49_a5NaUBkYlBjEiw.thPcx3t1AUcWuEygXIY3Fg\",\n\t\t\t},\n\t\t},\n\t\t\"nextCursor\": \"next\",\n\t}\n\n\texpBytes, err := json.Marshal(expected)\n\tassert.NoError(t, err)\n\n\tbr, err := r.MarshalJSON()\n\tassert.NoError(t, err)\n\tassert.JSONEq(t, string(expBytes), string(br))\n\n\tkeyCopy := key\n\texpList := provisioner.List{\n\t\t&provisioner.SCEP{\n\t\t\tName:                          \"scep\",\n\t\t\tType:                          \"scep\",\n\t\t\tChallengePassword:             \"not-so-secret\",\n\t\t\tMinimumPublicKeyLength:        2048,\n\t\t\tEncryptionAlgorithmIdentifier: 2,\n\t\t\tIncludeRoot:                   true,\n\t\t\tExcludeIntermediate:           true,\n\t\t\tDecrypterCertificate:          []byte{1, 2, 3, 4},\n\t\t\tDecrypterKeyPEM:               []byte{5, 6, 7, 8},\n\t\t\tDecrypterKeyURI:               \"softkms:path=/path/to/private.key\",\n\t\t\tDecrypterKeyPassword:          \"super-secret-password\",\n\t\t},\n\t\t&provisioner.JWK{\n\t\t\tEncryptedKey: \"eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJlbmMiOiJBMTI4R0NNIiwicDJjIjoxMDAwMDAsInAycyI6IlhOdmYxQjgxSUlLMFA2NUkwcmtGTGcifQ.XaN9zcPQeWt49zchUDm34FECUTHfQTn_.tmNHPQDqR3ebsWfd.9WZr3YVdeOyJh36vvx0VlRtluhvYp4K7jJ1KGDr1qypwZ3ziBVSNbYYQ71du7fTtrnfG1wgGTVR39tWSzBU-zwQ5hdV3rpMAaEbod5zeW6SHd95H3Bvcb43YiiqJFNL5sGZzFb7FqzVmpsZ1efiv6sZaGDHtnCAL6r12UG5EZuqGfM0jGCZitUz2m9TUKXJL5DJ7MOYbFfkCEsUBPDm_TInliSVn2kMJhFa0VOe5wZk5YOuYM3lNYW64HGtbf-llN2Xk-4O9TfeSPizBx9ZqGpeu8pz13efUDT2WL9tWo6-0UE-CrG0bScm8lFTncTkHcu49_a5NaUBkYlBjEiw.thPcx3t1AUcWuEygXIY3Fg\",\n\t\t\tKey:          &keyCopy,\n\t\t\tName:         \"step-cli\",\n\t\t\tType:         \"JWK\",\n\t\t},\n\t}\n\n\t// MarshalJSON must not affect the struct properties itself\n\tassert.Equal(t, expList, r.Provisioners)\n}\n\nconst (\n\tfixtureECDSACertificate = `ecdsa-sha2-nistp256-cert-v01@openssh.com AAAAKGVjZHNhLXNoYTItbmlzdHAyNTYtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgLnkvSk4odlo3b1R+RDw+LmorL3RkN354IilCIVFVen4AAAAIbmlzdHAyNTYAAABBBHjKHss8WM2ffMYlavisoLXR0I6UEIU+cidV1ogEH1U6+/SYaFPrlzQo0tGLM5CNkMbhInbyasQsrHzn8F1Rt7nHg5/tcSf9qwAAAAEAAAAGaGVybWFuAAAACgAAAAZoZXJtYW4AAAAAY8kvJwAAAABjyhBjAAAAAAAAAIIAAAAVcGVybWl0LVgxMS1mb3J3YXJkaW5nAAAAAAAAABdwZXJtaXQtYWdlbnQtZm9yd2FyZGluZwAAAAAAAAAWcGVybWl0LXBvcnQtZm9yd2FyZGluZwAAAAAAAAAKcGVybWl0LXB0eQAAAAAAAAAOcGVybWl0LXVzZXItcmMAAAAAAAAAAAAAAGgAAAATZWNkc2Etc2hhMi1uaXN0cDI1NgAAAAhuaXN0cDI1NgAAAEEE/ayqpPrZZF5uA1UlDt4FreTf15agztQIzpxnWq/XoxAHzagRSkFGkdgFpjgsfiRpP8URHH3BZScqc0ZDCTxhoQAAAGQAAAATZWNkc2Etc2hhMi1uaXN0cDI1NgAAAEkAAAAhAJuP1wCVwoyrKrEtHGfFXrVbRHySDjvXtS1tVTdHyqymAAAAIBa/CSSzfZb4D2NLP+eEmOOMJwSjYOiNM8fiOoAaqglI herman`\n)\n\nfunc TestLogSSHCertificate(t *testing.T) {\n\n\tout, _, _, _, err := ssh.ParseAuthorizedKey([]byte(fixtureECDSACertificate))\n\trequire.NoError(t, err)\n\n\tcert, ok := out.(*ssh.Certificate)\n\trequire.True(t, ok)\n\n\tw := httptest.NewRecorder()\n\trl := logging.NewResponseLogger(w)\n\tLogSSHCertificate(rl, cert)\n\n\tassert.Equal(t, 200, w.Result().StatusCode)\n\n\tfields := rl.Fields()\n\tassert.Equal(t, uint64(14376510277651266987), fields[\"serial\"])\n\tassert.Equal(t, []string{\"herman\"}, fields[\"principals\"])\n\tassert.Equal(t, \"ecdsa-sha2-nistp256-cert-v01@openssh.com user certificate\", fields[\"certificate-type\"])\n\tassert.Equal(t, time.Unix(1674129191, 0).Format(time.RFC3339), fields[\"valid-from\"])\n\tassert.Equal(t, time.Unix(1674186851, 0).Format(time.RFC3339), fields[\"valid-to\"])\n\tassert.Equal(t, \"AAAAKGVjZHNhLXNoYTItbmlzdHAyNTYtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgLnkvSk4odlo3b1R+RDw+LmorL3RkN354IilCIVFVen4AAAAIbmlzdHAyNTYAAABBBHjKHss8WM2ffMYlavisoLXR0I6UEIU+cidV1ogEH1U6+/SYaFPrlzQo0tGLM5CNkMbhInbyasQsrHzn8F1Rt7nHg5/tcSf9qwAAAAEAAAAGaGVybWFuAAAACgAAAAZoZXJtYW4AAAAAY8kvJwAAAABjyhBjAAAAAAAAAIIAAAAVcGVybWl0LVgxMS1mb3J3YXJkaW5nAAAAAAAAABdwZXJtaXQtYWdlbnQtZm9yd2FyZGluZwAAAAAAAAAWcGVybWl0LXBvcnQtZm9yd2FyZGluZwAAAAAAAAAKcGVybWl0LXB0eQAAAAAAAAAOcGVybWl0LXVzZXItcmMAAAAAAAAAAAAAAGgAAAATZWNkc2Etc2hhMi1uaXN0cDI1NgAAAAhuaXN0cDI1NgAAAEEE/ayqpPrZZF5uA1UlDt4FreTf15agztQIzpxnWq/XoxAHzagRSkFGkdgFpjgsfiRpP8URHH3BZScqc0ZDCTxhoQAAAGQAAAATZWNkc2Etc2hhMi1uaXN0cDI1NgAAAEkAAAAhAJuP1wCVwoyrKrEtHGfFXrVbRHySDjvXtS1tVTdHyqymAAAAIBa/CSSzfZb4D2NLP+eEmOOMJwSjYOiNM8fiOoAaqglI\", fields[\"certificate\"])\n\tassert.Equal(t, \"SHA256:RvkDPGwl/G9d7LUFm1kmWhvOD9I/moPq4yxcb0STwr0 (ECDSA-CERT)\", fields[\"public-key\"])\n}\n\nfunc TestIntermediates(t *testing.T) {\n\tca, err := minica.New()\n\trequire.NoError(t, err)\n\n\tgetRequest := func(t *testing.T, crt []*x509.Certificate) *http.Request {\n\t\tmockMustAuthority(t, &mockAuthority{\n\t\t\tret1: crt,\n\t\t})\n\t\treturn httptest.NewRequest(\"GET\", \"/intermediates\", http.NoBody)\n\t}\n\n\ttype args struct {\n\t\tcrts []*x509.Certificate\n\t}\n\ttests := []struct {\n\t\tname           string\n\t\targs           args\n\t\twantStatusCode int\n\t\twantBody       []byte\n\t}{\n\t\t{\"ok\", args{[]*x509.Certificate{ca.Intermediate}}, http.StatusCreated, mustJSON(t, IntermediatesResponse{\n\t\t\tCertificates: []Certificate{{ca.Intermediate}},\n\t\t})},\n\t\t{\"ok multiple\", args{[]*x509.Certificate{ca.Root, ca.Intermediate}}, http.StatusCreated, mustJSON(t, IntermediatesResponse{\n\t\t\tCertificates: []Certificate{{ca.Root}, {ca.Intermediate}},\n\t\t})},\n\t\t{\"fail\", args{}, http.StatusNotImplemented, mustJSON(t, errs.NotImplemented(\"not implemented\"))},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tw := httptest.NewRecorder()\n\t\t\tr := getRequest(t, tt.args.crts)\n\t\t\tIntermediates(w, r)\n\t\t\tassert.Equal(t, tt.wantStatusCode, w.Result().StatusCode)\n\t\t\tassert.Equal(t, tt.wantBody, w.Body.Bytes())\n\t\t})\n\t}\n}\n\nfunc TestIntermediatesPEM(t *testing.T) {\n\tca, err := minica.New()\n\trequire.NoError(t, err)\n\n\tgetRequest := func(t *testing.T, crt []*x509.Certificate) *http.Request {\n\t\tmockMustAuthority(t, &mockAuthority{\n\t\t\tret1: crt,\n\t\t})\n\t\treturn httptest.NewRequest(\"GET\", \"/intermediates.pem\", http.NoBody)\n\t}\n\n\ttype args struct {\n\t\tcrts []*x509.Certificate\n\t}\n\ttests := []struct {\n\t\tname           string\n\t\targs           args\n\t\twantStatusCode int\n\t\twantBody       []byte\n\t}{\n\t\t{\"ok\", args{[]*x509.Certificate{ca.Intermediate}}, http.StatusOK, pem.EncodeToMemory(&pem.Block{\n\t\t\tType: \"CERTIFICATE\", Bytes: ca.Intermediate.Raw,\n\t\t})},\n\t\t{\"ok multiple\", args{[]*x509.Certificate{ca.Root, ca.Intermediate}}, http.StatusOK, append(pem.EncodeToMemory(&pem.Block{\n\t\t\tType: \"CERTIFICATE\", Bytes: ca.Root.Raw,\n\t\t}), pem.EncodeToMemory(&pem.Block{\n\t\t\tType: \"CERTIFICATE\", Bytes: ca.Intermediate.Raw,\n\t\t})...)},\n\t\t{\"fail\", args{}, http.StatusNotImplemented, mustJSON(t, errs.NotImplemented(\"not implemented\"))},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tw := httptest.NewRecorder()\n\t\t\tr := getRequest(t, tt.args.crts)\n\t\t\tIntermediatesPEM(w, r)\n\t\t\tassert.Equal(t, tt.wantStatusCode, w.Result().StatusCode)\n\t\t\tassert.Equal(t, tt.wantBody, w.Body.Bytes())\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "api/crl.go",
    "content": "package api\n\nimport (\n\t\"encoding/pem\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/smallstep/certificates/api/render\"\n\t\"github.com/smallstep/certificates/errs\"\n)\n\n// CRL is an HTTP handler that returns the current CRL in DER or PEM format\nfunc CRL(w http.ResponseWriter, r *http.Request) {\n\tcrlInfo, err := mustAuthority(r.Context()).GetCertificateRevocationList()\n\tif err != nil {\n\t\trender.Error(w, r, err)\n\t\treturn\n\t}\n\n\tif crlInfo == nil {\n\t\trender.Error(w, r, errs.New(http.StatusNotFound, \"no CRL available\"))\n\t\treturn\n\t}\n\n\texpires := crlInfo.ExpiresAt\n\tif expires.IsZero() {\n\t\texpires = time.Now()\n\t}\n\n\tw.Header().Add(\"Expires\", expires.Format(time.RFC1123))\n\n\t_, formatAsPEM := r.URL.Query()[\"pem\"]\n\tif formatAsPEM {\n\t\tw.Header().Add(\"Content-Type\", \"application/x-pem-file\")\n\t\tw.Header().Add(\"Content-Disposition\", \"attachment; filename=\\\"crl.pem\\\"\")\n\n\t\t_ = pem.Encode(w, &pem.Block{\n\t\t\tType:  \"X509 CRL\",\n\t\t\tBytes: crlInfo.Data,\n\t\t})\n\t} else {\n\t\tw.Header().Add(\"Content-Type\", \"application/pkix-crl\")\n\t\tw.Header().Add(\"Content-Disposition\", \"attachment; filename=\\\"crl.crl\\\"\")\n\t\tw.Write(crlInfo.Data)\n\t}\n}\n"
  },
  {
    "path": "api/crl_test.go",
    "content": "package api\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/pem\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-chi/chi/v5\"\n\t\"github.com/pkg/errors\"\n\t\"github.com/smallstep/certificates/authority\"\n\t\"github.com/smallstep/certificates/errs\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc Test_CRL(t *testing.T) {\n\tdata := []byte{1, 2, 3, 4}\n\tpemData := pem.EncodeToMemory(&pem.Block{\n\t\tType:  \"X509 CRL\",\n\t\tBytes: data,\n\t})\n\tpemData = bytes.TrimSpace(pemData)\n\temptyPEMData := pem.EncodeToMemory(&pem.Block{\n\t\tType:  \"X509 CRL\",\n\t\tBytes: nil,\n\t})\n\temptyPEMData = bytes.TrimSpace(emptyPEMData)\n\ttests := []struct {\n\t\tname              string\n\t\turl               string\n\t\terr               error\n\t\tstatusCode        int\n\t\tcrlInfo           *authority.CertificateRevocationListInfo\n\t\texpectedBody      []byte\n\t\texpectedHeaders   http.Header\n\t\texpectedErrorJSON string\n\t}{\n\t\t{\"ok\", \"http://example.com/crl\", nil, http.StatusOK, &authority.CertificateRevocationListInfo{Data: data}, data, http.Header{\"Content-Type\": []string{\"application/pkix-crl\"}, \"Content-Disposition\": []string{`attachment; filename=\"crl.crl\"`}}, \"\"},\n\t\t{\"ok/pem\", \"http://example.com/crl?pem=true\", nil, http.StatusOK, &authority.CertificateRevocationListInfo{Data: data}, pemData, http.Header{\"Content-Type\": []string{\"application/x-pem-file\"}, \"Content-Disposition\": []string{`attachment; filename=\"crl.pem\"`}}, \"\"},\n\t\t{\"ok/empty\", \"http://example.com/crl\", nil, http.StatusOK, &authority.CertificateRevocationListInfo{Data: nil}, nil, http.Header{\"Content-Type\": []string{\"application/pkix-crl\"}, \"Content-Disposition\": []string{`attachment; filename=\"crl.crl\"`}}, \"\"},\n\t\t{\"ok/empty-pem\", \"http://example.com/crl?pem=true\", nil, http.StatusOK, &authority.CertificateRevocationListInfo{Data: nil}, emptyPEMData, http.Header{\"Content-Type\": []string{\"application/x-pem-file\"}, \"Content-Disposition\": []string{`attachment; filename=\"crl.pem\"`}}, \"\"},\n\t\t{\"fail/internal\", \"http://example.com/crl\", errs.Wrap(http.StatusInternalServerError, errors.New(\"failure\"), \"authority.GetCertificateRevocationList\"), http.StatusInternalServerError, nil, nil, http.Header{}, `{\"status\":500,\"message\":\"The certificate authority encountered an Internal Server Error. Please see the certificate authority logs for more info.\"}`},\n\t\t{\"fail/nil\", \"http://example.com/crl\", nil, http.StatusNotFound, nil, nil, http.Header{}, `{\"status\":404,\"message\":\"no CRL available\"}`},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tmockMustAuthority(t, &mockAuthority{ret1: tt.crlInfo, err: tt.err})\n\n\t\t\tchiCtx := chi.NewRouteContext()\n\t\t\treq := httptest.NewRequest(\"GET\", tt.url, http.NoBody)\n\t\t\treq = req.WithContext(context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx))\n\t\t\tw := httptest.NewRecorder()\n\t\t\tCRL(w, req)\n\t\t\tres := w.Result()\n\n\t\t\tassert.Equal(t, tt.statusCode, res.StatusCode)\n\n\t\t\tbody, err := io.ReadAll(res.Body)\n\t\t\tres.Body.Close()\n\t\t\trequire.NoError(t, err)\n\n\t\t\tif tt.statusCode >= 300 {\n\t\t\t\tassert.JSONEq(t, tt.expectedErrorJSON, string(bytes.TrimSpace(body)))\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t// check expected header values\n\t\t\tfor _, h := range []string{\"content-type\", \"content-disposition\"} {\n\t\t\t\tv := tt.expectedHeaders.Get(h)\n\t\t\t\trequire.NotEmpty(t, v)\n\n\t\t\t\tactual := res.Header.Get(h)\n\t\t\t\tassert.Equal(t, v, actual)\n\t\t\t}\n\n\t\t\t// check expires header value\n\t\t\tassert.NotEmpty(t, res.Header.Get(\"expires\"))\n\t\t\tt1, err := time.Parse(time.RFC1123, res.Header.Get(\"expires\"))\n\t\t\tif assert.NoError(t, err) {\n\t\t\t\tassert.False(t, t1.IsZero())\n\t\t\t}\n\n\t\t\t// check body contents\n\t\t\tassert.Equal(t, tt.expectedBody, bytes.TrimSpace(body))\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "api/log/log.go",
    "content": "// Package log implements API-related logging helpers.\npackage log\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"os\"\n\n\t\"github.com/pkg/errors\"\n)\n\ntype errorLoggerKey struct{}\n\n// ErrorLogger is the function type used to log errors.\ntype ErrorLogger func(http.ResponseWriter, *http.Request, error)\n\nfunc (fn ErrorLogger) call(w http.ResponseWriter, r *http.Request, err error) {\n\tif fn == nil {\n\t\treturn\n\t}\n\tfn(w, r, err)\n}\n\n// WithErrorLogger returns a new context with the given error logger.\nfunc WithErrorLogger(ctx context.Context, fn ErrorLogger) context.Context {\n\treturn context.WithValue(ctx, errorLoggerKey{}, fn)\n}\n\n// ErrorLoggerFromContext returns an error logger from the context.\nfunc ErrorLoggerFromContext(ctx context.Context) (fn ErrorLogger) {\n\tfn, _ = ctx.Value(errorLoggerKey{}).(ErrorLogger)\n\treturn\n}\n\n// StackTracedError is the set of errors implementing the StackTrace function.\n//\n// Errors implementing this interface have their stack traces logged when passed\n// to the Error function of this package.\ntype StackTracedError interface {\n\terror\n\n\tStackTrace() errors.StackTrace\n}\n\ntype fieldCarrier interface {\n\tWithFields(map[string]any)\n\tFields() map[string]any\n}\n\n// Error adds to the response writer the given error if it implements\n// logging.ResponseLogger. If it does not implement it, then writes the error\n// using the log package.\nfunc Error(w http.ResponseWriter, r *http.Request, err error) {\n\tErrorLoggerFromContext(r.Context()).call(w, r, err)\n\n\tfc, ok := w.(fieldCarrier)\n\tif !ok {\n\t\treturn\n\t}\n\n\tfc.WithFields(map[string]any{\n\t\t\"error\": err,\n\t})\n\n\tif os.Getenv(\"STEPDEBUG\") != \"1\" {\n\t\treturn\n\t}\n\n\tvar st StackTracedError\n\tif errors.As(err, &st) {\n\t\tfc.WithFields(map[string]any{\n\t\t\t\"stack-trace\": fmt.Sprintf(\"%+v\", st.StackTrace()),\n\t\t})\n\t}\n}\n\n// EnabledResponse log the response object if it implements the EnableLogger\n// interface.\nfunc EnabledResponse(rw http.ResponseWriter, r *http.Request, v any) {\n\ttype enableLogger interface {\n\t\tToLog() (any, error)\n\t}\n\n\tif el, ok := v.(enableLogger); ok {\n\t\tout, err := el.ToLog()\n\t\tif err != nil {\n\t\t\tError(rw, r, err)\n\n\t\t\treturn\n\t\t}\n\n\t\tif rl, ok := rw.(fieldCarrier); ok {\n\t\t\trl.WithFields(map[string]any{\n\t\t\t\t\"response\": out,\n\t\t\t})\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "api/log/log_test.go",
    "content": "package log\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"log/slog\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\t\"unsafe\"\n\n\tpkgerrors \"github.com/pkg/errors\"\n\t\"github.com/stretchr/testify/assert\"\n\n\t\"github.com/smallstep/certificates/logging\"\n)\n\ntype stackTracedError struct{}\n\nfunc (stackTracedError) Error() string {\n\treturn \"a stacktraced error\"\n}\n\nfunc (stackTracedError) StackTrace() pkgerrors.StackTrace {\n\tf := struct{}{}\n\treturn pkgerrors.StackTrace{ // fake stacktrace\n\t\tpkgerrors.Frame(unsafe.Pointer(&f)),\n\t\tpkgerrors.Frame(unsafe.Pointer(&f)),\n\t}\n}\n\nfunc TestError(t *testing.T) {\n\tvar buf bytes.Buffer\n\tlogger := slog.New(slog.NewJSONHandler(&buf, &slog.HandlerOptions{}))\n\treq := httptest.NewRequest(\"GET\", \"/test\", http.NoBody)\n\treqWithLogger := req.WithContext(WithErrorLogger(req.Context(), func(w http.ResponseWriter, r *http.Request, err error) {\n\t\tif err != nil {\n\t\t\tlogger.ErrorContext(r.Context(), \"request failed\", slog.Any(\"error\", err))\n\t\t}\n\t}))\n\n\ttests := []struct {\n\t\tname string\n\t\terror\n\t\trw               http.ResponseWriter\n\t\tr                *http.Request\n\t\tisFieldCarrier   bool\n\t\tisSlogLogger     bool\n\t\tstepDebug        bool\n\t\texpectStackTrace bool\n\t}{\n\t\t{\"noLogger\", nil, nil, req, false, false, false, false},\n\t\t{\"noError\", nil, logging.NewResponseLogger(httptest.NewRecorder()), req, true, false, false, false},\n\t\t{\"noErrorDebug\", nil, logging.NewResponseLogger(httptest.NewRecorder()), req, true, false, true, false},\n\t\t{\"anError\", assert.AnError, logging.NewResponseLogger(httptest.NewRecorder()), req, true, false, false, false},\n\t\t{\"anErrorDebug\", assert.AnError, logging.NewResponseLogger(httptest.NewRecorder()), req, true, false, true, false},\n\t\t{\"stackTracedError\", new(stackTracedError), logging.NewResponseLogger(httptest.NewRecorder()), req, true, false, true, true},\n\t\t{\"stackTracedErrorDebug\", new(stackTracedError), logging.NewResponseLogger(httptest.NewRecorder()), req, true, false, true, true},\n\t\t{\"slogWithNoError\", nil, logging.NewResponseLogger(httptest.NewRecorder()), reqWithLogger, true, true, false, false},\n\t\t{\"slogWithError\", assert.AnError, logging.NewResponseLogger(httptest.NewRecorder()), reqWithLogger, true, true, false, false},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif tt.stepDebug {\n\t\t\t\tt.Setenv(\"STEPDEBUG\", \"1\")\n\t\t\t} else {\n\t\t\t\tt.Setenv(\"STEPDEBUG\", \"0\")\n\t\t\t}\n\n\t\t\tError(tt.rw, tt.r, tt.error)\n\n\t\t\t// return early if test case doesn't use logger\n\t\t\tif !tt.isFieldCarrier && !tt.isSlogLogger {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif tt.isFieldCarrier {\n\t\t\t\tfields := tt.rw.(logging.ResponseLogger).Fields()\n\n\t\t\t\t// expect the error field to be (not) set and to be the same error that was fed to Error\n\t\t\t\tif tt.error == nil {\n\t\t\t\t\tassert.Nil(t, fields[\"error\"])\n\t\t\t\t} else {\n\t\t\t\t\tassert.Same(t, tt.error, fields[\"error\"])\n\t\t\t\t}\n\n\t\t\t\t// check if stack-trace is set when expected\n\t\t\t\tif _, hasStackTrace := fields[\"stack-trace\"]; tt.expectStackTrace && !hasStackTrace {\n\t\t\t\t\tt.Error(`ResponseLogger[\"stack-trace\"] not set`)\n\t\t\t\t} else if !tt.expectStackTrace && hasStackTrace {\n\t\t\t\t\tt.Error(`ResponseLogger[\"stack-trace\"] was set`)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif tt.isSlogLogger {\n\t\t\t\tb := buf.Bytes()\n\t\t\t\tif tt.error == nil {\n\t\t\t\t\tassert.Empty(t, b)\n\t\t\t\t} else if assert.NotEmpty(t, b) {\n\t\t\t\t\tvar m map[string]any\n\t\t\t\t\tassert.NoError(t, json.Unmarshal(b, &m))\n\t\t\t\t\tassert.Equal(t, tt.error.Error(), m[\"error\"])\n\t\t\t\t}\n\t\t\t\tbuf.Reset()\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "api/models/scep.go",
    "content": "package models\n\nimport (\n\t\"context\"\n\t\"crypto/x509\"\n\t\"errors\"\n\n\t\"github.com/smallstep/certificates/authority/provisioner\"\n\t\"golang.org/x/crypto/ssh\"\n)\n\nvar errDummyImplementation = errors.New(\"dummy implementation\")\n\n// SCEP is the SCEP provisioner model used solely in CA API\n// responses. All methods for the [provisioner.Interface] interface\n// are implemented, but return a dummy error.\n// TODO(hs): remove reliance on the interface for the API responses\ntype SCEP struct {\n\tID                            string               `json:\"-\"`\n\tType                          string               `json:\"type\"`\n\tName                          string               `json:\"name\"`\n\tForceCN                       bool                 `json:\"forceCN\"`\n\tChallengePassword             string               `json:\"challenge\"`\n\tCapabilities                  []string             `json:\"capabilities,omitempty\"`\n\tIncludeRoot                   bool                 `json:\"includeRoot\"`\n\tExcludeIntermediate           bool                 `json:\"excludeIntermediate\"`\n\tMinimumPublicKeyLength        int                  `json:\"minimumPublicKeyLength\"`\n\tDecrypterCertificate          []byte               `json:\"decrypterCertificate\"`\n\tDecrypterKeyPEM               []byte               `json:\"decrypterKeyPEM\"`\n\tDecrypterKeyURI               string               `json:\"decrypterKey\"`\n\tDecrypterKeyPassword          string               `json:\"decrypterKeyPassword\"`\n\tEncryptionAlgorithmIdentifier int                  `json:\"encryptionAlgorithmIdentifier\"`\n\tOptions                       *provisioner.Options `json:\"options,omitempty\"`\n\tClaims                        *provisioner.Claims  `json:\"claims,omitempty\"`\n}\n\n// GetID returns the provisioner unique identifier.\nfunc (s *SCEP) GetID() string {\n\tif s.ID != \"\" {\n\t\treturn s.ID\n\t}\n\treturn s.GetIDForToken()\n}\n\n// GetIDForToken returns an identifier that will be used to load the provisioner\n// from a token.\nfunc (s *SCEP) GetIDForToken() string {\n\treturn \"scep/\" + s.Name\n}\n\n// GetName returns the name of the provisioner.\nfunc (s *SCEP) GetName() string {\n\treturn s.Name\n}\n\n// GetType returns the type of provisioner.\nfunc (s *SCEP) GetType() provisioner.Type {\n\treturn provisioner.TypeSCEP\n}\n\n// GetEncryptedKey returns the base provisioner encrypted key if it's defined.\nfunc (s *SCEP) GetEncryptedKey() (string, string, bool) {\n\treturn \"\", \"\", false\n}\n\n// GetTokenID returns the identifier of the token.\nfunc (s *SCEP) GetTokenID(string) (string, error) {\n\treturn \"\", errDummyImplementation\n}\n\n// Init initializes and validates the fields of a SCEP type.\nfunc (s *SCEP) Init(_ provisioner.Config) (err error) {\n\treturn errDummyImplementation\n}\n\n// AuthorizeSign returns an unimplemented error. Provisioners should overwrite\n// this method if they will support authorizing tokens for signing x509 Certificates.\nfunc (s *SCEP) AuthorizeSign(context.Context, string) ([]provisioner.SignOption, error) {\n\treturn nil, errDummyImplementation\n}\n\n// AuthorizeRevoke returns an unimplemented error. Provisioners should overwrite\n// this method if they will support authorizing tokens for revoking x509 Certificates.\nfunc (s *SCEP) AuthorizeRevoke(context.Context, string) error {\n\treturn errDummyImplementation\n}\n\n// AuthorizeRenew returns an unimplemented error. Provisioners should overwrite\n// this method if they will support authorizing tokens for renewing x509 Certificates.\nfunc (s *SCEP) AuthorizeRenew(context.Context, *x509.Certificate) error {\n\treturn errDummyImplementation\n}\n\n// AuthorizeSSHSign returns an unimplemented error. Provisioners should overwrite\n// this method if they will support authorizing tokens for signing SSH Certificates.\nfunc (s *SCEP) AuthorizeSSHSign(context.Context, string) ([]provisioner.SignOption, error) {\n\treturn nil, errDummyImplementation\n}\n\n// AuthorizeSSHRevoke returns an unimplemented error. Provisioners should overwrite\n// this method if they will support authorizing tokens for revoking SSH Certificates.\nfunc (s *SCEP) AuthorizeSSHRevoke(context.Context, string) error {\n\treturn errDummyImplementation\n}\n\n// AuthorizeSSHRenew returns an unimplemented error. Provisioners should overwrite\n// this method if they will support authorizing tokens for renewing SSH Certificates.\nfunc (s *SCEP) AuthorizeSSHRenew(context.Context, string) (*ssh.Certificate, error) {\n\treturn nil, errDummyImplementation\n}\n\n// AuthorizeSSHRekey returns an unimplemented error. Provisioners should overwrite\n// this method if they will support authorizing tokens for rekeying SSH Certificates.\nfunc (s *SCEP) AuthorizeSSHRekey(context.Context, string) (*ssh.Certificate, []provisioner.SignOption, error) {\n\treturn nil, nil, errDummyImplementation\n}\n\nvar _ provisioner.Interface = (*SCEP)(nil)\n"
  },
  {
    "path": "api/read/read.go",
    "content": "// Package read implements request object readers.\npackage read\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"io\"\n\t\"net/http\"\n\t\"strings\"\n\n\t\"google.golang.org/protobuf/encoding/protojson\"\n\t\"google.golang.org/protobuf/proto\"\n\n\t\"github.com/smallstep/certificates/api/render\"\n\t\"github.com/smallstep/certificates/errs\"\n)\n\n// JSON reads JSON from the request body and stores it in the value\n// pointed to by v.\nfunc JSON(r io.Reader, v interface{}) error {\n\tif err := json.NewDecoder(r).Decode(v); err != nil {\n\t\treturn errs.BadRequestErr(err, \"error decoding json\")\n\t}\n\treturn nil\n}\n\n// ProtoJSON reads JSON from the request body and stores it in the value\n// pointed to by m.\nfunc ProtoJSON(r io.Reader, m proto.Message) error {\n\tdata, err := io.ReadAll(r)\n\tif err != nil {\n\t\treturn errs.BadRequestErr(err, \"error reading request body\")\n\t}\n\n\tswitch err := protojson.Unmarshal(data, m); {\n\tcase errors.Is(err, proto.Error):\n\t\treturn badProtoJSONError(err.Error())\n\tdefault:\n\t\treturn err\n\t}\n}\n\n// badProtoJSONError is an error type that is returned by ProtoJSON\n// when a proto message cannot be unmarshaled. Usually this is caused\n// by an error in the request body.\ntype badProtoJSONError string\n\n// Error implements error for badProtoJSONError\nfunc (e badProtoJSONError) Error() string {\n\treturn string(e)\n}\n\n// Render implements render.RenderableError for badProtoJSONError\nfunc (e badProtoJSONError) Render(w http.ResponseWriter, r *http.Request) {\n\tv := struct {\n\t\tType    string `json:\"type\"`\n\t\tDetail  string `json:\"detail\"`\n\t\tMessage string `json:\"message\"`\n\t}{\n\t\tType:   \"badRequest\",\n\t\tDetail: \"bad request\",\n\t\t// trim the proto prefix for the message\n\t\tMessage: strings.TrimSpace(strings.TrimPrefix(e.Error(), \"proto:\")),\n\t}\n\trender.JSONStatus(w, r, v, http.StatusBadRequest)\n}\n"
  },
  {
    "path": "api/read/read_test.go",
    "content": "package read\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"reflect\"\n\t\"strings\"\n\t\"testing\"\n\t\"testing/iotest\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"google.golang.org/protobuf/proto\"\n\t\"google.golang.org/protobuf/reflect/protoreflect\"\n\n\t\"github.com/smallstep/linkedca\"\n\n\t\"github.com/smallstep/certificates/errs\"\n)\n\nfunc TestJSON(t *testing.T) {\n\ttype args struct {\n\t\tr io.Reader\n\t\tv interface{}\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\targs    args\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", args{strings.NewReader(`{\"foo\":\"bar\"}`), make(map[string]interface{})}, false},\n\t\t{\"fail\", args{strings.NewReader(`{\"foo\"}`), make(map[string]interface{})}, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\terr := JSON(tt.args.r, &tt.args.v)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"JSON() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\n\t\t\tif tt.wantErr {\n\t\t\t\tvar e *errs.Error\n\t\t\t\tif errors.As(err, &e) {\n\t\t\t\t\tif code := e.StatusCode(); code != 400 {\n\t\t\t\t\t\tt.Errorf(\"error.StatusCode() = %v, wants 400\", code)\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tt.Errorf(\"error type = %T, wants *Error\", err)\n\t\t\t\t}\n\t\t\t} else if !reflect.DeepEqual(tt.args.v, map[string]interface{}{\"foo\": \"bar\"}) {\n\t\t\t\tt.Errorf(\"JSON value = %v, wants %v\", tt.args.v, map[string]interface{}{\"foo\": \"bar\"})\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestProtoJSON(t *testing.T) {\n\n\tp := new(linkedca.Policy) // TODO(hs): can we use something different, so we don't need the import?\n\n\ttype args struct {\n\t\tr io.Reader\n\t\tm proto.Message\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\targs    args\n\t\twantErr bool\n\t}{\n\t\t{\n\t\t\tname: \"fail/io.ReadAll\",\n\t\t\targs: args{\n\t\t\t\tr: iotest.ErrReader(errors.New(\"read error\")),\n\t\t\t\tm: p,\n\t\t\t},\n\t\t\twantErr: true,\n\t\t},\n\t\t{\n\t\t\tname: \"fail/proto\",\n\t\t\targs: args{\n\t\t\t\tr: strings.NewReader(`{?}`),\n\t\t\t\tm: p,\n\t\t\t},\n\t\t\twantErr: true,\n\t\t},\n\t\t{\n\t\t\tname: \"ok\",\n\t\t\targs: args{\n\t\t\t\tr: strings.NewReader(`{\"x509\":{}}`),\n\t\t\t\tm: p,\n\t\t\t},\n\t\t\twantErr: false,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\terr := ProtoJSON(tt.args.r, tt.args.m)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"ProtoJSON() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\n\t\t\tif tt.wantErr {\n\t\t\t\tvar (\n\t\t\t\t\tee  *errs.Error\n\t\t\t\t\tbpe badProtoJSONError\n\t\t\t\t)\n\t\t\t\tswitch {\n\t\t\t\tcase errors.As(err, &bpe):\n\t\t\t\t\tassert.Contains(t, err.Error(), \"syntax error\")\n\t\t\t\tcase errors.As(err, &ee):\n\t\t\t\t\tassert.Equal(t, http.StatusBadRequest, ee.Status)\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.Equal(t, protoreflect.FullName(\"linkedca.Policy\"), proto.MessageName(tt.args.m))\n\t\t\tassert.True(t, proto.Equal(&linkedca.Policy{X509: &linkedca.X509Policy{}}, tt.args.m))\n\t\t})\n\t}\n}\n\nfunc Test_badProtoJSONError_Render(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\te        badProtoJSONError\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname:     \"bad proto normal space\",\n\t\t\te:        badProtoJSONError(\"proto: syntax error (line 1:2): invalid value ?\"),\n\t\t\texpected: \"syntax error (line 1:2): invalid value ?\",\n\t\t},\n\t\t{\n\t\t\tname:     \"bad proto non breaking space\",\n\t\t\te:        badProtoJSONError(\"proto: syntax error (line 1:2): invalid value ?\"),\n\t\t\texpected: \"syntax error (line 1:2): invalid value ?\",\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\n\t\t\tw := httptest.NewRecorder()\n\t\t\tr := httptest.NewRequest(\"POST\", \"/test\", http.NoBody)\n\t\t\ttt.e.Render(w, r)\n\t\t\tres := w.Result()\n\t\t\tdefer res.Body.Close()\n\n\t\t\tdata, err := io.ReadAll(res.Body)\n\t\t\tassert.NoError(t, err)\n\n\t\t\tv := struct {\n\t\t\t\tType    string `json:\"type\"`\n\t\t\t\tDetail  string `json:\"detail\"`\n\t\t\t\tMessage string `json:\"message\"`\n\t\t\t}{}\n\n\t\t\tassert.NoError(t, json.Unmarshal(data, &v))\n\t\t\tassert.Equal(t, \"badRequest\", v.Type)\n\t\t\tassert.Equal(t, \"bad request\", v.Detail)\n\t\t\tassert.Equal(t, \"syntax error (line 1:2): invalid value ?\", v.Message)\n\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "api/rekey.go",
    "content": "package api\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/smallstep/certificates/api/read\"\n\t\"github.com/smallstep/certificates/api/render\"\n\t\"github.com/smallstep/certificates/errs\"\n)\n\n// RekeyRequest is the request body for a certificate rekey request.\ntype RekeyRequest struct {\n\tCsrPEM CertificateRequest `json:\"csr\"`\n}\n\n// Validate checks the fields of the RekeyRequest and returns nil if they are ok\n// or an error if something is wrong.\nfunc (s *RekeyRequest) Validate() error {\n\tif s.CsrPEM.CertificateRequest == nil {\n\t\treturn errs.BadRequest(\"missing csr\")\n\t}\n\tif err := s.CsrPEM.CertificateRequest.CheckSignature(); err != nil {\n\t\treturn errs.BadRequestErr(err, \"invalid csr\")\n\t}\n\n\treturn nil\n}\n\n// Rekey is similar to renew except that the certificate will be renewed with new key from csr.\nfunc Rekey(w http.ResponseWriter, r *http.Request) {\n\tif r.TLS == nil || len(r.TLS.PeerCertificates) == 0 {\n\t\trender.Error(w, r, errs.BadRequest(\"missing client certificate\"))\n\t\treturn\n\t}\n\n\tvar body RekeyRequest\n\tif err := read.JSON(r.Body, &body); err != nil {\n\t\trender.Error(w, r, errs.BadRequestErr(err, \"error reading request body\"))\n\t\treturn\n\t}\n\n\tif err := body.Validate(); err != nil {\n\t\trender.Error(w, r, err)\n\t\treturn\n\t}\n\n\ta := mustAuthority(r.Context())\n\tcertChain, err := a.Rekey(r.TLS.PeerCertificates[0], body.CsrPEM.CertificateRequest.PublicKey)\n\tif err != nil {\n\t\trender.Error(w, r, errs.Wrap(http.StatusInternalServerError, err, \"cahandler.Rekey\"))\n\t\treturn\n\t}\n\tcertChainPEM := certChainToPEM(certChain)\n\tvar caPEM Certificate\n\tif len(certChainPEM) > 1 {\n\t\tcaPEM = certChainPEM[1]\n\t}\n\n\tLogCertificate(w, certChain[0])\n\trender.JSONStatus(w, r, &SignResponse{\n\t\tServerPEM:    certChainPEM[0],\n\t\tCaPEM:        caPEM,\n\t\tCertChainPEM: certChainPEM,\n\t\tTLSOptions:   a.GetTLSOptions(),\n\t}, http.StatusCreated)\n}\n"
  },
  {
    "path": "api/render/render.go",
    "content": "// Package render implements functionality related to response rendering.\npackage render\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"net/http\"\n\n\t\"google.golang.org/protobuf/encoding/protojson\"\n\t\"google.golang.org/protobuf/proto\"\n\n\t\"github.com/smallstep/certificates/api/log\"\n)\n\n// JSON is shorthand for JSONStatus(w, v, http.StatusOK).\nfunc JSON(w http.ResponseWriter, r *http.Request, v interface{}) {\n\tJSONStatus(w, r, v, http.StatusOK)\n}\n\n// JSONStatus marshals v into w. It additionally sets the status code of\n// w to the given one.\n//\n// JSONStatus sets the Content-Type of w to application/json unless one is\n// specified.\nfunc JSONStatus(w http.ResponseWriter, r *http.Request, v interface{}, status int) {\n\tsetContentTypeUnlessPresent(w, \"application/json\")\n\tw.WriteHeader(status)\n\n\tif err := json.NewEncoder(w).Encode(v); err != nil {\n\t\tvar errUnsupportedType *json.UnsupportedTypeError\n\t\tif errors.As(err, &errUnsupportedType) {\n\t\t\tpanic(err)\n\t\t}\n\n\t\tvar errUnsupportedValue *json.UnsupportedValueError\n\t\tif errors.As(err, &errUnsupportedValue) {\n\t\t\tpanic(err)\n\t\t}\n\n\t\tvar errMarshalError *json.MarshalerError\n\t\tif errors.As(err, &errMarshalError) {\n\t\t\tpanic(err)\n\t\t}\n\t}\n\n\tlog.EnabledResponse(w, r, v)\n}\n\n// ProtoJSON is shorthand for ProtoJSONStatus(w, m, http.StatusOK).\nfunc ProtoJSON(w http.ResponseWriter, m proto.Message) {\n\tProtoJSONStatus(w, m, http.StatusOK)\n}\n\n// ProtoJSONStatus writes the given value into the http.ResponseWriter and the\n// given status is written as the status code of the response.\nfunc ProtoJSONStatus(w http.ResponseWriter, m proto.Message, status int) {\n\tb, err := protojson.Marshal(m)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tsetContentTypeUnlessPresent(w, \"application/json\")\n\tw.WriteHeader(status)\n\t_, _ = w.Write(b)\n}\n\nfunc setContentTypeUnlessPresent(w http.ResponseWriter, contentType string) {\n\tconst header = \"Content-Type\"\n\n\th := w.Header()\n\tif _, ok := h[header]; !ok {\n\t\th.Set(header, contentType)\n\t}\n}\n\n// RenderableError is the set of errors that implement the basic Render method.\n//\n// Errors that implement this interface will use their own Render method when\n// being rendered into responses.\ntype RenderableError interface {\n\terror\n\n\tRender(http.ResponseWriter, *http.Request)\n}\n\n// Error marshals the JSON representation of err to w. In case err implements\n// RenderableError its own Render method will be called instead.\nfunc Error(rw http.ResponseWriter, r *http.Request, err error) {\n\tlog.Error(rw, r, err)\n\n\tvar re RenderableError\n\tif errors.As(err, &re) {\n\t\tre.Render(rw, r)\n\n\t\treturn\n\t}\n\n\tJSONStatus(rw, r, err, statusCodeFromError(err))\n}\n\n// StatusCodedError is the set of errors that implement the basic StatusCode\n// function.\n//\n// Errors that implement this interface will use the code reported by StatusCode\n// as the HTTP response code when being rendered by this package.\ntype StatusCodedError interface {\n\terror\n\n\tStatusCode() int\n}\n\nfunc statusCodeFromError(err error) (code int) {\n\tcode = http.StatusInternalServerError\n\n\ttype causer interface {\n\t\tCause() error\n\t}\n\n\tfor err != nil {\n\t\tvar sc StatusCodedError\n\t\tif errors.As(err, &sc) {\n\t\t\tcode = sc.StatusCode()\n\n\t\t\tbreak\n\t\t}\n\n\t\tvar c causer\n\t\tif !errors.As(err, &c) {\n\t\t\tbreak\n\t\t}\n\t\terr = c.Cause()\n\t}\n\n\treturn\n}\n"
  },
  {
    "path": "api/render/render_test.go",
    "content": "package render\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"math\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"strconv\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\n\t\"github.com/smallstep/certificates/logging\"\n)\n\nfunc TestJSON(t *testing.T) {\n\trec := httptest.NewRecorder()\n\trw := logging.NewResponseLogger(rec)\n\tr := httptest.NewRequest(\"POST\", \"/test\", http.NoBody)\n\tJSON(rw, r, map[string]interface{}{\"foo\": \"bar\"})\n\n\tassert.Equal(t, http.StatusOK, rec.Result().StatusCode)\n\tassert.Equal(t, \"application/json\", rec.Header().Get(\"Content-Type\"))\n\tassert.Equal(t, \"{\\\"foo\\\":\\\"bar\\\"}\\n\", rec.Body.String())\n\n\tassert.Empty(t, rw.Fields())\n}\n\nfunc TestJSONPanicsOnUnsupportedType(t *testing.T) {\n\tjsonPanicTest[json.UnsupportedTypeError](t, make(chan struct{}))\n}\n\nfunc TestJSONPanicsOnUnsupportedValue(t *testing.T) {\n\tjsonPanicTest[json.UnsupportedValueError](t, math.NaN())\n}\n\nfunc TestJSONPanicsOnMarshalerError(t *testing.T) {\n\tvar v erroneousJSONMarshaler\n\tjsonPanicTest[json.MarshalerError](t, v)\n}\n\ntype erroneousJSONMarshaler struct{}\n\nfunc (erroneousJSONMarshaler) MarshalJSON() ([]byte, error) {\n\treturn nil, assert.AnError\n}\n\nfunc jsonPanicTest[T json.UnsupportedTypeError | json.UnsupportedValueError | json.MarshalerError](t *testing.T, v any) {\n\tt.Helper()\n\n\tdefer func() {\n\t\tvar err error\n\t\tif r := recover(); r == nil {\n\t\t\tt.Fatal(\"expected panic\")\n\t\t} else if e, ok := r.(error); !ok {\n\t\t\tt.Fatalf(\"did not panic with an error (%T)\", r)\n\t\t} else {\n\t\t\terr = e\n\t\t}\n\n\t\tvar e *T\n\t\tassert.ErrorAs(t, err, &e)\n\t}()\n\n\tr := httptest.NewRequest(\"POST\", \"/test\", http.NoBody)\n\tJSON(httptest.NewRecorder(), r, v)\n}\n\ntype renderableError struct {\n\tCode    int    `json:\"-\"`\n\tMessage string `json:\"message\"`\n}\n\nfunc (err renderableError) Error() string {\n\treturn err.Message\n}\n\nfunc (err renderableError) Render(w http.ResponseWriter, r *http.Request) {\n\tw.Header().Set(\"Content-Type\", \"something/custom\")\n\tJSONStatus(w, r, err, err.Code)\n}\n\ntype statusedError struct {\n\tContents string\n}\n\nfunc (err statusedError) Error() string { return err.Contents }\n\nfunc (statusedError) StatusCode() int { return 432 }\n\nfunc TestError(t *testing.T) {\n\tcases := []struct {\n\t\terr    error\n\t\tcode   int\n\t\tbody   string\n\t\theader string\n\t}{\n\t\t0: {\n\t\t\terr:    renderableError{532, \"some string\"},\n\t\t\tcode:   532,\n\t\t\tbody:   \"{\\\"message\\\":\\\"some string\\\"}\\n\",\n\t\t\theader: \"something/custom\",\n\t\t},\n\t\t1: {\n\t\t\terr:    statusedError{\"123\"},\n\t\t\tcode:   432,\n\t\t\tbody:   \"{\\\"Contents\\\":\\\"123\\\"}\\n\",\n\t\t\theader: \"application/json\",\n\t\t},\n\t}\n\n\tfor caseIndex := range cases {\n\t\tkase := cases[caseIndex]\n\n\t\tt.Run(strconv.Itoa(caseIndex), func(t *testing.T) {\n\t\t\trec := httptest.NewRecorder()\n\t\t\tr := httptest.NewRequest(\"POST\", \"/test\", http.NoBody)\n\t\t\tError(rec, r, kase.err)\n\n\t\t\tassert.Equal(t, kase.code, rec.Result().StatusCode)\n\t\t\tassert.Equal(t, kase.body, rec.Body.String())\n\t\t\tassert.Equal(t, kase.header, rec.Header().Get(\"Content-Type\"))\n\t\t})\n\t}\n}\n\ntype causedError struct {\n\tcause error\n}\n\nfunc (err causedError) Error() string { return fmt.Sprintf(\"cause: %s\", err.cause) }\nfunc (err causedError) Cause() error  { return err.cause }\n\nfunc TestStatusCodeFromError(t *testing.T) {\n\tcases := []struct {\n\t\terr error\n\t\texp int\n\t}{\n\t\t0: {nil, http.StatusInternalServerError},\n\t\t1: {io.EOF, http.StatusInternalServerError},\n\t\t2: {statusedError{\"123\"}, 432},\n\t\t3: {causedError{statusedError{\"432\"}}, 432},\n\t}\n\n\tfor caseIndex, kase := range cases {\n\t\tassert.Equal(t, kase.exp, statusCodeFromError(kase.err), \"case: %d\", caseIndex)\n\t}\n}\n"
  },
  {
    "path": "api/renew.go",
    "content": "package api\n\nimport (\n\t\"crypto/x509\"\n\t\"net/http\"\n\t\"strings\"\n\n\t\"github.com/smallstep/certificates/api/render\"\n\t\"github.com/smallstep/certificates/authority\"\n\t\"github.com/smallstep/certificates/authority/provisioner\"\n\t\"github.com/smallstep/certificates/errs\"\n)\n\nconst (\n\tauthorizationHeader = \"Authorization\"\n\tbearerScheme        = \"Bearer\"\n)\n\n// Renew uses the information of certificate in the TLS connection to create a\n// new one.\nfunc Renew(w http.ResponseWriter, r *http.Request) {\n\tctx := r.Context()\n\n\t// Get the leaf certificate from the peer or the token.\n\tcert, token, err := getPeerCertificate(r)\n\tif err != nil {\n\t\trender.Error(w, r, err)\n\t\treturn\n\t}\n\n\t// The token can be used by RAs to renew a certificate.\n\tif token != \"\" {\n\t\tctx = authority.NewTokenContext(ctx, token)\n\t\tlogOtt(w, token)\n\t}\n\n\ta := mustAuthority(ctx)\n\tcertChain, err := a.RenewContext(ctx, cert, nil)\n\tif err != nil {\n\t\trender.Error(w, r, errs.Wrap(http.StatusInternalServerError, err, \"cahandler.Renew\"))\n\t\treturn\n\t}\n\tcertChainPEM := certChainToPEM(certChain)\n\tvar caPEM Certificate\n\tif len(certChainPEM) > 1 {\n\t\tcaPEM = certChainPEM[1]\n\t}\n\n\tLogCertificate(w, certChain[0])\n\trender.JSONStatus(w, r, &SignResponse{\n\t\tServerPEM:    certChainPEM[0],\n\t\tCaPEM:        caPEM,\n\t\tCertChainPEM: certChainPEM,\n\t\tTLSOptions:   a.GetTLSOptions(),\n\t}, http.StatusCreated)\n}\n\nfunc getPeerCertificate(r *http.Request) (*x509.Certificate, string, error) {\n\tif r.TLS != nil && len(r.TLS.PeerCertificates) > 0 {\n\t\treturn r.TLS.PeerCertificates[0], \"\", nil\n\t}\n\tif s := r.Header.Get(authorizationHeader); s != \"\" {\n\t\tif parts := strings.SplitN(s, bearerScheme+\" \", 2); len(parts) == 2 {\n\t\t\tctx := provisioner.NewContextWithMethod(r.Context(), provisioner.RenewMethod)\n\t\t\tpeer, err := mustAuthority(ctx).AuthorizeRenewToken(ctx, parts[1])\n\t\t\treturn peer, parts[1], err\n\t\t}\n\t}\n\treturn nil, \"\", errs.BadRequest(\"missing client certificate\")\n}\n"
  },
  {
    "path": "api/revoke.go",
    "content": "package api\n\nimport (\n\t\"math/big\"\n\t\"net/http\"\n\n\t\"golang.org/x/crypto/ocsp\"\n\n\t\"github.com/smallstep/certificates/api/read\"\n\t\"github.com/smallstep/certificates/api/render\"\n\t\"github.com/smallstep/certificates/authority\"\n\t\"github.com/smallstep/certificates/authority/provisioner\"\n\t\"github.com/smallstep/certificates/errs\"\n\t\"github.com/smallstep/certificates/logging\"\n)\n\n// RevokeResponse is the response object that returns the health of the server.\ntype RevokeResponse struct {\n\tStatus string `json:\"status\"`\n}\n\n// RevokeRequest is the request body for a revocation request.\ntype RevokeRequest struct {\n\tSerial     string `json:\"serial\"`\n\tOTT        string `json:\"ott\"`\n\tReasonCode int    `json:\"reasonCode\"`\n\tReason     string `json:\"reason\"`\n\tPassive    bool   `json:\"passive\"`\n}\n\n// Validate checks the fields of the RevokeRequest and returns nil if they are ok\n// or an error if something is wrong.\nfunc (r *RevokeRequest) Validate() (err error) {\n\tif r.Serial == \"\" {\n\t\treturn errs.BadRequest(\"missing serial\")\n\t}\n\tsn, ok := new(big.Int).SetString(r.Serial, 0)\n\tif !ok {\n\t\treturn errs.BadRequest(\"'%s' is not a valid serial number - use a base 10 representation or a base 16 representation with '0x' prefix\", r.Serial)\n\t}\n\tr.Serial = sn.String()\n\tif r.ReasonCode < ocsp.Unspecified || r.ReasonCode > ocsp.AACompromise {\n\t\treturn errs.BadRequest(\"reasonCode out of bounds\")\n\t}\n\tif !r.Passive {\n\t\treturn errs.NotImplemented(\"non-passive revocation not implemented\")\n\t}\n\n\treturn\n}\n\n// Revoke supports handful of different methods that revoke a Certificate.\n//\n// NOTE: currently only Passive revocation is supported.\n//\n// TODO: Add CRL and OCSP support.\nfunc Revoke(w http.ResponseWriter, r *http.Request) {\n\tvar body RevokeRequest\n\tif err := read.JSON(r.Body, &body); err != nil {\n\t\trender.Error(w, r, errs.BadRequestErr(err, \"error reading request body\"))\n\t\treturn\n\t}\n\n\tif err := body.Validate(); err != nil {\n\t\trender.Error(w, r, err)\n\t\treturn\n\t}\n\n\topts := &authority.RevokeOptions{\n\t\tSerial:      body.Serial,\n\t\tReason:      body.Reason,\n\t\tReasonCode:  body.ReasonCode,\n\t\tPassiveOnly: body.Passive,\n\t}\n\n\tctx := provisioner.NewContextWithMethod(r.Context(), provisioner.RevokeMethod)\n\ta := mustAuthority(ctx)\n\n\t// A token indicates that we are using the api via a provisioner token,\n\t// otherwise it is assumed that the certificate is revoking itself over mTLS.\n\tif body.OTT != \"\" {\n\t\tlogOtt(w, body.OTT)\n\t\tif _, err := a.Authorize(ctx, body.OTT); err != nil {\n\t\t\trender.Error(w, r, errs.UnauthorizedErr(err))\n\t\t\treturn\n\t\t}\n\t\topts.OTT = body.OTT\n\t} else {\n\t\t// If no token is present, then the request must be made over mTLS and\n\t\t// the client certificate Serial Number must match the serial number\n\t\t// being revoked.\n\t\tif r.TLS == nil || len(r.TLS.PeerCertificates) == 0 {\n\t\t\trender.Error(w, r, errs.BadRequest(\"missing ott or client certificate\"))\n\t\t\treturn\n\t\t}\n\t\topts.Crt = r.TLS.PeerCertificates[0]\n\t\tif serialNumber := opts.Crt.SerialNumber.String(); opts.Serial != serialNumber {\n\t\t\trender.Error(w, r, errs.Forbidden(\n\t\t\t\t\"request serial number %q and certificate serial number %q do not match\", opts.Serial, serialNumber))\n\t\t\treturn\n\t\t}\n\t\t// TODO: should probably be checking if the certificate was revoked here.\n\t\t// Will need to thread that request down to the authority, so will need\n\t\t// to add API for that.\n\t\tLogCertificate(w, opts.Crt)\n\t\topts.MTLS = true\n\t}\n\n\tif err := a.Revoke(ctx, opts); err != nil {\n\t\trender.Error(w, r, errs.ForbiddenErr(err, \"error revoking certificate\"))\n\t\treturn\n\t}\n\n\tlogRevoke(w, opts)\n\trender.JSON(w, r, &RevokeResponse{Status: \"ok\"})\n}\n\nfunc logRevoke(w http.ResponseWriter, ri *authority.RevokeOptions) {\n\tif rl, ok := w.(logging.ResponseLogger); ok {\n\t\trl.WithFields(map[string]interface{}{\n\t\t\t\"serial\":      ri.Serial,\n\t\t\t\"reasonCode\":  ri.ReasonCode,\n\t\t\t\"reason\":      ri.Reason,\n\t\t\t\"passiveOnly\": ri.PassiveOnly,\n\t\t\t\"mTLS\":        ri.MTLS,\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "api/revoke_test.go",
    "content": "package api\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"encoding/json\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/pkg/errors\"\n\n\t\"github.com/smallstep/assert\"\n\t\"github.com/smallstep/certificates/authority\"\n\t\"github.com/smallstep/certificates/authority/provisioner\"\n\t\"github.com/smallstep/certificates/errs\"\n\t\"github.com/smallstep/certificates/logging\"\n)\n\nfunc TestRevokeRequestValidate(t *testing.T) {\n\ttype test struct {\n\t\trr  *RevokeRequest\n\t\terr *errs.Error\n\t}\n\ttests := map[string]test{\n\t\t\"error/missing serial\": {\n\t\t\trr:  &RevokeRequest{},\n\t\t\terr: &errs.Error{Err: errors.New(\"missing serial\"), Status: http.StatusBadRequest},\n\t\t},\n\t\t\"error/bad sn\": {\n\t\t\trr:  &RevokeRequest{Serial: \"sn\"},\n\t\t\terr: &errs.Error{Err: errors.New(\"'sn' is not a valid serial number - use a base 10 representation or a base 16 representation with '0x' prefix\"), Status: http.StatusBadRequest},\n\t\t},\n\t\t\"error/bad reasonCode\": {\n\t\t\trr: &RevokeRequest{\n\t\t\t\tSerial:     \"10\",\n\t\t\t\tReasonCode: 15,\n\t\t\t\tPassive:    true,\n\t\t\t},\n\t\t\terr: &errs.Error{Err: errors.New(\"reasonCode out of bounds\"), Status: http.StatusBadRequest},\n\t\t},\n\t\t\"error/non-passive not implemented\": {\n\t\t\trr: &RevokeRequest{\n\t\t\t\tSerial:     \"10\",\n\t\t\t\tReasonCode: 8,\n\t\t\t\tPassive:    false,\n\t\t\t},\n\t\t\terr: &errs.Error{Err: errors.New(\"non-passive revocation not implemented\"), Status: http.StatusNotImplemented},\n\t\t},\n\t\t\"ok\": {\n\t\t\trr: &RevokeRequest{\n\t\t\t\tSerial:     \"10\",\n\t\t\t\tReasonCode: 9,\n\t\t\t\tPassive:    true,\n\t\t\t},\n\t\t},\n\t}\n\tfor name, tc := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tif err := tc.rr.Validate(); err != nil {\n\t\t\t\tvar ee *errs.Error\n\t\t\t\tif errors.As(err, &ee) {\n\t\t\t\t\tassert.HasPrefix(t, ee.Error(), tc.err.Error())\n\t\t\t\t\tassert.Equals(t, ee.StatusCode(), tc.err.Status)\n\t\t\t\t} else {\n\t\t\t\t\tt.Errorf(\"unexpected error type: %T\", err)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tassert.Nil(t, tc.err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_caHandler_Revoke(t *testing.T) {\n\ttype test struct {\n\t\tinput      string\n\t\tauth       Authority\n\t\ttls        *tls.ConnectionState\n\t\tstatusCode int\n\t\texpected   []byte\n\t}\n\ttests := map[string]func(*testing.T) test{\n\t\t\"400/json read error\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tinput:      \"{\",\n\t\t\t\tstatusCode: http.StatusBadRequest,\n\t\t\t}\n\t\t},\n\t\t\"400/invalid request body\": func(t *testing.T) test {\n\t\t\tinput, err := json.Marshal(RevokeRequest{})\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tinput:      string(input),\n\t\t\t\tstatusCode: http.StatusBadRequest,\n\t\t\t}\n\t\t},\n\t\t\"200/ott\": func(t *testing.T) test {\n\t\t\tinput, err := json.Marshal(RevokeRequest{\n\t\t\t\tSerial:     \"10\",\n\t\t\t\tReasonCode: 4,\n\t\t\t\tReason:     \"foo\",\n\t\t\t\tOTT:        \"valid\",\n\t\t\t\tPassive:    true,\n\t\t\t})\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tinput:      string(input),\n\t\t\t\tstatusCode: http.StatusOK,\n\t\t\t\tauth: &mockAuthority{\n\t\t\t\t\tauthorize: func(ctx context.Context, ott string) ([]provisioner.SignOption, error) {\n\t\t\t\t\t\treturn nil, nil\n\t\t\t\t\t},\n\t\t\t\t\trevoke: func(ctx context.Context, opts *authority.RevokeOptions) error {\n\t\t\t\t\t\tassert.True(t, opts.PassiveOnly)\n\t\t\t\t\t\tassert.False(t, opts.MTLS)\n\t\t\t\t\t\tassert.Equals(t, opts.Serial, \"10\")\n\t\t\t\t\t\tassert.Equals(t, opts.ReasonCode, 4)\n\t\t\t\t\t\tassert.Equals(t, opts.Reason, \"foo\")\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\texpected: []byte(`{\"status\":\"ok\"}`),\n\t\t\t}\n\t\t},\n\t\t\"400/no OTT and no peer certificate\": func(t *testing.T) test {\n\t\t\tinput, err := json.Marshal(RevokeRequest{\n\t\t\t\tSerial:     \"10\",\n\t\t\t\tReasonCode: 4,\n\t\t\t\tPassive:    true,\n\t\t\t})\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tinput:      string(input),\n\t\t\t\tstatusCode: http.StatusBadRequest,\n\t\t\t}\n\t\t},\n\t\t\"200/no ott\": func(t *testing.T) test {\n\t\t\tcs := &tls.ConnectionState{\n\t\t\t\tPeerCertificates: []*x509.Certificate{parseCertificate(certPEM)},\n\t\t\t}\n\t\t\tinput, err := json.Marshal(RevokeRequest{\n\t\t\t\tSerial:     \"1404354960355712309\",\n\t\t\t\tReasonCode: 4,\n\t\t\t\tReason:     \"foo\",\n\t\t\t\tPassive:    true,\n\t\t\t})\n\t\t\tassert.FatalError(t, err)\n\n\t\t\treturn test{\n\t\t\t\tinput:      string(input),\n\t\t\t\tstatusCode: http.StatusOK,\n\t\t\t\ttls:        cs,\n\t\t\t\tauth: &mockAuthority{\n\t\t\t\t\tauthorize: func(ctx context.Context, ott string) ([]provisioner.SignOption, error) {\n\t\t\t\t\t\treturn nil, nil\n\t\t\t\t\t},\n\t\t\t\t\trevoke: func(ctx context.Context, ri *authority.RevokeOptions) error {\n\t\t\t\t\t\tassert.True(t, ri.PassiveOnly)\n\t\t\t\t\t\tassert.True(t, ri.MTLS)\n\t\t\t\t\t\tassert.Equals(t, ri.Serial, \"1404354960355712309\")\n\t\t\t\t\t\tassert.Equals(t, ri.ReasonCode, 4)\n\t\t\t\t\t\tassert.Equals(t, ri.Reason, \"foo\")\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t\tloadProvisionerByCertificate: func(crt *x509.Certificate) (provisioner.Interface, error) {\n\t\t\t\t\t\treturn &mockProvisioner{\n\t\t\t\t\t\t\tgetID: func() string {\n\t\t\t\t\t\t\t\treturn \"mock-provisioner-id\"\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t}, err\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\texpected: []byte(`{\"status\":\"ok\"}`),\n\t\t\t}\n\t\t},\n\t\t\"500/ott authority.Revoke\": func(t *testing.T) test {\n\t\t\tinput, err := json.Marshal(RevokeRequest{\n\t\t\t\tSerial:     \"10\",\n\t\t\t\tReasonCode: 4,\n\t\t\t\tReason:     \"foo\",\n\t\t\t\tOTT:        \"valid\",\n\t\t\t\tPassive:    true,\n\t\t\t})\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tinput:      string(input),\n\t\t\t\tstatusCode: http.StatusInternalServerError,\n\t\t\t\tauth: &mockAuthority{\n\t\t\t\t\tauthorize: func(ctx context.Context, ott string) ([]provisioner.SignOption, error) {\n\t\t\t\t\t\treturn nil, nil\n\t\t\t\t\t},\n\t\t\t\t\trevoke: func(ctx context.Context, opts *authority.RevokeOptions) error {\n\t\t\t\t\t\treturn errs.InternalServer(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"403/ott authority.Revoke\": func(t *testing.T) test {\n\t\t\tinput, err := json.Marshal(RevokeRequest{\n\t\t\t\tSerial:     \"10\",\n\t\t\t\tReasonCode: 4,\n\t\t\t\tReason:     \"foo\",\n\t\t\t\tOTT:        \"valid\",\n\t\t\t\tPassive:    true,\n\t\t\t})\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tinput:      string(input),\n\t\t\t\tstatusCode: http.StatusForbidden,\n\t\t\t\tauth: &mockAuthority{\n\t\t\t\t\tauthorize: func(ctx context.Context, ott string) ([]provisioner.SignOption, error) {\n\t\t\t\t\t\treturn nil, nil\n\t\t\t\t\t},\n\t\t\t\t\trevoke: func(ctx context.Context, opts *authority.RevokeOptions) error {\n\t\t\t\t\t\treturn errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t}\n\n\tfor name, _tc := range tests {\n\t\ttc := _tc(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tmockMustAuthority(t, tc.auth)\n\t\t\treq := httptest.NewRequest(\"POST\", \"http://example.com/revoke\", strings.NewReader(tc.input))\n\t\t\tif tc.tls != nil {\n\t\t\t\treq.TLS = tc.tls\n\t\t\t}\n\t\t\tw := httptest.NewRecorder()\n\t\t\tRevoke(logging.NewResponseLogger(w), req)\n\t\t\tres := w.Result()\n\n\t\t\tassert.Equals(t, tc.statusCode, res.StatusCode)\n\n\t\t\tbody, err := io.ReadAll(res.Body)\n\t\t\tres.Body.Close()\n\t\t\tassert.FatalError(t, err)\n\n\t\t\tif tc.statusCode < http.StatusBadRequest {\n\t\t\t\tif !bytes.Equal(bytes.TrimSpace(body), tc.expected) {\n\t\t\t\t\tt.Errorf(\"caHandler.Root Body = %s, wants %s\", body, tc.expected)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "api/sign.go",
    "content": "package api\n\nimport (\n\t\"crypto/tls\"\n\t\"encoding/json\"\n\t\"net/http\"\n\n\t\"github.com/smallstep/certificates/api/read\"\n\t\"github.com/smallstep/certificates/api/render\"\n\t\"github.com/smallstep/certificates/authority/config\"\n\t\"github.com/smallstep/certificates/authority/provisioner\"\n\t\"github.com/smallstep/certificates/errs\"\n)\n\n// SignRequest is the request body for a certificate signature request.\ntype SignRequest struct {\n\tCsrPEM       CertificateRequest `json:\"csr\"`\n\tOTT          string             `json:\"ott\"`\n\tNotAfter     TimeDuration       `json:\"notAfter,omitempty\"`\n\tNotBefore    TimeDuration       `json:\"notBefore,omitempty\"`\n\tTemplateData json.RawMessage    `json:\"templateData,omitempty\"`\n}\n\n// Validate checks the fields of the SignRequest and returns nil if they are ok\n// or an error if something is wrong.\nfunc (s *SignRequest) Validate() error {\n\tif s.CsrPEM.CertificateRequest == nil {\n\t\treturn errs.BadRequest(\"missing csr\")\n\t}\n\tif err := s.CsrPEM.CertificateRequest.CheckSignature(); err != nil {\n\t\treturn errs.BadRequestErr(err, \"invalid csr\")\n\t}\n\tif s.OTT == \"\" {\n\t\treturn errs.BadRequest(\"missing ott\")\n\t}\n\n\treturn nil\n}\n\n// SignResponse is the response object of the certificate signature request.\ntype SignResponse struct {\n\tServerPEM    Certificate          `json:\"crt\"`\n\tCaPEM        Certificate          `json:\"ca\"`\n\tCertChainPEM []Certificate        `json:\"certChain\"`\n\tTLSOptions   *config.TLSOptions   `json:\"tlsOptions,omitempty\"`\n\tTLS          *tls.ConnectionState `json:\"-\"`\n}\n\n// Sign is an HTTP handler that reads a certificate request and an\n// one-time-token (ott) from the body and creates a new certificate with the\n// information in the certificate request.\nfunc Sign(w http.ResponseWriter, r *http.Request) {\n\tvar body SignRequest\n\tif err := read.JSON(r.Body, &body); err != nil {\n\t\trender.Error(w, r, errs.BadRequestErr(err, \"error reading request body\"))\n\t\treturn\n\t}\n\n\tlogOtt(w, body.OTT)\n\tif err := body.Validate(); err != nil {\n\t\trender.Error(w, r, err)\n\t\treturn\n\t}\n\n\topts := provisioner.SignOptions{\n\t\tNotBefore:    body.NotBefore,\n\t\tNotAfter:     body.NotAfter,\n\t\tTemplateData: body.TemplateData,\n\t}\n\n\tctx := r.Context()\n\ta := mustAuthority(ctx)\n\n\tctx = provisioner.NewContextWithMethod(ctx, provisioner.SignMethod)\n\tsignOpts, err := a.Authorize(ctx, body.OTT)\n\tif err != nil {\n\t\trender.Error(w, r, errs.UnauthorizedErr(err))\n\t\treturn\n\t}\n\n\tcertChain, err := a.SignWithContext(ctx, body.CsrPEM.CertificateRequest, opts, signOpts...)\n\tif err != nil {\n\t\trender.Error(w, r, errs.ForbiddenErr(err, \"error signing certificate\"))\n\t\treturn\n\t}\n\tcertChainPEM := certChainToPEM(certChain)\n\tvar caPEM Certificate\n\tif len(certChainPEM) > 1 {\n\t\tcaPEM = certChainPEM[1]\n\t}\n\n\tLogCertificate(w, certChain[0])\n\trender.JSONStatus(w, r, &SignResponse{\n\t\tServerPEM:    certChainPEM[0],\n\t\tCaPEM:        caPEM,\n\t\tCertChainPEM: certChainPEM,\n\t\tTLSOptions:   a.GetTLSOptions(),\n\t}, http.StatusCreated)\n}\n"
  },
  {
    "path": "api/ssh.go",
    "content": "package api\n\nimport (\n\t\"context\"\n\t\"crypto/x509\"\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/google/uuid\"\n\t\"github.com/pkg/errors\"\n\t\"golang.org/x/crypto/ssh\"\n\n\t\"github.com/smallstep/certificates/api/read\"\n\t\"github.com/smallstep/certificates/api/render\"\n\t\"github.com/smallstep/certificates/authority\"\n\t\"github.com/smallstep/certificates/authority/config\"\n\t\"github.com/smallstep/certificates/authority/provisioner\"\n\t\"github.com/smallstep/certificates/errs\"\n\t\"github.com/smallstep/certificates/internal/cast\"\n\t\"github.com/smallstep/certificates/templates\"\n)\n\n// SSHAuthority is the interface implemented by a SSH CA authority.\ntype SSHAuthority interface {\n\tSignSSH(ctx context.Context, key ssh.PublicKey, opts provisioner.SignSSHOptions, signOpts ...provisioner.SignOption) (*ssh.Certificate, error)\n\tRenewSSH(ctx context.Context, cert *ssh.Certificate) (*ssh.Certificate, error)\n\tRekeySSH(ctx context.Context, cert *ssh.Certificate, key ssh.PublicKey, signOpts ...provisioner.SignOption) (*ssh.Certificate, error)\n\tSignSSHAddUser(ctx context.Context, key ssh.PublicKey, cert *ssh.Certificate) (*ssh.Certificate, error)\n\tGetSSHRoots(ctx context.Context) (*config.SSHKeys, error)\n\tGetSSHFederation(ctx context.Context) (*config.SSHKeys, error)\n\tGetSSHConfig(ctx context.Context, typ string, data map[string]string) ([]templates.Output, error)\n\tCheckSSHHost(ctx context.Context, principal string, token string) (bool, error)\n\tGetSSHHosts(ctx context.Context, cert *x509.Certificate) ([]config.Host, error)\n\tGetSSHBastion(ctx context.Context, user string, hostname string) (*config.Bastion, error)\n}\n\n// SSHSignRequest is the request body of an SSH certificate request.\ntype SSHSignRequest struct {\n\tPublicKey        []byte             `json:\"publicKey\"` // base64 encoded\n\tOTT              string             `json:\"ott\"`\n\tCertType         string             `json:\"certType,omitempty\"`\n\tKeyID            string             `json:\"keyID,omitempty\"`\n\tPrincipals       []string           `json:\"principals,omitempty\"`\n\tValidAfter       TimeDuration       `json:\"validAfter,omitempty\"`\n\tValidBefore      TimeDuration       `json:\"validBefore,omitempty\"`\n\tAddUserPublicKey []byte             `json:\"addUserPublicKey,omitempty\"`\n\tIdentityCSR      CertificateRequest `json:\"identityCSR,omitempty\"`\n\tTemplateData     json.RawMessage    `json:\"templateData,omitempty\"`\n}\n\n// Validate validates the SSHSignRequest.\nfunc (s *SSHSignRequest) Validate() error {\n\tswitch {\n\tcase s.CertType != \"\" && s.CertType != provisioner.SSHUserCert && s.CertType != provisioner.SSHHostCert:\n\t\treturn errs.BadRequest(\"invalid certType '%s'\", s.CertType)\n\tcase len(s.PublicKey) == 0:\n\t\treturn errs.BadRequest(\"missing or empty publicKey\")\n\tcase s.OTT == \"\":\n\t\treturn errs.BadRequest(\"missing or empty ott\")\n\tdefault:\n\t\t// Validate identity signature if provided\n\t\tif s.IdentityCSR.CertificateRequest != nil {\n\t\t\tif err := s.IdentityCSR.CertificateRequest.CheckSignature(); err != nil {\n\t\t\t\treturn errs.BadRequestErr(err, \"invalid identityCSR\")\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t}\n}\n\n// SSHSignResponse is the response object that returns the SSH certificate.\ntype SSHSignResponse struct {\n\tCertificate         SSHCertificate  `json:\"crt\"`\n\tAddUserCertificate  *SSHCertificate `json:\"addUserCrt,omitempty\"`\n\tIdentityCertificate []Certificate   `json:\"identityCrt,omitempty\"`\n}\n\n// SSHRootsResponse represents the response object that returns the SSH user and\n// host keys.\ntype SSHRootsResponse struct {\n\tUserKeys []SSHPublicKey `json:\"userKey,omitempty\"`\n\tHostKeys []SSHPublicKey `json:\"hostKey,omitempty\"`\n}\n\n// SSHCertificate represents the response SSH certificate.\ntype SSHCertificate struct {\n\t*ssh.Certificate `json:\"omitempty\"`\n}\n\n// SSHGetHostsResponse is the response object that returns the list of valid\n// hosts for SSH.\ntype SSHGetHostsResponse struct {\n\tHosts []config.Host `json:\"hosts\"`\n}\n\n// MarshalJSON implements the json.Marshaler interface. Returns a quoted,\n// base64 encoded, openssh wire format version of the certificate.\nfunc (c SSHCertificate) MarshalJSON() ([]byte, error) {\n\tif c.Certificate == nil {\n\t\treturn []byte(\"null\"), nil\n\t}\n\ts := base64.StdEncoding.EncodeToString(c.Certificate.Marshal())\n\treturn []byte(`\"` + s + `\"`), nil\n}\n\n// UnmarshalJSON implements the json.Unmarshaler interface. The certificate is\n// expected to be a quoted, base64 encoded, openssh wire formatted block of bytes.\nfunc (c *SSHCertificate) UnmarshalJSON(data []byte) error {\n\tvar s string\n\tif err := json.Unmarshal(data, &s); err != nil {\n\t\treturn errors.Wrap(err, \"error decoding certificate\")\n\t}\n\tif s == \"\" {\n\t\tc.Certificate = nil\n\t\treturn nil\n\t}\n\tcertData, err := base64.StdEncoding.DecodeString(s)\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"error decoding ssh certificate\")\n\t}\n\tpub, err := ssh.ParsePublicKey(certData)\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"error parsing ssh certificate\")\n\t}\n\tcert, ok := pub.(*ssh.Certificate)\n\tif !ok {\n\t\treturn errors.Errorf(\"error decoding ssh certificate: %T is not an *ssh.Certificate\", pub)\n\t}\n\tc.Certificate = cert\n\treturn nil\n}\n\n// SSHPublicKey represents a public key in a response object.\ntype SSHPublicKey struct {\n\tssh.PublicKey\n}\n\n// MarshalJSON implements the json.Marshaler interface. Returns a quoted,\n// base64 encoded, openssh wire format version of the public key.\nfunc (p *SSHPublicKey) MarshalJSON() ([]byte, error) {\n\tif p == nil || p.PublicKey == nil {\n\t\treturn []byte(\"null\"), nil\n\t}\n\ts := base64.StdEncoding.EncodeToString(p.PublicKey.Marshal())\n\treturn []byte(`\"` + s + `\"`), nil\n}\n\n// UnmarshalJSON implements the json.Unmarshaler interface. The public key is\n// expected to be a quoted, base64 encoded, openssh wire formatted block of\n// bytes.\nfunc (p *SSHPublicKey) UnmarshalJSON(data []byte) error {\n\tvar s string\n\tif err := json.Unmarshal(data, &s); err != nil {\n\t\treturn errors.Wrap(err, \"error decoding ssh public key\")\n\t}\n\tif s == \"\" {\n\t\tp.PublicKey = nil\n\t\treturn nil\n\t}\n\tdata, err := base64.StdEncoding.DecodeString(s)\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"error decoding ssh public key\")\n\t}\n\tpub, err := ssh.ParsePublicKey(data)\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"error parsing ssh public key\")\n\t}\n\tp.PublicKey = pub\n\treturn nil\n}\n\n// Template represents the output of a template.\ntype Template = templates.Output\n\n// SSHConfigRequest is the request body used to get the SSH configuration\n// templates.\ntype SSHConfigRequest struct {\n\tType string            `json:\"type\"`\n\tData map[string]string `json:\"data\"`\n}\n\n// Validate checks the values of the SSHConfigurationRequest.\nfunc (r *SSHConfigRequest) Validate() error {\n\tswitch r.Type {\n\tcase \"\":\n\t\tr.Type = provisioner.SSHUserCert\n\t\treturn nil\n\tcase provisioner.SSHUserCert, provisioner.SSHHostCert:\n\t\treturn nil\n\tdefault:\n\t\treturn errs.BadRequest(\"invalid type '%s'\", r.Type)\n\t}\n}\n\n// SSHConfigResponse is the response that returns the rendered templates.\ntype SSHConfigResponse struct {\n\tUserTemplates []Template `json:\"userTemplates,omitempty\"`\n\tHostTemplates []Template `json:\"hostTemplates,omitempty\"`\n}\n\n// SSHCheckPrincipalRequest is the request body used to check if a principal\n// certificate has been created. Right now it only supported for hosts\n// certificates.\ntype SSHCheckPrincipalRequest struct {\n\tType      string `json:\"type\"`\n\tPrincipal string `json:\"principal\"`\n\tToken     string `json:\"token,omitempty\"`\n}\n\n// Validate checks the check principal request.\nfunc (r *SSHCheckPrincipalRequest) Validate() error {\n\tswitch {\n\tcase r.Type != provisioner.SSHHostCert:\n\t\treturn errs.BadRequest(\"unsupported type '%s'\", r.Type)\n\tcase r.Principal == \"\":\n\t\treturn errs.BadRequest(\"missing or empty principal\")\n\tdefault:\n\t\treturn nil\n\t}\n}\n\n// SSHCheckPrincipalResponse is the response body used to check if a principal\n// exists.\ntype SSHCheckPrincipalResponse struct {\n\tExists bool `json:\"exists\"`\n}\n\n// SSHBastionRequest is the request body used to get the bastion for a given\n// host.\ntype SSHBastionRequest struct {\n\tUser     string `json:\"user\"`\n\tHostname string `json:\"hostname\"`\n}\n\n// Validate checks the values of the SSHBastionRequest.\nfunc (r *SSHBastionRequest) Validate() error {\n\tif r.Hostname == \"\" {\n\t\treturn errs.BadRequest(\"missing or empty hostname\")\n\t}\n\treturn nil\n}\n\n// SSHBastionResponse is the response body used to return the bastion for a\n// given host.\ntype SSHBastionResponse struct {\n\tHostname string          `json:\"hostname\"`\n\tBastion  *config.Bastion `json:\"bastion,omitempty\"`\n}\n\n// SSHSign is an HTTP handler that reads an SignSSHRequest with a one-time-token\n// (ott) from the body and creates a new SSH certificate with the information in\n// the request.\nfunc SSHSign(w http.ResponseWriter, r *http.Request) {\n\tvar body SSHSignRequest\n\tif err := read.JSON(r.Body, &body); err != nil {\n\t\trender.Error(w, r, errs.BadRequestErr(err, \"error reading request body\"))\n\t\treturn\n\t}\n\n\tlogOtt(w, body.OTT)\n\tif err := body.Validate(); err != nil {\n\t\trender.Error(w, r, err)\n\t\treturn\n\t}\n\n\tpublicKey, err := ssh.ParsePublicKey(body.PublicKey)\n\tif err != nil {\n\t\trender.Error(w, r, errs.BadRequestErr(err, \"error parsing publicKey\"))\n\t\treturn\n\t}\n\n\tvar addUserPublicKey ssh.PublicKey\n\tif body.AddUserPublicKey != nil {\n\t\taddUserPublicKey, err = ssh.ParsePublicKey(body.AddUserPublicKey)\n\t\tif err != nil {\n\t\t\trender.Error(w, r, errs.BadRequestErr(err, \"error parsing addUserPublicKey\"))\n\t\t\treturn\n\t\t}\n\t}\n\n\topts := provisioner.SignSSHOptions{\n\t\tCertType:     body.CertType,\n\t\tKeyID:        body.KeyID,\n\t\tPrincipals:   body.Principals,\n\t\tValidBefore:  body.ValidBefore,\n\t\tValidAfter:   body.ValidAfter,\n\t\tTemplateData: body.TemplateData,\n\t}\n\n\tctx := provisioner.NewContextWithMethod(r.Context(), provisioner.SSHSignMethod)\n\tctx = provisioner.NewContextWithToken(ctx, body.OTT)\n\tctx = provisioner.NewContextWithCertType(ctx, opts.CertType)\n\n\ta := mustAuthority(ctx)\n\tsignOpts, err := a.Authorize(ctx, body.OTT)\n\tif err != nil {\n\t\trender.Error(w, r, errs.UnauthorizedErr(err))\n\t\treturn\n\t}\n\n\tcert, err := a.SignSSH(ctx, publicKey, opts, signOpts...)\n\tif err != nil {\n\t\trender.Error(w, r, errs.ForbiddenErr(err, \"error signing ssh certificate\"))\n\t\treturn\n\t}\n\n\tvar addUserCertificate *SSHCertificate\n\tif addUserPublicKey != nil && authority.IsValidForAddUser(cert) == nil {\n\t\taddUserCert, err := a.SignSSHAddUser(ctx, addUserPublicKey, cert)\n\t\tif err != nil {\n\t\t\trender.Error(w, r, errs.ForbiddenErr(err, \"error signing ssh certificate\"))\n\t\t\treturn\n\t\t}\n\t\taddUserCertificate = &SSHCertificate{addUserCert}\n\t}\n\n\t// Sign identity certificate if available.\n\tvar identityCertificate []Certificate\n\tif cr := body.IdentityCSR.CertificateRequest; cr != nil {\n\t\tctx := authority.NewContextWithSkipTokenReuse(r.Context())\n\t\tctx = provisioner.NewContextWithMethod(ctx, provisioner.SignIdentityMethod)\n\t\tsignOpts, err := a.Authorize(ctx, body.OTT)\n\t\tif err != nil {\n\t\t\trender.Error(w, r, errs.UnauthorizedErr(err))\n\t\t\treturn\n\t\t}\n\n\t\t// Enforce the same duration as ssh certificate.\n\t\tsignOpts = append(signOpts, &identityModifier{\n\t\t\tIdentity:  getIdentityURI(cr),\n\t\t\tNotBefore: time.Unix(cast.Int64(cert.ValidAfter), 0),\n\t\t\tNotAfter:  time.Unix(cast.Int64(cert.ValidBefore), 0),\n\t\t})\n\n\t\tcertChain, err := a.SignWithContext(ctx, cr, provisioner.SignOptions{}, signOpts...)\n\t\tif err != nil {\n\t\t\trender.Error(w, r, errs.ForbiddenErr(err, \"error signing identity certificate\"))\n\t\t\treturn\n\t\t}\n\t\tidentityCertificate = certChainToPEM(certChain)\n\t}\n\n\tLogSSHCertificate(w, cert)\n\trender.JSONStatus(w, r, &SSHSignResponse{\n\t\tCertificate:         SSHCertificate{cert},\n\t\tAddUserCertificate:  addUserCertificate,\n\t\tIdentityCertificate: identityCertificate,\n\t}, http.StatusCreated)\n}\n\n// SSHRoots is an HTTP handler that returns the SSH public keys for user and host\n// certificates.\nfunc SSHRoots(w http.ResponseWriter, r *http.Request) {\n\tctx := r.Context()\n\tkeys, err := mustAuthority(ctx).GetSSHRoots(ctx)\n\tif err != nil {\n\t\trender.Error(w, r, errs.InternalServerErr(err))\n\t\treturn\n\t}\n\n\tif len(keys.HostKeys) == 0 && len(keys.UserKeys) == 0 {\n\t\trender.Error(w, r, errs.NotFound(\"no keys found\"))\n\t\treturn\n\t}\n\n\tresp := new(SSHRootsResponse)\n\tfor _, k := range keys.HostKeys {\n\t\tresp.HostKeys = append(resp.HostKeys, SSHPublicKey{PublicKey: k})\n\t}\n\tfor _, k := range keys.UserKeys {\n\t\tresp.UserKeys = append(resp.UserKeys, SSHPublicKey{PublicKey: k})\n\t}\n\n\trender.JSON(w, r, resp)\n}\n\n// SSHFederation is an HTTP handler that returns the federated SSH public keys\n// for user and host certificates.\nfunc SSHFederation(w http.ResponseWriter, r *http.Request) {\n\tctx := r.Context()\n\tkeys, err := mustAuthority(ctx).GetSSHFederation(ctx)\n\tif err != nil {\n\t\trender.Error(w, r, errs.InternalServerErr(err))\n\t\treturn\n\t}\n\n\tif len(keys.HostKeys) == 0 && len(keys.UserKeys) == 0 {\n\t\trender.Error(w, r, errs.NotFound(\"no keys found\"))\n\t\treturn\n\t}\n\n\tresp := new(SSHRootsResponse)\n\tfor _, k := range keys.HostKeys {\n\t\tresp.HostKeys = append(resp.HostKeys, SSHPublicKey{PublicKey: k})\n\t}\n\tfor _, k := range keys.UserKeys {\n\t\tresp.UserKeys = append(resp.UserKeys, SSHPublicKey{PublicKey: k})\n\t}\n\n\trender.JSON(w, r, resp)\n}\n\n// SSHConfig is an HTTP handler that returns rendered templates for ssh clients\n// and servers.\nfunc SSHConfig(w http.ResponseWriter, r *http.Request) {\n\tvar body SSHConfigRequest\n\tif err := read.JSON(r.Body, &body); err != nil {\n\t\trender.Error(w, r, errs.BadRequestErr(err, \"error reading request body\"))\n\t\treturn\n\t}\n\tif err := body.Validate(); err != nil {\n\t\trender.Error(w, r, err)\n\t\treturn\n\t}\n\n\tctx := r.Context()\n\tts, err := mustAuthority(ctx).GetSSHConfig(ctx, body.Type, body.Data)\n\tif err != nil {\n\t\trender.Error(w, r, errs.InternalServerErr(err))\n\t\treturn\n\t}\n\n\tvar cfg SSHConfigResponse\n\tswitch body.Type {\n\tcase provisioner.SSHUserCert:\n\t\tcfg.UserTemplates = ts\n\tcase provisioner.SSHHostCert:\n\t\tcfg.HostTemplates = ts\n\tdefault:\n\t\trender.Error(w, r, errs.InternalServer(\"it should hot get here\"))\n\t\treturn\n\t}\n\n\trender.JSON(w, r, cfg)\n}\n\n// SSHCheckHost is the HTTP handler that returns if a hosts certificate exists or not.\nfunc SSHCheckHost(w http.ResponseWriter, r *http.Request) {\n\tvar body SSHCheckPrincipalRequest\n\tif err := read.JSON(r.Body, &body); err != nil {\n\t\trender.Error(w, r, errs.BadRequestErr(err, \"error reading request body\"))\n\t\treturn\n\t}\n\tif err := body.Validate(); err != nil {\n\t\trender.Error(w, r, err)\n\t\treturn\n\t}\n\n\tctx := r.Context()\n\texists, err := mustAuthority(ctx).CheckSSHHost(ctx, body.Principal, body.Token)\n\tif err != nil {\n\t\trender.Error(w, r, errs.InternalServerErr(err))\n\t\treturn\n\t}\n\trender.JSON(w, r, &SSHCheckPrincipalResponse{\n\t\tExists: exists,\n\t})\n}\n\n// SSHGetHosts is the HTTP handler that returns a list of valid ssh hosts.\nfunc SSHGetHosts(w http.ResponseWriter, r *http.Request) {\n\tvar cert *x509.Certificate\n\tif r.TLS != nil && len(r.TLS.PeerCertificates) > 0 {\n\t\tcert = r.TLS.PeerCertificates[0]\n\t}\n\n\tctx := r.Context()\n\thosts, err := mustAuthority(ctx).GetSSHHosts(ctx, cert)\n\tif err != nil {\n\t\trender.Error(w, r, errs.InternalServerErr(err))\n\t\treturn\n\t}\n\trender.JSON(w, r, &SSHGetHostsResponse{\n\t\tHosts: hosts,\n\t})\n}\n\n// SSHBastion provides returns the bastion configured if any.\nfunc SSHBastion(w http.ResponseWriter, r *http.Request) {\n\tvar body SSHBastionRequest\n\tif err := read.JSON(r.Body, &body); err != nil {\n\t\trender.Error(w, r, errs.BadRequestErr(err, \"error reading request body\"))\n\t\treturn\n\t}\n\tif err := body.Validate(); err != nil {\n\t\trender.Error(w, r, err)\n\t\treturn\n\t}\n\n\tctx := r.Context()\n\tbastion, err := mustAuthority(ctx).GetSSHBastion(ctx, body.User, body.Hostname)\n\tif err != nil {\n\t\trender.Error(w, r, errs.InternalServerErr(err))\n\t\treturn\n\t}\n\n\trender.JSON(w, r, &SSHBastionResponse{\n\t\tHostname: body.Hostname,\n\t\tBastion:  bastion,\n\t})\n}\n\n// identityModifier is a custom modifier used to force a fixed duration, and set\n// the identity URI.\ntype identityModifier struct {\n\tIdentity  *url.URL\n\tNotBefore time.Time\n\tNotAfter  time.Time\n}\n\n// Enforce implements the enforcer interface and sets the validity bounds and\n// the identity uri to the certificate.\nfunc (m *identityModifier) Enforce(cert *x509.Certificate) error {\n\tcert.NotBefore = m.NotBefore\n\tcert.NotAfter = m.NotAfter\n\tif m.Identity != nil {\n\t\tvar identityURL = m.Identity.String()\n\t\tfor _, u := range cert.URIs {\n\t\t\tif u.String() == identityURL {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\t\tcert.URIs = append(cert.URIs, m.Identity)\n\t}\n\n\treturn nil\n}\n\n// getIdentityURI returns the first valid UUID URN from the given CSR.\nfunc getIdentityURI(cr *x509.CertificateRequest) *url.URL {\n\tfor _, u := range cr.URIs {\n\t\ts := u.String()\n\t\t// urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx\n\t\tif len(s) == 9+36 && strings.EqualFold(s[:9], \"urn:uuid:\") {\n\t\t\tif _, err := uuid.Parse(s); err == nil {\n\t\t\t\treturn u\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "api/sshRekey.go",
    "content": "package api\n\nimport (\n\t\"net/http\"\n\t\"time\"\n\n\t\"golang.org/x/crypto/ssh\"\n\n\t\"github.com/smallstep/certificates/api/read\"\n\t\"github.com/smallstep/certificates/api/render\"\n\t\"github.com/smallstep/certificates/authority/provisioner\"\n\t\"github.com/smallstep/certificates/errs\"\n\t\"github.com/smallstep/certificates/internal/cast\"\n)\n\n// SSHRekeyRequest is the request body of an SSH certificate request.\ntype SSHRekeyRequest struct {\n\tOTT       string `json:\"ott\"`\n\tPublicKey []byte `json:\"publicKey\"` //base64 encoded\n}\n\n// Validate validates the SSHSignRekey.\nfunc (s *SSHRekeyRequest) Validate() error {\n\tswitch {\n\tcase s.OTT == \"\":\n\t\treturn errs.BadRequest(\"missing or empty ott\")\n\tcase len(s.PublicKey) == 0:\n\t\treturn errs.BadRequest(\"missing or empty public key\")\n\tdefault:\n\t\treturn nil\n\t}\n}\n\n// SSHRekeyResponse is the response object that returns the SSH certificate.\ntype SSHRekeyResponse struct {\n\tCertificate         SSHCertificate `json:\"crt\"`\n\tIdentityCertificate []Certificate  `json:\"identityCrt,omitempty\"`\n}\n\n// SSHRekey is an HTTP handler that reads an RekeySSHRequest with a one-time-token\n// (ott) from the body and creates a new SSH certificate with the information in\n// the request.\nfunc SSHRekey(w http.ResponseWriter, r *http.Request) {\n\tvar body SSHRekeyRequest\n\tif err := read.JSON(r.Body, &body); err != nil {\n\t\trender.Error(w, r, errs.BadRequestErr(err, \"error reading request body\"))\n\t\treturn\n\t}\n\n\tlogOtt(w, body.OTT)\n\tif err := body.Validate(); err != nil {\n\t\trender.Error(w, r, err)\n\t\treturn\n\t}\n\n\tpublicKey, err := ssh.ParsePublicKey(body.PublicKey)\n\tif err != nil {\n\t\trender.Error(w, r, errs.BadRequestErr(err, \"error parsing publicKey\"))\n\t\treturn\n\t}\n\n\tctx := provisioner.NewContextWithMethod(r.Context(), provisioner.SSHRekeyMethod)\n\tctx = provisioner.NewContextWithToken(ctx, body.OTT)\n\n\ta := mustAuthority(ctx)\n\tsignOpts, err := a.Authorize(ctx, body.OTT)\n\tif err != nil {\n\t\trender.Error(w, r, errs.UnauthorizedErr(err))\n\t\treturn\n\t}\n\toldCert, _, err := provisioner.ExtractSSHPOPCert(body.OTT)\n\tif err != nil {\n\t\trender.Error(w, r, errs.InternalServerErr(err))\n\t\treturn\n\t}\n\n\tnewCert, err := a.RekeySSH(ctx, oldCert, publicKey, signOpts...)\n\tif err != nil {\n\t\trender.Error(w, r, errs.ForbiddenErr(err, \"error rekeying ssh certificate\"))\n\t\treturn\n\t}\n\n\t// Match identity cert with the SSH cert\n\tnotBefore := time.Unix(cast.Int64(oldCert.ValidAfter), 0)\n\tnotAfter := time.Unix(cast.Int64(oldCert.ValidBefore), 0)\n\n\tidentity, err := renewIdentityCertificate(r, notBefore, notAfter)\n\tif err != nil {\n\t\trender.Error(w, r, errs.ForbiddenErr(err, \"error renewing identity certificate\"))\n\t\treturn\n\t}\n\n\tLogSSHCertificate(w, newCert)\n\trender.JSONStatus(w, r, &SSHRekeyResponse{\n\t\tCertificate:         SSHCertificate{newCert},\n\t\tIdentityCertificate: identity,\n\t}, http.StatusCreated)\n}\n"
  },
  {
    "path": "api/sshRenew.go",
    "content": "package api\n\nimport (\n\t\"crypto/x509\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n\n\t\"github.com/smallstep/certificates/api/read\"\n\t\"github.com/smallstep/certificates/api/render\"\n\t\"github.com/smallstep/certificates/authority/provisioner\"\n\t\"github.com/smallstep/certificates/errs\"\n\t\"github.com/smallstep/certificates/internal/cast\"\n)\n\n// SSHRenewRequest is the request body of an SSH certificate request.\ntype SSHRenewRequest struct {\n\tOTT string `json:\"ott\"`\n}\n\n// Validate validates the SSHSignRequest.\nfunc (s *SSHRenewRequest) Validate() error {\n\tswitch s.OTT {\n\tcase \"\":\n\t\treturn errs.BadRequest(\"missing or empty ott\")\n\tdefault:\n\t\treturn nil\n\t}\n}\n\n// SSHRenewResponse is the response object that returns the SSH certificate.\ntype SSHRenewResponse struct {\n\tCertificate         SSHCertificate `json:\"crt\"`\n\tIdentityCertificate []Certificate  `json:\"identityCrt,omitempty\"`\n}\n\n// SSHRenew is an HTTP handler that reads an RenewSSHRequest with a one-time-token\n// (ott) from the body and creates a new SSH certificate with the information in\n// the request.\nfunc SSHRenew(w http.ResponseWriter, r *http.Request) {\n\tvar body SSHRenewRequest\n\tif err := read.JSON(r.Body, &body); err != nil {\n\t\trender.Error(w, r, errs.BadRequestErr(err, \"error reading request body\"))\n\t\treturn\n\t}\n\n\tlogOtt(w, body.OTT)\n\tif err := body.Validate(); err != nil {\n\t\trender.Error(w, r, err)\n\t\treturn\n\t}\n\n\tctx := provisioner.NewContextWithMethod(r.Context(), provisioner.SSHRenewMethod)\n\tctx = provisioner.NewContextWithToken(ctx, body.OTT)\n\n\ta := mustAuthority(ctx)\n\t_, err := a.Authorize(ctx, body.OTT)\n\tif err != nil {\n\t\trender.Error(w, r, errs.UnauthorizedErr(err))\n\t\treturn\n\t}\n\toldCert, _, err := provisioner.ExtractSSHPOPCert(body.OTT)\n\tif err != nil {\n\t\trender.Error(w, r, errs.InternalServerErr(err))\n\t\treturn\n\t}\n\n\tnewCert, err := a.RenewSSH(ctx, oldCert)\n\tif err != nil {\n\t\trender.Error(w, r, errs.ForbiddenErr(err, \"error renewing ssh certificate\"))\n\t\treturn\n\t}\n\n\t// Match identity cert with the SSH cert\n\tnotBefore := time.Unix(cast.Int64(oldCert.ValidAfter), 0)\n\tnotAfter := time.Unix(cast.Int64(oldCert.ValidBefore), 0)\n\n\tidentity, err := renewIdentityCertificate(r, notBefore, notAfter)\n\tif err != nil {\n\t\trender.Error(w, r, errs.ForbiddenErr(err, \"error renewing identity certificate\"))\n\t\treturn\n\t}\n\n\tLogSSHCertificate(w, newCert)\n\trender.JSONStatus(w, r, &SSHSignResponse{\n\t\tCertificate:         SSHCertificate{newCert},\n\t\tIdentityCertificate: identity,\n\t}, http.StatusCreated)\n}\n\n// renewIdentityCertificate request the client TLS certificate if present. If notBefore and notAfter are passed the\nfunc renewIdentityCertificate(r *http.Request, notBefore, notAfter time.Time) ([]Certificate, error) {\n\tif r.TLS == nil || len(r.TLS.PeerCertificates) == 0 {\n\t\treturn nil, nil\n\t}\n\n\t// Clone the certificate as we can modify it.\n\tcert, err := x509.ParseCertificate(r.TLS.PeerCertificates[0].Raw)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"error parsing client certificate\")\n\t}\n\n\t// Enforce the cert to match another certificate, for example an ssh\n\t// certificate.\n\tif !notBefore.IsZero() {\n\t\tcert.NotBefore = notBefore\n\t}\n\tif !notAfter.IsZero() {\n\t\tcert.NotAfter = notAfter\n\t}\n\n\tcertChain, err := mustAuthority(r.Context()).Renew(cert)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn certChainToPEM(certChain), nil\n}\n"
  },
  {
    "path": "api/sshRevoke.go",
    "content": "package api\n\nimport (\n\t\"net/http\"\n\n\t\"golang.org/x/crypto/ocsp\"\n\n\t\"github.com/smallstep/certificates/api/read\"\n\t\"github.com/smallstep/certificates/api/render\"\n\t\"github.com/smallstep/certificates/authority\"\n\t\"github.com/smallstep/certificates/authority/provisioner\"\n\t\"github.com/smallstep/certificates/errs\"\n\t\"github.com/smallstep/certificates/logging\"\n)\n\n// SSHRevokeResponse is the response object that returns the health of the server.\ntype SSHRevokeResponse struct {\n\tStatus string `json:\"status\"`\n}\n\n// SSHRevokeRequest is the request body for a revocation request.\ntype SSHRevokeRequest struct {\n\tSerial     string `json:\"serial\"`\n\tOTT        string `json:\"ott\"`\n\tReasonCode int    `json:\"reasonCode\"`\n\tReason     string `json:\"reason\"`\n\tPassive    bool   `json:\"passive\"`\n}\n\n// Validate checks the fields of the RevokeRequest and returns nil if they are ok\n// or an error if something is wrong.\nfunc (r *SSHRevokeRequest) Validate() (err error) {\n\tif r.Serial == \"\" {\n\t\treturn errs.BadRequest(\"missing serial\")\n\t}\n\tif r.ReasonCode < ocsp.Unspecified || r.ReasonCode > ocsp.AACompromise {\n\t\treturn errs.BadRequest(\"reasonCode out of bounds\")\n\t}\n\tif !r.Passive {\n\t\treturn errs.NotImplemented(\"non-passive revocation not implemented\")\n\t}\n\tif r.OTT == \"\" {\n\t\treturn errs.BadRequest(\"missing ott\")\n\t}\n\treturn\n}\n\n// Revoke supports handful of different methods that revoke a Certificate.\n//\n// NOTE: currently only Passive revocation is supported.\nfunc SSHRevoke(w http.ResponseWriter, r *http.Request) {\n\tvar body SSHRevokeRequest\n\tif err := read.JSON(r.Body, &body); err != nil {\n\t\trender.Error(w, r, errs.BadRequestErr(err, \"error reading request body\"))\n\t\treturn\n\t}\n\n\tif err := body.Validate(); err != nil {\n\t\trender.Error(w, r, err)\n\t\treturn\n\t}\n\n\topts := &authority.RevokeOptions{\n\t\tSerial:      body.Serial,\n\t\tReason:      body.Reason,\n\t\tReasonCode:  body.ReasonCode,\n\t\tPassiveOnly: body.Passive,\n\t}\n\n\tctx := provisioner.NewContextWithMethod(r.Context(), provisioner.SSHRevokeMethod)\n\ta := mustAuthority(ctx)\n\n\tlogOtt(w, body.OTT)\n\n\tif _, err := a.Authorize(ctx, body.OTT); err != nil {\n\t\trender.Error(w, r, errs.UnauthorizedErr(err))\n\t\treturn\n\t}\n\n\topts.OTT = body.OTT\n\n\tif err := a.Revoke(ctx, opts); err != nil {\n\t\trender.Error(w, r, errs.ForbiddenErr(err, \"error revoking ssh certificate\"))\n\t\treturn\n\t}\n\n\tlogSSHRevoke(w, opts)\n\trender.JSON(w, r, &SSHRevokeResponse{Status: \"ok\"})\n}\n\nfunc logSSHRevoke(w http.ResponseWriter, ri *authority.RevokeOptions) {\n\tif rl, ok := w.(logging.ResponseLogger); ok {\n\t\trl.WithFields(map[string]interface{}{\n\t\t\t\"serial\":      ri.Serial,\n\t\t\t\"reasonCode\":  ri.ReasonCode,\n\t\t\t\"reason\":      ri.Reason,\n\t\t\t\"passiveOnly\": ri.PassiveOnly,\n\t\t\t\"mTLS\":        ri.MTLS,\n\t\t\t\"ssh\":         true,\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "api/ssh_test.go",
    "content": "package api\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/ecdsa\"\n\t\"crypto/elliptic\"\n\t\"crypto/rand\"\n\t\"crypto/x509\"\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"reflect\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/google/uuid\"\n\t\"github.com/smallstep/certificates/authority\"\n\t\"github.com/smallstep/certificates/authority/provisioner\"\n\t\"github.com/smallstep/certificates/logging\"\n\t\"github.com/smallstep/certificates/templates\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"golang.org/x/crypto/ssh\"\n)\n\nvar (\n\tsshSignerKey = mustKey()\n\tsshUserKey   = mustKey()\n\tsshHostKey   = mustKey()\n)\n\nfunc mustKey() *ecdsa.PrivateKey {\n\tpriv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn priv\n}\n\nfunc signSSHCertificate(cert *ssh.Certificate) error {\n\tsignerKey, err := ssh.NewPublicKey(sshSignerKey.Public())\n\tif err != nil {\n\t\treturn err\n\t}\n\tsigner, err := ssh.NewSignerFromSigner(sshSignerKey)\n\tif err != nil {\n\t\treturn err\n\t}\n\tcert.SignatureKey = signerKey\n\tdata := cert.Marshal()\n\tdata = data[:len(data)-4]\n\tsig, err := signer.Sign(rand.Reader, data)\n\tif err != nil {\n\t\treturn err\n\t}\n\tcert.Signature = sig\n\treturn nil\n}\n\nfunc getSignedUserCertificate() (*ssh.Certificate, error) {\n\tkey, err := ssh.NewPublicKey(sshUserKey.Public())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tt := time.Now()\n\tcert := &ssh.Certificate{\n\t\tNonce:           []byte(\"1234567890\"),\n\t\tKey:             key,\n\t\tSerial:          1234567890,\n\t\tCertType:        ssh.UserCert,\n\t\tKeyId:           \"user@localhost\",\n\t\tValidPrincipals: []string{\"user\"},\n\t\tValidAfter:      uint64(t.Unix()),\n\t\tValidBefore:     uint64(t.Add(time.Hour).Unix()),\n\t\tPermissions: ssh.Permissions{\n\t\t\tCriticalOptions: map[string]string{},\n\t\t\tExtensions: map[string]string{\n\t\t\t\t\"permit-X11-forwarding\":   \"\",\n\t\t\t\t\"permit-agent-forwarding\": \"\",\n\t\t\t\t\"permit-port-forwarding\":  \"\",\n\t\t\t\t\"permit-pty\":              \"\",\n\t\t\t\t\"permit-user-rc\":          \"\",\n\t\t\t},\n\t\t},\n\t\tReserved: []byte{},\n\t}\n\tif err := signSSHCertificate(cert); err != nil {\n\t\treturn nil, err\n\t}\n\treturn cert, nil\n}\n\nfunc getSignedHostCertificate() (*ssh.Certificate, error) {\n\tkey, err := ssh.NewPublicKey(sshHostKey.Public())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tt := time.Now()\n\tcert := &ssh.Certificate{\n\t\tNonce:           []byte(\"1234567890\"),\n\t\tKey:             key,\n\t\tSerial:          1234567890,\n\t\tCertType:        ssh.UserCert,\n\t\tKeyId:           \"internal.smallstep.com\",\n\t\tValidPrincipals: []string{\"internal.smallstep.com\"},\n\t\tValidAfter:      uint64(t.Unix()),\n\t\tValidBefore:     uint64(t.Add(time.Hour).Unix()),\n\t\tPermissions: ssh.Permissions{\n\t\t\tCriticalOptions: map[string]string{},\n\t\t\tExtensions:      map[string]string{},\n\t\t},\n\t\tReserved: []byte{},\n\t}\n\tif err := signSSHCertificate(cert); err != nil {\n\t\treturn nil, err\n\t}\n\treturn cert, nil\n}\n\nfunc TestSSHCertificate_MarshalJSON(t *testing.T) {\n\tuser, err := getSignedUserCertificate()\n\trequire.NoError(t, err)\n\thost, err := getSignedHostCertificate()\n\trequire.NoError(t, err)\n\tuserB64 := base64.StdEncoding.EncodeToString(user.Marshal())\n\thostB64 := base64.StdEncoding.EncodeToString(host.Marshal())\n\n\ttype fields struct {\n\t\tCertificate *ssh.Certificate\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tfields  fields\n\t\twant    []byte\n\t\twantErr bool\n\t}{\n\t\t{\"nil\", fields{Certificate: nil}, []byte(\"null\"), false},\n\t\t{\"user\", fields{Certificate: user}, []byte(`\"` + userB64 + `\"`), false},\n\t\t{\"user\", fields{Certificate: host}, []byte(`\"` + hostB64 + `\"`), false},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tc := SSHCertificate{\n\t\t\t\tCertificate: tt.fields.Certificate,\n\t\t\t}\n\t\t\tgot, err := c.MarshalJSON()\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"SSHCertificate.MarshalJSON() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"SSHCertificate.MarshalJSON() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSSHCertificate_UnmarshalJSON(t *testing.T) {\n\tuser, err := getSignedUserCertificate()\n\trequire.NoError(t, err)\n\thost, err := getSignedHostCertificate()\n\trequire.NoError(t, err)\n\tuserB64 := base64.StdEncoding.EncodeToString(user.Marshal())\n\thostB64 := base64.StdEncoding.EncodeToString(host.Marshal())\n\tkeyB64 := base64.StdEncoding.EncodeToString(user.Key.Marshal())\n\n\ttype args struct {\n\t\tdata []byte\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\targs    args\n\t\twant    *ssh.Certificate\n\t\twantErr bool\n\t}{\n\t\t{\"null\", args{[]byte(`null`)}, nil, false},\n\t\t{\"empty\", args{[]byte(`\"\"`)}, nil, false},\n\t\t{\"user\", args{[]byte(`\"` + userB64 + `\"`)}, user, false},\n\t\t{\"host\", args{[]byte(`\"` + hostB64 + `\"`)}, host, false},\n\t\t{\"bad-string\", args{[]byte(userB64)}, nil, true},\n\t\t{\"bad-base64\", args{[]byte(`\"this-is-not-base64\"`)}, nil, true},\n\t\t{\"bad-key\", args{[]byte(`\"bm90LWEta2V5\"`)}, nil, true},\n\t\t{\"bat-cert\", args{[]byte(`\"` + keyB64 + `\"`)}, nil, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tc := &SSHCertificate{}\n\t\t\tif err := c.UnmarshalJSON(tt.args.data); (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"SSHCertificate.UnmarshalJSON() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(tt.want, c.Certificate) {\n\t\t\t\tt.Errorf(\"SSHCertificate.UnmarshalJSON() got = %v, want %v\\n\", c.Certificate, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSignSSHRequest_Validate(t *testing.T) {\n\tcsr := parseCertificateRequest(csrPEM)\n\tbadCSR := parseCertificateRequest(csrPEM)\n\tbadCSR.SignatureAlgorithm = x509.SHA1WithRSA\n\n\ttype fields struct {\n\t\tPublicKey        []byte\n\t\tOTT              string\n\t\tCertType         string\n\t\tPrincipals       []string\n\t\tValidAfter       TimeDuration\n\t\tValidBefore      TimeDuration\n\t\tAddUserPublicKey []byte\n\t\tKeyID            string\n\t\tIdentityCSR      CertificateRequest\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tfields  fields\n\t\twantErr bool\n\t}{\n\t\t{\"ok-empty\", fields{[]byte(\"Zm9v\"), \"ott\", \"\", []string{\"user\"}, TimeDuration{}, TimeDuration{}, nil, \"\", CertificateRequest{}}, false},\n\t\t{\"ok-user\", fields{[]byte(\"Zm9v\"), \"ott\", \"user\", []string{\"user\"}, TimeDuration{}, TimeDuration{}, nil, \"\", CertificateRequest{}}, false},\n\t\t{\"ok-host\", fields{[]byte(\"Zm9v\"), \"ott\", \"host\", []string{\"user\"}, TimeDuration{}, TimeDuration{}, nil, \"\", CertificateRequest{}}, false},\n\t\t{\"ok-keyID\", fields{[]byte(\"Zm9v\"), \"ott\", \"user\", []string{\"user\"}, TimeDuration{}, TimeDuration{}, nil, \"key-id\", CertificateRequest{}}, false},\n\t\t{\"ok-identityCSR\", fields{[]byte(\"Zm9v\"), \"ott\", \"user\", []string{\"user\"}, TimeDuration{}, TimeDuration{}, nil, \"key-id\", CertificateRequest{CertificateRequest: csr}}, false},\n\t\t{\"key\", fields{nil, \"ott\", \"user\", []string{\"user\"}, TimeDuration{}, TimeDuration{}, nil, \"\", CertificateRequest{}}, true},\n\t\t{\"key\", fields{[]byte(\"\"), \"ott\", \"user\", []string{\"user\"}, TimeDuration{}, TimeDuration{}, nil, \"\", CertificateRequest{}}, true},\n\t\t{\"type\", fields{[]byte(\"Zm9v\"), \"ott\", \"foo\", []string{\"user\"}, TimeDuration{}, TimeDuration{}, nil, \"\", CertificateRequest{}}, true},\n\t\t{\"ott\", fields{[]byte(\"Zm9v\"), \"\", \"user\", []string{\"user\"}, TimeDuration{}, TimeDuration{}, nil, \"\", CertificateRequest{}}, true},\n\t\t{\"identityCSR\", fields{[]byte(\"Zm9v\"), \"ott\", \"user\", []string{\"user\"}, TimeDuration{}, TimeDuration{}, nil, \"key-id\", CertificateRequest{CertificateRequest: badCSR}}, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ts := &SSHSignRequest{\n\t\t\t\tPublicKey:        tt.fields.PublicKey,\n\t\t\t\tOTT:              tt.fields.OTT,\n\t\t\t\tCertType:         tt.fields.CertType,\n\t\t\t\tPrincipals:       tt.fields.Principals,\n\t\t\t\tValidAfter:       tt.fields.ValidAfter,\n\t\t\t\tValidBefore:      tt.fields.ValidBefore,\n\t\t\t\tAddUserPublicKey: tt.fields.AddUserPublicKey,\n\t\t\t\tKeyID:            tt.fields.KeyID,\n\t\t\t\tIdentityCSR:      tt.fields.IdentityCSR,\n\t\t\t}\n\t\t\tif err := s.Validate(); (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"SignSSHRequest.Validate() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_SSHSign(t *testing.T) {\n\tuser, err := getSignedUserCertificate()\n\trequire.NoError(t, err)\n\thost, err := getSignedHostCertificate()\n\trequire.NoError(t, err)\n\n\tuserB64 := base64.StdEncoding.EncodeToString(user.Marshal())\n\thostB64 := base64.StdEncoding.EncodeToString(host.Marshal())\n\n\tuserReq, err := json.Marshal(SSHSignRequest{\n\t\tPublicKey: user.Key.Marshal(),\n\t\tOTT:       \"ott\",\n\t})\n\trequire.NoError(t, err)\n\thostReq, err := json.Marshal(SSHSignRequest{\n\t\tPublicKey: host.Key.Marshal(),\n\t\tOTT:       \"ott\",\n\t})\n\trequire.NoError(t, err)\n\tuserAddReq, err := json.Marshal(SSHSignRequest{\n\t\tPublicKey:        user.Key.Marshal(),\n\t\tOTT:              \"ott\",\n\t\tAddUserPublicKey: user.Key.Marshal(),\n\t})\n\trequire.NoError(t, err)\n\tuserIdentityReq, err := json.Marshal(SSHSignRequest{\n\t\tPublicKey:   user.Key.Marshal(),\n\t\tOTT:         \"ott\",\n\t\tIdentityCSR: CertificateRequest{parseCertificateRequest(csrPEM)},\n\t})\n\trequire.NoError(t, err)\n\tidentityCerts := []*x509.Certificate{\n\t\tparseCertificate(certPEM),\n\t}\n\tidentityCertsPEM := []byte(`\"` + strings.ReplaceAll(certPEM, \"\\n\", `\\n`) + `\\n\"`)\n\n\ttests := []struct {\n\t\tname         string\n\t\treq          []byte\n\t\tauthErr      error\n\t\tsignCert     *ssh.Certificate\n\t\tsignErr      error\n\t\taddUserCert  *ssh.Certificate\n\t\taddUserErr   error\n\t\ttlsSignCerts []*x509.Certificate\n\t\ttlsSignErr   error\n\t\tbody         []byte\n\t\tstatusCode   int\n\t}{\n\t\t{\"ok-user\", userReq, nil, user, nil, nil, nil, nil, nil, []byte(fmt.Sprintf(`{\"crt\":%q}`, userB64)), http.StatusCreated},\n\t\t{\"ok-host\", hostReq, nil, host, nil, nil, nil, nil, nil, []byte(fmt.Sprintf(`{\"crt\":%q}`, hostB64)), http.StatusCreated},\n\t\t{\"ok-user-add\", userAddReq, nil, user, nil, user, nil, nil, nil, []byte(fmt.Sprintf(`{\"crt\":%q,\"addUserCrt\":%q}`, userB64, userB64)), http.StatusCreated},\n\t\t{\"ok-user-identity\", userIdentityReq, nil, user, nil, user, nil, identityCerts, nil, []byte(fmt.Sprintf(`{\"crt\":%q,\"identityCrt\":[%s]}`, userB64, identityCertsPEM)), http.StatusCreated},\n\t\t{\"fail-body\", []byte(\"bad-json\"), nil, nil, nil, nil, nil, nil, nil, nil, http.StatusBadRequest},\n\t\t{\"fail-validate\", []byte(\"{}\"), nil, nil, nil, nil, nil, nil, nil, nil, http.StatusBadRequest},\n\t\t{\"fail-publicKey\", []byte(`{\"publicKey\":\"Zm9v\",\"ott\":\"ott\"}`), nil, nil, nil, nil, nil, nil, nil, nil, http.StatusBadRequest},\n\t\t{\"fail-publicKey\", []byte(fmt.Sprintf(`{\"publicKey\":%q,\"ott\":\"ott\",\"addUserPublicKey\":\"Zm9v\"}`, base64.StdEncoding.EncodeToString(user.Key.Marshal()))), nil, nil, nil, nil, nil, nil, nil, nil, http.StatusBadRequest},\n\t\t{\"fail-authorize\", userReq, fmt.Errorf(\"an-error\"), nil, nil, nil, nil, nil, nil, nil, http.StatusUnauthorized},\n\t\t{\"fail-signSSH\", userReq, nil, nil, fmt.Errorf(\"an-error\"), nil, nil, nil, nil, nil, http.StatusForbidden},\n\t\t{\"fail-SignSSHAddUser\", userAddReq, nil, user, nil, nil, fmt.Errorf(\"an-error\"), nil, nil, nil, http.StatusForbidden},\n\t\t{\"fail-user-identity\", userIdentityReq, nil, user, nil, user, nil, nil, fmt.Errorf(\"an-error\"), nil, http.StatusForbidden},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tmockMustAuthority(t, &mockAuthority{\n\t\t\t\tauthorize: func(ctx context.Context, ott string) ([]provisioner.SignOption, error) {\n\t\t\t\t\treturn []provisioner.SignOption{}, tt.authErr\n\t\t\t\t},\n\t\t\t\tsignSSH: func(ctx context.Context, key ssh.PublicKey, opts provisioner.SignSSHOptions, signOpts ...provisioner.SignOption) (*ssh.Certificate, error) {\n\t\t\t\t\treturn tt.signCert, tt.signErr\n\t\t\t\t},\n\t\t\t\tsignSSHAddUser: func(ctx context.Context, key ssh.PublicKey, cert *ssh.Certificate) (*ssh.Certificate, error) {\n\t\t\t\t\treturn tt.addUserCert, tt.addUserErr\n\t\t\t\t},\n\t\t\t\tsignWithContext: func(ctx context.Context, cr *x509.CertificateRequest, opts provisioner.SignOptions, signOpts ...provisioner.SignOption) ([]*x509.Certificate, error) {\n\t\t\t\t\treturn tt.tlsSignCerts, tt.tlsSignErr\n\t\t\t\t},\n\t\t\t})\n\n\t\t\treq := httptest.NewRequest(\"POST\", \"http://example.com/ssh/sign\", bytes.NewReader(tt.req))\n\t\t\tw := httptest.NewRecorder()\n\t\t\tSSHSign(logging.NewResponseLogger(w), req)\n\t\t\tres := w.Result()\n\n\t\t\tif res.StatusCode != tt.statusCode {\n\t\t\t\tt.Errorf(\"caHandler.SignSSH StatusCode = %d, wants %d\", res.StatusCode, tt.statusCode)\n\t\t\t}\n\n\t\t\tbody, err := io.ReadAll(res.Body)\n\t\t\tres.Body.Close()\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"caHandler.SignSSH unexpected error = %v\", err)\n\t\t\t}\n\t\t\tif tt.statusCode < http.StatusBadRequest {\n\t\t\t\tif !bytes.Equal(bytes.TrimSpace(body), tt.body) {\n\t\t\t\t\tt.Errorf(\"caHandler.SignSSH Body = %s, wants %s\", body, tt.body)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_SSHRoots(t *testing.T) {\n\tuser, err := ssh.NewPublicKey(sshUserKey.Public())\n\trequire.NoError(t, err)\n\tuserB64 := base64.StdEncoding.EncodeToString(user.Marshal())\n\n\thost, err := ssh.NewPublicKey(sshHostKey.Public())\n\trequire.NoError(t, err)\n\thostB64 := base64.StdEncoding.EncodeToString(host.Marshal())\n\n\ttests := []struct {\n\t\tname       string\n\t\tkeys       *authority.SSHKeys\n\t\tkeysErr    error\n\t\tbody       []byte\n\t\tstatusCode int\n\t}{\n\t\t{\"ok\", &authority.SSHKeys{HostKeys: []ssh.PublicKey{host}, UserKeys: []ssh.PublicKey{user}}, nil, []byte(fmt.Sprintf(`{\"userKey\":[%q],\"hostKey\":[%q]}`, userB64, hostB64)), http.StatusOK},\n\t\t{\"many\", &authority.SSHKeys{HostKeys: []ssh.PublicKey{host, host}, UserKeys: []ssh.PublicKey{user, user}}, nil, []byte(fmt.Sprintf(`{\"userKey\":[%q,%q],\"hostKey\":[%q,%q]}`, userB64, userB64, hostB64, hostB64)), http.StatusOK},\n\t\t{\"user\", &authority.SSHKeys{UserKeys: []ssh.PublicKey{user}}, nil, []byte(fmt.Sprintf(`{\"userKey\":[%q]}`, userB64)), http.StatusOK},\n\t\t{\"host\", &authority.SSHKeys{HostKeys: []ssh.PublicKey{host}}, nil, []byte(fmt.Sprintf(`{\"hostKey\":[%q]}`, hostB64)), http.StatusOK},\n\t\t{\"empty\", &authority.SSHKeys{}, nil, nil, http.StatusNotFound},\n\t\t{\"error\", nil, fmt.Errorf(\"an error\"), nil, http.StatusInternalServerError},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tmockMustAuthority(t, &mockAuthority{\n\t\t\t\tgetSSHRoots: func(ctx context.Context) (*authority.SSHKeys, error) {\n\t\t\t\t\treturn tt.keys, tt.keysErr\n\t\t\t\t},\n\t\t\t})\n\n\t\t\treq := httptest.NewRequest(\"GET\", \"http://example.com/ssh/roots\", http.NoBody)\n\t\t\tw := httptest.NewRecorder()\n\t\t\tSSHRoots(logging.NewResponseLogger(w), req)\n\t\t\tres := w.Result()\n\n\t\t\tif res.StatusCode != tt.statusCode {\n\t\t\t\tt.Errorf(\"caHandler.SSHRoots StatusCode = %d, wants %d\", res.StatusCode, tt.statusCode)\n\t\t\t}\n\n\t\t\tbody, err := io.ReadAll(res.Body)\n\t\t\tres.Body.Close()\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"caHandler.SSHRoots unexpected error = %v\", err)\n\t\t\t}\n\t\t\tif tt.statusCode < http.StatusBadRequest {\n\t\t\t\tif !bytes.Equal(bytes.TrimSpace(body), tt.body) {\n\t\t\t\t\tt.Errorf(\"caHandler.SSHRoots Body = %s, wants %s\", body, tt.body)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_SSHFederation(t *testing.T) {\n\tuser, err := ssh.NewPublicKey(sshUserKey.Public())\n\trequire.NoError(t, err)\n\tuserB64 := base64.StdEncoding.EncodeToString(user.Marshal())\n\n\thost, err := ssh.NewPublicKey(sshHostKey.Public())\n\trequire.NoError(t, err)\n\thostB64 := base64.StdEncoding.EncodeToString(host.Marshal())\n\n\ttests := []struct {\n\t\tname       string\n\t\tkeys       *authority.SSHKeys\n\t\tkeysErr    error\n\t\tbody       []byte\n\t\tstatusCode int\n\t}{\n\t\t{\"ok\", &authority.SSHKeys{HostKeys: []ssh.PublicKey{host}, UserKeys: []ssh.PublicKey{user}}, nil, []byte(fmt.Sprintf(`{\"userKey\":[%q],\"hostKey\":[%q]}`, userB64, hostB64)), http.StatusOK},\n\t\t{\"many\", &authority.SSHKeys{HostKeys: []ssh.PublicKey{host, host}, UserKeys: []ssh.PublicKey{user, user}}, nil, []byte(fmt.Sprintf(`{\"userKey\":[%q,%q],\"hostKey\":[%q,%q]}`, userB64, userB64, hostB64, hostB64)), http.StatusOK},\n\t\t{\"user\", &authority.SSHKeys{UserKeys: []ssh.PublicKey{user}}, nil, []byte(fmt.Sprintf(`{\"userKey\":[%q]}`, userB64)), http.StatusOK},\n\t\t{\"host\", &authority.SSHKeys{HostKeys: []ssh.PublicKey{host}}, nil, []byte(fmt.Sprintf(`{\"hostKey\":[%q]}`, hostB64)), http.StatusOK},\n\t\t{\"empty\", &authority.SSHKeys{}, nil, nil, http.StatusNotFound},\n\t\t{\"error\", nil, fmt.Errorf(\"an error\"), nil, http.StatusInternalServerError},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tmockMustAuthority(t, &mockAuthority{\n\t\t\t\tgetSSHFederation: func(ctx context.Context) (*authority.SSHKeys, error) {\n\t\t\t\t\treturn tt.keys, tt.keysErr\n\t\t\t\t},\n\t\t\t})\n\n\t\t\treq := httptest.NewRequest(\"GET\", \"http://example.com/ssh/federation\", http.NoBody)\n\t\t\tw := httptest.NewRecorder()\n\t\t\tSSHFederation(logging.NewResponseLogger(w), req)\n\t\t\tres := w.Result()\n\n\t\t\tif res.StatusCode != tt.statusCode {\n\t\t\t\tt.Errorf(\"caHandler.SSHFederation StatusCode = %d, wants %d\", res.StatusCode, tt.statusCode)\n\t\t\t}\n\n\t\t\tbody, err := io.ReadAll(res.Body)\n\t\t\tres.Body.Close()\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"caHandler.SSHFederation unexpected error = %v\", err)\n\t\t\t}\n\t\t\tif tt.statusCode < http.StatusBadRequest {\n\t\t\t\tif !bytes.Equal(bytes.TrimSpace(body), tt.body) {\n\t\t\t\t\tt.Errorf(\"caHandler.SSHFederation Body = %s, wants %s\", body, tt.body)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_SSHConfig(t *testing.T) {\n\tuserOutput := []templates.Output{\n\t\t{Name: \"config.tpl\", Type: templates.File, Comment: \"#\", Path: \"ssh/config\", Content: []byte(\"UserKnownHostsFile /home/user/.step/ssh/known_hosts\")},\n\t\t{Name: \"known_host.tpl\", Type: templates.File, Comment: \"#\", Path: \"ssh/known_host\", Content: []byte(\"@cert-authority * ecdsa-sha2-nistp256 AAAA...=\")},\n\t}\n\thostOutput := []templates.Output{\n\t\t{Name: \"sshd_config.tpl\", Type: templates.Snippet, Comment: \"#\", Path: \"/etc/ssh/sshd_config\", Content: []byte(\"TrustedUserCAKeys /etc/ssh/ca.pub\")},\n\t\t{Name: \"ca.tpl\", Type: templates.File, Comment: \"#\", Path: \"/etc/ssh/ca.pub\", Content: []byte(\"ecdsa-sha2-nistp256 AAAA...=\")},\n\t}\n\tuserJSON, err := json.Marshal(userOutput)\n\trequire.NoError(t, err)\n\thostJSON, err := json.Marshal(hostOutput)\n\trequire.NoError(t, err)\n\n\ttests := []struct {\n\t\tname       string\n\t\treq        string\n\t\toutput     []templates.Output\n\t\terr        error\n\t\tbody       []byte\n\t\tstatusCode int\n\t}{\n\t\t{\"user\", `{\"type\":\"user\"}`, userOutput, nil, []byte(fmt.Sprintf(`{\"userTemplates\":%s}`, userJSON)), http.StatusOK},\n\t\t{\"host\", `{\"type\":\"host\"}`, hostOutput, nil, []byte(fmt.Sprintf(`{\"hostTemplates\":%s}`, hostJSON)), http.StatusOK},\n\t\t{\"noType\", `{}`, userOutput, nil, []byte(fmt.Sprintf(`{\"userTemplates\":%s}`, userJSON)), http.StatusOK},\n\t\t{\"badType\", `{\"type\":\"bad\"}`, userOutput, nil, nil, http.StatusBadRequest},\n\t\t{\"badData\", `{\"type\":\"user\",\"data\":{\"bad\"}}`, userOutput, nil, nil, http.StatusBadRequest},\n\t\t{\"error\", `{\"type\": \"user\"}`, nil, fmt.Errorf(\"an error\"), nil, http.StatusInternalServerError},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tmockMustAuthority(t, &mockAuthority{\n\t\t\t\tgetSSHConfig: func(ctx context.Context, typ string, data map[string]string) ([]templates.Output, error) {\n\t\t\t\t\treturn tt.output, tt.err\n\t\t\t\t},\n\t\t\t})\n\n\t\t\treq := httptest.NewRequest(\"GET\", \"http://example.com/ssh/config\", strings.NewReader(tt.req))\n\t\t\tw := httptest.NewRecorder()\n\t\t\tSSHConfig(logging.NewResponseLogger(w), req)\n\t\t\tres := w.Result()\n\n\t\t\tif res.StatusCode != tt.statusCode {\n\t\t\t\tt.Errorf(\"caHandler.SSHConfig StatusCode = %d, wants %d\", res.StatusCode, tt.statusCode)\n\t\t\t}\n\n\t\t\tbody, err := io.ReadAll(res.Body)\n\t\t\tres.Body.Close()\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"caHandler.SSHConfig unexpected error = %v\", err)\n\t\t\t}\n\t\t\tif tt.statusCode < http.StatusBadRequest {\n\t\t\t\tif !bytes.Equal(bytes.TrimSpace(body), tt.body) {\n\t\t\t\t\tt.Errorf(\"caHandler.SSHConfig Body = %s, wants %s\", body, tt.body)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_SSHCheckHost(t *testing.T) {\n\ttests := []struct {\n\t\tname       string\n\t\treq        string\n\t\texists     bool\n\t\terr        error\n\t\tbody       []byte\n\t\tstatusCode int\n\t}{\n\t\t{\"true\", `{\"type\":\"host\",\"principal\":\"foo.example.com\"}`, true, nil, []byte(`{\"exists\":true}`), http.StatusOK},\n\t\t{\"false\", `{\"type\":\"host\",\"principal\":\"bar.example.com\"}`, false, nil, []byte(`{\"exists\":false}`), http.StatusOK},\n\t\t{\"badType\", `{\"type\":\"user\",\"principal\":\"bar.example.com\"}`, false, nil, nil, http.StatusBadRequest},\n\t\t{\"badPrincipal\", `{\"type\":\"host\",\"principal\":\"\"}`, false, nil, nil, http.StatusBadRequest},\n\t\t{\"badRequest\", `{\"foo\"}`, false, nil, nil, http.StatusBadRequest},\n\t\t{\"error\", `{\"type\":\"host\",\"principal\":\"foo.example.com\"}`, false, fmt.Errorf(\"an error\"), nil, http.StatusInternalServerError},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tmockMustAuthority(t, &mockAuthority{\n\t\t\t\tcheckSSHHost: func(ctx context.Context, principal, token string) (bool, error) {\n\t\t\t\t\treturn tt.exists, tt.err\n\t\t\t\t},\n\t\t\t})\n\n\t\t\treq := httptest.NewRequest(\"GET\", \"http://example.com/ssh/check-host\", strings.NewReader(tt.req))\n\t\t\tw := httptest.NewRecorder()\n\t\t\tSSHCheckHost(logging.NewResponseLogger(w), req)\n\t\t\tres := w.Result()\n\n\t\t\tif res.StatusCode != tt.statusCode {\n\t\t\t\tt.Errorf(\"caHandler.SSHCheckHost StatusCode = %d, wants %d\", res.StatusCode, tt.statusCode)\n\t\t\t}\n\n\t\t\tbody, err := io.ReadAll(res.Body)\n\t\t\tres.Body.Close()\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"caHandler.SSHCheckHost unexpected error = %v\", err)\n\t\t\t}\n\t\t\tif tt.statusCode < http.StatusBadRequest {\n\t\t\t\tif !bytes.Equal(bytes.TrimSpace(body), tt.body) {\n\t\t\t\t\tt.Errorf(\"caHandler.SSHCheckHost Body = %s, wants %s\", body, tt.body)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_SSHGetHosts(t *testing.T) {\n\thosts := []authority.Host{\n\t\t{HostID: \"1\", HostTags: []authority.HostTag{{ID: \"1\", Name: \"group\", Value: \"1\"}}, Hostname: \"host1\"},\n\t\t{HostID: \"2\", HostTags: []authority.HostTag{{ID: \"1\", Name: \"group\", Value: \"1\"}, {ID: \"2\", Name: \"group\", Value: \"2\"}}, Hostname: \"host2\"},\n\t}\n\thostsJSON, err := json.Marshal(hosts)\n\trequire.NoError(t, err)\n\n\ttests := []struct {\n\t\tname       string\n\t\thosts      []authority.Host\n\t\terr        error\n\t\tbody       []byte\n\t\tstatusCode int\n\t}{\n\t\t{\"ok\", hosts, nil, []byte(fmt.Sprintf(`{\"hosts\":%s}`, hostsJSON)), http.StatusOK},\n\t\t{\"empty (array)\", []authority.Host{}, nil, []byte(`{\"hosts\":[]}`), http.StatusOK},\n\t\t{\"empty (nil)\", nil, nil, []byte(`{\"hosts\":null}`), http.StatusOK},\n\t\t{\"error\", nil, fmt.Errorf(\"an error\"), nil, http.StatusInternalServerError},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tmockMustAuthority(t, &mockAuthority{\n\t\t\t\tgetSSHHosts: func(context.Context, *x509.Certificate) ([]authority.Host, error) {\n\t\t\t\t\treturn tt.hosts, tt.err\n\t\t\t\t},\n\t\t\t})\n\n\t\t\treq := httptest.NewRequest(\"GET\", \"http://example.com/ssh/host\", http.NoBody)\n\t\t\tw := httptest.NewRecorder()\n\t\t\tSSHGetHosts(logging.NewResponseLogger(w), req)\n\t\t\tres := w.Result()\n\n\t\t\tif res.StatusCode != tt.statusCode {\n\t\t\t\tt.Errorf(\"caHandler.SSHGetHosts StatusCode = %d, wants %d\", res.StatusCode, tt.statusCode)\n\t\t\t}\n\n\t\t\tbody, err := io.ReadAll(res.Body)\n\t\t\tres.Body.Close()\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"caHandler.SSHGetHosts unexpected error = %v\", err)\n\t\t\t}\n\t\t\tif tt.statusCode < http.StatusBadRequest {\n\t\t\t\tif !bytes.Equal(bytes.TrimSpace(body), tt.body) {\n\t\t\t\t\tt.Errorf(\"caHandler.SSHGetHosts Body = %s, wants %s\", body, tt.body)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_SSHBastion(t *testing.T) {\n\tbastion := &authority.Bastion{\n\t\tHostname: \"bastion.local\",\n\t}\n\tbastionPort := &authority.Bastion{\n\t\tHostname: \"bastion.local\",\n\t\tPort:     \"2222\",\n\t}\n\n\ttests := []struct {\n\t\tname       string\n\t\tbastion    *authority.Bastion\n\t\tbastionErr error\n\t\treq        []byte\n\t\tbody       []byte\n\t\tstatusCode int\n\t}{\n\t\t{\"ok\", bastion, nil, []byte(`{\"hostname\":\"host.local\"}`), []byte(`{\"hostname\":\"host.local\",\"bastion\":{\"hostname\":\"bastion.local\"}}`), http.StatusOK},\n\t\t{\"ok\", bastionPort, nil, []byte(`{\"hostname\":\"host.local\",\"user\":\"user\"}`), []byte(`{\"hostname\":\"host.local\",\"bastion\":{\"hostname\":\"bastion.local\",\"port\":\"2222\"}}`), http.StatusOK},\n\t\t{\"empty\", nil, nil, []byte(`{\"hostname\":\"host.local\"}`), []byte(`{\"hostname\":\"host.local\"}`), http.StatusOK},\n\t\t{\"bad json\", bastion, nil, []byte(`bad json`), nil, http.StatusBadRequest},\n\t\t{\"bad request\", bastion, nil, []byte(`{\"hostname\": \"\"}`), nil, http.StatusBadRequest},\n\t\t{\"error\", nil, fmt.Errorf(\"an error\"), []byte(`{\"hostname\":\"host.local\"}`), nil, http.StatusInternalServerError},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tmockMustAuthority(t, &mockAuthority{\n\t\t\t\tgetSSHBastion: func(ctx context.Context, user, hostname string) (*authority.Bastion, error) {\n\t\t\t\t\treturn tt.bastion, tt.bastionErr\n\t\t\t\t},\n\t\t\t})\n\n\t\t\treq := httptest.NewRequest(\"POST\", \"http://example.com/ssh/bastion\", bytes.NewReader(tt.req))\n\t\t\tw := httptest.NewRecorder()\n\t\t\tSSHBastion(logging.NewResponseLogger(w), req)\n\t\t\tres := w.Result()\n\n\t\t\tif res.StatusCode != tt.statusCode {\n\t\t\t\tt.Errorf(\"caHandler.SSHBastion StatusCode = %d, wants %d\", res.StatusCode, tt.statusCode)\n\t\t\t}\n\n\t\t\tbody, err := io.ReadAll(res.Body)\n\t\t\tres.Body.Close()\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"caHandler.SSHBastion unexpected error = %v\", err)\n\t\t\t}\n\t\t\tif tt.statusCode < http.StatusBadRequest {\n\t\t\t\tif !bytes.Equal(bytes.TrimSpace(body), tt.body) {\n\t\t\t\t\tt.Errorf(\"caHandler.SSHBastion Body = %s, wants %s\", body, tt.body)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSSHPublicKey_MarshalJSON(t *testing.T) {\n\tkey, err := ssh.NewPublicKey(sshUserKey.Public())\n\trequire.NoError(t, err)\n\tkeyB64 := base64.StdEncoding.EncodeToString(key.Marshal())\n\n\ttests := []struct {\n\t\tname      string\n\t\tpublicKey *SSHPublicKey\n\t\twant      []byte\n\t\twantErr   bool\n\t}{\n\t\t{\"ok\", &SSHPublicKey{PublicKey: key}, []byte(`\"` + keyB64 + `\"`), false},\n\t\t{\"null\", nil, []byte(\"null\"), false},\n\t\t{\"null\", &SSHPublicKey{PublicKey: nil}, []byte(\"null\"), false},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := tt.publicKey.MarshalJSON()\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"SSHPublicKey.MarshalJSON() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"SSHPublicKey.MarshalJSON() = %s, want %s\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSSHPublicKey_UnmarshalJSON(t *testing.T) {\n\tkey, err := ssh.NewPublicKey(sshUserKey.Public())\n\trequire.NoError(t, err)\n\tkeyB64 := base64.StdEncoding.EncodeToString(key.Marshal())\n\n\ttype args struct {\n\t\tdata []byte\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\targs    args\n\t\twant    *SSHPublicKey\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", args{[]byte(`\"` + keyB64 + `\"`)}, &SSHPublicKey{PublicKey: key}, false},\n\t\t{\"empty\", args{[]byte(`\"\"`)}, &SSHPublicKey{}, false},\n\t\t{\"null\", args{[]byte(`null`)}, &SSHPublicKey{}, false},\n\t\t{\"noString\", args{[]byte(\"123\")}, &SSHPublicKey{}, true},\n\t\t{\"badB64\", args{[]byte(`\"bad\"`)}, &SSHPublicKey{}, true},\n\t\t{\"badKey\", args{[]byte(`\"Zm9vYmFyCg==\"`)}, &SSHPublicKey{}, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tp := &SSHPublicKey{}\n\t\t\tif err := p.UnmarshalJSON(tt.args.data); (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"SSHPublicKey.UnmarshalJSON() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(p, tt.want) {\n\t\t\t\tt.Errorf(\"SSHPublicKey.UnmarshalJSON() = %v, want %v\", p, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_identityModifier_Enforce(t *testing.T) {\n\tnow := time.Now()\n\ttype fields struct {\n\t\tIdentity  *url.URL\n\t\tNotBefore time.Time\n\t\tNotAfter  time.Time\n\t}\n\ttype args struct {\n\t\tcert *x509.Certificate\n\t}\n\ttests := []struct {\n\t\tname      string\n\t\tfields    fields\n\t\targs      args\n\t\twant      *x509.Certificate\n\t\tassertion assert.ErrorAssertionFunc\n\t}{\n\t\t{\"ok\", fields{&url.URL{Scheme: \"urn\", Opaque: \"uuid:0c4670b2-d9f1-42bb-9045-184836f16733\"}, now, now.Add(time.Hour)},\n\t\t\targs{&x509.Certificate{}}, &x509.Certificate{\n\t\t\t\tNotBefore: now,\n\t\t\t\tNotAfter:  now.Add(time.Hour),\n\t\t\t\tURIs:      []*url.URL{{Scheme: \"urn\", Opaque: \"uuid:0c4670b2-d9f1-42bb-9045-184836f16733\"}},\n\t\t\t}, assert.NoError},\n\t\t{\"ok exists\", fields{&url.URL{Scheme: \"urn\", Opaque: \"uuid:0c4670b2-d9f1-42bb-9045-184836f16733\"}, now, now.Add(time.Hour)},\n\t\t\targs{&x509.Certificate{\n\t\t\t\tURIs: []*url.URL{{Scheme: \"urn\", Opaque: \"uuid:0c4670b2-d9f1-42bb-9045-184836f16733\"}},\n\t\t\t}}, &x509.Certificate{\n\t\t\t\tNotBefore: now,\n\t\t\t\tNotAfter:  now.Add(time.Hour),\n\t\t\t\tURIs:      []*url.URL{{Scheme: \"urn\", Opaque: \"uuid:0c4670b2-d9f1-42bb-9045-184836f16733\"}},\n\t\t\t}, assert.NoError},\n\t\t{\"ok append\", fields{&url.URL{Scheme: \"urn\", Opaque: \"uuid:0c4670b2-d9f1-42bb-9045-184836f16733\"}, now, now.Add(time.Hour)},\n\t\t\targs{&x509.Certificate{\n\t\t\t\tURIs: []*url.URL{{Scheme: \"urn\", Opaque: \"uuid:27bb66db-e12a-4ff6-9161-aa6b0a98f914\"}},\n\t\t\t}}, &x509.Certificate{\n\t\t\t\tNotBefore: now,\n\t\t\t\tNotAfter:  now.Add(time.Hour),\n\t\t\t\tURIs: []*url.URL{\n\t\t\t\t\t{Scheme: \"urn\", Opaque: \"uuid:27bb66db-e12a-4ff6-9161-aa6b0a98f914\"},\n\t\t\t\t\t{Scheme: \"urn\", Opaque: \"uuid:0c4670b2-d9f1-42bb-9045-184836f16733\"},\n\t\t\t\t},\n\t\t\t}, assert.NoError},\n\t\t{\"ok no identity\", fields{nil, now, now.Add(time.Hour)},\n\t\t\targs{&x509.Certificate{}}, &x509.Certificate{\n\t\t\t\tNotBefore: now,\n\t\t\t\tNotAfter:  now.Add(time.Hour),\n\t\t\t}, assert.NoError},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tm := &identityModifier{\n\t\t\t\tIdentity:  tt.fields.Identity,\n\t\t\t\tNotBefore: tt.fields.NotBefore,\n\t\t\t\tNotAfter:  tt.fields.NotAfter,\n\t\t\t}\n\t\t\ttt.assertion(t, m.Enforce(tt.args.cert))\n\t\t})\n\t}\n}\n\nfunc Test_getIdentityURI(t *testing.T) {\n\tid, err := uuid.Parse(\"54a2ec9d-a7d9-4b53-8f9a-efcd275e35e1\")\n\trequire.NoError(t, err)\n\tu, err := url.Parse(id.URN())\n\trequire.NoError(t, err)\n\n\ttype args struct {\n\t\tcr *x509.CertificateRequest\n\t}\n\ttests := []struct {\n\t\tname string\n\t\targs args\n\t\twant *url.URL\n\t}{\n\t\t{\"ok\", args{&x509.CertificateRequest{\n\t\t\tURIs: []*url.URL{u},\n\t\t}}, &url.URL{Scheme: \"urn\", Opaque: \"uuid:54a2ec9d-a7d9-4b53-8f9a-efcd275e35e1\"}},\n\t\t{\"ok multiple\", args{&x509.CertificateRequest{\n\t\t\tURIs: []*url.URL{u, {Scheme: \"urn\", Opaque: \"uuid:f0e74f3a-95fe-4cf6-98e3-68e55b69ba48\"}},\n\t\t}}, &url.URL{Scheme: \"urn\", Opaque: \"uuid:54a2ec9d-a7d9-4b53-8f9a-efcd275e35e1\"}},\n\t\t{\"ok multiple with invalid\", args{&x509.CertificateRequest{\n\t\t\tURIs: []*url.URL{{Scheme: \"urn\", Opaque: \"uuid:f0e74f3a+95fe+4cf6+98e3+68e55b69ba48\"}, u},\n\t\t}}, &url.URL{Scheme: \"urn\", Opaque: \"uuid:54a2ec9d-a7d9-4b53-8f9a-efcd275e35e1\"}},\n\t\t{\"ok missing\", args{&x509.CertificateRequest{\n\t\t\tURIs: []*url.URL{{Scheme: \"https\", Host: \"example.com\", Path: \"/54a2ec9d-a7d9-4b53-8f9a-efcd275e35e1\"}},\n\t\t}}, nil},\n\t\t{\"ok empty\", args{&x509.CertificateRequest{}}, nil},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tassert.Equal(t, tt.want, getIdentityURI(tt.args.cr))\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "authority/admin/api/acme.go",
    "content": "package api\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\n\t\"github.com/smallstep/linkedca\"\n\t\"google.golang.org/protobuf/types/known/timestamppb\"\n\n\t\"github.com/smallstep/certificates/acme\"\n\t\"github.com/smallstep/certificates/api/render\"\n\t\"github.com/smallstep/certificates/authority/admin\"\n)\n\n// CreateExternalAccountKeyRequest is the type for POST /admin/acme/eab requests\ntype CreateExternalAccountKeyRequest struct {\n\tReference string `json:\"reference\"`\n}\n\n// Validate validates a new ACME EAB Key request body.\nfunc (r *CreateExternalAccountKeyRequest) Validate() error {\n\tif len(r.Reference) > 256 { // an arbitrary, but sensible (IMO), limit\n\t\treturn fmt.Errorf(\"reference length %d exceeds the maximum (256)\", len(r.Reference))\n\t}\n\treturn nil\n}\n\n// GetExternalAccountKeysResponse is the type for GET /admin/acme/eab responses\ntype GetExternalAccountKeysResponse struct {\n\tEAKs       []*linkedca.EABKey `json:\"eaks\"`\n\tNextCursor string             `json:\"nextCursor\"`\n}\n\n// requireEABEnabled is a middleware that ensures ACME EAB is enabled\n// before serving requests that act on ACME EAB credentials.\nfunc requireEABEnabled(next http.HandlerFunc) http.HandlerFunc {\n\treturn func(w http.ResponseWriter, r *http.Request) {\n\t\tctx := r.Context()\n\t\tprov := linkedca.MustProvisionerFromContext(ctx)\n\n\t\tacmeProvisioner := prov.GetDetails().GetACME()\n\t\tif acmeProvisioner == nil {\n\t\t\trender.Error(w, r, admin.NewErrorISE(\"error getting ACME details for provisioner '%s'\", prov.GetName()))\n\t\t\treturn\n\t\t}\n\n\t\tif !acmeProvisioner.RequireEab {\n\t\t\trender.Error(w, r, admin.NewError(admin.ErrorBadRequestType, \"ACME EAB not enabled for provisioner '%s'\", prov.GetName()))\n\t\t\treturn\n\t\t}\n\n\t\tnext(w, r)\n\t}\n}\n\n// ACMEAdminResponder is responsible for writing ACME admin responses\ntype ACMEAdminResponder interface {\n\tGetExternalAccountKeys(w http.ResponseWriter, r *http.Request)\n\tCreateExternalAccountKey(w http.ResponseWriter, r *http.Request)\n\tDeleteExternalAccountKey(w http.ResponseWriter, r *http.Request)\n}\n\n// acmeAdminResponder implements ACMEAdminResponder.\ntype acmeAdminResponder struct{}\n\n// NewACMEAdminResponder returns a new ACMEAdminResponder\nfunc NewACMEAdminResponder() ACMEAdminResponder {\n\treturn &acmeAdminResponder{}\n}\n\n// GetExternalAccountKeys writes the response for the EAB keys GET endpoint\nfunc (h *acmeAdminResponder) GetExternalAccountKeys(w http.ResponseWriter, r *http.Request) {\n\trender.Error(w, r, admin.NewError(admin.ErrorNotImplementedType, \"this functionality is currently only available in Certificate Manager: https://u.step.sm/cm\"))\n}\n\n// CreateExternalAccountKey writes the response for the EAB key POST endpoint\nfunc (h *acmeAdminResponder) CreateExternalAccountKey(w http.ResponseWriter, r *http.Request) {\n\trender.Error(w, r, admin.NewError(admin.ErrorNotImplementedType, \"this functionality is currently only available in Certificate Manager: https://u.step.sm/cm\"))\n}\n\n// DeleteExternalAccountKey writes the response for the EAB key DELETE endpoint\nfunc (h *acmeAdminResponder) DeleteExternalAccountKey(w http.ResponseWriter, r *http.Request) {\n\trender.Error(w, r, admin.NewError(admin.ErrorNotImplementedType, \"this functionality is currently only available in Certificate Manager: https://u.step.sm/cm\"))\n}\n\nfunc eakToLinked(k *acme.ExternalAccountKey) *linkedca.EABKey {\n\tif k == nil {\n\t\treturn nil\n\t}\n\n\teak := &linkedca.EABKey{\n\t\tId:          k.ID,\n\t\tHmacKey:     k.HmacKey,\n\t\tProvisioner: k.ProvisionerID,\n\t\tReference:   k.Reference,\n\t\tAccount:     k.AccountID,\n\t\tCreatedAt:   timestamppb.New(k.CreatedAt),\n\t\tBoundAt:     timestamppb.New(k.BoundAt),\n\t}\n\n\tif k.Policy != nil {\n\t\teak.Policy = &linkedca.Policy{\n\t\t\tX509: &linkedca.X509Policy{\n\t\t\t\tAllow: &linkedca.X509Names{},\n\t\t\t\tDeny:  &linkedca.X509Names{},\n\t\t\t},\n\t\t}\n\t\teak.Policy.X509.Allow.Dns = k.Policy.X509.Allowed.DNSNames\n\t\teak.Policy.X509.Allow.Ips = k.Policy.X509.Allowed.IPRanges\n\t\teak.Policy.X509.Deny.Dns = k.Policy.X509.Denied.DNSNames\n\t\teak.Policy.X509.Deny.Ips = k.Policy.X509.Denied.IPRanges\n\t\teak.Policy.X509.AllowWildcardNames = k.Policy.X509.AllowWildcardNames\n\t}\n\n\treturn eak\n}\n\nfunc linkedEAKToCertificates(k *linkedca.EABKey) *acme.ExternalAccountKey {\n\tif k == nil {\n\t\treturn nil\n\t}\n\n\teak := &acme.ExternalAccountKey{\n\t\tID:            k.Id,\n\t\tProvisionerID: k.Provisioner,\n\t\tReference:     k.Reference,\n\t\tAccountID:     k.Account,\n\t\tHmacKey:       k.HmacKey,\n\t\tCreatedAt:     k.CreatedAt.AsTime(),\n\t\tBoundAt:       k.BoundAt.AsTime(),\n\t}\n\n\tif policy := k.GetPolicy(); policy != nil {\n\t\teak.Policy = &acme.Policy{}\n\t\tif x509 := policy.GetX509(); x509 != nil {\n\t\t\teak.Policy.X509 = acme.X509Policy{}\n\t\t\tif allow := x509.GetAllow(); allow != nil {\n\t\t\t\teak.Policy.X509.Allowed = acme.PolicyNames{}\n\t\t\t\teak.Policy.X509.Allowed.DNSNames = allow.Dns\n\t\t\t\teak.Policy.X509.Allowed.IPRanges = allow.Ips\n\t\t\t}\n\t\t\tif deny := x509.GetDeny(); deny != nil {\n\t\t\t\teak.Policy.X509.Denied = acme.PolicyNames{}\n\t\t\t\teak.Policy.X509.Denied.DNSNames = deny.Dns\n\t\t\t\teak.Policy.X509.Denied.IPRanges = deny.Ips\n\t\t\t}\n\t\t\teak.Policy.X509.AllowWildcardNames = x509.AllowWildcardNames\n\t\t}\n\t}\n\n\treturn eak\n}\n"
  },
  {
    "path": "authority/admin/api/acme_test.go",
    "content": "package api\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"reflect\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-chi/chi/v5\"\n\t\"google.golang.org/protobuf/encoding/protojson\"\n\t\"google.golang.org/protobuf/proto\"\n\t\"google.golang.org/protobuf/types/known/timestamppb\"\n\n\t\"github.com/smallstep/linkedca\"\n\n\t\"github.com/smallstep/assert\"\n\t\"github.com/smallstep/certificates/acme\"\n\t\"github.com/smallstep/certificates/authority/admin\"\n)\n\nfunc readProtoJSON(r io.ReadCloser, m proto.Message) error {\n\tdefer r.Close()\n\tdata, err := io.ReadAll(r)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn protojson.Unmarshal(data, m)\n}\n\nfunc mockMustAuthority(t *testing.T, a adminAuthority) {\n\tt.Helper()\n\tfn := mustAuthority\n\tt.Cleanup(func() {\n\t\tmustAuthority = fn\n\t})\n\tmustAuthority = func(ctx context.Context) adminAuthority {\n\t\treturn a\n\t}\n}\n\nfunc TestHandler_requireEABEnabled(t *testing.T) {\n\ttype test struct {\n\t\tctx        context.Context\n\t\tnext       http.HandlerFunc\n\t\terr        *admin.Error\n\t\tstatusCode int\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/prov.GetDetails\": func(t *testing.T) test {\n\t\t\tprov := &linkedca.Provisioner{\n\t\t\t\tId:   \"provID\",\n\t\t\t\tName: \"provName\",\n\t\t\t}\n\t\t\tctx := linkedca.NewContextWithProvisioner(context.Background(), prov)\n\t\t\terr := admin.NewErrorISE(\"error getting ACME details for provisioner 'provName'\")\n\t\t\terr.Message = \"error getting ACME details for provisioner 'provName'\"\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\terr:        err,\n\t\t\t\tstatusCode: 500,\n\t\t\t}\n\t\t},\n\t\t\"fail/prov.GetDetails.GetACME\": func(t *testing.T) test {\n\t\t\tprov := &linkedca.Provisioner{\n\t\t\t\tId:      \"provID\",\n\t\t\t\tName:    \"provName\",\n\t\t\t\tDetails: &linkedca.ProvisionerDetails{},\n\t\t\t}\n\t\t\tctx := linkedca.NewContextWithProvisioner(context.Background(), prov)\n\t\t\terr := admin.NewErrorISE(\"error getting ACME details for provisioner 'provName'\")\n\t\t\terr.Message = \"error getting ACME details for provisioner 'provName'\"\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\terr:        err,\n\t\t\t\tstatusCode: 500,\n\t\t\t}\n\t\t},\n\t\t\"ok/eab-disabled\": func(t *testing.T) test {\n\t\t\tprov := &linkedca.Provisioner{\n\t\t\t\tId:   \"provID\",\n\t\t\t\tName: \"provName\",\n\t\t\t\tDetails: &linkedca.ProvisionerDetails{\n\t\t\t\t\tData: &linkedca.ProvisionerDetails_ACME{\n\t\t\t\t\t\tACME: &linkedca.ACMEProvisioner{\n\t\t\t\t\t\t\tRequireEab: false,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t\tctx := linkedca.NewContextWithProvisioner(context.Background(), prov)\n\t\t\terr := admin.NewError(admin.ErrorBadRequestType, \"ACME EAB not enabled for provisioner provName\")\n\t\t\terr.Message = \"ACME EAB not enabled for provisioner 'provName'\"\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\terr:        err,\n\t\t\t\tstatusCode: 400,\n\t\t\t}\n\t\t},\n\t\t\"ok/eab-enabled\": func(t *testing.T) test {\n\t\t\tprov := &linkedca.Provisioner{\n\t\t\t\tId:   \"provID\",\n\t\t\t\tName: \"provName\",\n\t\t\t\tDetails: &linkedca.ProvisionerDetails{\n\t\t\t\t\tData: &linkedca.ProvisionerDetails_ACME{\n\t\t\t\t\t\tACME: &linkedca.ACMEProvisioner{\n\t\t\t\t\t\t\tRequireEab: true,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t\tctx := linkedca.NewContextWithProvisioner(context.Background(), prov)\n\t\t\treturn test{\n\t\t\t\tctx: ctx,\n\t\t\t\tnext: func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\t\tw.Write(nil) // mock response with status 200\n\t\t\t\t},\n\t\t\t\tstatusCode: 200,\n\t\t\t}\n\t\t},\n\t}\n\n\tfor name, prep := range tests {\n\t\ttc := prep(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\treq := httptest.NewRequest(\"GET\", \"/foo\", http.NoBody).WithContext(tc.ctx)\n\t\t\tw := httptest.NewRecorder()\n\t\t\trequireEABEnabled(tc.next)(w, req)\n\t\t\tres := w.Result()\n\n\t\t\tassert.Equals(t, tc.statusCode, res.StatusCode)\n\n\t\t\tbody, err := io.ReadAll(res.Body)\n\t\t\tres.Body.Close()\n\t\t\tassert.FatalError(t, err)\n\n\t\t\tif res.StatusCode >= 400 {\n\t\t\t\terr := admin.Error{}\n\t\t\t\tassert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &err))\n\n\t\t\t\tassert.Equals(t, tc.err.Type, err.Type)\n\t\t\t\tassert.Equals(t, tc.err.Message, err.Message)\n\t\t\t\tassert.Equals(t, tc.err.StatusCode(), res.StatusCode)\n\t\t\t\tassert.Equals(t, tc.err.Detail, err.Detail)\n\t\t\t\tassert.Equals(t, []string{\"application/json\"}, res.Header[\"Content-Type\"])\n\t\t\t\treturn\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestCreateExternalAccountKeyRequest_Validate(t *testing.T) {\n\ttype fields struct {\n\t\tReference string\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tfields  fields\n\t\twantErr bool\n\t}{\n\t\t{\n\t\t\tname: \"fail/reference-too-long\",\n\t\t\tfields: fields{\n\t\t\t\tReference: strings.Repeat(\"A\", 257),\n\t\t\t},\n\t\t\twantErr: true,\n\t\t},\n\t\t{\n\t\t\tname: \"ok/empty-reference\",\n\t\t\tfields: fields{\n\t\t\t\tReference: \"\",\n\t\t\t},\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"ok\",\n\t\t\tfields: fields{\n\t\t\t\tReference: \"my-eab-reference\",\n\t\t\t},\n\t\t\twantErr: false,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tr := &CreateExternalAccountKeyRequest{\n\t\t\t\tReference: tt.fields.Reference,\n\t\t\t}\n\t\t\tif err := r.Validate(); (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"CreateExternalAccountKeyRequest.Validate() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestHandler_CreateExternalAccountKey(t *testing.T) {\n\ttype test struct {\n\t\tctx        context.Context\n\t\tstatusCode int\n\t\terr        *admin.Error\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tchiCtx := chi.NewRouteContext()\n\t\t\tctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 501,\n\t\t\t\terr: &admin.Error{\n\t\t\t\t\tType:    admin.ErrorNotImplementedType.String(),\n\t\t\t\t\tStatus:  http.StatusNotImplemented,\n\t\t\t\t\tMessage: \"this functionality is currently only available in Certificate Manager: https://u.step.sm/cm\",\n\t\t\t\t\tDetail:  \"not implemented\",\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t}\n\tfor name, prep := range tests {\n\t\ttc := prep(t)\n\t\tt.Run(name, func(t *testing.T) {\n\n\t\t\treq := httptest.NewRequest(\"POST\", \"/foo\", http.NoBody) // chi routing is prepared in test setup\n\t\t\treq = req.WithContext(tc.ctx)\n\t\t\tw := httptest.NewRecorder()\n\t\t\tacmeResponder := NewACMEAdminResponder()\n\t\t\tacmeResponder.CreateExternalAccountKey(w, req)\n\t\t\tres := w.Result()\n\t\t\tassert.Equals(t, tc.statusCode, res.StatusCode)\n\n\t\t\tbody, err := io.ReadAll(res.Body)\n\t\t\tres.Body.Close()\n\t\t\tassert.FatalError(t, err)\n\n\t\t\tadminErr := admin.Error{}\n\t\t\tassert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &adminErr))\n\n\t\t\tassert.Equals(t, tc.err.Type, adminErr.Type)\n\t\t\tassert.Equals(t, tc.err.Message, adminErr.Message)\n\t\t\tassert.Equals(t, tc.err.StatusCode(), res.StatusCode)\n\t\t\tassert.Equals(t, tc.err.Detail, adminErr.Detail)\n\t\t\tassert.Equals(t, []string{\"application/json\"}, res.Header[\"Content-Type\"])\n\n\t\t})\n\t}\n}\n\nfunc TestHandler_DeleteExternalAccountKey(t *testing.T) {\n\ttype test struct {\n\t\tctx        context.Context\n\t\tstatusCode int\n\t\terr        *admin.Error\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tchiCtx := chi.NewRouteContext()\n\t\t\tchiCtx.URLParams.Add(\"provisionerName\", \"provName\")\n\t\t\tchiCtx.URLParams.Add(\"id\", \"keyID\")\n\t\t\tctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 501,\n\t\t\t\terr: &admin.Error{\n\t\t\t\t\tType:    admin.ErrorNotImplementedType.String(),\n\t\t\t\t\tStatus:  http.StatusNotImplemented,\n\t\t\t\t\tMessage: \"this functionality is currently only available in Certificate Manager: https://u.step.sm/cm\",\n\t\t\t\t\tDetail:  \"not implemented\",\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t}\n\tfor name, prep := range tests {\n\t\ttc := prep(t)\n\t\tt.Run(name, func(t *testing.T) {\n\n\t\t\treq := httptest.NewRequest(\"DELETE\", \"/foo\", http.NoBody) // chi routing is prepared in test setup\n\t\t\treq = req.WithContext(tc.ctx)\n\t\t\tw := httptest.NewRecorder()\n\t\t\tacmeResponder := NewACMEAdminResponder()\n\t\t\tacmeResponder.DeleteExternalAccountKey(w, req)\n\t\t\tres := w.Result()\n\t\t\tassert.Equals(t, tc.statusCode, res.StatusCode)\n\n\t\t\tbody, err := io.ReadAll(res.Body)\n\t\t\tres.Body.Close()\n\t\t\tassert.FatalError(t, err)\n\n\t\t\tadminErr := admin.Error{}\n\t\t\tassert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &adminErr))\n\n\t\t\tassert.Equals(t, tc.err.Type, adminErr.Type)\n\t\t\tassert.Equals(t, tc.err.Message, adminErr.Message)\n\t\t\tassert.Equals(t, tc.err.StatusCode(), res.StatusCode)\n\t\t\tassert.Equals(t, tc.err.Detail, adminErr.Detail)\n\t\t\tassert.Equals(t, []string{\"application/json\"}, res.Header[\"Content-Type\"])\n\t\t})\n\t}\n}\n\nfunc TestHandler_GetExternalAccountKeys(t *testing.T) {\n\ttype test struct {\n\t\tctx        context.Context\n\t\tstatusCode int\n\t\treq        *http.Request\n\t\terr        *admin.Error\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tchiCtx := chi.NewRouteContext()\n\t\t\tchiCtx.URLParams.Add(\"provisionerName\", \"provName\")\n\t\t\treq := httptest.NewRequest(\"GET\", \"/foo\", http.NoBody)\n\t\t\tctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\tstatusCode: 501,\n\t\t\t\treq:        req,\n\t\t\t\terr: &admin.Error{\n\t\t\t\t\tType:    admin.ErrorNotImplementedType.String(),\n\t\t\t\t\tStatus:  http.StatusNotImplemented,\n\t\t\t\t\tMessage: \"this functionality is currently only available in Certificate Manager: https://u.step.sm/cm\",\n\t\t\t\t\tDetail:  \"not implemented\",\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t}\n\tfor name, prep := range tests {\n\t\ttc := prep(t)\n\t\tt.Run(name, func(t *testing.T) {\n\n\t\t\treq := tc.req.WithContext(tc.ctx)\n\t\t\tw := httptest.NewRecorder()\n\t\t\tacmeResponder := NewACMEAdminResponder()\n\t\t\tacmeResponder.GetExternalAccountKeys(w, req)\n\n\t\t\tres := w.Result()\n\t\t\tassert.Equals(t, tc.statusCode, res.StatusCode)\n\n\t\t\tbody, err := io.ReadAll(res.Body)\n\t\t\tres.Body.Close()\n\t\t\tassert.FatalError(t, err)\n\n\t\t\tadminErr := admin.Error{}\n\t\t\tassert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &adminErr))\n\n\t\t\tassert.Equals(t, tc.err.Type, adminErr.Type)\n\t\t\tassert.Equals(t, tc.err.Message, adminErr.Message)\n\t\t\tassert.Equals(t, tc.err.StatusCode(), res.StatusCode)\n\t\t\tassert.Equals(t, tc.err.Detail, adminErr.Detail)\n\t\t\tassert.Equals(t, []string{\"application/json\"}, res.Header[\"Content-Type\"])\n\t\t})\n\t}\n}\n\nfunc Test_eakToLinked(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tk    *acme.ExternalAccountKey\n\t\twant *linkedca.EABKey\n\t}{\n\t\t{\n\t\t\tname: \"no-key\",\n\t\t\tk:    nil,\n\t\t\twant: nil,\n\t\t},\n\t\t{\n\t\t\tname: \"no-policy\",\n\t\t\tk: &acme.ExternalAccountKey{\n\t\t\t\tID:            \"keyID\",\n\t\t\t\tProvisionerID: \"provID\",\n\t\t\t\tReference:     \"ref\",\n\t\t\t\tAccountID:     \"accID\",\n\t\t\t\tHmacKey:       []byte{1, 3, 3, 7},\n\t\t\t\tCreatedAt:     time.Date(2022, 04, 12, 9, 30, 30, 0, time.UTC).Add(-1 * time.Hour),\n\t\t\t\tBoundAt:       time.Date(2022, 04, 12, 9, 30, 30, 0, time.UTC),\n\t\t\t\tPolicy:        nil,\n\t\t\t},\n\t\t\twant: &linkedca.EABKey{\n\t\t\t\tId:          \"keyID\",\n\t\t\t\tProvisioner: \"provID\",\n\t\t\t\tHmacKey:     []byte{1, 3, 3, 7},\n\t\t\t\tReference:   \"ref\",\n\t\t\t\tAccount:     \"accID\",\n\t\t\t\tCreatedAt:   timestamppb.New(time.Date(2022, 04, 12, 9, 30, 30, 0, time.UTC).Add(-1 * time.Hour)),\n\t\t\t\tBoundAt:     timestamppb.New(time.Date(2022, 04, 12, 9, 30, 30, 0, time.UTC)),\n\t\t\t\tPolicy:      nil,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"with-policy\",\n\t\t\tk: &acme.ExternalAccountKey{\n\t\t\t\tID:            \"keyID\",\n\t\t\t\tProvisionerID: \"provID\",\n\t\t\t\tReference:     \"ref\",\n\t\t\t\tAccountID:     \"accID\",\n\t\t\t\tHmacKey:       []byte{1, 3, 3, 7},\n\t\t\t\tCreatedAt:     time.Date(2022, 04, 12, 9, 30, 30, 0, time.UTC).Add(-1 * time.Hour),\n\t\t\t\tBoundAt:       time.Date(2022, 04, 12, 9, 30, 30, 0, time.UTC),\n\t\t\t\tPolicy: &acme.Policy{\n\t\t\t\t\tX509: acme.X509Policy{\n\t\t\t\t\t\tAllowed: acme.PolicyNames{\n\t\t\t\t\t\t\tDNSNames: []string{\"*.local\"},\n\t\t\t\t\t\t\tIPRanges: []string{\"10.0.0.0/24\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tDenied: acme.PolicyNames{\n\t\t\t\t\t\t\tDNSNames: []string{\"badhost.local\"},\n\t\t\t\t\t\t\tIPRanges: []string{\"10.0.0.30\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: &linkedca.EABKey{\n\t\t\t\tId:          \"keyID\",\n\t\t\t\tProvisioner: \"provID\",\n\t\t\t\tHmacKey:     []byte{1, 3, 3, 7},\n\t\t\t\tReference:   \"ref\",\n\t\t\t\tAccount:     \"accID\",\n\t\t\t\tCreatedAt:   timestamppb.New(time.Date(2022, 04, 12, 9, 30, 30, 0, time.UTC).Add(-1 * time.Hour)),\n\t\t\t\tBoundAt:     timestamppb.New(time.Date(2022, 04, 12, 9, 30, 30, 0, time.UTC)),\n\t\t\t\tPolicy: &linkedca.Policy{\n\t\t\t\t\tX509: &linkedca.X509Policy{\n\t\t\t\t\t\tAllow: &linkedca.X509Names{\n\t\t\t\t\t\t\tDns: []string{\"*.local\"},\n\t\t\t\t\t\t\tIps: []string{\"10.0.0.0/24\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tDeny: &linkedca.X509Names{\n\t\t\t\t\t\t\tDns: []string{\"badhost.local\"},\n\t\t\t\t\t\t\tIps: []string{\"10.0.0.30\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif got := eakToLinked(tt.k); !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"eakToLinked() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_linkedEAKToCertificates(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tk    *linkedca.EABKey\n\t\twant *acme.ExternalAccountKey\n\t}{\n\t\t{\n\t\t\tname: \"no-key\",\n\t\t\tk:    nil,\n\t\t\twant: nil,\n\t\t},\n\t\t{\n\t\t\tname: \"no-policy\",\n\t\t\tk: &linkedca.EABKey{\n\t\t\t\tId:          \"keyID\",\n\t\t\t\tProvisioner: \"provID\",\n\t\t\t\tHmacKey:     []byte{1, 3, 3, 7},\n\t\t\t\tReference:   \"ref\",\n\t\t\t\tAccount:     \"accID\",\n\t\t\t\tCreatedAt:   timestamppb.New(time.Date(2022, 04, 12, 9, 30, 30, 0, time.UTC).Add(-1 * time.Hour)),\n\t\t\t\tBoundAt:     timestamppb.New(time.Date(2022, 04, 12, 9, 30, 30, 0, time.UTC)),\n\t\t\t\tPolicy:      nil,\n\t\t\t},\n\t\t\twant: &acme.ExternalAccountKey{\n\t\t\t\tID:            \"keyID\",\n\t\t\t\tProvisionerID: \"provID\",\n\t\t\t\tReference:     \"ref\",\n\t\t\t\tAccountID:     \"accID\",\n\t\t\t\tHmacKey:       []byte{1, 3, 3, 7},\n\t\t\t\tCreatedAt:     time.Date(2022, 04, 12, 9, 30, 30, 0, time.UTC).Add(-1 * time.Hour),\n\t\t\t\tBoundAt:       time.Date(2022, 04, 12, 9, 30, 30, 0, time.UTC),\n\t\t\t\tPolicy:        nil,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"no-x509-policy\",\n\t\t\tk: &linkedca.EABKey{\n\t\t\t\tId:          \"keyID\",\n\t\t\t\tProvisioner: \"provID\",\n\t\t\t\tHmacKey:     []byte{1, 3, 3, 7},\n\t\t\t\tReference:   \"ref\",\n\t\t\t\tAccount:     \"accID\",\n\t\t\t\tCreatedAt:   timestamppb.New(time.Date(2022, 04, 12, 9, 30, 30, 0, time.UTC).Add(-1 * time.Hour)),\n\t\t\t\tBoundAt:     timestamppb.New(time.Date(2022, 04, 12, 9, 30, 30, 0, time.UTC)),\n\t\t\t\tPolicy:      &linkedca.Policy{},\n\t\t\t},\n\t\t\twant: &acme.ExternalAccountKey{\n\t\t\t\tID:            \"keyID\",\n\t\t\t\tProvisionerID: \"provID\",\n\t\t\t\tReference:     \"ref\",\n\t\t\t\tAccountID:     \"accID\",\n\t\t\t\tHmacKey:       []byte{1, 3, 3, 7},\n\t\t\t\tCreatedAt:     time.Date(2022, 04, 12, 9, 30, 30, 0, time.UTC).Add(-1 * time.Hour),\n\t\t\t\tBoundAt:       time.Date(2022, 04, 12, 9, 30, 30, 0, time.UTC),\n\t\t\t\tPolicy:        &acme.Policy{},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"with-x509-policy\",\n\t\t\tk: &linkedca.EABKey{\n\t\t\t\tId:          \"keyID\",\n\t\t\t\tProvisioner: \"provID\",\n\t\t\t\tHmacKey:     []byte{1, 3, 3, 7},\n\t\t\t\tReference:   \"ref\",\n\t\t\t\tAccount:     \"accID\",\n\t\t\t\tCreatedAt:   timestamppb.New(time.Date(2022, 04, 12, 9, 30, 30, 0, time.UTC).Add(-1 * time.Hour)),\n\t\t\t\tBoundAt:     timestamppb.New(time.Date(2022, 04, 12, 9, 30, 30, 0, time.UTC)),\n\t\t\t\tPolicy: &linkedca.Policy{\n\t\t\t\t\tX509: &linkedca.X509Policy{\n\t\t\t\t\t\tAllow: &linkedca.X509Names{\n\t\t\t\t\t\t\tDns: []string{\"*.local\"},\n\t\t\t\t\t\t\tIps: []string{\"10.0.0.0/24\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tDeny: &linkedca.X509Names{\n\t\t\t\t\t\t\tDns: []string{\"badhost.local\"},\n\t\t\t\t\t\t\tIps: []string{\"10.0.0.30\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tAllowWildcardNames: true,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: &acme.ExternalAccountKey{\n\t\t\t\tID:            \"keyID\",\n\t\t\t\tProvisionerID: \"provID\",\n\t\t\t\tReference:     \"ref\",\n\t\t\t\tAccountID:     \"accID\",\n\t\t\t\tHmacKey:       []byte{1, 3, 3, 7},\n\t\t\t\tCreatedAt:     time.Date(2022, 04, 12, 9, 30, 30, 0, time.UTC).Add(-1 * time.Hour),\n\t\t\t\tBoundAt:       time.Date(2022, 04, 12, 9, 30, 30, 0, time.UTC),\n\t\t\t\tPolicy: &acme.Policy{\n\t\t\t\t\tX509: acme.X509Policy{\n\t\t\t\t\t\tAllowed: acme.PolicyNames{\n\t\t\t\t\t\t\tDNSNames: []string{\"*.local\"},\n\t\t\t\t\t\t\tIPRanges: []string{\"10.0.0.0/24\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tDenied: acme.PolicyNames{\n\t\t\t\t\t\t\tDNSNames: []string{\"badhost.local\"},\n\t\t\t\t\t\t\tIPRanges: []string{\"10.0.0.30\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tAllowWildcardNames: true,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif got := linkedEAKToCertificates(tt.k); !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"linkedEAKToCertificates() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "authority/admin/api/admin.go",
    "content": "package api\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\n\t\"github.com/go-chi/chi/v5\"\n\n\t\"github.com/smallstep/linkedca\"\n\n\t\"github.com/smallstep/certificates/api\"\n\t\"github.com/smallstep/certificates/api/read\"\n\t\"github.com/smallstep/certificates/api/render\"\n\t\"github.com/smallstep/certificates/authority/admin\"\n\t\"github.com/smallstep/certificates/authority/provisioner\"\n)\n\ntype adminAuthority interface {\n\tLoadProvisionerByName(string) (provisioner.Interface, error)\n\tGetProvisioners(cursor string, limit int) (provisioner.List, string, error)\n\tIsAdminAPIEnabled() bool\n\tLoadAdminByID(id string) (*linkedca.Admin, bool)\n\tGetAdmins(cursor string, limit int) ([]*linkedca.Admin, string, error)\n\tStoreAdmin(ctx context.Context, adm *linkedca.Admin, prov provisioner.Interface) error\n\tUpdateAdmin(ctx context.Context, id string, nu *linkedca.Admin) (*linkedca.Admin, error)\n\tRemoveAdmin(ctx context.Context, id string) error\n\tAuthorizeAdminToken(r *http.Request, token string) (*linkedca.Admin, error)\n\tStoreProvisioner(ctx context.Context, prov *linkedca.Provisioner) error\n\tLoadProvisionerByID(id string) (provisioner.Interface, error)\n\tUpdateProvisioner(ctx context.Context, nu *linkedca.Provisioner) error\n\tRemoveProvisioner(ctx context.Context, id string) error\n\tGetAuthorityPolicy(ctx context.Context) (*linkedca.Policy, error)\n\tCreateAuthorityPolicy(ctx context.Context, admin *linkedca.Admin, policy *linkedca.Policy) (*linkedca.Policy, error)\n\tUpdateAuthorityPolicy(ctx context.Context, admin *linkedca.Admin, policy *linkedca.Policy) (*linkedca.Policy, error)\n\tRemoveAuthorityPolicy(ctx context.Context) error\n}\n\n// CreateAdminRequest represents the body for a CreateAdmin request.\ntype CreateAdminRequest struct {\n\tSubject     string              `json:\"subject\"`\n\tProvisioner string              `json:\"provisioner\"`\n\tType        linkedca.Admin_Type `json:\"type\"`\n}\n\n// Validate validates a new-admin request body.\nfunc (car *CreateAdminRequest) Validate() error {\n\tif car.Subject == \"\" {\n\t\treturn admin.NewError(admin.ErrorBadRequestType, \"subject cannot be empty\")\n\t}\n\tif car.Provisioner == \"\" {\n\t\treturn admin.NewError(admin.ErrorBadRequestType, \"provisioner cannot be empty\")\n\t}\n\tswitch car.Type {\n\tcase linkedca.Admin_SUPER_ADMIN, linkedca.Admin_ADMIN:\n\tdefault:\n\t\treturn admin.NewError(admin.ErrorBadRequestType, \"invalid value for admin type\")\n\t}\n\treturn nil\n}\n\n// GetAdminsResponse for returning a list of admins.\ntype GetAdminsResponse struct {\n\tAdmins     []*linkedca.Admin `json:\"admins\"`\n\tNextCursor string            `json:\"nextCursor\"`\n}\n\n// UpdateAdminRequest represents the body for a UpdateAdmin request.\ntype UpdateAdminRequest struct {\n\tType linkedca.Admin_Type `json:\"type\"`\n}\n\n// Validate validates a new-admin request body.\nfunc (uar *UpdateAdminRequest) Validate() error {\n\tswitch uar.Type {\n\tcase linkedca.Admin_SUPER_ADMIN, linkedca.Admin_ADMIN:\n\tdefault:\n\t\treturn admin.NewError(admin.ErrorBadRequestType, \"invalid value for admin type\")\n\t}\n\treturn nil\n}\n\n// DeleteResponse is the resource for successful DELETE responses.\ntype DeleteResponse struct {\n\tStatus string `json:\"status\"`\n}\n\n// GetAdmin returns the requested admin, or an error.\nfunc GetAdmin(w http.ResponseWriter, r *http.Request) {\n\tid := chi.URLParam(r, \"id\")\n\n\tadm, ok := mustAuthority(r.Context()).LoadAdminByID(id)\n\tif !ok {\n\t\trender.Error(w, r, admin.NewError(admin.ErrorNotFoundType,\n\t\t\t\"admin %s not found\", id))\n\t\treturn\n\t}\n\trender.ProtoJSON(w, adm)\n}\n\n// GetAdmins returns a segment of admins associated with the authority.\nfunc GetAdmins(w http.ResponseWriter, r *http.Request) {\n\tcursor, limit, err := api.ParseCursor(r)\n\tif err != nil {\n\t\trender.Error(w, r, admin.WrapError(admin.ErrorBadRequestType, err,\n\t\t\t\"error parsing cursor and limit from query params\"))\n\t\treturn\n\t}\n\n\tadmins, nextCursor, err := mustAuthority(r.Context()).GetAdmins(cursor, limit)\n\tif err != nil {\n\t\trender.Error(w, r, admin.WrapErrorISE(err, \"error retrieving paginated admins\"))\n\t\treturn\n\t}\n\trender.JSON(w, r, &GetAdminsResponse{\n\t\tAdmins:     admins,\n\t\tNextCursor: nextCursor,\n\t})\n}\n\n// CreateAdmin creates a new admin.\nfunc CreateAdmin(w http.ResponseWriter, r *http.Request) {\n\tvar body CreateAdminRequest\n\tif err := read.JSON(r.Body, &body); err != nil {\n\t\trender.Error(w, r, admin.WrapError(admin.ErrorBadRequestType, err, \"error reading request body\"))\n\t\treturn\n\t}\n\n\tif err := body.Validate(); err != nil {\n\t\trender.Error(w, r, err)\n\t\treturn\n\t}\n\n\tauth := mustAuthority(r.Context())\n\tp, err := auth.LoadProvisionerByName(body.Provisioner)\n\tif err != nil {\n\t\trender.Error(w, r, admin.WrapErrorISE(err, \"error loading provisioner %s\", body.Provisioner))\n\t\treturn\n\t}\n\tadm := &linkedca.Admin{\n\t\tProvisionerId: p.GetID(),\n\t\tSubject:       body.Subject,\n\t\tType:          body.Type,\n\t}\n\t// Store to authority collection.\n\tif err := auth.StoreAdmin(r.Context(), adm, p); err != nil {\n\t\trender.Error(w, r, admin.WrapErrorISE(err, \"error storing admin\"))\n\t\treturn\n\t}\n\n\trender.ProtoJSONStatus(w, adm, http.StatusCreated)\n}\n\n// DeleteAdmin deletes admin.\nfunc DeleteAdmin(w http.ResponseWriter, r *http.Request) {\n\tid := chi.URLParam(r, \"id\")\n\n\tif err := mustAuthority(r.Context()).RemoveAdmin(r.Context(), id); err != nil {\n\t\trender.Error(w, r, admin.WrapErrorISE(err, \"error deleting admin %s\", id))\n\t\treturn\n\t}\n\n\trender.JSON(w, r, &DeleteResponse{Status: \"ok\"})\n}\n\n// UpdateAdmin updates an existing admin.\nfunc UpdateAdmin(w http.ResponseWriter, r *http.Request) {\n\tvar body UpdateAdminRequest\n\tif err := read.JSON(r.Body, &body); err != nil {\n\t\trender.Error(w, r, admin.WrapError(admin.ErrorBadRequestType, err, \"error reading request body\"))\n\t\treturn\n\t}\n\n\tif err := body.Validate(); err != nil {\n\t\trender.Error(w, r, err)\n\t\treturn\n\t}\n\n\tid := chi.URLParam(r, \"id\")\n\tauth := mustAuthority(r.Context())\n\tadm, err := auth.UpdateAdmin(r.Context(), id, &linkedca.Admin{Type: body.Type})\n\tif err != nil {\n\t\trender.Error(w, r, admin.WrapErrorISE(err, \"error updating admin %s\", id))\n\t\treturn\n\t}\n\n\trender.ProtoJSON(w, adm)\n}\n"
  },
  {
    "path": "authority/admin/api/admin_test.go",
    "content": "package api\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-chi/chi/v5\"\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/google/go-cmp/cmp/cmpopts\"\n\t\"google.golang.org/protobuf/types/known/timestamppb\"\n\n\t\"github.com/smallstep/linkedca\"\n\n\t\"github.com/smallstep/assert\"\n\t\"github.com/smallstep/certificates/authority/admin\"\n\t\"github.com/smallstep/certificates/authority/provisioner\"\n)\n\ntype mockAdminAuthority struct {\n\tMockLoadProvisionerByName func(name string) (provisioner.Interface, error)\n\tMockGetProvisioners       func(nextCursor string, limit int) (provisioner.List, string, error)\n\tMockRet1, MockRet2        interface{} // TODO: refactor the ret1/ret2 into those two\n\tMockErr                   error\n\tMockIsAdminAPIEnabled     func() bool\n\tMockLoadAdminByID         func(id string) (*linkedca.Admin, bool)\n\tMockGetAdmins             func(cursor string, limit int) ([]*linkedca.Admin, string, error)\n\tMockStoreAdmin            func(ctx context.Context, adm *linkedca.Admin, prov provisioner.Interface) error\n\tMockUpdateAdmin           func(ctx context.Context, id string, nu *linkedca.Admin) (*linkedca.Admin, error)\n\tMockRemoveAdmin           func(ctx context.Context, id string) error\n\tMockAuthorizeAdminToken   func(r *http.Request, token string) (*linkedca.Admin, error)\n\tMockStoreProvisioner      func(ctx context.Context, prov *linkedca.Provisioner) error\n\tMockLoadProvisionerByID   func(id string) (provisioner.Interface, error)\n\tMockUpdateProvisioner     func(ctx context.Context, nu *linkedca.Provisioner) error\n\tMockRemoveProvisioner     func(ctx context.Context, id string) error\n\n\tMockGetAuthorityPolicy    func(ctx context.Context) (*linkedca.Policy, error)\n\tMockCreateAuthorityPolicy func(ctx context.Context, adm *linkedca.Admin, policy *linkedca.Policy) (*linkedca.Policy, error)\n\tMockUpdateAuthorityPolicy func(ctx context.Context, adm *linkedca.Admin, policy *linkedca.Policy) (*linkedca.Policy, error)\n\tMockRemoveAuthorityPolicy func(ctx context.Context) error\n}\n\nfunc (m *mockAdminAuthority) IsAdminAPIEnabled() bool {\n\tif m.MockIsAdminAPIEnabled != nil {\n\t\treturn m.MockIsAdminAPIEnabled()\n\t}\n\treturn m.MockRet1.(bool)\n}\n\nfunc (m *mockAdminAuthority) LoadProvisionerByName(name string) (provisioner.Interface, error) {\n\tif m.MockLoadProvisionerByName != nil {\n\t\treturn m.MockLoadProvisionerByName(name)\n\t}\n\treturn m.MockRet1.(provisioner.Interface), m.MockErr\n}\n\nfunc (m *mockAdminAuthority) GetProvisioners(nextCursor string, limit int) (provisioner.List, string, error) {\n\tif m.MockGetProvisioners != nil {\n\t\treturn m.MockGetProvisioners(nextCursor, limit)\n\t}\n\treturn m.MockRet1.(provisioner.List), m.MockRet2.(string), m.MockErr\n}\n\nfunc (m *mockAdminAuthority) LoadAdminByID(id string) (*linkedca.Admin, bool) {\n\tif m.MockLoadAdminByID != nil {\n\t\treturn m.MockLoadAdminByID(id)\n\t}\n\treturn m.MockRet1.(*linkedca.Admin), m.MockRet2.(bool)\n}\n\nfunc (m *mockAdminAuthority) GetAdmins(cursor string, limit int) ([]*linkedca.Admin, string, error) {\n\tif m.MockGetAdmins != nil {\n\t\treturn m.MockGetAdmins(cursor, limit)\n\t}\n\treturn m.MockRet1.([]*linkedca.Admin), m.MockRet2.(string), m.MockErr\n}\n\nfunc (m *mockAdminAuthority) StoreAdmin(ctx context.Context, adm *linkedca.Admin, prov provisioner.Interface) error {\n\tif m.MockStoreAdmin != nil {\n\t\treturn m.MockStoreAdmin(ctx, adm, prov)\n\t}\n\treturn m.MockErr\n}\n\nfunc (m *mockAdminAuthority) UpdateAdmin(ctx context.Context, id string, nu *linkedca.Admin) (*linkedca.Admin, error) {\n\tif m.MockUpdateAdmin != nil {\n\t\treturn m.MockUpdateAdmin(ctx, id, nu)\n\t}\n\treturn m.MockRet1.(*linkedca.Admin), m.MockErr\n}\n\nfunc (m *mockAdminAuthority) RemoveAdmin(ctx context.Context, id string) error {\n\tif m.MockRemoveAdmin != nil {\n\t\treturn m.MockRemoveAdmin(ctx, id)\n\t}\n\treturn m.MockErr\n}\n\nfunc (m *mockAdminAuthority) AuthorizeAdminToken(r *http.Request, token string) (*linkedca.Admin, error) {\n\tif m.MockAuthorizeAdminToken != nil {\n\t\treturn m.MockAuthorizeAdminToken(r, token)\n\t}\n\treturn m.MockRet1.(*linkedca.Admin), m.MockErr\n}\n\nfunc (m *mockAdminAuthority) StoreProvisioner(ctx context.Context, prov *linkedca.Provisioner) error {\n\tif m.MockStoreProvisioner != nil {\n\t\treturn m.MockStoreProvisioner(ctx, prov)\n\t}\n\treturn m.MockErr\n}\n\nfunc (m *mockAdminAuthority) LoadProvisionerByID(id string) (provisioner.Interface, error) {\n\tif m.MockLoadProvisionerByID != nil {\n\t\treturn m.MockLoadProvisionerByID(id)\n\t}\n\treturn m.MockRet1.(provisioner.Interface), m.MockErr\n}\n\nfunc (m *mockAdminAuthority) UpdateProvisioner(ctx context.Context, nu *linkedca.Provisioner) error {\n\tif m.MockUpdateProvisioner != nil {\n\t\treturn m.MockUpdateProvisioner(ctx, nu)\n\t}\n\treturn m.MockErr\n}\n\nfunc (m *mockAdminAuthority) RemoveProvisioner(ctx context.Context, id string) error {\n\tif m.MockRemoveProvisioner != nil {\n\t\treturn m.MockRemoveProvisioner(ctx, id)\n\t}\n\treturn m.MockErr\n}\n\nfunc (m *mockAdminAuthority) GetAuthorityPolicy(ctx context.Context) (*linkedca.Policy, error) {\n\tif m.MockGetAuthorityPolicy != nil {\n\t\treturn m.MockGetAuthorityPolicy(ctx)\n\t}\n\treturn m.MockRet1.(*linkedca.Policy), m.MockErr\n}\n\nfunc (m *mockAdminAuthority) CreateAuthorityPolicy(ctx context.Context, adm *linkedca.Admin, policy *linkedca.Policy) (*linkedca.Policy, error) {\n\tif m.MockCreateAuthorityPolicy != nil {\n\t\treturn m.MockCreateAuthorityPolicy(ctx, adm, policy)\n\t}\n\treturn m.MockRet1.(*linkedca.Policy), m.MockErr\n}\n\nfunc (m *mockAdminAuthority) UpdateAuthorityPolicy(ctx context.Context, adm *linkedca.Admin, policy *linkedca.Policy) (*linkedca.Policy, error) {\n\tif m.MockUpdateAuthorityPolicy != nil {\n\t\treturn m.MockUpdateAuthorityPolicy(ctx, adm, policy)\n\t}\n\treturn m.MockRet1.(*linkedca.Policy), m.MockErr\n}\n\nfunc (m *mockAdminAuthority) RemoveAuthorityPolicy(ctx context.Context) error {\n\tif m.MockRemoveAuthorityPolicy != nil {\n\t\treturn m.MockRemoveAuthorityPolicy(ctx)\n\t}\n\treturn m.MockErr\n}\n\nfunc TestCreateAdminRequest_Validate(t *testing.T) {\n\ttype fields struct {\n\t\tSubject     string\n\t\tProvisioner string\n\t\tType        linkedca.Admin_Type\n\t}\n\ttests := []struct {\n\t\tname   string\n\t\tfields fields\n\t\terr    *admin.Error\n\t}{\n\t\t{\n\t\t\tname: \"fail/subject-empty\",\n\t\t\tfields: fields{\n\t\t\t\tSubject:     \"\",\n\t\t\t\tProvisioner: \"\",\n\t\t\t\tType:        0,\n\t\t\t},\n\t\t\terr: admin.NewError(admin.ErrorBadRequestType, \"subject cannot be empty\"),\n\t\t},\n\t\t{\n\t\t\tname: \"fail/provisioner-empty\",\n\t\t\tfields: fields{\n\t\t\t\tSubject:     \"admin\",\n\t\t\t\tProvisioner: \"\",\n\t\t\t\tType:        0,\n\t\t\t},\n\t\t\terr: admin.NewError(admin.ErrorBadRequestType, \"provisioner cannot be empty\"),\n\t\t},\n\t\t{\n\t\t\tname: \"fail/invalid-type\",\n\t\t\tfields: fields{\n\t\t\t\tSubject:     \"admin\",\n\t\t\t\tProvisioner: \"prov\",\n\t\t\t\tType:        -1,\n\t\t\t},\n\t\t\terr: admin.NewError(admin.ErrorBadRequestType, \"invalid value for admin type\"),\n\t\t},\n\t\t{\n\t\t\tname: \"ok\",\n\t\t\tfields: fields{\n\t\t\t\tSubject:     \"admin\",\n\t\t\t\tProvisioner: \"prov\",\n\t\t\t\tType:        linkedca.Admin_SUPER_ADMIN,\n\t\t\t},\n\t\t\terr: nil,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tcar := &CreateAdminRequest{\n\t\t\t\tSubject:     tt.fields.Subject,\n\t\t\t\tProvisioner: tt.fields.Provisioner,\n\t\t\t\tType:        tt.fields.Type,\n\t\t\t}\n\t\t\terr := car.Validate()\n\n\t\t\tif (err != nil) != (tt.err != nil) {\n\t\t\t\tt.Errorf(\"CreateAdminRequest.Validate() error = %v, wantErr %v\", err, (tt.err != nil))\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif err != nil {\n\t\t\t\tassert.Type(t, &admin.Error{}, err)\n\t\t\t\tvar adminErr *admin.Error\n\t\t\t\tif assert.True(t, errors.As(err, &adminErr)) {\n\t\t\t\t\tassert.Equals(t, tt.err.Type, adminErr.Type)\n\t\t\t\t\tassert.Equals(t, tt.err.Detail, adminErr.Detail)\n\t\t\t\t\tassert.Equals(t, tt.err.Status, adminErr.Status)\n\t\t\t\t\tassert.Equals(t, tt.err.Message, adminErr.Message)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestUpdateAdminRequest_Validate(t *testing.T) {\n\ttype fields struct {\n\t\tType linkedca.Admin_Type\n\t}\n\ttests := []struct {\n\t\tname   string\n\t\tfields fields\n\t\terr    *admin.Error\n\t}{\n\t\t{\n\t\t\tname: \"fail/invalid-type\",\n\t\t\tfields: fields{\n\t\t\t\tType: -1,\n\t\t\t},\n\t\t\terr: admin.NewError(admin.ErrorBadRequestType, \"invalid value for admin type\"),\n\t\t},\n\t\t{\n\t\t\tname: \"ok\",\n\t\t\tfields: fields{\n\t\t\t\tType: linkedca.Admin_SUPER_ADMIN,\n\t\t\t},\n\t\t\terr: nil,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tuar := &UpdateAdminRequest{\n\t\t\t\tType: tt.fields.Type,\n\t\t\t}\n\n\t\t\terr := uar.Validate()\n\n\t\t\tif (err != nil) != (tt.err != nil) {\n\t\t\t\tt.Errorf(\"CreateAdminRequest.Validate() error = %v, wantErr %v\", err, (tt.err != nil))\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif err != nil {\n\t\t\t\tassert.Type(t, &admin.Error{}, err)\n\t\t\t\tvar ae *admin.Error\n\t\t\t\tif assert.True(t, errors.As(err, &ae)) {\n\t\t\t\t\tassert.Equals(t, tt.err.Type, ae.Type)\n\t\t\t\t\tassert.Equals(t, tt.err.Detail, ae.Detail)\n\t\t\t\t\tassert.Equals(t, tt.err.Status, ae.Status)\n\t\t\t\t\tassert.Equals(t, tt.err.Message, ae.Message)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestHandler_GetAdmin(t *testing.T) {\n\ttype test struct {\n\t\tctx        context.Context\n\t\tauth       adminAuthority\n\t\tstatusCode int\n\t\terr        *admin.Error\n\t\tadm        *linkedca.Admin\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/auth.LoadAdminByID-not-found\": func(t *testing.T) test {\n\t\t\tchiCtx := chi.NewRouteContext()\n\t\t\tchiCtx.URLParams.Add(\"id\", \"adminID\")\n\t\t\tctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)\n\t\t\tauth := &mockAdminAuthority{\n\t\t\t\tMockLoadAdminByID: func(id string) (*linkedca.Admin, bool) {\n\t\t\t\t\tassert.Equals(t, \"adminID\", id)\n\t\t\t\t\treturn nil, false\n\t\t\t\t},\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\tauth:       auth,\n\t\t\t\tstatusCode: 404,\n\t\t\t\terr: &admin.Error{\n\t\t\t\t\tType:    admin.ErrorNotFoundType.String(),\n\t\t\t\t\tStatus:  404,\n\t\t\t\t\tDetail:  \"resource not found\",\n\t\t\t\t\tMessage: \"admin adminID not found\",\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tchiCtx := chi.NewRouteContext()\n\t\t\tchiCtx.URLParams.Add(\"id\", \"adminID\")\n\t\t\tctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)\n\t\t\tcreatedAt := time.Now()\n\t\t\tvar deletedAt time.Time\n\t\t\tadm := &linkedca.Admin{\n\t\t\t\tId:            \"adminID\",\n\t\t\t\tAuthorityId:   \"authorityID\",\n\t\t\t\tSubject:       \"admin\",\n\t\t\t\tProvisionerId: \"provID\",\n\t\t\t\tType:          linkedca.Admin_SUPER_ADMIN,\n\t\t\t\tCreatedAt:     timestamppb.New(createdAt),\n\t\t\t\tDeletedAt:     timestamppb.New(deletedAt),\n\t\t\t}\n\t\t\tauth := &mockAdminAuthority{\n\t\t\t\tMockLoadAdminByID: func(id string) (*linkedca.Admin, bool) {\n\t\t\t\t\tassert.Equals(t, \"adminID\", id)\n\t\t\t\t\treturn adm, true\n\t\t\t\t},\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\tauth:       auth,\n\t\t\t\tstatusCode: 200,\n\t\t\t\terr:        nil,\n\t\t\t\tadm:        adm,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, prep := range tests {\n\t\ttc := prep(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tmockMustAuthority(t, tc.auth)\n\t\t\treq := httptest.NewRequest(\"GET\", \"/foo\", http.NoBody) // chi routing is prepared in test setup\n\t\t\treq = req.WithContext(tc.ctx)\n\t\t\tw := httptest.NewRecorder()\n\t\t\tGetAdmin(w, req)\n\t\t\tres := w.Result()\n\n\t\t\tassert.Equals(t, tc.statusCode, res.StatusCode)\n\n\t\t\tif res.StatusCode >= 400 {\n\n\t\t\t\tbody, err := io.ReadAll(res.Body)\n\t\t\t\tres.Body.Close()\n\t\t\t\tassert.FatalError(t, err)\n\n\t\t\t\tadminErr := admin.Error{}\n\t\t\t\tassert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &adminErr))\n\n\t\t\t\tassert.Equals(t, tc.err.Type, adminErr.Type)\n\t\t\t\tassert.Equals(t, tc.err.Message, adminErr.Message)\n\t\t\t\tassert.Equals(t, tc.err.Detail, adminErr.Detail)\n\t\t\t\tassert.Equals(t, []string{\"application/json\"}, res.Header[\"Content-Type\"])\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tadm := &linkedca.Admin{}\n\t\t\terr := readProtoJSON(res.Body, adm)\n\t\t\tassert.FatalError(t, err)\n\n\t\t\tassert.Equals(t, []string{\"application/json\"}, res.Header[\"Content-Type\"])\n\n\t\t\topts := []cmp.Option{cmpopts.IgnoreUnexported(linkedca.Admin{}, timestamppb.Timestamp{})}\n\t\t\tif !cmp.Equal(tc.adm, adm, opts...) {\n\t\t\t\tt.Errorf(\"linkedca.Admin diff =\\n%s\", cmp.Diff(tc.adm, adm, opts...))\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestHandler_GetAdmins(t *testing.T) {\n\ttype test struct {\n\t\tctx        context.Context\n\t\tauth       adminAuthority\n\t\treq        *http.Request\n\t\tstatusCode int\n\t\terr        *admin.Error\n\t\tresp       GetAdminsResponse\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/parse-cursor\": func(t *testing.T) test {\n\t\t\treq := httptest.NewRequest(\"GET\", \"/foo?limit=A\", http.NoBody)\n\t\t\treturn test{\n\t\t\t\tctx:        context.Background(),\n\t\t\t\treq:        req,\n\t\t\t\tstatusCode: 400,\n\t\t\t\terr: &admin.Error{\n\t\t\t\t\tStatus:  400,\n\t\t\t\t\tType:    admin.ErrorBadRequestType.String(),\n\t\t\t\t\tDetail:  \"bad request\",\n\t\t\t\t\tMessage: \"error parsing cursor and limit from query params: limit 'A' is not an integer: strconv.Atoi: parsing \\\"A\\\": invalid syntax\",\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"fail/auth.GetAdmins\": func(t *testing.T) test {\n\t\t\treq := httptest.NewRequest(\"GET\", \"/foo\", http.NoBody)\n\t\t\tauth := &mockAdminAuthority{\n\t\t\t\tMockGetAdmins: func(cursor string, limit int) ([]*linkedca.Admin, string, error) {\n\t\t\t\t\tassert.Equals(t, \"\", cursor)\n\t\t\t\t\tassert.Equals(t, 0, limit)\n\t\t\t\t\treturn nil, \"\", errors.New(\"force\")\n\t\t\t\t},\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tctx:        context.Background(),\n\t\t\t\treq:        req,\n\t\t\t\tauth:       auth,\n\t\t\t\tstatusCode: 500,\n\t\t\t\terr: &admin.Error{\n\t\t\t\t\tStatus:  500,\n\t\t\t\t\tType:    admin.ErrorServerInternalType.String(),\n\t\t\t\t\tDetail:  \"the server experienced an internal error\",\n\t\t\t\t\tMessage: \"error retrieving paginated admins: force\",\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\treq := httptest.NewRequest(\"GET\", \"/foo\", http.NoBody)\n\t\t\tcreatedAt := time.Now()\n\t\t\tvar deletedAt time.Time\n\t\t\tadm1 := &linkedca.Admin{\n\t\t\t\tId:            \"adminID1\",\n\t\t\t\tAuthorityId:   \"authorityID1\",\n\t\t\t\tSubject:       \"admin1\",\n\t\t\t\tProvisionerId: \"provID\",\n\t\t\t\tType:          linkedca.Admin_SUPER_ADMIN,\n\t\t\t\tCreatedAt:     timestamppb.New(createdAt),\n\t\t\t\tDeletedAt:     timestamppb.New(deletedAt),\n\t\t\t}\n\t\t\tadm2 := &linkedca.Admin{\n\t\t\t\tId:            \"adminID2\",\n\t\t\t\tAuthorityId:   \"authorityID\",\n\t\t\t\tSubject:       \"admin2\",\n\t\t\t\tProvisionerId: \"provID\",\n\t\t\t\tType:          linkedca.Admin_ADMIN,\n\t\t\t\tCreatedAt:     timestamppb.New(createdAt),\n\t\t\t\tDeletedAt:     timestamppb.New(deletedAt),\n\t\t\t}\n\t\t\tauth := &mockAdminAuthority{\n\t\t\t\tMockGetAdmins: func(cursor string, limit int) ([]*linkedca.Admin, string, error) {\n\t\t\t\t\tassert.Equals(t, \"\", cursor)\n\t\t\t\t\tassert.Equals(t, 0, limit)\n\t\t\t\t\treturn []*linkedca.Admin{\n\t\t\t\t\t\tadm1,\n\t\t\t\t\t\tadm2,\n\t\t\t\t\t}, \"nextCursorValue\", nil\n\t\t\t\t},\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tctx:        context.Background(),\n\t\t\t\treq:        req,\n\t\t\t\tauth:       auth,\n\t\t\t\tstatusCode: 200,\n\t\t\t\terr:        nil,\n\t\t\t\tresp: GetAdminsResponse{\n\t\t\t\t\tAdmins: []*linkedca.Admin{\n\t\t\t\t\t\tadm1,\n\t\t\t\t\t\tadm2,\n\t\t\t\t\t},\n\t\t\t\t\tNextCursor: \"nextCursorValue\",\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t}\n\tfor name, prep := range tests {\n\t\ttc := prep(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tmockMustAuthority(t, tc.auth)\n\t\t\treq := tc.req.WithContext(tc.ctx)\n\t\t\tw := httptest.NewRecorder()\n\t\t\tGetAdmins(w, req)\n\t\t\tres := w.Result()\n\n\t\t\tassert.Equals(t, tc.statusCode, res.StatusCode)\n\n\t\t\tbody, err := io.ReadAll(res.Body)\n\t\t\tres.Body.Close()\n\t\t\tassert.FatalError(t, err)\n\n\t\t\tif res.StatusCode >= 400 {\n\n\t\t\t\tadminErr := admin.Error{}\n\t\t\t\tassert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &adminErr))\n\n\t\t\t\tassert.Equals(t, tc.err.Type, adminErr.Type)\n\t\t\t\tassert.Equals(t, tc.err.Message, adminErr.Message)\n\t\t\t\tassert.Equals(t, tc.err.Detail, adminErr.Detail)\n\t\t\t\tassert.Equals(t, []string{\"application/json\"}, res.Header[\"Content-Type\"])\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tresponse := GetAdminsResponse{}\n\t\t\tassert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &response))\n\t\t\tassert.Equals(t, []string{\"application/json\"}, res.Header[\"Content-Type\"])\n\n\t\t\topts := []cmp.Option{cmpopts.IgnoreUnexported(linkedca.Admin{}, timestamppb.Timestamp{})}\n\t\t\tif !cmp.Equal(tc.resp, response, opts...) {\n\t\t\t\tt.Errorf(\"GetAdmins diff =\\n%s\", cmp.Diff(tc.resp, response, opts...))\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestHandler_CreateAdmin(t *testing.T) {\n\ttype test struct {\n\t\tctx        context.Context\n\t\tauth       adminAuthority\n\t\tbody       []byte\n\t\tstatusCode int\n\t\terr        *admin.Error\n\t\tadm        *linkedca.Admin\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/ReadJSON\": func(t *testing.T) test {\n\t\t\tbody := []byte(\"{!?}\")\n\t\t\treturn test{\n\t\t\t\tctx:        context.Background(),\n\t\t\t\tbody:       body,\n\t\t\t\tstatusCode: 400,\n\t\t\t\terr: &admin.Error{\n\t\t\t\t\tType:    admin.ErrorBadRequestType.String(),\n\t\t\t\t\tStatus:  400,\n\t\t\t\t\tDetail:  \"bad request\",\n\t\t\t\t\tMessage: \"error reading request body: error decoding json: invalid character '!' looking for beginning of object key string\",\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"fail/validate\": func(t *testing.T) test {\n\t\t\treq := CreateAdminRequest{\n\t\t\t\tSubject:     \"\",\n\t\t\t\tProvisioner: \"\",\n\t\t\t\tType:        -1,\n\t\t\t}\n\t\t\tbody, err := json.Marshal(req)\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tctx:        context.Background(),\n\t\t\t\tbody:       body,\n\t\t\t\tstatusCode: 400,\n\t\t\t\terr: &admin.Error{\n\t\t\t\t\tType:    admin.ErrorBadRequestType.String(),\n\t\t\t\t\tStatus:  400,\n\t\t\t\t\tDetail:  \"bad request\",\n\t\t\t\t\tMessage: \"subject cannot be empty\",\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"fail/auth.LoadProvisionerByName\": func(t *testing.T) test {\n\t\t\treq := CreateAdminRequest{\n\t\t\t\tSubject:     \"admin\",\n\t\t\t\tProvisioner: \"prov\",\n\t\t\t\tType:        linkedca.Admin_SUPER_ADMIN,\n\t\t\t}\n\t\t\tbody, err := json.Marshal(req)\n\t\t\tassert.FatalError(t, err)\n\t\t\tauth := &mockAdminAuthority{\n\t\t\t\tMockLoadProvisionerByName: func(name string) (provisioner.Interface, error) {\n\t\t\t\t\tassert.Equals(t, \"prov\", name)\n\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t},\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tctx:        context.Background(),\n\t\t\t\tbody:       body,\n\t\t\t\tauth:       auth,\n\t\t\t\tstatusCode: 500,\n\t\t\t\terr: &admin.Error{\n\t\t\t\t\tType:    admin.ErrorServerInternalType.String(),\n\t\t\t\t\tStatus:  500,\n\t\t\t\t\tDetail:  \"the server experienced an internal error\",\n\t\t\t\t\tMessage: \"error loading provisioner prov: force\",\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"fail/auth.StoreAdmin\": func(t *testing.T) test {\n\t\t\treq := CreateAdminRequest{\n\t\t\t\tSubject:     \"admin\",\n\t\t\t\tProvisioner: \"prov\",\n\t\t\t\tType:        linkedca.Admin_SUPER_ADMIN,\n\t\t\t}\n\t\t\tbody, err := json.Marshal(req)\n\t\t\tassert.FatalError(t, err)\n\t\t\tauth := &mockAdminAuthority{\n\t\t\t\tMockLoadProvisionerByName: func(name string) (provisioner.Interface, error) {\n\t\t\t\t\tassert.Equals(t, \"prov\", name)\n\t\t\t\t\treturn &provisioner.ACME{\n\t\t\t\t\t\tID:   \"provID\",\n\t\t\t\t\t\tName: \"prov\",\n\t\t\t\t\t}, nil\n\t\t\t\t},\n\t\t\t\tMockStoreAdmin: func(ctx context.Context, adm *linkedca.Admin, prov provisioner.Interface) error {\n\t\t\t\t\tassert.Equals(t, \"admin\", adm.Subject)\n\t\t\t\t\tassert.Equals(t, \"provID\", prov.GetID())\n\t\t\t\t\treturn errors.New(\"force\")\n\t\t\t\t},\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tctx:        context.Background(),\n\t\t\t\tbody:       body,\n\t\t\t\tauth:       auth,\n\t\t\t\tstatusCode: 500,\n\t\t\t\terr: &admin.Error{\n\t\t\t\t\tType:    admin.ErrorServerInternalType.String(),\n\t\t\t\t\tStatus:  500,\n\t\t\t\t\tDetail:  \"the server experienced an internal error\",\n\t\t\t\t\tMessage: \"error storing admin: force\",\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\treq := CreateAdminRequest{\n\t\t\t\tSubject:     \"admin\",\n\t\t\t\tProvisioner: \"prov\",\n\t\t\t\tType:        linkedca.Admin_SUPER_ADMIN,\n\t\t\t}\n\t\t\tbody, err := json.Marshal(req)\n\t\t\tassert.FatalError(t, err)\n\t\t\tauth := &mockAdminAuthority{\n\t\t\t\tMockLoadProvisionerByName: func(name string) (provisioner.Interface, error) {\n\t\t\t\t\tassert.Equals(t, \"prov\", name)\n\t\t\t\t\treturn &provisioner.ACME{\n\t\t\t\t\t\tID:   \"provID\",\n\t\t\t\t\t\tName: \"prov\",\n\t\t\t\t\t}, nil\n\t\t\t\t},\n\t\t\t\tMockStoreAdmin: func(ctx context.Context, adm *linkedca.Admin, prov provisioner.Interface) error {\n\t\t\t\t\tassert.Equals(t, \"admin\", adm.Subject)\n\t\t\t\t\tassert.Equals(t, \"provID\", prov.GetID())\n\t\t\t\t\treturn nil\n\t\t\t\t},\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tctx:        context.Background(),\n\t\t\t\tbody:       body,\n\t\t\t\tauth:       auth,\n\t\t\t\tstatusCode: 201,\n\t\t\t\terr:        nil,\n\t\t\t\tadm: &linkedca.Admin{\n\t\t\t\t\tProvisionerId: \"provID\",\n\t\t\t\t\tSubject:       \"admin\",\n\t\t\t\t\tType:          linkedca.Admin_SUPER_ADMIN,\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t}\n\tfor name, prep := range tests {\n\t\ttc := prep(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tmockMustAuthority(t, tc.auth)\n\t\t\treq := httptest.NewRequest(\"GET\", \"/foo\", io.NopCloser(bytes.NewBuffer(tc.body)))\n\t\t\treq = req.WithContext(tc.ctx)\n\t\t\tw := httptest.NewRecorder()\n\t\t\tCreateAdmin(w, req)\n\t\t\tres := w.Result()\n\n\t\t\tassert.Equals(t, tc.statusCode, res.StatusCode)\n\n\t\t\tif res.StatusCode >= 400 {\n\n\t\t\t\tbody, err := io.ReadAll(res.Body)\n\t\t\t\tres.Body.Close()\n\t\t\t\tassert.FatalError(t, err)\n\n\t\t\t\tadminErr := admin.Error{}\n\t\t\t\tassert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &adminErr))\n\n\t\t\t\tassert.Equals(t, tc.err.Type, adminErr.Type)\n\t\t\t\tassert.Equals(t, tc.err.Message, adminErr.Message)\n\t\t\t\tassert.Equals(t, tc.err.Detail, adminErr.Detail)\n\t\t\t\tassert.Equals(t, []string{\"application/json\"}, res.Header[\"Content-Type\"])\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tadm := &linkedca.Admin{}\n\t\t\terr := readProtoJSON(res.Body, adm)\n\t\t\tassert.FatalError(t, err)\n\n\t\t\tassert.Equals(t, []string{\"application/json\"}, res.Header[\"Content-Type\"])\n\n\t\t\topts := []cmp.Option{cmpopts.IgnoreUnexported(linkedca.Admin{}, timestamppb.Timestamp{})}\n\t\t\tif !cmp.Equal(tc.adm, adm, opts...) {\n\t\t\t\tt.Errorf(\"h.CreateAdmin diff =\\n%s\", cmp.Diff(tc.adm, adm, opts...))\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestHandler_DeleteAdmin(t *testing.T) {\n\ttype test struct {\n\t\tctx        context.Context\n\t\tauth       adminAuthority\n\t\tstatusCode int\n\t\terr        *admin.Error\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/auth.RemoveAdmin\": func(t *testing.T) test {\n\t\t\tchiCtx := chi.NewRouteContext()\n\t\t\tchiCtx.URLParams.Add(\"id\", \"adminID\")\n\t\t\tctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)\n\t\t\tauth := &mockAdminAuthority{\n\t\t\t\tMockRemoveAdmin: func(ctx context.Context, id string) error {\n\t\t\t\t\tassert.Equals(t, \"adminID\", id)\n\t\t\t\t\treturn errors.New(\"force\")\n\t\t\t\t},\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\tauth:       auth,\n\t\t\t\tstatusCode: 500,\n\t\t\t\terr: &admin.Error{\n\t\t\t\t\tType:    admin.ErrorServerInternalType.String(),\n\t\t\t\t\tStatus:  500,\n\t\t\t\t\tDetail:  \"the server experienced an internal error\",\n\t\t\t\t\tMessage: \"error deleting admin adminID: force\",\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tchiCtx := chi.NewRouteContext()\n\t\t\tchiCtx.URLParams.Add(\"id\", \"adminID\")\n\t\t\tctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)\n\t\t\tauth := &mockAdminAuthority{\n\t\t\t\tMockRemoveAdmin: func(ctx context.Context, id string) error {\n\t\t\t\t\tassert.Equals(t, \"adminID\", id)\n\t\t\t\t\treturn nil\n\t\t\t\t},\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\tauth:       auth,\n\t\t\t\tstatusCode: 200,\n\t\t\t\terr:        nil,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, prep := range tests {\n\t\ttc := prep(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tmockMustAuthority(t, tc.auth)\n\t\t\treq := httptest.NewRequest(\"DELETE\", \"/foo\", http.NoBody) // chi routing is prepared in test setup\n\t\t\treq = req.WithContext(tc.ctx)\n\t\t\tw := httptest.NewRecorder()\n\t\t\tDeleteAdmin(w, req)\n\t\t\tres := w.Result()\n\t\t\tassert.Equals(t, tc.statusCode, res.StatusCode)\n\n\t\t\tif res.StatusCode >= 400 {\n\t\t\t\tbody, err := io.ReadAll(res.Body)\n\t\t\t\tres.Body.Close()\n\t\t\t\tassert.FatalError(t, err)\n\n\t\t\t\tadminErr := admin.Error{}\n\t\t\t\tassert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &adminErr))\n\n\t\t\t\tassert.Equals(t, tc.err.Type, adminErr.Type)\n\t\t\t\tassert.Equals(t, tc.err.Message, adminErr.Message)\n\t\t\t\tassert.Equals(t, tc.err.StatusCode(), res.StatusCode)\n\t\t\t\tassert.Equals(t, tc.err.Detail, adminErr.Detail)\n\t\t\t\tassert.Equals(t, []string{\"application/json\"}, res.Header[\"Content-Type\"])\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tbody, err := io.ReadAll(res.Body)\n\t\t\tres.Body.Close()\n\t\t\tassert.FatalError(t, err)\n\n\t\t\tresponse := DeleteResponse{}\n\t\t\tassert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &response))\n\t\t\tassert.Equals(t, \"ok\", response.Status)\n\t\t\tassert.Equals(t, []string{\"application/json\"}, res.Header[\"Content-Type\"])\n\n\t\t})\n\t}\n}\n\nfunc TestHandler_UpdateAdmin(t *testing.T) {\n\ttype test struct {\n\t\tctx        context.Context\n\t\tauth       adminAuthority\n\t\tbody       []byte\n\t\tstatusCode int\n\t\terr        *admin.Error\n\t\tadm        *linkedca.Admin\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/ReadJSON\": func(t *testing.T) test {\n\t\t\tbody := []byte(\"{!?}\")\n\t\t\treturn test{\n\t\t\t\tctx:        context.Background(),\n\t\t\t\tbody:       body,\n\t\t\t\tstatusCode: 400,\n\t\t\t\terr: &admin.Error{\n\t\t\t\t\tType:    admin.ErrorBadRequestType.String(),\n\t\t\t\t\tStatus:  400,\n\t\t\t\t\tDetail:  \"bad request\",\n\t\t\t\t\tMessage: \"error reading request body: error decoding json: invalid character '!' looking for beginning of object key string\",\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"fail/validate\": func(t *testing.T) test {\n\t\t\treq := UpdateAdminRequest{\n\t\t\t\tType: -1,\n\t\t\t}\n\t\t\tbody, err := json.Marshal(req)\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tctx:        context.Background(),\n\t\t\t\tbody:       body,\n\t\t\t\tstatusCode: 400,\n\t\t\t\terr: &admin.Error{\n\t\t\t\t\tType:    admin.ErrorBadRequestType.String(),\n\t\t\t\t\tStatus:  400,\n\t\t\t\t\tDetail:  \"bad request\",\n\t\t\t\t\tMessage: \"invalid value for admin type\",\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"fail/auth.UpdateAdmin\": func(t *testing.T) test {\n\t\t\treq := UpdateAdminRequest{\n\t\t\t\tType: linkedca.Admin_ADMIN,\n\t\t\t}\n\t\t\tbody, err := json.Marshal(req)\n\t\t\tassert.FatalError(t, err)\n\t\t\tchiCtx := chi.NewRouteContext()\n\t\t\tchiCtx.URLParams.Add(\"id\", \"adminID\")\n\t\t\tctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)\n\t\t\tauth := &mockAdminAuthority{\n\t\t\t\tMockUpdateAdmin: func(ctx context.Context, id string, nu *linkedca.Admin) (*linkedca.Admin, error) {\n\t\t\t\t\tassert.Equals(t, \"adminID\", id)\n\t\t\t\t\tassert.Equals(t, linkedca.Admin_ADMIN, nu.Type)\n\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t},\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\tbody:       body,\n\t\t\t\tauth:       auth,\n\t\t\t\tstatusCode: 500,\n\t\t\t\terr: &admin.Error{\n\t\t\t\t\tType:    admin.ErrorServerInternalType.String(),\n\t\t\t\t\tStatus:  500,\n\t\t\t\t\tDetail:  \"the server experienced an internal error\",\n\t\t\t\t\tMessage: \"error updating admin adminID: force\",\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\treq := UpdateAdminRequest{\n\t\t\t\tType: linkedca.Admin_ADMIN,\n\t\t\t}\n\t\t\tbody, err := json.Marshal(req)\n\t\t\tassert.FatalError(t, err)\n\t\t\tchiCtx := chi.NewRouteContext()\n\t\t\tchiCtx.URLParams.Add(\"id\", \"adminID\")\n\t\t\tctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)\n\t\t\tadm := &linkedca.Admin{\n\t\t\t\tId:            \"adminID\",\n\t\t\t\tProvisionerId: \"provID\",\n\t\t\t\tSubject:       \"admin\",\n\t\t\t\tType:          linkedca.Admin_SUPER_ADMIN,\n\t\t\t}\n\t\t\tauth := &mockAdminAuthority{\n\t\t\t\tMockUpdateAdmin: func(ctx context.Context, id string, nu *linkedca.Admin) (*linkedca.Admin, error) {\n\t\t\t\t\tassert.Equals(t, \"adminID\", id)\n\t\t\t\t\tassert.Equals(t, linkedca.Admin_ADMIN, nu.Type)\n\t\t\t\t\treturn adm, nil\n\t\t\t\t},\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\tbody:       body,\n\t\t\t\tauth:       auth,\n\t\t\t\tstatusCode: 200,\n\t\t\t\terr:        nil,\n\t\t\t\tadm:        adm,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, prep := range tests {\n\t\ttc := prep(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tmockMustAuthority(t, tc.auth)\n\t\t\treq := httptest.NewRequest(\"GET\", \"/foo\", io.NopCloser(bytes.NewBuffer(tc.body)))\n\t\t\treq = req.WithContext(tc.ctx)\n\t\t\tw := httptest.NewRecorder()\n\t\t\tUpdateAdmin(w, req)\n\t\t\tres := w.Result()\n\n\t\t\tassert.Equals(t, tc.statusCode, res.StatusCode)\n\n\t\t\tif res.StatusCode >= 400 {\n\n\t\t\t\tbody, err := io.ReadAll(res.Body)\n\t\t\t\tres.Body.Close()\n\t\t\t\tassert.FatalError(t, err)\n\n\t\t\t\tadminErr := admin.Error{}\n\t\t\t\tassert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &adminErr))\n\n\t\t\t\tassert.Equals(t, tc.err.Type, adminErr.Type)\n\t\t\t\tassert.Equals(t, tc.err.Message, adminErr.Message)\n\t\t\t\tassert.Equals(t, tc.err.Detail, adminErr.Detail)\n\t\t\t\tassert.Equals(t, []string{\"application/json\"}, res.Header[\"Content-Type\"])\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tadm := &linkedca.Admin{}\n\t\t\terr := readProtoJSON(res.Body, adm)\n\t\t\tassert.FatalError(t, err)\n\n\t\t\tassert.Equals(t, []string{\"application/json\"}, res.Header[\"Content-Type\"])\n\n\t\t\topts := []cmp.Option{cmpopts.IgnoreUnexported(linkedca.Admin{}, timestamppb.Timestamp{})}\n\t\t\tif !cmp.Equal(tc.adm, adm, opts...) {\n\t\t\t\tt.Errorf(\"h.UpdateAdmin diff =\\n%s\", cmp.Diff(tc.adm, adm, opts...))\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "authority/admin/api/handler.go",
    "content": "package api\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\n\t\"github.com/smallstep/certificates/api\"\n\t\"github.com/smallstep/certificates/authority\"\n)\n\nvar mustAuthority = func(ctx context.Context) adminAuthority {\n\treturn authority.MustFromContext(ctx)\n}\n\ntype router struct {\n\tacmeResponder    ACMEAdminResponder\n\tpolicyResponder  PolicyAdminResponder\n\twebhookResponder WebhookAdminResponder\n}\n\ntype RouterOption func(*router)\n\nfunc WithACMEResponder(acmeResponder ACMEAdminResponder) RouterOption {\n\treturn func(r *router) {\n\t\tr.acmeResponder = acmeResponder\n\t}\n}\n\nfunc WithPolicyResponder(policyResponder PolicyAdminResponder) RouterOption {\n\treturn func(r *router) {\n\t\tr.policyResponder = policyResponder\n\t}\n}\n\nfunc WithWebhookResponder(webhookResponder WebhookAdminResponder) RouterOption {\n\treturn func(r *router) {\n\t\tr.webhookResponder = webhookResponder\n\t}\n}\n\n// Route traffic and implement the Router interface.\nfunc Route(r api.Router, options ...RouterOption) {\n\trouter := &router{}\n\tfor _, fn := range options {\n\t\tfn(router)\n\t}\n\n\tauthnz := func(next http.HandlerFunc) http.HandlerFunc {\n\t\treturn extractAuthorizeTokenAdmin(requireAPIEnabled(next))\n\t}\n\n\tenabledInStandalone := func(next http.HandlerFunc) http.HandlerFunc {\n\t\treturn checkAction(next, true)\n\t}\n\n\tdisabledInStandalone := func(next http.HandlerFunc) http.HandlerFunc {\n\t\treturn checkAction(next, false)\n\t}\n\n\tacmeEABMiddleware := func(next http.HandlerFunc) http.HandlerFunc {\n\t\treturn authnz(loadProvisionerByName(requireEABEnabled(next)))\n\t}\n\n\tauthorityPolicyMiddleware := func(next http.HandlerFunc) http.HandlerFunc {\n\t\treturn authnz(enabledInStandalone(next))\n\t}\n\n\tprovisionerPolicyMiddleware := func(next http.HandlerFunc) http.HandlerFunc {\n\t\treturn authnz(disabledInStandalone(loadProvisionerByName(next)))\n\t}\n\n\tacmePolicyMiddleware := func(next http.HandlerFunc) http.HandlerFunc {\n\t\treturn authnz(disabledInStandalone(loadProvisionerByName(requireEABEnabled(loadExternalAccountKey(next)))))\n\t}\n\n\twebhookMiddleware := func(next http.HandlerFunc) http.HandlerFunc {\n\t\treturn authnz(loadProvisionerByName(next))\n\t}\n\n\t// Provisioners\n\tr.MethodFunc(\"GET\", \"/provisioners/{name}\", authnz(GetProvisioner))\n\tr.MethodFunc(\"GET\", \"/provisioners\", authnz(GetProvisioners))\n\tr.MethodFunc(\"POST\", \"/provisioners\", authnz(CreateProvisioner))\n\tr.MethodFunc(\"PUT\", \"/provisioners/{name}\", authnz(UpdateProvisioner))\n\tr.MethodFunc(\"DELETE\", \"/provisioners/{name}\", authnz(DeleteProvisioner))\n\n\t// Admins\n\tr.MethodFunc(\"GET\", \"/admins/{id}\", authnz(GetAdmin))\n\tr.MethodFunc(\"GET\", \"/admins\", authnz(GetAdmins))\n\tr.MethodFunc(\"POST\", \"/admins\", authnz(CreateAdmin))\n\tr.MethodFunc(\"PATCH\", \"/admins/{id}\", authnz(UpdateAdmin))\n\tr.MethodFunc(\"DELETE\", \"/admins/{id}\", authnz(DeleteAdmin))\n\n\t// ACME responder\n\tif router.acmeResponder != nil {\n\t\t// ACME External Account Binding Keys\n\t\tr.MethodFunc(\"GET\", \"/acme/eab/{provisionerName}/{reference}\", acmeEABMiddleware(router.acmeResponder.GetExternalAccountKeys))\n\t\tr.MethodFunc(\"GET\", \"/acme/eab/{provisionerName}\", acmeEABMiddleware(router.acmeResponder.GetExternalAccountKeys))\n\t\tr.MethodFunc(\"POST\", \"/acme/eab/{provisionerName}\", acmeEABMiddleware(router.acmeResponder.CreateExternalAccountKey))\n\t\tr.MethodFunc(\"DELETE\", \"/acme/eab/{provisionerName}/{id}\", acmeEABMiddleware(router.acmeResponder.DeleteExternalAccountKey))\n\t}\n\n\t// Policy responder\n\tif router.policyResponder != nil {\n\t\t// Policy - Authority\n\t\tr.MethodFunc(\"GET\", \"/policy\", authorityPolicyMiddleware(router.policyResponder.GetAuthorityPolicy))\n\t\tr.MethodFunc(\"POST\", \"/policy\", authorityPolicyMiddleware(router.policyResponder.CreateAuthorityPolicy))\n\t\tr.MethodFunc(\"PUT\", \"/policy\", authorityPolicyMiddleware(router.policyResponder.UpdateAuthorityPolicy))\n\t\tr.MethodFunc(\"DELETE\", \"/policy\", authorityPolicyMiddleware(router.policyResponder.DeleteAuthorityPolicy))\n\n\t\t// Policy - Provisioner\n\t\tr.MethodFunc(\"GET\", \"/provisioners/{provisionerName}/policy\", provisionerPolicyMiddleware(router.policyResponder.GetProvisionerPolicy))\n\t\tr.MethodFunc(\"POST\", \"/provisioners/{provisionerName}/policy\", provisionerPolicyMiddleware(router.policyResponder.CreateProvisionerPolicy))\n\t\tr.MethodFunc(\"PUT\", \"/provisioners/{provisionerName}/policy\", provisionerPolicyMiddleware(router.policyResponder.UpdateProvisionerPolicy))\n\t\tr.MethodFunc(\"DELETE\", \"/provisioners/{provisionerName}/policy\", provisionerPolicyMiddleware(router.policyResponder.DeleteProvisionerPolicy))\n\n\t\t// Policy - ACME Account\n\t\tr.MethodFunc(\"GET\", \"/acme/policy/{provisionerName}/reference/{reference}\", acmePolicyMiddleware(router.policyResponder.GetACMEAccountPolicy))\n\t\tr.MethodFunc(\"GET\", \"/acme/policy/{provisionerName}/key/{keyID}\", acmePolicyMiddleware(router.policyResponder.GetACMEAccountPolicy))\n\t\tr.MethodFunc(\"POST\", \"/acme/policy/{provisionerName}/reference/{reference}\", acmePolicyMiddleware(router.policyResponder.CreateACMEAccountPolicy))\n\t\tr.MethodFunc(\"POST\", \"/acme/policy/{provisionerName}/key/{keyID}\", acmePolicyMiddleware(router.policyResponder.CreateACMEAccountPolicy))\n\t\tr.MethodFunc(\"PUT\", \"/acme/policy/{provisionerName}/reference/{reference}\", acmePolicyMiddleware(router.policyResponder.UpdateACMEAccountPolicy))\n\t\tr.MethodFunc(\"PUT\", \"/acme/policy/{provisionerName}/key/{keyID}\", acmePolicyMiddleware(router.policyResponder.UpdateACMEAccountPolicy))\n\t\tr.MethodFunc(\"DELETE\", \"/acme/policy/{provisionerName}/reference/{reference}\", acmePolicyMiddleware(router.policyResponder.DeleteACMEAccountPolicy))\n\t\tr.MethodFunc(\"DELETE\", \"/acme/policy/{provisionerName}/key/{keyID}\", acmePolicyMiddleware(router.policyResponder.DeleteACMEAccountPolicy))\n\t}\n\n\tif router.webhookResponder != nil {\n\t\tr.MethodFunc(\"POST\", \"/provisioners/{provisionerName}/webhooks\", webhookMiddleware(router.webhookResponder.CreateProvisionerWebhook))\n\t\tr.MethodFunc(\"PUT\", \"/provisioners/{provisionerName}/webhooks/{webhookName}\", webhookMiddleware(router.webhookResponder.UpdateProvisionerWebhook))\n\t\tr.MethodFunc(\"DELETE\", \"/provisioners/{provisionerName}/webhooks/{webhookName}\", webhookMiddleware(router.webhookResponder.DeleteProvisionerWebhook))\n\t}\n}\n"
  },
  {
    "path": "authority/admin/api/middleware.go",
    "content": "package api\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/go-chi/chi/v5\"\n\n\t\"github.com/smallstep/linkedca\"\n\n\t\"github.com/smallstep/certificates/acme\"\n\t\"github.com/smallstep/certificates/api/render\"\n\t\"github.com/smallstep/certificates/authority/admin\"\n\t\"github.com/smallstep/certificates/authority/admin/db/nosql\"\n\t\"github.com/smallstep/certificates/authority/provisioner\"\n)\n\n// requireAPIEnabled is a middleware that ensures the Administration API\n// is enabled before servicing requests.\nfunc requireAPIEnabled(next http.HandlerFunc) http.HandlerFunc {\n\treturn func(w http.ResponseWriter, r *http.Request) {\n\t\tif !mustAuthority(r.Context()).IsAdminAPIEnabled() {\n\t\t\trender.Error(w, r, admin.NewError(admin.ErrorNotImplementedType, \"administration API not enabled\"))\n\t\t\treturn\n\t\t}\n\t\tnext(w, r)\n\t}\n}\n\n// extractAuthorizeTokenAdmin is a middleware that extracts and caches the bearer token.\nfunc extractAuthorizeTokenAdmin(next http.HandlerFunc) http.HandlerFunc {\n\treturn func(w http.ResponseWriter, r *http.Request) {\n\t\ttok := r.Header.Get(\"Authorization\")\n\t\tif tok == \"\" {\n\t\t\trender.Error(w, r, admin.NewError(admin.ErrorUnauthorizedType,\n\t\t\t\t\"missing authorization header token\"))\n\t\t\treturn\n\t\t}\n\n\t\tctx := r.Context()\n\t\tadm, err := mustAuthority(ctx).AuthorizeAdminToken(r, tok)\n\t\tif err != nil {\n\t\t\trender.Error(w, r, err)\n\t\t\treturn\n\t\t}\n\n\t\tctx = linkedca.NewContextWithAdmin(ctx, adm)\n\t\tnext(w, r.WithContext(ctx))\n\t}\n}\n\n// loadProvisionerByName is a middleware that searches for a provisioner\n// by name and stores it in the context.\nfunc loadProvisionerByName(next http.HandlerFunc) http.HandlerFunc {\n\treturn func(w http.ResponseWriter, r *http.Request) {\n\t\tvar (\n\t\t\tp   provisioner.Interface\n\t\t\terr error\n\t\t)\n\n\t\tctx := r.Context()\n\t\tauth := mustAuthority(ctx)\n\t\tadminDB := admin.MustFromContext(ctx)\n\t\tname := chi.URLParam(r, \"provisionerName\")\n\n\t\t// TODO(hs): distinguish 404 vs. 500\n\t\tif p, err = auth.LoadProvisionerByName(name); err != nil {\n\t\t\trender.Error(w, r, admin.WrapErrorISE(err, \"error loading provisioner %s\", name))\n\t\t\treturn\n\t\t}\n\n\t\tprov, err := adminDB.GetProvisioner(ctx, p.GetID())\n\t\tif err != nil {\n\t\t\trender.Error(w, r, admin.WrapErrorISE(err, \"error retrieving provisioner %s\", name))\n\t\t\treturn\n\t\t}\n\n\t\tctx = linkedca.NewContextWithProvisioner(ctx, prov)\n\t\tnext(w, r.WithContext(ctx))\n\t}\n}\n\n// checkAction checks if an action is supported in standalone or not\nfunc checkAction(next http.HandlerFunc, supportedInStandalone bool) http.HandlerFunc {\n\treturn func(w http.ResponseWriter, r *http.Request) {\n\t\t// actions allowed in standalone mode are always supported\n\t\tif supportedInStandalone {\n\t\t\tnext(w, r)\n\t\t\treturn\n\t\t}\n\n\t\t// when an action is not supported in standalone mode and when\n\t\t// using a nosql.DB backend, actions are not supported\n\t\tif _, ok := admin.MustFromContext(r.Context()).(*nosql.DB); ok {\n\t\t\trender.Error(w, r, admin.NewError(admin.ErrorNotImplementedType,\n\t\t\t\t\"operation not supported in standalone mode\"))\n\t\t\treturn\n\t\t}\n\n\t\t// continue to next http handler\n\t\tnext(w, r)\n\t}\n}\n\n// loadExternalAccountKey is a middleware that searches for an ACME\n// External Account Key by reference or keyID and stores it in the context.\nfunc loadExternalAccountKey(next http.HandlerFunc) http.HandlerFunc {\n\treturn func(w http.ResponseWriter, r *http.Request) {\n\t\tctx := r.Context()\n\t\tprov := linkedca.MustProvisionerFromContext(ctx)\n\t\tacmeDB := acme.MustDatabaseFromContext(ctx)\n\n\t\treference := chi.URLParam(r, \"reference\")\n\t\tkeyID := chi.URLParam(r, \"keyID\")\n\n\t\tvar (\n\t\t\teak *acme.ExternalAccountKey\n\t\t\terr error\n\t\t)\n\n\t\tif keyID != \"\" {\n\t\t\teak, err = acmeDB.GetExternalAccountKey(ctx, prov.GetId(), keyID)\n\t\t} else {\n\t\t\teak, err = acmeDB.GetExternalAccountKeyByReference(ctx, prov.GetId(), reference)\n\t\t}\n\n\t\tif err != nil {\n\t\t\tif acme.IsErrNotFound(err) {\n\t\t\t\trender.Error(w, r, admin.NewError(admin.ErrorNotFoundType, \"ACME External Account Key not found\"))\n\t\t\t\treturn\n\t\t\t}\n\t\t\trender.Error(w, r, admin.WrapErrorISE(err, \"error retrieving ACME External Account Key\"))\n\t\t\treturn\n\t\t}\n\n\t\tif eak == nil {\n\t\t\trender.Error(w, r, admin.NewError(admin.ErrorNotFoundType, \"ACME External Account Key not found\"))\n\t\t\treturn\n\t\t}\n\n\t\tlinkedEAK := eakToLinked(eak)\n\n\t\tctx = linkedca.NewContextWithExternalAccountKey(ctx, linkedEAK)\n\n\t\tnext(w, r.WithContext(ctx))\n\t}\n}\n"
  },
  {
    "path": "authority/admin/api/middleware_test.go",
    "content": "package api\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-chi/chi/v5\"\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/google/go-cmp/cmp/cmpopts\"\n\t\"google.golang.org/protobuf/types/known/timestamppb\"\n\n\t\"github.com/smallstep/linkedca\"\n\n\t\"github.com/smallstep/assert\"\n\t\"github.com/smallstep/certificates/acme\"\n\t\"github.com/smallstep/certificates/authority/admin\"\n\t\"github.com/smallstep/certificates/authority/admin/db/nosql\"\n\t\"github.com/smallstep/certificates/authority/provisioner\"\n)\n\nfunc TestHandler_requireAPIEnabled(t *testing.T) {\n\ttype test struct {\n\t\tctx        context.Context\n\t\tauth       adminAuthority\n\t\tnext       http.HandlerFunc\n\t\terr        *admin.Error\n\t\tstatusCode int\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/auth.IsAdminAPIEnabled\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tctx: context.Background(),\n\t\t\t\tauth: &mockAdminAuthority{\n\t\t\t\t\tMockIsAdminAPIEnabled: func() bool {\n\t\t\t\t\t\treturn false\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: &admin.Error{\n\t\t\t\t\tType:    admin.ErrorNotImplementedType.String(),\n\t\t\t\t\tStatus:  501,\n\t\t\t\t\tDetail:  \"not implemented\",\n\t\t\t\t\tMessage: \"administration API not enabled\",\n\t\t\t\t},\n\t\t\t\tstatusCode: 501,\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tauth := &mockAdminAuthority{\n\t\t\t\tMockIsAdminAPIEnabled: func() bool {\n\t\t\t\t\treturn true\n\t\t\t\t},\n\t\t\t}\n\t\t\tnext := func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\tw.Write(nil) // mock response with status 200\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tctx:        context.Background(),\n\t\t\t\tauth:       auth,\n\t\t\t\tnext:       next,\n\t\t\t\tstatusCode: 200,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, prep := range tests {\n\t\ttc := prep(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tmockMustAuthority(t, tc.auth)\n\t\t\treq := httptest.NewRequest(\"GET\", \"/foo\", http.NoBody) // chi routing is prepared in test setup\n\t\t\treq = req.WithContext(tc.ctx)\n\t\t\tw := httptest.NewRecorder()\n\t\t\trequireAPIEnabled(tc.next)(w, req)\n\t\t\tres := w.Result()\n\n\t\t\tassert.Equals(t, tc.statusCode, res.StatusCode)\n\n\t\t\tbody, err := io.ReadAll(res.Body)\n\t\t\tres.Body.Close()\n\t\t\tassert.FatalError(t, err)\n\n\t\t\tif res.StatusCode >= 400 {\n\t\t\t\terr := admin.Error{}\n\t\t\t\tassert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &err))\n\n\t\t\t\tassert.Equals(t, tc.err.Type, err.Type)\n\t\t\t\tassert.Equals(t, tc.err.Message, err.Message)\n\t\t\t\tassert.Equals(t, tc.err.StatusCode(), res.StatusCode)\n\t\t\t\tassert.Equals(t, tc.err.Detail, err.Detail)\n\t\t\t\tassert.Equals(t, []string{\"application/json\"}, res.Header[\"Content-Type\"])\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t// nothing to test when the requireAPIEnabled middleware succeeds, currently\n\n\t\t})\n\t}\n}\n\nfunc TestHandler_extractAuthorizeTokenAdmin(t *testing.T) {\n\ttype test struct {\n\t\tctx        context.Context\n\t\tauth       adminAuthority\n\t\treq        *http.Request\n\t\tnext       http.HandlerFunc\n\t\terr        *admin.Error\n\t\tstatusCode int\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/missing-authorization-token\": func(t *testing.T) test {\n\t\t\treq := httptest.NewRequest(\"GET\", \"/foo\", http.NoBody)\n\t\t\treq.Header[\"Authorization\"] = []string{\"\"}\n\t\t\treturn test{\n\t\t\t\tctx:        context.Background(),\n\t\t\t\treq:        req,\n\t\t\t\tstatusCode: 401,\n\t\t\t\terr: &admin.Error{\n\t\t\t\t\tType:    admin.ErrorUnauthorizedType.String(),\n\t\t\t\t\tStatus:  401,\n\t\t\t\t\tDetail:  \"unauthorized\",\n\t\t\t\t\tMessage: \"missing authorization header token\",\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"fail/auth.AuthorizeAdminToken\": func(t *testing.T) test {\n\t\t\treq := httptest.NewRequest(\"GET\", \"/foo\", http.NoBody)\n\t\t\treq.Header[\"Authorization\"] = []string{\"token\"}\n\t\t\tauth := &mockAdminAuthority{\n\t\t\t\tMockAuthorizeAdminToken: func(r *http.Request, token string) (*linkedca.Admin, error) {\n\t\t\t\t\tassert.Equals(t, \"token\", token)\n\t\t\t\t\treturn nil, admin.NewError(\n\t\t\t\t\t\tadmin.ErrorUnauthorizedType,\n\t\t\t\t\t\t\"not authorized\",\n\t\t\t\t\t)\n\t\t\t\t},\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tctx:        context.Background(),\n\t\t\t\tauth:       auth,\n\t\t\t\treq:        req,\n\t\t\t\tstatusCode: 401,\n\t\t\t\terr: &admin.Error{\n\t\t\t\t\tType:    admin.ErrorUnauthorizedType.String(),\n\t\t\t\t\tStatus:  401,\n\t\t\t\t\tDetail:  \"unauthorized\",\n\t\t\t\t\tMessage: \"not authorized\",\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\treq := httptest.NewRequest(\"GET\", \"/foo\", http.NoBody)\n\t\t\treq.Header[\"Authorization\"] = []string{\"token\"}\n\t\t\tcreatedAt := time.Now()\n\t\t\tvar deletedAt time.Time\n\t\t\tadm := &linkedca.Admin{\n\t\t\t\tId:            \"adminID\",\n\t\t\t\tAuthorityId:   \"authorityID\",\n\t\t\t\tSubject:       \"admin\",\n\t\t\t\tProvisionerId: \"provID\",\n\t\t\t\tType:          linkedca.Admin_SUPER_ADMIN,\n\t\t\t\tCreatedAt:     timestamppb.New(createdAt),\n\t\t\t\tDeletedAt:     timestamppb.New(deletedAt),\n\t\t\t}\n\t\t\tauth := &mockAdminAuthority{\n\t\t\t\tMockAuthorizeAdminToken: func(r *http.Request, token string) (*linkedca.Admin, error) {\n\t\t\t\t\tassert.Equals(t, \"token\", token)\n\t\t\t\t\treturn adm, nil\n\t\t\t\t},\n\t\t\t}\n\t\t\tnext := func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\tctx := r.Context()\n\t\t\t\tadm := linkedca.MustAdminFromContext(ctx) // verifying that the context now has a linkedca.Admin\n\t\t\t\topts := []cmp.Option{cmpopts.IgnoreUnexported(linkedca.Admin{}, timestamppb.Timestamp{})}\n\t\t\t\tif !cmp.Equal(adm, adm, opts...) {\n\t\t\t\t\tt.Errorf(\"linkedca.Admin diff =\\n%s\", cmp.Diff(adm, adm, opts...))\n\t\t\t\t}\n\t\t\t\tw.Write(nil) // mock response with status 200\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tctx:        context.Background(),\n\t\t\t\tauth:       auth,\n\t\t\t\treq:        req,\n\t\t\t\tnext:       next,\n\t\t\t\tstatusCode: 200,\n\t\t\t\terr:        nil,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, prep := range tests {\n\t\ttc := prep(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tmockMustAuthority(t, tc.auth)\n\t\t\treq := tc.req.WithContext(tc.ctx)\n\t\t\tw := httptest.NewRecorder()\n\t\t\textractAuthorizeTokenAdmin(tc.next)(w, req)\n\t\t\tres := w.Result()\n\n\t\t\tassert.Equals(t, tc.statusCode, res.StatusCode)\n\n\t\t\tbody, err := io.ReadAll(res.Body)\n\t\t\tres.Body.Close()\n\t\t\tassert.FatalError(t, err)\n\n\t\t\tif res.StatusCode >= 400 {\n\t\t\t\terr := admin.Error{}\n\t\t\t\tassert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &err))\n\n\t\t\t\tassert.Equals(t, tc.err.Type, err.Type)\n\t\t\t\tassert.Equals(t, tc.err.Message, err.Message)\n\t\t\t\tassert.Equals(t, tc.err.StatusCode(), res.StatusCode)\n\t\t\t\tassert.Equals(t, tc.err.Detail, err.Detail)\n\t\t\t\tassert.Equals(t, []string{\"application/json\"}, res.Header[\"Content-Type\"])\n\t\t\t\treturn\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestHandler_loadProvisionerByName(t *testing.T) {\n\ttype test struct {\n\t\tadminDB    admin.DB\n\t\tauth       adminAuthority\n\t\tctx        context.Context\n\t\tnext       http.HandlerFunc\n\t\terr        *admin.Error\n\t\tstatusCode int\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/auth.LoadProvisionerByName\": func(t *testing.T) test {\n\t\t\tchiCtx := chi.NewRouteContext()\n\t\t\tchiCtx.URLParams.Add(\"provisionerName\", \"provName\")\n\t\t\tctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)\n\t\t\tauth := &mockAdminAuthority{\n\t\t\t\tMockLoadProvisionerByName: func(name string) (provisioner.Interface, error) {\n\t\t\t\t\tassert.Equals(t, \"provName\", name)\n\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t},\n\t\t\t}\n\t\t\terr := admin.WrapErrorISE(errors.New(\"force\"), \"error loading provisioner provName\")\n\t\t\terr.Message = \"error loading provisioner provName: force\"\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\tauth:       auth,\n\t\t\t\tadminDB:    &admin.MockDB{},\n\t\t\t\tstatusCode: 500,\n\t\t\t\terr:        err,\n\t\t\t}\n\t\t},\n\t\t\"fail/db.GetProvisioner\": func(t *testing.T) test {\n\t\t\tchiCtx := chi.NewRouteContext()\n\t\t\tchiCtx.URLParams.Add(\"provisionerName\", \"provName\")\n\t\t\tctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)\n\t\t\tauth := &mockAdminAuthority{\n\t\t\t\tMockLoadProvisionerByName: func(name string) (provisioner.Interface, error) {\n\t\t\t\t\tassert.Equals(t, \"provName\", name)\n\t\t\t\t\treturn &provisioner.MockProvisioner{\n\t\t\t\t\t\tMgetID: func() string {\n\t\t\t\t\t\t\treturn \"provID\"\n\t\t\t\t\t\t},\n\t\t\t\t\t}, nil\n\t\t\t\t},\n\t\t\t}\n\t\t\tdb := &admin.MockDB{\n\t\t\t\tMockGetProvisioner: func(ctx context.Context, id string) (*linkedca.Provisioner, error) {\n\t\t\t\t\tassert.Equals(t, \"provID\", id)\n\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t},\n\t\t\t}\n\t\t\terr := admin.WrapErrorISE(errors.New(\"force\"), \"error retrieving provisioner provName\")\n\t\t\terr.Message = \"error retrieving provisioner provName: force\"\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\tauth:       auth,\n\t\t\t\tadminDB:    db,\n\t\t\t\tstatusCode: 500,\n\t\t\t\terr:        err,\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tchiCtx := chi.NewRouteContext()\n\t\t\tchiCtx.URLParams.Add(\"provisionerName\", \"provName\")\n\t\t\tctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)\n\t\t\tauth := &mockAdminAuthority{\n\t\t\t\tMockLoadProvisionerByName: func(name string) (provisioner.Interface, error) {\n\t\t\t\t\tassert.Equals(t, \"provName\", name)\n\t\t\t\t\treturn &provisioner.MockProvisioner{\n\t\t\t\t\t\tMgetID: func() string {\n\t\t\t\t\t\t\treturn \"provID\"\n\t\t\t\t\t\t},\n\t\t\t\t\t}, nil\n\t\t\t\t},\n\t\t\t}\n\t\t\tdb := &admin.MockDB{\n\t\t\t\tMockGetProvisioner: func(ctx context.Context, id string) (*linkedca.Provisioner, error) {\n\t\t\t\t\tassert.Equals(t, \"provID\", id)\n\t\t\t\t\treturn &linkedca.Provisioner{\n\t\t\t\t\t\tId:   \"provID\",\n\t\t\t\t\t\tName: \"provName\",\n\t\t\t\t\t}, nil\n\t\t\t\t},\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\tauth:       auth,\n\t\t\t\tadminDB:    db,\n\t\t\t\tstatusCode: 200,\n\t\t\t\tnext: func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\t\tprov := linkedca.MustProvisionerFromContext(r.Context())\n\t\t\t\t\tassert.NotNil(t, prov)\n\t\t\t\t\tassert.Equals(t, \"provID\", prov.GetId())\n\t\t\t\t\tassert.Equals(t, \"provName\", prov.GetName())\n\t\t\t\t\tw.Write(nil) // mock response with status 200\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t}\n\tfor name, prep := range tests {\n\t\ttc := prep(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tmockMustAuthority(t, tc.auth)\n\t\t\tctx := admin.NewContext(tc.ctx, tc.adminDB)\n\t\t\treq := httptest.NewRequest(\"GET\", \"/foo\", http.NoBody) // chi routing is prepared in test setup\n\t\t\treq = req.WithContext(ctx)\n\n\t\t\tw := httptest.NewRecorder()\n\t\t\tloadProvisionerByName(tc.next)(w, req)\n\t\t\tres := w.Result()\n\n\t\t\tassert.Equals(t, tc.statusCode, res.StatusCode)\n\n\t\t\tbody, err := io.ReadAll(res.Body)\n\t\t\tres.Body.Close()\n\t\t\tassert.FatalError(t, err)\n\n\t\t\tif res.StatusCode >= 400 {\n\t\t\t\terr := admin.Error{}\n\t\t\t\tassert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &err))\n\n\t\t\t\tassert.Equals(t, tc.err.Type, err.Type)\n\t\t\t\tassert.Equals(t, tc.err.Message, err.Message)\n\t\t\t\tassert.Equals(t, tc.err.StatusCode(), res.StatusCode)\n\t\t\t\tassert.Equals(t, tc.err.Detail, err.Detail)\n\t\t\t\tassert.Equals(t, []string{\"application/json\"}, res.Header[\"Content-Type\"])\n\t\t\t\treturn\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestHandler_checkAction(t *testing.T) {\n\ttype test struct {\n\t\tadminDB               admin.DB\n\t\tnext                  http.HandlerFunc\n\t\tsupportedInStandalone bool\n\t\terr                   *admin.Error\n\t\tstatusCode            int\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"standalone-nosql-supported\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tsupportedInStandalone: true,\n\t\t\t\tadminDB:               &nosql.DB{},\n\t\t\t\tnext: func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\t\tw.Write(nil) // mock response with status 200\n\t\t\t\t},\n\t\t\t\tstatusCode: 200,\n\t\t\t}\n\t\t},\n\t\t\"standalone-nosql-not-supported\": func(t *testing.T) test {\n\t\t\terr := admin.NewError(admin.ErrorNotImplementedType, \"operation not supported in standalone mode\")\n\t\t\terr.Message = \"operation not supported in standalone mode\"\n\t\t\treturn test{\n\t\t\t\tsupportedInStandalone: false,\n\t\t\t\tadminDB:               &nosql.DB{},\n\t\t\t\tstatusCode:            501,\n\t\t\t\terr:                   err,\n\t\t\t}\n\t\t},\n\t\t\"standalone-no-nosql-not-supported\": func(t *testing.T) test {\n\t\t\terr := admin.NewError(admin.ErrorNotImplementedType, \"operation not supported\")\n\t\t\terr.Message = \"operation not supported\"\n\t\t\treturn test{\n\t\t\t\tsupportedInStandalone: false,\n\t\t\t\tadminDB:               &admin.MockDB{},\n\t\t\t\tnext: func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\t\tw.Write(nil) // mock response with status 200\n\t\t\t\t},\n\t\t\t\tstatusCode: 200,\n\t\t\t\terr:        err,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, prep := range tests {\n\t\ttc := prep(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tctx := admin.NewContext(context.Background(), tc.adminDB)\n\t\t\treq := httptest.NewRequest(\"GET\", \"/foo\", http.NoBody).WithContext(ctx)\n\t\t\tw := httptest.NewRecorder()\n\t\t\tcheckAction(tc.next, tc.supportedInStandalone)(w, req)\n\t\t\tres := w.Result()\n\n\t\t\tassert.Equals(t, tc.statusCode, res.StatusCode)\n\n\t\t\tbody, err := io.ReadAll(res.Body)\n\t\t\tres.Body.Close()\n\t\t\tassert.FatalError(t, err)\n\n\t\t\tif res.StatusCode >= 400 {\n\t\t\t\terr := admin.Error{}\n\t\t\t\tassert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &err))\n\n\t\t\t\tassert.Equals(t, tc.err.Type, err.Type)\n\t\t\t\tassert.Equals(t, tc.err.Message, err.Message)\n\t\t\t\tassert.Equals(t, tc.err.StatusCode(), res.StatusCode)\n\t\t\t\tassert.Equals(t, tc.err.Detail, err.Detail)\n\t\t\t\tassert.Equals(t, []string{\"application/json\"}, res.Header[\"Content-Type\"])\n\t\t\t\treturn\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestHandler_loadExternalAccountKey(t *testing.T) {\n\ttype test struct {\n\t\tctx        context.Context\n\t\tacmeDB     acme.DB\n\t\tnext       http.HandlerFunc\n\t\terr        *admin.Error\n\t\tstatusCode int\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/keyID-not-found-error\": func(t *testing.T) test {\n\t\t\tprov := &linkedca.Provisioner{\n\t\t\t\tId: \"provID\",\n\t\t\t}\n\t\t\tchiCtx := chi.NewRouteContext()\n\t\t\tchiCtx.URLParams.Add(\"keyID\", \"key\")\n\t\t\tctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)\n\t\t\tctx = linkedca.NewContextWithProvisioner(ctx, prov)\n\t\t\terr := admin.NewError(admin.ErrorNotFoundType, \"ACME External Account Key not found\")\n\t\t\terr.Message = \"ACME External Account Key not found\"\n\t\t\treturn test{\n\t\t\t\tctx: ctx,\n\t\t\t\tacmeDB: &acme.MockDB{\n\t\t\t\t\tMockGetExternalAccountKey: func(ctx context.Context, provisionerID, keyID string) (*acme.ExternalAccountKey, error) {\n\t\t\t\t\t\tassert.Equals(t, \"provID\", provisionerID)\n\t\t\t\t\t\tassert.Equals(t, \"key\", keyID)\n\t\t\t\t\t\treturn nil, acme.ErrNotFound\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr:        err,\n\t\t\t\tstatusCode: 404,\n\t\t\t}\n\t\t},\n\t\t\"fail/keyID-error\": func(t *testing.T) test {\n\t\t\tprov := &linkedca.Provisioner{\n\t\t\t\tId: \"provID\",\n\t\t\t}\n\t\t\tchiCtx := chi.NewRouteContext()\n\t\t\tchiCtx.URLParams.Add(\"keyID\", \"key\")\n\t\t\tctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)\n\t\t\tctx = linkedca.NewContextWithProvisioner(ctx, prov)\n\t\t\terr := admin.WrapErrorISE(errors.New(\"force\"), \"error retrieving ACME External Account Key\")\n\t\t\terr.Message = \"error retrieving ACME External Account Key: force\"\n\t\t\treturn test{\n\t\t\t\tctx: ctx,\n\t\t\t\tacmeDB: &acme.MockDB{\n\t\t\t\t\tMockGetExternalAccountKey: func(ctx context.Context, provisionerID, keyID string) (*acme.ExternalAccountKey, error) {\n\t\t\t\t\t\tassert.Equals(t, \"provID\", provisionerID)\n\t\t\t\t\t\tassert.Equals(t, \"key\", keyID)\n\t\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr:        err,\n\t\t\t\tstatusCode: 500,\n\t\t\t}\n\t\t},\n\t\t\"fail/reference-not-found-error\": func(t *testing.T) test {\n\t\t\tprov := &linkedca.Provisioner{\n\t\t\t\tId: \"provID\",\n\t\t\t}\n\t\t\tchiCtx := chi.NewRouteContext()\n\t\t\tchiCtx.URLParams.Add(\"reference\", \"ref\")\n\t\t\tctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)\n\t\t\tctx = linkedca.NewContextWithProvisioner(ctx, prov)\n\t\t\terr := admin.NewError(admin.ErrorNotFoundType, \"ACME External Account Key not found\")\n\t\t\terr.Message = \"ACME External Account Key not found\"\n\t\t\treturn test{\n\t\t\t\tctx: ctx,\n\t\t\t\tacmeDB: &acme.MockDB{\n\t\t\t\t\tMockGetExternalAccountKeyByReference: func(ctx context.Context, provisionerID, reference string) (*acme.ExternalAccountKey, error) {\n\t\t\t\t\t\tassert.Equals(t, \"provID\", provisionerID)\n\t\t\t\t\t\tassert.Equals(t, \"ref\", reference)\n\t\t\t\t\t\treturn nil, acme.ErrNotFound\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr:        err,\n\t\t\t\tstatusCode: 404,\n\t\t\t}\n\t\t},\n\t\t\"fail/reference-error\": func(t *testing.T) test {\n\t\t\tprov := &linkedca.Provisioner{\n\t\t\t\tId: \"provID\",\n\t\t\t}\n\t\t\tchiCtx := chi.NewRouteContext()\n\t\t\tchiCtx.URLParams.Add(\"reference\", \"ref\")\n\t\t\tctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)\n\t\t\tctx = linkedca.NewContextWithProvisioner(ctx, prov)\n\t\t\terr := admin.WrapErrorISE(errors.New(\"force\"), \"error retrieving ACME External Account Key\")\n\t\t\terr.Message = \"error retrieving ACME External Account Key: force\"\n\t\t\treturn test{\n\t\t\t\tctx: ctx,\n\t\t\t\tacmeDB: &acme.MockDB{\n\t\t\t\t\tMockGetExternalAccountKeyByReference: func(ctx context.Context, provisionerID, reference string) (*acme.ExternalAccountKey, error) {\n\t\t\t\t\t\tassert.Equals(t, \"provID\", provisionerID)\n\t\t\t\t\t\tassert.Equals(t, \"ref\", reference)\n\t\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr:        err,\n\t\t\t\tstatusCode: 500,\n\t\t\t}\n\t\t},\n\t\t\"fail/no-key\": func(t *testing.T) test {\n\t\t\tprov := &linkedca.Provisioner{\n\t\t\t\tId: \"provID\",\n\t\t\t}\n\t\t\tchiCtx := chi.NewRouteContext()\n\t\t\tchiCtx.URLParams.Add(\"reference\", \"ref\")\n\t\t\tctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)\n\t\t\tctx = linkedca.NewContextWithProvisioner(ctx, prov)\n\t\t\terr := admin.NewError(admin.ErrorNotFoundType, \"ACME External Account Key not found\")\n\t\t\terr.Message = \"ACME External Account Key not found\"\n\t\t\treturn test{\n\t\t\t\tctx: ctx,\n\t\t\t\tacmeDB: &acme.MockDB{\n\t\t\t\t\tMockGetExternalAccountKeyByReference: func(ctx context.Context, provisionerID, reference string) (*acme.ExternalAccountKey, error) {\n\t\t\t\t\t\tassert.Equals(t, \"provID\", provisionerID)\n\t\t\t\t\t\tassert.Equals(t, \"ref\", reference)\n\t\t\t\t\t\treturn nil, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr:        err,\n\t\t\t\tstatusCode: 404,\n\t\t\t}\n\t\t},\n\t\t\"ok/keyID\": func(t *testing.T) test {\n\t\t\tprov := &linkedca.Provisioner{\n\t\t\t\tId: \"provID\",\n\t\t\t}\n\t\t\tchiCtx := chi.NewRouteContext()\n\t\t\tchiCtx.URLParams.Add(\"keyID\", \"eakID\")\n\t\t\tctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)\n\t\t\tctx = linkedca.NewContextWithProvisioner(ctx, prov)\n\t\t\terr := admin.NewError(admin.ErrorNotFoundType, \"ACME External Account Key not found\")\n\t\t\terr.Message = \"ACME External Account Key not found\"\n\t\t\tcreatedAt := time.Now().Add(-1 * time.Hour)\n\t\t\tvar boundAt time.Time\n\t\t\teak := &acme.ExternalAccountKey{\n\t\t\t\tID:            \"eakID\",\n\t\t\t\tProvisionerID: \"provID\",\n\t\t\t\tCreatedAt:     createdAt,\n\t\t\t\tBoundAt:       boundAt,\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tctx: ctx,\n\t\t\t\tacmeDB: &acme.MockDB{\n\t\t\t\t\tMockGetExternalAccountKey: func(ctx context.Context, provisionerID, keyID string) (*acme.ExternalAccountKey, error) {\n\t\t\t\t\t\tassert.Equals(t, \"provID\", provisionerID)\n\t\t\t\t\t\tassert.Equals(t, \"eakID\", keyID)\n\t\t\t\t\t\treturn eak, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tnext: func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\t\tcontextEAK := linkedca.MustExternalAccountKeyFromContext(r.Context())\n\t\t\t\t\tassert.NotNil(t, eak)\n\t\t\t\t\texp := &linkedca.EABKey{\n\t\t\t\t\t\tId:          \"eakID\",\n\t\t\t\t\t\tProvisioner: \"provID\",\n\t\t\t\t\t\tCreatedAt:   timestamppb.New(createdAt),\n\t\t\t\t\t\tBoundAt:     timestamppb.New(boundAt),\n\t\t\t\t\t}\n\t\t\t\t\tassert.Equals(t, exp, contextEAK)\n\t\t\t\t\tw.Write(nil) // mock response with status 200\n\t\t\t\t},\n\t\t\t\terr:        nil,\n\t\t\t\tstatusCode: 200,\n\t\t\t}\n\t\t},\n\t\t\"ok/reference\": func(t *testing.T) test {\n\t\t\tprov := &linkedca.Provisioner{\n\t\t\t\tId: \"provID\",\n\t\t\t}\n\t\t\tchiCtx := chi.NewRouteContext()\n\t\t\tchiCtx.URLParams.Add(\"reference\", \"ref\")\n\t\t\tctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)\n\t\t\tctx = linkedca.NewContextWithProvisioner(ctx, prov)\n\t\t\terr := admin.NewError(admin.ErrorNotFoundType, \"ACME External Account Key not found\")\n\t\t\terr.Message = \"ACME External Account Key not found\"\n\t\t\tcreatedAt := time.Now().Add(-1 * time.Hour)\n\t\t\tvar boundAt time.Time\n\t\t\teak := &acme.ExternalAccountKey{\n\t\t\t\tID:            \"eakID\",\n\t\t\t\tProvisionerID: \"provID\",\n\t\t\t\tReference:     \"ref\",\n\t\t\t\tCreatedAt:     createdAt,\n\t\t\t\tBoundAt:       boundAt,\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tctx: ctx,\n\t\t\t\tacmeDB: &acme.MockDB{\n\t\t\t\t\tMockGetExternalAccountKeyByReference: func(ctx context.Context, provisionerID, reference string) (*acme.ExternalAccountKey, error) {\n\t\t\t\t\t\tassert.Equals(t, \"provID\", provisionerID)\n\t\t\t\t\t\tassert.Equals(t, \"ref\", reference)\n\t\t\t\t\t\treturn eak, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tnext: func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\t\tcontextEAK := linkedca.MustExternalAccountKeyFromContext(r.Context())\n\t\t\t\t\tassert.NotNil(t, eak)\n\t\t\t\t\texp := &linkedca.EABKey{\n\t\t\t\t\t\tId:          \"eakID\",\n\t\t\t\t\t\tProvisioner: \"provID\",\n\t\t\t\t\t\tReference:   \"ref\",\n\t\t\t\t\t\tCreatedAt:   timestamppb.New(createdAt),\n\t\t\t\t\t\tBoundAt:     timestamppb.New(boundAt),\n\t\t\t\t\t}\n\t\t\t\t\tassert.Equals(t, exp, contextEAK)\n\t\t\t\t\tw.Write(nil) // mock response with status 200\n\t\t\t\t},\n\t\t\t\terr:        nil,\n\t\t\t\tstatusCode: 200,\n\t\t\t}\n\t\t},\n\t}\n\n\tfor name, prep := range tests {\n\t\ttc := prep(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tctx := acme.NewDatabaseContext(tc.ctx, tc.acmeDB)\n\t\t\treq := httptest.NewRequest(\"GET\", \"/foo\", http.NoBody)\n\t\t\treq = req.WithContext(ctx)\n\t\t\tw := httptest.NewRecorder()\n\t\t\tloadExternalAccountKey(tc.next)(w, req)\n\t\t\tres := w.Result()\n\n\t\t\tassert.Equals(t, tc.statusCode, res.StatusCode)\n\n\t\t\tbody, err := io.ReadAll(res.Body)\n\t\t\tres.Body.Close()\n\t\t\tassert.FatalError(t, err)\n\n\t\t\tif res.StatusCode >= 400 {\n\t\t\t\terr := admin.Error{}\n\t\t\t\tassert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &err))\n\n\t\t\t\tassert.Equals(t, tc.err.Type, err.Type)\n\t\t\t\tassert.Equals(t, tc.err.Message, err.Message)\n\t\t\t\tassert.Equals(t, tc.err.StatusCode(), res.StatusCode)\n\t\t\t\tassert.Equals(t, tc.err.Detail, err.Detail)\n\t\t\t\tassert.Equals(t, []string{\"application/json\"}, res.Header[\"Content-Type\"])\n\t\t\t\treturn\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "authority/admin/api/policy.go",
    "content": "package api\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"net/http\"\n\n\t\"github.com/smallstep/linkedca\"\n\n\t\"github.com/smallstep/certificates/acme\"\n\t\"github.com/smallstep/certificates/api/read\"\n\t\"github.com/smallstep/certificates/api/render\"\n\t\"github.com/smallstep/certificates/authority\"\n\t\"github.com/smallstep/certificates/authority/admin\"\n\t\"github.com/smallstep/certificates/authority/policy\"\n)\n\n// PolicyAdminResponder is the interface responsible for writing ACME admin\n// responses.\ntype PolicyAdminResponder interface {\n\tGetAuthorityPolicy(w http.ResponseWriter, r *http.Request)\n\tCreateAuthorityPolicy(w http.ResponseWriter, r *http.Request)\n\tUpdateAuthorityPolicy(w http.ResponseWriter, r *http.Request)\n\tDeleteAuthorityPolicy(w http.ResponseWriter, r *http.Request)\n\tGetProvisionerPolicy(w http.ResponseWriter, r *http.Request)\n\tCreateProvisionerPolicy(w http.ResponseWriter, r *http.Request)\n\tUpdateProvisionerPolicy(w http.ResponseWriter, r *http.Request)\n\tDeleteProvisionerPolicy(w http.ResponseWriter, r *http.Request)\n\tGetACMEAccountPolicy(w http.ResponseWriter, r *http.Request)\n\tCreateACMEAccountPolicy(w http.ResponseWriter, r *http.Request)\n\tUpdateACMEAccountPolicy(w http.ResponseWriter, r *http.Request)\n\tDeleteACMEAccountPolicy(w http.ResponseWriter, r *http.Request)\n}\n\n// policyAdminResponder implements PolicyAdminResponder.\ntype policyAdminResponder struct{}\n\n// NewPolicyAdminResponder returns a new PolicyAdminResponder.\nfunc NewPolicyAdminResponder() PolicyAdminResponder {\n\treturn &policyAdminResponder{}\n}\n\n// GetAuthorityPolicy handles the GET /admin/authority/policy request\nfunc (par *policyAdminResponder) GetAuthorityPolicy(w http.ResponseWriter, r *http.Request) {\n\tctx := r.Context()\n\tif err := blockLinkedCA(ctx); err != nil {\n\t\trender.Error(w, r, err)\n\t\treturn\n\t}\n\n\tauth := mustAuthority(ctx)\n\tauthorityPolicy, err := auth.GetAuthorityPolicy(r.Context())\n\tvar ae *admin.Error\n\tif errors.As(err, &ae) && !ae.IsType(admin.ErrorNotFoundType) {\n\t\trender.Error(w, r, admin.WrapErrorISE(ae, \"error retrieving authority policy\"))\n\t\treturn\n\t}\n\n\tif authorityPolicy == nil {\n\t\trender.Error(w, r, admin.NewError(admin.ErrorNotFoundType, \"authority policy does not exist\"))\n\t\treturn\n\t}\n\n\trender.ProtoJSONStatus(w, authorityPolicy, http.StatusOK)\n}\n\n// CreateAuthorityPolicy handles the POST /admin/authority/policy request\nfunc (par *policyAdminResponder) CreateAuthorityPolicy(w http.ResponseWriter, r *http.Request) {\n\tctx := r.Context()\n\tif err := blockLinkedCA(ctx); err != nil {\n\t\trender.Error(w, r, err)\n\t\treturn\n\t}\n\n\tauth := mustAuthority(ctx)\n\tauthorityPolicy, err := auth.GetAuthorityPolicy(ctx)\n\n\tvar ae *admin.Error\n\tif errors.As(err, &ae) && !ae.IsType(admin.ErrorNotFoundType) {\n\t\trender.Error(w, r, admin.WrapErrorISE(err, \"error retrieving authority policy\"))\n\t\treturn\n\t}\n\n\tif authorityPolicy != nil {\n\t\tadminErr := admin.NewError(admin.ErrorConflictType, \"authority already has a policy\")\n\t\trender.Error(w, r, adminErr)\n\t\treturn\n\t}\n\n\tvar newPolicy = new(linkedca.Policy)\n\tif err := read.ProtoJSON(r.Body, newPolicy); err != nil {\n\t\trender.Error(w, r, err)\n\t\treturn\n\t}\n\n\tnewPolicy.Deduplicate()\n\n\tif err := validatePolicy(newPolicy); err != nil {\n\t\trender.Error(w, r, admin.WrapError(admin.ErrorBadRequestType, err, \"error validating authority policy\"))\n\t\treturn\n\t}\n\n\tadm := linkedca.MustAdminFromContext(ctx)\n\n\tvar createdPolicy *linkedca.Policy\n\tif createdPolicy, err = auth.CreateAuthorityPolicy(ctx, adm, newPolicy); err != nil {\n\t\tif isBadRequest(err) {\n\t\t\trender.Error(w, r, admin.WrapError(admin.ErrorBadRequestType, err, \"error storing authority policy\"))\n\t\t\treturn\n\t\t}\n\n\t\trender.Error(w, r, admin.WrapErrorISE(err, \"error storing authority policy\"))\n\t\treturn\n\t}\n\n\trender.ProtoJSONStatus(w, createdPolicy, http.StatusCreated)\n}\n\n// UpdateAuthorityPolicy handles the PUT /admin/authority/policy request\nfunc (par *policyAdminResponder) UpdateAuthorityPolicy(w http.ResponseWriter, r *http.Request) {\n\tctx := r.Context()\n\tif err := blockLinkedCA(ctx); err != nil {\n\t\trender.Error(w, r, err)\n\t\treturn\n\t}\n\n\tauth := mustAuthority(ctx)\n\tauthorityPolicy, err := auth.GetAuthorityPolicy(ctx)\n\n\tvar ae *admin.Error\n\tif errors.As(err, &ae) && !ae.IsType(admin.ErrorNotFoundType) {\n\t\trender.Error(w, r, admin.WrapErrorISE(err, \"error retrieving authority policy\"))\n\t\treturn\n\t}\n\n\tif authorityPolicy == nil {\n\t\trender.Error(w, r, admin.NewError(admin.ErrorNotFoundType, \"authority policy does not exist\"))\n\t\treturn\n\t}\n\n\tvar newPolicy = new(linkedca.Policy)\n\tif err := read.ProtoJSON(r.Body, newPolicy); err != nil {\n\t\trender.Error(w, r, err)\n\t\treturn\n\t}\n\n\tnewPolicy.Deduplicate()\n\n\tif err := validatePolicy(newPolicy); err != nil {\n\t\trender.Error(w, r, admin.WrapError(admin.ErrorBadRequestType, err, \"error validating authority policy\"))\n\t\treturn\n\t}\n\n\tadm := linkedca.MustAdminFromContext(ctx)\n\n\tvar updatedPolicy *linkedca.Policy\n\tif updatedPolicy, err = auth.UpdateAuthorityPolicy(ctx, adm, newPolicy); err != nil {\n\t\tif isBadRequest(err) {\n\t\t\trender.Error(w, r, admin.WrapError(admin.ErrorBadRequestType, err, \"error updating authority policy\"))\n\t\t\treturn\n\t\t}\n\n\t\trender.Error(w, r, admin.WrapErrorISE(err, \"error updating authority policy\"))\n\t\treturn\n\t}\n\n\trender.ProtoJSONStatus(w, updatedPolicy, http.StatusOK)\n}\n\n// DeleteAuthorityPolicy handles the DELETE /admin/authority/policy request\nfunc (par *policyAdminResponder) DeleteAuthorityPolicy(w http.ResponseWriter, r *http.Request) {\n\tctx := r.Context()\n\tif err := blockLinkedCA(ctx); err != nil {\n\t\trender.Error(w, r, err)\n\t\treturn\n\t}\n\n\tauth := mustAuthority(ctx)\n\tauthorityPolicy, err := auth.GetAuthorityPolicy(ctx)\n\n\tvar ae *admin.Error\n\tif errors.As(err, &ae) && !ae.IsType(admin.ErrorNotFoundType) {\n\t\trender.Error(w, r, admin.WrapErrorISE(ae, \"error retrieving authority policy\"))\n\t\treturn\n\t}\n\n\tif authorityPolicy == nil {\n\t\trender.Error(w, r, admin.NewError(admin.ErrorNotFoundType, \"authority policy does not exist\"))\n\t\treturn\n\t}\n\n\tif err := auth.RemoveAuthorityPolicy(ctx); err != nil {\n\t\trender.Error(w, r, admin.WrapErrorISE(err, \"error deleting authority policy\"))\n\t\treturn\n\t}\n\n\trender.JSONStatus(w, r, DeleteResponse{Status: \"ok\"}, http.StatusOK)\n}\n\n// GetProvisionerPolicy handles the GET /admin/provisioners/{name}/policy request\nfunc (par *policyAdminResponder) GetProvisionerPolicy(w http.ResponseWriter, r *http.Request) {\n\tctx := r.Context()\n\tif err := blockLinkedCA(ctx); err != nil {\n\t\trender.Error(w, r, err)\n\t\treturn\n\t}\n\n\tprov := linkedca.MustProvisionerFromContext(ctx)\n\tprovisionerPolicy := prov.GetPolicy()\n\tif provisionerPolicy == nil {\n\t\trender.Error(w, r, admin.NewError(admin.ErrorNotFoundType, \"provisioner policy does not exist\"))\n\t\treturn\n\t}\n\n\trender.ProtoJSONStatus(w, provisionerPolicy, http.StatusOK)\n}\n\n// CreateProvisionerPolicy handles the POST /admin/provisioners/{name}/policy request\nfunc (par *policyAdminResponder) CreateProvisionerPolicy(w http.ResponseWriter, r *http.Request) {\n\tctx := r.Context()\n\tif err := blockLinkedCA(ctx); err != nil {\n\t\trender.Error(w, r, err)\n\t\treturn\n\t}\n\n\tprov := linkedca.MustProvisionerFromContext(ctx)\n\tprovisionerPolicy := prov.GetPolicy()\n\tif provisionerPolicy != nil {\n\t\tadminErr := admin.NewError(admin.ErrorConflictType, \"provisioner %s already has a policy\", prov.Name)\n\t\trender.Error(w, r, adminErr)\n\t\treturn\n\t}\n\n\tvar newPolicy = new(linkedca.Policy)\n\tif err := read.ProtoJSON(r.Body, newPolicy); err != nil {\n\t\trender.Error(w, r, err)\n\t\treturn\n\t}\n\n\tnewPolicy.Deduplicate()\n\n\tif err := validatePolicy(newPolicy); err != nil {\n\t\trender.Error(w, r, admin.WrapError(admin.ErrorBadRequestType, err, \"error validating provisioner policy\"))\n\t\treturn\n\t}\n\n\tprov.Policy = newPolicy\n\tauth := mustAuthority(ctx)\n\tif err := auth.UpdateProvisioner(ctx, prov); err != nil {\n\t\tif isBadRequest(err) {\n\t\t\trender.Error(w, r, admin.WrapError(admin.ErrorBadRequestType, err, \"error creating provisioner policy\"))\n\t\t\treturn\n\t\t}\n\n\t\trender.Error(w, r, admin.WrapErrorISE(err, \"error creating provisioner policy\"))\n\t\treturn\n\t}\n\n\trender.ProtoJSONStatus(w, newPolicy, http.StatusCreated)\n}\n\n// UpdateProvisionerPolicy handles the PUT /admin/provisioners/{name}/policy request\nfunc (par *policyAdminResponder) UpdateProvisionerPolicy(w http.ResponseWriter, r *http.Request) {\n\tctx := r.Context()\n\tif err := blockLinkedCA(ctx); err != nil {\n\t\trender.Error(w, r, err)\n\t\treturn\n\t}\n\n\tprov := linkedca.MustProvisionerFromContext(ctx)\n\tprovisionerPolicy := prov.GetPolicy()\n\tif provisionerPolicy == nil {\n\t\trender.Error(w, r, admin.NewError(admin.ErrorNotFoundType, \"provisioner policy does not exist\"))\n\t\treturn\n\t}\n\n\tvar newPolicy = new(linkedca.Policy)\n\tif err := read.ProtoJSON(r.Body, newPolicy); err != nil {\n\t\trender.Error(w, r, err)\n\t\treturn\n\t}\n\n\tnewPolicy.Deduplicate()\n\n\tif err := validatePolicy(newPolicy); err != nil {\n\t\trender.Error(w, r, admin.WrapError(admin.ErrorBadRequestType, err, \"error validating provisioner policy\"))\n\t\treturn\n\t}\n\n\tprov.Policy = newPolicy\n\tauth := mustAuthority(ctx)\n\tif err := auth.UpdateProvisioner(ctx, prov); err != nil {\n\t\tif isBadRequest(err) {\n\t\t\trender.Error(w, r, admin.WrapError(admin.ErrorBadRequestType, err, \"error updating provisioner policy\"))\n\t\t\treturn\n\t\t}\n\n\t\trender.Error(w, r, admin.WrapErrorISE(err, \"error updating provisioner policy\"))\n\t\treturn\n\t}\n\n\trender.ProtoJSONStatus(w, newPolicy, http.StatusOK)\n}\n\n// DeleteProvisionerPolicy handles the DELETE /admin/provisioners/{name}/policy request\nfunc (par *policyAdminResponder) DeleteProvisionerPolicy(w http.ResponseWriter, r *http.Request) {\n\tctx := r.Context()\n\tif err := blockLinkedCA(ctx); err != nil {\n\t\trender.Error(w, r, err)\n\t\treturn\n\t}\n\n\tprov := linkedca.MustProvisionerFromContext(ctx)\n\tif prov.Policy == nil {\n\t\trender.Error(w, r, admin.NewError(admin.ErrorNotFoundType, \"provisioner policy does not exist\"))\n\t\treturn\n\t}\n\n\t// remove the policy\n\tprov.Policy = nil\n\n\tauth := mustAuthority(ctx)\n\tif err := auth.UpdateProvisioner(ctx, prov); err != nil {\n\t\trender.Error(w, r, admin.WrapErrorISE(err, \"error deleting provisioner policy\"))\n\t\treturn\n\t}\n\n\trender.JSONStatus(w, r, DeleteResponse{Status: \"ok\"}, http.StatusOK)\n}\n\nfunc (par *policyAdminResponder) GetACMEAccountPolicy(w http.ResponseWriter, r *http.Request) {\n\tctx := r.Context()\n\tif err := blockLinkedCA(ctx); err != nil {\n\t\trender.Error(w, r, err)\n\t\treturn\n\t}\n\n\teak := linkedca.MustExternalAccountKeyFromContext(ctx)\n\teakPolicy := eak.GetPolicy()\n\tif eakPolicy == nil {\n\t\trender.Error(w, r, admin.NewError(admin.ErrorNotFoundType, \"ACME EAK policy does not exist\"))\n\t\treturn\n\t}\n\n\trender.ProtoJSONStatus(w, eakPolicy, http.StatusOK)\n}\n\nfunc (par *policyAdminResponder) CreateACMEAccountPolicy(w http.ResponseWriter, r *http.Request) {\n\tctx := r.Context()\n\tif err := blockLinkedCA(ctx); err != nil {\n\t\trender.Error(w, r, err)\n\t\treturn\n\t}\n\n\tprov := linkedca.MustProvisionerFromContext(ctx)\n\teak := linkedca.MustExternalAccountKeyFromContext(ctx)\n\teakPolicy := eak.GetPolicy()\n\tif eakPolicy != nil {\n\t\tadminErr := admin.NewError(admin.ErrorConflictType, \"ACME EAK %s already has a policy\", eak.Id)\n\t\trender.Error(w, r, adminErr)\n\t\treturn\n\t}\n\n\tvar newPolicy = new(linkedca.Policy)\n\tif err := read.ProtoJSON(r.Body, newPolicy); err != nil {\n\t\trender.Error(w, r, err)\n\t\treturn\n\t}\n\n\tnewPolicy.Deduplicate()\n\n\tif err := validatePolicy(newPolicy); err != nil {\n\t\trender.Error(w, r, admin.WrapError(admin.ErrorBadRequestType, err, \"error validating ACME EAK policy\"))\n\t\treturn\n\t}\n\n\teak.Policy = newPolicy\n\n\tacmeEAK := linkedEAKToCertificates(eak)\n\tacmeDB := acme.MustDatabaseFromContext(ctx)\n\tif err := acmeDB.UpdateExternalAccountKey(ctx, prov.GetId(), acmeEAK); err != nil {\n\t\trender.Error(w, r, admin.WrapErrorISE(err, \"error creating ACME EAK policy\"))\n\t\treturn\n\t}\n\n\trender.ProtoJSONStatus(w, newPolicy, http.StatusCreated)\n}\n\nfunc (par *policyAdminResponder) UpdateACMEAccountPolicy(w http.ResponseWriter, r *http.Request) {\n\tctx := r.Context()\n\tif err := blockLinkedCA(ctx); err != nil {\n\t\trender.Error(w, r, err)\n\t\treturn\n\t}\n\n\tprov := linkedca.MustProvisionerFromContext(ctx)\n\teak := linkedca.MustExternalAccountKeyFromContext(ctx)\n\teakPolicy := eak.GetPolicy()\n\tif eakPolicy == nil {\n\t\trender.Error(w, r, admin.NewError(admin.ErrorNotFoundType, \"ACME EAK policy does not exist\"))\n\t\treturn\n\t}\n\n\tvar newPolicy = new(linkedca.Policy)\n\tif err := read.ProtoJSON(r.Body, newPolicy); err != nil {\n\t\trender.Error(w, r, err)\n\t\treturn\n\t}\n\n\tnewPolicy.Deduplicate()\n\n\tif err := validatePolicy(newPolicy); err != nil {\n\t\trender.Error(w, r, admin.WrapError(admin.ErrorBadRequestType, err, \"error validating ACME EAK policy\"))\n\t\treturn\n\t}\n\n\teak.Policy = newPolicy\n\tacmeEAK := linkedEAKToCertificates(eak)\n\tacmeDB := acme.MustDatabaseFromContext(ctx)\n\tif err := acmeDB.UpdateExternalAccountKey(ctx, prov.GetId(), acmeEAK); err != nil {\n\t\trender.Error(w, r, admin.WrapErrorISE(err, \"error updating ACME EAK policy\"))\n\t\treturn\n\t}\n\n\trender.ProtoJSONStatus(w, newPolicy, http.StatusOK)\n}\n\nfunc (par *policyAdminResponder) DeleteACMEAccountPolicy(w http.ResponseWriter, r *http.Request) {\n\tctx := r.Context()\n\tif err := blockLinkedCA(ctx); err != nil {\n\t\trender.Error(w, r, err)\n\t\treturn\n\t}\n\n\tprov := linkedca.MustProvisionerFromContext(ctx)\n\teak := linkedca.MustExternalAccountKeyFromContext(ctx)\n\teakPolicy := eak.GetPolicy()\n\tif eakPolicy == nil {\n\t\trender.Error(w, r, admin.NewError(admin.ErrorNotFoundType, \"ACME EAK policy does not exist\"))\n\t\treturn\n\t}\n\n\t// remove the policy\n\teak.Policy = nil\n\n\tacmeEAK := linkedEAKToCertificates(eak)\n\tacmeDB := acme.MustDatabaseFromContext(ctx)\n\tif err := acmeDB.UpdateExternalAccountKey(ctx, prov.GetId(), acmeEAK); err != nil {\n\t\trender.Error(w, r, admin.WrapErrorISE(err, \"error deleting ACME EAK policy\"))\n\t\treturn\n\t}\n\n\trender.JSONStatus(w, r, DeleteResponse{Status: \"ok\"}, http.StatusOK)\n}\n\n// blockLinkedCA blocks all API operations on linked deployments\nfunc blockLinkedCA(ctx context.Context) error {\n\t// temporary blocking linked deployments\n\tadminDB := admin.MustFromContext(ctx)\n\tif a, ok := adminDB.(interface{ IsLinkedCA() bool }); ok && a.IsLinkedCA() {\n\t\treturn admin.NewError(admin.ErrorNotImplementedType, \"policy operations not yet supported in linked deployments\")\n\t}\n\treturn nil\n}\n\n// isBadRequest checks if an error should result in a bad request error\n// returned to the client.\nfunc isBadRequest(err error) bool {\n\tvar pe *authority.PolicyError\n\tisPolicyError := errors.As(err, &pe)\n\treturn isPolicyError && (pe.Typ == authority.AdminLockOut || pe.Typ == authority.EvaluationFailure || pe.Typ == authority.ConfigurationFailure)\n}\n\nfunc validatePolicy(p *linkedca.Policy) error {\n\t// convert the policy; return early if nil\n\toptions := policy.LinkedToCertificates(p)\n\tif options == nil {\n\t\treturn nil\n\t}\n\n\tvar err error\n\n\t// Initialize a temporary x509 allow/deny policy engine\n\tif _, err = policy.NewX509PolicyEngine(options.GetX509Options()); err != nil {\n\t\treturn err\n\t}\n\n\t// Initialize a temporary SSH allow/deny policy engine for host certificates\n\tif _, err = policy.NewSSHHostPolicyEngine(options.GetSSHOptions()); err != nil {\n\t\treturn err\n\t}\n\n\t// Initialize a temporary SSH allow/deny policy engine for user certificates\n\tif _, err = policy.NewSSHUserPolicyEngine(options.GetSSHOptions()); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "authority/admin/api/policy_test.go",
    "content": "package api\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"google.golang.org/protobuf/encoding/protojson\"\n\n\t\"github.com/smallstep/linkedca\"\n\n\t\"github.com/smallstep/certificates/acme\"\n\t\"github.com/smallstep/certificates/authority\"\n\t\"github.com/smallstep/certificates/authority/admin\"\n)\n\ntype fakeLinkedCA struct {\n\tadmin.MockDB\n}\n\nfunc (f *fakeLinkedCA) IsLinkedCA() bool {\n\treturn true\n}\n\n// testAdminError is an error type that models the expected\n// error body returned.\ntype testAdminError struct {\n\tType    string `json:\"type\"`\n\tMessage string `json:\"message\"`\n\tDetail  string `json:\"detail\"`\n}\n\ntype testX509Policy struct {\n\tAllow              *testX509Names `json:\"allow,omitempty\"`\n\tDeny               *testX509Names `json:\"deny,omitempty\"`\n\tAllowWildcardNames bool           `json:\"allow_wildcard_names,omitempty\"`\n}\n\ntype testX509Names struct {\n\tCommonNames    []string `json:\"commonNames,omitempty\"`\n\tDNSDomains     []string `json:\"dns,omitempty\"`\n\tIPRanges       []string `json:\"ips,omitempty\"`\n\tEmailAddresses []string `json:\"emails,omitempty\"`\n\tURIDomains     []string `json:\"uris,omitempty\"`\n}\n\ntype testSSHPolicy struct {\n\tUser *testSSHUserPolicy `json:\"user,omitempty\"`\n\tHost *testSSHHostPolicy `json:\"host,omitempty\"`\n}\n\ntype testSSHHostPolicy struct {\n\tAllow *testSSHHostNames `json:\"allow,omitempty\"`\n\tDeny  *testSSHHostNames `json:\"deny,omitempty\"`\n}\n\ntype testSSHHostNames struct {\n\tDNSDomains []string `json:\"dns,omitempty\"`\n\tIPRanges   []string `json:\"ips,omitempty\"`\n\tPrincipals []string `json:\"principals,omitempty\"`\n}\n\ntype testSSHUserPolicy struct {\n\tAllow *testSSHUserNames `json:\"allow,omitempty\"`\n\tDeny  *testSSHUserNames `json:\"deny,omitempty\"`\n}\n\ntype testSSHUserNames struct {\n\tEmailAddresses []string `json:\"emails,omitempty\"`\n\tPrincipals     []string `json:\"principals,omitempty\"`\n}\n\n// testPolicyResponse models the Policy API JSON response\ntype testPolicyResponse struct {\n\tX509 *testX509Policy `json:\"x509,omitempty\"`\n\tSSH  *testSSHPolicy  `json:\"ssh,omitempty\"`\n}\n\nfunc TestPolicyAdminResponder_GetAuthorityPolicy(t *testing.T) {\n\ttype test struct {\n\t\tauth       adminAuthority\n\t\tadminDB    admin.DB\n\t\tctx        context.Context\n\t\terr        *admin.Error\n\t\tresponse   *testPolicyResponse\n\t\tstatusCode int\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/linkedca\": func(t *testing.T) test {\n\t\t\tctx := context.Background()\n\t\t\terr := admin.NewError(admin.ErrorNotImplementedType, \"policy operations not yet supported in linked deployments\")\n\t\t\terr.Message = \"policy operations not yet supported in linked deployments\"\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\tadminDB:    &fakeLinkedCA{},\n\t\t\t\terr:        err,\n\t\t\t\tstatusCode: 501,\n\t\t\t}\n\t\t},\n\t\t\"fail/auth.GetAuthorityPolicy-error\": func(t *testing.T) test {\n\t\t\tctx := context.Background()\n\t\t\terr := admin.WrapErrorISE(errors.New(\"force\"), \"error retrieving authority policy\")\n\t\t\terr.Message = \"error retrieving authority policy: force\"\n\t\t\treturn test{\n\t\t\t\tctx:     ctx,\n\t\t\t\tadminDB: &admin.MockDB{},\n\t\t\t\tauth: &mockAdminAuthority{\n\t\t\t\t\tMockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) {\n\t\t\t\t\t\treturn nil, admin.NewError(admin.ErrorServerInternalType, \"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr:        err,\n\t\t\t\tstatusCode: 500,\n\t\t\t}\n\t\t},\n\t\t\"fail/auth.GetAuthorityPolicy-not-found\": func(t *testing.T) test {\n\t\t\tctx := context.Background()\n\t\t\terr := admin.NewError(admin.ErrorNotFoundType, \"authority policy does not exist\")\n\t\t\terr.Message = \"authority policy does not exist\"\n\t\t\treturn test{\n\t\t\t\tctx:     ctx,\n\t\t\t\tadminDB: &admin.MockDB{},\n\t\t\t\tauth: &mockAdminAuthority{\n\t\t\t\t\tMockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) {\n\t\t\t\t\t\treturn nil, admin.NewError(admin.ErrorNotFoundType, \"not found\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr:        err,\n\t\t\t\tstatusCode: 404,\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tctx := context.Background()\n\t\t\tpolicy := &linkedca.Policy{\n\t\t\t\tX509: &linkedca.X509Policy{\n\t\t\t\t\tAllow: &linkedca.X509Names{\n\t\t\t\t\t\tDns:         []string{\"*.local\"},\n\t\t\t\t\t\tIps:         []string{\"10.0.0.0/16\"},\n\t\t\t\t\t\tEmails:      []string{\"@example.com\"},\n\t\t\t\t\t\tUris:        []string{\"example.com\"},\n\t\t\t\t\t\tCommonNames: []string{\"test\"},\n\t\t\t\t\t},\n\t\t\t\t\tDeny: &linkedca.X509Names{\n\t\t\t\t\t\tDns:         []string{\"bad.local\"},\n\t\t\t\t\t\tIps:         []string{\"10.0.0.30\"},\n\t\t\t\t\t\tEmails:      []string{\"bad@example.com\"},\n\t\t\t\t\t\tUris:        []string{\"notexample.com\"},\n\t\t\t\t\t\tCommonNames: []string{\"bad\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tSsh: &linkedca.SSHPolicy{\n\t\t\t\t\tUser: &linkedca.SSHUserPolicy{\n\t\t\t\t\t\tAllow: &linkedca.SSHUserNames{\n\t\t\t\t\t\t\tEmails:     []string{\"@example.com\"},\n\t\t\t\t\t\t\tPrincipals: []string{\"*\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tDeny: &linkedca.SSHUserNames{\n\t\t\t\t\t\t\tEmails:     []string{\"bad@example.com\"},\n\t\t\t\t\t\t\tPrincipals: []string{\"root\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tHost: &linkedca.SSHHostPolicy{\n\t\t\t\t\t\tAllow: &linkedca.SSHHostNames{\n\t\t\t\t\t\t\tDns:        []string{\"*.example.com\"},\n\t\t\t\t\t\t\tIps:        []string{\"10.10.0.0/16\"},\n\t\t\t\t\t\t\tPrincipals: []string{\"good\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tDeny: &linkedca.SSHHostNames{\n\t\t\t\t\t\t\tDns:        []string{\"bad@example.com\"},\n\t\t\t\t\t\t\tIps:        []string{\"10.10.0.30\"},\n\t\t\t\t\t\t\tPrincipals: []string{\"bad\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tctx:     ctx,\n\t\t\t\tadminDB: &admin.MockDB{},\n\t\t\t\tauth: &mockAdminAuthority{\n\t\t\t\t\tMockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) {\n\t\t\t\t\t\treturn policy, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tresponse: &testPolicyResponse{\n\t\t\t\t\tX509: &testX509Policy{\n\t\t\t\t\t\tAllow: &testX509Names{\n\t\t\t\t\t\t\tDNSDomains:     []string{\"*.local\"},\n\t\t\t\t\t\t\tIPRanges:       []string{\"10.0.0.0/16\"},\n\t\t\t\t\t\t\tEmailAddresses: []string{\"@example.com\"},\n\t\t\t\t\t\t\tURIDomains:     []string{\"example.com\"},\n\t\t\t\t\t\t\tCommonNames:    []string{\"test\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tDeny: &testX509Names{\n\t\t\t\t\t\t\tDNSDomains:     []string{\"bad.local\"},\n\t\t\t\t\t\t\tIPRanges:       []string{\"10.0.0.30\"},\n\t\t\t\t\t\t\tEmailAddresses: []string{\"bad@example.com\"},\n\t\t\t\t\t\t\tURIDomains:     []string{\"notexample.com\"},\n\t\t\t\t\t\t\tCommonNames:    []string{\"bad\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tSSH: &testSSHPolicy{\n\t\t\t\t\t\tUser: &testSSHUserPolicy{\n\t\t\t\t\t\t\tAllow: &testSSHUserNames{\n\t\t\t\t\t\t\t\tEmailAddresses: []string{\"@example.com\"},\n\t\t\t\t\t\t\t\tPrincipals:     []string{\"*\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tDeny: &testSSHUserNames{\n\t\t\t\t\t\t\t\tEmailAddresses: []string{\"bad@example.com\"},\n\t\t\t\t\t\t\t\tPrincipals:     []string{\"root\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tHost: &testSSHHostPolicy{\n\t\t\t\t\t\t\tAllow: &testSSHHostNames{\n\t\t\t\t\t\t\t\tDNSDomains: []string{\"*.example.com\"},\n\t\t\t\t\t\t\t\tIPRanges:   []string{\"10.10.0.0/16\"},\n\t\t\t\t\t\t\t\tPrincipals: []string{\"good\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tDeny: &testSSHHostNames{\n\t\t\t\t\t\t\t\tDNSDomains: []string{\"bad@example.com\"},\n\t\t\t\t\t\t\t\tIPRanges:   []string{\"10.10.0.30\"},\n\t\t\t\t\t\t\t\tPrincipals: []string{\"bad\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tstatusCode: 200,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, prep := range tests {\n\t\ttc := prep(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tmockMustAuthority(t, tc.auth)\n\t\t\tctx := admin.NewContext(tc.ctx, tc.adminDB)\n\t\t\tpar := NewPolicyAdminResponder()\n\n\t\t\treq := httptest.NewRequest(\"GET\", \"/foo\", http.NoBody)\n\t\t\treq = req.WithContext(ctx)\n\t\t\tw := httptest.NewRecorder()\n\n\t\t\tpar.GetAuthorityPolicy(w, req)\n\t\t\tres := w.Result()\n\n\t\t\tassert.Equal(t, tc.statusCode, res.StatusCode)\n\n\t\t\tif res.StatusCode >= 400 {\n\n\t\t\t\tbody, err := io.ReadAll(res.Body)\n\t\t\t\tres.Body.Close()\n\t\t\t\tassert.NoError(t, err)\n\n\t\t\t\tae := testAdminError{}\n\t\t\t\tassert.NoError(t, json.Unmarshal(bytes.TrimSpace(body), &ae))\n\n\t\t\t\tassert.Equal(t, tc.err.Type, ae.Type)\n\t\t\t\tassert.Equal(t, tc.err.Message, ae.Message)\n\t\t\t\tassert.Equal(t, tc.err.StatusCode(), res.StatusCode)\n\t\t\t\tassert.Equal(t, tc.err.Detail, ae.Detail)\n\t\t\t\tassert.Equal(t, []string{\"application/json\"}, res.Header[\"Content-Type\"])\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tp := &testPolicyResponse{}\n\t\t\tbody, err := io.ReadAll(res.Body)\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.NoError(t, json.Unmarshal(body, &p))\n\n\t\t\tassert.Equal(t, tc.response, p)\n\t\t})\n\t}\n}\n\nfunc TestPolicyAdminResponder_CreateAuthorityPolicy(t *testing.T) {\n\ttype test struct {\n\t\tauth       adminAuthority\n\t\tadminDB    admin.DB\n\t\tbody       []byte\n\t\tctx        context.Context\n\t\tacmeDB     acme.DB\n\t\terr        *admin.Error\n\t\tresponse   *testPolicyResponse\n\t\tstatusCode int\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/linkedca\": func(t *testing.T) test {\n\t\t\tctx := context.Background()\n\t\t\terr := admin.NewError(admin.ErrorNotImplementedType, \"policy operations not yet supported in linked deployments\")\n\t\t\terr.Message = \"policy operations not yet supported in linked deployments\"\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\tadminDB:    &fakeLinkedCA{},\n\t\t\t\terr:        err,\n\t\t\t\tstatusCode: 501,\n\t\t\t}\n\t\t},\n\t\t\"fail/auth.GetAuthorityPolicy-error\": func(t *testing.T) test {\n\t\t\tctx := context.Background()\n\t\t\terr := admin.WrapErrorISE(errors.New(\"force\"), \"error retrieving authority policy\")\n\t\t\terr.Message = \"error retrieving authority policy: force\"\n\t\t\treturn test{\n\t\t\t\tctx:     ctx,\n\t\t\t\tadminDB: &admin.MockDB{},\n\t\t\t\tauth: &mockAdminAuthority{\n\t\t\t\t\tMockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) {\n\t\t\t\t\t\treturn nil, admin.NewError(admin.ErrorServerInternalType, \"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr:        err,\n\t\t\t\tstatusCode: 500,\n\t\t\t}\n\t\t},\n\t\t\"fail/existing-policy\": func(t *testing.T) test {\n\t\t\tctx := context.Background()\n\t\t\terr := admin.NewError(admin.ErrorConflictType, \"authority already has a policy\")\n\t\t\terr.Message = \"authority already has a policy\"\n\t\t\treturn test{\n\t\t\t\tctx:     ctx,\n\t\t\t\tadminDB: &admin.MockDB{},\n\t\t\t\tauth: &mockAdminAuthority{\n\t\t\t\t\tMockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) {\n\t\t\t\t\t\treturn &linkedca.Policy{}, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr:        err,\n\t\t\t\tstatusCode: 409,\n\t\t\t}\n\t\t},\n\t\t\"fail/read.ProtoJSON\": func(t *testing.T) test {\n\t\t\tctx := context.Background()\n\t\t\tadminErr := admin.NewError(admin.ErrorBadRequestType, \"proto: syntax error (line 1:2): invalid value ?\")\n\t\t\tadminErr.Message = \"proto: syntax error (line 1:2): invalid value ?\"\n\t\t\tbody := []byte(\"{?}\")\n\t\t\treturn test{\n\t\t\t\tctx:     ctx,\n\t\t\t\tadminDB: &admin.MockDB{},\n\t\t\t\tauth: &mockAdminAuthority{\n\t\t\t\t\tMockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) {\n\t\t\t\t\t\treturn nil, admin.NewError(admin.ErrorNotFoundType, \"not found\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tbody:       body,\n\t\t\t\terr:        adminErr,\n\t\t\t\tstatusCode: 400,\n\t\t\t}\n\t\t},\n\t\t\"fail/validatePolicy\": func(t *testing.T) test {\n\t\t\tctx := context.Background()\n\t\t\tadminErr := admin.NewError(admin.ErrorBadRequestType, \"error validating authority policy: cannot parse permitted URI domain constraint \\\"https://example.com\\\": URI domain constraint \\\"https://example.com\\\" contains scheme (not supported yet)\")\n\t\t\tadminErr.Message = \"error validating authority policy: cannot parse permitted URI domain constraint \\\"https://example.com\\\": URI domain constraint \\\"https://example.com\\\" contains scheme (not supported yet)\"\n\t\t\tbody := []byte(`\n\t\t\t{\n\t\t\t\t\"x509\": {\n\t\t\t\t   \"allow\": {\n\t\t\t\t\t  \"uris\": [\n\t\t\t\t\t\t \t\"https://example.com\"\n\t\t\t\t\t\t]\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}`)\n\t\t\treturn test{\n\t\t\t\tctx:     ctx,\n\t\t\t\tadminDB: &admin.MockDB{},\n\t\t\t\tauth: &mockAdminAuthority{\n\t\t\t\t\tMockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) {\n\t\t\t\t\t\treturn nil, admin.NewError(admin.ErrorNotFoundType, \"not found\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tbody:       body,\n\t\t\t\terr:        adminErr,\n\t\t\t\tstatusCode: 400,\n\t\t\t}\n\t\t},\n\t\t\"fail/CreateAuthorityPolicy-policy-admin-lockout-error\": func(t *testing.T) test {\n\t\t\tadm := &linkedca.Admin{\n\t\t\t\tSubject: \"step\",\n\t\t\t}\n\t\t\tctx := context.Background()\n\t\t\tctx = linkedca.NewContextWithAdmin(ctx, adm)\n\t\t\tadminErr := admin.NewError(admin.ErrorBadRequestType, \"error storing authority policy\")\n\t\t\tadminErr.Message = \"error storing authority policy: admin lock out\"\n\t\t\tpolicy := &linkedca.Policy{\n\t\t\t\tX509: &linkedca.X509Policy{\n\t\t\t\t\tAllow: &linkedca.X509Names{\n\t\t\t\t\t\tDns: []string{\"*.local\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t\tbody, err := protojson.Marshal(policy)\n\t\t\tassert.NoError(t, err)\n\t\t\treturn test{\n\t\t\t\tctx: ctx,\n\t\t\t\tauth: &mockAdminAuthority{\n\t\t\t\t\tMockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) {\n\t\t\t\t\t\treturn nil, admin.NewError(admin.ErrorNotFoundType, \"not found\")\n\t\t\t\t\t},\n\t\t\t\t\tMockCreateAuthorityPolicy: func(ctx context.Context, adm *linkedca.Admin, policy *linkedca.Policy) (*linkedca.Policy, error) {\n\t\t\t\t\t\treturn nil, &authority.PolicyError{\n\t\t\t\t\t\t\tTyp: authority.AdminLockOut,\n\t\t\t\t\t\t\tErr: errors.New(\"admin lock out\"),\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tadminDB: &admin.MockDB{\n\t\t\t\t\tMockGetAdmins: func(ctx context.Context) ([]*linkedca.Admin, error) {\n\t\t\t\t\t\treturn []*linkedca.Admin{\n\t\t\t\t\t\t\tadm,\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tSubject: \"anotherAdmin\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t}, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tbody:       body,\n\t\t\t\terr:        adminErr,\n\t\t\t\tstatusCode: 400,\n\t\t\t}\n\t\t},\n\t\t\"fail/CreateAuthorityPolicy-error\": func(t *testing.T) test {\n\t\t\tadm := &linkedca.Admin{\n\t\t\t\tSubject: \"step\",\n\t\t\t}\n\t\t\tctx := context.Background()\n\t\t\tctx = linkedca.NewContextWithAdmin(ctx, adm)\n\t\t\tadminErr := admin.NewError(admin.ErrorServerInternalType, \"error storing authority policy: force\")\n\t\t\tadminErr.Message = \"error storing authority policy: force\"\n\t\t\tpolicy := &linkedca.Policy{\n\t\t\t\tX509: &linkedca.X509Policy{\n\t\t\t\t\tAllow: &linkedca.X509Names{\n\t\t\t\t\t\tDns: []string{\"*.local\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t\tbody, err := protojson.Marshal(policy)\n\t\t\tassert.NoError(t, err)\n\t\t\treturn test{\n\t\t\t\tctx: ctx,\n\t\t\t\tauth: &mockAdminAuthority{\n\t\t\t\t\tMockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) {\n\t\t\t\t\t\treturn nil, admin.NewError(admin.ErrorNotFoundType, \"not found\")\n\t\t\t\t\t},\n\t\t\t\t\tMockCreateAuthorityPolicy: func(ctx context.Context, adm *linkedca.Admin, policy *linkedca.Policy) (*linkedca.Policy, error) {\n\t\t\t\t\t\treturn nil, &authority.PolicyError{\n\t\t\t\t\t\t\tTyp: authority.StoreFailure,\n\t\t\t\t\t\t\tErr: errors.New(\"force\"),\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tadminDB: &admin.MockDB{\n\t\t\t\t\tMockGetAdmins: func(ctx context.Context) ([]*linkedca.Admin, error) {\n\t\t\t\t\t\treturn []*linkedca.Admin{\n\t\t\t\t\t\t\tadm,\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tSubject: \"anotherAdmin\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t}, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tbody:       body,\n\t\t\t\terr:        adminErr,\n\t\t\t\tstatusCode: 500,\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tadm := &linkedca.Admin{\n\t\t\t\tSubject: \"step\",\n\t\t\t}\n\t\t\tctx := context.Background()\n\t\t\tctx = linkedca.NewContextWithAdmin(ctx, adm)\n\t\t\tpolicy := &linkedca.Policy{\n\t\t\t\tX509: &linkedca.X509Policy{\n\t\t\t\t\tAllow: &linkedca.X509Names{\n\t\t\t\t\t\tDns: []string{\"*.local\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t\tbody, err := protojson.Marshal(policy)\n\t\t\tassert.NoError(t, err)\n\t\t\treturn test{\n\t\t\t\tctx: ctx,\n\t\t\t\tauth: &mockAdminAuthority{\n\t\t\t\t\tMockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) {\n\t\t\t\t\t\treturn nil, admin.NewError(admin.ErrorNotFoundType, \"not found\")\n\t\t\t\t\t},\n\t\t\t\t\tMockCreateAuthorityPolicy: func(ctx context.Context, adm *linkedca.Admin, policy *linkedca.Policy) (*linkedca.Policy, error) {\n\t\t\t\t\t\treturn policy, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tadminDB: &admin.MockDB{\n\t\t\t\t\tMockGetAdmins: func(ctx context.Context) ([]*linkedca.Admin, error) {\n\t\t\t\t\t\treturn []*linkedca.Admin{\n\t\t\t\t\t\t\tadm,\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tSubject: \"anotherAdmin\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t}, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tbody: body,\n\t\t\t\tresponse: &testPolicyResponse{\n\t\t\t\t\tX509: &testX509Policy{\n\t\t\t\t\t\tAllow: &testX509Names{\n\t\t\t\t\t\t\tDNSDomains: []string{\"*.local\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tstatusCode: 201,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, prep := range tests {\n\t\ttc := prep(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tmockMustAuthority(t, tc.auth)\n\t\t\tctx := admin.NewContext(tc.ctx, tc.adminDB)\n\t\t\tctx = acme.NewDatabaseContext(ctx, tc.acmeDB)\n\t\t\tpar := NewPolicyAdminResponder()\n\n\t\t\treq := httptest.NewRequest(\"POST\", \"/foo\", io.NopCloser(bytes.NewBuffer(tc.body)))\n\t\t\treq = req.WithContext(ctx)\n\t\t\tw := httptest.NewRecorder()\n\n\t\t\tpar.CreateAuthorityPolicy(w, req)\n\t\t\tres := w.Result()\n\n\t\t\tassert.Equal(t, tc.statusCode, res.StatusCode)\n\n\t\t\tif res.StatusCode >= 400 {\n\n\t\t\t\tbody, err := io.ReadAll(res.Body)\n\t\t\t\tres.Body.Close()\n\t\t\t\tassert.NoError(t, err)\n\n\t\t\t\tae := testAdminError{}\n\t\t\t\tassert.NoError(t, json.Unmarshal(bytes.TrimSpace(body), &ae))\n\n\t\t\t\tassert.Equal(t, tc.err.Type, ae.Type)\n\t\t\t\tassert.Equal(t, tc.err.StatusCode(), res.StatusCode)\n\t\t\t\tassert.Equal(t, tc.err.Detail, ae.Detail)\n\t\t\t\tassert.Equal(t, []string{\"application/json\"}, res.Header[\"Content-Type\"])\n\n\t\t\t\t// when the error message starts with \"proto\", we expect it to have\n\t\t\t\t// a syntax error (in the tests). If the message doesn't start with \"proto\",\n\t\t\t\t// we expect a full string match.\n\t\t\t\tif strings.HasPrefix(tc.err.Message, \"proto:\") {\n\t\t\t\t\tassert.True(t, strings.Contains(ae.Message, \"syntax error\"))\n\t\t\t\t} else {\n\t\t\t\t\tassert.Equal(t, tc.err.Message, ae.Message)\n\t\t\t\t}\n\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tp := &testPolicyResponse{}\n\t\t\tbody, err := io.ReadAll(res.Body)\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.NoError(t, json.Unmarshal(body, &p))\n\n\t\t\tassert.Equal(t, tc.response, p)\n\n\t\t})\n\t}\n}\n\nfunc TestPolicyAdminResponder_UpdateAuthorityPolicy(t *testing.T) {\n\ttype test struct {\n\t\tauth       adminAuthority\n\t\tadminDB    admin.DB\n\t\tbody       []byte\n\t\tctx        context.Context\n\t\tacmeDB     acme.DB\n\t\terr        *admin.Error\n\t\tresponse   *testPolicyResponse\n\t\tstatusCode int\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/linkedca\": func(t *testing.T) test {\n\t\t\tctx := context.Background()\n\t\t\terr := admin.NewError(admin.ErrorNotImplementedType, \"policy operations not yet supported in linked deployments\")\n\t\t\terr.Message = \"policy operations not yet supported in linked deployments\"\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\tadminDB:    &fakeLinkedCA{},\n\t\t\t\terr:        err,\n\t\t\t\tstatusCode: 501,\n\t\t\t}\n\t\t},\n\t\t\"fail/auth.GetAuthorityPolicy-error\": func(t *testing.T) test {\n\t\t\tctx := context.Background()\n\t\t\terr := admin.WrapErrorISE(errors.New(\"force\"), \"error retrieving authority policy\")\n\t\t\terr.Message = \"error retrieving authority policy: force\"\n\t\t\treturn test{\n\t\t\t\tctx:     ctx,\n\t\t\t\tadminDB: &admin.MockDB{},\n\t\t\t\tauth: &mockAdminAuthority{\n\t\t\t\t\tMockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) {\n\t\t\t\t\t\treturn nil, admin.NewError(admin.ErrorServerInternalType, \"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr:        err,\n\t\t\t\tstatusCode: 500,\n\t\t\t}\n\t\t},\n\t\t\"fail/no-existing-policy\": func(t *testing.T) test {\n\t\t\tctx := context.Background()\n\t\t\terr := admin.NewError(admin.ErrorNotFoundType, \"authority policy does not exist\")\n\t\t\terr.Message = \"authority policy does not exist\"\n\t\t\terr.Status = http.StatusNotFound\n\t\t\treturn test{\n\t\t\t\tctx:     ctx,\n\t\t\t\tadminDB: &admin.MockDB{},\n\t\t\t\tauth: &mockAdminAuthority{\n\t\t\t\t\tMockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) {\n\t\t\t\t\t\treturn nil, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr:        err,\n\t\t\t\tstatusCode: 404,\n\t\t\t}\n\t\t},\n\t\t\"fail/read.ProtoJSON\": func(t *testing.T) test {\n\t\t\tpolicy := &linkedca.Policy{\n\t\t\t\tX509: &linkedca.X509Policy{\n\t\t\t\t\tAllow: &linkedca.X509Names{\n\t\t\t\t\t\tDns: []string{\"*.local\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t\tctx := context.Background()\n\t\t\tadminErr := admin.NewError(admin.ErrorBadRequestType, \"proto: syntax error (line 1:2): invalid value ?\")\n\t\t\tadminErr.Message = \"proto: syntax error (line 1:2): invalid value ?\"\n\t\t\tbody := []byte(\"{?}\")\n\t\t\treturn test{\n\t\t\t\tctx:     ctx,\n\t\t\t\tadminDB: &admin.MockDB{},\n\t\t\t\tauth: &mockAdminAuthority{\n\t\t\t\t\tMockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) {\n\t\t\t\t\t\treturn policy, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tbody:       body,\n\t\t\t\terr:        adminErr,\n\t\t\t\tstatusCode: 400,\n\t\t\t}\n\t\t},\n\t\t\"fail/validatePolicy\": func(t *testing.T) test {\n\t\t\tpolicy := &linkedca.Policy{\n\t\t\t\tX509: &linkedca.X509Policy{\n\t\t\t\t\tAllow: &linkedca.X509Names{\n\t\t\t\t\t\tDns: []string{\"*.local\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t\tctx := context.Background()\n\t\t\tadminErr := admin.NewError(admin.ErrorBadRequestType, \"error validating authority policy: cannot parse permitted URI domain constraint \\\"https://example.com\\\": URI domain constraint \\\"https://example.com\\\" contains scheme (not supported yet)\")\n\t\t\tadminErr.Message = \"error validating authority policy: cannot parse permitted URI domain constraint \\\"https://example.com\\\": URI domain constraint \\\"https://example.com\\\" contains scheme (not supported yet)\"\n\t\t\tbody := []byte(`\n\t\t\t{\n\t\t\t\t\"x509\": {\n\t\t\t\t   \"allow\": {\n\t\t\t\t\t  \"uris\": [\n\t\t\t\t\t\t \t\"https://example.com\"\n\t\t\t\t\t\t]\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}`)\n\t\t\treturn test{\n\t\t\t\tctx:     ctx,\n\t\t\t\tadminDB: &admin.MockDB{},\n\t\t\t\tauth: &mockAdminAuthority{\n\t\t\t\t\tMockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) {\n\t\t\t\t\t\treturn policy, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tbody:       body,\n\t\t\t\terr:        adminErr,\n\t\t\t\tstatusCode: 400,\n\t\t\t}\n\t\t},\n\t\t\"fail/UpdateAuthorityPolicy-policy-admin-lockout-error\": func(t *testing.T) test {\n\t\t\tadm := &linkedca.Admin{\n\t\t\t\tSubject: \"step\",\n\t\t\t}\n\t\t\tctx := context.Background()\n\t\t\tctx = linkedca.NewContextWithAdmin(ctx, adm)\n\t\t\tadminErr := admin.NewError(admin.ErrorBadRequestType, \"error updating authority policy: force\")\n\t\t\tadminErr.Message = \"error updating authority policy: admin lock out\"\n\t\t\tpolicy := &linkedca.Policy{\n\t\t\t\tX509: &linkedca.X509Policy{\n\t\t\t\t\tAllow: &linkedca.X509Names{\n\t\t\t\t\t\tDns: []string{\"*.local\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t\tbody, err := protojson.Marshal(policy)\n\t\t\tassert.NoError(t, err)\n\t\t\treturn test{\n\t\t\t\tctx: ctx,\n\t\t\t\tauth: &mockAdminAuthority{\n\t\t\t\t\tMockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) {\n\t\t\t\t\t\treturn policy, nil\n\t\t\t\t\t},\n\t\t\t\t\tMockUpdateAuthorityPolicy: func(ctx context.Context, adm *linkedca.Admin, policy *linkedca.Policy) (*linkedca.Policy, error) {\n\t\t\t\t\t\treturn nil, &authority.PolicyError{\n\t\t\t\t\t\t\tTyp: authority.AdminLockOut,\n\t\t\t\t\t\t\tErr: errors.New(\"admin lock out\"),\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tadminDB: &admin.MockDB{\n\t\t\t\t\tMockGetAdmins: func(ctx context.Context) ([]*linkedca.Admin, error) {\n\t\t\t\t\t\treturn []*linkedca.Admin{\n\t\t\t\t\t\t\tadm,\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tSubject: \"anotherAdmin\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t}, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tbody:       body,\n\t\t\t\terr:        adminErr,\n\t\t\t\tstatusCode: 400,\n\t\t\t}\n\t\t},\n\t\t\"fail/UpdateAuthorityPolicy-error\": func(t *testing.T) test {\n\t\t\tadm := &linkedca.Admin{\n\t\t\t\tSubject: \"step\",\n\t\t\t}\n\t\t\tctx := context.Background()\n\t\t\tctx = linkedca.NewContextWithAdmin(ctx, adm)\n\t\t\tadminErr := admin.NewError(admin.ErrorServerInternalType, \"error updating authority policy: force\")\n\t\t\tadminErr.Message = \"error updating authority policy: force\"\n\t\t\tpolicy := &linkedca.Policy{\n\t\t\t\tX509: &linkedca.X509Policy{\n\t\t\t\t\tAllow: &linkedca.X509Names{\n\t\t\t\t\t\tDns: []string{\"*.local\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t\tbody, err := protojson.Marshal(policy)\n\t\t\tassert.NoError(t, err)\n\t\t\treturn test{\n\t\t\t\tctx: ctx,\n\t\t\t\tauth: &mockAdminAuthority{\n\t\t\t\t\tMockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) {\n\t\t\t\t\t\treturn policy, nil\n\t\t\t\t\t},\n\t\t\t\t\tMockUpdateAuthorityPolicy: func(ctx context.Context, adm *linkedca.Admin, policy *linkedca.Policy) (*linkedca.Policy, error) {\n\t\t\t\t\t\treturn nil, &authority.PolicyError{\n\t\t\t\t\t\t\tTyp: authority.StoreFailure,\n\t\t\t\t\t\t\tErr: errors.New(\"force\"),\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tadminDB: &admin.MockDB{\n\t\t\t\t\tMockGetAdmins: func(ctx context.Context) ([]*linkedca.Admin, error) {\n\t\t\t\t\t\treturn []*linkedca.Admin{\n\t\t\t\t\t\t\tadm,\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tSubject: \"anotherAdmin\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t}, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tbody:       body,\n\t\t\t\terr:        adminErr,\n\t\t\t\tstatusCode: 500,\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tadm := &linkedca.Admin{\n\t\t\t\tSubject: \"step\",\n\t\t\t}\n\t\t\tctx := context.Background()\n\t\t\tctx = linkedca.NewContextWithAdmin(ctx, adm)\n\t\t\tpolicy := &linkedca.Policy{\n\t\t\t\tX509: &linkedca.X509Policy{\n\t\t\t\t\tAllow: &linkedca.X509Names{\n\t\t\t\t\t\tDns: []string{\"*.local\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t\tbody, err := protojson.Marshal(policy)\n\t\t\tassert.NoError(t, err)\n\t\t\treturn test{\n\t\t\t\tctx: ctx,\n\t\t\t\tauth: &mockAdminAuthority{\n\t\t\t\t\tMockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) {\n\t\t\t\t\t\treturn policy, nil\n\t\t\t\t\t},\n\t\t\t\t\tMockUpdateAuthorityPolicy: func(ctx context.Context, adm *linkedca.Admin, policy *linkedca.Policy) (*linkedca.Policy, error) {\n\t\t\t\t\t\treturn policy, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tadminDB: &admin.MockDB{\n\t\t\t\t\tMockGetAdmins: func(ctx context.Context) ([]*linkedca.Admin, error) {\n\t\t\t\t\t\treturn []*linkedca.Admin{\n\t\t\t\t\t\t\tadm,\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tSubject: \"anotherAdmin\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t}, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tbody: body,\n\t\t\t\tresponse: &testPolicyResponse{\n\t\t\t\t\tX509: &testX509Policy{\n\t\t\t\t\t\tAllow: &testX509Names{\n\t\t\t\t\t\t\tDNSDomains: []string{\"*.local\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tstatusCode: 200,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, prep := range tests {\n\t\ttc := prep(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tmockMustAuthority(t, tc.auth)\n\t\t\tctx := admin.NewContext(tc.ctx, tc.adminDB)\n\t\t\tctx = acme.NewDatabaseContext(ctx, tc.acmeDB)\n\t\t\tpar := NewPolicyAdminResponder()\n\n\t\t\treq := httptest.NewRequest(\"POST\", \"/foo\", io.NopCloser(bytes.NewBuffer(tc.body)))\n\t\t\treq = req.WithContext(ctx)\n\t\t\tw := httptest.NewRecorder()\n\n\t\t\tpar.UpdateAuthorityPolicy(w, req)\n\t\t\tres := w.Result()\n\n\t\t\tassert.Equal(t, tc.statusCode, res.StatusCode)\n\n\t\t\tif res.StatusCode >= 400 {\n\n\t\t\t\tbody, err := io.ReadAll(res.Body)\n\t\t\t\tres.Body.Close()\n\t\t\t\tassert.NoError(t, err)\n\n\t\t\t\tae := testAdminError{}\n\t\t\t\tassert.NoError(t, json.Unmarshal(bytes.TrimSpace(body), &ae))\n\n\t\t\t\tassert.Equal(t, tc.err.Type, ae.Type)\n\t\t\t\tassert.Equal(t, tc.err.StatusCode(), res.StatusCode)\n\t\t\t\tassert.Equal(t, tc.err.Detail, ae.Detail)\n\t\t\t\tassert.Equal(t, []string{\"application/json\"}, res.Header[\"Content-Type\"])\n\n\t\t\t\t// when the error message starts with \"proto\", we expect it to have\n\t\t\t\t// a syntax error (in the tests). If the message doesn't start with \"proto\",\n\t\t\t\t// we expect a full string match.\n\t\t\t\tif strings.HasPrefix(tc.err.Message, \"proto:\") {\n\t\t\t\t\tassert.True(t, strings.Contains(ae.Message, \"syntax error\"))\n\t\t\t\t} else {\n\t\t\t\t\tassert.Equal(t, tc.err.Message, ae.Message)\n\t\t\t\t}\n\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tp := &testPolicyResponse{}\n\t\t\tbody, err := io.ReadAll(res.Body)\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.NoError(t, json.Unmarshal(body, &p))\n\n\t\t\tassert.Equal(t, tc.response, p)\n\n\t\t})\n\t}\n}\n\nfunc TestPolicyAdminResponder_DeleteAuthorityPolicy(t *testing.T) {\n\ttype test struct {\n\t\tauth       adminAuthority\n\t\tadminDB    admin.DB\n\t\tbody       []byte\n\t\tctx        context.Context\n\t\tacmeDB     acme.DB\n\t\terr        *admin.Error\n\t\tstatusCode int\n\t}\n\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/linkedca\": func(t *testing.T) test {\n\t\t\tctx := context.Background()\n\t\t\terr := admin.NewError(admin.ErrorNotImplementedType, \"policy operations not yet supported in linked deployments\")\n\t\t\terr.Message = \"policy operations not yet supported in linked deployments\"\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\tadminDB:    &fakeLinkedCA{},\n\t\t\t\terr:        err,\n\t\t\t\tstatusCode: 501,\n\t\t\t}\n\t\t},\n\t\t\"fail/auth.GetAuthorityPolicy-error\": func(t *testing.T) test {\n\t\t\tctx := context.Background()\n\t\t\terr := admin.WrapErrorISE(errors.New(\"force\"), \"error retrieving authority policy\")\n\t\t\terr.Message = \"error retrieving authority policy: force\"\n\t\t\treturn test{\n\t\t\t\tctx:     ctx,\n\t\t\t\tadminDB: &admin.MockDB{},\n\t\t\t\tauth: &mockAdminAuthority{\n\t\t\t\t\tMockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) {\n\t\t\t\t\t\treturn nil, admin.NewError(admin.ErrorServerInternalType, \"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr:        err,\n\t\t\t\tstatusCode: 500,\n\t\t\t}\n\t\t},\n\t\t\"fail/no-existing-policy\": func(t *testing.T) test {\n\t\t\tctx := context.Background()\n\t\t\terr := admin.NewError(admin.ErrorNotFoundType, \"authority policy does not exist\")\n\t\t\terr.Message = \"authority policy does not exist\"\n\t\t\terr.Status = http.StatusNotFound\n\t\t\treturn test{\n\t\t\t\tctx:     ctx,\n\t\t\t\tadminDB: &admin.MockDB{},\n\t\t\t\tauth: &mockAdminAuthority{\n\t\t\t\t\tMockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) {\n\t\t\t\t\t\treturn nil, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr:        err,\n\t\t\t\tstatusCode: 404,\n\t\t\t}\n\t\t},\n\t\t\"fail/auth.RemoveAuthorityPolicy-error\": func(t *testing.T) test {\n\t\t\tpolicy := &linkedca.Policy{\n\t\t\t\tX509: &linkedca.X509Policy{\n\t\t\t\t\tAllow: &linkedca.X509Names{\n\t\t\t\t\t\tDns: []string{\"*.local\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t\tctx := context.Background()\n\t\t\terr := admin.NewErrorISE(\"error deleting authority policy: force\")\n\t\t\terr.Message = \"error deleting authority policy: force\"\n\t\t\treturn test{\n\t\t\t\tctx:     ctx,\n\t\t\t\tadminDB: &admin.MockDB{},\n\t\t\t\tauth: &mockAdminAuthority{\n\t\t\t\t\tMockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) {\n\t\t\t\t\t\treturn policy, nil\n\t\t\t\t\t},\n\t\t\t\t\tMockRemoveAuthorityPolicy: func(ctx context.Context) error {\n\t\t\t\t\t\treturn errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr:        err,\n\t\t\t\tstatusCode: 500,\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tpolicy := &linkedca.Policy{\n\t\t\t\tX509: &linkedca.X509Policy{\n\t\t\t\t\tAllow: &linkedca.X509Names{\n\t\t\t\t\t\tDns: []string{\"*.local\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t\tctx := context.Background()\n\t\t\treturn test{\n\t\t\t\tctx:     ctx,\n\t\t\t\tadminDB: &admin.MockDB{},\n\t\t\t\tauth: &mockAdminAuthority{\n\t\t\t\t\tMockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) {\n\t\t\t\t\t\treturn policy, nil\n\t\t\t\t\t},\n\t\t\t\t\tMockRemoveAuthorityPolicy: func(ctx context.Context) error {\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tstatusCode: 200,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, prep := range tests {\n\t\ttc := prep(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tmockMustAuthority(t, tc.auth)\n\t\t\tctx := admin.NewContext(tc.ctx, tc.adminDB)\n\t\t\tctx = acme.NewDatabaseContext(ctx, tc.acmeDB)\n\t\t\tpar := NewPolicyAdminResponder()\n\n\t\t\treq := httptest.NewRequest(\"POST\", \"/foo\", io.NopCloser(bytes.NewBuffer(tc.body)))\n\t\t\treq = req.WithContext(ctx)\n\t\t\tw := httptest.NewRecorder()\n\n\t\t\tpar.DeleteAuthorityPolicy(w, req)\n\t\t\tres := w.Result()\n\n\t\t\tassert.Equal(t, tc.statusCode, res.StatusCode)\n\n\t\t\tif res.StatusCode >= 400 {\n\n\t\t\t\tbody, err := io.ReadAll(res.Body)\n\t\t\t\tres.Body.Close()\n\t\t\t\tassert.NoError(t, err)\n\n\t\t\t\tae := testAdminError{}\n\t\t\t\tassert.NoError(t, json.Unmarshal(bytes.TrimSpace(body), &ae))\n\n\t\t\t\tassert.Equal(t, tc.err.Type, ae.Type)\n\t\t\t\tassert.Equal(t, tc.err.Message, ae.Message)\n\t\t\t\tassert.Equal(t, tc.err.StatusCode(), res.StatusCode)\n\t\t\t\tassert.Equal(t, tc.err.Detail, ae.Detail)\n\t\t\t\tassert.Equal(t, []string{\"application/json\"}, res.Header[\"Content-Type\"])\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tbody, err := io.ReadAll(res.Body)\n\t\t\tassert.NoError(t, err)\n\t\t\tres.Body.Close()\n\t\t\tresponse := DeleteResponse{}\n\t\t\tassert.NoError(t, json.Unmarshal(bytes.TrimSpace(body), &response))\n\t\t\tassert.Equal(t, \"ok\", response.Status)\n\t\t\tassert.Equal(t, []string{\"application/json\"}, res.Header[\"Content-Type\"])\n\n\t\t})\n\t}\n}\n\nfunc TestPolicyAdminResponder_GetProvisionerPolicy(t *testing.T) {\n\ttype test struct {\n\t\tauth       adminAuthority\n\t\tadminDB    admin.DB\n\t\tctx        context.Context\n\t\tacmeDB     acme.DB\n\t\terr        *admin.Error\n\t\tresponse   *testPolicyResponse\n\t\tstatusCode int\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/linkedca\": func(t *testing.T) test {\n\t\t\tctx := context.Background()\n\t\t\terr := admin.NewError(admin.ErrorNotImplementedType, \"policy operations not yet supported in linked deployments\")\n\t\t\terr.Message = \"policy operations not yet supported in linked deployments\"\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\tadminDB:    &fakeLinkedCA{},\n\t\t\t\terr:        err,\n\t\t\t\tstatusCode: 501,\n\t\t\t}\n\t\t},\n\t\t\"fail/prov-no-policy\": func(t *testing.T) test {\n\t\t\tprov := &linkedca.Provisioner{}\n\t\t\tctx := linkedca.NewContextWithProvisioner(context.Background(), prov)\n\t\t\terr := admin.NewError(admin.ErrorNotFoundType, \"provisioner policy does not exist\")\n\t\t\terr.Message = \"provisioner policy does not exist\"\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\tadminDB:    &admin.MockDB{},\n\t\t\t\terr:        err,\n\t\t\t\tstatusCode: 404,\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tpolicy := &linkedca.Policy{\n\t\t\t\tX509: &linkedca.X509Policy{\n\t\t\t\t\tAllow: &linkedca.X509Names{\n\t\t\t\t\t\tDns:         []string{\"*.local\"},\n\t\t\t\t\t\tIps:         []string{\"10.0.0.0/16\"},\n\t\t\t\t\t\tEmails:      []string{\"@example.com\"},\n\t\t\t\t\t\tUris:        []string{\"example.com\"},\n\t\t\t\t\t\tCommonNames: []string{\"test\"},\n\t\t\t\t\t},\n\t\t\t\t\tDeny: &linkedca.X509Names{\n\t\t\t\t\t\tDns:         []string{\"bad.local\"},\n\t\t\t\t\t\tIps:         []string{\"10.0.0.30\"},\n\t\t\t\t\t\tEmails:      []string{\"bad@example.com\"},\n\t\t\t\t\t\tUris:        []string{\"notexample.com\"},\n\t\t\t\t\t\tCommonNames: []string{\"bad\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tSsh: &linkedca.SSHPolicy{\n\t\t\t\t\tUser: &linkedca.SSHUserPolicy{\n\t\t\t\t\t\tAllow: &linkedca.SSHUserNames{\n\t\t\t\t\t\t\tEmails:     []string{\"@example.com\"},\n\t\t\t\t\t\t\tPrincipals: []string{\"*\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tDeny: &linkedca.SSHUserNames{\n\t\t\t\t\t\t\tEmails:     []string{\"bad@example.com\"},\n\t\t\t\t\t\t\tPrincipals: []string{\"root\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tHost: &linkedca.SSHHostPolicy{\n\t\t\t\t\t\tAllow: &linkedca.SSHHostNames{\n\t\t\t\t\t\t\tDns:        []string{\"*.example.com\"},\n\t\t\t\t\t\t\tIps:        []string{\"10.10.0.0/16\"},\n\t\t\t\t\t\t\tPrincipals: []string{\"good\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tDeny: &linkedca.SSHHostNames{\n\t\t\t\t\t\t\tDns:        []string{\"bad@example.com\"},\n\t\t\t\t\t\t\tIps:        []string{\"10.10.0.30\"},\n\t\t\t\t\t\t\tPrincipals: []string{\"bad\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t\tprov := &linkedca.Provisioner{\n\t\t\t\tPolicy: policy,\n\t\t\t}\n\t\t\tctx := linkedca.NewContextWithProvisioner(context.Background(), prov)\n\t\t\treturn test{\n\t\t\t\tctx:     ctx,\n\t\t\t\tadminDB: &admin.MockDB{},\n\t\t\t\tresponse: &testPolicyResponse{\n\t\t\t\t\tX509: &testX509Policy{\n\t\t\t\t\t\tAllow: &testX509Names{\n\t\t\t\t\t\t\tDNSDomains:     []string{\"*.local\"},\n\t\t\t\t\t\t\tIPRanges:       []string{\"10.0.0.0/16\"},\n\t\t\t\t\t\t\tEmailAddresses: []string{\"@example.com\"},\n\t\t\t\t\t\t\tURIDomains:     []string{\"example.com\"},\n\t\t\t\t\t\t\tCommonNames:    []string{\"test\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tDeny: &testX509Names{\n\t\t\t\t\t\t\tDNSDomains:     []string{\"bad.local\"},\n\t\t\t\t\t\t\tIPRanges:       []string{\"10.0.0.30\"},\n\t\t\t\t\t\t\tEmailAddresses: []string{\"bad@example.com\"},\n\t\t\t\t\t\t\tURIDomains:     []string{\"notexample.com\"},\n\t\t\t\t\t\t\tCommonNames:    []string{\"bad\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tSSH: &testSSHPolicy{\n\t\t\t\t\t\tUser: &testSSHUserPolicy{\n\t\t\t\t\t\t\tAllow: &testSSHUserNames{\n\t\t\t\t\t\t\t\tEmailAddresses: []string{\"@example.com\"},\n\t\t\t\t\t\t\t\tPrincipals:     []string{\"*\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tDeny: &testSSHUserNames{\n\t\t\t\t\t\t\t\tEmailAddresses: []string{\"bad@example.com\"},\n\t\t\t\t\t\t\t\tPrincipals:     []string{\"root\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tHost: &testSSHHostPolicy{\n\t\t\t\t\t\t\tAllow: &testSSHHostNames{\n\t\t\t\t\t\t\t\tDNSDomains: []string{\"*.example.com\"},\n\t\t\t\t\t\t\t\tIPRanges:   []string{\"10.10.0.0/16\"},\n\t\t\t\t\t\t\t\tPrincipals: []string{\"good\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tDeny: &testSSHHostNames{\n\t\t\t\t\t\t\t\tDNSDomains: []string{\"bad@example.com\"},\n\t\t\t\t\t\t\t\tIPRanges:   []string{\"10.10.0.30\"},\n\t\t\t\t\t\t\t\tPrincipals: []string{\"bad\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tstatusCode: 200,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, prep := range tests {\n\t\ttc := prep(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tmockMustAuthority(t, tc.auth)\n\t\t\tctx := admin.NewContext(tc.ctx, tc.adminDB)\n\t\t\tctx = acme.NewDatabaseContext(ctx, tc.acmeDB)\n\t\t\tpar := NewPolicyAdminResponder()\n\n\t\t\treq := httptest.NewRequest(\"GET\", \"/foo\", http.NoBody)\n\t\t\treq = req.WithContext(ctx)\n\t\t\tw := httptest.NewRecorder()\n\n\t\t\tpar.GetProvisionerPolicy(w, req)\n\t\t\tres := w.Result()\n\n\t\t\tassert.Equal(t, tc.statusCode, res.StatusCode)\n\n\t\t\tif res.StatusCode >= 400 {\n\n\t\t\t\tbody, err := io.ReadAll(res.Body)\n\t\t\t\tres.Body.Close()\n\t\t\t\tassert.NoError(t, err)\n\n\t\t\t\tae := testAdminError{}\n\t\t\t\tassert.NoError(t, json.Unmarshal(bytes.TrimSpace(body), &ae))\n\n\t\t\t\tassert.Equal(t, tc.err.Type, ae.Type)\n\t\t\t\tassert.Equal(t, tc.err.Message, ae.Message)\n\t\t\t\tassert.Equal(t, tc.err.StatusCode(), res.StatusCode)\n\t\t\t\tassert.Equal(t, tc.err.Detail, ae.Detail)\n\t\t\t\tassert.Equal(t, []string{\"application/json\"}, res.Header[\"Content-Type\"])\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tp := &testPolicyResponse{}\n\t\t\tbody, err := io.ReadAll(res.Body)\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.NoError(t, json.Unmarshal(body, &p))\n\n\t\t\tassert.Equal(t, tc.response, p)\n\n\t\t})\n\t}\n}\n\nfunc TestPolicyAdminResponder_CreateProvisionerPolicy(t *testing.T) {\n\ttype test struct {\n\t\tauth       adminAuthority\n\t\tadminDB    admin.DB\n\t\tbody       []byte\n\t\tctx        context.Context\n\t\terr        *admin.Error\n\t\tresponse   *testPolicyResponse\n\t\tstatusCode int\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/linkedca\": func(t *testing.T) test {\n\t\t\tctx := context.Background()\n\t\t\terr := admin.NewError(admin.ErrorNotImplementedType, \"policy operations not yet supported in linked deployments\")\n\t\t\terr.Message = \"policy operations not yet supported in linked deployments\"\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\tadminDB:    &fakeLinkedCA{},\n\t\t\t\terr:        err,\n\t\t\t\tstatusCode: 501,\n\t\t\t}\n\t\t},\n\t\t\"fail/existing-policy\": func(t *testing.T) test {\n\t\t\tpolicy := &linkedca.Policy{\n\t\t\t\tX509: &linkedca.X509Policy{\n\t\t\t\t\tAllow: &linkedca.X509Names{\n\t\t\t\t\t\tDns: []string{\"*.local\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t\tprov := &linkedca.Provisioner{\n\t\t\t\tName:   \"provName\",\n\t\t\t\tPolicy: policy,\n\t\t\t}\n\t\t\tctx := linkedca.NewContextWithProvisioner(context.Background(), prov)\n\t\t\terr := admin.NewError(admin.ErrorConflictType, \"provisioner provName already has a policy\")\n\t\t\terr.Message = \"provisioner provName already has a policy\"\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\tadminDB:    &admin.MockDB{},\n\t\t\t\terr:        err,\n\t\t\t\tstatusCode: 409,\n\t\t\t}\n\t\t},\n\t\t\"fail/read.ProtoJSON\": func(t *testing.T) test {\n\t\t\tprov := &linkedca.Provisioner{\n\t\t\t\tName: \"provName\",\n\t\t\t}\n\t\t\tctx := linkedca.NewContextWithProvisioner(context.Background(), prov)\n\t\t\tadminErr := admin.NewError(admin.ErrorBadRequestType, \"proto: syntax error (line 1:2): invalid value ?\")\n\t\t\tadminErr.Message = \"proto: syntax error (line 1:2): invalid value ?\"\n\t\t\tbody := []byte(\"{?}\")\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\tadminDB:    &admin.MockDB{},\n\t\t\t\tbody:       body,\n\t\t\t\terr:        adminErr,\n\t\t\t\tstatusCode: 400,\n\t\t\t}\n\t\t},\n\t\t\"fail/validatePolicy\": func(t *testing.T) test {\n\t\t\tprov := &linkedca.Provisioner{\n\t\t\t\tName: \"provName\",\n\t\t\t}\n\t\t\tctx := linkedca.NewContextWithProvisioner(context.Background(), prov)\n\t\t\tadminErr := admin.NewError(admin.ErrorBadRequestType, \"error validating provisioner policy: cannot parse permitted URI domain constraint \\\"https://example.com\\\": URI domain constraint \\\"https://example.com\\\" contains scheme (not supported yet)\")\n\t\t\tadminErr.Message = \"error validating provisioner policy: cannot parse permitted URI domain constraint \\\"https://example.com\\\": URI domain constraint \\\"https://example.com\\\" contains scheme (not supported yet)\"\n\t\t\tbody := []byte(`\n\t\t\t{\n\t\t\t\t\"x509\": {\n\t\t\t\t   \"allow\": {\n\t\t\t\t\t  \"uris\": [\n\t\t\t\t\t\t \t\"https://example.com\"\n\t\t\t\t\t\t]\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}`)\n\t\t\treturn test{\n\t\t\t\tctx:     ctx,\n\t\t\t\tadminDB: &admin.MockDB{},\n\t\t\t\tauth: &mockAdminAuthority{\n\t\t\t\t\tMockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) {\n\t\t\t\t\t\treturn nil, admin.NewError(admin.ErrorNotFoundType, \"not found\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tbody:       body,\n\t\t\t\terr:        adminErr,\n\t\t\t\tstatusCode: 400,\n\t\t\t}\n\t\t},\n\t\t\"fail/auth.UpdateProvisioner-policy-admin-lockout-error\": func(t *testing.T) test {\n\t\t\tadm := &linkedca.Admin{\n\t\t\t\tSubject: \"step\",\n\t\t\t}\n\t\t\tprov := &linkedca.Provisioner{\n\t\t\t\tName: \"provName\",\n\t\t\t}\n\t\t\tctx := linkedca.NewContextWithAdmin(context.Background(), adm)\n\t\t\tctx = linkedca.NewContextWithProvisioner(ctx, prov)\n\t\t\tadminErr := admin.NewError(admin.ErrorBadRequestType, \"error creating provisioner policy\")\n\t\t\tadminErr.Message = \"error creating provisioner policy: admin lock out\"\n\t\t\tpolicy := &linkedca.Policy{\n\t\t\t\tX509: &linkedca.X509Policy{\n\t\t\t\t\tAllow: &linkedca.X509Names{\n\t\t\t\t\t\tDns: []string{\"*.local\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t\tbody, err := protojson.Marshal(policy)\n\t\t\tassert.NoError(t, err)\n\t\t\treturn test{\n\t\t\t\tctx:     ctx,\n\t\t\t\tadminDB: &admin.MockDB{},\n\t\t\t\tauth: &mockAdminAuthority{\n\t\t\t\t\tMockUpdateProvisioner: func(ctx context.Context, nu *linkedca.Provisioner) error {\n\t\t\t\t\t\treturn &authority.PolicyError{\n\t\t\t\t\t\t\tTyp: authority.AdminLockOut,\n\t\t\t\t\t\t\tErr: errors.New(\"admin lock out\"),\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tbody:       body,\n\t\t\t\terr:        adminErr,\n\t\t\t\tstatusCode: 400,\n\t\t\t}\n\t\t},\n\t\t\"fail/auth.UpdateProvisioner-error\": func(t *testing.T) test {\n\t\t\tadm := &linkedca.Admin{\n\t\t\t\tSubject: \"step\",\n\t\t\t}\n\t\t\tprov := &linkedca.Provisioner{\n\t\t\t\tName: \"provName\",\n\t\t\t}\n\t\t\tctx := linkedca.NewContextWithAdmin(context.Background(), adm)\n\t\t\tctx = linkedca.NewContextWithProvisioner(ctx, prov)\n\t\t\tadminErr := admin.NewError(admin.ErrorServerInternalType, \"error creating provisioner policy: force\")\n\t\t\tadminErr.Message = \"error creating provisioner policy: force\"\n\t\t\tpolicy := &linkedca.Policy{\n\t\t\t\tX509: &linkedca.X509Policy{\n\t\t\t\t\tAllow: &linkedca.X509Names{\n\t\t\t\t\t\tDns: []string{\"*.local\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t\tbody, err := protojson.Marshal(policy)\n\t\t\tassert.NoError(t, err)\n\t\t\treturn test{\n\t\t\t\tctx:     ctx,\n\t\t\t\tadminDB: &admin.MockDB{},\n\t\t\t\tauth: &mockAdminAuthority{\n\t\t\t\t\tMockUpdateProvisioner: func(ctx context.Context, nu *linkedca.Provisioner) error {\n\t\t\t\t\t\treturn &authority.PolicyError{\n\t\t\t\t\t\t\tTyp: authority.StoreFailure,\n\t\t\t\t\t\t\tErr: errors.New(\"force\"),\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tbody:       body,\n\t\t\t\terr:        adminErr,\n\t\t\t\tstatusCode: 500,\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tadm := &linkedca.Admin{\n\t\t\t\tSubject: \"step\",\n\t\t\t}\n\t\t\tprov := &linkedca.Provisioner{\n\t\t\t\tName: \"provName\",\n\t\t\t}\n\t\t\tctx := linkedca.NewContextWithAdmin(context.Background(), adm)\n\t\t\tctx = linkedca.NewContextWithProvisioner(ctx, prov)\n\t\t\tpolicy := &linkedca.Policy{\n\t\t\t\tX509: &linkedca.X509Policy{\n\t\t\t\t\tAllow: &linkedca.X509Names{\n\t\t\t\t\t\tDns: []string{\"*.local\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t\tbody, err := protojson.Marshal(policy)\n\t\t\tassert.NoError(t, err)\n\t\t\treturn test{\n\t\t\t\tctx:     ctx,\n\t\t\t\tadminDB: &admin.MockDB{},\n\t\t\t\tauth: &mockAdminAuthority{\n\t\t\t\t\tMockUpdateProvisioner: func(ctx context.Context, nu *linkedca.Provisioner) error {\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tbody: body,\n\t\t\t\tresponse: &testPolicyResponse{\n\t\t\t\t\tX509: &testX509Policy{\n\t\t\t\t\t\tAllow: &testX509Names{\n\t\t\t\t\t\t\tDNSDomains: []string{\"*.local\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tstatusCode: 201,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, prep := range tests {\n\t\ttc := prep(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tmockMustAuthority(t, tc.auth)\n\t\t\tctx := admin.NewContext(tc.ctx, tc.adminDB)\n\t\t\tpar := NewPolicyAdminResponder()\n\n\t\t\treq := httptest.NewRequest(\"POST\", \"/foo\", io.NopCloser(bytes.NewBuffer(tc.body)))\n\t\t\treq = req.WithContext(ctx)\n\t\t\tw := httptest.NewRecorder()\n\n\t\t\tpar.CreateProvisionerPolicy(w, req)\n\t\t\tres := w.Result()\n\n\t\t\tassert.Equal(t, tc.statusCode, res.StatusCode)\n\n\t\t\tif res.StatusCode >= 400 {\n\n\t\t\t\tbody, err := io.ReadAll(res.Body)\n\t\t\t\tres.Body.Close()\n\t\t\t\tassert.NoError(t, err)\n\n\t\t\t\tae := testAdminError{}\n\t\t\t\tassert.NoError(t, json.Unmarshal(bytes.TrimSpace(body), &ae))\n\n\t\t\t\tassert.Equal(t, tc.err.Type, ae.Type)\n\t\t\t\tassert.Equal(t, tc.err.StatusCode(), res.StatusCode)\n\t\t\t\tassert.Equal(t, tc.err.Detail, ae.Detail)\n\t\t\t\tassert.Equal(t, []string{\"application/json\"}, res.Header[\"Content-Type\"])\n\n\t\t\t\t// when the error message starts with \"proto\", we expect it to have\n\t\t\t\t// a syntax error (in the tests). If the message doesn't start with \"proto\",\n\t\t\t\t// we expect a full string match.\n\t\t\t\tif strings.HasPrefix(tc.err.Message, \"proto:\") {\n\t\t\t\t\tassert.True(t, strings.Contains(ae.Message, \"syntax error\"))\n\t\t\t\t} else {\n\t\t\t\t\tassert.Equal(t, tc.err.Message, ae.Message)\n\t\t\t\t}\n\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tp := &testPolicyResponse{}\n\t\t\tbody, err := io.ReadAll(res.Body)\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.NoError(t, json.Unmarshal(body, &p))\n\n\t\t\tassert.Equal(t, tc.response, p)\n\n\t\t})\n\t}\n}\n\nfunc TestPolicyAdminResponder_UpdateProvisionerPolicy(t *testing.T) {\n\ttype test struct {\n\t\tauth       adminAuthority\n\t\tbody       []byte\n\t\tadminDB    admin.DB\n\t\tctx        context.Context\n\t\terr        *admin.Error\n\t\tresponse   *testPolicyResponse\n\t\tstatusCode int\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/linkedca\": func(t *testing.T) test {\n\t\t\tctx := context.Background()\n\t\t\terr := admin.NewError(admin.ErrorNotImplementedType, \"policy operations not yet supported in linked deployments\")\n\t\t\terr.Message = \"policy operations not yet supported in linked deployments\"\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\tadminDB:    &fakeLinkedCA{},\n\t\t\t\terr:        err,\n\t\t\t\tstatusCode: 501,\n\t\t\t}\n\t\t},\n\t\t\"fail/no-existing-policy\": func(t *testing.T) test {\n\t\t\tprov := &linkedca.Provisioner{\n\t\t\t\tName: \"provName\",\n\t\t\t}\n\t\t\tctx := linkedca.NewContextWithProvisioner(context.Background(), prov)\n\t\t\terr := admin.NewError(admin.ErrorNotFoundType, \"provisioner policy does not exist\")\n\t\t\terr.Message = \"provisioner policy does not exist\"\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\tadminDB:    &admin.MockDB{},\n\t\t\t\terr:        err,\n\t\t\t\tstatusCode: 404,\n\t\t\t}\n\t\t},\n\t\t\"fail/read.ProtoJSON\": func(t *testing.T) test {\n\t\t\tpolicy := &linkedca.Policy{\n\t\t\t\tX509: &linkedca.X509Policy{\n\t\t\t\t\tAllow: &linkedca.X509Names{\n\t\t\t\t\t\tDns: []string{\"*.local\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t\tprov := &linkedca.Provisioner{\n\t\t\t\tName:   \"provName\",\n\t\t\t\tPolicy: policy,\n\t\t\t}\n\t\t\tctx := linkedca.NewContextWithProvisioner(context.Background(), prov)\n\t\t\tadminErr := admin.NewError(admin.ErrorBadRequestType, \"proto: syntax error (line 1:2): invalid value ?\")\n\t\t\tadminErr.Message = \"proto: syntax error (line 1:2): invalid value ?\"\n\t\t\tbody := []byte(\"{?}\")\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\tadminDB:    &admin.MockDB{},\n\t\t\t\tbody:       body,\n\t\t\t\terr:        adminErr,\n\t\t\t\tstatusCode: 400,\n\t\t\t}\n\t\t},\n\t\t\"fail/validatePolicy\": func(t *testing.T) test {\n\t\t\tpolicy := &linkedca.Policy{\n\t\t\t\tX509: &linkedca.X509Policy{\n\t\t\t\t\tAllow: &linkedca.X509Names{\n\t\t\t\t\t\tDns: []string{\"*.local\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t\tprov := &linkedca.Provisioner{\n\t\t\t\tName:   \"provName\",\n\t\t\t\tPolicy: policy,\n\t\t\t}\n\t\t\tctx := linkedca.NewContextWithProvisioner(context.Background(), prov)\n\t\t\tadminErr := admin.NewError(admin.ErrorBadRequestType, \"error validating provisioner policy: cannot parse permitted URI domain constraint \\\"https://example.com\\\": URI domain constraint \\\"https://example.com\\\" contains scheme (not supported yet)\")\n\t\t\tadminErr.Message = \"error validating provisioner policy: cannot parse permitted URI domain constraint \\\"https://example.com\\\": URI domain constraint \\\"https://example.com\\\" contains scheme (not supported yet)\"\n\t\t\tbody := []byte(`\n\t\t\t{\n\t\t\t\t\"x509\": {\n\t\t\t\t   \"allow\": {\n\t\t\t\t\t  \"uris\": [\n\t\t\t\t\t\t \t\"https://example.com\"\n\t\t\t\t\t\t]\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}`)\n\t\t\treturn test{\n\t\t\t\tctx:     ctx,\n\t\t\t\tadminDB: &admin.MockDB{},\n\t\t\t\tauth: &mockAdminAuthority{\n\t\t\t\t\tMockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) {\n\t\t\t\t\t\treturn nil, admin.NewError(admin.ErrorNotFoundType, \"not found\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tbody:       body,\n\t\t\t\terr:        adminErr,\n\t\t\t\tstatusCode: 400,\n\t\t\t}\n\t\t},\n\t\t\"fail/auth.UpdateProvisioner-policy-admin-lockout-error\": func(t *testing.T) test {\n\t\t\tadm := &linkedca.Admin{\n\t\t\t\tSubject: \"step\",\n\t\t\t}\n\t\t\tpolicy := &linkedca.Policy{\n\t\t\t\tX509: &linkedca.X509Policy{\n\t\t\t\t\tAllow: &linkedca.X509Names{\n\t\t\t\t\t\tDns: []string{\"*.local\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t\tprov := &linkedca.Provisioner{\n\t\t\t\tName:   \"provName\",\n\t\t\t\tPolicy: policy,\n\t\t\t}\n\t\t\tctx := linkedca.NewContextWithAdmin(context.Background(), adm)\n\t\t\tctx = linkedca.NewContextWithProvisioner(ctx, prov)\n\t\t\tadminErr := admin.NewError(admin.ErrorBadRequestType, \"error updating provisioner policy\")\n\t\t\tadminErr.Message = \"error updating provisioner policy: admin lock out\"\n\t\t\tbody, err := protojson.Marshal(policy)\n\t\t\tassert.NoError(t, err)\n\t\t\treturn test{\n\t\t\t\tctx:     ctx,\n\t\t\t\tadminDB: &admin.MockDB{},\n\t\t\t\tauth: &mockAdminAuthority{\n\t\t\t\t\tMockUpdateProvisioner: func(ctx context.Context, nu *linkedca.Provisioner) error {\n\t\t\t\t\t\treturn &authority.PolicyError{\n\t\t\t\t\t\t\tTyp: authority.AdminLockOut,\n\t\t\t\t\t\t\tErr: errors.New(\"admin lock out\"),\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tbody:       body,\n\t\t\t\terr:        adminErr,\n\t\t\t\tstatusCode: 400,\n\t\t\t}\n\t\t},\n\t\t\"fail/auth.UpdateProvisioner-error\": func(t *testing.T) test {\n\t\t\tadm := &linkedca.Admin{\n\t\t\t\tSubject: \"step\",\n\t\t\t}\n\t\t\tpolicy := &linkedca.Policy{\n\t\t\t\tX509: &linkedca.X509Policy{\n\t\t\t\t\tAllow: &linkedca.X509Names{\n\t\t\t\t\t\tDns: []string{\"*.local\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t\tprov := &linkedca.Provisioner{\n\t\t\t\tName:   \"provName\",\n\t\t\t\tPolicy: policy,\n\t\t\t}\n\t\t\tctx := linkedca.NewContextWithAdmin(context.Background(), adm)\n\t\t\tctx = linkedca.NewContextWithProvisioner(ctx, prov)\n\t\t\tadminErr := admin.NewError(admin.ErrorServerInternalType, \"error updating provisioner policy: force\")\n\t\t\tadminErr.Message = \"error updating provisioner policy: force\"\n\t\t\tbody, err := protojson.Marshal(policy)\n\t\t\tassert.NoError(t, err)\n\t\t\treturn test{\n\t\t\t\tctx:     ctx,\n\t\t\t\tadminDB: &admin.MockDB{},\n\t\t\t\tauth: &mockAdminAuthority{\n\t\t\t\t\tMockUpdateProvisioner: func(ctx context.Context, nu *linkedca.Provisioner) error {\n\t\t\t\t\t\treturn &authority.PolicyError{\n\t\t\t\t\t\t\tTyp: authority.StoreFailure,\n\t\t\t\t\t\t\tErr: errors.New(\"force\"),\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tbody:       body,\n\t\t\t\terr:        adminErr,\n\t\t\t\tstatusCode: 500,\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tadm := &linkedca.Admin{\n\t\t\t\tSubject: \"step\",\n\t\t\t}\n\t\t\tpolicy := &linkedca.Policy{\n\t\t\t\tX509: &linkedca.X509Policy{\n\t\t\t\t\tAllow: &linkedca.X509Names{\n\t\t\t\t\t\tDns: []string{\"*.local\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t\tprov := &linkedca.Provisioner{\n\t\t\t\tName:   \"provName\",\n\t\t\t\tPolicy: policy,\n\t\t\t}\n\t\t\tctx := linkedca.NewContextWithAdmin(context.Background(), adm)\n\t\t\tctx = linkedca.NewContextWithProvisioner(ctx, prov)\n\t\t\tbody, err := protojson.Marshal(policy)\n\t\t\tassert.NoError(t, err)\n\t\t\treturn test{\n\t\t\t\tctx:     ctx,\n\t\t\t\tadminDB: &admin.MockDB{},\n\t\t\t\tauth: &mockAdminAuthority{\n\t\t\t\t\tMockUpdateProvisioner: func(ctx context.Context, nu *linkedca.Provisioner) error {\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tbody: body,\n\t\t\t\tresponse: &testPolicyResponse{\n\t\t\t\t\tX509: &testX509Policy{\n\t\t\t\t\t\tAllow: &testX509Names{\n\t\t\t\t\t\t\tDNSDomains: []string{\"*.local\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tstatusCode: 200,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, prep := range tests {\n\t\ttc := prep(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tmockMustAuthority(t, tc.auth)\n\t\t\tctx := admin.NewContext(tc.ctx, tc.adminDB)\n\t\t\tpar := NewPolicyAdminResponder()\n\n\t\t\treq := httptest.NewRequest(\"POST\", \"/foo\", io.NopCloser(bytes.NewBuffer(tc.body)))\n\t\t\treq = req.WithContext(ctx)\n\t\t\tw := httptest.NewRecorder()\n\n\t\t\tpar.UpdateProvisionerPolicy(w, req)\n\t\t\tres := w.Result()\n\n\t\t\tassert.Equal(t, tc.statusCode, res.StatusCode)\n\n\t\t\tif res.StatusCode >= 400 {\n\n\t\t\t\tbody, err := io.ReadAll(res.Body)\n\t\t\t\tres.Body.Close()\n\t\t\t\tassert.NoError(t, err)\n\n\t\t\t\tae := testAdminError{}\n\t\t\t\tassert.NoError(t, json.Unmarshal(bytes.TrimSpace(body), &ae))\n\n\t\t\t\tassert.Equal(t, tc.err.Type, ae.Type)\n\t\t\t\tassert.Equal(t, tc.err.StatusCode(), res.StatusCode)\n\t\t\t\tassert.Equal(t, tc.err.Detail, ae.Detail)\n\t\t\t\tassert.Equal(t, []string{\"application/json\"}, res.Header[\"Content-Type\"])\n\n\t\t\t\t// when the error message starts with \"proto\", we expect it to have\n\t\t\t\t// a syntax error (in the tests). If the message doesn't start with \"proto\",\n\t\t\t\t// we expect a full string match.\n\t\t\t\tif strings.HasPrefix(tc.err.Message, \"proto:\") {\n\t\t\t\t\tassert.True(t, strings.Contains(ae.Message, \"syntax error\"))\n\t\t\t\t} else {\n\t\t\t\t\tassert.Equal(t, tc.err.Message, ae.Message)\n\t\t\t\t}\n\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tp := &testPolicyResponse{}\n\t\t\tbody, err := io.ReadAll(res.Body)\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.NoError(t, json.Unmarshal(body, &p))\n\n\t\t\tassert.Equal(t, tc.response, p)\n\n\t\t})\n\t}\n}\n\nfunc TestPolicyAdminResponder_DeleteProvisionerPolicy(t *testing.T) {\n\ttype test struct {\n\t\tauth       adminAuthority\n\t\tadminDB    admin.DB\n\t\tbody       []byte\n\t\tctx        context.Context\n\t\tacmeDB     acme.DB\n\t\terr        *admin.Error\n\t\tstatusCode int\n\t}\n\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/linkedca\": func(t *testing.T) test {\n\t\t\tctx := context.Background()\n\t\t\terr := admin.NewError(admin.ErrorNotImplementedType, \"policy operations not yet supported in linked deployments\")\n\t\t\terr.Message = \"policy operations not yet supported in linked deployments\"\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\tadminDB:    &fakeLinkedCA{},\n\t\t\t\terr:        err,\n\t\t\t\tstatusCode: 501,\n\t\t\t}\n\t\t},\n\t\t\"fail/no-existing-policy\": func(t *testing.T) test {\n\t\t\tprov := &linkedca.Provisioner{\n\t\t\t\tName: \"provName\",\n\t\t\t}\n\t\t\tctx := linkedca.NewContextWithProvisioner(context.Background(), prov)\n\t\t\terr := admin.NewError(admin.ErrorNotFoundType, \"provisioner policy does not exist\")\n\t\t\terr.Message = \"provisioner policy does not exist\"\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\tadminDB:    &admin.MockDB{},\n\t\t\t\terr:        err,\n\t\t\t\tstatusCode: 404,\n\t\t\t}\n\t\t},\n\t\t\"fail/auth.UpdateProvisioner-error\": func(t *testing.T) test {\n\t\t\tprov := &linkedca.Provisioner{\n\t\t\t\tName:   \"provName\",\n\t\t\t\tPolicy: &linkedca.Policy{},\n\t\t\t}\n\t\t\tctx := linkedca.NewContextWithProvisioner(context.Background(), prov)\n\t\t\terr := admin.NewErrorISE(\"error deleting provisioner policy: force\")\n\t\t\terr.Message = \"error deleting provisioner policy: force\"\n\t\t\treturn test{\n\t\t\t\tctx:     ctx,\n\t\t\t\tadminDB: &admin.MockDB{},\n\t\t\t\tauth: &mockAdminAuthority{\n\t\t\t\t\tMockUpdateProvisioner: func(ctx context.Context, nu *linkedca.Provisioner) error {\n\t\t\t\t\t\treturn errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr:        err,\n\t\t\t\tstatusCode: 500,\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tprov := &linkedca.Provisioner{\n\t\t\t\tName:   \"provName\",\n\t\t\t\tPolicy: &linkedca.Policy{},\n\t\t\t}\n\t\t\tctx := linkedca.NewContextWithProvisioner(context.Background(), prov)\n\t\t\treturn test{\n\t\t\t\tctx:     ctx,\n\t\t\t\tadminDB: &admin.MockDB{},\n\t\t\t\tauth: &mockAdminAuthority{\n\t\t\t\t\tMockUpdateProvisioner: func(ctx context.Context, nu *linkedca.Provisioner) error {\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tstatusCode: 200,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, prep := range tests {\n\t\ttc := prep(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tmockMustAuthority(t, tc.auth)\n\t\t\tctx := admin.NewContext(tc.ctx, tc.adminDB)\n\t\t\tctx = acme.NewDatabaseContext(ctx, tc.acmeDB)\n\t\t\tpar := NewPolicyAdminResponder()\n\n\t\t\treq := httptest.NewRequest(\"POST\", \"/foo\", io.NopCloser(bytes.NewBuffer(tc.body)))\n\t\t\treq = req.WithContext(ctx)\n\t\t\tw := httptest.NewRecorder()\n\n\t\t\tpar.DeleteProvisionerPolicy(w, req)\n\t\t\tres := w.Result()\n\n\t\t\tassert.Equal(t, tc.statusCode, res.StatusCode)\n\n\t\t\tif res.StatusCode >= 400 {\n\n\t\t\t\tbody, err := io.ReadAll(res.Body)\n\t\t\t\tres.Body.Close()\n\t\t\t\tassert.NoError(t, err)\n\n\t\t\t\tae := testAdminError{}\n\t\t\t\tassert.NoError(t, json.Unmarshal(bytes.TrimSpace(body), &ae))\n\n\t\t\t\tassert.Equal(t, tc.err.Type, ae.Type)\n\t\t\t\tassert.Equal(t, tc.err.Message, ae.Message)\n\t\t\t\tassert.Equal(t, tc.err.StatusCode(), res.StatusCode)\n\t\t\t\tassert.Equal(t, tc.err.Detail, ae.Detail)\n\t\t\t\tassert.Equal(t, []string{\"application/json\"}, res.Header[\"Content-Type\"])\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tbody, err := io.ReadAll(res.Body)\n\t\t\tassert.NoError(t, err)\n\t\t\tres.Body.Close()\n\t\t\tresponse := DeleteResponse{}\n\t\t\tassert.NoError(t, json.Unmarshal(bytes.TrimSpace(body), &response))\n\t\t\tassert.Equal(t, \"ok\", response.Status)\n\t\t\tassert.Equal(t, []string{\"application/json\"}, res.Header[\"Content-Type\"])\n\n\t\t})\n\t}\n}\n\nfunc TestPolicyAdminResponder_GetACMEAccountPolicy(t *testing.T) {\n\ttype test struct {\n\t\tctx        context.Context\n\t\tacmeDB     acme.DB\n\t\tadminDB    admin.DB\n\t\terr        *admin.Error\n\t\tresponse   *testPolicyResponse\n\t\tstatusCode int\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/linkedca\": func(t *testing.T) test {\n\t\t\tctx := context.Background()\n\t\t\terr := admin.NewError(admin.ErrorNotImplementedType, \"policy operations not yet supported in linked deployments\")\n\t\t\terr.Message = \"policy operations not yet supported in linked deployments\"\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\tadminDB:    &fakeLinkedCA{},\n\t\t\t\terr:        err,\n\t\t\t\tstatusCode: 501,\n\t\t\t}\n\t\t},\n\t\t\"fail/no-policy\": func(t *testing.T) test {\n\t\t\tprov := &linkedca.Provisioner{\n\t\t\t\tName: \"provName\",\n\t\t\t}\n\t\t\teak := &linkedca.EABKey{\n\t\t\t\tId: \"eakID\",\n\t\t\t}\n\t\t\tctx := linkedca.NewContextWithProvisioner(context.Background(), prov)\n\t\t\tctx = linkedca.NewContextWithExternalAccountKey(ctx, eak)\n\t\t\terr := admin.NewError(admin.ErrorNotFoundType, \"ACME EAK policy does not exist\")\n\t\t\terr.Message = \"ACME EAK policy does not exist\"\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\tadminDB:    &admin.MockDB{},\n\t\t\t\terr:        err,\n\t\t\t\tstatusCode: 404,\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tpolicy := &linkedca.Policy{\n\t\t\t\tX509: &linkedca.X509Policy{\n\t\t\t\t\tAllow: &linkedca.X509Names{\n\t\t\t\t\t\tDns:         []string{\"*.local\"},\n\t\t\t\t\t\tIps:         []string{\"10.0.0.0/16\"},\n\t\t\t\t\t\tEmails:      []string{\"@example.com\"},\n\t\t\t\t\t\tUris:        []string{\"example.com\"},\n\t\t\t\t\t\tCommonNames: []string{\"test\"},\n\t\t\t\t\t},\n\t\t\t\t\tDeny: &linkedca.X509Names{\n\t\t\t\t\t\tDns:         []string{\"bad.local\"},\n\t\t\t\t\t\tIps:         []string{\"10.0.0.30\"},\n\t\t\t\t\t\tEmails:      []string{\"bad@example.com\"},\n\t\t\t\t\t\tUris:        []string{\"notexample.com\"},\n\t\t\t\t\t\tCommonNames: []string{\"bad\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tSsh: &linkedca.SSHPolicy{\n\t\t\t\t\tUser: &linkedca.SSHUserPolicy{\n\t\t\t\t\t\tAllow: &linkedca.SSHUserNames{\n\t\t\t\t\t\t\tEmails:     []string{\"@example.com\"},\n\t\t\t\t\t\t\tPrincipals: []string{\"*\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tDeny: &linkedca.SSHUserNames{\n\t\t\t\t\t\t\tEmails:     []string{\"bad@example.com\"},\n\t\t\t\t\t\t\tPrincipals: []string{\"root\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tHost: &linkedca.SSHHostPolicy{\n\t\t\t\t\t\tAllow: &linkedca.SSHHostNames{\n\t\t\t\t\t\t\tDns:        []string{\"*.example.com\"},\n\t\t\t\t\t\t\tIps:        []string{\"10.10.0.0/16\"},\n\t\t\t\t\t\t\tPrincipals: []string{\"good\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tDeny: &linkedca.SSHHostNames{\n\t\t\t\t\t\t\tDns:        []string{\"bad@example.com\"},\n\t\t\t\t\t\t\tIps:        []string{\"10.10.0.30\"},\n\t\t\t\t\t\t\tPrincipals: []string{\"bad\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t\tprov := &linkedca.Provisioner{\n\t\t\t\tName: \"provName\",\n\t\t\t}\n\t\t\teak := &linkedca.EABKey{\n\t\t\t\tId:     \"eakID\",\n\t\t\t\tPolicy: policy,\n\t\t\t}\n\t\t\tctx := linkedca.NewContextWithProvisioner(context.Background(), prov)\n\t\t\tctx = linkedca.NewContextWithExternalAccountKey(ctx, eak)\n\t\t\treturn test{\n\t\t\t\tctx:     ctx,\n\t\t\t\tadminDB: &admin.MockDB{},\n\t\t\t\tresponse: &testPolicyResponse{\n\t\t\t\t\tX509: &testX509Policy{\n\t\t\t\t\t\tAllow: &testX509Names{\n\t\t\t\t\t\t\tDNSDomains:     []string{\"*.local\"},\n\t\t\t\t\t\t\tIPRanges:       []string{\"10.0.0.0/16\"},\n\t\t\t\t\t\t\tEmailAddresses: []string{\"@example.com\"},\n\t\t\t\t\t\t\tURIDomains:     []string{\"example.com\"},\n\t\t\t\t\t\t\tCommonNames:    []string{\"test\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tDeny: &testX509Names{\n\t\t\t\t\t\t\tDNSDomains:     []string{\"bad.local\"},\n\t\t\t\t\t\t\tIPRanges:       []string{\"10.0.0.30\"},\n\t\t\t\t\t\t\tEmailAddresses: []string{\"bad@example.com\"},\n\t\t\t\t\t\t\tURIDomains:     []string{\"notexample.com\"},\n\t\t\t\t\t\t\tCommonNames:    []string{\"bad\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tSSH: &testSSHPolicy{\n\t\t\t\t\t\tUser: &testSSHUserPolicy{\n\t\t\t\t\t\t\tAllow: &testSSHUserNames{\n\t\t\t\t\t\t\t\tEmailAddresses: []string{\"@example.com\"},\n\t\t\t\t\t\t\t\tPrincipals:     []string{\"*\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tDeny: &testSSHUserNames{\n\t\t\t\t\t\t\t\tEmailAddresses: []string{\"bad@example.com\"},\n\t\t\t\t\t\t\t\tPrincipals:     []string{\"root\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tHost: &testSSHHostPolicy{\n\t\t\t\t\t\t\tAllow: &testSSHHostNames{\n\t\t\t\t\t\t\t\tDNSDomains: []string{\"*.example.com\"},\n\t\t\t\t\t\t\t\tIPRanges:   []string{\"10.10.0.0/16\"},\n\t\t\t\t\t\t\t\tPrincipals: []string{\"good\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tDeny: &testSSHHostNames{\n\t\t\t\t\t\t\t\tDNSDomains: []string{\"bad@example.com\"},\n\t\t\t\t\t\t\t\tIPRanges:   []string{\"10.10.0.30\"},\n\t\t\t\t\t\t\t\tPrincipals: []string{\"bad\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tstatusCode: 200,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, prep := range tests {\n\t\ttc := prep(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tctx := admin.NewContext(tc.ctx, tc.adminDB)\n\t\t\tctx = acme.NewDatabaseContext(ctx, tc.acmeDB)\n\t\t\tpar := NewPolicyAdminResponder()\n\n\t\t\treq := httptest.NewRequest(\"GET\", \"/foo\", http.NoBody)\n\t\t\treq = req.WithContext(ctx)\n\t\t\tw := httptest.NewRecorder()\n\n\t\t\tpar.GetACMEAccountPolicy(w, req)\n\t\t\tres := w.Result()\n\n\t\t\tassert.Equal(t, tc.statusCode, res.StatusCode)\n\n\t\t\tif res.StatusCode >= 400 {\n\n\t\t\t\tbody, err := io.ReadAll(res.Body)\n\t\t\t\tres.Body.Close()\n\t\t\t\tassert.NoError(t, err)\n\n\t\t\t\tae := testAdminError{}\n\t\t\t\tassert.NoError(t, json.Unmarshal(bytes.TrimSpace(body), &ae))\n\n\t\t\t\tassert.Equal(t, tc.err.Type, ae.Type)\n\t\t\t\tassert.Equal(t, tc.err.Message, ae.Message)\n\t\t\t\tassert.Equal(t, tc.err.StatusCode(), res.StatusCode)\n\t\t\t\tassert.Equal(t, tc.err.Detail, ae.Detail)\n\t\t\t\tassert.Equal(t, []string{\"application/json\"}, res.Header[\"Content-Type\"])\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tp := &testPolicyResponse{}\n\t\t\tbody, err := io.ReadAll(res.Body)\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.NoError(t, json.Unmarshal(body, &p))\n\n\t\t\tassert.Equal(t, tc.response, p)\n\n\t\t})\n\t}\n}\n\nfunc TestPolicyAdminResponder_CreateACMEAccountPolicy(t *testing.T) {\n\ttype test struct {\n\t\tacmeDB     acme.DB\n\t\tadminDB    admin.DB\n\t\tbody       []byte\n\t\tctx        context.Context\n\t\terr        *admin.Error\n\t\tresponse   *testPolicyResponse\n\t\tstatusCode int\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/linkedca\": func(t *testing.T) test {\n\t\t\tctx := context.Background()\n\t\t\terr := admin.NewError(admin.ErrorNotImplementedType, \"policy operations not yet supported in linked deployments\")\n\t\t\terr.Message = \"policy operations not yet supported in linked deployments\"\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\tadminDB:    &fakeLinkedCA{},\n\t\t\t\terr:        err,\n\t\t\t\tstatusCode: 501,\n\t\t\t}\n\t\t},\n\t\t\"fail/existing-policy\": func(t *testing.T) test {\n\t\t\tpolicy := &linkedca.Policy{\n\t\t\t\tX509: &linkedca.X509Policy{\n\t\t\t\t\tAllow: &linkedca.X509Names{\n\t\t\t\t\t\tDns: []string{\"*.local\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t\tprov := &linkedca.Provisioner{\n\t\t\t\tName: \"provName\",\n\t\t\t}\n\t\t\teak := &linkedca.EABKey{\n\t\t\t\tId:     \"eakID\",\n\t\t\t\tPolicy: policy,\n\t\t\t}\n\t\t\tctx := linkedca.NewContextWithProvisioner(context.Background(), prov)\n\t\t\tctx = linkedca.NewContextWithExternalAccountKey(ctx, eak)\n\t\t\terr := admin.NewError(admin.ErrorConflictType, \"ACME EAK eakID already has a policy\")\n\t\t\terr.Message = \"ACME EAK eakID already has a policy\"\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\tadminDB:    &admin.MockDB{},\n\t\t\t\terr:        err,\n\t\t\t\tstatusCode: 409,\n\t\t\t}\n\t\t},\n\t\t\"fail/read.ProtoJSON\": func(t *testing.T) test {\n\t\t\tprov := &linkedca.Provisioner{\n\t\t\t\tName: \"provName\",\n\t\t\t}\n\t\t\teak := &linkedca.EABKey{\n\t\t\t\tId: \"eakID\",\n\t\t\t}\n\t\t\tctx := linkedca.NewContextWithProvisioner(context.Background(), prov)\n\t\t\tctx = linkedca.NewContextWithExternalAccountKey(ctx, eak)\n\t\t\tadminErr := admin.NewError(admin.ErrorBadRequestType, \"proto: syntax error (line 1:2): invalid value ?\")\n\t\t\tadminErr.Message = \"proto: syntax error (line 1:2): invalid value ?\"\n\t\t\tbody := []byte(\"{?}\")\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\tadminDB:    &admin.MockDB{},\n\t\t\t\tbody:       body,\n\t\t\t\terr:        adminErr,\n\t\t\t\tstatusCode: 400,\n\t\t\t}\n\t\t},\n\t\t\"fail/validatePolicy\": func(t *testing.T) test {\n\t\t\tprov := &linkedca.Provisioner{\n\t\t\t\tName: \"provName\",\n\t\t\t}\n\t\t\teak := &linkedca.EABKey{\n\t\t\t\tId: \"eakID\",\n\t\t\t}\n\t\t\tctx := linkedca.NewContextWithProvisioner(context.Background(), prov)\n\t\t\tctx = linkedca.NewContextWithExternalAccountKey(ctx, eak)\n\t\t\tadminErr := admin.NewError(admin.ErrorBadRequestType, \"error validating ACME EAK policy: cannot parse permitted URI domain constraint \\\"https://example.com\\\": URI domain constraint \\\"https://example.com\\\" contains scheme (not supported yet)\")\n\t\t\tadminErr.Message = \"error validating ACME EAK policy: cannot parse permitted URI domain constraint \\\"https://example.com\\\": URI domain constraint \\\"https://example.com\\\" contains scheme (not supported yet)\"\n\t\t\tbody := []byte(`\n\t\t\t{\n\t\t\t\t\"x509\": {\n\t\t\t\t   \"allow\": {\n\t\t\t\t\t  \"uris\": [\n\t\t\t\t\t\t \t\"https://example.com\"\n\t\t\t\t\t\t]\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}`)\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\tadminDB:    &admin.MockDB{},\n\t\t\t\tbody:       body,\n\t\t\t\terr:        adminErr,\n\t\t\t\tstatusCode: 400,\n\t\t\t}\n\t\t},\n\t\t\"fail/acmeDB.UpdateExternalAccountKey-error\": func(t *testing.T) test {\n\t\t\tprov := &linkedca.Provisioner{\n\t\t\t\tId:   \"provID\",\n\t\t\t\tName: \"provName\",\n\t\t\t}\n\t\t\teak := &linkedca.EABKey{\n\t\t\t\tId: \"eakID\",\n\t\t\t}\n\t\t\tctx := linkedca.NewContextWithProvisioner(context.Background(), prov)\n\t\t\tctx = linkedca.NewContextWithExternalAccountKey(ctx, eak)\n\t\t\tadminErr := admin.NewError(admin.ErrorServerInternalType, \"error creating ACME EAK policy\")\n\t\t\tadminErr.Message = \"error creating ACME EAK policy: force\"\n\t\t\tpolicy := &linkedca.Policy{\n\t\t\t\tX509: &linkedca.X509Policy{\n\t\t\t\t\tAllow: &linkedca.X509Names{\n\t\t\t\t\t\tDns: []string{\"*.local\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t\tbody, err := protojson.Marshal(policy)\n\t\t\tassert.NoError(t, err)\n\t\t\treturn test{\n\t\t\t\tctx:     ctx,\n\t\t\t\tadminDB: &admin.MockDB{},\n\t\t\t\tacmeDB: &acme.MockDB{\n\t\t\t\t\tMockUpdateExternalAccountKey: func(ctx context.Context, provisionerID string, eak *acme.ExternalAccountKey) error {\n\t\t\t\t\t\tassert.Equal(t, \"provID\", provisionerID)\n\t\t\t\t\t\tassert.Equal(t, \"eakID\", eak.ID)\n\t\t\t\t\t\treturn errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tbody:       body,\n\t\t\t\terr:        adminErr,\n\t\t\t\tstatusCode: 500,\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tprov := &linkedca.Provisioner{\n\t\t\t\tId:   \"provID\",\n\t\t\t\tName: \"provName\",\n\t\t\t}\n\t\t\teak := &linkedca.EABKey{\n\t\t\t\tId: \"eakID\",\n\t\t\t}\n\t\t\tctx := linkedca.NewContextWithProvisioner(context.Background(), prov)\n\t\t\tctx = linkedca.NewContextWithExternalAccountKey(ctx, eak)\n\t\t\tpolicy := &linkedca.Policy{\n\t\t\t\tX509: &linkedca.X509Policy{\n\t\t\t\t\tAllow: &linkedca.X509Names{\n\t\t\t\t\t\tDns: []string{\"*.local\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t\tbody, err := protojson.Marshal(policy)\n\t\t\tassert.NoError(t, err)\n\t\t\treturn test{\n\t\t\t\tctx:     ctx,\n\t\t\t\tadminDB: &admin.MockDB{},\n\t\t\t\tacmeDB: &acme.MockDB{\n\t\t\t\t\tMockUpdateExternalAccountKey: func(ctx context.Context, provisionerID string, eak *acme.ExternalAccountKey) error {\n\t\t\t\t\t\tassert.Equal(t, \"provID\", provisionerID)\n\t\t\t\t\t\tassert.Equal(t, \"eakID\", eak.ID)\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tbody: body,\n\t\t\t\tresponse: &testPolicyResponse{\n\t\t\t\t\tX509: &testX509Policy{\n\t\t\t\t\t\tAllow: &testX509Names{\n\t\t\t\t\t\t\tDNSDomains: []string{\"*.local\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tstatusCode: 201,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, prep := range tests {\n\t\ttc := prep(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tctx := admin.NewContext(tc.ctx, tc.adminDB)\n\t\t\tctx = acme.NewDatabaseContext(ctx, tc.acmeDB)\n\t\t\tpar := NewPolicyAdminResponder()\n\n\t\t\treq := httptest.NewRequest(\"POST\", \"/foo\", io.NopCloser(bytes.NewBuffer(tc.body)))\n\t\t\treq = req.WithContext(ctx)\n\t\t\tw := httptest.NewRecorder()\n\n\t\t\tpar.CreateACMEAccountPolicy(w, req)\n\t\t\tres := w.Result()\n\n\t\t\tassert.Equal(t, tc.statusCode, res.StatusCode)\n\n\t\t\tif res.StatusCode >= 400 {\n\n\t\t\t\tbody, err := io.ReadAll(res.Body)\n\t\t\t\tres.Body.Close()\n\t\t\t\tassert.NoError(t, err)\n\n\t\t\t\tae := testAdminError{}\n\t\t\t\tassert.NoError(t, json.Unmarshal(bytes.TrimSpace(body), &ae))\n\n\t\t\t\tassert.Equal(t, tc.err.Type, ae.Type)\n\t\t\t\tassert.Equal(t, tc.err.StatusCode(), res.StatusCode)\n\t\t\t\tassert.Equal(t, tc.err.Detail, ae.Detail)\n\t\t\t\tassert.Equal(t, []string{\"application/json\"}, res.Header[\"Content-Type\"])\n\n\t\t\t\t// when the error message starts with \"proto\", we expect it to have\n\t\t\t\t// a syntax error (in the tests). If the message doesn't start with \"proto\",\n\t\t\t\t// we expect a full string match.\n\t\t\t\tif strings.HasPrefix(tc.err.Message, \"proto:\") {\n\t\t\t\t\tassert.True(t, strings.Contains(ae.Message, \"syntax error\"))\n\t\t\t\t} else {\n\t\t\t\t\tassert.Equal(t, tc.err.Message, ae.Message)\n\t\t\t\t}\n\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tp := &testPolicyResponse{}\n\t\t\tbody, err := io.ReadAll(res.Body)\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.NoError(t, json.Unmarshal(body, &p))\n\n\t\t\tassert.Equal(t, tc.response, p)\n\n\t\t})\n\t}\n}\n\nfunc TestPolicyAdminResponder_UpdateACMEAccountPolicy(t *testing.T) {\n\ttype test struct {\n\t\tacmeDB     acme.DB\n\t\tadminDB    admin.DB\n\t\tbody       []byte\n\t\tctx        context.Context\n\t\terr        *admin.Error\n\t\tresponse   *testPolicyResponse\n\t\tstatusCode int\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/linkedca\": func(t *testing.T) test {\n\t\t\tctx := context.Background()\n\t\t\terr := admin.NewError(admin.ErrorNotImplementedType, \"policy operations not yet supported in linked deployments\")\n\t\t\terr.Message = \"policy operations not yet supported in linked deployments\"\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\tadminDB:    &fakeLinkedCA{},\n\t\t\t\terr:        err,\n\t\t\t\tstatusCode: 501,\n\t\t\t}\n\t\t},\n\t\t\"fail/no-existing-policy\": func(t *testing.T) test {\n\t\t\tprov := &linkedca.Provisioner{\n\t\t\t\tName: \"provName\",\n\t\t\t}\n\t\t\teak := &linkedca.EABKey{\n\t\t\t\tId: \"eakID\",\n\t\t\t}\n\t\t\tctx := linkedca.NewContextWithProvisioner(context.Background(), prov)\n\t\t\tctx = linkedca.NewContextWithExternalAccountKey(ctx, eak)\n\t\t\terr := admin.NewError(admin.ErrorNotFoundType, \"ACME EAK policy does not exist\")\n\t\t\terr.Message = \"ACME EAK policy does not exist\"\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\tadminDB:    &admin.MockDB{},\n\t\t\t\terr:        err,\n\t\t\t\tstatusCode: 404,\n\t\t\t}\n\t\t},\n\t\t\"fail/read.ProtoJSON\": func(t *testing.T) test {\n\t\t\tpolicy := &linkedca.Policy{\n\t\t\t\tX509: &linkedca.X509Policy{\n\t\t\t\t\tAllow: &linkedca.X509Names{\n\t\t\t\t\t\tDns: []string{\"*.local\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t\tprov := &linkedca.Provisioner{\n\t\t\t\tName: \"provName\",\n\t\t\t}\n\t\t\teak := &linkedca.EABKey{\n\t\t\t\tId:     \"eakID\",\n\t\t\t\tPolicy: policy,\n\t\t\t}\n\t\t\tctx := linkedca.NewContextWithProvisioner(context.Background(), prov)\n\t\t\tctx = linkedca.NewContextWithExternalAccountKey(ctx, eak)\n\t\t\tadminErr := admin.NewError(admin.ErrorBadRequestType, \"proto: syntax error (line 1:2): invalid value ?\")\n\t\t\tadminErr.Message = \"proto: syntax error (line 1:2): invalid value ?\"\n\t\t\tbody := []byte(\"{?}\")\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\tadminDB:    &admin.MockDB{},\n\t\t\t\tbody:       body,\n\t\t\t\terr:        adminErr,\n\t\t\t\tstatusCode: 400,\n\t\t\t}\n\t\t},\n\t\t\"fail/validatePolicy\": func(t *testing.T) test {\n\t\t\tpolicy := &linkedca.Policy{\n\t\t\t\tX509: &linkedca.X509Policy{\n\t\t\t\t\tAllow: &linkedca.X509Names{\n\t\t\t\t\t\tDns: []string{\"*.local\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t\tprov := &linkedca.Provisioner{\n\t\t\t\tName: \"provName\",\n\t\t\t}\n\t\t\teak := &linkedca.EABKey{\n\t\t\t\tId:     \"eakID\",\n\t\t\t\tPolicy: policy,\n\t\t\t}\n\t\t\tctx := linkedca.NewContextWithProvisioner(context.Background(), prov)\n\t\t\tctx = linkedca.NewContextWithExternalAccountKey(ctx, eak)\n\t\t\tadminErr := admin.NewError(admin.ErrorBadRequestType, \"error validating ACME EAK policy: cannot parse permitted URI domain constraint \\\"https://example.com\\\": URI domain constraint \\\"https://example.com\\\" contains scheme (not supported yet)\")\n\t\t\tadminErr.Message = \"error validating ACME EAK policy: cannot parse permitted URI domain constraint \\\"https://example.com\\\": URI domain constraint \\\"https://example.com\\\" contains scheme (not supported yet)\"\n\t\t\tbody := []byte(`\n\t\t\t{\n\t\t\t\t\"x509\": {\n\t\t\t\t   \"allow\": {\n\t\t\t\t\t  \"uris\": [\n\t\t\t\t\t\t \t\"https://example.com\"\n\t\t\t\t\t\t]\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}`)\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\tadminDB:    &admin.MockDB{},\n\t\t\t\tbody:       body,\n\t\t\t\terr:        adminErr,\n\t\t\t\tstatusCode: 400,\n\t\t\t}\n\t\t},\n\t\t\"fail/acmeDB.UpdateExternalAccountKey-error\": func(t *testing.T) test {\n\t\t\tpolicy := &linkedca.Policy{\n\t\t\t\tX509: &linkedca.X509Policy{\n\t\t\t\t\tAllow: &linkedca.X509Names{\n\t\t\t\t\t\tDns: []string{\"*.local\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t\tprov := &linkedca.Provisioner{\n\t\t\t\tName: \"provName\",\n\t\t\t\tId:   \"provID\",\n\t\t\t}\n\t\t\teak := &linkedca.EABKey{\n\t\t\t\tId:     \"eakID\",\n\t\t\t\tPolicy: policy,\n\t\t\t}\n\t\t\tctx := linkedca.NewContextWithProvisioner(context.Background(), prov)\n\t\t\tctx = linkedca.NewContextWithExternalAccountKey(ctx, eak)\n\t\t\tadminErr := admin.NewError(admin.ErrorServerInternalType, \"error updating ACME EAK policy: force\")\n\t\t\tadminErr.Message = \"error updating ACME EAK policy: force\"\n\t\t\tbody, err := protojson.Marshal(policy)\n\t\t\tassert.NoError(t, err)\n\t\t\treturn test{\n\t\t\t\tctx:     ctx,\n\t\t\t\tadminDB: &admin.MockDB{},\n\t\t\t\tacmeDB: &acme.MockDB{\n\t\t\t\t\tMockUpdateExternalAccountKey: func(ctx context.Context, provisionerID string, eak *acme.ExternalAccountKey) error {\n\t\t\t\t\t\tassert.Equal(t, \"provID\", provisionerID)\n\t\t\t\t\t\tassert.Equal(t, \"eakID\", eak.ID)\n\t\t\t\t\t\treturn errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tbody:       body,\n\t\t\t\terr:        adminErr,\n\t\t\t\tstatusCode: 500,\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tpolicy := &linkedca.Policy{\n\t\t\t\tX509: &linkedca.X509Policy{\n\t\t\t\t\tAllow: &linkedca.X509Names{\n\t\t\t\t\t\tDns: []string{\"*.local\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t\tprov := &linkedca.Provisioner{\n\t\t\t\tName: \"provName\",\n\t\t\t\tId:   \"provID\",\n\t\t\t}\n\t\t\teak := &linkedca.EABKey{\n\t\t\t\tId:     \"eakID\",\n\t\t\t\tPolicy: policy,\n\t\t\t}\n\t\t\tctx := linkedca.NewContextWithProvisioner(context.Background(), prov)\n\t\t\tctx = linkedca.NewContextWithExternalAccountKey(ctx, eak)\n\t\t\tbody, err := protojson.Marshal(policy)\n\t\t\tassert.NoError(t, err)\n\t\t\treturn test{\n\t\t\t\tctx:     ctx,\n\t\t\t\tadminDB: &admin.MockDB{},\n\t\t\t\tacmeDB: &acme.MockDB{\n\t\t\t\t\tMockUpdateExternalAccountKey: func(ctx context.Context, provisionerID string, eak *acme.ExternalAccountKey) error {\n\t\t\t\t\t\tassert.Equal(t, \"provID\", provisionerID)\n\t\t\t\t\t\tassert.Equal(t, \"eakID\", eak.ID)\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tbody: body,\n\t\t\t\tresponse: &testPolicyResponse{\n\t\t\t\t\tX509: &testX509Policy{\n\t\t\t\t\t\tAllow: &testX509Names{\n\t\t\t\t\t\t\tDNSDomains: []string{\"*.local\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tstatusCode: 200,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, prep := range tests {\n\t\ttc := prep(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tctx := admin.NewContext(tc.ctx, tc.adminDB)\n\t\t\tctx = acme.NewDatabaseContext(ctx, tc.acmeDB)\n\t\t\tpar := NewPolicyAdminResponder()\n\n\t\t\treq := httptest.NewRequest(\"POST\", \"/foo\", io.NopCloser(bytes.NewBuffer(tc.body)))\n\t\t\treq = req.WithContext(ctx)\n\t\t\tw := httptest.NewRecorder()\n\n\t\t\tpar.UpdateACMEAccountPolicy(w, req)\n\t\t\tres := w.Result()\n\n\t\t\tassert.Equal(t, tc.statusCode, res.StatusCode)\n\n\t\t\tif res.StatusCode >= 400 {\n\n\t\t\t\tbody, err := io.ReadAll(res.Body)\n\t\t\t\tres.Body.Close()\n\t\t\t\tassert.NoError(t, err)\n\n\t\t\t\tae := testAdminError{}\n\t\t\t\tassert.NoError(t, json.Unmarshal(bytes.TrimSpace(body), &ae))\n\n\t\t\t\tassert.Equal(t, tc.err.Type, ae.Type)\n\t\t\t\tassert.Equal(t, tc.err.StatusCode(), res.StatusCode)\n\t\t\t\tassert.Equal(t, tc.err.Detail, ae.Detail)\n\t\t\t\tassert.Equal(t, []string{\"application/json\"}, res.Header[\"Content-Type\"])\n\n\t\t\t\t// when the error message starts with \"proto\", we expect it to have\n\t\t\t\t// a syntax error (in the tests). If the message doesn't start with \"proto\",\n\t\t\t\t// we expect a full string match.\n\t\t\t\tif strings.HasPrefix(tc.err.Message, \"proto:\") {\n\t\t\t\t\tassert.True(t, strings.Contains(ae.Message, \"syntax error\"))\n\t\t\t\t} else {\n\t\t\t\t\tassert.Equal(t, tc.err.Message, ae.Message)\n\t\t\t\t}\n\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tp := &testPolicyResponse{}\n\t\t\tbody, err := io.ReadAll(res.Body)\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.NoError(t, json.Unmarshal(body, &p))\n\n\t\t\tassert.Equal(t, tc.response, p)\n\n\t\t})\n\t}\n}\n\nfunc TestPolicyAdminResponder_DeleteACMEAccountPolicy(t *testing.T) {\n\ttype test struct {\n\t\tbody       []byte\n\t\tadminDB    admin.DB\n\t\tctx        context.Context\n\t\tacmeDB     acme.DB\n\t\terr        *admin.Error\n\t\tstatusCode int\n\t}\n\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/linkedca\": func(t *testing.T) test {\n\t\t\tctx := context.Background()\n\t\t\terr := admin.NewError(admin.ErrorNotImplementedType, \"policy operations not yet supported in linked deployments\")\n\t\t\terr.Message = \"policy operations not yet supported in linked deployments\"\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\tadminDB:    &fakeLinkedCA{},\n\t\t\t\terr:        err,\n\t\t\t\tstatusCode: 501,\n\t\t\t}\n\t\t},\n\t\t\"fail/no-existing-policy\": func(t *testing.T) test {\n\t\t\tprov := &linkedca.Provisioner{\n\t\t\t\tName: \"provName\",\n\t\t\t}\n\t\t\teak := &linkedca.EABKey{\n\t\t\t\tId: \"eakID\",\n\t\t\t}\n\t\t\tctx := linkedca.NewContextWithProvisioner(context.Background(), prov)\n\t\t\tctx = linkedca.NewContextWithExternalAccountKey(ctx, eak)\n\t\t\terr := admin.NewError(admin.ErrorNotFoundType, \"ACME EAK policy does not exist\")\n\t\t\terr.Message = \"ACME EAK policy does not exist\"\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\tadminDB:    &admin.MockDB{},\n\t\t\t\terr:        err,\n\t\t\t\tstatusCode: 404,\n\t\t\t}\n\t\t},\n\t\t\"fail/acmeDB.UpdateExternalAccountKey-error\": func(t *testing.T) test {\n\t\t\tpolicy := &linkedca.Policy{\n\t\t\t\tX509: &linkedca.X509Policy{\n\t\t\t\t\tAllow: &linkedca.X509Names{\n\t\t\t\t\t\tDns: []string{\"*.local\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t\tprov := &linkedca.Provisioner{\n\t\t\t\tName: \"provName\",\n\t\t\t\tId:   \"provID\",\n\t\t\t}\n\t\t\teak := &linkedca.EABKey{\n\t\t\t\tId:     \"eakID\",\n\t\t\t\tPolicy: policy,\n\t\t\t}\n\t\t\tctx := linkedca.NewContextWithProvisioner(context.Background(), prov)\n\t\t\tctx = linkedca.NewContextWithExternalAccountKey(ctx, eak)\n\t\t\terr := admin.NewErrorISE(\"error deleting ACME EAK policy: force\")\n\t\t\terr.Message = \"error deleting ACME EAK policy: force\"\n\t\t\treturn test{\n\t\t\t\tctx:     ctx,\n\t\t\t\tadminDB: &admin.MockDB{},\n\t\t\t\tacmeDB: &acme.MockDB{\n\t\t\t\t\tMockUpdateExternalAccountKey: func(ctx context.Context, provisionerID string, eak *acme.ExternalAccountKey) error {\n\t\t\t\t\t\tassert.Equal(t, \"provID\", provisionerID)\n\t\t\t\t\t\tassert.Equal(t, \"eakID\", eak.ID)\n\t\t\t\t\t\treturn errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr:        err,\n\t\t\t\tstatusCode: 500,\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tpolicy := &linkedca.Policy{\n\t\t\t\tX509: &linkedca.X509Policy{\n\t\t\t\t\tAllow: &linkedca.X509Names{\n\t\t\t\t\t\tDns: []string{\"*.local\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t\tprov := &linkedca.Provisioner{\n\t\t\t\tName: \"provName\",\n\t\t\t\tId:   \"provID\",\n\t\t\t}\n\t\t\teak := &linkedca.EABKey{\n\t\t\t\tId:     \"eakID\",\n\t\t\t\tPolicy: policy,\n\t\t\t}\n\t\t\tctx := linkedca.NewContextWithProvisioner(context.Background(), prov)\n\t\t\tctx = linkedca.NewContextWithExternalAccountKey(ctx, eak)\n\t\t\treturn test{\n\t\t\t\tctx:     ctx,\n\t\t\t\tadminDB: &admin.MockDB{},\n\t\t\t\tacmeDB: &acme.MockDB{\n\t\t\t\t\tMockUpdateExternalAccountKey: func(ctx context.Context, provisionerID string, eak *acme.ExternalAccountKey) error {\n\t\t\t\t\t\tassert.Equal(t, \"provID\", provisionerID)\n\t\t\t\t\t\tassert.Equal(t, \"eakID\", eak.ID)\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tstatusCode: 200,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, prep := range tests {\n\t\ttc := prep(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tctx := admin.NewContext(tc.ctx, tc.adminDB)\n\t\t\tctx = acme.NewDatabaseContext(ctx, tc.acmeDB)\n\t\t\tpar := NewPolicyAdminResponder()\n\n\t\t\treq := httptest.NewRequest(\"POST\", \"/foo\", io.NopCloser(bytes.NewBuffer(tc.body)))\n\t\t\treq = req.WithContext(ctx)\n\t\t\tw := httptest.NewRecorder()\n\n\t\t\tpar.DeleteACMEAccountPolicy(w, req)\n\t\t\tres := w.Result()\n\n\t\t\tassert.Equal(t, tc.statusCode, res.StatusCode)\n\n\t\t\tif res.StatusCode >= 400 {\n\n\t\t\t\tbody, err := io.ReadAll(res.Body)\n\t\t\t\tres.Body.Close()\n\t\t\t\tassert.NoError(t, err)\n\n\t\t\t\tae := testAdminError{}\n\t\t\t\tassert.NoError(t, json.Unmarshal(bytes.TrimSpace(body), &ae))\n\n\t\t\t\tassert.Equal(t, tc.err.Type, ae.Type)\n\t\t\t\tassert.Equal(t, tc.err.Message, ae.Message)\n\t\t\t\tassert.Equal(t, tc.err.StatusCode(), res.StatusCode)\n\t\t\t\tassert.Equal(t, tc.err.Detail, ae.Detail)\n\t\t\t\tassert.Equal(t, []string{\"application/json\"}, res.Header[\"Content-Type\"])\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tbody, err := io.ReadAll(res.Body)\n\t\t\tassert.NoError(t, err)\n\t\t\tres.Body.Close()\n\t\t\tresponse := DeleteResponse{}\n\t\t\tassert.NoError(t, json.Unmarshal(bytes.TrimSpace(body), &response))\n\t\t\tassert.Equal(t, \"ok\", response.Status)\n\t\t\tassert.Equal(t, []string{\"application/json\"}, res.Header[\"Content-Type\"])\n\n\t\t})\n\t}\n}\n\nfunc Test_isBadRequest(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\terr  error\n\t\twant bool\n\t}{\n\t\t{\n\t\t\tname: \"nil\",\n\t\t\terr:  nil,\n\t\t\twant: false,\n\t\t},\n\t\t{\n\t\t\tname: \"no-policy-error\",\n\t\t\terr:  errors.New(\"some error\"),\n\t\t\twant: false,\n\t\t},\n\t\t{\n\t\t\tname: \"no-bad-request\",\n\t\t\terr: &authority.PolicyError{\n\t\t\t\tTyp: authority.InternalFailure,\n\t\t\t\tErr: errors.New(\"error\"),\n\t\t\t},\n\t\t\twant: false,\n\t\t},\n\t\t{\n\t\t\tname: \"bad-request\",\n\t\t\terr: &authority.PolicyError{\n\t\t\t\tTyp: authority.AdminLockOut,\n\t\t\t\tErr: errors.New(\"admin lock out\"),\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif got := isBadRequest(tt.err); got != tt.want {\n\t\t\t\tt.Errorf(\"isBadRequest() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_validatePolicy(t *testing.T) {\n\ttype args struct {\n\t\tp *linkedca.Policy\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\targs    args\n\t\twantErr bool\n\t}{\n\t\t{\n\t\t\tname: \"nil\",\n\t\t\targs: args{\n\t\t\t\tp: nil,\n\t\t\t},\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"x509\",\n\t\t\targs: args{\n\t\t\t\tp: &linkedca.Policy{\n\t\t\t\t\tX509: &linkedca.X509Policy{\n\t\t\t\t\t\tAllow: &linkedca.X509Names{\n\t\t\t\t\t\t\tDns: []string{\"**.local\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twantErr: true,\n\t\t},\n\t\t{\n\t\t\tname: \"ssh user\",\n\t\t\targs: args{\n\t\t\t\tp: &linkedca.Policy{\n\t\t\t\t\tSsh: &linkedca.SSHPolicy{\n\t\t\t\t\t\tUser: &linkedca.SSHUserPolicy{\n\t\t\t\t\t\t\tAllow: &linkedca.SSHUserNames{\n\t\t\t\t\t\t\t\tEmails: []string{\"@@example.com\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twantErr: true,\n\t\t},\n\t\t{\n\t\t\tname: \"ssh host\",\n\t\t\targs: args{\n\t\t\t\tp: &linkedca.Policy{\n\t\t\t\t\tSsh: &linkedca.SSHPolicy{\n\t\t\t\t\t\tHost: &linkedca.SSHHostPolicy{\n\t\t\t\t\t\t\tAllow: &linkedca.SSHHostNames{\n\t\t\t\t\t\t\t\tDns: []string{\"**.local\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twantErr: true,\n\t\t},\n\t\t{\n\t\t\tname: \"ok\",\n\t\t\targs: args{\n\t\t\t\tp: &linkedca.Policy{\n\t\t\t\t\tX509: &linkedca.X509Policy{\n\t\t\t\t\t\tAllow: &linkedca.X509Names{\n\t\t\t\t\t\t\tDns: []string{\"*.local\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tSsh: &linkedca.SSHPolicy{\n\t\t\t\t\t\tUser: &linkedca.SSHUserPolicy{\n\t\t\t\t\t\t\tAllow: &linkedca.SSHUserNames{\n\t\t\t\t\t\t\t\tEmails: []string{\"@example.com\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tHost: &linkedca.SSHHostPolicy{\n\t\t\t\t\t\t\tAllow: &linkedca.SSHHostNames{\n\t\t\t\t\t\t\t\tDns: []string{\"*.local\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twantErr: false,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif err := validatePolicy(tt.args.p); (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"validatePolicy() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "authority/admin/api/provisioner.go",
    "content": "package api\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\n\t\"github.com/go-chi/chi/v5\"\n\n\t\"github.com/smallstep/linkedca\"\n\t\"go.step.sm/crypto/sshutil\"\n\t\"go.step.sm/crypto/x509util\"\n\n\t\"github.com/smallstep/certificates/api\"\n\t\"github.com/smallstep/certificates/api/read\"\n\t\"github.com/smallstep/certificates/api/render\"\n\t\"github.com/smallstep/certificates/authority\"\n\t\"github.com/smallstep/certificates/authority/admin\"\n\t\"github.com/smallstep/certificates/authority/provisioner\"\n\t\"github.com/smallstep/certificates/errs\"\n)\n\n// GetProvisionersResponse is the type for GET /admin/provisioners responses.\ntype GetProvisionersResponse struct {\n\tProvisioners provisioner.List `json:\"provisioners\"`\n\tNextCursor   string           `json:\"nextCursor\"`\n}\n\n// GetProvisioner returns the requested provisioner, or an error.\nfunc GetProvisioner(w http.ResponseWriter, r *http.Request) {\n\tvar (\n\t\tp   provisioner.Interface\n\t\terr error\n\t)\n\n\tctx := r.Context()\n\tid := r.URL.Query().Get(\"id\")\n\tname := chi.URLParam(r, \"name\")\n\tauth := mustAuthority(ctx)\n\tdb := admin.MustFromContext(ctx)\n\n\tif id != \"\" {\n\t\tif p, err = auth.LoadProvisionerByID(id); err != nil {\n\t\t\trender.Error(w, r, admin.WrapErrorISE(err, \"error loading provisioner %s\", id))\n\t\t\treturn\n\t\t}\n\t} else {\n\t\tif p, err = auth.LoadProvisionerByName(name); err != nil {\n\t\t\trender.Error(w, r, admin.WrapErrorISE(err, \"error loading provisioner %s\", name))\n\t\t\treturn\n\t\t}\n\t}\n\n\tprov, err := db.GetProvisioner(ctx, p.GetID())\n\tif err != nil {\n\t\trender.Error(w, r, err)\n\t\treturn\n\t}\n\trender.ProtoJSON(w, prov)\n}\n\n// GetProvisioners returns the given segment of  provisioners associated with the authority.\nfunc GetProvisioners(w http.ResponseWriter, r *http.Request) {\n\tcursor, limit, err := api.ParseCursor(r)\n\tif err != nil {\n\t\trender.Error(w, r, admin.WrapError(admin.ErrorBadRequestType, err,\n\t\t\t\"error parsing cursor and limit from query params\"))\n\t\treturn\n\t}\n\n\tp, next, err := mustAuthority(r.Context()).GetProvisioners(cursor, limit)\n\tif err != nil {\n\t\trender.Error(w, r, errs.InternalServerErr(err))\n\t\treturn\n\t}\n\trender.JSON(w, r, &GetProvisionersResponse{\n\t\tProvisioners: p,\n\t\tNextCursor:   next,\n\t})\n}\n\n// CreateProvisioner creates a new prov.\nfunc CreateProvisioner(w http.ResponseWriter, r *http.Request) {\n\tvar prov = new(linkedca.Provisioner)\n\tif err := read.ProtoJSON(r.Body, prov); err != nil {\n\t\trender.Error(w, r, err)\n\t\treturn\n\t}\n\n\t// TODO: Validate inputs\n\tif err := authority.ValidateClaims(prov.Claims); err != nil {\n\t\trender.Error(w, r, err)\n\t\treturn\n\t}\n\n\t// validate the templates and template data\n\tif err := validateTemplates(prov.X509Template, prov.SshTemplate); err != nil {\n\t\trender.Error(w, r, admin.WrapError(admin.ErrorBadRequestType, err, \"invalid template\"))\n\t\treturn\n\t}\n\n\tif err := mustAuthority(r.Context()).StoreProvisioner(r.Context(), prov); err != nil {\n\t\trender.Error(w, r, admin.WrapErrorISE(err, \"error storing provisioner %s\", prov.Name))\n\t\treturn\n\t}\n\trender.ProtoJSONStatus(w, prov, http.StatusCreated)\n}\n\n// DeleteProvisioner deletes a provisioner.\nfunc DeleteProvisioner(w http.ResponseWriter, r *http.Request) {\n\tvar (\n\t\tp   provisioner.Interface\n\t\terr error\n\t)\n\n\tid := r.URL.Query().Get(\"id\")\n\tname := chi.URLParam(r, \"name\")\n\tauth := mustAuthority(r.Context())\n\n\tif id != \"\" {\n\t\tif p, err = auth.LoadProvisionerByID(id); err != nil {\n\t\t\trender.Error(w, r, admin.WrapErrorISE(err, \"error loading provisioner %s\", id))\n\t\t\treturn\n\t\t}\n\t} else {\n\t\tif p, err = auth.LoadProvisionerByName(name); err != nil {\n\t\t\trender.Error(w, r, admin.WrapErrorISE(err, \"error loading provisioner %s\", name))\n\t\t\treturn\n\t\t}\n\t}\n\n\tif err := auth.RemoveProvisioner(r.Context(), p.GetID()); err != nil {\n\t\trender.Error(w, r, admin.WrapErrorISE(err, \"error removing provisioner %s\", p.GetName()))\n\t\treturn\n\t}\n\n\trender.JSON(w, r, &DeleteResponse{Status: \"ok\"})\n}\n\n// UpdateProvisioner updates an existing prov.\nfunc UpdateProvisioner(w http.ResponseWriter, r *http.Request) {\n\tvar nu = new(linkedca.Provisioner)\n\tif err := read.ProtoJSON(r.Body, nu); err != nil {\n\t\trender.Error(w, r, err)\n\t\treturn\n\t}\n\n\tctx := r.Context()\n\tname := chi.URLParam(r, \"name\")\n\tauth := mustAuthority(ctx)\n\tdb := admin.MustFromContext(ctx)\n\n\tp, err := auth.LoadProvisionerByName(name)\n\tif err != nil {\n\t\trender.Error(w, r, admin.WrapErrorISE(err, \"error loading provisioner from cached configuration '%s'\", name))\n\t\treturn\n\t}\n\n\told, err := db.GetProvisioner(r.Context(), p.GetID())\n\tif err != nil {\n\t\trender.Error(w, r, admin.WrapErrorISE(err, \"error loading provisioner from db '%s'\", p.GetID()))\n\t\treturn\n\t}\n\n\tif nu.Id != old.Id {\n\t\trender.Error(w, r, admin.NewErrorISE(\"cannot change provisioner ID\"))\n\t\treturn\n\t}\n\tif nu.Type != old.Type {\n\t\trender.Error(w, r, admin.NewErrorISE(\"cannot change provisioner type\"))\n\t\treturn\n\t}\n\tif nu.AuthorityId != old.AuthorityId {\n\t\trender.Error(w, r, admin.NewErrorISE(\"cannot change provisioner authorityID\"))\n\t\treturn\n\t}\n\tif !nu.CreatedAt.AsTime().Equal(old.CreatedAt.AsTime()) {\n\t\trender.Error(w, r, admin.NewErrorISE(\"cannot change provisioner createdAt\"))\n\t\treturn\n\t}\n\tif !nu.DeletedAt.AsTime().Equal(old.DeletedAt.AsTime()) {\n\t\trender.Error(w, r, admin.NewErrorISE(\"cannot change provisioner deletedAt\"))\n\t\treturn\n\t}\n\n\t// TODO: Validate inputs\n\tif err := authority.ValidateClaims(nu.Claims); err != nil {\n\t\trender.Error(w, r, err)\n\t\treturn\n\t}\n\n\t// validate the templates and template data\n\tif err := validateTemplates(nu.X509Template, nu.SshTemplate); err != nil {\n\t\trender.Error(w, r, admin.WrapError(admin.ErrorBadRequestType, err, \"invalid template\"))\n\t\treturn\n\t}\n\n\tif err := auth.UpdateProvisioner(r.Context(), nu); err != nil {\n\t\trender.Error(w, r, err)\n\t\treturn\n\t}\n\trender.ProtoJSON(w, nu)\n}\n\n// validateTemplates validates the X.509 and SSH templates and template data if set.\nfunc validateTemplates(x509, ssh *linkedca.Template) error {\n\tif x509 != nil {\n\t\tif len(x509.Template) > 0 {\n\t\t\tif err := x509util.ValidateTemplate(x509.Template); err != nil {\n\t\t\t\treturn fmt.Errorf(\"invalid X.509 template: %w\", err)\n\t\t\t}\n\t\t}\n\t\tif len(x509.Data) > 0 {\n\t\t\tif err := x509util.ValidateTemplateData(x509.Data); err != nil {\n\t\t\t\treturn fmt.Errorf(\"invalid X.509 template data: %w\", err)\n\t\t\t}\n\t\t}\n\t}\n\n\tif ssh != nil {\n\t\tif len(ssh.Template) > 0 {\n\t\t\tif err := sshutil.ValidateTemplate(ssh.Template); err != nil {\n\t\t\t\treturn fmt.Errorf(\"invalid SSH template: %w\", err)\n\t\t\t}\n\t\t}\n\n\t\tif len(ssh.Data) > 0 {\n\t\t\tif err := sshutil.ValidateTemplateData(ssh.Data); err != nil {\n\t\t\t\treturn fmt.Errorf(\"invalid SSH template data: %w\", err)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "authority/admin/api/provisioner_test.go",
    "content": "package api\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/go-chi/chi/v5\"\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/google/go-cmp/cmp/cmpopts\"\n\t\"google.golang.org/protobuf/encoding/protojson\"\n\t\"google.golang.org/protobuf/types/known/timestamppb\"\n\n\t\"github.com/smallstep/assert\"\n\t\"github.com/smallstep/linkedca\"\n\n\t\"github.com/smallstep/certificates/authority/admin\"\n\t\"github.com/smallstep/certificates/authority/provisioner\"\n)\n\nfunc TestHandler_GetProvisioner(t *testing.T) {\n\ttype test struct {\n\t\tctx        context.Context\n\t\tauth       adminAuthority\n\t\tadminDB    admin.DB\n\t\treq        *http.Request\n\t\tstatusCode int\n\t\terr        *admin.Error\n\t\tprov       *linkedca.Provisioner\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/auth.LoadProvisionerByID\": func(t *testing.T) test {\n\t\t\treq := httptest.NewRequest(\"GET\", \"/foo?id=provID\", http.NoBody)\n\t\t\tchiCtx := chi.NewRouteContext()\n\t\t\tctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)\n\t\t\tauth := &mockAdminAuthority{\n\t\t\t\tMockLoadProvisionerByID: func(id string) (provisioner.Interface, error) {\n\t\t\t\t\tassert.Equals(t, \"provID\", id)\n\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t},\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\treq:        req,\n\t\t\t\tauth:       auth,\n\t\t\t\tadminDB:    &admin.MockDB{},\n\t\t\t\tstatusCode: 500,\n\t\t\t\terr: &admin.Error{\n\t\t\t\t\tType:    admin.ErrorServerInternalType.String(),\n\t\t\t\t\tStatus:  500,\n\t\t\t\t\tDetail:  \"the server experienced an internal error\",\n\t\t\t\t\tMessage: \"error loading provisioner provID: force\",\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"fail/auth.LoadProvisionerByName\": func(t *testing.T) test {\n\t\t\treq := httptest.NewRequest(\"GET\", \"/foo\", http.NoBody)\n\t\t\tchiCtx := chi.NewRouteContext()\n\t\t\tchiCtx.URLParams.Add(\"name\", \"provName\")\n\t\t\tctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)\n\t\t\tauth := &mockAdminAuthority{\n\t\t\t\tMockLoadProvisionerByName: func(name string) (provisioner.Interface, error) {\n\t\t\t\t\tassert.Equals(t, \"provName\", name)\n\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t},\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\treq:        req,\n\t\t\t\tauth:       auth,\n\t\t\t\tadminDB:    &admin.MockDB{},\n\t\t\t\tstatusCode: 500,\n\t\t\t\terr: &admin.Error{\n\t\t\t\t\tType:    admin.ErrorServerInternalType.String(),\n\t\t\t\t\tStatus:  500,\n\t\t\t\t\tDetail:  \"the server experienced an internal error\",\n\t\t\t\t\tMessage: \"error loading provisioner provName: force\",\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"fail/db.GetProvisioner\": func(t *testing.T) test {\n\t\t\treq := httptest.NewRequest(\"GET\", \"/foo\", http.NoBody)\n\t\t\tchiCtx := chi.NewRouteContext()\n\t\t\tchiCtx.URLParams.Add(\"name\", \"provName\")\n\t\t\tctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)\n\t\t\tauth := &mockAdminAuthority{\n\t\t\t\tMockLoadProvisionerByName: func(name string) (provisioner.Interface, error) {\n\t\t\t\t\tassert.Equals(t, \"provName\", name)\n\t\t\t\t\treturn &provisioner.ACME{\n\t\t\t\t\t\tID:   \"acmeID\",\n\t\t\t\t\t\tName: \"provName\",\n\t\t\t\t\t}, nil\n\t\t\t\t},\n\t\t\t}\n\t\t\tdb := &admin.MockDB{\n\t\t\t\tMockGetProvisioner: func(ctx context.Context, id string) (*linkedca.Provisioner, error) {\n\t\t\t\t\tassert.Equals(t, \"acmeID\", id)\n\t\t\t\t\treturn nil, admin.NewErrorISE(\"error loading provisioner provName: force\")\n\t\t\t\t},\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\treq:        req,\n\t\t\t\tauth:       auth,\n\t\t\t\tadminDB:    db,\n\t\t\t\tstatusCode: 500,\n\t\t\t\terr: &admin.Error{\n\t\t\t\t\tType:    admin.ErrorServerInternalType.String(),\n\t\t\t\t\tStatus:  500,\n\t\t\t\t\tDetail:  \"the server experienced an internal error\",\n\t\t\t\t\tMessage: \"error loading provisioner provName: force\",\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\treq := httptest.NewRequest(\"GET\", \"/foo\", http.NoBody)\n\t\t\tchiCtx := chi.NewRouteContext()\n\t\t\tchiCtx.URLParams.Add(\"name\", \"provName\")\n\t\t\tctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)\n\t\t\tauth := &mockAdminAuthority{\n\t\t\t\tMockLoadProvisionerByName: func(name string) (provisioner.Interface, error) {\n\t\t\t\t\tassert.Equals(t, \"provName\", name)\n\t\t\t\t\treturn &provisioner.ACME{\n\t\t\t\t\t\tID:   \"acmeID\",\n\t\t\t\t\t\tName: \"provName\",\n\t\t\t\t\t}, nil\n\t\t\t\t},\n\t\t\t}\n\t\t\tprov := &linkedca.Provisioner{\n\t\t\t\tId:   \"acmeID\",\n\t\t\t\tType: linkedca.Provisioner_ACME,\n\t\t\t\tName: \"provName\", // TODO(hs): other fields too?\n\t\t\t}\n\t\t\tdb := &admin.MockDB{\n\t\t\t\tMockGetProvisioner: func(ctx context.Context, id string) (*linkedca.Provisioner, error) {\n\t\t\t\t\tassert.Equals(t, \"acmeID\", id)\n\t\t\t\t\treturn prov, nil\n\t\t\t\t},\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\treq:        req,\n\t\t\t\tauth:       auth,\n\t\t\t\tadminDB:    db,\n\t\t\t\tstatusCode: 200,\n\t\t\t\terr:        nil,\n\t\t\t\tprov:       prov,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, prep := range tests {\n\t\ttc := prep(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tmockMustAuthority(t, tc.auth)\n\t\t\tctx := admin.NewContext(tc.ctx, tc.adminDB)\n\t\t\treq := tc.req.WithContext(ctx)\n\t\t\tw := httptest.NewRecorder()\n\t\t\tGetProvisioner(w, req)\n\t\t\tres := w.Result()\n\n\t\t\tassert.Equals(t, tc.statusCode, res.StatusCode)\n\n\t\t\tif res.StatusCode >= 400 {\n\n\t\t\t\tbody, err := io.ReadAll(res.Body)\n\t\t\t\tres.Body.Close()\n\t\t\t\tassert.FatalError(t, err)\n\n\t\t\t\tadminErr := admin.Error{}\n\t\t\t\tassert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &adminErr))\n\n\t\t\t\tassert.Equals(t, tc.err.Type, adminErr.Type)\n\t\t\t\tassert.Equals(t, tc.err.Message, adminErr.Message)\n\t\t\t\tassert.Equals(t, tc.err.Detail, adminErr.Detail)\n\t\t\t\tassert.Equals(t, []string{\"application/json\"}, res.Header[\"Content-Type\"])\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tprov := &linkedca.Provisioner{}\n\t\t\terr := readProtoJSON(res.Body, prov)\n\t\t\tassert.FatalError(t, err)\n\n\t\t\tassert.Equals(t, []string{\"application/json\"}, res.Header[\"Content-Type\"])\n\n\t\t\topts := []cmp.Option{cmpopts.IgnoreUnexported(linkedca.Provisioner{}, timestamppb.Timestamp{})}\n\t\t\tif !cmp.Equal(tc.prov, prov, opts...) {\n\t\t\t\tt.Errorf(\"h.GetProvisioner diff =\\n%s\", cmp.Diff(tc.prov, prov, opts...))\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestHandler_GetProvisioners(t *testing.T) {\n\ttype test struct {\n\t\tctx        context.Context\n\t\tauth       adminAuthority\n\t\treq        *http.Request\n\t\tstatusCode int\n\t\terr        *admin.Error\n\t\tresp       GetProvisionersResponse\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/parse-cursor\": func(t *testing.T) test {\n\t\t\treq := httptest.NewRequest(\"GET\", \"/foo?limit=X\", http.NoBody)\n\t\t\treturn test{\n\t\t\t\tctx:        context.Background(),\n\t\t\t\tstatusCode: 400,\n\t\t\t\treq:        req,\n\t\t\t\terr: &admin.Error{\n\t\t\t\t\tStatus:  400,\n\t\t\t\t\tType:    admin.ErrorBadRequestType.String(),\n\t\t\t\t\tDetail:  \"bad request\",\n\t\t\t\t\tMessage: \"error parsing cursor and limit from query params: limit 'X' is not an integer: strconv.Atoi: parsing \\\"X\\\": invalid syntax\",\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"fail/auth.GetProvisioners\": func(t *testing.T) test {\n\t\t\treq := httptest.NewRequest(\"GET\", \"/foo\", http.NoBody)\n\t\t\tauth := &mockAdminAuthority{\n\t\t\t\tMockGetProvisioners: func(cursor string, limit int) (provisioner.List, string, error) {\n\t\t\t\t\tassert.Equals(t, \"\", cursor)\n\t\t\t\t\tassert.Equals(t, 0, limit)\n\t\t\t\t\treturn nil, \"\", errors.New(\"force\")\n\t\t\t\t},\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tctx:        context.Background(),\n\t\t\t\treq:        req,\n\t\t\t\tauth:       auth,\n\t\t\t\tstatusCode: 500,\n\t\t\t\terr: &admin.Error{\n\t\t\t\t\tType:    \"\",\n\t\t\t\t\tStatus:  500,\n\t\t\t\t\tDetail:  \"\",\n\t\t\t\t\tMessage: \"The certificate authority encountered an Internal Server Error. Please see the certificate authority logs for more info.\",\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\treq := httptest.NewRequest(\"GET\", \"/foo\", http.NoBody)\n\t\t\tprovisioners := provisioner.List{\n\t\t\t\t&provisioner.OIDC{\n\t\t\t\t\tType: \"OIDC\",\n\t\t\t\t\tName: \"oidcProv\",\n\t\t\t\t},\n\t\t\t\t&provisioner.ACME{\n\t\t\t\t\tType:       \"ACME\",\n\t\t\t\t\tName:       \"provName\",\n\t\t\t\t\tForceCN:    false,\n\t\t\t\t\tRequireEAB: false,\n\t\t\t\t},\n\t\t\t}\n\t\t\tauth := &mockAdminAuthority{\n\t\t\t\tMockGetProvisioners: func(cursor string, limit int) (provisioner.List, string, error) {\n\t\t\t\t\tassert.Equals(t, \"\", cursor)\n\t\t\t\t\tassert.Equals(t, 0, limit)\n\t\t\t\t\treturn provisioners, \"nextCursorValue\", nil\n\t\t\t\t},\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tctx:        context.Background(),\n\t\t\t\treq:        req,\n\t\t\t\tauth:       auth,\n\t\t\t\tstatusCode: 200,\n\t\t\t\terr:        nil,\n\t\t\t\tresp: GetProvisionersResponse{\n\t\t\t\t\tProvisioners: provisioners,\n\t\t\t\t\tNextCursor:   \"nextCursorValue\",\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t}\n\tfor name, prep := range tests {\n\t\ttc := prep(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tmockMustAuthority(t, tc.auth)\n\t\t\treq := tc.req.WithContext(tc.ctx)\n\t\t\tw := httptest.NewRecorder()\n\t\t\tGetProvisioners(w, req)\n\t\t\tres := w.Result()\n\n\t\t\tassert.Equals(t, tc.statusCode, res.StatusCode)\n\n\t\t\tif res.StatusCode >= 400 {\n\n\t\t\t\tbody, err := io.ReadAll(res.Body)\n\t\t\t\tres.Body.Close()\n\t\t\t\tassert.FatalError(t, err)\n\n\t\t\t\tadminErr := admin.Error{}\n\t\t\t\tassert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &adminErr))\n\n\t\t\t\tassert.Equals(t, tc.err.Type, adminErr.Type)\n\t\t\t\tassert.Equals(t, tc.err.Message, adminErr.Message)\n\t\t\t\tassert.Equals(t, tc.err.Detail, adminErr.Detail)\n\t\t\t\tassert.Equals(t, []string{\"application/json\"}, res.Header[\"Content-Type\"])\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tbody, err := io.ReadAll(res.Body)\n\t\t\tres.Body.Close()\n\t\t\tassert.FatalError(t, err)\n\n\t\t\tresponse := GetProvisionersResponse{}\n\t\t\tassert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &response))\n\n\t\t\tassert.Equals(t, []string{\"application/json\"}, res.Header[\"Content-Type\"])\n\n\t\t\topts := []cmp.Option{cmpopts.IgnoreUnexported(provisioner.ACME{}, provisioner.OIDC{})}\n\t\t\tif !cmp.Equal(tc.resp, response, opts...) {\n\t\t\t\tt.Errorf(\"h.GetProvisioners diff =\\n%s\", cmp.Diff(tc.resp, response, opts...))\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestHandler_CreateProvisioner(t *testing.T) {\n\ttype test struct {\n\t\tctx        context.Context\n\t\tauth       adminAuthority\n\t\tbody       []byte\n\t\tstatusCode int\n\t\terr        *admin.Error\n\t\tprov       *linkedca.Provisioner\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/readProtoJSON\": func(t *testing.T) test {\n\t\t\tbody := []byte(\"{!?}\")\n\t\t\treturn test{\n\t\t\t\tctx:        context.Background(),\n\t\t\t\tbody:       body,\n\t\t\t\tstatusCode: 400,\n\t\t\t\terr: &admin.Error{\n\t\t\t\t\tType:    \"badRequest\",\n\t\t\t\t\tStatus:  400,\n\t\t\t\t\tDetail:  \"bad request\",\n\t\t\t\t\tMessage: \"proto: syntax error (line 1:2): invalid value !\",\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t// TODO(hs): ValidateClaims can't be mocked atm\n\t\t// \"fail/authority.ValidateClaims\": func(t *testing.T) test {\n\t\t// \treturn test{}\n\t\t// },\n\t\t\"fail/validateTemplates\": func(t *testing.T) test {\n\t\t\tprov := &linkedca.Provisioner{\n\t\t\t\tId:   \"provID\",\n\t\t\t\tType: linkedca.Provisioner_OIDC,\n\t\t\t\tName: \"provName\",\n\t\t\t\tX509Template: &linkedca.Template{\n\t\t\t\t\tTemplate: []byte(`{ {{missingFunction \"foo\"}} }`),\n\t\t\t\t},\n\t\t\t}\n\t\t\tbody, err := protojson.Marshal(prov)\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tctx:        context.Background(),\n\t\t\t\tbody:       body,\n\t\t\t\tstatusCode: 400,\n\t\t\t\terr: &admin.Error{\n\t\t\t\t\tType:    \"badRequest\",\n\t\t\t\t\tStatus:  400,\n\t\t\t\t\tDetail:  \"bad request\",\n\t\t\t\t\tMessage: \"invalid template: invalid X.509 template: error parsing template: template: template:1: function \\\"missingFunction\\\" not defined\",\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"fail/auth.StoreProvisioner\": func(t *testing.T) test {\n\t\t\tprov := &linkedca.Provisioner{\n\t\t\t\tId:   \"provID\",\n\t\t\t\tType: linkedca.Provisioner_OIDC,\n\t\t\t\tName: \"provName\",\n\t\t\t}\n\t\t\tbody, err := protojson.Marshal(prov)\n\t\t\tassert.FatalError(t, err)\n\t\t\tauth := &mockAdminAuthority{\n\t\t\t\tMockStoreProvisioner: func(ctx context.Context, prov *linkedca.Provisioner) error {\n\t\t\t\t\tassert.Equals(t, \"provID\", prov.Id)\n\t\t\t\t\treturn errors.New(\"force\")\n\t\t\t\t},\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tctx:        context.Background(),\n\t\t\t\tbody:       body,\n\t\t\t\tauth:       auth,\n\t\t\t\tstatusCode: 500,\n\t\t\t\terr: &admin.Error{\n\t\t\t\t\tType:    admin.ErrorServerInternalType.String(),\n\t\t\t\t\tStatus:  500,\n\t\t\t\t\tDetail:  \"the server experienced an internal error\",\n\t\t\t\t\tMessage: \"error storing provisioner provName: force\",\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tprov := &linkedca.Provisioner{\n\t\t\t\tId:   \"provID\",\n\t\t\t\tType: linkedca.Provisioner_OIDC,\n\t\t\t\tName: \"provName\",\n\t\t\t}\n\t\t\tbody, err := protojson.Marshal(prov)\n\t\t\tassert.FatalError(t, err)\n\t\t\tauth := &mockAdminAuthority{\n\t\t\t\tMockStoreProvisioner: func(ctx context.Context, prov *linkedca.Provisioner) error {\n\t\t\t\t\tassert.Equals(t, \"provID\", prov.Id)\n\t\t\t\t\treturn nil\n\t\t\t\t},\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tctx:        context.Background(),\n\t\t\t\tbody:       body,\n\t\t\t\tauth:       auth,\n\t\t\t\tstatusCode: 201,\n\t\t\t\terr:        nil,\n\t\t\t\tprov:       prov,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, prep := range tests {\n\t\ttc := prep(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tmockMustAuthority(t, tc.auth)\n\t\t\treq := httptest.NewRequest(\"POST\", \"/foo\", io.NopCloser(bytes.NewBuffer(tc.body)))\n\t\t\treq = req.WithContext(tc.ctx)\n\t\t\tw := httptest.NewRecorder()\n\t\t\tCreateProvisioner(w, req)\n\t\t\tres := w.Result()\n\n\t\t\tassert.Equals(t, tc.statusCode, res.StatusCode)\n\n\t\t\tif res.StatusCode >= 400 {\n\n\t\t\t\tbody, err := io.ReadAll(res.Body)\n\t\t\t\tres.Body.Close()\n\t\t\t\tassert.FatalError(t, err)\n\n\t\t\t\tadminErr := admin.Error{}\n\t\t\t\tassert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &adminErr))\n\n\t\t\t\tassert.Equals(t, tc.err.Type, adminErr.Type)\n\t\t\t\tassert.Equals(t, tc.err.Detail, adminErr.Detail)\n\t\t\t\tassert.Equals(t, []string{\"application/json\"}, res.Header[\"Content-Type\"])\n\n\t\t\t\tif strings.HasPrefix(tc.err.Message, \"proto:\") {\n\t\t\t\t\tassert.True(t, strings.Contains(adminErr.Message, \"syntax error\"))\n\t\t\t\t} else {\n\t\t\t\t\tassert.Equals(t, tc.err.Message, adminErr.Message)\n\t\t\t\t}\n\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tprov := &linkedca.Provisioner{}\n\t\t\terr := readProtoJSON(res.Body, prov)\n\t\t\tassert.FatalError(t, err)\n\n\t\t\tassert.Equals(t, []string{\"application/json\"}, res.Header[\"Content-Type\"])\n\n\t\t\topts := []cmp.Option{cmpopts.IgnoreUnexported(linkedca.Provisioner{}, timestamppb.Timestamp{})}\n\t\t\tif !cmp.Equal(tc.prov, prov, opts...) {\n\t\t\t\tt.Errorf(\"linkedca.Admin diff =\\n%s\", cmp.Diff(tc.prov, prov, opts...))\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestHandler_DeleteProvisioner(t *testing.T) {\n\ttype test struct {\n\t\tctx        context.Context\n\t\tauth       adminAuthority\n\t\treq        *http.Request\n\t\tstatusCode int\n\t\terr        *admin.Error\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/auth.LoadProvisionerByID\": func(t *testing.T) test {\n\t\t\treq := httptest.NewRequest(\"DELETE\", \"/foo?id=provID\", http.NoBody)\n\t\t\tchiCtx := chi.NewRouteContext()\n\t\t\tctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)\n\t\t\tauth := &mockAdminAuthority{\n\t\t\t\tMockLoadProvisionerByID: func(id string) (provisioner.Interface, error) {\n\t\t\t\t\tassert.Equals(t, \"provID\", id)\n\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t},\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\treq:        req,\n\t\t\t\tauth:       auth,\n\t\t\t\tstatusCode: 500,\n\t\t\t\terr: &admin.Error{\n\t\t\t\t\tType:    admin.ErrorServerInternalType.String(),\n\t\t\t\t\tStatus:  500,\n\t\t\t\t\tDetail:  \"the server experienced an internal error\",\n\t\t\t\t\tMessage: \"error loading provisioner provID: force\",\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"fail/auth.LoadProvisionerByName\": func(t *testing.T) test {\n\t\t\treq := httptest.NewRequest(\"DELETE\", \"/foo\", http.NoBody)\n\t\t\tchiCtx := chi.NewRouteContext()\n\t\t\tchiCtx.URLParams.Add(\"name\", \"provName\")\n\t\t\tctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)\n\t\t\tauth := &mockAdminAuthority{\n\t\t\t\tMockLoadProvisionerByName: func(name string) (provisioner.Interface, error) {\n\t\t\t\t\tassert.Equals(t, \"provName\", name)\n\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t},\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\treq:        req,\n\t\t\t\tauth:       auth,\n\t\t\t\tstatusCode: 500,\n\t\t\t\terr: &admin.Error{\n\t\t\t\t\tType:    admin.ErrorServerInternalType.String(),\n\t\t\t\t\tStatus:  500,\n\t\t\t\t\tDetail:  \"the server experienced an internal error\",\n\t\t\t\t\tMessage: \"error loading provisioner provName: force\",\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"fail/auth.RemoveProvisioner\": func(t *testing.T) test {\n\t\t\treq := httptest.NewRequest(\"DELETE\", \"/foo\", http.NoBody)\n\t\t\tchiCtx := chi.NewRouteContext()\n\t\t\tchiCtx.URLParams.Add(\"name\", \"provName\")\n\t\t\tctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)\n\t\t\tauth := &mockAdminAuthority{\n\t\t\t\tMockLoadProvisionerByName: func(name string) (provisioner.Interface, error) {\n\t\t\t\t\tassert.Equals(t, \"provName\", name)\n\t\t\t\t\treturn &provisioner.OIDC{\n\t\t\t\t\t\tID:   \"provID\",\n\t\t\t\t\t\tName: \"provName\",\n\t\t\t\t\t\tType: \"OIDC\",\n\t\t\t\t\t}, nil\n\t\t\t\t},\n\t\t\t\tMockRemoveProvisioner: func(ctx context.Context, id string) error {\n\t\t\t\t\tassert.Equals(t, \"provID\", id)\n\t\t\t\t\treturn errors.New(\"force\")\n\t\t\t\t},\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\treq:        req,\n\t\t\t\tauth:       auth,\n\t\t\t\tstatusCode: 500,\n\t\t\t\terr: &admin.Error{\n\t\t\t\t\tType:    admin.ErrorServerInternalType.String(),\n\t\t\t\t\tStatus:  500,\n\t\t\t\t\tDetail:  \"the server experienced an internal error\",\n\t\t\t\t\tMessage: \"error removing provisioner provName: force\",\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\treq := httptest.NewRequest(\"DELETE\", \"/foo\", http.NoBody)\n\t\t\tchiCtx := chi.NewRouteContext()\n\t\t\tchiCtx.URLParams.Add(\"name\", \"provName\")\n\t\t\tctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)\n\t\t\tauth := &mockAdminAuthority{\n\t\t\t\tMockLoadProvisionerByName: func(name string) (provisioner.Interface, error) {\n\t\t\t\t\tassert.Equals(t, \"provName\", name)\n\t\t\t\t\treturn &provisioner.OIDC{\n\t\t\t\t\t\tID:   \"provID\",\n\t\t\t\t\t\tName: \"provName\",\n\t\t\t\t\t\tType: \"OIDC\",\n\t\t\t\t\t}, nil\n\t\t\t\t},\n\t\t\t\tMockRemoveProvisioner: func(ctx context.Context, id string) error {\n\t\t\t\t\tassert.Equals(t, \"provID\", id)\n\t\t\t\t\treturn nil\n\t\t\t\t},\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\treq:        req,\n\t\t\t\tauth:       auth,\n\t\t\t\tstatusCode: 200,\n\t\t\t\terr:        nil,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, prep := range tests {\n\t\ttc := prep(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tmockMustAuthority(t, tc.auth)\n\t\t\treq := tc.req.WithContext(tc.ctx)\n\t\t\tw := httptest.NewRecorder()\n\t\t\tDeleteProvisioner(w, req)\n\t\t\tres := w.Result()\n\n\t\t\tassert.Equals(t, tc.statusCode, res.StatusCode)\n\n\t\t\tif res.StatusCode >= 400 {\n\n\t\t\t\tbody, err := io.ReadAll(res.Body)\n\t\t\t\tres.Body.Close()\n\t\t\t\tassert.FatalError(t, err)\n\n\t\t\t\tadminErr := admin.Error{}\n\t\t\t\tassert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &adminErr))\n\n\t\t\t\tassert.Equals(t, tc.err.Type, adminErr.Type)\n\t\t\t\tassert.Equals(t, tc.err.Message, adminErr.Message)\n\t\t\t\tassert.Equals(t, tc.err.Detail, adminErr.Detail)\n\t\t\t\tassert.Equals(t, []string{\"application/json\"}, res.Header[\"Content-Type\"])\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tbody, err := io.ReadAll(res.Body)\n\t\t\tres.Body.Close()\n\t\t\tassert.FatalError(t, err)\n\n\t\t\tresponse := DeleteResponse{}\n\t\t\tassert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &response))\n\t\t\tassert.Equals(t, \"ok\", response.Status)\n\t\t\tassert.Equals(t, []string{\"application/json\"}, res.Header[\"Content-Type\"])\n\t\t})\n\t}\n}\n\nfunc TestHandler_UpdateProvisioner(t *testing.T) {\n\ttype test struct {\n\t\tctx        context.Context\n\t\tauth       adminAuthority\n\t\tbody       []byte\n\t\tadminDB    admin.DB\n\t\tstatusCode int\n\t\terr        *admin.Error\n\t\tprov       *linkedca.Provisioner\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/readProtoJSON\": func(t *testing.T) test {\n\t\t\tbody := []byte(\"{!?}\")\n\t\t\treturn test{\n\t\t\t\tctx:        context.Background(),\n\t\t\t\tbody:       body,\n\t\t\t\tadminDB:    &admin.MockDB{},\n\t\t\t\tstatusCode: 400,\n\t\t\t\terr: &admin.Error{\n\t\t\t\t\tType:    \"badRequest\",\n\t\t\t\t\tStatus:  400,\n\t\t\t\t\tDetail:  \"bad request\",\n\t\t\t\t\tMessage: \"proto: syntax error (line 1:2): invalid value !\",\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"fail/auth.LoadProvisionerByName\": func(t *testing.T) test {\n\t\t\tchiCtx := chi.NewRouteContext()\n\t\t\tchiCtx.URLParams.Add(\"name\", \"provName\")\n\t\t\tctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)\n\t\t\tprov := &linkedca.Provisioner{\n\t\t\t\tId:   \"provID\",\n\t\t\t\tType: linkedca.Provisioner_OIDC,\n\t\t\t\tName: \"provName\",\n\t\t\t}\n\t\t\tbody, err := protojson.Marshal(prov)\n\t\t\tassert.FatalError(t, err)\n\t\t\tauth := &mockAdminAuthority{\n\t\t\t\tMockLoadProvisionerByName: func(name string) (provisioner.Interface, error) {\n\t\t\t\t\tassert.Equals(t, \"provName\", name)\n\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t},\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\tbody:       body,\n\t\t\t\tadminDB:    &admin.MockDB{},\n\t\t\t\tauth:       auth,\n\t\t\t\tstatusCode: 500,\n\t\t\t\terr: &admin.Error{\n\t\t\t\t\tType:    admin.ErrorServerInternalType.String(),\n\t\t\t\t\tStatus:  500,\n\t\t\t\t\tDetail:  \"the server experienced an internal error\",\n\t\t\t\t\tMessage: \"error loading provisioner from cached configuration 'provName': force\",\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"fail/db.GetProvisioner\": func(t *testing.T) test {\n\t\t\tchiCtx := chi.NewRouteContext()\n\t\t\tchiCtx.URLParams.Add(\"name\", \"provName\")\n\t\t\tctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)\n\t\t\tprov := &linkedca.Provisioner{\n\t\t\t\tId:   \"provID\",\n\t\t\t\tType: linkedca.Provisioner_OIDC,\n\t\t\t\tName: \"provName\",\n\t\t\t}\n\t\t\tbody, err := protojson.Marshal(prov)\n\t\t\tassert.FatalError(t, err)\n\t\t\tauth := &mockAdminAuthority{\n\t\t\t\tMockLoadProvisionerByName: func(name string) (provisioner.Interface, error) {\n\t\t\t\t\tassert.Equals(t, \"provName\", name)\n\t\t\t\t\treturn &provisioner.OIDC{\n\t\t\t\t\t\tID:   \"provID\",\n\t\t\t\t\t\tName: \"provName\",\n\t\t\t\t\t}, nil\n\t\t\t\t},\n\t\t\t}\n\t\t\tdb := &admin.MockDB{\n\t\t\t\tMockGetProvisioner: func(ctx context.Context, id string) (*linkedca.Provisioner, error) {\n\t\t\t\t\tassert.Equals(t, \"provID\", id)\n\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t},\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\tbody:       body,\n\t\t\t\tauth:       auth,\n\t\t\t\tadminDB:    db,\n\t\t\t\tstatusCode: 500,\n\t\t\t\terr: &admin.Error{\n\t\t\t\t\tType:    admin.ErrorServerInternalType.String(),\n\t\t\t\t\tStatus:  500,\n\t\t\t\t\tDetail:  \"the server experienced an internal error\",\n\t\t\t\t\tMessage: \"error loading provisioner from db 'provID': force\",\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"fail/change-id-error\": func(t *testing.T) test {\n\t\t\tchiCtx := chi.NewRouteContext()\n\t\t\tchiCtx.URLParams.Add(\"name\", \"provName\")\n\t\t\tctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)\n\t\t\tprov := &linkedca.Provisioner{\n\t\t\t\tId:   \"differentProvID\",\n\t\t\t\tType: linkedca.Provisioner_OIDC,\n\t\t\t\tName: \"provName\",\n\t\t\t}\n\t\t\tbody, err := protojson.Marshal(prov)\n\t\t\tassert.FatalError(t, err)\n\t\t\tauth := &mockAdminAuthority{\n\t\t\t\tMockLoadProvisionerByName: func(name string) (provisioner.Interface, error) {\n\t\t\t\t\tassert.Equals(t, \"provName\", name)\n\t\t\t\t\treturn &provisioner.OIDC{\n\t\t\t\t\t\tID:   \"provID\",\n\t\t\t\t\t\tName: \"provName\",\n\t\t\t\t\t}, nil\n\t\t\t\t},\n\t\t\t}\n\t\t\tdb := &admin.MockDB{\n\t\t\t\tMockGetProvisioner: func(ctx context.Context, id string) (*linkedca.Provisioner, error) {\n\t\t\t\t\tassert.Equals(t, \"provID\", id)\n\t\t\t\t\treturn &linkedca.Provisioner{\n\t\t\t\t\t\tId:   \"provID\",\n\t\t\t\t\t\tName: \"provName\",\n\t\t\t\t\t}, nil\n\t\t\t\t},\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\tbody:       body,\n\t\t\t\tauth:       auth,\n\t\t\t\tadminDB:    db,\n\t\t\t\tstatusCode: 500,\n\t\t\t\terr: &admin.Error{\n\t\t\t\t\tType:    admin.ErrorServerInternalType.String(),\n\t\t\t\t\tStatus:  500,\n\t\t\t\t\tDetail:  \"the server experienced an internal error\",\n\t\t\t\t\tMessage: \"cannot change provisioner ID\",\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"fail/change-type-error\": func(t *testing.T) test {\n\t\t\tchiCtx := chi.NewRouteContext()\n\t\t\tchiCtx.URLParams.Add(\"name\", \"provName\")\n\t\t\tctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)\n\t\t\tprov := &linkedca.Provisioner{\n\t\t\t\tId:   \"provID\",\n\t\t\t\tType: linkedca.Provisioner_JWK,\n\t\t\t\tName: \"provName\",\n\t\t\t}\n\t\t\tbody, err := protojson.Marshal(prov)\n\t\t\tassert.FatalError(t, err)\n\t\t\tauth := &mockAdminAuthority{\n\t\t\t\tMockLoadProvisionerByName: func(name string) (provisioner.Interface, error) {\n\t\t\t\t\tassert.Equals(t, \"provName\", name)\n\t\t\t\t\treturn &provisioner.OIDC{\n\t\t\t\t\t\tID:   \"provID\",\n\t\t\t\t\t\tName: \"provName\",\n\t\t\t\t\t}, nil\n\t\t\t\t},\n\t\t\t}\n\t\t\tdb := &admin.MockDB{\n\t\t\t\tMockGetProvisioner: func(ctx context.Context, id string) (*linkedca.Provisioner, error) {\n\t\t\t\t\tassert.Equals(t, \"provID\", id)\n\t\t\t\t\treturn &linkedca.Provisioner{\n\t\t\t\t\t\tId:   \"provID\",\n\t\t\t\t\t\tName: \"provName\",\n\t\t\t\t\t\tType: linkedca.Provisioner_OIDC,\n\t\t\t\t\t}, nil\n\t\t\t\t},\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\tbody:       body,\n\t\t\t\tauth:       auth,\n\t\t\t\tadminDB:    db,\n\t\t\t\tstatusCode: 500,\n\t\t\t\terr: &admin.Error{\n\t\t\t\t\tType:    admin.ErrorServerInternalType.String(),\n\t\t\t\t\tStatus:  500,\n\t\t\t\t\tDetail:  \"the server experienced an internal error\",\n\t\t\t\t\tMessage: \"cannot change provisioner type\",\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"fail/change-authority-id-error\": func(t *testing.T) test {\n\t\t\tchiCtx := chi.NewRouteContext()\n\t\t\tchiCtx.URLParams.Add(\"name\", \"provName\")\n\t\t\tctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)\n\t\t\tprov := &linkedca.Provisioner{\n\t\t\t\tId:          \"provID\",\n\t\t\t\tType:        linkedca.Provisioner_OIDC,\n\t\t\t\tName:        \"provName\",\n\t\t\t\tAuthorityId: \"differentAuthorityID\",\n\t\t\t}\n\t\t\tbody, err := protojson.Marshal(prov)\n\t\t\tassert.FatalError(t, err)\n\t\t\tauth := &mockAdminAuthority{\n\t\t\t\tMockLoadProvisionerByName: func(name string) (provisioner.Interface, error) {\n\t\t\t\t\tassert.Equals(t, \"provName\", name)\n\t\t\t\t\treturn &provisioner.OIDC{\n\t\t\t\t\t\tID:   \"provID\",\n\t\t\t\t\t\tName: \"provName\",\n\t\t\t\t\t}, nil\n\t\t\t\t},\n\t\t\t}\n\t\t\tdb := &admin.MockDB{\n\t\t\t\tMockGetProvisioner: func(ctx context.Context, id string) (*linkedca.Provisioner, error) {\n\t\t\t\t\tassert.Equals(t, \"provID\", id)\n\t\t\t\t\treturn &linkedca.Provisioner{\n\t\t\t\t\t\tId:          \"provID\",\n\t\t\t\t\t\tName:        \"provName\",\n\t\t\t\t\t\tType:        linkedca.Provisioner_OIDC,\n\t\t\t\t\t\tAuthorityId: \"authorityID\",\n\t\t\t\t\t}, nil\n\t\t\t\t},\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\tbody:       body,\n\t\t\t\tauth:       auth,\n\t\t\t\tadminDB:    db,\n\t\t\t\tstatusCode: 500,\n\t\t\t\terr: &admin.Error{\n\t\t\t\t\tType:    admin.ErrorServerInternalType.String(),\n\t\t\t\t\tStatus:  500,\n\t\t\t\t\tDetail:  \"the server experienced an internal error\",\n\t\t\t\t\tMessage: \"cannot change provisioner authorityID\",\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"fail/change-createdAt-error\": func(t *testing.T) test {\n\t\t\tchiCtx := chi.NewRouteContext()\n\t\t\tchiCtx.URLParams.Add(\"name\", \"provName\")\n\t\t\tctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)\n\t\t\tcreatedAt := time.Now()\n\t\t\tprov := &linkedca.Provisioner{\n\t\t\t\tId:          \"provID\",\n\t\t\t\tType:        linkedca.Provisioner_OIDC,\n\t\t\t\tName:        \"provName\",\n\t\t\t\tAuthorityId: \"authorityID\",\n\t\t\t\tCreatedAt:   timestamppb.New(time.Now().Add(-1 * time.Hour)),\n\t\t\t}\n\t\t\tbody, err := protojson.Marshal(prov)\n\t\t\tassert.FatalError(t, err)\n\t\t\tauth := &mockAdminAuthority{\n\t\t\t\tMockLoadProvisionerByName: func(name string) (provisioner.Interface, error) {\n\t\t\t\t\tassert.Equals(t, \"provName\", name)\n\t\t\t\t\treturn &provisioner.OIDC{\n\t\t\t\t\t\tID:   \"provID\",\n\t\t\t\t\t\tName: \"provName\",\n\t\t\t\t\t}, nil\n\t\t\t\t},\n\t\t\t}\n\t\t\tdb := &admin.MockDB{\n\t\t\t\tMockGetProvisioner: func(ctx context.Context, id string) (*linkedca.Provisioner, error) {\n\t\t\t\t\tassert.Equals(t, \"provID\", id)\n\t\t\t\t\treturn &linkedca.Provisioner{\n\t\t\t\t\t\tId:          \"provID\",\n\t\t\t\t\t\tName:        \"provName\",\n\t\t\t\t\t\tType:        linkedca.Provisioner_OIDC,\n\t\t\t\t\t\tAuthorityId: \"authorityID\",\n\t\t\t\t\t\tCreatedAt:   timestamppb.New(createdAt),\n\t\t\t\t\t}, nil\n\t\t\t\t},\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\tbody:       body,\n\t\t\t\tauth:       auth,\n\t\t\t\tadminDB:    db,\n\t\t\t\tstatusCode: 500,\n\t\t\t\terr: &admin.Error{\n\t\t\t\t\tType:    admin.ErrorServerInternalType.String(),\n\t\t\t\t\tStatus:  500,\n\t\t\t\t\tDetail:  \"the server experienced an internal error\",\n\t\t\t\t\tMessage: \"cannot change provisioner createdAt\",\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"fail/change-deletedAt-error\": func(t *testing.T) test {\n\t\t\tchiCtx := chi.NewRouteContext()\n\t\t\tchiCtx.URLParams.Add(\"name\", \"provName\")\n\t\t\tctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)\n\t\t\tcreatedAt := time.Now()\n\t\t\tvar deletedAt time.Time\n\t\t\tprov := &linkedca.Provisioner{\n\t\t\t\tId:          \"provID\",\n\t\t\t\tType:        linkedca.Provisioner_OIDC,\n\t\t\t\tName:        \"provName\",\n\t\t\t\tAuthorityId: \"authorityID\",\n\t\t\t\tCreatedAt:   timestamppb.New(createdAt),\n\t\t\t\tDeletedAt:   timestamppb.New(time.Now()),\n\t\t\t}\n\t\t\tbody, err := protojson.Marshal(prov)\n\t\t\tassert.FatalError(t, err)\n\t\t\tauth := &mockAdminAuthority{\n\t\t\t\tMockLoadProvisionerByName: func(name string) (provisioner.Interface, error) {\n\t\t\t\t\tassert.Equals(t, \"provName\", name)\n\t\t\t\t\treturn &provisioner.OIDC{\n\t\t\t\t\t\tID:   \"provID\",\n\t\t\t\t\t\tName: \"provName\",\n\t\t\t\t\t}, nil\n\t\t\t\t},\n\t\t\t}\n\t\t\tdb := &admin.MockDB{\n\t\t\t\tMockGetProvisioner: func(ctx context.Context, id string) (*linkedca.Provisioner, error) {\n\t\t\t\t\tassert.Equals(t, \"provID\", id)\n\t\t\t\t\treturn &linkedca.Provisioner{\n\t\t\t\t\t\tId:          \"provID\",\n\t\t\t\t\t\tName:        \"provName\",\n\t\t\t\t\t\tType:        linkedca.Provisioner_OIDC,\n\t\t\t\t\t\tAuthorityId: \"authorityID\",\n\t\t\t\t\t\tCreatedAt:   timestamppb.New(createdAt),\n\t\t\t\t\t\tDeletedAt:   timestamppb.New(deletedAt),\n\t\t\t\t\t}, nil\n\t\t\t\t},\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\tbody:       body,\n\t\t\t\tauth:       auth,\n\t\t\t\tadminDB:    db,\n\t\t\t\tstatusCode: 500,\n\t\t\t\terr: &admin.Error{\n\t\t\t\t\tType:    admin.ErrorServerInternalType.String(),\n\t\t\t\t\tStatus:  500,\n\t\t\t\t\tDetail:  \"the server experienced an internal error\",\n\t\t\t\t\tMessage: \"cannot change provisioner deletedAt\",\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t// TODO(hs): ValidateClaims can't be mocked atm\n\t\t//\"fail/ValidateClaims\": func(t *testing.T) test { return test{} },\n\t\t\"fail/validateTemplates\": func(t *testing.T) test {\n\t\t\tchiCtx := chi.NewRouteContext()\n\t\t\tchiCtx.URLParams.Add(\"name\", \"provName\")\n\t\t\tctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)\n\t\t\tcreatedAt := time.Now()\n\t\t\tvar deletedAt time.Time\n\t\t\tprov := &linkedca.Provisioner{\n\t\t\t\tId:          \"provID\",\n\t\t\t\tType:        linkedca.Provisioner_OIDC,\n\t\t\t\tName:        \"provName\",\n\t\t\t\tAuthorityId: \"authorityID\",\n\t\t\t\tCreatedAt:   timestamppb.New(createdAt),\n\t\t\t\tDeletedAt:   timestamppb.New(deletedAt),\n\t\t\t\tX509Template: &linkedca.Template{\n\t\t\t\t\tTemplate: []byte(\"{ {{ missingFunction }} }\"),\n\t\t\t\t},\n\t\t\t}\n\t\t\tbody, err := protojson.Marshal(prov)\n\t\t\tassert.FatalError(t, err)\n\t\t\tauth := &mockAdminAuthority{\n\t\t\t\tMockLoadProvisionerByName: func(name string) (provisioner.Interface, error) {\n\t\t\t\t\tassert.Equals(t, \"provName\", name)\n\t\t\t\t\treturn &provisioner.OIDC{\n\t\t\t\t\t\tID:   \"provID\",\n\t\t\t\t\t\tName: \"provName\",\n\t\t\t\t\t}, nil\n\t\t\t\t},\n\t\t\t}\n\t\t\tdb := &admin.MockDB{\n\t\t\t\tMockGetProvisioner: func(ctx context.Context, id string) (*linkedca.Provisioner, error) {\n\t\t\t\t\tassert.Equals(t, \"provID\", id)\n\t\t\t\t\treturn &linkedca.Provisioner{\n\t\t\t\t\t\tId:          \"provID\",\n\t\t\t\t\t\tName:        \"provName\",\n\t\t\t\t\t\tType:        linkedca.Provisioner_OIDC,\n\t\t\t\t\t\tAuthorityId: \"authorityID\",\n\t\t\t\t\t\tCreatedAt:   timestamppb.New(createdAt),\n\t\t\t\t\t\tDeletedAt:   timestamppb.New(deletedAt),\n\t\t\t\t\t}, nil\n\t\t\t\t},\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\tbody:       body,\n\t\t\t\tauth:       auth,\n\t\t\t\tadminDB:    db,\n\t\t\t\tstatusCode: 400,\n\t\t\t\terr: &admin.Error{\n\t\t\t\t\tType:    \"badRequest\",\n\t\t\t\t\tStatus:  400,\n\t\t\t\t\tDetail:  \"bad request\",\n\t\t\t\t\tMessage: \"invalid template: invalid X.509 template: error parsing template: template: template:1: function \\\"missingFunction\\\" not defined\",\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"fail/auth.UpdateProvisioner\": func(t *testing.T) test {\n\t\t\tchiCtx := chi.NewRouteContext()\n\t\t\tchiCtx.URLParams.Add(\"name\", \"provName\")\n\t\t\tctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)\n\t\t\tcreatedAt := time.Now()\n\t\t\tvar deletedAt time.Time\n\t\t\tprov := &linkedca.Provisioner{\n\t\t\t\tId:          \"provID\",\n\t\t\t\tType:        linkedca.Provisioner_OIDC,\n\t\t\t\tName:        \"provName\",\n\t\t\t\tAuthorityId: \"authorityID\",\n\t\t\t\tCreatedAt:   timestamppb.New(createdAt),\n\t\t\t\tDeletedAt:   timestamppb.New(deletedAt),\n\t\t\t}\n\t\t\tbody, err := protojson.Marshal(prov)\n\t\t\tassert.FatalError(t, err)\n\t\t\tauth := &mockAdminAuthority{\n\t\t\t\tMockLoadProvisionerByName: func(name string) (provisioner.Interface, error) {\n\t\t\t\t\tassert.Equals(t, \"provName\", name)\n\t\t\t\t\treturn &provisioner.OIDC{\n\t\t\t\t\t\tID:   \"provID\",\n\t\t\t\t\t\tName: \"provName\",\n\t\t\t\t\t}, nil\n\t\t\t\t},\n\t\t\t\tMockUpdateProvisioner: func(ctx context.Context, nu *linkedca.Provisioner) error {\n\t\t\t\t\tassert.Equals(t, \"provID\", nu.Id)\n\t\t\t\t\tassert.Equals(t, \"provName\", nu.Name)\n\t\t\t\t\treturn errors.New(\"force\")\n\t\t\t\t},\n\t\t\t}\n\t\t\tdb := &admin.MockDB{\n\t\t\t\tMockGetProvisioner: func(ctx context.Context, id string) (*linkedca.Provisioner, error) {\n\t\t\t\t\tassert.Equals(t, \"provID\", id)\n\t\t\t\t\treturn &linkedca.Provisioner{\n\t\t\t\t\t\tId:          \"provID\",\n\t\t\t\t\t\tName:        \"provName\",\n\t\t\t\t\t\tType:        linkedca.Provisioner_OIDC,\n\t\t\t\t\t\tAuthorityId: \"authorityID\",\n\t\t\t\t\t\tCreatedAt:   timestamppb.New(createdAt),\n\t\t\t\t\t\tDeletedAt:   timestamppb.New(deletedAt),\n\t\t\t\t\t}, nil\n\t\t\t\t},\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\tbody:       body,\n\t\t\t\tauth:       auth,\n\t\t\t\tadminDB:    db,\n\t\t\t\tstatusCode: 500,\n\t\t\t\terr: &admin.Error{\n\t\t\t\t\tType:    \"\", // TODO(hs): this error can be improved\n\t\t\t\t\tStatus:  500,\n\t\t\t\t\tDetail:  \"\",\n\t\t\t\t\tMessage: \"\",\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tchiCtx := chi.NewRouteContext()\n\t\t\tchiCtx.URLParams.Add(\"name\", \"provName\")\n\t\t\tctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)\n\t\t\tcreatedAt := time.Now()\n\t\t\tvar deletedAt time.Time\n\t\t\tprov := &linkedca.Provisioner{\n\t\t\t\tId:          \"provID\",\n\t\t\t\tType:        linkedca.Provisioner_OIDC,\n\t\t\t\tName:        \"provName\",\n\t\t\t\tAuthorityId: \"authorityID\",\n\t\t\t\tCreatedAt:   timestamppb.New(createdAt),\n\t\t\t\tDeletedAt:   timestamppb.New(deletedAt),\n\t\t\t\tDetails: &linkedca.ProvisionerDetails{\n\t\t\t\t\tData: &linkedca.ProvisionerDetails_OIDC{\n\t\t\t\t\t\tOIDC: &linkedca.OIDCProvisioner{\n\t\t\t\t\t\t\tClientId:     \"new-client-id\",\n\t\t\t\t\t\t\tClientSecret: \"new-client-secret\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t\tbody, err := protojson.Marshal(prov)\n\t\t\tassert.FatalError(t, err)\n\t\t\tauth := &mockAdminAuthority{\n\t\t\t\tMockLoadProvisionerByName: func(name string) (provisioner.Interface, error) {\n\t\t\t\t\tassert.Equals(t, \"provName\", name)\n\t\t\t\t\treturn &provisioner.OIDC{\n\t\t\t\t\t\tID:   \"provID\",\n\t\t\t\t\t\tName: \"provName\",\n\t\t\t\t\t}, nil\n\t\t\t\t},\n\t\t\t\tMockUpdateProvisioner: func(ctx context.Context, nu *linkedca.Provisioner) error {\n\t\t\t\t\tassert.Equals(t, \"provID\", nu.Id)\n\t\t\t\t\tassert.Equals(t, \"provName\", nu.Name)\n\t\t\t\t\treturn nil\n\t\t\t\t},\n\t\t\t}\n\t\t\tdb := &admin.MockDB{\n\t\t\t\tMockGetProvisioner: func(ctx context.Context, id string) (*linkedca.Provisioner, error) {\n\t\t\t\t\tassert.Equals(t, \"provID\", id)\n\t\t\t\t\treturn &linkedca.Provisioner{\n\t\t\t\t\t\tId:          \"provID\",\n\t\t\t\t\t\tName:        \"provName\",\n\t\t\t\t\t\tType:        linkedca.Provisioner_OIDC,\n\t\t\t\t\t\tAuthorityId: \"authorityID\",\n\t\t\t\t\t\tCreatedAt:   timestamppb.New(createdAt),\n\t\t\t\t\t\tDeletedAt:   timestamppb.New(deletedAt),\n\t\t\t\t\t}, nil\n\t\t\t\t},\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\tbody:       body,\n\t\t\t\tauth:       auth,\n\t\t\t\tadminDB:    db,\n\t\t\t\tstatusCode: 200,\n\t\t\t\tprov:       prov,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, prep := range tests {\n\t\ttc := prep(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tmockMustAuthority(t, tc.auth)\n\t\t\tctx := admin.NewContext(tc.ctx, tc.adminDB)\n\t\t\treq := httptest.NewRequest(\"POST\", \"/foo\", io.NopCloser(bytes.NewBuffer(tc.body)))\n\t\t\treq = req.WithContext(ctx)\n\t\t\tw := httptest.NewRecorder()\n\t\t\tUpdateProvisioner(w, req)\n\t\t\tres := w.Result()\n\n\t\t\tassert.Equals(t, tc.statusCode, res.StatusCode)\n\n\t\t\tif res.StatusCode >= 400 {\n\n\t\t\t\tbody, err := io.ReadAll(res.Body)\n\t\t\t\tres.Body.Close()\n\t\t\t\tassert.FatalError(t, err)\n\n\t\t\t\tadminErr := admin.Error{}\n\t\t\t\tassert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &adminErr))\n\n\t\t\t\tassert.Equals(t, tc.err.Type, adminErr.Type)\n\t\t\t\tassert.Equals(t, tc.err.Detail, adminErr.Detail)\n\t\t\t\tassert.Equals(t, []string{\"application/json\"}, res.Header[\"Content-Type\"])\n\n\t\t\t\tif strings.HasPrefix(tc.err.Message, \"proto:\") {\n\t\t\t\t\tassert.True(t, strings.Contains(adminErr.Message, \"syntax error\"))\n\t\t\t\t} else {\n\t\t\t\t\tassert.Equals(t, tc.err.Message, adminErr.Message)\n\t\t\t\t}\n\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tprov := &linkedca.Provisioner{}\n\t\t\terr := readProtoJSON(res.Body, prov)\n\t\t\tassert.FatalError(t, err)\n\n\t\t\tassert.Equals(t, []string{\"application/json\"}, res.Header[\"Content-Type\"])\n\n\t\t\topts := []cmp.Option{\n\t\t\t\tcmpopts.IgnoreUnexported(\n\t\t\t\t\tlinkedca.Provisioner{}, linkedca.ProvisionerDetails{}, linkedca.ProvisionerDetails_OIDC{},\n\t\t\t\t\tlinkedca.OIDCProvisioner{}, timestamppb.Timestamp{},\n\t\t\t\t),\n\t\t\t}\n\t\t\tif !cmp.Equal(tc.prov, prov, opts...) {\n\t\t\t\tt.Errorf(\"linkedca.Admin diff =\\n%s\", cmp.Diff(tc.prov, prov, opts...))\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_validateTemplates(t *testing.T) {\n\ttype args struct {\n\t\tx509 *linkedca.Template\n\t\tssh  *linkedca.Template\n\t}\n\ttests := []struct {\n\t\tname string\n\t\targs args\n\t\terr  error\n\t}{\n\t\t{\n\t\t\tname: \"ok\",\n\t\t\targs: args{},\n\t\t\terr:  nil,\n\t\t},\n\t\t{\n\t\t\tname: \"ok/x509\",\n\t\t\targs: args{\n\t\t\t\tx509: &linkedca.Template{\n\t\t\t\t\tTemplate: []byte(`{\"x\": 1}`),\n\t\t\t\t},\n\t\t\t},\n\t\t\terr: nil,\n\t\t},\n\t\t{\n\t\t\tname: \"ok/ssh\",\n\t\t\targs: args{\n\t\t\t\tssh: &linkedca.Template{\n\t\t\t\t\tTemplate: []byte(`{\"x\": 1}`),\n\t\t\t\t},\n\t\t\t},\n\t\t\terr: nil,\n\t\t},\n\t\t{\n\t\t\tname: \"fail/x509-template-missing-quote\",\n\t\t\targs: args{\n\t\t\t\tx509: &linkedca.Template{\n\t\t\t\t\tTemplate: []byte(`{ {{printf \"%q\" \"quoted}} }`),\n\t\t\t\t},\n\t\t\t},\n\t\t\terr: errors.New(\"invalid X.509 template: error parsing template: template: template:1: unterminated quoted string\"),\n\t\t},\n\t\t{\n\t\t\tname: \"fail/x509-template-data\",\n\t\t\targs: args{\n\t\t\t\tx509: &linkedca.Template{\n\t\t\t\t\tData: []byte(`{!?}`),\n\t\t\t\t},\n\t\t\t},\n\t\t\terr: errors.New(\"invalid X.509 template data: error validating json template data\"),\n\t\t},\n\t\t{\n\t\t\tname: \"fail/ssh-template-unknown-function\",\n\t\t\targs: args{\n\t\t\t\tssh: &linkedca.Template{\n\t\t\t\t\tTemplate: []byte(`{ {{unknownFunction \"foo\"}} }`),\n\t\t\t\t},\n\t\t\t},\n\t\t\terr: errors.New(\"invalid SSH template: error parsing template: template: template:1: function \\\"unknownFunction\\\" not defined\"),\n\t\t},\n\t\t{\n\t\t\tname: \"fail/ssh-template-data\",\n\t\t\targs: args{\n\t\t\t\tssh: &linkedca.Template{\n\t\t\t\t\tData: []byte(`{!?}`),\n\t\t\t\t},\n\t\t\t},\n\t\t\terr: errors.New(\"invalid SSH template data: error validating json template data\"),\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\terr := validateTemplates(tt.args.x509, tt.args.ssh)\n\t\t\tif tt.err != nil {\n\t\t\t\tassert.Error(t, err)\n\t\t\t\tassert.Equals(t, tt.err.Error(), err.Error())\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.Nil(t, err)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "authority/admin/api/webhook.go",
    "content": "package api\n\nimport (\n\t\"encoding/base64\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\n\t\"github.com/go-chi/chi/v5\"\n\t\"github.com/smallstep/certificates/api/read\"\n\t\"github.com/smallstep/certificates/api/render\"\n\t\"github.com/smallstep/certificates/authority/admin\"\n\t\"github.com/smallstep/linkedca\"\n\t\"go.step.sm/crypto/randutil\"\n)\n\n// WebhookAdminResponder is the interface responsible for writing webhook admin\n// responses.\ntype WebhookAdminResponder interface {\n\tCreateProvisionerWebhook(w http.ResponseWriter, r *http.Request)\n\tUpdateProvisionerWebhook(w http.ResponseWriter, r *http.Request)\n\tDeleteProvisionerWebhook(w http.ResponseWriter, r *http.Request)\n}\n\n// webhoookAdminResponder implements WebhookAdminResponder\ntype webhookAdminResponder struct{}\n\n// NewWebhookAdminResponder returns a new WebhookAdminResponder\nfunc NewWebhookAdminResponder() WebhookAdminResponder {\n\treturn &webhookAdminResponder{}\n}\n\nfunc validateWebhook(webhook *linkedca.Webhook) error {\n\tif webhook == nil {\n\t\treturn nil\n\t}\n\n\t// name\n\tif webhook.Name == \"\" {\n\t\treturn admin.NewError(admin.ErrorBadRequestType, \"webhook name is required\")\n\t}\n\n\t// url\n\tparsedURL, err := url.Parse(webhook.Url)\n\tif err != nil {\n\t\treturn admin.NewError(admin.ErrorBadRequestType, \"webhook url is invalid\")\n\t}\n\tif parsedURL.Host == \"\" {\n\t\treturn admin.NewError(admin.ErrorBadRequestType, \"webhook url is invalid\")\n\t}\n\tif parsedURL.Scheme != \"https\" {\n\t\treturn admin.NewError(admin.ErrorBadRequestType, \"webhook url must use https\")\n\t}\n\tif parsedURL.User != nil {\n\t\treturn admin.NewError(admin.ErrorBadRequestType, \"webhook url may not contain username or password\")\n\t}\n\n\t// kind\n\tif _, ok := linkedca.Webhook_Kind_name[int32(webhook.Kind)]; !ok || webhook.Kind == linkedca.Webhook_NO_KIND {\n\t\treturn admin.NewError(admin.ErrorBadRequestType, \"webhook kind %q is invalid\", webhook.Kind)\n\t}\n\n\treturn nil\n}\n\nfunc (war *webhookAdminResponder) CreateProvisionerWebhook(w http.ResponseWriter, r *http.Request) {\n\tctx := r.Context()\n\n\tauth := mustAuthority(ctx)\n\tprov := linkedca.MustProvisionerFromContext(ctx)\n\n\tvar newWebhook = new(linkedca.Webhook)\n\tif err := read.ProtoJSON(r.Body, newWebhook); err != nil {\n\t\trender.Error(w, r, err)\n\t\treturn\n\t}\n\n\tif err := validateWebhook(newWebhook); err != nil {\n\t\trender.Error(w, r, err)\n\t\treturn\n\t}\n\tif newWebhook.Secret != \"\" {\n\t\terr := admin.NewError(admin.ErrorBadRequestType, \"webhook secret must not be set\")\n\t\trender.Error(w, r, err)\n\t\treturn\n\t}\n\tif newWebhook.Id != \"\" {\n\t\terr := admin.NewError(admin.ErrorBadRequestType, \"webhook ID must not be set\")\n\t\trender.Error(w, r, err)\n\t\treturn\n\t}\n\n\tid, err := randutil.UUIDv4()\n\tif err != nil {\n\t\trender.Error(w, r, admin.WrapErrorISE(err, \"error generating webhook id\"))\n\t\treturn\n\t}\n\tnewWebhook.Id = id\n\n\t// verify the name is unique\n\tfor _, wh := range prov.Webhooks {\n\t\tif wh.Name == newWebhook.Name {\n\t\t\terr := admin.NewError(admin.ErrorConflictType, \"provisioner %q already has a webhook with the name %q\", prov.Name, newWebhook.Name)\n\t\t\trender.Error(w, r, err)\n\t\t\treturn\n\t\t}\n\t}\n\n\tsecret, err := randutil.Bytes(64)\n\tif err != nil {\n\t\trender.Error(w, r, admin.WrapErrorISE(err, \"error generating webhook secret\"))\n\t\treturn\n\t}\n\tnewWebhook.Secret = base64.StdEncoding.EncodeToString(secret)\n\n\tprov.Webhooks = append(prov.Webhooks, newWebhook)\n\n\tif err := auth.UpdateProvisioner(ctx, prov); err != nil {\n\t\tif isBadRequest(err) {\n\t\t\trender.Error(w, r, admin.WrapError(admin.ErrorBadRequestType, err, \"error creating provisioner webhook\"))\n\t\t\treturn\n\t\t}\n\n\t\trender.Error(w, r, admin.WrapErrorISE(err, \"error creating provisioner webhook\"))\n\t\treturn\n\t}\n\n\trender.ProtoJSONStatus(w, newWebhook, http.StatusCreated)\n}\n\nfunc (war *webhookAdminResponder) DeleteProvisionerWebhook(w http.ResponseWriter, r *http.Request) {\n\tctx := r.Context()\n\n\tauth := mustAuthority(ctx)\n\tprov := linkedca.MustProvisionerFromContext(ctx)\n\n\twebhookName := chi.URLParam(r, \"webhookName\")\n\n\tfound := false\n\tfor i, wh := range prov.Webhooks {\n\t\tif wh.Name == webhookName {\n\t\t\tprov.Webhooks = append(prov.Webhooks[0:i], prov.Webhooks[i+1:]...)\n\t\t\tfound = true\n\t\t\tbreak\n\t\t}\n\t}\n\tif !found {\n\t\trender.JSONStatus(w, r, DeleteResponse{Status: \"ok\"}, http.StatusOK)\n\t\treturn\n\t}\n\n\tif err := auth.UpdateProvisioner(ctx, prov); err != nil {\n\t\tif isBadRequest(err) {\n\t\t\trender.Error(w, r, admin.WrapError(admin.ErrorBadRequestType, err, \"error deleting provisioner webhook\"))\n\t\t\treturn\n\t\t}\n\n\t\trender.Error(w, r, admin.WrapErrorISE(err, \"error deleting provisioner webhook\"))\n\t\treturn\n\t}\n\n\trender.JSONStatus(w, r, DeleteResponse{Status: \"ok\"}, http.StatusOK)\n}\n\nfunc (war *webhookAdminResponder) UpdateProvisionerWebhook(w http.ResponseWriter, r *http.Request) {\n\tctx := r.Context()\n\n\tauth := mustAuthority(ctx)\n\tprov := linkedca.MustProvisionerFromContext(ctx)\n\n\tvar newWebhook = new(linkedca.Webhook)\n\tif err := read.ProtoJSON(r.Body, newWebhook); err != nil {\n\t\trender.Error(w, r, err)\n\t\treturn\n\t}\n\n\tif err := validateWebhook(newWebhook); err != nil {\n\t\trender.Error(w, r, err)\n\t\treturn\n\t}\n\n\tfound := false\n\tfor i, wh := range prov.Webhooks {\n\t\tif wh.Name != newWebhook.Name {\n\t\t\tcontinue\n\t\t}\n\t\tif newWebhook.Secret != \"\" && newWebhook.Secret != wh.Secret {\n\t\t\terr := admin.NewError(admin.ErrorBadRequestType, \"webhook secret cannot be updated\")\n\t\t\trender.Error(w, r, err)\n\t\t\treturn\n\t\t}\n\t\tnewWebhook.Secret = wh.Secret\n\t\tif newWebhook.Id != \"\" && newWebhook.Id != wh.Id {\n\t\t\terr := admin.NewError(admin.ErrorBadRequestType, \"webhook ID cannot be updated\")\n\t\t\trender.Error(w, r, err)\n\t\t\treturn\n\t\t}\n\t\tnewWebhook.Id = wh.Id\n\t\tprov.Webhooks[i] = newWebhook\n\t\tfound = true\n\t\tbreak\n\t}\n\tif !found {\n\t\tmsg := fmt.Sprintf(\"provisioner %q has no webhook with the name %q\", prov.Name, newWebhook.Name)\n\t\terr := admin.NewError(admin.ErrorNotFoundType, \"%s\", msg)\n\t\trender.Error(w, r, err)\n\t\treturn\n\t}\n\n\tif err := auth.UpdateProvisioner(ctx, prov); err != nil {\n\t\tif isBadRequest(err) {\n\t\t\trender.Error(w, r, admin.WrapError(admin.ErrorBadRequestType, err, \"error updating provisioner webhook\"))\n\t\t\treturn\n\t\t}\n\n\t\trender.Error(w, r, admin.WrapErrorISE(err, \"error updating provisioner webhook\"))\n\t\treturn\n\t}\n\n\t// Return a copy without the signing secret. Include the client-supplied\n\t// auth secrets since those may have been updated in this request and we\n\t// should show in the response that they changed\n\twhResponse := &linkedca.Webhook{\n\t\tId:                   newWebhook.Id,\n\t\tName:                 newWebhook.Name,\n\t\tUrl:                  newWebhook.Url,\n\t\tKind:                 newWebhook.Kind,\n\t\tCertType:             newWebhook.CertType,\n\t\tAuth:                 newWebhook.Auth,\n\t\tDisableTlsClientAuth: newWebhook.DisableTlsClientAuth,\n\t}\n\trender.ProtoJSONStatus(w, whResponse, http.StatusCreated)\n}\n"
  },
  {
    "path": "authority/admin/api/webhook_test.go",
    "content": "package api\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/go-chi/chi/v5\"\n\t\"github.com/smallstep/certificates/authority\"\n\t\"github.com/smallstep/certificates/authority/admin\"\n\t\"github.com/smallstep/linkedca\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"google.golang.org/protobuf/encoding/protojson\"\n)\n\n// ignore secret and id since those are set by the server\nfunc assertEqualWebhook(t *testing.T, a, b *linkedca.Webhook) {\n\tassert.Equal(t, a.Name, b.Name)\n\tassert.Equal(t, a.Url, b.Url)\n\tassert.Equal(t, a.Kind, b.Kind)\n\tassert.Equal(t, a.CertType, b.CertType)\n\tassert.Equal(t, a.DisableTlsClientAuth, b.DisableTlsClientAuth)\n\n\tassert.Equal(t, a.GetAuth(), b.GetAuth())\n}\n\nfunc TestWebhookAdminResponder_CreateProvisionerWebhook(t *testing.T) {\n\ttype test struct {\n\t\tauth       adminAuthority\n\t\tbody       []byte\n\t\tctx        context.Context\n\t\terr        *admin.Error\n\t\tresponse   *linkedca.Webhook\n\t\tstatusCode int\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/existing-webhook\": func(t *testing.T) test {\n\t\t\twebhook := &linkedca.Webhook{\n\t\t\t\tName: \"already-exists\",\n\t\t\t\tUrl:  \"https://example.com\",\n\t\t\t}\n\t\t\tprov := &linkedca.Provisioner{\n\t\t\t\tName:     \"provName\",\n\t\t\t\tWebhooks: []*linkedca.Webhook{webhook},\n\t\t\t}\n\t\t\tctx := linkedca.NewContextWithProvisioner(context.Background(), prov)\n\t\t\terr := admin.NewError(admin.ErrorConflictType, `provisioner \"provName\" already has a webhook with the name \"already-exists\"`)\n\t\t\terr.Message = `provisioner \"provName\" already has a webhook with the name \"already-exists\"`\n\t\t\tbody := []byte(`\n\t\t\t{\n\t\t\t\t\"name\": \"already-exists\",\n\t\t\t\t\"url\": \"https://example.com\",\n\t\t\t\t\"kind\": \"ENRICHING\"\n\t\t\t}`)\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\tbody:       body,\n\t\t\t\terr:        err,\n\t\t\t\tstatusCode: 409,\n\t\t\t}\n\t\t},\n\t\t\"fail/read.ProtoJSON\": func(t *testing.T) test {\n\t\t\tprov := &linkedca.Provisioner{\n\t\t\t\tName: \"provName\",\n\t\t\t}\n\t\t\tctx := linkedca.NewContextWithProvisioner(context.Background(), prov)\n\t\t\tadminErr := admin.NewError(admin.ErrorBadRequestType, \"proto: syntax error (line 1:2): invalid value ?\")\n\t\t\tadminErr.Message = \"proto: syntax error (line 1:2): invalid value ?\"\n\t\t\tbody := []byte(\"{?}\")\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\tbody:       body,\n\t\t\t\terr:        adminErr,\n\t\t\t\tstatusCode: 400,\n\t\t\t}\n\t\t},\n\t\t\"fail/missing-name\": func(t *testing.T) test {\n\t\t\tprov := &linkedca.Provisioner{\n\t\t\t\tName: \"provName\",\n\t\t\t}\n\t\t\tctx := linkedca.NewContextWithProvisioner(context.Background(), prov)\n\t\t\tadminErr := admin.NewError(admin.ErrorBadRequestType, \"webhook name is required\")\n\t\t\tadminErr.Message = \"webhook name is required\"\n\t\t\tbody := []byte(`{\"url\": \"https://example.com\", \"kind\": \"ENRICHING\"}`)\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\tbody:       body,\n\t\t\t\terr:        adminErr,\n\t\t\t\tstatusCode: 400,\n\t\t\t}\n\t\t},\n\t\t\"fail/missing-url\": func(t *testing.T) test {\n\t\t\tprov := &linkedca.Provisioner{\n\t\t\t\tName: \"provName\",\n\t\t\t}\n\t\t\tctx := linkedca.NewContextWithProvisioner(context.Background(), prov)\n\t\t\tadminErr := admin.NewError(admin.ErrorBadRequestType, \"webhook url is invalid\")\n\t\t\tadminErr.Message = \"webhook url is invalid\"\n\t\t\tbody := []byte(`{\"name\": \"metadata\", \"kind\": \"ENRICHING\"}`)\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\tbody:       body,\n\t\t\t\terr:        adminErr,\n\t\t\t\tstatusCode: 400,\n\t\t\t}\n\t\t},\n\t\t\"fail/relative-url\": func(t *testing.T) test {\n\t\t\tprov := &linkedca.Provisioner{\n\t\t\t\tName: \"provName\",\n\t\t\t}\n\t\t\tctx := linkedca.NewContextWithProvisioner(context.Background(), prov)\n\t\t\tadminErr := admin.NewError(admin.ErrorBadRequestType, \"webhook url is invalid\")\n\t\t\tadminErr.Message = \"webhook url is invalid\"\n\t\t\tbody := []byte(`{\"name\": \"metadata\", \"url\": \"example.com/path\", \"kind\": \"ENRICHING\"}`)\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\tbody:       body,\n\t\t\t\terr:        adminErr,\n\t\t\t\tstatusCode: 400,\n\t\t\t}\n\t\t},\n\t\t\"fail/http-url\": func(t *testing.T) test {\n\t\t\tprov := &linkedca.Provisioner{\n\t\t\t\tName: \"provName\",\n\t\t\t}\n\t\t\tctx := linkedca.NewContextWithProvisioner(context.Background(), prov)\n\t\t\tadminErr := admin.NewError(admin.ErrorBadRequestType, \"webhook url must use https\")\n\t\t\tadminErr.Message = \"webhook url must use https\"\n\t\t\tbody := []byte(`{\"name\": \"metadata\", \"url\": \"http://example.com\", \"kind\": \"ENRICHING\"}`)\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\tbody:       body,\n\t\t\t\terr:        adminErr,\n\t\t\t\tstatusCode: 400,\n\t\t\t}\n\t\t},\n\t\t\"fail/basic-auth-in-url\": func(t *testing.T) test {\n\t\t\tprov := &linkedca.Provisioner{\n\t\t\t\tName: \"provName\",\n\t\t\t}\n\t\t\tctx := linkedca.NewContextWithProvisioner(context.Background(), prov)\n\t\t\tadminErr := admin.NewError(admin.ErrorBadRequestType, \"webhook url may not contain username or password\")\n\t\t\tadminErr.Message = \"webhook url may not contain username or password\"\n\t\t\tbody := []byte(`\n\t\t\t{\n\t\t\t\t\"name\": \"metadata\",\n\t\t\t\t\"url\": \"https://user:pass@example.com\",\n\t\t\t\t\"kind\": \"ENRICHING\"\n\t\t\t}`)\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\tbody:       body,\n\t\t\t\terr:        adminErr,\n\t\t\t\tstatusCode: 400,\n\t\t\t}\n\t\t},\n\t\t\"fail/secret-in-request\": func(t *testing.T) test {\n\t\t\tprov := &linkedca.Provisioner{\n\t\t\t\tName: \"provName\",\n\t\t\t}\n\t\t\tctx := linkedca.NewContextWithProvisioner(context.Background(), prov)\n\t\t\tadminErr := admin.NewError(admin.ErrorBadRequestType, \"webhook secret must not be set\")\n\t\t\tadminErr.Message = \"webhook secret must not be set\"\n\t\t\tbody := []byte(`\n\t\t\t{\n\t\t\t\t\"name\": \"metadata\",\n\t\t\t\t\"url\": \"https://example.com\",\n\t\t\t\t\"kind\": \"ENRICHING\",\n\t\t\t\t\"secret\": \"secret\"\n\t\t\t}`)\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\tbody:       body,\n\t\t\t\terr:        adminErr,\n\t\t\t\tstatusCode: 400,\n\t\t\t}\n\t\t},\n\t\t\"fail/unsupported-webhook-kind\": func(t *testing.T) test {\n\t\t\tprov := &linkedca.Provisioner{\n\t\t\t\tName: \"provName\",\n\t\t\t}\n\t\t\tctx := linkedca.NewContextWithProvisioner(context.Background(), prov)\n\t\t\tadminErr := admin.NewError(admin.ErrorBadRequestType, `(line 5:13): invalid value for enum field kind: \"UNSUPPORTED\"`)\n\t\t\tadminErr.Message = `(line 5:13): invalid value for enum field kind: \"UNSUPPORTED\"`\n\t\t\tbody := []byte(`\n\t\t\t{\n\t\t\t\t\"name\": \"metadata\",\n\t\t\t\t\"url\": \"https://example.com\",\n\t\t\t\t\"kind\": \"UNSUPPORTED\",\n\t\t\t}`)\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\tbody:       body,\n\t\t\t\terr:        adminErr,\n\t\t\t\tstatusCode: 400,\n\t\t\t}\n\t\t},\n\t\t\"fail/auth.UpdateProvisioner-error\": func(t *testing.T) test {\n\t\t\tadm := &linkedca.Admin{\n\t\t\t\tSubject: \"step\",\n\t\t\t}\n\t\t\tprov := &linkedca.Provisioner{\n\t\t\t\tName: \"provName\",\n\t\t\t}\n\t\t\tctx := linkedca.NewContextWithAdmin(context.Background(), adm)\n\t\t\tctx = linkedca.NewContextWithProvisioner(ctx, prov)\n\t\t\tadminErr := admin.NewError(admin.ErrorServerInternalType, \"error creating provisioner webhook: force\")\n\t\t\tadminErr.Message = \"error creating provisioner webhook: force\"\n\t\t\tbody := []byte(`{\"name\": \"metadata\", \"url\": \"https://example.com\", \"kind\": \"ENRICHING\"}`)\n\t\t\treturn test{\n\t\t\t\tctx: ctx,\n\t\t\t\tauth: &mockAdminAuthority{\n\t\t\t\t\tMockUpdateProvisioner: func(ctx context.Context, nu *linkedca.Provisioner) error {\n\t\t\t\t\t\treturn &authority.PolicyError{\n\t\t\t\t\t\t\tTyp: authority.StoreFailure,\n\t\t\t\t\t\t\tErr: errors.New(\"force\"),\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tbody:       body,\n\t\t\t\terr:        adminErr,\n\t\t\t\tstatusCode: 500,\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tprov := &linkedca.Provisioner{\n\t\t\t\tName: \"provName\",\n\t\t\t}\n\t\t\tctx := linkedca.NewContextWithProvisioner(context.Background(), prov)\n\t\t\tbody := []byte(`{\"name\": \"metadata\", \"url\": \"https://example.com\", \"kind\": \"ENRICHING\", \"certType\": \"X509\"}`)\n\t\t\treturn test{\n\t\t\t\tctx: ctx,\n\t\t\t\tauth: &mockAdminAuthority{\n\t\t\t\t\tMockUpdateProvisioner: func(ctx context.Context, nu *linkedca.Provisioner) error {\n\t\t\t\t\t\tassert.Equal(t, linkedca.Webhook_X509, nu.Webhooks[0].CertType)\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tbody: body,\n\t\t\t\tresponse: &linkedca.Webhook{\n\t\t\t\t\tName:     \"metadata\",\n\t\t\t\t\tUrl:      \"https://example.com\",\n\t\t\t\t\tKind:     linkedca.Webhook_ENRICHING,\n\t\t\t\t\tCertType: linkedca.Webhook_X509,\n\t\t\t\t},\n\t\t\t\tstatusCode: 201,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, prep := range tests {\n\t\ttc := prep(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tmockMustAuthority(t, tc.auth)\n\t\t\tctx := admin.NewContext(tc.ctx, &admin.MockDB{})\n\t\t\twar := NewWebhookAdminResponder()\n\n\t\t\treq := httptest.NewRequest(\"POST\", \"/foo\", io.NopCloser(bytes.NewBuffer(tc.body)))\n\t\t\treq = req.WithContext(ctx)\n\t\t\tw := httptest.NewRecorder()\n\n\t\t\twar.CreateProvisionerWebhook(w, req)\n\t\t\tres := w.Result()\n\n\t\t\tassert.Equal(t, tc.statusCode, res.StatusCode)\n\n\t\t\tif res.StatusCode >= 400 {\n\n\t\t\t\tbody, err := io.ReadAll(res.Body)\n\t\t\t\tres.Body.Close()\n\t\t\t\tassert.NoError(t, err)\n\n\t\t\t\tae := testAdminError{}\n\t\t\t\tassert.NoError(t, json.Unmarshal(bytes.TrimSpace(body), &ae))\n\n\t\t\t\tassert.Equal(t, tc.err.Type, ae.Type)\n\t\t\t\tassert.Equal(t, tc.err.StatusCode(), res.StatusCode)\n\t\t\t\tassert.Equal(t, tc.err.Detail, ae.Detail)\n\t\t\t\tassert.Equal(t, []string{\"application/json\"}, res.Header[\"Content-Type\"])\n\n\t\t\t\t// when the error message starts with \"proto\", we expect it to have\n\t\t\t\t// a syntax error (in the tests). If the message doesn't start with \"proto\",\n\t\t\t\t// we expect a full string match.\n\t\t\t\tif strings.HasPrefix(tc.err.Message, \"proto:\") {\n\t\t\t\t\tassert.True(t, strings.Contains(ae.Message, \"syntax error\"))\n\t\t\t\t} else {\n\t\t\t\t\tassert.Equal(t, tc.err.Message, ae.Message)\n\t\t\t\t}\n\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tresp := &linkedca.Webhook{}\n\t\t\tbody, err := io.ReadAll(res.Body)\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.NoError(t, protojson.Unmarshal(body, resp))\n\n\t\t\tassertEqualWebhook(t, tc.response, resp)\n\t\t\tassert.NotEmpty(t, resp.Secret)\n\t\t\tassert.NotEmpty(t, resp.Id)\n\t\t})\n\t}\n}\n\nfunc TestWebhookAdminResponder_DeleteProvisionerWebhook(t *testing.T) {\n\ttype test struct {\n\t\tauth                adminAuthority\n\t\terr                 *admin.Error\n\t\tstatusCode          int\n\t\tprovisionerWebhooks []*linkedca.Webhook\n\t\twebhookName         string\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/auth.UpdateProvisioner-error\": func(t *testing.T) test {\n\t\t\tadminErr := admin.NewError(admin.ErrorServerInternalType, \"error deleting provisioner webhook: force\")\n\t\t\tadminErr.Message = \"error deleting provisioner webhook: force\"\n\t\t\treturn test{\n\t\t\t\terr: adminErr,\n\t\t\t\tauth: &mockAdminAuthority{\n\t\t\t\t\tMockUpdateProvisioner: func(ctx context.Context, nu *linkedca.Provisioner) error {\n\t\t\t\t\t\treturn &authority.PolicyError{\n\t\t\t\t\t\t\tTyp: authority.StoreFailure,\n\t\t\t\t\t\t\tErr: errors.New(\"force\"),\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tstatusCode:  500,\n\t\t\t\twebhookName: \"my-webhook\",\n\t\t\t\tprovisionerWebhooks: []*linkedca.Webhook{\n\t\t\t\t\t{Name: \"my-webhook\", Url: \"https://example.com\", Kind: linkedca.Webhook_ENRICHING},\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"ok/not-found\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tstatusCode:          200,\n\t\t\t\twebhookName:         \"no-exists\",\n\t\t\t\tprovisionerWebhooks: nil,\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tstatusCode:  200,\n\t\t\t\twebhookName: \"exists\",\n\t\t\t\tauth: &mockAdminAuthority{\n\t\t\t\t\tMockUpdateProvisioner: func(ctx context.Context, nu *linkedca.Provisioner) error {\n\t\t\t\t\t\tassert.Equal(t, nu.Webhooks, []*linkedca.Webhook{\n\t\t\t\t\t\t\t{Name: \"my-2nd-webhook\", Url: \"https://example.com\", Kind: linkedca.Webhook_ENRICHING},\n\t\t\t\t\t\t})\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tprovisionerWebhooks: []*linkedca.Webhook{\n\t\t\t\t\t{Name: \"exists\", Url: \"https.example.com\", Kind: linkedca.Webhook_ENRICHING},\n\t\t\t\t\t{Name: \"my-2nd-webhook\", Url: \"https://example.com\", Kind: linkedca.Webhook_ENRICHING},\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t}\n\tfor name, prep := range tests {\n\t\ttc := prep(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tmockMustAuthority(t, tc.auth)\n\n\t\t\tchiCtx := chi.NewRouteContext()\n\t\t\tchiCtx.URLParams.Add(\"webhookName\", tc.webhookName)\n\t\t\tctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)\n\t\t\tprov := &linkedca.Provisioner{\n\t\t\t\tName:     \"provName\",\n\t\t\t\tWebhooks: tc.provisionerWebhooks,\n\t\t\t}\n\t\t\tctx = linkedca.NewContextWithProvisioner(ctx, prov)\n\t\t\tctx = admin.NewContext(ctx, &admin.MockDB{})\n\t\t\treq := httptest.NewRequest(\"DELETE\", \"/foo\", http.NoBody).WithContext(ctx)\n\n\t\t\twar := NewWebhookAdminResponder()\n\n\t\t\tw := httptest.NewRecorder()\n\n\t\t\twar.DeleteProvisionerWebhook(w, req)\n\t\t\tres := w.Result()\n\n\t\t\tassert.Equal(t, tc.statusCode, res.StatusCode)\n\n\t\t\tif res.StatusCode >= 400 {\n\n\t\t\t\tbody, err := io.ReadAll(res.Body)\n\t\t\t\tres.Body.Close()\n\t\t\t\tassert.NoError(t, err)\n\n\t\t\t\tae := testAdminError{}\n\t\t\t\tassert.NoError(t, json.Unmarshal(bytes.TrimSpace(body), &ae))\n\n\t\t\t\tassert.Equal(t, tc.err.Type, ae.Type)\n\t\t\t\tassert.Equal(t, tc.err.StatusCode(), res.StatusCode)\n\t\t\t\tassert.Equal(t, tc.err.Detail, ae.Detail)\n\t\t\t\tassert.Equal(t, []string{\"application/json\"}, res.Header[\"Content-Type\"])\n\n\t\t\t\t// when the error message starts with \"proto\", we expect it to have\n\t\t\t\t// a syntax error (in the tests). If the message doesn't start with \"proto\",\n\t\t\t\t// we expect a full string match.\n\t\t\t\tif strings.HasPrefix(tc.err.Message, \"proto:\") {\n\t\t\t\t\tassert.True(t, strings.Contains(ae.Message, \"syntax error\"))\n\t\t\t\t} else {\n\t\t\t\t\tassert.Equal(t, tc.err.Message, ae.Message)\n\t\t\t\t}\n\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tbody, err := io.ReadAll(res.Body)\n\t\t\tassert.NoError(t, err)\n\t\t\tres.Body.Close()\n\t\t\tresponse := DeleteResponse{}\n\t\t\tassert.NoError(t, json.Unmarshal(bytes.TrimSpace(body), &response))\n\t\t\tassert.Equal(t, \"ok\", response.Status)\n\t\t\tassert.Equal(t, []string{\"application/json\"}, res.Header[\"Content-Type\"])\n\t\t})\n\t}\n}\n\nfunc TestWebhookAdminResponder_UpdateProvisionerWebhook(t *testing.T) {\n\ttype test struct {\n\t\tauth       adminAuthority\n\t\tadminDB    admin.DB\n\t\tbody       []byte\n\t\tctx        context.Context\n\t\terr        *admin.Error\n\t\tresponse   *linkedca.Webhook\n\t\tstatusCode int\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/not-found\": func(t *testing.T) test {\n\t\t\tprov := &linkedca.Provisioner{\n\t\t\t\tName:     \"provName\",\n\t\t\t\tWebhooks: []*linkedca.Webhook{{Name: \"exists\", Url: \"https://example.com\", Kind: linkedca.Webhook_ENRICHING}},\n\t\t\t}\n\t\t\tctx := linkedca.NewContextWithProvisioner(context.Background(), prov)\n\t\t\terr := admin.NewError(admin.ErrorNotFoundType, `provisioner \"provName\" has no webhook with the name \"no-exists\"`)\n\t\t\terr.Message = `provisioner \"provName\" has no webhook with the name \"no-exists\"`\n\t\t\tbody := []byte(`\n\t\t\t{\n\t\t\t\t\"name\": \"no-exists\",\n\t\t\t\t\"url\": \"https://example.com\",\n\t\t\t\t\"kind\": \"ENRICHING\"\n\t\t\t}`)\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\tadminDB:    &admin.MockDB{},\n\t\t\t\tbody:       body,\n\t\t\t\terr:        err,\n\t\t\t\tstatusCode: 404,\n\t\t\t}\n\t\t},\n\t\t\"fail/read.ProtoJSON\": func(t *testing.T) test {\n\t\t\tprov := &linkedca.Provisioner{\n\t\t\t\tName:     \"provName\",\n\t\t\t\tWebhooks: []*linkedca.Webhook{{Name: \"my-webhook\", Url: \"https://example.com\", Kind: linkedca.Webhook_ENRICHING}},\n\t\t\t}\n\t\t\tctx := linkedca.NewContextWithProvisioner(context.Background(), prov)\n\t\t\tadminErr := admin.NewError(admin.ErrorBadRequestType, \"proto: syntax error (line 1:2): invalid value ?\")\n\t\t\tadminErr.Message = \"proto: syntax error (line 1:2): invalid value ?\"\n\t\t\tbody := []byte(\"{?}\")\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\tadminDB:    &admin.MockDB{},\n\t\t\t\tbody:       body,\n\t\t\t\terr:        adminErr,\n\t\t\t\tstatusCode: 400,\n\t\t\t}\n\t\t},\n\t\t\"fail/missing-name\": func(t *testing.T) test {\n\t\t\tprov := &linkedca.Provisioner{\n\t\t\t\tName:     \"provName\",\n\t\t\t\tWebhooks: []*linkedca.Webhook{{Name: \"my-webhook\", Url: \"https://example.com\", Kind: linkedca.Webhook_ENRICHING}},\n\t\t\t}\n\t\t\tctx := linkedca.NewContextWithProvisioner(context.Background(), prov)\n\t\t\tadminErr := admin.NewError(admin.ErrorBadRequestType, \"webhook name is required\")\n\t\t\tadminErr.Message = \"webhook name is required\"\n\t\t\tbody := []byte(`{\"url\": \"https://example.com\", \"kind\": \"ENRICHING\"}`)\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\tadminDB:    &admin.MockDB{},\n\t\t\t\tbody:       body,\n\t\t\t\terr:        adminErr,\n\t\t\t\tstatusCode: 400,\n\t\t\t}\n\t\t},\n\t\t\"fail/missing-url\": func(t *testing.T) test {\n\t\t\tprov := &linkedca.Provisioner{\n\t\t\t\tName:     \"provName\",\n\t\t\t\tWebhooks: []*linkedca.Webhook{{Name: \"my-webhook\", Url: \"https://example.com\", Kind: linkedca.Webhook_ENRICHING}},\n\t\t\t}\n\t\t\tctx := linkedca.NewContextWithProvisioner(context.Background(), prov)\n\t\t\tadminErr := admin.NewError(admin.ErrorBadRequestType, \"webhook url is invalid\")\n\t\t\tadminErr.Message = \"webhook url is invalid\"\n\t\t\tbody := []byte(`{\"name\": \"metadata\", \"kind\": \"ENRICHING\"}`)\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\tadminDB:    &admin.MockDB{},\n\t\t\t\tbody:       body,\n\t\t\t\terr:        adminErr,\n\t\t\t\tstatusCode: 400,\n\t\t\t}\n\t\t},\n\t\t\"fail/relative-url\": func(t *testing.T) test {\n\t\t\tprov := &linkedca.Provisioner{\n\t\t\t\tName:     \"provName\",\n\t\t\t\tWebhooks: []*linkedca.Webhook{{Name: \"my-webhook\", Url: \"https://example.com\", Kind: linkedca.Webhook_ENRICHING}},\n\t\t\t}\n\t\t\tctx := linkedca.NewContextWithProvisioner(context.Background(), prov)\n\t\t\tadminErr := admin.NewError(admin.ErrorBadRequestType, \"webhook url is invalid\")\n\t\t\tadminErr.Message = \"webhook url is invalid\"\n\t\t\tbody := []byte(`{\"name\": \"metadata\", \"url\": \"example.com/path\", \"kind\": \"ENRICHING\"}`)\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\tadminDB:    &admin.MockDB{},\n\t\t\t\tbody:       body,\n\t\t\t\terr:        adminErr,\n\t\t\t\tstatusCode: 400,\n\t\t\t}\n\t\t},\n\t\t\"fail/http-url\": func(t *testing.T) test {\n\t\t\tprov := &linkedca.Provisioner{\n\t\t\t\tName:     \"provName\",\n\t\t\t\tWebhooks: []*linkedca.Webhook{{Name: \"my-webhook\", Url: \"https://example.com\", Kind: linkedca.Webhook_ENRICHING}},\n\t\t\t}\n\t\t\tctx := linkedca.NewContextWithProvisioner(context.Background(), prov)\n\t\t\tadminErr := admin.NewError(admin.ErrorBadRequestType, \"webhook url must use https\")\n\t\t\tadminErr.Message = \"webhook url must use https\"\n\t\t\tbody := []byte(`{\"name\": \"metadata\", \"url\": \"http://example.com\", \"kind\": \"ENRICHING\"}`)\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\tadminDB:    &admin.MockDB{},\n\t\t\t\tbody:       body,\n\t\t\t\terr:        adminErr,\n\t\t\t\tstatusCode: 400,\n\t\t\t}\n\t\t},\n\t\t\"fail/basic-auth-in-url\": func(t *testing.T) test {\n\t\t\tprov := &linkedca.Provisioner{\n\t\t\t\tName:     \"provName\",\n\t\t\t\tWebhooks: []*linkedca.Webhook{{Name: \"my-webhook\", Url: \"https://example.com\", Kind: linkedca.Webhook_ENRICHING}},\n\t\t\t}\n\t\t\tctx := linkedca.NewContextWithProvisioner(context.Background(), prov)\n\t\t\tadminErr := admin.NewError(admin.ErrorBadRequestType, \"webhook url may not contain username or password\")\n\t\t\tadminErr.Message = \"webhook url may not contain username or password\"\n\t\t\tbody := []byte(`\n\t\t\t{\n\t\t\t\t\"name\": \"my-webhook\",\n\t\t\t\t\"url\": \"https://user:pass@example.com\",\n\t\t\t\t\"kind\": \"ENRICHING\"\n\t\t\t}`)\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\tadminDB:    &admin.MockDB{},\n\t\t\t\tbody:       body,\n\t\t\t\terr:        adminErr,\n\t\t\t\tstatusCode: 400,\n\t\t\t}\n\t\t},\n\t\t\"fail/different-secret-in-request\": func(t *testing.T) test {\n\t\t\tprov := &linkedca.Provisioner{\n\t\t\t\tName:     \"provName\",\n\t\t\t\tWebhooks: []*linkedca.Webhook{{Name: \"my-webhook\", Url: \"https://example.com\", Kind: linkedca.Webhook_ENRICHING, Secret: \"c2VjcmV0\"}},\n\t\t\t}\n\t\t\tctx := linkedca.NewContextWithProvisioner(context.Background(), prov)\n\t\t\tadminErr := admin.NewError(admin.ErrorBadRequestType, \"webhook secret cannot be updated\")\n\t\t\tadminErr.Message = \"webhook secret cannot be updated\"\n\t\t\tbody := []byte(`\n\t\t\t{\n\t\t\t\t\"name\": \"my-webhook\",\n\t\t\t\t\"url\": \"https://example.com\",\n\t\t\t\t\"kind\": \"ENRICHING\",\n\t\t\t\t\"secret\": \"secret\"\n\t\t\t}`)\n\t\t\treturn test{\n\t\t\t\tctx:        ctx,\n\t\t\t\tbody:       body,\n\t\t\t\terr:        adminErr,\n\t\t\t\tstatusCode: 400,\n\t\t\t}\n\t\t},\n\t\t\"fail/auth.UpdateProvisioner-error\": func(t *testing.T) test {\n\t\t\tprov := &linkedca.Provisioner{\n\t\t\t\tName:     \"provName\",\n\t\t\t\tWebhooks: []*linkedca.Webhook{{Name: \"my-webhook\", Url: \"https://example.com\", Kind: linkedca.Webhook_ENRICHING}},\n\t\t\t}\n\t\t\tctx := linkedca.NewContextWithProvisioner(context.Background(), prov)\n\t\t\tadminErr := admin.NewError(admin.ErrorServerInternalType, \"error updating provisioner webhook: force\")\n\t\t\tadminErr.Message = \"error updating provisioner webhook: force\"\n\t\t\tbody := []byte(`{\"name\": \"my-webhook\", \"url\": \"https://example.com\", \"kind\": \"ENRICHING\"}`)\n\t\t\treturn test{\n\t\t\t\tctx:     ctx,\n\t\t\t\tadminDB: &admin.MockDB{},\n\t\t\t\tauth: &mockAdminAuthority{\n\t\t\t\t\tMockUpdateProvisioner: func(ctx context.Context, nu *linkedca.Provisioner) error {\n\t\t\t\t\t\treturn &authority.PolicyError{\n\t\t\t\t\t\t\tTyp: authority.StoreFailure,\n\t\t\t\t\t\t\tErr: errors.New(\"force\"),\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tbody:       body,\n\t\t\t\terr:        adminErr,\n\t\t\t\tstatusCode: 500,\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tprov := &linkedca.Provisioner{\n\t\t\t\tName:     \"provName\",\n\t\t\t\tWebhooks: []*linkedca.Webhook{{Name: \"my-webhook\", Url: \"https://example.com\", Kind: linkedca.Webhook_ENRICHING}},\n\t\t\t}\n\t\t\tctx := linkedca.NewContextWithProvisioner(context.Background(), prov)\n\t\t\tbody := []byte(`{\"name\": \"my-webhook\", \"url\": \"https://example.com\", \"kind\": \"ENRICHING\"}`)\n\t\t\treturn test{\n\t\t\t\tctx:     ctx,\n\t\t\t\tadminDB: &admin.MockDB{},\n\t\t\t\tauth: &mockAdminAuthority{\n\t\t\t\t\tMockUpdateProvisioner: func(ctx context.Context, nu *linkedca.Provisioner) error {\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tbody: body,\n\t\t\t\tresponse: &linkedca.Webhook{\n\t\t\t\t\tName: \"my-webhook\",\n\t\t\t\t\tUrl:  \"https://example.com\",\n\t\t\t\t\tKind: linkedca.Webhook_ENRICHING,\n\t\t\t\t},\n\t\t\t\tstatusCode: 201,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, prep := range tests {\n\t\ttc := prep(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tmockMustAuthority(t, tc.auth)\n\t\t\tctx := admin.NewContext(tc.ctx, tc.adminDB)\n\t\t\twar := NewWebhookAdminResponder()\n\n\t\t\treq := httptest.NewRequest(\"PUT\", \"/foo\", io.NopCloser(bytes.NewBuffer(tc.body)))\n\t\t\treq = req.WithContext(ctx)\n\t\t\tw := httptest.NewRecorder()\n\n\t\t\twar.UpdateProvisionerWebhook(w, req)\n\t\t\tres := w.Result()\n\n\t\t\tassert.Equal(t, tc.statusCode, res.StatusCode)\n\n\t\t\tif res.StatusCode >= 400 {\n\n\t\t\t\tbody, err := io.ReadAll(res.Body)\n\t\t\t\tres.Body.Close()\n\t\t\t\tassert.NoError(t, err)\n\n\t\t\t\tae := testAdminError{}\n\t\t\t\tassert.NoError(t, json.Unmarshal(bytes.TrimSpace(body), &ae))\n\n\t\t\t\tassert.Equal(t, tc.err.Type, ae.Type)\n\t\t\t\tassert.Equal(t, tc.err.StatusCode(), res.StatusCode)\n\t\t\t\tassert.Equal(t, tc.err.Detail, ae.Detail)\n\t\t\t\tassert.Equal(t, []string{\"application/json\"}, res.Header[\"Content-Type\"])\n\n\t\t\t\t// when the error message starts with \"proto\", we expect it to have\n\t\t\t\t// a syntax error (in the tests). If the message doesn't start with \"proto\",\n\t\t\t\t// we expect a full string match.\n\t\t\t\tif strings.HasPrefix(tc.err.Message, \"proto:\") {\n\t\t\t\t\tassert.True(t, strings.Contains(ae.Message, \"syntax error\"))\n\t\t\t\t} else {\n\t\t\t\t\tassert.Equal(t, tc.err.Message, ae.Message)\n\t\t\t\t}\n\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tresp := &linkedca.Webhook{}\n\t\t\tbody, err := io.ReadAll(res.Body)\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.NoError(t, protojson.Unmarshal(body, resp))\n\n\t\t\tassertEqualWebhook(t, tc.response, resp)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "authority/admin/db/nosql/admin.go",
    "content": "package nosql\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n\t\"github.com/smallstep/certificates/authority/admin\"\n\t\"github.com/smallstep/linkedca\"\n\t\"github.com/smallstep/nosql\"\n\t\"google.golang.org/protobuf/types/known/timestamppb\"\n)\n\n// dbAdmin is the database representation of the Admin type.\ntype dbAdmin struct {\n\tID            string              `json:\"id\"`\n\tAuthorityID   string              `json:\"authorityID\"`\n\tProvisionerID string              `json:\"provisionerID\"`\n\tSubject       string              `json:\"subject\"`\n\tType          linkedca.Admin_Type `json:\"type\"`\n\tCreatedAt     time.Time           `json:\"createdAt\"`\n\tDeletedAt     time.Time           `json:\"deletedAt\"`\n}\n\nfunc (dba *dbAdmin) convert() *linkedca.Admin {\n\treturn &linkedca.Admin{\n\t\tId:            dba.ID,\n\t\tAuthorityId:   dba.AuthorityID,\n\t\tProvisionerId: dba.ProvisionerID,\n\t\tSubject:       dba.Subject,\n\t\tType:          dba.Type,\n\t\tCreatedAt:     timestamppb.New(dba.CreatedAt),\n\t\tDeletedAt:     timestamppb.New(dba.DeletedAt),\n\t}\n}\n\nfunc (dba *dbAdmin) clone() *dbAdmin {\n\tu := *dba\n\treturn &u\n}\n\nfunc (db *DB) getDBAdminBytes(_ context.Context, id string) ([]byte, error) {\n\tdata, err := db.db.Get(adminsTable, []byte(id))\n\tif nosql.IsErrNotFound(err) {\n\t\treturn nil, admin.NewError(admin.ErrorNotFoundType, \"admin %s not found\", id)\n\t} else if err != nil {\n\t\treturn nil, errors.Wrapf(err, \"error loading admin %s\", id)\n\t}\n\treturn data, nil\n}\n\nfunc (db *DB) unmarshalDBAdmin(data []byte, id string) (*dbAdmin, error) {\n\tvar dba = new(dbAdmin)\n\tif err := json.Unmarshal(data, dba); err != nil {\n\t\treturn nil, errors.Wrapf(err, \"error unmarshaling admin %s into dbAdmin\", id)\n\t}\n\tif !dba.DeletedAt.IsZero() {\n\t\treturn nil, admin.NewError(admin.ErrorDeletedType, \"admin %s is deleted\", id)\n\t}\n\tif dba.AuthorityID != db.authorityID {\n\t\treturn nil, admin.NewError(admin.ErrorAuthorityMismatchType,\n\t\t\t\"admin %s is not owned by authority %s\", dba.ID, db.authorityID)\n\t}\n\treturn dba, nil\n}\n\nfunc (db *DB) getDBAdmin(ctx context.Context, id string) (*dbAdmin, error) {\n\tdata, err := db.getDBAdminBytes(ctx, id)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdba, err := db.unmarshalDBAdmin(data, id)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn dba, nil\n}\n\nfunc (db *DB) unmarshalAdmin(data []byte, id string) (*linkedca.Admin, error) {\n\tdba, err := db.unmarshalDBAdmin(data, id)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn dba.convert(), nil\n}\n\n// GetAdmin retrieves and unmarshals a admin from the database.\nfunc (db *DB) GetAdmin(ctx context.Context, id string) (*linkedca.Admin, error) {\n\tdata, err := db.getDBAdminBytes(ctx, id)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tadm, err := db.unmarshalAdmin(data, id)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn adm, nil\n}\n\n// GetAdmins retrieves and unmarshals all active (not deleted) admins\n// from the database.\n// TODO should we be paginating?\nfunc (db *DB) GetAdmins(context.Context) ([]*linkedca.Admin, error) {\n\tdbEntries, err := db.db.List(adminsTable)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"error loading admins\")\n\t}\n\tvar admins = []*linkedca.Admin{}\n\tfor _, entry := range dbEntries {\n\t\tadm, err := db.unmarshalAdmin(entry.Value, string(entry.Key))\n\t\tif err != nil {\n\t\t\tvar ae *admin.Error\n\t\t\tif errors.As(err, &ae) {\n\t\t\t\tif ae.IsType(admin.ErrorDeletedType) || ae.IsType(admin.ErrorAuthorityMismatchType) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\treturn nil, err\n\t\t}\n\t\tif adm.AuthorityId != db.authorityID {\n\t\t\tcontinue\n\t\t}\n\t\tadmins = append(admins, adm)\n\t}\n\treturn admins, nil\n}\n\n// CreateAdmin stores a new admin to the database.\nfunc (db *DB) CreateAdmin(ctx context.Context, adm *linkedca.Admin) error {\n\tvar err error\n\tadm.Id, err = randID()\n\tif err != nil {\n\t\treturn admin.WrapErrorISE(err, \"error generating random id for admin\")\n\t}\n\tadm.AuthorityId = db.authorityID\n\n\tdba := &dbAdmin{\n\t\tID:            adm.Id,\n\t\tAuthorityID:   db.authorityID,\n\t\tProvisionerID: adm.ProvisionerId,\n\t\tSubject:       adm.Subject,\n\t\tType:          adm.Type,\n\t\tCreatedAt:     clock.Now(),\n\t}\n\n\treturn db.save(ctx, dba.ID, dba, nil, \"admin\", adminsTable)\n}\n\n// UpdateAdmin saves an updated admin to the database.\nfunc (db *DB) UpdateAdmin(ctx context.Context, adm *linkedca.Admin) error {\n\told, err := db.getDBAdmin(ctx, adm.Id)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tnu := old.clone()\n\tnu.Type = adm.Type\n\n\treturn db.save(ctx, old.ID, nu, old, \"admin\", adminsTable)\n}\n\n// DeleteAdmin saves an updated admin to the database.\nfunc (db *DB) DeleteAdmin(ctx context.Context, id string) error {\n\told, err := db.getDBAdmin(ctx, id)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tnu := old.clone()\n\tnu.DeletedAt = clock.Now()\n\n\treturn db.save(ctx, old.ID, nu, old, \"admin\", adminsTable)\n}\n"
  },
  {
    "path": "authority/admin/db/nosql/admin_test.go",
    "content": "package nosql\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n\t\"github.com/smallstep/assert\"\n\t\"github.com/smallstep/certificates/authority/admin\"\n\t\"github.com/smallstep/certificates/db\"\n\t\"github.com/smallstep/linkedca\"\n\t\"github.com/smallstep/nosql\"\n\tnosqldb \"github.com/smallstep/nosql/database\"\n\t\"google.golang.org/protobuf/types/known/timestamppb\"\n)\n\nfunc TestDB_getDBAdminBytes(t *testing.T) {\n\tadminID := \"adminID\"\n\ttype test struct {\n\t\tdb       nosql.DB\n\t\terr      error\n\t\tadminErr *admin.Error\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/not-found\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, adminsTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), adminID)\n\n\t\t\t\t\t\treturn nil, nosqldb.ErrNotFound\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tadminErr: admin.NewError(admin.ErrorNotFoundType, \"admin adminID not found\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/db.Get-error\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, adminsTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), adminID)\n\n\t\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"error loading admin adminID: force\"),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, adminsTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), adminID)\n\n\t\t\t\t\t\treturn []byte(\"foo\"), nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\ttc := run(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\td := DB{db: tc.db}\n\t\t\tif b, err := d.getDBAdminBytes(context.Background(), adminID); err != nil {\n\t\t\t\tvar ae *admin.Error\n\t\t\t\tif errors.As(err, &ae) {\n\t\t\t\t\tif assert.NotNil(t, tc.adminErr) {\n\t\t\t\t\t\tassert.Equals(t, ae.Type, tc.adminErr.Type)\n\t\t\t\t\t\tassert.Equals(t, ae.Detail, tc.adminErr.Detail)\n\t\t\t\t\t\tassert.Equals(t, ae.Status, tc.adminErr.Status)\n\t\t\t\t\t\tassert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error())\n\t\t\t\t\t\tassert.Equals(t, ae.Detail, tc.adminErr.Detail)\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.err.Error())\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else if assert.Nil(t, tc.err) {\n\t\t\t\tassert.Equals(t, string(b), \"foo\")\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDB_getDBAdmin(t *testing.T) {\n\tadminID := \"adminID\"\n\ttype test struct {\n\t\tdb       nosql.DB\n\t\terr      error\n\t\tadminErr *admin.Error\n\t\tdba      *dbAdmin\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/not-found\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, adminsTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), adminID)\n\n\t\t\t\t\t\treturn nil, nosqldb.ErrNotFound\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tadminErr: admin.NewError(admin.ErrorNotFoundType, \"admin adminID not found\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/db.Get-error\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, adminsTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), adminID)\n\n\t\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"error loading admin adminID: force\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/unmarshal-error\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, adminsTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), adminID)\n\n\t\t\t\t\t\treturn []byte(\"foo\"), nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"error unmarshaling admin adminID into dbAdmin\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/deleted\": func(t *testing.T) test {\n\t\t\tnow := clock.Now()\n\t\t\tdba := &dbAdmin{\n\t\t\t\tID:            adminID,\n\t\t\t\tAuthorityID:   admin.DefaultAuthorityID,\n\t\t\t\tProvisionerID: \"provID\",\n\t\t\t\tSubject:       \"max@smallstep.com\",\n\t\t\t\tType:          linkedca.Admin_SUPER_ADMIN,\n\t\t\t\tCreatedAt:     now,\n\t\t\t\tDeletedAt:     now,\n\t\t\t}\n\t\t\tb, err := json.Marshal(dba)\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, adminsTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), adminID)\n\n\t\t\t\t\t\treturn b, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tadminErr: admin.NewError(admin.ErrorDeletedType, \"admin adminID is deleted\"),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tnow := clock.Now()\n\t\t\tdba := &dbAdmin{\n\t\t\t\tID:            adminID,\n\t\t\t\tAuthorityID:   admin.DefaultAuthorityID,\n\t\t\t\tProvisionerID: \"provID\",\n\t\t\t\tSubject:       \"max@smallstep.com\",\n\t\t\t\tType:          linkedca.Admin_SUPER_ADMIN,\n\t\t\t\tCreatedAt:     now,\n\t\t\t}\n\t\t\tb, err := json.Marshal(dba)\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, adminsTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), adminID)\n\n\t\t\t\t\t\treturn b, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tdba: dba,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\ttc := run(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\td := DB{db: tc.db, authorityID: admin.DefaultAuthorityID}\n\t\t\tif dba, err := d.getDBAdmin(context.Background(), adminID); err != nil {\n\t\t\t\tvar ae *admin.Error\n\t\t\t\tif errors.As(err, &ae) {\n\t\t\t\t\tif assert.NotNil(t, tc.adminErr) {\n\t\t\t\t\t\tassert.Equals(t, ae.Type, tc.adminErr.Type)\n\t\t\t\t\t\tassert.Equals(t, ae.Detail, tc.adminErr.Detail)\n\t\t\t\t\t\tassert.Equals(t, ae.Status, tc.adminErr.Status)\n\t\t\t\t\t\tassert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error())\n\t\t\t\t\t\tassert.Equals(t, ae.Detail, tc.adminErr.Detail)\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.err.Error())\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else if assert.Nil(t, tc.err) && assert.Nil(t, tc.adminErr) {\n\t\t\t\tassert.Equals(t, dba.ID, adminID)\n\t\t\t\tassert.Equals(t, dba.AuthorityID, tc.dba.AuthorityID)\n\t\t\t\tassert.Equals(t, dba.ProvisionerID, tc.dba.ProvisionerID)\n\t\t\t\tassert.Equals(t, dba.Subject, tc.dba.Subject)\n\t\t\t\tassert.Equals(t, dba.Type, tc.dba.Type)\n\t\t\t\tassert.Equals(t, dba.CreatedAt, tc.dba.CreatedAt)\n\t\t\t\tassert.Fatal(t, dba.DeletedAt.IsZero())\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDB_unmarshalDBAdmin(t *testing.T) {\n\tadminID := \"adminID\"\n\ttype test struct {\n\t\tin       []byte\n\t\terr      error\n\t\tadminErr *admin.Error\n\t\tdba      *dbAdmin\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/unmarshal-error\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tin:  []byte(\"foo\"),\n\t\t\t\terr: errors.New(\"error unmarshaling admin adminID into dbAdmin\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/deleted-error\": func(t *testing.T) test {\n\t\t\tdba := &dbAdmin{\n\t\t\t\tDeletedAt: time.Now(),\n\t\t\t}\n\t\t\tdata, err := json.Marshal(dba)\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tin:       data,\n\t\t\t\tadminErr: admin.NewError(admin.ErrorDeletedType, \"admin adminID is deleted\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/authority-mismatch-error\": func(t *testing.T) test {\n\t\t\tdba := &dbAdmin{\n\t\t\t\tID:          adminID,\n\t\t\t\tAuthorityID: \"foo\",\n\t\t\t}\n\t\t\tdata, err := json.Marshal(dba)\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tin: data,\n\t\t\t\tadminErr: admin.NewError(admin.ErrorAuthorityMismatchType,\n\t\t\t\t\t\"admin %s is not owned by authority %s\", adminID, admin.DefaultAuthorityID),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tdba := &dbAdmin{\n\t\t\t\tID:            adminID,\n\t\t\t\tSubject:       \"max@smallstep.com\",\n\t\t\t\tProvisionerID: \"provID\",\n\t\t\t\tAuthorityID:   admin.DefaultAuthorityID,\n\t\t\t\tType:          linkedca.Admin_SUPER_ADMIN,\n\t\t\t\tCreatedAt:     clock.Now(),\n\t\t\t}\n\t\t\tdata, err := json.Marshal(dba)\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tin:  data,\n\t\t\t\tdba: dba,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\ttc := run(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\td := DB{authorityID: admin.DefaultAuthorityID}\n\t\t\tif dba, err := d.unmarshalDBAdmin(tc.in, adminID); err != nil {\n\t\t\t\tvar ae *admin.Error\n\t\t\t\tif errors.As(err, &ae) {\n\t\t\t\t\tif assert.NotNil(t, tc.adminErr) {\n\t\t\t\t\t\tassert.Equals(t, ae.Type, tc.adminErr.Type)\n\t\t\t\t\t\tassert.Equals(t, ae.Detail, tc.adminErr.Detail)\n\t\t\t\t\t\tassert.Equals(t, ae.Status, tc.adminErr.Status)\n\t\t\t\t\t\tassert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error())\n\t\t\t\t\t\tassert.Equals(t, ae.Detail, tc.adminErr.Detail)\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.err.Error())\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else if assert.Nil(t, tc.err) && assert.Nil(t, tc.adminErr) {\n\t\t\t\tassert.Equals(t, dba.ID, adminID)\n\t\t\t\tassert.Equals(t, dba.AuthorityID, tc.dba.AuthorityID)\n\t\t\t\tassert.Equals(t, dba.ProvisionerID, tc.dba.ProvisionerID)\n\t\t\t\tassert.Equals(t, dba.Subject, tc.dba.Subject)\n\t\t\t\tassert.Equals(t, dba.Type, tc.dba.Type)\n\t\t\t\tassert.Equals(t, dba.CreatedAt, tc.dba.CreatedAt)\n\t\t\t\tassert.Fatal(t, dba.DeletedAt.IsZero())\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDB_unmarshalAdmin(t *testing.T) {\n\tadminID := \"adminID\"\n\ttype test struct {\n\t\tin       []byte\n\t\terr      error\n\t\tadminErr *admin.Error\n\t\tdba      *dbAdmin\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/unmarshal-error\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tin:  []byte(\"foo\"),\n\t\t\t\terr: errors.New(\"error unmarshaling admin adminID into dbAdmin\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/deleted-error\": func(t *testing.T) test {\n\t\t\tdba := &dbAdmin{\n\t\t\t\tDeletedAt: time.Now(),\n\t\t\t}\n\t\t\tdata, err := json.Marshal(dba)\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tin:       data,\n\t\t\t\tadminErr: admin.NewError(admin.ErrorDeletedType, \"admin adminID is deleted\"),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tdba := &dbAdmin{\n\t\t\t\tID:            adminID,\n\t\t\t\tSubject:       \"max@smallstep.com\",\n\t\t\t\tProvisionerID: \"provID\",\n\t\t\t\tAuthorityID:   admin.DefaultAuthorityID,\n\t\t\t\tType:          linkedca.Admin_SUPER_ADMIN,\n\t\t\t\tCreatedAt:     clock.Now(),\n\t\t\t}\n\t\t\tdata, err := json.Marshal(dba)\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tin:  data,\n\t\t\t\tdba: dba,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\ttc := run(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\td := DB{authorityID: admin.DefaultAuthorityID}\n\t\t\tif adm, err := d.unmarshalAdmin(tc.in, adminID); err != nil {\n\t\t\t\tvar ae *admin.Error\n\t\t\t\tif errors.As(err, &ae) {\n\t\t\t\t\tif assert.NotNil(t, tc.adminErr) {\n\t\t\t\t\t\tassert.Equals(t, ae.Type, tc.adminErr.Type)\n\t\t\t\t\t\tassert.Equals(t, ae.Detail, tc.adminErr.Detail)\n\t\t\t\t\t\tassert.Equals(t, ae.Status, tc.adminErr.Status)\n\t\t\t\t\t\tassert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error())\n\t\t\t\t\t\tassert.Equals(t, ae.Detail, tc.adminErr.Detail)\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.err.Error())\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else if assert.Nil(t, tc.err) && assert.Nil(t, tc.adminErr) {\n\t\t\t\tassert.Equals(t, adm.Id, adminID)\n\t\t\t\tassert.Equals(t, adm.AuthorityId, tc.dba.AuthorityID)\n\t\t\t\tassert.Equals(t, adm.ProvisionerId, tc.dba.ProvisionerID)\n\t\t\t\tassert.Equals(t, adm.Subject, tc.dba.Subject)\n\t\t\t\tassert.Equals(t, adm.Type, tc.dba.Type)\n\t\t\t\tassert.Equals(t, adm.CreatedAt, timestamppb.New(tc.dba.CreatedAt))\n\t\t\t\tassert.Equals(t, adm.DeletedAt, timestamppb.New(tc.dba.DeletedAt))\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDB_GetAdmin(t *testing.T) {\n\tadminID := \"adminID\"\n\ttype test struct {\n\t\tdb       nosql.DB\n\t\terr      error\n\t\tadminErr *admin.Error\n\t\tdba      *dbAdmin\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/not-found\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, adminsTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), adminID)\n\n\t\t\t\t\t\treturn nil, nosqldb.ErrNotFound\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tadminErr: admin.NewError(admin.ErrorNotFoundType, \"admin adminID not found\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/db.Get-error\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, adminsTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), adminID)\n\n\t\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"error loading admin adminID: force\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/unmarshal-error\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, adminsTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), adminID)\n\n\t\t\t\t\t\treturn []byte(\"foo\"), nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"error unmarshaling admin adminID into dbAdmin\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/deleted\": func(t *testing.T) test {\n\t\t\tdba := &dbAdmin{\n\t\t\t\tID:            adminID,\n\t\t\t\tAuthorityID:   admin.DefaultAuthorityID,\n\t\t\t\tProvisionerID: \"provID\",\n\t\t\t\tSubject:       \"max@smallstep.com\",\n\t\t\t\tType:          linkedca.Admin_SUPER_ADMIN,\n\t\t\t\tCreatedAt:     clock.Now(),\n\t\t\t\tDeletedAt:     clock.Now(),\n\t\t\t}\n\t\t\tb, err := json.Marshal(dba)\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, adminsTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), adminID)\n\n\t\t\t\t\t\treturn b, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tdba:      dba,\n\t\t\t\tadminErr: admin.NewError(admin.ErrorDeletedType, \"admin adminID is deleted\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/authorityID-mismatch\": func(t *testing.T) test {\n\t\t\tdba := &dbAdmin{\n\t\t\t\tID:            adminID,\n\t\t\t\tAuthorityID:   \"foo\",\n\t\t\t\tProvisionerID: \"provID\",\n\t\t\t\tSubject:       \"max@smallstep.com\",\n\t\t\t\tType:          linkedca.Admin_SUPER_ADMIN,\n\t\t\t\tCreatedAt:     clock.Now(),\n\t\t\t}\n\t\t\tb, err := json.Marshal(dba)\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, adminsTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), adminID)\n\n\t\t\t\t\t\treturn b, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tdba: dba,\n\t\t\t\tadminErr: admin.NewError(admin.ErrorAuthorityMismatchType,\n\t\t\t\t\t\"admin %s is not owned by authority %s\", dba.ID, admin.DefaultAuthorityID),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tdba := &dbAdmin{\n\t\t\t\tID:            adminID,\n\t\t\t\tAuthorityID:   admin.DefaultAuthorityID,\n\t\t\t\tProvisionerID: \"provID\",\n\t\t\t\tSubject:       \"max@smallstep.com\",\n\t\t\t\tType:          linkedca.Admin_SUPER_ADMIN,\n\t\t\t\tCreatedAt:     clock.Now(),\n\t\t\t}\n\t\t\tb, err := json.Marshal(dba)\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, adminsTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), adminID)\n\n\t\t\t\t\t\treturn b, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tdba: dba,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\ttc := run(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\td := DB{db: tc.db, authorityID: admin.DefaultAuthorityID}\n\t\t\tif adm, err := d.GetAdmin(context.Background(), adminID); err != nil {\n\t\t\t\tvar ae *admin.Error\n\t\t\t\tif errors.As(err, &ae) {\n\t\t\t\t\tif assert.NotNil(t, tc.adminErr) {\n\t\t\t\t\t\tassert.Equals(t, ae.Type, tc.adminErr.Type)\n\t\t\t\t\t\tassert.Equals(t, ae.Detail, tc.adminErr.Detail)\n\t\t\t\t\t\tassert.Equals(t, ae.Status, tc.adminErr.Status)\n\t\t\t\t\t\tassert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error())\n\t\t\t\t\t\tassert.Equals(t, ae.Detail, tc.adminErr.Detail)\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.err.Error())\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else if assert.Nil(t, tc.err) && assert.Nil(t, tc.adminErr) {\n\t\t\t\tassert.Equals(t, adm.Id, adminID)\n\t\t\t\tassert.Equals(t, adm.AuthorityId, tc.dba.AuthorityID)\n\t\t\t\tassert.Equals(t, adm.ProvisionerId, tc.dba.ProvisionerID)\n\t\t\t\tassert.Equals(t, adm.Subject, tc.dba.Subject)\n\t\t\t\tassert.Equals(t, adm.Type, tc.dba.Type)\n\t\t\t\tassert.Equals(t, adm.CreatedAt, timestamppb.New(tc.dba.CreatedAt))\n\t\t\t\tassert.Equals(t, adm.DeletedAt, timestamppb.New(tc.dba.DeletedAt))\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDB_DeleteAdmin(t *testing.T) {\n\tadminID := \"adminID\"\n\ttype test struct {\n\t\tdb       nosql.DB\n\t\terr      error\n\t\tadminErr *admin.Error\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/not-found\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, adminsTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), adminID)\n\n\t\t\t\t\t\treturn nil, nosqldb.ErrNotFound\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tadminErr: admin.NewError(admin.ErrorNotFoundType, \"admin adminID not found\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/db.Get-error\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, adminsTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), adminID)\n\n\t\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"error loading admin adminID: force\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/save-error\": func(t *testing.T) test {\n\t\t\tdba := &dbAdmin{\n\t\t\t\tID:            adminID,\n\t\t\t\tAuthorityID:   admin.DefaultAuthorityID,\n\t\t\t\tProvisionerID: \"provID\",\n\t\t\t\tSubject:       \"max@smallstep.com\",\n\t\t\t\tType:          linkedca.Admin_SUPER_ADMIN,\n\t\t\t\tCreatedAt:     clock.Now(),\n\t\t\t}\n\t\t\tdata, err := json.Marshal(dba)\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, adminsTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), adminID)\n\n\t\t\t\t\t\treturn data, nil\n\t\t\t\t\t},\n\t\t\t\t\tMCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, adminsTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), adminID)\n\t\t\t\t\t\tassert.Equals(t, string(old), string(data))\n\n\t\t\t\t\t\tvar _dba = new(dbAdmin)\n\t\t\t\t\t\tassert.FatalError(t, json.Unmarshal(nu, _dba))\n\n\t\t\t\t\t\tassert.Equals(t, _dba.ID, dba.ID)\n\t\t\t\t\t\tassert.Equals(t, _dba.AuthorityID, dba.AuthorityID)\n\t\t\t\t\t\tassert.Equals(t, _dba.ProvisionerID, dba.ProvisionerID)\n\t\t\t\t\t\tassert.Equals(t, _dba.Subject, dba.Subject)\n\t\t\t\t\t\tassert.Equals(t, _dba.Type, dba.Type)\n\t\t\t\t\t\tassert.Equals(t, _dba.CreatedAt, dba.CreatedAt)\n\n\t\t\t\t\t\tassert.True(t, _dba.DeletedAt.Before(time.Now()))\n\t\t\t\t\t\tassert.True(t, _dba.DeletedAt.After(time.Now().Add(-time.Minute)))\n\n\t\t\t\t\t\treturn nil, false, errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"error saving authority admin: force\"),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tdba := &dbAdmin{\n\t\t\t\tID:            adminID,\n\t\t\t\tAuthorityID:   admin.DefaultAuthorityID,\n\t\t\t\tProvisionerID: \"provID\",\n\t\t\t\tSubject:       \"max@smallstep.com\",\n\t\t\t\tType:          linkedca.Admin_SUPER_ADMIN,\n\t\t\t\tCreatedAt:     clock.Now(),\n\t\t\t}\n\t\t\tdata, err := json.Marshal(dba)\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, adminsTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), adminID)\n\n\t\t\t\t\t\treturn data, nil\n\t\t\t\t\t},\n\t\t\t\t\tMCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, adminsTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), adminID)\n\t\t\t\t\t\tassert.Equals(t, string(old), string(data))\n\n\t\t\t\t\t\tvar _dba = new(dbAdmin)\n\t\t\t\t\t\tassert.FatalError(t, json.Unmarshal(nu, _dba))\n\n\t\t\t\t\t\tassert.Equals(t, _dba.ID, dba.ID)\n\t\t\t\t\t\tassert.Equals(t, _dba.AuthorityID, dba.AuthorityID)\n\t\t\t\t\t\tassert.Equals(t, _dba.ProvisionerID, dba.ProvisionerID)\n\t\t\t\t\t\tassert.Equals(t, _dba.Subject, dba.Subject)\n\t\t\t\t\t\tassert.Equals(t, _dba.Type, dba.Type)\n\t\t\t\t\t\tassert.Equals(t, _dba.CreatedAt, dba.CreatedAt)\n\n\t\t\t\t\t\tassert.True(t, _dba.DeletedAt.Before(time.Now()))\n\t\t\t\t\t\tassert.True(t, _dba.DeletedAt.After(time.Now().Add(-time.Minute)))\n\n\t\t\t\t\t\treturn nu, true, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\ttc := run(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\td := DB{db: tc.db, authorityID: admin.DefaultAuthorityID}\n\t\t\tif err := d.DeleteAdmin(context.Background(), adminID); err != nil {\n\t\t\t\tvar ae *admin.Error\n\t\t\t\tif errors.As(err, &ae) {\n\t\t\t\t\tif assert.NotNil(t, tc.adminErr) {\n\t\t\t\t\t\tassert.Equals(t, ae.Type, tc.adminErr.Type)\n\t\t\t\t\t\tassert.Equals(t, ae.Detail, tc.adminErr.Detail)\n\t\t\t\t\t\tassert.Equals(t, ae.Status, tc.adminErr.Status)\n\t\t\t\t\t\tassert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error())\n\t\t\t\t\t\tassert.Equals(t, ae.Detail, tc.adminErr.Detail)\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.err.Error())\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDB_UpdateAdmin(t *testing.T) {\n\tadminID := \"adminID\"\n\ttype test struct {\n\t\tdb       nosql.DB\n\t\terr      error\n\t\tadminErr *admin.Error\n\t\tadm      *linkedca.Admin\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/not-found\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tadm: &linkedca.Admin{Id: adminID},\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, adminsTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), adminID)\n\n\t\t\t\t\t\treturn nil, nosqldb.ErrNotFound\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tadminErr: admin.NewError(admin.ErrorNotFoundType, \"admin adminID not found\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/db.Get-error\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tadm: &linkedca.Admin{Id: adminID},\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, adminsTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), adminID)\n\n\t\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"error loading admin adminID: force\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/save-error\": func(t *testing.T) test {\n\t\t\tdba := &dbAdmin{\n\t\t\t\tID:            adminID,\n\t\t\t\tAuthorityID:   admin.DefaultAuthorityID,\n\t\t\t\tProvisionerID: \"provID\",\n\t\t\t\tSubject:       \"max@smallstep.com\",\n\t\t\t\tType:          linkedca.Admin_SUPER_ADMIN,\n\t\t\t\tCreatedAt:     clock.Now(),\n\t\t\t}\n\n\t\t\tupd := dba.convert()\n\t\t\tupd.Type = linkedca.Admin_ADMIN\n\n\t\t\tdata, err := json.Marshal(dba)\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tadm: upd,\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, adminsTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), adminID)\n\n\t\t\t\t\t\treturn data, nil\n\t\t\t\t\t},\n\t\t\t\t\tMCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, adminsTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), adminID)\n\t\t\t\t\t\tassert.Equals(t, string(old), string(data))\n\n\t\t\t\t\t\tvar _dba = new(dbAdmin)\n\t\t\t\t\t\tassert.FatalError(t, json.Unmarshal(nu, _dba))\n\n\t\t\t\t\t\tassert.Equals(t, _dba.ID, dba.ID)\n\t\t\t\t\t\tassert.Equals(t, _dba.AuthorityID, dba.AuthorityID)\n\t\t\t\t\t\tassert.Equals(t, _dba.ProvisionerID, dba.ProvisionerID)\n\t\t\t\t\t\tassert.Equals(t, _dba.Subject, dba.Subject)\n\t\t\t\t\t\tassert.Equals(t, _dba.Type, linkedca.Admin_ADMIN)\n\t\t\t\t\t\tassert.Equals(t, _dba.CreatedAt, dba.CreatedAt)\n\n\t\t\t\t\t\treturn nil, false, errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"error saving authority admin: force\"),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tdba := &dbAdmin{\n\t\t\t\tID:            adminID,\n\t\t\t\tAuthorityID:   admin.DefaultAuthorityID,\n\t\t\t\tProvisionerID: \"provID\",\n\t\t\t\tSubject:       \"max@smallstep.com\",\n\t\t\t\tType:          linkedca.Admin_SUPER_ADMIN,\n\t\t\t\tCreatedAt:     clock.Now(),\n\t\t\t}\n\n\t\t\tupd := dba.convert()\n\t\t\tupd.Type = linkedca.Admin_ADMIN\n\n\t\t\tdata, err := json.Marshal(dba)\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tadm: upd,\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, adminsTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), adminID)\n\n\t\t\t\t\t\treturn data, nil\n\t\t\t\t\t},\n\t\t\t\t\tMCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, adminsTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), adminID)\n\t\t\t\t\t\tassert.Equals(t, string(old), string(data))\n\n\t\t\t\t\t\tvar _dba = new(dbAdmin)\n\t\t\t\t\t\tassert.FatalError(t, json.Unmarshal(nu, _dba))\n\n\t\t\t\t\t\tassert.Equals(t, _dba.ID, dba.ID)\n\t\t\t\t\t\tassert.Equals(t, _dba.AuthorityID, dba.AuthorityID)\n\t\t\t\t\t\tassert.Equals(t, _dba.ProvisionerID, dba.ProvisionerID)\n\t\t\t\t\t\tassert.Equals(t, _dba.Subject, dba.Subject)\n\t\t\t\t\t\tassert.Equals(t, _dba.Type, linkedca.Admin_ADMIN)\n\t\t\t\t\t\tassert.Equals(t, _dba.CreatedAt, dba.CreatedAt)\n\n\t\t\t\t\t\treturn nu, true, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\ttc := run(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\td := DB{db: tc.db, authorityID: admin.DefaultAuthorityID}\n\t\t\tif err := d.UpdateAdmin(context.Background(), tc.adm); err != nil {\n\t\t\t\tvar ae *admin.Error\n\t\t\t\tif errors.As(err, &ae) {\n\t\t\t\t\tif assert.NotNil(t, tc.adminErr) {\n\t\t\t\t\t\tassert.Equals(t, ae.Type, tc.adminErr.Type)\n\t\t\t\t\t\tassert.Equals(t, ae.Detail, tc.adminErr.Detail)\n\t\t\t\t\t\tassert.Equals(t, ae.Status, tc.adminErr.Status)\n\t\t\t\t\t\tassert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error())\n\t\t\t\t\t\tassert.Equals(t, ae.Detail, tc.adminErr.Detail)\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.err.Error())\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDB_CreateAdmin(t *testing.T) {\n\ttype test struct {\n\t\tdb       nosql.DB\n\t\terr      error\n\t\tadminErr *admin.Error\n\t\tadm      *linkedca.Admin\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/save-error\": func(t *testing.T) test {\n\t\t\tadm := &linkedca.Admin{\n\t\t\t\tAuthorityId:   admin.DefaultAuthorityID,\n\t\t\t\tProvisionerId: \"provID\",\n\t\t\t\tSubject:       \"max@smallstep.com\",\n\t\t\t\tType:          linkedca.Admin_ADMIN,\n\t\t\t}\n\n\t\t\treturn test{\n\t\t\t\tadm: adm,\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, adminsTable)\n\t\t\t\t\t\tassert.Equals(t, old, nil)\n\n\t\t\t\t\t\tvar _dba = new(dbAdmin)\n\t\t\t\t\t\tassert.FatalError(t, json.Unmarshal(nu, _dba))\n\n\t\t\t\t\t\tassert.True(t, _dba.ID != \"\" && _dba.ID == string(key))\n\t\t\t\t\t\tassert.Equals(t, _dba.AuthorityID, adm.AuthorityId)\n\t\t\t\t\t\tassert.Equals(t, _dba.ProvisionerID, adm.ProvisionerId)\n\t\t\t\t\t\tassert.Equals(t, _dba.Subject, adm.Subject)\n\t\t\t\t\t\tassert.Equals(t, _dba.Type, linkedca.Admin_ADMIN)\n\n\t\t\t\t\t\tassert.True(t, _dba.CreatedAt.Before(time.Now()))\n\t\t\t\t\t\tassert.True(t, _dba.CreatedAt.After(time.Now().Add(-time.Minute)))\n\n\t\t\t\t\t\treturn nil, false, errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"error saving authority admin: force\"),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tadm := &linkedca.Admin{\n\t\t\t\tAuthorityId:   admin.DefaultAuthorityID,\n\t\t\t\tProvisionerId: \"provID\",\n\t\t\t\tSubject:       \"max@smallstep.com\",\n\t\t\t\tType:          linkedca.Admin_ADMIN,\n\t\t\t}\n\n\t\t\treturn test{\n\t\t\t\tadm: adm,\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, adminsTable)\n\t\t\t\t\t\tassert.Equals(t, old, nil)\n\n\t\t\t\t\t\tvar _dba = new(dbAdmin)\n\t\t\t\t\t\tassert.FatalError(t, json.Unmarshal(nu, _dba))\n\n\t\t\t\t\t\tassert.True(t, _dba.ID != \"\" && _dba.ID == string(key))\n\t\t\t\t\t\tassert.Equals(t, _dba.AuthorityID, adm.AuthorityId)\n\t\t\t\t\t\tassert.Equals(t, _dba.ProvisionerID, adm.ProvisionerId)\n\t\t\t\t\t\tassert.Equals(t, _dba.Subject, adm.Subject)\n\t\t\t\t\t\tassert.Equals(t, _dba.Type, linkedca.Admin_ADMIN)\n\n\t\t\t\t\t\tassert.True(t, _dba.CreatedAt.Before(time.Now()))\n\t\t\t\t\t\tassert.True(t, _dba.CreatedAt.After(time.Now().Add(-time.Minute)))\n\n\t\t\t\t\t\treturn nu, true, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\ttc := run(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\td := DB{db: tc.db, authorityID: admin.DefaultAuthorityID}\n\t\t\tif err := d.CreateAdmin(context.Background(), tc.adm); err != nil {\n\t\t\t\tvar ae *admin.Error\n\t\t\t\tif errors.As(err, &ae) {\n\t\t\t\t\tif assert.NotNil(t, tc.adminErr) {\n\t\t\t\t\t\tassert.Equals(t, ae.Type, tc.adminErr.Type)\n\t\t\t\t\t\tassert.Equals(t, ae.Detail, tc.adminErr.Detail)\n\t\t\t\t\t\tassert.Equals(t, ae.Status, tc.adminErr.Status)\n\t\t\t\t\t\tassert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error())\n\t\t\t\t\t\tassert.Equals(t, ae.Detail, tc.adminErr.Detail)\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.err.Error())\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDB_GetAdmins(t *testing.T) {\n\tnow := clock.Now()\n\tfooAdmin := &dbAdmin{\n\t\tID:            \"foo\",\n\t\tAuthorityID:   admin.DefaultAuthorityID,\n\t\tProvisionerID: \"provID\",\n\t\tSubject:       \"foo@smallstep.com\",\n\t\tType:          linkedca.Admin_SUPER_ADMIN,\n\t\tCreatedAt:     now,\n\t}\n\tfoob, err := json.Marshal(fooAdmin)\n\tassert.FatalError(t, err)\n\n\tbarAdmin := &dbAdmin{\n\t\tID:            \"bar\",\n\t\tAuthorityID:   admin.DefaultAuthorityID,\n\t\tProvisionerID: \"provID\",\n\t\tSubject:       \"bar@smallstep.com\",\n\t\tType:          linkedca.Admin_ADMIN,\n\t\tCreatedAt:     now,\n\t\tDeletedAt:     now,\n\t}\n\tbarb, err := json.Marshal(barAdmin)\n\tassert.FatalError(t, err)\n\n\tbazAdmin := &dbAdmin{\n\t\tID:            \"baz\",\n\t\tAuthorityID:   \"bazzer\",\n\t\tProvisionerID: \"provID\",\n\t\tSubject:       \"baz@smallstep.com\",\n\t\tType:          linkedca.Admin_ADMIN,\n\t\tCreatedAt:     now,\n\t}\n\tbazb, err := json.Marshal(bazAdmin)\n\tassert.FatalError(t, err)\n\n\tzapAdmin := &dbAdmin{\n\t\tID:            \"zap\",\n\t\tAuthorityID:   admin.DefaultAuthorityID,\n\t\tProvisionerID: \"provID\",\n\t\tSubject:       \"zap@smallstep.com\",\n\t\tType:          linkedca.Admin_ADMIN,\n\t\tCreatedAt:     now,\n\t}\n\tzapb, err := json.Marshal(zapAdmin)\n\tassert.FatalError(t, err)\n\ttype test struct {\n\t\tdb       nosql.DB\n\t\terr      error\n\t\tadminErr *admin.Error\n\t\tverify   func(*testing.T, []*linkedca.Admin)\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/db.List-error\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMList: func(bucket []byte) ([]*nosqldb.Entry, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, adminsTable)\n\n\t\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"error loading admins: force\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/unmarshal-error\": func(t *testing.T) test {\n\t\t\tret := []*nosqldb.Entry{\n\t\t\t\t{Bucket: adminsTable, Key: []byte(\"foo\"), Value: foob},\n\t\t\t\t{Bucket: adminsTable, Key: []byte(\"bar\"), Value: barb},\n\t\t\t\t{Bucket: adminsTable, Key: []byte(\"zap\"), Value: []byte(\"zap\")},\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMList: func(bucket []byte) ([]*nosqldb.Entry, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, adminsTable)\n\n\t\t\t\t\t\treturn ret, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"error unmarshaling admin zap into dbAdmin\"),\n\t\t\t}\n\t\t},\n\t\t\"ok/none\": func(t *testing.T) test {\n\t\t\tret := []*nosqldb.Entry{}\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMList: func(bucket []byte) ([]*nosqldb.Entry, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, adminsTable)\n\n\t\t\t\t\t\treturn ret, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tverify: func(t *testing.T, admins []*linkedca.Admin) {\n\t\t\t\t\tassert.Equals(t, len(admins), 0)\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"ok/only-invalid\": func(t *testing.T) test {\n\t\t\tret := []*nosqldb.Entry{\n\t\t\t\t{Bucket: adminsTable, Key: []byte(\"bar\"), Value: barb},\n\t\t\t\t{Bucket: adminsTable, Key: []byte(\"baz\"), Value: bazb},\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMList: func(bucket []byte) ([]*nosqldb.Entry, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, adminsTable)\n\n\t\t\t\t\t\treturn ret, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tverify: func(t *testing.T, admins []*linkedca.Admin) {\n\t\t\t\t\tassert.Equals(t, len(admins), 0)\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tret := []*nosqldb.Entry{\n\t\t\t\t{Bucket: adminsTable, Key: []byte(\"foo\"), Value: foob},\n\t\t\t\t{Bucket: adminsTable, Key: []byte(\"bar\"), Value: barb},\n\t\t\t\t{Bucket: adminsTable, Key: []byte(\"baz\"), Value: bazb},\n\t\t\t\t{Bucket: adminsTable, Key: []byte(\"zap\"), Value: zapb},\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMList: func(bucket []byte) ([]*nosqldb.Entry, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, adminsTable)\n\n\t\t\t\t\t\treturn ret, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tverify: func(t *testing.T, admins []*linkedca.Admin) {\n\t\t\t\t\tassert.Equals(t, len(admins), 2)\n\n\t\t\t\t\tassert.Equals(t, admins[0].Id, fooAdmin.ID)\n\t\t\t\t\tassert.Equals(t, admins[0].AuthorityId, fooAdmin.AuthorityID)\n\t\t\t\t\tassert.Equals(t, admins[0].ProvisionerId, fooAdmin.ProvisionerID)\n\t\t\t\t\tassert.Equals(t, admins[0].Subject, fooAdmin.Subject)\n\t\t\t\t\tassert.Equals(t, admins[0].Type, fooAdmin.Type)\n\t\t\t\t\tassert.Equals(t, admins[0].CreatedAt, timestamppb.New(fooAdmin.CreatedAt))\n\t\t\t\t\tassert.Equals(t, admins[0].DeletedAt, timestamppb.New(fooAdmin.DeletedAt))\n\n\t\t\t\t\tassert.Equals(t, admins[1].Id, zapAdmin.ID)\n\t\t\t\t\tassert.Equals(t, admins[1].AuthorityId, zapAdmin.AuthorityID)\n\t\t\t\t\tassert.Equals(t, admins[1].ProvisionerId, zapAdmin.ProvisionerID)\n\t\t\t\t\tassert.Equals(t, admins[1].Subject, zapAdmin.Subject)\n\t\t\t\t\tassert.Equals(t, admins[1].Type, zapAdmin.Type)\n\t\t\t\t\tassert.Equals(t, admins[1].CreatedAt, timestamppb.New(zapAdmin.CreatedAt))\n\t\t\t\t\tassert.Equals(t, admins[1].DeletedAt, timestamppb.New(zapAdmin.DeletedAt))\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\ttc := run(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\td := DB{db: tc.db, authorityID: admin.DefaultAuthorityID}\n\t\t\tif admins, err := d.GetAdmins(context.Background()); err != nil {\n\t\t\t\tvar ae *admin.Error\n\t\t\t\tif errors.As(err, &ae) {\n\t\t\t\t\tif assert.NotNil(t, tc.adminErr) {\n\t\t\t\t\t\tassert.Equals(t, ae.Type, tc.adminErr.Type)\n\t\t\t\t\t\tassert.Equals(t, ae.Detail, tc.adminErr.Detail)\n\t\t\t\t\t\tassert.Equals(t, ae.Status, tc.adminErr.Status)\n\t\t\t\t\t\tassert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error())\n\t\t\t\t\t\tassert.Equals(t, ae.Detail, tc.adminErr.Detail)\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.err.Error())\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else if assert.Nil(t, tc.err) && assert.Nil(t, tc.adminErr) {\n\t\t\t\ttc.verify(t, admins)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "authority/admin/db/nosql/nosql.go",
    "content": "package nosql\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n\tnosqlDB \"github.com/smallstep/nosql/database\"\n\t\"go.step.sm/crypto/randutil\"\n)\n\nvar (\n\tadminsTable            = []byte(\"admins\")\n\tprovisionersTable      = []byte(\"provisioners\")\n\tauthorityPoliciesTable = []byte(\"authority_policies\")\n)\n\n// DB is a struct that implements the AdminDB interface.\ntype DB struct {\n\tdb          nosqlDB.DB\n\tauthorityID string\n}\n\n// New configures and returns a new Authority DB backend implemented using a nosql DB.\nfunc New(db nosqlDB.DB, authorityID string) (*DB, error) {\n\ttables := [][]byte{adminsTable, provisionersTable, authorityPoliciesTable}\n\tfor _, b := range tables {\n\t\tif err := db.CreateTable(b); err != nil {\n\t\t\treturn nil, errors.Wrapf(err, \"error creating table %s\",\n\t\t\t\tstring(b))\n\t\t}\n\t}\n\treturn &DB{db, authorityID}, nil\n}\n\n// save writes the new data to the database, overwriting the old data if it\n// existed.\nfunc (db *DB) save(_ context.Context, id string, nu, old interface{}, typ string, table []byte) error {\n\tvar (\n\t\terr  error\n\t\tnewB []byte\n\t)\n\tif nu == nil {\n\t\tnewB = nil\n\t} else {\n\t\tnewB, err = json.Marshal(nu)\n\t\tif err != nil {\n\t\t\treturn errors.Wrapf(err, \"error marshaling authority type: %s, value: %v\", typ, nu)\n\t\t}\n\t}\n\tvar oldB []byte\n\tif old == nil {\n\t\toldB = nil\n\t} else {\n\t\toldB, err = json.Marshal(old)\n\t\tif err != nil {\n\t\t\treturn errors.Wrapf(err, \"error marshaling admin type: %s, value: %v\", typ, old)\n\t\t}\n\t}\n\n\t_, swapped, err := db.db.CmpAndSwap(table, []byte(id), oldB, newB)\n\tswitch {\n\tcase err != nil:\n\t\treturn errors.Wrapf(err, \"error saving authority %s\", typ)\n\tcase !swapped:\n\t\treturn errors.Errorf(\"error saving authority %s; changed since last read\", typ)\n\tdefault:\n\t\treturn nil\n\t}\n}\n\nfunc randID() (val string, err error) {\n\tval, err = randutil.UUIDv4()\n\tif err != nil {\n\t\treturn \"\", errors.Wrap(err, \"error generating random alphanumeric ID\")\n\t}\n\treturn val, nil\n}\n\n// Clock that returns time in UTC rounded to seconds.\ntype Clock struct{}\n\n// Now returns the UTC time rounded to seconds.\nfunc (c *Clock) Now() time.Time {\n\treturn time.Now().UTC().Truncate(time.Second)\n}\n\nvar clock = new(Clock)\n"
  },
  {
    "path": "authority/admin/db/nosql/policy.go",
    "content": "package nosql\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\n\t\"github.com/smallstep/linkedca\"\n\n\t\"github.com/smallstep/certificates/authority/admin\"\n\t\"github.com/smallstep/nosql\"\n)\n\ntype dbX509Policy struct {\n\tAllow              *dbX509Names `json:\"allow,omitempty\"`\n\tDeny               *dbX509Names `json:\"deny,omitempty\"`\n\tAllowWildcardNames bool         `json:\"allow_wildcard_names,omitempty\"`\n}\n\ntype dbX509Names struct {\n\tCommonNames    []string `json:\"cn,omitempty\"`\n\tDNSDomains     []string `json:\"dns,omitempty\"`\n\tIPRanges       []string `json:\"ip,omitempty\"`\n\tEmailAddresses []string `json:\"email,omitempty\"`\n\tURIDomains     []string `json:\"uri,omitempty\"`\n}\n\ntype dbSSHPolicy struct {\n\t// User contains SSH user certificate options.\n\tUser *dbSSHUserPolicy `json:\"user,omitempty\"`\n\t// Host contains SSH host certificate options.\n\tHost *dbSSHHostPolicy `json:\"host,omitempty\"`\n}\n\ntype dbSSHHostPolicy struct {\n\tAllow *dbSSHHostNames `json:\"allow,omitempty\"`\n\tDeny  *dbSSHHostNames `json:\"deny,omitempty\"`\n}\n\ntype dbSSHHostNames struct {\n\tDNSDomains []string `json:\"dns,omitempty\"`\n\tIPRanges   []string `json:\"ip,omitempty\"`\n\tPrincipals []string `json:\"principal,omitempty\"`\n}\n\ntype dbSSHUserPolicy struct {\n\tAllow *dbSSHUserNames `json:\"allow,omitempty\"`\n\tDeny  *dbSSHUserNames `json:\"deny,omitempty\"`\n}\n\ntype dbSSHUserNames struct {\n\tEmailAddresses []string `json:\"email,omitempty\"`\n\tPrincipals     []string `json:\"principal,omitempty\"`\n}\n\ntype dbPolicy struct {\n\tX509 *dbX509Policy `json:\"x509,omitempty\"`\n\tSSH  *dbSSHPolicy  `json:\"ssh,omitempty\"`\n}\n\ntype dbAuthorityPolicy struct {\n\tID          string    `json:\"id\"`\n\tAuthorityID string    `json:\"authorityID\"`\n\tPolicy      *dbPolicy `json:\"policy,omitempty\"`\n}\n\nfunc (dbap *dbAuthorityPolicy) convert() *linkedca.Policy {\n\tif dbap == nil {\n\t\treturn nil\n\t}\n\treturn dbToLinked(dbap.Policy)\n}\n\nfunc (db *DB) getDBAuthorityPolicyBytes(_ context.Context, authorityID string) ([]byte, error) {\n\tdata, err := db.db.Get(authorityPoliciesTable, []byte(authorityID))\n\tif nosql.IsErrNotFound(err) {\n\t\treturn nil, admin.NewError(admin.ErrorNotFoundType, \"authority policy not found\")\n\t} else if err != nil {\n\t\treturn nil, fmt.Errorf(\"error loading authority policy: %w\", err)\n\t}\n\treturn data, nil\n}\n\nfunc (db *DB) unmarshalDBAuthorityPolicy(data []byte) (*dbAuthorityPolicy, error) {\n\tif len(data) == 0 {\n\t\t//nolint:nilnil // legacy\n\t\treturn nil, nil\n\t}\n\tvar dba = new(dbAuthorityPolicy)\n\tif err := json.Unmarshal(data, dba); err != nil {\n\t\treturn nil, fmt.Errorf(\"error unmarshaling policy bytes into dbAuthorityPolicy: %w\", err)\n\t}\n\treturn dba, nil\n}\n\nfunc (db *DB) getDBAuthorityPolicy(ctx context.Context, authorityID string) (*dbAuthorityPolicy, error) {\n\tdata, err := db.getDBAuthorityPolicyBytes(ctx, authorityID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdbap, err := db.unmarshalDBAuthorityPolicy(data)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif dbap == nil {\n\t\t//nolint:nilnil // legacy\n\t\treturn nil, nil\n\t}\n\tif dbap.AuthorityID != authorityID {\n\t\treturn nil, admin.NewError(admin.ErrorAuthorityMismatchType,\n\t\t\t\"authority policy is not owned by authority %s\", authorityID)\n\t}\n\treturn dbap, nil\n}\n\nfunc (db *DB) CreateAuthorityPolicy(ctx context.Context, policy *linkedca.Policy) error {\n\tdbap := &dbAuthorityPolicy{\n\t\tID:          db.authorityID,\n\t\tAuthorityID: db.authorityID,\n\t\tPolicy:      linkedToDB(policy),\n\t}\n\n\tif err := db.save(ctx, dbap.ID, dbap, nil, \"authority_policy\", authorityPoliciesTable); err != nil {\n\t\treturn admin.WrapErrorISE(err, \"error creating authority policy\")\n\t}\n\n\treturn nil\n}\n\nfunc (db *DB) GetAuthorityPolicy(ctx context.Context) (*linkedca.Policy, error) {\n\tdbap, err := db.getDBAuthorityPolicy(ctx, db.authorityID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn dbap.convert(), nil\n}\n\nfunc (db *DB) UpdateAuthorityPolicy(ctx context.Context, policy *linkedca.Policy) error {\n\told, err := db.getDBAuthorityPolicy(ctx, db.authorityID)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tdbap := &dbAuthorityPolicy{\n\t\tID:          db.authorityID,\n\t\tAuthorityID: db.authorityID,\n\t\tPolicy:      linkedToDB(policy),\n\t}\n\n\tif err := db.save(ctx, dbap.ID, dbap, old, \"authority_policy\", authorityPoliciesTable); err != nil {\n\t\treturn admin.WrapErrorISE(err, \"error updating authority policy\")\n\t}\n\n\treturn nil\n}\n\nfunc (db *DB) DeleteAuthorityPolicy(ctx context.Context) error {\n\told, err := db.getDBAuthorityPolicy(ctx, db.authorityID)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif err := db.save(ctx, old.ID, nil, old, \"authority_policy\", authorityPoliciesTable); err != nil {\n\t\treturn admin.WrapErrorISE(err, \"error deleting authority policy\")\n\t}\n\n\treturn nil\n}\n\nfunc dbToLinked(p *dbPolicy) *linkedca.Policy {\n\tif p == nil {\n\t\treturn nil\n\t}\n\tr := &linkedca.Policy{}\n\tif x509 := p.X509; x509 != nil {\n\t\tr.X509 = &linkedca.X509Policy{}\n\t\tif allow := x509.Allow; allow != nil {\n\t\t\tr.X509.Allow = &linkedca.X509Names{}\n\t\t\tr.X509.Allow.Dns = allow.DNSDomains\n\t\t\tr.X509.Allow.Emails = allow.EmailAddresses\n\t\t\tr.X509.Allow.Ips = allow.IPRanges\n\t\t\tr.X509.Allow.Uris = allow.URIDomains\n\t\t\tr.X509.Allow.CommonNames = allow.CommonNames\n\t\t}\n\t\tif deny := x509.Deny; deny != nil {\n\t\t\tr.X509.Deny = &linkedca.X509Names{}\n\t\t\tr.X509.Deny.Dns = deny.DNSDomains\n\t\t\tr.X509.Deny.Emails = deny.EmailAddresses\n\t\t\tr.X509.Deny.Ips = deny.IPRanges\n\t\t\tr.X509.Deny.Uris = deny.URIDomains\n\t\t\tr.X509.Deny.CommonNames = deny.CommonNames\n\t\t}\n\t\tr.X509.AllowWildcardNames = x509.AllowWildcardNames\n\t}\n\tif ssh := p.SSH; ssh != nil {\n\t\tr.Ssh = &linkedca.SSHPolicy{}\n\t\tif host := ssh.Host; host != nil {\n\t\t\tr.Ssh.Host = &linkedca.SSHHostPolicy{}\n\t\t\tif allow := host.Allow; allow != nil {\n\t\t\t\tr.Ssh.Host.Allow = &linkedca.SSHHostNames{}\n\t\t\t\tr.Ssh.Host.Allow.Dns = allow.DNSDomains\n\t\t\t\tr.Ssh.Host.Allow.Ips = allow.IPRanges\n\t\t\t\tr.Ssh.Host.Allow.Principals = allow.Principals\n\t\t\t}\n\t\t\tif deny := host.Deny; deny != nil {\n\t\t\t\tr.Ssh.Host.Deny = &linkedca.SSHHostNames{}\n\t\t\t\tr.Ssh.Host.Deny.Dns = deny.DNSDomains\n\t\t\t\tr.Ssh.Host.Deny.Ips = deny.IPRanges\n\t\t\t\tr.Ssh.Host.Deny.Principals = deny.Principals\n\t\t\t}\n\t\t}\n\t\tif user := ssh.User; user != nil {\n\t\t\tr.Ssh.User = &linkedca.SSHUserPolicy{}\n\t\t\tif allow := user.Allow; allow != nil {\n\t\t\t\tr.Ssh.User.Allow = &linkedca.SSHUserNames{}\n\t\t\t\tr.Ssh.User.Allow.Emails = allow.EmailAddresses\n\t\t\t\tr.Ssh.User.Allow.Principals = allow.Principals\n\t\t\t}\n\t\t\tif deny := user.Deny; deny != nil {\n\t\t\t\tr.Ssh.User.Deny = &linkedca.SSHUserNames{}\n\t\t\t\tr.Ssh.User.Deny.Emails = deny.EmailAddresses\n\t\t\t\tr.Ssh.User.Deny.Principals = deny.Principals\n\t\t\t}\n\t\t}\n\t}\n\n\treturn r\n}\n\nfunc linkedToDB(p *linkedca.Policy) *dbPolicy {\n\tif p == nil {\n\t\treturn nil\n\t}\n\n\t// return early if x509 nor SSH is set\n\tif p.GetX509() == nil && p.GetSsh() == nil {\n\t\treturn nil\n\t}\n\n\tr := &dbPolicy{}\n\t// fill x509 policy configuration\n\tif x509 := p.GetX509(); x509 != nil {\n\t\tr.X509 = &dbX509Policy{}\n\t\tif allow := x509.GetAllow(); allow != nil {\n\t\t\tr.X509.Allow = &dbX509Names{}\n\t\t\tif allow.Dns != nil {\n\t\t\t\tr.X509.Allow.DNSDomains = allow.Dns\n\t\t\t}\n\t\t\tif allow.Ips != nil {\n\t\t\t\tr.X509.Allow.IPRanges = allow.Ips\n\t\t\t}\n\t\t\tif allow.Emails != nil {\n\t\t\t\tr.X509.Allow.EmailAddresses = allow.Emails\n\t\t\t}\n\t\t\tif allow.Uris != nil {\n\t\t\t\tr.X509.Allow.URIDomains = allow.Uris\n\t\t\t}\n\t\t\tif allow.CommonNames != nil {\n\t\t\t\tr.X509.Allow.CommonNames = allow.CommonNames\n\t\t\t}\n\t\t}\n\t\tif deny := x509.GetDeny(); deny != nil {\n\t\t\tr.X509.Deny = &dbX509Names{}\n\t\t\tif deny.Dns != nil {\n\t\t\t\tr.X509.Deny.DNSDomains = deny.Dns\n\t\t\t}\n\t\t\tif deny.Ips != nil {\n\t\t\t\tr.X509.Deny.IPRanges = deny.Ips\n\t\t\t}\n\t\t\tif deny.Emails != nil {\n\t\t\t\tr.X509.Deny.EmailAddresses = deny.Emails\n\t\t\t}\n\t\t\tif deny.Uris != nil {\n\t\t\t\tr.X509.Deny.URIDomains = deny.Uris\n\t\t\t}\n\t\t\tif deny.CommonNames != nil {\n\t\t\t\tr.X509.Deny.CommonNames = deny.CommonNames\n\t\t\t}\n\t\t}\n\n\t\tr.X509.AllowWildcardNames = x509.GetAllowWildcardNames()\n\t}\n\n\t// fill ssh policy configuration\n\tif ssh := p.GetSsh(); ssh != nil {\n\t\tr.SSH = &dbSSHPolicy{}\n\t\tif host := ssh.GetHost(); host != nil {\n\t\t\tr.SSH.Host = &dbSSHHostPolicy{}\n\t\t\tif allow := host.GetAllow(); allow != nil {\n\t\t\t\tr.SSH.Host.Allow = &dbSSHHostNames{}\n\t\t\t\tif allow.Dns != nil {\n\t\t\t\t\tr.SSH.Host.Allow.DNSDomains = allow.Dns\n\t\t\t\t}\n\t\t\t\tif allow.Ips != nil {\n\t\t\t\t\tr.SSH.Host.Allow.IPRanges = allow.Ips\n\t\t\t\t}\n\t\t\t\tif allow.Principals != nil {\n\t\t\t\t\tr.SSH.Host.Allow.Principals = allow.Principals\n\t\t\t\t}\n\t\t\t}\n\t\t\tif deny := host.GetDeny(); deny != nil {\n\t\t\t\tr.SSH.Host.Deny = &dbSSHHostNames{}\n\t\t\t\tif deny.Dns != nil {\n\t\t\t\t\tr.SSH.Host.Deny.DNSDomains = deny.Dns\n\t\t\t\t}\n\t\t\t\tif deny.Ips != nil {\n\t\t\t\t\tr.SSH.Host.Deny.IPRanges = deny.Ips\n\t\t\t\t}\n\t\t\t\tif deny.Principals != nil {\n\t\t\t\t\tr.SSH.Host.Deny.Principals = deny.Principals\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif user := ssh.GetUser(); user != nil {\n\t\t\tr.SSH.User = &dbSSHUserPolicy{}\n\t\t\tif allow := user.GetAllow(); allow != nil {\n\t\t\t\tr.SSH.User.Allow = &dbSSHUserNames{}\n\t\t\t\tif allow.Emails != nil {\n\t\t\t\t\tr.SSH.User.Allow.EmailAddresses = allow.Emails\n\t\t\t\t}\n\t\t\t\tif allow.Principals != nil {\n\t\t\t\t\tr.SSH.User.Allow.Principals = allow.Principals\n\t\t\t\t}\n\t\t\t}\n\t\t\tif deny := user.GetDeny(); deny != nil {\n\t\t\t\tr.SSH.User.Deny = &dbSSHUserNames{}\n\t\t\t\tif deny.Emails != nil {\n\t\t\t\t\tr.SSH.User.Deny.EmailAddresses = deny.Emails\n\t\t\t\t}\n\t\t\t\tif deny.Principals != nil {\n\t\t\t\t\tr.SSH.User.Deny.Principals = deny.Principals\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn r\n}\n"
  },
  {
    "path": "authority/admin/db/nosql/policy_test.go",
    "content": "package nosql\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"reflect\"\n\t\"testing\"\n\n\t\"github.com/smallstep/assert\"\n\t\"github.com/smallstep/certificates/authority/admin\"\n\t\"github.com/smallstep/certificates/db\"\n\t\"github.com/smallstep/linkedca\"\n\t\"github.com/smallstep/nosql\"\n\tnosqldb \"github.com/smallstep/nosql/database\"\n)\n\nfunc TestDB_getDBAuthorityPolicyBytes(t *testing.T) {\n\tauthID := \"authID\"\n\ttype test struct {\n\t\tctx         context.Context\n\t\tauthorityID string\n\t\tdb          nosql.DB\n\t\terr         error\n\t\tadminErr    *admin.Error\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/not-found\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tctx:         context.Background(),\n\t\t\t\tauthorityID: authID,\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, authorityPoliciesTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), authID)\n\t\t\t\t\t\treturn nil, nosqldb.ErrNotFound\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tadminErr: admin.NewError(admin.ErrorNotFoundType, \"authority policy not found\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/db.Get-error\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tctx:         context.Background(),\n\t\t\t\tauthorityID: authID,\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, authorityPoliciesTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), authID)\n\t\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"error loading authority policy: force\"),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tctx:         context.Background(),\n\t\t\t\tauthorityID: authID,\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, authorityPoliciesTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), authID)\n\t\t\t\t\t\treturn []byte(\"foo\"), nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\ttc := run(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\td := DB{db: tc.db}\n\t\t\tif b, err := d.getDBAuthorityPolicyBytes(tc.ctx, tc.authorityID); err != nil {\n\t\t\t\tvar ae *admin.Error\n\t\t\t\tif errors.As(err, &ae) {\n\t\t\t\t\tif assert.NotNil(t, tc.adminErr) {\n\t\t\t\t\t\tassert.Equals(t, ae.Type, tc.adminErr.Type)\n\t\t\t\t\t\tassert.Equals(t, ae.Detail, tc.adminErr.Detail)\n\t\t\t\t\t\tassert.Equals(t, ae.Status, tc.adminErr.Status)\n\t\t\t\t\t\tassert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error())\n\t\t\t\t\t\tassert.Equals(t, ae.Detail, tc.adminErr.Detail)\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.err.Error())\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else if assert.Nil(t, tc.err) && assert.Nil(t, tc.adminErr) {\n\t\t\t\tassert.Equals(t, string(b), \"foo\")\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDB_getDBAuthorityPolicy(t *testing.T) {\n\tauthID := \"authID\"\n\ttype test struct {\n\t\tctx         context.Context\n\t\tauthorityID string\n\t\tdb          nosql.DB\n\t\terr         error\n\t\tadminErr    *admin.Error\n\t\tdbap        *dbAuthorityPolicy\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/not-found\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tctx:         context.Background(),\n\t\t\t\tauthorityID: authID,\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, authorityPoliciesTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), authID)\n\t\t\t\t\t\treturn nil, nosqldb.ErrNotFound\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tadminErr: admin.NewError(admin.ErrorNotFoundType, \"authority policy not found\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/unmarshal-error\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tctx:         context.Background(),\n\t\t\t\tauthorityID: authID,\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, authorityPoliciesTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), authID)\n\t\t\t\t\t\treturn []byte(\"foo\"), nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"error unmarshaling policy bytes into dbAuthorityPolicy\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/authorityID-error\": func(t *testing.T) test {\n\t\t\tdbp := &dbAuthorityPolicy{\n\t\t\t\tID:          \"ID\",\n\t\t\t\tAuthorityID: \"diffAuthID\",\n\t\t\t\tPolicy: linkedToDB(&linkedca.Policy{\n\t\t\t\t\tX509: &linkedca.X509Policy{\n\t\t\t\t\t\tAllow: &linkedca.X509Names{\n\t\t\t\t\t\t\tDns: []string{\"*.local\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t}),\n\t\t\t}\n\t\t\tb, err := json.Marshal(dbp)\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tctx:         context.Background(),\n\t\t\t\tauthorityID: authID,\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, authorityPoliciesTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), authID)\n\t\t\t\t\t\treturn b, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tadminErr: admin.NewError(admin.ErrorAuthorityMismatchType,\n\t\t\t\t\t\"authority policy is not owned by authority authID\"),\n\t\t\t}\n\t\t},\n\t\t\"ok/empty-bytes\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tctx:         context.Background(),\n\t\t\t\tauthorityID: authID,\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, authorityPoliciesTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), authID)\n\t\t\t\t\t\treturn []byte{}, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tdbap := &dbAuthorityPolicy{\n\t\t\t\tID:          \"ID\",\n\t\t\t\tAuthorityID: authID,\n\t\t\t\tPolicy: linkedToDB(&linkedca.Policy{\n\t\t\t\t\tX509: &linkedca.X509Policy{\n\t\t\t\t\t\tAllow: &linkedca.X509Names{\n\t\t\t\t\t\t\tDns: []string{\"*.local\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t}),\n\t\t\t}\n\t\t\tb, err := json.Marshal(dbap)\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tctx:         context.Background(),\n\t\t\t\tauthorityID: authID,\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, authorityPoliciesTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), authID)\n\t\t\t\t\t\treturn b, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tdbap: dbap,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\ttc := run(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\td := DB{db: tc.db, authorityID: admin.DefaultAuthorityID}\n\t\t\tdbp, err := d.getDBAuthorityPolicy(tc.ctx, tc.authorityID)\n\t\t\tswitch {\n\t\t\tcase err != nil:\n\t\t\t\tvar ae *admin.Error\n\t\t\t\tif errors.As(err, &ae) {\n\t\t\t\t\tif assert.NotNil(t, tc.adminErr) {\n\t\t\t\t\t\tassert.Equals(t, ae.Type, tc.adminErr.Type)\n\t\t\t\t\t\tassert.Equals(t, ae.Detail, tc.adminErr.Detail)\n\t\t\t\t\t\tassert.Equals(t, ae.Status, tc.adminErr.Status)\n\t\t\t\t\t\tassert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error())\n\t\t\t\t\t\tassert.Equals(t, ae.Detail, tc.adminErr.Detail)\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.err.Error())\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\tcase assert.Nil(t, tc.err) && assert.Nil(t, tc.adminErr) && tc.dbap == nil:\n\t\t\t\tassert.Nil(t, dbp)\n\t\t\tcase assert.Nil(t, tc.err) && assert.Nil(t, tc.adminErr):\n\t\t\t\tassert.Equals(t, dbp.ID, \"ID\")\n\t\t\t\tassert.Equals(t, dbp.AuthorityID, tc.dbap.AuthorityID)\n\t\t\t\tassert.Equals(t, dbp.Policy, tc.dbap.Policy)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDB_CreateAuthorityPolicy(t *testing.T) {\n\tauthID := \"authID\"\n\ttype test struct {\n\t\tctx         context.Context\n\t\tauthorityID string\n\t\tpolicy      *linkedca.Policy\n\t\tdb          nosql.DB\n\t\terr         error\n\t\tadminErr    *admin.Error\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/save-error\": func(t *testing.T) test {\n\t\t\tpolicy := &linkedca.Policy{\n\t\t\t\tX509: &linkedca.X509Policy{\n\t\t\t\t\tAllow: &linkedca.X509Names{\n\t\t\t\t\t\tDns: []string{\"*.local\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tctx:         context.Background(),\n\t\t\t\tauthorityID: authID,\n\t\t\t\tpolicy:      policy,\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, authorityPoliciesTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), authID)\n\n\t\t\t\t\t\tvar _dbap = new(dbAuthorityPolicy)\n\t\t\t\t\t\tassert.FatalError(t, json.Unmarshal(nu, _dbap))\n\n\t\t\t\t\t\tassert.Equals(t, _dbap.ID, authID)\n\t\t\t\t\t\tassert.Equals(t, _dbap.AuthorityID, authID)\n\t\t\t\t\t\tassert.Equals(t, _dbap.Policy, linkedToDB(policy))\n\n\t\t\t\t\t\treturn nil, false, errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tadminErr: admin.NewErrorISE(\"error creating authority policy: error saving authority authority_policy: force\"),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tpolicy := &linkedca.Policy{\n\t\t\t\tX509: &linkedca.X509Policy{\n\t\t\t\t\tAllow: &linkedca.X509Names{\n\t\t\t\t\t\tDns: []string{\"*.local\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tctx:         context.Background(),\n\t\t\t\tauthorityID: authID,\n\t\t\t\tpolicy:      policy,\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, authorityPoliciesTable)\n\t\t\t\t\t\tassert.Equals(t, old, nil)\n\n\t\t\t\t\t\tvar _dbap = new(dbAuthorityPolicy)\n\t\t\t\t\t\tassert.FatalError(t, json.Unmarshal(nu, _dbap))\n\n\t\t\t\t\t\tassert.Equals(t, _dbap.ID, authID)\n\t\t\t\t\t\tassert.Equals(t, _dbap.AuthorityID, authID)\n\t\t\t\t\t\tassert.Equals(t, _dbap.Policy, linkedToDB(policy))\n\n\t\t\t\t\t\treturn nil, true, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\ttc := run(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\td := DB{db: tc.db, authorityID: tc.authorityID}\n\t\t\tif err := d.CreateAuthorityPolicy(tc.ctx, tc.policy); err != nil {\n\t\t\t\tvar ae *admin.Error\n\t\t\t\tif errors.As(err, &ae) {\n\t\t\t\t\tif assert.NotNil(t, tc.adminErr) {\n\t\t\t\t\t\tassert.Equals(t, ae.Type, tc.adminErr.Type)\n\t\t\t\t\t\tassert.Equals(t, ae.Detail, tc.adminErr.Detail)\n\t\t\t\t\t\tassert.Equals(t, ae.Status, tc.adminErr.Status)\n\t\t\t\t\t\tassert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error())\n\t\t\t\t\t\tassert.Equals(t, ae.Detail, tc.adminErr.Detail)\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.err.Error())\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDB_GetAuthorityPolicy(t *testing.T) {\n\tauthID := \"authID\"\n\ttype test struct {\n\t\tctx         context.Context\n\t\tauthorityID string\n\t\tpolicy      *linkedca.Policy\n\t\tdb          nosql.DB\n\t\terr         error\n\t\tadminErr    *admin.Error\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/not-found\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tctx:         context.Background(),\n\t\t\t\tauthorityID: authID,\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, authorityPoliciesTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), authID)\n\t\t\t\t\t\treturn nil, nosqldb.ErrNotFound\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tadminErr: admin.NewError(admin.ErrorNotFoundType, \"authority policy not found\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/db.Get-error\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tctx:         context.Background(),\n\t\t\t\tauthorityID: authID,\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, authorityPoliciesTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), authID)\n\n\t\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"error loading authority policy: force\"),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tpolicy := &linkedca.Policy{\n\t\t\t\tX509: &linkedca.X509Policy{\n\t\t\t\t\tAllow: &linkedca.X509Names{\n\t\t\t\t\t\tDns: []string{\"*.local\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tctx:         context.Background(),\n\t\t\t\tauthorityID: authID,\n\t\t\t\tpolicy:      policy,\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, authorityPoliciesTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), authID)\n\n\t\t\t\t\t\tdbap := &dbAuthorityPolicy{\n\t\t\t\t\t\t\tID:          authID,\n\t\t\t\t\t\t\tAuthorityID: authID,\n\t\t\t\t\t\t\tPolicy:      linkedToDB(policy),\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tb, err := json.Marshal(dbap)\n\t\t\t\t\t\tassert.FatalError(t, err)\n\n\t\t\t\t\t\treturn b, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\ttc := run(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\td := DB{db: tc.db, authorityID: tc.authorityID}\n\t\t\tgot, err := d.GetAuthorityPolicy(tc.ctx)\n\t\t\tif err != nil {\n\t\t\t\tvar ae *admin.Error\n\t\t\t\tif errors.As(err, &ae) {\n\t\t\t\t\tif assert.NotNil(t, tc.adminErr) {\n\t\t\t\t\t\tassert.Equals(t, ae.Type, tc.adminErr.Type)\n\t\t\t\t\t\tassert.Equals(t, ae.Detail, tc.adminErr.Detail)\n\t\t\t\t\t\tassert.Equals(t, ae.Status, tc.adminErr.Status)\n\t\t\t\t\t\tassert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error())\n\t\t\t\t\t\tassert.Equals(t, ae.Detail, tc.adminErr.Detail)\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.err.Error())\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.NotNil(t, got)\n\t\t\tassert.Equals(t, tc.policy, got)\n\t\t})\n\t}\n}\n\nfunc TestDB_UpdateAuthorityPolicy(t *testing.T) {\n\tauthID := \"authID\"\n\ttype test struct {\n\t\tctx         context.Context\n\t\tauthorityID string\n\t\tpolicy      *linkedca.Policy\n\t\tdb          nosql.DB\n\t\terr         error\n\t\tadminErr    *admin.Error\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/not-found\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tctx:         context.Background(),\n\t\t\t\tauthorityID: authID,\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, authorityPoliciesTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), authID)\n\t\t\t\t\t\treturn nil, nosqldb.ErrNotFound\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tadminErr: admin.NewError(admin.ErrorNotFoundType, \"authority policy not found\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/db.Get-error\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tctx:         context.Background(),\n\t\t\t\tauthorityID: authID,\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, authorityPoliciesTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), authID)\n\n\t\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"error loading authority policy: force\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/save-error\": func(t *testing.T) test {\n\t\t\toldPolicy := &linkedca.Policy{\n\t\t\t\tX509: &linkedca.X509Policy{\n\t\t\t\t\tAllow: &linkedca.X509Names{\n\t\t\t\t\t\tDns: []string{\"*.localhost\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t\tpolicy := &linkedca.Policy{\n\t\t\t\tX509: &linkedca.X509Policy{\n\t\t\t\t\tAllow: &linkedca.X509Names{\n\t\t\t\t\t\tDns: []string{\"*.local\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tctx:         context.Background(),\n\t\t\t\tauthorityID: authID,\n\t\t\t\tpolicy:      policy,\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, authorityPoliciesTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), authID)\n\n\t\t\t\t\t\tdbap := &dbAuthorityPolicy{\n\t\t\t\t\t\t\tID:          authID,\n\t\t\t\t\t\t\tAuthorityID: authID,\n\t\t\t\t\t\t\tPolicy:      linkedToDB(oldPolicy),\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tb, err := json.Marshal(dbap)\n\t\t\t\t\t\tassert.FatalError(t, err)\n\n\t\t\t\t\t\treturn b, nil\n\t\t\t\t\t},\n\t\t\t\t\tMCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, authorityPoliciesTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), authID)\n\n\t\t\t\t\t\tvar _dbap = new(dbAuthorityPolicy)\n\t\t\t\t\t\tassert.FatalError(t, json.Unmarshal(nu, _dbap))\n\n\t\t\t\t\t\tassert.Equals(t, _dbap.ID, authID)\n\t\t\t\t\t\tassert.Equals(t, _dbap.AuthorityID, authID)\n\t\t\t\t\t\tassert.Equals(t, _dbap.Policy, linkedToDB(policy))\n\n\t\t\t\t\t\treturn nil, false, errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tadminErr: admin.NewErrorISE(\"error updating authority policy: error saving authority authority_policy: force\"),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\toldPolicy := &linkedca.Policy{\n\t\t\t\tX509: &linkedca.X509Policy{\n\t\t\t\t\tAllow: &linkedca.X509Names{\n\t\t\t\t\t\tDns: []string{\"*.localhost\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t\tpolicy := &linkedca.Policy{\n\t\t\t\tX509: &linkedca.X509Policy{\n\t\t\t\t\tAllow: &linkedca.X509Names{\n\t\t\t\t\t\tDns: []string{\"*.local\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tctx:         context.Background(),\n\t\t\t\tauthorityID: authID,\n\t\t\t\tpolicy:      policy,\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, authorityPoliciesTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), authID)\n\n\t\t\t\t\t\tdbap := &dbAuthorityPolicy{\n\t\t\t\t\t\t\tID:          authID,\n\t\t\t\t\t\t\tAuthorityID: authID,\n\t\t\t\t\t\t\tPolicy:      linkedToDB(oldPolicy),\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tb, err := json.Marshal(dbap)\n\t\t\t\t\t\tassert.FatalError(t, err)\n\n\t\t\t\t\t\treturn b, nil\n\t\t\t\t\t},\n\t\t\t\t\tMCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, authorityPoliciesTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), authID)\n\n\t\t\t\t\t\tvar _dbap = new(dbAuthorityPolicy)\n\t\t\t\t\t\tassert.FatalError(t, json.Unmarshal(nu, _dbap))\n\n\t\t\t\t\t\tassert.Equals(t, _dbap.ID, authID)\n\t\t\t\t\t\tassert.Equals(t, _dbap.AuthorityID, authID)\n\t\t\t\t\t\tassert.Equals(t, _dbap.Policy, linkedToDB(policy))\n\n\t\t\t\t\t\treturn nil, true, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\ttc := run(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\td := DB{db: tc.db, authorityID: tc.authorityID}\n\t\t\tif err := d.UpdateAuthorityPolicy(tc.ctx, tc.policy); err != nil {\n\t\t\t\tvar ae *admin.Error\n\t\t\t\tif errors.As(err, &ae) {\n\t\t\t\t\tif assert.NotNil(t, tc.adminErr) {\n\t\t\t\t\t\tassert.Equals(t, ae.Type, tc.adminErr.Type)\n\t\t\t\t\t\tassert.Equals(t, ae.Detail, tc.adminErr.Detail)\n\t\t\t\t\t\tassert.Equals(t, ae.Status, tc.adminErr.Status)\n\t\t\t\t\t\tassert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error())\n\t\t\t\t\t\tassert.Equals(t, ae.Detail, tc.adminErr.Detail)\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.err.Error())\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDB_DeleteAuthorityPolicy(t *testing.T) {\n\tauthID := \"authID\"\n\ttype test struct {\n\t\tctx         context.Context\n\t\tauthorityID string\n\t\tdb          nosql.DB\n\t\terr         error\n\t\tadminErr    *admin.Error\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/not-found\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tctx:         context.Background(),\n\t\t\t\tauthorityID: authID,\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, authorityPoliciesTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), authID)\n\t\t\t\t\t\treturn nil, nosqldb.ErrNotFound\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tadminErr: admin.NewError(admin.ErrorNotFoundType, \"authority policy not found\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/db.Get-error\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tctx:         context.Background(),\n\t\t\t\tauthorityID: authID,\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, authorityPoliciesTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), authID)\n\n\t\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"error loading authority policy: force\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/save-error\": func(t *testing.T) test {\n\t\t\toldPolicy := &linkedca.Policy{\n\t\t\t\tX509: &linkedca.X509Policy{\n\t\t\t\t\tAllow: &linkedca.X509Names{\n\t\t\t\t\t\tDns: []string{\"*.localhost\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tctx:         context.Background(),\n\t\t\t\tauthorityID: authID,\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, authorityPoliciesTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), authID)\n\n\t\t\t\t\t\tdbap := &dbAuthorityPolicy{\n\t\t\t\t\t\t\tID:          authID,\n\t\t\t\t\t\t\tAuthorityID: authID,\n\t\t\t\t\t\t\tPolicy:      linkedToDB(oldPolicy),\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tb, err := json.Marshal(dbap)\n\t\t\t\t\t\tassert.FatalError(t, err)\n\n\t\t\t\t\t\treturn b, nil\n\t\t\t\t\t},\n\t\t\t\t\tMCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, authorityPoliciesTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), authID)\n\t\t\t\t\t\tassert.Equals(t, nil, nu)\n\n\t\t\t\t\t\treturn nil, false, errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tadminErr: admin.NewErrorISE(\"error deleting authority policy: error saving authority authority_policy: force\"),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\toldPolicy := &linkedca.Policy{\n\t\t\t\tX509: &linkedca.X509Policy{\n\t\t\t\t\tAllow: &linkedca.X509Names{\n\t\t\t\t\t\tDns: []string{\"*.localhost\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tctx:         context.Background(),\n\t\t\t\tauthorityID: authID,\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, authorityPoliciesTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), authID)\n\n\t\t\t\t\t\tdbap := &dbAuthorityPolicy{\n\t\t\t\t\t\t\tID:          authID,\n\t\t\t\t\t\t\tAuthorityID: authID,\n\t\t\t\t\t\t\tPolicy:      linkedToDB(oldPolicy),\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tb, err := json.Marshal(dbap)\n\t\t\t\t\t\tassert.FatalError(t, err)\n\n\t\t\t\t\t\treturn b, nil\n\t\t\t\t\t},\n\t\t\t\t\tMCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, authorityPoliciesTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), authID)\n\t\t\t\t\t\tassert.Equals(t, nil, nu)\n\n\t\t\t\t\t\treturn nil, true, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\ttc := run(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\td := DB{db: tc.db, authorityID: tc.authorityID}\n\t\t\tif err := d.DeleteAuthorityPolicy(tc.ctx); err != nil {\n\t\t\t\tvar ae *admin.Error\n\t\t\t\tif errors.As(err, &ae) {\n\t\t\t\t\tif assert.NotNil(t, tc.adminErr) {\n\t\t\t\t\t\tassert.Equals(t, ae.Type, tc.adminErr.Type)\n\t\t\t\t\t\tassert.Equals(t, ae.Detail, tc.adminErr.Detail)\n\t\t\t\t\t\tassert.Equals(t, ae.Status, tc.adminErr.Status)\n\t\t\t\t\t\tassert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error())\n\t\t\t\t\t\tassert.Equals(t, ae.Detail, tc.adminErr.Detail)\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.err.Error())\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_linkedToDB(t *testing.T) {\n\ttype args struct {\n\t\tp *linkedca.Policy\n\t}\n\ttests := []struct {\n\t\tname string\n\t\targs args\n\t\twant *dbPolicy\n\t}{\n\t\t{\n\t\t\tname: \"nil policy\",\n\t\t\targs: args{\n\t\t\t\tp: nil,\n\t\t\t},\n\t\t\twant: nil,\n\t\t},\n\t\t{\n\t\t\tname: \"no x509 nor ssh\",\n\t\t\targs: args{\n\t\t\t\tp: &linkedca.Policy{},\n\t\t\t},\n\t\t\twant: nil,\n\t\t},\n\t\t{\n\t\t\tname: \"x509\",\n\t\t\targs: args{\n\t\t\t\tp: &linkedca.Policy{\n\t\t\t\t\tX509: &linkedca.X509Policy{\n\t\t\t\t\t\tAllow: &linkedca.X509Names{\n\t\t\t\t\t\t\tDns:         []string{\"*.local\"},\n\t\t\t\t\t\t\tIps:         []string{\"192.168.0.1/24\"},\n\t\t\t\t\t\t\tEmails:      []string{\"@example.com\"},\n\t\t\t\t\t\t\tUris:        []string{\"*.example.com\"},\n\t\t\t\t\t\t\tCommonNames: []string{\"some name\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tDeny: &linkedca.X509Names{\n\t\t\t\t\t\t\tDns:         []string{\"badhost.local\"},\n\t\t\t\t\t\t\tIps:         []string{\"192.168.0.30\"},\n\t\t\t\t\t\t\tEmails:      []string{\"root@example.com\"},\n\t\t\t\t\t\t\tUris:        []string{\"bad.example.com\"},\n\t\t\t\t\t\t\tCommonNames: []string{\"bad name\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tAllowWildcardNames: true,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: &dbPolicy{\n\t\t\t\tX509: &dbX509Policy{\n\t\t\t\t\tAllow: &dbX509Names{\n\t\t\t\t\t\tDNSDomains:     []string{\"*.local\"},\n\t\t\t\t\t\tIPRanges:       []string{\"192.168.0.1/24\"},\n\t\t\t\t\t\tEmailAddresses: []string{\"@example.com\"},\n\t\t\t\t\t\tURIDomains:     []string{\"*.example.com\"},\n\t\t\t\t\t\tCommonNames:    []string{\"some name\"},\n\t\t\t\t\t},\n\t\t\t\t\tDeny: &dbX509Names{\n\t\t\t\t\t\tDNSDomains:     []string{\"badhost.local\"},\n\t\t\t\t\t\tIPRanges:       []string{\"192.168.0.30\"},\n\t\t\t\t\t\tEmailAddresses: []string{\"root@example.com\"},\n\t\t\t\t\t\tURIDomains:     []string{\"bad.example.com\"},\n\t\t\t\t\t\tCommonNames:    []string{\"bad name\"},\n\t\t\t\t\t},\n\t\t\t\t\tAllowWildcardNames: true,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"ssh user\",\n\t\t\targs: args{\n\t\t\t\tp: &linkedca.Policy{\n\t\t\t\t\tSsh: &linkedca.SSHPolicy{\n\t\t\t\t\t\tUser: &linkedca.SSHUserPolicy{\n\t\t\t\t\t\t\tAllow: &linkedca.SSHUserNames{\n\t\t\t\t\t\t\t\tEmails:     []string{\"@example.com\"},\n\t\t\t\t\t\t\t\tPrincipals: []string{\"user\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tDeny: &linkedca.SSHUserNames{\n\t\t\t\t\t\t\t\tEmails:     []string{\"root@example.com\"},\n\t\t\t\t\t\t\t\tPrincipals: []string{\"root\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: &dbPolicy{\n\t\t\t\tSSH: &dbSSHPolicy{\n\t\t\t\t\tUser: &dbSSHUserPolicy{\n\t\t\t\t\t\tAllow: &dbSSHUserNames{\n\t\t\t\t\t\t\tEmailAddresses: []string{\"@example.com\"},\n\t\t\t\t\t\t\tPrincipals:     []string{\"user\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tDeny: &dbSSHUserNames{\n\t\t\t\t\t\t\tEmailAddresses: []string{\"root@example.com\"},\n\t\t\t\t\t\t\tPrincipals:     []string{\"root\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"full ssh policy\",\n\t\t\targs: args{\n\t\t\t\tp: &linkedca.Policy{\n\t\t\t\t\tSsh: &linkedca.SSHPolicy{\n\t\t\t\t\t\tHost: &linkedca.SSHHostPolicy{\n\t\t\t\t\t\t\tAllow: &linkedca.SSHHostNames{\n\t\t\t\t\t\t\t\tDns:        []string{\"*.local\"},\n\t\t\t\t\t\t\t\tIps:        []string{\"192.168.0.1/24\"},\n\t\t\t\t\t\t\t\tPrincipals: []string{\"host\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tDeny: &linkedca.SSHHostNames{\n\t\t\t\t\t\t\t\tDns:        []string{\"badhost.local\"},\n\t\t\t\t\t\t\t\tIps:        []string{\"192.168.0.30\"},\n\t\t\t\t\t\t\t\tPrincipals: []string{\"bad\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: &dbPolicy{\n\t\t\t\tSSH: &dbSSHPolicy{\n\t\t\t\t\tHost: &dbSSHHostPolicy{\n\t\t\t\t\t\tAllow: &dbSSHHostNames{\n\t\t\t\t\t\t\tDNSDomains: []string{\"*.local\"},\n\t\t\t\t\t\t\tIPRanges:   []string{\"192.168.0.1/24\"},\n\t\t\t\t\t\t\tPrincipals: []string{\"host\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tDeny: &dbSSHHostNames{\n\t\t\t\t\t\t\tDNSDomains: []string{\"badhost.local\"},\n\t\t\t\t\t\t\tIPRanges:   []string{\"192.168.0.30\"},\n\t\t\t\t\t\t\tPrincipals: []string{\"bad\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"full policy\",\n\t\t\targs: args{\n\t\t\t\tp: &linkedca.Policy{\n\t\t\t\t\tX509: &linkedca.X509Policy{\n\t\t\t\t\t\tAllow: &linkedca.X509Names{\n\t\t\t\t\t\t\tDns:         []string{\"*.local\"},\n\t\t\t\t\t\t\tIps:         []string{\"192.168.0.1/24\"},\n\t\t\t\t\t\t\tEmails:      []string{\"@example.com\"},\n\t\t\t\t\t\t\tUris:        []string{\"*.example.com\"},\n\t\t\t\t\t\t\tCommonNames: []string{\"some name\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tDeny: &linkedca.X509Names{\n\t\t\t\t\t\t\tDns:         []string{\"badhost.local\"},\n\t\t\t\t\t\t\tIps:         []string{\"192.168.0.30\"},\n\t\t\t\t\t\t\tEmails:      []string{\"root@example.com\"},\n\t\t\t\t\t\t\tUris:        []string{\"bad.example.com\"},\n\t\t\t\t\t\t\tCommonNames: []string{\"bad name\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tAllowWildcardNames: true,\n\t\t\t\t\t},\n\t\t\t\t\tSsh: &linkedca.SSHPolicy{\n\t\t\t\t\t\tUser: &linkedca.SSHUserPolicy{\n\t\t\t\t\t\t\tAllow: &linkedca.SSHUserNames{\n\t\t\t\t\t\t\t\tEmails:     []string{\"@example.com\"},\n\t\t\t\t\t\t\t\tPrincipals: []string{\"user\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tDeny: &linkedca.SSHUserNames{\n\t\t\t\t\t\t\t\tEmails:     []string{\"root@example.com\"},\n\t\t\t\t\t\t\t\tPrincipals: []string{\"root\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tHost: &linkedca.SSHHostPolicy{\n\t\t\t\t\t\t\tAllow: &linkedca.SSHHostNames{\n\t\t\t\t\t\t\t\tDns:        []string{\"*.local\"},\n\t\t\t\t\t\t\t\tIps:        []string{\"192.168.0.1/24\"},\n\t\t\t\t\t\t\t\tPrincipals: []string{\"host\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tDeny: &linkedca.SSHHostNames{\n\t\t\t\t\t\t\t\tDns:        []string{\"badhost.local\"},\n\t\t\t\t\t\t\t\tIps:        []string{\"192.168.0.30\"},\n\t\t\t\t\t\t\t\tPrincipals: []string{\"bad\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: &dbPolicy{\n\t\t\t\tX509: &dbX509Policy{\n\t\t\t\t\tAllow: &dbX509Names{\n\t\t\t\t\t\tDNSDomains:     []string{\"*.local\"},\n\t\t\t\t\t\tIPRanges:       []string{\"192.168.0.1/24\"},\n\t\t\t\t\t\tEmailAddresses: []string{\"@example.com\"},\n\t\t\t\t\t\tURIDomains:     []string{\"*.example.com\"},\n\t\t\t\t\t\tCommonNames:    []string{\"some name\"},\n\t\t\t\t\t},\n\t\t\t\t\tDeny: &dbX509Names{\n\t\t\t\t\t\tDNSDomains:     []string{\"badhost.local\"},\n\t\t\t\t\t\tIPRanges:       []string{\"192.168.0.30\"},\n\t\t\t\t\t\tEmailAddresses: []string{\"root@example.com\"},\n\t\t\t\t\t\tURIDomains:     []string{\"bad.example.com\"},\n\t\t\t\t\t\tCommonNames:    []string{\"bad name\"},\n\t\t\t\t\t},\n\t\t\t\t\tAllowWildcardNames: true,\n\t\t\t\t},\n\t\t\t\tSSH: &dbSSHPolicy{\n\t\t\t\t\tUser: &dbSSHUserPolicy{\n\t\t\t\t\t\tAllow: &dbSSHUserNames{\n\t\t\t\t\t\t\tEmailAddresses: []string{\"@example.com\"},\n\t\t\t\t\t\t\tPrincipals:     []string{\"user\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tDeny: &dbSSHUserNames{\n\t\t\t\t\t\t\tEmailAddresses: []string{\"root@example.com\"},\n\t\t\t\t\t\t\tPrincipals:     []string{\"root\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tHost: &dbSSHHostPolicy{\n\t\t\t\t\t\tAllow: &dbSSHHostNames{\n\t\t\t\t\t\t\tDNSDomains: []string{\"*.local\"},\n\t\t\t\t\t\t\tIPRanges:   []string{\"192.168.0.1/24\"},\n\t\t\t\t\t\t\tPrincipals: []string{\"host\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tDeny: &dbSSHHostNames{\n\t\t\t\t\t\t\tDNSDomains: []string{\"badhost.local\"},\n\t\t\t\t\t\t\tIPRanges:   []string{\"192.168.0.30\"},\n\t\t\t\t\t\t\tPrincipals: []string{\"bad\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif got := linkedToDB(tt.args.p); !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"linkedToDB() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_dbToLinked(t *testing.T) {\n\ttype args struct {\n\t\tp *dbPolicy\n\t}\n\ttests := []struct {\n\t\tname string\n\t\targs args\n\t\twant *linkedca.Policy\n\t}{\n\t\t{\n\t\t\tname: \"nil policy\",\n\t\t\targs: args{\n\t\t\t\tp: nil,\n\t\t\t},\n\t\t\twant: nil,\n\t\t},\n\t\t{\n\t\t\tname: \"x509\",\n\t\t\targs: args{\n\t\t\t\tp: &dbPolicy{\n\t\t\t\t\tX509: &dbX509Policy{\n\t\t\t\t\t\tAllow: &dbX509Names{\n\t\t\t\t\t\t\tDNSDomains:     []string{\"*.local\"},\n\t\t\t\t\t\t\tIPRanges:       []string{\"192.168.0.1/24\"},\n\t\t\t\t\t\t\tEmailAddresses: []string{\"@example.com\"},\n\t\t\t\t\t\t\tURIDomains:     []string{\"*.example.com\"},\n\t\t\t\t\t\t\tCommonNames:    []string{\"some name\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tDeny: &dbX509Names{\n\t\t\t\t\t\t\tDNSDomains:     []string{\"badhost.local\"},\n\t\t\t\t\t\t\tIPRanges:       []string{\"192.168.0.30\"},\n\t\t\t\t\t\t\tEmailAddresses: []string{\"root@example.com\"},\n\t\t\t\t\t\t\tURIDomains:     []string{\"bad.example.com\"},\n\t\t\t\t\t\t\tCommonNames:    []string{\"bad name\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tAllowWildcardNames: true,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: &linkedca.Policy{\n\t\t\t\tX509: &linkedca.X509Policy{\n\t\t\t\t\tAllow: &linkedca.X509Names{\n\t\t\t\t\t\tDns:         []string{\"*.local\"},\n\t\t\t\t\t\tIps:         []string{\"192.168.0.1/24\"},\n\t\t\t\t\t\tEmails:      []string{\"@example.com\"},\n\t\t\t\t\t\tUris:        []string{\"*.example.com\"},\n\t\t\t\t\t\tCommonNames: []string{\"some name\"},\n\t\t\t\t\t},\n\t\t\t\t\tDeny: &linkedca.X509Names{\n\t\t\t\t\t\tDns:         []string{\"badhost.local\"},\n\t\t\t\t\t\tIps:         []string{\"192.168.0.30\"},\n\t\t\t\t\t\tEmails:      []string{\"root@example.com\"},\n\t\t\t\t\t\tUris:        []string{\"bad.example.com\"},\n\t\t\t\t\t\tCommonNames: []string{\"bad name\"},\n\t\t\t\t\t},\n\t\t\t\t\tAllowWildcardNames: true,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"ssh user\",\n\t\t\targs: args{\n\t\t\t\tp: &dbPolicy{\n\t\t\t\t\tSSH: &dbSSHPolicy{\n\t\t\t\t\t\tUser: &dbSSHUserPolicy{\n\t\t\t\t\t\t\tAllow: &dbSSHUserNames{\n\t\t\t\t\t\t\t\tEmailAddresses: []string{\"@example.com\"},\n\t\t\t\t\t\t\t\tPrincipals:     []string{\"user\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tDeny: &dbSSHUserNames{\n\t\t\t\t\t\t\t\tEmailAddresses: []string{\"root@example.com\"},\n\t\t\t\t\t\t\t\tPrincipals:     []string{\"root\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: &linkedca.Policy{\n\t\t\t\tSsh: &linkedca.SSHPolicy{\n\t\t\t\t\tUser: &linkedca.SSHUserPolicy{\n\t\t\t\t\t\tAllow: &linkedca.SSHUserNames{\n\t\t\t\t\t\t\tEmails:     []string{\"@example.com\"},\n\t\t\t\t\t\t\tPrincipals: []string{\"user\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tDeny: &linkedca.SSHUserNames{\n\t\t\t\t\t\t\tEmails:     []string{\"root@example.com\"},\n\t\t\t\t\t\t\tPrincipals: []string{\"root\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"ssh host\",\n\t\t\targs: args{\n\t\t\t\tp: &dbPolicy{\n\t\t\t\t\tSSH: &dbSSHPolicy{\n\t\t\t\t\t\tHost: &dbSSHHostPolicy{\n\t\t\t\t\t\t\tAllow: &dbSSHHostNames{\n\t\t\t\t\t\t\t\tDNSDomains: []string{\"*.local\"},\n\t\t\t\t\t\t\t\tIPRanges:   []string{\"192.168.0.1/24\"},\n\t\t\t\t\t\t\t\tPrincipals: []string{\"host\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tDeny: &dbSSHHostNames{\n\t\t\t\t\t\t\t\tDNSDomains: []string{\"badhost.local\"},\n\t\t\t\t\t\t\t\tIPRanges:   []string{\"192.168.0.30\"},\n\t\t\t\t\t\t\t\tPrincipals: []string{\"bad\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: &linkedca.Policy{\n\t\t\t\tSsh: &linkedca.SSHPolicy{\n\t\t\t\t\tHost: &linkedca.SSHHostPolicy{\n\t\t\t\t\t\tAllow: &linkedca.SSHHostNames{\n\t\t\t\t\t\t\tDns:        []string{\"*.local\"},\n\t\t\t\t\t\t\tIps:        []string{\"192.168.0.1/24\"},\n\t\t\t\t\t\t\tPrincipals: []string{\"host\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tDeny: &linkedca.SSHHostNames{\n\t\t\t\t\t\t\tDns:        []string{\"badhost.local\"},\n\t\t\t\t\t\t\tIps:        []string{\"192.168.0.30\"},\n\t\t\t\t\t\t\tPrincipals: []string{\"bad\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"full policy\",\n\t\t\targs: args{\n\t\t\t\tp: &dbPolicy{\n\t\t\t\t\tX509: &dbX509Policy{\n\t\t\t\t\t\tAllow: &dbX509Names{\n\t\t\t\t\t\t\tDNSDomains:     []string{\"*.local\"},\n\t\t\t\t\t\t\tIPRanges:       []string{\"192.168.0.1/24\"},\n\t\t\t\t\t\t\tEmailAddresses: []string{\"@example.com\"},\n\t\t\t\t\t\t\tURIDomains:     []string{\"*.example.com\"},\n\t\t\t\t\t\t\tCommonNames:    []string{\"some name\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tDeny: &dbX509Names{\n\t\t\t\t\t\t\tDNSDomains:     []string{\"badhost.local\"},\n\t\t\t\t\t\t\tIPRanges:       []string{\"192.168.0.30\"},\n\t\t\t\t\t\t\tEmailAddresses: []string{\"root@example.com\"},\n\t\t\t\t\t\t\tURIDomains:     []string{\"bad.example.com\"},\n\t\t\t\t\t\t\tCommonNames:    []string{\"bad name\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tAllowWildcardNames: true,\n\t\t\t\t\t},\n\t\t\t\t\tSSH: &dbSSHPolicy{\n\t\t\t\t\t\tUser: &dbSSHUserPolicy{\n\t\t\t\t\t\t\tAllow: &dbSSHUserNames{\n\t\t\t\t\t\t\t\tEmailAddresses: []string{\"@example.com\"},\n\t\t\t\t\t\t\t\tPrincipals:     []string{\"user\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tDeny: &dbSSHUserNames{\n\t\t\t\t\t\t\t\tEmailAddresses: []string{\"root@example.com\"},\n\t\t\t\t\t\t\t\tPrincipals:     []string{\"root\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tHost: &dbSSHHostPolicy{\n\t\t\t\t\t\t\tAllow: &dbSSHHostNames{\n\t\t\t\t\t\t\t\tDNSDomains: []string{\"*.local\"},\n\t\t\t\t\t\t\t\tIPRanges:   []string{\"192.168.0.1/24\"},\n\t\t\t\t\t\t\t\tPrincipals: []string{\"host\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tDeny: &dbSSHHostNames{\n\t\t\t\t\t\t\t\tDNSDomains: []string{\"badhost.local\"},\n\t\t\t\t\t\t\t\tIPRanges:   []string{\"192.168.0.30\"},\n\t\t\t\t\t\t\t\tPrincipals: []string{\"bad\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: &linkedca.Policy{\n\t\t\t\tX509: &linkedca.X509Policy{\n\t\t\t\t\tAllow: &linkedca.X509Names{\n\t\t\t\t\t\tDns:         []string{\"*.local\"},\n\t\t\t\t\t\tIps:         []string{\"192.168.0.1/24\"},\n\t\t\t\t\t\tEmails:      []string{\"@example.com\"},\n\t\t\t\t\t\tUris:        []string{\"*.example.com\"},\n\t\t\t\t\t\tCommonNames: []string{\"some name\"},\n\t\t\t\t\t},\n\t\t\t\t\tDeny: &linkedca.X509Names{\n\t\t\t\t\t\tDns:         []string{\"badhost.local\"},\n\t\t\t\t\t\tIps:         []string{\"192.168.0.30\"},\n\t\t\t\t\t\tEmails:      []string{\"root@example.com\"},\n\t\t\t\t\t\tUris:        []string{\"bad.example.com\"},\n\t\t\t\t\t\tCommonNames: []string{\"bad name\"},\n\t\t\t\t\t},\n\t\t\t\t\tAllowWildcardNames: true,\n\t\t\t\t},\n\t\t\t\tSsh: &linkedca.SSHPolicy{\n\t\t\t\t\tUser: &linkedca.SSHUserPolicy{\n\t\t\t\t\t\tAllow: &linkedca.SSHUserNames{\n\t\t\t\t\t\t\tEmails:     []string{\"@example.com\"},\n\t\t\t\t\t\t\tPrincipals: []string{\"user\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tDeny: &linkedca.SSHUserNames{\n\t\t\t\t\t\t\tEmails:     []string{\"root@example.com\"},\n\t\t\t\t\t\t\tPrincipals: []string{\"root\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tHost: &linkedca.SSHHostPolicy{\n\t\t\t\t\t\tAllow: &linkedca.SSHHostNames{\n\t\t\t\t\t\t\tDns:        []string{\"*.local\"},\n\t\t\t\t\t\t\tIps:        []string{\"192.168.0.1/24\"},\n\t\t\t\t\t\t\tPrincipals: []string{\"host\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tDeny: &linkedca.SSHHostNames{\n\t\t\t\t\t\t\tDns:        []string{\"badhost.local\"},\n\t\t\t\t\t\t\tIps:        []string{\"192.168.0.30\"},\n\t\t\t\t\t\t\tPrincipals: []string{\"bad\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif got := dbToLinked(tt.args.p); !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"dbToLinked() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "authority/admin/db/nosql/provisioner.go",
    "content": "package nosql\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n\t\"github.com/smallstep/certificates/authority/admin\"\n\t\"github.com/smallstep/linkedca\"\n\t\"github.com/smallstep/nosql\"\n\t\"google.golang.org/protobuf/types/known/timestamppb\"\n)\n\n// dbProvisioner is the database representation of a Provisioner type.\ntype dbProvisioner struct {\n\tID           string                    `json:\"id\"`\n\tAuthorityID  string                    `json:\"authorityID\"`\n\tType         linkedca.Provisioner_Type `json:\"type\"`\n\tName         string                    `json:\"name\"`\n\tClaims       *linkedca.Claims          `json:\"claims\"`\n\tDetails      []byte                    `json:\"details\"`\n\tX509Template *linkedca.Template        `json:\"x509Template\"`\n\tSSHTemplate  *linkedca.Template        `json:\"sshTemplate\"`\n\tCreatedAt    time.Time                 `json:\"createdAt\"`\n\tDeletedAt    time.Time                 `json:\"deletedAt\"`\n\tWebhooks     []dbWebhook               `json:\"webhooks,omitempty\"`\n}\n\ntype dbBasicAuth struct {\n\tUsername string `json:\"username\"`\n\tPassword string `json:\"password\"`\n}\n\ntype dbWebhook struct {\n\tName                 string       `json:\"name\"`\n\tID                   string       `json:\"id\"`\n\tURL                  string       `json:\"url\"`\n\tKind                 string       `json:\"kind\"`\n\tSecret               string       `json:\"secret\"`\n\tBearerToken          string       `json:\"bearerToken,omitempty\"`\n\tBasicAuth            *dbBasicAuth `json:\"basicAuth,omitempty\"`\n\tDisableTLSClientAuth bool         `json:\"disableTLSClientAuth,omitempty\"`\n\tCertType             string       `json:\"certType,omitempty\"`\n}\n\nfunc (dbp *dbProvisioner) clone() *dbProvisioner {\n\tu := *dbp\n\treturn &u\n}\n\nfunc (dbp *dbProvisioner) convert2linkedca() (*linkedca.Provisioner, error) {\n\tdetails, err := admin.UnmarshalProvisionerDetails(dbp.Type, dbp.Details)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &linkedca.Provisioner{\n\t\tId:           dbp.ID,\n\t\tAuthorityId:  dbp.AuthorityID,\n\t\tType:         dbp.Type,\n\t\tName:         dbp.Name,\n\t\tClaims:       dbp.Claims,\n\t\tDetails:      details,\n\t\tX509Template: dbp.X509Template,\n\t\tSshTemplate:  dbp.SSHTemplate,\n\t\tCreatedAt:    timestamppb.New(dbp.CreatedAt),\n\t\tDeletedAt:    timestamppb.New(dbp.DeletedAt),\n\t\tWebhooks:     dbWebhooksToLinkedca(dbp.Webhooks),\n\t}, nil\n}\n\nfunc (db *DB) getDBProvisionerBytes(_ context.Context, id string) ([]byte, error) {\n\tdata, err := db.db.Get(provisionersTable, []byte(id))\n\tif nosql.IsErrNotFound(err) {\n\t\treturn nil, admin.NewError(admin.ErrorNotFoundType, \"provisioner %s not found\", id)\n\t} else if err != nil {\n\t\treturn nil, errors.Wrapf(err, \"error loading provisioner %s\", id)\n\t}\n\treturn data, nil\n}\n\nfunc (db *DB) unmarshalDBProvisioner(data []byte, id string) (*dbProvisioner, error) {\n\tvar dbp = new(dbProvisioner)\n\tif err := json.Unmarshal(data, dbp); err != nil {\n\t\treturn nil, errors.Wrapf(err, \"error unmarshaling provisioner %s into dbProvisioner\", id)\n\t}\n\tif !dbp.DeletedAt.IsZero() {\n\t\treturn nil, admin.NewError(admin.ErrorDeletedType, \"provisioner %s is deleted\", id)\n\t}\n\tif dbp.AuthorityID != db.authorityID {\n\t\treturn nil, admin.NewError(admin.ErrorAuthorityMismatchType,\n\t\t\t\"provisioner %s is not owned by authority %s\", id, db.authorityID)\n\t}\n\treturn dbp, nil\n}\n\nfunc (db *DB) getDBProvisioner(ctx context.Context, id string) (*dbProvisioner, error) {\n\tdata, err := db.getDBProvisionerBytes(ctx, id)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdbp, err := db.unmarshalDBProvisioner(data, id)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn dbp, nil\n}\n\nfunc (db *DB) unmarshalProvisioner(data []byte, id string) (*linkedca.Provisioner, error) {\n\tdbp, err := db.unmarshalDBProvisioner(data, id)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn dbp.convert2linkedca()\n}\n\n// GetProvisioner retrieves and unmarshals a provisioner from the database.\nfunc (db *DB) GetProvisioner(ctx context.Context, id string) (*linkedca.Provisioner, error) {\n\tdata, err := db.getDBProvisionerBytes(ctx, id)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tprov, err := db.unmarshalProvisioner(data, id)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn prov, nil\n}\n\n// GetProvisioners retrieves and unmarshals all active (not deleted) provisioners\n// from the database.\nfunc (db *DB) GetProvisioners(_ context.Context) ([]*linkedca.Provisioner, error) {\n\tdbEntries, err := db.db.List(provisionersTable)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"error loading provisioners\")\n\t}\n\tvar provs []*linkedca.Provisioner\n\tfor _, entry := range dbEntries {\n\t\tprov, err := db.unmarshalProvisioner(entry.Value, string(entry.Key))\n\t\tif err != nil {\n\t\t\tvar ae *admin.Error\n\t\t\tif errors.As(err, &ae) {\n\t\t\t\tif ae.IsType(admin.ErrorDeletedType) || ae.IsType(admin.ErrorAuthorityMismatchType) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\treturn nil, err\n\t\t}\n\t\tif prov.AuthorityId != db.authorityID {\n\t\t\tcontinue\n\t\t}\n\t\tprovs = append(provs, prov)\n\t}\n\treturn provs, nil\n}\n\n// CreateProvisioner stores a new provisioner to the database.\nfunc (db *DB) CreateProvisioner(ctx context.Context, prov *linkedca.Provisioner) error {\n\tvar err error\n\tprov.Id, err = randID()\n\tif err != nil {\n\t\treturn admin.WrapErrorISE(err, \"error generating random id for provisioner\")\n\t}\n\n\tdetails, err := json.Marshal(prov.Details.GetData())\n\tif err != nil {\n\t\treturn admin.WrapErrorISE(err, \"error marshaling details when creating provisioner %s\", prov.Name)\n\t}\n\n\tdbp := &dbProvisioner{\n\t\tID:           prov.Id,\n\t\tAuthorityID:  db.authorityID,\n\t\tType:         prov.Type,\n\t\tName:         prov.Name,\n\t\tClaims:       prov.Claims,\n\t\tDetails:      details,\n\t\tX509Template: prov.X509Template,\n\t\tSSHTemplate:  prov.SshTemplate,\n\t\tCreatedAt:    clock.Now(),\n\t\tWebhooks:     linkedcaWebhooksToDB(prov.Webhooks),\n\t}\n\n\tif err := db.save(ctx, prov.Id, dbp, nil, \"provisioner\", provisionersTable); err != nil {\n\t\treturn admin.WrapErrorISE(err, \"error creating provisioner %s\", prov.Name)\n\t}\n\n\treturn nil\n}\n\n// UpdateProvisioner saves an updated provisioner to the database.\nfunc (db *DB) UpdateProvisioner(ctx context.Context, prov *linkedca.Provisioner) error {\n\told, err := db.getDBProvisioner(ctx, prov.Id)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tnu := old.clone()\n\n\tif old.Type != prov.Type {\n\t\treturn admin.NewError(admin.ErrorBadRequestType, \"cannot update provisioner type\")\n\t}\n\tnu.Name = prov.Name\n\tnu.Claims = prov.Claims\n\tnu.Details, err = json.Marshal(prov.Details.GetData())\n\tif err != nil {\n\t\treturn admin.WrapErrorISE(err, \"error marshaling details when updating provisioner %s\", prov.Name)\n\t}\n\tnu.X509Template = prov.X509Template\n\tnu.SSHTemplate = prov.SshTemplate\n\tnu.Webhooks = linkedcaWebhooksToDB(prov.Webhooks)\n\n\treturn db.save(ctx, prov.Id, nu, old, \"provisioner\", provisionersTable)\n}\n\n// DeleteProvisioner saves an updated admin to the database.\nfunc (db *DB) DeleteProvisioner(ctx context.Context, id string) error {\n\told, err := db.getDBProvisioner(ctx, id)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tnu := old.clone()\n\tnu.DeletedAt = clock.Now()\n\n\treturn db.save(ctx, old.ID, nu, old, \"provisioner\", provisionersTable)\n}\n\nfunc dbWebhooksToLinkedca(dbwhs []dbWebhook) []*linkedca.Webhook {\n\tif len(dbwhs) == 0 {\n\t\treturn nil\n\t}\n\tlwhs := make([]*linkedca.Webhook, len(dbwhs))\n\n\tfor i, dbwh := range dbwhs {\n\t\tlwh := &linkedca.Webhook{\n\t\t\tName:                 dbwh.Name,\n\t\t\tId:                   dbwh.ID,\n\t\t\tUrl:                  dbwh.URL,\n\t\t\tKind:                 linkedca.Webhook_Kind(linkedca.Webhook_Kind_value[dbwh.Kind]),\n\t\t\tSecret:               dbwh.Secret,\n\t\t\tDisableTlsClientAuth: dbwh.DisableTLSClientAuth,\n\t\t\tCertType:             linkedca.Webhook_CertType(linkedca.Webhook_CertType_value[dbwh.CertType]),\n\t\t}\n\t\tif dbwh.BearerToken != \"\" {\n\t\t\tlwh.Auth = &linkedca.Webhook_BearerToken{\n\t\t\t\tBearerToken: &linkedca.BearerToken{\n\t\t\t\t\tBearerToken: dbwh.BearerToken,\n\t\t\t\t},\n\t\t\t}\n\t\t} else if dbwh.BasicAuth != nil && (dbwh.BasicAuth.Username != \"\" || dbwh.BasicAuth.Password != \"\") {\n\t\t\tlwh.Auth = &linkedca.Webhook_BasicAuth{\n\t\t\t\tBasicAuth: &linkedca.BasicAuth{\n\t\t\t\t\tUsername: dbwh.BasicAuth.Username,\n\t\t\t\t\tPassword: dbwh.BasicAuth.Password,\n\t\t\t\t},\n\t\t\t}\n\t\t}\n\t\tlwhs[i] = lwh\n\t}\n\n\treturn lwhs\n}\n\nfunc linkedcaWebhooksToDB(lwhs []*linkedca.Webhook) []dbWebhook {\n\tif len(lwhs) == 0 {\n\t\treturn nil\n\t}\n\tdbwhs := make([]dbWebhook, len(lwhs))\n\n\tfor i, lwh := range lwhs {\n\t\tdbwh := dbWebhook{\n\t\t\tName:                 lwh.Name,\n\t\t\tID:                   lwh.Id,\n\t\t\tURL:                  lwh.Url,\n\t\t\tKind:                 lwh.Kind.String(),\n\t\t\tSecret:               lwh.Secret,\n\t\t\tDisableTLSClientAuth: lwh.DisableTlsClientAuth,\n\t\t\tCertType:             lwh.CertType.String(),\n\t\t}\n\t\tswitch a := lwh.GetAuth().(type) {\n\t\tcase *linkedca.Webhook_BearerToken:\n\t\t\tdbwh.BearerToken = a.BearerToken.BearerToken\n\t\tcase *linkedca.Webhook_BasicAuth:\n\t\t\tdbwh.BasicAuth = &dbBasicAuth{\n\t\t\t\tUsername: a.BasicAuth.Username,\n\t\t\t\tPassword: a.BasicAuth.Password,\n\t\t\t}\n\t\t}\n\t\tdbwhs[i] = dbwh\n\t}\n\n\treturn dbwhs\n}\n"
  },
  {
    "path": "authority/admin/db/nosql/provisioner_test.go",
    "content": "package nosql\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n\t\"github.com/smallstep/assert\"\n\t\"github.com/smallstep/certificates/authority/admin\"\n\t\"github.com/smallstep/certificates/db\"\n\t\"github.com/smallstep/linkedca\"\n\t\"github.com/smallstep/nosql\"\n\tnosqldb \"github.com/smallstep/nosql/database\"\n)\n\nfunc TestDB_getDBProvisionerBytes(t *testing.T) {\n\tprovID := \"provID\"\n\ttype test struct {\n\t\tdb       nosql.DB\n\t\terr      error\n\t\tadminErr *admin.Error\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/not-found\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, provisionersTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), provID)\n\n\t\t\t\t\t\treturn nil, nosqldb.ErrNotFound\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tadminErr: admin.NewError(admin.ErrorNotFoundType, \"provisioner provID not found\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/db.Get-error\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, provisionersTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), provID)\n\n\t\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"error loading provisioner provID: force\"),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, provisionersTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), provID)\n\n\t\t\t\t\t\treturn []byte(\"foo\"), nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\ttc := run(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\td := DB{db: tc.db}\n\t\t\tif b, err := d.getDBProvisionerBytes(context.Background(), provID); err != nil {\n\t\t\t\tvar ae *admin.Error\n\t\t\t\tif errors.As(err, &ae) {\n\t\t\t\t\tif assert.NotNil(t, tc.adminErr) {\n\t\t\t\t\t\tassert.Equals(t, ae.Type, tc.adminErr.Type)\n\t\t\t\t\t\tassert.Equals(t, ae.Detail, tc.adminErr.Detail)\n\t\t\t\t\t\tassert.Equals(t, ae.Status, tc.adminErr.Status)\n\t\t\t\t\t\tassert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error())\n\t\t\t\t\t\tassert.Equals(t, ae.Detail, tc.adminErr.Detail)\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.err.Error())\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else if assert.Nil(t, tc.err) && assert.Nil(t, tc.adminErr) {\n\t\t\t\tassert.Equals(t, string(b), \"foo\")\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDB_getDBProvisioner(t *testing.T) {\n\tprovID := \"provID\"\n\ttype test struct {\n\t\tdb       nosql.DB\n\t\terr      error\n\t\tadminErr *admin.Error\n\t\tdbp      *dbProvisioner\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/not-found\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, provisionersTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), provID)\n\n\t\t\t\t\t\treturn nil, nosqldb.ErrNotFound\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tadminErr: admin.NewError(admin.ErrorNotFoundType, \"provisioner provID not found\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/db.Get-error\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, provisionersTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), provID)\n\n\t\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"error loading provisioner provID: force\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/unmarshal-error\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, provisionersTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), provID)\n\n\t\t\t\t\t\treturn []byte(\"foo\"), nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"error unmarshaling provisioner provID into dbProvisioner\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/deleted\": func(t *testing.T) test {\n\n\t\t\tnow := clock.Now()\n\t\t\tdbp := &dbProvisioner{\n\t\t\t\tID:          provID,\n\t\t\t\tAuthorityID: admin.DefaultAuthorityID,\n\t\t\t\tType:        linkedca.Provisioner_JWK,\n\t\t\t\tName:        \"provName\",\n\t\t\t\tCreatedAt:   now,\n\t\t\t\tDeletedAt:   now,\n\t\t\t}\n\t\t\tb, err := json.Marshal(dbp)\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, provisionersTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), provID)\n\n\t\t\t\t\t\treturn b, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tadminErr: admin.NewError(admin.ErrorDeletedType, \"provisioner provID is deleted\"),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tnow := clock.Now()\n\t\t\tdbp := &dbProvisioner{\n\t\t\t\tID:          provID,\n\t\t\t\tAuthorityID: admin.DefaultAuthorityID,\n\t\t\t\tType:        linkedca.Provisioner_JWK,\n\t\t\t\tName:        \"provName\",\n\t\t\t\tCreatedAt:   now,\n\t\t\t}\n\t\t\tb, err := json.Marshal(dbp)\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, provisionersTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), provID)\n\n\t\t\t\t\t\treturn b, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tdbp: dbp,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\ttc := run(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\td := DB{db: tc.db, authorityID: admin.DefaultAuthorityID}\n\t\t\tif dbp, err := d.getDBProvisioner(context.Background(), provID); err != nil {\n\t\t\t\tvar ae *admin.Error\n\t\t\t\tif errors.As(err, &ae) {\n\t\t\t\t\tif assert.NotNil(t, tc.adminErr) {\n\t\t\t\t\t\tassert.Equals(t, ae.Type, tc.adminErr.Type)\n\t\t\t\t\t\tassert.Equals(t, ae.Detail, tc.adminErr.Detail)\n\t\t\t\t\t\tassert.Equals(t, ae.Status, tc.adminErr.Status)\n\t\t\t\t\t\tassert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error())\n\t\t\t\t\t\tassert.Equals(t, ae.Detail, tc.adminErr.Detail)\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.err.Error())\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else if assert.Nil(t, tc.err) && assert.Nil(t, tc.adminErr) {\n\t\t\t\tassert.Equals(t, dbp.ID, provID)\n\t\t\t\tassert.Equals(t, dbp.AuthorityID, tc.dbp.AuthorityID)\n\t\t\t\tassert.Equals(t, dbp.Type, tc.dbp.Type)\n\t\t\t\tassert.Equals(t, dbp.Name, tc.dbp.Name)\n\t\t\t\tassert.Equals(t, dbp.CreatedAt, tc.dbp.CreatedAt)\n\t\t\t\tassert.Fatal(t, dbp.DeletedAt.IsZero())\n\t\t\t\tassert.Equals(t, dbp.Webhooks, tc.dbp.Webhooks)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDB_unmarshalDBProvisioner(t *testing.T) {\n\tprovID := \"provID\"\n\ttype test struct {\n\t\tin       []byte\n\t\terr      error\n\t\tadminErr *admin.Error\n\t\tdbp      *dbProvisioner\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/unmarshal-error\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tin:  []byte(\"foo\"),\n\t\t\t\terr: errors.New(\"error unmarshaling provisioner provID into dbProvisioner\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/deleted-error\": func(t *testing.T) test {\n\t\t\tdbp := &dbProvisioner{\n\t\t\t\tDeletedAt: clock.Now(),\n\t\t\t}\n\t\t\tdata, err := json.Marshal(dbp)\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tin:       data,\n\t\t\t\tadminErr: admin.NewError(admin.ErrorDeletedType, \"provisioner %s is deleted\", provID),\n\t\t\t}\n\t\t},\n\t\t\"fail/authority-mismatch-error\": func(t *testing.T) test {\n\t\t\tdbp := &dbProvisioner{\n\t\t\t\tID:          provID,\n\t\t\t\tAuthorityID: \"foo\",\n\t\t\t}\n\t\t\tdata, err := json.Marshal(dbp)\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tin: data,\n\t\t\t\tadminErr: admin.NewError(admin.ErrorAuthorityMismatchType,\n\t\t\t\t\t\"provisioner %s is not owned by authority %s\", provID, admin.DefaultAuthorityID),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tdbp := &dbProvisioner{\n\t\t\t\tID:          provID,\n\t\t\t\tAuthorityID: admin.DefaultAuthorityID,\n\t\t\t\tType:        linkedca.Provisioner_JWK,\n\t\t\t\tName:        \"provName\",\n\t\t\t\tCreatedAt:   clock.Now(),\n\t\t\t}\n\t\t\tdata, err := json.Marshal(dbp)\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tin:  data,\n\t\t\t\tdbp: dbp,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\ttc := run(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\td := DB{authorityID: admin.DefaultAuthorityID}\n\t\t\tif dbp, err := d.unmarshalDBProvisioner(tc.in, provID); err != nil {\n\t\t\t\tvar ae *admin.Error\n\t\t\t\tif errors.As(err, &ae) {\n\t\t\t\t\tif assert.NotNil(t, tc.adminErr) {\n\t\t\t\t\t\tassert.Equals(t, ae.Type, tc.adminErr.Type)\n\t\t\t\t\t\tassert.Equals(t, ae.Detail, tc.adminErr.Detail)\n\t\t\t\t\t\tassert.Equals(t, ae.Status, tc.adminErr.Status)\n\t\t\t\t\t\tassert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error())\n\t\t\t\t\t\tassert.Equals(t, ae.Detail, tc.adminErr.Detail)\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.err.Error())\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else if assert.Nil(t, tc.err) && assert.Nil(t, tc.adminErr) {\n\t\t\t\tassert.Equals(t, dbp.ID, provID)\n\t\t\t\tassert.Equals(t, dbp.AuthorityID, tc.dbp.AuthorityID)\n\t\t\t\tassert.Equals(t, dbp.Type, tc.dbp.Type)\n\t\t\t\tassert.Equals(t, dbp.Name, tc.dbp.Name)\n\t\t\t\tassert.Equals(t, dbp.Details, tc.dbp.Details)\n\t\t\t\tassert.Equals(t, dbp.Claims, tc.dbp.Claims)\n\t\t\t\tassert.Equals(t, dbp.X509Template, tc.dbp.X509Template)\n\t\t\t\tassert.Equals(t, dbp.SSHTemplate, tc.dbp.SSHTemplate)\n\t\t\t\tassert.Equals(t, dbp.CreatedAt, tc.dbp.CreatedAt)\n\t\t\t\tassert.Fatal(t, dbp.DeletedAt.IsZero())\n\t\t\t\tassert.Equals(t, dbp.Webhooks, tc.dbp.Webhooks)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc defaultDBP(t *testing.T) *dbProvisioner {\n\tdetails := &linkedca.ProvisionerDetails_ACME{\n\t\tACME: &linkedca.ACMEProvisioner{\n\t\t\tForceCn: true,\n\t\t},\n\t}\n\tdetailBytes, err := json.Marshal(details)\n\tassert.FatalError(t, err)\n\n\treturn &dbProvisioner{\n\t\tID:          \"provID\",\n\t\tAuthorityID: admin.DefaultAuthorityID,\n\t\tType:        linkedca.Provisioner_ACME,\n\t\tName:        \"provName\",\n\t\tDetails:     detailBytes,\n\t\tClaims: &linkedca.Claims{\n\t\t\tDisableRenewal: true,\n\t\t\tX509: &linkedca.X509Claims{\n\t\t\t\tEnabled: true,\n\t\t\t\tDurations: &linkedca.Durations{\n\t\t\t\t\tMin:     \"5m\",\n\t\t\t\t\tMax:     \"12h\",\n\t\t\t\t\tDefault: \"6h\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tSsh: &linkedca.SSHClaims{\n\t\t\t\tEnabled: true,\n\t\t\t\tUserDurations: &linkedca.Durations{\n\t\t\t\t\tMin:     \"5m\",\n\t\t\t\t\tMax:     \"12h\",\n\t\t\t\t\tDefault: \"6h\",\n\t\t\t\t},\n\t\t\t\tHostDurations: &linkedca.Durations{\n\t\t\t\t\tMin:     \"5m\",\n\t\t\t\t\tMax:     \"12h\",\n\t\t\t\t\tDefault: \"6h\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tX509Template: &linkedca.Template{\n\t\t\tTemplate: []byte(\"foo\"),\n\t\t\tData:     []byte(\"bar\"),\n\t\t},\n\t\tSSHTemplate: &linkedca.Template{\n\t\t\tTemplate: []byte(\"baz\"),\n\t\t\tData:     []byte(\"zap\"),\n\t\t},\n\t\tCreatedAt: clock.Now(),\n\t\tWebhooks: []dbWebhook{\n\t\t\t{\n\t\t\t\tName:        \"metadata\",\n\t\t\t\tURL:         \"https://inventory.smallstep.com\",\n\t\t\t\tKind:        linkedca.Webhook_ENRICHING.String(),\n\t\t\t\tSecret:      \"secret\",\n\t\t\t\tBearerToken: \"token\",\n\t\t\t},\n\t\t},\n\t}\n}\n\nfunc TestDB_unmarshalProvisioner(t *testing.T) {\n\tprovID := \"provID\"\n\ttype test struct {\n\t\tin       []byte\n\t\terr      error\n\t\tadminErr *admin.Error\n\t\tdbp      *dbProvisioner\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/unmarshal-error\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tin:  []byte(\"foo\"),\n\t\t\t\terr: errors.New(\"error unmarshaling provisioner provID into dbProvisioner\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/deleted-error\": func(t *testing.T) test {\n\t\t\tdbp := &dbProvisioner{\n\t\t\t\tDeletedAt: time.Now(),\n\t\t\t}\n\t\t\tdata, err := json.Marshal(dbp)\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tin:       data,\n\t\t\t\tadminErr: admin.NewError(admin.ErrorDeletedType, \"provisioner provID is deleted\"),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tdbp := defaultDBP(t)\n\t\t\tdata, err := json.Marshal(dbp)\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tin:  data,\n\t\t\t\tdbp: dbp,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\ttc := run(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\td := DB{authorityID: admin.DefaultAuthorityID}\n\t\t\tif prov, err := d.unmarshalProvisioner(tc.in, provID); err != nil {\n\t\t\t\tvar ae *admin.Error\n\t\t\t\tif errors.As(err, &ae) {\n\t\t\t\t\tif assert.NotNil(t, tc.adminErr) {\n\t\t\t\t\t\tassert.Equals(t, ae.Type, tc.adminErr.Type)\n\t\t\t\t\t\tassert.Equals(t, ae.Detail, tc.adminErr.Detail)\n\t\t\t\t\t\tassert.Equals(t, ae.Status, tc.adminErr.Status)\n\t\t\t\t\t\tassert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error())\n\t\t\t\t\t\tassert.Equals(t, ae.Detail, tc.adminErr.Detail)\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.err.Error())\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else if assert.Nil(t, tc.err) && assert.Nil(t, tc.adminErr) {\n\t\t\t\tassert.Equals(t, prov.Id, provID)\n\t\t\t\tassert.Equals(t, prov.AuthorityId, tc.dbp.AuthorityID)\n\t\t\t\tassert.Equals(t, prov.Type, tc.dbp.Type)\n\t\t\t\tassert.Equals(t, prov.Name, tc.dbp.Name)\n\t\t\t\tassert.Equals(t, prov.Claims, tc.dbp.Claims)\n\t\t\t\tassert.Equals(t, prov.X509Template, tc.dbp.X509Template)\n\t\t\t\tassert.Equals(t, prov.SshTemplate, tc.dbp.SSHTemplate)\n\t\t\t\tassert.Equals(t, prov.Webhooks, dbWebhooksToLinkedca(tc.dbp.Webhooks))\n\n\t\t\t\tretDetailsBytes, err := json.Marshal(prov.Details.GetData())\n\t\t\t\tassert.FatalError(t, err)\n\t\t\t\tassert.Equals(t, retDetailsBytes, tc.dbp.Details)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDB_GetProvisioner(t *testing.T) {\n\tprovID := \"provID\"\n\ttype test struct {\n\t\tdb       nosql.DB\n\t\terr      error\n\t\tadminErr *admin.Error\n\t\tdbp      *dbProvisioner\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/not-found\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, provisionersTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), provID)\n\n\t\t\t\t\t\treturn nil, nosqldb.ErrNotFound\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tadminErr: admin.NewError(admin.ErrorNotFoundType, \"provisioner provID not found\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/db.Get-error\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, provisionersTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), provID)\n\n\t\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"error loading provisioner provID: force\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/unmarshal-error\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, provisionersTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), provID)\n\n\t\t\t\t\t\treturn []byte(\"foo\"), nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"error unmarshaling provisioner provID into dbProvisioner\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/deleted\": func(t *testing.T) test {\n\t\t\tdbp := defaultDBP(t)\n\t\t\tdbp.DeletedAt = clock.Now()\n\t\t\tb, err := json.Marshal(dbp)\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, provisionersTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), provID)\n\n\t\t\t\t\t\treturn b, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tdbp:      dbp,\n\t\t\t\tadminErr: admin.NewError(admin.ErrorDeletedType, \"provisioner provID is deleted\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/authorityID-mismatch\": func(t *testing.T) test {\n\t\t\tdbp := defaultDBP(t)\n\t\t\tdbp.AuthorityID = \"foo\"\n\t\t\tb, err := json.Marshal(dbp)\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, provisionersTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), provID)\n\n\t\t\t\t\t\treturn b, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tdbp: dbp,\n\t\t\t\tadminErr: admin.NewError(admin.ErrorAuthorityMismatchType,\n\t\t\t\t\t\"provisioner %s is not owned by authority %s\", dbp.ID, admin.DefaultAuthorityID),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tdbp := defaultDBP(t)\n\t\t\tb, err := json.Marshal(dbp)\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, provisionersTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), provID)\n\n\t\t\t\t\t\treturn b, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tdbp: dbp,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\ttc := run(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\td := DB{db: tc.db, authorityID: admin.DefaultAuthorityID}\n\t\t\tif prov, err := d.GetProvisioner(context.Background(), provID); err != nil {\n\t\t\t\tvar ae *admin.Error\n\t\t\t\tif errors.As(err, &ae) {\n\t\t\t\t\tif assert.NotNil(t, tc.adminErr) {\n\t\t\t\t\t\tassert.Equals(t, ae.Type, tc.adminErr.Type)\n\t\t\t\t\t\tassert.Equals(t, ae.Detail, tc.adminErr.Detail)\n\t\t\t\t\t\tassert.Equals(t, ae.Status, tc.adminErr.Status)\n\t\t\t\t\t\tassert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error())\n\t\t\t\t\t\tassert.Equals(t, ae.Detail, tc.adminErr.Detail)\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.err.Error())\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else if assert.Nil(t, tc.err) && assert.Nil(t, tc.adminErr) {\n\t\t\t\tassert.Equals(t, prov.Id, provID)\n\t\t\t\tassert.Equals(t, prov.AuthorityId, tc.dbp.AuthorityID)\n\t\t\t\tassert.Equals(t, prov.Type, tc.dbp.Type)\n\t\t\t\tassert.Equals(t, prov.Name, tc.dbp.Name)\n\t\t\t\tassert.Equals(t, prov.Claims, tc.dbp.Claims)\n\t\t\t\tassert.Equals(t, prov.X509Template, tc.dbp.X509Template)\n\t\t\t\tassert.Equals(t, prov.SshTemplate, tc.dbp.SSHTemplate)\n\t\t\t\tassert.Equals(t, prov.Webhooks, dbWebhooksToLinkedca(tc.dbp.Webhooks))\n\n\t\t\t\tretDetailsBytes, err := json.Marshal(prov.Details.GetData())\n\t\t\t\tassert.FatalError(t, err)\n\t\t\t\tassert.Equals(t, retDetailsBytes, tc.dbp.Details)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDB_DeleteProvisioner(t *testing.T) {\n\tprovID := \"provID\"\n\ttype test struct {\n\t\tdb       nosql.DB\n\t\terr      error\n\t\tadminErr *admin.Error\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/not-found\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, provisionersTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), provID)\n\n\t\t\t\t\t\treturn nil, nosqldb.ErrNotFound\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tadminErr: admin.NewError(admin.ErrorNotFoundType, \"provisioner provID not found\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/db.Get-error\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, provisionersTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), provID)\n\n\t\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"error loading provisioner provID: force\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/save-error\": func(t *testing.T) test {\n\t\t\tdbp := defaultDBP(t)\n\t\t\tdata, err := json.Marshal(dbp)\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, provisionersTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), provID)\n\n\t\t\t\t\t\treturn data, nil\n\t\t\t\t\t},\n\t\t\t\t\tMCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, provisionersTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), provID)\n\t\t\t\t\t\tassert.Equals(t, string(old), string(data))\n\n\t\t\t\t\t\tvar _dbp = new(dbProvisioner)\n\t\t\t\t\t\tassert.FatalError(t, json.Unmarshal(nu, _dbp))\n\n\t\t\t\t\t\tassert.Equals(t, _dbp.ID, provID)\n\t\t\t\t\t\tassert.Equals(t, _dbp.AuthorityID, dbp.AuthorityID)\n\t\t\t\t\t\tassert.Equals(t, _dbp.Type, dbp.Type)\n\t\t\t\t\t\tassert.Equals(t, _dbp.Name, dbp.Name)\n\t\t\t\t\t\tassert.Equals(t, _dbp.Claims, dbp.Claims)\n\t\t\t\t\t\tassert.Equals(t, _dbp.X509Template, dbp.X509Template)\n\t\t\t\t\t\tassert.Equals(t, _dbp.SSHTemplate, dbp.SSHTemplate)\n\t\t\t\t\t\tassert.Equals(t, _dbp.CreatedAt, dbp.CreatedAt)\n\t\t\t\t\t\tassert.Equals(t, _dbp.Details, dbp.Details)\n\t\t\t\t\t\tassert.Equals(t, _dbp.Webhooks, dbp.Webhooks)\n\n\t\t\t\t\t\tassert.True(t, _dbp.DeletedAt.Before(time.Now()))\n\t\t\t\t\t\tassert.True(t, _dbp.DeletedAt.After(time.Now().Add(-time.Minute)))\n\n\t\t\t\t\t\treturn nil, false, errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"error saving authority provisioner: force\"),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tdbp := defaultDBP(t)\n\t\t\tdata, err := json.Marshal(dbp)\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, provisionersTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), provID)\n\n\t\t\t\t\t\treturn data, nil\n\t\t\t\t\t},\n\t\t\t\t\tMCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, provisionersTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), provID)\n\t\t\t\t\t\tassert.Equals(t, string(old), string(data))\n\n\t\t\t\t\t\tvar _dbp = new(dbProvisioner)\n\t\t\t\t\t\tassert.FatalError(t, json.Unmarshal(nu, _dbp))\n\n\t\t\t\t\t\tassert.Equals(t, _dbp.ID, provID)\n\t\t\t\t\t\tassert.Equals(t, _dbp.AuthorityID, dbp.AuthorityID)\n\t\t\t\t\t\tassert.Equals(t, _dbp.Type, dbp.Type)\n\t\t\t\t\t\tassert.Equals(t, _dbp.Name, dbp.Name)\n\t\t\t\t\t\tassert.Equals(t, _dbp.Claims, dbp.Claims)\n\t\t\t\t\t\tassert.Equals(t, _dbp.X509Template, dbp.X509Template)\n\t\t\t\t\t\tassert.Equals(t, _dbp.SSHTemplate, dbp.SSHTemplate)\n\t\t\t\t\t\tassert.Equals(t, _dbp.CreatedAt, dbp.CreatedAt)\n\t\t\t\t\t\tassert.Equals(t, _dbp.Details, dbp.Details)\n\t\t\t\t\t\tassert.Equals(t, _dbp.Webhooks, dbp.Webhooks)\n\n\t\t\t\t\t\tassert.True(t, _dbp.DeletedAt.Before(time.Now()))\n\t\t\t\t\t\tassert.True(t, _dbp.DeletedAt.After(time.Now().Add(-time.Minute)))\n\n\t\t\t\t\t\treturn nu, true, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\ttc := run(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\td := DB{db: tc.db, authorityID: admin.DefaultAuthorityID}\n\t\t\tif err := d.DeleteProvisioner(context.Background(), provID); err != nil {\n\t\t\t\tvar ae *admin.Error\n\t\t\t\tif errors.As(err, &ae) {\n\t\t\t\t\tif assert.NotNil(t, tc.adminErr) {\n\t\t\t\t\t\tassert.Equals(t, ae.Type, tc.adminErr.Type)\n\t\t\t\t\t\tassert.Equals(t, ae.Detail, tc.adminErr.Detail)\n\t\t\t\t\t\tassert.Equals(t, ae.Status, tc.adminErr.Status)\n\t\t\t\t\t\tassert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error())\n\t\t\t\t\t\tassert.Equals(t, ae.Detail, tc.adminErr.Detail)\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.err.Error())\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDB_GetProvisioners(t *testing.T) {\n\tfooProv := defaultDBP(t)\n\tfooProv.Name = \"foo\"\n\tfoob, err := json.Marshal(fooProv)\n\tassert.FatalError(t, err)\n\n\tbarProv := defaultDBP(t)\n\tbarProv.Name = \"bar\"\n\tbarProv.DeletedAt = clock.Now()\n\tbarb, err := json.Marshal(barProv)\n\tassert.FatalError(t, err)\n\n\tbazProv := defaultDBP(t)\n\tbazProv.Name = \"baz\"\n\tbazProv.AuthorityID = \"baz\"\n\tbazb, err := json.Marshal(bazProv)\n\tassert.FatalError(t, err)\n\n\tzapProv := defaultDBP(t)\n\tzapProv.Name = \"zap\"\n\tzapb, err := json.Marshal(zapProv)\n\tassert.FatalError(t, err)\n\n\ttype test struct {\n\t\tdb       nosql.DB\n\t\terr      error\n\t\tadminErr *admin.Error\n\t\tverify   func(*testing.T, []*linkedca.Provisioner)\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/db.List-error\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMList: func(bucket []byte) ([]*nosqldb.Entry, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, provisionersTable)\n\n\t\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"error loading provisioners\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/unmarshal-error\": func(t *testing.T) test {\n\t\t\tret := []*nosqldb.Entry{\n\t\t\t\t{Bucket: provisionersTable, Key: []byte(\"foo\"), Value: foob},\n\t\t\t\t{Bucket: provisionersTable, Key: []byte(\"bar\"), Value: barb},\n\t\t\t\t{Bucket: provisionersTable, Key: []byte(\"zap\"), Value: []byte(\"zap\")},\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMList: func(bucket []byte) ([]*nosqldb.Entry, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, provisionersTable)\n\n\t\t\t\t\t\treturn ret, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"error unmarshaling provisioner zap into dbProvisioner\"),\n\t\t\t}\n\t\t},\n\t\t\"ok/none\": func(t *testing.T) test {\n\t\t\tret := []*nosqldb.Entry{}\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMList: func(bucket []byte) ([]*nosqldb.Entry, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, provisionersTable)\n\n\t\t\t\t\t\treturn ret, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tverify: func(t *testing.T, provs []*linkedca.Provisioner) {\n\t\t\t\t\tassert.Equals(t, len(provs), 0)\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"ok/only-invalid\": func(t *testing.T) test {\n\t\t\tret := []*nosqldb.Entry{\n\t\t\t\t{Bucket: provisionersTable, Key: []byte(\"bar\"), Value: barb},\n\t\t\t\t{Bucket: provisionersTable, Key: []byte(\"baz\"), Value: bazb},\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMList: func(bucket []byte) ([]*nosqldb.Entry, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, provisionersTable)\n\n\t\t\t\t\t\treturn ret, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tverify: func(t *testing.T, provs []*linkedca.Provisioner) {\n\t\t\t\t\tassert.Equals(t, len(provs), 0)\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tret := []*nosqldb.Entry{\n\t\t\t\t{Bucket: provisionersTable, Key: []byte(\"foo\"), Value: foob},\n\t\t\t\t{Bucket: provisionersTable, Key: []byte(\"bar\"), Value: barb},\n\t\t\t\t{Bucket: provisionersTable, Key: []byte(\"baz\"), Value: bazb},\n\t\t\t\t{Bucket: provisionersTable, Key: []byte(\"zap\"), Value: zapb},\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMList: func(bucket []byte) ([]*nosqldb.Entry, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, provisionersTable)\n\n\t\t\t\t\t\treturn ret, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tverify: func(t *testing.T, provs []*linkedca.Provisioner) {\n\t\t\t\t\tassert.Equals(t, len(provs), 2)\n\n\t\t\t\t\tassert.Equals(t, provs[0].Id, fooProv.ID)\n\t\t\t\t\tassert.Equals(t, provs[0].AuthorityId, fooProv.AuthorityID)\n\t\t\t\t\tassert.Equals(t, provs[0].Type, fooProv.Type)\n\t\t\t\t\tassert.Equals(t, provs[0].Name, fooProv.Name)\n\t\t\t\t\tassert.Equals(t, provs[0].Claims, fooProv.Claims)\n\t\t\t\t\tassert.Equals(t, provs[0].X509Template, fooProv.X509Template)\n\t\t\t\t\tassert.Equals(t, provs[0].SshTemplate, fooProv.SSHTemplate)\n\t\t\t\t\tassert.Equals(t, provs[0].Webhooks, dbWebhooksToLinkedca(fooProv.Webhooks))\n\n\t\t\t\t\tretDetailsBytes, err := json.Marshal(provs[0].Details.GetData())\n\t\t\t\t\tassert.FatalError(t, err)\n\t\t\t\t\tassert.Equals(t, retDetailsBytes, fooProv.Details)\n\n\t\t\t\t\tassert.Equals(t, provs[1].Id, zapProv.ID)\n\t\t\t\t\tassert.Equals(t, provs[1].AuthorityId, zapProv.AuthorityID)\n\t\t\t\t\tassert.Equals(t, provs[1].Type, zapProv.Type)\n\t\t\t\t\tassert.Equals(t, provs[1].Name, zapProv.Name)\n\t\t\t\t\tassert.Equals(t, provs[1].Claims, zapProv.Claims)\n\t\t\t\t\tassert.Equals(t, provs[1].X509Template, zapProv.X509Template)\n\t\t\t\t\tassert.Equals(t, provs[1].SshTemplate, zapProv.SSHTemplate)\n\t\t\t\t\tassert.Equals(t, provs[1].Webhooks, dbWebhooksToLinkedca(zapProv.Webhooks))\n\n\t\t\t\t\tretDetailsBytes, err = json.Marshal(provs[1].Details.GetData())\n\t\t\t\t\tassert.FatalError(t, err)\n\t\t\t\t\tassert.Equals(t, retDetailsBytes, zapProv.Details)\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\ttc := run(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\td := DB{db: tc.db, authorityID: admin.DefaultAuthorityID}\n\t\t\tif provs, err := d.GetProvisioners(context.Background()); err != nil {\n\t\t\t\tvar ae *admin.Error\n\t\t\t\tif errors.As(err, &ae) {\n\t\t\t\t\tif assert.NotNil(t, tc.adminErr) {\n\t\t\t\t\t\tassert.Equals(t, ae.Type, tc.adminErr.Type)\n\t\t\t\t\t\tassert.Equals(t, ae.Detail, tc.adminErr.Detail)\n\t\t\t\t\t\tassert.Equals(t, ae.Status, tc.adminErr.Status)\n\t\t\t\t\t\tassert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error())\n\t\t\t\t\t\tassert.Equals(t, ae.Detail, tc.adminErr.Detail)\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.err.Error())\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else if assert.Nil(t, tc.err) && assert.Nil(t, tc.adminErr) {\n\t\t\t\ttc.verify(t, provs)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDB_CreateProvisioner(t *testing.T) {\n\ttype test struct {\n\t\tdb       nosql.DB\n\t\terr      error\n\t\tadminErr *admin.Error\n\t\tprov     *linkedca.Provisioner\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/save-error\": func(t *testing.T) test {\n\t\t\tdbp := defaultDBP(t)\n\t\t\tprov, err := dbp.convert2linkedca()\n\t\t\tassert.FatalError(t, err)\n\n\t\t\treturn test{\n\t\t\t\tprov: prov,\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, provisionersTable)\n\t\t\t\t\t\tassert.Equals(t, old, nil)\n\n\t\t\t\t\t\tvar _dbp = new(dbProvisioner)\n\t\t\t\t\t\tassert.FatalError(t, json.Unmarshal(nu, _dbp))\n\n\t\t\t\t\t\tassert.True(t, _dbp.ID != \"\" && _dbp.ID == string(key))\n\t\t\t\t\t\tassert.Equals(t, _dbp.AuthorityID, prov.AuthorityId)\n\t\t\t\t\t\tassert.Equals(t, _dbp.Type, prov.Type)\n\t\t\t\t\t\tassert.Equals(t, _dbp.Name, prov.Name)\n\t\t\t\t\t\tassert.Equals(t, _dbp.Claims, prov.Claims)\n\t\t\t\t\t\tassert.Equals(t, _dbp.X509Template, prov.X509Template)\n\t\t\t\t\t\tassert.Equals(t, _dbp.SSHTemplate, prov.SshTemplate)\n\t\t\t\t\t\tassert.Equals(t, _dbp.Webhooks, linkedcaWebhooksToDB(prov.Webhooks))\n\n\t\t\t\t\t\tretDetailsBytes, err := json.Marshal(prov.Details.GetData())\n\t\t\t\t\t\tassert.FatalError(t, err)\n\t\t\t\t\t\tassert.Equals(t, retDetailsBytes, _dbp.Details)\n\n\t\t\t\t\t\tassert.True(t, _dbp.DeletedAt.IsZero())\n\t\t\t\t\t\tassert.True(t, _dbp.CreatedAt.Before(time.Now()))\n\t\t\t\t\t\tassert.True(t, _dbp.CreatedAt.After(time.Now().Add(-time.Minute)))\n\n\t\t\t\t\t\treturn nil, false, errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tadminErr: admin.NewErrorISE(\"error creating provisioner provName: error saving authority provisioner: force\"),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tdbp := defaultDBP(t)\n\t\t\tprov, err := dbp.convert2linkedca()\n\t\t\tassert.FatalError(t, err)\n\n\t\t\treturn test{\n\t\t\t\tprov: prov,\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, provisionersTable)\n\t\t\t\t\t\tassert.Equals(t, old, nil)\n\n\t\t\t\t\t\tvar _dbp = new(dbProvisioner)\n\t\t\t\t\t\tassert.FatalError(t, json.Unmarshal(nu, _dbp))\n\n\t\t\t\t\t\tassert.True(t, _dbp.ID != \"\" && _dbp.ID == string(key))\n\t\t\t\t\t\tassert.Equals(t, _dbp.AuthorityID, prov.AuthorityId)\n\t\t\t\t\t\tassert.Equals(t, _dbp.Type, prov.Type)\n\t\t\t\t\t\tassert.Equals(t, _dbp.Name, prov.Name)\n\t\t\t\t\t\tassert.Equals(t, _dbp.Claims, prov.Claims)\n\t\t\t\t\t\tassert.Equals(t, _dbp.X509Template, prov.X509Template)\n\t\t\t\t\t\tassert.Equals(t, _dbp.SSHTemplate, prov.SshTemplate)\n\t\t\t\t\t\tassert.Equals(t, _dbp.Webhooks, linkedcaWebhooksToDB(prov.Webhooks))\n\n\t\t\t\t\t\tretDetailsBytes, err := json.Marshal(prov.Details.GetData())\n\t\t\t\t\t\tassert.FatalError(t, err)\n\t\t\t\t\t\tassert.Equals(t, retDetailsBytes, _dbp.Details)\n\n\t\t\t\t\t\tassert.True(t, _dbp.DeletedAt.IsZero())\n\t\t\t\t\t\tassert.True(t, _dbp.CreatedAt.Before(time.Now()))\n\t\t\t\t\t\tassert.True(t, _dbp.CreatedAt.After(time.Now().Add(-time.Minute)))\n\n\t\t\t\t\t\treturn nu, true, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\ttc := run(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\td := DB{db: tc.db, authorityID: admin.DefaultAuthorityID}\n\t\t\tif err := d.CreateProvisioner(context.Background(), tc.prov); err != nil {\n\t\t\t\tvar ae *admin.Error\n\t\t\t\tif errors.As(err, &ae) {\n\t\t\t\t\tif assert.NotNil(t, tc.adminErr) {\n\t\t\t\t\t\tassert.Equals(t, ae.Type, tc.adminErr.Type)\n\t\t\t\t\t\tassert.Equals(t, ae.Detail, tc.adminErr.Detail)\n\t\t\t\t\t\tassert.Equals(t, ae.Status, tc.adminErr.Status)\n\t\t\t\t\t\tassert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error())\n\t\t\t\t\t\tassert.Equals(t, ae.Detail, tc.adminErr.Detail)\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.err.Error())\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDB_UpdateProvisioner(t *testing.T) {\n\tprovID := \"provID\"\n\ttype test struct {\n\t\tdb       nosql.DB\n\t\terr      error\n\t\tadminErr *admin.Error\n\t\tprov     *linkedca.Provisioner\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/not-found\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tprov: &linkedca.Provisioner{Id: provID},\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, provisionersTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), provID)\n\n\t\t\t\t\t\treturn nil, nosqldb.ErrNotFound\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tadminErr: admin.NewError(admin.ErrorNotFoundType, \"provisioner provID not found\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/db.Get-error\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tprov: &linkedca.Provisioner{Id: provID},\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, provisionersTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), provID)\n\n\t\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"error loading provisioner provID: force\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/update-deleted\": func(t *testing.T) test {\n\t\t\tdbp := defaultDBP(t)\n\t\t\tdbp.DeletedAt = clock.Now()\n\t\t\tdata, err := json.Marshal(dbp)\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tprov: &linkedca.Provisioner{Id: provID},\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, provisionersTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), provID)\n\n\t\t\t\t\t\treturn data, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tadminErr: admin.NewError(admin.ErrorDeletedType, \"provisioner %s is deleted\", provID),\n\t\t\t}\n\t\t},\n\t\t\"fail/update-type-error\": func(t *testing.T) test {\n\t\t\tdbp := defaultDBP(t)\n\n\t\t\tupd, err := dbp.convert2linkedca()\n\t\t\tassert.FatalError(t, err)\n\t\t\tupd.Type = linkedca.Provisioner_JWK\n\n\t\t\tdata, err := json.Marshal(dbp)\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tprov: upd,\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, provisionersTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), provID)\n\n\t\t\t\t\t\treturn data, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tadminErr: admin.NewError(admin.ErrorBadRequestType, \"cannot update provisioner type\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/save-error\": func(t *testing.T) test {\n\t\t\tdbp := defaultDBP(t)\n\n\t\t\tprov, err := dbp.convert2linkedca()\n\t\t\tassert.FatalError(t, err)\n\n\t\t\tdata, err := json.Marshal(dbp)\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tprov: prov,\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, provisionersTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), provID)\n\n\t\t\t\t\t\treturn data, nil\n\t\t\t\t\t},\n\t\t\t\t\tMCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, provisionersTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), provID)\n\t\t\t\t\t\tassert.Equals(t, string(old), string(data))\n\n\t\t\t\t\t\tvar _dbp = new(dbProvisioner)\n\t\t\t\t\t\tassert.FatalError(t, json.Unmarshal(nu, _dbp))\n\n\t\t\t\t\t\tassert.True(t, _dbp.ID != \"\" && _dbp.ID == string(key))\n\t\t\t\t\t\tassert.Equals(t, _dbp.AuthorityID, prov.AuthorityId)\n\t\t\t\t\t\tassert.Equals(t, _dbp.Type, prov.Type)\n\t\t\t\t\t\tassert.Equals(t, _dbp.Name, prov.Name)\n\t\t\t\t\t\tassert.Equals(t, _dbp.Claims, prov.Claims)\n\t\t\t\t\t\tassert.Equals(t, _dbp.X509Template, prov.X509Template)\n\t\t\t\t\t\tassert.Equals(t, _dbp.SSHTemplate, prov.SshTemplate)\n\t\t\t\t\t\tassert.Equals(t, _dbp.Webhooks, linkedcaWebhooksToDB(prov.Webhooks))\n\n\t\t\t\t\t\tretDetailsBytes, err := json.Marshal(prov.Details.GetData())\n\t\t\t\t\t\tassert.FatalError(t, err)\n\t\t\t\t\t\tassert.Equals(t, retDetailsBytes, _dbp.Details)\n\n\t\t\t\t\t\tassert.True(t, _dbp.DeletedAt.IsZero())\n\t\t\t\t\t\tassert.True(t, _dbp.CreatedAt.Before(time.Now()))\n\t\t\t\t\t\tassert.True(t, _dbp.CreatedAt.After(time.Now().Add(-time.Minute)))\n\n\t\t\t\t\t\treturn nil, false, errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"error saving authority provisioner: force\"),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tdbp := defaultDBP(t)\n\n\t\t\tprov, err := dbp.convert2linkedca()\n\t\t\tassert.FatalError(t, err)\n\n\t\t\tprov.Name = \"new-name\"\n\t\t\tprov.Claims = &linkedca.Claims{\n\t\t\t\tDisableRenewal: true,\n\t\t\t\tX509: &linkedca.X509Claims{\n\t\t\t\t\tEnabled: true,\n\t\t\t\t\tDurations: &linkedca.Durations{\n\t\t\t\t\t\tMin:     \"10m\",\n\t\t\t\t\t\tMax:     \"8h\",\n\t\t\t\t\t\tDefault: \"4h\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tSsh: &linkedca.SSHClaims{\n\t\t\t\t\tEnabled: true,\n\t\t\t\t\tUserDurations: &linkedca.Durations{\n\t\t\t\t\t\tMin:     \"7m\",\n\t\t\t\t\t\tMax:     \"11h\",\n\t\t\t\t\t\tDefault: \"5h\",\n\t\t\t\t\t},\n\t\t\t\t\tHostDurations: &linkedca.Durations{\n\t\t\t\t\t\tMin:     \"4m\",\n\t\t\t\t\t\tMax:     \"24h\",\n\t\t\t\t\t\tDefault: \"24h\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t\tprov.X509Template = &linkedca.Template{\n\t\t\t\tTemplate: []byte(\"x\"),\n\t\t\t\tData:     []byte(\"y\"),\n\t\t\t}\n\t\t\tprov.SshTemplate = &linkedca.Template{\n\t\t\t\tTemplate: []byte(\"z\"),\n\t\t\t\tData:     []byte(\"w\"),\n\t\t\t}\n\t\t\tprov.Details = &linkedca.ProvisionerDetails{\n\t\t\t\tData: &linkedca.ProvisionerDetails_ACME{\n\t\t\t\t\tACME: &linkedca.ACMEProvisioner{\n\t\t\t\t\t\tForceCn: false,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t\tprov.Webhooks = []*linkedca.Webhook{\n\t\t\t\t{\n\t\t\t\t\tName: \"users\",\n\t\t\t\t\tUrl:  \"https://example.com/users\",\n\t\t\t\t},\n\t\t\t}\n\n\t\t\tdata, err := json.Marshal(dbp)\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tprov: prov,\n\t\t\t\tdb: &db.MockNoSQLDB{\n\t\t\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, provisionersTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), provID)\n\n\t\t\t\t\t\treturn data, nil\n\t\t\t\t\t},\n\t\t\t\t\tMCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) {\n\t\t\t\t\t\tassert.Equals(t, bucket, provisionersTable)\n\t\t\t\t\t\tassert.Equals(t, string(key), provID)\n\t\t\t\t\t\tassert.Equals(t, string(old), string(data))\n\n\t\t\t\t\t\tvar _dbp = new(dbProvisioner)\n\t\t\t\t\t\tassert.FatalError(t, json.Unmarshal(nu, _dbp))\n\n\t\t\t\t\t\tassert.True(t, _dbp.ID != \"\" && _dbp.ID == string(key))\n\t\t\t\t\t\tassert.Equals(t, _dbp.AuthorityID, prov.AuthorityId)\n\t\t\t\t\t\tassert.Equals(t, _dbp.Type, prov.Type)\n\t\t\t\t\t\tassert.Equals(t, _dbp.Name, prov.Name)\n\t\t\t\t\t\tassert.Equals(t, _dbp.Claims, prov.Claims)\n\t\t\t\t\t\tassert.Equals(t, _dbp.X509Template, prov.X509Template)\n\t\t\t\t\t\tassert.Equals(t, _dbp.SSHTemplate, prov.SshTemplate)\n\t\t\t\t\t\tassert.Equals(t, _dbp.Webhooks, linkedcaWebhooksToDB(prov.Webhooks))\n\n\t\t\t\t\t\tretDetailsBytes, err := json.Marshal(prov.Details.GetData())\n\t\t\t\t\t\tassert.FatalError(t, err)\n\t\t\t\t\t\tassert.Equals(t, retDetailsBytes, _dbp.Details)\n\n\t\t\t\t\t\tassert.True(t, _dbp.DeletedAt.IsZero())\n\t\t\t\t\t\tassert.True(t, _dbp.CreatedAt.Before(time.Now()))\n\t\t\t\t\t\tassert.True(t, _dbp.CreatedAt.After(time.Now().Add(-time.Minute)))\n\n\t\t\t\t\t\treturn nu, true, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\ttc := run(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\td := DB{db: tc.db, authorityID: admin.DefaultAuthorityID}\n\t\t\tif err := d.UpdateProvisioner(context.Background(), tc.prov); err != nil {\n\t\t\t\tvar ae *admin.Error\n\t\t\t\tif errors.As(err, &ae) {\n\t\t\t\t\tif assert.NotNil(t, tc.adminErr) {\n\t\t\t\t\t\tassert.Equals(t, ae.Type, tc.adminErr.Type)\n\t\t\t\t\t\tassert.Equals(t, ae.Detail, tc.adminErr.Detail)\n\t\t\t\t\t\tassert.Equals(t, ae.Status, tc.adminErr.Status)\n\t\t\t\t\t\tassert.Equals(t, ae.Err.Error(), tc.adminErr.Err.Error())\n\t\t\t\t\t\tassert.Equals(t, ae.Detail, tc.adminErr.Detail)\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.err.Error())\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_linkedcaWebhooksToDB(t *testing.T) {\n\ttype test struct {\n\t\tin   []*linkedca.Webhook\n\t\twant []dbWebhook\n\t}\n\tvar tests = map[string]test{\n\t\t\"nil\": {\n\t\t\tin:   nil,\n\t\t\twant: nil,\n\t\t},\n\t\t\"zero\": {\n\t\t\tin:   []*linkedca.Webhook{},\n\t\t\twant: nil,\n\t\t},\n\t\t\"bearer\": {\n\t\t\tin: []*linkedca.Webhook{\n\t\t\t\t{\n\t\t\t\t\tName:   \"bearer\",\n\t\t\t\t\tUrl:    \"https://example.com\",\n\t\t\t\t\tKind:   linkedca.Webhook_ENRICHING,\n\t\t\t\t\tSecret: \"secret\",\n\t\t\t\t\tAuth: &linkedca.Webhook_BearerToken{\n\t\t\t\t\t\tBearerToken: &linkedca.BearerToken{\n\t\t\t\t\t\t\tBearerToken: \"token\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tDisableTlsClientAuth: true,\n\t\t\t\t\tCertType:             linkedca.Webhook_X509,\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: []dbWebhook{\n\t\t\t\t{\n\t\t\t\t\tName:                 \"bearer\",\n\t\t\t\t\tURL:                  \"https://example.com\",\n\t\t\t\t\tKind:                 \"ENRICHING\",\n\t\t\t\t\tSecret:               \"secret\",\n\t\t\t\t\tBearerToken:          \"token\",\n\t\t\t\t\tDisableTLSClientAuth: true,\n\t\t\t\t\tCertType:             linkedca.Webhook_X509.String(),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t\"basic\": {\n\t\t\tin: []*linkedca.Webhook{\n\t\t\t\t{\n\t\t\t\t\tName:   \"basic\",\n\t\t\t\t\tUrl:    \"https://example.com\",\n\t\t\t\t\tKind:   linkedca.Webhook_ENRICHING,\n\t\t\t\t\tSecret: \"secret\",\n\t\t\t\t\tAuth: &linkedca.Webhook_BasicAuth{\n\t\t\t\t\t\tBasicAuth: &linkedca.BasicAuth{\n\t\t\t\t\t\t\tUsername: \"user\",\n\t\t\t\t\t\t\tPassword: \"pass\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: []dbWebhook{\n\t\t\t\t{\n\t\t\t\t\tName:   \"basic\",\n\t\t\t\t\tURL:    \"https://example.com\",\n\t\t\t\t\tKind:   \"ENRICHING\",\n\t\t\t\t\tSecret: \"secret\",\n\t\t\t\t\tBasicAuth: &dbBasicAuth{\n\t\t\t\t\t\tUsername: \"user\",\n\t\t\t\t\t\tPassword: \"pass\",\n\t\t\t\t\t},\n\t\t\t\t\tCertType: linkedca.Webhook_ALL.String(),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\tfor name, tc := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tgot := linkedcaWebhooksToDB(tc.in)\n\t\t\tassert.Equals(t, tc.want, got)\n\t\t})\n\t}\n}\n\nfunc Test_dbWebhooksToLinkedca(t *testing.T) {\n\ttype test struct {\n\t\tin   []dbWebhook\n\t\twant []*linkedca.Webhook\n\t}\n\tvar tests = map[string]test{\n\t\t\"nil\": {\n\t\t\tin:   nil,\n\t\t\twant: nil,\n\t\t},\n\t\t\"zero\": {\n\t\t\tin:   []dbWebhook{},\n\t\t\twant: nil,\n\t\t},\n\t\t\"bearer\": {\n\t\t\tin: []dbWebhook{\n\t\t\t\t{\n\t\t\t\t\tName:                 \"bearer\",\n\t\t\t\t\tID:                   \"69350cb6-6c31-4b5e-bf25-affd5053427d\",\n\t\t\t\t\tURL:                  \"https://example.com\",\n\t\t\t\t\tKind:                 \"ENRICHING\",\n\t\t\t\t\tSecret:               \"secret\",\n\t\t\t\t\tBearerToken:          \"token\",\n\t\t\t\t\tDisableTLSClientAuth: true,\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: []*linkedca.Webhook{\n\t\t\t\t{\n\t\t\t\t\tName:   \"bearer\",\n\t\t\t\t\tId:     \"69350cb6-6c31-4b5e-bf25-affd5053427d\",\n\t\t\t\t\tUrl:    \"https://example.com\",\n\t\t\t\t\tKind:   linkedca.Webhook_ENRICHING,\n\t\t\t\t\tSecret: \"secret\",\n\t\t\t\t\tAuth: &linkedca.Webhook_BearerToken{\n\t\t\t\t\t\tBearerToken: &linkedca.BearerToken{\n\t\t\t\t\t\t\tBearerToken: \"token\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tDisableTlsClientAuth: true,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t\"basic\": {\n\t\t\tin: []dbWebhook{\n\t\t\t\t{\n\t\t\t\t\tName:   \"basic\",\n\t\t\t\t\tID:     \"69350cb6-6c31-4b5e-bf25-affd5053427d\",\n\t\t\t\t\tURL:    \"https://example.com\",\n\t\t\t\t\tKind:   \"ENRICHING\",\n\t\t\t\t\tSecret: \"secret\",\n\t\t\t\t\tBasicAuth: &dbBasicAuth{\n\t\t\t\t\t\tUsername: \"user\",\n\t\t\t\t\t\tPassword: \"pass\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: []*linkedca.Webhook{\n\t\t\t\t{\n\t\t\t\t\tName:   \"basic\",\n\t\t\t\t\tId:     \"69350cb6-6c31-4b5e-bf25-affd5053427d\",\n\t\t\t\t\tUrl:    \"https://example.com\",\n\t\t\t\t\tKind:   linkedca.Webhook_ENRICHING,\n\t\t\t\t\tSecret: \"secret\",\n\t\t\t\t\tAuth: &linkedca.Webhook_BasicAuth{\n\t\t\t\t\t\tBasicAuth: &linkedca.BasicAuth{\n\t\t\t\t\t\t\tUsername: \"user\",\n\t\t\t\t\t\t\tPassword: \"pass\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\tfor name, tc := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tgot := dbWebhooksToLinkedca(tc.in)\n\t\t\tassert.Equals(t, tc.want, got)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "authority/admin/db.go",
    "content": "package admin\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\n\t\"github.com/pkg/errors\"\n\t\"github.com/smallstep/linkedca\"\n)\n\nconst (\n\t// DefaultAuthorityID is the default AuthorityID. This will be the ID\n\t// of the first Authority created, as well as the default AuthorityID\n\t// if one is not specified in the configuration.\n\tDefaultAuthorityID = \"00000000-0000-0000-0000-000000000000\"\n)\n\n// ErrNotFound is an error that should be used by the authority.DB interface to\n// indicate that an entity does not exist.\nvar ErrNotFound = errors.New(\"not found\")\n\n// UnmarshalProvisionerDetails unmarshals details type to the specific provisioner details.\nfunc UnmarshalProvisionerDetails(typ linkedca.Provisioner_Type, data []byte) (*linkedca.ProvisionerDetails, error) {\n\tvar v linkedca.ProvisionerDetails\n\tswitch typ {\n\tcase linkedca.Provisioner_JWK:\n\t\tv.Data = new(linkedca.ProvisionerDetails_JWK)\n\tcase linkedca.Provisioner_OIDC:\n\t\tv.Data = new(linkedca.ProvisionerDetails_OIDC)\n\tcase linkedca.Provisioner_GCP:\n\t\tv.Data = new(linkedca.ProvisionerDetails_GCP)\n\tcase linkedca.Provisioner_AWS:\n\t\tv.Data = new(linkedca.ProvisionerDetails_AWS)\n\tcase linkedca.Provisioner_AZURE:\n\t\tv.Data = new(linkedca.ProvisionerDetails_Azure)\n\tcase linkedca.Provisioner_ACME:\n\t\tv.Data = new(linkedca.ProvisionerDetails_ACME)\n\tcase linkedca.Provisioner_X5C:\n\t\tv.Data = new(linkedca.ProvisionerDetails_X5C)\n\tcase linkedca.Provisioner_K8SSA:\n\t\tv.Data = new(linkedca.ProvisionerDetails_K8SSA)\n\tcase linkedca.Provisioner_SSHPOP:\n\t\tv.Data = new(linkedca.ProvisionerDetails_SSHPOP)\n\tcase linkedca.Provisioner_SCEP:\n\t\tv.Data = new(linkedca.ProvisionerDetails_SCEP)\n\tcase linkedca.Provisioner_NEBULA:\n\t\tv.Data = new(linkedca.ProvisionerDetails_Nebula)\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported provisioner type %s\", typ)\n\t}\n\n\tif err := json.Unmarshal(data, v.Data); err != nil {\n\t\treturn nil, err\n\t}\n\treturn &linkedca.ProvisionerDetails{Data: v.Data}, nil\n}\n\n// DB is the DB interface expected by the step-ca Admin API.\ntype DB interface {\n\tCreateProvisioner(ctx context.Context, prov *linkedca.Provisioner) error\n\tGetProvisioner(ctx context.Context, id string) (*linkedca.Provisioner, error)\n\tGetProvisioners(ctx context.Context) ([]*linkedca.Provisioner, error)\n\tUpdateProvisioner(ctx context.Context, prov *linkedca.Provisioner) error\n\tDeleteProvisioner(ctx context.Context, id string) error\n\n\tCreateAdmin(ctx context.Context, admin *linkedca.Admin) error\n\tGetAdmin(ctx context.Context, id string) (*linkedca.Admin, error)\n\tGetAdmins(ctx context.Context) ([]*linkedca.Admin, error)\n\tUpdateAdmin(ctx context.Context, admin *linkedca.Admin) error\n\tDeleteAdmin(ctx context.Context, id string) error\n\n\tCreateAuthorityPolicy(ctx context.Context, policy *linkedca.Policy) error\n\tGetAuthorityPolicy(ctx context.Context) (*linkedca.Policy, error)\n\tUpdateAuthorityPolicy(ctx context.Context, policy *linkedca.Policy) error\n\tDeleteAuthorityPolicy(ctx context.Context) error\n}\n\ntype dbKey struct{}\n\n// NewContext adds the given admin database to the context.\nfunc NewContext(ctx context.Context, db DB) context.Context {\n\treturn context.WithValue(ctx, dbKey{}, db)\n}\n\n// FromContext returns the current admin database from the given context.\nfunc FromContext(ctx context.Context) (db DB, ok bool) {\n\tdb, ok = ctx.Value(dbKey{}).(DB)\n\treturn\n}\n\n// MustFromContext returns the current admin database from the given context. It\n// will panic if it's not in the context.\nfunc MustFromContext(ctx context.Context) DB {\n\tvar (\n\t\tdb DB\n\t\tok bool\n\t)\n\tif db, ok = FromContext(ctx); !ok {\n\t\tpanic(\"admin database is not in the context\")\n\t}\n\treturn db\n}\n\n// MockDB is an implementation of the DB interface that should only be used as\n// a mock in tests.\ntype MockDB struct {\n\tMockCreateProvisioner func(ctx context.Context, prov *linkedca.Provisioner) error\n\tMockGetProvisioner    func(ctx context.Context, id string) (*linkedca.Provisioner, error)\n\tMockGetProvisioners   func(ctx context.Context) ([]*linkedca.Provisioner, error)\n\tMockUpdateProvisioner func(ctx context.Context, prov *linkedca.Provisioner) error\n\tMockDeleteProvisioner func(ctx context.Context, id string) error\n\n\tMockCreateAdmin func(ctx context.Context, adm *linkedca.Admin) error\n\tMockGetAdmin    func(ctx context.Context, id string) (*linkedca.Admin, error)\n\tMockGetAdmins   func(ctx context.Context) ([]*linkedca.Admin, error)\n\tMockUpdateAdmin func(ctx context.Context, adm *linkedca.Admin) error\n\tMockDeleteAdmin func(ctx context.Context, id string) error\n\n\tMockCreateAuthorityPolicy func(ctx context.Context, policy *linkedca.Policy) error\n\tMockGetAuthorityPolicy    func(ctx context.Context) (*linkedca.Policy, error)\n\tMockUpdateAuthorityPolicy func(ctx context.Context, policy *linkedca.Policy) error\n\tMockDeleteAuthorityPolicy func(ctx context.Context) error\n\n\tMockError error\n\tMockRet1  interface{}\n}\n\n// CreateProvisioner mock.\nfunc (m *MockDB) CreateProvisioner(ctx context.Context, prov *linkedca.Provisioner) error {\n\tif m.MockCreateProvisioner != nil {\n\t\treturn m.MockCreateProvisioner(ctx, prov)\n\t} else if m.MockError != nil {\n\t\treturn m.MockError\n\t}\n\treturn m.MockError\n}\n\n// GetProvisioner mock.\nfunc (m *MockDB) GetProvisioner(ctx context.Context, id string) (*linkedca.Provisioner, error) {\n\tif m.MockGetProvisioner != nil {\n\t\treturn m.MockGetProvisioner(ctx, id)\n\t} else if m.MockError != nil {\n\t\treturn nil, m.MockError\n\t}\n\treturn m.MockRet1.(*linkedca.Provisioner), m.MockError\n}\n\n// GetProvisioners mock\nfunc (m *MockDB) GetProvisioners(ctx context.Context) ([]*linkedca.Provisioner, error) {\n\tif m.MockGetProvisioners != nil {\n\t\treturn m.MockGetProvisioners(ctx)\n\t} else if m.MockError != nil {\n\t\treturn nil, m.MockError\n\t}\n\treturn m.MockRet1.([]*linkedca.Provisioner), m.MockError\n}\n\n// UpdateProvisioner mock\nfunc (m *MockDB) UpdateProvisioner(ctx context.Context, prov *linkedca.Provisioner) error {\n\tif m.MockUpdateProvisioner != nil {\n\t\treturn m.MockUpdateProvisioner(ctx, prov)\n\t}\n\treturn m.MockError\n}\n\n// DeleteProvisioner mock\nfunc (m *MockDB) DeleteProvisioner(ctx context.Context, id string) error {\n\tif m.MockDeleteProvisioner != nil {\n\t\treturn m.MockDeleteProvisioner(ctx, id)\n\t}\n\treturn m.MockError\n}\n\n// CreateAdmin mock\nfunc (m *MockDB) CreateAdmin(ctx context.Context, admin *linkedca.Admin) error {\n\tif m.MockCreateAdmin != nil {\n\t\treturn m.MockCreateAdmin(ctx, admin)\n\t}\n\treturn m.MockError\n}\n\n// GetAdmin mock.\nfunc (m *MockDB) GetAdmin(ctx context.Context, id string) (*linkedca.Admin, error) {\n\tif m.MockGetAdmin != nil {\n\t\treturn m.MockGetAdmin(ctx, id)\n\t} else if m.MockError != nil {\n\t\treturn nil, m.MockError\n\t}\n\treturn m.MockRet1.(*linkedca.Admin), m.MockError\n}\n\n// GetAdmins mock\nfunc (m *MockDB) GetAdmins(ctx context.Context) ([]*linkedca.Admin, error) {\n\tif m.MockGetAdmins != nil {\n\t\treturn m.MockGetAdmins(ctx)\n\t} else if m.MockError != nil {\n\t\treturn nil, m.MockError\n\t}\n\treturn m.MockRet1.([]*linkedca.Admin), m.MockError\n}\n\n// UpdateAdmin mock\nfunc (m *MockDB) UpdateAdmin(ctx context.Context, adm *linkedca.Admin) error {\n\tif m.MockUpdateAdmin != nil {\n\t\treturn m.MockUpdateAdmin(ctx, adm)\n\t}\n\treturn m.MockError\n}\n\n// DeleteAdmin mock\nfunc (m *MockDB) DeleteAdmin(ctx context.Context, id string) error {\n\tif m.MockDeleteAdmin != nil {\n\t\treturn m.MockDeleteAdmin(ctx, id)\n\t}\n\treturn m.MockError\n}\n\n// CreateAuthorityPolicy mock\nfunc (m *MockDB) CreateAuthorityPolicy(ctx context.Context, policy *linkedca.Policy) error {\n\tif m.MockCreateAuthorityPolicy != nil {\n\t\treturn m.MockCreateAuthorityPolicy(ctx, policy)\n\t}\n\treturn m.MockError\n}\n\n// GetAuthorityPolicy mock\nfunc (m *MockDB) GetAuthorityPolicy(ctx context.Context) (*linkedca.Policy, error) {\n\tif m.MockGetAuthorityPolicy != nil {\n\t\treturn m.MockGetAuthorityPolicy(ctx)\n\t}\n\treturn m.MockRet1.(*linkedca.Policy), m.MockError\n}\n\n// UpdateAuthorityPolicy mock\nfunc (m *MockDB) UpdateAuthorityPolicy(ctx context.Context, policy *linkedca.Policy) error {\n\tif m.MockUpdateAuthorityPolicy != nil {\n\t\treturn m.MockUpdateAuthorityPolicy(ctx, policy)\n\t}\n\treturn m.MockError\n}\n\n// DeleteAuthorityPolicy mock\nfunc (m *MockDB) DeleteAuthorityPolicy(ctx context.Context) error {\n\tif m.MockDeleteAuthorityPolicy != nil {\n\t\treturn m.MockDeleteAuthorityPolicy(ctx)\n\t}\n\treturn m.MockError\n}\n"
  },
  {
    "path": "authority/admin/errors.go",
    "content": "package admin\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\n\t\"github.com/pkg/errors\"\n\t\"github.com/smallstep/certificates/api/render\"\n)\n\n// ProblemType is the type of the Admin problem.\ntype ProblemType int\n\nconst (\n\t// ErrorNotFoundType resource not found.\n\tErrorNotFoundType ProblemType = iota\n\t// ErrorAuthorityMismatchType resource Authority ID does not match the\n\t// context Authority ID.\n\tErrorAuthorityMismatchType\n\t// ErrorDeletedType resource has been deleted.\n\tErrorDeletedType\n\t// ErrorBadRequestType bad request.\n\tErrorBadRequestType\n\t// ErrorNotImplementedType not implemented.\n\tErrorNotImplementedType\n\t// ErrorUnauthorizedType unauthorized.\n\tErrorUnauthorizedType\n\t// ErrorServerInternalType internal server error.\n\tErrorServerInternalType\n\t// ErrorConflictType conflict.\n\tErrorConflictType\n)\n\n// String returns the string representation of the admin problem type,\n// fulfilling the Stringer interface.\nfunc (ap ProblemType) String() string {\n\tswitch ap {\n\tcase ErrorNotFoundType:\n\t\treturn \"notFound\"\n\tcase ErrorAuthorityMismatchType:\n\t\treturn \"authorityMismatch\"\n\tcase ErrorDeletedType:\n\t\treturn \"deleted\"\n\tcase ErrorBadRequestType:\n\t\treturn \"badRequest\"\n\tcase ErrorNotImplementedType:\n\t\treturn \"notImplemented\"\n\tcase ErrorUnauthorizedType:\n\t\treturn \"unauthorized\"\n\tcase ErrorServerInternalType:\n\t\treturn \"internalServerError\"\n\tcase ErrorConflictType:\n\t\treturn \"conflict\"\n\tdefault:\n\t\treturn fmt.Sprintf(\"unsupported error type '%d'\", int(ap))\n\t}\n}\n\ntype errorMetadata struct {\n\tdetails string\n\tstatus  int\n\ttyp     string\n\tString  string\n}\n\nvar (\n\terrorServerInternalMetadata = errorMetadata{\n\t\ttyp:     ErrorServerInternalType.String(),\n\t\tdetails: \"the server experienced an internal error\",\n\t\tstatus:  http.StatusInternalServerError,\n\t}\n\terrorMap = map[ProblemType]errorMetadata{\n\t\tErrorNotFoundType: {\n\t\t\ttyp:     ErrorNotFoundType.String(),\n\t\t\tdetails: \"resource not found\",\n\t\t\tstatus:  http.StatusNotFound,\n\t\t},\n\t\tErrorAuthorityMismatchType: {\n\t\t\ttyp:     ErrorAuthorityMismatchType.String(),\n\t\t\tdetails: \"resource not owned by authority\",\n\t\t\tstatus:  http.StatusUnauthorized,\n\t\t},\n\t\tErrorDeletedType: {\n\t\t\ttyp:     ErrorDeletedType.String(),\n\t\t\tdetails: \"resource is deleted\",\n\t\t\tstatus:  http.StatusNotFound,\n\t\t},\n\t\tErrorNotImplementedType: {\n\t\t\ttyp:     ErrorNotImplementedType.String(),\n\t\t\tdetails: \"not implemented\",\n\t\t\tstatus:  http.StatusNotImplemented,\n\t\t},\n\t\tErrorBadRequestType: {\n\t\t\ttyp:     ErrorBadRequestType.String(),\n\t\t\tdetails: \"bad request\",\n\t\t\tstatus:  http.StatusBadRequest,\n\t\t},\n\t\tErrorUnauthorizedType: {\n\t\t\ttyp:     ErrorUnauthorizedType.String(),\n\t\t\tdetails: \"unauthorized\",\n\t\t\tstatus:  http.StatusUnauthorized,\n\t\t},\n\t\tErrorServerInternalType: errorServerInternalMetadata,\n\t\tErrorConflictType: {\n\t\t\ttyp:     ErrorConflictType.String(),\n\t\t\tdetails: \"conflict\",\n\t\t\tstatus:  http.StatusConflict,\n\t\t},\n\t}\n)\n\n// Error represents an Admin error\ntype Error struct {\n\tType    string `json:\"type\"`\n\tDetail  string `json:\"detail\"`\n\tMessage string `json:\"message\"`\n\tErr     error  `json:\"-\"`\n\tStatus  int    `json:\"-\"`\n}\n\n// IsType returns true if the error type matches the input type.\nfunc (e *Error) IsType(pt ProblemType) bool {\n\treturn pt.String() == e.Type\n}\n\n// NewError creates a new Error type.\nfunc NewError(pt ProblemType, msg string, args ...interface{}) *Error {\n\treturn newError(pt, errors.Errorf(msg, args...))\n}\n\nfunc newError(pt ProblemType, err error) *Error {\n\tmeta, ok := errorMap[pt]\n\tif !ok {\n\t\tmeta = errorServerInternalMetadata\n\t\treturn &Error{\n\t\t\tType:   meta.typ,\n\t\t\tDetail: meta.details,\n\t\t\tStatus: meta.status,\n\t\t\tErr:    err,\n\t\t}\n\t}\n\n\treturn &Error{\n\t\tType:   meta.typ,\n\t\tDetail: meta.details,\n\t\tStatus: meta.status,\n\t\tErr:    err,\n\t}\n}\n\n// NewErrorISE creates a new ErrorServerInternalType Error.\nfunc NewErrorISE(msg string, args ...interface{}) *Error {\n\treturn NewError(ErrorServerInternalType, msg, args...)\n}\n\n// WrapError attempts to wrap the internal error.\nfunc WrapError(typ ProblemType, err error, msg string, args ...interface{}) *Error {\n\tvar ee *Error\n\tswitch {\n\tcase err == nil:\n\t\treturn nil\n\tcase errors.As(err, &ee):\n\t\tif ee.Err == nil {\n\t\t\tee.Err = errors.Errorf(msg+\"; \"+ee.Detail, args...)\n\t\t} else {\n\t\t\tee.Err = errors.Wrapf(ee.Err, msg, args...)\n\t\t}\n\t\treturn ee\n\tdefault:\n\t\treturn newError(typ, errors.Wrapf(err, msg, args...))\n\t}\n}\n\n// WrapErrorISE shortcut to wrap an internal server error type.\nfunc WrapErrorISE(err error, msg string, args ...interface{}) *Error {\n\treturn WrapError(ErrorServerInternalType, err, msg, args...)\n}\n\n// StatusCode returns the status code and implements the StatusCoder interface.\nfunc (e *Error) StatusCode() int {\n\treturn e.Status\n}\n\n// Error allows AError to implement the error interface.\nfunc (e *Error) Error() string {\n\treturn e.Err.Error()\n}\n\n// Cause returns the internal error and implements the Causer interface.\nfunc (e *Error) Cause() error {\n\tif e.Err == nil {\n\t\treturn errors.New(e.Detail)\n\t}\n\treturn e.Err\n}\n\n// ToLog implements the EnableLogger interface.\nfunc (e *Error) ToLog() (interface{}, error) {\n\tb, err := json.Marshal(e)\n\tif err != nil {\n\t\treturn nil, WrapErrorISE(err, \"error marshaling authority.Error for logging\")\n\t}\n\treturn string(b), nil\n}\n\n// Render implements render.RenderableError for Error.\nfunc (e *Error) Render(w http.ResponseWriter, r *http.Request) {\n\te.Message = e.Err.Error()\n\n\trender.JSONStatus(w, r, e, e.StatusCode())\n}\n"
  },
  {
    "path": "authority/administrator/collection.go",
    "content": "package administrator\n\nimport (\n\t\"sort\"\n\t\"sync\"\n\n\t\"github.com/pkg/errors\"\n\t\"github.com/smallstep/certificates/authority/admin\"\n\t\"github.com/smallstep/certificates/authority/provisioner\"\n\t\"github.com/smallstep/linkedca\"\n)\n\n// DefaultAdminLimit is the default limit for listing provisioners.\nconst DefaultAdminLimit = 20\n\n// DefaultAdminMax is the maximum limit for listing provisioners.\nconst DefaultAdminMax = 100\n\ntype adminSlice []*linkedca.Admin\n\nfunc (p adminSlice) Len() int           { return len(p) }\nfunc (p adminSlice) Less(i, j int) bool { return p[i].Id < p[j].Id }\nfunc (p adminSlice) Swap(i, j int)      { p[i], p[j] = p[j], p[i] }\n\n// Collection is a memory map of admins.\ntype Collection struct {\n\tbyID                    *sync.Map\n\tbySubProv               *sync.Map\n\tbyProv                  *sync.Map\n\tsorted                  adminSlice\n\tprovisioners            *provisioner.Collection\n\tsuperCount              int\n\tsuperCountByProvisioner map[string]int\n}\n\n// NewCollection initializes a collection of provisioners. The given list of\n// audiences are the audiences used by the JWT provisioner.\nfunc NewCollection(provisioners *provisioner.Collection) *Collection {\n\treturn &Collection{\n\t\tbyID:                    new(sync.Map),\n\t\tbyProv:                  new(sync.Map),\n\t\tbySubProv:               new(sync.Map),\n\t\tsuperCountByProvisioner: map[string]int{},\n\t\tprovisioners:            provisioners,\n\t}\n}\n\n// LoadByID a admin by the ID.\nfunc (c *Collection) LoadByID(id string) (*linkedca.Admin, bool) {\n\treturn loadAdmin(c.byID, id)\n}\n\ntype subProv struct {\n\tsubject     string\n\tprovisioner string\n}\n\nfunc newSubProv(subject, prov string) subProv {\n\treturn subProv{subject, prov}\n}\n\n// LoadBySubProv loads an admin by subject and provisioner name.\nfunc (c *Collection) LoadBySubProv(sub, provName string) (*linkedca.Admin, bool) {\n\treturn loadAdmin(c.bySubProv, newSubProv(sub, provName))\n}\n\n// LoadByProvisioner loads admins by provisioner name.\nfunc (c *Collection) LoadByProvisioner(provName string) ([]*linkedca.Admin, bool) {\n\tval, ok := c.byProv.Load(provName)\n\tif !ok {\n\t\treturn nil, false\n\t}\n\tadmins, ok := val.([]*linkedca.Admin)\n\tif !ok {\n\t\treturn nil, false\n\t}\n\treturn admins, true\n}\n\n// Store adds an admin to the collection and enforces the uniqueness of\n// admin IDs and admin subject <-> provisioner name combos.\nfunc (c *Collection) Store(adm *linkedca.Admin, prov provisioner.Interface) error {\n\t// Input validation.\n\tif adm.ProvisionerId != prov.GetID() {\n\t\treturn admin.NewErrorISE(\"admin.provisionerId does not match provisioner argument\")\n\t}\n\n\t// Store admin always in byID. ID must be unique.\n\tif _, loaded := c.byID.LoadOrStore(adm.Id, adm); loaded {\n\t\treturn errors.New(\"cannot add multiple admins with the same id\")\n\t}\n\n\tprovName := prov.GetName()\n\t// Store admin always in bySubProv. Subject <-> ProvisionerName must be unique.\n\tif _, loaded := c.bySubProv.LoadOrStore(newSubProv(adm.Subject, provName), adm); loaded {\n\t\tc.byID.Delete(adm.Id)\n\t\treturn errors.New(\"cannot add multiple admins with the same subject and provisioner\")\n\t}\n\n\tvar isSuper = (adm.Type == linkedca.Admin_SUPER_ADMIN)\n\tif admins, ok := c.LoadByProvisioner(provName); ok {\n\t\tc.byProv.Store(provName, append(admins, adm))\n\t\tif isSuper {\n\t\t\tc.superCountByProvisioner[provName]++\n\t\t}\n\t} else {\n\t\tc.byProv.Store(provName, []*linkedca.Admin{adm})\n\t\tif isSuper {\n\t\t\tc.superCountByProvisioner[provName] = 1\n\t\t}\n\t}\n\tif isSuper {\n\t\tc.superCount++\n\t}\n\n\tc.sorted = append(c.sorted, adm)\n\tsort.Sort(c.sorted)\n\n\treturn nil\n}\n\n// Remove deletes an admin from all associated collections and lists.\nfunc (c *Collection) Remove(id string) error {\n\tadm, ok := c.LoadByID(id)\n\tif !ok {\n\t\treturn admin.NewError(admin.ErrorNotFoundType, \"admin %s not found\", id)\n\t}\n\tif adm.Type == linkedca.Admin_SUPER_ADMIN && c.SuperCount() == 1 {\n\t\treturn admin.NewError(admin.ErrorBadRequestType, \"cannot remove the last super admin\")\n\t}\n\tprov, ok := c.provisioners.Load(adm.ProvisionerId)\n\tif !ok {\n\t\treturn admin.NewError(admin.ErrorNotFoundType,\n\t\t\t\"provisioner %s for admin %s not found\", adm.ProvisionerId, id)\n\t}\n\tprovName := prov.GetName()\n\tadminsByProv, ok := c.LoadByProvisioner(provName)\n\tif !ok {\n\t\treturn admin.NewError(admin.ErrorNotFoundType,\n\t\t\t\"admins not found for provisioner %s\", provName)\n\t}\n\n\t// Find index in sorted list.\n\tsortedIndex := sort.Search(c.sorted.Len(), func(i int) bool { return c.sorted[i].Id >= adm.Id })\n\tif c.sorted[sortedIndex].Id != adm.Id {\n\t\treturn admin.NewError(admin.ErrorNotFoundType,\n\t\t\t\"admin %s not found in sorted list\", adm.Id)\n\t}\n\n\tvar found bool\n\tfor i, a := range adminsByProv {\n\t\tif a.Id == adm.Id {\n\t\t\t// Remove admin from list. https://stackoverflow.com/questions/37334119/how-to-delete-an-element-from-a-slice-in-golang\n\t\t\t// Order does not matter.\n\t\t\tadminsByProv[i] = adminsByProv[len(adminsByProv)-1]\n\t\t\tc.byProv.Store(provName, adminsByProv[:len(adminsByProv)-1])\n\t\t\tfound = true\n\t\t}\n\t}\n\tif !found {\n\t\treturn admin.NewError(admin.ErrorNotFoundType,\n\t\t\t\"admin %s not found in adminsByProvisioner list\", adm.Id)\n\t}\n\n\t// Remove index in sorted list\n\tcopy(c.sorted[sortedIndex:], c.sorted[sortedIndex+1:]) // Shift a[i+1:] left one index.\n\tc.sorted[len(c.sorted)-1] = nil                        // Erase last element (write zero value).\n\tc.sorted = c.sorted[:len(c.sorted)-1]                  // Truncate slice.\n\n\tc.byID.Delete(adm.Id)\n\tc.bySubProv.Delete(newSubProv(adm.Subject, provName))\n\n\tif adm.Type == linkedca.Admin_SUPER_ADMIN {\n\t\tc.superCount--\n\t\tc.superCountByProvisioner[provName]--\n\t}\n\treturn nil\n}\n\n// Update updates the given admin in all related lists and collections.\nfunc (c *Collection) Update(id string, nu *linkedca.Admin) (*linkedca.Admin, error) {\n\tadm, ok := c.LoadByID(id)\n\tif !ok {\n\t\treturn nil, admin.NewError(admin.ErrorNotFoundType, \"admin %s not found\", adm.Id)\n\t}\n\tif adm.Type == nu.Type {\n\t\treturn adm, nil\n\t}\n\tif adm.Type == linkedca.Admin_SUPER_ADMIN && c.SuperCount() == 1 {\n\t\treturn nil, admin.NewError(admin.ErrorBadRequestType, \"cannot change role of last super admin\")\n\t}\n\n\tadm.Type = nu.Type\n\treturn adm, nil\n}\n\n// SuperCount returns the total number of admins.\nfunc (c *Collection) SuperCount() int {\n\treturn c.superCount\n}\n\n// SuperCountByProvisioner returns the total number of admins.\nfunc (c *Collection) SuperCountByProvisioner(provName string) int {\n\tif cnt, ok := c.superCountByProvisioner[provName]; ok {\n\t\treturn cnt\n\t}\n\treturn 0\n}\n\n// Find implements pagination on a list of sorted admins.\nfunc (c *Collection) Find(cursor string, limit int) ([]*linkedca.Admin, string) {\n\tswitch {\n\tcase limit <= 0:\n\t\tlimit = DefaultAdminLimit\n\tcase limit > DefaultAdminMax:\n\t\tlimit = DefaultAdminMax\n\t}\n\n\tn := c.sorted.Len()\n\ti := sort.Search(n, func(i int) bool { return c.sorted[i].Id >= cursor })\n\n\tslice := []*linkedca.Admin{}\n\tfor ; i < n && len(slice) < limit; i++ {\n\t\tslice = append(slice, c.sorted[i])\n\t}\n\n\tif i < n {\n\t\treturn slice, c.sorted[i].Id\n\t}\n\treturn slice, \"\"\n}\n\nfunc loadAdmin(m *sync.Map, key interface{}) (*linkedca.Admin, bool) {\n\tval, ok := m.Load(key)\n\tif !ok {\n\t\treturn nil, false\n\t}\n\tadm, ok := val.(*linkedca.Admin)\n\tif !ok {\n\t\treturn nil, false\n\t}\n\treturn adm, true\n}\n"
  },
  {
    "path": "authority/admins.go",
    "content": "package authority\n\nimport (\n\t\"context\"\n\n\t\"github.com/smallstep/certificates/authority/admin\"\n\t\"github.com/smallstep/certificates/authority/provisioner\"\n\t\"github.com/smallstep/linkedca\"\n)\n\n// LoadAdminByID returns an *linkedca.Admin with the given ID.\nfunc (a *Authority) LoadAdminByID(id string) (*linkedca.Admin, bool) {\n\ta.adminMutex.RLock()\n\tdefer a.adminMutex.RUnlock()\n\treturn a.admins.LoadByID(id)\n}\n\n// LoadAdminBySubProv returns an *linkedca.Admin with the given ID.\nfunc (a *Authority) LoadAdminBySubProv(subject, prov string) (*linkedca.Admin, bool) {\n\ta.adminMutex.RLock()\n\tdefer a.adminMutex.RUnlock()\n\treturn a.admins.LoadBySubProv(subject, prov)\n}\n\n// GetAdmins returns a map listing each provisioner and the JWK Key Set\n// with their public keys.\nfunc (a *Authority) GetAdmins(cursor string, limit int) ([]*linkedca.Admin, string, error) {\n\ta.adminMutex.RLock()\n\tdefer a.adminMutex.RUnlock()\n\tadmins, nextCursor := a.admins.Find(cursor, limit)\n\treturn admins, nextCursor, nil\n}\n\n// StoreAdmin stores an *linkedca.Admin to the authority.\nfunc (a *Authority) StoreAdmin(ctx context.Context, adm *linkedca.Admin, prov provisioner.Interface) error {\n\ta.adminMutex.Lock()\n\tdefer a.adminMutex.Unlock()\n\n\tif adm.ProvisionerId != prov.GetID() {\n\t\treturn admin.NewErrorISE(\"admin.provisionerId does not match provisioner argument\")\n\t}\n\n\tif _, ok := a.admins.LoadBySubProv(adm.Subject, prov.GetName()); ok {\n\t\treturn admin.NewError(admin.ErrorBadRequestType,\n\t\t\t\"admin with subject %s and provisioner %s already exists\", adm.Subject, prov.GetName())\n\t}\n\t// Store to database -- this will set the ID.\n\tif err := a.adminDB.CreateAdmin(ctx, adm); err != nil {\n\t\treturn admin.WrapErrorISE(err, \"error creating admin\")\n\t}\n\tif err := a.admins.Store(adm, prov); err != nil {\n\t\tif err := a.ReloadAdminResources(ctx); err != nil {\n\t\t\treturn admin.WrapErrorISE(err, \"error reloading admin resources on failed admin store\")\n\t\t}\n\t\treturn admin.WrapErrorISE(err, \"error storing admin in authority cache\")\n\t}\n\treturn nil\n}\n\n// UpdateAdmin stores an *linkedca.Admin to the authority.\nfunc (a *Authority) UpdateAdmin(ctx context.Context, id string, nu *linkedca.Admin) (*linkedca.Admin, error) {\n\ta.adminMutex.Lock()\n\tdefer a.adminMutex.Unlock()\n\tadm, err := a.admins.Update(id, nu)\n\tif err != nil {\n\t\treturn nil, admin.WrapErrorISE(err, \"error updating cached admin %s\", id)\n\t}\n\tif err := a.adminDB.UpdateAdmin(ctx, adm); err != nil {\n\t\tif err := a.ReloadAdminResources(ctx); err != nil {\n\t\t\treturn nil, admin.WrapErrorISE(err, \"error reloading admin resources on failed admin update\")\n\t\t}\n\t\treturn nil, admin.WrapErrorISE(err, \"error updating admin %s\", id)\n\t}\n\treturn adm, nil\n}\n\n// RemoveAdmin removes an *linkedca.Admin from the authority.\nfunc (a *Authority) RemoveAdmin(ctx context.Context, id string) error {\n\ta.adminMutex.Lock()\n\tdefer a.adminMutex.Unlock()\n\n\treturn a.removeAdmin(ctx, id)\n}\n\n// removeAdmin helper that assumes lock.\nfunc (a *Authority) removeAdmin(ctx context.Context, id string) error {\n\tif err := a.admins.Remove(id); err != nil {\n\t\treturn admin.WrapErrorISE(err, \"error removing admin %s from authority cache\", id)\n\t}\n\tif err := a.adminDB.DeleteAdmin(ctx, id); err != nil {\n\t\tif err := a.ReloadAdminResources(ctx); err != nil {\n\t\t\treturn admin.WrapErrorISE(err, \"error reloading admin resources on failed admin remove\")\n\t\t}\n\t\treturn admin.WrapErrorISE(err, \"error deleting admin %s\", id)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "authority/authority.go",
    "content": "package authority\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto\"\n\t\"crypto/rsa\"\n\t\"crypto/sha256\"\n\t\"crypto/x509\"\n\t\"encoding/hex\"\n\t\"fmt\"\n\t\"log\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n\t\"golang.org/x/crypto/ssh\"\n\n\t\"github.com/smallstep/linkedca\"\n\t\"go.step.sm/crypto/kms\"\n\tkmsapi \"go.step.sm/crypto/kms/apiv1\"\n\t\"go.step.sm/crypto/kms/sshagentkms\"\n\t\"go.step.sm/crypto/pemutil\"\n\n\t\"github.com/smallstep/certificates/authority/admin\"\n\tadminDBNosql \"github.com/smallstep/certificates/authority/admin/db/nosql\"\n\t\"github.com/smallstep/certificates/authority/administrator\"\n\t\"github.com/smallstep/certificates/authority/config\"\n\t\"github.com/smallstep/certificates/authority/internal/constraints\"\n\t\"github.com/smallstep/certificates/authority/policy\"\n\t\"github.com/smallstep/certificates/authority/provisioner\"\n\t\"github.com/smallstep/certificates/cas\"\n\tcasapi \"github.com/smallstep/certificates/cas/apiv1\"\n\t\"github.com/smallstep/certificates/db\"\n\t\"github.com/smallstep/certificates/internal/httptransport\"\n\t\"github.com/smallstep/certificates/scep\"\n\t\"github.com/smallstep/certificates/templates\"\n\t\"github.com/smallstep/nosql\"\n)\n\n// Authority implements the Certificate Authority internal interface.\ntype Authority struct {\n\tconfig        *config.Config\n\tkeyManager    kms.KeyManager\n\tprovisioners  *provisioner.Collection\n\tadmins        *administrator.Collection\n\tdb            db.AuthDB\n\tadminDB       admin.DB\n\ttemplates     *templates.Templates\n\tlinkedCAToken string\n\twrapTransport httptransport.Wrapper\n\twebhookClient provisioner.HTTPClient\n\thttpClient    provisioner.HTTPClient\n\n\t// X509 CA\n\tpassword              []byte\n\tissuerPassword        []byte\n\tx509CAService         cas.CertificateAuthorityService\n\trootX509Certs         []*x509.Certificate\n\trootX509CertPool      *x509.CertPool\n\tfederatedX509Certs    []*x509.Certificate\n\tintermediateX509Certs []*x509.Certificate\n\tcertificates          *sync.Map\n\tx509Enforcers         []provisioner.CertificateEnforcer\n\n\t// SCEP CA\n\tscepOptions    *scep.Options\n\tvalidateSCEP   bool\n\tscepAuthority  *scep.Authority\n\tscepKeyManager provisioner.SCEPKeyManager\n\n\t// SSH CA\n\tsshHostPassword         []byte\n\tsshUserPassword         []byte\n\tsshCAUserCertSignKey    ssh.Signer\n\tsshCAHostCertSignKey    ssh.Signer\n\tsshCAUserCerts          []ssh.PublicKey\n\tsshCAHostCerts          []ssh.PublicKey\n\tsshCAUserFederatedCerts []ssh.PublicKey\n\tsshCAHostFederatedCerts []ssh.PublicKey\n\n\t// CRL vars\n\tcrlTicker  *time.Ticker\n\tcrlStopper chan struct{}\n\tcrlMutex   sync.Mutex\n\n\t// If true, do not re-initialize\n\tinitOnce  bool\n\tstartTime time.Time\n\n\t// Custom functions\n\tsshBastionFunc        func(ctx context.Context, user, hostname string) (*config.Bastion, error)\n\tsshCheckHostFunc      func(ctx context.Context, principal string, tok string, roots []*x509.Certificate) (bool, error)\n\tsshGetHostsFunc       func(ctx context.Context, cert *x509.Certificate) ([]config.Host, error)\n\tgetIdentityFunc       provisioner.GetIdentityFunc\n\tauthorizeRenewFunc    provisioner.AuthorizeRenewFunc\n\tauthorizeSSHRenewFunc provisioner.AuthorizeSSHRenewFunc\n\n\t// Constraints and Policy engines\n\tconstraintsEngine *constraints.Engine\n\tpolicyEngine      *policy.Engine\n\n\tadminMutex sync.RWMutex\n\n\t// If true, do not initialize the authority\n\tskipInit bool\n\n\t// If true, do not output initialization logs\n\tquietInit bool\n\n\t// Called whenever applicable, in order to instrument the authority.\n\tmeter Meter\n}\n\n// Info contains information about the authority.\ntype Info struct {\n\tStartTime          time.Time\n\tRootX509Certs      []*x509.Certificate\n\tSSHCAUserPublicKey []byte\n\tSSHCAHostPublicKey []byte\n\tDNSNames           []string\n}\n\n// New creates and initiates a new Authority type.\nfunc New(cfg *config.Config, opts ...Option) (*Authority, error) {\n\terr := cfg.Validate()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar a = &Authority{\n\t\tconfig:        cfg,\n\t\tcertificates:  new(sync.Map),\n\t\tvalidateSCEP:  true,\n\t\tmeter:         noopMeter{},\n\t\twrapTransport: httptransport.NoopWrapper(),\n\t}\n\n\t// Apply options.\n\tfor _, fn := range opts {\n\t\tif err := fn(a); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\tif a.keyManager != nil {\n\t\ta.keyManager = newInstrumentedKeyManager(a.keyManager, a.meter)\n\t}\n\n\t// Initialize system cert pool\n\tif err := initializeSystemCertPool(); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to initialize the system cert pool: %w\", err)\n\t}\n\n\tif !a.skipInit {\n\t\t// Initialize authority from options or configuration.\n\t\tif err := a.init(); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn a, nil\n}\n\n// NewEmbedded initializes an authority that can be embedded in a different\n// project without the limitations of the config.\nfunc NewEmbedded(opts ...Option) (*Authority, error) {\n\ta := &Authority{\n\t\tconfig:        &config.Config{},\n\t\tcertificates:  new(sync.Map),\n\t\tmeter:         noopMeter{},\n\t\twrapTransport: httptransport.NoopWrapper(),\n\t}\n\n\t// Apply options.\n\tfor _, fn := range opts {\n\t\tif err := fn(a); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\tif a.keyManager != nil {\n\t\ta.keyManager = newInstrumentedKeyManager(a.keyManager, a.meter)\n\t}\n\n\t// Initialize system cert pool\n\tif err := initializeSystemCertPool(); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to initialize the system cert pool: %w\", err)\n\t}\n\n\t// Validate required options\n\tswitch {\n\tcase a.config == nil:\n\t\treturn nil, errors.New(\"cannot create an authority without a configuration\")\n\tcase len(a.rootX509Certs) == 0 && a.config.Root.HasEmpties():\n\t\treturn nil, errors.New(\"cannot create an authority without a root certificate\")\n\tcase a.x509CAService == nil && a.config.IntermediateCert == \"\":\n\t\treturn nil, errors.New(\"cannot create an authority without an issuer certificate\")\n\tcase a.x509CAService == nil && a.config.IntermediateKey == \"\":\n\t\treturn nil, errors.New(\"cannot create an authority without an issuer signer\")\n\t}\n\n\t// Initialize config required fields.\n\ta.config.Init()\n\n\tif !a.skipInit {\n\t\t// Initialize authority from options or configuration.\n\t\tif err := a.init(); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn a, nil\n}\n\ntype authorityKey struct{}\n\n// NewContext adds the given authority to the context.\nfunc NewContext(ctx context.Context, a *Authority) context.Context {\n\treturn context.WithValue(ctx, authorityKey{}, a)\n}\n\n// FromContext returns the current authority from the given context.\nfunc FromContext(ctx context.Context) (a *Authority, ok bool) {\n\ta, ok = ctx.Value(authorityKey{}).(*Authority)\n\treturn\n}\n\n// MustFromContext returns the current authority from the given context. It will\n// panic if the authority is not in the context.\nfunc MustFromContext(ctx context.Context) *Authority {\n\tvar (\n\t\ta  *Authority\n\t\tok bool\n\t)\n\tif a, ok = FromContext(ctx); !ok {\n\t\tpanic(\"authority is not in the context\")\n\t}\n\treturn a\n}\n\n// ReloadAdminResources reloads admins and provisioners from the DB.\nfunc (a *Authority) ReloadAdminResources(ctx context.Context) error {\n\tvar (\n\t\tprovList  provisioner.List\n\t\tadminList []*linkedca.Admin\n\t)\n\tif a.config.AuthorityConfig.EnableAdmin {\n\t\tprovs, err := a.adminDB.GetProvisioners(ctx)\n\t\tif err != nil {\n\t\t\treturn admin.WrapErrorISE(err, \"error getting provisioners to initialize authority\")\n\t\t}\n\t\tprovList, err = provisionerListToCertificates(provs)\n\t\tif err != nil {\n\t\t\treturn admin.WrapErrorISE(err, \"error converting provisioner list to certificates\")\n\t\t}\n\t\tadminList, err = a.adminDB.GetAdmins(ctx)\n\t\tif err != nil {\n\t\t\treturn admin.WrapErrorISE(err, \"error getting admins to initialize authority\")\n\t\t}\n\t} else {\n\t\tprovList = a.config.AuthorityConfig.Provisioners\n\t\tadminList = a.config.AuthorityConfig.Admins\n\t}\n\n\tprovisionerConfig, err := a.generateProvisionerConfig(ctx)\n\tif err != nil {\n\t\treturn admin.WrapErrorISE(err, \"error generating provisioner config\")\n\t}\n\n\t// Create provisioner collection.\n\tprovClxn := provisioner.NewCollection(provisionerConfig.Audiences)\n\tfor _, p := range provList {\n\t\tif err := p.Init(provisionerConfig); err != nil {\n\t\t\tlog.Printf(\"failed to initialize %s provisioner %q: %v\\n\", p.GetType(), p.GetName(), err)\n\t\t\tp = provisioner.Uninitialized{\n\t\t\t\tInterface: p, Reason: err,\n\t\t\t}\n\t\t}\n\t\tif err := provClxn.Store(p); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\t// Create admin collection.\n\tadminClxn := administrator.NewCollection(provClxn)\n\tfor _, adm := range adminList {\n\t\tp, ok := provClxn.Load(adm.ProvisionerId)\n\t\tif !ok {\n\t\t\treturn admin.NewErrorISE(\"provisioner %s not found when loading admin %s\",\n\t\t\t\tadm.ProvisionerId, adm.Id)\n\t\t}\n\t\tif err := adminClxn.Store(adm, p); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\ta.config.AuthorityConfig.Provisioners = provList\n\ta.provisioners = provClxn\n\ta.config.AuthorityConfig.Admins = adminList\n\ta.admins = adminClxn\n\n\tswitch {\n\tcase a.requiresSCEP() && a.GetSCEP() == nil:\n\t\t// TODO(hs): try to initialize SCEP here too? It's a bit\n\t\t// problematic if this method is called as part of an update\n\t\t// via Admin API and a password needs to be provided.\n\tcase a.requiresSCEP() && a.GetSCEP() != nil:\n\t\t// update the SCEP Authority with the currently active SCEP\n\t\t// provisioner names and revalidate the configuration.\n\t\ta.scepAuthority.UpdateProvisioners(a.getSCEPProvisionerNames())\n\t\tif err := a.scepAuthority.Validate(); err != nil {\n\t\t\tlog.Printf(\"failed validating SCEP authority: %v\\n\", err)\n\t\t}\n\tcase !a.requiresSCEP() && a.GetSCEP() != nil:\n\t\t// TODO(hs): don't remove the authority if we can't also\n\t\t// reload it.\n\t\t//a.scepAuthority = nil\n\t}\n\n\treturn nil\n}\n\n// init performs validation and initializes the fields of an Authority struct.\nfunc (a *Authority) init() error {\n\t// Check if handler has already been validated/initialized.\n\tif a.initOnce {\n\t\treturn nil\n\t}\n\n\tvar err error\n\tctx := NewContext(context.Background(), a)\n\n\t// Set password if they are not set.\n\tvar configPassword []byte\n\tif a.config.Password != \"\" {\n\t\tconfigPassword = []byte(a.config.Password)\n\t}\n\tif configPassword != nil && a.password == nil {\n\t\ta.password = configPassword\n\t}\n\tif a.sshHostPassword == nil {\n\t\ta.sshHostPassword = a.password\n\t}\n\tif a.sshUserPassword == nil {\n\t\ta.sshUserPassword = a.password\n\t}\n\n\t// Automatically enable admin for all linked cas.\n\tif a.linkedCAToken != \"\" {\n\t\ta.config.AuthorityConfig.EnableAdmin = true\n\t}\n\n\t// Initialize step-ca Database if it's not already initialized with WithDB.\n\t// If a.config.DB is nil then a simple, barebones in memory DB will be used.\n\tif a.db == nil {\n\t\tif a.db, err = db.New(a.config.DB); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\t// Initialize key manager if it has not been set in the options.\n\tif a.keyManager == nil {\n\t\tvar options kmsapi.Options\n\t\tif a.config.KMS != nil {\n\t\t\toptions = *a.config.KMS\n\t\t}\n\t\ta.keyManager, err = kms.New(ctx, options)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\ta.keyManager = newInstrumentedKeyManager(a.keyManager, a.meter)\n\t}\n\n\t// Initialize linkedca client if necessary. On a linked RA, the issuer\n\t// configuration might come from majordomo.\n\tvar linkedcaClient *linkedCaClient\n\tif a.config.AuthorityConfig.EnableAdmin && a.linkedCAToken != \"\" && a.adminDB == nil {\n\t\tlinkedcaClient, err = newLinkedCAClient(a.linkedCAToken)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\t// If authorityId is configured make sure it matches the one in the token\n\t\tif id := a.config.AuthorityConfig.AuthorityID; id != \"\" && !strings.EqualFold(id, linkedcaClient.authorityID) {\n\t\t\treturn errors.New(\"error initializing linkedca: token authority and configured authority do not match\")\n\t\t}\n\t\ta.config.AuthorityConfig.AuthorityID = linkedcaClient.authorityID\n\t\tlinkedcaClient.Run()\n\t}\n\n\t// Initialize the X.509 CA Service if it has not been set in the options.\n\tif a.x509CAService == nil {\n\t\tvar options casapi.Options\n\t\tif a.config.AuthorityConfig.Options != nil {\n\t\t\toptions = *a.config.AuthorityConfig.Options\n\t\t}\n\n\t\t// AuthorityID might be empty. It's always available linked CAs/RAs.\n\t\toptions.AuthorityID = a.config.AuthorityConfig.AuthorityID\n\n\t\t// Configure linked RA\n\t\tif linkedcaClient != nil && options.CertificateAuthority == \"\" {\n\t\t\tconf, err := linkedcaClient.GetConfiguration(ctx)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif conf.RaConfig != nil {\n\t\t\t\toptions.CertificateAuthority = conf.RaConfig.CaUrl\n\t\t\t\toptions.CertificateAuthorityFingerprint = conf.RaConfig.Fingerprint\n\t\t\t\toptions.CertificateIssuer = &casapi.CertificateIssuer{\n\t\t\t\t\tType:        conf.RaConfig.Provisioner.Type.String(),\n\t\t\t\t\tProvisioner: conf.RaConfig.Provisioner.Name,\n\t\t\t\t}\n\t\t\t\t// Configure the RA authority type if needed\n\t\t\t\tif options.Type == \"\" {\n\t\t\t\t\toptions.Type = casapi.StepCAS\n\t\t\t\t}\n\t\t\t}\n\t\t\t// Remote configuration is currently only supported on a linked RA\n\t\t\tif sc := conf.ServerConfig; sc != nil {\n\t\t\t\tif a.config.Address == \"\" {\n\t\t\t\t\ta.config.Address = sc.Address\n\t\t\t\t}\n\t\t\t\tif len(a.config.DNSNames) == 0 {\n\t\t\t\t\ta.config.DNSNames = sc.DnsNames\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Set the issuer password if passed in the flags.\n\t\tif options.CertificateIssuer != nil && a.issuerPassword != nil {\n\t\t\toptions.CertificateIssuer.Password = string(a.issuerPassword)\n\t\t}\n\n\t\t// Read intermediate and create X509 signer for default CAS.\n\t\tif options.Is(casapi.SoftCAS) {\n\t\t\toptions.CertificateChain, err = pemutil.ReadCertificateBundle(a.config.IntermediateCert)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\toptions.Signer, err = a.keyManager.CreateSigner(&kmsapi.CreateSignerRequest{\n\t\t\t\tSigningKey: a.config.IntermediateKey,\n\t\t\t\tPassword:   a.password,\n\t\t\t})\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\t// If not defined with an option, add intermediates to the list of\n\t\t\t// certificates used for name constraints validation at issuance\n\t\t\t// time.\n\t\t\tif len(a.intermediateX509Certs) == 0 {\n\t\t\t\ta.intermediateX509Certs = append(a.intermediateX509Certs, options.CertificateChain...)\n\t\t\t}\n\t\t}\n\t\ta.x509CAService, err = cas.New(ctx, options)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\t// Get root certificate from CAS.\n\t\tif srv, ok := a.x509CAService.(casapi.CertificateAuthorityGetter); ok {\n\t\t\tresp, err := srv.GetCertificateAuthority(&casapi.GetCertificateAuthorityRequest{\n\t\t\t\tName: options.CertificateAuthority,\n\t\t\t})\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\ta.rootX509Certs = append(a.rootX509Certs, resp.RootCertificate)\n\t\t\ta.intermediateX509Certs = append(a.intermediateX509Certs, resp.IntermediateCertificates...)\n\t\t}\n\t}\n\n\t// Read root certificates and store them in the certificates map.\n\tif len(a.rootX509Certs) == 0 {\n\t\ta.rootX509Certs = make([]*x509.Certificate, 0, len(a.config.Root))\n\t\tfor _, path := range a.config.Root {\n\t\t\tcrts, err := pemutil.ReadCertificateBundle(path)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\ta.rootX509Certs = append(a.rootX509Certs, crts...)\n\t\t}\n\t}\n\tfor _, crt := range a.rootX509Certs {\n\t\tsum := sha256.Sum256(crt.Raw)\n\t\ta.certificates.Store(hex.EncodeToString(sum[:]), crt)\n\t}\n\n\ta.rootX509CertPool = x509.NewCertPool()\n\tfor _, cert := range a.rootX509Certs {\n\t\ta.rootX509CertPool.AddCert(cert)\n\t}\n\n\t// Read federated certificates and store them in the certificates map.\n\tif len(a.federatedX509Certs) == 0 {\n\t\ta.federatedX509Certs = make([]*x509.Certificate, 0, len(a.config.FederatedRoots))\n\t\tfor _, path := range a.config.FederatedRoots {\n\t\t\tcrts, err := pemutil.ReadCertificateBundle(path)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\ta.federatedX509Certs = append(a.federatedX509Certs, crts...)\n\t\t}\n\t}\n\tfor _, crt := range a.federatedX509Certs {\n\t\tsum := sha256.Sum256(crt.Raw)\n\t\ta.certificates.Store(hex.EncodeToString(sum[:]), crt)\n\t}\n\n\t// Initialize HTTPClient with all root certs\n\tclientRoots := make([]*x509.Certificate, 0, len(a.rootX509Certs)+len(a.federatedX509Certs))\n\tclientRoots = append(clientRoots, a.rootX509Certs...)\n\tclientRoots = append(clientRoots, a.federatedX509Certs...)\n\ta.httpClient = newHTTPClient(a.wrapTransport, clientRoots...)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Decrypt and load SSH keys\n\tvar tmplVars templates.Step\n\tif a.config.SSH != nil {\n\t\tif a.config.SSH.HostKey != \"\" {\n\t\t\tsigner, err := a.keyManager.CreateSigner(&kmsapi.CreateSignerRequest{\n\t\t\t\tSigningKey: a.config.SSH.HostKey,\n\t\t\t\tPassword:   a.sshHostPassword,\n\t\t\t})\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\t// If our signer is from sshagentkms, just unwrap it instead of\n\t\t\t// wrapping it in another layer, and this prevents crypto from\n\t\t\t// erroring out with: ssh: unsupported key type *agent.Key\n\t\t\tswitch s := signer.(type) {\n\t\t\tcase *sshagentkms.WrappedSSHSigner:\n\t\t\t\ta.sshCAHostCertSignKey = s.Signer\n\t\t\tcase *instrumentedKMSSigner:\n\t\t\t\tswitch is := s.Signer.(type) {\n\t\t\t\tcase *sshagentkms.WrappedSSHSigner:\n\t\t\t\t\ta.sshCAHostCertSignKey = is.Signer\n\t\t\t\tdefault:\n\t\t\t\t\ta.sshCAHostCertSignKey, err = ssh.NewSignerFromSigner(s)\n\t\t\t\t}\n\t\t\tcase crypto.Signer:\n\t\t\t\ta.sshCAHostCertSignKey, err = ssh.NewSignerFromSigner(s)\n\t\t\tdefault:\n\t\t\t\treturn errors.Errorf(\"unsupported signer type %T\", signer)\n\t\t\t}\n\t\t\tif err != nil {\n\t\t\t\treturn errors.Wrap(err, \"error creating ssh signer\")\n\t\t\t}\n\t\t\t// Append public key to list of host certs\n\t\t\ta.sshCAHostCerts = append(a.sshCAHostCerts, a.sshCAHostCertSignKey.PublicKey())\n\t\t\ta.sshCAHostFederatedCerts = append(a.sshCAHostFederatedCerts, a.sshCAHostCertSignKey.PublicKey())\n\t\t}\n\t\tif a.config.SSH.UserKey != \"\" {\n\t\t\tsigner, err := a.keyManager.CreateSigner(&kmsapi.CreateSignerRequest{\n\t\t\t\tSigningKey: a.config.SSH.UserKey,\n\t\t\t\tPassword:   a.sshUserPassword,\n\t\t\t})\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\t// If our signer is from sshagentkms, just unwrap it instead of\n\t\t\t// wrapping it in another layer, and this prevents crypto from\n\t\t\t// erroring out with: ssh: unsupported key type *agent.Key\n\t\t\tswitch s := signer.(type) {\n\t\t\tcase *sshagentkms.WrappedSSHSigner:\n\t\t\t\ta.sshCAUserCertSignKey = s.Signer\n\t\t\tcase *instrumentedKMSSigner:\n\t\t\t\tswitch is := s.Signer.(type) {\n\t\t\t\tcase *sshagentkms.WrappedSSHSigner:\n\t\t\t\t\ta.sshCAUserCertSignKey = is.Signer\n\t\t\t\tdefault:\n\t\t\t\t\ta.sshCAUserCertSignKey, err = ssh.NewSignerFromSigner(s)\n\t\t\t\t}\n\t\t\tcase crypto.Signer:\n\t\t\t\ta.sshCAUserCertSignKey, err = ssh.NewSignerFromSigner(s)\n\t\t\tdefault:\n\t\t\t\treturn errors.Errorf(\"unsupported signer type %T\", signer)\n\t\t\t}\n\t\t\tif err != nil {\n\t\t\t\treturn errors.Wrap(err, \"error creating ssh signer\")\n\t\t\t}\n\t\t\t// Append public key to list of user certs\n\t\t\ta.sshCAUserCerts = append(a.sshCAUserCerts, a.sshCAUserCertSignKey.PublicKey())\n\t\t\ta.sshCAUserFederatedCerts = append(a.sshCAUserFederatedCerts, a.sshCAUserCertSignKey.PublicKey())\n\t\t}\n\n\t\t// Append other public keys and add them to the template variables.\n\t\tfor _, key := range a.config.SSH.Keys {\n\t\t\tpublicKey := key.PublicKey()\n\t\t\tswitch key.Type {\n\t\t\tcase provisioner.SSHHostCert:\n\t\t\t\tif key.Federated {\n\t\t\t\t\ta.sshCAHostFederatedCerts = append(a.sshCAHostFederatedCerts, publicKey)\n\t\t\t\t} else {\n\t\t\t\t\ta.sshCAHostCerts = append(a.sshCAHostCerts, publicKey)\n\t\t\t\t}\n\t\t\tcase provisioner.SSHUserCert:\n\t\t\t\tif key.Federated {\n\t\t\t\t\ta.sshCAUserFederatedCerts = append(a.sshCAUserFederatedCerts, publicKey)\n\t\t\t\t} else {\n\t\t\t\t\ta.sshCAUserCerts = append(a.sshCAUserCerts, publicKey)\n\t\t\t\t}\n\t\t\tdefault:\n\t\t\t\treturn errors.Errorf(\"unsupported type %s\", key.Type)\n\t\t\t}\n\t\t}\n\t}\n\n\t// Configure template variables. On the template variables HostFederatedKeys\n\t// and UserFederatedKeys we will skip the actual CA that will be available\n\t// in HostKey and UserKey.\n\t//\n\t// We cannot do it in the previous blocks because this configuration can be\n\t// injected using options.\n\tif a.sshCAHostCertSignKey != nil {\n\t\ttmplVars.SSH.HostKey = a.sshCAHostCertSignKey.PublicKey()\n\t\ttmplVars.SSH.HostFederatedKeys = append(tmplVars.SSH.HostFederatedKeys, a.sshCAHostFederatedCerts[1:]...)\n\t} else {\n\t\ttmplVars.SSH.HostFederatedKeys = append(tmplVars.SSH.HostFederatedKeys, a.sshCAHostFederatedCerts...)\n\t}\n\tif a.sshCAUserCertSignKey != nil {\n\t\ttmplVars.SSH.UserKey = a.sshCAUserCertSignKey.PublicKey()\n\t\ttmplVars.SSH.UserFederatedKeys = append(tmplVars.SSH.UserFederatedKeys, a.sshCAUserFederatedCerts[1:]...)\n\t} else {\n\t\ttmplVars.SSH.UserFederatedKeys = append(tmplVars.SSH.UserFederatedKeys, a.sshCAUserFederatedCerts...)\n\t}\n\n\tif a.config.AuthorityConfig.EnableAdmin {\n\t\t// Initialize step-ca Admin Database if it's not already initialized using\n\t\t// WithAdminDB.\n\t\tif a.adminDB == nil {\n\t\t\tif linkedcaClient != nil {\n\t\t\t\ta.adminDB = linkedcaClient\n\t\t\t} else {\n\t\t\t\ta.adminDB, err = adminDBNosql.New(a.db.(nosql.DB), admin.DefaultAuthorityID)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tprovs, err := a.adminDB.GetProvisioners(ctx)\n\t\tif err != nil {\n\t\t\treturn admin.WrapErrorISE(err, \"error loading provisioners to initialize authority\")\n\t\t}\n\t\tif len(provs) == 0 && !strings.EqualFold(a.config.AuthorityConfig.DeploymentType, \"linked\") {\n\t\t\t// Migration will currently only be kicked off once, because either one or more provisioners\n\t\t\t// are migrated or a default JWK provisioner will be created in the DB. It won't run for\n\t\t\t// linked or hosted deployments. Not for linked, because that case is explicitly checked\n\t\t\t// for above. Not for hosted, because there'll be at least an existing OIDC provisioner.\n\t\t\tvar firstJWKProvisioner *linkedca.Provisioner\n\t\t\tif len(a.config.AuthorityConfig.Provisioners) > 0 {\n\t\t\t\t// Existing provisioners detected; try migrating them to DB storage.\n\t\t\t\ta.initLogf(\"Starting migration of provisioners\")\n\t\t\t\tfor _, p := range a.config.AuthorityConfig.Provisioners {\n\t\t\t\t\tlp, err := ProvisionerToLinkedca(p)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn admin.WrapErrorISE(err, \"error transforming provisioner %q while migrating\", p.GetName())\n\t\t\t\t\t}\n\n\t\t\t\t\t// Store the provisioner to be migrated\n\t\t\t\t\tif err := a.adminDB.CreateProvisioner(ctx, lp); err != nil {\n\t\t\t\t\t\treturn admin.WrapErrorISE(err, \"error creating provisioner %q while migrating\", p.GetName())\n\t\t\t\t\t}\n\n\t\t\t\t\t// Mark the first JWK provisioner, so that it can be used for administration purposes\n\t\t\t\t\tif firstJWKProvisioner == nil && lp.Type == linkedca.Provisioner_JWK {\n\t\t\t\t\t\tfirstJWKProvisioner = lp\n\t\t\t\t\t\ta.initLogf(\"Migrated JWK provisioner %q with admin permissions\", p.GetName())\n\t\t\t\t\t} else {\n\t\t\t\t\t\ta.initLogf(\"Migrated %s provisioner %q\", p.GetType(), p.GetName())\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tc := a.config\n\t\t\t\tif c.WasLoadedFromFile() {\n\t\t\t\t\t// The provisioners in the configuration file can be deleted from\n\t\t\t\t\t// the file by editing it. Automatic rewriting of the file was considered\n\t\t\t\t\t// to be too surprising for users and not the right solution for all\n\t\t\t\t\t// use cases, so we leave it up to users to this themselves.\n\t\t\t\t\ta.initLogf(\"Provisioners that were migrated can now be removed from `ca.json` by editing it\")\n\t\t\t\t}\n\n\t\t\t\ta.initLogf(\"Finished migrating provisioners\")\n\t\t\t}\n\n\t\t\t// Create first JWK provisioner for remote administration purposes if none exists yet\n\t\t\tif firstJWKProvisioner == nil {\n\t\t\t\tfirstJWKProvisioner, err = CreateFirstProvisioner(ctx, a.adminDB, string(a.password))\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn admin.WrapErrorISE(err, \"error creating first provisioner\")\n\t\t\t\t}\n\t\t\t\ta.initLogf(\"Created JWK provisioner %q with admin permissions\", firstJWKProvisioner.GetName())\n\t\t\t}\n\n\t\t\t// Create first super admin, belonging to the first JWK provisioner\n\t\t\t// TODO(hs): pass a user-provided first super admin subject to here. With `ca init` it's\n\t\t\t// added to the DB immediately if using remote management. But when migrating from\n\t\t\t// ca.json to the DB, this option doesn't exist. Adding a flag just to do it during\n\t\t\t// migration isn't nice. We could opt for a user to change it afterwards. There exist\n\t\t\t// cases in which creation of `step` could lock out a user from API access. This is the\n\t\t\t// case if `step` isn't allowed to be signed by Name Constraints or the X.509 policy.\n\t\t\t// We have protection for that when creating and updating a policy, but if a policy or\n\t\t\t// Name Constraints are in use at the time of migration, that could lock the user out.\n\t\t\tsuperAdminSubject := \"step\"\n\t\t\tif err := a.adminDB.CreateAdmin(ctx, &linkedca.Admin{\n\t\t\t\tProvisionerId: firstJWKProvisioner.Id,\n\t\t\t\tSubject:       superAdminSubject,\n\t\t\t\tType:          linkedca.Admin_SUPER_ADMIN,\n\t\t\t}); err != nil {\n\t\t\t\treturn admin.WrapErrorISE(err, \"error creating first admin\")\n\t\t\t}\n\n\t\t\ta.initLogf(\"Created super admin %q for JWK provisioner %q\", superAdminSubject, firstJWKProvisioner.GetName())\n\t\t}\n\t}\n\n\t// Load Provisioners and Admins\n\tif err := a.ReloadAdminResources(ctx); err != nil {\n\t\treturn err\n\t}\n\n\t// The SCEP functionality is provided through an instance of\n\t// scep.Authority. It is initialized when the CA is started and\n\t// if it doesn't exist yet. It gets refreshed if it already\n\t// exists. If the SCEP authority is no longer required on reload,\n\t// it gets removed.\n\t// TODO(hs): reloading through SIGHUP doesn't hit these cases. This\n\t// is because an entirely new authority.Authority is created, including\n\t// a new scep.Authority. Look into this to see if we want this to\n\t// keep working like that, or want to reuse a single instance and\n\t// update that.\n\tswitch {\n\tcase a.requiresSCEP() && a.GetSCEP() == nil:\n\t\tif a.scepOptions == nil {\n\t\t\toptions := &scep.Options{\n\t\t\t\tRoots:         a.rootX509Certs,\n\t\t\t\tIntermediates: a.intermediateX509Certs,\n\t\t\t}\n\n\t\t\t// intermediate certificates can be empty in RA mode\n\t\t\tif len(a.intermediateX509Certs) > 0 {\n\t\t\t\toptions.SignerCert = a.intermediateX509Certs[0]\n\t\t\t}\n\n\t\t\t// attempt to create the (default) SCEP signer if the intermediate\n\t\t\t// key is configured.\n\t\t\tif a.config.IntermediateKey != \"\" {\n\t\t\t\tif options.Signer, err = a.keyManager.CreateSigner(&kmsapi.CreateSignerRequest{\n\t\t\t\t\tSigningKey: a.config.IntermediateKey,\n\t\t\t\t\tPassword:   a.password,\n\t\t\t\t}); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\t// TODO(hs): instead of creating the decrypter here, pass the\n\t\t\t\t// intermediate key + chain down to the SCEP authority,\n\t\t\t\t// and only instantiate it when required there. Is that possible?\n\t\t\t\t// Also with entering passwords?\n\t\t\t\t// TODO(hs): if moving the logic, try improving the logic for the\n\t\t\t\t// decrypter password too? Right now it needs to be entered multiple\n\t\t\t\t// times; I've observed it to be three times maximum, every time\n\t\t\t\t// the intermediate key is read.\n\t\t\t\t_, isRSAKey := options.Signer.Public().(*rsa.PublicKey)\n\t\t\t\tif km, ok := a.keyManager.(kmsapi.Decrypter); ok && isRSAKey {\n\t\t\t\t\tif decrypter, err := km.CreateDecrypter(&kmsapi.CreateDecrypterRequest{\n\t\t\t\t\t\tDecryptionKey: a.config.IntermediateKey,\n\t\t\t\t\t\tPassword:      a.password,\n\t\t\t\t\t}); err == nil {\n\t\t\t\t\t\t// only pass the decrypter down when it was successfully created,\n\t\t\t\t\t\t// meaning it's an RSA key, and `CreateDecrypter` did not fail.\n\t\t\t\t\t\toptions.Decrypter = decrypter\n\n\t\t\t\t\t\t// intermediate certificates can be empty in RA mode\n\t\t\t\t\t\tif len(options.Intermediates) > 0 {\n\t\t\t\t\t\t\toptions.DecrypterCert = options.Intermediates[0]\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\ta.scepOptions = options\n\t\t}\n\n\t\t// provide the current SCEP provisioner names, so that the provisioners\n\t\t// can be validated when the CA is started.\n\t\ta.scepOptions.SCEPProvisionerNames = a.getSCEPProvisionerNames()\n\n\t\t// create a new SCEP authority\n\t\tscepAuthority, err := scep.New(a, *a.scepOptions)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif a.validateSCEP {\n\t\t\t// validate the SCEP authority\n\t\t\tif err := scepAuthority.Validate(); err != nil {\n\t\t\t\ta.initLogf(\"failed validating SCEP authority: %v\", err)\n\t\t\t}\n\t\t}\n\n\t\t// set the SCEP authority\n\t\ta.scepAuthority = scepAuthority\n\tcase !a.requiresSCEP() && a.GetSCEP() != nil:\n\t\t// clear the SCEP authority if it's no longer required\n\t\ta.scepAuthority = nil\n\tcase a.requiresSCEP() && a.GetSCEP() != nil:\n\t\t// update the SCEP Authority with the currently active SCEP\n\t\t// provisioner names and revalidate the configuration.\n\t\ta.scepAuthority.UpdateProvisioners(a.getSCEPProvisionerNames())\n\t\tif err := a.scepAuthority.Validate(); err != nil {\n\t\t\tlog.Printf(\"failed validating SCEP authority: %v\\n\", err)\n\t\t}\n\t}\n\n\t// Load X509 constraints engine.\n\t//\n\t// This is currently only available in CA mode.\n\tif size := len(a.intermediateX509Certs); size > 0 {\n\t\tlast := a.intermediateX509Certs[size-1]\n\t\tconstraintCerts := make([]*x509.Certificate, 0, size+1)\n\t\tconstraintCerts = append(constraintCerts, a.intermediateX509Certs...)\n\t\tfor _, root := range a.rootX509Certs {\n\t\t\tif bytes.Equal(last.RawIssuer, root.RawSubject) && bytes.Equal(last.AuthorityKeyId, root.SubjectKeyId) {\n\t\t\t\tconstraintCerts = append(constraintCerts, root)\n\t\t\t}\n\t\t}\n\t\ta.constraintsEngine = constraints.New(constraintCerts...)\n\t}\n\n\t// Load x509 and SSH Policy Engines\n\tif err := a.reloadPolicyEngines(ctx); err != nil {\n\t\treturn err\n\t}\n\n\t// Configure templates, currently only ssh templates are supported.\n\tif a.sshCAHostCertSignKey != nil || a.sshCAUserCertSignKey != nil {\n\t\ta.templates = a.config.Templates\n\t\tif a.templates == nil {\n\t\t\ta.templates = templates.DefaultTemplates()\n\t\t}\n\t\tif a.templates.Data == nil {\n\t\t\ta.templates.Data = make(map[string]interface{})\n\t\t}\n\t\ta.templates.Data[\"Step\"] = tmplVars\n\t}\n\n\t// Start the CRL generator, we can assume the configuration is validated.\n\tif a.config.CRL.IsEnabled() {\n\t\t// Default cache duration to the default one\n\t\tif v := a.config.CRL.CacheDuration; v == nil || v.Duration <= 0 {\n\t\t\ta.config.CRL.CacheDuration = config.DefaultCRLCacheDuration\n\t\t}\n\t\t// Start CRL generator\n\t\tif err := a.startCRLGenerator(); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\t// JWT numeric dates are seconds.\n\ta.startTime = time.Now().Truncate(time.Second)\n\t// Set flag indicating that initialization has been completed, and should\n\t// not be repeated.\n\ta.initOnce = true\n\n\treturn nil\n}\n\n// initLogf is used to log initialization information. The output\n// can be disabled by starting the CA with the `--quiet` flag.\nfunc (a *Authority) initLogf(format string, v ...any) {\n\tif !a.quietInit {\n\t\tlog.Printf(format, v...)\n\t}\n}\n\n// GetID returns the define authority id or a zero uuid.\nfunc (a *Authority) GetID() string {\n\tconst zeroUUID = \"00000000-0000-0000-0000-000000000000\"\n\tif id := a.config.AuthorityConfig.AuthorityID; id != \"\" {\n\t\treturn id\n\t}\n\treturn zeroUUID\n}\n\n// GetDatabase returns the authority database. If the configuration does not\n// define a database, GetDatabase will return a db.SimpleDB instance.\nfunc (a *Authority) GetDatabase() db.AuthDB {\n\treturn a.db\n}\n\n// GetAdminDatabase returns the admin database, if one exists.\nfunc (a *Authority) GetAdminDatabase() admin.DB {\n\treturn a.adminDB\n}\n\n// GetConfig returns the config.\nfunc (a *Authority) GetConfig() *config.Config {\n\treturn a.config\n}\n\n// GetBackdate returns the [time.Duration] representing the\n// amount of time that is to be subtracted from the current\n// time when issuing a new certificate.\nfunc (a *Authority) GetBackdate() *time.Duration {\n\tif a.config == nil || a.config.AuthorityConfig == nil || a.config.AuthorityConfig.Backdate == nil {\n\t\treturn nil\n\t}\n\n\treturn &a.config.AuthorityConfig.Backdate.Duration\n}\n\n// GetInfo returns information about the authority.\nfunc (a *Authority) GetInfo() Info {\n\tai := Info{\n\t\tStartTime:     a.startTime,\n\t\tRootX509Certs: a.rootX509Certs,\n\t\tDNSNames:      a.config.DNSNames,\n\t}\n\tif a.sshCAUserCertSignKey != nil {\n\t\tai.SSHCAUserPublicKey = ssh.MarshalAuthorizedKey(a.sshCAUserCertSignKey.PublicKey())\n\t}\n\tif a.sshCAHostCertSignKey != nil {\n\t\tai.SSHCAHostPublicKey = ssh.MarshalAuthorizedKey(a.sshCAHostCertSignKey.PublicKey())\n\t}\n\treturn ai\n}\n\n// IsAdminAPIEnabled returns a boolean indicating whether the Admin API has\n// been enabled.\nfunc (a *Authority) IsAdminAPIEnabled() bool {\n\treturn a.config.AuthorityConfig.EnableAdmin\n}\n\n// Shutdown safely shuts down any clients, databases, etc. held by the Authority.\nfunc (a *Authority) Shutdown() error {\n\tif a.crlTicker != nil {\n\t\ta.crlTicker.Stop()\n\t\tclose(a.crlStopper)\n\t}\n\n\tif err := a.keyManager.Close(); err != nil {\n\t\tlog.Printf(\"error closing the key manager: %v\", err)\n\t}\n\treturn a.db.Shutdown()\n}\n\n// CloseForReload closes internal services, to allow a safe reload.\nfunc (a *Authority) CloseForReload() {\n\tif a.crlTicker != nil {\n\t\ta.crlTicker.Stop()\n\t\tclose(a.crlStopper)\n\t}\n\n\tif err := a.keyManager.Close(); err != nil {\n\t\tlog.Printf(\"error closing the key manager: %v\", err)\n\t}\n\tif client, ok := a.adminDB.(*linkedCaClient); ok {\n\t\tclient.Stop()\n\t}\n}\n\n// IsRevoked returns whether or not a certificate has been\n// revoked before.\nfunc (a *Authority) IsRevoked(sn string) (bool, error) {\n\t// Check the passive revocation table.\n\tif lca, ok := a.adminDB.(interface {\n\t\tIsRevoked(string) (bool, error)\n\t}); ok {\n\t\treturn lca.IsRevoked(sn)\n\t}\n\n\treturn a.db.IsRevoked(sn)\n}\n\n// requiresSCEP iterates over the configured provisioners\n// and determines if at least one of them is a SCEP provisioner.\nfunc (a *Authority) requiresSCEP() bool {\n\tfor _, p := range a.config.AuthorityConfig.Provisioners {\n\t\tif p.GetType() == provisioner.TypeSCEP {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// getSCEPProvisionerNames returns the names of the SCEP provisioners\n// that are currently available in the CA.\nfunc (a *Authority) getSCEPProvisionerNames() (names []string) {\n\tfor _, p := range a.config.AuthorityConfig.Provisioners {\n\t\tif p.GetType() == provisioner.TypeSCEP {\n\t\t\tnames = append(names, p.GetName())\n\t\t}\n\t}\n\n\treturn\n}\n\n// GetSCEP returns the configured SCEP Authority\nfunc (a *Authority) GetSCEP() *scep.Authority {\n\treturn a.scepAuthority\n}\n\n// HasACMEProvisioner returns true if at least one ACME provisioner is configured.\nfunc (a *Authority) HasACMEProvisioner() bool {\n\tfor _, p := range a.config.AuthorityConfig.Provisioners {\n\t\tif p.GetType() == provisioner.TypeACME {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc (a *Authority) startCRLGenerator() error {\n\tif !a.config.CRL.IsEnabled() {\n\t\treturn nil\n\t}\n\n\t// Check that there is a valid CRL in the DB right now. If it doesn't exist\n\t// or is expired, generate one now\n\t_, ok := a.db.(db.CertificateRevocationListDB)\n\tif !ok {\n\t\treturn errors.Errorf(\"CRL Generation requested, but database does not support CRL generation\")\n\t}\n\n\t// Always create a new CRL on startup in case the CA has been down and the\n\t// time to next expected CRL update is less than the cache duration.\n\tif err := a.GenerateCertificateRevocationList(); err != nil {\n\t\treturn errors.Wrap(err, \"could not generate a CRL\")\n\t}\n\n\ta.crlStopper = make(chan struct{}, 1)\n\ta.crlTicker = time.NewTicker(a.config.CRL.TickerDuration())\n\n\tgo func() {\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase <-a.crlTicker.C:\n\t\t\t\tlog.Println(\"Regenerating CRL\")\n\t\t\t\tif err := a.GenerateCertificateRevocationList(); err != nil {\n\t\t\t\t\tlog.Printf(\"error regenerating the CRL: %v\", err)\n\t\t\t\t}\n\t\t\tcase <-a.crlStopper:\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}()\n\n\treturn nil\n}\n"
  },
  {
    "path": "authority/authority_test.go",
    "content": "package authority\n\nimport (\n\t\"context\"\n\t\"crypto\"\n\t\"crypto/rand\"\n\t\"crypto/sha256\"\n\t\"crypto/x509\"\n\t\"encoding/hex\"\n\t\"encoding/pem\"\n\t\"fmt\"\n\t\"net\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"reflect\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n\t\"github.com/smallstep/assert\"\n\t\"github.com/smallstep/certificates/authority/config\"\n\t\"github.com/smallstep/certificates/authority/provisioner\"\n\t\"github.com/smallstep/certificates/db\"\n\t\"go.step.sm/crypto/jose\"\n\t\"go.step.sm/crypto/minica\"\n\t\"go.step.sm/crypto/pemutil\"\n)\n\nfunc TestMain(m *testing.M) {\n\tif err := initializeSystemCertPool(); err != nil {\n\t\tfmt.Fprintf(os.Stderr, \"failed to initialize system cert pool: %v\\n\", err)\n\t\tfmt.Fprintln(os.Stderr, \"See https://pkg.go.dev/crypto/x509#SystemCertPool\")\n\t\tos.Exit(2)\n\t}\n\n\tos.Exit(m.Run())\n}\n\nfunc testAuthority(t *testing.T, opts ...Option) *Authority {\n\tmaxjwk, err := jose.ReadKey(\"testdata/secrets/max_pub.jwk\")\n\tassert.FatalError(t, err)\n\tclijwk, err := jose.ReadKey(\"testdata/secrets/step_cli_key_pub.jwk\")\n\tassert.FatalError(t, err)\n\tdisableRenewal := true\n\tenableSSHCA := true\n\tp := provisioner.List{\n\t\t&provisioner.JWK{\n\t\t\tName: \"Max\",\n\t\t\tType: \"JWK\",\n\t\t\tKey:  maxjwk,\n\t\t},\n\t\t&provisioner.JWK{\n\t\t\tName: \"step-cli\",\n\t\t\tType: \"JWK\",\n\t\t\tKey:  clijwk,\n\t\t\tClaims: &provisioner.Claims{\n\t\t\t\tEnableSSHCA: &enableSSHCA,\n\t\t\t},\n\t\t},\n\t\t&provisioner.JWK{\n\t\t\tName: \"dev\",\n\t\t\tType: \"JWK\",\n\t\t\tKey:  maxjwk,\n\t\t\tClaims: &provisioner.Claims{\n\t\t\t\tDisableRenewal: &disableRenewal,\n\t\t\t},\n\t\t},\n\t\t&provisioner.JWK{\n\t\t\tName: \"renew_disabled\",\n\t\t\tType: \"JWK\",\n\t\t\tKey:  maxjwk,\n\t\t\tClaims: &provisioner.Claims{\n\t\t\t\tDisableRenewal: &disableRenewal,\n\t\t\t},\n\t\t},\n\t\t&provisioner.SSHPOP{\n\t\t\tName: \"sshpop\",\n\t\t\tType: \"SSHPOP\",\n\t\t\tClaims: &provisioner.Claims{\n\t\t\t\tEnableSSHCA: &enableSSHCA,\n\t\t\t},\n\t\t},\n\t\t&provisioner.ACME{\n\t\t\tName: \"acme\",\n\t\t\tType: \"ACME\",\n\t\t},\n\t\t&provisioner.JWK{\n\t\t\tName: \"uninitialized\",\n\t\t\tType: \"JWK\",\n\t\t\tKey:  clijwk,\n\t\t\tClaims: &provisioner.Claims{\n\t\t\t\tMinTLSDur: &provisioner.Duration{Duration: 5 * time.Minute},\n\t\t\t\tMaxTLSDur: &provisioner.Duration{Duration: time.Minute},\n\t\t\t},\n\t\t},\n\t}\n\tc := &Config{\n\t\tAddress:          \"127.0.0.1:443\",\n\t\tRoot:             []string{\"testdata/certs/root_ca.crt\"},\n\t\tIntermediateCert: \"testdata/certs/intermediate_ca.crt\",\n\t\tIntermediateKey:  \"testdata/secrets/intermediate_ca_key\",\n\t\tSSH: &SSHConfig{\n\t\t\tHostKey: \"testdata/secrets/ssh_host_ca_key\",\n\t\t\tUserKey: \"testdata/secrets/ssh_user_ca_key\",\n\t\t},\n\t\tDNSNames: []string{\"example.com\"},\n\t\tPassword: \"pass\",\n\t\tAuthorityConfig: &AuthConfig{\n\t\t\tProvisioners: p,\n\t\t},\n\t}\n\ta, err := New(c, opts...)\n\tassert.FatalError(t, err)\n\t// Avoid errors when test tokens are created before the test authority. This\n\t// happens in some tests where we re-create the same authority to test\n\t// special cases without re-creating the token.\n\ta.startTime = a.startTime.Add(-1 * time.Minute)\n\treturn a\n}\n\nfunc TestAuthorityNew(t *testing.T) {\n\ttype newTest struct {\n\t\tconfig *Config\n\t\terr    error\n\t}\n\ttests := map[string]func(t *testing.T) *newTest{\n\t\t\"ok\": func(t *testing.T) *newTest {\n\t\t\tc, err := LoadConfiguration(\"../ca/testdata/ca.json\")\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn &newTest{\n\t\t\t\tconfig: c,\n\t\t\t}\n\t\t},\n\t\t\"fail bad root\": func(t *testing.T) *newTest {\n\t\t\tc, err := LoadConfiguration(\"../ca/testdata/ca.json\")\n\t\t\tassert.FatalError(t, err)\n\t\t\tc.Root = []string{\"foo\"}\n\t\t\treturn &newTest{\n\t\t\t\tconfig: c,\n\t\t\t\terr:    errors.New(`error reading \"foo\": no such file or directory`),\n\t\t\t}\n\t\t},\n\t\t\"fail bad password\": func(t *testing.T) *newTest {\n\t\t\tc, err := LoadConfiguration(\"../ca/testdata/ca.json\")\n\t\t\tassert.FatalError(t, err)\n\t\t\tc.Password = \"wrong\"\n\t\t\treturn &newTest{\n\t\t\t\tconfig: c,\n\t\t\t\terr:    errors.New(\"error decrypting ../ca/testdata/secrets/intermediate_ca_key: x509: decryption password incorrect\"),\n\t\t\t}\n\t\t},\n\t\t\"fail loading CA cert\": func(t *testing.T) *newTest {\n\t\t\tc, err := LoadConfiguration(\"../ca/testdata/ca.json\")\n\t\t\tassert.FatalError(t, err)\n\t\t\tc.IntermediateCert = \"wrong\"\n\t\t\treturn &newTest{\n\t\t\t\tconfig: c,\n\t\t\t\terr:    errors.New(`error reading \"wrong\": no such file or directory`),\n\t\t\t}\n\t\t},\n\t}\n\n\tfor name, genTestCase := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ttc := genTestCase(t)\n\n\t\t\tauth, err := New(tc.config)\n\t\t\tif err != nil {\n\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.err.Error())\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif assert.Nil(t, tc.err) {\n\t\t\t\t\tsum := sha256.Sum256(auth.rootX509Certs[0].Raw)\n\t\t\t\t\troot, ok := auth.certificates.Load(hex.EncodeToString(sum[:]))\n\t\t\t\t\tassert.Fatal(t, ok)\n\t\t\t\t\tassert.Equals(t, auth.rootX509Certs[0], root)\n\n\t\t\t\t\tassert.True(t, auth.initOnce)\n\t\t\t\t\tassert.NotNil(t, auth.x509CAService)\n\t\t\t\t\tfor _, p := range tc.config.AuthorityConfig.Provisioners {\n\t\t\t\t\t\tvar _p provisioner.Interface\n\t\t\t\t\t\t_p, ok = auth.provisioners.Load(p.GetID())\n\t\t\t\t\t\tassert.True(t, ok)\n\t\t\t\t\t\tassert.Equals(t, p, _p)\n\t\t\t\t\t\tvar kid, encryptedKey string\n\t\t\t\t\t\tif kid, encryptedKey, ok = p.GetEncryptedKey(); ok {\n\t\t\t\t\t\t\tvar key string\n\t\t\t\t\t\t\tkey, ok = auth.provisioners.LoadEncryptedKey(kid)\n\t\t\t\t\t\t\tassert.True(t, ok)\n\t\t\t\t\t\t\tassert.Equals(t, encryptedKey, key)\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\t// sanity check\n\t\t\t\t\t_, ok = auth.provisioners.Load(\"fooo\")\n\t\t\t\t\tassert.False(t, ok)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestAuthorityNew_bundles(t *testing.T) {\n\tca0, err := minica.New()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tca1, err := minica.New()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tca2, err := minica.New()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\trootPath := t.TempDir()\n\twriteCert := func(fn string, certs ...*x509.Certificate) error {\n\t\tvar b []byte\n\t\tfor _, crt := range certs {\n\t\t\tb = append(b, pem.EncodeToMemory(&pem.Block{\n\t\t\t\tType:  \"CERTIFICATE\",\n\t\t\t\tBytes: crt.Raw,\n\t\t\t})...)\n\t\t}\n\t\treturn os.WriteFile(filepath.Join(rootPath, fn), b, 0600)\n\t}\n\twriteKey := func(fn string, signer crypto.Signer) error {\n\t\t_, err := pemutil.Serialize(signer, pemutil.ToFile(filepath.Join(rootPath, fn), 0600))\n\t\treturn err\n\t}\n\n\tif err := writeCert(\"root0.crt\", ca0.Root); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := writeCert(\"int0.crt\", ca0.Intermediate); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := writeKey(\"int0.key\", ca0.Signer); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := writeCert(\"root1.crt\", ca1.Root); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := writeCert(\"int1.crt\", ca1.Intermediate); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := writeKey(\"int1.key\", ca1.Signer); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := writeCert(\"bundle0.crt\", ca0.Root, ca1.Root); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := writeCert(\"bundle1.crt\", ca1.Root, ca2.Root); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\ttests := []struct {\n\t\tname    string\n\t\tconfig  *config.Config\n\t\twantErr bool\n\t}{\n\t\t{\"ok ca0\", &config.Config{\n\t\t\tAddress:          \"127.0.0.1:443\",\n\t\t\tRoot:             []string{filepath.Join(rootPath, \"root0.crt\")},\n\t\t\tIntermediateCert: filepath.Join(rootPath, \"int0.crt\"),\n\t\t\tIntermediateKey:  filepath.Join(rootPath, \"int0.key\"),\n\t\t\tDNSNames:         []string{\"127.0.0.1\"},\n\t\t\tAuthorityConfig:  &AuthConfig{},\n\t\t}, false},\n\t\t{\"ok bundle\", &config.Config{\n\t\t\tAddress:          \"127.0.0.1:443\",\n\t\t\tRoot:             []string{filepath.Join(rootPath, \"bundle0.crt\")},\n\t\t\tIntermediateCert: filepath.Join(rootPath, \"int0.crt\"),\n\t\t\tIntermediateKey:  filepath.Join(rootPath, \"int0.key\"),\n\t\t\tDNSNames:         []string{\"127.0.0.1\"},\n\t\t\tAuthorityConfig:  &AuthConfig{},\n\t\t}, false},\n\t\t{\"ok federated ca1\", &config.Config{\n\t\t\tAddress:          \"127.0.0.1:443\",\n\t\t\tRoot:             []string{filepath.Join(rootPath, \"root0.crt\")},\n\t\t\tFederatedRoots:   []string{filepath.Join(rootPath, \"root1.crt\")},\n\t\t\tIntermediateCert: filepath.Join(rootPath, \"int0.crt\"),\n\t\t\tIntermediateKey:  filepath.Join(rootPath, \"int0.key\"),\n\t\t\tDNSNames:         []string{\"127.0.0.1\"},\n\t\t\tAuthorityConfig:  &AuthConfig{},\n\t\t}, false},\n\t\t{\"ok federated bundle\", &config.Config{\n\t\t\tAddress:          \"127.0.0.1:443\",\n\t\t\tRoot:             []string{filepath.Join(rootPath, \"root0.crt\")},\n\t\t\tFederatedRoots:   []string{filepath.Join(rootPath, \"bundle1.crt\")},\n\t\t\tIntermediateCert: filepath.Join(rootPath, \"int0.crt\"),\n\t\t\tIntermediateKey:  filepath.Join(rootPath, \"int0.key\"),\n\t\t\tDNSNames:         []string{\"127.0.0.1\"},\n\t\t\tAuthorityConfig:  &AuthConfig{},\n\t\t}, false},\n\t\t{\"fail root\", &config.Config{\n\t\t\tAddress:          \"127.0.0.1:443\",\n\t\t\tRoot:             []string{filepath.Join(rootPath, \"missing.crt\")},\n\t\t\tIntermediateCert: filepath.Join(rootPath, \"int0.crt\"),\n\t\t\tIntermediateKey:  filepath.Join(rootPath, \"int0.key\"),\n\t\t\tDNSNames:         []string{\"127.0.0.1\"},\n\t\t\tAuthorityConfig:  &AuthConfig{},\n\t\t}, true},\n\t\t{\"fail federated\", &config.Config{\n\t\t\tAddress:          \"127.0.0.1:443\",\n\t\t\tRoot:             []string{filepath.Join(rootPath, \"root0.crt\")},\n\t\t\tFederatedRoots:   []string{filepath.Join(rootPath, \"missing.crt\")},\n\t\t\tIntermediateCert: filepath.Join(rootPath, \"int0.crt\"),\n\t\t\tIntermediateKey:  filepath.Join(rootPath, \"int0.key\"),\n\t\t\tDNSNames:         []string{\"127.0.0.1\"},\n\t\t\tAuthorityConfig:  &AuthConfig{},\n\t\t}, true},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\t_, err := New(tt.config)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"New() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestAuthority_GetDatabase(t *testing.T) {\n\tauth := testAuthority(t)\n\tauthWithDatabase, err := New(auth.config, WithDatabase(auth.db))\n\tassert.FatalError(t, err)\n\n\ttests := []struct {\n\t\tname string\n\t\tauth *Authority\n\t\twant db.AuthDB\n\t}{\n\t\t{\"ok\", auth, auth.db},\n\t\t{\"ok WithDatabase\", authWithDatabase, auth.db},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif got := tt.auth.GetDatabase(); !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"Authority.GetDatabase() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewEmbedded(t *testing.T) {\n\tcaPEM, err := os.ReadFile(\"testdata/certs/root_ca.crt\")\n\tassert.FatalError(t, err)\n\n\tcrt, err := pemutil.ReadCertificate(\"testdata/certs/intermediate_ca.crt\")\n\tassert.FatalError(t, err)\n\tkey, err := pemutil.Read(\"testdata/secrets/intermediate_ca_key\", pemutil.WithPassword([]byte(\"pass\")))\n\tassert.FatalError(t, err)\n\n\ttype args struct {\n\t\topts []Option\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\targs    args\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", args{[]Option{WithX509RootBundle(caPEM), WithX509Signer(crt, key.(crypto.Signer))}}, false},\n\t\t{\"ok empty config\", args{[]Option{WithConfig(&Config{}), WithX509RootBundle(caPEM), WithX509Signer(crt, key.(crypto.Signer))}}, false},\n\t\t{\"ok config file\", args{[]Option{WithConfigFile(\"../ca/testdata/ca.json\")}}, false},\n\t\t{\"ok config\", args{[]Option{WithConfig(&Config{\n\t\t\tRoot:             []string{\"testdata/certs/root_ca.crt\"},\n\t\t\tIntermediateCert: \"testdata/certs/intermediate_ca.crt\",\n\t\t\tIntermediateKey:  \"testdata/secrets/intermediate_ca_key\",\n\t\t\tPassword:         \"pass\",\n\t\t\tAuthorityConfig:  &AuthConfig{},\n\t\t})}}, false},\n\t\t{\"fail options\", args{[]Option{WithX509RootBundle([]byte(\"bad data\"))}}, true},\n\t\t{\"fail missing config\", args{[]Option{WithConfig(nil), WithX509RootBundle(caPEM), WithX509Signer(crt, key.(crypto.Signer))}}, true},\n\t\t{\"fail missing root\", args{[]Option{WithX509Signer(crt, key.(crypto.Signer))}}, true},\n\t\t{\"fail missing signer\", args{[]Option{WithX509RootBundle(caPEM)}}, true},\n\t\t{\"fail missing root file\", args{[]Option{WithConfig(&Config{\n\t\t\tIntermediateCert: \"testdata/certs/intermediate_ca.crt\",\n\t\t\tIntermediateKey:  \"testdata/secrets/intermediate_ca_key\",\n\t\t\tPassword:         \"pass\",\n\t\t\tAuthorityConfig:  &AuthConfig{},\n\t\t})}}, true},\n\t\t{\"fail missing issuer\", args{[]Option{WithConfig(&Config{\n\t\t\tRoot:            []string{\"testdata/certs/root_ca.crt\"},\n\t\t\tIntermediateKey: \"testdata/secrets/intermediate_ca_key\",\n\t\t\tPassword:        \"pass\",\n\t\t\tAuthorityConfig: &AuthConfig{},\n\t\t})}}, true},\n\t\t{\"fail missing signer\", args{[]Option{WithConfig(&Config{\n\t\t\tRoot:             []string{\"testdata/certs/root_ca.crt\"},\n\t\t\tIntermediateCert: \"testdata/certs/intermediate_ca.crt\",\n\t\t\tPassword:         \"pass\",\n\t\t\tAuthorityConfig:  &AuthConfig{},\n\t\t})}}, true},\n\t\t{\"fail bad password\", args{[]Option{WithConfig(&Config{\n\t\t\tRoot:             []string{\"testdata/certs/root_ca.crt\"},\n\t\t\tIntermediateCert: \"testdata/certs/intermediate_ca.crt\",\n\t\t\tIntermediateKey:  \"testdata/secrets/intermediate_ca_key\",\n\t\t\tPassword:         \"bad\",\n\t\t\tAuthorityConfig:  &AuthConfig{},\n\t\t})}}, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := NewEmbedded(tt.args.opts...)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"NewEmbedded() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif err == nil {\n\t\t\t\tassert.True(t, got.initOnce)\n\t\t\t\tassert.NotNil(t, got.rootX509Certs)\n\t\t\t\tassert.NotNil(t, got.x509CAService)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewEmbedded_Sign(t *testing.T) {\n\tcaPEM, err := os.ReadFile(\"testdata/certs/root_ca.crt\")\n\tassert.FatalError(t, err)\n\n\tcrt, err := pemutil.ReadCertificate(\"testdata/certs/intermediate_ca.crt\")\n\tassert.FatalError(t, err)\n\tkey, err := pemutil.Read(\"testdata/secrets/intermediate_ca_key\", pemutil.WithPassword([]byte(\"pass\")))\n\tassert.FatalError(t, err)\n\n\ta, err := NewEmbedded(WithX509RootBundle(caPEM), WithX509Signer(crt, key.(crypto.Signer)))\n\tassert.FatalError(t, err)\n\n\t// Sign\n\tcr, err := x509.CreateCertificateRequest(rand.Reader, &x509.CertificateRequest{\n\t\tDNSNames: []string{\"foo.bar.zar\"},\n\t}, key)\n\tassert.FatalError(t, err)\n\tcsr, err := x509.ParseCertificateRequest(cr)\n\tassert.FatalError(t, err)\n\n\tcert, err := a.SignWithContext(context.Background(), csr, provisioner.SignOptions{})\n\tassert.FatalError(t, err)\n\tassert.Equals(t, []string{\"foo.bar.zar\"}, cert[0].DNSNames)\n\tassert.Equals(t, crt, cert[1])\n}\n\nfunc TestNewEmbedded_GetTLSCertificate(t *testing.T) {\n\tcaPEM, err := os.ReadFile(\"testdata/certs/root_ca.crt\")\n\tassert.FatalError(t, err)\n\n\tcrt, err := pemutil.ReadCertificate(\"testdata/certs/intermediate_ca.crt\")\n\tassert.FatalError(t, err)\n\tkey, err := pemutil.Read(\"testdata/secrets/intermediate_ca_key\", pemutil.WithPassword([]byte(\"pass\")))\n\tassert.FatalError(t, err)\n\n\ta, err := NewEmbedded(WithX509RootBundle(caPEM), WithX509Signer(crt, key.(crypto.Signer)))\n\tassert.FatalError(t, err)\n\n\t// GetTLSCertificate\n\tcert, err := a.GetTLSCertificate()\n\tassert.FatalError(t, err)\n\tassert.Equals(t, []string{\"localhost\"}, cert.Leaf.DNSNames)\n\tassert.True(t, cert.Leaf.IPAddresses[0].Equal(net.ParseIP(\"127.0.0.1\")))\n\tassert.True(t, cert.Leaf.IPAddresses[1].Equal(net.ParseIP(\"::1\")))\n}\n\nfunc TestAuthority_CloseForReload(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tauth *Authority\n\t}{\n\t\t{\"ok\", testAuthority(t)},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ttt.auth.CloseForReload()\n\t\t})\n\t}\n}\n\nfunc testScepAuthority(t *testing.T, opts ...Option) *Authority {\n\tp := provisioner.List{\n\t\t&provisioner.SCEP{\n\t\t\tName: \"scep1\",\n\t\t\tType: \"SCEP\",\n\t\t},\n\t}\n\tc := &Config{\n\t\tAddress:          \"127.0.0.1:8443\",\n\t\tInsecureAddress:  \"127.0.0.1:8080\",\n\t\tRoot:             []string{\"testdata/scep/root.crt\"},\n\t\tIntermediateCert: \"testdata/scep/intermediate.crt\",\n\t\tIntermediateKey:  \"testdata/scep/intermediate.key\",\n\t\tDNSNames:         []string{\"example.com\"},\n\t\tPassword:         \"pass\",\n\t\tAuthorityConfig: &AuthConfig{\n\t\t\tProvisioners: p,\n\t\t},\n\t}\n\ta, err := New(c, opts...)\n\tassert.FatalError(t, err)\n\treturn a\n}\n\nfunc TestAuthority_GetSCEP(t *testing.T) {\n\t_ = testScepAuthority(t)\n\tp := provisioner.List{\n\t\t&provisioner.SCEP{\n\t\t\tName: \"scep1\",\n\t\t\tType: \"SCEP\",\n\t\t},\n\t}\n\ttype fields struct {\n\t\tconfig *Config\n\t}\n\ttests := []struct {\n\t\tname        string\n\t\tfields      fields\n\t\twantService bool\n\t\twantErr     bool\n\t}{\n\t\t{\n\t\t\tname: \"ok\",\n\t\t\tfields: fields{\n\t\t\t\tconfig: &Config{\n\t\t\t\t\tAddress:          \"127.0.0.1:8443\",\n\t\t\t\t\tInsecureAddress:  \"127.0.0.1:8080\",\n\t\t\t\t\tRoot:             []string{\"testdata/scep/root.crt\"},\n\t\t\t\t\tIntermediateCert: \"testdata/scep/intermediate.crt\",\n\t\t\t\t\tIntermediateKey:  \"testdata/scep/intermediate.key\",\n\t\t\t\t\tDNSNames:         []string{\"example.com\"},\n\t\t\t\t\tPassword:         \"pass\",\n\t\t\t\t\tAuthorityConfig: &AuthConfig{\n\t\t\t\t\t\tProvisioners: p,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twantService: true,\n\t\t\twantErr:     false,\n\t\t},\n\t\t{\n\t\t\tname: \"wrong password\",\n\t\t\tfields: fields{\n\t\t\t\tconfig: &Config{\n\t\t\t\t\tAddress:          \"127.0.0.1:8443\",\n\t\t\t\t\tInsecureAddress:  \"127.0.0.1:8080\",\n\t\t\t\t\tRoot:             []string{\"testdata/scep/root.crt\"},\n\t\t\t\t\tIntermediateCert: \"testdata/scep/intermediate.crt\",\n\t\t\t\t\tIntermediateKey:  \"testdata/scep/intermediate.key\",\n\t\t\t\t\tDNSNames:         []string{\"example.com\"},\n\t\t\t\t\tPassword:         \"wrongpass\",\n\t\t\t\t\tAuthorityConfig: &AuthConfig{\n\t\t\t\t\t\tProvisioners: p,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twantService: false,\n\t\t\twantErr:     true,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ta, err := New(tt.fields.config)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"Authority.New(), error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif tt.wantService {\n\t\t\t\tif got := a.GetSCEP(); (got != nil) != tt.wantService {\n\t\t\t\t\tt.Errorf(\"Authority.GetSCEPService() = %v, wantService %v\", got, tt.wantService)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestAuthority_GetID(t *testing.T) {\n\ttype fields struct {\n\t\tauthorityID string\n\t}\n\ttests := []struct {\n\t\tname   string\n\t\tfields fields\n\t\twant   string\n\t}{\n\t\t{\"ok\", fields{\"\"}, \"00000000-0000-0000-0000-000000000000\"},\n\t\t{\"ok with id\", fields{\"10b9a431-ed3b-4a5f-abee-ec35119b65e7\"}, \"10b9a431-ed3b-4a5f-abee-ec35119b65e7\"},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ta := &Authority{\n\t\t\t\tconfig: &config.Config{\n\t\t\t\t\tAuthorityConfig: &config.AuthConfig{\n\t\t\t\t\t\tAuthorityID: tt.fields.authorityID,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t\tif got := a.GetID(); got != tt.want {\n\t\t\t\tt.Errorf(\"Authority.GetID() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "authority/authorize.go",
    "content": "package authority\n\nimport (\n\t\"context\"\n\t\"crypto/sha256\"\n\t\"crypto/x509\"\n\t\"encoding/hex\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n\t\"github.com/smallstep/certificates/authority/admin\"\n\t\"github.com/smallstep/certificates/authority/provisioner\"\n\t\"github.com/smallstep/certificates/errs\"\n\t\"github.com/smallstep/linkedca\"\n\t\"go.step.sm/crypto/jose\"\n\t\"golang.org/x/crypto/ssh\"\n)\n\n// Claims extends jose.Claims with step attributes.\ntype Claims struct {\n\tjose.Claims\n\tSANs  []string `json:\"sans,omitempty\"`\n\tEmail string   `json:\"email,omitempty\"`\n\tNonce string   `json:\"nonce,omitempty\"`\n}\n\ntype skipTokenReuseKey struct{}\n\n// NewContextWithSkipTokenReuse creates a new context from ctx and attaches a\n// value to skip the token reuse.\nfunc NewContextWithSkipTokenReuse(ctx context.Context) context.Context {\n\treturn context.WithValue(ctx, skipTokenReuseKey{}, true)\n}\n\n// SkipTokenReuseFromContext returns if the token reuse needs to be ignored.\nfunc SkipTokenReuseFromContext(ctx context.Context) bool {\n\tm, _ := ctx.Value(skipTokenReuseKey{}).(bool)\n\treturn m\n}\n\n// getProvisionerFromToken extracts a provisioner from the given token without\n// doing any token validation.\nfunc (a *Authority) getProvisionerFromToken(token string) (provisioner.Interface, *Claims, error) {\n\ttok, err := jose.ParseSigned(token)\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"error parsing token: %w\", err)\n\t}\n\n\t// Get claims w/out verification. We need to look up the provisioner\n\t// key in order to verify the claims and we need the issuer from the claims\n\t// before we can look up the provisioner.\n\tvar claims Claims\n\tif err := tok.UnsafeClaimsWithoutVerification(&claims); err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"error unmarshaling token: %w\", err)\n\t}\n\n\t// This method will also validate the audiences for JWK provisioners.\n\tp, ok := a.provisioners.LoadByToken(tok, &claims.Claims)\n\tif !ok {\n\t\treturn nil, nil, fmt.Errorf(\"provisioner not found or invalid audience (%s)\", strings.Join(claims.Audience, \", \"))\n\t}\n\t// If the provisioner is disabled, send an appropriate message to the client\n\tif _, ok := p.(provisioner.Uninitialized); ok {\n\t\treturn nil, nil, errs.New(http.StatusUnauthorized, \"provisioner %q is disabled due to an initialization error\", p.GetName())\n\t}\n\n\treturn p, &claims, nil\n}\n\n// authorizeToken parses the token and returns the provisioner used to generate\n// the token. This method enforces the One-Time use policy (tokens can only be\n// used once).\nfunc (a *Authority) authorizeToken(ctx context.Context, token string) (provisioner.Interface, error) {\n\tp, claims, err := a.getProvisionerFromToken(token)\n\tif err != nil {\n\t\treturn nil, errs.UnauthorizedErr(err)\n\t}\n\n\t// TODO: use new persistence layer abstraction.\n\t// Do not accept tokens issued before the start of the ca.\n\t// This check is meant as a stopgap solution to the current lack of a persistence layer.\n\tif a.config.AuthorityConfig != nil && !a.config.AuthorityConfig.DisableIssuedAtCheck {\n\t\tif claims.IssuedAt != nil && claims.IssuedAt.Time().Before(a.startTime) {\n\t\t\treturn nil, errs.Unauthorized(\"token issued before the bootstrap of certificate authority\")\n\t\t}\n\t}\n\n\t// Store the token to protect against reuse unless it's skipped.\n\t// If we cannot get a token id from the provisioner, just hash the token.\n\tif !SkipTokenReuseFromContext(ctx) {\n\t\tif err := a.UseToken(ctx, token, p); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn p, nil\n}\n\n// AuthorizeAdminToken authorize an Admin token.\nfunc (a *Authority) AuthorizeAdminToken(r *http.Request, token string) (*linkedca.Admin, error) {\n\tjwt, err := jose.ParseSigned(token)\n\tif err != nil {\n\t\treturn nil, admin.WrapError(admin.ErrorUnauthorizedType, err, \"adminHandler.authorizeToken; error parsing x5c token\")\n\t}\n\n\tverifiedChains, err := jwt.Headers[0].Certificates(x509.VerifyOptions{\n\t\tRoots:     a.rootX509CertPool,\n\t\tKeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},\n\t})\n\tif err != nil {\n\t\treturn nil, admin.WrapError(admin.ErrorUnauthorizedType, err,\n\t\t\t\"adminHandler.authorizeToken; error verifying x5c certificate chain in token\")\n\t}\n\tleaf := verifiedChains[0][0]\n\n\tif leaf.KeyUsage&x509.KeyUsageDigitalSignature == 0 {\n\t\treturn nil, admin.NewError(admin.ErrorUnauthorizedType, \"adminHandler.authorizeToken; certificate used to sign x5c token cannot be used for digital signature\")\n\t}\n\n\t// Using the leaf certificates key to validate the claims accomplishes two\n\t// things:\n\t//   1. Asserts that the private key used to sign the token corresponds\n\t//      to the public certificate in the `x5c` header of the token.\n\t//   2. Asserts that the claims are valid - have not been tampered with.\n\tvar claims jose.Claims\n\tif err := jwt.Claims(leaf.PublicKey, &claims); err != nil {\n\t\treturn nil, admin.WrapError(admin.ErrorUnauthorizedType, err, \"adminHandler.authorizeToken; error parsing x5c claims\")\n\t}\n\n\tprov, err := a.LoadProvisionerByCertificate(leaf)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Check that the token has not been used.\n\tif err := a.UseToken(r.Context(), token, prov); err != nil {\n\t\treturn nil, admin.WrapError(admin.ErrorUnauthorizedType, err, \"adminHandler.authorizeToken; error with reuse token\")\n\t}\n\n\t// According to \"rfc7519 JSON Web Token\" acceptable skew should be no\n\t// more than a few minutes.\n\tif err := claims.ValidateWithLeeway(jose.Expected{\n\t\tTime: time.Now().UTC(),\n\t}, time.Minute); err != nil {\n\t\treturn nil, admin.WrapError(admin.ErrorUnauthorizedType, err, \"x5c.authorizeToken; invalid x5c claims\")\n\t}\n\n\t// validate audience: path matches the current path\n\tif !matchesAudience(claims.Audience, a.config.Audience(r.URL.Path)) {\n\t\treturn nil, admin.NewError(admin.ErrorUnauthorizedType, \"x5c.authorizeToken; x5c token has invalid audience claim (aud)\")\n\t}\n\n\t// validate issuer: old versions used the provisioner name, new version uses\n\t// 'step-admin-client/1.0'\n\tif claims.Issuer != \"step-admin-client/1.0\" && claims.Issuer != prov.GetName() {\n\t\treturn nil, admin.NewError(admin.ErrorUnauthorizedType, \"x5c.authorizeToken; x5c token has invalid issuer claim (iss)\")\n\t}\n\n\tif claims.Subject == \"\" {\n\t\treturn nil, admin.NewError(admin.ErrorUnauthorizedType, \"x5c.authorizeToken; x5c token subject cannot be empty\")\n\t}\n\n\tvar (\n\t\tok  bool\n\t\tadm *linkedca.Admin\n\t)\n\tadminFound := false\n\tadminSANs := append([]string{leaf.Subject.CommonName}, leaf.DNSNames...)\n\tadminSANs = append(adminSANs, leaf.EmailAddresses...)\n\tfor _, san := range adminSANs {\n\t\tif adm, ok = a.LoadAdminBySubProv(san, prov.GetName()); ok {\n\t\t\tadminFound = true\n\t\t\tbreak\n\t\t}\n\t}\n\tif !adminFound {\n\t\treturn nil, admin.NewError(admin.ErrorUnauthorizedType,\n\t\t\t\"adminHandler.authorizeToken; unable to load admin with subject(s) %s and provisioner '%s'\",\n\t\t\tadminSANs, prov.GetName())\n\t}\n\n\tif strings.HasPrefix(r.URL.Path, \"/admin/admins\") && (r.Method != \"GET\") && adm.Type != linkedca.Admin_SUPER_ADMIN {\n\t\treturn nil, admin.NewError(admin.ErrorUnauthorizedType, \"must have super admin access to make this request\")\n\t}\n\n\treturn adm, nil\n}\n\n// UseToken stores the token to protect against reuse.\n//\n// This method currently ignores most errors coming from the GetTokenID because\n// the token is already validated. But it should specifically ignore the errors\n// provisioner.ErrAllowTokenReuse, provisioner.ErrNotImplemented, and\n// provisioner.ErrTokenFlowNotSupported unless this latter one used in a renewal\n// flow without mTLS.\nfunc (a *Authority) UseToken(ctx context.Context, token string, prov provisioner.Interface) error {\n\treuseKey, err := prov.GetTokenID(token)\n\tif err != nil {\n\t\t// Fail on ErrTokenFlowNotSupported but allow x5cInsecure renew token\n\t\tif errors.Is(err, provisioner.ErrTokenFlowNotSupported) && provisioner.RenewMethod != provisioner.MethodFromContext(ctx) {\n\t\t\treturn errs.BadRequest(\"token flow is not supported\")\n\t\t}\n\n\t\treturn nil\n\t}\n\n\tif reuseKey == \"\" {\n\t\tsum := sha256.Sum256([]byte(token))\n\t\treuseKey = strings.ToLower(hex.EncodeToString(sum[:]))\n\t}\n\n\tok, err := a.db.UseToken(reuseKey, token)\n\tif err != nil {\n\t\treturn errs.Wrap(http.StatusInternalServerError, err, \"failed when attempting to store token\")\n\t}\n\tif !ok {\n\t\treturn errs.Unauthorized(\"token already used\")\n\t}\n\n\treturn nil\n}\n\n// Authorize grabs the method from the context and authorizes the request by\n// validating the one-time-token.\nfunc (a *Authority) Authorize(ctx context.Context, token string) ([]provisioner.SignOption, error) {\n\tvar opts = []interface{}{errs.WithKeyVal(\"token\", token)}\n\n\tswitch m := provisioner.MethodFromContext(ctx); m {\n\tcase provisioner.SignMethod, provisioner.SignIdentityMethod:\n\t\tsignOpts, err := a.authorizeSign(ctx, token)\n\t\treturn signOpts, errs.Wrap(http.StatusInternalServerError, err, \"authority.Authorize\", opts...)\n\tcase provisioner.RevokeMethod:\n\t\treturn nil, errs.Wrap(http.StatusInternalServerError, a.authorizeRevoke(ctx, token), \"authority.Authorize\", opts...)\n\tcase provisioner.SSHSignMethod:\n\t\tif a.sshCAHostCertSignKey == nil && a.sshCAUserCertSignKey == nil {\n\t\t\treturn nil, errs.NotImplemented(\"authority.Authorize; ssh certificate flows are not enabled\", opts...)\n\t\t}\n\t\tsignOpts, err := a.authorizeSSHSign(ctx, token)\n\t\treturn signOpts, errs.Wrap(http.StatusInternalServerError, err, \"authority.Authorize\", opts...)\n\tcase provisioner.SSHRenewMethod:\n\t\tif a.sshCAHostCertSignKey == nil && a.sshCAUserCertSignKey == nil {\n\t\t\treturn nil, errs.NotImplemented(\"authority.Authorize; ssh certificate flows are not enabled\", opts...)\n\t\t}\n\t\t_, err := a.authorizeSSHRenew(ctx, token)\n\t\treturn nil, errs.Wrap(http.StatusInternalServerError, err, \"authority.Authorize\", opts...)\n\tcase provisioner.SSHRevokeMethod:\n\t\treturn nil, errs.Wrap(http.StatusInternalServerError, a.authorizeSSHRevoke(ctx, token), \"authority.Authorize\", opts...)\n\tcase provisioner.SSHRekeyMethod:\n\t\tif a.sshCAHostCertSignKey == nil && a.sshCAUserCertSignKey == nil {\n\t\t\treturn nil, errs.NotImplemented(\"authority.Authorize; ssh certificate flows are not enabled\", opts...)\n\t\t}\n\t\t_, signOpts, err := a.authorizeSSHRekey(ctx, token)\n\t\treturn signOpts, errs.Wrap(http.StatusInternalServerError, err, \"authority.Authorize\", opts...)\n\tdefault:\n\t\treturn nil, errs.InternalServer(\"authority.Authorize; method %d is not supported\", append([]interface{}{m}, opts...)...)\n\t}\n}\n\n// authorizeSign loads the provisioner from the token and calls the provisioner\n// AuthorizeSign method. Returns a list of methods to apply to the signing flow.\nfunc (a *Authority) authorizeSign(ctx context.Context, token string) ([]provisioner.SignOption, error) {\n\tp, err := a.authorizeToken(ctx, token)\n\tif err != nil {\n\t\treturn nil, errs.Wrap(http.StatusInternalServerError, err, \"authority.authorizeSign\")\n\t}\n\tsignOpts, err := p.AuthorizeSign(ctx, token)\n\tif err != nil {\n\t\treturn nil, errs.Wrap(http.StatusInternalServerError, err, \"authority.authorizeSign\")\n\t}\n\treturn signOpts, nil\n}\n\n// AuthorizeSign authorizes a signature request by validating and authenticating\n// a token that must be sent w/ the request.\n//\n// Deprecated: Use Authorize(context.Context, string) ([]provisioner.SignOption, error).\nfunc (a *Authority) AuthorizeSign(token string) ([]provisioner.SignOption, error) {\n\tctx := NewContext(context.Background(), a)\n\tctx = provisioner.NewContextWithMethod(ctx, provisioner.SignMethod)\n\treturn a.Authorize(ctx, token)\n}\n\n// authorizeRevoke locates the provisioner used to generate the authenticating\n// token and then performs the token validation flow.\nfunc (a *Authority) authorizeRevoke(ctx context.Context, token string) error {\n\tp, err := a.authorizeToken(ctx, token)\n\tif err != nil {\n\t\treturn errs.Wrap(http.StatusInternalServerError, err, \"authority.authorizeRevoke\")\n\t}\n\tif err := p.AuthorizeRevoke(ctx, token); err != nil {\n\t\treturn errs.Wrap(http.StatusInternalServerError, err, \"authority.authorizeRevoke\")\n\t}\n\treturn nil\n}\n\n// authorizeRenew locates the provisioner (using the provisioner extension in the cert), and checks\n// if for the configured provisioner, the renewal is enabled or not. If the\n// extra extension cannot be found, authorize the renewal by default.\n//\n// TODO(mariano): should we authorize by default?\nfunc (a *Authority) authorizeRenew(ctx context.Context, cert *x509.Certificate) (provisioner.Interface, error) {\n\tserial := cert.SerialNumber.String()\n\tvar opts = []interface{}{errs.WithKeyVal(\"serialNumber\", serial)}\n\n\tisRevoked, err := a.IsRevoked(serial)\n\tif err != nil {\n\t\treturn nil, errs.Wrap(http.StatusInternalServerError, err, \"authority.authorizeRenew\", opts...)\n\t}\n\tif isRevoked {\n\t\treturn nil, errs.Unauthorized(\"authority.authorizeRenew: certificate has been revoked\", opts...)\n\t}\n\tp, err := a.LoadProvisionerByCertificate(cert)\n\tif err != nil {\n\t\tvar ok bool\n\t\t// For backward compatibility this method will also succeed if the\n\t\t// certificate does not have a provisioner extension. LoadByCertificate\n\t\t// returns the noop provisioner if this happens, and it allows\n\t\t// certificate renewals.\n\t\tif p, ok = a.provisioners.LoadByCertificate(cert); !ok {\n\t\t\treturn nil, errs.Unauthorized(\"authority.authorizeRenew: provisioner not found\", opts...)\n\t\t}\n\t}\n\tif err := p.AuthorizeRenew(ctx, cert); err != nil {\n\t\treturn nil, errs.Wrap(http.StatusInternalServerError, err, \"authority.authorizeRenew\", opts...)\n\t}\n\treturn p, nil\n}\n\n// authorizeSSHCertificate returns an error if the given certificate is revoked.\nfunc (a *Authority) authorizeSSHCertificate(_ context.Context, cert *ssh.Certificate) error {\n\tvar err error\n\tvar isRevoked bool\n\n\tserial := strconv.FormatUint(cert.Serial, 10)\n\tif lca, ok := a.adminDB.(interface {\n\t\tIsSSHRevoked(string) (bool, error)\n\t}); ok {\n\t\tisRevoked, err = lca.IsSSHRevoked(serial)\n\t} else {\n\t\tisRevoked, err = a.db.IsSSHRevoked(serial)\n\t}\n\tif err != nil {\n\t\treturn errs.Wrap(http.StatusInternalServerError, err, \"authority.authorizeSSHCertificate\", errs.WithKeyVal(\"serialNumber\", serial))\n\t}\n\tif isRevoked {\n\t\treturn errs.Unauthorized(\"authority.authorizeSSHCertificate: certificate has been revoked\", errs.WithKeyVal(\"serialNumber\", serial))\n\t}\n\treturn nil\n}\n\n// authorizeSSHSign loads the provisioner from the token, checks that it has not\n// been used again and calls the provisioner AuthorizeSSHSign method. Returns a\n// list of methods to apply to the signing flow.\nfunc (a *Authority) authorizeSSHSign(ctx context.Context, token string) ([]provisioner.SignOption, error) {\n\tp, err := a.authorizeToken(ctx, token)\n\tif err != nil {\n\t\treturn nil, errs.Wrap(http.StatusUnauthorized, err, \"authority.authorizeSSHSign\")\n\t}\n\tsignOpts, err := p.AuthorizeSSHSign(ctx, token)\n\tif err != nil {\n\t\treturn nil, errs.Wrap(http.StatusUnauthorized, err, \"authority.authorizeSSHSign\")\n\t}\n\treturn signOpts, nil\n}\n\n// authorizeSSHRenew authorizes an SSH certificate renewal request, by\n// validating the contents of an SSHPOP token.\nfunc (a *Authority) authorizeSSHRenew(ctx context.Context, token string) (*ssh.Certificate, error) {\n\tp, err := a.authorizeToken(ctx, token)\n\tif err != nil {\n\t\treturn nil, errs.Wrap(http.StatusInternalServerError, err, \"authority.authorizeSSHRenew\")\n\t}\n\tcert, err := p.AuthorizeSSHRenew(ctx, token)\n\tif err != nil {\n\t\treturn nil, errs.Wrap(http.StatusInternalServerError, err, \"authority.authorizeSSHRenew\")\n\t}\n\treturn cert, nil\n}\n\n// authorizeSSHRekey authorizes an SSH certificate rekey request, by\n// validating the contents of an SSHPOP token.\nfunc (a *Authority) authorizeSSHRekey(ctx context.Context, token string) (*ssh.Certificate, []provisioner.SignOption, error) {\n\tp, err := a.authorizeToken(ctx, token)\n\tif err != nil {\n\t\treturn nil, nil, errs.Wrap(http.StatusInternalServerError, err, \"authority.authorizeSSHRekey\")\n\t}\n\tcert, signOpts, err := p.AuthorizeSSHRekey(ctx, token)\n\tif err != nil {\n\t\treturn nil, nil, errs.Wrap(http.StatusInternalServerError, err, \"authority.authorizeSSHRekey\")\n\t}\n\treturn cert, signOpts, nil\n}\n\n// authorizeSSHRevoke authorizes an SSH certificate revoke request, by\n// validating the contents of an SSHPOP token.\nfunc (a *Authority) authorizeSSHRevoke(ctx context.Context, token string) error {\n\tp, err := a.authorizeToken(ctx, token)\n\tif err != nil {\n\t\treturn errs.Wrap(http.StatusInternalServerError, err, \"authority.authorizeSSHRevoke\")\n\t}\n\tif err = p.AuthorizeSSHRevoke(ctx, token); err != nil {\n\t\treturn errs.Wrap(http.StatusInternalServerError, err, \"authority.authorizeSSHRevoke\")\n\t}\n\treturn nil\n}\n\n// AuthorizeRenewToken validates the renew token and returns the leaf\n// certificate in the x5cInsecure header.\nfunc (a *Authority) AuthorizeRenewToken(ctx context.Context, ott string) (*x509.Certificate, error) {\n\tvar claims jose.Claims\n\tjwt, chain, err := jose.ParseX5cInsecure(ott, a.rootX509Certs)\n\tif err != nil {\n\t\treturn nil, errs.UnauthorizedErr(err, errs.WithMessage(\"error validating renew token\"))\n\t}\n\tleaf := chain[0][0]\n\tif err := jwt.Claims(leaf.PublicKey, &claims); err != nil {\n\t\treturn nil, errs.InternalServerErr(err, errs.WithMessage(\"error validating renew token\"))\n\t}\n\n\tp, err := a.LoadProvisionerByCertificate(leaf)\n\tif err != nil {\n\t\treturn nil, errs.Unauthorized(\"error validating renew token: cannot get provisioner from certificate\")\n\t}\n\tif err := a.UseToken(ctx, ott, p); err != nil {\n\t\treturn nil, err\n\t}\n\n\tif err := claims.ValidateWithLeeway(jose.Expected{\n\t\tSubject: leaf.Subject.CommonName,\n\t\tTime:    time.Now().UTC(),\n\t}, time.Minute); err != nil {\n\t\tswitch {\n\t\tcase errors.Is(err, jose.ErrInvalidIssuer):\n\t\t\treturn nil, errs.UnauthorizedErr(err, errs.WithMessage(\"error validating renew token: invalid issuer claim (iss)\"))\n\t\tcase errors.Is(err, jose.ErrInvalidSubject):\n\t\t\treturn nil, errs.UnauthorizedErr(err, errs.WithMessage(\"error validating renew token: invalid subject claim (sub)\"))\n\t\tcase errors.Is(err, jose.ErrNotValidYet):\n\t\t\treturn nil, errs.UnauthorizedErr(err, errs.WithMessage(\"error validating renew token: token not valid yet (nbf)\"))\n\t\tcase errors.Is(err, jose.ErrExpired):\n\t\t\treturn nil, errs.UnauthorizedErr(err, errs.WithMessage(\"error validating renew token: token is expired (exp)\"))\n\t\tcase errors.Is(err, jose.ErrIssuedInTheFuture):\n\t\t\treturn nil, errs.UnauthorizedErr(err, errs.WithMessage(\"error validating renew token: token issued in the future (iat)\"))\n\t\tdefault:\n\t\t\treturn nil, errs.UnauthorizedErr(err, errs.WithMessage(\"error validating renew token\"))\n\t\t}\n\t}\n\n\taudiences := a.config.GetAudiences().Renew\n\tif !matchesAudience(claims.Audience, audiences) && !isRAProvisioner(p) {\n\t\treturn nil, errs.InternalServerErr(jose.ErrInvalidAudience, errs.WithMessage(\"error validating renew token: invalid audience claim (aud)\"))\n\t}\n\n\t// validate issuer: old versions used the provisioner name, new version uses\n\t// 'step-ca-client/1.0'\n\tif claims.Issuer != \"step-ca-client/1.0\" && claims.Issuer != p.GetName() {\n\t\treturn nil, admin.NewError(admin.ErrorUnauthorizedType, \"error validating renew token: invalid issuer claim (iss)\")\n\t}\n\n\treturn leaf, nil\n}\n\n// matchesAudience returns true if A and B share at least one element.\nfunc matchesAudience(as, bs []string) bool {\n\tif len(bs) == 0 || len(as) == 0 {\n\t\treturn false\n\t}\n\n\tfor _, b := range bs {\n\t\tfor _, a := range as {\n\t\t\tif b == a || stripPort(a) == stripPort(b) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\t}\n\treturn false\n}\n\n// stripPort attempts to strip the port from the given url. If parsing the url\n// produces errors it will just return the passed argument.\nfunc stripPort(rawurl string) string {\n\tu, err := url.Parse(rawurl)\n\tif err != nil {\n\t\treturn rawurl\n\t}\n\tu.Host = u.Hostname()\n\treturn u.String()\n}\n"
  },
  {
    "path": "authority/authorize_test.go",
    "content": "package authority\n\nimport (\n\t\"context\"\n\t\"crypto\"\n\t\"crypto/ed25519\"\n\t\"crypto/rand\"\n\t\"crypto/x509\"\n\t\"crypto/x509/pkix\"\n\t\"encoding/asn1\"\n\t\"encoding/base64\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"reflect\"\n\t\"strconv\"\n\t\"testing\"\n\t\"time\"\n\n\t\"golang.org/x/crypto/ssh\"\n\n\t\"go.step.sm/crypto/jose\"\n\t\"go.step.sm/crypto/pemutil\"\n\t\"go.step.sm/crypto/randutil\"\n\t\"go.step.sm/crypto/x509util\"\n\n\t\"github.com/google/uuid\"\n\t\"github.com/smallstep/assert\"\n\t\"github.com/smallstep/certificates/api/render\"\n\t\"github.com/smallstep/certificates/authority/provisioner\"\n\t\"github.com/smallstep/certificates/db\"\n\t\"github.com/smallstep/certificates/errs\"\n)\n\nvar testAudiences = provisioner.Audiences{\n\tSign:      []string{\"https://example.com/1.0/sign\", \"https://example.com/sign\"},\n\tRevoke:    []string{\"https://example.com/1.0/revoke\", \"https://example.com/revoke\"},\n\tSSHSign:   []string{\"https://example.com/1.0/ssh/sign\"},\n\tSSHRevoke: []string{\"https://example.com/1.0/ssh/revoke\"},\n\tSSHRenew:  []string{\"https://example.com/1.0/ssh/renew\"},\n\tSSHRekey:  []string{\"https://example.com/1.0/ssh/rekey\"},\n}\n\ntype tokOption func(*jose.SignerOptions) error\n\nfunc withSSHPOPFile(cert *ssh.Certificate) tokOption {\n\treturn func(so *jose.SignerOptions) error {\n\t\tso.WithHeader(\"sshpop\", base64.StdEncoding.EncodeToString(cert.Marshal()))\n\t\treturn nil\n\t}\n}\n\nfunc generateToken(sub, iss, aud string, sans []string, iat time.Time, jwk *jose.JSONWebKey, tokOpts ...tokOption) (string, error) {\n\tso := new(jose.SignerOptions)\n\tso.WithType(\"JWT\")\n\tso.WithHeader(\"kid\", jwk.KeyID)\n\n\tfor _, o := range tokOpts {\n\t\tif err := o(so); err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t}\n\n\tsig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: jwk.Key}, so)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tid, err := randutil.ASCII(64)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tclaims := struct {\n\t\tjose.Claims\n\t\tSANS []string `json:\"sans\"`\n\t}{\n\t\tClaims: jose.Claims{\n\t\t\tID:        id,\n\t\t\tSubject:   sub,\n\t\t\tIssuer:    iss,\n\t\t\tIssuedAt:  jose.NewNumericDate(iat),\n\t\t\tNotBefore: jose.NewNumericDate(iat),\n\t\t\tExpiry:    jose.NewNumericDate(iat.Add(5 * time.Minute)),\n\t\t\tAudience:  []string{aud},\n\t\t},\n\t\tSANS: sans,\n\t}\n\treturn jose.Signed(sig).Claims(claims).CompactSerialize()\n}\n\nfunc generateCustomToken(sub, iss, aud string, jwk *jose.JSONWebKey, extraHeaders, extraClaims map[string]any) (string, error) {\n\tso := new(jose.SignerOptions)\n\tso.WithType(\"JWT\")\n\tso.WithHeader(\"kid\", jwk.KeyID)\n\n\tfor k, v := range extraHeaders {\n\t\tso.WithHeader(jose.HeaderKey(k), v)\n\t}\n\n\tsig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: jwk.Key}, so)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tid, err := randutil.ASCII(64)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tiat := time.Now()\n\tclaims := jose.Claims{\n\t\tID:        id,\n\t\tSubject:   sub,\n\t\tIssuer:    iss,\n\t\tIssuedAt:  jose.NewNumericDate(iat),\n\t\tNotBefore: jose.NewNumericDate(iat),\n\t\tExpiry:    jose.NewNumericDate(iat.Add(5 * time.Minute)),\n\t\tAudience:  []string{aud},\n\t}\n\n\treturn jose.Signed(sig).Claims(claims).Claims(extraClaims).CompactSerialize()\n}\n\nfunc TestAuthority_authorizeToken(t *testing.T) {\n\ta := testAuthority(t)\n\n\tjwk, err := jose.ReadKey(\"testdata/secrets/step_cli_key_priv.jwk\", jose.WithPassword([]byte(\"pass\")))\n\tassert.FatalError(t, err)\n\n\tsig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: jwk.Key},\n\t\t(&jose.SignerOptions{}).WithType(\"JWT\").WithHeader(\"kid\", jwk.KeyID))\n\tassert.FatalError(t, err)\n\n\tnow := time.Now().UTC()\n\n\tvalidIssuer := \"step-cli\"\n\tvalidAudience := []string{\"https://example.com/revoke\"}\n\n\ttype authorizeTest struct {\n\t\tauth  *Authority\n\t\ttoken string\n\t\terr   error\n\t\tcode  int\n\t}\n\ttests := map[string]func(t *testing.T) *authorizeTest{\n\t\t\"fail/invalid-token\": func(t *testing.T) *authorizeTest {\n\t\t\treturn &authorizeTest{\n\t\t\t\tauth:  a,\n\t\t\t\ttoken: \"foo\",\n\t\t\t\terr:   errors.New(\"error parsing token\"),\n\t\t\t\tcode:  http.StatusUnauthorized,\n\t\t\t}\n\t\t},\n\t\t\"fail/prehistoric-token\": func(t *testing.T) *authorizeTest {\n\t\t\tcl := jose.Claims{\n\t\t\t\tSubject:   \"test.smallstep.com\",\n\t\t\t\tIssuer:    validIssuer,\n\t\t\t\tNotBefore: jose.NewNumericDate(now),\n\t\t\t\tExpiry:    jose.NewNumericDate(now.Add(time.Minute)),\n\t\t\t\tIssuedAt:  jose.NewNumericDate(now.Add(-time.Hour)),\n\t\t\t\tAudience:  validAudience,\n\t\t\t\tID:        \"43\",\n\t\t\t}\n\t\t\traw, err := jose.Signed(sig).Claims(cl).CompactSerialize()\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn &authorizeTest{\n\t\t\t\tauth:  a,\n\t\t\t\ttoken: raw,\n\t\t\t\terr:   errors.New(\"token issued before the bootstrap of certificate authority\"),\n\t\t\t\tcode:  http.StatusUnauthorized,\n\t\t\t}\n\t\t},\n\t\t\"fail/provisioner-not-found\": func(t *testing.T) *authorizeTest {\n\t\t\tcl := jose.Claims{\n\t\t\t\tSubject:   \"test.smallstep.com\",\n\t\t\t\tIssuer:    validIssuer,\n\t\t\t\tNotBefore: jose.NewNumericDate(now),\n\t\t\t\tExpiry:    jose.NewNumericDate(now.Add(time.Minute)),\n\t\t\t\tAudience:  validAudience,\n\t\t\t\tID:        \"44\",\n\t\t\t}\n\t\t\t_sig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: jwk.Key},\n\t\t\t\t(&jose.SignerOptions{}).WithType(\"JWT\").WithHeader(\"kid\", \"foo\"))\n\t\t\tassert.FatalError(t, err)\n\n\t\t\traw, err := jose.Signed(_sig).Claims(cl).CompactSerialize()\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn &authorizeTest{\n\t\t\t\tauth:  a,\n\t\t\t\ttoken: raw,\n\t\t\t\terr:   errors.New(\"provisioner not found or invalid audience (https://example.com/revoke)\"),\n\t\t\t\tcode:  http.StatusUnauthorized,\n\t\t\t}\n\t\t},\n\t\t\"fail/token-flow-not-supported\": func(t *testing.T) *authorizeTest {\n\t\t\tcl := jose.Claims{\n\t\t\t\tSubject:   \"test.smallstep.com\",\n\t\t\t\tIssuer:    validIssuer,\n\t\t\t\tNotBefore: jose.NewNumericDate(now),\n\t\t\t\tExpiry:    jose.NewNumericDate(now.Add(time.Minute)),\n\t\t\t\tIssuedAt:  jose.NewNumericDate(now),\n\t\t\t\tAudience:  []string{\"acme/acme\"},\n\t\t\t\tID:        \"45\",\n\t\t\t}\n\t\t\traw, err := jose.Signed(sig).Claims(cl).CompactSerialize()\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn &authorizeTest{\n\t\t\t\tauth:  a,\n\t\t\t\ttoken: raw,\n\t\t\t\terr:   errors.New(\"token flow is not supported\"),\n\t\t\t\tcode:  http.StatusBadRequest,\n\t\t\t}\n\t\t},\n\t\t\"ok/simpledb\": func(t *testing.T) *authorizeTest {\n\t\t\tcl := jose.Claims{\n\t\t\t\tSubject:   \"test.smallstep.com\",\n\t\t\t\tIssuer:    validIssuer,\n\t\t\t\tNotBefore: jose.NewNumericDate(now),\n\t\t\t\tExpiry:    jose.NewNumericDate(now.Add(time.Minute)),\n\t\t\t\tAudience:  validAudience,\n\t\t\t\tID:        \"43\",\n\t\t\t}\n\t\t\traw, err := jose.Signed(sig).Claims(cl).CompactSerialize()\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn &authorizeTest{\n\t\t\t\tauth:  a,\n\t\t\t\ttoken: raw,\n\t\t\t}\n\t\t},\n\t\t\"fail/simpledb/token-already-used\": func(t *testing.T) *authorizeTest {\n\t\t\t_a := testAuthority(t)\n\t\t\tcl := jose.Claims{\n\t\t\t\tSubject:   \"test.smallstep.com\",\n\t\t\t\tIssuer:    validIssuer,\n\t\t\t\tNotBefore: jose.NewNumericDate(now),\n\t\t\t\tExpiry:    jose.NewNumericDate(now.Add(time.Minute)),\n\t\t\t\tAudience:  validAudience,\n\t\t\t\tID:        \"43\",\n\t\t\t}\n\t\t\traw, err := jose.Signed(sig).Claims(cl).CompactSerialize()\n\t\t\tassert.FatalError(t, err)\n\t\t\t_, err = _a.authorizeToken(context.Background(), raw)\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn &authorizeTest{\n\t\t\t\tauth:  _a,\n\t\t\t\ttoken: raw,\n\t\t\t\terr:   errors.New(\"token already used\"),\n\t\t\t\tcode:  http.StatusUnauthorized,\n\t\t\t}\n\t\t},\n\t\t\"ok/sha256\": func(t *testing.T) *authorizeTest {\n\t\t\tcl := jose.Claims{\n\t\t\t\tSubject:   \"test.smallstep.com\",\n\t\t\t\tIssuer:    validIssuer,\n\t\t\t\tNotBefore: jose.NewNumericDate(now),\n\t\t\t\tExpiry:    jose.NewNumericDate(now.Add(time.Minute)),\n\t\t\t\tAudience:  validAudience,\n\t\t\t}\n\t\t\traw, err := jose.Signed(sig).Claims(cl).CompactSerialize()\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn &authorizeTest{\n\t\t\t\tauth:  a,\n\t\t\t\ttoken: raw,\n\t\t\t}\n\t\t},\n\t\t\"fail/sha256/token-already-used\": func(t *testing.T) *authorizeTest {\n\t\t\t_a := testAuthority(t)\n\t\t\tcl := jose.Claims{\n\t\t\t\tSubject:   \"test.smallstep.com\",\n\t\t\t\tIssuer:    validIssuer,\n\t\t\t\tNotBefore: jose.NewNumericDate(now),\n\t\t\t\tExpiry:    jose.NewNumericDate(now.Add(time.Minute)),\n\t\t\t\tAudience:  validAudience,\n\t\t\t}\n\t\t\traw, err := jose.Signed(sig).Claims(cl).CompactSerialize()\n\t\t\tassert.FatalError(t, err)\n\t\t\t_, err = _a.authorizeToken(context.Background(), raw)\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn &authorizeTest{\n\t\t\t\tauth:  _a,\n\t\t\t\ttoken: raw,\n\t\t\t\terr:   errors.New(\"token already used\"),\n\t\t\t\tcode:  http.StatusUnauthorized,\n\t\t\t}\n\t\t},\n\t\t\"ok/mockNoSQLDB\": func(t *testing.T) *authorizeTest {\n\t\t\t_a := testAuthority(t)\n\t\t\t_a.db = &db.MockAuthDB{\n\t\t\t\tMUseToken: func(id, tok string) (bool, error) {\n\t\t\t\t\treturn true, nil\n\t\t\t\t},\n\t\t\t}\n\n\t\t\tcl := jose.Claims{\n\t\t\t\tSubject:   \"test.smallstep.com\",\n\t\t\t\tIssuer:    validIssuer,\n\t\t\t\tNotBefore: jose.NewNumericDate(now),\n\t\t\t\tExpiry:    jose.NewNumericDate(now.Add(time.Minute)),\n\t\t\t\tAudience:  validAudience,\n\t\t\t\tID:        \"43\",\n\t\t\t}\n\t\t\traw, err := jose.Signed(sig).Claims(cl).CompactSerialize()\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn &authorizeTest{\n\t\t\t\tauth:  _a,\n\t\t\t\ttoken: raw,\n\t\t\t}\n\t\t},\n\t\t\"fail/mockNoSQLDB/error\": func(t *testing.T) *authorizeTest {\n\t\t\t_a := testAuthority(t)\n\t\t\t_a.db = &db.MockAuthDB{\n\t\t\t\tMUseToken: func(id, tok string) (bool, error) {\n\t\t\t\t\treturn false, errors.New(\"force\")\n\t\t\t\t},\n\t\t\t}\n\n\t\t\tcl := jose.Claims{\n\t\t\t\tSubject:   \"test.smallstep.com\",\n\t\t\t\tIssuer:    validIssuer,\n\t\t\t\tNotBefore: jose.NewNumericDate(now),\n\t\t\t\tExpiry:    jose.NewNumericDate(now.Add(time.Minute)),\n\t\t\t\tAudience:  validAudience,\n\t\t\t\tID:        \"43\",\n\t\t\t}\n\t\t\traw, err := jose.Signed(sig).Claims(cl).CompactSerialize()\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn &authorizeTest{\n\t\t\t\tauth:  _a,\n\t\t\t\ttoken: raw,\n\t\t\t\terr:   errors.New(\"failed when attempting to store token: force\"),\n\t\t\t\tcode:  http.StatusInternalServerError,\n\t\t\t}\n\t\t},\n\t\t\"fail/mockNoSQLDB/token-already-used\": func(t *testing.T) *authorizeTest {\n\t\t\t_a := testAuthority(t)\n\t\t\t_a.db = &db.MockAuthDB{\n\t\t\t\tMUseToken: func(id, tok string) (bool, error) {\n\t\t\t\t\treturn false, nil\n\t\t\t\t},\n\t\t\t}\n\n\t\t\tcl := jose.Claims{\n\t\t\t\tSubject:   \"test.smallstep.com\",\n\t\t\t\tIssuer:    validIssuer,\n\t\t\t\tNotBefore: jose.NewNumericDate(now),\n\t\t\t\tExpiry:    jose.NewNumericDate(now.Add(time.Minute)),\n\t\t\t\tAudience:  validAudience,\n\t\t\t\tID:        \"43\",\n\t\t\t}\n\t\t\traw, err := jose.Signed(sig).Claims(cl).CompactSerialize()\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn &authorizeTest{\n\t\t\t\tauth:  _a,\n\t\t\t\ttoken: raw,\n\t\t\t\terr:   errors.New(\"token already used\"),\n\t\t\t\tcode:  http.StatusUnauthorized,\n\t\t\t}\n\t\t},\n\t\t\"fail/uninitialized\": func(t *testing.T) *authorizeTest {\n\t\t\tcl := jose.Claims{\n\t\t\t\tSubject:   \"test.smallstep.com\",\n\t\t\t\tIssuer:    \"uninitialized\",\n\t\t\t\tNotBefore: jose.NewNumericDate(now),\n\t\t\t\tExpiry:    jose.NewNumericDate(now.Add(time.Minute)),\n\t\t\t\tAudience:  validAudience,\n\t\t\t\tID:        uuid.NewString(),\n\t\t\t}\n\t\t\traw, err := jose.Signed(sig).Claims(cl).CompactSerialize()\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn &authorizeTest{\n\t\t\t\tauth:  a,\n\t\t\t\ttoken: raw,\n\t\t\t\terr:   errors.New(`provisioner \"uninitialized\" is disabled due to an initialization error`),\n\t\t\t\tcode:  http.StatusUnauthorized,\n\t\t\t}\n\t\t},\n\t}\n\n\tfor name, genTestCase := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ttc := genTestCase(t)\n\n\t\t\tp, err := tc.auth.authorizeToken(context.Background(), tc.token)\n\t\t\tif err != nil {\n\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\tvar sc render.StatusCodedError\n\t\t\t\t\tassert.Fatal(t, errors.As(err, &sc), \"error does not implement StatusCodedError interface\")\n\t\t\t\t\tassert.Equals(t, sc.StatusCode(), tc.code)\n\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.err.Error())\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif assert.Nil(t, tc.err) {\n\t\t\t\t\tassert.Equals(t, p.GetID(), \"step-cli:4UELJx8e0aS9m0CH3fZ0EB7D5aUPICb759zALHFejvc\")\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestAuthority_authorizeRevoke(t *testing.T) {\n\ta := testAuthority(t)\n\n\tjwk, err := jose.ReadKey(\"testdata/secrets/step_cli_key_priv.jwk\", jose.WithPassword([]byte(\"pass\")))\n\tassert.FatalError(t, err)\n\n\tsig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: jwk.Key},\n\t\t(&jose.SignerOptions{}).WithType(\"JWT\").WithHeader(\"kid\", jwk.KeyID))\n\tassert.FatalError(t, err)\n\n\tnow := time.Now().UTC()\n\n\tvalidIssuer := \"step-cli\"\n\tvalidAudience := []string{\"https://example.com/revoke\"}\n\n\ttype authorizeTest struct {\n\t\tauth  *Authority\n\t\ttoken string\n\t\terr   error\n\t\tcode  int\n\t}\n\ttests := map[string]func(t *testing.T) *authorizeTest{\n\t\t\"fail/token/invalid-token\": func(t *testing.T) *authorizeTest {\n\t\t\treturn &authorizeTest{\n\t\t\t\tauth:  a,\n\t\t\t\ttoken: \"foo\",\n\t\t\t\terr:   errors.New(\"authority.authorizeRevoke: error parsing token\"),\n\t\t\t\tcode:  http.StatusUnauthorized,\n\t\t\t}\n\t\t},\n\t\t\"fail/token/invalid-subject\": func(t *testing.T) *authorizeTest {\n\t\t\tcl := jose.Claims{\n\t\t\t\tSubject:   \"\",\n\t\t\t\tIssuer:    validIssuer,\n\t\t\t\tNotBefore: jose.NewNumericDate(now),\n\t\t\t\tExpiry:    jose.NewNumericDate(now.Add(time.Minute)),\n\t\t\t\tAudience:  validAudience,\n\t\t\t\tID:        \"43\",\n\t\t\t}\n\t\t\traw, err := jose.Signed(sig).Claims(cl).CompactSerialize()\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn &authorizeTest{\n\t\t\t\tauth:  a,\n\t\t\t\ttoken: raw,\n\t\t\t\terr:   errors.New(\"authority.authorizeRevoke: jwk.AuthorizeRevoke: jwk.authorizeToken; jwk token subject cannot be empty\"),\n\t\t\t\tcode:  http.StatusUnauthorized,\n\t\t\t}\n\t\t},\n\t\t\"ok/token\": func(t *testing.T) *authorizeTest {\n\t\t\tcl := jose.Claims{\n\t\t\t\tSubject:   \"test.smallstep.com\",\n\t\t\t\tIssuer:    validIssuer,\n\t\t\t\tNotBefore: jose.NewNumericDate(now),\n\t\t\t\tExpiry:    jose.NewNumericDate(now.Add(time.Minute)),\n\t\t\t\tAudience:  validAudience,\n\t\t\t\tID:        \"44\",\n\t\t\t}\n\t\t\traw, err := jose.Signed(sig).Claims(cl).CompactSerialize()\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn &authorizeTest{\n\t\t\t\tauth:  a,\n\t\t\t\ttoken: raw,\n\t\t\t}\n\t\t},\n\t}\n\n\tfor name, genTestCase := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ttc := genTestCase(t)\n\n\t\t\tif err := tc.auth.authorizeRevoke(context.Background(), tc.token); err != nil {\n\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\tvar sc render.StatusCodedError\n\t\t\t\t\tassert.Fatal(t, errors.As(err, &sc), \"error does not implement StatusCodedError interface\")\n\t\t\t\t\tassert.Equals(t, sc.StatusCode(), tc.code)\n\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.err.Error())\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tassert.Nil(t, tc.err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestAuthority_authorizeSign(t *testing.T) {\n\ta := testAuthority(t)\n\n\tjwk, err := jose.ReadKey(\"testdata/secrets/step_cli_key_priv.jwk\", jose.WithPassword([]byte(\"pass\")))\n\tassert.FatalError(t, err)\n\n\tsig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: jwk.Key},\n\t\t(&jose.SignerOptions{}).WithType(\"JWT\").WithHeader(\"kid\", jwk.KeyID))\n\tassert.FatalError(t, err)\n\n\tnow := time.Now().UTC()\n\n\tvalidIssuer := \"step-cli\"\n\tvalidAudience := []string{\"https://example.com/sign\"}\n\n\ttype authorizeTest struct {\n\t\tauth  *Authority\n\t\ttoken string\n\t\terr   error\n\t\tcode  int\n\t}\n\ttests := map[string]func(t *testing.T) *authorizeTest{\n\t\t\"fail/invalid-token\": func(t *testing.T) *authorizeTest {\n\t\t\treturn &authorizeTest{\n\t\t\t\tauth:  a,\n\t\t\t\ttoken: \"foo\",\n\t\t\t\terr:   errors.New(\"authority.authorizeSign: error parsing token\"),\n\t\t\t\tcode:  http.StatusUnauthorized,\n\t\t\t}\n\t\t},\n\t\t\"fail/invalid-subject\": func(t *testing.T) *authorizeTest {\n\t\t\tcl := jose.Claims{\n\t\t\t\tSubject:   \"\",\n\t\t\t\tIssuer:    validIssuer,\n\t\t\t\tNotBefore: jose.NewNumericDate(now),\n\t\t\t\tExpiry:    jose.NewNumericDate(now.Add(time.Minute)),\n\t\t\t\tAudience:  validAudience,\n\t\t\t\tID:        \"43\",\n\t\t\t}\n\t\t\traw, err := jose.Signed(sig).Claims(cl).CompactSerialize()\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn &authorizeTest{\n\t\t\t\tauth:  a,\n\t\t\t\ttoken: raw,\n\t\t\t\terr:   errors.New(\"authority.authorizeSign: jwk.AuthorizeSign: jwk.authorizeToken; jwk token subject cannot be empty\"),\n\t\t\t\tcode:  http.StatusUnauthorized,\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) *authorizeTest {\n\t\t\tcl := jose.Claims{\n\t\t\t\tSubject:   \"test.smallstep.com\",\n\t\t\t\tIssuer:    validIssuer,\n\t\t\t\tNotBefore: jose.NewNumericDate(now),\n\t\t\t\tExpiry:    jose.NewNumericDate(now.Add(time.Minute)),\n\t\t\t\tAudience:  validAudience,\n\t\t\t\tID:        \"44\",\n\t\t\t}\n\t\t\traw, err := jose.Signed(sig).Claims(cl).CompactSerialize()\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn &authorizeTest{\n\t\t\t\tauth:  a,\n\t\t\t\ttoken: raw,\n\t\t\t}\n\t\t},\n\t}\n\n\tfor name, genTestCase := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ttc := genTestCase(t)\n\n\t\t\tgot, err := tc.auth.authorizeSign(context.Background(), tc.token)\n\t\t\tif err != nil {\n\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\tvar sc render.StatusCodedError\n\t\t\t\t\tassert.Fatal(t, errors.As(err, &sc), \"error does not implement StatusCodedError interface\")\n\t\t\t\t\tassert.Equals(t, sc.StatusCode(), tc.code)\n\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.err.Error())\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif assert.Nil(t, tc.err) {\n\t\t\t\t\tassert.Equals(t, 11, len(got)) // number of provisioner.SignOptions returned\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestAuthority_Authorize(t *testing.T) {\n\ta := testAuthority(t)\n\n\tjwk, err := jose.ReadKey(\"testdata/secrets/step_cli_key_priv.jwk\", jose.WithPassword([]byte(\"pass\")))\n\tassert.FatalError(t, err)\n\n\tsig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: jwk.Key},\n\t\t(&jose.SignerOptions{}).WithType(\"JWT\").WithHeader(\"kid\", jwk.KeyID))\n\tassert.FatalError(t, err)\n\n\tnow := time.Now().UTC()\n\tvalidIssuer := \"step-cli\"\n\n\ttype authorizeTest struct {\n\t\tauth  *Authority\n\t\ttoken string\n\t\tctx   context.Context\n\t\terr   error\n\t\tcode  int\n\t}\n\ttests := map[string]func(t *testing.T) *authorizeTest{\n\t\t\"default-to-signMethod\": func(t *testing.T) *authorizeTest {\n\t\t\treturn &authorizeTest{\n\t\t\t\tauth:  a,\n\t\t\t\ttoken: \"foo\",\n\t\t\t\tctx:   context.Background(),\n\t\t\t\terr:   errors.New(\"authority.Authorize: authority.authorizeSign: error parsing token\"),\n\t\t\t\tcode:  http.StatusUnauthorized,\n\t\t\t}\n\t\t},\n\t\t\"fail/sign/invalid-token\": func(t *testing.T) *authorizeTest {\n\t\t\treturn &authorizeTest{\n\t\t\t\tauth:  a,\n\t\t\t\ttoken: \"foo\",\n\t\t\t\tctx:   provisioner.NewContextWithMethod(context.Background(), provisioner.SignMethod),\n\t\t\t\terr:   errors.New(\"authority.Authorize: authority.authorizeSign: error parsing token\"),\n\t\t\t\tcode:  http.StatusUnauthorized,\n\t\t\t}\n\t\t},\n\t\t\"ok/sign\": func(t *testing.T) *authorizeTest {\n\t\t\tcl := jose.Claims{\n\t\t\t\tSubject:   \"test.smallstep.com\",\n\t\t\t\tIssuer:    validIssuer,\n\t\t\t\tNotBefore: jose.NewNumericDate(now),\n\t\t\t\tExpiry:    jose.NewNumericDate(now.Add(time.Minute)),\n\t\t\t\tAudience:  testAudiences.Sign,\n\t\t\t\tID:        \"1\",\n\t\t\t}\n\t\t\ttoken, err := jose.Signed(sig).Claims(cl).CompactSerialize()\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn &authorizeTest{\n\t\t\t\tauth:  a,\n\t\t\t\ttoken: token,\n\t\t\t\tctx:   provisioner.NewContextWithMethod(context.Background(), provisioner.SignMethod),\n\t\t\t}\n\t\t},\n\t\t\"fail/revoke/invalid-token\": func(t *testing.T) *authorizeTest {\n\t\t\treturn &authorizeTest{\n\t\t\t\tauth:  a,\n\t\t\t\ttoken: \"foo\",\n\t\t\t\tctx:   provisioner.NewContextWithMethod(context.Background(), provisioner.RevokeMethod),\n\t\t\t\terr:   errors.New(\"authority.Authorize: authority.authorizeRevoke: error parsing token\"),\n\t\t\t\tcode:  http.StatusUnauthorized,\n\t\t\t}\n\t\t},\n\t\t\"ok/revoke\": func(t *testing.T) *authorizeTest {\n\t\t\tcl := jose.Claims{\n\t\t\t\tSubject:   \"test.smallstep.com\",\n\t\t\t\tIssuer:    validIssuer,\n\t\t\t\tNotBefore: jose.NewNumericDate(now),\n\t\t\t\tExpiry:    jose.NewNumericDate(now.Add(time.Minute)),\n\t\t\t\tAudience:  testAudiences.Revoke,\n\t\t\t\tID:        \"2\",\n\t\t\t}\n\t\t\ttoken, err := jose.Signed(sig).Claims(cl).CompactSerialize()\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn &authorizeTest{\n\t\t\t\tauth:  a,\n\t\t\t\ttoken: token,\n\t\t\t\tctx:   provisioner.NewContextWithMethod(context.Background(), provisioner.RevokeMethod),\n\t\t\t}\n\t\t},\n\t\t\"fail/sshSign/invalid-token\": func(t *testing.T) *authorizeTest {\n\t\t\treturn &authorizeTest{\n\t\t\t\tauth:  a,\n\t\t\t\ttoken: \"foo\",\n\t\t\t\tctx:   provisioner.NewContextWithMethod(context.Background(), provisioner.SSHSignMethod),\n\t\t\t\terr:   errors.New(\"authority.Authorize: authority.authorizeSSHSign: error parsing token\"),\n\t\t\t\tcode:  http.StatusUnauthorized,\n\t\t\t}\n\t\t},\n\t\t\"fail/sshSign/disabled\": func(t *testing.T) *authorizeTest {\n\t\t\t_a := testAuthority(t)\n\t\t\t_a.sshCAHostCertSignKey = nil\n\t\t\t_a.sshCAUserCertSignKey = nil\n\t\t\treturn &authorizeTest{\n\t\t\t\tauth:  _a,\n\t\t\t\ttoken: \"foo\",\n\t\t\t\tctx:   provisioner.NewContextWithMethod(context.Background(), provisioner.SSHSignMethod),\n\t\t\t\terr:   errors.New(\"authority.Authorize; ssh certificate flows are not enabled\"),\n\t\t\t\tcode:  http.StatusNotImplemented,\n\t\t\t}\n\t\t},\n\t\t\"ok/sshSign\": func(t *testing.T) *authorizeTest {\n\t\t\traw, err := generateSimpleSSHUserToken(validIssuer, testAudiences.SSHSign[0], jwk)\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn &authorizeTest{\n\t\t\t\tauth:  a,\n\t\t\t\ttoken: raw,\n\t\t\t\tctx:   provisioner.NewContextWithMethod(context.Background(), provisioner.SSHSignMethod),\n\t\t\t}\n\t\t},\n\t\t\"fail/sshRenew/invalid-token\": func(t *testing.T) *authorizeTest {\n\t\t\treturn &authorizeTest{\n\t\t\t\tauth:  a,\n\t\t\t\ttoken: \"foo\",\n\t\t\t\tctx:   provisioner.NewContextWithMethod(context.Background(), provisioner.SSHRenewMethod),\n\t\t\t\terr:   errors.New(\"authority.Authorize: authority.authorizeSSHRenew: error parsing token\"),\n\t\t\t\tcode:  http.StatusUnauthorized,\n\t\t\t}\n\t\t},\n\t\t\"fail/sshRenew/disabled\": func(t *testing.T) *authorizeTest {\n\t\t\t_a := testAuthority(t)\n\t\t\t_a.sshCAHostCertSignKey = nil\n\t\t\t_a.sshCAUserCertSignKey = nil\n\t\t\treturn &authorizeTest{\n\t\t\t\tauth:  _a,\n\t\t\t\ttoken: \"foo\",\n\t\t\t\tctx:   provisioner.NewContextWithMethod(context.Background(), provisioner.SSHRenewMethod),\n\t\t\t\terr:   errors.New(\"authority.Authorize; ssh certificate flows are not enabled\"),\n\t\t\t\tcode:  http.StatusNotImplemented,\n\t\t\t}\n\t\t},\n\t\t\"ok/sshRenew\": func(t *testing.T) *authorizeTest {\n\t\t\tkey, err := pemutil.Read(\"./testdata/secrets/ssh_host_ca_key\")\n\t\t\tassert.FatalError(t, err)\n\t\t\tsigner, ok := key.(crypto.Signer)\n\t\t\tassert.Fatal(t, ok, \"could not cast ssh signing key to crypto signer\")\n\t\t\tsshSigner, err := ssh.NewSignerFromSigner(signer)\n\t\t\tassert.FatalError(t, err)\n\n\t\t\tcert, _jwk, err := createSSHCert(&ssh.Certificate{CertType: ssh.HostCert}, sshSigner)\n\t\t\tassert.FatalError(t, err)\n\n\t\t\tp, ok := a.provisioners.Load(\"sshpop/sshpop\")\n\t\t\tassert.Fatal(t, ok, \"sshpop provisioner not found in test authority\")\n\n\t\t\ttok, err := generateToken(\"foo\", p.GetName(), testAudiences.SSHRenew[0]+\"#sshpop/sshpop\",\n\t\t\t\t[]string{\"foo.smallstep.com\"}, now, _jwk, withSSHPOPFile(cert))\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn &authorizeTest{\n\t\t\t\tauth:  a,\n\t\t\t\ttoken: tok,\n\t\t\t\tctx:   provisioner.NewContextWithMethod(context.Background(), provisioner.SSHRenewMethod),\n\t\t\t}\n\t\t},\n\t\t\"fail/sshRevoke/invalid-token\": func(t *testing.T) *authorizeTest {\n\t\t\treturn &authorizeTest{\n\t\t\t\tauth:  a,\n\t\t\t\ttoken: \"foo\",\n\t\t\t\tctx:   provisioner.NewContextWithMethod(context.Background(), provisioner.SSHRevokeMethod),\n\t\t\t\terr:   errors.New(\"authority.Authorize: authority.authorizeSSHRevoke: error parsing token\"),\n\t\t\t\tcode:  http.StatusUnauthorized,\n\t\t\t}\n\t\t},\n\t\t\"ok/sshRevoke\": func(t *testing.T) *authorizeTest {\n\t\t\tcl := jose.Claims{\n\t\t\t\tSubject:   \"test.smallstep.com\",\n\t\t\t\tIssuer:    validIssuer,\n\t\t\t\tNotBefore: jose.NewNumericDate(now),\n\t\t\t\tExpiry:    jose.NewNumericDate(now.Add(time.Minute)),\n\t\t\t\tAudience:  testAudiences.SSHRevoke,\n\t\t\t\tID:        \"3\",\n\t\t\t}\n\t\t\ttoken, err := jose.Signed(sig).Claims(cl).CompactSerialize()\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn &authorizeTest{\n\t\t\t\tauth:  a,\n\t\t\t\ttoken: token,\n\t\t\t\tctx:   provisioner.NewContextWithMethod(context.Background(), provisioner.SSHRevokeMethod),\n\t\t\t}\n\t\t},\n\t\t\"fail/sshRekey/invalid-token\": func(t *testing.T) *authorizeTest {\n\t\t\treturn &authorizeTest{\n\t\t\t\tauth:  a,\n\t\t\t\ttoken: \"foo\",\n\t\t\t\tctx:   provisioner.NewContextWithMethod(context.Background(), provisioner.SSHRekeyMethod),\n\t\t\t\terr:   errors.New(\"authority.Authorize: authority.authorizeSSHRekey: error parsing token\"),\n\t\t\t\tcode:  http.StatusUnauthorized,\n\t\t\t}\n\t\t},\n\t\t\"fail/sshRekey/disabled\": func(t *testing.T) *authorizeTest {\n\t\t\t_a := testAuthority(t)\n\t\t\t_a.sshCAHostCertSignKey = nil\n\t\t\t_a.sshCAUserCertSignKey = nil\n\t\t\treturn &authorizeTest{\n\t\t\t\tauth:  _a,\n\t\t\t\ttoken: \"foo\",\n\t\t\t\tctx:   provisioner.NewContextWithMethod(context.Background(), provisioner.SSHRekeyMethod),\n\t\t\t\terr:   errors.New(\"authority.Authorize; ssh certificate flows are not enabled\"),\n\t\t\t\tcode:  http.StatusNotImplemented,\n\t\t\t}\n\t\t},\n\t\t\"ok/sshRekey\": func(t *testing.T) *authorizeTest {\n\t\t\tkey, err := pemutil.Read(\"./testdata/secrets/ssh_host_ca_key\")\n\t\t\tassert.FatalError(t, err)\n\t\t\tsigner, ok := key.(crypto.Signer)\n\t\t\tassert.Fatal(t, ok, \"could not cast ssh signing key to crypto signer\")\n\t\t\tsshSigner, err := ssh.NewSignerFromSigner(signer)\n\t\t\tassert.FatalError(t, err)\n\n\t\t\tcert, _jwk, err := createSSHCert(&ssh.Certificate{CertType: ssh.HostCert}, sshSigner)\n\t\t\tassert.FatalError(t, err)\n\n\t\t\tp, ok := a.provisioners.Load(\"sshpop/sshpop\")\n\t\t\tassert.Fatal(t, ok, \"sshpop provisioner not found in test authority\")\n\n\t\t\ttok, err := generateToken(\"foo\", p.GetName(), testAudiences.SSHRekey[0]+\"#sshpop/sshpop\",\n\t\t\t\t[]string{\"foo.smallstep.com\"}, now, _jwk, withSSHPOPFile(cert))\n\t\t\tassert.FatalError(t, err)\n\n\t\t\treturn &authorizeTest{\n\t\t\t\tauth:  a,\n\t\t\t\ttoken: tok,\n\t\t\t\tctx:   provisioner.NewContextWithMethod(context.Background(), provisioner.SSHRekeyMethod),\n\t\t\t}\n\t\t},\n\t\t\"fail/unexpected-method\": func(t *testing.T) *authorizeTest {\n\t\t\treturn &authorizeTest{\n\t\t\t\tauth:  a,\n\t\t\t\ttoken: \"foo\",\n\t\t\t\tctx:   provisioner.NewContextWithMethod(context.Background(), 15),\n\t\t\t\terr:   errors.New(\"authority.Authorize; method 15 is not supported\"),\n\t\t\t\tcode:  http.StatusInternalServerError,\n\t\t\t}\n\t\t},\n\t}\n\n\tfor name, genTestCase := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ttc := genTestCase(t)\n\t\t\tgot, err := tc.auth.Authorize(tc.ctx, tc.token)\n\t\t\tif err != nil {\n\t\t\t\tif assert.NotNil(t, tc.err, fmt.Sprintf(\"unexpected error: %s\", err)) {\n\t\t\t\t\tassert.Nil(t, got)\n\t\t\t\t\tvar sc render.StatusCodedError\n\t\t\t\t\tassert.Fatal(t, errors.As(err, &sc), \"error does not implement StatusCodedError interface\")\n\t\t\t\t\tassert.Equals(t, sc.StatusCode(), tc.code)\n\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.err.Error())\n\n\t\t\t\t\tvar ctxErr *errs.Error\n\t\t\t\t\tassert.Fatal(t, errors.As(err, &ctxErr), \"error is not of type *errs.Error\")\n\t\t\t\t\tassert.Equals(t, ctxErr.Details[\"token\"], tc.token)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tassert.Nil(t, tc.err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestAuthority_authorizeRenew(t *testing.T) {\n\tfooCrt, err := pemutil.ReadCertificate(\"testdata/certs/foo.crt\")\n\tfooCrt.NotAfter = time.Now().Add(time.Hour)\n\tassert.FatalError(t, err)\n\n\trenewDisabledCrt, err := pemutil.ReadCertificate(\"testdata/certs/renew-disabled.crt\")\n\tassert.FatalError(t, err)\n\n\totherCrt, err := pemutil.ReadCertificate(\"testdata/certs/provisioner-not-found.crt\")\n\tassert.FatalError(t, err)\n\n\ttype authorizeTest struct {\n\t\tauth *Authority\n\t\tcert *x509.Certificate\n\t\terr  error\n\t\tcode int\n\t}\n\ttests := map[string]func(t *testing.T) *authorizeTest{\n\t\t\"fail/db.IsRevoked-error\": func(t *testing.T) *authorizeTest {\n\t\t\ta := testAuthority(t)\n\t\t\ta.db = &db.MockAuthDB{\n\t\t\t\tMIsRevoked: func(key string) (bool, error) {\n\t\t\t\t\treturn false, errors.New(\"force\")\n\t\t\t\t},\n\t\t\t}\n\n\t\t\treturn &authorizeTest{\n\t\t\t\tauth: a,\n\t\t\t\tcert: fooCrt,\n\t\t\t\terr:  errors.New(\"authority.authorizeRenew: force\"),\n\t\t\t\tcode: http.StatusInternalServerError,\n\t\t\t}\n\t\t},\n\t\t\"fail/revoked\": func(t *testing.T) *authorizeTest {\n\t\t\ta := testAuthority(t)\n\t\t\ta.db = &db.MockAuthDB{\n\t\t\t\tMIsRevoked: func(key string) (bool, error) {\n\t\t\t\t\treturn true, nil\n\t\t\t\t},\n\t\t\t}\n\t\t\treturn &authorizeTest{\n\t\t\t\tauth: a,\n\t\t\t\tcert: fooCrt,\n\t\t\t\terr:  errors.New(\"authority.authorizeRenew: certificate has been revoked\"),\n\t\t\t\tcode: http.StatusUnauthorized,\n\t\t\t}\n\t\t},\n\t\t\"fail/load-provisioner\": func(t *testing.T) *authorizeTest {\n\t\t\ta := testAuthority(t)\n\t\t\ta.db = &db.MockAuthDB{\n\t\t\t\tMIsRevoked: func(key string) (bool, error) {\n\t\t\t\t\treturn false, nil\n\t\t\t\t},\n\t\t\t}\n\t\t\treturn &authorizeTest{\n\t\t\t\tauth: a,\n\t\t\t\tcert: otherCrt,\n\t\t\t\terr:  errors.New(\"authority.authorizeRenew: provisioner not found\"),\n\t\t\t\tcode: http.StatusUnauthorized,\n\t\t\t}\n\t\t},\n\t\t\"fail/provisioner-authorize-renewal-fail\": func(t *testing.T) *authorizeTest {\n\t\t\ta := testAuthority(t)\n\t\t\ta.db = &db.MockAuthDB{\n\t\t\t\tMIsRevoked: func(key string) (bool, error) {\n\t\t\t\t\treturn false, nil\n\t\t\t\t},\n\t\t\t}\n\n\t\t\treturn &authorizeTest{\n\t\t\t\tauth: a,\n\t\t\t\tcert: renewDisabledCrt,\n\t\t\t\terr:  errors.New(\"authority.authorizeRenew: renew is disabled for provisioner 'renew_disabled'\"),\n\t\t\t\tcode: http.StatusUnauthorized,\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) *authorizeTest {\n\t\t\ta := testAuthority(t)\n\t\t\ta.db = &db.MockAuthDB{\n\t\t\t\tMIsRevoked: func(key string) (bool, error) {\n\t\t\t\t\treturn false, nil\n\t\t\t\t},\n\t\t\t}\n\t\t\treturn &authorizeTest{\n\t\t\t\tauth: a,\n\t\t\t\tcert: fooCrt,\n\t\t\t}\n\t\t},\n\t\t\"ok/from db\": func(t *testing.T) *authorizeTest {\n\t\t\ta := testAuthority(t)\n\t\t\ta.db = &db.MockAuthDB{\n\t\t\t\tMIsRevoked: func(key string) (bool, error) {\n\t\t\t\t\treturn false, nil\n\t\t\t\t},\n\t\t\t\tMGetCertificateData: func(serialNumber string) (*db.CertificateData, error) {\n\t\t\t\t\tp, ok := a.provisioners.LoadByName(\"step-cli\")\n\t\t\t\t\tif !ok {\n\t\t\t\t\t\tt.Fatal(\"provisioner step-cli not found\")\n\t\t\t\t\t}\n\t\t\t\t\treturn &db.CertificateData{\n\t\t\t\t\t\tProvisioner: &db.ProvisionerData{\n\t\t\t\t\t\t\tID: p.GetID(),\n\t\t\t\t\t\t},\n\t\t\t\t\t}, nil\n\t\t\t\t},\n\t\t\t}\n\t\t\treturn &authorizeTest{\n\t\t\t\tauth: a,\n\t\t\t\tcert: fooCrt,\n\t\t\t}\n\t\t},\n\t}\n\n\tfor name, genTestCase := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ttc := genTestCase(t)\n\n\t\t\t_, err := tc.auth.authorizeRenew(context.Background(), tc.cert)\n\t\t\tif err != nil {\n\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\tvar sc render.StatusCodedError\n\t\t\t\t\tassert.Fatal(t, errors.As(err, &sc), \"error does not implement StatusCodedError interface\")\n\t\t\t\t\tassert.Equals(t, sc.StatusCode(), tc.code)\n\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.err.Error())\n\n\t\t\t\t\tvar ctxErr *errs.Error\n\t\t\t\t\tassert.Fatal(t, errors.As(err, &ctxErr), \"error is not of type *errs.Error\")\n\t\t\t\t\tassert.Equals(t, ctxErr.Details[\"serialNumber\"], tc.cert.SerialNumber.String())\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tassert.Nil(t, tc.err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc generateSimpleSSHUserToken(iss, aud string, jwk *jose.JSONWebKey) (string, error) {\n\treturn generateSSHToken(\"subject@localhost\", iss, aud, time.Now(), &provisioner.SignSSHOptions{\n\t\tCertType:   \"user\",\n\t\tPrincipals: []string{\"name\"},\n\t}, jwk)\n}\n\ntype stepPayload struct {\n\tSSH *provisioner.SignSSHOptions `json:\"ssh,omitempty\"`\n}\n\nfunc generateSSHToken(sub, iss, aud string, iat time.Time, sshOpts *provisioner.SignSSHOptions, jwk *jose.JSONWebKey) (string, error) {\n\tsig, err := jose.NewSigner(\n\t\tjose.SigningKey{Algorithm: jose.ES256, Key: jwk.Key},\n\t\tnew(jose.SignerOptions).WithType(\"JWT\").WithHeader(\"kid\", jwk.KeyID),\n\t)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tid, err := randutil.ASCII(64)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tclaims := struct {\n\t\tjose.Claims\n\t\tStep *stepPayload `json:\"step,omitempty\"`\n\t}{\n\t\tClaims: jose.Claims{\n\t\t\tID:        id,\n\t\t\tSubject:   sub,\n\t\t\tIssuer:    iss,\n\t\t\tIssuedAt:  jose.NewNumericDate(iat),\n\t\t\tNotBefore: jose.NewNumericDate(iat),\n\t\t\tExpiry:    jose.NewNumericDate(iat.Add(5 * time.Minute)),\n\t\t\tAudience:  []string{aud},\n\t\t},\n\t\tStep: &stepPayload{\n\t\t\tSSH: sshOpts,\n\t\t},\n\t}\n\treturn jose.Signed(sig).Claims(claims).CompactSerialize()\n}\n\nfunc createSSHCert(cert *ssh.Certificate, signer ssh.Signer) (*ssh.Certificate, *jose.JSONWebKey, error) {\n\tnow := time.Now()\n\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"foo\", 0)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\tcert.Key, err = ssh.NewPublicKey(jwk.Public().Key)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\tif cert.ValidAfter == 0 {\n\t\tcert.ValidAfter = uint64(now.Unix())\n\t}\n\tif cert.ValidBefore == 0 {\n\t\tcert.ValidBefore = uint64(now.Add(time.Hour).Unix())\n\t}\n\tif err := cert.SignCert(rand.Reader, signer); err != nil {\n\t\treturn nil, nil, err\n\t}\n\treturn cert, jwk, nil\n}\n\nfunc TestAuthority_authorizeSSHSign(t *testing.T) {\n\ta := testAuthority(t)\n\n\tjwk, err := jose.ReadKey(\"testdata/secrets/step_cli_key_priv.jwk\", jose.WithPassword([]byte(\"pass\")))\n\tassert.FatalError(t, err)\n\n\tsig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: jwk.Key},\n\t\t(&jose.SignerOptions{}).WithType(\"JWT\").WithHeader(\"kid\", jwk.KeyID))\n\tassert.FatalError(t, err)\n\n\tnow := time.Now().UTC()\n\n\tvalidIssuer := \"step-cli\"\n\tvalidAudience := []string{\"https://example.com/ssh/sign\"}\n\n\ttype authorizeTest struct {\n\t\tauth  *Authority\n\t\ttoken string\n\t\terr   error\n\t\tcode  int\n\t}\n\ttests := map[string]func(t *testing.T) *authorizeTest{\n\t\t\"fail/invalid-token\": func(t *testing.T) *authorizeTest {\n\t\t\treturn &authorizeTest{\n\t\t\t\tauth:  a,\n\t\t\t\ttoken: \"foo\",\n\t\t\t\terr:   errors.New(\"authority.authorizeSSHSign: error parsing token\"),\n\t\t\t\tcode:  http.StatusUnauthorized,\n\t\t\t}\n\t\t},\n\t\t\"fail/invalid-subject\": func(t *testing.T) *authorizeTest {\n\t\t\tcl := jose.Claims{\n\t\t\t\tSubject:   \"\",\n\t\t\t\tIssuer:    validIssuer,\n\t\t\t\tNotBefore: jose.NewNumericDate(now),\n\t\t\t\tExpiry:    jose.NewNumericDate(now.Add(time.Minute)),\n\t\t\t\tAudience:  validAudience,\n\t\t\t\tID:        \"43\",\n\t\t\t}\n\t\t\traw, err := jose.Signed(sig).Claims(cl).CompactSerialize()\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn &authorizeTest{\n\t\t\t\tauth:  a,\n\t\t\t\ttoken: raw,\n\t\t\t\terr:   errors.New(\"authority.authorizeSSHSign: jwk.AuthorizeSSHSign: jwk.authorizeToken; jwk token subject cannot be empty\"),\n\t\t\t\tcode:  http.StatusUnauthorized,\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) *authorizeTest {\n\t\t\traw, err := generateSimpleSSHUserToken(validIssuer, validAudience[0], jwk)\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn &authorizeTest{\n\t\t\t\tauth:  a,\n\t\t\t\ttoken: raw,\n\t\t\t}\n\t\t},\n\t}\n\n\tfor name, genTestCase := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ttc := genTestCase(t)\n\n\t\t\tgot, err := tc.auth.authorizeSSHSign(context.Background(), tc.token)\n\t\t\tif err != nil {\n\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\tvar sc render.StatusCodedError\n\t\t\t\t\tassert.Fatal(t, errors.As(err, &sc), \"error does not implement StatusCodedError interface\")\n\t\t\t\t\tassert.Equals(t, sc.StatusCode(), tc.code)\n\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.err.Error())\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif assert.Nil(t, tc.err) {\n\t\t\t\t\tassert.Len(t, 10, got) // number of provisioner.SignOptions returned\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestAuthority_authorizeSSHRenew(t *testing.T) {\n\tnow := time.Now().UTC()\n\tsshpop := func(a *Authority) (*ssh.Certificate, string) {\n\t\tp, ok := a.provisioners.Load(\"sshpop/sshpop\")\n\t\tassert.Fatal(t, ok, \"sshpop provisioner not found in test authority\")\n\t\tkey, err := pemutil.Read(\"./testdata/secrets/ssh_host_ca_key\")\n\t\tassert.FatalError(t, err)\n\t\tsigner, ok := key.(crypto.Signer)\n\t\tassert.Fatal(t, ok, \"could not cast ssh signing key to crypto signer\")\n\t\tsshSigner, err := ssh.NewSignerFromSigner(signer)\n\t\tassert.FatalError(t, err)\n\t\tcert, jwk, err := createSSHCert(&ssh.Certificate{CertType: ssh.HostCert}, sshSigner)\n\t\tassert.FatalError(t, err)\n\t\ttoken, err := generateToken(\"foo\", p.GetName(), testAudiences.SSHRenew[0]+\"#sshpop/sshpop\", []string{\"foo.smallstep.com\"}, now, jwk, withSSHPOPFile(cert))\n\t\tassert.FatalError(t, err)\n\t\treturn cert, token\n\t}\n\n\ta := testAuthority(t)\n\n\tjwk, err := jose.ReadKey(\"testdata/secrets/step_cli_key_priv.jwk\", jose.WithPassword([]byte(\"pass\")))\n\tassert.FatalError(t, err)\n\n\tsig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: jwk.Key},\n\t\t(&jose.SignerOptions{}).WithType(\"JWT\").WithHeader(\"kid\", jwk.KeyID))\n\tassert.FatalError(t, err)\n\n\tvalidIssuer := \"step-cli\"\n\n\ttype authorizeTest struct {\n\t\tauth  *Authority\n\t\ttoken string\n\t\tcert  *ssh.Certificate\n\t\terr   error\n\t\tcode  int\n\t}\n\ttests := map[string]func(t *testing.T) *authorizeTest{\n\t\t\"fail/invalid-token\": func(t *testing.T) *authorizeTest {\n\t\t\treturn &authorizeTest{\n\t\t\t\tauth:  a,\n\t\t\t\ttoken: \"foo\",\n\t\t\t\terr:   errors.New(\"authority.authorizeSSHRenew: error parsing token\"),\n\t\t\t\tcode:  http.StatusUnauthorized,\n\t\t\t}\n\t\t},\n\t\t\"fail/sshRenew-unimplemented-jwk-provisioner\": func(t *testing.T) *authorizeTest {\n\t\t\tcl := jose.Claims{\n\t\t\t\tSubject:   \"\",\n\t\t\t\tIssuer:    validIssuer,\n\t\t\t\tNotBefore: jose.NewNumericDate(now),\n\t\t\t\tExpiry:    jose.NewNumericDate(now.Add(time.Minute)),\n\t\t\t\tAudience:  testAudiences.SSHRenew,\n\t\t\t\tID:        \"43\",\n\t\t\t}\n\t\t\traw, err := jose.Signed(sig).Claims(cl).CompactSerialize()\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn &authorizeTest{\n\t\t\t\tauth:  a,\n\t\t\t\ttoken: raw,\n\t\t\t\terr:   errors.New(\"authority.authorizeSSHRenew: provisioner.AuthorizeSSHRenew not implemented\"),\n\t\t\t\tcode:  http.StatusUnauthorized,\n\t\t\t}\n\t\t},\n\t\t\"fail/WithAuthorizeSSHRenewFunc\": func(t *testing.T) *authorizeTest {\n\t\t\taa := testAuthority(t, WithAuthorizeSSHRenewFunc(func(ctx context.Context, p *provisioner.Controller, cert *ssh.Certificate) error {\n\t\t\t\treturn errs.Forbidden(\"forbidden\")\n\t\t\t}))\n\t\t\t_, token := sshpop(aa)\n\t\t\treturn &authorizeTest{\n\t\t\t\tauth:  aa,\n\t\t\t\ttoken: token,\n\t\t\t\terr:   errors.New(\"authority.authorizeSSHRenew: forbidden\"),\n\t\t\t\tcode:  http.StatusForbidden,\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) *authorizeTest {\n\t\t\tcert, token := sshpop(a)\n\t\t\treturn &authorizeTest{\n\t\t\t\tauth:  a,\n\t\t\t\ttoken: token,\n\t\t\t\tcert:  cert,\n\t\t\t}\n\t\t},\n\t\t\"ok/WithAuthorizeSSHRenewFunc\": func(t *testing.T) *authorizeTest {\n\t\t\taa := testAuthority(t, WithAuthorizeSSHRenewFunc(func(ctx context.Context, p *provisioner.Controller, cert *ssh.Certificate) error {\n\t\t\t\treturn nil\n\t\t\t}))\n\t\t\tcert, token := sshpop(aa)\n\t\t\treturn &authorizeTest{\n\t\t\t\tauth:  aa,\n\t\t\t\ttoken: token,\n\t\t\t\tcert:  cert,\n\t\t\t}\n\t\t},\n\t}\n\n\tfor name, genTestCase := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ttc := genTestCase(t)\n\n\t\t\tgot, err := tc.auth.authorizeSSHRenew(context.Background(), tc.token)\n\t\t\tif err != nil {\n\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\tvar sc render.StatusCodedError\n\t\t\t\t\tassert.Fatal(t, errors.As(err, &sc), \"error does not implement StatusCodedError interface\")\n\t\t\t\t\tassert.Equals(t, sc.StatusCode(), tc.code)\n\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.err.Error())\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif assert.Nil(t, tc.err) {\n\t\t\t\t\tassert.Equals(t, tc.cert.Serial, got.Serial)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestAuthority_authorizeSSHRevoke(t *testing.T) {\n\ta := testAuthority(t, []Option{WithDatabase(&db.MockAuthDB{\n\t\tMIsSSHRevoked: func(serial string) (bool, error) {\n\t\t\treturn false, nil\n\t\t},\n\t\tMUseToken: func(id, tok string) (bool, error) {\n\t\t\treturn true, nil\n\t\t},\n\t})}...)\n\n\tjwk, err := jose.ReadKey(\"testdata/secrets/step_cli_key_priv.jwk\", jose.WithPassword([]byte(\"pass\")))\n\tassert.FatalError(t, err)\n\n\tsig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: jwk.Key},\n\t\t(&jose.SignerOptions{}).WithType(\"JWT\").WithHeader(\"kid\", jwk.KeyID))\n\tassert.FatalError(t, err)\n\n\tnow := time.Now().UTC()\n\tvalidIssuer := \"step-cli\"\n\n\ttype authorizeTest struct {\n\t\tauth  *Authority\n\t\ttoken string\n\t\tcert  *ssh.Certificate\n\t\terr   error\n\t\tcode  int\n\t}\n\ttests := map[string]func(t *testing.T) *authorizeTest{\n\t\t\"fail/invalid-token\": func(t *testing.T) *authorizeTest {\n\t\t\treturn &authorizeTest{\n\t\t\t\tauth:  a,\n\t\t\t\ttoken: \"foo\",\n\t\t\t\terr:   errors.New(\"authority.authorizeSSHRevoke: error parsing token\"),\n\t\t\t\tcode:  http.StatusUnauthorized,\n\t\t\t}\n\t\t},\n\t\t\"fail/invalid-subject\": func(t *testing.T) *authorizeTest {\n\t\t\tcl := jose.Claims{\n\t\t\t\tSubject:   \"\",\n\t\t\t\tIssuer:    validIssuer,\n\t\t\t\tNotBefore: jose.NewNumericDate(now),\n\t\t\t\tExpiry:    jose.NewNumericDate(now.Add(time.Minute)),\n\t\t\t\tAudience:  testAudiences.SSHRevoke,\n\t\t\t\tID:        \"43\",\n\t\t\t}\n\t\t\traw, err := jose.Signed(sig).Claims(cl).CompactSerialize()\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn &authorizeTest{\n\t\t\t\tauth:  a,\n\t\t\t\ttoken: raw,\n\t\t\t\terr:   errors.New(\"authority.authorizeSSHRevoke: jwk.AuthorizeSSHRevoke: jwk.authorizeToken; jwk token subject cannot be empty\"),\n\t\t\t\tcode:  http.StatusUnauthorized,\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) *authorizeTest {\n\t\t\tkey, err := pemutil.Read(\"./testdata/secrets/ssh_host_ca_key\")\n\t\t\tassert.FatalError(t, err)\n\t\t\tsigner, ok := key.(crypto.Signer)\n\t\t\tassert.Fatal(t, ok, \"could not cast ssh signing key to crypto signer\")\n\t\t\tsshSigner, err := ssh.NewSignerFromSigner(signer)\n\t\t\tassert.FatalError(t, err)\n\n\t\t\tcert, _jwk, err := createSSHCert(&ssh.Certificate{CertType: ssh.HostCert}, sshSigner)\n\t\t\tassert.FatalError(t, err)\n\n\t\t\tp, ok := a.provisioners.Load(\"sshpop/sshpop\")\n\t\t\tassert.Fatal(t, ok, \"sshpop provisioner not found in test authority\")\n\n\t\t\ttok, err := generateToken(strconv.FormatUint(cert.Serial, 10), p.GetName(), testAudiences.SSHRevoke[0]+\"#sshpop/sshpop\",\n\t\t\t\t[]string{\"foo.smallstep.com\"}, now, _jwk, withSSHPOPFile(cert))\n\t\t\tassert.FatalError(t, err)\n\n\t\t\treturn &authorizeTest{\n\t\t\t\tauth:  a,\n\t\t\t\ttoken: tok,\n\t\t\t\tcert:  cert,\n\t\t\t}\n\t\t},\n\t}\n\n\tfor name, genTestCase := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ttc := genTestCase(t)\n\n\t\t\tif err := tc.auth.authorizeSSHRevoke(context.Background(), tc.token); err != nil {\n\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\tvar sc render.StatusCodedError\n\t\t\t\t\tassert.Fatal(t, errors.As(err, &sc), \"error does not implement StatusCodedError interface\")\n\t\t\t\t\tassert.Equals(t, sc.StatusCode(), tc.code)\n\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.err.Error())\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tassert.Nil(t, tc.err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestAuthority_authorizeSSHRekey(t *testing.T) {\n\ta := testAuthority(t)\n\n\tjwk, err := jose.ReadKey(\"testdata/secrets/step_cli_key_priv.jwk\", jose.WithPassword([]byte(\"pass\")))\n\tassert.FatalError(t, err)\n\n\tsig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: jwk.Key},\n\t\t(&jose.SignerOptions{}).WithType(\"JWT\").WithHeader(\"kid\", jwk.KeyID))\n\tassert.FatalError(t, err)\n\n\tnow := time.Now().UTC()\n\n\tvalidIssuer := \"step-cli\"\n\n\ttype authorizeTest struct {\n\t\tauth  *Authority\n\t\ttoken string\n\t\tcert  *ssh.Certificate\n\t\terr   error\n\t\tcode  int\n\t}\n\ttests := map[string]func(t *testing.T) *authorizeTest{\n\t\t\"fail/invalid-token\": func(t *testing.T) *authorizeTest {\n\t\t\treturn &authorizeTest{\n\t\t\t\tauth:  a,\n\t\t\t\ttoken: \"foo\",\n\t\t\t\terr:   errors.New(\"authority.authorizeSSHRekey: error parsing token\"),\n\t\t\t\tcode:  http.StatusUnauthorized,\n\t\t\t}\n\t\t},\n\t\t\"fail/sshRekey-unimplemented-jwk-provisioner\": func(t *testing.T) *authorizeTest {\n\t\t\tcl := jose.Claims{\n\t\t\t\tSubject:   \"\",\n\t\t\t\tIssuer:    validIssuer,\n\t\t\t\tNotBefore: jose.NewNumericDate(now),\n\t\t\t\tExpiry:    jose.NewNumericDate(now.Add(time.Minute)),\n\t\t\t\tAudience:  testAudiences.SSHRekey,\n\t\t\t\tID:        \"43\",\n\t\t\t}\n\t\t\traw, err := jose.Signed(sig).Claims(cl).CompactSerialize()\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn &authorizeTest{\n\t\t\t\tauth:  a,\n\t\t\t\ttoken: raw,\n\t\t\t\terr:   errors.New(\"authority.authorizeSSHRekey: provisioner.AuthorizeSSHRekey not implemented\"),\n\t\t\t\tcode:  http.StatusUnauthorized,\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) *authorizeTest {\n\t\t\tkey, err := pemutil.Read(\"./testdata/secrets/ssh_host_ca_key\")\n\t\t\tassert.FatalError(t, err)\n\t\t\tsigner, ok := key.(crypto.Signer)\n\t\t\tassert.Fatal(t, ok, \"could not cast ssh signing key to crypto signer\")\n\t\t\tsshSigner, err := ssh.NewSignerFromSigner(signer)\n\t\t\tassert.FatalError(t, err)\n\n\t\t\tcert, _jwk, err := createSSHCert(&ssh.Certificate{CertType: ssh.HostCert}, sshSigner)\n\t\t\tassert.FatalError(t, err)\n\n\t\t\tp, ok := a.provisioners.Load(\"sshpop/sshpop\")\n\t\t\tassert.Fatal(t, ok, \"sshpop provisioner not found in test authority\")\n\n\t\t\ttok, err := generateToken(\"foo\", p.GetName(), testAudiences.SSHRekey[0]+\"#sshpop/sshpop\",\n\t\t\t\t[]string{\"foo.smallstep.com\"}, now, _jwk, withSSHPOPFile(cert))\n\t\t\tassert.FatalError(t, err)\n\n\t\t\treturn &authorizeTest{\n\t\t\t\tauth:  a,\n\t\t\t\ttoken: tok,\n\t\t\t\tcert:  cert,\n\t\t\t}\n\t\t},\n\t}\n\n\tfor name, genTestCase := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ttc := genTestCase(t)\n\n\t\t\tcert, signOpts, err := tc.auth.authorizeSSHRekey(context.Background(), tc.token)\n\t\t\tif err != nil {\n\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\tvar sc render.StatusCodedError\n\t\t\t\t\tassert.Fatal(t, errors.As(err, &sc), \"error does not implement StatusCodedError interface\")\n\t\t\t\t\tassert.Equals(t, sc.StatusCode(), tc.code)\n\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.err.Error())\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif assert.Nil(t, tc.err) {\n\t\t\t\t\tassert.Equals(t, tc.cert.Serial, cert.Serial)\n\t\t\t\t\tassert.Len(t, 4, signOpts)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestAuthority_AuthorizeRenewToken(t *testing.T) {\n\tctx := context.Background()\n\ttype stepProvisionerASN1 struct {\n\t\tType          int\n\t\tName          []byte\n\t\tCredentialID  []byte\n\t\tKeyValuePairs []string `asn1:\"optional,omitempty\"`\n\t}\n\n\t_, signer, err := ed25519.GenerateKey(rand.Reader)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tcsr, err := x509util.CreateCertificateRequest(\"test.example.com\", []string{\"test.example.com\"}, signer)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\t_, otherSigner, err := ed25519.GenerateKey(rand.Reader)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tgenerateX5cToken := func(a *Authority, key crypto.Signer, claims jose.Claims, opts ...provisioner.SignOption) (string, *x509.Certificate) {\n\t\tchain, err := a.SignWithContext(ctx, csr, provisioner.SignOptions{}, opts...)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\tvar x5c []string\n\t\tfor _, c := range chain {\n\t\t\tx5c = append(x5c, base64.StdEncoding.EncodeToString(c.Raw))\n\t\t}\n\n\t\tso := new(jose.SignerOptions)\n\t\tso.WithType(\"JWT\")\n\t\tso.WithHeader(\"x5cInsecure\", x5c)\n\t\tsig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.EdDSA, Key: key}, so)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\ts, err := jose.Signed(sig).Claims(claims).CompactSerialize()\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\treturn s, chain[0]\n\t}\n\n\tnow := time.Now()\n\ta1 := testAuthority(t)\n\tt1, c1 := generateX5cToken(a1, signer, jose.Claims{\n\t\tAudience:  []string{\"https://example.com/1.0/renew\"},\n\t\tSubject:   \"test.example.com\",\n\t\tIssuer:    \"step-ca-client/1.0\",\n\t\tNotBefore: jose.NewNumericDate(now),\n\t\tExpiry:    jose.NewNumericDate(now.Add(5 * time.Minute)),\n\t}, provisioner.CertificateEnforcerFunc(func(cert *x509.Certificate) error {\n\t\tcert.NotBefore = now\n\t\tcert.NotAfter = now.Add(time.Hour)\n\t\tb, err := asn1.Marshal(stepProvisionerASN1{int(provisioner.TypeJWK), []byte(\"step-cli\"), nil, nil})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tcert.ExtraExtensions = append(cert.ExtraExtensions, pkix.Extension{\n\t\t\tId:    asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 37476, 9000, 64, 1},\n\t\t\tValue: b,\n\t\t})\n\t\treturn nil\n\t}))\n\tt2, c2 := generateX5cToken(a1, signer, jose.Claims{\n\t\tAudience:  []string{\"https://example.com/1.0/renew\"},\n\t\tSubject:   \"test.example.com\",\n\t\tIssuer:    \"step-ca-client/1.0\",\n\t\tNotBefore: jose.NewNumericDate(now),\n\t\tExpiry:    jose.NewNumericDate(now.Add(5 * time.Minute)),\n\t\tIssuedAt:  jose.NewNumericDate(now),\n\t}, provisioner.CertificateEnforcerFunc(func(cert *x509.Certificate) error {\n\t\tcert.NotBefore = now.Add(-time.Hour)\n\t\tcert.NotAfter = now.Add(-time.Minute)\n\t\tb, err := asn1.Marshal(stepProvisionerASN1{int(provisioner.TypeJWK), []byte(\"step-cli\"), nil, nil})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tcert.ExtraExtensions = append(cert.ExtraExtensions, pkix.Extension{\n\t\t\tId:    asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 37476, 9000, 64, 1},\n\t\t\tValue: b,\n\t\t})\n\t\treturn nil\n\t}))\n\tt3, c3 := generateX5cToken(a1, signer, jose.Claims{\n\t\tAudience:  []string{\"https://example.com/1.0/renew\"},\n\t\tSubject:   \"test.example.com\",\n\t\tIssuer:    \"step-cli\",\n\t\tNotBefore: jose.NewNumericDate(now),\n\t\tExpiry:    jose.NewNumericDate(now.Add(5 * time.Minute)),\n\t}, provisioner.CertificateEnforcerFunc(func(cert *x509.Certificate) error {\n\t\tcert.NotBefore = now\n\t\tcert.NotAfter = now.Add(time.Hour)\n\t\tb, err := asn1.Marshal(stepProvisionerASN1{int(provisioner.TypeJWK), []byte(\"step-cli\"), nil, nil})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tcert.ExtraExtensions = append(cert.ExtraExtensions, pkix.Extension{\n\t\t\tId:    asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 37476, 9000, 64, 1},\n\t\t\tValue: b,\n\t\t})\n\t\treturn nil\n\t}))\n\ta4 := testAuthority(t)\n\ta4.db = &db.MockAuthDB{\n\t\tMUseToken: func(id, tok string) (bool, error) {\n\t\t\treturn true, nil\n\t\t},\n\t\tMGetCertificateData: func(serialNumber string) (*db.CertificateData, error) {\n\t\t\treturn &db.CertificateData{\n\t\t\t\tProvisioner: &db.ProvisionerData{ID: \"Max:IMi94WBNI6gP5cNHXlZYNUzvMjGdHyBRmFoo-lCEaqk\", Name: \"Max\"},\n\t\t\t\tRaInfo:      &provisioner.RAInfo{ProvisionerName: \"ra\"},\n\t\t\t}, nil\n\t\t},\n\t}\n\tt4, c4 := generateX5cToken(a1, signer, jose.Claims{\n\t\tAudience:  []string{\"https://ra.example.com/1.0/renew\"},\n\t\tSubject:   \"test.example.com\",\n\t\tIssuer:    \"step-ca-client/1.0\",\n\t\tNotBefore: jose.NewNumericDate(now),\n\t\tExpiry:    jose.NewNumericDate(now.Add(5 * time.Minute)),\n\t}, provisioner.CertificateEnforcerFunc(func(cert *x509.Certificate) error {\n\t\tcert.NotBefore = now\n\t\tcert.NotAfter = now.Add(time.Hour)\n\t\tb, err := asn1.Marshal(stepProvisionerASN1{int(provisioner.TypeJWK), []byte(\"step-cli\"), nil, nil})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tcert.ExtraExtensions = append(cert.ExtraExtensions, pkix.Extension{\n\t\t\tId:    asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 37476, 9000, 64, 1},\n\t\t\tValue: b,\n\t\t})\n\t\treturn nil\n\t}))\n\tbadSigner, _ := generateX5cToken(a1, otherSigner, jose.Claims{\n\t\tAudience:  []string{\"https://example.com/1.0/renew\"},\n\t\tSubject:   \"test.example.com\",\n\t\tIssuer:    \"step-ca-client/1.0\",\n\t\tNotBefore: jose.NewNumericDate(now),\n\t\tExpiry:    jose.NewNumericDate(now.Add(5 * time.Minute)),\n\t}, provisioner.CertificateEnforcerFunc(func(cert *x509.Certificate) error {\n\t\tcert.NotBefore = now\n\t\tcert.NotAfter = now.Add(time.Hour)\n\t\tb, err := asn1.Marshal(stepProvisionerASN1{int(provisioner.TypeJWK), []byte(\"foobar\"), nil, nil})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tcert.ExtraExtensions = append(cert.ExtraExtensions, pkix.Extension{\n\t\t\tId:    asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 37476, 9000, 64, 1},\n\t\t\tValue: b,\n\t\t})\n\t\treturn nil\n\t}))\n\tbadProvisioner, _ := generateX5cToken(a1, signer, jose.Claims{\n\t\tAudience:  []string{\"https://example.com/1.0/renew\"},\n\t\tSubject:   \"test.example.com\",\n\t\tIssuer:    \"step-ca-client/1.0\",\n\t\tNotBefore: jose.NewNumericDate(now),\n\t\tExpiry:    jose.NewNumericDate(now.Add(5 * time.Minute)),\n\t}, provisioner.CertificateEnforcerFunc(func(cert *x509.Certificate) error {\n\t\tcert.NotBefore = now\n\t\tcert.NotAfter = now.Add(time.Hour)\n\t\tb, err := asn1.Marshal(stepProvisionerASN1{int(provisioner.TypeJWK), []byte(\"foobar\"), nil, nil})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tcert.ExtraExtensions = append(cert.ExtraExtensions, pkix.Extension{\n\t\t\tId:    asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 37476, 9000, 64, 1},\n\t\t\tValue: b,\n\t\t})\n\t\treturn nil\n\t}))\n\tbadIssuer, _ := generateX5cToken(a1, signer, jose.Claims{\n\t\tAudience:  []string{\"https://example.com/1.0/renew\"},\n\t\tSubject:   \"test.example.com\",\n\t\tIssuer:    \"bad-issuer\",\n\t\tNotBefore: jose.NewNumericDate(now),\n\t\tExpiry:    jose.NewNumericDate(now.Add(5 * time.Minute)),\n\t}, provisioner.CertificateEnforcerFunc(func(cert *x509.Certificate) error {\n\t\tcert.NotBefore = now\n\t\tcert.NotAfter = now.Add(time.Hour)\n\t\tb, err := asn1.Marshal(stepProvisionerASN1{int(provisioner.TypeJWK), []byte(\"step-cli\"), nil, nil})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tcert.ExtraExtensions = append(cert.ExtraExtensions, pkix.Extension{\n\t\t\tId:    asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 37476, 9000, 64, 1},\n\t\t\tValue: b,\n\t\t})\n\t\treturn nil\n\t}))\n\tbadSubject, _ := generateX5cToken(a1, signer, jose.Claims{\n\t\tAudience:  []string{\"https://example.com/1.0/renew\"},\n\t\tSubject:   \"bad-subject\",\n\t\tIssuer:    \"step-ca-client/1.0\",\n\t\tNotBefore: jose.NewNumericDate(now),\n\t\tExpiry:    jose.NewNumericDate(now.Add(5 * time.Minute)),\n\t}, provisioner.CertificateEnforcerFunc(func(cert *x509.Certificate) error {\n\t\tcert.NotBefore = now\n\t\tcert.NotAfter = now.Add(time.Hour)\n\t\tb, err := asn1.Marshal(stepProvisionerASN1{int(provisioner.TypeJWK), []byte(\"step-cli\"), nil, nil})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tcert.ExtraExtensions = append(cert.ExtraExtensions, pkix.Extension{\n\t\t\tId:    asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 37476, 9000, 64, 1},\n\t\t\tValue: b,\n\t\t})\n\t\treturn nil\n\t}))\n\tbadNotBefore, _ := generateX5cToken(a1, signer, jose.Claims{\n\t\tAudience:  []string{\"https://example.com/1.0/sign\"},\n\t\tSubject:   \"test.example.com\",\n\t\tIssuer:    \"step-ca-client/1.0\",\n\t\tNotBefore: jose.NewNumericDate(now.Add(5 * time.Minute)),\n\t\tExpiry:    jose.NewNumericDate(now.Add(10 * time.Minute)),\n\t}, provisioner.CertificateEnforcerFunc(func(cert *x509.Certificate) error {\n\t\tcert.NotBefore = now\n\t\tcert.NotAfter = now.Add(time.Hour)\n\t\tb, err := asn1.Marshal(stepProvisionerASN1{int(provisioner.TypeJWK), []byte(\"step-cli\"), nil, nil})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tcert.ExtraExtensions = append(cert.ExtraExtensions, pkix.Extension{\n\t\t\tId:    asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 37476, 9000, 64, 1},\n\t\t\tValue: b,\n\t\t})\n\t\treturn nil\n\t}))\n\tbadExpiry, _ := generateX5cToken(a1, signer, jose.Claims{\n\t\tAudience:  []string{\"https://example.com/1.0/sign\"},\n\t\tSubject:   \"test.example.com\",\n\t\tIssuer:    \"step-ca-client/1.0\",\n\t\tNotBefore: jose.NewNumericDate(now.Add(-5 * time.Minute)),\n\t\tExpiry:    jose.NewNumericDate(now.Add(-time.Minute)),\n\t}, provisioner.CertificateEnforcerFunc(func(cert *x509.Certificate) error {\n\t\tcert.NotBefore = now\n\t\tcert.NotAfter = now.Add(time.Hour)\n\t\tb, err := asn1.Marshal(stepProvisionerASN1{int(provisioner.TypeJWK), []byte(\"step-cli\"), nil, nil})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tcert.ExtraExtensions = append(cert.ExtraExtensions, pkix.Extension{\n\t\t\tId:    asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 37476, 9000, 64, 1},\n\t\t\tValue: b,\n\t\t})\n\t\treturn nil\n\t}))\n\tbadIssuedAt, _ := generateX5cToken(a1, signer, jose.Claims{\n\t\tAudience:  []string{\"https://example.com/1.0/sign\"},\n\t\tSubject:   \"test.example.com\",\n\t\tIssuer:    \"step-ca-client/1.0\",\n\t\tNotBefore: jose.NewNumericDate(now),\n\t\tExpiry:    jose.NewNumericDate(now.Add(5 * time.Minute)),\n\t\tIssuedAt:  jose.NewNumericDate(now.Add(5 * time.Minute)),\n\t}, provisioner.CertificateEnforcerFunc(func(cert *x509.Certificate) error {\n\t\tcert.NotBefore = now\n\t\tcert.NotAfter = now.Add(time.Hour)\n\t\tb, err := asn1.Marshal(stepProvisionerASN1{int(provisioner.TypeJWK), []byte(\"step-cli\"), nil, nil})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tcert.ExtraExtensions = append(cert.ExtraExtensions, pkix.Extension{\n\t\t\tId:    asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 37476, 9000, 64, 1},\n\t\t\tValue: b,\n\t\t})\n\t\treturn nil\n\t}))\n\tbadAudience, _ := generateX5cToken(a1, signer, jose.Claims{\n\t\tAudience:  []string{\"https://example.com/1.0/sign\"},\n\t\tSubject:   \"test.example.com\",\n\t\tIssuer:    \"step-ca-client/1.0\",\n\t\tNotBefore: jose.NewNumericDate(now),\n\t\tExpiry:    jose.NewNumericDate(now.Add(5 * time.Minute)),\n\t}, provisioner.CertificateEnforcerFunc(func(cert *x509.Certificate) error {\n\t\tcert.NotBefore = now\n\t\tcert.NotAfter = now.Add(time.Hour)\n\t\tb, err := asn1.Marshal(stepProvisionerASN1{int(provisioner.TypeJWK), []byte(\"step-cli\"), nil, nil})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tcert.ExtraExtensions = append(cert.ExtraExtensions, pkix.Extension{\n\t\t\tId:    asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 37476, 9000, 64, 1},\n\t\t\tValue: b,\n\t\t})\n\t\treturn nil\n\t}))\n\n\ttype args struct {\n\t\tctx context.Context\n\t\tott string\n\t}\n\ttests := []struct {\n\t\tname      string\n\t\tauthority *Authority\n\t\targs      args\n\t\twant      *x509.Certificate\n\t\twantErr   bool\n\t}{\n\t\t{\"ok\", a1, args{ctx, t1}, c1, false},\n\t\t{\"ok expired cert\", a1, args{ctx, t2}, c2, false},\n\t\t{\"ok provisioner issuer\", a1, args{ctx, t3}, c3, false},\n\t\t{\"ok ra provisioner\", a4, args{ctx, t4}, c4, false},\n\t\t{\"fail token\", a1, args{ctx, \"not.a.token\"}, nil, true},\n\t\t{\"fail token reuse\", a1, args{ctx, t1}, nil, true},\n\t\t{\"fail token signature\", a1, args{ctx, badSigner}, nil, true},\n\t\t{\"fail token provisioner\", a1, args{ctx, badProvisioner}, nil, true},\n\t\t{\"fail token iss\", a1, args{ctx, badIssuer}, nil, true},\n\t\t{\"fail token sub\", a1, args{ctx, badSubject}, nil, true},\n\t\t{\"fail token iat\", a1, args{ctx, badNotBefore}, nil, true},\n\t\t{\"fail token iat\", a1, args{ctx, badExpiry}, nil, true},\n\t\t{\"fail token iat\", a1, args{ctx, badIssuedAt}, nil, true},\n\t\t{\"fail token aud\", a1, args{ctx, badAudience}, nil, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := tt.authority.AuthorizeRenewToken(tt.args.ctx, tt.args.ott)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"Authority.AuthorizeRenewToken() error = %+v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"Authority.AuthorizeRenewToken() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "authority/config/config.go",
    "content": "package config\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net\"\n\t\"os\"\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n\n\t\"github.com/smallstep/linkedca\"\n\tkms \"go.step.sm/crypto/kms/apiv1\"\n\n\t\"github.com/smallstep/certificates/authority/policy\"\n\t\"github.com/smallstep/certificates/authority/provisioner\"\n\tcas \"github.com/smallstep/certificates/cas/apiv1\"\n\t\"github.com/smallstep/certificates/db\"\n\t\"github.com/smallstep/certificates/templates\"\n)\n\nconst (\n\tlegacyAuthority = \"step-certificate-authority\"\n)\n\nvar (\n\t// DefaultBackdate length of time to backdate certificates to avoid\n\t// clock skew validation issues.\n\tDefaultBackdate = time.Minute\n\t// DefaultDisableRenewal disables renewals per provisioner.\n\tDefaultDisableRenewal = false\n\t// DefaultAllowRenewalAfterExpiry allows renewals even if the certificate is\n\t// expired.\n\tDefaultAllowRenewalAfterExpiry = false\n\t// DefaultEnableSSHCA enable SSH CA features per provisioner or globally\n\t// for all provisioners.\n\tDefaultEnableSSHCA = false\n\t// DefaultDisableSmallstepExtensions is the default value for the\n\t// DisableSmallstepExtensions provisioner claim.\n\tDefaultDisableSmallstepExtensions = false\n\t// DefaultCRLCacheDuration is the default cache duration for the CRL.\n\tDefaultCRLCacheDuration = &provisioner.Duration{Duration: 24 * time.Hour}\n\t// DefaultCRLExpiredDuration is the default duration in which expired\n\t// certificates will remain in the CRL after expiration.\n\tDefaultCRLExpiredDuration = time.Hour\n\t// GlobalProvisionerClaims is the default duration that expired certificates\n\t// remain in the CRL after expiration.\n\tGlobalProvisionerClaims = provisioner.Claims{\n\t\tMinTLSDur:                  &provisioner.Duration{Duration: 5 * time.Minute}, // TLS certs\n\t\tMaxTLSDur:                  &provisioner.Duration{Duration: 24 * time.Hour},\n\t\tDefaultTLSDur:              &provisioner.Duration{Duration: 24 * time.Hour},\n\t\tMinUserSSHDur:              &provisioner.Duration{Duration: 5 * time.Minute}, // User SSH certs\n\t\tMaxUserSSHDur:              &provisioner.Duration{Duration: 24 * time.Hour},\n\t\tDefaultUserSSHDur:          &provisioner.Duration{Duration: 16 * time.Hour},\n\t\tMinHostSSHDur:              &provisioner.Duration{Duration: 5 * time.Minute}, // Host SSH certs\n\t\tMaxHostSSHDur:              &provisioner.Duration{Duration: 30 * 24 * time.Hour},\n\t\tDefaultHostSSHDur:          &provisioner.Duration{Duration: 30 * 24 * time.Hour},\n\t\tEnableSSHCA:                &DefaultEnableSSHCA,\n\t\tDisableRenewal:             &DefaultDisableRenewal,\n\t\tAllowRenewalAfterExpiry:    &DefaultAllowRenewalAfterExpiry,\n\t\tDisableSmallstepExtensions: &DefaultDisableSmallstepExtensions,\n\t}\n)\n\n// Config represents the CA configuration and it's mapped to a JSON object.\ntype Config struct {\n\tRoot             multiString          `json:\"root\"`\n\tFederatedRoots   []string             `json:\"federatedRoots\"`\n\tIntermediateCert string               `json:\"crt\"`\n\tIntermediateKey  string               `json:\"key\"`\n\tAddress          string               `json:\"address\"`\n\tInsecureAddress  string               `json:\"insecureAddress\"`\n\tDNSNames         []string             `json:\"dnsNames\"`\n\tKMS              *kms.Options         `json:\"kms,omitempty\"`\n\tSSH              *SSHConfig           `json:\"ssh,omitempty\"`\n\tLogger           json.RawMessage      `json:\"logger,omitempty\"`\n\tDB               *db.Config           `json:\"db,omitempty\"`\n\tMonitoring       json.RawMessage      `json:\"monitoring,omitempty\"`\n\tAuthorityConfig  *AuthConfig          `json:\"authority,omitempty\"`\n\tTLS              *TLSOptions          `json:\"tls,omitempty\"`\n\tPassword         string               `json:\"password,omitempty\"`\n\tTemplates        *templates.Templates `json:\"templates,omitempty\"`\n\tCommonName       string               `json:\"commonName,omitempty\"`\n\tCRL              *CRLConfig           `json:\"crl,omitempty\"`\n\tMetricsAddress   string               `json:\"metricsAddress,omitempty\"`\n\tSkipValidation   bool                 `json:\"-\"`\n\n\t// Keeps record of the filename the Config is read from\n\tloadedFromFilepath string\n}\n\n// CRLConfig represents config options for CRL generation\ntype CRLConfig struct {\n\tEnabled          bool                  `json:\"enabled\"`\n\tGenerateOnRevoke bool                  `json:\"generateOnRevoke,omitempty\"`\n\tCacheDuration    *provisioner.Duration `json:\"cacheDuration,omitempty\"`\n\tRenewPeriod      *provisioner.Duration `json:\"renewPeriod,omitempty\"`\n\tIDPurl           string                `json:\"idpURL,omitempty\"`\n}\n\n// IsEnabled returns if the CRL is enabled.\nfunc (c *CRLConfig) IsEnabled() bool {\n\treturn c != nil && c.Enabled\n}\n\n// Validate validates the CRL configuration.\nfunc (c *CRLConfig) Validate() error {\n\tif c == nil {\n\t\treturn nil\n\t}\n\n\tif c.CacheDuration != nil && c.CacheDuration.Duration < 0 {\n\t\treturn errors.New(\"crl.cacheDuration must be greater than or equal to 0\")\n\t}\n\n\tif c.RenewPeriod != nil && c.RenewPeriod.Duration < 0 {\n\t\treturn errors.New(\"crl.renewPeriod must be greater than or equal to 0\")\n\t}\n\n\tif c.RenewPeriod != nil && c.CacheDuration != nil &&\n\t\tc.RenewPeriod.Duration > c.CacheDuration.Duration {\n\t\treturn errors.New(\"crl.cacheDuration must be greater than or equal to crl.renewPeriod\")\n\t}\n\n\treturn nil\n}\n\n// TickerDuration the renewal ticker duration. This is set by renewPeriod, of it\n// is not set is ~2/3 of cacheDuration.\nfunc (c *CRLConfig) TickerDuration() time.Duration {\n\tif !c.IsEnabled() {\n\t\treturn 0\n\t}\n\n\tif c.RenewPeriod != nil && c.RenewPeriod.Duration > 0 {\n\t\treturn c.RenewPeriod.Duration\n\t}\n\n\treturn (c.CacheDuration.Duration / 3) * 2\n}\n\n// ASN1DN contains ASN1.DN attributes that are used in Subject and Issuer\n// x509 Certificate blocks.\ntype ASN1DN struct {\n\tCountry            string `json:\"country,omitempty\"`\n\tOrganization       string `json:\"organization,omitempty\"`\n\tOrganizationalUnit string `json:\"organizationalUnit,omitempty\"`\n\tLocality           string `json:\"locality,omitempty\"`\n\tProvince           string `json:\"province,omitempty\"`\n\tStreetAddress      string `json:\"streetAddress,omitempty\"`\n\tSerialNumber       string `json:\"serialNumber,omitempty\"`\n\tCommonName         string `json:\"commonName,omitempty\"`\n}\n\n// AuthConfig represents the configuration options for the authority. An\n// underlaying registration authority can also be configured using the\n// cas.Options.\ntype AuthConfig struct {\n\t*cas.Options\n\tAuthorityID          string                `json:\"authorityId,omitempty\"`\n\tDeploymentType       string                `json:\"deploymentType,omitempty\"`\n\tProvisioners         provisioner.List      `json:\"provisioners,omitempty\"`\n\tAdmins               []*linkedca.Admin     `json:\"-\"`\n\tTemplate             *ASN1DN               `json:\"template,omitempty\"`\n\tClaims               *provisioner.Claims   `json:\"claims,omitempty\"`\n\tPolicy               *policy.Options       `json:\"policy,omitempty\"`\n\tDisableIssuedAtCheck bool                  `json:\"disableIssuedAtCheck,omitempty\"`\n\tBackdate             *provisioner.Duration `json:\"backdate,omitempty\"`\n\tEnableAdmin          bool                  `json:\"enableAdmin,omitempty\"`\n\tDisableGetSSHHosts   bool                  `json:\"disableGetSSHHosts,omitempty\"`\n}\n\n// init initializes the required fields in the AuthConfig if they are not\n// provided.\nfunc (c *AuthConfig) init() {\n\tif c.Provisioners == nil {\n\t\tc.Provisioners = provisioner.List{}\n\t}\n\tif c.Template == nil {\n\t\tc.Template = &ASN1DN{}\n\t}\n\tif c.Backdate == nil {\n\t\tc.Backdate = &provisioner.Duration{\n\t\t\tDuration: DefaultBackdate,\n\t\t}\n\t}\n}\n\n// Validate validates the authority configuration.\nfunc (c *AuthConfig) Validate(provisioner.Audiences) error {\n\tif c == nil {\n\t\treturn errors.New(\"authority cannot be undefined\")\n\t}\n\n\t// Initialize required fields.\n\tc.init()\n\n\t// Check that only one K8sSA is enabled\n\tvar k8sCount int\n\tfor _, p := range c.Provisioners {\n\t\tif p.GetType() == provisioner.TypeK8sSA {\n\t\t\tk8sCount++\n\t\t}\n\t}\n\tif k8sCount > 1 {\n\t\treturn errors.New(\"cannot have more than one kubernetes service account provisioner\")\n\t}\n\n\tif c.Backdate.Duration < 0 {\n\t\treturn errors.New(\"authority.backdate cannot be less than 0\")\n\t}\n\n\treturn nil\n}\n\n// LoadConfiguration parses the given filename in JSON format and returns the\n// configuration struct.\nfunc LoadConfiguration(filename string) (*Config, error) {\n\tf, err := os.Open(filename)\n\tif err != nil {\n\t\treturn nil, errors.Wrapf(err, \"error opening %s\", filename)\n\t}\n\tdefer f.Close()\n\n\tvar c Config\n\tif err := json.NewDecoder(f).Decode(&c); err != nil {\n\t\treturn nil, errors.Wrapf(err, \"error parsing %s\", filename)\n\t}\n\n\t// store filename that was read to populate Config\n\tc.loadedFromFilepath = filename\n\n\t// initialize the Config\n\tc.Init()\n\n\treturn &c, nil\n}\n\n// Init initializes the minimal configuration required to create an authority. This\n// is mainly used on embedded authorities.\nfunc (c *Config) Init() {\n\tif c.DNSNames == nil {\n\t\tc.DNSNames = []string{\"localhost\", \"127.0.0.1\", \"::1\"}\n\t}\n\tif c.TLS == nil {\n\t\tc.TLS = &DefaultTLSOptions\n\t}\n\tif c.AuthorityConfig == nil {\n\t\tc.AuthorityConfig = &AuthConfig{}\n\t}\n\tif c.CommonName == \"\" {\n\t\tc.CommonName = \"Step Online CA\"\n\t}\n\tif c.CRL != nil && c.CRL.Enabled && c.CRL.CacheDuration == nil {\n\t\tc.CRL.CacheDuration = DefaultCRLCacheDuration\n\t}\n\tc.AuthorityConfig.init()\n}\n\n// Save saves the configuration to the given filename.\nfunc (c *Config) Save(filename string) error {\n\tvar b bytes.Buffer\n\tenc := json.NewEncoder(&b)\n\tenc.SetIndent(\"\", \"\\t\")\n\tif err := enc.Encode(c); err != nil { //nolint:gosec // config struct contains password field by design\n\t\treturn fmt.Errorf(\"error encoding configuration: %w\", err)\n\t}\n\tif err := os.WriteFile(filename, b.Bytes(), 0600); err != nil {\n\t\treturn fmt.Errorf(\"error writing %q: %w\", filename, err)\n\t}\n\treturn nil\n}\n\n// Commit saves the current configuration to the same\n// file it was initially loaded from.\n//\n// TODO(hs): rename Save() to WriteTo() and replace this\n// with Save()? Or is Commit clear enough.\nfunc (c *Config) Commit() error {\n\tif !c.WasLoadedFromFile() {\n\t\treturn errors.New(\"cannot commit configuration if not loaded from file\")\n\t}\n\treturn c.Save(c.loadedFromFilepath)\n}\n\n// WasLoadedFromFile returns whether or not the Config was\n// loaded from a file.\nfunc (c *Config) WasLoadedFromFile() bool {\n\treturn c.loadedFromFilepath != \"\"\n}\n\n// Filepath returns the path to the file the Config was\n// loaded from.\nfunc (c *Config) Filepath() string {\n\treturn c.loadedFromFilepath\n}\n\n// Validate validates the configuration.\nfunc (c *Config) Validate() error {\n\tswitch {\n\tcase c.SkipValidation:\n\t\treturn nil\n\tcase c.Address == \"\":\n\t\treturn errors.New(\"address cannot be empty\")\n\tcase len(c.DNSNames) == 0:\n\t\treturn errors.New(\"dnsNames cannot be empty\")\n\tcase c.AuthorityConfig == nil:\n\t\treturn errors.New(\"authority cannot be nil\")\n\t}\n\n\t// Options holds the RA/CAS configuration.\n\tra := c.AuthorityConfig.Options\n\t// The default RA/CAS requires root, crt and key.\n\tif ra.Is(cas.SoftCAS) {\n\t\tswitch {\n\t\tcase c.Root.HasEmpties():\n\t\t\treturn errors.New(\"root cannot be empty\")\n\t\tcase c.IntermediateCert == \"\":\n\t\t\treturn errors.New(\"crt cannot be empty\")\n\t\tcase c.IntermediateKey == \"\":\n\t\t\treturn errors.New(\"key cannot be empty\")\n\t\t}\n\t}\n\n\t// Validate address (a port is required)\n\tif _, _, err := net.SplitHostPort(c.Address); err != nil {\n\t\treturn errors.Errorf(\"invalid address %s\", c.Address)\n\t}\n\n\tif addr := c.MetricsAddress; addr != \"\" {\n\t\tif _, _, err := net.SplitHostPort(addr); err != nil {\n\t\t\treturn errors.Errorf(\"invalid metrics address %q\", c.Address)\n\t\t}\n\t}\n\n\tif c.TLS == nil {\n\t\tc.TLS = &DefaultTLSOptions\n\t} else {\n\t\tif len(c.TLS.CipherSuites) == 0 {\n\t\t\tc.TLS.CipherSuites = DefaultTLSOptions.CipherSuites\n\t\t}\n\t\tif c.TLS.MaxVersion == 0 {\n\t\t\tc.TLS.MaxVersion = DefaultTLSOptions.MaxVersion\n\t\t}\n\t\tif c.TLS.MinVersion == 0 {\n\t\t\tc.TLS.MinVersion = DefaultTLSOptions.MinVersion\n\t\t}\n\t\tif c.TLS.MinVersion > c.TLS.MaxVersion {\n\t\t\treturn errors.New(\"tls minVersion cannot exceed tls maxVersion\")\n\t\t}\n\t\tc.TLS.Renegotiation = c.TLS.Renegotiation || DefaultTLSOptions.Renegotiation\n\t}\n\n\t// Validate KMS options, nil is ok.\n\tif err := c.KMS.Validate(); err != nil {\n\t\treturn err\n\t}\n\n\t// Validate RA/CAS options, nil is ok.\n\tif err := ra.Validate(); err != nil {\n\t\treturn err\n\t}\n\n\t// Validate ssh: nil is ok\n\tif err := c.SSH.Validate(); err != nil {\n\t\treturn err\n\t}\n\n\t// Validate templates: nil is ok\n\tif err := c.Templates.Validate(); err != nil {\n\t\treturn err\n\t}\n\n\t// Validate crl config: nil is ok\n\tif err := c.CRL.Validate(); err != nil {\n\t\treturn err\n\t}\n\n\treturn c.AuthorityConfig.Validate(c.GetAudiences())\n}\n\n// GetAudiences returns the legacy and possible urls without the ports that will\n// be used as the default provisioner audiences. The CA might have proxies in\n// front so we cannot rely on the port.\nfunc (c *Config) GetAudiences() provisioner.Audiences {\n\taudiences := provisioner.Audiences{\n\t\tSign:      []string{legacyAuthority},\n\t\tRevoke:    []string{legacyAuthority},\n\t\tSSHSign:   []string{},\n\t\tSSHRevoke: []string{},\n\t\tSSHRenew:  []string{},\n\t}\n\n\tfor _, name := range c.DNSNames {\n\t\thostname := toHostname(name)\n\t\taudiences.Sign = append(audiences.Sign,\n\t\t\tfmt.Sprintf(\"https://%s/1.0/sign\", hostname),\n\t\t\tfmt.Sprintf(\"https://%s/sign\", hostname),\n\t\t\tfmt.Sprintf(\"https://%s/1.0/ssh/sign\", hostname),\n\t\t\tfmt.Sprintf(\"https://%s/ssh/sign\", hostname))\n\t\taudiences.Renew = append(audiences.Renew,\n\t\t\tfmt.Sprintf(\"https://%s/1.0/renew\", hostname),\n\t\t\tfmt.Sprintf(\"https://%s/renew\", hostname))\n\t\taudiences.Revoke = append(audiences.Revoke,\n\t\t\tfmt.Sprintf(\"https://%s/1.0/revoke\", hostname),\n\t\t\tfmt.Sprintf(\"https://%s/revoke\", hostname))\n\t\taudiences.SSHSign = append(audiences.SSHSign,\n\t\t\tfmt.Sprintf(\"https://%s/1.0/ssh/sign\", hostname),\n\t\t\tfmt.Sprintf(\"https://%s/ssh/sign\", hostname),\n\t\t\tfmt.Sprintf(\"https://%s/1.0/sign\", hostname),\n\t\t\tfmt.Sprintf(\"https://%s/sign\", hostname))\n\t\taudiences.SSHRevoke = append(audiences.SSHRevoke,\n\t\t\tfmt.Sprintf(\"https://%s/1.0/ssh/revoke\", hostname),\n\t\t\tfmt.Sprintf(\"https://%s/ssh/revoke\", hostname))\n\t\taudiences.SSHRenew = append(audiences.SSHRenew,\n\t\t\tfmt.Sprintf(\"https://%s/1.0/ssh/renew\", hostname),\n\t\t\tfmt.Sprintf(\"https://%s/ssh/renew\", hostname))\n\t\taudiences.SSHRekey = append(audiences.SSHRekey,\n\t\t\tfmt.Sprintf(\"https://%s/1.0/ssh/rekey\", hostname),\n\t\t\tfmt.Sprintf(\"https://%s/ssh/rekey\", hostname))\n\t}\n\n\treturn audiences\n}\n\n// Audience returns the list of audiences for a given path.\nfunc (c *Config) Audience(path string) []string {\n\taudiences := make([]string, len(c.DNSNames)+1)\n\tfor i, name := range c.DNSNames {\n\t\thostname := toHostname(name)\n\t\taudiences[i] = \"https://\" + hostname + path\n\t}\n\t// For backward compatibility\n\taudiences[len(c.DNSNames)] = path\n\treturn audiences\n}\n\nfunc toHostname(name string) string {\n\t// ensure an IPv6 address is represented with square brackets when used as hostname\n\tif ip := net.ParseIP(name); ip != nil && ip.To4() == nil {\n\t\tname = \"[\" + name + \"]\"\n\t}\n\treturn name\n}\n"
  },
  {
    "path": "authority/config/config_test.go",
    "content": "package config\n\nimport (\n\t\"fmt\"\n\t\"reflect\"\n\t\"testing\"\n\n\t\"github.com/pkg/errors\"\n\t\"github.com/smallstep/assert\"\n\t\"github.com/smallstep/certificates/authority/provisioner\"\n\t_ \"github.com/smallstep/certificates/cas\"\n\t\"go.step.sm/crypto/jose\"\n)\n\nfunc TestConfigValidate(t *testing.T) {\n\tmaxjwk, err := jose.ReadKey(\"../testdata/secrets/max_pub.jwk\")\n\tassert.FatalError(t, err)\n\tclijwk, err := jose.ReadKey(\"../testdata/secrets/step_cli_key_pub.jwk\")\n\tassert.FatalError(t, err)\n\tac := &AuthConfig{\n\t\tProvisioners: provisioner.List{\n\t\t\t&provisioner.JWK{\n\t\t\t\tName: \"Max\",\n\t\t\t\tType: \"JWK\",\n\t\t\t\tKey:  maxjwk,\n\t\t\t},\n\t\t\t&provisioner.JWK{\n\t\t\t\tName: \"step-cli\",\n\t\t\t\tType: \"JWK\",\n\t\t\t\tKey:  clijwk,\n\t\t\t},\n\t\t},\n\t}\n\n\ttype ConfigValidateTest struct {\n\t\tconfig *Config\n\t\terr    error\n\t\ttls    *TLSOptions\n\t}\n\ttests := map[string]func(*testing.T) ConfigValidateTest{\n\t\t\"skip-validation\": func(t *testing.T) ConfigValidateTest {\n\t\t\treturn ConfigValidateTest{\n\t\t\t\tconfig: &Config{\n\t\t\t\t\tSkipValidation: true,\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"empty-address\": func(t *testing.T) ConfigValidateTest {\n\t\t\treturn ConfigValidateTest{\n\t\t\t\tconfig: &Config{\n\t\t\t\t\tRoot:             []string{\"../testdata/secrets/root_ca.crt\"},\n\t\t\t\t\tIntermediateCert: \"../testdata/secrets/intermediate_ca.crt\",\n\t\t\t\t\tIntermediateKey:  \"../testdata/secrets/intermediate_ca_key\",\n\t\t\t\t\tDNSNames:         []string{\"test.smallstep.com\"},\n\t\t\t\t\tPassword:         \"pass\",\n\t\t\t\t\tAuthorityConfig:  ac,\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"address cannot be empty\"),\n\t\t\t}\n\t\t},\n\t\t\"invalid-address\": func(t *testing.T) ConfigValidateTest {\n\t\t\treturn ConfigValidateTest{\n\t\t\t\tconfig: &Config{\n\t\t\t\t\tAddress:          \"127.0.0.1\",\n\t\t\t\t\tRoot:             []string{\"../testdata/secrets/root_ca.crt\"},\n\t\t\t\t\tIntermediateCert: \"../testdata/secrets/intermediate_ca.crt\",\n\t\t\t\t\tIntermediateKey:  \"../testdata/secrets/intermediate_ca_key\",\n\t\t\t\t\tDNSNames:         []string{\"test.smallstep.com\"},\n\t\t\t\t\tPassword:         \"pass\",\n\t\t\t\t\tAuthorityConfig:  ac,\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"invalid address 127.0.0.1\"),\n\t\t\t}\n\t\t},\n\t\t\"empty-root\": func(t *testing.T) ConfigValidateTest {\n\t\t\treturn ConfigValidateTest{\n\t\t\t\tconfig: &Config{\n\t\t\t\t\tAddress:          \"127.0.0.1:443\",\n\t\t\t\t\tIntermediateCert: \"../testdata/secrets/intermediate_ca.crt\",\n\t\t\t\t\tIntermediateKey:  \"../testdata/secrets/intermediate_ca_key\",\n\t\t\t\t\tDNSNames:         []string{\"test.smallstep.com\"},\n\t\t\t\t\tPassword:         \"pass\",\n\t\t\t\t\tAuthorityConfig:  ac,\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"root cannot be empty\"),\n\t\t\t}\n\t\t},\n\t\t\"empty-intermediate-cert\": func(t *testing.T) ConfigValidateTest {\n\t\t\treturn ConfigValidateTest{\n\t\t\t\tconfig: &Config{\n\t\t\t\t\tAddress:         \"127.0.0.1:443\",\n\t\t\t\t\tRoot:            []string{\"../testdata/secrets/root_ca.crt\"},\n\t\t\t\t\tIntermediateKey: \"../testdata/secrets/intermediate_ca_key\",\n\t\t\t\t\tDNSNames:        []string{\"test.smallstep.com\"},\n\t\t\t\t\tPassword:        \"pass\",\n\t\t\t\t\tAuthorityConfig: ac,\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"crt cannot be empty\"),\n\t\t\t}\n\t\t},\n\t\t\"empty-intermediate-key\": func(t *testing.T) ConfigValidateTest {\n\t\t\treturn ConfigValidateTest{\n\t\t\t\tconfig: &Config{\n\t\t\t\t\tAddress:          \"127.0.0.1:443\",\n\t\t\t\t\tRoot:             []string{\"../testdata/secrets/root_ca.crt\"},\n\t\t\t\t\tIntermediateCert: \"../testdata/secrets/intermediate_ca.crt\",\n\t\t\t\t\tDNSNames:         []string{\"test.smallstep.com\"},\n\t\t\t\t\tPassword:         \"pass\",\n\t\t\t\t\tAuthorityConfig:  ac,\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"key cannot be empty\"),\n\t\t\t}\n\t\t},\n\t\t\"empty-dnsNames\": func(t *testing.T) ConfigValidateTest {\n\t\t\treturn ConfigValidateTest{\n\t\t\t\tconfig: &Config{\n\t\t\t\t\tAddress:          \"127.0.0.1:443\",\n\t\t\t\t\tRoot:             []string{\"../testdata/secrets/root_ca.crt\"},\n\t\t\t\t\tIntermediateCert: \"../testdata/secrets/intermediate_ca.crt\",\n\t\t\t\t\tIntermediateKey:  \"../testdata/secrets/intermediate_ca_key\",\n\t\t\t\t\tPassword:         \"pass\",\n\t\t\t\t\tAuthorityConfig:  ac,\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"dnsNames cannot be empty\"),\n\t\t\t}\n\t\t},\n\t\t\"empty-TLS\": func(t *testing.T) ConfigValidateTest {\n\t\t\treturn ConfigValidateTest{\n\t\t\t\tconfig: &Config{\n\t\t\t\t\tAddress:          \"127.0.0.1:443\",\n\t\t\t\t\tRoot:             []string{\"../testdata/secrets/root_ca.crt\"},\n\t\t\t\t\tIntermediateCert: \"../testdata/secrets/intermediate_ca.crt\",\n\t\t\t\t\tIntermediateKey:  \"../testdata/secrets/intermediate_ca_key\",\n\t\t\t\t\tDNSNames:         []string{\"test.smallstep.com\"},\n\t\t\t\t\tPassword:         \"pass\",\n\t\t\t\t\tAuthorityConfig:  ac,\n\t\t\t\t},\n\t\t\t\ttls: &DefaultTLSOptions,\n\t\t\t}\n\t\t},\n\t\t\"empty-TLS-values\": func(t *testing.T) ConfigValidateTest {\n\t\t\treturn ConfigValidateTest{\n\t\t\t\tconfig: &Config{\n\t\t\t\t\tAddress:          \"127.0.0.1:443\",\n\t\t\t\t\tRoot:             []string{\"../testdata/secrets/root_ca.crt\"},\n\t\t\t\t\tIntermediateCert: \"../testdata/secrets/intermediate_ca.crt\",\n\t\t\t\t\tIntermediateKey:  \"../testdata/secrets/intermediate_ca_key\",\n\t\t\t\t\tDNSNames:         []string{\"test.smallstep.com\"},\n\t\t\t\t\tPassword:         \"pass\",\n\t\t\t\t\tAuthorityConfig:  ac,\n\t\t\t\t\tTLS:              &TLSOptions{},\n\t\t\t\t},\n\t\t\t\ttls: &DefaultTLSOptions,\n\t\t\t}\n\t\t},\n\t\t\"custom-tls-values\": func(t *testing.T) ConfigValidateTest {\n\t\t\treturn ConfigValidateTest{\n\t\t\t\tconfig: &Config{\n\t\t\t\t\tAddress:          \"127.0.0.1:443\",\n\t\t\t\t\tRoot:             []string{\"../testdata/secrets/root_ca.crt\"},\n\t\t\t\t\tIntermediateCert: \"../testdata/secrets/intermediate_ca.crt\",\n\t\t\t\t\tIntermediateKey:  \"../testdata/secrets/intermediate_ca_key\",\n\t\t\t\t\tDNSNames:         []string{\"test.smallstep.com\"},\n\t\t\t\t\tPassword:         \"pass\",\n\t\t\t\t\tAuthorityConfig:  ac,\n\t\t\t\t\tTLS: &TLSOptions{\n\t\t\t\t\t\tCipherSuites: CipherSuites{\n\t\t\t\t\t\t\t\"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tMinVersion:    1.0,\n\t\t\t\t\t\tMaxVersion:    1.1,\n\t\t\t\t\t\tRenegotiation: true,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\ttls: &TLSOptions{\n\t\t\t\t\tCipherSuites: CipherSuites{\n\t\t\t\t\t\t\"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305\",\n\t\t\t\t\t},\n\t\t\t\t\tMinVersion:    1.0,\n\t\t\t\t\tMaxVersion:    1.1,\n\t\t\t\t\tRenegotiation: true,\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"tls-min>max\": func(t *testing.T) ConfigValidateTest {\n\t\t\treturn ConfigValidateTest{\n\t\t\t\tconfig: &Config{\n\t\t\t\t\tAddress:          \"127.0.0.1:443\",\n\t\t\t\t\tRoot:             []string{\"../testdata/secrets/root_ca.crt\"},\n\t\t\t\t\tIntermediateCert: \"../testdata/secrets/intermediate_ca.crt\",\n\t\t\t\t\tIntermediateKey:  \"../testdata/secrets/intermediate_ca_key\",\n\t\t\t\t\tDNSNames:         []string{\"test.smallstep.com\"},\n\t\t\t\t\tPassword:         \"pass\",\n\t\t\t\t\tAuthorityConfig:  ac,\n\t\t\t\t\tTLS: &TLSOptions{\n\t\t\t\t\t\tCipherSuites: CipherSuites{\n\t\t\t\t\t\t\t\"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tMinVersion:    1.2,\n\t\t\t\t\t\tMaxVersion:    1.1,\n\t\t\t\t\t\tRenegotiation: true,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"tls minVersion cannot exceed tls maxVersion\"),\n\t\t\t}\n\t\t},\n\t}\n\n\tfor name, get := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ttc := get(t)\n\t\t\terr := tc.config.Validate()\n\t\t\tif err != nil {\n\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\tassert.Equals(t, tc.err.Error(), err.Error())\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif assert.Nil(t, tc.err) {\n\t\t\t\t\tfmt.Printf(\"tc.tls = %v\\n\", tc.tls)\n\t\t\t\t\tfmt.Printf(\"*tc.config.TLS = %v\\n\", tc.config.TLS)\n\t\t\t\t\tassert.Equals(t, tc.config.TLS, tc.tls)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestAuthConfigValidate(t *testing.T) {\n\tasn1dn := ASN1DN{\n\t\tCountry:       \"Tazmania\",\n\t\tOrganization:  \"Acme Co\",\n\t\tLocality:      \"Landscapes\",\n\t\tProvince:      \"Sudden Cliffs\",\n\t\tStreetAddress: \"TNT\",\n\t\tCommonName:    \"test\",\n\t}\n\n\tmaxjwk, err := jose.ReadKey(\"../testdata/secrets/max_pub.jwk\")\n\tassert.FatalError(t, err)\n\tclijwk, err := jose.ReadKey(\"../testdata/secrets/step_cli_key_pub.jwk\")\n\tassert.FatalError(t, err)\n\tp := provisioner.List{\n\t\t&provisioner.JWK{\n\t\t\tName: \"Max\",\n\t\t\tType: \"JWK\",\n\t\t\tKey:  maxjwk,\n\t\t},\n\t\t&provisioner.JWK{\n\t\t\tName: \"step-cli\",\n\t\t\tType: \"JWK\",\n\t\t\tKey:  clijwk,\n\t\t},\n\t}\n\n\ttype AuthConfigValidateTest struct {\n\t\tac     *AuthConfig\n\t\tasn1dn ASN1DN\n\t\terr    error\n\t}\n\ttests := map[string]func(*testing.T) AuthConfigValidateTest{\n\t\t\"fail-nil-authconfig\": func(t *testing.T) AuthConfigValidateTest {\n\t\t\treturn AuthConfigValidateTest{\n\t\t\t\tac:  nil,\n\t\t\t\terr: errors.New(\"authority cannot be undefined\"),\n\t\t\t}\n\t\t},\n\t\t\"ok-empty-provisioners\": func(t *testing.T) AuthConfigValidateTest {\n\t\t\treturn AuthConfigValidateTest{\n\t\t\t\tac:     &AuthConfig{},\n\t\t\t\tasn1dn: ASN1DN{},\n\t\t\t}\n\t\t},\n\t\t\"ok-empty-asn1dn-template\": func(t *testing.T) AuthConfigValidateTest {\n\t\t\treturn AuthConfigValidateTest{\n\t\t\t\tac: &AuthConfig{\n\t\t\t\t\tProvisioners: p,\n\t\t\t\t},\n\t\t\t\tasn1dn: ASN1DN{},\n\t\t\t}\n\t\t},\n\t\t\"ok-custom-asn1dn\": func(t *testing.T) AuthConfigValidateTest {\n\t\t\treturn AuthConfigValidateTest{\n\t\t\t\tac: &AuthConfig{\n\t\t\t\t\tProvisioners: p,\n\t\t\t\t\tTemplate:     &asn1dn,\n\t\t\t\t},\n\t\t\t\tasn1dn: asn1dn,\n\t\t\t}\n\t\t},\n\t}\n\n\tfor name, get := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ttc := get(t)\n\t\t\terr := tc.ac.Validate(provisioner.Audiences{})\n\t\t\tif err != nil {\n\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\tassert.Equals(t, tc.err.Error(), err.Error())\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif assert.Nil(t, tc.err, fmt.Sprintf(\"expected error: %s, but got <nil>\", tc.err)) {\n\t\t\t\t\tassert.Equals(t, *tc.ac.Template, tc.asn1dn)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_toHostname(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\twant string\n\t}{\n\t\t{name: \"localhost\", want: \"localhost\"},\n\t\t{name: \"ca.smallstep.com\", want: \"ca.smallstep.com\"},\n\t\t{name: \"127.0.0.1\", want: \"127.0.0.1\"},\n\t\t{name: \"::1\", want: \"[::1]\"},\n\t\t{name: \"[::1]\", want: \"[::1]\"},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif got := toHostname(tt.name); got != tt.want {\n\t\t\t\tt.Errorf(\"toHostname() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestConfig_Audience(t *testing.T) {\n\ttype fields struct {\n\t\tDNSNames []string\n\t}\n\ttype args struct {\n\t\tpath string\n\t}\n\ttests := []struct {\n\t\tname   string\n\t\tfields fields\n\t\targs   args\n\t\twant   []string\n\t}{\n\t\t{\"ok\", fields{[]string{\n\t\t\t\"ca\", \"ca.example.com\", \"127.0.0.1\", \"::1\",\n\t\t}}, args{\"/path\"}, []string{\n\t\t\t\"https://ca/path\",\n\t\t\t\"https://ca.example.com/path\",\n\t\t\t\"https://127.0.0.1/path\",\n\t\t\t\"https://[::1]/path\",\n\t\t\t\"/path\",\n\t\t}},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tc := &Config{\n\t\t\t\tDNSNames: tt.fields.DNSNames,\n\t\t\t}\n\t\t\tif got := c.Audience(tt.args.path); !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"Config.Audience() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "authority/config/ssh.go",
    "content": "package config\n\nimport (\n\t\"github.com/pkg/errors\"\n\t\"github.com/smallstep/certificates/authority/provisioner\"\n\t\"go.step.sm/crypto/jose\"\n\t\"golang.org/x/crypto/ssh\"\n)\n\n// SSHConfig contains the user and host keys.\ntype SSHConfig struct {\n\tHostKey          string          `json:\"hostKey\"`\n\tUserKey          string          `json:\"userKey\"`\n\tKeys             []*SSHPublicKey `json:\"keys,omitempty\"`\n\tAddUserPrincipal string          `json:\"addUserPrincipal,omitempty\"`\n\tAddUserCommand   string          `json:\"addUserCommand,omitempty\"`\n\tBastion          *Bastion        `json:\"bastion,omitempty\"`\n}\n\n// Bastion contains the custom properties used on bastion.\ntype Bastion struct {\n\tHostname string `json:\"hostname\"`\n\tUser     string `json:\"user,omitempty\"`\n\tPort     string `json:\"port,omitempty\"`\n\tCommand  string `json:\"cmd,omitempty\"`\n\tFlags    string `json:\"flags,omitempty\"`\n}\n\n// HostTag are tagged with k,v pairs. These tags are how a user is ultimately\n// associated with a host.\ntype HostTag struct {\n\tID    string\n\tName  string\n\tValue string\n}\n\n// Host defines expected attributes for an ssh host.\ntype Host struct {\n\tHostID   string    `json:\"hid\"`\n\tHostTags []HostTag `json:\"host_tags\"`\n\tHostname string    `json:\"hostname\"`\n}\n\n// Validate checks the fields in SSHConfig.\nfunc (c *SSHConfig) Validate() error {\n\tif c == nil {\n\t\treturn nil\n\t}\n\tfor _, k := range c.Keys {\n\t\tif err := k.Validate(); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\n// SSHPublicKey contains a public key used by federated CAs to keep old signing\n// keys for this ca.\ntype SSHPublicKey struct {\n\tType      string          `json:\"type\"`\n\tFederated bool            `json:\"federated\"`\n\tKey       jose.JSONWebKey `json:\"key\"`\n\tpublicKey ssh.PublicKey\n}\n\n// Validate checks the fields in SSHPublicKey.\nfunc (k *SSHPublicKey) Validate() error {\n\tswitch {\n\tcase k.Type == \"\":\n\t\treturn errors.New(\"type cannot be empty\")\n\tcase k.Type != provisioner.SSHHostCert && k.Type != provisioner.SSHUserCert:\n\t\treturn errors.Errorf(\"invalid type %s, it must be user or host\", k.Type)\n\tcase !k.Key.IsPublic():\n\t\treturn errors.New(\"invalid key type, it must be a public key\")\n\t}\n\n\tkey, err := ssh.NewPublicKey(k.Key.Key)\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"error creating ssh key\")\n\t}\n\tk.publicKey = key\n\treturn nil\n}\n\n// PublicKey returns the ssh public key.\nfunc (k *SSHPublicKey) PublicKey() ssh.PublicKey {\n\treturn k.publicKey\n}\n\n// SSHKeys represents the SSH User and Host public keys.\ntype SSHKeys struct {\n\tUserKeys []ssh.PublicKey\n\tHostKeys []ssh.PublicKey\n}\n"
  },
  {
    "path": "authority/config/ssh_test.go",
    "content": "package config\n\nimport (\n\t\"reflect\"\n\t\"testing\"\n\n\t\"github.com/smallstep/assert\"\n\t\"go.step.sm/crypto/jose\"\n\t\"golang.org/x/crypto/ssh\"\n)\n\nfunc TestSSHPublicKey_Validate(t *testing.T) {\n\tkey, err := jose.GenerateJWK(\"EC\", \"P-256\", \"\", \"sig\", \"\", 0)\n\tassert.FatalError(t, err)\n\n\ttype fields struct {\n\t\tType      string\n\t\tFederated bool\n\t\tKey       jose.JSONWebKey\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tfields  fields\n\t\twantErr bool\n\t}{\n\t\t{\"user\", fields{\"user\", true, key.Public()}, false},\n\t\t{\"host\", fields{\"host\", false, key.Public()}, false},\n\t\t{\"empty\", fields{\"\", true, key.Public()}, true},\n\t\t{\"badType\", fields{\"bad\", false, key.Public()}, true},\n\t\t{\"badKey\", fields{\"user\", false, *key}, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tk := &SSHPublicKey{\n\t\t\t\tType:      tt.fields.Type,\n\t\t\t\tFederated: tt.fields.Federated,\n\t\t\t\tKey:       tt.fields.Key,\n\t\t\t}\n\t\t\tif err := k.Validate(); (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"SSHPublicKey.Validate() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSSHPublicKey_PublicKey(t *testing.T) {\n\tkey, err := jose.GenerateJWK(\"EC\", \"P-256\", \"\", \"sig\", \"\", 0)\n\tassert.FatalError(t, err)\n\tpub, err := ssh.NewPublicKey(key.Public().Key)\n\tassert.FatalError(t, err)\n\n\ttype fields struct {\n\t\tpublicKey ssh.PublicKey\n\t}\n\ttests := []struct {\n\t\tname   string\n\t\tfields fields\n\t\twant   ssh.PublicKey\n\t}{\n\t\t{\"ok\", fields{pub}, pub},\n\t\t{\"nil\", fields{nil}, nil},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tk := &SSHPublicKey{\n\t\t\t\tpublicKey: tt.fields.publicKey,\n\t\t\t}\n\t\t\tif got := k.PublicKey(); !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"SSHPublicKey.PublicKey() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "authority/config/tls_options.go",
    "content": "package config\n\nimport (\n\t\"crypto/tls\"\n\t\"fmt\"\n\n\t\"github.com/pkg/errors\"\n)\n\nvar (\n\t// DefaultTLSMinVersion default minimum version of TLS.\n\tDefaultTLSMinVersion = TLSVersion(1.2)\n\t// DefaultTLSMaxVersion default maximum version of TLS.\n\tDefaultTLSMaxVersion = TLSVersion(1.3)\n\t// DefaultTLSRenegotiation default TLS connection renegotiation policy.\n\tDefaultTLSRenegotiation = false // Never regnegotiate.\n\t// DefaultTLSCipherSuites specifies default step ciphersuite(s).\n\t// These are TLS 1.0 - 1.2 cipher suites.\n\tDefaultTLSCipherSuites = CipherSuites{\n\t\t\"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256\",\n\t\t\"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256\",\n\t}\n\t// ApprovedTLSCipherSuites smallstep approved ciphersuites.\n\tApprovedTLSCipherSuites = CipherSuites{\n\t\t// AEADs w/ ECDHE\n\t\t\"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256\",\n\t\t\"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256\",\n\t\t\"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384\",\n\t\t\"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384\",\n\t\t\"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256\",\n\t\t\"TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256\",\n\n\t\t// CBC w/ ECDHE\n\t\t\"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA\",\n\t\t\"TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA\",\n\t\t\"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA\",\n\t\t\"TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA\",\n\t}\n\t// DefaultTLSOptions represents the default TLS version as well as the cipher\n\t// suites used in the TLS certificates.\n\tDefaultTLSOptions = TLSOptions{\n\t\tCipherSuites:  DefaultTLSCipherSuites,\n\t\tMinVersion:    DefaultTLSMinVersion,\n\t\tMaxVersion:    DefaultTLSMaxVersion,\n\t\tRenegotiation: DefaultTLSRenegotiation,\n\t}\n)\n\n// TLSVersion represents a TLS version number.\ntype TLSVersion float64\n\n// Validate implements models.Validator and checks that a cipher suite is\n// valid.\nfunc (v TLSVersion) Validate() error {\n\tif _, ok := tlsVersions[v]; ok {\n\t\treturn nil\n\t}\n\treturn errors.Errorf(\"%f is not a valid tls version\", v)\n}\n\n// Value returns the Go constant for the TLSVersion.\nfunc (v TLSVersion) Value() uint16 {\n\treturn tlsVersions[v]\n}\n\n// String returns the Go constant for the TLSVersion.\nfunc (v TLSVersion) String() string {\n\tk := v.Value()\n\tswitch k {\n\tcase tls.VersionTLS10:\n\t\treturn \"1.0\"\n\tcase tls.VersionTLS11:\n\t\treturn \"1.1\"\n\tcase tls.VersionTLS12:\n\t\treturn \"1.2\"\n\tcase tls.VersionTLS13:\n\t\treturn \"1.3\"\n\tdefault:\n\t\treturn fmt.Sprintf(\"unexpected value: %f\", v)\n\t}\n}\n\n// tlsVersions has the list of supported tls version.\nvar tlsVersions = map[TLSVersion]uint16{\n\t// Defaults to TLS 1.3\n\t0: tls.VersionTLS13,\n\t// Options\n\t1.0: tls.VersionTLS10,\n\t1.1: tls.VersionTLS11,\n\t1.2: tls.VersionTLS12,\n\t1.3: tls.VersionTLS13,\n}\n\n// CipherSuites represents an array of string codes representing the cipher\n// suites.\ntype CipherSuites []string\n\n// Validate implements models.Validator and checks that a cipher suite is\n// valid.\nfunc (c CipherSuites) Validate() error {\n\tfor _, s := range c {\n\t\tif _, ok := cipherSuites[s]; !ok {\n\t\t\treturn errors.Errorf(\"%s is not a valid cipher suite\", s)\n\t\t}\n\t}\n\treturn nil\n}\n\n// Value returns an []uint16 for the cipher suites.\nfunc (c CipherSuites) Value() []uint16 {\n\tvalues := make([]uint16, len(c))\n\tfor i, s := range c {\n\t\tvalues[i] = cipherSuites[s]\n\t}\n\treturn values\n}\n\n// cipherSuites has the list of supported cipher suites.\nvar cipherSuites = map[string]uint16{\n\t// TLS 1.0 - 1.2 cipher suites.\n\t\"TLS_RSA_WITH_RC4_128_SHA\":                      tls.TLS_RSA_WITH_RC4_128_SHA, // lgtm[go/insecure-tls]\n\t\"TLS_RSA_WITH_3DES_EDE_CBC_SHA\":                 tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA,\n\t\"TLS_RSA_WITH_AES_128_CBC_SHA\":                  tls.TLS_RSA_WITH_AES_128_CBC_SHA,\n\t\"TLS_RSA_WITH_AES_256_CBC_SHA\":                  tls.TLS_RSA_WITH_AES_256_CBC_SHA,\n\t\"TLS_RSA_WITH_AES_128_CBC_SHA256\":               tls.TLS_RSA_WITH_AES_128_CBC_SHA256, // lgtm[go/insecure-tls]\n\t\"TLS_RSA_WITH_AES_128_GCM_SHA256\":               tls.TLS_RSA_WITH_AES_128_GCM_SHA256,\n\t\"TLS_RSA_WITH_AES_256_GCM_SHA384\":               tls.TLS_RSA_WITH_AES_256_GCM_SHA384,\n\t\"TLS_ECDHE_ECDSA_WITH_RC4_128_SHA\":              tls.TLS_ECDHE_ECDSA_WITH_RC4_128_SHA, // lgtm[go/insecure-tls]\n\t\"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA\":          tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,\n\t\"TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA\":          tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,\n\t\"TLS_ECDHE_RSA_WITH_RC4_128_SHA\":                tls.TLS_ECDHE_RSA_WITH_RC4_128_SHA, // lgtm[go/insecure-tls]\n\t\"TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA\":           tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA,\n\t\"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA\":            tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,\n\t\"TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA\":            tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,\n\t\"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256\":       tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256, // lgtm[go/insecure-tls]\n\t\"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256\":         tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256,   // lgtm[go/insecure-tls]\n\t\"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256\":         tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,\n\t\"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256\":       tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,\n\t\"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384\":         tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,\n\t\"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384\":       tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,\n\t\"TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256\":   tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,\n\t\"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256\": tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,\n\n\t// TLS 1.3 cipher sutes.\n\t\"TLS_AES_128_GCM_SHA256\":       tls.TLS_AES_128_GCM_SHA256,\n\t\"TLS_AES_256_GCM_SHA384\":       tls.TLS_AES_256_GCM_SHA384,\n\t\"TLS_CHACHA20_POLY1305_SHA256\": tls.TLS_CHACHA20_POLY1305_SHA256,\n\n\t// Legacy names.\n\t\"TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305\":   tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,\n\t\"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305\": tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,\n}\n\n// TLSOptions represents the TLS options that can be specified on *tls.Config\n// types to configure HTTPS servers and clients.\ntype TLSOptions struct {\n\tCipherSuites  CipherSuites `json:\"cipherSuites\"`\n\tMinVersion    TLSVersion   `json:\"minVersion\"`\n\tMaxVersion    TLSVersion   `json:\"maxVersion\"`\n\tRenegotiation bool         `json:\"renegotiation\"`\n}\n\n// TLSConfig returns the tls.Config equivalent of the TLSOptions.\nfunc (t *TLSOptions) TLSConfig() *tls.Config {\n\tvar rs tls.RenegotiationSupport\n\tif t.Renegotiation {\n\t\trs = tls.RenegotiateFreelyAsClient\n\t} else {\n\t\trs = tls.RenegotiateNever\n\t}\n\n\t//nolint:gosec // default MinVersion 1.2, if defined but empty 1.3 is used\n\treturn &tls.Config{\n\t\tCipherSuites:  t.CipherSuites.Value(),\n\t\tMinVersion:    t.MinVersion.Value(),\n\t\tMaxVersion:    t.MaxVersion.Value(),\n\t\tRenegotiation: rs,\n\t}\n}\n"
  },
  {
    "path": "authority/config/tls_options_test.go",
    "content": "package config\n\nimport (\n\t\"crypto/tls\"\n\t\"reflect\"\n\t\"testing\"\n)\n\nfunc TestTLSVersion_Validate(t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\tv       TLSVersion\n\t\twantErr bool\n\t}{\n\t\t{\"default\", TLSVersion(0), false},\n\t\t{\"1.0\", TLSVersion(1.0), false},\n\t\t{\"1.1\", TLSVersion(1.1), false},\n\t\t{\"1.2\", TLSVersion(1.2), false},\n\t\t{\"1.3\", TLSVersion(1.3), false},\n\t\t{\"0.99\", TLSVersion(0.99), true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif err := tt.v.Validate(); (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"TLSVersion.Validate() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestTLSVersion_String(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tv    TLSVersion\n\t\twant string\n\t}{\n\t\t{\"default\", TLSVersion(0), \"1.3\"},\n\t\t{\"1.0\", TLSVersion(1.0), \"1.0\"},\n\t\t{\"1.1\", TLSVersion(1.1), \"1.1\"},\n\t\t{\"1.2\", TLSVersion(1.2), \"1.2\"},\n\t\t{\"1.3\", TLSVersion(1.3), \"1.3\"},\n\t\t{\"0.99\", TLSVersion(0.99), \"unexpected value: 0.990000\"},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif got := tt.v.String(); got != tt.want {\n\t\t\t\tt.Errorf(\"TLSVersion.String() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestCipherSuites_Validate(t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\tc       CipherSuites\n\t\twantErr bool\n\t}{\n\t\t{\"TLS_RSA_WITH_RC4_128_SHA\", CipherSuites{\"TLS_RSA_WITH_RC4_128_SHA\"}, false},\n\t\t{\"TLS_RSA_WITH_3DES_EDE_CBC_SHA\", CipherSuites{\"TLS_RSA_WITH_3DES_EDE_CBC_SHA\"}, false},\n\t\t{\"TLS_RSA_WITH_AES_128_CBC_SHA\", CipherSuites{\"TLS_RSA_WITH_AES_128_CBC_SHA\"}, false},\n\t\t{\"TLS_RSA_WITH_AES_256_CBC_SHA\", CipherSuites{\"TLS_RSA_WITH_AES_256_CBC_SHA\"}, false},\n\t\t{\"TLS_RSA_WITH_AES_128_CBC_SHA256\", CipherSuites{\"TLS_RSA_WITH_AES_128_CBC_SHA256\"}, false},\n\t\t{\"TLS_RSA_WITH_AES_128_GCM_SHA256\", CipherSuites{\"TLS_RSA_WITH_AES_128_GCM_SHA256\"}, false},\n\t\t{\"TLS_RSA_WITH_AES_256_GCM_SHA384\", CipherSuites{\"TLS_RSA_WITH_AES_256_GCM_SHA384\"}, false},\n\t\t{\"TLS_ECDHE_ECDSA_WITH_RC4_128_SHA\", CipherSuites{\"TLS_ECDHE_ECDSA_WITH_RC4_128_SHA\"}, false},\n\t\t{\"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA\", CipherSuites{\"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA\"}, false},\n\t\t{\"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256\", CipherSuites{\"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256\"}, false},\n\t\t{\"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256\", CipherSuites{\"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256\"}, false},\n\t\t{\"TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA\", CipherSuites{\"TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA\"}, false},\n\t\t{\"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384\", CipherSuites{\"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384\"}, false},\n\t\t{\"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305\", CipherSuites{\"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305\"}, false},\n\t\t{\"TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA\", CipherSuites{\"TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA\"}, false},\n\t\t{\"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA\", CipherSuites{\"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA\"}, false},\n\t\t{\"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256\", CipherSuites{\"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256\"}, false},\n\t\t{\"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256\", CipherSuites{\"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256\"}, false},\n\t\t{\"TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA\", CipherSuites{\"TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA\"}, false},\n\t\t{\"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384\", CipherSuites{\"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384\"}, false},\n\t\t{\"TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305\", CipherSuites{\"TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305\"}, false},\n\t\t{\"multiple\", CipherSuites{\"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305\", \"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256\"}, false},\n\t\t{\"fail\", CipherSuites{\"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305\", \"TLS_BAD_CIPHERSUITE\"}, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif err := tt.c.Validate(); (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"CipherSuites.Validate() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestCipherSuites_Value(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tc    CipherSuites\n\t\twant []uint16\n\t}{\n\t\t{\"TLS_RSA_WITH_RC4_128_SHA\", CipherSuites{\"TLS_RSA_WITH_RC4_128_SHA\"}, []uint16{tls.TLS_RSA_WITH_RC4_128_SHA}},\n\t\t{\"TLS_RSA_WITH_3DES_EDE_CBC_SHA\", CipherSuites{\"TLS_RSA_WITH_3DES_EDE_CBC_SHA\"}, []uint16{tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA}},\n\t\t{\"TLS_RSA_WITH_AES_128_CBC_SHA\", CipherSuites{\"TLS_RSA_WITH_AES_128_CBC_SHA\"}, []uint16{tls.TLS_RSA_WITH_AES_128_CBC_SHA}},\n\t\t{\"TLS_RSA_WITH_AES_256_CBC_SHA\", CipherSuites{\"TLS_RSA_WITH_AES_256_CBC_SHA\"}, []uint16{tls.TLS_RSA_WITH_AES_256_CBC_SHA}},\n\t\t{\"TLS_RSA_WITH_AES_128_CBC_SHA256\", CipherSuites{\"TLS_RSA_WITH_AES_128_CBC_SHA256\"}, []uint16{tls.TLS_RSA_WITH_AES_128_CBC_SHA256}},\n\t\t{\"TLS_RSA_WITH_AES_128_GCM_SHA256\", CipherSuites{\"TLS_RSA_WITH_AES_128_GCM_SHA256\"}, []uint16{tls.TLS_RSA_WITH_AES_128_GCM_SHA256}},\n\t\t{\"TLS_RSA_WITH_AES_256_GCM_SHA384\", CipherSuites{\"TLS_RSA_WITH_AES_256_GCM_SHA384\"}, []uint16{tls.TLS_RSA_WITH_AES_256_GCM_SHA384}},\n\t\t{\"TLS_ECDHE_ECDSA_WITH_RC4_128_SHA\", CipherSuites{\"TLS_ECDHE_ECDSA_WITH_RC4_128_SHA\"}, []uint16{tls.TLS_ECDHE_ECDSA_WITH_RC4_128_SHA}},\n\t\t{\"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA\", CipherSuites{\"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA\"}, []uint16{tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA}},\n\t\t{\"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256\", CipherSuites{\"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256\"}, []uint16{tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256}},\n\t\t{\"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256\", CipherSuites{\"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256\"}, []uint16{tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256}},\n\t\t{\"TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA\", CipherSuites{\"TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA\"}, []uint16{tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA}},\n\t\t{\"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384\", CipherSuites{\"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384\"}, []uint16{tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384}},\n\t\t{\"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305\", CipherSuites{\"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305\"}, []uint16{tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305}},\n\t\t{\"TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA\", CipherSuites{\"TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA\"}, []uint16{tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA}},\n\t\t{\"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA\", CipherSuites{\"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA\"}, []uint16{tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA}},\n\t\t{\"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256\", CipherSuites{\"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256\"}, []uint16{tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256}},\n\t\t{\"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256\", CipherSuites{\"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256\"}, []uint16{tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256}},\n\t\t{\"TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA\", CipherSuites{\"TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA\"}, []uint16{tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA}},\n\t\t{\"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384\", CipherSuites{\"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384\"}, []uint16{tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384}},\n\t\t{\"TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305\", CipherSuites{\"TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305\"}, []uint16{tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305}},\n\t\t{\"multiple\", CipherSuites{\"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305\", \"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256\"}, []uint16{tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256}},\n\t\t{\"fail\", CipherSuites{\"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305\", \"TLS_BAD_CIPHERSUITE\"}, []uint16{tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, 0}},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif got := tt.c.Value(); !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"CipherSuites.Value() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestTLSOptions_TLSConfig(t *testing.T) {\n\ttype fields struct {\n\t\tCipherSuites  CipherSuites\n\t\tMinVersion    TLSVersion\n\t\tMaxVersion    TLSVersion\n\t\tRenegotiation bool\n\t}\n\ttests := []struct {\n\t\tname   string\n\t\tfields fields\n\t\twant   *tls.Config\n\t}{\n\t\t{\"default\", fields{DefaultTLSCipherSuites, DefaultTLSMinVersion, DefaultTLSMaxVersion, DefaultTLSRenegotiation}, &tls.Config{\n\t\t\tCipherSuites:  []uint16{tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256},\n\t\t\tMinVersion:    tls.VersionTLS12,\n\t\t\tMaxVersion:    tls.VersionTLS13,\n\t\t\tRenegotiation: tls.RenegotiateNever,\n\t\t}},\n\t\t{\"renegotation\", fields{DefaultTLSCipherSuites, DefaultTLSMinVersion, DefaultTLSMaxVersion, true}, &tls.Config{\n\t\t\tCipherSuites:  []uint16{tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256},\n\t\t\tMinVersion:    tls.VersionTLS12,\n\t\t\tMaxVersion:    tls.VersionTLS13,\n\t\t\tRenegotiation: tls.RenegotiateFreelyAsClient,\n\t\t}},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\to := &TLSOptions{\n\t\t\t\tCipherSuites:  tt.fields.CipherSuites,\n\t\t\t\tMinVersion:    tt.fields.MinVersion,\n\t\t\t\tMaxVersion:    tt.fields.MaxVersion,\n\t\t\t\tRenegotiation: tt.fields.Renegotiation,\n\t\t\t}\n\t\t\tif got := o.TLSConfig(); !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"TLSOptions.TLSConfig() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "authority/config/types.go",
    "content": "package config\n\nimport (\n\t\"encoding/json\"\n\n\t\"github.com/pkg/errors\"\n)\n\n// multiString represents a type that can be encoded/decoded in JSON as a single\n// string or an array of strings.\ntype multiString []string\n\n// First returns the first element of a multiString. It will return an empty\n// string if the multistring is empty.\nfunc (s multiString) First() string {\n\tif len(s) > 0 {\n\t\treturn s[0]\n\t}\n\treturn \"\"\n}\n\n// HasEmpties returns `true` if any string in the array is empty.\nfunc (s multiString) HasEmpties() bool {\n\tif len(s) == 0 {\n\t\treturn true\n\t}\n\tfor _, ss := range s {\n\t\tif ss == \"\" {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// MarshalJSON marshals the multistring as a string or a slice of strings . With\n// 0 elements it will return the empty string, with 1 element a regular string,\n// otherwise a slice of strings.\nfunc (s multiString) MarshalJSON() ([]byte, error) {\n\tswitch len(s) {\n\tcase 0:\n\t\treturn []byte(`\"\"`), nil\n\tcase 1:\n\t\treturn json.Marshal(s[0])\n\tdefault:\n\t\treturn json.Marshal([]string(s))\n\t}\n}\n\n// UnmarshalJSON parses a string or a slice and sets it to the multiString.\nfunc (s *multiString) UnmarshalJSON(data []byte) error {\n\tif s == nil {\n\t\treturn errors.New(\"multiString cannot be nil\")\n\t}\n\tif len(data) == 0 {\n\t\t*s = nil\n\t\treturn nil\n\t}\n\t// Parse string\n\tif data[0] == '\"' {\n\t\tvar str string\n\t\tif err := json.Unmarshal(data, &str); err != nil {\n\t\t\treturn errors.Wrapf(err, \"error unmarshalling %s\", data)\n\t\t}\n\t\t*s = []string{str}\n\t\treturn nil\n\t}\n\t// Parse array\n\tvar ss []string\n\tif err := json.Unmarshal(data, &ss); err != nil {\n\t\treturn errors.Wrapf(err, \"error unmarshalling %s\", data)\n\t}\n\t*s = ss\n\treturn nil\n}\n"
  },
  {
    "path": "authority/config/types_test.go",
    "content": "package config\n\nimport (\n\t\"reflect\"\n\t\"testing\"\n)\n\nfunc Test_multiString_First(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\ts    multiString\n\t\twant string\n\t}{\n\t\t{\"empty\", multiString{}, \"\"},\n\t\t{\"string\", multiString{\"one\"}, \"one\"},\n\t\t{\"slice\", multiString{\"one\", \"two\"}, \"one\"},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif got := tt.s.First(); got != tt.want {\n\t\t\t\tt.Errorf(\"multiString.First() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_multiString_Empties(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\ts    multiString\n\t\twant bool\n\t}{\n\t\t{\"empty\", multiString{}, true},\n\t\t{\"string\", multiString{\"one\"}, false},\n\t\t{\"empty string\", multiString{\"\"}, true},\n\t\t{\"slice\", multiString{\"one\", \"two\"}, false},\n\t\t{\"empty slice\", multiString{\"one\", \"\"}, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif got := tt.s.HasEmpties(); got != tt.want {\n\t\t\t\tt.Errorf(\"multiString.Empties() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_multiString_MarshalJSON(t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\ts       multiString\n\t\twant    []byte\n\t\twantErr bool\n\t}{\n\t\t{\"empty\", []string{}, []byte(`\"\"`), false},\n\t\t{\"string\", []string{\"a string\"}, []byte(`\"a string\"`), false},\n\t\t{\"slice\", []string{\"string one\", \"string two\"}, []byte(`[\"string one\",\"string two\"]`), false},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := tt.s.MarshalJSON()\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"multiString.MarshalJSON() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"multiString.MarshalJSON() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_multiString_UnmarshalJSON(t *testing.T) {\n\ttype args struct {\n\t\tdata []byte\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\ts       *multiString\n\t\targs    args\n\t\twant    *multiString\n\t\twantErr bool\n\t}{\n\t\t{\"empty\", new(multiString), args{[]byte{}}, new(multiString), false},\n\t\t{\"empty string\", new(multiString), args{[]byte(`\"\"`)}, &multiString{\"\"}, false},\n\t\t{\"string\", new(multiString), args{[]byte(`\"a string\"`)}, &multiString{\"a string\"}, false},\n\t\t{\"slice\", new(multiString), args{[]byte(`[\"string one\",\"string two\"]`)}, &multiString{\"string one\", \"string two\"}, false},\n\t\t{\"error\", new(multiString), args{[]byte(`[\"123\",123]`)}, new(multiString), true},\n\t\t{\"nil\", nil, args{nil}, nil, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif err := tt.s.UnmarshalJSON(tt.args.data); (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"multiString.UnmarshalJSON() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(tt.s, tt.want) {\n\t\t\t\tt.Errorf(\"multiString.UnmarshalJSON() = %v, want %v\", tt.s, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "authority/config.go",
    "content": "package authority\n\nimport \"github.com/smallstep/certificates/authority/config\"\n\n// Config is an alias to support older APIs.\ntype Config = config.Config\n\n// LoadConfiguration is an alias to support older APIs.\nvar LoadConfiguration = config.LoadConfiguration\n\n// AuthConfig is an alias to support older APIs.\ntype AuthConfig = config.AuthConfig\n\n// TLS\n\n// ASN1DN is an alias to support older APIs.\ntype ASN1DN = config.ASN1DN\n\n// DefaultTLSOptions is an alias to support older APIs.\nvar DefaultTLSOptions = config.DefaultTLSOptions\n\n// TLSOptions is an alias to support older APIs.\ntype TLSOptions = config.TLSOptions\n\n// CipherSuites is an alias to support older APIs.\ntype CipherSuites = config.CipherSuites\n\n// SSH\n\n// SSHConfig is an alias to support older APIs.\ntype SSHConfig = config.SSHConfig\n\n// Bastion is an alias to support older APIs.\ntype Bastion = config.Bastion\n\n// HostTag is an alias to support older APIs.\ntype HostTag = config.HostTag\n\n// Host is an alias to support older APIs.\ntype Host = config.Host\n\n// SSHPublicKey is an alias to support older APIs.\ntype SSHPublicKey = config.SSHPublicKey\n\n// SSHKeys is an alias to support older APIs.\ntype SSHKeys = config.SSHKeys\n"
  },
  {
    "path": "authority/export.go",
    "content": "package authority\n\nimport (\n\t\"encoding/json\"\n\t\"net/url\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/pkg/errors\"\n\t\"google.golang.org/protobuf/types/known/structpb\"\n\n\t\"github.com/smallstep/cli-utils/step\"\n\t\"github.com/smallstep/linkedca\"\n\n\t\"github.com/smallstep/certificates/authority/provisioner\"\n)\n\n// Export creates a linkedca configuration form the current ca.json and loaded\n// authorities.\n//\n// Note that export will not export neither the pki password nor the certificate\n// issuer password.\nfunc (a *Authority) Export() (c *linkedca.Configuration, err error) {\n\t// Recover from panics\n\tdefer func() {\n\t\tif r := recover(); r != nil {\n\t\t\terr = r.(error)\n\t\t}\n\t}()\n\n\tfiles := make(map[string][]byte)\n\n\t// The exported configuration should not include the password in it.\n\tc = &linkedca.Configuration{\n\t\tVersion:         \"1.0\",\n\t\tRoot:            mustReadFilesOrURIs(a.config.Root, files),\n\t\tFederatedRoots:  mustReadFilesOrURIs(a.config.FederatedRoots, files),\n\t\tIntermediate:    mustReadFileOrURI(a.config.IntermediateCert, files),\n\t\tIntermediateKey: mustReadFileOrURI(a.config.IntermediateKey, files),\n\t\tAddress:         a.config.Address,\n\t\tInsecureAddress: a.config.InsecureAddress,\n\t\tDnsNames:        a.config.DNSNames,\n\t\tDb:              mustMarshalToStruct(a.config.DB),\n\t\tLogger:          mustMarshalToStruct(a.config.Logger),\n\t\tMonitoring:      mustMarshalToStruct(a.config.Monitoring),\n\t\tAuthority: &linkedca.Authority{\n\t\t\tId:                   a.config.AuthorityConfig.AuthorityID,\n\t\t\tEnableAdmin:          a.config.AuthorityConfig.EnableAdmin,\n\t\t\tDisableIssuedAtCheck: a.config.AuthorityConfig.DisableIssuedAtCheck,\n\t\t\tBackdate:             mustDuration(a.config.AuthorityConfig.Backdate),\n\t\t\tDeploymentType:       a.config.AuthorityConfig.DeploymentType,\n\t\t},\n\t\tFiles: files,\n\t}\n\n\t// SSH\n\tif v := a.config.SSH; v != nil {\n\t\tc.Ssh = &linkedca.SSH{\n\t\t\tHostKey:          mustReadFileOrURI(v.HostKey, files),\n\t\t\tUserKey:          mustReadFileOrURI(v.UserKey, files),\n\t\t\tAddUserPrincipal: v.AddUserPrincipal,\n\t\t\tAddUserCommand:   v.AddUserCommand,\n\t\t}\n\t\tfor _, k := range v.Keys {\n\t\t\ttyp, ok := linkedca.SSHPublicKey_Type_value[strings.ToUpper(k.Type)]\n\t\t\tif !ok {\n\t\t\t\treturn nil, errors.Errorf(\"unsupported ssh key type %s\", k.Type)\n\t\t\t}\n\t\t\tc.Ssh.Keys = append(c.Ssh.Keys, &linkedca.SSHPublicKey{\n\t\t\t\tType:      linkedca.SSHPublicKey_Type(typ),\n\t\t\t\tFederated: k.Federated,\n\t\t\t\tKey:       mustMarshalToStruct(k),\n\t\t\t})\n\t\t}\n\t\tif b := v.Bastion; b != nil {\n\t\t\tc.Ssh.Bastion = &linkedca.Bastion{\n\t\t\t\tHostname: b.Hostname,\n\t\t\t\tUser:     b.User,\n\t\t\t\tPort:     b.Port,\n\t\t\t\tCommand:  b.Command,\n\t\t\t\tFlags:    b.Flags,\n\t\t\t}\n\t\t}\n\t}\n\n\t// KMS\n\tif v := a.config.KMS; v != nil {\n\t\tvar typ int32\n\t\tvar ok bool\n\t\tif v.Type == \"\" {\n\t\t\ttyp = int32(linkedca.KMS_SOFTKMS)\n\t\t} else {\n\t\t\ttyp, ok = linkedca.KMS_Type_value[strings.ToUpper(string(v.Type))]\n\t\t\tif !ok {\n\t\t\t\treturn nil, errors.Errorf(\"unsupported kms type %s\", v.Type)\n\t\t\t}\n\t\t}\n\t\tc.Kms = &linkedca.KMS{\n\t\t\tType:            linkedca.KMS_Type(typ),\n\t\t\tCredentialsFile: v.CredentialsFile,\n\t\t\tUri:             v.URI,\n\t\t\tPin:             v.Pin,\n\t\t\tManagementKey:   v.ManagementKey,\n\t\t\tRegion:          v.Region,\n\t\t\tProfile:         v.Profile,\n\t\t}\n\t}\n\n\t// Authority\n\t// cas options\n\tif v := a.config.AuthorityConfig.Options; v != nil {\n\t\tc.Authority.Type = 0\n\t\tc.Authority.CertificateAuthority = v.CertificateAuthority\n\t\tc.Authority.CertificateAuthorityFingerprint = v.CertificateAuthorityFingerprint\n\t\tc.Authority.CredentialsFile = v.CredentialsFile\n\t\tif iss := v.CertificateIssuer; iss != nil {\n\t\t\ttyp, ok := linkedca.CertificateIssuer_Type_value[strings.ToUpper(iss.Type)]\n\t\t\tif !ok {\n\t\t\t\treturn nil, errors.Errorf(\"unknown certificate issuer type %s\", iss.Type)\n\t\t\t}\n\t\t\t// The exported certificate issuer should not include the password.\n\t\t\tc.Authority.CertificateIssuer = &linkedca.CertificateIssuer{\n\t\t\t\tType:        linkedca.CertificateIssuer_Type(typ),\n\t\t\t\tProvisioner: iss.Provisioner,\n\t\t\t\tCertificate: mustReadFileOrURI(iss.Certificate, files),\n\t\t\t\tKey:         mustReadFileOrURI(iss.Key, files),\n\t\t\t}\n\t\t}\n\t}\n\t// admins\n\tfor {\n\t\tlist, cursor := a.admins.Find(\"\", 100)\n\t\tc.Authority.Admins = append(c.Authority.Admins, list...)\n\t\tif cursor == \"\" {\n\t\t\tbreak\n\t\t}\n\t}\n\t// provisioners\n\tfor {\n\t\tlist, cursor := a.provisioners.Find(\"\", 100)\n\t\tfor _, p := range list {\n\t\t\tlp, err := ProvisionerToLinkedca(p)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tc.Authority.Provisioners = append(c.Authority.Provisioners, lp)\n\t\t}\n\t\tif cursor == \"\" {\n\t\t\tbreak\n\t\t}\n\t}\n\t// global claims\n\tc.Authority.Claims = claimsToLinkedca(a.config.AuthorityConfig.Claims)\n\t// Distinguished names template\n\tif v := a.config.AuthorityConfig.Template; v != nil {\n\t\tc.Authority.Template = &linkedca.DistinguishedName{\n\t\t\tCountry:            v.Country,\n\t\t\tOrganization:       v.Organization,\n\t\t\tOrganizationalUnit: v.OrganizationalUnit,\n\t\t\tLocality:           v.Locality,\n\t\t\tProvince:           v.Province,\n\t\t\tStreetAddress:      v.StreetAddress,\n\t\t\tSerialNumber:       v.SerialNumber,\n\t\t\tCommonName:         v.CommonName,\n\t\t}\n\t}\n\n\t// TLS\n\tif v := a.config.TLS; v != nil {\n\t\tc.Tls = &linkedca.TLS{\n\t\t\tMinVersion:    v.MinVersion.String(),\n\t\t\tMaxVersion:    v.MaxVersion.String(),\n\t\t\tRenegotiation: v.Renegotiation,\n\t\t}\n\t\tfor _, cs := range v.CipherSuites.Value() {\n\t\t\tc.Tls.CipherSuites = append(c.Tls.CipherSuites, linkedca.TLS_CiperSuite(cs))\n\t\t}\n\t}\n\n\t// Templates\n\tif v := a.config.Templates; v != nil {\n\t\tc.Templates = &linkedca.ConfigTemplates{\n\t\t\tSsh:  &linkedca.SSHConfigTemplate{},\n\t\t\tData: mustMarshalToStruct(v.Data),\n\t\t}\n\t\t// Remove automatically loaded vars\n\t\tif c.Templates.Data != nil && c.Templates.Data.Fields != nil {\n\t\t\tdelete(c.Templates.Data.Fields, \"Step\")\n\t\t}\n\t\tfor _, t := range v.SSH.Host {\n\t\t\ttyp, ok := linkedca.ConfigTemplate_Type_value[strings.ToUpper(string(t.Type))]\n\t\t\tif !ok {\n\t\t\t\treturn nil, errors.Errorf(\"unsupported template type %s\", t.Type)\n\t\t\t}\n\t\t\tc.Templates.Ssh.Hosts = append(c.Templates.Ssh.Hosts, &linkedca.ConfigTemplate{\n\t\t\t\tType:     linkedca.ConfigTemplate_Type(typ),\n\t\t\t\tName:     t.Name,\n\t\t\t\tTemplate: mustReadFileOrURI(t.TemplatePath, files),\n\t\t\t\tPath:     t.Path,\n\t\t\t\tComment:  t.Comment,\n\t\t\t\tRequires: t.RequiredData,\n\t\t\t\tContent:  t.Content,\n\t\t\t})\n\t\t}\n\t\tfor _, t := range v.SSH.User {\n\t\t\ttyp, ok := linkedca.ConfigTemplate_Type_value[strings.ToUpper(string(t.Type))]\n\t\t\tif !ok {\n\t\t\t\treturn nil, errors.Errorf(\"unsupported template type %s\", t.Type)\n\t\t\t}\n\t\t\tc.Templates.Ssh.Users = append(c.Templates.Ssh.Users, &linkedca.ConfigTemplate{\n\t\t\t\tType:     linkedca.ConfigTemplate_Type(typ),\n\t\t\t\tName:     t.Name,\n\t\t\t\tTemplate: mustReadFileOrURI(t.TemplatePath, files),\n\t\t\t\tPath:     t.Path,\n\t\t\t\tComment:  t.Comment,\n\t\t\t\tRequires: t.RequiredData,\n\t\t\t\tContent:  t.Content,\n\t\t\t})\n\t\t}\n\t}\n\n\treturn c, nil\n}\n\nfunc mustDuration(d *provisioner.Duration) string {\n\tif d == nil || d.Duration == 0 {\n\t\treturn \"\"\n\t}\n\treturn d.String()\n}\n\nfunc mustMarshalToStruct(v interface{}) *structpb.Struct {\n\tb, err := json.Marshal(v)\n\tif err != nil {\n\t\tpanic(errors.Wrapf(err, \"error marshaling %T\", v))\n\t}\n\tvar r *structpb.Struct\n\tif err := json.Unmarshal(b, &r); err != nil {\n\t\tpanic(errors.Wrapf(err, \"error unmarshaling %T\", v))\n\t}\n\treturn r\n}\n\nfunc mustReadFileOrURI(fn string, m map[string][]byte) string {\n\tif fn == \"\" {\n\t\treturn \"\"\n\t}\n\n\tstepPath := filepath.ToSlash(step.Path())\n\tif !strings.HasSuffix(stepPath, \"/\") {\n\t\tstepPath += \"/\"\n\t}\n\n\tfn = strings.TrimPrefix(filepath.ToSlash(fn), stepPath)\n\n\tok, err := isFilename(fn)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tif ok {\n\t\tb, err := os.ReadFile(step.Abs(fn))\n\t\tif err != nil {\n\t\t\tpanic(errors.Wrapf(err, \"error reading %s\", fn))\n\t\t}\n\t\tm[fn] = b\n\t\treturn fn\n\t}\n\treturn fn\n}\n\nfunc mustReadFilesOrURIs(fns []string, m map[string][]byte) []string {\n\tvar result []string\n\tfor _, fn := range fns {\n\t\tresult = append(result, mustReadFileOrURI(fn, m))\n\t}\n\treturn result\n}\n\nfunc isFilename(fn string) (bool, error) {\n\tu, err := url.Parse(fn)\n\tif err != nil {\n\t\treturn false, errors.Wrapf(err, \"error parsing %s\", fn)\n\t}\n\treturn u.Scheme == \"\" || u.Scheme == \"file\", nil\n}\n"
  },
  {
    "path": "authority/http_client.go",
    "content": "package authority\n\nimport (\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"net/http\"\n\t\"sync/atomic\"\n\n\t\"github.com/smallstep/certificates/authority/poolhttp\"\n\t\"github.com/smallstep/certificates/authority/provisioner\"\n\t\"github.com/smallstep/certificates/internal/httptransport\"\n)\n\n// systemCertPool holds a copy of the system cert pool. This cert pool must be\n// initialized when the authority is created and we should always get a clone of\n// this pool.\nvar systemCertPool atomic.Pointer[x509.CertPool]\n\n// initializeSystemCertPool initializes the system cert pool if necessary.\nfunc initializeSystemCertPool() error {\n\tif systemCertPool.Load() == nil {\n\t\tpool, err := x509.SystemCertPool()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tsystemCertPool.Store(pool)\n\t}\n\treturn nil\n}\n\n// newHTTPClient will return an HTTP client that trusts the system cert pool and\n// the given roots.\nfunc newHTTPClient(wt httptransport.Wrapper, roots ...*x509.Certificate) provisioner.HTTPClient {\n\treturn poolhttp.New(func() *http.Client {\n\t\tpool := systemCertPool.Load().Clone()\n\t\tfor _, crt := range roots {\n\t\t\tpool.AddCert(crt)\n\t\t}\n\n\t\ttr, ok := http.DefaultTransport.(*http.Transport)\n\t\tif !ok {\n\t\t\ttr = httptransport.New()\n\t\t} else {\n\t\t\ttr = tr.Clone()\n\t\t}\n\n\t\ttr.TLSClientConfig = &tls.Config{\n\t\t\tMinVersion: tls.VersionTLS12,\n\t\t\tRootCAs:    pool,\n\t\t}\n\n\t\trr := wt(tr)\n\n\t\treturn &http.Client{Transport: rr}\n\t})\n}\n"
  },
  {
    "path": "authority/http_client_test.go",
    "content": "package authority\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/smallstep/certificates/authority/provisioner\"\n\t\"github.com/smallstep/certificates/internal/httptransport\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"go.step.sm/crypto/jose\"\n\t\"go.step.sm/crypto/keyutil\"\n\t\"go.step.sm/crypto/x509util\"\n)\n\nfunc mustCertificate(t *testing.T, a *Authority, csr *x509.CertificateRequest) []*x509.Certificate {\n\tt.Helper()\n\n\tctx := provisioner.NewContextWithMethod(context.Background(), provisioner.SignMethod)\n\n\tnow := time.Now()\n\tsignOpts := provisioner.SignOptions{\n\t\tNotBefore: provisioner.NewTimeDuration(now),\n\t\tNotAfter:  provisioner.NewTimeDuration(now.Add(5 * time.Minute)),\n\t\tBackdate:  1 * time.Minute,\n\t}\n\n\tsans := []string{}\n\tsans = append(sans, csr.DNSNames...)\n\tsans = append(sans, csr.EmailAddresses...)\n\tfor _, s := range csr.IPAddresses {\n\t\tsans = append(sans, s.String())\n\t}\n\tfor _, s := range csr.URIs {\n\t\tsans = append(sans, s.String())\n\t}\n\n\tkey, err := jose.ReadKey(\"testdata/secrets/step_cli_key_priv.jwk\", jose.WithPassword([]byte(\"pass\")))\n\trequire.NoError(t, err)\n\n\ttoken, err := generateToken(csr.Subject.CommonName, \"step-cli\", testAudiences.Sign[0], sans, now, key)\n\trequire.NoError(t, err)\n\n\textraOpts, err := a.Authorize(ctx, token)\n\trequire.NoError(t, err)\n\n\tchain, err := a.SignWithContext(ctx, csr, signOpts, extraOpts...)\n\trequire.NoError(t, err)\n\n\treturn chain\n}\n\nfunc Test_newHTTPClient(t *testing.T) {\n\tsigner, err := keyutil.GenerateDefaultSigner()\n\trequire.NoError(t, err)\n\n\tcsr, err := x509util.CreateCertificateRequest(\"test\", []string{\"localhost\", \"127.0.0.1\", \"[::1]\"}, signer)\n\trequire.NoError(t, err)\n\n\tauth := testAuthority(t)\n\tchain := mustCertificate(t, auth, csr)\n\n\tt.Run(\"SystemCertPool\", func(t *testing.T) {\n\t\tresp, err := auth.httpClient.Get(\"https://smallstep.com\")\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, http.StatusOK, resp.StatusCode)\n\t\tb, err := io.ReadAll(resp.Body)\n\t\tassert.NoError(t, err)\n\t\tassert.NotEmpty(t, b)\n\t\tassert.NoError(t, resp.Body.Close())\n\t})\n\n\tt.Run(\"LocalCertPool\", func(t *testing.T) {\n\t\tsrv := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tfmt.Fprint(w, \"ok\")\n\t\t}))\n\t\tsrv.TLS = &tls.Config{\n\t\t\tCertificates: []tls.Certificate{\n\t\t\t\t{Certificate: [][]byte{chain[0].Raw, chain[1].Raw}, PrivateKey: signer, Leaf: chain[0]},\n\t\t\t},\n\t\t}\n\t\tsrv.StartTLS()\n\t\tdefer srv.Close()\n\n\t\tresp, err := auth.httpClient.Get(srv.URL)\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, http.StatusOK, resp.StatusCode)\n\t\tb, err := io.ReadAll(resp.Body)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, []byte(\"ok\"), b)\n\t\tassert.NoError(t, resp.Body.Close())\n\n\t\tt.Run(\"DefaultClient\", func(t *testing.T) {\n\t\t\tclient := &http.Client{}\n\t\t\t_, err := client.Get(srv.URL)\n\t\t\tassert.Error(t, err)\n\t\t})\n\t})\n\n\tt.Run(\"custom transport\", func(t *testing.T) {\n\t\ttmp := http.DefaultTransport\n\t\tt.Cleanup(func() {\n\t\t\thttp.DefaultTransport = tmp\n\t\t})\n\t\ttransport := struct {\n\t\t\thttp.RoundTripper\n\t\t}{http.DefaultTransport}\n\t\thttp.DefaultTransport = transport\n\n\t\tclient := newHTTPClient(httptransport.NoopWrapper(), auth.rootX509Certs...)\n\t\tassert.NotNil(t, client)\n\t})\n}\n"
  },
  {
    "path": "authority/internal/constraints/constraints.go",
    "content": "package constraints\n\nimport (\n\t\"crypto/x509\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/url\"\n\n\t\"github.com/smallstep/certificates/errs\"\n)\n\n// ConstraintError is the typed error that will be returned if a constraint\n// error is found.\ntype ConstraintError struct {\n\tType   string\n\tName   string\n\tDetail string\n}\n\n// Error implements the error interface.\nfunc (e ConstraintError) Error() string {\n\treturn e.Detail\n}\n\n// As implements the As(any) bool interface and allows to use \"errors.As()\" to\n// convert the ConstraintError to an errs.Error.\nfunc (e ConstraintError) As(v any) bool {\n\tif err, ok := v.(**errs.Error); ok {\n\t\t*err = &errs.Error{\n\t\t\tStatus: http.StatusForbidden,\n\t\t\tMsg:    e.Detail,\n\t\t\tErr:    e,\n\t\t}\n\t\treturn true\n\t}\n\treturn false\n}\n\n// Engine implements a constraint validator for DNS names, IP addresses, Email\n// addresses and URIs.\ntype Engine struct {\n\thasNameConstraints      bool\n\tpermittedDNSDomains     []string\n\texcludedDNSDomains      []string\n\tpermittedIPRanges       []*net.IPNet\n\texcludedIPRanges        []*net.IPNet\n\tpermittedEmailAddresses []string\n\texcludedEmailAddresses  []string\n\tpermittedURIDomains     []string\n\texcludedURIDomains      []string\n}\n\n// New creates a constraint validation engine that contains the given chain of\n// certificates.\nfunc New(chain ...*x509.Certificate) *Engine {\n\te := new(Engine)\n\tfor _, crt := range chain {\n\t\te.permittedDNSDomains = append(e.permittedDNSDomains, crt.PermittedDNSDomains...)\n\t\te.excludedDNSDomains = append(e.excludedDNSDomains, crt.ExcludedDNSDomains...)\n\t\te.permittedIPRanges = append(e.permittedIPRanges, crt.PermittedIPRanges...)\n\t\te.excludedIPRanges = append(e.excludedIPRanges, crt.ExcludedIPRanges...)\n\t\te.permittedEmailAddresses = append(e.permittedEmailAddresses, crt.PermittedEmailAddresses...)\n\t\te.excludedEmailAddresses = append(e.excludedEmailAddresses, crt.ExcludedEmailAddresses...)\n\t\te.permittedURIDomains = append(e.permittedURIDomains, crt.PermittedURIDomains...)\n\t\te.excludedURIDomains = append(e.excludedURIDomains, crt.ExcludedURIDomains...)\n\t}\n\n\te.hasNameConstraints = len(e.permittedDNSDomains) > 0 || len(e.excludedDNSDomains) > 0 ||\n\t\tlen(e.permittedIPRanges) > 0 || len(e.excludedIPRanges) > 0 ||\n\t\tlen(e.permittedEmailAddresses) > 0 || len(e.excludedEmailAddresses) > 0 ||\n\t\tlen(e.permittedURIDomains) > 0 || len(e.excludedURIDomains) > 0\n\n\treturn e\n}\n\n// Validate checks the given names with the name constraints defined in the\n// service.\nfunc (e *Engine) Validate(dnsNames []string, ipAddresses []net.IP, emailAddresses []string, uris []*url.URL) error {\n\tif e == nil || !e.hasNameConstraints {\n\t\treturn nil\n\t}\n\n\tfor _, name := range dnsNames {\n\t\tif err := checkNameConstraints(\"DNS name\", name, name, e.permittedDNSDomains, e.excludedDNSDomains,\n\t\t\tfunc(parsedName, constraint any) (bool, error) {\n\t\t\t\treturn matchDomainConstraint(parsedName.(string), constraint.(string))\n\t\t\t},\n\t\t); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tfor _, ip := range ipAddresses {\n\t\tif err := checkNameConstraints(\"IP address\", ip.String(), ip, e.permittedIPRanges, e.excludedIPRanges,\n\t\t\tfunc(parsedName, constraint any) (bool, error) {\n\t\t\t\treturn matchIPConstraint(parsedName.(net.IP), constraint.(*net.IPNet))\n\t\t\t},\n\t\t); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tfor _, email := range emailAddresses {\n\t\tmailbox, ok := parseRFC2821Mailbox(email)\n\t\tif !ok {\n\t\t\treturn fmt.Errorf(\"cannot parse rfc822Name %q\", email)\n\t\t}\n\t\tif err := checkNameConstraints(\"Email address\", email, mailbox, e.permittedEmailAddresses, e.excludedEmailAddresses,\n\t\t\tfunc(parsedName, constraint any) (bool, error) {\n\t\t\t\treturn matchEmailConstraint(parsedName.(rfc2821Mailbox), constraint.(string))\n\t\t\t},\n\t\t); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tfor _, uri := range uris {\n\t\tif err := checkNameConstraints(\"URI\", uri.String(), uri, e.permittedURIDomains, e.excludedURIDomains,\n\t\t\tfunc(parsedName, constraint any) (bool, error) {\n\t\t\t\treturn matchURIConstraint(parsedName.(*url.URL), constraint.(string))\n\t\t\t},\n\t\t); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// ValidateCertificate validates the DNS names, IP addresses, Email addresses\n// and URIs present in the given certificate.\nfunc (e *Engine) ValidateCertificate(cert *x509.Certificate) error {\n\treturn e.Validate(cert.DNSNames, cert.IPAddresses, cert.EmailAddresses, cert.URIs)\n}\n"
  },
  {
    "path": "authority/internal/constraints/constraints_test.go",
    "content": "package constraints\n\nimport (\n\t\"crypto/x509\"\n\t\"net\"\n\t\"net/url\"\n\t\"reflect\"\n\t\"testing\"\n\n\t\"go.step.sm/crypto/minica\"\n)\n\nfunc TestNew(t *testing.T) {\n\tca1, err := minica.New()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tca2, err := minica.New(\n\t\tminica.WithIntermediateTemplate(`{\n\t\t\t\"subject\": {{ toJson .Subject }},\n\t\t\t\"keyUsage\": [\"certSign\", \"crlSign\"],\n\t\t\t\"basicConstraints\": {\n\t\t\t\t\"isCA\": true,\n\t\t\t\t\"maxPathLen\": 0\n\t\t\t},\n\t\t\t\"nameConstraints\": {\n\t\t\t\t\"critical\": true,\n\t\t\t\t\"permittedDNSDomains\": [\"internal.example.org\"],\n\t\t\t\t\"excludedDNSDomains\": [\"internal.example.com\"],\n\t\t\t\t\"permittedIPRanges\": [\"192.168.1.0/24\", \"192.168.2.1/32\"],\n\t\t\t\t\"excludedIPRanges\": [\"192.168.3.0/24\", \"192.168.4.0/28\"],\n\t\t\t\t\"permittedEmailAddresses\": [\"root@example.org\", \"example.org\", \".acme.org\"],\n\t\t\t\t\"excludedEmailAddresses\": [\"root@example.com\", \"example.com\", \".acme.com\"],\n\t\t\t\t\"permittedURIDomains\": [\"host.example.org\", \".acme.org\"],\n\t\t\t\t\"excludedURIDomains\": [\"host.example.com\", \".acme.com\"]\n\t\t\t}\n\t\t}`),\n\t)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\ttype args struct {\n\t\tchain []*x509.Certificate\n\t}\n\ttests := []struct {\n\t\tname string\n\t\targs args\n\t\twant *Engine\n\t}{\n\t\t{\"ok\", args{[]*x509.Certificate{ca1.Intermediate, ca1.Root}}, &Engine{\n\t\t\thasNameConstraints: false,\n\t\t}},\n\t\t{\"ok with constraints\", args{[]*x509.Certificate{ca2.Intermediate, ca2.Root}}, &Engine{\n\t\t\thasNameConstraints:  true,\n\t\t\tpermittedDNSDomains: []string{\"internal.example.org\"},\n\t\t\texcludedDNSDomains:  []string{\"internal.example.com\"},\n\t\t\tpermittedIPRanges: []*net.IPNet{\n\t\t\t\t{IP: net.ParseIP(\"192.168.1.0\").To4(), Mask: net.IPMask{255, 255, 255, 0}},\n\t\t\t\t{IP: net.ParseIP(\"192.168.2.1\").To4(), Mask: net.IPMask{255, 255, 255, 255}},\n\t\t\t},\n\t\t\texcludedIPRanges: []*net.IPNet{\n\t\t\t\t{IP: net.ParseIP(\"192.168.3.0\").To4(), Mask: net.IPMask{255, 255, 255, 0}},\n\t\t\t\t{IP: net.ParseIP(\"192.168.4.0\").To4(), Mask: net.IPMask{255, 255, 255, 240}},\n\t\t\t},\n\t\t\tpermittedEmailAddresses: []string{\"root@example.org\", \"example.org\", \".acme.org\"},\n\t\t\texcludedEmailAddresses:  []string{\"root@example.com\", \"example.com\", \".acme.com\"},\n\t\t\tpermittedURIDomains:     []string{\"host.example.org\", \".acme.org\"},\n\t\t\texcludedURIDomains:      []string{\"host.example.com\", \".acme.com\"},\n\t\t}},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif got := New(tt.args.chain...); !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"New() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNew_hasNameConstraints(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tfn   func(c *x509.Certificate)\n\t\twant bool\n\t}{\n\t\t{\"no constraints\", func(c *x509.Certificate) {}, false},\n\t\t{\"permittedDNSDomains\", func(c *x509.Certificate) { c.PermittedDNSDomains = []string{\"constraint\"} }, true},\n\t\t{\"excludedDNSDomains\", func(c *x509.Certificate) { c.ExcludedDNSDomains = []string{\"constraint\"} }, true},\n\t\t{\"permittedIPRanges\", func(c *x509.Certificate) {\n\t\t\tc.PermittedIPRanges = []*net.IPNet{{IP: net.ParseIP(\"192.168.3.0\").To4(), Mask: net.IPMask{255, 255, 255, 0}}}\n\t\t}, true},\n\t\t{\"excludedIPRanges\", func(c *x509.Certificate) {\n\t\t\tc.ExcludedIPRanges = []*net.IPNet{{IP: net.ParseIP(\"192.168.3.0\").To4(), Mask: net.IPMask{255, 255, 255, 0}}}\n\t\t}, true},\n\t\t{\"permittedEmailAddresses\", func(c *x509.Certificate) { c.PermittedEmailAddresses = []string{\"constraint\"} }, true},\n\t\t{\"excludedEmailAddresses\", func(c *x509.Certificate) { c.ExcludedEmailAddresses = []string{\"constraint\"} }, true},\n\t\t{\"permittedURIDomains\", func(c *x509.Certificate) { c.PermittedURIDomains = []string{\"constraint\"} }, true},\n\t\t{\"excludedURIDomains\", func(c *x509.Certificate) { c.ExcludedURIDomains = []string{\"constraint\"} }, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tcert := &x509.Certificate{}\n\t\t\ttt.fn(cert)\n\t\t\tif e := New(cert); e.hasNameConstraints != tt.want {\n\t\t\t\tt.Errorf(\"Engine.hasNameConstraints = %v, want %v\", e.hasNameConstraints, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestEngine_Validate(t *testing.T) {\n\ttype fields struct {\n\t\thasNameConstraints      bool\n\t\tpermittedDNSDomains     []string\n\t\texcludedDNSDomains      []string\n\t\tpermittedIPRanges       []*net.IPNet\n\t\texcludedIPRanges        []*net.IPNet\n\t\tpermittedEmailAddresses []string\n\t\texcludedEmailAddresses  []string\n\t\tpermittedURIDomains     []string\n\t\texcludedURIDomains      []string\n\t}\n\ttype args struct {\n\t\tdnsNames       []string\n\t\tipAddresses    []net.IP\n\t\temailAddresses []string\n\t\turis           []*url.URL\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tfields  fields\n\t\targs    args\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", fields{hasNameConstraints: false}, args{\n\t\t\tdnsNames:       []string{\"example.com\", \"host.example.com\"},\n\t\t\tipAddresses:    []net.IP{{192, 168, 1, 1}, {0x26, 0x00, 0x1f, 0x1c, 0x47, 0x01, 0x9d, 0x00, 0xc3, 0xa7, 0x66, 0x94, 0x87, 0x0f, 0x20, 0x72}},\n\t\t\temailAddresses: []string{\"root@example.com\"},\n\t\t\turis:           []*url.URL{{Scheme: \"https\", Host: \"example.com\", Path: \"/uuid/c6d1a755-0c12-431e-9136-b64cb3173ec7\"}},\n\t\t}, false},\n\t\t{\"ok permitted dns\", fields{\n\t\t\thasNameConstraints:  true,\n\t\t\tpermittedDNSDomains: []string{\"example.com\"},\n\t\t}, args{dnsNames: []string{\"example.com\", \"www.example.com\"}}, false},\n\t\t{\"ok not excluded dns\", fields{\n\t\t\thasNameConstraints: true,\n\t\t\texcludedDNSDomains: []string{\"example.org\"},\n\t\t}, args{dnsNames: []string{\"example.com\", \"www.example.com\"}}, false},\n\t\t{\"ok permitted ip\", fields{\n\t\t\thasNameConstraints: true,\n\t\t\tpermittedIPRanges: []*net.IPNet{\n\t\t\t\t{IP: net.ParseIP(\"192.168.1.0\"), Mask: net.IPMask{255, 255, 255, 0}},\n\t\t\t\t{IP: net.ParseIP(\"192.168.2.1\").To4(), Mask: net.IPMask{255, 255, 255, 255}},\n\t\t\t\t{IP: net.ParseIP(\"2600:1700:22f8:2600:e559:bd88:350a:34d6\"), Mask: net.IPMask{255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}},\n\t\t\t},\n\t\t}, args{ipAddresses: []net.IP{{192, 168, 1, 10}, {192, 168, 2, 1}, {0x26, 0x0, 0x17, 0x00, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xa, 0xb, 0xc}}}, false},\n\t\t{\"ok not excluded ip\", fields{\n\t\t\thasNameConstraints: true,\n\t\t\texcludedIPRanges: []*net.IPNet{\n\t\t\t\t{IP: net.ParseIP(\"192.168.1.0\"), Mask: net.IPMask{255, 255, 255, 0}},\n\t\t\t\t{IP: net.ParseIP(\"192.168.2.1\").To4(), Mask: net.IPMask{255, 255, 255, 255}},\n\t\t\t},\n\t\t}, args{ipAddresses: []net.IP{{192, 168, 2, 2}, {192, 168, 3, 1}}}, false},\n\t\t{\"ok permitted emails\", fields{\n\t\t\thasNameConstraints:      true,\n\t\t\tpermittedEmailAddresses: []string{\"root@example.com\", \"acme.org\", \".acme.com\"},\n\t\t}, args{emailAddresses: []string{\"root@example.com\", \"name@acme.org\", \"name@coyote.acme.com\", `\"(quoted)\"@www.acme.com`}}, false},\n\t\t{\"ok not excluded emails\", fields{\n\t\t\thasNameConstraints:     true,\n\t\t\texcludedEmailAddresses: []string{\"root@example.com\", \"acme.org\", \".acme.com\"},\n\t\t}, args{emailAddresses: []string{\"name@example.com\", \"root@acme.com\", \"root@other.com\"}}, false},\n\t\t{\"ok permitted uris\", fields{\n\t\t\thasNameConstraints:  true,\n\t\t\tpermittedURIDomains: []string{\"example.com\", \".acme.com\"},\n\t\t}, args{uris: []*url.URL{{Scheme: \"https\", Host: \"example.com\", Path: \"/path\"}, {Scheme: \"https\", Host: \"www.acme.com\", Path: \"/path\"}}}, false},\n\t\t{\"ok not excluded uris\", fields{\n\t\t\thasNameConstraints: true,\n\t\t\texcludedURIDomains: []string{\"example.com\", \".acme.com\"},\n\t\t}, args{uris: []*url.URL{{Scheme: \"https\", Host: \"example.org\", Path: \"/path\"}, {Scheme: \"https\", Host: \"acme.com\", Path: \"/path\"}}}, false},\n\t\t{\"fail permitted dns\", fields{\n\t\t\thasNameConstraints:  true,\n\t\t\tpermittedDNSDomains: []string{\"example.com\"},\n\t\t}, args{dnsNames: []string{\"www.example.com\", \"www.example.org\"}}, true},\n\t\t{\"fail not excluded dns\", fields{\n\t\t\thasNameConstraints: true,\n\t\t\texcludedDNSDomains: []string{\"example.org\"},\n\t\t}, args{dnsNames: []string{\"example.com\", \"www.example.org\"}}, true},\n\t\t{\"fail permitted ip\", fields{\n\t\t\thasNameConstraints: true,\n\t\t\tpermittedIPRanges: []*net.IPNet{\n\t\t\t\t{IP: net.ParseIP(\"192.168.1.0\").To4(), Mask: net.IPMask{255, 255, 255, 0}},\n\t\t\t\t{IP: net.ParseIP(\"192.168.2.1\").To4(), Mask: net.IPMask{255, 255, 255, 255}},\n\t\t\t},\n\t\t}, args{ipAddresses: []net.IP{{192, 168, 1, 10}, {192, 168, 2, 10}}}, true},\n\t\t{\"fail not excluded ip\", fields{\n\t\t\thasNameConstraints: true,\n\t\t\texcludedIPRanges: []*net.IPNet{\n\t\t\t\t{IP: net.ParseIP(\"192.168.1.0\").To4(), Mask: net.IPMask{255, 255, 255, 0}},\n\t\t\t\t{IP: net.ParseIP(\"192.168.2.1\").To4(), Mask: net.IPMask{255, 255, 255, 255}},\n\t\t\t},\n\t\t}, args{ipAddresses: []net.IP{{192, 168, 2, 2}, {192, 168, 1, 1}}}, true},\n\t\t{\"fail permitted emails\", fields{\n\t\t\thasNameConstraints:      true,\n\t\t\tpermittedEmailAddresses: []string{\"root@example.com\", \"acme.org\", \".acme.com\"},\n\t\t}, args{emailAddresses: []string{\"root@example.com\", \"name@acme.org\", \"name@acme.com\"}}, true},\n\t\t{\"fail not excluded emails\", fields{\n\t\t\thasNameConstraints:     true,\n\t\t\texcludedEmailAddresses: []string{\"root@example.com\", \"acme.org\", \".acme.com\"},\n\t\t}, args{emailAddresses: []string{\"name@example.com\", \"root@example.com\"}}, true},\n\t\t{\"fail permitted uris\", fields{\n\t\t\thasNameConstraints:  true,\n\t\t\tpermittedURIDomains: []string{\"example.com\", \".acme.com\"},\n\t\t}, args{uris: []*url.URL{{Scheme: \"https\", Host: \"example.com\", Path: \"/path\"}, {Scheme: \"https\", Host: \"acme.com\", Path: \"/path\"}}}, true},\n\t\t{\"fail not excluded uris\", fields{\n\t\t\thasNameConstraints: true,\n\t\t\texcludedURIDomains: []string{\"example.com\", \".acme.com\"},\n\t\t}, args{uris: []*url.URL{{Scheme: \"https\", Host: \"www.example.com\", Path: \"/path\"}, {Scheme: \"https\", Host: \"acme.com\", Path: \"/path\"}}}, true},\n\t\t{\"fail parse emails\", fields{\n\t\t\thasNameConstraints:      true,\n\t\t\tpermittedEmailAddresses: []string{\"example.com\"},\n\t\t}, args{emailAddresses: []string{`(notquoted)@example.com`}}, true},\n\t\t{\"fail match dns\", fields{\n\t\t\thasNameConstraints:  true,\n\t\t\tpermittedDNSDomains: []string{\"example.com\"},\n\t\t}, args{dnsNames: []string{`www.example.com.`}}, true},\n\t\t{\"fail match email\", fields{\n\t\t\thasNameConstraints:     true,\n\t\t\texcludedEmailAddresses: []string{`(notquoted)@example.com`},\n\t\t}, args{emailAddresses: []string{`ok@example.com`}}, true},\n\t\t{\"fail match uri\", fields{\n\t\t\thasNameConstraints:  true,\n\t\t\tpermittedURIDomains: []string{\"example.com\"},\n\t\t}, args{uris: []*url.URL{{Scheme: \"urn\", Opaque: \"uuid:36efb1ae-6617-4b23-b799-874a37aaea1c\"}}}, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\te := &Engine{\n\t\t\t\thasNameConstraints:      tt.fields.hasNameConstraints,\n\t\t\t\tpermittedDNSDomains:     tt.fields.permittedDNSDomains,\n\t\t\t\texcludedDNSDomains:      tt.fields.excludedDNSDomains,\n\t\t\t\tpermittedIPRanges:       tt.fields.permittedIPRanges,\n\t\t\t\texcludedIPRanges:        tt.fields.excludedIPRanges,\n\t\t\t\tpermittedEmailAddresses: tt.fields.permittedEmailAddresses,\n\t\t\t\texcludedEmailAddresses:  tt.fields.excludedEmailAddresses,\n\t\t\t\tpermittedURIDomains:     tt.fields.permittedURIDomains,\n\t\t\t\texcludedURIDomains:      tt.fields.excludedURIDomains,\n\t\t\t}\n\t\t\tif err := e.Validate(tt.args.dnsNames, tt.args.ipAddresses, tt.args.emailAddresses, tt.args.uris); (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"service.Validate() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestEngine_Validate_nil(t *testing.T) {\n\tvar e *Engine\n\tif err := e.Validate([]string{\"www.example.com\"}, nil, nil, nil); err != nil {\n\t\tt.Errorf(\"service.Validate() error = %v, wantErr false\", err)\n\t}\n}\n\nfunc TestEngine_ValidateCertificate(t *testing.T) {\n\ttype fields struct {\n\t\thasNameConstraints      bool\n\t\tpermittedDNSDomains     []string\n\t\texcludedDNSDomains      []string\n\t\tpermittedIPRanges       []*net.IPNet\n\t\texcludedIPRanges        []*net.IPNet\n\t\tpermittedEmailAddresses []string\n\t\texcludedEmailAddresses  []string\n\t\tpermittedURIDomains     []string\n\t\texcludedURIDomains      []string\n\t}\n\ttype args struct {\n\t\tcert *x509.Certificate\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tfields  fields\n\t\targs    args\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", fields{hasNameConstraints: false}, args{&x509.Certificate{\n\t\t\tDNSNames:       []string{\"example.com\"},\n\t\t\tIPAddresses:    []net.IP{{127, 0, 0, 1}},\n\t\t\tEmailAddresses: []string{\"info@example.com\"},\n\t\t\tURIs:           []*url.URL{{Scheme: \"https\", Host: \"uuid.example.com\", Path: \"/dc4c76b5-5262-4551-a881-48094a604d63\"}},\n\t\t}}, false},\n\t\t{\"ok with constraints\", fields{\n\t\t\thasNameConstraints:  true,\n\t\t\tpermittedDNSDomains: []string{\"example.com\"},\n\t\t\tpermittedIPRanges: []*net.IPNet{\n\t\t\t\t{IP: net.ParseIP(\"127.0.0.1\").To4(), Mask: net.IPMask{255, 255, 255, 255}},\n\t\t\t\t{IP: net.ParseIP(\"10.3.0.0\").To4(), Mask: net.IPMask{255, 255, 0, 0}},\n\t\t\t},\n\t\t\tpermittedEmailAddresses: []string{\"example.com\"},\n\t\t\tpermittedURIDomains:     []string{\".example.com\"},\n\t\t}, args{&x509.Certificate{\n\t\t\tDNSNames:       []string{\"www.example.com\"},\n\t\t\tIPAddresses:    []net.IP{{127, 0, 0, 1}, {10, 3, 1, 1}},\n\t\t\tEmailAddresses: []string{\"info@example.com\"},\n\t\t\tURIs:           []*url.URL{{Scheme: \"https\", Host: \"uuid.example.com\", Path: \"/dc4c76b5-5262-4551-a881-48094a604d63\"}},\n\t\t}}, false},\n\t\t{\"fail\", fields{\n\t\t\thasNameConstraints:  true,\n\t\t\tpermittedURIDomains: []string{\".example.com\"},\n\t\t}, args{&x509.Certificate{\n\t\t\tDNSNames:       []string{\"example.com\"},\n\t\t\tIPAddresses:    []net.IP{{127, 0, 0, 1}},\n\t\t\tEmailAddresses: []string{\"info@example.com\"},\n\t\t\tURIs:           []*url.URL{{Scheme: \"https\", Host: \"uuid.example.org\", Path: \"/dc4c76b5-5262-4551-a881-48094a604d63\"}},\n\t\t}}, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\te := &Engine{\n\t\t\t\thasNameConstraints:      tt.fields.hasNameConstraints,\n\t\t\t\tpermittedDNSDomains:     tt.fields.permittedDNSDomains,\n\t\t\t\texcludedDNSDomains:      tt.fields.excludedDNSDomains,\n\t\t\t\tpermittedIPRanges:       tt.fields.permittedIPRanges,\n\t\t\t\texcludedIPRanges:        tt.fields.excludedIPRanges,\n\t\t\t\tpermittedEmailAddresses: tt.fields.permittedEmailAddresses,\n\t\t\t\texcludedEmailAddresses:  tt.fields.excludedEmailAddresses,\n\t\t\t\tpermittedURIDomains:     tt.fields.permittedURIDomains,\n\t\t\t\texcludedURIDomains:      tt.fields.excludedURIDomains,\n\t\t\t}\n\t\t\tif err := e.ValidateCertificate(tt.args.cert); (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"Engine.ValidateCertificate() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "authority/internal/constraints/verify.go",
    "content": "// Copyright (c) 2009 The Go Authors. All rights reserved.\n//\n// Redistribution and use in source and binary forms, with or without\n// modification, are permitted provided that the following conditions are\n// met:\n//\n//    * Redistributions of source code must retain the above copyright\n// notice, this list of conditions and the following disclaimer.\n//    * Redistributions in binary form must reproduce the above\n// copyright notice, this list of conditions and the following disclaimer\n// in the documentation and/or other materials provided with the\n// distribution.\n//    * Neither the name of Google Inc. nor the names of its\n// contributors may be used to endorse or promote products derived from\n// this software without specific prior written permission.\n//\n// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n// \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\n// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\n// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\npackage constraints\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/url\"\n\t\"reflect\"\n\t\"strings\"\n)\n\nfunc checkNameConstraints(nameType, name string, parsedName, permitted, excluded any, match func(name, constraint any) (bool, error)) error {\n\texcludedValue := reflect.ValueOf(excluded)\n\tfor i := 0; i < excludedValue.Len(); i++ {\n\t\tconstraint := excludedValue.Index(i).Interface()\n\t\tmatch, err := match(parsedName, constraint)\n\t\tif err != nil {\n\t\t\treturn ConstraintError{\n\t\t\t\tType:   nameType,\n\t\t\t\tName:   name,\n\t\t\t\tDetail: err.Error(),\n\t\t\t}\n\t\t}\n\n\t\tif match {\n\t\t\treturn ConstraintError{\n\t\t\t\tType:   nameType,\n\t\t\t\tName:   name,\n\t\t\t\tDetail: fmt.Sprintf(\"%s %q is excluded by constraint %q\", nameType, name, constraint),\n\t\t\t}\n\t\t}\n\t}\n\n\tvar (\n\t\terr error\n\t\tok  = true\n\t)\n\n\tpermittedValue := reflect.ValueOf(permitted)\n\tfor i := 0; i < permittedValue.Len(); i++ {\n\t\tconstraint := permittedValue.Index(i).Interface()\n\t\tif ok, err = match(parsedName, constraint); err != nil {\n\t\t\treturn ConstraintError{\n\t\t\t\tType:   nameType,\n\t\t\t\tName:   name,\n\t\t\t\tDetail: err.Error(),\n\t\t\t}\n\t\t}\n\t\tif ok {\n\t\t\tbreak\n\t\t}\n\t}\n\tif !ok {\n\t\treturn ConstraintError{\n\t\t\tType:   nameType,\n\t\t\tName:   name,\n\t\t\tDetail: fmt.Sprintf(\"%s %q is not permitted by any constraint\", nameType, name),\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc matchDomainConstraint(domain, constraint string) (bool, error) {\n\t// The meaning of zero length constraints is not specified, but this\n\t// code follows NSS and accepts them as matching everything.\n\tif constraint == \"\" {\n\t\treturn true, nil\n\t}\n\n\tdomainLabels, ok := domainToReverseLabels(domain)\n\tif !ok {\n\t\treturn false, fmt.Errorf(\"internal error: cannot parse domain %q\", domain)\n\t}\n\n\t// RFC 5280 says that a leading period in a domain name means that at least\n\t// one label must be prepended, but only for URI and email constraints, not\n\t// DNS constraints. The code also supports that behavior for DNS\n\t// constraints.\n\n\tmustHaveSubdomains := false\n\tif constraint[0] == '.' {\n\t\tmustHaveSubdomains = true\n\t\tconstraint = constraint[1:]\n\t}\n\n\tconstraintLabels, ok := domainToReverseLabels(constraint)\n\tif !ok {\n\t\treturn false, fmt.Errorf(\"internal error: cannot parse domain %q\", constraint)\n\t}\n\n\tif len(domainLabels) < len(constraintLabels) ||\n\t\t(mustHaveSubdomains && len(domainLabels) == len(constraintLabels)) {\n\t\treturn false, nil\n\t}\n\n\tfor i, constraintLabel := range constraintLabels {\n\t\tif !strings.EqualFold(constraintLabel, domainLabels[i]) {\n\t\t\treturn false, nil\n\t\t}\n\t}\n\n\treturn true, nil\n}\n\nfunc normalizeIP(ip net.IP) net.IP {\n\tif ip4 := ip.To4(); ip4 != nil {\n\t\treturn ip4\n\t}\n\treturn ip\n}\n\nfunc matchIPConstraint(ip net.IP, constraint *net.IPNet) (bool, error) {\n\tip = normalizeIP(ip)\n\tconstraintIP := normalizeIP(constraint.IP)\n\tif len(ip) != len(constraintIP) {\n\t\treturn false, nil\n\t}\n\n\tfor i := range ip {\n\t\tif mask := constraint.Mask[i]; ip[i]&mask != constraintIP[i]&mask {\n\t\t\treturn false, nil\n\t\t}\n\t}\n\n\treturn true, nil\n}\n\nfunc matchEmailConstraint(mailbox rfc2821Mailbox, constraint string) (bool, error) {\n\t// If the constraint contains an @, then it specifies an exact mailbox\n\t// name.\n\tif strings.Contains(constraint, \"@\") {\n\t\tconstraintMailbox, ok := parseRFC2821Mailbox(constraint)\n\t\tif !ok {\n\t\t\treturn false, fmt.Errorf(\"internal error: cannot parse constraint %q\", constraint)\n\t\t}\n\t\treturn mailbox.local == constraintMailbox.local && strings.EqualFold(mailbox.domain, constraintMailbox.domain), nil\n\t}\n\n\t// Otherwise the constraint is like a DNS constraint of the domain part\n\t// of the mailbox.\n\treturn matchDomainConstraint(mailbox.domain, constraint)\n}\n\nfunc matchURIConstraint(uri *url.URL, constraint string) (bool, error) {\n\t// From RFC 5280, Section 4.2.1.10:\n\t// “a uniformResourceIdentifier that does not include an authority\n\t// component with a host name specified as a fully qualified domain\n\t// name (e.g., if the URI either does not include an authority\n\t// component or includes an authority component in which the host name\n\t// is specified as an IP address), then the application MUST reject the\n\t// certificate.”\n\n\thost := uri.Host\n\tif host == \"\" {\n\t\treturn false, fmt.Errorf(\"URI with empty host (%q) cannot be matched against constraints\", uri.String())\n\t}\n\n\tif strings.Contains(host, \":\") && !strings.HasSuffix(host, \"]\") {\n\t\tvar err error\n\t\thost, _, err = net.SplitHostPort(uri.Host)\n\t\tif err != nil {\n\t\t\treturn false, err\n\t\t}\n\t}\n\n\tif strings.HasPrefix(host, \"[\") && strings.HasSuffix(host, \"]\") ||\n\t\tnet.ParseIP(host) != nil {\n\t\treturn false, fmt.Errorf(\"URI with IP (%q) cannot be matched against constraints\", uri.String())\n\t}\n\n\treturn matchDomainConstraint(host, constraint)\n}\n\n// domainToReverseLabels converts a textual domain name like foo.example.com to\n// the list of labels in reverse order, e.g. [\"com\", \"example\", \"foo\"].\nfunc domainToReverseLabels(domain string) (reverseLabels []string, ok bool) {\n\tfor domain != \"\" {\n\t\tif i := strings.LastIndexByte(domain, '.'); i == -1 {\n\t\t\treverseLabels = append(reverseLabels, domain)\n\t\t\tdomain = \"\"\n\t\t} else {\n\t\t\treverseLabels = append(reverseLabels, domain[i+1:])\n\t\t\tdomain = domain[:i]\n\t\t}\n\t}\n\n\tif len(reverseLabels) > 0 && reverseLabels[0] == \"\" {\n\t\t// An empty label at the end indicates an absolute value.\n\t\treturn nil, false\n\t}\n\n\tfor _, label := range reverseLabels {\n\t\tif label == \"\" {\n\t\t\t// Empty labels are otherwise invalid.\n\t\t\treturn nil, false\n\t\t}\n\n\t\tfor _, c := range label {\n\t\t\tif c < 33 || c > 126 {\n\t\t\t\t// Invalid character.\n\t\t\t\treturn nil, false\n\t\t\t}\n\t\t}\n\t}\n\n\treturn reverseLabels, true\n}\n\n// rfc2821Mailbox represents a “mailbox” (which is an email address to most\n// people) by breaking it into the “local” (i.e. before the '@') and “domain”\n// parts.\ntype rfc2821Mailbox struct {\n\tlocal, domain string\n}\n\n// parseRFC2821Mailbox parses an email address into local and domain parts,\n// based on the ABNF for a “Mailbox” from RFC 2821. According to RFC 5280,\n// Section 4.2.1.6 that's correct for an rfc822Name from a certificate: “The\n// format of an rfc822Name is a \"Mailbox\" as defined in RFC 2821, Section 4.1.2”.\nfunc parseRFC2821Mailbox(in string) (mailbox rfc2821Mailbox, ok bool) {\n\tif in == \"\" {\n\t\treturn mailbox, false\n\t}\n\n\tlocalPartBytes := make([]byte, 0, len(in)/2)\n\n\tif in[0] == '\"' {\n\t\t// Quoted-string = DQUOTE *qcontent DQUOTE\n\t\t// non-whitespace-control = %d1-8 / %d11 / %d12 / %d14-31 / %d127\n\t\t// qcontent = qtext / quoted-pair\n\t\t// qtext = non-whitespace-control /\n\t\t//         %d33 / %d35-91 / %d93-126\n\t\t// quoted-pair = (\"\\\" text) / obs-qp\n\t\t// text = %d1-9 / %d11 / %d12 / %d14-127 / obs-text\n\t\t//\n\t\t// (Names beginning with “obs-” are the obsolete syntax from RFC 2822,\n\t\t// Section 4. Since it has been 16 years, we no longer accept that.)\n\t\tin = in[1:]\n\tQuotedString:\n\t\tfor {\n\t\t\tif in == \"\" {\n\t\t\t\treturn mailbox, false\n\t\t\t}\n\t\t\tc := in[0]\n\t\t\tin = in[1:]\n\n\t\t\tswitch {\n\t\t\tcase c == '\"':\n\t\t\t\tbreak QuotedString\n\n\t\t\tcase c == '\\\\':\n\t\t\t\t// quoted-pair\n\t\t\t\tif in == \"\" {\n\t\t\t\t\treturn mailbox, false\n\t\t\t\t}\n\t\t\t\tif in[0] == 11 ||\n\t\t\t\t\tin[0] == 12 ||\n\t\t\t\t\t(1 <= in[0] && in[0] <= 9) ||\n\t\t\t\t\t(14 <= in[0] && in[0] <= 127) {\n\t\t\t\t\tlocalPartBytes = append(localPartBytes, in[0])\n\t\t\t\t\tin = in[1:]\n\t\t\t\t} else {\n\t\t\t\t\treturn mailbox, false\n\t\t\t\t}\n\n\t\t\tcase c == 11 ||\n\t\t\t\tc == 12 ||\n\t\t\t\t// Space (char 32) is not allowed based on the\n\t\t\t\t// BNF, but RFC 3696 gives an example that\n\t\t\t\t// assumes that it is. Several “verified”\n\t\t\t\t// errata continue to argue about this point.\n\t\t\t\t// We choose to accept it.\n\t\t\t\tc == 32 ||\n\t\t\t\tc == 33 ||\n\t\t\t\tc == 127 ||\n\t\t\t\t(1 <= c && c <= 8) ||\n\t\t\t\t(14 <= c && c <= 31) ||\n\t\t\t\t(35 <= c && c <= 91) ||\n\t\t\t\t(93 <= c && c <= 126):\n\t\t\t\t// qtext\n\t\t\t\tlocalPartBytes = append(localPartBytes, c)\n\n\t\t\tdefault:\n\t\t\t\treturn mailbox, false\n\t\t\t}\n\t\t}\n\t} else {\n\t\t// Atom (\".\" Atom)*\n\tNextChar:\n\t\tfor in != \"\" {\n\t\t\t// atext from RFC 2822, Section 3.2.4\n\t\t\tc := in[0]\n\n\t\t\tswitch {\n\t\t\tcase c == '\\\\':\n\t\t\t\t// Examples given in RFC 3696 suggest that\n\t\t\t\t// escaped characters can appear outside of a\n\t\t\t\t// quoted string. Several “verified” errata\n\t\t\t\t// continue to argue the point. We choose to\n\t\t\t\t// accept it.\n\t\t\t\tin = in[1:]\n\t\t\t\tif in == \"\" {\n\t\t\t\t\treturn mailbox, false\n\t\t\t\t}\n\t\t\t\tfallthrough\n\n\t\t\tcase ('0' <= c && c <= '9') ||\n\t\t\t\t('a' <= c && c <= 'z') ||\n\t\t\t\t('A' <= c && c <= 'Z') ||\n\t\t\t\tc == '!' || c == '#' || c == '$' || c == '%' ||\n\t\t\t\tc == '&' || c == '\\'' || c == '*' || c == '+' ||\n\t\t\t\tc == '-' || c == '/' || c == '=' || c == '?' ||\n\t\t\t\tc == '^' || c == '_' || c == '`' || c == '{' ||\n\t\t\t\tc == '|' || c == '}' || c == '~' || c == '.':\n\t\t\t\tlocalPartBytes = append(localPartBytes, in[0])\n\t\t\t\tin = in[1:]\n\n\t\t\tdefault:\n\t\t\t\tbreak NextChar\n\t\t\t}\n\t\t}\n\n\t\tif len(localPartBytes) == 0 {\n\t\t\treturn mailbox, false\n\t\t}\n\n\t\t// From RFC 3696, Section 3:\n\t\t// “period (\".\") may also appear, but may not be used to start\n\t\t// or end the local part, nor may two or more consecutive\n\t\t// periods appear.”\n\t\ttwoDots := []byte{'.', '.'}\n\t\tif localPartBytes[0] == '.' ||\n\t\t\tlocalPartBytes[len(localPartBytes)-1] == '.' ||\n\t\t\tbytes.Contains(localPartBytes, twoDots) {\n\t\t\treturn mailbox, false\n\t\t}\n\t}\n\n\tif in == \"\" || in[0] != '@' {\n\t\treturn mailbox, false\n\t}\n\tin = in[1:]\n\n\t// The RFC species a format for domains, but that's known to be\n\t// violated in practice so we accept that anything after an '@' is the\n\t// domain part.\n\tif _, ok := domainToReverseLabels(in); !ok {\n\t\treturn mailbox, false\n\t}\n\n\tmailbox.local = string(localPartBytes)\n\tmailbox.domain = in\n\treturn mailbox, true\n}\n"
  },
  {
    "path": "authority/linkedca.go",
    "content": "package authority\n\nimport (\n\t\"context\"\n\t\"crypto\"\n\t\"crypto/sha256\"\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"encoding/hex\"\n\t\"encoding/pem\"\n\t\"fmt\"\n\t\"net/url\"\n\t\"regexp\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n\t\"golang.org/x/crypto/ssh\"\n\t\"google.golang.org/grpc\"\n\t\"google.golang.org/grpc/credentials\"\n\n\t\"github.com/smallstep/linkedca\"\n\t\"go.step.sm/crypto/jose\"\n\t\"go.step.sm/crypto/keyutil\"\n\t\"go.step.sm/crypto/tlsutil\"\n\t\"go.step.sm/crypto/x509util\"\n\n\t\"github.com/smallstep/certificates/authority/admin\"\n\t\"github.com/smallstep/certificates/authority/provisioner\"\n\t\"github.com/smallstep/certificates/db\"\n\t\"github.com/smallstep/certificates/internal/cast\"\n)\n\nconst uuidPattern = \"^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$\"\n\ntype linkedCaClient struct {\n\trenewer     *tlsutil.Renewer\n\tclient      linkedca.MajordomoClient\n\tauthorityID string\n}\n\n// interface guard\nvar _ admin.DB = (*linkedCaClient)(nil)\n\ntype linkedCAClaims struct {\n\tjose.Claims\n\tSANs []string `json:\"sans\"`\n\tSHA  string   `json:\"sha\"`\n}\n\nfunc newLinkedCAClient(token string) (*linkedCaClient, error) {\n\ttok, err := jose.ParseSigned(token)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"error parsing token\")\n\t}\n\n\tvar claims linkedCAClaims\n\tif err := tok.UnsafeClaimsWithoutVerification(&claims); err != nil {\n\t\treturn nil, errors.Wrap(err, \"error parsing token\")\n\t}\n\t// Validate claims\n\tif len(claims.Audience) != 1 {\n\t\treturn nil, errors.New(\"error parsing token: invalid aud claim\")\n\t}\n\tif claims.SHA == \"\" {\n\t\treturn nil, errors.New(\"error parsing token: invalid sha claim\")\n\t}\n\t// Get linkedCA endpoint from audience.\n\tu, err := url.Parse(claims.Audience[0])\n\tif err != nil {\n\t\treturn nil, errors.New(\"error parsing token: invalid aud claim\")\n\t}\n\t// Get authority from SANs\n\tauthority, err := getAuthority(claims.SANs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Create csr to login with\n\tsigner, err := keyutil.GenerateDefaultSigner()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tcsr, err := x509util.CreateCertificateRequest(claims.Subject, claims.SANs, signer)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Get and verify root certificate\n\troot, err := getRootCertificate(u.Host, claims.SHA)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tpool := x509.NewCertPool()\n\tpool.AddCert(root)\n\n\t// Login with majordomo and get certificates\n\tcert, tlsConfig, err := login(authority, token, csr, signer, u.Host, pool)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Start TLS renewer and set the GetClientCertificate callback to it.\n\trenewer, err := tlsutil.NewRenewer(cert, tlsConfig, func() (*tls.Certificate, *tls.Config, error) {\n\t\treturn login(authority, token, csr, signer, u.Host, pool)\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\ttlsConfig.GetClientCertificate = renewer.GetClientCertificate\n\n\t// Start mTLS client\n\tconn, err := grpc.NewClient(u.Host, grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig)))\n\tif err != nil {\n\t\treturn nil, errors.Wrapf(err, \"error connecting %s\", u.Host)\n\t}\n\n\treturn &linkedCaClient{\n\t\trenewer:     renewer,\n\t\tclient:      linkedca.NewMajordomoClient(conn),\n\t\tauthorityID: authority,\n\t}, nil\n}\n\n// IsLinkedCA is a sentinel function that can be used to\n// check if a linkedCaClient is the underlying type of an\n// admin.DB interface.\nfunc (c *linkedCaClient) IsLinkedCA() bool {\n\treturn true\n}\n\nfunc (c *linkedCaClient) Run() {\n\tc.renewer.Run()\n}\n\nfunc (c *linkedCaClient) Stop() {\n\tc.renewer.Stop()\n}\n\nfunc (c *linkedCaClient) CreateProvisioner(ctx context.Context, prov *linkedca.Provisioner) error {\n\tresp, err := c.client.CreateProvisioner(ctx, &linkedca.CreateProvisionerRequest{\n\t\tType:         prov.Type,\n\t\tName:         prov.Name,\n\t\tDetails:      prov.Details,\n\t\tClaims:       prov.Claims,\n\t\tX509Template: prov.X509Template,\n\t\tSshTemplate:  prov.SshTemplate,\n\t})\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"error creating provisioner\")\n\t}\n\tprov.Id = resp.Id\n\tprov.AuthorityId = resp.AuthorityId\n\treturn nil\n}\n\nfunc (c *linkedCaClient) GetProvisioner(ctx context.Context, id string) (*linkedca.Provisioner, error) {\n\tresp, err := c.client.GetProvisioner(ctx, &linkedca.GetProvisionerRequest{\n\t\tId: id,\n\t})\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"error getting provisioners\")\n\t}\n\treturn resp, nil\n}\n\nfunc (c *linkedCaClient) GetProvisioners(ctx context.Context) ([]*linkedca.Provisioner, error) {\n\tresp, err := c.GetConfiguration(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn resp.Provisioners, nil\n}\n\nfunc (c *linkedCaClient) GetConfiguration(ctx context.Context) (*linkedca.ConfigurationResponse, error) {\n\tresp, err := c.client.GetConfiguration(ctx, &linkedca.ConfigurationRequest{\n\t\tAuthorityId: c.authorityID,\n\t})\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"error getting configuration\")\n\t}\n\treturn resp, nil\n}\n\nfunc (c *linkedCaClient) UpdateProvisioner(ctx context.Context, prov *linkedca.Provisioner) error {\n\t_, err := c.client.UpdateProvisioner(ctx, &linkedca.UpdateProvisionerRequest{\n\t\tId:           prov.Id,\n\t\tName:         prov.Name,\n\t\tDetails:      prov.Details,\n\t\tClaims:       prov.Claims,\n\t\tX509Template: prov.X509Template,\n\t\tSshTemplate:  prov.SshTemplate,\n\t})\n\treturn errors.Wrap(err, \"error updating provisioner\")\n}\n\nfunc (c *linkedCaClient) DeleteProvisioner(ctx context.Context, id string) error {\n\t_, err := c.client.DeleteProvisioner(ctx, &linkedca.DeleteProvisionerRequest{\n\t\tId: id,\n\t})\n\treturn errors.Wrap(err, \"error deleting provisioner\")\n}\n\nfunc (c *linkedCaClient) CreateAdmin(ctx context.Context, adm *linkedca.Admin) error {\n\tresp, err := c.client.CreateAdmin(ctx, &linkedca.CreateAdminRequest{\n\t\tSubject:       adm.Subject,\n\t\tProvisionerId: adm.ProvisionerId,\n\t\tType:          adm.Type,\n\t})\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"error creating admin\")\n\t}\n\tadm.Id = resp.Id\n\tadm.AuthorityId = resp.AuthorityId\n\treturn nil\n}\n\nfunc (c *linkedCaClient) GetAdmin(ctx context.Context, id string) (*linkedca.Admin, error) {\n\tresp, err := c.client.GetAdmin(ctx, &linkedca.GetAdminRequest{\n\t\tId: id,\n\t})\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"error getting admins\")\n\t}\n\treturn resp, nil\n}\n\nfunc (c *linkedCaClient) GetAdmins(ctx context.Context) ([]*linkedca.Admin, error) {\n\tresp, err := c.GetConfiguration(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn resp.Admins, nil\n}\n\nfunc (c *linkedCaClient) UpdateAdmin(ctx context.Context, adm *linkedca.Admin) error {\n\t_, err := c.client.UpdateAdmin(ctx, &linkedca.UpdateAdminRequest{\n\t\tId:   adm.Id,\n\t\tType: adm.Type,\n\t})\n\treturn errors.Wrap(err, \"error updating admin\")\n}\n\nfunc (c *linkedCaClient) DeleteAdmin(ctx context.Context, id string) error {\n\t_, err := c.client.DeleteAdmin(ctx, &linkedca.DeleteAdminRequest{\n\t\tId: id,\n\t})\n\treturn errors.Wrap(err, \"error deleting admin\")\n}\n\nfunc (c *linkedCaClient) GetCertificateData(serial string) (*db.CertificateData, error) {\n\tctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)\n\tdefer cancel()\n\n\tresp, err := c.client.GetCertificate(ctx, &linkedca.GetCertificateRequest{\n\t\tSerial: serial,\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar pd *db.ProvisionerData\n\tif p := resp.Provisioner; p != nil {\n\t\tpd = &db.ProvisionerData{\n\t\t\tID: p.Id, Name: p.Name, Type: p.Type.String(),\n\t\t}\n\t}\n\n\tvar raInfo *provisioner.RAInfo\n\tif p := resp.RaProvisioner; p != nil && p.Provisioner != nil {\n\t\traInfo = &provisioner.RAInfo{\n\t\t\tAuthorityID:     p.AuthorityId,\n\t\t\tProvisionerID:   p.Provisioner.Id,\n\t\t\tProvisionerType: p.Provisioner.Type.String(),\n\t\t\tProvisionerName: p.Provisioner.Name,\n\t\t}\n\t}\n\n\treturn &db.CertificateData{\n\t\tProvisioner: pd,\n\t\tRaInfo:      raInfo,\n\t}, nil\n}\n\nfunc (c *linkedCaClient) StoreCertificateChain(p provisioner.Interface, fullchain ...*x509.Certificate) error {\n\tctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)\n\tdefer cancel()\n\traProvisioner, endpointID := createRegistrationAuthorityProvisioner(p)\n\t_, err := c.client.PostCertificate(ctx, &linkedca.CertificateRequest{\n\t\tPemCertificate:      serializeCertificateChain(fullchain[0]),\n\t\tPemCertificateChain: serializeCertificateChain(fullchain[1:]...),\n\t\tProvisioner:         createProvisionerIdentity(p),\n\t\tAttestationData:     createAttestationData(p),\n\t\tRaProvisioner:       raProvisioner,\n\t\tEndpointId:          endpointID,\n\t})\n\treturn errors.Wrap(err, \"error posting certificate\")\n}\n\nfunc (c *linkedCaClient) StoreRenewedCertificate(parent *x509.Certificate, fullchain ...*x509.Certificate) error {\n\tctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)\n\tdefer cancel()\n\t_, err := c.client.PostCertificate(ctx, &linkedca.CertificateRequest{\n\t\tPemCertificate:       serializeCertificateChain(fullchain[0]),\n\t\tPemCertificateChain:  serializeCertificateChain(fullchain[1:]...),\n\t\tPemParentCertificate: serializeCertificateChain(parent),\n\t})\n\treturn errors.Wrap(err, \"error posting renewed certificate\")\n}\n\nfunc (c *linkedCaClient) StoreSSHCertificate(p provisioner.Interface, crt *ssh.Certificate) error {\n\tctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)\n\tdefer cancel()\n\t_, err := c.client.PostSSHCertificate(ctx, &linkedca.SSHCertificateRequest{\n\t\tCertificate: string(ssh.MarshalAuthorizedKey(crt)),\n\t\tProvisioner: createProvisionerIdentity(p),\n\t})\n\treturn errors.Wrap(err, \"error posting ssh certificate\")\n}\n\nfunc (c *linkedCaClient) StoreRenewedSSHCertificate(p provisioner.Interface, parent, crt *ssh.Certificate) error {\n\tctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)\n\tdefer cancel()\n\t_, err := c.client.PostSSHCertificate(ctx, &linkedca.SSHCertificateRequest{\n\t\tCertificate:       string(ssh.MarshalAuthorizedKey(crt)),\n\t\tParentCertificate: string(ssh.MarshalAuthorizedKey(parent)),\n\t\tProvisioner:       createProvisionerIdentity(p),\n\t})\n\treturn errors.Wrap(err, \"error posting renewed ssh certificate\")\n}\n\nfunc (c *linkedCaClient) Revoke(crt *x509.Certificate, rci *db.RevokedCertificateInfo) error {\n\tctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)\n\tdefer cancel()\n\t_, err := c.client.RevokeCertificate(ctx, &linkedca.RevokeCertificateRequest{\n\t\tSerial:         rci.Serial,\n\t\tPemCertificate: serializeCertificate(crt),\n\t\tReason:         rci.Reason,\n\t\tReasonCode:     linkedca.RevocationReasonCode(cast.Int32(rci.ReasonCode)),\n\t\tPassive:        true,\n\t})\n\n\treturn errors.Wrap(err, \"error revoking certificate\")\n}\n\nfunc (c *linkedCaClient) RevokeSSH(cert *ssh.Certificate, rci *db.RevokedCertificateInfo) error {\n\tctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)\n\tdefer cancel()\n\t_, err := c.client.RevokeSSHCertificate(ctx, &linkedca.RevokeSSHCertificateRequest{\n\t\tSerial:      rci.Serial,\n\t\tCertificate: serializeSSHCertificate(cert),\n\t\tReason:      rci.Reason,\n\t\tReasonCode:  linkedca.RevocationReasonCode(cast.Int32(rci.ReasonCode)),\n\t\tPassive:     true,\n\t})\n\n\treturn errors.Wrap(err, \"error revoking ssh certificate\")\n}\n\nfunc (c *linkedCaClient) IsRevoked(serial string) (bool, error) {\n\tctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)\n\tdefer cancel()\n\tresp, err := c.client.GetCertificateStatus(ctx, &linkedca.GetCertificateStatusRequest{\n\t\tSerial: serial,\n\t})\n\tif err != nil {\n\t\treturn false, errors.Wrap(err, \"error getting certificate status\")\n\t}\n\treturn resp.Status != linkedca.RevocationStatus_ACTIVE, nil\n}\n\nfunc (c *linkedCaClient) IsSSHRevoked(serial string) (bool, error) {\n\tctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)\n\tdefer cancel()\n\tresp, err := c.client.GetSSHCertificateStatus(ctx, &linkedca.GetSSHCertificateStatusRequest{\n\t\tSerial: serial,\n\t})\n\tif err != nil {\n\t\treturn false, errors.Wrap(err, \"error getting certificate status\")\n\t}\n\treturn resp.Status != linkedca.RevocationStatus_ACTIVE, nil\n}\n\nfunc (c *linkedCaClient) CreateAuthorityPolicy(_ context.Context, _ *linkedca.Policy) error {\n\treturn errors.New(\"not implemented yet\")\n}\n\nfunc (c *linkedCaClient) GetAuthorityPolicy(context.Context) (*linkedca.Policy, error) {\n\treturn nil, errors.New(\"not implemented yet\")\n}\n\nfunc (c *linkedCaClient) UpdateAuthorityPolicy(_ context.Context, _ *linkedca.Policy) error {\n\treturn errors.New(\"not implemented yet\")\n}\n\nfunc (c *linkedCaClient) DeleteAuthorityPolicy(context.Context) error {\n\treturn errors.New(\"not implemented yet\")\n}\n\nfunc createProvisionerIdentity(p provisioner.Interface) *linkedca.ProvisionerIdentity {\n\tif p == nil {\n\t\treturn nil\n\t}\n\treturn &linkedca.ProvisionerIdentity{\n\t\tId:   p.GetID(),\n\t\tType: linkedca.Provisioner_Type(cast.Int32(int(p.GetType()))),\n\t\tName: p.GetName(),\n\t}\n}\n\nfunc createRegistrationAuthorityProvisioner(p provisioner.Interface) (*linkedca.RegistrationAuthorityProvisioner, string) {\n\tif rap, ok := p.(raProvisioner); ok {\n\t\tif info := rap.RAInfo(); info != nil {\n\t\t\ttyp := linkedca.Provisioner_Type_value[strings.ToUpper(info.ProvisionerType)]\n\t\t\treturn &linkedca.RegistrationAuthorityProvisioner{\n\t\t\t\tAuthorityId: info.AuthorityID,\n\t\t\t\tProvisioner: &linkedca.ProvisionerIdentity{\n\t\t\t\t\tId:   info.ProvisionerID,\n\t\t\t\t\tType: linkedca.Provisioner_Type(typ),\n\t\t\t\t\tName: info.ProvisionerName,\n\t\t\t\t},\n\t\t\t}, info.EndpointID\n\t\t}\n\t}\n\treturn nil, \"\"\n}\n\nfunc createAttestationData(p provisioner.Interface) *linkedca.AttestationData {\n\tif ap, ok := p.(attProvisioner); ok {\n\t\tif data := ap.AttestationData(); data != nil {\n\t\t\treturn &linkedca.AttestationData{\n\t\t\t\tPermanentIdentifier: data.PermanentIdentifier,\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc serializeCertificate(crt *x509.Certificate) string {\n\tif crt == nil {\n\t\treturn \"\"\n\t}\n\treturn string(pem.EncodeToMemory(&pem.Block{\n\t\tType:  \"CERTIFICATE\",\n\t\tBytes: crt.Raw,\n\t}))\n}\n\nfunc serializeCertificateChain(fullchain ...*x509.Certificate) string {\n\tvar chain string\n\tfor _, crt := range fullchain {\n\t\tchain += string(pem.EncodeToMemory(&pem.Block{\n\t\t\tType:  \"CERTIFICATE\",\n\t\t\tBytes: crt.Raw,\n\t\t}))\n\t}\n\treturn chain\n}\n\nfunc serializeSSHCertificate(crt *ssh.Certificate) string {\n\tif crt == nil {\n\t\treturn \"\"\n\t}\n\treturn string(ssh.MarshalAuthorizedKey(crt))\n}\n\nfunc getAuthority(sans []string) (string, error) {\n\tfor _, s := range sans {\n\t\tif strings.HasPrefix(s, \"urn:smallstep:authority:\") {\n\t\t\tif regexp.MustCompile(uuidPattern).MatchString(s[24:]) {\n\t\t\t\treturn s[24:], nil\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\", fmt.Errorf(\"error parsing token: invalid sans claim\")\n}\n\n// getRootCertificate creates an insecure majordomo client and returns the\n// verified root certificate.\nfunc getRootCertificate(endpoint, fingerprint string) (*x509.Certificate, error) {\n\tconn, err := grpc.NewClient(endpoint, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{\n\t\t//nolint:gosec // used in bootstrap protocol\n\t\tInsecureSkipVerify: true, // lgtm[go/disabled-certificate-check]\n\t})))\n\tif err != nil {\n\t\treturn nil, errors.Wrapf(err, \"error connecting %s\", endpoint)\n\t}\n\n\tctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)\n\tdefer cancel()\n\n\tclient := linkedca.NewMajordomoClient(conn)\n\tresp, err := client.GetRootCertificate(ctx, &linkedca.GetRootCertificateRequest{\n\t\tFingerprint: fingerprint,\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error getting root certificate: %w\", err)\n\t}\n\n\tvar block *pem.Block\n\tb := []byte(resp.PemCertificate)\n\tfor len(b) > 0 {\n\t\tblock, b = pem.Decode(b)\n\t\tif block == nil {\n\t\t\tbreak\n\t\t}\n\t\tif block.Type != \"CERTIFICATE\" || len(block.Headers) != 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\tcert, err := x509.ParseCertificate(block.Bytes)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"error parsing certificate: %w\", err)\n\t\t}\n\n\t\t// verify the sha256\n\t\tsum := sha256.Sum256(cert.Raw)\n\t\tif !strings.EqualFold(fingerprint, hex.EncodeToString(sum[:])) {\n\t\t\treturn nil, fmt.Errorf(\"error verifying certificate: SHA256 fingerprint does not match\")\n\t\t}\n\n\t\treturn cert, nil\n\t}\n\n\treturn nil, fmt.Errorf(\"error getting root certificate: certificate not found\")\n}\n\n// login creates a new majordomo client with just the root ca pool and returns\n// the signed certificate and tls configuration.\nfunc login(authority, token string, csr *x509.CertificateRequest, signer crypto.PrivateKey, endpoint string, rootCAs *x509.CertPool) (*tls.Certificate, *tls.Config, error) {\n\tconn, err := grpc.NewClient(endpoint, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{\n\t\tMinVersion: tls.VersionTLS12,\n\t\tRootCAs:    rootCAs,\n\t})))\n\tif err != nil {\n\t\treturn nil, nil, errors.Wrapf(err, \"error connecting %s\", endpoint)\n\t}\n\n\t// Login to get the signed certificate\n\tctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)\n\tdefer cancel()\n\n\tclient := linkedca.NewMajordomoClient(conn)\n\tresp, err := client.Login(ctx, &linkedca.LoginRequest{\n\t\tAuthorityId: authority,\n\t\tToken:       token,\n\t\tPemCertificateRequest: string(pem.EncodeToMemory(&pem.Block{\n\t\t\tType:  \"CERTIFICATE REQUEST\",\n\t\t\tBytes: csr.Raw,\n\t\t})),\n\t})\n\tif err != nil {\n\t\treturn nil, nil, errors.Wrapf(err, \"error logging in %s\", endpoint)\n\t}\n\n\t// Parse login response\n\tvar block *pem.Block\n\tvar bundle []*x509.Certificate\n\trest := []byte(resp.PemCertificateChain)\n\tfor {\n\t\tblock, rest = pem.Decode(rest)\n\t\tif block == nil {\n\t\t\tbreak\n\t\t}\n\t\tif block.Type != \"CERTIFICATE\" {\n\t\t\treturn nil, nil, errors.New(\"error decoding login response: pemCertificateChain is not a certificate bundle\")\n\t\t}\n\t\tcrt, err := x509.ParseCertificate(block.Bytes)\n\t\tif err != nil {\n\t\t\treturn nil, nil, errors.Wrap(err, \"error parsing login response\")\n\t\t}\n\t\tbundle = append(bundle, crt)\n\t}\n\tif len(bundle) == 0 {\n\t\treturn nil, nil, errors.New(\"error decoding login response: pemCertificateChain should not be empty\")\n\t}\n\n\t// Build tls.Certificate with PemCertificate and intermediates in the\n\t// PemCertificateChain\n\tcert := &tls.Certificate{\n\t\tPrivateKey: signer,\n\t}\n\trest = []byte(resp.PemCertificate)\n\tfor {\n\t\tblock, rest = pem.Decode(rest)\n\t\tif block == nil {\n\t\t\tbreak\n\t\t}\n\t\tif block.Type == \"CERTIFICATE\" {\n\t\t\tleaf, err := x509.ParseCertificate(block.Bytes)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, nil, errors.Wrap(err, \"error parsing pemCertificate\")\n\t\t\t}\n\t\t\tcert.Certificate = append(cert.Certificate, block.Bytes)\n\t\t\tcert.Leaf = leaf\n\t\t}\n\t}\n\n\t// Add intermediates to the tls.Certificate\n\tlast := len(bundle) - 1\n\tfor i := 0; i < last; i++ {\n\t\tcert.Certificate = append(cert.Certificate, bundle[i].Raw)\n\t}\n\n\t// Add root to the pool if it's not there yet\n\trootCAs.AddCert(bundle[last])\n\n\treturn cert, &tls.Config{\n\t\tMinVersion: tls.VersionTLS12,\n\t\tRootCAs:    rootCAs,\n\t}, nil\n}\n"
  },
  {
    "path": "authority/meter.go",
    "content": "package authority\n\nimport (\n\t\"crypto\"\n\t\"crypto/x509\"\n\t\"io\"\n\n\t\"go.step.sm/crypto/kms\"\n\tkmsapi \"go.step.sm/crypto/kms/apiv1\"\n\t\"golang.org/x/crypto/ssh\"\n\n\t\"github.com/smallstep/certificates/authority/provisioner\"\n)\n\n// Meter wraps the set of defined callbacks for metrics gatherers.\ntype Meter interface {\n\t// X509Signed is called whenever an X509 certificate is signed.\n\tX509Signed([]*x509.Certificate, provisioner.Interface, error)\n\n\t// X509Renewed is called whenever an X509 certificate is renewed.\n\tX509Renewed([]*x509.Certificate, provisioner.Interface, error)\n\n\t// X509Rekeyed is called whenever an X509 certificate is rekeyed.\n\tX509Rekeyed([]*x509.Certificate, provisioner.Interface, error)\n\n\t// X509WebhookAuthorized is called whenever an X509 authoring webhook is called.\n\tX509WebhookAuthorized(provisioner.Interface, error)\n\n\t// X509WebhookEnriched is called whenever an X509 enriching webhook is called.\n\tX509WebhookEnriched(provisioner.Interface, error)\n\n\t// SSHSigned is called whenever an SSH certificate is signed.\n\tSSHSigned(*ssh.Certificate, provisioner.Interface, error)\n\n\t// SSHRenewed is called whenever an SSH certificate is renewed.\n\tSSHRenewed(*ssh.Certificate, provisioner.Interface, error)\n\n\t// SSHRekeyed is called whenever an SSH certificate is rekeyed.\n\tSSHRekeyed(*ssh.Certificate, provisioner.Interface, error)\n\n\t// SSHWebhookAuthorized is called whenever an SSH authoring webhook is called.\n\tSSHWebhookAuthorized(provisioner.Interface, error)\n\n\t// SSHWebhookEnriched is called whenever an SSH enriching webhook is called.\n\tSSHWebhookEnriched(provisioner.Interface, error)\n\n\t// KMSSigned is called per KMS signer signature.\n\tKMSSigned(error)\n}\n\n// noopMeter implements a noop [Meter].\ntype noopMeter struct{}\n\nfunc (noopMeter) SSHRekeyed(*ssh.Certificate, provisioner.Interface, error)     {}\nfunc (noopMeter) SSHRenewed(*ssh.Certificate, provisioner.Interface, error)     {}\nfunc (noopMeter) SSHSigned(*ssh.Certificate, provisioner.Interface, error)      {}\nfunc (noopMeter) SSHWebhookAuthorized(provisioner.Interface, error)             {}\nfunc (noopMeter) SSHWebhookEnriched(provisioner.Interface, error)               {}\nfunc (noopMeter) X509Rekeyed([]*x509.Certificate, provisioner.Interface, error) {}\nfunc (noopMeter) X509Renewed([]*x509.Certificate, provisioner.Interface, error) {}\nfunc (noopMeter) X509Signed([]*x509.Certificate, provisioner.Interface, error)  {}\nfunc (noopMeter) X509WebhookAuthorized(provisioner.Interface, error)            {}\nfunc (noopMeter) X509WebhookEnriched(provisioner.Interface, error)              {}\nfunc (noopMeter) KMSSigned(error)                                               {}\n\ntype instrumentedKeyManager struct {\n\tkms.KeyManager\n\tmeter Meter\n}\n\ntype instrumentedKeyAndDecrypterManager struct {\n\tkms.KeyManager\n\tdecrypter kmsapi.Decrypter\n\tmeter     Meter\n}\n\nfunc newInstrumentedKeyManager(k kms.KeyManager, m Meter) kms.KeyManager {\n\tdecrypter, isDecrypter := k.(kmsapi.Decrypter)\n\tswitch {\n\tcase isDecrypter:\n\t\treturn &instrumentedKeyAndDecrypterManager{&instrumentedKeyManager{k, m}, decrypter, m}\n\tdefault:\n\t\treturn &instrumentedKeyManager{k, m}\n\t}\n}\n\nfunc (i *instrumentedKeyManager) CreateSigner(req *kmsapi.CreateSignerRequest) (s crypto.Signer, err error) {\n\tif s, err = i.KeyManager.CreateSigner(req); err == nil {\n\t\ts = &instrumentedKMSSigner{s, i.meter}\n\t}\n\n\treturn\n}\n\nfunc (i *instrumentedKeyAndDecrypterManager) CreateDecrypter(req *kmsapi.CreateDecrypterRequest) (s crypto.Decrypter, err error) {\n\treturn i.decrypter.CreateDecrypter(req)\n}\n\ntype instrumentedKMSSigner struct {\n\tcrypto.Signer\n\tmeter Meter\n}\n\nfunc (i *instrumentedKMSSigner) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) (signature []byte, err error) {\n\tsignature, err = i.Signer.Sign(rand, digest, opts)\n\ti.meter.KMSSigned(err)\n\n\treturn\n}\n\nvar _ kms.KeyManager = (*instrumentedKeyManager)(nil)\nvar _ kms.KeyManager = (*instrumentedKeyAndDecrypterManager)(nil)\nvar _ kmsapi.Decrypter = (*instrumentedKeyAndDecrypterManager)(nil)\n"
  },
  {
    "path": "authority/options.go",
    "content": "package authority\n\nimport (\n\t\"context\"\n\t\"crypto\"\n\t\"crypto/x509\"\n\t\"encoding/pem\"\n\n\t\"github.com/pkg/errors\"\n\t\"golang.org/x/crypto/ssh\"\n\n\t\"go.step.sm/crypto/kms\"\n\n\t\"github.com/smallstep/certificates/authority/admin\"\n\t\"github.com/smallstep/certificates/authority/config\"\n\t\"github.com/smallstep/certificates/authority/provisioner\"\n\t\"github.com/smallstep/certificates/cas\"\n\tcasapi \"github.com/smallstep/certificates/cas/apiv1\"\n\t\"github.com/smallstep/certificates/db\"\n\t\"github.com/smallstep/certificates/internal/httptransport\"\n\t\"github.com/smallstep/certificates/scep\"\n)\n\n// Option sets options to the Authority.\ntype Option func(*Authority) error\n\n// WithConfig replaces the current config with the given one. No validation is\n// performed in the given value.\nfunc WithConfig(cfg *config.Config) Option {\n\treturn func(a *Authority) error {\n\t\ta.config = cfg\n\t\treturn nil\n\t}\n}\n\n// WithConfigFile reads the given filename as a configuration file and replaces\n// the current one. No validation is performed in the given configuration.\nfunc WithConfigFile(filename string) Option {\n\treturn func(a *Authority) (err error) {\n\t\ta.config, err = config.LoadConfiguration(filename)\n\t\treturn\n\t}\n}\n\n// WithPassword set the password to decrypt the intermediate key as well as the\n// ssh host and user keys if they are not overridden by other options.\nfunc WithPassword(password []byte) Option {\n\treturn func(a *Authority) (err error) {\n\t\ta.password = password\n\t\treturn\n\t}\n}\n\n// WithSSHHostPassword set the password to decrypt the key used to sign SSH host\n// certificates.\nfunc WithSSHHostPassword(password []byte) Option {\n\treturn func(a *Authority) (err error) {\n\t\ta.sshHostPassword = password\n\t\treturn\n\t}\n}\n\n// WithSSHUserPassword set the password to decrypt the key used to sign SSH user\n// certificates.\nfunc WithSSHUserPassword(password []byte) Option {\n\treturn func(a *Authority) (err error) {\n\t\ta.sshUserPassword = password\n\t\treturn\n\t}\n}\n\n// WithIssuerPassword set the password to decrypt the certificate issuer private\n// key used in RA mode.\nfunc WithIssuerPassword(password []byte) Option {\n\treturn func(a *Authority) (err error) {\n\t\ta.issuerPassword = password\n\t\treturn\n\t}\n}\n\n// WithDatabase sets an already initialized authority database to a new\n// authority. This option is intended to be use on graceful reloads.\nfunc WithDatabase(d db.AuthDB) Option {\n\treturn func(a *Authority) error {\n\t\ta.db = d\n\t\treturn nil\n\t}\n}\n\n// WithQuietInit disables log output when the authority is initialized.\nfunc WithQuietInit() Option {\n\treturn func(a *Authority) error {\n\t\ta.quietInit = true\n\t\treturn nil\n\t}\n}\n\n// WithWebhookClient sets the http.Client to be used for outbound requests.\nfunc WithWebhookClient(c provisioner.HTTPClient) Option {\n\treturn func(a *Authority) error {\n\t\ta.webhookClient = c\n\t\treturn nil\n\t}\n}\n\n// Wrapper wraps the set of functions mapping [http.Transport] references to [http.RoundTripper].\ntype TransportWrapper = httptransport.Wrapper\n\n// WithTransportWrapper sets the transport wrapper of the authority to the provided one or, in case\n// that one is nil, to a noop one.\nfunc WithTransportWrapper(tw httptransport.Wrapper) Option {\n\tif tw == nil {\n\t\ttw = httptransport.NoopWrapper()\n\t}\n\n\treturn func(a *Authority) error {\n\t\ta.wrapTransport = tw\n\t\treturn nil\n\t}\n}\n\n// WithGetIdentityFunc sets a custom function to retrieve the identity from\n// an external resource.\nfunc WithGetIdentityFunc(fn func(ctx context.Context, p provisioner.Interface, email string) (*provisioner.Identity, error)) Option {\n\treturn func(a *Authority) error {\n\t\ta.getIdentityFunc = fn\n\t\treturn nil\n\t}\n}\n\n// WithAuthorizeRenewFunc sets a custom function that authorizes the renewal of\n// an X.509 certificate.\nfunc WithAuthorizeRenewFunc(fn func(ctx context.Context, p *provisioner.Controller, cert *x509.Certificate) error) Option {\n\treturn func(a *Authority) error {\n\t\ta.authorizeRenewFunc = fn\n\t\treturn nil\n\t}\n}\n\n// WithAuthorizeSSHRenewFunc sets a custom function that authorizes the renewal\n// of a SSH certificate.\nfunc WithAuthorizeSSHRenewFunc(fn func(ctx context.Context, p *provisioner.Controller, cert *ssh.Certificate) error) Option {\n\treturn func(a *Authority) error {\n\t\ta.authorizeSSHRenewFunc = fn\n\t\treturn nil\n\t}\n}\n\n// WithSSHBastionFunc sets a custom function to get the bastion for a\n// given user-host pair.\nfunc WithSSHBastionFunc(fn func(ctx context.Context, user, host string) (*config.Bastion, error)) Option {\n\treturn func(a *Authority) error {\n\t\ta.sshBastionFunc = fn\n\t\treturn nil\n\t}\n}\n\n// WithSSHGetHosts sets a custom function to return a list of step ssh enabled\n// hosts.\nfunc WithSSHGetHosts(fn func(ctx context.Context, cert *x509.Certificate) ([]config.Host, error)) Option {\n\treturn func(a *Authority) error {\n\t\ta.sshGetHostsFunc = fn\n\t\treturn nil\n\t}\n}\n\n// WithSSHCheckHost sets a custom function to check whether a given host is\n// step ssh enabled. The token is used to validate the request, while the roots\n// are used to validate the token.\nfunc WithSSHCheckHost(fn func(ctx context.Context, principal string, tok string, roots []*x509.Certificate) (bool, error)) Option {\n\treturn func(a *Authority) error {\n\t\ta.sshCheckHostFunc = fn\n\t\treturn nil\n\t}\n}\n\n// WithKeyManager defines the key manager used to get and create keys, and sign\n// certificates.\nfunc WithKeyManager(k kms.KeyManager) Option {\n\treturn func(a *Authority) error {\n\t\ta.keyManager = k\n\t\treturn nil\n\t}\n}\n\n// WithX509CAService allows the consumer to provide an externally implemented\n// API implementation of apiv1.CertificateAuthorityService\nfunc WithX509CAService(svc casapi.CertificateAuthorityService) Option {\n\treturn func(a *Authority) error {\n\t\ta.x509CAService = svc\n\t\treturn nil\n\t}\n}\n\n// WithX509Signer defines the signer used to sign X509 certificates.\nfunc WithX509Signer(crt *x509.Certificate, s crypto.Signer) Option {\n\treturn WithX509SignerChain([]*x509.Certificate{crt}, s)\n}\n\n// WithX509SignerChain defines the signer used to sign X509 certificates. This\n// option is similar to WithX509Signer but it supports a chain of intermediates.\nfunc WithX509SignerChain(issuerChain []*x509.Certificate, s crypto.Signer) Option {\n\treturn func(a *Authority) error {\n\t\tsrv, err := cas.New(context.Background(), casapi.Options{\n\t\t\tType:             casapi.SoftCAS,\n\t\t\tSigner:           s,\n\t\t\tCertificateChain: issuerChain,\n\t\t})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\ta.x509CAService = srv\n\t\ta.intermediateX509Certs = append(a.intermediateX509Certs, issuerChain...)\n\t\treturn nil\n\t}\n}\n\n// WithX509SignerFunc defines the function used to get the chain of certificates\n// and signer used when we sign X.509 certificates.\nfunc WithX509SignerFunc(fn func() ([]*x509.Certificate, crypto.Signer, error)) Option {\n\treturn func(a *Authority) error {\n\t\tsrv, err := cas.New(context.Background(), casapi.Options{\n\t\t\tType:              casapi.SoftCAS,\n\t\t\tCertificateSigner: fn,\n\t\t})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\ta.x509CAService = srv\n\t\treturn nil\n\t}\n}\n\n// WithFullSCEPOptions defines the options used for SCEP support.\n//\n// This feature is EXPERIMENTAL and might change at any time.\nfunc WithFullSCEPOptions(options *scep.Options) Option {\n\treturn func(a *Authority) error {\n\t\ta.scepOptions = options\n\t\ta.validateSCEP = false\n\t\treturn nil\n\t}\n}\n\n// WithSCEPKeyManager defines the key manager used on SCEP provisioners.\n//\n// This feature is EXPERIMENTAL and might change at any time.\nfunc WithSCEPKeyManager(skm provisioner.SCEPKeyManager) Option {\n\treturn func(a *Authority) error {\n\t\ta.scepKeyManager = skm\n\t\treturn nil\n\t}\n}\n\n// WithSSHUserSigner defines the signer used to sign SSH user certificates.\nfunc WithSSHUserSigner(s crypto.Signer) Option {\n\treturn func(a *Authority) error {\n\t\tsigner, err := ssh.NewSignerFromSigner(s)\n\t\tif err != nil {\n\t\t\treturn errors.Wrap(err, \"error creating ssh user signer\")\n\t\t}\n\t\ta.sshCAUserCertSignKey = signer\n\t\t// Append public key to list of user certs\n\t\tpub := signer.PublicKey()\n\t\ta.sshCAUserCerts = append(a.sshCAUserCerts, pub)\n\t\ta.sshCAUserFederatedCerts = append(a.sshCAUserFederatedCerts, pub)\n\t\treturn nil\n\t}\n}\n\n// WithSSHHostSigner defines the signer used to sign SSH host certificates.\nfunc WithSSHHostSigner(s crypto.Signer) Option {\n\treturn func(a *Authority) error {\n\t\tsigner, err := ssh.NewSignerFromSigner(s)\n\t\tif err != nil {\n\t\t\treturn errors.Wrap(err, \"error creating ssh host signer\")\n\t\t}\n\t\ta.sshCAHostCertSignKey = signer\n\t\t// Append public key to list of host certs\n\t\tpub := signer.PublicKey()\n\t\ta.sshCAHostCerts = append(a.sshCAHostCerts, pub)\n\t\ta.sshCAHostFederatedCerts = append(a.sshCAHostFederatedCerts, pub)\n\t\treturn nil\n\t}\n}\n\n// WithX509RootCerts is an option that allows to define the list of root\n// certificates to use. This option will replace any root certificate defined\n// before.\nfunc WithX509RootCerts(rootCerts ...*x509.Certificate) Option {\n\treturn func(a *Authority) error {\n\t\ta.rootX509Certs = rootCerts\n\t\treturn nil\n\t}\n}\n\n// WithX509FederatedCerts is an option that allows to define the list of\n// federated certificates. This option will replace any federated certificate\n// defined before.\nfunc WithX509FederatedCerts(certs ...*x509.Certificate) Option {\n\treturn func(a *Authority) error {\n\t\ta.federatedX509Certs = certs\n\t\treturn nil\n\t}\n}\n\n// WithX509IntermediateCerts is an option that allows to define the list of\n// intermediate certificates that the CA will be using. This option will replace\n// any intermediate certificate defined before.\n//\n// Note that these certificates will not be bundled with the certificates signed\n// by the CA, because the CAS service will take care of that. They should match,\n// but that's not guaranteed. These certificates will be mainly used for name\n// constraint validation before a certificate is issued.\n//\n// This option should only be used on specific configurations, for example when\n// WithX509SignerFunc is used, as we don't know the list of intermediates in\n// advance.\nfunc WithX509IntermediateCerts(intermediateCerts ...*x509.Certificate) Option {\n\treturn func(a *Authority) error {\n\t\ta.intermediateX509Certs = intermediateCerts\n\t\treturn nil\n\t}\n}\n\n// WithX509RootBundle is an option that allows to define the list of root\n// certificates. This option will replace any root certificate defined before.\nfunc WithX509RootBundle(pemCerts []byte) Option {\n\treturn func(a *Authority) error {\n\t\tcerts, err := readCertificateBundle(pemCerts)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\ta.rootX509Certs = certs\n\t\treturn nil\n\t}\n}\n\n// WithX509FederatedBundle is an option that allows to define the list of\n// federated certificates. This option will replace any federated certificate\n// defined before.\nfunc WithX509FederatedBundle(pemCerts []byte) Option {\n\treturn func(a *Authority) error {\n\t\tcerts, err := readCertificateBundle(pemCerts)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\ta.federatedX509Certs = certs\n\t\treturn nil\n\t}\n}\n\n// WithAdminDB is an option to set the database backing the admin APIs.\nfunc WithAdminDB(d admin.DB) Option {\n\treturn func(a *Authority) error {\n\t\ta.adminDB = d\n\t\treturn nil\n\t}\n}\n\n// WithProvisioners is an option to set the provisioner collection.\n//\n// Deprecated: provisioner collections will likely change\nfunc WithProvisioners(ps *provisioner.Collection) Option {\n\treturn func(a *Authority) error {\n\t\ta.provisioners = ps\n\t\treturn nil\n\t}\n}\n\n// WithLinkedCAToken is an option to set the authentication token used to enable\n// linked ca.\nfunc WithLinkedCAToken(token string) Option {\n\treturn func(a *Authority) error {\n\t\ta.linkedCAToken = token\n\t\treturn nil\n\t}\n}\n\n// WithX509Enforcers is an option that allows to define custom certificate\n// modifiers that will be processed just before the signing of the certificate.\nfunc WithX509Enforcers(ces ...provisioner.CertificateEnforcer) Option {\n\treturn func(a *Authority) error {\n\t\ta.x509Enforcers = ces\n\t\treturn nil\n\t}\n}\n\n// WithSkipInit is an option that allows the constructor to skip initializtion\n// of the authority.\nfunc WithSkipInit() Option {\n\treturn func(a *Authority) error {\n\t\ta.skipInit = true\n\t\treturn nil\n\t}\n}\n\nfunc readCertificateBundle(pemCerts []byte) ([]*x509.Certificate, error) {\n\tvar block *pem.Block\n\tvar certs []*x509.Certificate\n\tfor len(pemCerts) > 0 {\n\t\tblock, pemCerts = pem.Decode(pemCerts)\n\t\tif block == nil {\n\t\t\tbreak\n\t\t}\n\t\tif block.Type != \"CERTIFICATE\" || len(block.Headers) != 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\tcert, err := x509.ParseCertificate(block.Bytes)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tcerts = append(certs, cert)\n\t}\n\treturn certs, nil\n}\n\n// WithMeter is an option that sets the authority's [Meter] to the provided one.\nfunc WithMeter(m Meter) Option {\n\tif m == nil {\n\t\tm = noopMeter{}\n\t}\n\n\treturn func(a *Authority) (_ error) {\n\t\ta.meter = m\n\n\t\treturn\n\t}\n}\n"
  },
  {
    "path": "authority/policy/engine.go",
    "content": "package policy\n\nimport (\n\t\"crypto/x509\"\n\t\"errors\"\n\t\"fmt\"\n\n\t\"golang.org/x/crypto/ssh\"\n)\n\n// Engine is a container for multiple policies.\ntype Engine struct {\n\tx509Policy    X509Policy\n\tsshUserPolicy UserPolicy\n\tsshHostPolicy HostPolicy\n}\n\n// New returns a new Engine using Options.\nfunc New(options *Options) (*Engine, error) {\n\t// if no options provided, return early\n\tif options == nil {\n\t\t//nolint:nilnil // legacy\n\t\treturn nil, nil\n\t}\n\n\tvar (\n\t\tx509Policy    X509Policy\n\t\tsshHostPolicy HostPolicy\n\t\tsshUserPolicy UserPolicy\n\t\terr           error\n\t)\n\n\t// initialize the x509 allow/deny policy engine\n\tif x509Policy, err = NewX509PolicyEngine(options.GetX509Options()); err != nil {\n\t\treturn nil, err\n\t}\n\n\t// initialize the SSH allow/deny policy engine for host certificates\n\tif sshHostPolicy, err = NewSSHHostPolicyEngine(options.GetSSHOptions()); err != nil {\n\t\treturn nil, err\n\t}\n\n\t// initialize the SSH allow/deny policy engine for user certificates\n\tif sshUserPolicy, err = NewSSHUserPolicyEngine(options.GetSSHOptions()); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &Engine{\n\t\tx509Policy:    x509Policy,\n\t\tsshHostPolicy: sshHostPolicy,\n\t\tsshUserPolicy: sshUserPolicy,\n\t}, nil\n}\n\n// IsX509CertificateAllowed evaluates an X.509 certificate against\n// the X.509 policy (if available) and returns an error if one of the\n// names in the certificate is not allowed.\nfunc (e *Engine) IsX509CertificateAllowed(cert *x509.Certificate) error {\n\t// return early if there's no policy to evaluate\n\tif e == nil || e.x509Policy == nil {\n\t\treturn nil\n\t}\n\n\t// return result of X.509 policy evaluation\n\treturn e.x509Policy.IsX509CertificateAllowed(cert)\n}\n\n// AreSANsAllowed evaluates the slice of SANs against the X.509 policy\n// (if available) and returns an error if one of the SANs is not allowed.\nfunc (e *Engine) AreSANsAllowed(sans []string) error {\n\t// return early if there's no policy to evaluate\n\tif e == nil || e.x509Policy == nil {\n\t\treturn nil\n\t}\n\n\t// return result of X.509 policy evaluation\n\treturn e.x509Policy.AreSANsAllowed(sans)\n}\n\n// IsSSHCertificateAllowed evaluates an SSH certificate against the\n// user or host policy (if configured) and returns an error if one of the\n// principals in the certificate is not allowed.\nfunc (e *Engine) IsSSHCertificateAllowed(cert *ssh.Certificate) error {\n\t// return early if there's no policy to evaluate\n\tif e == nil || (e.sshHostPolicy == nil && e.sshUserPolicy == nil) {\n\t\treturn nil\n\t}\n\n\tswitch cert.CertType {\n\tcase ssh.HostCert:\n\t\t// when no host policy engine is configured, but a user policy engine is\n\t\t// configured, the host certificate is denied.\n\t\tif e.sshHostPolicy == nil && e.sshUserPolicy != nil {\n\t\t\treturn errors.New(\"authority not allowed to sign SSH host certificates when SSH user certificate policy is active\")\n\t\t}\n\n\t\t// return result of SSH host policy evaluation\n\t\treturn e.sshHostPolicy.IsSSHCertificateAllowed(cert)\n\tcase ssh.UserCert:\n\t\t// \twhen no user policy engine is configured, but a host policy engine is\n\t\t// \tconfigured, the user certificate is denied.\n\t\tif e.sshUserPolicy == nil && e.sshHostPolicy != nil {\n\t\t\treturn errors.New(\"authority not allowed to sign SSH user certificates when SSH host certificate policy is active\")\n\t\t}\n\n\t\t// return result of SSH user policy evaluation\n\t\treturn e.sshUserPolicy.IsSSHCertificateAllowed(cert)\n\tdefault:\n\t\treturn fmt.Errorf(\"unexpected SSH certificate type %q\", cert.CertType)\n\t}\n}\n"
  },
  {
    "path": "authority/policy/options.go",
    "content": "package policy\n\n// Options is a container for authority level x509 and SSH\n// policy configuration.\ntype Options struct {\n\tX509 *X509PolicyOptions `json:\"x509,omitempty\"`\n\tSSH  *SSHPolicyOptions  `json:\"ssh,omitempty\"`\n}\n\n// GetX509Options returns the x509 authority level policy\n// configuration\nfunc (o *Options) GetX509Options() *X509PolicyOptions {\n\tif o == nil {\n\t\treturn nil\n\t}\n\treturn o.X509\n}\n\n// GetSSHOptions returns the SSH authority level policy\n// configuration\nfunc (o *Options) GetSSHOptions() *SSHPolicyOptions {\n\tif o == nil {\n\t\treturn nil\n\t}\n\treturn o.SSH\n}\n\n// X509PolicyOptionsInterface is an interface for providers\n// of x509 allowed and denied names.\ntype X509PolicyOptionsInterface interface {\n\tGetAllowedNameOptions() *X509NameOptions\n\tGetDeniedNameOptions() *X509NameOptions\n\tAreWildcardNamesAllowed() bool\n}\n\n// X509PolicyOptions is a container for x509 allowed and denied\n// names.\ntype X509PolicyOptions struct {\n\t// AllowedNames contains the x509 allowed names\n\tAllowedNames *X509NameOptions `json:\"allow,omitempty\"`\n\n\t// DeniedNames contains the x509 denied names\n\tDeniedNames *X509NameOptions `json:\"deny,omitempty\"`\n\n\t// AllowWildcardNames indicates if literal wildcard names\n\t// like *.example.com are allowed. Defaults to false.\n\tAllowWildcardNames bool `json:\"allowWildcardNames,omitempty\"`\n}\n\n// X509NameOptions models the X509 name policy configuration.\ntype X509NameOptions struct {\n\tCommonNames    []string `json:\"cn,omitempty\"`\n\tDNSDomains     []string `json:\"dns,omitempty\"`\n\tIPRanges       []string `json:\"ip,omitempty\"`\n\tEmailAddresses []string `json:\"email,omitempty\"`\n\tURIDomains     []string `json:\"uri,omitempty\"`\n}\n\n// HasNames checks if the AllowedNameOptions has one or more\n// names configured.\nfunc (o *X509NameOptions) HasNames() bool {\n\treturn len(o.CommonNames) > 0 ||\n\t\tlen(o.DNSDomains) > 0 ||\n\t\tlen(o.IPRanges) > 0 ||\n\t\tlen(o.EmailAddresses) > 0 ||\n\t\tlen(o.URIDomains) > 0\n}\n\n// GetAllowedNameOptions returns x509 allowed name policy configuration\nfunc (o *X509PolicyOptions) GetAllowedNameOptions() *X509NameOptions {\n\tif o == nil {\n\t\treturn nil\n\t}\n\treturn o.AllowedNames\n}\n\n// GetDeniedNameOptions returns the x509 denied name policy configuration\nfunc (o *X509PolicyOptions) GetDeniedNameOptions() *X509NameOptions {\n\tif o == nil {\n\t\treturn nil\n\t}\n\treturn o.DeniedNames\n}\n\n// AreWildcardNamesAllowed returns whether the authority allows\n// literal wildcard names to be signed.\nfunc (o *X509PolicyOptions) AreWildcardNamesAllowed() bool {\n\tif o == nil {\n\t\treturn true\n\t}\n\treturn o.AllowWildcardNames\n}\n\n// SSHPolicyOptionsInterface is an interface for providers of\n// SSH user and host name policy configuration.\ntype SSHPolicyOptionsInterface interface {\n\tGetAllowedUserNameOptions() *SSHNameOptions\n\tGetDeniedUserNameOptions() *SSHNameOptions\n\tGetAllowedHostNameOptions() *SSHNameOptions\n\tGetDeniedHostNameOptions() *SSHNameOptions\n}\n\n// SSHPolicyOptions is a container for SSH user and host policy\n// configuration\ntype SSHPolicyOptions struct {\n\t// User contains SSH user certificate options.\n\tUser *SSHUserCertificateOptions `json:\"user,omitempty\"`\n\t// Host contains SSH host certificate options.\n\tHost *SSHHostCertificateOptions `json:\"host,omitempty\"`\n}\n\n// GetAllowedUserNameOptions returns the SSH allowed user name policy\n// configuration.\nfunc (o *SSHPolicyOptions) GetAllowedUserNameOptions() *SSHNameOptions {\n\tif o == nil || o.User == nil {\n\t\treturn nil\n\t}\n\treturn o.User.AllowedNames\n}\n\n// GetDeniedUserNameOptions returns the SSH denied user name policy\n// configuration.\nfunc (o *SSHPolicyOptions) GetDeniedUserNameOptions() *SSHNameOptions {\n\tif o == nil || o.User == nil {\n\t\treturn nil\n\t}\n\treturn o.User.DeniedNames\n}\n\n// GetAllowedHostNameOptions returns the SSH allowed host name policy\n// configuration.\nfunc (o *SSHPolicyOptions) GetAllowedHostNameOptions() *SSHNameOptions {\n\tif o == nil || o.Host == nil {\n\t\treturn nil\n\t}\n\treturn o.Host.AllowedNames\n}\n\n// GetDeniedHostNameOptions returns the SSH denied host name policy\n// configuration.\nfunc (o *SSHPolicyOptions) GetDeniedHostNameOptions() *SSHNameOptions {\n\tif o == nil || o.Host == nil {\n\t\treturn nil\n\t}\n\treturn o.Host.DeniedNames\n}\n\n// SSHUserCertificateOptions is a collection of SSH user certificate options.\ntype SSHUserCertificateOptions struct {\n\t// AllowedNames contains the names the provisioner is authorized to sign\n\tAllowedNames *SSHNameOptions `json:\"allow,omitempty\"`\n\t// DeniedNames contains the names the provisioner is not authorized to sign\n\tDeniedNames *SSHNameOptions `json:\"deny,omitempty\"`\n}\n\n// SSHHostCertificateOptions is a collection of SSH host certificate options.\n// It's an alias of SSHUserCertificateOptions, as the options are the same\n// for both types of certificates.\ntype SSHHostCertificateOptions SSHUserCertificateOptions\n\n// SSHNameOptions models the SSH name policy configuration.\ntype SSHNameOptions struct {\n\tDNSDomains     []string `json:\"dns,omitempty\"`\n\tIPRanges       []string `json:\"ip,omitempty\"`\n\tEmailAddresses []string `json:\"email,omitempty\"`\n\tPrincipals     []string `json:\"principal,omitempty\"`\n}\n\n// GetAllowedNameOptions returns the AllowedSSHNameOptions, which models the\n// names that a provisioner is authorized to sign SSH certificates for.\nfunc (o *SSHUserCertificateOptions) GetAllowedNameOptions() *SSHNameOptions {\n\tif o == nil {\n\t\treturn nil\n\t}\n\treturn o.AllowedNames\n}\n\n// GetDeniedNameOptions returns the DeniedSSHNameOptions, which models the\n// names that a provisioner is NOT authorized to sign SSH certificates for.\nfunc (o *SSHUserCertificateOptions) GetDeniedNameOptions() *SSHNameOptions {\n\tif o == nil {\n\t\treturn nil\n\t}\n\treturn o.DeniedNames\n}\n\n// HasNames checks if the SSHNameOptions has one or more\n// names configured.\nfunc (o *SSHNameOptions) HasNames() bool {\n\treturn len(o.DNSDomains) > 0 ||\n\t\tlen(o.IPRanges) > 0 ||\n\t\tlen(o.EmailAddresses) > 0 ||\n\t\tlen(o.Principals) > 0\n}\n"
  },
  {
    "path": "authority/policy/options_test.go",
    "content": "package policy\n\nimport (\n\t\"testing\"\n)\n\nfunc TestX509PolicyOptions_IsWildcardLiteralAllowed(t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\toptions *X509PolicyOptions\n\t\twant    bool\n\t}{\n\t\t{\n\t\t\tname:    \"nil-options\",\n\t\t\toptions: nil,\n\t\t\twant:    true,\n\t\t},\n\t\t{\n\t\t\tname:    \"not-set\",\n\t\t\toptions: &X509PolicyOptions{},\n\t\t\twant:    false,\n\t\t},\n\t\t{\n\t\t\tname: \"set-true\",\n\t\t\toptions: &X509PolicyOptions{\n\t\t\t\tAllowWildcardNames: true,\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"set-false\",\n\t\t\toptions: &X509PolicyOptions{\n\t\t\t\tAllowWildcardNames: false,\n\t\t\t},\n\t\t\twant: false,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif got := tt.options.AreWildcardNamesAllowed(); got != tt.want {\n\t\t\t\tt.Errorf(\"X509PolicyOptions.IsWildcardLiteralAllowed() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "authority/policy/policy.go",
    "content": "package policy\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/smallstep/linkedca\"\n\n\t\"github.com/smallstep/certificates/policy\"\n)\n\n// X509Policy is an alias for policy.X509NamePolicyEngine\ntype X509Policy policy.X509NamePolicyEngine\n\n// UserPolicy is an alias for policy.SSHNamePolicyEngine\ntype UserPolicy policy.SSHNamePolicyEngine\n\n// HostPolicy is an alias for policy.SSHNamePolicyEngine\ntype HostPolicy policy.SSHNamePolicyEngine\n\n// NewX509PolicyEngine creates a new x509 name policy engine\nfunc NewX509PolicyEngine(policyOptions X509PolicyOptionsInterface) (X509Policy, error) {\n\t// return early if no policy engine options to configure\n\tif policyOptions == nil {\n\t\t//nolint:nilnil,nolintlint // expected values\n\t\treturn nil, nil\n\t}\n\n\toptions := []policy.NamePolicyOption{}\n\n\tallowed := policyOptions.GetAllowedNameOptions()\n\tif allowed != nil && allowed.HasNames() {\n\t\toptions = append(options,\n\t\t\tpolicy.WithPermittedCommonNames(allowed.CommonNames...),\n\t\t\tpolicy.WithPermittedDNSDomains(allowed.DNSDomains...),\n\t\t\tpolicy.WithPermittedIPsOrCIDRs(allowed.IPRanges...),\n\t\t\tpolicy.WithPermittedEmailAddresses(allowed.EmailAddresses...),\n\t\t\tpolicy.WithPermittedURIDomains(allowed.URIDomains...),\n\t\t)\n\t}\n\n\tdenied := policyOptions.GetDeniedNameOptions()\n\tif denied != nil && denied.HasNames() {\n\t\toptions = append(options,\n\t\t\tpolicy.WithExcludedCommonNames(denied.CommonNames...),\n\t\t\tpolicy.WithExcludedDNSDomains(denied.DNSDomains...),\n\t\t\tpolicy.WithExcludedIPsOrCIDRs(denied.IPRanges...),\n\t\t\tpolicy.WithExcludedEmailAddresses(denied.EmailAddresses...),\n\t\t\tpolicy.WithExcludedURIDomains(denied.URIDomains...),\n\t\t)\n\t}\n\n\t// ensure no policy engine is returned when no name options were provided\n\tif len(options) == 0 {\n\t\t//nolint:nilnil,nolintlint // expected values\n\t\treturn nil, nil\n\t}\n\n\t// check if configuration specifies that wildcard names are allowed\n\tif policyOptions.AreWildcardNamesAllowed() {\n\t\toptions = append(options, policy.WithAllowLiteralWildcardNames())\n\t}\n\n\t// enable subject common name verification by default\n\toptions = append(options, policy.WithSubjectCommonNameVerification())\n\n\treturn policy.New(options...)\n}\n\ntype sshPolicyEngineType string\n\nconst (\n\tUserPolicyEngineType sshPolicyEngineType = \"user\"\n\tHostPolicyEngineType sshPolicyEngineType = \"host\"\n)\n\n// newSSHUserPolicyEngine creates a new SSH user certificate policy engine\nfunc NewSSHUserPolicyEngine(policyOptions SSHPolicyOptionsInterface) (UserPolicy, error) {\n\tpolicyEngine, err := newSSHPolicyEngine(policyOptions, UserPolicyEngineType)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn policyEngine, nil\n}\n\n// newSSHHostPolicyEngine create a new SSH host certificate policy engine\nfunc NewSSHHostPolicyEngine(policyOptions SSHPolicyOptionsInterface) (HostPolicy, error) {\n\tpolicyEngine, err := newSSHPolicyEngine(policyOptions, HostPolicyEngineType)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn policyEngine, nil\n}\n\n// newSSHPolicyEngine creates a new SSH name policy engine\nfunc newSSHPolicyEngine(policyOptions SSHPolicyOptionsInterface, typ sshPolicyEngineType) (policy.SSHNamePolicyEngine, error) {\n\t// return early if no policy engine options to configure\n\tif policyOptions == nil {\n\t\t//nolint:nilnil,nolintlint // expected values\n\t\treturn nil, nil\n\t}\n\n\tvar (\n\t\tallowed *SSHNameOptions\n\t\tdenied  *SSHNameOptions\n\t)\n\n\tswitch typ {\n\tcase UserPolicyEngineType:\n\t\tallowed = policyOptions.GetAllowedUserNameOptions()\n\t\tdenied = policyOptions.GetDeniedUserNameOptions()\n\tcase HostPolicyEngineType:\n\t\tallowed = policyOptions.GetAllowedHostNameOptions()\n\t\tdenied = policyOptions.GetDeniedHostNameOptions()\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unknown SSH policy engine type %s provided\", typ)\n\t}\n\n\toptions := []policy.NamePolicyOption{}\n\n\tif allowed != nil && allowed.HasNames() {\n\t\toptions = append(options,\n\t\t\tpolicy.WithPermittedDNSDomains(allowed.DNSDomains...),\n\t\t\tpolicy.WithPermittedIPsOrCIDRs(allowed.IPRanges...),\n\t\t\tpolicy.WithPermittedEmailAddresses(allowed.EmailAddresses...),\n\t\t\tpolicy.WithPermittedPrincipals(allowed.Principals...),\n\t\t)\n\t}\n\n\tif denied != nil && denied.HasNames() {\n\t\toptions = append(options,\n\t\t\tpolicy.WithExcludedDNSDomains(denied.DNSDomains...),\n\t\t\tpolicy.WithExcludedIPsOrCIDRs(denied.IPRanges...),\n\t\t\tpolicy.WithExcludedEmailAddresses(denied.EmailAddresses...),\n\t\t\tpolicy.WithExcludedPrincipals(denied.Principals...),\n\t\t)\n\t}\n\n\t// ensure no policy engine is returned when no name options were provided\n\tif len(options) == 0 {\n\t\t//nolint:nilnil,nolintlint // expected values\n\t\treturn nil, nil\n\t}\n\n\treturn policy.New(options...)\n}\n\nfunc LinkedToCertificates(p *linkedca.Policy) *Options {\n\t// return early\n\tif p == nil {\n\t\treturn nil\n\t}\n\n\t// return early if x509 nor SSH is set\n\tif p.GetX509() == nil && p.GetSsh() == nil {\n\t\treturn nil\n\t}\n\n\topts := &Options{}\n\n\t// fill x509 policy configuration\n\tif x509 := p.GetX509(); x509 != nil {\n\t\topts.X509 = &X509PolicyOptions{}\n\t\tif allow := x509.GetAllow(); allow != nil {\n\t\t\topts.X509.AllowedNames = &X509NameOptions{}\n\t\t\tif allow.Dns != nil {\n\t\t\t\topts.X509.AllowedNames.DNSDomains = allow.Dns\n\t\t\t}\n\t\t\tif allow.Ips != nil {\n\t\t\t\topts.X509.AllowedNames.IPRanges = allow.Ips\n\t\t\t}\n\t\t\tif allow.Emails != nil {\n\t\t\t\topts.X509.AllowedNames.EmailAddresses = allow.Emails\n\t\t\t}\n\t\t\tif allow.Uris != nil {\n\t\t\t\topts.X509.AllowedNames.URIDomains = allow.Uris\n\t\t\t}\n\t\t\tif allow.CommonNames != nil {\n\t\t\t\topts.X509.AllowedNames.CommonNames = allow.CommonNames\n\t\t\t}\n\t\t}\n\t\tif deny := x509.GetDeny(); deny != nil {\n\t\t\topts.X509.DeniedNames = &X509NameOptions{}\n\t\t\tif deny.Dns != nil {\n\t\t\t\topts.X509.DeniedNames.DNSDomains = deny.Dns\n\t\t\t}\n\t\t\tif deny.Ips != nil {\n\t\t\t\topts.X509.DeniedNames.IPRanges = deny.Ips\n\t\t\t}\n\t\t\tif deny.Emails != nil {\n\t\t\t\topts.X509.DeniedNames.EmailAddresses = deny.Emails\n\t\t\t}\n\t\t\tif deny.Uris != nil {\n\t\t\t\topts.X509.DeniedNames.URIDomains = deny.Uris\n\t\t\t}\n\t\t\tif deny.CommonNames != nil {\n\t\t\t\topts.X509.DeniedNames.CommonNames = deny.CommonNames\n\t\t\t}\n\t\t}\n\n\t\topts.X509.AllowWildcardNames = x509.GetAllowWildcardNames()\n\t}\n\n\t// fill ssh policy configuration\n\tif ssh := p.GetSsh(); ssh != nil {\n\t\topts.SSH = &SSHPolicyOptions{}\n\t\tif host := ssh.GetHost(); host != nil {\n\t\t\topts.SSH.Host = &SSHHostCertificateOptions{}\n\t\t\tif allow := host.GetAllow(); allow != nil {\n\t\t\t\topts.SSH.Host.AllowedNames = &SSHNameOptions{}\n\t\t\t\tif allow.Dns != nil {\n\t\t\t\t\topts.SSH.Host.AllowedNames.DNSDomains = allow.Dns\n\t\t\t\t}\n\t\t\t\tif allow.Ips != nil {\n\t\t\t\t\topts.SSH.Host.AllowedNames.IPRanges = allow.Ips\n\t\t\t\t}\n\t\t\t\tif allow.Principals != nil {\n\t\t\t\t\topts.SSH.Host.AllowedNames.Principals = allow.Principals\n\t\t\t\t}\n\t\t\t}\n\t\t\tif deny := host.GetDeny(); deny != nil {\n\t\t\t\topts.SSH.Host.DeniedNames = &SSHNameOptions{}\n\t\t\t\tif deny.Dns != nil {\n\t\t\t\t\topts.SSH.Host.DeniedNames.DNSDomains = deny.Dns\n\t\t\t\t}\n\t\t\t\tif deny.Ips != nil {\n\t\t\t\t\topts.SSH.Host.DeniedNames.IPRanges = deny.Ips\n\t\t\t\t}\n\t\t\t\tif deny.Principals != nil {\n\t\t\t\t\topts.SSH.Host.DeniedNames.Principals = deny.Principals\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif user := ssh.GetUser(); user != nil {\n\t\t\topts.SSH.User = &SSHUserCertificateOptions{}\n\t\t\tif allow := user.GetAllow(); allow != nil {\n\t\t\t\topts.SSH.User.AllowedNames = &SSHNameOptions{}\n\t\t\t\tif allow.Emails != nil {\n\t\t\t\t\topts.SSH.User.AllowedNames.EmailAddresses = allow.Emails\n\t\t\t\t}\n\t\t\t\tif allow.Principals != nil {\n\t\t\t\t\topts.SSH.User.AllowedNames.Principals = allow.Principals\n\t\t\t\t}\n\t\t\t}\n\t\t\tif deny := user.GetDeny(); deny != nil {\n\t\t\t\topts.SSH.User.DeniedNames = &SSHNameOptions{}\n\t\t\t\tif deny.Emails != nil {\n\t\t\t\t\topts.SSH.User.DeniedNames.EmailAddresses = deny.Emails\n\t\t\t\t}\n\t\t\t\tif deny.Principals != nil {\n\t\t\t\t\topts.SSH.User.DeniedNames.Principals = deny.Principals\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn opts\n}\n"
  },
  {
    "path": "authority/policy/policy_test.go",
    "content": "package policy\n\nimport (\n\t\"testing\"\n\n\t\"github.com/google/go-cmp/cmp\"\n\n\t\"github.com/smallstep/linkedca\"\n)\n\nfunc TestPolicyToCertificates(t *testing.T) {\n\ttype args struct {\n\t\tpolicy *linkedca.Policy\n\t}\n\ttests := []struct {\n\t\tname string\n\t\targs args\n\t\twant *Options\n\t}{\n\t\t{\n\t\t\tname: \"nil\",\n\t\t\targs: args{\n\t\t\t\tpolicy: nil,\n\t\t\t},\n\t\t\twant: nil,\n\t\t},\n\t\t{\n\t\t\tname: \"no-policy\",\n\t\t\targs: args{\n\t\t\t\t&linkedca.Policy{},\n\t\t\t},\n\t\t\twant: nil,\n\t\t},\n\t\t{\n\t\t\tname: \"partial-policy\",\n\t\t\targs: args{\n\t\t\t\t&linkedca.Policy{\n\t\t\t\t\tX509: &linkedca.X509Policy{\n\t\t\t\t\t\tAllow: &linkedca.X509Names{\n\t\t\t\t\t\t\tDns: []string{\"*.local\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tAllowWildcardNames: false,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: &Options{\n\t\t\t\tX509: &X509PolicyOptions{\n\t\t\t\t\tAllowedNames: &X509NameOptions{\n\t\t\t\t\t\tDNSDomains: []string{\"*.local\"},\n\t\t\t\t\t},\n\t\t\t\t\tAllowWildcardNames: false,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"full-policy\",\n\t\t\targs: args{\n\t\t\t\t&linkedca.Policy{\n\t\t\t\t\tX509: &linkedca.X509Policy{\n\t\t\t\t\t\tAllow: &linkedca.X509Names{\n\t\t\t\t\t\t\tDns:         []string{\"step\"},\n\t\t\t\t\t\t\tIps:         []string{\"127.0.0.1/24\"},\n\t\t\t\t\t\t\tEmails:      []string{\"*.example.com\"},\n\t\t\t\t\t\t\tUris:        []string{\"https://*.local\"},\n\t\t\t\t\t\t\tCommonNames: []string{\"some name\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tDeny: &linkedca.X509Names{\n\t\t\t\t\t\t\tDns:         []string{\"bad\"},\n\t\t\t\t\t\t\tIps:         []string{\"127.0.0.30\"},\n\t\t\t\t\t\t\tEmails:      []string{\"badhost.example.com\"},\n\t\t\t\t\t\t\tUris:        []string{\"https://badhost.local\"},\n\t\t\t\t\t\t\tCommonNames: []string{\"another name\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tAllowWildcardNames: true,\n\t\t\t\t\t},\n\t\t\t\t\tSsh: &linkedca.SSHPolicy{\n\t\t\t\t\t\tHost: &linkedca.SSHHostPolicy{\n\t\t\t\t\t\t\tAllow: &linkedca.SSHHostNames{\n\t\t\t\t\t\t\t\tDns:        []string{\"*.localhost\"},\n\t\t\t\t\t\t\t\tIps:        []string{\"127.0.0.1/24\"},\n\t\t\t\t\t\t\t\tPrincipals: []string{\"user\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tDeny: &linkedca.SSHHostNames{\n\t\t\t\t\t\t\t\tDns:        []string{\"badhost.localhost\"},\n\t\t\t\t\t\t\t\tIps:        []string{\"127.0.0.40\"},\n\t\t\t\t\t\t\t\tPrincipals: []string{\"root\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tUser: &linkedca.SSHUserPolicy{\n\t\t\t\t\t\t\tAllow: &linkedca.SSHUserNames{\n\t\t\t\t\t\t\t\tEmails:     []string{\"@work\"},\n\t\t\t\t\t\t\t\tPrincipals: []string{\"user\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tDeny: &linkedca.SSHUserNames{\n\t\t\t\t\t\t\t\tEmails:     []string{\"root@work\"},\n\t\t\t\t\t\t\t\tPrincipals: []string{\"root\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: &Options{\n\t\t\t\tX509: &X509PolicyOptions{\n\t\t\t\t\tAllowedNames: &X509NameOptions{\n\t\t\t\t\t\tDNSDomains:     []string{\"step\"},\n\t\t\t\t\t\tIPRanges:       []string{\"127.0.0.1/24\"},\n\t\t\t\t\t\tEmailAddresses: []string{\"*.example.com\"},\n\t\t\t\t\t\tURIDomains:     []string{\"https://*.local\"},\n\t\t\t\t\t\tCommonNames:    []string{\"some name\"},\n\t\t\t\t\t},\n\t\t\t\t\tDeniedNames: &X509NameOptions{\n\t\t\t\t\t\tDNSDomains:     []string{\"bad\"},\n\t\t\t\t\t\tIPRanges:       []string{\"127.0.0.30\"},\n\t\t\t\t\t\tEmailAddresses: []string{\"badhost.example.com\"},\n\t\t\t\t\t\tURIDomains:     []string{\"https://badhost.local\"},\n\t\t\t\t\t\tCommonNames:    []string{\"another name\"},\n\t\t\t\t\t},\n\t\t\t\t\tAllowWildcardNames: true,\n\t\t\t\t},\n\t\t\t\tSSH: &SSHPolicyOptions{\n\t\t\t\t\tHost: &SSHHostCertificateOptions{\n\t\t\t\t\t\tAllowedNames: &SSHNameOptions{\n\t\t\t\t\t\t\tDNSDomains: []string{\"*.localhost\"},\n\t\t\t\t\t\t\tIPRanges:   []string{\"127.0.0.1/24\"},\n\t\t\t\t\t\t\tPrincipals: []string{\"user\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tDeniedNames: &SSHNameOptions{\n\t\t\t\t\t\t\tDNSDomains: []string{\"badhost.localhost\"},\n\t\t\t\t\t\t\tIPRanges:   []string{\"127.0.0.40\"},\n\t\t\t\t\t\t\tPrincipals: []string{\"root\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tUser: &SSHUserCertificateOptions{\n\t\t\t\t\t\tAllowedNames: &SSHNameOptions{\n\t\t\t\t\t\t\tEmailAddresses: []string{\"@work\"},\n\t\t\t\t\t\t\tPrincipals:     []string{\"user\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tDeniedNames: &SSHNameOptions{\n\t\t\t\t\t\t\tEmailAddresses: []string{\"root@work\"},\n\t\t\t\t\t\t\tPrincipals:     []string{\"root\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot := LinkedToCertificates(tt.args.policy)\n\t\t\tif !cmp.Equal(tt.want, got) {\n\t\t\t\tt.Errorf(\"policyToCertificates() diff=\\n%s\", cmp.Diff(tt.want, got))\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "authority/policy.go",
    "content": "package authority\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\n\t\"github.com/smallstep/linkedca\"\n\n\t\"github.com/smallstep/certificates/authority/admin\"\n\tauthPolicy \"github.com/smallstep/certificates/authority/policy\"\n\tpolicy \"github.com/smallstep/certificates/policy\"\n)\n\ntype policyErrorType int\n\nconst (\n\tAdminLockOut policyErrorType = iota + 1\n\tStoreFailure\n\tReloadFailure\n\tConfigurationFailure\n\tEvaluationFailure\n\tInternalFailure\n)\n\ntype PolicyError struct {\n\tTyp policyErrorType\n\tErr error\n}\n\nfunc (p *PolicyError) Error() string {\n\treturn p.Err.Error()\n}\n\nfunc (a *Authority) GetAuthorityPolicy(ctx context.Context) (*linkedca.Policy, error) {\n\ta.adminMutex.Lock()\n\tdefer a.adminMutex.Unlock()\n\n\tp, err := a.adminDB.GetAuthorityPolicy(ctx)\n\tif err != nil {\n\t\treturn nil, &PolicyError{\n\t\t\tTyp: InternalFailure,\n\t\t\tErr: err,\n\t\t}\n\t}\n\n\treturn p, nil\n}\n\nfunc (a *Authority) CreateAuthorityPolicy(ctx context.Context, adm *linkedca.Admin, p *linkedca.Policy) (*linkedca.Policy, error) {\n\ta.adminMutex.Lock()\n\tdefer a.adminMutex.Unlock()\n\n\tif err := a.checkAuthorityPolicy(ctx, adm, p); err != nil {\n\t\treturn nil, err\n\t}\n\n\tif err := a.adminDB.CreateAuthorityPolicy(ctx, p); err != nil {\n\t\treturn nil, &PolicyError{\n\t\t\tTyp: StoreFailure,\n\t\t\tErr: err,\n\t\t}\n\t}\n\n\tif err := a.reloadPolicyEngines(ctx); err != nil {\n\t\treturn nil, &PolicyError{\n\t\t\tTyp: ReloadFailure,\n\t\t\tErr: fmt.Errorf(\"error reloading policy engines when creating authority policy: %w\", err),\n\t\t}\n\t}\n\n\treturn p, nil\n}\n\nfunc (a *Authority) UpdateAuthorityPolicy(ctx context.Context, adm *linkedca.Admin, p *linkedca.Policy) (*linkedca.Policy, error) {\n\ta.adminMutex.Lock()\n\tdefer a.adminMutex.Unlock()\n\n\tif err := a.checkAuthorityPolicy(ctx, adm, p); err != nil {\n\t\treturn nil, err\n\t}\n\n\tif err := a.adminDB.UpdateAuthorityPolicy(ctx, p); err != nil {\n\t\treturn nil, &PolicyError{\n\t\t\tTyp: StoreFailure,\n\t\t\tErr: err,\n\t\t}\n\t}\n\n\tif err := a.reloadPolicyEngines(ctx); err != nil {\n\t\treturn nil, &PolicyError{\n\t\t\tTyp: ReloadFailure,\n\t\t\tErr: fmt.Errorf(\"error reloading policy engines when updating authority policy: %w\", err),\n\t\t}\n\t}\n\n\treturn p, nil\n}\n\nfunc (a *Authority) RemoveAuthorityPolicy(ctx context.Context) error {\n\ta.adminMutex.Lock()\n\tdefer a.adminMutex.Unlock()\n\n\tif err := a.adminDB.DeleteAuthorityPolicy(ctx); err != nil {\n\t\treturn &PolicyError{\n\t\t\tTyp: StoreFailure,\n\t\t\tErr: err,\n\t\t}\n\t}\n\n\tif err := a.reloadPolicyEngines(ctx); err != nil {\n\t\treturn &PolicyError{\n\t\t\tTyp: ReloadFailure,\n\t\t\tErr: fmt.Errorf(\"error reloading policy engines when deleting authority policy: %w\", err),\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (a *Authority) checkAuthorityPolicy(ctx context.Context, currentAdmin *linkedca.Admin, p *linkedca.Policy) error {\n\t// no policy and thus nothing to evaluate; return early\n\tif p == nil {\n\t\treturn nil\n\t}\n\n\t// get all current admins from the database\n\tallAdmins, err := a.adminDB.GetAdmins(ctx)\n\tif err != nil {\n\t\treturn &PolicyError{\n\t\t\tTyp: InternalFailure,\n\t\t\tErr: fmt.Errorf(\"error retrieving admins: %w\", err),\n\t\t}\n\t}\n\n\treturn a.checkPolicy(ctx, currentAdmin, allAdmins, p)\n}\n\nfunc (a *Authority) checkProvisionerPolicy(ctx context.Context, provName string, p *linkedca.Policy) error {\n\t// no policy and thus nothing to evaluate; return early\n\tif p == nil {\n\t\treturn nil\n\t}\n\n\t// get all admins for the provisioner; ignoring case in which they're not found\n\tallProvisionerAdmins, _ := a.admins.LoadByProvisioner(provName)\n\n\t// check the policy; pass in nil as the current admin, as all admins for the\n\t// provisioner will be checked by looping through allProvisionerAdmins. Also,\n\t// the current admin may be a super admin not belonging to the provisioner, so\n\t// can't be blocked, but is not required to be in the policy, either.\n\treturn a.checkPolicy(ctx, nil, allProvisionerAdmins, p)\n}\n\n// checkPolicy checks if a new or updated policy configuration results in the user\n// locking themselves or other admins out of the CA.\nfunc (a *Authority) checkPolicy(_ context.Context, currentAdmin *linkedca.Admin, otherAdmins []*linkedca.Admin, p *linkedca.Policy) error {\n\t// convert the policy; return early if nil\n\tpolicyOptions := authPolicy.LinkedToCertificates(p)\n\tif policyOptions == nil {\n\t\treturn nil\n\t}\n\n\tengine, err := authPolicy.NewX509PolicyEngine(policyOptions.GetX509Options())\n\tif err != nil {\n\t\treturn &PolicyError{\n\t\t\tTyp: ConfigurationFailure,\n\t\t\tErr: err,\n\t\t}\n\t}\n\n\t// when an empty X.509 policy is provided, the resulting engine is nil\n\t// and there's no policy to evaluate.\n\tif engine == nil {\n\t\treturn nil\n\t}\n\n\t// TODO(hs): Provide option to force the policy, even when the admin subject would be locked out?\n\n\t// check if the admin user that instructed the authority policy to be\n\t// created or updated, would still be allowed when the provided policy\n\t// would be applied. This case is skipped when current admin is nil, which\n\t// is the case when a provisioner policy is checked.\n\tif currentAdmin != nil {\n\t\tsans := []string{currentAdmin.GetSubject()}\n\t\tif err := isAllowed(engine, sans); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\t// loop through admins to verify that none of them would be\n\t// locked out when the new policy were to be applied. Returns\n\t// an error with a message that includes the admin subject that\n\t// would be locked out.\n\tfor _, adm := range otherAdmins {\n\t\tsans := []string{adm.GetSubject()}\n\t\tif err := isAllowed(engine, sans); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\t// TODO(hs): mask the error message for non-super admins?\n\n\treturn nil\n}\n\n// reloadPolicyEngines reloads x509 and SSH policy engines using\n// configuration stored in the DB or from the configuration file.\nfunc (a *Authority) reloadPolicyEngines(ctx context.Context) error {\n\tvar (\n\t\terr           error\n\t\tpolicyOptions *authPolicy.Options\n\t)\n\n\tif a.config.AuthorityConfig.EnableAdmin {\n\t\t// temporarily disable policy loading when LinkedCA is in use\n\t\tif _, ok := a.adminDB.(*linkedCaClient); ok {\n\t\t\treturn nil\n\t\t}\n\n\t\tlinkedPolicy, err := a.adminDB.GetAuthorityPolicy(ctx)\n\t\tif err != nil {\n\t\t\tvar ae *admin.Error\n\t\t\tif isAdminError := errors.As(err, &ae); (isAdminError && ae.Type != admin.ErrorNotFoundType.String()) || !isAdminError {\n\t\t\t\treturn fmt.Errorf(\"error getting policy to (re)load policy engines: %w\", err)\n\t\t\t}\n\t\t}\n\t\tpolicyOptions = authPolicy.LinkedToCertificates(linkedPolicy)\n\t} else {\n\t\tpolicyOptions = a.config.AuthorityConfig.Policy\n\t}\n\n\tengine, err := authPolicy.New(policyOptions)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// only update the policy engine when no error was returned\n\ta.policyEngine = engine\n\n\treturn nil\n}\n\nfunc isAllowed(engine authPolicy.X509Policy, sans []string) error {\n\tif err := engine.AreSANsAllowed(sans); err != nil {\n\t\tvar policyErr *policy.NamePolicyError\n\t\tisNamePolicyError := errors.As(err, &policyErr)\n\t\tif isNamePolicyError && policyErr.Reason == policy.NotAllowed {\n\t\t\treturn &PolicyError{\n\t\t\t\tTyp: AdminLockOut,\n\t\t\t\tErr: fmt.Errorf(\"the provided policy would lock out %s from the CA. Please create an x509 policy to include %s as an allowed DNS name\", sans, sans),\n\t\t\t}\n\t\t}\n\t\treturn &PolicyError{\n\t\t\tTyp: EvaluationFailure,\n\t\t\tErr: err,\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "authority/policy_test.go",
    "content": "package authority\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"reflect\"\n\t\"testing\"\n\n\t\"github.com/go-jose/go-jose/v3\"\n\t\"github.com/stretchr/testify/assert\"\n\n\t\"github.com/smallstep/linkedca\"\n\n\t\"github.com/smallstep/certificates/authority/admin\"\n\t\"github.com/smallstep/certificates/authority/administrator\"\n\t\"github.com/smallstep/certificates/authority/config\"\n\t\"github.com/smallstep/certificates/authority/policy\"\n\t\"github.com/smallstep/certificates/authority/provisioner\"\n\t\"github.com/smallstep/certificates/db\"\n)\n\nfunc TestAuthority_checkPolicy(t *testing.T) {\n\ttype test struct {\n\t\tctx          context.Context\n\t\tcurrentAdmin *linkedca.Admin\n\t\totherAdmins  []*linkedca.Admin\n\t\tpolicy       *linkedca.Policy\n\t\terr          *PolicyError\n\t}\n\ttests := map[string]func(t *testing.T) test{\n\t\t\"fail/NewX509PolicyEngine-error\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tctx: context.Background(),\n\t\t\t\tpolicy: &linkedca.Policy{\n\t\t\t\t\tX509: &linkedca.X509Policy{\n\t\t\t\t\t\tAllow: &linkedca.X509Names{\n\t\t\t\t\t\t\tDns: []string{\"**.local\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: &PolicyError{\n\t\t\t\t\tTyp: ConfigurationFailure,\n\t\t\t\t\tErr: errors.New(\"cannot parse permitted domain constraint \\\"**.local\\\": domain constraint \\\"**.local\\\" can only have wildcard as starting character\"),\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"fail/currentAdmin-evaluation-error\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tctx:          context.Background(),\n\t\t\t\tcurrentAdmin: &linkedca.Admin{Subject: \"*\"},\n\t\t\t\totherAdmins:  []*linkedca.Admin{},\n\t\t\t\tpolicy: &linkedca.Policy{\n\t\t\t\t\tX509: &linkedca.X509Policy{\n\t\t\t\t\t\tAllow: &linkedca.X509Names{\n\t\t\t\t\t\t\tDns: []string{\"*.local\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: &PolicyError{\n\t\t\t\t\tTyp: EvaluationFailure,\n\t\t\t\t\tErr: errors.New(\"cannot parse dns domain \\\"*\\\"\"),\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"fail/currentAdmin-lockout\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tctx:          context.Background(),\n\t\t\t\tcurrentAdmin: &linkedca.Admin{Subject: \"step\"},\n\t\t\t\totherAdmins: []*linkedca.Admin{\n\t\t\t\t\t{\n\t\t\t\t\t\tSubject: \"otherAdmin\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tpolicy: &linkedca.Policy{\n\t\t\t\t\tX509: &linkedca.X509Policy{\n\t\t\t\t\t\tAllow: &linkedca.X509Names{\n\t\t\t\t\t\t\tDns: []string{\"*.local\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: &PolicyError{\n\t\t\t\t\tTyp: AdminLockOut,\n\t\t\t\t\tErr: errors.New(\"the provided policy would lock out [step] from the CA. Please create an x509 policy to include [step] as an allowed DNS name\"),\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"fail/otherAdmins-evaluation-error\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tctx:          context.Background(),\n\t\t\t\tcurrentAdmin: &linkedca.Admin{Subject: \"step\"},\n\t\t\t\totherAdmins: []*linkedca.Admin{\n\t\t\t\t\t{\n\t\t\t\t\t\tSubject: \"other\",\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tSubject: \"**\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tpolicy: &linkedca.Policy{\n\t\t\t\t\tX509: &linkedca.X509Policy{\n\t\t\t\t\t\tAllow: &linkedca.X509Names{\n\t\t\t\t\t\t\tDns: []string{\"step\", \"other\", \"*.local\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: &PolicyError{\n\t\t\t\t\tTyp: EvaluationFailure,\n\t\t\t\t\tErr: errors.New(\"cannot parse dns domain \\\"**\\\"\"),\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"fail/otherAdmins-lockout\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tctx:          context.Background(),\n\t\t\t\tcurrentAdmin: &linkedca.Admin{Subject: \"step\"},\n\t\t\t\totherAdmins: []*linkedca.Admin{\n\t\t\t\t\t{\n\t\t\t\t\t\tSubject: \"otherAdmin\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tpolicy: &linkedca.Policy{\n\t\t\t\t\tX509: &linkedca.X509Policy{\n\t\t\t\t\t\tAllow: &linkedca.X509Names{\n\t\t\t\t\t\t\tDns: []string{\"step\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: &PolicyError{\n\t\t\t\t\tTyp: AdminLockOut,\n\t\t\t\t\tErr: errors.New(\"the provided policy would lock out [otherAdmin] from the CA. Please create an x509 policy to include [otherAdmin] as an allowed DNS name\"),\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"ok/no-policy\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tctx:          context.Background(),\n\t\t\t\tcurrentAdmin: &linkedca.Admin{Subject: \"step\"},\n\t\t\t\totherAdmins:  []*linkedca.Admin{},\n\t\t\t\tpolicy:       nil,\n\t\t\t}\n\t\t},\n\t\t\"ok/empty-policy\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tctx:          context.Background(),\n\t\t\t\tcurrentAdmin: &linkedca.Admin{Subject: \"step\"},\n\t\t\t\totherAdmins:  []*linkedca.Admin{},\n\t\t\t\tpolicy: &linkedca.Policy{\n\t\t\t\t\tX509: &linkedca.X509Policy{\n\t\t\t\t\t\tAllow: &linkedca.X509Names{\n\t\t\t\t\t\t\tDns: []string{},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"ok/policy\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tctx:          context.Background(),\n\t\t\t\tcurrentAdmin: &linkedca.Admin{Subject: \"step\"},\n\t\t\t\totherAdmins: []*linkedca.Admin{\n\t\t\t\t\t{\n\t\t\t\t\t\tSubject: \"otherAdmin\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tpolicy: &linkedca.Policy{\n\t\t\t\t\tX509: &linkedca.X509Policy{\n\t\t\t\t\t\tAllow: &linkedca.X509Names{\n\t\t\t\t\t\t\tDns: []string{\"step\", \"otherAdmin\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t}\n\n\tfor name, prep := range tests {\n\t\ttc := prep(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ta := &Authority{}\n\n\t\t\terr := a.checkPolicy(tc.ctx, tc.currentAdmin, tc.otherAdmins, tc.policy)\n\n\t\t\tif tc.err == nil {\n\t\t\t\tassert.Nil(t, err)\n\t\t\t} else {\n\t\t\t\tassert.IsType(t, &PolicyError{}, err)\n\n\t\t\t\tvar pe *PolicyError\n\t\t\t\tif assert.True(t, errors.As(err, &pe)) {\n\t\t\t\t\tassert.Equal(t, tc.err.Typ, pe.Typ)\n\t\t\t\t\tassert.Equal(t, tc.err.Error(), pe.Error())\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc mustPolicyEngine(t *testing.T, options *policy.Options) *policy.Engine {\n\tengine, err := policy.New(options)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn engine\n}\n\nfunc TestAuthority_reloadPolicyEngines(t *testing.T) {\n\n\texistingPolicyEngine, err := policy.New(&policy.Options{\n\t\tX509: &policy.X509PolicyOptions{\n\t\t\tAllowedNames: &policy.X509NameOptions{\n\t\t\t\tDNSDomains: []string{\"*.hosts.example.com\"},\n\t\t\t},\n\t\t},\n\t\tSSH: &policy.SSHPolicyOptions{\n\t\t\tHost: &policy.SSHHostCertificateOptions{\n\t\t\t\tAllowedNames: &policy.SSHNameOptions{\n\t\t\t\t\tDNSDomains: []string{\"*.hosts.example.com\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tUser: &policy.SSHUserCertificateOptions{\n\t\t\t\tAllowedNames: &policy.SSHNameOptions{\n\t\t\t\t\tEmailAddresses: []string{\"@mails.example.com\"},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t})\n\tassert.NoError(t, err)\n\n\tnewX509Options := &policy.Options{\n\t\tX509: &policy.X509PolicyOptions{\n\t\t\tAllowedNames: &policy.X509NameOptions{\n\t\t\t\tDNSDomains: []string{\"*.local\"},\n\t\t\t},\n\t\t\tDeniedNames: &policy.X509NameOptions{\n\t\t\t\tDNSDomains: []string{\"badhost.local\"},\n\t\t\t},\n\t\t\tAllowWildcardNames: true,\n\t\t},\n\t}\n\n\tnewSSHHostOptions := &policy.Options{\n\t\tSSH: &policy.SSHPolicyOptions{\n\t\t\tHost: &policy.SSHHostCertificateOptions{\n\t\t\t\tAllowedNames: &policy.SSHNameOptions{\n\t\t\t\t\tDNSDomains: []string{\"*.local\"},\n\t\t\t\t},\n\t\t\t\tDeniedNames: &policy.SSHNameOptions{\n\t\t\t\t\tDNSDomains: []string{\"badhost.local\"},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tnewSSHUserOptions := &policy.Options{\n\t\tSSH: &policy.SSHPolicyOptions{\n\t\t\tUser: &policy.SSHUserCertificateOptions{\n\t\t\t\tAllowedNames: &policy.SSHNameOptions{\n\t\t\t\t\tPrincipals: []string{\"*\"},\n\t\t\t\t},\n\t\t\t\tDeniedNames: &policy.SSHNameOptions{\n\t\t\t\t\tPrincipals: []string{\"root\"},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tnewSSHOptions := &policy.Options{\n\t\tSSH: &policy.SSHPolicyOptions{\n\t\t\tHost: &policy.SSHHostCertificateOptions{\n\t\t\t\tAllowedNames: &policy.SSHNameOptions{\n\t\t\t\t\tDNSDomains: []string{\"*.local\"},\n\t\t\t\t},\n\t\t\t\tDeniedNames: &policy.SSHNameOptions{\n\t\t\t\t\tDNSDomains: []string{\"badhost.local\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tUser: &policy.SSHUserCertificateOptions{\n\t\t\t\tAllowedNames: &policy.SSHNameOptions{\n\t\t\t\t\tPrincipals: []string{\"*\"},\n\t\t\t\t},\n\t\t\t\tDeniedNames: &policy.SSHNameOptions{\n\t\t\t\t\tPrincipals: []string{\"root\"},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tnewOptions := &policy.Options{\n\t\tX509: &policy.X509PolicyOptions{\n\t\t\tAllowedNames: &policy.X509NameOptions{\n\t\t\t\tDNSDomains: []string{\"*.local\"},\n\t\t\t},\n\t\t\tDeniedNames: &policy.X509NameOptions{\n\t\t\t\tDNSDomains: []string{\"badhost.local\"},\n\t\t\t},\n\t\t\tAllowWildcardNames: true,\n\t\t},\n\t\tSSH: &policy.SSHPolicyOptions{\n\t\t\tHost: &policy.SSHHostCertificateOptions{\n\t\t\t\tAllowedNames: &policy.SSHNameOptions{\n\t\t\t\t\tDNSDomains: []string{\"*.local\"},\n\t\t\t\t},\n\t\t\t\tDeniedNames: &policy.SSHNameOptions{\n\t\t\t\t\tDNSDomains: []string{\"badhost.local\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tUser: &policy.SSHUserCertificateOptions{\n\t\t\t\tAllowedNames: &policy.SSHNameOptions{\n\t\t\t\t\tPrincipals: []string{\"*\"},\n\t\t\t\t},\n\t\t\t\tDeniedNames: &policy.SSHNameOptions{\n\t\t\t\t\tPrincipals: []string{\"root\"},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tnewAdminX509Options := &policy.Options{\n\t\tX509: &policy.X509PolicyOptions{\n\t\t\tAllowedNames: &policy.X509NameOptions{\n\t\t\t\tDNSDomains: []string{\"*.local\"},\n\t\t\t},\n\t\t},\n\t}\n\n\tnewAdminSSHHostOptions := &policy.Options{\n\t\tSSH: &policy.SSHPolicyOptions{\n\t\t\tHost: &policy.SSHHostCertificateOptions{\n\t\t\t\tAllowedNames: &policy.SSHNameOptions{\n\t\t\t\t\tDNSDomains: []string{\"*.local\"},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tnewAdminSSHUserOptions := &policy.Options{\n\t\tSSH: &policy.SSHPolicyOptions{\n\t\t\tUser: &policy.SSHUserCertificateOptions{\n\t\t\t\tAllowedNames: &policy.SSHNameOptions{\n\t\t\t\t\tEmailAddresses: []string{\"@example.com\"},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tnewAdminOptions := &policy.Options{\n\t\tX509: &policy.X509PolicyOptions{\n\t\t\tAllowedNames: &policy.X509NameOptions{\n\t\t\t\tDNSDomains: []string{\"*.local\"},\n\t\t\t},\n\t\t\tDeniedNames: &policy.X509NameOptions{\n\t\t\t\tDNSDomains: []string{\"badhost.local\"},\n\t\t\t},\n\t\t\tAllowWildcardNames: true,\n\t\t},\n\t\tSSH: &policy.SSHPolicyOptions{\n\t\t\tHost: &policy.SSHHostCertificateOptions{\n\t\t\t\tAllowedNames: &policy.SSHNameOptions{\n\t\t\t\t\tDNSDomains: []string{\"*.local\"},\n\t\t\t\t},\n\t\t\t\tDeniedNames: &policy.SSHNameOptions{\n\t\t\t\t\tDNSDomains: []string{\"badhost.local\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tUser: &policy.SSHUserCertificateOptions{\n\t\t\t\tAllowedNames: &policy.SSHNameOptions{\n\t\t\t\t\tEmailAddresses: []string{\"@example.com\"},\n\t\t\t\t},\n\t\t\t\tDeniedNames: &policy.SSHNameOptions{\n\t\t\t\t\tEmailAddresses: []string{\"baduser@example.com\"},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\ttests := []struct {\n\t\tname     string\n\t\tconfig   *config.Config\n\t\tadminDB  admin.DB\n\t\tctx      context.Context\n\t\texpected *policy.Engine\n\t\twantErr  bool\n\t}{\n\t\t{\n\t\t\tname: \"fail/standalone-x509-policy\",\n\t\t\tconfig: &config.Config{\n\t\t\t\tAuthorityConfig: &config.AuthConfig{\n\t\t\t\t\tEnableAdmin: false,\n\t\t\t\t\tPolicy: &policy.Options{\n\t\t\t\t\t\tX509: &policy.X509PolicyOptions{\n\t\t\t\t\t\t\tAllowedNames: &policy.X509NameOptions{\n\t\t\t\t\t\t\t\tDNSDomains: []string{\"**.local\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tctx:      context.Background(),\n\t\t\twantErr:  true,\n\t\t\texpected: existingPolicyEngine,\n\t\t},\n\t\t{\n\t\t\tname: \"fail/standalone-ssh-host-policy\",\n\t\t\tconfig: &config.Config{\n\t\t\t\tAuthorityConfig: &config.AuthConfig{\n\t\t\t\t\tEnableAdmin: false,\n\t\t\t\t\tPolicy: &policy.Options{\n\t\t\t\t\t\tSSH: &policy.SSHPolicyOptions{\n\t\t\t\t\t\t\tHost: &policy.SSHHostCertificateOptions{\n\t\t\t\t\t\t\t\tAllowedNames: &policy.SSHNameOptions{\n\t\t\t\t\t\t\t\t\tDNSDomains: []string{\"**.local\"},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tctx:      context.Background(),\n\t\t\twantErr:  true,\n\t\t\texpected: existingPolicyEngine,\n\t\t},\n\t\t{\n\t\t\tname: \"fail/standalone-ssh-user-policy\",\n\t\t\tconfig: &config.Config{\n\t\t\t\tAuthorityConfig: &config.AuthConfig{\n\t\t\t\t\tEnableAdmin: false,\n\t\t\t\t\tPolicy: &policy.Options{\n\t\t\t\t\t\tSSH: &policy.SSHPolicyOptions{\n\t\t\t\t\t\t\tUser: &policy.SSHUserCertificateOptions{\n\t\t\t\t\t\t\t\tAllowedNames: &policy.SSHNameOptions{\n\t\t\t\t\t\t\t\t\tEmailAddresses: []string{\"**example.com\"},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tctx:      context.Background(),\n\t\t\twantErr:  true,\n\t\t\texpected: existingPolicyEngine,\n\t\t},\n\t\t{\n\t\t\tname: \"fail/adminDB.GetAuthorityPolicy-error\",\n\t\t\tconfig: &config.Config{\n\t\t\t\tAuthorityConfig: &config.AuthConfig{\n\t\t\t\t\tEnableAdmin: true,\n\t\t\t\t},\n\t\t\t},\n\t\t\tadminDB: &admin.MockDB{\n\t\t\t\tMockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) {\n\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t},\n\t\t\t},\n\t\t\tctx:      context.Background(),\n\t\t\twantErr:  true,\n\t\t\texpected: existingPolicyEngine,\n\t\t},\n\t\t{\n\t\t\tname: \"fail/admin-x509-policy\",\n\t\t\tconfig: &config.Config{\n\t\t\t\tAuthorityConfig: &config.AuthConfig{\n\t\t\t\t\tEnableAdmin: true,\n\t\t\t\t},\n\t\t\t},\n\t\t\tadminDB: &admin.MockDB{\n\t\t\t\tMockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) {\n\t\t\t\t\treturn &linkedca.Policy{\n\t\t\t\t\t\tX509: &linkedca.X509Policy{\n\t\t\t\t\t\t\tAllow: &linkedca.X509Names{\n\t\t\t\t\t\t\t\tDns: []string{\"**.local\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t}, nil\n\t\t\t\t},\n\t\t\t},\n\t\t\tctx:      context.Background(),\n\t\t\twantErr:  true,\n\t\t\texpected: existingPolicyEngine,\n\t\t},\n\t\t{\n\t\t\tname: \"fail/admin-ssh-host-policy\",\n\t\t\tconfig: &config.Config{\n\t\t\t\tAuthorityConfig: &config.AuthConfig{\n\t\t\t\t\tEnableAdmin: true,\n\t\t\t\t},\n\t\t\t},\n\t\t\tadminDB: &admin.MockDB{\n\t\t\t\tMockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) {\n\t\t\t\t\treturn &linkedca.Policy{\n\t\t\t\t\t\tSsh: &linkedca.SSHPolicy{\n\t\t\t\t\t\t\tHost: &linkedca.SSHHostPolicy{\n\t\t\t\t\t\t\t\tAllow: &linkedca.SSHHostNames{\n\t\t\t\t\t\t\t\t\tDns: []string{\"**.local\"},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t}, nil\n\t\t\t\t},\n\t\t\t},\n\t\t\tctx:      context.Background(),\n\t\t\twantErr:  true,\n\t\t\texpected: existingPolicyEngine,\n\t\t},\n\t\t{\n\t\t\tname: \"fail/admin-ssh-user-policy\",\n\t\t\tconfig: &config.Config{\n\t\t\t\tAuthorityConfig: &config.AuthConfig{\n\t\t\t\t\tEnableAdmin: true,\n\t\t\t\t},\n\t\t\t},\n\t\t\tadminDB: &admin.MockDB{\n\t\t\t\tMockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) {\n\t\t\t\t\treturn &linkedca.Policy{\n\t\t\t\t\t\tSsh: &linkedca.SSHPolicy{\n\t\t\t\t\t\t\tUser: &linkedca.SSHUserPolicy{\n\t\t\t\t\t\t\t\tAllow: &linkedca.SSHUserNames{\n\t\t\t\t\t\t\t\t\tEmails: []string{\"@@example.com\"},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t}, nil\n\t\t\t\t},\n\t\t\t},\n\t\t\tctx:      context.Background(),\n\t\t\twantErr:  true,\n\t\t\texpected: existingPolicyEngine,\n\t\t},\n\t\t{\n\t\t\tname: \"ok/linkedca-unsupported\",\n\t\t\tconfig: &config.Config{\n\t\t\t\tAuthorityConfig: &config.AuthConfig{\n\t\t\t\t\tEnableAdmin: true,\n\t\t\t\t},\n\t\t\t},\n\t\t\tadminDB:  &linkedCaClient{},\n\t\t\tctx:      context.Background(),\n\t\t\twantErr:  false,\n\t\t\texpected: existingPolicyEngine,\n\t\t},\n\t\t{\n\t\t\tname: \"ok/standalone-no-policy\",\n\t\t\tconfig: &config.Config{\n\t\t\t\tAuthorityConfig: &config.AuthConfig{\n\t\t\t\t\tEnableAdmin: false,\n\t\t\t\t\tPolicy:      nil,\n\t\t\t\t},\n\t\t\t},\n\t\t\tctx:      context.Background(),\n\t\t\twantErr:  false,\n\t\t\texpected: mustPolicyEngine(t, nil),\n\t\t},\n\t\t{\n\t\t\tname: \"ok/standalone-x509-policy\",\n\t\t\tconfig: &config.Config{\n\t\t\t\tAuthorityConfig: &config.AuthConfig{\n\t\t\t\t\tEnableAdmin: false,\n\t\t\t\t\tPolicy: &policy.Options{\n\t\t\t\t\t\tX509: &policy.X509PolicyOptions{\n\t\t\t\t\t\t\tAllowedNames: &policy.X509NameOptions{\n\t\t\t\t\t\t\t\tDNSDomains: []string{\"*.local\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tDeniedNames: &policy.X509NameOptions{\n\t\t\t\t\t\t\t\tDNSDomains: []string{\"badhost.local\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tAllowWildcardNames: true,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tctx:      context.Background(),\n\t\t\twantErr:  false,\n\t\t\texpected: mustPolicyEngine(t, newX509Options),\n\t\t},\n\t\t{\n\t\t\tname: \"ok/standalone-ssh-host-policy\",\n\t\t\tconfig: &config.Config{\n\t\t\t\tAuthorityConfig: &config.AuthConfig{\n\t\t\t\t\tEnableAdmin: false,\n\t\t\t\t\tPolicy: &policy.Options{\n\t\t\t\t\t\tSSH: &policy.SSHPolicyOptions{\n\t\t\t\t\t\t\tHost: &policy.SSHHostCertificateOptions{\n\t\t\t\t\t\t\t\tAllowedNames: &policy.SSHNameOptions{\n\t\t\t\t\t\t\t\t\tDNSDomains: []string{\"*.local\"},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tDeniedNames: &policy.SSHNameOptions{\n\t\t\t\t\t\t\t\t\tDNSDomains: []string{\"badhost.local\"},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tctx:      context.Background(),\n\t\t\twantErr:  false,\n\t\t\texpected: mustPolicyEngine(t, newSSHHostOptions),\n\t\t},\n\t\t{\n\t\t\tname: \"ok/standalone-ssh-user-policy\",\n\t\t\tconfig: &config.Config{\n\t\t\t\tAuthorityConfig: &config.AuthConfig{\n\t\t\t\t\tEnableAdmin: false,\n\t\t\t\t\tPolicy: &policy.Options{\n\t\t\t\t\t\tSSH: &policy.SSHPolicyOptions{\n\t\t\t\t\t\t\tUser: &policy.SSHUserCertificateOptions{\n\t\t\t\t\t\t\t\tAllowedNames: &policy.SSHNameOptions{\n\t\t\t\t\t\t\t\t\tPrincipals: []string{\"*\"},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tDeniedNames: &policy.SSHNameOptions{\n\t\t\t\t\t\t\t\t\tPrincipals: []string{\"root\"},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tctx:      context.Background(),\n\t\t\twantErr:  false,\n\t\t\texpected: mustPolicyEngine(t, newSSHUserOptions),\n\t\t},\n\t\t{\n\t\t\tname: \"ok/standalone-ssh-policy\",\n\t\t\tconfig: &config.Config{\n\t\t\t\tAuthorityConfig: &config.AuthConfig{\n\t\t\t\t\tEnableAdmin: false,\n\t\t\t\t\tPolicy: &policy.Options{\n\t\t\t\t\t\tSSH: &policy.SSHPolicyOptions{\n\t\t\t\t\t\t\tHost: &policy.SSHHostCertificateOptions{\n\t\t\t\t\t\t\t\tAllowedNames: &policy.SSHNameOptions{\n\t\t\t\t\t\t\t\t\tDNSDomains: []string{\"*.local\"},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tDeniedNames: &policy.SSHNameOptions{\n\t\t\t\t\t\t\t\t\tDNSDomains: []string{\"badhost.local\"},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tUser: &policy.SSHUserCertificateOptions{\n\t\t\t\t\t\t\t\tAllowedNames: &policy.SSHNameOptions{\n\t\t\t\t\t\t\t\t\tPrincipals: []string{\"*\"},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tDeniedNames: &policy.SSHNameOptions{\n\t\t\t\t\t\t\t\t\tPrincipals: []string{\"root\"},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tctx:      context.Background(),\n\t\t\twantErr:  false,\n\t\t\texpected: mustPolicyEngine(t, newSSHOptions),\n\t\t},\n\t\t{\n\t\t\tname: \"ok/standalone-full-policy\",\n\t\t\tconfig: &config.Config{\n\t\t\t\tAuthorityConfig: &config.AuthConfig{\n\t\t\t\t\tEnableAdmin: false,\n\t\t\t\t\tPolicy: &policy.Options{\n\t\t\t\t\t\tX509: &policy.X509PolicyOptions{\n\t\t\t\t\t\t\tAllowedNames: &policy.X509NameOptions{\n\t\t\t\t\t\t\t\tDNSDomains: []string{\"*.local\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tDeniedNames: &policy.X509NameOptions{\n\t\t\t\t\t\t\t\tDNSDomains: []string{\"badhost.local\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tAllowWildcardNames: true,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tSSH: &policy.SSHPolicyOptions{\n\t\t\t\t\t\t\tHost: &policy.SSHHostCertificateOptions{\n\t\t\t\t\t\t\t\tAllowedNames: &policy.SSHNameOptions{\n\t\t\t\t\t\t\t\t\tDNSDomains: []string{\"*.local\"},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tDeniedNames: &policy.SSHNameOptions{\n\t\t\t\t\t\t\t\t\tDNSDomains: []string{\"badhost.local\"},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tUser: &policy.SSHUserCertificateOptions{\n\t\t\t\t\t\t\t\tAllowedNames: &policy.SSHNameOptions{\n\t\t\t\t\t\t\t\t\tPrincipals: []string{\"*\"},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tDeniedNames: &policy.SSHNameOptions{\n\t\t\t\t\t\t\t\t\tPrincipals: []string{\"root\"},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tctx:      context.Background(),\n\t\t\twantErr:  false,\n\t\t\texpected: mustPolicyEngine(t, newOptions),\n\t\t},\n\t\t{\n\t\t\tname: \"ok/admin-x509-policy\",\n\t\t\tconfig: &config.Config{\n\t\t\t\tAuthorityConfig: &config.AuthConfig{\n\t\t\t\t\tEnableAdmin: true,\n\t\t\t\t},\n\t\t\t},\n\t\t\tadminDB: &admin.MockDB{\n\t\t\t\tMockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) {\n\t\t\t\t\treturn &linkedca.Policy{\n\t\t\t\t\t\tX509: &linkedca.X509Policy{\n\t\t\t\t\t\t\tAllow: &linkedca.X509Names{\n\t\t\t\t\t\t\t\tDns: []string{\"*.local\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t}, nil\n\t\t\t\t},\n\t\t\t},\n\t\t\tctx:      context.Background(),\n\t\t\twantErr:  false,\n\t\t\texpected: mustPolicyEngine(t, newAdminX509Options),\n\t\t},\n\t\t{\n\t\t\tname: \"ok/admin-ssh-host-policy\",\n\t\t\tconfig: &config.Config{\n\t\t\t\tAuthorityConfig: &config.AuthConfig{\n\t\t\t\t\tEnableAdmin: true,\n\t\t\t\t},\n\t\t\t},\n\t\t\tadminDB: &admin.MockDB{\n\t\t\t\tMockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) {\n\t\t\t\t\treturn &linkedca.Policy{\n\t\t\t\t\t\tSsh: &linkedca.SSHPolicy{\n\t\t\t\t\t\t\tHost: &linkedca.SSHHostPolicy{\n\t\t\t\t\t\t\t\tAllow: &linkedca.SSHHostNames{\n\t\t\t\t\t\t\t\t\tDns: []string{\"*.local\"},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t}, nil\n\t\t\t\t},\n\t\t\t},\n\t\t\tctx:      context.Background(),\n\t\t\twantErr:  false,\n\t\t\texpected: mustPolicyEngine(t, newAdminSSHHostOptions),\n\t\t},\n\t\t{\n\t\t\tname: \"ok/admin-ssh-user-policy\",\n\t\t\tconfig: &config.Config{\n\t\t\t\tAuthorityConfig: &config.AuthConfig{\n\t\t\t\t\tEnableAdmin: true,\n\t\t\t\t},\n\t\t\t},\n\t\t\tadminDB: &admin.MockDB{\n\t\t\t\tMockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) {\n\t\t\t\t\treturn &linkedca.Policy{\n\t\t\t\t\t\tSsh: &linkedca.SSHPolicy{\n\t\t\t\t\t\t\tUser: &linkedca.SSHUserPolicy{\n\t\t\t\t\t\t\t\tAllow: &linkedca.SSHUserNames{\n\t\t\t\t\t\t\t\t\tEmails: []string{\"@example.com\"},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t}, nil\n\t\t\t\t},\n\t\t\t},\n\t\t\tctx:      context.Background(),\n\t\t\twantErr:  false,\n\t\t\texpected: mustPolicyEngine(t, newAdminSSHUserOptions),\n\t\t},\n\t\t{\n\t\t\tname: \"ok/admin-full-policy\",\n\t\t\tconfig: &config.Config{\n\t\t\t\tAuthorityConfig: &config.AuthConfig{\n\t\t\t\t\tEnableAdmin: true,\n\t\t\t\t},\n\t\t\t},\n\t\t\tctx: context.Background(),\n\t\t\tadminDB: &admin.MockDB{\n\t\t\t\tMockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) {\n\t\t\t\t\treturn &linkedca.Policy{\n\t\t\t\t\t\tX509: &linkedca.X509Policy{\n\t\t\t\t\t\t\tAllow: &linkedca.X509Names{\n\t\t\t\t\t\t\t\tDns: []string{\"*.local\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tDeny: &linkedca.X509Names{\n\t\t\t\t\t\t\t\tDns: []string{\"badhost.local\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tAllowWildcardNames: true,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tSsh: &linkedca.SSHPolicy{\n\t\t\t\t\t\t\tHost: &linkedca.SSHHostPolicy{\n\t\t\t\t\t\t\t\tAllow: &linkedca.SSHHostNames{\n\t\t\t\t\t\t\t\t\tDns: []string{\"*.local\"},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tDeny: &linkedca.SSHHostNames{\n\t\t\t\t\t\t\t\t\tDns: []string{\"badhost.local\"},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tUser: &linkedca.SSHUserPolicy{\n\t\t\t\t\t\t\t\tAllow: &linkedca.SSHUserNames{\n\t\t\t\t\t\t\t\t\tEmails: []string{\"@example.com\"},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tDeny: &linkedca.SSHUserNames{\n\t\t\t\t\t\t\t\t\tEmails: []string{\"baduser@example.com\"},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t}, nil\n\t\t\t\t},\n\t\t\t},\n\t\t\twantErr:  false,\n\t\t\texpected: mustPolicyEngine(t, newAdminOptions),\n\t\t},\n\t\t{\n\t\t\t// both DB and JSON config; DB config is taken if Admin API is enabled\n\t\t\tname: \"ok/admin-over-standalone\",\n\t\t\tconfig: &config.Config{\n\t\t\t\tAuthorityConfig: &config.AuthConfig{\n\t\t\t\t\tEnableAdmin: true,\n\t\t\t\t\tPolicy: &policy.Options{\n\t\t\t\t\t\tSSH: &policy.SSHPolicyOptions{\n\t\t\t\t\t\t\tHost: &policy.SSHHostCertificateOptions{\n\t\t\t\t\t\t\t\tAllowedNames: &policy.SSHNameOptions{\n\t\t\t\t\t\t\t\t\tDNSDomains: []string{\"*.local\"},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tDeniedNames: &policy.SSHNameOptions{\n\t\t\t\t\t\t\t\t\tDNSDomains: []string{\"badhost.local\"},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tUser: &policy.SSHUserCertificateOptions{\n\t\t\t\t\t\t\t\tAllowedNames: &policy.SSHNameOptions{\n\t\t\t\t\t\t\t\t\tPrincipals: []string{\"*\"},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tDeniedNames: &policy.SSHNameOptions{\n\t\t\t\t\t\t\t\t\tPrincipals: []string{\"root\"},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tctx: context.Background(),\n\t\t\tadminDB: &admin.MockDB{\n\t\t\t\tMockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) {\n\t\t\t\t\treturn &linkedca.Policy{\n\t\t\t\t\t\tX509: &linkedca.X509Policy{\n\t\t\t\t\t\t\tAllow: &linkedca.X509Names{\n\t\t\t\t\t\t\t\tDns: []string{\"*.local\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tDeny: &linkedca.X509Names{\n\t\t\t\t\t\t\t\tDns: []string{\"badhost.local\"},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tAllowWildcardNames: true,\n\t\t\t\t\t\t},\n\t\t\t\t\t}, nil\n\t\t\t\t},\n\t\t\t},\n\t\t\twantErr:  false,\n\t\t\texpected: mustPolicyEngine(t, newX509Options),\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ta := &Authority{\n\t\t\t\tconfig:       tt.config,\n\t\t\t\tadminDB:      tt.adminDB,\n\t\t\t\tpolicyEngine: existingPolicyEngine,\n\t\t\t}\n\t\t\tif err := a.reloadPolicyEngines(tt.ctx); (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"Authority.reloadPolicyEngines() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\n\t\t\tassert.Equal(t, tt.expected, a.policyEngine)\n\t\t})\n\t}\n}\n\nfunc TestAuthority_checkAuthorityPolicy(t *testing.T) {\n\ttype fields struct {\n\t\tprovisioners *provisioner.Collection\n\t\tadmins       *administrator.Collection\n\t\tdb           db.AuthDB\n\t\tadminDB      admin.DB\n\t}\n\ttype args struct {\n\t\tctx          context.Context\n\t\tcurrentAdmin *linkedca.Admin\n\t\tprovName     string\n\t\tp            *linkedca.Policy\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tfields  fields\n\t\targs    args\n\t\twantErr bool\n\t}{\n\t\t{\n\t\t\tname:   \"no policy\",\n\t\t\tfields: fields{},\n\t\t\targs: args{\n\t\t\t\tcurrentAdmin: nil,\n\t\t\t\tprovName:     \"prov\",\n\t\t\t\tp:            nil,\n\t\t\t},\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"fail/adminDB.GetAdmins-error\",\n\t\t\tfields: fields{\n\t\t\t\tadmins: administrator.NewCollection(nil),\n\t\t\t\tadminDB: &admin.MockDB{\n\t\t\t\t\tMockGetAdmins: func(ctx context.Context) ([]*linkedca.Admin, error) {\n\t\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\targs: args{\n\t\t\t\tcurrentAdmin: &linkedca.Admin{Subject: \"step\"},\n\t\t\t\tprovName:     \"prov\",\n\t\t\t\tp: &linkedca.Policy{\n\t\t\t\t\tX509: &linkedca.X509Policy{\n\t\t\t\t\t\tAllow: &linkedca.X509Names{\n\t\t\t\t\t\t\tDns: []string{\"step\", \"otherAdmin\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twantErr: true,\n\t\t},\n\t\t{\n\t\t\tname: \"fail/policy\",\n\t\t\tfields: fields{\n\t\t\t\tadmins: administrator.NewCollection(nil),\n\t\t\t\tadminDB: &admin.MockDB{\n\t\t\t\t\tMockGetAdmins: func(ctx context.Context) ([]*linkedca.Admin, error) {\n\t\t\t\t\t\treturn []*linkedca.Admin{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tId:      \"adminID1\",\n\t\t\t\t\t\t\t\tSubject: \"anotherAdmin\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tId:      \"adminID2\",\n\t\t\t\t\t\t\t\tSubject: \"step\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tId:      \"adminID3\",\n\t\t\t\t\t\t\t\tSubject: \"otherAdmin\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t}, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\targs: args{\n\t\t\t\tcurrentAdmin: &linkedca.Admin{Subject: \"step\"},\n\t\t\t\tprovName:     \"prov\",\n\t\t\t\tp: &linkedca.Policy{\n\t\t\t\t\tX509: &linkedca.X509Policy{\n\t\t\t\t\t\tAllow: &linkedca.X509Names{\n\t\t\t\t\t\t\tDns: []string{\"step\", \"otherAdmin\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twantErr: true,\n\t\t},\n\t\t{\n\t\t\tname: \"ok\",\n\t\t\tfields: fields{\n\t\t\t\tadmins: administrator.NewCollection(nil),\n\t\t\t\tadminDB: &admin.MockDB{\n\t\t\t\t\tMockGetAdmins: func(ctx context.Context) ([]*linkedca.Admin, error) {\n\t\t\t\t\t\treturn []*linkedca.Admin{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tId:      \"adminID2\",\n\t\t\t\t\t\t\t\tSubject: \"step\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tId:      \"adminID3\",\n\t\t\t\t\t\t\t\tSubject: \"otherAdmin\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t}, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\targs: args{\n\t\t\t\tcurrentAdmin: &linkedca.Admin{Subject: \"step\"},\n\t\t\t\tprovName:     \"prov\",\n\t\t\t\tp: &linkedca.Policy{\n\t\t\t\t\tX509: &linkedca.X509Policy{\n\t\t\t\t\t\tAllow: &linkedca.X509Names{\n\t\t\t\t\t\t\tDns: []string{\"step\", \"otherAdmin\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twantErr: false,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ta := &Authority{\n\t\t\t\tprovisioners: tt.fields.provisioners,\n\t\t\t\tadmins:       tt.fields.admins,\n\t\t\t\tdb:           tt.fields.db,\n\t\t\t\tadminDB:      tt.fields.adminDB,\n\t\t\t}\n\t\t\tif err := a.checkAuthorityPolicy(tt.args.ctx, tt.args.currentAdmin, tt.args.p); (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"Authority.checkProvisionerPolicy() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestAuthority_checkProvisionerPolicy(t *testing.T) {\n\tjwkProvisioner := &provisioner.JWK{\n\t\tID:   \"jwkID\",\n\t\tType: \"JWK\",\n\t\tName: \"jwkProv\",\n\t\tKey:  &jose.JSONWebKey{KeyID: \"jwkKeyID\"},\n\t}\n\tprovisioners := provisioner.NewCollection(testAudiences)\n\tprovisioners.Store(jwkProvisioner)\n\tadmins := administrator.NewCollection(provisioners)\n\tadmins.Store(&linkedca.Admin{\n\t\tId:            \"adminID\",\n\t\tSubject:       \"step\",\n\t\tProvisionerId: \"jwkID\",\n\t}, jwkProvisioner)\n\ttype fields struct {\n\t\tprovisioners *provisioner.Collection\n\t\tadmins       *administrator.Collection\n\t\tdb           db.AuthDB\n\t\tadminDB      admin.DB\n\t}\n\ttype args struct {\n\t\tctx      context.Context\n\t\tprovName string\n\t\tp        *linkedca.Policy\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tfields  fields\n\t\targs    args\n\t\twantErr bool\n\t}{\n\t\t{\n\t\t\tname:   \"no policy\",\n\t\t\tfields: fields{},\n\t\t\targs: args{\n\t\t\t\tprovName: \"prov\",\n\t\t\t\tp:        nil,\n\t\t\t},\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"fail/policy\",\n\t\t\tfields: fields{\n\t\t\t\tprovisioners: provisioners,\n\t\t\t\tadmins:       admins,\n\t\t\t},\n\t\t\targs: args{\n\t\t\t\tprovName: \"jwkProv\",\n\t\t\t\tp: &linkedca.Policy{\n\t\t\t\t\tX509: &linkedca.X509Policy{\n\t\t\t\t\t\tAllow: &linkedca.X509Names{\n\t\t\t\t\t\t\tDns: []string{\"otherAdmin\"}, // step not in policy\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twantErr: true,\n\t\t},\n\t\t{\n\t\t\tname: \"ok\",\n\t\t\tfields: fields{\n\t\t\t\tprovisioners: provisioners,\n\t\t\t\tadmins:       admins,\n\t\t\t},\n\t\t\targs: args{\n\t\t\t\tprovName: \"jwkProv\",\n\t\t\t\tp: &linkedca.Policy{\n\t\t\t\t\tX509: &linkedca.X509Policy{\n\t\t\t\t\t\tAllow: &linkedca.X509Names{\n\t\t\t\t\t\t\tDns: []string{\"step\", \"otherAdmin\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twantErr: false,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ta := &Authority{\n\t\t\t\tprovisioners: tt.fields.provisioners,\n\t\t\t\tadmins:       tt.fields.admins,\n\t\t\t\tdb:           tt.fields.db,\n\t\t\t\tadminDB:      tt.fields.adminDB,\n\t\t\t}\n\t\t\tif err := a.checkProvisionerPolicy(tt.args.ctx, tt.args.provName, tt.args.p); (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"Authority.checkProvisionerPolicy() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestAuthority_RemoveAuthorityPolicy(t *testing.T) {\n\ttype fields struct {\n\t\tconfig  *config.Config\n\t\tdb      db.AuthDB\n\t\tadminDB admin.DB\n\t}\n\ttype args struct {\n\t\tctx context.Context\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tfields  fields\n\t\targs    args\n\t\twantErr *PolicyError\n\t}{\n\t\t{\n\t\t\tname: \"fail/adminDB.DeleteAuthorityPolicy\",\n\t\t\tfields: fields{\n\t\t\t\tconfig: &config.Config{\n\t\t\t\t\tAuthorityConfig: &config.AuthConfig{\n\t\t\t\t\t\tEnableAdmin: true,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tadminDB: &admin.MockDB{\n\t\t\t\t\tMockDeleteAuthorityPolicy: func(ctx context.Context) error {\n\t\t\t\t\t\treturn errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twantErr: &PolicyError{\n\t\t\t\tTyp: StoreFailure,\n\t\t\t\tErr: errors.New(\"force\"),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"fail/a.reloadPolicyEngines\",\n\t\t\tfields: fields{\n\t\t\t\tconfig: &config.Config{\n\t\t\t\t\tAuthorityConfig: &config.AuthConfig{\n\t\t\t\t\t\tEnableAdmin: true,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tadminDB: &admin.MockDB{\n\t\t\t\t\tMockDeleteAuthorityPolicy: func(ctx context.Context) error {\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t\tMockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) {\n\t\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twantErr: &PolicyError{\n\t\t\t\tTyp: ReloadFailure,\n\t\t\t\tErr: errors.New(\"error reloading policy engines when deleting authority policy: error getting policy to (re)load policy engines: force\"),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"ok\",\n\t\t\tfields: fields{\n\t\t\t\tconfig: &config.Config{\n\t\t\t\t\tAuthorityConfig: &config.AuthConfig{\n\t\t\t\t\t\tEnableAdmin: true,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tadminDB: &admin.MockDB{\n\t\t\t\t\tMockDeleteAuthorityPolicy: func(ctx context.Context) error {\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t\tMockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) {\n\t\t\t\t\t\treturn nil, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ta := &Authority{\n\t\t\t\tconfig:  tt.fields.config,\n\t\t\t\tdb:      tt.fields.db,\n\t\t\t\tadminDB: tt.fields.adminDB,\n\t\t\t}\n\t\t\terr := a.RemoveAuthorityPolicy(tt.args.ctx)\n\t\t\tif err != nil {\n\t\t\t\tvar pe *PolicyError\n\t\t\t\tif assert.True(t, errors.As(err, &pe)) {\n\t\t\t\t\tassert.Equal(t, tt.wantErr.Typ, pe.Typ)\n\t\t\t\t\tassert.Equal(t, tt.wantErr.Err.Error(), pe.Err.Error())\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestAuthority_GetAuthorityPolicy(t *testing.T) {\n\ttype fields struct {\n\t\tconfig  *config.Config\n\t\tdb      db.AuthDB\n\t\tadminDB admin.DB\n\t}\n\ttype args struct {\n\t\tctx context.Context\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tfields  fields\n\t\targs    args\n\t\twant    *linkedca.Policy\n\t\twantErr *PolicyError\n\t}{\n\t\t{\n\t\t\tname: \"fail/adminDB.GetAuthorityPolicy\",\n\t\t\tfields: fields{\n\t\t\t\tconfig: &config.Config{\n\t\t\t\t\tAuthorityConfig: &config.AuthConfig{\n\t\t\t\t\t\tEnableAdmin: true,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tadminDB: &admin.MockDB{\n\t\t\t\t\tMockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) {\n\t\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twantErr: &PolicyError{\n\t\t\t\tTyp: InternalFailure,\n\t\t\t\tErr: errors.New(\"force\"),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"ok\",\n\t\t\tfields: fields{\n\t\t\t\tconfig: &config.Config{\n\t\t\t\t\tAuthorityConfig: &config.AuthConfig{\n\t\t\t\t\t\tEnableAdmin: true,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tadminDB: &admin.MockDB{\n\t\t\t\t\tMockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) {\n\t\t\t\t\t\treturn &linkedca.Policy{}, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: &linkedca.Policy{},\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ta := &Authority{\n\t\t\t\tconfig:  tt.fields.config,\n\t\t\t\tdb:      tt.fields.db,\n\t\t\t\tadminDB: tt.fields.adminDB,\n\t\t\t}\n\t\t\tgot, err := a.GetAuthorityPolicy(tt.args.ctx)\n\t\t\tif err != nil {\n\t\t\t\tvar pe *PolicyError\n\t\t\t\tif assert.True(t, errors.As(err, &pe)) {\n\t\t\t\t\tassert.Equal(t, tt.wantErr.Typ, pe.Typ)\n\t\t\t\t\tassert.Equal(t, tt.wantErr.Err.Error(), pe.Err.Error())\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"Authority.GetAuthorityPolicy() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestAuthority_CreateAuthorityPolicy(t *testing.T) {\n\ttype fields struct {\n\t\tconfig  *config.Config\n\t\tdb      db.AuthDB\n\t\tadminDB admin.DB\n\t}\n\ttype args struct {\n\t\tctx context.Context\n\t\tadm *linkedca.Admin\n\t\tp   *linkedca.Policy\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tfields  fields\n\t\targs    args\n\t\twant    *linkedca.Policy\n\t\twantErr *PolicyError\n\t}{\n\t\t{\n\t\t\tname: \"fail/a.checkAuthorityPolicy\",\n\t\t\tfields: fields{\n\t\t\t\tconfig: &config.Config{\n\t\t\t\t\tAuthorityConfig: &config.AuthConfig{\n\t\t\t\t\t\tEnableAdmin: true,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tadminDB: &admin.MockDB{\n\t\t\t\t\tMockGetAdmins: func(ctx context.Context) ([]*linkedca.Admin, error) {\n\t\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\targs: args{\n\t\t\t\tctx: context.Background(),\n\t\t\t\tadm: &linkedca.Admin{Subject: \"step\"},\n\t\t\t\tp: &linkedca.Policy{\n\t\t\t\t\tX509: &linkedca.X509Policy{\n\t\t\t\t\t\tAllow: &linkedca.X509Names{\n\t\t\t\t\t\t\tDns: []string{\"step\", \"otherAdmin\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twantErr: &PolicyError{\n\t\t\t\tTyp: InternalFailure,\n\t\t\t\tErr: errors.New(\"error retrieving admins: force\"),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"fail/adminDB.CreateAuthorityPolicy\",\n\t\t\tfields: fields{\n\t\t\t\tconfig: &config.Config{\n\t\t\t\t\tAuthorityConfig: &config.AuthConfig{\n\t\t\t\t\t\tEnableAdmin: true,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tadminDB: &admin.MockDB{\n\t\t\t\t\tMockGetAdmins: func(ctx context.Context) ([]*linkedca.Admin, error) {\n\t\t\t\t\t\treturn []*linkedca.Admin{}, nil\n\t\t\t\t\t},\n\t\t\t\t\tMockCreateAuthorityPolicy: func(ctx context.Context, policy *linkedca.Policy) error {\n\t\t\t\t\t\treturn errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\targs: args{\n\t\t\t\tctx: context.Background(),\n\t\t\t\tadm: &linkedca.Admin{Subject: \"step\"},\n\t\t\t\tp: &linkedca.Policy{\n\t\t\t\t\tX509: &linkedca.X509Policy{\n\t\t\t\t\t\tAllow: &linkedca.X509Names{\n\t\t\t\t\t\t\tDns: []string{\"step\", \"otherAdmin\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twantErr: &PolicyError{\n\t\t\t\tTyp: StoreFailure,\n\t\t\t\tErr: errors.New(\"force\"),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"fail/a.reloadPolicyEngines\",\n\t\t\tfields: fields{\n\t\t\t\tconfig: &config.Config{\n\t\t\t\t\tAuthorityConfig: &config.AuthConfig{\n\t\t\t\t\t\tEnableAdmin: true,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tadminDB: &admin.MockDB{\n\t\t\t\t\tMockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) {\n\t\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t\tMockGetAdmins: func(ctx context.Context) ([]*linkedca.Admin, error) {\n\t\t\t\t\t\treturn []*linkedca.Admin{}, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\targs: args{\n\t\t\t\tctx: context.Background(),\n\t\t\t\tadm: &linkedca.Admin{Subject: \"step\"},\n\t\t\t\tp: &linkedca.Policy{\n\t\t\t\t\tX509: &linkedca.X509Policy{\n\t\t\t\t\t\tAllow: &linkedca.X509Names{\n\t\t\t\t\t\t\tDns: []string{\"step\", \"otherAdmin\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twantErr: &PolicyError{\n\t\t\t\tTyp: ReloadFailure,\n\t\t\t\tErr: errors.New(\"error reloading policy engines when creating authority policy: error getting policy to (re)load policy engines: force\"),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"ok\",\n\t\t\tfields: fields{\n\t\t\t\tconfig: &config.Config{\n\t\t\t\t\tAuthorityConfig: &config.AuthConfig{\n\t\t\t\t\t\tEnableAdmin: true,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tadminDB: &admin.MockDB{\n\t\t\t\t\tMockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) {\n\t\t\t\t\t\treturn &linkedca.Policy{\n\t\t\t\t\t\t\tX509: &linkedca.X509Policy{\n\t\t\t\t\t\t\t\tAllow: &linkedca.X509Names{\n\t\t\t\t\t\t\t\t\tDns: []string{\"step\", \"otherAdmin\"},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t}, nil\n\t\t\t\t\t},\n\t\t\t\t\tMockGetAdmins: func(ctx context.Context) ([]*linkedca.Admin, error) {\n\t\t\t\t\t\treturn []*linkedca.Admin{}, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\targs: args{\n\t\t\t\tctx: context.Background(),\n\t\t\t\tadm: &linkedca.Admin{Subject: \"step\"},\n\t\t\t\tp: &linkedca.Policy{\n\t\t\t\t\tX509: &linkedca.X509Policy{\n\t\t\t\t\t\tAllow: &linkedca.X509Names{\n\t\t\t\t\t\t\tDns: []string{\"step\", \"otherAdmin\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: &linkedca.Policy{\n\t\t\t\tX509: &linkedca.X509Policy{\n\t\t\t\t\tAllow: &linkedca.X509Names{\n\t\t\t\t\t\tDns: []string{\"step\", \"otherAdmin\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ta := &Authority{\n\t\t\t\tconfig:  tt.fields.config,\n\t\t\t\tdb:      tt.fields.db,\n\t\t\t\tadminDB: tt.fields.adminDB,\n\t\t\t}\n\t\t\tgot, err := a.CreateAuthorityPolicy(tt.args.ctx, tt.args.adm, tt.args.p)\n\t\t\tif err != nil {\n\t\t\t\tvar pe *PolicyError\n\t\t\t\tif assert.True(t, errors.As(err, &pe)) {\n\t\t\t\t\tassert.Equal(t, tt.wantErr.Typ, pe.Typ)\n\t\t\t\t\tassert.Equal(t, tt.wantErr.Err.Error(), pe.Err.Error())\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"Authority.CreateAuthorityPolicy() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestAuthority_UpdateAuthorityPolicy(t *testing.T) {\n\ttype fields struct {\n\t\tconfig  *config.Config\n\t\tdb      db.AuthDB\n\t\tadminDB admin.DB\n\t}\n\ttype args struct {\n\t\tctx context.Context\n\t\tadm *linkedca.Admin\n\t\tp   *linkedca.Policy\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tfields  fields\n\t\targs    args\n\t\twant    *linkedca.Policy\n\t\twantErr *PolicyError\n\t}{\n\t\t{\n\t\t\tname: \"fail/a.checkAuthorityPolicy\",\n\t\t\tfields: fields{\n\t\t\t\tconfig: &config.Config{\n\t\t\t\t\tAuthorityConfig: &config.AuthConfig{\n\t\t\t\t\t\tEnableAdmin: true,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tadminDB: &admin.MockDB{\n\t\t\t\t\tMockGetAdmins: func(ctx context.Context) ([]*linkedca.Admin, error) {\n\t\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\targs: args{\n\t\t\t\tctx: context.Background(),\n\t\t\t\tadm: &linkedca.Admin{Subject: \"step\"},\n\t\t\t\tp: &linkedca.Policy{\n\t\t\t\t\tX509: &linkedca.X509Policy{\n\t\t\t\t\t\tAllow: &linkedca.X509Names{\n\t\t\t\t\t\t\tDns: []string{\"step\", \"otherAdmin\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twantErr: &PolicyError{\n\t\t\t\tTyp: InternalFailure,\n\t\t\t\tErr: errors.New(\"error retrieving admins: force\"),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"fail/adminDB.UpdateAuthorityPolicy\",\n\t\t\tfields: fields{\n\t\t\t\tconfig: &config.Config{\n\t\t\t\t\tAuthorityConfig: &config.AuthConfig{\n\t\t\t\t\t\tEnableAdmin: true,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tadminDB: &admin.MockDB{\n\t\t\t\t\tMockGetAdmins: func(ctx context.Context) ([]*linkedca.Admin, error) {\n\t\t\t\t\t\treturn []*linkedca.Admin{}, nil\n\t\t\t\t\t},\n\t\t\t\t\tMockUpdateAuthorityPolicy: func(ctx context.Context, policy *linkedca.Policy) error {\n\t\t\t\t\t\treturn errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\targs: args{\n\t\t\t\tctx: context.Background(),\n\t\t\t\tadm: &linkedca.Admin{Subject: \"step\"},\n\t\t\t\tp: &linkedca.Policy{\n\t\t\t\t\tX509: &linkedca.X509Policy{\n\t\t\t\t\t\tAllow: &linkedca.X509Names{\n\t\t\t\t\t\t\tDns: []string{\"step\", \"otherAdmin\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twantErr: &PolicyError{\n\t\t\t\tTyp: StoreFailure,\n\t\t\t\tErr: errors.New(\"force\"),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"fail/a.reloadPolicyEngines\",\n\t\t\tfields: fields{\n\t\t\t\tconfig: &config.Config{\n\t\t\t\t\tAuthorityConfig: &config.AuthConfig{\n\t\t\t\t\t\tEnableAdmin: true,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tadminDB: &admin.MockDB{\n\t\t\t\t\tMockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) {\n\t\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t\tMockGetAdmins: func(ctx context.Context) ([]*linkedca.Admin, error) {\n\t\t\t\t\t\treturn []*linkedca.Admin{}, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\targs: args{\n\t\t\t\tctx: context.Background(),\n\t\t\t\tadm: &linkedca.Admin{Subject: \"step\"},\n\t\t\t\tp: &linkedca.Policy{\n\t\t\t\t\tX509: &linkedca.X509Policy{\n\t\t\t\t\t\tAllow: &linkedca.X509Names{\n\t\t\t\t\t\t\tDns: []string{\"step\", \"otherAdmin\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twantErr: &PolicyError{\n\t\t\t\tTyp: ReloadFailure,\n\t\t\t\tErr: errors.New(\"error reloading policy engines when updating authority policy: error getting policy to (re)load policy engines: force\"),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"ok\",\n\t\t\tfields: fields{\n\t\t\t\tconfig: &config.Config{\n\t\t\t\t\tAuthorityConfig: &config.AuthConfig{\n\t\t\t\t\t\tEnableAdmin: true,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tadminDB: &admin.MockDB{\n\t\t\t\t\tMockGetAuthorityPolicy: func(ctx context.Context) (*linkedca.Policy, error) {\n\t\t\t\t\t\treturn &linkedca.Policy{\n\t\t\t\t\t\t\tX509: &linkedca.X509Policy{\n\t\t\t\t\t\t\t\tAllow: &linkedca.X509Names{\n\t\t\t\t\t\t\t\t\tDns: []string{\"step\", \"otherAdmin\"},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t}, nil\n\t\t\t\t\t},\n\t\t\t\t\tMockUpdateAuthorityPolicy: func(ctx context.Context, policy *linkedca.Policy) error {\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t},\n\t\t\t\t\tMockGetAdmins: func(ctx context.Context) ([]*linkedca.Admin, error) {\n\t\t\t\t\t\treturn []*linkedca.Admin{}, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\targs: args{\n\t\t\t\tctx: context.Background(),\n\t\t\t\tadm: &linkedca.Admin{Subject: \"step\"},\n\t\t\t\tp: &linkedca.Policy{\n\t\t\t\t\tX509: &linkedca.X509Policy{\n\t\t\t\t\t\tAllow: &linkedca.X509Names{\n\t\t\t\t\t\t\tDns: []string{\"step\", \"otherAdmin\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: &linkedca.Policy{\n\t\t\t\tX509: &linkedca.X509Policy{\n\t\t\t\t\tAllow: &linkedca.X509Names{\n\t\t\t\t\t\tDns: []string{\"step\", \"otherAdmin\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ta := &Authority{\n\t\t\t\tconfig:  tt.fields.config,\n\t\t\t\tdb:      tt.fields.db,\n\t\t\t\tadminDB: tt.fields.adminDB,\n\t\t\t}\n\t\t\tgot, err := a.UpdateAuthorityPolicy(tt.args.ctx, tt.args.adm, tt.args.p)\n\t\t\tif err != nil {\n\t\t\t\tvar pe *PolicyError\n\t\t\t\tif assert.True(t, errors.As(err, &pe)) {\n\t\t\t\t\tassert.Equal(t, tt.wantErr.Typ, pe.Typ)\n\t\t\t\t\tassert.Equal(t, tt.wantErr.Err.Error(), pe.Err.Error())\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"Authority.UpdateAuthorityPolicy() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "authority/poolhttp/poolhttp.go",
    "content": "package poolhttp\n\nimport (\n\t\"net/http\"\n\t\"sync\"\n\n\t\"github.com/smallstep/certificates/internal/httptransport\"\n)\n\n// Transporter is implemented by custom HTTP clients with a method that\n// returns an [*http.Transport].\ntype Transporter interface {\n\tTransport() *http.Transport\n}\n\n// Client is an HTTP client that uses a [sync.Pool] to create new and reuse HTTP\n// clients. It implements the [provisioner.HTTPClient] and [Transporter]\n// interfaces. This is the HTTP client used by the provisioners.\ntype Client struct {\n\trw   sync.RWMutex\n\tpool sync.Pool\n}\n\n// New creates a new poolhttp [Client], the [sync.Pool] will initialize a new\n// [*http.Client] with the given function.\nfunc New(fn func() *http.Client) *Client {\n\treturn &Client{\n\t\tpool: sync.Pool{\n\t\t\tNew: func() any { return fn() },\n\t\t},\n\t}\n}\n\n// SetNew replaces the inner pool with a new [sync.Pool] with the given New\n// function. This method can be use concurrently with other methods of this\n// package.\nfunc (c *Client) SetNew(fn func() *http.Client) {\n\tc.rw.Lock()\n\tc.pool = sync.Pool{\n\t\tNew: func() any { return fn() },\n\t}\n\tc.rw.Unlock()\n}\n\n// getClient gets a client from the pool.\nfunc (c *Client) getClient() *http.Client {\n\tc.rw.RLock()\n\tdefer c.rw.RUnlock()\n\tif hc, ok := c.pool.Get().(*http.Client); ok && hc != nil {\n\t\treturn hc\n\t}\n\treturn nil\n}\n\n// Get issues a GET request to the specified URL. If the response is one of the\n// following redirect codes, Get follows the redirect after calling the\n// [Client.CheckRedirect] function:\nfunc (c *Client) Get(u string) (resp *http.Response, err error) {\n\tif hc := c.getClient(); hc != nil {\n\t\tresp, err = hc.Get(u)\n\t\tc.pool.Put(hc)\n\t} else {\n\t\tresp, err = http.DefaultClient.Get(u)\n\t}\n\n\treturn\n}\n\n// Do sends an HTTP request and returns an HTTP response, following policy (such\n// as redirects, cookies, auth) as configured on the client.\nfunc (c *Client) Do(req *http.Request) (resp *http.Response, err error) {\n\tif hc := c.getClient(); hc != nil {\n\t\tresp, err = hc.Do(req) //nolint:gosec // intentional HTTP request to configured endpoint\n\t\tc.pool.Put(hc)\n\t} else {\n\t\tresp, err = http.DefaultClient.Do(req) //nolint:gosec // intentional HTTP request to configured endpoint\n\t}\n\n\treturn\n}\n\n// Transport() returns a clone of the http.Client Transport or returns the\n// default transport.\nfunc (c *Client) Transport() *http.Transport {\n\tif hc := c.getClient(); hc != nil {\n\t\ttr, ok := hc.Transport.(*http.Transport)\n\t\tc.pool.Put(hc)\n\t\tif ok {\n\t\t\treturn tr.Clone()\n\t\t}\n\t}\n\n\treturn httptransport.New()\n}\n"
  },
  {
    "path": "authority/poolhttp/poolhttp_test.go",
    "content": "package poolhttp\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"strconv\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc requireBody(t *testing.T, want string, r io.ReadCloser) {\n\tt.Helper()\n\tt.Cleanup(func() {\n\t\trequire.NoError(t, r.Close())\n\t})\n\n\tb, err := io.ReadAll(r)\n\trequire.NoError(t, err)\n\trequire.Equal(t, want, string(b))\n}\n\nfunc TestClient(t *testing.T) {\n\thttpSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tfmt.Fprintln(w, \"Hello World\")\n\t}))\n\tt.Cleanup(httpSrv.Close)\n\ttlsSrv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tfmt.Fprintln(w, \"Hello World\")\n\t}))\n\tt.Cleanup(tlsSrv.Close)\n\n\ttests := []struct {\n\t\tname   string\n\t\tclient *Client\n\t\tsrv    *httptest.Server\n\t}{\n\t\t{\"http\", New(func() *http.Client { return httpSrv.Client() }), httpSrv},\n\t\t{\"tls\", New(func() *http.Client { return tlsSrv.Client() }), tlsSrv},\n\t\t{\"nil\", New(func() *http.Client { return nil }), httpSrv},\n\t\t{\"empty\", &Client{}, httpSrv},\n\t}\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tresp, err := tc.client.Get(tc.srv.URL)\n\t\t\trequire.NoError(t, err)\n\t\t\trequireBody(t, \"Hello World\\n\", resp.Body)\n\n\t\t\treq, err := http.NewRequest(\"GET\", tc.srv.URL, http.NoBody)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tresp, err = tc.client.Do(req)\n\t\t\trequire.NoError(t, err)\n\t\t\trequireBody(t, \"Hello World\\n\", resp.Body)\n\n\t\t\tclient := &http.Client{\n\t\t\t\tTransport: tc.client.Transport(),\n\t\t\t}\n\t\t\tresp, err = client.Get(tc.srv.URL)\n\t\t\trequire.NoError(t, err)\n\t\t\trequireBody(t, \"Hello World\\n\", resp.Body)\n\t\t})\n\t}\n}\n\nfunc TestClient_SetNew(t *testing.T) {\n\tsrv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tfmt.Fprintln(w, \"Hello World\")\n\t}))\n\tt.Cleanup(srv.Close)\n\n\tc := New(func() *http.Client {\n\t\treturn srv.Client()\n\t})\n\n\ttests := []struct {\n\t\tname      string\n\t\tclient    *http.Client\n\t\tassertion assert.ErrorAssertionFunc\n\t}{\n\t\t{\"ok\", srv.Client(), assert.NoError},\n\t\t{\"fail\", http.DefaultClient, assert.Error},\n\t\t{\"ok again\", srv.Client(), assert.NoError},\n\t}\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tc.SetNew(func() *http.Client {\n\t\t\t\treturn tc.client\n\t\t\t})\n\t\t\t_, err := c.Get(srv.URL)\n\t\t\ttc.assertion(t, err)\n\n\t\t})\n\t}\n}\n\nfunc TestClient_parallel(t *testing.T) {\n\tt.Parallel()\n\n\tsrv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tfmt.Fprintln(w, \"Hello World\")\n\t}))\n\tt.Cleanup(srv.Close)\n\n\tc := New(func() *http.Client {\n\t\treturn srv.Client()\n\t})\n\treq, err := http.NewRequest(\"GET\", srv.URL, http.NoBody)\n\trequire.NoError(t, err)\n\n\tfor i := range 10 {\n\t\tt.Run(strconv.Itoa(i), func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tresp, err := c.Get(srv.URL)\n\t\t\trequire.NoError(t, err)\n\t\t\trequireBody(t, \"Hello World\\n\", resp.Body)\n\n\t\t\tresp, err = c.Do(req)\n\t\t\trequire.NoError(t, err)\n\t\t\trequireBody(t, \"Hello World\\n\", resp.Body)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "authority/provisioner/acme.go",
    "content": "package provisioner\n\nimport (\n\t\"context\"\n\t\"crypto/x509\"\n\t\"encoding/pem\"\n\t\"fmt\"\n\t\"net\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n\t\"github.com/smallstep/certificates/acme/wire\"\n\t\"github.com/smallstep/linkedca\"\n)\n\n// ACMEChallenge represents the supported acme challenges.\ntype ACMEChallenge string\n\n//nolint:staticcheck,revive // better names\nconst (\n\t// HTTP_01 is the http-01 ACME challenge.\n\tHTTP_01 ACMEChallenge = \"http-01\"\n\t// DNS_01 is the dns-01 ACME challenge.\n\tDNS_01 ACMEChallenge = \"dns-01\"\n\t// TLS_ALPN_01 is the tls-alpn-01 ACME challenge.\n\tTLS_ALPN_01 ACMEChallenge = \"tls-alpn-01\"\n\t// DEVICE_ATTEST_01 is the device-attest-01 ACME challenge.\n\tDEVICE_ATTEST_01 ACMEChallenge = \"device-attest-01\"\n\t// WIREOIDC_01 is the Wire OIDC challenge.\n\tWIREOIDC_01 ACMEChallenge = \"wire-oidc-01\"\n\t// WIREDPOP_01 is the Wire DPoP challenge.\n\tWIREDPOP_01 ACMEChallenge = \"wire-dpop-01\"\n)\n\n// String returns a normalized version of the challenge.\nfunc (c ACMEChallenge) String() string {\n\treturn strings.ToLower(string(c))\n}\n\n// Validate returns an error if the acme challenge is not a valid one.\nfunc (c ACMEChallenge) Validate() error {\n\tswitch ACMEChallenge(c.String()) {\n\tcase HTTP_01, DNS_01, TLS_ALPN_01, DEVICE_ATTEST_01, WIREOIDC_01, WIREDPOP_01:\n\t\treturn nil\n\tdefault:\n\t\treturn fmt.Errorf(\"acme challenge %q is not supported\", c)\n\t}\n}\n\n// ACMEAttestationFormat represents the format used on a device-attest-01\n// challenge.\ntype ACMEAttestationFormat string\n\nconst (\n\t// APPLE is the format used to enable device-attest-01 on Apple devices.\n\tAPPLE ACMEAttestationFormat = \"apple\"\n\n\t// STEP is the format used to enable device-attest-01 on devices that\n\t// provide attestation certificates like the PIV interface on YubiKeys.\n\t//\n\t// TODO(mariano): should we rename this to something else.\n\tSTEP ACMEAttestationFormat = \"step\"\n\n\t// TPM is the format used to enable device-attest-01 with TPMs.\n\tTPM ACMEAttestationFormat = \"tpm\"\n)\n\n// String returns a normalized version of the attestation format.\nfunc (f ACMEAttestationFormat) String() string {\n\treturn strings.ToLower(string(f))\n}\n\n// Validate returns an error if the attestation format is not a valid one.\nfunc (f ACMEAttestationFormat) Validate() error {\n\tswitch ACMEAttestationFormat(f.String()) {\n\tcase APPLE, STEP, TPM:\n\t\treturn nil\n\tdefault:\n\t\treturn fmt.Errorf(\"acme attestation format %q is not supported\", f)\n\t}\n}\n\n// ACME is the acme provisioner type, an entity that can authorize the ACME\n// provisioning flow.\ntype ACME struct {\n\t*base\n\tID      string `json:\"-\"`\n\tType    string `json:\"type\"`\n\tName    string `json:\"name\"`\n\tForceCN bool   `json:\"forceCN,omitempty\"`\n\t// TermsOfService contains a URL pointing to the ACME server's\n\t// terms of service. Defaults to empty.\n\tTermsOfService string `json:\"termsOfService,omitempty\"`\n\t// Website contains an URL pointing to more information about\n\t// the ACME server. Defaults to empty.\n\tWebsite string `json:\"website,omitempty\"`\n\t// CaaIdentities is an array of hostnames that the ACME server\n\t// identifies itself with. These hostnames can be used by ACME\n\t// clients to determine the correct issuer domain name to use\n\t// when configuring CAA records. Defaults to empty array.\n\tCaaIdentities []string `json:\"caaIdentities,omitempty\"`\n\t// RequireEAB makes the provisioner require ACME EAB to be provided\n\t// by clients when creating a new Account. If set to true, the provided\n\t// EAB will be verified. If set to false and an EAB is provided, it is\n\t// not verified. Defaults to false.\n\tRequireEAB bool `json:\"requireEAB,omitempty\"`\n\t// Challenges contains the enabled challenges for this provisioner. If this\n\t// value is not set the default http-01, dns-01 and tls-alpn-01 challenges\n\t// will be enabled, device-attest-01, wire-oidc-01 and wire-dpop-01 will be\n\t// disabled.\n\tChallenges []ACMEChallenge `json:\"challenges,omitempty\"`\n\t// AttestationFormats contains the enabled attestation formats for this\n\t// provisioner. If this value is not set the default apple, step and tpm\n\t// will be used.\n\tAttestationFormats []ACMEAttestationFormat `json:\"attestationFormats,omitempty\"`\n\t// AttestationRoots contains a bundle of root certificates in PEM format\n\t// that will be used to verify the attestation certificates. If provided,\n\t// this bundle will be used even for well-known CAs like Apple and Yubico.\n\tAttestationRoots    []byte   `json:\"attestationRoots,omitempty\"`\n\tClaims              *Claims  `json:\"claims,omitempty\"`\n\tOptions             *Options `json:\"options,omitempty\"`\n\tattestationRootPool *x509.CertPool\n\tctl                 *Controller\n}\n\n// GetID returns the provisioner unique identifier.\nfunc (p ACME) GetID() string {\n\tif p.ID != \"\" {\n\t\treturn p.ID\n\t}\n\treturn p.GetIDForToken()\n}\n\n// GetIDForToken returns an identifier that will be used to load the provisioner\n// from a token.\nfunc (p *ACME) GetIDForToken() string {\n\treturn \"acme/\" + p.Name\n}\n\n// GetTokenID returns the identifier of the token. This provisioner will always\n// return [ErrTokenFlowNotSupported].\nfunc (p *ACME) GetTokenID(string) (string, error) {\n\treturn \"\", ErrTokenFlowNotSupported\n}\n\n// GetName returns the name of the provisioner.\nfunc (p *ACME) GetName() string {\n\treturn p.Name\n}\n\n// GetType returns the type of provisioner.\nfunc (p *ACME) GetType() Type {\n\treturn TypeACME\n}\n\n// GetEncryptedKey returns the base provisioner encrypted key if it's defined.\nfunc (p *ACME) GetEncryptedKey() (string, string, bool) {\n\treturn \"\", \"\", false\n}\n\n// GetOptions returns the configured provisioner options.\nfunc (p *ACME) GetOptions() *Options {\n\treturn p.Options\n}\n\n// DefaultTLSCertDuration returns the default TLS cert duration enforced by\n// the provisioner.\nfunc (p *ACME) DefaultTLSCertDuration() time.Duration {\n\treturn p.ctl.Claimer.DefaultTLSCertDuration()\n}\n\n// Init initializes and validates the fields of an ACME type.\nfunc (p *ACME) Init(config Config) (err error) {\n\tswitch {\n\tcase p.Type == \"\":\n\t\treturn errors.New(\"provisioner type cannot be empty\")\n\tcase p.Name == \"\":\n\t\treturn errors.New(\"provisioner name cannot be empty\")\n\t}\n\n\tfor _, c := range p.Challenges {\n\t\tif err := c.Validate(); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tfor _, f := range p.AttestationFormats {\n\t\tif err := f.Validate(); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\t// Parse attestation roots.\n\t// The pool will be nil if there are no roots.\n\tif rest := p.AttestationRoots; len(rest) > 0 {\n\t\tvar block *pem.Block\n\t\tvar hasCert bool\n\t\tp.attestationRootPool = x509.NewCertPool()\n\t\tfor rest != nil {\n\t\t\tblock, rest = pem.Decode(rest)\n\t\t\tif block == nil {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tcert, err := x509.ParseCertificate(block.Bytes)\n\t\t\tif err != nil {\n\t\t\t\treturn errors.New(\"error parsing attestationRoots: malformed certificate\")\n\t\t\t}\n\t\t\tp.attestationRootPool.AddCert(cert)\n\t\t\thasCert = true\n\t\t}\n\t\tif !hasCert {\n\t\t\treturn errors.New(\"error parsing attestationRoots: no certificates found\")\n\t\t}\n\t}\n\n\tif err := p.initializeWireOptions(); err != nil {\n\t\treturn fmt.Errorf(\"failed initializing Wire options: %w\", err)\n\t}\n\n\tp.ctl, err = NewController(p, p.Claims, config, p.Options)\n\treturn\n}\n\n// initializeWireOptions initializes the options for the ACME Wire\n// integration. It'll return early if no Wire challenge types are\n// enabled.\nfunc (p *ACME) initializeWireOptions() error {\n\thasWireChallenges := false\n\tfor _, c := range p.Challenges {\n\t\tif c == WIREOIDC_01 || c == WIREDPOP_01 {\n\t\t\thasWireChallenges = true\n\t\t\tbreak\n\t\t}\n\t}\n\tif !hasWireChallenges {\n\t\treturn nil\n\t}\n\n\tw, err := p.GetOptions().GetWireOptions()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed getting Wire options: %w\", err)\n\t}\n\n\tif err := w.Validate(); err != nil {\n\t\treturn fmt.Errorf(\"failed validating Wire options: %w\", err)\n\t}\n\n\t// at this point the Wire options have been validated, and (mostly)\n\t// initialized. Remote keys will be loaded upon the first verification,\n\t// currently.\n\t// TODO(hs): can/should we \"prime\" the underlying remote keyset, to verify\n\t// auto discovery works as expected? Because of the current way provisioners\n\t// are initialized, doing that as part of the initialization isn't the best\n\t// time to do it, because it could result in operations not resulting in the\n\t// expected result in all cases.\n\n\treturn nil\n}\n\n// ACMEIdentifierType encodes ACME Identifier types\ntype ACMEIdentifierType string\n\nconst (\n\t// IP is the ACME ip identifier type\n\tIP ACMEIdentifierType = \"ip\"\n\t// DNS is the ACME dns identifier type\n\tDNS ACMEIdentifierType = \"dns\"\n\t// WireUser is the Wire user identifier type\n\tWireUser ACMEIdentifierType = \"wireapp-user\"\n\t// WireDevice is the Wire device identifier type\n\tWireDevice ACMEIdentifierType = \"wireapp-device\"\n)\n\n// ACMEIdentifier encodes ACME Order Identifiers\ntype ACMEIdentifier struct {\n\tType  ACMEIdentifierType\n\tValue string\n}\n\n// AuthorizeOrderIdentifier verifies the provisioner is allowed to issue a\n// certificate for an ACME Order Identifier.\nfunc (p *ACME) AuthorizeOrderIdentifier(_ context.Context, identifier ACMEIdentifier) error {\n\tx509Policy := p.ctl.getPolicy().getX509()\n\n\t// identifier is allowed if no policy is configured\n\tif x509Policy == nil {\n\t\treturn nil\n\t}\n\n\t// assuming only valid identifiers (IP or DNS) are provided\n\tvar err error\n\tswitch identifier.Type {\n\tcase IP:\n\t\terr = x509Policy.IsIPAllowed(net.ParseIP(identifier.Value))\n\tcase DNS:\n\t\terr = x509Policy.IsDNSAllowed(identifier.Value)\n\tcase WireUser:\n\t\tvar wireID wire.UserID\n\t\tif wireID, err = wire.ParseUserID(identifier.Value); err != nil {\n\t\t\treturn fmt.Errorf(\"failed parsing Wire SANs: %w\", err)\n\t\t}\n\t\terr = x509Policy.AreSANsAllowed([]string{wireID.Handle})\n\tcase WireDevice:\n\t\tvar wireID wire.DeviceID\n\t\tif wireID, err = wire.ParseDeviceID(identifier.Value); err != nil {\n\t\t\treturn fmt.Errorf(\"failed parsing Wire SANs: %w\", err)\n\t\t}\n\t\terr = x509Policy.AreSANsAllowed([]string{wireID.ClientID})\n\tdefault:\n\t\terr = fmt.Errorf(\"invalid ACME identifier type '%s' provided\", identifier.Type)\n\t}\n\n\treturn err\n}\n\n// AuthorizeSign does not do any validation, because all validation is handled\n// in the ACME protocol. This method returns a list of modifiers / constraints\n// on the resulting certificate.\nfunc (p *ACME) AuthorizeSign(context.Context, string) ([]SignOption, error) {\n\topts := []SignOption{\n\t\tp,\n\t\t// modifiers / withOptions\n\t\tnewProvisionerExtensionOption(TypeACME, p.Name, \"\").WithControllerOptions(p.ctl),\n\t\tnewForceCNOption(p.ForceCN),\n\t\tprofileDefaultDuration(p.ctl.Claimer.DefaultTLSCertDuration()),\n\t\t// validators\n\t\tdefaultPublicKeyValidator{},\n\t\tnewValidityValidator(p.ctl.Claimer.MinTLSCertDuration(), p.ctl.Claimer.MaxTLSCertDuration()),\n\t\tnewX509NamePolicyValidator(p.ctl.getPolicy().getX509()),\n\t\tp.ctl.newWebhookController(nil, linkedca.Webhook_X509),\n\t}\n\n\treturn opts, nil\n}\n\n// AuthorizeRevoke is called just before the certificate is to be revoked by\n// the CA. It can be used to authorize revocation of a certificate. With the\n// ACME protocol, revocation authorization is specified and performed as part\n// of the client/server interaction, so this is a no-op.\nfunc (p *ACME) AuthorizeRevoke(context.Context, string) error {\n\treturn nil\n}\n\n// AuthorizeRenew returns an error if the renewal is disabled.\n// NOTE: This method does not actually validate the certificate or check its\n// revocation status. Just confirms that the provisioner that created the\n// certificate was configured to allow renewals.\nfunc (p *ACME) AuthorizeRenew(ctx context.Context, cert *x509.Certificate) error {\n\treturn p.ctl.AuthorizeRenew(ctx, cert)\n}\n\n// IsChallengeEnabled checks if the given challenge is enabled. By default\n// http-01, dns-01 and tls-alpn-01 are enabled, to disable any of them the\n// Challenge provisioner property should have at least one element.\nfunc (p *ACME) IsChallengeEnabled(_ context.Context, challenge ACMEChallenge) bool {\n\tenabledChallenges := []ACMEChallenge{\n\t\tHTTP_01, DNS_01, TLS_ALPN_01,\n\t}\n\tif len(p.Challenges) > 0 {\n\t\tenabledChallenges = p.Challenges\n\t}\n\tfor _, ch := range enabledChallenges {\n\t\tif strings.EqualFold(string(ch), string(challenge)) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// IsAttestationFormatEnabled checks if the given attestation format is enabled.\n// By default apple, step and tpm are enabled, to disable any of them the\n// AttestationFormat provisioner property should have at least one element.\nfunc (p *ACME) IsAttestationFormatEnabled(_ context.Context, format ACMEAttestationFormat) bool {\n\tenabledFormats := []ACMEAttestationFormat{\n\t\tAPPLE, STEP, TPM,\n\t}\n\tif len(p.AttestationFormats) > 0 {\n\t\tenabledFormats = p.AttestationFormats\n\t}\n\tfor _, f := range enabledFormats {\n\t\tif strings.EqualFold(string(f), string(format)) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// GetAttestationRoots returns certificate pool with the configured attestation\n// roots and reports if the pool contains at least one certificate.\n//\n// TODO(hs): we may not want to expose the root pool like this; call into an\n// interface function instead to authorize?\nfunc (p *ACME) GetAttestationRoots() (*x509.CertPool, bool) {\n\treturn p.attestationRootPool, p.attestationRootPool != nil\n}\n"
  },
  {
    "path": "authority/provisioner/acme_118_test.go",
    "content": "//go:build go1.18\n\npackage provisioner\n\nimport (\n\t\"bytes\"\n\t\"crypto/x509\"\n\t\"os\"\n\t\"testing\"\n)\n\nfunc TestACME_GetAttestationRoots(t *testing.T) {\n\tappleCA, err := os.ReadFile(\"testdata/certs/apple-att-ca.crt\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tyubicoCA, err := os.ReadFile(\"testdata/certs/yubico-piv-ca.crt\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tpool := x509.NewCertPool()\n\tpool.AppendCertsFromPEM(appleCA)\n\tpool.AppendCertsFromPEM(yubicoCA)\n\n\ttype fields struct {\n\t\tType             string\n\t\tName             string\n\t\tAttestationRoots []byte\n\t}\n\ttests := []struct {\n\t\tname   string\n\t\tfields fields\n\t\twant   *x509.CertPool\n\t\twant1  bool\n\t}{\n\t\t{\"ok\", fields{\"ACME\", \"acme\", bytes.Join([][]byte{appleCA, yubicoCA}, []byte(\"\\n\"))}, pool, true},\n\t\t{\"nil\", fields{\"ACME\", \"acme\", nil}, nil, false},\n\t\t{\"empty\", fields{\"ACME\", \"acme\", []byte{}}, nil, false},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tp := &ACME{\n\t\t\t\tType:             tt.fields.Type,\n\t\t\t\tName:             tt.fields.Name,\n\t\t\t\tAttestationRoots: tt.fields.AttestationRoots,\n\t\t\t}\n\t\t\tif err := p.Init(Config{\n\t\t\t\tClaims:    globalProvisionerClaims,\n\t\t\t\tAudiences: testAudiences,\n\t\t\t}); err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\tgot, got1 := p.GetAttestationRoots()\n\t\t\tswitch {\n\t\t\tcase tt.want == nil && got == nil:\n\t\t\t\tbreak\n\t\t\tcase tt.want == nil && got != nil, tt.want != nil && got == nil:\n\t\t\t\tt.Errorf(\"ACME.GetAttestationRoots() got = %v, want %v\", got, tt.want)\n\t\t\tdefault:\n\t\t\t\t//nolint:staticcheck // this file only runs in go1.18\n\t\t\t\tgotSubjects := got.Subjects()\n\t\t\t\t//nolint:staticcheck // this file only runs in go1.18\n\t\t\t\twantSubjects := tt.want.Subjects()\n\t\t\t\tif len(gotSubjects) != len(wantSubjects) {\n\t\t\t\t\tt.Errorf(\"ACME.GetAttestationRoots() got = %v, want %v\", got, tt.want)\n\t\t\t\t} else {\n\t\t\t\t\tfor i, gotSub := range gotSubjects {\n\t\t\t\t\t\tif !bytes.Equal(gotSub, wantSubjects[i]) {\n\t\t\t\t\t\t\tt.Errorf(\"ACME.GetAttestationRoots() got = %v, want %v\", got, tt.want)\n\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tif got1 != tt.want1 {\n\t\t\t\tt.Errorf(\"ACME.GetAttestationRoots() got1 = %v, want %v\", got1, tt.want1)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "authority/provisioner/acme_119_test.go",
    "content": "//go:build !go1.18\n\npackage provisioner\n\nimport (\n\t\"bytes\"\n\t\"crypto/x509\"\n\t\"os\"\n\t\"testing\"\n)\n\nfunc TestACME_GetAttestationRoots(t *testing.T) {\n\tappleCA, err := os.ReadFile(\"testdata/certs/apple-att-ca.crt\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tyubicoCA, err := os.ReadFile(\"testdata/certs/yubico-piv-ca.crt\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tpool := x509.NewCertPool()\n\tpool.AppendCertsFromPEM(appleCA)\n\tpool.AppendCertsFromPEM(yubicoCA)\n\n\ttype fields struct {\n\t\tType             string\n\t\tName             string\n\t\tAttestationRoots []byte\n\t}\n\ttests := []struct {\n\t\tname   string\n\t\tfields fields\n\t\twant   *x509.CertPool\n\t\twant1  bool\n\t}{\n\t\t{\"ok\", fields{\"ACME\", \"acme\", bytes.Join([][]byte{appleCA, yubicoCA}, []byte(\"\\n\"))}, pool, true},\n\t\t{\"nil\", fields{\"ACME\", \"acme\", nil}, nil, false},\n\t\t{\"empty\", fields{\"ACME\", \"acme\", []byte{}}, nil, false},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tp := &ACME{\n\t\t\t\tType:             tt.fields.Type,\n\t\t\t\tName:             tt.fields.Name,\n\t\t\t\tAttestationRoots: tt.fields.AttestationRoots,\n\t\t\t}\n\t\t\tif err := p.Init(Config{\n\t\t\t\tClaims:    globalProvisionerClaims,\n\t\t\t\tAudiences: testAudiences,\n\t\t\t}); err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\tgot, got1 := p.GetAttestationRoots()\n\t\t\tif tt.want == nil && got != nil {\n\t\t\t\tt.Errorf(\"ACME.GetAttestationRoots() got = %v, want %v\", got, tt.want)\n\t\t\t} else if !tt.want.Equal(got) {\n\t\t\t\tt.Errorf(\"ACME.GetAttestationRoots() got = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t\tif got1 != tt.want1 {\n\t\t\t\tt.Errorf(\"ACME.GetAttestationRoots() got1 = %v, want %v\", got1, tt.want1)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "authority/provisioner/acme_test.go",
    "content": "package provisioner\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/x509\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"os\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/smallstep/certificates/api/render\"\n\t\"github.com/smallstep/certificates/authority/provisioner/wire\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestACMEChallenge_Validate(t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\tc       ACMEChallenge\n\t\twantErr bool\n\t}{\n\t\t{\"http-01\", HTTP_01, false},\n\t\t{\"dns-01\", DNS_01, false},\n\t\t{\"tls-alpn-01\", TLS_ALPN_01, false},\n\t\t{\"device-attest-01\", DEVICE_ATTEST_01, false},\n\t\t{\"wire-oidc-01\", DEVICE_ATTEST_01, false},\n\t\t{\"wire-dpop-01\", DEVICE_ATTEST_01, false},\n\t\t{\"uppercase\", \"HTTP-01\", false},\n\t\t{\"fail\", \"http-02\", true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\terr := tt.c.Validate()\n\t\t\tif tt.wantErr {\n\t\t\t\tassert.Error(t, err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.NoError(t, err)\n\t\t})\n\t}\n}\n\nfunc TestACMEAttestationFormat_Validate(t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\tf       ACMEAttestationFormat\n\t\twantErr bool\n\t}{\n\t\t{\"apple\", APPLE, false},\n\t\t{\"step\", STEP, false},\n\t\t{\"tpm\", TPM, false},\n\t\t{\"uppercase\", \"APPLE\", false},\n\t\t{\"fail\", \"FOO\", true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\terr := tt.f.Validate()\n\t\t\tif tt.wantErr {\n\t\t\t\tassert.Error(t, err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.NoError(t, err)\n\t\t})\n\t}\n}\n\nfunc TestACME_Getters(t *testing.T) {\n\tp, err := generateACME()\n\trequire.NoError(t, err)\n\tid := \"acme/test@acme-provisioner.com\"\n\tassert.Equal(t, id, p.GetID())\n\tassert.Equal(t, \"test@acme-provisioner.com\", p.GetName())\n\tassert.Equal(t, TypeACME, p.GetType())\n\tkid, key, ok := p.GetEncryptedKey()\n\tif kid != \"\" || key != \"\" || ok == true {\n\t\tt.Errorf(\"ACME.GetEncryptedKey() = (%v, %v, %v), want (%v, %v, %v)\",\n\t\t\tkid, key, ok, \"\", \"\", false)\n\t}\n\ttokenID, err := p.GetTokenID(\"token\")\n\tassert.Empty(t, tokenID)\n\tassert.Equal(t, ErrTokenFlowNotSupported, err)\n}\n\nfunc TestACME_Init(t *testing.T) {\n\tappleCA, err := os.ReadFile(\"testdata/certs/apple-att-ca.crt\")\n\trequire.NoError(t, err)\n\tyubicoCA, err := os.ReadFile(\"testdata/certs/yubico-piv-ca.crt\")\n\trequire.NoError(t, err)\n\tfakeWireDPoPKey := []byte(`-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEA5c+4NKZSNQcR1T8qN6SjwgdPZQ0Ge12Ylx/YeGAJ35k=\n-----END PUBLIC KEY-----`)\n\n\ttype ProvisionerValidateTest struct {\n\t\tp   *ACME\n\t\terr error\n\t}\n\ttests := map[string]func(*testing.T) ProvisionerValidateTest{\n\t\t\"fail/empty\": func(t *testing.T) ProvisionerValidateTest {\n\t\t\treturn ProvisionerValidateTest{\n\t\t\t\tp:   &ACME{},\n\t\t\t\terr: errors.New(\"provisioner type cannot be empty\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/empty-name\": func(t *testing.T) ProvisionerValidateTest {\n\t\t\treturn ProvisionerValidateTest{\n\t\t\t\tp: &ACME{\n\t\t\t\t\tType: \"ACME\",\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"provisioner name cannot be empty\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/empty-type\": func(t *testing.T) ProvisionerValidateTest {\n\t\t\treturn ProvisionerValidateTest{\n\t\t\t\tp:   &ACME{Name: \"foo\"},\n\t\t\t\terr: errors.New(\"provisioner type cannot be empty\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/bad-claims\": func(t *testing.T) ProvisionerValidateTest {\n\t\t\treturn ProvisionerValidateTest{\n\t\t\t\tp:   &ACME{Name: \"foo\", Type: \"ACME\", Claims: &Claims{DefaultTLSDur: &Duration{0}}},\n\t\t\t\terr: errors.New(\"claims: MinTLSCertDuration must be greater than 0\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/bad-challenge\": func(t *testing.T) ProvisionerValidateTest {\n\t\t\treturn ProvisionerValidateTest{\n\t\t\t\tp:   &ACME{Name: \"foo\", Type: \"ACME\", Challenges: []ACMEChallenge{HTTP_01, \"zar\"}},\n\t\t\t\terr: errors.New(\"acme challenge \\\"zar\\\" is not supported\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/bad-attestation-format\": func(t *testing.T) ProvisionerValidateTest {\n\t\t\treturn ProvisionerValidateTest{\n\t\t\t\tp:   &ACME{Name: \"foo\", Type: \"ACME\", AttestationFormats: []ACMEAttestationFormat{APPLE, \"zar\"}},\n\t\t\t\terr: errors.New(\"acme attestation format \\\"zar\\\" is not supported\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/parse-attestation-roots\": func(t *testing.T) ProvisionerValidateTest {\n\t\t\treturn ProvisionerValidateTest{\n\t\t\t\tp:   &ACME{Name: \"foo\", Type: \"ACME\", AttestationRoots: []byte(\"-----BEGIN CERTIFICATE-----\\nZm9v\\n-----END CERTIFICATE-----\")},\n\t\t\t\terr: errors.New(\"error parsing attestationRoots: malformed certificate\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/empty-attestation-roots\": func(t *testing.T) ProvisionerValidateTest {\n\t\t\treturn ProvisionerValidateTest{\n\t\t\t\tp:   &ACME{Name: \"foo\", Type: \"ACME\", AttestationRoots: []byte(\"\\n\")},\n\t\t\t\terr: errors.New(\"error parsing attestationRoots: no certificates found\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/wire-missing-options\": func(t *testing.T) ProvisionerValidateTest {\n\t\t\treturn ProvisionerValidateTest{\n\t\t\t\tp: &ACME{\n\t\t\t\t\tName:       \"foo\",\n\t\t\t\t\tType:       \"ACME\",\n\t\t\t\t\tChallenges: []ACMEChallenge{WIREOIDC_01, WIREDPOP_01},\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"failed initializing Wire options: failed getting Wire options: no options available\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/wire-missing-wire-options\": func(t *testing.T) ProvisionerValidateTest {\n\t\t\treturn ProvisionerValidateTest{\n\t\t\t\tp: &ACME{\n\t\t\t\t\tName:       \"foo\",\n\t\t\t\t\tType:       \"ACME\",\n\t\t\t\t\tChallenges: []ACMEChallenge{WIREOIDC_01, WIREDPOP_01},\n\t\t\t\t\tOptions:    &Options{},\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"failed initializing Wire options: failed getting Wire options: no Wire options available\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/wire-validate-options\": func(t *testing.T) ProvisionerValidateTest {\n\t\t\treturn ProvisionerValidateTest{\n\t\t\t\tp: &ACME{\n\t\t\t\t\tName:       \"foo\",\n\t\t\t\t\tType:       \"ACME\",\n\t\t\t\t\tChallenges: []ACMEChallenge{WIREOIDC_01, WIREDPOP_01},\n\t\t\t\t\tOptions: &Options{\n\t\t\t\t\t\tWire: &wire.Options{\n\t\t\t\t\t\t\tOIDC: &wire.OIDCOptions{},\n\t\t\t\t\t\t\tDPOP: &wire.DPOPOptions{\n\t\t\t\t\t\t\t\tSigningKey: fakeWireDPoPKey,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"failed initializing Wire options: failed validating Wire options: failed initializing OIDC options: provider not set\"),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) ProvisionerValidateTest {\n\t\t\treturn ProvisionerValidateTest{\n\t\t\t\tp: &ACME{Name: \"foo\", Type: \"ACME\"},\n\t\t\t}\n\t\t},\n\t\t\"ok/attestation\": func(t *testing.T) ProvisionerValidateTest {\n\t\t\treturn ProvisionerValidateTest{\n\t\t\t\tp: &ACME{\n\t\t\t\t\tName:               \"foo\",\n\t\t\t\t\tType:               \"ACME\",\n\t\t\t\t\tChallenges:         []ACMEChallenge{DNS_01, DEVICE_ATTEST_01},\n\t\t\t\t\tAttestationFormats: []ACMEAttestationFormat{APPLE, STEP},\n\t\t\t\t\tAttestationRoots:   bytes.Join([][]byte{appleCA, yubicoCA}, []byte(\"\\n\")),\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"ok/wire\": func(t *testing.T) ProvisionerValidateTest {\n\t\t\treturn ProvisionerValidateTest{\n\t\t\t\tp: &ACME{\n\t\t\t\t\tName:       \"foo\",\n\t\t\t\t\tType:       \"ACME\",\n\t\t\t\t\tChallenges: []ACMEChallenge{WIREOIDC_01, WIREDPOP_01},\n\t\t\t\t\tOptions: &Options{\n\t\t\t\t\t\tWire: &wire.Options{\n\t\t\t\t\t\t\tOIDC: &wire.OIDCOptions{\n\t\t\t\t\t\t\t\tProvider: &wire.Provider{\n\t\t\t\t\t\t\t\t\tIssuerURL: \"https://issuer.example.com\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tDPOP: &wire.DPOPOptions{\n\t\t\t\t\t\t\t\tSigningKey: fakeWireDPoPKey,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t}\n\tconfig := Config{\n\t\tClaims:    globalProvisionerClaims,\n\t\tAudiences: testAudiences,\n\t}\n\tfor name, get := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ttc := get(t)\n\t\t\tt.Log(string(tc.p.AttestationRoots))\n\t\t\terr := tc.p.Init(config)\n\t\t\tif tc.err != nil {\n\t\t\t\tassert.EqualError(t, err, tc.err.Error())\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.NoError(t, err)\n\t\t})\n\t}\n}\n\nfunc TestACME_AuthorizeRenew(t *testing.T) {\n\tnow := time.Now().Truncate(time.Second)\n\ttype test struct {\n\t\tp    *ACME\n\t\tcert *x509.Certificate\n\t\terr  error\n\t\tcode int\n\t}\n\ttests := map[string]func(*testing.T) test{\n\t\t\"fail/renew-disabled\": func(t *testing.T) test {\n\t\t\tp, err := generateACME()\n\t\t\trequire.NoError(t, err)\n\t\t\t// disable renewal\n\t\t\tdisable := true\n\t\t\tp.Claims = &Claims{DisableRenewal: &disable}\n\t\t\tp.ctl.Claimer, err = NewClaimer(p.Claims, globalProvisionerClaims)\n\t\t\trequire.NoError(t, err)\n\t\t\treturn test{\n\t\t\t\tp: p,\n\t\t\t\tcert: &x509.Certificate{\n\t\t\t\t\tNotBefore: now,\n\t\t\t\t\tNotAfter:  now.Add(time.Hour),\n\t\t\t\t},\n\t\t\t\tcode: http.StatusUnauthorized,\n\t\t\t\terr:  fmt.Errorf(\"renew is disabled for provisioner '%s'\", p.GetName()),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tp, err := generateACME()\n\t\t\trequire.NoError(t, err)\n\t\t\treturn test{\n\t\t\t\tp: p,\n\t\t\t\tcert: &x509.Certificate{\n\t\t\t\t\tNotBefore: now,\n\t\t\t\t\tNotAfter:  now.Add(time.Hour),\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t}\n\tfor name, tt := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ttc := tt(t)\n\t\t\terr := tc.p.AuthorizeRenew(context.Background(), tc.cert)\n\t\t\tif tc.err != nil {\n\t\t\t\tif assert.Implements(t, (*render.StatusCodedError)(nil), err) {\n\t\t\t\t\tvar sc render.StatusCodedError\n\t\t\t\t\tif errors.As(err, &sc) {\n\t\t\t\t\t\tassert.Equal(t, tc.code, sc.StatusCode())\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tassert.EqualError(t, err, tc.err.Error())\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.NoError(t, err)\n\t\t})\n\t}\n}\n\nfunc TestACME_AuthorizeSign(t *testing.T) {\n\ttype test struct {\n\t\tp     *ACME\n\t\ttoken string\n\t\tcode  int\n\t\terr   error\n\t}\n\ttests := map[string]func(*testing.T) test{\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tp, err := generateACME()\n\t\t\trequire.NoError(t, err)\n\t\t\treturn test{\n\t\t\t\tp:     p,\n\t\t\t\ttoken: \"foo\",\n\t\t\t}\n\t\t},\n\t}\n\tfor name, tt := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ttc := tt(t)\n\t\t\topts, err := tc.p.AuthorizeSign(context.Background(), tc.token)\n\t\t\tif tc.err != nil {\n\t\t\t\tif assert.Implements(t, (*render.StatusCodedError)(nil), err) {\n\t\t\t\t\tvar sc render.StatusCodedError\n\t\t\t\t\tif errors.As(err, &sc) {\n\t\t\t\t\t\tassert.Equal(t, tc.code, sc.StatusCode())\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tassert.EqualError(t, err, tc.err.Error())\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.NoError(t, err)\n\t\t\tif assert.NotNil(t, opts) {\n\t\t\t\tassert.Len(t, opts, 8) // number of SignOptions returned\n\t\t\t\tfor _, o := range opts {\n\t\t\t\t\tswitch v := o.(type) {\n\t\t\t\t\tcase *ACME:\n\t\t\t\t\tcase *provisionerExtensionOption:\n\t\t\t\t\t\tassert.Equal(t, v.Type, TypeACME)\n\t\t\t\t\t\tassert.Equal(t, v.Name, tc.p.GetName())\n\t\t\t\t\t\tassert.Equal(t, v.CredentialID, \"\")\n\t\t\t\t\t\tassert.Len(t, v.KeyValuePairs, 0)\n\t\t\t\t\tcase *forceCNOption:\n\t\t\t\t\t\tassert.Equal(t, v.ForceCN, tc.p.ForceCN)\n\t\t\t\t\tcase profileDefaultDuration:\n\t\t\t\t\t\tassert.Equal(t, time.Duration(v), tc.p.ctl.Claimer.DefaultTLSCertDuration())\n\t\t\t\t\tcase defaultPublicKeyValidator:\n\t\t\t\t\tcase *validityValidator:\n\t\t\t\t\t\tassert.Equal(t, v.min, tc.p.ctl.Claimer.MinTLSCertDuration())\n\t\t\t\t\t\tassert.Equal(t, v.max, tc.p.ctl.Claimer.MaxTLSCertDuration())\n\t\t\t\t\tcase *x509NamePolicyValidator:\n\t\t\t\t\t\tassert.Equal(t, nil, v.policyEngine)\n\t\t\t\t\tcase *WebhookController:\n\t\t\t\t\t\tassert.Len(t, v.webhooks, 0)\n\t\t\t\t\tdefault:\n\t\t\t\t\t\trequire.NoError(t, fmt.Errorf(\"unexpected sign option of type %T\", v))\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestACME_IsChallengeEnabled(t *testing.T) {\n\tctx := context.Background()\n\ttype fields struct {\n\t\tChallenges []ACMEChallenge\n\t}\n\ttype args struct {\n\t\tctx       context.Context\n\t\tchallenge ACMEChallenge\n\t}\n\ttests := []struct {\n\t\tname   string\n\t\tfields fields\n\t\targs   args\n\t\twant   bool\n\t}{\n\t\t{\"ok http-01\", fields{nil}, args{ctx, HTTP_01}, true},\n\t\t{\"ok dns-01\", fields{nil}, args{ctx, DNS_01}, true},\n\t\t{\"ok tls-alpn-01\", fields{[]ACMEChallenge{}}, args{ctx, TLS_ALPN_01}, true},\n\t\t{\"fail device-attest-01\", fields{[]ACMEChallenge{}}, args{ctx, \"device-attest-01\"}, false},\n\t\t{\"ok http-01 enabled\", fields{[]ACMEChallenge{\"http-01\"}}, args{ctx, \"HTTP-01\"}, true},\n\t\t{\"ok dns-01 enabled\", fields{[]ACMEChallenge{\"http-01\", \"dns-01\"}}, args{ctx, DNS_01}, true},\n\t\t{\"ok tls-alpn-01 enabled\", fields{[]ACMEChallenge{\"http-01\", \"dns-01\", \"tls-alpn-01\"}}, args{ctx, TLS_ALPN_01}, true},\n\t\t{\"ok device-attest-01 enabled\", fields{[]ACMEChallenge{\"device-attest-01\", \"dns-01\"}}, args{ctx, DEVICE_ATTEST_01}, true},\n\t\t{\"ok wire-oidc-01 enabled\", fields{[]ACMEChallenge{\"wire-oidc-01\"}}, args{ctx, WIREOIDC_01}, true},\n\t\t{\"ok wire-dpop-01 enabled\", fields{[]ACMEChallenge{\"wire-dpop-01\"}}, args{ctx, WIREDPOP_01}, true},\n\t\t{\"fail http-01\", fields{[]ACMEChallenge{\"dns-01\"}}, args{ctx, \"http-01\"}, false},\n\t\t{\"fail dns-01\", fields{[]ACMEChallenge{\"http-01\", \"tls-alpn-01\"}}, args{ctx, \"dns-01\"}, false},\n\t\t{\"fail tls-alpn-01\", fields{[]ACMEChallenge{\"http-01\", \"dns-01\", \"device-attest-01\"}}, args{ctx, \"tls-alpn-01\"}, false},\n\t\t{\"fail device-attest-01\", fields{[]ACMEChallenge{\"http-01\", \"dns-01\"}}, args{ctx, \"device-attest-01\"}, false},\n\t\t{\"fail wire-oidc-01\", fields{[]ACMEChallenge{\"http-01\", \"dns-01\"}}, args{ctx, \"wire-oidc-01\"}, false},\n\t\t{\"fail wire-dpop-01\", fields{[]ACMEChallenge{\"http-01\", \"dns-01\"}}, args{ctx, \"wire-dpop-01\"}, false},\n\t\t{\"fail unknown\", fields{[]ACMEChallenge{\"http-01\", \"dns-01\", \"tls-alpn-01\", \"device-attest-01\"}}, args{ctx, \"unknown\"}, false},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tp := &ACME{\n\t\t\t\tChallenges: tt.fields.Challenges,\n\t\t\t}\n\t\t\tgot := p.IsChallengeEnabled(tt.args.ctx, tt.args.challenge)\n\t\t\tassert.Equal(t, tt.want, got)\n\t\t})\n\t}\n}\n\nfunc TestACME_IsAttestationFormatEnabled(t *testing.T) {\n\tctx := context.Background()\n\ttype fields struct {\n\t\tAttestationFormats []ACMEAttestationFormat\n\t}\n\ttype args struct {\n\t\tctx    context.Context\n\t\tformat ACMEAttestationFormat\n\t}\n\ttests := []struct {\n\t\tname   string\n\t\tfields fields\n\t\targs   args\n\t\twant   bool\n\t}{\n\t\t{\"ok\", fields{[]ACMEAttestationFormat{APPLE, STEP, TPM}}, args{ctx, TPM}, true},\n\t\t{\"ok empty apple\", fields{nil}, args{ctx, APPLE}, true},\n\t\t{\"ok empty step\", fields{nil}, args{ctx, STEP}, true},\n\t\t{\"ok empty tpm\", fields{[]ACMEAttestationFormat{}}, args{ctx, \"tpm\"}, true},\n\t\t{\"ok uppercase\", fields{[]ACMEAttestationFormat{APPLE, STEP, TPM}}, args{ctx, \"STEP\"}, true},\n\t\t{\"fail apple\", fields{[]ACMEAttestationFormat{STEP, TPM}}, args{ctx, APPLE}, false},\n\t\t{\"fail step\", fields{[]ACMEAttestationFormat{APPLE, TPM}}, args{ctx, STEP}, false},\n\t\t{\"fail step\", fields{[]ACMEAttestationFormat{APPLE, STEP}}, args{ctx, TPM}, false},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tp := &ACME{\n\t\t\t\tAttestationFormats: tt.fields.AttestationFormats,\n\t\t\t}\n\t\t\tgot := p.IsAttestationFormatEnabled(tt.args.ctx, tt.args.format)\n\t\t\tassert.Equal(t, tt.want, got)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "authority/provisioner/aws.go",
    "content": "package provisioner\n\nimport (\n\t\"context\"\n\t\"crypto/sha256\"\n\t\"crypto/x509\"\n\t\"encoding/base64\"\n\t\"encoding/hex\"\n\t\"encoding/json\"\n\t\"encoding/pem\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"net/http\"\n\t\"os\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n\n\t\"github.com/smallstep/linkedca\"\n\t\"go.step.sm/crypto/jose\"\n\t\"go.step.sm/crypto/sshutil\"\n\t\"go.step.sm/crypto/x509util\"\n\n\t\"github.com/smallstep/certificates/errs\"\n\t\"github.com/smallstep/certificates/webhook\"\n\n\t_ \"embed\"\n)\n\n// awsIssuer is the string used as issuer in the generated tokens.\nconst awsIssuer = \"ec2.amazonaws.com\"\n\n// awsIdentityURL is the url used to retrieve the instance identity document.\nconst awsIdentityURL = \"http://169.254.169.254/latest/dynamic/instance-identity/document\"\n\n// awsSignatureURL is the url used to retrieve the instance identity signature.\nconst awsSignatureURL = \"http://169.254.169.254/latest/dynamic/instance-identity/signature\"\n\n// awsAPITokenURL is the url used to get the IMDSv2 API token\nconst awsAPITokenURL = \"http://169.254.169.254/latest/api/token\" //nolint:gosec // no credentials here\n\n// awsAPITokenTTL is the default TTL to use when requesting IMDSv2 API tokens\n// -- we keep this short-lived since we get a new token with every call to readURL()\nconst awsAPITokenTTL = \"30\"\n\n// awsMetadataTokenHeader is the header that must be passed with every IMDSv2 request\nconst awsMetadataTokenHeader = \"X-aws-ec2-metadata-token\" //nolint:gosec // no credentials here\n\n// awsMetadataTokenTTLHeader is the header used to indicate the token TTL requested\nconst awsMetadataTokenTTLHeader = \"X-aws-ec2-metadata-token-ttl-seconds\" //nolint:gosec // no credentials here\n\n// awsCertificate is the certificate used to validate the instance identity\n// signature. It is embedded in the binary at compile time.\n//\n//go:embed aws_certificates.pem\nvar awsCertificate string\n\n// awsSignatureAlgorithm is the signature algorithm used to verify the identity\n// document signature.\nconst awsSignatureAlgorithm = x509.SHA256WithRSA\n\ntype awsConfig struct {\n\tidentityURL        string\n\tsignatureURL       string\n\ttokenURL           string\n\ttokenTTL           string\n\tcertificates       []*x509.Certificate\n\tsignatureAlgorithm x509.SignatureAlgorithm\n}\n\nfunc newAWSConfig(certPath string) (*awsConfig, error) {\n\tvar certBytes []byte\n\tif certPath == \"\" {\n\t\tcertBytes = []byte(awsCertificate)\n\t} else {\n\t\tif b, err := os.ReadFile(certPath); err == nil {\n\t\t\tcertBytes = b\n\t\t} else {\n\t\t\treturn nil, errors.Wrapf(err, \"error reading %s\", certPath)\n\t\t}\n\t}\n\n\t// Read all the certificates.\n\tvar certs []*x509.Certificate\n\tfor len(certBytes) > 0 {\n\t\tvar block *pem.Block\n\t\tblock, certBytes = pem.Decode(certBytes)\n\t\tif block == nil {\n\t\t\tbreak\n\t\t}\n\t\tif block.Type != \"CERTIFICATE\" || len(block.Headers) != 0 {\n\t\t\tcontinue\n\t\t}\n\t\tcert, err := x509.ParseCertificate(block.Bytes)\n\t\tif err != nil {\n\t\t\treturn nil, errors.Wrap(err, \"error parsing AWS IID certificate\")\n\t\t}\n\t\tcerts = append(certs, cert)\n\t}\n\tif len(certs) == 0 {\n\t\treturn nil, errors.New(\"error parsing AWS IID certificate: no certificates found\")\n\t}\n\n\treturn &awsConfig{\n\t\tidentityURL:        awsIdentityURL,\n\t\tsignatureURL:       awsSignatureURL,\n\t\ttokenURL:           awsAPITokenURL,\n\t\ttokenTTL:           awsAPITokenTTL,\n\t\tcertificates:       certs,\n\t\tsignatureAlgorithm: awsSignatureAlgorithm,\n\t}, nil\n}\n\ntype awsPayload struct {\n\tjose.Claims\n\tAmazon   awsAmazonPayload `json:\"amazon\"`\n\tSANs     []string         `json:\"sans\"`\n\tdocument awsInstanceIdentityDocument\n}\n\ntype awsAmazonPayload struct {\n\tDocument  []byte `json:\"document\"`\n\tSignature []byte `json:\"signature\"`\n}\n\ntype awsInstanceIdentityDocument struct {\n\tAccountID          string    `json:\"accountId\"`\n\tArchitecture       string    `json:\"architecture\"`\n\tAvailabilityZone   string    `json:\"availabilityZone\"`\n\tBillingProducts    []string  `json:\"billingProducts\"`\n\tDevpayProductCodes []string  `json:\"devpayProductCodes\"`\n\tImageID            string    `json:\"imageId\"`\n\tInstanceID         string    `json:\"instanceId\"`\n\tInstanceType       string    `json:\"instanceType\"`\n\tKernelID           string    `json:\"kernelId\"`\n\tPendingTime        time.Time `json:\"pendingTime\"`\n\tPrivateIP          string    `json:\"privateIp\"`\n\tRamdiskID          string    `json:\"ramdiskId\"`\n\tRegion             string    `json:\"region\"`\n\tVersion            string    `json:\"version\"`\n}\n\n// AWS is the provisioner that supports identity tokens created from the Amazon\n// Web Services Instance Identity Documents.\n//\n// If DisableCustomSANs is true, only the internal DNS and IP will be added as a\n// SAN. By default it will accept any SAN in the CSR.\n//\n// If DisableTrustOnFirstUse is true, multiple sign request for this provisioner\n// with the same instance will be accepted. By default only the first request\n// will be accepted.\n//\n// If InstanceAge is set, only the instances with a pendingTime within the given\n// period will be accepted.\n//\n// IIDRoots can be used to specify a path to the certificates used to verify the\n// identity certificate signature.\n//\n// Amazon Identity docs are available at\n// https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-identity-documents.html\ntype AWS struct {\n\t*base\n\tID                     string   `json:\"-\"`\n\tType                   string   `json:\"type\"`\n\tName                   string   `json:\"name\"`\n\tAccounts               []string `json:\"accounts\"`\n\tDisableCustomSANs      bool     `json:\"disableCustomSANs\"`\n\tDisableTrustOnFirstUse bool     `json:\"disableTrustOnFirstUse\"`\n\tIMDSVersions           []string `json:\"imdsVersions\"`\n\tInstanceAge            Duration `json:\"instanceAge,omitempty\"`\n\tIIDRoots               string   `json:\"iidRoots,omitempty\"`\n\tClaims                 *Claims  `json:\"claims,omitempty\"`\n\tOptions                *Options `json:\"options,omitempty\"`\n\tconfig                 *awsConfig\n\tctl                    *Controller\n}\n\n// GetID returns the provisioner unique identifier.\nfunc (p *AWS) GetID() string {\n\tif p.ID != \"\" {\n\t\treturn p.ID\n\t}\n\treturn p.GetIDForToken()\n}\n\n// GetIDForToken returns an identifier that will be used to load the provisioner\n// from a token.\nfunc (p *AWS) GetIDForToken() string {\n\treturn \"aws/\" + p.Name\n}\n\n// GetTokenID returns the identifier of the token.\nfunc (p *AWS) GetTokenID(token string) (string, error) {\n\tpayload, err := p.authorizeToken(token)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\t// If TOFU is disabled create an ID for the token, so it cannot be reused.\n\t// The timestamps, document and signatures should be mostly unique.\n\tif p.DisableTrustOnFirstUse {\n\t\tsum := sha256.Sum256([]byte(token))\n\t\treturn strings.ToLower(hex.EncodeToString(sum[:])), nil\n\t}\n\n\t// Use provisioner + instance-id as the identifier.\n\tunique := fmt.Sprintf(\"%s.%s\", p.GetIDForToken(), payload.document.InstanceID)\n\tsum := sha256.Sum256([]byte(unique))\n\treturn strings.ToLower(hex.EncodeToString(sum[:])), nil\n}\n\n// GetName returns the name of the provisioner.\nfunc (p *AWS) GetName() string {\n\treturn p.Name\n}\n\n// GetType returns the type of provisioner.\nfunc (p *AWS) GetType() Type {\n\treturn TypeAWS\n}\n\n// GetEncryptedKey is not available in an AWS provisioner.\nfunc (p *AWS) GetEncryptedKey() (kid, key string, ok bool) {\n\treturn \"\", \"\", false\n}\n\n// GetIdentityToken retrieves the identity document and it's signature and\n// generates a token with them.\nfunc (p *AWS) GetIdentityToken(subject, caURL string) (string, error) {\n\t// Initialize the config if this method is used from the cli.\n\tif err := p.assertConfig(); err != nil {\n\t\treturn \"\", err\n\t}\n\n\tvar idoc awsInstanceIdentityDocument\n\tdoc, err := p.readURL(p.config.identityURL)\n\tif err != nil {\n\t\treturn \"\", errors.Wrap(err, \"error retrieving identity document:\\n  Are you in an AWS VM?\\n  Is the metadata service enabled?\\n  Are you using the proper metadata service version?\")\n\t}\n\tif err := json.Unmarshal(doc, &idoc); err != nil {\n\t\treturn \"\", errors.Wrap(err, \"error unmarshaling identity document\")\n\t}\n\tsig, err := p.readURL(p.config.signatureURL)\n\tif err != nil {\n\t\treturn \"\", errors.Wrap(err, \"error retrieving identity document:\\n  Are you in an AWS VM?\\n  Is the metadata service enabled?\\n  Are you using the proper metadata service version?\")\n\t}\n\tsignature, err := base64.StdEncoding.DecodeString(string(sig))\n\tif err != nil {\n\t\treturn \"\", errors.Wrap(err, \"error decoding identity document signature\")\n\t}\n\tif err := p.checkSignature(doc, signature); err != nil {\n\t\treturn \"\", err\n\t}\n\n\taudience, err := generateSignAudience(caURL, p.GetIDForToken())\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\t// Create unique ID for Trust On First Use (TOFU). Only the first instance\n\t// per provisioner is allowed as we don't have a way to trust the given\n\t// sans.\n\tunique := fmt.Sprintf(\"%s.%s\", p.GetIDForToken(), idoc.InstanceID)\n\tsum := sha256.Sum256([]byte(unique))\n\n\t// Create a JWT from the identity document\n\tsigner, err := jose.NewSigner(\n\t\tjose.SigningKey{Algorithm: jose.HS256, Key: signature},\n\t\tnew(jose.SignerOptions).WithType(\"JWT\"),\n\t)\n\tif err != nil {\n\t\treturn \"\", errors.Wrap(err, \"error creating signer\")\n\t}\n\n\tnow := time.Now()\n\tpayload := awsPayload{\n\t\tClaims: jose.Claims{\n\t\t\tIssuer:    awsIssuer,\n\t\t\tSubject:   subject,\n\t\t\tAudience:  []string{audience},\n\t\t\tExpiry:    jose.NewNumericDate(now.Add(5 * time.Minute)),\n\t\t\tNotBefore: jose.NewNumericDate(now),\n\t\t\tIssuedAt:  jose.NewNumericDate(now),\n\t\t\tID:        strings.ToLower(hex.EncodeToString(sum[:])),\n\t\t},\n\t\tAmazon: awsAmazonPayload{\n\t\t\tDocument:  doc,\n\t\t\tSignature: signature,\n\t\t},\n\t}\n\n\ttok, err := jose.Signed(signer).Claims(payload).CompactSerialize()\n\tif err != nil {\n\t\treturn \"\", errors.Wrap(err, \"error serializing token\")\n\t}\n\n\treturn tok, nil\n}\n\n// Init validates and initializes the AWS provisioner.\nfunc (p *AWS) Init(config Config) (err error) {\n\tswitch {\n\tcase p.Type == \"\":\n\t\treturn errors.New(\"provisioner type cannot be empty\")\n\tcase p.Name == \"\":\n\t\treturn errors.New(\"provisioner name cannot be empty\")\n\tcase p.InstanceAge.Value() < 0:\n\t\treturn errors.New(\"provisioner instanceAge cannot be negative\")\n\t}\n\n\t// Add default config\n\tif p.config, err = newAWSConfig(p.IIDRoots); err != nil {\n\t\treturn err\n\t}\n\n\t// validate IMDS versions\n\tif len(p.IMDSVersions) == 0 {\n\t\tp.IMDSVersions = []string{\"v2\", \"v1\"}\n\t}\n\tfor _, v := range p.IMDSVersions {\n\t\tswitch v {\n\t\tcase \"v1\":\n\t\t\t// valid\n\t\tcase \"v2\":\n\t\t\t// valid\n\t\tdefault:\n\t\t\treturn errors.Errorf(\"%s: not a supported AWS Instance Metadata Service version\", v)\n\t\t}\n\t}\n\n\tconfig.Audiences = config.Audiences.WithFragment(p.GetIDForToken())\n\tp.ctl, err = NewController(p, p.Claims, config, p.Options)\n\treturn\n}\n\n// AuthorizeSign validates the given token and returns the sign options that\n// will be used on certificate creation.\nfunc (p *AWS) AuthorizeSign(ctx context.Context, token string) ([]SignOption, error) {\n\tpayload, err := p.authorizeToken(token)\n\tif err != nil {\n\t\treturn nil, errs.Wrap(http.StatusInternalServerError, err, \"aws.AuthorizeSign\")\n\t}\n\n\tdoc := payload.document\n\n\t// Template options\n\tdata := x509util.NewTemplateData()\n\tdata.SetCommonName(payload.Claims.Subject)\n\tif v, err := unsafeParseSigned(token); err == nil {\n\t\tdata.SetToken(v)\n\t}\n\n\t// Enforce known CN and default DNS and IP if configured.\n\t// By default we'll accept the CN and SANs in the CSR.\n\t// There's no way to trust them other than TOFU.\n\tvar so []SignOption\n\tif p.DisableCustomSANs {\n\t\tdnsName := fmt.Sprintf(\"ip-%s.%s.compute.internal\", strings.ReplaceAll(doc.PrivateIP, \".\", \"-\"), doc.Region)\n\t\tso = append(so,\n\t\t\tdnsNamesSubsetValidator([]string{dnsName}),\n\t\t\tipAddressesValidator([]net.IP{\n\t\t\t\tnet.ParseIP(doc.PrivateIP),\n\t\t\t}),\n\t\t\temailAddressesValidator(nil),\n\t\t\tnewURIsValidator(ctx, nil),\n\t\t)\n\n\t\t// Template options\n\t\tdata.SetSANs([]string{dnsName, doc.PrivateIP})\n\t}\n\n\ttemplateOptions, err := CustomTemplateOptions(p.Options, data, x509util.DefaultIIDLeafTemplate)\n\tif err != nil {\n\t\treturn nil, errs.Wrap(http.StatusInternalServerError, err, \"aws.AuthorizeSign\")\n\t}\n\n\treturn append(so,\n\t\tp,\n\t\ttemplateOptions,\n\t\t// modifiers / withOptions\n\t\tnewProvisionerExtensionOption(TypeAWS, p.Name, doc.AccountID, \"InstanceID\", doc.InstanceID).WithControllerOptions(p.ctl),\n\t\tprofileDefaultDuration(p.ctl.Claimer.DefaultTLSCertDuration()),\n\t\t// validators\n\t\tdefaultPublicKeyValidator{},\n\t\tcommonNameValidator(payload.Claims.Subject),\n\t\tnewValidityValidator(p.ctl.Claimer.MinTLSCertDuration(), p.ctl.Claimer.MaxTLSCertDuration()),\n\t\tnewX509NamePolicyValidator(p.ctl.getPolicy().getX509()),\n\t\tp.ctl.newWebhookController(\n\t\t\tdata,\n\t\t\tlinkedca.Webhook_X509,\n\t\t\twebhook.WithAuthorizationPrincipal(doc.InstanceID),\n\t\t),\n\t), nil\n}\n\n// AuthorizeRenew returns an error if the renewal is disabled.\n// NOTE: This method does not actually validate the certificate or check it's\n// revocation status. Just confirms that the provisioner that created the\n// certificate was configured to allow renewals.\nfunc (p *AWS) AuthorizeRenew(ctx context.Context, cert *x509.Certificate) error {\n\treturn p.ctl.AuthorizeRenew(ctx, cert)\n}\n\n// assertConfig initializes the config if it has not been initialized\nfunc (p *AWS) assertConfig() (err error) {\n\tif p.config != nil {\n\t\treturn\n\t}\n\tp.config, err = newAWSConfig(p.IIDRoots)\n\treturn err\n}\n\n// checkSignature returns an error if the signature is not valid.\nfunc (p *AWS) checkSignature(signed, signature []byte) error {\n\tfor _, crt := range p.config.certificates {\n\t\tif err := crt.CheckSignature(p.config.signatureAlgorithm, signed, signature); err == nil {\n\t\t\treturn nil\n\t\t}\n\t}\n\treturn errors.New(\"error validating identity document signature\")\n}\n\n// readURL does a GET request to the given url and returns the body. It's not\n// using pkg/errors to avoid verbose errors, the caller should use it and write\n// the appropriate error.\nfunc (p *AWS) readURL(url string) ([]byte, error) {\n\tvar resp *http.Response\n\tvar err error\n\n\t// Initialize IMDS versions when this is called from the cli.\n\tif len(p.IMDSVersions) == 0 {\n\t\tp.IMDSVersions = []string{\"v2\", \"v1\"}\n\t}\n\n\tfor _, v := range p.IMDSVersions {\n\t\tswitch v {\n\t\tcase \"v1\":\n\t\t\tresp, err = p.readURLv1(url)\n\t\t\tif err == nil && resp.StatusCode < 400 {\n\t\t\t\treturn p.readResponseBody(resp)\n\t\t\t}\n\t\tcase \"v2\":\n\t\t\tresp, err = p.readURLv2(url)\n\t\t\tif err == nil && resp.StatusCode < 400 {\n\t\t\t\treturn p.readResponseBody(resp)\n\t\t\t}\n\t\tdefault:\n\t\t\treturn nil, fmt.Errorf(\"%s: not a supported AWS Instance Metadata Service version\", v)\n\t\t}\n\t\tif resp != nil {\n\t\t\tresp.Body.Close()\n\t\t}\n\t}\n\n\t// all versions have been exhausted and we haven't returned successfully yet so pass\n\t// the error on to the caller\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn nil, fmt.Errorf(\"request for metadata returned non-successful status code %d\",\n\t\tresp.StatusCode)\n}\n\nfunc (p *AWS) readURLv1(url string) (*http.Response, error) {\n\tclient := http.Client{}\n\n\treq, err := http.NewRequest(http.MethodGet, url, http.NoBody)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn resp, nil\n}\n\nfunc (p *AWS) readURLv2(url string) (*http.Response, error) {\n\tclient := http.Client{}\n\n\t// first get the token\n\treq, err := http.NewRequest(http.MethodPut, p.config.tokenURL, http.NoBody)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treq.Header.Set(awsMetadataTokenTTLHeader, p.config.tokenTTL)\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer resp.Body.Close()\n\tif resp.StatusCode >= 400 {\n\t\treturn nil, fmt.Errorf(\"request for API token returned non-successful status code %d\", resp.StatusCode)\n\t}\n\ttoken, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// now make the request\n\treq, err = http.NewRequest(http.MethodGet, url, http.NoBody)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treq.Header.Set(awsMetadataTokenHeader, string(token))\n\tresp, err = client.Do(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn resp, nil\n}\n\nfunc (p *AWS) readResponseBody(resp *http.Response) ([]byte, error) {\n\tdefer resp.Body.Close()\n\tb, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn b, nil\n}\n\n// authorizeToken performs common jwt authorization actions and returns the\n// claims for case specific downstream parsing.\n// e.g. a Sign request will auth/validate different fields than a Revoke request.\nfunc (p *AWS) authorizeToken(token string) (*awsPayload, error) {\n\tjwt, err := jose.ParseSigned(token)\n\tif err != nil {\n\t\treturn nil, errs.Wrapf(http.StatusUnauthorized, err, \"aws.authorizeToken; error parsing aws token\")\n\t}\n\tif len(jwt.Headers) == 0 {\n\t\treturn nil, errs.InternalServer(\"aws.authorizeToken; error parsing token, header is missing\")\n\t}\n\n\tvar unsafeClaims awsPayload\n\tif err := jwt.UnsafeClaimsWithoutVerification(&unsafeClaims); err != nil {\n\t\treturn nil, errs.Wrap(http.StatusUnauthorized, err, \"aws.authorizeToken; error unmarshaling claims\")\n\t}\n\n\tvar payload awsPayload\n\tif err := jwt.Claims(unsafeClaims.Amazon.Signature, &payload); err != nil {\n\t\treturn nil, errs.Wrap(http.StatusUnauthorized, err, \"aws.authorizeToken; error verifying claims\")\n\t}\n\n\t// Validate identity document signature\n\tif err := p.checkSignature(payload.Amazon.Document, payload.Amazon.Signature); err != nil {\n\t\treturn nil, errs.Wrap(http.StatusUnauthorized, err, \"aws.authorizeToken; invalid aws token signature\")\n\t}\n\n\tvar doc awsInstanceIdentityDocument\n\tif err := json.Unmarshal(payload.Amazon.Document, &doc); err != nil {\n\t\treturn nil, errs.Wrap(http.StatusUnauthorized, err, \"aws.authorizeToken; error unmarshaling aws identity document\")\n\t}\n\n\tswitch {\n\tcase doc.AccountID == \"\":\n\t\treturn nil, errs.Unauthorized(\"aws.authorizeToken; aws identity document accountId cannot be empty\")\n\tcase doc.InstanceID == \"\":\n\t\treturn nil, errs.Unauthorized(\"aws.authorizeToken; aws identity document instanceId cannot be empty\")\n\tcase doc.PrivateIP == \"\":\n\t\treturn nil, errs.Unauthorized(\"aws.authorizeToken; aws identity document privateIp cannot be empty\")\n\tcase doc.Region == \"\":\n\t\treturn nil, errs.Unauthorized(\"aws.authorizeToken; aws identity document region cannot be empty\")\n\t}\n\n\t// According to \"rfc7519 JSON Web Token\" acceptable skew should be no\n\t// more than a few minutes.\n\tnow := time.Now().UTC()\n\tif err = payload.ValidateWithLeeway(jose.Expected{\n\t\tIssuer: awsIssuer,\n\t\tTime:   now,\n\t}, time.Minute); err != nil {\n\t\treturn nil, errs.Wrapf(http.StatusUnauthorized, err, \"aws.authorizeToken; invalid aws token\")\n\t}\n\n\t// validate audiences with the defaults\n\tif !matchesAudience(payload.Audience, p.ctl.Audiences.Sign) {\n\t\treturn nil, errs.Unauthorized(\"aws.authorizeToken; invalid token - invalid audience claim (aud)\")\n\t}\n\n\t// Validate subject, it has to be known if disableCustomSANs is enabled\n\tif p.DisableCustomSANs {\n\t\tif payload.Subject != doc.InstanceID &&\n\t\t\tpayload.Subject != doc.PrivateIP &&\n\t\t\tpayload.Subject != fmt.Sprintf(\"ip-%s.%s.compute.internal\", strings.ReplaceAll(doc.PrivateIP, \".\", \"-\"), doc.Region) {\n\t\t\treturn nil, errs.Unauthorized(\"aws.authorizeToken; invalid token - invalid subject claim (sub)\")\n\t\t}\n\t}\n\n\t// validate accounts\n\tif len(p.Accounts) > 0 {\n\t\tvar found bool\n\t\tfor _, sa := range p.Accounts {\n\t\t\tif sa == doc.AccountID {\n\t\t\t\tfound = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif !found {\n\t\t\treturn nil, errs.Unauthorized(\"aws.authorizeToken; invalid aws identity document - accountId is not valid\")\n\t\t}\n\t}\n\n\t// validate instance age\n\tif d := p.InstanceAge.Value(); d > 0 {\n\t\tif now.Sub(doc.PendingTime) > d {\n\t\t\treturn nil, errs.Unauthorized(\"aws.authorizeToken; aws identity document pendingTime is too old\")\n\t\t}\n\t}\n\n\tpayload.document = doc\n\treturn &payload, nil\n}\n\n// AuthorizeSSHSign returns the list of SignOption for a SignSSH request.\nfunc (p *AWS) AuthorizeSSHSign(_ context.Context, token string) ([]SignOption, error) {\n\tif !p.ctl.Claimer.IsSSHCAEnabled() {\n\t\treturn nil, errs.Unauthorized(\"aws.AuthorizeSSHSign; ssh ca is disabled for aws provisioner '%s'\", p.GetName())\n\t}\n\tclaims, err := p.authorizeToken(token)\n\tif err != nil {\n\t\treturn nil, errs.Wrap(http.StatusInternalServerError, err, \"aws.AuthorizeSSHSign\")\n\t}\n\n\tdoc := claims.document\n\tsignOptions := []SignOption{}\n\n\t// Enforce host certificate.\n\tdefaults := SignSSHOptions{\n\t\tCertType: SSHHostCert,\n\t}\n\n\t// Validated principals.\n\tprincipals := []string{\n\t\tdoc.PrivateIP,\n\t\tfmt.Sprintf(\"ip-%s.%s.compute.internal\", strings.ReplaceAll(doc.PrivateIP, \".\", \"-\"), doc.Region),\n\t}\n\n\t// Only enforce known principals if disable custom sans is true.\n\tif p.DisableCustomSANs {\n\t\tdefaults.Principals = principals\n\t} else {\n\t\t// Check that at least one principal is sent in the request.\n\t\tsignOptions = append(signOptions, &sshCertOptionsRequireValidator{\n\t\t\tPrincipals: true,\n\t\t})\n\t}\n\n\t// Certificate templates.\n\tdata := sshutil.CreateTemplateData(sshutil.HostCert, doc.InstanceID, principals)\n\tif v, err := unsafeParseSigned(token); err == nil {\n\t\tdata.SetToken(v)\n\t}\n\n\ttemplateOptions, err := CustomSSHTemplateOptions(p.Options, data, sshutil.DefaultIIDTemplate)\n\tif err != nil {\n\t\treturn nil, errs.Wrap(http.StatusInternalServerError, err, \"aws.AuthorizeSSHSign\")\n\t}\n\tsignOptions = append(signOptions, templateOptions)\n\n\treturn append(signOptions,\n\t\tp,\n\t\t// Validate user SignSSHOptions.\n\t\tsshCertOptionsValidator(defaults),\n\t\t// Set the validity bounds if not set.\n\t\t&sshDefaultDuration{p.ctl.Claimer},\n\t\t// Validate public key\n\t\t&sshDefaultPublicKeyValidator{},\n\t\t// Validate the validity period.\n\t\t&sshCertValidityValidator{p.ctl.Claimer},\n\t\t// Require all the fields in the SSH certificate\n\t\t&sshCertDefaultValidator{},\n\t\t// Ensure that all principal names are allowed\n\t\tnewSSHNamePolicyValidator(p.ctl.getPolicy().getSSHHost(), nil),\n\t\t// Call webhooks\n\t\tp.ctl.newWebhookController(\n\t\t\tdata,\n\t\t\tlinkedca.Webhook_SSH,\n\t\t\twebhook.WithAuthorizationPrincipal(doc.InstanceID),\n\t\t),\n\t), nil\n}\n"
  },
  {
    "path": "authority/provisioner/aws_certificates.pem",
    "content": "# https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/verify-signature.html\n# https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/regions-certs.html use RSA format\n\n\n# certificate for us-east-2\n-----BEGIN CERTIFICATE-----\nMIIDITCCAoqgAwIBAgIUVJTc+hOU+8Gk3JlqsX438Dk5c58wDQYJKoZIhvcNAQEL\nBQAwXDELMAkGA1UEBhMCVVMxGTAXBgNVBAgTEFdhc2hpbmd0b24gU3RhdGUxEDAO\nBgNVBAcTB1NlYXR0bGUxIDAeBgNVBAoTF0FtYXpvbiBXZWIgU2VydmljZXMgTExD\nMB4XDTI0MDQyOTE3MTE0OVoXDTI5MDQyODE3MTE0OVowXDELMAkGA1UEBhMCVVMx\nGTAXBgNVBAgTEFdhc2hpbmd0b24gU3RhdGUxEDAOBgNVBAcTB1NlYXR0bGUxIDAe\nBgNVBAoTF0FtYXpvbiBXZWIgU2VydmljZXMgTExDMIGfMA0GCSqGSIb3DQEBAQUA\nA4GNADCBiQKBgQCHvRjf/0kStpJ248khtIaN8qkDN3tkw4VjvA9nvPl2anJO+eIB\nUqPfQG09kZlwpWpmyO8bGB2RWqWxCwuB/dcnIob6w420k9WY5C0IIGtDRNauN3ku\nvGXkw3HEnF0EjYr0pcyWUvByWY4KswZV42X7Y7XSS13hOIcL6NLA+H94/QIDAQAB\no4HfMIHcMAsGA1UdDwQEAwIHgDAdBgNVHQ4EFgQUJdbMCBXKtvCcWdwUUizvtUF2\nUTgwgZkGA1UdIwSBkTCBjoAUJdbMCBXKtvCcWdwUUizvtUF2UTihYKReMFwxCzAJ\nBgNVBAYTAlVTMRkwFwYDVQQIExBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHEwdT\nZWF0dGxlMSAwHgYDVQQKExdBbWF6b24gV2ViIFNlcnZpY2VzIExMQ4IUVJTc+hOU\n+8Gk3JlqsX438Dk5c58wEgYDVR0TAQH/BAgwBgEB/wIBADANBgkqhkiG9w0BAQsF\nAAOBgQAywJQaVNWJqW0R0T0xVOSoN1GLk9x9kKEuN67RN9CLin4dA97qa7Mr5W4P\nFZ6vnh5CjOhQBRXV9xJUeYSdqVItNAUFK/fEzDdjf1nUfPlQ3OJ49u6CV01NoJ9m\nusvY9kWcV46dqn2bk2MyfTTgvmeqP8fiMRPxxnVRkSzlldP5Fg==\n-----END CERTIFICATE-----\n\n# certificate for us-east-1\n-----BEGIN CERTIFICATE-----\nMIIDITCCAoqgAwIBAgIUE1y2NIKCU+Rg4uu4u32koG9QEYIwDQYJKoZIhvcNAQEL\nBQAwXDELMAkGA1UEBhMCVVMxGTAXBgNVBAgTEFdhc2hpbmd0b24gU3RhdGUxEDAO\nBgNVBAcTB1NlYXR0bGUxIDAeBgNVBAoTF0FtYXpvbiBXZWIgU2VydmljZXMgTExD\nMB4XDTI0MDQyOTE3MzQwMVoXDTI5MDQyODE3MzQwMVowXDELMAkGA1UEBhMCVVMx\nGTAXBgNVBAgTEFdhc2hpbmd0b24gU3RhdGUxEDAOBgNVBAcTB1NlYXR0bGUxIDAe\nBgNVBAoTF0FtYXpvbiBXZWIgU2VydmljZXMgTExDMIGfMA0GCSqGSIb3DQEBAQUA\nA4GNADCBiQKBgQCHvRjf/0kStpJ248khtIaN8qkDN3tkw4VjvA9nvPl2anJO+eIB\nUqPfQG09kZlwpWpmyO8bGB2RWqWxCwuB/dcnIob6w420k9WY5C0IIGtDRNauN3ku\nvGXkw3HEnF0EjYr0pcyWUvByWY4KswZV42X7Y7XSS13hOIcL6NLA+H94/QIDAQAB\no4HfMIHcMAsGA1UdDwQEAwIHgDAdBgNVHQ4EFgQUJdbMCBXKtvCcWdwUUizvtUF2\nUTgwgZkGA1UdIwSBkTCBjoAUJdbMCBXKtvCcWdwUUizvtUF2UTihYKReMFwxCzAJ\nBgNVBAYTAlVTMRkwFwYDVQQIExBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHEwdT\nZWF0dGxlMSAwHgYDVQQKExdBbWF6b24gV2ViIFNlcnZpY2VzIExMQ4IUE1y2NIKC\nU+Rg4uu4u32koG9QEYIwEgYDVR0TAQH/BAgwBgEB/wIBADANBgkqhkiG9w0BAQsF\nAAOBgQAlxSmwcWnhT4uAeSinJuz+1BTcKhVSWb5jT8pYjQb8ZoZkXXRGb09mvYeU\nNeqOBr27rvRAnaQ/9LUQf72+SahDFuS4CMI8nwowytqbmwquqFr4dxA/SDADyRiF\nea1UoMuNHTY49J/1vPomqsVn7mugTp+TbjqCfOJTpu0temHcFA==\n-----END CERTIFICATE-----\n\n# certificate for us-west-1\n-----BEGIN CERTIFICATE-----\nMIIDITCCAoqgAwIBAgIUK2zmY9PUSTR7rc1k2OwPYu4+g7wwDQYJKoZIhvcNAQEL\nBQAwXDELMAkGA1UEBhMCVVMxGTAXBgNVBAgTEFdhc2hpbmd0b24gU3RhdGUxEDAO\nBgNVBAcTB1NlYXR0bGUxIDAeBgNVBAoTF0FtYXpvbiBXZWIgU2VydmljZXMgTExD\nMB4XDTI0MDQyOTE3MDI0M1oXDTI5MDQyODE3MDI0M1owXDELMAkGA1UEBhMCVVMx\nGTAXBgNVBAgTEFdhc2hpbmd0b24gU3RhdGUxEDAOBgNVBAcTB1NlYXR0bGUxIDAe\nBgNVBAoTF0FtYXpvbiBXZWIgU2VydmljZXMgTExDMIGfMA0GCSqGSIb3DQEBAQUA\nA4GNADCBiQKBgQCHvRjf/0kStpJ248khtIaN8qkDN3tkw4VjvA9nvPl2anJO+eIB\nUqPfQG09kZlwpWpmyO8bGB2RWqWxCwuB/dcnIob6w420k9WY5C0IIGtDRNauN3ku\nvGXkw3HEnF0EjYr0pcyWUvByWY4KswZV42X7Y7XSS13hOIcL6NLA+H94/QIDAQAB\no4HfMIHcMAsGA1UdDwQEAwIHgDAdBgNVHQ4EFgQUJdbMCBXKtvCcWdwUUizvtUF2\nUTgwgZkGA1UdIwSBkTCBjoAUJdbMCBXKtvCcWdwUUizvtUF2UTihYKReMFwxCzAJ\nBgNVBAYTAlVTMRkwFwYDVQQIExBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHEwdT\nZWF0dGxlMSAwHgYDVQQKExdBbWF6b24gV2ViIFNlcnZpY2VzIExMQ4IUK2zmY9PU\nSTR7rc1k2OwPYu4+g7wwEgYDVR0TAQH/BAgwBgEB/wIBADANBgkqhkiG9w0BAQsF\nAAOBgQA1Ng4QmN4n7iPh5CnadSOc0ZfM7by0dBePwZJyGvOHdaw6P6E/vEk76KsC\nQ8p+akuzVzVPkU4kBK/TRqLp19wEWoVwhhTaxHjQ1tTRHqXIVlrkw4JrtFbeNM21\nGlkSLonuzmNZdivn9WuQYeGe7nUD4w3q9GgiF3CPorJe+UxtbA==\n-----END CERTIFICATE-----\n\n# certificate for us-west-2\n-----BEGIN CERTIFICATE-----\nMIIDITCCAoqgAwIBAgIUFx8PxCkbHwpD31bOyCtyz3GclbgwDQYJKoZIhvcNAQEL\nBQAwXDELMAkGA1UEBhMCVVMxGTAXBgNVBAgTEFdhc2hpbmd0b24gU3RhdGUxEDAO\nBgNVBAcTB1NlYXR0bGUxIDAeBgNVBAoTF0FtYXpvbiBXZWIgU2VydmljZXMgTExD\nMB4XDTI0MDQyOTE3MjM1OVoXDTI5MDQyODE3MjM1OVowXDELMAkGA1UEBhMCVVMx\nGTAXBgNVBAgTEFdhc2hpbmd0b24gU3RhdGUxEDAOBgNVBAcTB1NlYXR0bGUxIDAe\nBgNVBAoTF0FtYXpvbiBXZWIgU2VydmljZXMgTExDMIGfMA0GCSqGSIb3DQEBAQUA\nA4GNADCBiQKBgQCHvRjf/0kStpJ248khtIaN8qkDN3tkw4VjvA9nvPl2anJO+eIB\nUqPfQG09kZlwpWpmyO8bGB2RWqWxCwuB/dcnIob6w420k9WY5C0IIGtDRNauN3ku\nvGXkw3HEnF0EjYr0pcyWUvByWY4KswZV42X7Y7XSS13hOIcL6NLA+H94/QIDAQAB\no4HfMIHcMAsGA1UdDwQEAwIHgDAdBgNVHQ4EFgQUJdbMCBXKtvCcWdwUUizvtUF2\nUTgwgZkGA1UdIwSBkTCBjoAUJdbMCBXKtvCcWdwUUizvtUF2UTihYKReMFwxCzAJ\nBgNVBAYTAlVTMRkwFwYDVQQIExBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHEwdT\nZWF0dGxlMSAwHgYDVQQKExdBbWF6b24gV2ViIFNlcnZpY2VzIExMQ4IUFx8PxCkb\nHwpD31bOyCtyz3GclbgwEgYDVR0TAQH/BAgwBgEB/wIBADANBgkqhkiG9w0BAQsF\nAAOBgQBzOl+9Xy1+UsbUBI95HO9mbbdnuX+aMJXgG9uFZNjgNEbMcvx+h8P9IMko\nz7PzFdheQQ1NLjsHH9mSR1SyC4m9ja6BsejH5nLBWyCdjfdP3muZM4O5+r7vUa1O\ndWU+hP/T7DUrPAIVMOE7mpYa+WPWJrN6BlRwQkKQ7twm9kDalA==\n-----END CERTIFICATE-----\n\n# certificate for eu-south-1\n-----BEGIN CERTIFICATE-----\nMIICNjCCAZ+gAwIBAgIJAOZ3GEIaDcugMA0GCSqGSIb3DQEBCwUAMFwxCzAJBgNV\nBAYTAlVTMRkwFwYDVQQIExBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHEwdTZWF0\ndGxlMSAwHgYDVQQKExdBbWF6b24gV2ViIFNlcnZpY2VzIExMQzAgFw0xOTEwMjQx\nNTE5MDlaGA8yMTk5MDMyOTE1MTkwOVowXDELMAkGA1UEBhMCVVMxGTAXBgNVBAgT\nEFdhc2hpbmd0b24gU3RhdGUxEDAOBgNVBAcTB1NlYXR0bGUxIDAeBgNVBAoTF0Ft\nYXpvbiBXZWIgU2VydmljZXMgTExDMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKB\ngQCjiPgW3vsXRj4JoA16WQDyoPc/eh3QBARaApJEc4nPIGoUolpAXcjFhWplo2O+\nivgfCsc4AU9OpYdAPha3spLey/bhHPRi1JZHRNqScKP0hzsCNmKhfnZTIEQCFvsp\nDRp4zr91/WS06/flJFBYJ6JHhp0KwM81XQG59lV6kkoW7QIDAQABMA0GCSqGSIb3\nDQEBCwUAA4GBAGLLrY3P+HH6C57dYgtJkuGZGT2+rMkk2n81/abzTJvsqRqGRrWv\nXRKRXlKdM/dfiuYGokDGxiC0Mg6TYy6wvsR2qRhtXW1OtZkiHWcQCnOttz+8vpew\nwx8JGMvowtuKB1iMsbwyRqZkFYLcvH+Opfb/Aayi20/ChQLdI6M2R5VU\n-----END CERTIFICATE-----\n\n# certificate for ap-east-1\n-----BEGIN CERTIFICATE-----\nMIICSzCCAbQCCQDtQvkVxRvK9TANBgkqhkiG9w0BAQsFADBqMQswCQYDVQQGEwJV\nUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHU2VhdHRsZTEYMBYGA1UE\nChMPQW1hem9uLmNvbSBJbmMuMRowGAYDVQQDExFlYzIuYW1hem9uYXdzLmNvbTAe\nFw0xOTAyMDMwMzAwMDZaFw0yOTAyMDIwMzAwMDZaMGoxCzAJBgNVBAYTAlVTMRMw\nEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdTZWF0dGxlMRgwFgYDVQQKEw9B\nbWF6b24uY29tIEluYy4xGjAYBgNVBAMTEWVjMi5hbWF6b25hd3MuY29tMIGfMA0G\nCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC1kkHXYTfc7gY5Q55JJhjTieHAgacaQkiR\nPity9QPDE3b+NXDh4UdP1xdIw73JcIIG3sG9RhWiXVCHh6KkuCTqJfPUknIKk8vs\nM3RXflUpBe8Pf+P92pxqPMCz1Fr2NehS3JhhpkCZVGxxwLC5gaG0Lr4rFORubjYY\nRh84dK98VwIDAQABMA0GCSqGSIb3DQEBCwUAA4GBAA6xV9f0HMqXjPHuGILDyaNN\ndKcvplNFwDTydVg32MNubAGnecoEBtUPtxBsLoVYXCOb+b5/ZMDubPF9tU/vSXuo\nTpYM5Bq57gJzDRaBOntQbX9bgHiUxw6XZWaTS/6xjRJDT5p3S1E0mPI3lP/eJv4o\nEzk5zb3eIf10/sqt4756\n-----END CERTIFICATE-----\n\n# certificate for af-south-1\n-----BEGIN CERTIFICATE-----\nMIICNjCCAZ+gAwIBAgIJAKumfZiRrNvHMA0GCSqGSIb3DQEBCwUAMFwxCzAJBgNV\nBAYTAlVTMRkwFwYDVQQIExBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHEwdTZWF0\ndGxlMSAwHgYDVQQKExdBbWF6b24gV2ViIFNlcnZpY2VzIExMQzAgFw0xOTExMjcw\nNzE0MDVaGA8yMTk5MDUwMjA3MTQwNVowXDELMAkGA1UEBhMCVVMxGTAXBgNVBAgT\nEFdhc2hpbmd0b24gU3RhdGUxEDAOBgNVBAcTB1NlYXR0bGUxIDAeBgNVBAoTF0Ft\nYXpvbiBXZWIgU2VydmljZXMgTExDMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKB\ngQDFd571nUzVtke3rPyRkYfvs3jh0C0EMzzG72boyUNjnfw1+m0TeFraTLKb9T6F\n7TuB/ZEN+vmlYqr2+5Va8U8qLbPF0bRH+FdaKjhgWZdYXxGzQzU3ioy5W5ZM1VyB\n7iUsxEAlxsybC3ziPYaHI42UiTkQNahmoroNeqVyHNnBpQIDAQABMA0GCSqGSIb3\nDQEBCwUAA4GBAAJLylWyElEgOpW4B1XPyRVD4pAds8Guw2+krgqkY0HxLCdjosuH\nRytGDGN+q75aAoXzW5a7SGpxLxk6Hfv0xp3RjDHsoeP0i1d8MD3hAC5ezxS4oukK\ns5gbPOnokhKTMPXbTdRn5ZifCbWlx+bYN/mTYKvxho7b5SVg2o1La9aK\n-----END CERTIFICATE-----\n\n# certificate for me-south-1\n-----BEGIN CERTIFICATE-----\nMIIDPDCCAqWgAwIBAgIJAMl6uIV/zqJFMA0GCSqGSIb3DQEBCwUAMHIxCzAJBgNV\nBAYTAlVTMRMwEQYDVQQIDApXYXNoaW5ndG9uMRAwDgYDVQQHDAdTZWF0dGxlMSAw\nHgYDVQQKDBdBbWF6b24gV2ViIFNlcnZpY2VzIExMQzEaMBgGA1UEAwwRZWMyLmFt\nYXpvbmF3cy5jb20wIBcNMTkwNDI2MTQzMjQ3WhgPMjE5ODA5MjkxNDMyNDdaMHIx\nCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApXYXNoaW5ndG9uMRAwDgYDVQQHDAdTZWF0\ndGxlMSAwHgYDVQQKDBdBbWF6b24gV2ViIFNlcnZpY2VzIExMQzEaMBgGA1UEAwwR\nZWMyLmFtYXpvbmF3cy5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBALVN\nCDTZEnIeoX1SEYqq6k1BV0ZlpY5y3KnoOreCAE589TwS4MX5+8Fzd6AmACmugeBP\nQk7Hm6b2+g/d4tWycyxLaQlcq81DB1GmXehRkZRgGeRge1ePWd1TUA0I8P/QBT7S\ngUePm/kANSFU+P7s7u1NNl+vynyi0wUUrw7/wIZTAgMBAAGjgdcwgdQwHQYDVR0O\nBBYEFILtMd+T4YgH1cgc+hVsVOV+480FMIGkBgNVHSMEgZwwgZmAFILtMd+T4YgH\n1cgc+hVsVOV+480FoXakdDByMQswCQYDVQQGEwJVUzETMBEGA1UECAwKV2FzaGlu\nZ3RvbjEQMA4GA1UEBwwHU2VhdHRsZTEgMB4GA1UECgwXQW1hem9uIFdlYiBTZXJ2\naWNlcyBMTEMxGjAYBgNVBAMMEWVjMi5hbWF6b25hd3MuY29tggkAyXq4hX/OokUw\nDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOBgQBhkNTBIFgWFd+ZhC/LhRUY\n4OjEiykmbEp6hlzQ79T0Tfbn5A4NYDI2icBP0+hmf6qSnIhwJF6typyd1yPK5Fqt\nNTpxxcXmUKquX+pHmIkK1LKDO8rNE84jqxrxRsfDi6by82fjVYf2pgjJW8R1FAw+\nmL5WQRFexbfB5aXhcMo0AA==\n-----END CERTIFICATE-----\n\n# certificate for cn-north-1\n-----BEGIN CERTIFICATE-----\nMIIDCzCCAnSgAwIBAgIJALSOMbOoU2svMA0GCSqGSIb3DQEBCwUAMFwxCzAJBgNV\nBAYTAlVTMRkwFwYDVQQIExBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHEwdTZWF0\ndGxlMSAwHgYDVQQKExdBbWF6b24gV2ViIFNlcnZpY2VzIExMQzAeFw0yMzA3MDQw\nODM1MzlaFw0yODA3MDIwODM1MzlaMFwxCzAJBgNVBAYTAlVTMRkwFwYDVQQIExBX\nYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHEwdTZWF0dGxlMSAwHgYDVQQKExdBbWF6\nb24gV2ViIFNlcnZpY2VzIExMQzCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA\nuhhUNlqAZdcWWB/OSDVDGk3OA99EFzOn/mJlmciQ/Xwu2dFJWmSCqEAE6gjufCjQ\nq3voxAhC2CF+elKtJW/C0Sz/LYo60PUqd6iXF4h+upB9HkOOGuWHXsHBTsvgkgGA\n1CGgel4U0Cdq+23eANr8N8m28UzljjSnTlrYCHtzN4sCAwEAAaOB1DCB0TALBgNV\nHQ8EBAMCB4AwHQYDVR0OBBYEFBkZu3wT27NnYgrfH+xJz4HJaNJoMIGOBgNVHSME\ngYYwgYOAFBkZu3wT27NnYgrfH+xJz4HJaNJooWCkXjBcMQswCQYDVQQGEwJVUzEZ\nMBcGA1UECBMQV2FzaGluZ3RvbiBTdGF0ZTEQMA4GA1UEBxMHU2VhdHRsZTEgMB4G\nA1UEChMXQW1hem9uIFdlYiBTZXJ2aWNlcyBMTEOCCQC0jjGzqFNrLzASBgNVHRMB\nAf8ECDAGAQH/AgEAMA0GCSqGSIb3DQEBCwUAA4GBAECji43p+oPkYqmzll7e8Hgb\noADS0ph+YUz5P/bUCm61wFjlxaTfwKcuTR3ytj7bFLoW5Bm7Sa+TCl3lOGb2taon\n2h+9NirRK6JYk87LMNvbS40HGPFumJL2NzEsGUeK+MRiWu+Oh5/lJGii3qw4YByx\nSUDlRyNy1jJFstEZjOhs\n-----END CERTIFICATE-----\n\n# certificate for cn-northwest-1\n-----BEGIN CERTIFICATE-----\nMIIDCzCCAnSgAwIBAgIJALSOMbOoU2svMA0GCSqGSIb3DQEBCwUAMFwxCzAJBgNV\nBAYTAlVTMRkwFwYDVQQIExBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHEwdTZWF0\ndGxlMSAwHgYDVQQKExdBbWF6b24gV2ViIFNlcnZpY2VzIExMQzAeFw0yMzA3MDQw\nODM1MzlaFw0yODA3MDIwODM1MzlaMFwxCzAJBgNVBAYTAlVTMRkwFwYDVQQIExBX\nYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHEwdTZWF0dGxlMSAwHgYDVQQKExdBbWF6\nb24gV2ViIFNlcnZpY2VzIExMQzCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA\nuhhUNlqAZdcWWB/OSDVDGk3OA99EFzOn/mJlmciQ/Xwu2dFJWmSCqEAE6gjufCjQ\nq3voxAhC2CF+elKtJW/C0Sz/LYo60PUqd6iXF4h+upB9HkOOGuWHXsHBTsvgkgGA\n1CGgel4U0Cdq+23eANr8N8m28UzljjSnTlrYCHtzN4sCAwEAAaOB1DCB0TALBgNV\nHQ8EBAMCB4AwHQYDVR0OBBYEFBkZu3wT27NnYgrfH+xJz4HJaNJoMIGOBgNVHSME\ngYYwgYOAFBkZu3wT27NnYgrfH+xJz4HJaNJooWCkXjBcMQswCQYDVQQGEwJVUzEZ\nMBcGA1UECBMQV2FzaGluZ3RvbiBTdGF0ZTEQMA4GA1UEBxMHU2VhdHRsZTEgMB4G\nA1UEChMXQW1hem9uIFdlYiBTZXJ2aWNlcyBMTEOCCQC0jjGzqFNrLzASBgNVHRMB\nAf8ECDAGAQH/AgEAMA0GCSqGSIb3DQEBCwUAA4GBAECji43p+oPkYqmzll7e8Hgb\noADS0ph+YUz5P/bUCm61wFjlxaTfwKcuTR3ytj7bFLoW5Bm7Sa+TCl3lOGb2taon\n2h+9NirRK6JYk87LMNvbS40HGPFumJL2NzEsGUeK+MRiWu+Oh5/lJGii3qw4YByx\nSUDlRyNy1jJFstEZjOhs\n-----END CERTIFICATE-----\n\n# certificate for eu-central-1\n-----BEGIN CERTIFICATE-----\nMIIDITCCAoqgAwIBAgIUFD5GsmkxRuecttwsCG763m3u63UwDQYJKoZIhvcNAQEL\nBQAwXDELMAkGA1UEBhMCVVMxGTAXBgNVBAgTEFdhc2hpbmd0b24gU3RhdGUxEDAO\nBgNVBAcTB1NlYXR0bGUxIDAeBgNVBAoTF0FtYXpvbiBXZWIgU2VydmljZXMgTExD\nMB4XDTI0MDQyOTE1NTUyOVoXDTI5MDQyODE1NTUyOVowXDELMAkGA1UEBhMCVVMx\nGTAXBgNVBAgTEFdhc2hpbmd0b24gU3RhdGUxEDAOBgNVBAcTB1NlYXR0bGUxIDAe\nBgNVBAoTF0FtYXpvbiBXZWIgU2VydmljZXMgTExDMIGfMA0GCSqGSIb3DQEBAQUA\nA4GNADCBiQKBgQCHvRjf/0kStpJ248khtIaN8qkDN3tkw4VjvA9nvPl2anJO+eIB\nUqPfQG09kZlwpWpmyO8bGB2RWqWxCwuB/dcnIob6w420k9WY5C0IIGtDRNauN3ku\nvGXkw3HEnF0EjYr0pcyWUvByWY4KswZV42X7Y7XSS13hOIcL6NLA+H94/QIDAQAB\no4HfMIHcMAsGA1UdDwQEAwIHgDAdBgNVHQ4EFgQUJdbMCBXKtvCcWdwUUizvtUF2\nUTgwgZkGA1UdIwSBkTCBjoAUJdbMCBXKtvCcWdwUUizvtUF2UTihYKReMFwxCzAJ\nBgNVBAYTAlVTMRkwFwYDVQQIExBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHEwdT\nZWF0dGxlMSAwHgYDVQQKExdBbWF6b24gV2ViIFNlcnZpY2VzIExMQ4IUFD5Gsmkx\nRuecttwsCG763m3u63UwEgYDVR0TAQH/BAgwBgEB/wIBADANBgkqhkiG9w0BAQsF\nAAOBgQBBh0WaXlBsW56Hqk588MmJxsOrvcKfDjF57RgEDgnGnQaJcStCVWDO9UYO\nJX2tdsPw+E7AjDqjsuxYaotLn3Mr3mK0sNOXq9BljBnWD4pARg89KZnZI8FN35HQ\nO/LYOVHCknuPL123VmVRNs51qQA9hkPjvw21UzpDLxaUxt9Z/w==\n-----END CERTIFICATE-----\n\n# certificate for eu-central-2\n-----BEGIN CERTIFICATE-----\nMIICMzCCAZygAwIBAgIGAXjSGFGiMA0GCSqGSIb3DQEBBQUAMFwxCzAJBgNVBAYT\nAlVTMRkwFwYDVQQIDBBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHDAdTZWF0dGxl\nMSAwHgYDVQQKDBdBbWF6b24gV2ViIFNlcnZpY2VzIExMQzAgFw0yMTA0MTQyMDM1\nMTJaGA8yMjAwMDQxNDIwMzUxMlowXDELMAkGA1UEBhMCVVMxGTAXBgNVBAgMEFdh\nc2hpbmd0b24gU3RhdGUxEDAOBgNVBAcMB1NlYXR0bGUxIDAeBgNVBAoMF0FtYXpv\nbiBXZWIgU2VydmljZXMgTExDMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC2\nmdGdps5Rz2jzYcGNsgETTGUthJRrVqSnUWJXTlVaIbkGPLKO6Or7AfWKFp2sgRJ8\nvLsjoBVR5cESVK7cuK1wItjvJyi/opKZAUusJx2hpgU3pUHhlp9ATh/VeVD582jT\nd9IY+8t5MDa6Z3fGliByEiXz0LEHdi8MBacLREu1TwIDAQABMA0GCSqGSIb3DQEB\nBQUAA4GBAILlpoE3k9o7KdALAxsFJNitVS+g3RMzdbiFM+7MA63Nv5fsf+0xgcjS\nNBElvPCDKFvTJl4QQhToy056llO5GvdS9RK+H8xrP2mrqngApoKTApv93vHBixgF\nSn5KrczRO0YSm3OjkqbydU7DFlmkXXR7GYE+5jbHvQHYiT1J5sMu\n-----END CERTIFICATE-----\n\n# certificate for ap-south-1\n-----BEGIN CERTIFICATE-----\nMIIDITCCAoqgAwIBAgIUDLA+x6tTAP3LRTr0z6nOxfsozdMwDQYJKoZIhvcNAQEL\nBQAwXDELMAkGA1UEBhMCVVMxGTAXBgNVBAgTEFdhc2hpbmd0b24gU3RhdGUxEDAO\nBgNVBAcTB1NlYXR0bGUxIDAeBgNVBAoTF0FtYXpvbiBXZWIgU2VydmljZXMgTExD\nMB4XDTI0MDQyOTE0MTMwMVoXDTI5MDQyODE0MTMwMVowXDELMAkGA1UEBhMCVVMx\nGTAXBgNVBAgTEFdhc2hpbmd0b24gU3RhdGUxEDAOBgNVBAcTB1NlYXR0bGUxIDAe\nBgNVBAoTF0FtYXpvbiBXZWIgU2VydmljZXMgTExDMIGfMA0GCSqGSIb3DQEBAQUA\nA4GNADCBiQKBgQCHvRjf/0kStpJ248khtIaN8qkDN3tkw4VjvA9nvPl2anJO+eIB\nUqPfQG09kZlwpWpmyO8bGB2RWqWxCwuB/dcnIob6w420k9WY5C0IIGtDRNauN3ku\nvGXkw3HEnF0EjYr0pcyWUvByWY4KswZV42X7Y7XSS13hOIcL6NLA+H94/QIDAQAB\no4HfMIHcMAsGA1UdDwQEAwIHgDAdBgNVHQ4EFgQUJdbMCBXKtvCcWdwUUizvtUF2\nUTgwgZkGA1UdIwSBkTCBjoAUJdbMCBXKtvCcWdwUUizvtUF2UTihYKReMFwxCzAJ\nBgNVBAYTAlVTMRkwFwYDVQQIExBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHEwdT\nZWF0dGxlMSAwHgYDVQQKExdBbWF6b24gV2ViIFNlcnZpY2VzIExMQ4IUDLA+x6tT\nAP3LRTr0z6nOxfsozdMwEgYDVR0TAQH/BAgwBgEB/wIBADANBgkqhkiG9w0BAQsF\nAAOBgQAZ7rYKoAwwiiH1M5GJbrT/BEk3OO2VrEPw8ZxgpqQ/EKlzMlOs/0Cyrmp7\nUYyUgYFQe5nq37Z94rOUSeMgv/WRxaMwrLlLqD78cuF9DSkXaZIX/kECtVaUnjk8\nBZx0QhoIHOpQocJUSlm/dLeMuE0+0A3HNR6JVktGsUdv9ulmKw==\n-----END CERTIFICATE-----\n\n# certificate for ap-south-2\n-----BEGIN CERTIFICATE-----\nMIICMzCCAZygAwIBAgIGAXjwLj9CMA0GCSqGSIb3DQEBBQUAMFwxCzAJBgNVBAYT\nAlVTMRkwFwYDVQQIDBBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHDAdTZWF0dGxl\nMSAwHgYDVQQKDBdBbWF6b24gV2ViIFNlcnZpY2VzIExMQzAgFw0yMTA0MjAxNjQ3\nNDVaGA8yMjAwMDQyMDE2NDc0NVowXDELMAkGA1UEBhMCVVMxGTAXBgNVBAgMEFdh\nc2hpbmd0b24gU3RhdGUxEDAOBgNVBAcMB1NlYXR0bGUxIDAeBgNVBAoMF0FtYXpv\nbiBXZWIgU2VydmljZXMgTExDMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDT\nwHu0ND+sFcobrjvcAYm0PNRD8f4R1jAzvoLt2+qGeOTAyO1Httj6cmsYN3AP1hN5\niYuppFiYsl2eNPa/CD0Vg0BAfDFlV5rzjpA0j7TJabVh4kj7JvtD+xYMi6wEQA4x\n6SPONY4OeZ2+8o/HS8nucpWDVdPRO6ciWUlMhjmDmwIDAQABMA0GCSqGSIb3DQEB\nBQUAA4GBAAy6sgTdRkTqELHBeWj69q60xHyUmsWqHAQNXKVc9ApWGG4onzuqlMbG\nETwUZ9mTq2vxlV0KvuetCDNS5u4cJsxe/TGGbYP0yP2qfMl0cCImzRI5W0gn8gog\ndervfeT7nH5ih0TWEy/QDWfkQ601L4erm4yh4YQq8vcqAPSkf04N\n-----END CERTIFICATE-----\n\n# certificate for ap-southeast-1\n-----BEGIN CERTIFICATE-----\nMIIDITCCAoqgAwIBAgIUSqP6ih+++5KF07NXngrWf26mhSUwDQYJKoZIhvcNAQEL\nBQAwXDELMAkGA1UEBhMCVVMxGTAXBgNVBAgTEFdhc2hpbmd0b24gU3RhdGUxEDAO\nBgNVBAcTB1NlYXR0bGUxIDAeBgNVBAoTF0FtYXpvbiBXZWIgU2VydmljZXMgTExD\nMB4XDTI0MDQyOTE0MzAxNFoXDTI5MDQyODE0MzAxNFowXDELMAkGA1UEBhMCVVMx\nGTAXBgNVBAgTEFdhc2hpbmd0b24gU3RhdGUxEDAOBgNVBAcTB1NlYXR0bGUxIDAe\nBgNVBAoTF0FtYXpvbiBXZWIgU2VydmljZXMgTExDMIGfMA0GCSqGSIb3DQEBAQUA\nA4GNADCBiQKBgQCHvRjf/0kStpJ248khtIaN8qkDN3tkw4VjvA9nvPl2anJO+eIB\nUqPfQG09kZlwpWpmyO8bGB2RWqWxCwuB/dcnIob6w420k9WY5C0IIGtDRNauN3ku\nvGXkw3HEnF0EjYr0pcyWUvByWY4KswZV42X7Y7XSS13hOIcL6NLA+H94/QIDAQAB\no4HfMIHcMAsGA1UdDwQEAwIHgDAdBgNVHQ4EFgQUJdbMCBXKtvCcWdwUUizvtUF2\nUTgwgZkGA1UdIwSBkTCBjoAUJdbMCBXKtvCcWdwUUizvtUF2UTihYKReMFwxCzAJ\nBgNVBAYTAlVTMRkwFwYDVQQIExBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHEwdT\nZWF0dGxlMSAwHgYDVQQKExdBbWF6b24gV2ViIFNlcnZpY2VzIExMQ4IUSqP6ih++\n+5KF07NXngrWf26mhSUwEgYDVR0TAQH/BAgwBgEB/wIBADANBgkqhkiG9w0BAQsF\nAAOBgQAw13BxW11U/JL58j//Fmk7qqtrZTqXmaz1qm2WlIpJpW750MOcP4ux1uPy\neM0RdVZ4jHSMv5gtLAv/PjExBfw9n6vNCk+5GZG4Xec5DoapBZHXmfMo93sjxBFP\n4x9rWn0GuwAVO9ukjYPevq2Rerilrq5VvppHtbATVNY2qecXDA==\n-----END CERTIFICATE-----\n\n# certificate for ap-southeast-2\n-----BEGIN CERTIFICATE-----\nMIIDITCCAoqgAwIBAgIUFxWyAdk4oiXIOC9PxcgjYYh71mwwDQYJKoZIhvcNAQEL\nBQAwXDELMAkGA1UEBhMCVVMxGTAXBgNVBAgTEFdhc2hpbmd0b24gU3RhdGUxEDAO\nBgNVBAcTB1NlYXR0bGUxIDAeBgNVBAoTF0FtYXpvbiBXZWIgU2VydmljZXMgTExD\nMB4XDTI0MDQyOTE1MjE0M1oXDTI5MDQyODE1MjE0M1owXDELMAkGA1UEBhMCVVMx\nGTAXBgNVBAgTEFdhc2hpbmd0b24gU3RhdGUxEDAOBgNVBAcTB1NlYXR0bGUxIDAe\nBgNVBAoTF0FtYXpvbiBXZWIgU2VydmljZXMgTExDMIGfMA0GCSqGSIb3DQEBAQUA\nA4GNADCBiQKBgQCHvRjf/0kStpJ248khtIaN8qkDN3tkw4VjvA9nvPl2anJO+eIB\nUqPfQG09kZlwpWpmyO8bGB2RWqWxCwuB/dcnIob6w420k9WY5C0IIGtDRNauN3ku\nvGXkw3HEnF0EjYr0pcyWUvByWY4KswZV42X7Y7XSS13hOIcL6NLA+H94/QIDAQAB\no4HfMIHcMAsGA1UdDwQEAwIHgDAdBgNVHQ4EFgQUJdbMCBXKtvCcWdwUUizvtUF2\nUTgwgZkGA1UdIwSBkTCBjoAUJdbMCBXKtvCcWdwUUizvtUF2UTihYKReMFwxCzAJ\nBgNVBAYTAlVTMRkwFwYDVQQIExBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHEwdT\nZWF0dGxlMSAwHgYDVQQKExdBbWF6b24gV2ViIFNlcnZpY2VzIExMQ4IUFxWyAdk4\noiXIOC9PxcgjYYh71mwwEgYDVR0TAQH/BAgwBgEB/wIBADANBgkqhkiG9w0BAQsF\nAAOBgQByjeQe6lr7fiIhoGdjBXYzDfkX0lGGvMIhRh57G1bbceQfaYdZd7Ptc0jl\nbpycKGaTvhUdkpMOiV2Hi9dOOYawkdhyJDstmDNKu6P9+b6Kak8He5z3NU1tUR2Y\nuTwcz7Ye8Nldx//ws3raErfTI7D6s9m63OX8cAJ/f8bNgikwpw==\n-----END CERTIFICATE-----\n\n# certificate for ap-southeast-3\n-----BEGIN CERTIFICATE-----\nMIICMzCCAZygAwIBAgIGAXbVDG2yMA0GCSqGSIb3DQEBBQUAMFwxCzAJBgNVBAYT\nAlVTMRkwFwYDVQQIDBBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHDAdTZWF0dGxl\nMSAwHgYDVQQKDBdBbWF6b24gV2ViIFNlcnZpY2VzIExMQzAgFw0yMTAxMDYwMDE1\nMzBaGA8yMjAwMDEwNjAwMTUzMFowXDELMAkGA1UEBhMCVVMxGTAXBgNVBAgMEFdh\nc2hpbmd0b24gU3RhdGUxEDAOBgNVBAcMB1NlYXR0bGUxIDAeBgNVBAoMF0FtYXpv\nbiBXZWIgU2VydmljZXMgTExDMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCn\nCS/Vbt0gQ1ebWcur2hSO7PnJifE4OPxQ7RgSAlc4/spJp1sDP+ZrS0LO1ZJfKhXf\n1R9S3AUwLnsc7b+IuVXdY5LK9RKqu64nyXP5dx170zoL8loEyCSuRR2fs+04i2Qs\nWBVP+KFNAn7P5L1EHRjkgTO8kjNKviwRV+OkP9ab5wIDAQABMA0GCSqGSIb3DQEB\nBQUAA4GBAI4WUy6+DKh0JDSzQEZNyBgNlSoSuC2owtMxCwGB6nBfzzfcekWvs6eo\nfLTSGovrReX7MtVgrcJBZjmPIentw5dWUs+87w/g9lNwUnUt0ZHYyh2tuBG6hVJu\nUEwDJ/z3wDd6wQviLOTF3MITawt9P8siR1hXqLJNxpjRQFZrgHqi\n-----END CERTIFICATE-----\n\n# certificate for ap-southeast-4\n-----BEGIN CERTIFICATE-----\nMIICMzCCAZygAwIBAgIGAXjSh40SMA0GCSqGSIb3DQEBBQUAMFwxCzAJBgNVBAYT\nAlVTMRkwFwYDVQQIDBBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHDAdTZWF0dGxl\nMSAwHgYDVQQKDBdBbWF6b24gV2ViIFNlcnZpY2VzIExMQzAgFw0yMTA0MTQyMjM2\nNDJaGA8yMjAwMDQxNDIyMzY0MlowXDELMAkGA1UEBhMCVVMxGTAXBgNVBAgMEFdh\nc2hpbmd0b24gU3RhdGUxEDAOBgNVBAcMB1NlYXR0bGUxIDAeBgNVBAoMF0FtYXpv\nbiBXZWIgU2VydmljZXMgTExDMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDH\nezwQr2VQpQSTW5TXNefiQrP+qWTGAbGsPeMX4hBMjAJUKys2NIRcRZaLM/BCew2F\nIPVjNtlaj6Gwn9ipU4Mlz3zIwAMWi1AvGMSreppt+wV6MRtfOjh0Dvj/veJe88aE\nZJMozNgkJFRS+WFWsckQeL56tf6kY6QTlNo8V/0CsQIDAQABMA0GCSqGSIb3DQEB\nBQUAA4GBAF7vpPghH0FRo5gu49EArRNPrIvW1egMdZHrzJNqbztLCtV/wcgkqIww\nuXYj+1rhlL+/iMpQWjdVGEqIZSeXn5fLmdx50eegFCwND837r9e8XYTiQS143Sxt\n9+Yi6BZ7U7YD8kK9NBWoJxFqUeHdpRCs0O7COjT3gwm7ZxvAmssh\n-----END CERTIFICATE-----\n\n# certificate for eu-south-2\n-----BEGIN CERTIFICATE-----\nMIICMzCCAZygAwIBAgIGAXjwLkiaMA0GCSqGSIb3DQEBBQUAMFwxCzAJBgNVBAYT\nAlVTMRkwFwYDVQQIDBBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHDAdTZWF0dGxl\nMSAwHgYDVQQKDBdBbWF6b24gV2ViIFNlcnZpY2VzIExMQzAgFw0yMTA0MjAxNjQ3\nNDhaGA8yMjAwMDQyMDE2NDc0OFowXDELMAkGA1UEBhMCVVMxGTAXBgNVBAgMEFdh\nc2hpbmd0b24gU3RhdGUxEDAOBgNVBAcMB1NlYXR0bGUxIDAeBgNVBAoMF0FtYXpv\nbiBXZWIgU2VydmljZXMgTExDMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDB\n/VvR1+45Aey5zn3vPk6xBm5o9grSDL6D2iAuprQnfVXn8CIbSDbWFhA3fi5ippjK\nkh3sl8VyCvCOUXKdOaNrYBrPRkrdHdBuL2Tc84RO+3m/rxIUZ2IK1fDlC6sWAjdd\nf6sBrV2w2a78H0H8EwuwiSgttURBjwJ7KPPJCqaqrQIDAQABMA0GCSqGSIb3DQEB\nBQUAA4GBAKR+FzqQDzun/iMMzcFucmLMl5BxEblrFXOz7IIuOeiGkndmrqUeDCyk\nztLku45s7hxdNy4ltTuVAaE5aNBdw5J8U1mRvsKvHLy2ThH6hAWKwTqtPAJp7M21\nGDwgDDOkPSz6XVOehg+hBgiphYp84DUbWVYeP8YqLEJSqscKscWC\n-----END CERTIFICATE-----\n\n# certificate for il-central-1\n-----BEGIN CERTIFICATE-----\nMIICMzCCAZygAwIBAgIGAX0QQGVLMA0GCSqGSIb3DQEBBQUAMFwxCzAJBgNVBAYT\nAlVTMRkwFwYDVQQIDBBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHDAdTZWF0dGxl\nMSAwHgYDVQQKDBdBbWF6b24gV2ViIFNlcnZpY2VzIExMQzAgFw0yMTExMTExODI2\nMzVaGA8yMjAwMTExMTE4MjYzNVowXDELMAkGA1UEBhMCVVMxGTAXBgNVBAgMEFdh\nc2hpbmd0b24gU3RhdGUxEDAOBgNVBAcMB1NlYXR0bGUxIDAeBgNVBAoMF0FtYXpv\nbiBXZWIgU2VydmljZXMgTExDMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDr\nc24u3AgFxnoPgzxR6yFXOamcPuxYXhYKWmapb+S8vOy5hpLoRe4RkOrY0cM3bN07\nGdEMlin5mU0y1t8y3ct4YewvmkgT42kTyMM+t1K4S0xsqjXxxS716uGYh7eWtkxr\nCihj8AbXN/6pa095h+7TZyl2n83keiNUzM2KoqQVMwIDAQABMA0GCSqGSIb3DQEB\nBQUAA4GBADwA6VVEIIZD2YL00F12po40xDLzIc9XvqFPS9iFaWi2ho8wLio7wA49\nVYEFZSI9CR3SGB9tL8DUib97mlxmd1AcGShMmMlhSB29vhuhrUNB/FmU7H8s62/j\nD6cOR1A1cClIyZUe1yT1ZbPySCs43J+Thr8i8FSRxzDBSZZi5foW\n-----END CERTIFICATE-----\n\n# certificate for me-central-1\n-----BEGIN CERTIFICATE-----\nMIICMzCCAZygAwIBAgIGAXjRrnDjMA0GCSqGSIb3DQEBBQUAMFwxCzAJBgNVBAYT\nAlVTMRkwFwYDVQQIDBBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHDAdTZWF0dGxl\nMSAwHgYDVQQKDBdBbWF6b24gV2ViIFNlcnZpY2VzIExMQzAgFw0yMTA0MTQxODM5\nMzNaGA8yMjAwMDQxNDE4MzkzM1owXDELMAkGA1UEBhMCVVMxGTAXBgNVBAgMEFdh\nc2hpbmd0b24gU3RhdGUxEDAOBgNVBAcMB1NlYXR0bGUxIDAeBgNVBAoMF0FtYXpv\nbiBXZWIgU2VydmljZXMgTExDMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDc\naTgW/KyA6zyruJQrYy00a6wqLA7eeUzk3bMiTkLsTeDQfrkaZMfBAjGaaOymRo1C\n3qzE4rIenmahvUplu9ZmLwL1idWXMRX2RlSvIt+d2SeoKOKQWoc2UOFZMHYxDue7\nzkyk1CIRaBukTeY13/RIrlc6X61zJ5BBtZXlHwayjQIDAQABMA0GCSqGSIb3DQEB\nBQUAA4GBABTqTy3R6RXKPW45FA+cgo7YZEj/Cnz5YaoUivRRdX2A83BHuBTvJE2+\nWX00FTEj4hRVjameE1nENoO8Z7fUVloAFDlDo69fhkJeSvn51D1WRrPnoWGgEfr1\n+OfK1bAcKTtfkkkP9r4RdwSjKzO5Zu/B+Wqm3kVEz/QNcz6npmA6\n-----END CERTIFICATE-----\n\n# certificate for us-gov-east-1\n-----BEGIN CERTIFICATE-----\nMIIDITCCAoqgAwIBAgIULVyrqjjwZ461qelPCiShB1KCCj4wDQYJKoZIhvcNAQEL\nBQAwXDELMAkGA1UEBhMCVVMxGTAXBgNVBAgTEFdhc2hpbmd0b24gU3RhdGUxEDAO\nBgNVBAcTB1NlYXR0bGUxIDAeBgNVBAoTF0FtYXpvbiBXZWIgU2VydmljZXMgTExD\nMB4XDTI0MDUwNzE1MjIzNloXDTI5MDUwNjE1MjIzNlowXDELMAkGA1UEBhMCVVMx\nGTAXBgNVBAgTEFdhc2hpbmd0b24gU3RhdGUxEDAOBgNVBAcTB1NlYXR0bGUxIDAe\nBgNVBAoTF0FtYXpvbiBXZWIgU2VydmljZXMgTExDMIGfMA0GCSqGSIb3DQEBAQUA\nA4GNADCBiQKBgQCpohwYUVPH9I7Vbkb3WMe/JB0Y/bmfVj3VpcK445YBRO9K80al\nesjgBc2tAX4KYg4Lht4EBKccLHTzaNi51YEGX1aLNrSmxhz1+WtzNLNUsyY3zD9z\nvwX/3k1+JB2dRA+m+Cpwx4mjzZyAeQtHtegVaAytkmqtxQrSCexBxvqRqQIDAQAB\no4HfMIHcMAsGA1UdDwQEAwIHgDAdBgNVHQ4EFgQU1ZXneBYnPvYXkHVlVjg7918V\ngE8wgZkGA1UdIwSBkTCBjoAU1ZXneBYnPvYXkHVlVjg7918VgE+hYKReMFwxCzAJ\nBgNVBAYTAlVTMRkwFwYDVQQIExBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHEwdT\nZWF0dGxlMSAwHgYDVQQKExdBbWF6b24gV2ViIFNlcnZpY2VzIExMQ4IULVyrqjjw\nZ461qelPCiShB1KCCj4wEgYDVR0TAQH/BAgwBgEB/wIBADANBgkqhkiG9w0BAQsF\nAAOBgQBfAL/YZv0y3zmVbXjyxQCsDloeDCJjFKIu3ameEckeIWJbST9LMto0zViZ\npuIAf05x6GQiEqfBMk+YMxJfcTmJB4Ebaj4egFlslJPSHyC2xuydHlr3B04INOH5\nZ2oCM68u6GGbj0jZjg7GJonkReG9N72kDva/ukwZKgq8zErQVQ==\n-----END CERTIFICATE-----\n\n# certificate for us-gov-west-1\n-----BEGIN CERTIFICATE-----\nMIIDITCCAoqgAwIBAgIUe5wGF3jfb7lUHzvDxmM/ktGCLwwwDQYJKoZIhvcNAQEL\nBQAwXDELMAkGA1UEBhMCVVMxGTAXBgNVBAgTEFdhc2hpbmd0b24gU3RhdGUxEDAO\nBgNVBAcTB1NlYXR0bGUxIDAeBgNVBAoTF0FtYXpvbiBXZWIgU2VydmljZXMgTExD\nMB4XDTI0MDUwNzE3MzAzMloXDTI5MDUwNjE3MzAzMlowXDELMAkGA1UEBhMCVVMx\nGTAXBgNVBAgTEFdhc2hpbmd0b24gU3RhdGUxEDAOBgNVBAcTB1NlYXR0bGUxIDAe\nBgNVBAoTF0FtYXpvbiBXZWIgU2VydmljZXMgTExDMIGfMA0GCSqGSIb3DQEBAQUA\nA4GNADCBiQKBgQCpohwYUVPH9I7Vbkb3WMe/JB0Y/bmfVj3VpcK445YBRO9K80al\nesjgBc2tAX4KYg4Lht4EBKccLHTzaNi51YEGX1aLNrSmxhz1+WtzNLNUsyY3zD9z\nvwX/3k1+JB2dRA+m+Cpwx4mjzZyAeQtHtegVaAytkmqtxQrSCexBxvqRqQIDAQAB\no4HfMIHcMAsGA1UdDwQEAwIHgDAdBgNVHQ4EFgQU1ZXneBYnPvYXkHVlVjg7918V\ngE8wgZkGA1UdIwSBkTCBjoAU1ZXneBYnPvYXkHVlVjg7918VgE+hYKReMFwxCzAJ\nBgNVBAYTAlVTMRkwFwYDVQQIExBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHEwdT\nZWF0dGxlMSAwHgYDVQQKExdBbWF6b24gV2ViIFNlcnZpY2VzIExMQ4IUe5wGF3jf\nb7lUHzvDxmM/ktGCLwwwEgYDVR0TAQH/BAgwBgEB/wIBADANBgkqhkiG9w0BAQsF\nAAOBgQCbTdpx1Iob9SwUReY4exMnlwQlmkTLyA8tYGWzchCJOJJEPfsW0ryy1A0H\nYIuvyUty3rJdp9ib8h3GZR71BkZnNddHhy06kPs4p8ewF8+d8OWtOJQcI+ZnFfG4\nKyM4rUsBrljpG2aOCm12iACEyrvgJJrS8VZwUDZS6mZEnn/lhA==\n-----END CERTIFICATE-----\n\n# certificate for ca-west-1\n-----BEGIN CERTIFICATE-----\nMIICMzCCAZygAwIBAgIGAYPou9weMA0GCSqGSIb3DQEBBQUAMFwxCzAJBgNVBAYT\nAlVTMRkwFwYDVQQIDBBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHDAdTZWF0dGxl\nMSAwHgYDVQQKDBdBbWF6b24gV2ViIFNlcnZpY2VzIExMQzAgFw0yMjEwMTgwMTM2\nMDlaGA8yMjAxMTAxODAxMzYwOVowXDELMAkGA1UEBhMCVVMxGTAXBgNVBAgMEFdh\nc2hpbmd0b24gU3RhdGUxEDAOBgNVBAcMB1NlYXR0bGUxIDAeBgNVBAoMF0FtYXpv\nbiBXZWIgU2VydmljZXMgTExDMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDK\n1kIcG5Q6adBXQM75GldfTSiXl7tn54p10TnspI0ErDdb2B6q2Ji/v4XBVH13ZCMg\nqlRHMqV8AWI5iO6gFn2A9sN3AZXTMqwtZeiDdebq3k6Wt7ieYvpXTg0qvgsjQIov\nRZWaBDBJy9x8C2hW+w9lMQjFHkJ7Jy/PHCJ69EzebQIDAQABMA0GCSqGSIb3DQEB\nBQUAA4GBAGe9Snkz1A6rHBH6/5kDtYvtPYwhx2sXNxztbhkXErFk40Nw5l459NZx\nEeudxJBLoCkkSgYjhRcOZ/gvDVtWG7qyb6fAqgoisyAbk8K9LzxSim2S1nmT9vD8\n4B/t/VvwQBylc+ej8kRxMH7fquZLp7IXfmtBzyUqu6Dpbne+chG2\n-----END CERTIFICATE-----\n\n# certificate for ap-northeast-1\n-----BEGIN CERTIFICATE-----\nMIIDITCCAoqgAwIBAgIULgwDh7TiDrPPBJwscqDwiBHkEFQwDQYJKoZIhvcNAQEL\nBQAwXDELMAkGA1UEBhMCVVMxGTAXBgNVBAgTEFdhc2hpbmd0b24gU3RhdGUxEDAO\nBgNVBAcTB1NlYXR0bGUxIDAeBgNVBAoTF0FtYXpvbiBXZWIgU2VydmljZXMgTExD\nMB4XDTI0MDQyOTEyMjMxMFoXDTI5MDQyODEyMjMxMFowXDELMAkGA1UEBhMCVVMx\nGTAXBgNVBAgTEFdhc2hpbmd0b24gU3RhdGUxEDAOBgNVBAcTB1NlYXR0bGUxIDAe\nBgNVBAoTF0FtYXpvbiBXZWIgU2VydmljZXMgTExDMIGfMA0GCSqGSIb3DQEBAQUA\nA4GNADCBiQKBgQCHvRjf/0kStpJ248khtIaN8qkDN3tkw4VjvA9nvPl2anJO+eIB\nUqPfQG09kZlwpWpmyO8bGB2RWqWxCwuB/dcnIob6w420k9WY5C0IIGtDRNauN3ku\nvGXkw3HEnF0EjYr0pcyWUvByWY4KswZV42X7Y7XSS13hOIcL6NLA+H94/QIDAQAB\no4HfMIHcMAsGA1UdDwQEAwIHgDAdBgNVHQ4EFgQUJdbMCBXKtvCcWdwUUizvtUF2\nUTgwgZkGA1UdIwSBkTCBjoAUJdbMCBXKtvCcWdwUUizvtUF2UTihYKReMFwxCzAJ\nBgNVBAYTAlVTMRkwFwYDVQQIExBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHEwdT\nZWF0dGxlMSAwHgYDVQQKExdBbWF6b24gV2ViIFNlcnZpY2VzIExMQ4IULgwDh7Ti\nDrPPBJwscqDwiBHkEFQwEgYDVR0TAQH/BAgwBgEB/wIBADANBgkqhkiG9w0BAQsF\nAAOBgQBtjAglBde1t4F9EHCZOj4qnY6Gigy07Ou54i+lR77MhbpzE8V28Li9l+YT\nQMIn6SzJqU3/fIycIro1OVY1lHmaKYgPGSEZxBenSBHfzwDLRmC9oRp4QMe0BjOC\ngepj1lUoiN7OA6PtA+ycNlsP0oJvdBjhvayLiuM3tUfLTrgHbw==\n-----END CERTIFICATE-----\n\n# certificate for ap-northeast-2\n-----BEGIN CERTIFICATE-----\nMIIDITCCAoqgAwIBAgIUbBSn2UIO6vYk4iNWV0RPxJJtHlgwDQYJKoZIhvcNAQEL\nBQAwXDELMAkGA1UEBhMCVVMxGTAXBgNVBAgTEFdhc2hpbmd0b24gU3RhdGUxEDAO\nBgNVBAcTB1NlYXR0bGUxIDAeBgNVBAoTF0FtYXpvbiBXZWIgU2VydmljZXMgTExD\nMB4XDTI0MDQyOTEzMzg0NloXDTI5MDQyODEzMzg0NlowXDELMAkGA1UEBhMCVVMx\nGTAXBgNVBAgTEFdhc2hpbmd0b24gU3RhdGUxEDAOBgNVBAcTB1NlYXR0bGUxIDAe\nBgNVBAoTF0FtYXpvbiBXZWIgU2VydmljZXMgTExDMIGfMA0GCSqGSIb3DQEBAQUA\nA4GNADCBiQKBgQCHvRjf/0kStpJ248khtIaN8qkDN3tkw4VjvA9nvPl2anJO+eIB\nUqPfQG09kZlwpWpmyO8bGB2RWqWxCwuB/dcnIob6w420k9WY5C0IIGtDRNauN3ku\nvGXkw3HEnF0EjYr0pcyWUvByWY4KswZV42X7Y7XSS13hOIcL6NLA+H94/QIDAQAB\no4HfMIHcMAsGA1UdDwQEAwIHgDAdBgNVHQ4EFgQUJdbMCBXKtvCcWdwUUizvtUF2\nUTgwgZkGA1UdIwSBkTCBjoAUJdbMCBXKtvCcWdwUUizvtUF2UTihYKReMFwxCzAJ\nBgNVBAYTAlVTMRkwFwYDVQQIExBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHEwdT\nZWF0dGxlMSAwHgYDVQQKExdBbWF6b24gV2ViIFNlcnZpY2VzIExMQ4IUbBSn2UIO\n6vYk4iNWV0RPxJJtHlgwEgYDVR0TAQH/BAgwBgEB/wIBADANBgkqhkiG9w0BAQsF\nAAOBgQAmjTjalG8MGLqWTC2uYqEM8nzI3px1eo0ArvFRsyqQ3fgmWcQpxExqUqRy\nl3+2134Kv8dFab04Gut5wlfRtc2OwPKKicmv/IXGN+9bKFnQFjTqif08NIzrDZch\naFT/uvxrIiM+oN2YsHq66GUhO2+xVRXDXVxM/VObFgPERbJpyA==\n-----END CERTIFICATE-----\n\n# certificate for ap-northeast-3\n-----BEGIN CERTIFICATE-----\nMIICMzCCAZygAwIBAgIGAYPou9weMA0GCSqGSIb3DQEBBQUAMFwxCzAJBgNVBAYT\nAlVTMRkwFwYDVQQIDBBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHDAdTZWF0dGxl\nMSAwHgYDVQQKDBdBbWF6b24gV2ViIFNlcnZpY2VzIExMQzAgFw0yMjEwMTgwMTM2\nMDlaGA8yMjAxMTAxODAxMzYwOVowXDELMAkGA1UEBhMCVVMxGTAXBgNVBAgMEFdh\nc2hpbmd0b24gU3RhdGUxEDAOBgNVBAcMB1NlYXR0bGUxIDAeBgNVBAoMF0FtYXpv\nbiBXZWIgU2VydmljZXMgTExDMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDK\n1kIcG5Q6adBXQM75GldfTSiXl7tn54p10TnspI0ErDdb2B6q2Ji/v4XBVH13ZCMg\nqlRHMqV8AWI5iO6gFn2A9sN3AZXTMqwtZeiDdebq3k6Wt7ieYvpXTg0qvgsjQIov\nRZWaBDBJy9x8C2hW+w9lMQjFHkJ7Jy/PHCJ69EzebQIDAQABMA0GCSqGSIb3DQEB\nBQUAA4GBAGe9Snkz1A6rHBH6/5kDtYvtPYwhx2sXNxztbhkXErFk40Nw5l459NZx\nEeudxJBLoCkkSgYjhRcOZ/gvDVtWG7qyb6fAqgoisyAbk8K9LzxSim2S1nmT9vD8\n4B/t/VvwQBylc+ej8kRxMH7fquZLp7IXfmtBzyUqu6Dpbne+chG2\n-----END CERTIFICATE-----\n\n# certificate for ca-central-1\n-----BEGIN CERTIFICATE-----\nMIIDITCCAoqgAwIBAgIUIrLgixJJB5C4G8z6pZ5rB0JU2aQwDQYJKoZIhvcNAQEL\nBQAwXDELMAkGA1UEBhMCVVMxGTAXBgNVBAgTEFdhc2hpbmd0b24gU3RhdGUxEDAO\nBgNVBAcTB1NlYXR0bGUxIDAeBgNVBAoTF0FtYXpvbiBXZWIgU2VydmljZXMgTExD\nMB4XDTI0MDQyOTE1MzU0M1oXDTI5MDQyODE1MzU0M1owXDELMAkGA1UEBhMCVVMx\nGTAXBgNVBAgTEFdhc2hpbmd0b24gU3RhdGUxEDAOBgNVBAcTB1NlYXR0bGUxIDAe\nBgNVBAoTF0FtYXpvbiBXZWIgU2VydmljZXMgTExDMIGfMA0GCSqGSIb3DQEBAQUA\nA4GNADCBiQKBgQCHvRjf/0kStpJ248khtIaN8qkDN3tkw4VjvA9nvPl2anJO+eIB\nUqPfQG09kZlwpWpmyO8bGB2RWqWxCwuB/dcnIob6w420k9WY5C0IIGtDRNauN3ku\nvGXkw3HEnF0EjYr0pcyWUvByWY4KswZV42X7Y7XSS13hOIcL6NLA+H94/QIDAQAB\no4HfMIHcMAsGA1UdDwQEAwIHgDAdBgNVHQ4EFgQUJdbMCBXKtvCcWdwUUizvtUF2\nUTgwgZkGA1UdIwSBkTCBjoAUJdbMCBXKtvCcWdwUUizvtUF2UTihYKReMFwxCzAJ\nBgNVBAYTAlVTMRkwFwYDVQQIExBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHEwdT\nZWF0dGxlMSAwHgYDVQQKExdBbWF6b24gV2ViIFNlcnZpY2VzIExMQ4IUIrLgixJJ\nB5C4G8z6pZ5rB0JU2aQwEgYDVR0TAQH/BAgwBgEB/wIBADANBgkqhkiG9w0BAQsF\nAAOBgQBHiQJmzyFAaSYs8SpiRijIDZW2RIo7qBKb/pI3rqK6yOWDlPuMr6yNI81D\nIrKGGftg4Z+2KETYU4x76HSf0s//vfH3QA57qFaAwddhKYy4BhteFQl/Wex3xTlX\nLiwI07kwJvJy3mS6UfQ4HcvZy219tY+0iyOWrz/jVxwq7TOkCw==\n-----END CERTIFICATE-----\n\n# certificate for eu-west-1\n-----BEGIN CERTIFICATE-----\nMIIDITCCAoqgAwIBAgIUakDaQ1Zqy87Hy9ESXA1pFC116HkwDQYJKoZIhvcNAQEL\nBQAwXDELMAkGA1UEBhMCVVMxGTAXBgNVBAgTEFdhc2hpbmd0b24gU3RhdGUxEDAO\nBgNVBAcTB1NlYXR0bGUxIDAeBgNVBAoTF0FtYXpvbiBXZWIgU2VydmljZXMgTExD\nMB4XDTI0MDQyOTE2MTgxMFoXDTI5MDQyODE2MTgxMFowXDELMAkGA1UEBhMCVVMx\nGTAXBgNVBAgTEFdhc2hpbmd0b24gU3RhdGUxEDAOBgNVBAcTB1NlYXR0bGUxIDAe\nBgNVBAoTF0FtYXpvbiBXZWIgU2VydmljZXMgTExDMIGfMA0GCSqGSIb3DQEBAQUA\nA4GNADCBiQKBgQCHvRjf/0kStpJ248khtIaN8qkDN3tkw4VjvA9nvPl2anJO+eIB\nUqPfQG09kZlwpWpmyO8bGB2RWqWxCwuB/dcnIob6w420k9WY5C0IIGtDRNauN3ku\nvGXkw3HEnF0EjYr0pcyWUvByWY4KswZV42X7Y7XSS13hOIcL6NLA+H94/QIDAQAB\no4HfMIHcMAsGA1UdDwQEAwIHgDAdBgNVHQ4EFgQUJdbMCBXKtvCcWdwUUizvtUF2\nUTgwgZkGA1UdIwSBkTCBjoAUJdbMCBXKtvCcWdwUUizvtUF2UTihYKReMFwxCzAJ\nBgNVBAYTAlVTMRkwFwYDVQQIExBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHEwdT\nZWF0dGxlMSAwHgYDVQQKExdBbWF6b24gV2ViIFNlcnZpY2VzIExMQ4IUakDaQ1Zq\ny87Hy9ESXA1pFC116HkwEgYDVR0TAQH/BAgwBgEB/wIBADANBgkqhkiG9w0BAQsF\nAAOBgQADIkn/MqaLGPuK5+prZZ5Ox4bBZLPtreO2C7r0pqU2kPM2lVPyYYydkvP0\nlgSmmsErGu/oL9JNztDe2oCA+kNy17ehcsf8cw0uP861czNFKCeU8b7FgBbL+sIm\nqi33rAq6owWGi/5uEcfCR+JP7W+oSYVir5r/yDmWzx+BVH5S/g==\n-----END CERTIFICATE-----\n\n# certificate for eu-west-2\n-----BEGIN CERTIFICATE-----\nMIIDITCCAoqgAwIBAgIUCgCV/DPxYNND/swDgEKGiC5I+EwwDQYJKoZIhvcNAQEL\nBQAwXDELMAkGA1UEBhMCVVMxGTAXBgNVBAgTEFdhc2hpbmd0b24gU3RhdGUxEDAO\nBgNVBAcTB1NlYXR0bGUxIDAeBgNVBAoTF0FtYXpvbiBXZWIgU2VydmljZXMgTExD\nMB4XDTI0MDQyOTE2MjkxNFoXDTI5MDQyODE2MjkxNFowXDELMAkGA1UEBhMCVVMx\nGTAXBgNVBAgTEFdhc2hpbmd0b24gU3RhdGUxEDAOBgNVBAcTB1NlYXR0bGUxIDAe\nBgNVBAoTF0FtYXpvbiBXZWIgU2VydmljZXMgTExDMIGfMA0GCSqGSIb3DQEBAQUA\nA4GNADCBiQKBgQCHvRjf/0kStpJ248khtIaN8qkDN3tkw4VjvA9nvPl2anJO+eIB\nUqPfQG09kZlwpWpmyO8bGB2RWqWxCwuB/dcnIob6w420k9WY5C0IIGtDRNauN3ku\nvGXkw3HEnF0EjYr0pcyWUvByWY4KswZV42X7Y7XSS13hOIcL6NLA+H94/QIDAQAB\no4HfMIHcMAsGA1UdDwQEAwIHgDAdBgNVHQ4EFgQUJdbMCBXKtvCcWdwUUizvtUF2\nUTgwgZkGA1UdIwSBkTCBjoAUJdbMCBXKtvCcWdwUUizvtUF2UTihYKReMFwxCzAJ\nBgNVBAYTAlVTMRkwFwYDVQQIExBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHEwdT\nZWF0dGxlMSAwHgYDVQQKExdBbWF6b24gV2ViIFNlcnZpY2VzIExMQ4IUCgCV/DPx\nYNND/swDgEKGiC5I+EwwEgYDVR0TAQH/BAgwBgEB/wIBADANBgkqhkiG9w0BAQsF\nAAOBgQATPu/sOE2esNa4+XPEGKlEJSgqzyBSQLQc+VWo6FAJhGG9fp7D97jhHeLC\n5vwfmtTAfnGBxadfAOT3ASkxnOZhXtnRna460LtnNHm7ArCVgXKJo7uBn6ViXtFh\nuEEw4y6p9YaLQna+VC8Xtgw6WKq2JXuKzuhuNKSFaGGw9vRcHg==\n-----END CERTIFICATE-----\n\n# certificate for eu-west-3\n-----BEGIN CERTIFICATE-----\nMIIDITCCAoqgAwIBAgIUaC9fX57UDr6u1vBvsCsECKBZQyIwDQYJKoZIhvcNAQEL\nBQAwXDELMAkGA1UEBhMCVVMxGTAXBgNVBAgTEFdhc2hpbmd0b24gU3RhdGUxEDAO\nBgNVBAcTB1NlYXR0bGUxIDAeBgNVBAoTF0FtYXpvbiBXZWIgU2VydmljZXMgTExD\nMB4XDTI0MDQyOTE2MzczOFoXDTI5MDQyODE2MzczOFowXDELMAkGA1UEBhMCVVMx\nGTAXBgNVBAgTEFdhc2hpbmd0b24gU3RhdGUxEDAOBgNVBAcTB1NlYXR0bGUxIDAe\nBgNVBAoTF0FtYXpvbiBXZWIgU2VydmljZXMgTExDMIGfMA0GCSqGSIb3DQEBAQUA\nA4GNADCBiQKBgQCHvRjf/0kStpJ248khtIaN8qkDN3tkw4VjvA9nvPl2anJO+eIB\nUqPfQG09kZlwpWpmyO8bGB2RWqWxCwuB/dcnIob6w420k9WY5C0IIGtDRNauN3ku\nvGXkw3HEnF0EjYr0pcyWUvByWY4KswZV42X7Y7XSS13hOIcL6NLA+H94/QIDAQAB\no4HfMIHcMAsGA1UdDwQEAwIHgDAdBgNVHQ4EFgQUJdbMCBXKtvCcWdwUUizvtUF2\nUTgwgZkGA1UdIwSBkTCBjoAUJdbMCBXKtvCcWdwUUizvtUF2UTihYKReMFwxCzAJ\nBgNVBAYTAlVTMRkwFwYDVQQIExBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHEwdT\nZWF0dGxlMSAwHgYDVQQKExdBbWF6b24gV2ViIFNlcnZpY2VzIExMQ4IUaC9fX57U\nDr6u1vBvsCsECKBZQyIwEgYDVR0TAQH/BAgwBgEB/wIBADANBgkqhkiG9w0BAQsF\nAAOBgQCARv1bQEDaMEzYI0nPlu8GHcMXgmgA94HyrXhMMcaIlQwocGBs6VILGVhM\nTXP2r3JFaPEpmXSQNQHvGA13clKwAZbni8wtzv6qXb4L4muF34iQRHF0nYrEDoK7\nmMPR8+oXKKuPO/mv/XKo6XAV5DDERdSYHX5kkA2R9wtvyZjPnQ==\n-----END CERTIFICATE-----\n\n# certificate for eu-north-1\n-----BEGIN CERTIFICATE-----\nMIIDITCCAoqgAwIBAgIUN1c9U6U/xiVDFgJcYKZB4NkH1QEwDQYJKoZIhvcNAQEL\nBQAwXDELMAkGA1UEBhMCVVMxGTAXBgNVBAgTEFdhc2hpbmd0b24gU3RhdGUxEDAO\nBgNVBAcTB1NlYXR0bGUxIDAeBgNVBAoTF0FtYXpvbiBXZWIgU2VydmljZXMgTExD\nMB4XDTI0MDQyOTE2MDYwM1oXDTI5MDQyODE2MDYwM1owXDELMAkGA1UEBhMCVVMx\nGTAXBgNVBAgTEFdhc2hpbmd0b24gU3RhdGUxEDAOBgNVBAcTB1NlYXR0bGUxIDAe\nBgNVBAoTF0FtYXpvbiBXZWIgU2VydmljZXMgTExDMIGfMA0GCSqGSIb3DQEBAQUA\nA4GNADCBiQKBgQCHvRjf/0kStpJ248khtIaN8qkDN3tkw4VjvA9nvPl2anJO+eIB\nUqPfQG09kZlwpWpmyO8bGB2RWqWxCwuB/dcnIob6w420k9WY5C0IIGtDRNauN3ku\nvGXkw3HEnF0EjYr0pcyWUvByWY4KswZV42X7Y7XSS13hOIcL6NLA+H94/QIDAQAB\no4HfMIHcMAsGA1UdDwQEAwIHgDAdBgNVHQ4EFgQUJdbMCBXKtvCcWdwUUizvtUF2\nUTgwgZkGA1UdIwSBkTCBjoAUJdbMCBXKtvCcWdwUUizvtUF2UTihYKReMFwxCzAJ\nBgNVBAYTAlVTMRkwFwYDVQQIExBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHEwdT\nZWF0dGxlMSAwHgYDVQQKExdBbWF6b24gV2ViIFNlcnZpY2VzIExMQ4IUN1c9U6U/\nxiVDFgJcYKZB4NkH1QEwEgYDVR0TAQH/BAgwBgEB/wIBADANBgkqhkiG9w0BAQsF\nAAOBgQBTIQdoFSDRHkpqNPUbZ9WXR2O5v/9bpmHojMYZb3Hw46wsaRso7STiGGX/\ntRqjIkPUIXsdhZ3+7S/RmhFznmZc8e0bjU4n5vi9CJtQSt+1u4E17+V2bF+D3h/7\nwcfE0l3414Q8JaTDtfEf/aF3F0uyBvr4MDMd7mFvAMmDmBPSlA==\n-----END CERTIFICATE-----\n\n# certificate for sa-east-1\n-----BEGIN CERTIFICATE-----\nMIIDITCCAoqgAwIBAgIUX4Bh4MQ86Roh37VDRRX1MNOB3TcwDQYJKoZIhvcNAQEL\nBQAwXDELMAkGA1UEBhMCVVMxGTAXBgNVBAgTEFdhc2hpbmd0b24gU3RhdGUxEDAO\nBgNVBAcTB1NlYXR0bGUxIDAeBgNVBAoTF0FtYXpvbiBXZWIgU2VydmljZXMgTExD\nMB4XDTI0MDQyOTE2NDYwOVoXDTI5MDQyODE2NDYwOVowXDELMAkGA1UEBhMCVVMx\nGTAXBgNVBAgTEFdhc2hpbmd0b24gU3RhdGUxEDAOBgNVBAcTB1NlYXR0bGUxIDAe\nBgNVBAoTF0FtYXpvbiBXZWIgU2VydmljZXMgTExDMIGfMA0GCSqGSIb3DQEBAQUA\nA4GNADCBiQKBgQCHvRjf/0kStpJ248khtIaN8qkDN3tkw4VjvA9nvPl2anJO+eIB\nUqPfQG09kZlwpWpmyO8bGB2RWqWxCwuB/dcnIob6w420k9WY5C0IIGtDRNauN3ku\nvGXkw3HEnF0EjYr0pcyWUvByWY4KswZV42X7Y7XSS13hOIcL6NLA+H94/QIDAQAB\no4HfMIHcMAsGA1UdDwQEAwIHgDAdBgNVHQ4EFgQUJdbMCBXKtvCcWdwUUizvtUF2\nUTgwgZkGA1UdIwSBkTCBjoAUJdbMCBXKtvCcWdwUUizvtUF2UTihYKReMFwxCzAJ\nBgNVBAYTAlVTMRkwFwYDVQQIExBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHEwdT\nZWF0dGxlMSAwHgYDVQQKExdBbWF6b24gV2ViIFNlcnZpY2VzIExMQ4IUX4Bh4MQ8\n6Roh37VDRRX1MNOB3TcwEgYDVR0TAQH/BAgwBgEB/wIBADANBgkqhkiG9w0BAQsF\nAAOBgQBnhocfH6ZIX6F5K9+Y9V4HFk8vSaaKL5ytw/P5td1h9ej94KF3xkZ5fyjN\nURvGQv3kNmNJBoNarcP9I7JIMjsNPmVzqWawyCEGCZImoARxSS3Fc5EAs2PyBfcD\n9nCtzMTaKO09Xyq0wqXVYn1xJsE5d5yBDsGrzaTHKjxo61+ezQ==\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "authority/provisioner/aws_test.go",
    "content": "package provisioner\n\nimport (\n\t\"context\"\n\t\"crypto\"\n\t\"crypto/rand\"\n\t\"crypto/rsa\"\n\t\"crypto/sha256\"\n\t\"crypto/x509\"\n\t\"encoding/hex\"\n\t\"encoding/pem\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"go.step.sm/crypto/jose\"\n\n\t\"github.com/smallstep/assert\"\n\t\"github.com/smallstep/certificates/api/render\"\n)\n\nfunc TestAWS_Getters(t *testing.T) {\n\tp, err := generateAWS()\n\tassert.FatalError(t, err)\n\taud := \"aws/\" + p.Name\n\tif got := p.GetID(); got != aud {\n\t\tt.Errorf(\"AWS.GetID() = %v, want %v\", got, aud)\n\t}\n\tif got := p.GetName(); got != p.Name {\n\t\tt.Errorf(\"AWS.GetName() = %v, want %v\", got, p.Name)\n\t}\n\tif got := p.GetType(); got != TypeAWS {\n\t\tt.Errorf(\"AWS.GetType() = %v, want %v\", got, TypeAWS)\n\t}\n\tkid, key, ok := p.GetEncryptedKey()\n\tif kid != \"\" || key != \"\" || ok == true {\n\t\tt.Errorf(\"AWS.GetEncryptedKey() = (%v, %v, %v), want (%v, %v, %v)\",\n\t\t\tkid, key, ok, \"\", \"\", false)\n\t}\n}\n\nfunc TestAWS_GetTokenID(t *testing.T) {\n\tp1, srv, err := generateAWSWithServer()\n\tassert.FatalError(t, err)\n\tdefer srv.Close()\n\n\tp2, err := generateAWS()\n\tassert.FatalError(t, err)\n\tp2.Accounts = p1.Accounts\n\tp2.config = p1.config\n\tp2.DisableTrustOnFirstUse = true\n\n\tt1, err := p1.GetIdentityToken(\"foo.local\", \"https://ca.smallstep.com\")\n\tassert.FatalError(t, err)\n\t_, claims, err := parseAWSToken(t1)\n\tassert.FatalError(t, err)\n\tsum := sha256.Sum256([]byte(fmt.Sprintf(\"%s.%s\", p1.GetID(), claims.document.InstanceID)))\n\tw1 := strings.ToLower(hex.EncodeToString(sum[:]))\n\n\tt2, err := p2.GetIdentityToken(\"foo.local\", \"https://ca.smallstep.com\")\n\tassert.FatalError(t, err)\n\tsum = sha256.Sum256([]byte(t2))\n\tw2 := strings.ToLower(hex.EncodeToString(sum[:]))\n\n\ttype args struct {\n\t\ttoken string\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\taws     *AWS\n\t\targs    args\n\t\twant    string\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", p1, args{t1}, w1, false},\n\t\t{\"ok no TOFU\", p2, args{t2}, w2, false},\n\t\t{\"fail\", p1, args{\"bad-token\"}, \"\", true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := tt.aws.GetTokenID(tt.args.token)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"AWS.GetTokenID() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif got != tt.want {\n\t\t\t\tt.Errorf(\"AWS.GetTokenID() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestAWS_GetIdentityToken(t *testing.T) {\n\tp1, srv, err := generateAWSWithServer()\n\tassert.FatalError(t, err)\n\tdefer srv.Close()\n\n\tp2, err := generateAWS()\n\tassert.FatalError(t, err)\n\tp2.Accounts = p1.Accounts\n\tp2.config.identityURL = srv.URL + \"/bad-document\"\n\tp2.config.signatureURL = p1.config.signatureURL\n\tp2.config.tokenURL = p1.config.tokenURL\n\n\tp3, err := generateAWS()\n\tassert.FatalError(t, err)\n\tp3.Accounts = p1.Accounts\n\tp3.config.signatureURL = srv.URL\n\tp3.config.identityURL = p1.config.identityURL\n\tp3.config.tokenURL = p1.config.tokenURL\n\n\tp4, err := generateAWS()\n\tassert.FatalError(t, err)\n\tp4.Accounts = p1.Accounts\n\tp4.config.signatureURL = srv.URL + \"/bad-signature\"\n\tp4.config.identityURL = p1.config.identityURL\n\tp4.config.tokenURL = p1.config.tokenURL\n\n\tp5, err := generateAWS()\n\tassert.FatalError(t, err)\n\tp5.Accounts = p1.Accounts\n\tp5.config.identityURL = \"https://1234.1234.1234.1234\"\n\tp5.config.signatureURL = p1.config.signatureURL\n\tp5.config.tokenURL = p1.config.tokenURL\n\n\tp6, err := generateAWS()\n\tassert.FatalError(t, err)\n\tp6.Accounts = p1.Accounts\n\tp6.config.identityURL = p1.config.identityURL\n\tp6.config.signatureURL = \"https://1234.1234.1234.1234\"\n\tp6.config.tokenURL = p1.config.tokenURL\n\n\tp7, err := generateAWS()\n\tassert.FatalError(t, err)\n\tp7.Accounts = p1.Accounts\n\tp7.config.identityURL = srv.URL + \"/bad-json\"\n\tp7.config.signatureURL = p1.config.signatureURL\n\tp7.config.tokenURL = p1.config.tokenURL\n\n\tp8, err := generateAWS()\n\tassert.FatalError(t, err)\n\tp8.IMDSVersions = nil\n\tp8.Accounts = p1.Accounts\n\tp8.config = p1.config\n\n\tcaURL := \"https://ca.smallstep.com\"\n\tu, err := url.Parse(caURL)\n\tassert.FatalError(t, err)\n\n\ttype args struct {\n\t\tsubject string\n\t\tcaURL   string\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\taws     *AWS\n\t\targs    args\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", p1, args{\"foo.local\", caURL}, false},\n\t\t{\"ok no imds\", p8, args{\"foo.local\", caURL}, false},\n\t\t{\"fail ca url\", p1, args{\"foo.local\", \"://ca.smallstep.com\"}, true},\n\t\t{\"fail identityURL\", p2, args{\"foo.local\", caURL}, true},\n\t\t{\"fail signatureURL\", p3, args{\"foo.local\", caURL}, true},\n\t\t{\"fail signature\", p4, args{\"foo.local\", caURL}, true},\n\t\t{\"fail read identityURL\", p5, args{\"foo.local\", caURL}, true},\n\t\t{\"fail read signatureURL\", p6, args{\"foo.local\", caURL}, true},\n\t\t{\"fail unmarshal identityURL\", p7, args{\"foo.local\", caURL}, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := tt.aws.GetIdentityToken(tt.args.subject, tt.args.caURL)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"AWS.GetIdentityToken() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif tt.wantErr == false {\n\t\t\t\t_, c, err := parseAWSToken(got)\n\t\t\t\tif assert.NoError(t, err) {\n\t\t\t\t\tassert.Equals(t, awsIssuer, c.Issuer)\n\t\t\t\t\tassert.Equals(t, tt.args.subject, c.Subject)\n\t\t\t\t\tassert.Equals(t, jose.Audience{u.ResolveReference(&url.URL{Path: \"/1.0/sign\", Fragment: tt.aws.GetID()}).String()}, c.Audience)\n\t\t\t\t\tassert.Equals(t, tt.aws.Accounts[0], c.document.AccountID)\n\t\t\t\t\tfor _, crt := range tt.aws.config.certificates {\n\t\t\t\t\t\terr = crt.CheckSignature(tt.aws.config.signatureAlgorithm, c.Amazon.Document, c.Amazon.Signature)\n\t\t\t\t\t\tif err == nil {\n\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tassert.NoError(t, err)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestAWS_GetIdentityToken_V1Only(t *testing.T) {\n\taws, srv, err := generateAWSWithServerV1Only()\n\tassert.FatalError(t, err)\n\tdefer srv.Close()\n\n\tsubject := \"foo.local\"\n\tcaURL := \"https://ca.smallstep.com\"\n\tu, err := url.Parse(caURL)\n\tassert.Nil(t, err)\n\n\ttoken, err := aws.GetIdentityToken(subject, caURL)\n\tassert.Nil(t, err)\n\n\t_, c, err := parseAWSToken(token)\n\tif assert.NoError(t, err) {\n\t\tassert.Equals(t, awsIssuer, c.Issuer)\n\t\tassert.Equals(t, subject, c.Subject)\n\t\tassert.Equals(t, jose.Audience{u.ResolveReference(&url.URL{Path: \"/1.0/sign\", Fragment: aws.GetID()}).String()}, c.Audience)\n\t\tassert.Equals(t, aws.Accounts[0], c.document.AccountID)\n\t\tfor _, crt := range aws.config.certificates {\n\t\t\terr = crt.CheckSignature(aws.config.signatureAlgorithm, c.Amazon.Document, c.Amazon.Signature)\n\t\t\tif err == nil {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tassert.NoError(t, err)\n\t}\n}\n\nfunc TestAWS_GetIdentityToken_BadIDMS(t *testing.T) {\n\taws, srv, err := generateAWSWithServer()\n\n\taws.IMDSVersions = []string{\"bad\"}\n\n\tassert.FatalError(t, err)\n\tdefer srv.Close()\n\n\tsubject := \"foo.local\"\n\tcaURL := \"https://ca.smallstep.com\"\n\n\ttoken, err := aws.GetIdentityToken(subject, caURL)\n\tassert.Equals(t, token, \"\")\n\n\tbadIDMS := errors.New(\"bad: not a supported AWS Instance Metadata Service version\")\n\tassert.HasSuffix(t, err.Error(), badIDMS.Error())\n}\n\nfunc TestAWS_Init(t *testing.T) {\n\tconfig := Config{\n\t\tClaims: globalProvisionerClaims,\n\t}\n\tbadClaims := &Claims{\n\t\tDefaultTLSDur: &Duration{0},\n\t}\n\tzero := Duration{Duration: 0}\n\n\ttype fields struct {\n\t\tType                   string\n\t\tName                   string\n\t\tAccounts               []string\n\t\tDisableCustomSANs      bool\n\t\tDisableTrustOnFirstUse bool\n\t\tInstanceAge            Duration\n\t\tIMDSVersions           []string\n\t\tIIDRoots               string\n\t\tClaims                 *Claims\n\t}\n\ttype args struct {\n\t\tconfig Config\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tfields  fields\n\t\targs    args\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", fields{\"AWS\", \"name\", []string{\"account\"}, false, false, zero, []string{\"v1\", \"v2\"}, \"\", nil}, args{config}, false},\n\t\t{\"ok/v1\", fields{\"AWS\", \"name\", []string{\"account\"}, false, false, zero, []string{\"v1\"}, \"\", nil}, args{config}, false},\n\t\t{\"ok/v2\", fields{\"AWS\", \"name\", []string{\"account\"}, false, false, zero, []string{\"v2\"}, \"\", nil}, args{config}, false},\n\t\t{\"ok/empty\", fields{\"AWS\", \"name\", []string{\"account\"}, false, false, zero, []string{}, \"\", nil}, args{config}, false},\n\t\t{\"ok/duration\", fields{\"AWS\", \"name\", []string{\"account\"}, true, true, Duration{Duration: 1 * time.Minute}, []string{\"v1\", \"v2\"}, \"\", nil}, args{config}, false},\n\t\t{\"ok/cert\", fields{\"AWS\", \"name\", []string{\"account\"}, false, false, zero, []string{\"v1\", \"v2\"}, \"testdata/certs/aws.crt\", nil}, args{config}, false},\n\t\t{\"fail type \", fields{\"\", \"name\", []string{\"account\"}, false, false, zero, []string{\"v1\", \"v2\"}, \"\", nil}, args{config}, true},\n\t\t{\"fail name\", fields{\"AWS\", \"\", []string{\"account\"}, false, false, zero, []string{\"v1\", \"v2\"}, \"\", nil}, args{config}, true},\n\t\t{\"bad instance age\", fields{\"AWS\", \"name\", []string{\"account\"}, false, false, Duration{Duration: -1 * time.Minute}, []string{\"v1\", \"v2\"}, \"\", nil}, args{config}, true},\n\t\t{\"fail/imds\", fields{\"AWS\", \"name\", []string{\"account\"}, false, false, zero, []string{\"bad\"}, \"\", nil}, args{config}, true},\n\t\t{\"fail/missing\", fields{\"AWS\", \"name\", []string{\"account\"}, false, false, zero, []string{\"bad\"}, \"testdata/missing.crt\", nil}, args{config}, true},\n\t\t{\"fail/cert\", fields{\"AWS\", \"name\", []string{\"account\"}, false, false, zero, []string{\"bad\"}, \"testdata/certs/rsa.csr\", nil}, args{config}, true},\n\t\t{\"fail claims\", fields{\"AWS\", \"name\", []string{\"account\"}, false, false, zero, []string{\"v1\", \"v2\"}, \"\", badClaims}, args{config}, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tp := &AWS{\n\t\t\t\tType:                   tt.fields.Type,\n\t\t\t\tName:                   tt.fields.Name,\n\t\t\t\tAccounts:               tt.fields.Accounts,\n\t\t\t\tDisableCustomSANs:      tt.fields.DisableCustomSANs,\n\t\t\t\tDisableTrustOnFirstUse: tt.fields.DisableTrustOnFirstUse,\n\t\t\t\tInstanceAge:            tt.fields.InstanceAge,\n\t\t\t\tIMDSVersions:           tt.fields.IMDSVersions,\n\t\t\t\tIIDRoots:               tt.fields.IIDRoots,\n\t\t\t\tClaims:                 tt.fields.Claims,\n\t\t\t}\n\t\t\tif err := p.Init(tt.args.config); (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"AWS.Init() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestAWS_authorizeToken(t *testing.T) {\n\tblock, _ := pem.Decode([]byte(awsTestKey))\n\tif block == nil || block.Type != \"RSA PRIVATE KEY\" {\n\t\tt.Fatal(\"error decoding AWS key\")\n\t}\n\tkey, err := x509.ParsePKCS1PrivateKey(block.Bytes)\n\tassert.FatalError(t, err)\n\tbadKey, err := rsa.GenerateKey(rand.Reader, 2048)\n\tassert.FatalError(t, err)\n\n\ttype test struct {\n\t\tp     *AWS\n\t\ttoken string\n\t\terr   error\n\t\tcode  int\n\t}\n\ttests := map[string]func(*testing.T) test{\n\t\t\"fail/bad-token\": func(t *testing.T) test {\n\t\t\tp, err := generateAWS()\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tp:     p,\n\t\t\t\ttoken: \"foo\",\n\t\t\t\tcode:  http.StatusUnauthorized,\n\t\t\t\terr:   errors.New(\"aws.authorizeToken; error parsing aws token\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/cannot-validate-sig\": func(t *testing.T) test {\n\t\t\tp, err := generateAWS()\n\t\t\tassert.FatalError(t, err)\n\t\t\ttok, err := generateAWSToken(\n\t\t\t\tp, \"instance-id\", awsIssuer, p.GetID(), p.Accounts[0], \"instance-id\",\n\t\t\t\t\"127.0.0.1\", \"us-west-1\", time.Now(), badKey)\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tp:     p,\n\t\t\t\ttoken: tok,\n\t\t\t\tcode:  http.StatusUnauthorized,\n\t\t\t\terr:   errors.New(\"aws.authorizeToken; invalid aws token signature\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/empty-account-id\": func(t *testing.T) test {\n\t\t\tp, err := generateAWS()\n\t\t\tassert.FatalError(t, err)\n\t\t\ttok, err := generateAWSToken(\n\t\t\t\tp, \"instance-id\", awsIssuer, p.GetID(), \"\", \"instance-id\",\n\t\t\t\t\"127.0.0.1\", \"us-west-1\", time.Now(), key)\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tp:     p,\n\t\t\t\ttoken: tok,\n\t\t\t\tcode:  http.StatusUnauthorized,\n\t\t\t\terr:   errors.New(\"aws.authorizeToken; aws identity document accountId cannot be empty\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/empty-instance-id\": func(t *testing.T) test {\n\t\t\tp, err := generateAWS()\n\t\t\tassert.FatalError(t, err)\n\t\t\ttok, err := generateAWSToken(\n\t\t\t\tp, \"instance-id\", awsIssuer, p.GetID(), p.Accounts[0], \"\",\n\t\t\t\t\"127.0.0.1\", \"us-west-1\", time.Now(), key)\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tp:     p,\n\t\t\t\ttoken: tok,\n\t\t\t\tcode:  http.StatusUnauthorized,\n\t\t\t\terr:   errors.New(\"aws.authorizeToken; aws identity document instanceId cannot be empty\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/empty-private-ip\": func(t *testing.T) test {\n\t\t\tp, err := generateAWS()\n\t\t\tassert.FatalError(t, err)\n\t\t\ttok, err := generateAWSToken(\n\t\t\t\tp, \"instance-id\", awsIssuer, p.GetID(), p.Accounts[0], \"instance-id\",\n\t\t\t\t\"\", \"us-west-1\", time.Now(), key)\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tp:     p,\n\t\t\t\ttoken: tok,\n\t\t\t\tcode:  http.StatusUnauthorized,\n\t\t\t\terr:   errors.New(\"aws.authorizeToken; aws identity document privateIp cannot be empty\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/empty-region\": func(t *testing.T) test {\n\t\t\tp, err := generateAWS()\n\t\t\tassert.FatalError(t, err)\n\t\t\ttok, err := generateAWSToken(\n\t\t\t\tp, \"instance-id\", awsIssuer, p.GetID(), p.Accounts[0], \"instance-id\",\n\t\t\t\t\"127.0.0.1\", \"\", time.Now(), key)\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tp:     p,\n\t\t\t\ttoken: tok,\n\t\t\t\tcode:  http.StatusUnauthorized,\n\t\t\t\terr:   errors.New(\"aws.authorizeToken; aws identity document region cannot be empty\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/invalid-token-issuer\": func(t *testing.T) test {\n\t\t\tp, err := generateAWS()\n\t\t\tassert.FatalError(t, err)\n\t\t\ttok, err := generateAWSToken(\n\t\t\t\tp, \"instance-id\", \"bad-issuer\", p.GetID(), p.Accounts[0], \"instance-id\",\n\t\t\t\t\"127.0.0.1\", \"us-west-1\", time.Now(), key)\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tp:     p,\n\t\t\t\ttoken: tok,\n\t\t\t\tcode:  http.StatusUnauthorized,\n\t\t\t\terr:   errors.New(\"aws.authorizeToken; invalid aws token\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/invalid-audience\": func(t *testing.T) test {\n\t\t\tp, err := generateAWS()\n\t\t\tassert.FatalError(t, err)\n\t\t\ttok, err := generateAWSToken(\n\t\t\t\tp, \"instance-id\", awsIssuer, \"bad-audience\", p.Accounts[0], \"instance-id\",\n\t\t\t\t\"127.0.0.1\", \"us-west-1\", time.Now(), key)\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tp:     p,\n\t\t\t\ttoken: tok,\n\t\t\t\tcode:  http.StatusUnauthorized,\n\t\t\t\terr:   errors.New(\"aws.authorizeToken; invalid token - invalid audience claim (aud)\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/invalid-subject-disabled-custom-SANs\": func(t *testing.T) test {\n\t\t\tp, err := generateAWS()\n\t\t\tassert.FatalError(t, err)\n\t\t\tp.DisableCustomSANs = true\n\t\t\ttok, err := generateAWSToken(\n\t\t\t\tp, \"foo\", awsIssuer, p.GetID(), p.Accounts[0], \"instance-id\",\n\t\t\t\t\"127.0.0.1\", \"us-west-1\", time.Now(), key)\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tp:     p,\n\t\t\t\ttoken: tok,\n\t\t\t\tcode:  http.StatusUnauthorized,\n\t\t\t\terr:   errors.New(\"aws.authorizeToken; invalid token - invalid subject claim (sub)\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/invalid-account-id\": func(t *testing.T) test {\n\t\t\tp, err := generateAWS()\n\t\t\tassert.FatalError(t, err)\n\t\t\ttok, err := generateAWSToken(\n\t\t\t\tp, \"instance-id\", awsIssuer, p.GetID(), \"foo\", \"instance-id\",\n\t\t\t\t\"127.0.0.1\", \"us-west-1\", time.Now(), key)\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tp:     p,\n\t\t\t\ttoken: tok,\n\t\t\t\tcode:  http.StatusUnauthorized,\n\t\t\t\terr:   errors.New(\"aws.authorizeToken; invalid aws identity document - accountId is not valid\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/instance-age\": func(t *testing.T) test {\n\t\t\tp, err := generateAWS()\n\t\t\tassert.FatalError(t, err)\n\t\t\tp.InstanceAge = Duration{1 * time.Minute}\n\t\t\ttok, err := generateAWSToken(\n\t\t\t\tp, \"instance-id\", awsIssuer, p.GetID(), p.Accounts[0], \"instance-id\",\n\t\t\t\t\"127.0.0.1\", \"us-west-1\", time.Now().Add(-1*time.Minute), key)\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tp:     p,\n\t\t\t\ttoken: tok,\n\t\t\t\tcode:  http.StatusUnauthorized,\n\t\t\t\terr:   errors.New(\"aws.authorizeToken; aws identity document pendingTime is too old\"),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tp, err := generateAWS()\n\t\t\tassert.FatalError(t, err)\n\t\t\ttok, err := generateAWSToken(\n\t\t\t\tp, \"instance-id\", awsIssuer, p.GetID(), p.Accounts[0], \"instance-id\",\n\t\t\t\t\"127.0.0.1\", \"us-west-1\", time.Now(), key)\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tp:     p,\n\t\t\t\ttoken: tok,\n\t\t\t}\n\t\t},\n\t\t\"ok/identityCert\": func(t *testing.T) test {\n\t\t\tp, err := generateAWS()\n\t\t\tp.IIDRoots = \"testdata/certs/aws-test.crt\"\n\t\t\tassert.FatalError(t, err)\n\t\t\ttok, err := generateAWSToken(\n\t\t\t\tp, \"instance-id\", awsIssuer, p.GetID(), p.Accounts[0], \"instance-id\",\n\t\t\t\t\"127.0.0.1\", \"us-west-1\", time.Now(), key)\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tp:     p,\n\t\t\t\ttoken: tok,\n\t\t\t}\n\t\t},\n\t\t\"ok/identityCert2\": func(t *testing.T) test {\n\t\t\tp, err := generateAWS()\n\t\t\tp.IIDRoots = \"testdata/certs/aws.crt\"\n\t\t\tassert.FatalError(t, err)\n\t\t\ttok, err := generateAWSToken(\n\t\t\t\tp, \"instance-id\", awsIssuer, p.GetID(), p.Accounts[0], \"instance-id\",\n\t\t\t\t\"127.0.0.1\", \"us-west-1\", time.Now(), key)\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tp:     p,\n\t\t\t\ttoken: tok,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, tt := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ttc := tt(t)\n\t\t\tif claims, err := tc.p.authorizeToken(tc.token); err != nil {\n\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\tvar sc render.StatusCodedError\n\t\t\t\t\tassert.Fatal(t, errors.As(err, &sc), \"error does not implement StatusCodedError interface\")\n\t\t\t\t\tassert.Equals(t, sc.StatusCode(), tc.code)\n\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.err.Error())\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif assert.Nil(t, tc.err) && assert.NotNil(t, claims) {\n\t\t\t\t\tassert.Equals(t, claims.Subject, \"instance-id\")\n\t\t\t\t\tassert.Equals(t, claims.Issuer, awsIssuer)\n\t\t\t\t\tassert.NotNil(t, claims.Amazon)\n\n\t\t\t\t\taud, err := generateSignAudience(\"https://ca.smallstep.com\", tc.p.GetID())\n\t\t\t\t\tassert.FatalError(t, err)\n\t\t\t\t\tassert.Equals(t, claims.Audience[0], aud)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestAWS_AuthorizeSign(t *testing.T) {\n\tp1, srv, err := generateAWSWithServer()\n\tassert.FatalError(t, err)\n\tdefer srv.Close()\n\n\tp2, err := generateAWS()\n\tassert.FatalError(t, err)\n\tp2.Accounts = p1.Accounts\n\tp2.config = p1.config\n\tp2.DisableCustomSANs = true\n\tp2.InstanceAge = Duration{1 * time.Minute}\n\n\tp3, err := generateAWS()\n\tassert.FatalError(t, err)\n\tp3.config = p1.config\n\n\tt1, err := p1.GetIdentityToken(\"foo.local\", \"https://ca.smallstep.com\")\n\tassert.FatalError(t, err)\n\tt2, err := p2.GetIdentityToken(\"instance-id\", \"https://ca.smallstep.com\")\n\tassert.FatalError(t, err)\n\tassert.FatalError(t, err)\n\tt3, err := p3.GetIdentityToken(\"foo.local\", \"https://ca.smallstep.com\")\n\tassert.FatalError(t, err)\n\n\t// Alternative common names with DisableCustomSANs = true\n\tt2PrivateIP, err := p2.GetIdentityToken(\"127.0.0.1\", \"https://ca.smallstep.com\")\n\tassert.FatalError(t, err)\n\tt2Hostname, err := p2.GetIdentityToken(\"ip-127-0-0-1.us-west-1.compute.internal\", \"https://ca.smallstep.com\")\n\tassert.FatalError(t, err)\n\n\tblock, _ := pem.Decode([]byte(awsTestKey))\n\tif block == nil || block.Type != \"RSA PRIVATE KEY\" {\n\t\tt.Fatal(\"error decoding AWS key\")\n\t}\n\tkey, err := x509.ParsePKCS1PrivateKey(block.Bytes)\n\tassert.FatalError(t, err)\n\n\tbadKey, err := rsa.GenerateKey(rand.Reader, 2048)\n\tassert.FatalError(t, err)\n\n\tt4, err := generateAWSToken(\n\t\tp1, \"instance-id\", awsIssuer, p1.GetID(), p1.Accounts[0], \"instance-id\",\n\t\t\"127.0.0.1\", \"us-west-1\", time.Now(), key)\n\tassert.FatalError(t, err)\n\tfailSubject, err := generateAWSToken(\n\t\tp2, \"bad-subject\", awsIssuer, p2.GetID(), p2.Accounts[0], \"instance-id\",\n\t\t\"127.0.0.1\", \"us-west-1\", time.Now(), key)\n\tassert.FatalError(t, err)\n\tfailIssuer, err := generateAWSToken(\n\t\tp1, \"instance-id\", \"bad-issuer\", p1.GetID(), p1.Accounts[0], \"instance-id\",\n\t\t\"127.0.0.1\", \"us-west-1\", time.Now(), key)\n\tassert.FatalError(t, err)\n\tfailAudience, err := generateAWSToken(\n\t\tp1, \"instance-id\", awsIssuer, \"bad-audience\", p1.Accounts[0], \"instance-id\",\n\t\t\"127.0.0.1\", \"us-west-1\", time.Now(), key)\n\tassert.FatalError(t, err)\n\tfailAccount, err := generateAWSToken(\n\t\tp1, \"instance-id\", awsIssuer, p1.GetID(), \"\", \"instance-id\",\n\t\t\"127.0.0.1\", \"us-west-1\", time.Now(), key)\n\tassert.FatalError(t, err)\n\tfailInstanceID, err := generateAWSToken(\n\t\tp1, \"instance-id\", awsIssuer, p1.GetID(), p1.Accounts[0], \"\",\n\t\t\"127.0.0.1\", \"us-west-1\", time.Now(), key)\n\tassert.FatalError(t, err)\n\tfailPrivateIP, err := generateAWSToken(\n\t\tp1, \"instance-id\", awsIssuer, p1.GetID(), p1.Accounts[0], \"instance-id\",\n\t\t\"\", \"us-west-1\", time.Now(), key)\n\tassert.FatalError(t, err)\n\tfailRegion, err := generateAWSToken(\n\t\tp1, \"instance-id\", awsIssuer, p1.GetID(), p1.Accounts[0], \"instance-id\",\n\t\t\"127.0.0.1\", \"\", time.Now(), key)\n\tassert.FatalError(t, err)\n\tfailExp, err := generateAWSToken(\n\t\tp1, \"instance-id\", awsIssuer, p1.GetID(), p1.Accounts[0], \"instance-id\",\n\t\t\"127.0.0.1\", \"us-west-1\", time.Now().Add(-360*time.Second), key)\n\tassert.FatalError(t, err)\n\tfailNbf, err := generateAWSToken(\n\t\tp1, \"instance-id\", awsIssuer, p1.GetID(), p1.Accounts[0], \"instance-id\",\n\t\t\"127.0.0.1\", \"us-west-1\", time.Now().Add(360*time.Second), key)\n\tassert.FatalError(t, err)\n\tfailKey, err := generateAWSToken(\n\t\tp1, \"instance-id\", awsIssuer, p1.GetID(), p1.Accounts[0], \"instance-id\",\n\t\t\"127.0.0.1\", \"us-west-1\", time.Now(), badKey)\n\tassert.FatalError(t, err)\n\tfailInstanceAge, err := generateAWSToken(\n\t\tp2, \"instance-id\", awsIssuer, p2.GetID(), p2.Accounts[0], \"instance-id\",\n\t\t\"127.0.0.1\", \"us-west-1\", time.Now().Add(-1*time.Minute), key)\n\tassert.FatalError(t, err)\n\n\ttype args struct {\n\t\ttoken, cn string\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\taws     *AWS\n\t\targs    args\n\t\twantLen int\n\t\tcode    int\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", p1, args{t1, \"foo.local\"}, 9, http.StatusOK, false},\n\t\t{\"ok\", p2, args{t2, \"instance-id\"}, 13, http.StatusOK, false},\n\t\t{\"ok\", p2, args{t2Hostname, \"ip-127-0-0-1.us-west-1.compute.internal\"}, 13, http.StatusOK, false},\n\t\t{\"ok\", p2, args{t2PrivateIP, \"127.0.0.1\"}, 13, http.StatusOK, false},\n\t\t{\"ok\", p1, args{t4, \"instance-id\"}, 9, http.StatusOK, false},\n\t\t{\"fail account\", p3, args{token: t3}, 0, http.StatusUnauthorized, true},\n\t\t{\"fail token\", p1, args{token: \"token\"}, 0, http.StatusUnauthorized, true},\n\t\t{\"fail subject\", p1, args{token: failSubject}, 0, http.StatusUnauthorized, true},\n\t\t{\"fail issuer\", p1, args{token: failIssuer}, 0, http.StatusUnauthorized, true},\n\t\t{\"fail audience\", p1, args{token: failAudience}, 0, http.StatusUnauthorized, true},\n\t\t{\"fail account\", p1, args{token: failAccount}, 0, http.StatusUnauthorized, true},\n\t\t{\"fail instanceID\", p1, args{token: failInstanceID}, 0, http.StatusUnauthorized, true},\n\t\t{\"fail privateIP\", p1, args{token: failPrivateIP}, 0, http.StatusUnauthorized, true},\n\t\t{\"fail region\", p1, args{token: failRegion}, 0, http.StatusUnauthorized, true},\n\t\t{\"fail exp\", p1, args{token: failExp}, 0, http.StatusUnauthorized, true},\n\t\t{\"fail nbf\", p1, args{token: failNbf}, 0, http.StatusUnauthorized, true},\n\t\t{\"fail key\", p1, args{token: failKey}, 0, http.StatusUnauthorized, true},\n\t\t{\"fail instance age\", p2, args{token: failInstanceAge}, 0, http.StatusUnauthorized, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tctx := NewContextWithMethod(context.Background(), SignMethod)\n\t\t\tswitch got, err := tt.aws.AuthorizeSign(ctx, tt.args.token); {\n\t\t\tcase (err != nil) != tt.wantErr:\n\t\t\t\tt.Errorf(\"AWS.AuthorizeSign() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\tcase err != nil:\n\t\t\t\tvar sc render.StatusCodedError\n\t\t\t\tassert.Fatal(t, errors.As(err, &sc), \"error does not implement StatusCodedError interface\")\n\t\t\t\tassert.Equals(t, sc.StatusCode(), tt.code)\n\t\t\tdefault:\n\t\t\t\tassert.Equals(t, tt.wantLen, len(got))\n\t\t\t\tfor _, o := range got {\n\t\t\t\t\tswitch v := o.(type) {\n\t\t\t\t\tcase *AWS:\n\t\t\t\t\tcase certificateOptionsFunc:\n\t\t\t\t\tcase *provisionerExtensionOption:\n\t\t\t\t\t\tassert.Equals(t, v.Type, TypeAWS)\n\t\t\t\t\t\tassert.Equals(t, v.Name, tt.aws.GetName())\n\t\t\t\t\t\tassert.Equals(t, v.CredentialID, tt.aws.Accounts[0])\n\t\t\t\t\t\tassert.Len(t, 2, v.KeyValuePairs)\n\t\t\t\t\tcase profileDefaultDuration:\n\t\t\t\t\t\tassert.Equals(t, time.Duration(v), tt.aws.ctl.Claimer.DefaultTLSCertDuration())\n\t\t\t\t\tcase commonNameValidator:\n\t\t\t\t\t\tassert.Equals(t, string(v), tt.args.cn)\n\t\t\t\t\tcase defaultPublicKeyValidator:\n\t\t\t\t\tcase *validityValidator:\n\t\t\t\t\t\tassert.Equals(t, v.min, tt.aws.ctl.Claimer.MinTLSCertDuration())\n\t\t\t\t\t\tassert.Equals(t, v.max, tt.aws.ctl.Claimer.MaxTLSCertDuration())\n\t\t\t\t\tcase ipAddressesValidator:\n\t\t\t\t\t\tassert.Equals(t, []net.IP(v), []net.IP{net.ParseIP(\"127.0.0.1\")})\n\t\t\t\t\tcase emailAddressesValidator:\n\t\t\t\t\t\tassert.Equals(t, v, nil)\n\t\t\t\t\tcase *urisValidator:\n\t\t\t\t\t\tassert.Equals(t, v.uris, nil)\n\t\t\t\t\t\tassert.Equals(t, MethodFromContext(v.ctx), SignMethod)\n\t\t\t\t\tcase dnsNamesSubsetValidator:\n\t\t\t\t\t\tassert.Equals(t, []string(v), []string{\"ip-127-0-0-1.us-west-1.compute.internal\"})\n\t\t\t\t\tcase *x509NamePolicyValidator:\n\t\t\t\t\t\tassert.Equals(t, nil, v.policyEngine)\n\t\t\t\t\tcase *WebhookController:\n\t\t\t\t\t\tassert.Len(t, 0, v.webhooks)\n\t\t\t\t\tdefault:\n\t\t\t\t\t\tassert.FatalError(t, fmt.Errorf(\"unexpected sign option of type %T\", v))\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestAWS_AuthorizeSSHSign(t *testing.T) {\n\ttm, fn := mockNow()\n\tdefer fn()\n\n\tp1, srv, err := generateAWSWithServer()\n\tassert.FatalError(t, err)\n\tp1.DisableCustomSANs = true\n\tdefer srv.Close()\n\n\tp2, err := generateAWS()\n\tassert.FatalError(t, err)\n\tp2.Accounts = p1.Accounts\n\tp2.config = p1.config\n\tp2.DisableCustomSANs = false\n\n\tp3, err := generateAWS()\n\tassert.FatalError(t, err)\n\t// disable sshCA\n\tdisable := false\n\tp3.Claims = &Claims{EnableSSHCA: &disable}\n\tp3.ctl.Claimer, err = NewClaimer(p3.Claims, globalProvisionerClaims)\n\tassert.FatalError(t, err)\n\n\tt1, err := p1.GetIdentityToken(\"127.0.0.1\", \"https://ca.smallstep.com\")\n\tassert.FatalError(t, err)\n\n\tt2, err := p2.GetIdentityToken(\"foo.local\", \"https://ca.smallstep.com\")\n\tassert.FatalError(t, err)\n\n\tkey, err := generateJSONWebKey()\n\tassert.FatalError(t, err)\n\n\tsigner, err := generateJSONWebKey()\n\tassert.FatalError(t, err)\n\n\tpub := key.Public().Key\n\trsa2048, err := rsa.GenerateKey(rand.Reader, 2048)\n\tassert.FatalError(t, err)\n\t//nolint:gosec // tests minimum size of the key\n\trsa1024, err := rsa.GenerateKey(rand.Reader, 1024)\n\tassert.FatalError(t, err)\n\n\thostDuration := p1.ctl.Claimer.DefaultHostSSHCertDuration()\n\texpectedHostOptions := &SignSSHOptions{\n\t\tCertType: \"host\", Principals: []string{\"127.0.0.1\", \"ip-127-0-0-1.us-west-1.compute.internal\"},\n\t\tValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(hostDuration)),\n\t}\n\texpectedHostOptionsIP := &SignSSHOptions{\n\t\tCertType: \"host\", Principals: []string{\"127.0.0.1\"},\n\t\tValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(hostDuration)),\n\t}\n\texpectedHostOptionsHostname := &SignSSHOptions{\n\t\tCertType: \"host\", Principals: []string{\"ip-127-0-0-1.us-west-1.compute.internal\"},\n\t\tValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(hostDuration)),\n\t}\n\texpectedCustomOptions := &SignSSHOptions{\n\t\tCertType: \"host\", Principals: []string{\"foo.local\"},\n\t\tValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(hostDuration)),\n\t}\n\n\ttype args struct {\n\t\ttoken   string\n\t\tsshOpts SignSSHOptions\n\t\tkey     interface{}\n\t}\n\ttests := []struct {\n\t\tname        string\n\t\taws         *AWS\n\t\targs        args\n\t\texpected    *SignSSHOptions\n\t\tcode        int\n\t\twantErr     bool\n\t\twantSignErr bool\n\t}{\n\t\t{\"ok\", p1, args{t1, SignSSHOptions{}, pub}, expectedHostOptions, http.StatusOK, false, false},\n\t\t{\"ok-rsa2048\", p1, args{t1, SignSSHOptions{}, rsa2048.Public()}, expectedHostOptions, http.StatusOK, false, false},\n\t\t{\"ok-type\", p1, args{t1, SignSSHOptions{CertType: \"host\"}, pub}, expectedHostOptions, http.StatusOK, false, false},\n\t\t{\"ok-principals\", p1, args{t1, SignSSHOptions{Principals: []string{\"127.0.0.1\", \"ip-127-0-0-1.us-west-1.compute.internal\"}}, pub}, expectedHostOptions, http.StatusOK, false, false},\n\t\t{\"ok-principal-ip\", p1, args{t1, SignSSHOptions{Principals: []string{\"127.0.0.1\"}}, pub}, expectedHostOptionsIP, http.StatusOK, false, false},\n\t\t{\"ok-principal-hostname\", p1, args{t1, SignSSHOptions{Principals: []string{\"ip-127-0-0-1.us-west-1.compute.internal\"}}, pub}, expectedHostOptionsHostname, http.StatusOK, false, false},\n\t\t{\"ok-options\", p1, args{t1, SignSSHOptions{CertType: \"host\", Principals: []string{\"127.0.0.1\", \"ip-127-0-0-1.us-west-1.compute.internal\"}}, pub}, expectedHostOptions, http.StatusOK, false, false},\n\t\t{\"ok-custom\", p2, args{t2, SignSSHOptions{Principals: []string{\"foo.local\"}}, pub}, expectedCustomOptions, http.StatusOK, false, false},\n\t\t{\"fail-rsa1024\", p1, args{t1, SignSSHOptions{}, rsa1024.Public()}, expectedHostOptions, http.StatusOK, false, true},\n\t\t{\"fail-type\", p1, args{t1, SignSSHOptions{CertType: \"user\"}, pub}, nil, http.StatusOK, false, true},\n\t\t{\"fail-principal\", p1, args{t1, SignSSHOptions{Principals: []string{\"smallstep.com\"}}, pub}, nil, http.StatusOK, false, true},\n\t\t{\"fail-extra-principal\", p1, args{t1, SignSSHOptions{Principals: []string{\"127.0.0.1\", \"ip-127-0-0-1.us-west-1.compute.internal\", \"smallstep.com\"}}, pub}, nil, http.StatusOK, false, true},\n\t\t{\"fail-sshCA-disabled\", p3, args{\"foo\", SignSSHOptions{}, pub}, expectedHostOptions, http.StatusUnauthorized, true, false},\n\t\t{\"fail-invalid-token\", p1, args{\"foo\", SignSSHOptions{}, pub}, expectedHostOptions, http.StatusUnauthorized, true, false},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := tt.aws.AuthorizeSSHSign(context.Background(), tt.args.token)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"AWS.AuthorizeSSHSign() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif err != nil {\n\t\t\t\tvar sc render.StatusCodedError\n\t\t\t\tassert.Fatal(t, errors.As(err, &sc), \"error does not implement StatusCodedError interface\")\n\t\t\t\tassert.Equals(t, sc.StatusCode(), tt.code)\n\t\t\t\tassert.Nil(t, got)\n\t\t\t} else if assert.NotNil(t, got) {\n\t\t\t\tcert, err := signSSHCertificate(tt.args.key, tt.args.sshOpts, got, signer.Key.(crypto.Signer))\n\t\t\t\tif (err != nil) != tt.wantSignErr {\n\t\t\t\t\tt.Errorf(\"SignSSH error = %v, wantSignErr %v\", err, tt.wantSignErr)\n\t\t\t\t} else {\n\t\t\t\t\tif tt.wantSignErr {\n\t\t\t\t\t\tassert.Nil(t, cert)\n\t\t\t\t\t} else {\n\t\t\t\t\t\tassert.NoError(t, validateSSHCertificate(cert, tt.expected))\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestAWS_AuthorizeRenew(t *testing.T) {\n\tnow := time.Now().Truncate(time.Second)\n\tp1, err := generateAWS()\n\tassert.FatalError(t, err)\n\tp2, err := generateAWS()\n\tassert.FatalError(t, err)\n\n\t// disable renewal\n\tdisable := true\n\tp2.Claims = &Claims{DisableRenewal: &disable}\n\tp2.ctl.Claimer, err = NewClaimer(p2.Claims, globalProvisionerClaims)\n\tassert.FatalError(t, err)\n\n\ttype args struct {\n\t\tcert *x509.Certificate\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\taws     *AWS\n\t\targs    args\n\t\tcode    int\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", p1, args{&x509.Certificate{\n\t\t\tNotBefore: now,\n\t\t\tNotAfter:  now.Add(time.Hour),\n\t\t}}, http.StatusOK, false},\n\t\t{\"fail/renew-disabled\", p2, args{&x509.Certificate{\n\t\t\tNotBefore: now,\n\t\t\tNotAfter:  now.Add(time.Hour),\n\t\t}}, http.StatusUnauthorized, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif err := tt.aws.AuthorizeRenew(context.Background(), tt.args.cert); (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"AWS.AuthorizeRenew() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t} else if err != nil {\n\t\t\t\tvar sc render.StatusCodedError\n\t\t\t\tassert.Fatal(t, errors.As(err, &sc), \"error does not implement StatusCodedError interface\")\n\t\t\t\tassert.Equals(t, sc.StatusCode(), tt.code)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestAWS_HardcodedCertificates(t *testing.T) {\n\tcertBytes := []byte(awsCertificate)\n\n\tvar certs []*x509.Certificate\n\tfor len(certBytes) > 0 {\n\t\tvar block *pem.Block\n\t\tblock, certBytes = pem.Decode(certBytes)\n\t\tif block == nil {\n\t\t\tbreak\n\t\t}\n\t\tif block.Type != \"CERTIFICATE\" || len(block.Headers) != 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\tcert, err := x509.ParseCertificate(block.Bytes)\n\t\tassert.FatalError(t, err)\n\n\t\t// check that the certificate is not expired\n\t\tassert.True(t, cert.NotAfter.After(time.Now()))\n\t\tcerts = append(certs, cert)\n\t}\n\tassert.Len(t, 33, certs, \"expected 33 certificates in aws_certificates.pem, but got %d\", len(certs))\n}\n"
  },
  {
    "path": "authority/provisioner/azure.go",
    "content": "package provisioner\n\nimport (\n\t\"context\"\n\t\"crypto/sha256\"\n\t\"crypto/x509\"\n\t\"encoding/hex\"\n\t\"encoding/json\"\n\t\"io\"\n\t\"net/http\"\n\t\"regexp\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n\n\t\"github.com/smallstep/linkedca\"\n\t\"go.step.sm/crypto/jose\"\n\t\"go.step.sm/crypto/sshutil\"\n\t\"go.step.sm/crypto/x509util\"\n\n\t\"github.com/smallstep/certificates/errs\"\n\t\"github.com/smallstep/certificates/webhook\"\n)\n\n// azureOIDCBaseURL is the base discovery url for Microsoft Azure tokens.\nconst azureOIDCBaseURL = \"https://login.microsoftonline.com\"\n\n//nolint:gosec // azureIdentityTokenURL is the URL to get the identity token for an instance.\nconst azureIdentityTokenURL = \"http://169.254.169.254/metadata/identity/oauth2/token\"\n\nconst azureIdentityTokenAPIVersion = \"2018-02-01\"\n\n// azureInstanceComputeURL is the URL to get the instance compute metadata.\nconst azureInstanceComputeURL = \"http://169.254.169.254/metadata/instance/compute/azEnvironment\"\n\n// azureDefaultAudience is the default audience used.\nconst azureDefaultAudience = \"https://management.azure.com/\"\n\n// azureXMSMirIDRegExp is the regular expression used to parse the xms_mirid claim.\n// Using case insensitive as resourceGroups appears as resourcegroups.\nvar azureXMSMirIDRegExp = regexp.MustCompile(`(?i)^/subscriptions/([^/]+)/resourceGroups/([^/]+)/providers/Microsoft.(Compute/virtualMachines|ManagedIdentity/userAssignedIdentities)/([^/]+)$`)\n\n// azureEnvironments is the list of all Azure environments.\nvar azureEnvironments = map[string]string{\n\t\"AzurePublicCloud\":       \"https://management.azure.com/\",\n\t\"AzureCloud\":             \"https://management.azure.com/\",\n\t\"AzureUSGovernmentCloud\": \"https://management.usgovcloudapi.net/\",\n\t\"AzureUSGovernment\":      \"https://management.usgovcloudapi.net/\",\n\t\"AzureChinaCloud\":        \"https://management.chinacloudapi.cn/\",\n\t\"AzureGermanCloud\":       \"https://management.microsoftazure.de/\",\n}\n\ntype azureConfig struct {\n\toidcDiscoveryURL   string\n\tidentityTokenURL   string\n\tinstanceComputeURL string\n}\n\nfunc newAzureConfig(tenantID string) *azureConfig {\n\treturn &azureConfig{\n\t\toidcDiscoveryURL:   azureOIDCBaseURL + \"/\" + tenantID + \"/.well-known/openid-configuration\",\n\t\tidentityTokenURL:   azureIdentityTokenURL,\n\t\tinstanceComputeURL: azureInstanceComputeURL,\n\t}\n}\n\ntype azureIdentityToken struct {\n\tAccessToken  string `json:\"access_token\"`\n\tRefreshToken string `json:\"refresh_token\"`\n\tClientID     string `json:\"client_id\"`\n\tExpiresIn    int64  `json:\"expires_in,string\"`\n\tExpiresOn    int64  `json:\"expires_on,string\"`\n\tExtExpiresIn int64  `json:\"ext_expires_in,string\"`\n\tNotBefore    int64  `json:\"not_before,string\"`\n\tResource     string `json:\"resource\"`\n\tTokenType    string `json:\"token_type\"`\n}\n\ntype azurePayload struct {\n\tjose.Claims\n\tAppID            string `json:\"appid\"`\n\tAppIDAcr         string `json:\"appidacr\"`\n\tIdentityProvider string `json:\"idp\"`\n\tObjectID         string `json:\"oid\"`\n\tTenantID         string `json:\"tid\"`\n\tVersion          string `json:\"ver\"`\n\tXMSMirID         string `json:\"xms_mirid\"`\n}\n\n// Azure is the provisioner that supports identity tokens created from the\n// Microsoft Azure Instance Metadata service.\n//\n// The default audience is \"https://management.azure.com/\".\n//\n// If DisableCustomSANs is true, only the internal DNS and IP will be added as a\n// SAN. By default it will accept any SAN in the CSR.\n//\n// If DisableTrustOnFirstUse is true, multiple sign request for this provisioner\n// with the same instance will be accepted. By default only the first request\n// will be accepted.\n//\n// Microsoft Azure identity docs are available at\n// https://docs.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/how-to-use-vm-token\n// and https://docs.microsoft.com/en-us/azure/virtual-machines/windows/instance-metadata-service\ntype Azure struct {\n\t*base\n\tID                     string   `json:\"-\"`\n\tType                   string   `json:\"type\"`\n\tName                   string   `json:\"name\"`\n\tTenantID               string   `json:\"tenantID\"`\n\tResourceGroups         []string `json:\"resourceGroups\"`\n\tSubscriptionIDs        []string `json:\"subscriptionIDs\"`\n\tObjectIDs              []string `json:\"objectIDs\"`\n\tAudience               string   `json:\"audience,omitempty\"`\n\tDisableCustomSANs      bool     `json:\"disableCustomSANs\"`\n\tDisableTrustOnFirstUse bool     `json:\"disableTrustOnFirstUse\"`\n\tClaims                 *Claims  `json:\"claims,omitempty\"`\n\tOptions                *Options `json:\"options,omitempty\"`\n\tconfig                 *azureConfig\n\toidcConfig             openIDConfiguration\n\tkeyStore               *keyStore\n\tctl                    *Controller\n\tenvironment            string\n}\n\n// GetID returns the provisioner unique identifier.\nfunc (p *Azure) GetID() string {\n\tif p.ID != \"\" {\n\t\treturn p.ID\n\t}\n\treturn p.GetIDForToken()\n}\n\n// GetIDForToken returns an identifier that will be used to load the provisioner\n// from a token.\nfunc (p *Azure) GetIDForToken() string {\n\treturn p.TenantID\n}\n\n// GetTokenID returns the identifier of the token. The default value for Azure\n// the SHA256 of \"xms_mirid\", but if DisableTrustOnFirstUse is set to true, then\n// it will be the token kid.\nfunc (p *Azure) GetTokenID(token string) (string, error) {\n\tjwt, err := jose.ParseSigned(token)\n\tif err != nil {\n\t\treturn \"\", errors.Wrap(err, \"error parsing token\")\n\t}\n\n\t// Get claims w/out verification. We need to look up the provisioner\n\t// key in order to verify the claims and we need the issuer from the claims\n\t// before we can look up the provisioner.\n\tvar claims azurePayload\n\tif err = jwt.UnsafeClaimsWithoutVerification(&claims); err != nil {\n\t\treturn \"\", errors.Wrap(err, \"error verifying claims\")\n\t}\n\n\t// If TOFU is disabled then allow token re-use. Azure caches the token for\n\t// 24h and without allowing the re-use we cannot use it twice.\n\tif p.DisableTrustOnFirstUse {\n\t\treturn \"\", ErrAllowTokenReuse\n\t}\n\n\tsum := sha256.Sum256([]byte(claims.XMSMirID))\n\treturn strings.ToLower(hex.EncodeToString(sum[:])), nil\n}\n\n// GetName returns the name of the provisioner.\nfunc (p *Azure) GetName() string {\n\treturn p.Name\n}\n\n// GetType returns the type of provisioner.\nfunc (p *Azure) GetType() Type {\n\treturn TypeAzure\n}\n\n// GetEncryptedKey is not available in an Azure provisioner.\nfunc (p *Azure) GetEncryptedKey() (kid, key string, ok bool) {\n\treturn \"\", \"\", false\n}\n\n// GetIdentityToken retrieves from the metadata service the identity token and\n// returns it.\nfunc (p *Azure) GetIdentityToken(subject, caURL string) (string, error) {\n\t_, _ = subject, caURL // unused input\n\n\t// Initialize the config if this method is used from the cli.\n\tp.assertConfig()\n\n\t// default to AzurePublicCloud to keep existing behavior\n\tidentityTokenResource := azureEnvironments[\"AzurePublicCloud\"]\n\n\tvar err error\n\tp.environment, err = p.getAzureEnvironment()\n\tif err != nil {\n\t\treturn \"\", errors.Wrap(err, \"error getting azure environment\")\n\t}\n\n\tif resource, ok := azureEnvironments[p.environment]; ok {\n\t\tidentityTokenResource = resource\n\t}\n\n\treq, err := http.NewRequest(\"GET\", p.config.identityTokenURL, http.NoBody)\n\tif err != nil {\n\t\treturn \"\", errors.Wrap(err, \"error creating request\")\n\t}\n\treq.Header.Set(\"Metadata\", \"true\")\n\n\tquery := req.URL.Query()\n\tquery.Add(\"resource\", identityTokenResource)\n\tquery.Add(\"api-version\", azureIdentityTokenAPIVersion)\n\treq.URL.RawQuery = query.Encode()\n\n\tresp, err := http.DefaultClient.Do(req)\n\tif err != nil {\n\t\treturn \"\", errors.Wrap(err, \"error getting identity token, are you in a Azure VM?\")\n\t}\n\tdefer resp.Body.Close()\n\n\tb, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn \"\", errors.Wrap(err, \"error reading identity token response\")\n\t}\n\tif resp.StatusCode >= 400 {\n\t\treturn \"\", errors.Errorf(\"error getting identity token: status=%d, response=%s\", resp.StatusCode, b)\n\t}\n\n\tvar identityToken azureIdentityToken\n\tif err := json.Unmarshal(b, &identityToken); err != nil {\n\t\treturn \"\", errors.Wrap(err, \"error unmarshaling identity token response\")\n\t}\n\n\treturn identityToken.AccessToken, nil\n}\n\n// Init validates and initializes the Azure provisioner.\nfunc (p *Azure) Init(config Config) (err error) {\n\tswitch {\n\tcase p.Type == \"\":\n\t\treturn errors.New(\"provisioner type cannot be empty\")\n\tcase p.Name == \"\":\n\t\treturn errors.New(\"provisioner name cannot be empty\")\n\tcase p.TenantID == \"\":\n\t\treturn errors.New(\"provisioner tenantId cannot be empty\")\n\tcase p.Audience == \"\": // use default audience\n\t\tp.Audience = azureDefaultAudience\n\t}\n\n\t// Initialize config\n\tp.assertConfig()\n\n\t// Decode and validate openid-configuration endpoint\n\tif err = getAndDecode(http.DefaultClient, p.config.oidcDiscoveryURL, &p.oidcConfig); err != nil {\n\t\treturn\n\t}\n\tif err := p.oidcConfig.Validate(); err != nil {\n\t\treturn errors.Wrapf(err, \"error parsing %s\", p.config.oidcDiscoveryURL)\n\t}\n\t// Get JWK key set\n\tif p.keyStore, err = newKeyStore(http.DefaultClient, p.oidcConfig.JWKSetURI); err != nil {\n\t\treturn\n\t}\n\n\tp.ctl, err = NewController(p, p.Claims, config, p.Options)\n\treturn\n}\n\n// authorizeToken returns the claims, name, group, subscription, identityObjectID, error.\nfunc (p *Azure) authorizeToken(token string) (*azurePayload, string, string, string, string, error) {\n\tjwt, err := jose.ParseSigned(token)\n\tif err != nil {\n\t\treturn nil, \"\", \"\", \"\", \"\", errs.Wrap(http.StatusUnauthorized, err, \"azure.authorizeToken; error parsing azure token\")\n\t}\n\tif len(jwt.Headers) == 0 {\n\t\treturn nil, \"\", \"\", \"\", \"\", errs.Unauthorized(\"azure.authorizeToken; azure token missing header\")\n\t}\n\n\tvar found bool\n\tvar claims azurePayload\n\tkeys := p.keyStore.Get(jwt.Headers[0].KeyID)\n\tfor _, key := range keys {\n\t\tif err := jwt.Claims(key.Public(), &claims); err == nil {\n\t\t\tfound = true\n\t\t\tbreak\n\t\t}\n\t}\n\tif !found {\n\t\treturn nil, \"\", \"\", \"\", \"\", errs.Unauthorized(\"azure.authorizeToken; cannot validate azure token\")\n\t}\n\n\tif err := claims.ValidateWithLeeway(jose.Expected{\n\t\tAudience: []string{p.Audience},\n\t\tIssuer:   p.oidcConfig.Issuer,\n\t\tTime:     time.Now(),\n\t}, 1*time.Minute); err != nil {\n\t\treturn nil, \"\", \"\", \"\", \"\", errs.Wrap(http.StatusUnauthorized, err, \"azure.authorizeToken; failed to validate azure token payload\")\n\t}\n\n\t// Validate TenantID\n\tif claims.TenantID != p.TenantID {\n\t\treturn nil, \"\", \"\", \"\", \"\", errs.Unauthorized(\"azure.authorizeToken; azure token validation failed - invalid tenant id claim (tid)\")\n\t}\n\n\tre := azureXMSMirIDRegExp.FindStringSubmatch(claims.XMSMirID)\n\tif len(re) != 5 {\n\t\treturn nil, \"\", \"\", \"\", \"\", errs.Unauthorized(\"azure.authorizeToken; error parsing xms_mirid claim - %s\", claims.XMSMirID)\n\t}\n\n\tvar subscription, group, name string\n\tidentityObjectID := claims.ObjectID\n\tsubscription, group, name = re[1], re[2], re[4]\n\n\treturn &claims, name, group, subscription, identityObjectID, nil\n}\n\n// AuthorizeSign validates the given token and returns the sign options that\n// will be used on certificate creation.\nfunc (p *Azure) AuthorizeSign(ctx context.Context, token string) ([]SignOption, error) {\n\t_, name, group, subscription, identityObjectID, err := p.authorizeToken(token)\n\tif err != nil {\n\t\treturn nil, errs.Wrap(http.StatusInternalServerError, err, \"azure.AuthorizeSign\")\n\t}\n\n\t// Filter by resource group\n\tif len(p.ResourceGroups) > 0 {\n\t\tvar found bool\n\t\tfor _, g := range p.ResourceGroups {\n\t\t\tif g == group {\n\t\t\t\tfound = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif !found {\n\t\t\treturn nil, errs.Unauthorized(\"azure.AuthorizeSign; azure token validation failed - invalid resource group\")\n\t\t}\n\t}\n\n\t// Filter by subscription id\n\tif len(p.SubscriptionIDs) > 0 {\n\t\tvar found bool\n\t\tfor _, s := range p.SubscriptionIDs {\n\t\t\tif s == subscription {\n\t\t\t\tfound = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif !found {\n\t\t\treturn nil, errs.Unauthorized(\"azure.AuthorizeSign; azure token validation failed - invalid subscription id\")\n\t\t}\n\t}\n\n\t// Filter by Azure AD identity object id\n\tif len(p.ObjectIDs) > 0 {\n\t\tvar found bool\n\t\tfor _, i := range p.ObjectIDs {\n\t\t\tif i == identityObjectID {\n\t\t\t\tfound = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif !found {\n\t\t\treturn nil, errs.Unauthorized(\"azure.AuthorizeSign; azure token validation failed - invalid identity object id\")\n\t\t}\n\t}\n\n\t// Template options\n\tdata := x509util.NewTemplateData()\n\tdata.SetCommonName(name)\n\tif v, err := unsafeParseSigned(token); err == nil {\n\t\tdata.SetToken(v)\n\t}\n\n\t// Enforce known common name and default DNS if configured.\n\t// By default we'll accept the CN and SANs in the CSR.\n\t// There's no way to trust them other than TOFU.\n\tvar so []SignOption\n\tif p.DisableCustomSANs {\n\t\t// name will work only inside the virtual network\n\t\tso = append(so,\n\t\t\tcommonNameValidator(name),\n\t\t\tdnsNamesSubsetValidator([]string{name}),\n\t\t\tipAddressesValidator(nil),\n\t\t\temailAddressesValidator(nil),\n\t\t\tnewURIsValidator(ctx, nil),\n\t\t)\n\n\t\t// Enforce SANs in the template.\n\t\tdata.SetSANs([]string{name})\n\t}\n\n\ttemplateOptions, err := CustomTemplateOptions(p.Options, data, x509util.DefaultIIDLeafTemplate)\n\tif err != nil {\n\t\treturn nil, errs.Wrap(http.StatusInternalServerError, err, \"aws.AuthorizeSign\")\n\t}\n\n\treturn append(so,\n\t\tp,\n\t\ttemplateOptions,\n\t\t// modifiers / withOptions\n\t\tnewProvisionerExtensionOption(TypeAzure, p.Name, p.TenantID).WithControllerOptions(p.ctl),\n\t\tprofileDefaultDuration(p.ctl.Claimer.DefaultTLSCertDuration()),\n\t\t// validators\n\t\tdefaultPublicKeyValidator{},\n\t\tnewValidityValidator(p.ctl.Claimer.MinTLSCertDuration(), p.ctl.Claimer.MaxTLSCertDuration()),\n\t\tnewX509NamePolicyValidator(p.ctl.getPolicy().getX509()),\n\t\tp.ctl.newWebhookController(\n\t\t\tdata,\n\t\t\tlinkedca.Webhook_X509,\n\t\t\twebhook.WithAuthorizationPrincipal(identityObjectID),\n\t\t),\n\t), nil\n}\n\n// AuthorizeRenew returns an error if the renewal is disabled.\n// NOTE: This method does not actually validate the certificate or check it's\n// revocation status. Just confirms that the provisioner that created the\n// certificate was configured to allow renewals.\nfunc (p *Azure) AuthorizeRenew(ctx context.Context, cert *x509.Certificate) error {\n\treturn p.ctl.AuthorizeRenew(ctx, cert)\n}\n\n// AuthorizeSSHSign returns the list of SignOption for a SignSSH request.\nfunc (p *Azure) AuthorizeSSHSign(_ context.Context, token string) ([]SignOption, error) {\n\tif !p.ctl.Claimer.IsSSHCAEnabled() {\n\t\treturn nil, errs.Unauthorized(\"azure.AuthorizeSSHSign; sshCA is disabled for provisioner '%s'\", p.GetName())\n\t}\n\n\t_, name, _, _, identityObjectID, err := p.authorizeToken(token)\n\tif err != nil {\n\t\treturn nil, errs.Wrap(http.StatusInternalServerError, err, \"azure.AuthorizeSSHSign\")\n\t}\n\n\tsignOptions := []SignOption{}\n\n\t// Enforce host certificate.\n\tdefaults := SignSSHOptions{\n\t\tCertType: SSHHostCert,\n\t}\n\n\t// Validated principals.\n\tprincipals := []string{name}\n\n\t// Only enforce known principals if disable custom sans is true.\n\tif p.DisableCustomSANs {\n\t\tdefaults.Principals = principals\n\t} else {\n\t\t// Check that at least one principal is sent in the request.\n\t\tsignOptions = append(signOptions, &sshCertOptionsRequireValidator{\n\t\t\tPrincipals: true,\n\t\t})\n\t}\n\n\t// Certificate templates.\n\tdata := sshutil.CreateTemplateData(sshutil.HostCert, name, principals)\n\tif v, err := unsafeParseSigned(token); err == nil {\n\t\tdata.SetToken(v)\n\t}\n\n\ttemplateOptions, err := CustomSSHTemplateOptions(p.Options, data, sshutil.DefaultIIDTemplate)\n\tif err != nil {\n\t\treturn nil, errs.Wrap(http.StatusInternalServerError, err, \"azure.AuthorizeSSHSign\")\n\t}\n\tsignOptions = append(signOptions, templateOptions)\n\n\treturn append(signOptions,\n\t\tp,\n\t\t// Validate user SignSSHOptions.\n\t\tsshCertOptionsValidator(defaults),\n\t\t// Set the validity bounds if not set.\n\t\t&sshDefaultDuration{p.ctl.Claimer},\n\t\t// Validate public key\n\t\t&sshDefaultPublicKeyValidator{},\n\t\t// Validate the validity period.\n\t\t&sshCertValidityValidator{p.ctl.Claimer},\n\t\t// Require all the fields in the SSH certificate\n\t\t&sshCertDefaultValidator{},\n\t\t// Ensure that all principal names are allowed\n\t\tnewSSHNamePolicyValidator(p.ctl.getPolicy().getSSHHost(), nil),\n\t\t// Call webhooks\n\t\tp.ctl.newWebhookController(\n\t\t\tdata,\n\t\t\tlinkedca.Webhook_SSH,\n\t\t\twebhook.WithAuthorizationPrincipal(identityObjectID),\n\t\t),\n\t), nil\n}\n\n// assertConfig initializes the config if it has not been initialized\nfunc (p *Azure) assertConfig() {\n\tif p.config == nil {\n\t\tp.config = newAzureConfig(p.TenantID)\n\t}\n}\n\n// getAzureEnvironment returns the Azure environment for the current instance\nfunc (p *Azure) getAzureEnvironment() (string, error) {\n\tif p.environment != \"\" {\n\t\treturn p.environment, nil\n\t}\n\n\treq, err := http.NewRequest(\"GET\", p.config.instanceComputeURL, http.NoBody)\n\tif err != nil {\n\t\treturn \"\", errors.Wrap(err, \"error creating request\")\n\t}\n\treq.Header.Add(\"Metadata\", \"True\")\n\n\tquery := req.URL.Query()\n\tquery.Add(\"format\", \"text\")\n\tquery.Add(\"api-version\", \"2021-02-01\")\n\treq.URL.RawQuery = query.Encode()\n\n\tresp, err := http.DefaultClient.Do(req)\n\tif err != nil {\n\t\treturn \"\", errors.Wrap(err, \"error getting azure instance environment, are you in a Azure VM?\")\n\t}\n\tdefer resp.Body.Close()\n\n\tb, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn \"\", errors.Wrap(err, \"error reading azure environment response\")\n\t}\n\tif resp.StatusCode >= 400 {\n\t\treturn \"\", errors.Errorf(\"error getting azure environment: status=%d, response=%s\", resp.StatusCode, b)\n\t}\n\n\treturn string(b), nil\n}\n"
  },
  {
    "path": "authority/provisioner/azure_test.go",
    "content": "package provisioner\n\nimport (\n\t\"context\"\n\t\"crypto\"\n\t\"crypto/rand\"\n\t\"crypto/rsa\"\n\t\"crypto/sha256\"\n\t\"crypto/x509\"\n\t\"encoding/hex\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"go.step.sm/crypto/jose\"\n\n\t\"github.com/smallstep/assert\"\n\t\"github.com/smallstep/certificates/api/render\"\n)\n\nfunc TestAzure_Getters(t *testing.T) {\n\tp, err := generateAzure()\n\tassert.FatalError(t, err)\n\tif got := p.GetID(); got != p.TenantID {\n\t\tt.Errorf(\"Azure.GetID() = %v, want %v\", got, p.TenantID)\n\t}\n\tif got := p.GetName(); got != p.Name {\n\t\tt.Errorf(\"Azure.GetName() = %v, want %v\", got, p.Name)\n\t}\n\tif got := p.GetType(); got != TypeAzure {\n\t\tt.Errorf(\"Azure.GetType() = %v, want %v\", got, TypeAzure)\n\t}\n\tkid, key, ok := p.GetEncryptedKey()\n\tif kid != \"\" || key != \"\" || ok == true {\n\t\tt.Errorf(\"Azure.GetEncryptedKey() = (%v, %v, %v), want (%v, %v, %v)\",\n\t\t\tkid, key, ok, \"\", \"\", false)\n\t}\n}\n\nfunc TestAzure_GetTokenID(t *testing.T) {\n\tp1, srv, err := generateAzureWithServer()\n\tassert.FatalError(t, err)\n\tdefer srv.Close()\n\n\tp2, err := generateAzure()\n\tassert.FatalError(t, err)\n\tp2.TenantID = p1.TenantID\n\tp2.config = p1.config\n\tp2.oidcConfig = p1.oidcConfig\n\tp2.keyStore = p1.keyStore\n\tp2.DisableTrustOnFirstUse = true\n\n\tt1, err := p1.GetIdentityToken(\"subject\", \"caURL\")\n\tassert.FatalError(t, err)\n\tt2, err := p2.GetIdentityToken(\"subject\", \"caURL\")\n\tassert.FatalError(t, err)\n\n\tsum := sha256.Sum256([]byte(\"/subscriptions/subscriptionID/resourceGroups/resourceGroup/providers/Microsoft.Compute/virtualMachines/virtualMachine\"))\n\tw1 := strings.ToLower(hex.EncodeToString(sum[:]))\n\n\ttype args struct {\n\t\ttoken string\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tazure   *Azure\n\t\targs    args\n\t\twant    string\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", p1, args{t1}, w1, false},\n\t\t{\"ok no TOFU\", p2, args{t2}, \"\", true},\n\t\t{\"fail token\", p1, args{\"bad-token\"}, \"\", true},\n\t\t{\"fail claims\", p1, args{\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.ey.fooo\"}, \"\", true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := tt.azure.GetTokenID(tt.args.token)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"Azure.GetTokenID() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif got != tt.want {\n\t\t\t\tt.Errorf(\"Azure.GetTokenID() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestAzure_GetIdentityToken(t *testing.T) {\n\tp1, err := generateAzure()\n\tassert.FatalError(t, err)\n\n\tt1, err := generateAzureToken(\"subject\", p1.oidcConfig.Issuer, azureDefaultAudience,\n\t\tp1.TenantID, \"subscriptionID\", \"resourceGroup\", \"virtualMachine\", \"vm\",\n\t\ttime.Now(), &p1.keyStore.keySet.Keys[0])\n\tassert.FatalError(t, err)\n\n\tsrvIdentity := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\twantResource := r.URL.Query().Get(\"want_resource\")\n\t\tresource := r.URL.Query().Get(\"resource\")\n\t\tif wantResource == \"\" || resource != wantResource {\n\t\t\thttp.Error(w, fmt.Sprintf(\"Azure query param resource = %s, wantResource %s\", resource, wantResource), http.StatusBadRequest)\n\t\t\treturn\n\t\t}\n\n\t\tswitch r.URL.Path {\n\t\tcase \"/bad-request\":\n\t\t\thttp.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)\n\t\tcase \"/bad-json\":\n\t\t\tw.Write([]byte(t1))\n\t\tdefault:\n\t\t\tw.Header().Add(\"Content-Type\", \"application/json\")\n\t\t\tfmt.Fprintf(w, `{\"access_token\":\"%s\"}`, t1)\n\t\t}\n\t}))\n\tdefer srvIdentity.Close()\n\n\tsrvInstance := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tswitch r.URL.Path {\n\t\tcase \"/bad-request\":\n\t\t\thttp.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)\n\t\tcase \"/AzureChinaCloud\":\n\t\t\tw.Header().Add(\"Content-Type\", \"text/plain\")\n\t\t\tw.Write([]byte(\"AzureChinaCloud\"))\n\t\tcase \"/AzureGermanCloud\":\n\t\t\tw.Header().Add(\"Content-Type\", \"text/plain\")\n\t\t\tw.Write([]byte(\"AzureGermanCloud\"))\n\t\tcase \"/AzureUSGovernmentCloud\":\n\t\t\tw.Header().Add(\"Content-Type\", \"text/plain\")\n\t\t\tw.Write([]byte(\"AzureUSGovernmentCloud\"))\n\t\tdefault:\n\t\t\tw.Header().Add(\"Content-Type\", \"text/plain\")\n\t\t\tw.Write([]byte(\"AzurePublicCloud\"))\n\t\t}\n\t}))\n\tdefer srvInstance.Close()\n\n\ttype args struct {\n\t\tsubject string\n\t\tcaURL   string\n\t}\n\ttests := []struct {\n\t\tname               string\n\t\tazure              *Azure\n\t\targs               args\n\t\tidentityTokenURL   string\n\t\tinstanceComputeURL string\n\t\twantEnvironment    string\n\t\twant               string\n\t\twantErr            bool\n\t}{\n\t\t{\"ok\", p1, args{\"subject\", \"caURL\"}, srvIdentity.URL, srvInstance.URL, \"AzurePublicCloud\", t1, false},\n\t\t{\"ok azure china\", p1, args{\"subject\", \"caURL\"}, srvIdentity.URL, srvInstance.URL, \"AzurePublicCloud\", t1, false},\n\t\t{\"ok azure germany\", p1, args{\"subject\", \"caURL\"}, srvIdentity.URL, srvInstance.URL, \"AzureGermanCloud\", t1, false},\n\t\t{\"ok azure us gov\", p1, args{\"subject\", \"caURL\"}, srvIdentity.URL, srvInstance.URL, \"AzureUSGovernmentCloud\", t1, false},\n\t\t{\"fail instance request\", p1, args{\"subject\", \"caURL\"}, srvIdentity.URL + \"/bad-request\", srvInstance.URL + \"/bad-request\", \"AzurePublicCloud\", \"\", true},\n\t\t{\"fail request\", p1, args{\"subject\", \"caURL\"}, srvIdentity.URL + \"/bad-request\", srvInstance.URL, \"AzurePublicCloud\", \"\", true},\n\t\t{\"fail unmarshal\", p1, args{\"subject\", \"caURL\"}, srvIdentity.URL + \"/bad-json\", srvInstance.URL, \"AzurePublicCloud\", \"\", true},\n\t\t{\"fail url\", p1, args{\"subject\", \"caURL\"}, \"://ca.smallstep.com\", srvInstance.URL, \"AzurePublicCloud\", \"\", true},\n\t\t{\"fail connect\", p1, args{\"subject\", \"caURL\"}, \"foobarzar\", srvInstance.URL, \"AzurePublicCloud\", \"\", true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\t// reset environment between tests to avoid caching issues\n\t\t\tp1.environment = \"\"\n\t\t\ttt.azure.config.identityTokenURL = tt.identityTokenURL + \"?want_resource=\" + azureEnvironments[tt.wantEnvironment]\n\t\t\ttt.azure.config.instanceComputeURL = tt.instanceComputeURL + \"/\" + tt.wantEnvironment\n\t\t\tgot, err := tt.azure.GetIdentityToken(tt.args.subject, tt.args.caURL)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"Azure.GetIdentityToken() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif got != tt.want {\n\t\t\t\tt.Errorf(\"Azure.GetIdentityToken() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestAzure_Init(t *testing.T) {\n\tp1, srv, err := generateAzureWithServer()\n\tassert.FatalError(t, err)\n\tdefer srv.Close()\n\n\tconfig := Config{\n\t\tClaims: globalProvisionerClaims,\n\t}\n\tbadClaims := &Claims{\n\t\tDefaultTLSDur: &Duration{0},\n\t}\n\n\tbadDiscoveryURL := &azureConfig{\n\t\toidcDiscoveryURL: srv.URL + \"/error\",\n\t\tidentityTokenURL: p1.config.identityTokenURL,\n\t}\n\tbadJWKURL := &azureConfig{\n\t\toidcDiscoveryURL: srv.URL + \"/openid-configuration-fail-jwk\",\n\t\tidentityTokenURL: p1.config.identityTokenURL,\n\t}\n\tbadAzureConfig := &azureConfig{\n\t\toidcDiscoveryURL: srv.URL + \"/openid-configuration-no-issuer\",\n\t\tidentityTokenURL: p1.config.identityTokenURL,\n\t}\n\n\ttype fields struct {\n\t\tType     string\n\t\tName     string\n\t\tTenantID string\n\t\tClaims   *Claims\n\t\tconfig   *azureConfig\n\t}\n\ttype args struct {\n\t\tconfig Config\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tfields  fields\n\t\targs    args\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", fields{p1.Type, p1.Name, p1.TenantID, nil, p1.config}, args{config}, false},\n\t\t{\"ok with config\", fields{p1.Type, p1.Name, p1.TenantID, nil, p1.config}, args{config}, false},\n\t\t{\"fail type\", fields{\"\", p1.Name, p1.TenantID, nil, p1.config}, args{config}, true},\n\t\t{\"fail name\", fields{p1.Type, \"\", p1.TenantID, nil, p1.config}, args{config}, true},\n\t\t{\"fail tenant id\", fields{p1.Type, p1.Name, \"\", nil, p1.config}, args{config}, true},\n\t\t{\"fail claims\", fields{p1.Type, p1.Name, p1.TenantID, badClaims, p1.config}, args{config}, true},\n\t\t{\"fail discovery URL\", fields{p1.Type, p1.Name, p1.TenantID, nil, badDiscoveryURL}, args{config}, true},\n\t\t{\"fail JWK URL\", fields{p1.Type, p1.Name, p1.TenantID, nil, badJWKURL}, args{config}, true},\n\t\t{\"fail config Validate\", fields{p1.Type, p1.Name, p1.TenantID, nil, badAzureConfig}, args{config}, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tp := &Azure{\n\t\t\t\tType:     tt.fields.Type,\n\t\t\t\tName:     tt.fields.Name,\n\t\t\t\tTenantID: tt.fields.TenantID,\n\t\t\t\tClaims:   tt.fields.Claims,\n\t\t\t\tconfig:   tt.fields.config,\n\t\t\t}\n\t\t\tif err := p.Init(tt.args.config); (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"Azure.Init() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestAzure_authorizeToken(t *testing.T) {\n\ttype test struct {\n\t\tp     *Azure\n\t\ttoken string\n\t\terr   error\n\t\tcode  int\n\t}\n\ttests := map[string]func(*testing.T) test{\n\t\t\"fail/bad-token\": func(t *testing.T) test {\n\t\t\tp, err := generateAzure()\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tp:     p,\n\t\t\t\ttoken: \"foo\",\n\t\t\t\tcode:  http.StatusUnauthorized,\n\t\t\t\terr:   errors.New(\"azure.authorizeToken; error parsing azure token\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/cannot-validate-sig\": func(t *testing.T) test {\n\t\t\tp, srv, err := generateAzureWithServer()\n\t\t\tassert.FatalError(t, err)\n\t\t\tdefer srv.Close()\n\t\t\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\tassert.FatalError(t, err)\n\t\t\ttok, err := generateAzureToken(\"subject\", p.oidcConfig.Issuer, azureDefaultAudience,\n\t\t\t\tp.TenantID, \"subscriptionID\", \"resourceGroup\", \"virtualMachine\", \"vm\",\n\t\t\t\ttime.Now(), jwk)\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tp:     p,\n\t\t\t\ttoken: tok,\n\t\t\t\tcode:  http.StatusUnauthorized,\n\t\t\t\terr:   errors.New(\"azure.authorizeToken; cannot validate azure token\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/invalid-token-issuer\": func(t *testing.T) test {\n\t\t\tp, srv, err := generateAzureWithServer()\n\t\t\tassert.FatalError(t, err)\n\t\t\tdefer srv.Close()\n\t\t\ttok, err := generateAzureToken(\"subject\", \"bad-issuer\", azureDefaultAudience,\n\t\t\t\tp.TenantID, \"subscriptionID\", \"resourceGroup\", \"virtualMachine\", \"vm\",\n\t\t\t\ttime.Now(), &p.keyStore.keySet.Keys[0])\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tp:     p,\n\t\t\t\ttoken: tok,\n\t\t\t\tcode:  http.StatusUnauthorized,\n\t\t\t\terr:   errors.New(\"azure.authorizeToken; failed to validate azure token payload\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/invalid-tenant-id\": func(t *testing.T) test {\n\t\t\tp, srv, err := generateAzureWithServer()\n\t\t\tassert.FatalError(t, err)\n\t\t\tdefer srv.Close()\n\t\t\ttok, err := generateAzureToken(\"subject\", p.oidcConfig.Issuer, azureDefaultAudience,\n\t\t\t\t\"foo\", \"subscriptionID\", \"resourceGroup\", \"virtualMachine\", \"vm\",\n\t\t\t\ttime.Now(), &p.keyStore.keySet.Keys[0])\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tp:     p,\n\t\t\t\ttoken: tok,\n\t\t\t\tcode:  http.StatusUnauthorized,\n\t\t\t\terr:   errors.New(\"azure.authorizeToken; azure token validation failed - invalid tenant id claim (tid)\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/invalid-xms-mir-id\": func(t *testing.T) test {\n\t\t\tp, srv, err := generateAzureWithServer()\n\t\t\tassert.FatalError(t, err)\n\t\t\tdefer srv.Close()\n\t\t\tjwk := &p.keyStore.keySet.Keys[0]\n\t\t\tsig, err := jose.NewSigner(\n\t\t\t\tjose.SigningKey{Algorithm: jose.ES256, Key: jwk.Key},\n\t\t\t\tnew(jose.SignerOptions).WithType(\"JWT\").WithHeader(\"kid\", jwk.KeyID),\n\t\t\t)\n\t\t\tassert.FatalError(t, err)\n\n\t\t\tnow := time.Now()\n\t\t\tclaims := azurePayload{\n\t\t\t\tClaims: jose.Claims{\n\t\t\t\t\tSubject:   \"subject\",\n\t\t\t\t\tIssuer:    p.oidcConfig.Issuer,\n\t\t\t\t\tIssuedAt:  jose.NewNumericDate(now),\n\t\t\t\t\tNotBefore: jose.NewNumericDate(now),\n\t\t\t\t\tExpiry:    jose.NewNumericDate(now.Add(5 * time.Minute)),\n\t\t\t\t\tAudience:  []string{azureDefaultAudience},\n\t\t\t\t\tID:        \"the-jti\",\n\t\t\t\t},\n\t\t\t\tAppID:            \"the-appid\",\n\t\t\t\tAppIDAcr:         \"the-appidacr\",\n\t\t\t\tIdentityProvider: \"the-idp\",\n\t\t\t\tObjectID:         \"the-oid\",\n\t\t\t\tTenantID:         p.TenantID,\n\t\t\t\tVersion:          \"the-version\",\n\t\t\t\tXMSMirID:         \"foo\",\n\t\t\t}\n\t\t\ttok, err := jose.Signed(sig).Claims(claims).CompactSerialize()\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tp:     p,\n\t\t\t\ttoken: tok,\n\t\t\t\tcode:  http.StatusUnauthorized,\n\t\t\t\terr:   errors.New(\"azure.authorizeToken; error parsing xms_mirid claim - foo\"),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tp, srv, err := generateAzureWithServer()\n\t\t\tassert.FatalError(t, err)\n\t\t\tdefer srv.Close()\n\t\t\ttok, err := generateAzureToken(\"subject\", p.oidcConfig.Issuer, azureDefaultAudience,\n\t\t\t\tp.TenantID, \"subscriptionID\", \"resourceGroup\", \"virtualMachine\", \"vm\",\n\t\t\t\ttime.Now(), &p.keyStore.keySet.Keys[0])\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tp:     p,\n\t\t\t\ttoken: tok,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, tt := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ttc := tt(t)\n\t\t\tif claims, name, group, subscriptionID, objectID, err := tc.p.authorizeToken(tc.token); err != nil {\n\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\tvar sc render.StatusCodedError\n\t\t\t\t\tassert.Fatal(t, errors.As(err, &sc), \"error does not implement StatusCodedError interface\")\n\t\t\t\t\tassert.Equals(t, sc.StatusCode(), tc.code)\n\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.err.Error())\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif assert.Nil(t, tc.err) {\n\t\t\t\t\tassert.Equals(t, claims.Subject, \"subject\")\n\t\t\t\t\tassert.Equals(t, claims.Issuer, tc.p.oidcConfig.Issuer)\n\t\t\t\t\tassert.Equals(t, claims.Audience[0], azureDefaultAudience)\n\n\t\t\t\t\tassert.Equals(t, name, \"virtualMachine\")\n\t\t\t\t\tassert.Equals(t, group, \"resourceGroup\")\n\t\t\t\t\tassert.Equals(t, subscriptionID, \"subscriptionID\")\n\t\t\t\t\tassert.Equals(t, objectID, \"the-oid\")\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestAzure_AuthorizeSign(t *testing.T) {\n\tp1, srv, err := generateAzureWithServer()\n\tassert.FatalError(t, err)\n\tdefer srv.Close()\n\n\tp2, err := generateAzure()\n\tassert.FatalError(t, err)\n\tp2.TenantID = p1.TenantID\n\tp2.ResourceGroups = []string{\"resourceGroup\"}\n\tp2.config = p1.config\n\tp2.oidcConfig = p1.oidcConfig\n\tp2.keyStore = p1.keyStore\n\tp2.DisableCustomSANs = true\n\n\tp3, err := generateAzure()\n\tassert.FatalError(t, err)\n\tp3.config = p1.config\n\tp3.oidcConfig = p1.oidcConfig\n\tp3.keyStore = p1.keyStore\n\n\tp4, err := generateAzure()\n\tassert.FatalError(t, err)\n\tp4.TenantID = p1.TenantID\n\tp4.ResourceGroups = []string{\"foobarzar\"}\n\tp4.config = p1.config\n\tp4.oidcConfig = p1.oidcConfig\n\tp4.keyStore = p1.keyStore\n\n\tp5, err := generateAzure()\n\tassert.FatalError(t, err)\n\tp5.TenantID = p1.TenantID\n\tp5.SubscriptionIDs = []string{\"subscriptionID\"}\n\tp5.config = p1.config\n\tp5.oidcConfig = p1.oidcConfig\n\tp5.keyStore = p1.keyStore\n\n\tp6, err := generateAzure()\n\tassert.FatalError(t, err)\n\tp6.TenantID = p1.TenantID\n\tp6.SubscriptionIDs = []string{\"foobarzar\"}\n\tp6.config = p1.config\n\tp6.oidcConfig = p1.oidcConfig\n\tp6.keyStore = p1.keyStore\n\n\tp7, err := generateAzure()\n\tassert.FatalError(t, err)\n\tp7.TenantID = p1.TenantID\n\tp7.ObjectIDs = []string{\"the-oid\"}\n\tp7.config = p1.config\n\tp7.oidcConfig = p1.oidcConfig\n\tp7.keyStore = p1.keyStore\n\n\tp8, err := generateAzure()\n\tassert.FatalError(t, err)\n\tp8.TenantID = p1.TenantID\n\tp8.ObjectIDs = []string{\"foobarzar\"}\n\tp8.config = p1.config\n\tp8.oidcConfig = p1.oidcConfig\n\tp8.keyStore = p1.keyStore\n\n\tbadKey, err := generateJSONWebKey()\n\tassert.FatalError(t, err)\n\n\tt1, err := p1.GetIdentityToken(\"subject\", \"caURL\")\n\tassert.FatalError(t, err)\n\tt2, err := p2.GetIdentityToken(\"subject\", \"caURL\")\n\tassert.FatalError(t, err)\n\tt3, err := p3.GetIdentityToken(\"subject\", \"caURL\")\n\tassert.FatalError(t, err)\n\tt4, err := p4.GetIdentityToken(\"subject\", \"caURL\")\n\tassert.FatalError(t, err)\n\tt5, err := p5.GetIdentityToken(\"subject\", \"caURL\")\n\tassert.FatalError(t, err)\n\tt6, err := p6.GetIdentityToken(\"subject\", \"caURL\")\n\tassert.FatalError(t, err)\n\tt7, err := p6.GetIdentityToken(\"subject\", \"caURL\")\n\tassert.FatalError(t, err)\n\tt8, err := p6.GetIdentityToken(\"subject\", \"caURL\")\n\tassert.FatalError(t, err)\n\n\tt11, err := generateAzureToken(\"subject\", p1.oidcConfig.Issuer, azureDefaultAudience,\n\t\tp1.TenantID, \"subscriptionID\", \"resourceGroup\", \"virtualMachine\", \"vm\",\n\t\ttime.Now(), &p1.keyStore.keySet.Keys[0])\n\tassert.FatalError(t, err)\n\n\tfailIssuer, err := generateAzureToken(\"subject\", \"bad-issuer\", azureDefaultAudience,\n\t\tp1.TenantID, \"subscriptionID\", \"resourceGroup\", \"virtualMachine\", \"vm\",\n\t\ttime.Now(), &p1.keyStore.keySet.Keys[0])\n\tassert.FatalError(t, err)\n\tfailAudience, err := generateAzureToken(\"subject\", p1.oidcConfig.Issuer, \"bad-audience\",\n\t\tp1.TenantID, \"subscriptionID\", \"resourceGroup\", \"virtualMachine\", \"vm\",\n\t\ttime.Now(), &p1.keyStore.keySet.Keys[0])\n\tassert.FatalError(t, err)\n\tfailExp, err := generateAzureToken(\"subject\", p1.oidcConfig.Issuer, azureDefaultAudience,\n\t\tp1.TenantID, \"subscriptionID\", \"resourceGroup\", \"virtualMachine\", \"vm\",\n\t\ttime.Now().Add(-360*time.Second), &p1.keyStore.keySet.Keys[0])\n\tassert.FatalError(t, err)\n\tfailNbf, err := generateAzureToken(\"subject\", p1.oidcConfig.Issuer, azureDefaultAudience,\n\t\tp1.TenantID, \"subscriptionID\", \"resourceGroup\", \"virtualMachine\", \"vm\",\n\t\ttime.Now().Add(360*time.Second), &p1.keyStore.keySet.Keys[0])\n\tassert.FatalError(t, err)\n\tfailKey, err := generateAzureToken(\"subject\", p1.oidcConfig.Issuer, azureDefaultAudience,\n\t\tp1.TenantID, \"subscriptionID\", \"resourceGroup\", \"virtualMachine\", \"vm\",\n\t\ttime.Now(), badKey)\n\tassert.FatalError(t, err)\n\n\ttype args struct {\n\t\ttoken string\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tazure   *Azure\n\t\targs    args\n\t\twantLen int\n\t\tcode    int\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", p1, args{t1}, 8, http.StatusOK, false},\n\t\t{\"ok\", p2, args{t2}, 13, http.StatusOK, false},\n\t\t{\"ok\", p1, args{t11}, 8, http.StatusOK, false},\n\t\t{\"ok\", p5, args{t5}, 8, http.StatusOK, false},\n\t\t{\"ok\", p7, args{t7}, 8, http.StatusOK, false},\n\t\t{\"fail tenant\", p3, args{t3}, 0, http.StatusUnauthorized, true},\n\t\t{\"fail resource group\", p4, args{t4}, 0, http.StatusUnauthorized, true},\n\t\t{\"fail subscription\", p6, args{t6}, 0, http.StatusUnauthorized, true},\n\t\t{\"fail object id\", p8, args{t8}, 0, http.StatusUnauthorized, true},\n\t\t{\"fail token\", p1, args{\"token\"}, 0, http.StatusUnauthorized, true},\n\t\t{\"fail issuer\", p1, args{failIssuer}, 0, http.StatusUnauthorized, true},\n\t\t{\"fail audience\", p1, args{failAudience}, 0, http.StatusUnauthorized, true},\n\t\t{\"fail exp\", p1, args{failExp}, 0, http.StatusUnauthorized, true},\n\t\t{\"fail nbf\", p1, args{failNbf}, 0, http.StatusUnauthorized, true},\n\t\t{\"fail key\", p1, args{failKey}, 0, http.StatusUnauthorized, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tctx := NewContextWithMethod(context.Background(), SignMethod)\n\t\t\tswitch got, err := tt.azure.AuthorizeSign(ctx, tt.args.token); {\n\t\t\tcase (err != nil) != tt.wantErr:\n\t\t\t\tt.Errorf(\"Azure.AuthorizeSign() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\tcase err != nil:\n\t\t\t\tvar sc render.StatusCodedError\n\t\t\t\tassert.Fatal(t, errors.As(err, &sc), \"error does not implement StatusCodedError interface\")\n\t\t\t\tassert.Equals(t, sc.StatusCode(), tt.code)\n\t\t\tdefault:\n\t\t\t\tassert.Equals(t, tt.wantLen, len(got))\n\t\t\t\tfor _, o := range got {\n\t\t\t\t\tswitch v := o.(type) {\n\t\t\t\t\tcase *Azure:\n\t\t\t\t\tcase certificateOptionsFunc:\n\t\t\t\t\tcase *provisionerExtensionOption:\n\t\t\t\t\t\tassert.Equals(t, v.Type, TypeAzure)\n\t\t\t\t\t\tassert.Equals(t, v.Name, tt.azure.GetName())\n\t\t\t\t\t\tassert.Equals(t, v.CredentialID, tt.azure.TenantID)\n\t\t\t\t\t\tassert.Len(t, 0, v.KeyValuePairs)\n\t\t\t\t\tcase profileDefaultDuration:\n\t\t\t\t\t\tassert.Equals(t, time.Duration(v), tt.azure.ctl.Claimer.DefaultTLSCertDuration())\n\t\t\t\t\tcase commonNameValidator:\n\t\t\t\t\t\tassert.Equals(t, string(v), \"virtualMachine\")\n\t\t\t\t\tcase defaultPublicKeyValidator:\n\t\t\t\t\tcase *validityValidator:\n\t\t\t\t\t\tassert.Equals(t, v.min, tt.azure.ctl.Claimer.MinTLSCertDuration())\n\t\t\t\t\t\tassert.Equals(t, v.max, tt.azure.ctl.Claimer.MaxTLSCertDuration())\n\t\t\t\t\tcase ipAddressesValidator:\n\t\t\t\t\t\tassert.Equals(t, v, nil)\n\t\t\t\t\tcase emailAddressesValidator:\n\t\t\t\t\t\tassert.Equals(t, v, nil)\n\t\t\t\t\tcase *urisValidator:\n\t\t\t\t\t\tassert.Equals(t, v.uris, nil)\n\t\t\t\t\t\tassert.Equals(t, MethodFromContext(v.ctx), SignMethod)\n\t\t\t\t\tcase dnsNamesSubsetValidator:\n\t\t\t\t\t\tassert.Equals(t, []string(v), []string{\"virtualMachine\"})\n\t\t\t\t\tcase *x509NamePolicyValidator:\n\t\t\t\t\t\tassert.Equals(t, nil, v.policyEngine)\n\t\t\t\t\tcase *WebhookController:\n\t\t\t\t\t\tassert.Len(t, 0, v.webhooks)\n\t\t\t\t\tdefault:\n\t\t\t\t\t\tassert.FatalError(t, fmt.Errorf(\"unexpected sign option of type %T\", v))\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestAzure_AuthorizeRenew(t *testing.T) {\n\tnow := time.Now().Truncate(time.Second)\n\tp1, err := generateAzure()\n\tassert.FatalError(t, err)\n\tp2, err := generateAzure()\n\tassert.FatalError(t, err)\n\n\t// disable renewal\n\tdisable := true\n\tp2.Claims = &Claims{DisableRenewal: &disable}\n\tp2.ctl.Claimer, err = NewClaimer(p2.Claims, globalProvisionerClaims)\n\tassert.FatalError(t, err)\n\n\ttype args struct {\n\t\tcert *x509.Certificate\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tazure   *Azure\n\t\targs    args\n\t\tcode    int\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", p1, args{&x509.Certificate{\n\t\t\tNotBefore: now,\n\t\t\tNotAfter:  now.Add(time.Hour),\n\t\t}}, http.StatusOK, false},\n\t\t{\"fail/renew-disabled\", p2, args{&x509.Certificate{\n\t\t\tNotBefore: now,\n\t\t\tNotAfter:  now.Add(time.Hour),\n\t\t}}, http.StatusUnauthorized, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif err := tt.azure.AuthorizeRenew(context.Background(), tt.args.cert); (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"Azure.AuthorizeRenew() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t} else if err != nil {\n\t\t\t\tvar sc render.StatusCodedError\n\t\t\t\tassert.Fatal(t, errors.As(err, &sc), \"error does not implement StatusCodedError interface\")\n\t\t\t\tassert.Equals(t, sc.StatusCode(), tt.code)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestAzure_AuthorizeSSHSign(t *testing.T) {\n\ttm, fn := mockNow()\n\tdefer fn()\n\n\tp1, srv, err := generateAzureWithServer()\n\tassert.FatalError(t, err)\n\tp1.DisableCustomSANs = true\n\tdefer srv.Close()\n\n\tp2, err := generateAzure()\n\tassert.FatalError(t, err)\n\tp2.TenantID = p1.TenantID\n\tp2.config = p1.config\n\tp2.oidcConfig = p1.oidcConfig\n\tp2.keyStore = p1.keyStore\n\tp2.DisableCustomSANs = false\n\n\tp3, err := generateAzure()\n\tassert.FatalError(t, err)\n\t// disable sshCA\n\tdisable := false\n\tp3.Claims = &Claims{EnableSSHCA: &disable}\n\tp3.ctl.Claimer, err = NewClaimer(p3.Claims, globalProvisionerClaims)\n\tassert.FatalError(t, err)\n\n\tt1, err := p1.GetIdentityToken(\"subject\", \"caURL\")\n\tassert.FatalError(t, err)\n\n\tt2, err := p2.GetIdentityToken(\"subject\", \"caURL\")\n\tassert.FatalError(t, err)\n\n\tkey, err := generateJSONWebKey()\n\tassert.FatalError(t, err)\n\n\tsigner, err := generateJSONWebKey()\n\tassert.FatalError(t, err)\n\n\tpub := key.Public().Key\n\trsa2048, err := rsa.GenerateKey(rand.Reader, 2048)\n\tassert.FatalError(t, err)\n\t//nolint:gosec // tests minimum size of the key\n\trsa1024, err := rsa.GenerateKey(rand.Reader, 1024)\n\tassert.FatalError(t, err)\n\n\thostDuration := p1.ctl.Claimer.DefaultHostSSHCertDuration()\n\texpectedHostOptions := &SignSSHOptions{\n\t\tCertType: \"host\", Principals: []string{\"virtualMachine\"},\n\t\tValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(hostDuration)),\n\t}\n\texpectedCustomOptions := &SignSSHOptions{\n\t\tCertType: \"host\", Principals: []string{\"foo.bar\"},\n\t\tValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(hostDuration)),\n\t}\n\n\ttype args struct {\n\t\ttoken   string\n\t\tsshOpts SignSSHOptions\n\t\tkey     interface{}\n\t}\n\ttests := []struct {\n\t\tname        string\n\t\tazure       *Azure\n\t\targs        args\n\t\texpected    *SignSSHOptions\n\t\tcode        int\n\t\twantErr     bool\n\t\twantSignErr bool\n\t}{\n\t\t{\"ok\", p1, args{t1, SignSSHOptions{}, pub}, expectedHostOptions, http.StatusOK, false, false},\n\t\t{\"ok-rsa2048\", p1, args{t1, SignSSHOptions{}, rsa2048.Public()}, expectedHostOptions, http.StatusOK, false, false},\n\t\t{\"ok-type\", p1, args{t1, SignSSHOptions{CertType: \"host\"}, pub}, expectedHostOptions, http.StatusOK, false, false},\n\t\t{\"ok-principals\", p1, args{t1, SignSSHOptions{Principals: []string{\"virtualMachine\"}}, pub}, expectedHostOptions, http.StatusOK, false, false},\n\t\t{\"ok-options\", p1, args{t1, SignSSHOptions{CertType: \"host\", Principals: []string{\"virtualMachine\"}}, pub}, expectedHostOptions, http.StatusOK, false, false},\n\t\t{\"ok-custom\", p2, args{t2, SignSSHOptions{Principals: []string{\"foo.bar\"}}, pub}, expectedCustomOptions, http.StatusOK, false, false},\n\t\t{\"fail-rsa1024\", p1, args{t1, SignSSHOptions{}, rsa1024.Public()}, expectedHostOptions, http.StatusOK, false, true},\n\t\t{\"fail-type\", p1, args{t1, SignSSHOptions{CertType: \"user\"}, pub}, nil, http.StatusOK, false, true},\n\t\t{\"fail-principal\", p1, args{t1, SignSSHOptions{Principals: []string{\"smallstep.com\"}}, pub}, nil, http.StatusOK, false, true},\n\t\t{\"fail-extra-principal\", p1, args{t1, SignSSHOptions{Principals: []string{\"virtualMachine\", \"smallstep.com\"}}, pub}, nil, http.StatusOK, false, true},\n\t\t{\"fail-sshCA-disabled\", p3, args{\"foo\", SignSSHOptions{}, pub}, expectedHostOptions, http.StatusUnauthorized, true, false},\n\t\t{\"fail-invalid-token\", p1, args{\"foo\", SignSSHOptions{}, pub}, expectedHostOptions, http.StatusUnauthorized, true, false},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := tt.azure.AuthorizeSSHSign(context.Background(), tt.args.token)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"Azure.AuthorizeSSHSign() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif err != nil {\n\t\t\t\tvar sc render.StatusCodedError\n\t\t\t\tassert.Fatal(t, errors.As(err, &sc), \"error does not implement StatusCodedError interface\")\n\t\t\t\tassert.Equals(t, sc.StatusCode(), tt.code)\n\t\t\t\tassert.Nil(t, got)\n\t\t\t} else if assert.NotNil(t, got) {\n\t\t\t\tcert, err := signSSHCertificate(tt.args.key, tt.args.sshOpts, got, signer.Key.(crypto.Signer))\n\t\t\t\tif (err != nil) != tt.wantSignErr {\n\t\t\t\t\tt.Errorf(\"SignSSH error = %v, wantSignErr %v\", err, tt.wantSignErr)\n\t\t\t\t} else {\n\t\t\t\t\tif tt.wantSignErr {\n\t\t\t\t\t\tassert.Nil(t, cert)\n\t\t\t\t\t} else {\n\t\t\t\t\t\tassert.NoError(t, validateSSHCertificate(cert, tt.expected))\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestAzure_assertConfig(t *testing.T) {\n\tp1, err := generateAzure()\n\tassert.FatalError(t, err)\n\tp2, err := generateAzure()\n\tassert.FatalError(t, err)\n\tp2.config = nil\n\n\ttests := []struct {\n\t\tname  string\n\t\tazure *Azure\n\t}{\n\t\t{\"ok with config\", p1},\n\t\t{\"ok no config\", p2},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ttt.azure.assertConfig()\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "authority/provisioner/claims.go",
    "content": "package provisioner\n\nimport (\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n\t\"golang.org/x/crypto/ssh\"\n)\n\n// Claims so that individual provisioners can override global claims.\ntype Claims struct {\n\t// TLS CA properties\n\tMinTLSDur     *Duration `json:\"minTLSCertDuration,omitempty\"`\n\tMaxTLSDur     *Duration `json:\"maxTLSCertDuration,omitempty\"`\n\tDefaultTLSDur *Duration `json:\"defaultTLSCertDuration,omitempty\"`\n\n\t// SSH CA properties\n\tMinUserSSHDur     *Duration `json:\"minUserSSHCertDuration,omitempty\"`\n\tMaxUserSSHDur     *Duration `json:\"maxUserSSHCertDuration,omitempty\"`\n\tDefaultUserSSHDur *Duration `json:\"defaultUserSSHCertDuration,omitempty\"`\n\tMinHostSSHDur     *Duration `json:\"minHostSSHCertDuration,omitempty\"`\n\tMaxHostSSHDur     *Duration `json:\"maxHostSSHCertDuration,omitempty\"`\n\tDefaultHostSSHDur *Duration `json:\"defaultHostSSHCertDuration,omitempty\"`\n\tEnableSSHCA       *bool     `json:\"enableSSHCA,omitempty\"`\n\n\t// Renewal properties\n\tDisableRenewal          *bool `json:\"disableRenewal,omitempty\"`\n\tAllowRenewalAfterExpiry *bool `json:\"allowRenewalAfterExpiry,omitempty\"`\n\n\t// Other properties\n\tDisableSmallstepExtensions *bool `json:\"disableSmallstepExtensions,omitempty\"`\n}\n\n// Claimer is the type that controls claims. It provides an interface around the\n// current claim and the global one.\ntype Claimer struct {\n\tglobal Claims\n\tclaims *Claims\n}\n\n// NewClaimer initializes a new claimer with the given claims.\nfunc NewClaimer(claims *Claims, global Claims) (*Claimer, error) {\n\tc := &Claimer{global: global, claims: claims}\n\terr := c.Validate()\n\treturn c, err\n}\n\n// Claims returns the merge of the inner and global claims.\nfunc (c *Claimer) Claims() Claims {\n\tdisableRenewal := c.IsDisableRenewal()\n\tallowRenewalAfterExpiry := c.AllowRenewalAfterExpiry()\n\tenableSSHCA := c.IsSSHCAEnabled()\n\tdisableSmallstepExtensions := c.IsDisableSmallstepExtensions()\n\n\treturn Claims{\n\t\tMinTLSDur:                  &Duration{c.MinTLSCertDuration()},\n\t\tMaxTLSDur:                  &Duration{c.MaxTLSCertDuration()},\n\t\tDefaultTLSDur:              &Duration{c.DefaultTLSCertDuration()},\n\t\tMinUserSSHDur:              &Duration{c.MinUserSSHCertDuration()},\n\t\tMaxUserSSHDur:              &Duration{c.MaxUserSSHCertDuration()},\n\t\tDefaultUserSSHDur:          &Duration{c.DefaultUserSSHCertDuration()},\n\t\tMinHostSSHDur:              &Duration{c.MinHostSSHCertDuration()},\n\t\tMaxHostSSHDur:              &Duration{c.MaxHostSSHCertDuration()},\n\t\tDefaultHostSSHDur:          &Duration{c.DefaultHostSSHCertDuration()},\n\t\tEnableSSHCA:                &enableSSHCA,\n\t\tDisableRenewal:             &disableRenewal,\n\t\tAllowRenewalAfterExpiry:    &allowRenewalAfterExpiry,\n\t\tDisableSmallstepExtensions: &disableSmallstepExtensions,\n\t}\n}\n\n// DefaultTLSCertDuration returns the default TLS cert duration for the\n// provisioner. If the default is not set within the provisioner, then the global\n// default from the authority configuration will be used.\nfunc (c *Claimer) DefaultTLSCertDuration() time.Duration {\n\tif c.claims == nil || c.claims.DefaultTLSDur == nil {\n\t\treturn c.global.DefaultTLSDur.Duration\n\t}\n\treturn c.claims.DefaultTLSDur.Duration\n}\n\n// MinTLSCertDuration returns the minimum TLS cert duration for the provisioner.\n// If the minimum is not set within the provisioner, then the global\n// minimum from the authority configuration will be used.\nfunc (c *Claimer) MinTLSCertDuration() time.Duration {\n\tif c.claims == nil || c.claims.MinTLSDur == nil {\n\t\tif c.claims != nil && c.claims.DefaultTLSDur != nil && c.claims.DefaultTLSDur.Duration < c.global.MinTLSDur.Duration {\n\t\t\treturn c.claims.DefaultTLSDur.Duration\n\t\t}\n\t\treturn c.global.MinTLSDur.Duration\n\t}\n\treturn c.claims.MinTLSDur.Duration\n}\n\n// MaxTLSCertDuration returns the maximum TLS cert duration for the provisioner.\n// If the maximum is not set within the provisioner, then the global\n// maximum from the authority configuration will be used.\nfunc (c *Claimer) MaxTLSCertDuration() time.Duration {\n\tif c.claims == nil || c.claims.MaxTLSDur == nil {\n\t\tif c.claims != nil && c.claims.DefaultTLSDur != nil && c.claims.DefaultTLSDur.Duration > c.global.MaxTLSDur.Duration {\n\t\t\treturn c.claims.DefaultTLSDur.Duration\n\t\t}\n\t\treturn c.global.MaxTLSDur.Duration\n\t}\n\treturn c.claims.MaxTLSDur.Duration\n}\n\n// IsDisableRenewal returns if the renewal flow is disabled for the\n// provisioner. If the property is not set within the provisioner, then the\n// global value from the authority configuration will be used.\nfunc (c *Claimer) IsDisableRenewal() bool {\n\tif c.claims == nil || c.claims.DisableRenewal == nil {\n\t\treturn *c.global.DisableRenewal\n\t}\n\treturn *c.claims.DisableRenewal\n}\n\n// IsDisableSmallstepExtensions returns whether Smallstep extensions, such as\n// the provisioner extension, should be excluded from the certificate.\nfunc (c *Claimer) IsDisableSmallstepExtensions() bool {\n\tif c.claims == nil || c.claims.DisableSmallstepExtensions == nil {\n\t\treturn *c.global.DisableSmallstepExtensions\n\t}\n\treturn *c.claims.DisableSmallstepExtensions\n}\n\n// AllowRenewalAfterExpiry returns if the renewal flow is authorized if the\n// certificate is expired. If the property is not set within the provisioner\n// then the global value from the authority configuration will be used.\nfunc (c *Claimer) AllowRenewalAfterExpiry() bool {\n\tif c.claims == nil || c.claims.AllowRenewalAfterExpiry == nil {\n\t\treturn *c.global.AllowRenewalAfterExpiry\n\t}\n\treturn *c.claims.AllowRenewalAfterExpiry\n}\n\n// DefaultSSHCertDuration returns the default SSH certificate duration for the\n// given certificate type.\nfunc (c *Claimer) DefaultSSHCertDuration(certType uint32) (time.Duration, error) {\n\tswitch certType {\n\tcase ssh.UserCert:\n\t\treturn c.DefaultUserSSHCertDuration(), nil\n\tcase ssh.HostCert:\n\t\treturn c.DefaultHostSSHCertDuration(), nil\n\tcase 0:\n\t\treturn 0, errors.New(\"ssh certificate type has not been set\")\n\tdefault:\n\t\treturn 0, errors.Errorf(\"ssh certificate has an unknown type: %d\", certType)\n\t}\n}\n\n// DefaultUserSSHCertDuration returns the default SSH user cert duration for the\n// provisioner. If the default is not set within the provisioner, then the\n// global default from the authority configuration will be used.\nfunc (c *Claimer) DefaultUserSSHCertDuration() time.Duration {\n\tif c.claims == nil || c.claims.DefaultUserSSHDur == nil {\n\t\treturn c.global.DefaultUserSSHDur.Duration\n\t}\n\treturn c.claims.DefaultUserSSHDur.Duration\n}\n\n// MinUserSSHCertDuration returns the minimum SSH user cert duration for the\n// provisioner. If the minimum is not set within the provisioner, then the\n// global minimum from the authority configuration will be used.\nfunc (c *Claimer) MinUserSSHCertDuration() time.Duration {\n\tif c.claims == nil || c.claims.MinUserSSHDur == nil {\n\t\tif c.claims != nil && c.claims.DefaultUserSSHDur != nil && c.claims.DefaultUserSSHDur.Duration < c.global.MinUserSSHDur.Duration {\n\t\t\treturn c.claims.DefaultUserSSHDur.Duration\n\t\t}\n\t\treturn c.global.MinUserSSHDur.Duration\n\t}\n\treturn c.claims.MinUserSSHDur.Duration\n}\n\n// MaxUserSSHCertDuration returns the maximum SSH user cert duration for the\n// provisioner. If the maximum is not set within the provisioner, then the\n// global maximum from the authority configuration will be used.\nfunc (c *Claimer) MaxUserSSHCertDuration() time.Duration {\n\tif c.claims == nil || c.claims.MaxUserSSHDur == nil {\n\t\tif c.claims != nil && c.claims.DefaultUserSSHDur != nil && c.claims.DefaultUserSSHDur.Duration > c.global.MaxUserSSHDur.Duration {\n\t\t\treturn c.claims.DefaultUserSSHDur.Duration\n\t\t}\n\t\treturn c.global.MaxUserSSHDur.Duration\n\t}\n\treturn c.claims.MaxUserSSHDur.Duration\n}\n\n// DefaultHostSSHCertDuration returns the default SSH host cert duration for the\n// provisioner. If the default is not set within the provisioner, then the\n// global default from the authority configuration will be used.\nfunc (c *Claimer) DefaultHostSSHCertDuration() time.Duration {\n\tif c.claims == nil || c.claims.DefaultHostSSHDur == nil {\n\t\treturn c.global.DefaultHostSSHDur.Duration\n\t}\n\treturn c.claims.DefaultHostSSHDur.Duration\n}\n\n// MinHostSSHCertDuration returns the minimum SSH host cert duration for the\n// provisioner. If the minimum is not set within the provisioner, then the\n// global minimum from the authority configuration will be used.\nfunc (c *Claimer) MinHostSSHCertDuration() time.Duration {\n\tif c.claims == nil || c.claims.MinHostSSHDur == nil {\n\t\tif c.claims != nil && c.claims.DefaultHostSSHDur != nil && c.claims.DefaultHostSSHDur.Duration < c.global.MinHostSSHDur.Duration {\n\t\t\treturn c.claims.DefaultHostSSHDur.Duration\n\t\t}\n\t\treturn c.global.MinHostSSHDur.Duration\n\t}\n\treturn c.claims.MinHostSSHDur.Duration\n}\n\n// MaxHostSSHCertDuration returns the maximum SSH Host cert duration for the\n// provisioner. If the maximum is not set within the provisioner, then the\n// global maximum from the authority configuration will be used.\nfunc (c *Claimer) MaxHostSSHCertDuration() time.Duration {\n\tif c.claims == nil || c.claims.MaxHostSSHDur == nil {\n\t\tif c.claims != nil && c.claims.DefaultHostSSHDur != nil && c.claims.DefaultHostSSHDur.Duration > c.global.MaxHostSSHDur.Duration {\n\t\t\treturn c.claims.DefaultHostSSHDur.Duration\n\t\t}\n\t\treturn c.global.MaxHostSSHDur.Duration\n\t}\n\treturn c.claims.MaxHostSSHDur.Duration\n}\n\n// IsSSHCAEnabled returns if the SSH CA is enabled for the provisioner. If the\n// property is not set within the provisioner, then the global value from the\n// authority configuration will be used.\nfunc (c *Claimer) IsSSHCAEnabled() bool {\n\tif c.claims == nil || c.claims.EnableSSHCA == nil {\n\t\treturn *c.global.EnableSSHCA\n\t}\n\treturn *c.claims.EnableSSHCA\n}\n\n// Validate validates and modifies the Claims with default values.\nfunc (c *Claimer) Validate() error {\n\tvar (\n\t\tminDur = c.MinTLSCertDuration()\n\t\tmaxDur = c.MaxTLSCertDuration()\n\t\tdefDur = c.DefaultTLSCertDuration()\n\t)\n\tswitch {\n\tcase minDur <= 0:\n\t\treturn errors.Errorf(\"claims: MinTLSCertDuration must be greater than 0\")\n\tcase maxDur <= 0:\n\t\treturn errors.Errorf(\"claims: MaxTLSCertDuration must be greater than 0\")\n\tcase defDur <= 0:\n\t\treturn errors.Errorf(\"claims: DefaultTLSCertDuration must be greater than 0\")\n\tcase maxDur < minDur:\n\t\treturn errors.Errorf(\"claims: MaxCertDuration cannot be less \"+\n\t\t\t\"than MinCertDuration: MaxCertDuration - %v, MinCertDuration - %v\", maxDur, minDur)\n\tcase defDur < minDur:\n\t\treturn errors.Errorf(\"claims: DefaultCertDuration cannot be less than MinCertDuration: DefaultCertDuration - %v, MinCertDuration - %v\", defDur, minDur)\n\tcase maxDur < defDur:\n\t\treturn errors.Errorf(\"claims: MaxCertDuration cannot be less than DefaultCertDuration: MaxCertDuration - %v, DefaultCertDuration - %v\", maxDur, defDur)\n\tdefault:\n\t\treturn nil\n\t}\n}\n"
  },
  {
    "path": "authority/provisioner/claims_test.go",
    "content": "package provisioner\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"golang.org/x/crypto/ssh\"\n)\n\nfunc TestClaimer_DefaultSSHCertDuration(t *testing.T) {\n\tduration := Duration{\n\t\tDuration: time.Hour,\n\t}\n\ttype fields struct {\n\t\tglobal Claims\n\t\tclaims *Claims\n\t}\n\ttype args struct {\n\t\tcertType uint32\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tfields  fields\n\t\targs    args\n\t\twant    time.Duration\n\t\twantErr bool\n\t}{\n\t\t{\"user\", fields{globalProvisionerClaims, &Claims{DefaultUserSSHDur: &duration}}, args{1}, time.Hour, false},\n\t\t{\"user global\", fields{globalProvisionerClaims, nil}, args{ssh.UserCert}, 16 * time.Hour, false},\n\t\t{\"host global\", fields{globalProvisionerClaims, &Claims{DefaultHostSSHDur: &duration}}, args{2}, time.Hour, false},\n\t\t{\"host global\", fields{globalProvisionerClaims, nil}, args{ssh.HostCert}, 30 * 24 * time.Hour, false},\n\t\t{\"invalid\", fields{globalProvisionerClaims, nil}, args{0}, 0, true},\n\t\t{\"invalid global\", fields{globalProvisionerClaims, nil}, args{3}, 0, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tc := &Claimer{\n\t\t\t\tglobal: tt.fields.global,\n\t\t\t\tclaims: tt.fields.claims,\n\t\t\t}\n\t\t\tgot, err := c.DefaultSSHCertDuration(tt.args.certType)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"Claimer.DefaultSSHCertDuration() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif got != tt.want {\n\t\t\t\tt.Errorf(\"Claimer.DefaultSSHCertDuration() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "authority/provisioner/collection.go",
    "content": "package provisioner\n\nimport (\n\t\"crypto/sha1\" //nolint:gosec // not used for cryptographic security\n\t\"crypto/x509\"\n\t\"encoding/asn1\"\n\t\"encoding/binary\"\n\t\"encoding/hex\"\n\t\"fmt\"\n\t\"net/url\"\n\t\"sort\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"go.step.sm/crypto/jose\"\n\n\t\"github.com/smallstep/certificates/authority/admin\"\n\t\"github.com/smallstep/certificates/internal/cast\"\n)\n\n// DefaultProvisionersLimit is the default limit for listing provisioners.\nconst DefaultProvisionersLimit = 20\n\n// DefaultProvisionersMax is the maximum limit for listing provisioners.\nconst DefaultProvisionersMax = 100\n\ntype uidProvisioner struct {\n\tprovisioner Interface\n\tuid         string\n}\n\ntype provisionerSlice []uidProvisioner\n\nfunc (p provisionerSlice) Len() int           { return len(p) }\nfunc (p provisionerSlice) Less(i, j int) bool { return p[i].uid < p[j].uid }\nfunc (p provisionerSlice) Swap(i, j int)      { p[i], p[j] = p[j], p[i] }\n\n// loadByTokenPayload is a payload used to extract the id used to load the\n// provisioner.\ntype loadByTokenPayload struct {\n\tjose.Claims\n\tEmail           string `json:\"email\"` // OIDC email\n\tAuthorizedParty string `json:\"azp\"`   // OIDC client id\n\tTenantID        string `json:\"tid\"`   // Microsoft Azure tenant id\n}\n\n// Collection is a memory map of provisioners.\ntype Collection struct {\n\tbyID      *sync.Map\n\tbyKey     *sync.Map\n\tbyName    *sync.Map\n\tbyTokenID *sync.Map\n\tsorted    provisionerSlice\n\taudiences Audiences\n}\n\n// NewCollection initializes a collection of provisioners. The given list of\n// audiences are the audiences used by the JWT provisioner.\nfunc NewCollection(audiences Audiences) *Collection {\n\treturn &Collection{\n\t\tbyID:      new(sync.Map),\n\t\tbyKey:     new(sync.Map),\n\t\tbyName:    new(sync.Map),\n\t\tbyTokenID: new(sync.Map),\n\t\taudiences: audiences,\n\t}\n}\n\n// Load a provisioner by the ID.\nfunc (c *Collection) Load(id string) (Interface, bool) {\n\treturn loadProvisioner(c.byID, id)\n}\n\n// LoadByName a provisioner by name.\nfunc (c *Collection) LoadByName(name string) (Interface, bool) {\n\treturn loadProvisioner(c.byName, name)\n}\n\n// LoadByTokenID a provisioner by identifier found in token.\n// For different provisioner types this identifier may be found in different\n// attributes of the token.\nfunc (c *Collection) LoadByTokenID(tokenProvisionerID string) (Interface, bool) {\n\treturn loadProvisioner(c.byTokenID, tokenProvisionerID)\n}\n\n// LoadByToken parses the token claims and loads the provisioner associated.\nfunc (c *Collection) LoadByToken(token *jose.JSONWebToken, claims *jose.Claims) (Interface, bool) {\n\tvar audiences []string\n\t// Get all audiences with the given fragment\n\tfragment := extractFragment(claims.Audience)\n\tif fragment == \"\" {\n\t\taudiences = c.audiences.All()\n\t} else {\n\t\taudiences = c.audiences.WithFragment(fragment).All()\n\t}\n\n\t// match with server audiences\n\tif matchesAudience(claims.Audience, audiences) {\n\t\t// Use fragment to get provisioner name (GCP, AWS, SSHPOP)\n\t\tif fragment != \"\" {\n\t\t\treturn c.LoadByTokenID(fragment)\n\t\t}\n\t\t// If matches with stored audiences it will be a JWT token (default), and\n\t\t// the id would be <issuer>:<kid>.\n\t\t// TODO: is this ok?\n\t\treturn c.LoadByTokenID(claims.Issuer + \":\" + token.Headers[0].KeyID)\n\t}\n\n\t// The ID will be just the clientID stored in azp, aud or tid.\n\tvar payload loadByTokenPayload\n\tif err := token.UnsafeClaimsWithoutVerification(&payload); err != nil {\n\t\treturn nil, false\n\t}\n\n\t// Kubernetes Service Account tokens.\n\tif payload.Issuer == k8sSAIssuer {\n\t\tif p, ok := c.LoadByTokenID(K8sSAID); ok {\n\t\t\treturn p, ok\n\t\t}\n\t\t// Kubernetes service account provisioner not found\n\t\treturn nil, false\n\t}\n\n\t// Audience is required for non k8sSA tokens.\n\tif len(payload.Audience) == 0 {\n\t\treturn nil, false\n\t}\n\n\t// Try with azp (OIDC)\n\tif payload.AuthorizedParty != \"\" {\n\t\tif p, ok := c.LoadByTokenID(payload.AuthorizedParty); ok {\n\t\t\treturn p, ok\n\t\t}\n\t}\n\t// Try with tid (Azure, Azure OIDC)\n\tif payload.TenantID != \"\" {\n\t\t// Try to load an OIDC provisioner first.\n\t\tif payload.Email != \"\" {\n\t\t\tif p, ok := c.LoadByTokenID(payload.Audience[0]); ok {\n\t\t\t\treturn p, ok\n\t\t\t}\n\t\t}\n\t\t// Try to load an Azure provisioner.\n\t\tif p, ok := c.LoadByTokenID(payload.TenantID); ok {\n\t\t\treturn p, ok\n\t\t}\n\t}\n\n\t// Fallback to aud\n\treturn c.LoadByTokenID(payload.Audience[0])\n}\n\n// LoadByCertificate looks for the provisioner extension and extracts the\n// proper id to load the provisioner.\nfunc (c *Collection) LoadByCertificate(cert *x509.Certificate) (Interface, bool) {\n\tfor _, e := range cert.Extensions {\n\t\tif e.Id.Equal(StepOIDProvisioner) {\n\t\t\tvar provisioner extensionASN1\n\t\t\tif _, err := asn1.Unmarshal(e.Value, &provisioner); err != nil {\n\t\t\t\treturn nil, false\n\t\t\t}\n\t\t\treturn c.LoadByName(string(provisioner.Name))\n\t\t}\n\t}\n\n\t// Default to noop provisioner if an extension is not found. This allows to\n\t// accept a renewal of a cert without the provisioner extension.\n\treturn &noop{}, true\n}\n\n// LoadEncryptedKey returns an encrypted key by indexed by KeyID. At this moment\n// only JWK encrypted keys are indexed by KeyID.\nfunc (c *Collection) LoadEncryptedKey(keyID string) (string, bool) {\n\tp, ok := loadProvisioner(c.byKey, keyID)\n\tif !ok {\n\t\treturn \"\", false\n\t}\n\t_, key, ok := p.GetEncryptedKey()\n\treturn key, ok\n}\n\n// Store adds a provisioner to the collection and enforces the uniqueness of\n// provisioner IDs.\nfunc (c *Collection) Store(p Interface) error {\n\t// Store provisioner always in byID. ID must be unique.\n\tif _, loaded := c.byID.LoadOrStore(p.GetID(), p); loaded {\n\t\treturn admin.NewError(admin.ErrorBadRequestType,\n\t\t\t\"cannot add multiple provisioners with the same id\")\n\t}\n\t// Store provisioner always by name.\n\tif _, loaded := c.byName.LoadOrStore(p.GetName(), p); loaded {\n\t\tc.byID.Delete(p.GetID())\n\t\treturn admin.NewError(admin.ErrorBadRequestType,\n\t\t\t\"cannot add multiple provisioners with the same name\")\n\t}\n\t// Store provisioner always by ID presented in token.\n\tif _, loaded := c.byTokenID.LoadOrStore(p.GetIDForToken(), p); loaded {\n\t\tc.byID.Delete(p.GetID())\n\t\tc.byName.Delete(p.GetName())\n\t\treturn admin.NewError(admin.ErrorBadRequestType,\n\t\t\t\"cannot add multiple provisioners with the same token identifier\")\n\t}\n\n\t// Store provisioner in byKey if EncryptedKey is defined.\n\tif kid, _, ok := p.GetEncryptedKey(); ok {\n\t\tc.byKey.Store(kid, p)\n\t}\n\n\t// Store sorted provisioners.\n\t// Use the first 4 bytes (32bit) of the sum to insert the order\n\t// Using big endian format to get the strings sorted:\n\t// 0x00000000, 0x00000001, 0x00000002, ...\n\tbi := make([]byte, 4)\n\tsum := provisionerSum(p)\n\tbinary.BigEndian.PutUint32(bi, cast.Uint32(c.sorted.Len()))\n\tsum[0], sum[1], sum[2], sum[3] = bi[0], bi[1], bi[2], bi[3]\n\tc.sorted = append(c.sorted, uidProvisioner{\n\t\tprovisioner: p,\n\t\tuid:         hex.EncodeToString(sum),\n\t})\n\tsort.Sort(c.sorted)\n\treturn nil\n}\n\n// Remove deletes an provisioner from all associated collections and lists.\nfunc (c *Collection) Remove(id string) error {\n\tprov, ok := c.Load(id)\n\tif !ok {\n\t\treturn admin.NewError(admin.ErrorNotFoundType, \"provisioner %s not found\", id)\n\t}\n\n\tvar found bool\n\tfor i, elem := range c.sorted {\n\t\tif elem.provisioner.GetID() != id {\n\t\t\tcontinue\n\t\t}\n\t\t// Remove index in sorted list\n\t\tcopy(c.sorted[i:], c.sorted[i+1:])           // Shift a[i+1:] left one index.\n\t\tc.sorted[len(c.sorted)-1] = uidProvisioner{} // Erase last element (write zero value).\n\t\tc.sorted = c.sorted[:len(c.sorted)-1]        // Truncate slice.\n\t\tfound = true\n\t\tbreak\n\t}\n\tif !found {\n\t\treturn admin.NewError(admin.ErrorNotFoundType, \"provisioner %s not found in sorted list\", prov.GetName())\n\t}\n\n\tc.byID.Delete(id)\n\tc.byName.Delete(prov.GetName())\n\tc.byTokenID.Delete(prov.GetIDForToken())\n\tif kid, _, ok := prov.GetEncryptedKey(); ok {\n\t\tc.byKey.Delete(kid)\n\t}\n\n\treturn nil\n}\n\n// Update updates the given provisioner in all related lists and collections.\nfunc (c *Collection) Update(nu Interface) error {\n\told, ok := c.Load(nu.GetID())\n\tif !ok {\n\t\treturn admin.NewError(admin.ErrorNotFoundType, \"provisioner %s not found\", nu.GetID())\n\t}\n\n\tif old.GetName() != nu.GetName() {\n\t\tif _, ok := c.LoadByName(nu.GetName()); ok {\n\t\t\treturn admin.NewError(admin.ErrorBadRequestType,\n\t\t\t\t\"provisioner with name %s already exists\", nu.GetName())\n\t\t}\n\t}\n\tif old.GetIDForToken() != nu.GetIDForToken() {\n\t\tif _, ok := c.LoadByTokenID(nu.GetIDForToken()); ok {\n\t\t\treturn admin.NewError(admin.ErrorBadRequestType,\n\t\t\t\t\"provisioner with Token ID %s already exists\", nu.GetIDForToken())\n\t\t}\n\t}\n\n\tif err := c.Remove(old.GetID()); err != nil {\n\t\treturn err\n\t}\n\n\treturn c.Store(nu)\n}\n\n// Find implements pagination on a list of sorted provisioners.\nfunc (c *Collection) Find(cursor string, limit int) (List, string) {\n\tswitch {\n\tcase limit <= 0:\n\t\tlimit = DefaultProvisionersLimit\n\tcase limit > DefaultProvisionersMax:\n\t\tlimit = DefaultProvisionersMax\n\t}\n\n\tn := c.sorted.Len()\n\tcursor = fmt.Sprintf(\"%040s\", cursor)\n\ti := sort.Search(n, func(i int) bool { return c.sorted[i].uid >= cursor })\n\n\tslice := List{}\n\tfor ; i < n && len(slice) < limit; i++ {\n\t\tslice = append(slice, c.sorted[i].provisioner)\n\t}\n\n\tif i < n {\n\t\treturn slice, strings.TrimLeft(c.sorted[i].uid, \"0\")\n\t}\n\treturn slice, \"\"\n}\n\nfunc loadProvisioner(m *sync.Map, key string) (Interface, bool) {\n\ti, ok := m.Load(key)\n\tif !ok {\n\t\treturn nil, false\n\t}\n\tp, ok := i.(Interface)\n\tif !ok {\n\t\treturn nil, false\n\t}\n\treturn p, true\n}\n\n// provisionerSum returns the SHA1 of the provisioners ID. From this we will\n// create the unique and sorted id.\nfunc provisionerSum(p Interface) []byte {\n\t//nolint:gosec // not used for cryptographic security\n\tsum := sha1.Sum([]byte(p.GetID()))\n\treturn sum[:]\n}\n\n// matchesAudience returns true if A and B share at least one element.\nfunc matchesAudience(as, bs []string) bool {\n\tif len(bs) == 0 || len(as) == 0 {\n\t\treturn false\n\t}\n\n\tfor _, b := range bs {\n\t\tfor _, a := range as {\n\t\t\tif b == a || stripPort(a) == stripPort(b) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\t}\n\treturn false\n}\n\n// stripPort attempts to strip the port from the given url. If parsing the url\n// produces errors it will just return the passed argument.\nfunc stripPort(rawurl string) string {\n\tu, err := url.Parse(rawurl)\n\tif err != nil {\n\t\treturn rawurl\n\t}\n\tu.Host = u.Hostname()\n\treturn u.String()\n}\n\n// extractFragment extracts the first fragment of an audience url.\nfunc extractFragment(audience []string) string {\n\tfor _, s := range audience {\n\t\tif u, err := url.Parse(s); err == nil && u.Fragment != \"\" {\n\t\t\treturn u.Fragment\n\t\t}\n\t}\n\treturn \"\"\n}\n"
  },
  {
    "path": "authority/provisioner/collection_test.go",
    "content": "package provisioner\n\nimport (\n\t\"crypto/x509\"\n\t\"crypto/x509/pkix\"\n\t\"reflect\"\n\t\"strings\"\n\t\"sync\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"go.step.sm/crypto/jose\"\n)\n\nfunc TestCollection_Load(t *testing.T) {\n\tp, err := generateJWK()\n\trequire.NoError(t, err)\n\tbyID := new(sync.Map)\n\tbyID.Store(p.GetID(), p)\n\tbyID.Store(\"string\", \"a-string\")\n\n\ttype fields struct {\n\t\tbyID *sync.Map\n\t}\n\ttype args struct {\n\t\tid string\n\t}\n\ttests := []struct {\n\t\tname   string\n\t\tfields fields\n\t\targs   args\n\t\twant   Interface\n\t\twant1  bool\n\t}{\n\t\t{\"ok\", fields{byID}, args{p.GetID()}, p, true},\n\t\t{\"fail\", fields{byID}, args{\"fail\"}, nil, false},\n\t\t{\"invalid\", fields{byID}, args{\"string\"}, nil, false},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tc := &Collection{\n\t\t\t\tbyID: tt.fields.byID,\n\t\t\t}\n\t\t\tgot, got1 := c.Load(tt.args.id)\n\t\t\tif !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"Collection.Load() got = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t\tif got1 != tt.want1 {\n\t\t\t\tt.Errorf(\"Collection.Load() got1 = %v, want %v\", got1, tt.want1)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestCollection_LoadByTokenID(t *testing.T) {\n\tp1, err := generateJWK()\n\trequire.NoError(t, err)\n\tp2, err := generateACME()\n\trequire.NoError(t, err)\n\n\tbyTokenID := new(sync.Map)\n\tbyTokenID.Store(p1.GetIDForToken(), p1)\n\tbyTokenID.Store(p2.GetIDForToken(), p2)\n\tbyTokenID.Store(\"string\", \"a-string\")\n\n\ttype fields struct {\n\t\tbyTokenID *sync.Map\n\t}\n\ttype args struct {\n\t\tid string\n\t}\n\ttests := []struct {\n\t\tname   string\n\t\tfields fields\n\t\targs   args\n\t\twant   Interface\n\t\twant1  bool\n\t}{\n\t\t{\"ok jwk\", fields{byTokenID}, args{p1.GetIDForToken()}, p1, true},\n\t\t{\"ok acme\", fields{byTokenID}, args{p2.GetIDForToken()}, p2, true},\n\t\t{\"fail missing\", fields{byTokenID}, args{\"missing\"}, nil, false},\n\t\t{\"invalid\", fields{byTokenID}, args{\"string\"}, nil, false},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tc := &Collection{\n\t\t\t\tbyTokenID: tt.fields.byTokenID,\n\t\t\t}\n\t\t\tgot, got1 := c.LoadByTokenID(tt.args.id)\n\t\t\tif !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"Collection.Load() got = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t\tif got1 != tt.want1 {\n\t\t\t\tt.Errorf(\"Collection.Load() got1 = %v, want %v\", got1, tt.want1)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestCollection_LoadByToken(t *testing.T) {\n\tp1, err := generateJWK()\n\trequire.NoError(t, err)\n\tp2, err := generateJWK()\n\trequire.NoError(t, err)\n\tp3, err := generateOIDC()\n\trequire.NoError(t, err)\n\tp4, err := generateK8sSA(nil)\n\trequire.NoError(t, err)\n\n\tbyID := new(sync.Map)\n\tbyID.Store(p1.GetID(), p1)\n\tbyID.Store(p2.GetID(), p2)\n\tbyID.Store(p3.GetID(), p3)\n\tbyID.Store(p4.GetID(), p4)\n\tbyID.Store(\"string\", \"a-string\")\n\n\tbyID2 := new(sync.Map)\n\tbyID2.Store(p1.GetID(), p1)\n\tbyID2.Store(p2.GetID(), p2)\n\tbyID2.Store(p3.GetID(), p3)\n\n\tjwk, err := decryptJSONWebKey(p1.EncryptedKey)\n\trequire.NoError(t, err)\n\ttoken, err := generateSimpleToken(p1.Name, testAudiences.Sign[0], jwk)\n\trequire.NoError(t, err)\n\tt1, c1, err := parseToken(token)\n\trequire.NoError(t, err)\n\n\tjwk, err = decryptJSONWebKey(p2.EncryptedKey)\n\trequire.NoError(t, err)\n\ttoken, err = generateSimpleToken(p2.Name, testAudiences.Sign[1], jwk)\n\trequire.NoError(t, err)\n\tt2, c2, err := parseToken(token)\n\trequire.NoError(t, err)\n\n\ttoken, err = generateSimpleToken(p3.configuration.Issuer, p3.ClientID, &p3.keyStore.keySet.Keys[0])\n\trequire.NoError(t, err)\n\tt3, c3, err := parseToken(token)\n\trequire.NoError(t, err)\n\n\ttoken, err = generateSimpleToken(p3.configuration.Issuer, \"string\", &p3.keyStore.keySet.Keys[0])\n\trequire.NoError(t, err)\n\tt4, c4, err := parseToken(token)\n\trequire.NoError(t, err)\n\n\tjwk, err = jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\trequire.NoError(t, err)\n\ttoken, err = generateK8sSAToken(jwk, nil)\n\trequire.NoError(t, err)\n\tt5, c5, err := parseToken(token)\n\trequire.NoError(t, err)\n\n\ttype fields struct {\n\t\tbyID      *sync.Map\n\t\taudiences Audiences\n\t}\n\ttype args struct {\n\t\ttoken  *jose.JSONWebToken\n\t\tclaims *jose.Claims\n\t}\n\ttests := []struct {\n\t\tname   string\n\t\tfields fields\n\t\targs   args\n\t\twant   Interface\n\t\twant1  bool\n\t}{\n\t\t{\"ok1\", fields{byID, testAudiences}, args{t1, c1}, p1, true},\n\t\t{\"ok2\", fields{byID, testAudiences}, args{t2, c2}, p2, true},\n\t\t{\"ok3\", fields{byID, testAudiences}, args{t3, c3}, p3, true},\n\t\t{\"ok4\", fields{byID, testAudiences}, args{t5, c5}, p4, true},\n\t\t{\"bad\", fields{byID, testAudiences}, args{t4, c4}, nil, false},\n\t\t{\"fail\", fields{byID, Audiences{Sign: []string{\"https://foo\"}}}, args{t1, c1}, nil, false},\n\t\t{\"fail-no-k8sSa-provisioner\", fields{byID2, testAudiences}, args{t5, c5}, nil, false},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tc := &Collection{\n\t\t\t\tbyID:      tt.fields.byID,\n\t\t\t\tbyTokenID: tt.fields.byID,\n\t\t\t\taudiences: tt.fields.audiences,\n\t\t\t}\n\t\t\tgot, got1 := c.LoadByToken(tt.args.token, tt.args.claims)\n\t\t\tif !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"Collection.LoadByToken() got = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t\tif got1 != tt.want1 {\n\t\t\t\tt.Errorf(\"Collection.LoadByToken() got1 = %v, want %v\", got1, tt.want1)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestCollection_LoadByCertificate(t *testing.T) {\n\tmustExtension := func(typ Type, name, credentialID string) pkix.Extension {\n\t\te := Extension{\n\t\t\tType: typ, Name: name, CredentialID: credentialID,\n\t\t}\n\t\text, err := e.ToExtension()\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\treturn ext\n\t}\n\n\tp1, err := generateJWK()\n\trequire.NoError(t, err)\n\tp2, err := generateOIDC()\n\trequire.NoError(t, err)\n\tp3, err := generateACME()\n\trequire.NoError(t, err)\n\n\tbyName := new(sync.Map)\n\tbyName.Store(p1.GetName(), p1)\n\tbyName.Store(p2.GetName(), p2)\n\tbyName.Store(p3.GetName(), p3)\n\n\tok1Cert := &x509.Certificate{\n\t\tExtensions: []pkix.Extension{mustExtension(1, p1.Name, p1.Key.KeyID)},\n\t}\n\tok2Cert := &x509.Certificate{\n\t\tExtensions: []pkix.Extension{mustExtension(2, p2.Name, p2.ClientID)},\n\t}\n\tok3Cert := &x509.Certificate{\n\t\tExtensions: []pkix.Extension{mustExtension(TypeACME, p3.Name, \"\")},\n\t}\n\tnotFoundCert := &x509.Certificate{\n\t\tExtensions: []pkix.Extension{mustExtension(1, \"foo\", \"bar\")},\n\t}\n\tbadCert := &x509.Certificate{\n\t\tExtensions: []pkix.Extension{\n\t\t\t{Id: StepOIDProvisioner, Critical: false, Value: []byte(\"foobar\")},\n\t\t},\n\t}\n\n\ttype fields struct {\n\t\tbyName    *sync.Map\n\t\taudiences Audiences\n\t}\n\ttype args struct {\n\t\tcert *x509.Certificate\n\t}\n\ttests := []struct {\n\t\tname   string\n\t\tfields fields\n\t\targs   args\n\t\twant   Interface\n\t\twant1  bool\n\t}{\n\t\t{\"ok1\", fields{byName, testAudiences}, args{ok1Cert}, p1, true},\n\t\t{\"ok2\", fields{byName, testAudiences}, args{ok2Cert}, p2, true},\n\t\t{\"ok3\", fields{byName, testAudiences}, args{ok3Cert}, p3, true},\n\t\t{\"noExtension\", fields{byName, testAudiences}, args{&x509.Certificate{}}, &noop{}, true},\n\t\t{\"notFound\", fields{byName, testAudiences}, args{notFoundCert}, nil, false},\n\t\t{\"badCert\", fields{byName, testAudiences}, args{badCert}, nil, false},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tc := &Collection{\n\t\t\t\tbyName:    tt.fields.byName,\n\t\t\t\taudiences: tt.fields.audiences,\n\t\t\t}\n\t\t\tgot, got1 := c.LoadByCertificate(tt.args.cert)\n\t\t\tif !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"Collection.LoadByCertificate() got = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t\tif got1 != tt.want1 {\n\t\t\t\tt.Errorf(\"Collection.LoadByCertificate() got1 = %v, want %v\", got1, tt.want1)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestCollection_LoadEncryptedKey(t *testing.T) {\n\tc := NewCollection(testAudiences)\n\tp1, err := generateJWK()\n\trequire.NoError(t, err)\n\trequire.NoError(t, c.Store(p1))\n\tp2, err := generateOIDC()\n\trequire.NoError(t, err)\n\trequire.NoError(t, c.Store(p2))\n\n\t// Add oidc in byKey.\n\t// It should not happen.\n\tp2KeyID := p2.keyStore.keySet.Keys[0].KeyID\n\tc.byKey.Store(p2KeyID, p2)\n\n\ttype args struct {\n\t\tkeyID string\n\t}\n\ttests := []struct {\n\t\tname  string\n\t\targs  args\n\t\twant  string\n\t\twant1 bool\n\t}{\n\t\t{\"ok\", args{p1.Key.KeyID}, p1.EncryptedKey, true},\n\t\t{\"oidc\", args{p2KeyID}, \"\", false},\n\t\t{\"notFound\", args{\"not-found\"}, \"\", false},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, got1 := c.LoadEncryptedKey(tt.args.keyID)\n\t\t\tif got != tt.want {\n\t\t\t\tt.Errorf(\"Collection.LoadEncryptedKey() got = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t\tif got1 != tt.want1 {\n\t\t\t\tt.Errorf(\"Collection.LoadEncryptedKey() got1 = %v, want %v\", got1, tt.want1)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestCollection_Store(t *testing.T) {\n\tc := NewCollection(testAudiences)\n\tp1, err := generateJWK()\n\trequire.NoError(t, err)\n\tp2, err := generateOIDC()\n\trequire.NoError(t, err)\n\n\ttype args struct {\n\t\tp Interface\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\targs    args\n\t\twantErr bool\n\t}{\n\t\t{\"ok1\", args{p1}, false},\n\t\t{\"ok2\", args{p2}, false},\n\t\t{\"fail1\", args{p1}, true},\n\t\t{\"fail2\", args{p2}, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif err := c.Store(tt.args.p); (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"Collection.Store() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestCollection_Find(t *testing.T) {\n\tc, err := generateCollection(10, 10)\n\trequire.NoError(t, err)\n\n\ttrim := func(s string) string {\n\t\treturn strings.TrimLeft(s, \"0\")\n\t}\n\ttoList := func(ps provisionerSlice) List {\n\t\tl := List{}\n\t\tfor _, p := range ps {\n\t\t\tl = append(l, p.provisioner)\n\t\t}\n\t\treturn l\n\t}\n\n\ttype args struct {\n\t\tcursor string\n\t\tlimit  int\n\t}\n\ttests := []struct {\n\t\tname  string\n\t\targs  args\n\t\twant  List\n\t\twant1 string\n\t}{\n\t\t{\"all\", args{\"\", DefaultProvisionersMax}, toList(c.sorted[0:20]), \"\"},\n\t\t{\"0 to 19\", args{\"\", 20}, toList(c.sorted[0:20]), \"\"},\n\t\t{\"0 to 9\", args{\"\", 10}, toList(c.sorted[0:10]), trim(c.sorted[10].uid)},\n\t\t{\"9 to 19\", args{trim(c.sorted[10].uid), 10}, toList(c.sorted[10:20]), \"\"},\n\t\t{\"1\", args{trim(c.sorted[1].uid), 1}, toList(c.sorted[1:2]), trim(c.sorted[2].uid)},\n\t\t{\"1 to 5\", args{trim(c.sorted[1].uid), 4}, toList(c.sorted[1:5]), trim(c.sorted[5].uid)},\n\t\t{\"defaultLimit\", args{\"\", 0}, toList(c.sorted[0:20]), \"\"},\n\t\t{\"overTheLimit\", args{\"\", DefaultProvisionersMax + 1}, toList(c.sorted[0:20]), \"\"},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, got1 := c.Find(tt.args.cursor, tt.args.limit)\n\t\t\tif !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"Collection.Find() got = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t\tif got1 != tt.want1 {\n\t\t\t\tt.Errorf(\"Collection.Find() got1 = %v, want %v\", got1, tt.want1)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_matchesAudience(t *testing.T) {\n\ttype matchesTest struct {\n\t\ta, b []string\n\t\texp  bool\n\t}\n\ttests := map[string]matchesTest{\n\t\t\"false arg1 empty\": {\n\t\t\ta:   []string{},\n\t\t\tb:   []string{\"https://127.0.0.1:0/sign\", \"https://test.ca.smallstep.com/sign\"},\n\t\t\texp: false,\n\t\t},\n\t\t\"false arg2 empty\": {\n\t\t\ta:   []string{\"https://127.0.0.1:0/sign\", \"https://test.ca.smallstep.com/sign\"},\n\t\t\tb:   []string{},\n\t\t\texp: false,\n\t\t},\n\t\t\"false arg1,arg2 empty\": {\n\t\t\ta:   []string{\"https://127.0.0.1:0/sign\", \"https://test.ca.smallstep.com/sign\"},\n\t\t\tb:   []string{\"step-gateway\", \"step-cli\"},\n\t\t\texp: false,\n\t\t},\n\t\t\"false\": {\n\t\t\ta:   []string{\"step-gateway\", \"step-cli\"},\n\t\t\tb:   []string{\"https://127.0.0.1:0/sign\", \"https://test.ca.smallstep.com/sign\"},\n\t\t\texp: false,\n\t\t},\n\t\t\"true\": {\n\t\t\ta:   []string{\"step-gateway\", \"https://test.ca.smallstep.com/sign\"},\n\t\t\tb:   []string{\"https://127.0.0.1:0/sign\", \"https://test.ca.smallstep.com/sign\"},\n\t\t\texp: true,\n\t\t},\n\t\t\"true,portsA\": {\n\t\t\ta:   []string{\"step-gateway\", \"https://test.ca.smallstep.com:9000/sign\"},\n\t\t\tb:   []string{\"https://127.0.0.1:0/sign\", \"https://test.ca.smallstep.com/sign\"},\n\t\t\texp: true,\n\t\t},\n\t\t\"true,portsB\": {\n\t\t\ta:   []string{\"step-gateway\", \"https://test.ca.smallstep.com/sign\"},\n\t\t\tb:   []string{\"https://127.0.0.1:0/sign\", \"https://test.ca.smallstep.com:9000/sign\"},\n\t\t\texp: true,\n\t\t},\n\t\t\"true,portsAB\": {\n\t\t\ta:   []string{\"step-gateway\", \"https://test.ca.smallstep.com:9000/sign\"},\n\t\t\tb:   []string{\"https://127.0.0.1:0/sign\", \"https://test.ca.smallstep.com:8000/sign\"},\n\t\t\texp: true,\n\t\t},\n\t}\n\tfor name, tc := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tassert.Equal(t, tc.exp, matchesAudience(tc.a, tc.b))\n\t\t})\n\t}\n}\n\nfunc Test_stripPort(t *testing.T) {\n\ttype args struct {\n\t\trawurl string\n\t}\n\ttests := []struct {\n\t\tname string\n\t\targs args\n\t\twant string\n\t}{\n\t\t{\"with port\", args{\"https://ca.smallstep.com:9000/sign\"}, \"https://ca.smallstep.com/sign\"},\n\t\t{\"with no port\", args{\"https://ca.smallstep.com/sign/\"}, \"https://ca.smallstep.com/sign/\"},\n\t\t{\"bad url\", args{\"https://a bad url:9000\"}, \"https://a bad url:9000\"},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif got := stripPort(tt.args.rawurl); got != tt.want {\n\t\t\t\tt.Errorf(\"stripPort() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "authority/provisioner/controller.go",
    "content": "package provisioner\n\nimport (\n\t\"context\"\n\t\"crypto/x509\"\n\t\"net/http\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n\t\"golang.org/x/crypto/ssh\"\n\n\t\"github.com/smallstep/linkedca\"\n\n\t\"github.com/smallstep/certificates/errs\"\n\t\"github.com/smallstep/certificates/internal/cast\"\n\t\"github.com/smallstep/certificates/internal/httptransport\"\n\t\"github.com/smallstep/certificates/webhook\"\n)\n\n// Controller wraps a provisioner with other attributes useful in callback\n// functions.\ntype Controller struct {\n\tInterface\n\tAudiences             *Audiences\n\tClaimer               *Claimer\n\tIdentityFunc          GetIdentityFunc\n\tAuthorizeRenewFunc    AuthorizeRenewFunc\n\tAuthorizeSSHRenewFunc AuthorizeSSHRenewFunc\n\tpolicy                *policyEngine\n\thttpClient            HTTPClient\n\twebhookClient         HTTPClient\n\twebhooks              []*Webhook\n\twrapTransport         httptransport.Wrapper\n}\n\n// NewController initializes a new provisioner controller.\nfunc NewController(p Interface, claims *Claims, config Config, options *Options) (*Controller, error) {\n\tclaimer, err := NewClaimer(claims, config.Claims)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tpolicy, err := newPolicyEngine(options)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\twt := config.WrapTransport\n\tif wt == nil {\n\t\twt = httptransport.NoopWrapper()\n\t}\n\tfor _, wh := range options.GetWebhooks() {\n\t\tif err := wh.Validate(); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn &Controller{\n\t\tInterface:             p,\n\t\tAudiences:             &config.Audiences,\n\t\tClaimer:               claimer,\n\t\tIdentityFunc:          config.GetIdentityFunc,\n\t\tAuthorizeRenewFunc:    config.AuthorizeRenewFunc,\n\t\tAuthorizeSSHRenewFunc: config.AuthorizeSSHRenewFunc,\n\t\tpolicy:                policy,\n\t\twebhookClient:         config.WebhookClient,\n\t\twebhooks:              options.GetWebhooks(),\n\t\thttpClient:            config.HTTPClient,\n\t\twrapTransport:         wt,\n\t}, nil\n}\n\n// GetHTTPClient returns the configured HTTP client or the default one if none\n// is configured.\nfunc (c *Controller) GetHTTPClient() HTTPClient {\n\tif c.httpClient != nil {\n\t\treturn c.httpClient\n\t}\n\treturn &http.Client{}\n}\n\n// GetIdentity returns the identity for a given email.\nfunc (c *Controller) GetIdentity(ctx context.Context, email string) (*Identity, error) {\n\tif c.IdentityFunc != nil {\n\t\treturn c.IdentityFunc(ctx, c.Interface, email)\n\t}\n\treturn DefaultIdentityFunc(ctx, c.Interface, email)\n}\n\n// AuthorizeRenew returns nil if the given cert can be renewed, returns an error\n// otherwise.\nfunc (c *Controller) AuthorizeRenew(ctx context.Context, cert *x509.Certificate) error {\n\tif c.AuthorizeRenewFunc != nil {\n\t\treturn c.AuthorizeRenewFunc(ctx, c, cert)\n\t}\n\treturn DefaultAuthorizeRenew(ctx, c, cert)\n}\n\n// AuthorizeSSHRenew returns nil if the given cert can be renewed, returns an\n// error otherwise.\nfunc (c *Controller) AuthorizeSSHRenew(ctx context.Context, cert *ssh.Certificate) error {\n\tif c.AuthorizeSSHRenewFunc != nil {\n\t\treturn c.AuthorizeSSHRenewFunc(ctx, c, cert)\n\t}\n\treturn DefaultAuthorizeSSHRenew(ctx, c, cert)\n}\n\nfunc (c *Controller) newWebhookController(templateData WebhookSetter, certType linkedca.Webhook_CertType, opts ...webhook.RequestBodyOption) *WebhookController {\n\tclient := c.webhookClient\n\tif client == nil {\n\t\tclient = &http.Client{\n\t\t\tTransport: c.wrapTransport(httptransport.New()),\n\t\t}\n\t}\n\n\treturn &WebhookController{\n\t\tTemplateData:  templateData,\n\t\tclient:        client,\n\t\twrapTransport: c.wrapTransport,\n\t\twebhooks:      c.webhooks,\n\t\tcertType:      certType,\n\t\toptions:       opts,\n\t}\n}\n\n// Identity is the type representing an externally supplied identity that is used\n// by provisioners to populate certificate fields.\ntype Identity struct {\n\tUsernames   []string `json:\"usernames\"`\n\tPermissions `json:\"permissions\"`\n}\n\n// GetIdentityFunc is a function that returns an identity.\ntype GetIdentityFunc func(ctx context.Context, p Interface, email string) (*Identity, error)\n\n// AuthorizeRenewFunc is a function that returns nil if the renewal of a\n// certificate is enabled.\ntype AuthorizeRenewFunc func(ctx context.Context, p *Controller, cert *x509.Certificate) error\n\n// AuthorizeSSHRenewFunc is a function that returns nil if the renewal of the\n// given SSH certificate is enabled.\ntype AuthorizeSSHRenewFunc func(ctx context.Context, p *Controller, cert *ssh.Certificate) error\n\n// DefaultIdentityFunc return a default identity depending on the provisioner\n// type. For OIDC email is always present and the usernames might\n// contain empty strings.\nfunc DefaultIdentityFunc(_ context.Context, p Interface, email string) (*Identity, error) {\n\tswitch k := p.(type) {\n\tcase *OIDC:\n\t\t// OIDC principals would be:\n\t\t//   ~~1. Preferred usernames.~~ Note: Under discussion, currently disabled\n\t\t//   2. Sanitized local.\n\t\t//   3. Raw local (if different).\n\t\t//   4. Email address.\n\t\tname := SanitizeSSHUserPrincipal(email)\n\t\tusernames := []string{name}\n\t\tif i := strings.LastIndex(email, \"@\"); i >= 0 {\n\t\t\tusernames = append(usernames, email[:i])\n\t\t}\n\t\tusernames = append(usernames, email)\n\t\treturn &Identity{\n\t\t\t// Remove duplicated and empty usernames.\n\t\t\tUsernames: SanitizeStringSlices(usernames),\n\t\t}, nil\n\tdefault:\n\t\treturn nil, errors.Errorf(\"provisioner type '%T' not supported by identity function\", k)\n\t}\n}\n\n// DefaultAuthorizeRenew is the default implementation of AuthorizeRenew. It\n// will return an error if the provisioner has the renewal disabled, if the\n// certificate is not yet valid or if the certificate is expired and renew after\n// expiry is disabled.\nfunc DefaultAuthorizeRenew(_ context.Context, p *Controller, cert *x509.Certificate) error {\n\tif p.Claimer.IsDisableRenewal() {\n\t\treturn errs.Unauthorized(\"renew is disabled for provisioner '%s'\", p.GetName())\n\t}\n\n\tnow := time.Now().Truncate(time.Second)\n\tif now.Before(cert.NotBefore) {\n\t\treturn errs.Unauthorized(\"certificate is not yet valid\" + \" \" + now.UTC().Format(time.RFC3339Nano) + \" vs \" + cert.NotBefore.Format(time.RFC3339Nano))\n\t}\n\tif now.After(cert.NotAfter) && !p.Claimer.AllowRenewalAfterExpiry() {\n\t\t// return a custom 401 Unauthorized error with a clearer message for the client\n\t\t// TODO(hs): these errors likely need to be refactored as a whole; HTTP status codes shouldn't be in this layer.\n\t\treturn errs.New(http.StatusUnauthorized, \"The request lacked necessary authorization to be completed: certificate expired on %s\", cert.NotAfter)\n\t}\n\n\treturn nil\n}\n\n// DefaultAuthorizeSSHRenew is the default implementation of AuthorizeSSHRenew. It\n// will return an error if the provisioner has the renewal disabled, if the\n// certificate is not yet valid or if the certificate is expired and renew after\n// expiry is disabled.\nfunc DefaultAuthorizeSSHRenew(_ context.Context, p *Controller, cert *ssh.Certificate) error {\n\tif p.Claimer.IsDisableRenewal() {\n\t\treturn errs.Unauthorized(\"renew is disabled for provisioner '%s'\", p.GetName())\n\t}\n\n\tunixNow := time.Now().Unix()\n\tif after := cast.Int64(cert.ValidAfter); after < 0 || unixNow < cast.Int64(cert.ValidAfter) {\n\t\treturn errs.Unauthorized(\"certificate is not yet valid\")\n\t}\n\tif before := cast.Int64(cert.ValidBefore); cert.ValidBefore != uint64(ssh.CertTimeInfinity) && (unixNow >= before || before < 0) && !p.Claimer.AllowRenewalAfterExpiry() {\n\t\treturn errs.Unauthorized(\"certificate has expired\")\n\t}\n\n\treturn nil\n}\n\n// SanitizeStringSlices removes duplicated an empty strings.\nfunc SanitizeStringSlices(original []string) []string {\n\toutput := []string{}\n\tseen := make(map[string]struct{})\n\tfor _, entry := range original {\n\t\tif entry == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif _, value := seen[entry]; !value {\n\t\t\tseen[entry] = struct{}{}\n\t\t\toutput = append(output, entry)\n\t\t}\n\t}\n\treturn output\n}\n\n// SanitizeSSHUserPrincipal grabs an email or a string with the format\n// local@domain and returns a sanitized version of the local, valid to be used\n// as a user name. If the email starts with a letter between a and z, the\n// resulting string will match the regular expression `^[a-z][-a-z0-9_]*$`.\nfunc SanitizeSSHUserPrincipal(email string) string {\n\tif i := strings.LastIndex(email, \"@\"); i >= 0 {\n\t\temail = email[:i]\n\t}\n\treturn strings.Map(func(r rune) rune {\n\t\tswitch {\n\t\tcase r >= 'a' && r <= 'z':\n\t\t\treturn r\n\t\tcase r >= '0' && r <= '9':\n\t\t\treturn r\n\t\tcase r == '-':\n\t\t\treturn '-'\n\t\tcase r == '.': // drop dots\n\t\t\treturn -1\n\t\tdefault:\n\t\t\treturn '_'\n\t\t}\n\t}, strings.ToLower(email))\n}\n\nfunc (c *Controller) getPolicy() *policyEngine {\n\tif c == nil {\n\t\treturn nil\n\t}\n\treturn c.policy\n}\n"
  },
  {
    "path": "authority/provisioner/controller_test.go",
    "content": "package provisioner\n\nimport (\n\t\"context\"\n\t\"crypto/x509\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"reflect\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/smallstep/certificates/authority/policy\"\n\t\"github.com/smallstep/certificates/internal/httptransport\"\n\t\"github.com/smallstep/certificates/webhook\"\n\t\"github.com/smallstep/linkedca\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"go.step.sm/crypto/pemutil\"\n\t\"go.step.sm/crypto/x509util\"\n\t\"golang.org/x/crypto/ssh\"\n)\n\nvar trueValue = true\n\nfunc mustClaimer(t *testing.T, claims *Claims, global Claims) *Claimer {\n\tt.Helper()\n\tc, err := NewClaimer(claims, global)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn c\n}\nfunc mustDuration(t *testing.T, s string) *Duration {\n\tt.Helper()\n\td, err := NewDuration(s)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn d\n}\n\nfunc mustNewPolicyEngine(t *testing.T, options *Options) *policyEngine {\n\tt.Helper()\n\tc, err := newPolicyEngine(options)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn c\n}\n\nfunc TestNewController(t *testing.T) {\n\toptions := &Options{\n\t\tX509: &X509Options{\n\t\t\tAllowedNames: &policy.X509NameOptions{\n\t\t\t\tDNSDomains: []string{\"*.local\"},\n\t\t\t},\n\t\t},\n\t\tSSH: &SSHOptions{\n\t\t\tHost: &policy.SSHHostCertificateOptions{\n\t\t\t\tAllowedNames: &policy.SSHNameOptions{\n\t\t\t\t\tDNSDomains: []string{\"*.local\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tUser: &policy.SSHUserCertificateOptions{\n\t\t\t\tAllowedNames: &policy.SSHNameOptions{\n\t\t\t\t\tEmailAddresses: []string{\"@example.com\"},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\ttype args struct {\n\t\tp       Interface\n\t\tclaims  *Claims\n\t\tconfig  Config\n\t\toptions *Options\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\targs    args\n\t\twant    *Controller\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", args{&JWK{}, nil, Config{\n\t\t\tClaims:        globalProvisionerClaims,\n\t\t\tAudiences:     testAudiences,\n\t\t\tHTTPClient:    &http.Client{},\n\t\t\tWrapTransport: httptransport.NoopWrapper(),\n\t\t}, nil}, &Controller{\n\t\t\tInterface:     &JWK{},\n\t\t\tAudiences:     &testAudiences,\n\t\t\tClaimer:       mustClaimer(t, nil, globalProvisionerClaims),\n\t\t\thttpClient:    &http.Client{},\n\t\t\twrapTransport: httptransport.NoopWrapper(),\n\t\t}, false},\n\t\t{\"ok with claims\", args{&JWK{}, &Claims{\n\t\t\tDisableRenewal: &defaultDisableRenewal,\n\t\t}, Config{\n\t\t\tClaims:    globalProvisionerClaims,\n\t\t\tAudiences: testAudiences,\n\t\t}, nil}, &Controller{\n\t\t\tInterface: &JWK{},\n\t\t\tAudiences: &testAudiences,\n\t\t\tClaimer: mustClaimer(t, &Claims{\n\t\t\t\tDisableRenewal: &defaultDisableRenewal,\n\t\t\t}, globalProvisionerClaims),\n\t\t\twrapTransport: httptransport.NoopWrapper(),\n\t\t}, false},\n\t\t{\"ok with claims and options\", args{&JWK{}, &Claims{\n\t\t\tDisableRenewal: &defaultDisableRenewal,\n\t\t}, Config{\n\t\t\tClaims:    globalProvisionerClaims,\n\t\t\tAudiences: testAudiences,\n\t\t}, options}, &Controller{\n\t\t\tInterface: &JWK{},\n\t\t\tAudiences: &testAudiences,\n\t\t\tClaimer: mustClaimer(t, &Claims{\n\t\t\t\tDisableRenewal: &defaultDisableRenewal,\n\t\t\t}, globalProvisionerClaims),\n\t\t\tpolicy:        mustNewPolicyEngine(t, options),\n\t\t\twrapTransport: httptransport.NoopWrapper(),\n\t\t}, false},\n\t\t{\"fail claimer\", args{&JWK{}, &Claims{\n\t\t\tMinTLSDur: mustDuration(t, \"24h\"),\n\t\t\tMaxTLSDur: mustDuration(t, \"2h\"),\n\t\t}, Config{\n\t\t\tClaims:    globalProvisionerClaims,\n\t\t\tAudiences: testAudiences,\n\t\t}, nil}, nil, true},\n\t\t{\"fail options\", args{&JWK{}, &Claims{\n\t\t\tDisableRenewal: &defaultDisableRenewal,\n\t\t}, Config{\n\t\t\tClaims:    globalProvisionerClaims,\n\t\t\tAudiences: testAudiences,\n\t\t}, &Options{\n\t\t\tX509: &X509Options{\n\t\t\t\tAllowedNames: &policy.X509NameOptions{\n\t\t\t\t\tDNSDomains: []string{\"**.local\"},\n\t\t\t\t},\n\t\t\t},\n\t\t}}, nil, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := NewController(tt.args.p, tt.args.claims, tt.args.config, tt.args.options)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"NewController() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t// A function can only be compared to nil\n\t\t\tif tt.want != nil && got != nil {\n\t\t\t\tassert.NotNil(t, got.wrapTransport)\n\t\t\t\ttt.want.wrapTransport = nil\n\t\t\t\tgot.wrapTransport = nil\n\t\t\t}\n\n\t\t\tif !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"NewController() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestController_GetHTTPClient(t *testing.T) {\n\tsrv := generateTLSJWKServer(2)\n\tdefer srv.Close()\n\ttype fields struct {\n\t\thttpClient *http.Client\n\t}\n\ttests := []struct {\n\t\tname   string\n\t\tfields fields\n\t\twant   *http.Client\n\t}{\n\t\t{\"ok custom\", fields{srv.Client()}, srv.Client()},\n\t\t{\"ok default\", fields{http.DefaultClient}, http.DefaultClient},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tc := &Controller{\n\t\t\t\thttpClient: tt.fields.httpClient,\n\t\t\t}\n\t\t\tassert.Equal(t, tt.want, c.GetHTTPClient())\n\t\t})\n\t}\n}\n\nfunc TestController_GetIdentity(t *testing.T) {\n\tctx := context.Background()\n\ttype fields struct {\n\t\tInterface    Interface\n\t\tIdentityFunc GetIdentityFunc\n\t}\n\ttype args struct {\n\t\tctx   context.Context\n\t\temail string\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tfields  fields\n\t\targs    args\n\t\twant    *Identity\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", fields{&OIDC{}, nil}, args{ctx, \"jane@doe.org\"}, &Identity{\n\t\t\tUsernames: []string{\"jane\", \"jane@doe.org\"},\n\t\t}, false},\n\t\t{\"ok custom\", fields{&OIDC{}, func(ctx context.Context, p Interface, email string) (*Identity, error) {\n\t\t\treturn &Identity{Usernames: []string{\"jane\"}}, nil\n\t\t}}, args{ctx, \"jane@doe.org\"}, &Identity{\n\t\t\tUsernames: []string{\"jane\"},\n\t\t}, false},\n\t\t{\"ok badname\", fields{&OIDC{}, nil}, args{ctx, \"1000@doe.org\"}, &Identity{\n\t\t\tUsernames: []string{\"1000\", \"1000@doe.org\"},\n\t\t}, false},\n\t\t{\"ok sanitized badname\", fields{&OIDC{}, nil}, args{ctx, \"1000+10@doe.org\"}, &Identity{\n\t\t\tUsernames: []string{\"1000_10\", \"1000+10\", \"1000+10@doe.org\"},\n\t\t}, false},\n\t\t{\"fail provisioner\", fields{&JWK{}, nil}, args{ctx, \"jane@doe.org\"}, nil, true},\n\t\t{\"fail custom\", fields{&OIDC{}, func(ctx context.Context, p Interface, email string) (*Identity, error) {\n\t\t\treturn nil, fmt.Errorf(\"an error\")\n\t\t}}, args{ctx, \"jane@doe.org\"}, nil, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tc := &Controller{\n\t\t\t\tInterface:    tt.fields.Interface,\n\t\t\t\tIdentityFunc: tt.fields.IdentityFunc,\n\t\t\t}\n\t\t\tgot, err := c.GetIdentity(tt.args.ctx, tt.args.email)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"Controller.GetIdentity() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"Controller.GetIdentity() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestController_AuthorizeRenew(t *testing.T) {\n\tctx := context.Background()\n\tnow := time.Now().Truncate(time.Second)\n\ttype fields struct {\n\t\tInterface          Interface\n\t\tClaimer            *Claimer\n\t\tAuthorizeRenewFunc AuthorizeRenewFunc\n\t}\n\ttype args struct {\n\t\tctx  context.Context\n\t\tcert *x509.Certificate\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tfields  fields\n\t\targs    args\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", fields{&JWK{}, mustClaimer(t, nil, globalProvisionerClaims), nil}, args{ctx, &x509.Certificate{\n\t\t\tNotBefore: now,\n\t\t\tNotAfter:  now.Add(time.Hour),\n\t\t}}, false},\n\t\t{\"ok custom\", fields{&JWK{}, mustClaimer(t, nil, globalProvisionerClaims), func(ctx context.Context, p *Controller, cert *x509.Certificate) error {\n\t\t\treturn nil\n\t\t}}, args{ctx, &x509.Certificate{\n\t\t\tNotBefore: now,\n\t\t\tNotAfter:  now.Add(time.Hour),\n\t\t}}, false},\n\t\t{\"ok custom disabled\", fields{&JWK{}, mustClaimer(t, &Claims{AllowRenewalAfterExpiry: &trueValue}, globalProvisionerClaims), func(ctx context.Context, p *Controller, cert *x509.Certificate) error {\n\t\t\treturn nil\n\t\t}}, args{ctx, &x509.Certificate{\n\t\t\tNotBefore: now,\n\t\t\tNotAfter:  now.Add(time.Hour),\n\t\t}}, false},\n\t\t{\"ok renew after expiry\", fields{&JWK{}, mustClaimer(t, &Claims{AllowRenewalAfterExpiry: &trueValue}, globalProvisionerClaims), nil}, args{ctx, &x509.Certificate{\n\t\t\tNotBefore: now.Add(-time.Hour),\n\t\t\tNotAfter:  now.Add(-time.Minute),\n\t\t}}, false},\n\t\t{\"fail disabled\", fields{&JWK{}, mustClaimer(t, &Claims{DisableRenewal: &trueValue}, globalProvisionerClaims), nil}, args{ctx, &x509.Certificate{\n\t\t\tNotBefore: now,\n\t\t\tNotAfter:  now.Add(time.Hour),\n\t\t}}, true},\n\t\t{\"fail not yet valid\", fields{&JWK{}, mustClaimer(t, nil, globalProvisionerClaims), nil}, args{ctx, &x509.Certificate{\n\t\t\tNotBefore: now.Add(time.Hour),\n\t\t\tNotAfter:  now.Add(2 * time.Hour),\n\t\t}}, true},\n\t\t{\"fail expired\", fields{&JWK{}, mustClaimer(t, nil, globalProvisionerClaims), nil}, args{ctx, &x509.Certificate{\n\t\t\tNotBefore: now.Add(-time.Hour),\n\t\t\tNotAfter:  now.Add(-time.Minute),\n\t\t}}, true},\n\t\t{\"fail custom\", fields{&JWK{}, mustClaimer(t, nil, globalProvisionerClaims), func(ctx context.Context, p *Controller, cert *x509.Certificate) error {\n\t\t\treturn fmt.Errorf(\"an error\")\n\t\t}}, args{ctx, &x509.Certificate{\n\t\t\tNotBefore: now,\n\t\t\tNotAfter:  now.Add(time.Hour),\n\t\t}}, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tc := &Controller{\n\t\t\t\tInterface:          tt.fields.Interface,\n\t\t\t\tClaimer:            tt.fields.Claimer,\n\t\t\t\tAuthorizeRenewFunc: tt.fields.AuthorizeRenewFunc,\n\t\t\t}\n\t\t\tif err := c.AuthorizeRenew(tt.args.ctx, tt.args.cert); (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"Controller.AuthorizeRenew() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestController_AuthorizeSSHRenew(t *testing.T) {\n\tctx := context.Background()\n\tnow := time.Now()\n\ttype fields struct {\n\t\tInterface             Interface\n\t\tClaimer               *Claimer\n\t\tAuthorizeSSHRenewFunc AuthorizeSSHRenewFunc\n\t}\n\ttype args struct {\n\t\tctx  context.Context\n\t\tcert *ssh.Certificate\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tfields  fields\n\t\targs    args\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", fields{&JWK{}, mustClaimer(t, nil, globalProvisionerClaims), nil}, args{ctx, &ssh.Certificate{\n\t\t\tValidAfter:  uint64(now.Unix()),\n\t\t\tValidBefore: uint64(now.Add(time.Hour).Unix()),\n\t\t}}, false},\n\t\t{\"ok custom\", fields{&JWK{}, mustClaimer(t, nil, globalProvisionerClaims), func(ctx context.Context, p *Controller, cert *ssh.Certificate) error {\n\t\t\treturn nil\n\t\t}}, args{ctx, &ssh.Certificate{\n\t\t\tValidAfter:  uint64(now.Unix()),\n\t\t\tValidBefore: uint64(now.Add(time.Hour).Unix()),\n\t\t}}, false},\n\t\t{\"ok custom disabled\", fields{&JWK{}, mustClaimer(t, &Claims{AllowRenewalAfterExpiry: &trueValue}, globalProvisionerClaims), func(ctx context.Context, p *Controller, cert *ssh.Certificate) error {\n\t\t\treturn nil\n\t\t}}, args{ctx, &ssh.Certificate{\n\t\t\tValidAfter:  uint64(now.Unix()),\n\t\t\tValidBefore: uint64(now.Add(time.Hour).Unix()),\n\t\t}}, false},\n\t\t{\"ok renew after expiry\", fields{&JWK{}, mustClaimer(t, &Claims{AllowRenewalAfterExpiry: &trueValue}, globalProvisionerClaims), nil}, args{ctx, &ssh.Certificate{\n\t\t\tValidAfter:  uint64(now.Add(-time.Hour).Unix()),\n\t\t\tValidBefore: uint64(now.Add(-time.Minute).Unix()),\n\t\t}}, false},\n\t\t{\"fail disabled\", fields{&JWK{}, mustClaimer(t, &Claims{DisableRenewal: &trueValue}, globalProvisionerClaims), nil}, args{ctx, &ssh.Certificate{\n\t\t\tValidAfter:  uint64(now.Unix()),\n\t\t\tValidBefore: uint64(now.Add(time.Hour).Unix()),\n\t\t}}, true},\n\t\t{\"fail not yet valid\", fields{&JWK{}, mustClaimer(t, nil, globalProvisionerClaims), nil}, args{ctx, &ssh.Certificate{\n\t\t\tValidAfter:  uint64(now.Add(time.Hour).Unix()),\n\t\t\tValidBefore: uint64(now.Add(2 * time.Hour).Unix()),\n\t\t}}, true},\n\t\t{\"fail expired\", fields{&JWK{}, mustClaimer(t, nil, globalProvisionerClaims), nil}, args{ctx, &ssh.Certificate{\n\t\t\tValidAfter:  uint64(now.Add(-time.Hour).Unix()),\n\t\t\tValidBefore: uint64(now.Add(-time.Minute).Unix()),\n\t\t}}, true},\n\t\t{\"fail custom\", fields{&JWK{}, mustClaimer(t, nil, globalProvisionerClaims), func(ctx context.Context, p *Controller, cert *ssh.Certificate) error {\n\t\t\treturn fmt.Errorf(\"an error\")\n\t\t}}, args{ctx, &ssh.Certificate{\n\t\t\tValidAfter:  uint64(now.Unix()),\n\t\t\tValidBefore: uint64(now.Add(time.Hour).Unix()),\n\t\t}}, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tc := &Controller{\n\t\t\t\tInterface:             tt.fields.Interface,\n\t\t\t\tClaimer:               tt.fields.Claimer,\n\t\t\t\tAuthorizeSSHRenewFunc: tt.fields.AuthorizeSSHRenewFunc,\n\t\t\t}\n\t\t\tif err := c.AuthorizeSSHRenew(tt.args.ctx, tt.args.cert); (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"Controller.AuthorizeSSHRenew() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDefaultAuthorizeRenew(t *testing.T) {\n\tctx := context.Background()\n\tnow := time.Now().Truncate(time.Second)\n\ttype args struct {\n\t\tctx  context.Context\n\t\tp    *Controller\n\t\tcert *x509.Certificate\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\targs    args\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", args{ctx, &Controller{\n\t\t\tInterface: &JWK{},\n\t\t\tClaimer:   mustClaimer(t, nil, globalProvisionerClaims),\n\t\t}, &x509.Certificate{\n\t\t\tNotBefore: now,\n\t\t\tNotAfter:  now.Add(time.Hour),\n\t\t}}, false},\n\t\t{\"ok renew after expiry\", args{ctx, &Controller{\n\t\t\tInterface: &JWK{},\n\t\t\tClaimer:   mustClaimer(t, &Claims{AllowRenewalAfterExpiry: &trueValue}, globalProvisionerClaims),\n\t\t}, &x509.Certificate{\n\t\t\tNotBefore: now.Add(-time.Hour),\n\t\t\tNotAfter:  now.Add(-time.Minute),\n\t\t}}, false},\n\t\t{\"fail disabled\", args{ctx, &Controller{\n\t\t\tInterface: &JWK{},\n\t\t\tClaimer:   mustClaimer(t, &Claims{DisableRenewal: &trueValue}, globalProvisionerClaims),\n\t\t}, &x509.Certificate{\n\t\t\tNotBefore: now,\n\t\t\tNotAfter:  now.Add(time.Hour),\n\t\t}}, true},\n\t\t{\"fail not yet valid\", args{ctx, &Controller{\n\t\t\tInterface: &JWK{},\n\t\t\tClaimer:   mustClaimer(t, &Claims{DisableRenewal: &trueValue}, globalProvisionerClaims),\n\t\t}, &x509.Certificate{\n\t\t\tNotBefore: now.Add(time.Hour),\n\t\t\tNotAfter:  now.Add(2 * time.Hour),\n\t\t}}, true},\n\t\t{\"fail expired\", args{ctx, &Controller{\n\t\t\tInterface: &JWK{},\n\t\t\tClaimer:   mustClaimer(t, &Claims{DisableRenewal: &trueValue}, globalProvisionerClaims),\n\t\t}, &x509.Certificate{\n\t\t\tNotBefore: now.Add(-time.Hour),\n\t\t\tNotAfter:  now.Add(-time.Minute),\n\t\t}}, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif err := DefaultAuthorizeRenew(tt.args.ctx, tt.args.p, tt.args.cert); (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"DefaultAuthorizeRenew() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDefaultAuthorizeSSHRenew(t *testing.T) {\n\tctx := context.Background()\n\tnow := time.Now()\n\ttype args struct {\n\t\tctx  context.Context\n\t\tp    *Controller\n\t\tcert *ssh.Certificate\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\targs    args\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", args{ctx, &Controller{\n\t\t\tInterface: &JWK{},\n\t\t\tClaimer:   mustClaimer(t, nil, globalProvisionerClaims),\n\t\t}, &ssh.Certificate{\n\t\t\tValidAfter:  uint64(now.Unix()),\n\t\t\tValidBefore: uint64(now.Add(time.Hour).Unix()),\n\t\t}}, false},\n\t\t{\"ok renew after expiry\", args{ctx, &Controller{\n\t\t\tInterface: &JWK{},\n\t\t\tClaimer:   mustClaimer(t, &Claims{AllowRenewalAfterExpiry: &trueValue}, globalProvisionerClaims),\n\t\t}, &ssh.Certificate{\n\t\t\tValidAfter:  uint64(now.Add(-time.Hour).Unix()),\n\t\t\tValidBefore: uint64(now.Add(-time.Minute).Unix()),\n\t\t}}, false},\n\t\t{\"fail disabled\", args{ctx, &Controller{\n\t\t\tInterface: &JWK{},\n\t\t\tClaimer:   mustClaimer(t, &Claims{DisableRenewal: &trueValue}, globalProvisionerClaims),\n\t\t}, &ssh.Certificate{\n\t\t\tValidAfter:  uint64(now.Unix()),\n\t\t\tValidBefore: uint64(now.Add(time.Hour).Unix()),\n\t\t}}, true},\n\t\t{\"fail not yet valid\", args{ctx, &Controller{\n\t\t\tInterface: &JWK{},\n\t\t\tClaimer:   mustClaimer(t, &Claims{DisableRenewal: &trueValue}, globalProvisionerClaims),\n\t\t}, &ssh.Certificate{\n\t\t\tValidAfter:  uint64(now.Add(time.Hour).Unix()),\n\t\t\tValidBefore: uint64(now.Add(2 * time.Hour).Unix()),\n\t\t}}, true},\n\t\t{\"fail expired\", args{ctx, &Controller{\n\t\t\tInterface: &JWK{},\n\t\t\tClaimer:   mustClaimer(t, &Claims{DisableRenewal: &trueValue}, globalProvisionerClaims),\n\t\t}, &ssh.Certificate{\n\t\t\tValidAfter:  uint64(now.Add(-time.Hour).Unix()),\n\t\t\tValidBefore: uint64(now.Add(-time.Minute).Unix()),\n\t\t}}, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif err := DefaultAuthorizeSSHRenew(tt.args.ctx, tt.args.p, tt.args.cert); (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"DefaultAuthorizeSSHRenew() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_newWebhookController(t *testing.T) {\n\tcert, err := pemutil.ReadCertificate(\"testdata/certs/x5c-leaf.crt\", pemutil.WithFirstBlock())\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\topts := []webhook.RequestBodyOption{webhook.WithX5CCertificate(cert)}\n\n\ttype args struct {\n\t\ttemplateData WebhookSetter\n\t\tcertType     linkedca.Webhook_CertType\n\t\topts         []webhook.RequestBodyOption\n\t}\n\ttests := []struct {\n\t\tname string\n\t\targs args\n\t\twant *WebhookController\n\t}{\n\t\t{\"ok\", args{x509util.TemplateData{\"foo\": \"bar\"}, linkedca.Webhook_X509, nil}, &WebhookController{\n\t\t\tTemplateData: x509util.TemplateData{\"foo\": \"bar\"},\n\t\t\tcertType:     linkedca.Webhook_X509,\n\t\t\tclient:       http.DefaultClient,\n\t\t}},\n\t\t{\"ok with options\", args{x509util.TemplateData{\"foo\": \"bar\"}, linkedca.Webhook_SSH, opts}, &WebhookController{\n\t\t\tTemplateData: x509util.TemplateData{\"foo\": \"bar\"},\n\t\t\tcertType:     linkedca.Webhook_SSH,\n\t\t\tclient:       http.DefaultClient,\n\t\t\toptions:      opts,\n\t\t}},\n\t}\n\n\tfor _, tt := range tests {\n\t\tc := Controller{\n\t\t\twebhookClient: new(http.Client),\n\t\t\twrapTransport: httptransport.NoopWrapper(),\n\t\t}\n\t\tgot := c.newWebhookController(tt.args.templateData, tt.args.certType, tt.args.opts...)\n\n\t\tassert.Equal(t, tt.args.templateData, got.TemplateData)\n\t\tassert.Same(t, c.webhookClient, got.client)\n\t\tassert.Equal(t, c.webhooks, got.webhooks)\n\t\tassert.Equal(t, tt.args.opts, got.options)\n\t\tassert.Equal(t, tt.args.certType, got.certType)\n\t}\n}\n"
  },
  {
    "path": "authority/provisioner/duration.go",
    "content": "package provisioner\n\nimport (\n\t\"encoding/json\"\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n)\n\n// Duration is a wrapper around Time.Duration to aid with marshal/unmarshal.\ntype Duration struct {\n\ttime.Duration\n}\n\n// NewDuration parses a duration string and returns a Duration type or an error\n// if the given string is not a duration.\nfunc NewDuration(s string) (*Duration, error) {\n\td, err := time.ParseDuration(s)\n\tif err != nil {\n\t\treturn nil, errors.Wrapf(err, \"error parsing %s as duration\", s)\n\t}\n\treturn &Duration{Duration: d}, nil\n}\n\n// MarshalJSON parses a duration string and sets it to the duration.\n//\n// A duration string is a possibly signed sequence of decimal numbers, each with\n// optional fraction and a unit suffix, such as \"300ms\", \"-1.5h\" or \"2h45m\".\n// Valid time units are \"ns\", \"us\" (or \"µs\"), \"ms\", \"s\", \"m\", \"h\".\nfunc (d *Duration) MarshalJSON() ([]byte, error) {\n\treturn json.Marshal(d.Duration.String())\n}\n\n// UnmarshalJSON parses a duration string and sets it to the duration.\n//\n// A duration string is a possibly signed sequence of decimal numbers, each with\n// optional fraction and a unit suffix, such as \"300ms\", \"-1.5h\" or \"2h45m\".\n// Valid time units are \"ns\", \"us\" (or \"µs\"), \"ms\", \"s\", \"m\", \"h\".\nfunc (d *Duration) UnmarshalJSON(data []byte) (err error) {\n\tvar (\n\t\ts  string\n\t\tdd time.Duration\n\t)\n\tif d == nil {\n\t\treturn errors.New(\"duration cannot be nil\")\n\t}\n\tif err = json.Unmarshal(data, &s); err != nil {\n\t\treturn errors.Wrapf(err, \"error unmarshaling %s\", data)\n\t}\n\tif dd, err = time.ParseDuration(s); err != nil {\n\t\treturn errors.Wrapf(err, \"error parsing %s as duration\", s)\n\t}\n\td.Duration = dd\n\treturn\n}\n\n// Value returns 0 if the duration is null, the inner duration otherwise.\nfunc (d *Duration) Value() time.Duration {\n\tif d == nil {\n\t\treturn 0\n\t}\n\treturn d.Duration\n}\n"
  },
  {
    "path": "authority/provisioner/duration_test.go",
    "content": "package provisioner\n\nimport (\n\t\"reflect\"\n\t\"testing\"\n\t\"time\"\n)\n\nfunc TestNewDuration(t *testing.T) {\n\ttype args struct {\n\t\ts string\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\targs    args\n\t\twant    *Duration\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", args{\"1h2m3s\"}, &Duration{Duration: 3723 * time.Second}, false},\n\t\t{\"fail empty\", args{\"\"}, nil, true},\n\t\t{\"fail number\", args{\"123\"}, nil, true},\n\t\t{\"fail string\", args{\"1hour\"}, nil, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := NewDuration(tt.args.s)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"NewDuration() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"NewDuration() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDuration_UnmarshalJSON(t *testing.T) {\n\ttype args struct {\n\t\tdata []byte\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\td       *Duration\n\t\targs    args\n\t\twant    *Duration\n\t\twantErr bool\n\t}{\n\t\t{\"empty\", new(Duration), args{[]byte{}}, new(Duration), true},\n\t\t{\"bad type\", new(Duration), args{[]byte(`15`)}, new(Duration), true},\n\t\t{\"empty string\", new(Duration), args{[]byte(`\"\"`)}, new(Duration), true},\n\t\t{\"non duration\", new(Duration), args{[]byte(`\"15\"`)}, new(Duration), true},\n\t\t{\"duration\", new(Duration), args{[]byte(`\"15m30s\"`)}, &Duration{15*time.Minute + 30*time.Second}, false},\n\t\t{\"nil\", nil, args{nil}, nil, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif err := tt.d.UnmarshalJSON(tt.args.data); (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"Duration.UnmarshalJSON() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(tt.d, tt.want) {\n\t\t\t\tt.Errorf(\"Duration.UnmarshalJSON() = %v, want %v\", tt.d, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDuration_MarshalJSON(t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\td       *Duration\n\t\twant    []byte\n\t\twantErr bool\n\t}{\n\t\t{\"string\", &Duration{15*time.Minute + 30*time.Second}, []byte(`\"15m30s\"`), false},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := tt.d.MarshalJSON()\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"Duration.MarshalJSON() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"Duration.MarshalJSON() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDuration_Value(t *testing.T) {\n\tvar dur *Duration\n\ttests := []struct {\n\t\tname     string\n\t\tduration *Duration\n\t\twant     time.Duration\n\t}{\n\t\t{\"ok\", &Duration{Duration: 1 * time.Minute}, 1 * time.Minute},\n\t\t{\"ok new\", new(Duration), 0},\n\t\t{\"ok nil\", nil, 0},\n\t\t{\"ok nil var\", dur, 0},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif got := tt.duration.Value(); got != tt.want {\n\t\t\t\tt.Errorf(\"Duration.Value() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "authority/provisioner/extension.go",
    "content": "package provisioner\n\nimport (\n\t\"crypto/x509\"\n\t\"crypto/x509/pkix\"\n\t\"encoding/asn1\"\n)\n\nvar (\n\t// StepOIDRoot is the root OID for smallstep.\n\tStepOIDRoot = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 37476, 9000, 64}\n\n\t// StepOIDProvisioner is the OID for the provisioner extension.\n\tStepOIDProvisioner = append(asn1.ObjectIdentifier(nil), append(StepOIDRoot, 1)...)\n)\n\n// Extension is the Go representation of the provisioner extension.\ntype Extension struct {\n\tType          Type\n\tName          string\n\tCredentialID  string\n\tKeyValuePairs []string\n}\n\ntype extensionASN1 struct {\n\tType          int\n\tName          []byte\n\tCredentialID  []byte\n\tKeyValuePairs []string `asn1:\"optional,omitempty\"`\n}\n\n// Marshal marshals the extension using encoding/asn1.\nfunc (e *Extension) Marshal() ([]byte, error) {\n\treturn asn1.Marshal(extensionASN1{\n\t\tType:          int(e.Type),\n\t\tName:          []byte(e.Name),\n\t\tCredentialID:  []byte(e.CredentialID),\n\t\tKeyValuePairs: e.KeyValuePairs,\n\t})\n}\n\n// ToExtension returns the pkix.Extension representation of the provisioner\n// extension.\nfunc (e *Extension) ToExtension() (pkix.Extension, error) {\n\tb, err := e.Marshal()\n\tif err != nil {\n\t\treturn pkix.Extension{}, err\n\t}\n\treturn pkix.Extension{\n\t\tId:    StepOIDProvisioner,\n\t\tValue: b,\n\t}, nil\n}\n\n// GetProvisionerExtension goes through all the certificate extensions and\n// returns the provisioner extension (1.3.6.1.4.1.37476.9000.64.1).\nfunc GetProvisionerExtension(cert *x509.Certificate) (*Extension, bool) {\n\tfor _, e := range cert.Extensions {\n\t\tif e.Id.Equal(StepOIDProvisioner) {\n\t\t\tvar provisioner extensionASN1\n\t\t\tif _, err := asn1.Unmarshal(e.Value, &provisioner); err != nil {\n\t\t\t\treturn nil, false\n\t\t\t}\n\t\t\treturn &Extension{\n\t\t\t\tType:          Type(provisioner.Type),\n\t\t\t\tName:          string(provisioner.Name),\n\t\t\t\tCredentialID:  string(provisioner.CredentialID),\n\t\t\t\tKeyValuePairs: provisioner.KeyValuePairs,\n\t\t\t}, true\n\t\t}\n\t}\n\treturn nil, false\n}\n"
  },
  {
    "path": "authority/provisioner/extension_test.go",
    "content": "package provisioner\n\nimport (\n\t\"crypto/x509\"\n\t\"crypto/x509/pkix\"\n\t\"reflect\"\n\t\"testing\"\n\n\t\"go.step.sm/crypto/pemutil\"\n)\n\nfunc TestExtension_Marshal(t *testing.T) {\n\ttype fields struct {\n\t\tType          Type\n\t\tName          string\n\t\tCredentialID  string\n\t\tKeyValuePairs []string\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tfields  fields\n\t\twant    []byte\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", fields{TypeJWK, \"name\", \"credentialID\", nil}, []byte{\n\t\t\t0x30, 0x17, 0x02, 0x01, 0x01, 0x04, 0x04, 0x6e,\n\t\t\t0x61, 0x6d, 0x65, 0x04, 0x0c, 0x63, 0x72, 0x65,\n\t\t\t0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x49,\n\t\t\t0x44,\n\t\t}, false},\n\t\t{\"ok with pairs\", fields{TypeJWK, \"name\", \"credentialID\", []string{\"foo\", \"bar\"}}, []byte{\n\t\t\t0x30, 0x23, 0x02, 0x01, 0x01, 0x04, 0x04, 0x6e,\n\t\t\t0x61, 0x6d, 0x65, 0x04, 0x0c, 0x63, 0x72, 0x65,\n\t\t\t0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x49,\n\t\t\t0x44, 0x30, 0x0a, 0x13, 0x03, 0x66, 0x6f, 0x6f,\n\t\t\t0x13, 0x03, 0x62, 0x61, 0x72,\n\t\t}, false},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\te := &Extension{\n\t\t\t\tType:          tt.fields.Type,\n\t\t\t\tName:          tt.fields.Name,\n\t\t\t\tCredentialID:  tt.fields.CredentialID,\n\t\t\t\tKeyValuePairs: tt.fields.KeyValuePairs,\n\t\t\t}\n\t\t\tgot, err := e.Marshal()\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"Extension.Marshal() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"Extension.Marshal() = %x, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestExtension_ToExtension(t *testing.T) {\n\ttype fields struct {\n\t\tType          Type\n\t\tName          string\n\t\tCredentialID  string\n\t\tKeyValuePairs []string\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tfields  fields\n\t\twant    pkix.Extension\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", fields{TypeJWK, \"name\", \"credentialID\", nil}, pkix.Extension{\n\t\t\tId: StepOIDProvisioner,\n\t\t\tValue: []byte{\n\t\t\t\t0x30, 0x17, 0x02, 0x01, 0x01, 0x04, 0x04, 0x6e,\n\t\t\t\t0x61, 0x6d, 0x65, 0x04, 0x0c, 0x63, 0x72, 0x65,\n\t\t\t\t0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x49,\n\t\t\t\t0x44,\n\t\t\t},\n\t\t}, false},\n\t\t{\"ok empty pairs\", fields{TypeJWK, \"name\", \"credentialID\", []string{}}, pkix.Extension{\n\t\t\tId: StepOIDProvisioner,\n\t\t\tValue: []byte{\n\t\t\t\t0x30, 0x17, 0x02, 0x01, 0x01, 0x04, 0x04, 0x6e,\n\t\t\t\t0x61, 0x6d, 0x65, 0x04, 0x0c, 0x63, 0x72, 0x65,\n\t\t\t\t0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x49,\n\t\t\t\t0x44,\n\t\t\t},\n\t\t}, false},\n\t\t{\"ok with pairs\", fields{TypeJWK, \"name\", \"credentialID\", []string{\"foo\", \"bar\"}}, pkix.Extension{\n\t\t\tId: StepOIDProvisioner,\n\t\t\tValue: []byte{\n\t\t\t\t0x30, 0x23, 0x02, 0x01, 0x01, 0x04, 0x04, 0x6e,\n\t\t\t\t0x61, 0x6d, 0x65, 0x04, 0x0c, 0x63, 0x72, 0x65,\n\t\t\t\t0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x49,\n\t\t\t\t0x44, 0x30, 0x0a, 0x13, 0x03, 0x66, 0x6f, 0x6f,\n\t\t\t\t0x13, 0x03, 0x62, 0x61, 0x72,\n\t\t\t},\n\t\t}, false},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\te := &Extension{\n\t\t\t\tType:          tt.fields.Type,\n\t\t\t\tName:          tt.fields.Name,\n\t\t\t\tCredentialID:  tt.fields.CredentialID,\n\t\t\t\tKeyValuePairs: tt.fields.KeyValuePairs,\n\t\t\t}\n\t\t\tgot, err := e.ToExtension()\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"Extension.ToExtension() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"Extension.ToExtension() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestGetProvisionerExtension(t *testing.T) {\n\tmustCertificate := func(fn string) *x509.Certificate {\n\t\tcert, err := pemutil.ReadCertificate(fn)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\treturn cert\n\t}\n\n\ttype args struct {\n\t\tcert *x509.Certificate\n\t}\n\ttests := []struct {\n\t\tname  string\n\t\targs  args\n\t\twant  *Extension\n\t\twant1 bool\n\t}{\n\t\t{\"ok\", args{mustCertificate(\"testdata/certs/good-extension.crt\")}, &Extension{\n\t\t\tType:         TypeJWK,\n\t\t\tName:         \"mariano@smallstep.com\",\n\t\t\tCredentialID: \"nvgnR8wSzpUlrt_tC3mvrhwhBx9Y7T1WL_JjcFVWYBQ\",\n\t\t}, true},\n\t\t{\"fail unmarshal\", args{mustCertificate(\"testdata/certs/bad-extension.crt\")}, nil, false},\n\t\t{\"missing extension\", args{mustCertificate(\"testdata/certs/aws.crt\")}, nil, false},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, got1 := GetProvisionerExtension(tt.args.cert)\n\t\t\tif !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"GetProvisionerExtension() got = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t\tif got1 != tt.want1 {\n\t\t\t\tt.Errorf(\"GetProvisionerExtension() got1 = %v, want %v\", got1, tt.want1)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "authority/provisioner/gcp/projectvalidator.go",
    "content": "package gcp\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\n\t\"google.golang.org/api/cloudresourcemanager/v1\"\n\n\t\"github.com/smallstep/certificates/errs\"\n)\n\ntype ProjectValidator struct {\n\tProjectIDs []string\n}\n\nfunc (p *ProjectValidator) ValidateProject(_ context.Context, projectID string) error {\n\tif len(p.ProjectIDs) == 0 {\n\t\treturn nil\n\t}\n\n\tfor _, pi := range p.ProjectIDs {\n\t\tif pi == projectID {\n\t\t\treturn nil\n\t\t}\n\t}\n\n\treturn errs.Unauthorized(\"gcp.authorizeToken; invalid gcp token - invalid project id\")\n}\n\ntype OrganizationValidator struct {\n\t*ProjectValidator\n\tOrganizationID  string\n\tprojectsService *cloudresourcemanager.ProjectsService\n}\n\nfunc NewOrganizationValidator(projectIDs []string, organizationID string) (*OrganizationValidator, error) {\n\tvar svc *cloudresourcemanager.ProjectsService\n\n\tif organizationID != \"\" {\n\t\tcrm, err := cloudresourcemanager.NewService(context.Background())\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tsvc = crm.Projects\n\t}\n\n\treturn &OrganizationValidator{\n\t\tProjectValidator: &ProjectValidator{projectIDs},\n\t\tOrganizationID:   organizationID,\n\t\tprojectsService:  svc,\n\t}, nil\n}\n\nfunc (p *OrganizationValidator) ValidateProject(ctx context.Context, projectID string) error {\n\tif err := p.ProjectValidator.ValidateProject(ctx, projectID); err != nil {\n\t\treturn err\n\t}\n\n\tif p.OrganizationID == \"\" {\n\t\treturn nil\n\t}\n\n\tancestry, err := p.projectsService.\n\t\tGetAncestry(projectID, &cloudresourcemanager.GetAncestryRequest{}).\n\t\tContext(ctx).\n\t\tDo()\n\n\tif err != nil {\n\t\treturn errs.Wrap(http.StatusInternalServerError, err, \"gcp.authorizeToken\")\n\t}\n\n\tif len(ancestry.Ancestor) < 1 {\n\t\treturn errs.InternalServer(\"gcp.authorizeToken; getAncestry response malformed\")\n\t}\n\n\tprogenitor := ancestry.Ancestor[len(ancestry.Ancestor)-1]\n\n\tif progenitor.ResourceId.Type != \"organization\" || progenitor.ResourceId.Id != p.OrganizationID {\n\t\treturn errs.Unauthorized(\"gcp.authorizeToken; invalid gcp token - project does not belong to organization\")\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "authority/provisioner/gcp/projectvalidator_test.go",
    "content": "package gcp\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"google.golang.org/api/cloudresourcemanager/v1\"\n)\n\nfunc TestProjectValidator_ValidateProject(t *testing.T) {\n\tctx := context.Background()\n\ttype fields struct {\n\t\tProjectIDs []string\n\t}\n\ttype args struct {\n\t\tin0       context.Context\n\t\tprojectID string\n\t}\n\ttests := []struct {\n\t\tname      string\n\t\tfields    fields\n\t\targs      args\n\t\tassertion assert.ErrorAssertionFunc\n\t}{\n\t\t{\"allowed-1\", fields{[]string{\"allowed-1\", \"allowed-2\"}}, args{ctx, \"allowed-1\"}, assert.NoError},\n\t\t{\"allowed-2\", fields{[]string{\"allowed-1\", \"allowed-2\"}}, args{ctx, \"allowed-2\"}, assert.NoError},\n\t\t{\"empty\", fields{nil}, args{ctx, \"allowed-1\"}, assert.NoError},\n\t\t{\"not allowed\", fields{[]string{\"allowed-1\", \"allowed-2\"}}, args{ctx, \"not-allowed\"}, assert.Error},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tp := &ProjectValidator{\n\t\t\t\tProjectIDs: tt.fields.ProjectIDs,\n\t\t\t}\n\t\t\ttt.assertion(t, p.ValidateProject(tt.args.in0, tt.args.projectID))\n\t\t})\n\t}\n}\n\nfunc TestNewOrganizationValidator(t *testing.T) {\n\tctx := context.Background()\n\t_, err := cloudresourcemanager.NewService(ctx)\n\tskip := (err != nil)\n\n\ttype args struct {\n\t\tprojectIDs     []string\n\t\torganizationID string\n\t}\n\ttests := []struct {\n\t\tname      string\n\t\tskip      bool\n\t\targs      args\n\t\twant      *OrganizationValidator\n\t\tassertion assert.ErrorAssertionFunc\n\t}{\n\t\t{\"ok projects\", false, args{[]string{\"project-1\", \"project-2\"}, \"\"}, &OrganizationValidator{\n\t\t\tProjectValidator: &ProjectValidator{[]string{\"project-1\", \"project-2\"}},\n\t\t}, assert.NoError},\n\t\t{\"ok organization\", skip, args{[]string{}, \"organization\"}, &OrganizationValidator{\n\t\t\tProjectValidator: &ProjectValidator{[]string{}},\n\t\t\tOrganizationID:   \"organization\",\n\t\t\tprojectsService:  &cloudresourcemanager.ProjectsService{},\n\t\t}, assert.NoError},\n\t\t{\"ok projects organization\", skip, args{[]string{\"project-1\"}, \"organization\"}, &OrganizationValidator{\n\t\t\tProjectValidator: &ProjectValidator{[]string{\"project-1\"}},\n\t\t\tOrganizationID:   \"organization\",\n\t\t\tprojectsService:  &cloudresourcemanager.ProjectsService{},\n\t\t}, assert.NoError},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif tt.skip {\n\t\t\t\tt.SkipNow()\n\t\t\t\treturn\n\t\t\t}\n\t\t\tgot, err := NewOrganizationValidator(tt.args.projectIDs, tt.args.organizationID)\n\t\t\ttt.assertion(t, err)\n\t\t\tassert.EqualExportedValues(t, tt.want, got)\n\t\t})\n\t}\n}\n\nfunc TestOrganizationValidator_ValidateProject(t *testing.T) {\n\tctx := context.Background()\n\tsvc, err := cloudresourcemanager.NewService(ctx)\n\tskip := (err != nil)\n\n\tvar projectsService *cloudresourcemanager.ProjectsService\n\tif !skip {\n\t\tprojectsService = svc.Projects\n\t}\n\n\ttype fields struct {\n\t\tProjectValidator *ProjectValidator\n\t\tOrganizationID   string\n\t\tprojectsService  *cloudresourcemanager.ProjectsService\n\t}\n\ttype args struct {\n\t\tctx       context.Context\n\t\tprojectID string\n\t}\n\ttests := []struct {\n\t\tname      string\n\t\tskip      bool\n\t\tfields    fields\n\t\targs      args\n\t\tassertion assert.ErrorAssertionFunc\n\t}{\n\t\t{\"ok projects\", false, fields{&ProjectValidator{ProjectIDs: []string{\"allowed\"}}, \"\", projectsService}, args{ctx, \"allowed\"}, assert.NoError},\n\t\t{\"fail projects\", false, fields{&ProjectValidator{ProjectIDs: []string{\"allowed\"}}, \"organization\", projectsService}, args{ctx, \"not-allowed\"}, assert.Error},\n\t\t{\"fail organization\", skip, fields{&ProjectValidator{ProjectIDs: []string{\"allowed\"}}, \"fake-organization\", projectsService}, args{ctx, \"allowed\"}, assert.Error},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tp := &OrganizationValidator{\n\t\t\t\tProjectValidator: tt.fields.ProjectValidator,\n\t\t\t\tOrganizationID:   tt.fields.OrganizationID,\n\t\t\t\tprojectsService:  tt.fields.projectsService,\n\t\t\t}\n\t\t\tif tt.skip {\n\t\t\t\tt.SkipNow()\n\t\t\t\treturn\n\t\t\t}\n\t\t\ttt.assertion(t, p.ValidateProject(tt.args.ctx, tt.args.projectID))\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "authority/provisioner/gcp.go",
    "content": "package provisioner\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/sha256\"\n\t\"crypto/x509\"\n\t\"encoding/hex\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n\n\t\"github.com/smallstep/linkedca\"\n\t\"go.step.sm/crypto/jose\"\n\t\"go.step.sm/crypto/sshutil\"\n\t\"go.step.sm/crypto/x509util\"\n\n\t\"github.com/smallstep/certificates/authority/provisioner/gcp\"\n\t\"github.com/smallstep/certificates/errs\"\n\t\"github.com/smallstep/certificates/webhook\"\n)\n\n// gcpCertsURL is the url that serves Google OAuth2 public keys.\nconst gcpCertsURL = \"https://www.googleapis.com/oauth2/v3/certs\"\n\n// gcpIdentityURL is the base url for the identity document in GCP.\nconst gcpIdentityURL = \"http://metadata/computeMetadata/v1/instance/service-accounts/default/identity\"\n\n// DefaultDisableSSHCAHost is the default value for SSH Host CA used when DisableSSHCAHost is not set\nvar DefaultDisableSSHCAHost = false\n\n// DefaultDisableSSHCAUser is the default value for SSH User CA used when DisableSSHCAUser is not set\nvar DefaultDisableSSHCAUser = true\n\n// gcpPayload extends jwt.Claims with custom GCP attributes.\ntype gcpPayload struct {\n\tjose.Claims\n\tAuthorizedParty string           `json:\"azp\"`\n\tEmail           string           `json:\"email\"`\n\tEmailVerified   bool             `json:\"email_verified\"`\n\tGoogle          gcpGooglePayload `json:\"google\"`\n}\n\ntype gcpGooglePayload struct {\n\tComputeEngine gcpComputeEnginePayload `json:\"compute_engine\"`\n}\n\ntype gcpComputeEnginePayload struct {\n\tInstanceID                string            `json:\"instance_id\"`\n\tInstanceName              string            `json:\"instance_name\"`\n\tInstanceCreationTimestamp *jose.NumericDate `json:\"instance_creation_timestamp\"`\n\tProjectID                 string            `json:\"project_id\"`\n\tProjectNumber             int64             `json:\"project_number\"`\n\tZone                      string            `json:\"zone\"`\n\tLicenseID                 []string          `json:\"license_id\"`\n}\n\ntype gcpConfig struct {\n\tCertsURL    string\n\tIdentityURL string\n}\n\nfunc newGCPConfig() *gcpConfig {\n\treturn &gcpConfig{\n\t\tCertsURL:    gcpCertsURL,\n\t\tIdentityURL: gcpIdentityURL,\n\t}\n}\n\n// projectValidator is an interface to enable testing without using\n// gcp.OrganizationProjectValidator.\ntype projectValidator interface {\n\tValidateProject(ctx context.Context, projectID string) error\n}\n\n// GCP is the provisioner that supports identity tokens created by the Google\n// Cloud Platform metadata API.\n//\n// If DisableCustomSANs is true, only the internal DNS and IP will be added as a\n// SAN. By default it will accept any SAN in the CSR.\n//\n// If DisableTrustOnFirstUse is true, multiple sign request for this provisioner\n// with the same instance will be accepted. By default only the first request\n// will be accepted.\n//\n// If InstanceAge is set, only the instances with an instance_creation_timestamp\n// within the given period will be accepted.\n//\n// Google Identity docs are available at\n// https://cloud.google.com/compute/docs/instances/verifying-instance-identity\ntype GCP struct {\n\t*base\n\tID                     string   `json:\"-\"`\n\tType                   string   `json:\"type\"`\n\tName                   string   `json:\"name\"`\n\tServiceAccounts        []string `json:\"serviceAccounts,omitempty\"`\n\tProjectIDs             []string `json:\"projectIDs,omitempty\"`\n\tOrganizationID         string   `json:\"organizationID,omitempty\"`\n\tDisableCustomSANs      bool     `json:\"disableCustomSANs\"`\n\tDisableTrustOnFirstUse bool     `json:\"disableTrustOnFirstUse\"`\n\tDisableSSHCAUser       *bool    `json:\"disableSSHCAUser,omitempty\"`\n\tDisableSSHCAHost       *bool    `json:\"disableSSHCAHost,omitempty\"`\n\tInstanceAge            Duration `json:\"instanceAge,omitempty\"`\n\tClaims                 *Claims  `json:\"claims,omitempty\"`\n\tOptions                *Options `json:\"options,omitempty\"`\n\tconfig                 *gcpConfig\n\tkeyStore               *keyStore\n\tctl                    *Controller\n\tprojectValidator       projectValidator\n}\n\n// GetID returns the provisioner unique identifier. The name should uniquely\n// identify any GCP provisioner.\nfunc (p *GCP) GetID() string {\n\tif p.ID != \"\" {\n\t\treturn p.ID\n\t}\n\treturn p.GetIDForToken()\n}\n\n// GetIDForToken returns an identifier that will be used to load the provisioner\n// from a token.\nfunc (p *GCP) GetIDForToken() string {\n\treturn \"gcp/\" + p.Name\n}\n\n// GetTokenID returns the identifier of the token. The default value for GCP the\n// SHA256 of \"provisioner_id.instance_id\", but if DisableTrustOnFirstUse is set\n// to true, then it will be the SHA256 of the token.\nfunc (p *GCP) GetTokenID(token string) (string, error) {\n\tjwt, err := jose.ParseSigned(token)\n\tif err != nil {\n\t\treturn \"\", errors.Wrap(err, \"error parsing token\")\n\t}\n\n\t// If TOFU is disabled create an ID for the token, so it cannot be reused.\n\tif p.DisableTrustOnFirstUse {\n\t\tsum := sha256.Sum256([]byte(token))\n\t\treturn strings.ToLower(hex.EncodeToString(sum[:])), nil\n\t}\n\n\t// Get claims w/out verification.\n\tvar claims gcpPayload\n\tif err = jwt.UnsafeClaimsWithoutVerification(&claims); err != nil {\n\t\treturn \"\", errors.Wrap(err, \"error verifying claims\")\n\t}\n\n\t// Create unique ID for Trust On First Use (TOFU). Only the first instance\n\t// per provisioner is allowed as we don't have a way to trust the given\n\t// sans.\n\tunique := fmt.Sprintf(\"%s.%s\", p.GetIDForToken(), claims.Google.ComputeEngine.InstanceID)\n\tsum := sha256.Sum256([]byte(unique))\n\treturn strings.ToLower(hex.EncodeToString(sum[:])), nil\n}\n\n// GetName returns the name of the provisioner.\nfunc (p *GCP) GetName() string {\n\treturn p.Name\n}\n\n// GetType returns the type of provisioner.\nfunc (p *GCP) GetType() Type {\n\treturn TypeGCP\n}\n\n// GetEncryptedKey is not available in a GCP provisioner.\nfunc (p *GCP) GetEncryptedKey() (kid, key string, ok bool) {\n\treturn \"\", \"\", false\n}\n\n// GetIdentityURL returns the url that generates the GCP token.\nfunc (p *GCP) GetIdentityURL(audience string) string {\n\t// Initialize config if required\n\tp.assertConfig()\n\n\tq := url.Values{}\n\tq.Add(\"audience\", audience)\n\tq.Add(\"format\", \"full\")\n\tq.Add(\"licenses\", \"FALSE\")\n\treturn fmt.Sprintf(\"%s?%s\", p.config.IdentityURL, q.Encode())\n}\n\n// GetIdentityToken does an HTTP request to the identity url.\nfunc (p *GCP) GetIdentityToken(subject, caURL string) (string, error) {\n\t_ = subject // unused input\n\n\taudience, err := generateSignAudience(caURL, p.GetIDForToken())\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treq, err := http.NewRequest(\"GET\", p.GetIdentityURL(audience), http.NoBody)\n\tif err != nil {\n\t\treturn \"\", errors.Wrap(err, \"error creating identity request\")\n\t}\n\treq.Header.Set(\"Metadata-Flavor\", \"Google\")\n\tresp, err := http.DefaultClient.Do(req)\n\tif err != nil {\n\t\treturn \"\", errors.Wrap(err, \"error doing identity request, are you in a GCP VM?\")\n\t}\n\tdefer resp.Body.Close()\n\tb, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn \"\", errors.Wrap(err, \"error on identity request\")\n\t}\n\tif resp.StatusCode >= 400 {\n\t\treturn \"\", errors.Errorf(\"error on identity request: status=%d, response=%s\", resp.StatusCode, b)\n\t}\n\treturn string(bytes.TrimSpace(b)), nil\n}\n\n// Init validates and initializes the GCP provisioner.\nfunc (p *GCP) Init(config Config) (err error) {\n\tif p.DisableSSHCAHost == nil {\n\t\tp.DisableSSHCAHost = &DefaultDisableSSHCAHost\n\t}\n\n\tif p.DisableSSHCAUser == nil {\n\t\tp.DisableSSHCAUser = &DefaultDisableSSHCAUser\n\t}\n\n\tswitch {\n\tcase p.Type == \"\":\n\t\treturn errors.New(\"provisioner type cannot be empty\")\n\tcase p.Name == \"\":\n\t\treturn errors.New(\"provisioner name cannot be empty\")\n\tcase p.InstanceAge.Value() < 0:\n\t\treturn errors.New(\"provisioner instanceAge cannot be negative\")\n\t}\n\n\tif len(p.ProjectIDs) > 0 && p.OrganizationID != \"\" {\n\t\treturn errors.New(\"provisioner cannot have both `projectIDs` and `organizationID` set\")\n\t}\n\n\t// Initialize config\n\tp.assertConfig()\n\n\t// Initialize key store\n\tif p.keyStore, err = newKeyStore(http.DefaultClient, p.config.CertsURL); err != nil {\n\t\treturn\n\t}\n\n\t// Initialize the project validator\n\tif p.projectValidator, err = gcp.NewOrganizationValidator(p.ProjectIDs, p.OrganizationID); err != nil {\n\t\treturn\n\t}\n\n\tconfig.Audiences = config.Audiences.WithFragment(p.GetIDForToken())\n\n\tif p.ctl, err = NewController(p, p.Claims, config, p.Options); err != nil {\n\t\treturn\n\t}\n\n\treturn\n}\n\n// AuthorizeSign validates the given token and returns the sign options that\n// will be used on certificate creation.\nfunc (p *GCP) AuthorizeSign(ctx context.Context, token string) ([]SignOption, error) {\n\tclaims, err := p.authorizeToken(ctx, token)\n\tif err != nil {\n\t\treturn nil, errs.Wrap(http.StatusInternalServerError, err, \"gcp.AuthorizeSign\")\n\t}\n\n\tce := claims.Google.ComputeEngine\n\n\t// Template options\n\tdata := x509util.NewTemplateData()\n\tdata.SetCommonName(ce.InstanceName)\n\tif v, err := unsafeParseSigned(token); err == nil {\n\t\tdata.SetToken(v)\n\t}\n\n\t// Enforce known common name and default DNS if configured.\n\t// By default we we'll accept the CN and SANs in the CSR.\n\t// There's no way to trust them other than TOFU.\n\tvar so []SignOption\n\tif p.DisableCustomSANs {\n\t\tdnsName1 := fmt.Sprintf(\"%s.c.%s.internal\", ce.InstanceName, ce.ProjectID)\n\t\tdnsName2 := fmt.Sprintf(\"%s.%s.c.%s.internal\", ce.InstanceName, ce.Zone, ce.ProjectID)\n\t\tso = append(so,\n\t\t\tcommonNameSliceValidator([]string{\n\t\t\t\tce.InstanceName, ce.InstanceID, dnsName1, dnsName2,\n\t\t\t}),\n\t\t\tdnsNamesSubsetValidator([]string{\n\t\t\t\tdnsName1, dnsName2,\n\t\t\t}),\n\t\t\tipAddressesValidator(nil),\n\t\t\temailAddressesValidator(nil),\n\t\t\tnewURIsValidator(ctx, nil),\n\t\t)\n\n\t\t// Template SANs\n\t\tdata.SetSANs([]string{dnsName1, dnsName2})\n\t}\n\n\ttemplateOptions, err := CustomTemplateOptions(p.Options, data, x509util.DefaultIIDLeafTemplate)\n\tif err != nil {\n\t\treturn nil, errs.Wrap(http.StatusInternalServerError, err, \"gcp.AuthorizeSign\")\n\t}\n\n\treturn append(so,\n\t\tp,\n\t\ttemplateOptions,\n\t\t// modifiers / withOptions\n\t\tnewProvisionerExtensionOption(TypeGCP, p.Name, claims.Subject, \"InstanceID\", ce.InstanceID, \"InstanceName\", ce.InstanceName).WithControllerOptions(p.ctl),\n\t\tprofileDefaultDuration(p.ctl.Claimer.DefaultTLSCertDuration()),\n\t\t// validators\n\t\tdefaultPublicKeyValidator{},\n\t\tnewValidityValidator(p.ctl.Claimer.MinTLSCertDuration(), p.ctl.Claimer.MaxTLSCertDuration()),\n\t\tnewX509NamePolicyValidator(p.ctl.getPolicy().getX509()),\n\t\tp.ctl.newWebhookController(\n\t\t\tdata,\n\t\t\tlinkedca.Webhook_X509,\n\t\t\twebhook.WithAuthorizationPrincipal(ce.InstanceID),\n\t\t),\n\t), nil\n}\n\n// AuthorizeRenew returns an error if the renewal is disabled.\nfunc (p *GCP) AuthorizeRenew(ctx context.Context, cert *x509.Certificate) error {\n\treturn p.ctl.AuthorizeRenew(ctx, cert)\n}\n\n// assertConfig initializes the config if it has not been initialized.\nfunc (p *GCP) assertConfig() {\n\tif p.config == nil {\n\t\tp.config = newGCPConfig()\n\t}\n}\n\n// authorizeToken performs common jwt authorization actions and returns the\n// claims for case specific downstream parsing.\n// e.g. a Sign request will auth/validate different fields than a Revoke request.\nfunc (p *GCP) authorizeToken(ctx context.Context, token string) (*gcpPayload, error) {\n\tjwt, err := jose.ParseSigned(token)\n\tif err != nil {\n\t\treturn nil, errs.Wrap(http.StatusUnauthorized, err, \"gcp.authorizeToken; error parsing gcp token\")\n\t}\n\tif len(jwt.Headers) == 0 {\n\t\treturn nil, errs.Unauthorized(\"gcp.authorizeToken; error parsing gcp token - header is missing\")\n\t}\n\n\tvar found bool\n\tvar claims gcpPayload\n\tkid := jwt.Headers[0].KeyID\n\tkeys := p.keyStore.Get(kid)\n\tfor _, key := range keys {\n\t\tif err := jwt.Claims(key.Public(), &claims); err == nil {\n\t\t\tfound = true\n\t\t\tbreak\n\t\t}\n\t}\n\tif !found {\n\t\treturn nil, errs.Unauthorized(\"gcp.authorizeToken; failed to validate gcp token payload - cannot find key for kid %s\", kid)\n\t}\n\n\t// According to \"rfc7519 JSON Web Token\" acceptable skew should be no\n\t// more than a few minutes.\n\tnow := time.Now().UTC()\n\tif err = claims.ValidateWithLeeway(jose.Expected{\n\t\tIssuer: \"https://accounts.google.com\",\n\t\tTime:   now,\n\t}, time.Minute); err != nil {\n\t\treturn nil, errs.Wrap(http.StatusUnauthorized, err, \"gcp.authorizeToken; invalid gcp token payload\")\n\t}\n\n\t// validate audiences with the defaults\n\tif !matchesAudience(claims.Audience, p.ctl.Audiences.Sign) {\n\t\treturn nil, errs.Unauthorized(\"gcp.authorizeToken; invalid gcp token - invalid audience claim (aud)\")\n\t}\n\n\t// validate subject (service account)\n\tif len(p.ServiceAccounts) > 0 {\n\t\tvar found bool\n\t\tfor _, sa := range p.ServiceAccounts {\n\t\t\tif sa == claims.Subject || sa == claims.Email {\n\t\t\t\tfound = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif !found {\n\t\t\treturn nil, errs.Unauthorized(\"gcp.authorizeToken; invalid gcp token - invalid subject claim\")\n\t\t}\n\t}\n\n\t// validate projects\n\tif err := p.projectValidator.ValidateProject(ctx, claims.Google.ComputeEngine.ProjectID); err != nil {\n\t\treturn nil, err\n\t}\n\n\t// validate instance age\n\tif d := p.InstanceAge.Value(); d > 0 {\n\t\tif now.Sub(claims.Google.ComputeEngine.InstanceCreationTimestamp.Time()) > d {\n\t\t\treturn nil, errs.Unauthorized(\"gcp.authorizeToken; token google.compute_engine.instance_creation_timestamp is too old\")\n\t\t}\n\t}\n\n\tswitch {\n\tcase claims.Google.ComputeEngine.InstanceID == \"\":\n\t\treturn nil, errs.Unauthorized(\"gcp.authorizeToken; gcp token google.compute_engine.instance_id cannot be empty\")\n\tcase claims.Google.ComputeEngine.InstanceName == \"\":\n\t\treturn nil, errs.Unauthorized(\"gcp.authorizeToken; gcp token google.compute_engine.instance_name cannot be empty\")\n\tcase claims.Google.ComputeEngine.ProjectID == \"\":\n\t\treturn nil, errs.Unauthorized(\"gcp.authorizeToken; gcp token google.compute_engine.project_id cannot be empty\")\n\tcase claims.Google.ComputeEngine.Zone == \"\":\n\t\treturn nil, errs.Unauthorized(\"gcp.authorizeToken; gcp token google.compute_engine.zone cannot be empty\")\n\t}\n\n\treturn &claims, nil\n}\n\n// AuthorizeSSHSign returns the list of SignOption for a SignSSH request.\nfunc (p *GCP) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, error) {\n\tcertType, hasCertType := CertTypeFromContext(ctx)\n\tif !hasCertType {\n\t\tcertType = SSHHostCert\n\t}\n\n\terr := p.isUnauthorizedToIssueSSHCert(certType)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tclaims, err := p.authorizeToken(ctx, token)\n\tif err != nil {\n\t\treturn nil, errs.Wrap(http.StatusInternalServerError, err, \"gcp.AuthorizeSSHSign\")\n\t}\n\n\tvar principals []string\n\tvar keyID string\n\tvar defaults SignSSHOptions\n\tvar ct sshutil.CertType\n\tvar template string\n\n\tswitch certType {\n\tcase SSHHostCert:\n\t\tdefaults, keyID, principals, ct, template = p.genHostOptions(ctx, claims)\n\tcase SSHUserCert:\n\t\tdefaults, keyID, principals, ct, template = p.genUserOptions(ctx, claims)\n\tdefault:\n\t\treturn nil, errs.Unauthorized(\"gcp.AuthorizeSSHSign; invalid requested certType\")\n\t}\n\n\tsignOptions := []SignOption{}\n\n\t// Only enforce known principals if disable custom sans is true, or it is a user cert request\n\tif p.DisableCustomSANs || certType == SSHUserCert {\n\t\tdefaults.Principals = principals\n\t} else {\n\t\t// Check that at least one principal is sent in the request.\n\t\tsignOptions = append(signOptions, &sshCertOptionsRequireValidator{\n\t\t\tPrincipals: true,\n\t\t})\n\t}\n\n\t// Certificate templates.\n\tdata := sshutil.CreateTemplateData(ct, keyID, principals)\n\tif v, err := unsafeParseSigned(token); err == nil {\n\t\tdata.SetToken(v)\n\t}\n\n\ttemplateOptions, err := CustomSSHTemplateOptions(p.Options, data, template)\n\tif err != nil {\n\t\treturn nil, errs.Wrap(http.StatusInternalServerError, err, \"gcp.AuthorizeSSHSign\")\n\t}\n\tsignOptions = append(signOptions, templateOptions)\n\n\treturn append(signOptions,\n\t\tp,\n\t\t// Validate user SignSSHOptions.\n\t\tsshCertOptionsValidator(defaults),\n\t\t// Set the validity bounds if not set.\n\t\t&sshDefaultDuration{p.ctl.Claimer},\n\t\t// Validate public key\n\t\t&sshDefaultPublicKeyValidator{},\n\t\t// Validate the validity period.\n\t\t&sshCertValidityValidator{p.ctl.Claimer},\n\t\t// Require all the fields in the SSH certificate\n\t\t&sshCertDefaultValidator{},\n\t\t// Ensure that all principal names are allowed\n\t\tnewSSHNamePolicyValidator(p.ctl.getPolicy().getSSHHost(), p.ctl.getPolicy().getSSHUser()),\n\t\t// Call webhooks\n\t\tp.ctl.newWebhookController(\n\t\t\tdata,\n\t\t\tlinkedca.Webhook_SSH,\n\t\t\twebhook.WithAuthorizationPrincipal(keyID),\n\t\t),\n\t), nil\n}\n\nfunc (p *GCP) genHostOptions(_ context.Context, claims *gcpPayload) (SignSSHOptions, string, []string, sshutil.CertType, string) {\n\tce := claims.Google.ComputeEngine\n\tkeyID := ce.InstanceName\n\n\tprincipals := []string{\n\t\tfmt.Sprintf(\"%s.c.%s.internal\", ce.InstanceName, ce.ProjectID),\n\t\tfmt.Sprintf(\"%s.%s.c.%s.internal\", ce.InstanceName, ce.Zone, ce.ProjectID),\n\t}\n\n\treturn SignSSHOptions{CertType: SSHHostCert}, keyID, principals, sshutil.HostCert, sshutil.DefaultIIDTemplate\n}\n\nfunc FormatServiceAccountUsername(serviceAccountID string) string {\n\treturn fmt.Sprintf(\"sa_%v\", serviceAccountID)\n}\n\nfunc (p *GCP) genUserOptions(_ context.Context, claims *gcpPayload) (SignSSHOptions, string, []string, sshutil.CertType, string) {\n\tkeyID := claims.Email\n\tprincipals := []string{\n\t\tFormatServiceAccountUsername(claims.Subject),\n\t\tclaims.Email,\n\t}\n\n\treturn SignSSHOptions{CertType: SSHUserCert}, keyID, principals, sshutil.UserCert, sshutil.DefaultTemplate\n}\n\nfunc (p *GCP) isUnauthorizedToIssueSSHCert(certType string) error {\n\tif !p.ctl.Claimer.IsSSHCAEnabled() {\n\t\treturn errs.Unauthorized(\"gcp.AuthorizeSSHSign; sshCA is disabled for gcp provisioner '%s'\", p.GetName())\n\t}\n\n\tif certType == SSHHostCert && *p.DisableSSHCAHost {\n\t\treturn errs.Unauthorized(\"gcp.AuthorizeSSHSign; sshCA for Hosts is disabled for gcp provisioner '%s'\", p.GetName())\n\t}\n\n\tif certType == SSHUserCert && *p.DisableSSHCAUser {\n\t\treturn errs.Unauthorized(\"gcp.AuthorizeSSHSign; sshCA for Users is disabled for gcp provisioner '%s'\", p.GetName())\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "authority/provisioner/gcp_test.go",
    "content": "package provisioner\n\nimport (\n\t\"context\"\n\t\"crypto\"\n\t\"crypto/rand\"\n\t\"crypto/rsa\"\n\t\"crypto/sha256\"\n\t\"crypto/x509\"\n\t\"encoding/hex\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"go.step.sm/crypto/jose\"\n\n\t\"github.com/smallstep/assert\"\n\t\"github.com/smallstep/certificates/api/render\"\n\t\"github.com/smallstep/certificates/authority/provisioner/gcp\"\n)\n\nfunc TestGCP_Getters(t *testing.T) {\n\tp, err := generateGCP()\n\tassert.FatalError(t, err)\n\tid := \"gcp/\" + p.Name\n\tif got := p.GetID(); got != id {\n\t\tt.Errorf(\"GCP.GetID() = %v, want %v\", got, id)\n\t}\n\tif got := p.GetName(); got != p.Name {\n\t\tt.Errorf(\"GCP.GetName() = %v, want %v\", got, p.Name)\n\t}\n\tif got := p.GetType(); got != TypeGCP {\n\t\tt.Errorf(\"GCP.GetType() = %v, want %v\", got, TypeGCP)\n\t}\n\tkid, key, ok := p.GetEncryptedKey()\n\tif kid != \"\" || key != \"\" || ok == true {\n\t\tt.Errorf(\"GCP.GetEncryptedKey() = (%v, %v, %v), want (%v, %v, %v)\",\n\t\t\tkid, key, ok, \"\", \"\", false)\n\t}\n\n\taud := \"https://ca.smallstep.com/1.0/sign#\" + url.QueryEscape(id)\n\texpected := fmt.Sprintf(\"http://metadata/computeMetadata/v1/instance/service-accounts/default/identity?audience=%s&format=full&licenses=FALSE\", url.QueryEscape(aud))\n\tif got := p.GetIdentityURL(aud); got != expected {\n\t\tt.Errorf(\"GCP.GetIdentityURL() = %v, want %v\", got, expected)\n\t}\n}\n\nfunc TestGCP_GetTokenID(t *testing.T) {\n\tp1, err := generateGCP()\n\tassert.FatalError(t, err)\n\tp1.Name = \"name\"\n\n\tp2, err := generateGCP()\n\tassert.FatalError(t, err)\n\tp2.DisableTrustOnFirstUse = true\n\n\tnow := time.Now()\n\tt1, err := generateGCPToken(p1.ServiceAccounts[0],\n\t\t\"https://accounts.google.com\", \"gcp/name\",\n\t\t\"instance-id\", \"instance-name\", \"project-id\", \"zone\",\n\t\tnow, &p1.keyStore.keySet.Keys[0])\n\tassert.FatalError(t, err)\n\tt2, err := generateGCPToken(p2.ServiceAccounts[0],\n\t\t\"https://accounts.google.com\", p2.GetID(),\n\t\t\"instance-id\", \"instance-name\", \"project-id\", \"zone\",\n\t\tnow, &p2.keyStore.keySet.Keys[0])\n\tassert.FatalError(t, err)\n\n\tsum := sha256.Sum256([]byte(\"gcp/name.instance-id\"))\n\twant1 := strings.ToLower(hex.EncodeToString(sum[:]))\n\tsum = sha256.Sum256([]byte(t2))\n\twant2 := strings.ToLower(hex.EncodeToString(sum[:]))\n\n\ttype args struct {\n\t\ttoken string\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tgcp     *GCP\n\t\targs    args\n\t\twant    string\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", p1, args{t1}, want1, false},\n\t\t{\"ok\", p2, args{t2}, want2, false},\n\t\t{\"fail token\", p1, args{\"token\"}, \"\", true},\n\t\t{\"fail claims\", p1, args{\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.ey.fooo\"}, \"\", true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := tt.gcp.GetTokenID(tt.args.token)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"GCP.GetTokenID() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif got != tt.want {\n\t\t\t\tt.Errorf(\"GCP.GetTokenID() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestGCP_GetIdentityToken(t *testing.T) {\n\tp1, err := generateGCP()\n\tassert.FatalError(t, err)\n\n\tt1, err := generateGCPToken(p1.ServiceAccounts[0],\n\t\t\"https://accounts.google.com\", p1.GetID(),\n\t\t\"instance-id\", \"instance-name\", \"project-id\", \"zone\",\n\t\ttime.Now(), &p1.keyStore.keySet.Keys[0])\n\tassert.FatalError(t, err)\n\n\tsrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tswitch r.URL.Path {\n\t\tcase \"/bad-request\":\n\t\t\thttp.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)\n\t\tdefault:\n\t\t\tw.Write([]byte(t1))\n\t\t}\n\t}))\n\tdefer srv.Close()\n\n\ttype args struct {\n\t\tsubject string\n\t\tcaURL   string\n\t}\n\ttests := []struct {\n\t\tname        string\n\t\tgcp         *GCP\n\t\targs        args\n\t\tidentityURL string\n\t\twant        string\n\t\twantErr     bool\n\t}{\n\t\t{\"ok\", p1, args{\"subject\", \"https://ca\"}, srv.URL, t1, false},\n\t\t{\"fail ca url\", p1, args{\"subject\", \"://ca\"}, srv.URL, \"\", true},\n\t\t{\"fail request\", p1, args{\"subject\", \"https://ca\"}, srv.URL + \"/bad-request\", \"\", true},\n\t\t{\"fail url\", p1, args{\"subject\", \"https://ca\"}, \"://ca.smallstep.com\", \"\", true},\n\t\t{\"fail connect\", p1, args{\"subject\", \"https://ca\"}, \"foobarzar\", \"\", true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ttt.gcp.config.IdentityURL = tt.identityURL\n\t\t\tgot, err := tt.gcp.GetIdentityToken(tt.args.subject, tt.args.caURL)\n\t\t\tt.Log(err)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"GCP.GetIdentityToken() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif got != tt.want {\n\t\t\t\tt.Errorf(\"GCP.GetIdentityToken() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestGCP_Init(t *testing.T) {\n\tsrv := generateJWKServer(2)\n\tdefer srv.Close()\n\n\tconfig := Config{\n\t\tClaims: globalProvisionerClaims,\n\t}\n\tbadClaims := &Claims{\n\t\tDefaultTLSDur: &Duration{0},\n\t}\n\tzero := Duration{Duration: 0}\n\ttype fields struct {\n\t\tType            string\n\t\tName            string\n\t\tServiceAccounts []string\n\t\tInstanceAge     Duration\n\t\tClaims          *Claims\n\t}\n\ttype args struct {\n\t\tconfig   Config\n\t\tcertsURL string\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tfields  fields\n\t\targs    args\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", fields{\"GCP\", \"name\", nil, zero, nil}, args{config, srv.URL}, false},\n\t\t{\"ok\", fields{\"GCP\", \"name\", nil, zero, nil}, args{config, srv.URL}, false},\n\t\t{\"ok\", fields{\"GCP\", \"name\", []string{\"service-account\"}, zero, nil}, args{config, srv.URL}, false},\n\t\t{\"ok\", fields{\"GCP\", \"name\", []string{\"service-account\"}, Duration{Duration: 1 * time.Minute}, nil}, args{config, srv.URL}, false},\n\t\t{\"bad type\", fields{\"\", \"name\", nil, zero, nil}, args{config, srv.URL}, true},\n\t\t{\"bad name\", fields{\"GCP\", \"\", nil, zero, nil}, args{config, srv.URL}, true},\n\t\t{\"bad duration\", fields{\"GCP\", \"name\", nil, Duration{Duration: -1 * time.Minute}, nil}, args{config, srv.URL}, true},\n\t\t{\"bad claims\", fields{\"GCP\", \"name\", nil, zero, badClaims}, args{config, srv.URL}, true},\n\t\t{\"bad certs\", fields{\"GCP\", \"name\", nil, zero, nil}, args{config, srv.URL + \"/error\"}, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tp := &GCP{\n\t\t\t\tType:            tt.fields.Type,\n\t\t\t\tName:            tt.fields.Name,\n\t\t\t\tServiceAccounts: tt.fields.ServiceAccounts,\n\t\t\t\tInstanceAge:     tt.fields.InstanceAge,\n\t\t\t\tClaims:          tt.fields.Claims,\n\t\t\t\tconfig: &gcpConfig{\n\t\t\t\t\tCertsURL:    tt.args.certsURL,\n\t\t\t\t\tIdentityURL: gcpIdentityURL,\n\t\t\t\t},\n\t\t\t}\n\t\t\tif err := p.Init(tt.args.config); (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"GCP.Init() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\n\t\t\tif *p.DisableSSHCAUser != true {\n\t\t\t\tt.Errorf(\"By default DisableSSHCAUser should be true\")\n\t\t\t}\n\n\t\t\tif *p.DisableSSHCAHost != false {\n\t\t\t\tt.Errorf(\"By default DisableSSHCAHost should be false\")\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestGCP_authorizeToken(t *testing.T) {\n\ttype test struct {\n\t\tp     *GCP\n\t\ttoken string\n\t\terr   error\n\t\tcode  int\n\t}\n\ttests := map[string]func(*testing.T) test{\n\t\t\"fail/bad-token\": func(t *testing.T) test {\n\t\t\tp, err := generateGCP()\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tp:     p,\n\t\t\t\ttoken: \"foo\",\n\t\t\t\tcode:  http.StatusUnauthorized,\n\t\t\t\terr:   errors.New(\"gcp.authorizeToken; error parsing gcp token\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/cannot-validate-sig\": func(t *testing.T) test {\n\t\t\tp, err := generateGCP()\n\t\t\tassert.FatalError(t, err)\n\t\t\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\tassert.FatalError(t, err)\n\t\t\ttok, err := generateGCPToken(p.ServiceAccounts[0],\n\t\t\t\t\"https://accounts.google.com\", p.GetID(),\n\t\t\t\t\"instance-id\", \"instance-name\", \"project-id\", \"zone\",\n\t\t\t\ttime.Now(), jwk)\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tp:     p,\n\t\t\t\ttoken: tok,\n\t\t\t\tcode:  http.StatusUnauthorized,\n\t\t\t\terr:   errors.New(\"gcp.authorizeToken; failed to validate gcp token payload - cannot find key for kid \"),\n\t\t\t}\n\t\t},\n\t\t\"fail/invalid-issuer\": func(t *testing.T) test {\n\t\t\tp, err := generateGCP()\n\t\t\tassert.FatalError(t, err)\n\t\t\ttok, err := generateGCPToken(p.ServiceAccounts[0],\n\t\t\t\t\"https://foo.bar.zap\", p.GetID(),\n\t\t\t\t\"instance-id\", \"instance-name\", \"project-id\", \"zone\",\n\t\t\t\ttime.Now(), &p.keyStore.keySet.Keys[0])\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tp:     p,\n\t\t\t\ttoken: tok,\n\t\t\t\tcode:  http.StatusUnauthorized,\n\t\t\t\terr:   errors.New(\"gcp.authorizeToken; invalid gcp token payload\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/invalid-serviceAccount\": func(t *testing.T) test {\n\t\t\tp, err := generateGCP()\n\t\t\tassert.FatalError(t, err)\n\t\t\ttok, err := generateGCPToken(\"foo\",\n\t\t\t\t\"https://accounts.google.com\", p.GetID(),\n\t\t\t\t\"instance-id\", \"instance-name\", \"project-id\", \"zone\",\n\t\t\t\ttime.Now(), &p.keyStore.keySet.Keys[0])\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tp:     p,\n\t\t\t\ttoken: tok,\n\t\t\t\tcode:  http.StatusUnauthorized,\n\t\t\t\terr:   errors.New(\"gcp.authorizeToken; invalid gcp token - invalid subject claim\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/invalid-projectID\": func(t *testing.T) test {\n\t\t\tp, err := generateGCP()\n\t\t\tassert.FatalError(t, err)\n\t\t\tp.projectValidator = &gcp.ProjectValidator{ProjectIDs: []string{\"foo\", \"bar\"}}\n\t\t\ttok, err := generateGCPToken(p.ServiceAccounts[0],\n\t\t\t\t\"https://accounts.google.com\", p.GetID(),\n\t\t\t\t\"instance-id\", \"instance-name\", \"project-id\", \"zone\",\n\t\t\t\ttime.Now(), &p.keyStore.keySet.Keys[0])\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tp:     p,\n\t\t\t\ttoken: tok,\n\t\t\t\tcode:  http.StatusUnauthorized,\n\t\t\t\terr:   errors.New(\"gcp.authorizeToken; invalid gcp token - invalid project id\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/instance-age\": func(t *testing.T) test {\n\t\t\tp, err := generateGCP()\n\t\t\tassert.FatalError(t, err)\n\t\t\tp.InstanceAge = Duration{1 * time.Minute}\n\t\t\ttok, err := generateGCPToken(p.ServiceAccounts[0],\n\t\t\t\t\"https://accounts.google.com\", p.GetID(),\n\t\t\t\t\"instance-id\", \"instance-name\", \"project-id\", \"zone\",\n\t\t\t\ttime.Now().Add(-1*time.Minute), &p.keyStore.keySet.Keys[0])\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tp:     p,\n\t\t\t\ttoken: tok,\n\t\t\t\tcode:  http.StatusUnauthorized,\n\t\t\t\terr:   errors.New(\"gcp.authorizeToken; token google.compute_engine.instance_creation_timestamp is too old\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/empty-instance-id\": func(t *testing.T) test {\n\t\t\tp, err := generateGCP()\n\t\t\tassert.FatalError(t, err)\n\t\t\ttok, err := generateGCPToken(p.ServiceAccounts[0],\n\t\t\t\t\"https://accounts.google.com\", p.GetID(),\n\t\t\t\t\"\", \"instance-name\", \"project-id\", \"zone\",\n\t\t\t\ttime.Now(), &p.keyStore.keySet.Keys[0])\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tp:     p,\n\t\t\t\ttoken: tok,\n\t\t\t\tcode:  http.StatusUnauthorized,\n\t\t\t\terr:   errors.New(\"gcp.authorizeToken; gcp token google.compute_engine.instance_id cannot be empty\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/empty-instance-name\": func(t *testing.T) test {\n\t\t\tp, err := generateGCP()\n\t\t\tassert.FatalError(t, err)\n\t\t\ttok, err := generateGCPToken(p.ServiceAccounts[0],\n\t\t\t\t\"https://accounts.google.com\", p.GetID(),\n\t\t\t\t\"instance-id\", \"\", \"project-id\", \"zone\",\n\t\t\t\ttime.Now(), &p.keyStore.keySet.Keys[0])\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tp:     p,\n\t\t\t\ttoken: tok,\n\t\t\t\tcode:  http.StatusUnauthorized,\n\t\t\t\terr:   errors.New(\"gcp.authorizeToken; gcp token google.compute_engine.instance_name cannot be empty\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/empty-project-id\": func(t *testing.T) test {\n\t\t\tp, err := generateGCP()\n\t\t\tassert.FatalError(t, err)\n\t\t\ttok, err := generateGCPToken(p.ServiceAccounts[0],\n\t\t\t\t\"https://accounts.google.com\", p.GetID(),\n\t\t\t\t\"instance-id\", \"instance-name\", \"\", \"zone\",\n\t\t\t\ttime.Now(), &p.keyStore.keySet.Keys[0])\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tp:     p,\n\t\t\t\ttoken: tok,\n\t\t\t\tcode:  http.StatusUnauthorized,\n\t\t\t\terr:   errors.New(\"gcp.authorizeToken; gcp token google.compute_engine.project_id cannot be empty\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/empty-zone\": func(t *testing.T) test {\n\t\t\tp, err := generateGCP()\n\t\t\tassert.FatalError(t, err)\n\t\t\ttok, err := generateGCPToken(p.ServiceAccounts[0],\n\t\t\t\t\"https://accounts.google.com\", p.GetID(),\n\t\t\t\t\"instance-id\", \"instance-name\", \"project-id\", \"\",\n\t\t\t\ttime.Now(), &p.keyStore.keySet.Keys[0])\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tp:     p,\n\t\t\t\ttoken: tok,\n\t\t\t\tcode:  http.StatusUnauthorized,\n\t\t\t\terr:   errors.New(\"gcp.authorizeToken; gcp token google.compute_engine.zone cannot be empty\"),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tp, err := generateGCP()\n\t\t\tassert.FatalError(t, err)\n\t\t\ttok, err := generateGCPToken(p.ServiceAccounts[0],\n\t\t\t\t\"https://accounts.google.com\", p.GetID(),\n\t\t\t\t\"instance-id\", \"instance-name\", \"project-id\", \"zone\",\n\t\t\t\ttime.Now(), &p.keyStore.keySet.Keys[0])\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tp:     p,\n\t\t\t\ttoken: tok,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, tt := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ttc := tt(t)\n\t\t\tif claims, err := tc.p.authorizeToken(context.Background(), tc.token); err != nil {\n\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\tvar sc render.StatusCodedError\n\t\t\t\t\tassert.Fatal(t, errors.As(err, &sc), \"error does not implement StatusCodedError interface\")\n\t\t\t\t\tassert.Equals(t, sc.StatusCode(), tc.code)\n\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.err.Error())\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif assert.Nil(t, tc.err) && assert.NotNil(t, claims) {\n\t\t\t\t\tassert.Equals(t, claims.Subject, tc.p.ServiceAccounts[0])\n\t\t\t\t\tassert.Equals(t, claims.Issuer, \"https://accounts.google.com\")\n\t\t\t\t\tassert.NotNil(t, claims.Google)\n\n\t\t\t\t\taud, err := generateSignAudience(\"https://ca.smallstep.com\", tc.p.GetID())\n\t\t\t\t\tassert.FatalError(t, err)\n\t\t\t\t\tassert.Equals(t, claims.Audience[0], aud)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestGCP_AuthorizeSign(t *testing.T) {\n\tp1, err := generateGCP()\n\tassert.FatalError(t, err)\n\n\tp2, err := generateGCP()\n\tassert.FatalError(t, err)\n\tp2.DisableCustomSANs = true\n\n\tp3, err := generateGCP()\n\tassert.FatalError(t, err)\n\tp3.projectValidator = &gcp.ProjectValidator{ProjectIDs: []string{\"other-project-id\"}}\n\tp3.ServiceAccounts = []string{\"foo@developer.gserviceaccount.com\"}\n\tp3.InstanceAge = Duration{1 * time.Minute}\n\n\taKey, err := generateJSONWebKey()\n\tassert.FatalError(t, err)\n\n\tt1, err := generateGCPToken(p1.ServiceAccounts[0],\n\t\t\"https://accounts.google.com\", p1.GetID(),\n\t\t\"instance-id\", \"instance-name\", \"project-id\", \"zone\",\n\t\ttime.Now(), &p1.keyStore.keySet.Keys[0])\n\tassert.FatalError(t, err)\n\tt2, err := generateGCPToken(p2.ServiceAccounts[0],\n\t\t\"https://accounts.google.com\", p2.GetID(),\n\t\t\"instance-id\", \"instance-name\", \"project-id\", \"zone\",\n\t\ttime.Now(), &p2.keyStore.keySet.Keys[0])\n\tassert.FatalError(t, err)\n\tt3, err := generateGCPToken(p3.ServiceAccounts[0],\n\t\t\"https://accounts.google.com\", p3.GetID(),\n\t\t\"instance-id\", \"instance-name\", \"other-project-id\", \"zone\",\n\t\ttime.Now(), &p3.keyStore.keySet.Keys[0])\n\tassert.FatalError(t, err)\n\n\tfailKey, err := generateGCPToken(p1.ServiceAccounts[0],\n\t\t\"https://accounts.google.com\", p1.GetID(),\n\t\t\"instance-id\", \"instance-name\", \"project-id\", \"zone\",\n\t\ttime.Now(), aKey)\n\tassert.FatalError(t, err)\n\tfailIss, err := generateGCPToken(p1.ServiceAccounts[0],\n\t\t\"https://foo.bar.zar\", p1.GetID(),\n\t\t\"instance-id\", \"instance-name\", \"project-id\", \"zone\",\n\t\ttime.Now(), &p1.keyStore.keySet.Keys[0])\n\tassert.FatalError(t, err)\n\tfailAud, err := generateGCPToken(p1.ServiceAccounts[0],\n\t\t\"https://accounts.google.com\", \"gcp:foo\",\n\t\t\"instance-id\", \"instance-name\", \"project-id\", \"zone\",\n\t\ttime.Now(), &p1.keyStore.keySet.Keys[0])\n\tassert.FatalError(t, err)\n\tfailExp, err := generateGCPToken(p1.ServiceAccounts[0],\n\t\t\"https://accounts.google.com\", p1.GetID(),\n\t\t\"instance-id\", \"instance-name\", \"project-id\", \"zone\",\n\t\ttime.Now().Add(-360*time.Second), &p1.keyStore.keySet.Keys[0])\n\tassert.FatalError(t, err)\n\tfailNbf, err := generateGCPToken(p1.ServiceAccounts[0],\n\t\t\"https://accounts.google.com\", p1.GetID(),\n\t\t\"instance-id\", \"instance-name\", \"project-id\", \"zone\",\n\t\ttime.Now().Add(360*time.Second), &p1.keyStore.keySet.Keys[0])\n\tassert.FatalError(t, err)\n\tfailServiceAccount, err := generateGCPToken(\"foo\",\n\t\t\"https://accounts.google.com\", p1.GetID(),\n\t\t\"instance-id\", \"instance-name\", \"project-id\", \"zone\",\n\t\ttime.Now(), &p1.keyStore.keySet.Keys[0])\n\tassert.FatalError(t, err)\n\tfailInvalidProjectID, err := generateGCPToken(p3.ServiceAccounts[0],\n\t\t\"https://accounts.google.com\", p3.GetID(),\n\t\t\"instance-id\", \"instance-name\", \"project-id\", \"zone\",\n\t\ttime.Now(), &p3.keyStore.keySet.Keys[0])\n\tassert.FatalError(t, err)\n\tfailInvalidInstanceAge, err := generateGCPToken(p3.ServiceAccounts[0],\n\t\t\"https://accounts.google.com\", p3.GetID(),\n\t\t\"instance-id\", \"instance-name\", \"other-project-id\", \"zone\",\n\t\ttime.Now().Add(-1*time.Minute), &p3.keyStore.keySet.Keys[0])\n\tassert.FatalError(t, err)\n\tfailInstanceID, err := generateGCPToken(p1.ServiceAccounts[0],\n\t\t\"https://accounts.google.com\", p1.GetID(),\n\t\t\"\", \"instance-name\", \"project-id\", \"zone\",\n\t\ttime.Now(), &p1.keyStore.keySet.Keys[0])\n\tassert.FatalError(t, err)\n\tfailInstanceName, err := generateGCPToken(p1.ServiceAccounts[0],\n\t\t\"https://accounts.google.com\", p1.GetID(),\n\t\t\"instance-id\", \"\", \"project-id\", \"zone\",\n\t\ttime.Now(), &p1.keyStore.keySet.Keys[0])\n\tassert.FatalError(t, err)\n\tfailProjectID, err := generateGCPToken(p1.ServiceAccounts[0],\n\t\t\"https://accounts.google.com\", p1.GetID(),\n\t\t\"instance-id\", \"instance-name\", \"\", \"zone\",\n\t\ttime.Now(), &p1.keyStore.keySet.Keys[0])\n\tassert.FatalError(t, err)\n\tfailZone, err := generateGCPToken(p1.ServiceAccounts[0],\n\t\t\"https://accounts.google.com\", p1.GetID(),\n\t\t\"instance-id\", \"instance-name\", \"project-id\", \"\",\n\t\ttime.Now(), &p1.keyStore.keySet.Keys[0])\n\tassert.FatalError(t, err)\n\n\ttype args struct {\n\t\ttoken string\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tgcp     *GCP\n\t\targs    args\n\t\twantLen int\n\t\tcode    int\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", p1, args{t1}, 8, http.StatusOK, false},\n\t\t{\"ok\", p2, args{t2}, 13, http.StatusOK, false},\n\t\t{\"ok\", p3, args{t3}, 8, http.StatusOK, false},\n\t\t{\"fail token\", p1, args{\"token\"}, 0, http.StatusUnauthorized, true},\n\t\t{\"fail key\", p1, args{failKey}, 0, http.StatusUnauthorized, true},\n\t\t{\"fail iss\", p1, args{failIss}, 0, http.StatusUnauthorized, true},\n\t\t{\"fail aud\", p1, args{failAud}, 0, http.StatusUnauthorized, true},\n\t\t{\"fail exp\", p1, args{failExp}, 0, http.StatusUnauthorized, true},\n\t\t{\"fail nbf\", p1, args{failNbf}, 0, http.StatusUnauthorized, true},\n\t\t{\"fail service account\", p1, args{failServiceAccount}, 0, http.StatusUnauthorized, true},\n\t\t{\"fail invalid project id\", p3, args{failInvalidProjectID}, 0, http.StatusUnauthorized, true},\n\t\t{\"fail invalid instance age\", p3, args{failInvalidInstanceAge}, 0, http.StatusUnauthorized, true},\n\t\t{\"fail instance id\", p1, args{failInstanceID}, 0, http.StatusUnauthorized, true},\n\t\t{\"fail instance name\", p1, args{failInstanceName}, 0, http.StatusUnauthorized, true},\n\t\t{\"fail project id\", p1, args{failProjectID}, 0, http.StatusUnauthorized, true},\n\t\t{\"fail zone\", p1, args{failZone}, 0, http.StatusUnauthorized, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tctx := NewContextWithMethod(context.Background(), SignMethod)\n\t\t\tswitch got, err := tt.gcp.AuthorizeSign(ctx, tt.args.token); {\n\t\t\tcase (err != nil) != tt.wantErr:\n\t\t\t\tt.Errorf(\"GCP.AuthorizeSign() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\tcase err != nil:\n\t\t\t\tvar sc render.StatusCodedError\n\t\t\t\tassert.Fatal(t, errors.As(err, &sc), \"error does not implement StatusCodedError interface\")\n\t\t\t\tassert.Equals(t, sc.StatusCode(), tt.code)\n\t\t\tdefault:\n\t\t\t\tassert.Equals(t, tt.wantLen, len(got))\n\t\t\t\tfor _, o := range got {\n\t\t\t\t\tswitch v := o.(type) {\n\t\t\t\t\tcase *GCP:\n\t\t\t\t\tcase certificateOptionsFunc:\n\t\t\t\t\tcase *provisionerExtensionOption:\n\t\t\t\t\t\tassert.Equals(t, v.Type, TypeGCP)\n\t\t\t\t\t\tassert.Equals(t, v.Name, tt.gcp.GetName())\n\t\t\t\t\t\tassert.Equals(t, v.CredentialID, tt.gcp.ServiceAccounts[0])\n\t\t\t\t\t\tassert.Len(t, 4, v.KeyValuePairs)\n\t\t\t\t\tcase profileDefaultDuration:\n\t\t\t\t\t\tassert.Equals(t, time.Duration(v), tt.gcp.ctl.Claimer.DefaultTLSCertDuration())\n\t\t\t\t\tcase commonNameSliceValidator:\n\t\t\t\t\t\tassert.Equals(t, []string(v), []string{\"instance-name\", \"instance-id\", \"instance-name.c.project-id.internal\", \"instance-name.zone.c.project-id.internal\"})\n\t\t\t\t\tcase defaultPublicKeyValidator:\n\t\t\t\t\tcase *validityValidator:\n\t\t\t\t\t\tassert.Equals(t, v.min, tt.gcp.ctl.Claimer.MinTLSCertDuration())\n\t\t\t\t\t\tassert.Equals(t, v.max, tt.gcp.ctl.Claimer.MaxTLSCertDuration())\n\t\t\t\t\tcase ipAddressesValidator:\n\t\t\t\t\t\tassert.Equals(t, v, nil)\n\t\t\t\t\tcase emailAddressesValidator:\n\t\t\t\t\t\tassert.Equals(t, v, nil)\n\t\t\t\t\tcase *urisValidator:\n\t\t\t\t\t\tassert.Equals(t, v.uris, nil)\n\t\t\t\t\t\tassert.Equals(t, MethodFromContext(v.ctx), SignMethod)\n\t\t\t\t\tcase dnsNamesSubsetValidator:\n\t\t\t\t\t\tassert.Equals(t, []string(v), []string{\"instance-name.c.project-id.internal\", \"instance-name.zone.c.project-id.internal\"})\n\t\t\t\t\tcase *x509NamePolicyValidator:\n\t\t\t\t\t\tassert.Equals(t, nil, v.policyEngine)\n\t\t\t\t\tcase *WebhookController:\n\t\t\t\t\t\tassert.Len(t, 0, v.webhooks)\n\t\t\t\t\tdefault:\n\t\t\t\t\t\tassert.FatalError(t, fmt.Errorf(\"unexpected sign option of type %T\", v))\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestGCP_AuthorizeSSHSign(t *testing.T) {\n\ttm, fn := mockNow()\n\tdefer fn()\n\n\tp1, err := generateGCP()\n\tassert.FatalError(t, err)\n\tp1.DisableCustomSANs = true\n\t// enable ssh user CA\n\tdisableSSCAUser := false\n\tp1.DisableSSHCAUser = &disableSSCAUser\n\n\tp2, err := generateGCP()\n\tassert.FatalError(t, err)\n\tp2.DisableCustomSANs = false\n\n\tp3, err := generateGCP()\n\tassert.FatalError(t, err)\n\t// disable sshCA\n\tdisable := false\n\tp3.Claims = &Claims{EnableSSHCA: &disable}\n\tp3.ctl.Claimer, err = NewClaimer(p3.Claims, globalProvisionerClaims)\n\tassert.FatalError(t, err)\n\n\tp4, err := generateGCP()\n\tassert.FatalError(t, err)\n\t// disable ssh host CA\n\tdisableSSCAHost := true\n\tp4.DisableSSHCAHost = &disableSSCAHost\n\n\tt1, err := generateGCPToken(p1.ServiceAccounts[0],\n\t\t\"https://accounts.google.com\", p1.GetID(),\n\t\t\"instance-id\", \"instance-name\", \"project-id\", \"zone\",\n\t\ttime.Now(), &p1.keyStore.keySet.Keys[0])\n\tassert.FatalError(t, err)\n\n\tt2, err := generateGCPToken(p2.ServiceAccounts[0],\n\t\t\"https://accounts.google.com\", p2.GetID(),\n\t\t\"instance-id\", \"instance-name\", \"project-id\", \"zone\",\n\t\ttime.Now(), &p2.keyStore.keySet.Keys[0])\n\tassert.FatalError(t, err)\n\n\tkey, err := generateJSONWebKey()\n\tassert.FatalError(t, err)\n\n\tsigner, err := generateJSONWebKey()\n\tassert.FatalError(t, err)\n\n\tpub := key.Public().Key\n\trsa2048, err := rsa.GenerateKey(rand.Reader, 2048)\n\tassert.FatalError(t, err)\n\t//nolint:gosec // tests minimum size of the key\n\trsa1024, err := rsa.GenerateKey(rand.Reader, 1024)\n\tassert.FatalError(t, err)\n\n\thostDuration := p1.ctl.Claimer.DefaultHostSSHCertDuration()\n\texpectedHostOptions := &SignSSHOptions{\n\t\tCertType: \"host\", Principals: []string{\"instance-name.c.project-id.internal\", \"instance-name.zone.c.project-id.internal\"},\n\t\tValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(hostDuration)),\n\t}\n\texpectedHostOptionsPrincipal1 := &SignSSHOptions{\n\t\tCertType: \"host\", Principals: []string{\"instance-name.c.project-id.internal\"},\n\t\tValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(hostDuration)),\n\t}\n\texpectedHostOptionsPrincipal2 := &SignSSHOptions{\n\t\tCertType: \"host\", Principals: []string{\"instance-name.zone.c.project-id.internal\"},\n\t\tValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(hostDuration)),\n\t}\n\texpectedCustomOptions := &SignSSHOptions{\n\t\tCertType: \"host\", Principals: []string{\"foo.bar\", \"bar.foo\"},\n\t\tValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(hostDuration)),\n\t}\n\texpectedUserOptions := &SignSSHOptions{\n\t\tCertType: \"user\", Principals: []string{FormatServiceAccountUsername(p1.ServiceAccounts[0]), \"foo@developer.gserviceaccount.com\"},\n\t\tValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(p1.ctl.Claimer.DefaultUserSSHCertDuration())),\n\t}\n\n\ttype args struct {\n\t\ttoken   string\n\t\tsshOpts SignSSHOptions\n\t\tkey     interface{}\n\t}\n\ttests := []struct {\n\t\tname        string\n\t\tgcp         *GCP\n\t\targs        args\n\t\texpected    *SignSSHOptions\n\t\tcode        int\n\t\twantErr     bool\n\t\twantSignErr bool\n\t}{\n\t\t{\"ok\", p1, args{t1, SignSSHOptions{}, pub}, expectedHostOptions, http.StatusOK, false, false},\n\t\t{\"ok-rsa2048\", p1, args{t1, SignSSHOptions{}, rsa2048.Public()}, expectedHostOptions, http.StatusOK, false, false},\n\t\t{\"ok-type-host\", p1, args{t1, SignSSHOptions{CertType: \"host\"}, pub}, expectedHostOptions, http.StatusOK, false, false},\n\t\t{\"ok-type-user\", p1, args{t1, SignSSHOptions{CertType: \"user\"}, pub}, expectedUserOptions, http.StatusOK, false, false},\n\t\t{\"ok-principals\", p1, args{t1, SignSSHOptions{Principals: []string{\"instance-name.c.project-id.internal\", \"instance-name.zone.c.project-id.internal\"}}, pub}, expectedHostOptions, http.StatusOK, false, false},\n\t\t{\"ok-principal1\", p1, args{t1, SignSSHOptions{Principals: []string{\"instance-name.c.project-id.internal\"}}, pub}, expectedHostOptionsPrincipal1, http.StatusOK, false, false},\n\t\t{\"ok-principal2\", p1, args{t1, SignSSHOptions{Principals: []string{\"instance-name.zone.c.project-id.internal\"}}, pub}, expectedHostOptionsPrincipal2, http.StatusOK, false, false},\n\t\t{\"ok-options\", p1, args{t1, SignSSHOptions{CertType: \"host\", Principals: []string{\"instance-name.c.project-id.internal\", \"instance-name.zone.c.project-id.internal\"}}, pub}, expectedHostOptions, http.StatusOK, false, false},\n\t\t{\"ok-custom\", p2, args{t2, SignSSHOptions{Principals: []string{\"foo.bar\", \"bar.foo\"}}, pub}, expectedCustomOptions, http.StatusOK, false, false},\n\t\t{\"fail-rsa1024\", p1, args{t1, SignSSHOptions{}, rsa1024.Public()}, expectedHostOptions, http.StatusOK, false, true},\n\t\t{\"fail-principal\", p1, args{t1, SignSSHOptions{Principals: []string{\"smallstep.com\"}}, pub}, nil, http.StatusOK, false, true},\n\t\t{\"fail-extra-principal\", p1, args{t1, SignSSHOptions{Principals: []string{\"instance-name.c.project-id.internal\", \"instance-name.zone.c.project-id.internal\", \"smallstep.com\"}}, pub}, nil, http.StatusOK, false, true},\n\t\t{\"fail-sshCA-disabled\", p3, args{\"foo\", SignSSHOptions{}, pub}, expectedHostOptions, http.StatusUnauthorized, true, false},\n\t\t{\"fail-type-host\", p4, args{\"foo\", SignSSHOptions{CertType: \"host\"}, pub}, nil, http.StatusUnauthorized, true, false},\n\t\t{\"fail-type-user\", p4, args{\"foo\", SignSSHOptions{CertType: \"host\"}, pub}, nil, http.StatusUnauthorized, true, false},\n\t\t{\"fail-invalid-token\", p1, args{\"foo\", SignSSHOptions{}, pub}, expectedHostOptions, http.StatusUnauthorized, true, false},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tctx := context.Background()\n\t\t\tif tt.args.sshOpts.CertType == SSHUserCert {\n\t\t\t\tctx = NewContextWithCertType(ctx, SSHUserCert)\n\t\t\t}\n\n\t\t\tgot, err := tt.gcp.AuthorizeSSHSign(ctx, tt.args.token)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"GCP.AuthorizeSSHSign() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif err != nil {\n\t\t\t\tvar sc render.StatusCodedError\n\t\t\t\tassert.Fatal(t, errors.As(err, &sc), \"error does not implement StatusCodedError interface\")\n\t\t\t\tassert.Equals(t, sc.StatusCode(), tt.code)\n\t\t\t\tassert.Nil(t, got)\n\t\t\t} else if assert.NotNil(t, got) {\n\t\t\t\tcert, err := signSSHCertificate(tt.args.key, tt.args.sshOpts, got, signer.Key.(crypto.Signer))\n\t\t\t\tif (err != nil) != tt.wantSignErr {\n\t\t\t\t\tt.Errorf(\"SignSSH error = %v, wantSignErr %v\", err, tt.wantSignErr)\n\t\t\t\t} else {\n\t\t\t\t\tif tt.wantSignErr {\n\t\t\t\t\t\tassert.Nil(t, cert)\n\t\t\t\t\t} else {\n\t\t\t\t\t\tassert.NoError(t, validateSSHCertificate(cert, tt.expected))\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestGCP_AuthorizeRenew(t *testing.T) {\n\tnow := time.Now().Truncate(time.Second)\n\tp1, err := generateGCP()\n\tassert.FatalError(t, err)\n\tp2, err := generateGCP()\n\tassert.FatalError(t, err)\n\n\t// disable renewal\n\tdisable := true\n\tp2.Claims = &Claims{DisableRenewal: &disable}\n\tp2.ctl.Claimer, err = NewClaimer(p2.Claims, globalProvisionerClaims)\n\tassert.FatalError(t, err)\n\n\ttype args struct {\n\t\tcert *x509.Certificate\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tprov    *GCP\n\t\targs    args\n\t\tcode    int\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", p1, args{&x509.Certificate{\n\t\t\tNotBefore: now,\n\t\t\tNotAfter:  now.Add(time.Hour),\n\t\t}}, http.StatusOK, false},\n\t\t{\"fail/renewal-disabled\", p2, args{&x509.Certificate{\n\t\t\tNotBefore: now,\n\t\t\tNotAfter:  now.Add(time.Hour),\n\t\t}}, http.StatusUnauthorized, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif err := tt.prov.AuthorizeRenew(context.Background(), tt.args.cert); (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"GCP.AuthorizeRenew() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t} else if err != nil {\n\t\t\t\tvar sc render.StatusCodedError\n\t\t\t\tassert.Fatal(t, errors.As(err, &sc), \"error does not implement StatusCodedError interface\")\n\t\t\t\tassert.Equals(t, sc.StatusCode(), tt.code)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "authority/provisioner/jwk.go",
    "content": "package provisioner\n\nimport (\n\t\"context\"\n\t\"crypto/x509\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n\n\t\"github.com/smallstep/linkedca\"\n\t\"go.step.sm/crypto/jose\"\n\t\"go.step.sm/crypto/sshutil\"\n\t\"go.step.sm/crypto/x509util\"\n\n\t\"github.com/smallstep/certificates/errs\"\n\t\"github.com/smallstep/certificates/internal/cast\"\n)\n\n// jwtPayload extends jwt.Claims with step attributes.\ntype jwtPayload struct {\n\tjose.Claims\n\tSANs         []string     `json:\"sans,omitempty\"`\n\tStep         *stepPayload `json:\"step,omitempty\"`\n\tConfirmation *cnfPayload  `json:\"cnf,omitempty\"`\n}\n\ntype stepPayload struct {\n\tSSH *SignSSHOptions `json:\"ssh,omitempty\"`\n\tRA  *RAInfo         `json:\"ra,omitempty\"`\n}\n\ntype cnfPayload struct {\n\tFingerprint string `json:\"x5rt#S256,omitempty\"`\n}\n\n// JWK is the default provisioner, an entity that can sign tokens necessary for\n// signature requests.\ntype JWK struct {\n\t*base\n\tID           string           `json:\"-\"`\n\tType         string           `json:\"type\"`\n\tName         string           `json:\"name\"`\n\tKey          *jose.JSONWebKey `json:\"key\"`\n\tEncryptedKey string           `json:\"encryptedKey,omitempty\"`\n\tClaims       *Claims          `json:\"claims,omitempty\"`\n\tOptions      *Options         `json:\"options,omitempty\"`\n\tctl          *Controller\n}\n\n// GetID returns the provisioner unique identifier. The name and credential id\n// should uniquely identify any JWK provisioner.\nfunc (p *JWK) GetID() string {\n\tif p.ID != \"\" {\n\t\treturn p.ID\n\t}\n\treturn p.GetIDForToken()\n}\n\n// GetIDForToken returns an identifier that will be used to load the provisioner\n// from a token.\nfunc (p *JWK) GetIDForToken() string {\n\treturn p.Name + \":\" + p.Key.KeyID\n}\n\n// GetTokenID returns the identifier of the token.\nfunc (p *JWK) GetTokenID(ott string) (string, error) {\n\t// Validate payload\n\ttoken, err := jose.ParseSigned(ott)\n\tif err != nil {\n\t\treturn \"\", errors.Wrap(err, \"error parsing token\")\n\t}\n\n\t// Get claims w/out verification. We need to look up the provisioner\n\t// key in order to verify the claims and we need the issuer from the claims\n\t// before we can look up the provisioner.\n\tvar claims jose.Claims\n\tif err = token.UnsafeClaimsWithoutVerification(&claims); err != nil {\n\t\treturn \"\", errors.Wrap(err, \"error verifying claims\")\n\t}\n\treturn claims.ID, nil\n}\n\n// GetName returns the name of the provisioner.\nfunc (p *JWK) GetName() string {\n\treturn p.Name\n}\n\n// GetType returns the type of provisioner.\nfunc (p *JWK) GetType() Type {\n\treturn TypeJWK\n}\n\n// GetEncryptedKey returns the base provisioner encrypted key if it's defined.\nfunc (p *JWK) GetEncryptedKey() (string, string, bool) {\n\treturn p.Key.KeyID, p.EncryptedKey, p.EncryptedKey != \"\"\n}\n\n// Init initializes and validates the fields of a JWK type.\nfunc (p *JWK) Init(config Config) (err error) {\n\tswitch {\n\tcase p.Type == \"\":\n\t\treturn errors.New(\"provisioner type cannot be empty\")\n\tcase p.Name == \"\":\n\t\treturn errors.New(\"provisioner name cannot be empty\")\n\tcase p.Key == nil:\n\t\treturn errors.New(\"provisioner key cannot be empty\")\n\t}\n\n\tp.ctl, err = NewController(p, p.Claims, config, p.Options)\n\treturn\n}\n\n// authorizeToken performs common jwt authorization actions and returns the\n// claims for case specific downstream parsing.\n// e.g. a Sign request will auth/validate different fields than a Revoke request.\nfunc (p *JWK) authorizeToken(token string, audiences []string) (*jwtPayload, error) {\n\tjwt, err := jose.ParseSigned(token)\n\tif err != nil {\n\t\treturn nil, errs.Wrap(http.StatusUnauthorized, err, \"jwk.authorizeToken; error parsing jwk token\")\n\t}\n\n\tvar claims jwtPayload\n\tif err = jwt.Claims(p.Key, &claims); err != nil {\n\t\treturn nil, errs.Wrap(http.StatusUnauthorized, err, \"jwk.authorizeToken; error parsing jwk claims\")\n\t}\n\n\t// According to \"rfc7519 JSON Web Token\" acceptable skew should be no\n\t// more than a few minutes.\n\tif err = claims.ValidateWithLeeway(jose.Expected{\n\t\tIssuer: p.Name,\n\t\tTime:   time.Now().UTC(),\n\t}, time.Minute); err != nil {\n\t\treturn nil, errs.Wrapf(http.StatusUnauthorized, err, \"jwk.authorizeToken; invalid jwk claims\")\n\t}\n\n\t// validate audiences with the defaults\n\tif !matchesAudience(claims.Audience, audiences) {\n\t\treturn nil, errs.Unauthorized(\"jwk.authorizeToken; invalid jwk token audience claim (aud); want %s, but got %s\",\n\t\t\taudiences, claims.Audience)\n\t}\n\n\tif claims.Subject == \"\" {\n\t\treturn nil, errs.Unauthorized(\"jwk.authorizeToken; jwk token subject cannot be empty\")\n\t}\n\n\treturn &claims, nil\n}\n\n// AuthorizeRevoke returns an error if the provisioner does not have rights to\n// revoke the certificate with serial number in the `sub` property.\nfunc (p *JWK) AuthorizeRevoke(_ context.Context, token string) error {\n\t_, err := p.authorizeToken(token, p.ctl.Audiences.Revoke)\n\t// TODO(hs): authorize the SANs using x509 name policy allow/deny rules (also for other provisioners with AuthorizeRevoke)\n\treturn errs.Wrap(http.StatusInternalServerError, err, \"jwk.AuthorizeRevoke\")\n}\n\n// AuthorizeSign validates the given token.\nfunc (p *JWK) AuthorizeSign(ctx context.Context, token string) ([]SignOption, error) {\n\tclaims, err := p.authorizeToken(token, p.ctl.Audiences.Sign)\n\tif err != nil {\n\t\treturn nil, errs.Wrap(http.StatusInternalServerError, err, \"jwk.AuthorizeSign\")\n\t}\n\n\t// NOTE: This is for backwards compatibility with older versions of cli\n\t// and certificates. Older versions added the token subject as the only SAN\n\t// in a CSR by default.\n\tif len(claims.SANs) == 0 {\n\t\tclaims.SANs = []string{claims.Subject}\n\t}\n\n\t// Certificate templates\n\tdata := x509util.CreateTemplateData(claims.Subject, claims.SANs)\n\tif v, err := unsafeParseSigned(token); err == nil {\n\t\tdata.SetToken(v)\n\t}\n\n\ttemplateOptions, err := TemplateOptions(p.Options, data)\n\tif err != nil {\n\t\treturn nil, errs.Wrap(http.StatusInternalServerError, err, \"jwk.AuthorizeSign\")\n\t}\n\n\t// Wrap provisioner if the token is an RA token.\n\tvar self Interface = p\n\tif claims.Step != nil && claims.Step.RA != nil {\n\t\tself = &raProvisioner{\n\t\t\tInterface: p,\n\t\t\traInfo:    claims.Step.RA,\n\t\t}\n\t}\n\n\t// Check the fingerprint of the certificate request if given.\n\tvar fingerprint string\n\tif claims.Confirmation != nil {\n\t\tfingerprint = claims.Confirmation.Fingerprint\n\t}\n\n\treturn []SignOption{\n\t\tself,\n\t\ttemplateOptions,\n\t\t// modifiers / withOptions\n\t\tnewProvisionerExtensionOption(TypeJWK, p.Name, p.Key.KeyID).WithControllerOptions(p.ctl),\n\t\tprofileDefaultDuration(p.ctl.Claimer.DefaultTLSCertDuration()),\n\t\t// validators\n\t\tcsrFingerprintValidator(fingerprint),\n\t\tcommonNameSliceValidator(append([]string{claims.Subject}, claims.SANs...)),\n\t\tdefaultPublicKeyValidator{},\n\t\tnewDefaultSANsValidator(ctx, claims.SANs),\n\t\tnewValidityValidator(p.ctl.Claimer.MinTLSCertDuration(), p.ctl.Claimer.MaxTLSCertDuration()),\n\t\tnewX509NamePolicyValidator(p.ctl.getPolicy().getX509()),\n\t\tp.ctl.newWebhookController(data, linkedca.Webhook_X509),\n\t}, nil\n}\n\n// AuthorizeRenew returns an error if the renewal is disabled.\n// NOTE: This method does not actually validate the certificate or check it's\n// revocation status. Just confirms that the provisioner that created the\n// certificate was configured to allow renewals.\nfunc (p *JWK) AuthorizeRenew(ctx context.Context, cert *x509.Certificate) error {\n\t// TODO(hs): authorize the SANs using x509 name policy allow/deny rules (also for other provisioners with AuthorizeRewew and AuthorizeSSHRenew)\n\treturn p.ctl.AuthorizeRenew(ctx, cert)\n}\n\n// AuthorizeSSHSign returns the list of SignOption for a SignSSH request.\nfunc (p *JWK) AuthorizeSSHSign(_ context.Context, token string) ([]SignOption, error) {\n\tif !p.ctl.Claimer.IsSSHCAEnabled() {\n\t\treturn nil, errs.Unauthorized(\"jwk.AuthorizeSSHSign; sshCA is disabled for jwk provisioner '%s'\", p.GetName())\n\t}\n\tclaims, err := p.authorizeToken(token, p.ctl.Audiences.SSHSign)\n\tif err != nil {\n\t\treturn nil, errs.Wrap(http.StatusInternalServerError, err, \"jwk.AuthorizeSSHSign\")\n\t}\n\tif claims.Step == nil || claims.Step.SSH == nil {\n\t\treturn nil, errs.Unauthorized(\"jwk.AuthorizeSSHSign; jwk token must be an SSH provisioning token\")\n\t}\n\n\topts := claims.Step.SSH\n\tsignOptions := []SignOption{\n\t\t// validates user's SignSSHOptions with the ones in the token\n\t\tsshCertOptionsValidator(*opts),\n\t\t// validate users's KeyID is the token subject.\n\t\tsshCertOptionsValidator(SignSSHOptions{KeyID: claims.Subject}),\n\t}\n\n\t// Default template attributes.\n\tcertType := sshutil.UserCert\n\tkeyID := claims.Subject\n\tprincipals := []string{claims.Subject}\n\n\t// Use options in the token.\n\tif opts.CertType != \"\" {\n\t\tif certType, err = sshutil.CertTypeFromString(opts.CertType); err != nil {\n\t\t\treturn nil, errs.BadRequestErr(err, \"%s\", err.Error())\n\t\t}\n\t}\n\tif opts.KeyID != \"\" {\n\t\tkeyID = opts.KeyID\n\t}\n\tif len(opts.Principals) > 0 {\n\t\tprincipals = opts.Principals\n\t}\n\n\t// Certificate templates.\n\tdata := sshutil.CreateTemplateData(certType, keyID, principals)\n\tif v, err := unsafeParseSigned(token); err == nil {\n\t\tdata.SetToken(v)\n\t}\n\n\ttemplateOptions, err := TemplateSSHOptions(p.Options, data)\n\tif err != nil {\n\t\treturn nil, errs.Wrap(http.StatusInternalServerError, err, \"jwk.AuthorizeSign\")\n\t}\n\tsignOptions = append(signOptions, templateOptions)\n\n\t// Add modifiers from custom claims\n\tt := now()\n\tif !opts.ValidAfter.IsZero() {\n\t\tsignOptions = append(signOptions, sshCertValidAfterModifier(cast.Uint64(opts.ValidAfter.RelativeTime(t).Unix())))\n\t}\n\tif !opts.ValidBefore.IsZero() {\n\t\tsignOptions = append(signOptions, sshCertValidBeforeModifier(cast.Uint64(opts.ValidBefore.RelativeTime(t).Unix())))\n\t}\n\n\treturn append(signOptions,\n\t\tp,\n\t\t// Set the validity bounds if not set.\n\t\t&sshDefaultDuration{p.ctl.Claimer},\n\t\t// Validate public key\n\t\t&sshDefaultPublicKeyValidator{},\n\t\t// Validate the validity period.\n\t\t&sshCertValidityValidator{p.ctl.Claimer},\n\t\t// Require and validate all the default fields in the SSH certificate.\n\t\t&sshCertDefaultValidator{},\n\t\t// Ensure that all principal names are allowed\n\t\tnewSSHNamePolicyValidator(p.ctl.getPolicy().getSSHHost(), p.ctl.getPolicy().getSSHUser()),\n\t\t// Call webhooks\n\t\tp.ctl.newWebhookController(data, linkedca.Webhook_SSH),\n\t), nil\n}\n\n// AuthorizeSSHRevoke returns nil if the token is valid, false otherwise.\nfunc (p *JWK) AuthorizeSSHRevoke(_ context.Context, token string) error {\n\t_, err := p.authorizeToken(token, p.ctl.Audiences.SSHRevoke)\n\t// TODO(hs): authorize the principals using SSH name policy allow/deny rules (also for other provisioners with AuthorizeSSHRevoke)\n\treturn errs.Wrap(http.StatusInternalServerError, err, \"jwk.AuthorizeSSHRevoke\")\n}\n"
  },
  {
    "path": "authority/provisioner/jwk_test.go",
    "content": "package provisioner\n\nimport (\n\t\"context\"\n\t\"crypto\"\n\t\"crypto/rand\"\n\t\"crypto/rsa\"\n\t\"crypto/x509\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"go.step.sm/crypto/fingerprint\"\n\t\"go.step.sm/crypto/jose\"\n\t\"golang.org/x/crypto/ssh\"\n\n\t\"github.com/smallstep/assert\"\n\t\"github.com/smallstep/certificates/api/render\"\n)\n\nfunc TestJWK_Getters(t *testing.T) {\n\tp, err := generateJWK()\n\tassert.FatalError(t, err)\n\tif got := p.GetID(); got != p.Name+\":\"+p.Key.KeyID {\n\t\tt.Errorf(\"JWK.GetID() = %v, want %v:%v\", got, p.Name, p.Key.KeyID)\n\t}\n\tif got := p.GetName(); got != p.Name {\n\t\tt.Errorf(\"JWK.GetName() = %v, want %v\", got, p.Name)\n\t}\n\tif got := p.GetType(); got != TypeJWK {\n\t\tt.Errorf(\"JWK.GetType() = %v, want %v\", got, TypeJWK)\n\t}\n\tkid, key, ok := p.GetEncryptedKey()\n\tif kid != p.Key.KeyID || key != p.EncryptedKey || ok == false {\n\t\tt.Errorf(\"JWK.GetEncryptedKey() = (%v, %v, %v), want (%v, %v, %v)\",\n\t\t\tkid, key, ok, p.Key.KeyID, p.EncryptedKey, true)\n\t}\n\tp.EncryptedKey = \"\"\n\tkid, key, ok = p.GetEncryptedKey()\n\tif kid != p.Key.KeyID || key != \"\" || ok == true {\n\t\tt.Errorf(\"JWK.GetEncryptedKey() = (%v, %v, %v), want (%v, %v, %v)\",\n\t\t\tkid, key, ok, p.Key.KeyID, \"\", false)\n\t}\n}\n\nfunc TestJWK_Init(t *testing.T) {\n\ttype ProvisionerValidateTest struct {\n\t\tp   *JWK\n\t\terr error\n\t}\n\ttests := map[string]func(*testing.T) ProvisionerValidateTest{\n\t\t\"fail-empty\": func(t *testing.T) ProvisionerValidateTest {\n\t\t\treturn ProvisionerValidateTest{\n\t\t\t\tp:   &JWK{},\n\t\t\t\terr: errors.New(\"provisioner type cannot be empty\"),\n\t\t\t}\n\t\t},\n\t\t\"fail-empty-name\": func(t *testing.T) ProvisionerValidateTest {\n\t\t\treturn ProvisionerValidateTest{\n\t\t\t\tp: &JWK{\n\t\t\t\t\tType: \"JWK\",\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"provisioner name cannot be empty\"),\n\t\t\t}\n\t\t},\n\t\t\"fail-empty-type\": func(t *testing.T) ProvisionerValidateTest {\n\t\t\treturn ProvisionerValidateTest{\n\t\t\t\tp:   &JWK{Name: \"foo\"},\n\t\t\t\terr: errors.New(\"provisioner type cannot be empty\"),\n\t\t\t}\n\t\t},\n\t\t\"fail-empty-key\": func(t *testing.T) ProvisionerValidateTest {\n\t\t\treturn ProvisionerValidateTest{\n\t\t\t\tp:   &JWK{Name: \"foo\", Type: \"bar\"},\n\t\t\t\terr: errors.New(\"provisioner key cannot be empty\"),\n\t\t\t}\n\t\t},\n\t\t\"fail-bad-claims\": func(t *testing.T) ProvisionerValidateTest {\n\t\t\treturn ProvisionerValidateTest{\n\t\t\t\tp:   &JWK{Name: \"foo\", Type: \"bar\", Key: &jose.JSONWebKey{}, Claims: &Claims{DefaultTLSDur: &Duration{0}}},\n\t\t\t\terr: errors.New(\"claims: MinTLSCertDuration must be greater than 0\"),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) ProvisionerValidateTest {\n\t\t\treturn ProvisionerValidateTest{\n\t\t\t\tp: &JWK{Name: \"foo\", Type: \"bar\", Key: &jose.JSONWebKey{}},\n\t\t\t}\n\t\t},\n\t}\n\n\tconfig := Config{\n\t\tClaims:    globalProvisionerClaims,\n\t\tAudiences: testAudiences,\n\t}\n\tfor name, get := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ttc := get(t)\n\t\t\terr := tc.p.Init(config)\n\t\t\tif err != nil {\n\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\tassert.Equals(t, tc.err.Error(), err.Error())\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tassert.Nil(t, tc.err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestJWK_authorizeToken(t *testing.T) {\n\tp1, err := generateJWK()\n\tassert.FatalError(t, err)\n\tp2, err := generateJWK()\n\tassert.FatalError(t, err)\n\n\tkey1, err := decryptJSONWebKey(p1.EncryptedKey)\n\tassert.FatalError(t, err)\n\tkey2, err := decryptJSONWebKey(p2.EncryptedKey)\n\tassert.FatalError(t, err)\n\n\tt1, err := generateSimpleToken(p1.Name, testAudiences.Sign[0], key1)\n\tassert.FatalError(t, err)\n\tt2, err := generateSimpleToken(p2.Name, testAudiences.Sign[1], key2)\n\tassert.FatalError(t, err)\n\tt3, err := generateToken(\"test.smallstep.com\", p1.Name, testAudiences.Sign[0], \"\", []string{}, time.Now(), key1)\n\tassert.FatalError(t, err)\n\n\t// Invalid tokens\n\tparts := strings.Split(t1, \".\")\n\tkey3, err := generateJSONWebKey()\n\tassert.FatalError(t, err)\n\t// missing key\n\tfailKey, err := generateSimpleToken(p1.Name, testAudiences.Sign[0], key3)\n\tassert.FatalError(t, err)\n\t// invalid token\n\tfailTok := \"foo.\" + parts[1] + \".\" + parts[2]\n\t// invalid claims\n\tfailClaims := parts[0] + \".foo.\" + parts[1]\n\t// invalid issuer\n\tfailIss, err := generateSimpleToken(\"foobar\", testAudiences.Sign[0], key1)\n\tassert.FatalError(t, err)\n\t// invalid audience\n\tfailAud, err := generateSimpleToken(p1.Name, \"foobar\", key1)\n\tassert.FatalError(t, err)\n\t// invalid signature\n\tfailSig := t1[0 : len(t1)-2]\n\t// no subject\n\tfailSub, err := generateToken(\"\", p1.Name, testAudiences.Sign[0], \"\", []string{\"test.smallstep.com\"}, time.Now(), key1)\n\tassert.FatalError(t, err)\n\t// expired\n\tfailExp, err := generateToken(\"subject\", p1.Name, testAudiences.Sign[0], \"\", []string{\"test.smallstep.com\"}, time.Now().Add(-360*time.Second), key1)\n\tassert.FatalError(t, err)\n\t// not before\n\tfailNbf, err := generateToken(\"subject\", p1.Name, testAudiences.Sign[0], \"\", []string{\"test.smallstep.com\"}, time.Now().Add(360*time.Second), key1)\n\tassert.FatalError(t, err)\n\n\t// Remove encrypted key for p2\n\tp2.EncryptedKey = \"\"\n\n\ttype args struct {\n\t\ttoken string\n\t}\n\ttests := []struct {\n\t\tname string\n\t\tprov *JWK\n\t\targs args\n\t\tcode int\n\t\terr  error\n\t}{\n\t\t{\"fail-token\", p1, args{failTok}, http.StatusUnauthorized, errors.New(\"jwk.authorizeToken; error parsing jwk token\")},\n\t\t{\"fail-key\", p1, args{failKey}, http.StatusUnauthorized, errors.New(\"jwk.authorizeToken; error parsing jwk claims\")},\n\t\t{\"fail-claims\", p1, args{failClaims}, http.StatusUnauthorized, errors.New(\"jwk.authorizeToken; error parsing jwk claims\")},\n\t\t{\"fail-signature\", p1, args{failSig}, http.StatusUnauthorized, errors.New(\"jwk.authorizeToken; error parsing jwk claims: go-jose/go-jose: error in cryptographic primitive\")},\n\t\t{\"fail-issuer\", p1, args{failIss}, http.StatusUnauthorized, errors.New(\"jwk.authorizeToken; invalid jwk claims: go-jose/go-jose/jwt: validation failed, invalid issuer claim (iss)\")},\n\t\t{\"fail-expired\", p1, args{failExp}, http.StatusUnauthorized, errors.New(\"jwk.authorizeToken; invalid jwk claims: go-jose/go-jose/jwt: validation failed, token is expired (exp)\")},\n\t\t{\"fail-not-before\", p1, args{failNbf}, http.StatusUnauthorized, errors.New(\"jwk.authorizeToken; invalid jwk claims: go-jose/go-jose/jwt: validation failed, token not valid yet (nbf)\")},\n\t\t{\"fail-audience\", p1, args{failAud}, http.StatusUnauthorized, errors.New(\"jwk.authorizeToken; invalid jwk token audience claim (aud)\")},\n\t\t{\"fail-subject\", p1, args{failSub}, http.StatusUnauthorized, errors.New(\"jwk.authorizeToken; jwk token subject cannot be empty\")},\n\t\t{\"ok\", p1, args{t1}, http.StatusOK, nil},\n\t\t{\"ok-no-encrypted-key\", p2, args{t2}, http.StatusOK, nil},\n\t\t{\"ok-no-sans\", p1, args{t3}, http.StatusOK, nil},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif got, err := tt.prov.authorizeToken(tt.args.token, testAudiences.Sign); err != nil {\n\t\t\t\tif assert.NotNil(t, tt.err) {\n\t\t\t\t\tvar sc render.StatusCodedError\n\t\t\t\t\tassert.Fatal(t, errors.As(err, &sc), \"error does not implement StatusCodedError interface\")\n\t\t\t\t\tassert.Equals(t, sc.StatusCode(), tt.code)\n\t\t\t\t\tassert.HasPrefix(t, err.Error(), tt.err.Error())\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tassert.Nil(t, tt.err)\n\t\t\t\tassert.NotNil(t, got)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestJWK_AuthorizeRevoke(t *testing.T) {\n\tp1, err := generateJWK()\n\tassert.FatalError(t, err)\n\tkey1, err := decryptJSONWebKey(p1.EncryptedKey)\n\tassert.FatalError(t, err)\n\tt1, err := generateSimpleToken(p1.Name, testAudiences.Revoke[0], key1)\n\tassert.FatalError(t, err)\n\t// invalid signature\n\tfailSig := t1[0 : len(t1)-2]\n\n\ttype args struct {\n\t\ttoken string\n\t}\n\ttests := []struct {\n\t\tname string\n\t\tprov *JWK\n\t\targs args\n\t\tcode int\n\t\terr  error\n\t}{\n\t\t{\"fail-signature\", p1, args{failSig}, http.StatusUnauthorized, errors.New(\"jwk.AuthorizeRevoke: jwk.authorizeToken; error parsing jwk claims: go-jose/go-jose: error in cryptographic primitive\")},\n\t\t{\"ok\", p1, args{t1}, http.StatusOK, nil},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif err := tt.prov.AuthorizeRevoke(context.Background(), tt.args.token); err != nil {\n\t\t\t\tif assert.NotNil(t, tt.err) {\n\t\t\t\t\tvar sc render.StatusCodedError\n\t\t\t\t\tassert.Fatal(t, errors.As(err, &sc), \"error does not implement StatusCodedError interface\")\n\t\t\t\t\tassert.Equals(t, sc.StatusCode(), tt.code)\n\t\t\t\t\tassert.HasPrefix(t, err.Error(), tt.err.Error())\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestJWK_AuthorizeSign(t *testing.T) {\n\tp1, err := generateJWK()\n\tassert.FatalError(t, err)\n\tkey1, err := decryptJSONWebKey(p1.EncryptedKey)\n\tassert.FatalError(t, err)\n\n\tt1, err := generateToken(\"subject\", p1.Name, testAudiences.Sign[0], \"name@smallstep.com\", []string{\"127.0.0.1\", \"max@smallstep.com\", \"foo\"}, time.Now(), key1)\n\tassert.FatalError(t, err)\n\n\tt2, err := generateToken(\"subject\", p1.Name, testAudiences.Sign[0], \"name@smallstep.com\", []string{}, time.Now(), key1)\n\tassert.FatalError(t, err)\n\n\tt3, err := generateCustomToken(\"subject\", p1.Name, testAudiences.Sign[0], key1, nil, map[string]any{\"cnf\": map[string]any{\"x5rt#S256\": \"fingerprint\"}})\n\tassert.FatalError(t, err)\n\n\t// invalid signature\n\tfailSig := t1[0 : len(t1)-2]\n\n\ttype args struct {\n\t\ttoken string\n\t}\n\ttests := []struct {\n\t\tname        string\n\t\tprov        *JWK\n\t\targs        args\n\t\tcode        int\n\t\terr         error\n\t\tsans        []string\n\t\tfingerprint string\n\t}{\n\t\t{\n\t\t\tname: \"fail-signature\",\n\t\t\tprov: p1,\n\t\t\targs: args{failSig},\n\t\t\tcode: http.StatusUnauthorized,\n\t\t\terr:  errors.New(\"jwk.AuthorizeSign: jwk.authorizeToken; error parsing jwk claims: go-jose/go-jose: error in cryptographic primitive\"),\n\t\t},\n\t\t{\n\t\t\tname: \"ok-sans\",\n\t\t\tprov: p1,\n\t\t\targs: args{t1},\n\t\t\tcode: http.StatusOK,\n\t\t\terr:  nil,\n\t\t\tsans: []string{\"127.0.0.1\", \"max@smallstep.com\", \"foo\"},\n\t\t},\n\t\t{\n\t\t\tname: \"ok-no-sans\",\n\t\t\tprov: p1,\n\t\t\targs: args{t2},\n\t\t\tcode: http.StatusOK,\n\t\t\terr:  nil,\n\t\t\tsans: []string{\"subject\"},\n\t\t},\n\t\t{\n\t\t\tname:        \"ok-cnf\",\n\t\t\tprov:        p1,\n\t\t\targs:        args{t3},\n\t\t\tcode:        http.StatusOK,\n\t\t\terr:         nil,\n\t\t\tsans:        []string{\"subject\"},\n\t\t\tfingerprint: \"fingerprint\",\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tctx := NewContextWithMethod(context.Background(), SignMethod)\n\t\t\tif got, err := tt.prov.AuthorizeSign(ctx, tt.args.token); err != nil {\n\t\t\t\tif assert.NotNil(t, tt.err) {\n\t\t\t\t\tvar sc render.StatusCodedError\n\t\t\t\t\tassert.Fatal(t, errors.As(err, &sc), \"error does not implement StatusCodedError interface\")\n\t\t\t\t\tassert.Equals(t, sc.StatusCode(), tt.code)\n\t\t\t\t\tassert.HasPrefix(t, err.Error(), tt.err.Error())\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif assert.NotNil(t, got) {\n\t\t\t\t\tassert.Equals(t, 11, len(got))\n\t\t\t\t\tfor _, o := range got {\n\t\t\t\t\t\tswitch v := o.(type) {\n\t\t\t\t\t\tcase *JWK:\n\t\t\t\t\t\tcase certificateOptionsFunc:\n\t\t\t\t\t\tcase *provisionerExtensionOption:\n\t\t\t\t\t\t\tassert.Equals(t, v.Type, TypeJWK)\n\t\t\t\t\t\t\tassert.Equals(t, v.Name, tt.prov.GetName())\n\t\t\t\t\t\t\tassert.Equals(t, v.CredentialID, tt.prov.Key.KeyID)\n\t\t\t\t\t\t\tassert.Len(t, 0, v.KeyValuePairs)\n\t\t\t\t\t\tcase profileDefaultDuration:\n\t\t\t\t\t\t\tassert.Equals(t, time.Duration(v), tt.prov.ctl.Claimer.DefaultTLSCertDuration())\n\t\t\t\t\t\tcase commonNameSliceValidator:\n\t\t\t\t\t\t\tassert.Equals(t, []string(v), append([]string{\"subject\"}, tt.sans...))\n\t\t\t\t\t\tcase defaultPublicKeyValidator:\n\t\t\t\t\t\tcase *validityValidator:\n\t\t\t\t\t\t\tassert.Equals(t, v.min, tt.prov.ctl.Claimer.MinTLSCertDuration())\n\t\t\t\t\t\t\tassert.Equals(t, v.max, tt.prov.ctl.Claimer.MaxTLSCertDuration())\n\t\t\t\t\t\tcase *defaultSANsValidator:\n\t\t\t\t\t\t\tassert.Equals(t, v.sans, tt.sans)\n\t\t\t\t\t\t\tassert.Equals(t, MethodFromContext(v.ctx), SignMethod)\n\t\t\t\t\t\tcase *x509NamePolicyValidator:\n\t\t\t\t\t\t\tassert.Equals(t, nil, v.policyEngine)\n\t\t\t\t\t\tcase *WebhookController:\n\t\t\t\t\t\tcase csrFingerprintValidator:\n\t\t\t\t\t\t\tassert.Equals(t, tt.fingerprint, string(v))\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\tassert.FatalError(t, fmt.Errorf(\"unexpected sign option of type %T\", v))\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestJWK_AuthorizeRenew(t *testing.T) {\n\tnow := time.Now().Truncate(time.Second)\n\tp1, err := generateJWK()\n\tassert.FatalError(t, err)\n\tp2, err := generateJWK()\n\tassert.FatalError(t, err)\n\n\t// disable renewal\n\tdisable := true\n\tp2.Claims = &Claims{DisableRenewal: &disable}\n\tp2.ctl.Claimer, err = NewClaimer(p2.Claims, globalProvisionerClaims)\n\tassert.FatalError(t, err)\n\n\ttype args struct {\n\t\tcert *x509.Certificate\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tprov    *JWK\n\t\targs    args\n\t\tcode    int\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", p1, args{&x509.Certificate{\n\t\t\tNotBefore: now,\n\t\t\tNotAfter:  now.Add(time.Hour),\n\t\t}}, http.StatusOK, false},\n\t\t{\"fail/renew-disabled\", p2, args{&x509.Certificate{\n\t\t\tNotBefore: now,\n\t\t\tNotAfter:  now.Add(time.Hour),\n\t\t}}, http.StatusUnauthorized, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif err := tt.prov.AuthorizeRenew(context.Background(), tt.args.cert); (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"JWK.AuthorizeRenew() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t} else if err != nil {\n\t\t\t\tvar sc render.StatusCodedError\n\t\t\t\tassert.Fatal(t, errors.As(err, &sc), \"error does not implement StatusCodedError interface\")\n\t\t\t\tassert.Equals(t, sc.StatusCode(), tt.code)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestJWK_AuthorizeSSHSign(t *testing.T) {\n\ttm, fn := mockNow()\n\tdefer fn()\n\n\tp1, err := generateJWK()\n\tassert.FatalError(t, err)\n\tp2, err := generateJWK()\n\tassert.FatalError(t, err)\n\t// disable sshCA\n\tdisable := false\n\tp2.Claims = &Claims{EnableSSHCA: &disable}\n\tp2.ctl.Claimer, err = NewClaimer(p2.Claims, globalProvisionerClaims)\n\tassert.FatalError(t, err)\n\n\tjwk, err := decryptJSONWebKey(p1.EncryptedKey)\n\tassert.FatalError(t, err)\n\n\tkey, err := generateJSONWebKey()\n\tassert.FatalError(t, err)\n\n\tsigner, err := generateJSONWebKey()\n\tassert.FatalError(t, err)\n\n\tpub := key.Public().Key\n\trsa2048, err := rsa.GenerateKey(rand.Reader, 2048)\n\tassert.FatalError(t, err)\n\t//nolint:gosec // tests minimum size of the key\n\trsa1024, err := rsa.GenerateKey(rand.Reader, 1024)\n\tassert.FatalError(t, err)\n\n\t// Calculate fingerprint\n\tsshPub, err := ssh.NewPublicKey(pub)\n\tassert.FatalError(t, err)\n\tfp, err := fingerprint.New(sshPub.Marshal(), crypto.SHA256, fingerprint.Base64RawURLFingerprint)\n\tassert.FatalError(t, err)\n\n\tiss, aud := p1.Name, testAudiences.SSHSign[0]\n\n\tt1, err := generateSimpleSSHUserToken(iss, aud, jwk)\n\tassert.FatalError(t, err)\n\n\tt2, err := generateSimpleSSHHostToken(iss, aud, jwk)\n\tassert.FatalError(t, err)\n\n\tt3, err := generateCustomToken(\"sub\", iss, aud, jwk, nil, map[string]any{\n\t\t\"step\": map[string]any{\n\t\t\t\"ssh\": map[string]any{\"certType\": \"host\", \"principals\": []string{\"smallstep.com\"}},\n\t\t},\n\t\t\"cnf\": map[string]any{\"kid\": fp},\n\t})\n\tassert.FatalError(t, err)\n\n\tt4, err := generateCustomToken(\"sub\", iss, aud, jwk, nil, map[string]any{\n\t\t\"step\": map[string]any{\n\t\t\t\"ssh\": map[string]any{\"certType\": \"host\", \"principals\": []string{\"smallstep.com\"}},\n\t\t},\n\t\t\"cnf\": map[string]any{\"kid\": \"bad-fingerprint\"},\n\t})\n\tassert.FatalError(t, err)\n\n\t// invalid signature\n\tfailSig := t1[0 : len(t1)-2]\n\n\tuserDuration := p1.ctl.Claimer.DefaultUserSSHCertDuration()\n\thostDuration := p1.ctl.Claimer.DefaultHostSSHCertDuration()\n\texpectedUserOptions := &SignSSHOptions{\n\t\tCertType: \"user\", Principals: []string{\"name\"},\n\t\tValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(userDuration)),\n\t}\n\texpectedHostOptions := &SignSSHOptions{\n\t\tCertType: \"host\", Principals: []string{\"smallstep.com\"},\n\t\tValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(hostDuration)),\n\t}\n\n\ttype args struct {\n\t\ttoken   string\n\t\tsshOpts SignSSHOptions\n\t\tkey     interface{}\n\t}\n\ttests := []struct {\n\t\tname        string\n\t\tprov        *JWK\n\t\targs        args\n\t\texpected    *SignSSHOptions\n\t\tcode        int\n\t\twantErr     bool\n\t\twantSignErr bool\n\t}{\n\t\t{\"user\", p1, args{t1, SignSSHOptions{}, pub}, expectedUserOptions, http.StatusOK, false, false},\n\t\t{\"user-rsa2048\", p1, args{t1, SignSSHOptions{}, rsa2048.Public()}, expectedUserOptions, http.StatusOK, false, false},\n\t\t{\"user-type\", p1, args{t1, SignSSHOptions{CertType: \"user\"}, pub}, expectedUserOptions, http.StatusOK, false, false},\n\t\t{\"user-principals\", p1, args{t1, SignSSHOptions{Principals: []string{\"name\"}}, pub}, expectedUserOptions, http.StatusOK, false, false},\n\t\t{\"user-options\", p1, args{t1, SignSSHOptions{CertType: \"user\", Principals: []string{\"name\"}}, pub}, expectedUserOptions, http.StatusOK, false, false},\n\t\t{\"host\", p1, args{t2, SignSSHOptions{}, pub}, expectedHostOptions, http.StatusOK, false, false},\n\t\t{\"host-type\", p1, args{t2, SignSSHOptions{CertType: \"host\"}, pub}, expectedHostOptions, http.StatusOK, false, false},\n\t\t{\"host-principals\", p1, args{t2, SignSSHOptions{Principals: []string{\"smallstep.com\"}}, pub}, expectedHostOptions, http.StatusOK, false, false},\n\t\t{\"host-options\", p1, args{t2, SignSSHOptions{CertType: \"host\", Principals: []string{\"smallstep.com\"}}, pub}, expectedHostOptions, http.StatusOK, false, false},\n\t\t{\"host-cnf\", p1, args{t3, SignSSHOptions{CertType: \"host\", Principals: []string{\"smallstep.com\"}}, pub}, expectedHostOptions, http.StatusOK, false, false},\n\t\t{\"ignore-bad-cnf\", p1, args{t4, SignSSHOptions{CertType: \"host\", Principals: []string{\"smallstep.com\"}}, pub}, expectedHostOptions, http.StatusOK, false, false},\n\t\t{\"fail-sshCA-disabled\", p2, args{\"foo\", SignSSHOptions{}, pub}, expectedUserOptions, http.StatusUnauthorized, true, false},\n\t\t{\"fail-signature\", p1, args{failSig, SignSSHOptions{}, pub}, nil, http.StatusUnauthorized, true, false},\n\t\t{\"fail-rsa1024\", p1, args{t1, SignSSHOptions{}, rsa1024.Public()}, expectedUserOptions, http.StatusOK, false, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := tt.prov.AuthorizeSSHSign(context.Background(), tt.args.token)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"JWK.AuthorizeSSHSign() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif err != nil {\n\t\t\t\tvar sc render.StatusCodedError\n\t\t\t\tassert.Fatal(t, errors.As(err, &sc), \"error does not implement StatusCodedError interface\")\n\t\t\t\tassert.Equals(t, sc.StatusCode(), tt.code)\n\t\t\t\tassert.Nil(t, got)\n\t\t\t} else if assert.NotNil(t, got) {\n\t\t\t\tcert, err := signSSHCertificate(tt.args.key, tt.args.sshOpts, got, signer.Key.(crypto.Signer))\n\t\t\t\tif (err != nil) != tt.wantSignErr {\n\t\t\t\t\tt.Errorf(\"SignSSH error = %v, wantSignErr %v\", err, tt.wantSignErr)\n\t\t\t\t} else {\n\t\t\t\t\tif tt.wantSignErr {\n\t\t\t\t\t\tassert.Nil(t, cert)\n\t\t\t\t\t} else {\n\t\t\t\t\t\tassert.NoError(t, validateSSHCertificate(cert, tt.expected))\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestJWK_AuthorizeSign_SSHOptions(t *testing.T) {\n\ttm, fn := mockNow()\n\tdefer fn()\n\n\tp1, err := generateJWK()\n\tassert.FatalError(t, err)\n\tjwk, err := decryptJSONWebKey(p1.EncryptedKey)\n\tassert.FatalError(t, err)\n\n\tsub, iss, aud, iat := \"subject@smallstep.com\", p1.Name, testAudiences.SSHSign[0], time.Now()\n\n\tkey, err := generateJSONWebKey()\n\tassert.FatalError(t, err)\n\n\tsigner, err := generateJSONWebKey()\n\tassert.FatalError(t, err)\n\n\tuserDuration := p1.ctl.Claimer.DefaultUserSSHCertDuration()\n\thostDuration := p1.ctl.Claimer.DefaultHostSSHCertDuration()\n\texpectedUserOptions := &SignSSHOptions{\n\t\tCertType: \"user\", Principals: []string{\"name\"},\n\t\tValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(userDuration)),\n\t}\n\texpectedHostOptions := &SignSSHOptions{\n\t\tCertType: \"host\", Principals: []string{\"smallstep.com\"},\n\t\tValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(hostDuration)),\n\t}\n\ttype args struct {\n\t\tsub, iss, aud string\n\t\tiat           time.Time\n\t\ttokSSHOpts    *SignSSHOptions\n\t\tuserSSHOpts   *SignSSHOptions\n\t\tjwk           *jose.JSONWebKey\n\t}\n\ttests := []struct {\n\t\tname        string\n\t\tprov        *JWK\n\t\targs        args\n\t\texpected    *SignSSHOptions\n\t\twantErr     bool\n\t\twantSignErr bool\n\t}{\n\t\t{\"ok-user\", p1, args{sub, iss, aud, iat, &SignSSHOptions{CertType: \"user\", Principals: []string{\"name\"}}, &SignSSHOptions{}, jwk}, expectedUserOptions, false, false},\n\t\t{\"ok-host\", p1, args{sub, iss, aud, iat, &SignSSHOptions{CertType: \"host\", Principals: []string{\"smallstep.com\"}}, &SignSSHOptions{}, jwk}, expectedHostOptions, false, false},\n\t\t{\"ok-user-validAfter\", p1, args{sub, iss, aud, iat, &SignSSHOptions{\n\t\t\tCertType: \"user\", Principals: []string{\"name\"},\n\t\t}, &SignSSHOptions{\n\t\t\tValidAfter: NewTimeDuration(tm.Add(-time.Hour)),\n\t\t}, jwk}, &SignSSHOptions{\n\t\t\tCertType: \"user\", Principals: []string{\"name\"}, ValidAfter: NewTimeDuration(tm.Add(-time.Hour)), ValidBefore: NewTimeDuration(tm.Add(userDuration - time.Hour)),\n\t\t}, false, false},\n\t\t{\"ok-user-validBefore\", p1, args{sub, iss, aud, iat, &SignSSHOptions{\n\t\t\tCertType: \"user\", Principals: []string{\"name\"},\n\t\t}, &SignSSHOptions{\n\t\t\tValidBefore: NewTimeDuration(tm.Add(time.Hour)),\n\t\t}, jwk}, &SignSSHOptions{\n\t\t\tCertType: \"user\", Principals: []string{\"name\"}, ValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(time.Hour)),\n\t\t}, false, false},\n\t\t{\"ok-user-validAfter-validBefore\", p1, args{sub, iss, aud, iat, &SignSSHOptions{\n\t\t\tCertType: \"user\", Principals: []string{\"name\"},\n\t\t}, &SignSSHOptions{\n\t\t\tValidAfter: NewTimeDuration(tm.Add(10 * time.Minute)), ValidBefore: NewTimeDuration(tm.Add(time.Hour)),\n\t\t}, jwk}, &SignSSHOptions{\n\t\t\tCertType: \"user\", Principals: []string{\"name\"}, ValidAfter: NewTimeDuration(tm.Add(10 * time.Minute)), ValidBefore: NewTimeDuration(tm.Add(time.Hour)),\n\t\t}, false, false},\n\t\t{\"ok-user-match\", p1, args{sub, iss, aud, iat, &SignSSHOptions{\n\t\t\tCertType: \"user\", Principals: []string{\"name\"}, ValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(1 * time.Hour)),\n\t\t}, &SignSSHOptions{\n\t\t\tCertType: \"user\", Principals: []string{\"name\"}, ValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(1 * time.Hour)),\n\t\t}, jwk}, &SignSSHOptions{\n\t\t\tCertType: \"user\", Principals: []string{\"name\"}, ValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(time.Hour)),\n\t\t}, false, false},\n\t\t{\"fail-certType\", p1, args{sub, iss, aud, iat, &SignSSHOptions{CertType: \"user\", Principals: []string{\"name\"}}, &SignSSHOptions{CertType: \"host\"}, jwk}, nil, false, true},\n\t\t{\"fail-principals\", p1, args{sub, iss, aud, iat, &SignSSHOptions{CertType: \"user\", Principals: []string{\"name\"}}, &SignSSHOptions{Principals: []string{\"root\"}}, jwk}, nil, false, true},\n\t\t{\"fail-validAfter\", p1, args{sub, iss, aud, iat, &SignSSHOptions{CertType: \"user\", Principals: []string{\"name\"}, ValidAfter: NewTimeDuration(tm)}, &SignSSHOptions{ValidAfter: NewTimeDuration(tm.Add(time.Hour))}, jwk}, nil, false, true},\n\t\t{\"fail-validBefore\", p1, args{sub, iss, aud, iat, &SignSSHOptions{CertType: \"user\", Principals: []string{\"name\"}, ValidBefore: NewTimeDuration(tm.Add(time.Hour))}, &SignSSHOptions{ValidBefore: NewTimeDuration(tm.Add(10 * time.Hour))}, jwk}, nil, false, true},\n\t\t{\"fail-subject\", p1, args{\"\", iss, aud, iat, &SignSSHOptions{CertType: \"user\", Principals: []string{\"name\"}}, &SignSSHOptions{}, jwk}, nil, true, false},\n\t\t{\"fail-issuer\", p1, args{sub, \"invalid\", aud, iat, &SignSSHOptions{CertType: \"user\", Principals: []string{\"name\"}}, &SignSSHOptions{}, jwk}, nil, true, false},\n\t\t{\"fail-audience\", p1, args{sub, iss, \"invalid\", iat, &SignSSHOptions{CertType: \"user\", Principals: []string{\"name\"}}, &SignSSHOptions{}, jwk}, nil, true, false},\n\t\t{\"fail-expired\", p1, args{sub, iss, aud, iat.Add(-6 * time.Minute), &SignSSHOptions{CertType: \"user\", Principals: []string{\"name\"}}, &SignSSHOptions{}, jwk}, nil, true, false},\n\t\t{\"fail-notBefore\", p1, args{sub, iss, aud, iat.Add(5 * time.Minute), &SignSSHOptions{CertType: \"user\", Principals: []string{\"name\"}}, &SignSSHOptions{}, jwk}, nil, true, false},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ttoken, err := generateSSHToken(tt.args.sub, tt.args.iss, tt.args.aud, tt.args.iat, tt.args.tokSSHOpts, tt.args.jwk)\n\t\t\tassert.FatalError(t, err)\n\t\t\tif got, err := tt.prov.AuthorizeSSHSign(context.Background(), token); (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"JWK.AuthorizeSSHSign() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t} else if !tt.wantErr && assert.NotNil(t, got) {\n\t\t\t\tvar opts SignSSHOptions\n\t\t\t\tif tt.args.userSSHOpts != nil {\n\t\t\t\t\topts = *tt.args.userSSHOpts\n\t\t\t\t}\n\t\t\t\tcert, err := signSSHCertificate(key.Public().Key, opts, got, signer.Key.(crypto.Signer))\n\t\t\t\tif (err != nil) != tt.wantSignErr {\n\t\t\t\t\tt.Errorf(\"SignSSH error = %v, wantSignErr %v\", err, tt.wantSignErr)\n\t\t\t\t} else {\n\t\t\t\t\tif tt.wantSignErr {\n\t\t\t\t\t\tassert.Nil(t, cert)\n\t\t\t\t\t} else {\n\t\t\t\t\t\tassert.NoError(t, validateSSHCertificate(cert, tt.expected))\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestJWK_AuthorizeSSHRevoke(t *testing.T) {\n\ttype test struct {\n\t\tp     *JWK\n\t\ttoken string\n\t\tcode  int\n\t\terr   error\n\t}\n\ttests := map[string]func(*testing.T) test{\n\t\t\"fail/invalid-token\": func(t *testing.T) test {\n\t\t\tp, err := generateJWK()\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tp:     p,\n\t\t\t\ttoken: \"foo\",\n\t\t\t\tcode:  http.StatusUnauthorized,\n\t\t\t\terr:   errors.New(\"jwk.AuthorizeSSHRevoke: jwk.authorizeToken; error parsing jwk token\"),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tp, err := generateJWK()\n\t\t\tassert.FatalError(t, err)\n\t\t\tjwk, err := decryptJSONWebKey(p.EncryptedKey)\n\t\t\tassert.FatalError(t, err)\n\n\t\t\ttok, err := generateToken(\"subject\", p.Name, testAudiences.SSHRevoke[0], \"name@smallstep.com\", []string{\"127.0.0.1\", \"max@smallstep.com\", \"foo\"}, time.Now(), jwk)\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tp:     p,\n\t\t\t\ttoken: tok,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, tt := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ttc := tt(t)\n\t\t\tif err := tc.p.AuthorizeSSHRevoke(context.Background(), tc.token); err != nil {\n\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\tvar sc render.StatusCodedError\n\t\t\t\t\tassert.Fatal(t, errors.As(err, &sc), \"error does not implement StatusCodedError interface\")\n\t\t\t\t\tassert.Equals(t, sc.StatusCode(), tc.code)\n\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.err.Error())\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tassert.Nil(t, tc.err)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "authority/provisioner/k8sSA.go",
    "content": "package provisioner\n\nimport (\n\t\"context\"\n\t\"crypto/ecdsa\"\n\t\"crypto/ed25519\"\n\t\"crypto/rsa\"\n\t\"crypto/x509\"\n\t\"encoding/pem\"\n\t\"net/http\"\n\n\t\"github.com/pkg/errors\"\n\n\t\"github.com/smallstep/linkedca\"\n\t\"go.step.sm/crypto/jose\"\n\t\"go.step.sm/crypto/pemutil\"\n\t\"go.step.sm/crypto/sshutil\"\n\t\"go.step.sm/crypto/x509util\"\n\n\t\"github.com/smallstep/certificates/errs\"\n)\n\n// NOTE: There can be at most one kubernetes service account provisioner configured\n// per instance of step-ca. This is due to a lack of distinguishing information\n// contained in kubernetes service account tokens.\n\nconst (\n\t// K8sSAName is the default name used for kubernetes service account provisioners.\n\tK8sSAName = \"k8sSA-default\"\n\t// K8sSAID is the default ID for kubernetes service account provisioners.\n\tK8sSAID     = \"k8ssa/\" + K8sSAName\n\tk8sSAIssuer = \"kubernetes/serviceaccount\"\n)\n\n// jwtPayload extends jwt.Claims with step attributes.\ntype k8sSAPayload struct {\n\tjose.Claims\n\tNamespace          string `json:\"kubernetes.io/serviceaccount/namespace,omitempty\"`\n\tSecretName         string `json:\"kubernetes.io/serviceaccount/secret.name,omitempty\"`\n\tServiceAccountName string `json:\"kubernetes.io/serviceaccount/service-account.name,omitempty\"`\n\tServiceAccountUID  string `json:\"kubernetes.io/serviceaccount/service-account.uid,omitempty\"`\n}\n\n// K8sSA represents a Kubernetes ServiceAccount provisioner; an\n// entity trusted to make signature requests.\ntype K8sSA struct {\n\t*base\n\tID      string   `json:\"-\"`\n\tType    string   `json:\"type\"`\n\tName    string   `json:\"name\"`\n\tPubKeys []byte   `json:\"publicKeys,omitempty\"`\n\tClaims  *Claims  `json:\"claims,omitempty\"`\n\tOptions *Options `json:\"options,omitempty\"`\n\t//kauthn    kauthn.AuthenticationV1Interface\n\tpubKeys []interface{}\n\tctl     *Controller\n}\n\n// GetID returns the provisioner unique identifier. The name and credential id\n// should uniquely identify any K8sSA provisioner.\nfunc (p *K8sSA) GetID() string {\n\tif p.ID != \"\" {\n\t\treturn p.ID\n\t}\n\treturn p.GetIDForToken()\n}\n\n// GetIDForToken returns an identifier that will be used to load the provisioner\n// from a token.\nfunc (p *K8sSA) GetIDForToken() string {\n\treturn K8sSAID\n}\n\n// GetTokenID returns an unimplemented error and does not use the input ott.\nfunc (p *K8sSA) GetTokenID(string) (string, error) {\n\treturn \"\", ErrNotImplemented\n}\n\n// GetName returns the name of the provisioner.\nfunc (p *K8sSA) GetName() string {\n\treturn p.Name\n}\n\n// GetType returns the type of provisioner.\nfunc (p *K8sSA) GetType() Type {\n\treturn TypeK8sSA\n}\n\n// GetEncryptedKey returns false, because the kubernetes provisioner does not\n// have access to the private key.\nfunc (p *K8sSA) GetEncryptedKey() (string, string, bool) {\n\treturn \"\", \"\", false\n}\n\n// Init initializes and validates the fields of a K8sSA type.\nfunc (p *K8sSA) Init(config Config) (err error) {\n\tswitch {\n\tcase p.Type == \"\":\n\t\treturn errors.New(\"provisioner type cannot be empty\")\n\tcase p.Name == \"\":\n\t\treturn errors.New(\"provisioner name cannot be empty\")\n\t}\n\n\tif p.PubKeys != nil {\n\t\tvar (\n\t\t\tblock *pem.Block\n\t\t\trest  = p.PubKeys\n\t\t)\n\t\tfor rest != nil {\n\t\t\tblock, rest = pem.Decode(rest)\n\t\t\tif block == nil {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tkey, err := pemutil.ParseKey(pem.EncodeToMemory(block))\n\t\t\tif err != nil {\n\t\t\t\treturn errors.Wrapf(err, \"error parsing public key in provisioner '%s'\", p.GetName())\n\t\t\t}\n\t\t\tswitch q := key.(type) {\n\t\t\tcase *rsa.PublicKey, *ecdsa.PublicKey, ed25519.PublicKey:\n\t\t\tdefault:\n\t\t\t\treturn errors.Errorf(\"Unexpected public key type %T in provisioner '%s'\", q, p.GetName())\n\t\t\t}\n\t\t\tp.pubKeys = append(p.pubKeys, key)\n\t\t}\n\t} else {\n\t\t// TODO: Use the TokenReview API if no pub keys provided. This will need to\n\t\t// be configured with additional attributes in the K8sSA struct for\n\t\t// connecting to the kubernetes API server.\n\t\treturn errors.New(\"K8s Service Account provisioner cannot be initialized without pub keys\")\n\t}\n\t/*\n\t\t// NOTE: Not sure if we should be doing this initialization here ...\n\t\t// If you have a k8sSA provisioner defined in your config, but you're not\n\t\t// in a kubernetes pod then your CA will fail to startup. Maybe we just postpone\n\t\t// creating the authn until token validation time?\n\t\tif err := checkAccess(k8s.AuthorizationV1()); err != nil {\n\t\t\treturn errors.Wrapf(err, \"error verifying access to kubernetes authz service for provisioner %s\", p.GetID())\n\t\t}\n\n\t\tp.kauthn = k8s.AuthenticationV1()\n\t*/\n\n\tp.ctl, err = NewController(p, p.Claims, config, p.Options)\n\treturn\n}\n\n// authorizeToken performs common jwt authorization actions and returns the\n// claims for case specific downstream parsing.\n// e.g. a Sign request will auth/validate different fields than a Revoke request.\nfunc (p *K8sSA) authorizeToken(token string, audiences []string) (*k8sSAPayload, error) {\n\t_ = audiences // unused input\n\tjwt, err := jose.ParseSigned(token)\n\tif err != nil {\n\t\treturn nil, errs.Wrap(http.StatusUnauthorized, err,\n\t\t\t\"k8ssa.authorizeToken; error parsing k8sSA token\")\n\t}\n\n\tvar (\n\t\tvalid  bool\n\t\tclaims k8sSAPayload\n\t)\n\tif p.pubKeys == nil {\n\t\treturn nil, errs.Unauthorized(\"k8ssa.authorizeToken; k8sSA TokenReview API integration not implemented\")\n\t\t/* NOTE: We plan to support the TokenReview API in a future release.\n\t\t         Below is some code that should be useful when we prioritize\n\t\t\t\t this integration.\n\n\t\t\ttr := kauthnApi.TokenReview{Spec: kauthnApi.TokenReviewSpec{Token: string(token)}}\n\t\t\trvw, err := p.kauthn.TokenReviews().Create(&tr)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, errors.Wrap(err, \"error using kubernetes TokenReview API\")\n\t\t\t}\n\t\t\tif rvw.Status.Error != \"\" {\n\t\t\t\treturn nil, errors.Errorf(\"error from kubernetes TokenReviewAPI: %s\", rvw.Status.Error)\n\t\t\t}\n\t\t\tif !rvw.Status.Authenticated {\n\t\t\t\treturn nil, errors.New(\"error from kubernetes TokenReviewAPI: token could not be authenticated\")\n\t\t\t}\n\t\t\tif err = jwt.UnsafeClaimsWithoutVerification(&claims); err != nil {\n\t\t\t\treturn nil, errors.Wrap(err, \"error parsing claims\")\n\t\t\t}\n\t\t*/\n\t}\n\tfor _, pk := range p.pubKeys {\n\t\tif err = jwt.Claims(pk, &claims); err == nil {\n\t\t\tvalid = true\n\t\t\tbreak\n\t\t}\n\t}\n\tif !valid {\n\t\treturn nil, errs.Unauthorized(\"k8ssa.authorizeToken; error validating k8sSA token and extracting claims\")\n\t}\n\n\t// According to \"rfc7519 JSON Web Token\" acceptable skew should be no\n\t// more than a few minutes.\n\tif err = claims.Validate(jose.Expected{\n\t\tIssuer: k8sSAIssuer,\n\t}); err != nil {\n\t\treturn nil, errs.Wrap(http.StatusUnauthorized, err, \"k8ssa.authorizeToken; invalid k8sSA token claims\")\n\t}\n\n\tif claims.Subject == \"\" {\n\t\treturn nil, errs.Unauthorized(\"k8ssa.authorizeToken; k8sSA token subject cannot be empty\")\n\t}\n\n\treturn &claims, nil\n}\n\n// AuthorizeRevoke returns an error if the provisioner does not have rights to\n// revoke the certificate with serial number in the `sub` property.\nfunc (p *K8sSA) AuthorizeRevoke(_ context.Context, token string) error {\n\t_, err := p.authorizeToken(token, p.ctl.Audiences.Revoke)\n\treturn errs.Wrap(http.StatusInternalServerError, err, \"k8ssa.AuthorizeRevoke\")\n}\n\n// AuthorizeSign validates the given token.\nfunc (p *K8sSA) AuthorizeSign(_ context.Context, token string) ([]SignOption, error) {\n\tclaims, err := p.authorizeToken(token, p.ctl.Audiences.Sign)\n\tif err != nil {\n\t\treturn nil, errs.Wrap(http.StatusInternalServerError, err, \"k8ssa.AuthorizeSign\")\n\t}\n\n\t// Add some values to use in custom templates.\n\tdata := x509util.NewTemplateData()\n\tdata.SetCommonName(claims.ServiceAccountName)\n\tif v, err := unsafeParseSigned(token); err == nil {\n\t\tdata.SetToken(v)\n\t}\n\n\t// Certificate templates: on K8sSA the default template is the certificate\n\t// request.\n\ttemplateOptions, err := CustomTemplateOptions(p.Options, data, x509util.DefaultAdminLeafTemplate)\n\tif err != nil {\n\t\treturn nil, errs.Wrap(http.StatusInternalServerError, err, \"k8ssa.AuthorizeSign\")\n\t}\n\n\treturn []SignOption{\n\t\tp,\n\t\ttemplateOptions,\n\t\t// modifiers / withOptions\n\t\tnewProvisionerExtensionOption(TypeK8sSA, p.Name, \"\").WithControllerOptions(p.ctl),\n\t\tprofileDefaultDuration(p.ctl.Claimer.DefaultTLSCertDuration()),\n\t\t// validators\n\t\tdefaultPublicKeyValidator{},\n\t\tnewValidityValidator(p.ctl.Claimer.MinTLSCertDuration(), p.ctl.Claimer.MaxTLSCertDuration()),\n\t\tnewX509NamePolicyValidator(p.ctl.getPolicy().getX509()),\n\t\tp.ctl.newWebhookController(data, linkedca.Webhook_X509),\n\t}, nil\n}\n\n// AuthorizeRenew returns an error if the renewal is disabled.\nfunc (p *K8sSA) AuthorizeRenew(ctx context.Context, cert *x509.Certificate) error {\n\treturn p.ctl.AuthorizeRenew(ctx, cert)\n}\n\n// AuthorizeSSHSign validates an request for an SSH certificate.\nfunc (p *K8sSA) AuthorizeSSHSign(_ context.Context, token string) ([]SignOption, error) {\n\tif !p.ctl.Claimer.IsSSHCAEnabled() {\n\t\treturn nil, errs.Unauthorized(\"k8ssa.AuthorizeSSHSign; sshCA is disabled for k8sSA provisioner '%s'\", p.GetName())\n\t}\n\tclaims, err := p.authorizeToken(token, p.ctl.Audiences.SSHSign)\n\tif err != nil {\n\t\treturn nil, errs.Wrap(http.StatusInternalServerError, err, \"k8ssa.AuthorizeSSHSign\")\n\t}\n\n\t// Certificate templates.\n\t// Set some default variables to be used in the templates.\n\tdata := sshutil.CreateTemplateData(sshutil.HostCert, claims.ServiceAccountName, []string{claims.ServiceAccountName})\n\tif v, err := unsafeParseSigned(token); err == nil {\n\t\tdata.SetToken(v)\n\t}\n\n\ttemplateOptions, err := CustomSSHTemplateOptions(p.Options, data, sshutil.CertificateRequestTemplate)\n\tif err != nil {\n\t\treturn nil, errs.Wrap(http.StatusInternalServerError, err, \"k8ssa.AuthorizeSSHSign\")\n\t}\n\tsignOptions := []SignOption{templateOptions}\n\n\treturn append(signOptions,\n\t\tp,\n\t\t// Require type, key-id and principals in the SignSSHOptions.\n\t\t&sshCertOptionsRequireValidator{CertType: true, KeyID: true, Principals: true},\n\t\t// Set the validity bounds if not set.\n\t\t&sshDefaultDuration{p.ctl.Claimer},\n\t\t// Validate public key\n\t\t&sshDefaultPublicKeyValidator{},\n\t\t// Validate the validity period.\n\t\t&sshCertValidityValidator{p.ctl.Claimer},\n\t\t// Require and validate all the default fields in the SSH certificate.\n\t\t&sshCertDefaultValidator{},\n\t\t// Ensure that all principal names are allowed\n\t\tnewSSHNamePolicyValidator(p.ctl.getPolicy().getSSHHost(), p.ctl.getPolicy().getSSHUser()),\n\t\t// Call webhooks\n\t\tp.ctl.newWebhookController(data, linkedca.Webhook_SSH),\n\t), nil\n}\n\n/*\nfunc checkAccess(authz kauthz.AuthorizationV1Interface) error {\n\tr := &kauthzApi.SelfSubjectAccessReview{\n\t\tSpec: kauthzApi.SelfSubjectAccessReviewSpec{\n\t\t\tResourceAttributes: &kauthzApi.ResourceAttributes{\n\t\t\t\tGroup:    \"authentication.k8s.io\",\n\t\t\t\tVersion:  \"v1\",\n\t\t\t\tResource: \"tokenreviews\",\n\t\t\t\tVerb:     \"create\",\n\t\t\t},\n\t\t},\n\t}\n\trvw, err := authz.SelfSubjectAccessReviews().Create(r)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif !rvw.Status.Allowed {\n\t\treturn fmt.Errorf(\"Unable to create kubernetes token reviews: %s\", rvw.Status.Reason)\n\t}\n\n\treturn nil\n}\n*/\n"
  },
  {
    "path": "authority/provisioner/k8sSA_test.go",
    "content": "package provisioner\n\nimport (\n\t\"context\"\n\t\"crypto/x509\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"testing\"\n\t\"time\"\n\n\t\"go.step.sm/crypto/jose\"\n\n\t\"github.com/smallstep/assert\"\n\t\"github.com/smallstep/certificates/api/render\"\n)\n\nfunc TestK8sSA_Getters(t *testing.T) {\n\tp, err := generateK8sSA(nil)\n\tassert.FatalError(t, err)\n\tid := \"k8ssa/\" + p.Name\n\tif got := p.GetID(); got != id {\n\t\tt.Errorf(\"K8sSA.GetID() = %v, want %v\", got, id)\n\t}\n\tif got := p.GetName(); got != p.Name {\n\t\tt.Errorf(\"K8sSA.GetName() = %v, want %v\", got, p.Name)\n\t}\n\tif got := p.GetType(); got != TypeK8sSA {\n\t\tt.Errorf(\"K8sSA.GetType() = %v, want %v\", got, TypeK8sSA)\n\t}\n\tkid, key, ok := p.GetEncryptedKey()\n\tif kid != \"\" || key != \"\" || ok == true {\n\t\tt.Errorf(\"K8sSA.GetEncryptedKey() = (%v, %v, %v), want (%v, %v, %v)\",\n\t\t\tkid, key, ok, \"\", \"\", false)\n\t}\n}\n\nfunc TestK8sSA_authorizeToken(t *testing.T) {\n\ttype test struct {\n\t\tp     *K8sSA\n\t\ttoken string\n\t\terr   error\n\t\tcode  int\n\t}\n\ttests := map[string]func(*testing.T) test{\n\t\t\"fail/bad-token\": func(t *testing.T) test {\n\t\t\tp, err := generateK8sSA(nil)\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tp:     p,\n\t\t\t\ttoken: \"foo\",\n\t\t\t\tcode:  http.StatusUnauthorized,\n\t\t\t\terr:   errors.New(\"k8ssa.authorizeToken; error parsing k8sSA token\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/not-implemented\": func(t *testing.T) test {\n\t\t\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\tassert.FatalError(t, err)\n\t\t\tp, err := generateK8sSA(nil)\n\t\t\tassert.FatalError(t, err)\n\t\t\ttok, err := generateToken(\"\", p.Name, testAudiences.Sign[0], \"\",\n\t\t\t\t[]string{\"test.smallstep.com\"}, time.Now(), jwk)\n\t\t\tp.pubKeys = nil\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tp:     p,\n\t\t\t\ttoken: tok,\n\t\t\t\terr:   errors.New(\"k8ssa.authorizeToken; k8sSA TokenReview API integration not implemented\"),\n\t\t\t\tcode:  http.StatusUnauthorized,\n\t\t\t}\n\t\t},\n\t\t\"fail/error-validating-token\": func(t *testing.T) test {\n\t\t\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\tassert.FatalError(t, err)\n\t\t\tp, err := generateK8sSA(nil)\n\t\t\tassert.FatalError(t, err)\n\t\t\ttok, err := generateToken(\"\", p.Name, testAudiences.Sign[0], \"\",\n\t\t\t\t[]string{\"test.smallstep.com\"}, time.Now(), jwk)\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tp:     p,\n\t\t\t\ttoken: tok,\n\t\t\t\terr:   errors.New(\"k8ssa.authorizeToken; error validating k8sSA token and extracting claims\"),\n\t\t\t\tcode:  http.StatusUnauthorized,\n\t\t\t}\n\t\t},\n\t\t\"fail/invalid-issuer\": func(t *testing.T) test {\n\t\t\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\tassert.FatalError(t, err)\n\t\t\tp, err := generateK8sSA(jwk.Public().Key)\n\t\t\tassert.FatalError(t, err)\n\t\t\tclaims := getK8sSAPayload()\n\t\t\tclaims.Claims.Issuer = \"invalid\"\n\t\t\ttok, err := generateK8sSAToken(jwk, claims)\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tp:     p,\n\t\t\t\ttoken: tok,\n\t\t\t\tcode:  http.StatusUnauthorized,\n\t\t\t\terr:   errors.New(\"k8ssa.authorizeToken; invalid k8sSA token claims: go-jose/go-jose/jwt: validation failed, invalid issuer claim (iss)\"),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\tassert.FatalError(t, err)\n\t\t\tp, err := generateK8sSA(jwk.Public().Key)\n\t\t\tassert.FatalError(t, err)\n\t\t\ttok, err := generateK8sSAToken(jwk, nil)\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tp:     p,\n\t\t\t\ttoken: tok,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, tt := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ttc := tt(t)\n\t\t\tif claims, err := tc.p.authorizeToken(tc.token, testAudiences.Sign); err != nil {\n\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\tvar sc render.StatusCodedError\n\t\t\t\t\tassert.Fatal(t, errors.As(err, &sc), \"error does not implement StatusCodedError interface\")\n\t\t\t\t\tassert.Equals(t, sc.StatusCode(), tc.code)\n\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.err.Error())\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif assert.Nil(t, tc.err) {\n\t\t\t\t\tassert.NotNil(t, claims)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestK8sSA_AuthorizeRevoke(t *testing.T) {\n\ttype test struct {\n\t\tp     *K8sSA\n\t\ttoken string\n\t\terr   error\n\t\tcode  int\n\t}\n\ttests := map[string]func(*testing.T) test{\n\t\t\"fail/invalid-token\": func(t *testing.T) test {\n\t\t\tp, err := generateK8sSA(nil)\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tp:     p,\n\t\t\t\ttoken: \"foo\",\n\t\t\t\tcode:  http.StatusUnauthorized,\n\t\t\t\terr:   errors.New(\"k8ssa.AuthorizeRevoke: k8ssa.authorizeToken; error parsing k8sSA token\"),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\tassert.FatalError(t, err)\n\t\t\tp, err := generateK8sSA(jwk.Public().Key)\n\t\t\tassert.FatalError(t, err)\n\t\t\ttok, err := generateK8sSAToken(jwk, nil)\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tp:     p,\n\t\t\t\ttoken: tok,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, tt := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ttc := tt(t)\n\t\t\tif err := tc.p.AuthorizeRevoke(context.Background(), tc.token); err != nil {\n\t\t\t\tvar sc render.StatusCodedError\n\t\t\t\tassert.Fatal(t, errors.As(err, &sc), \"error does not implement StatusCodedError interface\")\n\t\t\t\tassert.Equals(t, sc.StatusCode(), tc.code)\n\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.err.Error())\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tassert.Nil(t, tc.err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestK8sSA_AuthorizeRenew(t *testing.T) {\n\tnow := time.Now().Truncate(time.Second)\n\ttype test struct {\n\t\tp    *K8sSA\n\t\tcert *x509.Certificate\n\t\terr  error\n\t\tcode int\n\t}\n\ttests := map[string]func(*testing.T) test{\n\t\t\"fail/renew-disabled\": func(t *testing.T) test {\n\t\t\tp, err := generateK8sSA(nil)\n\t\t\tassert.FatalError(t, err)\n\t\t\t// disable renewal\n\t\t\tdisable := true\n\t\t\tp.Claims = &Claims{DisableRenewal: &disable}\n\t\t\tp.ctl.Claimer, err = NewClaimer(p.Claims, globalProvisionerClaims)\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tp: p,\n\t\t\t\tcert: &x509.Certificate{\n\t\t\t\t\tNotBefore: now,\n\t\t\t\t\tNotAfter:  now.Add(time.Hour),\n\t\t\t\t},\n\t\t\t\tcode: http.StatusUnauthorized,\n\t\t\t\terr:  fmt.Errorf(\"renew is disabled for provisioner '%s'\", p.GetName()),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tp, err := generateK8sSA(nil)\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tp: p,\n\t\t\t\tcert: &x509.Certificate{\n\t\t\t\t\tNotBefore: now,\n\t\t\t\t\tNotAfter:  now.Add(time.Hour),\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t}\n\tfor name, tt := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ttc := tt(t)\n\t\t\tif err := tc.p.AuthorizeRenew(context.Background(), tc.cert); err != nil {\n\t\t\t\tvar sc render.StatusCodedError\n\t\t\t\tassert.Fatal(t, errors.As(err, &sc), \"error does not implement StatusCodedError interface\")\n\t\t\t\tassert.Equals(t, sc.StatusCode(), tc.code)\n\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.err.Error())\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tassert.Nil(t, tc.err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestK8sSA_AuthorizeSign(t *testing.T) {\n\ttype test struct {\n\t\tp     *K8sSA\n\t\ttoken string\n\t\tcode  int\n\t\terr   error\n\t}\n\ttests := map[string]func(*testing.T) test{\n\t\t\"fail/invalid-token\": func(t *testing.T) test {\n\t\t\tp, err := generateK8sSA(nil)\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tp:     p,\n\t\t\t\ttoken: \"foo\",\n\t\t\t\tcode:  http.StatusUnauthorized,\n\t\t\t\terr:   errors.New(\"k8ssa.AuthorizeSign: k8ssa.authorizeToken; error parsing k8sSA token\"),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\tassert.FatalError(t, err)\n\t\t\tp, err := generateK8sSA(jwk.Public().Key)\n\t\t\tassert.FatalError(t, err)\n\t\t\ttok, err := generateK8sSAToken(jwk, nil)\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tp:     p,\n\t\t\t\ttoken: tok,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, tt := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ttc := tt(t)\n\t\t\tif opts, err := tc.p.AuthorizeSign(context.Background(), tc.token); err != nil {\n\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\tvar sc render.StatusCodedError\n\t\t\t\t\tassert.Fatal(t, errors.As(err, &sc), \"error does not implement StatusCodedError interface\")\n\t\t\t\t\tassert.Equals(t, sc.StatusCode(), tc.code)\n\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.err.Error())\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif assert.Nil(t, tc.err) {\n\t\t\t\t\tif assert.NotNil(t, opts) {\n\t\t\t\t\t\tfor _, o := range opts {\n\t\t\t\t\t\t\tswitch v := o.(type) {\n\t\t\t\t\t\t\tcase *K8sSA:\n\t\t\t\t\t\t\tcase certificateOptionsFunc:\n\t\t\t\t\t\t\tcase *provisionerExtensionOption:\n\t\t\t\t\t\t\t\tassert.Equals(t, v.Type, TypeK8sSA)\n\t\t\t\t\t\t\t\tassert.Equals(t, v.Name, tc.p.GetName())\n\t\t\t\t\t\t\t\tassert.Equals(t, v.CredentialID, \"\")\n\t\t\t\t\t\t\t\tassert.Len(t, 0, v.KeyValuePairs)\n\t\t\t\t\t\t\tcase profileDefaultDuration:\n\t\t\t\t\t\t\t\tassert.Equals(t, time.Duration(v), tc.p.ctl.Claimer.DefaultTLSCertDuration())\n\t\t\t\t\t\t\tcase defaultPublicKeyValidator:\n\t\t\t\t\t\t\tcase *validityValidator:\n\t\t\t\t\t\t\t\tassert.Equals(t, v.min, tc.p.ctl.Claimer.MinTLSCertDuration())\n\t\t\t\t\t\t\t\tassert.Equals(t, v.max, tc.p.ctl.Claimer.MaxTLSCertDuration())\n\t\t\t\t\t\t\tcase *x509NamePolicyValidator:\n\t\t\t\t\t\t\t\tassert.Equals(t, nil, v.policyEngine)\n\t\t\t\t\t\t\tcase *WebhookController:\n\t\t\t\t\t\t\t\tassert.Len(t, 0, v.webhooks)\n\t\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\t\tassert.FatalError(t, fmt.Errorf(\"unexpected sign option of type %T\", v))\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tassert.Equals(t, 8, len(opts))\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestK8sSA_AuthorizeSSHSign(t *testing.T) {\n\ttype test struct {\n\t\tp     *K8sSA\n\t\ttoken string\n\t\tcode  int\n\t\terr   error\n\t}\n\ttests := map[string]func(*testing.T) test{\n\t\t\"fail/sshCA-disabled\": func(t *testing.T) test {\n\t\t\tp, err := generateK8sSA(nil)\n\t\t\tassert.FatalError(t, err)\n\t\t\t// disable sshCA\n\t\t\tdisable := false\n\t\t\tp.Claims = &Claims{EnableSSHCA: &disable}\n\t\t\tp.ctl.Claimer, err = NewClaimer(p.Claims, globalProvisionerClaims)\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tp:     p,\n\t\t\t\ttoken: \"foo\",\n\t\t\t\tcode:  http.StatusUnauthorized,\n\t\t\t\terr:   fmt.Errorf(\"k8ssa.AuthorizeSSHSign; sshCA is disabled for k8sSA provisioner '%s'\", p.GetName()),\n\t\t\t}\n\t\t},\n\t\t\"fail/invalid-token\": func(t *testing.T) test {\n\t\t\tp, err := generateK8sSA(nil)\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tp:     p,\n\t\t\t\ttoken: \"foo\",\n\t\t\t\tcode:  http.StatusUnauthorized,\n\t\t\t\terr:   errors.New(\"k8ssa.AuthorizeSSHSign: k8ssa.authorizeToken; error parsing k8sSA token\"),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\tassert.FatalError(t, err)\n\t\t\tp, err := generateK8sSA(jwk.Public().Key)\n\t\t\tassert.FatalError(t, err)\n\t\t\ttok, err := generateK8sSAToken(jwk, nil)\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tp:     p,\n\t\t\t\ttoken: tok,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, tt := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ttc := tt(t)\n\t\t\tif opts, err := tc.p.AuthorizeSSHSign(context.Background(), tc.token); err != nil {\n\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\tvar sc render.StatusCodedError\n\t\t\t\t\tassert.Fatal(t, errors.As(err, &sc), \"error does not implement StatusCodedError interface\")\n\t\t\t\t\tassert.Equals(t, sc.StatusCode(), tc.code)\n\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.err.Error())\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif assert.Nil(t, tc.err) {\n\t\t\t\t\tif assert.NotNil(t, opts) {\n\t\t\t\t\t\tassert.Len(t, 9, opts)\n\t\t\t\t\t\tfor _, o := range opts {\n\t\t\t\t\t\t\tswitch v := o.(type) {\n\t\t\t\t\t\t\tcase Interface:\n\t\t\t\t\t\t\tcase sshCertificateOptionsFunc:\n\t\t\t\t\t\t\tcase *sshCertOptionsRequireValidator:\n\t\t\t\t\t\t\t\tassert.Equals(t, v, &sshCertOptionsRequireValidator{CertType: true, KeyID: true, Principals: true})\n\t\t\t\t\t\t\tcase *sshCertValidityValidator:\n\t\t\t\t\t\t\t\tassert.Equals(t, v.Claimer, tc.p.ctl.Claimer)\n\t\t\t\t\t\t\tcase *sshDefaultPublicKeyValidator:\n\t\t\t\t\t\t\tcase *sshCertDefaultValidator:\n\t\t\t\t\t\t\tcase *sshDefaultDuration:\n\t\t\t\t\t\t\t\tassert.Equals(t, v.Claimer, tc.p.ctl.Claimer)\n\t\t\t\t\t\t\tcase *sshNamePolicyValidator:\n\t\t\t\t\t\t\t\tassert.Equals(t, nil, v.userPolicyEngine)\n\t\t\t\t\t\t\t\tassert.Equals(t, nil, v.hostPolicyEngine)\n\t\t\t\t\t\t\tcase *WebhookController:\n\t\t\t\t\t\t\t\tassert.Len(t, 0, v.webhooks)\n\t\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\t\tassert.FatalError(t, fmt.Errorf(\"unexpected sign option of type %T\", v))\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "authority/provisioner/keystore.go",
    "content": "package provisioner\n\nimport (\n\t\"encoding/json\"\n\t\"math/rand\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n\t\"go.step.sm/crypto/jose\"\n)\n\nconst (\n\tdefaultCacheAge    = 12 * time.Hour\n\tdefaultCacheJitter = 1 * time.Hour\n)\n\nvar maxAgeRegex = regexp.MustCompile(`max-age=(\\d+)`)\n\ntype keyStore struct {\n\tsync.RWMutex\n\tclient HTTPClient\n\turi    string\n\tkeySet jose.JSONWebKeySet\n\texpiry time.Time\n\tjitter time.Duration\n}\n\nfunc newKeyStore(client HTTPClient, uri string) (*keyStore, error) {\n\tkeys, age, err := getKeysFromJWKsURI(client, uri)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tjitter := getCacheJitter(age)\n\treturn &keyStore{\n\t\tclient: client,\n\t\turi:    uri,\n\t\tkeySet: keys,\n\t\texpiry: getExpirationTime(age, jitter),\n\t\tjitter: jitter,\n\t}, nil\n}\n\nfunc (ks *keyStore) Get(kid string) (keys []jose.JSONWebKey) {\n\tks.RLock()\n\t// Force reload if expiration has passed\n\tif time.Now().After(ks.expiry) {\n\t\tks.RUnlock()\n\t\tks.reload()\n\t\tks.RLock()\n\t}\n\tkeys = ks.keySet.Key(kid)\n\tks.RUnlock()\n\treturn\n}\n\nfunc (ks *keyStore) reload() {\n\tif keys, age, err := getKeysFromJWKsURI(ks.client, ks.uri); err == nil {\n\t\tks.Lock()\n\t\tks.keySet = keys\n\t\tks.jitter = getCacheJitter(age)\n\t\tks.expiry = getExpirationTime(age, ks.jitter)\n\t\tks.Unlock()\n\t}\n}\n\nfunc getKeysFromJWKsURI(client HTTPClient, uri string) (jose.JSONWebKeySet, time.Duration, error) {\n\tvar keys jose.JSONWebKeySet\n\tresp, err := client.Get(uri)\n\tif err != nil {\n\t\treturn keys, 0, errors.Wrapf(err, \"failed to connect to %s\", uri)\n\t}\n\tdefer resp.Body.Close()\n\tif err := json.NewDecoder(resp.Body).Decode(&keys); err != nil {\n\t\treturn keys, 0, errors.Wrapf(err, \"error reading %s\", uri)\n\t}\n\treturn keys, getCacheAge(resp.Header.Get(\"cache-control\")), nil\n}\n\nfunc getCacheAge(cacheControl string) time.Duration {\n\tage := defaultCacheAge\n\tif cacheControl != \"\" {\n\t\tmatch := maxAgeRegex.FindAllStringSubmatch(cacheControl, -1)\n\t\tif len(match) > 0 {\n\t\t\tif len(match[0]) == 2 {\n\t\t\t\tmaxAge := match[0][1]\n\t\t\t\tmaxAgeInt, err := strconv.ParseInt(maxAge, 10, 64)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn defaultCacheAge\n\t\t\t\t}\n\t\t\t\tage = time.Duration(maxAgeInt) * time.Second\n\t\t\t}\n\t\t}\n\t}\n\treturn age\n}\n\nfunc getCacheJitter(age time.Duration) time.Duration {\n\tswitch {\n\tcase age > time.Hour:\n\t\treturn defaultCacheJitter\n\tcase age == 0:\n\t\t// Avoids a 0 jitter. The duration is not important as it will rotate\n\t\t// automatically on each Get request.\n\t\treturn defaultCacheJitter\n\tdefault:\n\t\treturn age / 3\n\t}\n}\n\nfunc getExpirationTime(age, jitter time.Duration) time.Time {\n\tif age > 0 {\n\t\tn := rand.Int63n(int64(jitter)) //nolint:gosec // not used for cryptographic security\n\t\tage -= time.Duration(n)\n\t}\n\treturn time.Now().Truncate(time.Second).Add(abs(age))\n}\n\n// abs returns the absolute value of n.\nfunc abs(n time.Duration) time.Duration {\n\tif n < 0 {\n\t\treturn -n\n\t}\n\treturn n\n}\n"
  },
  {
    "path": "authority/provisioner/keystore_test.go",
    "content": "package provisioner\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"reflect\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/smallstep/assert\"\n\t\"go.step.sm/crypto/jose\"\n)\n\nfunc Test_newKeyStore(t *testing.T) {\n\tsrv := generateTLSJWKServer(2)\n\tsrv.Close()\n\n\tsrv = httptest.NewTLSServer(srv.Config.Handler)\n\tdefer srv.Close()\n\n\tks, err := newKeyStore(srv.Client(), srv.URL)\n\tassert.FatalError(t, err)\n\n\ttype args struct {\n\t\tclient *http.Client\n\t\turi    string\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\targs    args\n\t\twant    jose.JSONWebKeySet\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", args{srv.Client(), srv.URL}, ks.keySet, false},\n\t\t{\"fail\", args{srv.Client(), srv.URL + \"/error\"}, jose.JSONWebKeySet{}, true},\n\t\t{\"fail client\", args{http.DefaultClient, srv.URL}, jose.JSONWebKeySet{}, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := newKeyStore(tt.args.client, tt.args.uri)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"newKeyStore() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif err == nil {\n\t\t\t\tif !reflect.DeepEqual(got.keySet, tt.want) {\n\t\t\t\t\tt.Errorf(\"newKeyStore() = %v, want %v\", got, tt.want)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_keyStore(t *testing.T) {\n\tsrv := generateJWKServer(2)\n\tdefer srv.Close()\n\n\tks, err := newKeyStore(srv.Client(), srv.URL+\"/random\")\n\tassert.FatalError(t, err)\n\tks.RLock()\n\tkeySet1 := ks.keySet\n\tks.RUnlock()\n\t// Check contents\n\tassert.Len(t, 2, keySet1.Keys)\n\tassert.Len(t, 1, ks.Get(keySet1.Keys[0].KeyID))\n\tassert.Len(t, 1, ks.Get(keySet1.Keys[1].KeyID))\n\tassert.Len(t, 0, ks.Get(\"foobar\"))\n\n\t// Wait for rotation\n\ttime.Sleep(5 * time.Second)\n\tassert.Len(t, 0, ks.Get(\"foobar\")) // force refresh\n\n\tks.RLock()\n\tkeySet2 := ks.keySet\n\tks.RUnlock()\n\tif reflect.DeepEqual(keySet1, keySet2) {\n\t\tt.Error(\"keyStore did not rotated\")\n\t}\n\n\t// Check contents\n\tassert.Len(t, 2, keySet2.Keys)\n\tassert.Len(t, 1, ks.Get(keySet2.Keys[0].KeyID))\n\tassert.Len(t, 1, ks.Get(keySet2.Keys[1].KeyID))\n\tassert.Len(t, 0, ks.Get(\"foobar\"))\n\n\t// Check hits\n\tresp, err := srv.Client().Get(srv.URL + \"/hits\")\n\tassert.FatalError(t, err)\n\thits := struct {\n\t\tHits int `json:\"hits\"`\n\t}{}\n\tdefer resp.Body.Close()\n\terr = json.NewDecoder(resp.Body).Decode(&hits)\n\tassert.FatalError(t, err)\n\tassert.True(t, hits.Hits > 1, fmt.Sprintf(\"invalid number of hits: %d is not greater than 1\", hits.Hits))\n}\n\nfunc Test_keyStore_noCache(t *testing.T) {\n\tsrv := generateJWKServer(2)\n\tdefer srv.Close()\n\n\tks, err := newKeyStore(srv.Client(), srv.URL+\"/no-cache\")\n\tassert.FatalError(t, err)\n\tks.RLock()\n\tkeySet1 := ks.keySet\n\tks.RUnlock()\n\t// The keys will rotate on Get.\n\t// So we won't be able to find the cached ones\n\tassert.Len(t, 2, keySet1.Keys)\n\tassert.Len(t, 0, ks.Get(keySet1.Keys[0].KeyID))\n\tassert.Len(t, 0, ks.Get(keySet1.Keys[1].KeyID))\n\tassert.Len(t, 0, ks.Get(\"foobar\"))\n\n\t// Check hits\n\tresp, err := srv.Client().Get(srv.URL + \"/hits\")\n\tassert.FatalError(t, err)\n\thits := struct {\n\t\tHits int `json:\"hits\"`\n\t}{}\n\tdefer resp.Body.Close()\n\terr = json.NewDecoder(resp.Body).Decode(&hits)\n\tassert.FatalError(t, err)\n\tassert.True(t, hits.Hits > 1, fmt.Sprintf(\"invalid number of hits: %d is not greater than 1\", hits.Hits))\n}\n\nfunc Test_keyStore_Get(t *testing.T) {\n\tsrv := generateJWKServer(2)\n\tdefer srv.Close()\n\tks, err := newKeyStore(srv.Client(), srv.URL)\n\tassert.FatalError(t, err)\n\n\ttype args struct {\n\t\tkid string\n\t}\n\ttests := []struct {\n\t\tname     string\n\t\tks       *keyStore\n\t\targs     args\n\t\twantKeys []jose.JSONWebKey\n\t}{\n\t\t{\"ok1\", ks, args{ks.keySet.Keys[0].KeyID}, []jose.JSONWebKey{ks.keySet.Keys[0]}},\n\t\t{\"ok2\", ks, args{ks.keySet.Keys[1].KeyID}, []jose.JSONWebKey{ks.keySet.Keys[1]}},\n\t\t{\"fail\", ks, args{\"fail\"}, []jose.JSONWebKey(nil)},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif gotKeys := tt.ks.Get(tt.args.kid); !reflect.DeepEqual(gotKeys, tt.wantKeys) {\n\t\t\t\tt.Errorf(\"keyStore.Get() = %v, want %v\", gotKeys, tt.wantKeys)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_abs(t *testing.T) {\n\tmaxInt64 := time.Duration(1<<63 - 1)\n\tminInt64 := time.Duration(-1 << 63)\n\ttype args struct {\n\t\tn time.Duration\n\t}\n\ttests := []struct {\n\t\tname string\n\t\targs args\n\t\twant time.Duration\n\t}{\n\t\t{\"ok\", args{0}, 0},\n\t\t{\"ok\", args{-time.Hour}, time.Hour},\n\t\t{\"ok\", args{time.Hour}, time.Hour},\n\t\t{\"ok maxInt64\", args{maxInt64}, maxInt64},\n\t\t{\"ok minInt64 + 1\", args{minInt64 + 1}, maxInt64},\n\t\t{\"overflow on minInt64\", args{minInt64}, minInt64},\n\t\t{\"overflow on minInt64\", args{minInt64}, -minInt64},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif got := abs(tt.args.n); got != tt.want {\n\t\t\t\tt.Errorf(\"abs() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "authority/provisioner/method.go",
    "content": "package provisioner\n\nimport (\n\t\"context\"\n)\n\n// Method indicates the action to action that we will perform, it's used as part\n// of the context in the call to authorize. It defaults to Sing.\ntype Method int\n\n// The key to save the Method in the context.\ntype methodKey struct{}\n\nconst (\n\t// SignMethod is the method used to sign X.509 certificates.\n\tSignMethod Method = iota\n\t// SignIdentityMethod is the method used to sign X.509 identity certificates.\n\tSignIdentityMethod\n\t// RevokeMethod is the method used to revoke X.509 certificates.\n\tRevokeMethod\n\t// RenewMethod is the method used to renew X.509 certificates.\n\tRenewMethod\n\t// SSHSignMethod is the method used to sign SSH certificates.\n\tSSHSignMethod\n\t// SSHRenewMethod is the method used to renew SSH certificates.\n\tSSHRenewMethod\n\t// SSHRevokeMethod is the method used to revoke SSH certificates.\n\tSSHRevokeMethod\n\t// SSHRekeyMethod is the method used to rekey SSH certificates.\n\tSSHRekeyMethod\n)\n\n// String returns a string representation of the context method.\nfunc (m Method) String() string {\n\tswitch m {\n\tcase SignMethod:\n\t\treturn \"sign-method\"\n\tcase SignIdentityMethod:\n\t\treturn \"sign-identity-method\"\n\tcase RevokeMethod:\n\t\treturn \"revoke-method\"\n\tcase RenewMethod:\n\t\treturn \"renew-method\"\n\tcase SSHSignMethod:\n\t\treturn \"ssh-sign-method\"\n\tcase SSHRenewMethod:\n\t\treturn \"ssh-renew-method\"\n\tcase SSHRevokeMethod:\n\t\treturn \"ssh-revoke-method\"\n\tcase SSHRekeyMethod:\n\t\treturn \"ssh-rekey-method\"\n\tdefault:\n\t\treturn \"unknown\"\n\t}\n}\n\n// NewContextWithMethod creates a new context from ctx and attaches method to\n// it.\nfunc NewContextWithMethod(ctx context.Context, method Method) context.Context {\n\treturn context.WithValue(ctx, methodKey{}, method)\n}\n\n// MethodFromContext returns the Method saved in ctx.\nfunc MethodFromContext(ctx context.Context) Method {\n\tm, _ := ctx.Value(methodKey{}).(Method)\n\treturn m\n}\n\ntype tokenKey struct{}\n\n// NewContextWithToken creates a new context with the given token.\nfunc NewContextWithToken(ctx context.Context, token string) context.Context {\n\treturn context.WithValue(ctx, tokenKey{}, token)\n}\n\n// TokenFromContext returns the token stored in the given context.\nfunc TokenFromContext(ctx context.Context) (string, bool) {\n\ttoken, ok := ctx.Value(tokenKey{}).(string)\n\treturn token, ok\n}\n\n// The key to save the certTypeKey in the context.\ntype certTypeKey struct{}\n\n// NewContextWithCertType creates a new context with the given CertType.\nfunc NewContextWithCertType(ctx context.Context, certType string) context.Context {\n\treturn context.WithValue(ctx, certTypeKey{}, certType)\n}\n\n// CertTypeFromContext returns the certType stored in the given context.\nfunc CertTypeFromContext(ctx context.Context) (string, bool) {\n\tcertType, ok := ctx.Value(certTypeKey{}).(string)\n\treturn certType, ok\n}\n"
  },
  {
    "path": "authority/provisioner/nebula.go",
    "content": "package provisioner\n\nimport (\n\t\"context\"\n\t\"crypto/ecdh\"\n\t\"crypto/ecdsa\"\n\t\"crypto/ed25519\"\n\t\"crypto/elliptic\"\n\t\"crypto/x509\"\n\t\"encoding/base64\"\n\t\"encoding/pem\"\n\t\"math/big\"\n\t\"net\"\n\t\"net/netip\"\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n\tnebula \"github.com/slackhq/nebula/cert\"\n\t\"golang.org/x/crypto/ssh\"\n\n\t\"github.com/smallstep/linkedca\"\n\t\"go.step.sm/crypto/jose\"\n\t\"go.step.sm/crypto/sshutil\"\n\t\"go.step.sm/crypto/x25519\"\n\t\"go.step.sm/crypto/x509util\"\n\n\t\"github.com/smallstep/certificates/errs\"\n\t\"github.com/smallstep/certificates/internal/cast\"\n)\n\nconst (\n\t// NebulaCertHeader is the token header that contains a Nebula certificate.\n\tNebulaCertHeader jose.HeaderKey = \"nebula\"\n)\n\n// Nebula is a provisioner that verifies tokens signed using Nebula private\n// keys. The tokens contain a Nebula certificate in the header, which can be\n// used to verify the token signature. The certificates are themselves verified\n// using the Nebula CA certificates encoded in Roots. The verification process\n// is similar to the process for X5C tokens.\n//\n// Because Nebula \"leaf\" certificates use X25519 keys, the tokens are signed\n// using XEd25519 defined at\n// https://signal.org/docs/specifications/xeddsa/#xeddsa and implemented by\n// go.step.sm/crypto/x25519.\ntype Nebula struct {\n\tID      string   `json:\"-\"`\n\tType    string   `json:\"type\"`\n\tName    string   `json:\"name\"`\n\tRoots   []byte   `json:\"roots\"`\n\tClaims  *Claims  `json:\"claims,omitempty\"`\n\tOptions *Options `json:\"options,omitempty\"`\n\tcaPool  *nebula.CAPool\n\tctl     *Controller\n}\n\n// Init verifies and initializes the Nebula provisioner.\nfunc (p *Nebula) Init(config Config) (err error) {\n\tswitch {\n\tcase p.Type == \"\":\n\t\treturn errors.New(\"provisioner type cannot be empty\")\n\tcase p.Name == \"\":\n\t\treturn errors.New(\"provisioner name cannot be empty\")\n\tcase len(p.Roots) == 0:\n\t\treturn errors.New(\"provisioner root(s) cannot be empty\")\n\t}\n\n\tp.caPool, err = nebula.NewCAPoolFromPEM(p.Roots)\n\tif err != nil {\n\t\treturn errs.InternalServer(\"failed to create CA pool: %v\", err)\n\t}\n\n\tconfig.Audiences = config.Audiences.WithFragment(p.GetIDForToken())\n\tp.ctl, err = NewController(p, p.Claims, config, p.Options)\n\treturn\n}\n\n// GetID returns the provisioner id.\nfunc (p *Nebula) GetID() string {\n\tif p.ID != \"\" {\n\t\treturn p.ID\n\t}\n\treturn p.GetIDForToken()\n}\n\n// GetIDForToken returns an identifier that will be used to load the provisioner\n// from a token.\nfunc (p *Nebula) GetIDForToken() string {\n\treturn \"nebula/\" + p.Name\n}\n\n// GetTokenID returns the identifier of the token.\nfunc (p *Nebula) GetTokenID(token string) (string, error) {\n\t// Validate payload\n\tt, err := jose.ParseSigned(token)\n\tif err != nil {\n\t\treturn \"\", errors.Wrap(err, \"error parsing token\")\n\t}\n\n\t// Get claims w/out verification. We need to look up the provisioner\n\t// key in order to verify the claims and we need the issuer from the claims\n\t// before we can look up the provisioner.\n\tvar claims jose.Claims\n\tif err = t.UnsafeClaimsWithoutVerification(&claims); err != nil {\n\t\treturn \"\", errors.Wrap(err, \"error verifying claims\")\n\t}\n\treturn claims.ID, nil\n}\n\n// GetName returns the name of the provisioner.\nfunc (p *Nebula) GetName() string {\n\treturn p.Name\n}\n\n// GetType returns the type of provisioner.\nfunc (p *Nebula) GetType() Type {\n\treturn TypeNebula\n}\n\n// GetEncryptedKey returns the base provisioner encrypted key if it's defined.\nfunc (p *Nebula) GetEncryptedKey() (kid, key string, ok bool) {\n\treturn \"\", \"\", false\n}\n\n// AuthorizeSign returns the list of SignOption for a Sign request.\nfunc (p *Nebula) AuthorizeSign(_ context.Context, token string) ([]SignOption, error) {\n\tcrt, claims, err := p.authorizeToken(token, p.ctl.Audiences.Sign)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tsans := claims.SANs\n\tif len(sans) == 0 {\n\t\tnetworks := crt.Networks()\n\t\tsans = make([]string, len(networks)+1)\n\t\tsans[0] = crt.Name()\n\t\tfor i, network := range networks {\n\t\t\tsans[i+1] = network.Addr().String()\n\t\t}\n\t}\n\n\tdata := x509util.CreateTemplateData(claims.Subject, sans)\n\tif v, err := unsafeParseSigned(token); err == nil {\n\t\tdata.SetToken(v)\n\t}\n\n\t// The Nebula certificate will be available using the template variable\n\t// AuthorizationCrt. For example {{ .AuthorizationCrt.Details.Groups }} can\n\t// be used to get all the groups.\n\tdata.SetAuthorizationCertificate(crt)\n\n\ttemplateOptions, err := TemplateOptions(p.Options, data)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn []SignOption{\n\t\tp,\n\t\ttemplateOptions,\n\t\t// modifiers / withOptions\n\t\tnewProvisionerExtensionOption(TypeNebula, p.Name, \"\").WithControllerOptions(p.ctl),\n\t\tprofileLimitDuration{\n\t\t\tdef:       p.ctl.Claimer.DefaultTLSCertDuration(),\n\t\t\tnotBefore: crt.NotBefore(),\n\t\t\tnotAfter:  crt.NotAfter(),\n\t\t},\n\t\t// validators\n\t\tcommonNameValidator(claims.Subject),\n\t\tnebulaSANsValidator{\n\t\t\tName:     crt.Name(),\n\t\t\tNetworks: crt.Networks(),\n\t\t},\n\t\tdefaultPublicKeyValidator{},\n\t\tnewValidityValidator(p.ctl.Claimer.MinTLSCertDuration(), p.ctl.Claimer.MaxTLSCertDuration()),\n\t\tnewX509NamePolicyValidator(p.ctl.getPolicy().getX509()),\n\t\tp.ctl.newWebhookController(data, linkedca.Webhook_X509),\n\t}, nil\n}\n\n// AuthorizeSSHSign returns the list of SignOption for a SignSSH request.\n// Currently the Nebula provisioner only grants host SSH certificates.\nfunc (p *Nebula) AuthorizeSSHSign(_ context.Context, token string) ([]SignOption, error) {\n\tif !p.ctl.Claimer.IsSSHCAEnabled() {\n\t\treturn nil, errs.Unauthorized(\"ssh is disabled for nebula provisioner '%s'\", p.Name)\n\t}\n\n\tcrt, claims, err := p.authorizeToken(token, p.ctl.Audiences.SSHSign)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Default template attributes.\n\tkeyID := claims.Subject\n\tnetworks := crt.Networks()\n\tprincipals := make([]string, len(networks)+1)\n\tprincipals[0] = crt.Name()\n\tfor i, network := range networks {\n\t\tprincipals[i+1] = network.Addr().String()\n\t}\n\n\tvar signOptions []SignOption\n\t// If step ssh options are given, validate them and set key id, principals\n\t// and validity.\n\tif claims.Step != nil && claims.Step.SSH != nil {\n\t\topts := claims.Step.SSH\n\n\t\t// Check that the token only contains valid principals.\n\t\tv := nebulaPrincipalsValidator{\n\t\t\tName:     crt.Name(),\n\t\t\tNetworks: crt.Networks(),\n\t\t}\n\t\tif err := v.Valid(*opts); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\t// Check that the cert type is a valid one.\n\t\tif opts.CertType != \"\" && opts.CertType != SSHHostCert {\n\t\t\treturn nil, errs.Forbidden(\"ssh certificate type does not match - got %v, want %v\", opts.CertType, SSHHostCert)\n\t\t}\n\n\t\tsignOptions = []SignOption{\n\t\t\t// validate is a host certificate and users's KeyID is the subject.\n\t\t\tsshCertOptionsValidator(SignSSHOptions{\n\t\t\t\tCertType: SSHHostCert,\n\t\t\t\tKeyID:    claims.Subject,\n\t\t\t}),\n\t\t\t// validates user's SSHOptions with the ones in the token\n\t\t\tsshCertOptionsValidator(*opts),\n\t\t}\n\n\t\t// Use options in the token.\n\t\tif opts.KeyID != \"\" {\n\t\t\tkeyID = opts.KeyID\n\t\t}\n\t\tif len(opts.Principals) > 0 {\n\t\t\tprincipals = opts.Principals\n\t\t}\n\n\t\t// Add modifiers from custom claims\n\t\tt := now()\n\t\tif !opts.ValidAfter.IsZero() {\n\t\t\tsignOptions = append(signOptions, sshCertValidAfterModifier(cast.Uint64(opts.ValidAfter.RelativeTime(t).Unix())))\n\t\t}\n\t\tif !opts.ValidBefore.IsZero() {\n\t\t\tsignOptions = append(signOptions, sshCertValidBeforeModifier(cast.Uint64(opts.ValidBefore.RelativeTime(t).Unix())))\n\t\t}\n\t}\n\n\t// Certificate templates.\n\tdata := sshutil.CreateTemplateData(sshutil.HostCert, keyID, principals)\n\tif v, err := unsafeParseSigned(token); err == nil {\n\t\tdata.SetToken(v)\n\t}\n\n\t// The Nebula certificate will be available using the template variable Crt.\n\t// For example {{ .AuthorizationCrt.Details.Groups }} can be used to get all the groups.\n\tdata.SetAuthorizationCertificate(crt)\n\n\ttemplateOptions, err := TemplateSSHOptions(p.Options, data)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn append(signOptions,\n\t\tp,\n\t\ttemplateOptions,\n\t\t// Checks the validity bounds, and set the validity if has not been set.\n\t\t&sshLimitDuration{p.ctl.Claimer, crt.NotAfter()},\n\t\t// Validate public key.\n\t\t&sshDefaultPublicKeyValidator{},\n\t\t// Validate the validity period.\n\t\t&sshCertValidityValidator{p.ctl.Claimer},\n\t\t// Require all the fields in the SSH certificate\n\t\t&sshCertDefaultValidator{},\n\t\t// Ensure that all principal names are allowed\n\t\tnewSSHNamePolicyValidator(p.ctl.getPolicy().getSSHHost(), nil),\n\t\t// Call webhooks\n\t\tp.ctl.newWebhookController(data, linkedca.Webhook_SSH),\n\t), nil\n}\n\n// AuthorizeRenew returns an error if the renewal is disabled.\nfunc (p *Nebula) AuthorizeRenew(ctx context.Context, crt *x509.Certificate) error {\n\treturn p.ctl.AuthorizeRenew(ctx, crt)\n}\n\n// AuthorizeRevoke returns an unauthorized error.\nfunc (p *Nebula) AuthorizeRevoke(context.Context, string) error {\n\treturn errs.Unauthorized(\"nebula provisioner does not support revoke\")\n}\n\n// AuthorizeSSHRevoke returns an unauthorized error.\nfunc (p *Nebula) AuthorizeSSHRevoke(context.Context, string) error {\n\treturn errs.Unauthorized(\"nebula provisioner does not support SSH revoke\")\n}\n\n// AuthorizeSSHRenew returns an unauthorized error.\nfunc (p *Nebula) AuthorizeSSHRenew(context.Context, string) (*ssh.Certificate, error) {\n\treturn nil, errs.Unauthorized(\"nebula provisioner does not support SSH renew\")\n}\n\n// AuthorizeSSHRekey returns an unauthorized error.\nfunc (p *Nebula) AuthorizeSSHRekey(context.Context, string) (*ssh.Certificate, []SignOption, error) {\n\treturn nil, nil, errs.Unauthorized(\"nebula provisioner does not support SSH rekey\")\n}\n\nfunc (p *Nebula) authorizeToken(token string, audiences []string) (nebula.Certificate, *jwtPayload, error) {\n\tjwt, err := jose.ParseSigned(token)\n\tif err != nil {\n\t\treturn nil, nil, errs.UnauthorizedErr(err, errs.WithMessage(\"failed to parse token\"))\n\t}\n\n\t// Extract Nebula certificate\n\th, ok := jwt.Headers[0].ExtraHeaders[NebulaCertHeader]\n\tif !ok {\n\t\treturn nil, nil, errs.Unauthorized(\"failed to parse token: nebula header is missing\")\n\t}\n\ts, ok := h.(string)\n\tif !ok {\n\t\treturn nil, nil, errs.Unauthorized(\"failed to parse token: nebula header is not valid\")\n\t}\n\tb, err := base64.StdEncoding.DecodeString(s)\n\tif err != nil {\n\t\treturn nil, nil, errs.UnauthorizedErr(err, errs.WithMessage(\"failed to parse token: nebula header is not valid\"))\n\t}\n\t// Wrap raw certificate bytes in PEM for unmarshaling. Try v1 banner\n\t// first, then fall back to v2 if that fails.\n\tpemData := pem.EncodeToMemory(&pem.Block{Type: nebula.CertificateBanner, Bytes: b})\n\tc, _, err := nebula.UnmarshalCertificateFromPEM(pemData)\n\tif err != nil {\n\t\tpemData = pem.EncodeToMemory(&pem.Block{Type: nebula.CertificateV2Banner, Bytes: b})\n\t\tc, _, err = nebula.UnmarshalCertificateFromPEM(pemData)\n\t}\n\tif err != nil {\n\t\treturn nil, nil, errs.UnauthorizedErr(err, errs.WithMessage(\"failed to parse nebula certificate: nebula header is not valid\"))\n\t}\n\n\t// Validate nebula certificate against CAs\n\tif _, err := p.caPool.VerifyCertificate(now(), c); err != nil {\n\t\treturn nil, nil, errs.UnauthorizedErr(err, errs.WithMessage(\"token is not valid: failed to verify certificate against configured CA\"))\n\t}\n\n\tvar pub any\n\tswitch {\n\tcase c.Curve() == nebula.Curve_P256:\n\t\t// When Nebula is used with ECDSA P-256 keys, both CAs and clients use the same type.\n\t\tecdhPub, err := ecdh.P256().NewPublicKey(c.PublicKey())\n\t\tif err != nil {\n\t\t\treturn nil, nil, errs.UnauthorizedErr(err, errs.WithMessage(\"failed to parse nebula public key\"))\n\t\t}\n\t\tpublicKeyBytes := ecdhPub.Bytes()\n\t\tpub = &ecdsa.PublicKey{ // convert back to *ecdsa.PublicKey, because our jose package nor go-jose supports *ecdh.PublicKey\n\t\t\tCurve: elliptic.P256(),\n\t\t\tX:     big.NewInt(0).SetBytes(publicKeyBytes[1:33]),\n\t\t\tY:     big.NewInt(0).SetBytes(publicKeyBytes[33:]),\n\t\t}\n\tcase c.IsCA():\n\t\tpub = ed25519.PublicKey(c.PublicKey())\n\tdefault:\n\t\tpub = x25519.PublicKey(c.PublicKey())\n\t}\n\n\t// Validate token with public key\n\tvar claims jwtPayload\n\tif err := jose.Verify(jwt, pub, &claims); err != nil {\n\t\treturn nil, nil, errs.UnauthorizedErr(err, errs.WithMessage(\"token is not valid: signature does not match\"))\n\t}\n\n\t// According to \"rfc7519 JSON Web Token\" acceptable skew should be no\n\t// more than a few minutes.\n\tif err = claims.ValidateWithLeeway(jose.Expected{\n\t\tIssuer: p.Name,\n\t\tTime:   now(),\n\t}, time.Minute); err != nil {\n\t\treturn nil, nil, errs.UnauthorizedErr(err, errs.WithMessage(\"token is not valid: invalid claims\"))\n\t}\n\n\t// Validate token and subject too.\n\tif !matchesAudience(claims.Audience, audiences) {\n\t\treturn nil, nil, errs.Unauthorized(\"token is not valid: invalid claims\")\n\t}\n\tif claims.Subject == \"\" {\n\t\treturn nil, nil, errs.Unauthorized(\"token is not valid: subject cannot be empty\")\n\t}\n\n\treturn c, &claims, nil\n}\n\ntype nebulaSANsValidator struct {\n\tName     string\n\tNetworks []netip.Prefix\n}\n\n// Valid verifies that the SANs stored in the validator are contained with those\n// requested in the x509 certificate request.\nfunc (v nebulaSANsValidator) Valid(req *x509.CertificateRequest) error {\n\tdnsNames, ips, emails, uris := x509util.SplitSANs([]string{v.Name})\n\tif len(req.DNSNames) > 0 {\n\t\tif err := dnsNamesValidator(dnsNames).Valid(req); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tif len(req.EmailAddresses) > 0 {\n\t\tif err := emailAddressesValidator(emails).Valid(req); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tif len(req.URIs) > 0 {\n\t\tif err := newURIsValidator(context.Background(), uris).Valid(req); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tif len(req.IPAddresses) > 0 {\n\t\tfor _, ip := range req.IPAddresses {\n\t\t\tvar valid bool\n\t\t\t// Check ip in name\n\t\t\tfor _, ipInName := range ips {\n\t\t\t\tif ip.Equal(ipInName) {\n\t\t\t\t\tvalid = true\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t\t// Check ip network\n\t\t\tif !valid {\n\t\t\t\tfor _, network := range v.Networks {\n\t\t\t\t\tif ip.Equal(net.IP(network.Addr().AsSlice())) {\n\t\t\t\t\t\tvalid = true\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tif !valid {\n\t\t\t\tfor _, network := range v.Networks {\n\t\t\t\t\tips = append(ips, net.IP(network.Addr().AsSlice()))\n\t\t\t\t}\n\t\t\t\treturn errs.Forbidden(\"certificate request contains invalid IP addresses - got %v, want %v\", req.IPAddresses, ips)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n\ntype nebulaPrincipalsValidator struct {\n\tName     string\n\tNetworks []netip.Prefix\n}\n\n// Valid checks that the SignSSHOptions principals contains only names in the\n// Nebula certificate.\nfunc (v nebulaPrincipalsValidator) Valid(got SignSSHOptions) error {\n\tfor _, p := range got.Principals {\n\t\tvar valid bool\n\t\tif p == v.Name {\n\t\t\tvalid = true\n\t\t}\n\t\tif !valid {\n\t\t\tif ip := net.ParseIP(p); ip != nil {\n\t\t\t\tfor _, network := range v.Networks {\n\t\t\t\t\tif ip.Equal(net.IP(network.Addr().AsSlice())) {\n\t\t\t\t\t\tvalid = true\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif !valid {\n\t\t\tips := make([]net.IP, len(v.Networks))\n\t\t\tfor i, network := range v.Networks {\n\t\t\t\tips[i] = net.IP(network.Addr().AsSlice())\n\t\t\t}\n\t\t\treturn errs.Forbidden(\n\t\t\t\t\"ssh certificate principals contains invalid name or IP addresses - got %v, want %s or %v\",\n\t\t\t\tgot.Principals, v.Name, ips,\n\t\t\t)\n\t\t}\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "authority/provisioner/nebula_test.go",
    "content": "package provisioner\n\nimport (\n\t\"context\"\n\t\"crypto\"\n\t\"crypto/ecdsa\"\n\t\"crypto/ed25519\"\n\t\"crypto/elliptic\"\n\t\"crypto/rand\"\n\t\"crypto/x509\"\n\t\"net\"\n\t\"net/netip\"\n\t\"net/url\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/slackhq/nebula/cert\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"golang.org/x/crypto/ssh\"\n\n\t\"go.step.sm/crypto/jose\"\n\t\"go.step.sm/crypto/randutil\"\n\t\"go.step.sm/crypto/x25519\"\n\t\"go.step.sm/crypto/x509util\"\n)\n\nfunc mustNebulaPrefix(t *testing.T, s string) netip.Prefix {\n\tt.Helper()\n\tp, err := netip.ParsePrefix(s)\n\trequire.NoError(t, err)\n\treturn p\n}\n\nfunc mustNebulaCA(t *testing.T) (cert.Certificate, ed25519.PrivateKey) {\n\tt.Helper()\n\tpub, priv, err := ed25519.GenerateKey(rand.Reader)\n\trequire.NoError(t, err)\n\tnow := time.Now()\n\ttbs := &cert.TBSCertificate{\n\t\tVersion:   cert.Version1,\n\t\tCurve:     cert.Curve_CURVE25519,\n\t\tName:      \"TestCA\",\n\t\tGroups:    []string{\"test\"},\n\t\tNetworks:  []netip.Prefix{netip.MustParsePrefix(\"10.1.0.0/16\")},\n\t\tNotBefore: time.Unix(now.Unix(), 0),\n\t\tNotAfter:  time.Unix(now.Add(10*time.Minute).Unix(), 0),\n\t\tPublicKey: pub,\n\t\tIsCA:      true,\n\t}\n\tnc, err := tbs.Sign(nil, cert.Curve_CURVE25519, priv)\n\trequire.NoError(t, err)\n\n\treturn nc, priv\n}\n\nfunc mustExpiredNebulaCA(t *testing.T) (cert.Certificate, ed25519.PrivateKey) {\n\tt.Helper()\n\tpub, priv, err := ed25519.GenerateKey(rand.Reader)\n\trequire.NoError(t, err)\n\tnow := time.Now()\n\ttbs := &cert.TBSCertificate{\n\t\tVersion:   cert.Version1,\n\t\tCurve:     cert.Curve_CURVE25519,\n\t\tName:      \"ExpiredTestCA\",\n\t\tGroups:    []string{\"expired\"},\n\t\tNetworks:  []netip.Prefix{netip.MustParsePrefix(\"10.2.0.0/16\")},\n\t\tNotBefore: time.Unix(now.Add(-2*time.Hour).Unix(), 0),\n\t\tNotAfter:  time.Unix(now.Add(-1*time.Hour).Unix(), 0),\n\t\tPublicKey: pub,\n\t\tIsCA:      true,\n\t}\n\tnc, err := tbs.Sign(nil, cert.Curve_CURVE25519, priv)\n\trequire.NoError(t, err)\n\n\treturn nc, priv\n}\n\nfunc mustNebulaP256CA(t *testing.T) (cert.Certificate, *ecdsa.PrivateKey) {\n\tt.Helper()\n\n\tkey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)\n\trequire.NoError(t, err)\n\tecdhPriv, err := key.ECDH()\n\trequire.NoError(t, err)\n\n\tnow := time.Now()\n\ttbs := &cert.TBSCertificate{\n\t\tVersion:   cert.Version1,\n\t\tCurve:     cert.Curve_P256,\n\t\tName:      \"TestCA\",\n\t\tGroups:    []string{\"test\"},\n\t\tNetworks:  []netip.Prefix{netip.MustParsePrefix(\"10.1.0.0/16\")},\n\t\tNotBefore: time.Unix(now.Unix(), 0),\n\t\tNotAfter:  time.Unix(now.Add(10*time.Minute).Unix(), 0),\n\t\tPublicKey: ecdhPriv.PublicKey().Bytes(),\n\t\tIsCA:      true,\n\t}\n\n\t// For P256 CAs, Sign expects the raw 32-byte scalar as the key.\n\tnc, err := tbs.Sign(nil, cert.Curve_P256, key.D.FillBytes(make([]byte, 32)))\n\trequire.NoError(t, err)\n\treturn nc, key\n}\n\nfunc mustNebulaCert(t *testing.T, name string, network netip.Prefix, groups []string, ca cert.Certificate, signer ed25519.PrivateKey) (cert.Certificate, crypto.Signer) {\n\tt.Helper()\n\n\tpub, priv, err := x25519.GenerateKey(rand.Reader)\n\trequire.NoError(t, err)\n\n\tt1 := time.Now().Truncate(time.Second)\n\ttbs := &cert.TBSCertificate{\n\t\tVersion:   cert.Version1,\n\t\tCurve:     cert.Curve_CURVE25519,\n\t\tName:      name,\n\t\tNetworks:  []netip.Prefix{network},\n\t\tGroups:    groups,\n\t\tNotBefore: t1,\n\t\tNotAfter:  t1.Add(5 * time.Minute),\n\t\tPublicKey: pub,\n\t\tIsCA:      false,\n\t}\n\n\tnc, err := tbs.Sign(ca, cert.Curve_CURVE25519, signer)\n\trequire.NoError(t, err)\n\n\treturn nc, priv\n}\n\nfunc mustNebulaP256Cert(t *testing.T, name string, network netip.Prefix, groups []string, ca cert.Certificate, signer *ecdsa.PrivateKey) (cert.Certificate, crypto.Signer) {\n\tt.Helper()\n\n\tkey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)\n\trequire.NoError(t, err)\n\tecdhPriv, err := key.ECDH()\n\trequire.NoError(t, err)\n\n\tt1 := time.Now().Truncate(time.Second)\n\ttbs := &cert.TBSCertificate{\n\t\tVersion:   cert.Version1,\n\t\tCurve:     cert.Curve_P256,\n\t\tName:      name,\n\t\tNetworks:  []netip.Prefix{network},\n\t\tGroups:    groups,\n\t\tNotBefore: t1,\n\t\tNotAfter:  t1.Add(5 * time.Minute),\n\t\tPublicKey: ecdhPriv.PublicKey().Bytes(),\n\t\tIsCA:      false,\n\t}\n\n\tecdhSigner, err := signer.ECDH()\n\trequire.NoError(t, err)\n\n\tnc, err := tbs.Sign(ca, cert.Curve_P256, ecdhSigner.Bytes())\n\trequire.NoError(t, err)\n\n\treturn nc, key\n}\n\nfunc mustNebulaProvisioner(t *testing.T) (*Nebula, cert.Certificate, ed25519.PrivateKey) {\n\tt.Helper()\n\n\tnc, signer := mustNebulaCA(t)\n\tncPem, err := nc.MarshalPEM()\n\trequire.NoError(t, err)\n\tbTrue := true\n\tp := &Nebula{\n\t\tType:  TypeNebula.String(),\n\t\tName:  \"nebulous\",\n\t\tRoots: ncPem,\n\t\tClaims: &Claims{\n\t\t\tEnableSSHCA: &bTrue,\n\t\t},\n\t}\n\terr = p.Init(Config{\n\t\tClaims:    globalProvisionerClaims,\n\t\tAudiences: testAudiences,\n\t})\n\trequire.NoError(t, err)\n\n\treturn p, nc, signer\n}\n\nfunc mustNebulaP256Provisioner(t *testing.T) (*Nebula, cert.Certificate, *ecdsa.PrivateKey) {\n\tt.Helper()\n\n\tnc, signer := mustNebulaP256CA(t)\n\tncPem, err := nc.MarshalPEM()\n\trequire.NoError(t, err)\n\tbTrue := true\n\tp := &Nebula{\n\t\tType:  TypeNebula.String(),\n\t\tName:  \"nebulous\",\n\t\tRoots: ncPem,\n\t\tClaims: &Claims{\n\t\t\tEnableSSHCA: &bTrue,\n\t\t},\n\t}\n\terr = p.Init(Config{\n\t\tClaims:    globalProvisionerClaims,\n\t\tAudiences: testAudiences,\n\t})\n\trequire.NoError(t, err)\n\n\treturn p, nc, signer\n}\n\nfunc mustNebulaToken(t *testing.T, sub, iss, aud string, iat time.Time, sans []string, nc cert.Certificate, key crypto.Signer, algorithm jose.SignatureAlgorithm) string {\n\tt.Helper()\n\tncDer, err := nc.Marshal()\n\trequire.NoError(t, err)\n\n\tso := new(jose.SignerOptions)\n\tso.WithType(\"JWT\")\n\tso.WithHeader(NebulaCertHeader, ncDer)\n\n\tsig, err := jose.NewSigner(jose.SigningKey{Algorithm: algorithm, Key: key}, so)\n\trequire.NoError(t, err)\n\n\tid, err := randutil.ASCII(64)\n\trequire.NoError(t, err)\n\n\tclaims := struct {\n\t\tjose.Claims\n\t\tSANS []string `json:\"sans\"`\n\t}{\n\t\tClaims: jose.Claims{\n\t\t\tID:        id,\n\t\t\tSubject:   sub,\n\t\t\tIssuer:    iss,\n\t\t\tIssuedAt:  jose.NewNumericDate(iat),\n\t\t\tNotBefore: jose.NewNumericDate(iat),\n\t\t\tExpiry:    jose.NewNumericDate(iat.Add(5 * time.Minute)),\n\t\t\tAudience:  []string{aud},\n\t\t},\n\t\tSANS: sans,\n\t}\n\ttok, err := jose.Signed(sig).Claims(claims).CompactSerialize()\n\trequire.NoError(t, err)\n\n\treturn tok\n}\n\nfunc mustNebulaSSHToken(t *testing.T, sub, iss, aud string, iat time.Time, opts *SignSSHOptions, nc cert.Certificate, key crypto.Signer, algorithm jose.SignatureAlgorithm) string {\n\tt.Helper()\n\tncDer, err := nc.Marshal()\n\trequire.NoError(t, err)\n\n\tso := new(jose.SignerOptions)\n\tso.WithType(\"JWT\")\n\tso.WithHeader(NebulaCertHeader, ncDer)\n\n\tsig, err := jose.NewSigner(jose.SigningKey{Algorithm: algorithm, Key: key}, so)\n\trequire.NoError(t, err)\n\n\tid, err := randutil.ASCII(64)\n\trequire.NoError(t, err)\n\n\tclaims := struct {\n\t\tjose.Claims\n\t\tStep *stepPayload `json:\"step,omitempty\"`\n\t}{\n\t\tClaims: jose.Claims{\n\t\t\tID:        id,\n\t\t\tSubject:   sub,\n\t\t\tIssuer:    iss,\n\t\t\tIssuedAt:  jose.NewNumericDate(iat),\n\t\t\tNotBefore: jose.NewNumericDate(iat),\n\t\t\tExpiry:    jose.NewNumericDate(iat.Add(5 * time.Minute)),\n\t\t\tAudience:  []string{aud},\n\t\t},\n\t}\n\tif opts != nil {\n\t\tclaims.Step = &stepPayload{\n\t\t\tSSH: opts,\n\t\t}\n\t}\n\n\ttok, err := jose.Signed(sig).Claims(claims).CompactSerialize()\n\trequire.NoError(t, err)\n\n\treturn tok\n}\n\nfunc TestNebula_Init(t *testing.T) {\n\tnc, _ := mustNebulaCA(t)\n\tncPem, err := nc.MarshalPEM()\n\trequire.NoError(t, err)\n\texpiredNC, _ := mustExpiredNebulaCA(t)\n\texpiredPEM, err := expiredNC.MarshalPEM()\n\trequire.NoError(t, err)\n\texpiredPEM = append(expiredPEM, ncPem...) // needed so that regular error isn't triggered\n\n\tcfg := Config{\n\t\tClaims:    globalProvisionerClaims,\n\t\tAudiences: testAudiences,\n\t}\n\n\ttype fields struct {\n\t\tType    string\n\t\tName    string\n\t\tRoots   []byte\n\t\tClaims  *Claims\n\t\tOptions *Options\n\t}\n\ttype args struct {\n\t\tconfig Config\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tfields  fields\n\t\targs    args\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", fields{\"Nebula\", \"Nebulous\", ncPem, nil, nil}, args{cfg}, false},\n\t\t{\"ok with claims\", fields{\"Nebula\", \"Nebulous\", ncPem, &Claims{DefaultTLSDur: &Duration{Duration: time.Hour}}, nil}, args{cfg}, false},\n\t\t{\"ok with options\", fields{\"Nebula\", \"Nebulous\", ncPem, nil, &Options{X509: &X509Options{Template: x509util.DefaultLeafTemplate}}}, args{cfg}, false},\n\t\t{\"fail type\", fields{\"\", \"Nebulous\", ncPem, nil, nil}, args{cfg}, true},\n\t\t{\"fail name\", fields{\"Nebula\", \"\", ncPem, nil, nil}, args{cfg}, true},\n\t\t{\"fail root\", fields{\"Nebula\", \"Nebulous\", nil, nil, nil}, args{cfg}, true},\n\t\t{\"fail expired root\", fields{\"Nebula\", \"Nebulous\", expiredPEM, nil, nil}, args{cfg}, true},\n\t\t{\"fail bad root\", fields{\"Nebula\", \"Nebulous\", ncPem[:16], nil, nil}, args{cfg}, true},\n\t\t{\"fail bad claims\", fields{\"Nebula\", \"Nebulous\", ncPem, &Claims{\n\t\t\tMinTLSDur: &Duration{Duration: 0},\n\t\t}, nil}, args{cfg}, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tp := &Nebula{\n\t\t\t\tType:    tt.fields.Type,\n\t\t\t\tName:    tt.fields.Name,\n\t\t\t\tRoots:   tt.fields.Roots,\n\t\t\t\tClaims:  tt.fields.Claims,\n\t\t\t\tOptions: tt.fields.Options,\n\t\t\t}\n\t\t\tif err := p.Init(tt.args.config); (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"Nebula.Init() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNebula_GetID(t *testing.T) {\n\ttype fields struct {\n\t\tID   string\n\t\tName string\n\t}\n\ttests := []struct {\n\t\tname   string\n\t\tfields fields\n\t\twant   string\n\t}{\n\t\t{\"ok with id\", fields{\"1234\", \"nebulous\"}, \"1234\"},\n\t\t{\"ok with name\", fields{\"\", \"nebulous\"}, \"nebula/nebulous\"},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tp := &Nebula{\n\t\t\t\tID:   tt.fields.ID,\n\t\t\t\tName: tt.fields.Name,\n\t\t\t}\n\t\t\tif got := p.GetID(); got != tt.want {\n\t\t\t\tt.Errorf(\"Nebula.GetID() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNebula_GetIDForToken(t *testing.T) {\n\ttype fields struct {\n\t\tName string\n\t}\n\ttests := []struct {\n\t\tname   string\n\t\tfields fields\n\t\twant   string\n\t}{\n\t\t{\"ok\", fields{\"nebulous\"}, \"nebula/nebulous\"},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tp := &Nebula{\n\t\t\t\tName: tt.fields.Name,\n\t\t\t}\n\t\t\tif got := p.GetIDForToken(); got != tt.want {\n\t\t\t\tt.Errorf(\"Nebula.GetIDForToken() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNebula_GetTokenID(t *testing.T) {\n\tp, ca, signer := mustNebulaProvisioner(t)\n\tc1, priv := mustNebulaCert(t, \"test.lan\", mustNebulaPrefix(t, \"10.1.0.1/16\"), []string{\"test\"}, ca, signer)\n\tt1 := mustNebulaToken(t, \"test.lan\", p.Name, p.ctl.Audiences.Sign[0], now(), []string{\"test.lan\", \"10.1.0.1\"}, c1, priv, jose.XEdDSA)\n\t_, claims, err := parseToken(t1)\n\trequire.NoError(t, err)\n\n\ttype args struct {\n\t\ttoken string\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tp       *Nebula\n\t\targs    args\n\t\twant    string\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", p, args{t1}, claims.ID, false},\n\t\t{\"fail parse\", p, args{\"token\"}, \"\", true},\n\t\t{\"fail claims\", p, args{func() string {\n\t\t\tparts := strings.Split(t1, \".\")\n\n\t\t\treturn parts[0] + \".eyIifQ.\" + parts[1]\n\t\t}()}, \"\", true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := tt.p.GetTokenID(tt.args.token)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"Nebula.GetTokenID() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif got != tt.want {\n\t\t\t\tt.Errorf(\"Nebula.GetTokenID() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNebula_GetName(t *testing.T) {\n\ttype fields struct {\n\t\tName string\n\t}\n\ttests := []struct {\n\t\tname   string\n\t\tfields fields\n\t\twant   string\n\t}{\n\t\t{\"ok\", fields{\"nebulous\"}, \"nebulous\"},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tp := &Nebula{\n\t\t\t\tName: tt.fields.Name,\n\t\t\t}\n\t\t\tif got := p.GetName(); got != tt.want {\n\t\t\t\tt.Errorf(\"Nebula.GetName() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNebula_GetType(t *testing.T) {\n\ttype fields struct {\n\t\tType string\n\t}\n\ttests := []struct {\n\t\tname   string\n\t\tfields fields\n\t\twant   Type\n\t}{\n\t\t{\"ok\", fields{\"Nebula\"}, TypeNebula},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tp := &Nebula{\n\t\t\t\tType: tt.fields.Type,\n\t\t\t}\n\t\t\tif got := p.GetType(); got != tt.want {\n\t\t\t\tt.Errorf(\"Nebula.GetType() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNebula_GetEncryptedKey(t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\tp       *Nebula\n\t\twantKid string\n\t\twantKey string\n\t\twantOk  bool\n\t}{\n\t\t{\"ok\", &Nebula{}, \"\", \"\", false},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgotKid, gotKey, gotOk := tt.p.GetEncryptedKey()\n\t\t\tif gotKid != tt.wantKid {\n\t\t\t\tt.Errorf(\"Nebula.GetEncryptedKey() gotKid = %v, want %v\", gotKid, tt.wantKid)\n\t\t\t}\n\t\t\tif gotKey != tt.wantKey {\n\t\t\t\tt.Errorf(\"Nebula.GetEncryptedKey() gotKey = %v, want %v\", gotKey, tt.wantKey)\n\t\t\t}\n\t\t\tif gotOk != tt.wantOk {\n\t\t\t\tt.Errorf(\"Nebula.GetEncryptedKey() gotOk = %v, want %v\", gotOk, tt.wantOk)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNebula_AuthorizeSign(t *testing.T) {\n\tctx := context.TODO()\n\tp, ca, signer := mustNebulaProvisioner(t)\n\tcrt, priv := mustNebulaCert(t, \"test.lan\", mustNebulaPrefix(t, \"10.1.0.1/16\"), []string{\"test\"}, ca, signer)\n\tok := mustNebulaToken(t, \"test.lan\", p.Name, p.ctl.Audiences.Sign[0], now(), []string{\"test.lan\", \"10.1.0.1\"}, crt, priv, jose.XEdDSA)\n\tokNoSANs := mustNebulaToken(t, \"test.lan\", p.Name, p.ctl.Audiences.Sign[0], now(), nil, crt, priv, jose.XEdDSA)\n\n\tpBadOptions, _, _ := mustNebulaProvisioner(t)\n\tpBadOptions.caPool = p.caPool\n\tpBadOptions.Options = &Options{\n\t\tX509: &X509Options{\n\t\t\tTemplateData: []byte(`{\"\"}`),\n\t\t},\n\t}\n\n\ttype args struct {\n\t\tctx   context.Context\n\t\ttoken string\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tp       *Nebula\n\t\targs    args\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", p, args{ctx, ok}, false},\n\t\t{\"ok no sans\", p, args{ctx, okNoSANs}, false},\n\t\t{\"fail token\", p, args{ctx, \"token\"}, true},\n\t\t{\"fail template\", pBadOptions, args{ctx, ok}, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\t_, err := tt.p.AuthorizeSign(tt.args.ctx, tt.args.token)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"Nebula.AuthorizeSign() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNebula_AuthorizeSSHSign(t *testing.T) {\n\tctx := context.TODO()\n\t// Ok provisioner\n\tp, ca, signer := mustNebulaProvisioner(t)\n\tcrt, priv := mustNebulaCert(t, \"test.lan\", mustNebulaPrefix(t, \"10.1.0.1/16\"), []string{\"test\"}, ca, signer)\n\tok := mustNebulaSSHToken(t, \"test.lan\", p.Name, p.ctl.Audiences.SSHSign[0], now(), &SignSSHOptions{\n\t\tCertType:   \"host\",\n\t\tKeyID:      \"test.lan\",\n\t\tPrincipals: []string{\"test.lan\", \"10.1.0.1\"},\n\t}, crt, priv, jose.XEdDSA)\n\tokNoOptions := mustNebulaSSHToken(t, \"test.lan\", p.Name, p.ctl.Audiences.SSHSign[0], now(), nil, crt, priv, jose.XEdDSA)\n\tokWithValidity := mustNebulaSSHToken(t, \"test.lan\", p.Name, p.ctl.Audiences.SSHSign[0], now(), &SignSSHOptions{\n\t\tValidAfter:  NewTimeDuration(now().Add(1 * time.Hour)),\n\t\tValidBefore: NewTimeDuration(now().Add(10 * time.Hour)),\n\t}, crt, priv, jose.XEdDSA)\n\tfailUserCert := mustNebulaSSHToken(t, \"test.lan\", p.Name, p.ctl.Audiences.SSHSign[0], now(), &SignSSHOptions{\n\t\tCertType: \"user\",\n\t}, crt, priv, jose.XEdDSA)\n\tfailPrincipals := mustNebulaSSHToken(t, \"test.lan\", p.Name, p.ctl.Audiences.SSHSign[0], now(), &SignSSHOptions{\n\t\tCertType:   \"host\",\n\t\tKeyID:      \"test.lan\",\n\t\tPrincipals: []string{\"test.lan\", \"10.1.0.1\", \"foo.bar\"},\n\t}, crt, priv, jose.XEdDSA)\n\n\t// Provisioner with SSH disabled\n\tvar bFalse bool\n\tpDisabled, _, _ := mustNebulaProvisioner(t)\n\tpDisabled.caPool = p.caPool\n\tpDisabled.Claims.EnableSSHCA = &bFalse\n\n\t// Provisioner with bad templates\n\tpBadOptions, _, _ := mustNebulaProvisioner(t)\n\tpBadOptions.caPool = p.caPool\n\tpBadOptions.Options = &Options{\n\t\tSSH: &SSHOptions{\n\t\t\tTemplateData: []byte(`{\"\"}`),\n\t\t},\n\t}\n\n\ttype args struct {\n\t\tctx   context.Context\n\t\ttoken string\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tp       *Nebula\n\t\targs    args\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", p, args{ctx, ok}, false},\n\t\t{\"ok no options\", p, args{ctx, okNoOptions}, false},\n\t\t{\"ok with validity\", p, args{ctx, okWithValidity}, false},\n\t\t{\"fail token\", p, args{ctx, \"token\"}, true},\n\t\t{\"fail user\", p, args{ctx, failUserCert}, true},\n\t\t{\"fail principals\", p, args{ctx, failPrincipals}, true},\n\t\t{\"fail disabled\", pDisabled, args{ctx, ok}, true},\n\t\t{\"fail template\", pBadOptions, args{ctx, ok}, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\t_, err := tt.p.AuthorizeSSHSign(tt.args.ctx, tt.args.token)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"Nebula.AuthorizeSSHSign() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNebula_AuthorizeRenew(t *testing.T) {\n\tctx := context.TODO()\n\tnow := time.Now().Truncate(time.Second)\n\n\t// Ok provisioner\n\tp, _, _ := mustNebulaProvisioner(t)\n\n\t// Provisioner with renewal disabled\n\tbTrue := true\n\tpDisabled, _, _ := mustNebulaProvisioner(t)\n\tpDisabled.Claims.DisableRenewal = &bTrue\n\n\ttype args struct {\n\t\tctx context.Context\n\t\tcrt *x509.Certificate\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tp       *Nebula\n\t\targs    args\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", p, args{ctx, &x509.Certificate{\n\t\t\tNotBefore: now,\n\t\t\tNotAfter:  now.Add(time.Hour),\n\t\t}}, false},\n\t\t{\"fail disabled\", pDisabled, args{ctx, &x509.Certificate{\n\t\t\tNotBefore: now,\n\t\t\tNotAfter:  now.Add(time.Hour),\n\t\t}}, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif err := tt.p.AuthorizeRenew(tt.args.ctx, tt.args.crt); (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"Nebula.AuthorizeRenew() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNebula_AuthorizeRevoke(t *testing.T) {\n\tctx := context.TODO()\n\t// Ok provisioner\n\tp, ca, signer := mustNebulaProvisioner(t)\n\tcrt, priv := mustNebulaCert(t, \"test.lan\", mustNebulaPrefix(t, \"10.1.0.1/16\"), []string{\"test\"}, ca, signer)\n\tok := mustNebulaToken(t, \"test.lan\", p.Name, p.ctl.Audiences.Revoke[0], now(), nil, crt, priv, jose.XEdDSA)\n\n\t// Fail different CA\n\tnc, signer := mustNebulaCA(t)\n\tcrt, priv = mustNebulaCert(t, \"test.lan\", mustNebulaPrefix(t, \"10.1.0.1/16\"), []string{\"test\"}, nc, signer)\n\tfailToken := mustNebulaToken(t, \"test.lan\", p.Name, p.ctl.Audiences.Revoke[0], now(), nil, crt, priv, jose.XEdDSA)\n\n\ttype args struct {\n\t\tctx   context.Context\n\t\ttoken string\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tp       *Nebula\n\t\targs    args\n\t\twantErr bool\n\t}{\n\t\t{\"fail unauthorized\", p, args{ctx, ok}, true},\n\t\t{\"fail token\", p, args{ctx, failToken}, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif err := tt.p.AuthorizeRevoke(tt.args.ctx, tt.args.token); (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"Nebula.AuthorizeRevoke() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNebula_AuthorizeSSHRevoke(t *testing.T) {\n\tctx := context.TODO()\n\t// Ok provisioner\n\tp, ca, signer := mustNebulaProvisioner(t)\n\tcrt, priv := mustNebulaCert(t, \"test.lan\", mustNebulaPrefix(t, \"10.1.0.1/16\"), []string{\"test\"}, ca, signer)\n\tok := mustNebulaSSHToken(t, \"test.lan\", p.Name, p.ctl.Audiences.SSHRevoke[0], now(), nil, crt, priv, jose.XEdDSA)\n\n\t// Fail different CA\n\tnc, signer := mustNebulaCA(t)\n\tcrt, priv = mustNebulaCert(t, \"test.lan\", mustNebulaPrefix(t, \"10.1.0.1/16\"), []string{\"test\"}, nc, signer)\n\tfailToken := mustNebulaSSHToken(t, \"test.lan\", p.Name, p.ctl.Audiences.SSHRevoke[0], now(), nil, crt, priv, jose.XEdDSA)\n\n\t// Provisioner with SSH disabled\n\tvar bFalse bool\n\tpDisabled, _, _ := mustNebulaProvisioner(t)\n\tpDisabled.caPool = p.caPool\n\tpDisabled.Claims.EnableSSHCA = &bFalse\n\n\ttype args struct {\n\t\tctx   context.Context\n\t\ttoken string\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tp       *Nebula\n\t\targs    args\n\t\twantErr bool\n\t}{\n\t\t{\"fail unauthorized\", p, args{ctx, ok}, true},\n\t\t{\"fail token\", p, args{ctx, failToken}, true},\n\t\t{\"fail disabled\", pDisabled, args{ctx, ok}, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif err := tt.p.AuthorizeSSHRevoke(tt.args.ctx, tt.args.token); (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"Nebula.AuthorizeSSHRevoke() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNebula_AuthorizeSSHRenew(t *testing.T) {\n\tp, ca, signer := mustNebulaProvisioner(t)\n\tcrt, priv := mustNebulaCert(t, \"test.lan\", mustNebulaPrefix(t, \"10.1.0.1/16\"), []string{\"test\"}, ca, signer)\n\tt1 := mustNebulaSSHToken(t, \"test.lan\", p.Name, p.ctl.Audiences.SSHRenew[0], now(), nil, crt, priv, jose.XEdDSA)\n\n\ttype args struct {\n\t\tctx   context.Context\n\t\ttoken string\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tp       *Nebula\n\t\targs    args\n\t\twant    *ssh.Certificate\n\t\twantErr bool\n\t}{\n\t\t{\"fail\", p, args{context.TODO(), t1}, nil, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := tt.p.AuthorizeSSHRenew(tt.args.ctx, tt.args.token)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"Nebula.AuthorizeSSHRenew() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tassert.Equal(t, tt.want, got)\n\t\t})\n\t}\n}\n\nfunc TestNebula_AuthorizeSSHRekey(t *testing.T) {\n\tp, ca, signer := mustNebulaProvisioner(t)\n\tcrt, priv := mustNebulaCert(t, \"test.lan\", mustNebulaPrefix(t, \"10.1.0.1/16\"), []string{\"test\"}, ca, signer)\n\tt1 := mustNebulaSSHToken(t, \"test.lan\", p.Name, p.ctl.Audiences.SSHRekey[0], now(), nil, crt, priv, jose.XEdDSA)\n\n\ttype args struct {\n\t\tctx   context.Context\n\t\ttoken string\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tp       *Nebula\n\t\targs    args\n\t\twant    *ssh.Certificate\n\t\twant1   []SignOption\n\t\twantErr bool\n\t}{\n\t\t{\"fail\", p, args{context.TODO(), t1}, nil, nil, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, got1, err := tt.p.AuthorizeSSHRekey(tt.args.ctx, tt.args.token)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"Nebula.AuthorizeSSHRekey() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tassert.Equal(t, tt.want, got)\n\t\t\tassert.Equal(t, tt.want1, got1)\n\t\t})\n\t}\n}\n\nfunc TestNebula_authorizeToken(t *testing.T) {\n\tt1 := now()\n\tp, ca, signer := mustNebulaProvisioner(t)\n\tcrt, priv := mustNebulaCert(t, \"test.lan\", mustNebulaPrefix(t, \"10.1.0.1/16\"), []string{\"test\"}, ca, signer)\n\tok := mustNebulaToken(t, \"test.lan\", p.Name, p.ctl.Audiences.Sign[0], t1, []string{\"10.1.0.1\"}, crt, priv, jose.XEdDSA)\n\tokNoSANs := mustNebulaToken(t, \"test.lan\", p.Name, p.ctl.Audiences.Sign[0], t1, nil, crt, priv, jose.XEdDSA)\n\tokSSH := mustNebulaSSHToken(t, \"test.lan\", p.Name, p.ctl.Audiences.SSHSign[0], t1, &SignSSHOptions{\n\t\tCertType:   \"host\",\n\t\tKeyID:      \"test.lan\",\n\t\tPrincipals: []string{\"test.lan\"},\n\t}, crt, priv, jose.XEdDSA)\n\tokSSHNoOptions := mustNebulaSSHToken(t, \"test.lan\", p.Name, p.ctl.Audiences.SSHSign[0], t1, nil, crt, priv, jose.XEdDSA)\n\n\t// Token with errors\n\tfailNotBefore := mustNebulaToken(t, \"test.lan\", p.Name, p.ctl.Audiences.Sign[0], t1.Add(1*time.Hour), []string{\"10.1.0.1\"}, crt, priv, jose.XEdDSA)\n\tfailIssuer := mustNebulaToken(t, \"test.lan\", \"foo\", p.ctl.Audiences.Sign[0], t1, []string{\"10.1.0.1\"}, crt, priv, jose.XEdDSA)\n\tfailAudience := mustNebulaToken(t, \"test.lan\", p.Name, \"foo\", t1, []string{\"10.1.0.1\"}, crt, priv, jose.XEdDSA)\n\tfailSubject := mustNebulaToken(t, \"\", p.Name, p.ctl.Audiences.Sign[0], t1, []string{\"10.1.0.1\"}, crt, priv, jose.XEdDSA)\n\n\t// Not a nebula token\n\tjwk, err := generateJSONWebKey()\n\trequire.NoError(t, err)\n\tsimpleToken, err := generateSimpleToken(\"iss\", \"aud\", jwk)\n\trequire.NoError(t, err)\n\n\t// Provisioner with a different CA\n\tp2, _, _ := mustNebulaProvisioner(t)\n\n\tx509Claims := jose.Claims{\n\t\tID:        \"[REPLACEME]\",\n\t\tSubject:   \"test.lan\",\n\t\tIssuer:    p.Name,\n\t\tIssuedAt:  jose.NewNumericDate(t1),\n\t\tNotBefore: jose.NewNumericDate(t1),\n\t\tExpiry:    jose.NewNumericDate(t1.Add(5 * time.Minute)),\n\t\tAudience:  []string{p.ctl.Audiences.Sign[0]},\n\t}\n\tsshClaims := jose.Claims{\n\t\tID:        \"[REPLACEME]\",\n\t\tSubject:   \"test.lan\",\n\t\tIssuer:    p.Name,\n\t\tIssuedAt:  jose.NewNumericDate(t1),\n\t\tNotBefore: jose.NewNumericDate(t1),\n\t\tExpiry:    jose.NewNumericDate(t1.Add(5 * time.Minute)),\n\t\tAudience:  []string{p.ctl.Audiences.SSHSign[0]},\n\t}\n\n\ttype args struct {\n\t\ttoken     string\n\t\taudiences []string\n\t}\n\ttests := []struct {\n\t\tname       string\n\t\tp          *Nebula\n\t\targs       args\n\t\twantClaims *jwtPayload\n\t\twantErr    bool\n\t}{\n\t\t{\"ok x509\", p, args{ok, p.ctl.Audiences.Sign}, &jwtPayload{\n\t\t\tClaims: x509Claims,\n\t\t\tSANs:   []string{\"10.1.0.1\"},\n\t\t}, false},\n\t\t{\"ok x509 no sans\", p, args{okNoSANs, p.ctl.Audiences.Sign}, &jwtPayload{\n\t\t\tClaims: x509Claims,\n\t\t}, false},\n\t\t{\"ok ssh\", p, args{okSSH, p.ctl.Audiences.SSHSign}, &jwtPayload{\n\t\t\tClaims: sshClaims,\n\t\t\tStep: &stepPayload{\n\t\t\t\tSSH: &SignSSHOptions{\n\t\t\t\t\tCertType:   \"host\",\n\t\t\t\t\tKeyID:      \"test.lan\",\n\t\t\t\t\tPrincipals: []string{\"test.lan\"},\n\t\t\t\t},\n\t\t\t},\n\t\t}, false},\n\t\t{\"ok ssh no principals\", p, args{okSSHNoOptions, p.ctl.Audiences.SSHSign}, &jwtPayload{\n\t\t\tClaims: sshClaims,\n\t\t}, false},\n\t\t{\"fail parse\", p, args{\"bad.token\", p.ctl.Audiences.Sign}, nil, true},\n\t\t{\"fail header\", p, args{simpleToken, p.ctl.Audiences.Sign}, nil, true},\n\t\t{\"fail verify\", p2, args{ok, p.ctl.Audiences.Sign}, nil, true},\n\t\t{\"fail claims nbf\", p, args{failNotBefore, p.ctl.Audiences.Sign}, nil, true},\n\t\t{\"fail claims iss\", p, args{failIssuer, p.ctl.Audiences.Sign}, nil, true},\n\t\t{\"fail claims aud\", p, args{failAudience, p.ctl.Audiences.Sign}, nil, true},\n\t\t{\"fail claims sub\", p, args{failSubject, p.ctl.Audiences.Sign}, nil, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, got1, err := tt.p.authorizeToken(tt.args.token, tt.args.audiences)\n\t\t\tif tt.wantErr {\n\t\t\t\tassert.Error(t, err)\n\t\t\t\tassert.Nil(t, got)\n\t\t\t\tassert.Nil(t, got1)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif got1 != nil && tt.wantClaims != nil {\n\t\t\t\ttt.wantClaims.ID = got1.ID\n\t\t\t}\n\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.NotNil(t, got)\n\t\t\tassert.Equal(t, tt.wantClaims, got1)\n\t\t})\n\t}\n}\n\nfunc TestNebula_authorizeToken_P256(t *testing.T) {\n\tt1 := now()\n\tp, ca, signer := mustNebulaP256Provisioner(t)\n\tcrt, priv := mustNebulaP256Cert(t, \"test.lan\", mustNebulaPrefix(t, \"10.1.0.1/16\"), []string{\"test\"}, ca, signer)\n\tok := mustNebulaToken(t, \"test.lan\", p.Name, p.ctl.Audiences.Sign[0], t1, []string{\"10.1.0.1\"}, crt, priv, jose.ES256)\n\tokNoSANs := mustNebulaToken(t, \"test.lan\", p.Name, p.ctl.Audiences.Sign[0], t1, nil, crt, priv, jose.ES256)\n\tokSSH := mustNebulaSSHToken(t, \"test.lan\", p.Name, p.ctl.Audiences.SSHSign[0], t1, &SignSSHOptions{\n\t\tCertType:   \"host\",\n\t\tKeyID:      \"test.lan\",\n\t\tPrincipals: []string{\"test.lan\"},\n\t}, crt, priv, jose.ES256)\n\tokSSHNoOptions := mustNebulaSSHToken(t, \"test.lan\", p.Name, p.ctl.Audiences.SSHSign[0], t1, nil, crt, priv, jose.ES256)\n\n\t// Token with errors\n\tfailNotBefore := mustNebulaToken(t, \"test.lan\", p.Name, p.ctl.Audiences.Sign[0], t1.Add(1*time.Hour), []string{\"10.1.0.1\"}, crt, priv, jose.ES256)\n\tfailIssuer := mustNebulaToken(t, \"test.lan\", \"foo\", p.ctl.Audiences.Sign[0], t1, []string{\"10.1.0.1\"}, crt, priv, jose.ES256)\n\tfailAudience := mustNebulaToken(t, \"test.lan\", p.Name, \"foo\", t1, []string{\"10.1.0.1\"}, crt, priv, jose.ES256)\n\tfailSubject := mustNebulaToken(t, \"\", p.Name, p.ctl.Audiences.Sign[0], t1, []string{\"10.1.0.1\"}, crt, priv, jose.ES256)\n\n\t// Not a nebula token\n\tjwk, err := generateJSONWebKey()\n\trequire.NoError(t, err)\n\tsimpleToken, err := generateSimpleToken(\"iss\", \"aud\", jwk)\n\trequire.NoError(t, err)\n\n\t// Provisioner with a different CA\n\tp2, _, _ := mustNebulaP256Provisioner(t)\n\n\tx509Claims := jose.Claims{\n\t\tID:        \"[REPLACEME]\",\n\t\tSubject:   \"test.lan\",\n\t\tIssuer:    p.Name,\n\t\tIssuedAt:  jose.NewNumericDate(t1),\n\t\tNotBefore: jose.NewNumericDate(t1),\n\t\tExpiry:    jose.NewNumericDate(t1.Add(5 * time.Minute)),\n\t\tAudience:  []string{p.ctl.Audiences.Sign[0]},\n\t}\n\tsshClaims := jose.Claims{\n\t\tID:        \"[REPLACEME]\",\n\t\tSubject:   \"test.lan\",\n\t\tIssuer:    p.Name,\n\t\tIssuedAt:  jose.NewNumericDate(t1),\n\t\tNotBefore: jose.NewNumericDate(t1),\n\t\tExpiry:    jose.NewNumericDate(t1.Add(5 * time.Minute)),\n\t\tAudience:  []string{p.ctl.Audiences.SSHSign[0]},\n\t}\n\n\ttype args struct {\n\t\ttoken     string\n\t\taudiences []string\n\t}\n\ttests := []struct {\n\t\tname       string\n\t\tp          *Nebula\n\t\targs       args\n\t\twantClaims *jwtPayload\n\t\twantErr    bool\n\t}{\n\t\t{\"ok x509\", p, args{ok, p.ctl.Audiences.Sign}, &jwtPayload{\n\t\t\tClaims: x509Claims,\n\t\t\tSANs:   []string{\"10.1.0.1\"},\n\t\t}, false},\n\t\t{\"ok x509 no sans\", p, args{okNoSANs, p.ctl.Audiences.Sign}, &jwtPayload{\n\t\t\tClaims: x509Claims,\n\t\t}, false},\n\t\t{\"ok ssh\", p, args{okSSH, p.ctl.Audiences.SSHSign}, &jwtPayload{\n\t\t\tClaims: sshClaims,\n\t\t\tStep: &stepPayload{\n\t\t\t\tSSH: &SignSSHOptions{\n\t\t\t\t\tCertType:   \"host\",\n\t\t\t\t\tKeyID:      \"test.lan\",\n\t\t\t\t\tPrincipals: []string{\"test.lan\"},\n\t\t\t\t},\n\t\t\t},\n\t\t}, false},\n\t\t{\"ok ssh no principals\", p, args{okSSHNoOptions, p.ctl.Audiences.SSHSign}, &jwtPayload{\n\t\t\tClaims: sshClaims,\n\t\t}, false},\n\t\t{\"fail parse\", p, args{\"bad.token\", p.ctl.Audiences.Sign}, nil, true},\n\t\t{\"fail header\", p, args{simpleToken, p.ctl.Audiences.Sign}, nil, true},\n\t\t{\"fail verify\", p2, args{ok, p.ctl.Audiences.Sign}, nil, true},\n\t\t{\"fail claims nbf\", p, args{failNotBefore, p.ctl.Audiences.Sign}, nil, true},\n\t\t{\"fail claims iss\", p, args{failIssuer, p.ctl.Audiences.Sign}, nil, true},\n\t\t{\"fail claims aud\", p, args{failAudience, p.ctl.Audiences.Sign}, nil, true},\n\t\t{\"fail claims sub\", p, args{failSubject, p.ctl.Audiences.Sign}, nil, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, got1, err := tt.p.authorizeToken(tt.args.token, tt.args.audiences)\n\t\t\tif tt.wantErr {\n\t\t\t\tassert.Error(t, err)\n\t\t\t\tassert.Nil(t, got)\n\t\t\t\tassert.Nil(t, got1)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif got1 != nil && tt.wantClaims != nil {\n\t\t\t\ttt.wantClaims.ID = got1.ID\n\t\t\t}\n\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.NotNil(t, got)\n\t\t\tassert.Equal(t, tt.wantClaims, got1)\n\t\t})\n\t}\n}\n\nfunc Test_nebulaSANsValidator_Valid(t *testing.T) {\n\tprefix := mustNebulaPrefix(t, \"10.1.2.3/16\")\n\ttype fields struct {\n\t\tName     string\n\t\tNetworks []netip.Prefix\n\t}\n\ttype args struct {\n\t\treq *x509.CertificateRequest\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tfields  fields\n\t\targs    args\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", fields{\"dns.name\", []netip.Prefix{prefix}}, args{&x509.CertificateRequest{\n\t\t\tDNSNames:    []string{\"dns.name\"},\n\t\t\tIPAddresses: []net.IP{net.IPv4(10, 1, 2, 3)},\n\t\t}}, false},\n\t\t{\"ok name only\", fields{\"dns.name\", []netip.Prefix{prefix}}, args{&x509.CertificateRequest{\n\t\t\tDNSNames: []string{\"dns.name\"},\n\t\t}}, false},\n\t\t{\"ok ip only\", fields{\"dns.name\", []netip.Prefix{prefix}}, args{&x509.CertificateRequest{\n\t\t\tIPAddresses: []net.IP{net.IPv4(10, 1, 2, 3)},\n\t\t}}, false},\n\t\t{\"ok email name\", fields{\"jane@doe.org\", []netip.Prefix{prefix}}, args{&x509.CertificateRequest{\n\t\t\tEmailAddresses: []string{\"jane@doe.org\"},\n\t\t\tIPAddresses:    []net.IP{net.IPv4(10, 1, 2, 3)},\n\t\t}}, false},\n\t\t{\"ok uri name\", fields{\"urn:foobar\", []netip.Prefix{prefix}}, args{&x509.CertificateRequest{\n\t\t\tURIs:        []*url.URL{{Scheme: \"urn\", Opaque: \"foobar\"}},\n\t\t\tIPAddresses: []net.IP{net.IPv4(10, 1, 2, 3)},\n\t\t}}, false},\n\t\t{\"ok ip name\", fields{\"127.0.0.1\", []netip.Prefix{prefix}}, args{&x509.CertificateRequest{\n\t\t\tIPAddresses: []net.IP{net.IPv4(127, 0, 0, 1), net.IPv4(10, 1, 2, 3)},\n\t\t}}, false},\n\t\t{\"ok multiple ips\", fields{\"dns.name\", []netip.Prefix{prefix, mustNebulaPrefix(t, \"10.2.2.3/8\")}}, args{&x509.CertificateRequest{\n\t\t\tDNSNames:    []string{\"dns.name\"},\n\t\t\tIPAddresses: []net.IP{net.IPv4(10, 1, 2, 3), net.IPv4(10, 2, 2, 3)},\n\t\t}}, false},\n\t\t{\"fail dns\", fields{\"fail.name\", []netip.Prefix{prefix}}, args{&x509.CertificateRequest{\n\t\t\tDNSNames:    []string{\"dns.name\"},\n\t\t\tIPAddresses: []net.IP{net.IPv4(10, 1, 2, 3)},\n\t\t}}, true},\n\t\t{\"fail email\", fields{\"fail@doe.org\", []netip.Prefix{prefix}}, args{&x509.CertificateRequest{\n\t\t\tEmailAddresses: []string{\"jane@doe.org\"},\n\t\t\tIPAddresses:    []net.IP{net.IPv4(10, 1, 2, 3)},\n\t\t}}, true},\n\t\t{\"fail uri\", fields{\"urn:barfoo\", []netip.Prefix{prefix}}, args{&x509.CertificateRequest{\n\t\t\tURIs:        []*url.URL{{Scheme: \"urn\", Opaque: \"foobar\"}},\n\t\t\tIPAddresses: []net.IP{net.IPv4(10, 1, 2, 3)},\n\t\t}}, true},\n\t\t{\"fail ip\", fields{\"127.0.0.1\", []netip.Prefix{prefix}}, args{&x509.CertificateRequest{\n\t\t\tIPAddresses: []net.IP{net.IPv4(10, 1, 2, 1), net.IPv4(10, 1, 2, 3)},\n\t\t}}, true},\n\t\t{\"fail nebula ip\", fields{\"dns.name\", []netip.Prefix{prefix}}, args{&x509.CertificateRequest{\n\t\t\tDNSNames:    []string{\"dns.name\"},\n\t\t\tIPAddresses: []net.IP{net.IPv4(10, 2, 2, 3)},\n\t\t}}, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tv := nebulaSANsValidator{\n\t\t\t\tName:     tt.fields.Name,\n\t\t\t\tNetworks: tt.fields.Networks,\n\t\t\t}\n\t\t\tif err := v.Valid(tt.args.req); (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"nebulaSANsValidator.Valid() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_nebulaPrincipalsValidator_Valid(t *testing.T) {\n\tprefix := mustNebulaPrefix(t, \"10.1.2.3/16\")\n\n\ttype fields struct {\n\t\tName     string\n\t\tNetworks []netip.Prefix\n\t}\n\ttype args struct {\n\t\tgot SignSSHOptions\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tfields  fields\n\t\targs    args\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", fields{\"dns.name\", []netip.Prefix{prefix}}, args{SignSSHOptions{\n\t\t\tPrincipals: []string{\"dns.name\", \"10.1.2.3\"},\n\t\t}}, false},\n\t\t{\"ok name\", fields{\"dns.name\", []netip.Prefix{prefix}}, args{SignSSHOptions{\n\t\t\tPrincipals: []string{\"dns.name\"},\n\t\t}}, false},\n\t\t{\"ok ip\", fields{\"dns.name\", []netip.Prefix{prefix}}, args{SignSSHOptions{\n\t\t\tPrincipals: []string{\"10.1.2.3\"},\n\t\t}}, false},\n\t\t{\"fail name\", fields{\"dns.name\", []netip.Prefix{prefix}}, args{SignSSHOptions{\n\t\t\tPrincipals: []string{\"foo.name\", \"10.1.2.3\"},\n\t\t}}, true},\n\t\t{\"fail ip\", fields{\"dns.name\", []netip.Prefix{prefix}}, args{SignSSHOptions{\n\t\t\tPrincipals: []string{\"dns.name\", \"10.2.2.3\"},\n\t\t}}, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tv := nebulaPrincipalsValidator{\n\t\t\t\tName:     tt.fields.Name,\n\t\t\t\tNetworks: tt.fields.Networks,\n\t\t\t}\n\t\t\tif err := v.Valid(tt.args.got); (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"nebulaPrincipalsValidator.Valid() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "authority/provisioner/noop.go",
    "content": "package provisioner\n\nimport (\n\t\"context\"\n\t\"crypto/x509\"\n\n\t\"golang.org/x/crypto/ssh\"\n)\n\n// noop provisioners is a provisioner that accepts anything.\ntype noop struct{}\n\nfunc (p *noop) GetID() string {\n\treturn \"noop\"\n}\n\nfunc (p *noop) GetIDForToken() string {\n\treturn \"noop\"\n}\n\nfunc (p *noop) GetTokenID(string) (string, error) {\n\treturn \"\", nil\n}\n\nfunc (p *noop) GetName() string {\n\treturn \"noop\"\n}\nfunc (p *noop) GetType() Type {\n\treturn noopType\n}\n\nfunc (p *noop) GetEncryptedKey() (kid, key string, ok bool) {\n\treturn \"\", \"\", false\n}\n\nfunc (p *noop) Init(Config) error {\n\treturn nil\n}\n\nfunc (p *noop) AuthorizeSign(context.Context, string) ([]SignOption, error) {\n\treturn []SignOption{p}, nil\n}\n\nfunc (p *noop) AuthorizeRenew(context.Context, *x509.Certificate) error {\n\treturn nil\n}\n\nfunc (p *noop) AuthorizeRevoke(context.Context, string) error {\n\treturn nil\n}\n\nfunc (p *noop) AuthorizeSSHSign(context.Context, string) ([]SignOption, error) {\n\treturn []SignOption{p}, nil\n}\n\nfunc (p *noop) AuthorizeSSHRenew(context.Context, string) (*ssh.Certificate, error) {\n\t//nolint:nilnil // fine for noop\n\treturn nil, nil\n}\n\nfunc (p *noop) AuthorizeSSHRevoke(context.Context, string) error {\n\treturn nil\n}\n\nfunc (p *noop) AuthorizeSSHRekey(context.Context, string) (*ssh.Certificate, []SignOption, error) {\n\treturn nil, []SignOption{}, nil\n}\n"
  },
  {
    "path": "authority/provisioner/noop_test.go",
    "content": "package provisioner\n\nimport (\n\t\"context\"\n\t\"crypto/x509\"\n\t\"testing\"\n\n\t\"github.com/smallstep/assert\"\n)\n\nfunc Test_noop(t *testing.T) {\n\tp := noop{}\n\tassert.Equals(t, \"noop\", p.GetID())\n\tassert.Equals(t, \"noop\", p.GetName())\n\tassert.Equals(t, noopType, p.GetType())\n\tassert.Equals(t, nil, p.Init(Config{}))\n\tassert.Equals(t, nil, p.AuthorizeRenew(context.Background(), &x509.Certificate{}))\n\tassert.Equals(t, nil, p.AuthorizeRevoke(context.Background(), \"foo\"))\n\n\tkid, key, ok := p.GetEncryptedKey()\n\tassert.Equals(t, \"\", kid)\n\tassert.Equals(t, \"\", key)\n\tassert.Equals(t, false, ok)\n\n\tctx := NewContextWithMethod(context.Background(), SignMethod)\n\tsigOptions, err := p.AuthorizeSign(ctx, \"foo\")\n\tassert.Equals(t, []SignOption{&p}, sigOptions)\n\tassert.Equals(t, nil, err)\n}\n"
  },
  {
    "path": "authority/provisioner/oidc.go",
    "content": "package provisioner\n\nimport (\n\t\"context\"\n\t\"crypto/x509\"\n\t\"encoding/json\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"path\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n\n\t\"github.com/smallstep/linkedca\"\n\t\"go.step.sm/crypto/jose\"\n\t\"go.step.sm/crypto/sshutil\"\n\t\"go.step.sm/crypto/x509util\"\n\n\t\"github.com/smallstep/certificates/errs\"\n)\n\n// openIDConfiguration contains the necessary properties in the\n// `/.well-known/openid-configuration` document.\ntype openIDConfiguration struct {\n\tIssuer    string `json:\"issuer\"`\n\tJWKSetURI string `json:\"jwks_uri\"`\n}\n\n// Validate validates the values in a well-known OpenID configuration endpoint.\nfunc (c openIDConfiguration) Validate() error {\n\tswitch {\n\tcase c.Issuer == \"\":\n\t\treturn errors.New(\"issuer cannot be empty\")\n\tcase c.JWKSetURI == \"\":\n\t\treturn errors.New(\"jwks_uri cannot be empty\")\n\tdefault:\n\t\treturn nil\n\t}\n}\n\n// openIDPayload represents the fields on the id_token JWT payload.\ntype openIDPayload struct {\n\tjose.Claims\n\tAtHash          string   `json:\"at_hash\"`\n\tAuthorizedParty string   `json:\"azp\"`\n\tEmail           string   `json:\"email\"`\n\tEmailVerified   bool     `json:\"email_verified\"`\n\tHd              string   `json:\"hd\"`\n\tNonce           string   `json:\"nonce\"`\n\tGroups          []string `json:\"groups\"`\n}\n\nfunc (o *openIDPayload) IsAdmin(admins []string) bool {\n\tif o.Email != \"\" {\n\t\temail := sanitizeEmail(o.Email)\n\t\tfor _, e := range admins {\n\t\t\tif email == sanitizeEmail(e) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\t}\n\n\t// The groups and emails can be in the same array for now, but consider\n\t// making a specialized option later.\n\tfor _, name := range o.Groups {\n\t\tfor _, admin := range admins {\n\t\t\tif name == admin {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\t}\n\n\treturn false\n}\n\n// OIDC represents an OAuth 2.0 OpenID Connect provider.\n//\n// ClientSecret is mandatory, but it can be an empty string.\ntype OIDC struct {\n\t*base\n\tID                    string   `json:\"-\"`\n\tType                  string   `json:\"type\"`\n\tName                  string   `json:\"name\"`\n\tClientID              string   `json:\"clientID\"`\n\tClientSecret          string   `json:\"clientSecret\"`\n\tConfigurationEndpoint string   `json:\"configurationEndpoint\"`\n\tTenantID              string   `json:\"tenantID,omitempty\"`\n\tAdmins                []string `json:\"admins,omitempty\"`\n\tDomains               []string `json:\"domains,omitempty\"`\n\tGroups                []string `json:\"groups,omitempty\"`\n\tListenAddress         string   `json:\"listenAddress,omitempty\"`\n\tClaims                *Claims  `json:\"claims,omitempty\"`\n\tOptions               *Options `json:\"options,omitempty\"`\n\tScopes                []string `json:\"scopes,omitempty\"`\n\tAuthParams            []string `json:\"authParams,omitempty\"`\n\tconfiguration         openIDConfiguration\n\tkeyStore              *keyStore\n\tctl                   *Controller\n}\n\nfunc sanitizeEmail(email string) string {\n\tif i := strings.LastIndex(email, \"@\"); i >= 0 {\n\t\temail = email[:i] + strings.ToLower(email[i:])\n\t}\n\treturn email\n}\n\n// GetID returns the provisioner unique identifier, the OIDC provisioner the\n// uses the clientID for this.\nfunc (o *OIDC) GetID() string {\n\tif o.ID != \"\" {\n\t\treturn o.ID\n\t}\n\treturn o.GetIDForToken()\n}\n\n// GetIDForToken returns an identifier that will be used to load the provisioner\n// from a token.\nfunc (o *OIDC) GetIDForToken() string {\n\treturn o.ClientID\n}\n\n// GetTokenID returns the provisioner unique identifier, the OIDC provisioner the\n// uses the clientID for this.\nfunc (o *OIDC) GetTokenID(ott string) (string, error) {\n\t// Validate payload\n\ttoken, err := jose.ParseSigned(ott)\n\tif err != nil {\n\t\treturn \"\", errors.Wrap(err, \"error parsing token\")\n\t}\n\n\t// Get claims w/out verification. We need to look up the provisioner\n\t// key in order to verify the claims and we need the issuer from the claims\n\t// before we can look up the provisioner.\n\tvar claims openIDPayload\n\tif err = token.UnsafeClaimsWithoutVerification(&claims); err != nil {\n\t\treturn \"\", errors.Wrap(err, \"error verifying claims\")\n\t}\n\treturn claims.Nonce, nil\n}\n\n// GetName returns the name of the provisioner.\nfunc (o *OIDC) GetName() string {\n\treturn o.Name\n}\n\n// GetType returns the type of provisioner.\nfunc (o *OIDC) GetType() Type {\n\treturn TypeOIDC\n}\n\n// GetEncryptedKey is not available in an OIDC provisioner.\nfunc (o *OIDC) GetEncryptedKey() (kid, key string, ok bool) {\n\treturn \"\", \"\", false\n}\n\n// Init validates and initializes the OIDC provider.\nfunc (o *OIDC) Init(config Config) (err error) {\n\tswitch {\n\tcase o.Type == \"\":\n\t\treturn errors.New(\"type cannot be empty\")\n\tcase o.Name == \"\":\n\t\treturn errors.New(\"name cannot be empty\")\n\tcase o.ClientID == \"\":\n\t\treturn errors.New(\"clientID cannot be empty\")\n\tcase o.ConfigurationEndpoint == \"\":\n\t\treturn errors.New(\"configurationEndpoint cannot be empty\")\n\t}\n\n\t// Validate listenAddress if given\n\tif o.ListenAddress != \"\" {\n\t\tif _, _, err := net.SplitHostPort(o.ListenAddress); err != nil {\n\t\t\treturn errors.Wrap(err, \"error parsing listenAddress\")\n\t\t}\n\t}\n\n\t// Decode and validate openid-configuration endpoint\n\tu, err := url.Parse(o.ConfigurationEndpoint)\n\tif err != nil {\n\t\treturn errors.Wrapf(err, \"error parsing %s\", o.ConfigurationEndpoint)\n\t}\n\tif !strings.Contains(u.Path, \"/.well-known/openid-configuration\") {\n\t\tu.Path = path.Join(u.Path, \"/.well-known/openid-configuration\")\n\t}\n\n\t// Initialize the common provisioner controller\n\to.ctl, err = NewController(o, o.Claims, config, o.Options)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Decode and validate openid-configuration\n\thttpClient := o.ctl.GetHTTPClient()\n\tif err := getAndDecode(httpClient, u.String(), &o.configuration); err != nil {\n\t\treturn err\n\t}\n\tif err := o.configuration.Validate(); err != nil {\n\t\treturn errors.Wrapf(err, \"error parsing %s\", o.ConfigurationEndpoint)\n\t}\n\n\t// Replace {tenantid} with the configured one\n\tif o.TenantID != \"\" {\n\t\to.configuration.Issuer = strings.ReplaceAll(o.configuration.Issuer, \"{tenantid}\", o.TenantID)\n\t}\n\n\t// Get JWK key set\n\to.keyStore, err = newKeyStore(httpClient, o.configuration.JWKSetURI)\n\treturn\n}\n\n// ValidatePayload validates the given token payload.\nfunc (o *OIDC) ValidatePayload(p openIDPayload) error {\n\t// According to \"rfc7519 JSON Web Token\" acceptable skew should be no more\n\t// than a few minutes.\n\tif err := p.ValidateWithLeeway(jose.Expected{\n\t\tIssuer:   o.configuration.Issuer,\n\t\tAudience: jose.Audience{o.ClientID},\n\t\tTime:     time.Now().UTC(),\n\t}, time.Minute); err != nil {\n\t\treturn errs.Wrap(http.StatusUnauthorized, err, \"validatePayload: failed to validate oidc token payload\")\n\t}\n\n\t// Validate azp if present\n\tif p.AuthorizedParty != \"\" && p.AuthorizedParty != o.ClientID {\n\t\treturn errs.Unauthorized(\"validatePayload: failed to validate oidc token payload: invalid azp\")\n\t}\n\n\t// Validate domains (case-insensitive)\n\tif p.Email != \"\" && len(o.Domains) > 0 && !p.IsAdmin(o.Admins) {\n\t\temail := sanitizeEmail(p.Email)\n\t\tvar found bool\n\t\tfor _, d := range o.Domains {\n\t\t\tif strings.HasSuffix(email, \"@\"+strings.ToLower(d)) {\n\t\t\t\tfound = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif !found {\n\t\t\treturn errs.Unauthorized(\"validatePayload: failed to validate oidc token payload: email %q is not allowed\", p.Email)\n\t\t}\n\t}\n\n\t// Filter by oidc group claim\n\tif len(o.Groups) > 0 {\n\t\tvar found bool\n\t\tfor _, group := range o.Groups {\n\t\t\tfor _, g := range p.Groups {\n\t\t\t\tif g == group {\n\t\t\t\t\tfound = true\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif !found {\n\t\t\treturn errs.Unauthorized(\"validatePayload: oidc token payload validation failed: invalid group\")\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// authorizeToken applies the most common provisioner authorization claims,\n// leaving the rest to context specific methods.\nfunc (o *OIDC) authorizeToken(token string) (*openIDPayload, error) {\n\tjwt, err := jose.ParseSigned(token)\n\tif err != nil {\n\t\treturn nil, errs.Wrap(http.StatusUnauthorized, err,\n\t\t\t\"oidc.AuthorizeToken; error parsing oidc token\")\n\t}\n\n\t// Parse claims to get the kid\n\tvar claims openIDPayload\n\tif err := jwt.UnsafeClaimsWithoutVerification(&claims); err != nil {\n\t\treturn nil, errs.Wrap(http.StatusUnauthorized, err,\n\t\t\t\"oidc.AuthorizeToken; error parsing oidc token claims\")\n\t}\n\n\tfound := false\n\tkid := jwt.Headers[0].KeyID\n\tkeys := o.keyStore.Get(kid)\n\tfor _, key := range keys {\n\t\tif err := jwt.Claims(key, &claims); err == nil {\n\t\t\tfound = true\n\t\t\tbreak\n\t\t}\n\t}\n\tif !found {\n\t\treturn nil, errs.Unauthorized(\"oidc.AuthorizeToken; cannot validate oidc token\")\n\t}\n\n\tif err := o.ValidatePayload(claims); err != nil {\n\t\treturn nil, errs.Wrap(http.StatusInternalServerError, err, \"oidc.AuthorizeToken\")\n\t}\n\n\treturn &claims, nil\n}\n\n// AuthorizeRevoke returns an error if the provisioner does not have rights to\n// revoke the certificate with serial number in the `sub` property.\n// Only tokens generated by an admin have the right to revoke a certificate.\nfunc (o *OIDC) AuthorizeRevoke(_ context.Context, token string) error {\n\tclaims, err := o.authorizeToken(token)\n\tif err != nil {\n\t\treturn errs.Wrap(http.StatusInternalServerError, err, \"oidc.AuthorizeRevoke\")\n\t}\n\n\t// Only admins can revoke certificates.\n\tif claims.IsAdmin(o.Admins) {\n\t\treturn nil\n\t}\n\n\treturn errs.Unauthorized(\"oidc.AuthorizeRevoke; cannot revoke with non-admin oidc token\")\n}\n\n// AuthorizeSign validates the given token.\nfunc (o *OIDC) AuthorizeSign(_ context.Context, token string) ([]SignOption, error) {\n\tclaims, err := o.authorizeToken(token)\n\tif err != nil {\n\t\treturn nil, errs.Wrap(http.StatusInternalServerError, err, \"oidc.AuthorizeSign\")\n\t}\n\n\t// Certificate templates\n\tsans := []string{}\n\tif claims.Email != \"\" {\n\t\tsans = append(sans, claims.Email)\n\t}\n\n\t// Add uri SAN with iss#sub if issuer is a URL with schema.\n\t//\n\t// According to https://openid.net/specs/openid-connect-core-1_0.html the\n\t// iss value is a case sensitive URL using the https scheme that contains\n\t// scheme, host, and optionally, port number and path components and no\n\t// query or fragment components.\n\tif iss, err := url.Parse(claims.Issuer); err == nil && iss.Scheme != \"\" {\n\t\tiss.Fragment = claims.Subject\n\t\tsans = append(sans, iss.String())\n\t}\n\n\tdata := x509util.CreateTemplateData(claims.Subject, sans)\n\tif v, err := unsafeParseSigned(token); err == nil {\n\t\tdata.SetToken(v)\n\t}\n\n\t// Use the default template unless no-templates are configured and email is\n\t// an admin, in that case we will use the CR template.\n\tdefaultTemplate := x509util.DefaultLeafTemplate\n\tif !o.Options.GetX509Options().HasTemplate() && claims.IsAdmin(o.Admins) {\n\t\tdefaultTemplate = x509util.DefaultAdminLeafTemplate\n\t}\n\n\ttemplateOptions, err := CustomTemplateOptions(o.Options, data, defaultTemplate)\n\tif err != nil {\n\t\treturn nil, errs.Wrap(http.StatusInternalServerError, err, \"oidc.AuthorizeSign\")\n\t}\n\n\treturn []SignOption{\n\t\to,\n\t\ttemplateOptions,\n\t\t// modifiers / withOptions\n\t\tnewProvisionerExtensionOption(TypeOIDC, o.Name, o.ClientID).WithControllerOptions(o.ctl),\n\t\tprofileDefaultDuration(o.ctl.Claimer.DefaultTLSCertDuration()),\n\t\t// validators\n\t\tdefaultPublicKeyValidator{},\n\t\tnewValidityValidator(o.ctl.Claimer.MinTLSCertDuration(), o.ctl.Claimer.MaxTLSCertDuration()),\n\t\tnewX509NamePolicyValidator(o.ctl.getPolicy().getX509()),\n\t\t// webhooks\n\t\to.ctl.newWebhookController(data, linkedca.Webhook_X509),\n\t}, nil\n}\n\n// AuthorizeRenew returns an error if the renewal is disabled.\n// NOTE: This method does not actually validate the certificate or check it's\n// revocation status. Just confirms that the provisioner that created the\n// certificate was configured to allow renewals.\nfunc (o *OIDC) AuthorizeRenew(ctx context.Context, cert *x509.Certificate) error {\n\treturn o.ctl.AuthorizeRenew(ctx, cert)\n}\n\n// AuthorizeSSHSign returns the list of SignOption for a SignSSH request.\nfunc (o *OIDC) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, error) {\n\tif !o.ctl.Claimer.IsSSHCAEnabled() {\n\t\treturn nil, errs.Unauthorized(\"oidc.AuthorizeSSHSign; sshCA is disabled for oidc provisioner '%s'\", o.GetName())\n\t}\n\tclaims, err := o.authorizeToken(token)\n\tif err != nil {\n\t\treturn nil, errs.Wrap(http.StatusInternalServerError, err, \"oidc.AuthorizeSSHSign\")\n\t}\n\n\tif claims.Subject == \"\" {\n\t\treturn nil, errs.Unauthorized(\"oidc.AuthorizeSSHSign: failed to validate oidc token payload: subject not found\")\n\t}\n\n\tvar data sshutil.TemplateData\n\tif claims.Email == \"\" {\n\t\t// If email is empty, use the Subject claim instead to create minimal\n\t\t// data for the template to use.\n\t\tdata = sshutil.CreateTemplateData(sshutil.UserCert, claims.Subject, nil)\n\t\tif v, err := unsafeParseSigned(token); err == nil {\n\t\t\tdata.SetToken(v)\n\t\t}\n\t} else {\n\t\t// Get the identity using either the default identityFunc or one injected\n\t\t// externally. Note that the PreferredUsername might be empty.\n\t\t// TBD: Would preferred_username present a safety issue here?\n\t\tiden, err := o.ctl.GetIdentity(ctx, claims.Email)\n\t\tif err != nil {\n\t\t\treturn nil, errs.Wrap(http.StatusInternalServerError, err, \"oidc.AuthorizeSSHSign\")\n\t\t}\n\n\t\t// Certificate templates.\n\t\tdata = sshutil.CreateTemplateData(sshutil.UserCert, claims.Email, iden.Usernames)\n\t\tif v, err := unsafeParseSigned(token); err == nil {\n\t\t\tdata.SetToken(v)\n\t\t}\n\t\t// Add custom extensions added in the identity function.\n\t\tfor k, v := range iden.Permissions.Extensions {\n\t\t\tdata.AddExtension(k, v)\n\t\t}\n\t\t// Add custom critical options added in the identity function.\n\t\tfor k, v := range iden.Permissions.CriticalOptions {\n\t\t\tdata.AddCriticalOption(k, v)\n\t\t}\n\t}\n\n\t// Use the default template unless no-templates are configured and email is\n\t// an admin, in that case we will use the parameters in the request.\n\tisAdmin := claims.IsAdmin(o.Admins)\n\tdefaultTemplate := sshutil.DefaultTemplate\n\tif isAdmin && !o.Options.GetSSHOptions().HasTemplate() {\n\t\tdefaultTemplate = sshutil.DefaultAdminTemplate\n\t}\n\n\ttemplateOptions, err := CustomSSHTemplateOptions(o.Options, data, defaultTemplate)\n\tif err != nil {\n\t\treturn nil, errs.Wrap(http.StatusInternalServerError, err, \"jwk.AuthorizeSign\")\n\t}\n\tsignOptions := []SignOption{templateOptions}\n\n\t// Admin users can use any principal, and can sign user and host certificates.\n\t// Non-admin users can only use principals returned by the identityFunc, and\n\t// can only sign user certificates.\n\tif isAdmin {\n\t\tsignOptions = append(signOptions, &sshCertOptionsRequireValidator{\n\t\t\tCertType:   true,\n\t\t\tKeyID:      true,\n\t\t\tPrincipals: true,\n\t\t})\n\t} else {\n\t\tsignOptions = append(signOptions, sshCertOptionsValidator(SignSSHOptions{\n\t\t\tCertType: SSHUserCert,\n\t\t}))\n\t}\n\n\treturn append(signOptions,\n\t\to,\n\t\t// Set the validity bounds if not set.\n\t\t&sshDefaultDuration{o.ctl.Claimer},\n\t\t// Validate public key\n\t\t&sshDefaultPublicKeyValidator{},\n\t\t// Validate the validity period.\n\t\t&sshCertValidityValidator{o.ctl.Claimer},\n\t\t// Require all the fields in the SSH certificate\n\t\t&sshCertDefaultValidator{},\n\t\t// Ensure that all principal names are allowed\n\t\tnewSSHNamePolicyValidator(o.ctl.getPolicy().getSSHHost(), o.ctl.getPolicy().getSSHUser()),\n\t\t// Call webhooks\n\t\to.ctl.newWebhookController(data, linkedca.Webhook_SSH),\n\t), nil\n}\n\n// AuthorizeSSHRevoke returns nil if the token is valid, false otherwise.\nfunc (o *OIDC) AuthorizeSSHRevoke(_ context.Context, token string) error {\n\tclaims, err := o.authorizeToken(token)\n\tif err != nil {\n\t\treturn errs.Wrap(http.StatusInternalServerError, err, \"oidc.AuthorizeSSHRevoke\")\n\t}\n\n\t// Only admins can revoke certificates.\n\tif claims.IsAdmin(o.Admins) {\n\t\treturn nil\n\t}\n\n\treturn errs.Unauthorized(\"oidc.AuthorizeSSHRevoke; cannot revoke with non-admin oidc token\")\n}\n\nfunc getAndDecode(client HTTPClient, uri string, v interface{}) error {\n\tresp, err := client.Get(uri)\n\tif err != nil {\n\t\treturn errors.Wrapf(err, \"failed to connect to %s\", uri)\n\t}\n\tdefer resp.Body.Close()\n\tif err := json.NewDecoder(resp.Body).Decode(v); err != nil {\n\t\treturn errors.Wrapf(err, \"error reading %s\", uri)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "authority/provisioner/oidc_test.go",
    "content": "package provisioner\n\nimport (\n\t\"context\"\n\t\"crypto\"\n\t\"crypto/rand\"\n\t\"crypto/rsa\"\n\t\"crypto/x509\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/require\"\n\t\"go.step.sm/crypto/jose\"\n\n\t\"github.com/smallstep/assert\"\n\t\"github.com/smallstep/certificates/api/render\"\n)\n\nfunc Test_openIDConfiguration_Validate(t *testing.T) {\n\ttype fields struct {\n\t\tIssuer    string\n\t\tJWKSetURI string\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tfields  fields\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", fields{\"the-issuer\", \"the-jwks-uri\"}, false},\n\t\t{\"no-issuer\", fields{\"\", \"the-jwks-uri\"}, true},\n\t\t{\"no-jwks-uri\", fields{\"the-issuer\", \"\"}, true},\n\t\t{\"empty\", fields{\"\", \"\"}, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tc := openIDConfiguration{\n\t\t\t\tIssuer:    tt.fields.Issuer,\n\t\t\t\tJWKSetURI: tt.fields.JWKSetURI,\n\t\t\t}\n\t\t\tif err := c.Validate(); (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"openIDConfiguration.Validate() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestOIDC_Getters(t *testing.T) {\n\tp, err := generateOIDC()\n\tassert.FatalError(t, err)\n\tif got := p.GetID(); got != p.ClientID {\n\t\tt.Errorf(\"OIDC.GetID() = %v, want %v\", got, p.ClientID)\n\t}\n\tif got := p.GetName(); got != p.Name {\n\t\tt.Errorf(\"OIDC.GetName() = %v, want %v\", got, p.Name)\n\t}\n\tif got := p.GetType(); got != TypeOIDC {\n\t\tt.Errorf(\"OIDC.GetType() = %v, want %v\", got, TypeOIDC)\n\t}\n\tkid, key, ok := p.GetEncryptedKey()\n\tif kid != \"\" || key != \"\" || ok == true {\n\t\tt.Errorf(\"OIDC.GetEncryptedKey() = (%v, %v, %v), want (%v, %v, %v)\",\n\t\t\tkid, key, ok, \"\", \"\", false)\n\t}\n}\n\nfunc TestOIDC_Init(t *testing.T) {\n\tsrv := generateJWKServer(2)\n\tdefer srv.Close()\n\n\ttlsSrv := generateTLSJWKServer(2)\n\tdefer tlsSrv.Close()\n\n\tconfig := Config{\n\t\tClaims:     globalProvisionerClaims,\n\t\tHTTPClient: tlsSrv.Client(),\n\t}\n\tbadHTTPClientConfig := Config{\n\t\tClaims:     globalProvisionerClaims,\n\t\tHTTPClient: http.DefaultClient,\n\t}\n\tbadClaims := &Claims{\n\t\tDefaultTLSDur: &Duration{0},\n\t}\n\n\ttype fields struct {\n\t\tType                  string\n\t\tName                  string\n\t\tClientID              string\n\t\tClientSecret          string\n\t\tConfigurationEndpoint string\n\t\tClaims                *Claims\n\t\tAdmins                []string\n\t\tDomains               []string\n\t\tListenAddress         string\n\t}\n\ttype args struct {\n\t\tconfig Config\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tfields  fields\n\t\targs    args\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", fields{\"oidc\", \"name\", \"client-id\", \"client-secret\", srv.URL, nil, nil, nil, \"\"}, args{config}, false},\n\t\t{\"ok tls\", fields{\"oidc\", \"name\", \"client-id\", \"client-secret\", tlsSrv.URL, nil, nil, nil, \"\"}, args{config}, false},\n\t\t{\"ok-admins\", fields{\"oidc\", \"name\", \"client-id\", \"client-secret\", srv.URL + \"/.well-known/openid-configuration\", nil, []string{\"foo@smallstep.com\"}, nil, \"\"}, args{config}, false},\n\t\t{\"ok-domains\", fields{\"oidc\", \"name\", \"client-id\", \"client-secret\", srv.URL, nil, nil, []string{\"smallstep.com\"}, \"\"}, args{config}, false},\n\t\t{\"ok-listen-port\", fields{\"oidc\", \"name\", \"client-id\", \"client-secret\", srv.URL, nil, nil, nil, \":10000\"}, args{config}, false},\n\t\t{\"ok-listen-host-port\", fields{\"oidc\", \"name\", \"client-id\", \"client-secret\", srv.URL, nil, nil, nil, \"127.0.0.1:10000\"}, args{config}, false},\n\t\t{\"ok-no-secret\", fields{\"oidc\", \"name\", \"client-id\", \"\", srv.URL, nil, nil, nil, \"\"}, args{config}, false},\n\t\t{\"no-name\", fields{\"oidc\", \"\", \"client-id\", \"client-secret\", srv.URL, nil, nil, nil, \"\"}, args{config}, true},\n\t\t{\"no-type\", fields{\"\", \"name\", \"client-id\", \"client-secret\", srv.URL, nil, nil, nil, \"\"}, args{config}, true},\n\t\t{\"no-client-id\", fields{\"oidc\", \"name\", \"\", \"client-secret\", srv.URL, nil, nil, nil, \"\"}, args{config}, true},\n\t\t{\"no-configuration\", fields{\"oidc\", \"name\", \"client-id\", \"client-secret\", \"\", nil, nil, nil, \"\"}, args{config}, true},\n\t\t{\"bad-configuration\", fields{\"oidc\", \"name\", \"client-id\", \"client-secret\", srv.URL + \"/random\", nil, nil, nil, \"\"}, args{config}, true},\n\t\t{\"bad-claims\", fields{\"oidc\", \"name\", \"client-id\", \"client-secret\", srv.URL + \"/.well-known/openid-configuration\", badClaims, nil, nil, \"\"}, args{config}, true},\n\t\t{\"bad-parse-url\", fields{\"oidc\", \"name\", \"client-id\", \"client-secret\", \":\", nil, nil, nil, \"\"}, args{config}, true},\n\t\t{\"bad-get-url\", fields{\"oidc\", \"name\", \"client-id\", \"client-secret\", \"https://\", nil, nil, nil, \"\"}, args{config}, true},\n\t\t{\"bad-listen-address\", fields{\"oidc\", \"name\", \"client-id\", \"client-secret\", srv.URL, nil, nil, nil, \"127.0.0.1\"}, args{config}, true},\n\t\t{\"bad-http-client\", fields{\"oidc\", \"name\", \"client-id\", \"client-secret\", tlsSrv.URL, nil, nil, nil, \"\"}, args{badHTTPClientConfig}, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tp := &OIDC{\n\t\t\t\tType:                  tt.fields.Type,\n\t\t\t\tName:                  tt.fields.Name,\n\t\t\t\tClientID:              tt.fields.ClientID,\n\t\t\t\tConfigurationEndpoint: tt.fields.ConfigurationEndpoint,\n\t\t\t\tClaims:                tt.fields.Claims,\n\t\t\t\tAdmins:                tt.fields.Admins,\n\t\t\t\tDomains:               tt.fields.Domains,\n\t\t\t\tListenAddress:         tt.fields.ListenAddress,\n\t\t\t}\n\t\t\tif err := p.Init(tt.args.config); (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"OIDC.Init() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif tt.wantErr == false {\n\t\t\t\tassert.Len(t, 2, p.keyStore.keySet.Keys)\n\n\t\t\t\tu, err := url.Parse(tt.fields.ConfigurationEndpoint)\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\tassert.Equals(t, openIDConfiguration{\n\t\t\t\t\tIssuer:    \"the-issuer\",\n\t\t\t\t\tJWKSetURI: u.ResolveReference(&url.URL{Path: \"/jwks_uri\"}).String(),\n\t\t\t\t}, p.configuration)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestOIDC_authorizeToken(t *testing.T) {\n\tsrv := generateJWKServer(3)\n\tdefer srv.Close()\n\n\tvar keys jose.JSONWebKeySet\n\tassert.FatalError(t, getAndDecode(srv.Client(), srv.URL+\"/private\", &keys))\n\n\tissuer := \"the-issuer\"\n\ttenantID := \"ab800f7d-2c87-45fb-b1d0-f90d0bc5ec25\"\n\ttenantIssuer := \"https://login.microsoftonline.com/\" + tenantID + \"/v2.0\"\n\n\t// Create test provisioners\n\tp1, err := generateOIDC()\n\tassert.FatalError(t, err)\n\tp2, err := generateOIDC()\n\tassert.FatalError(t, err)\n\tp3, err := generateOIDC()\n\tassert.FatalError(t, err)\n\t// TenantID\n\tp2.TenantID = tenantID\n\t// Admin + Domains\n\tp3.Admins = []string{\"name@smallstep.com\", \"root@example.com\"}\n\tp3.Domains = []string{\"smallstep.com\"}\n\n\t// Update configuration endpoints and initialize\n\tconfig := Config{Claims: globalProvisionerClaims}\n\tp1.ConfigurationEndpoint = srv.URL + \"/.well-known/openid-configuration\"\n\tp2.ConfigurationEndpoint = srv.URL + \"/common/.well-known/openid-configuration\"\n\tp3.ConfigurationEndpoint = srv.URL + \"/.well-known/openid-configuration\"\n\tassert.FatalError(t, p1.Init(config))\n\tassert.FatalError(t, p2.Init(config))\n\tassert.FatalError(t, p3.Init(config))\n\n\tt1, err := generateSimpleToken(issuer, p1.ClientID, &keys.Keys[0])\n\tassert.FatalError(t, err)\n\tt2, err := generateSimpleToken(tenantIssuer, p2.ClientID, &keys.Keys[1])\n\tassert.FatalError(t, err)\n\tt3, err := generateToken(\"subject\", issuer, p3.ClientID, \"name@smallstep.com\", []string{}, time.Now(), &keys.Keys[2])\n\tassert.FatalError(t, err)\n\tt4, err := generateToken(\"subject\", issuer, p3.ClientID, \"foo@smallstep.com\", []string{}, time.Now(), &keys.Keys[2])\n\tassert.FatalError(t, err)\n\tt5, err := generateToken(\"subject\", issuer, p3.ClientID, \"\", []string{}, time.Now(), &keys.Keys[2])\n\tassert.FatalError(t, err)\n\n\t// Invalid email\n\tfailDomain, err := generateToken(\"subject\", issuer, p3.ClientID, \"name@example.com\", []string{}, time.Now(), &keys.Keys[2])\n\tassert.FatalError(t, err)\n\t// Invalid tokens\n\tparts := strings.Split(t1, \".\")\n\tkey, err := generateJSONWebKey()\n\tassert.FatalError(t, err)\n\t// missing key\n\tfailKey, err := generateSimpleToken(issuer, p1.ClientID, key)\n\tassert.FatalError(t, err)\n\t// invalid token\n\tfailTok := \"foo.\" + parts[1] + \".\" + parts[2]\n\t// invalid claims\n\tfailClaims := parts[0] + \".foo.\" + parts[1]\n\t// invalid issuer\n\tfailIss, err := generateSimpleToken(\"bad-issuer\", p1.ClientID, &keys.Keys[0])\n\tassert.FatalError(t, err)\n\t// invalid audience\n\tfailAud, err := generateSimpleToken(issuer, \"foobar\", &keys.Keys[0])\n\tassert.FatalError(t, err)\n\t// invalid signature\n\tfailSig := t1[0 : len(t1)-2]\n\t// expired\n\tfailExp, err := generateToken(\"subject\", issuer, p1.ClientID, \"name@smallstep.com\", []string{}, time.Now().Add(-360*time.Second), &keys.Keys[0])\n\tassert.FatalError(t, err)\n\t// not before\n\tfailNbf, err := generateToken(\"subject\", issuer, p1.ClientID, \"name@smallstep.com\", []string{}, time.Now().Add(360*time.Second), &keys.Keys[0])\n\tassert.FatalError(t, err)\n\n\ttype args struct {\n\t\ttoken string\n\t}\n\ttests := []struct {\n\t\tname       string\n\t\tprov       *OIDC\n\t\targs       args\n\t\tcode       int\n\t\twantIssuer string\n\t\texpErr     error\n\t}{\n\t\t{\"ok1\", p1, args{t1}, http.StatusOK, issuer, nil},\n\t\t{\"ok tenantid\", p2, args{t2}, http.StatusOK, tenantIssuer, nil},\n\t\t{\"ok admin\", p3, args{t3}, http.StatusOK, issuer, nil},\n\t\t{\"ok domain\", p3, args{t4}, http.StatusOK, issuer, nil},\n\t\t{\"ok no email\", p3, args{t5}, http.StatusOK, issuer, nil},\n\t\t{\"fail-domain\", p3, args{failDomain}, http.StatusUnauthorized, \"\", errors.New(`oidc.AuthorizeToken: validatePayload: failed to validate oidc token payload: email \"name@example.com\" is not allowed`)},\n\t\t{\"fail-key\", p1, args{failKey}, http.StatusUnauthorized, \"\", errors.New(`oidc.AuthorizeToken; cannot validate oidc token`)},\n\t\t{\"fail-token\", p1, args{failTok}, http.StatusUnauthorized, \"\", errors.New(`oidc.AuthorizeToken; error parsing oidc token: invalid character '~' looking for beginning of value`)},\n\t\t{\"fail-claims\", p1, args{failClaims}, http.StatusUnauthorized, \"\", errors.New(`oidc.AuthorizeToken; error parsing oidc token claims: invalid character '~' looking for beginning of value`)},\n\t\t{\"fail-issuer\", p1, args{failIss}, http.StatusUnauthorized, \"\", errors.New(`oidc.AuthorizeToken: validatePayload: failed to validate oidc token payload: go-jose/go-jose/jwt: validation failed, invalid issuer claim (iss)`)},\n\t\t{\"fail-audience\", p1, args{failAud}, http.StatusUnauthorized, \"\", errors.New(`oidc.AuthorizeToken: validatePayload: failed to validate oidc token payload: go-jose/go-jose/jwt: validation failed, invalid audience claim (aud)`)},\n\t\t{\"fail-signature\", p1, args{failSig}, http.StatusUnauthorized, \"\", errors.New(`oidc.AuthorizeToken; cannot validate oidc token`)},\n\t\t{\"fail-expired\", p1, args{failExp}, http.StatusUnauthorized, \"\", errors.New(`oidc.AuthorizeToken: validatePayload: failed to validate oidc token payload: go-jose/go-jose/jwt: validation failed, token is expired (exp)`)},\n\t\t{\"fail-not-before\", p1, args{failNbf}, http.StatusUnauthorized, \"\", errors.New(`oidc.AuthorizeToken: validatePayload: failed to validate oidc token payload: go-jose/go-jose/jwt: validation failed, token not valid yet (nbf)`)},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := tt.prov.authorizeToken(tt.args.token)\n\t\t\tif tt.expErr != nil {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\trequire.EqualError(t, err, tt.expErr.Error())\n\n\t\t\t\tvar sc render.StatusCodedError\n\t\t\t\trequire.ErrorAs(t, err, &sc, \"error does not implement StatusCodedError interface\")\n\t\t\t\trequire.Equal(t, tt.code, sc.StatusCode())\n\t\t\t\trequire.Nil(t, got)\n\t\t\t} else {\n\t\t\t\trequire.NotNil(t, got)\n\t\t\t\trequire.Equal(t, tt.wantIssuer, got.Issuer)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestOIDC_AuthorizeSign(t *testing.T) {\n\tsrv := generateJWKServer(2)\n\tdefer srv.Close()\n\n\tvar keys jose.JSONWebKeySet\n\tassert.FatalError(t, getAndDecode(srv.Client(), srv.URL+\"/private\", &keys))\n\n\t// Create test provisioners\n\tp1, err := generateOIDC()\n\tassert.FatalError(t, err)\n\tp2, err := generateOIDC()\n\tassert.FatalError(t, err)\n\tp3, err := generateOIDC()\n\tassert.FatalError(t, err)\n\t// Admin + Domains\n\tp3.Admins = []string{\"name@smallstep.com\", \"root@example.com\"}\n\tp3.Domains = []string{\"smallstep.com\"}\n\n\t// Update configuration endpoints and initialize\n\tconfig := Config{Claims: globalProvisionerClaims}\n\tp1.ConfigurationEndpoint = srv.URL + \"/.well-known/openid-configuration\"\n\tp2.ConfigurationEndpoint = srv.URL + \"/.well-known/openid-configuration\"\n\tp3.ConfigurationEndpoint = srv.URL + \"/.well-known/openid-configuration\"\n\tassert.FatalError(t, p1.Init(config))\n\tassert.FatalError(t, p2.Init(config))\n\tassert.FatalError(t, p3.Init(config))\n\n\tt1, err := generateSimpleToken(\"the-issuer\", p1.ClientID, &keys.Keys[0])\n\tassert.FatalError(t, err)\n\t// Admin email not in domains\n\tokAdmin, err := generateToken(\"subject\", \"the-issuer\", p3.ClientID, \"root@example.com\", []string{\"test.smallstep.com\"}, time.Now(), &keys.Keys[0])\n\tassert.FatalError(t, err)\n\t// No email\n\tnoEmail, err := generateToken(\"subject\", \"the-issuer\", p3.ClientID, \"\", []string{}, time.Now(), &keys.Keys[0])\n\tassert.FatalError(t, err)\n\n\ttype args struct {\n\t\ttoken string\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tprov    *OIDC\n\t\targs    args\n\t\tcode    int\n\t\twantErr bool\n\t}{\n\t\t{\"ok1\", p1, args{t1}, http.StatusOK, false},\n\t\t{\"admin\", p3, args{okAdmin}, http.StatusOK, false},\n\t\t{\"no-email\", p3, args{noEmail}, http.StatusOK, false},\n\t\t{\"bad-token\", p3, args{\"foobar\"}, http.StatusUnauthorized, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := tt.prov.AuthorizeSign(context.Background(), tt.args.token)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"OIDC.Authorize() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif err != nil {\n\t\t\t\tvar sc render.StatusCodedError\n\t\t\t\tassert.Fatal(t, errors.As(err, &sc), \"error does not implement StatusCodedError interface\")\n\t\t\t\tassert.Equals(t, sc.StatusCode(), tt.code)\n\t\t\t\tassert.Nil(t, got)\n\t\t\t} else if assert.NotNil(t, got) {\n\t\t\t\tassert.Equals(t, 8, len(got))\n\t\t\t\tfor _, o := range got {\n\t\t\t\t\tswitch v := o.(type) {\n\t\t\t\t\tcase *OIDC:\n\t\t\t\t\tcase certificateOptionsFunc:\n\t\t\t\t\tcase *provisionerExtensionOption:\n\t\t\t\t\t\tassert.Equals(t, v.Type, TypeOIDC)\n\t\t\t\t\t\tassert.Equals(t, v.Name, tt.prov.GetName())\n\t\t\t\t\t\tassert.Equals(t, v.CredentialID, tt.prov.ClientID)\n\t\t\t\t\t\tassert.Len(t, 0, v.KeyValuePairs)\n\t\t\t\t\tcase profileDefaultDuration:\n\t\t\t\t\t\tassert.Equals(t, time.Duration(v), tt.prov.ctl.Claimer.DefaultTLSCertDuration())\n\t\t\t\t\tcase defaultPublicKeyValidator:\n\t\t\t\t\tcase *validityValidator:\n\t\t\t\t\t\tassert.Equals(t, v.min, tt.prov.ctl.Claimer.MinTLSCertDuration())\n\t\t\t\t\t\tassert.Equals(t, v.max, tt.prov.ctl.Claimer.MaxTLSCertDuration())\n\t\t\t\t\tcase *x509NamePolicyValidator:\n\t\t\t\t\t\tassert.Equals(t, nil, v.policyEngine)\n\t\t\t\t\tcase *WebhookController:\n\t\t\t\t\t\tassert.Len(t, 0, v.webhooks)\n\t\t\t\t\tdefault:\n\t\t\t\t\t\tassert.FatalError(t, fmt.Errorf(\"unexpected sign option of type %T\", v))\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestOIDC_AuthorizeRevoke(t *testing.T) {\n\tsrv := generateJWKServer(2)\n\tdefer srv.Close()\n\n\tvar keys jose.JSONWebKeySet\n\tassert.FatalError(t, getAndDecode(srv.Client(), srv.URL+\"/private\", &keys))\n\n\t// Create test provisioners\n\tp1, err := generateOIDC()\n\tassert.FatalError(t, err)\n\tp3, err := generateOIDC()\n\tassert.FatalError(t, err)\n\t// Admin + Domains\n\tp3.Admins = []string{\"name@smallstep.com\", \"root@example.com\"}\n\tp3.Domains = []string{\"smallstep.com\"}\n\n\t// Update configuration endpoints and initialize\n\tconfig := Config{Claims: globalProvisionerClaims}\n\tp1.ConfigurationEndpoint = srv.URL + \"/.well-known/openid-configuration\"\n\tp3.ConfigurationEndpoint = srv.URL + \"/.well-known/openid-configuration\"\n\tassert.FatalError(t, p1.Init(config))\n\tassert.FatalError(t, p3.Init(config))\n\n\tt1, err := generateSimpleToken(\"the-issuer\", p1.ClientID, &keys.Keys[0])\n\tassert.FatalError(t, err)\n\t// Admin email not in domains\n\tokAdmin, err := generateToken(\"subject\", \"the-issuer\", p3.ClientID, \"root@example.com\", []string{\"test.smallstep.com\"}, time.Now(), &keys.Keys[0])\n\tassert.FatalError(t, err)\n\t// Invalid email\n\tfailEmail, err := generateToken(\"subject\", \"the-issuer\", p3.ClientID, \"\", []string{}, time.Now(), &keys.Keys[0])\n\tassert.FatalError(t, err)\n\n\ttype args struct {\n\t\ttoken string\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tprov    *OIDC\n\t\targs    args\n\t\tcode    int\n\t\twantErr bool\n\t}{\n\t\t{\"ok1\", p1, args{t1}, http.StatusUnauthorized, true},\n\t\t{\"admin\", p3, args{okAdmin}, http.StatusOK, false},\n\t\t{\"fail-email\", p3, args{failEmail}, http.StatusUnauthorized, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\terr := tt.prov.AuthorizeRevoke(context.Background(), tt.args.token)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tfmt.Println(tt)\n\t\t\t\tt.Errorf(\"OIDC.Authorize() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t} else if err != nil {\n\t\t\t\tvar sc render.StatusCodedError\n\t\t\t\tassert.Fatal(t, errors.As(err, &sc), \"error does not implement StatusCodedError interface\")\n\t\t\t\tassert.Equals(t, sc.StatusCode(), tt.code)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestOIDC_AuthorizeRenew(t *testing.T) {\n\tnow := time.Now().Truncate(time.Second)\n\tp1, err := generateOIDC()\n\tassert.FatalError(t, err)\n\tp2, err := generateOIDC()\n\tassert.FatalError(t, err)\n\n\t// disable renewal\n\tdisable := true\n\tp2.Claims = &Claims{DisableRenewal: &disable}\n\tp2.ctl.Claimer, err = NewClaimer(p2.Claims, globalProvisionerClaims)\n\tassert.FatalError(t, err)\n\n\ttype args struct {\n\t\tcert *x509.Certificate\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tprov    *OIDC\n\t\targs    args\n\t\tcode    int\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", p1, args{&x509.Certificate{\n\t\t\tNotBefore: now,\n\t\t\tNotAfter:  now.Add(time.Hour),\n\t\t}}, http.StatusOK, false},\n\t\t{\"fail/renew-disabled\", p2, args{&x509.Certificate{\n\t\t\tNotBefore: now,\n\t\t\tNotAfter:  now.Add(time.Hour),\n\t\t}}, http.StatusUnauthorized, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\terr := tt.prov.AuthorizeRenew(context.Background(), tt.args.cert)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"OIDC.AuthorizeRenew() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t} else if err != nil {\n\t\t\t\tvar sc render.StatusCodedError\n\t\t\t\tassert.Fatal(t, errors.As(err, &sc), \"error does not implement StatusCodedError interface\")\n\t\t\t\tassert.Equals(t, sc.StatusCode(), tt.code)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestOIDC_AuthorizeSSHSign(t *testing.T) {\n\ttm, fn := mockNow()\n\tdefer fn()\n\n\tsrv := generateJWKServer(2)\n\tdefer srv.Close()\n\n\tvar keys jose.JSONWebKeySet\n\tassert.FatalError(t, getAndDecode(srv.Client(), srv.URL+\"/private\", &keys))\n\n\t// Create test provisioners\n\tp1, err := generateOIDC()\n\tassert.FatalError(t, err)\n\tp2, err := generateOIDC()\n\tassert.FatalError(t, err)\n\tp3, err := generateOIDC()\n\tassert.FatalError(t, err)\n\tp4, err := generateOIDC()\n\tassert.FatalError(t, err)\n\tp5, err := generateOIDC()\n\tassert.FatalError(t, err)\n\tp6, err := generateOIDC()\n\tassert.FatalError(t, err)\n\t// Admin + Domains\n\tp3.Admins = []string{\"name@smallstep.com\", \"root@example.com\"}\n\tp3.Domains = []string{\"smallstep.com\"}\n\t// disable sshCA\n\tdisable := false\n\tp6.Claims = &Claims{EnableSSHCA: &disable}\n\tp6.ctl.Claimer, err = NewClaimer(p6.Claims, globalProvisionerClaims)\n\tassert.FatalError(t, err)\n\n\t// Update configuration endpoints and initialize\n\tconfig := Config{Claims: globalProvisionerClaims}\n\tp1.ConfigurationEndpoint = srv.URL + \"/.well-known/openid-configuration\"\n\tp2.ConfigurationEndpoint = srv.URL + \"/.well-known/openid-configuration\"\n\tp3.ConfigurationEndpoint = srv.URL + \"/.well-known/openid-configuration\"\n\tp4.ConfigurationEndpoint = srv.URL + \"/.well-known/openid-configuration\"\n\tp5.ConfigurationEndpoint = srv.URL + \"/.well-known/openid-configuration\"\n\tassert.FatalError(t, p1.Init(config))\n\tassert.FatalError(t, p2.Init(config))\n\tassert.FatalError(t, p3.Init(config))\n\tassert.FatalError(t, p4.Init(config))\n\tassert.FatalError(t, p5.Init(config))\n\n\tp4.ctl.IdentityFunc = func(ctx context.Context, p Interface, email string) (*Identity, error) {\n\t\treturn &Identity{Usernames: []string{\"max\", \"mariano\"}}, nil\n\t}\n\tp5.ctl.IdentityFunc = func(ctx context.Context, p Interface, email string) (*Identity, error) {\n\t\treturn nil, errors.New(\"force\")\n\t}\n\t// Additional test needed for empty usernames and duplicate email and usernames\n\n\tt1, err := generateSimpleToken(\"the-issuer\", p1.ClientID, &keys.Keys[0])\n\tassert.FatalError(t, err)\n\tokGetIdentityToken, err := generateSimpleToken(\"the-issuer\", p4.ClientID, &keys.Keys[0])\n\tassert.FatalError(t, err)\n\tfailGetIdentityToken, err := generateSimpleToken(\"the-issuer\", p5.ClientID, &keys.Keys[0])\n\tassert.FatalError(t, err)\n\t// Admin email not in domains\n\tokAdmin, err := generateOIDCToken(\"subject\", \"the-issuer\", p3.ClientID, \"root@example.com\", \"\", time.Now(), &keys.Keys[0])\n\tassert.FatalError(t, err)\n\t// Empty email\n\temptyEmail, err := generateToken(\"subject\", \"the-issuer\", p1.ClientID, \"\", []string{}, time.Now(), &keys.Keys[0])\n\texpectemptyEmailOptions := &SignSSHOptions{\n\t\tCertType:   \"user\",\n\t\tPrincipals: []string{},\n\t\tValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(p1.ctl.Claimer.DefaultUserSSHCertDuration())),\n\t}\n\tassert.FatalError(t, err)\n\n\tkey, err := generateJSONWebKey()\n\tassert.FatalError(t, err)\n\n\tsigner, err := generateJSONWebKey()\n\tassert.FatalError(t, err)\n\n\tpub := key.Public().Key\n\trsa2048, err := rsa.GenerateKey(rand.Reader, 2048)\n\tassert.FatalError(t, err)\n\t//nolint:gosec // tests minimum size of the key\n\trsa1024, err := rsa.GenerateKey(rand.Reader, 1024)\n\tassert.FatalError(t, err)\n\n\tuserDuration := p1.ctl.Claimer.DefaultUserSSHCertDuration()\n\thostDuration := p1.ctl.Claimer.DefaultHostSSHCertDuration()\n\texpectedUserOptions := &SignSSHOptions{\n\t\tCertType: \"user\", Principals: []string{\"name\", \"name@smallstep.com\"},\n\t\tValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(userDuration)),\n\t}\n\texpectedAdminOptions := &SignSSHOptions{\n\t\tCertType: \"user\", Principals: []string{\"root\", \"root@example.com\"},\n\t\tValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(userDuration)),\n\t}\n\texpectedHostOptions := &SignSSHOptions{\n\t\tCertType: \"host\", Principals: []string{\"smallstep.com\"},\n\t\tValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(hostDuration)),\n\t}\n\n\ttype args struct {\n\t\ttoken   string\n\t\tsshOpts SignSSHOptions\n\t\tkey     interface{}\n\t}\n\ttests := []struct {\n\t\tname        string\n\t\tprov        *OIDC\n\t\targs        args\n\t\texpected    *SignSSHOptions\n\t\tcode        int\n\t\twantErr     bool\n\t\twantSignErr bool\n\t}{\n\t\t{\"ok\", p1, args{t1, SignSSHOptions{}, pub}, expectedUserOptions, http.StatusOK, false, false},\n\t\t{\"ok-rsa2048\", p1, args{t1, SignSSHOptions{}, rsa2048.Public()}, expectedUserOptions, http.StatusOK, false, false},\n\t\t{\"ok-user\", p1, args{t1, SignSSHOptions{CertType: \"user\"}, pub}, expectedUserOptions, http.StatusOK, false, false},\n\t\t{\"ok-empty-email\", p1, args{emptyEmail, SignSSHOptions{CertType: \"user\"}, pub}, expectemptyEmailOptions, http.StatusOK, false, false},\n\t\t{\"ok-principals\", p1, args{t1, SignSSHOptions{Principals: []string{\"name\"}}, pub},\n\t\t\t&SignSSHOptions{CertType: \"user\", Principals: []string{\"name\", \"name@smallstep.com\"},\n\t\t\t\tValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(userDuration))}, http.StatusOK, false, false},\n\t\t{\"ok-principals-ignore-passed\", p1, args{t1, SignSSHOptions{Principals: []string{\"root\"}}, pub},\n\t\t\t&SignSSHOptions{CertType: \"user\", Principals: []string{\"name\", \"name@smallstep.com\"},\n\t\t\t\tValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(userDuration))}, http.StatusOK, false, false},\n\t\t{\"ok-principals-getIdentity\", p4, args{okGetIdentityToken, SignSSHOptions{Principals: []string{\"mariano\"}}, pub},\n\t\t\t&SignSSHOptions{CertType: \"user\", Principals: []string{\"max\", \"mariano\"},\n\t\t\t\tValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(userDuration))}, http.StatusOK, false, false},\n\t\t{\"ok-emptyPrincipals-getIdentity\", p4, args{okGetIdentityToken, SignSSHOptions{}, pub},\n\t\t\t&SignSSHOptions{CertType: \"user\", Principals: []string{\"max\", \"mariano\"},\n\t\t\t\tValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(userDuration))}, http.StatusOK, false, false},\n\t\t{\"ok-options\", p1, args{t1, SignSSHOptions{CertType: \"user\", Principals: []string{\"name\"}}, pub},\n\t\t\t&SignSSHOptions{CertType: \"user\", Principals: []string{\"name\", \"name@smallstep.com\"},\n\t\t\t\tValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(userDuration))}, http.StatusOK, false, false},\n\t\t{\"ok-admin-user\", p3, args{okAdmin, SignSSHOptions{CertType: \"user\", KeyID: \"root@example.com\", Principals: []string{\"root\", \"root@example.com\"}}, pub},\n\t\t\texpectedAdminOptions, http.StatusOK, false, false},\n\t\t{\"ok-admin-host\", p3, args{okAdmin, SignSSHOptions{CertType: \"host\", KeyID: \"smallstep.com\", Principals: []string{\"smallstep.com\"}}, pub},\n\t\t\texpectedHostOptions, http.StatusOK, false, false},\n\t\t{\"ok-admin-options\", p3, args{okAdmin, SignSSHOptions{CertType: \"user\", KeyID: \"name\", Principals: []string{\"name\"}}, pub},\n\t\t\t&SignSSHOptions{CertType: \"user\", Principals: []string{\"name\"},\n\t\t\t\tValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(userDuration))}, http.StatusOK, false, false},\n\t\t{\"fail-rsa1024\", p1, args{t1, SignSSHOptions{}, rsa1024.Public()}, expectedUserOptions, http.StatusOK, false, true},\n\t\t{\"fail-user-host\", p1, args{t1, SignSSHOptions{CertType: \"host\"}, pub}, nil, http.StatusOK, false, true},\n\t\t{\"fail-getIdentity\", p5, args{failGetIdentityToken, SignSSHOptions{}, pub}, nil, http.StatusInternalServerError, true, false},\n\t\t{\"fail-sshCA-disabled\", p6, args{\"foo\", SignSSHOptions{}, pub}, nil, http.StatusUnauthorized, true, false},\n\t\t// Missing parametrs\n\t\t{\"fail-admin-type\", p3, args{okAdmin, SignSSHOptions{KeyID: \"root@example.com\", Principals: []string{\"root@example.com\"}}, pub}, nil, http.StatusUnauthorized, false, true},\n\t\t{\"fail-admin-key-id\", p3, args{okAdmin, SignSSHOptions{CertType: \"user\", Principals: []string{\"root@example.com\"}}, pub}, nil, http.StatusUnauthorized, false, true},\n\t\t{\"fail-admin-principals\", p3, args{okAdmin, SignSSHOptions{CertType: \"user\", KeyID: \"root@example.com\"}, pub}, nil, http.StatusUnauthorized, false, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := tt.prov.AuthorizeSSHSign(context.Background(), tt.args.token)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"OIDC.AuthorizeSSHSign() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif err != nil {\n\t\t\t\tvar sc render.StatusCodedError\n\t\t\t\tassert.Fatal(t, errors.As(err, &sc), \"error does not implement StatusCodedError interface\")\n\t\t\t\tassert.Equals(t, sc.StatusCode(), tt.code)\n\t\t\t\tassert.Nil(t, got)\n\t\t\t} else if assert.NotNil(t, got) {\n\t\t\t\tcert, err := signSSHCertificate(tt.args.key, tt.args.sshOpts, got, signer.Key.(crypto.Signer))\n\t\t\t\tif (err != nil) != tt.wantSignErr {\n\t\t\t\t\tt.Errorf(\"SignSSH error = %v, wantSignErr %v\", err, tt.wantSignErr)\n\t\t\t\t} else {\n\t\t\t\t\tif tt.wantSignErr {\n\t\t\t\t\t\tassert.Nil(t, cert)\n\t\t\t\t\t} else {\n\t\t\t\t\t\tassert.NoError(t, validateSSHCertificate(cert, tt.expected))\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestOIDC_AuthorizeSSHRevoke(t *testing.T) {\n\tp1, err := generateOIDC()\n\tassert.FatalError(t, err)\n\tp2, err := generateOIDC()\n\tassert.FatalError(t, err)\n\tp2.Admins = []string{\"root@example.com\"}\n\n\tsrv := generateJWKServer(2)\n\tdefer srv.Close()\n\tvar keys jose.JSONWebKeySet\n\tassert.FatalError(t, getAndDecode(srv.Client(), srv.URL+\"/private\", &keys))\n\n\tconfig := Config{Claims: globalProvisionerClaims}\n\tp1.ConfigurationEndpoint = srv.URL + \"/.well-known/openid-configuration\"\n\tp2.ConfigurationEndpoint = srv.URL + \"/.well-known/openid-configuration\"\n\tassert.FatalError(t, p1.Init(config))\n\tassert.FatalError(t, p2.Init(config))\n\n\t// Invalid email\n\tfailEmail, err := generateToken(\"subject\", \"the-issuer\", p1.ClientID, \"\", []string{}, time.Now(), &keys.Keys[0])\n\tassert.FatalError(t, err)\n\t// Admin email not in domains\n\tnoAdmin, err := generateToken(\"subject\", \"the-issuer\", p1.ClientID, \"root@example.com\", []string{\"test.smallstep.com\"}, time.Now(), &keys.Keys[0])\n\tassert.FatalError(t, err)\n\t// Admin email in domains\n\tokAdmin, err := generateToken(\"subject\", \"the-issuer\", p2.ClientID, \"root@example.com\", []string{\"test.smallstep.com\"}, time.Now(), &keys.Keys[0])\n\tassert.FatalError(t, err)\n\n\ttype args struct {\n\t\ttoken string\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tprov    *OIDC\n\t\targs    args\n\t\tcode    int\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", p2, args{okAdmin}, http.StatusOK, false},\n\t\t{\"fail/invalid-token\", p1, args{failEmail}, http.StatusUnauthorized, true},\n\t\t{\"fail/not-admin\", p1, args{noAdmin}, http.StatusUnauthorized, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\terr := tt.prov.AuthorizeSSHRevoke(context.Background(), tt.args.token)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"OIDC.AuthorizeSSHRevoke() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t} else if err != nil {\n\t\t\t\tvar sc render.StatusCodedError\n\t\t\t\tassert.Fatal(t, errors.As(err, &sc), \"error does not implement StatusCodedError interface\")\n\t\t\t\tassert.Equals(t, sc.StatusCode(), tt.code)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_sanitizeEmail(t *testing.T) {\n\ttests := []struct {\n\t\tname  string\n\t\temail string\n\t\twant  string\n\t}{\n\t\t{\"equal\", \"name@smallstep.com\", \"name@smallstep.com\"},\n\t\t{\"domain-insensitive\", \"name@SMALLSTEP.COM\", \"name@smallstep.com\"},\n\t\t{\"local-sensitive\", \"NaMe@smallSTEP.CoM\", \"NaMe@smallstep.com\"},\n\t\t{\"multiple-@\", \"NaMe@NaMe@smallSTEP.CoM\", \"NaMe@NaMe@smallstep.com\"},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif got := sanitizeEmail(tt.email); got != tt.want {\n\t\t\t\tt.Errorf(\"sanitizeEmail() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_openIDPayload_IsAdmin(t *testing.T) {\n\ttype fields struct {\n\t\tEmail  string\n\t\tGroups []string\n\t}\n\ttype args struct {\n\t\tadmins []string\n\t}\n\ttests := []struct {\n\t\tname   string\n\t\tfields fields\n\t\targs   args\n\t\twant   bool\n\t}{\n\t\t{\"ok email\", fields{\"admin@smallstep.com\", nil}, args{[]string{\"admin@smallstep.com\"}}, true},\n\t\t{\"ok email multiple\", fields{\"admin@smallstep.com\", []string{\"admin\", \"eng\"}}, args{[]string{\"eng@smallstep.com\", \"admin@smallstep.com\"}}, true},\n\t\t{\"ok email sanitized\", fields{\"admin@Smallstep.com\", nil}, args{[]string{\"admin@smallStep.com\"}}, true},\n\t\t{\"ok group\", fields{\"\", []string{\"admin\"}}, args{[]string{\"admin\"}}, true},\n\t\t{\"ok group multiple\", fields{\"admin@smallstep.com\", []string{\"engineering\", \"admin\"}}, args{[]string{\"admin\"}}, true},\n\t\t{\"fail missing\", fields{\"eng@smallstep.com\", []string{\"admin\"}}, args{[]string{\"admin@smallstep.com\"}}, false},\n\t\t{\"fail email letter case\", fields{\"Admin@smallstep.com\", []string{}}, args{[]string{\"admin@smallstep.com\"}}, false},\n\t\t{\"fail group letter case\", fields{\"\", []string{\"Admin\"}}, args{[]string{\"admin\"}}, false},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\to := &openIDPayload{\n\t\t\t\tEmail:  tt.fields.Email,\n\t\t\t\tGroups: tt.fields.Groups,\n\t\t\t}\n\t\t\tif got := o.IsAdmin(tt.args.admins); got != tt.want {\n\t\t\t\tt.Errorf(\"openIDPayload.IsAdmin() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "authority/provisioner/options.go",
    "content": "package provisioner\n\nimport (\n\t\"encoding/json\"\n\t\"strings\"\n\n\t\"github.com/pkg/errors\"\n\n\t\"github.com/smallstep/cli-utils/step\"\n\t\"go.step.sm/crypto/jose\"\n\t\"go.step.sm/crypto/x509util\"\n\n\t\"github.com/smallstep/certificates/authority/policy\"\n\t\"github.com/smallstep/certificates/authority/provisioner/wire\"\n)\n\n// CertificateOptions is an interface that returns a list of options passed when\n// creating a new certificate.\ntype CertificateOptions interface {\n\tOptions(SignOptions) []x509util.Option\n}\n\ntype certificateOptionsFunc func(SignOptions) []x509util.Option\n\nfunc (fn certificateOptionsFunc) Options(so SignOptions) []x509util.Option {\n\treturn fn(so)\n}\n\n// Options are a collection of custom options that can be added to\n// each provisioner.\ntype Options struct {\n\tX509 *X509Options `json:\"x509,omitempty\"`\n\tSSH  *SSHOptions  `json:\"ssh,omitempty\"`\n\t// Webhooks is a list of webhooks that can augment template data\n\tWebhooks []*Webhook `json:\"webhooks,omitempty\"`\n\t// Wire holds the options used for the ACME Wire integration\n\tWire *wire.Options `json:\"wire,omitempty\"`\n}\n\n// GetX509Options returns the X.509 options.\nfunc (o *Options) GetX509Options() *X509Options {\n\tif o == nil {\n\t\treturn nil\n\t}\n\treturn o.X509\n}\n\n// GetSSHOptions returns the SSH options.\nfunc (o *Options) GetSSHOptions() *SSHOptions {\n\tif o == nil {\n\t\treturn nil\n\t}\n\treturn o.SSH\n}\n\n// GetWireOptions returns the Wire options if available. It\n// returns an error if they're not available.\nfunc (o *Options) GetWireOptions() (*wire.Options, error) {\n\tif o == nil {\n\t\treturn nil, errors.New(\"no options available\")\n\t}\n\tif o.Wire == nil {\n\t\treturn nil, errors.New(\"no Wire options available\")\n\t}\n\treturn o.Wire, nil\n}\n\n// GetWebhooks returns the webhooks options.\nfunc (o *Options) GetWebhooks() []*Webhook {\n\tif o == nil {\n\t\treturn nil\n\t}\n\treturn o.Webhooks\n}\n\n// X509Options contains specific options for X.509 certificates.\ntype X509Options struct {\n\t// Template contains a X.509 certificate template. It can be a JSON template\n\t// escaped in a string or it can be also encoded in base64.\n\tTemplate string `json:\"template,omitempty\"`\n\n\t// TemplateFile points to a file containing a X.509 certificate template.\n\tTemplateFile string `json:\"templateFile,omitempty\"`\n\n\t// TemplateData is a JSON object with variables that can be used in custom\n\t// templates.\n\tTemplateData json.RawMessage `json:\"templateData,omitempty\"`\n\n\t// AllowedNames contains the SANs the provisioner is authorized to sign\n\tAllowedNames *policy.X509NameOptions `json:\"-\"`\n\n\t// DeniedNames contains the SANs the provisioner is not authorized to sign\n\tDeniedNames *policy.X509NameOptions `json:\"-\"`\n\n\t// AllowWildcardNames indicates if literal wildcard names\n\t// like *.example.com are allowed. Defaults to false.\n\tAllowWildcardNames bool `json:\"-\"`\n}\n\n// HasTemplate returns true if a template is defined in the provisioner options.\nfunc (o *X509Options) HasTemplate() bool {\n\treturn o != nil && (o.Template != \"\" || o.TemplateFile != \"\")\n}\n\n// GetAllowedNameOptions returns the AllowedNames, which models the\n// SANs that a provisioner is authorized to sign x509 certificates for.\nfunc (o *X509Options) GetAllowedNameOptions() *policy.X509NameOptions {\n\tif o == nil {\n\t\treturn nil\n\t}\n\treturn o.AllowedNames\n}\n\n// GetDeniedNameOptions returns the DeniedNames, which models the\n// SANs that a provisioner is NOT authorized to sign x509 certificates for.\nfunc (o *X509Options) GetDeniedNameOptions() *policy.X509NameOptions {\n\tif o == nil {\n\t\treturn nil\n\t}\n\treturn o.DeniedNames\n}\n\nfunc (o *X509Options) AreWildcardNamesAllowed() bool {\n\tif o == nil {\n\t\treturn true\n\t}\n\treturn o.AllowWildcardNames\n}\n\n// TemplateOptions generates a CertificateOptions with the template and data\n// defined in the ProvisionerOptions, the provisioner generated data, and the\n// user data provided in the request. If no template has been provided,\n// x509util.DefaultLeafTemplate will be used.\nfunc TemplateOptions(o *Options, data x509util.TemplateData) (CertificateOptions, error) {\n\treturn CustomTemplateOptions(o, data, x509util.DefaultLeafTemplate)\n}\n\n// CustomTemplateOptions generates a CertificateOptions with the template, data\n// defined in the ProvisionerOptions, the provisioner generated data and the\n// user data provided in the request. If no template has been provided in the\n// ProvisionerOptions, the given template will be used.\nfunc CustomTemplateOptions(o *Options, data x509util.TemplateData, defaultTemplate string) (CertificateOptions, error) {\n\topts := o.GetX509Options()\n\tif data == nil {\n\t\tdata = x509util.NewTemplateData()\n\t}\n\n\tif opts != nil {\n\t\t// Add template data if any.\n\t\tif len(opts.TemplateData) > 0 && string(opts.TemplateData) != \"null\" {\n\t\t\tif err := json.Unmarshal(opts.TemplateData, &data); err != nil {\n\t\t\t\treturn nil, errors.Wrap(err, \"error unmarshaling template data\")\n\t\t\t}\n\t\t}\n\t}\n\n\treturn certificateOptionsFunc(func(so SignOptions) []x509util.Option {\n\t\t// We're not provided user data without custom templates.\n\t\tif !opts.HasTemplate() {\n\t\t\treturn []x509util.Option{\n\t\t\t\tx509util.WithTemplate(defaultTemplate, data),\n\t\t\t}\n\t\t}\n\n\t\t// Add user provided data.\n\t\tif len(so.TemplateData) > 0 {\n\t\t\tuserObject := make(map[string]interface{})\n\t\t\tif err := json.Unmarshal(so.TemplateData, &userObject); err != nil {\n\t\t\t\tdata.SetUserData(map[string]interface{}{})\n\t\t\t} else {\n\t\t\t\tdata.SetUserData(userObject)\n\t\t\t}\n\t\t}\n\n\t\t// Load a template from a file if Template is not defined.\n\t\tif opts.Template == \"\" && opts.TemplateFile != \"\" {\n\t\t\treturn []x509util.Option{\n\t\t\t\tx509util.WithTemplateFile(step.Abs(opts.TemplateFile), data),\n\t\t\t}\n\t\t}\n\n\t\t// Load a template from the Template fields\n\t\t// 1. As a JSON in a string.\n\t\ttemplate := strings.TrimSpace(opts.Template)\n\t\tif strings.HasPrefix(template, \"{\") {\n\t\t\treturn []x509util.Option{\n\t\t\t\tx509util.WithTemplate(template, data),\n\t\t\t}\n\t\t}\n\t\t// 2. As a base64 encoded JSON.\n\t\treturn []x509util.Option{\n\t\t\tx509util.WithTemplateBase64(template, data),\n\t\t}\n\t}), nil\n}\n\n// unsafeParseSigned parses the given token and returns all the claims without\n// verifying the signature of the token.\nfunc unsafeParseSigned(s string) (map[string]interface{}, error) {\n\ttoken, err := jose.ParseSigned(s)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tclaims := make(map[string]interface{})\n\tif err := token.UnsafeClaimsWithoutVerification(&claims); err != nil {\n\t\treturn nil, err\n\t}\n\treturn claims, nil\n}\n"
  },
  {
    "path": "authority/provisioner/options_test.go",
    "content": "package provisioner\n\nimport (\n\t\"bytes\"\n\t\"crypto/x509\"\n\t\"encoding/json\"\n\t\"reflect\"\n\t\"testing\"\n\n\t\"go.step.sm/crypto/pemutil\"\n\t\"go.step.sm/crypto/x509util\"\n)\n\nfunc parseCertificateRequest(t *testing.T, filename string) *x509.CertificateRequest {\n\tt.Helper()\n\tv, err := pemutil.Read(filename)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tcsr, ok := v.(*x509.CertificateRequest)\n\tif !ok {\n\t\tt.Fatalf(\"%s is not a certificate request\", filename)\n\t}\n\treturn csr\n}\n\nfunc TestOptions_GetX509Options(t *testing.T) {\n\ttype fields struct {\n\t\to *Options\n\t}\n\ttests := []struct {\n\t\tname   string\n\t\tfields fields\n\t\twant   *X509Options\n\t}{\n\t\t{\"ok\", fields{&Options{X509: &X509Options{Template: \"foo\"}}}, &X509Options{Template: \"foo\"}},\n\t\t{\"nil\", fields{&Options{}}, nil},\n\t\t{\"nilOptions\", fields{nil}, nil},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif got := tt.fields.o.GetX509Options(); !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"Options.GetX509Options() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestOptions_GetSSHOptions(t *testing.T) {\n\ttype fields struct {\n\t\to *Options\n\t}\n\ttests := []struct {\n\t\tname   string\n\t\tfields fields\n\t\twant   *SSHOptions\n\t}{\n\t\t{\"ok\", fields{&Options{SSH: &SSHOptions{Template: \"foo\"}}}, &SSHOptions{Template: \"foo\"}},\n\t\t{\"nil\", fields{&Options{}}, nil},\n\t\t{\"nilOptions\", fields{nil}, nil},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif got := tt.fields.o.GetSSHOptions(); !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"Options.GetSSHOptions() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestOptions_GetWebhooks(t *testing.T) {\n\ttype fields struct {\n\t\to *Options\n\t}\n\ttests := []struct {\n\t\tname   string\n\t\tfields fields\n\t\twant   []*Webhook\n\t}{\n\t\t{\"ok\", fields{&Options{Webhooks: []*Webhook{\n\t\t\t{Name: \"foo\"},\n\t\t\t{Name: \"bar\"},\n\t\t}}},\n\t\t\t[]*Webhook{\n\t\t\t\t{Name: \"foo\"},\n\t\t\t\t{Name: \"bar\"},\n\t\t\t},\n\t\t},\n\t\t{\"nil\", fields{&Options{}}, nil},\n\t\t{\"nilOptions\", fields{nil}, nil},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif got := tt.fields.o.GetWebhooks(); !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"Options.GetWebhooks() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestProvisionerX509Options_HasTemplate(t *testing.T) {\n\ttype fields struct {\n\t\tTemplate     string\n\t\tTemplateFile string\n\t\tTemplateData json.RawMessage\n\t}\n\ttests := []struct {\n\t\tname   string\n\t\tfields fields\n\t\twant   bool\n\t}{\n\t\t{\"template\", fields{Template: \"the template\"}, true},\n\t\t{\"templateFile\", fields{TemplateFile: \"the template file\"}, true},\n\t\t{\"false\", fields{}, false},\n\t\t{\"falseWithTemplateData\", fields{TemplateData: []byte(`{\"foo\":\"bar\"}`)}, false},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\to := &X509Options{\n\t\t\t\tTemplate:     tt.fields.Template,\n\t\t\t\tTemplateFile: tt.fields.TemplateFile,\n\t\t\t\tTemplateData: tt.fields.TemplateData,\n\t\t\t}\n\t\t\tif got := o.HasTemplate(); got != tt.want {\n\t\t\t\tt.Errorf(\"ProvisionerOptions.HasTemplate() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestTemplateOptions(t *testing.T) {\n\tcsr := parseCertificateRequest(t, \"testdata/certs/ecdsa.csr\")\n\tdata := x509util.TemplateData{\n\t\tx509util.SubjectKey: x509util.Subject{\n\t\t\tCommonName: \"foobar\",\n\t\t},\n\t\tx509util.SANsKey: []x509util.SubjectAlternativeName{\n\t\t\t{Type: \"dns\", Value: \"foo.com\"},\n\t\t},\n\t}\n\ttype args struct {\n\t\to    *Options\n\t\tdata x509util.TemplateData\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\targs    args\n\t\twant    x509util.Options\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", args{nil, data}, x509util.Options{\n\t\t\tCertBuffer: bytes.NewBufferString(`{\n\t\"subject\": {\"commonName\":\"foobar\"},\n\t\"sans\": [{\"type\":\"dns\",\"value\":\"foo.com\"}],\n\t\"keyUsage\": [\"digitalSignature\"],\n\t\"extKeyUsage\": [\"serverAuth\", \"clientAuth\"]\n}`)}, false},\n\t\t{\"okCustomTemplate\", args{&Options{X509: &X509Options{Template: x509util.DefaultIIDLeafTemplate}}, data}, x509util.Options{\n\t\t\tCertBuffer: bytes.NewBufferString(`{\n\t\"subject\": {\"commonName\": \"foo\"},\n\t\"sans\": [{\"type\":\"dns\",\"value\":\"foo.com\"}],\n\t\"keyUsage\": [\"digitalSignature\"],\n\t\"extKeyUsage\": [\"serverAuth\", \"clientAuth\"]\n}`)}, false},\n\t\t{\"fail\", args{&Options{X509: &X509Options{TemplateData: []byte(`{\"badJSON`)}}, data}, x509util.Options{}, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tcof, err := TemplateOptions(tt.args.o, tt.args.data)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"TemplateOptions() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tvar opts x509util.Options\n\t\t\tif cof != nil {\n\t\t\t\tfor _, fn := range cof.Options(SignOptions{}) {\n\t\t\t\t\tif err := fn(csr, &opts); err != nil {\n\t\t\t\t\t\tt.Errorf(\"x509util.Options() error = %v\", err)\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(opts, tt.want) {\n\t\t\t\tt.Errorf(\"x509util.Option = %v, want %v\", opts, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestCustomTemplateOptions(t *testing.T) {\n\tcsr := parseCertificateRequest(t, \"testdata/certs/ecdsa.csr\")\n\tcsrCertificate := `{\"version\":0,\"subject\":{\"commonName\":\"foo\"},\"rawSubject\":\"MA4xDDAKBgNVBAMTA2Zvbw==\",\"dnsNames\":[\"foo\"],\"emailAddresses\":null,\"ipAddresses\":null,\"uris\":null,\"sans\":null,\"extensions\":[{\"id\":\"2.5.29.17\",\"critical\":false,\"value\":\"MAWCA2Zvbw==\"}],\"keyUsage\":null,\"extKeyUsage\":[],\"unknownExtKeyUsage\":null,\"basicConstraints\":null,\"signatureAlgorithm\":\"\"}`\n\tdata := x509util.TemplateData{\n\t\tx509util.SubjectKey: x509util.Subject{\n\t\t\tCommonName: \"foobar\",\n\t\t},\n\t\tx509util.SANsKey: []x509util.SubjectAlternativeName{\n\t\t\t{Type: \"dns\", Value: \"foo.com\"},\n\t\t},\n\t}\n\ttype args struct {\n\t\to               *Options\n\t\tdata            x509util.TemplateData\n\t\tdefaultTemplate string\n\t\tuserOptions     SignOptions\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\targs    args\n\t\twant    x509util.Options\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", args{nil, data, x509util.DefaultLeafTemplate, SignOptions{}}, x509util.Options{\n\t\t\tCertBuffer: bytes.NewBufferString(`{\n\t\"subject\": {\"commonName\":\"foobar\"},\n\t\"sans\": [{\"type\":\"dns\",\"value\":\"foo.com\"}],\n\t\"keyUsage\": [\"digitalSignature\"],\n\t\"extKeyUsage\": [\"serverAuth\", \"clientAuth\"]\n}`)}, false},\n\t\t{\"okIID\", args{nil, data, x509util.DefaultIIDLeafTemplate, SignOptions{}}, x509util.Options{\n\t\t\tCertBuffer: bytes.NewBufferString(`{\n\t\"subject\": {\"commonName\": \"foo\"},\n\t\"sans\": [{\"type\":\"dns\",\"value\":\"foo.com\"}],\n\t\"keyUsage\": [\"digitalSignature\"],\n\t\"extKeyUsage\": [\"serverAuth\", \"clientAuth\"]\n}`)}, false},\n\t\t{\"okNoData\", args{&Options{}, nil, x509util.DefaultLeafTemplate, SignOptions{}}, x509util.Options{\n\t\t\tCertBuffer: bytes.NewBufferString(`{\n\t\"subject\": null,\n\t\"sans\": null,\n\t\"keyUsage\": [\"digitalSignature\"],\n\t\"extKeyUsage\": [\"serverAuth\", \"clientAuth\"]\n}`)}, false},\n\t\t{\"okTemplateData\", args{&Options{X509: &X509Options{TemplateData: []byte(`{\"foo\":\"bar\"}`)}}, data, x509util.DefaultLeafTemplate, SignOptions{}}, x509util.Options{\n\t\t\tCertBuffer: bytes.NewBufferString(`{\n\t\"subject\": {\"commonName\":\"foobar\"},\n\t\"sans\": [{\"type\":\"dns\",\"value\":\"foo.com\"}],\n\t\"keyUsage\": [\"digitalSignature\"],\n\t\"extKeyUsage\": [\"serverAuth\", \"clientAuth\"]\n}`)}, false},\n\t\t{\"okTemplate\", args{&Options{X509: &X509Options{Template: \"{{ toJson .Insecure.CR }}\"}}, data, x509util.DefaultLeafTemplate, SignOptions{}}, x509util.Options{\n\t\t\tCertBuffer: bytes.NewBufferString(csrCertificate)}, false},\n\t\t{\"okFile\", args{&Options{X509: &X509Options{TemplateFile: \"./testdata/templates/cr.tpl\"}}, data, x509util.DefaultLeafTemplate, SignOptions{}}, x509util.Options{\n\t\t\tCertBuffer: bytes.NewBufferString(csrCertificate)}, false},\n\t\t{\"okBase64\", args{&Options{X509: &X509Options{Template: \"e3sgdG9Kc29uIC5JbnNlY3VyZS5DUiB9fQ==\"}}, data, x509util.DefaultLeafTemplate, SignOptions{}}, x509util.Options{\n\t\t\tCertBuffer: bytes.NewBufferString(csrCertificate)}, false},\n\t\t{\"okUserOptions\", args{&Options{X509: &X509Options{Template: `{\"foo\": \"{{.Insecure.User.foo}}\"}`}}, data, x509util.DefaultLeafTemplate, SignOptions{TemplateData: []byte(`{\"foo\":\"bar\"}`)}}, x509util.Options{\n\t\t\tCertBuffer: bytes.NewBufferString(`{\"foo\": \"bar\"}`),\n\t\t}, false},\n\t\t{\"okBadUserOptions\", args{&Options{X509: &X509Options{Template: `{\"foo\": \"{{.Insecure.User.foo}}\"}`}}, data, x509util.DefaultLeafTemplate, SignOptions{TemplateData: []byte(`{\"badJSON\"}`)}}, x509util.Options{\n\t\t\tCertBuffer: bytes.NewBufferString(`{\"foo\": \"<no value>\"}`),\n\t\t}, false},\n\t\t{\"okNullTemplateData\", args{&Options{X509: &X509Options{TemplateData: []byte(`null`)}}, data, x509util.DefaultLeafTemplate, SignOptions{}}, x509util.Options{\n\t\t\tCertBuffer: bytes.NewBufferString(`{\n\t\"subject\": {\"commonName\":\"foobar\"},\n\t\"sans\": [{\"type\":\"dns\",\"value\":\"foo.com\"}],\n\t\"keyUsage\": [\"digitalSignature\"],\n\t\"extKeyUsage\": [\"serverAuth\", \"clientAuth\"]\n}`)}, false},\n\t\t{\"fail\", args{&Options{X509: &X509Options{TemplateData: []byte(`{\"badJSON`)}}, data, x509util.DefaultLeafTemplate, SignOptions{}}, x509util.Options{}, true},\n\t\t{\"failTemplateData\", args{&Options{X509: &X509Options{TemplateData: []byte(`{\"badJSON}`)}}, data, x509util.DefaultLeafTemplate, SignOptions{}}, x509util.Options{}, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tcof, err := CustomTemplateOptions(tt.args.o, tt.args.data, tt.args.defaultTemplate)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"CustomTemplateOptions() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tvar opts x509util.Options\n\t\t\tif cof != nil {\n\t\t\t\tfor _, fn := range cof.Options(tt.args.userOptions) {\n\t\t\t\t\tif err := fn(csr, &opts); err != nil {\n\t\t\t\t\t\tt.Errorf(\"x509util.Options() error = %v\", err)\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(opts, tt.want) {\n\t\t\t\tt.Errorf(\"x509util.Option = %v, want %v\", opts, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_unsafeParseSigned(t *testing.T) {\n\t//nolint:gosec // no credentials here\n\tokToken := \"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJqYW5lQGRvZS5jb20iLCJpc3MiOiJodHRwczovL2RvZS5jb20iLCJqdGkiOiI4ZmYzMjQ4MS1mZDVmLTRlMmUtOTZkZi05MDhjMTI3Yzg1ZjciLCJpYXQiOjE1OTUzNjAwMjgsImV4cCI6MTU5NTM2MzYyOH0.aid8UuhFucJOFHXaob9zpNtVvhul9ulTGsA52mU6XIw\"\n\ttype args struct {\n\t\ts string\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\targs    args\n\t\twant    map[string]interface{}\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", args{okToken}, map[string]interface{}{\n\t\t\t\"sub\": \"jane@doe.com\",\n\t\t\t\"iss\": \"https://doe.com\",\n\t\t\t\"jti\": \"8ff32481-fd5f-4e2e-96df-908c127c85f7\",\n\t\t\t\"iat\": float64(1595360028),\n\t\t\t\"exp\": float64(1595363628),\n\t\t}, false},\n\t\t{\"failToken\", args{\"foobar\"}, nil, true},\n\t\t{\"failPayload\", args{\"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.ew.aid8UuhFucJOFHXaob9zpNtVvhul9ulTGsA52mU6XIw\"}, nil, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := unsafeParseSigned(tt.args.s)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"unsafeParseSigned() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"unsafeParseSigned() = \\n%v, want \\n%v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestX509Options_IsWildcardLiteralAllowed(t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\toptions *X509Options\n\t\twant    bool\n\t}{\n\t\t{\n\t\t\tname:    \"nil-options\",\n\t\t\toptions: nil,\n\t\t\twant:    true,\n\t\t},\n\t\t{\n\t\t\tname: \"set-true\",\n\t\t\toptions: &X509Options{\n\t\t\t\tAllowWildcardNames: true,\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"set-false\",\n\t\t\toptions: &X509Options{\n\t\t\t\tAllowWildcardNames: false,\n\t\t\t},\n\t\t\twant: false,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif got := tt.options.AreWildcardNamesAllowed(); got != tt.want {\n\t\t\t\tt.Errorf(\"X509PolicyOptions.IsWildcardLiteralAllowed() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "authority/provisioner/policy.go",
    "content": "package provisioner\n\nimport \"github.com/smallstep/certificates/authority/policy\"\n\ntype policyEngine struct {\n\tx509Policy    policy.X509Policy\n\tsshHostPolicy policy.HostPolicy\n\tsshUserPolicy policy.UserPolicy\n}\n\nfunc newPolicyEngine(options *Options) (*policyEngine, error) {\n\tif options == nil {\n\t\t//nolint:nilnil // legacy\n\t\treturn nil, nil\n\t}\n\n\tvar (\n\t\tx509Policy    policy.X509Policy\n\t\tsshHostPolicy policy.HostPolicy\n\t\tsshUserPolicy policy.UserPolicy\n\t\terr           error\n\t)\n\n\t// Initialize the x509 allow/deny policy engine\n\tif x509Policy, err = policy.NewX509PolicyEngine(options.GetX509Options()); err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Initialize the SSH allow/deny policy engine for host certificates\n\tif sshHostPolicy, err = policy.NewSSHHostPolicyEngine(options.GetSSHOptions()); err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Initialize the SSH allow/deny policy engine for user certificates\n\tif sshUserPolicy, err = policy.NewSSHUserPolicyEngine(options.GetSSHOptions()); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &policyEngine{\n\t\tx509Policy:    x509Policy,\n\t\tsshHostPolicy: sshHostPolicy,\n\t\tsshUserPolicy: sshUserPolicy,\n\t}, nil\n}\n\nfunc (p *policyEngine) getX509() policy.X509Policy {\n\tif p == nil {\n\t\treturn nil\n\t}\n\treturn p.x509Policy\n}\n\nfunc (p *policyEngine) getSSHHost() policy.HostPolicy {\n\tif p == nil {\n\t\treturn nil\n\t}\n\treturn p.sshHostPolicy\n}\n\nfunc (p *policyEngine) getSSHUser() policy.UserPolicy {\n\tif p == nil {\n\t\treturn nil\n\t}\n\treturn p.sshUserPolicy\n}\n"
  },
  {
    "path": "authority/provisioner/provisioner.go",
    "content": "package provisioner\n\nimport (\n\t\"context\"\n\t\"crypto/x509\"\n\t\"encoding/json\"\n\tstderrors \"errors\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n\n\t\"github.com/pkg/errors\"\n\tkmsapi \"go.step.sm/crypto/kms/apiv1\"\n\t\"golang.org/x/crypto/ssh\"\n\n\t\"github.com/smallstep/certificates/errs\"\n)\n\n// Interface is the interface that all provisioner types must implement.\ntype Interface interface {\n\tGetID() string\n\tGetIDForToken() string\n\tGetTokenID(token string) (string, error)\n\tGetName() string\n\tGetType() Type\n\tGetEncryptedKey() (kid string, key string, ok bool)\n\tInit(config Config) error\n\tAuthorizeSign(ctx context.Context, token string) ([]SignOption, error)\n\tAuthorizeRevoke(ctx context.Context, token string) error\n\tAuthorizeRenew(ctx context.Context, cert *x509.Certificate) error\n\tAuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, error)\n\tAuthorizeSSHRevoke(ctx context.Context, token string) error\n\tAuthorizeSSHRenew(ctx context.Context, token string) (*ssh.Certificate, error)\n\tAuthorizeSSHRekey(ctx context.Context, token string) (*ssh.Certificate, []SignOption, error)\n}\n\n// HTTPClient is the interface implemented by the HTTP clients used by the\n// provisioners.\ntype HTTPClient interface {\n\tGet(string) (*http.Response, error)\n\tDo(*http.Request) (*http.Response, error)\n}\n\n// Uninitialized represents a disabled provisioner. Uninitialized provisioners\n// are created when the Init methods fails.\ntype Uninitialized struct {\n\tInterface\n\tReason error\n}\n\n// MarshalJSON returns the JSON encoding of the provisioner with the disabled\n// reason.\nfunc (p Uninitialized) MarshalJSON() ([]byte, error) {\n\tprovisionerJSON, err := json.Marshal(p.Interface)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treasonJSON, err := json.Marshal(struct {\n\t\tState       string `json:\"state\"`\n\t\tStateReason string `json:\"stateReason\"`\n\t}{\"Uninitialized\", p.Reason.Error()})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treasonJSON[0] = ','\n\treturn append(provisionerJSON[:len(provisionerJSON)-1], reasonJSON...), nil\n}\n\n// ErrAllowTokenReuse is an error that is returned by provisioners that allows\n// the reuse of tokens.\n//\n// This is, for example, returned by the Azure provisioner when\n// DisableTrustOnFirstUse is set to true. Azure caches tokens for up to 24hr and\n// has no mechanism for getting a different token - this can be an issue when\n// rebooting a VM. In contrast, AWS and GCP have facilities for requesting a new\n// token. Therefore, for the Azure provisioner we are enabling token reuse, with\n// the understanding that we are not following security best practices\nvar ErrAllowTokenReuse = stderrors.New(\"allow token reuse\")\n\n// ErrTokenFlowNotSupported is an error that is returned by provisioners on\n// GetTokenID when the use of tokens is not supported.\nvar ErrTokenFlowNotSupported = stderrors.New(\"token flow is not supported\")\n\n// ErrNotImplemented is an error returned when one method is not implemented.\nvar ErrNotImplemented = stderrors.New(\"not implemented\")\n\n// Audiences stores all supported audiences by request type.\ntype Audiences struct {\n\tSign      []string\n\tRenew     []string\n\tRevoke    []string\n\tSSHSign   []string\n\tSSHRevoke []string\n\tSSHRenew  []string\n\tSSHRekey  []string\n}\n\n// All returns all supported audiences across all request types in one list.\nfunc (a Audiences) All() (auds []string) {\n\tauds = a.Sign\n\tauds = append(auds, a.Renew...)\n\tauds = append(auds, a.Revoke...)\n\tauds = append(auds, a.SSHSign...)\n\tauds = append(auds, a.SSHRevoke...)\n\tauds = append(auds, a.SSHRenew...)\n\tauds = append(auds, a.SSHRekey...)\n\treturn\n}\n\n// WithFragment returns a copy of audiences where the url audiences contains the\n// given fragment.\nfunc (a Audiences) WithFragment(fragment string) Audiences {\n\tret := Audiences{\n\t\tSign:      make([]string, len(a.Sign)),\n\t\tRenew:     make([]string, len(a.Renew)),\n\t\tRevoke:    make([]string, len(a.Revoke)),\n\t\tSSHSign:   make([]string, len(a.SSHSign)),\n\t\tSSHRevoke: make([]string, len(a.SSHRevoke)),\n\t\tSSHRenew:  make([]string, len(a.SSHRenew)),\n\t\tSSHRekey:  make([]string, len(a.SSHRekey)),\n\t}\n\tfor i, s := range a.Sign {\n\t\tif u, err := url.Parse(s); err == nil {\n\t\t\tret.Sign[i] = u.ResolveReference(&url.URL{Fragment: fragment}).String()\n\t\t} else {\n\t\t\tret.Sign[i] = s\n\t\t}\n\t}\n\tfor i, s := range a.Renew {\n\t\tif u, err := url.Parse(s); err == nil {\n\t\t\tret.Renew[i] = u.ResolveReference(&url.URL{Fragment: fragment}).String()\n\t\t} else {\n\t\t\tret.Renew[i] = s\n\t\t}\n\t}\n\tfor i, s := range a.Revoke {\n\t\tif u, err := url.Parse(s); err == nil {\n\t\t\tret.Revoke[i] = u.ResolveReference(&url.URL{Fragment: fragment}).String()\n\t\t} else {\n\t\t\tret.Revoke[i] = s\n\t\t}\n\t}\n\tfor i, s := range a.SSHSign {\n\t\tif u, err := url.Parse(s); err == nil {\n\t\t\tret.SSHSign[i] = u.ResolveReference(&url.URL{Fragment: fragment}).String()\n\t\t} else {\n\t\t\tret.SSHSign[i] = s\n\t\t}\n\t}\n\tfor i, s := range a.SSHRevoke {\n\t\tif u, err := url.Parse(s); err == nil {\n\t\t\tret.SSHRevoke[i] = u.ResolveReference(&url.URL{Fragment: fragment}).String()\n\t\t} else {\n\t\t\tret.SSHRevoke[i] = s\n\t\t}\n\t}\n\tfor i, s := range a.SSHRenew {\n\t\tif u, err := url.Parse(s); err == nil {\n\t\t\tret.SSHRenew[i] = u.ResolveReference(&url.URL{Fragment: fragment}).String()\n\t\t} else {\n\t\t\tret.SSHRenew[i] = s\n\t\t}\n\t}\n\tfor i, s := range a.SSHRekey {\n\t\tif u, err := url.Parse(s); err == nil {\n\t\t\tret.SSHRekey[i] = u.ResolveReference(&url.URL{Fragment: fragment}).String()\n\t\t} else {\n\t\t\tret.SSHRekey[i] = s\n\t\t}\n\t}\n\treturn ret\n}\n\n// generateSignAudience generates a sign audience with the format\n// https://<host>/1.0/sign#provisionerID\nfunc generateSignAudience(caURL, provisionerID string) (string, error) {\n\tu, err := url.Parse(caURL)\n\tif err != nil {\n\t\treturn \"\", errors.Wrapf(err, \"error parsing %s\", caURL)\n\t}\n\treturn u.ResolveReference(&url.URL{Path: \"/1.0/sign\", Fragment: provisionerID}).String(), nil\n}\n\n// Type indicates the provisioner Type.\ntype Type int\n\nconst (\n\tnoopType Type = 0\n\t// TypeJWK is used to indicate the JWK provisioners.\n\tTypeJWK Type = 1\n\t// TypeOIDC is used to indicate the OIDC provisioners.\n\tTypeOIDC Type = 2\n\t// TypeGCP is used to indicate the GCP provisioners.\n\tTypeGCP Type = 3\n\t// TypeAWS is used to indicate the AWS provisioners.\n\tTypeAWS Type = 4\n\t// TypeAzure is used to indicate the Azure provisioners.\n\tTypeAzure Type = 5\n\t// TypeACME is used to indicate the ACME provisioners.\n\tTypeACME Type = 6\n\t// TypeX5C is used to indicate the X5C provisioners.\n\tTypeX5C Type = 7\n\t// TypeK8sSA is used to indicate the X5C provisioners.\n\tTypeK8sSA Type = 8\n\t// TypeSSHPOP is used to indicate the SSHPOP provisioners.\n\tTypeSSHPOP Type = 9\n\t// TypeSCEP is used to indicate the SCEP provisioners\n\tTypeSCEP Type = 10\n\t// TypeNebula is used to indicate the Nebula provisioners\n\tTypeNebula Type = 11\n)\n\n// String returns the string representation of the type.\nfunc (t Type) String() string {\n\tswitch t {\n\tcase TypeJWK:\n\t\treturn \"JWK\"\n\tcase TypeOIDC:\n\t\treturn \"OIDC\"\n\tcase TypeGCP:\n\t\treturn \"GCP\"\n\tcase TypeAWS:\n\t\treturn \"AWS\"\n\tcase TypeAzure:\n\t\treturn \"Azure\"\n\tcase TypeACME:\n\t\treturn \"ACME\"\n\tcase TypeX5C:\n\t\treturn \"X5C\"\n\tcase TypeK8sSA:\n\t\treturn \"K8sSA\"\n\tcase TypeSSHPOP:\n\t\treturn \"SSHPOP\"\n\tcase TypeSCEP:\n\t\treturn \"SCEP\"\n\tcase TypeNebula:\n\t\treturn \"Nebula\"\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\n// SSHKeys represents the SSH User and Host public keys.\ntype SSHKeys struct {\n\tUserKeys []ssh.PublicKey\n\tHostKeys []ssh.PublicKey\n}\n\n// SCEPKeyManager is a KMS interface that combines a KeyManager with a\n// Decrypter.\ntype SCEPKeyManager interface {\n\tkmsapi.KeyManager\n\tkmsapi.Decrypter\n}\n\n// Config defines the default parameters used in the initialization of\n// provisioners.\ntype Config struct {\n\t// Claims are the default claims.\n\tClaims Claims\n\t// Audiences are the audiences used in the default provisioner, (JWK).\n\tAudiences Audiences\n\t// SSHKeys are the root SSH public keys.\n\tSSHKeys *SSHKeys\n\t// GetIdentityFunc is a function that returns an identity that will be\n\t// used by the provisioner to populate certificate attributes.\n\tGetIdentityFunc GetIdentityFunc\n\t// AuthorizeRenewFunc is a function that returns nil if a given X.509\n\t// certificate can be renewed.\n\tAuthorizeRenewFunc AuthorizeRenewFunc\n\t// AuthorizeSSHRenewFunc is a function that returns nil if a given SSH\n\t// certificate can be renewed.\n\tAuthorizeSSHRenewFunc AuthorizeSSHRenewFunc\n\t// WebhookClient is an HTTP client used when performing webhook requests.\n\tWebhookClient HTTPClient\n\t// SCEPKeyManager, if defined, is the interface used by SCEP provisioners.\n\tSCEPKeyManager SCEPKeyManager\n\t// HTTPClient is an HTTP client that trusts the system cert pool and the CA\n\t// roots.\n\tHTTPClient HTTPClient\n\t// WrapTransport references the function that should wrap any [http.Transport] initialized\n\t// down the Config's chain.\n\tWrapTransport TransportWrapper\n}\n\ntype provisioner struct {\n\tType string `json:\"type\"`\n}\n\n// List represents a list of provisioners.\ntype List []Interface\n\n// UnmarshalJSON implements json.Unmarshaler and allows to unmarshal a list of a\n// interfaces into the right type.\nfunc (l *List) UnmarshalJSON(data []byte) error {\n\tps := []json.RawMessage{}\n\tif err := json.Unmarshal(data, &ps); err != nil {\n\t\treturn errors.Wrap(err, \"error unmarshaling provisioner list\")\n\t}\n\n\t*l = List{}\n\tfor _, data := range ps {\n\t\tvar typ provisioner\n\t\tif err := json.Unmarshal(data, &typ); err != nil {\n\t\t\treturn errors.Errorf(\"error unmarshaling provisioner\")\n\t\t}\n\t\tvar p Interface\n\t\tswitch strings.ToLower(typ.Type) {\n\t\tcase \"jwk\":\n\t\t\tp = &JWK{}\n\t\tcase \"oidc\":\n\t\t\tp = &OIDC{}\n\t\tcase \"gcp\":\n\t\t\tp = &GCP{}\n\t\tcase \"aws\":\n\t\t\tp = &AWS{}\n\t\tcase \"azure\":\n\t\t\tp = &Azure{}\n\t\tcase \"acme\":\n\t\t\tp = &ACME{}\n\t\tcase \"x5c\":\n\t\t\tp = &X5C{}\n\t\tcase \"k8ssa\":\n\t\t\tp = &K8sSA{}\n\t\tcase \"sshpop\":\n\t\t\tp = &SSHPOP{}\n\t\tcase \"scep\":\n\t\t\tp = &SCEP{}\n\t\tcase \"nebula\":\n\t\t\tp = &Nebula{}\n\t\tdefault:\n\t\t\t// Skip unsupported provisioners. A client using this method may be\n\t\t\t// compiled with a version of smallstep/certificates that does not\n\t\t\t// support a specific provisioner type. If we don't skip unknown\n\t\t\t// provisioners, a client encountering an unknown provisioner will\n\t\t\t// break. Rather than break the client, we skip the provisioner.\n\t\t\t// TODO: accept a pluggable logger (depending on client) that can\n\t\t\t// warn the user that an unknown provisioner was found and suggest\n\t\t\t// that the user update their client's dependency on\n\t\t\t// step/certificates and recompile.\n\t\t\tcontinue\n\t\t}\n\t\tif err := json.Unmarshal(data, p); err != nil {\n\t\t\treturn errors.Wrap(err, \"error unmarshaling provisioner\")\n\t\t}\n\t\t*l = append(*l, p)\n\t}\n\n\treturn nil\n}\n\ntype base struct{}\n\n// AuthorizeSign returns an unimplemented error. Provisioners should overwrite\n// this method if they will support authorizing tokens for signing x509 Certificates.\nfunc (b *base) AuthorizeSign(context.Context, string) ([]SignOption, error) {\n\treturn nil, errs.Unauthorized(\"provisioner.AuthorizeSign not implemented\")\n}\n\n// AuthorizeRevoke returns an unimplemented error. Provisioners should overwrite\n// this method if they will support authorizing tokens for revoking x509 Certificates.\nfunc (b *base) AuthorizeRevoke(context.Context, string) error {\n\treturn errs.Unauthorized(\"provisioner.AuthorizeRevoke not implemented\")\n}\n\n// AuthorizeRenew returns an unimplemented error. Provisioners should overwrite\n// this method if they will support authorizing tokens for renewing x509 Certificates.\nfunc (b *base) AuthorizeRenew(context.Context, *x509.Certificate) error {\n\treturn errs.Unauthorized(\"provisioner.AuthorizeRenew not implemented\")\n}\n\n// AuthorizeSSHSign returns an unimplemented error. Provisioners should overwrite\n// this method if they will support authorizing tokens for signing SSH Certificates.\nfunc (b *base) AuthorizeSSHSign(context.Context, string) ([]SignOption, error) {\n\treturn nil, errs.Unauthorized(\"provisioner.AuthorizeSSHSign not implemented\")\n}\n\n// AuthorizeSSHRevoke returns an unimplemented error. Provisioners should overwrite\n// this method if they will support authorizing tokens for revoking SSH Certificates.\nfunc (b *base) AuthorizeSSHRevoke(context.Context, string) error {\n\treturn errs.Unauthorized(\"provisioner.AuthorizeSSHRevoke not implemented\")\n}\n\n// AuthorizeSSHRenew returns an unimplemented error. Provisioners should overwrite\n// this method if they will support authorizing tokens for renewing SSH Certificates.\nfunc (b *base) AuthorizeSSHRenew(context.Context, string) (*ssh.Certificate, error) {\n\treturn nil, errs.Unauthorized(\"provisioner.AuthorizeSSHRenew not implemented\")\n}\n\n// AuthorizeSSHRekey returns an unimplemented error. Provisioners should overwrite\n// this method if they will support authorizing tokens for rekeying SSH Certificates.\nfunc (b *base) AuthorizeSSHRekey(context.Context, string) (*ssh.Certificate, []SignOption, error) {\n\treturn nil, nil, errs.Unauthorized(\"provisioner.AuthorizeSSHRekey not implemented\")\n}\n\n// Permissions defines extra extensions and critical options to grant to an SSH certificate.\ntype Permissions struct {\n\tExtensions      map[string]string `json:\"extensions\"`\n\tCriticalOptions map[string]string `json:\"criticalOptions\"`\n}\n\n// RAInfo is the information about a provisioner present in RA tokens generated\n// by StepCAS.\ntype RAInfo struct {\n\tAuthorityID     string `json:\"authorityId,omitempty\"`\n\tEndpointID      string `json:\"endpointId,omitempty\"`\n\tProvisionerID   string `json:\"provisionerId,omitempty\"`\n\tProvisionerType string `json:\"provisionerType,omitempty\"`\n\tProvisionerName string `json:\"provisionerName,omitempty\"`\n}\n\n// raProvisioner wraps a provisioner with RA data.\ntype raProvisioner struct {\n\tInterface\n\traInfo *RAInfo\n}\n\n// RAInfo returns the RAInfo in the wrapped provisioner.\nfunc (p *raProvisioner) RAInfo() *RAInfo {\n\treturn p.raInfo\n}\n\n// MockProvisioner for testing\ntype MockProvisioner struct {\n\tMret1, Mret2, Mret3 interface{}\n\tMerr                error\n\tMgetID              func() string\n\tMgetIDForToken      func() string\n\tMgetTokenID         func(string) (string, error)\n\tMgetName            func() string\n\tMgetType            func() Type\n\tMgetEncryptedKey    func() (string, string, bool)\n\tMinit               func(Config) error\n\tMauthorizeSign      func(ctx context.Context, ott string) ([]SignOption, error)\n\tMauthorizeRenew     func(ctx context.Context, cert *x509.Certificate) error\n\tMauthorizeRevoke    func(ctx context.Context, ott string) error\n\tMauthorizeSSHSign   func(ctx context.Context, ott string) ([]SignOption, error)\n\tMauthorizeSSHRenew  func(ctx context.Context, ott string) (*ssh.Certificate, error)\n\tMauthorizeSSHRekey  func(ctx context.Context, ott string) (*ssh.Certificate, []SignOption, error)\n\tMauthorizeSSHRevoke func(ctx context.Context, ott string) error\n}\n\n// GetID mock\nfunc (m *MockProvisioner) GetID() string {\n\tif m.MgetID != nil {\n\t\treturn m.MgetID()\n\t}\n\treturn m.Mret1.(string)\n}\n\n// GetIDForToken mock\nfunc (m *MockProvisioner) GetIDForToken() string {\n\tif m.MgetIDForToken != nil {\n\t\treturn m.MgetIDForToken()\n\t}\n\treturn m.Mret1.(string)\n}\n\n// GetTokenID mock\nfunc (m *MockProvisioner) GetTokenID(token string) (string, error) {\n\tif m.MgetTokenID != nil {\n\t\treturn m.MgetTokenID(token)\n\t}\n\tif m.Mret1 == nil {\n\t\treturn \"\", m.Merr\n\t}\n\treturn m.Mret1.(string), m.Merr\n}\n\n// GetName mock\nfunc (m *MockProvisioner) GetName() string {\n\tif m.MgetName != nil {\n\t\treturn m.MgetName()\n\t}\n\treturn m.Mret1.(string)\n}\n\n// GetType mock\nfunc (m *MockProvisioner) GetType() Type {\n\tif m.MgetType != nil {\n\t\treturn m.MgetType()\n\t}\n\treturn m.Mret1.(Type)\n}\n\n// GetEncryptedKey mock\nfunc (m *MockProvisioner) GetEncryptedKey() (string, string, bool) {\n\tif m.MgetEncryptedKey != nil {\n\t\treturn m.MgetEncryptedKey()\n\t}\n\treturn m.Mret1.(string), m.Mret2.(string), m.Mret3.(bool)\n}\n\n// Init mock\nfunc (m *MockProvisioner) Init(c Config) error {\n\tif m.Minit != nil {\n\t\treturn m.Minit(c)\n\t}\n\treturn m.Merr\n}\n\n// AuthorizeSign mock\nfunc (m *MockProvisioner) AuthorizeSign(ctx context.Context, ott string) ([]SignOption, error) {\n\tif m.MauthorizeSign != nil {\n\t\treturn m.MauthorizeSign(ctx, ott)\n\t}\n\treturn m.Mret1.([]SignOption), m.Merr\n}\n\n// AuthorizeRevoke mock\nfunc (m *MockProvisioner) AuthorizeRevoke(ctx context.Context, ott string) error {\n\tif m.MauthorizeRevoke != nil {\n\t\treturn m.MauthorizeRevoke(ctx, ott)\n\t}\n\treturn m.Merr\n}\n\n// AuthorizeRenew mock\nfunc (m *MockProvisioner) AuthorizeRenew(ctx context.Context, c *x509.Certificate) error {\n\tif m.MauthorizeRenew != nil {\n\t\treturn m.MauthorizeRenew(ctx, c)\n\t}\n\treturn m.Merr\n}\n\n// AuthorizeSSHSign mock\nfunc (m *MockProvisioner) AuthorizeSSHSign(ctx context.Context, ott string) ([]SignOption, error) {\n\tif m.MauthorizeSign != nil {\n\t\treturn m.MauthorizeSign(ctx, ott)\n\t}\n\treturn m.Mret1.([]SignOption), m.Merr\n}\n\n// AuthorizeSSHRenew mock\nfunc (m *MockProvisioner) AuthorizeSSHRenew(ctx context.Context, ott string) (*ssh.Certificate, error) {\n\tif m.MauthorizeRenew != nil {\n\t\treturn m.MauthorizeSSHRenew(ctx, ott)\n\t}\n\treturn m.Mret1.(*ssh.Certificate), m.Merr\n}\n\n// AuthorizeSSHRekey mock\nfunc (m *MockProvisioner) AuthorizeSSHRekey(ctx context.Context, ott string) (*ssh.Certificate, []SignOption, error) {\n\tif m.MauthorizeSSHRekey != nil {\n\t\treturn m.MauthorizeSSHRekey(ctx, ott)\n\t}\n\treturn m.Mret1.(*ssh.Certificate), m.Mret2.([]SignOption), m.Merr\n}\n\n// AuthorizeSSHRevoke mock\nfunc (m *MockProvisioner) AuthorizeSSHRevoke(ctx context.Context, ott string) error {\n\tif m.MauthorizeSSHRevoke != nil {\n\t\treturn m.MauthorizeSSHRevoke(ctx, ott)\n\t}\n\treturn m.Merr\n}\n"
  },
  {
    "path": "authority/provisioner/provisioner_test.go",
    "content": "package provisioner\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"net/http\"\n\t\"testing\"\n\n\t\"github.com/go-jose/go-jose/v3\"\n\t\"github.com/smallstep/certificates/api/render\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"golang.org/x/crypto/ssh\"\n)\n\nfunc TestType_String(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tt    Type\n\t\twant string\n\t}{\n\t\t{\"JWK\", TypeJWK, \"JWK\"},\n\t\t{\"OIDC\", TypeOIDC, \"OIDC\"},\n\t\t{\"AWS\", TypeAWS, \"AWS\"},\n\t\t{\"Azure\", TypeAzure, \"Azure\"},\n\t\t{\"GCP\", TypeGCP, \"GCP\"},\n\t\t{\"noop\", noopType, \"\"},\n\t\t{\"notFound\", 1000, \"\"},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif got := tt.t.String(); got != tt.want {\n\t\t\t\tt.Errorf(\"Type.String() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSanitizeSSHUserPrincipal(t *testing.T) {\n\ttype args struct {\n\t\temail string\n\t}\n\ttests := []struct {\n\t\tname string\n\t\targs args\n\t\twant string\n\t}{\n\t\t{\"simple\", args{\"foobar\"}, \"foobar\"},\n\t\t{\"camelcase\", args{\"FooBar\"}, \"foobar\"},\n\t\t{\"email\", args{\"foo@example.com\"}, \"foo\"},\n\t\t{\"email with dots\", args{\"foo.bar.zar@example.com\"}, \"foobarzar\"},\n\t\t{\"email with dashes\", args{\"foo-bar-zar@example.com\"}, \"foo-bar-zar\"},\n\t\t{\"email with underscores\", args{\"foo_bar_zar@example.com\"}, \"foo_bar_zar\"},\n\t\t{\"email with symbols\", args{\"Foo.Bar0123456789!#$%&'*+-/=?^_`{|}~;@example.com\"}, \"foobar0123456789________-___________\"},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif got := SanitizeSSHUserPrincipal(tt.args.email); got != tt.want {\n\t\t\t\tt.Errorf(\"SanitizeSSHUserPrincipal() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDefaultIdentityFunc(t *testing.T) {\n\ttype test struct {\n\t\tp         Interface\n\t\temail     string\n\t\tusernames []string\n\t\terr       error\n\t\tidentity  *Identity\n\t}\n\ttests := map[string]func(*testing.T) test{\n\t\t\"fail/unsupported-provisioner\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tp:   &X5C{},\n\t\t\t\terr: errors.New(\"provisioner type '*provisioner.X5C' not supported by identity function\"),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tp:        &OIDC{},\n\t\t\t\temail:    \"max.furman@smallstep.com\",\n\t\t\t\tidentity: &Identity{Usernames: []string{\"maxfurman\", \"max.furman\", \"max.furman@smallstep.com\"}},\n\t\t\t}\n\t\t},\n\t\t\"ok letter case\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tp:        &OIDC{},\n\t\t\t\temail:    \"Max.Furman@smallstep.com\",\n\t\t\t\tidentity: &Identity{Usernames: []string{\"maxfurman\", \"Max.Furman\", \"Max.Furman@smallstep.com\"}},\n\t\t\t}\n\t\t},\n\t\t\"ok simple\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tp:        &OIDC{},\n\t\t\t\temail:    \"john@smallstep.com\",\n\t\t\t\tidentity: &Identity{Usernames: []string{\"john\", \"john@smallstep.com\"}},\n\t\t\t}\n\t\t},\n\t\t\"ok simple letter case\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tp:        &OIDC{},\n\t\t\t\temail:    \"John@smallstep.com\",\n\t\t\t\tidentity: &Identity{Usernames: []string{\"john\", \"John\", \"John@smallstep.com\"}},\n\t\t\t}\n\t\t},\n\t\t\"ok symbol\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tp:        &OIDC{},\n\t\t\t\temail:    \"John+Doe@smallstep.com\",\n\t\t\t\tidentity: &Identity{Usernames: []string{\"john_doe\", \"John+Doe\", \"John+Doe@smallstep.com\"}},\n\t\t\t}\n\t\t},\n\t\t\"ok username\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tp:         &OIDC{},\n\t\t\t\temail:     \"john@smallstep.com\",\n\t\t\t\tusernames: []string{\"johnny\"},\n\t\t\t\tidentity:  &Identity{Usernames: []string{\"john\", \"john@smallstep.com\"}},\n\t\t\t}\n\t\t},\n\t\t\"ok usernames\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tp:         &OIDC{},\n\t\t\t\temail:     \"john@smallstep.com\",\n\t\t\t\tusernames: []string{\"johnny\", \"js\", \"\", \"johnny\", \"\"},\n\t\t\t\tidentity:  &Identity{Usernames: []string{\"john\", \"john@smallstep.com\"}},\n\t\t\t}\n\t\t},\n\t\t\"ok empty username\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tp:         &OIDC{},\n\t\t\t\temail:     \"john@smallstep.com\",\n\t\t\t\tusernames: []string{\"\"},\n\t\t\t\tidentity:  &Identity{Usernames: []string{\"john\", \"john@smallstep.com\"}},\n\t\t\t}\n\t\t},\n\t\t\"ok/badname\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tp:        &OIDC{},\n\t\t\t\temail:    \"$%^#_>@smallstep.com\",\n\t\t\t\tidentity: &Identity{Usernames: []string{\"______\", \"$%^#_>\", \"$%^#_>@smallstep.com\"}},\n\t\t\t}\n\t\t},\n\t}\n\tfor name, get := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ttc := get(t)\n\t\t\tidentity, err := DefaultIdentityFunc(context.Background(), tc.p, tc.email)\n\t\t\tif err != nil {\n\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\tassert.Equal(t, tc.err.Error(), err.Error())\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif assert.Nil(t, tc.err) {\n\t\t\t\t\tassert.Equal(t, identity.Usernames, tc.identity.Usernames)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestUnimplementedMethods(t *testing.T) {\n\ttests := []struct {\n\t\tname   string\n\t\tp      Interface\n\t\tmethod Method\n\t}{\n\t\t{\"jwk/sshRekey\", &JWK{}, SSHRekeyMethod},\n\t\t{\"jwk/sshRenew\", &JWK{}, SSHRenewMethod},\n\t\t{\"aws/revoke\", &AWS{}, RevokeMethod},\n\t\t{\"aws/sshRenew\", &AWS{}, SSHRenewMethod},\n\t\t{\"aws/rekey\", &AWS{}, SSHRekeyMethod},\n\t\t{\"aws/sshRevoke\", &AWS{}, SSHRevokeMethod},\n\t\t{\"azure/revoke\", &Azure{}, RevokeMethod},\n\t\t{\"azure/sshRenew\", &Azure{}, SSHRenewMethod},\n\t\t{\"azure/sshRekey\", &Azure{}, SSHRekeyMethod},\n\t\t{\"azure/sshRevoke\", &Azure{}, SSHRevokeMethod},\n\t\t{\"gcp/revoke\", &GCP{}, RevokeMethod},\n\t\t{\"gcp/sshRenew\", &GCP{}, SSHRenewMethod},\n\t\t{\"gcp/sshRekey\", &GCP{}, SSHRekeyMethod},\n\t\t{\"gcp/sshRevoke\", &GCP{}, SSHRevokeMethod},\n\t\t{\"oidc/sshRenew\", &OIDC{}, SSHRenewMethod},\n\t\t{\"oidc/sshRekey\", &OIDC{}, SSHRekeyMethod},\n\t\t{\"x5c/sshRenew\", &X5C{}, SSHRenewMethod},\n\t\t{\"x5c/sshRekey\", &X5C{}, SSHRekeyMethod},\n\t\t{\"x5c/sshRevoke\", &X5C{}, SSHRekeyMethod},\n\t\t{\"acme/sshSign\", &ACME{}, SSHSignMethod},\n\t\t{\"acme/sshRekey\", &ACME{}, SSHRekeyMethod},\n\t\t{\"acme/sshRenew\", &ACME{}, SSHRenewMethod},\n\t\t{\"acme/sshRevoke\", &ACME{}, SSHRevokeMethod},\n\t\t{\"sshpop/sign\", &SSHPOP{}, SignMethod},\n\t\t{\"sshpop/renew\", &SSHPOP{}, RenewMethod},\n\t\t{\"sshpop/revoke\", &SSHPOP{}, RevokeMethod},\n\t\t{\"sshpop/sshSign\", &SSHPOP{}, SSHSignMethod},\n\t\t{\"k8ssa/sshRekey\", &K8sSA{}, SSHRekeyMethod},\n\t\t{\"k8ssa/sshRenew\", &K8sSA{}, SSHRenewMethod},\n\t\t{\"k8ssa/sshRevoke\", &K8sSA{}, SSHRevokeMethod},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tvar (\n\t\t\t\terr error\n\t\t\t\tmsg string\n\t\t\t)\n\n\t\t\tswitch tt.method {\n\t\t\tcase SignMethod:\n\t\t\t\tvar signOpts []SignOption\n\t\t\t\tsignOpts, err = tt.p.AuthorizeSign(context.Background(), \"\")\n\t\t\t\tassert.Nil(t, signOpts)\n\t\t\t\tmsg = \"provisioner.AuthorizeSign not implemented\"\n\t\t\tcase RenewMethod:\n\t\t\t\terr = tt.p.AuthorizeRenew(context.Background(), nil)\n\t\t\t\tmsg = \"provisioner.AuthorizeRenew not implemented\"\n\t\t\tcase RevokeMethod:\n\t\t\t\terr = tt.p.AuthorizeRevoke(context.Background(), \"\")\n\t\t\t\tmsg = \"provisioner.AuthorizeRevoke not implemented\"\n\t\t\tcase SSHSignMethod:\n\t\t\t\tvar signOpts []SignOption\n\t\t\t\tsignOpts, err = tt.p.AuthorizeSSHSign(context.Background(), \"\")\n\t\t\t\tassert.Nil(t, signOpts)\n\t\t\t\tmsg = \"provisioner.AuthorizeSSHSign not implemented\"\n\t\t\tcase SSHRenewMethod:\n\t\t\t\tvar cert *ssh.Certificate\n\t\t\t\tcert, err = tt.p.AuthorizeSSHRenew(context.Background(), \"\")\n\t\t\t\tassert.Nil(t, cert)\n\t\t\t\tmsg = \"provisioner.AuthorizeSSHRenew not implemented\"\n\t\t\tcase SSHRekeyMethod:\n\t\t\t\tvar (\n\t\t\t\t\tcert     *ssh.Certificate\n\t\t\t\t\tsignOpts []SignOption\n\t\t\t\t)\n\t\t\t\tcert, signOpts, err = tt.p.AuthorizeSSHRekey(context.Background(), \"\")\n\t\t\t\tassert.Nil(t, cert)\n\t\t\t\tassert.Nil(t, signOpts)\n\t\t\t\tmsg = \"provisioner.AuthorizeSSHRekey not implemented\"\n\t\t\tcase SSHRevokeMethod:\n\t\t\t\terr = tt.p.AuthorizeSSHRevoke(context.Background(), \"\")\n\t\t\t\tmsg = \"provisioner.AuthorizeSSHRevoke not implemented\"\n\t\t\tdefault:\n\t\t\t\tt.Errorf(\"unexpected method %s\", tt.method)\n\t\t\t}\n\t\t\tvar sc render.StatusCodedError\n\t\t\tif assert.True(t, errors.As(err, &sc), \"error does not implement StatusCodedError interface\") {\n\t\t\t\tassert.Equal(t, http.StatusUnauthorized, sc.StatusCode())\n\t\t\t}\n\t\t\tassert.Equal(t, msg, err.Error())\n\t\t})\n\t}\n}\n\nfunc TestUninitialized_MarshalJSON(t *testing.T) {\n\tp := &JWK{\n\t\tName: \"bad-provisioner\",\n\t\tType: \"JWK\",\n\t\tKey: &jose.JSONWebKey{\n\t\t\tKey: []byte(\"foo\"),\n\t\t},\n\t}\n\n\ttype fields struct {\n\t\tInterface Interface\n\t\tReason    error\n\t}\n\ttests := []struct {\n\t\tname      string\n\t\tfields    fields\n\t\twant      []byte\n\t\tassertion assert.ErrorAssertionFunc\n\t}{\n\t\t{\"ok\", fields{p, errors.New(\"bad key\")}, []byte(`{\"type\":\"JWK\",\"name\":\"bad-provisioner\",\"key\":{\"kty\":\"oct\",\"k\":\"Zm9v\"},\"state\":\"Uninitialized\",\"stateReason\":\"bad key\"}`), assert.NoError},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tp := Uninitialized{\n\t\t\t\tInterface: tt.fields.Interface,\n\t\t\t\tReason:    tt.fields.Reason,\n\t\t\t}\n\t\t\tgot, err := p.MarshalJSON()\n\t\t\ttt.assertion(t, err)\n\t\t\tassert.Equal(t, tt.want, got)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "authority/provisioner/scep.go",
    "content": "package provisioner\n\nimport (\n\t\"context\"\n\t\"crypto\"\n\t\"crypto/rsa\"\n\t\"crypto/subtle\"\n\t\"crypto/x509\"\n\t\"encoding/pem\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n\n\t\"github.com/smallstep/linkedca\"\n\t\"go.step.sm/crypto/kms\"\n\tkmsapi \"go.step.sm/crypto/kms/apiv1\"\n\t\"go.step.sm/crypto/x509util\"\n\n\t\"github.com/smallstep/certificates/internal/httptransport\"\n\t\"github.com/smallstep/certificates/webhook\"\n)\n\n// SCEP is the SCEP provisioner type, an entity that can authorize the\n// SCEP provisioning flow\ntype SCEP struct {\n\t*base\n\tID                string   `json:\"-\"`\n\tType              string   `json:\"type\"`\n\tName              string   `json:\"name\"`\n\tForceCN           bool     `json:\"forceCN,omitempty\"`\n\tChallengePassword string   `json:\"challenge,omitempty\"`\n\tCapabilities      []string `json:\"capabilities,omitempty\"`\n\n\t// IncludeRoot makes the provisioner return the CA root in addition to the\n\t// intermediate in the GetCACerts response\n\tIncludeRoot bool `json:\"includeRoot,omitempty\"`\n\n\t// ExcludeIntermediate makes the provisioner skip the intermediate CA in the\n\t// GetCACerts response\n\tExcludeIntermediate bool `json:\"excludeIntermediate,omitempty\"`\n\n\t// MinimumPublicKeyLength is the minimum length for public keys in CSRs\n\tMinimumPublicKeyLength int `json:\"minimumPublicKeyLength,omitempty\"`\n\n\t// TODO(hs): also support a separate signer configuration?\n\tDecrypterCertificate []byte `json:\"decrypterCertificate,omitempty\"`\n\tDecrypterKeyPEM      []byte `json:\"decrypterKeyPEM,omitempty\"`\n\tDecrypterKeyURI      string `json:\"decrypterKey,omitempty\"`\n\tDecrypterKeyPassword string `json:\"decrypterKeyPassword,omitempty\"`\n\n\t// Numerical identifier for the ContentEncryptionAlgorithm as defined in github.com/mozilla-services/pkcs7\n\t// at https://github.com/mozilla-services/pkcs7/blob/33d05740a3526e382af6395d3513e73d4e66d1cb/encrypt.go#L63\n\t// Defaults to 0, being DES-CBC\n\tEncryptionAlgorithmIdentifier int      `json:\"encryptionAlgorithmIdentifier,omitempty\"`\n\tOptions                       *Options `json:\"options,omitempty\"`\n\tClaims                        *Claims  `json:\"claims,omitempty\"`\n\tctl                           *Controller\n\tencryptionAlgorithm           int\n\tchallengeValidationController *challengeValidationController\n\tnotificationController        *notificationController\n\tkeyManager                    SCEPKeyManager\n\tdecrypter                     crypto.Decrypter\n\tdecrypterCertificate          *x509.Certificate\n\tsigner                        crypto.Signer\n\tsignerCertificate             *x509.Certificate\n}\n\n// GetID returns the provisioner unique identifier.\nfunc (s *SCEP) GetID() string {\n\tif s.ID != \"\" {\n\t\treturn s.ID\n\t}\n\treturn s.GetIDForToken()\n}\n\n// GetIDForToken returns an identifier that will be used to load the provisioner\n// from a token.\nfunc (s *SCEP) GetIDForToken() string {\n\treturn \"scep/\" + s.Name\n}\n\n// GetName returns the name of the provisioner.\nfunc (s *SCEP) GetName() string {\n\treturn s.Name\n}\n\n// GetType returns the type of provisioner.\nfunc (s *SCEP) GetType() Type {\n\treturn TypeSCEP\n}\n\n// GetEncryptedKey returns the base provisioner encrypted key if it's defined.\nfunc (s *SCEP) GetEncryptedKey() (string, string, bool) {\n\treturn \"\", \"\", false\n}\n\n// GetTokenID returns the identifier of the token. This provisioner will always\n// return [ErrTokenFlowNotSupported].\nfunc (s *SCEP) GetTokenID(string) (string, error) {\n\treturn \"\", ErrTokenFlowNotSupported\n}\n\n// GetOptions returns the configured provisioner options.\nfunc (s *SCEP) GetOptions() *Options {\n\treturn s.Options\n}\n\n// DefaultTLSCertDuration returns the default TLS cert duration enforced by\n// the provisioner.\nfunc (s *SCEP) DefaultTLSCertDuration() time.Duration {\n\treturn s.ctl.Claimer.DefaultTLSCertDuration()\n}\n\ntype challengeValidationController struct {\n\tclient        HTTPClient\n\twrapTransport httptransport.Wrapper\n\twebhooks      []*Webhook\n}\n\n// newChallengeValidationController creates a new challengeValidationController\n// that performs challenge validation through webhooks.\nfunc newChallengeValidationController(client HTTPClient, tw httptransport.Wrapper, webhooks []*Webhook) *challengeValidationController {\n\tscepHooks := []*Webhook{}\n\tfor _, wh := range webhooks {\n\t\tif wh.Kind != linkedca.Webhook_SCEPCHALLENGE.String() {\n\t\t\tcontinue\n\t\t}\n\t\tif !isCertTypeOK(wh) {\n\t\t\tcontinue\n\t\t}\n\t\tscepHooks = append(scepHooks, wh)\n\t}\n\treturn &challengeValidationController{\n\t\tclient:        client,\n\t\twrapTransport: tw,\n\t\twebhooks:      scepHooks,\n\t}\n}\n\nvar (\n\tErrSCEPChallengeInvalid   = errors.New(\"webhook server did not allow request\")\n\tErrSCEPNotificationFailed = errors.New(\"scep notification failed\")\n)\n\n// Validate executes zero or more configured webhooks to\n// validate the SCEP challenge. If at least one of them indicates\n// the challenge value is accepted, validation succeeds. In\n// that case, the other webhooks will be skipped. If none of\n// the webhooks indicates the value of the challenge was accepted,\n// an error is returned.\nfunc (c *challengeValidationController) Validate(ctx context.Context, csr *x509.CertificateRequest, provisionerName, challenge, transactionID string) ([]SignCSROption, error) {\n\tvar opts []SignCSROption\n\n\tfor _, wh := range c.webhooks {\n\t\treq, err := webhook.NewRequestBody(webhook.WithX509CertificateRequest(csr))\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed creating new webhook request: %w\", err)\n\t\t}\n\t\treq.ProvisionerName = provisionerName\n\t\treq.SCEPChallenge = challenge\n\t\treq.SCEPTransactionID = transactionID\n\t\tresp, err := wh.DoWithContext(ctx, c.client, c.wrapTransport, req, nil) // TODO(hs): support templated URL? Requires some refactoring\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed executing webhook request: %w\", err)\n\t\t}\n\t\tif resp.Allow {\n\t\t\topts = append(opts, TemplateDataModifierFunc(func(data x509util.TemplateData) {\n\t\t\t\tdata.SetWebhook(wh.Name, resp.Data)\n\t\t\t}))\n\t\t}\n\t}\n\n\tif len(opts) == 0 {\n\t\treturn nil, ErrSCEPChallengeInvalid\n\t}\n\n\treturn opts, nil\n}\n\ntype notificationController struct {\n\tclient        HTTPClient\n\twrapTransport httptransport.Wrapper\n\twebhooks      []*Webhook\n}\n\n// newNotificationController creates a new notificationController\n// that performs SCEP notifications through webhooks.\nfunc newNotificationController(client HTTPClient, tw httptransport.Wrapper, webhooks []*Webhook) *notificationController {\n\tscepHooks := []*Webhook{}\n\tfor _, wh := range webhooks {\n\t\tif wh.Kind != linkedca.Webhook_NOTIFYING.String() {\n\t\t\tcontinue\n\t\t}\n\t\tif !isCertTypeOK(wh) {\n\t\t\tcontinue\n\t\t}\n\t\tscepHooks = append(scepHooks, wh)\n\t}\n\treturn &notificationController{\n\t\tclient:        client,\n\t\twrapTransport: tw,\n\t\twebhooks:      scepHooks,\n\t}\n}\n\nfunc (c *notificationController) Success(ctx context.Context, csr *x509.CertificateRequest, cert *x509.Certificate, transactionID string) error {\n\tfor _, wh := range c.webhooks {\n\t\treq, err := webhook.NewRequestBody(webhook.WithX509CertificateRequest(csr), webhook.WithX509Certificate(nil, cert)) // TODO(hs): pass in the x509util.Certifiate too?\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed creating new webhook request: %w\", err)\n\t\t}\n\t\treq.X509Certificate.Raw = cert.Raw // adding the full certificate DER bytes\n\t\treq.SCEPTransactionID = transactionID\n\t\tif _, err = wh.DoWithContext(ctx, c.client, c.wrapTransport, req, nil); err != nil {\n\t\t\treturn fmt.Errorf(\"failed executing webhook request: %w: %w\", ErrSCEPNotificationFailed, err)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (c *notificationController) Failure(ctx context.Context, csr *x509.CertificateRequest, transactionID string, errorCode int, errorDescription string) error {\n\tfor _, wh := range c.webhooks {\n\t\treq, err := webhook.NewRequestBody(webhook.WithX509CertificateRequest(csr))\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed creating new webhook request: %w\", err)\n\t\t}\n\t\treq.SCEPTransactionID = transactionID\n\t\treq.SCEPErrorCode = errorCode\n\t\treq.SCEPErrorDescription = errorDescription\n\t\tif _, err = wh.DoWithContext(ctx, c.client, c.wrapTransport, req, nil); err != nil {\n\t\t\treturn fmt.Errorf(\"failed executing webhook request: %w: %w\", ErrSCEPNotificationFailed, err)\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// isCertTypeOK returns whether or not the webhook can be used\n// with the SCEP challenge validation webhook controller.\nfunc isCertTypeOK(wh *Webhook) bool {\n\tif wh.CertType == linkedca.Webhook_ALL.String() || wh.CertType == \"\" {\n\t\treturn true\n\t}\n\treturn linkedca.Webhook_X509.String() == wh.CertType\n}\n\n// Init initializes and validates the fields of a SCEP type.\nfunc (s *SCEP) Init(config Config) (err error) {\n\tswitch {\n\tcase s.Type == \"\":\n\t\treturn errors.New(\"provisioner type cannot be empty\")\n\tcase s.Name == \"\":\n\t\treturn errors.New(\"provisioner name cannot be empty\")\n\t}\n\n\t// Default to 2048 bits minimum public key length (for CSRs) if not set\n\tif s.MinimumPublicKeyLength == 0 {\n\t\ts.MinimumPublicKeyLength = 2048\n\t}\n\tif s.MinimumPublicKeyLength%8 != 0 {\n\t\treturn errors.Errorf(\"%d bits is not exactly divisible by 8\", s.MinimumPublicKeyLength)\n\t}\n\n\t// Set the encryption algorithm to use\n\ts.encryptionAlgorithm = s.EncryptionAlgorithmIdentifier // TODO(hs): we might want to upgrade the default security to AES-CBC?\n\tif s.encryptionAlgorithm < 0 || s.encryptionAlgorithm > 4 {\n\t\treturn errors.New(\"only encryption algorithm identifiers from 0 to 4 are valid\")\n\t}\n\n\t// Prepare the SCEP challenge validator\n\ts.challengeValidationController = newChallengeValidationController(\n\t\tconfig.WebhookClient,\n\t\tconfig.WrapTransport,\n\t\ts.GetOptions().GetWebhooks(),\n\t)\n\n\t// Prepare the SCEP notification controller\n\ts.notificationController = newNotificationController(\n\t\tconfig.WebhookClient,\n\t\tconfig.WrapTransport,\n\t\ts.GetOptions().GetWebhooks(),\n\t)\n\n\t// parse the decrypter key PEM contents if available\n\tif len(s.DecrypterKeyPEM) > 0 {\n\t\t// try reading the PEM for validation\n\t\tblock, rest := pem.Decode(s.DecrypterKeyPEM)\n\t\tif len(rest) > 0 {\n\t\t\treturn errors.New(\"failed parsing decrypter key: trailing data\")\n\t\t}\n\t\tif block == nil {\n\t\t\treturn errors.New(\"failed parsing decrypter key: no PEM block found\")\n\t\t}\n\n\t\topts := kms.Options{\n\t\t\tType: kmsapi.SoftKMS,\n\t\t}\n\t\tkm, err := kms.New(context.Background(), opts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed initializing kms: %w\", err)\n\t\t}\n\t\tscepKeyManager, ok := km.(SCEPKeyManager)\n\t\tif !ok {\n\t\t\treturn fmt.Errorf(\"%q is not a kmsapi.Decrypter\", opts.Type)\n\t\t}\n\t\ts.keyManager = scepKeyManager\n\n\t\tif s.decrypter, err = s.keyManager.CreateDecrypter(&kmsapi.CreateDecrypterRequest{\n\t\t\tDecryptionKeyPEM: s.DecrypterKeyPEM,\n\t\t\tPassword:         []byte(s.DecrypterKeyPassword),\n\t\t\tPasswordPrompter: kmsapi.NonInteractivePasswordPrompter,\n\t\t}); err != nil {\n\t\t\treturn fmt.Errorf(\"failed creating decrypter: %w\", err)\n\t\t}\n\t\tif s.signer, err = s.keyManager.CreateSigner(&kmsapi.CreateSignerRequest{\n\t\t\tSigningKeyPEM:    s.DecrypterKeyPEM, // TODO(hs): support distinct signer key in the future?\n\t\t\tPassword:         []byte(s.DecrypterKeyPassword),\n\t\t\tPasswordPrompter: kmsapi.NonInteractivePasswordPrompter,\n\t\t}); err != nil {\n\t\t\treturn fmt.Errorf(\"failed creating signer: %w\", err)\n\t\t}\n\t}\n\n\tif s.DecrypterKeyURI != \"\" {\n\t\tkmsType, err := kmsapi.TypeOf(s.DecrypterKeyURI)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed parsing decrypter key: %w\", err)\n\t\t}\n\n\t\tif config.SCEPKeyManager != nil {\n\t\t\ts.keyManager = config.SCEPKeyManager\n\t\t} else {\n\t\t\tif kmsType == kmsapi.DefaultKMS {\n\t\t\t\tkmsType = kmsapi.SoftKMS\n\t\t\t}\n\t\t\topts := kms.Options{\n\t\t\t\tType: kmsType,\n\t\t\t\tURI:  s.DecrypterKeyURI,\n\t\t\t}\n\t\t\tkm, err := kms.New(context.Background(), opts)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed initializing kms: %w\", err)\n\t\t\t}\n\t\t\tscepKeyManager, ok := km.(SCEPKeyManager)\n\t\t\tif !ok {\n\t\t\t\treturn fmt.Errorf(\"%q is not a kmsapi.Decrypter\", opts.Type)\n\t\t\t}\n\t\t\ts.keyManager = scepKeyManager\n\t\t}\n\n\t\t// Create decrypter and signer with the same key:\n\t\t// TODO(hs): support distinct signer key in the future?\n\t\tif s.decrypter, err = s.keyManager.CreateDecrypter(&kmsapi.CreateDecrypterRequest{\n\t\t\tDecryptionKey:    s.DecrypterKeyURI,\n\t\t\tPassword:         []byte(s.DecrypterKeyPassword),\n\t\t\tPasswordPrompter: kmsapi.NonInteractivePasswordPrompter,\n\t\t}); err != nil {\n\t\t\treturn fmt.Errorf(\"failed creating decrypter: %w\", err)\n\t\t}\n\t\tif s.signer, err = s.keyManager.CreateSigner(&kmsapi.CreateSignerRequest{\n\t\t\tSigningKey:       s.DecrypterKeyURI,\n\t\t\tPassword:         []byte(s.DecrypterKeyPassword),\n\t\t\tPasswordPrompter: kmsapi.NonInteractivePasswordPrompter,\n\t\t}); err != nil {\n\t\t\treturn fmt.Errorf(\"failed creating signer: %w\", err)\n\t\t}\n\t}\n\n\t// parse the decrypter certificate contents if available\n\tif len(s.DecrypterCertificate) > 0 {\n\t\tblock, rest := pem.Decode(s.DecrypterCertificate)\n\t\tif len(rest) > 0 {\n\t\t\treturn errors.New(\"failed parsing decrypter certificate: trailing data\")\n\t\t}\n\t\tif block == nil {\n\t\t\treturn errors.New(\"failed parsing decrypter certificate: no PEM block found\")\n\t\t}\n\t\tif s.decrypterCertificate, err = x509.ParseCertificate(block.Bytes); err != nil {\n\t\t\treturn fmt.Errorf(\"failed parsing decrypter certificate: %w\", err)\n\t\t}\n\t\t// the decrypter certificate is also the signer certificate\n\t\ts.signerCertificate = s.decrypterCertificate\n\t}\n\n\t// TODO(hs): alternatively, check if the KMS keyManager is a CertificateManager\n\t// and load the certificate corresponding to the decryption key?\n\n\t// Final validation for the decrypter.\n\tif s.decrypter != nil {\n\t\tdecrypterPublicKey, ok := s.decrypter.Public().(*rsa.PublicKey)\n\t\tif !ok {\n\t\t\treturn fmt.Errorf(\"only RSA keys are supported\")\n\t\t}\n\t\tif s.decrypterCertificate == nil {\n\t\t\treturn fmt.Errorf(\"provisioner %q does not have a decrypter certificate set\", s.Name)\n\t\t}\n\t\tif !decrypterPublicKey.Equal(s.decrypterCertificate.PublicKey) {\n\t\t\treturn errors.New(\"mismatch between decrypter certificate and decrypter public keys\")\n\t\t}\n\t}\n\n\t// TODO: add other, SCEP specific, options?\n\n\ts.ctl, err = NewController(s, s.Claims, config, s.Options)\n\treturn\n}\n\n// AuthorizeSign does not do any verification, because all verification is handled\n// in the SCEP protocol. This method returns a list of modifiers / constraints\n// on the resulting certificate.\nfunc (s *SCEP) AuthorizeSign(context.Context, string) ([]SignOption, error) {\n\treturn []SignOption{\n\t\ts,\n\t\t// modifiers / withOptions\n\t\tnewProvisionerExtensionOption(TypeSCEP, s.Name, \"\").WithControllerOptions(s.ctl),\n\t\tnewForceCNOption(s.ForceCN),\n\t\tprofileDefaultDuration(s.ctl.Claimer.DefaultTLSCertDuration()),\n\t\t// validators\n\t\tnewPublicKeyMinimumLengthValidator(s.MinimumPublicKeyLength),\n\t\tnewValidityValidator(s.ctl.Claimer.MinTLSCertDuration(), s.ctl.Claimer.MaxTLSCertDuration()),\n\t\tnewX509NamePolicyValidator(s.ctl.getPolicy().getX509()),\n\t\ts.ctl.newWebhookController(nil, linkedca.Webhook_X509),\n\t}, nil\n}\n\n// GetCapabilities returns the CA capabilities\nfunc (s *SCEP) GetCapabilities() []string {\n\treturn s.Capabilities\n}\n\n// ShouldIncludeRootInChain indicates if the CA should\n// return its intermediate, which is currently used for\n// both signing and decryption, as well as the root in\n// its chain.\nfunc (s *SCEP) ShouldIncludeRootInChain() bool {\n\treturn s.IncludeRoot\n}\n\n// ShouldIncludeIntermediateInChain indicates if the\n// CA should include the intermediate CA certificate in the\n// GetCACerts response. This is true by default, but can be\n// overridden through configuration in case SCEP clients\n// don't pick the right recipient.\nfunc (s *SCEP) ShouldIncludeIntermediateInChain() bool {\n\treturn !s.ExcludeIntermediate\n}\n\n// GetContentEncryptionAlgorithm returns the numeric identifier\n// for the pkcs7 package encryption algorithm to use.\nfunc (s *SCEP) GetContentEncryptionAlgorithm() int {\n\treturn s.encryptionAlgorithm\n}\n\n// ValidateChallenge validates the provided challenge. It starts by\n// selecting the validation method to use, then performs validation\n// according to that method.\nfunc (s *SCEP) ValidateChallenge(ctx context.Context, csr *x509.CertificateRequest, challenge, transactionID string) ([]SignCSROption, error) {\n\tif s.challengeValidationController == nil {\n\t\treturn nil, fmt.Errorf(\"provisioner %q wasn't initialized\", s.Name)\n\t}\n\tswitch s.selectValidationMethod() {\n\tcase validationMethodWebhook:\n\t\treturn s.challengeValidationController.Validate(ctx, csr, s.Name, challenge, transactionID)\n\tdefault:\n\t\tif subtle.ConstantTimeCompare([]byte(s.ChallengePassword), []byte(challenge)) == 0 {\n\t\t\treturn nil, errors.New(\"invalid challenge password provided\")\n\t\t}\n\t\treturn []SignCSROption{}, nil\n\t}\n}\n\nfunc (s *SCEP) NotifySuccess(ctx context.Context, csr *x509.CertificateRequest, cert *x509.Certificate, transactionID string) error {\n\tif s.notificationController == nil {\n\t\treturn fmt.Errorf(\"provisioner %q wasn't initialized\", s.Name)\n\t}\n\treturn s.notificationController.Success(ctx, csr, cert, transactionID)\n}\n\nfunc (s *SCEP) NotifyFailure(ctx context.Context, csr *x509.CertificateRequest, transactionID string, errorCode int, errorDescription string) error {\n\tif s.notificationController == nil {\n\t\treturn fmt.Errorf(\"provisioner %q wasn't initialized\", s.Name)\n\t}\n\treturn s.notificationController.Failure(ctx, csr, transactionID, errorCode, errorDescription)\n}\n\ntype validationMethod string\n\nconst (\n\tvalidationMethodNone    validationMethod = \"none\"\n\tvalidationMethodStatic  validationMethod = \"static\"\n\tvalidationMethodWebhook validationMethod = \"webhook\"\n)\n\n// selectValidationMethod returns the method to validate SCEP\n// challenges. If a webhook is configured with kind `SCEPCHALLENGE`,\n// the webhook method will be used. If a challenge password is set,\n// the static method is used. It will default to the `none` method.\nfunc (s *SCEP) selectValidationMethod() validationMethod {\n\tif len(s.challengeValidationController.webhooks) > 0 {\n\t\treturn validationMethodWebhook\n\t}\n\tif s.ChallengePassword != \"\" {\n\t\treturn validationMethodStatic\n\t}\n\treturn validationMethodNone\n}\n\n// GetDecrypter returns the provisioner specific decrypter,\n// used to decrypt SCEP request messages sent by a SCEP client.\n// The decrypter consists of a crypto.Decrypter (a private key)\n// and a certificate for the public key corresponding to the\n// private key.\nfunc (s *SCEP) GetDecrypter() (*x509.Certificate, crypto.Decrypter) {\n\treturn s.decrypterCertificate, s.decrypter\n}\n\n// GetSigner returns the provisioner specific signer, used to\n// sign SCEP response messages for the client. The signer consists\n// of a crypto.Signer and a certificate for the public key\n// corresponding to the private key.\nfunc (s *SCEP) GetSigner() (*x509.Certificate, crypto.Signer) {\n\treturn s.signerCertificate, s.signer\n}\n"
  },
  {
    "path": "authority/provisioner/scep_test.go",
    "content": "package provisioner\n\nimport (\n\t\"context\"\n\t\"crypto\"\n\t\"crypto/rand\"\n\t\"crypto/rsa\"\n\t\"crypto/x509\"\n\t\"crypto/x509/pkix\"\n\t\"encoding/json\"\n\t\"encoding/pem\"\n\t\"errors\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/smallstep/certificates/webhook\"\n\t\"github.com/smallstep/linkedca\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"go.step.sm/crypto/kms/softkms\"\n\t\"go.step.sm/crypto/minica\"\n\t\"go.step.sm/crypto/pemutil\"\n\t\"go.step.sm/crypto/x509util\"\n)\n\nfunc generateSCEP(t *testing.T) *SCEP {\n\tt.Helper()\n\n\tca, err := minica.New()\n\trequire.NoError(t, err)\n\n\tkey, err := rsa.GenerateKey(rand.Reader, 2048)\n\trequire.NoError(t, err)\n\n\tcert, err := ca.Sign(&x509.Certificate{\n\t\tSubject:   pkix.Name{CommonName: \"SCEP decrypter\"},\n\t\tPublicKey: key.Public(),\n\t})\n\trequire.NoError(t, err)\n\n\tcertPEM := pem.EncodeToMemory(&pem.Block{\n\t\tType: \"CERTIFICATE\", Bytes: cert.Raw,\n\t})\n\n\tblock, err := pemutil.Serialize(key, pemutil.WithPassword([]byte(\"password\")))\n\trequire.NoError(t, err)\n\tkeyPEM := pem.EncodeToMemory(block)\n\n\tp := &SCEP{\n\t\tType:                          \"SCEP\",\n\t\tName:                          \"scep\",\n\t\tChallengePassword:             \"password123\",\n\t\tMinimumPublicKeyLength:        0,\n\t\tDecrypterCertificate:          certPEM,\n\t\tDecrypterKeyPEM:               keyPEM,\n\t\tDecrypterKeyPassword:          \"password\",\n\t\tEncryptionAlgorithmIdentifier: 0,\n\t}\n\trequire.NoError(t, p.Init(Config{Claims: globalProvisionerClaims}))\n\treturn p\n\n}\n\nfunc Test_challengeValidationController_Validate(t *testing.T) {\n\tdummyCSR := &x509.CertificateRequest{\n\t\tRaw: []byte{1},\n\t}\n\ttype request struct {\n\t\tProvisionerName string                          `json:\"provisionerName,omitempty\"`\n\t\tRequest         *webhook.X509CertificateRequest `json:\"x509CertificateRequest,omitempty\"`\n\t\tChallenge       string                          `json:\"scepChallenge\"`\n\t\tTransactionID   string                          `json:\"scepTransactionID\"`\n\t}\n\ttype response struct {\n\t\tAllow bool `json:\"allow\"`\n\t\tData  any  `json:\"data\"`\n\t}\n\tnokServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\treq := &request{}\n\t\terr := json.NewDecoder(r.Body).Decode(req)\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, \"my-scep-provisioner\", req.ProvisionerName)\n\t\tassert.Equal(t, \"not-allowed\", req.Challenge)\n\t\tassert.Equal(t, \"transaction-1\", req.TransactionID)\n\t\tb, err := json.Marshal(response{Allow: false})\n\t\trequire.NoError(t, err)\n\t\tw.WriteHeader(200)\n\t\tw.Write(b)\n\t}))\n\tokServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\treq := &request{}\n\t\terr := json.NewDecoder(r.Body).Decode(req)\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, \"my-scep-provisioner\", req.ProvisionerName)\n\t\tassert.Equal(t, \"challenge\", req.Challenge)\n\t\tassert.Equal(t, \"transaction-1\", req.TransactionID)\n\t\tif assert.NotNil(t, req.Request) {\n\t\t\tassert.Equal(t, []byte{1}, req.Request.Raw)\n\t\t}\n\t\tresp := response{Allow: true}\n\t\tif r.Header.Get(\"X-Smallstep-Webhook-Id\") == \"webhook-id-2\" {\n\t\t\tresp.Data = map[string]any{\n\t\t\t\t\"ID\":    \"2adcbfec-5e4a-4b93-8913-640e24faf101\",\n\t\t\t\t\"Email\": \"admin@example.com\",\n\t\t\t}\n\t\t}\n\t\tb, err := json.Marshal(resp)\n\t\trequire.NoError(t, err)\n\t\tw.WriteHeader(200)\n\t\tw.Write(b)\n\t}))\n\tt.Cleanup(func() {\n\t\tnokServer.Close()\n\t\tokServer.Close()\n\t})\n\ttype fields struct {\n\t\tclient   *http.Client\n\t\twebhooks []*Webhook\n\t}\n\ttype args struct {\n\t\tprovisionerName string\n\t\tchallenge       string\n\t\ttransactionID   string\n\t}\n\ttests := []struct {\n\t\tname   string\n\t\tfields fields\n\t\targs   args\n\t\twant   x509util.TemplateData\n\t\texpErr error\n\t}{\n\t\t{\n\t\t\tname:   \"fail/no-webhook\",\n\t\t\tfields: fields{http.DefaultClient, nil},\n\t\t\targs:   args{\"my-scep-provisioner\", \"no-webhook\", \"transaction-1\"},\n\t\t\texpErr: errors.New(\"webhook server did not allow request\"),\n\t\t},\n\t\t{\n\t\t\tname: \"fail/wrong-cert-type\",\n\t\t\tfields: fields{http.DefaultClient, []*Webhook{\n\t\t\t\t{\n\t\t\t\t\tKind:     linkedca.Webhook_SCEPCHALLENGE.String(),\n\t\t\t\t\tCertType: linkedca.Webhook_SSH.String(),\n\t\t\t\t},\n\t\t\t}},\n\t\t\targs:   args{\"my-scep-provisioner\", \"wrong-cert-type\", \"transaction-1\"},\n\t\t\texpErr: errors.New(\"webhook server did not allow request\"),\n\t\t},\n\t\t{\n\t\t\tname: \"fail/wrong-secret-value\",\n\t\t\tfields: fields{http.DefaultClient, []*Webhook{\n\t\t\t\t{\n\t\t\t\t\tID:       \"webhook-id-1\",\n\t\t\t\t\tName:     \"webhook-name-1\",\n\t\t\t\t\tSecret:   \"{{}}\",\n\t\t\t\t\tKind:     linkedca.Webhook_SCEPCHALLENGE.String(),\n\t\t\t\t\tCertType: linkedca.Webhook_X509.String(),\n\t\t\t\t\tURL:      okServer.URL,\n\t\t\t\t},\n\t\t\t}},\n\t\t\targs: args{\n\t\t\t\tprovisionerName: \"my-scep-provisioner\",\n\t\t\t\tchallenge:       \"wrong-secret-value\",\n\t\t\t\ttransactionID:   \"transaction-1\",\n\t\t\t},\n\t\t\texpErr: errors.New(\"failed executing webhook request: illegal base64 data at input byte 0\"),\n\t\t},\n\t\t{\n\t\t\tname: \"fail/not-allowed\",\n\t\t\tfields: fields{http.DefaultClient, []*Webhook{\n\t\t\t\t{\n\t\t\t\t\tID:       \"webhook-id-1\",\n\t\t\t\t\tName:     \"webhook-name-1\",\n\t\t\t\t\tSecret:   \"MTIzNAo=\",\n\t\t\t\t\tKind:     linkedca.Webhook_SCEPCHALLENGE.String(),\n\t\t\t\t\tCertType: linkedca.Webhook_X509.String(),\n\t\t\t\t\tURL:      nokServer.URL,\n\t\t\t\t},\n\t\t\t}},\n\t\t\targs: args{\n\t\t\t\tprovisionerName: \"my-scep-provisioner\",\n\t\t\t\tchallenge:       \"not-allowed\",\n\t\t\t\ttransactionID:   \"transaction-1\",\n\t\t\t},\n\t\t\texpErr: errors.New(\"webhook server did not allow request\"),\n\t\t},\n\t\t{\n\t\t\tname: \"ok\",\n\t\t\tfields: fields{http.DefaultClient, []*Webhook{\n\t\t\t\t{\n\t\t\t\t\tID:       \"webhook-id-1\",\n\t\t\t\t\tName:     \"webhook-name-1\",\n\t\t\t\t\tSecret:   \"MTIzNAo=\",\n\t\t\t\t\tKind:     linkedca.Webhook_SCEPCHALLENGE.String(),\n\t\t\t\t\tCertType: linkedca.Webhook_X509.String(),\n\t\t\t\t\tURL:      okServer.URL,\n\t\t\t\t},\n\t\t\t}},\n\t\t\targs: args{\n\t\t\t\tprovisionerName: \"my-scep-provisioner\",\n\t\t\t\tchallenge:       \"challenge\",\n\t\t\t\ttransactionID:   \"transaction-1\",\n\t\t\t},\n\t\t\twant: x509util.TemplateData{\n\t\t\t\tx509util.WebhooksKey: map[string]any{\n\t\t\t\t\t\"webhook-name-1\": nil,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"ok with data\",\n\t\t\tfields: fields{http.DefaultClient, []*Webhook{\n\t\t\t\t{\n\t\t\t\t\tID:       \"webhook-id-2\",\n\t\t\t\t\tName:     \"webhook-name-2\",\n\t\t\t\t\tSecret:   \"MTIzNAo=\",\n\t\t\t\t\tKind:     linkedca.Webhook_SCEPCHALLENGE.String(),\n\t\t\t\t\tCertType: linkedca.Webhook_X509.String(),\n\t\t\t\t\tURL:      okServer.URL,\n\t\t\t\t},\n\t\t\t}},\n\t\t\targs: args{\n\t\t\t\tprovisionerName: \"my-scep-provisioner\",\n\t\t\t\tchallenge:       \"challenge\",\n\t\t\t\ttransactionID:   \"transaction-1\",\n\t\t\t},\n\t\t\twant: x509util.TemplateData{\n\t\t\t\tx509util.WebhooksKey: map[string]any{\n\t\t\t\t\t\"webhook-name-2\": map[string]any{\n\t\t\t\t\t\t\"ID\":    \"2adcbfec-5e4a-4b93-8913-640e24faf101\",\n\t\t\t\t\t\t\"Email\": \"admin@example.com\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tc := newChallengeValidationController(tt.fields.client, nil, tt.fields.webhooks)\n\t\t\tctx := context.Background()\n\t\t\tgot, err := c.Validate(ctx, dummyCSR, tt.args.provisionerName, tt.args.challenge, tt.args.transactionID)\n\t\t\tif tt.expErr != nil {\n\t\t\t\tassert.EqualError(t, err, tt.expErr.Error())\n\t\t\t\treturn\n\t\t\t}\n\t\t\tassert.NoError(t, err)\n\t\t\tdata := x509util.TemplateData{}\n\t\t\tfor _, o := range got {\n\t\t\t\tif m, ok := o.(TemplateDataModifier); ok {\n\t\t\t\t\tm.Modify(data)\n\t\t\t\t} else {\n\t\t\t\t\tt.Errorf(\"Validate() got = %T, want TemplateDataModifier\", o)\n\t\t\t\t}\n\t\t\t}\n\t\t\tassert.Equal(t, tt.want, data)\n\t\t})\n\t}\n}\n\nfunc TestController_isCertTypeOK(t *testing.T) {\n\tassert.True(t, isCertTypeOK(&Webhook{CertType: linkedca.Webhook_X509.String()}))\n\tassert.True(t, isCertTypeOK(&Webhook{CertType: linkedca.Webhook_ALL.String()}))\n\tassert.True(t, isCertTypeOK(&Webhook{CertType: \"\"}))\n\tassert.False(t, isCertTypeOK(&Webhook{CertType: linkedca.Webhook_SSH.String()}))\n}\n\nfunc Test_selectValidationMethod(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tp    *SCEP\n\t\twant validationMethod\n\t}{\n\t\t{\"webhooks\", &SCEP{\n\t\t\tName: \"SCEP\",\n\t\t\tType: \"SCEP\",\n\t\t\tOptions: &Options{\n\t\t\t\tWebhooks: []*Webhook{\n\t\t\t\t\t{\n\t\t\t\t\t\tName: \"challenge\",\n\t\t\t\t\t\tURL:  \"https://scep.challenge\",\n\t\t\t\t\t\tKind: linkedca.Webhook_SCEPCHALLENGE.String(),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t}, \"webhook\"},\n\t\t{\"challenge\", &SCEP{\n\t\t\tName:              \"SCEP\",\n\t\t\tType:              \"SCEP\",\n\t\t\tChallengePassword: \"pass\",\n\t\t}, \"static\"},\n\t\t{\"challenge-with-different-webhook\", &SCEP{\n\t\t\tName: \"SCEP\",\n\t\t\tType: \"SCEP\",\n\t\t\tOptions: &Options{\n\t\t\t\tWebhooks: []*Webhook{\n\t\t\t\t\t{\n\t\t\t\t\t\tName: \"authorizing\",\n\t\t\t\t\t\tURL:  \"https://scep.authorizing\",\n\t\t\t\t\t\tKind: linkedca.Webhook_AUTHORIZING.String(),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tChallengePassword: \"pass\",\n\t\t}, \"static\"},\n\t\t{\"none\", &SCEP{\n\t\t\tName: \"SCEP\",\n\t\t\tType: \"SCEP\",\n\t\t}, \"none\"},\n\t\t{\"none-with-different-webhook\", &SCEP{\n\t\t\tName: \"SCEP\",\n\t\t\tType: \"SCEP\",\n\t\t\tOptions: &Options{\n\t\t\t\tWebhooks: []*Webhook{\n\t\t\t\t\t{\n\t\t\t\t\t\tName: \"authorizing\",\n\t\t\t\t\t\tURL:  \"https://scep.authorizing\",\n\t\t\t\t\t\tKind: linkedca.Webhook_AUTHORIZING.String(),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t}, \"none\"},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\terr := tt.p.Init(Config{Claims: globalProvisionerClaims})\n\t\t\trequire.NoError(t, err)\n\t\t\tgot := tt.p.selectValidationMethod()\n\t\t\tassert.Equal(t, tt.want, got)\n\t\t})\n\t}\n}\n\nfunc TestSCEP_ValidateChallenge(t *testing.T) {\n\tdummyCSR := &x509.CertificateRequest{\n\t\tRaw: []byte{1},\n\t}\n\ttype request struct {\n\t\tProvisionerName string                          `json:\"provisionerName,omitempty\"`\n\t\tRequest         *webhook.X509CertificateRequest `json:\"x509CertificateRequest,omitempty\"`\n\t\tChallenge       string                          `json:\"scepChallenge\"`\n\t\tTransactionID   string                          `json:\"scepTransactionID\"`\n\t}\n\ttype response struct {\n\t\tAllow bool `json:\"allow\"`\n\t\tData  any  `json:\"data\"`\n\t}\n\tokServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\treq := &request{}\n\t\terr := json.NewDecoder(r.Body).Decode(req)\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, \"SCEP\", req.ProvisionerName)\n\t\tassert.Equal(t, \"webhook-challenge\", req.Challenge)\n\t\tassert.Equal(t, \"webhook-transaction-1\", req.TransactionID)\n\t\tif assert.NotNil(t, req.Request) {\n\t\t\tassert.Equal(t, []byte{1}, req.Request.Raw)\n\t\t}\n\t\tresp := response{Allow: true}\n\t\tif r.Header.Get(\"X-Smallstep-Webhook-Id\") == \"webhook-id-2\" {\n\t\t\tresp.Data = map[string]any{\n\t\t\t\t\"ID\":    \"2adcbfec-5e4a-4b93-8913-640e24faf101\",\n\t\t\t\t\"Email\": \"admin@example.com\",\n\t\t\t}\n\t\t}\n\t\tb, err := json.Marshal(resp)\n\t\trequire.NoError(t, err)\n\t\tw.WriteHeader(200)\n\t\tw.Write(b)\n\t}))\n\thttpclient := okServer.Client()\n\tt.Cleanup(okServer.Close)\n\ttype args struct {\n\t\tchallenge     string\n\t\ttransactionID string\n\t}\n\ttests := []struct {\n\t\tname   string\n\t\tp      *SCEP\n\t\tserver *httptest.Server\n\t\targs   args\n\t\twant   x509util.TemplateData\n\t\texpErr error\n\t}{\n\t\t{\"ok/webhooks\", &SCEP{\n\t\t\tName: \"SCEP\",\n\t\t\tType: \"SCEP\",\n\t\t\tOptions: &Options{\n\t\t\t\tWebhooks: []*Webhook{\n\t\t\t\t\t{\n\t\t\t\t\t\tID:       \"webhook-id-1\",\n\t\t\t\t\t\tName:     \"webhook-name-1\",\n\t\t\t\t\t\tSecret:   \"MTIzNAo=\",\n\t\t\t\t\t\tKind:     linkedca.Webhook_SCEPCHALLENGE.String(),\n\t\t\t\t\t\tCertType: linkedca.Webhook_X509.String(),\n\t\t\t\t\t\tURL:      okServer.URL,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t}, okServer, args{\"webhook-challenge\", \"webhook-transaction-1\"}, x509util.TemplateData{\n\t\t\tx509util.WebhooksKey: map[string]any{\n\t\t\t\t\"webhook-name-1\": nil,\n\t\t\t},\n\t\t}, nil},\n\t\t{\"ok/with-data\", &SCEP{\n\t\t\tName: \"SCEP\",\n\t\t\tType: \"SCEP\",\n\t\t\tOptions: &Options{\n\t\t\t\tWebhooks: []*Webhook{\n\t\t\t\t\t{\n\t\t\t\t\t\tID:       \"webhook-id-1\",\n\t\t\t\t\t\tName:     \"webhook-name-1\",\n\t\t\t\t\t\tSecret:   \"MTIzNAo=\",\n\t\t\t\t\t\tKind:     linkedca.Webhook_SCEPCHALLENGE.String(),\n\t\t\t\t\t\tCertType: linkedca.Webhook_X509.String(),\n\t\t\t\t\t\tURL:      okServer.URL,\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tID:       \"webhook-id-2\",\n\t\t\t\t\t\tName:     \"webhook-name-2\",\n\t\t\t\t\t\tSecret:   \"MTIzNAo=\",\n\t\t\t\t\t\tKind:     linkedca.Webhook_SCEPCHALLENGE.String(),\n\t\t\t\t\t\tCertType: linkedca.Webhook_X509.String(),\n\t\t\t\t\t\tURL:      okServer.URL,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t}, okServer, args{\"webhook-challenge\", \"webhook-transaction-1\"}, x509util.TemplateData{\n\t\t\tx509util.WebhooksKey: map[string]any{\n\t\t\t\t\"webhook-name-1\": nil,\n\t\t\t\t\"webhook-name-2\": map[string]any{\n\t\t\t\t\t\"ID\":    \"2adcbfec-5e4a-4b93-8913-640e24faf101\",\n\t\t\t\t\t\"Email\": \"admin@example.com\",\n\t\t\t\t},\n\t\t\t},\n\t\t}, nil},\n\t\t{\"fail/webhooks-secret-configuration\", &SCEP{\n\t\t\tName: \"SCEP\",\n\t\t\tType: \"SCEP\",\n\t\t\tOptions: &Options{\n\t\t\t\tWebhooks: []*Webhook{\n\t\t\t\t\t{\n\t\t\t\t\t\tID:       \"webhook-id-1\",\n\t\t\t\t\t\tName:     \"webhook-name-1\",\n\t\t\t\t\t\tSecret:   \"{{}}\",\n\t\t\t\t\t\tKind:     linkedca.Webhook_SCEPCHALLENGE.String(),\n\t\t\t\t\t\tCertType: linkedca.Webhook_X509.String(),\n\t\t\t\t\t\tURL:      okServer.URL,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t}, nil, args{\"webhook-challenge\", \"webhook-transaction-1\"}, nil, errors.New(\"failed executing webhook request: illegal base64 data at input byte 0\")},\n\t\t{\"ok/static-challenge\", &SCEP{\n\t\t\tName:              \"SCEP\",\n\t\t\tType:              \"SCEP\",\n\t\t\tOptions:           &Options{},\n\t\t\tChallengePassword: \"secret-static-challenge\",\n\t\t}, nil, args{\"secret-static-challenge\", \"static-transaction-1\"}, x509util.TemplateData{}, nil},\n\t\t{\"fail/wrong-static-challenge\", &SCEP{\n\t\t\tName:              \"SCEP\",\n\t\t\tType:              \"SCEP\",\n\t\t\tOptions:           &Options{},\n\t\t\tChallengePassword: \"secret-static-challenge\",\n\t\t}, nil, args{\"the-wrong-challenge-secret\", \"static-transaction-1\"}, nil, errors.New(\"invalid challenge password provided\")},\n\t\t{\"ok/no-challenge\", &SCEP{\n\t\t\tName:              \"SCEP\",\n\t\t\tType:              \"SCEP\",\n\t\t\tOptions:           &Options{},\n\t\t\tChallengePassword: \"\",\n\t\t}, nil, args{\"\", \"static-transaction-1\"}, x509util.TemplateData{}, nil},\n\t\t{\"fail/no-challenge-but-provided\", &SCEP{\n\t\t\tName:              \"SCEP\",\n\t\t\tType:              \"SCEP\",\n\t\t\tOptions:           &Options{},\n\t\t\tChallengePassword: \"\",\n\t\t}, nil, args{\"a-challenge-value\", \"static-transaction-1\"}, nil, errors.New(\"invalid challenge password provided\")},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\terr := tt.p.Init(Config{Claims: globalProvisionerClaims, WebhookClient: httpclient})\n\t\t\trequire.NoError(t, err)\n\t\t\tctx := context.Background()\n\n\t\t\tgot, err := tt.p.ValidateChallenge(ctx, dummyCSR, tt.args.challenge, tt.args.transactionID)\n\t\t\tif tt.expErr != nil {\n\t\t\t\tassert.EqualError(t, err, tt.expErr.Error())\n\t\t\t\treturn\n\t\t\t}\n\t\t\tassert.NoError(t, err)\n\t\t\tdata := x509util.TemplateData{}\n\t\t\tfor _, o := range got {\n\t\t\t\tif m, ok := o.(TemplateDataModifier); ok {\n\t\t\t\t\tm.Modify(data)\n\t\t\t\t} else {\n\t\t\t\t\tt.Errorf(\"Validate() got = %T, want TemplateDataModifier\", o)\n\t\t\t\t}\n\t\t\t}\n\t\t\tassert.Equal(t, tt.want, data)\n\t\t})\n\t}\n}\n\nfunc TestSCEP_Init(t *testing.T) {\n\tserialize := func(key crypto.PrivateKey, password string) []byte {\n\t\tvar opts []pemutil.Options\n\t\tif password == \"\" {\n\t\t\topts = append(opts, pemutil.WithPasswordPrompt(\"no password\", func(s string) ([]byte, error) {\n\t\t\t\treturn nil, nil\n\t\t\t}))\n\t\t} else {\n\t\t\topts = append(opts, pemutil.WithPassword([]byte(\"password\")))\n\t\t}\n\t\tblock, err := pemutil.Serialize(key, opts...)\n\t\trequire.NoError(t, err)\n\t\treturn pem.EncodeToMemory(block)\n\t}\n\n\tca, err := minica.New()\n\trequire.NoError(t, err)\n\n\tkey, err := rsa.GenerateKey(rand.Reader, 2048)\n\trequire.NoError(t, err)\n\tbadKey, err := rsa.GenerateKey(rand.Reader, 2048)\n\trequire.NoError(t, err)\n\n\tcert, err := ca.Sign(&x509.Certificate{\n\t\tSubject:   pkix.Name{CommonName: \"SCEP decryptor\"},\n\t\tPublicKey: key.Public(),\n\t})\n\trequire.NoError(t, err)\n\n\tcertPEM := pem.EncodeToMemory(&pem.Block{\n\t\tType: \"CERTIFICATE\", Bytes: cert.Raw,\n\t})\n\tcertPEMWithIntermediate := append(pem.EncodeToMemory(&pem.Block{\n\t\tType: \"CERTIFICATE\", Bytes: cert.Raw,\n\t}), pem.EncodeToMemory(&pem.Block{\n\t\tType: \"CERTIFICATE\", Bytes: ca.Intermediate.Raw,\n\t})...)\n\n\tkeyPEM := serialize(key, \"password\")\n\tkeyPEMNoPassword := serialize(key, \"\")\n\tbadKeyPEM := serialize(badKey, \"password\")\n\n\ttmp := t.TempDir()\n\tpath := filepath.Join(tmp, \"rsa.priv\")\n\tpathNoPassword := filepath.Join(tmp, \"rsa.key\")\n\n\trequire.NoError(t, os.WriteFile(path, keyPEM, 0600))\n\trequire.NoError(t, os.WriteFile(pathNoPassword, keyPEMNoPassword, 0600))\n\n\ttype args struct {\n\t\tconfig Config\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\ts       *SCEP\n\t\targs    args\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", &SCEP{\n\t\t\tType:                          \"SCEP\",\n\t\t\tName:                          \"scep\",\n\t\t\tChallengePassword:             \"password123\",\n\t\t\tMinimumPublicKeyLength:        0,\n\t\t\tDecrypterCertificate:          certPEM,\n\t\t\tDecrypterKeyPEM:               keyPEM,\n\t\t\tDecrypterKeyPassword:          \"password\",\n\t\t\tEncryptionAlgorithmIdentifier: 0,\n\t\t}, args{Config{Claims: globalProvisionerClaims}}, false},\n\t\t{\"ok no password\", &SCEP{\n\t\t\tType:                          \"SCEP\",\n\t\t\tName:                          \"scep\",\n\t\t\tChallengePassword:             \"password123\",\n\t\t\tMinimumPublicKeyLength:        0,\n\t\t\tDecrypterCertificate:          certPEM,\n\t\t\tDecrypterKeyPEM:               keyPEMNoPassword,\n\t\t\tDecrypterKeyPassword:          \"\",\n\t\t\tEncryptionAlgorithmIdentifier: 1,\n\t\t}, args{Config{Claims: globalProvisionerClaims}}, false},\n\t\t{\"ok with uri\", &SCEP{\n\t\t\tType:                          \"SCEP\",\n\t\t\tName:                          \"scep\",\n\t\t\tChallengePassword:             \"password123\",\n\t\t\tMinimumPublicKeyLength:        1024,\n\t\t\tDecrypterCertificate:          certPEM,\n\t\t\tDecrypterKeyURI:               \"softkms:path=\" + path,\n\t\t\tDecrypterKeyPassword:          \"password\",\n\t\t\tEncryptionAlgorithmIdentifier: 2,\n\t\t}, args{Config{Claims: globalProvisionerClaims}}, false},\n\t\t{\"ok with uri no password\", &SCEP{\n\t\t\tType:                          \"SCEP\",\n\t\t\tName:                          \"scep\",\n\t\t\tChallengePassword:             \"password123\",\n\t\t\tMinimumPublicKeyLength:        2048,\n\t\t\tDecrypterCertificate:          certPEM,\n\t\t\tDecrypterKeyURI:               \"softkms:path=\" + pathNoPassword,\n\t\t\tDecrypterKeyPassword:          \"\",\n\t\t\tEncryptionAlgorithmIdentifier: 3,\n\t\t}, args{Config{Claims: globalProvisionerClaims}}, false},\n\t\t{\"ok with SCEPKeyManager\", &SCEP{\n\t\t\tType:                          \"SCEP\",\n\t\t\tName:                          \"scep\",\n\t\t\tChallengePassword:             \"password123\",\n\t\t\tMinimumPublicKeyLength:        2048,\n\t\t\tDecrypterCertificate:          certPEM,\n\t\t\tDecrypterKeyURI:               \"softkms:path=\" + pathNoPassword,\n\t\t\tDecrypterKeyPassword:          \"\",\n\t\t\tEncryptionAlgorithmIdentifier: 4,\n\t\t}, args{Config{Claims: globalProvisionerClaims, SCEPKeyManager: &softkms.SoftKMS{}}}, false},\n\t\t{\"ok intermediate\", &SCEP{\n\t\t\tType:                          \"SCEP\",\n\t\t\tName:                          \"scep\",\n\t\t\tChallengePassword:             \"password123\",\n\t\t\tMinimumPublicKeyLength:        0,\n\t\t\tDecrypterCertificate:          nil,\n\t\t\tDecrypterKeyPEM:               nil,\n\t\t\tDecrypterKeyPassword:          \"\",\n\t\t\tEncryptionAlgorithmIdentifier: 0,\n\t\t}, args{Config{Claims: globalProvisionerClaims}}, false},\n\t\t{\"fail type\", &SCEP{\n\t\t\tType:                          \"\",\n\t\t\tName:                          \"scep\",\n\t\t\tChallengePassword:             \"password123\",\n\t\t\tMinimumPublicKeyLength:        0,\n\t\t\tDecrypterCertificate:          certPEM,\n\t\t\tDecrypterKeyPEM:               keyPEM,\n\t\t\tDecrypterKeyPassword:          \"password\",\n\t\t\tEncryptionAlgorithmIdentifier: 0,\n\t\t}, args{Config{Claims: globalProvisionerClaims}}, true},\n\t\t{\"fail name\", &SCEP{\n\t\t\tType:                          \"SCEP\",\n\t\t\tName:                          \"\",\n\t\t\tChallengePassword:             \"password123\",\n\t\t\tMinimumPublicKeyLength:        0,\n\t\t\tDecrypterCertificate:          certPEM,\n\t\t\tDecrypterKeyPEM:               keyPEM,\n\t\t\tDecrypterKeyPassword:          \"password\",\n\t\t\tEncryptionAlgorithmIdentifier: 0,\n\t\t}, args{Config{Claims: globalProvisionerClaims}}, true},\n\t\t{\"fail minimumPublicKeyLength\", &SCEP{\n\t\t\tType:                          \"SCEP\",\n\t\t\tName:                          \"scep\",\n\t\t\tChallengePassword:             \"password123\",\n\t\t\tMinimumPublicKeyLength:        2001,\n\t\t\tDecrypterCertificate:          certPEM,\n\t\t\tDecrypterKeyPEM:               keyPEM,\n\t\t\tDecrypterKeyPassword:          \"password\",\n\t\t\tEncryptionAlgorithmIdentifier: 0,\n\t\t}, args{Config{Claims: globalProvisionerClaims}}, true},\n\t\t{\"fail encryptionAlgorithmIdentifier\", &SCEP{\n\t\t\tType:                          \"SCEP\",\n\t\t\tName:                          \"scep\",\n\t\t\tChallengePassword:             \"password123\",\n\t\t\tMinimumPublicKeyLength:        0,\n\t\t\tDecrypterCertificate:          certPEM,\n\t\t\tDecrypterKeyPEM:               keyPEM,\n\t\t\tDecrypterKeyPassword:          \"password\",\n\t\t\tEncryptionAlgorithmIdentifier: 5,\n\t\t}, args{Config{Claims: globalProvisionerClaims}}, true},\n\t\t{\"fail negative encryptionAlgorithmIdentifier\", &SCEP{\n\t\t\tType:                          \"SCEP\",\n\t\t\tName:                          \"scep\",\n\t\t\tChallengePassword:             \"password123\",\n\t\t\tMinimumPublicKeyLength:        0,\n\t\t\tDecrypterCertificate:          certPEM,\n\t\t\tDecrypterKeyPEM:               keyPEM,\n\t\t\tDecrypterKeyPassword:          \"password\",\n\t\t\tEncryptionAlgorithmIdentifier: -1,\n\t\t}, args{Config{Claims: globalProvisionerClaims}}, true},\n\t\t{\"fail key decode\", &SCEP{\n\t\t\tType:                          \"SCEP\",\n\t\t\tName:                          \"scep\",\n\t\t\tChallengePassword:             \"password123\",\n\t\t\tMinimumPublicKeyLength:        0,\n\t\t\tDecrypterCertificate:          certPEM,\n\t\t\tDecrypterKeyPEM:               []byte(\"not a pem\"),\n\t\t\tDecrypterKeyPassword:          \"password\",\n\t\t\tEncryptionAlgorithmIdentifier: 0,\n\t\t}, args{Config{Claims: globalProvisionerClaims}}, true},\n\t\t{\"fail certificate decode\", &SCEP{\n\t\t\tType:                          \"SCEP\",\n\t\t\tName:                          \"scep\",\n\t\t\tChallengePassword:             \"password123\",\n\t\t\tMinimumPublicKeyLength:        0,\n\t\t\tDecrypterCertificate:          []byte(\"not a pem\"),\n\t\t\tDecrypterKeyPEM:               keyPEM,\n\t\t\tDecrypterKeyPassword:          \"password\",\n\t\t\tEncryptionAlgorithmIdentifier: 0,\n\t\t}, args{Config{Claims: globalProvisionerClaims}}, true},\n\t\t{\"fail certificate with intermediate\", &SCEP{\n\t\t\tType:                   \"SCEP\",\n\t\t\tName:                   \"scep\",\n\t\t\tChallengePassword:      \"password123\",\n\t\t\tMinimumPublicKeyLength: 0,\n\t\t\tDecrypterCertificate:   certPEMWithIntermediate,\n\t\t\tDecrypterKeyPEM:        keyPEM,\n\t\t\tDecrypterKeyPassword:   \"password\",\n\t\t}, args{Config{Claims: globalProvisionerClaims}}, true},\n\t\t{\"fail decrypter password\", &SCEP{\n\t\t\tType:                          \"SCEP\",\n\t\t\tName:                          \"scep\",\n\t\t\tChallengePassword:             \"password123\",\n\t\t\tMinimumPublicKeyLength:        0,\n\t\t\tDecrypterCertificate:          certPEM,\n\t\t\tDecrypterKeyPEM:               keyPEM,\n\t\t\tDecrypterKeyPassword:          \"badpassword\",\n\t\t\tEncryptionAlgorithmIdentifier: 0,\n\t\t}, args{Config{Claims: globalProvisionerClaims}}, true},\n\t\t{\"fail uri\", &SCEP{\n\t\t\tType:                          \"SCEP\",\n\t\t\tName:                          \"scep\",\n\t\t\tChallengePassword:             \"password123\",\n\t\t\tMinimumPublicKeyLength:        0,\n\t\t\tDecrypterCertificate:          certPEM,\n\t\t\tDecrypterKeyURI:               \"softkms:path=missing.key\",\n\t\t\tDecrypterKeyPassword:          \"password\",\n\t\t\tEncryptionAlgorithmIdentifier: 0,\n\t\t}, args{Config{Claims: globalProvisionerClaims}}, true},\n\t\t{\"fail uri password\", &SCEP{\n\t\t\tType:                          \"SCEP\",\n\t\t\tName:                          \"scep\",\n\t\t\tChallengePassword:             \"password123\",\n\t\t\tMinimumPublicKeyLength:        0,\n\t\t\tDecrypterCertificate:          certPEM,\n\t\t\tDecrypterKeyURI:               \"softkms:path=\" + path,\n\t\t\tDecrypterKeyPassword:          \"badpassword\",\n\t\t\tEncryptionAlgorithmIdentifier: 0,\n\t\t}, args{Config{Claims: globalProvisionerClaims}}, true},\n\t\t{\"fail uri type\", &SCEP{\n\t\t\tType:                          \"SCEP\",\n\t\t\tName:                          \"scep\",\n\t\t\tChallengePassword:             \"password123\",\n\t\t\tMinimumPublicKeyLength:        0,\n\t\t\tDecrypterCertificate:          certPEM,\n\t\t\tDecrypterKeyURI:               \"foo:path=\" + path,\n\t\t\tDecrypterKeyPassword:          \"password\",\n\t\t\tEncryptionAlgorithmIdentifier: 0,\n\t\t}, args{Config{Claims: globalProvisionerClaims}}, true},\n\t\t{\"fail missing certificate\", &SCEP{\n\t\t\tType:                          \"SCEP\",\n\t\t\tName:                          \"scep\",\n\t\t\tChallengePassword:             \"password123\",\n\t\t\tMinimumPublicKeyLength:        0,\n\t\t\tDecrypterCertificate:          nil,\n\t\t\tDecrypterKeyPEM:               keyPEM,\n\t\t\tDecrypterKeyPassword:          \"password\",\n\t\t\tEncryptionAlgorithmIdentifier: 0,\n\t\t}, args{Config{Claims: globalProvisionerClaims}}, true},\n\t\t{\"fail key match\", &SCEP{\n\t\t\tType:                          \"SCEP\",\n\t\t\tName:                          \"scep\",\n\t\t\tChallengePassword:             \"password123\",\n\t\t\tMinimumPublicKeyLength:        0,\n\t\t\tDecrypterCertificate:          certPEM,\n\t\t\tDecrypterKeyPEM:               badKeyPEM,\n\t\t\tDecrypterKeyPassword:          \"password\",\n\t\t\tEncryptionAlgorithmIdentifier: 0,\n\t\t}, args{Config{Claims: globalProvisionerClaims}}, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif err := tt.s.Init(tt.args.config); (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"SCEP.Init() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSCEP_Getters(t *testing.T) {\n\tp := generateSCEP(t)\n\tassert.Equal(t, \"scep/scep\", p.GetID())\n\tassert.Equal(t, \"scep\", p.GetName())\n\tassert.Equal(t, TypeSCEP, p.GetType())\n\tkid, key, ok := p.GetEncryptedKey()\n\tif kid != \"\" || key != \"\" || ok == true {\n\t\tt.Errorf(\"ACME.GetEncryptedKey() = (%v, %v, %v), want (%v, %v, %v)\", kid, key, ok, \"\", \"\", false)\n\t}\n\ttokenID, err := p.GetTokenID(\"token\")\n\tassert.Empty(t, tokenID)\n\tassert.Equal(t, ErrTokenFlowNotSupported, err)\n}\n"
  },
  {
    "path": "authority/provisioner/sign_options.go",
    "content": "package provisioner\n\nimport (\n\t\"context\"\n\t\"crypto/ecdsa\"\n\t\"crypto/ed25519\"\n\t\"crypto/rsa\"\n\t\"crypto/sha256\"\n\t\"crypto/subtle\"\n\t\"crypto/x509\"\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"reflect\"\n\t\"time\"\n\n\t\"go.step.sm/crypto/keyutil\"\n\t\"go.step.sm/crypto/x509util\"\n\n\t\"github.com/smallstep/certificates/authority/policy\"\n\t\"github.com/smallstep/certificates/errs\"\n)\n\n// DefaultCertValidity is the default validity for a certificate if none is specified.\nconst DefaultCertValidity = 24 * time.Hour\n\n// SignOptions contains the options that can be passed to the Sign method. Backdate\n// is automatically filled and can only be configured in the CA.\ntype SignOptions struct {\n\tNotAfter     TimeDuration    `json:\"notAfter\"`\n\tNotBefore    TimeDuration    `json:\"notBefore\"`\n\tTemplateData json.RawMessage `json:\"templateData\"`\n\tBackdate     time.Duration   `json:\"-\"`\n}\n\n// SignOption is the interface used to collect all extra options used in the\n// Sign method.\ntype SignOption interface{}\n\n// CertificateValidator is an interface used to validate a given X.509 certificate.\ntype CertificateValidator interface {\n\tValid(cert *x509.Certificate, opts SignOptions) error\n}\n\n// CertificateRequestValidator is an interface used to validate a given X.509 certificate request.\ntype CertificateRequestValidator interface {\n\tValid(cr *x509.CertificateRequest) error\n}\n\n// CertificateModifier is an interface used to modify a given X.509 certificate.\n// Types implementing this interface will be validated with a\n// CertificateValidator.\ntype CertificateModifier interface {\n\tModify(cert *x509.Certificate, opts SignOptions) error\n}\n\n// CertificateEnforcer is an interface used to modify a given X.509 certificate.\n// Types implemented this interface will NOT be validated with a\n// CertificateValidator.\ntype CertificateEnforcer interface {\n\tEnforce(cert *x509.Certificate) error\n}\n\n// CertificateModifierFunc allows to create simple certificate modifiers just\n// with a function.\ntype CertificateModifierFunc func(cert *x509.Certificate, opts SignOptions) error\n\n// Modify implements CertificateModifier and just calls the defined function.\nfunc (fn CertificateModifierFunc) Modify(cert *x509.Certificate, opts SignOptions) error {\n\treturn fn(cert, opts)\n}\n\n// CertificateEnforcerFunc allows to create simple certificate enforcer just\n// with a function.\ntype CertificateEnforcerFunc func(cert *x509.Certificate) error\n\n// Enforce implements CertificateEnforcer and just calls the defined function.\nfunc (fn CertificateEnforcerFunc) Enforce(cert *x509.Certificate) error {\n\treturn fn(cert)\n}\n\n// AttestationData is a SignOption used to pass attestation information to the\n// sign methods.\ntype AttestationData struct {\n\tPermanentIdentifier string\n}\n\n// defaultPublicKeyValidator validates the public key of a certificate request.\ntype defaultPublicKeyValidator struct{}\n\n// Valid checks that certificate request common name matches the one configured.\nfunc (v defaultPublicKeyValidator) Valid(req *x509.CertificateRequest) error {\n\tswitch k := req.PublicKey.(type) {\n\tcase *rsa.PublicKey:\n\t\tif k.Size() < keyutil.MinRSAKeyBytes {\n\t\t\treturn errs.Forbidden(\"certificate request RSA key must be at least %d bits (%d bytes)\",\n\t\t\t\t8*keyutil.MinRSAKeyBytes, keyutil.MinRSAKeyBytes)\n\t\t}\n\tcase *ecdsa.PublicKey, ed25519.PublicKey:\n\tdefault:\n\t\treturn errs.BadRequest(\"certificate request key of type '%T' is not supported\", k)\n\t}\n\treturn nil\n}\n\n// publicKeyMinimumLengthValidator validates the length (in bits) of the public key\n// of a certificate request is at least a certain length\ntype publicKeyMinimumLengthValidator struct {\n\tlength int\n}\n\n// newPublicKeyMinimumLengthValidator creates a new publicKeyMinimumLengthValidator\n// with the given length as its minimum value\n// TODO: change the defaultPublicKeyValidator to have a configurable length instead?\nfunc newPublicKeyMinimumLengthValidator(length int) publicKeyMinimumLengthValidator {\n\treturn publicKeyMinimumLengthValidator{\n\t\tlength: length,\n\t}\n}\n\n// Valid checks that certificate request common name matches the one configured.\nfunc (v publicKeyMinimumLengthValidator) Valid(req *x509.CertificateRequest) error {\n\tswitch k := req.PublicKey.(type) {\n\tcase *rsa.PublicKey:\n\t\tminimumLengthInBytes := v.length / 8\n\t\tif k.Size() < minimumLengthInBytes {\n\t\t\treturn errs.Forbidden(\"certificate request RSA key must be at least %d bits (%d bytes)\",\n\t\t\t\tv.length, minimumLengthInBytes)\n\t\t}\n\tcase *ecdsa.PublicKey, ed25519.PublicKey:\n\tdefault:\n\t\treturn errs.BadRequest(\"certificate request key of type '%T' is not supported\", k)\n\t}\n\treturn nil\n}\n\n// commonNameValidator validates the common name of a certificate request.\ntype commonNameValidator string\n\n// Valid checks that certificate request common name matches the one configured.\n// An empty common name is considered valid.\nfunc (v commonNameValidator) Valid(req *x509.CertificateRequest) error {\n\tif req.Subject.CommonName == \"\" {\n\t\treturn nil\n\t}\n\tif req.Subject.CommonName != string(v) {\n\t\treturn errs.Forbidden(\"certificate request does not contain the valid common name - got %s, want %s\", req.Subject.CommonName, v)\n\t}\n\treturn nil\n}\n\n// commonNameSliceValidator validates thats the common name of a certificate\n// request is present in the slice. An empty common name is considered valid.\ntype commonNameSliceValidator []string\n\nfunc (v commonNameSliceValidator) Valid(req *x509.CertificateRequest) error {\n\tif req.Subject.CommonName == \"\" {\n\t\treturn nil\n\t}\n\tfor _, cn := range v {\n\t\tif req.Subject.CommonName == cn {\n\t\t\treturn nil\n\t\t}\n\t}\n\treturn errs.Forbidden(\"certificate request does not contain the valid common name - got %s, want %s\", req.Subject.CommonName, v)\n}\n\n// dnsNamesValidator validates the DNS names SAN of a certificate request.\ntype dnsNamesValidator []string\n\n// Valid checks that certificate request DNS Names match those configured in\n// the bootstrap (token) flow.\nfunc (v dnsNamesValidator) Valid(req *x509.CertificateRequest) error {\n\tif len(req.DNSNames) == 0 {\n\t\treturn nil\n\t}\n\twant := make(map[string]bool)\n\tfor _, s := range v {\n\t\twant[s] = true\n\t}\n\tgot := make(map[string]bool)\n\tfor _, s := range req.DNSNames {\n\t\tgot[s] = true\n\t}\n\tif !reflect.DeepEqual(want, got) {\n\t\treturn errs.Forbidden(\"certificate request does not contain the valid DNS names - got %v, want %v\", req.DNSNames, v)\n\t}\n\treturn nil\n}\n\n// dnsNamesSubsetValidator validates the DNS name SANs of a certificate request.\ntype dnsNamesSubsetValidator []string\n\n// Valid checks that all DNS name SANs in the certificate request are present in\n// the allowed list of DNS names.\nfunc (v dnsNamesSubsetValidator) Valid(req *x509.CertificateRequest) error {\n\tif len(req.DNSNames) == 0 {\n\t\treturn nil\n\t}\n\tallowed := make(map[string]struct{}, len(v))\n\tfor _, s := range v {\n\t\tallowed[s] = struct{}{}\n\t}\n\tfor _, s := range req.DNSNames {\n\t\tif _, ok := allowed[s]; !ok {\n\t\t\treturn errs.Forbidden(\"certificate request contains unauthorized DNS names - got %v, allowed %v\", req.DNSNames, v)\n\t\t}\n\t}\n\treturn nil\n}\n\n// ipAddressesValidator validates the IP addresses SAN of a certificate request.\ntype ipAddressesValidator []net.IP\n\n// Valid checks that certificate request IP Addresses match those configured in\n// the bootstrap (token) flow.\nfunc (v ipAddressesValidator) Valid(req *x509.CertificateRequest) error {\n\tif len(req.IPAddresses) == 0 {\n\t\treturn nil\n\t}\n\twant := make(map[string]bool)\n\tfor _, ip := range v {\n\t\twant[ip.String()] = true\n\t}\n\tgot := make(map[string]bool)\n\tfor _, ip := range req.IPAddresses {\n\t\tgot[ip.String()] = true\n\t}\n\tif !reflect.DeepEqual(want, got) {\n\t\treturn errs.Forbidden(\"certificate request does not contain the valid IP addresses - got %v, want %v\", req.IPAddresses, v)\n\t}\n\treturn nil\n}\n\n// emailAddressesValidator validates the email address SANs of a certificate request.\ntype emailAddressesValidator []string\n\n// Valid checks that certificate request IP Addresses match those configured in\n// the bootstrap (token) flow.\nfunc (v emailAddressesValidator) Valid(req *x509.CertificateRequest) error {\n\tif len(req.EmailAddresses) == 0 {\n\t\treturn nil\n\t}\n\twant := make(map[string]bool)\n\tfor _, s := range v {\n\t\twant[s] = true\n\t}\n\tgot := make(map[string]bool)\n\tfor _, s := range req.EmailAddresses {\n\t\tgot[s] = true\n\t}\n\tif !reflect.DeepEqual(want, got) {\n\t\treturn errs.Forbidden(\"certificate request does not contain the valid email addresses - got %v, want %v\", req.EmailAddresses, v)\n\t}\n\treturn nil\n}\n\n// urisValidator validates the URI SANs of a certificate request.\ntype urisValidator struct {\n\tctx  context.Context\n\turis []*url.URL\n}\n\nfunc newURIsValidator(ctx context.Context, uris []*url.URL) *urisValidator {\n\treturn &urisValidator{ctx, uris}\n}\n\n// Valid checks that certificate request IP Addresses match those configured in\n// the bootstrap (token) flow.\nfunc (v urisValidator) Valid(req *x509.CertificateRequest) error {\n\t// SignIdentityMethod does not need to validate URIs.\n\tif MethodFromContext(v.ctx) == SignIdentityMethod {\n\t\treturn nil\n\t}\n\n\tif len(req.URIs) == 0 {\n\t\treturn nil\n\t}\n\twant := make(map[string]bool)\n\tfor _, u := range v.uris {\n\t\twant[u.String()] = true\n\t}\n\tgot := make(map[string]bool)\n\tfor _, u := range req.URIs {\n\t\tgot[u.String()] = true\n\t}\n\tif !reflect.DeepEqual(want, got) {\n\t\treturn errs.Forbidden(\"certificate request does not contain the valid URIs - got %v, want %v\", req.URIs, v.uris)\n\t}\n\treturn nil\n}\n\n// defaultsSANsValidator stores a set of SANs to eventually validate 1:1 against\n// the SANs in an x509 certificate request.\ntype defaultSANsValidator struct {\n\tctx  context.Context\n\tsans []string\n}\n\nfunc newDefaultSANsValidator(ctx context.Context, sans []string) *defaultSANsValidator {\n\treturn &defaultSANsValidator{ctx, sans}\n}\n\n// Valid verifies that the SANs stored in the validator match 1:1 with those\n// requested in the x509 certificate request.\nfunc (v defaultSANsValidator) Valid(req *x509.CertificateRequest) (err error) {\n\tdnsNames, ips, emails, uris := x509util.SplitSANs(v.sans)\n\tif err = dnsNamesValidator(dnsNames).Valid(req); err != nil {\n\t\treturn\n\t} else if err = emailAddressesValidator(emails).Valid(req); err != nil {\n\t\treturn\n\t} else if err = ipAddressesValidator(ips).Valid(req); err != nil {\n\t\treturn\n\t} else if err = newURIsValidator(v.ctx, uris).Valid(req); err != nil {\n\t\treturn\n\t}\n\treturn\n}\n\n// profileDefaultDuration is a modifier that sets the certificate\n// duration.\ntype profileDefaultDuration time.Duration\n\n// Modify sets the certificate NotBefore and NotAfter using the following order:\n//   - From the SignOptions that we get from flags.\n//   - From x509.Certificate that we get from the template.\n//   - NotBefore from the current time with a backdate.\n//   - NotAfter from NotBefore plus the duration in v.\nfunc (v profileDefaultDuration) Modify(cert *x509.Certificate, so SignOptions) error {\n\tvar backdate time.Duration\n\tnotBefore := timeOr(so.NotBefore.Time(), cert.NotBefore)\n\tif notBefore.IsZero() {\n\t\tnotBefore = now()\n\t\tbackdate = -1 * so.Backdate\n\t}\n\tnotAfter := timeOr(so.NotAfter.RelativeTime(notBefore), cert.NotAfter)\n\tif notAfter.IsZero() {\n\t\tif v != 0 {\n\t\t\tnotAfter = notBefore.Add(time.Duration(v))\n\t\t} else {\n\t\t\tnotAfter = notBefore.Add(DefaultCertValidity)\n\t\t}\n\t}\n\n\tcert.NotBefore = notBefore.Add(backdate)\n\tcert.NotAfter = notAfter\n\treturn nil\n}\n\n// profileLimitDuration is an x509 profile option that modifies an x509 validity\n// period according to an imposed expiration time.\ntype profileLimitDuration struct {\n\tdef                 time.Duration\n\tnotBefore, notAfter time.Time\n}\n\n// Modify sets the certificate NotBefore and NotAfter but limits the validity\n// period to the certificate to one that is superficially imposed.\n//\n// The expected NotBefore and NotAfter are set using the following order:\n//   - From the SignOptions that we get from flags.\n//   - From x509.Certificate that we get from the template.\n//   - NotBefore from the current time with a backdate.\n//   - NotAfter from NotBefore plus the duration v or the notAfter in v if lower.\nfunc (v profileLimitDuration) Modify(cert *x509.Certificate, so SignOptions) error {\n\tvar backdate time.Duration\n\tnotBefore := timeOr(so.NotBefore.Time(), cert.NotBefore)\n\tif notBefore.IsZero() {\n\t\tnotBefore = now()\n\t\tbackdate = -1 * so.Backdate\n\t}\n\tif notBefore.Before(v.notBefore) {\n\t\treturn errs.Forbidden(\n\t\t\t\"requested certificate notBefore (%s) is before the active validity window of the provisioning credential (%s)\",\n\t\t\tnotBefore, v.notBefore)\n\t}\n\n\tnotAfter := timeOr(so.NotAfter.RelativeTime(notBefore), cert.NotAfter)\n\tif notAfter.After(v.notAfter) {\n\t\treturn errs.Forbidden(\n\t\t\t\"requested certificate notAfter (%s) is after the expiration of the provisioning credential (%s)\",\n\t\t\tnotAfter, v.notAfter)\n\t}\n\tif notAfter.IsZero() {\n\t\tt := notBefore.Add(v.def)\n\t\tif t.After(v.notAfter) {\n\t\t\tnotAfter = v.notAfter\n\t\t} else {\n\t\t\tnotAfter = t\n\t\t}\n\t}\n\n\tcert.NotBefore = notBefore.Add(backdate)\n\tcert.NotAfter = notAfter\n\treturn nil\n}\n\n// validityValidator validates the certificate validity settings.\ntype validityValidator struct {\n\tmin time.Duration\n\tmax time.Duration\n}\n\n// newValidityValidator return a new validity validator.\nfunc newValidityValidator(minDur, maxDur time.Duration) *validityValidator {\n\treturn &validityValidator{min: minDur, max: maxDur}\n}\n\n// Valid validates the certificate validity settings (notBefore/notAfter) and\n// total duration.\nfunc (v *validityValidator) Valid(cert *x509.Certificate, o SignOptions) error {\n\tvar (\n\t\tna  = cert.NotAfter.Truncate(time.Second)\n\t\tnb  = cert.NotBefore.Truncate(time.Second)\n\t\tnow = time.Now().Truncate(time.Second)\n\t)\n\n\td := na.Sub(nb)\n\n\tif na.Before(now) {\n\t\treturn errs.BadRequest(\"notAfter cannot be in the past; na=%v\", na)\n\t}\n\tif na.Before(nb) {\n\t\treturn errs.BadRequest(\"notAfter cannot be before notBefore; na=%v, nb=%v\", na, nb)\n\t}\n\tif d < v.min {\n\t\treturn errs.Forbidden(\"requested duration of %v is less than the authorized minimum certificate duration of %v\", d, v.min)\n\t}\n\t// NOTE: this check is not \"technically correct\". We're allowing the max\n\t// duration of a cert to be \"max + backdate\" and not all certificates will\n\t// be backdated (e.g. if a user passes the NotBefore value then we do not\n\t// apply a backdate). This is good enough.\n\tif d > v.max+o.Backdate {\n\t\treturn errs.Forbidden(\"requested duration of %v is more than the authorized maximum certificate duration of %v\", d, v.max+o.Backdate)\n\t}\n\treturn nil\n}\n\n// x509NamePolicyValidator validates that the certificate (to be signed)\n// contains only allowed SANs.\ntype x509NamePolicyValidator struct {\n\tpolicyEngine policy.X509Policy\n}\n\n// newX509NamePolicyValidator return a new SANs allow/deny validator.\nfunc newX509NamePolicyValidator(engine policy.X509Policy) *x509NamePolicyValidator {\n\treturn &x509NamePolicyValidator{\n\t\tpolicyEngine: engine,\n\t}\n}\n\n// Valid validates that the certificate (to be signed) contains only allowed SANs.\nfunc (v *x509NamePolicyValidator) Valid(cert *x509.Certificate, _ SignOptions) error {\n\tif v.policyEngine == nil {\n\t\treturn nil\n\t}\n\treturn v.policyEngine.IsX509CertificateAllowed(cert)\n}\n\ntype forceCNOption struct {\n\tForceCN bool\n}\n\nfunc newForceCNOption(forceCN bool) *forceCNOption {\n\treturn &forceCNOption{forceCN}\n}\n\nfunc (o *forceCNOption) Modify(cert *x509.Certificate, _ SignOptions) error {\n\tif !o.ForceCN {\n\t\treturn nil\n\t}\n\n\t// Force the common name to be the first DNS if not provided.\n\tif cert.Subject.CommonName == \"\" {\n\t\tif len(cert.DNSNames) == 0 {\n\t\t\treturn errs.BadRequest(\"cannot force common name, DNS names is empty\")\n\t\t}\n\t\tcert.Subject.CommonName = cert.DNSNames[0]\n\t}\n\n\treturn nil\n}\n\ntype provisionerExtensionOption struct {\n\tExtension\n\tDisabled bool\n}\n\nfunc newProvisionerExtensionOption(typ Type, name, credentialID string, keyValuePairs ...string) *provisionerExtensionOption {\n\treturn &provisionerExtensionOption{\n\t\tExtension: Extension{\n\t\t\tType:          typ,\n\t\t\tName:          name,\n\t\t\tCredentialID:  credentialID,\n\t\t\tKeyValuePairs: keyValuePairs,\n\t\t},\n\t}\n}\n\n// WithControllerOptions updates the provisionerExtensionOption with options\n// from the controller. Currently only the DisableSmallstepExtensions\n// provisioner claim is used.\nfunc (o *provisionerExtensionOption) WithControllerOptions(c *Controller) *provisionerExtensionOption {\n\to.Disabled = c.Claimer.IsDisableSmallstepExtensions()\n\treturn o\n}\n\nfunc (o *provisionerExtensionOption) Modify(cert *x509.Certificate, _ SignOptions) error {\n\tif o.Disabled {\n\t\treturn nil\n\t}\n\n\text, err := o.ToExtension()\n\tif err != nil {\n\t\treturn errs.NewError(http.StatusInternalServerError, err, \"error creating certificate\")\n\t}\n\t// Replace or append the provisioner extension to avoid the inclusions of\n\t// malicious stepOIDProvisioner using templates.\n\tfor i, e := range cert.ExtraExtensions {\n\t\tif e.Id.Equal(StepOIDProvisioner) {\n\t\t\tcert.ExtraExtensions[i] = ext\n\t\t\treturn nil\n\t\t}\n\t}\n\tcert.ExtraExtensions = append(cert.ExtraExtensions, ext)\n\treturn nil\n}\n\n// csrFingerprintValidator is a CertificateRequestValidator that checks the\n// fingerprint of the certificate request with the provided one.\ntype csrFingerprintValidator string\n\nfunc (s csrFingerprintValidator) Valid(cr *x509.CertificateRequest) error {\n\tif s != \"\" {\n\t\texpected, err := base64.RawURLEncoding.DecodeString(string(s))\n\t\tif err != nil {\n\t\t\treturn errs.ForbiddenErr(err, \"error decoding fingerprint\")\n\t\t}\n\t\tsum := sha256.Sum256(cr.Raw)\n\t\tif subtle.ConstantTimeCompare(expected, sum[:]) != 1 {\n\t\t\treturn errs.Forbidden(\"certificate request fingerprint does not match %q\", s)\n\t\t}\n\t}\n\treturn nil\n}\n\n// SignCSROption is the interface used to collect extra options in the SignCSR\n// method of the SCEP authority.\ntype SignCSROption any\n\n// TemplateDataModifier is an interface that allows to modify template data.\ntype TemplateDataModifier interface {\n\tModify(data x509util.TemplateData)\n}\n\ntype templateDataModifier struct {\n\tfn func(x509util.TemplateData)\n}\n\nfunc (t *templateDataModifier) Modify(data x509util.TemplateData) {\n\tt.fn(data)\n}\n\n// TemplateDataModifierFunc returns a TemplateDataModifier with the given\n// function.\nfunc TemplateDataModifierFunc(fn func(data x509util.TemplateData)) TemplateDataModifier {\n\treturn &templateDataModifier{\n\t\tfn: fn,\n\t}\n}\n"
  },
  {
    "path": "authority/provisioner/sign_options_test.go",
    "content": "package provisioner\n\nimport (\n\t\"context\"\n\t\"crypto/x509\"\n\t\"crypto/x509/pkix\"\n\t\"encoding/asn1\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/url\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n\t\"github.com/smallstep/assert\"\n\t\"go.step.sm/crypto/pemutil\"\n)\n\nfunc Test_defaultPublicKeyValidator_Valid(t *testing.T) {\n\t_shortRSA, err := pemutil.Read(\"./testdata/certs/short-rsa.csr\")\n\tassert.FatalError(t, err)\n\tshortRSA, ok := _shortRSA.(*x509.CertificateRequest)\n\tassert.Fatal(t, ok)\n\n\t_rsa, err := pemutil.Read(\"./testdata/certs/rsa.csr\")\n\tassert.FatalError(t, err)\n\trsaCSR, ok := _rsa.(*x509.CertificateRequest)\n\tassert.Fatal(t, ok)\n\n\t_ecdsa, err := pemutil.Read(\"./testdata/certs/ecdsa.csr\")\n\tassert.FatalError(t, err)\n\tecdsaCSR, ok := _ecdsa.(*x509.CertificateRequest)\n\tassert.Fatal(t, ok)\n\n\t_ed25519, err := pemutil.Read(\"./testdata/certs/ed25519.csr\")\n\tassert.FatalError(t, err)\n\ted25519CSR, ok := _ed25519.(*x509.CertificateRequest)\n\tassert.Fatal(t, ok)\n\n\tv := defaultPublicKeyValidator{}\n\ttests := []struct {\n\t\tname string\n\t\tcsr  *x509.CertificateRequest\n\t\terr  error\n\t}{\n\t\t{\n\t\t\t\"fail/unrecognized-key-type\",\n\t\t\t&x509.CertificateRequest{PublicKey: \"foo\"},\n\t\t\terrors.New(\"certificate request key of type 'string' is not supported\"),\n\t\t},\n\t\t{\n\t\t\t\"fail/rsa/too-short\",\n\t\t\tshortRSA,\n\t\t\terrors.New(\"certificate request RSA key must be at least 2048 bits (256 bytes)\"),\n\t\t},\n\t\t{\n\t\t\t\"ok/rsa\",\n\t\t\trsaCSR,\n\t\t\tnil,\n\t\t},\n\t\t{\n\t\t\t\"ok/ecdsa\",\n\t\t\tecdsaCSR,\n\t\t\tnil,\n\t\t},\n\t\t{\n\t\t\t\"ok/ed25519\",\n\t\t\ted25519CSR,\n\t\t\tnil,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif err := v.Valid(tt.csr); err != nil {\n\t\t\t\tif assert.NotNil(t, tt.err) {\n\t\t\t\t\tassert.HasPrefix(t, err.Error(), tt.err.Error())\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tassert.Nil(t, tt.err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_commonNameValidator_Valid(t *testing.T) {\n\ttype args struct {\n\t\treq *x509.CertificateRequest\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tv       commonNameValidator\n\t\targs    args\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", \"foo.bar.zar\", args{&x509.CertificateRequest{Subject: pkix.Name{CommonName: \"foo.bar.zar\"}}}, false},\n\t\t{\"empty\", \"\", args{&x509.CertificateRequest{Subject: pkix.Name{CommonName: \"\"}}}, false},\n\t\t{\"wrong\", \"foo.bar.zar\", args{&x509.CertificateRequest{Subject: pkix.Name{CommonName: \"example.com\"}}}, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif err := tt.v.Valid(tt.args.req); (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"commonNameValidator.Valid() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_commonNameSliceValidator_Valid(t *testing.T) {\n\ttype args struct {\n\t\treq *x509.CertificateRequest\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tv       commonNameSliceValidator\n\t\targs    args\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", []string{\"foo.bar.zar\"}, args{&x509.CertificateRequest{Subject: pkix.Name{CommonName: \"foo.bar.zar\"}}}, false},\n\t\t{\"ok\", []string{\"example.com\", \"foo.bar.zar\"}, args{&x509.CertificateRequest{Subject: pkix.Name{CommonName: \"foo.bar.zar\"}}}, false},\n\t\t{\"empty\", []string{\"\"}, args{&x509.CertificateRequest{Subject: pkix.Name{CommonName: \"\"}}}, false},\n\t\t{\"wrong\", []string{\"foo.bar.zar\"}, args{&x509.CertificateRequest{Subject: pkix.Name{CommonName: \"example.com\"}}}, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif err := tt.v.Valid(tt.args.req); (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"commonNameSliceValidator.Valid() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_emailAddressesValidator_Valid(t *testing.T) {\n\ttype args struct {\n\t\treq *x509.CertificateRequest\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tv       emailAddressesValidator\n\t\targs    args\n\t\twantErr bool\n\t}{\n\t\t{\"ok0\", []string{}, args{&x509.CertificateRequest{EmailAddresses: []string{}}}, false},\n\t\t{\"ok1\", []string{\"max@smallstep.com\"}, args{&x509.CertificateRequest{EmailAddresses: []string{\"max@smallstep.com\"}}}, false},\n\t\t{\"ok2\", []string{\"max@step.com\", \"mike@step.com\"}, args{&x509.CertificateRequest{EmailAddresses: []string{\"max@step.com\", \"mike@step.com\"}}}, false},\n\t\t{\"ok3\", []string{\"max@step.com\", \"mike@step.com\"}, args{&x509.CertificateRequest{EmailAddresses: []string{\"mike@step.com\", \"max@step.com\"}}}, false},\n\t\t{\"ok3\", []string{\"max@step.com\", \"mike@step.com\"}, args{&x509.CertificateRequest{}}, false},\n\t\t{\"fail1\", []string{\"max@step.com\"}, args{&x509.CertificateRequest{EmailAddresses: []string{\"mike@step.com\"}}}, true},\n\t\t{\"fail2\", []string{\"mike@step.com\"}, args{&x509.CertificateRequest{EmailAddresses: []string{\"max@step.com\", \"mike@step.com\"}}}, true},\n\t\t{\"fail3\", []string{\"mike@step.com\", \"max@step.com\"}, args{&x509.CertificateRequest{EmailAddresses: []string{\"mike@step.com\", \"mex@step.com\"}}}, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif err := tt.v.Valid(tt.args.req); (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"emailAddressesValidator.Valid() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_dnsNamesValidator_Valid(t *testing.T) {\n\ttype args struct {\n\t\treq *x509.CertificateRequest\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tv       dnsNamesValidator\n\t\targs    args\n\t\twantErr bool\n\t}{\n\t\t{\"ok0\", []string{}, args{&x509.CertificateRequest{DNSNames: []string{}}}, false},\n\t\t{\"ok1\", []string{\"foo.bar.zar\"}, args{&x509.CertificateRequest{DNSNames: []string{\"foo.bar.zar\"}}}, false},\n\t\t{\"ok2\", []string{\"foo.bar.zar\", \"bar.zar\"}, args{&x509.CertificateRequest{DNSNames: []string{\"foo.bar.zar\", \"bar.zar\"}}}, false},\n\t\t{\"ok3\", []string{\"foo.bar.zar\", \"bar.zar\"}, args{&x509.CertificateRequest{DNSNames: []string{\"bar.zar\", \"foo.bar.zar\"}}}, false},\n\t\t{\"ok4\", []string{\"foo.bar.zar\", \"bar.zar\"}, args{&x509.CertificateRequest{}}, false},\n\t\t{\"fail1\", []string{\"foo.bar.zar\"}, args{&x509.CertificateRequest{DNSNames: []string{\"bar.zar\"}}}, true},\n\t\t{\"fail2\", []string{\"foo.bar.zar\"}, args{&x509.CertificateRequest{DNSNames: []string{\"bar.zar\", \"foo.bar.zar\"}}}, true},\n\t\t{\"fail3\", []string{\"foo.bar.zar\", \"bar.zar\"}, args{&x509.CertificateRequest{DNSNames: []string{\"foo.bar.zar\", \"zar.bar\"}}}, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif err := tt.v.Valid(tt.args.req); (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"dnsNamesValidator.Valid() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_dnsNamesSubsetValidator_Valid(t *testing.T) {\n\ttype args struct {\n\t\treq *x509.CertificateRequest\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tv       dnsNamesSubsetValidator\n\t\targs    args\n\t\twantErr bool\n\t}{\n\t\t{\"ok0\", []string{}, args{&x509.CertificateRequest{DNSNames: []string{}}}, false},\n\t\t{\"ok1\", []string{\"foo.bar.zar\"}, args{&x509.CertificateRequest{DNSNames: []string{\"foo.bar.zar\"}}}, false},\n\t\t{\"ok2\", []string{\"foo.bar.zar\", \"bar.zar\"}, args{&x509.CertificateRequest{DNSNames: []string{\"foo.bar.zar\", \"bar.zar\"}}}, false},\n\t\t{\"ok3\", []string{\"foo.bar.zar\", \"bar.zar\"}, args{&x509.CertificateRequest{DNSNames: []string{\"bar.zar\", \"foo.bar.zar\"}}}, false},\n\t\t{\"ok4\", []string{\"foo.bar.zar\", \"bar.zar\"}, args{&x509.CertificateRequest{}}, false},\n\t\t{\"ok5\", []string{\"foo.bar.zar\", \"bar.zar\"}, args{&x509.CertificateRequest{DNSNames: []string{\"bar.zar\"}}}, false},\n\t\t{\"ok6\", []string{\"foo\", \"bar\", \"baz\", \"zar\", \"zap\"}, args{&x509.CertificateRequest{DNSNames: []string{\"zap\", \"baz\", \"foo\"}}}, false},\n\t\t{\"fail1\", []string{\"foo.bar.zar\"}, args{&x509.CertificateRequest{DNSNames: []string{\"bar.zar\"}}}, true},\n\t\t{\"fail2\", []string{\"foo.bar.zar\"}, args{&x509.CertificateRequest{DNSNames: []string{\"bar.zar\", \"foo.bar.zar\"}}}, true},\n\t\t{\"fail3\", []string{\"foo.bar.zar\", \"bar.zar\"}, args{&x509.CertificateRequest{DNSNames: []string{\"foo.bar.zar\", \"zar.bar\"}}}, true},\n\t\t{\"fail4\", []string{\"foo\", \"bar\", \"baz\", \"zar\", \"zap\"}, args{&x509.CertificateRequest{DNSNames: []string{\"zap\", \"baz\", \"foO\"}}}, true},\n\t\t{\"fail5\", []string{\"foo\", \"bar\", \"baz\", \"zar\", \"zap\"}, args{&x509.CertificateRequest{DNSNames: []string{\"zap\", \"baz\", \"fax\", \"foo\"}}}, true},\n\t\t{\"fail6\", []string{}, args{&x509.CertificateRequest{DNSNames: []string{\"zap\", \"baz\", \"fax\", \"foo\"}}}, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif err := tt.v.Valid(tt.args.req); (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"dnsNamesSubsetValidator.Valid() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_ipAddressesValidator_Valid(t *testing.T) {\n\tip1 := net.IPv4(10, 3, 2, 1)\n\tip2 := net.IPv4(10, 3, 2, 2)\n\tip3 := net.IPv4(10, 3, 2, 3)\n\n\ttype args struct {\n\t\treq *x509.CertificateRequest\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tv       ipAddressesValidator\n\t\targs    args\n\t\twantErr bool\n\t}{\n\t\t{\"ok0\", []net.IP{}, args{&x509.CertificateRequest{IPAddresses: []net.IP{}}}, false},\n\t\t{\"ok1\", []net.IP{ip1}, args{&x509.CertificateRequest{IPAddresses: []net.IP{ip1}}}, false},\n\t\t{\"ok2\", []net.IP{ip1, ip2}, args{&x509.CertificateRequest{IPAddresses: []net.IP{ip1, ip2}}}, false},\n\t\t{\"ok3\", []net.IP{ip1, ip2}, args{&x509.CertificateRequest{IPAddresses: []net.IP{ip2, ip1}}}, false},\n\t\t{\"ok4\", []net.IP{ip1, ip2}, args{&x509.CertificateRequest{}}, false},\n\t\t{\"fail1\", []net.IP{ip1}, args{&x509.CertificateRequest{IPAddresses: []net.IP{ip2}}}, true},\n\t\t{\"fail2\", []net.IP{ip1}, args{&x509.CertificateRequest{IPAddresses: []net.IP{ip2, ip1}}}, true},\n\t\t{\"fail3\", []net.IP{ip1, ip2}, args{&x509.CertificateRequest{IPAddresses: []net.IP{ip1, ip3}}}, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif err := tt.v.Valid(tt.args.req); (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"ipAddressesValidator.Valid() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_urisValidator_Valid(t *testing.T) {\n\tu1, err := url.Parse(\"https://ca.smallstep.com\")\n\tassert.FatalError(t, err)\n\tu2, err := url.Parse(\"https://google.com/index.html\")\n\tassert.FatalError(t, err)\n\tu3, err := url.Parse(\"urn:uuid:ddfe62ba-7e99-4bc1-83b3-8f57fe3e9959\")\n\tassert.FatalError(t, err)\n\tfu, err := url.Parse(\"https://unexpected.com\")\n\tassert.FatalError(t, err)\n\n\tsignContext := NewContextWithMethod(context.Background(), SignMethod)\n\tsignIdentityContext := NewContextWithMethod(context.Background(), SignIdentityMethod)\n\n\ttype args struct {\n\t\treq *x509.CertificateRequest\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tv       *urisValidator\n\t\targs    args\n\t\twantErr bool\n\t}{\n\t\t{\"ok0\", newURIsValidator(signContext, []*url.URL{}), args{&x509.CertificateRequest{URIs: []*url.URL{}}}, false},\n\t\t{\"ok1\", newURIsValidator(signContext, []*url.URL{u1}), args{&x509.CertificateRequest{URIs: []*url.URL{u1}}}, false},\n\t\t{\"ok2\", newURIsValidator(signContext, []*url.URL{u1, u2}), args{&x509.CertificateRequest{URIs: []*url.URL{u2, u1}}}, false},\n\t\t{\"ok3\", newURIsValidator(signContext, []*url.URL{u2, u1, u3}), args{&x509.CertificateRequest{URIs: []*url.URL{u3, u2, u1}}}, false},\n\t\t{\"ok4\", newURIsValidator(signIdentityContext, []*url.URL{u1, u2}), args{&x509.CertificateRequest{URIs: []*url.URL{u1, fu}}}, false},\n\t\t{\"fail1\", newURIsValidator(signContext, []*url.URL{u1}), args{&x509.CertificateRequest{URIs: []*url.URL{u2}}}, true},\n\t\t{\"fail2\", newURIsValidator(signContext, []*url.URL{u1}), args{&x509.CertificateRequest{URIs: []*url.URL{u2, u1}}}, true},\n\t\t{\"fail3\", newURIsValidator(signContext, []*url.URL{u1, u2}), args{&x509.CertificateRequest{URIs: []*url.URL{u1, fu}}}, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif err := tt.v.Valid(tt.args.req); (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"urisValidator.Valid() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_defaultSANsValidator_Valid(t *testing.T) {\n\ttype test struct {\n\t\tcsr          *x509.CertificateRequest\n\t\tctx          context.Context\n\t\texpectedSANs []string\n\t\terr          error\n\t}\n\n\tsignContext := NewContextWithMethod(context.Background(), SignMethod)\n\tsignIdentityContext := NewContextWithMethod(context.Background(), SignIdentityMethod)\n\n\ttests := map[string]func() test{\n\t\t\"fail/dnsNamesValidator\": func() test {\n\t\t\treturn test{\n\t\t\t\tcsr:          &x509.CertificateRequest{DNSNames: []string{\"foo\", \"bar\"}},\n\t\t\t\tctx:          signContext,\n\t\t\t\texpectedSANs: []string{\"foo\"},\n\t\t\t\terr:          errors.New(\"certificate request does not contain the valid DNS names\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/emailAddressesValidator\": func() test {\n\t\t\treturn test{\n\t\t\t\tcsr:          &x509.CertificateRequest{EmailAddresses: []string{\"max@fx.com\", \"mariano@fx.com\"}},\n\t\t\t\tctx:          signContext,\n\t\t\t\texpectedSANs: []string{\"dcow@fx.com\"},\n\t\t\t\terr:          errors.New(\"certificate request does not contain the valid email addresses\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/ipAddressesValidator\": func() test {\n\t\t\treturn test{\n\t\t\t\tcsr:          &x509.CertificateRequest{IPAddresses: []net.IP{net.ParseIP(\"1.1.1.1\"), net.ParseIP(\"127.0.0.1\")}},\n\t\t\t\tctx:          signContext,\n\t\t\t\texpectedSANs: []string{\"127.0.0.1\"},\n\t\t\t\terr:          errors.New(\"certificate request does not contain the valid IP addresses\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/urisValidator\": func() test {\n\t\t\tu1, err := url.Parse(\"https://google.com\")\n\t\t\tassert.FatalError(t, err)\n\t\t\tu2, err := url.Parse(\"urn:uuid:ddfe62ba-7e99-4bc1-83b3-8f57fe3e9959\")\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tcsr:          &x509.CertificateRequest{URIs: []*url.URL{u1, u2}},\n\t\t\t\tctx:          signContext,\n\t\t\t\texpectedSANs: []string{\"urn:uuid:ddfe62ba-7e99-4bc1-83b3-8f57fe3e9959\"},\n\t\t\t\terr:          errors.New(\"certificate request does not contain the valid URIs\"),\n\t\t\t}\n\t\t},\n\t\t\"ok/urisBadValidator-SignIdentity\": func() test {\n\t\t\tu1, err := url.Parse(\"https://google.com\")\n\t\t\tassert.FatalError(t, err)\n\t\t\tu2, err := url.Parse(\"urn:uuid:ddfe62ba-7e99-4bc1-83b3-8f57fe3e9959\")\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tcsr:          &x509.CertificateRequest{URIs: []*url.URL{u1, u2}},\n\t\t\t\tctx:          signIdentityContext,\n\t\t\t\texpectedSANs: []string{\"urn:uuid:ddfe62ba-7e99-4bc1-83b3-8f57fe3e9959\"},\n\t\t\t}\n\t\t},\n\t\t\"ok\": func() test {\n\t\t\tu1, err := url.Parse(\"https://google.com\")\n\t\t\tassert.FatalError(t, err)\n\t\t\tu2, err := url.Parse(\"urn:uuid:ddfe62ba-7e99-4bc1-83b3-8f57fe3e9959\")\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tctx: signContext,\n\t\t\t\tcsr: &x509.CertificateRequest{\n\t\t\t\t\tDNSNames:       []string{\"foo\", \"bar\"},\n\t\t\t\t\tEmailAddresses: []string{\"max@fx.com\", \"mariano@fx.com\"},\n\t\t\t\t\tIPAddresses:    []net.IP{net.ParseIP(\"1.1.1.1\"), net.ParseIP(\"127.0.0.1\")},\n\t\t\t\t\tURIs:           []*url.URL{u1, u2},\n\t\t\t\t},\n\t\t\t\texpectedSANs: []string{\"foo\", \"127.0.0.1\", \"max@fx.com\", \"mariano@fx.com\", \"https://google.com\", \"1.1.1.1\", \"bar\", \"urn:uuid:ddfe62ba-7e99-4bc1-83b3-8f57fe3e9959\"},\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ttt := run()\n\t\t\tif err := newDefaultSANsValidator(tt.ctx, tt.expectedSANs).Valid(tt.csr); err != nil {\n\t\t\t\tif assert.NotNil(t, tt.err, fmt.Sprintf(\"expected no error, but got err = %s\", err.Error())) {\n\t\t\t\t\tassert.True(t, strings.Contains(err.Error(), tt.err.Error()),\n\t\t\t\t\t\tfmt.Sprintf(\"want err = %s, but got err = %s\", tt.err.Error(), err.Error()))\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tassert.Nil(t, tt.err, fmt.Sprintf(\"expected err = %s, but not <nil>\", tt.err))\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_validityValidator_Valid(t *testing.T) {\n\ttype test struct {\n\t\tcert *x509.Certificate\n\t\topts SignOptions\n\t\tvv   *validityValidator\n\t\terr  error\n\t}\n\ttests := map[string]func() test{\n\t\t\"fail/notAfter-past\": func() test {\n\t\t\treturn test{\n\t\t\t\tvv:   &validityValidator{5 * time.Minute, 24 * time.Hour},\n\t\t\t\tcert: &x509.Certificate{NotAfter: time.Now().Add(-5 * time.Minute)},\n\t\t\t\topts: SignOptions{},\n\t\t\t\terr:  errors.New(\"notAfter cannot be in the past\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/notBefore-after-notAfter\": func() test {\n\t\t\treturn test{\n\t\t\t\tvv: &validityValidator{5 * time.Minute, 24 * time.Hour},\n\t\t\t\tcert: &x509.Certificate{NotBefore: time.Now().Add(10 * time.Minute),\n\t\t\t\t\tNotAfter: time.Now().Add(5 * time.Minute)},\n\t\t\t\topts: SignOptions{},\n\t\t\t\terr:  errors.New(\"notAfter cannot be before notBefore\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/duration-too-short\": func() test {\n\t\t\tn := now()\n\t\t\treturn test{\n\t\t\t\tvv: &validityValidator{5 * time.Minute, 24 * time.Hour},\n\t\t\t\tcert: &x509.Certificate{NotBefore: n,\n\t\t\t\t\tNotAfter: n.Add(3 * time.Minute)},\n\t\t\t\topts: SignOptions{},\n\t\t\t\terr:  errors.New(\"is less than the authorized minimum certificate duration of \"),\n\t\t\t}\n\t\t},\n\t\t\"ok/duration-exactly-min\": func() test {\n\t\t\tn := now()\n\t\t\treturn test{\n\t\t\t\tvv: &validityValidator{5 * time.Minute, 24 * time.Hour},\n\t\t\t\tcert: &x509.Certificate{NotBefore: n,\n\t\t\t\t\tNotAfter: n.Add(5 * time.Minute)},\n\t\t\t\topts: SignOptions{},\n\t\t\t}\n\t\t},\n\t\t\"fail/duration-too-great\": func() test {\n\t\t\tn := now()\n\t\t\treturn test{\n\t\t\t\tvv: &validityValidator{5 * time.Minute, 24 * time.Hour},\n\t\t\t\tcert: &x509.Certificate{NotBefore: n,\n\t\t\t\t\tNotAfter: n.Add(24*time.Hour + time.Second)},\n\t\t\t\terr: errors.New(\"is more than the authorized maximum certificate duration of \"),\n\t\t\t}\n\t\t},\n\t\t\"ok/duration-exactly-max\": func() test {\n\t\t\tn := time.Now()\n\t\t\treturn test{\n\t\t\t\tvv: &validityValidator{5 * time.Minute, 24 * time.Hour},\n\t\t\t\tcert: &x509.Certificate{NotBefore: n,\n\t\t\t\t\tNotAfter: n.Add(24 * time.Hour)},\n\t\t\t}\n\t\t},\n\t\t\"ok/duration-exact-min-with-backdate\": func() test {\n\t\t\tnow := time.Now()\n\t\t\tcert := &x509.Certificate{NotBefore: now, NotAfter: now.Add(5 * time.Minute)}\n\t\t\ttime.Sleep(time.Second)\n\t\t\treturn test{\n\t\t\t\tvv:   &validityValidator{5 * time.Minute, 24 * time.Hour},\n\t\t\t\tcert: cert,\n\t\t\t\topts: SignOptions{Backdate: time.Second},\n\t\t\t}\n\t\t},\n\t\t\"ok/duration-exact-max-with-backdate\": func() test {\n\t\t\tbackdate := time.Second\n\t\t\tnow := time.Now()\n\t\t\tcert := &x509.Certificate{NotBefore: now, NotAfter: now.Add(24*time.Hour + backdate)}\n\t\t\ttime.Sleep(backdate)\n\t\t\treturn test{\n\t\t\t\tvv:   &validityValidator{5 * time.Minute, 24 * time.Hour},\n\t\t\t\tcert: cert,\n\t\t\t\topts: SignOptions{Backdate: backdate},\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ttt := run()\n\t\t\tif err := tt.vv.Valid(tt.cert, tt.opts); err != nil {\n\t\t\t\tif assert.NotNil(t, tt.err, fmt.Sprintf(\"expected no error, but got err = %s\", err.Error())) {\n\t\t\t\t\tassert.True(t, strings.Contains(err.Error(), tt.err.Error()),\n\t\t\t\t\t\tfmt.Sprintf(\"want err = %s, but got err = %s\", tt.err.Error(), err.Error()))\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tassert.Nil(t, tt.err, fmt.Sprintf(\"expected err = %s, but not <nil>\", tt.err))\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_forceCN_Option(t *testing.T) {\n\ttype test struct {\n\t\tso    SignOptions\n\t\tfcn   forceCNOption\n\t\tcert  *x509.Certificate\n\t\tvalid func(*x509.Certificate)\n\t\terr   error\n\t}\n\n\ttests := map[string]func() test{\n\t\t\"ok/CN-not-forced\": func() test {\n\t\t\treturn test{\n\t\t\t\tfcn: forceCNOption{false},\n\t\t\t\tso:  SignOptions{},\n\t\t\t\tcert: &x509.Certificate{\n\t\t\t\t\tSubject:  pkix.Name{},\n\t\t\t\t\tDNSNames: []string{\"acme.example.com\", \"step.example.com\"},\n\t\t\t\t},\n\t\t\t\tvalid: func(cert *x509.Certificate) {\n\t\t\t\t\tassert.Equals(t, cert.Subject.CommonName, \"\")\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"ok/CN-forced-and-set\": func() test {\n\t\t\treturn test{\n\t\t\t\tfcn: forceCNOption{true},\n\t\t\t\tso:  SignOptions{},\n\t\t\t\tcert: &x509.Certificate{\n\t\t\t\t\tSubject: pkix.Name{\n\t\t\t\t\t\tCommonName: \"Some Common Name\",\n\t\t\t\t\t},\n\t\t\t\t\tDNSNames: []string{\"acme.example.com\", \"step.example.com\"},\n\t\t\t\t},\n\t\t\t\tvalid: func(cert *x509.Certificate) {\n\t\t\t\t\tassert.Equals(t, cert.Subject.CommonName, \"Some Common Name\")\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"ok/CN-forced-and-not-set\": func() test {\n\t\t\treturn test{\n\t\t\t\tfcn: forceCNOption{true},\n\t\t\t\tso:  SignOptions{},\n\t\t\t\tcert: &x509.Certificate{\n\t\t\t\t\tSubject:  pkix.Name{},\n\t\t\t\t\tDNSNames: []string{\"acme.example.com\", \"step.example.com\"},\n\t\t\t\t},\n\t\t\t\tvalid: func(cert *x509.Certificate) {\n\t\t\t\t\tassert.Equals(t, cert.Subject.CommonName, \"acme.example.com\")\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"fail/CN-forced-and-empty-DNSNames\": func() test {\n\t\t\treturn test{\n\t\t\t\tfcn: forceCNOption{true},\n\t\t\t\tso:  SignOptions{},\n\t\t\t\tcert: &x509.Certificate{\n\t\t\t\t\tSubject:  pkix.Name{},\n\t\t\t\t\tDNSNames: []string{},\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"cannot force common name, DNS names is empty\"),\n\t\t\t}\n\t\t},\n\t}\n\n\tfor name, run := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ttt := run()\n\t\t\tif err := tt.fcn.Modify(tt.cert, tt.so); err != nil {\n\t\t\t\tif assert.NotNil(t, tt.err) {\n\t\t\t\t\tassert.HasPrefix(t, err.Error(), tt.err.Error())\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif assert.Nil(t, tt.err) {\n\t\t\t\t\ttt.valid(tt.cert)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_profileDefaultDuration_Option(t *testing.T) {\n\ttype test struct {\n\t\tso    SignOptions\n\t\tpdd   profileDefaultDuration\n\t\tcert  *x509.Certificate\n\t\tvalid func(*x509.Certificate)\n\t}\n\ttests := map[string]func() test{\n\t\t\"ok/notBefore-notAfter-duration-empty\": func() test {\n\t\t\treturn test{\n\t\t\t\tpdd:  profileDefaultDuration(0),\n\t\t\t\tso:   SignOptions{},\n\t\t\t\tcert: new(x509.Certificate),\n\t\t\t\tvalid: func(cert *x509.Certificate) {\n\t\t\t\t\tn := now()\n\t\t\t\t\tassert.True(t, n.After(cert.NotBefore.Add(-time.Second)))\n\t\t\t\t\tassert.True(t, n.Add(-1*time.Minute).Before(cert.NotBefore))\n\n\t\t\t\t\tassert.True(t, n.Add(24*time.Hour).After(cert.NotAfter.Add(-time.Second)))\n\t\t\t\t\tassert.True(t, n.Add(24*time.Hour).Add(-1*time.Minute).Before(cert.NotAfter))\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"ok/notBefore-set\": func() test {\n\t\t\tnb := time.Now().Add(5 * time.Minute).UTC()\n\t\t\treturn test{\n\t\t\t\tpdd:  profileDefaultDuration(0),\n\t\t\t\tso:   SignOptions{NotBefore: NewTimeDuration(nb)},\n\t\t\t\tcert: new(x509.Certificate),\n\t\t\t\tvalid: func(cert *x509.Certificate) {\n\t\t\t\t\tassert.Equals(t, cert.NotBefore, nb)\n\t\t\t\t\tassert.Equals(t, cert.NotAfter, nb.Add(24*time.Hour))\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"ok/duration-set\": func() test {\n\t\t\td := 4 * time.Hour\n\t\t\treturn test{\n\t\t\t\tpdd:  profileDefaultDuration(d),\n\t\t\t\tso:   SignOptions{Backdate: time.Second},\n\t\t\t\tcert: new(x509.Certificate),\n\t\t\t\tvalid: func(cert *x509.Certificate) {\n\t\t\t\t\tn := now()\n\t\t\t\t\tassert.True(t, n.After(cert.NotBefore), fmt.Sprintf(\"expected now = %s to be after cert.NotBefore = %s\", n, cert.NotBefore))\n\t\t\t\t\tassert.True(t, n.Add(-1*time.Minute).Before(cert.NotBefore))\n\n\t\t\t\t\tassert.True(t, n.Add(d).After(cert.NotAfter))\n\t\t\t\t\tassert.True(t, n.Add(d).Add(-1*time.Minute).Before(cert.NotAfter))\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"ok/notAfter-set\": func() test {\n\t\t\tna := now().Add(10 * time.Minute).UTC()\n\t\t\treturn test{\n\t\t\t\tpdd:  profileDefaultDuration(0),\n\t\t\t\tso:   SignOptions{NotAfter: NewTimeDuration(na)},\n\t\t\t\tcert: new(x509.Certificate),\n\t\t\t\tvalid: func(cert *x509.Certificate) {\n\t\t\t\t\tn := now()\n\t\t\t\t\tassert.True(t, n.Add(3*time.Second).After(cert.NotBefore), fmt.Sprintf(\"expected now = %s to be after cert.NotBefore = %s\", n.Add(3*time.Second), cert.NotBefore))\n\t\t\t\t\tassert.True(t, n.Add(-1*time.Minute).Before(cert.NotBefore))\n\n\t\t\t\t\tassert.Equals(t, cert.NotAfter, na)\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"ok/notBefore-and-notAfter-set\": func() test {\n\t\t\tnb := time.Now().Add(5 * time.Minute).UTC()\n\t\t\tna := time.Now().Add(10 * time.Minute).UTC()\n\t\t\td := 4 * time.Hour\n\t\t\treturn test{\n\t\t\t\tpdd: profileDefaultDuration(d),\n\t\t\t\tso:  SignOptions{NotBefore: NewTimeDuration(nb), NotAfter: NewTimeDuration(na)},\n\t\t\t\tcert: &x509.Certificate{\n\t\t\t\t\tNotBefore: time.Now(),\n\t\t\t\t\tNotAfter:  time.Now().Add(time.Hour),\n\t\t\t\t},\n\t\t\t\tvalid: func(cert *x509.Certificate) {\n\t\t\t\t\tassert.Equals(t, nb, cert.NotBefore)\n\t\t\t\t\tassert.Equals(t, na, cert.NotAfter)\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"ok/cert-with-validity\": func() test {\n\t\t\tnb := time.Now().Add(5 * time.Minute).UTC()\n\t\t\tna := time.Now().Add(10 * time.Minute).UTC()\n\t\t\td := 4 * time.Hour\n\t\t\treturn test{\n\t\t\t\tpdd: profileDefaultDuration(d),\n\t\t\t\tso:  SignOptions{},\n\t\t\t\tcert: &x509.Certificate{\n\t\t\t\t\tNotBefore: nb,\n\t\t\t\t\tNotAfter:  na,\n\t\t\t\t},\n\t\t\t\tvalid: func(cert *x509.Certificate) {\n\t\t\t\t\tassert.Equals(t, nb, cert.NotBefore)\n\t\t\t\t\tassert.Equals(t, na, cert.NotAfter)\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"ok/cert-notBefore-option-notafter\": func() test {\n\t\t\tnb := time.Now().Add(5 * time.Minute).UTC()\n\t\t\tna := time.Now().Add(10 * time.Minute).UTC()\n\t\t\td := 4 * time.Hour\n\t\t\treturn test{\n\t\t\t\tpdd: profileDefaultDuration(d),\n\t\t\t\tso:  SignOptions{NotAfter: NewTimeDuration(na)},\n\t\t\t\tcert: &x509.Certificate{\n\t\t\t\t\tNotBefore: nb,\n\t\t\t\t},\n\t\t\t\tvalid: func(cert *x509.Certificate) {\n\t\t\t\t\tassert.Equals(t, nb, cert.NotBefore)\n\t\t\t\t\tassert.Equals(t, na, cert.NotAfter)\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"ok/cert-notAfter-option-notBefore\": func() test {\n\t\t\tnb := time.Now().Add(5 * time.Minute).UTC()\n\t\t\tna := time.Now().Add(10 * time.Minute).UTC()\n\t\t\td := 4 * time.Hour\n\t\t\treturn test{\n\t\t\t\tpdd: profileDefaultDuration(d),\n\t\t\t\tso:  SignOptions{NotBefore: NewTimeDuration(nb)},\n\t\t\t\tcert: &x509.Certificate{\n\t\t\t\t\tNotAfter: na,\n\t\t\t\t},\n\t\t\t\tvalid: func(cert *x509.Certificate) {\n\t\t\t\t\tassert.Equals(t, cert.NotBefore, nb)\n\t\t\t\t\tassert.Equals(t, cert.NotAfter, na)\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ttt := run()\n\t\t\tassert.FatalError(t, tt.pdd.Modify(tt.cert, tt.so), \"unexpected error\")\n\t\t\ttime.Sleep(100 * time.Millisecond)\n\t\t\ttt.valid(tt.cert)\n\t\t})\n\t}\n}\n\nfunc Test_newProvisionerExtension_Option(t *testing.T) {\n\texpectedValue, err := asn1.Marshal(extensionASN1{\n\t\tType:          int(TypeJWK),\n\t\tName:          []byte(\"name\"),\n\t\tCredentialID:  []byte(\"credentialId\"),\n\t\tKeyValuePairs: []string{\"key\", \"value\"},\n\t})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\t// Claims with smallstep extensions disabled.\n\tclaimer, err := NewClaimer(&Claims{\n\t\tDisableSmallstepExtensions: &trueValue,\n\t}, globalProvisionerClaims)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\ttype test struct {\n\t\tmodifier *provisionerExtensionOption\n\t\tcert     *x509.Certificate\n\t\tvalid    func(*x509.Certificate)\n\t}\n\ttests := map[string]func() test{\n\t\t\"ok/one-element\": func() test {\n\t\t\treturn test{\n\t\t\t\tmodifier: newProvisionerExtensionOption(TypeJWK, \"name\", \"credentialId\", \"key\", \"value\"),\n\t\t\t\tcert:     new(x509.Certificate),\n\t\t\t\tvalid: func(cert *x509.Certificate) {\n\t\t\t\t\tif assert.Len(t, 1, cert.ExtraExtensions) {\n\t\t\t\t\t\text := cert.ExtraExtensions[0]\n\t\t\t\t\t\tassert.Equals(t, StepOIDProvisioner, ext.Id)\n\t\t\t\t\t\tassert.Equals(t, expectedValue, ext.Value)\n\t\t\t\t\t\tassert.False(t, ext.Critical)\n\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"ok/replace\": func() test {\n\t\t\treturn test{\n\t\t\t\tmodifier: newProvisionerExtensionOption(TypeJWK, \"name\", \"credentialId\", \"key\", \"value\"),\n\t\t\t\tcert:     &x509.Certificate{ExtraExtensions: []pkix.Extension{{Id: StepOIDProvisioner, Critical: true}, {Id: []int{1, 2, 3}}}},\n\t\t\t\tvalid: func(cert *x509.Certificate) {\n\t\t\t\t\tif assert.Len(t, 2, cert.ExtraExtensions) {\n\t\t\t\t\t\text := cert.ExtraExtensions[0]\n\t\t\t\t\t\tassert.Equals(t, StepOIDProvisioner, ext.Id)\n\t\t\t\t\t\tassert.Equals(t, expectedValue, ext.Value)\n\t\t\t\t\t\tassert.False(t, ext.Critical)\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"ok/disabled\": func() test {\n\t\t\treturn test{\n\t\t\t\tmodifier: newProvisionerExtensionOption(TypeJWK, \"name\", \"credentialId\", \"key\", \"value\").WithControllerOptions(&Controller{\n\t\t\t\t\tClaimer: claimer,\n\t\t\t\t}),\n\t\t\t\tcert: new(x509.Certificate),\n\t\t\t\tvalid: func(cert *x509.Certificate) {\n\t\t\t\t\tassert.Len(t, 0, cert.ExtraExtensions)\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ttt := run()\n\t\t\tassert.FatalError(t, tt.modifier.Modify(tt.cert, SignOptions{}))\n\t\t\ttt.valid(tt.cert)\n\t\t})\n\t}\n}\n\nfunc Test_profileLimitDuration_Option(t *testing.T) {\n\tn, fn := mockNow()\n\tdefer fn()\n\n\ttype test struct {\n\t\tpld   profileLimitDuration\n\t\tso    SignOptions\n\t\tcert  *x509.Certificate\n\t\tvalid func(*x509.Certificate)\n\t\terr   error\n\t}\n\ttests := map[string]func() test{\n\t\t\"fail/notBefore-before-active-window\": func() test {\n\t\t\td, err := ParseTimeDuration(\"6h\")\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tpld:  profileLimitDuration{def: 4 * time.Hour, notBefore: n.Add(8 * time.Hour)},\n\t\t\t\tso:   SignOptions{NotBefore: d},\n\t\t\t\tcert: new(x509.Certificate),\n\t\t\t\terr:  errors.New(\"requested certificate notBefore (\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/requested-notAfter-after-limit\": func() test {\n\t\t\td, err := ParseTimeDuration(\"4h\")\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tpld:  profileLimitDuration{def: 4 * time.Hour, notAfter: n.Add(6 * time.Hour)},\n\t\t\t\tso:   SignOptions{NotBefore: NewTimeDuration(n.Add(3 * time.Hour)), NotAfter: d},\n\t\t\t\tcert: new(x509.Certificate),\n\t\t\t\terr:  errors.New(\"requested certificate notAfter (\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/cert-validity-notBefore\": func() test {\n\t\t\treturn test{\n\t\t\t\tpld: profileLimitDuration{def: 4 * time.Hour, notBefore: n, notAfter: n.Add(6 * time.Hour)},\n\t\t\t\tso:  SignOptions{},\n\t\t\t\tcert: &x509.Certificate{\n\t\t\t\t\tNotBefore: n.Add(-time.Second),\n\t\t\t\t\tNotAfter:  n.Add(5 * time.Hour),\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"requested certificate notBefore (\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/cert-validity-notAfter\": func() test {\n\t\t\treturn test{\n\t\t\t\tpld: profileLimitDuration{def: 4 * time.Hour, notBefore: n, notAfter: n.Add(6 * time.Hour)},\n\t\t\t\tso:  SignOptions{},\n\t\t\t\tcert: &x509.Certificate{\n\t\t\t\t\tNotBefore: n,\n\t\t\t\t\tNotAfter:  n.Add(6*time.Hour + time.Second),\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"requested certificate notAfter (\"),\n\t\t\t}\n\t\t},\n\t\t\"ok/valid-notAfter-requested\": func() test {\n\t\t\td, err := ParseTimeDuration(\"2h\")\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tpld:  profileLimitDuration{def: 4 * time.Hour, notAfter: n.Add(6 * time.Hour)},\n\t\t\t\tso:   SignOptions{NotBefore: NewTimeDuration(n.Add(3 * time.Hour)), NotAfter: d, Backdate: 1 * time.Minute},\n\t\t\t\tcert: new(x509.Certificate),\n\t\t\t\tvalid: func(cert *x509.Certificate) {\n\t\t\t\t\tassert.Equals(t, cert.NotBefore, n.Add(3*time.Hour))\n\t\t\t\t\tassert.Equals(t, cert.NotAfter, n.Add(5*time.Hour))\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"ok/valid-notAfter-nil-limit-over-default\": func() test {\n\t\t\treturn test{\n\t\t\t\tpld:  profileLimitDuration{def: 1 * time.Hour, notAfter: n.Add(6 * time.Hour)},\n\t\t\t\tso:   SignOptions{NotBefore: NewTimeDuration(n.Add(3 * time.Hour)), Backdate: 1 * time.Minute},\n\t\t\t\tcert: new(x509.Certificate),\n\t\t\t\tvalid: func(cert *x509.Certificate) {\n\t\t\t\t\tassert.Equals(t, cert.NotBefore, n.Add(3*time.Hour))\n\t\t\t\t\tassert.Equals(t, cert.NotAfter, n.Add(4*time.Hour))\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"ok/valid-notAfter-nil-limit-under-default\": func() test {\n\t\t\treturn test{\n\t\t\t\tpld:  profileLimitDuration{def: 4 * time.Hour, notAfter: n.Add(6 * time.Hour)},\n\t\t\t\tso:   SignOptions{NotBefore: NewTimeDuration(n.Add(3 * time.Hour)), Backdate: 1 * time.Minute},\n\t\t\t\tcert: new(x509.Certificate),\n\t\t\t\tvalid: func(cert *x509.Certificate) {\n\t\t\t\t\tassert.Equals(t, cert.NotBefore, n.Add(3*time.Hour))\n\t\t\t\t\tassert.Equals(t, cert.NotAfter, n.Add(6*time.Hour))\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"ok/over-limit-with-backdate\": func() test {\n\t\t\treturn test{\n\t\t\t\tpld:  profileLimitDuration{def: 24 * time.Hour, notAfter: n.Add(6 * time.Hour)},\n\t\t\t\tso:   SignOptions{Backdate: 1 * time.Minute},\n\t\t\t\tcert: new(x509.Certificate),\n\t\t\t\tvalid: func(cert *x509.Certificate) {\n\t\t\t\t\tassert.Equals(t, cert.NotBefore, n.Add(-time.Minute))\n\t\t\t\t\tassert.Equals(t, cert.NotAfter, n.Add(6*time.Hour))\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"ok/under-limit-with-backdate\": func() test {\n\t\t\treturn test{\n\t\t\t\tpld:  profileLimitDuration{def: 24 * time.Hour, notAfter: n.Add(30 * time.Hour)},\n\t\t\t\tso:   SignOptions{Backdate: 1 * time.Minute},\n\t\t\t\tcert: new(x509.Certificate),\n\t\t\t\tvalid: func(cert *x509.Certificate) {\n\t\t\t\t\tassert.Equals(t, cert.NotBefore, n.Add(-time.Minute))\n\t\t\t\t\tassert.Equals(t, cert.NotAfter, n.Add(24*time.Hour))\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"ok/cert-validity\": func() test {\n\t\t\treturn test{\n\t\t\t\tpld: profileLimitDuration{def: 4 * time.Hour, notBefore: n, notAfter: n.Add(6 * time.Hour)},\n\t\t\t\tso:  SignOptions{},\n\t\t\t\tcert: &x509.Certificate{\n\t\t\t\t\tNotBefore: n,\n\t\t\t\t\tNotAfter:  n.Add(5 * time.Hour),\n\t\t\t\t},\n\t\t\t\tvalid: func(cert *x509.Certificate) {\n\t\t\t\t\tassert.Equals(t, n, cert.NotBefore)\n\t\t\t\t\tassert.Equals(t, n.Add(5*time.Hour), cert.NotAfter)\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"ok/cert-notBefore-default\": func() test {\n\t\t\treturn test{\n\t\t\t\tpld: profileLimitDuration{def: 4 * time.Hour, notBefore: n, notAfter: n.Add(6 * time.Hour)},\n\t\t\t\tso:  SignOptions{},\n\t\t\t\tcert: &x509.Certificate{\n\t\t\t\t\tNotBefore: n,\n\t\t\t\t},\n\t\t\t\tvalid: func(cert *x509.Certificate) {\n\t\t\t\t\tassert.Equals(t, n, cert.NotBefore)\n\t\t\t\t\tassert.Equals(t, n.Add(4*time.Hour), cert.NotAfter)\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"ok/cert-notAfter-default\": func() test {\n\t\t\treturn test{\n\t\t\t\tpld: profileLimitDuration{def: 4 * time.Hour, notBefore: n, notAfter: n.Add(6 * time.Hour)},\n\t\t\t\tso:  SignOptions{},\n\t\t\t\tcert: &x509.Certificate{\n\t\t\t\t\tNotAfter: n.Add(5 * time.Hour),\n\t\t\t\t},\n\t\t\t\tvalid: func(cert *x509.Certificate) {\n\t\t\t\t\tassert.Equals(t, n, cert.NotBefore)\n\t\t\t\t\tassert.Equals(t, n.Add(5*time.Hour), cert.NotAfter)\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"ok/cert-notBefore-option\": func() test {\n\t\t\treturn test{\n\t\t\t\tpld: profileLimitDuration{def: 4 * time.Hour, notBefore: n, notAfter: n.Add(6 * time.Hour)},\n\t\t\t\tso:  SignOptions{NotAfter: NewTimeDuration(n.Add(5 * time.Hour))},\n\t\t\t\tcert: &x509.Certificate{\n\t\t\t\t\tNotBefore: n,\n\t\t\t\t},\n\t\t\t\tvalid: func(cert *x509.Certificate) {\n\t\t\t\t\tassert.Equals(t, n, cert.NotBefore)\n\t\t\t\t\tassert.Equals(t, n.Add(5*time.Hour), cert.NotAfter)\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"ok/cert-notAfter-option\": func() test {\n\t\t\treturn test{\n\t\t\t\tpld: profileLimitDuration{def: 4 * time.Hour, notBefore: n, notAfter: n.Add(6 * time.Hour)},\n\t\t\t\tso:  SignOptions{NotBefore: NewTimeDuration(n.Add(4 * time.Hour))},\n\t\t\t\tcert: &x509.Certificate{\n\t\t\t\t\tNotAfter: n.Add(5 * time.Hour),\n\t\t\t\t},\n\t\t\t\tvalid: func(cert *x509.Certificate) {\n\t\t\t\t\tassert.Equals(t, n.Add(4*time.Hour), cert.NotBefore)\n\t\t\t\t\tassert.Equals(t, n.Add(5*time.Hour), cert.NotAfter)\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ttt := run()\n\t\t\tif err := tt.pld.Modify(tt.cert, tt.so); err != nil {\n\t\t\t\tif assert.NotNil(t, tt.err) {\n\t\t\t\t\tassert.HasPrefix(t, err.Error(), tt.err.Error())\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif assert.Nil(t, tt.err) {\n\t\t\t\t\ttt.valid(tt.cert)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "authority/provisioner/sign_ssh_options.go",
    "content": "package provisioner\n\nimport (\n\t\"crypto/rsa\"\n\t\"encoding/binary\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"math/big\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n\t\"golang.org/x/crypto/ssh\"\n\n\t\"go.step.sm/crypto/keyutil\"\n\n\t\"github.com/smallstep/certificates/authority/policy\"\n\t\"github.com/smallstep/certificates/errs\"\n\t\"github.com/smallstep/certificates/internal/cast\"\n)\n\nconst (\n\t// SSHUserCert is the string used to represent ssh.UserCert.\n\tSSHUserCert = \"user\"\n\n\t// SSHHostCert is the string used to represent ssh.HostCert.\n\tSSHHostCert = \"host\"\n)\n\n// SSHCertModifier is the interface used to change properties in an SSH\n// certificate.\ntype SSHCertModifier interface {\n\tSignOption\n\tModify(cert *ssh.Certificate, opts SignSSHOptions) error\n}\n\n// SSHCertValidator is the interface used to validate an SSH certificate.\ntype SSHCertValidator interface {\n\tSignOption\n\tValid(cert *ssh.Certificate, opts SignSSHOptions) error\n}\n\n// SSHCertOptionsValidator is the interface used to validate the custom\n// options used to modify the SSH certificate.\ntype SSHCertOptionsValidator interface {\n\tSignOption\n\tValid(got SignSSHOptions) error\n}\n\n// SSHPublicKeyValidator is the interface used to validate the public key of an\n// SSH certificate.\ntype SSHPublicKeyValidator interface {\n\tSignOption\n\tValid(got ssh.PublicKey) error\n}\n\n// SignSSHOptions contains the options that can be passed to the SignSSH method.\ntype SignSSHOptions struct {\n\tCertType     string          `json:\"certType\"`\n\tKeyID        string          `json:\"keyID\"`\n\tPrincipals   []string        `json:\"principals\"`\n\tValidAfter   TimeDuration    `json:\"validAfter,omitempty\"`\n\tValidBefore  TimeDuration    `json:\"validBefore,omitempty\"`\n\tTemplateData json.RawMessage `json:\"templateData,omitempty\"`\n\tBackdate     time.Duration   `json:\"-\"`\n}\n\n// Validate validates the given SignSSHOptions.\nfunc (o SignSSHOptions) Validate() error {\n\tif o.CertType != \"\" && o.CertType != SSHUserCert && o.CertType != SSHHostCert {\n\t\treturn errs.BadRequest(\"certType '%s' is not valid\", o.CertType)\n\t}\n\tfor _, p := range o.Principals {\n\t\tif p == \"\" {\n\t\t\treturn errs.BadRequest(\"principals cannot contain empty values\")\n\t\t}\n\t}\n\treturn nil\n}\n\n// Type returns the uint32 representation of the CertType.\nfunc (o SignSSHOptions) Type() uint32 {\n\treturn sshCertTypeUInt32(o.CertType)\n}\n\n// Modify implements SSHCertModifier and sets the SSHOption in the ssh.Certificate.\nfunc (o SignSSHOptions) Modify(cert *ssh.Certificate, _ SignSSHOptions) error {\n\tswitch o.CertType {\n\tcase \"\": // ignore\n\tcase SSHUserCert:\n\t\tcert.CertType = ssh.UserCert\n\tcase SSHHostCert:\n\t\tcert.CertType = ssh.HostCert\n\tdefault:\n\t\treturn errs.BadRequest(\"ssh certificate has an unknown type '%s'\", o.CertType)\n\t}\n\n\tcert.KeyId = o.KeyID\n\tcert.ValidPrincipals = o.Principals\n\n\treturn o.ModifyValidity(cert)\n}\n\n// ModifyValidity modifies only the ValidAfter and ValidBefore on the given\n// ssh.Certificate.\nfunc (o SignSSHOptions) ModifyValidity(cert *ssh.Certificate) error {\n\tt := now()\n\tif !o.ValidAfter.IsZero() {\n\t\tcert.ValidAfter = cast.Uint64(o.ValidAfter.RelativeTime(t).Unix())\n\t}\n\tif !o.ValidBefore.IsZero() {\n\t\tcert.ValidBefore = cast.Uint64(o.ValidBefore.RelativeTime(t).Unix())\n\t}\n\tif cert.ValidAfter > 0 && cert.ValidBefore > 0 && cert.ValidAfter > cert.ValidBefore {\n\t\treturn errs.BadRequest(\"ssh certificate validAfter cannot be greater than validBefore\")\n\t}\n\treturn nil\n}\n\n// match compares two SSHOptions and return an error if they don't match. It\n// ignores zero values.\nfunc (o SignSSHOptions) match(got SignSSHOptions) error {\n\tif o.CertType != \"\" && got.CertType != \"\" && o.CertType != got.CertType {\n\t\treturn errs.Forbidden(\"ssh certificate type does not match - got %v, want %v\", got.CertType, o.CertType)\n\t}\n\tif len(o.Principals) > 0 && len(got.Principals) > 0 && !containsAllMembers(o.Principals, got.Principals) {\n\t\treturn errs.Forbidden(\"ssh certificate principals does not match - got %v, want %v\", got.Principals, o.Principals)\n\t}\n\tif !o.ValidAfter.IsZero() && !got.ValidAfter.IsZero() && !o.ValidAfter.Equal(&got.ValidAfter) {\n\t\treturn errs.Forbidden(\"ssh certificate validAfter does not match - got %v, want %v\", got.ValidAfter, o.ValidAfter)\n\t}\n\tif !o.ValidBefore.IsZero() && !got.ValidBefore.IsZero() && !o.ValidBefore.Equal(&got.ValidBefore) {\n\t\treturn errs.Forbidden(\"ssh certificate validBefore does not match - got %v, want %v\", got.ValidBefore, o.ValidBefore)\n\t}\n\treturn nil\n}\n\n// sshCertValidAfterModifier is an SSHCertModifier that sets the\n// ValidAfter in the SSH certificate.\ntype sshCertValidAfterModifier uint64\n\nfunc (m sshCertValidAfterModifier) Modify(cert *ssh.Certificate, _ SignSSHOptions) error {\n\tcert.ValidAfter = uint64(m)\n\treturn nil\n}\n\n// sshCertValidBeforeModifier is an SSHCertModifier that sets the\n// ValidBefore in the SSH certificate.\ntype sshCertValidBeforeModifier uint64\n\nfunc (m sshCertValidBeforeModifier) Modify(cert *ssh.Certificate, _ SignSSHOptions) error {\n\tcert.ValidBefore = uint64(m)\n\treturn nil\n}\n\n// sshDefaultDuration is an SSHCertModifier that sets the certificate\n// ValidAfter and ValidBefore if they have not been set. It will fail if a\n// CertType has not been set or is not valid.\ntype sshDefaultDuration struct {\n\t*Claimer\n}\n\n// Modify implements SSHCertModifier and sets the validity if it has not been\n// set, but it always applies the backdate.\nfunc (m *sshDefaultDuration) Modify(cert *ssh.Certificate, o SignSSHOptions) error {\n\td, err := m.DefaultSSHCertDuration(cert.CertType)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar backdate uint64\n\tif cert.ValidAfter == 0 {\n\t\tbackdate = cast.Uint64(o.Backdate / time.Second)\n\t\tcert.ValidAfter = cast.Uint64(now().Truncate(time.Second).Unix())\n\t}\n\tif cert.ValidBefore == 0 {\n\t\tcert.ValidBefore = cert.ValidAfter + cast.Uint64(d/time.Second)\n\t}\n\t// Apply backdate safely\n\tif cert.ValidAfter > backdate {\n\t\tcert.ValidAfter -= backdate\n\t}\n\treturn nil\n}\n\n// sshLimitDuration adjusts the duration to min(default, remaining provisioning\n// credential duration). E.g. if the default is 12hrs but the remaining validity\n// of the provisioning credential is only 4hrs, this option will set the value\n// to 4hrs (the min of the two values). It will fail if a CertType has not been\n// set or is not valid.\ntype sshLimitDuration struct {\n\t*Claimer\n\tNotAfter time.Time\n}\n\n// Modify implements SSHCertModifier and modifies the validity of the\n// certificate to expire before the configured limit.\nfunc (m *sshLimitDuration) Modify(cert *ssh.Certificate, o SignSSHOptions) error {\n\tif m.NotAfter.IsZero() {\n\t\tdefaultDuration := &sshDefaultDuration{m.Claimer}\n\t\treturn defaultDuration.Modify(cert, o)\n\t}\n\n\t// Make sure the duration is within the limits.\n\td, err := m.DefaultSSHCertDuration(cert.CertType)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar backdate uint64\n\tif cert.ValidAfter == 0 {\n\t\tbackdate = cast.Uint64(o.Backdate / time.Second)\n\t\tcert.ValidAfter = cast.Uint64(now().Truncate(time.Second).Unix())\n\t}\n\n\tcertValidAfter := time.Unix(cast.Int64(cert.ValidAfter), 0)\n\tif certValidAfter.After(m.NotAfter) {\n\t\treturn errs.Forbidden(\"provisioning credential expiration (%s) is before requested certificate validAfter (%s)\",\n\t\t\tm.NotAfter, certValidAfter)\n\t}\n\n\tif cert.ValidBefore == 0 {\n\t\tcertValidBefore := certValidAfter.Add(d)\n\t\tif m.NotAfter.Before(certValidBefore) {\n\t\t\tcertValidBefore = m.NotAfter\n\t\t}\n\t\tcert.ValidBefore = cast.Uint64(certValidBefore.Unix())\n\t} else {\n\t\tcertValidBefore := time.Unix(cast.Int64(cert.ValidBefore), 0)\n\t\tif m.NotAfter.Before(certValidBefore) {\n\t\t\treturn errs.Forbidden(\"provisioning credential expiration (%s) is before requested certificate validBefore (%s)\",\n\t\t\t\tm.NotAfter, certValidBefore)\n\t\t}\n\t}\n\n\t// Apply backdate safely\n\tif cert.ValidAfter > backdate {\n\t\tcert.ValidAfter -= backdate\n\t}\n\n\treturn nil\n}\n\n// sshCertOptionsValidator validates the user SSHOptions with the ones\n// usually present in the token.\ntype sshCertOptionsValidator SignSSHOptions\n\n// Valid implements SSHCertOptionsValidator and returns nil if both\n// SSHOptions match.\nfunc (v sshCertOptionsValidator) Valid(got SignSSHOptions) error {\n\twant := SignSSHOptions(v)\n\treturn want.match(got)\n}\n\n// sshCertOptionsRequireValidator defines which elements in the SignSSHOptions are required.\ntype sshCertOptionsRequireValidator struct {\n\tCertType   bool\n\tKeyID      bool\n\tPrincipals bool\n}\n\nfunc (v *sshCertOptionsRequireValidator) Valid(got SignSSHOptions) error {\n\tswitch {\n\tcase v.CertType && got.CertType == \"\":\n\t\treturn errs.BadRequest(\"ssh certificate certType cannot be empty\")\n\tcase v.KeyID && got.KeyID == \"\":\n\t\treturn errs.BadRequest(\"ssh certificate keyID cannot be empty\")\n\tcase v.Principals && len(got.Principals) == 0:\n\t\treturn errs.BadRequest(\"ssh certificate principals cannot be empty\")\n\tdefault:\n\t\treturn nil\n\t}\n}\n\ntype sshCertValidityValidator struct {\n\t*Claimer\n}\n\nfunc (v *sshCertValidityValidator) Valid(cert *ssh.Certificate, opts SignSSHOptions) error {\n\tswitch {\n\tcase cert.ValidAfter == 0:\n\t\treturn errs.BadRequest(\"ssh certificate validAfter cannot be 0\")\n\tcase cert.ValidBefore < cast.Uint64(now().Unix()):\n\t\treturn errs.BadRequest(\"ssh certificate validBefore cannot be in the past\")\n\tcase cert.ValidBefore < cert.ValidAfter:\n\t\treturn errs.BadRequest(\"ssh certificate validBefore cannot be before validAfter\")\n\t}\n\n\tvar minDur, maxDur time.Duration\n\tswitch cert.CertType {\n\tcase ssh.UserCert:\n\t\tminDur = v.MinUserSSHCertDuration()\n\t\tmaxDur = v.MaxUserSSHCertDuration()\n\tcase ssh.HostCert:\n\t\tminDur = v.MinHostSSHCertDuration()\n\t\tmaxDur = v.MaxHostSSHCertDuration()\n\tcase 0:\n\t\treturn errs.BadRequest(\"ssh certificate type has not been set\")\n\tdefault:\n\t\treturn errs.BadRequest(\"ssh certificate has an unknown type '%d'\", cert.CertType)\n\t}\n\n\t// To not take into account the backdate, time.Now() will be used to\n\t// calculate the duration if ValidAfter is in the past.\n\tdur := time.Duration(cast.Int64(cert.ValidBefore-cert.ValidAfter)) * time.Second\n\n\tswitch {\n\tcase dur < minDur:\n\t\treturn errs.Forbidden(\"requested duration of %s is less than minimum accepted duration for selected provisioner of %s\", dur, minDur)\n\tcase dur > maxDur+opts.Backdate:\n\t\treturn errs.Forbidden(\"requested duration of %s is greater than maximum accepted duration for selected provisioner of %s\", dur, maxDur+opts.Backdate)\n\tdefault:\n\t\treturn nil\n\t}\n}\n\n// sshCertDefaultValidator implements a simple validator for all the\n// fields in the SSH certificate.\ntype sshCertDefaultValidator struct{}\n\n// Valid returns an error if the given certificate does not contain the\n// necessary fields. We skip ValidPrincipals and Extensions as with custom\n// templates you can set them empty.\nfunc (v *sshCertDefaultValidator) Valid(cert *ssh.Certificate, _ SignSSHOptions) error {\n\tswitch {\n\tcase len(cert.Nonce) == 0:\n\t\treturn errs.Forbidden(\"ssh certificate nonce cannot be empty\")\n\tcase cert.Key == nil:\n\t\treturn errs.Forbidden(\"ssh certificate key cannot be nil\")\n\tcase cert.Serial == 0:\n\t\treturn errs.Forbidden(\"ssh certificate serial cannot be 0\")\n\tcase cert.CertType != ssh.UserCert && cert.CertType != ssh.HostCert:\n\t\treturn errs.Forbidden(\"ssh certificate has an unknown type '%d'\", cert.CertType)\n\tcase cert.KeyId == \"\":\n\t\treturn errs.Forbidden(\"ssh certificate key id cannot be empty\")\n\tcase cert.ValidAfter == 0:\n\t\treturn errs.Forbidden(\"ssh certificate validAfter cannot be 0\")\n\tcase cert.ValidBefore < cast.Uint64(now().Unix()):\n\t\treturn errs.Forbidden(\"ssh certificate validBefore cannot be in the past\")\n\tcase cert.ValidBefore < cert.ValidAfter:\n\t\treturn errs.Forbidden(\"ssh certificate validBefore cannot be before validAfter\")\n\tcase cert.SignatureKey == nil:\n\t\treturn errs.Forbidden(\"ssh certificate signature key cannot be nil\")\n\tcase cert.Signature == nil:\n\t\treturn errs.Forbidden(\"ssh certificate signature cannot be nil\")\n\tdefault:\n\t\treturn nil\n\t}\n}\n\n// sshDefaultPublicKeyValidator implements a validator for the certificate key.\ntype sshDefaultPublicKeyValidator struct{}\n\n// Valid checks that certificate request common name matches the one configured.\n//\n// TODO: this is the only validator that checks the key type. We should execute\n// this before the signing. We should add a new validations interface or extend\n// SSHCertOptionsValidator with the key.\nfunc (v sshDefaultPublicKeyValidator) Valid(cert *ssh.Certificate, _ SignSSHOptions) error {\n\tif cert.Key == nil {\n\t\treturn errs.BadRequest(\"ssh certificate key cannot be nil\")\n\t}\n\tswitch cert.Key.Type() {\n\tcase ssh.KeyAlgoRSA:\n\t\t_, in, ok := sshParseString(cert.Key.Marshal())\n\t\tif !ok {\n\t\t\treturn errs.BadRequest(\"ssh certificate key is invalid\")\n\t\t}\n\t\tkey, err := sshParseRSAPublicKey(in)\n\t\tif err != nil {\n\t\t\treturn errs.BadRequestErr(err, \"error parsing public key\")\n\t\t}\n\t\tif key.Size() < keyutil.MinRSAKeyBytes {\n\t\t\treturn errs.Forbidden(\"ssh certificate key must be at least %d bits (%d bytes)\",\n\t\t\t\t8*keyutil.MinRSAKeyBytes, keyutil.MinRSAKeyBytes)\n\t\t}\n\t\treturn nil\n\tcase ssh.InsecureKeyAlgoDSA: //nolint:staticcheck // only using the constant for lookup; no dependent logic\n\t\treturn errs.BadRequest(\"ssh certificate key algorithm (DSA) is not supported\")\n\tdefault:\n\t\treturn nil\n\t}\n}\n\n// sshNamePolicyValidator validates that the certificate (to be signed)\n// contains only allowed principals.\ntype sshNamePolicyValidator struct {\n\thostPolicyEngine policy.HostPolicy\n\tuserPolicyEngine policy.UserPolicy\n}\n\n// newSSHNamePolicyValidator return a new SSH allow/deny validator.\nfunc newSSHNamePolicyValidator(host policy.HostPolicy, user policy.UserPolicy) *sshNamePolicyValidator {\n\treturn &sshNamePolicyValidator{\n\t\thostPolicyEngine: host,\n\t\tuserPolicyEngine: user,\n\t}\n}\n\n// Valid validates that the certificate (to be signed) contains only allowed principals.\nfunc (v *sshNamePolicyValidator) Valid(cert *ssh.Certificate, _ SignSSHOptions) error {\n\tif v.hostPolicyEngine == nil && v.userPolicyEngine == nil {\n\t\t// no policy configured at all; allow anything\n\t\treturn nil\n\t}\n\n\t// Check the policy type to execute based on type of the certificate.\n\t// We don't allow user certs if only a host policy engine is configured and\n\t// the same for host certs: if only a user policy engine is configured, host\n\t// certs are denied. When both policy engines are configured, the type of\n\t// cert determines which policy engine is used.\n\tswitch cert.CertType {\n\tcase ssh.HostCert:\n\t\t// when no host policy engine is configured, but a user policy engine is\n\t\t// configured, the host certificate is denied.\n\t\tif v.hostPolicyEngine == nil && v.userPolicyEngine != nil {\n\t\t\treturn errors.New(\"SSH host certificate not authorized\")\n\t\t}\n\t\treturn v.hostPolicyEngine.IsSSHCertificateAllowed(cert)\n\tcase ssh.UserCert:\n\t\t// when no user policy engine is configured, but a host policy engine is\n\t\t// configured, the user certificate is denied.\n\t\tif v.userPolicyEngine == nil && v.hostPolicyEngine != nil {\n\t\t\treturn errors.New(\"SSH user certificate not authorized\")\n\t\t}\n\t\treturn v.userPolicyEngine.IsSSHCertificateAllowed(cert)\n\tdefault:\n\t\treturn fmt.Errorf(\"unexpected SSH certificate type %d\", cert.CertType) // satisfy return; shouldn't happen\n\t}\n}\n\n// sshCertTypeUInt32\nfunc sshCertTypeUInt32(ct string) uint32 {\n\tswitch ct {\n\tcase SSHUserCert:\n\t\treturn ssh.UserCert\n\tcase SSHHostCert:\n\t\treturn ssh.HostCert\n\tdefault:\n\t\treturn 0\n\t}\n}\n\n// containsAllMembers reports whether all members of subgroup are within group.\nfunc containsAllMembers(group, subgroup []string) bool {\n\tlg, lsg := len(group), len(subgroup)\n\tif lsg > lg || (lg > 0 && lsg == 0) {\n\t\treturn false\n\t}\n\tvisit := make(map[string]struct{}, lg)\n\tfor i := 0; i < lg; i++ {\n\t\tvisit[strings.ToLower(group[i])] = struct{}{}\n\t}\n\tfor i := 0; i < lsg; i++ {\n\t\tif _, ok := visit[strings.ToLower(subgroup[i])]; !ok {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\nfunc sshParseString(in []byte) (out, rest []byte, ok bool) {\n\tif len(in) < 4 {\n\t\treturn\n\t}\n\tlength := binary.BigEndian.Uint32(in)\n\tin = in[4:]\n\tif cast.Uint32(len(in)) < length {\n\t\treturn\n\t}\n\tout = in[:length]\n\trest = in[length:]\n\tok = true\n\treturn\n}\n\nfunc sshParseRSAPublicKey(in []byte) (*rsa.PublicKey, error) {\n\tvar w struct {\n\t\tE    *big.Int\n\t\tN    *big.Int\n\t\tRest []byte `ssh:\"rest\"`\n\t}\n\tif err := ssh.Unmarshal(in, &w); err != nil {\n\t\treturn nil, errors.Wrap(err, \"error unmarshalling public key\")\n\t}\n\tif w.E.BitLen() > 24 {\n\t\treturn nil, errors.New(\"invalid public key: exponent too large\")\n\t}\n\te := w.E.Int64()\n\tif e < 3 || e&1 == 0 {\n\t\treturn nil, errors.New(\"invalid public key: incorrect exponent\")\n\t}\n\n\tvar key rsa.PublicKey\n\tkey.E = int(e)\n\tkey.N = w.N\n\treturn &key, nil\n}\n"
  },
  {
    "path": "authority/provisioner/sign_ssh_options_test.go",
    "content": "package provisioner\n\nimport (\n\t\"reflect\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n\t\"github.com/smallstep/assert\"\n\t\"go.step.sm/crypto/keyutil\"\n\t\"golang.org/x/crypto/ssh\"\n)\n\nfunc TestSSHOptions_Type(t *testing.T) {\n\ttype fields struct {\n\t\tCertType string\n\t}\n\ttests := []struct {\n\t\tname   string\n\t\tfields fields\n\t\twant   uint32\n\t}{\n\t\t{\"user\", fields{\"user\"}, 1},\n\t\t{\"host\", fields{\"host\"}, 2},\n\t\t{\"empty\", fields{\"\"}, 0},\n\t\t{\"invalid\", fields{\"invalid\"}, 0},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\to := SignSSHOptions{\n\t\t\t\tCertType: tt.fields.CertType,\n\t\t\t}\n\t\t\tif got := o.Type(); got != tt.want {\n\t\t\t\tt.Errorf(\"SSHOptions.Type() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSSHOptions_Modify(t *testing.T) {\n\ttype test struct {\n\t\tso    SignSSHOptions\n\t\tcert  *ssh.Certificate\n\t\tvalid func(*ssh.Certificate)\n\t\terr   error\n\t}\n\ttests := map[string]func() test{\n\t\t\"fail/unexpected-cert-type\": func() test {\n\t\t\treturn test{\n\t\t\t\tso:   SignSSHOptions{CertType: \"foo\"},\n\t\t\t\tcert: new(ssh.Certificate),\n\t\t\t\terr:  errors.Errorf(\"ssh certificate has an unknown type 'foo'\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/validAfter-greater-validBefore\": func() test {\n\t\t\treturn test{\n\t\t\t\tso:   SignSSHOptions{CertType: \"user\"},\n\t\t\t\tcert: &ssh.Certificate{ValidAfter: uint64(15), ValidBefore: uint64(10)},\n\t\t\t\terr:  errors.Errorf(\"ssh certificate validAfter cannot be greater than validBefore\"),\n\t\t\t}\n\t\t},\n\t\t\"ok/user-cert\": func() test {\n\t\t\treturn test{\n\t\t\t\tso:   SignSSHOptions{CertType: \"user\"},\n\t\t\t\tcert: new(ssh.Certificate),\n\t\t\t\tvalid: func(cert *ssh.Certificate) {\n\t\t\t\t\tassert.Equals(t, cert.CertType, uint32(ssh.UserCert))\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"ok/host-cert\": func() test {\n\t\t\treturn test{\n\t\t\t\tso:   SignSSHOptions{CertType: \"host\"},\n\t\t\t\tcert: new(ssh.Certificate),\n\t\t\t\tvalid: func(cert *ssh.Certificate) {\n\t\t\t\t\tassert.Equals(t, cert.CertType, uint32(ssh.HostCert))\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"ok\": func() test {\n\t\t\tva := time.Now().Add(5 * time.Minute)\n\t\t\tvb := time.Now().Add(1 * time.Hour)\n\t\t\tso := SignSSHOptions{CertType: \"host\", KeyID: \"foo\", Principals: []string{\"foo\", \"bar\"},\n\t\t\t\tValidAfter: NewTimeDuration(va), ValidBefore: NewTimeDuration(vb)}\n\t\t\treturn test{\n\t\t\t\tso:   so,\n\t\t\t\tcert: new(ssh.Certificate),\n\t\t\t\tvalid: func(cert *ssh.Certificate) {\n\t\t\t\t\tassert.Equals(t, cert.CertType, uint32(ssh.HostCert))\n\t\t\t\t\tassert.Equals(t, cert.KeyId, so.KeyID)\n\t\t\t\t\tassert.Equals(t, cert.ValidPrincipals, so.Principals)\n\t\t\t\t\tassert.Equals(t, cert.ValidAfter, uint64(so.ValidAfter.RelativeTime(time.Now()).Unix()))\n\t\t\t\t\tassert.Equals(t, cert.ValidBefore, uint64(so.ValidBefore.RelativeTime(time.Now()).Unix()))\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ttc := run()\n\t\t\tif err := tc.so.Modify(tc.cert, tc.so); err != nil {\n\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.err.Error())\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif assert.Nil(t, tc.err) {\n\t\t\t\t\ttc.valid(tc.cert)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSSHOptions_Match(t *testing.T) {\n\ttype test struct {\n\t\tso  SignSSHOptions\n\t\tcmp SignSSHOptions\n\t\terr error\n\t}\n\ttests := map[string]func() test{\n\t\t\"fail/cert-type\": func() test {\n\t\t\treturn test{\n\t\t\t\tso:  SignSSHOptions{CertType: \"foo\"},\n\t\t\t\tcmp: SignSSHOptions{CertType: \"bar\"},\n\t\t\t\terr: errors.Errorf(\"ssh certificate type does not match - got bar, want foo\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/pricipals\": func() test {\n\t\t\treturn test{\n\t\t\t\tso:  SignSSHOptions{Principals: []string{\"foo\"}},\n\t\t\t\tcmp: SignSSHOptions{Principals: []string{\"bar\"}},\n\t\t\t\terr: errors.Errorf(\"ssh certificate principals does not match - got [bar], want [foo]\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/validAfter\": func() test {\n\t\t\treturn test{\n\t\t\t\tso:  SignSSHOptions{ValidAfter: NewTimeDuration(time.Now().Add(1 * time.Minute))},\n\t\t\t\tcmp: SignSSHOptions{ValidAfter: NewTimeDuration(time.Now().Add(5 * time.Minute))},\n\t\t\t\terr: errors.Errorf(\"ssh certificate validAfter does not match\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/validBefore\": func() test {\n\t\t\treturn test{\n\t\t\t\tso:  SignSSHOptions{ValidBefore: NewTimeDuration(time.Now().Add(1 * time.Minute))},\n\t\t\t\tcmp: SignSSHOptions{ValidBefore: NewTimeDuration(time.Now().Add(5 * time.Minute))},\n\t\t\t\terr: errors.Errorf(\"ssh certificate validBefore does not match\"),\n\t\t\t}\n\t\t},\n\t\t\"ok/original-empty\": func() test {\n\t\t\treturn test{\n\t\t\t\tso: SignSSHOptions{},\n\t\t\t\tcmp: SignSSHOptions{\n\t\t\t\t\tCertType:    \"foo\",\n\t\t\t\t\tPrincipals:  []string{\"foo\"},\n\t\t\t\t\tValidAfter:  NewTimeDuration(time.Now().Add(1 * time.Minute)),\n\t\t\t\t\tValidBefore: NewTimeDuration(time.Now().Add(5 * time.Minute)),\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"ok/cmp-empty\": func() test {\n\t\t\treturn test{\n\t\t\t\tcmp: SignSSHOptions{},\n\t\t\t\tso: SignSSHOptions{\n\t\t\t\t\tCertType:    \"foo\",\n\t\t\t\t\tPrincipals:  []string{\"foo\"},\n\t\t\t\t\tValidAfter:  NewTimeDuration(time.Now().Add(1 * time.Minute)),\n\t\t\t\t\tValidBefore: NewTimeDuration(time.Now().Add(5 * time.Minute)),\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"ok/equal\": func() test {\n\t\t\tn := time.Now()\n\t\t\tva := NewTimeDuration(n.Add(1 * time.Minute))\n\t\t\tvb := NewTimeDuration(n.Add(5 * time.Minute))\n\t\t\treturn test{\n\t\t\t\tcmp: SignSSHOptions{\n\t\t\t\t\tCertType:    \"foo\",\n\t\t\t\t\tPrincipals:  []string{\"foo\"},\n\t\t\t\t\tValidAfter:  va,\n\t\t\t\t\tValidBefore: vb,\n\t\t\t\t},\n\t\t\t\tso: SignSSHOptions{\n\t\t\t\t\tCertType:    \"foo\",\n\t\t\t\t\tPrincipals:  []string{\"foo\"},\n\t\t\t\t\tValidAfter:  va,\n\t\t\t\t\tValidBefore: vb,\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ttc := run()\n\t\t\tif err := tc.so.match(tc.cmp); err != nil {\n\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.err.Error())\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tassert.Nil(t, tc.err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_sshCertValidAfterModifier_Modify(t *testing.T) {\n\ttype test struct {\n\t\tmodifier sshCertValidAfterModifier\n\t\tcert     *ssh.Certificate\n\t\texpected uint64\n\t}\n\ttests := map[string]func() test{\n\t\t\"ok\": func() test {\n\t\t\treturn test{\n\t\t\t\tmodifier: sshCertValidAfterModifier(15),\n\t\t\t\tcert:     new(ssh.Certificate),\n\t\t\t\texpected: 15,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ttc := run()\n\t\t\tif assert.Nil(t, tc.modifier.Modify(tc.cert, SignSSHOptions{})) {\n\t\t\t\tassert.Equals(t, tc.cert.ValidAfter, tc.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_sshCertDefaultValidator_Valid(t *testing.T) {\n\tpub, _, err := keyutil.GenerateDefaultKeyPair()\n\tassert.FatalError(t, err)\n\tsshPub, err := ssh.NewPublicKey(pub)\n\tassert.FatalError(t, err)\n\tv := sshCertDefaultValidator{}\n\ttests := []struct {\n\t\tname string\n\t\tcert *ssh.Certificate\n\t\terr  error\n\t}{\n\t\t{\n\t\t\t\"fail/zero-nonce\",\n\t\t\t&ssh.Certificate{},\n\t\t\terrors.New(\"ssh certificate nonce cannot be empty\"),\n\t\t},\n\t\t{\n\t\t\t\"fail/nil-key\",\n\t\t\t&ssh.Certificate{Nonce: []byte(\"foo\")},\n\t\t\terrors.New(\"ssh certificate key cannot be nil\"),\n\t\t},\n\t\t{\n\t\t\t\"fail/zero-serial\",\n\t\t\t&ssh.Certificate{Nonce: []byte(\"foo\"), Key: sshPub},\n\t\t\terrors.New(\"ssh certificate serial cannot be 0\"),\n\t\t},\n\t\t{\n\t\t\t\"fail/unexpected-cert-type\",\n\t\t\t// UserCert = 1, HostCert = 2\n\t\t\t&ssh.Certificate{Nonce: []byte(\"foo\"), Key: sshPub, CertType: 3, Serial: 1},\n\t\t\terrors.New(\"ssh certificate has an unknown type '3'\"),\n\t\t},\n\t\t{\n\t\t\t\"fail/empty-cert-key-id\",\n\t\t\t&ssh.Certificate{Nonce: []byte(\"foo\"), Key: sshPub, Serial: 1, CertType: 1},\n\t\t\terrors.New(\"ssh certificate key id cannot be empty\"),\n\t\t},\n\t\t{\n\t\t\t\"fail/zero-validAfter\",\n\t\t\t&ssh.Certificate{\n\t\t\t\tNonce:           []byte(\"foo\"),\n\t\t\t\tKey:             sshPub,\n\t\t\t\tSerial:          1,\n\t\t\t\tCertType:        1,\n\t\t\t\tKeyId:           \"foo\",\n\t\t\t\tValidPrincipals: []string{\"foo\"},\n\t\t\t\tValidAfter:      0,\n\t\t\t},\n\t\t\terrors.New(\"ssh certificate validAfter cannot be 0\"),\n\t\t},\n\t\t{\n\t\t\t\"fail/validBefore-past\",\n\t\t\t&ssh.Certificate{\n\t\t\t\tNonce:           []byte(\"foo\"),\n\t\t\t\tKey:             sshPub,\n\t\t\t\tSerial:          1,\n\t\t\t\tCertType:        1,\n\t\t\t\tKeyId:           \"foo\",\n\t\t\t\tValidPrincipals: []string{\"foo\"},\n\t\t\t\tValidAfter:      uint64(time.Now().Add(-10 * time.Minute).Unix()),\n\t\t\t\tValidBefore:     uint64(time.Now().Add(-5 * time.Minute).Unix()),\n\t\t\t},\n\t\t\terrors.New(\"ssh certificate validBefore cannot be in the past\"),\n\t\t},\n\t\t{\n\t\t\t\"fail/validAfter-after-validBefore\",\n\t\t\t&ssh.Certificate{\n\t\t\t\tNonce:           []byte(\"foo\"),\n\t\t\t\tKey:             sshPub,\n\t\t\t\tSerial:          1,\n\t\t\t\tCertType:        1,\n\t\t\t\tKeyId:           \"foo\",\n\t\t\t\tValidPrincipals: []string{\"foo\"},\n\t\t\t\tValidAfter:      uint64(time.Now().Add(15 * time.Minute).Unix()),\n\t\t\t\tValidBefore:     uint64(time.Now().Add(10 * time.Minute).Unix()),\n\t\t\t},\n\t\t\terrors.New(\"ssh certificate validBefore cannot be before validAfter\"),\n\t\t},\n\t\t{\n\t\t\t\"fail/nil-signature-key\",\n\t\t\t&ssh.Certificate{\n\t\t\t\tNonce:           []byte(\"foo\"),\n\t\t\t\tKey:             sshPub,\n\t\t\t\tSerial:          1,\n\t\t\t\tCertType:        1,\n\t\t\t\tKeyId:           \"foo\",\n\t\t\t\tValidPrincipals: []string{\"foo\"},\n\t\t\t\tValidAfter:      uint64(time.Now().Unix()),\n\t\t\t\tValidBefore:     uint64(time.Now().Add(10 * time.Minute).Unix()),\n\t\t\t\tPermissions: ssh.Permissions{\n\t\t\t\t\tExtensions: map[string]string{\"foo\": \"bar\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\terrors.New(\"ssh certificate signature key cannot be nil\"),\n\t\t},\n\t\t{\n\t\t\t\"fail/nil-signature\",\n\t\t\t&ssh.Certificate{\n\t\t\t\tNonce:           []byte(\"foo\"),\n\t\t\t\tKey:             sshPub,\n\t\t\t\tSerial:          1,\n\t\t\t\tCertType:        1,\n\t\t\t\tKeyId:           \"foo\",\n\t\t\t\tValidPrincipals: []string{\"foo\"},\n\t\t\t\tValidAfter:      uint64(time.Now().Unix()),\n\t\t\t\tValidBefore:     uint64(time.Now().Add(10 * time.Minute).Unix()),\n\t\t\t\tPermissions: ssh.Permissions{\n\t\t\t\t\tExtensions: map[string]string{\"foo\": \"bar\"},\n\t\t\t\t},\n\t\t\t\tSignatureKey: sshPub,\n\t\t\t},\n\t\t\terrors.New(\"ssh certificate signature cannot be nil\"),\n\t\t},\n\t\t{\n\t\t\t\"ok/userCert\",\n\t\t\t&ssh.Certificate{\n\t\t\t\tNonce:           []byte(\"foo\"),\n\t\t\t\tKey:             sshPub,\n\t\t\t\tSerial:          1,\n\t\t\t\tCertType:        1,\n\t\t\t\tKeyId:           \"foo\",\n\t\t\t\tValidPrincipals: []string{\"foo\"},\n\t\t\t\tValidAfter:      uint64(time.Now().Unix()),\n\t\t\t\tValidBefore:     uint64(time.Now().Add(10 * time.Minute).Unix()),\n\t\t\t\tPermissions: ssh.Permissions{\n\t\t\t\t\tExtensions: map[string]string{\"foo\": \"bar\"},\n\t\t\t\t},\n\t\t\t\tSignatureKey: sshPub,\n\t\t\t\tSignature:    &ssh.Signature{},\n\t\t\t},\n\t\t\tnil,\n\t\t},\n\t\t{\n\t\t\t\"ok/hostCert\",\n\t\t\t&ssh.Certificate{\n\t\t\t\tNonce:           []byte(\"foo\"),\n\t\t\t\tKey:             sshPub,\n\t\t\t\tSerial:          1,\n\t\t\t\tCertType:        2,\n\t\t\t\tKeyId:           \"foo\",\n\t\t\t\tValidPrincipals: []string{\"foo\"},\n\t\t\t\tValidAfter:      uint64(time.Now().Unix()),\n\t\t\t\tValidBefore:     uint64(time.Now().Add(10 * time.Minute).Unix()),\n\t\t\t\tSignatureKey:    sshPub,\n\t\t\t\tSignature:       &ssh.Signature{},\n\t\t\t},\n\t\t\tnil,\n\t\t},\n\t\t{\n\t\t\t\"ok/emptyPrincipals\",\n\t\t\t&ssh.Certificate{\n\t\t\t\tNonce:           []byte(\"foo\"),\n\t\t\t\tKey:             sshPub,\n\t\t\t\tSerial:          1,\n\t\t\t\tCertType:        1,\n\t\t\t\tKeyId:           \"foo\",\n\t\t\t\tValidPrincipals: []string{},\n\t\t\t\tValidAfter:      uint64(time.Now().Unix()),\n\t\t\t\tValidBefore:     uint64(time.Now().Add(10 * time.Minute).Unix()),\n\t\t\t\tSignatureKey:    sshPub,\n\t\t\t\tSignature:       &ssh.Signature{},\n\t\t\t},\n\t\t\tnil,\n\t\t},\n\t\t{\n\t\t\t\"ok/empty-extensions\",\n\t\t\t&ssh.Certificate{\n\t\t\t\tNonce:           []byte(\"foo\"),\n\t\t\t\tKey:             sshPub,\n\t\t\t\tSerial:          1,\n\t\t\t\tCertType:        1,\n\t\t\t\tKeyId:           \"foo\",\n\t\t\t\tValidPrincipals: []string{},\n\t\t\t\tValidAfter:      uint64(time.Now().Unix()),\n\t\t\t\tValidBefore:     uint64(time.Now().Add(10 * time.Minute).Unix()),\n\t\t\t\tSignatureKey:    sshPub,\n\t\t\t\tSignature:       &ssh.Signature{},\n\t\t\t},\n\t\t\tnil,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif err := v.Valid(tt.cert, SignSSHOptions{}); err != nil {\n\t\t\t\tif assert.NotNil(t, tt.err) {\n\t\t\t\t\tassert.HasPrefix(t, err.Error(), tt.err.Error())\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tassert.Nil(t, tt.err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_sshCertValidityValidator(t *testing.T) {\n\tp, err := generateX5C(nil)\n\tassert.FatalError(t, err)\n\tv := sshCertValidityValidator{p.ctl.Claimer}\n\tn := now()\n\ttests := []struct {\n\t\tname string\n\t\tcert *ssh.Certificate\n\t\topts SignSSHOptions\n\t\terr  error\n\t}{\n\t\t{\n\t\t\t\"fail/validAfter-0\",\n\t\t\t&ssh.Certificate{CertType: ssh.UserCert},\n\t\t\tSignSSHOptions{},\n\t\t\terrors.New(\"ssh certificate validAfter cannot be 0\"),\n\t\t},\n\t\t{\n\t\t\t\"fail/validBefore-in-past\",\n\t\t\t&ssh.Certificate{CertType: ssh.UserCert, ValidAfter: uint64(now().Unix()), ValidBefore: uint64(now().Add(-time.Minute).Unix())},\n\t\t\tSignSSHOptions{},\n\t\t\terrors.New(\"ssh certificate validBefore cannot be in the past\"),\n\t\t},\n\t\t{\n\t\t\t\"fail/validBefore-before-validAfter\",\n\t\t\t&ssh.Certificate{CertType: ssh.UserCert, ValidAfter: uint64(now().Add(5 * time.Minute).Unix()), ValidBefore: uint64(now().Add(3 * time.Minute).Unix())},\n\t\t\tSignSSHOptions{},\n\t\t\terrors.New(\"ssh certificate validBefore cannot be before validAfter\"),\n\t\t},\n\t\t{\n\t\t\t\"fail/cert-type-not-set\",\n\t\t\t&ssh.Certificate{ValidAfter: uint64(now().Unix()), ValidBefore: uint64(now().Add(10 * time.Minute).Unix())},\n\t\t\tSignSSHOptions{},\n\t\t\terrors.New(\"ssh certificate type has not been set\"),\n\t\t},\n\t\t{\n\t\t\t\"fail/unexpected-cert-type\",\n\t\t\t&ssh.Certificate{\n\t\t\t\tCertType:    3,\n\t\t\t\tValidAfter:  uint64(now().Unix()),\n\t\t\t\tValidBefore: uint64(now().Add(10 * time.Minute).Unix()),\n\t\t\t},\n\t\t\tSignSSHOptions{},\n\t\t\terrors.New(\"ssh certificate has an unknown type '3'\"),\n\t\t},\n\t\t{\n\t\t\t\"fail/duration<min\",\n\t\t\t&ssh.Certificate{\n\t\t\t\tCertType:    1,\n\t\t\t\tValidAfter:  uint64(n.Unix()),\n\t\t\t\tValidBefore: uint64(n.Add(4 * time.Minute).Unix()),\n\t\t\t},\n\t\t\tSignSSHOptions{Backdate: time.Second},\n\t\t\terrors.New(\"requested duration of 4m0s is less than minimum accepted duration for selected provisioner of 5m0s\"),\n\t\t},\n\t\t{\n\t\t\t\"ok/duration-exactly-min\",\n\t\t\t&ssh.Certificate{\n\t\t\t\tCertType:    1,\n\t\t\t\tValidAfter:  uint64(n.Unix()),\n\t\t\t\tValidBefore: uint64(n.Add(5 * time.Minute).Unix()),\n\t\t\t},\n\t\t\tSignSSHOptions{Backdate: time.Second},\n\t\t\tnil,\n\t\t},\n\t\t{\n\t\t\t\"fail/duration>max\",\n\t\t\t&ssh.Certificate{\n\t\t\t\tCertType:    1,\n\t\t\t\tValidAfter:  uint64(n.Unix()),\n\t\t\t\tValidBefore: uint64(n.Add(48 * time.Hour).Unix()),\n\t\t\t},\n\t\t\tSignSSHOptions{Backdate: time.Second},\n\t\t\terrors.New(\"requested duration of 48h0m0s is greater than maximum accepted duration for selected provisioner of 24h0m1s\"),\n\t\t},\n\t\t{\n\t\t\t\"ok/duration-exactly-max\",\n\t\t\t&ssh.Certificate{\n\t\t\t\tCertType:    1,\n\t\t\t\tValidAfter:  uint64(n.Unix()),\n\t\t\t\tValidBefore: uint64(n.Add(24*time.Hour + time.Second).Unix()),\n\t\t\t},\n\t\t\tSignSSHOptions{Backdate: time.Second},\n\t\t\tnil,\n\t\t},\n\t\t{\n\t\t\t\"ok\",\n\t\t\t&ssh.Certificate{\n\t\t\t\tCertType:    1,\n\t\t\t\tValidAfter:  uint64(now().Unix()),\n\t\t\t\tValidBefore: uint64(now().Add(8 * time.Hour).Unix()),\n\t\t\t},\n\t\t\tSignSSHOptions{Backdate: time.Second},\n\t\t\tnil,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif err := v.Valid(tt.cert, tt.opts); err != nil {\n\t\t\t\tif assert.NotNil(t, tt.err) {\n\t\t\t\t\tassert.HasPrefix(t, err.Error(), tt.err.Error())\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tassert.Nil(t, tt.err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_sshValidityModifier(t *testing.T) {\n\tn, fn := mockNow()\n\tdefer fn()\n\n\tp, err := generateX5C(nil)\n\tassert.FatalError(t, err)\n\ttype test struct {\n\t\tsvm   *sshLimitDuration\n\t\tcert  *ssh.Certificate\n\t\tvalid func(*ssh.Certificate)\n\t\terr   error\n\t}\n\ttests := map[string]func() test{\n\t\t\"fail/type-not-set\": func() test {\n\t\t\treturn test{\n\t\t\t\tsvm: &sshLimitDuration{Claimer: p.ctl.Claimer, NotAfter: n.Add(6 * time.Hour)},\n\t\t\t\tcert: &ssh.Certificate{\n\t\t\t\t\tValidAfter:  uint64(n.Unix()),\n\t\t\t\t\tValidBefore: uint64(n.Add(8 * time.Hour).Unix()),\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"ssh certificate type has not been set\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/type-not-recognized\": func() test {\n\t\t\treturn test{\n\t\t\t\tsvm: &sshLimitDuration{Claimer: p.ctl.Claimer, NotAfter: n.Add(6 * time.Hour)},\n\t\t\t\tcert: &ssh.Certificate{\n\t\t\t\t\tCertType:    4,\n\t\t\t\t\tValidAfter:  uint64(n.Unix()),\n\t\t\t\t\tValidBefore: uint64(n.Add(8 * time.Hour).Unix()),\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"ssh certificate has an unknown type: 4\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/requested-validAfter-after-limit\": func() test {\n\t\t\treturn test{\n\t\t\t\tsvm: &sshLimitDuration{Claimer: p.ctl.Claimer, NotAfter: n.Add(1 * time.Hour)},\n\t\t\t\tcert: &ssh.Certificate{\n\t\t\t\t\tCertType:    1,\n\t\t\t\t\tValidAfter:  uint64(n.Add(2 * time.Hour).Unix()),\n\t\t\t\t\tValidBefore: uint64(n.Add(8 * time.Hour).Unix()),\n\t\t\t\t},\n\t\t\t\terr: errors.Errorf(\"provisioning credential expiration (\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/requested-validBefore-after-limit\": func() test {\n\t\t\treturn test{\n\t\t\t\tsvm: &sshLimitDuration{Claimer: p.ctl.Claimer, NotAfter: n.Add(1 * time.Hour)},\n\t\t\t\tcert: &ssh.Certificate{\n\t\t\t\t\tCertType:    1,\n\t\t\t\t\tValidAfter:  uint64(n.Unix()),\n\t\t\t\t\tValidBefore: uint64(n.Add(2 * time.Hour).Unix()),\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"provisioning credential expiration (\"),\n\t\t\t}\n\t\t},\n\t\t\"ok/no-limit\": func() test {\n\t\t\tva, vb := uint64(n.Unix()), uint64(n.Add(16*time.Hour).Unix())\n\t\t\treturn test{\n\t\t\t\tsvm: &sshLimitDuration{Claimer: p.ctl.Claimer},\n\t\t\t\tcert: &ssh.Certificate{\n\t\t\t\t\tCertType: 1,\n\t\t\t\t},\n\t\t\t\tvalid: func(cert *ssh.Certificate) {\n\t\t\t\t\tassert.Equals(t, cert.ValidAfter, va)\n\t\t\t\t\tassert.Equals(t, cert.ValidBefore, vb)\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"ok/defaults\": func() test {\n\t\t\tva, vb := uint64(n.Unix()), uint64(n.Add(16*time.Hour).Unix())\n\t\t\treturn test{\n\t\t\t\tsvm: &sshLimitDuration{Claimer: p.ctl.Claimer},\n\t\t\t\tcert: &ssh.Certificate{\n\t\t\t\t\tCertType: 1,\n\t\t\t\t},\n\t\t\t\tvalid: func(cert *ssh.Certificate) {\n\t\t\t\t\tassert.Equals(t, cert.ValidAfter, va)\n\t\t\t\t\tassert.Equals(t, cert.ValidBefore, vb)\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"ok/valid-requested-validBefore\": func() test {\n\t\t\tva, vb := uint64(n.Unix()), uint64(n.Add(2*time.Hour).Unix())\n\t\t\treturn test{\n\t\t\t\tsvm: &sshLimitDuration{Claimer: p.ctl.Claimer, NotAfter: n.Add(3 * time.Hour)},\n\t\t\t\tcert: &ssh.Certificate{\n\t\t\t\t\tCertType:    1,\n\t\t\t\t\tValidAfter:  va,\n\t\t\t\t\tValidBefore: vb,\n\t\t\t\t},\n\t\t\t\tvalid: func(cert *ssh.Certificate) {\n\t\t\t\t\tassert.Equals(t, cert.ValidAfter, va)\n\t\t\t\t\tassert.Equals(t, cert.ValidBefore, vb)\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"ok/empty-requested-validBefore-limit-after-default\": func() test {\n\t\t\tva := uint64(n.Unix())\n\t\t\treturn test{\n\t\t\t\tsvm: &sshLimitDuration{Claimer: p.ctl.Claimer, NotAfter: n.Add(24 * time.Hour)},\n\t\t\t\tcert: &ssh.Certificate{\n\t\t\t\t\tCertType:   1,\n\t\t\t\t\tValidAfter: va,\n\t\t\t\t},\n\t\t\t\tvalid: func(cert *ssh.Certificate) {\n\t\t\t\t\tassert.Equals(t, cert.ValidAfter, va)\n\t\t\t\t\tassert.Equals(t, cert.ValidBefore, uint64(n.Add(16*time.Hour).Unix()))\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"ok/empty-requested-validBefore-limit-before-default\": func() test {\n\t\t\tva := uint64(n.Unix())\n\t\t\treturn test{\n\t\t\t\tsvm: &sshLimitDuration{Claimer: p.ctl.Claimer, NotAfter: n.Add(3 * time.Hour)},\n\t\t\t\tcert: &ssh.Certificate{\n\t\t\t\t\tCertType:   1,\n\t\t\t\t\tValidAfter: va,\n\t\t\t\t},\n\t\t\t\tvalid: func(cert *ssh.Certificate) {\n\t\t\t\t\tassert.Equals(t, cert.ValidAfter, va)\n\t\t\t\t\tassert.Equals(t, cert.ValidBefore, uint64(n.Add(3*time.Hour).Unix()))\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ttt := run()\n\t\t\tif err := tt.svm.Modify(tt.cert, SignSSHOptions{}); err != nil {\n\t\t\t\tif assert.NotNil(t, tt.err) {\n\t\t\t\t\tassert.HasPrefix(t, err.Error(), tt.err.Error())\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif assert.Nil(t, tt.err) {\n\t\t\t\t\ttt.valid(tt.cert)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_sshDefaultDuration_Option(t *testing.T) {\n\ttm, fn := mockNow()\n\tdefer fn()\n\n\tnewClaimer := func(claims *Claims) *Claimer {\n\t\tc, err := NewClaimer(claims, globalProvisionerClaims)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\treturn c\n\t}\n\tunix := func(d time.Duration) uint64 {\n\t\treturn uint64(tm.Add(d).Unix())\n\t}\n\n\ttype fields struct {\n\t\tClaimer *Claimer\n\t}\n\ttype args struct {\n\t\to    SignSSHOptions\n\t\tcert *ssh.Certificate\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tfields  fields\n\t\targs    args\n\t\twant    *ssh.Certificate\n\t\twantErr bool\n\t}{\n\t\t{\"user\", fields{newClaimer(nil)}, args{SignSSHOptions{}, &ssh.Certificate{CertType: ssh.UserCert}},\n\t\t\t&ssh.Certificate{CertType: ssh.UserCert, ValidAfter: unix(0), ValidBefore: unix(16 * time.Hour)}, false},\n\t\t{\"host\", fields{newClaimer(nil)}, args{SignSSHOptions{}, &ssh.Certificate{CertType: ssh.HostCert}},\n\t\t\t&ssh.Certificate{CertType: ssh.HostCert, ValidAfter: unix(0), ValidBefore: unix(30 * 24 * time.Hour)}, false},\n\t\t{\"user claim\", fields{newClaimer(&Claims{DefaultUserSSHDur: &Duration{1 * time.Hour}})}, args{SignSSHOptions{}, &ssh.Certificate{CertType: ssh.UserCert}},\n\t\t\t&ssh.Certificate{CertType: ssh.UserCert, ValidAfter: unix(0), ValidBefore: unix(1 * time.Hour)}, false},\n\t\t{\"host claim\", fields{newClaimer(&Claims{DefaultHostSSHDur: &Duration{1 * time.Hour}})}, args{SignSSHOptions{}, &ssh.Certificate{CertType: ssh.HostCert}},\n\t\t\t&ssh.Certificate{CertType: ssh.HostCert, ValidAfter: unix(0), ValidBefore: unix(1 * time.Hour)}, false},\n\t\t{\"user backdate\", fields{newClaimer(nil)}, args{SignSSHOptions{Backdate: 1 * time.Minute}, &ssh.Certificate{CertType: ssh.UserCert}},\n\t\t\t&ssh.Certificate{CertType: ssh.UserCert, ValidAfter: unix(-1 * time.Minute), ValidBefore: unix(16 * time.Hour)}, false},\n\t\t{\"host backdate\", fields{newClaimer(nil)}, args{SignSSHOptions{Backdate: 1 * time.Minute}, &ssh.Certificate{CertType: ssh.HostCert}},\n\t\t\t&ssh.Certificate{CertType: ssh.HostCert, ValidAfter: unix(-1 * time.Minute), ValidBefore: unix(30 * 24 * time.Hour)}, false},\n\t\t{\"user validAfter\", fields{newClaimer(nil)}, args{SignSSHOptions{Backdate: 1 * time.Minute}, &ssh.Certificate{CertType: ssh.UserCert, ValidAfter: unix(1 * time.Hour)}},\n\t\t\t&ssh.Certificate{CertType: ssh.UserCert, ValidAfter: unix(time.Hour), ValidBefore: unix(17 * time.Hour)}, false},\n\t\t{\"user validBefore\", fields{newClaimer(nil)}, args{SignSSHOptions{Backdate: 1 * time.Minute}, &ssh.Certificate{CertType: ssh.UserCert, ValidBefore: unix(1 * time.Hour)}},\n\t\t\t&ssh.Certificate{CertType: ssh.UserCert, ValidAfter: unix(-1 * time.Minute), ValidBefore: unix(time.Hour)}, false},\n\t\t{\"host validAfter validBefore\", fields{newClaimer(nil)}, args{SignSSHOptions{Backdate: 1 * time.Minute}, &ssh.Certificate{CertType: ssh.HostCert, ValidAfter: unix(1 * time.Minute), ValidBefore: unix(2 * time.Minute)}},\n\t\t\t&ssh.Certificate{CertType: ssh.HostCert, ValidAfter: unix(1 * time.Minute), ValidBefore: unix(2 * time.Minute)}, false},\n\t\t{\"fail zero\", fields{newClaimer(nil)}, args{SignSSHOptions{}, &ssh.Certificate{}}, &ssh.Certificate{}, true},\n\t\t{\"fail type\", fields{newClaimer(nil)}, args{SignSSHOptions{}, &ssh.Certificate{CertType: 3}}, &ssh.Certificate{CertType: 3}, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tm := &sshDefaultDuration{\n\t\t\t\tClaimer: tt.fields.Claimer,\n\t\t\t}\n\t\t\tif err := m.Modify(tt.args.cert, tt.args.o); (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"sshDefaultDuration.Option() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(tt.args.cert, tt.want) {\n\t\t\t\tt.Errorf(\"sshDefaultDuration.Option() = %v, want %v\", tt.args.cert, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "authority/provisioner/ssh_options.go",
    "content": "package provisioner\n\nimport (\n\t\"encoding/json\"\n\t\"strings\"\n\n\t\"github.com/pkg/errors\"\n\n\t\"github.com/smallstep/cli-utils/step\"\n\t\"go.step.sm/crypto/sshutil\"\n\n\t\"github.com/smallstep/certificates/authority/policy\"\n)\n\n// SSHCertificateOptions is an interface that returns a list of options passed when\n// creating a new certificate.\ntype SSHCertificateOptions interface {\n\tOptions(SignSSHOptions) []sshutil.Option\n}\n\ntype sshCertificateOptionsFunc func(SignSSHOptions) []sshutil.Option\n\nfunc (fn sshCertificateOptionsFunc) Options(so SignSSHOptions) []sshutil.Option {\n\treturn fn(so)\n}\n\n// SSHOptions are a collection of custom options that can be added to each\n// provisioner.\ntype SSHOptions struct {\n\t// Template contains an SSH certificate template. It can be a JSON template\n\t// escaped in a string or it can be also encoded in base64.\n\tTemplate string `json:\"template,omitempty\"`\n\n\t// TemplateFile points to a file containing a SSH certificate template.\n\tTemplateFile string `json:\"templateFile,omitempty\"`\n\n\t// TemplateData is a JSON object with variables that can be used in custom\n\t// templates.\n\tTemplateData json.RawMessage `json:\"templateData,omitempty\"`\n\n\t// User contains SSH user certificate options.\n\tUser *policy.SSHUserCertificateOptions `json:\"-\"`\n\n\t// Host contains SSH host certificate options.\n\tHost *policy.SSHHostCertificateOptions `json:\"-\"`\n}\n\n// GetAllowedUserNameOptions returns the SSHNameOptions that are\n// allowed when SSH User certificates are requested.\nfunc (o *SSHOptions) GetAllowedUserNameOptions() *policy.SSHNameOptions {\n\tif o == nil {\n\t\treturn nil\n\t}\n\tif o.User == nil {\n\t\treturn nil\n\t}\n\treturn o.User.AllowedNames\n}\n\n// GetDeniedUserNameOptions returns the SSHNameOptions that are\n// denied when SSH user certificates are requested.\nfunc (o *SSHOptions) GetDeniedUserNameOptions() *policy.SSHNameOptions {\n\tif o == nil {\n\t\treturn nil\n\t}\n\tif o.User == nil {\n\t\treturn nil\n\t}\n\treturn o.User.DeniedNames\n}\n\n// GetAllowedHostNameOptions returns the SSHNameOptions that are\n// allowed when SSH host certificates are requested.\nfunc (o *SSHOptions) GetAllowedHostNameOptions() *policy.SSHNameOptions {\n\tif o == nil {\n\t\treturn nil\n\t}\n\tif o.Host == nil {\n\t\treturn nil\n\t}\n\treturn o.Host.AllowedNames\n}\n\n// GetDeniedHostNameOptions returns the SSHNameOptions that are\n// denied when SSH host certificates are requested.\nfunc (o *SSHOptions) GetDeniedHostNameOptions() *policy.SSHNameOptions {\n\tif o == nil {\n\t\treturn nil\n\t}\n\tif o.Host == nil {\n\t\treturn nil\n\t}\n\treturn o.Host.DeniedNames\n}\n\n// HasTemplate returns true if a template is defined in the provisioner options.\nfunc (o *SSHOptions) HasTemplate() bool {\n\treturn o != nil && (o.Template != \"\" || o.TemplateFile != \"\")\n}\n\n// TemplateSSHOptions generates a SSHCertificateOptions with the template and\n// data defined in the ProvisionerOptions, the provisioner generated data, and\n// the user data provided in the request. If no template has been provided,\n// x509util.DefaultLeafTemplate will be used.\nfunc TemplateSSHOptions(o *Options, data sshutil.TemplateData) (SSHCertificateOptions, error) {\n\treturn CustomSSHTemplateOptions(o, data, sshutil.DefaultTemplate)\n}\n\n// CustomSSHTemplateOptions generates a CertificateOptions with the template, data\n// defined in the ProvisionerOptions, the provisioner generated data and the\n// user data provided in the request. If no template has been provided in the\n// ProvisionerOptions, the given template will be used.\nfunc CustomSSHTemplateOptions(o *Options, data sshutil.TemplateData, defaultTemplate string) (SSHCertificateOptions, error) {\n\topts := o.GetSSHOptions()\n\tif data == nil {\n\t\tdata = sshutil.NewTemplateData()\n\t}\n\n\tif opts != nil {\n\t\t// Add template data if any.\n\t\tif len(opts.TemplateData) > 0 && string(opts.TemplateData) != \"null\" {\n\t\t\tif err := json.Unmarshal(opts.TemplateData, &data); err != nil {\n\t\t\t\treturn nil, errors.Wrap(err, \"error unmarshaling template data\")\n\t\t\t}\n\t\t}\n\t}\n\n\treturn sshCertificateOptionsFunc(func(so SignSSHOptions) []sshutil.Option {\n\t\t// We're not provided user data without custom templates.\n\t\tif !opts.HasTemplate() {\n\t\t\treturn []sshutil.Option{\n\t\t\t\tsshutil.WithTemplate(defaultTemplate, data),\n\t\t\t}\n\t\t}\n\n\t\t// Add user provided data.\n\t\tif len(so.TemplateData) > 0 {\n\t\t\tuserObject := make(map[string]interface{})\n\t\t\tif err := json.Unmarshal(so.TemplateData, &userObject); err != nil {\n\t\t\t\tdata.SetUserData(map[string]interface{}{})\n\t\t\t} else {\n\t\t\t\tdata.SetUserData(userObject)\n\t\t\t}\n\t\t}\n\n\t\t// Load a template from a file if Template is not defined.\n\t\tif opts.Template == \"\" && opts.TemplateFile != \"\" {\n\t\t\treturn []sshutil.Option{\n\t\t\t\tsshutil.WithTemplateFile(step.Abs(opts.TemplateFile), data),\n\t\t\t}\n\t\t}\n\n\t\t// Load a template from the Template fields\n\t\t// 1. As a JSON in a string.\n\t\ttemplate := strings.TrimSpace(opts.Template)\n\t\tif strings.HasPrefix(template, \"{\") {\n\t\t\treturn []sshutil.Option{\n\t\t\t\tsshutil.WithTemplate(template, data),\n\t\t\t}\n\t\t}\n\t\t// 2. As a base64 encoded JSON.\n\t\treturn []sshutil.Option{\n\t\t\tsshutil.WithTemplateBase64(template, data),\n\t\t}\n\t}), nil\n}\n"
  },
  {
    "path": "authority/provisioner/ssh_options_test.go",
    "content": "package provisioner\n\nimport (\n\t\"bytes\"\n\t\"reflect\"\n\t\"testing\"\n\n\t\"go.step.sm/crypto/sshutil\"\n)\n\nfunc TestCustomSSHTemplateOptions(t *testing.T) {\n\tcr := sshutil.CertificateRequest{\n\t\tType:       \"user\",\n\t\tKeyID:      \"foo@smallstep.com\",\n\t\tPrincipals: []string{\"foo\"},\n\t}\n\tcrCertificate := `{\"Key\":null,\"Type\":\"user\",\"KeyID\":\"foo@smallstep.com\",\"Principals\":[\"foo\"]}`\n\tdata := sshutil.CreateTemplateData(sshutil.HostCert, \"smallstep.com\", []string{\"smallstep.com\"})\n\ttype args struct {\n\t\to               *Options\n\t\tdata            sshutil.TemplateData\n\t\tdefaultTemplate string\n\t\tuserOptions     SignSSHOptions\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\targs    args\n\t\twant    sshutil.Options\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", args{nil, data, sshutil.DefaultTemplate, SignSSHOptions{}}, sshutil.Options{\n\t\t\tCertBuffer: bytes.NewBufferString(`{\n\t\"type\": \"host\",\n\t\"keyId\": \"smallstep.com\",\n\t\"principals\": [\"smallstep.com\"],\n\t\"extensions\": null,\n\t\"criticalOptions\": null\n}`),\n\t\t}, false},\n\t\t{\"okNoData\", args{nil, nil, sshutil.DefaultTemplate, SignSSHOptions{}}, sshutil.Options{\n\t\t\tCertBuffer: bytes.NewBufferString(`{\n\t\"type\": null,\n\t\"keyId\": null,\n\t\"principals\": null,\n\t\"extensions\": null,\n\t\"criticalOptions\": null\n}`),\n\t\t}, false},\n\t\t{\"okTemplateData\", args{&Options{SSH: &SSHOptions{TemplateData: []byte(`{\"foo\":\"bar\"}`)}}, data, sshutil.DefaultTemplate, SignSSHOptions{}}, sshutil.Options{\n\t\t\tCertBuffer: bytes.NewBufferString(`{\n\t\"type\": \"host\",\n\t\"keyId\": \"smallstep.com\",\n\t\"principals\": [\"smallstep.com\"],\n\t\"extensions\": null,\n\t\"criticalOptions\": null\n}`),\n\t\t}, false},\n\t\t{\"okNullTemplateData\", args{&Options{SSH: &SSHOptions{TemplateData: []byte(`null`)}}, data, sshutil.DefaultTemplate, SignSSHOptions{}}, sshutil.Options{\n\t\t\tCertBuffer: bytes.NewBufferString(`{\n\t\"type\": \"host\",\n\t\"keyId\": \"smallstep.com\",\n\t\"principals\": [\"smallstep.com\"],\n\t\"extensions\": null,\n\t\"criticalOptions\": null\n}`),\n\t\t}, false},\n\t\t// Note: `{{ toJson .Insecure.CR }}` is not a valid ssh template\n\t\t{\"okTemplate\", args{&Options{SSH: &SSHOptions{Template: \"{{ toJson .Insecure.CR }}\"}}, data, sshutil.DefaultTemplate, SignSSHOptions{}}, sshutil.Options{\n\t\t\tCertBuffer: bytes.NewBufferString(crCertificate)}, false},\n\t\t{\"okFile\", args{&Options{SSH: &SSHOptions{TemplateFile: \"./testdata/templates/cr.tpl\"}}, data, sshutil.DefaultTemplate, SignSSHOptions{}}, sshutil.Options{\n\t\t\tCertBuffer: bytes.NewBufferString(crCertificate)}, false},\n\t\t{\"okBase64\", args{&Options{SSH: &SSHOptions{Template: \"e3sgdG9Kc29uIC5JbnNlY3VyZS5DUiB9fQ==\"}}, data, sshutil.DefaultTemplate, SignSSHOptions{}}, sshutil.Options{\n\t\t\tCertBuffer: bytes.NewBufferString(crCertificate)}, false},\n\t\t{\"okUserOptions\", args{&Options{SSH: &SSHOptions{Template: `{\"foo\": \"{{.Insecure.User.foo}}\"}`}}, data, sshutil.DefaultTemplate, SignSSHOptions{TemplateData: []byte(`{\"foo\":\"bar\"}`)}}, sshutil.Options{\n\t\t\tCertBuffer: bytes.NewBufferString(`{\"foo\": \"bar\"}`),\n\t\t}, false},\n\t\t{\"okNulUserOptions\", args{&Options{SSH: &SSHOptions{Template: `{\"foo\": \"{{.Insecure.User.foo}}\"}`}}, data, sshutil.DefaultTemplate, SignSSHOptions{TemplateData: []byte(`null`)}}, sshutil.Options{\n\t\t\tCertBuffer: bytes.NewBufferString(`{\"foo\": \"<no value>\"}`),\n\t\t}, false},\n\t\t{\"okBadUserOptions\", args{&Options{SSH: &SSHOptions{Template: `{\"foo\": \"{{.Insecure.User.foo}}\"}`}}, data, sshutil.DefaultTemplate, SignSSHOptions{TemplateData: []byte(`{\"badJSON\"}`)}}, sshutil.Options{\n\t\t\tCertBuffer: bytes.NewBufferString(`{\"foo\": \"<no value>\"}`),\n\t\t}, false},\n\t\t{\"fail\", args{&Options{SSH: &SSHOptions{TemplateData: []byte(`{\"badJSON`)}}, data, sshutil.DefaultTemplate, SignSSHOptions{}}, sshutil.Options{}, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tcof, err := CustomSSHTemplateOptions(tt.args.o, tt.args.data, tt.args.defaultTemplate)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"CustomSSHTemplateOptions() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tvar opts sshutil.Options\n\t\t\tif cof != nil {\n\t\t\t\tfor _, fn := range cof.Options(tt.args.userOptions) {\n\t\t\t\t\tif err := fn(cr, &opts); err != nil {\n\t\t\t\t\t\tt.Errorf(\"x509util.Options() error = %v\", err)\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(opts, tt.want) {\n\t\t\t\tt.Errorf(\"CustomSSHTemplateOptions() = %v, want %v\", opts, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "authority/provisioner/ssh_test.go",
    "content": "package provisioner\n\nimport (\n\t\"crypto\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"reflect\"\n\t\"time\"\n\n\t\"golang.org/x/crypto/ssh\"\n\n\t\"go.step.sm/crypto/sshutil\"\n\n\t\"github.com/smallstep/certificates/errs\"\n\t\"github.com/smallstep/certificates/internal/cast\"\n)\n\nfunc validateSSHCertificate(cert *ssh.Certificate, opts *SignSSHOptions) error {\n\tswitch {\n\tcase cert == nil:\n\t\treturn fmt.Errorf(\"certificate is nil\")\n\tcase cert.Signature == nil:\n\t\treturn fmt.Errorf(\"certificate signature is nil\")\n\tcase cert.SignatureKey == nil:\n\t\treturn fmt.Errorf(\"certificate signature is nil\")\n\tcase !reflect.DeepEqual(cert.ValidPrincipals, opts.Principals) && (len(opts.Principals) > 0 || len(cert.ValidPrincipals) > 0):\n\t\treturn fmt.Errorf(\"certificate principals are not equal, want %v, got %v\", opts.Principals, cert.ValidPrincipals)\n\tcase cert.CertType != ssh.UserCert && cert.CertType != ssh.HostCert:\n\t\treturn fmt.Errorf(\"certificate type %v is not valid\", cert.CertType)\n\tcase opts.CertType == \"user\" && cert.CertType != ssh.UserCert:\n\t\treturn fmt.Errorf(\"certificate type is not valid, want %v, got %v\", ssh.UserCert, cert.CertType)\n\tcase opts.CertType == \"host\" && cert.CertType != ssh.HostCert:\n\t\treturn fmt.Errorf(\"certificate type is not valid, want %v, got %v\", ssh.HostCert, cert.CertType)\n\tcase cert.ValidAfter != uint64(opts.ValidAfter.Unix()):\n\t\treturn fmt.Errorf(\"certificate valid after is not valid, want %v, got %v\", opts.ValidAfter.Unix(), time.Unix(cast.Int64(cert.ValidAfter), 0))\n\tcase cert.ValidBefore != uint64(opts.ValidBefore.Unix()):\n\t\treturn fmt.Errorf(\"certificate valid after is not valid, want %v, got %v\", opts.ValidAfter.Unix(), time.Unix(cast.Int64(cert.ValidAfter), 0))\n\tcase opts.CertType == \"user\" && len(cert.Extensions) != 5:\n\t\treturn fmt.Errorf(\"certificate extensions number is invalid, want 5, got %d\", len(cert.Extensions))\n\tcase opts.CertType == \"host\" && len(cert.Extensions) != 0:\n\t\treturn fmt.Errorf(\"certificate extensions number is invalid, want 0, got %d\", len(cert.Extensions))\n\tdefault:\n\t\treturn nil\n\t}\n}\n\nfunc signSSHCertificate(key crypto.PublicKey, opts SignSSHOptions, signOpts []SignOption, signKey crypto.Signer) (*ssh.Certificate, error) {\n\tpub, err := ssh.NewPublicKey(key)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar mods []SSHCertModifier\n\tvar certOptions []sshutil.Option\n\tvar validators []SSHCertValidator\n\n\tfor _, op := range signOpts {\n\t\tswitch o := op.(type) {\n\t\tcase Interface:\n\t\t// add options to NewCertificate\n\t\tcase SSHCertificateOptions:\n\t\t\tcertOptions = append(certOptions, o.Options(opts)...)\n\t\t// modify the ssh.Certificate\n\t\tcase SSHCertModifier:\n\t\t\tmods = append(mods, o)\n\t\t// validate the ssh.Certificate\n\t\tcase SSHCertValidator:\n\t\t\tvalidators = append(validators, o)\n\t\t// validate the given SSHOptions\n\t\tcase SSHCertOptionsValidator:\n\t\t\tif err := o.Valid(opts); err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t// call webhooks\n\t\tcase *WebhookController:\n\t\tdefault:\n\t\t\treturn nil, fmt.Errorf(\"signSSH: invalid extra option type %T\", o)\n\t\t}\n\t}\n\n\t// Simulated certificate request with request options.\n\tcr := sshutil.CertificateRequest{\n\t\tType:       opts.CertType,\n\t\tKeyID:      opts.KeyID,\n\t\tPrincipals: opts.Principals,\n\t\tKey:        pub,\n\t}\n\n\t// Create certificate from template.\n\tcertificate, err := sshutil.NewCertificate(cr, certOptions...)\n\tif err != nil {\n\t\tvar templErr *sshutil.TemplateError\n\t\tif errors.As(err, &templErr) {\n\t\t\treturn nil, errs.NewErr(http.StatusBadRequest, templErr,\n\t\t\t\terrs.WithMessage(\"%s\", templErr.Error()),\n\t\t\t\terrs.WithKeyVal(\"signOptions\", signOpts),\n\t\t\t)\n\t\t}\n\t\treturn nil, errs.Wrap(http.StatusInternalServerError, err, \"authority.SignSSH\")\n\t}\n\n\t// Get actual *ssh.Certificate and continue with provisioner modifiers.\n\tcert := certificate.GetCertificate()\n\n\t// Use SignSSHOptions to modify the certificate validity. It will be later\n\t// checked or set if not defined.\n\tif err := opts.ModifyValidity(cert); err != nil {\n\t\treturn nil, errs.Wrap(http.StatusBadRequest, err, \"authority.SignSSH\")\n\t}\n\n\t// Use provisioner modifiers.\n\tfor _, m := range mods {\n\t\tif err := m.Modify(cert, opts); err != nil {\n\t\t\treturn nil, errs.Wrap(http.StatusForbidden, err, \"authority.SignSSH\")\n\t\t}\n\t}\n\n\t// Get signer from authority keys\n\tvar signer ssh.Signer\n\tswitch cert.CertType {\n\tcase ssh.UserCert:\n\t\tsigner, err = ssh.NewSignerFromSigner(signKey)\n\tcase ssh.HostCert:\n\t\tsigner, err = ssh.NewSignerFromSigner(signKey)\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unexpected ssh certificate type: %d\", cert.CertType)\n\t}\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Sign certificate.\n\tcert, err = sshutil.CreateCertificate(cert, signer)\n\tif err != nil {\n\t\treturn nil, errs.Wrap(http.StatusInternalServerError, err, \"authority.SignSSH: error signing certificate\")\n\t}\n\n\t// User provisioners validators.\n\tfor _, v := range validators {\n\t\tif err := v.Valid(cert, opts); err != nil {\n\t\t\treturn nil, errs.Wrap(http.StatusForbidden, err, \"authority.SignSSH\")\n\t\t}\n\t}\n\n\treturn cert, nil\n}\n"
  },
  {
    "path": "authority/provisioner/sshpop.go",
    "content": "package provisioner\n\nimport (\n\t\"context\"\n\t\"encoding/base64\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n\t\"golang.org/x/crypto/ssh\"\n\n\t\"go.step.sm/crypto/jose\"\n\n\t\"github.com/smallstep/certificates/errs\"\n\t\"github.com/smallstep/certificates/internal/cast\"\n)\n\n// sshPOPPayload extends jwt.Claims with step attributes.\ntype sshPOPPayload struct {\n\tjose.Claims\n\tSANs    []string     `json:\"sans,omitempty\"`\n\tStep    *stepPayload `json:\"step,omitempty\"`\n\tsshCert *ssh.Certificate\n}\n\n// SSHPOP is the default provisioner, an entity that can sign tokens necessary for\n// signature requests.\ntype SSHPOP struct {\n\t*base\n\tID         string  `json:\"-\"`\n\tType       string  `json:\"type\"`\n\tName       string  `json:\"name\"`\n\tClaims     *Claims `json:\"claims,omitempty\"`\n\tctl        *Controller\n\tsshPubKeys *SSHKeys\n}\n\n// GetID returns the provisioner unique identifier. The name and credential id\n// should uniquely identify any SSH-POP provisioner.\nfunc (p *SSHPOP) GetID() string {\n\tif p.ID != \"\" {\n\t\treturn p.ID\n\t}\n\treturn p.GetIDForToken()\n}\n\n// GetIDForToken returns an identifier that will be used to load the provisioner\n// from a token.\nfunc (p *SSHPOP) GetIDForToken() string {\n\treturn \"sshpop/\" + p.Name\n}\n\n// GetTokenID returns the identifier of the token.\nfunc (p *SSHPOP) GetTokenID(ott string) (string, error) {\n\t// Validate payload\n\ttoken, err := jose.ParseSigned(ott)\n\tif err != nil {\n\t\treturn \"\", errors.Wrap(err, \"error parsing token\")\n\t}\n\n\t// Get claims w/out verification. We need to look up the provisioner\n\t// key in order to verify the claims and we need the issuer from the claims\n\t// before we can look up the provisioner.\n\tvar claims jose.Claims\n\tif err = token.UnsafeClaimsWithoutVerification(&claims); err != nil {\n\t\treturn \"\", errors.Wrap(err, \"error verifying claims\")\n\t}\n\treturn claims.ID, nil\n}\n\n// GetName returns the name of the provisioner.\nfunc (p *SSHPOP) GetName() string {\n\treturn p.Name\n}\n\n// GetType returns the type of provisioner.\nfunc (p *SSHPOP) GetType() Type {\n\treturn TypeSSHPOP\n}\n\n// GetEncryptedKey returns the base provisioner encrypted key if it's defined.\nfunc (p *SSHPOP) GetEncryptedKey() (string, string, bool) {\n\treturn \"\", \"\", false\n}\n\n// Init initializes and validates the fields of a SSHPOP type.\nfunc (p *SSHPOP) Init(config Config) (err error) {\n\tswitch {\n\tcase p.Type == \"\":\n\t\treturn errors.New(\"provisioner type cannot be empty\")\n\tcase p.Name == \"\":\n\t\treturn errors.New(\"provisioner name cannot be empty\")\n\tcase config.SSHKeys == nil:\n\t\treturn errors.New(\"provisioner public SSH validation keys cannot be empty\")\n\t}\n\n\tp.sshPubKeys = config.SSHKeys\n\n\tconfig.Audiences = config.Audiences.WithFragment(p.GetIDForToken())\n\tp.ctl, err = NewController(p, p.Claims, config, nil)\n\treturn\n}\n\n// authorizeToken performs common jwt authorization actions and returns the\n// claims for case specific downstream parsing.\n// e.g. a Sign request will auth/validate different fields than a Revoke request.\n//\n// Checking for certificate revocation has been moved to the authority package.\nfunc (p *SSHPOP) authorizeToken(token string, audiences []string, checkValidity bool) (*sshPOPPayload, error) {\n\tsshCert, jwt, err := ExtractSSHPOPCert(token)\n\tif err != nil {\n\t\treturn nil, errs.Wrap(http.StatusUnauthorized, err,\n\t\t\t\"sshpop.authorizeToken; error extracting sshpop header from token\")\n\t}\n\n\t// Check validity period of the certificate.\n\t//\n\t// Controller.AuthorizeSSHRenew will validate this on the renewal flow.\n\tif checkValidity {\n\t\tunixNow := time.Now().Unix()\n\t\tif after := cast.Int64(sshCert.ValidAfter); after < 0 || unixNow < cast.Int64(sshCert.ValidAfter) {\n\t\t\treturn nil, errs.Unauthorized(\"sshpop.authorizeToken; sshpop certificate validAfter is in the future\")\n\t\t}\n\t\tif before := cast.Int64(sshCert.ValidBefore); sshCert.ValidBefore != uint64(ssh.CertTimeInfinity) && (unixNow >= before || before < 0) {\n\t\t\treturn nil, errs.Unauthorized(\"sshpop.authorizeToken; sshpop certificate validBefore is in the past\")\n\t\t}\n\t}\n\n\tsshCryptoPubKey, ok := sshCert.Key.(ssh.CryptoPublicKey)\n\tif !ok {\n\t\treturn nil, errs.InternalServer(\"sshpop.authorizeToken; sshpop public key could not be cast to ssh CryptoPublicKey\")\n\t}\n\tpubKey := sshCryptoPubKey.CryptoPublicKey()\n\n\tvar (\n\t\tfound bool\n\t\tdata  = bytesForSigning(sshCert)\n\t\tkeys  []ssh.PublicKey\n\t)\n\tif sshCert.CertType == ssh.UserCert {\n\t\tkeys = p.sshPubKeys.UserKeys\n\t} else {\n\t\tkeys = p.sshPubKeys.HostKeys\n\t}\n\tfor _, k := range keys {\n\t\tif err = (&ssh.Certificate{Key: k}).Verify(data, sshCert.Signature); err == nil {\n\t\t\tfound = true\n\t\t\tbreak\n\t\t}\n\t}\n\tif !found {\n\t\treturn nil, errs.Unauthorized(\"sshpop.authorizeToken; could not find valid ca signer to verify sshpop certificate\")\n\t}\n\n\t// Using the ssh certificates key to validate the claims accomplishes two\n\t// things:\n\t//   1. Asserts that the private key used to sign the token corresponds\n\t//      to the public certificate in the `sshpop` header of the token.\n\t//   2. Asserts that the claims are valid - have not been tampered with.\n\tvar claims sshPOPPayload\n\tif err = jwt.Claims(pubKey, &claims); err != nil {\n\t\treturn nil, errs.Wrap(http.StatusUnauthorized, err, \"sshpop.authorizeToken; error parsing sshpop token claims\")\n\t}\n\n\t// According to \"rfc7519 JSON Web Token\" acceptable skew should be no\n\t// more than a few minutes.\n\tif err = claims.ValidateWithLeeway(jose.Expected{\n\t\tIssuer: p.Name,\n\t\tTime:   time.Now().UTC(),\n\t}, time.Minute); err != nil {\n\t\treturn nil, errs.Wrap(http.StatusUnauthorized, err, \"sshpop.authorizeToken; invalid sshpop token\")\n\t}\n\n\t// validate audiences with the defaults\n\tif !matchesAudience(claims.Audience, audiences) {\n\t\treturn nil, errs.Unauthorized(\"sshpop.authorizeToken; sshpop token has invalid audience \"+\n\t\t\t\"claim (aud): expected %s, but got %s\", audiences, claims.Audience)\n\t}\n\n\tif claims.Subject == \"\" {\n\t\treturn nil, errs.Unauthorized(\"sshpop.authorizeToken; sshpop token subject cannot be empty\")\n\t}\n\n\tclaims.sshCert = sshCert\n\treturn &claims, nil\n}\n\n// AuthorizeSSHRevoke validates the authorization token and extracts/validates\n// the SSH certificate from the ssh-pop header.\nfunc (p *SSHPOP) AuthorizeSSHRevoke(_ context.Context, token string) error {\n\tclaims, err := p.authorizeToken(token, p.ctl.Audiences.SSHRevoke, true)\n\tif err != nil {\n\t\treturn errs.Wrap(http.StatusInternalServerError, err, \"sshpop.AuthorizeSSHRevoke\")\n\t}\n\tif serial := strconv.FormatUint(claims.sshCert.Serial, 10); claims.Subject != serial {\n\t\treturn errs.Forbidden(\n\t\t\t\"token subject %q and sshpop certificate serial number %q do not match\",\n\t\t\tclaims.Subject, serial,\n\t\t)\n\t}\n\treturn nil\n}\n\n// AuthorizeSSHRenew validates the authorization token and extracts/validates\n// the SSH certificate from the ssh-pop header.\nfunc (p *SSHPOP) AuthorizeSSHRenew(ctx context.Context, token string) (*ssh.Certificate, error) {\n\tclaims, err := p.authorizeToken(token, p.ctl.Audiences.SSHRenew, false)\n\tif err != nil {\n\t\treturn nil, errs.Wrap(http.StatusInternalServerError, err, \"sshpop.AuthorizeSSHRenew\")\n\t}\n\tif claims.sshCert.CertType != ssh.HostCert {\n\t\treturn nil, errs.BadRequest(\"sshpop certificate must be a host ssh certificate\")\n\t}\n\treturn claims.sshCert, p.ctl.AuthorizeSSHRenew(ctx, claims.sshCert)\n}\n\n// AuthorizeSSHRekey validates the authorization token and extracts/validates\n// the SSH certificate from the ssh-pop header.\nfunc (p *SSHPOP) AuthorizeSSHRekey(_ context.Context, token string) (*ssh.Certificate, []SignOption, error) {\n\tclaims, err := p.authorizeToken(token, p.ctl.Audiences.SSHRekey, true)\n\tif err != nil {\n\t\treturn nil, nil, errs.Wrap(http.StatusInternalServerError, err, \"sshpop.AuthorizeSSHRekey\")\n\t}\n\tif claims.sshCert.CertType != ssh.HostCert {\n\t\treturn nil, nil, errs.BadRequest(\"sshpop certificate must be a host ssh certificate\")\n\t}\n\treturn claims.sshCert, []SignOption{\n\t\tp,\n\t\t// Validate public key\n\t\t&sshDefaultPublicKeyValidator{},\n\t\t// Validate the validity period.\n\t\t&sshCertValidityValidator{p.ctl.Claimer},\n\t\t// Require and validate all the default fields in the SSH certificate.\n\t\t&sshCertDefaultValidator{},\n\t}, nil\n}\n\n// ExtractSSHPOPCert parses a JWT and extracts and loads the SSH Certificate\n// in the sshpop header. If the header is missing, an error is returned.\nfunc ExtractSSHPOPCert(token string) (*ssh.Certificate, *jose.JSONWebToken, error) {\n\tjwt, err := jose.ParseSigned(token)\n\tif err != nil {\n\t\treturn nil, nil, errors.Wrapf(err, \"extractSSHPOPCert; error parsing token\")\n\t}\n\n\tencodedSSHCert, ok := jwt.Headers[0].ExtraHeaders[\"sshpop\"]\n\tif !ok {\n\t\treturn nil, nil, errors.New(\"extractSSHPOPCert; token missing sshpop header\")\n\t}\n\tencodedSSHCertStr, ok := encodedSSHCert.(string)\n\tif !ok {\n\t\treturn nil, nil, errors.Errorf(\"extractSSHPOPCert; error unexpected type for sshpop header: \"+\n\t\t\t\"want 'string', but got '%T'\", encodedSSHCert)\n\t}\n\tsshCertBytes, err := base64.StdEncoding.DecodeString(encodedSSHCertStr)\n\tif err != nil {\n\t\treturn nil, nil, errors.Wrap(err, \"extractSSHPOPCert; error base64 decoding sshpop header\")\n\t}\n\tsshPub, err := ssh.ParsePublicKey(sshCertBytes)\n\tif err != nil {\n\t\treturn nil, nil, errors.Wrap(err, \"extractSSHPOPCert; error parsing ssh public key\")\n\t}\n\tsshCert, ok := sshPub.(*ssh.Certificate)\n\tif !ok {\n\t\treturn nil, nil, errors.New(\"extractSSHPOPCert; error converting ssh public key to ssh certificate\")\n\t}\n\treturn sshCert, jwt, nil\n}\n\nfunc bytesForSigning(cert *ssh.Certificate) []byte {\n\tc2 := *cert\n\tc2.Signature = nil\n\tout := c2.Marshal()\n\t// Drop trailing signature length.\n\treturn out[:len(out)-4]\n}\n"
  },
  {
    "path": "authority/provisioner/sshpop_test.go",
    "content": "package provisioner\n\nimport (\n\t\"context\"\n\t\"crypto\"\n\t\"crypto/rand\"\n\t\"encoding/base64\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"testing\"\n\t\"time\"\n\n\t\"golang.org/x/crypto/ssh\"\n\n\t\"go.step.sm/crypto/jose\"\n\t\"go.step.sm/crypto/pemutil\"\n\n\t\"github.com/smallstep/assert\"\n\t\"github.com/smallstep/certificates/api/render\"\n)\n\nfunc TestSSHPOP_Getters(t *testing.T) {\n\tp, err := generateSSHPOP()\n\tassert.FatalError(t, err)\n\tid := \"sshpop/\" + p.Name\n\tif got := p.GetID(); got != id {\n\t\tt.Errorf(\"SSHPOP.GetID() = %v, want %v\", got, id)\n\t}\n\tif got := p.GetName(); got != p.Name {\n\t\tt.Errorf(\"SSHPOP.GetName() = %v, want %v\", got, p.Name)\n\t}\n\tif got := p.GetType(); got != TypeSSHPOP {\n\t\tt.Errorf(\"SSHPOP.GetType() = %v, want %v\", got, TypeSSHPOP)\n\t}\n\tkid, key, ok := p.GetEncryptedKey()\n\tif kid != \"\" || key != \"\" || ok == true {\n\t\tt.Errorf(\"SSHPOP.GetEncryptedKey() = (%v, %v, %v), want (%v, %v, %v)\",\n\t\t\tkid, key, ok, \"\", \"\", false)\n\t}\n}\n\nfunc createSSHCert(cert *ssh.Certificate, signer ssh.Signer) (*ssh.Certificate, *jose.JSONWebKey, error) {\n\tnow := time.Now()\n\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"foo\", 0)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\tcert.Key, err = ssh.NewPublicKey(jwk.Public().Key)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\tif cert.ValidAfter == 0 {\n\t\tcert.ValidAfter = uint64(now.Unix())\n\t}\n\tif cert.ValidBefore == 0 {\n\t\tcert.ValidBefore = uint64(now.Add(time.Hour).Unix())\n\t}\n\tif err := cert.SignCert(rand.Reader, signer); err != nil {\n\t\treturn nil, nil, err\n\t}\n\treturn cert, jwk, nil\n}\n\nfunc generateSSHPOPToken(p Interface, cert *ssh.Certificate, jwk *jose.JSONWebKey) (string, error) {\n\treturn generateToken(\"foo\", p.GetName(), testAudiences.Sign[0], \"\",\n\t\t[]string{\"test.smallstep.com\"}, time.Now(), jwk, withSSHPOPFile(cert))\n}\n\nfunc TestSSHPOP_authorizeToken(t *testing.T) {\n\tkey, err := pemutil.Read(\"./testdata/secrets/ssh_user_ca_key\")\n\tassert.FatalError(t, err)\n\tsigner, ok := key.(crypto.Signer)\n\tassert.Fatal(t, ok, \"could not cast ssh signing key to crypto signer\")\n\tsshSigner, err := ssh.NewSignerFromSigner(signer)\n\tassert.FatalError(t, err)\n\n\ttype test struct {\n\t\tp     *SSHPOP\n\t\ttoken string\n\t\terr   error\n\t\tcode  int\n\t}\n\ttests := map[string]func(*testing.T) test{\n\t\t\"fail/bad-token\": func(t *testing.T) test {\n\t\t\tp, err := generateSSHPOP()\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tp:     p,\n\t\t\t\ttoken: \"foo\",\n\t\t\t\tcode:  http.StatusUnauthorized,\n\t\t\t\terr:   errors.New(\"sshpop.authorizeToken; error extracting sshpop header from token: extractSSHPOPCert; error parsing token: \"),\n\t\t\t}\n\t\t},\n\t\t\"fail/cert-not-yet-valid\": func(t *testing.T) test {\n\t\t\tp, err := generateSSHPOP()\n\t\t\tassert.FatalError(t, err)\n\t\t\tcert, jwk, err := createSSHCert(&ssh.Certificate{\n\t\t\t\tCertType:   ssh.UserCert,\n\t\t\t\tValidAfter: uint64(time.Now().Add(time.Minute).Unix()),\n\t\t\t}, sshSigner)\n\t\t\tassert.FatalError(t, err)\n\t\t\ttok, err := generateSSHPOPToken(p, cert, jwk)\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tp:     p,\n\t\t\t\ttoken: tok,\n\t\t\t\tcode:  http.StatusUnauthorized,\n\t\t\t\terr:   errors.New(\"sshpop.authorizeToken; sshpop certificate validAfter is in the future\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/cert-past-validity\": func(t *testing.T) test {\n\t\t\tp, err := generateSSHPOP()\n\t\t\tassert.FatalError(t, err)\n\t\t\tcert, jwk, err := createSSHCert(&ssh.Certificate{\n\t\t\t\tCertType:    ssh.UserCert,\n\t\t\t\tValidBefore: uint64(time.Now().Add(-time.Minute).Unix()),\n\t\t\t}, sshSigner)\n\t\t\tassert.FatalError(t, err)\n\t\t\ttok, err := generateSSHPOPToken(p, cert, jwk)\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tp:     p,\n\t\t\t\ttoken: tok,\n\t\t\t\tcode:  http.StatusUnauthorized,\n\t\t\t\terr:   errors.New(\"sshpop.authorizeToken; sshpop certificate validBefore is in the past\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/no-signer-found\": func(t *testing.T) test {\n\t\t\tp, err := generateSSHPOP()\n\t\t\tassert.FatalError(t, err)\n\t\t\tcert, jwk, err := createSSHCert(&ssh.Certificate{CertType: ssh.HostCert}, sshSigner)\n\t\t\tassert.FatalError(t, err)\n\t\t\ttok, err := generateSSHPOPToken(p, cert, jwk)\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tp:     p,\n\t\t\t\ttoken: tok,\n\t\t\t\tcode:  http.StatusUnauthorized,\n\t\t\t\terr:   errors.New(\"sshpop.authorizeToken; could not find valid ca signer to verify sshpop certificate\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/error-parsing-claims-bad-sig\": func(t *testing.T) test {\n\t\t\tp, err := generateSSHPOP()\n\t\t\tassert.FatalError(t, err)\n\t\t\tcert, _, err := createSSHCert(&ssh.Certificate{CertType: ssh.UserCert}, sshSigner)\n\t\t\tassert.FatalError(t, err)\n\t\t\totherJWK, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\tassert.FatalError(t, err)\n\t\t\ttok, err := generateSSHPOPToken(p, cert, otherJWK)\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tp:     p,\n\t\t\t\ttoken: tok,\n\t\t\t\tcode:  http.StatusUnauthorized,\n\t\t\t\terr:   errors.New(\"sshpop.authorizeToken; error parsing sshpop token claims\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/invalid-claims-issuer\": func(t *testing.T) test {\n\t\t\tp, err := generateSSHPOP()\n\t\t\tassert.FatalError(t, err)\n\t\t\tcert, jwk, err := createSSHCert(&ssh.Certificate{CertType: ssh.UserCert}, sshSigner)\n\t\t\tassert.FatalError(t, err)\n\t\t\ttok, err := generateToken(\"foo\", \"bar\", testAudiences.Sign[0], \"\",\n\t\t\t\t[]string{\"test.smallstep.com\"}, time.Now(), jwk, withSSHPOPFile(cert))\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tp:     p,\n\t\t\t\ttoken: tok,\n\t\t\t\tcode:  http.StatusUnauthorized,\n\t\t\t\terr:   errors.New(\"sshpop.authorizeToken; invalid sshpop token\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/invalid-audience\": func(t *testing.T) test {\n\t\t\tp, err := generateSSHPOP()\n\t\t\tassert.FatalError(t, err)\n\t\t\tcert, jwk, err := createSSHCert(&ssh.Certificate{CertType: ssh.UserCert}, sshSigner)\n\t\t\tassert.FatalError(t, err)\n\t\t\ttok, err := generateToken(\"foo\", p.GetName(), \"invalid-aud\", \"\",\n\t\t\t\t[]string{\"test.smallstep.com\"}, time.Now(), jwk, withSSHPOPFile(cert))\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tp:     p,\n\t\t\t\ttoken: tok,\n\t\t\t\tcode:  http.StatusUnauthorized,\n\t\t\t\terr:   errors.New(\"sshpop.authorizeToken; sshpop token has invalid audience claim (aud)\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/empty-subject\": func(t *testing.T) test {\n\t\t\tp, err := generateSSHPOP()\n\t\t\tassert.FatalError(t, err)\n\t\t\tcert, jwk, err := createSSHCert(&ssh.Certificate{CertType: ssh.UserCert}, sshSigner)\n\t\t\tassert.FatalError(t, err)\n\t\t\ttok, err := generateToken(\"\", p.GetName(), testAudiences.Sign[0], \"\",\n\t\t\t\t[]string{\"test.smallstep.com\"}, time.Now(), jwk, withSSHPOPFile(cert))\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tp:     p,\n\t\t\t\ttoken: tok,\n\t\t\t\tcode:  http.StatusUnauthorized,\n\t\t\t\terr:   errors.New(\"sshpop.authorizeToken; sshpop token subject cannot be empty\"),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tp, err := generateSSHPOP()\n\t\t\tassert.FatalError(t, err)\n\t\t\tcert, jwk, err := createSSHCert(&ssh.Certificate{CertType: ssh.UserCert}, sshSigner)\n\t\t\tassert.FatalError(t, err)\n\t\t\ttok, err := generateSSHPOPToken(p, cert, jwk)\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tp:     p,\n\t\t\t\ttoken: tok,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, tt := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ttc := tt(t)\n\t\t\tif claims, err := tc.p.authorizeToken(tc.token, testAudiences.Sign, true); err != nil {\n\t\t\t\tvar sc render.StatusCodedError\n\t\t\t\tif assert.True(t, errors.As(err, &sc), \"error does not implement StatusCodedError interface\") {\n\t\t\t\t\tassert.Equals(t, sc.StatusCode(), tc.code)\n\t\t\t\t}\n\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.err.Error())\n\t\t\t\t}\n\t\t\t} else if assert.Nil(t, tc.err) {\n\t\t\t\tassert.NotNil(t, claims)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSSHPOP_AuthorizeSSHRevoke(t *testing.T) {\n\tkey, err := pemutil.Read(\"./testdata/secrets/ssh_user_ca_key\")\n\tassert.FatalError(t, err)\n\tsigner, ok := key.(crypto.Signer)\n\tassert.Fatal(t, ok, \"could not cast ssh signing key to crypto signer\")\n\tsshSigner, err := ssh.NewSignerFromSigner(signer)\n\tassert.FatalError(t, err)\n\n\ttype test struct {\n\t\tp     *SSHPOP\n\t\ttoken string\n\t\terr   error\n\t\tcode  int\n\t}\n\ttests := map[string]func(*testing.T) test{\n\t\t\"fail/bad-token\": func(t *testing.T) test {\n\t\t\tp, err := generateSSHPOP()\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tp:     p,\n\t\t\t\ttoken: \"foo\",\n\t\t\t\tcode:  http.StatusUnauthorized,\n\t\t\t\terr:   errors.New(\"sshpop.AuthorizeSSHRevoke: sshpop.authorizeToken; error extracting sshpop header from token: extractSSHPOPCert; error parsing token: \"),\n\t\t\t}\n\t\t},\n\t\t\"fail/subject-not-equal-serial\": func(t *testing.T) test {\n\t\t\tp, err := generateSSHPOP()\n\t\t\tassert.FatalError(t, err)\n\t\t\tcert, jwk, err := createSSHCert(&ssh.Certificate{CertType: ssh.UserCert}, sshSigner)\n\t\t\tassert.FatalError(t, err)\n\t\t\ttok, err := generateToken(\"foo\", p.GetName(), testAudiences.SSHRevoke[0], \"\",\n\t\t\t\t[]string{\"test.smallstep.com\"}, time.Now(), jwk, withSSHPOPFile(cert))\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tp:     p,\n\t\t\t\ttoken: tok,\n\t\t\t\tcode:  http.StatusForbidden,\n\t\t\t\terr:   errors.New(`token subject \"foo\" and sshpop certificate serial number \"0\" do not match`),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tp, err := generateSSHPOP()\n\t\t\tassert.FatalError(t, err)\n\t\t\tcert, jwk, err := createSSHCert(&ssh.Certificate{Serial: 123455, CertType: ssh.UserCert}, sshSigner)\n\t\t\tassert.FatalError(t, err)\n\t\t\ttok, err := generateToken(\"123455\", p.GetName(), testAudiences.SSHRevoke[0], \"\",\n\t\t\t\t[]string{\"test.smallstep.com\"}, time.Now(), jwk, withSSHPOPFile(cert))\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tp:     p,\n\t\t\t\ttoken: tok,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, tt := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ttc := tt(t)\n\t\t\tif err := tc.p.AuthorizeSSHRevoke(context.Background(), tc.token); err != nil {\n\t\t\t\tvar sc render.StatusCodedError\n\t\t\t\tif assert.True(t, errors.As(err, &sc), \"error does not implement StatusCodedError interface\") {\n\t\t\t\t\tassert.Equals(t, sc.StatusCode(), tc.code)\n\t\t\t\t}\n\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.err.Error())\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tassert.Nil(t, tc.err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSSHPOP_AuthorizeSSHRenew(t *testing.T) {\n\tkey, err := pemutil.Read(\"./testdata/secrets/ssh_user_ca_key\")\n\tassert.FatalError(t, err)\n\tuserSigner, ok := key.(crypto.Signer)\n\tassert.Fatal(t, ok, \"could not cast ssh user signing key to crypto signer\")\n\tsshUserSigner, err := ssh.NewSignerFromSigner(userSigner)\n\tassert.FatalError(t, err)\n\n\thostKey, err := pemutil.Read(\"./testdata/secrets/ssh_host_ca_key\")\n\tassert.FatalError(t, err)\n\thostSigner, ok := hostKey.(crypto.Signer)\n\tassert.Fatal(t, ok, \"could not cast ssh host signing key to crypto signer\")\n\tsshHostSigner, err := ssh.NewSignerFromSigner(hostSigner)\n\tassert.FatalError(t, err)\n\n\ttype test struct {\n\t\tp     *SSHPOP\n\t\ttoken string\n\t\tcert  *ssh.Certificate\n\t\terr   error\n\t\tcode  int\n\t}\n\ttests := map[string]func(*testing.T) test{\n\t\t\"fail/bad-token\": func(t *testing.T) test {\n\t\t\tp, err := generateSSHPOP()\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tp:     p,\n\t\t\t\ttoken: \"foo\",\n\t\t\t\tcode:  http.StatusUnauthorized,\n\t\t\t\terr:   errors.New(\"sshpop.AuthorizeSSHRenew: sshpop.authorizeToken; error extracting sshpop header from token: extractSSHPOPCert; error parsing token: \"),\n\t\t\t}\n\t\t},\n\t\t\"fail/not-host-cert\": func(t *testing.T) test {\n\t\t\tp, err := generateSSHPOP()\n\t\t\tassert.FatalError(t, err)\n\t\t\tcert, jwk, err := createSSHCert(&ssh.Certificate{CertType: ssh.UserCert}, sshUserSigner)\n\t\t\tassert.FatalError(t, err)\n\t\t\ttok, err := generateToken(\"foo\", p.GetName(), testAudiences.SSHRenew[0], \"\",\n\t\t\t\t[]string{\"test.smallstep.com\"}, time.Now(), jwk, withSSHPOPFile(cert))\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tp:     p,\n\t\t\t\ttoken: tok,\n\t\t\t\tcode:  http.StatusBadRequest,\n\t\t\t\terr:   errors.New(\"sshpop certificate must be a host ssh certificate\"),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tp, err := generateSSHPOP()\n\t\t\tassert.FatalError(t, err)\n\t\t\tcert, jwk, err := createSSHCert(&ssh.Certificate{Serial: 123455, CertType: ssh.HostCert}, sshHostSigner)\n\t\t\tassert.FatalError(t, err)\n\t\t\ttok, err := generateToken(\"123455\", p.GetName(), testAudiences.SSHRenew[0], \"\",\n\t\t\t\t[]string{\"test.smallstep.com\"}, time.Now(), jwk, withSSHPOPFile(cert))\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tp:     p,\n\t\t\t\ttoken: tok,\n\t\t\t\tcert:  cert,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, tt := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ttc := tt(t)\n\t\t\tif cert, err := tc.p.AuthorizeSSHRenew(context.Background(), tc.token); err != nil {\n\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\tvar sc render.StatusCodedError\n\t\t\t\t\tif assert.True(t, errors.As(err, &sc), \"error does not implement StatusCodedError interface\") {\n\t\t\t\t\t\tassert.Equals(t, sc.StatusCode(), tc.code)\n\t\t\t\t\t}\n\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.err.Error())\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif assert.Nil(t, tc.err) {\n\t\t\t\t\tassert.Equals(t, tc.cert.Nonce, cert.Nonce)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSSHPOP_AuthorizeSSHRekey(t *testing.T) {\n\tkey, err := pemutil.Read(\"./testdata/secrets/ssh_user_ca_key\")\n\tassert.FatalError(t, err)\n\tuserSigner, ok := key.(crypto.Signer)\n\tassert.Fatal(t, ok, \"could not cast ssh user signing key to crypto signer\")\n\tsshUserSigner, err := ssh.NewSignerFromSigner(userSigner)\n\tassert.FatalError(t, err)\n\n\thostKey, err := pemutil.Read(\"./testdata/secrets/ssh_host_ca_key\")\n\tassert.FatalError(t, err)\n\thostSigner, ok := hostKey.(crypto.Signer)\n\tassert.Fatal(t, ok, \"could not cast ssh host signing key to crypto signer\")\n\tsshHostSigner, err := ssh.NewSignerFromSigner(hostSigner)\n\tassert.FatalError(t, err)\n\n\ttype test struct {\n\t\tp     *SSHPOP\n\t\ttoken string\n\t\tcert  *ssh.Certificate\n\t\terr   error\n\t\tcode  int\n\t}\n\ttests := map[string]func(*testing.T) test{\n\t\t\"fail/bad-token\": func(t *testing.T) test {\n\t\t\tp, err := generateSSHPOP()\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tp:     p,\n\t\t\t\ttoken: \"foo\",\n\t\t\t\tcode:  http.StatusUnauthorized,\n\t\t\t\terr:   errors.New(\"sshpop.AuthorizeSSHRekey: sshpop.authorizeToken; error extracting sshpop header from token: extractSSHPOPCert; error parsing token: \"),\n\t\t\t}\n\t\t},\n\t\t\"fail/not-host-cert\": func(t *testing.T) test {\n\t\t\tp, err := generateSSHPOP()\n\t\t\tassert.FatalError(t, err)\n\t\t\tcert, jwk, err := createSSHCert(&ssh.Certificate{CertType: ssh.UserCert}, sshUserSigner)\n\t\t\tassert.FatalError(t, err)\n\t\t\ttok, err := generateToken(\"foo\", p.GetName(), testAudiences.SSHRekey[0], \"\",\n\t\t\t\t[]string{\"test.smallstep.com\"}, time.Now(), jwk, withSSHPOPFile(cert))\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tp:     p,\n\t\t\t\ttoken: tok,\n\t\t\t\tcode:  http.StatusBadRequest,\n\t\t\t\terr:   errors.New(\"sshpop certificate must be a host ssh certificate\"),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tp, err := generateSSHPOP()\n\t\t\tassert.FatalError(t, err)\n\t\t\tcert, jwk, err := createSSHCert(&ssh.Certificate{Serial: 123455, CertType: ssh.HostCert}, sshHostSigner)\n\t\t\tassert.FatalError(t, err)\n\t\t\ttok, err := generateToken(\"123455\", p.GetName(), testAudiences.SSHRekey[0], \"\",\n\t\t\t\t[]string{\"test.smallstep.com\"}, time.Now(), jwk, withSSHPOPFile(cert))\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\tp:     p,\n\t\t\t\ttoken: tok,\n\t\t\t\tcert:  cert,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, tt := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ttc := tt(t)\n\t\t\tif cert, opts, err := tc.p.AuthorizeSSHRekey(context.Background(), tc.token); err != nil {\n\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\tvar sc render.StatusCodedError\n\t\t\t\t\tif assert.True(t, errors.As(err, &sc), \"error does not implement StatusCodedError interface\") {\n\t\t\t\t\t\tassert.Equals(t, sc.StatusCode(), tc.code)\n\t\t\t\t\t}\n\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.err.Error())\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif assert.Nil(t, tc.err) {\n\t\t\t\t\tassert.Len(t, 4, opts)\n\t\t\t\t\tfor _, o := range opts {\n\t\t\t\t\t\tswitch v := o.(type) {\n\t\t\t\t\t\tcase Interface:\n\t\t\t\t\t\tcase *sshDefaultPublicKeyValidator:\n\t\t\t\t\t\tcase *sshCertDefaultValidator:\n\t\t\t\t\t\tcase *sshCertValidityValidator:\n\t\t\t\t\t\t\tassert.Equals(t, v.Claimer, tc.p.ctl.Claimer)\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\tassert.FatalError(t, fmt.Errorf(\"unexpected sign option of type %T\", v))\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tassert.Equals(t, tc.cert.Nonce, cert.Nonce)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSSHPOP_ExtractSSHPOPCert(t *testing.T) {\n\thostKey, err := pemutil.Read(\"./testdata/secrets/ssh_host_ca_key\")\n\tassert.FatalError(t, err)\n\thostSigner, ok := hostKey.(crypto.Signer)\n\tassert.Fatal(t, ok, \"could not cast ssh host signing key to crypto signer\")\n\tsshHostSigner, err := ssh.NewSignerFromSigner(hostSigner)\n\tassert.FatalError(t, err)\n\n\ttype test struct {\n\t\ttoken string\n\t\tcert  *ssh.Certificate\n\t\tjwk   *jose.JSONWebKey\n\t\terr   error\n\t}\n\ttests := map[string]func(*testing.T) test{\n\t\t\"fail/bad-token\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\ttoken: \"foo\",\n\t\t\t\terr:   errors.New(\"extractSSHPOPCert; error parsing token\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/sshpop-missing\": func(t *testing.T) test {\n\t\t\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\tassert.FatalError(t, err)\n\t\t\ttok, err := generateToken(\"sub\", \"sshpop-provisioner\", testAudiences.SSHRekey[0], \"\",\n\t\t\t\t[]string{\"test.smallstep.com\"}, time.Now(), jwk)\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\ttoken: tok,\n\t\t\t\terr:   errors.New(\"extractSSHPOPCert; token missing sshpop header\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/wrong-sshpop-type\": func(t *testing.T) test {\n\t\t\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\tassert.FatalError(t, err)\n\t\t\ttok, err := generateToken(\"123455\", \"sshpop-provisioner\", testAudiences.SSHRekey[0], \"\",\n\t\t\t\t[]string{\"test.smallstep.com\"}, time.Now(), jwk, func(so *jose.SignerOptions) error {\n\t\t\t\t\tso.WithHeader(\"sshpop\", 12345)\n\t\t\t\t\treturn nil\n\t\t\t\t})\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\ttoken: tok,\n\t\t\t\terr:   errors.New(\"extractSSHPOPCert; error unexpected type for sshpop header: \"),\n\t\t\t}\n\t\t},\n\t\t\"fail/base64decode-error\": func(t *testing.T) test {\n\t\t\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\tassert.FatalError(t, err)\n\t\t\ttok, err := generateToken(\"123455\", \"sshpop-provisioner\", testAudiences.SSHRekey[0], \"\",\n\t\t\t\t[]string{\"test.smallstep.com\"}, time.Now(), jwk, func(so *jose.SignerOptions) error {\n\t\t\t\t\tso.WithHeader(\"sshpop\", \"!@#$%^&*\")\n\t\t\t\t\treturn nil\n\t\t\t\t})\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\ttoken: tok,\n\t\t\t\terr:   errors.New(\"extractSSHPOPCert; error base64 decoding sshpop header: illegal base64\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/parsing-sshpop-pubkey\": func(t *testing.T) test {\n\t\t\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\tassert.FatalError(t, err)\n\t\t\ttok, err := generateToken(\"123455\", \"sshpop-provisioner\", testAudiences.SSHRekey[0], \"\",\n\t\t\t\t[]string{\"test.smallstep.com\"}, time.Now(), jwk, func(so *jose.SignerOptions) error {\n\t\t\t\t\tso.WithHeader(\"sshpop\", base64.StdEncoding.EncodeToString([]byte(\"foo\")))\n\t\t\t\t\treturn nil\n\t\t\t\t})\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\ttoken: tok,\n\t\t\t\terr:   errors.New(\"extractSSHPOPCert; error parsing ssh public key\"),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tcert, jwk, err := createSSHCert(&ssh.Certificate{Serial: 123455, CertType: ssh.HostCert}, sshHostSigner)\n\n\t\t\tassert.FatalError(t, err)\n\t\t\ttok, err := generateToken(\"123455\", \"sshpop-provisioner\", testAudiences.SSHRekey[0], \"\",\n\t\t\t\t[]string{\"test.smallstep.com\"}, time.Now(), jwk, withSSHPOPFile(cert))\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn test{\n\t\t\t\ttoken: tok,\n\t\t\t\tjwk:   jwk,\n\t\t\t\tcert:  cert,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, tt := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ttc := tt(t)\n\t\t\tif cert, jwt, err := ExtractSSHPOPCert(tc.token); err != nil {\n\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.err.Error())\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif assert.Nil(t, tc.err) {\n\t\t\t\t\tassert.Equals(t, tc.cert.Nonce, cert.Nonce)\n\t\t\t\t\tassert.Equals(t, tc.jwk.KeyID, jwt.Headers[0].KeyID)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "authority/provisioner/testdata/certs/apple-att-ca.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIICJDCCAamgAwIBAgIUQsDCuyxyfFxeq/bxpm8frF15hzcwCgYIKoZIzj0EAwMw\nUTEtMCsGA1UEAwwkQXBwbGUgRW50ZXJwcmlzZSBBdHRlc3RhdGlvbiBSb290IENB\nMRMwEQYDVQQKDApBcHBsZSBJbmMuMQswCQYDVQQGEwJVUzAeFw0yMjAyMTYxOTAx\nMjRaFw00NzAyMjAwMDAwMDBaMFExLTArBgNVBAMMJEFwcGxlIEVudGVycHJpc2Ug\nQXR0ZXN0YXRpb24gUm9vdCBDQTETMBEGA1UECgwKQXBwbGUgSW5jLjELMAkGA1UE\nBhMCVVMwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAT6Jigq+Ps9Q4CoT8t8q+UnOe2p\noT9nRaUfGhBTbgvqSGXPjVkbYlIWYO+1zPk2Sz9hQ5ozzmLrPmTBgEWRcHjA2/y7\n7GEicps9wn2tj+G89l3INNDKETdxSPPIZpPj8VmjQjBAMA8GA1UdEwEB/wQFMAMB\nAf8wHQYDVR0OBBYEFPNqTQGd8muBpV5du+UIbVbi+d66MA4GA1UdDwEB/wQEAwIB\nBjAKBggqhkjOPQQDAwNpADBmAjEA1xpWmTLSpr1VH4f8Ypk8f3jMUKYz4QPG8mL5\n8m9sX/b2+eXpTv2pH4RZgJjucnbcAjEA4ZSB6S45FlPuS/u4pTnzoz632rA+xW/T\nZwFEh9bhKjJ+5VQ9/Do1os0u3LEkgN/r\n-----END CERTIFICATE-----"
  },
  {
    "path": "authority/provisioner/testdata/certs/aws-test.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIDIjCCAougAwIBAgIJAKnL4UEDMN/FMA0GCSqGSIb3DQEBBQUAMGoxCzAJBgNV\nBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdTZWF0dGxlMRgw\nFgYDVQQKEw9BbWF6b24uY29tIEluYy4xGjAYBgNVBAMTEWVjMi5hbWF6b25hd3Mu\nY29tMB4XDTE0MDYwNTE0MjgwMloXDTI0MDYwNTE0MjgwMlowajELMAkGA1UEBhMC\nVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1NlYXR0bGUxGDAWBgNV\nBAoTD0FtYXpvbi5jb20gSW5jLjEaMBgGA1UEAxMRZWMyLmFtYXpvbmF3cy5jb20w\ngZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAIe9GN//SRK2knbjySG0ho3yqQM3\ne2TDhWO8D2e8+XZqck754gFSo99AbT2RmXClambI7xsYHZFapbELC4H91ycihvrD\njbST1ZjkLQgga0NE1q43eS68ZeTDccScXQSNivSlzJZS8HJZjgqzBlXjZftjtdJL\nXeE4hwvo0sD4f3j9AgMBAAGjgc8wgcwwHQYDVR0OBBYEFCXWzAgVyrbwnFncFFIs\n77VBdlE4MIGcBgNVHSMEgZQwgZGAFCXWzAgVyrbwnFncFFIs77VBdlE4oW6kbDBq\nMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHU2Vh\ndHRsZTEYMBYGA1UEChMPQW1hem9uLmNvbSBJbmMuMRowGAYDVQQDExFlYzIuYW1h\nem9uYXdzLmNvbYIJAKnL4UEDMN/FMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEF\nBQADgYEAFYcz1OgEhQBXIwIdsgCOS8vEtiJYF+j9uO6jz7VOmJqO+pRlAbRlvY8T\nC1haGgSI/A1uZUKs/Zfnph0oEI0/hu1IIJ/SKBDtN5lvmZ/IzbOPIJWirlsllQIQ\n7zvWbGd9c9+Rm3p04oTvhup99la7kZqevJK0QRdD/6NpCKsqP/0=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIICFTCCAX6gAwIBAgIRAKmbVVYAl/1XEqRfF3eJ97MwDQYJKoZIhvcNAQELBQAw\nGDEWMBQGA1UEAxMNQVdTIFRlc3QgQ2VydDAeFw0xOTA0MjQyMjU3MzlaFw0yOTA0\nMjEyMjU3MzlaMBgxFjAUBgNVBAMTDUFXUyBUZXN0IENlcnQwgZ8wDQYJKoZIhvcN\nAQEBBQADgY0AMIGJAoGBAOHMmMXwbXN90SoRl/xXAcJs5TacaVYJ5iNAVWM5KYyF\n+JwqYuJp/umLztFUi0oX0luu3EzD4KurVeUJSzZjTFTX1d/NX6hA45+bvdSUOcgV\nUghO+2uhBZ4SNFxFRZ7SKvoWIN195l5bVX6/60Eo6+kUCKCkyxW4V/ksWzdXjHnf\nAgMBAAGjXzBdMA4GA1UdDwEB/wQEAwIBBjASBgNVHRMBAf8ECDAGAQH/AgEBMB0G\nA1UdDgQWBBRHfLOjEddK/CWCIHNg8Oc/oJa1IzAYBgNVHREEETAPgg1BV1MgVGVz\ndCBDZXJ0MA0GCSqGSIb3DQEBCwUAA4GBAKNCiVM9eGb9dW2xNyHaHAmmy7ERB2OJ\n7oXHfLjooOavk9lU/Gs2jfX/JSBa84+DzWg9ShmCNLti8CxU/dhzXW7jE/5CcdTa\nDCA6B3Yl5TmfG9+D9dtFqRB2CiMgNcsJJE5Dc6pDwBIiSj/MkE0AaGVQmSwn6Cb6\nvX1TAxqeWJHq\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "authority/provisioner/testdata/certs/aws.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIDIjCCAougAwIBAgIJAKnL4UEDMN/FMA0GCSqGSIb3DQEBBQUAMGoxCzAJBgNV\nBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdTZWF0dGxlMRgw\nFgYDVQQKEw9BbWF6b24uY29tIEluYy4xGjAYBgNVBAMTEWVjMi5hbWF6b25hd3Mu\nY29tMB4XDTE0MDYwNTE0MjgwMloXDTI0MDYwNTE0MjgwMlowajELMAkGA1UEBhMC\nVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1NlYXR0bGUxGDAWBgNV\nBAoTD0FtYXpvbi5jb20gSW5jLjEaMBgGA1UEAxMRZWMyLmFtYXpvbmF3cy5jb20w\ngZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAIe9GN//SRK2knbjySG0ho3yqQM3\ne2TDhWO8D2e8+XZqck754gFSo99AbT2RmXClambI7xsYHZFapbELC4H91ycihvrD\njbST1ZjkLQgga0NE1q43eS68ZeTDccScXQSNivSlzJZS8HJZjgqzBlXjZftjtdJL\nXeE4hwvo0sD4f3j9AgMBAAGjgc8wgcwwHQYDVR0OBBYEFCXWzAgVyrbwnFncFFIs\n77VBdlE4MIGcBgNVHSMEgZQwgZGAFCXWzAgVyrbwnFncFFIs77VBdlE4oW6kbDBq\nMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHU2Vh\ndHRsZTEYMBYGA1UEChMPQW1hem9uLmNvbSBJbmMuMRowGAYDVQQDExFlYzIuYW1h\nem9uYXdzLmNvbYIJAKnL4UEDMN/FMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEF\nBQADgYEAFYcz1OgEhQBXIwIdsgCOS8vEtiJYF+j9uO6jz7VOmJqO+pRlAbRlvY8T\nC1haGgSI/A1uZUKs/Zfnph0oEI0/hu1IIJ/SKBDtN5lvmZ/IzbOPIJWirlsllQIQ\n7zvWbGd9c9+Rm3p04oTvhup99la7kZqevJK0QRdD/6NpCKsqP/0=\n-----END CERTIFICATE-----"
  },
  {
    "path": "authority/provisioner/testdata/certs/bad-extension.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIDeTCCAx+gAwIBAgIRAOTItW2pYuSU+PkmLW090iUwCgYIKoZIzj0EAwIwJDEi\nMCAGA1UEAxMZU21hbGxzdGVwIEludGVybWVkaWF0ZSBDQTAeFw0yMjAzMTEyMjUy\nMjBaFw0yMjAzMTIyMjUzMjBaMIGcMQswCQYDVQQGEwJDSDETMBEGA1UECBMKQ2Fs\naWZvcm5pYTEWMBQGA1UEBxMNU2FuIEZyYW5jaXNjbzEYMBYGA1UECRMPMSBUaGUg\nU3RyZWV0IFN0MRMwEQYDVQQKDAo8bm8gdmFsdWU+MRYwFAYDVQQLEw1TbWFsbHN0\nZXAgRW5nMRkwFwYDVQQDDBB0ZXN0QGV4YW1wbGUuY29tMFkwEwYHKoZIzj0CAQYI\nKoZIzj0DAQcDQgAE/9vvOZ1Zzysnf3VeGyotMJEMZdAborB36Ah5QL/3yQNMRWIc\npv9Dwx19pHw7SquVE8jIaPPJSjaeWnfMPDYDxaOCAbcwggGzMA4GA1UdDwEB/wQE\nAwIHgDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDAYDVR0TAQH/BAIw\nADAdBgNVHQ4EFgQUkJUg6AsqWlqTZt6BHidRMwh1vKYwHwYDVR0jBBgwFoAUDpTg\nd3VFCn6e71wXcwbDCURBomUwgZoGCCsGAQUFBwEBBIGNMIGKMBcGCCsGAQUFBzAB\nhgtodHRwczovL2ZvbzBvBggrBgEFBQcwAoZjaHR0cHM6Ly9jYS5zbWFsbHN0ZXAu\nY29tOjkwMDAvcm9vdC9hNzhhODUwMDI1YzBjMjM0Mzg1ZWRhMjNkNzE5Mjk2NGNh\nNTZhYTlkNzI3ZjUzNTY1M2IwYWZiODFjMWUwNTU5MBsGA1UdEQQUMBKBEHRlc3RA\nZXhhbXBsZS5jb20wIAYDVR0gBBkwFzALBglghkgBhv1sAQEwCAYGZ4EMAQICMD8G\nA1UdHwQ4MDYwNKAyoDCGLmh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9zaGEyLWV2\nLXNlcnZlci1nMy5jcmwwFwYMKwYBBAGCpGTGKEABBAdmb29vYmFyMAoGCCqGSM49\nBAMCA0gAMEUCIQCWYqOuk4bLkVVeHvo3P8TlJJ3fw6ijDDLstvdrQqAl5wIgEjSY\nwVcR649Oc8PJGh/43Kpx0+4OTYPQrD/JqphVF7g=\n-----END CERTIFICATE-----"
  },
  {
    "path": "authority/provisioner/testdata/certs/bar.pub",
    "content": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEGQIXbsr73X28pzwC1wa+ccY2H3s8\nPplbkapCrwxyYVvM78y/GmeSA7fv2MKQ8iKpCw461MlOQGX+VlWT+ChRFw==\n-----END PUBLIC KEY-----\n"
  },
  {
    "path": "authority/provisioner/testdata/certs/ecdsa.csr",
    "content": "-----BEGIN CERTIFICATE REQUEST-----\nMIHqMIGRAgEAMA4xDDAKBgNVBAMTA2ZvbzBZMBMGByqGSM49AgEGCCqGSM49AwEH\nA0IABKdDjTb7XIYCWC4QUq1xn5hgf3J4WpfWbd3C5frKrA4/VdQ+XfpHQIxDoHqh\njcWke0SEETc9i6HDDtWv8bXSETegITAfBgkqhkiG9w0BCQ4xEjAQMA4GA1UdEQQH\nMAWCA2ZvbzAKBggqhkjOPQQDAgNIADBFAiEA1pFLT8p/YogG0o6NEEmdxzwbOzJA\nA+C+DvoT91c1OcQCIGUjP3s+k6Xwdf/VukUZXTfG1lobmkZhO3vYxAjPkwA7\n-----END CERTIFICATE REQUEST-----\n"
  },
  {
    "path": "authority/provisioner/testdata/certs/ed25519.csr",
    "content": "-----BEGIN CERTIFICATE REQUEST-----\nMIGuMGICAQAwDjEMMAoGA1UEAxMDZm9vMCowBQYDK2VwAyEA3yF/Igqb5UTp6XOq\nyj+cZL9nIfjDKrUT0fMzDAHtIqqgITAfBgkqhkiG9w0BCQ4xEjAQMA4GA1UdEQQH\nMAWCA2ZvbzAFBgMrZXADQQAIAx7N6ezi4NL8n0oJU8v3AmVSi0XvTuIHXUtcLGoU\nOZtlO3zjWI+DgcT/ADeEKn+T8OazDxcCbTBbHiM2hIsA\n-----END CERTIFICATE REQUEST-----\n"
  },
  {
    "path": "authority/provisioner/testdata/certs/foo.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIICIDCCAcagAwIBAgIQTL7pKDl8mFzRziotXbgjEjAKBggqhkjOPQQDAjAnMSUw\nIwYDVQQDExxFeGFtcGxlIEluYy4gSW50ZXJtZWRpYXRlIENBMB4XDTE5MDMyMjIy\nMjkyOVoXDTE5MDMyMzIyMjkyOVowHDEaMBgGA1UEAxMRZm9vLnNtYWxsc3RlcC5j\nb20wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQbptfDonFaeUPiTr52wl9r3dcz\ngreolwDRmsgyFgnr1EuKH56WRcgH1gjfL0pybFlO3PdgBukR4u+sveq343OAo4He\nMIHbMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUH\nAwIwHQYDVR0OBBYEFP9pHiVlsx5mr4L2QirOb1G9Mo4jMB8GA1UdIwQYMBaAFKEe\n9IdMyaHdURMjoJce7FN9HC9wMBwGA1UdEQQVMBOCEWZvby5zbWFsbHN0ZXAuY29t\nMEwGDCsGAQQBgqRkxihAAQQ8MDoCAQEECHN0ZXAtY2xpBCs0VUVMSng4ZTBhUzlt\nMENIM2ZaMEVCN0Q1YVVQSUNiNzU5ekFMSEZlanZjMAoGCCqGSM49BAMCA0gAMEUC\nIDxtNo1BX/4Sbf/+k1n+v//kh8ETr3clPvhjcyfvBIGTAiEAiT0kvbkPdCCnmHIw\nlhpgBwT5YReZzBwIYXyKyJXc07M=\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "authority/provisioner/testdata/certs/foo.pub",
    "content": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEzriaeV2e1aEz33x62kyqVC6ootU7\nrl41L8cyeOJ4SjTu4FV+o5i4NsS6DCE07JJSHlqc9PsrzjSs4LZD4gWVLQ==\n-----END PUBLIC KEY-----\n"
  },
  {
    "path": "authority/provisioner/testdata/certs/good-extension.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIDujCCA2GgAwIBAgIRAM5celDKTTqAGycljO7FZdEwCgYIKoZIzj0EAwIwJDEi\nMCAGA1UEAxMZU21hbGxzdGVwIEludGVybWVkaWF0ZSBDQTAeFw0yMjAzMTEyMjQx\nMDRaFw0yMjAzMTIyMjQyMDRaMIGcMQswCQYDVQQGEwJDSDETMBEGA1UECBMKQ2Fs\naWZvcm5pYTEWMBQGA1UEBxMNU2FuIEZyYW5jaXNjbzEYMBYGA1UECRMPMSBUaGUg\nU3RyZWV0IFN0MRMwEQYDVQQKDAo8bm8gdmFsdWU+MRYwFAYDVQQLEw1TbWFsbHN0\nZXAgRW5nMRkwFwYDVQQDDBB0ZXN0QGV4YW1wbGUuY29tMFkwEwYHKoZIzj0CAQYI\nKoZIzj0DAQcDQgAEkXffZYlSJRMxJrZHmUpEMC4jQYCkF86mLJY0iLZ8k00N/xF0\n4rAGwzTU/l9tfRpNl+z/XfMMWPXS0Q8NU/o4S6OCAfkwggH1MA4GA1UdDwEB/wQE\nAwIHgDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDAYDVR0TAQH/BAIw\nADAdBgNVHQ4EFgQUL3sSlYW8Tf2l2P+gFTdn5wsUjfgwHwYDVR0jBBgwFoAUDpTg\nd3VFCn6e71wXcwbDCURBomUwgZoGCCsGAQUFBwEBBIGNMIGKMBcGCCsGAQUFBzAB\nhgtodHRwczovL2ZvbzBvBggrBgEFBQcwAoZjaHR0cHM6Ly9jYS5zbWFsbHN0ZXAu\nY29tOjkwMDAvcm9vdC9hNzhhODUwMDI1YzBjMjM0Mzg1ZWRhMjNkNzE5Mjk2NGNh\nNTZhYTlkNzI3ZjUzNTY1M2IwYWZiODFjMWUwNTU5MBsGA1UdEQQUMBKBEHRlc3RA\nZXhhbXBsZS5jb20wIAYDVR0gBBkwFzALBglghkgBhv1sAQEwCAYGZ4EMAQICMD8G\nA1UdHwQ4MDYwNKAyoDCGLmh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9zaGEyLWV2\nLXNlcnZlci1nMy5jcmwwWQYMKwYBBAGCpGTGKEABBEkwRwIBAQQVbWFyaWFub0Bz\nbWFsbHN0ZXAuY29tBCtudmduUjh3U3pwVWxydF90QzNtdnJod2hCeDlZN1QxV0xf\nSmpjRlZXWUJRMAoGCCqGSM49BAMCA0cAMEQCIE6umrhSbeQWWVK5cWBvXj5c0cGB\nbUF0rNw/dsaCaWcwAiAKSkmjhsC63DVPXPCNUki90YgVovO69foO1ZaB43lx5w==\n-----END CERTIFICATE-----"
  },
  {
    "path": "authority/provisioner/testdata/certs/root_ca.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIBhzCCASygAwIBAgIRANJiwPnM38wWznkJGOcIyIYwCgYIKoZIzj0EAwIwITEf\nMB0GA1UEAxMWU21hbGxzdGVwIFRlc3QgUm9vdCBDQTAeFw0xODA5MjcxODE4MDla\nFw0yODA5MjQxODE4MDlaMCExHzAdBgNVBAMTFlNtYWxsc3RlcCBUZXN0IFJvb3Qg\nQ0EwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAS15w7dx9zPjCnQ7+RlRkvUXQJN\nFjk5Hg5K9nCoiiNQQhcQMw63/pXQxHNsugiMshcN59XJC8195KJPm25nXN8co0Uw\nQzAOBgNVHQ8BAf8EBAMCAaYwEgYDVR0TAQH/BAgwBgEB/wIBATAdBgNVHQ4EFgQU\nB2BAXUSPZbFjnY6VzbApV48Tn3owCgYIKoZIzj0EAwIDSQAwRgIhAJRTVmc2xW8c\nESx4oIp2d/OX9KBZzpcNi9fHnnJCS0FXAiEA7OpFb2+b8KBzg1c02x21PS7pHoET\n/A8LXNH4M06A7vE=\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "authority/provisioner/testdata/certs/rsa.csr",
    "content": "-----BEGIN CERTIFICATE REQUEST-----\nMIICdDCCAVwCAQAwDjEMMAoGA1UEAxMDZm9vMIIBIjANBgkqhkiG9w0BAQEFAAOC\nAQ8AMIIBCgKCAQEA86h3t/KJylE0/aPxvF9JqPaOwSsGexuDWqDVJSOWBJi/ZqUA\nEa2Gy05ZIJkQ5GOy0bUs2JCNCVXVkfPrUkX6IvIlXpTjutjMDYyYGdgQjzpKPnOA\nv3mO2a7mLMzJunws7pvrUPP7z5KDCKSAPf6VAcu/na8rGDWn1TUYR8hINK1rLQQf\nOcyNWrr7yLkR84jSsrw/Qgc8NS//F4ccca1NfZecPEtxgcHjKdDQZ3SYRAfb6Dc0\njRuvoByAd3q9okOOr70gpMXgpoFVArDynaHMPK9xJ1w2p3s2/NhOYgY9f9rtcWTo\nafoAcHK1jy5iQCogFUKt1bUCz5IsaYkRt+D+HQIDAQABoCEwHwYJKoZIhvcNAQkO\nMRIwEDAOBgNVHREEBzAFggNmb28wDQYJKoZIhvcNAQELBQADggEBAOsv1UKwEbcY\n8Fj2Pl55BjkqQG4PqSQdWJZfK0ol/GRty5XFaTgOUZyTeXOag84OGw0qM0E7kkUa\nO5QwDOpnmIgg01Ywr4QM166l1iED+eOUscXJMonBAsS3JNYF1JxcDyKzIl/dt9+w\nJXQ64uquuD57amOs8++ROfKW988HzXm0OnoHj8LZ1Mq2yUmxvnnfVnmMpZWo43sA\n8NQs4v9dT5wLByFvBjcaWiGVZwZiwT4Q/Msskv9L0o1On0fgCJ6PjLYdblTwMHDZ\nsyH+X8SsUqeEmyvtiRc1XUeFbxS2hnPXJCXeyfljqwsBNGaVhBXcsV2Lg7IaloBF\n/RyWqQZ44eE=\n-----END CERTIFICATE REQUEST-----\n"
  },
  {
    "path": "authority/provisioner/testdata/certs/short-rsa.csr",
    "content": "-----BEGIN CERTIFICATE REQUEST-----\nMIIBdDCB2wIBADAOMQwwCgYDVQQDEwNmb28wgaIwDQYJKoZIhvcNAQEBBQADgZAA\nMIGMAoGEAK8dks7oV6kcIFEaWna7CDGYPAE8IL7rNi+ruQ1dIYz+JtxT7OPjbCn/\nt5iqni96+35iS/8CvMtEuquOMTMSWOWwlurrbTbLqCazuz/g233o8udxSxhny3cY\nwHogp4cXCX6cFll6DeUnoCEuTTSIu8IBHbK48VfNw4V4gGz6cp/H93HrAgMBAAGg\nITAfBgkqhkiG9w0BCQ4xEjAQMA4GA1UdEQQHMAWCA2ZvbzANBgkqhkiG9w0BAQsF\nAAOBhABCZsYM+Kgje68Z9Fjl2+cBwtQHvZDarh+cz6W1SchinZ1T0aNQvSj/otOe\nttnEF4Rq8zqzr4fbv+AF451Mx36AkfgZr9XWGzxidrH+fBCNWXWNR+ymhrL6UFTG\n2FbarLt9jN2aJLAYQPwtSeGTAZ74tLOPRPnTP6aMfFNg4XCR0uveHA==\n-----END CERTIFICATE REQUEST-----\n"
  },
  {
    "path": "authority/provisioner/testdata/certs/ssh_host_ca_key.pub",
    "content": "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBJj80EJXJR9vxefhdqOLSdzRzBw24t9YKPxb+eCYLf7BU50pJQnB/jK2ZM3qLFbieLaYjngZ86T4DzHxlPAnlAY=\n"
  },
  {
    "path": "authority/provisioner/testdata/certs/ssh_user_ca_key.pub",
    "content": "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBJ8einS88ZaWpcTZG27D5N9JDKfGv0rzjDByLGsZzMsLYl3XcsN9IWKXB6b+5GJ3UaoZf/pFxzRzIdDIh7Ypw3Y=\n"
  },
  {
    "path": "authority/provisioner/testdata/certs/x5c-leaf.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIBuDCCAV+gAwIBAgIQFdu723gqgGaTaqjf6ny88zAKBggqhkjOPQQDAjAcMRow\nGAYDVQQDExFpbnRlcm1lZGlhdGUtdGVzdDAgFw0xOTEwMDIwMzE4NTNaGA8yMTE5\nMDkwODAzMTg1MVowFDESMBAGA1UEAxMJbGVhZi10ZXN0MFkwEwYHKoZIzj0CAQYI\nKoZIzj0DAQcDQgAEaV6807GhWEtMxA39zjuMVHAiN2/Ri5B1R1s+Y/8mlrKIvuvr\nVpgSPXYruNRFduPWX564Abz/TDmb276JbKGeQqOBiDCBhTAOBgNVHQ8BAf8EBAMC\nBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMB0GA1UdDgQWBBReMkPW\nf4MNWdg7KN4xI4ZLJd0IJDAfBgNVHSMEGDAWgBSckDGJlzLaJsdy698XH32gPDMp\nczAUBgNVHREEDTALgglsZWFmLXRlc3QwCgYIKoZIzj0EAwIDRwAwRAIgKYLKXpTN\nwtvZZaIvDzq1p8MO/SZ8yI42Ot69dNk/QtkCIBSvg5PozYcfbvwkgX5SwsjfYu0Z\nAvUgkUQ2G25NBRmX\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIBtjCCAVygAwIBAgIQNr+f4IkABY2n4wx4sLOMrTAKBggqhkjOPQQDAjAUMRIw\nEAYDVQQDEwlyb290LXRlc3QwIBcNMTkxMDAyMDI0MDM0WhgPMjExOTA5MDgwMjQw\nMzJaMBwxGjAYBgNVBAMTEWludGVybWVkaWF0ZS10ZXN0MFkwEwYHKoZIzj0CAQYI\nKoZIzj0DAQcDQgAEflfRhPjgJXv4zsPWahXjM2UU61aRFErN0iw88ZPyxea22fxl\nqN9ezntTXxzsS+mZiWapl8B40ACJgvP+WLQBHKOBhTCBgjAOBgNVHQ8BAf8EBAMC\nAQYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUnJAxiZcy2ibHcuvfFx99\noDwzKXMwHwYDVR0jBBgwFoAUpHS7FfaQ5bCrTxUeu6R2ZC3VGOowHAYDVR0RBBUw\nE4IRaW50ZXJtZWRpYXRlLXRlc3QwCgYIKoZIzj0EAwIDSAAwRQIgII8XpQ8ezDO1\n2xdq3hShf155C5X/5jO8qr0VyEJgzlkCIQCTqph1Gwu/dmuf6dYLCfQqJyb371LC\nlgsqsR63is+0YQ==\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "authority/provisioner/testdata/certs/yubico-piv-ca.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIDFzCCAf+gAwIBAgIDBAZHMA0GCSqGSIb3DQEBCwUAMCsxKTAnBgNVBAMMIFl1\nYmljbyBQSVYgUm9vdCBDQSBTZXJpYWwgMjYzNzUxMCAXDTE2MDMxNDAwMDAwMFoY\nDzIwNTIwNDE3MDAwMDAwWjArMSkwJwYDVQQDDCBZdWJpY28gUElWIFJvb3QgQ0Eg\nU2VyaWFsIDI2Mzc1MTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMN2\ncMTNR6YCdcTFRxuPy31PabRn5m6pJ+nSE0HRWpoaM8fc8wHC+Tmb98jmNvhWNE2E\nilU85uYKfEFP9d6Q2GmytqBnxZsAa3KqZiCCx2LwQ4iYEOb1llgotVr/whEpdVOq\njoU0P5e1j1y7OfwOvky/+AXIN/9Xp0VFlYRk2tQ9GcdYKDmqU+db9iKwpAzid4oH\nBVLIhmD3pvkWaRA2H3DA9t7H/HNq5v3OiO1jyLZeKqZoMbPObrxqDg+9fOdShzgf\nwCqgT3XVmTeiwvBSTctyi9mHQfYd2DwkaqxRnLbNVyK9zl+DzjSGp9IhVPiVtGet\nX02dxhQnGS7K6BO0Qe8CAwEAAaNCMEAwHQYDVR0OBBYEFMpfyvLEojGc6SJf8ez0\n1d8Cv4O/MA8GA1UdEwQIMAYBAf8CAQEwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3\nDQEBCwUAA4IBAQBc7Ih8Bc1fkC+FyN1fhjWioBCMr3vjneh7MLbA6kSoyWF70N3s\nXhbXvT4eRh0hvxqvMZNjPU/VlRn6gLVtoEikDLrYFXN6Hh6Wmyy1GTnspnOvMvz2\nlLKuym9KYdYLDgnj3BeAvzIhVzzYSeU77/Cupofj093OuAswW0jYvXsGTyix6B3d\nbW5yWvyS9zNXaqGaUmP3U9/b6DlHdDogMLu3VLpBB9bm5bjaKWWJYgWltCVgUbFq\nFqyi4+JE014cSgR57Jcu3dZiehB6UtAPgad9L5cNvua/IWRmm+ANy3O2LH++Pyl8\nSREzU8onbBsjMg9QDiSf5oJLKvd/Ren+zGY7\n-----END CERTIFICATE-----"
  },
  {
    "path": "authority/provisioner/testdata/secrets/bar.priv",
    "content": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIM8wGIzCKjAOGdBFmYHtS791Ly2I9FtmknEsR2sa63s7oAoGCCqGSM49\nAwEHoUQDQgAEGQIXbsr73X28pzwC1wa+ccY2H3s8PplbkapCrwxyYVvM78y/GmeS\nA7fv2MKQ8iKpCw461MlOQGX+VlWT+ChRFw==\n-----END EC PRIVATE KEY-----\n"
  },
  {
    "path": "authority/provisioner/testdata/secrets/bar_host_ssh_key",
    "content": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIHzAUYu3h8e1gL5ONGZo+lghJJa9rl1TvP2UlqDXazxvoAoGCCqGSM49\nAwEHoUQDQgAEOLScS+1Yzmqdyots9lSC0tzTSXUXEgyOD9wYrQ0BqnVZtBXlQw1p\nm3fnF/7Ehl6bD1YZWjrF1t+IBZQMq1uBBw==\n-----END EC PRIVATE KEY-----\n"
  },
  {
    "path": "authority/provisioner/testdata/secrets/ecdsa.key",
    "content": "-----BEGIN EC PRIVATE KEY-----\nProc-Type: 4,ENCRYPTED\nDEK-Info: AES-256-CBC,54abd40e525b255542ee6161ec438721\n\nfJvmEc5n0IG4t4FKF+ekKhpog4ods2nZjBR5KLkGH5oSGAOEADSXIRBK76Jnm/nz\nKv8ZwGqxNnoJUQyeTMlyg5OnOUAQPyNBPvoItOlD2DP32WJXgQ+NSHB2h9pcBGYG\nyLWrCtzl9/P9REWskanPO4RujP27Ht62omcMO7SxxNI=\n-----END EC PRIVATE KEY-----\n"
  },
  {
    "path": "authority/provisioner/testdata/secrets/ed25519.key",
    "content": "-----BEGIN ENCRYPTED PRIVATE KEY-----\nMIGkMGAGCSqGSIb3DQEFDTBTMDIGCSqGSIb3DQEFDDAlBBDJ0vCXdpPyUiLlbge5\n1g0jAgMBhqAwDAYIKoZIhvcNAgkAADAdBglghkgBZQMEASoEENtOknzU2eS2mlxl\n73Yo/IoEQEyJS2EEx3+oYaKlFIB90e1Zkmi8da7d3r2iUlfc7faRAiKChcEvtEas\nvYF2l9LEZ9DXv1Rm1uyNuSpXuddHScE=\n-----END ENCRYPTED PRIVATE KEY-----\n"
  },
  {
    "path": "authority/provisioner/testdata/secrets/foo.key",
    "content": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIJmnxm3N/ahRA2PWeZhRGJUKPU1lI44WcE4P1bynIim6oAoGCCqGSM49\nAwEHoUQDQgAEG6bXw6JxWnlD4k6+dsJfa93XM4K3qJcA0ZrIMhYJ69RLih+elkXI\nB9YI3y9KcmxZTtz3YAbpEeLvrL3qt+NzgA==\n-----END EC PRIVATE KEY-----\n"
  },
  {
    "path": "authority/provisioner/testdata/secrets/foo.priv",
    "content": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIB8ovUg0Atvz+b+XiF8QV722OivOm1geGtI3sP0F48N1oAoGCCqGSM49\nAwEHoUQDQgAEzriaeV2e1aEz33x62kyqVC6ootU7rl41L8cyeOJ4SjTu4FV+o5i4\nNsS6DCE07JJSHlqc9PsrzjSs4LZD4gWVLQ==\n-----END EC PRIVATE KEY-----\n"
  },
  {
    "path": "authority/provisioner/testdata/secrets/foo_user_ssh_key",
    "content": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEINWGD2xneE43YeytQzORItISxv6d/oH+9TXvDKHo6TyXoAoGCCqGSM49\nAwEHoUQDQgAEVK/EtXgVV7+7ppnQSjCtI5qb/gIGnQUF4i//F/JKKho7kRNyMDSn\nBP3kndiv8Yfxg4PsyIRY5ZofbEo5eJE6bg==\n-----END EC PRIVATE KEY-----\n"
  },
  {
    "path": "authority/provisioner/testdata/secrets/rsa.key",
    "content": "-----BEGIN RSA PRIVATE KEY-----\nProc-Type: 4,ENCRYPTED\nDEK-Info: AES-256-CBC,e77ed7e2d2572b5a246a1c4b994190bb\n\n29o7UA/L7OF7inTPKkwBrtd8CJdXVQs9R3oJPitmFk8SZbrLHEiEhF1C0uB3xK3s\nGWQ9O7bjERM3uAvQkd7MSkUUBpyPXS9GFacd85e85d1Ubl6miTAwkFrQnT9yn6n/\nFak5JtkmdB6ObfVTioOwT1jdtGTifKg1bIhYISgwqCWhgV2fUFk6HQAAIMXTTRc+\nZK1WbunT7LimYrnN3gQ4ylm/4C8nQl3JCGpvWZaRoH91q1LLD6IwWmX0D09F37dU\nX3KoKv/GvuDlV3H1dUBwxhU+GI5/lItPp9OcdLZnnr67Gs+X+do3MFT1h675TM4N\nc9QEIJB6RYatLBKHCS7j8W7EbuJAFZ+MCCapP92ERmVVVsWPY+V7CDVQxM2v4X/w\n7C7JYx8b4xuQbdvu9KVU5irsXg8hBx7kDb/mtWjT4+8+sseLKA4oOmI6XwVMdbow\nMciGilAIaNtWwQHe0EK9E9tiQfc9OyzxdrfplRckAAehHuPGU7+iMCsigCLT3aiV\nCDHmnLdTXKIvGe8faTQoJphrb9F8bobGo5D4ZqX5f6gKuPIJsfd/r0GD8VNSF/Q7\nSJQMhkVyaixFB0gQbmea7sTyScdW+Qne7nLpam3ISgo+G4CAH8W88wLnuHMLmvoC\nZE3HvArSeQZ0WPHgB86AfoNRIxd6Emgb+dFyA6wPJC29nZkB8PFSrAHp0zp7KilF\nfe9K2dVAUBZFhQthQIYAjJmYLCukLhxUALiqdSmQZrt6DSE33K8s5ed2KJu/60G6\nlZwIzQHPXesRhwmwbkfPB8CyWM+L6osdWv8QyMdM8Wb+66zkhKWBNbm+ccMfP6Zf\n1ynF/a/DRX8bf81w+nvLsCGTdxVuEVEpuzS1NclKTmYQu58Ol0RgQe2JSxL89n+A\nJAHUu9g9LcTg2jNPjxeA/vusSXMZRrPqrUCYhHhcgR4mE13uyyFI/9frk0gPpKXp\n/FislMydWov2JRp1ixzypMBqlFR/zF6j6m3P1g7gchwScWzrZQHD58xdRin4Udiv\nOR4huswh5v2i/0KozBoUAwbvPGERnMlTaGoBMPJ5Xe/jkBJw3uC3Dhi74uyUCjqU\nhMQW4RJKmuiZVfAIX0RdgeUWXPs+8pf2pXrpIiVHCAHDrxXNMC7X6/9EcBN15B88\nW5/KIRngDeB2oVYrn1GfO7iLu1Rd8VFXyaVItOXq7WrL2pwm8ANhWcFDdnXf6jHW\nBcKss1j8rZxOchksf+ZPXhn3QkdooD9iVONky1zLIsV5GPwMe8+yXwXznzJSbHH7\ndOfhK93fZqUwx4gFULwCuWIwLTfNmQ3VzdKioGt39RFDVQb+pbR7p9jv899VjsVO\nTBBpRa00fvbK1H2CMVHnwwIf82M4XypSNGR/tSD3AImZPb5RfZnznoXXCMEfYVsd\n8/Ry4GHusA+zxCjCxHFtXVkb9sklewJtnUmN5mUzo/81szuigLB5IADR21IOyVBq\nA4kz96Ta885Z5owhonfZp1HD53pDEbxCuuIy+fgYfjfDSAj3L/QT3ZKrdcIdYQap\nPhrNRW3j38koAatTLd3+E9KqBO5BiY+T5h+Q3XesWnaXInfu5WKiiEm5hHiejA0C\n-----END RSA PRIVATE KEY-----\n"
  },
  {
    "path": "authority/provisioner/testdata/secrets/ssh_host_ca_key",
    "content": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIKZCgb5pTSSCbr/xcHCOkl9O6tQtZmNahr3Ap3/c2nBLoAoGCCqGSM49\nAwEHoUQDQgAEmPzQQlclH2/F5+F2o4tJ3NHMHDbi31go/Fv54Jgt/sFTnSklCcH+\nMrZkzeosVuJ4tpiOeBnzpPgPMfGU8CeUBg==\n-----END EC PRIVATE KEY-----\n"
  },
  {
    "path": "authority/provisioner/testdata/secrets/ssh_user_ca_key",
    "content": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIDuzykyPM6rLnSoyF4jnOpPAlyKZERqtaB8PTh179DMgoAoGCCqGSM49\nAwEHoUQDQgAEnx6KdLzxlpalxNkbbsPk30kMp8a/SvOMMHIsaxnMywtiXddyw30h\nYpcHpv7kYndRqhl/+kXHNHMh0MiHtinDdg==\n-----END EC PRIVATE KEY-----\n"
  },
  {
    "path": "authority/provisioner/testdata/secrets/x5c-leaf.key",
    "content": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIALytC4LyTTAagMLMv+rzq2vtfhFkhuyBz4kqsnRs6zioAoGCCqGSM49\nAwEHoUQDQgAEaV6807GhWEtMxA39zjuMVHAiN2/Ri5B1R1s+Y/8mlrKIvuvrVpgS\nPXYruNRFduPWX564Abz/TDmb276JbKGeQg==\n-----END EC PRIVATE KEY-----\n"
  },
  {
    "path": "authority/provisioner/testdata/templates/cr.tpl",
    "content": "{{ toJson .Insecure.CR }}"
  },
  {
    "path": "authority/provisioner/timeduration.go",
    "content": "package provisioner\n\nimport (\n\t\"encoding/json\"\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n)\n\nvar now = func() time.Time {\n\treturn time.Now().UTC()\n}\n\n// timeOr returns the first of its arguments that is not equal to the zero time.\n// This method can be replaced with cmp.Or when step-ca requires Go 1.22.\nfunc timeOr(ts ...time.Time) time.Time {\n\tfor _, t := range ts {\n\t\tif !t.IsZero() {\n\t\t\treturn t\n\t\t}\n\t}\n\treturn time.Time{}\n}\n\n// TimeDuration is a type that represents a time but the JSON unmarshaling can\n// use a time using the RFC 3339 format or a time.Duration string. If a duration\n// is used, the time will be set on the first call to TimeDuration.Time.\ntype TimeDuration struct {\n\tt time.Time\n\td time.Duration\n}\n\n// NewTimeDuration returns a TimeDuration with the defined time.\nfunc NewTimeDuration(t time.Time) TimeDuration {\n\treturn TimeDuration{t: t}\n}\n\n// ParseTimeDuration returns a new TimeDuration parsing the RFC 3339 time or\n// time.Duration string.\nfunc ParseTimeDuration(s string) (TimeDuration, error) {\n\tif s == \"\" {\n\t\treturn TimeDuration{}, nil\n\t}\n\n\t// Try to use the unquoted RFC 3339 format\n\tvar t time.Time\n\tif err := t.UnmarshalText([]byte(s)); err == nil {\n\t\treturn TimeDuration{t: t.UTC()}, nil\n\t}\n\n\t// Try to use the time.Duration string format\n\tif d, err := time.ParseDuration(s); err == nil {\n\t\treturn TimeDuration{d: d}, nil\n\t}\n\n\treturn TimeDuration{}, errors.Errorf(\"failed to parse %s\", s)\n}\n\n// SetDuration initializes the TimeDuration with the given duration string. If\n// the time was set it will re-set to zero.\nfunc (t *TimeDuration) SetDuration(d time.Duration) {\n\tt.t, t.d = time.Time{}, d\n}\n\n// SetTime initializes the TimeDuration with the given time. If the duration is\n// set it will be re-set to zero.\nfunc (t *TimeDuration) SetTime(tt time.Time) {\n\tt.t, t.d = tt, 0\n}\n\n// IsZero returns true the TimeDuration represents the zero value, false\n// otherwise.\nfunc (t *TimeDuration) IsZero() bool {\n\treturn t.t.IsZero() && t.d == 0\n}\n\n// Equal returns if t and other are equal.\nfunc (t *TimeDuration) Equal(other *TimeDuration) bool {\n\treturn t.t.Equal(other.t) && t.d == other.d\n}\n\n// MarshalJSON implements the json.Marshaler interface. If the time is set it\n// will return the time in RFC 3339 format if not it will return the duration\n// string.\nfunc (t TimeDuration) MarshalJSON() ([]byte, error) {\n\tswitch {\n\tcase t.t.IsZero():\n\t\tif t.d == 0 {\n\t\t\treturn []byte(`\"\"`), nil\n\t\t}\n\t\treturn json.Marshal(t.d.String())\n\tdefault:\n\t\treturn t.t.MarshalJSON()\n\t}\n}\n\n// UnmarshalJSON implements the json.Unmarshaler interface. The time is expected\n// to be a quoted string in RFC 3339 format or a quoted time.Duration string.\nfunc (t *TimeDuration) UnmarshalJSON(data []byte) error {\n\tvar s string\n\tif err := json.Unmarshal(data, &s); err != nil {\n\t\treturn errors.Wrapf(err, \"error unmarshaling %s\", data)\n\t}\n\n\t// Empty TimeDuration\n\tif s == \"\" {\n\t\t*t = TimeDuration{}\n\t\treturn nil\n\t}\n\n\t// Try to use the unquoted RFC 3339 format\n\tvar tt time.Time\n\tif err := tt.UnmarshalText([]byte(s)); err == nil {\n\t\t*t = TimeDuration{t: tt}\n\t\treturn nil\n\t}\n\n\t// Try to use the time.Duration string format\n\tif d, err := time.ParseDuration(s); err == nil {\n\t\t*t = TimeDuration{d: d}\n\t\treturn nil\n\t}\n\n\treturn errors.Errorf(\"failed to parse %s\", data)\n}\n\n// Time calculates the time if needed and returns it.\nfunc (t *TimeDuration) Time() time.Time {\n\treturn t.RelativeTime(now())\n}\n\n// Unix calculates the time if needed it and returns the Unix time in seconds.\nfunc (t *TimeDuration) Unix() int64 {\n\treturn t.RelativeTime(now()).Unix()\n}\n\n// RelativeTime returns the embedded time.Time or the base time plus the\n// duration if this is not zero.\nfunc (t *TimeDuration) RelativeTime(base time.Time) time.Time {\n\tswitch {\n\tcase t == nil:\n\t\treturn time.Time{}\n\tcase t.t.IsZero():\n\t\tif t.d == 0 {\n\t\t\treturn time.Time{}\n\t\t}\n\t\tt.t = base.Add(t.d)\n\t\treturn t.t.UTC()\n\tdefault:\n\t\treturn t.t.UTC()\n\t}\n}\n\n// String implements the fmt.Stringer interface.\nfunc (t *TimeDuration) String() string {\n\treturn t.Time().String()\n}\n"
  },
  {
    "path": "authority/provisioner/timeduration_test.go",
    "content": "package provisioner\n\nimport (\n\t\"reflect\"\n\t\"testing\"\n\t\"time\"\n)\n\nfunc mockNow() (time.Time, func()) {\n\ttm := time.Unix(1584198566, 535897000).UTC()\n\tnowFn := now\n\tnow = func() time.Time {\n\t\treturn tm\n\t}\n\treturn tm, func() {\n\t\tnow = nowFn\n\t}\n}\n\nfunc TestNewTimeDuration(t *testing.T) {\n\ttm := time.Unix(1584198566, 535897000).UTC()\n\ttype args struct {\n\t\tt time.Time\n\t}\n\ttests := []struct {\n\t\tname string\n\t\targs args\n\t\twant TimeDuration\n\t}{\n\t\t{\"ok\", args{tm}, TimeDuration{t: tm}},\n\t\t{\"zero\", args{time.Time{}}, TimeDuration{}},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif got := NewTimeDuration(tt.args.t); !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"NewTimeDuration() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestParseTimeDuration(t *testing.T) {\n\ttype args struct {\n\t\ts string\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\targs    args\n\t\twant    TimeDuration\n\t\twantErr bool\n\t}{\n\t\t{\"timestamp\", args{\"2020-03-14T15:09:26.535897Z\"}, TimeDuration{t: time.Unix(1584198566, 535897000).UTC()}, false},\n\t\t{\"timestamp\", args{\"2020-03-14T15:09:26Z\"}, TimeDuration{t: time.Unix(1584198566, 0).UTC()}, false},\n\t\t{\"timestamp\", args{\"2020-03-14T15:09:26.535897-07:00\"}, TimeDuration{t: time.Unix(1584223766, 535897000).UTC()}, false},\n\t\t{\"timestamp\", args{\"2020-03-14T15:09:26-07:00\"}, TimeDuration{t: time.Unix(1584223766, 0).UTC()}, false},\n\t\t{\"timestamp\", args{\"2020-03-14T15:09:26.535897+07:00\"}, TimeDuration{t: time.Unix(1584173366, 535897000).UTC()}, false},\n\t\t{\"timestamp\", args{\"2020-03-14T15:09:26+07:00\"}, TimeDuration{t: time.Unix(1584173366, 0).UTC()}, false},\n\t\t{\"1h\", args{\"1h\"}, TimeDuration{d: 1 * time.Hour}, false},\n\t\t{\"-24h60m60s\", args{\"-24h60m60s\"}, TimeDuration{d: -24*time.Hour - 60*time.Minute - 60*time.Second}, false},\n\t\t{\"0\", args{\"0\"}, TimeDuration{}, false},\n\t\t{\"empty\", args{\"\"}, TimeDuration{}, false},\n\t\t{\"fail\", args{\"2020-03-14T15:09:26Z07:00\"}, TimeDuration{}, true},\n\t\t{\"fail\", args{\"1d\"}, TimeDuration{}, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := ParseTimeDuration(tt.args.s)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"ParseTimeDuration() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"ParseTimeDuration() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestTimeDuration_SetDuration(t *testing.T) {\n\ttype fields struct {\n\t\tt time.Time\n\t\td time.Duration\n\t}\n\ttype args struct {\n\t\td time.Duration\n\t}\n\ttests := []struct {\n\t\tname   string\n\t\tfields fields\n\t\targs   args\n\t\twant   *TimeDuration\n\t}{\n\t\t{\"new\", fields{}, args{2 * time.Hour}, &TimeDuration{d: 2 * time.Hour}},\n\t\t{\"old\", fields{time.Now(), 1 * time.Hour}, args{2 * time.Hour}, &TimeDuration{d: 2 * time.Hour}},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ttd := &TimeDuration{\n\t\t\t\tt: tt.fields.t,\n\t\t\t\td: tt.fields.d,\n\t\t\t}\n\t\t\ttd.SetDuration(tt.args.d)\n\t\t\tif !reflect.DeepEqual(td, tt.want) {\n\t\t\t\tt.Errorf(\"SetDuration() = %v, want %v\", td, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestTimeDuration_SetTime(t *testing.T) {\n\ttm := time.Unix(1584198566, 535897000).UTC()\n\n\ttype fields struct {\n\t\tt time.Time\n\t\td time.Duration\n\t}\n\ttype args struct {\n\t\ttt time.Time\n\t}\n\ttests := []struct {\n\t\tname   string\n\t\tfields fields\n\t\targs   args\n\t\twant   *TimeDuration\n\t}{\n\t\t{\"new\", fields{}, args{tm}, &TimeDuration{t: tm}},\n\t\t{\"old\", fields{time.Now(), 1 * time.Hour}, args{tm}, &TimeDuration{t: tm}},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ttd := &TimeDuration{\n\t\t\t\tt: tt.fields.t,\n\t\t\t\td: tt.fields.d,\n\t\t\t}\n\t\t\ttd.SetTime(tt.args.tt)\n\t\t\tif !reflect.DeepEqual(td, tt.want) {\n\t\t\t\tt.Errorf(\"SetTime() = %v, want %v\", td, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestTimeDuration_MarshalJSON(t *testing.T) {\n\ttm := time.Unix(1584198566, 535897000).UTC()\n\ttests := []struct {\n\t\tname         string\n\t\ttimeDuration TimeDuration\n\t\twant         []byte\n\t\twantErr      bool\n\t}{\n\t\t{\"empty\", TimeDuration{}, []byte(`\"\"`), false},\n\t\t{\"timestamp\", TimeDuration{t: tm}, []byte(`\"2020-03-14T15:09:26.535897Z\"`), false},\n\t\t{\"duration\", TimeDuration{d: 1 * time.Hour}, []byte(`\"1h0m0s\"`), false},\n\t\t{\"fail\", TimeDuration{t: time.Date(-1, 0, 0, 0, 0, 0, 0, time.UTC)}, nil, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := tt.timeDuration.MarshalJSON()\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"TimeDuration.MarshalJSON() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"TimeDuration.MarshalJSON() = %s, want %s\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestTimeDuration_UnmarshalJSON(t *testing.T) {\n\ttype args struct {\n\t\tdata []byte\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\targs    args\n\t\twant    *TimeDuration\n\t\twantErr bool\n\t}{\n\t\t{\"empty\", args{[]byte(`\"\"`)}, &TimeDuration{}, false},\n\t\t{\"timestamp\", args{[]byte(`\"2020-03-14T15:09:26.535897Z\"`)}, &TimeDuration{t: time.Unix(1584198566, 535897000).UTC()}, false},\n\t\t{\"duration\", args{[]byte(`\"1h\"`)}, &TimeDuration{d: time.Hour}, false},\n\t\t{\"fail\", args{[]byte(\"123\")}, &TimeDuration{}, true},\n\t\t{\"fail\", args{[]byte(`\"2020-03-14T15:09:26.535897Z07:00\"`)}, &TimeDuration{}, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ttd := &TimeDuration{}\n\t\t\tif err := td.UnmarshalJSON(tt.args.data); (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"TimeDuration.UnmarshalJSON() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(td, tt.want) {\n\t\t\t\tt.Errorf(\"TimeDuration.UnmarshalJSON() = %s, want %s\", td, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestTimeDuration_Time(t *testing.T) {\n\ttm, fn := mockNow()\n\tdefer fn()\n\ttests := []struct {\n\t\tname         string\n\t\ttimeDuration *TimeDuration\n\t\twant         time.Time\n\t}{\n\t\t{\"zero\", nil, time.Time{}},\n\t\t{\"zero\", &TimeDuration{}, time.Time{}},\n\t\t{\"timestamp\", &TimeDuration{t: tm}, tm},\n\t\t{\"local\", &TimeDuration{t: tm.Local()}, tm},\n\t\t{\"duration\", &TimeDuration{d: 1 * time.Hour}, tm.Add(1 * time.Hour)},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot := tt.timeDuration.Time()\n\t\t\tif !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"TimeDuration.Time() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestTimeDuration_Unix(t *testing.T) {\n\ttm, fn := mockNow()\n\tdefer fn()\n\ttests := []struct {\n\t\tname         string\n\t\ttimeDuration *TimeDuration\n\t\twant         int64\n\t}{\n\t\t{\"zero\", nil, -62135596800},\n\t\t{\"zero\", &TimeDuration{}, -62135596800},\n\t\t{\"timestamp\", &TimeDuration{t: tm}, 1584198566},\n\t\t{\"local\", &TimeDuration{t: tm.Local()}, 1584198566},\n\t\t{\"duration\", &TimeDuration{d: 1 * time.Hour}, 1584202166},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot := tt.timeDuration.Unix()\n\t\t\tif !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"TimeDuration.Unix() = %v, want %v\", got, tt.want)\n\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestTimeDuration_String(t *testing.T) {\n\ttm, fn := mockNow()\n\tdefer fn()\n\ttests := []struct {\n\t\tname         string\n\t\ttimeDuration *TimeDuration\n\t\twant         string\n\t}{\n\t\t{\"zero\", nil, \"0001-01-01 00:00:00 +0000 UTC\"},\n\t\t{\"zero\", &TimeDuration{}, \"0001-01-01 00:00:00 +0000 UTC\"},\n\t\t{\"timestamp\", &TimeDuration{t: tm}, \"2020-03-14 15:09:26.535897 +0000 UTC\"},\n\t\t{\"duration\", &TimeDuration{d: 1 * time.Hour}, \"2020-03-14 16:09:26.535897 +0000 UTC\"},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif got := tt.timeDuration.String(); got != tt.want {\n\t\t\t\tt.Errorf(\"TimeDuration.String() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "authority/provisioner/utils_test.go",
    "content": "package provisioner\n\nimport (\n\t\"crypto\"\n\t\"crypto/rand\"\n\t\"crypto/sha256\"\n\t\"crypto/x509\"\n\t\"encoding/base64\"\n\t\"encoding/hex\"\n\t\"encoding/json\"\n\t\"encoding/pem\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"os\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n\t\"go.step.sm/crypto/jose\"\n\t\"go.step.sm/crypto/pemutil\"\n\t\"go.step.sm/crypto/randutil\"\n\t\"golang.org/x/crypto/ssh\"\n\n\t\"github.com/smallstep/certificates/authority/provisioner/gcp\"\n)\n\nvar (\n\tdefaultDisableRenewal             = false\n\tdefaultAllowRenewalAfterExpiry    = false\n\tdefaultEnableSSHCA                = true\n\tdefaultDisableSmallstepExtensions = false\n\tglobalProvisionerClaims           = Claims{\n\t\tMinTLSDur:                  &Duration{5 * time.Minute},\n\t\tMaxTLSDur:                  &Duration{24 * time.Hour},\n\t\tDefaultTLSDur:              &Duration{24 * time.Hour},\n\t\tMinUserSSHDur:              &Duration{Duration: 5 * time.Minute}, // User SSH certs\n\t\tMaxUserSSHDur:              &Duration{Duration: 24 * time.Hour},\n\t\tDefaultUserSSHDur:          &Duration{Duration: 16 * time.Hour},\n\t\tMinHostSSHDur:              &Duration{Duration: 5 * time.Minute}, // Host SSH certs\n\t\tMaxHostSSHDur:              &Duration{Duration: 30 * 24 * time.Hour},\n\t\tDefaultHostSSHDur:          &Duration{Duration: 30 * 24 * time.Hour},\n\t\tEnableSSHCA:                &defaultEnableSSHCA,\n\t\tDisableRenewal:             &defaultDisableRenewal,\n\t\tAllowRenewalAfterExpiry:    &defaultAllowRenewalAfterExpiry,\n\t\tDisableSmallstepExtensions: &defaultDisableSmallstepExtensions,\n\t}\n\ttestAudiences = Audiences{\n\t\tSign:      []string{\"https://ca.smallstep.com/1.0/sign\", \"https://ca.smallstep.com/sign\"},\n\t\tRevoke:    []string{\"https://ca.smallstep.com/1.0/revoke\", \"https://ca.smallstep.com/revoke\"},\n\t\tSSHSign:   []string{\"https://ca.smallstep.com/1.0/ssh/sign\"},\n\t\tSSHRevoke: []string{\"https://ca.smallstep.com/1.0/ssh/revoke\"},\n\t\tSSHRenew:  []string{\"https://ca.smallstep.com/1.0/ssh/renew\"},\n\t\tSSHRekey:  []string{\"https://ca.smallstep.com/1.0/ssh/rekey\"},\n\t}\n)\n\nconst awsTestCertificate = `-----BEGIN CERTIFICATE-----\nMIICFTCCAX6gAwIBAgIRAKmbVVYAl/1XEqRfF3eJ97MwDQYJKoZIhvcNAQELBQAw\nGDEWMBQGA1UEAxMNQVdTIFRlc3QgQ2VydDAeFw0xOTA0MjQyMjU3MzlaFw0yOTA0\nMjEyMjU3MzlaMBgxFjAUBgNVBAMTDUFXUyBUZXN0IENlcnQwgZ8wDQYJKoZIhvcN\nAQEBBQADgY0AMIGJAoGBAOHMmMXwbXN90SoRl/xXAcJs5TacaVYJ5iNAVWM5KYyF\n+JwqYuJp/umLztFUi0oX0luu3EzD4KurVeUJSzZjTFTX1d/NX6hA45+bvdSUOcgV\nUghO+2uhBZ4SNFxFRZ7SKvoWIN195l5bVX6/60Eo6+kUCKCkyxW4V/ksWzdXjHnf\nAgMBAAGjXzBdMA4GA1UdDwEB/wQEAwIBBjASBgNVHRMBAf8ECDAGAQH/AgEBMB0G\nA1UdDgQWBBRHfLOjEddK/CWCIHNg8Oc/oJa1IzAYBgNVHREEETAPgg1BV1MgVGVz\ndCBDZXJ0MA0GCSqGSIb3DQEBCwUAA4GBAKNCiVM9eGb9dW2xNyHaHAmmy7ERB2OJ\n7oXHfLjooOavk9lU/Gs2jfX/JSBa84+DzWg9ShmCNLti8CxU/dhzXW7jE/5CcdTa\nDCA6B3Yl5TmfG9+D9dtFqRB2CiMgNcsJJE5Dc6pDwBIiSj/MkE0AaGVQmSwn6Cb6\nvX1TAxqeWJHq\n-----END CERTIFICATE-----`\n\nconst awsTestKey = `-----BEGIN RSA PRIVATE KEY-----\nMIICXAIBAAKBgQDhzJjF8G1zfdEqEZf8VwHCbOU2nGlWCeYjQFVjOSmMhficKmLi\naf7pi87RVItKF9JbrtxMw+Crq1XlCUs2Y0xU19XfzV+oQOOfm73UlDnIFVIITvtr\noQWeEjRcRUWe0ir6FiDdfeZeW1V+v+tBKOvpFAigpMsVuFf5LFs3V4x53wIDAQAB\nAoGADZQFF9oWatyFCHeYYSdGRs/PlNIhD3h262XB/L6CPh4MTi/KVH01RAwROstP\nuPvnvXWtb7xTtV8PQj+l0zZzb4W/DLCSBdoRwpuNXyffUCtbI22jPupTsVu+ENWR\n3x7HHzoZYjU45ADSTMxEtwD7/zyNgpRKjIA2HYpkt+fI27ECQQD5/AOr9/yQD73x\ncquF+FWahWgDL25YeMwdfe1HfpUxUxd9kJJKieB8E2BtBAv9XNguxIBpf7VlAKsF\nNFhdfWFHAkEA5zuX8vqDecSzyNNEQd3tugxt1pGOXNesHzuPbdlw3ppN9Rbd93an\nuU2TaAvTjr/3EkxulYNRmHs+RSVK54+uqQJAKWurhBQMAibJlzcj2ofiTz8pk9WJ\nGBmz4HMcHMuJlumoq8KHqtgbnRNs18Ni5TE8FMu0Z0ak3L52l98rgRokQwJBAJS8\n9KTLF79AFBVeME3eH4jJbe3TeyulX4ZHnZ8fe0b1IqhAqU8A+CpuCB+pW9A7Ewam\nO4vZCKd4vzljH6eL+OECQHHxhYoTW7lFpKGnUDG9fPZ3eYzWpgka6w1vvBk10BAu\n6fbwppM9pQ7DPMg7V6YGEjjT0gX9B9TttfHxGhvtZNQ=\n-----END RSA PRIVATE KEY-----`\n\nfunc must(args ...interface{}) []interface{} {\n\tif l := len(args); l > 0 && args[l-1] != nil {\n\t\tif err, ok := args[l-1].(error); ok {\n\t\t\tpanic(err)\n\t\t}\n\t}\n\treturn args\n}\n\nfunc generateJSONWebKey() (*jose.JSONWebKey, error) {\n\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tfp, err := jwk.Thumbprint(crypto.SHA256)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tjwk.KeyID = hex.EncodeToString(fp)\n\treturn jwk, nil\n}\n\nfunc generateJSONWebKeySet(n int) (jose.JSONWebKeySet, error) {\n\tvar keySet jose.JSONWebKeySet\n\tfor i := 0; i < n; i++ {\n\t\tkey, err := generateJSONWebKey()\n\t\tif err != nil {\n\t\t\treturn jose.JSONWebKeySet{}, err\n\t\t}\n\t\tkeySet.Keys = append(keySet.Keys, *key)\n\t}\n\treturn keySet, nil\n}\n\nfunc encryptJSONWebKey(jwk *jose.JSONWebKey) (*jose.JSONWebEncryption, error) {\n\tb, err := json.Marshal(jwk)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tsalt, err := randutil.Salt(jose.PBKDF2SaltSize)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\topts := new(jose.EncrypterOptions)\n\topts.WithContentType(jose.ContentType(\"jwk+json\"))\n\trecipient := jose.Recipient{\n\t\tAlgorithm:  jose.PBES2_HS256_A128KW,\n\t\tKey:        []byte(\"password\"),\n\t\tPBES2Count: jose.PBKDF2Iterations,\n\t\tPBES2Salt:  salt,\n\t}\n\tencrypter, err := jose.NewEncrypter(jose.DefaultEncAlgorithm, recipient, opts)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn encrypter.Encrypt(b)\n}\n\nfunc decryptJSONWebKey(key string) (*jose.JSONWebKey, error) {\n\tenc, err := jose.ParseEncrypted(key)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tb, err := enc.Decrypt([]byte(\"password\"))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tjwk := new(jose.JSONWebKey)\n\tif err := json.Unmarshal(b, jwk); err != nil {\n\t\treturn nil, err\n\t}\n\treturn jwk, nil\n}\n\nfunc generateJWK() (*JWK, error) {\n\tname, err := randutil.Alphanumeric(10)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tjwk, err := generateJSONWebKey()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tjwe, err := encryptJSONWebKey(jwk)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tpublic := jwk.Public()\n\tencrypted, err := jwe.CompactSerialize()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tp := &JWK{\n\t\tName:         name,\n\t\tType:         \"JWK\",\n\t\tKey:          &public,\n\t\tEncryptedKey: encrypted,\n\t\tClaims:       &globalProvisionerClaims,\n\t}\n\tp.ctl, err = NewController(p, p.Claims, Config{\n\t\tAudiences: testAudiences,\n\t}, nil)\n\treturn p, err\n}\n\nfunc generateK8sSA(inputPubKey interface{}) (*K8sSA, error) {\n\tfooPubB, err := os.ReadFile(\"./testdata/certs/foo.pub\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tfooPub, err := pemutil.ParseKey(fooPubB)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tbarPubB, err := os.ReadFile(\"./testdata/certs/bar.pub\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tbarPub, err := pemutil.ParseKey(barPubB)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tpubKeys := []interface{}{fooPub, barPub}\n\tif inputPubKey != nil {\n\t\tpubKeys = append(pubKeys, inputPubKey)\n\t}\n\n\tp := &K8sSA{\n\t\tName:    K8sSAName,\n\t\tType:    \"K8sSA\",\n\t\tClaims:  &globalProvisionerClaims,\n\t\tpubKeys: pubKeys,\n\t}\n\tp.ctl, err = NewController(p, p.Claims, Config{\n\t\tAudiences: testAudiences,\n\t}, nil)\n\treturn p, err\n}\n\nfunc generateSSHPOP() (*SSHPOP, error) {\n\tname, err := randutil.Alphanumeric(10)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tuserB, err := os.ReadFile(\"./testdata/certs/ssh_user_ca_key.pub\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tuserKey, _, _, _, err := ssh.ParseAuthorizedKey(userB)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\thostB, err := os.ReadFile(\"./testdata/certs/ssh_host_ca_key.pub\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\thostKey, _, _, _, err := ssh.ParseAuthorizedKey(hostB)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tp := &SSHPOP{\n\t\tName:   name,\n\t\tType:   \"SSHPOP\",\n\t\tClaims: &globalProvisionerClaims,\n\t\tsshPubKeys: &SSHKeys{\n\t\t\tUserKeys: []ssh.PublicKey{userKey},\n\t\t\tHostKeys: []ssh.PublicKey{hostKey},\n\t\t},\n\t}\n\tp.ctl, err = NewController(p, p.Claims, Config{\n\t\tAudiences: testAudiences,\n\t}, nil)\n\treturn p, err\n}\n\nfunc generateX5C(root []byte) (*X5C, error) {\n\tif root == nil {\n\t\troot = []byte(`-----BEGIN CERTIFICATE-----\nMIIBhTCCASqgAwIBAgIRAMalM7pKi0GCdKjO6u88OyowCgYIKoZIzj0EAwIwFDES\nMBAGA1UEAxMJcm9vdC10ZXN0MCAXDTE5MTAwMjAyMzk0OFoYDzIxMTkwOTA4MDIz\nOTQ4WjAUMRIwEAYDVQQDEwlyb290LXRlc3QwWTATBgcqhkjOPQIBBggqhkjOPQMB\nBwNCAAS29QTCXUu7cx9sa9wZPpRSFq/zXaw8Ai3EIygayrBsKnX42U2atBUjcBZO\nBWL6A+PpLzU9ja867U5SYNHERS+Oo1swWTAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0T\nAQH/BAgwBgEB/wIBATAdBgNVHQ4EFgQUpHS7FfaQ5bCrTxUeu6R2ZC3VGOowFAYD\nVR0RBA0wC4IJcm9vdC10ZXN0MAoGCCqGSM49BAMCA0kAMEYCIQC2vgqwla0u8LHH\n1MHob14qvS5o76HautbIBW7fcHzz5gIhAIx5A2+wkJYX4026kqaZCk/1sAwTxSGY\nM46l92gdOozT\n-----END CERTIFICATE-----`)\n\t}\n\n\tname, err := randutil.Alphanumeric(10)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\trootPool := x509.NewCertPool()\n\n\tvar (\n\t\tblock *pem.Block\n\t\trest  = root\n\t)\n\tfor rest != nil {\n\t\tblock, rest = pem.Decode(rest)\n\t\tif block == nil {\n\t\t\tbreak\n\t\t}\n\t\tcert, err := x509.ParseCertificate(block.Bytes)\n\t\tif err != nil {\n\t\t\treturn nil, errors.Wrap(err, \"error parsing x509 certificate from PEM block\")\n\t\t}\n\t\trootPool.AddCert(cert)\n\t}\n\tp := &X5C{\n\t\tName:     name,\n\t\tType:     \"X5C\",\n\t\tRoots:    root,\n\t\tClaims:   &globalProvisionerClaims,\n\t\trootPool: rootPool,\n\t}\n\tp.ctl, err = NewController(p, p.Claims, Config{\n\t\tAudiences: testAudiences,\n\t}, nil)\n\treturn p, err\n}\n\nfunc generateOIDC() (*OIDC, error) {\n\tname, err := randutil.Alphanumeric(10)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tclientID, err := randutil.Alphanumeric(10)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tissuer, err := randutil.Alphanumeric(10)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tjwk, err := generateJSONWebKey()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tp := &OIDC{\n\t\tName:                  name,\n\t\tType:                  \"OIDC\",\n\t\tClientID:              clientID,\n\t\tConfigurationEndpoint: \"https://example.com/.well-known/openid-configuration\",\n\t\tClaims:                &globalProvisionerClaims,\n\t\tconfiguration: openIDConfiguration{\n\t\t\tIssuer:    issuer,\n\t\t\tJWKSetURI: \"https://example.com/.well-known/jwks\",\n\t\t},\n\t\tkeyStore: &keyStore{\n\t\t\tkeySet: jose.JSONWebKeySet{Keys: []jose.JSONWebKey{*jwk}},\n\t\t\texpiry: time.Now().Add(24 * time.Hour),\n\t\t},\n\t}\n\tp.ctl, err = NewController(p, p.Claims, Config{\n\t\tAudiences: testAudiences,\n\t}, nil)\n\treturn p, err\n}\n\nfunc generateGCP() (*GCP, error) {\n\tname, err := randutil.Alphanumeric(10)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tserviceAccount, err := randutil.Alphanumeric(10)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tjwk, err := generateJSONWebKey()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tp := &GCP{\n\t\tType:             \"GCP\",\n\t\tName:             name,\n\t\tServiceAccounts:  []string{serviceAccount},\n\t\tClaims:           &globalProvisionerClaims,\n\t\tDisableSSHCAHost: &DefaultDisableSSHCAHost,\n\t\tDisableSSHCAUser: &DefaultDisableSSHCAUser,\n\t\tconfig:           newGCPConfig(),\n\t\tkeyStore: &keyStore{\n\t\t\tkeySet: jose.JSONWebKeySet{Keys: []jose.JSONWebKey{*jwk}},\n\t\t\texpiry: time.Now().Add(24 * time.Hour),\n\t\t},\n\t\tprojectValidator: &gcp.ProjectValidator{},\n\t}\n\tp.ctl, err = NewController(p, p.Claims, Config{\n\t\tAudiences: testAudiences.WithFragment(\"gcp/\" + name),\n\t}, nil)\n\treturn p, err\n}\n\nfunc generateAWS() (*AWS, error) {\n\tname, err := randutil.Alphanumeric(10)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\taccountID, err := randutil.Alphanumeric(10)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tblock, _ := pem.Decode([]byte(awsTestCertificate))\n\tif block == nil || block.Type != \"CERTIFICATE\" {\n\t\treturn nil, errors.New(\"error decoding AWS certificate\")\n\t}\n\tcert, err := x509.ParseCertificate(block.Bytes)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"error parsing AWS certificate\")\n\t}\n\tp := &AWS{\n\t\tType:         \"AWS\",\n\t\tName:         name,\n\t\tAccounts:     []string{accountID},\n\t\tClaims:       &globalProvisionerClaims,\n\t\tIMDSVersions: []string{\"v2\", \"v1\"},\n\t\tconfig: &awsConfig{\n\t\t\tidentityURL:        awsIdentityURL,\n\t\t\tsignatureURL:       awsSignatureURL,\n\t\t\ttokenURL:           awsAPITokenURL,\n\t\t\ttokenTTL:           awsAPITokenTTL,\n\t\t\tcertificates:       []*x509.Certificate{cert},\n\t\t\tsignatureAlgorithm: awsSignatureAlgorithm,\n\t\t},\n\t}\n\tp.ctl, err = NewController(p, p.Claims, Config{\n\t\tAudiences: testAudiences.WithFragment(\"aws/\" + name),\n\t}, nil)\n\treturn p, err\n}\n\nfunc generateAWSWithServer() (*AWS, *httptest.Server, error) {\n\taws, err := generateAWS()\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\tblock, _ := pem.Decode([]byte(awsTestKey))\n\tif block == nil || block.Type != \"RSA PRIVATE KEY\" {\n\t\treturn nil, nil, errors.New(\"error decoding AWS key\")\n\t}\n\tkey, err := x509.ParsePKCS1PrivateKey(block.Bytes)\n\tif err != nil {\n\t\treturn nil, nil, errors.Wrap(err, \"error parsing AWS private key\")\n\t}\n\tdoc, err := json.MarshalIndent(awsInstanceIdentityDocument{\n\t\tAccountID:        aws.Accounts[0],\n\t\tArchitecture:     \"x86_64\",\n\t\tAvailabilityZone: \"us-west-2b\",\n\t\tImageID:          \"image-id\",\n\t\tInstanceID:       \"instance-id\",\n\t\tInstanceType:     \"t2.micro\",\n\t\tPendingTime:      time.Now(),\n\t\tPrivateIP:        \"127.0.0.1\",\n\t\tRegion:           \"us-west-1\",\n\t\tVersion:          \"2017-09-30\",\n\t}, \"\", \"  \")\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\n\tsum := sha256.Sum256(doc)\n\tsignature, err := key.Sign(rand.Reader, sum[:], crypto.SHA256)\n\tif err != nil {\n\t\treturn nil, nil, errors.Wrap(err, \"error signing document\")\n\t}\n\t//nolint:gosec // tests minimum size of the key\n\ttoken := \"AQAEAEEO9-7Z88ewKFpboZuDlFYWz9A3AN-wMOVzjEhfAyXW31BvVw==\"\n\tsrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tswitch r.URL.Path {\n\t\tcase \"/latest/dynamic/instance-identity/document\":\n\t\t\t// check for API token\n\t\t\tif r.Header.Get(\"X-aws-ec2-metadata-token\") != token {\n\t\t\t\tw.WriteHeader(http.StatusUnauthorized)\n\t\t\t\tw.Write([]byte(\"401 Unauthorized\"))\n\t\t\t}\n\t\t\tw.Write(doc)\n\t\tcase \"/latest/dynamic/instance-identity/signature\":\n\t\t\t// check for API token\n\t\t\tif r.Header.Get(\"X-aws-ec2-metadata-token\") != token {\n\t\t\t\tw.WriteHeader(http.StatusUnauthorized)\n\t\t\t\tw.Write([]byte(\"401 Unauthorized\"))\n\t\t\t}\n\t\t\tw.Write([]byte(base64.StdEncoding.EncodeToString(signature)))\n\t\tcase \"/latest/api/token\":\n\t\t\tw.Write([]byte(token))\n\t\tcase \"/bad-document\":\n\t\t\tw.Write([]byte(\"{}\"))\n\t\tcase \"/bad-signature\":\n\t\t\tw.Write([]byte(\"YmFkLXNpZ25hdHVyZQo=\"))\n\t\tcase \"/bad-json\":\n\t\t\tw.Write([]byte(\"{\"))\n\t\tdefault:\n\t\t\thttp.NotFound(w, r)\n\t\t}\n\t}))\n\taws.config.identityURL = srv.URL + \"/latest/dynamic/instance-identity/document\"\n\taws.config.signatureURL = srv.URL + \"/latest/dynamic/instance-identity/signature\"\n\taws.config.tokenURL = srv.URL + \"/latest/api/token\"\n\treturn aws, srv, nil\n}\n\nfunc generateAWSV1Only() (*AWS, error) {\n\tname, err := randutil.Alphanumeric(10)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\taccountID, err := randutil.Alphanumeric(10)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tblock, _ := pem.Decode([]byte(awsTestCertificate))\n\tif block == nil || block.Type != \"CERTIFICATE\" {\n\t\treturn nil, errors.New(\"error decoding AWS certificate\")\n\t}\n\tcert, err := x509.ParseCertificate(block.Bytes)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"error parsing AWS certificate\")\n\t}\n\tp := &AWS{\n\t\tType:         \"AWS\",\n\t\tName:         name,\n\t\tAccounts:     []string{accountID},\n\t\tClaims:       &globalProvisionerClaims,\n\t\tIMDSVersions: []string{\"v1\"},\n\t\tconfig: &awsConfig{\n\t\t\tidentityURL:        awsIdentityURL,\n\t\t\tsignatureURL:       awsSignatureURL,\n\t\t\ttokenURL:           awsAPITokenURL,\n\t\t\ttokenTTL:           awsAPITokenTTL,\n\t\t\tcertificates:       []*x509.Certificate{cert},\n\t\t\tsignatureAlgorithm: awsSignatureAlgorithm,\n\t\t},\n\t}\n\tp.ctl, err = NewController(p, p.Claims, Config{\n\t\tAudiences: testAudiences.WithFragment(\"aws/\" + name),\n\t}, nil)\n\treturn p, err\n}\n\nfunc generateAWSWithServerV1Only() (*AWS, *httptest.Server, error) {\n\taws, err := generateAWSV1Only()\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\tblock, _ := pem.Decode([]byte(awsTestKey))\n\tif block == nil || block.Type != \"RSA PRIVATE KEY\" {\n\t\treturn nil, nil, errors.New(\"error decoding AWS key\")\n\t}\n\tkey, err := x509.ParsePKCS1PrivateKey(block.Bytes)\n\tif err != nil {\n\t\treturn nil, nil, errors.Wrap(err, \"error parsing AWS private key\")\n\t}\n\tdoc, err := json.MarshalIndent(awsInstanceIdentityDocument{\n\t\tAccountID:        aws.Accounts[0],\n\t\tArchitecture:     \"x86_64\",\n\t\tAvailabilityZone: \"us-west-2b\",\n\t\tImageID:          \"image-id\",\n\t\tInstanceID:       \"instance-id\",\n\t\tInstanceType:     \"t2.micro\",\n\t\tPendingTime:      time.Now(),\n\t\tPrivateIP:        \"127.0.0.1\",\n\t\tRegion:           \"us-west-1\",\n\t\tVersion:          \"2017-09-30\",\n\t}, \"\", \"  \")\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\n\tsum := sha256.Sum256(doc)\n\tsignature, err := key.Sign(rand.Reader, sum[:], crypto.SHA256)\n\tif err != nil {\n\t\treturn nil, nil, errors.Wrap(err, \"error signing document\")\n\t}\n\tsrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tswitch r.URL.Path {\n\t\tcase \"/latest/dynamic/instance-identity/document\":\n\t\t\tw.Write(doc)\n\t\tcase \"/latest/dynamic/instance-identity/signature\":\n\t\t\tw.Write([]byte(base64.StdEncoding.EncodeToString(signature)))\n\t\tcase \"/bad-document\":\n\t\t\tw.Write([]byte(\"{}\"))\n\t\tcase \"/bad-signature\":\n\t\t\tw.Write([]byte(\"YmFkLXNpZ25hdHVyZQo=\"))\n\t\tcase \"/bad-json\":\n\t\t\tw.Write([]byte(\"{\"))\n\t\tdefault:\n\t\t\thttp.NotFound(w, r)\n\t\t}\n\t}))\n\taws.config.identityURL = srv.URL + \"/latest/dynamic/instance-identity/document\"\n\taws.config.signatureURL = srv.URL + \"/latest/dynamic/instance-identity/signature\"\n\treturn aws, srv, nil\n}\n\nfunc generateAzure() (*Azure, error) {\n\tname, err := randutil.Alphanumeric(10)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\ttenantID, err := randutil.Alphanumeric(10)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tjwk, err := generateJSONWebKey()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tp := &Azure{\n\t\tType:     \"Azure\",\n\t\tName:     name,\n\t\tTenantID: tenantID,\n\t\tAudience: azureDefaultAudience,\n\t\tClaims:   &globalProvisionerClaims,\n\t\tconfig:   newAzureConfig(tenantID),\n\t\toidcConfig: openIDConfiguration{\n\t\t\tIssuer:    \"https://sts.windows.net/\" + tenantID + \"/\",\n\t\t\tJWKSetURI: \"https://login.microsoftonline.com/common/discovery/keys\",\n\t\t},\n\t\tkeyStore: &keyStore{\n\t\t\tkeySet: jose.JSONWebKeySet{Keys: []jose.JSONWebKey{*jwk}},\n\t\t\texpiry: time.Now().Add(24 * time.Hour),\n\t\t},\n\t}\n\tp.ctl, err = NewController(p, p.Claims, Config{\n\t\tAudiences: testAudiences,\n\t}, nil)\n\treturn p, err\n}\n\nfunc generateAzureWithServer() (*Azure, *httptest.Server, error) {\n\taz, err := generateAzure()\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\twriteJSON := func(w http.ResponseWriter, v interface{}) {\n\t\tb, err := json.Marshal(v)\n\t\tif err != nil {\n\t\t\tw.WriteHeader(http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\t\tw.Header().Add(\"Content-Type\", \"application/json\")\n\t\tw.WriteHeader(http.StatusOK)\n\t\tw.Write(b)\n\t}\n\tgetPublic := func(ks jose.JSONWebKeySet) jose.JSONWebKeySet {\n\t\tvar ret jose.JSONWebKeySet\n\t\tfor _, k := range ks.Keys {\n\t\t\tret.Keys = append(ret.Keys, k.Public())\n\t\t}\n\t\treturn ret\n\t}\n\tissuer := \"https://sts.windows.net/\" + az.TenantID + \"/\"\n\tsrv := httptest.NewUnstartedServer(nil)\n\tsrv.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tswitch r.URL.Path {\n\t\tcase \"/error\":\n\t\t\thttp.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)\n\t\tcase \"/\" + az.TenantID + \"/.well-known/openid-configuration\":\n\t\t\twriteJSON(w, openIDConfiguration{Issuer: issuer, JWKSetURI: srv.URL + \"/jwks_uri\"})\n\t\tcase \"/openid-configuration-no-issuer\":\n\t\t\twriteJSON(w, openIDConfiguration{Issuer: \"\", JWKSetURI: srv.URL + \"/jwks_uri\"})\n\t\tcase \"/openid-configuration-fail-jwk\":\n\t\t\twriteJSON(w, openIDConfiguration{Issuer: issuer, JWKSetURI: srv.URL + \"/error\"})\n\t\tcase \"/random\":\n\t\t\tkeySet := must(generateJSONWebKeySet(2))[0].(jose.JSONWebKeySet)\n\t\t\tw.Header().Add(\"Cache-Control\", \"max-age=5\")\n\t\t\twriteJSON(w, getPublic(keySet))\n\t\tcase \"/private\":\n\t\t\twriteJSON(w, az.keyStore.keySet)\n\t\tcase \"/jwks_uri\":\n\t\t\tw.Header().Add(\"Cache-Control\", \"max-age=5\")\n\t\t\twriteJSON(w, getPublic(az.keyStore.keySet))\n\t\tcase \"/metadata/identity/oauth2/token\":\n\t\t\ttok, err := generateAzureToken(\"subject\", issuer, \"https://management.azure.com/\", az.TenantID, \"subscriptionID\", \"resourceGroup\", \"virtualMachine\", \"vm\", time.Now(), &az.keyStore.keySet.Keys[0])\n\t\t\tif err != nil {\n\t\t\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\t\t} else {\n\t\t\t\twriteJSON(w, azureIdentityToken{\n\t\t\t\t\tAccessToken: tok,\n\t\t\t\t})\n\t\t\t}\n\t\tcase \"/metadata/instance/compute/azEnvironment\":\n\t\t\tw.Header().Add(\"Content-Type\", \"text/plain\")\n\t\t\tw.Write([]byte(\"AzurePublicCloud\"))\n\t\tdefault:\n\t\t\thttp.NotFound(w, r)\n\t\t}\n\t})\n\tsrv.Start()\n\taz.config.oidcDiscoveryURL = srv.URL + \"/\" + az.TenantID + \"/.well-known/openid-configuration\"\n\taz.config.identityTokenURL = srv.URL + \"/metadata/identity/oauth2/token\"\n\taz.config.instanceComputeURL = srv.URL + \"/metadata/instance/compute/azEnvironment\"\n\treturn az, srv, nil\n}\n\nfunc generateCollection(nJWK, nOIDC int) (*Collection, error) {\n\tcol := NewCollection(testAudiences)\n\tfor i := 0; i < nJWK; i++ {\n\t\tp, err := generateJWK()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tcol.Store(p)\n\t}\n\tfor i := 0; i < nOIDC; i++ {\n\t\tp, err := generateOIDC()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tcol.Store(p)\n\t}\n\treturn col, nil\n}\n\nfunc generateSimpleToken(iss, aud string, jwk *jose.JSONWebKey) (string, error) {\n\treturn generateToken(\"subject\", iss, aud, \"name@smallstep.com\", []string{\"test.smallstep.com\"}, time.Now(), jwk)\n}\n\ntype tokOption func(*jose.SignerOptions) error\n\nfunc withX5CHdr(certs []*x509.Certificate) tokOption {\n\treturn func(so *jose.SignerOptions) error {\n\t\tstrs := make([]string, len(certs))\n\t\tfor i, cert := range certs {\n\t\t\tstrs[i] = base64.StdEncoding.EncodeToString(cert.Raw)\n\t\t}\n\t\tso.WithHeader(\"x5c\", strs)\n\t\treturn nil\n\t}\n}\n\nfunc withSSHPOPFile(cert *ssh.Certificate) tokOption {\n\treturn func(so *jose.SignerOptions) error {\n\t\tso.WithHeader(\"sshpop\", base64.StdEncoding.EncodeToString(cert.Marshal()))\n\t\treturn nil\n\t}\n}\n\nfunc generateToken(sub, iss, aud, email string, sans []string, iat time.Time, jwk *jose.JSONWebKey, tokOpts ...tokOption) (string, error) {\n\tso := new(jose.SignerOptions)\n\tso.WithType(\"JWT\")\n\tso.WithHeader(\"kid\", jwk.KeyID)\n\n\tfor _, o := range tokOpts {\n\t\tif err := o(so); err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t}\n\n\tsig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: jwk.Key}, so)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tid, err := randutil.ASCII(64)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tclaims := struct {\n\t\tjose.Claims\n\t\tEmail string   `json:\"email\"`\n\t\tSANS  []string `json:\"sans\"`\n\t}{\n\t\tClaims: jose.Claims{\n\t\t\tID:        id,\n\t\t\tSubject:   sub,\n\t\t\tIssuer:    iss,\n\t\t\tIssuedAt:  jose.NewNumericDate(iat),\n\t\t\tNotBefore: jose.NewNumericDate(iat),\n\t\t\tExpiry:    jose.NewNumericDate(iat.Add(5 * time.Minute)),\n\t\t\tAudience:  []string{aud},\n\t\t},\n\t\tEmail: email,\n\t\tSANS:  sans,\n\t}\n\treturn jose.Signed(sig).Claims(claims).CompactSerialize()\n}\n\nfunc generateCustomToken(sub, iss, aud string, jwk *jose.JSONWebKey, extraHeaders, extraClaims map[string]any) (string, error) {\n\tso := new(jose.SignerOptions)\n\tso.WithType(\"JWT\")\n\tso.WithHeader(\"kid\", jwk.KeyID)\n\n\tfor k, v := range extraHeaders {\n\t\tso.WithHeader(jose.HeaderKey(k), v)\n\t}\n\n\tsig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: jwk.Key}, so)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tid, err := randutil.ASCII(64)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tiat := time.Now()\n\tclaims := jose.Claims{\n\t\tID:        id,\n\t\tSubject:   sub,\n\t\tIssuer:    iss,\n\t\tIssuedAt:  jose.NewNumericDate(iat),\n\t\tNotBefore: jose.NewNumericDate(iat),\n\t\tExpiry:    jose.NewNumericDate(iat.Add(5 * time.Minute)),\n\t\tAudience:  []string{aud},\n\t}\n\treturn jose.Signed(sig).Claims(claims).Claims(extraClaims).CompactSerialize()\n}\n\nfunc generateOIDCToken(sub, iss, aud, email, preferredUsername string, iat time.Time, jwk *jose.JSONWebKey, tokOpts ...tokOption) (string, error) {\n\tso := new(jose.SignerOptions)\n\tso.WithType(\"JWT\")\n\tso.WithHeader(\"kid\", jwk.KeyID)\n\n\tfor _, o := range tokOpts {\n\t\tif err := o(so); err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t}\n\n\tsig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: jwk.Key}, so)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tid, err := randutil.ASCII(64)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tclaims := struct {\n\t\tjose.Claims\n\t\tEmail             string `json:\"email\"`\n\t\tPreferredUsername string `json:\"preferred_username,omitempty\"`\n\t}{\n\t\tClaims: jose.Claims{\n\t\t\tID:        id,\n\t\t\tSubject:   sub,\n\t\t\tIssuer:    iss,\n\t\t\tIssuedAt:  jose.NewNumericDate(iat),\n\t\t\tNotBefore: jose.NewNumericDate(iat),\n\t\t\tExpiry:    jose.NewNumericDate(iat.Add(5 * time.Minute)),\n\t\t\tAudience:  []string{aud},\n\t\t},\n\t\tEmail:             email,\n\t\tPreferredUsername: preferredUsername,\n\t}\n\treturn jose.Signed(sig).Claims(claims).CompactSerialize()\n}\n\nfunc generateX5CSSHToken(jwk *jose.JSONWebKey, claims *x5cPayload, tokOpts ...tokOption) (string, error) {\n\tso := new(jose.SignerOptions)\n\tso.WithType(\"JWT\")\n\tso.WithHeader(\"kid\", jwk.KeyID)\n\n\tfor _, o := range tokOpts {\n\t\tif err := o(so); err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t}\n\n\tsig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: jwk.Key}, so)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn jose.Signed(sig).Claims(claims).CompactSerialize()\n}\n\nfunc getK8sSAPayload() *k8sSAPayload {\n\treturn &k8sSAPayload{\n\t\tClaims: jose.Claims{\n\t\t\tIssuer:  k8sSAIssuer,\n\t\t\tSubject: \"foo\",\n\t\t},\n\t\tNamespace:          \"ns-foo\",\n\t\tSecretName:         \"sn-foo\",\n\t\tServiceAccountName: \"san-foo\",\n\t\tServiceAccountUID:  \"sauid-foo\",\n\t}\n}\n\nfunc generateK8sSAToken(jwk *jose.JSONWebKey, claims *k8sSAPayload, tokOpts ...tokOption) (string, error) {\n\tso := new(jose.SignerOptions)\n\tso.WithHeader(\"kid\", jwk.KeyID)\n\n\tfor _, o := range tokOpts {\n\t\tif err := o(so); err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t}\n\n\tsig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: jwk.Key}, so)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tif claims == nil {\n\t\tclaims = getK8sSAPayload()\n\t}\n\treturn jose.Signed(sig).Claims(*claims).CompactSerialize()\n}\n\nfunc generateSimpleSSHUserToken(iss, aud string, jwk *jose.JSONWebKey) (string, error) {\n\treturn generateSSHToken(\"subject@localhost\", iss, aud, time.Now(), &SignSSHOptions{\n\t\tCertType:   \"user\",\n\t\tPrincipals: []string{\"name\"},\n\t}, jwk)\n}\n\nfunc generateSimpleSSHHostToken(iss, aud string, jwk *jose.JSONWebKey) (string, error) {\n\treturn generateSSHToken(\"subject@localhost\", iss, aud, time.Now(), &SignSSHOptions{\n\t\tCertType:   \"host\",\n\t\tPrincipals: []string{\"smallstep.com\"},\n\t}, jwk)\n}\n\nfunc generateSSHToken(sub, iss, aud string, iat time.Time, sshOpts *SignSSHOptions, jwk *jose.JSONWebKey) (string, error) {\n\tsig, err := jose.NewSigner(\n\t\tjose.SigningKey{Algorithm: jose.ES256, Key: jwk.Key},\n\t\tnew(jose.SignerOptions).WithType(\"JWT\").WithHeader(\"kid\", jwk.KeyID),\n\t)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tid, err := randutil.ASCII(64)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tclaims := struct {\n\t\tjose.Claims\n\t\tStep *stepPayload `json:\"step,omitempty\"`\n\t}{\n\t\tClaims: jose.Claims{\n\t\t\tID:        id,\n\t\t\tSubject:   sub,\n\t\t\tIssuer:    iss,\n\t\t\tIssuedAt:  jose.NewNumericDate(iat),\n\t\t\tNotBefore: jose.NewNumericDate(iat),\n\t\t\tExpiry:    jose.NewNumericDate(iat.Add(5 * time.Minute)),\n\t\t\tAudience:  []string{aud},\n\t\t},\n\t\tStep: &stepPayload{\n\t\t\tSSH: sshOpts,\n\t\t},\n\t}\n\treturn jose.Signed(sig).Claims(claims).CompactSerialize()\n}\n\nfunc generateGCPToken(sub, iss, aud, instanceID, instanceName, projectID, zone string, iat time.Time, jwk *jose.JSONWebKey) (string, error) {\n\tsig, err := jose.NewSigner(\n\t\tjose.SigningKey{Algorithm: jose.ES256, Key: jwk.Key},\n\t\tnew(jose.SignerOptions).WithType(\"JWT\").WithHeader(\"kid\", jwk.KeyID),\n\t)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\taud, err = generateSignAudience(\"https://ca.smallstep.com\", aud)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tclaims := gcpPayload{\n\t\tClaims: jose.Claims{\n\t\t\tSubject:   sub,\n\t\t\tIssuer:    iss,\n\t\t\tIssuedAt:  jose.NewNumericDate(iat),\n\t\t\tNotBefore: jose.NewNumericDate(iat),\n\t\t\tExpiry:    jose.NewNumericDate(iat.Add(5 * time.Minute)),\n\t\t\tAudience:  []string{aud},\n\t\t},\n\t\tAuthorizedParty: sub,\n\t\tEmail:           \"foo@developer.gserviceaccount.com\",\n\t\tEmailVerified:   true,\n\t\tGoogle: gcpGooglePayload{\n\t\t\tComputeEngine: gcpComputeEnginePayload{\n\t\t\t\tInstanceID:                instanceID,\n\t\t\t\tInstanceName:              instanceName,\n\t\t\t\tInstanceCreationTimestamp: jose.NewNumericDate(iat),\n\t\t\t\tProjectID:                 projectID,\n\t\t\t\tProjectNumber:             1234567890,\n\t\t\t\tZone:                      zone,\n\t\t\t},\n\t\t},\n\t}\n\treturn jose.Signed(sig).Claims(claims).CompactSerialize()\n}\n\nfunc generateAWSToken(p *AWS, sub, iss, aud, accountID, instanceID, privateIP, region string, iat time.Time, key crypto.Signer) (string, error) {\n\tdoc, err := json.MarshalIndent(awsInstanceIdentityDocument{\n\t\tAccountID:        accountID,\n\t\tArchitecture:     \"x86_64\",\n\t\tAvailabilityZone: \"us-west-2b\",\n\t\tImageID:          \"ami-123123\",\n\t\tInstanceID:       instanceID,\n\t\tInstanceType:     \"t2.micro\",\n\t\tPendingTime:      iat,\n\t\tPrivateIP:        privateIP,\n\t\tRegion:           region,\n\t\tVersion:          \"2017-09-30\",\n\t}, \"\", \"  \")\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tsum := sha256.Sum256(doc)\n\tsignature, err := key.Sign(rand.Reader, sum[:], crypto.SHA256)\n\tif err != nil {\n\t\treturn \"\", errors.Wrap(err, \"error signing document\")\n\t}\n\n\tsig, err := jose.NewSigner(\n\t\tjose.SigningKey{Algorithm: jose.HS256, Key: signature},\n\t\tnew(jose.SignerOptions).WithType(\"JWT\"),\n\t)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\taud, err = generateSignAudience(\"https://ca.smallstep.com\", aud)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tunique := fmt.Sprintf(\"%s.%s\", p.GetID(), instanceID)\n\tsum = sha256.Sum256([]byte(unique))\n\n\tclaims := awsPayload{\n\t\tClaims: jose.Claims{\n\t\t\tID:        strings.ToLower(hex.EncodeToString(sum[:])),\n\t\t\tSubject:   sub,\n\t\t\tIssuer:    iss,\n\t\t\tIssuedAt:  jose.NewNumericDate(iat),\n\t\t\tNotBefore: jose.NewNumericDate(iat),\n\t\t\tExpiry:    jose.NewNumericDate(iat.Add(5 * time.Minute)),\n\t\t\tAudience:  []string{aud},\n\t\t},\n\t\tAmazon: awsAmazonPayload{\n\t\t\tDocument:  doc,\n\t\t\tSignature: signature,\n\t\t},\n\t}\n\treturn jose.Signed(sig).Claims(claims).CompactSerialize()\n}\n\nfunc generateAzureToken(sub, iss, aud, tenantID, subscriptionID, resourceGroup, resourceName, resourceType string, iat time.Time, jwk *jose.JSONWebKey) (string, error) {\n\tsig, err := jose.NewSigner(\n\t\tjose.SigningKey{Algorithm: jose.ES256, Key: jwk.Key},\n\t\tnew(jose.SignerOptions).WithType(\"JWT\").WithHeader(\"kid\", jwk.KeyID),\n\t)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tvar xmsMirID string\n\tswitch resourceType {\n\tcase \"vm\":\n\t\txmsMirID = fmt.Sprintf(\"/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Compute/virtualMachines/%s\", subscriptionID, resourceGroup, resourceName)\n\tcase \"uai\":\n\t\txmsMirID = fmt.Sprintf(\"/subscriptions/%s/resourceGroups/%s/providers/Microsoft.ManagedIdentity/userAssignedIdentities/%s\", subscriptionID, resourceGroup, resourceName)\n\t}\n\n\tclaims := azurePayload{\n\t\tClaims: jose.Claims{\n\t\t\tSubject:   sub,\n\t\t\tIssuer:    iss,\n\t\t\tIssuedAt:  jose.NewNumericDate(iat),\n\t\t\tNotBefore: jose.NewNumericDate(iat),\n\t\t\tExpiry:    jose.NewNumericDate(iat.Add(5 * time.Minute)),\n\t\t\tAudience:  []string{aud},\n\t\t\tID:        \"the-jti\",\n\t\t},\n\t\tAppID:            \"the-appid\",\n\t\tAppIDAcr:         \"the-appidacr\",\n\t\tIdentityProvider: \"the-idp\",\n\t\tObjectID:         \"the-oid\",\n\t\tTenantID:         tenantID,\n\t\tVersion:          \"the-version\",\n\t\tXMSMirID:         xmsMirID,\n\t}\n\treturn jose.Signed(sig).Claims(claims).CompactSerialize()\n}\n\nfunc parseToken(token string) (*jose.JSONWebToken, *jose.Claims, error) {\n\ttok, err := jose.ParseSigned(token)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\tclaims := new(jose.Claims)\n\tif err := tok.UnsafeClaimsWithoutVerification(claims); err != nil {\n\t\treturn nil, nil, err\n\t}\n\treturn tok, claims, nil\n}\n\nfunc parseAWSToken(token string) (*jose.JSONWebToken, *awsPayload, error) {\n\ttok, err := jose.ParseSigned(token)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\tclaims := new(awsPayload)\n\tif err := tok.UnsafeClaimsWithoutVerification(claims); err != nil {\n\t\treturn nil, nil, err\n\t}\n\tvar doc awsInstanceIdentityDocument\n\tif err := json.Unmarshal(claims.Amazon.Document, &doc); err != nil {\n\t\treturn nil, nil, errors.Wrap(err, \"error unmarshaling identity document\")\n\t}\n\tclaims.document = doc\n\treturn tok, claims, nil\n}\n\nfunc generateJWKServerHandler(n int, srv *httptest.Server) http.Handler {\n\thits := struct {\n\t\tHits int `json:\"hits\"`\n\t}{}\n\twriteJSON := func(w http.ResponseWriter, v interface{}) {\n\t\tb, err := json.Marshal(v)\n\t\tif err != nil {\n\t\t\tw.WriteHeader(http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\t\tw.Header().Add(\"Content-Type\", \"application/json\")\n\t\tw.WriteHeader(http.StatusOK)\n\t\tw.Write(b)\n\t}\n\tgetPublic := func(ks jose.JSONWebKeySet) jose.JSONWebKeySet {\n\t\tvar ret jose.JSONWebKeySet\n\t\tfor _, k := range ks.Keys {\n\t\t\tret.Keys = append(ret.Keys, k.Public())\n\t\t}\n\t\treturn ret\n\t}\n\n\tdefaultKeySet := must(generateJSONWebKeySet(n))[0].(jose.JSONWebKeySet)\n\treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\thits.Hits++\n\t\tswitch r.RequestURI {\n\t\tcase \"/error\":\n\t\t\thttp.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)\n\t\tcase \"/hits\":\n\t\t\twriteJSON(w, hits)\n\t\tcase \"/.well-known/openid-configuration\":\n\t\t\twriteJSON(w, openIDConfiguration{Issuer: \"the-issuer\", JWKSetURI: srv.URL + \"/jwks_uri\"})\n\t\tcase \"/common/.well-known/openid-configuration\":\n\t\t\twriteJSON(w, openIDConfiguration{Issuer: \"https://login.microsoftonline.com/{tenantid}/v2.0\", JWKSetURI: srv.URL + \"/jwks_uri\"})\n\t\tcase \"/random\":\n\t\t\tkeySet := must(generateJSONWebKeySet(n))[0].(jose.JSONWebKeySet)\n\t\t\tw.Header().Add(\"Cache-Control\", \"max-age=5\")\n\t\t\twriteJSON(w, getPublic(keySet))\n\t\tcase \"/no-cache\":\n\t\t\tkeySet := must(generateJSONWebKeySet(n))[0].(jose.JSONWebKeySet)\n\t\t\tw.Header().Add(\"Cache-Control\", \"no-cache, no-store, max-age=0, must-revalidate\")\n\t\t\twriteJSON(w, getPublic(keySet))\n\t\tcase \"/private\":\n\t\t\twriteJSON(w, defaultKeySet)\n\t\tdefault:\n\t\t\tw.Header().Add(\"Cache-Control\", \"max-age=5\")\n\t\t\twriteJSON(w, getPublic(defaultKeySet))\n\t\t}\n\t})\n}\n\nfunc generateJWKServer(n int) *httptest.Server {\n\tsrv := httptest.NewUnstartedServer(nil)\n\tsrv.Config.Handler = generateJWKServerHandler(n, srv)\n\tsrv.Start()\n\treturn srv\n}\n\nfunc generateTLSJWKServer(n int) *httptest.Server {\n\tsrv := httptest.NewUnstartedServer(nil)\n\tsrv.Config.Handler = generateJWKServerHandler(n, srv)\n\tsrv.StartTLS()\n\treturn srv\n}\n\nfunc generateACME() (*ACME, error) {\n\t// Initialize provisioners\n\tp := &ACME{\n\t\tType: \"ACME\",\n\t\tName: \"test@acme-provisioner.com\",\n\t}\n\tif err := p.Init(Config{Claims: globalProvisionerClaims}); err != nil {\n\t\treturn nil, err\n\t}\n\treturn p, nil\n}\n\nfunc parseCerts(b []byte) ([]*x509.Certificate, error) {\n\tvar (\n\t\tblock *pem.Block\n\t\trest  = b\n\t\tcerts = []*x509.Certificate{}\n\t)\n\tfor rest != nil {\n\t\tblock, rest = pem.Decode(rest)\n\t\tif block == nil {\n\t\t\tbreak\n\t\t}\n\t\tcert, err := x509.ParseCertificate(block.Bytes)\n\t\tif err != nil {\n\t\t\treturn nil, errors.Wrap(err, \"error parsing x509 certificate from PEM block\")\n\t\t}\n\n\t\tcerts = append(certs, cert)\n\t}\n\treturn certs, nil\n}\n"
  },
  {
    "path": "authority/provisioner/webhook.go",
    "content": "package provisioner\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/hmac\"\n\t\"crypto/sha256\"\n\t\"encoding/base64\"\n\t\"encoding/hex\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"log\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n\t\"text/template\"\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n\n\t\"github.com/smallstep/linkedca\"\n\n\t\"github.com/smallstep/certificates/authority/poolhttp\"\n\t\"github.com/smallstep/certificates/internal/httptransport\"\n\t\"github.com/smallstep/certificates/middleware/requestid\"\n\t\"github.com/smallstep/certificates/templates\"\n\t\"github.com/smallstep/certificates/webhook\"\n)\n\nvar ErrWebhookDenied = errors.New(\"webhook server did not allow request\")\n\ntype WebhookSetter interface {\n\tSetWebhook(string, any)\n}\n\ntype WebhookController struct {\n\tclient        HTTPClient\n\twrapTransport httptransport.Wrapper\n\twebhooks      []*Webhook\n\tcertType      linkedca.Webhook_CertType\n\toptions       []webhook.RequestBodyOption\n\tTemplateData  WebhookSetter\n}\n\n// Enrich fetches data from remote servers and adds returned data to the\n// templateData\nfunc (wc *WebhookController) Enrich(ctx context.Context, req *webhook.RequestBody) error {\n\tif wc == nil {\n\t\treturn nil\n\t}\n\n\t// Apply extra options in the webhook controller\n\tfor _, fn := range wc.options {\n\t\tif err := fn(req); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tfor _, wh := range wc.webhooks {\n\t\tif wh.Kind != linkedca.Webhook_ENRICHING.String() {\n\t\t\tcontinue\n\t\t}\n\t\tif !wc.isCertTypeOK(wh) {\n\t\t\tcontinue\n\t\t}\n\n\t\twhCtx, cancel := context.WithTimeout(ctx, time.Second*10)\n\t\tdefer cancel() //nolint:gocritic // every request canceled with its own timeout\n\n\t\tresp, err := wh.DoWithContext(whCtx, wc.client, wc.wrapTransport, req, wc.TemplateData)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif !resp.Allow {\n\t\t\tif resp.Error != nil {\n\t\t\t\treturn resp.Error\n\t\t\t}\n\t\t\treturn ErrWebhookDenied\n\t\t}\n\t\twc.TemplateData.SetWebhook(wh.Name, resp.Data)\n\t}\n\treturn nil\n}\n\n// Authorize checks that all remote servers allow the request\nfunc (wc *WebhookController) Authorize(ctx context.Context, req *webhook.RequestBody) error {\n\tif wc == nil {\n\t\treturn nil\n\t}\n\n\t// Apply extra options in the webhook controller\n\tfor _, fn := range wc.options {\n\t\tif err := fn(req); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tfor _, wh := range wc.webhooks {\n\t\tif wh.Kind != linkedca.Webhook_AUTHORIZING.String() {\n\t\t\tcontinue\n\t\t}\n\t\tif !wc.isCertTypeOK(wh) {\n\t\t\tcontinue\n\t\t}\n\n\t\twhCtx, cancel := context.WithTimeout(ctx, time.Second*10)\n\t\tdefer cancel() //nolint:gocritic // every request canceled with its own timeout\n\n\t\tresp, err := wh.DoWithContext(whCtx, wc.client, wc.wrapTransport, req, wc.TemplateData)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif !resp.Allow {\n\t\t\tif resp.Error != nil {\n\t\t\t\treturn resp.Error\n\t\t\t}\n\t\t\treturn ErrWebhookDenied\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (wc *WebhookController) isCertTypeOK(wh *Webhook) bool {\n\tif wc.certType == linkedca.Webhook_ALL {\n\t\treturn true\n\t}\n\tif wh.CertType == linkedca.Webhook_ALL.String() || wh.CertType == \"\" {\n\t\treturn true\n\t}\n\treturn wc.certType.String() == wh.CertType\n}\n\ntype Webhook struct {\n\tID                   string `json:\"id\"`\n\tName                 string `json:\"name\"`\n\tURL                  string `json:\"url\"`\n\tKind                 string `json:\"kind\"`\n\tDisableTLSClientAuth bool   `json:\"disableTLSClientAuth,omitempty\"`\n\tCertType             string `json:\"certType\"`\n\tSecret               string `json:\"-\"`\n\tBearerToken          string `json:\"-\"`\n\tBasicAuth            struct {\n\t\tUsername string\n\t\tPassword string\n\t} `json:\"-\"`\n}\n\n// Validate validates a webhook, only name, url and kind are required.\nfunc (w *Webhook) Validate() error {\n\tif w == nil {\n\t\treturn nil\n\t}\n\n\t// name\n\tif w.Name == \"\" {\n\t\treturn errors.New(\"webhook name is required\")\n\t}\n\n\t// url\n\tparsedURL, err := url.Parse(w.URL)\n\tif err != nil {\n\t\treturn errors.New(\"webhook url is invalid\")\n\t}\n\tif parsedURL.Host == \"\" {\n\t\treturn errors.New(\"webhook url is invalid\")\n\t}\n\tif parsedURL.Scheme != \"https\" {\n\t\treturn errors.New(\"webhook url must use https\")\n\t}\n\tif parsedURL.User != nil {\n\t\treturn errors.New(\"webhook url may not contain username or password\")\n\t}\n\n\t// kind\n\tif w.Kind == \"\" {\n\t\treturn errors.New(\"webhook kind is required\")\n\t}\n\tw.Kind = strings.ToUpper(w.Kind)\n\tkind, ok := linkedca.Webhook_Kind_value[w.Kind]\n\tif !ok || kind == 0 {\n\t\treturn errors.New(\"webhook kind is invalid\")\n\t}\n\n\treturn nil\n}\n\n// TransportWrapper wraps the set of functions mapping [http.Transport] references to\n// [http.RoundTripper].\ntype TransportWrapper = httptransport.Wrapper\n\nfunc (w *Webhook) DoWithContext(ctx context.Context, client HTTPClient, tw TransportWrapper, reqBody *webhook.RequestBody, data any) (*webhook.ResponseBody, error) {\n\ttmpl, err := template.New(\"url\").Funcs(templates.StepFuncMap()).Parse(w.URL)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tbuf := &bytes.Buffer{}\n\tif err := tmpl.Execute(buf, data); err != nil {\n\t\treturn nil, err\n\t}\n\twebhookURL := buf.String()\n\n\t/*\n\t\tSending the token to the webhook server is a security risk. A K8sSA\n\t\ttoken can be reused multiple times. The webhook can misuse it to get\n\t\tfake certificates. A webhook can misuse any other token to get its own\n\t\tcertificate before responding.\n\t\tswitch tmpl := data.(type) {\n\t\tcase x509util.TemplateData:\n\t\t\treqBody.Token = tmpl[x509util.TokenKey]\n\t\tcase sshutil.TemplateData:\n\t\t\treqBody.Token = tmpl[sshutil.TokenKey]\n\t\t}\n\t*/\n\n\treqBody.Timestamp = time.Now()\n\n\treqBytes, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tretries := 1\nretry:\n\n\treq, err := http.NewRequestWithContext(ctx, \"POST\", webhookURL, bytes.NewReader(reqBytes))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif requestID, ok := requestid.FromContext(ctx); ok {\n\t\treq.Header.Set(\"X-Request-Id\", requestID)\n\t}\n\n\tsecret, err := base64.StdEncoding.DecodeString(w.Secret)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\th := hmac.New(sha256.New, secret)\n\th.Write(reqBytes)\n\tsig := h.Sum(nil)\n\treq.Header.Set(\"X-Smallstep-Signature\", hex.EncodeToString(sig))\n\treq.Header.Set(\"X-Smallstep-Webhook-ID\", w.ID)\n\n\tif w.BearerToken != \"\" {\n\t\treq.Header.Set(\"Authorization\", fmt.Sprintf(\"Bearer %s\", w.BearerToken))\n\t} else if w.BasicAuth.Username != \"\" || w.BasicAuth.Password != \"\" {\n\t\treq.SetBasicAuth(w.BasicAuth.Username, w.BasicAuth.Password)\n\t}\n\n\tif w.DisableTLSClientAuth {\n\t\tvar transport *http.Transport\n\t\tif ct, ok := client.(poolhttp.Transporter); ok {\n\t\t\ttransport = ct.Transport()\n\t\t} else {\n\t\t\ttransport = httptransport.New()\n\t\t}\n\n\t\tif transport.TLSClientConfig != nil {\n\t\t\ttransport.TLSClientConfig.GetClientCertificate = nil\n\t\t\ttransport.TLSClientConfig.Certificates = nil\n\t\t}\n\n\t\tclient = &http.Client{\n\t\t\tTransport: tw(transport),\n\t\t}\n\t}\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\tif errors.Is(err, context.DeadlineExceeded) {\n\t\t\treturn nil, err\n\t\t} else if retries > 0 {\n\t\t\tretries--\n\t\t\ttime.Sleep(time.Second)\n\t\t\tgoto retry\n\t\t}\n\t\treturn nil, err\n\t}\n\tdefer func() {\n\t\tif err := resp.Body.Close(); err != nil {\n\t\t\tlog.Printf(\"Failed to close body of response from %s\", w.URL)\n\t\t}\n\t}()\n\tif resp.StatusCode >= 500 && retries > 0 {\n\t\tretries--\n\t\ttime.Sleep(time.Second)\n\t\tgoto retry\n\t}\n\tif resp.StatusCode >= 400 {\n\t\treturn nil, fmt.Errorf(\"Webhook server responded with %d\", resp.StatusCode)\n\t}\n\n\trespBody := &webhook.ResponseBody{}\n\tif err := json.NewDecoder(resp.Body).Decode(respBody); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn respBody, nil\n}\n"
  },
  {
    "path": "authority/provisioner/webhook_test.go",
    "content": "package provisioner\n\nimport (\n\t\"context\"\n\t\"crypto/hmac\"\n\t\"crypto/sha256\"\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t_ \"embed\"\n\t\"encoding/base64\"\n\t\"encoding/hex\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/smallstep/certificates/internal/httptransport\"\n\t\"github.com/smallstep/certificates/middleware/requestid\"\n\t\"github.com/smallstep/certificates/webhook\"\n\t\"github.com/smallstep/linkedca\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"go.step.sm/crypto/pemutil\"\n\t\"go.step.sm/crypto/x509util\"\n)\n\nfunc TestWebhookController_isCertTypeOK(t *testing.T) {\n\ttype test struct {\n\t\twc   *WebhookController\n\t\twh   *Webhook\n\t\twant bool\n\t}\n\ttests := map[string]test{\n\t\t\"all/all\": {\n\t\t\twc:   &WebhookController{certType: linkedca.Webhook_ALL},\n\t\t\twh:   &Webhook{CertType: linkedca.Webhook_ALL.String()},\n\t\t\twant: true,\n\t\t},\n\t\t\"all/x509\": {\n\t\t\twc:   &WebhookController{certType: linkedca.Webhook_ALL},\n\t\t\twh:   &Webhook{CertType: linkedca.Webhook_X509.String()},\n\t\t\twant: true,\n\t\t},\n\t\t\"all/ssh\": {\n\t\t\twc:   &WebhookController{certType: linkedca.Webhook_ALL},\n\t\t\twh:   &Webhook{CertType: linkedca.Webhook_SSH.String()},\n\t\t\twant: true,\n\t\t},\n\t\t`all/\"\"`: {\n\t\t\twc:   &WebhookController{certType: linkedca.Webhook_ALL},\n\t\t\twh:   &Webhook{},\n\t\t\twant: true,\n\t\t},\n\t\t\"x509/all\": {\n\t\t\twc:   &WebhookController{certType: linkedca.Webhook_X509},\n\t\t\twh:   &Webhook{CertType: linkedca.Webhook_ALL.String()},\n\t\t\twant: true,\n\t\t},\n\t\t\"x509/x509\": {\n\t\t\twc:   &WebhookController{certType: linkedca.Webhook_X509},\n\t\t\twh:   &Webhook{CertType: linkedca.Webhook_X509.String()},\n\t\t\twant: true,\n\t\t},\n\t\t\"x509/ssh\": {\n\t\t\twc:   &WebhookController{certType: linkedca.Webhook_X509},\n\t\t\twh:   &Webhook{CertType: linkedca.Webhook_SSH.String()},\n\t\t\twant: false,\n\t\t},\n\t\t`x509/\"\"`: {\n\t\t\twc:   &WebhookController{certType: linkedca.Webhook_X509},\n\t\t\twh:   &Webhook{},\n\t\t\twant: true,\n\t\t},\n\t\t\"ssh/all\": {\n\t\t\twc:   &WebhookController{certType: linkedca.Webhook_SSH},\n\t\t\twh:   &Webhook{CertType: linkedca.Webhook_ALL.String()},\n\t\t\twant: true,\n\t\t},\n\t\t\"ssh/x509\": {\n\t\t\twc:   &WebhookController{certType: linkedca.Webhook_SSH},\n\t\t\twh:   &Webhook{CertType: linkedca.Webhook_X509.String()},\n\t\t\twant: false,\n\t\t},\n\t\t\"ssh/ssh\": {\n\t\t\twc:   &WebhookController{certType: linkedca.Webhook_SSH},\n\t\t\twh:   &Webhook{CertType: linkedca.Webhook_SSH.String()},\n\t\t\twant: true,\n\t\t},\n\t\t`ssh/\"\"`: {\n\t\t\twc:   &WebhookController{certType: linkedca.Webhook_SSH},\n\t\t\twh:   &Webhook{},\n\t\t\twant: true,\n\t\t},\n\t}\n\tfor name, test := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tassert.Equal(t, test.want, test.wc.isCertTypeOK(test.wh))\n\t\t})\n\t}\n}\n\n// withRequestID is a helper that calls into [requestid.NewContext] and returns\n// a new context with the requestID added.\nfunc withRequestID(t *testing.T, ctx context.Context, requestID string) context.Context {\n\tt.Helper()\n\treturn requestid.NewContext(ctx, requestID)\n}\n\nfunc TestWebhookController_Enrich(t *testing.T) {\n\tcert, err := pemutil.ReadCertificate(\"testdata/certs/x5c-leaf.crt\", pemutil.WithFirstBlock())\n\trequire.NoError(t, err)\n\n\ttype test struct {\n\t\tctl                *WebhookController\n\t\tctx                context.Context\n\t\treq                *webhook.RequestBody\n\t\tresponses          []*webhook.ResponseBody\n\t\texpectErr          bool\n\t\texpectTemplateData any\n\t\tassertRequest      func(t *testing.T, req *webhook.RequestBody)\n\t\tassertError        func(t *testing.T, err error)\n\t}\n\ttests := map[string]test{\n\t\t\"ok/no enriching webhooks\": {\n\t\t\tctl: &WebhookController{\n\t\t\t\tclient:       http.DefaultClient,\n\t\t\t\twebhooks:     []*Webhook{{Name: \"people\", Kind: \"AUTHORIZING\"}},\n\t\t\t\tTemplateData: nil,\n\t\t\t},\n\t\t\treq:                &webhook.RequestBody{},\n\t\t\tresponses:          nil,\n\t\t\texpectErr:          false,\n\t\t\texpectTemplateData: nil,\n\t\t},\n\t\t\"ok/one webhook\": {\n\t\t\tctl: &WebhookController{\n\t\t\t\tclient:       http.DefaultClient,\n\t\t\t\twebhooks:     []*Webhook{{Name: \"people\", Kind: \"ENRICHING\"}},\n\t\t\t\tTemplateData: x509util.TemplateData{},\n\t\t\t},\n\t\t\tctx:                withRequestID(t, context.Background(), \"reqID\"),\n\t\t\treq:                &webhook.RequestBody{},\n\t\t\tresponses:          []*webhook.ResponseBody{{Allow: true, Data: map[string]any{\"role\": \"bar\"}}},\n\t\t\texpectErr:          false,\n\t\t\texpectTemplateData: x509util.TemplateData{\"Webhooks\": map[string]any{\"people\": map[string]any{\"role\": \"bar\"}}},\n\t\t},\n\t\t\"ok/two webhooks\": {\n\t\t\tctl: &WebhookController{\n\t\t\t\tclient: http.DefaultClient,\n\t\t\t\twebhooks: []*Webhook{\n\t\t\t\t\t{Name: \"people\", Kind: \"ENRICHING\"},\n\t\t\t\t\t{Name: \"devices\", Kind: \"ENRICHING\"},\n\t\t\t\t},\n\t\t\t\tTemplateData: x509util.TemplateData{},\n\t\t\t},\n\t\t\tctx: withRequestID(t, context.Background(), \"reqID\"),\n\t\t\treq: &webhook.RequestBody{},\n\t\t\tresponses: []*webhook.ResponseBody{\n\t\t\t\t{Allow: true, Data: map[string]any{\"role\": \"bar\"}},\n\t\t\t\t{Allow: true, Data: map[string]any{\"serial\": \"123\"}},\n\t\t\t},\n\t\t\texpectErr: false,\n\t\t\texpectTemplateData: x509util.TemplateData{\n\t\t\t\t\"Webhooks\": map[string]any{\n\t\t\t\t\t\"devices\": map[string]any{\"serial\": \"123\"},\n\t\t\t\t\t\"people\":  map[string]any{\"role\": \"bar\"},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t\"ok/x509 only\": {\n\t\t\tctl: &WebhookController{\n\t\t\t\tclient: http.DefaultClient,\n\t\t\t\twebhooks: []*Webhook{\n\t\t\t\t\t{Name: \"people\", Kind: \"ENRICHING\", CertType: linkedca.Webhook_SSH.String()},\n\t\t\t\t\t{Name: \"devices\", Kind: \"ENRICHING\"},\n\t\t\t\t},\n\t\t\t\tTemplateData: x509util.TemplateData{},\n\t\t\t\tcertType:     linkedca.Webhook_X509,\n\t\t\t},\n\t\t\tctx: withRequestID(t, context.Background(), \"reqID\"),\n\t\t\treq: &webhook.RequestBody{},\n\t\t\tresponses: []*webhook.ResponseBody{\n\t\t\t\t{Allow: true, Data: map[string]any{\"role\": \"bar\"}},\n\t\t\t\t{Allow: true, Data: map[string]any{\"serial\": \"123\"}},\n\t\t\t},\n\t\t\texpectErr: false,\n\t\t\texpectTemplateData: x509util.TemplateData{\n\t\t\t\t\"Webhooks\": map[string]any{\n\t\t\t\t\t\"devices\": map[string]any{\"serial\": \"123\"},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t\"ok/with options\": {\n\t\t\tctl: &WebhookController{\n\t\t\t\tclient:       http.DefaultClient,\n\t\t\t\twebhooks:     []*Webhook{{Name: \"people\", Kind: \"ENRICHING\"}},\n\t\t\t\tTemplateData: x509util.TemplateData{},\n\t\t\t\toptions:      []webhook.RequestBodyOption{webhook.WithX5CCertificate(cert)},\n\t\t\t},\n\t\t\tctx:                withRequestID(t, context.Background(), \"reqID\"),\n\t\t\treq:                &webhook.RequestBody{},\n\t\t\tresponses:          []*webhook.ResponseBody{{Allow: true, Data: map[string]any{\"role\": \"bar\"}}},\n\t\t\texpectErr:          false,\n\t\t\texpectTemplateData: x509util.TemplateData{\"Webhooks\": map[string]any{\"people\": map[string]any{\"role\": \"bar\"}}},\n\t\t\tassertRequest: func(t *testing.T, req *webhook.RequestBody) {\n\t\t\t\tkey, err := x509.MarshalPKIXPublicKey(cert.PublicKey)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tassert.Equal(t, &webhook.X5CCertificate{\n\t\t\t\t\tRaw:                cert.Raw,\n\t\t\t\t\tPublicKey:          key,\n\t\t\t\t\tPublicKeyAlgorithm: cert.PublicKeyAlgorithm.String(),\n\t\t\t\t\tNotBefore:          cert.NotBefore,\n\t\t\t\t\tNotAfter:           cert.NotAfter,\n\t\t\t\t}, req.X5CCertificate)\n\t\t\t},\n\t\t},\n\t\t\"deny\": {\n\t\t\tctl: &WebhookController{\n\t\t\t\tclient:       http.DefaultClient,\n\t\t\t\twebhooks:     []*Webhook{{Name: \"people\", Kind: \"ENRICHING\"}},\n\t\t\t\tTemplateData: x509util.TemplateData{},\n\t\t\t},\n\t\t\tctx:                withRequestID(t, context.Background(), \"reqID\"),\n\t\t\treq:                &webhook.RequestBody{},\n\t\t\tresponses:          []*webhook.ResponseBody{{Allow: false}},\n\t\t\texpectErr:          true,\n\t\t\texpectTemplateData: x509util.TemplateData{},\n\t\t\tassertError: func(t *testing.T, err error) {\n\t\t\t\tassert.Equal(t, ErrWebhookDenied, err)\n\t\t\t},\n\t\t},\n\t\t\"deny/with error\": {\n\t\t\tctl: &WebhookController{\n\t\t\t\tclient:       http.DefaultClient,\n\t\t\t\twebhooks:     []*Webhook{{Name: \"people\", Kind: \"ENRICHING\"}},\n\t\t\t\tTemplateData: x509util.TemplateData{},\n\t\t\t},\n\t\t\tctx: withRequestID(t, context.Background(), \"reqID\"),\n\t\t\treq: &webhook.RequestBody{},\n\t\t\tresponses: []*webhook.ResponseBody{{Allow: false, Error: &webhook.Error{\n\t\t\t\tCode: \"theCode\", Message: \"Some message\",\n\t\t\t}}},\n\t\t\texpectErr:          true,\n\t\t\texpectTemplateData: x509util.TemplateData{},\n\t\t\tassertError: func(t *testing.T, err error) {\n\t\t\t\tassert.Equal(t, &webhook.Error{\n\t\t\t\t\tCode: \"theCode\", Message: \"Some message\",\n\t\t\t\t}, err)\n\t\t\t},\n\t\t},\n\t\t\"fail/with options\": {\n\t\t\tctl: &WebhookController{\n\t\t\t\tclient:       http.DefaultClient,\n\t\t\t\twebhooks:     []*Webhook{{Name: \"people\", Kind: \"ENRICHING\"}},\n\t\t\t\tTemplateData: x509util.TemplateData{},\n\t\t\t\toptions: []webhook.RequestBodyOption{webhook.WithX5CCertificate(&x509.Certificate{\n\t\t\t\t\tPublicKey: []byte(\"bad\"),\n\t\t\t\t})},\n\t\t\t},\n\t\t\tctx:                withRequestID(t, context.Background(), \"reqID\"),\n\t\t\treq:                &webhook.RequestBody{},\n\t\t\tresponses:          []*webhook.ResponseBody{{Allow: false}},\n\t\t\texpectErr:          true,\n\t\t\texpectTemplateData: x509util.TemplateData{},\n\t\t},\n\t}\n\tfor name, test := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tfor i, wh := range test.ctl.webhooks {\n\t\t\t\tvar j = i\n\t\t\t\tts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\t\tassert.Equal(t, \"reqID\", r.Header.Get(\"X-Request-ID\"))\n\n\t\t\t\t\terr := json.NewEncoder(w).Encode(test.responses[j])\n\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t}))\n\t\t\t\t// nolint: gocritic // defer in loop isn't a memory leak\n\t\t\t\tdefer ts.Close()\n\t\t\t\twh.URL = ts.URL\n\t\t\t}\n\n\t\t\terr := test.ctl.Enrich(test.ctx, test.req)\n\t\t\tif (err != nil) != test.expectErr {\n\t\t\t\tt.Fatalf(\"Got err %v, want %v\", err, test.expectErr)\n\t\t\t}\n\t\t\tassert.Equal(t, test.expectTemplateData, test.ctl.TemplateData)\n\t\t\tif test.assertRequest != nil {\n\t\t\t\ttest.assertRequest(t, test.req)\n\t\t\t}\n\t\t\tif test.assertError != nil {\n\t\t\t\ttest.assertError(t, err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestWebhookController_Authorize(t *testing.T) {\n\tcert, err := pemutil.ReadCertificate(\"testdata/certs/x5c-leaf.crt\", pemutil.WithFirstBlock())\n\trequire.NoError(t, err)\n\n\ttype test struct {\n\t\tctl           *WebhookController\n\t\tctx           context.Context\n\t\treq           *webhook.RequestBody\n\t\tresponses     []*webhook.ResponseBody\n\t\texpectErr     bool\n\t\tassertRequest func(t *testing.T, req *webhook.RequestBody)\n\t\tassertError   func(t *testing.T, err error)\n\t}\n\ttests := map[string]test{\n\t\t\"ok/no enriching webhooks\": {\n\t\t\tctl: &WebhookController{\n\t\t\t\tclient:   http.DefaultClient,\n\t\t\t\twebhooks: []*Webhook{{Name: \"people\", Kind: \"ENRICHING\"}},\n\t\t\t},\n\t\t\treq:       &webhook.RequestBody{},\n\t\t\tresponses: nil,\n\t\t\texpectErr: false,\n\t\t},\n\t\t\"ok\": {\n\t\t\tctl: &WebhookController{\n\t\t\t\tclient:   http.DefaultClient,\n\t\t\t\twebhooks: []*Webhook{{Name: \"people\", Kind: \"AUTHORIZING\"}},\n\t\t\t},\n\t\t\tctx:       withRequestID(t, context.Background(), \"reqID\"),\n\t\t\treq:       &webhook.RequestBody{},\n\t\t\tresponses: []*webhook.ResponseBody{{Allow: true}},\n\t\t\texpectErr: false,\n\t\t},\n\t\t\"ok/ssh only\": {\n\t\t\tctl: &WebhookController{\n\t\t\t\tclient:   http.DefaultClient,\n\t\t\t\twebhooks: []*Webhook{{Name: \"people\", Kind: \"AUTHORIZING\", CertType: linkedca.Webhook_X509.String()}},\n\t\t\t\tcertType: linkedca.Webhook_SSH,\n\t\t\t},\n\t\t\tctx:       withRequestID(t, context.Background(), \"reqID\"),\n\t\t\treq:       &webhook.RequestBody{},\n\t\t\tresponses: []*webhook.ResponseBody{{Allow: false}},\n\t\t\texpectErr: false,\n\t\t},\n\t\t\"ok/with options\": {\n\t\t\tctl: &WebhookController{\n\t\t\t\tclient:   http.DefaultClient,\n\t\t\t\twebhooks: []*Webhook{{Name: \"people\", Kind: \"AUTHORIZING\"}},\n\t\t\t\toptions:  []webhook.RequestBodyOption{webhook.WithX5CCertificate(cert)},\n\t\t\t},\n\t\t\tctx:       withRequestID(t, context.Background(), \"reqID\"),\n\t\t\treq:       &webhook.RequestBody{},\n\t\t\tresponses: []*webhook.ResponseBody{{Allow: true}},\n\t\t\texpectErr: false,\n\t\t\tassertRequest: func(t *testing.T, req *webhook.RequestBody) {\n\t\t\t\tkey, err := x509.MarshalPKIXPublicKey(cert.PublicKey)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tassert.Equal(t, &webhook.X5CCertificate{\n\t\t\t\t\tRaw:                cert.Raw,\n\t\t\t\t\tPublicKey:          key,\n\t\t\t\t\tPublicKeyAlgorithm: cert.PublicKeyAlgorithm.String(),\n\t\t\t\t\tNotBefore:          cert.NotBefore,\n\t\t\t\t\tNotAfter:           cert.NotAfter,\n\t\t\t\t}, req.X5CCertificate)\n\t\t\t},\n\t\t},\n\t\t\"deny\": {\n\t\t\tctl: &WebhookController{\n\t\t\t\tclient:   http.DefaultClient,\n\t\t\t\twebhooks: []*Webhook{{Name: \"people\", Kind: \"AUTHORIZING\"}},\n\t\t\t},\n\t\t\tctx:       withRequestID(t, context.Background(), \"reqID\"),\n\t\t\treq:       &webhook.RequestBody{},\n\t\t\tresponses: []*webhook.ResponseBody{{Allow: false}},\n\t\t\texpectErr: true,\n\t\t\tassertError: func(t *testing.T, err error) {\n\t\t\t\tassert.Equal(t, ErrWebhookDenied, err)\n\t\t\t},\n\t\t},\n\t\t\"deny/withError\": {\n\t\t\tctl: &WebhookController{\n\t\t\t\tclient:   http.DefaultClient,\n\t\t\t\twebhooks: []*Webhook{{Name: \"people\", Kind: \"AUTHORIZING\"}},\n\t\t\t},\n\t\t\tctx: withRequestID(t, context.Background(), \"reqID\"),\n\t\t\treq: &webhook.RequestBody{},\n\t\t\tresponses: []*webhook.ResponseBody{{Allow: false, Error: &webhook.Error{\n\t\t\t\tCode: \"theCode\", Message: \"Some message\",\n\t\t\t}}},\n\t\t\texpectErr: true,\n\t\t\tassertError: func(t *testing.T, err error) {\n\t\t\t\tassert.Equal(t, &webhook.Error{\n\t\t\t\t\tCode: \"theCode\", Message: \"Some message\",\n\t\t\t\t}, err)\n\t\t\t},\n\t\t},\n\t\t\"fail/with options\": {\n\t\t\tctl: &WebhookController{\n\t\t\t\tclient:   http.DefaultClient,\n\t\t\t\twebhooks: []*Webhook{{Name: \"people\", Kind: \"AUTHORIZING\"}},\n\t\t\t\toptions: []webhook.RequestBodyOption{webhook.WithX5CCertificate(&x509.Certificate{\n\t\t\t\t\tPublicKey: []byte(\"bad\"),\n\t\t\t\t})},\n\t\t\t},\n\t\t\tctx:       withRequestID(t, context.Background(), \"reqID\"),\n\t\t\treq:       &webhook.RequestBody{},\n\t\t\tresponses: []*webhook.ResponseBody{{Allow: false}},\n\t\t\texpectErr: true,\n\t\t},\n\t}\n\tfor name, test := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tfor i, wh := range test.ctl.webhooks {\n\t\t\t\tvar j = i\n\t\t\t\tts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\t\tassert.Equal(t, \"reqID\", r.Header.Get(\"X-Request-ID\"))\n\n\t\t\t\t\terr := json.NewEncoder(w).Encode(test.responses[j])\n\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t}))\n\t\t\t\t// nolint: gocritic // defer in loop isn't a memory leak\n\t\t\t\tdefer ts.Close()\n\t\t\t\twh.URL = ts.URL\n\t\t\t}\n\n\t\t\terr := test.ctl.Authorize(test.ctx, test.req)\n\t\t\tif (err != nil) != test.expectErr {\n\t\t\t\tt.Fatalf(\"Got err %v, want %v\", err, test.expectErr)\n\t\t\t}\n\t\t\tif test.assertRequest != nil {\n\t\t\t\ttest.assertRequest(t, test.req)\n\t\t\t}\n\t\t\tif test.assertError != nil {\n\t\t\t\ttest.assertError(t, err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestWebhook_Do(t *testing.T) {\n\tcsr := parseCertificateRequest(t, \"testdata/certs/ecdsa.csr\")\n\ttype test struct {\n\t\twebhook         Webhook\n\t\tdataArg         any\n\t\trequestID       string\n\t\twebhookResponse webhook.ResponseBody\n\t\texpectPath      string\n\t\terrStatusCode   int\n\t\tserverErrMsg    string\n\t\texpectErr       error\n\t\t// expectToken     any\n\t}\n\ttests := map[string]test{\n\t\t\"ok\": {\n\t\t\twebhook: Webhook{\n\t\t\t\tID:     \"abc123\",\n\t\t\t\tSecret: \"c2VjcmV0Cg==\",\n\t\t\t},\n\t\t\trequestID: \"reqID\",\n\t\t\twebhookResponse: webhook.ResponseBody{\n\t\t\t\tData: map[string]interface{}{\"role\": \"dba\"},\n\t\t\t},\n\t\t},\n\t\t\"ok/no-request-id\": {\n\t\t\twebhook: Webhook{\n\t\t\t\tID:     \"abc123\",\n\t\t\t\tSecret: \"c2VjcmV0Cg==\",\n\t\t\t},\n\t\t\twebhookResponse: webhook.ResponseBody{\n\t\t\t\tData: map[string]interface{}{\"role\": \"dba\"},\n\t\t\t},\n\t\t},\n\t\t\"ok/bearer\": {\n\t\t\twebhook: Webhook{\n\t\t\t\tID:          \"abc123\",\n\t\t\t\tSecret:      \"c2VjcmV0Cg==\",\n\t\t\t\tBearerToken: \"mytoken\",\n\t\t\t},\n\t\t\trequestID: \"reqID\",\n\t\t\twebhookResponse: webhook.ResponseBody{\n\t\t\t\tData: map[string]interface{}{\"role\": \"dba\"},\n\t\t\t},\n\t\t},\n\t\t\"ok/basic\": {\n\t\t\twebhook: Webhook{\n\t\t\t\tID:     \"abc123\",\n\t\t\t\tSecret: \"c2VjcmV0Cg==\",\n\t\t\t\tBasicAuth: struct {\n\t\t\t\t\tUsername string\n\t\t\t\t\tPassword string\n\t\t\t\t}{\n\t\t\t\t\tUsername: \"myuser\",\n\t\t\t\t\tPassword: \"mypass\",\n\t\t\t\t},\n\t\t\t},\n\t\t\trequestID: \"reqID\",\n\t\t\twebhookResponse: webhook.ResponseBody{\n\t\t\t\tData: map[string]interface{}{\"role\": \"dba\"},\n\t\t\t},\n\t\t},\n\t\t\"ok/templated-url\": {\n\t\t\twebhook: Webhook{\n\t\t\t\tID: \"abc123\",\n\t\t\t\t// scheme, host, port will come from test server\n\t\t\t\tURL:    \"/users/{{ .username }}?region={{ .region }}\",\n\t\t\t\tSecret: \"c2VjcmV0Cg==\",\n\t\t\t},\n\t\t\trequestID: \"reqID\",\n\t\t\tdataArg:   map[string]interface{}{\"username\": \"areed\", \"region\": \"central\"},\n\t\t\twebhookResponse: webhook.ResponseBody{\n\t\t\t\tData: map[string]interface{}{\"role\": \"dba\"},\n\t\t\t},\n\t\t\texpectPath: \"/users/areed?region=central\",\n\t\t},\n\t\t/*\n\t\t\t\"ok/token from ssh template\": {\n\t\t\t\twebhook: Webhook{\n\t\t\t\t\tID:     \"abc123\",\n\t\t\t\t\tSecret: \"c2VjcmV0Cg==\",\n\t\t\t\t},\n\t\t\t\twebhookResponse: webhook.ResponseBody{\n\t\t\t\t\tData: map[string]interface{}{\"role\": \"dba\"},\n\t\t\t\t},\n\t\t\t\tdataArg:     sshutil.TemplateData{sshutil.TokenKey: \"token\"},\n\t\t\t\texpectToken: \"token\",\n\t\t\t},\n\t\t\t\"ok/token from x509 template\": {\n\t\t\t\twebhook: Webhook{\n\t\t\t\t\tID:     \"abc123\",\n\t\t\t\t\tSecret: \"c2VjcmV0Cg==\",\n\t\t\t\t},\n\t\t\t\twebhookResponse: webhook.ResponseBody{\n\t\t\t\t\tData: map[string]interface{}{\"role\": \"dba\"},\n\t\t\t\t},\n\t\t\t\tdataArg:     x509util.TemplateData{sshutil.TokenKey: \"token\"},\n\t\t\t\texpectToken: \"token\",\n\t\t\t},\n\t\t*/\n\t\t\"ok/allow\": {\n\t\t\twebhook: Webhook{\n\t\t\t\tID:     \"abc123\",\n\t\t\t\tSecret: \"c2VjcmV0Cg==\",\n\t\t\t},\n\t\t\trequestID: \"reqID\",\n\t\t\twebhookResponse: webhook.ResponseBody{\n\t\t\t\tAllow: true,\n\t\t\t},\n\t\t},\n\t\t\"fail/404\": {\n\t\t\twebhook: Webhook{\n\t\t\t\tID:     \"abc123\",\n\t\t\t\tSecret: \"c2VjcmV0Cg==\",\n\t\t\t},\n\t\t\twebhookResponse: webhook.ResponseBody{\n\t\t\t\tData: map[string]interface{}{\"role\": \"dba\"},\n\t\t\t},\n\t\t\trequestID:     \"reqID\",\n\t\t\terrStatusCode: 404,\n\t\t\tserverErrMsg:  \"item not found\",\n\t\t\texpectErr:     errors.New(\"Webhook server responded with 404\"),\n\t\t},\n\t}\n\tfor name, tc := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\tif tc.requestID != \"\" {\n\t\t\t\t\tassert.Equal(t, tc.requestID, r.Header.Get(\"X-Request-ID\"))\n\t\t\t\t}\n\n\t\t\t\tassert.Equal(t, tc.webhook.ID, r.Header.Get(\"X-Smallstep-Webhook-ID\"))\n\n\t\t\t\tsig, err := hex.DecodeString(r.Header.Get(\"X-Smallstep-Signature\"))\n\t\t\t\tassert.NoError(t, err)\n\n\t\t\t\tbody, err := io.ReadAll(r.Body)\n\t\t\t\tassert.NoError(t, err)\n\n\t\t\t\tsecret, err := base64.StdEncoding.DecodeString(tc.webhook.Secret)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\th := hmac.New(sha256.New, secret)\n\t\t\t\th.Write(body)\n\t\t\t\tmac := h.Sum(nil)\n\t\t\t\tassert.True(t, hmac.Equal(sig, mac))\n\n\t\t\t\tswitch {\n\t\t\t\tcase tc.webhook.BearerToken != \"\":\n\t\t\t\t\tah := fmt.Sprintf(\"Bearer %s\", tc.webhook.BearerToken)\n\t\t\t\t\tassert.Equal(t, ah, r.Header.Get(\"Authorization\"))\n\t\t\t\tcase tc.webhook.BasicAuth.Username != \"\" || tc.webhook.BasicAuth.Password != \"\":\n\t\t\t\t\twhReq, err := http.NewRequest(\"\", \"\", http.NoBody)\n\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t\twhReq.SetBasicAuth(tc.webhook.BasicAuth.Username, tc.webhook.BasicAuth.Password)\n\t\t\t\t\tah := whReq.Header.Get(\"Authorization\")\n\t\t\t\t\tassert.Equal(t, ah, whReq.Header.Get(\"Authorization\"))\n\t\t\t\tdefault:\n\t\t\t\t\tassert.Equal(t, \"\", r.Header.Get(\"Authorization\"))\n\t\t\t\t}\n\n\t\t\t\tif tc.expectPath != \"\" {\n\t\t\t\t\tassert.Equal(t, tc.expectPath, r.URL.Path+\"?\"+r.URL.RawQuery)\n\t\t\t\t}\n\n\t\t\t\tif tc.errStatusCode != 0 {\n\t\t\t\t\thttp.Error(w, tc.serverErrMsg, tc.errStatusCode)\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\treqBody := new(webhook.RequestBody)\n\t\t\t\terr = json.Unmarshal(body, reqBody)\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\terr = json.NewEncoder(w).Encode(tc.webhookResponse)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}))\n\t\t\tdefer ts.Close()\n\n\t\t\ttc.webhook.URL = ts.URL + tc.webhook.URL\n\n\t\t\treqBody, err := webhook.NewRequestBody(webhook.WithX509CertificateRequest(csr))\n\t\t\trequire.NoError(t, err)\n\n\t\t\tctx := context.Background()\n\t\t\tif tc.requestID != \"\" {\n\t\t\t\tctx = withRequestID(t, ctx, tc.requestID)\n\t\t\t}\n\t\t\tctx, cancel := context.WithTimeout(ctx, time.Second*10)\n\t\t\tdefer cancel()\n\n\t\t\tgot, err := tc.webhook.DoWithContext(ctx, http.DefaultClient, httptransport.NoopWrapper(), reqBody, tc.dataArg)\n\t\t\tif tc.expectErr != nil {\n\t\t\t\tassert.Equal(t, tc.expectErr.Error(), err.Error())\n\t\t\t\treturn\n\t\t\t}\n\t\t\tassert.NoError(t, err)\n\n\t\t\tassert.Equal(t, &tc.webhookResponse, got)\n\t\t})\n\t}\n\n\tt.Run(\"disableTLSClientAuth\", func(t *testing.T) {\n\t\tts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tw.Write([]byte(\"{}\"))\n\t\t}))\n\t\tts.TLS.ClientAuth = tls.RequireAnyClientCert\n\t\twh := Webhook{\n\t\t\tURL: ts.URL,\n\t\t}\n\t\tcert, err := tls.LoadX509KeyPair(\"testdata/certs/foo.crt\", \"testdata/secrets/foo.key\")\n\t\trequire.NoError(t, err)\n\n\t\ttransport := httptransport.New()\n\t\ttransport.TLSClientConfig = &tls.Config{\n\t\t\tInsecureSkipVerify: true,\n\t\t\tCertificates:       []tls.Certificate{cert},\n\t\t}\n\t\tclient := &http.Client{\n\t\t\tTransport: transport,\n\t\t}\n\t\treqBody, err := webhook.NewRequestBody(webhook.WithX509CertificateRequest(csr))\n\t\trequire.NoError(t, err)\n\n\t\tctx, cancel := context.WithTimeout(context.Background(), time.Second*10)\n\t\tdefer cancel()\n\n\t\t_, err = wh.DoWithContext(ctx, client, httptransport.NoopWrapper(), reqBody, nil)\n\t\trequire.NoError(t, err)\n\n\t\tctx, cancel = context.WithTimeout(context.Background(), time.Second*10)\n\t\tdefer cancel()\n\n\t\twh.DisableTLSClientAuth = true\n\t\t_, err = wh.DoWithContext(ctx, client, httptransport.NoopWrapper(), reqBody, nil)\n\t\trequire.Error(t, err)\n\t})\n}\n\nfunc TestWebhook_Validate(t *testing.T) {\n\ttests := []struct {\n\t\tname      string\n\t\twebhook   *Webhook\n\t\tassertion assert.ErrorAssertionFunc\n\t}{\n\t\t{\"ok enriching\", &Webhook{Name: \"devices\", URL: \"https://localhost:3000\", Kind: \"ENRICHING\"}, assert.NoError},\n\t\t{\"ok authorizing\", &Webhook{Name: \"devices\", URL: \"https://localhost:3000/devices\", Kind: \"AUTHORIZING\"}, assert.NoError},\n\t\t{\"fail name\", &Webhook{Name: \"\", URL: \"https://localhost:3000\", Kind: \"ENRICHING\"}, assert.Error},\n\t\t{\"fail url\", &Webhook{Name: \"devices\", URL: \"\", Kind: \"ENRICHING\"}, assert.Error},\n\t\t{\"fail bad url\", &Webhook{Name: \"devices\", URL: \"https://{{.Templated.Host}}\", Kind: \"ENRICHING\"}, assert.Error},\n\t\t{\"fail host\", &Webhook{Name: \"devices\", URL: \"https:opaque\", Kind: \"ENRICHING\"}, assert.Error},\n\t\t{\"fail scheme\", &Webhook{Name: \"devices\", URL: \"http://localhost\", Kind: \"ENRICHING\"}, assert.Error},\n\t\t{\"fail user\", &Webhook{Name: \"devices\", URL: \"https://user:pass@localhost\", Kind: \"ENRICHING\"}, assert.Error},\n\t\t{\"fail kind\", &Webhook{Name: \"devices\", URL: \"https://localhost:3000\", Kind: \"\"}, assert.Error},\n\t\t{\"fail bad kind\", &Webhook{Name: \"devices\", URL: \"https://localhost:3000\", Kind: \"SOMETHING\"}, assert.Error},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ttt.assertion(t, tt.webhook.Validate())\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "authority/provisioner/wire/dpop_options.go",
    "content": "package wire\n\nimport (\n\t\"bytes\"\n\t\"crypto\"\n\t\"errors\"\n\t\"fmt\"\n\t\"text/template\"\n\n\t\"go.step.sm/crypto/pemutil\"\n)\n\ntype DPOPOptions struct {\n\t// Public part of the  signing key for DPoP access token in PEM format\n\tSigningKey []byte `json:\"key\"`\n\t// URI template for the URI the ACME client must call to fetch the DPoP challenge proof (an access token from wire-server)\n\tTarget string `json:\"target\"`\n\n\tsigningKey crypto.PublicKey\n\ttarget     *template.Template\n}\n\nfunc (o *DPOPOptions) GetSigningKey() crypto.PublicKey {\n\treturn o.signingKey\n}\n\nfunc (o *DPOPOptions) EvaluateTarget(deviceID string) (string, error) {\n\tif deviceID == \"\" {\n\t\treturn \"\", errors.New(\"deviceID must not be empty\")\n\t}\n\tbuf := new(bytes.Buffer)\n\tif err := o.target.Execute(buf, struct{ DeviceID string }{DeviceID: deviceID}); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed executing DPoP template: %w\", err)\n\t}\n\treturn buf.String(), nil\n}\n\nfunc (o *DPOPOptions) validateAndInitialize() (err error) {\n\to.signingKey, err = pemutil.Parse(o.SigningKey)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed parsing key: %w\", err)\n\t}\n\to.target, err = template.New(\"DeviceID\").Parse(o.Target)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed parsing DPoP template: %w\", err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "authority/provisioner/wire/dpop_options_test.go",
    "content": "package wire\n\nimport (\n\t\"errors\"\n\t\"testing\"\n\t\"text/template\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestDPOPOptions_EvaluateTarget(t *testing.T) {\n\ttu := \"http://wire.com:15958/clients/{{.DeviceID}}/access-token\"\n\ttarget, err := template.New(\"DeviceID\").Parse(tu)\n\trequire.NoError(t, err)\n\tfail := \"https:/wire.com:15958/clients/{{.DeviceId}}/access-token\"\n\tfailTarget, err := template.New(\"DeviceID\").Parse(fail)\n\trequire.NoError(t, err)\n\ttype fields struct {\n\t\ttarget *template.Template\n\t}\n\ttype args struct {\n\t\tdeviceID string\n\t}\n\ttests := []struct {\n\t\tname        string\n\t\tfields      fields\n\t\targs        args\n\t\twant        string\n\t\texpectedErr error\n\t}{\n\t\t{\n\t\t\tname: \"ok\", fields: fields{target: target}, args: args{deviceID: \"deviceID\"}, want: \"http://wire.com:15958/clients/deviceID/access-token\",\n\t\t},\n\t\t{\n\t\t\tname: \"fail/empty\", fields: fields{target: target}, args: args{deviceID: \"\"}, expectedErr: errors.New(\"deviceID must not be empty\"),\n\t\t},\n\t\t{\n\t\t\tname: \"fail/template\", fields: fields{target: failTarget}, args: args{deviceID: \"bla\"}, expectedErr: errors.New(`failed executing DPoP template: template: DeviceID:1:32: executing \"DeviceID\" at <.DeviceId>: can't evaluate field DeviceId in type struct { DeviceID string }`),\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\to := &DPOPOptions{\n\t\t\t\ttarget: tt.fields.target,\n\t\t\t}\n\t\t\tgot, err := o.EvaluateTarget(tt.args.deviceID)\n\t\t\tif tt.expectedErr != nil {\n\t\t\t\tassert.EqualError(t, err, tt.expectedErr.Error())\n\t\t\t\tassert.Empty(t, got)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, tt.want, got)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "authority/provisioner/wire/oidc_options.go",
    "content": "package wire\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/url\"\n\t\"text/template\"\n\t\"time\"\n\n\t\"github.com/coreos/go-oidc/v3/oidc\"\n\t\"go.step.sm/crypto/x509util\"\n)\n\ntype Provider struct {\n\tDiscoveryBaseURL string   `json:\"discoveryBaseUrl,omitempty\"`\n\tIssuerURL        string   `json:\"issuerUrl,omitempty\"`\n\tAuthURL          string   `json:\"authorizationUrl,omitempty\"`\n\tTokenURL         string   `json:\"tokenUrl,omitempty\"`\n\tJWKSURL          string   `json:\"jwksUrl,omitempty\"`\n\tUserInfoURL      string   `json:\"userInfoUrl,omitempty\"`\n\tAlgorithms       []string `json:\"signatureAlgorithms,omitempty\"`\n}\n\ntype Config struct {\n\tClientID            string   `json:\"clientId,omitempty\"`\n\tSignatureAlgorithms []string `json:\"signatureAlgorithms,omitempty\"`\n\n\t// the properties below are only used for testing\n\tSkipClientIDCheck          bool             `json:\"-\"`\n\tSkipExpiryCheck            bool             `json:\"-\"`\n\tSkipIssuerCheck            bool             `json:\"-\"`\n\tInsecureSkipSignatureCheck bool             `json:\"-\"`\n\tNow                        func() time.Time `json:\"-\"`\n}\n\ntype OIDCOptions struct {\n\tProvider          *Provider `json:\"provider,omitempty\"`\n\tConfig            *Config   `json:\"config,omitempty\"`\n\tTransformTemplate string    `json:\"transform,omitempty\"`\n\n\ttarget             *template.Template\n\ttransform          *template.Template\n\toidcProviderConfig *oidc.ProviderConfig\n\tprovider           *oidc.Provider\n\tverifier           *oidc.IDTokenVerifier\n}\n\nfunc (o *OIDCOptions) GetVerifier(ctx context.Context) (*oidc.IDTokenVerifier, error) {\n\tif o.verifier == nil {\n\t\tswitch {\n\t\tcase o.Provider.DiscoveryBaseURL != \"\":\n\t\t\t// creates a new OIDC provider using automatic discovery and the default HTTP client\n\t\t\tprovider, err := oidc.NewProvider(ctx, o.Provider.DiscoveryBaseURL)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"failed creating new OIDC provider using discovery: %w\", err)\n\t\t\t}\n\t\t\to.provider = provider\n\t\tdefault:\n\t\t\to.provider = o.oidcProviderConfig.NewProvider(ctx)\n\t\t}\n\n\t\tif o.provider == nil {\n\t\t\treturn nil, errors.New(\"no OIDC provider available\")\n\t\t}\n\n\t\to.verifier = o.provider.Verifier(o.getConfig())\n\t}\n\n\treturn o.verifier, nil\n}\n\nfunc (o *OIDCOptions) getConfig() *oidc.Config {\n\tif o == nil || o.Config == nil {\n\t\treturn &oidc.Config{}\n\t}\n\n\treturn &oidc.Config{\n\t\tClientID:                   o.Config.ClientID,\n\t\tSupportedSigningAlgs:       o.Config.SignatureAlgorithms,\n\t\tSkipClientIDCheck:          o.Config.SkipClientIDCheck,\n\t\tSkipExpiryCheck:            o.Config.SkipExpiryCheck,\n\t\tSkipIssuerCheck:            o.Config.SkipIssuerCheck,\n\t\tNow:                        o.Config.Now,\n\t\tInsecureSkipSignatureCheck: o.Config.InsecureSkipSignatureCheck,\n\t}\n}\n\nconst defaultTemplate = `{\"name\": \"{{ .name }}\", \"preferred_username\": \"{{ .preferred_username }}\"}`\n\nfunc (o *OIDCOptions) validateAndInitialize() (err error) {\n\tif o.Provider == nil {\n\t\treturn errors.New(\"provider not set\")\n\t}\n\tif o.Provider.IssuerURL == \"\" && o.Provider.DiscoveryBaseURL == \"\" {\n\t\treturn errors.New(\"either OIDC discovery or issuer URL must be set\")\n\t}\n\n\tif o.Provider.DiscoveryBaseURL == \"\" {\n\t\to.oidcProviderConfig, err = toOIDCProviderConfig(o.Provider)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed creationg OIDC provider config: %w\", err)\n\t\t}\n\t}\n\n\to.target, err = template.New(\"DeviceID\").Parse(o.Provider.IssuerURL)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed parsing OIDC template: %w\", err)\n\t}\n\n\to.transform, err = parseTransform(o.TransformTemplate)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed parsing OIDC transformation template: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc parseTransform(transformTemplate string) (*template.Template, error) {\n\tif transformTemplate == \"\" {\n\t\ttransformTemplate = defaultTemplate\n\t}\n\n\treturn template.New(\"transform\").Funcs(x509util.GetFuncMap()).Parse(transformTemplate)\n}\n\nfunc (o *OIDCOptions) EvaluateTarget(deviceID string) (string, error) {\n\tbuf := new(bytes.Buffer)\n\tif err := o.target.Execute(buf, struct{ DeviceID string }{DeviceID: deviceID}); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed executing OIDC template: %w\", err)\n\t}\n\treturn buf.String(), nil\n}\n\nfunc (o *OIDCOptions) Transform(v map[string]any) (map[string]any, error) {\n\tif o.transform == nil || v == nil {\n\t\treturn v, nil\n\t}\n\t// TODO(hs): add support for extracting error message from template \"fail\" function?\n\tbuf := new(bytes.Buffer)\n\tif err := o.transform.Execute(buf, v); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed executing OIDC transformation: %w\", err)\n\t}\n\tvar r map[string]any\n\tif err := json.Unmarshal(buf.Bytes(), &r); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed unmarshaling transformed OIDC token: %w\", err)\n\t}\n\t// add original claims if not yet in the transformed result\n\tfor key, value := range v {\n\t\tif _, ok := r[key]; !ok {\n\t\t\tr[key] = value\n\t\t}\n\t}\n\treturn r, nil\n}\n\nfunc toOIDCProviderConfig(in *Provider) (*oidc.ProviderConfig, error) {\n\tissuerURL, err := url.Parse(in.IssuerURL)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed parsing issuer URL: %w\", err)\n\t}\n\t// Removes query params from the URL because we use it as a way to notify client about the actual OAuth ClientId\n\t// for this provisioner.\n\t// This URL is going to look like: \"https://idp:5556/dex?clientid=foo\"\n\t// If we don't trim the query params here i.e. 'clientid' then the idToken verification is going to fail because\n\t// the 'iss' claim of the idToken will be \"https://idp:5556/dex\"\n\tissuerURL.RawQuery = \"\"\n\tissuerURL.Fragment = \"\"\n\treturn &oidc.ProviderConfig{\n\t\tIssuerURL:   issuerURL.String(),\n\t\tAuthURL:     in.AuthURL,\n\t\tTokenURL:    in.TokenURL,\n\t\tUserInfoURL: in.UserInfoURL,\n\t\tJWKSURL:     in.JWKSURL,\n\t\tAlgorithms:  in.Algorithms,\n\t}, nil\n}\n"
  },
  {
    "path": "authority/provisioner/wire/oidc_options_test.go",
    "content": "package wire\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\t\"text/template\"\n\n\t\"github.com/coreos/go-oidc/v3/oidc\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"go.step.sm/crypto/jose\"\n)\n\nfunc TestOIDCOptions_Transform(t *testing.T) {\n\tdefaultTransform, err := parseTransform(``)\n\trequire.NoError(t, err)\n\tswapTransform, err := parseTransform(`{\"name\": \"{{ .preferred_username }}\", \"preferred_username\": \"{{ .name }}\"}`)\n\trequire.NoError(t, err)\n\tfuncTransform, err := parseTransform(`{\"name\": \"{{ .name }}\", \"preferred_username\": \"{{ first .usernames }}\"}`)\n\trequire.NoError(t, err)\n\ttype fields struct {\n\t\ttransform *template.Template\n\t}\n\ttype args struct {\n\t\tv map[string]any\n\t}\n\ttests := []struct {\n\t\tname        string\n\t\tfields      fields\n\t\targs        args\n\t\twant        map[string]any\n\t\texpectedErr error\n\t}{\n\t\t{\n\t\t\tname: \"ok/no-transform\",\n\t\t\tfields: fields{\n\t\t\t\ttransform: nil,\n\t\t\t},\n\t\t\targs: args{\n\t\t\t\tv: map[string]any{\n\t\t\t\t\t\"name\":               \"Example\",\n\t\t\t\t\t\"preferred_username\": \"Preferred\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: map[string]any{\n\t\t\t\t\"name\":               \"Example\",\n\t\t\t\t\"preferred_username\": \"Preferred\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"ok/empty-data\",\n\t\t\tfields: fields{\n\t\t\t\ttransform: nil,\n\t\t\t},\n\t\t\targs: args{\n\t\t\t\tv: map[string]any{},\n\t\t\t},\n\t\t\twant: map[string]any{},\n\t\t},\n\t\t{\n\t\t\tname: \"ok/default-transform\",\n\t\t\tfields: fields{\n\t\t\t\ttransform: defaultTransform,\n\t\t\t},\n\t\t\targs: args{\n\t\t\t\tv: map[string]any{\n\t\t\t\t\t\"name\":               \"Example\",\n\t\t\t\t\t\"preferred_username\": \"Preferred\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: map[string]any{\n\t\t\t\t\"name\":               \"Example\",\n\t\t\t\t\"preferred_username\": \"Preferred\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"ok/swap-transform\",\n\t\t\tfields: fields{\n\t\t\t\ttransform: swapTransform,\n\t\t\t},\n\t\t\targs: args{\n\t\t\t\tv: map[string]any{\n\t\t\t\t\t\"name\":               \"Example\",\n\t\t\t\t\t\"preferred_username\": \"Preferred\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: map[string]any{\n\t\t\t\t\"name\":               \"Preferred\",\n\t\t\t\t\"preferred_username\": \"Example\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"ok/transform-with-functions\",\n\t\t\tfields: fields{\n\t\t\t\ttransform: funcTransform,\n\t\t\t},\n\t\t\targs: args{\n\t\t\t\tv: map[string]any{\n\t\t\t\t\t\"name\":      \"Example\",\n\t\t\t\t\t\"usernames\": []string{\"name-1\", \"name-2\", \"name-3\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: map[string]any{\n\t\t\t\t\"name\":               \"Example\",\n\t\t\t\t\"preferred_username\": \"name-1\",\n\t\t\t\t\"usernames\":          []string{\"name-1\", \"name-2\", \"name-3\"},\n\t\t\t},\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\to := &OIDCOptions{\n\t\t\t\ttransform: tt.fields.transform,\n\t\t\t}\n\t\t\tgot, err := o.Transform(tt.args.v)\n\t\t\tif tt.expectedErr != nil {\n\t\t\t\tassert.Error(t, err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.Equal(t, tt.want, got)\n\t\t})\n\t}\n}\n\nfunc TestOIDCOptions_EvaluateTarget(t *testing.T) {\n\ttu := \"http://target.example.com/{{.DeviceID}}\"\n\ttarget, err := template.New(\"DeviceID\").Parse(tu)\n\trequire.NoError(t, err)\n\tempty := \"http://target.example.com\"\n\temptyTarget, err := template.New(\"DeviceID\").Parse(empty)\n\trequire.NoError(t, err)\n\tfail := \"https:/wire.com:15958/clients/{{.DeviceId}}/access-token\"\n\tfailTarget, err := template.New(\"DeviceID\").Parse(fail)\n\trequire.NoError(t, err)\n\ttype fields struct {\n\t\ttarget *template.Template\n\t}\n\ttype args struct {\n\t\tdeviceID string\n\t}\n\ttests := []struct {\n\t\tname        string\n\t\tfields      fields\n\t\targs        args\n\t\twant        string\n\t\texpectedErr error\n\t}{\n\t\t{\n\t\t\tname: \"ok\", fields: fields{target: target}, args: args{deviceID: \"deviceID\"}, want: \"http://target.example.com/deviceID\",\n\t\t},\n\t\t{\n\t\t\tname: \"ok/empty\", fields: fields{target: emptyTarget}, args: args{deviceID: \"\"}, want: \"http://target.example.com\",\n\t\t},\n\t\t{\n\t\t\tname: \"fail/template\", fields: fields{target: failTarget}, args: args{deviceID: \"bla\"}, expectedErr: errors.New(`failed executing OIDC template: template: DeviceID:1:32: executing \"DeviceID\" at <.DeviceId>: can't evaluate field DeviceId in type struct { DeviceID string }`),\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\to := &OIDCOptions{\n\t\t\t\ttarget: tt.fields.target,\n\t\t\t}\n\t\t\tgot, err := o.EvaluateTarget(tt.args.deviceID)\n\t\t\tif tt.expectedErr != nil {\n\t\t\t\tassert.EqualError(t, err, tt.expectedErr.Error())\n\t\t\t\tassert.Empty(t, got)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, tt.want, got)\n\t\t})\n\t}\n}\n\nfunc TestOIDCOptions_GetVerifier(t *testing.T) {\n\tsignerJWK, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\trequire.NoError(t, err)\n\trequire.NoError(t, err)\n\tsrv := mustDiscoveryServer(t, signerJWK.Public())\n\tdefer srv.Close()\n\ttype fields struct {\n\t\tProvider          *Provider\n\t\tConfig            *Config\n\t\tTransformTemplate string\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tfields  fields\n\t\tctx     context.Context\n\t\twant    *oidc.IDTokenVerifier\n\t\twantErr bool\n\t}{\n\t\t{\n\t\t\tname: \"fail/invalid-discovery-url\",\n\t\t\tfields: fields{\n\t\t\t\tProvider: &Provider{\n\t\t\t\t\tDiscoveryBaseURL: \"http://invalid.example.com\",\n\t\t\t\t},\n\t\t\t\tConfig: &Config{\n\t\t\t\t\tClientID: \"client-id\",\n\t\t\t\t},\n\t\t\t\tTransformTemplate: \"http://target.example.com/{{.DeviceID}}\",\n\t\t\t},\n\t\t\tctx:     context.Background(),\n\t\t\twantErr: true,\n\t\t},\n\t\t{\n\t\t\tname: \"ok/auto\",\n\t\t\tfields: fields{\n\t\t\t\tProvider: &Provider{\n\t\t\t\t\tDiscoveryBaseURL: srv.URL,\n\t\t\t\t},\n\t\t\t\tConfig: &Config{\n\t\t\t\t\tClientID: \"client-id\",\n\t\t\t\t},\n\t\t\t\tTransformTemplate: \"http://target.example.com/{{.DeviceID}}\",\n\t\t\t},\n\t\t\tctx: context.Background(),\n\t\t},\n\t\t{\n\t\t\tname: \"ok/fixed\",\n\t\t\tfields: fields{\n\t\t\t\tProvider: &Provider{\n\t\t\t\t\tIssuerURL: \"http://issuer.example.com\",\n\t\t\t\t},\n\t\t\t\tConfig: &Config{\n\t\t\t\t\tClientID: \"client-id\",\n\t\t\t\t},\n\t\t\t\tTransformTemplate: \"http://target.example.com/{{.DeviceID}}\",\n\t\t\t},\n\t\t\tctx: context.Background(),\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\to := &OIDCOptions{\n\t\t\t\tProvider:          tt.fields.Provider,\n\t\t\t\tConfig:            tt.fields.Config,\n\t\t\t\tTransformTemplate: tt.fields.TransformTemplate,\n\t\t\t}\n\n\t\t\terr := o.validateAndInitialize()\n\t\t\trequire.NoError(t, err)\n\n\t\t\tverifier, err := o.GetVerifier(tt.ctx)\n\t\t\tif tt.wantErr {\n\t\t\t\tassert.Error(t, err)\n\t\t\t\tassert.Nil(t, verifier)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.NotNil(t, verifier)\n\t\t\tif assert.NotNil(t, o.provider) {\n\t\t\t\tassert.NotNil(t, o.provider.Endpoint())\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc mustDiscoveryServer(t *testing.T, pub jose.JSONWebKey) *httptest.Server {\n\tt.Helper()\n\tmux := http.NewServeMux()\n\tserver := httptest.NewServer(mux)\n\tb, err := json.Marshal(struct {\n\t\tKeys []jose.JSONWebKey `json:\"keys,omitempty\"`\n\t}{\n\t\tKeys: []jose.JSONWebKey{pub},\n\t})\n\trequire.NoError(t, err)\n\tjwks := string(b)\n\n\twellKnown := fmt.Sprintf(`{\n\t\t\"issuer\": \"%[1]s\",\n\t\t\"authorization_endpoint\": \"%[1]s/auth\",\n\t\t\"token_endpoint\": \"%[1]s/token\",\n\t\t\"jwks_uri\": \"%[1]s/keys\",\n\t\t\"userinfo_endpoint\": \"%[1]s/userinfo\",\n\t\t\"id_token_signing_alg_values_supported\": [\"ES256\"]\n\t}`, server.URL)\n\n\tmux.HandleFunc(\"/.well-known/openid-configuration\", func(w http.ResponseWriter, req *http.Request) {\n\t\t_, err := io.WriteString(w, wellKnown)\n\t\tif err != nil {\n\t\t\tw.WriteHeader(500)\n\t\t}\n\t})\n\tmux.HandleFunc(\"/keys\", func(w http.ResponseWriter, req *http.Request) {\n\t\t_, err := io.WriteString(w, jwks)\n\t\tif err != nil {\n\t\t\tw.WriteHeader(500)\n\t\t}\n\t})\n\n\tt.Cleanup(server.Close)\n\treturn server\n}\n"
  },
  {
    "path": "authority/provisioner/wire/wire_options.go",
    "content": "package wire\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n)\n\n// Options holds the Wire ACME extension options\ntype Options struct {\n\tOIDC *OIDCOptions `json:\"oidc,omitempty\"`\n\tDPOP *DPOPOptions `json:\"dpop,omitempty\"`\n}\n\n// GetOIDCOptions returns the OIDC options.\nfunc (o *Options) GetOIDCOptions() *OIDCOptions {\n\tif o == nil {\n\t\treturn nil\n\t}\n\treturn o.OIDC\n}\n\n// GetDPOPOptions returns the DPoP options.\nfunc (o *Options) GetDPOPOptions() *DPOPOptions {\n\tif o == nil {\n\t\treturn nil\n\t}\n\treturn o.DPOP\n}\n\n// Validate validates and initializes the Wire OIDC and DPoP options.\n//\n// TODO(hs): find a good way to perform this only once.\nfunc (o *Options) Validate() error {\n\tif oidc := o.GetOIDCOptions(); oidc != nil {\n\t\tif err := oidc.validateAndInitialize(); err != nil {\n\t\t\treturn fmt.Errorf(\"failed initializing OIDC options: %w\", err)\n\t\t}\n\t} else {\n\t\treturn errors.New(\"no OIDC options available\")\n\t}\n\n\tif dpop := o.GetDPOPOptions(); dpop != nil {\n\t\tif err := dpop.validateAndInitialize(); err != nil {\n\t\t\treturn fmt.Errorf(\"failed initializing DPoP options: %w\", err)\n\t\t}\n\t} else {\n\t\treturn errors.New(\"no DPoP options available\")\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "authority/provisioner/wire/wire_options_test.go",
    "content": "package wire\n\nimport (\n\t\"errors\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestOptions_Validate(t *testing.T) {\n\tkey := []byte(`-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEA5c+4NKZSNQcR1T8qN6SjwgdPZQ0Ge12Ylx/YeGAJ35k=\n-----END PUBLIC KEY-----`)\n\n\ttype fields struct {\n\t\tOIDC *OIDCOptions\n\t\tDPOP *DPOPOptions\n\t}\n\ttests := []struct {\n\t\tname        string\n\t\tfields      fields\n\t\texpectedErr error\n\t}{\n\t\t{\n\t\t\tname: \"ok\",\n\t\t\tfields: fields{\n\t\t\t\tOIDC: &OIDCOptions{\n\t\t\t\t\tProvider: &Provider{\n\t\t\t\t\t\tIssuerURL: \"https://example.com\",\n\t\t\t\t\t},\n\t\t\t\t\tConfig: &Config{},\n\t\t\t\t},\n\t\t\t\tDPOP: &DPOPOptions{\n\t\t\t\t\tSigningKey: key,\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedErr: nil,\n\t\t},\n\t\t{\n\t\t\tname: \"fail/no-oidc-options\",\n\t\t\tfields: fields{\n\t\t\t\tOIDC: nil,\n\t\t\t\tDPOP: &DPOPOptions{},\n\t\t\t},\n\t\t\texpectedErr: errors.New(\"no OIDC options available\"),\n\t\t},\n\t\t{\n\t\t\tname: \"fail/empty-issuer-url\",\n\t\t\tfields: fields{\n\t\t\t\tOIDC: &OIDCOptions{\n\t\t\t\t\tProvider: &Provider{\n\t\t\t\t\t\tIssuerURL: \"\",\n\t\t\t\t\t},\n\t\t\t\t\tConfig: &Config{},\n\t\t\t\t},\n\t\t\t\tDPOP: &DPOPOptions{},\n\t\t\t},\n\t\t\texpectedErr: errors.New(\"failed initializing OIDC options: either OIDC discovery or issuer URL must be set\"),\n\t\t},\n\t\t{\n\t\t\tname: \"fail/invalid-issuer-url\",\n\t\t\tfields: fields{\n\t\t\t\tOIDC: &OIDCOptions{\n\t\t\t\t\tProvider: &Provider{\n\t\t\t\t\t\tIssuerURL: \"\\x00\",\n\t\t\t\t\t},\n\t\t\t\t\tConfig: &Config{},\n\t\t\t\t},\n\t\t\t\tDPOP: &DPOPOptions{},\n\t\t\t},\n\t\t\texpectedErr: errors.New(`failed initializing OIDC options: failed creationg OIDC provider config: failed parsing issuer URL: parse \"\\x00\": net/url: invalid control character in URL`),\n\t\t},\n\t\t{\n\t\t\tname: \"fail/issuer-url-template\",\n\t\t\tfields: fields{\n\t\t\t\tOIDC: &OIDCOptions{\n\t\t\t\t\tProvider: &Provider{\n\t\t\t\t\t\tIssuerURL: \"https://issuer.example.com/{{}\",\n\t\t\t\t\t},\n\t\t\t\t\tConfig: &Config{},\n\t\t\t\t},\n\t\t\t\tDPOP: &DPOPOptions{},\n\t\t\t},\n\t\t\texpectedErr: errors.New(`failed initializing OIDC options: failed parsing OIDC template: template: DeviceID:1: unexpected \"}\" in command`),\n\t\t},\n\t\t{\n\t\t\tname: \"fail/invalid-transform-template\",\n\t\t\tfields: fields{\n\t\t\t\tOIDC: &OIDCOptions{\n\t\t\t\t\tProvider: &Provider{\n\t\t\t\t\t\tIssuerURL: \"https://example.com\",\n\t\t\t\t\t},\n\t\t\t\t\tConfig:            &Config{},\n\t\t\t\t\tTransformTemplate: \"{{}\",\n\t\t\t\t},\n\t\t\t\tDPOP: &DPOPOptions{\n\t\t\t\t\tSigningKey: key,\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedErr: errors.New(`failed initializing OIDC options: failed parsing OIDC transformation template: template: transform:1: unexpected \"}\" in command`),\n\t\t},\n\t\t{\n\t\t\tname: \"fail/no-dpop-options\",\n\t\t\tfields: fields{\n\t\t\t\tOIDC: &OIDCOptions{\n\t\t\t\t\tProvider: &Provider{\n\t\t\t\t\t\tIssuerURL: \"https://example.com\",\n\t\t\t\t\t},\n\t\t\t\t\tConfig: &Config{},\n\t\t\t\t},\n\t\t\t\tDPOP: nil,\n\t\t\t},\n\t\t\texpectedErr: errors.New(\"no DPoP options available\"),\n\t\t},\n\t\t{\n\t\t\tname: \"fail/invalid-key\",\n\t\t\tfields: fields{\n\t\t\t\tOIDC: &OIDCOptions{\n\t\t\t\t\tProvider: &Provider{\n\t\t\t\t\t\tIssuerURL: \"https://example.com\",\n\t\t\t\t\t},\n\t\t\t\t\tConfig: &Config{},\n\t\t\t\t},\n\t\t\t\tDPOP: &DPOPOptions{\n\t\t\t\t\tSigningKey: []byte{0x00},\n\t\t\t\t\tTarget:     \"\",\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedErr: errors.New(`failed initializing DPoP options: failed parsing key: error decoding PEM: not a valid PEM encoded block`),\n\t\t},\n\t\t{\n\t\t\tname: \"fail/target-template\",\n\t\t\tfields: fields{\n\t\t\t\tOIDC: &OIDCOptions{\n\t\t\t\t\tProvider: &Provider{\n\t\t\t\t\t\tIssuerURL: \"https://example.com\",\n\t\t\t\t\t},\n\t\t\t\t\tConfig: &Config{},\n\t\t\t\t},\n\t\t\t\tDPOP: &DPOPOptions{\n\t\t\t\t\tSigningKey: key,\n\t\t\t\t\tTarget:     \"{{}\",\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedErr: errors.New(`failed initializing DPoP options: failed parsing DPoP template: template: DeviceID:1: unexpected \"}\" in command`),\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\to := &Options{\n\t\t\t\tOIDC: tt.fields.OIDC,\n\t\t\t\tDPOP: tt.fields.DPOP,\n\t\t\t}\n\t\t\terr := o.Validate()\n\t\t\tif tt.expectedErr != nil {\n\t\t\t\tassert.EqualError(t, err, tt.expectedErr.Error())\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.NoError(t, err)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "authority/provisioner/x5c.go",
    "content": "package provisioner\n\nimport (\n\t\"context\"\n\t\"crypto/x509\"\n\t\"encoding/pem\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n\n\t\"github.com/smallstep/linkedca\"\n\t\"go.step.sm/crypto/jose\"\n\t\"go.step.sm/crypto/sshutil\"\n\t\"go.step.sm/crypto/x509util\"\n\n\t\"github.com/smallstep/certificates/errs\"\n\t\"github.com/smallstep/certificates/internal/cast\"\n\t\"github.com/smallstep/certificates/webhook\"\n)\n\n// x5cPayload extends jwt.Claims with step attributes.\ntype x5cPayload struct {\n\tjose.Claims\n\tSANs         []string     `json:\"sans,omitempty\"`\n\tStep         *stepPayload `json:\"step,omitempty\"`\n\tConfirmation *cnfPayload  `json:\"cnf,omitempty\"`\n\tchains       [][]*x509.Certificate\n}\n\n// X5C is the default provisioner, an entity that can sign tokens necessary for\n// signature requests.\ntype X5C struct {\n\t*base\n\tID       string   `json:\"-\"`\n\tType     string   `json:\"type\"`\n\tName     string   `json:\"name\"`\n\tRoots    []byte   `json:\"roots\"`\n\tClaims   *Claims  `json:\"claims,omitempty\"`\n\tOptions  *Options `json:\"options,omitempty\"`\n\tctl      *Controller\n\trootPool *x509.CertPool\n}\n\n// GetID returns the provisioner unique identifier. The name and credential id\n// should uniquely identify any X5C provisioner.\nfunc (p *X5C) GetID() string {\n\tif p.ID != \"\" {\n\t\treturn p.ID\n\t}\n\treturn p.GetIDForToken()\n}\n\n// GetIDForToken returns an identifier that will be used to load the provisioner\n// from a token.\nfunc (p *X5C) GetIDForToken() string {\n\treturn \"x5c/\" + p.Name\n}\n\n// GetTokenID returns the identifier of the token.\nfunc (p *X5C) GetTokenID(ott string) (string, error) {\n\t// Validate payload\n\ttoken, err := jose.ParseSigned(ott)\n\tif err != nil {\n\t\treturn \"\", errors.Wrap(err, \"error parsing token\")\n\t}\n\n\t// Get claims w/out verification. We need to look up the provisioner\n\t// key in order to verify the claims and we need the issuer from the claims\n\t// before we can look up the provisioner.\n\tvar claims jose.Claims\n\tif err = token.UnsafeClaimsWithoutVerification(&claims); err != nil {\n\t\treturn \"\", errors.Wrap(err, \"error verifying claims\")\n\t}\n\treturn claims.ID, nil\n}\n\n// GetName returns the name of the provisioner.\nfunc (p *X5C) GetName() string {\n\treturn p.Name\n}\n\n// GetType returns the type of provisioner.\nfunc (p *X5C) GetType() Type {\n\treturn TypeX5C\n}\n\n// GetEncryptedKey returns the base provisioner encrypted key if it's defined.\nfunc (p *X5C) GetEncryptedKey() (string, string, bool) {\n\treturn \"\", \"\", false\n}\n\n// Init initializes and validates the fields of a X5C type.\nfunc (p *X5C) Init(config Config) (err error) {\n\tswitch {\n\tcase p.Type == \"\":\n\t\treturn errors.New(\"provisioner type cannot be empty\")\n\tcase p.Name == \"\":\n\t\treturn errors.New(\"provisioner name cannot be empty\")\n\tcase len(p.Roots) == 0:\n\t\treturn errors.New(\"provisioner root(s) cannot be empty\")\n\t}\n\n\tp.rootPool = x509.NewCertPool()\n\n\tvar (\n\t\tblock *pem.Block\n\t\trest  = p.Roots\n\t\tcount int\n\t)\n\tfor rest != nil {\n\t\tblock, rest = pem.Decode(rest)\n\t\tif block == nil {\n\t\t\tbreak\n\t\t}\n\t\tcert, err := x509.ParseCertificate(block.Bytes)\n\t\tif err != nil {\n\t\t\treturn errors.Wrap(err, \"error parsing x509 certificate from PEM block\")\n\t\t}\n\t\tcount++\n\t\tp.rootPool.AddCert(cert)\n\t}\n\n\t// Verify that at least one root was found.\n\tif count == 0 {\n\t\treturn errors.Errorf(\"no x509 certificates found in roots attribute for provisioner '%s'\", p.GetName())\n\t}\n\n\tconfig.Audiences = config.Audiences.WithFragment(p.GetIDForToken())\n\tp.ctl, err = NewController(p, p.Claims, config, p.Options)\n\treturn\n}\n\n// authorizeToken performs common jwt authorization actions and returns the\n// claims for case specific downstream parsing.\n// e.g. a Sign request will auth/validate different fields than a Revoke request.\nfunc (p *X5C) authorizeToken(token string, audiences []string) (*x5cPayload, error) {\n\tjwt, err := jose.ParseSigned(token)\n\tif err != nil {\n\t\treturn nil, errs.Wrap(http.StatusUnauthorized, err, \"x5c.authorizeToken; error parsing x5c token\")\n\t}\n\n\tverifiedChains, err := jwt.Headers[0].Certificates(x509.VerifyOptions{\n\t\tRoots:     p.rootPool,\n\t\tKeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},\n\t})\n\tif err != nil {\n\t\treturn nil, errs.Wrap(http.StatusUnauthorized, err,\n\t\t\t\"x5c.authorizeToken; error verifying x5c certificate chain in token\")\n\t}\n\tleaf := verifiedChains[0][0]\n\n\tif leaf.KeyUsage&x509.KeyUsageDigitalSignature == 0 {\n\t\treturn nil, errs.Unauthorized(\"x5c.authorizeToken; certificate used to sign x5c token cannot be used for digital signature\")\n\t}\n\n\t// Using the leaf certificate's key to validate the claims accomplishes two\n\t// things:\n\t//   1. Asserts that the private key used to sign the token corresponds\n\t//      to the public certificate in the `x5c` header of the token.\n\t//   2. Asserts that the claims are valid - have not been tampered with.\n\tvar claims x5cPayload\n\tif err = jwt.Claims(leaf.PublicKey, &claims); err != nil {\n\t\treturn nil, errs.Wrap(http.StatusUnauthorized, err, \"x5c.authorizeToken; error parsing x5c claims\")\n\t}\n\n\t// According to \"rfc7519 JSON Web Token\" acceptable skew should be no\n\t// more than a few minutes.\n\tif err = claims.ValidateWithLeeway(jose.Expected{\n\t\tIssuer: p.Name,\n\t\tTime:   time.Now().UTC(),\n\t}, time.Minute); err != nil {\n\t\treturn nil, errs.Wrapf(http.StatusUnauthorized, err, \"x5c.authorizeToken; invalid x5c claims\")\n\t}\n\n\t// validate audiences with the defaults\n\tif !matchesAudience(claims.Audience, audiences) {\n\t\treturn nil, errs.Unauthorized(\"x5c.authorizeToken; x5c token has invalid audience \"+\n\t\t\t\"claim (aud); expected %s, but got %s\", audiences, claims.Audience)\n\t}\n\n\tif claims.Subject == \"\" {\n\t\treturn nil, errs.Unauthorized(\"x5c.authorizeToken; x5c token subject cannot be empty\")\n\t}\n\n\t// Save the verified chains on the x5c payload object.\n\tclaims.chains = verifiedChains\n\treturn &claims, nil\n}\n\n// AuthorizeRevoke returns an error if the provisioner does not have rights to\n// revoke the certificate with serial number in the `sub` property.\nfunc (p *X5C) AuthorizeRevoke(_ context.Context, token string) error {\n\t_, err := p.authorizeToken(token, p.ctl.Audiences.Revoke)\n\treturn errs.Wrap(http.StatusInternalServerError, err, \"x5c.AuthorizeRevoke\")\n}\n\n// AuthorizeSign validates the given token.\nfunc (p *X5C) AuthorizeSign(ctx context.Context, token string) ([]SignOption, error) {\n\tclaims, err := p.authorizeToken(token, p.ctl.Audiences.Sign)\n\tif err != nil {\n\t\treturn nil, errs.Wrap(http.StatusInternalServerError, err, \"x5c.AuthorizeSign\")\n\t}\n\n\t// NOTE: This is for backwards compatibility with older versions of cli\n\t// and certificates. Older versions added the token subject as the only SAN\n\t// in a CSR by default.\n\tif len(claims.SANs) == 0 {\n\t\tclaims.SANs = []string{claims.Subject}\n\t}\n\n\t// Certificate templates\n\tdata := x509util.CreateTemplateData(claims.Subject, claims.SANs)\n\tif v, err := unsafeParseSigned(token); err == nil {\n\t\tdata.SetToken(v)\n\t}\n\n\t// The X509 certificate will be available using the template variable\n\t// AuthorizationCrt. For example {{ .AuthorizationCrt.DNSNames }} can be\n\t// used to get all the domains.\n\tx5cLeaf := claims.chains[0][0]\n\tdata.SetAuthorizationCertificate(x5cLeaf)\n\n\ttemplateOptions, err := TemplateOptions(p.Options, data)\n\tif err != nil {\n\t\treturn nil, errs.Wrap(http.StatusInternalServerError, err, \"jwk.AuthorizeSign\")\n\t}\n\n\t// Wrap provisioner if the token is an RA token.\n\tvar self Interface = p\n\tif claims.Step != nil && claims.Step.RA != nil {\n\t\tself = &raProvisioner{\n\t\t\tInterface: p,\n\t\t\traInfo:    claims.Step.RA,\n\t\t}\n\t}\n\n\t// Check the fingerprint of the certificate request if given.\n\tvar fingerprint string\n\tif claims.Confirmation != nil {\n\t\tfingerprint = claims.Confirmation.Fingerprint\n\t}\n\n\treturn []SignOption{\n\t\tself,\n\t\ttemplateOptions,\n\t\t// modifiers / withOptions\n\t\tnewProvisionerExtensionOption(TypeX5C, p.Name, \"\").WithControllerOptions(p.ctl),\n\t\tprofileLimitDuration{\n\t\t\tp.ctl.Claimer.DefaultTLSCertDuration(),\n\t\t\tx5cLeaf.NotBefore, x5cLeaf.NotAfter,\n\t\t},\n\t\t// validators\n\t\tcsrFingerprintValidator(fingerprint),\n\t\tcommonNameValidator(claims.Subject),\n\t\tnewDefaultSANsValidator(ctx, claims.SANs),\n\t\tdefaultPublicKeyValidator{},\n\t\tnewValidityValidator(p.ctl.Claimer.MinTLSCertDuration(), p.ctl.Claimer.MaxTLSCertDuration()),\n\t\tnewX509NamePolicyValidator(p.ctl.getPolicy().getX509()),\n\t\tp.ctl.newWebhookController(\n\t\t\tdata,\n\t\t\tlinkedca.Webhook_X509,\n\t\t\twebhook.WithX5CCertificate(x5cLeaf),\n\t\t\twebhook.WithAuthorizationPrincipal(x5cLeaf.Subject.CommonName),\n\t\t),\n\t}, nil\n}\n\n// AuthorizeRenew returns an error if the renewal is disabled.\nfunc (p *X5C) AuthorizeRenew(ctx context.Context, cert *x509.Certificate) error {\n\treturn p.ctl.AuthorizeRenew(ctx, cert)\n}\n\n// AuthorizeSSHSign returns the list of SignOption for a SignSSH request.\nfunc (p *X5C) AuthorizeSSHSign(_ context.Context, token string) ([]SignOption, error) {\n\tif !p.ctl.Claimer.IsSSHCAEnabled() {\n\t\treturn nil, errs.Unauthorized(\"x5c.AuthorizeSSHSign; sshCA is disabled for x5c provisioner '%s'\", p.GetName())\n\t}\n\n\tclaims, err := p.authorizeToken(token, p.ctl.Audiences.SSHSign)\n\tif err != nil {\n\t\treturn nil, errs.Wrap(http.StatusInternalServerError, err, \"x5c.AuthorizeSSHSign\")\n\t}\n\n\tif claims.Step == nil || claims.Step.SSH == nil {\n\t\treturn nil, errs.Unauthorized(\"x5c.AuthorizeSSHSign; x5c token must be an SSH provisioning token\")\n\t}\n\n\topts := claims.Step.SSH\n\tsignOptions := []SignOption{\n\t\t// validates user's SSHOptions with the ones in the token\n\t\tsshCertOptionsValidator(*opts),\n\t\t// validate users's KeyID is the token subject.\n\t\tsshCertOptionsValidator(SignSSHOptions{KeyID: claims.Subject}),\n\t}\n\n\t// Default template attributes.\n\tcertType := sshutil.UserCert\n\tkeyID := claims.Subject\n\tprincipals := []string{claims.Subject}\n\n\t// Use options in the token.\n\tif opts.CertType != \"\" {\n\t\tif certType, err = sshutil.CertTypeFromString(opts.CertType); err != nil {\n\t\t\treturn nil, errs.BadRequestErr(err, \"%s\", err.Error())\n\t\t}\n\t}\n\tif opts.KeyID != \"\" {\n\t\tkeyID = opts.KeyID\n\t}\n\tif len(opts.Principals) > 0 {\n\t\tprincipals = opts.Principals\n\t}\n\n\t// Certificate templates.\n\tdata := sshutil.CreateTemplateData(certType, keyID, principals)\n\tif v, err := unsafeParseSigned(token); err == nil {\n\t\tdata.SetToken(v)\n\t}\n\n\t// The X509 certificate will be available using the template variable\n\t// AuthorizationCrt. For example {{ .AuthorizationCrt.DNSNames }} can be\n\t// used to get all the domains.\n\tx5cLeaf := claims.chains[0][0]\n\tdata.SetAuthorizationCertificate(x5cLeaf)\n\n\ttemplateOptions, err := TemplateSSHOptions(p.Options, data)\n\tif err != nil {\n\t\treturn nil, errs.Wrap(http.StatusInternalServerError, err, \"x5c.AuthorizeSSHSign\")\n\t}\n\tsignOptions = append(signOptions, templateOptions)\n\n\t// Add modifiers from custom claims\n\tt := now()\n\tif !opts.ValidAfter.IsZero() {\n\t\tsignOptions = append(signOptions, sshCertValidAfterModifier(cast.Uint64(opts.ValidAfter.RelativeTime(t).Unix())))\n\t}\n\tif !opts.ValidBefore.IsZero() {\n\t\tsignOptions = append(signOptions, sshCertValidBeforeModifier(cast.Uint64(opts.ValidBefore.RelativeTime(t).Unix())))\n\t}\n\n\treturn append(signOptions,\n\t\tp,\n\t\t// Checks the validity bounds, and set the validity if has not been set.\n\t\t&sshLimitDuration{p.ctl.Claimer, x5cLeaf.NotAfter},\n\t\t// Validate public key.\n\t\t&sshDefaultPublicKeyValidator{},\n\t\t// Validate the validity period.\n\t\t&sshCertValidityValidator{p.ctl.Claimer},\n\t\t// Require all the fields in the SSH certificate\n\t\t&sshCertDefaultValidator{},\n\t\t// Ensure that all principal names are allowed\n\t\tnewSSHNamePolicyValidator(p.ctl.getPolicy().getSSHHost(), p.ctl.getPolicy().getSSHUser()),\n\t\t// Call webhooks\n\t\tp.ctl.newWebhookController(\n\t\t\tdata,\n\t\t\tlinkedca.Webhook_SSH,\n\t\t\twebhook.WithX5CCertificate(x5cLeaf),\n\t\t\twebhook.WithAuthorizationPrincipal(x5cLeaf.Subject.CommonName),\n\t\t),\n\t), nil\n}\n"
  },
  {
    "path": "authority/provisioner/x5c_test.go",
    "content": "package provisioner\n\nimport (\n\t\"context\"\n\t\"crypto/x509\"\n\t\"encoding/base64\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/smallstep/linkedca\"\n\t\"go.step.sm/crypto/jose\"\n\t\"go.step.sm/crypto/pemutil\"\n\t\"go.step.sm/crypto/randutil\"\n\n\t\"github.com/smallstep/certificates/api/render\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc assertHasPrefix(t *testing.T, s, p string) bool {\n\tt.Helper()\n\treturn assert.True(t, strings.HasPrefix(s, p), \"%q is not a prefix of %q\", p, s)\n}\n\nfunc TestX5C_Getters(t *testing.T) {\n\tp, err := generateX5C(nil)\n\trequire.NoError(t, err)\n\tid := \"x5c/\" + p.Name\n\tif got := p.GetID(); got != id {\n\t\tt.Errorf(\"X5C.GetID() = %v, want %v:%v\", got, p.Name, id)\n\t}\n\tif got := p.GetName(); got != p.Name {\n\t\tt.Errorf(\"X5C.GetName() = %v, want %v\", got, p.Name)\n\t}\n\tif got := p.GetType(); got != TypeX5C {\n\t\tt.Errorf(\"X5C.GetType() = %v, want %v\", got, TypeX5C)\n\t}\n\tkid, key, ok := p.GetEncryptedKey()\n\tif kid != \"\" || key != \"\" || ok == true {\n\t\tt.Errorf(\"X5C.GetEncryptedKey() = (%v, %v, %v), want (%v, %v, %v)\",\n\t\t\tkid, key, ok, \"\", \"\", false)\n\t}\n}\n\nfunc TestX5C_Init(t *testing.T) {\n\ttype ProvisionerValidateTest struct {\n\t\tp          *X5C\n\t\terr        error\n\t\textraValid func(*X5C) error\n\t}\n\ttests := map[string]func(*testing.T) ProvisionerValidateTest{\n\t\t\"fail/empty\": func(t *testing.T) ProvisionerValidateTest {\n\t\t\treturn ProvisionerValidateTest{\n\t\t\t\tp:   &X5C{},\n\t\t\t\terr: errors.New(\"provisioner type cannot be empty\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/empty-name\": func(t *testing.T) ProvisionerValidateTest {\n\t\t\treturn ProvisionerValidateTest{\n\t\t\t\tp: &X5C{\n\t\t\t\t\tType: \"X5C\",\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"provisioner name cannot be empty\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/empty-type\": func(t *testing.T) ProvisionerValidateTest {\n\t\t\treturn ProvisionerValidateTest{\n\t\t\t\tp:   &X5C{Name: \"foo\"},\n\t\t\t\terr: errors.New(\"provisioner type cannot be empty\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/empty-key\": func(t *testing.T) ProvisionerValidateTest {\n\t\t\treturn ProvisionerValidateTest{\n\t\t\t\tp:   &X5C{Name: \"foo\", Type: \"bar\"},\n\t\t\t\terr: errors.New(\"provisioner root(s) cannot be empty\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/no-valid-root-certs\": func(t *testing.T) ProvisionerValidateTest {\n\t\t\treturn ProvisionerValidateTest{\n\t\t\t\tp:   &X5C{Name: \"foo\", Type: \"bar\", Roots: []byte(\"foo\")},\n\t\t\t\terr: errors.New(\"no x509 certificates found in roots attribute for provisioner 'foo'\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/invalid-duration\": func(t *testing.T) ProvisionerValidateTest {\n\t\t\tp, err := generateX5C(nil)\n\t\t\trequire.NoError(t, err)\n\t\t\tp.Claims = &Claims{DefaultTLSDur: &Duration{0}}\n\t\t\treturn ProvisionerValidateTest{\n\t\t\t\tp:   p,\n\t\t\t\terr: errors.New(\"claims: MinTLSCertDuration must be greater than 0\"),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) ProvisionerValidateTest {\n\t\t\tp, err := generateX5C(nil)\n\t\t\trequire.NoError(t, err)\n\t\t\treturn ProvisionerValidateTest{\n\t\t\t\tp: p,\n\t\t\t}\n\t\t},\n\t\t\"ok/root-chain\": func(t *testing.T) ProvisionerValidateTest {\n\t\t\tp, err := generateX5C([]byte(`-----BEGIN CERTIFICATE-----\nMIIBtjCCAVygAwIBAgIQNr+f4IkABY2n4wx4sLOMrTAKBggqhkjOPQQDAjAUMRIw\nEAYDVQQDEwlyb290LXRlc3QwIBcNMTkxMDAyMDI0MDM0WhgPMjExOTA5MDgwMjQw\nMzJaMBwxGjAYBgNVBAMTEWludGVybWVkaWF0ZS10ZXN0MFkwEwYHKoZIzj0CAQYI\nKoZIzj0DAQcDQgAEflfRhPjgJXv4zsPWahXjM2UU61aRFErN0iw88ZPyxea22fxl\nqN9ezntTXxzsS+mZiWapl8B40ACJgvP+WLQBHKOBhTCBgjAOBgNVHQ8BAf8EBAMC\nAQYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUnJAxiZcy2ibHcuvfFx99\noDwzKXMwHwYDVR0jBBgwFoAUpHS7FfaQ5bCrTxUeu6R2ZC3VGOowHAYDVR0RBBUw\nE4IRaW50ZXJtZWRpYXRlLXRlc3QwCgYIKoZIzj0EAwIDSAAwRQIgII8XpQ8ezDO1\n2xdq3hShf155C5X/5jO8qr0VyEJgzlkCIQCTqph1Gwu/dmuf6dYLCfQqJyb371LC\nlgsqsR63is+0YQ==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIBhTCCASqgAwIBAgIRAMalM7pKi0GCdKjO6u88OyowCgYIKoZIzj0EAwIwFDES\nMBAGA1UEAxMJcm9vdC10ZXN0MCAXDTE5MTAwMjAyMzk0OFoYDzIxMTkwOTA4MDIz\nOTQ4WjAUMRIwEAYDVQQDEwlyb290LXRlc3QwWTATBgcqhkjOPQIBBggqhkjOPQMB\nBwNCAAS29QTCXUu7cx9sa9wZPpRSFq/zXaw8Ai3EIygayrBsKnX42U2atBUjcBZO\nBWL6A+PpLzU9ja867U5SYNHERS+Oo1swWTAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0T\nAQH/BAgwBgEB/wIBATAdBgNVHQ4EFgQUpHS7FfaQ5bCrTxUeu6R2ZC3VGOowFAYD\nVR0RBA0wC4IJcm9vdC10ZXN0MAoGCCqGSM49BAMCA0kAMEYCIQC2vgqwla0u8LHH\n1MHob14qvS5o76HautbIBW7fcHzz5gIhAIx5A2+wkJYX4026kqaZCk/1sAwTxSGY\nM46l92gdOozT\n-----END CERTIFICATE-----`))\n\t\t\trequire.NoError(t, err)\n\t\t\treturn ProvisionerValidateTest{\n\t\t\t\tp: p,\n\t\t\t\textraValid: func(p *X5C) error {\n\t\t\t\t\t//nolint:staticcheck // We don't have a different way to\n\t\t\t\t\t// check the number of certificates in the pool.\n\t\t\t\t\tnumCerts := len(p.rootPool.Subjects())\n\t\t\t\t\tif numCerts != 2 {\n\t\t\t\t\t\treturn fmt.Errorf(\"unexpected number of certs: want 2, but got %d\", numCerts)\n\t\t\t\t\t}\n\t\t\t\t\treturn nil\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t}\n\n\tconfig := Config{\n\t\tClaims:    globalProvisionerClaims,\n\t\tAudiences: testAudiences,\n\t}\n\tfor name, get := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ttc := get(t)\n\t\t\terr := tc.p.Init(config)\n\t\t\tif err != nil {\n\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\tassert.EqualError(t, tc.err, err.Error())\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif assert.Nil(t, tc.err) {\n\t\t\t\t\tassert.Equal(t, *tc.p.ctl.Audiences, config.Audiences.WithFragment(tc.p.GetID()))\n\t\t\t\t\tif tc.extraValid != nil {\n\t\t\t\t\t\tassert.Nil(t, tc.extraValid(tc.p))\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestX5C_authorizeToken(t *testing.T) {\n\tx5cCerts, err := pemutil.ReadCertificateBundle(\"./testdata/certs/x5c-leaf.crt\")\n\trequire.NoError(t, err)\n\tx5cJWK, err := jose.ReadKey(\"./testdata/secrets/x5c-leaf.key\")\n\trequire.NoError(t, err)\n\n\ttype test struct {\n\t\tp     *X5C\n\t\ttoken string\n\t\tcode  int\n\t\terr   error\n\t}\n\ttests := map[string]func(*testing.T) test{\n\t\t\"fail/bad-token\": func(t *testing.T) test {\n\t\t\tp, err := generateX5C(nil)\n\t\t\trequire.NoError(t, err)\n\t\t\treturn test{\n\t\t\t\tp:     p,\n\t\t\t\ttoken: \"foo\",\n\t\t\t\tcode:  http.StatusUnauthorized,\n\t\t\t\terr:   errors.New(\"x5c.authorizeToken; error parsing x5c token\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/invalid-cert-chain\": func(t *testing.T) test {\n\t\t\tcerts, err := parseCerts([]byte(`-----BEGIN CERTIFICATE-----\nMIIBpTCCAUugAwIBAgIRAOn2LHXjYyTXQ7PNjDTSKiIwCgYIKoZIzj0EAwIwHDEa\nMBgGA1UEAxMRU21hbGxzdGVwIFJvb3QgQ0EwHhcNMTkwOTE0MDk1NTM2WhcNMjkw\nOTExMDk1NTM2WjAkMSIwIAYDVQQDExlTbWFsbHN0ZXAgSW50ZXJtZWRpYXRlIENB\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE2Cs0TY0dLM4b2s+z8+cc3JJp/W5H\nzQRvICX/1aJ4MuObNLcvoSguJwJEkYpGB5fhb0KvoL+ebHfEOywGNwrWkaNmMGQw\nDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFNLJ\n4ZXoX9cI6YkGPxgs2US3ssVzMB8GA1UdIwQYMBaAFGIwpqz85wL29aF47Vj9XSVM\nP9K7MAoGCCqGSM49BAMCA0gAMEUCIQC5c1ldDcesDb31GlO5cEJvOcRrIrNtkk8m\na5wpg+9s6QIgHIW6L60F8klQX+EO3o0SBqLeNcaskA4oSZsKjEdpSGo=\n-----END CERTIFICATE-----`))\n\t\t\trequire.NoError(t, err)\n\t\t\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\trequire.NoError(t, err)\n\t\t\tp, err := generateX5C(nil)\n\t\t\trequire.NoError(t, err)\n\t\t\ttok, err := generateToken(\"\", p.Name, testAudiences.Sign[0], \"\",\n\t\t\t\t[]string{\"test.smallstep.com\"}, time.Now(), jwk,\n\t\t\t\twithX5CHdr(certs))\n\t\t\trequire.NoError(t, err)\n\t\t\treturn test{\n\t\t\t\tp:     p,\n\t\t\t\ttoken: tok,\n\t\t\t\tcode:  http.StatusUnauthorized,\n\t\t\t\terr:   errors.New(\"x5c.authorizeToken; error verifying x5c certificate chain in token\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/doubled-up-self-signed-cert\": func(t *testing.T) test {\n\t\t\tcerts, err := parseCerts([]byte(`-----BEGIN CERTIFICATE-----\nMIIBgjCCASigAwIBAgIQIZiE9wpmSj6SMMDfHD17qjAKBggqhkjOPQQDAjAQMQ4w\nDAYDVQQDEwVsZWFmMjAgFw0xOTEwMDIwMzEzNTlaGA8yMTE5MDkwODAzMTM1OVow\nEDEOMAwGA1UEAxMFbGVhZjIwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATuajJI\n3YgDaj+jorioJzGJc2+V1hUM7XzN9tIHoUeItgny9GW08TrTc23h1cCZteNZvayG\nM0wGpGeXOnE4IlH9o2IwYDAOBgNVHQ8BAf8EBAMCBSAwHQYDVR0lBBYwFAYIKwYB\nBQUHAwEGCCsGAQUFBwMCMB0GA1UdDgQWBBT99+JChTh3LWOHaqlSwNiwND18/zAQ\nBgNVHREECTAHggVsZWFmMjAKBggqhkjOPQQDAgNIADBFAiB7gMRy3t81HpcnoRAS\nELZmDFaEnoLCsVfbmanFykazQQIhAI0sZjoE9t6gvzQp7XQp6CoxzCc3Jv3FwZ8G\nEXAHTA9L\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIBgjCCASigAwIBAgIQIZiE9wpmSj6SMMDfHD17qjAKBggqhkjOPQQDAjAQMQ4w\nDAYDVQQDEwVsZWFmMjAgFw0xOTEwMDIwMzEzNTlaGA8yMTE5MDkwODAzMTM1OVow\nEDEOMAwGA1UEAxMFbGVhZjIwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATuajJI\n3YgDaj+jorioJzGJc2+V1hUM7XzN9tIHoUeItgny9GW08TrTc23h1cCZteNZvayG\nM0wGpGeXOnE4IlH9o2IwYDAOBgNVHQ8BAf8EBAMCBSAwHQYDVR0lBBYwFAYIKwYB\nBQUHAwEGCCsGAQUFBwMCMB0GA1UdDgQWBBT99+JChTh3LWOHaqlSwNiwND18/zAQ\nBgNVHREECTAHggVsZWFmMjAKBggqhkjOPQQDAgNIADBFAiB7gMRy3t81HpcnoRAS\nELZmDFaEnoLCsVfbmanFykazQQIhAI0sZjoE9t6gvzQp7XQp6CoxzCc3Jv3FwZ8G\nEXAHTA9L\n-----END CERTIFICATE-----`))\n\t\t\trequire.NoError(t, err)\n\t\t\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\trequire.NoError(t, err)\n\t\t\tp, err := generateX5C(nil)\n\t\t\trequire.NoError(t, err)\n\t\t\ttok, err := generateToken(\"\", p.Name, testAudiences.Sign[0], \"\",\n\t\t\t\t[]string{\"test.smallstep.com\"}, time.Now(), jwk,\n\t\t\t\twithX5CHdr(certs))\n\t\t\trequire.NoError(t, err)\n\t\t\treturn test{\n\t\t\t\tp:     p,\n\t\t\t\ttoken: tok,\n\t\t\t\tcode:  http.StatusUnauthorized,\n\t\t\t\terr:   errors.New(\"x5c.authorizeToken; error verifying x5c certificate chain in token\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/digital-signature-ext-required\": func(t *testing.T) test {\n\t\t\tcerts, err := parseCerts([]byte(`-----BEGIN CERTIFICATE-----\nMIIBuTCCAV+gAwIBAgIQeRJLdDMIdn/T2ORKxYABezAKBggqhkjOPQQDAjAcMRow\nGAYDVQQDExFpbnRlcm1lZGlhdGUtdGVzdDAgFw0xOTEwMDIwMjQxMTRaGA8yMTE5\nMDkwODAyNDExMlowFDESMBAGA1UEAxMJbGVhZi10ZXN0MFkwEwYHKoZIzj0CAQYI\nKoZIzj0DAQcDQgAEDA1nGTOujobkcBWklyvymhWE5gQlvNLarVzhhhvPDw+MK2LX\nyqkXrYZM10GrwQZuQ7ykHnjz00U/KXpPRQ7+0qOBiDCBhTAOBgNVHQ8BAf8EBAMC\nBSAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMB0GA1UdDgQWBBQYv0AK\n3GUOvC+m8ZTfyhn7tKQOazAfBgNVHSMEGDAWgBSckDGJlzLaJsdy698XH32gPDMp\nczAUBgNVHREEDTALgglsZWFmLXRlc3QwCgYIKoZIzj0EAwIDSAAwRQIhAPmertx0\nlchRU3kAu647exvlhEr1xosPOu6P8kVYbtTEAiAA51w9EYIT/Zb26M3eQV817T2g\nDnhl0ElPQsA92pkqbA==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIBtjCCAVygAwIBAgIQNr+f4IkABY2n4wx4sLOMrTAKBggqhkjOPQQDAjAUMRIw\nEAYDVQQDEwlyb290LXRlc3QwIBcNMTkxMDAyMDI0MDM0WhgPMjExOTA5MDgwMjQw\nMzJaMBwxGjAYBgNVBAMTEWludGVybWVkaWF0ZS10ZXN0MFkwEwYHKoZIzj0CAQYI\nKoZIzj0DAQcDQgAEflfRhPjgJXv4zsPWahXjM2UU61aRFErN0iw88ZPyxea22fxl\nqN9ezntTXxzsS+mZiWapl8B40ACJgvP+WLQBHKOBhTCBgjAOBgNVHQ8BAf8EBAMC\nAQYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUnJAxiZcy2ibHcuvfFx99\noDwzKXMwHwYDVR0jBBgwFoAUpHS7FfaQ5bCrTxUeu6R2ZC3VGOowHAYDVR0RBBUw\nE4IRaW50ZXJtZWRpYXRlLXRlc3QwCgYIKoZIzj0EAwIDSAAwRQIgII8XpQ8ezDO1\n2xdq3hShf155C5X/5jO8qr0VyEJgzlkCIQCTqph1Gwu/dmuf6dYLCfQqJyb371LC\nlgsqsR63is+0YQ==\n-----END CERTIFICATE-----`))\n\t\t\trequire.NoError(t, err)\n\t\t\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\trequire.NoError(t, err)\n\t\t\tp, err := generateX5C(nil)\n\t\t\trequire.NoError(t, err)\n\n\t\t\ttok, err := generateToken(\"\", p.Name, testAudiences.Sign[0], \"\",\n\t\t\t\t[]string{\"test.smallstep.com\"}, time.Now(), jwk,\n\t\t\t\twithX5CHdr(certs))\n\t\t\trequire.NoError(t, err)\n\t\t\treturn test{\n\t\t\t\tp:     p,\n\t\t\t\ttoken: tok,\n\t\t\t\tcode:  http.StatusUnauthorized,\n\t\t\t\terr:   errors.New(\"x5c.authorizeToken; certificate used to sign x5c token cannot be used for digital signature\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/signature-does-not-match-x5c-pub-key\": func(t *testing.T) test {\n\t\t\tcerts, err := parseCerts([]byte(`-----BEGIN CERTIFICATE-----\nMIIBuDCCAV+gAwIBAgIQFdu723gqgGaTaqjf6ny88zAKBggqhkjOPQQDAjAcMRow\nGAYDVQQDExFpbnRlcm1lZGlhdGUtdGVzdDAgFw0xOTEwMDIwMzE4NTNaGA8yMTE5\nMDkwODAzMTg1MVowFDESMBAGA1UEAxMJbGVhZi10ZXN0MFkwEwYHKoZIzj0CAQYI\nKoZIzj0DAQcDQgAEaV6807GhWEtMxA39zjuMVHAiN2/Ri5B1R1s+Y/8mlrKIvuvr\nVpgSPXYruNRFduPWX564Abz/TDmb276JbKGeQqOBiDCBhTAOBgNVHQ8BAf8EBAMC\nBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMB0GA1UdDgQWBBReMkPW\nf4MNWdg7KN4xI4ZLJd0IJDAfBgNVHSMEGDAWgBSckDGJlzLaJsdy698XH32gPDMp\nczAUBgNVHREEDTALgglsZWFmLXRlc3QwCgYIKoZIzj0EAwIDRwAwRAIgKYLKXpTN\nwtvZZaIvDzq1p8MO/SZ8yI42Ot69dNk/QtkCIBSvg5PozYcfbvwkgX5SwsjfYu0Z\nAvUgkUQ2G25NBRmX\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIBtjCCAVygAwIBAgIQNr+f4IkABY2n4wx4sLOMrTAKBggqhkjOPQQDAjAUMRIw\nEAYDVQQDEwlyb290LXRlc3QwIBcNMTkxMDAyMDI0MDM0WhgPMjExOTA5MDgwMjQw\nMzJaMBwxGjAYBgNVBAMTEWludGVybWVkaWF0ZS10ZXN0MFkwEwYHKoZIzj0CAQYI\nKoZIzj0DAQcDQgAEflfRhPjgJXv4zsPWahXjM2UU61aRFErN0iw88ZPyxea22fxl\nqN9ezntTXxzsS+mZiWapl8B40ACJgvP+WLQBHKOBhTCBgjAOBgNVHQ8BAf8EBAMC\nAQYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUnJAxiZcy2ibHcuvfFx99\noDwzKXMwHwYDVR0jBBgwFoAUpHS7FfaQ5bCrTxUeu6R2ZC3VGOowHAYDVR0RBBUw\nE4IRaW50ZXJtZWRpYXRlLXRlc3QwCgYIKoZIzj0EAwIDSAAwRQIgII8XpQ8ezDO1\n2xdq3hShf155C5X/5jO8qr0VyEJgzlkCIQCTqph1Gwu/dmuf6dYLCfQqJyb371LC\nlgsqsR63is+0YQ==\n-----END CERTIFICATE-----`))\n\t\t\trequire.NoError(t, err)\n\t\t\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\t\t\trequire.NoError(t, err)\n\t\t\tp, err := generateX5C(nil)\n\t\t\trequire.NoError(t, err)\n\t\t\ttok, err := generateToken(\"\", \"foobar\", testAudiences.Sign[0], \"\",\n\t\t\t\t[]string{\"test.smallstep.com\"}, time.Now(), jwk,\n\t\t\t\twithX5CHdr(certs))\n\t\t\trequire.NoError(t, err)\n\t\t\treturn test{\n\t\t\t\tp:     p,\n\t\t\t\ttoken: tok,\n\t\t\t\tcode:  http.StatusUnauthorized,\n\t\t\t\terr:   errors.New(\"x5c.authorizeToken; error parsing x5c claims\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/invalid-issuer\": func(t *testing.T) test {\n\t\t\tp, err := generateX5C(nil)\n\t\t\trequire.NoError(t, err)\n\t\t\ttok, err := generateToken(\"\", \"foobar\", testAudiences.Sign[0], \"\",\n\t\t\t\t[]string{\"test.smallstep.com\"}, time.Now(), x5cJWK,\n\t\t\t\twithX5CHdr(x5cCerts))\n\t\t\trequire.NoError(t, err)\n\t\t\treturn test{\n\t\t\t\tp:     p,\n\t\t\t\ttoken: tok,\n\t\t\t\tcode:  http.StatusUnauthorized,\n\t\t\t\terr:   errors.New(\"x5c.authorizeToken; invalid x5c claims\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/invalid-audience\": func(t *testing.T) test {\n\t\t\tp, err := generateX5C(nil)\n\t\t\trequire.NoError(t, err)\n\t\t\ttok, err := generateToken(\"\", p.GetName(), \"foobar\", \"\",\n\t\t\t\t[]string{\"test.smallstep.com\"}, time.Now(), x5cJWK,\n\t\t\t\twithX5CHdr(x5cCerts))\n\t\t\trequire.NoError(t, err)\n\t\t\treturn test{\n\t\t\t\tp:     p,\n\t\t\t\ttoken: tok,\n\t\t\t\tcode:  http.StatusUnauthorized,\n\t\t\t\terr:   errors.New(\"x5c.authorizeToken; x5c token has invalid audience claim (aud)\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/empty-subject\": func(t *testing.T) test {\n\t\t\tp, err := generateX5C(nil)\n\t\t\trequire.NoError(t, err)\n\t\t\ttok, err := generateToken(\"\", p.GetName(), testAudiences.Sign[0], \"\",\n\t\t\t\t[]string{\"test.smallstep.com\"}, time.Now(), x5cJWK,\n\t\t\t\twithX5CHdr(x5cCerts))\n\t\t\trequire.NoError(t, err)\n\t\t\treturn test{\n\t\t\t\tp:     p,\n\t\t\t\ttoken: tok,\n\t\t\t\tcode:  http.StatusUnauthorized,\n\t\t\t\terr:   errors.New(\"x5c.authorizeToken; x5c token subject cannot be empty\"),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tp, err := generateX5C(nil)\n\t\t\trequire.NoError(t, err)\n\t\t\ttok, err := generateToken(\"foo\", p.GetName(), testAudiences.Sign[0], \"\",\n\t\t\t\t[]string{\"test.smallstep.com\"}, time.Now(), x5cJWK,\n\t\t\t\twithX5CHdr(x5cCerts))\n\t\t\trequire.NoError(t, err)\n\t\t\treturn test{\n\t\t\t\tp:     p,\n\t\t\t\ttoken: tok,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, tt := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ttc := tt(t)\n\t\t\tif claims, err := tc.p.authorizeToken(tc.token, testAudiences.Sign); err != nil {\n\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\tvar sc render.StatusCodedError\n\t\t\t\t\tif assert.True(t, errors.As(err, &sc), \"error does not implement StatusCodedError interface\") {\n\t\t\t\t\t\tassert.Equal(t, tc.code, sc.StatusCode())\n\t\t\t\t\t}\n\t\t\t\t\tassertHasPrefix(t, err.Error(), tc.err.Error())\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif assert.NoError(t, tc.err) {\n\t\t\t\t\tassert.NotNil(t, claims)\n\t\t\t\t\tassert.NotNil(t, claims.chains)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestX5C_AuthorizeSign(t *testing.T) {\n\tcerts, err := pemutil.ReadCertificateBundle(\"./testdata/certs/x5c-leaf.crt\")\n\trequire.NoError(t, err)\n\tjwk, err := jose.ReadKey(\"./testdata/secrets/x5c-leaf.key\")\n\trequire.NoError(t, err)\n\n\ttype test struct {\n\t\tp           *X5C\n\t\ttoken       string\n\t\tcode        int\n\t\terr         error\n\t\tsans        []string\n\t\tfingerprint string\n\t}\n\ttests := map[string]func(*testing.T) test{\n\t\t\"fail/invalid-token\": func(t *testing.T) test {\n\t\t\tp, err := generateX5C(nil)\n\t\t\trequire.NoError(t, err)\n\t\t\treturn test{\n\t\t\t\tp:     p,\n\t\t\t\ttoken: \"foo\",\n\t\t\t\tcode:  http.StatusUnauthorized,\n\t\t\t\terr:   errors.New(\"x5c.AuthorizeSign: x5c.authorizeToken; error parsing x5c token\"),\n\t\t\t}\n\t\t},\n\t\t\"ok/empty-sans\": func(t *testing.T) test {\n\t\t\tp, err := generateX5C(nil)\n\t\t\trequire.NoError(t, err)\n\t\t\ttok, err := generateToken(\"foo\", p.GetName(), testAudiences.Sign[0], \"\",\n\t\t\t\t[]string{}, time.Now(), jwk,\n\t\t\t\twithX5CHdr(certs))\n\t\t\trequire.NoError(t, err)\n\t\t\treturn test{\n\t\t\t\tp:     p,\n\t\t\t\ttoken: tok,\n\t\t\t\tsans:  []string{\"foo\"},\n\t\t\t}\n\t\t},\n\t\t\"ok/multi-sans\": func(t *testing.T) test {\n\t\t\tp, err := generateX5C(nil)\n\t\t\trequire.NoError(t, err)\n\t\t\ttok, err := generateToken(\"foo\", p.GetName(), testAudiences.Sign[0], \"\",\n\t\t\t\t[]string{\"127.0.0.1\", \"foo\", \"max@smallstep.com\"}, time.Now(), jwk,\n\t\t\t\twithX5CHdr(certs))\n\t\t\trequire.NoError(t, err)\n\t\t\treturn test{\n\t\t\t\tp:     p,\n\t\t\t\ttoken: tok,\n\t\t\t\tsans:  []string{\"127.0.0.1\", \"foo\", \"max@smallstep.com\"},\n\t\t\t}\n\t\t},\n\t\t\"ok/cnf\": func(t *testing.T) test {\n\t\t\tp, err := generateX5C(nil)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tx5c := make([]string, len(certs))\n\t\t\tfor i, cert := range certs {\n\t\t\t\tx5c[i] = base64.StdEncoding.EncodeToString(cert.Raw)\n\t\t\t}\n\t\t\textraHeaders := map[string]any{\"x5c\": x5c}\n\t\t\textraClaims := map[string]any{\n\t\t\t\t\"sans\": []string{\"127.0.0.1\", \"foo\", \"max@smallstep.com\"},\n\t\t\t\t\"cnf\":  map[string]any{\"x5rt#S256\": \"fingerprint\"},\n\t\t\t}\n\n\t\t\ttok, err := generateCustomToken(\"foo\", p.GetName(), testAudiences.Sign[0], jwk, extraHeaders, extraClaims)\n\t\t\trequire.NoError(t, err)\n\t\t\treturn test{\n\t\t\t\tp:           p,\n\t\t\t\ttoken:       tok,\n\t\t\t\tsans:        []string{\"127.0.0.1\", \"foo\", \"max@smallstep.com\"},\n\t\t\t\tfingerprint: \"fingerprint\",\n\t\t\t}\n\t\t},\n\t}\n\tfor name, tt := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ttc := tt(t)\n\t\t\tctx := NewContextWithMethod(context.Background(), SignIdentityMethod)\n\t\t\tif opts, err := tc.p.AuthorizeSign(ctx, tc.token); err != nil {\n\t\t\t\tif assert.NotNil(t, tc.err, err.Error()) {\n\t\t\t\t\tvar sc render.StatusCodedError\n\t\t\t\t\tif assert.True(t, errors.As(err, &sc), \"error does not implement StatusCodedError interface\") {\n\t\t\t\t\t\tassert.Equal(t, tc.code, sc.StatusCode())\n\t\t\t\t\t}\n\t\t\t\t\tassertHasPrefix(t, err.Error(), tc.err.Error())\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif assert.Nil(t, tc.err) {\n\t\t\t\t\tif assert.NotNil(t, opts) {\n\t\t\t\t\t\tassert.Len(t, opts, 11)\n\t\t\t\t\t\tfor _, o := range opts {\n\t\t\t\t\t\t\tswitch v := o.(type) {\n\t\t\t\t\t\t\tcase *X5C:\n\t\t\t\t\t\t\tcase certificateOptionsFunc:\n\t\t\t\t\t\t\tcase *provisionerExtensionOption:\n\t\t\t\t\t\t\t\tassert.Equal(t, TypeX5C, v.Type)\n\t\t\t\t\t\t\t\tassert.Equal(t, tc.p.GetName(), v.Name)\n\t\t\t\t\t\t\t\tassert.Equal(t, \"\", v.CredentialID)\n\t\t\t\t\t\t\t\tassert.Len(t, v.KeyValuePairs, 0)\n\t\t\t\t\t\t\tcase profileLimitDuration:\n\t\t\t\t\t\t\t\tassert.Equal(t, tc.p.ctl.Claimer.DefaultTLSCertDuration(), v.def)\n\t\t\t\t\t\t\t\tclaims, err := tc.p.authorizeToken(tc.token, tc.p.ctl.Audiences.Sign)\n\t\t\t\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t\t\t\t\tassert.Equal(t, claims.chains[0][0].NotAfter, v.notAfter)\n\t\t\t\t\t\t\tcase commonNameValidator:\n\t\t\t\t\t\t\t\tassert.Equal(t, \"foo\", string(v))\n\t\t\t\t\t\t\tcase defaultPublicKeyValidator:\n\t\t\t\t\t\t\tcase *defaultSANsValidator:\n\t\t\t\t\t\t\t\tassert.Equal(t, tc.sans, v.sans)\n\t\t\t\t\t\t\t\tassert.Equal(t, SignIdentityMethod, MethodFromContext(v.ctx))\n\t\t\t\t\t\t\tcase *validityValidator:\n\t\t\t\t\t\t\t\tassert.Equal(t, tc.p.ctl.Claimer.MinTLSCertDuration(), v.min)\n\t\t\t\t\t\t\t\tassert.Equal(t, tc.p.ctl.Claimer.MaxTLSCertDuration(), v.max)\n\t\t\t\t\t\t\tcase *x509NamePolicyValidator:\n\t\t\t\t\t\t\t\tassert.Equal(t, nil, v.policyEngine)\n\t\t\t\t\t\t\tcase *WebhookController:\n\t\t\t\t\t\t\t\tassert.Len(t, v.webhooks, 0)\n\t\t\t\t\t\t\t\tassert.Equal(t, linkedca.Webhook_X509, v.certType)\n\t\t\t\t\t\t\t\tassert.Len(t, v.options, 2)\n\t\t\t\t\t\t\tcase csrFingerprintValidator:\n\t\t\t\t\t\t\t\tassert.Equal(t, tc.fingerprint, string(v))\n\t\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\t\trequire.NoError(t, fmt.Errorf(\"unexpected sign option of type %T\", v))\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestX5C_AuthorizeRevoke(t *testing.T) {\n\ttype test struct {\n\t\tp     *X5C\n\t\ttoken string\n\t\tcode  int\n\t\terr   error\n\t}\n\ttests := map[string]func(*testing.T) test{\n\t\t\"fail/invalid-token\": func(t *testing.T) test {\n\t\t\tp, err := generateX5C(nil)\n\t\t\trequire.NoError(t, err)\n\t\t\treturn test{\n\t\t\t\tp:     p,\n\t\t\t\ttoken: \"foo\",\n\t\t\t\tcode:  http.StatusUnauthorized,\n\t\t\t\terr:   errors.New(\"x5c.AuthorizeRevoke: x5c.authorizeToken; error parsing x5c token\"),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tcerts, err := pemutil.ReadCertificateBundle(\"./testdata/certs/x5c-leaf.crt\")\n\t\t\trequire.NoError(t, err)\n\t\t\tserialNumber := certs[0].SerialNumber.String()\n\t\t\tjwk, err := jose.ReadKey(\"./testdata/secrets/x5c-leaf.key\")\n\t\t\trequire.NoError(t, err)\n\n\t\t\tp, err := generateX5C(nil)\n\t\t\trequire.NoError(t, err)\n\t\t\ttok, err := generateToken(serialNumber, p.GetName(), testAudiences.Revoke[0], \"\",\n\t\t\t\t[]string{\"test.smallstep.com\"}, time.Now(), jwk,\n\t\t\t\twithX5CHdr(certs))\n\t\t\trequire.NoError(t, err)\n\t\t\treturn test{\n\t\t\t\tp:     p,\n\t\t\t\ttoken: tok,\n\t\t\t}\n\t\t},\n\t\t\"ok/different-serial-number\": func(t *testing.T) test {\n\t\t\tcerts, err := pemutil.ReadCertificateBundle(\"./testdata/certs/x5c-leaf.crt\")\n\t\t\trequire.NoError(t, err)\n\t\t\tjwk, err := jose.ReadKey(\"./testdata/secrets/x5c-leaf.key\")\n\t\t\trequire.NoError(t, err)\n\n\t\t\tp, err := generateX5C(nil)\n\t\t\trequire.NoError(t, err)\n\t\t\ttok, err := generateToken(\"123456789\", p.GetName(), testAudiences.Revoke[0], \"\",\n\t\t\t\t[]string{\"test.smallstep.com\"}, time.Now(), jwk,\n\t\t\t\twithX5CHdr(certs))\n\t\t\trequire.NoError(t, err)\n\t\t\treturn test{\n\t\t\t\tp:     p,\n\t\t\t\ttoken: tok,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, tt := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ttc := tt(t)\n\t\t\tif err := tc.p.AuthorizeRevoke(context.Background(), tc.token); err != nil {\n\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\tvar sc render.StatusCodedError\n\t\t\t\t\tif assert.True(t, errors.As(err, &sc), \"error does not implement StatusCodedError interface\") {\n\t\t\t\t\t\tassert.Equal(t, tc.code, sc.StatusCode())\n\t\t\t\t\t}\n\t\t\t\t\tassertHasPrefix(t, err.Error(), tc.err.Error())\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tassert.Nil(t, tc.err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestX5C_AuthorizeRenew(t *testing.T) {\n\tnow := time.Now().Truncate(time.Second)\n\ttype test struct {\n\t\tp    *X5C\n\t\tcode int\n\t\terr  error\n\t}\n\ttests := map[string]func(*testing.T) test{\n\t\t\"fail/renew-disabled\": func(t *testing.T) test {\n\t\t\tp, err := generateX5C(nil)\n\t\t\trequire.NoError(t, err)\n\t\t\t// disable renewal\n\t\t\tdisable := true\n\t\t\tp.Claims = &Claims{DisableRenewal: &disable}\n\t\t\tp.ctl.Claimer, err = NewClaimer(p.Claims, globalProvisionerClaims)\n\t\t\trequire.NoError(t, err)\n\t\t\treturn test{\n\t\t\t\tp:    p,\n\t\t\t\tcode: http.StatusUnauthorized,\n\t\t\t\terr:  fmt.Errorf(\"renew is disabled for provisioner '%s'\", p.GetName()),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\tp, err := generateX5C(nil)\n\t\t\trequire.NoError(t, err)\n\t\t\treturn test{\n\t\t\t\tp: p,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, tt := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ttc := tt(t)\n\t\t\tif err := tc.p.AuthorizeRenew(context.Background(), &x509.Certificate{\n\t\t\t\tNotBefore: now,\n\t\t\t\tNotAfter:  now.Add(time.Hour),\n\t\t\t}); err != nil {\n\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\tvar sc render.StatusCodedError\n\t\t\t\t\tif assert.True(t, errors.As(err, &sc), \"error does not implement StatusCodedError interface\") {\n\t\t\t\t\t\tassert.Equal(t, tc.code, sc.StatusCode())\n\t\t\t\t\t}\n\t\t\t\t\tassertHasPrefix(t, err.Error(), tc.err.Error())\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tassert.Nil(t, tc.err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestX5C_AuthorizeSSHSign(t *testing.T) {\n\tx5cCerts, err := pemutil.ReadCertificateBundle(\"./testdata/certs/x5c-leaf.crt\")\n\trequire.NoError(t, err)\n\tx5cJWK, err := jose.ReadKey(\"./testdata/secrets/x5c-leaf.key\")\n\trequire.NoError(t, err)\n\n\t_, fn := mockNow()\n\tdefer fn()\n\ttype test struct {\n\t\tp           *X5C\n\t\ttoken       string\n\t\tclaims      *x5cPayload\n\t\tfingerprint string\n\t\tcount       int\n\t\tcode        int\n\t\terr         error\n\t}\n\ttests := map[string]func(*testing.T) test{\n\t\t\"fail/sshCA-disabled\": func(t *testing.T) test {\n\t\t\tp, err := generateX5C(nil)\n\t\t\trequire.NoError(t, err)\n\t\t\t// disable sshCA\n\t\t\tenable := false\n\t\t\tp.Claims = &Claims{EnableSSHCA: &enable}\n\t\t\tp.ctl.Claimer, err = NewClaimer(p.Claims, globalProvisionerClaims)\n\t\t\trequire.NoError(t, err)\n\t\t\treturn test{\n\t\t\t\tp:     p,\n\t\t\t\ttoken: \"foo\",\n\t\t\t\tcode:  http.StatusUnauthorized,\n\t\t\t\terr:   fmt.Errorf(\"x5c.AuthorizeSSHSign; sshCA is disabled for x5c provisioner '%s'\", p.GetName()),\n\t\t\t}\n\t\t},\n\t\t\"fail/invalid-token\": func(t *testing.T) test {\n\t\t\tp, err := generateX5C(nil)\n\t\t\trequire.NoError(t, err)\n\t\t\treturn test{\n\t\t\t\tp:     p,\n\t\t\t\ttoken: \"foo\",\n\t\t\t\tcode:  http.StatusUnauthorized,\n\t\t\t\terr:   errors.New(\"x5c.AuthorizeSSHSign: x5c.authorizeToken; error parsing x5c token\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/no-Step-claim\": func(t *testing.T) test {\n\t\t\tp, err := generateX5C(nil)\n\t\t\trequire.NoError(t, err)\n\t\t\ttok, err := generateToken(\"foo\", p.GetName(), testAudiences.SSHSign[0], \"\",\n\t\t\t\t[]string{\"test.smallstep.com\"}, time.Now(), x5cJWK,\n\t\t\t\twithX5CHdr(x5cCerts))\n\t\t\trequire.NoError(t, err)\n\t\t\treturn test{\n\t\t\t\tp:     p,\n\t\t\t\ttoken: tok,\n\t\t\t\tcode:  http.StatusUnauthorized,\n\t\t\t\terr:   errors.New(\"x5c.AuthorizeSSHSign; x5c token must be an SSH provisioning token\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/no-SSH-subattribute-in-claims\": func(t *testing.T) test {\n\t\t\tp, err := generateX5C(nil)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tid, err := randutil.ASCII(64)\n\t\t\trequire.NoError(t, err)\n\t\t\tnow := time.Now()\n\t\t\tclaims := &x5cPayload{\n\t\t\t\tClaims: jose.Claims{\n\t\t\t\t\tID:        id,\n\t\t\t\t\tSubject:   \"foo\",\n\t\t\t\t\tIssuer:    p.GetName(),\n\t\t\t\t\tIssuedAt:  jose.NewNumericDate(now),\n\t\t\t\t\tNotBefore: jose.NewNumericDate(now),\n\t\t\t\t\tExpiry:    jose.NewNumericDate(now.Add(5 * time.Minute)),\n\t\t\t\t\tAudience:  []string{testAudiences.SSHSign[0]},\n\t\t\t\t},\n\t\t\t\tStep: &stepPayload{},\n\t\t\t}\n\t\t\ttok, err := generateX5CSSHToken(x5cJWK, claims, withX5CHdr(x5cCerts))\n\t\t\trequire.NoError(t, err)\n\t\t\treturn test{\n\t\t\t\tp:     p,\n\t\t\t\ttoken: tok,\n\t\t\t\tcode:  http.StatusUnauthorized,\n\t\t\t\terr:   errors.New(\"x5c.AuthorizeSSHSign; x5c token must be an SSH provisioning token\"),\n\t\t\t}\n\t\t},\n\t\t\"ok/with-claims\": func(t *testing.T) test {\n\t\t\tp, err := generateX5C(nil)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tid, err := randutil.ASCII(64)\n\t\t\trequire.NoError(t, err)\n\t\t\tnow := time.Now()\n\t\t\tclaims := &x5cPayload{\n\t\t\t\tClaims: jose.Claims{\n\t\t\t\t\tID:        id,\n\t\t\t\t\tSubject:   \"foo\",\n\t\t\t\t\tIssuer:    p.GetName(),\n\t\t\t\t\tIssuedAt:  jose.NewNumericDate(now),\n\t\t\t\t\tNotBefore: jose.NewNumericDate(now),\n\t\t\t\t\tExpiry:    jose.NewNumericDate(now.Add(5 * time.Minute)),\n\t\t\t\t\tAudience:  []string{testAudiences.SSHSign[0]},\n\t\t\t\t},\n\t\t\t\tStep: &stepPayload{SSH: &SignSSHOptions{\n\t\t\t\t\tCertType:    SSHUserCert,\n\t\t\t\t\tKeyID:       \"foo\",\n\t\t\t\t\tPrincipals:  []string{\"max\", \"mariano\", \"alan\"},\n\t\t\t\t\tValidAfter:  TimeDuration{d: 5 * time.Minute},\n\t\t\t\t\tValidBefore: TimeDuration{d: 10 * time.Minute},\n\t\t\t\t}},\n\t\t\t}\n\t\t\ttok, err := generateX5CSSHToken(x5cJWK, claims, withX5CHdr(x5cCerts))\n\t\t\trequire.NoError(t, err)\n\t\t\treturn test{\n\t\t\t\tp:      p,\n\t\t\t\tclaims: claims,\n\t\t\t\ttoken:  tok,\n\t\t\t\tcount:  12,\n\t\t\t}\n\t\t},\n\t\t\"ok/without-claims\": func(t *testing.T) test {\n\t\t\tp, err := generateX5C(nil)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tid, err := randutil.ASCII(64)\n\t\t\trequire.NoError(t, err)\n\t\t\tnow := time.Now()\n\t\t\tclaims := &x5cPayload{\n\t\t\t\tClaims: jose.Claims{\n\t\t\t\t\tID:        id,\n\t\t\t\t\tSubject:   \"foo\",\n\t\t\t\t\tIssuer:    p.GetName(),\n\t\t\t\t\tIssuedAt:  jose.NewNumericDate(now),\n\t\t\t\t\tNotBefore: jose.NewNumericDate(now),\n\t\t\t\t\tExpiry:    jose.NewNumericDate(now.Add(5 * time.Minute)),\n\t\t\t\t\tAudience:  []string{testAudiences.SSHSign[0]},\n\t\t\t\t},\n\t\t\t\tStep: &stepPayload{SSH: &SignSSHOptions{}},\n\t\t\t}\n\t\t\ttok, err := generateX5CSSHToken(x5cJWK, claims, withX5CHdr(x5cCerts))\n\t\t\trequire.NoError(t, err)\n\t\t\treturn test{\n\t\t\t\tp:      p,\n\t\t\t\tclaims: claims,\n\t\t\t\ttoken:  tok,\n\t\t\t\tcount:  10,\n\t\t\t}\n\t\t},\n\t\t\"ok/cnf\": func(t *testing.T) test {\n\t\t\tp, err := generateX5C(nil)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tid, err := randutil.ASCII(64)\n\t\t\trequire.NoError(t, err)\n\t\t\tnow := time.Now()\n\t\t\tclaims := &x5cPayload{\n\t\t\t\tClaims: jose.Claims{\n\t\t\t\t\tID:        id,\n\t\t\t\t\tSubject:   \"foo\",\n\t\t\t\t\tIssuer:    p.GetName(),\n\t\t\t\t\tIssuedAt:  jose.NewNumericDate(now),\n\t\t\t\t\tNotBefore: jose.NewNumericDate(now),\n\t\t\t\t\tExpiry:    jose.NewNumericDate(now.Add(5 * time.Minute)),\n\t\t\t\t\tAudience:  []string{testAudiences.SSHSign[0]},\n\t\t\t\t},\n\t\t\t\tStep: &stepPayload{SSH: &SignSSHOptions{\n\t\t\t\t\tCertType:   SSHHostCert,\n\t\t\t\t\tPrincipals: []string{\"host.smallstep.com\"},\n\t\t\t\t}},\n\t\t\t\tConfirmation: &cnfPayload{\n\t\t\t\t\tFingerprint: \"fingerprint\",\n\t\t\t\t},\n\t\t\t}\n\t\t\ttok, err := generateX5CSSHToken(x5cJWK, claims, withX5CHdr(x5cCerts))\n\t\t\trequire.NoError(t, err)\n\t\t\treturn test{\n\t\t\t\tp:           p,\n\t\t\t\tclaims:      claims,\n\t\t\t\ttoken:       tok,\n\t\t\t\tfingerprint: \"fingerprint\",\n\t\t\t\tcount:       10,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, tt := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ttc := tt(t)\n\t\t\tif opts, err := tc.p.AuthorizeSSHSign(context.Background(), tc.token); err != nil {\n\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\tvar sc render.StatusCodedError\n\t\t\t\t\tif assert.True(t, errors.As(err, &sc), \"error does not implement StatusCodedError interface\") {\n\t\t\t\t\t\tassert.Equal(t, tc.code, sc.StatusCode())\n\t\t\t\t\t}\n\t\t\t\t\tassertHasPrefix(t, err.Error(), tc.err.Error())\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif assert.Nil(t, tc.err) {\n\t\t\t\t\tif assert.NotNil(t, opts) {\n\t\t\t\t\t\ttot := 0\n\t\t\t\t\t\tfirstValidator := true\n\t\t\t\t\t\tnw := now()\n\t\t\t\t\t\tfor _, o := range opts {\n\t\t\t\t\t\t\tswitch v := o.(type) {\n\t\t\t\t\t\t\tcase Interface:\n\t\t\t\t\t\t\tcase sshCertOptionsValidator:\n\t\t\t\t\t\t\t\ttc.claims.Step.SSH.ValidAfter.t = time.Time{}\n\t\t\t\t\t\t\t\ttc.claims.Step.SSH.ValidBefore.t = time.Time{}\n\t\t\t\t\t\t\t\tif firstValidator {\n\t\t\t\t\t\t\t\t\tassert.Equal(t, *tc.claims.Step.SSH, SignSSHOptions(v))\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\tassert.Equal(t, SignSSHOptions{KeyID: tc.claims.Subject}, SignSSHOptions(v))\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tfirstValidator = false\n\t\t\t\t\t\t\tcase sshCertValidAfterModifier:\n\t\t\t\t\t\t\t\tassert.Equal(t, tc.claims.Step.SSH.ValidAfter.RelativeTime(nw).Unix(), int64(v))\n\t\t\t\t\t\t\tcase sshCertValidBeforeModifier:\n\t\t\t\t\t\t\t\tassert.Equal(t, tc.claims.Step.SSH.ValidBefore.RelativeTime(nw).Unix(), int64(v))\n\t\t\t\t\t\t\tcase *sshLimitDuration:\n\t\t\t\t\t\t\t\tassert.Equal(t, tc.p.ctl.Claimer, v.Claimer)\n\t\t\t\t\t\t\t\tassert.Equal(t, x5cCerts[0].NotAfter, v.NotAfter)\n\t\t\t\t\t\t\tcase *sshCertValidityValidator:\n\t\t\t\t\t\t\t\tassert.Equal(t, tc.p.ctl.Claimer, v.Claimer)\n\t\t\t\t\t\t\tcase *sshNamePolicyValidator:\n\t\t\t\t\t\t\t\tassert.Nil(t, v.userPolicyEngine)\n\t\t\t\t\t\t\t\tassert.Nil(t, v.hostPolicyEngine)\n\t\t\t\t\t\t\tcase *sshDefaultPublicKeyValidator, *sshCertDefaultValidator, sshCertificateOptionsFunc:\n\t\t\t\t\t\t\tcase *WebhookController:\n\t\t\t\t\t\t\t\tassert.Len(t, v.webhooks, 0)\n\t\t\t\t\t\t\t\tassert.Equal(t, linkedca.Webhook_SSH, v.certType)\n\t\t\t\t\t\t\t\tassert.Len(t, v.options, 2)\n\t\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\t\trequire.NoError(t, fmt.Errorf(\"unexpected sign option of type %T\", v))\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\ttot++\n\t\t\t\t\t\t}\n\t\t\t\t\t\tassert.Equal(t, tc.count, tot)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "authority/provisioners.go",
    "content": "package authority\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/x509\"\n\t\"encoding/json\"\n\t\"encoding/pem\"\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/pkg/errors\"\n\n\t\"github.com/smallstep/cli-utils/step\"\n\t\"github.com/smallstep/cli-utils/ui\"\n\t\"github.com/smallstep/linkedca\"\n\t\"go.step.sm/crypto/jose\"\n\n\t\"github.com/smallstep/certificates/authority/admin\"\n\t\"github.com/smallstep/certificates/authority/config\"\n\t\"github.com/smallstep/certificates/authority/policy\"\n\t\"github.com/smallstep/certificates/authority/provisioner\"\n\t\"github.com/smallstep/certificates/db\"\n\t\"github.com/smallstep/certificates/errs\"\n\t\"github.com/smallstep/certificates/internal/cast\"\n)\n\ntype raProvisioner interface {\n\tRAInfo() *provisioner.RAInfo\n}\n\ntype attProvisioner interface {\n\tAttestationData() *provisioner.AttestationData\n}\n\n// wrapProvisioner wraps the given provisioner with RA information and\n// attestation data.\nfunc wrapProvisioner(p provisioner.Interface, attData *provisioner.AttestationData) *wrappedProvisioner {\n\tvar raInfo *provisioner.RAInfo\n\tif rap, ok := p.(raProvisioner); ok {\n\t\traInfo = rap.RAInfo()\n\t}\n\n\treturn &wrappedProvisioner{\n\t\tInterface:       p,\n\t\tattestationData: attData,\n\t\traInfo:          raInfo,\n\t}\n}\n\n// wrapRAProvisioner wraps the given provisioner with RA information.\nfunc wrapRAProvisioner(p provisioner.Interface, raInfo *provisioner.RAInfo) *wrappedProvisioner {\n\treturn &wrappedProvisioner{\n\t\tInterface: p,\n\t\traInfo:    raInfo,\n\t}\n}\n\n// isRAProvisioner returns if the given provisioner is an RA provisioner.\nfunc isRAProvisioner(p provisioner.Interface) bool {\n\tif rap, ok := p.(raProvisioner); ok {\n\t\treturn rap.RAInfo() != nil\n\t}\n\treturn false\n}\n\n// wrappedProvisioner implements raProvisioner and attProvisioner.\ntype wrappedProvisioner struct {\n\tprovisioner.Interface\n\tattestationData *provisioner.AttestationData\n\traInfo          *provisioner.RAInfo\n}\n\nfunc (p *wrappedProvisioner) AttestationData() *provisioner.AttestationData {\n\treturn p.attestationData\n}\n\nfunc (p *wrappedProvisioner) RAInfo() *provisioner.RAInfo {\n\treturn p.raInfo\n}\n\n// GetEncryptedKey returns the JWE key corresponding to the given kid argument.\nfunc (a *Authority) GetEncryptedKey(kid string) (string, error) {\n\ta.adminMutex.RLock()\n\tdefer a.adminMutex.RUnlock()\n\tkey, ok := a.provisioners.LoadEncryptedKey(kid)\n\tif !ok {\n\t\treturn \"\", errs.NotFound(\"encrypted key with kid %s was not found\", kid)\n\t}\n\treturn key, nil\n}\n\n// GetProvisioners returns a map listing each provisioner and the JWK Key Set\n// with their public keys.\nfunc (a *Authority) GetProvisioners(cursor string, limit int) (provisioner.List, string, error) {\n\ta.adminMutex.RLock()\n\tdefer a.adminMutex.RUnlock()\n\tprovisioners, nextCursor := a.provisioners.Find(cursor, limit)\n\treturn provisioners, nextCursor, nil\n}\n\n// LoadProvisionerByCertificate returns an interface to the provisioner that\n// provisioned the certificate.\nfunc (a *Authority) LoadProvisionerByCertificate(crt *x509.Certificate) (provisioner.Interface, error) {\n\ta.adminMutex.RLock()\n\tdefer a.adminMutex.RUnlock()\n\tif p, err := a.unsafeLoadProvisionerFromDatabase(crt); err == nil {\n\t\treturn p, nil\n\t}\n\treturn a.unsafeLoadProvisionerFromExtension(crt)\n}\n\nfunc (a *Authority) unsafeLoadProvisionerFromExtension(crt *x509.Certificate) (provisioner.Interface, error) {\n\tp, ok := a.provisioners.LoadByCertificate(crt)\n\tif !ok || p.GetType() == 0 {\n\t\treturn nil, admin.NewError(admin.ErrorNotFoundType, \"unable to load provisioner from certificate\")\n\t}\n\treturn p, nil\n}\n\nfunc (a *Authority) unsafeLoadProvisionerFromDatabase(crt *x509.Certificate) (provisioner.Interface, error) {\n\t// certificateDataGetter is an interface that can be used to retrieve the\n\t// provisioner from a db or a linked ca.\n\ttype certificateDataGetter interface {\n\t\tGetCertificateData(string) (*db.CertificateData, error)\n\t}\n\n\tvar err error\n\tvar data *db.CertificateData\n\n\tif cdg, ok := a.adminDB.(certificateDataGetter); ok {\n\t\tdata, err = cdg.GetCertificateData(crt.SerialNumber.String())\n\t} else if cdg, ok := a.db.(certificateDataGetter); ok {\n\t\tdata, err = cdg.GetCertificateData(crt.SerialNumber.String())\n\t}\n\tif err == nil && data != nil && data.Provisioner != nil {\n\t\tif p, ok := a.provisioners.Load(data.Provisioner.ID); ok {\n\t\t\tif data.RaInfo != nil {\n\t\t\t\treturn wrapRAProvisioner(p, data.RaInfo), nil\n\t\t\t}\n\t\t\treturn p, nil\n\t\t}\n\t}\n\treturn nil, admin.NewError(admin.ErrorNotFoundType, \"unable to load provisioner from certificate\")\n}\n\n// LoadProvisionerByToken returns an interface to the provisioner that\n// provisioned the token.\nfunc (a *Authority) LoadProvisionerByToken(token *jose.JSONWebToken, claims *jose.Claims) (provisioner.Interface, error) {\n\ta.adminMutex.RLock()\n\tdefer a.adminMutex.RUnlock()\n\tp, ok := a.provisioners.LoadByToken(token, claims)\n\tif !ok {\n\t\treturn nil, admin.NewError(admin.ErrorNotFoundType, \"unable to load provisioner from token\")\n\t}\n\treturn p, nil\n}\n\n// LoadProvisionerByID returns an interface to the provisioner with the given ID.\nfunc (a *Authority) LoadProvisionerByID(id string) (provisioner.Interface, error) {\n\ta.adminMutex.RLock()\n\tdefer a.adminMutex.RUnlock()\n\tp, ok := a.provisioners.Load(id)\n\tif !ok {\n\t\treturn nil, admin.NewError(admin.ErrorNotFoundType, \"provisioner %s not found\", id)\n\t}\n\treturn p, nil\n}\n\n// LoadProvisionerByName returns an interface to the provisioner with the given Name.\nfunc (a *Authority) LoadProvisionerByName(name string) (provisioner.Interface, error) {\n\ta.adminMutex.RLock()\n\tdefer a.adminMutex.RUnlock()\n\tp, ok := a.provisioners.LoadByName(name)\n\tif !ok {\n\t\treturn nil, admin.NewError(admin.ErrorNotFoundType, \"provisioner %s not found\", name)\n\t}\n\treturn p, nil\n}\n\nfunc (a *Authority) generateProvisionerConfig(ctx context.Context) (provisioner.Config, error) {\n\t// Merge global and configuration claims\n\tclaimer, err := provisioner.NewClaimer(a.config.AuthorityConfig.Claims, config.GlobalProvisionerClaims)\n\tif err != nil {\n\t\treturn provisioner.Config{}, err\n\t}\n\t// TODO: should we also be combining the ssh federated roots here?\n\t// If we rotate ssh roots keys, sshpop provisioner will lose ability to\n\t// validate old SSH certificates, unless they are added as federated certs.\n\tsshKeys, err := a.GetSSHRoots(ctx)\n\tif err != nil {\n\t\treturn provisioner.Config{}, err\n\t}\n\treturn provisioner.Config{\n\t\tClaims:    claimer.Claims(),\n\t\tAudiences: a.config.GetAudiences(),\n\t\tSSHKeys: &provisioner.SSHKeys{\n\t\t\tUserKeys: sshKeys.UserKeys,\n\t\t\tHostKeys: sshKeys.HostKeys,\n\t\t},\n\t\tGetIdentityFunc:       a.getIdentityFunc,\n\t\tAuthorizeRenewFunc:    a.authorizeRenewFunc,\n\t\tAuthorizeSSHRenewFunc: a.authorizeSSHRenewFunc,\n\t\tWebhookClient:         a.webhookClient,\n\t\tHTTPClient:            a.httpClient,\n\t\tWrapTransport:         a.wrapTransport,\n\t\tSCEPKeyManager:        a.scepKeyManager,\n\t}, nil\n}\n\n// StoreProvisioner stores a provisioner to the authority.\nfunc (a *Authority) StoreProvisioner(ctx context.Context, prov *linkedca.Provisioner) error {\n\ta.adminMutex.Lock()\n\tdefer a.adminMutex.Unlock()\n\n\tcertProv, err := ProvisionerToCertificates(prov)\n\tif err != nil {\n\t\treturn admin.WrapErrorISE(err,\n\t\t\t\"error converting to certificates provisioner from linkedca provisioner\")\n\t}\n\n\tif _, ok := a.provisioners.LoadByName(prov.GetName()); ok {\n\t\treturn admin.NewError(admin.ErrorBadRequestType,\n\t\t\t\"provisioner with name %s already exists\", prov.GetName())\n\t}\n\tif _, ok := a.provisioners.LoadByTokenID(certProv.GetIDForToken()); ok {\n\t\treturn admin.NewError(admin.ErrorBadRequestType,\n\t\t\t\"provisioner with token ID %s already exists\", certProv.GetIDForToken())\n\t}\n\n\tprovisionerConfig, err := a.generateProvisionerConfig(ctx)\n\tif err != nil {\n\t\treturn admin.WrapErrorISE(err, \"error generating provisioner config\")\n\t}\n\n\tif err := a.checkProvisionerPolicy(ctx, prov.Name, prov.Policy); err != nil {\n\t\treturn err\n\t}\n\n\tif err := certProv.Init(provisionerConfig); err != nil {\n\t\treturn admin.WrapError(admin.ErrorBadRequestType, err, \"error validating configuration for provisioner %q\", prov.Name)\n\t}\n\n\t// Store to database -- this will set the ID.\n\tif err := a.adminDB.CreateProvisioner(ctx, prov); err != nil {\n\t\treturn admin.WrapErrorISE(err, \"error creating provisioner\")\n\t}\n\n\t// We need a new conversion that has the newly set ID.\n\tcertProv, err = ProvisionerToCertificates(prov)\n\tif err != nil {\n\t\treturn admin.WrapErrorISE(err,\n\t\t\t\"error converting to certificates provisioner from linkedca provisioner\")\n\t}\n\n\tif err := certProv.Init(provisionerConfig); err != nil {\n\t\treturn admin.WrapErrorISE(err, \"error initializing provisioner %s\", prov.Name)\n\t}\n\n\tif err := a.provisioners.Store(certProv); err != nil {\n\t\tif err := a.ReloadAdminResources(ctx); err != nil {\n\t\t\treturn admin.WrapErrorISE(err, \"error reloading admin resources on failed provisioner store\")\n\t\t}\n\t\treturn admin.WrapErrorISE(err, \"error storing provisioner in authority cache\")\n\t}\n\treturn nil\n}\n\n// UpdateProvisioner stores an provisioner.Interface to the authority.\nfunc (a *Authority) UpdateProvisioner(ctx context.Context, nu *linkedca.Provisioner) error {\n\ta.adminMutex.Lock()\n\tdefer a.adminMutex.Unlock()\n\n\tcertProv, err := ProvisionerToCertificates(nu)\n\tif err != nil {\n\t\treturn admin.WrapErrorISE(err,\n\t\t\t\"error converting to certificates provisioner from linkedca provisioner\")\n\t}\n\n\tprovisionerConfig, err := a.generateProvisionerConfig(ctx)\n\tif err != nil {\n\t\treturn admin.WrapErrorISE(err, \"error generating provisioner config\")\n\t}\n\n\tif err := a.checkProvisionerPolicy(ctx, nu.Name, nu.Policy); err != nil {\n\t\treturn err\n\t}\n\n\tif err := certProv.Init(provisionerConfig); err != nil {\n\t\treturn admin.WrapErrorISE(err, \"error initializing provisioner %s\", nu.Name)\n\t}\n\n\tif err := a.provisioners.Update(certProv); err != nil {\n\t\treturn admin.WrapErrorISE(err, \"error updating provisioner '%s' in authority cache\", nu.Name)\n\t}\n\tif err := a.adminDB.UpdateProvisioner(ctx, nu); err != nil {\n\t\tif err := a.ReloadAdminResources(ctx); err != nil {\n\t\t\treturn admin.WrapErrorISE(err, \"error reloading admin resources on failed provisioner update\")\n\t\t}\n\t\treturn admin.WrapErrorISE(err, \"error updating provisioner '%s'\", nu.Name)\n\t}\n\treturn nil\n}\n\n// RemoveProvisioner removes an provisioner.Interface from the authority.\nfunc (a *Authority) RemoveProvisioner(ctx context.Context, id string) error {\n\ta.adminMutex.Lock()\n\tdefer a.adminMutex.Unlock()\n\n\tp, ok := a.provisioners.Load(id)\n\tif !ok {\n\t\treturn admin.NewError(admin.ErrorBadRequestType,\n\t\t\t\"provisioner %s not found\", id)\n\t}\n\n\tprovName, provID := p.GetName(), p.GetID()\n\tif a.IsAdminAPIEnabled() {\n\t\t// Validate\n\t\t//  - Check that there will be SUPER_ADMINs that remain after we\n\t\t//    remove this provisioner.\n\t\tif a.IsAdminAPIEnabled() && a.admins.SuperCount() == a.admins.SuperCountByProvisioner(provName) {\n\t\t\treturn admin.NewError(admin.ErrorBadRequestType,\n\t\t\t\t\"cannot remove provisioner %s because no super admins will remain\", provName)\n\t\t}\n\n\t\t// Delete all admins associated with the provisioner.\n\t\tadmins, ok := a.admins.LoadByProvisioner(provName)\n\t\tif ok {\n\t\t\tfor _, adm := range admins {\n\t\t\t\tif err := a.removeAdmin(ctx, adm.Id); err != nil {\n\t\t\t\t\treturn admin.WrapErrorISE(err, \"error deleting admin %s, as part of provisioner %s deletion\", adm.Subject, provName)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// Remove provisioner from authority caches.\n\tif err := a.provisioners.Remove(provID); err != nil {\n\t\treturn admin.WrapErrorISE(err, \"error removing provisioner from authority cache\")\n\t}\n\t// Remove provisioner from database.\n\tif err := a.adminDB.DeleteProvisioner(ctx, provID); err != nil {\n\t\tif err := a.ReloadAdminResources(ctx); err != nil {\n\t\t\treturn admin.WrapErrorISE(err, \"error reloading admin resources on failed provisioner remove\")\n\t\t}\n\t\treturn admin.WrapErrorISE(err, \"error deleting provisioner %s\", provName)\n\t}\n\treturn nil\n}\n\n// CreateFirstProvisioner creates and stores the first provisioner when using\n// admin database provisioner storage.\nfunc CreateFirstProvisioner(ctx context.Context, adminDB admin.DB, password string) (*linkedca.Provisioner, error) {\n\tif password == \"\" {\n\t\tpass, err := ui.PromptPasswordGenerate(\"Please enter the password to encrypt your first provisioner, leave empty and we'll generate one\")\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tpassword = string(pass)\n\t}\n\n\tjwk, jwe, err := jose.GenerateDefaultKeyPair([]byte(password))\n\tif err != nil {\n\t\treturn nil, admin.WrapErrorISE(err, \"error generating JWK key pair\")\n\t}\n\n\tjwkPubBytes, err := jwk.MarshalJSON()\n\tif err != nil {\n\t\treturn nil, admin.WrapErrorISE(err, \"error marshaling JWK\")\n\t}\n\tjwePrivStr, err := jwe.CompactSerialize()\n\tif err != nil {\n\t\treturn nil, admin.WrapErrorISE(err, \"error serializing JWE\")\n\t}\n\n\tp := &linkedca.Provisioner{\n\t\tName: \"Admin JWK\",\n\t\tType: linkedca.Provisioner_JWK,\n\t\tDetails: &linkedca.ProvisionerDetails{\n\t\t\tData: &linkedca.ProvisionerDetails_JWK{\n\t\t\t\tJWK: &linkedca.JWKProvisioner{\n\t\t\t\t\tPublicKey:           jwkPubBytes,\n\t\t\t\t\tEncryptedPrivateKey: []byte(jwePrivStr),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tClaims: &linkedca.Claims{\n\t\t\tX509: &linkedca.X509Claims{\n\t\t\t\tEnabled: true,\n\t\t\t\tDurations: &linkedca.Durations{\n\t\t\t\t\tDefault: \"5m\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\tif err := adminDB.CreateProvisioner(ctx, p); err != nil {\n\t\treturn nil, admin.WrapErrorISE(err, \"error creating provisioner\")\n\t}\n\treturn p, nil\n}\n\n// ValidateClaims validates the Claims type.\nfunc ValidateClaims(c *linkedca.Claims) error {\n\tif c == nil {\n\t\treturn nil\n\t}\n\tif c.X509 != nil {\n\t\tif c.X509.Durations != nil {\n\t\t\tif err := ValidateDurations(c.X509.Durations); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\tif c.Ssh != nil {\n\t\tif c.Ssh.UserDurations != nil {\n\t\t\tif err := ValidateDurations(c.Ssh.UserDurations); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\tif c.Ssh.HostDurations != nil {\n\t\t\tif err := ValidateDurations(c.Ssh.HostDurations); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\n// ValidateDurations validates the Durations type.\nfunc ValidateDurations(d *linkedca.Durations) error {\n\tvar (\n\t\terr                 error\n\t\tminDur, maxDur, def *provisioner.Duration\n\t)\n\n\tif d.Min != \"\" {\n\t\tminDur, err = provisioner.NewDuration(d.Min)\n\t\tif err != nil {\n\t\t\treturn admin.WrapError(admin.ErrorBadRequestType, err, \"min duration '%s' is invalid\", d.Min)\n\t\t}\n\t\tif minDur.Value() < 0 {\n\t\t\treturn admin.WrapError(admin.ErrorBadRequestType, err, \"min duration '%s' cannot be less than 0\", d.Min)\n\t\t}\n\t}\n\tif d.Max != \"\" {\n\t\tmaxDur, err = provisioner.NewDuration(d.Max)\n\t\tif err != nil {\n\t\t\treturn admin.WrapError(admin.ErrorBadRequestType, err, \"max duration '%s' is invalid\", d.Max)\n\t\t}\n\t\tif maxDur.Value() < 0 {\n\t\t\treturn admin.WrapError(admin.ErrorBadRequestType, err, \"max duration '%s' cannot be less than 0\", d.Max)\n\t\t}\n\t}\n\tif d.Default != \"\" {\n\t\tdef, err = provisioner.NewDuration(d.Default)\n\t\tif err != nil {\n\t\t\treturn admin.WrapError(admin.ErrorBadRequestType, err, \"default duration '%s' is invalid\", d.Default)\n\t\t}\n\t\tif def.Value() < 0 {\n\t\t\treturn admin.WrapError(admin.ErrorBadRequestType, err, \"default duration '%s' cannot be less than 0\", d.Default)\n\t\t}\n\t}\n\tif d.Min != \"\" && d.Max != \"\" && minDur.Value() > maxDur.Value() {\n\t\treturn admin.NewError(admin.ErrorBadRequestType,\n\t\t\t\"min duration '%s' cannot be greater than max duration '%s'\", d.Min, d.Max)\n\t}\n\tif d.Min != \"\" && d.Default != \"\" && minDur.Value() > def.Value() {\n\t\treturn admin.NewError(admin.ErrorBadRequestType,\n\t\t\t\"min duration '%s' cannot be greater than default duration '%s'\", d.Min, d.Default)\n\t}\n\tif d.Default != \"\" && d.Max != \"\" && minDur.Value() > def.Value() {\n\t\treturn admin.NewError(admin.ErrorBadRequestType,\n\t\t\t\"default duration '%s' cannot be greater than max duration '%s'\", d.Default, d.Max)\n\t}\n\treturn nil\n}\n\nfunc provisionerListToCertificates(l []*linkedca.Provisioner) (provisioner.List, error) {\n\tvar nu provisioner.List\n\tfor _, p := range l {\n\t\tcertProv, err := ProvisionerToCertificates(p)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tnu = append(nu, certProv)\n\t}\n\treturn nu, nil\n}\n\nfunc optionsToCertificates(p *linkedca.Provisioner) *provisioner.Options {\n\tops := &provisioner.Options{\n\t\tX509: &provisioner.X509Options{},\n\t\tSSH:  &provisioner.SSHOptions{},\n\t}\n\tif p.X509Template != nil {\n\t\tops.X509.Template = string(p.X509Template.Template)\n\t\tops.X509.TemplateData = p.X509Template.Data\n\t}\n\tif p.SshTemplate != nil {\n\t\tops.SSH.Template = string(p.SshTemplate.Template)\n\t\tops.SSH.TemplateData = p.SshTemplate.Data\n\t}\n\tif pol := p.GetPolicy(); pol != nil {\n\t\tif x := pol.GetX509(); x != nil {\n\t\t\tif allow := x.GetAllow(); allow != nil {\n\t\t\t\tops.X509.AllowedNames = &policy.X509NameOptions{\n\t\t\t\t\tDNSDomains:     allow.Dns,\n\t\t\t\t\tIPRanges:       allow.Ips,\n\t\t\t\t\tEmailAddresses: allow.Emails,\n\t\t\t\t\tURIDomains:     allow.Uris,\n\t\t\t\t}\n\t\t\t}\n\t\t\tif deny := x.GetDeny(); deny != nil {\n\t\t\t\tops.X509.DeniedNames = &policy.X509NameOptions{\n\t\t\t\t\tDNSDomains:     deny.Dns,\n\t\t\t\t\tIPRanges:       deny.Ips,\n\t\t\t\t\tEmailAddresses: deny.Emails,\n\t\t\t\t\tURIDomains:     deny.Uris,\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif ssh := pol.GetSsh(); ssh != nil {\n\t\t\tif host := ssh.GetHost(); host != nil {\n\t\t\t\tops.SSH.Host = &policy.SSHHostCertificateOptions{}\n\t\t\t\tif allow := host.GetAllow(); allow != nil {\n\t\t\t\t\tops.SSH.Host.AllowedNames = &policy.SSHNameOptions{\n\t\t\t\t\t\tDNSDomains: allow.Dns,\n\t\t\t\t\t\tIPRanges:   allow.Ips,\n\t\t\t\t\t\tPrincipals: allow.Principals,\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif deny := host.GetDeny(); deny != nil {\n\t\t\t\t\tops.SSH.Host.DeniedNames = &policy.SSHNameOptions{\n\t\t\t\t\t\tDNSDomains: deny.Dns,\n\t\t\t\t\t\tIPRanges:   deny.Ips,\n\t\t\t\t\t\tPrincipals: deny.Principals,\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tif user := ssh.GetUser(); user != nil {\n\t\t\t\tops.SSH.User = &policy.SSHUserCertificateOptions{}\n\t\t\t\tif allow := user.GetAllow(); allow != nil {\n\t\t\t\t\tops.SSH.User.AllowedNames = &policy.SSHNameOptions{\n\t\t\t\t\t\tEmailAddresses: allow.Emails,\n\t\t\t\t\t\tPrincipals:     allow.Principals,\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif deny := user.GetDeny(); deny != nil {\n\t\t\t\t\tops.SSH.User.DeniedNames = &policy.SSHNameOptions{\n\t\t\t\t\t\tEmailAddresses: deny.Emails,\n\t\t\t\t\t\tPrincipals:     deny.Principals,\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\tfor _, wh := range p.Webhooks {\n\t\twhCert := webhookToCertificates(wh)\n\t\tops.Webhooks = append(ops.Webhooks, whCert)\n\t}\n\treturn ops\n}\n\nfunc webhookToCertificates(wh *linkedca.Webhook) *provisioner.Webhook {\n\tpwh := &provisioner.Webhook{\n\t\tID:                   wh.Id,\n\t\tName:                 wh.Name,\n\t\tURL:                  wh.Url,\n\t\tKind:                 wh.Kind.String(),\n\t\tSecret:               wh.Secret,\n\t\tDisableTLSClientAuth: wh.DisableTlsClientAuth,\n\t\tCertType:             wh.CertType.String(),\n\t}\n\n\tswitch a := wh.GetAuth().(type) {\n\tcase *linkedca.Webhook_BearerToken:\n\t\tpwh.BearerToken = a.BearerToken.BearerToken\n\tcase *linkedca.Webhook_BasicAuth:\n\t\tpwh.BasicAuth.Username = a.BasicAuth.Username\n\t\tpwh.BasicAuth.Password = a.BasicAuth.Password\n\t}\n\n\treturn pwh\n}\n\nfunc provisionerWebhookToLinkedca(pwh *provisioner.Webhook) *linkedca.Webhook {\n\tlwh := &linkedca.Webhook{\n\t\tId:                   pwh.ID,\n\t\tName:                 pwh.Name,\n\t\tUrl:                  pwh.URL,\n\t\tKind:                 linkedca.Webhook_Kind(linkedca.Webhook_Kind_value[pwh.Kind]),\n\t\tSecret:               pwh.Secret,\n\t\tDisableTlsClientAuth: pwh.DisableTLSClientAuth,\n\t\tCertType:             linkedca.Webhook_CertType(linkedca.Webhook_CertType_value[pwh.CertType]),\n\t}\n\tif pwh.BearerToken != \"\" {\n\t\tlwh.Auth = &linkedca.Webhook_BearerToken{\n\t\t\tBearerToken: &linkedca.BearerToken{\n\t\t\t\tBearerToken: pwh.BearerToken,\n\t\t\t},\n\t\t}\n\t} else if pwh.BasicAuth.Username != \"\" || pwh.BasicAuth.Password != \"\" {\n\t\tlwh.Auth = &linkedca.Webhook_BasicAuth{\n\t\t\tBasicAuth: &linkedca.BasicAuth{\n\t\t\t\tUsername: pwh.BasicAuth.Username,\n\t\t\t\tPassword: pwh.BasicAuth.Password,\n\t\t\t},\n\t\t}\n\t}\n\n\treturn lwh\n}\n\nfunc durationsToCertificates(d *linkedca.Durations) (minDur, maxDur, def *provisioner.Duration, err error) {\n\tif d.Min != \"\" {\n\t\tminDur, err = provisioner.NewDuration(d.Min)\n\t\tif err != nil {\n\t\t\treturn nil, nil, nil, admin.WrapErrorISE(err, \"error parsing minimum duration '%s'\", d.Min)\n\t\t}\n\t}\n\tif d.Max != \"\" {\n\t\tmaxDur, err = provisioner.NewDuration(d.Max)\n\t\tif err != nil {\n\t\t\treturn nil, nil, nil, admin.WrapErrorISE(err, \"error parsing maximum duration '%s'\", d.Max)\n\t\t}\n\t}\n\tif d.Default != \"\" {\n\t\tdef, err = provisioner.NewDuration(d.Default)\n\t\tif err != nil {\n\t\t\treturn nil, nil, nil, admin.WrapErrorISE(err, \"error parsing default duration '%s'\", d.Default)\n\t\t}\n\t}\n\treturn\n}\n\nfunc durationsToLinkedca(d *provisioner.Duration) string {\n\tif d == nil {\n\t\treturn \"\"\n\t}\n\treturn d.Duration.String()\n}\n\n// claimsToCertificates converts the linkedca provisioner claims type to the\n// certifictes claims type.\nfunc claimsToCertificates(c *linkedca.Claims) (*provisioner.Claims, error) {\n\tif c == nil {\n\t\t//nolint:nilnil // nil claims do not pose an issue.\n\t\treturn nil, nil\n\t}\n\n\tpc := &provisioner.Claims{\n\t\tDisableRenewal:             &c.DisableRenewal,\n\t\tAllowRenewalAfterExpiry:    &c.AllowRenewalAfterExpiry,\n\t\tDisableSmallstepExtensions: &c.DisableSmallstepExtensions,\n\t}\n\n\tvar err error\n\n\tif xc := c.X509; xc != nil {\n\t\tif d := xc.Durations; d != nil {\n\t\t\tpc.MinTLSDur, pc.MaxTLSDur, pc.DefaultTLSDur, err = durationsToCertificates(d)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\t}\n\tif sc := c.Ssh; sc != nil {\n\t\tpc.EnableSSHCA = &sc.Enabled\n\t\tif d := sc.UserDurations; d != nil {\n\t\t\tpc.MinUserSSHDur, pc.MaxUserSSHDur, pc.DefaultUserSSHDur, err = durationsToCertificates(d)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\t\tif d := sc.HostDurations; d != nil {\n\t\t\tpc.MinHostSSHDur, pc.MaxHostSSHDur, pc.DefaultHostSSHDur, err = durationsToCertificates(d)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\t}\n\n\treturn pc, nil\n}\n\nfunc claimsToLinkedca(c *provisioner.Claims) *linkedca.Claims {\n\tif c == nil {\n\t\treturn nil\n\t}\n\n\tdisableRenewal := config.DefaultDisableRenewal\n\tallowRenewalAfterExpiry := config.DefaultAllowRenewalAfterExpiry\n\tdisableSmallstepExtensions := config.DefaultDisableSmallstepExtensions\n\n\tif c.DisableRenewal != nil {\n\t\tdisableRenewal = *c.DisableRenewal\n\t}\n\tif c.AllowRenewalAfterExpiry != nil {\n\t\tallowRenewalAfterExpiry = *c.AllowRenewalAfterExpiry\n\t}\n\tif c.DisableSmallstepExtensions != nil {\n\t\tdisableSmallstepExtensions = *c.DisableSmallstepExtensions\n\t}\n\n\tlc := &linkedca.Claims{\n\t\tDisableRenewal:             disableRenewal,\n\t\tAllowRenewalAfterExpiry:    allowRenewalAfterExpiry,\n\t\tDisableSmallstepExtensions: disableSmallstepExtensions,\n\t}\n\n\tif c.DefaultTLSDur != nil || c.MinTLSDur != nil || c.MaxTLSDur != nil {\n\t\tlc.X509 = &linkedca.X509Claims{\n\t\t\tEnabled: true,\n\t\t\tDurations: &linkedca.Durations{\n\t\t\t\tDefault: durationsToLinkedca(c.DefaultTLSDur),\n\t\t\t\tMin:     durationsToLinkedca(c.MinTLSDur),\n\t\t\t\tMax:     durationsToLinkedca(c.MaxTLSDur),\n\t\t\t},\n\t\t}\n\t}\n\n\tif c.EnableSSHCA != nil && *c.EnableSSHCA {\n\t\tlc.Ssh = &linkedca.SSHClaims{\n\t\t\tEnabled: true,\n\t\t}\n\t\tif c.DefaultUserSSHDur != nil || c.MinUserSSHDur != nil || c.MaxUserSSHDur != nil {\n\t\t\tlc.Ssh.UserDurations = &linkedca.Durations{\n\t\t\t\tDefault: durationsToLinkedca(c.DefaultUserSSHDur),\n\t\t\t\tMin:     durationsToLinkedca(c.MinUserSSHDur),\n\t\t\t\tMax:     durationsToLinkedca(c.MaxUserSSHDur),\n\t\t\t}\n\t\t}\n\t\tif c.DefaultHostSSHDur != nil || c.MinHostSSHDur != nil || c.MaxHostSSHDur != nil {\n\t\t\tlc.Ssh.HostDurations = &linkedca.Durations{\n\t\t\t\tDefault: durationsToLinkedca(c.DefaultHostSSHDur),\n\t\t\t\tMin:     durationsToLinkedca(c.MinHostSSHDur),\n\t\t\t\tMax:     durationsToLinkedca(c.MaxHostSSHDur),\n\t\t\t}\n\t\t}\n\t}\n\n\treturn lc\n}\n\nfunc provisionerOptionsToLinkedca(p *provisioner.Options) (*linkedca.Template, *linkedca.Template, []*linkedca.Webhook, error) {\n\tvar err error\n\tvar x509Template, sshTemplate *linkedca.Template\n\n\tif p == nil {\n\t\treturn nil, nil, nil, nil\n\t}\n\n\tif p.X509 != nil && p.X509.HasTemplate() {\n\t\tx509Template = &linkedca.Template{\n\t\t\tTemplate: nil,\n\t\t\tData:     nil,\n\t\t}\n\n\t\tif p.X509.Template != \"\" {\n\t\t\tx509Template.Template = []byte(p.X509.Template)\n\t\t} else if p.X509.TemplateFile != \"\" {\n\t\t\tfilename := step.Abs(p.X509.TemplateFile)\n\t\t\tif x509Template.Template, err = os.ReadFile(filename); err != nil {\n\t\t\t\treturn nil, nil, nil, errors.Wrap(err, \"error reading x509 template\")\n\t\t\t}\n\t\t}\n\n\t\tif p.X509.TemplateData != nil {\n\t\t\tx509Template.Data = p.X509.TemplateData\n\t\t}\n\t}\n\n\tif p.SSH != nil && p.SSH.HasTemplate() {\n\t\tsshTemplate = &linkedca.Template{\n\t\t\tTemplate: nil,\n\t\t\tData:     nil,\n\t\t}\n\n\t\tif p.SSH.Template != \"\" {\n\t\t\tsshTemplate.Template = []byte(p.SSH.Template)\n\t\t} else if p.SSH.TemplateFile != \"\" {\n\t\t\tfilename := step.Abs(p.SSH.TemplateFile)\n\t\t\tif sshTemplate.Template, err = os.ReadFile(filename); err != nil {\n\t\t\t\treturn nil, nil, nil, errors.Wrap(err, \"error reading ssh template\")\n\t\t\t}\n\t\t}\n\n\t\tif p.SSH.TemplateData != nil {\n\t\t\tsshTemplate.Data = p.SSH.TemplateData\n\t\t}\n\t}\n\n\tvar webhooks []*linkedca.Webhook\n\tfor _, pwh := range p.Webhooks {\n\t\twebhooks = append(webhooks, provisionerWebhookToLinkedca(pwh))\n\t}\n\n\treturn x509Template, sshTemplate, webhooks, nil\n}\n\nfunc provisionerPEMToLinkedca(b []byte) [][]byte {\n\tvar roots [][]byte\n\tvar block *pem.Block\n\tfor {\n\t\tif block, b = pem.Decode(b); block == nil {\n\t\t\tbreak\n\t\t}\n\t\troots = append(roots, pem.EncodeToMemory(block))\n\t}\n\treturn roots\n}\n\nfunc provisionerPEMToCertificates(bs [][]byte) []byte {\n\tvar roots []byte\n\tfor i, root := range bs {\n\t\tif i > 0 && !bytes.HasSuffix(root, []byte{'\\n'}) {\n\t\t\troots = append(roots, '\\n')\n\t\t}\n\t\troots = append(roots, root...)\n\t}\n\treturn roots\n}\n\n// ProvisionerToCertificates converts the linkedca provisioner type to the certificates provisioner\n// interface.\nfunc ProvisionerToCertificates(p *linkedca.Provisioner) (provisioner.Interface, error) {\n\tclaims, err := claimsToCertificates(p.Claims)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tdetails := p.Details.GetData()\n\tif details == nil {\n\t\treturn nil, errors.New(\"provisioner does not have any details\")\n\t}\n\n\toptions := optionsToCertificates(p)\n\n\tswitch d := details.(type) {\n\tcase *linkedca.ProvisionerDetails_JWK:\n\t\tjwk := new(jose.JSONWebKey)\n\t\tif err := json.Unmarshal(d.JWK.PublicKey, &jwk); err != nil {\n\t\t\treturn nil, errors.Wrap(err, \"error unmarshaling public key\")\n\t\t}\n\t\treturn &provisioner.JWK{\n\t\t\tID:           p.Id,\n\t\t\tType:         p.Type.String(),\n\t\t\tName:         p.Name,\n\t\t\tKey:          jwk,\n\t\t\tEncryptedKey: string(d.JWK.EncryptedPrivateKey),\n\t\t\tClaims:       claims,\n\t\t\tOptions:      options,\n\t\t}, nil\n\tcase *linkedca.ProvisionerDetails_X5C:\n\t\tvar roots []byte\n\t\tfor i, root := range d.X5C.GetRoots() {\n\t\t\tif i > 0 {\n\t\t\t\troots = append(roots, '\\n')\n\t\t\t}\n\t\t\troots = append(roots, root...)\n\t\t}\n\t\treturn &provisioner.X5C{\n\t\t\tID:      p.Id,\n\t\t\tType:    p.Type.String(),\n\t\t\tName:    p.Name,\n\t\t\tRoots:   roots,\n\t\t\tClaims:  claims,\n\t\t\tOptions: options,\n\t\t}, nil\n\tcase *linkedca.ProvisionerDetails_K8SSA:\n\t\tvar publicKeys []byte\n\t\tfor i, k := range d.K8SSA.GetPublicKeys() {\n\t\t\tif i > 0 {\n\t\t\t\tpublicKeys = append(publicKeys, '\\n')\n\t\t\t}\n\t\t\tpublicKeys = append(publicKeys, k...)\n\t\t}\n\t\treturn &provisioner.K8sSA{\n\t\t\tID:      p.Id,\n\t\t\tType:    p.Type.String(),\n\t\t\tName:    p.Name,\n\t\t\tPubKeys: publicKeys,\n\t\t\tClaims:  claims,\n\t\t\tOptions: options,\n\t\t}, nil\n\tcase *linkedca.ProvisionerDetails_SSHPOP:\n\t\treturn &provisioner.SSHPOP{\n\t\t\tID:     p.Id,\n\t\t\tType:   p.Type.String(),\n\t\t\tName:   p.Name,\n\t\t\tClaims: claims,\n\t\t}, nil\n\tcase *linkedca.ProvisionerDetails_ACME:\n\t\tcfg := d.ACME\n\t\treturn &provisioner.ACME{\n\t\t\tID:                 p.Id,\n\t\t\tType:               p.Type.String(),\n\t\t\tName:               p.Name,\n\t\t\tForceCN:            cfg.ForceCn,\n\t\t\tTermsOfService:     cfg.TermsOfService,\n\t\t\tWebsite:            cfg.Website,\n\t\t\tCaaIdentities:      cfg.CaaIdentities,\n\t\t\tRequireEAB:         cfg.RequireEab,\n\t\t\tChallenges:         challengesToCertificates(cfg.Challenges),\n\t\t\tAttestationFormats: attestationFormatsToCertificates(cfg.AttestationFormats),\n\t\t\tAttestationRoots:   provisionerPEMToCertificates(cfg.AttestationRoots),\n\t\t\tClaims:             claims,\n\t\t\tOptions:            options,\n\t\t}, nil\n\tcase *linkedca.ProvisionerDetails_OIDC:\n\t\tcfg := d.OIDC\n\t\treturn &provisioner.OIDC{\n\t\t\tID:                    p.Id,\n\t\t\tType:                  p.Type.String(),\n\t\t\tName:                  p.Name,\n\t\t\tTenantID:              cfg.TenantId,\n\t\t\tClientID:              cfg.ClientId,\n\t\t\tClientSecret:          cfg.ClientSecret,\n\t\t\tConfigurationEndpoint: cfg.ConfigurationEndpoint,\n\t\t\tAdmins:                cfg.Admins,\n\t\t\tDomains:               cfg.Domains,\n\t\t\tGroups:                cfg.Groups,\n\t\t\tListenAddress:         cfg.ListenAddress,\n\t\t\tScopes:                cfg.Scopes,\n\t\t\tAuthParams:            cfg.AuthParams,\n\t\t\tClaims:                claims,\n\t\t\tOptions:               options,\n\t\t}, nil\n\tcase *linkedca.ProvisionerDetails_AWS:\n\t\tcfg := d.AWS\n\t\tinstanceAge, err := parseInstanceAge(cfg.InstanceAge)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn &provisioner.AWS{\n\t\t\tID:                     p.Id,\n\t\t\tType:                   p.Type.String(),\n\t\t\tName:                   p.Name,\n\t\t\tAccounts:               cfg.Accounts,\n\t\t\tDisableCustomSANs:      cfg.DisableCustomSans,\n\t\t\tDisableTrustOnFirstUse: cfg.DisableTrustOnFirstUse,\n\t\t\tInstanceAge:            instanceAge,\n\t\t\tClaims:                 claims,\n\t\t\tOptions:                options,\n\t\t}, nil\n\tcase *linkedca.ProvisionerDetails_GCP:\n\t\tcfg := d.GCP\n\t\tinstanceAge, err := parseInstanceAge(cfg.InstanceAge)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn &provisioner.GCP{\n\t\t\tID:                     p.Id,\n\t\t\tType:                   p.Type.String(),\n\t\t\tName:                   p.Name,\n\t\t\tServiceAccounts:        cfg.ServiceAccounts,\n\t\t\tProjectIDs:             cfg.ProjectIds,\n\t\t\tOrganizationID:         cfg.OrganizationId,\n\t\t\tDisableCustomSANs:      cfg.DisableCustomSans,\n\t\t\tDisableTrustOnFirstUse: cfg.DisableTrustOnFirstUse,\n\t\t\tDisableSSHCAUser:       cfg.DisableSshCaUser,\n\t\t\tDisableSSHCAHost:       cfg.DisableSshCaHost,\n\t\t\tInstanceAge:            instanceAge,\n\t\t\tClaims:                 claims,\n\t\t\tOptions:                options,\n\t\t}, nil\n\tcase *linkedca.ProvisionerDetails_Azure:\n\t\tcfg := d.Azure\n\t\treturn &provisioner.Azure{\n\t\t\tID:                     p.Id,\n\t\t\tType:                   p.Type.String(),\n\t\t\tName:                   p.Name,\n\t\t\tTenantID:               cfg.TenantId,\n\t\t\tResourceGroups:         cfg.ResourceGroups,\n\t\t\tSubscriptionIDs:        cfg.SubscriptionIds,\n\t\t\tObjectIDs:              cfg.ObjectIds,\n\t\t\tAudience:               cfg.Audience,\n\t\t\tDisableCustomSANs:      cfg.DisableCustomSans,\n\t\t\tDisableTrustOnFirstUse: cfg.DisableTrustOnFirstUse,\n\t\t\tClaims:                 claims,\n\t\t\tOptions:                options,\n\t\t}, nil\n\tcase *linkedca.ProvisionerDetails_SCEP:\n\t\tcfg := d.SCEP\n\t\ts := &provisioner.SCEP{\n\t\t\tID:                            p.Id,\n\t\t\tType:                          p.Type.String(),\n\t\t\tName:                          p.Name,\n\t\t\tForceCN:                       cfg.ForceCn,\n\t\t\tChallengePassword:             cfg.Challenge,\n\t\t\tCapabilities:                  cfg.Capabilities,\n\t\t\tIncludeRoot:                   cfg.IncludeRoot,\n\t\t\tExcludeIntermediate:           cfg.ExcludeIntermediate,\n\t\t\tMinimumPublicKeyLength:        int(cfg.MinimumPublicKeyLength),\n\t\t\tEncryptionAlgorithmIdentifier: int(cfg.EncryptionAlgorithmIdentifier),\n\t\t\tClaims:                        claims,\n\t\t\tOptions:                       options,\n\t\t}\n\t\tif decrypter := cfg.GetDecrypter(); decrypter != nil {\n\t\t\ts.DecrypterCertificate = decrypter.Certificate\n\t\t\ts.DecrypterKeyPEM = decrypter.Key\n\t\t\ts.DecrypterKeyURI = decrypter.KeyUri\n\t\t\ts.DecrypterKeyPassword = string(decrypter.KeyPassword)\n\t\t}\n\t\treturn s, nil\n\tcase *linkedca.ProvisionerDetails_Nebula:\n\t\tvar roots []byte\n\t\tfor i, root := range d.Nebula.GetRoots() {\n\t\t\tif i > 0 && !bytes.HasSuffix(root, []byte{'\\n'}) {\n\t\t\t\troots = append(roots, '\\n')\n\t\t\t}\n\t\t\troots = append(roots, root...)\n\t\t}\n\t\treturn &provisioner.Nebula{\n\t\t\tID:      p.Id,\n\t\t\tType:    p.Type.String(),\n\t\t\tName:    p.Name,\n\t\t\tRoots:   roots,\n\t\t\tClaims:  claims,\n\t\t\tOptions: options,\n\t\t}, nil\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"provisioner %s not implemented\", p.Type)\n\t}\n}\n\n// ProvisionerToLinkedca converts a provisioner.Interface to a\n// linkedca.Provisioner type.\nfunc ProvisionerToLinkedca(p provisioner.Interface) (*linkedca.Provisioner, error) {\n\tswitch p := p.(type) {\n\tcase *provisioner.JWK:\n\t\tx509Template, sshTemplate, webhooks, err := provisionerOptionsToLinkedca(p.Options)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tpublicKey, err := json.Marshal(p.Key)\n\t\tif err != nil {\n\t\t\treturn nil, errors.Wrap(err, \"error marshaling key\")\n\t\t}\n\t\treturn &linkedca.Provisioner{\n\t\t\tId:   p.ID,\n\t\t\tType: linkedca.Provisioner_JWK,\n\t\t\tName: p.GetName(),\n\t\t\tDetails: &linkedca.ProvisionerDetails{\n\t\t\t\tData: &linkedca.ProvisionerDetails_JWK{\n\t\t\t\t\tJWK: &linkedca.JWKProvisioner{\n\t\t\t\t\t\tPublicKey:           publicKey,\n\t\t\t\t\t\tEncryptedPrivateKey: []byte(p.EncryptedKey),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tClaims:       claimsToLinkedca(p.Claims),\n\t\t\tX509Template: x509Template,\n\t\t\tSshTemplate:  sshTemplate,\n\t\t\tWebhooks:     webhooks,\n\t\t}, nil\n\tcase *provisioner.OIDC:\n\t\tx509Template, sshTemplate, webhooks, err := provisionerOptionsToLinkedca(p.Options)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn &linkedca.Provisioner{\n\t\t\tId:   p.ID,\n\t\t\tType: linkedca.Provisioner_OIDC,\n\t\t\tName: p.GetName(),\n\t\t\tDetails: &linkedca.ProvisionerDetails{\n\t\t\t\tData: &linkedca.ProvisionerDetails_OIDC{\n\t\t\t\t\tOIDC: &linkedca.OIDCProvisioner{\n\t\t\t\t\t\tClientId:              p.ClientID,\n\t\t\t\t\t\tClientSecret:          p.ClientSecret,\n\t\t\t\t\t\tConfigurationEndpoint: p.ConfigurationEndpoint,\n\t\t\t\t\t\tAdmins:                p.Admins,\n\t\t\t\t\t\tDomains:               p.Domains,\n\t\t\t\t\t\tGroups:                p.Groups,\n\t\t\t\t\t\tListenAddress:         p.ListenAddress,\n\t\t\t\t\t\tTenantId:              p.TenantID,\n\t\t\t\t\t\tScopes:                p.Scopes,\n\t\t\t\t\t\tAuthParams:            p.AuthParams,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tClaims:       claimsToLinkedca(p.Claims),\n\t\t\tX509Template: x509Template,\n\t\t\tSshTemplate:  sshTemplate,\n\t\t\tWebhooks:     webhooks,\n\t\t}, nil\n\tcase *provisioner.GCP:\n\t\tx509Template, sshTemplate, webhooks, err := provisionerOptionsToLinkedca(p.Options)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn &linkedca.Provisioner{\n\t\t\tId:   p.ID,\n\t\t\tType: linkedca.Provisioner_GCP,\n\t\t\tName: p.GetName(),\n\t\t\tDetails: &linkedca.ProvisionerDetails{\n\t\t\t\tData: &linkedca.ProvisionerDetails_GCP{\n\t\t\t\t\tGCP: &linkedca.GCPProvisioner{\n\t\t\t\t\t\tServiceAccounts:        p.ServiceAccounts,\n\t\t\t\t\t\tProjectIds:             p.ProjectIDs,\n\t\t\t\t\t\tOrganizationId:         p.OrganizationID,\n\t\t\t\t\t\tDisableCustomSans:      p.DisableCustomSANs,\n\t\t\t\t\t\tDisableTrustOnFirstUse: p.DisableTrustOnFirstUse,\n\t\t\t\t\t\tDisableSshCaUser:       p.DisableSSHCAUser,\n\t\t\t\t\t\tDisableSshCaHost:       p.DisableSSHCAHost,\n\t\t\t\t\t\tInstanceAge:            p.InstanceAge.String(),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tClaims:       claimsToLinkedca(p.Claims),\n\t\t\tX509Template: x509Template,\n\t\t\tSshTemplate:  sshTemplate,\n\t\t\tWebhooks:     webhooks,\n\t\t}, nil\n\tcase *provisioner.AWS:\n\t\tx509Template, sshTemplate, webhooks, err := provisionerOptionsToLinkedca(p.Options)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn &linkedca.Provisioner{\n\t\t\tId:   p.ID,\n\t\t\tType: linkedca.Provisioner_AWS,\n\t\t\tName: p.GetName(),\n\t\t\tDetails: &linkedca.ProvisionerDetails{\n\t\t\t\tData: &linkedca.ProvisionerDetails_AWS{\n\t\t\t\t\tAWS: &linkedca.AWSProvisioner{\n\t\t\t\t\t\tAccounts:               p.Accounts,\n\t\t\t\t\t\tDisableCustomSans:      p.DisableCustomSANs,\n\t\t\t\t\t\tDisableTrustOnFirstUse: p.DisableTrustOnFirstUse,\n\t\t\t\t\t\tInstanceAge:            p.InstanceAge.String(),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tClaims:       claimsToLinkedca(p.Claims),\n\t\t\tX509Template: x509Template,\n\t\t\tSshTemplate:  sshTemplate,\n\t\t\tWebhooks:     webhooks,\n\t\t}, nil\n\tcase *provisioner.Azure:\n\t\tx509Template, sshTemplate, webhooks, err := provisionerOptionsToLinkedca(p.Options)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn &linkedca.Provisioner{\n\t\t\tId:   p.ID,\n\t\t\tType: linkedca.Provisioner_AZURE,\n\t\t\tName: p.GetName(),\n\t\t\tDetails: &linkedca.ProvisionerDetails{\n\t\t\t\tData: &linkedca.ProvisionerDetails_Azure{\n\t\t\t\t\tAzure: &linkedca.AzureProvisioner{\n\t\t\t\t\t\tTenantId:               p.TenantID,\n\t\t\t\t\t\tResourceGroups:         p.ResourceGroups,\n\t\t\t\t\t\tSubscriptionIds:        p.SubscriptionIDs,\n\t\t\t\t\t\tObjectIds:              p.ObjectIDs,\n\t\t\t\t\t\tAudience:               p.Audience,\n\t\t\t\t\t\tDisableCustomSans:      p.DisableCustomSANs,\n\t\t\t\t\t\tDisableTrustOnFirstUse: p.DisableTrustOnFirstUse,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tClaims:       claimsToLinkedca(p.Claims),\n\t\t\tX509Template: x509Template,\n\t\t\tSshTemplate:  sshTemplate,\n\t\t\tWebhooks:     webhooks,\n\t\t}, nil\n\tcase *provisioner.ACME:\n\t\tx509Template, sshTemplate, webhooks, err := provisionerOptionsToLinkedca(p.Options)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn &linkedca.Provisioner{\n\t\t\tId:   p.ID,\n\t\t\tType: linkedca.Provisioner_ACME,\n\t\t\tName: p.GetName(),\n\t\t\tDetails: &linkedca.ProvisionerDetails{\n\t\t\t\tData: &linkedca.ProvisionerDetails_ACME{\n\t\t\t\t\tACME: &linkedca.ACMEProvisioner{\n\t\t\t\t\t\tForceCn:            p.ForceCN,\n\t\t\t\t\t\tTermsOfService:     p.TermsOfService,\n\t\t\t\t\t\tWebsite:            p.Website,\n\t\t\t\t\t\tCaaIdentities:      p.CaaIdentities,\n\t\t\t\t\t\tRequireEab:         p.RequireEAB,\n\t\t\t\t\t\tChallenges:         challengesToLinkedca(p.Challenges),\n\t\t\t\t\t\tAttestationFormats: attestationFormatsToLinkedca(p.AttestationFormats),\n\t\t\t\t\t\tAttestationRoots:   provisionerPEMToLinkedca(p.AttestationRoots),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tClaims:       claimsToLinkedca(p.Claims),\n\t\t\tX509Template: x509Template,\n\t\t\tSshTemplate:  sshTemplate,\n\t\t\tWebhooks:     webhooks,\n\t\t}, nil\n\tcase *provisioner.X5C:\n\t\tx509Template, sshTemplate, webhooks, err := provisionerOptionsToLinkedca(p.Options)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn &linkedca.Provisioner{\n\t\t\tId:   p.ID,\n\t\t\tType: linkedca.Provisioner_X5C,\n\t\t\tName: p.GetName(),\n\t\t\tDetails: &linkedca.ProvisionerDetails{\n\t\t\t\tData: &linkedca.ProvisionerDetails_X5C{\n\t\t\t\t\tX5C: &linkedca.X5CProvisioner{\n\t\t\t\t\t\tRoots: provisionerPEMToLinkedca(p.Roots),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tClaims:       claimsToLinkedca(p.Claims),\n\t\t\tX509Template: x509Template,\n\t\t\tSshTemplate:  sshTemplate,\n\t\t\tWebhooks:     webhooks,\n\t\t}, nil\n\tcase *provisioner.K8sSA:\n\t\tx509Template, sshTemplate, webhooks, err := provisionerOptionsToLinkedca(p.Options)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn &linkedca.Provisioner{\n\t\t\tId:   p.ID,\n\t\t\tType: linkedca.Provisioner_K8SSA,\n\t\t\tName: p.GetName(),\n\t\t\tDetails: &linkedca.ProvisionerDetails{\n\t\t\t\tData: &linkedca.ProvisionerDetails_K8SSA{\n\t\t\t\t\tK8SSA: &linkedca.K8SSAProvisioner{\n\t\t\t\t\t\tPublicKeys: provisionerPEMToLinkedca(p.PubKeys),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tClaims:       claimsToLinkedca(p.Claims),\n\t\t\tX509Template: x509Template,\n\t\t\tSshTemplate:  sshTemplate,\n\t\t\tWebhooks:     webhooks,\n\t\t}, nil\n\tcase *provisioner.SSHPOP:\n\t\treturn &linkedca.Provisioner{\n\t\t\tId:   p.ID,\n\t\t\tType: linkedca.Provisioner_SSHPOP,\n\t\t\tName: p.GetName(),\n\t\t\tDetails: &linkedca.ProvisionerDetails{\n\t\t\t\tData: &linkedca.ProvisionerDetails_SSHPOP{\n\t\t\t\t\tSSHPOP: &linkedca.SSHPOPProvisioner{},\n\t\t\t\t},\n\t\t\t},\n\t\t\tClaims: claimsToLinkedca(p.Claims),\n\t\t}, nil\n\tcase *provisioner.SCEP:\n\t\tx509Template, sshTemplate, webhooks, err := provisionerOptionsToLinkedca(p.Options)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn &linkedca.Provisioner{\n\t\t\tId:   p.ID,\n\t\t\tType: linkedca.Provisioner_SCEP,\n\t\t\tName: p.GetName(),\n\t\t\tDetails: &linkedca.ProvisionerDetails{\n\t\t\t\tData: &linkedca.ProvisionerDetails_SCEP{\n\t\t\t\t\tSCEP: &linkedca.SCEPProvisioner{\n\t\t\t\t\t\tForceCn:                       p.ForceCN,\n\t\t\t\t\t\tChallenge:                     p.ChallengePassword,\n\t\t\t\t\t\tCapabilities:                  p.Capabilities,\n\t\t\t\t\t\tMinimumPublicKeyLength:        cast.Int32(p.MinimumPublicKeyLength),\n\t\t\t\t\t\tIncludeRoot:                   p.IncludeRoot,\n\t\t\t\t\t\tExcludeIntermediate:           p.ExcludeIntermediate,\n\t\t\t\t\t\tEncryptionAlgorithmIdentifier: cast.Int32(p.EncryptionAlgorithmIdentifier),\n\t\t\t\t\t\tDecrypter: &linkedca.SCEPDecrypter{\n\t\t\t\t\t\t\tCertificate: p.DecrypterCertificate,\n\t\t\t\t\t\t\tKey:         p.DecrypterKeyPEM,\n\t\t\t\t\t\t\tKeyUri:      p.DecrypterKeyURI,\n\t\t\t\t\t\t\tKeyPassword: []byte(p.DecrypterKeyPassword),\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tClaims:       claimsToLinkedca(p.Claims),\n\t\t\tX509Template: x509Template,\n\t\t\tSshTemplate:  sshTemplate,\n\t\t\tWebhooks:     webhooks,\n\t\t}, nil\n\tcase *provisioner.Nebula:\n\t\tx509Template, sshTemplate, webhooks, err := provisionerOptionsToLinkedca(p.Options)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn &linkedca.Provisioner{\n\t\t\tId:   p.ID,\n\t\t\tType: linkedca.Provisioner_NEBULA,\n\t\t\tName: p.GetName(),\n\t\t\tDetails: &linkedca.ProvisionerDetails{\n\t\t\t\tData: &linkedca.ProvisionerDetails_Nebula{\n\t\t\t\t\tNebula: &linkedca.NebulaProvisioner{\n\t\t\t\t\t\tRoots: provisionerPEMToLinkedca(p.Roots),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tClaims:       claimsToLinkedca(p.Claims),\n\t\t\tX509Template: x509Template,\n\t\t\tSshTemplate:  sshTemplate,\n\t\t\tWebhooks:     webhooks,\n\t\t}, nil\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"provisioner %s not implemented\", p.GetType())\n\t}\n}\n\nfunc parseInstanceAge(age string) (provisioner.Duration, error) {\n\tvar instanceAge provisioner.Duration\n\tif age != \"\" {\n\t\tiap, err := provisioner.NewDuration(age)\n\t\tif err != nil {\n\t\t\treturn instanceAge, err\n\t\t}\n\t\tinstanceAge = *iap\n\t}\n\treturn instanceAge, nil\n}\n\n// challengesToCertificates converts linkedca challenges to provisioner ones\n// skipping the unknown ones.\nfunc challengesToCertificates(challenges []linkedca.ACMEProvisioner_ChallengeType) []provisioner.ACMEChallenge {\n\tret := make([]provisioner.ACMEChallenge, 0, len(challenges))\n\tfor _, ch := range challenges {\n\t\tswitch ch {\n\t\tcase linkedca.ACMEProvisioner_HTTP_01:\n\t\t\tret = append(ret, provisioner.HTTP_01)\n\t\tcase linkedca.ACMEProvisioner_DNS_01:\n\t\t\tret = append(ret, provisioner.DNS_01)\n\t\tcase linkedca.ACMEProvisioner_TLS_ALPN_01:\n\t\t\tret = append(ret, provisioner.TLS_ALPN_01)\n\t\tcase linkedca.ACMEProvisioner_DEVICE_ATTEST_01:\n\t\t\tret = append(ret, provisioner.DEVICE_ATTEST_01)\n\t\t}\n\t}\n\treturn ret\n}\n\n// challengesToLinkedca converts provisioner challenges to linkedca ones\n// skipping the unknown ones.\nfunc challengesToLinkedca(challenges []provisioner.ACMEChallenge) []linkedca.ACMEProvisioner_ChallengeType {\n\tret := make([]linkedca.ACMEProvisioner_ChallengeType, 0, len(challenges))\n\tfor _, ch := range challenges {\n\t\tswitch provisioner.ACMEChallenge(ch.String()) {\n\t\tcase provisioner.HTTP_01:\n\t\t\tret = append(ret, linkedca.ACMEProvisioner_HTTP_01)\n\t\tcase provisioner.DNS_01:\n\t\t\tret = append(ret, linkedca.ACMEProvisioner_DNS_01)\n\t\tcase provisioner.TLS_ALPN_01:\n\t\t\tret = append(ret, linkedca.ACMEProvisioner_TLS_ALPN_01)\n\t\tcase provisioner.DEVICE_ATTEST_01:\n\t\t\tret = append(ret, linkedca.ACMEProvisioner_DEVICE_ATTEST_01)\n\t\t}\n\t}\n\treturn ret\n}\n\n// attestationFormatsToCertificates converts linkedca attestation formats to\n// provisioner ones skipping the unknown ones.\nfunc attestationFormatsToCertificates(formats []linkedca.ACMEProvisioner_AttestationFormatType) []provisioner.ACMEAttestationFormat {\n\tret := make([]provisioner.ACMEAttestationFormat, 0, len(formats))\n\tfor _, f := range formats {\n\t\tswitch f {\n\t\tcase linkedca.ACMEProvisioner_APPLE:\n\t\t\tret = append(ret, provisioner.APPLE)\n\t\tcase linkedca.ACMEProvisioner_STEP:\n\t\t\tret = append(ret, provisioner.STEP)\n\t\tcase linkedca.ACMEProvisioner_TPM:\n\t\t\tret = append(ret, provisioner.TPM)\n\t\t}\n\t}\n\treturn ret\n}\n\n// attestationFormatsToLinkedca converts provisioner attestation formats to\n// linkedca ones skipping the unknown ones.\nfunc attestationFormatsToLinkedca(formats []provisioner.ACMEAttestationFormat) []linkedca.ACMEProvisioner_AttestationFormatType {\n\tret := make([]linkedca.ACMEProvisioner_AttestationFormatType, 0, len(formats))\n\tfor _, f := range formats {\n\t\tswitch provisioner.ACMEAttestationFormat(f.String()) {\n\t\tcase provisioner.APPLE:\n\t\t\tret = append(ret, linkedca.ACMEProvisioner_APPLE)\n\t\tcase provisioner.STEP:\n\t\t\tret = append(ret, linkedca.ACMEProvisioner_STEP)\n\t\tcase provisioner.TPM:\n\t\t\tret = append(ret, linkedca.ACMEProvisioner_TPM)\n\t\t}\n\t}\n\treturn ret\n}\n"
  },
  {
    "path": "authority/provisioners_test.go",
    "content": "package authority\n\nimport (\n\t\"context\"\n\t\"crypto/x509\"\n\t\"errors\"\n\t\"net/http\"\n\t\"reflect\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/smallstep/linkedca\"\n\t\"go.step.sm/crypto/jose\"\n\t\"go.step.sm/crypto/keyutil\"\n\n\t\"github.com/smallstep/assert\"\n\t\"github.com/smallstep/certificates/api/render\"\n\t\"github.com/smallstep/certificates/authority/admin\"\n\t\"github.com/smallstep/certificates/authority/provisioner\"\n\t\"github.com/smallstep/certificates/db\"\n)\n\nfunc TestGetEncryptedKey(t *testing.T) {\n\ttype ek struct {\n\t\ta    *Authority\n\t\tkid  string\n\t\terr  error\n\t\tcode int\n\t}\n\ttests := map[string]func(t *testing.T) *ek{\n\t\t\"ok\": func(t *testing.T) *ek {\n\t\t\tc, err := LoadConfiguration(\"../ca/testdata/ca.json\")\n\t\t\trequire.NoError(t, err)\n\t\t\ta, err := New(c)\n\t\t\trequire.NoError(t, err)\n\t\t\treturn &ek{\n\t\t\t\ta:   a,\n\t\t\t\tkid: c.AuthorityConfig.Provisioners[1].(*provisioner.JWK).Key.KeyID,\n\t\t\t}\n\t\t},\n\t\t\"fail-not-found\": func(t *testing.T) *ek {\n\t\t\tc, err := LoadConfiguration(\"../ca/testdata/ca.json\")\n\t\t\trequire.NoError(t, err)\n\t\t\ta, err := New(c)\n\t\t\trequire.NoError(t, err)\n\t\t\treturn &ek{\n\t\t\t\ta:    a,\n\t\t\t\tkid:  \"foo\",\n\t\t\t\terr:  errors.New(\"encrypted key with kid foo was not found\"),\n\t\t\t\tcode: http.StatusNotFound,\n\t\t\t}\n\t\t},\n\t}\n\n\tfor name, genTestCase := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ttc := genTestCase(t)\n\n\t\t\tek, err := tc.a.GetEncryptedKey(tc.kid)\n\t\t\tif err != nil {\n\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\tvar sc render.StatusCodedError\n\t\t\t\t\tif assert.True(t, errors.As(err, &sc), \"error does not implement StatusCodedError interface\") {\n\t\t\t\t\t\tassert.Equals(t, sc.StatusCode(), tc.code)\n\t\t\t\t\t}\n\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.err.Error())\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif assert.Nil(t, tc.err) {\n\t\t\t\t\tval, ok := tc.a.provisioners.Load(\"mike:\" + tc.kid)\n\t\t\t\t\tassert.Fatal(t, ok)\n\t\t\t\t\tp, ok := val.(*provisioner.JWK)\n\t\t\t\t\tassert.Fatal(t, ok)\n\t\t\t\t\tassert.Equals(t, p.EncryptedKey, ek)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\ntype mockAdminDB struct {\n\tadmin.MockDB\n\tMGetCertificateData func(string) (*db.CertificateData, error)\n}\n\nfunc (c *mockAdminDB) GetCertificateData(sn string) (*db.CertificateData, error) {\n\treturn c.MGetCertificateData(sn)\n}\n\nfunc TestGetProvisioners(t *testing.T) {\n\ttype gp struct {\n\t\ta    *Authority\n\t\terr  error\n\t\tcode int\n\t}\n\ttests := map[string]func(t *testing.T) *gp{\n\t\t\"ok\": func(t *testing.T) *gp {\n\t\t\tc, err := LoadConfiguration(\"../ca/testdata/ca.json\")\n\t\t\trequire.NoError(t, err)\n\t\t\ta, err := New(c)\n\t\t\trequire.NoError(t, err)\n\t\t\treturn &gp{a: a}\n\t\t},\n\t\t\"ok/rsa\": func(t *testing.T) *gp {\n\t\t\tc, err := LoadConfiguration(\"../ca/testdata/rsaca.json\")\n\t\t\trequire.NoError(t, err)\n\t\t\ta, err := New(c)\n\t\t\trequire.NoError(t, err)\n\t\t\treturn &gp{a: a}\n\t\t},\n\t}\n\n\tfor name, genTestCase := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ttc := genTestCase(t)\n\n\t\t\tps, next, err := tc.a.GetProvisioners(\"\", 0)\n\t\t\tif err != nil {\n\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\tvar sc render.StatusCodedError\n\t\t\t\t\tif assert.True(t, errors.As(err, &sc), \"error does not implement StatusCodedError interface\") {\n\t\t\t\t\t\tassert.Equals(t, tc.code, sc.StatusCode())\n\t\t\t\t\t}\n\t\t\t\t\tassert.HasPrefix(t, tc.err.Error(), err.Error())\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif assert.Nil(t, tc.err) {\n\t\t\t\t\tassert.Equals(t, tc.a.config.AuthorityConfig.Provisioners, ps)\n\t\t\t\t\tassert.Equals(t, \"\", next)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestAuthority_LoadProvisionerByCertificate(t *testing.T) {\n\t_, priv, err := keyutil.GenerateDefaultKeyPair()\n\trequire.NoError(t, err)\n\tcsr := getCSR(t, priv)\n\n\tsign := func(a *Authority, extraOpts ...provisioner.SignOption) *x509.Certificate {\n\t\tkey, err := jose.ReadKey(\"testdata/secrets/step_cli_key_priv.jwk\", jose.WithPassword([]byte(\"pass\")))\n\t\trequire.NoError(t, err)\n\t\ttoken, err := generateToken(\"smallstep test\", \"step-cli\", testAudiences.Sign[0], []string{\"test.smallstep.com\"}, time.Now(), key)\n\t\trequire.NoError(t, err)\n\t\tctx := provisioner.NewContextWithMethod(context.Background(), provisioner.SignMethod)\n\t\topts, err := a.Authorize(ctx, token)\n\t\trequire.NoError(t, err)\n\t\topts = append(opts, extraOpts...)\n\t\tcerts, err := a.SignWithContext(ctx, csr, provisioner.SignOptions{}, opts...)\n\t\trequire.NoError(t, err)\n\t\treturn certs[0]\n\t}\n\tgetProvisioner := func(a *Authority, name string) provisioner.Interface {\n\t\tp, ok := a.provisioners.LoadByName(name)\n\t\tif !ok {\n\t\t\tt.Fatalf(\"provisioner %s does not exists\", name)\n\t\t}\n\t\treturn p\n\t}\n\tremoveExtension := provisioner.CertificateEnforcerFunc(func(cert *x509.Certificate) error {\n\t\tfor i, ext := range cert.ExtraExtensions {\n\t\t\tif ext.Id.Equal(provisioner.StepOIDProvisioner) {\n\t\t\t\tcert.ExtraExtensions = append(cert.ExtraExtensions[:i], cert.ExtraExtensions[i+1:]...)\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t})\n\n\ta0 := testAuthority(t)\n\n\ta1 := testAuthority(t)\n\ta1.db = &db.MockAuthDB{\n\t\tMUseToken: func(id, tok string) (bool, error) {\n\t\t\treturn true, nil\n\t\t},\n\t\tMGetCertificateData: func(serialNumber string) (*db.CertificateData, error) {\n\t\t\tp, err := a1.LoadProvisionerByName(\"dev\")\n\t\t\trequire.NoError(t, err)\n\t\t\treturn &db.CertificateData{\n\t\t\t\tProvisioner: &db.ProvisionerData{\n\t\t\t\t\tID:   p.GetID(),\n\t\t\t\t\tName: p.GetName(),\n\t\t\t\t\tType: p.GetType().String(),\n\t\t\t\t},\n\t\t\t}, nil\n\t\t},\n\t}\n\n\ta2 := testAuthority(t)\n\ta2.adminDB = &mockAdminDB{\n\t\tMGetCertificateData: (func(s string) (*db.CertificateData, error) {\n\t\t\tp, err := a2.LoadProvisionerByName(\"dev\")\n\t\t\trequire.NoError(t, err)\n\t\t\treturn &db.CertificateData{\n\t\t\t\tProvisioner: &db.ProvisionerData{\n\t\t\t\t\tID:   p.GetID(),\n\t\t\t\t\tName: p.GetName(),\n\t\t\t\t\tType: p.GetType().String(),\n\t\t\t\t},\n\t\t\t}, nil\n\t\t}),\n\t}\n\n\ta3 := testAuthority(t)\n\ta3.db = &db.MockAuthDB{\n\t\tMUseToken: func(id, tok string) (bool, error) {\n\t\t\treturn true, nil\n\t\t},\n\t\tMGetCertificateData: func(serialNumber string) (*db.CertificateData, error) {\n\t\t\treturn &db.CertificateData{\n\t\t\t\tProvisioner: &db.ProvisionerData{\n\t\t\t\t\tID: \"foo\", Name: \"foo\", Type: \"foo\",\n\t\t\t\t},\n\t\t\t}, nil\n\t\t},\n\t}\n\n\ta4 := testAuthority(t)\n\ta4.adminDB = &mockAdminDB{\n\t\tMGetCertificateData: func(serialNumber string) (*db.CertificateData, error) {\n\t\t\treturn &db.CertificateData{\n\t\t\t\tProvisioner: &db.ProvisionerData{\n\t\t\t\t\tID: \"foo\", Name: \"foo\", Type: \"foo\",\n\t\t\t\t},\n\t\t\t}, nil\n\t\t},\n\t}\n\n\ttype args struct {\n\t\tcrt *x509.Certificate\n\t}\n\ttests := []struct {\n\t\tname      string\n\t\tauthority *Authority\n\t\targs      args\n\t\twant      provisioner.Interface\n\t\twantErr   bool\n\t}{\n\t\t{\"ok from certificate\", a0, args{sign(a0)}, getProvisioner(a0, \"step-cli\"), false},\n\t\t{\"ok from db\", a1, args{sign(a1)}, getProvisioner(a1, \"dev\"), false},\n\t\t{\"ok from admindb\", a2, args{sign(a2)}, getProvisioner(a2, \"dev\"), false},\n\t\t{\"fail from certificate\", a0, args{sign(a0, removeExtension)}, nil, true},\n\t\t{\"fail from db\", a3, args{sign(a3, removeExtension)}, nil, true},\n\t\t{\"fail from admindb\", a4, args{sign(a4, removeExtension)}, nil, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := tt.authority.LoadProvisionerByCertificate(tt.args.crt)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"Authority.LoadProvisionerByCertificate() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"Authority.LoadProvisionerByCertificate() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestProvisionerWebhookToLinkedca(t *testing.T) {\n\ttype test struct {\n\t\tlwh *linkedca.Webhook\n\t\tpwh *provisioner.Webhook\n\t}\n\ttests := map[string]test{\n\t\t\"empty\": test{\n\t\t\tlwh: &linkedca.Webhook{},\n\t\t\tpwh: &provisioner.Webhook{Kind: \"NO_KIND\", CertType: \"ALL\"},\n\t\t},\n\t\t\"enriching ssh basic auth\": test{\n\t\t\tlwh: &linkedca.Webhook{\n\t\t\t\tId:     \"abc123\",\n\t\t\t\tName:   \"people\",\n\t\t\t\tUrl:    \"https://localhost\",\n\t\t\t\tKind:   linkedca.Webhook_ENRICHING,\n\t\t\t\tSecret: \"secret\",\n\t\t\t\tAuth: &linkedca.Webhook_BasicAuth{\n\t\t\t\t\tBasicAuth: &linkedca.BasicAuth{\n\t\t\t\t\t\tUsername: \"user\",\n\t\t\t\t\t\tPassword: \"pass\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tDisableTlsClientAuth: true,\n\t\t\t\tCertType:             linkedca.Webhook_SSH,\n\t\t\t},\n\t\t\tpwh: &provisioner.Webhook{\n\t\t\t\tID:     \"abc123\",\n\t\t\t\tName:   \"people\",\n\t\t\t\tURL:    \"https://localhost\",\n\t\t\t\tKind:   \"ENRICHING\",\n\t\t\t\tSecret: \"secret\",\n\t\t\t\tBasicAuth: struct {\n\t\t\t\t\tUsername string\n\t\t\t\t\tPassword string\n\t\t\t\t}{\n\t\t\t\t\tUsername: \"user\",\n\t\t\t\t\tPassword: \"pass\",\n\t\t\t\t},\n\t\t\t\tDisableTLSClientAuth: true,\n\t\t\t\tCertType:             \"SSH\",\n\t\t\t},\n\t\t},\n\t\t\"authorizing x509 bearer auth\": test{\n\t\t\tlwh: &linkedca.Webhook{\n\t\t\t\tId:     \"abc123\",\n\t\t\t\tName:   \"people\",\n\t\t\t\tUrl:    \"https://localhost\",\n\t\t\t\tKind:   linkedca.Webhook_AUTHORIZING,\n\t\t\t\tSecret: \"secret\",\n\t\t\t\tAuth: &linkedca.Webhook_BearerToken{\n\t\t\t\t\tBearerToken: &linkedca.BearerToken{\n\t\t\t\t\t\tBearerToken: \"tkn\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tCertType: linkedca.Webhook_X509,\n\t\t\t},\n\t\t\tpwh: &provisioner.Webhook{\n\t\t\t\tID:          \"abc123\",\n\t\t\t\tName:        \"people\",\n\t\t\t\tURL:         \"https://localhost\",\n\t\t\t\tKind:        \"AUTHORIZING\",\n\t\t\t\tSecret:      \"secret\",\n\t\t\t\tBearerToken: \"tkn\",\n\t\t\t\tCertType:    \"X509\",\n\t\t\t},\n\t\t},\n\t}\n\tfor name, test := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tgotLWH := provisionerWebhookToLinkedca(test.pwh)\n\t\t\tassert.Equals(t, test.lwh, gotLWH)\n\n\t\t\tgotPWH := webhookToCertificates(test.lwh)\n\t\t\tassert.Equals(t, test.pwh, gotPWH)\n\t\t})\n\t}\n}\n\nfunc Test_wrapRAProvisioner(t *testing.T) {\n\ttype args struct {\n\t\tp      provisioner.Interface\n\t\traInfo *provisioner.RAInfo\n\t}\n\ttests := []struct {\n\t\tname string\n\t\targs args\n\t\twant *wrappedProvisioner\n\t}{\n\t\t{\"ok\", args{&provisioner.JWK{Name: \"jwt\"}, &provisioner.RAInfo{ProvisionerName: \"ra\"}}, &wrappedProvisioner{\n\t\t\tInterface: &provisioner.JWK{Name: \"jwt\"},\n\t\t\traInfo:    &provisioner.RAInfo{ProvisionerName: \"ra\"},\n\t\t}},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif got := wrapRAProvisioner(tt.args.p, tt.args.raInfo); !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"wrapRAProvisioner() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_isRAProvisioner(t *testing.T) {\n\ttype args struct {\n\t\tp provisioner.Interface\n\t}\n\ttests := []struct {\n\t\tname string\n\t\targs args\n\t\twant bool\n\t}{\n\t\t{\"true\", args{&wrappedProvisioner{\n\t\t\tInterface: &provisioner.JWK{Name: \"jwt\"},\n\t\t\traInfo:    &provisioner.RAInfo{ProvisionerName: \"ra\"},\n\t\t}}, true},\n\t\t{\"nil ra\", args{&wrappedProvisioner{\n\t\t\tInterface: &provisioner.JWK{Name: \"jwt\"},\n\t\t}}, false},\n\t\t{\"not ra\", args{&provisioner.JWK{Name: \"jwt\"}}, false},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif got := isRAProvisioner(tt.args.p); got != tt.want {\n\t\t\t\tt.Errorf(\"isRAProvisioner() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "authority/root.go",
    "content": "package authority\n\nimport (\n\t\"crypto/x509\"\n\n\t\"github.com/smallstep/certificates/errs\"\n)\n\n// Root returns the certificate corresponding to the given SHA sum argument.\nfunc (a *Authority) Root(sum string) (*x509.Certificate, error) {\n\tval, ok := a.certificates.Load(sum)\n\tif !ok {\n\t\treturn nil, errs.NotFound(\"certificate with fingerprint %s was not found\", sum)\n\t}\n\n\tcrt, ok := val.(*x509.Certificate)\n\tif !ok {\n\t\treturn nil, errs.InternalServer(\"stored value is not a *x509.Certificate\")\n\t}\n\treturn crt, nil\n}\n\n// GetRootCertificate returns the server root certificate.\nfunc (a *Authority) GetRootCertificate() *x509.Certificate {\n\treturn a.rootX509Certs[0]\n}\n\n// GetRootCertificates returns the server root certificates.\n//\n// In the Authority interface we also have a similar method, GetRoots, at the\n// moment the functionality of these two methods are almost identical, but this\n// method is intended to be used internally by CA HTTP server to load the roots\n// that will be set in the tls.Config while GetRoots will be used by the\n// Authority interface and might have extra checks in the future.\nfunc (a *Authority) GetRootCertificates() []*x509.Certificate {\n\treturn a.rootX509Certs\n}\n\n// GetRoots returns all the root certificates for this CA.\n// This method implements the Authority interface.\nfunc (a *Authority) GetRoots() ([]*x509.Certificate, error) {\n\treturn a.rootX509Certs, nil\n}\n\n// GetFederation returns all the root certificates in the federation.\n// This method implements the Authority interface.\nfunc (a *Authority) GetFederation() (federation []*x509.Certificate, err error) {\n\ta.certificates.Range(func(_, v interface{}) bool {\n\t\tcrt, ok := v.(*x509.Certificate)\n\t\tif !ok {\n\t\t\tfederation = nil\n\t\t\terr = errs.InternalServer(\"stored value is not a *x509.Certificate\")\n\t\t\treturn false\n\t\t}\n\t\tfederation = append(federation, crt)\n\t\treturn true\n\t})\n\treturn\n}\n\n// GetIntermediateCertificate return the intermediate certificate that issues\n// the leaf certificates in the CA.\n//\n// This method can return nil if the CA is configured with a Certificate\n// Authority Service (CAS) that does not implement the\n// CertificateAuthorityGetter interface.\nfunc (a *Authority) GetIntermediateCertificate() *x509.Certificate {\n\tif len(a.intermediateX509Certs) > 0 {\n\t\treturn a.intermediateX509Certs[0]\n\t}\n\treturn nil\n}\n\n// GetIntermediateCertificates returns a list of all intermediate certificates\n// configured. The first certificate in the list will be the issuer certificate.\n//\n// This method can return an empty list or nil if the CA is configured with a\n// Certificate Authority Service (CAS) that does not implement the\n// CertificateAuthorityGetter interface.\nfunc (a *Authority) GetIntermediateCertificates() []*x509.Certificate {\n\treturn a.intermediateX509Certs\n}\n"
  },
  {
    "path": "authority/root_test.go",
    "content": "package authority\n\nimport (\n\t\"crypto/x509\"\n\t\"crypto/x509/pkix\"\n\t\"errors\"\n\t\"net/http\"\n\t\"reflect\"\n\t\"testing\"\n\n\t\"github.com/smallstep/assert\"\n\t\"github.com/smallstep/certificates/api/render\"\n\t\"github.com/stretchr/testify/require\"\n\t\"go.step.sm/crypto/keyutil\"\n\t\"go.step.sm/crypto/minica\"\n\t\"go.step.sm/crypto/pemutil\"\n)\n\nfunc TestRoot(t *testing.T) {\n\ta := testAuthority(t)\n\ta.certificates.Store(\"invaliddata\", \"a string\") // invalid cert for testing\n\n\ttests := map[string]struct {\n\t\tsum  string\n\t\terr  error\n\t\tcode int\n\t}{\n\t\t\"not-found\":                  {\"foo\", errors.New(\"certificate with fingerprint foo was not found\"), http.StatusNotFound},\n\t\t\"invalid-stored-certificate\": {\"invaliddata\", errors.New(\"stored value is not a *x509.Certificate\"), http.StatusInternalServerError},\n\t\t\"success\":                    {\"189f573cfa159251e445530847ef80b1b62a3a380ee670dcb49e33ed34da0616\", nil, http.StatusOK},\n\t}\n\n\tfor name, tc := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tcrt, err := a.Root(tc.sum)\n\t\t\tif err != nil {\n\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\tvar sc render.StatusCodedError\n\t\t\t\t\tassert.Fatal(t, errors.As(err, &sc), \"error does not implement StatusCodedError interface\")\n\t\t\t\t\tassert.Equals(t, sc.StatusCode(), tc.code)\n\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.err.Error())\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif assert.Nil(t, tc.err) {\n\t\t\t\t\tassert.Equals(t, crt, a.rootX509Certs[0])\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestAuthority_GetRootCertificate(t *testing.T) {\n\tcert, err := pemutil.ReadCertificate(\"testdata/certs/root_ca.crt\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\ttests := []struct {\n\t\tname string\n\t\twant *x509.Certificate\n\t}{\n\t\t{\"ok\", cert},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ta := testAuthority(t)\n\t\t\tif got := a.GetRootCertificate(); !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"Authority.GetRootCertificate() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestAuthority_GetRootCertificates(t *testing.T) {\n\tcert, err := pemutil.ReadCertificate(\"testdata/certs/root_ca.crt\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\ttests := []struct {\n\t\tname string\n\t\twant []*x509.Certificate\n\t}{\n\t\t{\"ok\", []*x509.Certificate{cert}},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ta := testAuthority(t)\n\t\t\tif got := a.GetRootCertificates(); !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"Authority.GetRootCertificates() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestAuthority_GetRoots(t *testing.T) {\n\tcert, err := pemutil.ReadCertificate(\"testdata/certs/root_ca.crt\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\ttests := []struct {\n\t\tname    string\n\t\twant    []*x509.Certificate\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", []*x509.Certificate{cert}, false},\n\t}\n\tfor _, tt := range tests {\n\t\ta := testAuthority(t)\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := a.GetRoots()\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"Authority.GetRoots() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"Authority.GetRoots() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestAuthority_GetFederation(t *testing.T) {\n\tcert, err := pemutil.ReadCertificate(\"testdata/certs/root_ca.crt\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\ttests := []struct {\n\t\tname           string\n\t\twantFederation []*x509.Certificate\n\t\twantErr        bool\n\t\tfn             func(a *Authority)\n\t}{\n\t\t{\"ok\", []*x509.Certificate{cert}, false, nil},\n\t\t{\"fail\", nil, true, func(a *Authority) {\n\t\t\ta.certificates.Store(\"foo\", \"bar\")\n\t\t}},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ta := testAuthority(t)\n\t\t\tif tt.fn != nil {\n\t\t\t\ttt.fn(a)\n\t\t\t}\n\t\t\tgotFederation, err := a.GetFederation()\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"Authority.GetFederation() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(gotFederation, tt.wantFederation) {\n\t\t\t\tt.Errorf(\"Authority.GetFederation() = %v, want %v\", gotFederation, tt.wantFederation)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestAuthority_GetIntermediateCertificate(t *testing.T) {\n\tca, err := minica.New(minica.WithRootTemplate(`{\n\t\t\"subject\": {{ toJson .Subject }},\n\t\t\"issuer\": {{ toJson .Subject }},\n\t\t\"keyUsage\": [\"certSign\", \"crlSign\"],\n\t\t\"basicConstraints\": {\n\t\t\t\"isCA\": true,\n\t\t\t\"maxPathLen\": -1\n\t\t}\n\t}`), minica.WithIntermediateTemplate(`{\n\t\t\"subject\": {{ toJson .Subject }},\n\t\t\"keyUsage\": [\"certSign\", \"crlSign\"],\n\t\t\"basicConstraints\": {\n\t\t\t\"isCA\": true,\n\t\t\t\"maxPathLen\": 1\n\t\t}\n\t}`))\n\trequire.NoError(t, err)\n\n\tsigner, err := keyutil.GenerateDefaultSigner()\n\trequire.NoError(t, err)\n\n\tcert, err := ca.Sign(&x509.Certificate{\n\t\tSubject:               pkix.Name{CommonName: \"MiniCA Intermediate CA 0\"},\n\t\tPublicKey:             signer.Public(),\n\t\tBasicConstraintsValid: true,\n\t\tIsCA:                  true,\n\t\tMaxPathLen:            0,\n\t})\n\trequire.NoError(t, err)\n\n\ttype fields struct {\n\t\tintermediateX509Certs []*x509.Certificate\n\t}\n\ttests := []struct {\n\t\tname      string\n\t\tfields    fields\n\t\twant      *x509.Certificate\n\t\twantSlice []*x509.Certificate\n\t}{\n\t\t{\"ok one\", fields{[]*x509.Certificate{ca.Intermediate}}, ca.Intermediate, []*x509.Certificate{ca.Intermediate}},\n\t\t{\"ok multiple\", fields{[]*x509.Certificate{cert, ca.Intermediate}}, cert, []*x509.Certificate{cert, ca.Intermediate}},\n\t\t{\"ok empty\", fields{[]*x509.Certificate{}}, nil, []*x509.Certificate{}},\n\t\t{\"ok nil\", fields{nil}, nil, nil},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ta := &Authority{\n\t\t\t\tintermediateX509Certs: tt.fields.intermediateX509Certs,\n\t\t\t}\n\t\t\tif got := a.GetIntermediateCertificate(); !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"Authority.GetIntermediateCertificate() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t\tif got := a.GetIntermediateCertificates(); !reflect.DeepEqual(got, tt.wantSlice) {\n\t\t\t\tt.Errorf(\"Authority.GetIntermediateCertificates() = %v, want %v\", got, tt.wantSlice)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "authority/ssh.go",
    "content": "package authority\n\nimport (\n\t\"context\"\n\t\"crypto/rand\"\n\t\"crypto/x509\"\n\t\"encoding/binary\"\n\t\"errors\"\n\t\"net/http\"\n\t\"strings\"\n\t\"time\"\n\n\t\"golang.org/x/crypto/ssh\"\n\n\t\"go.step.sm/crypto/randutil\"\n\t\"go.step.sm/crypto/sshutil\"\n\n\t\"github.com/smallstep/certificates/authority/config\"\n\t\"github.com/smallstep/certificates/authority/provisioner\"\n\t\"github.com/smallstep/certificates/db\"\n\t\"github.com/smallstep/certificates/errs\"\n\t\"github.com/smallstep/certificates/internal/cast\"\n\t\"github.com/smallstep/certificates/templates\"\n\t\"github.com/smallstep/certificates/webhook\"\n)\n\nconst (\n\t// SSHAddUserPrincipal is the principal that will run the add user command.\n\t// Defaults to \"provisioner\" but it can be changed in the configuration.\n\tSSHAddUserPrincipal = \"provisioner\"\n\n\t// SSHAddUserCommand is the default command to run to add a new user.\n\t// Defaults to \"sudo useradd -m <principal>; nc -q0 localhost 22\" but it can be changed in the\n\t// configuration. The string \"<principal>\" will be replace by the new\n\t// principal to add.\n\tSSHAddUserCommand = \"sudo useradd -m <principal>; nc -q0 localhost 22\"\n)\n\n// GetSSHRoots returns the SSH User and Host public keys.\nfunc (a *Authority) GetSSHRoots(context.Context) (*config.SSHKeys, error) {\n\treturn &config.SSHKeys{\n\t\tHostKeys: a.sshCAHostCerts,\n\t\tUserKeys: a.sshCAUserCerts,\n\t}, nil\n}\n\n// GetSSHFederation returns the public keys for federated SSH signers.\nfunc (a *Authority) GetSSHFederation(context.Context) (*config.SSHKeys, error) {\n\treturn &config.SSHKeys{\n\t\tHostKeys: a.sshCAHostFederatedCerts,\n\t\tUserKeys: a.sshCAUserFederatedCerts,\n\t}, nil\n}\n\n// GetSSHConfig returns rendered templates for clients (user) or servers (host).\nfunc (a *Authority) GetSSHConfig(_ context.Context, typ string, data map[string]string) ([]templates.Output, error) {\n\tif a.sshCAUserCertSignKey == nil && a.sshCAHostCertSignKey == nil {\n\t\treturn nil, errs.NotFound(\"getSSHConfig: ssh is not configured\")\n\t}\n\n\tif a.templates == nil {\n\t\treturn nil, errs.NotFound(\"getSSHConfig: ssh templates are not configured\")\n\t}\n\n\tvar ts []templates.Template\n\tswitch typ {\n\tcase provisioner.SSHUserCert:\n\t\tif a.templates != nil && a.templates.SSH != nil {\n\t\t\tts = a.templates.SSH.User\n\t\t}\n\tcase provisioner.SSHHostCert:\n\t\tif a.templates != nil && a.templates.SSH != nil {\n\t\t\tts = a.templates.SSH.Host\n\t\t}\n\tdefault:\n\t\treturn nil, errs.BadRequest(\"invalid certificate type '%s'\", typ)\n\t}\n\n\t// Merge user and default data\n\tvar mergedData map[string]interface{}\n\n\tif len(data) == 0 {\n\t\tmergedData = a.templates.Data\n\t} else {\n\t\tmergedData = make(map[string]interface{}, len(a.templates.Data)+1)\n\t\tmergedData[\"User\"] = data\n\t\tfor k, v := range a.templates.Data {\n\t\t\tmergedData[k] = v\n\t\t}\n\t}\n\n\t// Render templates\n\toutput := []templates.Output{}\n\tfor _, t := range ts {\n\t\tif err := t.Load(); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\t// Check for required variables.\n\t\tif err := t.ValidateRequiredData(data); err != nil {\n\t\t\treturn nil, errs.BadRequestErr(err, \"%v, please use `--set <key=value>` flag\", err)\n\t\t}\n\n\t\to, err := t.Output(mergedData)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\t// Backwards compatibility for version of the cli older than v0.18.0.\n\t\t// Before v0.18.0 we were not passing any value for SSHTemplateVersionKey\n\t\t// from the cli.\n\t\tif o.Name == \"step_includes.tpl\" && data[templates.SSHTemplateVersionKey] == \"\" {\n\t\t\to.Type = templates.File\n\t\t\to.Path = strings.TrimPrefix(o.Path, \"${STEPPATH}/\")\n\t\t}\n\n\t\toutput = append(output, o)\n\t}\n\treturn output, nil\n}\n\n// GetSSHBastion returns the bastion configuration, for the given pair user,\n// hostname.\nfunc (a *Authority) GetSSHBastion(ctx context.Context, user, hostname string) (*config.Bastion, error) {\n\tif a.sshBastionFunc != nil {\n\t\tbs, err := a.sshBastionFunc(ctx, user, hostname)\n\t\treturn bs, errs.Wrap(http.StatusInternalServerError, err, \"authority.GetSSHBastion\")\n\t}\n\tif a.config.SSH != nil {\n\t\tif a.config.SSH.Bastion != nil && a.config.SSH.Bastion.Hostname != \"\" {\n\t\t\t// Do not return a bastion for a bastion host.\n\t\t\t//\n\t\t\t// This condition might fail if a different name or IP is used.\n\t\t\t// Trying to resolve hostnames to IPs and compare them won't be a\n\t\t\t// complete solution because it depends on the network\n\t\t\t// configuration, of the CA and clients and can also return false\n\t\t\t// positives. Although not perfect, this simple solution will work\n\t\t\t// in most cases.\n\t\t\tif !strings.EqualFold(hostname, a.config.SSH.Bastion.Hostname) {\n\t\t\t\treturn a.config.SSH.Bastion, nil\n\t\t\t}\n\t\t}\n\t\t//nolint:nilnil // legacy\n\t\treturn nil, nil\n\t}\n\treturn nil, errs.NotFound(\"authority.GetSSHBastion; ssh is not configured\")\n}\n\n// SignSSH creates a signed SSH certificate with the given public key and options.\nfunc (a *Authority) SignSSH(ctx context.Context, key ssh.PublicKey, opts provisioner.SignSSHOptions, signOpts ...provisioner.SignOption) (*ssh.Certificate, error) {\n\tcert, prov, err := a.signSSH(ctx, key, opts, signOpts...)\n\ta.meter.SSHSigned(cert, prov, err)\n\treturn cert, err\n}\n\nfunc (a *Authority) signSSH(ctx context.Context, key ssh.PublicKey, opts provisioner.SignSSHOptions, signOpts ...provisioner.SignOption) (*ssh.Certificate, provisioner.Interface, error) {\n\tvar (\n\t\tcertOptions   []sshutil.Option\n\t\tmods          []provisioner.SSHCertModifier\n\t\tvalidators    []provisioner.SSHCertValidator\n\t\tkeyValidators []provisioner.SSHPublicKeyValidator\n\t)\n\n\t// Validate given key and options\n\tif key == nil {\n\t\treturn nil, nil, errs.BadRequest(\"ssh public key cannot be nil\")\n\t}\n\tif err := opts.Validate(); err != nil {\n\t\treturn nil, nil, err\n\t}\n\n\t// Set backdate with the configured value\n\topts.Backdate = a.config.AuthorityConfig.Backdate.Duration\n\n\tvar prov provisioner.Interface\n\tvar webhookCtl webhookController\n\tfor _, op := range signOpts {\n\t\tswitch o := op.(type) {\n\t\t// Capture current provisioner\n\t\tcase provisioner.Interface:\n\t\t\tprov = o\n\n\t\t// add options to NewCertificate\n\t\tcase provisioner.SSHCertificateOptions:\n\t\t\tcertOptions = append(certOptions, o.Options(opts)...)\n\n\t\t// modify the ssh.Certificate\n\t\tcase provisioner.SSHCertModifier:\n\t\t\tmods = append(mods, o)\n\n\t\t// validate the ssh public key\n\t\tcase provisioner.SSHPublicKeyValidator:\n\t\t\tkeyValidators = append(keyValidators, o)\n\n\t\t// validate the ssh.Certificate\n\t\tcase provisioner.SSHCertValidator:\n\t\t\tvalidators = append(validators, o)\n\n\t\t// validate the given SSHOptions\n\t\tcase provisioner.SSHCertOptionsValidator:\n\t\t\tif err := o.Valid(opts); err != nil {\n\t\t\t\treturn nil, prov, errs.BadRequestErr(err, \"error validating ssh certificate options\")\n\t\t\t}\n\n\t\t// call webhooks\n\t\tcase webhookController:\n\t\t\twebhookCtl = o\n\n\t\tdefault:\n\t\t\treturn nil, prov, errs.InternalServer(\"authority.SignSSH: invalid extra option type %T\", o)\n\t\t}\n\t}\n\n\t// Validate public key\n\tfor _, v := range keyValidators {\n\t\tif err := v.Valid(key); err != nil {\n\t\t\treturn nil, nil, errs.ApplyOptions(\n\t\t\t\terrs.ForbiddenErr(err, \"%s\", err.Error()),\n\t\t\t\terrs.WithKeyVal(\"signOptions\", signOpts),\n\t\t\t)\n\t\t}\n\t}\n\n\t// Simulated certificate request with request options.\n\tcr := sshutil.CertificateRequest{\n\t\tType:       opts.CertType,\n\t\tKeyID:      opts.KeyID,\n\t\tPrincipals: opts.Principals,\n\t\tKey:        key,\n\t}\n\n\t// Call enriching webhooks\n\tif err := a.callEnrichingWebhooksSSH(ctx, prov, webhookCtl, cr); err != nil {\n\t\treturn nil, prov, errs.ApplyOptions(\n\t\t\terrs.ForbiddenErr(err, \"%s\", err.Error()),\n\t\t\terrs.WithKeyVal(\"signOptions\", signOpts),\n\t\t)\n\t}\n\n\t// Create certificate from template.\n\tcertificate, err := sshutil.NewCertificate(cr, certOptions...)\n\tif err != nil {\n\t\tvar te *sshutil.TemplateError\n\t\tswitch {\n\t\tcase errors.As(err, &te):\n\t\t\treturn nil, prov, errs.ApplyOptions(\n\t\t\t\terrs.BadRequestErr(err, \"%s\", err.Error()),\n\t\t\t\terrs.WithKeyVal(\"signOptions\", signOpts),\n\t\t\t)\n\t\tcase strings.HasPrefix(err.Error(), \"error unmarshaling certificate\"):\n\t\t\t// explicitly check for unmarshaling errors, which are most probably caused by JSON template syntax errors\n\t\t\treturn nil, prov, errs.InternalServerErr(templatingError(err),\n\t\t\t\terrs.WithKeyVal(\"signOptions\", signOpts),\n\t\t\t\terrs.WithMessage(\"error applying certificate template\"),\n\t\t\t)\n\t\tdefault:\n\t\t\treturn nil, prov, errs.Wrap(http.StatusInternalServerError, err, \"authority.SignSSH\")\n\t\t}\n\t}\n\n\t// Get actual *ssh.Certificate and continue with provisioner modifiers.\n\tcertTpl := certificate.GetCertificate()\n\n\t// Use SignSSHOptions to modify the certificate validity. It will be later\n\t// checked or set if not defined.\n\tif err := opts.ModifyValidity(certTpl); err != nil {\n\t\treturn nil, prov, errs.BadRequestErr(err, \"%s\", err.Error())\n\t}\n\n\t// Use provisioner modifiers.\n\tfor _, m := range mods {\n\t\tif err := m.Modify(certTpl, opts); err != nil {\n\t\t\treturn nil, prov, errs.ForbiddenErr(err, \"error creating ssh certificate\")\n\t\t}\n\t}\n\n\t// Get signer from authority keys\n\tvar signer ssh.Signer\n\tswitch certTpl.CertType {\n\tcase ssh.UserCert:\n\t\tif a.sshCAUserCertSignKey == nil {\n\t\t\treturn nil, prov, errs.NotImplemented(\"authority.SignSSH: user certificate signing is not enabled\")\n\t\t}\n\t\tsigner = a.sshCAUserCertSignKey\n\tcase ssh.HostCert:\n\t\tif a.sshCAHostCertSignKey == nil {\n\t\t\treturn nil, prov, errs.NotImplemented(\"authority.SignSSH: host certificate signing is not enabled\")\n\t\t}\n\t\tsigner = a.sshCAHostCertSignKey\n\tdefault:\n\t\treturn nil, prov, errs.InternalServer(\"authority.SignSSH: unexpected ssh certificate type: %d\", certTpl.CertType)\n\t}\n\n\t// Check if authority is allowed to sign the certificate\n\tif err := a.isAllowedToSignSSHCertificate(certTpl); err != nil {\n\t\tvar ee *errs.Error\n\t\tif errors.As(err, &ee) {\n\t\t\treturn nil, prov, ee\n\t\t}\n\t\treturn nil, prov, errs.InternalServerErr(err,\n\t\t\terrs.WithMessage(\"authority.SignSSH: error creating ssh certificate\"),\n\t\t)\n\t}\n\n\t// Send certificate to webhooks for authorization\n\tif err := a.callAuthorizingWebhooksSSH(ctx, prov, webhookCtl, certificate, certTpl); err != nil {\n\t\treturn nil, prov, errs.ApplyOptions(\n\t\t\terrs.ForbiddenErr(err, \"authority.SignSSH: error signing certificate\"),\n\t\t)\n\t}\n\n\t// Sign certificate.\n\tcert, err := sshutil.CreateCertificate(certTpl, signer)\n\tif err != nil {\n\t\treturn nil, prov, errs.Wrap(http.StatusInternalServerError, err, \"authority.SignSSH: error signing certificate\")\n\t}\n\n\t// User provisioners validators.\n\tfor _, v := range validators {\n\t\tif err := v.Valid(cert, opts); err != nil {\n\t\t\treturn nil, prov, errs.ForbiddenErr(err, \"error validating ssh certificate\")\n\t\t}\n\t}\n\n\tif err := a.storeSSHCertificate(prov, cert); err != nil && !errors.Is(err, db.ErrNotImplemented) {\n\t\treturn nil, prov, errs.Wrap(http.StatusInternalServerError, err, \"authority.SignSSH: error storing certificate in db\")\n\t}\n\n\treturn cert, prov, nil\n}\n\n// isAllowedToSignSSHCertificate checks if the Authority is allowed to sign the SSH certificate.\nfunc (a *Authority) isAllowedToSignSSHCertificate(cert *ssh.Certificate) error {\n\treturn a.policyEngine.IsSSHCertificateAllowed(cert)\n}\n\n// RenewSSH creates a signed SSH certificate using the old SSH certificate as a template.\nfunc (a *Authority) RenewSSH(ctx context.Context, oldCert *ssh.Certificate) (*ssh.Certificate, error) {\n\tcert, prov, err := a.renewSSH(ctx, oldCert)\n\ta.meter.SSHRenewed(cert, prov, err)\n\treturn cert, err\n}\n\nfunc (a *Authority) renewSSH(ctx context.Context, oldCert *ssh.Certificate) (*ssh.Certificate, provisioner.Interface, error) {\n\tif oldCert.ValidAfter == 0 || oldCert.ValidBefore == 0 {\n\t\treturn nil, nil, errs.BadRequest(\"cannot renew a certificate without validity period\")\n\t}\n\n\tif err := a.authorizeSSHCertificate(ctx, oldCert); err != nil {\n\t\treturn nil, nil, err\n\t}\n\n\t// Attempt to extract the provisioner from the token.\n\tvar prov provisioner.Interface\n\tif token, ok := provisioner.TokenFromContext(ctx); ok {\n\t\tprov, _, _ = a.getProvisionerFromToken(token)\n\t}\n\n\tbackdate := a.config.AuthorityConfig.Backdate.Duration\n\tduration := time.Duration(cast.Int64(oldCert.ValidBefore-oldCert.ValidAfter)) * time.Second\n\tnow := time.Now()\n\tva := now.Add(-1 * backdate)\n\tvb := now.Add(duration - backdate)\n\n\t// Build base certificate with the old key.\n\t// Nonce and serial will be automatically generated on signing.\n\tcertTpl := &ssh.Certificate{\n\t\tKey:             oldCert.Key,\n\t\tCertType:        oldCert.CertType,\n\t\tKeyId:           oldCert.KeyId,\n\t\tValidPrincipals: oldCert.ValidPrincipals,\n\t\tPermissions:     oldCert.Permissions,\n\t\tReserved:        oldCert.Reserved,\n\t\tValidAfter:      cast.Uint64(va.Unix()),\n\t\tValidBefore:     cast.Uint64(vb.Unix()),\n\t}\n\n\t// Get signer from authority keys\n\tvar signer ssh.Signer\n\tswitch certTpl.CertType {\n\tcase ssh.UserCert:\n\t\tif a.sshCAUserCertSignKey == nil {\n\t\t\treturn nil, prov, errs.NotImplemented(\"renewSSH: user certificate signing is not enabled\")\n\t\t}\n\t\tsigner = a.sshCAUserCertSignKey\n\tcase ssh.HostCert:\n\t\tif a.sshCAHostCertSignKey == nil {\n\t\t\treturn nil, prov, errs.NotImplemented(\"renewSSH: host certificate signing is not enabled\")\n\t\t}\n\t\tsigner = a.sshCAHostCertSignKey\n\tdefault:\n\t\treturn nil, prov, errs.InternalServer(\"renewSSH: unexpected ssh certificate type: %d\", certTpl.CertType)\n\t}\n\n\t// Sign certificate.\n\tcert, err := sshutil.CreateCertificate(certTpl, signer)\n\tif err != nil {\n\t\treturn nil, prov, errs.Wrap(http.StatusInternalServerError, err, \"signSSH: error signing certificate\")\n\t}\n\n\tif err := a.storeRenewedSSHCertificate(prov, oldCert, cert); err != nil && !errors.Is(err, db.ErrNotImplemented) {\n\t\treturn nil, prov, errs.Wrap(http.StatusInternalServerError, err, \"renewSSH: error storing certificate in db\")\n\t}\n\n\treturn cert, prov, nil\n}\n\n// RekeySSH creates a signed SSH certificate using the old SSH certificate as a template.\nfunc (a *Authority) RekeySSH(ctx context.Context, oldCert *ssh.Certificate, pub ssh.PublicKey, signOpts ...provisioner.SignOption) (*ssh.Certificate, error) {\n\tcert, prov, err := a.rekeySSH(ctx, oldCert, pub, signOpts...)\n\ta.meter.SSHRekeyed(cert, prov, err)\n\treturn cert, err\n}\n\nfunc (a *Authority) rekeySSH(ctx context.Context, oldCert *ssh.Certificate, pub ssh.PublicKey, signOpts ...provisioner.SignOption) (*ssh.Certificate, provisioner.Interface, error) {\n\tvar prov provisioner.Interface\n\tvar validators []provisioner.SSHCertValidator\n\tfor _, op := range signOpts {\n\t\tswitch o := op.(type) {\n\t\t// Capture current provisioner\n\t\tcase provisioner.Interface:\n\t\t\tprov = o\n\t\t// validate the ssh.Certificate\n\t\tcase provisioner.SSHCertValidator:\n\t\t\tvalidators = append(validators, o)\n\t\tdefault:\n\t\t\treturn nil, prov, errs.InternalServer(\"rekeySSH; invalid extra option type %T\", o)\n\t\t}\n\t}\n\n\tif oldCert.ValidAfter == 0 || oldCert.ValidBefore == 0 {\n\t\treturn nil, prov, errs.BadRequest(\"cannot rekey a certificate without validity period\")\n\t}\n\n\tif err := a.authorizeSSHCertificate(ctx, oldCert); err != nil {\n\t\treturn nil, prov, err\n\t}\n\n\tbackdate := a.config.AuthorityConfig.Backdate.Duration\n\tduration := time.Duration(cast.Int64(oldCert.ValidBefore-oldCert.ValidAfter)) * time.Second\n\tnow := time.Now()\n\tva := now.Add(-1 * backdate)\n\tvb := now.Add(duration - backdate)\n\n\t// Build base certificate with the new key.\n\t// Nonce and serial will be automatically generated on signing.\n\tcert := &ssh.Certificate{\n\t\tKey:             pub,\n\t\tCertType:        oldCert.CertType,\n\t\tKeyId:           oldCert.KeyId,\n\t\tValidPrincipals: oldCert.ValidPrincipals,\n\t\tPermissions:     oldCert.Permissions,\n\t\tReserved:        oldCert.Reserved,\n\t\tValidAfter:      cast.Uint64(va.Unix()),\n\t\tValidBefore:     cast.Uint64(vb.Unix()),\n\t}\n\n\t// Get signer from authority keys\n\tvar signer ssh.Signer\n\tswitch cert.CertType {\n\tcase ssh.UserCert:\n\t\tif a.sshCAUserCertSignKey == nil {\n\t\t\treturn nil, prov, errs.NotImplemented(\"rekeySSH; user certificate signing is not enabled\")\n\t\t}\n\t\tsigner = a.sshCAUserCertSignKey\n\tcase ssh.HostCert:\n\t\tif a.sshCAHostCertSignKey == nil {\n\t\t\treturn nil, prov, errs.NotImplemented(\"rekeySSH; host certificate signing is not enabled\")\n\t\t}\n\t\tsigner = a.sshCAHostCertSignKey\n\tdefault:\n\t\treturn nil, prov, errs.BadRequest(\"unexpected certificate type '%d'\", cert.CertType)\n\t}\n\n\tvar err error\n\t// Sign certificate.\n\tcert, err = sshutil.CreateCertificate(cert, signer)\n\tif err != nil {\n\t\treturn nil, prov, errs.Wrap(http.StatusInternalServerError, err, \"signSSH: error signing certificate\")\n\t}\n\n\t// Apply validators from provisioner.\n\tfor _, v := range validators {\n\t\tif err := v.Valid(cert, provisioner.SignSSHOptions{Backdate: backdate}); err != nil {\n\t\t\treturn nil, prov, errs.ForbiddenErr(err, \"error validating ssh certificate\")\n\t\t}\n\t}\n\n\tif err := a.storeRenewedSSHCertificate(prov, oldCert, cert); err != nil && !errors.Is(err, db.ErrNotImplemented) {\n\t\treturn nil, prov, errs.Wrap(http.StatusInternalServerError, err, \"rekeySSH; error storing certificate in db\")\n\t}\n\n\treturn cert, prov, nil\n}\n\nfunc (a *Authority) storeSSHCertificate(prov provisioner.Interface, cert *ssh.Certificate) error {\n\ttype sshCertificateStorer interface {\n\t\tStoreSSHCertificate(provisioner.Interface, *ssh.Certificate) error\n\t}\n\n\t// Store certificate in admindb or linkedca\n\tswitch s := a.adminDB.(type) {\n\tcase sshCertificateStorer:\n\t\treturn s.StoreSSHCertificate(prov, cert)\n\tcase db.CertificateStorer:\n\t\treturn s.StoreSSHCertificate(cert)\n\t}\n\n\t// Store certificate in localdb\n\tswitch s := a.db.(type) {\n\tcase sshCertificateStorer:\n\t\treturn s.StoreSSHCertificate(prov, cert)\n\tcase db.CertificateStorer:\n\t\treturn s.StoreSSHCertificate(cert)\n\tdefault:\n\t\treturn nil\n\t}\n}\n\nfunc (a *Authority) storeRenewedSSHCertificate(prov provisioner.Interface, parent, cert *ssh.Certificate) error {\n\ttype sshRenewerCertificateStorer interface {\n\t\tStoreRenewedSSHCertificate(p provisioner.Interface, parent, cert *ssh.Certificate) error\n\t}\n\n\t// Store certificate in admindb or linkedca\n\tswitch s := a.adminDB.(type) {\n\tcase sshRenewerCertificateStorer:\n\t\treturn s.StoreRenewedSSHCertificate(prov, parent, cert)\n\tcase db.CertificateStorer:\n\t\treturn s.StoreSSHCertificate(cert)\n\t}\n\n\t// Store certificate in localdb\n\tswitch s := a.db.(type) {\n\tcase sshRenewerCertificateStorer:\n\t\treturn s.StoreRenewedSSHCertificate(prov, parent, cert)\n\tcase db.CertificateStorer:\n\t\treturn s.StoreSSHCertificate(cert)\n\tdefault:\n\t\treturn nil\n\t}\n}\n\n// IsValidForAddUser checks if a user provisioner certificate can be issued to\n// the given certificate.\nfunc IsValidForAddUser(cert *ssh.Certificate) error {\n\tif cert.CertType != ssh.UserCert {\n\t\treturn errs.Forbidden(\"certificate is not a user certificate\")\n\t}\n\n\tswitch len(cert.ValidPrincipals) {\n\tcase 0:\n\t\treturn errs.Forbidden(\"certificate does not have any principals\")\n\tcase 1:\n\t\treturn nil\n\tcase 2:\n\t\t// OIDC provisioners adds a second principal with the email address.\n\t\t// @ cannot be the first character.\n\t\tif strings.Index(cert.ValidPrincipals[1], \"@\") > 0 {\n\t\t\treturn nil\n\t\t}\n\t\treturn errs.Forbidden(\"certificate does not have only one principal\")\n\tdefault:\n\t\treturn errs.Forbidden(\"certificate does not have only one principal\")\n\t}\n}\n\n// SignSSHAddUser signs a certificate that provisions a new user in a server.\nfunc (a *Authority) SignSSHAddUser(ctx context.Context, key ssh.PublicKey, subject *ssh.Certificate) (*ssh.Certificate, error) {\n\tif a.sshCAUserCertSignKey == nil {\n\t\treturn nil, errs.NotImplemented(\"signSSHAddUser: user certificate signing is not enabled\")\n\t}\n\tif err := IsValidForAddUser(subject); err != nil {\n\t\treturn nil, err\n\t}\n\n\tnonce, err := randutil.ASCII(32)\n\tif err != nil {\n\t\treturn nil, errs.Wrap(http.StatusInternalServerError, err, \"signSSHAddUser\")\n\t}\n\n\tvar serial uint64\n\tif err := binary.Read(rand.Reader, binary.BigEndian, &serial); err != nil {\n\t\treturn nil, errs.Wrap(http.StatusInternalServerError, err, \"signSSHAddUser: error reading random number\")\n\t}\n\n\t// Attempt to extract the provisioner from the token.\n\tvar prov provisioner.Interface\n\tif token, ok := provisioner.TokenFromContext(ctx); ok {\n\t\tprov, _, _ = a.getProvisionerFromToken(token)\n\t}\n\n\tsigner := a.sshCAUserCertSignKey\n\tprincipal := subject.ValidPrincipals[0]\n\taddUserPrincipal := a.getAddUserPrincipal()\n\n\tcert := &ssh.Certificate{\n\t\tNonce:           []byte(nonce),\n\t\tKey:             key,\n\t\tSerial:          serial,\n\t\tCertType:        ssh.UserCert,\n\t\tKeyId:           principal + \"-\" + addUserPrincipal,\n\t\tValidPrincipals: []string{addUserPrincipal},\n\t\tValidAfter:      subject.ValidAfter,\n\t\tValidBefore:     subject.ValidBefore,\n\t\tPermissions: ssh.Permissions{\n\t\t\tCriticalOptions: map[string]string{\n\t\t\t\t\"force-command\": a.getAddUserCommand(principal),\n\t\t\t},\n\t\t},\n\t\tSignatureKey: signer.PublicKey(),\n\t}\n\n\t// Get bytes for signing trailing the signature length.\n\tdata := cert.Marshal()\n\tdata = data[:len(data)-4]\n\n\t// Sign the certificate\n\tsig, err := signer.Sign(rand.Reader, data)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tcert.Signature = sig\n\n\tif err = a.storeRenewedSSHCertificate(prov, subject, cert); err != nil && !errors.Is(err, db.ErrNotImplemented) {\n\t\treturn nil, errs.Wrap(http.StatusInternalServerError, err, \"signSSHAddUser: error storing certificate in db\")\n\t}\n\n\treturn cert, nil\n}\n\n// CheckSSHHost checks the given principal has been registered before.\nfunc (a *Authority) CheckSSHHost(ctx context.Context, principal, token string) (bool, error) {\n\tif a.sshCheckHostFunc != nil {\n\t\texists, err := a.sshCheckHostFunc(ctx, principal, token, a.GetRootCertificates())\n\t\tif err != nil {\n\t\t\treturn false, errs.Wrap(http.StatusInternalServerError, err,\n\t\t\t\t\"checkSSHHost: error from injected checkSSHHost func\")\n\t\t}\n\t\treturn exists, nil\n\t}\n\texists, err := a.db.IsSSHHost(principal)\n\tif err != nil {\n\t\tif errors.Is(err, db.ErrNotImplemented) {\n\t\t\treturn false, errs.Wrap(http.StatusNotImplemented, err,\n\t\t\t\t\"checkSSHHost: isSSHHost is not implemented\")\n\t\t}\n\t\treturn false, errs.Wrap(http.StatusInternalServerError, err,\n\t\t\t\"checkSSHHost: error checking if hosts exists\")\n\t}\n\n\treturn exists, nil\n}\n\n// GetSSHHosts returns a list of valid host principals.\nfunc (a *Authority) GetSSHHosts(ctx context.Context, cert *x509.Certificate) ([]config.Host, error) {\n\tif a.GetConfig().AuthorityConfig.DisableGetSSHHosts {\n\t\treturn nil, errs.New(http.StatusNotFound, \"ssh hosts list api disabled\")\n\t}\n\tif a.sshGetHostsFunc != nil {\n\t\thosts, err := a.sshGetHostsFunc(ctx, cert)\n\t\treturn hosts, errs.Wrap(http.StatusInternalServerError, err, \"getSSHHosts\")\n\t}\n\thostnames, err := a.db.GetSSHHostPrincipals()\n\tif err != nil {\n\t\treturn nil, errs.Wrap(http.StatusInternalServerError, err, \"getSSHHosts\")\n\t}\n\n\thosts := make([]config.Host, len(hostnames))\n\tfor i, hn := range hostnames {\n\t\thosts[i] = config.Host{Hostname: hn}\n\t}\n\treturn hosts, nil\n}\n\nfunc (a *Authority) getAddUserPrincipal() (cmd string) {\n\tif a.config.SSH.AddUserPrincipal == \"\" {\n\t\treturn SSHAddUserPrincipal\n\t}\n\treturn a.config.SSH.AddUserPrincipal\n}\n\nfunc (a *Authority) getAddUserCommand(principal string) string {\n\tvar cmd string\n\tif a.config.SSH.AddUserCommand == \"\" {\n\t\tcmd = SSHAddUserCommand\n\t} else {\n\t\tcmd = a.config.SSH.AddUserCommand\n\t}\n\treturn strings.ReplaceAll(cmd, \"<principal>\", principal)\n}\n\nfunc (a *Authority) callEnrichingWebhooksSSH(ctx context.Context, prov provisioner.Interface, webhookCtl webhookController, cr sshutil.CertificateRequest) (err error) {\n\tif webhookCtl == nil {\n\t\treturn\n\t}\n\tdefer func() { a.meter.SSHWebhookEnriched(prov, err) }()\n\n\tvar whEnrichReq *webhook.RequestBody\n\tif whEnrichReq, err = webhook.NewRequestBody(\n\t\twebhook.WithSSHCertificateRequest(cr),\n\t); err == nil {\n\t\terr = webhookCtl.Enrich(ctx, whEnrichReq)\n\t}\n\n\treturn\n}\n\nfunc (a *Authority) callAuthorizingWebhooksSSH(ctx context.Context, prov provisioner.Interface, webhookCtl webhookController, cert *sshutil.Certificate, certTpl *ssh.Certificate) (err error) {\n\tif webhookCtl == nil {\n\t\treturn\n\t}\n\tdefer func() { a.meter.SSHWebhookAuthorized(prov, err) }()\n\n\tvar whAuthBody *webhook.RequestBody\n\tif whAuthBody, err = webhook.NewRequestBody(\n\t\twebhook.WithSSHCertificate(cert, certTpl),\n\t); err == nil {\n\t\terr = webhookCtl.Authorize(ctx, whAuthBody)\n\t}\n\n\treturn\n}\n"
  },
  {
    "path": "authority/ssh_test.go",
    "content": "package authority\n\nimport (\n\t\"context\"\n\t\"crypto/ecdsa\"\n\t\"crypto/elliptic\"\n\t\"crypto/rand\"\n\t\"crypto/x509\"\n\t\"encoding/base64\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"reflect\"\n\t\"testing\"\n\t\"time\"\n\n\t\"go.step.sm/crypto/jose\"\n\t\"go.step.sm/crypto/sshutil\"\n\t\"golang.org/x/crypto/ssh\"\n\n\t\"github.com/smallstep/assert\"\n\t\"github.com/smallstep/certificates/api/render\"\n\t\"github.com/smallstep/certificates/authority/policy\"\n\t\"github.com/smallstep/certificates/authority/provisioner\"\n\t\"github.com/smallstep/certificates/db\"\n\t\"github.com/smallstep/certificates/templates\"\n)\n\ntype sshTestModifier ssh.Certificate\n\nfunc (m sshTestModifier) Modify(cert *ssh.Certificate, _ provisioner.SignSSHOptions) error {\n\tif m.CertType != 0 {\n\t\tcert.CertType = m.CertType\n\t}\n\tif m.KeyId != \"\" {\n\t\tcert.KeyId = m.KeyId\n\t}\n\tif m.ValidAfter != 0 {\n\t\tcert.ValidAfter = m.ValidAfter\n\t}\n\tif m.ValidBefore != 0 {\n\t\tcert.ValidBefore = m.ValidBefore\n\t}\n\tif len(m.ValidPrincipals) != 0 {\n\t\tcert.ValidPrincipals = m.ValidPrincipals\n\t}\n\tif m.Permissions.CriticalOptions != nil {\n\t\tcert.Permissions.CriticalOptions = m.Permissions.CriticalOptions\n\t}\n\tif m.Permissions.Extensions != nil {\n\t\tcert.Permissions.Extensions = m.Permissions.Extensions\n\t}\n\treturn nil\n}\n\ntype sshTestCertModifier string\n\nfunc (m sshTestCertModifier) Modify(*ssh.Certificate, provisioner.SignSSHOptions) error {\n\tif m == \"\" {\n\t\treturn nil\n\t}\n\treturn errors.New(string(m))\n}\n\ntype sshTestCertValidator string\n\nfunc (v sshTestCertValidator) Valid(*ssh.Certificate, provisioner.SignSSHOptions) error {\n\tif v == \"\" {\n\t\treturn nil\n\t}\n\treturn errors.New(string(v))\n}\n\ntype sshTestOptionsValidator string\n\nfunc (v sshTestOptionsValidator) Valid(provisioner.SignSSHOptions) error {\n\tif v == \"\" {\n\t\treturn nil\n\t}\n\treturn errors.New(string(v))\n}\n\ntype sshTestOptionsModifier string\n\nfunc (m sshTestOptionsModifier) Modify(*ssh.Certificate, provisioner.SignSSHOptions) error {\n\tif m == \"\" {\n\t\treturn nil\n\t}\n\treturn errors.New(string(m))\n}\n\nfunc TestAuthority_initHostOnly(t *testing.T) {\n\tauth := testAuthority(t, func(a *Authority) error {\n\t\ta.config.SSH.UserKey = \"\"\n\t\treturn nil\n\t})\n\n\t// Check keys\n\tkeys, err := auth.GetSSHRoots(context.Background())\n\tassert.NoError(t, err)\n\tassert.Len(t, 1, keys.HostKeys)\n\tassert.Len(t, 0, keys.UserKeys)\n\n\t// Check templates, user templates should work fine.\n\t_, err = auth.GetSSHConfig(context.Background(), \"user\", nil)\n\tassert.NoError(t, err)\n\n\t_, err = auth.GetSSHConfig(context.Background(), \"host\", map[string]string{\n\t\t\"Certificate\": \"ssh_host_ecdsa_key-cert.pub\",\n\t\t\"Key\":         \"ssh_host_ecdsa_key\",\n\t})\n\tassert.Error(t, err)\n}\n\nfunc TestAuthority_initUserOnly(t *testing.T) {\n\tauth := testAuthority(t, func(a *Authority) error {\n\t\ta.config.SSH.HostKey = \"\"\n\t\treturn nil\n\t})\n\n\t// Check keys\n\tkeys, err := auth.GetSSHRoots(context.Background())\n\tassert.NoError(t, err)\n\tassert.Len(t, 0, keys.HostKeys)\n\tassert.Len(t, 1, keys.UserKeys)\n\n\t// Check templates, host templates should work fine.\n\t_, err = auth.GetSSHConfig(context.Background(), \"host\", map[string]string{\n\t\t\"Certificate\": \"ssh_host_ecdsa_key-cert.pub\",\n\t\t\"Key\":         \"ssh_host_ecdsa_key\",\n\t})\n\tassert.NoError(t, err)\n\n\t_, err = auth.GetSSHConfig(context.Background(), \"user\", nil)\n\tassert.Error(t, err)\n}\n\nfunc TestAuthority_SignSSH(t *testing.T) {\n\tkey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)\n\tassert.FatalError(t, err)\n\tpub, err := ssh.NewPublicKey(key.Public())\n\tassert.FatalError(t, err)\n\tsignKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)\n\tassert.FatalError(t, err)\n\tsigner, err := ssh.NewSignerFromKey(signKey)\n\tassert.FatalError(t, err)\n\n\tuserOptions := sshTestModifier{\n\t\tCertType: ssh.UserCert,\n\t}\n\thostOptions := sshTestModifier{\n\t\tCertType: ssh.HostCert,\n\t}\n\n\tuserTemplate, err := provisioner.TemplateSSHOptions(nil, sshutil.CreateTemplateData(sshutil.UserCert, \"key-id\", nil))\n\tassert.FatalError(t, err)\n\thostTemplate, err := provisioner.TemplateSSHOptions(nil, sshutil.CreateTemplateData(sshutil.HostCert, \"key-id\", nil))\n\tassert.FatalError(t, err)\n\tuserTemplateWithUser, err := provisioner.TemplateSSHOptions(nil, sshutil.CreateTemplateData(sshutil.UserCert, \"key-id\", []string{\"user\"}))\n\tassert.FatalError(t, err)\n\thostTemplateWithHosts, err := provisioner.TemplateSSHOptions(nil, sshutil.CreateTemplateData(sshutil.HostCert, \"key-id\", []string{\"foo.test.com\", \"bar.test.com\"}))\n\tassert.FatalError(t, err)\n\tuserTemplateWithRoot, err := provisioner.TemplateSSHOptions(nil, sshutil.CreateTemplateData(sshutil.UserCert, \"key-id\", []string{\"root\"}))\n\tassert.FatalError(t, err)\n\thostTemplateWithExampleDotCom, err := provisioner.TemplateSSHOptions(nil, sshutil.CreateTemplateData(sshutil.HostCert, \"key-id\", []string{\"example.com\"}))\n\tassert.FatalError(t, err)\n\tbadUserTemplate, err := provisioner.TemplateSSHOptions(nil, sshutil.CreateTemplateData(sshutil.UserCert, \"key-id\", []string{\"127.0.0.1\"}))\n\tassert.FatalError(t, err)\n\tbadHostTemplate, err := provisioner.TemplateSSHOptions(nil, sshutil.CreateTemplateData(sshutil.HostCert, \"key-id\", []string{\"host...local\"}))\n\tassert.FatalError(t, err)\n\tuserCustomTemplate, err := provisioner.TemplateSSHOptions(&provisioner.Options{\n\t\tSSH: &provisioner.SSHOptions{Template: `{\n\t\t\t\"type\": \"{{ .Type }}\",\n\t\t\t\"keyId\": \"{{ .KeyID }}\",\n\t\t\t\"principals\": {{ append .Principals \"admin\" | toJson }},\n\t\t\t\"extensions\": {{ set .Extensions \"login@github.com\" .Insecure.User.username | toJson }},\n\t\t\t\"criticalOptions\": {{ toJson .CriticalOptions }}\n\t\t}`},\n\t}, sshutil.CreateTemplateData(sshutil.UserCert, \"key-id\", []string{\"user\"}))\n\tassert.FatalError(t, err)\n\tenrichTemplateData := sshutil.CreateTemplateData(sshutil.UserCert, \"key-id\", []string{\"user\"})\n\tenrichTemplate, err := provisioner.TemplateSSHOptions(&provisioner.Options{\n\t\tSSH: &provisioner.SSHOptions{Template: `{\n\t\t\t\"type\": \"{{ .Type }}\",\n\t\t\t\"keyId\": \"{{ .KeyID }}\",\n\t\t\t\"principals\": {{ toJson .Webhooks.people.role }},\n\t\t\t\"extensions\": {{ set .Extensions \"login@github.com\" .Insecure.User.username | toJson }},\n\t\t\t\"criticalOptions\": {{ toJson .CriticalOptions }}\n\t\t}`},\n\t}, enrichTemplateData)\n\tassert.FatalError(t, err)\n\tuserFailTemplate, err := provisioner.TemplateSSHOptions(&provisioner.Options{\n\t\tSSH: &provisioner.SSHOptions{Template: `{{ fail \"an error\"}}`},\n\t}, sshutil.CreateTemplateData(sshutil.UserCert, \"key-id\", []string{\"user\"}))\n\tassert.FatalError(t, err)\n\tuserJSONSyntaxErrorTemplateFile, err := provisioner.TemplateSSHOptions(&provisioner.Options{\n\t\tSSH: &provisioner.SSHOptions{TemplateFile: \"./testdata/templates/badjsonsyntax.tpl\"},\n\t}, sshutil.CreateTemplateData(sshutil.UserCert, \"key-id\", []string{\"user\"}))\n\tassert.FatalError(t, err)\n\tuserJSONValueErrorTemplateFile, err := provisioner.TemplateSSHOptions(&provisioner.Options{\n\t\tSSH: &provisioner.SSHOptions{TemplateFile: \"./testdata/templates/badjsonvalue.tpl\"},\n\t}, sshutil.CreateTemplateData(sshutil.UserCert, \"key-id\", []string{\"user\"}))\n\tassert.FatalError(t, err)\n\n\tuserPolicyOptions := &policy.Options{\n\t\tSSH: &policy.SSHPolicyOptions{\n\t\t\tUser: &policy.SSHUserCertificateOptions{\n\t\t\t\tAllowedNames: &policy.SSHNameOptions{\n\t\t\t\t\tPrincipals: []string{\"user\"},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\tuserPolicy, err := policy.New(userPolicyOptions)\n\tassert.FatalError(t, err)\n\n\thostPolicyOptions := &policy.Options{\n\t\tSSH: &policy.SSHPolicyOptions{\n\t\t\tHost: &policy.SSHHostCertificateOptions{\n\t\t\t\tAllowedNames: &policy.SSHNameOptions{\n\t\t\t\t\tDNSDomains: []string{\"*.test.com\"},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\thostPolicy, err := policy.New(hostPolicyOptions)\n\tassert.FatalError(t, err)\n\n\tnow := time.Now()\n\n\ttype fields struct {\n\t\tsshCAUserCertSignKey ssh.Signer\n\t\tsshCAHostCertSignKey ssh.Signer\n\t\tpolicyEngine         *policy.Engine\n\t}\n\ttype args struct {\n\t\tkey      ssh.PublicKey\n\t\topts     provisioner.SignSSHOptions\n\t\tsignOpts []provisioner.SignOption\n\t}\n\ttype want struct {\n\t\tCertType    uint32\n\t\tPrincipals  []string\n\t\tValidAfter  uint64\n\t\tValidBefore uint64\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tfields  fields\n\t\targs    args\n\t\twant    want\n\t\twantErr bool\n\t}{\n\t\t{\"ok-user\", fields{signer, signer, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userTemplate, userOptions}}, want{CertType: ssh.UserCert}, false},\n\t\t{\"ok-host\", fields{signer, signer, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{hostTemplate, hostOptions}}, want{CertType: ssh.HostCert}, false},\n\t\t{\"ok-user-only\", fields{signer, nil, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userTemplate, userOptions}}, want{CertType: ssh.UserCert}, false},\n\t\t{\"ok-host-only\", fields{nil, signer, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{hostTemplate, hostOptions}}, want{CertType: ssh.HostCert}, false},\n\t\t{\"ok-opts-type-user\", fields{signer, signer, nil}, args{pub, provisioner.SignSSHOptions{CertType: \"user\"}, []provisioner.SignOption{userTemplate}}, want{CertType: ssh.UserCert}, false},\n\t\t{\"ok-opts-type-host\", fields{signer, signer, nil}, args{pub, provisioner.SignSSHOptions{CertType: \"host\"}, []provisioner.SignOption{hostTemplate}}, want{CertType: ssh.HostCert}, false},\n\t\t{\"ok-opts-principals\", fields{signer, signer, nil}, args{pub, provisioner.SignSSHOptions{CertType: \"user\", Principals: []string{\"user\"}}, []provisioner.SignOption{userTemplateWithUser}}, want{CertType: ssh.UserCert, Principals: []string{\"user\"}}, false},\n\t\t{\"ok-opts-principals\", fields{signer, signer, nil}, args{pub, provisioner.SignSSHOptions{CertType: \"host\", Principals: []string{\"foo.test.com\", \"bar.test.com\"}}, []provisioner.SignOption{hostTemplateWithHosts}}, want{CertType: ssh.HostCert, Principals: []string{\"foo.test.com\", \"bar.test.com\"}}, false},\n\t\t{\"ok-opts-valid-after\", fields{signer, signer, nil}, args{pub, provisioner.SignSSHOptions{CertType: \"user\", ValidAfter: provisioner.NewTimeDuration(now)}, []provisioner.SignOption{userTemplate}}, want{CertType: ssh.UserCert, ValidAfter: uint64(now.Unix())}, false},\n\t\t{\"ok-opts-valid-before\", fields{signer, signer, nil}, args{pub, provisioner.SignSSHOptions{CertType: \"host\", ValidBefore: provisioner.NewTimeDuration(now)}, []provisioner.SignOption{hostTemplate}}, want{CertType: ssh.HostCert, ValidBefore: uint64(now.Unix())}, false},\n\t\t{\"ok-cert-validator\", fields{signer, signer, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userTemplate, userOptions, sshTestCertValidator(\"\")}}, want{CertType: ssh.UserCert}, false},\n\t\t{\"ok-cert-modifier\", fields{signer, signer, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userTemplate, userOptions, sshTestCertModifier(\"\")}}, want{CertType: ssh.UserCert}, false},\n\t\t{\"ok-opts-validator\", fields{signer, signer, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userTemplate, userOptions, sshTestOptionsValidator(\"\")}}, want{CertType: ssh.UserCert}, false},\n\t\t{\"ok-opts-modifier\", fields{signer, signer, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userTemplate, userOptions, sshTestOptionsModifier(\"\")}}, want{CertType: ssh.UserCert}, false},\n\t\t{\"ok-custom-template\", fields{signer, signer, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userCustomTemplate, userOptions}}, want{CertType: ssh.UserCert, Principals: []string{\"user\", \"admin\"}}, false},\n\t\t{\"ok-enrich-template\", fields{signer, signer, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{enrichTemplate, userOptions, &mockWebhookController{templateData: enrichTemplateData, respData: map[string]any{\"people\": map[string]any{\"role\": []string{\"user\", \"eng\"}}}}}}, want{CertType: ssh.UserCert, Principals: []string{\"user\", \"eng\"}}, false},\n\t\t{\"ok-user-policy\", fields{signer, signer, userPolicy}, args{pub, provisioner.SignSSHOptions{CertType: \"user\", Principals: []string{\"user\"}}, []provisioner.SignOption{userTemplateWithUser}}, want{CertType: ssh.UserCert, Principals: []string{\"user\"}}, false},\n\t\t{\"ok-host-policy\", fields{signer, signer, hostPolicy}, args{pub, provisioner.SignSSHOptions{CertType: \"host\", Principals: []string{\"foo.test.com\", \"bar.test.com\"}}, []provisioner.SignOption{hostTemplateWithHosts}}, want{CertType: ssh.HostCert, Principals: []string{\"foo.test.com\", \"bar.test.com\"}}, false},\n\t\t{\"fail-opts-type\", fields{signer, signer, nil}, args{pub, provisioner.SignSSHOptions{CertType: \"foo\"}, []provisioner.SignOption{userTemplate}}, want{}, true},\n\t\t{\"fail-cert-validator\", fields{signer, signer, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userTemplate, userOptions, sshTestCertValidator(\"an error\")}}, want{}, true},\n\t\t{\"fail-cert-modifier\", fields{signer, signer, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userTemplate, userOptions, sshTestCertModifier(\"an error\")}}, want{}, true},\n\t\t{\"fail-opts-validator\", fields{signer, signer, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userTemplate, userOptions, sshTestOptionsValidator(\"an error\")}}, want{}, true},\n\t\t{\"fail-opts-modifier\", fields{signer, signer, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userTemplate, userOptions, sshTestOptionsModifier(\"an error\")}}, want{}, true},\n\t\t{\"fail-bad-sign-options\", fields{signer, signer, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userTemplate, userOptions, \"wrong type\"}}, want{}, true},\n\t\t{\"fail-no-user-key\", fields{nil, signer, nil}, args{pub, provisioner.SignSSHOptions{CertType: \"user\"}, []provisioner.SignOption{userTemplate}}, want{}, true},\n\t\t{\"fail-no-host-key\", fields{signer, nil, nil}, args{pub, provisioner.SignSSHOptions{CertType: \"host\"}, []provisioner.SignOption{hostTemplate}}, want{}, true},\n\t\t{\"fail-bad-type\", fields{signer, nil, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userTemplate, sshTestModifier{CertType: 100}}}, want{}, true},\n\t\t{\"fail-custom-template\", fields{signer, signer, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userFailTemplate, userOptions}}, want{}, true},\n\t\t{\"fail-custom-template-syntax-error-file\", fields{signer, signer, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userJSONSyntaxErrorTemplateFile, userOptions}}, want{}, true},\n\t\t{\"fail-custom-template-syntax-value-file\", fields{signer, signer, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userJSONValueErrorTemplateFile, userOptions}}, want{}, true},\n\t\t{\"fail-user-policy\", fields{signer, signer, userPolicy}, args{pub, provisioner.SignSSHOptions{CertType: \"user\", Principals: []string{\"root\"}}, []provisioner.SignOption{userTemplateWithRoot}}, want{}, true},\n\t\t{\"fail-user-policy-with-host-cert\", fields{signer, signer, userPolicy}, args{pub, provisioner.SignSSHOptions{CertType: \"host\", Principals: []string{\"foo.test.com\"}}, []provisioner.SignOption{hostTemplateWithExampleDotCom}}, want{}, true},\n\t\t{\"fail-user-policy-with-bad-user\", fields{signer, signer, userPolicy}, args{pub, provisioner.SignSSHOptions{CertType: \"user\", Principals: []string{\"user\"}}, []provisioner.SignOption{badUserTemplate}}, want{}, true},\n\t\t{\"fail-host-policy\", fields{signer, signer, hostPolicy}, args{pub, provisioner.SignSSHOptions{CertType: \"host\", Principals: []string{\"example.com\"}}, []provisioner.SignOption{hostTemplateWithExampleDotCom}}, want{}, true},\n\t\t{\"fail-host-policy-with-user-cert\", fields{signer, signer, hostPolicy}, args{pub, provisioner.SignSSHOptions{CertType: \"user\", Principals: []string{\"user\"}}, []provisioner.SignOption{userTemplateWithUser}}, want{}, true},\n\t\t{\"fail-host-policy-with-bad-host\", fields{signer, signer, hostPolicy}, args{pub, provisioner.SignSSHOptions{CertType: \"host\", Principals: []string{\"example.com\"}}, []provisioner.SignOption{badHostTemplate}}, want{}, true},\n\t\t{\"fail-enriching-webhooks\", fields{signer, signer, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userTemplate, userOptions, &mockWebhookController{enrichErr: provisioner.ErrWebhookDenied}}}, want{}, true},\n\t\t{\"fail-authorizing-webhooks\", fields{signer, signer, nil}, args{pub, provisioner.SignSSHOptions{}, []provisioner.SignOption{userTemplate, userOptions, &mockWebhookController{authorizeErr: provisioner.ErrWebhookDenied}}}, want{}, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ta := testAuthority(t)\n\t\t\ta.sshCAUserCertSignKey = tt.fields.sshCAUserCertSignKey\n\t\t\ta.sshCAHostCertSignKey = tt.fields.sshCAHostCertSignKey\n\t\t\ta.policyEngine = tt.fields.policyEngine\n\n\t\t\tgot, err := a.SignSSH(context.Background(), tt.args.key, tt.args.opts, tt.args.signOpts...)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"Authority.SignSSH() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif err == nil && assert.NotNil(t, got) {\n\t\t\t\tassert.Equals(t, tt.want.CertType, got.CertType)\n\t\t\t\tassert.Equals(t, tt.want.Principals, got.ValidPrincipals)\n\t\t\t\tassert.Equals(t, tt.want.ValidAfter, got.ValidAfter)\n\t\t\t\tassert.Equals(t, tt.want.ValidBefore, got.ValidBefore)\n\t\t\t\tassert.NotNil(t, got.Key)\n\t\t\t\tassert.NotNil(t, got.Nonce)\n\t\t\t\tassert.NotEquals(t, 0, got.Serial)\n\t\t\t\tassert.NotNil(t, got.Signature)\n\t\t\t\tassert.NotNil(t, got.SignatureKey)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestAuthority_SignSSHAddUser(t *testing.T) {\n\tkey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)\n\tassert.FatalError(t, err)\n\tpub, err := ssh.NewPublicKey(key.Public())\n\tassert.FatalError(t, err)\n\tsignKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)\n\tassert.FatalError(t, err)\n\tsigner, err := ssh.NewSignerFromKey(signKey)\n\tassert.FatalError(t, err)\n\n\ttype fields struct {\n\t\tsshCAUserCertSignKey ssh.Signer\n\t\tsshCAHostCertSignKey ssh.Signer\n\t\taddUserPrincipal     string\n\t\taddUserCommand       string\n\t}\n\ttype args struct {\n\t\tkey     ssh.PublicKey\n\t\tsubject *ssh.Certificate\n\t}\n\ttype want struct {\n\t\tCertType     uint32\n\t\tPrincipals   []string\n\t\tValidAfter   uint64\n\t\tValidBefore  uint64\n\t\tForceCommand string\n\t}\n\n\tnow := time.Now()\n\tvalidCert := &ssh.Certificate{\n\t\tCertType:        ssh.UserCert,\n\t\tValidPrincipals: []string{\"user\"},\n\t\tValidAfter:      uint64(now.Unix()),\n\t\tValidBefore:     uint64(now.Add(time.Hour).Unix()),\n\t}\n\tvalidWant := want{\n\t\tCertType:     ssh.UserCert,\n\t\tPrincipals:   []string{\"provisioner\"},\n\t\tValidAfter:   uint64(now.Unix()),\n\t\tValidBefore:  uint64(now.Add(time.Hour).Unix()),\n\t\tForceCommand: \"sudo useradd -m user; nc -q0 localhost 22\",\n\t}\n\n\ttests := []struct {\n\t\tname    string\n\t\tfields  fields\n\t\targs    args\n\t\twant    want\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", fields{signer, signer, \"\", \"\"}, args{pub, validCert}, validWant, false},\n\t\t{\"ok-no-host-key\", fields{signer, nil, \"\", \"\"}, args{pub, validCert}, validWant, false},\n\t\t{\"ok-custom-principal\", fields{signer, signer, \"my-principal\", \"\"}, args{pub, &ssh.Certificate{CertType: ssh.UserCert, ValidPrincipals: []string{\"user\"}}}, want{CertType: ssh.UserCert, Principals: []string{\"my-principal\"}, ForceCommand: \"sudo useradd -m user; nc -q0 localhost 22\"}, false},\n\t\t{\"ok-custom-command\", fields{signer, signer, \"\", \"foo <principal> <principal>\"}, args{pub, &ssh.Certificate{CertType: ssh.UserCert, ValidPrincipals: []string{\"user\"}}}, want{CertType: ssh.UserCert, Principals: []string{\"provisioner\"}, ForceCommand: \"foo user user\"}, false},\n\t\t{\"ok-custom-principal-and-command\", fields{signer, signer, \"my-principal\", \"foo <principal> <principal>\"}, args{pub, &ssh.Certificate{CertType: ssh.UserCert, ValidPrincipals: []string{\"user\"}}}, want{CertType: ssh.UserCert, Principals: []string{\"my-principal\"}, ForceCommand: \"foo user user\"}, false},\n\t\t{\"fail-no-user-key\", fields{nil, signer, \"\", \"\"}, args{pub, validCert}, want{}, true},\n\t\t{\"fail-no-user-cert\", fields{signer, signer, \"\", \"\"}, args{pub, &ssh.Certificate{CertType: ssh.HostCert, ValidPrincipals: []string{\"foo\"}}}, want{}, true},\n\t\t{\"fail-no-principals\", fields{signer, signer, \"\", \"\"}, args{pub, &ssh.Certificate{CertType: ssh.UserCert, ValidPrincipals: []string{}}}, want{}, true},\n\t\t{\"fail-many-principals\", fields{signer, signer, \"\", \"\"}, args{pub, &ssh.Certificate{CertType: ssh.UserCert, ValidPrincipals: []string{\"foo\", \"bar\"}}}, want{}, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ta := testAuthority(t)\n\t\t\ta.sshCAUserCertSignKey = tt.fields.sshCAUserCertSignKey\n\t\t\ta.sshCAHostCertSignKey = tt.fields.sshCAHostCertSignKey\n\t\t\ta.config.SSH = &SSHConfig{\n\t\t\t\tAddUserPrincipal: tt.fields.addUserPrincipal,\n\t\t\t\tAddUserCommand:   tt.fields.addUserCommand,\n\t\t\t}\n\t\t\tgot, err := a.SignSSHAddUser(context.Background(), tt.args.key, tt.args.subject)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"Authority.SignSSHAddUser() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif err == nil && assert.NotNil(t, got) {\n\t\t\t\tassert.Equals(t, tt.want.CertType, got.CertType)\n\t\t\t\tassert.Equals(t, tt.want.Principals, got.ValidPrincipals)\n\t\t\t\tassert.Equals(t, tt.args.subject.ValidPrincipals[0]+\"-\"+tt.want.Principals[0], got.KeyId)\n\t\t\t\tassert.Equals(t, tt.want.ValidAfter, got.ValidAfter)\n\t\t\t\tassert.Equals(t, tt.want.ValidBefore, got.ValidBefore)\n\t\t\t\tassert.Equals(t, map[string]string{\"force-command\": tt.want.ForceCommand}, got.CriticalOptions)\n\t\t\t\tassert.Equals(t, nil, got.Extensions)\n\t\t\t\tassert.NotNil(t, got.Key)\n\t\t\t\tassert.NotNil(t, got.Nonce)\n\t\t\t\tassert.NotEquals(t, 0, got.Serial)\n\t\t\t\tassert.NotNil(t, got.Signature)\n\t\t\t\tassert.NotNil(t, got.SignatureKey)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestAuthority_GetSSHRoots(t *testing.T) {\n\tkey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)\n\tassert.FatalError(t, err)\n\tuser, err := ssh.NewPublicKey(key.Public())\n\tassert.FatalError(t, err)\n\n\tkey, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)\n\tassert.FatalError(t, err)\n\thost, err := ssh.NewPublicKey(key.Public())\n\tassert.FatalError(t, err)\n\n\ttype fields struct {\n\t\tsshCAUserCerts []ssh.PublicKey\n\t\tsshCAHostCerts []ssh.PublicKey\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tfields  fields\n\t\twant    *SSHKeys\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", fields{[]ssh.PublicKey{user}, []ssh.PublicKey{host}}, &SSHKeys{UserKeys: []ssh.PublicKey{user}, HostKeys: []ssh.PublicKey{host}}, false},\n\t\t{\"nil\", fields{}, &SSHKeys{}, false},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ta := testAuthority(t)\n\t\t\ta.sshCAUserCerts = tt.fields.sshCAUserCerts\n\t\t\ta.sshCAHostCerts = tt.fields.sshCAHostCerts\n\n\t\t\tgot, err := a.GetSSHRoots(context.Background())\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"Authority.GetSSHRoots() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"Authority.GetSSHRoots() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestAuthority_GetSSHFederation(t *testing.T) {\n\tkey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)\n\tassert.FatalError(t, err)\n\tuser, err := ssh.NewPublicKey(key.Public())\n\tassert.FatalError(t, err)\n\n\tkey, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)\n\tassert.FatalError(t, err)\n\thost, err := ssh.NewPublicKey(key.Public())\n\tassert.FatalError(t, err)\n\n\ttype fields struct {\n\t\tsshCAUserFederatedCerts []ssh.PublicKey\n\t\tsshCAHostFederatedCerts []ssh.PublicKey\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tfields  fields\n\t\twant    *SSHKeys\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", fields{[]ssh.PublicKey{user}, []ssh.PublicKey{host}}, &SSHKeys{UserKeys: []ssh.PublicKey{user}, HostKeys: []ssh.PublicKey{host}}, false},\n\t\t{\"nil\", fields{}, &SSHKeys{}, false},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ta := testAuthority(t)\n\t\t\ta.sshCAUserFederatedCerts = tt.fields.sshCAUserFederatedCerts\n\t\t\ta.sshCAHostFederatedCerts = tt.fields.sshCAHostFederatedCerts\n\n\t\t\tgot, err := a.GetSSHFederation(context.Background())\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"Authority.GetSSHFederation() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"Authority.GetSSHFederation() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestAuthority_GetSSHConfig(t *testing.T) {\n\tkey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)\n\tassert.FatalError(t, err)\n\tuser, err := ssh.NewPublicKey(key.Public())\n\tassert.FatalError(t, err)\n\tuserSigner, err := ssh.NewSignerFromSigner(key)\n\tassert.FatalError(t, err)\n\tuserB64 := base64.StdEncoding.EncodeToString(user.Marshal())\n\n\tkey, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)\n\tassert.FatalError(t, err)\n\thost, err := ssh.NewPublicKey(key.Public())\n\tassert.FatalError(t, err)\n\thostSigner, err := ssh.NewSignerFromSigner(key)\n\tassert.FatalError(t, err)\n\thostB64 := base64.StdEncoding.EncodeToString(host.Marshal())\n\n\ttmplConfig := &templates.Templates{\n\t\tSSH: &templates.SSHTemplates{\n\t\t\tUser: []templates.Template{\n\t\t\t\t{Name: \"known_host.tpl\", Type: templates.File, TemplatePath: \"./testdata/templates/known_hosts.tpl\", Path: \"ssh/known_host\", Comment: \"#\"},\n\t\t\t},\n\t\t\tHost: []templates.Template{\n\t\t\t\t{Name: \"ca.tpl\", Type: templates.File, TemplatePath: \"./testdata/templates/ca.tpl\", Path: \"/etc/ssh/ca.pub\", Comment: \"#\"},\n\t\t\t},\n\t\t},\n\t\tData: map[string]interface{}{\n\t\t\t\"Step\": &templates.Step{\n\t\t\t\tSSH: templates.StepSSH{\n\t\t\t\t\tUserKey: user,\n\t\t\t\t\tHostKey: host,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\tuserOutput := []templates.Output{\n\t\t{Name: \"known_host.tpl\", Type: templates.File, Comment: \"#\", Path: \"ssh/known_host\", Content: []byte(fmt.Sprintf(\"@cert-authority * %s %s\", host.Type(), hostB64))},\n\t}\n\thostOutput := []templates.Output{\n\t\t{Name: \"ca.tpl\", Type: templates.File, Comment: \"#\", Path: \"/etc/ssh/ca.pub\", Content: []byte(user.Type() + \" \" + userB64)},\n\t}\n\n\ttmplConfigWithUserData := &templates.Templates{\n\t\tSSH: &templates.SSHTemplates{\n\t\t\tUser: []templates.Template{\n\t\t\t\t{Name: \"include.tpl\", Type: templates.File, TemplatePath: \"./testdata/templates/include.tpl\", Path: \"ssh/include\", Comment: \"#\"},\n\t\t\t\t{Name: \"config.tpl\", Type: templates.File, TemplatePath: \"./testdata/templates/config.tpl\", Path: \"ssh/config\", Comment: \"#\"},\n\t\t\t},\n\t\t\tHost: []templates.Template{\n\t\t\t\t{\n\t\t\t\t\tName:         \"sshd_config.tpl\",\n\t\t\t\t\tType:         templates.File,\n\t\t\t\t\tTemplatePath: \"./testdata/templates/sshd_config.tpl\",\n\t\t\t\t\tPath:         \"/etc/ssh/sshd_config\",\n\t\t\t\t\tComment:      \"#\",\n\t\t\t\t\tRequiredData: []string{\"Certificate\", \"Key\"},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tData: map[string]interface{}{\n\t\t\t\"Step\": &templates.Step{\n\t\t\t\tSSH: templates.StepSSH{\n\t\t\t\t\tUserKey: user,\n\t\t\t\t\tHostKey: host,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\tuserOutputWithUserData := []templates.Output{\n\t\t{Name: \"include.tpl\", Type: templates.File, Comment: \"#\", Path: \"ssh/include\", Content: []byte(\"Host *\\n\\tInclude /home/user/.step/ssh/config\")},\n\t\t{Name: \"config.tpl\", Type: templates.File, Comment: \"#\", Path: \"ssh/config\", Content: []byte(\"Match exec \\\"step ssh check-host %h\\\"\\n\\tUserKnownHostsFile /home/user/.step/ssh/known_hosts\\n\\tProxyCommand step ssh proxycommand %r %h %p\\n\")},\n\t}\n\thostOutputWithUserData := []templates.Output{\n\t\t{Name: \"sshd_config.tpl\", Type: templates.File, Comment: \"#\", Path: \"/etc/ssh/sshd_config\", Content: []byte(\"Match all\\n\\tTrustedUserCAKeys /etc/ssh/ca.pub\\n\\tHostCertificate /etc/ssh/ssh_host_ecdsa_key-cert.pub\\n\\tHostKey /etc/ssh/ssh_host_ecdsa_key\")},\n\t}\n\n\ttmplConfigUserIncludes := &templates.Templates{\n\t\tSSH: &templates.SSHTemplates{\n\t\t\tUser: []templates.Template{\n\t\t\t\t{Name: \"step_includes.tpl\", Type: templates.PrependLine, TemplatePath: \"./testdata/templates/step_includes.tpl\", Path: \"${STEPPATH}/ssh/includes\", Comment: \"#\"},\n\t\t\t},\n\t\t},\n\t\tData: map[string]interface{}{\n\t\t\t\"Step\": &templates.Step{\n\t\t\t\tSSH: templates.StepSSH{\n\t\t\t\t\tUserKey: user,\n\t\t\t\t\tHostKey: host,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tuserOutputEmptyData := []templates.Output{\n\t\t{Name: \"step_includes.tpl\", Type: templates.File, Comment: \"#\", Path: \"ssh/includes\", Content: []byte(\"Include \\\"<no value>/ssh/config\\\"\\n\")},\n\t}\n\tuserOutputWithoutTemplateVersion := []templates.Output{\n\t\t{Name: \"step_includes.tpl\", Type: templates.File, Comment: \"#\", Path: \"ssh/includes\", Content: []byte(\"Include \\\"/home/user/.step/ssh/config\\\"\\n\")},\n\t}\n\tuserOutputWithTemplateVersion := []templates.Output{\n\t\t{Name: \"step_includes.tpl\", Type: templates.PrependLine, Comment: \"#\", Path: \"${STEPPATH}/ssh/includes\", Content: []byte(\"Include \\\"/home/user/.step/ssh/config\\\"\\n\")},\n\t}\n\n\ttmplConfigErr := &templates.Templates{\n\t\tSSH: &templates.SSHTemplates{\n\t\t\tUser: []templates.Template{\n\t\t\t\t{Name: \"error.tpl\", Type: templates.File, TemplatePath: \"./testdata/templates/error.tpl\", Path: \"ssh/error\", Comment: \"#\"},\n\t\t\t},\n\t\t\tHost: []templates.Template{\n\t\t\t\t{Name: \"error.tpl\", Type: templates.File, TemplatePath: \"./testdata/templates/error.tpl\", Path: \"ssh/error\", Comment: \"#\"},\n\t\t\t},\n\t\t},\n\t}\n\n\ttmplConfigFail := &templates.Templates{\n\t\tSSH: &templates.SSHTemplates{\n\t\t\tUser: []templates.Template{\n\t\t\t\t{Name: \"fail.tpl\", Type: templates.File, TemplatePath: \"./testdata/templates/fail.tpl\", Path: \"ssh/fail\", Comment: \"#\"},\n\t\t\t},\n\t\t},\n\t}\n\n\ttype fields struct {\n\t\ttemplates  *templates.Templates\n\t\tuserSigner ssh.Signer\n\t\thostSigner ssh.Signer\n\t}\n\ttype args struct {\n\t\ttyp  string\n\t\tdata map[string]string\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tfields  fields\n\t\targs    args\n\t\twant    []templates.Output\n\t\twantErr bool\n\t}{\n\t\t{\"user\", fields{tmplConfig, userSigner, hostSigner}, args{\"user\", nil}, userOutput, false},\n\t\t{\"user\", fields{tmplConfig, userSigner, nil}, args{\"user\", nil}, userOutput, false},\n\t\t{\"host\", fields{tmplConfig, userSigner, hostSigner}, args{\"host\", nil}, hostOutput, false},\n\t\t{\"host\", fields{tmplConfig, nil, hostSigner}, args{\"host\", nil}, hostOutput, false},\n\t\t{\"userWithData\", fields{tmplConfigWithUserData, userSigner, hostSigner}, args{\"user\", map[string]string{\"StepPath\": \"/home/user/.step\"}}, userOutputWithUserData, false},\n\t\t{\"hostWithData\", fields{tmplConfigWithUserData, userSigner, hostSigner}, args{\"host\", map[string]string{\"Certificate\": \"ssh_host_ecdsa_key-cert.pub\", \"Key\": \"ssh_host_ecdsa_key\"}}, hostOutputWithUserData, false},\n\t\t{\"userIncludesEmptyData\", fields{tmplConfigUserIncludes, userSigner, hostSigner}, args{\"user\", nil}, userOutputEmptyData, false},\n\t\t{\"userIncludesWithoutTemplateVersion\", fields{tmplConfigUserIncludes, userSigner, hostSigner}, args{\"user\", map[string]string{\"StepPath\": \"/home/user/.step\"}}, userOutputWithoutTemplateVersion, false},\n\t\t{\"userIncludesWithTemplateVersion\", fields{tmplConfigUserIncludes, userSigner, hostSigner}, args{\"user\", map[string]string{\"StepPath\": \"/home/user/.step\", \"StepSSHTemplateVersion\": \"v2\"}}, userOutputWithTemplateVersion, false},\n\t\t{\"disabled\", fields{tmplConfig, nil, nil}, args{\"host\", nil}, nil, true},\n\t\t{\"badType\", fields{tmplConfig, userSigner, hostSigner}, args{\"bad\", nil}, nil, true},\n\t\t{\"userError\", fields{tmplConfigErr, userSigner, hostSigner}, args{\"user\", nil}, nil, true},\n\t\t{\"hostError\", fields{tmplConfigErr, userSigner, hostSigner}, args{\"host\", map[string]string{\"Function\": \"foo\"}}, nil, true},\n\t\t{\"noTemplates\", fields{nil, userSigner, hostSigner}, args{\"user\", nil}, nil, true},\n\t\t{\"missingData\", fields{tmplConfigWithUserData, userSigner, hostSigner}, args{\"host\", map[string]string{\"Certificate\": \"ssh_host_ecdsa_key-cert.pub\"}}, nil, true},\n\t\t{\"failError\", fields{tmplConfigFail, userSigner, hostSigner}, args{\"user\", nil}, nil, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ta := testAuthority(t)\n\t\t\ta.templates = tt.fields.templates\n\t\t\ta.sshCAUserCertSignKey = tt.fields.userSigner\n\t\t\ta.sshCAHostCertSignKey = tt.fields.hostSigner\n\n\t\t\tgot, err := a.GetSSHConfig(context.Background(), tt.args.typ, tt.args.data)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"Authority.GetSSHConfig() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"Authority.GetSSHConfig() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestAuthority_CheckSSHHost(t *testing.T) {\n\ttype fields struct {\n\t\texists bool\n\t\terr    error\n\t}\n\ttype args struct {\n\t\tctx       context.Context\n\t\tprincipal string\n\t\ttoken     string\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tfields  fields\n\t\targs    args\n\t\twant    bool\n\t\twantErr bool\n\t}{\n\t\t{\"true\", fields{true, nil}, args{context.Background(), \"foo.internal.com\", \"\"}, true, false},\n\t\t{\"false\", fields{false, nil}, args{context.Background(), \"foo.internal.com\", \"\"}, false, false},\n\t\t{\"notImplemented\", fields{false, db.ErrNotImplemented}, args{context.Background(), \"foo.internal.com\", \"\"}, false, true},\n\t\t{\"notImplemented\", fields{true, db.ErrNotImplemented}, args{context.Background(), \"foo.internal.com\", \"\"}, false, true},\n\t\t{\"internal\", fields{false, fmt.Errorf(\"an error\")}, args{context.Background(), \"foo.internal.com\", \"\"}, false, true},\n\t\t{\"internal\", fields{true, fmt.Errorf(\"an error\")}, args{context.Background(), \"foo.internal.com\", \"\"}, false, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ta := testAuthority(t)\n\t\t\ta.db = &db.MockAuthDB{\n\t\t\t\tMIsSSHHost: func(_ string) (bool, error) {\n\t\t\t\t\treturn tt.fields.exists, tt.fields.err\n\t\t\t\t},\n\t\t\t}\n\t\t\tgot, err := a.CheckSSHHost(tt.args.ctx, tt.args.principal, tt.args.token)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"Authority.CheckSSHHost() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif got != tt.want {\n\t\t\t\tt.Errorf(\"Authority.CheckSSHHost() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSSHConfig_Validate(t *testing.T) {\n\tkey, err := jose.GenerateJWK(\"EC\", \"P-256\", \"\", \"sig\", \"\", 0)\n\tassert.FatalError(t, err)\n\n\ttests := []struct {\n\t\tname      string\n\t\tsshConfig *SSHConfig\n\t\twantErr   bool\n\t}{\n\t\t{\"nil\", nil, false},\n\t\t{\"ok\", &SSHConfig{Keys: []*SSHPublicKey{{Type: \"user\", Key: key.Public()}}}, false},\n\t\t{\"ok\", &SSHConfig{Keys: []*SSHPublicKey{{Type: \"host\", Key: key.Public()}}}, false},\n\t\t{\"badType\", &SSHConfig{Keys: []*SSHPublicKey{{Type: \"bad\", Key: key.Public()}}}, true},\n\t\t{\"badKey\", &SSHConfig{Keys: []*SSHPublicKey{{Type: \"user\", Key: *key}}}, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\n\t\t\tif err := tt.sshConfig.Validate(); (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"SSHConfig.Validate() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestAuthority_GetSSHBastion(t *testing.T) {\n\tbastion := &Bastion{\n\t\tHostname: \"bastion.local\",\n\t\tPort:     \"2222\",\n\t}\n\ttype fields struct {\n\t\tconfig         *Config\n\t\tsshBastionFunc func(ctx context.Context, user, hostname string) (*Bastion, error)\n\t}\n\ttype args struct {\n\t\tuser     string\n\t\thostname string\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tfields  fields\n\t\targs    args\n\t\twant    *Bastion\n\t\twantErr bool\n\t}{\n\t\t{\"config\", fields{&Config{SSH: &SSHConfig{Bastion: bastion}}, nil}, args{\"user\", \"host.local\"}, bastion, false},\n\t\t{\"bastion\", fields{&Config{SSH: &SSHConfig{Bastion: bastion}}, nil}, args{\"user\", \"bastion.local\"}, nil, false},\n\t\t{\"nil\", fields{&Config{SSH: &SSHConfig{Bastion: nil}}, nil}, args{\"user\", \"host.local\"}, nil, false},\n\t\t{\"empty\", fields{&Config{SSH: &SSHConfig{Bastion: &Bastion{}}}, nil}, args{\"user\", \"host.local\"}, nil, false},\n\t\t{\"func\", fields{&Config{}, func(_ context.Context, _, _ string) (*Bastion, error) { return bastion, nil }}, args{\"user\", \"host.local\"}, bastion, false},\n\t\t{\"func err\", fields{&Config{}, func(_ context.Context, _, _ string) (*Bastion, error) { return nil, errors.New(\"foo\") }}, args{\"user\", \"host.local\"}, nil, true},\n\t\t{\"error\", fields{&Config{SSH: nil}, nil}, args{\"user\", \"host.local\"}, nil, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ta := &Authority{\n\t\t\t\tconfig:         tt.fields.config,\n\t\t\t\tsshBastionFunc: tt.fields.sshBastionFunc,\n\t\t\t}\n\t\t\tgot, err := a.GetSSHBastion(context.Background(), tt.args.user, tt.args.hostname)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"Authority.GetSSHBastion() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t} else if err != nil {\n\t\t\t\tvar sc render.StatusCodedError\n\t\t\t\tassert.True(t, errors.As(err, &sc), \"error does not implement StatusCodedError interface\")\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"Authority.GetSSHBastion() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestAuthority_GetSSHHosts(t *testing.T) {\n\ta := testAuthority(t)\n\n\ttype test struct {\n\t\tgetHostsFunc func(context.Context, *x509.Certificate) ([]Host, error)\n\t\tauth         *Authority\n\t\tcert         *x509.Certificate\n\t\tcmp          func(got []Host)\n\t\terr          error\n\t\tcode         int\n\t}\n\ttests := map[string]func(t *testing.T) *test{\n\t\t\"fail/getHostsFunc-fail\": func(t *testing.T) *test {\n\t\t\treturn &test{\n\t\t\t\tgetHostsFunc: func(ctx context.Context, cert *x509.Certificate) ([]Host, error) {\n\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t},\n\t\t\t\tcert: &x509.Certificate{},\n\t\t\t\terr:  errors.New(\"getSSHHosts: force\"),\n\t\t\t\tcode: http.StatusInternalServerError,\n\t\t\t}\n\t\t},\n\t\t\"ok/getHostsFunc-defined\": func(t *testing.T) *test {\n\t\t\thosts := []Host{\n\t\t\t\t{HostID: \"1\", Hostname: \"foo\"},\n\t\t\t\t{HostID: \"2\", Hostname: \"bar\"},\n\t\t\t}\n\n\t\t\treturn &test{\n\t\t\t\tgetHostsFunc: func(ctx context.Context, cert *x509.Certificate) ([]Host, error) {\n\t\t\t\t\treturn hosts, nil\n\t\t\t\t},\n\t\t\t\tcert: &x509.Certificate{},\n\t\t\t\tcmp: func(got []Host) {\n\t\t\t\t\tassert.Equals(t, got, hosts)\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"fail/db-get-fail\": func(t *testing.T) *test {\n\t\t\treturn &test{\n\t\t\t\tauth: testAuthority(t, WithDatabase(&db.MockAuthDB{\n\t\t\t\t\tMGetSSHHostPrincipals: func() ([]string, error) {\n\t\t\t\t\t\treturn nil, errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t})),\n\t\t\t\tcert: &x509.Certificate{},\n\t\t\t\terr:  errors.New(\"getSSHHosts: force\"),\n\t\t\t\tcode: http.StatusInternalServerError,\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) *test {\n\t\t\treturn &test{\n\t\t\t\tauth: testAuthority(t, WithDatabase(&db.MockAuthDB{\n\t\t\t\t\tMGetSSHHostPrincipals: func() ([]string, error) {\n\t\t\t\t\t\treturn []string{\"foo\", \"bar\"}, nil\n\t\t\t\t\t},\n\t\t\t\t})),\n\t\t\t\tcert: &x509.Certificate{},\n\t\t\t\tcmp: func(got []Host) {\n\t\t\t\t\tassert.Equals(t, got, []Host{\n\t\t\t\t\t\t{Hostname: \"foo\"},\n\t\t\t\t\t\t{Hostname: \"bar\"},\n\t\t\t\t\t})\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t}\n\tfor name, genTestCase := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ttc := genTestCase(t)\n\n\t\t\tauth := tc.auth\n\t\t\tif auth == nil {\n\t\t\t\tauth = a\n\t\t\t}\n\t\t\tauth.sshGetHostsFunc = tc.getHostsFunc\n\n\t\t\thosts, err := auth.GetSSHHosts(context.Background(), tc.cert)\n\t\t\tif err != nil {\n\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\tvar sc render.StatusCodedError\n\t\t\t\t\tif assert.True(t, errors.As(err, &sc), \"error does not implement StatusCodedError interface\") {\n\t\t\t\t\t\tassert.Equals(t, sc.StatusCode(), tc.code)\n\t\t\t\t\t}\n\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.err.Error())\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif assert.Nil(t, tc.err) {\n\t\t\t\t\ttc.cmp(hosts)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestAuthority_RekeySSH(t *testing.T) {\n\tkey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)\n\tassert.FatalError(t, err)\n\tpub, err := ssh.NewPublicKey(key.Public())\n\tassert.FatalError(t, err)\n\tsignKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)\n\tassert.FatalError(t, err)\n\tsigner, err := ssh.NewSignerFromKey(signKey)\n\tassert.FatalError(t, err)\n\n\tuserOptions := sshTestModifier{\n\t\tCertType: ssh.UserCert,\n\t}\n\n\tnow := time.Now().UTC()\n\n\ta := testAuthority(t)\n\ta.db = &db.MockAuthDB{\n\t\tMIsSSHRevoked: func(sn string) (bool, error) {\n\t\t\treturn false, nil\n\t\t},\n\t}\n\n\ttype test struct {\n\t\tauth       *Authority\n\t\tuserSigner ssh.Signer\n\t\thostSigner ssh.Signer\n\t\tcert       *ssh.Certificate\n\t\tkey        ssh.PublicKey\n\t\tsignOpts   []provisioner.SignOption\n\t\tcmpResult  func(old, n *ssh.Certificate)\n\t\terr        error\n\t\tcode       int\n\t}\n\ttests := map[string]func(t *testing.T) *test{\n\t\t\"fail/is-revoked\": func(t *testing.T) *test {\n\t\t\tauth := testAuthority(t)\n\t\t\tauth.db = &db.MockAuthDB{\n\t\t\t\tMIsSSHRevoked: func(sn string) (bool, error) {\n\t\t\t\t\treturn true, nil\n\t\t\t\t},\n\t\t\t}\n\t\t\treturn &test{\n\t\t\t\tauth:       auth,\n\t\t\t\tuserSigner: signer,\n\t\t\t\thostSigner: signer,\n\t\t\t\tcert: &ssh.Certificate{\n\t\t\t\t\tSerial:          1234567890,\n\t\t\t\t\tValidAfter:      uint64(now.Unix()),\n\t\t\t\t\tValidBefore:     uint64(now.Add(time.Hour).Unix()),\n\t\t\t\t\tCertType:        ssh.UserCert,\n\t\t\t\t\tValidPrincipals: []string{\"foo\", \"bar\"},\n\t\t\t\t\tKeyId:           \"foo\",\n\t\t\t\t},\n\t\t\t\tkey:      pub,\n\t\t\t\tsignOpts: []provisioner.SignOption{},\n\t\t\t\terr:      errors.New(\"authority.authorizeSSHCertificate: certificate has been revoked\"),\n\t\t\t\tcode:     http.StatusUnauthorized,\n\t\t\t}\n\t\t},\n\t\t\"fail/is-revoked-error\": func(t *testing.T) *test {\n\t\t\tauth := testAuthority(t)\n\t\t\tauth.db = &db.MockAuthDB{\n\t\t\t\tMIsSSHRevoked: func(sn string) (bool, error) {\n\t\t\t\t\treturn false, errors.New(\"an error\")\n\t\t\t\t},\n\t\t\t}\n\t\t\treturn &test{\n\t\t\t\tauth:       auth,\n\t\t\t\tuserSigner: signer,\n\t\t\t\thostSigner: signer,\n\t\t\t\tcert: &ssh.Certificate{\n\t\t\t\t\tSerial:          1234567890,\n\t\t\t\t\tValidAfter:      uint64(now.Unix()),\n\t\t\t\t\tValidBefore:     uint64(now.Add(time.Hour).Unix()),\n\t\t\t\t\tCertType:        ssh.UserCert,\n\t\t\t\t\tValidPrincipals: []string{\"foo\", \"bar\"},\n\t\t\t\t\tKeyId:           \"foo\",\n\t\t\t\t},\n\t\t\t\tkey:      pub,\n\t\t\t\tsignOpts: []provisioner.SignOption{},\n\t\t\t\terr:      errors.New(\"authority.authorizeSSHCertificate: an error\"),\n\t\t\t\tcode:     http.StatusInternalServerError,\n\t\t\t}\n\t\t},\n\t\t\"fail/opts-type\": func(t *testing.T) *test {\n\t\t\treturn &test{\n\t\t\t\tuserSigner: signer,\n\t\t\t\thostSigner: signer,\n\t\t\t\tkey:        pub,\n\t\t\t\tsignOpts:   []provisioner.SignOption{userOptions},\n\t\t\t\terr:        errors.New(\"rekeySSH; invalid extra option type\"),\n\t\t\t\tcode:       http.StatusInternalServerError,\n\t\t\t}\n\t\t},\n\t\t\"fail/old-cert-validAfter\": func(t *testing.T) *test {\n\t\t\treturn &test{\n\t\t\t\tuserSigner: signer,\n\t\t\t\thostSigner: signer,\n\t\t\t\tcert:       &ssh.Certificate{},\n\t\t\t\tkey:        pub,\n\t\t\t\tsignOpts:   []provisioner.SignOption{},\n\t\t\t\terr:        errors.New(\"cannot rekey a certificate without validity period\"),\n\t\t\t\tcode:       http.StatusBadRequest,\n\t\t\t}\n\t\t},\n\t\t\"fail/old-cert-validBefore\": func(t *testing.T) *test {\n\t\t\treturn &test{\n\t\t\t\tuserSigner: signer,\n\t\t\t\thostSigner: signer,\n\t\t\t\tcert:       &ssh.Certificate{ValidAfter: uint64(now.Unix())},\n\t\t\t\tkey:        pub,\n\t\t\t\tsignOpts:   []provisioner.SignOption{},\n\t\t\t\terr:        errors.New(\"cannot rekey a certificate without validity period\"),\n\t\t\t\tcode:       http.StatusBadRequest,\n\t\t\t}\n\t\t},\n\t\t\"fail/old-cert-no-user-key\": func(t *testing.T) *test {\n\t\t\treturn &test{\n\t\t\t\tuserSigner: nil,\n\t\t\t\thostSigner: signer,\n\t\t\t\tcert:       &ssh.Certificate{ValidAfter: uint64(now.Unix()), ValidBefore: uint64(now.Add(10 * time.Minute).Unix()), CertType: ssh.UserCert},\n\t\t\t\tkey:        pub,\n\t\t\t\tsignOpts:   []provisioner.SignOption{},\n\t\t\t\terr:        errors.New(\"rekeySSH; user certificate signing is not enabled\"),\n\t\t\t\tcode:       http.StatusNotImplemented,\n\t\t\t}\n\t\t},\n\t\t\"fail/old-cert-no-host-key\": func(t *testing.T) *test {\n\t\t\treturn &test{\n\t\t\t\tuserSigner: signer,\n\t\t\t\thostSigner: nil,\n\t\t\t\tcert:       &ssh.Certificate{ValidAfter: uint64(now.Unix()), ValidBefore: uint64(now.Add(10 * time.Minute).Unix()), CertType: ssh.HostCert},\n\t\t\t\tkey:        pub,\n\t\t\t\tsignOpts:   []provisioner.SignOption{},\n\t\t\t\terr:        errors.New(\"rekeySSH; host certificate signing is not enabled\"),\n\t\t\t\tcode:       http.StatusNotImplemented,\n\t\t\t}\n\t\t},\n\t\t\"fail/unexpected-old-cert-type\": func(t *testing.T) *test {\n\t\t\treturn &test{\n\t\t\t\tuserSigner: signer,\n\t\t\t\thostSigner: signer,\n\t\t\t\tcert:       &ssh.Certificate{ValidAfter: uint64(now.Unix()), ValidBefore: uint64(now.Add(10 * time.Minute).Unix()), CertType: 0},\n\t\t\t\tkey:        pub,\n\t\t\t\tsignOpts:   []provisioner.SignOption{},\n\t\t\t\terr:        errors.New(\"unexpected certificate type '0'\"),\n\t\t\t\tcode:       http.StatusBadRequest,\n\t\t\t}\n\t\t},\n\t\t\"fail/db-store\": func(t *testing.T) *test {\n\t\t\treturn &test{\n\t\t\t\tauth: testAuthority(t, WithDatabase(&db.MockAuthDB{\n\t\t\t\t\tMIsSSHRevoked: func(sn string) (bool, error) {\n\t\t\t\t\t\treturn false, nil\n\t\t\t\t\t},\n\t\t\t\t\tMStoreSSHCertificate: func(cert *ssh.Certificate) error {\n\t\t\t\t\t\treturn errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t})),\n\t\t\t\tuserSigner: signer,\n\t\t\t\thostSigner: nil,\n\t\t\t\tcert:       &ssh.Certificate{ValidAfter: uint64(now.Unix()), ValidBefore: uint64(now.Add(10 * time.Minute).Unix()), CertType: ssh.UserCert},\n\t\t\t\tkey:        pub,\n\t\t\t\tsignOpts:   []provisioner.SignOption{},\n\t\t\t\terr:        errors.New(\"rekeySSH; error storing certificate in db: force\"),\n\t\t\t\tcode:       http.StatusInternalServerError,\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) *test {\n\t\t\tva1 := now.Add(-24 * time.Hour)\n\t\t\tvb1 := now.Add(-23 * time.Hour)\n\t\t\treturn &test{\n\t\t\t\tuserSigner: signer,\n\t\t\t\thostSigner: nil,\n\t\t\t\tcert: &ssh.Certificate{\n\t\t\t\t\tValidAfter:      uint64(va1.Unix()),\n\t\t\t\t\tValidBefore:     uint64(vb1.Unix()),\n\t\t\t\t\tCertType:        ssh.UserCert,\n\t\t\t\t\tValidPrincipals: []string{\"foo\", \"bar\"},\n\t\t\t\t\tKeyId:           \"foo\",\n\t\t\t\t},\n\t\t\t\tkey:      pub,\n\t\t\t\tsignOpts: []provisioner.SignOption{},\n\t\t\t\tcmpResult: func(old, n *ssh.Certificate) {\n\t\t\t\t\tassert.Equals(t, n.CertType, old.CertType)\n\t\t\t\t\tassert.Equals(t, n.ValidPrincipals, old.ValidPrincipals)\n\t\t\t\t\tassert.Equals(t, n.KeyId, old.KeyId)\n\n\t\t\t\t\tassert.True(t, n.ValidAfter > uint64(now.Add(-5*time.Minute).Unix()))\n\t\t\t\t\tassert.True(t, n.ValidAfter < uint64(now.Add(5*time.Minute).Unix()))\n\n\t\t\t\t\tl8r := now.Add(1 * time.Hour)\n\t\t\t\t\tassert.True(t, n.ValidBefore > uint64(l8r.Add(-5*time.Minute).Unix()))\n\t\t\t\t\tassert.True(t, n.ValidBefore < uint64(l8r.Add(5*time.Minute).Unix()))\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t}\n\tfor name, genTestCase := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ttc := genTestCase(t)\n\n\t\t\tauth := tc.auth\n\t\t\tif auth == nil {\n\t\t\t\tauth = a\n\t\t\t}\n\t\t\ta.sshCAUserCertSignKey = tc.userSigner\n\t\t\ta.sshCAHostCertSignKey = tc.hostSigner\n\n\t\t\tcert, err := auth.RekeySSH(context.Background(), tc.cert, tc.key, tc.signOpts...)\n\t\t\tif err != nil {\n\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\tvar sc render.StatusCodedError\n\t\t\t\t\tif assert.True(t, errors.As(err, &sc), \"error does not implement StatusCodedError interface\") {\n\t\t\t\t\t\tassert.Equals(t, sc.StatusCode(), tc.code)\n\t\t\t\t\t}\n\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.err.Error())\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif assert.Nil(t, tc.err) {\n\t\t\t\t\ttc.cmpResult(tc.cert, cert)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestIsValidForAddUser(t *testing.T) {\n\ttype args struct {\n\t\tcert *ssh.Certificate\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\targs    args\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", args{&ssh.Certificate{CertType: ssh.UserCert, ValidPrincipals: []string{\"john\"}}}, false},\n\t\t{\"ok oidc\", args{&ssh.Certificate{CertType: ssh.UserCert, ValidPrincipals: []string{\"jane\", \"jane@smallstep.com\"}}}, false},\n\t\t{\"fail at\", args{&ssh.Certificate{CertType: ssh.UserCert, ValidPrincipals: []string{\"jane\", \"@smallstep.com\"}}}, true},\n\t\t{\"fail host\", args{&ssh.Certificate{CertType: ssh.HostCert, ValidPrincipals: []string{\"john\"}}}, true},\n\t\t{\"fail principals\", args{&ssh.Certificate{CertType: ssh.UserCert, ValidPrincipals: []string{\"john\", \"jane\"}}}, true},\n\t\t{\"fail no principals\", args{&ssh.Certificate{CertType: ssh.UserCert, ValidPrincipals: []string{}}}, true},\n\t\t{\"fail extra principals\", args{&ssh.Certificate{CertType: ssh.UserCert, ValidPrincipals: []string{\"john\", \"jane\", \"doe\"}}}, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif err := IsValidForAddUser(tt.args.cert); (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"IsValidForAddUser() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "authority/testdata/certs/badsig.csr",
    "content": "-----BEGIN CERTIFICATE REQUEST-----\nMIIBBTCBqwIBADAbMRkwFwYDVQQDExBleGFtcGxlLmFjbWUuY29tMFkwEwYHKoZI\nzj0CAQYIKoZIzj0DAQcDQgAEk67TNST5NIdTgAutRDPfO0wa8CGAFjO7D1IoUlJI\ncOA48D4pSkar8v/l4dmKvxdiCNEaU8G0S16zI6dZoBGYAaAuMCwGCSqGSIb3DQEJ\nDjEfMB0wGwYDVR0RBBQwEoIQZXhhbXBsZS5hY21lLmNvbTAKBggqhkjOPQQDAgNJ\nADBGAiEAiuk3HO986dhTjxNBBUsw7sorDWSX2+6sWvYsYkDfJrQCIQDS32JVK0P5\nOI+cWOIc/IGwqZul/zEF5dani5ihOL7UwA==\n-----END CERTIFICATE REQUEST-----\n"
  },
  {
    "path": "authority/testdata/certs/foo.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIICIDCCAcagAwIBAgIQTL7pKDl8mFzRziotXbgjEjAKBggqhkjOPQQDAjAnMSUw\nIwYDVQQDExxFeGFtcGxlIEluYy4gSW50ZXJtZWRpYXRlIENBMB4XDTE5MDMyMjIy\nMjkyOVoXDTE5MDMyMzIyMjkyOVowHDEaMBgGA1UEAxMRZm9vLnNtYWxsc3RlcC5j\nb20wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQbptfDonFaeUPiTr52wl9r3dcz\ngreolwDRmsgyFgnr1EuKH56WRcgH1gjfL0pybFlO3PdgBukR4u+sveq343OAo4He\nMIHbMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUH\nAwIwHQYDVR0OBBYEFP9pHiVlsx5mr4L2QirOb1G9Mo4jMB8GA1UdIwQYMBaAFKEe\n9IdMyaHdURMjoJce7FN9HC9wMBwGA1UdEQQVMBOCEWZvby5zbWFsbHN0ZXAuY29t\nMEwGDCsGAQQBgqRkxihAAQQ8MDoCAQEECHN0ZXAtY2xpBCs0VUVMSng4ZTBhUzlt\nMENIM2ZaMEVCN0Q1YVVQSUNiNzU5ekFMSEZlanZjMAoGCCqGSM49BAMCA0gAMEUC\nIDxtNo1BX/4Sbf/+k1n+v//kh8ETr3clPvhjcyfvBIGTAiEAiT0kvbkPdCCnmHIw\nlhpgBwT5YReZzBwIYXyKyJXc07M=\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "authority/testdata/certs/foo.csr",
    "content": "-----BEGIN CERTIFICATE REQUEST-----\nMIIBBTCBqwIBADAbMRkwFwYDVQQDExBleGFtcGxlLmFjbWUuY29tMFkwEwYHKoZI\nzj0CAQYIKoZIzj0DAQcDQgAEk67TNST5NIdTgAutRDPfO0wa8CGAFjO7D1IoUlJI\ncOA48D4pSkar8v/l4dmKvxdiCNEaU8G0S16zI6dZoBGYAaAuMCwGCSqGSIb3DQEJ\nDjEfMB0wGwYDVR0RBBQwEoIQZXhhbXBsZS5hY21lLmNvbTAKBggqhkjOPQQDAgNJ\nADBGAiEAiuk3HO986dhTjxNBBUsw7sorDWSX2+6sWvYsYkDfJrQCIQDS32JVK0P5\nOI+cWOIc/IGwqZul/zEF5dani5ihOR7UwA==\n-----END CERTIFICATE REQUEST-----\n"
  },
  {
    "path": "authority/testdata/certs/intermediate_ca.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIBxTCCAWugAwIBAgIQfkaUVV4yh8gQZa/EsIECpTAKBggqhkjOPQQDAjAcMRow\nGAYDVQQDExFzbWFsbHN0ZXAgUm9vdCBDQTAeFw0xODA4MTgxOTAxNDZaFw0yODA4\nMTUxOTAxNDZaMCQxIjAgBgNVBAMTGXNtYWxsc3RlcCBJbnRlcm1lZGlhdGUgQ0Ew\nWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATfuJeqP7FHMaVq1uMU9avTZ9JW+VzL\nNS7rJrkhs41j38Oru9UpZWCqXr5uNNioqElRLB6xRfTPd1mCNctQoTUpo4GGMIGD\nMA4GA1UdDwEB/wQEAwIBpjAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIw\nEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQU1rz/ojOuK6vKFH4Qi8mwpXtv\nOzkwHwYDVR0jBBgwFoAUjoa24fWu22FipFrMI2rjBkzVDhEwCgYIKoZIzj0EAwID\nSAAwRQIgWDEWlEaleq5ubnm21k4Zc+agdh1pwOQ41uS4GxXEY5ACIQDkY+MvTLLe\nuBjherwnoVagcftox+GmRwgFpLJC/gRLzw==\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "authority/testdata/certs/provisioner-not-found.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIICTDCCAfGgAwIBAgIQH1JRmbStwdCkiuqf7SM8dzAKBggqhkjOPQQDAjAnMSUw\nIwYDVQQDExxFeGFtcGxlIEluYy4gSW50ZXJtZWRpYXRlIENBMB4XDTE5MDMyMjIz\nMDI0OVoXDTE5MDMyMzIzMDI0OVowLjEsMCoGA1UEAxMjcHJvdmlzaW9uZXItbm90\nLWZvdW5kLnNtYWxsc3RlcC5jb20wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARw\nDOZEqgkXXY0PqnEvl5ADX4xXMDNgX4lraK8SP48Ljo3vUn5FqARjKaBgPLfowFkQ\ngnjsAbBPwzt4SUWZW0ybo4H3MIH0MA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAU\nBggrBgEFBQcDAQYIKwYBBQUHAwIwHQYDVR0OBBYEFDLOyjWD26FV5lfIwPqegYIt\nPdmSMB8GA1UdIwQYMBaAFKEe9IdMyaHdURMjoJce7FN9HC9wMC4GA1UdEQQnMCWC\nI3Byb3Zpc2lvbmVyLW5vdC1mb3VuZC5zbWFsbHN0ZXAuY29tMFMGDCsGAQQBgqRk\nxihAAQRDMEECAQEED2dpZkBleGFtcGxlLmNvbQQrRVdDQThsdFJCdEwxN2VFQS1I\ndW4zQWtCN0sxTERhUXItNkdvdXc3RXBoVTAKBggqhkjOPQQDAgNJADBGAiEAkaHR\ndE706JI8eLio/AqPbH8A/qK1INlbKbrkZ03K5wECIQCqTGY4TYopJqLYt3HkQeTy\ncJfHpuPfIzvpT8X0h3zlwQ==\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "authority/testdata/certs/renew-disabled.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIICJjCCAcygAwIBAgIQWhtLLuWC1foM7eq1jefkGDAKBggqhkjOPQQDAjAnMSUw\nIwYDVQQDExxFeGFtcGxlIEluYy4gSW50ZXJtZWRpYXRlIENBMB4XDTE5MDMyNzIz\nMzk0M1oXDTE5MDMyODIzMzk0M1owHDEaMBgGA1UEAxMRYmF6LnNtYWxsc3RlcC5j\nb20wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATxC77uJiCHgxIoctoHZbEauQwV\n1FStMSKnEQwNkm88GD0HVUcz3g9OEHJbdMuY7VJjefD2NfdMil2N1jOw8VzMo4Hk\nMIHhMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUH\nAwIwHQYDVR0OBBYEFCEoFgFtPV3v3YsJt7uYoz7GgChEMB8GA1UdIwQYMBaAFKEe\n9IdMyaHdURMjoJce7FN9HC9wMBwGA1UdEQQVMBOCEWJhei5zbWFsbHN0ZXAuY29t\nMFIGDCsGAQQBgqRkxihAAQRCMEACAQEEDnJlbmV3X2Rpc2FibGVkBCtJTWk5NFdC\nTkk2Z1A1Y05IWGxaWU5VenZNakdkSHlCUm1Gb28tbENFYXFrMAoGCCqGSM49BAMC\nA0gAMEUCIQD1uGcIQYdEEtVtOFWZGhDk+QJTznH5C182k74Kj/Ns3QIgeNtqYeto\nUr1bgN1pwEwjTyr4aNz+pUWHZhyodduVaCE=\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "authority/testdata/certs/root_ca.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIBezCCASGgAwIBAgIQO4IwgRBrTxUIHlMdV9j5NDAKBggqhkjOPQQDAjAcMRow\nGAYDVQQDExFzbWFsbHN0ZXAgUm9vdCBDQTAeFw0xODA4MTgxOTAxNDZaFw0yODA4\nMTUxOTAxNDZaMBwxGjAYBgNVBAMTEXNtYWxsc3RlcCBSb290IENBMFkwEwYHKoZI\nzj0CAQYIKoZIzj0DAQcDQgAEsA5O9AoNi/LslXQ2LRXrcWsTH3Urlyrw4RNLs4nK\nFep6C/kRk83eD4eGr0Nfh0EYvUc4J6kYIQl62/bD2RjqCqNFMEMwDgYDVR0PAQH/\nBAQDAgGmMBIGA1UdEwEB/wQIMAYBAf8CAQEwHQYDVR0OBBYEFI6GtuH1rtthYqRa\nzCNq4wZM1Q4RMAoGCCqGSM49BAMCA0gAMEUCIQCiC+3oVXGMmUp1xeQ/vOwRWTat\nI96I5ms2tY8LA6z9RQIgdhiWiYwvvgIMlm57sGpol7evVuAibYH6CE3Mqn4jIE4=\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "authority/testdata/certs/ssh_host_ca_key.pub",
    "content": "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBJj80EJXJR9vxefhdqOLSdzRzBw24t9YKPxb+eCYLf7BU50pJQnB/jK2ZM3qLFbieLaYjngZ86T4DzHxlPAnlAY=\n"
  },
  {
    "path": "authority/testdata/certs/ssh_user_ca_key.pub",
    "content": "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBJ8einS88ZaWpcTZG27D5N9JDKfGv0rzjDByLGsZzMsLYl3XcsN9IWKXB6b+5GJ3UaoZf/pFxzRzIdDIh7Ypw3Y=\n"
  },
  {
    "path": "authority/testdata/scep/intermediate.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIICZTCCAgugAwIBAgIQDPpOQXW7OLMFNR/+iOUdQjAKBggqhkjOPQQDAjAXMRUw\nEwYDVQQDEwxzY2VwdGVzdHJvb3QwHhcNMjEwNTA3MTUyMjU2WhcNMzEwNTA1MTUy\nMjU2WjAfMR0wGwYDVQQDExRzY2VwdGVzdGludGVybWVkaWF0ZTCCASIwDQYJKoZI\nhvcNAQEBBQADggEPADCCAQoCggEBAJTw49z9/MeZ/YeRO89ylMV3HnYpw52/Vs2G\nNsgYZRKiPz2RjixUp1iWRPoDONdlEOIAo0TALNOqz4EqJHB+FpBPBA1ZfwG/PlP/\neWFubNXLXIhZPSQOiHmL4dIw0FS/VFGZm1eqc9JPG/V2G6UaKvOa8+W9/nhi4eeL\n+/9nTwG4cTav9ltaVxQ55kcoJtMcvouYQ4oPSZ6yNuVYbFAoaqZnJqNQhxDvKsFH\nlHmvl28FAVM+otmEQNTm91uPwXuVusxEGn9N/d7M4iojCiMGg0S3luBS8IrGRI1Y\nbSKZvGsFnqUjHh2cLL1lqqo5+QvhvP9ut6+g8QGoq8NTc2yCRy8CAwEAAaNmMGQw\nDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFGfO\njTNTKTAyra+rAd/NL2ydarSFMB8GA1UdIwQYMBaAFKJr1p5QRfkHzewG3YEhPAtv\nFQNrMAoGCCqGSM49BAMCA0gAMEUCIEYK76FN9a/hWkMZcQ+NXyzGtfW+bnwsX3oN\nwT6jfyO0AiEAojTeSwf/H2l/E1lvsWJfNr8nOokWz+ZsbmMm5PU0Y+g=\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "authority/testdata/scep/intermediate.key",
    "content": "-----BEGIN RSA PRIVATE KEY-----\nProc-Type: 4,ENCRYPTED\nDEK-Info: AES-256-CBC,a54ae9388ce050f0a479a258d105fbb7\n\nVkJp9kKZQ7O9Gy9orvXaO+klt4Lrqp9oSABSBy8yFcc3neniLixqcyZZ4+CC/OG2\nTGTm4TiB9RBucrUyPwoxBraWbtTLHvS4nfPwr2feSTKoHDhSIr4Z1VMDF8PWiOSg\nvD3iYs5F1lz78hcB/SNdSZ2jm0ze84DFC2E49agWeiFLwezcLhXKQ2HHRJ6PmJv7\nIYB7+aLw8cUis/eJquWv7vrmlnshXBXLOrDekNq/mGhdpUmguDNEGX/3yT+8QYRv\nyeCqLVWcfkQ7KkXAeet0tVPNGQQF0+yS80Hv2/LBcskhL467qa79Xm+QPbBbhsEB\naa4rettMLEdxk3IB1dgXdWhdJ4zBD+RFjczJbQlZRfmPb8sR20V/xp3x9i+SLqKp\nseVoNF+LhLhEwJdMF23t2KpuiOShzC60ApjALN6/O2/XGCl0KQ+NzucX+wpirS6z\nd2XfEYpsUaUFEFraOwfGXxLmluRtS6Q3+0+NPgwVQuH7EE7KuoTDUoSrUG4OFjaq\nCeUeZv1IVf0sYqZQVRiMxxdoFBKUSgcaR1gzzLZgHeoZCGP0PewmZDfJMQ5rWe0D\nzYYIKXUg8+oytHsz+5pQ277psXsl7iApZu56s6w3rD45w/zBeEyBhyL5JMBP8Y6y\n7ReaUGsoFu3WEvrMcOsN+0Vag/SdQsvEH0PGA/ltlrlhaHKq+4t/ZwP6WxUmnaVV\nJNtTWB8IqxtO0zbwK1owxjrO7t42K2isSryg/y2sQb4wgokoOzg1PqEaM8PIUvjl\nqkGhwrOz4lNNQ9b6Hgy81DpnXnJkRNY7B5yKi62TCc6K/DHrFs0fHKb9Qxac5KKf\npaasGWuEC5IP0lUyn81BmAVlfByBvnGmYiDmmGXLmfsyqtGFL9fpOl1Txq3/URfT\nf705lzeUt9r2BT5FJtV5lkTntRzjpi5QeRiJsvfXA7nCPZj2hoLWgIm/D/HRgfVR\nPIX1M7nxefRgES+T6UJNsBbGjSTgEVIPqVnyWs0JUyg4+KQ5VMU8g8SGA0dtnJyF\n9JrZHy2OA/AYt/c96vJj4WdFvqw3kodIKOipBbKjBBGokaOTsLADFEYgOr51BfvO\nQmxGZoXsRpD4sBOAwW039Ka5uCfuBETa+XQPtlHailaRZLlK9cZaDlzQr/K9jAgM\nqOmZIKr3L8YPK3mQV+mWVYchPXTf+UyTFiWIt30z1JlyrTw1H+h62pV9f1QXDB6P\nFIlfWHUK2mohWqzBnv4zFRBTVUnUDC9ONT+cVLh0cvlbRt2yy2ZgR4+d6IGH6mRH\nVLgWAFpS3KS1/4NfwWRBaMvIBfqfXCzXSqVJsq7RlBSW/EBwe9TDXhcTzOLHjx4E\nvdp+hqyXT62cTd7oWe78BBw3xOgpQwQ8bUdhye0kXMLNpU9j70pA7CjLVoVsdzH6\nn1EG7Mz/5NmXLy7LP8RuVU90mNQzNu8PFWtfjZ/jr3/OxoOc0Wx6mFykXkZbxKXI\nxOlaOnUHKnEmsCLnZUkIxEqwKo+RYWBRtKxYsS8x8TLXyFGEfHidI75ulZM7eAS8\njWtVNKbPIyal+nQMpqa/lKW6fiGGUVp0u2x3Pnd8luRCs2htBmXSB7W7mJ2SMCui\n-----END RSA PRIVATE KEY-----\n"
  },
  {
    "path": "authority/testdata/scep/root.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIBczCCARigAwIBAgIRAImbSwfqrrI6p72t0b9f6l4wCgYIKoZIzj0EAwIwFzEV\nMBMGA1UEAxMMc2NlcHRlc3Ryb290MB4XDTIxMDUwNzE1MjEzMFoXDTMxMDUwNTE1\nMjEzMFowFzEVMBMGA1UEAxMMc2NlcHRlc3Ryb290MFkwEwYHKoZIzj0CAQYIKoZI\nzj0DAQcDQgAE3fyAgJsDICrnXhhoxHKmXMHLoW0EM9bYiBmx1xRyol0Qa3SZMW43\nrtTykqVP3HUA3rIrLdX106s9IFcA3eIYiaNFMEMwDgYDVR0PAQH/BAQDAgEGMBIG\nA1UdEwEB/wQIMAYBAf8CAQEwHQYDVR0OBBYEFKJr1p5QRfkHzewG3YEhPAtvFQNr\nMAoGCCqGSM49BAMCA0kAMEYCIQDlXU695zKmSSfVPaPbM2cx7OlKr2n6NSyifatH\n9zDITwIhAJUbbHzRJVgscxx+VSMqC2TkFvug6ryNu6kQIKNRwolr\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "authority/testdata/scep/root.key",
    "content": "-----BEGIN EC PRIVATE KEY-----\nProc-Type: 4,ENCRYPTED\nDEK-Info: AES-256-CBC,0ea78864d21de199d3a737e4337589c2\n\nZD3ggzw3eDYJp8NovTWgTxk6MagLutgU2UfwbYliAl7wKvVyzwkPytwRkyAXPBM6\njMfiAdq6wY2wEpc8OSfrvAXrGuYqlCakDhdMaFDPcS3K29VLl4BaO2X2Rfk55nBd\nASBNREKVb+hg2HV22DO7r6t+EYXTSD6iO7EB90bvKdE=\n-----END EC PRIVATE KEY-----\n"
  },
  {
    "path": "authority/testdata/secrets/foo.key",
    "content": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIJmnxm3N/ahRA2PWeZhRGJUKPU1lI44WcE4P1bynIim6oAoGCCqGSM49\nAwEHoUQDQgAEG6bXw6JxWnlD4k6+dsJfa93XM4K3qJcA0ZrIMhYJ69RLih+elkXI\nB9YI3y9KcmxZTtz3YAbpEeLvrL3qt+NzgA==\n-----END EC PRIVATE KEY-----\n"
  },
  {
    "path": "authority/testdata/secrets/intermediate_ca_key",
    "content": "-----BEGIN EC PRIVATE KEY-----\nProc-Type: 4,ENCRYPTED\nDEK-Info: AES-128-CBC,856c18a6a0d6654d0e3aed6e3211a285\n\nJ1j4qQjtBsh6+ETLy/wlG4eSmQSkmxNQkyzt5zkpqFozS8yssAmTdkIFM6JGnQcc\ne0jGRXCy+Sx/vYQCY1uKR5FKlVpcT9I02r1nwgNHfd6zVmbQcXuYKvZQjJKLP27p\ngqluC9+nPA+NLJM/oP0GjNtQGasCc7oX6jYP4f1XFpw=\n-----END EC PRIVATE KEY-----\n"
  },
  {
    "path": "authority/testdata/secrets/max_priv.jwk",
    "content": "{\n  \"protected\": \"eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJlbmMiOiJBMTI4R0NNIiwicDJjIjoxMDAwMDAsInAycyI6IkpsNkZLWUp4V1UwdGRIbG9UanA1aGcifQ\",\n  \"encrypted_key\": \"Qy0EP6u5-t0ggOweoc3Z1DCzR5BllsQi\",\n  \"iv\": \"KUkviZ_TJKY4c0Mi\",\n  \"ciphertext\": \"h7QZqgh_Fl2MZpmVy4h375yC0DORjB1dQULbNqc6MuUCW2iweWVRysFImUXiXMUKRarJC5adwWy1GhyAqUj6Xj1iOZDGLjYnqMETGWcI0rKDBwcSU7y7Y-2VYBRDSM2b7aWtTBfz3_kvEaw_vc3b5CEPJ86UlZc-jhKFRr_IcGWU-vXX5-bppoH15IPreyzi55YdjCll338lYpDecB_Paym3XBXotyd2iGXXUwoA1npEFwuyRMMEhl9zLp7rVcMW6A_32EzB8cZANEnA0C4FXGHQalY6u_2UeqxcC8_FuXPay6VIYODyRqcABvvkft3nwOcrI0pYDGBdk2w2Euk\",\n  \"tag\": \"kOAFq3Tg6s4vBGS_plMpSw\"\n}"
  },
  {
    "path": "authority/testdata/secrets/max_pub.jwk",
    "content": "{\n  \"use\": \"sig\",\n  \"kty\": \"EC\",\n  \"kid\": \"IMi94WBNI6gP5cNHXlZYNUzvMjGdHyBRmFoo-lCEaqk\",\n  \"crv\": \"P-256\",\n  \"alg\": \"ES256\",\n  \"x\": \"XmaY0c9Cc_kjfn9uhimiDiKnKn00gmFzzsvElg4KxoE\",\n  \"y\": \"ZhYcFQBqtErdC_pA7sOXrO7AboCEPIKP9Ik4CHJqANk\"\n}"
  },
  {
    "path": "authority/testdata/secrets/provisioner-not-found.key",
    "content": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEILWLnE+pkh9QQ0CcM89sCBAWMEK7EtoJOmHvvFpugj2joAoGCCqGSM49\nAwEHoUQDQgAEcAzmRKoJF12ND6pxL5eQA1+MVzAzYF+Ja2ivEj+PC46N71J+RagE\nYymgYDy36MBZEIJ47AGwT8M7eElFmVtMmw==\n-----END EC PRIVATE KEY-----\n"
  },
  {
    "path": "authority/testdata/secrets/renew-disabled.key",
    "content": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIKmDvbNqeIZA9zssZxixJzAQBEUEBSyVnjCKvTWGMAd2oAoGCCqGSM49\nAwEHoUQDQgAE8Qu+7iYgh4MSKHLaB2WxGrkMFdRUrTEipxEMDZJvPBg9B1VHM94P\nThByW3TLmO1SY3nw9jX3TIpdjdYzsPFczA==\n-----END EC PRIVATE KEY-----\n"
  },
  {
    "path": "authority/testdata/secrets/ssh_host_ca_key",
    "content": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIKZCgb5pTSSCbr/xcHCOkl9O6tQtZmNahr3Ap3/c2nBLoAoGCCqGSM49\nAwEHoUQDQgAEmPzQQlclH2/F5+F2o4tJ3NHMHDbi31go/Fv54Jgt/sFTnSklCcH+\nMrZkzeosVuJ4tpiOeBnzpPgPMfGU8CeUBg==\n-----END EC PRIVATE KEY-----\n"
  },
  {
    "path": "authority/testdata/secrets/ssh_user_ca_key",
    "content": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIDuzykyPM6rLnSoyF4jnOpPAlyKZERqtaB8PTh179DMgoAoGCCqGSM49\nAwEHoUQDQgAEnx6KdLzxlpalxNkbbsPk30kMp8a/SvOMMHIsaxnMywtiXddyw30h\nYpcHpv7kYndRqhl/+kXHNHMh0MiHtinDdg==\n-----END EC PRIVATE KEY-----\n"
  },
  {
    "path": "authority/testdata/secrets/step_cli_key",
    "content": "-----BEGIN EC PRIVATE KEY-----\nProc-Type: 4,ENCRYPTED\nDEK-Info: AES-128-CBC,e2c9c7cdad45b5032f1990b929cf83fd\n\nk3Yd307VgDrdllCBGN7PP8dOMQvEAUkq1lYtyxAWa7u/DuxeDP7SYlDB+xEk/UL8\nbgoYYCProydEElYFzGg8Z98WYAzbNoP2p6PPPpAhOZsxJjc5OfTHf/OQleR8PjD5\nryN4woGuq7Tiq5xritlyhluPc91ODqMsm4P98X1sPYA=\n-----END EC PRIVATE KEY-----\n"
  },
  {
    "path": "authority/testdata/secrets/step_cli_key.public",
    "content": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE7ZdAAMZCFU4XwgblI5RfZouBi8lY\nmF6DlZusNNnsbm+xCvYl3PAPZ+DKvKYERdazEPEU2OOo3riostJst0tn1g==\n-----END PUBLIC KEY-----\n"
  },
  {
    "path": "authority/testdata/secrets/step_cli_key_priv.jwk",
    "content": "{\n  \"protected\": \"eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJlbmMiOiJBMTI4R0NNIiwicDJjIjoxMDAwMDAsInAycyI6IlhOdmYxQjgxSUlLMFA2NUkwcmtGTGcifQ\",\n  \"encrypted_key\": \"XaN9zcPQeWt49zchUDm34FECUTHfQTn_\",\n  \"iv\": \"tmNHPQDqR3ebsWfd\",\n  \"ciphertext\": \"9WZr3YVdeOyJh36vvx0VlRtluhvYp4K7jJ1KGDr1qypwZ3ziBVSNbYYQ71du7fTtrnfG1wgGTVR39tWSzBU-zwQ5hdV3rpMAaEbod5zeW6SHd95H3Bvcb43YiiqJFNL5sGZzFb7FqzVmpsZ1efiv6sZaGDHtnCAL6r12UG5EZuqGfM0jGCZitUz2m9TUKXJL5DJ7MOYbFfkCEsUBPDm_TInliSVn2kMJhFa0VOe5wZk5YOuYM3lNYW64HGtbf-llN2Xk-4O9TfeSPizBx9ZqGpeu8pz13efUDT2WL9tWo6-0UE-CrG0bScm8lFTncTkHcu49_a5NaUBkYlBjEiw\",\n  \"tag\": \"thPcx3t1AUcWuEygXIY3Fg\"\n}"
  },
  {
    "path": "authority/testdata/secrets/step_cli_key_pub.jwk",
    "content": "{\n  \"use\": \"sig\",\n  \"kty\": \"EC\",\n  \"kid\": \"4UELJx8e0aS9m0CH3fZ0EB7D5aUPICb759zALHFejvc\",\n  \"crv\": \"P-256\",\n  \"alg\": \"ES256\",\n  \"x\": \"7ZdAAMZCFU4XwgblI5RfZouBi8lYmF6DlZusNNnsbm8\",\n  \"y\": \"sQr2JdzwD2fgyrymBEXWsxDxFNjjqN64qLLSbLdLZ9Y\"\n}"
  },
  {
    "path": "authority/testdata/templates/badjsonsyntax.tpl",
    "content": "{\n    \"subject\": \"badjson.localhost,\n}"
  },
  {
    "path": "authority/testdata/templates/badjsonvalue.tpl",
    "content": "{\n    \"subject\": 1,\n    \"sans\": {{ toJson .SANs }},\n{{- if typeIs \"*rsa.PublicKey\" .Insecure.CR.PublicKey }}\n    \"keyUsage\": [\"keyEncipherment\", \"digitalSignature\"],\n{{- else }}\n    \"keyUsage\": [\"digitalSignature\"],\n{{- end }}\n    \"extKeyUsage\": [\"serverAuth\", \"clientAuth\"]\n}"
  },
  {
    "path": "authority/testdata/templates/ca.tpl",
    "content": "{{.Step.SSH.UserKey.Type}} {{.Step.SSH.UserKey.Marshal | toString | b64enc}}\n{{- range .Step.SSH.UserFederatedKeys}}\n{{.Type}} {{.Marshal | toString | b64enc}}\n{{- end}}"
  },
  {
    "path": "authority/testdata/templates/config.tpl",
    "content": "Match exec \"step ssh check-host %h\"\n{{- if .User.User }}\n\tUser {{.User.User}}\n{{- end }}\n{{- if or .User.GOOS \"none\" | eq \"windows\" }}\n\tUserKnownHostsFile {{.User.StepPath}}\\ssh\\known_hosts\n\tProxyCommand C:\\Windows\\System32\\cmd.exe /c step ssh proxycommand %r %h %p\n{{- else }}\n\tUserKnownHostsFile {{.User.StepPath}}/ssh/known_hosts\n\tProxyCommand step ssh proxycommand %r %h %p\n{{- end }}\n"
  },
  {
    "path": "authority/testdata/templates/error.tpl",
    "content": "Missing function {{Function}}"
  },
  {
    "path": "authority/testdata/templates/fail.tpl",
    "content": "{{ fail \"This template will fail\" }}"
  },
  {
    "path": "authority/testdata/templates/include.tpl",
    "content": "Host *\n{{- if or .User.GOOS \"linux\" | eq \"windows\" }}\n\tInclude {{ .User.StepPath | replace \"\\\\\" \"/\" | trimPrefix \"C:\" }}/ssh/config\n{{- else }}\n\tInclude {{.User.StepPath}}/ssh/config\n{{- end }}"
  },
  {
    "path": "authority/testdata/templates/known_hosts.tpl",
    "content": "@cert-authority * {{.Step.SSH.HostKey.Type}} {{.Step.SSH.HostKey.Marshal | toString | b64enc}}\n{{- range .Step.SSH.HostFederatedKeys}}\n@cert-authority * {{.Type}} {{.Marshal | toString | b64enc}}\n{{- end}}"
  },
  {
    "path": "authority/testdata/templates/sshd_config.tpl",
    "content": "Match all\n\tTrustedUserCAKeys /etc/ssh/ca.pub\n\tHostCertificate /etc/ssh/{{.User.Certificate}}\n\tHostKey /etc/ssh/{{.User.Key}}"
  },
  {
    "path": "authority/testdata/templates/step_includes.tpl",
    "content": "{{- if or .User.GOOS \"none\" | eq \"windows\" }}Include \"{{ .User.StepPath | replace \"\\\\\" \"/\" | trimPrefix \"C:\" }}/ssh/config\"{{- else }}Include \"{{.User.StepPath}}/ssh/config\"{{- end }}\n"
  },
  {
    "path": "authority/tls.go",
    "content": "package authority\n\nimport (\n\t\"context\"\n\t\"crypto\"\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"crypto/x509/pkix\"\n\t\"encoding/asn1\"\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"encoding/pem\"\n\t\"fmt\"\n\t\"math/big\"\n\t\"net\"\n\t\"net/http\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n\t\"golang.org/x/crypto/ssh\"\n\n\t\"go.step.sm/crypto/jose\"\n\t\"go.step.sm/crypto/keyutil\"\n\t\"go.step.sm/crypto/pemutil\"\n\t\"go.step.sm/crypto/x509util\"\n\n\t\"github.com/smallstep/certificates/authority/config\"\n\t\"github.com/smallstep/certificates/authority/provisioner\"\n\tcasapi \"github.com/smallstep/certificates/cas/apiv1\"\n\t\"github.com/smallstep/certificates/db\"\n\t\"github.com/smallstep/certificates/errs\"\n\t\"github.com/smallstep/certificates/webhook\"\n\t\"github.com/smallstep/nosql/database\"\n)\n\ntype tokenKey struct{}\n\n// NewTokenContext adds the given token to the context.\nfunc NewTokenContext(ctx context.Context, token string) context.Context {\n\treturn context.WithValue(ctx, tokenKey{}, token)\n}\n\n// TokenFromContext returns the token from the given context.\nfunc TokenFromContext(ctx context.Context) (token string, ok bool) {\n\ttoken, ok = ctx.Value(tokenKey{}).(string)\n\treturn\n}\n\n// GetTLSOptions returns the tls options configured.\nfunc (a *Authority) GetTLSOptions() *config.TLSOptions {\n\treturn a.config.TLS\n}\n\nvar (\n\toidAuthorityKeyIdentifier            = asn1.ObjectIdentifier{2, 5, 29, 35}\n\toidSubjectKeyIdentifier              = asn1.ObjectIdentifier{2, 5, 29, 14}\n\toidExtensionIssuingDistributionPoint = asn1.ObjectIdentifier{2, 5, 29, 28}\n)\n\nfunc withDefaultASN1DN(def *config.ASN1DN) provisioner.CertificateModifierFunc {\n\treturn func(crt *x509.Certificate, _ provisioner.SignOptions) error {\n\t\tif def == nil {\n\t\t\treturn errors.New(\"default ASN1DN template cannot be nil\")\n\t\t}\n\t\tif len(crt.Subject.Country) == 0 && def.Country != \"\" {\n\t\t\tcrt.Subject.Country = append(crt.Subject.Country, def.Country)\n\t\t}\n\t\tif len(crt.Subject.Organization) == 0 && def.Organization != \"\" {\n\t\t\tcrt.Subject.Organization = append(crt.Subject.Organization, def.Organization)\n\t\t}\n\t\tif len(crt.Subject.OrganizationalUnit) == 0 && def.OrganizationalUnit != \"\" {\n\t\t\tcrt.Subject.OrganizationalUnit = append(crt.Subject.OrganizationalUnit, def.OrganizationalUnit)\n\t\t}\n\t\tif len(crt.Subject.Locality) == 0 && def.Locality != \"\" {\n\t\t\tcrt.Subject.Locality = append(crt.Subject.Locality, def.Locality)\n\t\t}\n\t\tif len(crt.Subject.Province) == 0 && def.Province != \"\" {\n\t\t\tcrt.Subject.Province = append(crt.Subject.Province, def.Province)\n\t\t}\n\t\tif len(crt.Subject.StreetAddress) == 0 && def.StreetAddress != \"\" {\n\t\t\tcrt.Subject.StreetAddress = append(crt.Subject.StreetAddress, def.StreetAddress)\n\t\t}\n\t\tif crt.Subject.SerialNumber == \"\" && def.SerialNumber != \"\" {\n\t\t\tcrt.Subject.SerialNumber = def.SerialNumber\n\t\t}\n\t\tif crt.Subject.CommonName == \"\" && def.CommonName != \"\" {\n\t\t\tcrt.Subject.CommonName = def.CommonName\n\t\t}\n\t\treturn nil\n\t}\n}\n\n// GetX509Signer returns a [crypto.Signer] implementation using the intermediate\n// key.\n//\n// This method can return a [NotImplementedError] if the CA is configured with a\n// Certificate Authority Service (CAS) that does not implement the\n// CertificateAuthoritySigner interface.\n//\n// [NotImplementedError]: https://pkg.go.dev/github.com/smallstep/certificates/cas/apiv1#NotImplementedError\nfunc (a *Authority) GetX509Signer() (crypto.Signer, error) {\n\tif s, ok := a.x509CAService.(casapi.CertificateAuthoritySigner); ok {\n\t\treturn s.GetSigner()\n\t}\n\treturn nil, casapi.NotImplementedError{}\n}\n\n// Sign creates a signed certificate from a certificate signing request. It\n// creates a new context.Context, and calls into SignWithContext.\n//\n// Deprecated: Use authority.SignWithContext with an actual context.Context.\nfunc (a *Authority) Sign(csr *x509.CertificateRequest, signOpts provisioner.SignOptions, extraOpts ...provisioner.SignOption) ([]*x509.Certificate, error) {\n\treturn a.SignWithContext(context.Background(), csr, signOpts, extraOpts...)\n}\n\n// SignWithContext creates a signed certificate from a certificate signing\n// request, taking the provided context.Context.\nfunc (a *Authority) SignWithContext(ctx context.Context, csr *x509.CertificateRequest, signOpts provisioner.SignOptions, extraOpts ...provisioner.SignOption) ([]*x509.Certificate, error) {\n\tchain, prov, err := a.signX509(ctx, csr, signOpts, extraOpts...)\n\ta.meter.X509Signed(chain, prov, err)\n\treturn chain, err\n}\n\nfunc (a *Authority) signX509(ctx context.Context, csr *x509.CertificateRequest, signOpts provisioner.SignOptions, extraOpts ...provisioner.SignOption) ([]*x509.Certificate, provisioner.Interface, error) {\n\tvar (\n\t\tcertOptions    []x509util.Option\n\t\tcertValidators []provisioner.CertificateValidator\n\t\tcertModifiers  []provisioner.CertificateModifier\n\t\tcertEnforcers  []provisioner.CertificateEnforcer\n\t)\n\n\topts := []any{errs.WithKeyVal(\"csr\", csr), errs.WithKeyVal(\"signOptions\", signOpts)}\n\tif err := csr.CheckSignature(); err != nil {\n\t\treturn nil, nil, errs.ApplyOptions(\n\t\t\terrs.BadRequestErr(err, \"invalid certificate request\"),\n\t\t\topts...,\n\t\t)\n\t}\n\n\t// Set backdate with the configured value\n\tsignOpts.Backdate = a.config.AuthorityConfig.Backdate.Duration\n\n\tvar (\n\t\tprov       provisioner.Interface\n\t\tpInfo      *casapi.ProvisionerInfo\n\t\tattData    *provisioner.AttestationData\n\t\twebhookCtl webhookController\n\t)\n\tfor _, op := range extraOpts {\n\t\tswitch k := op.(type) {\n\t\t// Capture current provisioner\n\t\tcase provisioner.Interface:\n\t\t\tprov = k\n\t\t\tpInfo = &casapi.ProvisionerInfo{\n\t\t\t\tID:   prov.GetID(),\n\t\t\t\tType: prov.GetType().String(),\n\t\t\t\tName: prov.GetName(),\n\t\t\t}\n\t\t// Adds new options to NewCertificate\n\t\tcase provisioner.CertificateOptions:\n\t\t\tcertOptions = append(certOptions, k.Options(signOpts)...)\n\n\t\t// Validate the given certificate request.\n\t\tcase provisioner.CertificateRequestValidator:\n\t\t\tif err := k.Valid(csr); err != nil {\n\t\t\t\treturn nil, prov, errs.ApplyOptions(\n\t\t\t\t\terrs.ForbiddenErr(err, \"error validating certificate request\"),\n\t\t\t\t\topts...,\n\t\t\t\t)\n\t\t\t}\n\n\t\t// Validates the unsigned certificate template.\n\t\tcase provisioner.CertificateValidator:\n\t\t\tcertValidators = append(certValidators, k)\n\n\t\t// Modifies a certificate before validating it.\n\t\tcase provisioner.CertificateModifier:\n\t\t\tcertModifiers = append(certModifiers, k)\n\n\t\t// Modifies a certificate after validating it.\n\t\tcase provisioner.CertificateEnforcer:\n\t\t\tcertEnforcers = append(certEnforcers, k)\n\n\t\t// Extra information from ACME attestations.\n\t\tcase provisioner.AttestationData:\n\t\t\tattData = &k\n\n\t\t// Capture the provisioner's webhook controller\n\t\tcase webhookController:\n\t\t\twebhookCtl = k\n\n\t\tdefault:\n\t\t\treturn nil, prov, errs.InternalServer(\"authority.Sign; invalid extra option type %T\", append([]any{k}, opts...)...)\n\t\t}\n\t}\n\n\tif err := a.callEnrichingWebhooksX509(ctx, prov, webhookCtl, attData, csr); err != nil {\n\t\treturn nil, prov, errs.ApplyOptions(\n\t\t\terrs.ForbiddenErr(err, \"%s\", err.Error()),\n\t\t\terrs.WithKeyVal(\"csr\", csr),\n\t\t\terrs.WithKeyVal(\"signOptions\", signOpts),\n\t\t)\n\t}\n\n\tcrt, err := x509util.NewCertificate(csr, certOptions...)\n\tif err != nil {\n\t\tvar te *x509util.TemplateError\n\t\tswitch {\n\t\tcase errors.As(err, &te):\n\t\t\treturn nil, prov, errs.ApplyOptions(\n\t\t\t\terrs.BadRequestErr(err, \"%s\", err.Error()),\n\t\t\t\terrs.WithKeyVal(\"csr\", csr),\n\t\t\t\terrs.WithKeyVal(\"signOptions\", signOpts),\n\t\t\t)\n\t\tcase strings.HasPrefix(err.Error(), \"error unmarshaling certificate\"):\n\t\t\t// explicitly check for unmarshaling errors, which are most probably caused by JSON template (syntax) errors\n\t\t\treturn nil, prov, errs.InternalServerErr(templatingError(err),\n\t\t\t\terrs.WithKeyVal(\"csr\", csr),\n\t\t\t\terrs.WithKeyVal(\"signOptions\", signOpts),\n\t\t\t\terrs.WithMessage(\"error applying certificate template\"),\n\t\t\t)\n\t\tdefault:\n\t\t\treturn nil, prov, errs.Wrap(http.StatusInternalServerError, err, \"authority.Sign\", opts...)\n\t\t}\n\t}\n\n\t// Certificate modifiers before validation\n\tleaf := crt.GetCertificate()\n\n\t// Set default subject\n\tif err := withDefaultASN1DN(a.config.AuthorityConfig.Template).Modify(leaf, signOpts); err != nil {\n\t\treturn nil, prov, errs.ApplyOptions(\n\t\t\terrs.ForbiddenErr(err, \"error creating certificate\"),\n\t\t\topts...,\n\t\t)\n\t}\n\n\tfor _, m := range certModifiers {\n\t\tif err := m.Modify(leaf, signOpts); err != nil {\n\t\t\treturn nil, prov, errs.ApplyOptions(\n\t\t\t\terrs.ForbiddenErr(err, \"error creating certificate\"),\n\t\t\t\topts...,\n\t\t\t)\n\t\t}\n\t}\n\n\t// Certificate validation.\n\tfor _, v := range certValidators {\n\t\tif err := v.Valid(leaf, signOpts); err != nil {\n\t\t\treturn nil, prov, errs.ApplyOptions(\n\t\t\t\terrs.ForbiddenErr(err, \"error validating certificate\"),\n\t\t\t\topts...,\n\t\t\t)\n\t\t}\n\t}\n\n\t// Certificate modifiers after validation\n\tfor _, m := range certEnforcers {\n\t\tif err = m.Enforce(leaf); err != nil {\n\t\t\treturn nil, prov, errs.ApplyOptions(\n\t\t\t\terrs.ForbiddenErr(err, \"error creating certificate\"),\n\t\t\t\topts...,\n\t\t\t)\n\t\t}\n\t}\n\n\t// Process injected modifiers after validation\n\tfor _, m := range a.x509Enforcers {\n\t\tif err = m.Enforce(leaf); err != nil {\n\t\t\treturn nil, prov, errs.ApplyOptions(\n\t\t\t\terrs.ForbiddenErr(err, \"error creating certificate\"),\n\t\t\t\topts...,\n\t\t\t)\n\t\t}\n\t}\n\n\t// Check if authority is allowed to sign the certificate\n\tif err = a.isAllowedToSignX509Certificate(leaf); err != nil {\n\t\tvar ee *errs.Error\n\t\tif errors.As(err, &ee) {\n\t\t\treturn nil, prov, errs.ApplyOptions(ee, opts...)\n\t\t}\n\t\treturn nil, prov, errs.InternalServerErr(err,\n\t\t\terrs.WithKeyVal(\"csr\", csr),\n\t\t\terrs.WithKeyVal(\"signOptions\", signOpts),\n\t\t\terrs.WithMessage(\"error creating certificate\"),\n\t\t)\n\t}\n\n\t// Send certificate to webhooks for authorization\n\tif err := a.callAuthorizingWebhooksX509(ctx, prov, webhookCtl, crt, leaf, attData); err != nil {\n\t\treturn nil, prov, errs.ApplyOptions(\n\t\t\terrs.ForbiddenErr(err, \"error creating certificate\"),\n\t\t\topts...,\n\t\t)\n\t}\n\n\t// Sign certificate\n\tlifetime := leaf.NotAfter.Sub(leaf.NotBefore.Add(signOpts.Backdate))\n\n\tresp, err := a.x509CAService.CreateCertificate(&casapi.CreateCertificateRequest{\n\t\tTemplate:    leaf,\n\t\tCSR:         csr,\n\t\tLifetime:    lifetime,\n\t\tBackdate:    signOpts.Backdate,\n\t\tProvisioner: pInfo,\n\t})\n\tif err != nil {\n\t\treturn nil, prov, errs.Wrap(http.StatusInternalServerError, err, \"authority.Sign; error creating certificate\", opts...)\n\t}\n\n\tchain := append([]*x509.Certificate{resp.Certificate}, resp.CertificateChain...)\n\n\t// Wrap provisioner with extra information, if not nil\n\tif prov != nil {\n\t\tprov = wrapProvisioner(prov, attData)\n\t}\n\n\t// Store certificate in the db.\n\tif err := a.storeCertificate(prov, chain); err != nil && !errors.Is(err, db.ErrNotImplemented) {\n\t\treturn nil, prov, errs.Wrap(http.StatusInternalServerError, err, \"authority.Sign; error storing certificate in db\", opts...)\n\t}\n\n\treturn chain, prov, nil\n}\n\n// isAllowedToSignX509Certificate checks if the Authority is allowed\n// to sign the X.509 certificate.\nfunc (a *Authority) isAllowedToSignX509Certificate(cert *x509.Certificate) error {\n\tif err := a.constraintsEngine.ValidateCertificate(cert); err != nil {\n\t\treturn err\n\t}\n\treturn a.policyEngine.IsX509CertificateAllowed(cert)\n}\n\n// AreSANsAllowed evaluates the provided sans against the\n// authority X.509 policy.\nfunc (a *Authority) AreSANsAllowed(_ context.Context, sans []string) error {\n\treturn a.policyEngine.AreSANsAllowed(sans)\n}\n\n// Renew creates a new Certificate identical to the old certificate, except with\n// a validity window that begins 'now'.\nfunc (a *Authority) Renew(oldCert *x509.Certificate) ([]*x509.Certificate, error) {\n\treturn a.RenewContext(context.Background(), oldCert, nil)\n}\n\n// Rekey is used for rekeying and renewing based on the public key. If the\n// public key is 'nil' then it's assumed that the cert should be renewed using\n// the existing public key. If the public key is not 'nil' then it's assumed\n// that the cert should be rekeyed.\n//\n// For both Rekey and Renew all other attributes of the new certificate should\n// match the old certificate. The exceptions are 'AuthorityKeyId' (which may\n// have changed), 'SubjectKeyId' (different in case of rekey), and\n// 'NotBefore/NotAfter' (the validity duration of the new certificate should be\n// equal to the old one, but starting 'now').\nfunc (a *Authority) Rekey(oldCert *x509.Certificate, pk crypto.PublicKey) ([]*x509.Certificate, error) {\n\treturn a.RenewContext(context.Background(), oldCert, pk)\n}\n\n// RenewContext creates a new certificate identical to the old one, but it can\n// optionally replace the public key with the given one. When running on RA\n// mode, it can only renew a certificate using a renew token instead.\n//\n// For both rekey and renew operations, all other attributes of the new\n// certificate should match the old certificate. The exceptions are\n// 'AuthorityKeyId' (which may have changed), 'SubjectKeyId' (different in case\n// of rekey), and 'NotBefore/NotAfter' (the validity duration of the new\n// certificate should be equal to the old one, but starting 'now').\nfunc (a *Authority) RenewContext(ctx context.Context, oldCert *x509.Certificate, pk crypto.PublicKey) ([]*x509.Certificate, error) {\n\tchain, prov, err := a.renewContext(ctx, oldCert, pk)\n\tif pk == nil {\n\t\ta.meter.X509Renewed(chain, prov, err)\n\t} else {\n\t\ta.meter.X509Rekeyed(chain, prov, err)\n\t}\n\treturn chain, err\n}\n\nfunc (a *Authority) renewContext(ctx context.Context, oldCert *x509.Certificate, pk crypto.PublicKey) ([]*x509.Certificate, provisioner.Interface, error) {\n\tisRekey := (pk != nil)\n\topts := []errs.Option{\n\t\terrs.WithKeyVal(\"serialNumber\", oldCert.SerialNumber.String()),\n\t}\n\n\t// Check step provisioner extensions\n\tprov, err := a.authorizeRenew(ctx, oldCert)\n\tif err != nil {\n\t\treturn nil, prov, errs.StatusCodeError(http.StatusInternalServerError, err, opts...)\n\t}\n\n\t// Durations\n\tbackdate := a.config.AuthorityConfig.Backdate.Duration\n\tduration := oldCert.NotAfter.Sub(oldCert.NotBefore)\n\tlifetime := duration - backdate\n\n\t// Create new certificate from previous values.\n\t// Issuer, NotBefore, NotAfter and SubjectKeyId will be set by the CAS.\n\tnewCert := &x509.Certificate{\n\t\tRawSubject:                  oldCert.RawSubject,\n\t\tKeyUsage:                    oldCert.KeyUsage,\n\t\tUnhandledCriticalExtensions: oldCert.UnhandledCriticalExtensions,\n\t\tExtKeyUsage:                 oldCert.ExtKeyUsage,\n\t\tUnknownExtKeyUsage:          oldCert.UnknownExtKeyUsage,\n\t\tBasicConstraintsValid:       oldCert.BasicConstraintsValid,\n\t\tIsCA:                        oldCert.IsCA,\n\t\tMaxPathLen:                  oldCert.MaxPathLen,\n\t\tMaxPathLenZero:              oldCert.MaxPathLenZero,\n\t\tOCSPServer:                  oldCert.OCSPServer,\n\t\tIssuingCertificateURL:       oldCert.IssuingCertificateURL,\n\t\tPermittedDNSDomainsCritical: oldCert.PermittedDNSDomainsCritical,\n\t\tPermittedEmailAddresses:     oldCert.PermittedEmailAddresses,\n\t\tDNSNames:                    oldCert.DNSNames,\n\t\tEmailAddresses:              oldCert.EmailAddresses,\n\t\tIPAddresses:                 oldCert.IPAddresses,\n\t\tURIs:                        oldCert.URIs,\n\t\tPermittedDNSDomains:         oldCert.PermittedDNSDomains,\n\t\tExcludedDNSDomains:          oldCert.ExcludedDNSDomains,\n\t\tPermittedIPRanges:           oldCert.PermittedIPRanges,\n\t\tExcludedIPRanges:            oldCert.ExcludedIPRanges,\n\t\tExcludedEmailAddresses:      oldCert.ExcludedEmailAddresses,\n\t\tPermittedURIDomains:         oldCert.PermittedURIDomains,\n\t\tExcludedURIDomains:          oldCert.ExcludedURIDomains,\n\t\tCRLDistributionPoints:       oldCert.CRLDistributionPoints,\n\t\tPolicyIdentifiers:           oldCert.PolicyIdentifiers,\n\t}\n\n\tif isRekey {\n\t\tnewCert.PublicKey = pk\n\t} else {\n\t\tnewCert.PublicKey = oldCert.PublicKey\n\t}\n\n\t// Copy all extensions except:\n\t//\n\t//  1. Authority Key Identifier - This one might be different if we rotate\n\t//  the intermediate certificate and it will cause a TLS bad certificate\n\t//  error.\n\t//\n\t//  2. Subject Key Identifier, if rekey - For rekey, SubjectKeyIdentifier\n\t//  extension will be calculated for the new public key by\n\t//  x509util.CreateCertificate()\n\tfor _, ext := range oldCert.Extensions {\n\t\tif ext.Id.Equal(oidAuthorityKeyIdentifier) {\n\t\t\tcontinue\n\t\t}\n\t\tif ext.Id.Equal(oidSubjectKeyIdentifier) && isRekey {\n\t\t\tnewCert.SubjectKeyId = nil\n\t\t\tcontinue\n\t\t}\n\t\tnewCert.ExtraExtensions = append(newCert.ExtraExtensions, ext)\n\t}\n\n\t// Check if the certificate is allowed to be renewed, name constraints might\n\t// change over time.\n\t//\n\t// TODO(hslatman,maraino): consider adding policies too and consider if\n\t// RenewSSH should check policies.\n\tif err = a.constraintsEngine.ValidateCertificate(newCert); err != nil {\n\t\tvar ee *errs.Error\n\t\tswitch {\n\t\tcase errors.As(err, &ee):\n\t\t\treturn nil, prov, errs.StatusCodeError(ee.StatusCode(), err, opts...)\n\t\tdefault:\n\t\t\treturn nil, prov, errs.InternalServerErr(err,\n\t\t\t\terrs.WithKeyVal(\"serialNumber\", oldCert.SerialNumber.String()),\n\t\t\t\terrs.WithMessage(\"error renewing certificate\"),\n\t\t\t)\n\t\t}\n\t}\n\n\t// The token can optionally be in the context. If the CA is running in RA\n\t// mode, this can be used to renew a certificate.\n\ttoken, _ := TokenFromContext(ctx)\n\n\tresp, err := a.x509CAService.RenewCertificate(&casapi.RenewCertificateRequest{\n\t\tTemplate: newCert,\n\t\tLifetime: lifetime,\n\t\tBackdate: backdate,\n\t\tToken:    token,\n\t})\n\tif err != nil {\n\t\treturn nil, prov, errs.StatusCodeError(http.StatusInternalServerError, err, opts...)\n\t}\n\n\tchain := append([]*x509.Certificate{resp.Certificate}, resp.CertificateChain...)\n\n\tif err = a.storeRenewedCertificate(oldCert, chain); err != nil && !errors.Is(err, db.ErrNotImplemented) {\n\t\treturn nil, prov, errs.StatusCodeError(http.StatusInternalServerError, err, opts...)\n\t}\n\n\treturn chain, prov, nil\n}\n\n// storeCertificate allows to use an extension of the db.AuthDB interface that\n// can log the full chain of certificates.\n//\n// TODO: at some point we should replace the db.AuthDB interface to implement\n// `StoreCertificate(...*x509.Certificate) error` instead of just\n// `StoreCertificate(*x509.Certificate) error`.\nfunc (a *Authority) storeCertificate(prov provisioner.Interface, fullchain []*x509.Certificate) error {\n\ttype certificateChainStorer interface {\n\t\tStoreCertificateChain(provisioner.Interface, ...*x509.Certificate) error\n\t}\n\ttype certificateChainSimpleStorer interface {\n\t\tStoreCertificateChain(...*x509.Certificate) error\n\t}\n\n\t// Store certificate in linkedca\n\tswitch s := a.adminDB.(type) {\n\tcase certificateChainStorer:\n\t\treturn s.StoreCertificateChain(prov, fullchain...)\n\tcase certificateChainSimpleStorer:\n\t\treturn s.StoreCertificateChain(fullchain...)\n\t}\n\n\t// Store certificate in local db\n\tswitch s := a.db.(type) {\n\tcase certificateChainStorer:\n\t\treturn s.StoreCertificateChain(prov, fullchain...)\n\tcase certificateChainSimpleStorer:\n\t\treturn s.StoreCertificateChain(fullchain...)\n\tcase db.CertificateStorer:\n\t\treturn s.StoreCertificate(fullchain[0])\n\tdefault:\n\t\treturn nil\n\t}\n}\n\n// storeRenewedCertificate allows to use an extension of the db.AuthDB interface\n// that can log if a certificate has been renewed or rekeyed.\n//\n// TODO: at some point we should implement this in the standard implementation.\nfunc (a *Authority) storeRenewedCertificate(oldCert *x509.Certificate, fullchain []*x509.Certificate) error {\n\ttype renewedCertificateChainStorer interface {\n\t\tStoreRenewedCertificate(*x509.Certificate, ...*x509.Certificate) error\n\t}\n\n\t// Store certificate in linkedca\n\tif s, ok := a.adminDB.(renewedCertificateChainStorer); ok {\n\t\treturn s.StoreRenewedCertificate(oldCert, fullchain...)\n\t}\n\n\t// Store certificate in local db\n\tswitch s := a.db.(type) {\n\tcase renewedCertificateChainStorer:\n\t\treturn s.StoreRenewedCertificate(oldCert, fullchain...)\n\tcase db.CertificateStorer:\n\t\treturn s.StoreCertificate(fullchain[0])\n\tdefault:\n\t\treturn nil\n\t}\n}\n\n// RevokeOptions are the options for the Revoke API.\ntype RevokeOptions struct {\n\tSerial      string\n\tReason      string\n\tReasonCode  int\n\tPassiveOnly bool\n\tMTLS        bool\n\tACME        bool\n\tCrt         *x509.Certificate\n\tOTT         string\n}\n\n// Revoke revokes a certificate.\n//\n// NOTE: Only supports passive revocation - prevent existing certificates from\n// being renewed.\n//\n// TODO: Add OCSP and CRL support.\nfunc (a *Authority) Revoke(ctx context.Context, revokeOpts *RevokeOptions) error {\n\topts := []interface{}{\n\t\terrs.WithKeyVal(\"serialNumber\", revokeOpts.Serial),\n\t\terrs.WithKeyVal(\"reasonCode\", revokeOpts.ReasonCode),\n\t\terrs.WithKeyVal(\"reason\", revokeOpts.Reason),\n\t\terrs.WithKeyVal(\"passiveOnly\", revokeOpts.PassiveOnly),\n\t\terrs.WithKeyVal(\"MTLS\", revokeOpts.MTLS),\n\t\terrs.WithKeyVal(\"ACME\", revokeOpts.ACME),\n\t\terrs.WithKeyVal(\"context\", provisioner.MethodFromContext(ctx).String()),\n\t}\n\tif revokeOpts.MTLS || revokeOpts.ACME {\n\t\topts = append(opts, errs.WithKeyVal(\"certificate\", base64.StdEncoding.EncodeToString(revokeOpts.Crt.Raw)))\n\t} else {\n\t\topts = append(opts, errs.WithKeyVal(\"token\", revokeOpts.OTT))\n\t}\n\n\trci := &db.RevokedCertificateInfo{\n\t\tSerial:     revokeOpts.Serial,\n\t\tReasonCode: revokeOpts.ReasonCode,\n\t\tReason:     revokeOpts.Reason,\n\t\tMTLS:       revokeOpts.MTLS,\n\t\tACME:       revokeOpts.ACME,\n\t\tRevokedAt:  time.Now().UTC(),\n\t}\n\n\t// For X509 CRLs attempt to get the expiration date of the certificate.\n\tif provisioner.MethodFromContext(ctx) == provisioner.RevokeMethod {\n\t\tif revokeOpts.Crt == nil {\n\t\t\tcert, err := a.db.GetCertificate(revokeOpts.Serial)\n\t\t\tif err == nil {\n\t\t\t\trci.ExpiresAt = cert.NotAfter\n\t\t\t}\n\t\t} else {\n\t\t\trci.ExpiresAt = revokeOpts.Crt.NotAfter\n\t\t}\n\t}\n\n\t// If not mTLS nor ACME, then get the TokenID of the token.\n\tif !revokeOpts.MTLS && !revokeOpts.ACME {\n\t\ttoken, err := jose.ParseSigned(revokeOpts.OTT)\n\t\tif err != nil {\n\t\t\treturn errs.Wrap(http.StatusUnauthorized, err, \"authority.Revoke; error parsing token\", opts...)\n\t\t}\n\n\t\t// Get claims w/out verification.\n\t\tvar claims Claims\n\t\tif err = token.UnsafeClaimsWithoutVerification(&claims); err != nil {\n\t\t\treturn errs.Wrap(http.StatusUnauthorized, err, \"authority.Revoke\", opts...)\n\t\t}\n\n\t\t// Verify that the serial in the token matches the serial from the request.\n\t\tif revokeOpts.Serial != claims.Subject {\n\t\t\treturn errs.ApplyOptions(\n\t\t\t\terrs.Forbidden(\n\t\t\t\t\t\"request serial number %q and token subject %q do not match\",\n\t\t\t\t\trevokeOpts.Serial, claims.Subject,\n\t\t\t\t), opts...,\n\t\t\t)\n\t\t}\n\n\t\t// This method will also validate the audiences for JWK provisioners.\n\t\tp, err := a.LoadProvisionerByToken(token, &claims.Claims)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\trci.ProvisionerID = p.GetID()\n\t\trci.TokenID, err = p.GetTokenID(revokeOpts.OTT)\n\t\tif err != nil && !errors.Is(err, provisioner.ErrAllowTokenReuse) {\n\t\t\treturn errs.Wrap(http.StatusInternalServerError, err, \"authority.Revoke; could not get ID for token\")\n\t\t}\n\t\topts = append(opts,\n\t\t\terrs.WithKeyVal(\"provisionerID\", rci.ProvisionerID),\n\t\t\terrs.WithKeyVal(\"tokenID\", rci.TokenID),\n\t\t)\n\t} else if p, err := a.LoadProvisionerByCertificate(revokeOpts.Crt); err == nil {\n\t\t// Load the Certificate provisioner if one exists.\n\t\trci.ProvisionerID = p.GetID()\n\t\topts = append(opts, errs.WithKeyVal(\"provisionerID\", rci.ProvisionerID))\n\t}\n\n\tfailRevoke := func(err error) error {\n\t\tswitch {\n\t\tcase errors.Is(err, db.ErrNotImplemented):\n\t\t\treturn errs.NotImplemented(\"authority.Revoke; no persistence layer configured\", opts...)\n\t\tcase errors.Is(err, db.ErrAlreadyExists):\n\t\t\treturn errs.ApplyOptions(\n\t\t\t\terrs.BadRequest(\"certificate with serial number '%s' is already revoked\", rci.Serial),\n\t\t\t\topts...,\n\t\t\t)\n\t\tdefault:\n\t\t\treturn errs.Wrap(http.StatusInternalServerError, err, \"authority.Revoke\", opts...)\n\t\t}\n\t}\n\n\tif provisioner.MethodFromContext(ctx) == provisioner.SSHRevokeMethod {\n\t\tif err := a.revokeSSH(nil, rci); err != nil {\n\t\t\treturn failRevoke(err)\n\t\t}\n\t} else {\n\t\t// Revoke an X.509 certificate using CAS. If the certificate is not\n\t\t// provided we will try to read it from the db. If the read fails we\n\t\t// won't throw an error as it will be responsibility of the CAS\n\t\t// implementation to require a certificate.\n\t\tvar revokedCert *x509.Certificate\n\t\tif revokeOpts.Crt != nil {\n\t\t\trevokedCert = revokeOpts.Crt\n\t\t} else if rci.Serial != \"\" {\n\t\t\trevokedCert, _ = a.db.GetCertificate(rci.Serial)\n\t\t}\n\n\t\t// CAS operation, note that SoftCAS (default) is a noop.\n\t\t// The revoke happens when this is stored in the db.\n\t\t_, err := a.x509CAService.RevokeCertificate(&casapi.RevokeCertificateRequest{\n\t\t\tCertificate:  revokedCert,\n\t\t\tSerialNumber: rci.Serial,\n\t\t\tReason:       rci.Reason,\n\t\t\tReasonCode:   rci.ReasonCode,\n\t\t\tPassiveOnly:  revokeOpts.PassiveOnly,\n\t\t})\n\t\tif err != nil {\n\t\t\treturn errs.Wrap(http.StatusInternalServerError, err, \"authority.Revoke\", opts...)\n\t\t}\n\n\t\t// Save as revoked in the Db.\n\t\tif err := a.revoke(revokedCert, rci); err != nil {\n\t\t\treturn failRevoke(err)\n\t\t}\n\n\t\t// Generate a new CRL so CRL requesters will always get an up-to-date\n\t\t// CRL whenever they request it.\n\t\tif a.config.CRL.IsEnabled() && a.config.CRL.GenerateOnRevoke {\n\t\t\tif err := a.GenerateCertificateRevocationList(); err != nil {\n\t\t\t\treturn errs.Wrap(http.StatusInternalServerError, err, \"authority.Revoke\", opts...)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (a *Authority) revoke(crt *x509.Certificate, rci *db.RevokedCertificateInfo) error {\n\tif lca, ok := a.adminDB.(interface {\n\t\tRevoke(*x509.Certificate, *db.RevokedCertificateInfo) error\n\t}); ok {\n\t\treturn lca.Revoke(crt, rci)\n\t}\n\treturn a.db.Revoke(rci)\n}\n\nfunc (a *Authority) revokeSSH(crt *ssh.Certificate, rci *db.RevokedCertificateInfo) error {\n\tif lca, ok := a.adminDB.(interface {\n\t\tRevokeSSH(*ssh.Certificate, *db.RevokedCertificateInfo) error\n\t}); ok {\n\t\treturn lca.RevokeSSH(crt, rci)\n\t}\n\treturn a.db.RevokeSSH(rci)\n}\n\n// CertificateRevocationListInfo contains a CRL in DER format and associated metadata.\ntype CertificateRevocationListInfo struct {\n\tNumber    int64\n\tExpiresAt time.Time\n\tDuration  time.Duration\n\tData      []byte\n}\n\n// GetCertificateRevocationList will return the currently generated CRL from the DB, or a not implemented\n// error if the underlying AuthDB does not support CRLs\nfunc (a *Authority) GetCertificateRevocationList() (*CertificateRevocationListInfo, error) {\n\tif !a.config.CRL.IsEnabled() {\n\t\treturn nil, errs.Wrap(http.StatusNotFound, errors.Errorf(\"Certificate Revocation Lists are not enabled\"), \"authority.GetCertificateRevocationList\")\n\t}\n\n\tcrlDB, ok := a.db.(db.CertificateRevocationListDB)\n\tif !ok {\n\t\treturn nil, errs.Wrap(http.StatusNotImplemented, errors.Errorf(\"Database does not support Certificate Revocation Lists\"), \"authority.GetCertificateRevocationList\")\n\t}\n\n\tcrlInfo, err := crlDB.GetCRL()\n\tif err != nil {\n\t\treturn nil, errs.Wrap(http.StatusInternalServerError, err, \"authority.GetCertificateRevocationList\")\n\t}\n\n\treturn &CertificateRevocationListInfo{\n\t\tNumber:    crlInfo.Number,\n\t\tExpiresAt: crlInfo.ExpiresAt,\n\t\tDuration:  crlInfo.Duration,\n\t\tData:      crlInfo.DER,\n\t}, nil\n}\n\n// GenerateCertificateRevocationList generates a DER representation of a signed CRL and stores it in the\n// database. Returns nil if CRL generation has been disabled in the config\nfunc (a *Authority) GenerateCertificateRevocationList() error {\n\tif !a.config.CRL.IsEnabled() {\n\t\treturn nil\n\t}\n\n\tcrlDB, ok := a.db.(db.CertificateRevocationListDB)\n\tif !ok {\n\t\treturn errors.Errorf(\"Database does not support CRL generation\")\n\t}\n\n\t// some CAS may not implement the CRLGenerator interface, so check before we proceed\n\tcaCRLGenerator, ok := a.x509CAService.(casapi.CertificateAuthorityCRLGenerator)\n\tif !ok {\n\t\treturn errors.Errorf(\"CA does not support CRL Generation\")\n\t}\n\n\t// use a mutex to ensure only one CRL is generated at a time to avoid\n\t// concurrency issues\n\ta.crlMutex.Lock()\n\tdefer a.crlMutex.Unlock()\n\n\tcrlInfo, err := crlDB.GetCRL()\n\tif err != nil && !database.IsErrNotFound(err) {\n\t\treturn errors.Wrap(err, \"could not retrieve CRL from database\")\n\t}\n\n\tnow := time.Now().Truncate(time.Second).UTC()\n\trevokedList, err := crlDB.GetRevokedCertificates()\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"could not retrieve revoked certificates list from database\")\n\t}\n\n\t// Number is a monotonically increasing integer (essentially the CRL version\n\t// number) that we need to keep track of and increase every time we generate\n\t// a new CRL\n\tvar bn big.Int\n\tif crlInfo != nil {\n\t\tbn.SetInt64(crlInfo.Number + 1)\n\t}\n\n\t// Convert our database db.RevokedCertificateInfo types into the x509\n\t// representation ready for the CAS to sign it\n\tvar revokedCertificateEntries []x509.RevocationListEntry\n\tskipExpiredTime := now.Add(-config.DefaultCRLExpiredDuration)\n\tfor _, revokedCert := range *revokedList {\n\t\t// skip expired certificates\n\t\tif !revokedCert.ExpiresAt.IsZero() && revokedCert.ExpiresAt.Before(skipExpiredTime) {\n\t\t\tcontinue\n\t\t}\n\n\t\tvar sn big.Int\n\t\tsn.SetString(revokedCert.Serial, 10)\n\t\trevokedCertificateEntries = append(revokedCertificateEntries, x509.RevocationListEntry{\n\t\t\tSerialNumber:   &sn,\n\t\t\tRevocationTime: revokedCert.RevokedAt,\n\t\t\tReasonCode:     revokedCert.ReasonCode,\n\t\t})\n\t}\n\n\tvar updateDuration time.Duration\n\tif a.config.CRL.CacheDuration != nil {\n\t\tupdateDuration = a.config.CRL.CacheDuration.Duration\n\t} else if crlInfo != nil {\n\t\tupdateDuration = crlInfo.Duration\n\t}\n\n\t// Create a RevocationList representation ready for the CAS to sign\n\t// TODO: allow SignatureAlgorithm to be specified?\n\trevocationList := x509.RevocationList{\n\t\tSignatureAlgorithm:        0,\n\t\tRevokedCertificateEntries: revokedCertificateEntries,\n\t\tNumber:                    &bn,\n\t\tThisUpdate:                now,\n\t\tNextUpdate:                now.Add(updateDuration),\n\t}\n\n\t// Set CRL IDP to config item, otherwise, leave as default\n\tvar fullName string\n\tif a.config.CRL.IDPurl != \"\" {\n\t\tfullName = a.config.CRL.IDPurl\n\t} else {\n\t\tfullName = a.config.Audience(\"/1.0/crl\")[0]\n\t}\n\n\t// Add distribution point.\n\t//\n\t// Note that this is currently using the port 443 by default.\n\tif b, err := marshalDistributionPoint(fullName); err == nil {\n\t\trevocationList.ExtraExtensions = []pkix.Extension{\n\t\t\t{Id: oidExtensionIssuingDistributionPoint, Critical: true, Value: b},\n\t\t}\n\t}\n\n\tcertificateRevocationList, err := caCRLGenerator.CreateCRL(&casapi.CreateCRLRequest{RevocationList: &revocationList})\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"could not create CRL\")\n\t}\n\n\t// Create a new db.CertificateRevocationListInfo, which stores the new Number we just generated, the\n\t// expiry time, duration, and the DER-encoded CRL\n\tnewCRLInfo := db.CertificateRevocationListInfo{\n\t\tNumber:    bn.Int64(),\n\t\tExpiresAt: revocationList.NextUpdate,\n\t\tDER:       certificateRevocationList.CRL,\n\t\tDuration:  updateDuration,\n\t}\n\n\t// Store the CRL in the database ready for retrieval by api endpoints\n\terr = crlDB.StoreCRL(&newCRLInfo)\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"could not store CRL in database\")\n\t}\n\n\treturn nil\n}\n\n// GetTLSCertificate creates a new leaf certificate to be used by the CA HTTPS server.\nfunc (a *Authority) GetTLSCertificate() (*tls.Certificate, error) {\n\tfatal := func(err error) (*tls.Certificate, error) {\n\t\treturn nil, errs.Wrap(http.StatusInternalServerError, err, \"authority.GetTLSCertificate\")\n\t}\n\n\t// Generate default key.\n\tpriv, err := keyutil.GenerateDefaultKey()\n\tif err != nil {\n\t\treturn fatal(err)\n\t}\n\tsigner, ok := priv.(crypto.Signer)\n\tif !ok {\n\t\treturn fatal(errors.New(\"private key is not a crypto.Signer\"))\n\t}\n\n\t// prepare the sans: IPv6 DNS hostname representations are converted to their IP representation\n\tsans := make([]string, len(a.config.DNSNames))\n\tfor i, san := range a.config.DNSNames {\n\t\tif strings.HasPrefix(san, \"[\") && strings.HasSuffix(san, \"]\") {\n\t\t\tif ip := net.ParseIP(san[1 : len(san)-1]); ip != nil {\n\t\t\t\tsan = ip.String()\n\t\t\t}\n\t\t}\n\t\tsans[i] = san\n\t}\n\n\t// Create initial certificate request.\n\tcr, err := x509util.CreateCertificateRequest(a.config.CommonName, sans, signer)\n\tif err != nil {\n\t\treturn fatal(err)\n\t}\n\n\t// Generate certificate template directly from the certificate request.\n\ttemplate, err := x509util.NewCertificate(cr)\n\tif err != nil {\n\t\treturn fatal(err)\n\t}\n\n\t// Get x509 certificate template, set validity and sign it.\n\tnow := time.Now()\n\tcertTpl := template.GetCertificate()\n\tcertTpl.NotBefore = now.Add(-1 * time.Minute)\n\tcertTpl.NotAfter = now.Add(24 * time.Hour)\n\n\t// Policy and constraints require this fields to be set. At this moment they\n\t// are only present in the extra extension.\n\tcertTpl.DNSNames = cr.DNSNames\n\tcertTpl.IPAddresses = cr.IPAddresses\n\tcertTpl.EmailAddresses = cr.EmailAddresses\n\tcertTpl.URIs = cr.URIs\n\n\t// Fail if name constraints do not allow the server names.\n\tif err := a.constraintsEngine.ValidateCertificate(certTpl); err != nil {\n\t\treturn fatal(err)\n\t}\n\n\t// Set the cert lifetime as follows:\n\t//   i) If the CA is not a StepCAS RA use 24h, else\n\t//  ii) if the CA is a StepCAS RA, leave the lifetime empty and\n\t//      let the provisioner of the CA decide the lifetime of the RA cert.\n\tvar lifetime time.Duration\n\tif casapi.TypeOf(a.x509CAService) != casapi.StepCAS {\n\t\tlifetime = 24 * time.Hour\n\t}\n\n\tresp, err := a.x509CAService.CreateCertificate(&casapi.CreateCertificateRequest{\n\t\tTemplate:       certTpl,\n\t\tCSR:            cr,\n\t\tLifetime:       lifetime,\n\t\tBackdate:       1 * time.Minute,\n\t\tIsCAServerCert: true,\n\t})\n\tif err != nil {\n\t\treturn fatal(err)\n\t}\n\n\t// Generate PEM blocks to create tls.Certificate\n\tpemBlocks := pem.EncodeToMemory(&pem.Block{\n\t\tType:  \"CERTIFICATE\",\n\t\tBytes: resp.Certificate.Raw,\n\t})\n\tfor _, crt := range resp.CertificateChain {\n\t\tpemBlocks = append(pemBlocks, pem.EncodeToMemory(&pem.Block{\n\t\t\tType:  \"CERTIFICATE\",\n\t\t\tBytes: crt.Raw,\n\t\t})...)\n\t}\n\tkeyPEM, err := pemutil.Serialize(priv)\n\tif err != nil {\n\t\treturn fatal(err)\n\t}\n\n\ttlsCrt, err := tls.X509KeyPair(pemBlocks, pem.EncodeToMemory(keyPEM))\n\tif err != nil {\n\t\treturn fatal(err)\n\t}\n\t// Set leaf certificate\n\ttlsCrt.Leaf = resp.Certificate\n\treturn &tlsCrt, nil\n}\n\n// RFC 5280, 5.2.5\ntype distributionPoint struct {\n\tDistributionPoint          distributionPointName `asn1:\"optional,tag:0\"`\n\tOnlyContainsUserCerts      bool                  `asn1:\"optional,tag:1\"`\n\tOnlyContainsCACerts        bool                  `asn1:\"optional,tag:2\"`\n\tOnlySomeReasons            asn1.BitString        `asn1:\"optional,tag:3\"`\n\tIndirectCRL                bool                  `asn1:\"optional,tag:4\"`\n\tOnlyContainsAttributeCerts bool                  `asn1:\"optional,tag:5\"`\n}\n\ntype distributionPointName struct {\n\tFullName     []asn1.RawValue  `asn1:\"optional,tag:0\"`\n\tRelativeName pkix.RDNSequence `asn1:\"optional,tag:1\"`\n}\n\n/*\nmarshalDistributionPoint currently marshals only DP, citing spec\nhttps://datatracker.ietf.org/doc/html/rfc5280#section-5.2.5:\n\n\tThat is, if onlyContainsUserCerts, onlyContainsCACerts, indirectCRL, and\n\tonlyContainsAttributeCerts are all FALSE, then either the\n\tdistributionPoint field or the onlySomeReasons field MUST be present.\n*/\nfunc marshalDistributionPoint(fullName string) ([]byte, error) {\n\treturn asn1.Marshal(distributionPoint{\n\t\tDistributionPoint: distributionPointName{\n\t\t\tFullName: []asn1.RawValue{\n\t\t\t\t{Class: 2, Tag: 6, Bytes: []byte(fullName)},\n\t\t\t},\n\t\t},\n\t})\n}\n\n// templatingError tries to extract more information about the cause of\n// an error related to (most probably) malformed template data and adds\n// this to the error message.\nfunc templatingError(err error) error {\n\tcause := errors.Cause(err)\n\tvar (\n\t\tsyntaxError *json.SyntaxError\n\t\ttypeError   *json.UnmarshalTypeError\n\t)\n\tif errors.As(err, &syntaxError) {\n\t\t// offset is arguably not super clear to the user, but it's the best we can do here\n\t\tcause = fmt.Errorf(\"%w at offset %d\", cause, syntaxError.Offset)\n\t} else if errors.As(err, &typeError) {\n\t\t// slightly rewriting the default error message to include the offset\n\t\tcause = fmt.Errorf(\"cannot unmarshal %s at offset %d into Go value of type %s\", typeError.Value, typeError.Offset, typeError.Type)\n\t}\n\treturn errors.Wrap(cause, \"error applying certificate template\")\n}\n\nfunc (a *Authority) callEnrichingWebhooksX509(ctx context.Context, prov provisioner.Interface, webhookCtl webhookController, attData *provisioner.AttestationData, csr *x509.CertificateRequest) (err error) {\n\tif webhookCtl == nil {\n\t\treturn\n\t}\n\tdefer func() { a.meter.X509WebhookEnriched(prov, err) }()\n\n\tvar attested *webhook.AttestationData\n\tif attData != nil {\n\t\tattested = &webhook.AttestationData{\n\t\t\tPermanentIdentifier: attData.PermanentIdentifier,\n\t\t}\n\t}\n\n\tvar whEnrichReq *webhook.RequestBody\n\tif whEnrichReq, err = webhook.NewRequestBody(\n\t\twebhook.WithX509CertificateRequest(csr),\n\t\twebhook.WithAttestationData(attested),\n\t); err == nil {\n\t\terr = webhookCtl.Enrich(ctx, whEnrichReq)\n\t}\n\n\treturn\n}\n\nfunc (a *Authority) callAuthorizingWebhooksX509(ctx context.Context, prov provisioner.Interface, webhookCtl webhookController, cert *x509util.Certificate, leaf *x509.Certificate, attData *provisioner.AttestationData) (err error) {\n\tif webhookCtl == nil {\n\t\treturn\n\t}\n\tdefer func() { a.meter.X509WebhookAuthorized(prov, err) }()\n\n\tvar attested *webhook.AttestationData\n\tif attData != nil {\n\t\tattested = &webhook.AttestationData{\n\t\t\tPermanentIdentifier: attData.PermanentIdentifier,\n\t\t}\n\t}\n\n\tvar whAuthBody *webhook.RequestBody\n\tif whAuthBody, err = webhook.NewRequestBody(\n\t\twebhook.WithX509Certificate(cert, leaf),\n\t\twebhook.WithAttestationData(attested),\n\t); err == nil {\n\t\terr = webhookCtl.Authorize(ctx, whAuthBody)\n\t}\n\n\treturn\n}\n"
  },
  {
    "path": "authority/tls_test.go",
    "content": "package authority\n\nimport (\n\t\"context\"\n\t\"crypto\"\n\t\"crypto/ecdsa\"\n\t\"crypto/elliptic\"\n\t\"crypto/rand\"\n\t\"crypto/sha1\" //nolint:gosec // used to create the Subject Key Identifier by RFC 5280\n\t\"crypto/x509\"\n\t\"crypto/x509/pkix\"\n\t\"encoding/asn1\"\n\t\"encoding/pem\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"reflect\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"go.step.sm/crypto/fingerprint\"\n\t\"go.step.sm/crypto/jose\"\n\t\"go.step.sm/crypto/keyutil\"\n\t\"go.step.sm/crypto/minica\"\n\t\"go.step.sm/crypto/pemutil\"\n\t\"go.step.sm/crypto/x509util\"\n\n\t\"github.com/smallstep/certificates/api/render\"\n\t\"github.com/smallstep/certificates/authority/config\"\n\t\"github.com/smallstep/certificates/authority/policy\"\n\t\"github.com/smallstep/certificates/authority/provisioner\"\n\t\"github.com/smallstep/certificates/cas/apiv1\"\n\t\"github.com/smallstep/certificates/cas/softcas\"\n\t\"github.com/smallstep/certificates/db\"\n\t\"github.com/smallstep/certificates/errs\"\n\t\"github.com/smallstep/nosql/database\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nvar (\n\tstepOIDRoot        = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 37476, 9000, 64}\n\tstepOIDProvisioner = append(asn1.ObjectIdentifier(nil), append(stepOIDRoot, 1)...)\n)\n\nconst provisionerTypeJWK = 1\n\ntype stepProvisionerASN1 struct {\n\tType         int\n\tName         []byte\n\tCredentialID []byte\n}\n\ntype certificateDurationEnforcer struct {\n\tNotBefore time.Time\n\tNotAfter  time.Time\n}\n\nfunc (m *certificateDurationEnforcer) Enforce(cert *x509.Certificate) error {\n\tcert.NotBefore = m.NotBefore\n\tcert.NotAfter = m.NotAfter\n\treturn nil\n}\n\ntype certificateChainDB struct {\n\tdb.MockAuthDB\n\tMStoreCertificateChain func(provisioner.Interface, ...*x509.Certificate) error\n}\n\nfunc (d *certificateChainDB) StoreCertificateChain(p provisioner.Interface, certs ...*x509.Certificate) error {\n\treturn d.MStoreCertificateChain(p, certs...)\n}\n\nfunc getDefaultIssuer(a *Authority) *x509.Certificate {\n\treturn a.x509CAService.(*softcas.SoftCAS).CertificateChain[len(a.x509CAService.(*softcas.SoftCAS).CertificateChain)-1]\n}\n\nfunc getDefaultSigner(a *Authority) crypto.Signer {\n\treturn a.x509CAService.(*softcas.SoftCAS).Signer\n}\n\nfunc generateCertificate(t *testing.T, commonName string, sans []string, opts ...interface{}) *x509.Certificate {\n\tt.Helper()\n\n\tpriv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)\n\trequire.NoError(t, err)\n\n\tcr, err := x509util.CreateCertificateRequest(commonName, sans, priv)\n\trequire.NoError(t, err)\n\n\ttemplate, err := x509util.NewCertificate(cr)\n\trequire.NoError(t, err)\n\n\tcert := template.GetCertificate()\n\tfor _, m := range opts {\n\t\tswitch m := m.(type) {\n\t\tcase provisioner.CertificateModifierFunc:\n\t\t\terr = m.Modify(cert, provisioner.SignOptions{})\n\t\t\trequire.NoError(t, err)\n\t\tcase signerFunc:\n\t\t\tcert, err = m(cert, priv.Public())\n\t\t\trequire.NoError(t, err)\n\t\tdefault:\n\t\t\trequire.Fail(t, \"\", \"unknown type %T\", m)\n\t\t}\n\n\t}\n\treturn cert\n}\n\nfunc generateRootCertificate(t *testing.T) (*x509.Certificate, crypto.Signer) {\n\tt.Helper()\n\tpriv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)\n\trequire.NoError(t, err)\n\n\tcr, err := x509util.CreateCertificateRequest(\"TestRootCA\", nil, priv)\n\trequire.NoError(t, err)\n\n\tdata := x509util.CreateTemplateData(\"TestRootCA\", nil)\n\ttemplate, err := x509util.NewCertificate(cr, x509util.WithTemplate(x509util.DefaultRootTemplate, data))\n\trequire.NoError(t, err)\n\n\tcert := template.GetCertificate()\n\tcert, err = x509util.CreateCertificate(cert, cert, priv.Public(), priv)\n\trequire.NoError(t, err)\n\treturn cert, priv\n}\n\nfunc generateIntermidiateCertificate(t *testing.T, issuer *x509.Certificate, signer crypto.Signer) (*x509.Certificate, crypto.Signer) {\n\tt.Helper()\n\tpriv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)\n\trequire.NoError(t, err)\n\n\tcr, err := x509util.CreateCertificateRequest(\"TestIntermediateCA\", nil, priv)\n\trequire.NoError(t, err)\n\n\tdata := x509util.CreateTemplateData(\"TestIntermediateCA\", nil)\n\ttemplate, err := x509util.NewCertificate(cr, x509util.WithTemplate(x509util.DefaultRootTemplate, data))\n\trequire.NoError(t, err)\n\n\tcert := template.GetCertificate()\n\tcert, err = x509util.CreateCertificate(cert, issuer, priv.Public(), signer)\n\trequire.NoError(t, err)\n\treturn cert, priv\n}\n\nfunc withSubject(sub pkix.Name) provisioner.CertificateModifierFunc {\n\treturn func(crt *x509.Certificate, _ provisioner.SignOptions) error {\n\t\tcrt.Subject = sub\n\t\treturn nil\n\t}\n}\n\nfunc withProvisionerOID(name, kid string) provisioner.CertificateModifierFunc {\n\treturn func(crt *x509.Certificate, _ provisioner.SignOptions) error {\n\t\tb, err := asn1.Marshal(stepProvisionerASN1{\n\t\t\tType:         provisionerTypeJWK,\n\t\t\tName:         []byte(name),\n\t\t\tCredentialID: []byte(kid),\n\t\t})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tcrt.ExtraExtensions = append(crt.ExtraExtensions, pkix.Extension{\n\t\t\tId:       stepOIDProvisioner,\n\t\t\tCritical: false,\n\t\t\tValue:    b,\n\t\t})\n\t\treturn nil\n\t}\n}\n\nfunc withNotBeforeNotAfter(notBefore, notAfter time.Time) provisioner.CertificateModifierFunc {\n\treturn func(crt *x509.Certificate, _ provisioner.SignOptions) error {\n\t\tcrt.NotBefore = notBefore\n\t\tcrt.NotAfter = notAfter\n\t\treturn nil\n\t}\n}\n\ntype signerFunc func(crt *x509.Certificate, pub crypto.PublicKey) (*x509.Certificate, error)\n\nfunc withSigner(issuer *x509.Certificate, signer crypto.Signer) signerFunc {\n\treturn func(crt *x509.Certificate, pub crypto.PublicKey) (*x509.Certificate, error) {\n\t\treturn x509util.CreateCertificate(crt, issuer, pub, signer)\n\t}\n}\n\nfunc getCSR(t *testing.T, priv interface{}, opts ...func(*x509.CertificateRequest)) *x509.CertificateRequest {\n\t_csr := &x509.CertificateRequest{\n\t\tSubject:  pkix.Name{CommonName: \"smallstep test\"},\n\t\tDNSNames: []string{\"test.smallstep.com\"},\n\t}\n\tfor _, opt := range opts {\n\t\topt(_csr)\n\t}\n\tcsrBytes, err := x509.CreateCertificateRequest(rand.Reader, _csr, priv)\n\trequire.NoError(t, err)\n\tcsr, err := x509.ParseCertificateRequest(csrBytes)\n\trequire.NoError(t, err)\n\treturn csr\n}\n\nfunc setExtraExtsCSR(exts []pkix.Extension) func(*x509.CertificateRequest) {\n\treturn func(csr *x509.CertificateRequest) {\n\t\tcsr.ExtraExtensions = exts\n\t}\n}\n\nfunc generateSubjectKeyID(pub crypto.PublicKey) ([]byte, error) {\n\tb, err := x509.MarshalPKIXPublicKey(pub)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error marshaling public key: %w\", err)\n\t}\n\tinfo := struct {\n\t\tAlgorithm        pkix.AlgorithmIdentifier\n\t\tSubjectPublicKey asn1.BitString\n\t}{}\n\tif _, err = asn1.Unmarshal(b, &info); err != nil {\n\t\treturn nil, fmt.Errorf(\"error unmarshaling public key: %w\", err)\n\t}\n\t//nolint:gosec // used to create the Subject Key Identifier by RFC 5280\n\thash := sha1.Sum(info.SubjectPublicKey.Bytes)\n\treturn hash[:], nil\n}\n\ntype basicConstraints struct {\n\tIsCA       bool `asn1:\"optional\"`\n\tMaxPathLen int  `asn1:\"optional,default:-1\"`\n}\n\ntype testEnforcer struct {\n\tenforcer func(*x509.Certificate) error\n}\n\nfunc (e *testEnforcer) Enforce(cert *x509.Certificate) error {\n\tif e.enforcer != nil {\n\t\treturn e.enforcer(cert)\n\t}\n\treturn nil\n}\n\nfunc assertHasPrefix(t *testing.T, s, p string) bool {\n\tt.Helper()\n\treturn assert.True(t, strings.HasPrefix(s, p), \"%q is not a prefix of %q\", p, s)\n}\n\nfunc TestAuthority_SignWithContext(t *testing.T) {\n\tpub, priv, err := keyutil.GenerateDefaultKeyPair()\n\trequire.NoError(t, err)\n\n\ta := testAuthority(t)\n\trequire.NoError(t, err)\n\ta.config.AuthorityConfig.Template = &ASN1DN{\n\t\tCountry:       \"Tazmania\",\n\t\tOrganization:  \"Acme Co\",\n\t\tLocality:      \"Landscapes\",\n\t\tProvince:      \"Sudden Cliffs\",\n\t\tStreetAddress: \"TNT\",\n\t\tCommonName:    \"test.smallstep.com\",\n\t}\n\n\tnb := time.Now()\n\tsignOpts := provisioner.SignOptions{\n\t\tNotBefore: provisioner.NewTimeDuration(nb),\n\t\tNotAfter:  provisioner.NewTimeDuration(nb.Add(time.Minute * 5)),\n\t\tBackdate:  1 * time.Minute,\n\t}\n\n\t// Create a token to get test extra opts.\n\tp := a.config.AuthorityConfig.Provisioners[1].(*provisioner.JWK)\n\tkey, err := jose.ReadKey(\"testdata/secrets/step_cli_key_priv.jwk\", jose.WithPassword([]byte(\"pass\")))\n\trequire.NoError(t, err)\n\ttoken, err := generateToken(\"smallstep test\", \"step-cli\", testAudiences.Sign[0], []string{\"test.smallstep.com\"}, time.Now(), key)\n\trequire.NoError(t, err)\n\tctx := provisioner.NewContextWithMethod(context.Background(), provisioner.SignMethod)\n\textraOpts, err := a.Authorize(ctx, token)\n\trequire.NoError(t, err)\n\n\ttype signTest struct {\n\t\tauth            *Authority\n\t\tcsr             *x509.CertificateRequest\n\t\tsignOpts        provisioner.SignOptions\n\t\textraOpts       []provisioner.SignOption\n\t\tnotBefore       time.Time\n\t\tnotAfter        time.Time\n\t\textensionsCount int\n\t\terr             error\n\t\tcode            int\n\t}\n\ttests := map[string]func(*testing.T) *signTest{\n\t\t\"fail invalid signature\": func(t *testing.T) *signTest {\n\t\t\tcsr := getCSR(t, priv)\n\t\t\tcsr.Signature = []byte(\"foo\")\n\t\t\treturn &signTest{\n\t\t\t\tauth:      a,\n\t\t\t\tcsr:       csr,\n\t\t\t\textraOpts: extraOpts,\n\t\t\t\tsignOpts:  signOpts,\n\t\t\t\terr:       errors.New(\"invalid certificate request\"),\n\t\t\t\tcode:      http.StatusBadRequest,\n\t\t\t}\n\t\t},\n\t\t\"fail invalid extra option\": func(t *testing.T) *signTest {\n\t\t\tcsr := getCSR(t, priv)\n\t\t\tcsr.Raw = []byte(\"foo\")\n\t\t\treturn &signTest{\n\t\t\t\tauth:      a,\n\t\t\t\tcsr:       csr,\n\t\t\t\textraOpts: append(extraOpts, \"42\"),\n\t\t\t\tsignOpts:  signOpts,\n\t\t\t\terr:       errors.New(\"authority.Sign; invalid extra option type string\"),\n\t\t\t\tcode:      http.StatusInternalServerError,\n\t\t\t}\n\t\t},\n\t\t\"fail merge default ASN1DN\": func(t *testing.T) *signTest {\n\t\t\t_a := testAuthority(t)\n\t\t\t_a.config.AuthorityConfig.Template = nil\n\t\t\tcsr := getCSR(t, priv)\n\t\t\treturn &signTest{\n\t\t\t\tauth:      _a,\n\t\t\t\tcsr:       csr,\n\t\t\t\textraOpts: extraOpts,\n\t\t\t\tsignOpts:  signOpts,\n\t\t\t\terr:       errors.New(\"default ASN1DN template cannot be nil\"),\n\t\t\t\tcode:      http.StatusForbidden,\n\t\t\t}\n\t\t},\n\t\t\"fail create cert\": func(t *testing.T) *signTest {\n\t\t\t_a := testAuthority(t)\n\t\t\t_a.x509CAService.(*softcas.SoftCAS).Signer = nil\n\t\t\tcsr := getCSR(t, priv)\n\t\t\treturn &signTest{\n\t\t\t\tauth:      _a,\n\t\t\t\tcsr:       csr,\n\t\t\t\textraOpts: extraOpts,\n\t\t\t\tsignOpts:  signOpts,\n\t\t\t\terr:       errors.New(\"authority.Sign; error creating certificate\"),\n\t\t\t\tcode:      http.StatusInternalServerError,\n\t\t\t}\n\t\t},\n\t\t\"fail provisioner duration claim\": func(t *testing.T) *signTest {\n\t\t\tcsr := getCSR(t, priv)\n\t\t\t_signOpts := provisioner.SignOptions{\n\t\t\t\tNotBefore: provisioner.NewTimeDuration(nb),\n\t\t\t\tNotAfter:  provisioner.NewTimeDuration(nb.Add(time.Hour * 25)),\n\t\t\t}\n\t\t\treturn &signTest{\n\t\t\t\tauth:      a,\n\t\t\t\tcsr:       csr,\n\t\t\t\textraOpts: extraOpts,\n\t\t\t\tsignOpts:  _signOpts,\n\t\t\t\terr:       errors.New(\"requested duration of 25h0m0s is more than the authorized maximum certificate duration of 24h1m0s\"),\n\t\t\t\tcode:      http.StatusForbidden,\n\t\t\t}\n\t\t},\n\t\t\"fail validate sans when adding common name not in claims\": func(t *testing.T) *signTest {\n\t\t\tcsr := getCSR(t, priv, func(csr *x509.CertificateRequest) {\n\t\t\t\tcsr.DNSNames = append(csr.DNSNames, csr.Subject.CommonName)\n\t\t\t})\n\t\t\treturn &signTest{\n\t\t\t\tauth:      a,\n\t\t\t\tcsr:       csr,\n\t\t\t\textraOpts: extraOpts,\n\t\t\t\tsignOpts:  signOpts,\n\t\t\t\terr:       errors.New(\"certificate request does not contain the valid DNS names - got [test.smallstep.com smallstep test], want [test.smallstep.com]\"),\n\t\t\t\tcode:      http.StatusForbidden,\n\t\t\t}\n\t\t},\n\t\t\"fail rsa key too short\": func(t *testing.T) *signTest {\n\t\t\tshortRSAKeyPEM := `-----BEGIN CERTIFICATE REQUEST-----\nMIIBhDCB7gIBADAZMRcwFQYDVQQDEw5zbWFsbHN0ZXAgdGVzdDCBnzANBgkqhkiG\n9w0BAQEFAAOBjQAwgYkCgYEA5JlgH99HvHHsCD6XTqqYj3bXU2oIlnYGoLVs7IJ4\nk205rv5/YWky2gjdpIv0Tnaf3o57IJ891lB7GiyO5iHIEUv5N9dVzrdUboyzk2uZ\n7JMMNB43CSLB2oNuwJjLeAM/yBzlhRnvpKjrNSfSV+cH54FXdnbFbcTFMStnjqKG\nMeECAwEAAaAsMCoGCSqGSIb3DQEJDjEdMBswGQYDVR0RBBIwEIIOc21hbGxzdGVw\nIHRlc3QwDQYJKoZIhvcNAQELBQADgYEAKwsbr8Zfcq05DgOoJ//cXMFK1SP8ktRU\nN2++E8Ww0Tet9oyNRArqxxS/UyVio63D3wynzRAB25PFGpYG1cN4b81Gv/foFUT6\nW5kR63lNVHBHgQmv5mA8YFsfrJHstaz5k727v2LMHEYIf5/3i16d5zhuxUoaPTYr\nZYtQ9Ot36qc=\n-----END CERTIFICATE REQUEST-----`\n\t\t\tblock, _ := pem.Decode([]byte(shortRSAKeyPEM))\n\t\t\trequire.NoError(t, err)\n\t\t\tcsr, err := x509.ParseCertificateRequest(block.Bytes)\n\t\t\trequire.NoError(t, err)\n\n\t\t\treturn &signTest{\n\t\t\t\tauth:      a,\n\t\t\t\tcsr:       csr,\n\t\t\t\textraOpts: extraOpts,\n\t\t\t\tsignOpts:  signOpts,\n\t\t\t\terr:       errors.New(\"certificate request RSA key must be at least 2048 bits (256 bytes)\"),\n\t\t\t\tcode:      http.StatusForbidden,\n\t\t\t}\n\t\t},\n\t\t\"fail store cert in db\": func(t *testing.T) *signTest {\n\t\t\tcsr := getCSR(t, priv)\n\t\t\t_a := testAuthority(t)\n\t\t\t_a.db = &db.MockAuthDB{\n\t\t\t\tMStoreCertificate: func(crt *x509.Certificate) error {\n\t\t\t\t\treturn errors.New(\"force\")\n\t\t\t\t},\n\t\t\t}\n\t\t\treturn &signTest{\n\t\t\t\tauth:      _a,\n\t\t\t\tcsr:       csr,\n\t\t\t\textraOpts: extraOpts,\n\t\t\t\tsignOpts:  signOpts,\n\t\t\t\terr:       errors.New(\"authority.Sign; error storing certificate in db: force\"),\n\t\t\t\tcode:      http.StatusInternalServerError,\n\t\t\t}\n\t\t},\n\t\t\"fail custom template\": func(t *testing.T) *signTest {\n\t\t\tcsr := getCSR(t, priv)\n\t\t\ttestAuthority := testAuthority(t)\n\t\t\tp, ok := testAuthority.provisioners.Load(\"step-cli:4UELJx8e0aS9m0CH3fZ0EB7D5aUPICb759zALHFejvc\")\n\t\t\tif !ok {\n\t\t\t\tt.Fatal(\"provisioner not found\")\n\t\t\t}\n\t\t\tp.(*provisioner.JWK).Options = &provisioner.Options{\n\t\t\t\tX509: &provisioner.X509Options{Template: `{{ fail \"fail message\" }}`},\n\t\t\t}\n\t\t\ttestExtraOpts, err := testAuthority.Authorize(ctx, token)\n\t\t\trequire.NoError(t, err)\n\t\t\ttestAuthority.db = &db.MockAuthDB{\n\t\t\t\tMStoreCertificate: func(crt *x509.Certificate) error {\n\t\t\t\t\tassert.Equal(t, \"smallstep test\", crt.Subject.CommonName)\n\t\t\t\t\treturn nil\n\t\t\t\t},\n\t\t\t}\n\t\t\treturn &signTest{\n\t\t\t\tauth:      testAuthority,\n\t\t\t\tcsr:       csr,\n\t\t\t\textraOpts: testExtraOpts,\n\t\t\t\tsignOpts:  signOpts,\n\t\t\t\terr:       errors.New(\"fail message\"),\n\t\t\t\tcode:      http.StatusBadRequest,\n\t\t\t}\n\t\t},\n\t\t\"fail bad JSON syntax template file\": func(t *testing.T) *signTest {\n\t\t\tcsr := getCSR(t, priv)\n\t\t\ttestAuthority := testAuthority(t)\n\t\t\tp, ok := testAuthority.provisioners.Load(\"step-cli:4UELJx8e0aS9m0CH3fZ0EB7D5aUPICb759zALHFejvc\")\n\t\t\tif !ok {\n\t\t\t\tt.Fatal(\"provisioner not found\")\n\t\t\t}\n\t\t\tp.(*provisioner.JWK).Options = &provisioner.Options{\n\t\t\t\tX509: &provisioner.X509Options{\n\t\t\t\t\tTemplateFile: \"./testdata/templates/badjsonsyntax.tpl\",\n\t\t\t\t},\n\t\t\t}\n\t\t\ttestExtraOpts, err := testAuthority.Authorize(ctx, token)\n\t\t\trequire.NoError(t, err)\n\t\t\ttestAuthority.db = &db.MockAuthDB{\n\t\t\t\tMStoreCertificate: func(crt *x509.Certificate) error {\n\t\t\t\t\tassert.Equal(t, \"smallstep test\", crt.Subject.CommonName)\n\t\t\t\t\treturn nil\n\t\t\t\t},\n\t\t\t}\n\t\t\treturn &signTest{\n\t\t\t\tauth:      testAuthority,\n\t\t\t\tcsr:       csr,\n\t\t\t\textraOpts: testExtraOpts,\n\t\t\t\tsignOpts:  signOpts,\n\t\t\t\terr:       errors.New(\"error applying certificate template: invalid character\"),\n\t\t\t\tcode:      http.StatusInternalServerError,\n\t\t\t}\n\t\t},\n\t\t\"fail bad JSON value template file\": func(t *testing.T) *signTest {\n\t\t\tcsr := getCSR(t, priv)\n\t\t\ttestAuthority := testAuthority(t)\n\t\t\tp, ok := testAuthority.provisioners.Load(\"step-cli:4UELJx8e0aS9m0CH3fZ0EB7D5aUPICb759zALHFejvc\")\n\t\t\tif !ok {\n\t\t\t\tt.Fatal(\"provisioner not found\")\n\t\t\t}\n\t\t\tp.(*provisioner.JWK).Options = &provisioner.Options{\n\t\t\t\tX509: &provisioner.X509Options{\n\t\t\t\t\tTemplateFile: \"./testdata/templates/badjsonvalue.tpl\",\n\t\t\t\t},\n\t\t\t}\n\t\t\ttestExtraOpts, err := testAuthority.Authorize(ctx, token)\n\t\t\trequire.NoError(t, err)\n\t\t\ttestAuthority.db = &db.MockAuthDB{\n\t\t\t\tMStoreCertificate: func(crt *x509.Certificate) error {\n\t\t\t\t\tassert.Equal(t, \"smallstep test\", crt.Subject.CommonName)\n\t\t\t\t\treturn nil\n\t\t\t\t},\n\t\t\t}\n\t\t\treturn &signTest{\n\t\t\t\tauth:      testAuthority,\n\t\t\t\tcsr:       csr,\n\t\t\t\textraOpts: testExtraOpts,\n\t\t\t\tsignOpts:  signOpts,\n\t\t\t\terr:       errors.New(\"error applying certificate template: cannot unmarshal\"),\n\t\t\t\tcode:      http.StatusInternalServerError,\n\t\t\t}\n\t\t},\n\t\t\"fail with provisioner enforcer\": func(t *testing.T) *signTest {\n\t\t\tcsr := getCSR(t, priv)\n\t\t\taa := testAuthority(t)\n\t\t\taa.db = &db.MockAuthDB{\n\t\t\t\tMStoreCertificate: func(crt *x509.Certificate) error {\n\t\t\t\t\tassert.Equal(t, \"smallstep test\", crt.Subject.CommonName)\n\t\t\t\t\treturn nil\n\t\t\t\t},\n\t\t\t}\n\n\t\t\treturn &signTest{\n\t\t\t\tauth: aa,\n\t\t\t\tcsr:  csr,\n\t\t\t\textraOpts: append(extraOpts, &testEnforcer{\n\t\t\t\t\tenforcer: func(crt *x509.Certificate) error { return fmt.Errorf(\"an error\") },\n\t\t\t\t}),\n\t\t\t\tsignOpts: signOpts,\n\t\t\t\terr:      errors.New(\"error creating certificate\"),\n\t\t\t\tcode:     http.StatusForbidden,\n\t\t\t}\n\t\t},\n\t\t\"fail with custom enforcer\": func(t *testing.T) *signTest {\n\t\t\tcsr := getCSR(t, priv)\n\t\t\taa := testAuthority(t, WithX509Enforcers(&testEnforcer{\n\t\t\t\tenforcer: func(cert *x509.Certificate) error {\n\t\t\t\t\treturn fmt.Errorf(\"an error\")\n\t\t\t\t},\n\t\t\t}))\n\t\t\taa.db = &db.MockAuthDB{\n\t\t\t\tMStoreCertificate: func(crt *x509.Certificate) error {\n\t\t\t\t\tassert.Equal(t, \"smallstep test\", crt.Subject.CommonName)\n\t\t\t\t\treturn nil\n\t\t\t\t},\n\t\t\t}\n\t\t\treturn &signTest{\n\t\t\t\tauth:      aa,\n\t\t\t\tcsr:       csr,\n\t\t\t\textraOpts: extraOpts,\n\t\t\t\tsignOpts:  signOpts,\n\t\t\t\terr:       errors.New(\"error creating certificate\"),\n\t\t\t\tcode:      http.StatusForbidden,\n\t\t\t}\n\t\t},\n\t\t\"fail with policy\": func(t *testing.T) *signTest {\n\t\t\tcsr := getCSR(t, priv)\n\t\t\taa := testAuthority(t)\n\t\t\taa.config.AuthorityConfig.Template = a.config.AuthorityConfig.Template\n\t\t\taa.db = &db.MockAuthDB{\n\t\t\t\tMStoreCertificate: func(crt *x509.Certificate) error {\n\t\t\t\t\tfmt.Println(crt.Subject)\n\t\t\t\t\tassert.Equal(t, \"smallstep test\", crt.Subject.CommonName)\n\t\t\t\t\treturn nil\n\t\t\t\t},\n\t\t\t}\n\t\t\toptions := &policy.Options{\n\t\t\t\tX509: &policy.X509PolicyOptions{\n\t\t\t\t\tDeniedNames: &policy.X509NameOptions{\n\t\t\t\t\t\tDNSDomains: []string{\"test.smallstep.com\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t\tengine, err := policy.New(options)\n\t\t\trequire.NoError(t, err)\n\t\t\taa.policyEngine = engine\n\t\t\treturn &signTest{\n\t\t\t\tauth:            aa,\n\t\t\t\tcsr:             csr,\n\t\t\t\textraOpts:       extraOpts,\n\t\t\t\tsignOpts:        signOpts,\n\t\t\t\tnotBefore:       signOpts.NotBefore.Time().Truncate(time.Second),\n\t\t\t\tnotAfter:        signOpts.NotAfter.Time().Truncate(time.Second),\n\t\t\t\textensionsCount: 6,\n\t\t\t\terr:             errors.New(\"dns name \\\"test.smallstep.com\\\" not allowed\"),\n\t\t\t\tcode:            http.StatusForbidden,\n\t\t\t}\n\t\t},\n\t\t\"fail enriching webhooks\": func(t *testing.T) *signTest {\n\t\t\tcsr := getCSR(t, priv)\n\t\t\tcsr.Raw = []byte(\"foo\")\n\t\t\treturn &signTest{\n\t\t\t\tauth:            a,\n\t\t\t\tcsr:             csr,\n\t\t\t\textensionsCount: 7,\n\t\t\t\textraOpts: append(extraOpts, &mockWebhookController{\n\t\t\t\t\tenrichErr: provisioner.ErrWebhookDenied,\n\t\t\t\t}),\n\t\t\t\tsignOpts: signOpts,\n\t\t\t\terr:      provisioner.ErrWebhookDenied,\n\t\t\t\tcode:     http.StatusForbidden,\n\t\t\t}\n\t\t},\n\t\t\"fail authorizing webhooks\": func(t *testing.T) *signTest {\n\t\t\tcsr := getCSR(t, priv)\n\t\t\tcsr.Raw = []byte(\"foo\")\n\t\t\treturn &signTest{\n\t\t\t\tauth:            a,\n\t\t\t\tcsr:             csr,\n\t\t\t\textensionsCount: 7,\n\t\t\t\textraOpts: append(extraOpts, &mockWebhookController{\n\t\t\t\t\tauthorizeErr: provisioner.ErrWebhookDenied,\n\t\t\t\t}),\n\t\t\t\tsignOpts: signOpts,\n\t\t\t\terr:      provisioner.ErrWebhookDenied,\n\t\t\t\tcode:     http.StatusForbidden,\n\t\t\t}\n\t\t},\n\t\t\"fail with cnf\": func(t *testing.T) *signTest {\n\t\t\tcsr := getCSR(t, priv)\n\n\t\t\tauth := testAuthority(t)\n\t\t\tauth.config.AuthorityConfig.Template = a.config.AuthorityConfig.Template\n\t\t\tauth.db = &db.MockAuthDB{\n\t\t\t\tMUseToken: func(id, tok string) (bool, error) {\n\t\t\t\t\treturn true, nil\n\t\t\t\t},\n\t\t\t\tMStoreCertificate: func(crt *x509.Certificate) error {\n\t\t\t\t\tassert.Equal(t, crt.Subject.CommonName, \"smallstep test\")\n\t\t\t\t\tassert.Equal(t, crt.DNSNames, []string{\"test.smallstep.com\"})\n\t\t\t\t\treturn nil\n\t\t\t\t},\n\t\t\t}\n\n\t\t\t// Create a token with cnf\n\t\t\ttok, err := generateCustomToken(\"smallstep test\", \"step-cli\", testAudiences.Sign[0], key, nil, map[string]any{\n\t\t\t\t\"sans\": []string{\"test.smallstep.com\"},\n\t\t\t\t\"cnf\":  map[string]any{\"x5rt#S256\": \"bad-fingerprint\"},\n\t\t\t})\n\t\t\trequire.NoError(t, err)\n\n\t\t\topts, err := auth.Authorize(ctx, tok)\n\t\t\trequire.NoError(t, err)\n\n\t\t\treturn &signTest{\n\t\t\t\tauth:      auth,\n\t\t\t\tcsr:       csr,\n\t\t\t\textraOpts: opts,\n\t\t\t\tsignOpts:  signOpts,\n\t\t\t\tnotBefore: signOpts.NotBefore.Time().Truncate(time.Second),\n\t\t\t\tnotAfter:  signOpts.NotAfter.Time().Truncate(time.Second),\n\t\t\t\terr:       errors.New(`certificate request fingerprint does not match \"bad-fingerprint\"`),\n\t\t\t\tcode:      http.StatusForbidden,\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) *signTest {\n\t\t\tcsr := getCSR(t, priv)\n\t\t\t_a := testAuthority(t)\n\t\t\t_a.db = &db.MockAuthDB{\n\t\t\t\tMStoreCertificate: func(crt *x509.Certificate) error {\n\t\t\t\t\tassert.Equal(t, \"smallstep test\", crt.Subject.CommonName)\n\t\t\t\t\treturn nil\n\t\t\t\t},\n\t\t\t}\n\t\t\treturn &signTest{\n\t\t\t\tauth:            a,\n\t\t\t\tcsr:             csr,\n\t\t\t\textraOpts:       extraOpts,\n\t\t\t\tsignOpts:        signOpts,\n\t\t\t\tnotBefore:       signOpts.NotBefore.Time().Truncate(time.Second),\n\t\t\t\tnotAfter:        signOpts.NotAfter.Time().Truncate(time.Second),\n\t\t\t\textensionsCount: 6,\n\t\t\t}\n\t\t},\n\t\t\"ok with enforced modifier\": func(t *testing.T) *signTest {\n\t\t\tbcExt := pkix.Extension{}\n\t\t\tbcExt.Id = asn1.ObjectIdentifier{2, 5, 29, 19}\n\t\t\tbcExt.Critical = false\n\t\t\tbcExt.Value, err = asn1.Marshal(basicConstraints{IsCA: true, MaxPathLen: 4})\n\t\t\trequire.NoError(t, err)\n\n\t\t\tcsr := getCSR(t, priv, setExtraExtsCSR([]pkix.Extension{\n\t\t\t\tbcExt,\n\t\t\t\t{Id: stepOIDProvisioner, Value: []byte(\"foo\")},\n\t\t\t\t{Id: []int{1, 1, 1}, Value: []byte(\"bar\")}}))\n\t\t\tnow := time.Now().UTC()\n\t\t\t//nolint:gocritic\n\t\t\tenforcedExtraOptions := append(extraOpts, &certificateDurationEnforcer{\n\t\t\t\tNotBefore: now,\n\t\t\t\tNotAfter:  now.Add(365 * 24 * time.Hour),\n\t\t\t})\n\t\t\t_a := testAuthority(t)\n\t\t\t_a.db = &db.MockAuthDB{\n\t\t\t\tMStoreCertificate: func(crt *x509.Certificate) error {\n\t\t\t\t\tassert.Equal(t, \"smallstep test\", crt.Subject.CommonName)\n\t\t\t\t\treturn nil\n\t\t\t\t},\n\t\t\t}\n\t\t\treturn &signTest{\n\t\t\t\tauth:            a,\n\t\t\t\tcsr:             csr,\n\t\t\t\textraOpts:       enforcedExtraOptions,\n\t\t\t\tsignOpts:        signOpts,\n\t\t\t\tnotBefore:       now.Truncate(time.Second),\n\t\t\t\tnotAfter:        now.Add(365 * 24 * time.Hour).Truncate(time.Second),\n\t\t\t\textensionsCount: 6,\n\t\t\t}\n\t\t},\n\t\t\"ok with custom template\": func(t *testing.T) *signTest {\n\t\t\tcsr := getCSR(t, priv)\n\t\t\ttestAuthority := testAuthority(t)\n\t\t\ttestAuthority.config.AuthorityConfig.Template = a.config.AuthorityConfig.Template\n\t\t\tp, ok := testAuthority.provisioners.Load(\"step-cli:4UELJx8e0aS9m0CH3fZ0EB7D5aUPICb759zALHFejvc\")\n\t\t\tif !ok {\n\t\t\t\tt.Fatal(\"provisioner not found\")\n\t\t\t}\n\t\t\tp.(*provisioner.JWK).Options = &provisioner.Options{\n\t\t\t\tX509: &provisioner.X509Options{Template: `{\n\t\t\t\t\t\"subject\": {{toJson .Subject}},\n\t\t\t\t\t\"dnsNames\": {{ toJson .Insecure.CR.DNSNames }},\n\t\t\t\t\t\"keyUsage\": [\"digitalSignature\"],\n\t\t\t\t\t\"extKeyUsage\": [\"serverAuth\",\"clientAuth\"]\n\t\t\t\t}`},\n\t\t\t}\n\t\t\ttestExtraOpts, err := testAuthority.Authorize(ctx, token)\n\t\t\trequire.NoError(t, err)\n\t\t\ttestAuthority.db = &db.MockAuthDB{\n\t\t\t\tMStoreCertificate: func(crt *x509.Certificate) error {\n\t\t\t\t\tassert.Equal(t, \"smallstep test\", crt.Subject.CommonName)\n\t\t\t\t\treturn nil\n\t\t\t\t},\n\t\t\t}\n\t\t\treturn &signTest{\n\t\t\t\tauth:            testAuthority,\n\t\t\t\tcsr:             csr,\n\t\t\t\textraOpts:       testExtraOpts,\n\t\t\t\tsignOpts:        signOpts,\n\t\t\t\tnotBefore:       signOpts.NotBefore.Time().Truncate(time.Second),\n\t\t\t\tnotAfter:        signOpts.NotAfter.Time().Truncate(time.Second),\n\t\t\t\textensionsCount: 6,\n\t\t\t}\n\t\t},\n\t\t\"ok with enriching webhook\": func(t *testing.T) *signTest {\n\t\t\tcsr := getCSR(t, priv)\n\t\t\ttestAuthority := testAuthority(t)\n\t\t\ttestAuthority.config.AuthorityConfig.Template = a.config.AuthorityConfig.Template\n\t\t\tp, ok := testAuthority.provisioners.Load(\"step-cli:4UELJx8e0aS9m0CH3fZ0EB7D5aUPICb759zALHFejvc\")\n\t\t\tif !ok {\n\t\t\t\tt.Fatal(\"provisioner not found\")\n\t\t\t}\n\t\t\tp.(*provisioner.JWK).Options = &provisioner.Options{\n\t\t\t\tX509: &provisioner.X509Options{Template: `{\n\t\t\t\t\t\"subject\": {\"commonName\": {{ toJson .Webhooks.people.role }} },\n\t\t\t\t\t\"dnsNames\": {{ toJson .Insecure.CR.DNSNames }},\n\t\t\t\t\t\"keyUsage\": [\"digitalSignature\"],\n\t\t\t\t\t\"extKeyUsage\": [\"serverAuth\",\"clientAuth\"]\n\t\t\t\t}`},\n\t\t\t}\n\t\t\ttestExtraOpts, err := testAuthority.Authorize(ctx, token)\n\t\t\trequire.NoError(t, err)\n\t\t\ttestAuthority.db = &db.MockAuthDB{\n\t\t\t\tMStoreCertificate: func(crt *x509.Certificate) error {\n\t\t\t\t\tassert.Equal(t, \"smallstep test\", crt.Subject.CommonName)\n\t\t\t\t\treturn nil\n\t\t\t\t},\n\t\t\t}\n\t\t\tfor i, o := range testExtraOpts {\n\t\t\t\tif wc, ok := o.(*provisioner.WebhookController); ok {\n\t\t\t\t\ttestExtraOpts[i] = &mockWebhookController{\n\t\t\t\t\t\ttemplateData: wc.TemplateData,\n\t\t\t\t\t\trespData:     map[string]any{\"people\": map[string]any{\"role\": \"smallstep test\"}},\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn &signTest{\n\t\t\t\tauth:            testAuthority,\n\t\t\t\tcsr:             csr,\n\t\t\t\textraOpts:       testExtraOpts,\n\t\t\t\tsignOpts:        signOpts,\n\t\t\t\tnotBefore:       signOpts.NotBefore.Time().Truncate(time.Second),\n\t\t\t\tnotAfter:        signOpts.NotAfter.Time().Truncate(time.Second),\n\t\t\t\textensionsCount: 6,\n\t\t\t}\n\t\t},\n\t\t\"ok/csr with no template critical SAN extension\": func(t *testing.T) *signTest {\n\t\t\tcsr := getCSR(t, priv, func(csr *x509.CertificateRequest) {\n\t\t\t\tcsr.Subject = pkix.Name{}\n\t\t\t}, func(csr *x509.CertificateRequest) {\n\t\t\t\tcsr.DNSNames = []string{\"foo\", \"bar\"}\n\t\t\t})\n\t\t\tnow := time.Now().UTC()\n\t\t\tenforcedExtraOptions := []provisioner.SignOption{&certificateDurationEnforcer{\n\t\t\t\tNotBefore: now,\n\t\t\t\tNotAfter:  now.Add(365 * 24 * time.Hour),\n\t\t\t}}\n\t\t\t_a := testAuthority(t)\n\t\t\t_a.config.AuthorityConfig.Template = &ASN1DN{}\n\t\t\t_a.db = &db.MockAuthDB{\n\t\t\t\tMStoreCertificate: func(crt *x509.Certificate) error {\n\t\t\t\t\tassert.Equal(t, pkix.Name{}, crt.Subject)\n\t\t\t\t\treturn nil\n\t\t\t\t},\n\t\t\t}\n\t\t\treturn &signTest{\n\t\t\t\tauth:            _a,\n\t\t\t\tcsr:             csr,\n\t\t\t\textraOpts:       enforcedExtraOptions,\n\t\t\t\tsignOpts:        provisioner.SignOptions{},\n\t\t\t\tnotBefore:       now.Truncate(time.Second),\n\t\t\t\tnotAfter:        now.Add(365 * 24 * time.Hour).Truncate(time.Second),\n\t\t\t\textensionsCount: 5,\n\t\t\t}\n\t\t},\n\t\t\"ok with custom enforcer\": func(t *testing.T) *signTest {\n\t\t\tcsr := getCSR(t, priv)\n\t\t\taa := testAuthority(t, WithX509Enforcers(&testEnforcer{\n\t\t\t\tenforcer: func(cert *x509.Certificate) error {\n\t\t\t\t\tcert.CRLDistributionPoints = []string{\"http://ca.example.org/leaf.crl\"}\n\t\t\t\t\treturn nil\n\t\t\t\t},\n\t\t\t}))\n\t\t\taa.config.AuthorityConfig.Template = a.config.AuthorityConfig.Template\n\t\t\taa.db = &db.MockAuthDB{\n\t\t\t\tMStoreCertificate: func(crt *x509.Certificate) error {\n\t\t\t\t\tassert.Equal(t, \"smallstep test\", crt.Subject.CommonName)\n\t\t\t\t\tassert.Equal(t, []string{\"http://ca.example.org/leaf.crl\"}, crt.CRLDistributionPoints)\n\t\t\t\t\treturn nil\n\t\t\t\t},\n\t\t\t}\n\t\t\treturn &signTest{\n\t\t\t\tauth:            aa,\n\t\t\t\tcsr:             csr,\n\t\t\t\textraOpts:       extraOpts,\n\t\t\t\tsignOpts:        signOpts,\n\t\t\t\tnotBefore:       signOpts.NotBefore.Time().Truncate(time.Second),\n\t\t\t\tnotAfter:        signOpts.NotAfter.Time().Truncate(time.Second),\n\t\t\t\textensionsCount: 7,\n\t\t\t}\n\t\t},\n\t\t\"ok with policy\": func(t *testing.T) *signTest {\n\t\t\tcsr := getCSR(t, priv)\n\t\t\taa := testAuthority(t)\n\t\t\taa.config.AuthorityConfig.Template = a.config.AuthorityConfig.Template\n\t\t\taa.db = &db.MockAuthDB{\n\t\t\t\tMStoreCertificate: func(crt *x509.Certificate) error {\n\t\t\t\t\tassert.Equal(t, crt.Subject.CommonName, \"smallstep test\")\n\t\t\t\t\treturn nil\n\t\t\t\t},\n\t\t\t}\n\t\t\toptions := &policy.Options{\n\t\t\t\tX509: &policy.X509PolicyOptions{\n\t\t\t\t\tAllowedNames: &policy.X509NameOptions{\n\t\t\t\t\t\tCommonNames: []string{\"smallstep test\"},\n\t\t\t\t\t\tDNSDomains:  []string{\"*.smallstep.com\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t\tengine, err := policy.New(options)\n\t\t\trequire.NoError(t, err)\n\t\t\taa.policyEngine = engine\n\t\t\treturn &signTest{\n\t\t\t\tauth:            aa,\n\t\t\t\tcsr:             csr,\n\t\t\t\textraOpts:       extraOpts,\n\t\t\t\tsignOpts:        signOpts,\n\t\t\t\tnotBefore:       signOpts.NotBefore.Time().Truncate(time.Second),\n\t\t\t\tnotAfter:        signOpts.NotAfter.Time().Truncate(time.Second),\n\t\t\t\textensionsCount: 6,\n\t\t\t}\n\t\t},\n\t\t\"ok with attestation data\": func(t *testing.T) *signTest {\n\t\t\tcsr := getCSR(t, priv)\n\t\t\taa := testAuthority(t)\n\t\t\taa.config.AuthorityConfig.Template = a.config.AuthorityConfig.Template\n\t\t\taa.db = &certificateChainDB{\n\t\t\t\tMStoreCertificateChain: func(prov provisioner.Interface, certs ...*x509.Certificate) error {\n\t\t\t\t\tp, ok := prov.(attProvisioner)\n\t\t\t\t\tif assert.True(t, ok) {\n\t\t\t\t\t\tassert.Equal(t, &provisioner.AttestationData{\n\t\t\t\t\t\t\tPermanentIdentifier: \"1234567890\",\n\t\t\t\t\t\t}, p.AttestationData())\n\t\t\t\t\t}\n\t\t\t\t\tif assert.Len(t, certs, 2) {\n\t\t\t\t\t\tassert.Equal(t, \"smallstep test\", certs[0].Subject.CommonName)\n\t\t\t\t\t\tassert.Equal(t, \"smallstep Intermediate CA\", certs[1].Subject.CommonName)\n\t\t\t\t\t}\n\t\t\t\t\treturn nil\n\t\t\t\t},\n\t\t\t}\n\n\t\t\treturn &signTest{\n\t\t\t\tauth: aa,\n\t\t\t\tcsr:  csr,\n\t\t\t\textraOpts: append(extraOpts, provisioner.AttestationData{\n\t\t\t\t\tPermanentIdentifier: \"1234567890\",\n\t\t\t\t}),\n\t\t\t\tsignOpts:        signOpts,\n\t\t\t\tnotBefore:       signOpts.NotBefore.Time().Truncate(time.Second),\n\t\t\t\tnotAfter:        signOpts.NotAfter.Time().Truncate(time.Second),\n\t\t\t\textensionsCount: 6,\n\t\t\t}\n\t\t},\n\t\t\"ok with cnf\": func(t *testing.T) *signTest {\n\t\t\tcsr := getCSR(t, priv)\n\t\t\tfingerprint, err := fingerprint.New(csr.Raw, crypto.SHA256, fingerprint.Base64RawURLFingerprint)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tauth := testAuthority(t)\n\t\t\tauth.config.AuthorityConfig.Template = a.config.AuthorityConfig.Template\n\t\t\tauth.db = &db.MockAuthDB{\n\t\t\t\tMUseToken: func(id, tok string) (bool, error) {\n\t\t\t\t\treturn true, nil\n\t\t\t\t},\n\t\t\t\tMStoreCertificate: func(crt *x509.Certificate) error {\n\t\t\t\t\tassert.Equal(t, crt.Subject.CommonName, \"smallstep test\")\n\t\t\t\t\tassert.Equal(t, crt.DNSNames, []string{\"test.smallstep.com\"})\n\t\t\t\t\treturn nil\n\t\t\t\t},\n\t\t\t}\n\n\t\t\t// Create a token with cnf\n\t\t\ttok, err := generateCustomToken(\"smallstep test\", \"step-cli\", testAudiences.Sign[0], key, nil, map[string]any{\n\t\t\t\t\"sans\": []string{\"test.smallstep.com\"},\n\t\t\t\t\"cnf\":  map[string]any{\"x5rt#S256\": fingerprint},\n\t\t\t})\n\t\t\trequire.NoError(t, err)\n\n\t\t\topts, err := auth.Authorize(ctx, tok)\n\t\t\trequire.NoError(t, err)\n\n\t\t\treturn &signTest{\n\t\t\t\tauth:            auth,\n\t\t\t\tcsr:             csr,\n\t\t\t\textraOpts:       opts,\n\t\t\t\tsignOpts:        signOpts,\n\t\t\t\tnotBefore:       signOpts.NotBefore.Time().Truncate(time.Second),\n\t\t\t\tnotAfter:        signOpts.NotAfter.Time().Truncate(time.Second),\n\t\t\t\textensionsCount: 6,\n\t\t\t}\n\t\t},\n\t}\n\n\tfor name, genTestCase := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ttc := genTestCase(t)\n\n\t\t\tcertChain, err := tc.auth.SignWithContext(context.Background(), tc.csr, tc.signOpts, tc.extraOpts...)\n\t\t\tif err != nil {\n\t\t\t\tif assert.NotNil(t, tc.err, fmt.Sprintf(\"unexpected error: %s\", err)) {\n\t\t\t\t\tassert.Nil(t, certChain)\n\t\t\t\t\tvar sc render.StatusCodedError\n\t\t\t\t\trequire.True(t, errors.As(err, &sc), \"error does not implement StatusCodedError interface\")\n\t\t\t\t\tassert.Equal(t, tc.code, sc.StatusCode())\n\t\t\t\t\tassertHasPrefix(t, err.Error(), tc.err.Error())\n\n\t\t\t\t\tvar ctxErr *errs.Error\n\t\t\t\t\trequire.True(t, errors.As(err, &ctxErr), \"error is not of type *errs.Error\")\n\t\t\t\t\tassert.Equal(t, tc.csr, ctxErr.Details[\"csr\"])\n\t\t\t\t\tassert.Equal(t, tc.signOpts, ctxErr.Details[\"signOptions\"])\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tleaf := certChain[0]\n\t\t\t\tintermediate := certChain[1]\n\t\t\t\tif assert.Nil(t, tc.err) {\n\t\t\t\t\tassert.Equal(t, tc.notBefore, leaf.NotBefore)\n\t\t\t\t\tassert.Equal(t, tc.notAfter, leaf.NotAfter)\n\t\t\t\t\ttmplt := a.config.AuthorityConfig.Template\n\t\t\t\t\tif tc.csr.Subject.CommonName == \"\" {\n\t\t\t\t\t\tassert.Equal(t, pkix.Name{}, leaf.Subject)\n\t\t\t\t\t} else {\n\t\t\t\t\t\tassert.Equal(t, pkix.Name{\n\t\t\t\t\t\t\tCountry:       []string{tmplt.Country},\n\t\t\t\t\t\t\tOrganization:  []string{tmplt.Organization},\n\t\t\t\t\t\t\tLocality:      []string{tmplt.Locality},\n\t\t\t\t\t\t\tStreetAddress: []string{tmplt.StreetAddress},\n\t\t\t\t\t\t\tProvince:      []string{tmplt.Province},\n\t\t\t\t\t\t\tCommonName:    \"smallstep test\",\n\t\t\t\t\t\t}.String(), leaf.Subject.String())\n\t\t\t\t\t\tassert.Equal(t, []string{\"test.smallstep.com\"}, leaf.DNSNames)\n\t\t\t\t\t}\n\t\t\t\t\tassert.Equal(t, intermediate.Subject, leaf.Issuer)\n\t\t\t\t\tassert.Equal(t, x509.ECDSAWithSHA256, leaf.SignatureAlgorithm)\n\t\t\t\t\tassert.Equal(t, x509.ECDSA, leaf.PublicKeyAlgorithm)\n\t\t\t\t\tassert.Equal(t, []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}, leaf.ExtKeyUsage)\n\n\t\t\t\t\tissuer := getDefaultIssuer(a)\n\t\t\t\t\tsubjectKeyID, err := generateSubjectKeyID(pub)\n\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t\tassert.Equal(t, subjectKeyID, leaf.SubjectKeyId)\n\t\t\t\t\tassert.Equal(t, issuer.SubjectKeyId, leaf.AuthorityKeyId)\n\n\t\t\t\t\t// Verify Provisioner OID\n\t\t\t\t\tfound := 0\n\t\t\t\t\tfor _, ext := range leaf.Extensions {\n\t\t\t\t\t\tswitch {\n\t\t\t\t\t\tcase ext.Id.Equal(stepOIDProvisioner):\n\t\t\t\t\t\t\tfound++\n\t\t\t\t\t\t\tval := stepProvisionerASN1{}\n\t\t\t\t\t\t\t_, err := asn1.Unmarshal(ext.Value, &val)\n\t\t\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t\t\t\tassert.Equal(t, provisionerTypeJWK, val.Type)\n\t\t\t\t\t\t\tassert.Equal(t, []byte(p.Name), val.Name)\n\t\t\t\t\t\t\tassert.Equal(t, []byte(p.Key.KeyID), val.CredentialID)\n\n\t\t\t\t\t\t// Basic Constraints\n\t\t\t\t\t\tcase ext.Id.Equal(asn1.ObjectIdentifier([]int{2, 5, 29, 19})):\n\t\t\t\t\t\t\tval := basicConstraints{}\n\t\t\t\t\t\t\t_, err := asn1.Unmarshal(ext.Value, &val)\n\t\t\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t\t\t\tassert.False(t, val.IsCA, false)\n\t\t\t\t\t\t\tassert.Equal(t, val.MaxPathLen, 0)\n\n\t\t\t\t\t\t// SAN extension\n\t\t\t\t\t\tcase ext.Id.Equal(asn1.ObjectIdentifier([]int{2, 5, 29, 17})):\n\t\t\t\t\t\t\tif tc.csr.Subject.CommonName == \"\" {\n\t\t\t\t\t\t\t\t// Empty CSR subject test does not use any provisioner extensions.\n\t\t\t\t\t\t\t\t// So provisioner ID ext will be missing.\n\t\t\t\t\t\t\t\tfound = 1\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tassert.Equal(t, found, 1)\n\t\t\t\t\trealIntermediate, err := x509.ParseCertificate(issuer.Raw)\n\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t\tassert.Equal(t, realIntermediate, intermediate)\n\t\t\t\t\tassert.Len(t, leaf.Extensions, tc.extensionsCount)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestAuthority_Renew(t *testing.T) {\n\ta := testAuthority(t)\n\ta.config.AuthorityConfig.Template = &ASN1DN{\n\t\tCountry:       \"Tazmania\",\n\t\tOrganization:  \"Acme Co\",\n\t\tLocality:      \"Landscapes\",\n\t\tProvince:      \"Sudden Cliffs\",\n\t\tStreetAddress: \"TNT\",\n\t\tCommonName:    \"renew\",\n\t}\n\n\tnow := time.Now().UTC()\n\tnb1 := now.Add(-time.Minute * 7)\n\tna1 := now.Add(time.Hour)\n\tso := &provisioner.SignOptions{\n\t\tNotBefore: provisioner.NewTimeDuration(nb1),\n\t\tNotAfter:  provisioner.NewTimeDuration(na1),\n\t}\n\n\tissuer := getDefaultIssuer(a)\n\tsigner := getDefaultSigner(a)\n\n\tcert := generateCertificate(t, \"renew\", []string{\"test.smallstep.com\", \"test\"},\n\t\twithNotBeforeNotAfter(so.NotBefore.Time(), so.NotAfter.Time()),\n\t\twithDefaultASN1DN(a.config.AuthorityConfig.Template),\n\t\twithProvisionerOID(\"Max\", a.config.AuthorityConfig.Provisioners[0].(*provisioner.JWK).Key.KeyID),\n\t\twithSigner(issuer, signer))\n\n\tcertExtraNames := generateCertificate(t, \"renew\", []string{\"test.smallstep.com\", \"test\"},\n\t\twithSubject(pkix.Name{\n\t\t\tCommonName: \"renew\",\n\t\t\tExtraNames: []pkix.AttributeTypeAndValue{\n\t\t\t\t{Type: asn1.ObjectIdentifier{0, 9, 2342, 19200300, 100, 1, 25}, Value: \"dc\"},\n\t\t\t},\n\t\t}),\n\t\twithNotBeforeNotAfter(so.NotBefore.Time(), so.NotAfter.Time()),\n\t\twithDefaultASN1DN(a.config.AuthorityConfig.Template),\n\t\twithProvisionerOID(\"Max\", a.config.AuthorityConfig.Provisioners[0].(*provisioner.JWK).Key.KeyID),\n\t\twithSigner(issuer, signer))\n\n\tcertNoRenew := generateCertificate(t, \"renew\", []string{\"test.smallstep.com\", \"test\"},\n\t\twithNotBeforeNotAfter(so.NotBefore.Time(), so.NotAfter.Time()),\n\t\twithDefaultASN1DN(a.config.AuthorityConfig.Template),\n\t\twithProvisionerOID(\"dev\", a.config.AuthorityConfig.Provisioners[2].(*provisioner.JWK).Key.KeyID),\n\t\twithSigner(issuer, signer))\n\n\ttype renewTest struct {\n\t\tauth *Authority\n\t\tcert *x509.Certificate\n\t\terr  error\n\t\tcode int\n\t}\n\ttests := map[string]func() (*renewTest, error){\n\t\t\"fail/create-cert\": func() (*renewTest, error) {\n\t\t\t_a := testAuthority(t)\n\t\t\t_a.x509CAService.(*softcas.SoftCAS).Signer = nil\n\t\t\treturn &renewTest{\n\t\t\t\tauth: _a,\n\t\t\t\tcert: cert,\n\t\t\t\terr:  errors.New(\"error creating certificate\"),\n\t\t\t\tcode: http.StatusInternalServerError,\n\t\t\t}, nil\n\t\t},\n\t\t\"fail/unauthorized\": func() (*renewTest, error) {\n\t\t\treturn &renewTest{\n\t\t\t\tcert: certNoRenew,\n\t\t\t\terr:  errors.New(\"authority.authorizeRenew: renew is disabled for provisioner 'dev'\"),\n\t\t\t\tcode: http.StatusUnauthorized,\n\t\t\t}, nil\n\t\t},\n\t\t\"fail/WithAuthorizeRenewFunc\": func() (*renewTest, error) {\n\t\t\taa := testAuthority(t, WithAuthorizeRenewFunc(func(ctx context.Context, p *provisioner.Controller, cert *x509.Certificate) error {\n\t\t\t\treturn errs.Unauthorized(\"not authorized\")\n\t\t\t}))\n\t\t\taa.x509CAService = a.x509CAService\n\t\t\taa.config.AuthorityConfig.Template = a.config.AuthorityConfig.Template\n\t\t\treturn &renewTest{\n\t\t\t\tauth: aa,\n\t\t\t\tcert: cert,\n\t\t\t\terr:  errors.New(\"authority.authorizeRenew: not authorized\"),\n\t\t\t\tcode: http.StatusUnauthorized,\n\t\t\t}, nil\n\t\t},\n\t\t\"ok\": func() (*renewTest, error) {\n\t\t\treturn &renewTest{\n\t\t\t\tauth: a,\n\t\t\t\tcert: cert,\n\t\t\t}, nil\n\t\t},\n\t\t\"ok/WithExtraNames\": func() (*renewTest, error) {\n\t\t\treturn &renewTest{\n\t\t\t\tauth: a,\n\t\t\t\tcert: certExtraNames,\n\t\t\t}, nil\n\t\t},\n\t\t\"ok/success-new-intermediate\": func() (*renewTest, error) {\n\t\t\trootCert, rootSigner := generateRootCertificate(t)\n\t\t\tintCert, intSigner := generateIntermidiateCertificate(t, rootCert, rootSigner)\n\n\t\t\t_a := testAuthority(t)\n\t\t\t_a.x509CAService.(*softcas.SoftCAS).CertificateChain = []*x509.Certificate{intCert}\n\t\t\t_a.x509CAService.(*softcas.SoftCAS).Signer = intSigner\n\t\t\treturn &renewTest{\n\t\t\t\tauth: _a,\n\t\t\t\tcert: cert,\n\t\t\t}, nil\n\t\t},\n\t\t\"ok/WithAuthorizeRenewFunc\": func() (*renewTest, error) {\n\t\t\taa := testAuthority(t, WithAuthorizeRenewFunc(func(ctx context.Context, p *provisioner.Controller, cert *x509.Certificate) error {\n\t\t\t\treturn nil\n\t\t\t}))\n\t\t\taa.x509CAService = a.x509CAService\n\t\t\taa.config.AuthorityConfig.Template = a.config.AuthorityConfig.Template\n\t\t\treturn &renewTest{\n\t\t\t\tauth: aa,\n\t\t\t\tcert: cert,\n\t\t\t}, nil\n\t\t},\n\t}\n\n\tfor name, genTestCase := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ttc, err := genTestCase()\n\t\t\trequire.NoError(t, err)\n\n\t\t\tvar certChain []*x509.Certificate\n\t\t\tif tc.auth != nil {\n\t\t\t\tcertChain, err = tc.auth.Renew(tc.cert)\n\t\t\t} else {\n\t\t\t\tcertChain, err = a.Renew(tc.cert)\n\t\t\t}\n\t\t\tif err != nil {\n\t\t\t\tif assert.NotNil(t, tc.err, fmt.Sprintf(\"unexpected error: %s\", err)) {\n\t\t\t\t\tassert.Nil(t, certChain)\n\t\t\t\t\tvar sc render.StatusCodedError\n\t\t\t\t\trequire.True(t, errors.As(err, &sc), \"error does not implement StatusCodedError interface\")\n\t\t\t\t\tassert.Equal(t, tc.code, sc.StatusCode())\n\t\t\t\t\tassertHasPrefix(t, err.Error(), tc.err.Error())\n\n\t\t\t\t\tvar ctxErr *errs.Error\n\t\t\t\t\trequire.True(t, errors.As(err, &ctxErr), \"error is not of type *errs.Error\")\n\t\t\t\t\tassert.Equal(t, tc.cert.SerialNumber.String(), ctxErr.Details[\"serialNumber\"])\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tleaf := certChain[0]\n\t\t\t\tintermediate := certChain[1]\n\t\t\t\tif assert.Nil(t, tc.err) {\n\t\t\t\t\tassert.Equal(t, tc.cert.NotAfter.Sub(cert.NotBefore), leaf.NotAfter.Sub(leaf.NotBefore))\n\n\t\t\t\t\tassert.True(t, leaf.NotBefore.After(now.Add(-2*time.Minute)))\n\t\t\t\t\tassert.True(t, leaf.NotBefore.Before(now.Add(time.Minute)))\n\n\t\t\t\t\texpiry := now.Add(time.Minute * 7)\n\t\t\t\t\tassert.True(t, leaf.NotAfter.After(expiry.Add(-2*time.Minute)))\n\t\t\t\t\tassert.True(t, leaf.NotAfter.Before(expiry.Add(time.Hour)))\n\n\t\t\t\t\ttmplt := a.config.AuthorityConfig.Template\n\t\t\t\t\tassert.Equal(t, tc.cert.RawSubject, leaf.RawSubject)\n\t\t\t\t\tassert.Equal(t, []string{tmplt.Country}, leaf.Subject.Country)\n\t\t\t\t\tassert.Equal(t, []string{tmplt.Organization}, leaf.Subject.Organization)\n\t\t\t\t\tassert.Equal(t, []string{tmplt.Locality}, leaf.Subject.Locality)\n\t\t\t\t\tassert.Equal(t, []string{tmplt.StreetAddress}, leaf.Subject.StreetAddress)\n\t\t\t\t\tassert.Equal(t, []string{tmplt.Province}, leaf.Subject.Province)\n\t\t\t\t\tassert.Equal(t, tmplt.CommonName, leaf.Subject.CommonName)\n\n\t\t\t\t\tassert.Equal(t, intermediate.Subject, leaf.Issuer)\n\n\t\t\t\t\tassert.Equal(t, x509.ECDSAWithSHA256, leaf.SignatureAlgorithm)\n\t\t\t\t\tassert.Equal(t, x509.ECDSA, leaf.PublicKeyAlgorithm)\n\t\t\t\t\tassert.Equal(t, []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}, leaf.ExtKeyUsage)\n\t\t\t\t\tassert.Equal(t, []string{\"test.smallstep.com\", \"test\"}, leaf.DNSNames)\n\n\t\t\t\t\tsubjectKeyID, err := generateSubjectKeyID(leaf.PublicKey)\n\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t\tassert.Equal(t, subjectKeyID, leaf.SubjectKeyId)\n\n\t\t\t\t\t// We did not change the intermediate before renewing.\n\t\t\t\t\tauthIssuer := getDefaultIssuer(tc.auth)\n\t\t\t\t\tif issuer.SerialNumber == authIssuer.SerialNumber {\n\t\t\t\t\t\tassert.Equal(t, issuer.SubjectKeyId, leaf.AuthorityKeyId)\n\t\t\t\t\t\t// Compare extensions: they can be in a different order\n\t\t\t\t\t\tfor _, ext1 := range tc.cert.Extensions {\n\t\t\t\t\t\t\t//skip SubjectKeyIdentifier\n\t\t\t\t\t\t\tif ext1.Id.Equal(oidSubjectKeyIdentifier) {\n\t\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tfound := false\n\t\t\t\t\t\t\tfor _, ext2 := range leaf.Extensions {\n\t\t\t\t\t\t\t\tif reflect.DeepEqual(ext1, ext2) {\n\t\t\t\t\t\t\t\t\tfound = true\n\t\t\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif !found {\n\t\t\t\t\t\t\t\tt.Errorf(\"x509 extension %s not found in renewed certificate\", ext1.Id.String())\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// We did change the intermediate before renewing.\n\t\t\t\t\t\tassert.Equal(t, authIssuer.SubjectKeyId, leaf.AuthorityKeyId)\n\t\t\t\t\t\t// Compare extensions: they can be in a different order\n\t\t\t\t\t\tfor _, ext1 := range tc.cert.Extensions {\n\t\t\t\t\t\t\t//skip SubjectKeyIdentifier\n\t\t\t\t\t\t\tif ext1.Id.Equal(oidSubjectKeyIdentifier) {\n\t\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t// The authority key id extension should be different b/c the intermediates are different.\n\t\t\t\t\t\t\tif ext1.Id.Equal(oidAuthorityKeyIdentifier) {\n\t\t\t\t\t\t\t\tfor _, ext2 := range leaf.Extensions {\n\t\t\t\t\t\t\t\t\tassert.False(t, reflect.DeepEqual(ext1, ext2))\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tfound := false\n\t\t\t\t\t\t\tfor _, ext2 := range leaf.Extensions {\n\t\t\t\t\t\t\t\tif reflect.DeepEqual(ext1, ext2) {\n\t\t\t\t\t\t\t\t\tfound = true\n\t\t\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif !found {\n\t\t\t\t\t\t\t\tt.Errorf(\"x509 extension %s not found in renewed certificate\", ext1.Id.String())\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\trealIntermediate, err := x509.ParseCertificate(authIssuer.Raw)\n\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t\tassert.Equal(t, realIntermediate, intermediate)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestAuthority_Rekey(t *testing.T) {\n\tpub, _, err := keyutil.GenerateDefaultKeyPair()\n\trequire.NoError(t, err)\n\n\ta := testAuthority(t)\n\ta.config.AuthorityConfig.Template = &ASN1DN{\n\t\tCountry:       \"Tazmania\",\n\t\tOrganization:  \"Acme Co\",\n\t\tLocality:      \"Landscapes\",\n\t\tProvince:      \"Sudden Cliffs\",\n\t\tStreetAddress: \"TNT\",\n\t\tCommonName:    \"renew\",\n\t}\n\n\tnow := time.Now().UTC()\n\tnb1 := now.Add(-time.Minute * 7)\n\tna1 := now.Add(time.Hour)\n\tso := &provisioner.SignOptions{\n\t\tNotBefore: provisioner.NewTimeDuration(nb1),\n\t\tNotAfter:  provisioner.NewTimeDuration(na1),\n\t}\n\n\tissuer := getDefaultIssuer(a)\n\tsigner := getDefaultSigner(a)\n\n\tcert := generateCertificate(t, \"renew\", []string{\"test.smallstep.com\", \"test\"},\n\t\twithNotBeforeNotAfter(so.NotBefore.Time(), so.NotAfter.Time()),\n\t\twithDefaultASN1DN(a.config.AuthorityConfig.Template),\n\t\twithProvisionerOID(\"Max\", a.config.AuthorityConfig.Provisioners[0].(*provisioner.JWK).Key.KeyID),\n\t\twithSigner(issuer, signer))\n\n\tcertNoRenew := generateCertificate(t, \"renew\", []string{\"test.smallstep.com\", \"test\"},\n\t\twithNotBeforeNotAfter(so.NotBefore.Time(), so.NotAfter.Time()),\n\t\twithDefaultASN1DN(a.config.AuthorityConfig.Template),\n\t\twithProvisionerOID(\"dev\", a.config.AuthorityConfig.Provisioners[2].(*provisioner.JWK).Key.KeyID),\n\t\twithSigner(issuer, signer))\n\n\ttype renewTest struct {\n\t\tauth *Authority\n\t\tcert *x509.Certificate\n\t\tpk   crypto.PublicKey\n\t\terr  error\n\t\tcode int\n\t}\n\ttests := map[string]func() (*renewTest, error){\n\t\t\"fail/create-cert\": func() (*renewTest, error) {\n\t\t\t_a := testAuthority(t)\n\t\t\t_a.x509CAService.(*softcas.SoftCAS).Signer = nil\n\t\t\treturn &renewTest{\n\t\t\t\tauth: _a,\n\t\t\t\tcert: cert,\n\t\t\t\terr:  errors.New(\"error creating certificate\"),\n\t\t\t\tcode: http.StatusInternalServerError,\n\t\t\t}, nil\n\t\t},\n\t\t\"fail/unauthorized\": func() (*renewTest, error) {\n\t\t\treturn &renewTest{\n\t\t\t\tcert: certNoRenew,\n\t\t\t\terr:  errors.New(\"authority.authorizeRenew: renew is disabled for provisioner 'dev'\"),\n\t\t\t\tcode: http.StatusUnauthorized,\n\t\t\t}, nil\n\t\t},\n\t\t\"ok/renew\": func() (*renewTest, error) {\n\t\t\treturn &renewTest{\n\t\t\t\tauth: a,\n\t\t\t\tcert: cert,\n\t\t\t}, nil\n\t\t},\n\t\t\"ok/rekey\": func() (*renewTest, error) {\n\t\t\treturn &renewTest{\n\t\t\t\tauth: a,\n\t\t\t\tcert: cert,\n\t\t\t\tpk:   pub,\n\t\t\t}, nil\n\t\t},\n\t\t\"ok/renew/success-new-intermediate\": func() (*renewTest, error) {\n\t\t\trootCert, rootSigner := generateRootCertificate(t)\n\t\t\tintCert, intSigner := generateIntermidiateCertificate(t, rootCert, rootSigner)\n\n\t\t\t_a := testAuthority(t)\n\t\t\t_a.x509CAService.(*softcas.SoftCAS).CertificateChain = []*x509.Certificate{intCert}\n\t\t\t_a.x509CAService.(*softcas.SoftCAS).Signer = intSigner\n\t\t\treturn &renewTest{\n\t\t\t\tauth: _a,\n\t\t\t\tcert: cert,\n\t\t\t}, nil\n\t\t},\n\t}\n\n\tfor name, genTestCase := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ttc, err := genTestCase()\n\t\t\trequire.NoError(t, err)\n\n\t\t\tvar certChain []*x509.Certificate\n\t\t\tif tc.auth != nil {\n\t\t\t\tcertChain, err = tc.auth.Rekey(tc.cert, tc.pk)\n\t\t\t} else {\n\t\t\t\tcertChain, err = a.Rekey(tc.cert, tc.pk)\n\t\t\t}\n\t\t\tif err != nil {\n\t\t\t\tif assert.NotNil(t, tc.err, fmt.Sprintf(\"unexpected error: %s\", err)) {\n\t\t\t\t\tassert.Nil(t, certChain)\n\t\t\t\t\tvar sc render.StatusCodedError\n\t\t\t\t\trequire.True(t, errors.As(err, &sc), \"error does not implement StatusCodedError interface\")\n\t\t\t\t\tassert.Equal(t, tc.code, sc.StatusCode())\n\t\t\t\t\tassertHasPrefix(t, err.Error(), tc.err.Error())\n\n\t\t\t\t\tvar ctxErr *errs.Error\n\t\t\t\t\trequire.True(t, errors.As(err, &ctxErr), \"error is not of type *errs.Error\")\n\t\t\t\t\tassert.Equal(t, tc.cert.SerialNumber.String(), ctxErr.Details[\"serialNumber\"])\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tleaf := certChain[0]\n\t\t\t\tintermediate := certChain[1]\n\t\t\t\tif assert.Nil(t, tc.err) {\n\t\t\t\t\tassert.Equal(t, tc.cert.NotAfter.Sub(cert.NotBefore), leaf.NotAfter.Sub(leaf.NotBefore))\n\n\t\t\t\t\tassert.True(t, leaf.NotBefore.After(now.Add(-2*time.Minute)))\n\t\t\t\t\tassert.True(t, leaf.NotBefore.Before(now.Add(time.Minute)))\n\n\t\t\t\t\texpiry := now.Add(time.Minute * 7)\n\t\t\t\t\tassert.True(t, leaf.NotAfter.After(expiry.Add(-2*time.Minute)))\n\t\t\t\t\tassert.True(t, leaf.NotAfter.Before(expiry.Add(time.Hour)))\n\n\t\t\t\t\ttmplt := a.config.AuthorityConfig.Template\n\t\t\t\t\tassert.Equal(t, pkix.Name{\n\t\t\t\t\t\tCountry:       []string{tmplt.Country},\n\t\t\t\t\t\tOrganization:  []string{tmplt.Organization},\n\t\t\t\t\t\tLocality:      []string{tmplt.Locality},\n\t\t\t\t\t\tStreetAddress: []string{tmplt.StreetAddress},\n\t\t\t\t\t\tProvince:      []string{tmplt.Province},\n\t\t\t\t\t\tCommonName:    tmplt.CommonName,\n\t\t\t\t\t}.String(), leaf.Subject.String())\n\t\t\t\t\tassert.Equal(t, intermediate.Subject, leaf.Issuer)\n\n\t\t\t\t\tassert.Equal(t, x509.ECDSAWithSHA256, leaf.SignatureAlgorithm)\n\t\t\t\t\tassert.Equal(t, x509.ECDSA, leaf.PublicKeyAlgorithm)\n\t\t\t\t\tassert.Equal(t, []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}, leaf.ExtKeyUsage)\n\t\t\t\t\tassert.Equal(t, []string{\"test.smallstep.com\", \"test\"}, leaf.DNSNames)\n\n\t\t\t\t\t// Test Public Key and SubjectKeyId\n\t\t\t\t\texpectedPK := tc.pk\n\t\t\t\t\tif tc.pk == nil {\n\t\t\t\t\t\texpectedPK = cert.PublicKey\n\t\t\t\t\t}\n\t\t\t\t\tassert.Equal(t, expectedPK, leaf.PublicKey)\n\n\t\t\t\t\tsubjectKeyID, err := generateSubjectKeyID(expectedPK)\n\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t\tassert.Equal(t, subjectKeyID, leaf.SubjectKeyId)\n\t\t\t\t\tif tc.pk == nil {\n\t\t\t\t\t\tassert.Equal(t, cert.SubjectKeyId, leaf.SubjectKeyId)\n\t\t\t\t\t}\n\n\t\t\t\t\t// We did not change the intermediate before renewing.\n\t\t\t\t\tauthIssuer := getDefaultIssuer(tc.auth)\n\t\t\t\t\tif issuer.SerialNumber == authIssuer.SerialNumber {\n\t\t\t\t\t\tassert.Equal(t, issuer.SubjectKeyId, leaf.AuthorityKeyId)\n\t\t\t\t\t\t// Compare extensions: they can be in a different order\n\t\t\t\t\t\tfor _, ext1 := range tc.cert.Extensions {\n\t\t\t\t\t\t\t//skip SubjectKeyIdentifier\n\t\t\t\t\t\t\tif ext1.Id.Equal(oidSubjectKeyIdentifier) {\n\t\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tfound := false\n\t\t\t\t\t\t\tfor _, ext2 := range leaf.Extensions {\n\t\t\t\t\t\t\t\tif reflect.DeepEqual(ext1, ext2) {\n\t\t\t\t\t\t\t\t\tfound = true\n\t\t\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif !found {\n\t\t\t\t\t\t\t\tt.Errorf(\"x509 extension %s not found in renewed certificate\", ext1.Id.String())\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// We did change the intermediate before renewing.\n\t\t\t\t\t\tassert.Equal(t, authIssuer.SubjectKeyId, leaf.AuthorityKeyId)\n\t\t\t\t\t\t// Compare extensions: they can be in a different order\n\t\t\t\t\t\tfor _, ext1 := range tc.cert.Extensions {\n\t\t\t\t\t\t\t//skip SubjectKeyIdentifier\n\t\t\t\t\t\t\tif ext1.Id.Equal(oidSubjectKeyIdentifier) {\n\t\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t// The authority key id extension should be different b/c the intermediates are different.\n\t\t\t\t\t\t\tif ext1.Id.Equal(oidAuthorityKeyIdentifier) {\n\t\t\t\t\t\t\t\tfor _, ext2 := range leaf.Extensions {\n\t\t\t\t\t\t\t\t\tassert.False(t, reflect.DeepEqual(ext1, ext2))\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tfound := false\n\t\t\t\t\t\t\tfor _, ext2 := range leaf.Extensions {\n\t\t\t\t\t\t\t\tif reflect.DeepEqual(ext1, ext2) {\n\t\t\t\t\t\t\t\t\tfound = true\n\t\t\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif !found {\n\t\t\t\t\t\t\t\tt.Errorf(\"x509 extension %s not found in renewed certificate\", ext1.Id.String())\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\trealIntermediate, err := x509.ParseCertificate(authIssuer.Raw)\n\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t\tassert.Equal(t, realIntermediate, intermediate)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestAuthority_GetTLSOptions(t *testing.T) {\n\ttype renewTest struct {\n\t\tauth *Authority\n\t\topts *TLSOptions\n\t}\n\ttests := map[string]func() (*renewTest, error){\n\t\t\"default\": func() (*renewTest, error) {\n\t\t\ta := testAuthority(t)\n\t\t\treturn &renewTest{auth: a, opts: &DefaultTLSOptions}, nil\n\t\t},\n\t\t\"non-default\": func() (*renewTest, error) {\n\t\t\ta := testAuthority(t)\n\t\t\ta.config.TLS = &TLSOptions{\n\t\t\t\tCipherSuites: CipherSuites{\n\t\t\t\t\t\"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305\",\n\t\t\t\t\t\"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384\",\n\t\t\t\t},\n\t\t\t\tMinVersion:    1.0,\n\t\t\t\tMaxVersion:    1.1,\n\t\t\t\tRenegotiation: true,\n\t\t\t}\n\t\t\treturn &renewTest{auth: a, opts: a.config.TLS}, nil\n\t\t},\n\t}\n\n\tfor name, genTestCase := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ttc, err := genTestCase()\n\t\t\trequire.NoError(t, err)\n\n\t\t\topts := tc.auth.GetTLSOptions()\n\t\t\tassert.Equal(t, tc.opts, opts)\n\t\t})\n\t}\n}\n\nfunc TestAuthority_Revoke(t *testing.T) {\n\treasonCode := 2\n\treason := \"bob was let go\"\n\tvalidIssuer := \"step-cli\"\n\tvalidAudience := testAudiences.Revoke\n\tnow := time.Now().UTC()\n\n\tjwk, err := jose.ReadKey(\"testdata/secrets/step_cli_key_priv.jwk\", jose.WithPassword([]byte(\"pass\")))\n\trequire.NoError(t, err)\n\n\tsig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: jwk.Key},\n\t\t(&jose.SignerOptions{}).WithType(\"JWT\").WithHeader(\"kid\", jwk.KeyID))\n\trequire.NoError(t, err)\n\n\ta := testAuthority(t)\n\n\ttlsRevokeCtx := provisioner.NewContextWithMethod(context.Background(), provisioner.RevokeMethod)\n\n\ttype test struct {\n\t\tauth            *Authority\n\t\tctx             context.Context\n\t\topts            *RevokeOptions\n\t\terr             error\n\t\tcode            int\n\t\tcheckErrDetails func(err *errs.Error)\n\t}\n\ttests := map[string]func() test{\n\t\t\"fail/token/authorizeRevoke error\": func() test {\n\t\t\treturn test{\n\t\t\t\tauth: a,\n\t\t\t\tctx:  tlsRevokeCtx,\n\t\t\t\topts: &RevokeOptions{\n\t\t\t\t\tOTT:        \"foo\",\n\t\t\t\t\tSerial:     \"sn\",\n\t\t\t\t\tReasonCode: reasonCode,\n\t\t\t\t\tReason:     reason,\n\t\t\t\t},\n\t\t\t\terr:  errors.New(\"authority.Revoke; error parsing token\"),\n\t\t\t\tcode: http.StatusUnauthorized,\n\t\t\t}\n\t\t},\n\t\t\"fail/nil-db\": func() test {\n\t\t\tcl := jose.Claims{\n\t\t\t\tSubject:   \"sn\",\n\t\t\t\tIssuer:    validIssuer,\n\t\t\t\tNotBefore: jose.NewNumericDate(now),\n\t\t\t\tExpiry:    jose.NewNumericDate(now.Add(time.Minute)),\n\t\t\t\tAudience:  validAudience,\n\t\t\t\tID:        \"44\",\n\t\t\t}\n\t\t\traw, err := jose.Signed(sig).Claims(cl).CompactSerialize()\n\t\t\trequire.NoError(t, err)\n\n\t\t\treturn test{\n\t\t\t\tauth: a,\n\t\t\t\tctx:  tlsRevokeCtx,\n\t\t\t\topts: &RevokeOptions{\n\t\t\t\t\tSerial:     \"sn\",\n\t\t\t\t\tReasonCode: reasonCode,\n\t\t\t\t\tReason:     reason,\n\t\t\t\t\tOTT:        raw,\n\t\t\t\t},\n\t\t\t\terr:  errors.New(\"authority.Revoke; no persistence layer configured\"),\n\t\t\t\tcode: http.StatusNotImplemented,\n\t\t\t\tcheckErrDetails: func(err *errs.Error) {\n\t\t\t\t\tassert.Equal(t, raw, err.Details[\"token\"])\n\t\t\t\t\tassert.Equal(t, \"44\", err.Details[\"tokenID\"])\n\t\t\t\t\tassert.Equal(t, \"step-cli:4UELJx8e0aS9m0CH3fZ0EB7D5aUPICb759zALHFejvc\", err.Details[\"provisionerID\"])\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"fail/db-revoke\": func() test {\n\t\t\t_a := testAuthority(t, WithDatabase(&db.MockAuthDB{\n\t\t\t\tMUseToken: func(id, tok string) (bool, error) {\n\t\t\t\t\treturn true, nil\n\t\t\t\t},\n\t\t\t\tMGetCertificate: func(sn string) (*x509.Certificate, error) {\n\t\t\t\t\treturn nil, errors.New(\"not found\")\n\t\t\t\t},\n\t\t\t\tErr: errors.New(\"force\"),\n\t\t\t}))\n\n\t\t\tcl := jose.Claims{\n\t\t\t\tSubject:   \"sn\",\n\t\t\t\tIssuer:    validIssuer,\n\t\t\t\tNotBefore: jose.NewNumericDate(now),\n\t\t\t\tExpiry:    jose.NewNumericDate(now.Add(time.Minute)),\n\t\t\t\tAudience:  validAudience,\n\t\t\t\tID:        \"44\",\n\t\t\t}\n\t\t\traw, err := jose.Signed(sig).Claims(cl).CompactSerialize()\n\t\t\trequire.NoError(t, err)\n\n\t\t\treturn test{\n\t\t\t\tauth: _a,\n\t\t\t\tctx:  tlsRevokeCtx,\n\t\t\t\topts: &RevokeOptions{\n\t\t\t\t\tSerial:     \"sn\",\n\t\t\t\t\tReasonCode: reasonCode,\n\t\t\t\t\tReason:     reason,\n\t\t\t\t\tOTT:        raw,\n\t\t\t\t},\n\t\t\t\terr:  errors.New(\"authority.Revoke: force\"),\n\t\t\t\tcode: http.StatusInternalServerError,\n\t\t\t\tcheckErrDetails: func(err *errs.Error) {\n\t\t\t\t\tassert.Equal(t, raw, err.Details[\"token\"])\n\t\t\t\t\tassert.Equal(t, \"44\", err.Details[\"tokenID\"])\n\t\t\t\t\tassert.Equal(t, \"step-cli:4UELJx8e0aS9m0CH3fZ0EB7D5aUPICb759zALHFejvc\", err.Details[\"provisionerID\"])\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"fail/already-revoked\": func() test {\n\t\t\t_a := testAuthority(t, WithDatabase(&db.MockAuthDB{\n\t\t\t\tMUseToken: func(id, tok string) (bool, error) {\n\t\t\t\t\treturn true, nil\n\t\t\t\t},\n\t\t\t\tMGetCertificate: func(sn string) (*x509.Certificate, error) {\n\t\t\t\t\treturn nil, errors.New(\"not found\")\n\t\t\t\t},\n\t\t\t\tErr: db.ErrAlreadyExists,\n\t\t\t}))\n\n\t\t\tcl := jose.Claims{\n\t\t\t\tSubject:   \"sn\",\n\t\t\t\tIssuer:    validIssuer,\n\t\t\t\tNotBefore: jose.NewNumericDate(now),\n\t\t\t\tExpiry:    jose.NewNumericDate(now.Add(time.Minute)),\n\t\t\t\tAudience:  validAudience,\n\t\t\t\tID:        \"44\",\n\t\t\t}\n\t\t\traw, err := jose.Signed(sig).Claims(cl).CompactSerialize()\n\t\t\trequire.NoError(t, err)\n\n\t\t\treturn test{\n\t\t\t\tauth: _a,\n\t\t\t\tctx:  tlsRevokeCtx,\n\t\t\t\topts: &RevokeOptions{\n\t\t\t\t\tSerial:     \"sn\",\n\t\t\t\t\tReasonCode: reasonCode,\n\t\t\t\t\tReason:     reason,\n\t\t\t\t\tOTT:        raw,\n\t\t\t\t},\n\t\t\t\terr:  errors.New(\"certificate with serial number 'sn' is already revoked\"),\n\t\t\t\tcode: http.StatusBadRequest,\n\t\t\t\tcheckErrDetails: func(err *errs.Error) {\n\t\t\t\t\tassert.Equal(t, raw, err.Details[\"token\"])\n\t\t\t\t\tassert.Equal(t, \"44\", err.Details[\"tokenID\"])\n\t\t\t\t\tassert.Equal(t, \"step-cli:4UELJx8e0aS9m0CH3fZ0EB7D5aUPICb759zALHFejvc\", err.Details[\"provisionerID\"])\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"fail/serial-number\": func() test {\n\t\t\t_a := testAuthority(t, WithDatabase(&db.MockAuthDB{\n\t\t\t\tMUseToken: func(id, tok string) (bool, error) {\n\t\t\t\t\treturn true, nil\n\t\t\t\t},\n\t\t\t\tMGetCertificate: func(sn string) (*x509.Certificate, error) {\n\t\t\t\t\treturn nil, errors.New(\"not found\")\n\t\t\t\t},\n\t\t\t}))\n\n\t\t\tcl := jose.Claims{\n\t\t\t\tSubject:   \"token-sn\",\n\t\t\t\tIssuer:    validIssuer,\n\t\t\t\tNotBefore: jose.NewNumericDate(now),\n\t\t\t\tExpiry:    jose.NewNumericDate(now.Add(time.Minute)),\n\t\t\t\tAudience:  validAudience,\n\t\t\t\tID:        \"44\",\n\t\t\t}\n\t\t\traw, err := jose.Signed(sig).Claims(cl).CompactSerialize()\n\t\t\trequire.NoError(t, err)\n\t\t\treturn test{\n\t\t\t\tauth: _a,\n\t\t\t\tctx:  tlsRevokeCtx,\n\t\t\t\topts: &RevokeOptions{\n\t\t\t\t\tSerial:     \"request-sn\",\n\t\t\t\t\tReasonCode: reasonCode,\n\t\t\t\t\tReason:     reason,\n\t\t\t\t\tOTT:        raw,\n\t\t\t\t},\n\t\t\t\terr:  errors.New(`request serial number \"request-sn\" and token subject \"token-sn\" do not match`),\n\t\t\t\tcode: http.StatusForbidden,\n\t\t\t\tcheckErrDetails: func(err *errs.Error) {\n\t\t\t\t\tassert.Equal(t, raw, err.Details[\"token\"])\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"ok/token\": func() test {\n\t\t\t_a := testAuthority(t, WithDatabase(&db.MockAuthDB{\n\t\t\t\tMUseToken: func(id, tok string) (bool, error) {\n\t\t\t\t\treturn true, nil\n\t\t\t\t},\n\t\t\t\tMGetCertificate: func(sn string) (*x509.Certificate, error) {\n\t\t\t\t\treturn nil, errors.New(\"not found\")\n\t\t\t\t},\n\t\t\t}))\n\n\t\t\tcl := jose.Claims{\n\t\t\t\tSubject:   \"sn\",\n\t\t\t\tIssuer:    validIssuer,\n\t\t\t\tNotBefore: jose.NewNumericDate(now),\n\t\t\t\tExpiry:    jose.NewNumericDate(now.Add(time.Minute)),\n\t\t\t\tAudience:  validAudience,\n\t\t\t\tID:        \"44\",\n\t\t\t}\n\t\t\traw, err := jose.Signed(sig).Claims(cl).CompactSerialize()\n\t\t\trequire.NoError(t, err)\n\t\t\treturn test{\n\t\t\t\tauth: _a,\n\t\t\t\tctx:  tlsRevokeCtx,\n\t\t\t\topts: &RevokeOptions{\n\t\t\t\t\tSerial:     \"sn\",\n\t\t\t\t\tReasonCode: reasonCode,\n\t\t\t\t\tReason:     reason,\n\t\t\t\t\tOTT:        raw,\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"ok/mTLS\": func() test {\n\t\t\t_a := testAuthority(t, WithDatabase(&db.MockAuthDB{}))\n\n\t\t\tcrt, err := pemutil.ReadCertificate(\"./testdata/certs/foo.crt\")\n\t\t\trequire.NoError(t, err)\n\n\t\t\treturn test{\n\t\t\t\tauth: _a,\n\t\t\t\tctx:  tlsRevokeCtx,\n\t\t\t\topts: &RevokeOptions{\n\t\t\t\t\tCrt:        crt,\n\t\t\t\t\tSerial:     \"102012593071130646873265215610956555026\",\n\t\t\t\t\tReasonCode: reasonCode,\n\t\t\t\t\tReason:     reason,\n\t\t\t\t\tMTLS:       true,\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"ok/mTLS-no-provisioner\": func() test {\n\t\t\t_a := testAuthority(t, WithDatabase(&db.MockAuthDB{}))\n\n\t\t\tcrt, err := pemutil.ReadCertificate(\"./testdata/certs/foo.crt\")\n\t\t\trequire.NoError(t, err)\n\t\t\t// Filter out provisioner extension.\n\t\t\tfor i, ext := range crt.Extensions {\n\t\t\t\tif ext.Id.Equal(asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 37476, 9000, 64, 1}) {\n\t\t\t\t\tcrt.Extensions = append(crt.Extensions[:i], crt.Extensions[i+1:]...)\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn test{\n\t\t\t\tauth: _a,\n\t\t\t\tctx:  tlsRevokeCtx,\n\t\t\t\topts: &RevokeOptions{\n\t\t\t\t\tCrt:        crt,\n\t\t\t\t\tSerial:     \"102012593071130646873265215610956555026\",\n\t\t\t\t\tReasonCode: reasonCode,\n\t\t\t\t\tReason:     reason,\n\t\t\t\t\tMTLS:       true,\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"ok/ACME\": func() test {\n\t\t\t_a := testAuthority(t, WithDatabase(&db.MockAuthDB{}))\n\n\t\t\tcrt, err := pemutil.ReadCertificate(\"./testdata/certs/foo.crt\")\n\t\t\trequire.NoError(t, err)\n\n\t\t\treturn test{\n\t\t\t\tauth: _a,\n\t\t\t\tctx:  tlsRevokeCtx,\n\t\t\t\topts: &RevokeOptions{\n\t\t\t\t\tCrt:        crt,\n\t\t\t\t\tSerial:     \"102012593071130646873265215610956555026\",\n\t\t\t\t\tReasonCode: reasonCode,\n\t\t\t\t\tReason:     reason,\n\t\t\t\t\tACME:       true,\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"ok/ssh\": func() test {\n\t\t\ta := testAuthority(t, WithDatabase(&db.MockAuthDB{\n\t\t\t\tMRevoke: func(rci *db.RevokedCertificateInfo) error {\n\t\t\t\t\treturn errors.New(\"Revoke was called\")\n\t\t\t\t},\n\t\t\t\tMRevokeSSH: func(rci *db.RevokedCertificateInfo) error {\n\t\t\t\t\treturn nil\n\t\t\t\t},\n\t\t\t}))\n\n\t\t\tcl := jose.Claims{\n\t\t\t\tSubject:   \"sn\",\n\t\t\t\tIssuer:    validIssuer,\n\t\t\t\tNotBefore: jose.NewNumericDate(now),\n\t\t\t\tExpiry:    jose.NewNumericDate(now.Add(time.Minute)),\n\t\t\t\tAudience:  validAudience,\n\t\t\t\tID:        \"44\",\n\t\t\t}\n\t\t\traw, err := jose.Signed(sig).Claims(cl).CompactSerialize()\n\t\t\trequire.NoError(t, err)\n\t\t\treturn test{\n\t\t\t\tauth: a,\n\t\t\t\tctx:  provisioner.NewContextWithMethod(context.Background(), provisioner.SSHRevokeMethod),\n\t\t\t\topts: &RevokeOptions{\n\t\t\t\t\tSerial:     \"sn\",\n\t\t\t\t\tReasonCode: reasonCode,\n\t\t\t\t\tReason:     reason,\n\t\t\t\t\tOTT:        raw,\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t}\n\tfor name, f := range tests {\n\t\ttc := f()\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tif err := tc.auth.Revoke(tc.ctx, tc.opts); err != nil {\n\t\t\t\tif assert.NotNil(t, tc.err, fmt.Sprintf(\"unexpected error: %s\", err)) {\n\t\t\t\t\tvar sc render.StatusCodedError\n\t\t\t\t\trequire.True(t, errors.As(err, &sc), \"error does not implement StatusCodedError interface\")\n\t\t\t\t\tassert.Equal(t, tc.code, sc.StatusCode())\n\t\t\t\t\tassertHasPrefix(t, err.Error(), tc.err.Error())\n\n\t\t\t\t\tvar ctxErr *errs.Error\n\t\t\t\t\trequire.True(t, errors.As(err, &ctxErr), \"error is not of type *errs.Error\")\n\t\t\t\t\tassert.Equal(t, tc.opts.Serial, ctxErr.Details[\"serialNumber\"])\n\t\t\t\t\tassert.Equal(t, tc.opts.ReasonCode, ctxErr.Details[\"reasonCode\"])\n\t\t\t\t\tassert.Equal(t, tc.opts.Reason, ctxErr.Details[\"reason\"])\n\t\t\t\t\tassert.Equal(t, tc.opts.MTLS, ctxErr.Details[\"MTLS\"])\n\t\t\t\t\tassert.Equal(t, provisioner.RevokeMethod.String(), ctxErr.Details[\"context\"])\n\n\t\t\t\t\tif tc.checkErrDetails != nil {\n\t\t\t\t\t\ttc.checkErrDetails(ctxErr)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tassert.Nil(t, tc.err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestAuthority_constraints(t *testing.T) {\n\tca, err := minica.New(\n\t\tminica.WithIntermediateTemplate(`{\n\t\t\t\t\"subject\": {{ toJson .Subject }},\n\t\t\t\t\"keyUsage\": [\"certSign\", \"crlSign\"],\n\t\t\t\t\"basicConstraints\": {\n\t\t\t\t\t\"isCA\": true,\n\t\t\t\t\t\"maxPathLen\": 0\n\t\t\t\t},\n\t\t\t\t\"nameConstraints\": {\n\t\t\t\t\t\"critical\": true,\n\t\t\t\t\t\"permittedDNSDomains\": [\"internal.example.org\"],\n\t\t\t\t\t\"excludedDNSDomains\": [\"internal.example.com\"],\n\t\t\t\t\t\"permittedIPRanges\": [\"192.168.1.0/24\", \"192.168.2.1/32\"],\n\t\t\t\t\t\"excludedIPRanges\": [\"192.168.3.0/24\", \"192.168.4.0/28\"],\n\t\t\t\t\t\"permittedEmailAddresses\": [\"root@example.org\", \"example.org\", \".acme.org\"],\n\t\t\t\t\t\"excludedEmailAddresses\": [\"root@example.com\", \"example.com\", \".acme.com\"],\n\t\t\t\t\t\"permittedURIDomains\": [\"uuid.example.org\", \".acme.org\"],\n\t\t\t\t\t\"excludedURIDomains\": [\"uuid.example.com\", \".acme.com\"]\n\t\t\t\t}\n\t\t\t}`),\n\t)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tauth, err := NewEmbedded(WithX509RootCerts(ca.Root), WithX509Signer(ca.Intermediate, ca.Signer))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tsigner, err := keyutil.GenerateDefaultSigner()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\ttests := []struct {\n\t\tname    string\n\t\tsans    []string\n\t\twantErr bool\n\t}{\n\t\t{\"ok dns\", []string{\"internal.example.org\", \"host.internal.example.org\"}, false},\n\t\t{\"ok ip\", []string{\"192.168.1.10\", \"192.168.2.1\"}, false},\n\t\t{\"ok email\", []string{\"root@example.org\", \"info@example.org\", \"info@www.acme.org\"}, false},\n\t\t{\"ok uri\", []string{\"https://uuid.example.org/b908d973-5167-4a62-abe3-6beda358d82a\", \"https://uuid.acme.org/1724aae1-1bb3-44fb-83c3-9a1a18df67c8\"}, false},\n\t\t{\"fail permitted dns\", []string{\"internal.acme.org\"}, true},\n\t\t{\"fail excluded dns\", []string{\"internal.example.com\"}, true},\n\t\t{\"fail permitted ips\", []string{\"192.168.2.10\"}, true},\n\t\t{\"fail excluded ips\", []string{\"192.168.3.1\"}, true},\n\t\t{\"fail permitted emails\", []string{\"root@acme.org\"}, true},\n\t\t{\"fail excluded emails\", []string{\"root@example.com\"}, true},\n\t\t{\"fail permitted uris\", []string{\"https://acme.org/uuid/7848819c-9d0b-4e12-bbff-cd66079a3444\"}, true},\n\t\t{\"fail excluded uris\", []string{\"https://uuid.example.com/d325eda7-6356-4d60-b8f6-3d64724afeb3\"}, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tcsr, err := x509util.CreateCertificateRequest(tt.sans[0], tt.sans, signer)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\tcert, err := ca.SignCSR(csr)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\tdata := x509util.CreateTemplateData(tt.sans[0], tt.sans)\n\t\t\ttemplateOption, err := provisioner.TemplateOptions(nil, data)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\t_, err = auth.SignWithContext(context.Background(), csr, provisioner.SignOptions{}, templateOption)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"Authority.SignWithContext() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\n\t\t\t_, err = auth.Renew(cert)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"Authority.Renew() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestAuthority_CRL(t *testing.T) {\n\treasonCode := 2\n\treason := \"bob was let go\"\n\tvalidIssuer := \"step-cli\"\n\tvalidAudience := testAudiences.Revoke\n\tnow := time.Now().UTC()\n\tjwk, err := jose.ReadKey(\"testdata/secrets/step_cli_key_priv.jwk\", jose.WithPassword([]byte(\"pass\")))\n\trequire.NoError(t, err)\n\tsig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: jwk.Key},\n\t\t(&jose.SignerOptions{}).WithType(\"JWT\").WithHeader(\"kid\", jwk.KeyID))\n\trequire.NoError(t, err)\n\n\tcrlCtx := provisioner.NewContextWithMethod(context.Background(), provisioner.RevokeMethod)\n\n\tvar crlStore db.CertificateRevocationListInfo\n\tvar revokedList []db.RevokedCertificateInfo\n\n\ttype test struct {\n\t\tauth               *Authority\n\t\tctx                context.Context\n\t\texpected           []string\n\t\texpectedReasonCode *int\n\t\terr                error\n\t}\n\ttests := map[string]func() test{\n\t\t\"fail/empty-crl\": func() test {\n\t\t\ta := testAuthority(t, WithDatabase(&db.MockAuthDB{\n\t\t\t\tMUseToken: func(id, tok string) (bool, error) {\n\t\t\t\t\treturn true, nil\n\t\t\t\t},\n\t\t\t\tMGetCertificate: func(sn string) (*x509.Certificate, error) {\n\t\t\t\t\treturn nil, errors.New(\"not found\")\n\t\t\t\t},\n\t\t\t\tMStoreCRL: func(i *db.CertificateRevocationListInfo) error {\n\t\t\t\t\tcrlStore = *i\n\t\t\t\t\treturn nil\n\t\t\t\t},\n\t\t\t\tMGetCRL: func() (*db.CertificateRevocationListInfo, error) {\n\t\t\t\t\treturn nil, database.ErrNotFound\n\t\t\t\t},\n\t\t\t\tMGetRevokedCertificates: func() (*[]db.RevokedCertificateInfo, error) {\n\t\t\t\t\treturn &revokedList, nil\n\t\t\t\t},\n\t\t\t\tMRevoke: func(rci *db.RevokedCertificateInfo) error {\n\t\t\t\t\trevokedList = append(revokedList, *rci)\n\t\t\t\t\treturn nil\n\t\t\t\t},\n\t\t\t}))\n\t\t\ta.config.CRL = &config.CRLConfig{\n\t\t\t\tEnabled: true,\n\t\t\t}\n\n\t\t\treturn test{\n\t\t\t\tauth:     a,\n\t\t\t\tctx:      crlCtx,\n\t\t\t\texpected: nil,\n\t\t\t\terr:      errors.New(\"authority.GetCertificateRevocationList: not found\"),\n\t\t\t}\n\t\t},\n\t\t\"ok/crl-full\": func() test {\n\t\t\ta := testAuthority(t, WithDatabase(&db.MockAuthDB{\n\t\t\t\tMUseToken: func(id, tok string) (bool, error) {\n\t\t\t\t\treturn true, nil\n\t\t\t\t},\n\t\t\t\tMGetCertificate: func(sn string) (*x509.Certificate, error) {\n\t\t\t\t\treturn nil, errors.New(\"not found\")\n\t\t\t\t},\n\t\t\t\tMStoreCRL: func(i *db.CertificateRevocationListInfo) error {\n\t\t\t\t\tcrlStore = *i\n\t\t\t\t\treturn nil\n\t\t\t\t},\n\t\t\t\tMGetCRL: func() (*db.CertificateRevocationListInfo, error) {\n\t\t\t\t\treturn &crlStore, nil\n\t\t\t\t},\n\t\t\t\tMGetRevokedCertificates: func() (*[]db.RevokedCertificateInfo, error) {\n\t\t\t\t\treturn &revokedList, nil\n\t\t\t\t},\n\t\t\t\tMRevoke: func(rci *db.RevokedCertificateInfo) error {\n\t\t\t\t\trevokedList = append(revokedList, *rci)\n\t\t\t\t\treturn nil\n\t\t\t\t},\n\t\t\t}))\n\t\t\ta.config.CRL = &config.CRLConfig{\n\t\t\t\tEnabled:          true,\n\t\t\t\tGenerateOnRevoke: true,\n\t\t\t}\n\n\t\t\tvar ex []string\n\n\t\t\tfor i := 0; i < 100; i++ {\n\t\t\t\tsn := fmt.Sprintf(\"%v\", i)\n\n\t\t\t\tcl := jose.Claims{\n\t\t\t\t\tSubject:   sn,\n\t\t\t\t\tIssuer:    validIssuer,\n\t\t\t\t\tNotBefore: jose.NewNumericDate(now),\n\t\t\t\t\tExpiry:    jose.NewNumericDate(now.Add(time.Minute)),\n\t\t\t\t\tAudience:  validAudience,\n\t\t\t\t\tID:        sn,\n\t\t\t\t}\n\t\t\t\traw, err := jose.Signed(sig).Claims(cl).CompactSerialize()\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\terr = a.Revoke(crlCtx, &RevokeOptions{\n\t\t\t\t\tSerial:     sn,\n\t\t\t\t\tReasonCode: reasonCode,\n\t\t\t\t\tReason:     reason,\n\t\t\t\t\tOTT:        raw,\n\t\t\t\t})\n\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\tex = append(ex, sn)\n\t\t\t}\n\n\t\t\treturn test{\n\t\t\t\tauth:               a,\n\t\t\t\tctx:                crlCtx,\n\t\t\t\texpected:           ex,\n\t\t\t\texpectedReasonCode: &reasonCode,\n\t\t\t}\n\t\t},\n\t\t\"ok/crl-no-reason-code\": func() test {\n\t\t\tvar localRevokedList []db.RevokedCertificateInfo\n\t\t\tvar localCRLStore db.CertificateRevocationListInfo\n\t\t\ta := testAuthority(t, WithDatabase(&db.MockAuthDB{\n\t\t\t\tMUseToken: func(id, tok string) (bool, error) {\n\t\t\t\t\treturn true, nil\n\t\t\t\t},\n\t\t\t\tMGetCertificate: func(sn string) (*x509.Certificate, error) {\n\t\t\t\t\treturn nil, errors.New(\"not found\")\n\t\t\t\t},\n\t\t\t\tMStoreCRL: func(i *db.CertificateRevocationListInfo) error {\n\t\t\t\t\tlocalCRLStore = *i\n\t\t\t\t\treturn nil\n\t\t\t\t},\n\t\t\t\tMGetCRL: func() (*db.CertificateRevocationListInfo, error) {\n\t\t\t\t\treturn &localCRLStore, nil\n\t\t\t\t},\n\t\t\t\tMGetRevokedCertificates: func() (*[]db.RevokedCertificateInfo, error) {\n\t\t\t\t\treturn &localRevokedList, nil\n\t\t\t\t},\n\t\t\t\tMRevoke: func(rci *db.RevokedCertificateInfo) error {\n\t\t\t\t\tlocalRevokedList = append(localRevokedList, *rci)\n\t\t\t\t\treturn nil\n\t\t\t\t},\n\t\t\t}))\n\t\t\ta.config.CRL = &config.CRLConfig{\n\t\t\t\tEnabled:          true,\n\t\t\t\tGenerateOnRevoke: true,\n\t\t\t}\n\n\t\t\tvar ex []string\n\t\t\tzeroReasonCode := 0\n\n\t\t\tfor i := 0; i < 5; i++ {\n\t\t\t\tsn := fmt.Sprintf(\"%v\", i)\n\t\t\t\tcl := jose.Claims{\n\t\t\t\t\tSubject:   sn,\n\t\t\t\t\tIssuer:    validIssuer,\n\t\t\t\t\tNotBefore: jose.NewNumericDate(now),\n\t\t\t\t\tExpiry:    jose.NewNumericDate(now.Add(time.Minute)),\n\t\t\t\t\tAudience:  validAudience,\n\t\t\t\t\tID:        sn,\n\t\t\t\t}\n\t\t\t\traw, err := jose.Signed(sig).Claims(cl).CompactSerialize()\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\terr = a.Revoke(crlCtx, &RevokeOptions{\n\t\t\t\t\tSerial:     sn,\n\t\t\t\t\tReasonCode: zeroReasonCode,\n\t\t\t\t\tReason:     reason,\n\t\t\t\t\tOTT:        raw,\n\t\t\t\t})\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tex = append(ex, sn)\n\t\t\t}\n\n\t\t\treturn test{\n\t\t\t\tauth:               a,\n\t\t\t\tctx:                crlCtx,\n\t\t\t\texpected:           ex,\n\t\t\t\texpectedReasonCode: &zeroReasonCode,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, f := range tests {\n\t\ttc := f()\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tcrlInfo, err := tc.auth.GetCertificateRevocationList()\n\t\t\tif tc.err != nil {\n\t\t\t\tassert.EqualError(t, err, tc.err.Error())\n\t\t\t\tassert.Nil(t, crlInfo)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tcrl, parseErr := x509.ParseRevocationList(crlInfo.Data)\n\t\t\trequire.NoError(t, parseErr)\n\n\t\t\tvar cmpList []string\n\t\t\tfor _, c := range crl.RevokedCertificateEntries {\n\t\t\t\tcmpList = append(cmpList, c.SerialNumber.String())\n\t\t\t\t// ReasonCode 0 causes Go's x509 package to omit the reasonCode\n\t\t\t\t// extension entirely. Parsing it back yields 0 as the zero value\n\t\t\t\t// of the field, not from an explicit extension. This confirms the\n\t\t\t\t// zero-code path produces a well-formed CRL, not that the\n\t\t\t\t// extension round-trips.\n\t\t\t\tif tc.expectedReasonCode != nil {\n\t\t\t\t\tassert.Equal(t, *tc.expectedReasonCode, c.ReasonCode)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tassert.Equal(t, tc.expected, cmpList)\n\t\t})\n\t}\n}\n\ntype notImplementedCAS struct{}\n\nfunc (notImplementedCAS) CreateCertificate(req *apiv1.CreateCertificateRequest) (*apiv1.CreateCertificateResponse, error) {\n\treturn nil, apiv1.NotImplementedError{}\n}\nfunc (notImplementedCAS) RenewCertificate(req *apiv1.RenewCertificateRequest) (*apiv1.RenewCertificateResponse, error) {\n\treturn nil, apiv1.NotImplementedError{}\n}\nfunc (notImplementedCAS) RevokeCertificate(req *apiv1.RevokeCertificateRequest) (*apiv1.RevokeCertificateResponse, error) {\n\treturn nil, apiv1.NotImplementedError{}\n}\n\nfunc TestAuthority_GetX509Signer(t *testing.T) {\n\tauth := testAuthority(t)\n\trequire.IsType(t, &softcas.SoftCAS{}, auth.x509CAService)\n\tsigner := auth.x509CAService.(*softcas.SoftCAS).Signer\n\trequire.NotNil(t, signer)\n\n\ttests := []struct {\n\t\tname      string\n\t\tauthority *Authority\n\t\twant      crypto.Signer\n\t\tassertion assert.ErrorAssertionFunc\n\t}{\n\t\t{\"ok\", auth, signer, assert.NoError},\n\t\t{\"fail\", testAuthority(t, WithX509CAService(notImplementedCAS{})), nil, assert.Error},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := tt.authority.GetX509Signer()\n\t\t\ttt.assertion(t, err)\n\t\t\tassert.Equal(t, tt.want, got)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "authority/version.go",
    "content": "package authority\n\n// GlobalVersion stores the version information of the server.\nvar GlobalVersion = Version{\n\tVersion: \"0.0.0\",\n}\n\n// Version defines the\ntype Version struct {\n\tVersion                     string\n\tRequireClientAuthentication bool\n}\n\n// Version returns the version information of the server.\nfunc (a *Authority) Version() Version {\n\treturn GlobalVersion\n}\n"
  },
  {
    "path": "authority/webhook.go",
    "content": "package authority\n\nimport (\n\t\"context\"\n\n\t\"github.com/smallstep/certificates/webhook\"\n)\n\ntype webhookController interface {\n\tEnrich(context.Context, *webhook.RequestBody) error\n\tAuthorize(context.Context, *webhook.RequestBody) error\n}\n"
  },
  {
    "path": "authority/webhook_test.go",
    "content": "package authority\n\nimport (\n\t\"context\"\n\n\t\"github.com/smallstep/certificates/authority/provisioner\"\n\t\"github.com/smallstep/certificates/webhook\"\n)\n\ntype mockWebhookController struct {\n\tenrichErr    error\n\tauthorizeErr error\n\ttemplateData provisioner.WebhookSetter\n\trespData     map[string]any\n}\n\nvar _ webhookController = &mockWebhookController{}\n\nfunc (wc *mockWebhookController) Enrich(context.Context, *webhook.RequestBody) error {\n\tfor key, data := range wc.respData {\n\t\twc.templateData.SetWebhook(key, data)\n\t}\n\n\treturn wc.enrichErr\n}\n\nfunc (wc *mockWebhookController) Authorize(context.Context, *webhook.RequestBody) error {\n\treturn wc.authorizeErr\n}\n"
  },
  {
    "path": "autocert/README.md",
    "content": "# ⚠️ Autocert has moved to https://github.com/smallstep/autocert\n\nIf you're looking for hello-mTLS examples they're at\nhttps://github.com/smallstep/autocert/tree/master/examples/hello-mtls"
  },
  {
    "path": "ca/acmeClient.go",
    "content": "package ca\n\nimport (\n\t\"crypto/x509\"\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"encoding/pem\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"strings\"\n\n\t\"github.com/pkg/errors\"\n\n\t\"go.step.sm/crypto/jose\"\n\n\t\"github.com/smallstep/certificates/acme\"\n\tacmeAPI \"github.com/smallstep/certificates/acme/api\"\n)\n\n// ACMEClient implements an HTTP client to an ACME API.\ntype ACMEClient struct {\n\tclient *http.Client\n\tdirLoc string\n\tdir    *acmeAPI.Directory\n\tacc    *acme.Account\n\tKey    *jose.JSONWebKey\n\tkid    string\n}\n\n// NewACMEClient initializes a new ACMEClient.\nfunc NewACMEClient(endpoint string, contact []string, opts ...ClientOption) (*ACMEClient, error) {\n\t// Retrieve transport from options.\n\to := defaultClientOptions()\n\tif err := o.apply(opts); err != nil {\n\t\treturn nil, err\n\t}\n\ttr, err := o.getTransport(endpoint)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tac := &ACMEClient{\n\t\tclient: &http.Client{\n\t\t\tTransport: tr,\n\t\t},\n\t\tdirLoc: endpoint,\n\t}\n\treq, err := http.NewRequest(\"GET\", endpoint, http.NoBody)\n\tif err != nil {\n\t\treturn nil, errors.Wrapf(err, \"creating GET request %s failed\", endpoint)\n\t}\n\treq.Header.Set(\"User-Agent\", UserAgent)\n\tenforceRequestID(req)\n\tresp, err := ac.client.Do(req)\n\tif err != nil {\n\t\treturn nil, errors.Wrapf(err, \"client GET %s failed\", endpoint)\n\t}\n\tdefer resp.Body.Close()\n\tif resp.StatusCode >= 400 {\n\t\treturn nil, readACMEError(resp.Body)\n\t}\n\tvar dir acmeAPI.Directory\n\tif err := readJSON(resp.Body, &dir); err != nil {\n\t\treturn nil, errors.Wrapf(err, \"error reading %s\", endpoint)\n\t}\n\n\tac.dir = &dir\n\n\tac.Key, err = jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tnar := &acmeAPI.NewAccountRequest{\n\t\tContact:              contact,\n\t\tTermsOfServiceAgreed: true,\n\t}\n\tpayload, err := json.Marshal(nar)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"error marshaling new account request\")\n\t}\n\n\tresp, err = ac.post(payload, ac.dir.NewAccount, withJWK(ac))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer resp.Body.Close()\n\tif resp.StatusCode >= 400 {\n\t\treturn nil, readACMEError(resp.Body)\n\t}\n\tvar acc acme.Account\n\tif err := readJSON(resp.Body, &acc); err != nil {\n\t\treturn nil, errors.Wrapf(err, \"error reading %s\", dir.NewAccount)\n\t}\n\tac.acc = &acc\n\tac.kid = resp.Header.Get(\"Location\")\n\n\treturn ac, nil\n}\n\n// GetDirectory makes a directory request to the ACME api and returns an\n// ACME directory object.\nfunc (c *ACMEClient) GetDirectory() (*acmeAPI.Directory, error) {\n\treturn c.dir, nil\n}\n\n// GetNonce makes a nonce request to the ACME api and returns an\n// ACME directory object.\nfunc (c *ACMEClient) GetNonce() (string, error) {\n\treq, err := http.NewRequest(\"GET\", c.dir.NewNonce, http.NoBody)\n\tif err != nil {\n\t\treturn \"\", errors.Wrapf(err, \"creating GET request %s failed\", c.dir.NewNonce)\n\t}\n\treq.Header.Set(\"User-Agent\", UserAgent)\n\tenforceRequestID(req)\n\tresp, err := c.client.Do(req)\n\tif err != nil {\n\t\treturn \"\", errors.Wrapf(err, \"client GET %s failed\", c.dir.NewNonce)\n\t}\n\tdefer resp.Body.Close()\n\tif resp.StatusCode >= 400 {\n\t\treturn \"\", readACMEError(resp.Body)\n\t}\n\treturn resp.Header.Get(\"Replay-Nonce\"), nil\n}\n\ntype withHeaderOption func(so *jose.SignerOptions)\n\nfunc withJWK(c *ACMEClient) withHeaderOption {\n\treturn func(so *jose.SignerOptions) {\n\t\tso.WithHeader(\"jwk\", c.Key.Public())\n\t}\n}\n\nfunc withKid(c *ACMEClient) withHeaderOption {\n\treturn func(so *jose.SignerOptions) {\n\t\tso.WithHeader(\"kid\", c.kid)\n\t}\n}\n\n// serialize serializes a json web signature and doesn't omit empty fields.\nfunc serialize(obj *jose.JSONWebSignature) (string, error) {\n\traw, err := obj.CompactSerialize()\n\tif err != nil {\n\t\treturn \"\", errors.Wrap(err, \"error serializing JWS\")\n\t}\n\tparts := strings.Split(raw, \".\")\n\tmsg := struct {\n\t\tProtected string `json:\"protected\"`\n\t\tPayload   string `json:\"payload\"`\n\t\tSignature string `json:\"signature\"`\n\t}{Protected: parts[0], Payload: parts[1], Signature: parts[2]}\n\tb, err := json.Marshal(msg)\n\tif err != nil {\n\t\treturn \"\", errors.Wrap(err, \"error marshaling jws message\")\n\t}\n\treturn string(b), nil\n}\n\nfunc (c *ACMEClient) post(payload []byte, url string, headerOps ...withHeaderOption) (*http.Response, error) {\n\tif c.Key == nil {\n\t\treturn nil, errors.New(\"acme client not configured with account\")\n\t}\n\tnonce, err := c.GetNonce()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tso := new(jose.SignerOptions)\n\tso.WithHeader(\"nonce\", nonce)\n\tso.WithHeader(\"url\", url)\n\tfor _, hop := range headerOps {\n\t\thop(so)\n\t}\n\tsigner, err := jose.NewSigner(jose.SigningKey{\n\t\tAlgorithm: jose.SignatureAlgorithm(c.Key.Algorithm),\n\t\tKey:       c.Key.Key,\n\t}, so)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"error creating JWS signer\")\n\t}\n\tsigned, err := signer.Sign(payload)\n\tif err != nil {\n\t\treturn nil, errors.Errorf(\"error signing payload: %s\", jose.TrimPrefix(err))\n\t}\n\traw, err := serialize(signed)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treq, err := http.NewRequest(\"POST\", url, strings.NewReader(raw))\n\tif err != nil {\n\t\treturn nil, errors.Wrapf(err, \"creating POST request %s failed\", url)\n\t}\n\treq.Header.Set(\"Content-Type\", \"application/jose+json\")\n\treq.Header.Set(\"User-Agent\", UserAgent)\n\tenforceRequestID(req)\n\tresp, err := c.client.Do(req)\n\tif err != nil {\n\t\treturn nil, errors.Wrapf(err, \"client POST %s failed\", c.dir.NewOrder)\n\t}\n\treturn resp, nil\n}\n\n// NewOrder creates and returns the information for a new ACME order.\nfunc (c *ACMEClient) NewOrder(payload []byte) (*acme.Order, error) {\n\tresp, err := c.post(payload, c.dir.NewOrder, withKid(c))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer resp.Body.Close()\n\tif resp.StatusCode >= 400 {\n\t\treturn nil, readACMEError(resp.Body)\n\t}\n\n\tvar o acme.Order\n\tif err := readJSON(resp.Body, &o); err != nil {\n\t\treturn nil, errors.Wrapf(err, \"error reading %s\", c.dir.NewOrder)\n\t}\n\to.ID = resp.Header.Get(\"Location\")\n\treturn &o, nil\n}\n\n// GetChallenge returns the Challenge at the given path.\n// With the validate parameter set to True this method will attempt to validate the\n// challenge before returning it.\nfunc (c *ACMEClient) GetChallenge(url string) (*acme.Challenge, error) {\n\tresp, err := c.post(nil, url, withKid(c))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer resp.Body.Close()\n\tif resp.StatusCode >= 400 {\n\t\treturn nil, readACMEError(resp.Body)\n\t}\n\n\tvar ch acme.Challenge\n\tif err := readJSON(resp.Body, &ch); err != nil {\n\t\treturn nil, errors.Wrapf(err, \"error reading %s\", url)\n\t}\n\treturn &ch, nil\n}\n\n// ValidateChallenge returns the Challenge at the given path.\n// With the validate parameter set to True this method will attempt to validate the\n// challenge before returning it.\nfunc (c *ACMEClient) ValidateChallenge(url string) error {\n\tresp, err := c.post([]byte(\"{}\"), url, withKid(c))\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer resp.Body.Close()\n\tif resp.StatusCode >= 400 {\n\t\treturn readACMEError(resp.Body)\n\t}\n\treturn nil\n}\n\n// ValidateWithPayload will attempt to validate the challenge at the given url\n// with the given attestation payload.\nfunc (c *ACMEClient) ValidateWithPayload(url string, payload []byte) error {\n\tresp, err := c.post(payload, url, withKid(c))\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer resp.Body.Close()\n\tif resp.StatusCode >= 400 {\n\t\treturn readACMEError(resp.Body)\n\t}\n\treturn nil\n}\n\n// GetAuthz returns the Authz at the given path.\nfunc (c *ACMEClient) GetAuthz(url string) (*acme.Authorization, error) {\n\tresp, err := c.post(nil, url, withKid(c))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer resp.Body.Close()\n\tif resp.StatusCode >= 400 {\n\t\treturn nil, readACMEError(resp.Body)\n\t}\n\n\tvar az acme.Authorization\n\tif err := readJSON(resp.Body, &az); err != nil {\n\t\treturn nil, errors.Wrapf(err, \"error reading %s\", url)\n\t}\n\treturn &az, nil\n}\n\n// GetOrder returns the Order at the given path.\nfunc (c *ACMEClient) GetOrder(url string) (*acme.Order, error) {\n\tresp, err := c.post(nil, url, withKid(c))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer resp.Body.Close()\n\tif resp.StatusCode >= 400 {\n\t\treturn nil, readACMEError(resp.Body)\n\t}\n\n\tvar o acme.Order\n\tif err := readJSON(resp.Body, &o); err != nil {\n\t\treturn nil, errors.Wrapf(err, \"error reading %s\", url)\n\t}\n\treturn &o, nil\n}\n\n// FinalizeOrder makes a finalize request to the ACME api.\nfunc (c *ACMEClient) FinalizeOrder(url string, csr *x509.CertificateRequest) error {\n\tpayload, err := json.Marshal(acmeAPI.FinalizeRequest{\n\t\tCSR: base64.RawURLEncoding.EncodeToString(csr.Raw),\n\t})\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"error marshaling finalize request\")\n\t}\n\tresp, err := c.post(payload, url, withKid(c))\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer resp.Body.Close()\n\tif resp.StatusCode >= 400 {\n\t\treturn readACMEError(resp.Body)\n\t}\n\treturn nil\n}\n\n// GetCertificate retrieves the certificate along with all intermediates.\nfunc (c *ACMEClient) GetCertificate(url string) (*x509.Certificate, []*x509.Certificate, error) {\n\tresp, err := c.post(nil, url, withKid(c))\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\tdefer resp.Body.Close()\n\tif resp.StatusCode >= 400 {\n\t\treturn nil, nil, readACMEError(resp.Body)\n\t}\n\tdefer resp.Body.Close()\n\tbodyBytes, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn nil, nil, errors.Wrap(err, \"error reading GET certificate response\")\n\t}\n\n\tvar certs []*x509.Certificate\n\n\tblock, rest := pem.Decode(bodyBytes)\n\tif block == nil {\n\t\treturn nil, nil, errors.New(\"failed to parse any certificates from response\")\n\t}\n\tfor block != nil {\n\t\tcert, err := x509.ParseCertificate(block.Bytes)\n\t\tif err != nil {\n\t\t\treturn nil, nil, errors.Wrap(err, \"error parsing certificate pem response\")\n\t\t}\n\t\tcerts = append(certs, cert)\n\t\tblock, rest = pem.Decode(rest)\n\t}\n\n\treturn certs[0], certs[1:], nil\n}\n\n// GetAccountOrders retrieves the orders belonging to the given account.\nfunc (c *ACMEClient) GetAccountOrders() ([]string, error) {\n\tif c.acc == nil {\n\t\treturn nil, errors.New(\"acme client not configured with account\")\n\t}\n\tresp, err := c.post(nil, c.acc.OrdersURL, withKid(c))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer resp.Body.Close()\n\tif resp.StatusCode >= 400 {\n\t\treturn nil, readACMEError(resp.Body)\n\t}\n\n\tvar orders []string\n\tif err := readJSON(resp.Body, &orders); err != nil {\n\t\treturn nil, errors.Wrapf(err, \"error reading %s\", c.acc.OrdersURL)\n\t}\n\n\treturn orders, nil\n}\n\nfunc readACMEError(r io.ReadCloser) error {\n\tdefer r.Close()\n\tb, err := io.ReadAll(r)\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"error reading from body\")\n\t}\n\tae := new(acme.Error)\n\terr = json.Unmarshal(b, &ae)\n\t// If we successfully marshaled to an ACMEError then return the ACMEError.\n\tif err != nil || ae.Error() == \"\" {\n\t\tfmt.Printf(\"b = %s\\n\", b)\n\t\t// Throw up our hands.\n\t\treturn errors.Errorf(\"%s\", b)\n\t}\n\treturn ae\n}\n"
  },
  {
    "path": "ca/acmeClient_test.go",
    "content": "package ca\n\nimport (\n\t\"crypto/x509\"\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"encoding/pem\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n\n\t\"github.com/smallstep/assert\"\n\t\"go.step.sm/crypto/jose\"\n\t\"go.step.sm/crypto/pemutil\"\n\n\t\"github.com/smallstep/certificates/acme\"\n\tacmeAPI \"github.com/smallstep/certificates/acme/api\"\n\t\"github.com/smallstep/certificates/api/render\"\n)\n\nfunc TestNewACMEClient(t *testing.T) {\n\ttype test struct {\n\t\tops      []ClientOption\n\t\tr1, r2   interface{}\n\t\trc1, rc2 int\n\t\terr      error\n\t}\n\n\tsrv := httptest.NewServer(nil)\n\tdefer srv.Close()\n\n\tdir := acmeAPI.Directory{\n\t\tNewNonce:   srv.URL + \"/foo\",\n\t\tNewAccount: srv.URL + \"/bar\",\n\t\tNewOrder:   srv.URL + \"/baz\",\n\t\tRevokeCert: srv.URL + \"/zip\",\n\t\tKeyChange:  srv.URL + \"/blorp\",\n\t}\n\tacc := acme.Account{\n\t\tContact:   []string{\"max\", \"mariano\"},\n\t\tStatus:    \"valid\",\n\t\tOrdersURL: \"orders-url\",\n\t}\n\ttests := map[string]func(t *testing.T) test{\n\t\t\"fail/client-option-error\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tops: []ClientOption{\n\t\t\t\t\tfunc(o *clientOptions) error {\n\t\t\t\t\t\treturn errors.New(\"force\")\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\terr: errors.New(\"force\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/get-directory\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tops: []ClientOption{WithTransport(http.DefaultTransport)},\n\t\t\t\tr1:  acme.NewError(acme.ErrorMalformedType, \"malformed request\"),\n\t\t\t\trc1: 400,\n\t\t\t\terr: errors.New(\"The request message was malformed\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/bad-directory\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tops: []ClientOption{WithTransport(http.DefaultTransport)},\n\t\t\t\tr1:  \"foo\",\n\t\t\t\trc1: 200,\n\t\t\t\terr: errors.New(\"error reading http://127.0.0.1\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/error-post-newAccount\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tops: []ClientOption{WithTransport(http.DefaultTransport)},\n\t\t\t\tr1:  dir,\n\t\t\t\trc1: 200,\n\t\t\t\tr2:  acme.NewError(acme.ErrorAccountDoesNotExistType, \"account does not exist\"),\n\t\t\t\trc2: 400,\n\t\t\t\terr: errors.New(\"Account does not exist\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/error-bad-account\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tops: []ClientOption{WithTransport(http.DefaultTransport)},\n\t\t\t\tr1:  dir,\n\t\t\t\trc1: 200,\n\t\t\t\tr2:  \"foo\",\n\t\t\t\trc2: 200,\n\t\t\t\terr: errors.New(\"error reading http://127.0.0.1\"),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tops: []ClientOption{WithTransport(http.DefaultTransport)},\n\t\t\t\tr1:  dir,\n\t\t\t\trc1: 200,\n\t\t\t\tr2:  acc,\n\t\t\t\trc2: 200,\n\t\t\t}\n\t\t},\n\t}\n\n\taccLocation := \"linkitylink\"\n\n\tfor name, run := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ttc := run(t)\n\n\t\t\ti := 0\n\t\t\tsrv.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\tassert.Equals(t, \"step-http-client/1.0\", r.Header.Get(\"User-Agent\")) // check default User-Agent header\n\t\t\t\tswitch i {\n\t\t\t\tcase 0:\n\t\t\t\t\trender.JSONStatus(w, r, tc.r1, tc.rc1)\n\t\t\t\t\ti++\n\t\t\t\tcase 1:\n\t\t\t\t\tw.Header().Set(\"Replay-Nonce\", \"abc123\")\n\t\t\t\t\trender.JSONStatus(w, r, []byte{}, 200)\n\t\t\t\t\ti++\n\t\t\t\tdefault:\n\t\t\t\t\tw.Header().Set(\"Location\", accLocation)\n\t\t\t\t\trender.JSONStatus(w, r, tc.r2, tc.rc2)\n\t\t\t\t}\n\t\t\t})\n\n\t\t\tif client, err := NewACMEClient(srv.URL, []string{\"max\", \"mariano\"}, tc.ops...); err != nil {\n\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.err.Error())\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif assert.Nil(t, tc.err) {\n\t\t\t\t\tassert.Equals(t, *client.dir, dir)\n\t\t\t\t\tassert.NotNil(t, client.Key)\n\t\t\t\t\tassert.NotNil(t, client.acc)\n\t\t\t\t\tassert.Equals(t, client.kid, accLocation)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestACMEClient_GetDirectory(t *testing.T) {\n\tc := &ACMEClient{\n\t\tdir: &acmeAPI.Directory{\n\t\t\tNewNonce:   \"/foo\",\n\t\t\tNewAccount: \"/bar\",\n\t\t\tNewOrder:   \"/baz\",\n\t\t\tRevokeCert: \"/zip\",\n\t\t\tKeyChange:  \"/blorp\",\n\t\t},\n\t}\n\tdir, err := c.GetDirectory()\n\tassert.FatalError(t, err)\n\tassert.Equals(t, c.dir, dir)\n}\n\nfunc TestACMEClient_GetNonce(t *testing.T) {\n\ttype test struct {\n\t\tr1  interface{}\n\t\trc1 int\n\t\terr error\n\t}\n\n\tsrv := httptest.NewServer(nil)\n\tdefer srv.Close()\n\n\tdir := acmeAPI.Directory{\n\t\tNewNonce: srv.URL + \"/foo\",\n\t}\n\t// Retrieve transport from options.\n\to := defaultClientOptions()\n\tassert.FatalError(t, o.apply([]ClientOption{WithTransport(http.DefaultTransport)}))\n\ttr, err := o.getTransport(srv.URL)\n\tassert.FatalError(t, err)\n\tac := &ACMEClient{\n\t\tclient: &http.Client{\n\t\t\tTransport: tr,\n\t\t},\n\t\tdirLoc: srv.URL,\n\t\tdir:    &dir,\n\t}\n\n\ttests := map[string]func(t *testing.T) test{\n\t\t\"fail/GET-nonce\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tr1:  acme.NewError(acme.ErrorMalformedType, \"malformed request\"),\n\t\t\t\trc1: 400,\n\t\t\t\terr: errors.New(\"The request message was malformed\"),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tr1:  []byte{},\n\t\t\t\trc1: 200,\n\t\t\t}\n\t\t},\n\t}\n\n\texpectedNonce := \"abc123\"\n\n\tfor name, run := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ttc := run(t)\n\n\t\t\tsrv.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\tassert.Equals(t, \"step-http-client/1.0\", r.Header.Get(\"User-Agent\")) // check default User-Agent header\n\t\t\t\tw.Header().Set(\"Replay-Nonce\", expectedNonce)\n\t\t\t\trender.JSONStatus(w, r, tc.r1, tc.rc1)\n\t\t\t})\n\n\t\t\tif nonce, err := ac.GetNonce(); err != nil {\n\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.err.Error())\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif assert.Nil(t, tc.err) {\n\t\t\t\t\tassert.Equals(t, expectedNonce, nonce)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestACMEClient_post(t *testing.T) {\n\ttype test struct {\n\t\tpayload  []byte\n\t\tKey      *jose.JSONWebKey\n\t\tops      []withHeaderOption\n\t\tr1, r2   interface{}\n\t\trc1, rc2 int\n\t\tjwkInJWS bool\n\t\tclient   *ACMEClient\n\t\terr      error\n\t}\n\n\tsrv := httptest.NewServer(nil)\n\tdefer srv.Close()\n\n\tdir := acmeAPI.Directory{\n\t\tNewNonce: srv.URL + \"/foo\",\n\t}\n\t// Retrieve transport from options.\n\to := defaultClientOptions()\n\tassert.FatalError(t, o.apply([]ClientOption{WithTransport(http.DefaultTransport)}))\n\ttr, err := o.getTransport(srv.URL)\n\tassert.FatalError(t, err)\n\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\tassert.FatalError(t, err)\n\tacc := acme.Account{\n\t\tContact:   []string{\"max\", \"mariano\"},\n\t\tStatus:    \"valid\",\n\t\tOrdersURL: \"orders-url\",\n\t}\n\tac := &ACMEClient{\n\t\tclient: &http.Client{\n\t\t\tTransport: tr,\n\t\t},\n\t\tdirLoc: srv.URL,\n\t\tdir:    &dir,\n\t\tKey:    jwk,\n\t\tkid:    \"foobar\",\n\t}\n\n\ttests := map[string]func(t *testing.T) test{\n\t\t\"fail/account-not-configured\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tclient: &ACMEClient{},\n\t\t\t\tr1:     acme.NewError(acme.ErrorMalformedType, \"malformed request\"),\n\t\t\t\trc1:    400,\n\t\t\t\terr:    errors.New(\"acme client not configured with account\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/GET-nonce\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tclient: ac,\n\t\t\t\tr1:     acme.NewError(acme.ErrorMalformedType, \"malformed request\"),\n\t\t\t\trc1:    400,\n\t\t\t\terr:    errors.New(\"The request message was malformed\"),\n\t\t\t}\n\t\t},\n\t\t\"ok/jwk\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tclient:   ac,\n\t\t\t\tr1:       []byte{},\n\t\t\t\trc1:      200,\n\t\t\t\tr2:       acc,\n\t\t\t\trc2:      200,\n\t\t\t\tops:      []withHeaderOption{withJWK(ac)},\n\t\t\t\tjwkInJWS: true,\n\t\t\t}\n\t\t},\n\t\t\"ok/kid\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tclient: ac,\n\t\t\t\tr1:     []byte{},\n\t\t\t\trc1:    200,\n\t\t\t\tr2:     acc,\n\t\t\t\trc2:    200,\n\t\t\t\tops:    []withHeaderOption{withKid(ac)},\n\t\t\t}\n\t\t},\n\t}\n\n\texpectedNonce := \"abc123\"\n\turl := srv.URL + \"/foo\"\n\n\tfor name, run := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ttc := run(t)\n\n\t\t\ti := 0\n\t\t\tsrv.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\tassert.Equals(t, \"step-http-client/1.0\", r.Header.Get(\"User-Agent\")) // check default User-Agent header\n\n\t\t\t\tw.Header().Set(\"Replay-Nonce\", expectedNonce)\n\t\t\t\tif i == 0 {\n\t\t\t\t\trender.JSONStatus(w, r, tc.r1, tc.rc1)\n\t\t\t\t\ti++\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\t// validate jws request protected headers and body\n\t\t\t\tbody, err := io.ReadAll(r.Body)\n\t\t\t\tassert.FatalError(t, err)\n\t\t\t\tjws, err := jose.ParseJWS(string(body))\n\t\t\t\tassert.FatalError(t, err)\n\t\t\t\thdr := jws.Signatures[0].Protected\n\n\t\t\t\tassert.Equals(t, hdr.Nonce, expectedNonce)\n\t\t\t\tjwsURL, ok := hdr.ExtraHeaders[\"url\"].(string)\n\t\t\t\tassert.Fatal(t, ok)\n\t\t\t\tassert.Equals(t, jwsURL, url)\n\n\t\t\t\tif tc.jwkInJWS {\n\t\t\t\t\tassert.Equals(t, hdr.JSONWebKey.KeyID, ac.Key.KeyID)\n\t\t\t\t} else {\n\t\t\t\t\tassert.Equals(t, hdr.KeyID, ac.kid)\n\t\t\t\t}\n\n\t\t\t\trender.JSONStatus(w, r, tc.r2, tc.rc2)\n\t\t\t})\n\n\t\t\tif resp, err := tc.client.post(tc.payload, url, tc.ops...); err != nil {\n\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.err.Error())\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif assert.Nil(t, tc.err) {\n\t\t\t\t\tvar res acme.Account\n\t\t\t\t\tassert.FatalError(t, readJSON(resp.Body, &res))\n\t\t\t\t\tassert.Equals(t, res, acc)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestACMEClient_NewOrder(t *testing.T) {\n\ttype test struct {\n\t\tops      []withHeaderOption\n\t\tr1, r2   interface{}\n\t\trc1, rc2 int\n\t\terr      error\n\t}\n\n\tsrv := httptest.NewServer(nil)\n\tdefer srv.Close()\n\n\tdir := acmeAPI.Directory{\n\t\tNewNonce: srv.URL + \"/foo\",\n\t\tNewOrder: srv.URL + \"/bar\",\n\t}\n\t// Retrieve transport from options.\n\to := defaultClientOptions()\n\tassert.FatalError(t, o.apply([]ClientOption{WithTransport(http.DefaultTransport)}))\n\ttr, err := o.getTransport(srv.URL)\n\tassert.FatalError(t, err)\n\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\tassert.FatalError(t, err)\n\tnow := time.Now().UTC().Round(time.Second)\n\tnor := acmeAPI.NewOrderRequest{\n\t\tIdentifiers: []acme.Identifier{\n\t\t\t{Type: \"dns\", Value: \"example.com\"},\n\t\t\t{Type: \"dns\", Value: \"acme.example.com\"},\n\t\t},\n\t\tNotBefore: now,\n\t\tNotAfter:  now.Add(time.Minute),\n\t}\n\tnorb, err := json.Marshal(nor)\n\tassert.FatalError(t, err)\n\tord := acme.Order{\n\t\tStatus:      \"valid\",\n\t\tExpiresAt:   now, // \"soon\"\n\t\tFinalizeURL: \"finalize-url\",\n\t}\n\tac := &ACMEClient{\n\t\tclient: &http.Client{\n\t\t\tTransport: tr,\n\t\t},\n\t\tdirLoc: srv.URL,\n\t\tdir:    &dir,\n\t\tKey:    jwk,\n\t\tkid:    \"foobar\",\n\t}\n\n\ttests := map[string]func(t *testing.T) test{\n\t\t\"fail/client-post\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tr1:  acme.NewError(acme.ErrorMalformedType, \"malformed request\"),\n\t\t\t\trc1: 400,\n\t\t\t\terr: errors.New(\"The request message was malformed\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/newOrder-error\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tr1:  []byte{},\n\t\t\t\trc1: 200,\n\t\t\t\tr2:  acme.NewError(acme.ErrorMalformedType, \"malformed request\"),\n\t\t\t\trc2: 400,\n\t\t\t\tops: []withHeaderOption{withKid(ac)},\n\t\t\t\terr: errors.New(\"The request message was malformed\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/bad-order\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tr1:  []byte{},\n\t\t\t\trc1: 200,\n\t\t\t\tr2:  \"foo\",\n\t\t\t\trc2: 200,\n\t\t\t\tops: []withHeaderOption{withKid(ac)},\n\t\t\t\terr: errors.New(\"error reading http://127.0.0.1\"),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tr1:  []byte{},\n\t\t\t\trc1: 200,\n\t\t\t\tr2:  ord,\n\t\t\t\trc2: 200,\n\t\t\t\tops: []withHeaderOption{withKid(ac)},\n\t\t\t}\n\t\t},\n\t}\n\n\texpectedNonce := \"abc123\"\n\n\tfor name, run := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ttc := run(t)\n\n\t\t\ti := 0\n\t\t\tsrv.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\tassert.Equals(t, \"step-http-client/1.0\", r.Header.Get(\"User-Agent\")) // check default User-Agent header\n\n\t\t\t\tw.Header().Set(\"Replay-Nonce\", expectedNonce)\n\t\t\t\tif i == 0 {\n\t\t\t\t\trender.JSONStatus(w, r, tc.r1, tc.rc1)\n\t\t\t\t\ti++\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\t// validate jws request protected headers and body\n\t\t\t\tbody, err := io.ReadAll(r.Body)\n\t\t\t\tassert.FatalError(t, err)\n\t\t\t\tjws, err := jose.ParseJWS(string(body))\n\t\t\t\tassert.FatalError(t, err)\n\t\t\t\thdr := jws.Signatures[0].Protected\n\n\t\t\t\tassert.Equals(t, hdr.Nonce, expectedNonce)\n\t\t\t\tjwsURL, ok := hdr.ExtraHeaders[\"url\"].(string)\n\t\t\t\tassert.Fatal(t, ok)\n\t\t\t\tassert.Equals(t, jwsURL, dir.NewOrder)\n\t\t\t\tassert.Equals(t, hdr.KeyID, ac.kid)\n\n\t\t\t\tpayload, err := jws.Verify(ac.Key.Public())\n\t\t\t\tassert.FatalError(t, err)\n\t\t\t\tassert.Equals(t, payload, norb)\n\n\t\t\t\trender.JSONStatus(w, r, tc.r2, tc.rc2)\n\t\t\t})\n\n\t\t\tif res, err := ac.NewOrder(norb); err != nil {\n\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.err.Error())\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif assert.Nil(t, tc.err) {\n\t\t\t\t\tassert.Equals(t, *res, ord)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestACMEClient_GetOrder(t *testing.T) {\n\ttype test struct {\n\t\tr1, r2   interface{}\n\t\trc1, rc2 int\n\t\terr      error\n\t}\n\n\tsrv := httptest.NewServer(nil)\n\tdefer srv.Close()\n\n\tdir := acmeAPI.Directory{\n\t\tNewNonce: srv.URL + \"/foo\",\n\t}\n\t// Retrieve transport from options.\n\to := defaultClientOptions()\n\tassert.FatalError(t, o.apply([]ClientOption{WithTransport(http.DefaultTransport)}))\n\ttr, err := o.getTransport(srv.URL)\n\tassert.FatalError(t, err)\n\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\tassert.FatalError(t, err)\n\tord := acme.Order{\n\t\tStatus:      \"valid\",\n\t\tExpiresAt:   time.Now().UTC().Round(time.Second), // \"soon\"\n\t\tFinalizeURL: \"finalize-url\",\n\t}\n\tac := &ACMEClient{\n\t\tclient: &http.Client{\n\t\t\tTransport: tr,\n\t\t},\n\t\tdirLoc: srv.URL,\n\t\tdir:    &dir,\n\t\tKey:    jwk,\n\t\tkid:    \"foobar\",\n\t}\n\n\ttests := map[string]func(t *testing.T) test{\n\t\t\"fail/client-post\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tr1:  acme.NewError(acme.ErrorMalformedType, \"malformed request\"),\n\t\t\t\trc1: 400,\n\t\t\t\terr: errors.New(\"The request message was malformed\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/getOrder-error\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tr1:  []byte{},\n\t\t\t\trc1: 200,\n\t\t\t\tr2:  acme.NewError(acme.ErrorMalformedType, \"malformed request\"),\n\t\t\t\trc2: 400,\n\t\t\t\terr: errors.New(\"The request message was malformed\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/bad-order\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tr1:  []byte{},\n\t\t\t\trc1: 200,\n\t\t\t\tr2:  \"foo\",\n\t\t\t\trc2: 200,\n\t\t\t\terr: errors.New(\"error reading http://127.0.0.1\"),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tr1:  []byte{},\n\t\t\t\trc1: 200,\n\t\t\t\tr2:  ord,\n\t\t\t\trc2: 200,\n\t\t\t}\n\t\t},\n\t}\n\n\texpectedNonce := \"abc123\"\n\turl := srv.URL + \"/hullaballoo\"\n\n\tfor name, run := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ttc := run(t)\n\n\t\t\ti := 0\n\t\t\tsrv.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\tassert.Equals(t, \"step-http-client/1.0\", r.Header.Get(\"User-Agent\")) // check default User-Agent header\n\n\t\t\t\tw.Header().Set(\"Replay-Nonce\", expectedNonce)\n\t\t\t\tif i == 0 {\n\t\t\t\t\trender.JSONStatus(w, r, tc.r1, tc.rc1)\n\t\t\t\t\ti++\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\t// validate jws request protected headers and body\n\t\t\t\tbody, err := io.ReadAll(r.Body)\n\t\t\t\tassert.FatalError(t, err)\n\t\t\t\tjws, err := jose.ParseJWS(string(body))\n\t\t\t\tassert.FatalError(t, err)\n\t\t\t\thdr := jws.Signatures[0].Protected\n\n\t\t\t\tassert.Equals(t, hdr.Nonce, expectedNonce)\n\t\t\t\tjwsURL, ok := hdr.ExtraHeaders[\"url\"].(string)\n\t\t\t\tassert.Fatal(t, ok)\n\t\t\t\tassert.Equals(t, jwsURL, url)\n\t\t\t\tassert.Equals(t, hdr.KeyID, ac.kid)\n\n\t\t\t\tpayload, err := jws.Verify(ac.Key.Public())\n\t\t\t\tassert.FatalError(t, err)\n\t\t\t\tassert.Equals(t, len(payload), 0)\n\n\t\t\t\trender.JSONStatus(w, r, tc.r2, tc.rc2)\n\t\t\t})\n\n\t\t\tif res, err := ac.GetOrder(url); err != nil {\n\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.err.Error())\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif assert.Nil(t, tc.err) {\n\t\t\t\t\tassert.Equals(t, *res, ord)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestACMEClient_GetAuthz(t *testing.T) {\n\ttype test struct {\n\t\tr1, r2   interface{}\n\t\trc1, rc2 int\n\t\terr      error\n\t}\n\n\tsrv := httptest.NewServer(nil)\n\tdefer srv.Close()\n\n\tdir := acmeAPI.Directory{\n\t\tNewNonce: srv.URL + \"/foo\",\n\t}\n\t// Retrieve transport from options.\n\to := defaultClientOptions()\n\tassert.FatalError(t, o.apply([]ClientOption{WithTransport(http.DefaultTransport)}))\n\ttr, err := o.getTransport(srv.URL)\n\tassert.FatalError(t, err)\n\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\tassert.FatalError(t, err)\n\taz := acme.Authorization{\n\t\tStatus:     \"valid\",\n\t\tExpiresAt:  time.Now().UTC().Round(time.Second),\n\t\tIdentifier: acme.Identifier{Type: \"dns\", Value: \"example.com\"},\n\t}\n\tac := &ACMEClient{\n\t\tclient: &http.Client{\n\t\t\tTransport: tr,\n\t\t},\n\t\tdirLoc: srv.URL,\n\t\tdir:    &dir,\n\t\tKey:    jwk,\n\t\tkid:    \"foobar\",\n\t}\n\n\ttests := map[string]func(t *testing.T) test{\n\t\t\"fail/client-post\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tr1:  acme.NewError(acme.ErrorMalformedType, \"malformed request\"),\n\t\t\t\trc1: 400,\n\t\t\t\terr: errors.New(\"The request message was malformed\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/getChallenge-error\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tr1:  []byte{},\n\t\t\t\trc1: 200,\n\t\t\t\tr2:  acme.NewError(acme.ErrorMalformedType, \"malformed request\"),\n\t\t\t\trc2: 400,\n\t\t\t\terr: errors.New(\"The request message was malformed\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/bad-challenge\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tr1:  []byte{},\n\t\t\t\trc1: 200,\n\t\t\t\tr2:  \"foo\",\n\t\t\t\trc2: 200,\n\t\t\t\terr: errors.New(\"error reading http://127.0.0.1\"),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tr1:  []byte{},\n\t\t\t\trc1: 200,\n\t\t\t\tr2:  az,\n\t\t\t\trc2: 200,\n\t\t\t}\n\t\t},\n\t}\n\n\texpectedNonce := \"abc123\"\n\turl := srv.URL + \"/hullaballoo\"\n\n\tfor name, run := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ttc := run(t)\n\n\t\t\ti := 0\n\t\t\tsrv.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\tassert.Equals(t, \"step-http-client/1.0\", r.Header.Get(\"User-Agent\")) // check default User-Agent header\n\n\t\t\t\tw.Header().Set(\"Replay-Nonce\", expectedNonce)\n\t\t\t\tif i == 0 {\n\t\t\t\t\trender.JSONStatus(w, r, tc.r1, tc.rc1)\n\t\t\t\t\ti++\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\t// validate jws request protected headers and body\n\t\t\t\tbody, err := io.ReadAll(r.Body)\n\t\t\t\tassert.FatalError(t, err)\n\t\t\t\tjws, err := jose.ParseJWS(string(body))\n\t\t\t\tassert.FatalError(t, err)\n\t\t\t\thdr := jws.Signatures[0].Protected\n\n\t\t\t\tassert.Equals(t, hdr.Nonce, expectedNonce)\n\t\t\t\tjwsURL, ok := hdr.ExtraHeaders[\"url\"].(string)\n\t\t\t\tassert.Fatal(t, ok)\n\t\t\t\tassert.Equals(t, jwsURL, url)\n\t\t\t\tassert.Equals(t, hdr.KeyID, ac.kid)\n\n\t\t\t\tpayload, err := jws.Verify(ac.Key.Public())\n\t\t\t\tassert.FatalError(t, err)\n\t\t\t\tassert.Equals(t, len(payload), 0)\n\n\t\t\t\trender.JSONStatus(w, r, tc.r2, tc.rc2)\n\t\t\t})\n\n\t\t\tif res, err := ac.GetAuthz(url); err != nil {\n\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.err.Error())\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif assert.Nil(t, tc.err) {\n\t\t\t\t\tassert.Equals(t, *res, az)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestACMEClient_GetChallenge(t *testing.T) {\n\ttype test struct {\n\t\tr1, r2   interface{}\n\t\trc1, rc2 int\n\t\terr      error\n\t}\n\n\tsrv := httptest.NewServer(nil)\n\tdefer srv.Close()\n\n\tdir := acmeAPI.Directory{\n\t\tNewNonce: srv.URL + \"/foo\",\n\t}\n\t// Retrieve transport from options.\n\to := defaultClientOptions()\n\tassert.FatalError(t, o.apply([]ClientOption{WithTransport(http.DefaultTransport)}))\n\ttr, err := o.getTransport(srv.URL)\n\tassert.FatalError(t, err)\n\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\tassert.FatalError(t, err)\n\tch := acme.Challenge{\n\t\tType:   \"http-01\",\n\t\tStatus: \"valid\",\n\t\tToken:  \"foo\",\n\t}\n\tac := &ACMEClient{\n\t\tclient: &http.Client{\n\t\t\tTransport: tr,\n\t\t},\n\t\tdirLoc: srv.URL,\n\t\tdir:    &dir,\n\t\tKey:    jwk,\n\t\tkid:    \"foobar\",\n\t}\n\n\ttests := map[string]func(t *testing.T) test{\n\t\t\"fail/client-post\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tr1:  acme.NewError(acme.ErrorMalformedType, \"malformed request\"),\n\t\t\t\trc1: 400,\n\t\t\t\terr: errors.New(\"The request message was malformed\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/getChallenge-error\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tr1:  []byte{},\n\t\t\t\trc1: 200,\n\t\t\t\tr2:  acme.NewError(acme.ErrorMalformedType, \"malformed request\"),\n\t\t\t\trc2: 400,\n\t\t\t\terr: errors.New(\"The request message was malformed\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/bad-challenge\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tr1:  []byte{},\n\t\t\t\trc1: 200,\n\t\t\t\tr2:  \"foo\",\n\t\t\t\trc2: 200,\n\t\t\t\terr: errors.New(\"error reading http://127.0.0.1\"),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tr1:  []byte{},\n\t\t\t\trc1: 200,\n\t\t\t\tr2:  ch,\n\t\t\t\trc2: 200,\n\t\t\t}\n\t\t},\n\t}\n\n\texpectedNonce := \"abc123\"\n\turl := srv.URL + \"/hullaballoo\"\n\n\tfor name, run := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ttc := run(t)\n\n\t\t\ti := 0\n\t\t\tsrv.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\tassert.Equals(t, \"step-http-client/1.0\", r.Header.Get(\"User-Agent\")) // check default User-Agent header\n\n\t\t\t\tw.Header().Set(\"Replay-Nonce\", expectedNonce)\n\t\t\t\tif i == 0 {\n\t\t\t\t\trender.JSONStatus(w, r, tc.r1, tc.rc1)\n\t\t\t\t\ti++\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\t// validate jws request protected headers and body\n\t\t\t\tbody, err := io.ReadAll(r.Body)\n\t\t\t\tassert.FatalError(t, err)\n\t\t\t\tjws, err := jose.ParseJWS(string(body))\n\t\t\t\tassert.FatalError(t, err)\n\t\t\t\thdr := jws.Signatures[0].Protected\n\n\t\t\t\tassert.Equals(t, hdr.Nonce, expectedNonce)\n\t\t\t\tjwsURL, ok := hdr.ExtraHeaders[\"url\"].(string)\n\t\t\t\tassert.Fatal(t, ok)\n\t\t\t\tassert.Equals(t, jwsURL, url)\n\t\t\t\tassert.Equals(t, hdr.KeyID, ac.kid)\n\n\t\t\t\tpayload, err := jws.Verify(ac.Key.Public())\n\t\t\t\tassert.FatalError(t, err)\n\n\t\t\t\tassert.Equals(t, len(payload), 0)\n\n\t\t\t\trender.JSONStatus(w, r, tc.r2, tc.rc2)\n\t\t\t})\n\n\t\t\tif res, err := ac.GetChallenge(url); err != nil {\n\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.err.Error())\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif assert.Nil(t, tc.err) {\n\t\t\t\t\tassert.Equals(t, *res, ch)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestACMEClient_ValidateChallenge(t *testing.T) {\n\ttype test struct {\n\t\tr1, r2   interface{}\n\t\trc1, rc2 int\n\t\terr      error\n\t}\n\n\tsrv := httptest.NewServer(nil)\n\tdefer srv.Close()\n\n\tdir := acmeAPI.Directory{\n\t\tNewNonce: srv.URL + \"/foo\",\n\t}\n\t// Retrieve transport from options.\n\to := defaultClientOptions()\n\tassert.FatalError(t, o.apply([]ClientOption{WithTransport(http.DefaultTransport)}))\n\ttr, err := o.getTransport(srv.URL)\n\tassert.FatalError(t, err)\n\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\tassert.FatalError(t, err)\n\tch := acme.Challenge{\n\t\tType:   \"http-01\",\n\t\tStatus: \"valid\",\n\t\tToken:  \"foo\",\n\t}\n\tac := &ACMEClient{\n\t\tclient: &http.Client{\n\t\t\tTransport: tr,\n\t\t},\n\t\tdirLoc: srv.URL,\n\t\tdir:    &dir,\n\t\tKey:    jwk,\n\t\tkid:    \"foobar\",\n\t}\n\n\ttests := map[string]func(t *testing.T) test{\n\t\t\"fail/client-post\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tr1:  acme.NewError(acme.ErrorMalformedType, \"malformed request\"),\n\t\t\t\trc1: 400,\n\t\t\t\terr: errors.New(\"The request message was malformed\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/getChallenge-error\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tr1:  []byte{},\n\t\t\t\trc1: 200,\n\t\t\t\tr2:  acme.NewError(acme.ErrorMalformedType, \"malformed request\"),\n\t\t\t\trc2: 400,\n\t\t\t\terr: errors.New(\"The request message was malformed\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/bad-challenge\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tr1:  []byte{},\n\t\t\t\trc1: 200,\n\t\t\t\tr2:  \"foo\",\n\t\t\t\trc2: 200,\n\t\t\t\terr: errors.New(\"error reading http://127.0.0.1\"),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tr1:  []byte{},\n\t\t\t\trc1: 200,\n\t\t\t\tr2:  ch,\n\t\t\t\trc2: 200,\n\t\t\t}\n\t\t},\n\t}\n\n\texpectedNonce := \"abc123\"\n\turl := srv.URL + \"/hullaballoo\"\n\n\tfor name, run := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ttc := run(t)\n\n\t\t\ti := 0\n\t\t\tsrv.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\tassert.Equals(t, \"step-http-client/1.0\", r.Header.Get(\"User-Agent\")) // check default User-Agent header\n\n\t\t\t\tw.Header().Set(\"Replay-Nonce\", expectedNonce)\n\t\t\t\tif i == 0 {\n\t\t\t\t\trender.JSONStatus(w, r, tc.r1, tc.rc1)\n\t\t\t\t\ti++\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\t// validate jws request protected headers and body\n\t\t\t\tbody, err := io.ReadAll(r.Body)\n\t\t\t\tassert.FatalError(t, err)\n\t\t\t\tjws, err := jose.ParseJWS(string(body))\n\t\t\t\tassert.FatalError(t, err)\n\t\t\t\thdr := jws.Signatures[0].Protected\n\n\t\t\t\tassert.Equals(t, hdr.Nonce, expectedNonce)\n\t\t\t\tjwsURL, ok := hdr.ExtraHeaders[\"url\"].(string)\n\t\t\t\tassert.Fatal(t, ok)\n\t\t\t\tassert.Equals(t, jwsURL, url)\n\t\t\t\tassert.Equals(t, hdr.KeyID, ac.kid)\n\n\t\t\t\tpayload, err := jws.Verify(ac.Key.Public())\n\t\t\t\tassert.FatalError(t, err)\n\n\t\t\t\tassert.Equals(t, payload, []byte(\"{}\"))\n\n\t\t\t\trender.JSONStatus(w, r, tc.r2, tc.rc2)\n\t\t\t})\n\n\t\t\tif err := ac.ValidateChallenge(url); err != nil {\n\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.err.Error())\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestACMEClient_ValidateWithPayload(t *testing.T) {\n\tkey, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\tassert.FatalError(t, err)\n\n\tsrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tassert.Equals(t, \"step-http-client/1.0\", r.Header.Get(\"User-Agent\")) // check default User-Agent header\n\n\t\tt.Log(r.RequestURI)\n\t\tw.Header().Set(\"Replay-Nonce\", \"nonce\")\n\t\tswitch r.RequestURI {\n\t\tcase \"/nonce\":\n\t\t\trender.JSONStatus(w, r, []byte{}, 200)\n\t\t\treturn\n\t\tcase \"/fail-nonce\":\n\t\t\trender.JSONStatus(w, r, acme.NewError(acme.ErrorMalformedType, \"malformed request\"), 400)\n\t\t\treturn\n\t\t}\n\n\t\t// validate jws request protected headers and body\n\t\tbody, err := io.ReadAll(r.Body)\n\t\tassert.FatalError(t, err)\n\n\t\tjws, err := jose.ParseJWS(string(body))\n\t\tassert.FatalError(t, err)\n\n\t\thdr := jws.Signatures[0].Protected\n\t\tassert.Equals(t, hdr.Nonce, \"nonce\")\n\n\t\t_, ok := hdr.ExtraHeaders[\"url\"].(string)\n\t\tassert.Fatal(t, ok)\n\t\tassert.Equals(t, hdr.KeyID, \"kid\")\n\n\t\tpayload, err := jws.Verify(key.Public())\n\t\tassert.FatalError(t, err)\n\t\tassert.Equals(t, payload, []byte(\"the-payload\"))\n\n\t\tswitch r.RequestURI {\n\t\tcase \"/ok\":\n\t\t\trender.JSONStatus(w, r, acme.Challenge{\n\t\t\t\tType:   \"device-attestation-01\",\n\t\t\t\tStatus: \"valid\",\n\t\t\t\tToken:  \"foo\",\n\t\t\t}, 200)\n\t\tcase \"/fail\":\n\t\t\trender.JSONStatus(w, r, acme.NewError(acme.ErrorMalformedType, \"malformed request\"), 400)\n\t\t}\n\t}))\n\tdefer srv.Close()\n\n\ttype fields struct {\n\t\tclient *http.Client\n\t\tdirLoc string\n\t\tdir    *acmeAPI.Directory\n\t\tacc    *acme.Account\n\t\tKey    *jose.JSONWebKey\n\t\tkid    string\n\t}\n\ttype args struct {\n\t\turl     string\n\t\tpayload []byte\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tfields  fields\n\t\targs    args\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", fields{srv.Client(), srv.URL, &acmeAPI.Directory{\n\t\t\tNewNonce: srv.URL + \"/nonce\",\n\t\t}, nil, key, \"kid\"}, args{srv.URL + \"/ok\", []byte(\"the-payload\")}, false},\n\t\t{\"fail nonce\", fields{srv.Client(), srv.URL, &acmeAPI.Directory{\n\t\t\tNewNonce: srv.URL + \"/fail-nonce\",\n\t\t}, nil, key, \"kid\"}, args{srv.URL + \"/ok\", []byte(\"the-payload\")}, true},\n\t\t{\"fail payload\", fields{srv.Client(), srv.URL, &acmeAPI.Directory{\n\t\t\tNewNonce: srv.URL + \"/nonce\",\n\t\t}, nil, key, \"kid\"}, args{srv.URL + \"/fail\", []byte(\"the-payload\")}, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tc := &ACMEClient{\n\t\t\t\tclient: tt.fields.client,\n\t\t\t\tdirLoc: tt.fields.dirLoc,\n\t\t\t\tdir:    tt.fields.dir,\n\t\t\t\tacc:    tt.fields.acc,\n\t\t\t\tKey:    tt.fields.Key,\n\t\t\t\tkid:    tt.fields.kid,\n\t\t\t}\n\t\t\tif err := c.ValidateWithPayload(tt.args.url, tt.args.payload); (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"ACMEClient.ValidateWithPayload() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestACMEClient_FinalizeOrder(t *testing.T) {\n\ttype test struct {\n\t\tr1, r2   interface{}\n\t\trc1, rc2 int\n\t\terr      error\n\t}\n\n\tsrv := httptest.NewServer(nil)\n\tdefer srv.Close()\n\n\tdir := acmeAPI.Directory{\n\t\tNewNonce: srv.URL + \"/foo\",\n\t}\n\t// Retrieve transport from options.\n\to := defaultClientOptions()\n\tassert.FatalError(t, o.apply([]ClientOption{WithTransport(http.DefaultTransport)}))\n\ttr, err := o.getTransport(srv.URL)\n\tassert.FatalError(t, err)\n\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\tassert.FatalError(t, err)\n\tord := acme.Order{\n\t\tStatus:         \"valid\",\n\t\tExpiresAt:      time.Now(), // \"soon\"\n\t\tFinalizeURL:    \"finalize-url\",\n\t\tCertificateURL: \"cert-url\",\n\t}\n\t_csr, err := pemutil.Read(\"../authority/testdata/certs/foo.csr\")\n\tassert.FatalError(t, err)\n\tcsr, ok := _csr.(*x509.CertificateRequest)\n\tassert.Fatal(t, ok)\n\tfr := acmeAPI.FinalizeRequest{CSR: base64.RawURLEncoding.EncodeToString(csr.Raw)}\n\tfrb, err := json.Marshal(fr)\n\tassert.FatalError(t, err)\n\tac := &ACMEClient{\n\t\tclient: &http.Client{\n\t\t\tTransport: tr,\n\t\t},\n\t\tdirLoc: srv.URL,\n\t\tdir:    &dir,\n\t\tKey:    jwk,\n\t\tkid:    \"foobar\",\n\t}\n\n\ttests := map[string]func(t *testing.T) test{\n\t\t\"fail/client-post\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tr1:  acme.NewError(acme.ErrorMalformedType, \"malformed request\"),\n\t\t\t\trc1: 400,\n\t\t\t\terr: errors.New(\"The request message was malformed\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/finalizeOrder-error\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tr1:  []byte{},\n\t\t\t\trc1: 200,\n\t\t\t\tr2:  acme.NewError(acme.ErrorMalformedType, \"malformed request\"),\n\t\t\t\trc2: 400,\n\t\t\t\terr: errors.New(\"The request message was malformed\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/bad-order\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tr1:  []byte{},\n\t\t\t\trc1: 200,\n\t\t\t\tr2:  \"foo\",\n\t\t\t\trc2: 200,\n\t\t\t\terr: errors.New(\"error reading http://127.0.0.1\"),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tr1:  []byte{},\n\t\t\t\trc1: 200,\n\t\t\t\tr2:  ord,\n\t\t\t\trc2: 200,\n\t\t\t}\n\t\t},\n\t}\n\n\texpectedNonce := \"abc123\"\n\turl := srv.URL + \"/hullaballoo\"\n\n\tfor name, run := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ttc := run(t)\n\n\t\t\ti := 0\n\t\t\tsrv.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\tassert.Equals(t, \"step-http-client/1.0\", r.Header.Get(\"User-Agent\")) // check default User-Agent header\n\n\t\t\t\tw.Header().Set(\"Replay-Nonce\", expectedNonce)\n\t\t\t\tif i == 0 {\n\t\t\t\t\trender.JSONStatus(w, r, tc.r1, tc.rc1)\n\t\t\t\t\ti++\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\t// validate jws request protected headers and body\n\t\t\t\tbody, err := io.ReadAll(r.Body)\n\t\t\t\tassert.FatalError(t, err)\n\t\t\t\tjws, err := jose.ParseJWS(string(body))\n\t\t\t\tassert.FatalError(t, err)\n\t\t\t\thdr := jws.Signatures[0].Protected\n\n\t\t\t\tassert.Equals(t, hdr.Nonce, expectedNonce)\n\t\t\t\tjwsURL, ok := hdr.ExtraHeaders[\"url\"].(string)\n\t\t\t\tassert.Fatal(t, ok)\n\t\t\t\tassert.Equals(t, jwsURL, url)\n\t\t\t\tassert.Equals(t, hdr.KeyID, ac.kid)\n\n\t\t\t\tpayload, err := jws.Verify(ac.Key.Public())\n\t\t\t\tassert.FatalError(t, err)\n\t\t\t\tassert.Equals(t, payload, frb)\n\n\t\t\t\trender.JSONStatus(w, r, tc.r2, tc.rc2)\n\t\t\t})\n\n\t\t\tif err := ac.FinalizeOrder(url, csr); err != nil {\n\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.err.Error())\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestACMEClient_GetAccountOrders(t *testing.T) {\n\ttype test struct {\n\t\tr1, r2   interface{}\n\t\trc1, rc2 int\n\t\terr      error\n\t\tclient   *ACMEClient\n\t}\n\n\tsrv := httptest.NewServer(nil)\n\tdefer srv.Close()\n\n\tdir := acmeAPI.Directory{\n\t\tNewNonce: srv.URL + \"/foo\",\n\t}\n\t// Retrieve transport from options.\n\to := defaultClientOptions()\n\tassert.FatalError(t, o.apply([]ClientOption{WithTransport(http.DefaultTransport)}))\n\ttr, err := o.getTransport(srv.URL)\n\tassert.FatalError(t, err)\n\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\tassert.FatalError(t, err)\n\torders := []string{\"foo\", \"bar\", \"baz\"}\n\tac := &ACMEClient{\n\t\tclient: &http.Client{\n\t\t\tTransport: tr,\n\t\t},\n\t\tdirLoc: srv.URL,\n\t\tdir:    &dir,\n\t\tKey:    jwk,\n\t\tkid:    \"foobar\",\n\t\tacc: &acme.Account{\n\t\t\tContact:   []string{\"max\", \"mariano\"},\n\t\t\tStatus:    \"valid\",\n\t\t\tOrdersURL: srv.URL + \"/orders-url\",\n\t\t},\n\t}\n\n\ttests := map[string]func(t *testing.T) test{\n\t\t\"fail/account-not-configured\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tclient: &ACMEClient{},\n\t\t\t\terr:    errors.New(\"acme client not configured with account\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/client-post\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tclient: ac,\n\t\t\t\tr1:     acme.NewError(acme.ErrorMalformedType, \"malformed request\"),\n\t\t\t\trc1:    400,\n\t\t\t\terr:    errors.New(\"The request message was malformed\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/getAccountOrders-error\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tclient: ac,\n\t\t\t\tr1:     []byte{},\n\t\t\t\trc1:    200,\n\t\t\t\tr2:     acme.NewError(acme.ErrorMalformedType, \"malformed request\"),\n\t\t\t\trc2:    400,\n\t\t\t\terr:    errors.New(\"The request message was malformed\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/bad-accountOrders\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tclient: ac,\n\t\t\t\tr1:     []byte{},\n\t\t\t\trc1:    200,\n\t\t\t\tr2:     \"foo\",\n\t\t\t\trc2:    200,\n\t\t\t\terr:    errors.New(\"error reading http://127.0.0.1\"),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tclient: ac,\n\t\t\t\tr1:     []byte{},\n\t\t\t\trc1:    200,\n\t\t\t\tr2:     orders,\n\t\t\t\trc2:    200,\n\t\t\t}\n\t\t},\n\t}\n\n\texpectedNonce := \"abc123\"\n\n\tfor name, run := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ttc := run(t)\n\n\t\t\ti := 0\n\t\t\tsrv.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\tassert.Equals(t, \"step-http-client/1.0\", r.Header.Get(\"User-Agent\")) // check default User-Agent header\n\n\t\t\t\tw.Header().Set(\"Replay-Nonce\", expectedNonce)\n\t\t\t\tif i == 0 {\n\t\t\t\t\trender.JSONStatus(w, r, tc.r1, tc.rc1)\n\t\t\t\t\ti++\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\t// validate jws request protected headers and body\n\t\t\t\tbody, err := io.ReadAll(r.Body)\n\t\t\t\tassert.FatalError(t, err)\n\t\t\t\tjws, err := jose.ParseJWS(string(body))\n\t\t\t\tassert.FatalError(t, err)\n\t\t\t\thdr := jws.Signatures[0].Protected\n\n\t\t\t\tassert.Equals(t, hdr.Nonce, expectedNonce)\n\t\t\t\tjwsURL, ok := hdr.ExtraHeaders[\"url\"].(string)\n\t\t\t\tassert.Fatal(t, ok)\n\t\t\t\tassert.Equals(t, jwsURL, ac.acc.OrdersURL)\n\t\t\t\tassert.Equals(t, hdr.KeyID, ac.kid)\n\n\t\t\t\tpayload, err := jws.Verify(ac.Key.Public())\n\t\t\t\tassert.FatalError(t, err)\n\t\t\t\tassert.Equals(t, len(payload), 0)\n\n\t\t\t\trender.JSONStatus(w, r, tc.r2, tc.rc2)\n\t\t\t})\n\n\t\t\tif res, err := tc.client.GetAccountOrders(); err != nil {\n\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.err.Error())\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif assert.Nil(t, tc.err) {\n\t\t\t\t\tassert.Equals(t, res, orders)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestACMEClient_GetCertificate(t *testing.T) {\n\ttype test struct {\n\t\tr1, r2    interface{}\n\t\tcertBytes []byte\n\t\trc1, rc2  int\n\t\terr       error\n\t}\n\n\tsrv := httptest.NewServer(nil)\n\tdefer srv.Close()\n\n\tdir := acmeAPI.Directory{\n\t\tNewNonce: srv.URL + \"/foo\",\n\t}\n\t// Retrieve transport from options.\n\to := defaultClientOptions()\n\tassert.FatalError(t, o.apply([]ClientOption{WithTransport(http.DefaultTransport)}))\n\ttr, err := o.getTransport(srv.URL)\n\tassert.FatalError(t, err)\n\tjwk, err := jose.GenerateJWK(\"EC\", \"P-256\", \"ES256\", \"sig\", \"\", 0)\n\tassert.FatalError(t, err)\n\tleaf, err := pemutil.ReadCertificate(\"../authority/testdata/certs/foo.crt\")\n\tassert.FatalError(t, err)\n\tleafb := pem.EncodeToMemory(&pem.Block{\n\t\tType:  \"Certificate\",\n\t\tBytes: leaf.Raw,\n\t})\n\t//nolint:gocritic\n\tcertBytes := append(leafb, leafb...)\n\tcertBytes = append(certBytes, leafb...)\n\tac := &ACMEClient{\n\t\tclient: &http.Client{\n\t\t\tTransport: tr,\n\t\t},\n\t\tdirLoc: srv.URL,\n\t\tdir:    &dir,\n\t\tKey:    jwk,\n\t\tkid:    \"foobar\",\n\t\tacc: &acme.Account{\n\t\t\tContact:   []string{\"max\", \"mariano\"},\n\t\t\tStatus:    \"valid\",\n\t\t\tOrdersURL: srv.URL + \"/orders-url\",\n\t\t},\n\t}\n\n\ttests := map[string]func(t *testing.T) test{\n\t\t\"fail/client-post\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tr1:  acme.NewError(acme.ErrorMalformedType, \"malformed request\"),\n\t\t\t\trc1: 400,\n\t\t\t\terr: errors.New(\"The request message was malformed\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/getAccountOrders-error\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tr1:  []byte{},\n\t\t\t\trc1: 200,\n\t\t\t\tr2:  acme.NewError(acme.ErrorMalformedType, \"malformed request\"),\n\t\t\t\trc2: 400,\n\t\t\t\terr: errors.New(\"The request message was malformed\"),\n\t\t\t}\n\t\t},\n\t\t\"fail/bad-certificate\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tr1:  []byte{},\n\t\t\t\trc1: 200,\n\t\t\t\tr2:  \"foo\",\n\t\t\t\trc2: 200,\n\t\t\t\terr: errors.New(\"failed to parse any certificates from response\"),\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tr1:        []byte{},\n\t\t\t\trc1:       200,\n\t\t\t\tcertBytes: certBytes,\n\t\t\t}\n\t\t},\n\t}\n\n\texpectedNonce := \"abc123\"\n\turl := srv.URL + \"/cert/foo\"\n\n\tfor name, run := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ttc := run(t)\n\n\t\t\ti := 0\n\t\t\tsrv.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\tassert.Equals(t, \"step-http-client/1.0\", r.Header.Get(\"User-Agent\")) // check default User-Agent header\n\n\t\t\t\tw.Header().Set(\"Replay-Nonce\", expectedNonce)\n\t\t\t\tif i == 0 {\n\t\t\t\t\trender.JSONStatus(w, r, tc.r1, tc.rc1)\n\t\t\t\t\ti++\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\t// validate jws request protected headers and body\n\t\t\t\tbody, err := io.ReadAll(r.Body)\n\t\t\t\tassert.FatalError(t, err)\n\t\t\t\tjws, err := jose.ParseJWS(string(body))\n\t\t\t\tassert.FatalError(t, err)\n\t\t\t\thdr := jws.Signatures[0].Protected\n\n\t\t\t\tassert.Equals(t, hdr.Nonce, expectedNonce)\n\t\t\t\tjwsURL, ok := hdr.ExtraHeaders[\"url\"].(string)\n\t\t\t\tassert.Fatal(t, ok)\n\t\t\t\tassert.Equals(t, jwsURL, url)\n\t\t\t\tassert.Equals(t, hdr.KeyID, ac.kid)\n\n\t\t\t\tpayload, err := jws.Verify(ac.Key.Public())\n\t\t\t\tassert.FatalError(t, err)\n\t\t\t\tassert.Equals(t, len(payload), 0)\n\n\t\t\t\tif tc.certBytes != nil {\n\t\t\t\t\tw.Write(tc.certBytes)\n\t\t\t\t} else {\n\t\t\t\t\trender.JSONStatus(w, r, tc.r2, tc.rc2)\n\t\t\t\t}\n\t\t\t})\n\n\t\t\tif crt, chain, err := ac.GetCertificate(url); err != nil {\n\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.err.Error())\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif assert.Nil(t, tc.err) {\n\t\t\t\t\tassert.Equals(t, crt, leaf)\n\t\t\t\t\tassert.Equals(t, chain, []*x509.Certificate{leaf, leaf})\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "ca/adminClient.go",
    "content": "package ca\n\nimport (\n\t\"bytes\"\n\t\"crypto/x509\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"path\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n\t\"google.golang.org/protobuf/encoding/protojson\"\n\n\t\"github.com/smallstep/cli-utils/token\"\n\t\"github.com/smallstep/cli-utils/token/provision\"\n\t\"github.com/smallstep/linkedca\"\n\t\"go.step.sm/crypto/jose\"\n\t\"go.step.sm/crypto/randutil\"\n\n\tadminAPI \"github.com/smallstep/certificates/authority/admin/api\"\n\t\"github.com/smallstep/certificates/authority/provisioner\"\n\t\"github.com/smallstep/certificates/errs\"\n)\n\nconst (\n\tadminURLPrefix = \"admin\"\n\tadminIssuer    = \"step-admin-client/1.0\"\n)\n\n// AdminClient implements an HTTP client for the CA server.\ntype AdminClient struct {\n\tclient      *uaClient\n\tendpoint    *url.URL\n\tretryFunc   RetryFunc\n\topts        []ClientOption\n\tx5cJWK      *jose.JSONWebKey\n\tx5cCertFile string\n\tx5cCertStrs []string\n\tx5cCert     *x509.Certificate\n\tx5cSubject  string\n}\n\nvar ErrAdminAPINotImplemented = errors.New(\"admin API not implemented\")\nvar ErrAdminAPINotAuthorized = errors.New(\"admin API not authorized\")\n\n// AdminClientError is the client side representation of an\n// AdminError returned by the CA.\ntype AdminClientError struct {\n\tType    string `json:\"type\"`\n\tDetail  string `json:\"detail\"`\n\tMessage string `json:\"message\"`\n}\n\n// Error returns the AdminClientError message as the error message\nfunc (e *AdminClientError) Error() string {\n\treturn e.Message\n}\n\n// defaultClientOptions returns a new [clientOptions] with a\n// default timeout set.\nfunc defaultClientOptions() clientOptions {\n\treturn clientOptions{\n\t\ttimeout: 15 * time.Second,\n\t}\n}\n\n// NewAdminClient creates a new AdminClient with the given endpoint and options.\nfunc NewAdminClient(endpoint string, opts ...ClientOption) (*AdminClient, error) {\n\tu, err := parseEndpoint(endpoint)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Retrieve transport from options.\n\to := defaultClientOptions()\n\tif err := o.apply(opts); err != nil {\n\t\treturn nil, err\n\t}\n\ttr, err := o.getTransport(endpoint)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &AdminClient{\n\t\tclient:      newClient(tr, o.timeout),\n\t\tendpoint:    u,\n\t\tretryFunc:   o.retryFunc,\n\t\topts:        opts,\n\t\tx5cJWK:      o.x5cJWK,\n\t\tx5cCertFile: o.x5cCertFile,\n\t\tx5cCertStrs: o.x5cCertStrs,\n\t\tx5cCert:     o.x5cCert,\n\t\tx5cSubject:  o.x5cSubject,\n\t}, nil\n}\n\nfunc (c *AdminClient) generateAdminToken(aud *url.URL) (string, error) {\n\t// A random jwt id will be used to identify duplicated tokens\n\tjwtID, err := randutil.Hex(64) // 256 bits\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\t// Drop any query string parameter from the token audience\n\taud = &url.URL{\n\t\tScheme: aud.Scheme,\n\t\tHost:   aud.Host,\n\t\tPath:   aud.Path,\n\t}\n\n\tnow := time.Now()\n\ttokOptions := []token.Options{\n\t\ttoken.WithJWTID(jwtID),\n\t\ttoken.WithKid(c.x5cJWK.KeyID),\n\t\ttoken.WithIssuer(adminIssuer),\n\t\ttoken.WithAudience(aud.String()),\n\t\ttoken.WithValidity(now, now.Add(token.DefaultValidity)),\n\t\ttoken.WithX5CCerts(c.x5cCertStrs),\n\t}\n\n\ttok, err := provision.New(c.x5cSubject, tokOptions...)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn tok.SignedString(c.x5cJWK.Algorithm, c.x5cJWK.Key)\n}\n\nfunc (c *AdminClient) retryOnError(r *http.Response) bool {\n\tif c.retryFunc != nil {\n\t\tif c.retryFunc(r.StatusCode) {\n\t\t\to := defaultClientOptions()\n\t\t\tif err := o.apply(c.opts); err != nil {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\ttr, err := o.getTransport(c.endpoint.String())\n\t\t\tif err != nil {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tr.Body.Close()\n\t\t\tc.client.SetTransport(tr)\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// IsEnabled checks if the admin API is enabled.\nfunc (c *AdminClient) IsEnabled() error {\n\tu := c.endpoint.ResolveReference(&url.URL{Path: path.Join(adminURLPrefix, \"admins\")})\n\tresp, err := c.client.Get(u.String())\n\tif err != nil {\n\t\treturn clientError(err)\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode < http.StatusBadRequest {\n\t\treturn nil\n\t}\n\tswitch resp.StatusCode {\n\tcase http.StatusNotFound, http.StatusNotImplemented:\n\t\treturn ErrAdminAPINotImplemented\n\tcase http.StatusUnauthorized:\n\t\treturn ErrAdminAPINotAuthorized\n\tdefault:\n\t\treturn errors.Errorf(\"unexpected status code when performing is-enabled check for Admin API: %d\", resp.StatusCode)\n\t}\n}\n\n// GetAdmin performs the GET /admin/admin/{id} request to the CA.\nfunc (c *AdminClient) GetAdmin(id string) (*linkedca.Admin, error) {\n\tvar retried bool\n\tu := c.endpoint.ResolveReference(&url.URL{Path: path.Join(adminURLPrefix, \"admins\", id)})\nretry:\n\tresp, err := c.client.Get(u.String())\n\tif err != nil {\n\t\treturn nil, clientError(err)\n\t}\n\tif resp.StatusCode >= 400 {\n\t\tif !retried && c.retryOnError(resp) {\n\t\t\tretried = true\n\t\t\tgoto retry\n\t\t}\n\t\treturn nil, readAdminError(resp.Body)\n\t}\n\tvar adm = new(linkedca.Admin)\n\tif err := readProtoJSON(resp.Body, adm); err != nil {\n\t\treturn nil, errors.Wrapf(err, \"error reading %s\", u)\n\t}\n\treturn adm, nil\n}\n\n// AdminOption is the type of options passed to the Admin method.\ntype AdminOption func(o *adminOptions) error\n\ntype adminOptions struct {\n\tcursor string\n\tlimit  int\n}\n\nfunc (o *adminOptions) apply(opts []AdminOption) (err error) {\n\tfor _, fn := range opts {\n\t\tif err = fn(o); err != nil {\n\t\t\treturn\n\t\t}\n\t}\n\treturn\n}\n\nfunc (o *adminOptions) rawQuery() string {\n\tv := url.Values{}\n\tif o.cursor != \"\" {\n\t\tv.Set(\"cursor\", o.cursor)\n\t}\n\tif o.limit > 0 {\n\t\tv.Set(\"limit\", strconv.Itoa(o.limit))\n\t}\n\treturn v.Encode()\n}\n\n// WithAdminCursor will request the admins starting with the given cursor.\nfunc WithAdminCursor(cursor string) AdminOption {\n\treturn func(o *adminOptions) error {\n\t\to.cursor = cursor\n\t\treturn nil\n\t}\n}\n\n// WithAdminLimit will request the given number of admins.\nfunc WithAdminLimit(limit int) AdminOption {\n\treturn func(o *adminOptions) error {\n\t\to.limit = limit\n\t\treturn nil\n\t}\n}\n\n// GetAdminsPaginate returns a page from the the GET /admin/admins request to the CA.\nfunc (c *AdminClient) GetAdminsPaginate(opts ...AdminOption) (*adminAPI.GetAdminsResponse, error) {\n\tvar retried bool\n\to := new(adminOptions)\n\tif err := o.apply(opts); err != nil {\n\t\treturn nil, err\n\t}\n\tu := c.endpoint.ResolveReference(&url.URL{\n\t\tPath:     \"/admin/admins\",\n\t\tRawQuery: o.rawQuery(),\n\t})\n\ttok, err := c.generateAdminToken(u)\n\tif err != nil {\n\t\treturn nil, errors.Wrapf(err, \"error generating admin token\")\n\t}\n\treq, err := http.NewRequest(\"GET\", u.String(), http.NoBody)\n\tif err != nil {\n\t\treturn nil, errors.Wrapf(err, \"create GET %s request failed\", u)\n\t}\n\treq.Header.Add(\"Authorization\", tok)\nretry:\n\tresp, err := c.client.Do(req)\n\tif err != nil {\n\t\treturn nil, clientError(err)\n\t}\n\tif resp.StatusCode >= 400 {\n\t\tif !retried && c.retryOnError(resp) {\n\t\t\tretried = true\n\t\t\tgoto retry\n\t\t}\n\t\treturn nil, readAdminError(resp.Body)\n\t}\n\tvar body = new(adminAPI.GetAdminsResponse)\n\tif err := readJSON(resp.Body, body); err != nil {\n\t\treturn nil, errors.Wrapf(err, \"error reading %s\", u)\n\t}\n\treturn body, nil\n}\n\n// GetAdmins returns all admins from the GET /admin/admins request to the CA.\nfunc (c *AdminClient) GetAdmins(...AdminOption) ([]*linkedca.Admin, error) {\n\tvar (\n\t\tcursor = \"\"\n\t\tadmins = []*linkedca.Admin{}\n\t)\n\tfor {\n\t\tresp, err := c.GetAdminsPaginate(WithAdminCursor(cursor), WithAdminLimit(100))\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tadmins = append(admins, resp.Admins...)\n\t\tif resp.NextCursor == \"\" {\n\t\t\treturn admins, nil\n\t\t}\n\t\tcursor = resp.NextCursor\n\t}\n}\n\n// CreateAdmin performs the POST /admin/admins request to the CA.\nfunc (c *AdminClient) CreateAdmin(createAdminRequest *adminAPI.CreateAdminRequest) (*linkedca.Admin, error) {\n\tvar retried bool\n\tbody, err := json.Marshal(createAdminRequest)\n\tif err != nil {\n\t\treturn nil, errs.Wrap(http.StatusInternalServerError, err, \"error marshaling request\")\n\t}\n\tu := c.endpoint.ResolveReference(&url.URL{Path: \"/admin/admins\"})\n\ttok, err := c.generateAdminToken(u)\n\tif err != nil {\n\t\treturn nil, errors.Wrapf(err, \"error generating admin token\")\n\t}\n\treq, err := http.NewRequest(\"POST\", u.String(), bytes.NewReader(body))\n\tif err != nil {\n\t\treturn nil, errors.Wrapf(err, \"create GET %s request failed\", u)\n\t}\n\treq.Header.Add(\"Authorization\", tok)\nretry:\n\tresp, err := c.client.Do(req)\n\tif err != nil {\n\t\treturn nil, clientError(err)\n\t}\n\tif resp.StatusCode >= 400 {\n\t\tif !retried && c.retryOnError(resp) {\n\t\t\tretried = true\n\t\t\tgoto retry\n\t\t}\n\t\treturn nil, readAdminError(resp.Body)\n\t}\n\tvar adm = new(linkedca.Admin)\n\tif err := readProtoJSON(resp.Body, adm); err != nil {\n\t\treturn nil, errors.Wrapf(err, \"error reading %s\", u)\n\t}\n\treturn adm, nil\n}\n\n// RemoveAdmin performs the DELETE /admin/admins/{id} request to the CA.\nfunc (c *AdminClient) RemoveAdmin(id string) error {\n\tvar retried bool\n\tu := c.endpoint.ResolveReference(&url.URL{Path: path.Join(adminURLPrefix, \"admins\", id)})\n\ttok, err := c.generateAdminToken(u)\n\tif err != nil {\n\t\treturn errors.Wrapf(err, \"error generating admin token\")\n\t}\n\treq, err := http.NewRequest(\"DELETE\", u.String(), http.NoBody)\n\tif err != nil {\n\t\treturn errors.Wrapf(err, \"create DELETE %s request failed\", u)\n\t}\n\treq.Header.Add(\"Authorization\", tok)\nretry:\n\tresp, err := c.client.Do(req)\n\tif err != nil {\n\t\treturn clientError(err)\n\t}\n\tif resp.StatusCode >= 400 {\n\t\tif !retried && c.retryOnError(resp) {\n\t\t\tretried = true\n\t\t\tgoto retry\n\t\t}\n\t\treturn readAdminError(resp.Body)\n\t}\n\treturn nil\n}\n\n// UpdateAdmin performs the PUT /admin/admins/{id} request to the CA.\nfunc (c *AdminClient) UpdateAdmin(id string, uar *adminAPI.UpdateAdminRequest) (*linkedca.Admin, error) {\n\tvar retried bool\n\tbody, err := json.Marshal(uar)\n\tif err != nil {\n\t\treturn nil, errs.Wrap(http.StatusInternalServerError, err, \"error marshaling request\")\n\t}\n\tu := c.endpoint.ResolveReference(&url.URL{Path: path.Join(adminURLPrefix, \"admins\", id)})\n\ttok, err := c.generateAdminToken(u)\n\tif err != nil {\n\t\treturn nil, errors.Wrapf(err, \"error generating admin token\")\n\t}\n\treq, err := http.NewRequest(\"PATCH\", u.String(), bytes.NewReader(body))\n\tif err != nil {\n\t\treturn nil, errors.Wrapf(err, \"create PATCH %s request failed\", u)\n\t}\n\treq.Header.Add(\"Authorization\", tok)\nretry:\n\tresp, err := c.client.Do(req)\n\tif err != nil {\n\t\treturn nil, clientError(err)\n\t}\n\tif resp.StatusCode >= 400 {\n\t\tif !retried && c.retryOnError(resp) {\n\t\t\tretried = true\n\t\t\tgoto retry\n\t\t}\n\t\treturn nil, readAdminError(resp.Body)\n\t}\n\tvar adm = new(linkedca.Admin)\n\tif err := readProtoJSON(resp.Body, adm); err != nil {\n\t\treturn nil, errors.Wrapf(err, \"error reading %s\", u)\n\t}\n\treturn adm, nil\n}\n\n// GetProvisioner performs the GET /admin/provisioners/{name} request to the CA.\nfunc (c *AdminClient) GetProvisioner(opts ...ProvisionerOption) (*linkedca.Provisioner, error) {\n\tvar retried bool\n\to := new(ProvisionerOptions)\n\tif err := o.Apply(opts); err != nil {\n\t\treturn nil, err\n\t}\n\tvar u *url.URL\n\tswitch {\n\tcase o.ID != \"\":\n\t\tu = c.endpoint.ResolveReference(&url.URL{\n\t\t\tPath:     \"/admin/provisioners/id\",\n\t\t\tRawQuery: o.rawQuery(),\n\t\t})\n\tcase o.Name != \"\":\n\t\tu = c.endpoint.ResolveReference(&url.URL{Path: path.Join(adminURLPrefix, \"provisioners\", o.Name)})\n\tdefault:\n\t\treturn nil, errors.New(\"must set either name or id in method options\")\n\t}\n\ttok, err := c.generateAdminToken(u)\n\tif err != nil {\n\t\treturn nil, errors.Wrapf(err, \"error generating admin token\")\n\t}\n\treq, err := http.NewRequest(\"GET\", u.String(), http.NoBody)\n\tif err != nil {\n\t\treturn nil, errors.Wrapf(err, \"create GET %s request failed\", u)\n\t}\n\treq.Header.Add(\"Authorization\", tok)\nretry:\n\tresp, err := c.client.Do(req)\n\tif err != nil {\n\t\treturn nil, clientError(err)\n\t}\n\tif resp.StatusCode >= 400 {\n\t\tif !retried && c.retryOnError(resp) {\n\t\t\tretried = true\n\t\t\tgoto retry\n\t\t}\n\t\treturn nil, readAdminError(resp.Body)\n\t}\n\tvar prov = new(linkedca.Provisioner)\n\tif err := readProtoJSON(resp.Body, prov); err != nil {\n\t\treturn nil, errors.Wrapf(err, \"error reading %s\", u)\n\t}\n\treturn prov, nil\n}\n\n// GetProvisionersPaginate performs the GET /admin/provisioners request to the CA.\nfunc (c *AdminClient) GetProvisionersPaginate(opts ...ProvisionerOption) (*adminAPI.GetProvisionersResponse, error) {\n\tvar retried bool\n\to := new(ProvisionerOptions)\n\tif err := o.Apply(opts); err != nil {\n\t\treturn nil, err\n\t}\n\tu := c.endpoint.ResolveReference(&url.URL{\n\t\tPath:     \"/admin/provisioners\",\n\t\tRawQuery: o.rawQuery(),\n\t})\n\ttok, err := c.generateAdminToken(u)\n\tif err != nil {\n\t\treturn nil, errors.Wrapf(err, \"error generating admin token\")\n\t}\n\treq, err := http.NewRequest(\"GET\", u.String(), http.NoBody)\n\tif err != nil {\n\t\treturn nil, errors.Wrapf(err, \"create GET %s request failed\", u)\n\t}\n\treq.Header.Add(\"Authorization\", tok)\nretry:\n\tresp, err := c.client.Do(req)\n\tif err != nil {\n\t\treturn nil, clientError(err)\n\t}\n\tif resp.StatusCode >= 400 {\n\t\tif !retried && c.retryOnError(resp) {\n\t\t\tretried = true\n\t\t\tgoto retry\n\t\t}\n\t\treturn nil, readAdminError(resp.Body)\n\t}\n\tvar body = new(adminAPI.GetProvisionersResponse)\n\tif err := readJSON(resp.Body, body); err != nil {\n\t\treturn nil, errors.Wrapf(err, \"error reading %s\", u)\n\t}\n\treturn body, nil\n}\n\n// GetProvisioners returns all admins from the GET /admin/admins request to the CA.\nfunc (c *AdminClient) GetProvisioners(...AdminOption) (provisioner.List, error) {\n\tvar (\n\t\tcursor = \"\"\n\t\tprovs  = provisioner.List{}\n\t)\n\tfor {\n\t\tresp, err := c.GetProvisionersPaginate(WithProvisionerCursor(cursor), WithProvisionerLimit(100))\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tprovs = append(provs, resp.Provisioners...)\n\t\tif resp.NextCursor == \"\" {\n\t\t\treturn provs, nil\n\t\t}\n\t\tcursor = resp.NextCursor\n\t}\n}\n\n// RemoveProvisioner performs the DELETE /admin/provisioners/{name} request to the CA.\nfunc (c *AdminClient) RemoveProvisioner(opts ...ProvisionerOption) error {\n\tvar (\n\t\tu       *url.URL\n\t\tretried bool\n\t)\n\n\to := new(ProvisionerOptions)\n\tif err := o.Apply(opts); err != nil {\n\t\treturn err\n\t}\n\n\tswitch {\n\tcase o.ID != \"\":\n\t\tu = c.endpoint.ResolveReference(&url.URL{\n\t\t\tPath:     path.Join(adminURLPrefix, \"provisioners/id\"),\n\t\t\tRawQuery: o.rawQuery(),\n\t\t})\n\tcase o.Name != \"\":\n\t\tu = c.endpoint.ResolveReference(&url.URL{Path: path.Join(adminURLPrefix, \"provisioners\", o.Name)})\n\tdefault:\n\t\treturn errors.New(\"must set either name or id in method options\")\n\t}\n\ttok, err := c.generateAdminToken(u)\n\tif err != nil {\n\t\treturn errors.Wrapf(err, \"error generating admin token\")\n\t}\n\treq, err := http.NewRequest(\"DELETE\", u.String(), http.NoBody)\n\tif err != nil {\n\t\treturn errors.Wrapf(err, \"create DELETE %s request failed\", u)\n\t}\n\treq.Header.Add(\"Authorization\", tok)\nretry:\n\tresp, err := c.client.Do(req)\n\tif err != nil {\n\t\treturn clientError(err)\n\t}\n\tif resp.StatusCode >= 400 {\n\t\tif !retried && c.retryOnError(resp) {\n\t\t\tretried = true\n\t\t\tgoto retry\n\t\t}\n\t\treturn readAdminError(resp.Body)\n\t}\n\treturn nil\n}\n\n// CreateProvisioner performs the POST /admin/provisioners request to the CA.\nfunc (c *AdminClient) CreateProvisioner(prov *linkedca.Provisioner) (*linkedca.Provisioner, error) {\n\tvar retried bool\n\tbody, err := protojson.Marshal(prov)\n\tif err != nil {\n\t\treturn nil, errs.Wrap(http.StatusInternalServerError, err, \"error marshaling request\")\n\t}\n\tu := c.endpoint.ResolveReference(&url.URL{Path: path.Join(adminURLPrefix, \"provisioners\")})\n\ttok, err := c.generateAdminToken(u)\n\tif err != nil {\n\t\treturn nil, errors.Wrapf(err, \"error generating admin token\")\n\t}\n\treq, err := http.NewRequest(\"POST\", u.String(), bytes.NewReader(body))\n\tif err != nil {\n\t\treturn nil, errors.Wrapf(err, \"create POST %s request failed\", u)\n\t}\n\treq.Header.Add(\"Authorization\", tok)\nretry:\n\tresp, err := c.client.Do(req)\n\tif err != nil {\n\t\treturn nil, clientError(err)\n\t}\n\tif resp.StatusCode >= 400 {\n\t\tif !retried && c.retryOnError(resp) {\n\t\t\tretried = true\n\t\t\tgoto retry\n\t\t}\n\t\treturn nil, readAdminError(resp.Body)\n\t}\n\tvar nuProv = new(linkedca.Provisioner)\n\tif err := readProtoJSON(resp.Body, nuProv); err != nil {\n\t\treturn nil, errors.Wrapf(err, \"error reading %s\", u)\n\t}\n\treturn nuProv, nil\n}\n\n// UpdateProvisioner performs the PUT /admin/provisioners/{name} request to the CA.\nfunc (c *AdminClient) UpdateProvisioner(name string, prov *linkedca.Provisioner) error {\n\tvar retried bool\n\tbody, err := protojson.Marshal(prov)\n\tif err != nil {\n\t\treturn errs.Wrap(http.StatusInternalServerError, err, \"error marshaling request\")\n\t}\n\tu := c.endpoint.ResolveReference(&url.URL{Path: path.Join(adminURLPrefix, \"provisioners\", name)})\n\ttok, err := c.generateAdminToken(u)\n\tif err != nil {\n\t\treturn errors.Wrapf(err, \"error generating admin token\")\n\t}\n\treq, err := http.NewRequest(\"PUT\", u.String(), bytes.NewReader(body))\n\tif err != nil {\n\t\treturn errors.Wrapf(err, \"create PUT %s request failed\", u)\n\t}\n\treq.Header.Add(\"Authorization\", tok)\nretry:\n\tresp, err := c.client.Do(req)\n\tif err != nil {\n\t\treturn clientError(err)\n\t}\n\tif resp.StatusCode >= 400 {\n\t\tif !retried && c.retryOnError(resp) {\n\t\t\tretried = true\n\t\t\tgoto retry\n\t\t}\n\t\treturn readAdminError(resp.Body)\n\t}\n\treturn nil\n}\n\n// GetExternalAccountKeysPaginate returns a page from the GET /admin/acme/eab request to the CA.\nfunc (c *AdminClient) GetExternalAccountKeysPaginate(provisionerName, reference string, opts ...AdminOption) (*adminAPI.GetExternalAccountKeysResponse, error) {\n\tvar retried bool\n\to := new(adminOptions)\n\tif err := o.apply(opts); err != nil {\n\t\treturn nil, err\n\t}\n\tp := path.Join(adminURLPrefix, \"acme/eab\", provisionerName)\n\tif reference != \"\" {\n\t\tp = path.Join(p, \"/\", reference)\n\t}\n\tu := c.endpoint.ResolveReference(&url.URL{\n\t\tPath:     p,\n\t\tRawQuery: o.rawQuery(),\n\t})\n\ttok, err := c.generateAdminToken(u)\n\tif err != nil {\n\t\treturn nil, errors.Wrapf(err, \"error generating admin token\")\n\t}\n\treq, err := http.NewRequest(\"GET\", u.String(), http.NoBody)\n\tif err != nil {\n\t\treturn nil, errors.Wrapf(err, \"create GET %s request failed\", u)\n\t}\n\treq.Header.Add(\"Authorization\", tok)\nretry:\n\tresp, err := c.client.Do(req)\n\tif err != nil {\n\t\treturn nil, clientError(err)\n\t}\n\tif resp.StatusCode >= 400 {\n\t\tif !retried && c.retryOnError(resp) {\n\t\t\tretried = true\n\t\t\tgoto retry\n\t\t}\n\t\treturn nil, readAdminError(resp.Body)\n\t}\n\tvar body = new(adminAPI.GetExternalAccountKeysResponse)\n\tif err := readJSON(resp.Body, body); err != nil {\n\t\treturn nil, errors.Wrapf(err, \"error reading %s\", u)\n\t}\n\treturn body, nil\n}\n\n// CreateExternalAccountKey performs the POST /admin/acme/eab request to the CA.\nfunc (c *AdminClient) CreateExternalAccountKey(provisionerName string, eakRequest *adminAPI.CreateExternalAccountKeyRequest) (*linkedca.EABKey, error) {\n\tvar retried bool\n\tbody, err := json.Marshal(eakRequest)\n\tif err != nil {\n\t\treturn nil, errs.Wrap(http.StatusInternalServerError, err, \"error marshaling request\")\n\t}\n\tu := c.endpoint.ResolveReference(&url.URL{Path: path.Join(adminURLPrefix, \"acme/eab/\", provisionerName)})\n\ttok, err := c.generateAdminToken(u)\n\tif err != nil {\n\t\treturn nil, errors.Wrapf(err, \"error generating admin token\")\n\t}\n\treq, err := http.NewRequest(\"POST\", u.String(), bytes.NewReader(body))\n\tif err != nil {\n\t\treturn nil, errors.Wrapf(err, \"create POST %s request failed\", u)\n\t}\n\treq.Header.Add(\"Authorization\", tok)\nretry:\n\tresp, err := c.client.Do(req)\n\tif err != nil {\n\t\treturn nil, clientError(err)\n\t}\n\tif resp.StatusCode >= 400 {\n\t\tif !retried && c.retryOnError(resp) {\n\t\t\tretried = true\n\t\t\tgoto retry\n\t\t}\n\t\treturn nil, readAdminError(resp.Body)\n\t}\n\tvar eabKey = new(linkedca.EABKey)\n\tif err := readProtoJSON(resp.Body, eabKey); err != nil {\n\t\treturn nil, errors.Wrapf(err, \"error reading %s\", u)\n\t}\n\treturn eabKey, nil\n}\n\n// RemoveExternalAccountKey performs the DELETE /admin/acme/eab/{prov}/{key_id} request to the CA.\nfunc (c *AdminClient) RemoveExternalAccountKey(provisionerName, keyID string) error {\n\tvar retried bool\n\tu := c.endpoint.ResolveReference(&url.URL{Path: path.Join(adminURLPrefix, \"acme/eab\", provisionerName, \"/\", keyID)})\n\ttok, err := c.generateAdminToken(u)\n\tif err != nil {\n\t\treturn errors.Wrapf(err, \"error generating admin token\")\n\t}\n\treq, err := http.NewRequest(\"DELETE\", u.String(), http.NoBody)\n\tif err != nil {\n\t\treturn errors.Wrapf(err, \"create DELETE %s request failed\", u)\n\t}\n\treq.Header.Add(\"Authorization\", tok)\nretry:\n\tresp, err := c.client.Do(req)\n\tif err != nil {\n\t\treturn clientError(err)\n\t}\n\tif resp.StatusCode >= 400 {\n\t\tif !retried && c.retryOnError(resp) {\n\t\t\tretried = true\n\t\t\tgoto retry\n\t\t}\n\t\treturn readAdminError(resp.Body)\n\t}\n\treturn nil\n}\n\nfunc (c *AdminClient) GetAuthorityPolicy() (*linkedca.Policy, error) {\n\tvar retried bool\n\tu := c.endpoint.ResolveReference(&url.URL{Path: path.Join(adminURLPrefix, \"policy\")})\n\ttok, err := c.generateAdminToken(u)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error generating admin token: %w\", err)\n\t}\n\treq, err := http.NewRequest(http.MethodGet, u.String(), http.NoBody)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"creating GET %s request failed: %w\", u, err)\n\t}\n\treq.Header.Add(\"Authorization\", tok)\nretry:\n\tresp, err := c.client.Do(req)\n\tif err != nil {\n\t\treturn nil, clientError(err)\n\t}\n\tif resp.StatusCode >= 400 {\n\t\tif !retried && c.retryOnError(resp) {\n\t\t\tretried = true\n\t\t\tgoto retry\n\t\t}\n\t\treturn nil, readAdminError(resp.Body)\n\t}\n\tvar policy = new(linkedca.Policy)\n\tif err := readProtoJSON(resp.Body, policy); err != nil {\n\t\treturn nil, fmt.Errorf(\"error reading %s: %w\", u, err)\n\t}\n\treturn policy, nil\n}\n\nfunc (c *AdminClient) CreateAuthorityPolicy(p *linkedca.Policy) (*linkedca.Policy, error) {\n\tvar retried bool\n\tbody, err := protojson.Marshal(p)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error marshaling request: %w\", err)\n\t}\n\tu := c.endpoint.ResolveReference(&url.URL{Path: path.Join(adminURLPrefix, \"policy\")})\n\ttok, err := c.generateAdminToken(u)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error generating admin token: %w\", err)\n\t}\n\treq, err := http.NewRequest(http.MethodPost, u.String(), bytes.NewReader(body))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"creating POST %s request failed: %w\", u, err)\n\t}\n\treq.Header.Add(\"Authorization\", tok)\nretry:\n\tresp, err := c.client.Do(req)\n\tif err != nil {\n\t\treturn nil, clientError(err)\n\t}\n\tif resp.StatusCode >= 400 {\n\t\tif !retried && c.retryOnError(resp) {\n\t\t\tretried = true\n\t\t\tgoto retry\n\t\t}\n\t\treturn nil, readAdminError(resp.Body)\n\t}\n\tvar policy = new(linkedca.Policy)\n\tif err := readProtoJSON(resp.Body, policy); err != nil {\n\t\treturn nil, fmt.Errorf(\"error reading %s: %w\", u, err)\n\t}\n\treturn policy, nil\n}\n\nfunc (c *AdminClient) UpdateAuthorityPolicy(p *linkedca.Policy) (*linkedca.Policy, error) {\n\tvar retried bool\n\tbody, err := protojson.Marshal(p)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error marshaling request: %w\", err)\n\t}\n\tu := c.endpoint.ResolveReference(&url.URL{Path: path.Join(adminURLPrefix, \"policy\")})\n\ttok, err := c.generateAdminToken(u)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error generating admin token: %w\", err)\n\t}\n\treq, err := http.NewRequest(http.MethodPut, u.String(), bytes.NewReader(body))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"creating PUT %s request failed: %w\", u, err)\n\t}\n\treq.Header.Add(\"Authorization\", tok)\nretry:\n\tresp, err := c.client.Do(req)\n\tif err != nil {\n\t\treturn nil, clientError(err)\n\t}\n\tif resp.StatusCode >= 400 {\n\t\tif !retried && c.retryOnError(resp) {\n\t\t\tretried = true\n\t\t\tgoto retry\n\t\t}\n\t\treturn nil, readAdminError(resp.Body)\n\t}\n\tvar policy = new(linkedca.Policy)\n\tif err := readProtoJSON(resp.Body, policy); err != nil {\n\t\treturn nil, fmt.Errorf(\"error reading %s: %w\", u, err)\n\t}\n\treturn policy, nil\n}\n\nfunc (c *AdminClient) RemoveAuthorityPolicy() error {\n\tvar retried bool\n\tu := c.endpoint.ResolveReference(&url.URL{Path: path.Join(adminURLPrefix, \"policy\")})\n\ttok, err := c.generateAdminToken(u)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error generating admin token: %w\", err)\n\t}\n\treq, err := http.NewRequest(http.MethodDelete, u.String(), http.NoBody)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"creating DELETE %s request failed: %w\", u, err)\n\t}\n\treq.Header.Add(\"Authorization\", tok)\nretry:\n\tresp, err := c.client.Do(req)\n\tif err != nil {\n\t\treturn clientError(err)\n\t}\n\tif resp.StatusCode >= 400 {\n\t\tif !retried && c.retryOnError(resp) {\n\t\t\tretried = true\n\t\t\tgoto retry\n\t\t}\n\t\treturn readAdminError(resp.Body)\n\t}\n\treturn nil\n}\n\nfunc (c *AdminClient) GetProvisionerPolicy(provisionerName string) (*linkedca.Policy, error) {\n\tvar retried bool\n\tu := c.endpoint.ResolveReference(&url.URL{Path: path.Join(adminURLPrefix, \"provisioners\", provisionerName, \"policy\")})\n\ttok, err := c.generateAdminToken(u)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error generating admin token: %w\", err)\n\t}\n\treq, err := http.NewRequest(http.MethodGet, u.String(), http.NoBody)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"creating GET %s request failed: %w\", u, err)\n\t}\n\treq.Header.Add(\"Authorization\", tok)\nretry:\n\tresp, err := c.client.Do(req)\n\tif err != nil {\n\t\treturn nil, clientError(err)\n\t}\n\tif resp.StatusCode >= 400 {\n\t\tif !retried && c.retryOnError(resp) {\n\t\t\tretried = true\n\t\t\tgoto retry\n\t\t}\n\t\treturn nil, readAdminError(resp.Body)\n\t}\n\tvar policy = new(linkedca.Policy)\n\tif err := readProtoJSON(resp.Body, policy); err != nil {\n\t\treturn nil, fmt.Errorf(\"error reading %s: %w\", u, err)\n\t}\n\treturn policy, nil\n}\n\nfunc (c *AdminClient) CreateProvisionerPolicy(provisionerName string, p *linkedca.Policy) (*linkedca.Policy, error) {\n\tvar retried bool\n\tbody, err := protojson.Marshal(p)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error marshaling request: %w\", err)\n\t}\n\tu := c.endpoint.ResolveReference(&url.URL{Path: path.Join(adminURLPrefix, \"provisioners\", provisionerName, \"policy\")})\n\ttok, err := c.generateAdminToken(u)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error generating admin token: %w\", err)\n\t}\n\treq, err := http.NewRequest(http.MethodPost, u.String(), bytes.NewReader(body))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"creating POST %s request failed: %w\", u, err)\n\t}\n\treq.Header.Add(\"Authorization\", tok)\nretry:\n\tresp, err := c.client.Do(req)\n\tif err != nil {\n\t\treturn nil, clientError(err)\n\t}\n\tif resp.StatusCode >= 400 {\n\t\tif !retried && c.retryOnError(resp) {\n\t\t\tretried = true\n\t\t\tgoto retry\n\t\t}\n\t\treturn nil, readAdminError(resp.Body)\n\t}\n\tvar policy = new(linkedca.Policy)\n\tif err := readProtoJSON(resp.Body, policy); err != nil {\n\t\treturn nil, fmt.Errorf(\"error reading %s: %w\", u, err)\n\t}\n\treturn policy, nil\n}\n\nfunc (c *AdminClient) UpdateProvisionerPolicy(provisionerName string, p *linkedca.Policy) (*linkedca.Policy, error) {\n\tvar retried bool\n\tbody, err := protojson.Marshal(p)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error marshaling request: %w\", err)\n\t}\n\tu := c.endpoint.ResolveReference(&url.URL{Path: path.Join(adminURLPrefix, \"provisioners\", provisionerName, \"policy\")})\n\ttok, err := c.generateAdminToken(u)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error generating admin token: %w\", err)\n\t}\n\treq, err := http.NewRequest(http.MethodPut, u.String(), bytes.NewReader(body))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"creating PUT %s request failed: %w\", u, err)\n\t}\n\treq.Header.Add(\"Authorization\", tok)\nretry:\n\tresp, err := c.client.Do(req)\n\tif err != nil {\n\t\treturn nil, clientError(err)\n\t}\n\tif resp.StatusCode >= 400 {\n\t\tif !retried && c.retryOnError(resp) {\n\t\t\tretried = true\n\t\t\tgoto retry\n\t\t}\n\t\treturn nil, readAdminError(resp.Body)\n\t}\n\tvar policy = new(linkedca.Policy)\n\tif err := readProtoJSON(resp.Body, policy); err != nil {\n\t\treturn nil, fmt.Errorf(\"error reading %s: %w\", u, err)\n\t}\n\treturn policy, nil\n}\n\nfunc (c *AdminClient) RemoveProvisionerPolicy(provisionerName string) error {\n\tvar retried bool\n\tu := c.endpoint.ResolveReference(&url.URL{Path: path.Join(adminURLPrefix, \"provisioners\", provisionerName, \"policy\")})\n\ttok, err := c.generateAdminToken(u)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error generating admin token: %w\", err)\n\t}\n\treq, err := http.NewRequest(http.MethodDelete, u.String(), http.NoBody)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"creating DELETE %s request failed: %w\", u, err)\n\t}\n\treq.Header.Add(\"Authorization\", tok)\nretry:\n\tresp, err := c.client.Do(req)\n\tif err != nil {\n\t\treturn clientError(err)\n\t}\n\tif resp.StatusCode >= 400 {\n\t\tif !retried && c.retryOnError(resp) {\n\t\t\tretried = true\n\t\t\tgoto retry\n\t\t}\n\t\treturn readAdminError(resp.Body)\n\t}\n\treturn nil\n}\n\nfunc (c *AdminClient) GetACMEPolicy(provisionerName, reference, keyID string) (*linkedca.Policy, error) {\n\tvar retried bool\n\tvar urlPath string\n\tswitch {\n\tcase keyID != \"\":\n\t\turlPath = path.Join(adminURLPrefix, \"acme\", \"policy\", provisionerName, \"key\", keyID)\n\tdefault:\n\t\turlPath = path.Join(adminURLPrefix, \"acme\", \"policy\", provisionerName, \"reference\", reference)\n\t}\n\tu := c.endpoint.ResolveReference(&url.URL{Path: urlPath})\n\ttok, err := c.generateAdminToken(u)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error generating admin token: %w\", err)\n\t}\n\treq, err := http.NewRequest(http.MethodGet, u.String(), http.NoBody)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"creating GET %s request failed: %w\", u, err)\n\t}\n\treq.Header.Add(\"Authorization\", tok)\nretry:\n\tresp, err := c.client.Do(req)\n\tif err != nil {\n\t\treturn nil, clientError(err)\n\t}\n\tif resp.StatusCode >= 400 {\n\t\tif !retried && c.retryOnError(resp) {\n\t\t\tretried = true\n\t\t\tgoto retry\n\t\t}\n\t\treturn nil, readAdminError(resp.Body)\n\t}\n\tvar policy = new(linkedca.Policy)\n\tif err := readProtoJSON(resp.Body, policy); err != nil {\n\t\treturn nil, fmt.Errorf(\"error reading %s: %w\", u, err)\n\t}\n\treturn policy, nil\n}\n\nfunc (c *AdminClient) CreateACMEPolicy(provisionerName, reference, keyID string, p *linkedca.Policy) (*linkedca.Policy, error) {\n\tvar retried bool\n\tbody, err := protojson.Marshal(p)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error marshaling request: %w\", err)\n\t}\n\tvar urlPath string\n\tswitch {\n\tcase keyID != \"\":\n\t\turlPath = path.Join(adminURLPrefix, \"acme\", \"policy\", provisionerName, \"key\", keyID)\n\tdefault:\n\t\turlPath = path.Join(adminURLPrefix, \"acme\", \"policy\", provisionerName, \"reference\", reference)\n\t}\n\tu := c.endpoint.ResolveReference(&url.URL{Path: urlPath})\n\ttok, err := c.generateAdminToken(u)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error generating admin token: %w\", err)\n\t}\n\treq, err := http.NewRequest(http.MethodPost, u.String(), bytes.NewReader(body))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"creating POST %s request failed: %w\", u, err)\n\t}\n\treq.Header.Add(\"Authorization\", tok)\nretry:\n\tresp, err := c.client.Do(req)\n\tif err != nil {\n\t\treturn nil, clientError(err)\n\t}\n\tif resp.StatusCode >= 400 {\n\t\tif !retried && c.retryOnError(resp) {\n\t\t\tretried = true\n\t\t\tgoto retry\n\t\t}\n\t\treturn nil, readAdminError(resp.Body)\n\t}\n\tvar policy = new(linkedca.Policy)\n\tif err := readProtoJSON(resp.Body, policy); err != nil {\n\t\treturn nil, fmt.Errorf(\"error reading %s: %w\", u, err)\n\t}\n\treturn policy, nil\n}\n\nfunc (c *AdminClient) UpdateACMEPolicy(provisionerName, reference, keyID string, p *linkedca.Policy) (*linkedca.Policy, error) {\n\tvar retried bool\n\tbody, err := protojson.Marshal(p)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error marshaling request: %w\", err)\n\t}\n\tvar urlPath string\n\tswitch {\n\tcase keyID != \"\":\n\t\turlPath = path.Join(adminURLPrefix, \"acme\", \"policy\", provisionerName, \"key\", keyID)\n\tdefault:\n\t\turlPath = path.Join(adminURLPrefix, \"acme\", \"policy\", provisionerName, \"reference\", reference)\n\t}\n\tu := c.endpoint.ResolveReference(&url.URL{Path: urlPath})\n\ttok, err := c.generateAdminToken(u)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error generating admin token: %w\", err)\n\t}\n\treq, err := http.NewRequest(http.MethodPut, u.String(), bytes.NewReader(body))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"creating PUT %s request failed: %w\", u, err)\n\t}\n\treq.Header.Add(\"Authorization\", tok)\nretry:\n\tresp, err := c.client.Do(req)\n\tif err != nil {\n\t\treturn nil, clientError(err)\n\t}\n\tif resp.StatusCode >= 400 {\n\t\tif !retried && c.retryOnError(resp) {\n\t\t\tretried = true\n\t\t\tgoto retry\n\t\t}\n\t\treturn nil, readAdminError(resp.Body)\n\t}\n\tvar policy = new(linkedca.Policy)\n\tif err := readProtoJSON(resp.Body, policy); err != nil {\n\t\treturn nil, fmt.Errorf(\"error reading %s: %w\", u, err)\n\t}\n\treturn policy, nil\n}\n\nfunc (c *AdminClient) RemoveACMEPolicy(provisionerName, reference, keyID string) error {\n\tvar retried bool\n\tvar urlPath string\n\tswitch {\n\tcase keyID != \"\":\n\t\turlPath = path.Join(adminURLPrefix, \"acme\", \"policy\", provisionerName, \"key\", keyID)\n\tdefault:\n\t\turlPath = path.Join(adminURLPrefix, \"acme\", \"policy\", provisionerName, \"reference\", reference)\n\t}\n\tu := c.endpoint.ResolveReference(&url.URL{Path: urlPath})\n\ttok, err := c.generateAdminToken(u)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error generating admin token: %w\", err)\n\t}\n\treq, err := http.NewRequest(http.MethodDelete, u.String(), http.NoBody)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"creating DELETE %s request failed: %w\", u, err)\n\t}\n\treq.Header.Add(\"Authorization\", tok)\nretry:\n\tresp, err := c.client.Do(req)\n\tif err != nil {\n\t\treturn clientError(err)\n\t}\n\tif resp.StatusCode >= 400 {\n\t\tif !retried && c.retryOnError(resp) {\n\t\t\tretried = true\n\t\t\tgoto retry\n\t\t}\n\t\treturn readAdminError(resp.Body)\n\t}\n\treturn nil\n}\n\nfunc (c *AdminClient) CreateProvisionerWebhook(provisionerName string, wh *linkedca.Webhook) (*linkedca.Webhook, error) {\n\tvar retried bool\n\tbody, err := protojson.Marshal(wh)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error marshaling request: %w\", err)\n\t}\n\tu := c.endpoint.ResolveReference(&url.URL{Path: path.Join(adminURLPrefix, \"provisioners\", provisionerName, \"webhooks\")})\n\ttok, err := c.generateAdminToken(u)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error generating admin token: %w\", err)\n\t}\nretry:\n\treq, err := http.NewRequest(http.MethodPost, u.String(), bytes.NewReader(body))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"creating POST %s request failed: %w\", u, err)\n\t}\n\treq.Header.Add(\"Authorization\", tok)\n\tresp, err := c.client.Do(req)\n\tif err != nil {\n\t\treturn nil, clientError(err)\n\t}\n\tif resp.StatusCode >= 400 {\n\t\tif !retried && c.retryOnError(resp) {\n\t\t\tretried = true\n\t\t\tgoto retry\n\t\t}\n\t\treturn nil, readAdminError(resp.Body)\n\t}\n\tvar webhook = new(linkedca.Webhook)\n\tif err := readProtoJSON(resp.Body, webhook); err != nil {\n\t\treturn nil, fmt.Errorf(\"error reading %s: %w\", u, err)\n\t}\n\treturn webhook, nil\n}\n\nfunc (c *AdminClient) UpdateProvisionerWebhook(provisionerName string, wh *linkedca.Webhook) (*linkedca.Webhook, error) {\n\tvar retried bool\n\tbody, err := protojson.Marshal(wh)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error marshaling request: %w\", err)\n\t}\n\tu := c.endpoint.ResolveReference(&url.URL{Path: path.Join(adminURLPrefix, \"provisioners\", provisionerName, \"webhooks\", wh.Name)})\n\ttok, err := c.generateAdminToken(u)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error generating admin token: %w\", err)\n\t}\nretry:\n\treq, err := http.NewRequest(http.MethodPut, u.String(), bytes.NewReader(body))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"creating PUT %s request failed: %w\", u, err)\n\t}\n\treq.Header.Add(\"Authorization\", tok)\n\tresp, err := c.client.Do(req)\n\tif err != nil {\n\t\treturn nil, clientError(err)\n\t}\n\tif resp.StatusCode >= 400 {\n\t\tif !retried && c.retryOnError(resp) {\n\t\t\tretried = true\n\t\t\tgoto retry\n\t\t}\n\t\treturn nil, readAdminError(resp.Body)\n\t}\n\tvar webhook = new(linkedca.Webhook)\n\tif err := readProtoJSON(resp.Body, webhook); err != nil {\n\t\treturn nil, fmt.Errorf(\"error reading %s: %w\", u, err)\n\t}\n\treturn webhook, nil\n}\n\nfunc (c *AdminClient) DeleteProvisionerWebhook(provisionerName, webhookName string) error {\n\tvar retried bool\n\tu := c.endpoint.ResolveReference(&url.URL{Path: path.Join(adminURLPrefix, \"provisioners\", provisionerName, \"webhooks\", webhookName)})\n\ttok, err := c.generateAdminToken(u)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error generating admin token: %w\", err)\n\t}\nretry:\n\treq, err := http.NewRequest(http.MethodDelete, u.String(), http.NoBody)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"creating DELETE %s request failed: %w\", u, err)\n\t}\n\treq.Header.Add(\"Authorization\", tok)\n\tresp, err := c.client.Do(req)\n\tif err != nil {\n\t\treturn clientError(err)\n\t}\n\tif resp.StatusCode >= 400 {\n\t\tif !retried && c.retryOnError(resp) {\n\t\t\tretried = true\n\t\t\tgoto retry\n\t\t}\n\t\treturn readAdminError(resp.Body)\n\t}\n\treturn nil\n}\n\nfunc readAdminError(r io.ReadCloser) error {\n\t// TODO: not all errors can be read (i.e. 404); seems to be a bigger issue\n\tdefer r.Close()\n\tadminErr := new(AdminClientError)\n\tif err := json.NewDecoder(r).Decode(adminErr); err != nil {\n\t\treturn err\n\t}\n\treturn adminErr\n}\n"
  },
  {
    "path": "ca/bootstrap.go",
    "content": "package ca\n\nimport (\n\t\"context\"\n\t\"crypto\"\n\t\"crypto/tls\"\n\t\"net\"\n\t\"net/http\"\n\t\"strings\"\n\n\t\"github.com/pkg/errors\"\n\t\"github.com/smallstep/certificates/api\"\n\t\"go.step.sm/crypto/jose\"\n)\n\ntype tokenClaims struct {\n\tSHA string `json:\"sha\"`\n\tjose.Claims\n}\n\n// Bootstrap is a helper function that initializes a client with the\n// configuration in the bootstrap token.\nfunc Bootstrap(token string) (*Client, error) {\n\ttok, err := jose.ParseSigned(token)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"error parsing token\")\n\t}\n\tvar claims tokenClaims\n\tif err := tok.UnsafeClaimsWithoutVerification(&claims); err != nil {\n\t\treturn nil, errors.Wrap(err, \"error parsing token\")\n\t}\n\n\t// Validate bootstrap token\n\tswitch {\n\tcase claims.SHA == \"\":\n\t\treturn nil, errors.New(\"invalid bootstrap token: sha claim is not present\")\n\tcase !strings.HasPrefix(strings.ToLower(claims.Audience[0]), \"http\"):\n\t\treturn nil, errors.New(\"invalid bootstrap token: aud claim is not a url\")\n\t}\n\n\treturn NewClient(claims.Audience[0], WithRootSHA256(claims.SHA))\n}\n\n// BootstrapClient is a helper function that using the given bootstrap token\n// return an http.Client configured with a Transport prepared to do TLS\n// connections using the client certificate returned by the certificate\n// authority. By default the server will kick off a routine that will renew the\n// certificate after 2/3rd of the certificate's lifetime has expired.\n//\n// Usage:\n//\n//\t// Default example with certificate rotation.\n//\tclient, err := ca.BootstrapClient(ctx.Background(), token)\n//\n//\t// Example canceling automatic certificate rotation.\n//\tctx, cancel := context.WithCancel(context.Background())\n//\tdefer cancel()\n//\tclient, err := ca.BootstrapClient(ctx, token)\n//\tif err != nil {\n//\t  return err\n//\t}\n//\tresp, err := client.Get(\"https://internal.smallstep.com\")\nfunc BootstrapClient(ctx context.Context, token string, options ...TLSOption) (*http.Client, error) {\n\tb, err := createBootstrap(token) //nolint:contextcheck // deeply nested context; temporary\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Make sure the tlsConfig has all supported roots on RootCAs.\n\t//\n\t// The roots request is only supported if identity certificates are not\n\t// required. In all cases the current root is also added after applying all\n\t// options too.\n\tif !b.RequireClientAuth {\n\t\toptions = append(options, AddRootsToRootCAs())\n\t}\n\n\ttransport, err := b.Client.Transport(ctx, b.SignResponse, b.PrivateKey, options...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &http.Client{\n\t\tTransport: transport,\n\t}, nil\n}\n\n// BootstrapServer is a helper function that using the given token returns the\n// given http.Server configured with a TLS certificate signed by the Certificate\n// Authority. By default the server will kick off a routine that will renew the\n// certificate after 2/3rd of the certificate's lifetime has expired.\n//\n// Without any extra option the server will be configured for mTLS, it will\n// require and verify clients certificates, but options can be used to drop this\n// requirement, the most common will be only verify the certs if given with\n// ca.VerifyClientCertIfGiven(), or add extra CAs with\n// ca.AddClientCA(*x509.Certificate).\n//\n// Usage:\n//\n//\t// Default example with certificate rotation.\n//\tsrv, err := ca.BootstrapServer(context.Background(), token, &http.Server{\n//\t    Addr: \":443\",\n//\t    Handler: handler,\n//\t})\n//\n//\t// Example canceling automatic certificate rotation.\n//\tctx, cancel := context.WithCancel(context.Background())\n//\tdefer cancel()\n//\tsrv, err := ca.BootstrapServer(ctx, token, &http.Server{\n//\t    Addr: \":443\",\n//\t    Handler: handler,\n//\t})\n//\tif err != nil {\n//\t    return err\n//\t}\n//\tsrv.ListenAndServeTLS(\"\", \"\")\nfunc BootstrapServer(ctx context.Context, token string, base *http.Server, options ...TLSOption) (*http.Server, error) {\n\tif base.TLSConfig != nil {\n\t\treturn nil, errors.New(\"server TLSConfig is already set\")\n\t}\n\n\tb, err := createBootstrap(token) //nolint:contextcheck // deeply nested context; temporary\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Make sure the tlsConfig has all supported roots on RootCAs.\n\t//\n\t// The roots request is only supported if identity certificates are not\n\t// required. In all cases the current root is also added after applying all\n\t// options too.\n\tif !b.RequireClientAuth {\n\t\toptions = append(options, AddRootsToCAs())\n\t}\n\n\ttlsConfig, err := b.Client.GetServerTLSConfig(ctx, b.SignResponse, b.PrivateKey, options...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tbase.TLSConfig = tlsConfig\n\treturn base, nil\n}\n\n// BootstrapListener is a helper function that using the given token returns a\n// TLS listener which accepts connections from an inner listener and wraps each\n// connection with Server.\n//\n// Without any extra option the server will be configured for mTLS, it will\n// require and verify clients certificates, but options can be used to drop this\n// requirement, the most common will be only verify the certs if given with\n// ca.VerifyClientCertIfGiven(), or add extra CAs with\n// ca.AddClientCA(*x509.Certificate).\n//\n// Usage:\n//\n//\tinner, err := net.Listen(\"tcp\", \":443\")\n//\tif err != nil {\n//\t  return nil\n//\t}\n//\tctx, cancel := context.WithCancel(context.Background())\n//\tdefer cancel()\n//\tlis, err := ca.BootstrapListener(ctx, token, inner)\n//\tif err != nil {\n//\t    return err\n//\t}\n//\tsrv := grpc.NewServer()\n//\t... // register services\n//\tsrv.Serve(lis)\nfunc BootstrapListener(ctx context.Context, token string, inner net.Listener, options ...TLSOption) (net.Listener, error) {\n\tb, err := createBootstrap(token) //nolint:contextcheck // deeply nested context; temporary\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Make sure the tlsConfig has all supported roots on RootCAs.\n\t//\n\t// The roots request is only supported if identity certificates are not\n\t// required. In all cases the current root is also added after applying all\n\t// options too.\n\tif !b.RequireClientAuth {\n\t\toptions = append(options, AddRootsToCAs())\n\t}\n\n\ttlsConfig, err := b.Client.GetServerTLSConfig(ctx, b.SignResponse, b.PrivateKey, options...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn tls.NewListener(inner, tlsConfig), nil\n}\n\ntype bootstrap struct {\n\tClient            *Client\n\tRequireClientAuth bool\n\tSignResponse      *api.SignResponse\n\tPrivateKey        crypto.PrivateKey\n}\n\nfunc createBootstrap(token string) (*bootstrap, error) {\n\tclient, err := Bootstrap(token)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tversion, err := client.Version()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treq, pk, err := CreateSignRequest(token)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tsign, err := client.Sign(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &bootstrap{\n\t\tClient:            client,\n\t\tRequireClientAuth: version.RequireClientAuthentication,\n\t\tSignResponse:      sign,\n\t\tPrivateKey:        pk,\n\t}, nil\n}\n"
  },
  {
    "path": "ca/bootstrap_test.go",
    "content": "package ca\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"io\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"os\"\n\t\"reflect\"\n\t\"strings\"\n\t\"sync\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n\n\t\"go.step.sm/crypto/jose\"\n\t\"go.step.sm/crypto/randutil\"\n\n\t\"github.com/smallstep/certificates/api\"\n\t\"github.com/smallstep/certificates/api/render\"\n\t\"github.com/smallstep/certificates/authority\"\n\t\"github.com/smallstep/certificates/errs\"\n)\n\nfunc newLocalListener() net.Listener {\n\tl, err := net.Listen(\"tcp\", \"127.0.0.1:0\")\n\tif err != nil {\n\t\tif l, err = net.Listen(\"tcp6\", \"[::1]:0\"); err != nil {\n\t\t\tpanic(errors.Wrap(err, \"failed to listen on a port\"))\n\t\t}\n\t}\n\treturn l\n}\n\nfunc setMinCertDuration(time.Duration) func() {\n\ttmp := minCertDuration\n\tminCertDuration = 1 * time.Second\n\treturn func() {\n\t\tminCertDuration = tmp\n\t}\n}\n\nfunc startCABootstrapServer() *httptest.Server {\n\tconfig, err := authority.LoadConfiguration(\"testdata/ca.json\")\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tsrv := httptest.NewUnstartedServer(nil)\n\tconfig.Address = srv.Listener.Addr().String()\n\tca, err := New(config)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tbaseContext := buildContext(ca.auth, nil, nil, nil)\n\tsrv.Config.Handler = ca.srv.Handler\n\tsrv.Config.BaseContext = func(net.Listener) context.Context {\n\t\treturn baseContext\n\t}\n\tsrv.TLS = ca.srv.TLSConfig\n\tsrv.StartTLS()\n\t// Force the use of GetCertificate on IPs\n\tsrv.TLS.Certificates = nil\n\treturn srv\n}\n\nfunc startCAServer(configFile string) (*CA, string, error) {\n\tconfig, err := authority.LoadConfiguration(configFile)\n\tif err != nil {\n\t\treturn nil, \"\", err\n\t}\n\tlistener := newLocalListener()\n\tconfig.Address = listener.Addr().String()\n\tcaURL := \"https://\" + listener.Addr().String()\n\tca, err := New(config)\n\tif err != nil {\n\t\treturn nil, \"\", err\n\t}\n\tgo func() {\n\t\tca.srv.Serve(listener)\n\t}()\n\treturn ca, caURL, nil\n}\n\nfunc mTLSMiddleware(next http.Handler, nonAuthenticatedPaths ...string) http.Handler {\n\treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tif r.URL.Path == \"/version\" {\n\t\t\trender.JSON(w, r, api.VersionResponse{\n\t\t\t\tVersion:                     \"test\",\n\t\t\t\tRequireClientAuthentication: true,\n\t\t\t})\n\t\t\treturn\n\t\t}\n\n\t\tfor _, s := range nonAuthenticatedPaths {\n\t\t\tif strings.HasPrefix(r.URL.Path, s) || strings.HasPrefix(r.URL.Path, \"/1.0\"+s) {\n\t\t\t\tnext.ServeHTTP(w, r)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t\tisMTLS := r.TLS != nil && len(r.TLS.PeerCertificates) > 0\n\t\tif !isMTLS {\n\t\t\trender.Error(w, r, errs.Unauthorized(\"missing peer certificate\"))\n\t\t} else {\n\t\t\tnext.ServeHTTP(w, r)\n\t\t}\n\t})\n}\n\nfunc generateBootstrapToken(ca, subject, sha string) string {\n\tnow := time.Now()\n\tjwk, err := jose.ReadKey(\"testdata/secrets/ott_mariano_priv.jwk\", jose.WithPassword([]byte(\"password\")))\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\topts := new(jose.SignerOptions).WithType(\"JWT\").WithHeader(\"kid\", jwk.KeyID)\n\tsig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: jwk.Key}, opts)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tid, err := randutil.ASCII(64)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tcl := struct {\n\t\tSHA string `json:\"sha\"`\n\t\tjose.Claims\n\t\tSANS []string `json:\"sans\"`\n\t}{\n\t\tSHA: sha,\n\t\tClaims: jose.Claims{\n\t\t\tID:        id,\n\t\t\tSubject:   subject,\n\t\t\tIssuer:    \"mariano\",\n\t\t\tNotBefore: jose.NewNumericDate(now),\n\t\t\tExpiry:    jose.NewNumericDate(now.Add(time.Minute)),\n\t\t\tAudience:  []string{ca + \"/sign\"},\n\t\t},\n\t\tSANS: []string{subject},\n\t}\n\traw, err := jose.Signed(sig).Claims(cl).CompactSerialize()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn raw\n}\n\nfunc TestBootstrap(t *testing.T) {\n\tsrv := startCABootstrapServer()\n\tdefer srv.Close()\n\ttoken := generateBootstrapToken(srv.URL, \"subject\", \"ef742f95dc0d8aa82d3cca4017af6dac3fce84290344159891952d18c53eefe7\")\n\tclient, err := NewClient(srv.URL+\"/sign\", WithRootFile(\"testdata/secrets/root_ca.crt\"))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\ttype args struct {\n\t\ttoken string\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\targs    args\n\t\twant    *Client\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", args{token}, client, false},\n\t\t{\"token err\", args{\"badtoken\"}, nil, true},\n\t\t{\"bad claims\", args{\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.foo.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c\"}, nil, true},\n\t\t{\"bad sha\", args{generateBootstrapToken(srv.URL, \"subject\", \"\")}, nil, true},\n\t\t{\"bad aud\", args{generateBootstrapToken(\"\", \"subject\", \"ef742f95dc0d8aa82d3cca4017af6dac3fce84290344159891952d18c53eefe7\")}, nil, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := Bootstrap(tt.args.token)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"Bootstrap() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif tt.wantErr {\n\t\t\t\tif !reflect.DeepEqual(got, tt.want) {\n\t\t\t\t\tt.Errorf(\"Bootstrap() = %v, want %v\", got, tt.want)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif got == nil {\n\t\t\t\t\tt.Error(\"Bootstrap() = nil, want not nil\")\n\t\t\t\t} else {\n\t\t\t\t\tif !reflect.DeepEqual(got.endpoint, tt.want.endpoint) {\n\t\t\t\t\t\tt.Errorf(\"Bootstrap() endpoint = %v, want %v\", got.endpoint, tt.want.endpoint)\n\t\t\t\t\t}\n\t\t\t\t\tgotTR := got.client.GetTransport().(*http.Transport)\n\t\t\t\t\twantTR := tt.want.client.GetTransport().(*http.Transport)\n\t\t\t\t\tif !equalPools(gotTR.TLSClientConfig.RootCAs, wantTR.TLSClientConfig.RootCAs) {\n\t\t\t\t\t\tt.Errorf(\"Bootstrap() certPool = %v, want %v\", gotTR.TLSClientConfig.RootCAs, wantTR.TLSClientConfig.RootCAs)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\n//nolint:gosec // insecure test servers\nfunc TestBootstrapServerWithoutMTLS(t *testing.T) {\n\tsrv := startCABootstrapServer()\n\tdefer srv.Close()\n\ttoken := func() string {\n\t\treturn generateBootstrapToken(srv.URL, \"subject\", \"ef742f95dc0d8aa82d3cca4017af6dac3fce84290344159891952d18c53eefe7\")\n\t}\n\n\tmtlsServer := startCABootstrapServer()\n\tnext := mtlsServer.Config.Handler\n\tmtlsServer.Config.Handler = mTLSMiddleware(next, \"/root/\", \"/sign\")\n\tdefer mtlsServer.Close()\n\tmtlsToken := func() string {\n\t\treturn generateBootstrapToken(mtlsServer.URL, \"subject\", \"ef742f95dc0d8aa82d3cca4017af6dac3fce84290344159891952d18c53eefe7\")\n\t}\n\n\ttype args struct {\n\t\tctx   context.Context\n\t\ttoken string\n\t\tbase  *http.Server\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\targs    args\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", args{context.Background(), token(), &http.Server{}}, false},\n\t\t{\"ok mtls\", args{context.Background(), mtlsToken(), &http.Server{}}, false},\n\t\t{\"fail\", args{context.Background(), \"bad-token\", &http.Server{}}, true},\n\t\t{\"fail with TLSConfig\", args{context.Background(), token(), &http.Server{TLSConfig: &tls.Config{}}}, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := BootstrapServer(tt.args.ctx, tt.args.token, tt.args.base, VerifyClientCertIfGiven())\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"BootstrapServer() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif tt.wantErr {\n\t\t\t\tif got != nil {\n\t\t\t\t\tt.Errorf(\"BootstrapServer() = %v, want nil\", got)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\texpected := &http.Server{\n\t\t\t\t\tTLSConfig: got.TLSConfig,\n\t\t\t\t}\n\t\t\t\t//nolint:govet // not comparing errors\n\t\t\t\tif !reflect.DeepEqual(got, expected) {\n\t\t\t\t\tt.Errorf(\"BootstrapServer() = %v, want %v\", got, expected)\n\t\t\t\t}\n\t\t\t\tif got.TLSConfig == nil || got.TLSConfig.ClientCAs == nil || got.TLSConfig.RootCAs == nil || got.TLSConfig.GetCertificate == nil || got.TLSConfig.GetClientCertificate == nil {\n\t\t\t\t\tt.Errorf(\"BootstrapServer() invalid TLSConfig = %#v\", got.TLSConfig)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\n//nolint:gosec // insecure test servers\nfunc TestBootstrapServerWithMTLS(t *testing.T) {\n\tsrv := startCABootstrapServer()\n\tdefer srv.Close()\n\ttoken := func() string {\n\t\treturn generateBootstrapToken(srv.URL, \"subject\", \"ef742f95dc0d8aa82d3cca4017af6dac3fce84290344159891952d18c53eefe7\")\n\t}\n\n\tmtlsServer := startCABootstrapServer()\n\tnext := mtlsServer.Config.Handler\n\tmtlsServer.Config.Handler = mTLSMiddleware(next, \"/root/\", \"/sign\")\n\tdefer mtlsServer.Close()\n\tmtlsToken := func() string {\n\t\treturn generateBootstrapToken(mtlsServer.URL, \"subject\", \"ef742f95dc0d8aa82d3cca4017af6dac3fce84290344159891952d18c53eefe7\")\n\t}\n\n\ttype args struct {\n\t\tctx   context.Context\n\t\ttoken string\n\t\tbase  *http.Server\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\targs    args\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", args{context.Background(), token(), &http.Server{}}, false},\n\t\t{\"ok mtls\", args{context.Background(), mtlsToken(), &http.Server{}}, false},\n\t\t{\"fail\", args{context.Background(), \"bad-token\", &http.Server{}}, true},\n\t\t{\"fail with TLSConfig\", args{context.Background(), token(), &http.Server{TLSConfig: &tls.Config{}}}, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := BootstrapServer(tt.args.ctx, tt.args.token, tt.args.base)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"BootstrapServer() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif tt.wantErr {\n\t\t\t\tif got != nil {\n\t\t\t\t\tt.Errorf(\"BootstrapServer() = %v, want nil\", got)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\texpected := &http.Server{\n\t\t\t\t\tTLSConfig: got.TLSConfig,\n\t\t\t\t}\n\t\t\t\t//nolint:govet // not comparing errors\n\t\t\t\tif !reflect.DeepEqual(got, expected) {\n\t\t\t\t\tt.Errorf(\"BootstrapServer() = %v, want %v\", got, expected)\n\t\t\t\t}\n\t\t\t\tif got.TLSConfig == nil || got.TLSConfig.ClientCAs == nil || got.TLSConfig.RootCAs == nil || got.TLSConfig.GetCertificate == nil || got.TLSConfig.GetClientCertificate == nil {\n\t\t\t\t\tt.Errorf(\"BootstrapServer() invalid TLSConfig = %#v\", got.TLSConfig)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestBootstrapClient(t *testing.T) {\n\tsrv := startCABootstrapServer()\n\tdefer srv.Close()\n\ttoken := func() string {\n\t\treturn generateBootstrapToken(srv.URL, \"subject\", \"ef742f95dc0d8aa82d3cca4017af6dac3fce84290344159891952d18c53eefe7\")\n\t}\n\n\tmtlsServer := startCABootstrapServer()\n\tnext := mtlsServer.Config.Handler\n\tmtlsServer.Config.Handler = mTLSMiddleware(next, \"/root/\", \"/sign\")\n\tdefer mtlsServer.Close()\n\tmtlsToken := func() string {\n\t\treturn generateBootstrapToken(mtlsServer.URL, \"subject\", \"ef742f95dc0d8aa82d3cca4017af6dac3fce84290344159891952d18c53eefe7\")\n\t}\n\n\ttype args struct {\n\t\tctx   context.Context\n\t\ttoken string\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\targs    args\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", args{context.Background(), token()}, false},\n\t\t{\"ok mtls\", args{context.Background(), mtlsToken()}, false},\n\t\t{\"fail\", args{context.Background(), \"bad-token\"}, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := BootstrapClient(tt.args.ctx, tt.args.token)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"BootstrapClient() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif tt.wantErr {\n\t\t\t\tif got != nil {\n\t\t\t\t\tt.Errorf(\"BootstrapClient() = %v, want nil\", got)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\ttlsConfig := got.Transport.(*http.Transport).TLSClientConfig\n\t\t\t\tif tlsConfig == nil || tlsConfig.ClientCAs != nil || tlsConfig.GetClientCertificate == nil || tlsConfig.RootCAs == nil || tlsConfig.GetCertificate != nil {\n\t\t\t\t\tt.Errorf(\"BootstrapClient() invalid Transport = %#v\", tlsConfig)\n\t\t\t\t}\n\t\t\t\tresp, err := got.Post(srv.URL+\"/renew\", \"application/json\", http.NoBody)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Errorf(\"BootstrapClient() failed renewing certificate\")\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tvar renewal api.SignResponse\n\t\t\t\tif err := readJSON(resp.Body, &renewal); err != nil {\n\t\t\t\t\tt.Errorf(\"BootstrapClient() error reading response: %v\", err)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tif renewal.CaPEM.Certificate == nil || renewal.ServerPEM.Certificate == nil || len(renewal.CertChainPEM) == 0 {\n\t\t\t\t\tt.Errorf(\"BootstrapClient() invalid renewal response: %v\", renewal)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestBootstrapClientServerRotation(t *testing.T) {\n\tif os.Getenv(\"CI\") == \"true\" {\n\t\tt.Skipf(\"skip until we fix https://github.com/smallstep/certificates/issues/873\")\n\t}\n\treset := setMinCertDuration(1 * time.Second)\n\tdefer reset()\n\n\t// Configuration with current root\n\tconfig, err := authority.LoadConfiguration(\"testdata/rotate-ca-0.json\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\t// Get local address\n\tlistener := newLocalListener()\n\tconfig.Address = listener.Addr().String()\n\tcaURL := \"https://\" + listener.Addr().String()\n\n\t// Start CA server\n\tca, err := New(config)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tgo func() {\n\t\tca.srv.Serve(listener)\n\t}()\n\tdefer ca.Stop()\n\ttime.Sleep(1 * time.Second)\n\n\t// Create bootstrap server\n\ttoken := generateBootstrapToken(caURL, \"127.0.0.1\", \"ef742f95dc0d8aa82d3cca4017af6dac3fce84290344159891952d18c53eefe7\")\n\t//nolint:gosec // insecure test server\n\tserver, err := BootstrapServer(context.Background(), token, &http.Server{\n\t\tAddr: \":0\",\n\t\tHandler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tw.Write([]byte(\"ok\"))\n\t\t}),\n\t}, RequireAndVerifyClientCert())\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tlistener = newLocalListener()\n\tsrvURL := \"https://\" + listener.Addr().String()\n\tgo func() {\n\t\tserver.ServeTLS(listener, \"\", \"\")\n\t}()\n\tdefer server.Close()\n\ttime.Sleep(1 * time.Second)\n\n\t// Create bootstrap client\n\ttoken = generateBootstrapToken(caURL, \"client\", \"ef742f95dc0d8aa82d3cca4017af6dac3fce84290344159891952d18c53eefe7\")\n\tclient, err := BootstrapClient(context.Background(), token)\n\tif err != nil {\n\t\tt.Errorf(\"BootstrapClient() error = %v\", err)\n\t\treturn\n\t}\n\n\t// doTest does a request that requires mTLS\n\tdoTest := func(client *http.Client) error {\n\t\t// test with ca\n\t\tresp, err := client.Post(caURL+\"/renew\", \"application/json\", http.NoBody)\n\t\tif err != nil {\n\t\t\treturn errors.Wrap(err, \"client.Post() failed\")\n\t\t}\n\t\tvar renew api.SignResponse\n\t\tif err := readJSON(resp.Body, &renew); err != nil {\n\t\t\treturn errors.Wrap(err, \"client.Post() error reading response\")\n\t\t}\n\t\tif renew.ServerPEM.Certificate == nil || renew.CaPEM.Certificate == nil || len(renew.CertChainPEM) == 0 {\n\t\t\treturn errors.New(\"client.Post() unexpected response found\")\n\t\t}\n\t\t// test with bootstrap server\n\t\tresp, err = client.Get(srvURL)\n\t\tif err != nil {\n\t\t\treturn errors.Wrapf(err, \"client.Get(%s) failed\", srvURL)\n\t\t}\n\t\tdefer resp.Body.Close()\n\t\tb, err := io.ReadAll(resp.Body)\n\t\tif err != nil {\n\t\t\treturn errors.Wrap(err, \"client.Get() error reading response\")\n\t\t}\n\t\tif string(b) != \"ok\" {\n\t\t\treturn errors.New(\"client.Get() unexpected response found\")\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Test with default root\n\tif err := doTest(client); err != nil {\n\t\tt.Errorf(\"Test with rotate-ca-0.json failed: %v\", err)\n\t}\n\n\t// wait for renew\n\ttime.Sleep(5 * time.Second)\n\n\t// Reload with configuration with current and future root\n\tca.opts.configFile = \"testdata/rotate-ca-1.json\"\n\tif err := doReload(ca); err != nil {\n\t\tt.Errorf(\"ca.Reload() error = %v\", err)\n\t\treturn\n\t}\n\tif err := doTest(client); err != nil {\n\t\tt.Errorf(\"Test with rotate-ca-1.json failed: %v\", err)\n\t}\n\n\t// wait for renew\n\ttime.Sleep(5 * time.Second)\n\n\t// Reload with new and old root\n\tca.opts.configFile = \"testdata/rotate-ca-2.json\"\n\tif err := doReload(ca); err != nil {\n\t\tt.Errorf(\"ca.Reload() error = %v\", err)\n\t\treturn\n\t}\n\tif err := doTest(client); err != nil {\n\t\tt.Errorf(\"Test with rotate-ca-2.json failed: %v\", err)\n\t}\n\n\t// wait for renew\n\ttime.Sleep(5 * time.Second)\n\n\t// Reload with pnly the new root\n\tca.opts.configFile = \"testdata/rotate-ca-3.json\"\n\tif err := doReload(ca); err != nil {\n\t\tt.Errorf(\"ca.Reload() error = %v\", err)\n\t\treturn\n\t}\n\tif err := doTest(client); err != nil {\n\t\tt.Errorf(\"Test with rotate-ca-3.json failed: %v\", err)\n\t}\n}\n\nfunc TestBootstrapClientServerFederation(t *testing.T) {\n\treset := setMinCertDuration(1 * time.Second)\n\tdefer reset()\n\n\tca1, caURL1, err := startCAServer(\"testdata/ca.json\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdefer ca1.Stop()\n\n\tca2, caURL2, err := startCAServer(\"testdata/federated-ca.json\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdefer ca2.Stop()\n\n\t// Create bootstrap server\n\ttoken := generateBootstrapToken(caURL1, \"127.0.0.1\", \"ef742f95dc0d8aa82d3cca4017af6dac3fce84290344159891952d18c53eefe7\")\n\t//nolint:gosec // insecure test server\n\tserver, err := BootstrapServer(context.Background(), token, &http.Server{\n\t\tAddr: \":0\",\n\t\tHandler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tw.Write([]byte(\"ok\"))\n\t\t}),\n\t}, RequireAndVerifyClientCert(), AddFederationToClientCAs())\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tlistener := newLocalListener()\n\tsrvURL := \"https://\" + listener.Addr().String()\n\tgo func() {\n\t\tserver.ServeTLS(listener, \"\", \"\")\n\t}()\n\tdefer server.Close()\n\n\t// Create bootstrap client\n\ttoken = generateBootstrapToken(caURL2, \"client\", \"c86f74bb7eb2eabef45c4f7fc6c146359ed3a5bbad416b31da5dce8093bcbffd\")\n\tclient, err := BootstrapClient(context.Background(), token, AddFederationToRootCAs())\n\tif err != nil {\n\t\tt.Errorf(\"BootstrapClient() error = %v\", err)\n\t\treturn\n\t}\n\n\t// doTest does a request that requires mTLS\n\tdoTest := func(client *http.Client) error {\n\t\t// test with ca\n\t\tresp, err := client.Post(caURL2+\"/renew\", \"application/json\", http.NoBody)\n\t\tif err != nil {\n\t\t\treturn errors.Wrap(err, \"client.Post() failed\")\n\t\t}\n\t\tvar renew api.SignResponse\n\t\tif err := readJSON(resp.Body, &renew); err != nil {\n\t\t\treturn errors.Wrap(err, \"client.Post() error reading response\")\n\t\t}\n\t\tif renew.ServerPEM.Certificate == nil || renew.CaPEM.Certificate == nil || len(renew.CertChainPEM) == 0 {\n\t\t\treturn errors.New(\"client.Post() unexpected response found\")\n\t\t}\n\t\t// test with bootstrap server\n\t\tresp, err = client.Get(srvURL)\n\t\tif err != nil {\n\t\t\treturn errors.Wrapf(err, \"client.Get(%s) failed\", srvURL)\n\t\t}\n\t\tdefer resp.Body.Close()\n\t\tb, err := io.ReadAll(resp.Body)\n\t\tif err != nil {\n\t\t\treturn errors.Wrap(err, \"client.Get() error reading response\")\n\t\t}\n\t\tif string(b) != \"ok\" {\n\t\t\treturn errors.New(\"client.Get() unexpected response found\")\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Test with default root\n\tif err := doTest(client); err != nil {\n\t\tt.Errorf(\"Test with rotate-ca-0.json failed: %v\", err)\n\t}\n}\n\n// doReload uses the reload implementation but overwrites the new address with\n// the one being used.\nfunc doReload(ca *CA) error {\n\tconfig, err := authority.LoadConfiguration(ca.opts.configFile)\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"error reloading ca\")\n\t}\n\n\tnewCA, err := New(config,\n\t\tWithPassword(ca.opts.password),\n\t\tWithConfigFile(ca.opts.configFile),\n\t\tWithDatabase(ca.auth.GetDatabase()))\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"error reloading ca\")\n\t}\n\t// Use same address in new server\n\tnewCA.srv.Addr = ca.srv.Addr\n\tif err := ca.srv.Reload(newCA.srv); err != nil {\n\t\treturn err\n\t}\n\n\t// Wait a few ms until the http server calls listener.Accept()\n\ttime.Sleep(100 * time.Millisecond)\n\treturn nil\n}\n\nfunc TestBootstrapListener(t *testing.T) {\n\tsrv := startCABootstrapServer()\n\tdefer srv.Close()\n\ttoken := func() string {\n\t\treturn generateBootstrapToken(srv.URL, \"127.0.0.1\", \"ef742f95dc0d8aa82d3cca4017af6dac3fce84290344159891952d18c53eefe7\")\n\t}\n\n\tmtlsServer := startCABootstrapServer()\n\tnext := mtlsServer.Config.Handler\n\tmtlsServer.Config.Handler = mTLSMiddleware(next, \"/root/\", \"/sign\")\n\tdefer mtlsServer.Close()\n\tmtlsToken := func() string {\n\t\treturn generateBootstrapToken(mtlsServer.URL, \"127.0.0.1\", \"ef742f95dc0d8aa82d3cca4017af6dac3fce84290344159891952d18c53eefe7\")\n\t}\n\n\ttype args struct {\n\t\ttoken string\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\targs    args\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", args{token()}, false},\n\t\t{\"ok mtls\", args{mtlsToken()}, false},\n\t\t{\"fail\", args{\"bad-token\"}, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tinner := newLocalListener()\n\t\t\tdefer inner.Close()\n\t\t\tlis, err := BootstrapListener(context.Background(), tt.args.token, inner)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"BootstrapListener() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif tt.wantErr {\n\t\t\t\tif lis != nil {\n\t\t\t\t\tt.Errorf(\"BootstrapListener() = %v, want nil\", lis)\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\t\t\twg := new(sync.WaitGroup)\n\t\t\twg.Add(1)\n\t\t\tgo func() {\n\t\t\t\thttp.Serve(lis, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\t\tw.Write([]byte(\"ok\"))\n\t\t\t\t}))\n\t\t\t\twg.Done()\n\t\t\t}()\n\t\t\tdefer wg.Wait()\n\t\t\tdefer lis.Close()\n\n\t\t\tclient, err := BootstrapClient(context.Background(), token())\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"BootstrapClient() error = %v\", err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tresp, err := client.Get(\"https://\" + lis.Addr().String())\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"client.Get() error = %v\", err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tdefer resp.Body.Close()\n\t\t\tb, err := io.ReadAll(resp.Body)\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"io.ReadAll() error = %v\", err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif string(b) != \"ok\" {\n\t\t\t\tt.Errorf(\"client.Get() = %s, want ok\", string(b))\n\t\t\t\treturn\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "ca/ca.go",
    "content": "package ca\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"reflect\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/coreos/go-systemd/v22/daemon\"\n\t\"github.com/go-chi/chi/v5\"\n\t\"github.com/go-chi/chi/v5/middleware\"\n\t\"golang.org/x/sync/errgroup\"\n\n\t\"github.com/smallstep/cli-utils/step\"\n\t\"github.com/smallstep/nosql\"\n\t\"go.step.sm/crypto/x509util\"\n\n\t\"github.com/smallstep/certificates/acme\"\n\tacmeAPI \"github.com/smallstep/certificates/acme/api\"\n\tacmeNoSQL \"github.com/smallstep/certificates/acme/db/nosql\"\n\t\"github.com/smallstep/certificates/api\"\n\t\"github.com/smallstep/certificates/authority\"\n\t\"github.com/smallstep/certificates/authority/admin\"\n\tadminAPI \"github.com/smallstep/certificates/authority/admin/api\"\n\t\"github.com/smallstep/certificates/authority/config\"\n\t\"github.com/smallstep/certificates/cas/apiv1\"\n\t\"github.com/smallstep/certificates/db\"\n\t\"github.com/smallstep/certificates/internal/httptransport\"\n\t\"github.com/smallstep/certificates/internal/metrix\"\n\t\"github.com/smallstep/certificates/logging\"\n\t\"github.com/smallstep/certificates/middleware/requestid\"\n\t\"github.com/smallstep/certificates/monitoring\"\n\t\"github.com/smallstep/certificates/scep\"\n\tscepAPI \"github.com/smallstep/certificates/scep/api\"\n\t\"github.com/smallstep/certificates/server\"\n)\n\ntype options struct {\n\tconfigFile      string\n\tlinkedCAToken   string\n\tquiet           bool\n\tpassword        []byte\n\tissuerPassword  []byte\n\tsshHostPassword []byte\n\tsshUserPassword []byte\n\tdatabase        db.AuthDB\n\tx509CAService   apiv1.CertificateAuthorityService\n\ttlsConfig       *tls.Config\n}\n\nfunc (o *options) apply(opts []Option) {\n\tfor _, fn := range opts {\n\t\tfn(o)\n\t}\n}\n\n// Option is the type of options passed to the CA constructor.\ntype Option func(o *options)\n\n// WithConfigFile sets the given name as the configuration file name in the CA\n// options.\nfunc WithConfigFile(name string) Option {\n\treturn func(o *options) {\n\t\to.configFile = name\n\t}\n}\n\n// WithX509CAService provides the x509CAService to be used for signing x509 requests\nfunc WithX509CAService(svc apiv1.CertificateAuthorityService) Option {\n\treturn func(o *options) {\n\t\to.x509CAService = svc\n\t}\n}\n\n// WithPassword sets the given password as the configured password in the CA\n// options.\nfunc WithPassword(password []byte) Option {\n\treturn func(o *options) {\n\t\to.password = password\n\t}\n}\n\n// WithSSHHostPassword sets the given password to decrypt the key used to sign\n// ssh host certificates.\nfunc WithSSHHostPassword(password []byte) Option {\n\treturn func(o *options) {\n\t\to.sshHostPassword = password\n\t}\n}\n\n// WithSSHUserPassword sets the given password to decrypt the key used to sign\n// ssh user certificates.\nfunc WithSSHUserPassword(password []byte) Option {\n\treturn func(o *options) {\n\t\to.sshUserPassword = password\n\t}\n}\n\n// WithIssuerPassword sets the given password as the configured certificate\n// issuer password in the CA options.\nfunc WithIssuerPassword(password []byte) Option {\n\treturn func(o *options) {\n\t\to.issuerPassword = password\n\t}\n}\n\n// WithDatabase sets the given authority database to the CA options.\nfunc WithDatabase(d db.AuthDB) Option {\n\treturn func(o *options) {\n\t\to.database = d\n\t}\n}\n\n// WithTLSConfig sets the TLS configuration to be used by the HTTP(s) server\n// spun by step-ca.\nfunc WithTLSConfig(t *tls.Config) Option {\n\treturn func(o *options) {\n\t\to.tlsConfig = t\n\t}\n}\n\n// WithLinkedCAToken sets the token used to authenticate with the linkedca.\nfunc WithLinkedCAToken(token string) Option {\n\treturn func(o *options) {\n\t\to.linkedCAToken = token\n\t}\n}\n\n// WithQuiet sets the quiet flag.\nfunc WithQuiet(quiet bool) Option {\n\treturn func(o *options) {\n\t\to.quiet = quiet\n\t}\n}\n\n// CA is the type used to build the complete certificate authority. It builds\n// the HTTP server, set ups the middlewares and the HTTP handlers.\ntype CA struct {\n\tauth        *authority.Authority\n\tconfig      *config.Config\n\tsrv         *server.Server\n\tinsecureSrv *server.Server\n\tmetricsSrv  *server.Server\n\topts        *options\n\trenewer     *TLSRenewer\n\tcompactStop chan struct{}\n}\n\n// New creates and initializes the CA with the given configuration and options.\nfunc New(cfg *config.Config, opts ...Option) (*CA, error) {\n\tca := &CA{\n\t\tconfig:      cfg,\n\t\topts:        new(options),\n\t\tcompactStop: make(chan struct{}),\n\t}\n\tca.opts.apply(opts)\n\treturn ca.Init(cfg)\n}\n\n// Init initializes the CA with the given configuration.\nfunc (ca *CA) Init(cfg *config.Config) (*CA, error) {\n\t// Set password, it's ok to set nil password, the ca will prompt for them if\n\t// they are required.\n\topts := []authority.Option{\n\t\tauthority.WithPassword(ca.opts.password),\n\t\tauthority.WithSSHHostPassword(ca.opts.sshHostPassword),\n\t\tauthority.WithSSHUserPassword(ca.opts.sshUserPassword),\n\t\tauthority.WithIssuerPassword(ca.opts.issuerPassword),\n\t}\n\tif ca.opts.linkedCAToken != \"\" {\n\t\topts = append(opts, authority.WithLinkedCAToken(ca.opts.linkedCAToken))\n\t}\n\n\tif ca.opts.database != nil {\n\t\topts = append(opts, authority.WithDatabase(ca.opts.database))\n\t}\n\n\tif ca.opts.quiet {\n\t\topts = append(opts, authority.WithQuietInit())\n\t}\n\n\tif ca.opts.x509CAService != nil {\n\t\topts = append(opts, authority.WithX509CAService(ca.opts.x509CAService))\n\t}\n\n\tvar meter *metrix.Meter\n\tif ca.config.MetricsAddress != \"\" {\n\t\tmeter = metrix.New()\n\t\topts = append(opts, authority.WithMeter(meter))\n\t}\n\n\twebhookTransport := httptransport.New()\n\topts = append(opts,\n\t\tauthority.WithWebhookClient(&http.Client{Transport: webhookTransport}),\n\t)\n\n\tauth, err := authority.New(cfg, opts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tca.auth = auth\n\n\tvar tlsConfig *tls.Config\n\tvar clientTLSConfig *tls.Config\n\tif ca.opts.tlsConfig != nil {\n\t\t// try using the tls Configuration supplied by the caller\n\t\tlog.Print(\"Using tls configuration supplied by the application\")\n\t\ttlsConfig = ca.opts.tlsConfig\n\t\tclientTLSConfig = ca.opts.tlsConfig\n\t} else {\n\t\t// default to using the step-ca x509 Signer Interface\n\t\tlog.Print(\"Building new tls configuration using step-ca x509 Signer Interface\")\n\t\ttlsConfig, clientTLSConfig, err = ca.getTLSConfig(auth)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\twebhookTransport.TLSClientConfig = clientTLSConfig\n\n\t// Using chi as the main router\n\tmux := chi.NewRouter()\n\thandler := http.Handler(mux)\n\n\tinsecureMux := chi.NewRouter()\n\tinsecureHandler := http.Handler(insecureMux)\n\n\t// Add HEAD middleware\n\tmux.Use(middleware.GetHead)\n\tinsecureMux.Use(middleware.GetHead)\n\n\t// Add regular CA api endpoints in / and /1.0\n\tapi.Route(mux)\n\tmux.Route(\"/1.0\", func(r chi.Router) {\n\t\tapi.Route(r)\n\t})\n\n\t// Mount the CRL to the insecure mux\n\tinsecureMux.Get(\"/crl\", api.CRL)\n\tinsecureMux.Get(\"/1.0/crl\", api.CRL)\n\n\t// Add ACME api endpoints in /acme and /1.0/acme\n\tdns := cfg.DNSNames[0]\n\tu, err := url.Parse(\"https://\" + cfg.Address)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tport := u.Port()\n\tif port != \"\" && port != \"443\" {\n\t\tdns = fmt.Sprintf(\"%s:%s\", dns, port)\n\t}\n\n\t// ACME Router is only available if we have a database.\n\tvar acmeDB acme.DB\n\tvar acmeLinker acme.Linker\n\tif cfg.DB == nil && auth.HasACMEProvisioner() {\n\t\tlog.Println(\"WARNING: No database is configured. ACME provisioners are disabled.\")\n\t}\n\tif cfg.DB != nil {\n\t\tacmeDB, err = acmeNoSQL.New(auth.GetDatabase().(nosql.DB))\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"error configuring ACME DB interface: %w\", err)\n\t\t}\n\t\tacmeLinker = acme.NewLinker(dns, \"acme\")\n\t\tmux.Route(\"/acme\", func(r chi.Router) {\n\t\t\tacmeAPI.Route(r)\n\t\t})\n\t\t// Use 2.0 because, at the moment, our ACME api is only compatible with v2.0\n\t\t// of the ACME spec.\n\t\tmux.Route(\"/2.0/acme\", func(r chi.Router) {\n\t\t\tacmeAPI.Route(r)\n\t\t})\n\t}\n\n\t// Admin API Router\n\tif cfg.AuthorityConfig.EnableAdmin {\n\t\tadminDB := auth.GetAdminDatabase()\n\t\tif adminDB != nil {\n\t\t\tacmeAdminResponder := adminAPI.NewACMEAdminResponder()\n\t\t\tpolicyAdminResponder := adminAPI.NewPolicyAdminResponder()\n\t\t\twebhookAdminResponder := adminAPI.NewWebhookAdminResponder()\n\t\t\tmux.Route(\"/admin\", func(r chi.Router) {\n\t\t\t\tadminAPI.Route(\n\t\t\t\t\tr,\n\t\t\t\t\tadminAPI.WithACMEResponder(acmeAdminResponder),\n\t\t\t\t\tadminAPI.WithPolicyResponder(policyAdminResponder),\n\t\t\t\t\tadminAPI.WithWebhookResponder(webhookAdminResponder),\n\t\t\t\t)\n\t\t\t})\n\t\t}\n\t}\n\n\tvar scepAuthority *scep.Authority\n\tif ca.shouldServeSCEPEndpoints() {\n\t\t// get the SCEP authority configuration. Validation is\n\t\t// performed within the authority instantiation process.\n\t\tscepAuthority = auth.GetSCEP()\n\n\t\t// According to the RFC (https://tools.ietf.org/html/rfc8894#section-7.10),\n\t\t// SCEP operations are performed using HTTP, so that's why the API is mounted\n\t\t// to the insecure mux.\n\t\tscepPrefix := \"scep\"\n\t\tinsecureMux.Route(\"/\"+scepPrefix, func(r chi.Router) {\n\t\t\tscepAPI.Route(r)\n\t\t})\n\n\t\t// The RFC also mentions usage of HTTPS, but seems to advise\n\t\t// against it, because of potential interoperability issues.\n\t\t// Currently I think it's not bad to use HTTPS also, so that's\n\t\t// why I've kept the API endpoints in both muxes and both HTTP\n\t\t// as well as HTTPS can be used to request certificates\n\t\t// using SCEP.\n\t\tmux.Route(\"/\"+scepPrefix, func(r chi.Router) {\n\t\t\tscepAPI.Route(r)\n\t\t})\n\t}\n\n\t// helpful routine for logging all routes\n\t//dumpRoutes(mux)\n\t//dumpRoutes(insecureMux)\n\n\t// Add monitoring if configured\n\tif len(cfg.Monitoring) > 0 {\n\t\tm, err := monitoring.New(cfg.Monitoring)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\thandler = m.Middleware(handler)\n\t\tinsecureHandler = m.Middleware(insecureHandler)\n\t}\n\n\t// Add logger if configured\n\tvar legacyTraceHeader string\n\tif len(cfg.Logger) > 0 {\n\t\tlogger, err := logging.New(\"ca\", cfg.Logger)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tlegacyTraceHeader = logger.GetTraceHeader()\n\t\thandler = logger.Middleware(handler)\n\t\tinsecureHandler = logger.Middleware(insecureHandler)\n\t}\n\n\t// always use request ID middleware; traceHeader is provided for backwards compatibility (for now)\n\thandler = requestid.New(legacyTraceHeader).Middleware(handler)\n\tinsecureHandler = requestid.New(legacyTraceHeader).Middleware(insecureHandler)\n\n\t// Create context with all the necessary values.\n\tbaseContext := buildContext(auth, scepAuthority, acmeDB, acmeLinker)\n\n\tca.srv = server.New(cfg.Address, handler, tlsConfig)\n\tca.srv.BaseContext = func(net.Listener) context.Context {\n\t\treturn baseContext\n\t}\n\n\t// only start the insecure server if the insecure address is configured\n\t// and, currently, also only when it should serve SCEP endpoints.\n\tif ca.shouldServeInsecureServer() {\n\t\t// TODO: instead opt for having a single server.Server but two\n\t\t// http.Servers handling the HTTP and HTTPS handler? The latter\n\t\t// will probably introduce more complexity in terms of graceful\n\t\t// reload.\n\t\tca.insecureSrv = server.New(cfg.InsecureAddress, insecureHandler, nil)\n\t\tca.insecureSrv.BaseContext = func(net.Listener) context.Context {\n\t\t\treturn baseContext\n\t\t}\n\t}\n\n\tif meter != nil {\n\t\tca.metricsSrv = server.New(ca.config.MetricsAddress, meter, nil)\n\t\tca.metricsSrv.BaseContext = func(net.Listener) context.Context {\n\t\t\treturn baseContext\n\t\t}\n\t}\n\n\treturn ca, nil\n}\n\n// shouldServeInsecureServer returns whether or not the insecure\n// server should also be started. This is (currently) only the case\n// if the insecure address has been configured AND when a SCEP\n// provisioner is configured or when a CRL is configured.\nfunc (ca *CA) shouldServeInsecureServer() bool {\n\tswitch {\n\tcase ca.config.InsecureAddress == \"\":\n\t\treturn false\n\tcase ca.shouldServeSCEPEndpoints():\n\t\treturn true\n\tcase ca.config.CRL.IsEnabled():\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\n// buildContext builds the server base context.\nfunc buildContext(a *authority.Authority, scepAuthority *scep.Authority, acmeDB acme.DB, acmeLinker acme.Linker) context.Context {\n\tctx := authority.NewContext(context.Background(), a)\n\tif authDB := a.GetDatabase(); authDB != nil {\n\t\tctx = db.NewContext(ctx, authDB)\n\t}\n\tif adminDB := a.GetAdminDatabase(); adminDB != nil {\n\t\tctx = admin.NewContext(ctx, adminDB)\n\t}\n\tif scepAuthority != nil {\n\t\tctx = scep.NewContext(ctx, scepAuthority)\n\t}\n\tif acmeDB != nil {\n\t\tctx = acme.NewContext(ctx, acmeDB, acme.NewClient(), acmeLinker, nil)\n\t}\n\treturn ctx\n}\n\n// Run starts the CA calling to the server ListenAndServe method.\nfunc (ca *CA) Run() error {\n\tif !ca.opts.quiet {\n\t\tauthorityInfo := ca.auth.GetInfo()\n\t\tlog.Printf(\"Starting %s\", step.Version())\n\t\tlog.Printf(\"Documentation: https://u.step.sm/docs/ca\")\n\t\tlog.Printf(\"Community Discord: https://u.step.sm/discord\")\n\t\tif step.Contexts().GetCurrent() != nil {\n\t\t\tlog.Printf(\"Current context: %s\", step.Contexts().GetCurrent().Name)\n\t\t}\n\t\tlog.Printf(\"Config file: %s\", ca.getConfigFileOutput())\n\t\tbaseURL := fmt.Sprintf(\"https://%s%s\",\n\t\t\tauthorityInfo.DNSNames[0],\n\t\t\tca.config.Address[strings.LastIndex(ca.config.Address, \":\"):])\n\t\tlog.Printf(\"The primary server URL is %s\", baseURL)\n\t\tlog.Printf(\"Root certificates are available at %s/roots.pem\", baseURL)\n\t\tif len(authorityInfo.DNSNames) > 1 {\n\t\t\tlog.Printf(\"Additional configured hostnames: %s\",\n\t\t\t\tstrings.Join(authorityInfo.DNSNames[1:], \", \"))\n\t\t}\n\t\tfor _, crt := range authorityInfo.RootX509Certs {\n\t\t\tlog.Printf(\"X.509 Root Fingerprint: %s\", x509util.Fingerprint(crt))\n\t\t}\n\t\tif authorityInfo.SSHCAHostPublicKey != nil {\n\t\t\tlog.Printf(\"SSH Host CA Key: %s\\n\", bytes.TrimSpace(authorityInfo.SSHCAHostPublicKey))\n\t\t}\n\t\tif authorityInfo.SSHCAUserPublicKey != nil {\n\t\t\tlog.Printf(\"SSH User CA Key: %s\\n\", bytes.TrimSpace(authorityInfo.SSHCAUserPublicKey))\n\t\t}\n\t}\n\n\teg := new(errgroup.Group)\n\teg.Go(func() error {\n\t\tca.runCompactJob()\n\t\treturn nil\n\t})\n\n\tif ca.insecureSrv != nil {\n\t\teg.Go(func() error {\n\t\t\treturn ca.insecureSrv.ListenAndServe()\n\t\t})\n\t}\n\n\tif ca.metricsSrv != nil {\n\t\teg.Go(func() error {\n\t\t\treturn ca.metricsSrv.ListenAndServe()\n\t\t})\n\t}\n\n\teg.Go(func() error {\n\t\treturn ca.srv.ListenAndServe()\n\t})\n\n\t_, _ = daemon.SdNotify(true, daemon.SdNotifyReady)\n\n\terr := eg.Wait()\n\n\t_, _ = daemon.SdNotify(true, daemon.SdNotifyStopping)\n\n\t// if the error is not the usual HTTP server closed error, it is\n\t// highly likely that an error occurred when starting one of the\n\t// CA servers, possibly because of a port already being in use or\n\t// some part of the configuration not being correct. This case is\n\t// handled by stopping the CA in its entirety.\n\tif !errors.Is(err, http.ErrServerClosed) {\n\t\tlog.Println(\"shutting down due to startup error ...\")\n\t\tif stopErr := ca.Stop(); stopErr != nil {\n\t\t\terr = fmt.Errorf(\"failed stopping CA after error occurred: %w: %w\", err, stopErr)\n\t\t} else {\n\t\t\terr = fmt.Errorf(\"stopped CA after error occurred: %w\", err)\n\t\t}\n\t}\n\n\treturn err\n}\n\n// Stop stops the CA calling to the server Shutdown method.\nfunc (ca *CA) Stop() error {\n\tclose(ca.compactStop)\n\tif ca.renewer != nil {\n\t\tca.renewer.Stop()\n\t}\n\n\tif err := ca.auth.Shutdown(); err != nil {\n\t\tlog.Printf(\"error stopping ca.Authority: %+v\\n\", err)\n\t}\n\n\t// Concurrently shutdown services\n\tvar eg errgroup.Group\n\tif ca.insecureSrv != nil {\n\t\teg.Go(func() error {\n\t\t\treturn ca.insecureSrv.Shutdown()\n\t\t})\n\t}\n\n\tif ca.metricsSrv != nil {\n\t\teg.Go(func() error {\n\t\t\treturn ca.metricsSrv.Shutdown()\n\t\t})\n\t}\n\n\tif ca.srv != nil {\n\t\teg.Go(func() error {\n\t\t\treturn ca.srv.Shutdown()\n\t\t})\n\t}\n\n\t// Return first error\n\treturn eg.Wait()\n}\n\n// Reload reloads the configuration of the CA and calls to the server Reload\n// method.\nfunc (ca *CA) Reload() error {\n\t_, _ = daemon.SdNotify(true, daemon.SdNotifyReloading)\n\n\tcfg, err := config.LoadConfiguration(ca.opts.configFile)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error reloading ca configuration: %w\", err)\n\t}\n\n\tlogContinue := func(reason string) {\n\t\tlog.Println(reason)\n\t\tlog.Println(\"Continuing to run with the original configuration.\")\n\t\tlog.Println(\"You can force a restart by sending a SIGTERM signal and then restarting the step-ca.\")\n\t}\n\n\t// Do not allow reload if the database configuration has changed.\n\tif !reflect.DeepEqual(ca.config.DB, cfg.DB) {\n\t\tlogContinue(\"Reload failed because the database configuration has changed.\")\n\t\treturn errors.New(\"error reloading ca: database configuration cannot change\")\n\t}\n\n\tnewCA, err := New(cfg,\n\t\tWithPassword(ca.opts.password),\n\t\tWithSSHHostPassword(ca.opts.sshHostPassword),\n\t\tWithSSHUserPassword(ca.opts.sshUserPassword),\n\t\tWithIssuerPassword(ca.opts.issuerPassword),\n\t\tWithLinkedCAToken(ca.opts.linkedCAToken),\n\t\tWithQuiet(ca.opts.quiet),\n\t\tWithConfigFile(ca.opts.configFile),\n\t\tWithDatabase(ca.auth.GetDatabase()),\n\t)\n\tif err != nil {\n\t\tlogContinue(\"Reload failed because the CA with new configuration could not be initialized.\")\n\t\treturn fmt.Errorf(\"error reloading ca: %w\", err)\n\t}\n\n\tif ca.insecureSrv != nil {\n\t\tif err = ca.insecureSrv.Reload(newCA.insecureSrv); err != nil {\n\t\t\tlogContinue(\"Reload failed because insecure server could not be replaced.\")\n\t\t\treturn fmt.Errorf(\"error reloading insecure server: %w\", err)\n\t\t}\n\t}\n\n\tif ca.metricsSrv != nil {\n\t\tif err = ca.metricsSrv.Reload(newCA.metricsSrv); err != nil {\n\t\t\tlogContinue(\"Reload failed because metrics server could not be replaced.\")\n\t\t\treturn fmt.Errorf(\"error reloading metrics server: %w\", err)\n\t\t}\n\t}\n\n\tif err = ca.srv.Reload(newCA.srv); err != nil {\n\t\tlogContinue(\"Reload failed because server could not be replaced.\")\n\t\treturn fmt.Errorf(\"error reloading server: %w\", err)\n\t}\n\n\t// 1. Stop previous renewer\n\t// 2. Safely shutdown any internal resources (e.g. key manager)\n\t// 3. Replace ca properties\n\t// Do not replace ca.srv\n\tif ca.renewer != nil {\n\t\tca.renewer.Stop()\n\t}\n\n\tca.auth.CloseForReload()\n\tca.auth = newCA.auth\n\tca.config = newCA.config\n\tca.opts = newCA.opts\n\tca.renewer = newCA.renewer\n\n\t_, _ = daemon.SdNotify(true, daemon.SdNotifyReady)\n\n\treturn nil\n}\n\n// get TLSConfig returns separate TLSConfigs for server and client with the\n// same self-renewing certificate.\nfunc (ca *CA) getTLSConfig(auth *authority.Authority) (*tls.Config, *tls.Config, error) {\n\t// Create initial TLS certificate\n\ttlsCrt, err := auth.GetTLSCertificate()\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\n\t// Start tls renewer with the new certificate.\n\t// If a renewer was started, attempt to stop it before.\n\tif ca.renewer != nil {\n\t\tca.renewer.Stop()\n\t}\n\n\tca.renewer, err = NewTLSRenewer(tlsCrt, auth.GetTLSCertificate)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\tca.renewer.Run()\n\n\tvar serverTLSConfig *tls.Config\n\tif ca.config.TLS != nil {\n\t\tserverTLSConfig = ca.config.TLS.TLSConfig()\n\t} else {\n\t\tserverTLSConfig = &tls.Config{\n\t\t\tMinVersion: tls.VersionTLS12,\n\t\t}\n\t}\n\n\t// GetCertificate will only be called if the client supplies SNI\n\t// information or if tlsConfig.Certificates is empty.\n\t// When client requests are made using an IP address (as opposed to a domain\n\t// name) the server does not receive any SNI and may fallback to using the\n\t// first entry in the Certificates attribute; by setting the attribute to\n\t// empty we are implicitly forcing GetCertificate to be the only mechanism\n\t// by which the server can find it's own leaf Certificate.\n\tserverTLSConfig.Certificates = []tls.Certificate{}\n\n\tclientTLSConfig := serverTLSConfig.Clone()\n\n\tserverTLSConfig.GetCertificate = ca.renewer.GetCertificateForCA\n\tclientTLSConfig.GetClientCertificate = ca.renewer.GetClientCertificate\n\n\t// initialize a certificate pool with root CA certificates to trust when doing mTLS.\n\tcertPool := x509.NewCertPool()\n\t// initialize a certificate pool with root CA certificates to trust when connecting\n\t// to webhook servers\n\trootCAsPool, err := x509.SystemCertPool()\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\tfor _, crt := range auth.GetRootCertificates() {\n\t\tcertPool.AddCert(crt)\n\t\trootCAsPool.AddCert(crt)\n\t}\n\n\t// adding the intermediate CA certificates to the pool will allow clients that\n\t// do mTLS but don't send an intermediate to successfully connect. The intermediates\n\t// added here are used when building a certificate chain.\n\tintermediates := tlsCrt.Certificate[1:]\n\tfor _, certBytes := range intermediates {\n\t\tcert, err := x509.ParseCertificate(certBytes)\n\t\tif err != nil {\n\t\t\treturn nil, nil, err\n\t\t}\n\t\tcertPool.AddCert(cert)\n\t\trootCAsPool.AddCert(cert)\n\t}\n\n\t// Add support for mutual tls to renew certificates\n\tserverTLSConfig.ClientAuth = tls.VerifyClientCertIfGiven\n\tserverTLSConfig.ClientCAs = certPool\n\n\tclientTLSConfig.RootCAs = rootCAsPool\n\n\treturn serverTLSConfig, clientTLSConfig, nil\n}\n\n// shouldServeSCEPEndpoints returns if the CA should be\n// configured with endpoints for SCEP. This is assumed to be\n// true if a SCEPService exists, which is true in case at\n// least one SCEP provisioner was configured.\nfunc (ca *CA) shouldServeSCEPEndpoints() bool {\n\treturn ca.auth.GetSCEP() != nil\n}\n\n//nolint:unused // useful for debugging\nfunc dumpRoutes(mux chi.Routes) {\n\t// helpful routine for logging all routes\n\twalkFunc := func(method string, route string, _ http.Handler, _ ...func(http.Handler) http.Handler) error {\n\t\tfmt.Printf(\"%s %s\\n\", method, route)\n\t\treturn nil\n\t}\n\tif err := chi.Walk(mux, walkFunc); err != nil {\n\t\tfmt.Printf(\"Logging err: %s\\n\", err.Error())\n\t}\n}\n\nfunc (ca *CA) getConfigFileOutput() string {\n\tif ca.config.WasLoadedFromFile() {\n\t\treturn ca.config.Filepath()\n\t}\n\treturn \"loaded from token\"\n}\n\n// runCompactJob will run the value log garbage collector if the nosql database\n// supports it.\nfunc (ca *CA) runCompactJob() {\n\tcaDB, ok := ca.auth.GetDatabase().(*db.DB)\n\tif !ok {\n\t\treturn\n\t}\n\tcompactor, ok := caDB.DB.(nosql.Compactor)\n\tif !ok {\n\t\treturn\n\t}\n\n\t// Compact database at start.\n\trunCompact(compactor)\n\n\t// Compact database every minute.\n\tticker := time.NewTicker(time.Minute)\n\tdefer ticker.Stop()\n\n\tfor {\n\t\tselect {\n\t\tcase <-ca.compactStop:\n\t\t\treturn\n\t\tcase <-ticker.C:\n\t\t\trunCompact(compactor)\n\t\t}\n\t}\n}\n\n// runCompact executes the compact job until it returns an error.\nfunc runCompact(c nosql.Compactor) {\n\tfor err := error(nil); err == nil; {\n\t\terr = c.Compact(0.7)\n\t}\n}\n"
  },
  {
    "path": "ca/ca_test.go",
    "content": "package ca\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto\"\n\t\"crypto/rand\"\n\t\"crypto/sha1\" //nolint:gosec // used to create the Subject Key Identifier by RFC 5280\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"crypto/x509/pkix\"\n\t\"encoding/asn1\"\n\t\"encoding/json\"\n\t\"encoding/pem\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n\t\"github.com/smallstep/assert\"\n\t\"github.com/smallstep/certificates/api\"\n\t\"github.com/smallstep/certificates/authority\"\n\t\"github.com/smallstep/certificates/authority/provisioner\"\n\t\"github.com/smallstep/certificates/errs\"\n\t\"go.step.sm/crypto/jose\"\n\t\"go.step.sm/crypto/keyutil\"\n\t\"go.step.sm/crypto/pemutil\"\n\t\"go.step.sm/crypto/randutil\"\n\t\"go.step.sm/crypto/x509util\"\n)\n\ntype ClosingBuffer struct {\n\t*bytes.Buffer\n}\n\nfunc (cb *ClosingBuffer) Close() error {\n\treturn nil\n}\n\nfunc getCSR(priv interface{}) (*x509.CertificateRequest, error) {\n\t_csr := &x509.CertificateRequest{\n\t\tSubject:  pkix.Name{CommonName: \"test.smallstep.com\"},\n\t\tDNSNames: []string{\"test.smallstep.com\"},\n\t}\n\tcsrBytes, err := x509.CreateCertificateRequest(rand.Reader, _csr, priv)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn x509.ParseCertificateRequest(csrBytes)\n}\n\nfunc generateSubjectKeyID(pub crypto.PublicKey) ([]byte, error) {\n\tb, err := x509.MarshalPKIXPublicKey(pub)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"error marshaling public key\")\n\t}\n\tinfo := struct {\n\t\tAlgorithm        pkix.AlgorithmIdentifier\n\t\tSubjectPublicKey asn1.BitString\n\t}{}\n\tif _, err = asn1.Unmarshal(b, &info); err != nil {\n\t\treturn nil, errors.Wrap(err, \"error unmarshaling public key\")\n\t}\n\n\t//nolint:gosec // used to create the Subject Key Identifier by RFC 5280\n\thash := sha1.Sum(info.SubjectPublicKey.Bytes)\n\treturn hash[:], nil\n}\n\nfunc TestMain(m *testing.M) {\n\tDisableIdentity = true\n\tos.Exit(m.Run())\n}\n\nfunc TestCASign(t *testing.T) {\n\tpub, priv, err := keyutil.GenerateDefaultKeyPair()\n\tassert.FatalError(t, err)\n\n\tasn1dn := &authority.ASN1DN{\n\t\tCountry:       \"Tazmania\",\n\t\tOrganization:  \"Acme Co\",\n\t\tLocality:      \"Landscapes\",\n\t\tProvince:      \"Sudden Cliffs\",\n\t\tStreetAddress: \"TNT\",\n\t\tCommonName:    \"test.smallstep.com\",\n\t}\n\n\tconfig, err := authority.LoadConfiguration(\"testdata/ca.json\")\n\tassert.FatalError(t, err)\n\tconfig.AuthorityConfig.Template = asn1dn\n\tca, err := New(config)\n\tassert.FatalError(t, err)\n\tintermediateCert, err := pemutil.ReadCertificate(\"testdata/secrets/intermediate_ca.crt\")\n\tassert.FatalError(t, err)\n\tclijwk, err := jose.ReadKey(\"testdata/secrets/step_cli_key_priv.jwk\", jose.WithPassword([]byte(\"pass\")))\n\tassert.FatalError(t, err)\n\tsig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: clijwk.Key},\n\t\t(&jose.SignerOptions{}).WithType(\"JWT\").WithHeader(\"kid\", clijwk.KeyID))\n\tassert.FatalError(t, err)\n\tvalidAud := []string{\"https://127.0.0.1:0/sign\"}\n\n\tnow := time.Now().UTC()\n\tleafExpiry := now.Add(time.Minute * 5)\n\n\ttype signTest struct {\n\t\tca     *CA\n\t\tbody   string\n\t\tstatus int\n\t\terrMsg string\n\t}\n\ttests := map[string]func(t *testing.T) *signTest{\n\t\t\"fail invalid-json-body\": func(t *testing.T) *signTest {\n\t\t\treturn &signTest{\n\t\t\t\tca:     ca,\n\t\t\t\tbody:   \"invalid json\",\n\t\t\t\tstatus: http.StatusBadRequest,\n\t\t\t\terrMsg: errs.BadRequestPrefix,\n\t\t\t}\n\t\t},\n\t\t\"fail invalid-csr-sig\": func(t *testing.T) *signTest {\n\t\t\tder := []byte(`-----BEGIN CERTIFICATE REQUEST-----\nMIIDNjCCAh4CAQAwYzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRYwFAYDVQQH\nDA1TYW4gRnJhbmNpc2NvMRIwEAYDVQQKDAlzbWFsbHN0ZXAxGzAZBgNVBAMMEnRl\nc3Quc21hbGxzdGVwLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB\nANPahliigZ38QpBLmQMS3MVKKZ5gapNjqR7LIEYoYWa4lTFiUnbwg8tSfIFcgLZr\njNIxn7/98+JOJHKgS03NhFJoS5hej0LyypleOGJ0nk2qawYVKnn1ftoKjkfxkfZI\na/5rsDF1jhNBspB/KPHWE0eimKQJbUiVG1zA1sExnXDecF3vJfBj+DPDWngx4yxR\n/jYEKjt4tQ6Ei752TbosrCHYeYXzkr6iAwiNz6vT/ewLb6b8JmuN8X6Y1I9ogDGx\nhntBJ1jAK8x3IGTjYbkm+mqVuCyhNcHtGfEHcBnUEzLAPrVFn8kGiAnU17FJ0uQ7\n1C9CtUzgBRZCxSBm6Qs+Zs8CAwEAAaCBjTCBigYJKoZIhvcNAQkOMX0wezAMBgNV\nHRMBAf8EAjAAMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcDATAOBgNVHQ8B\nAf8EBAMCBaAwHQYDVR0RBBYwFIISdGVzdC5zbWFsbHN0ZXAuY29tMB0GA1UdDgQW\nBBQj6N4RTAAjhV3UBYXH72mkdOGpqzANBgkqhkiG9w0BAQsFAAOCAQEAN0/ivCBk\nFD53SqtRmqqc7C9saoRNvV+wDi4Sg6YGLFQLjbZPJrqQURWdHtV9O3sb3p8O5erX\n9Kgq3C7fqd//0mro4GZ1GTpjsPKIMocZFfH7zEhAZlvQLRKWICjoBaOwxQum2qY/\nB3+ltAXb4uqGdbI0jPkkyWGN5CQhK+ZHoYe/zGtTEmHBcPxRtJJkukQQjUgZhjU2\nZ7K+w3AjOxj47XLNHHlW83QYUJ2mN+mEZF9DhrZb2ydYOlpy0V2NJwv7QrmnFaDj\nR0v3BFLTblIp100li3oV2QaM/yESrgo9XIjEEGzCGz5cNs5ovNadufUZDCJyyT4q\nZEp7knvU2psWRw==\n-----END CERTIFICATE REQUEST-----`)\n\t\t\tblock, _ := pem.Decode(der)\n\t\t\tassert.NotNil(t, block)\n\t\t\tcsr, err := x509.ParseCertificateRequest(block.Bytes)\n\t\t\tassert.FatalError(t, err)\n\n\t\t\tbody, err := json.Marshal(&api.SignRequest{\n\t\t\t\tCsrPEM: api.CertificateRequest{CertificateRequest: csr},\n\t\t\t\tOTT:    \"foo\",\n\t\t\t})\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn &signTest{\n\t\t\t\tca:     ca,\n\t\t\t\tbody:   string(body),\n\t\t\t\tstatus: http.StatusBadRequest,\n\t\t\t\terrMsg: errs.BadRequestPrefix,\n\t\t\t}\n\t\t},\n\t\t\"fail unauthorized-ott\": func(t *testing.T) *signTest {\n\t\t\tcsr, err := getCSR(priv)\n\t\t\tassert.FatalError(t, err)\n\t\t\tbody, err := json.Marshal(&api.SignRequest{\n\t\t\t\tCsrPEM: api.CertificateRequest{CertificateRequest: csr},\n\t\t\t\tOTT:    \"foo\",\n\t\t\t})\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn &signTest{\n\t\t\t\tca:     ca,\n\t\t\t\tbody:   string(body),\n\t\t\t\tstatus: http.StatusUnauthorized,\n\t\t\t\terrMsg: errs.UnauthorizedDefaultMsg,\n\t\t\t}\n\t\t},\n\t\t\"fail commonname-claim\": func(t *testing.T) *signTest {\n\t\t\tjti, err := randutil.ASCII(32)\n\t\t\tassert.FatalError(t, err)\n\t\t\tcl := struct {\n\t\t\t\tjose.Claims\n\t\t\t\tSANS []string `json:\"sans\"`\n\t\t\t}{\n\t\t\t\tClaims: jose.Claims{\n\t\t\t\t\tSubject:   \"invalid\",\n\t\t\t\t\tIssuer:    \"step-cli\",\n\t\t\t\t\tNotBefore: jose.NewNumericDate(now),\n\t\t\t\t\tExpiry:    jose.NewNumericDate(now.Add(time.Minute)),\n\t\t\t\t\tAudience:  validAud,\n\t\t\t\t\tID:        jti,\n\t\t\t\t},\n\t\t\t\tSANS: []string{\"invalid\"},\n\t\t\t}\n\t\t\traw, err := jose.Signed(sig).Claims(cl).CompactSerialize()\n\t\t\tassert.FatalError(t, err)\n\t\t\tcsr, err := getCSR(priv)\n\t\t\tassert.FatalError(t, err)\n\t\t\tbody, err := json.Marshal(&api.SignRequest{\n\t\t\t\tCsrPEM: api.CertificateRequest{CertificateRequest: csr},\n\t\t\t\tOTT:    raw,\n\t\t\t})\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn &signTest{\n\t\t\t\tca:     ca,\n\t\t\t\tbody:   string(body),\n\t\t\t\tstatus: http.StatusForbidden,\n\t\t\t\terrMsg: errs.ForbiddenPrefix,\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) *signTest {\n\t\t\tjti, err := randutil.ASCII(32)\n\t\t\tassert.FatalError(t, err)\n\t\t\tcl := struct {\n\t\t\t\tjose.Claims\n\t\t\t\tSANS []string `json:\"sans\"`\n\t\t\t}{\n\t\t\t\tClaims: jose.Claims{\n\t\t\t\t\tSubject:   \"test.smallstep.com\",\n\t\t\t\t\tIssuer:    \"step-cli\",\n\t\t\t\t\tNotBefore: jose.NewNumericDate(now),\n\t\t\t\t\tExpiry:    jose.NewNumericDate(now.Add(time.Minute)),\n\t\t\t\t\tAudience:  validAud,\n\t\t\t\t\tID:        jti,\n\t\t\t\t},\n\t\t\t\tSANS: []string{\"test.smallstep.com\"},\n\t\t\t}\n\t\t\traw, err := jose.Signed(sig).Claims(cl).CompactSerialize()\n\t\t\tassert.FatalError(t, err)\n\t\t\tcsr, err := getCSR(priv)\n\t\t\tassert.FatalError(t, err)\n\t\t\tbody, err := json.Marshal(&api.SignRequest{\n\t\t\t\tCsrPEM:    api.CertificateRequest{CertificateRequest: csr},\n\t\t\t\tOTT:       raw,\n\t\t\t\tNotBefore: api.NewTimeDuration(now),\n\t\t\t\tNotAfter:  api.NewTimeDuration(leafExpiry),\n\t\t\t})\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn &signTest{\n\t\t\t\tca:     ca,\n\t\t\t\tbody:   string(body),\n\t\t\t\tstatus: http.StatusCreated,\n\t\t\t}\n\t\t},\n\t\t\"ok-backwards-compat-missing-subject-SAN\": func(t *testing.T) *signTest {\n\t\t\tjti, err := randutil.ASCII(32)\n\t\t\tassert.FatalError(t, err)\n\t\t\tcl := struct {\n\t\t\t\tjose.Claims\n\t\t\t\tSANS []string `json:\"sans\"`\n\t\t\t}{\n\t\t\t\tClaims: jose.Claims{\n\t\t\t\t\tSubject:   \"test.smallstep.com\",\n\t\t\t\t\tIssuer:    \"step-cli\",\n\t\t\t\t\tNotBefore: jose.NewNumericDate(now),\n\t\t\t\t\tExpiry:    jose.NewNumericDate(now.Add(time.Minute)),\n\t\t\t\t\tAudience:  validAud,\n\t\t\t\t\tID:        jti,\n\t\t\t\t},\n\t\t\t}\n\t\t\traw, err := jose.Signed(sig).Claims(cl).CompactSerialize()\n\t\t\tassert.FatalError(t, err)\n\t\t\tcsr, err := getCSR(priv)\n\t\t\tassert.FatalError(t, err)\n\t\t\tbody, err := json.Marshal(&api.SignRequest{\n\t\t\t\tCsrPEM:    api.CertificateRequest{CertificateRequest: csr},\n\t\t\t\tOTT:       raw,\n\t\t\t\tNotBefore: api.NewTimeDuration(now),\n\t\t\t\tNotAfter:  api.NewTimeDuration(leafExpiry),\n\t\t\t})\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn &signTest{\n\t\t\t\tca:     ca,\n\t\t\t\tbody:   string(body),\n\t\t\t\tstatus: http.StatusCreated,\n\t\t\t}\n\t\t},\n\t}\n\n\tfor name, genTestCase := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ttc := genTestCase(t)\n\n\t\t\trq, err := http.NewRequest(\"POST\", \"/sign\", strings.NewReader(tc.body))\n\t\t\tassert.FatalError(t, err)\n\t\t\trr := httptest.NewRecorder()\n\n\t\t\tctx := authority.NewContext(context.Background(), tc.ca.auth)\n\t\t\ttc.ca.srv.Handler.ServeHTTP(rr, rq.WithContext(ctx))\n\n\t\t\tif assert.Equals(t, rr.Code, tc.status) {\n\t\t\t\tbody := &ClosingBuffer{rr.Body}\n\t\t\t\tresp := &http.Response{\n\t\t\t\t\tBody: body,\n\t\t\t\t}\n\t\t\t\tif rr.Code < http.StatusBadRequest {\n\t\t\t\t\tvar sign api.SignResponse\n\t\t\t\t\tassert.FatalError(t, readJSON(body, &sign))\n\t\t\t\t\tleaf := sign.ServerPEM.Certificate\n\t\t\t\t\tintermediate := sign.CaPEM.Certificate\n\n\t\t\t\t\tassert.Equals(t, leaf.NotBefore, now.Truncate(time.Second))\n\t\t\t\t\tassert.Equals(t, leaf.NotAfter, leafExpiry.Truncate(time.Second))\n\n\t\t\t\t\tassert.Equals(t, leaf.Subject.String(),\n\t\t\t\t\t\tpkix.Name{\n\t\t\t\t\t\t\tCountry:       []string{asn1dn.Country},\n\t\t\t\t\t\t\tOrganization:  []string{asn1dn.Organization},\n\t\t\t\t\t\t\tLocality:      []string{asn1dn.Locality},\n\t\t\t\t\t\t\tStreetAddress: []string{asn1dn.StreetAddress},\n\t\t\t\t\t\t\tProvince:      []string{asn1dn.Province},\n\t\t\t\t\t\t\tCommonName:    asn1dn.CommonName,\n\t\t\t\t\t\t}.String())\n\t\t\t\t\tassert.Equals(t, leaf.Issuer, intermediate.Subject)\n\n\t\t\t\t\tassert.Equals(t, leaf.SignatureAlgorithm, x509.ECDSAWithSHA256)\n\t\t\t\t\tassert.Equals(t, leaf.PublicKeyAlgorithm, x509.ECDSA)\n\t\t\t\t\tassert.Equals(t, leaf.ExtKeyUsage,\n\t\t\t\t\t\t[]x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth})\n\t\t\t\t\tassert.Equals(t, leaf.DNSNames, []string{\"test.smallstep.com\"})\n\n\t\t\t\t\tsubjectKeyID, err := generateSubjectKeyID(pub)\n\t\t\t\t\tassert.FatalError(t, err)\n\t\t\t\t\tassert.Equals(t, leaf.SubjectKeyId, subjectKeyID)\n\n\t\t\t\t\tassert.Equals(t, leaf.AuthorityKeyId, intermediateCert.SubjectKeyId)\n\n\t\t\t\t\trealIntermediate, err := x509.ParseCertificate(intermediateCert.Raw)\n\t\t\t\t\tassert.FatalError(t, err)\n\t\t\t\t\tassert.Equals(t, intermediate, realIntermediate)\n\t\t\t\t} else {\n\t\t\t\t\terr := readError(resp)\n\t\t\t\t\tif tc.errMsg == \"\" {\n\t\t\t\t\t\tassert.FatalError(t, errors.New(\"must validate response error\"))\n\t\t\t\t\t}\n\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.errMsg)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestCAProvisioners(t *testing.T) {\n\tconfig, err := authority.LoadConfiguration(\"testdata/ca.json\")\n\tassert.FatalError(t, err)\n\tca, err := New(config)\n\tassert.FatalError(t, err)\n\n\ttype ekt struct {\n\t\tca     *CA\n\t\tstatus int\n\t\terrMsg string\n\t}\n\ttests := map[string]func(t *testing.T) *ekt{\n\t\t\"ok\": func(t *testing.T) *ekt {\n\t\t\treturn &ekt{\n\t\t\t\tca:     ca,\n\t\t\t\tstatus: http.StatusOK,\n\t\t\t}\n\t\t},\n\t}\n\n\tfor name, genTestCase := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ttc := genTestCase(t)\n\n\t\t\trq, err := http.NewRequest(\"GET\", \"/provisioners\", strings.NewReader(\"\"))\n\t\t\tassert.FatalError(t, err)\n\t\t\trr := httptest.NewRecorder()\n\n\t\t\tctx := authority.NewContext(context.Background(), tc.ca.auth)\n\t\t\ttc.ca.srv.Handler.ServeHTTP(rr, rq.WithContext(ctx))\n\n\t\t\tif assert.Equals(t, rr.Code, tc.status) {\n\t\t\t\tbody := &ClosingBuffer{rr.Body}\n\t\t\t\tresp := &http.Response{\n\t\t\t\t\tBody: body,\n\t\t\t\t}\n\t\t\t\tif rr.Code < http.StatusBadRequest {\n\t\t\t\t\tvar resp api.ProvisionersResponse\n\n\t\t\t\t\tassert.FatalError(t, readJSON(body, &resp))\n\t\t\t\t\ta, err := json.Marshal(config.AuthorityConfig.Provisioners)\n\t\t\t\t\tassert.FatalError(t, err)\n\t\t\t\t\tb, err := json.Marshal(resp.Provisioners)\n\t\t\t\t\tassert.FatalError(t, err)\n\t\t\t\t\tassert.Equals(t, a, b)\n\t\t\t\t} else {\n\t\t\t\t\terr := readError(resp)\n\t\t\t\t\tif tc.errMsg == \"\" {\n\t\t\t\t\t\tassert.FatalError(t, errors.New(\"must validate response error\"))\n\t\t\t\t\t}\n\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.errMsg)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestCAProvisionerEncryptedKey(t *testing.T) {\n\tconfig, err := authority.LoadConfiguration(\"testdata/ca.json\")\n\tassert.FatalError(t, err)\n\tca, err := New(config)\n\tassert.FatalError(t, err)\n\n\ttype ekt struct {\n\t\tca          *CA\n\t\tkid         string\n\t\texpectedKey string\n\t\tstatus      int\n\t\terrMsg      string\n\t}\n\ttests := map[string]func(t *testing.T) *ekt{\n\t\t\"not-found\": func(t *testing.T) *ekt {\n\t\t\treturn &ekt{\n\t\t\t\tca:     ca,\n\t\t\t\tkid:    \"foo\",\n\t\t\t\tstatus: http.StatusNotFound,\n\t\t\t\terrMsg: errs.NotFoundDefaultMsg,\n\t\t\t}\n\t\t},\n\t\t\"ok\": func(t *testing.T) *ekt {\n\t\t\tp := config.AuthorityConfig.Provisioners[2].(*provisioner.JWK)\n\t\t\treturn &ekt{\n\t\t\t\tca:          ca,\n\t\t\t\tkid:         p.Key.KeyID,\n\t\t\t\texpectedKey: p.EncryptedKey,\n\t\t\t\tstatus:      http.StatusOK,\n\t\t\t}\n\t\t},\n\t}\n\n\tfor name, genTestCase := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ttc := genTestCase(t)\n\n\t\t\trq, err := http.NewRequest(\"GET\", fmt.Sprintf(\"/provisioners/%s/encrypted-key\", tc.kid), strings.NewReader(\"\"))\n\t\t\tassert.FatalError(t, err)\n\t\t\trr := httptest.NewRecorder()\n\n\t\t\tctx := authority.NewContext(context.Background(), tc.ca.auth)\n\t\t\ttc.ca.srv.Handler.ServeHTTP(rr, rq.WithContext(ctx))\n\n\t\t\tif assert.Equals(t, rr.Code, tc.status) {\n\t\t\t\tbody := &ClosingBuffer{rr.Body}\n\t\t\t\tresp := &http.Response{\n\t\t\t\t\tBody: body,\n\t\t\t\t}\n\t\t\t\tif rr.Code < http.StatusBadRequest {\n\t\t\t\t\tvar ek api.ProvisionerKeyResponse\n\t\t\t\t\tassert.FatalError(t, readJSON(body, &ek))\n\t\t\t\t\tassert.Equals(t, ek.Key, tc.expectedKey)\n\t\t\t\t} else {\n\t\t\t\t\terr := readError(resp)\n\t\t\t\t\tif tc.errMsg == \"\" {\n\t\t\t\t\t\tassert.FatalError(t, errors.New(\"must validate response error\"))\n\t\t\t\t\t}\n\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.errMsg)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestCARoot(t *testing.T) {\n\tconfig, err := authority.LoadConfiguration(\"testdata/ca.json\")\n\tassert.FatalError(t, err)\n\tca, err := New(config)\n\tassert.FatalError(t, err)\n\n\trootCrt, err := pemutil.ReadCertificate(\"testdata/secrets/root_ca.crt\")\n\tassert.FatalError(t, err)\n\n\ttype rootTest struct {\n\t\tca     *CA\n\t\tsha    string\n\t\tstatus int\n\t\terrMsg string\n\t}\n\ttests := map[string]func(t *testing.T) *rootTest{\n\t\t\"not-found\": func(t *testing.T) *rootTest {\n\t\t\treturn &rootTest{\n\t\t\t\tca:     ca,\n\t\t\t\tsha:    \"foo\",\n\t\t\t\tstatus: http.StatusNotFound,\n\t\t\t\terrMsg: `root certificate with fingerprint \"foo\" was not found`,\n\t\t\t}\n\t\t},\n\t\t\"success\": func(t *testing.T) *rootTest {\n\t\t\treturn &rootTest{\n\t\t\t\tca:     ca,\n\t\t\t\tsha:    \"ef742f95dc0d8aa82d3cca4017af6dac3fce84290344159891952d18c53eefe7\",\n\t\t\t\tstatus: http.StatusOK,\n\t\t\t}\n\t\t},\n\t}\n\n\tfor name, genTestCase := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ttc := genTestCase(t)\n\n\t\t\trq, err := http.NewRequest(\"GET\", fmt.Sprintf(\"/root/%s\", tc.sha), strings.NewReader(\"\"))\n\t\t\tassert.FatalError(t, err)\n\t\t\trr := httptest.NewRecorder()\n\n\t\t\tctx := authority.NewContext(context.Background(), tc.ca.auth)\n\t\t\ttc.ca.srv.Handler.ServeHTTP(rr, rq.WithContext(ctx))\n\n\t\t\tif assert.Equals(t, rr.Code, tc.status) {\n\t\t\t\tbody := &ClosingBuffer{rr.Body}\n\t\t\t\tresp := &http.Response{\n\t\t\t\t\tBody: body,\n\t\t\t\t}\n\t\t\t\tif rr.Code < http.StatusBadRequest {\n\t\t\t\t\tvar root api.RootResponse\n\t\t\t\t\tassert.FatalError(t, readJSON(body, &root))\n\t\t\t\t\tassert.Equals(t, root.RootPEM.Certificate, rootCrt)\n\t\t\t\t} else {\n\t\t\t\t\terr := readError(resp)\n\t\t\t\t\tif tc.errMsg == \"\" {\n\t\t\t\t\t\tassert.FatalError(t, errors.New(\"must validate response error\"))\n\t\t\t\t\t}\n\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.errMsg)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestCAHealth(t *testing.T) {\n\tconfig, err := authority.LoadConfiguration(\"testdata/ca.json\")\n\tassert.FatalError(t, err)\n\tca, err := New(config)\n\tassert.FatalError(t, err)\n\n\ttype rootTest struct {\n\t\tca     *CA\n\t\tstatus int\n\t}\n\ttests := map[string]func(t *testing.T) *rootTest{\n\t\t\"success\": func(t *testing.T) *rootTest {\n\t\t\treturn &rootTest{\n\t\t\t\tca:     ca,\n\t\t\t\tstatus: http.StatusOK,\n\t\t\t}\n\t\t},\n\t}\n\n\tfor name, genTestCase := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ttc := genTestCase(t)\n\n\t\t\trq, err := http.NewRequest(\"GET\", \"/health\", strings.NewReader(\"\"))\n\t\t\tassert.FatalError(t, err)\n\t\t\trr := httptest.NewRecorder()\n\n\t\t\tctx := authority.NewContext(context.Background(), tc.ca.auth)\n\t\t\ttc.ca.srv.Handler.ServeHTTP(rr, rq.WithContext(ctx))\n\n\t\t\tif assert.Equals(t, rr.Code, tc.status) {\n\t\t\t\tbody := &ClosingBuffer{rr.Body}\n\t\t\t\tif rr.Code < http.StatusBadRequest {\n\t\t\t\t\tvar health api.HealthResponse\n\t\t\t\t\tassert.FatalError(t, readJSON(body, &health))\n\t\t\t\t\tassert.Equals(t, health, api.HealthResponse{Status: \"ok\"})\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestCARenew(t *testing.T) {\n\tpub, priv, err := keyutil.GenerateDefaultKeyPair()\n\tassert.FatalError(t, err)\n\n\tasn1dn := &authority.ASN1DN{\n\t\tCountry:       \"Tazmania\",\n\t\tOrganization:  \"Acme Co\",\n\t\tLocality:      \"Landscapes\",\n\t\tProvince:      \"Sudden Cliffs\",\n\t\tStreetAddress: \"TNT\",\n\t\tCommonName:    \"test\",\n\t}\n\n\tconfig, err := authority.LoadConfiguration(\"testdata/ca.json\")\n\tassert.FatalError(t, err)\n\tconfig.AuthorityConfig.Template = asn1dn\n\tca, err := New(config)\n\tassert.FatalError(t, err)\n\tassert.FatalError(t, err)\n\n\tintermediateCert, err := pemutil.ReadCertificate(\"testdata/secrets/intermediate_ca.crt\")\n\tassert.FatalError(t, err)\n\tintermediateKey, err := pemutil.Read(\"testdata/secrets/intermediate_ca_key\", pemutil.WithPassword([]byte(\"password\")))\n\tassert.FatalError(t, err)\n\n\tnow := time.Now().UTC()\n\tleafExpiry := now.Add(time.Minute * 5)\n\n\ttype renewTest struct {\n\t\tca           *CA\n\t\ttlsConnState *tls.ConnectionState\n\t\tstatus       int\n\t\terrMsg       string\n\t}\n\ttests := map[string]func(t *testing.T) *renewTest{\n\t\t\"request-missing-tls\": func(t *testing.T) *renewTest {\n\t\t\treturn &renewTest{\n\t\t\t\tca:           ca,\n\t\t\t\ttlsConnState: nil,\n\t\t\t\tstatus:       http.StatusBadRequest,\n\t\t\t\terrMsg:       errs.BadRequestPrefix,\n\t\t\t}\n\t\t},\n\t\t\"request-missing-peer-certificate\": func(t *testing.T) *renewTest {\n\t\t\treturn &renewTest{\n\t\t\t\tca:           ca,\n\t\t\t\ttlsConnState: &tls.ConnectionState{PeerCertificates: []*x509.Certificate{}},\n\t\t\t\tstatus:       http.StatusBadRequest,\n\t\t\t\terrMsg:       errs.BadRequestPrefix,\n\t\t\t}\n\t\t},\n\t\t\"success\": func(t *testing.T) *renewTest {\n\t\t\tcr, err := x509util.CreateCertificateRequest(\"test\", []string{\"funk\"}, priv.(crypto.Signer))\n\t\t\tassert.FatalError(t, err)\n\t\t\tcert, err := x509util.NewCertificate(cr)\n\t\t\tassert.FatalError(t, err)\n\t\t\tcrt := cert.GetCertificate()\n\t\t\tcrt.NotBefore = now\n\t\t\tcrt.NotAfter = leafExpiry\n\t\t\tcrt, err = x509util.CreateCertificate(crt, intermediateCert, pub, intermediateKey.(crypto.Signer))\n\t\t\tassert.FatalError(t, err)\n\t\t\treturn &renewTest{\n\t\t\t\tca: ca,\n\t\t\t\ttlsConnState: &tls.ConnectionState{\n\t\t\t\t\tPeerCertificates: []*x509.Certificate{crt},\n\t\t\t\t},\n\t\t\t\tstatus: http.StatusCreated,\n\t\t\t}\n\t\t},\n\t}\n\n\tfor name, genTestCase := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\ttc := genTestCase(t)\n\n\t\t\trq, err := http.NewRequest(\"POST\", \"/renew\", strings.NewReader(\"\"))\n\t\t\tassert.FatalError(t, err)\n\t\t\trq.TLS = tc.tlsConnState\n\t\t\trr := httptest.NewRecorder()\n\n\t\t\tctx := authority.NewContext(context.Background(), tc.ca.auth)\n\t\t\ttc.ca.srv.Handler.ServeHTTP(rr, rq.WithContext(ctx))\n\n\t\t\tif assert.Equals(t, rr.Code, tc.status) {\n\t\t\t\tbody := &ClosingBuffer{rr.Body}\n\t\t\t\tresp := &http.Response{\n\t\t\t\t\tBody: body,\n\t\t\t\t}\n\t\t\t\tif rr.Code < http.StatusBadRequest {\n\t\t\t\t\tvar sign api.SignResponse\n\t\t\t\t\tassert.FatalError(t, readJSON(body, &sign))\n\t\t\t\t\tleaf := sign.ServerPEM.Certificate\n\t\t\t\t\tintermediate := sign.CaPEM.Certificate\n\n\t\t\t\t\tassert.Equals(t, leaf.NotBefore, now.Truncate(time.Second))\n\t\t\t\t\tassert.Equals(t, leaf.NotAfter, leafExpiry.Truncate(time.Second))\n\n\t\t\t\t\tassert.Equals(t, leaf.Subject.String(),\n\t\t\t\t\t\tpkix.Name{\n\t\t\t\t\t\t\tCommonName: asn1dn.CommonName,\n\t\t\t\t\t\t}.String())\n\t\t\t\t\tassert.Equals(t, leaf.Issuer, intermediate.Subject)\n\n\t\t\t\t\tassert.Equals(t, leaf.SignatureAlgorithm, x509.ECDSAWithSHA256)\n\t\t\t\t\tassert.Equals(t, leaf.PublicKeyAlgorithm, x509.ECDSA)\n\t\t\t\t\tassert.Equals(t, leaf.ExtKeyUsage,\n\t\t\t\t\t\t[]x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth})\n\t\t\t\t\tassert.Equals(t, leaf.DNSNames, []string{\"funk\"})\n\n\t\t\t\t\tsubjectKeyID, err := generateSubjectKeyID(pub)\n\t\t\t\t\tassert.FatalError(t, err)\n\t\t\t\t\tassert.Equals(t, leaf.SubjectKeyId, subjectKeyID)\n\t\t\t\t\tassert.Equals(t, leaf.AuthorityKeyId, intermediateCert.SubjectKeyId)\n\n\t\t\t\t\trealIntermediate, err := x509.ParseCertificate(intermediateCert.Raw)\n\t\t\t\t\tassert.FatalError(t, err)\n\t\t\t\t\tassert.Equals(t, intermediate, realIntermediate)\n\n\t\t\t\t\tassert.Equals(t, *sign.TLSOptions, authority.DefaultTLSOptions)\n\t\t\t\t} else {\n\t\t\t\t\terr := readError(resp)\n\t\t\t\t\tif tc.errMsg == \"\" {\n\t\t\t\t\t\tassert.FatalError(t, errors.New(\"must validate response error\"))\n\t\t\t\t\t}\n\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.errMsg)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "ca/client/requestid.go",
    "content": "package client\n\nimport \"context\"\n\ntype contextKey struct{}\n\n// NewRequestIDContext returns a new context with the given request ID added to the\n// context.\nfunc NewRequestIDContext(ctx context.Context, requestID string) context.Context {\n\treturn context.WithValue(ctx, contextKey{}, requestID)\n}\n\n// RequestIDFromContext returns the request ID from the context if it exists.\n// and is not empty.\nfunc RequestIDFromContext(ctx context.Context) (string, bool) {\n\tv, ok := ctx.Value(contextKey{}).(string)\n\treturn v, ok && v != \"\"\n}\n"
  },
  {
    "path": "ca/client.go",
    "content": "package ca\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto\"\n\t\"crypto/ecdsa\"\n\t\"crypto/elliptic\"\n\t\"crypto/rand\"\n\t\"crypto/sha256\"\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"crypto/x509/pkix\"\n\t\"encoding/hex\"\n\t\"encoding/json\"\n\t\"encoding/pem\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n\t\"golang.org/x/net/http2\"\n\t\"google.golang.org/protobuf/encoding/protojson\"\n\t\"google.golang.org/protobuf/proto\"\n\n\t\"github.com/smallstep/cli-utils/step\"\n\t\"go.step.sm/crypto/jose\"\n\t\"go.step.sm/crypto/keyutil\"\n\t\"go.step.sm/crypto/pemutil\"\n\t\"go.step.sm/crypto/randutil\"\n\t\"go.step.sm/crypto/x509util\"\n\n\t\"github.com/smallstep/certificates/api\"\n\t\"github.com/smallstep/certificates/authority\"\n\t\"github.com/smallstep/certificates/authority/provisioner\"\n\t\"github.com/smallstep/certificates/ca/client\"\n\t\"github.com/smallstep/certificates/ca/identity\"\n\t\"github.com/smallstep/certificates/errs\"\n)\n\n// DisableIdentity is a global variable to disable the identity.\nvar DisableIdentity = false\n\n// UserAgent will set the User-Agent header in the client requests.\nvar UserAgent = \"step-http-client/1.0\"\n\ntype uaClient struct {\n\tClient *http.Client\n}\n\nfunc newClient(transport http.RoundTripper, timeout time.Duration) *uaClient {\n\treturn &uaClient{\n\t\tClient: &http.Client{\n\t\t\tTransport: transport,\n\t\t\tTimeout:   timeout,\n\t\t},\n\t}\n}\n\n//nolint:gosec // used in bootstrap protocol\nfunc newInsecureClient() *uaClient {\n\treturn &uaClient{\n\t\tClient: &http.Client{\n\t\t\tTransport: getDefaultTransport(&tls.Config{InsecureSkipVerify: true}),\n\t\t},\n\t}\n}\n\nfunc (c *uaClient) GetTransport() http.RoundTripper {\n\treturn c.Client.Transport\n}\n\nfunc (c *uaClient) SetTransport(tr http.RoundTripper) {\n\tc.Client.Transport = tr\n}\n\nfunc (c *uaClient) CloseIdleConnections() {\n\tc.Client.CloseIdleConnections()\n}\n\nfunc (c *uaClient) Get(u string) (*http.Response, error) {\n\treturn c.GetWithContext(context.Background(), u)\n}\n\nfunc (c *uaClient) GetWithContext(ctx context.Context, u string) (*http.Response, error) {\n\treq, err := http.NewRequestWithContext(ctx, \"GET\", u, http.NoBody)\n\tif err != nil {\n\t\treturn nil, errors.Wrapf(err, \"create GET %s request failed\", u)\n\t}\n\treturn c.Do(req)\n}\n\nfunc (c *uaClient) Post(u, contentType string, body io.Reader) (*http.Response, error) {\n\treturn c.PostWithContext(context.Background(), u, contentType, body)\n}\n\nfunc (c *uaClient) PostWithContext(ctx context.Context, u, contentType string, body io.Reader) (*http.Response, error) {\n\treq, err := http.NewRequestWithContext(ctx, \"POST\", u, body)\n\tif err != nil {\n\t\treturn nil, errors.Wrapf(err, \"create POST %s request failed\", u)\n\t}\n\treq.Header.Set(\"Content-Type\", contentType)\n\treturn c.Do(req)\n}\n\n// requestIDHeader is the header name used for propagating request IDs from\n// the CA client to the CA and back again.\nconst requestIDHeader = \"X-Request-Id\"\n\n// newRequestID generates a new random UUIDv4 request ID. If it fails,\n// the request ID will be the empty string.\nfunc newRequestID() string {\n\trequestID, err := randutil.UUIDv4()\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\n\treturn requestID\n}\n\n// enforceRequestID checks if the X-Request-Id HTTP header is filled. If it's\n// empty, the context is searched for a request ID. If that's also empty, a new\n// request ID is generated.\nfunc enforceRequestID(r *http.Request) {\n\tif requestID := r.Header.Get(requestIDHeader); requestID == \"\" {\n\t\tif reqID, ok := client.RequestIDFromContext(r.Context()); ok {\n\t\t\t// TODO(hs): ensure the request ID from the context is fresh, and thus hasn't been\n\t\t\t// used before by the client (unless it's a retry for the same request)?\n\t\t\trequestID = reqID\n\t\t} else {\n\t\t\trequestID = newRequestID()\n\t\t}\n\t\tr.Header.Set(requestIDHeader, requestID)\n\t}\n}\n\nfunc (c *uaClient) Do(req *http.Request) (*http.Response, error) {\n\treq.Header.Set(\"User-Agent\", UserAgent)\n\tenforceRequestID(req)\n\treturn c.Client.Do(req) //nolint:gosec // request to user-configured CA server\n}\n\n// RetryFunc defines the method used to retry a request. If it returns true, the\n// request will be retried once.\ntype RetryFunc func(code int) bool\n\n// ClientOption is the type of options passed to the Client constructor.\ntype ClientOption func(o *clientOptions) error\n\n// TransportDecorator is the type used to support customization of the HTTP\n// transport.\ntype TransportDecorator func(http.RoundTripper) http.RoundTripper\n\ntype clientOptions struct {\n\ttransport            http.RoundTripper\n\ttransportDecorator   TransportDecorator\n\ttimeout              time.Duration\n\trootSHA256           string\n\trootFilename         string\n\trootBundle           []byte\n\tcertificate          tls.Certificate\n\tgetClientCertificate func(*tls.CertificateRequestInfo) (*tls.Certificate, error)\n\tretryFunc            RetryFunc\n\tx5cJWK               *jose.JSONWebKey\n\tx5cCertFile          string\n\tx5cCertStrs          []string\n\tx5cCert              *x509.Certificate\n\tx5cSubject           string\n}\n\nfunc (o *clientOptions) apply(opts []ClientOption) (err error) {\n\to.applyDefaultIdentity()\n\tfor _, fn := range opts {\n\t\tif err = fn(o); err != nil {\n\t\t\treturn\n\t\t}\n\t}\n\treturn\n}\n\n// applyDefaultIdentity sets the options for the default identity if the\n// identity file is present. The identity is enabled by default.\nfunc (o *clientOptions) applyDefaultIdentity() {\n\tif DisableIdentity {\n\t\treturn\n\t}\n\n\t// Do not load an identity if something fails\n\ti, err := identity.LoadDefaultIdentity()\n\tif err != nil {\n\t\treturn\n\t}\n\tif err := i.Validate(); err != nil {\n\t\treturn\n\t}\n\tcrt, err := i.TLSCertificate()\n\tif err != nil {\n\t\treturn\n\t}\n\to.certificate = crt\n\to.getClientCertificate = i.GetClientCertificateFunc()\n}\n\n// checkTransport checks if other ways to set up a transport have been provided.\n// If they have it returns an error.\nfunc (o *clientOptions) checkTransport() error {\n\tif o.transport != nil || o.rootFilename != \"\" || o.rootSHA256 != \"\" || o.rootBundle != nil {\n\t\treturn errors.New(\"multiple transport methods have been configured\")\n\t}\n\treturn nil\n}\n\n// getTransport returns the transport configured in the clientOptions.\nfunc (o *clientOptions) getTransport(endpoint string) (tr http.RoundTripper, err error) {\n\tif o.transport != nil {\n\t\ttr = o.transport\n\t}\n\tif o.rootFilename != \"\" {\n\t\tif tr, err = getTransportFromFile(o.rootFilename); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\tif o.rootSHA256 != \"\" {\n\t\tif tr, err = getTransportFromSHA256(endpoint, o.rootSHA256); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\tif o.rootBundle != nil {\n\t\tif tr, err = getTransportFromCABundle(o.rootBundle); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\t// As the last option attempt to load the default root ca\n\tif tr == nil {\n\t\trootFile := getRootCAPath()\n\t\tif _, err := os.Stat(rootFile); err == nil {\n\t\t\tif tr, err = getTransportFromFile(rootFile); err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\t\tif tr == nil {\n\t\t\treturn nil, errors.New(\"a transport, a root cert, or a root sha256 must be used\")\n\t\t}\n\t}\n\n\t// Add client certificate if available\n\tif o.certificate.Certificate != nil {\n\t\tswitch tr := tr.(type) {\n\t\tcase *http.Transport:\n\t\t\tif tr.TLSClientConfig == nil {\n\t\t\t\ttr.TLSClientConfig = &tls.Config{\n\t\t\t\t\tMinVersion: tls.VersionTLS12,\n\t\t\t\t}\n\t\t\t}\n\t\t\tif len(tr.TLSClientConfig.Certificates) == 0 && tr.TLSClientConfig.GetClientCertificate == nil {\n\t\t\t\ttr.TLSClientConfig.Certificates = []tls.Certificate{o.certificate}\n\t\t\t\ttr.TLSClientConfig.GetClientCertificate = o.getClientCertificate\n\t\t\t}\n\t\tcase *http2.Transport:\n\t\t\tif tr.TLSClientConfig == nil {\n\t\t\t\ttr.TLSClientConfig = &tls.Config{\n\t\t\t\t\tMinVersion: tls.VersionTLS12,\n\t\t\t\t}\n\t\t\t}\n\t\t\tif len(tr.TLSClientConfig.Certificates) == 0 && tr.TLSClientConfig.GetClientCertificate == nil {\n\t\t\t\ttr.TLSClientConfig.Certificates = []tls.Certificate{o.certificate}\n\t\t\t\ttr.TLSClientConfig.GetClientCertificate = o.getClientCertificate\n\t\t\t}\n\t\tdefault:\n\t\t\treturn nil, errors.Errorf(\"unsupported transport type %T\", tr)\n\t\t}\n\t}\n\n\t// Wrap the transport using the decorator function if necessary\n\treturn decorateRoundTripper(tr, o.transportDecorator), nil\n}\n\n// WithTransport adds a custom transport to the Client. It will fail if a\n// previous option to create the transport has been configured.\nfunc WithTransport(tr http.RoundTripper) ClientOption {\n\treturn func(o *clientOptions) error {\n\t\tif err := o.checkTransport(); err != nil {\n\t\t\treturn err\n\t\t}\n\t\to.transport = tr\n\t\treturn nil\n\t}\n}\n\n// WithTransportDecorator allows customization of the HTTP transport used by the\n// client. The provided function receives the configured [http.RoundTripper] and\n// can wrap it with additional functionality.\nfunc WithTransportDecorator(fn TransportDecorator) ClientOption {\n\treturn func(o *clientOptions) error {\n\t\to.transportDecorator = fn\n\t\treturn nil\n\t}\n}\n\n// WithInsecure adds a insecure transport that bypasses TLS verification.\nfunc WithInsecure() ClientOption {\n\treturn func(o *clientOptions) error {\n\t\to.transport = &http.Transport{\n\t\t\tProxy: http.ProxyFromEnvironment,\n\t\t\tTLSClientConfig: &tls.Config{\n\t\t\t\tMinVersion: tls.VersionTLS12,\n\t\t\t\t//nolint:gosec // insecure option\n\t\t\t\tInsecureSkipVerify: true,\n\t\t\t},\n\t\t}\n\t\treturn nil\n\t}\n}\n\n// WithRootFile will create the transport using the given root certificate. It\n// will fail if a previous option to create the transport has been configured.\nfunc WithRootFile(filename string) ClientOption {\n\treturn func(o *clientOptions) error {\n\t\tif err := o.checkTransport(); err != nil {\n\t\t\treturn err\n\t\t}\n\t\to.rootFilename = filename\n\t\treturn nil\n\t}\n}\n\n// WithRootSHA256 will create the transport using an insecure client to retrieve\n// the root certificate using its fingerprint. It will fail if a previous option\n// to create the transport has been configured.\nfunc WithRootSHA256(sum string) ClientOption {\n\treturn func(o *clientOptions) error {\n\t\tif err := o.checkTransport(); err != nil {\n\t\t\treturn err\n\t\t}\n\t\to.rootSHA256 = sum\n\t\treturn nil\n\t}\n}\n\n// WithCABundle will create the transport using the given root certificates. It\n// will fail if a previous option to create the transport has been configured.\nfunc WithCABundle(bundle []byte) ClientOption {\n\treturn func(o *clientOptions) error {\n\t\tif err := o.checkTransport(); err != nil {\n\t\t\treturn err\n\t\t}\n\t\to.rootBundle = bundle\n\t\treturn nil\n\t}\n}\n\n// WithCertificate will set the given certificate as the TLS client certificate\n// in the client.\nfunc WithCertificate(cert tls.Certificate) ClientOption {\n\treturn func(o *clientOptions) error {\n\t\to.certificate = cert\n\t\treturn nil\n\t}\n}\n\n// WithAdminX5C will set the given file as the X5C certificate for use\n// by the client.\nfunc WithAdminX5C(certs []*x509.Certificate, key interface{}, passwordFile string) ClientOption {\n\treturn func(o *clientOptions) error {\n\t\t// Get private key from given key file\n\t\tvar (\n\t\t\terr  error\n\t\t\topts []jose.Option\n\t\t)\n\t\tif passwordFile != \"\" {\n\t\t\topts = append(opts, jose.WithPasswordFile(passwordFile))\n\t\t}\n\t\tblk, err := pemutil.Serialize(key)\n\t\tif err != nil {\n\t\t\treturn errors.Wrap(err, \"error serializing private key\")\n\t\t}\n\t\to.x5cJWK, err = jose.ParseKey(pem.EncodeToMemory(blk), opts...)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\to.x5cCertStrs, err = jose.ValidateX5C(certs, o.x5cJWK.Key)\n\t\tif err != nil {\n\t\t\treturn errors.Wrap(err, \"error validating x5c certificate chain and key for use in x5c header\")\n\t\t}\n\n\t\to.x5cCert = certs[0]\n\t\tswitch leaf := certs[0]; {\n\t\tcase leaf.Subject.CommonName != \"\":\n\t\t\to.x5cSubject = leaf.Subject.CommonName\n\t\tcase len(leaf.DNSNames) > 0:\n\t\t\to.x5cSubject = leaf.DNSNames[0]\n\t\tcase len(leaf.EmailAddresses) > 0:\n\t\t\to.x5cSubject = leaf.EmailAddresses[0]\n\t\t}\n\n\t\treturn nil\n\t}\n}\n\n// WithRetryFunc defines a method used to retry a request.\nfunc WithRetryFunc(fn RetryFunc) ClientOption {\n\treturn func(o *clientOptions) error {\n\t\to.retryFunc = fn\n\t\treturn nil\n\t}\n}\n\n// WithTimeout defines the time limit for requests made by this client. The\n// timeout includes connection time, any redirects, and reading the response\n// body.\nfunc WithTimeout(d time.Duration) ClientOption {\n\treturn func(o *clientOptions) error {\n\t\to.timeout = d\n\t\treturn nil\n\t}\n}\n\nfunc getTransportFromFile(filename string) (http.RoundTripper, error) {\n\tdata, err := os.ReadFile(filename) // #nosec G703 -- filename is based on configuration; data read from file is processed with expected format\n\tif err != nil {\n\t\treturn nil, errors.Wrapf(err, \"error reading %s\", filename)\n\t}\n\tpool := x509.NewCertPool()\n\tif !pool.AppendCertsFromPEM(data) {\n\t\treturn nil, errors.Errorf(\"error parsing %s: no certificates found\", filename)\n\t}\n\treturn getDefaultTransport(&tls.Config{\n\t\tMinVersion:               tls.VersionTLS12,\n\t\tPreferServerCipherSuites: true,\n\t\tRootCAs:                  pool,\n\t}), nil\n}\n\nfunc getTransportFromSHA256(endpoint, sum string) (http.RoundTripper, error) {\n\tu, err := parseEndpoint(endpoint)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tcaClient := &Client{endpoint: u}\n\troot, err := caClient.Root(sum)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tpool := x509.NewCertPool()\n\tpool.AddCert(root.RootPEM.Certificate)\n\treturn getDefaultTransport(&tls.Config{\n\t\tMinVersion:               tls.VersionTLS12,\n\t\tPreferServerCipherSuites: true,\n\t\tRootCAs:                  pool,\n\t}), nil\n}\n\nfunc getTransportFromCABundle(bundle []byte) (http.RoundTripper, error) {\n\tpool := x509.NewCertPool()\n\tif !pool.AppendCertsFromPEM(bundle) {\n\t\treturn nil, errors.New(\"error parsing ca bundle: no certificates found\")\n\t}\n\treturn getDefaultTransport(&tls.Config{\n\t\tMinVersion:               tls.VersionTLS12,\n\t\tPreferServerCipherSuites: true,\n\t\tRootCAs:                  pool,\n\t}), nil\n}\n\n// parseEndpoint parses and validates the given endpoint. It supports general\n// URLs like https://ca.smallstep.com[:port][/path], and incomplete URLs like\n// ca.smallstep.com[:port][/path].\nfunc parseEndpoint(endpoint string) (*url.URL, error) {\n\tu, err := url.Parse(endpoint)\n\tif err != nil {\n\t\treturn nil, errors.Wrapf(err, \"error parsing endpoint '%s'\", endpoint)\n\t}\n\n\t// URLs are generally parsed as:\n\t// [scheme:][//[userinfo@]host][/]path[?query][#fragment]\n\t// But URLs that do not start with a slash after the scheme are interpreted as\n\t// scheme:opaque[?query][#fragment]\n\tif u.Opaque == \"\" {\n\t\tif u.Scheme == \"\" {\n\t\t\tu.Scheme = \"https\"\n\t\t}\n\t\tif u.Host == \"\" {\n\t\t\t// endpoint looks like ca.smallstep.com or ca.smallstep.com/1.0/sign\n\t\t\tif u.Path != \"\" {\n\t\t\t\tparts := strings.SplitN(u.Path, \"/\", 2)\n\t\t\t\tu.Host = parts[0]\n\t\t\t\tif len(parts) == 2 {\n\t\t\t\t\tu.Path = parts[1]\n\t\t\t\t} else {\n\t\t\t\t\tu.Path = \"\"\n\t\t\t\t}\n\t\t\t\treturn parseEndpoint(u.String())\n\t\t\t}\n\t\t\treturn nil, errors.Errorf(\"error parsing endpoint: url '%s' is not valid\", endpoint)\n\t\t}\n\t\treturn u, nil\n\t}\n\t// scheme:opaque[?query][#fragment]\n\t// endpoint looks like ca.smallstep.com:443 or ca.smallstep.com:443/1.0/sign\n\treturn parseEndpoint(\"https://\" + endpoint)\n}\n\n// ProvisionerOption is the type of options passed to the Provisioner method.\ntype ProvisionerOption func(o *ProvisionerOptions) error\n\n// ProvisionerOptions stores options for the provisioner CRUD API.\ntype ProvisionerOptions struct {\n\tCursor string\n\tLimit  int\n\tID     string\n\tName   string\n}\n\n// Apply caches provisioner options on a struct for later use.\nfunc (o *ProvisionerOptions) Apply(opts []ProvisionerOption) (err error) {\n\tfor _, fn := range opts {\n\t\tif err = fn(o); err != nil {\n\t\t\treturn\n\t\t}\n\t}\n\treturn\n}\n\nfunc (o *ProvisionerOptions) rawQuery() string {\n\tv := url.Values{}\n\tif o.Cursor != \"\" {\n\t\tv.Set(\"cursor\", o.Cursor)\n\t}\n\tif o.Limit > 0 {\n\t\tv.Set(\"limit\", strconv.Itoa(o.Limit))\n\t}\n\tif o.ID != \"\" {\n\t\tv.Set(\"id\", o.ID)\n\t}\n\tif o.Name != \"\" {\n\t\tv.Set(\"name\", o.Name)\n\t}\n\treturn v.Encode()\n}\n\n// WithProvisionerCursor will request the provisioners starting with the given cursor.\nfunc WithProvisionerCursor(cursor string) ProvisionerOption {\n\treturn func(o *ProvisionerOptions) error {\n\t\to.Cursor = cursor\n\t\treturn nil\n\t}\n}\n\n// WithProvisionerLimit will request the given number of provisioners.\nfunc WithProvisionerLimit(limit int) ProvisionerOption {\n\treturn func(o *ProvisionerOptions) error {\n\t\to.Limit = limit\n\t\treturn nil\n\t}\n}\n\n// WithProvisionerID will request the given provisioner.\nfunc WithProvisionerID(id string) ProvisionerOption {\n\treturn func(o *ProvisionerOptions) error {\n\t\to.ID = id\n\t\treturn nil\n\t}\n}\n\n// WithProvisionerName will request the given provisioner.\nfunc WithProvisionerName(name string) ProvisionerOption {\n\treturn func(o *ProvisionerOptions) error {\n\t\to.Name = name\n\t\treturn nil\n\t}\n}\n\n// Client implements an HTTP client for the CA server.\ntype Client struct {\n\tclient             *uaClient\n\tendpoint           *url.URL\n\tretryFunc          RetryFunc\n\ttimeout            time.Duration\n\topts               []ClientOption\n\ttransportDecorator TransportDecorator\n}\n\n// NewClient creates a new Client with the given endpoint and options.\nfunc NewClient(endpoint string, opts ...ClientOption) (*Client, error) {\n\tu, err := parseEndpoint(endpoint)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Retrieve transport from options.\n\to := defaultClientOptions()\n\tif err := o.apply(opts); err != nil {\n\t\treturn nil, err\n\t}\n\ttr, err := o.getTransport(endpoint)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &Client{\n\t\tclient:             newClient(tr, o.timeout),\n\t\tendpoint:           u,\n\t\tretryFunc:          o.retryFunc,\n\t\ttimeout:            o.timeout,\n\t\topts:               opts,\n\t\ttransportDecorator: o.transportDecorator,\n\t}, nil\n}\n\nfunc (c *Client) retryOnError(r *http.Response) bool {\n\tif c.retryFunc != nil {\n\t\tif c.retryFunc(r.StatusCode) {\n\t\t\to := defaultClientOptions()\n\t\t\tif err := o.apply(c.opts); err != nil {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\ttr, err := o.getTransport(c.endpoint.String())\n\t\t\tif err != nil {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tr.Body.Close()\n\t\t\tc.client.SetTransport(tr)\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// GetCaURL returns the configured CA url.\nfunc (c *Client) GetCaURL() string {\n\treturn c.endpoint.String()\n}\n\n// GetRootCAs returns the RootCAs certificate pool from the configured\n// transport.\nfunc (c *Client) GetRootCAs() *x509.CertPool {\n\tswitch t := c.client.GetTransport().(type) {\n\tcase *http.Transport:\n\t\tif t.TLSClientConfig != nil {\n\t\t\treturn t.TLSClientConfig.RootCAs\n\t\t}\n\t\treturn nil\n\tcase *http2.Transport:\n\t\tif t.TLSClientConfig != nil {\n\t\t\treturn t.TLSClientConfig.RootCAs\n\t\t}\n\t\treturn nil\n\tdefault:\n\t\treturn nil\n\t}\n}\n\n// SetTransport updates the transport of the internal HTTP client.\nfunc (c *Client) SetTransport(tr http.RoundTripper) {\n\tc.client.SetTransport(tr)\n}\n\n// CloseIdleConnections closes any connections on its Transport which were\n// previously connected from previous requests but are now sitting idle in a\n// \"keep-alive\" state. It does not interrupt any connections currently in use.\nfunc (c *Client) CloseIdleConnections() {\n\tc.client.CloseIdleConnections()\n}\n\n// Version performs the version request to the CA with an empty context and returns the\n// api.VersionResponse struct.\nfunc (c *Client) Version() (*api.VersionResponse, error) {\n\treturn c.VersionWithContext(context.Background())\n}\n\n// VersionWithContext performs the version request to the CA with the provided context\n// and returns the api.VersionResponse struct.\nfunc (c *Client) VersionWithContext(ctx context.Context) (*api.VersionResponse, error) {\n\tvar retried bool\n\tu := c.endpoint.ResolveReference(&url.URL{Path: \"/version\"})\nretry:\n\tresp, err := c.client.GetWithContext(ctx, u.String())\n\tif err != nil {\n\t\treturn nil, clientError(err)\n\t}\n\tif resp.StatusCode >= 400 {\n\t\tif !retried && c.retryOnError(resp) { //nolint:contextcheck // deeply nested context; retry using the same context\n\t\t\tretried = true\n\t\t\tgoto retry\n\t\t}\n\t\treturn nil, readError(resp)\n\t}\n\tvar version api.VersionResponse\n\tif err := readJSON(resp.Body, &version); err != nil {\n\t\treturn nil, errs.Wrapf(http.StatusInternalServerError, err, \"client.Version; error reading %s\", u)\n\t}\n\treturn &version, nil\n}\n\n// Health performs the health request to the CA with an empty context\n// and returns the api.HealthResponse struct.\nfunc (c *Client) Health() (*api.HealthResponse, error) {\n\treturn c.HealthWithContext(context.Background())\n}\n\n// HealthWithContext performs the health request to the CA with the provided context\n// and returns the api.HealthResponse struct.\nfunc (c *Client) HealthWithContext(ctx context.Context) (*api.HealthResponse, error) {\n\tvar retried bool\n\tu := c.endpoint.ResolveReference(&url.URL{Path: \"/health\"})\nretry:\n\tresp, err := c.client.GetWithContext(ctx, u.String())\n\tif err != nil {\n\t\treturn nil, clientError(err)\n\t}\n\tif resp.StatusCode >= 400 {\n\t\tif !retried && c.retryOnError(resp) { //nolint:contextcheck // deeply nested context; retry using the same context\n\t\t\tretried = true\n\t\t\tgoto retry\n\t\t}\n\t\treturn nil, readError(resp)\n\t}\n\tvar health api.HealthResponse\n\tif err := readJSON(resp.Body, &health); err != nil {\n\t\treturn nil, errs.Wrapf(http.StatusInternalServerError, err, \"client.Health; error reading %s\", u)\n\t}\n\treturn &health, nil\n}\n\n// Root performs the root request to the CA with an empty context and the provided\n// SHA256 and returns the api.RootResponse struct. It uses an insecure client, but\n// it checks the resulting root certificate with the given SHA256, returning an error\n// if they do not match.\nfunc (c *Client) Root(sha256Sum string) (*api.RootResponse, error) {\n\treturn c.RootWithContext(context.Background(), sha256Sum)\n}\n\n// RootWithContext performs the root request to the CA with an empty context and the provided\n// SHA256 and returns the api.RootResponse struct. It uses an insecure client, but\n// it checks the resulting root certificate with the given SHA256, returning an error\n// if they do not match.\nfunc (c *Client) RootWithContext(ctx context.Context, sha256Sum string) (*api.RootResponse, error) {\n\tvar retried bool\n\tsha256Sum = strings.ToLower(strings.ReplaceAll(sha256Sum, \"-\", \"\"))\n\tu := c.endpoint.ResolveReference(&url.URL{Path: \"/root/\" + sha256Sum})\nretry:\n\tresp, err := newInsecureClient().GetWithContext(ctx, u.String())\n\tif err != nil {\n\t\treturn nil, clientError(err)\n\t}\n\tif resp.StatusCode >= 400 {\n\t\tif !retried && c.retryOnError(resp) { //nolint:contextcheck // deeply nested context; retry using the same context\n\t\t\tretried = true\n\t\t\tgoto retry\n\t\t}\n\t\treturn nil, readError(resp)\n\t}\n\tvar root api.RootResponse\n\tif err := readJSON(resp.Body, &root); err != nil {\n\t\treturn nil, errs.Wrapf(http.StatusInternalServerError, err, \"client.Root; error reading %s\", u)\n\t}\n\t// verify the sha256\n\tsum := sha256.Sum256(root.RootPEM.Raw)\n\tif !strings.EqualFold(sha256Sum, strings.ToLower(hex.EncodeToString(sum[:]))) {\n\t\treturn nil, errs.BadRequest(\"root certificate fingerprint does not match\")\n\t}\n\treturn &root, nil\n}\n\n// Sign performs the sign request to the CA with an empty context and returns\n// the api.SignResponse struct.\nfunc (c *Client) Sign(req *api.SignRequest) (*api.SignResponse, error) {\n\treturn c.SignWithContext(context.Background(), req)\n}\n\n// SignWithContext performs the sign request to the CA with the provided context\n// and returns the api.SignResponse struct.\nfunc (c *Client) SignWithContext(ctx context.Context, req *api.SignRequest) (*api.SignResponse, error) {\n\tvar retried bool\n\tbody, err := json.Marshal(req)\n\tif err != nil {\n\t\treturn nil, errs.Wrap(http.StatusInternalServerError, err, \"client.Sign; error marshaling request\")\n\t}\n\tu := c.endpoint.ResolveReference(&url.URL{Path: \"/sign\"})\nretry:\n\tresp, err := c.client.PostWithContext(ctx, u.String(), \"application/json\", bytes.NewReader(body))\n\tif err != nil {\n\t\treturn nil, clientError(err)\n\t}\n\tif resp.StatusCode >= 400 {\n\t\tif !retried && c.retryOnError(resp) { //nolint:contextcheck // deeply nested context; retry using the same context\n\t\t\tretried = true\n\t\t\tgoto retry\n\t\t}\n\t\treturn nil, readError(resp)\n\t}\n\tvar sign api.SignResponse\n\tif err := readJSON(resp.Body, &sign); err != nil {\n\t\treturn nil, errs.Wrapf(http.StatusInternalServerError, err, \"client.Sign; error reading %s\", u)\n\t}\n\t// Add tls.ConnectionState:\n\t// We'll extract the root certificate from the verified chains\n\tsign.TLS = resp.TLS\n\treturn &sign, nil\n}\n\n// Renew performs the renew request to the CA with an empty context and\n// returns the api.SignResponse struct.\nfunc (c *Client) Renew(tr http.RoundTripper) (*api.SignResponse, error) {\n\treturn c.RenewWithContext(context.Background(), tr)\n}\n\n// RenewWithContext performs the renew request to the CA with the provided context\n// and returns the api.SignResponse struct.\nfunc (c *Client) RenewWithContext(ctx context.Context, tr http.RoundTripper) (*api.SignResponse, error) {\n\tvar retried bool\n\tu := c.endpoint.ResolveReference(&url.URL{Path: \"/renew\"})\n\thttpClient := &http.Client{Transport: tr}\nretry:\n\treq, err := http.NewRequestWithContext(ctx, \"POST\", u.String(), http.NoBody)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\tresp, err := httpClient.Do(req)\n\tif err != nil {\n\t\treturn nil, clientError(err)\n\t}\n\tif resp.StatusCode >= 400 {\n\t\tif !retried && c.retryOnError(resp) { //nolint:contextcheck // deeply nested context; retry using the same context\n\t\t\tretried = true\n\t\t\tgoto retry\n\t\t}\n\t\treturn nil, readError(resp)\n\t}\n\tvar sign api.SignResponse\n\tif err := readJSON(resp.Body, &sign); err != nil {\n\t\treturn nil, errs.Wrapf(http.StatusInternalServerError, err, \"client.Renew; error reading %s\", u)\n\t}\n\treturn &sign, nil\n}\n\n// RenewWithToken performs the renew request to the CA with the given\n// authorization token and and empty context and returns the api.SignResponse struct.\n// This method is generally used to renew an expired certificate.\nfunc (c *Client) RenewWithToken(token string) (*api.SignResponse, error) {\n\treturn c.RenewWithTokenAndContext(context.Background(), token)\n}\n\n// RenewWithTokenAndContext performs the renew request to the CA with the given\n// authorization token and context and returns the api.SignResponse struct.\n// This method is generally used to renew an expired certificate.\nfunc (c *Client) RenewWithTokenAndContext(ctx context.Context, token string) (*api.SignResponse, error) {\n\tvar retried bool\n\tu := c.endpoint.ResolveReference(&url.URL{Path: \"/renew\"})\n\treq, err := http.NewRequestWithContext(ctx, \"POST\", u.String(), http.NoBody)\n\tif err != nil {\n\t\treturn nil, errors.Wrapf(err, \"create POST %s request failed\", u)\n\t}\n\treq.Header.Add(\"Authorization\", \"Bearer \"+token)\nretry:\n\tresp, err := c.client.Do(req)\n\tif err != nil {\n\t\treturn nil, clientError(err)\n\t}\n\tif resp.StatusCode >= 400 {\n\t\tif !retried && c.retryOnError(resp) { //nolint:contextcheck // deeply nested context; retry using the same context\n\t\t\tretried = true\n\t\t\tgoto retry\n\t\t}\n\t\treturn nil, readError(resp)\n\t}\n\tvar sign api.SignResponse\n\tif err := readJSON(resp.Body, &sign); err != nil {\n\t\treturn nil, errs.Wrapf(http.StatusInternalServerError, err, \"client.RenewWithToken; error reading %s\", u)\n\t}\n\treturn &sign, nil\n}\n\n// Rekey performs the rekey request to the CA with an empty context and\n// returns the api.SignResponse struct.\nfunc (c *Client) Rekey(req *api.RekeyRequest, tr http.RoundTripper) (*api.SignResponse, error) {\n\treturn c.RekeyWithContext(context.Background(), req, tr)\n}\n\n// RekeyWithContext performs the rekey request to the CA with the provided context\n// and returns the api.SignResponse struct.\nfunc (c *Client) RekeyWithContext(ctx context.Context, req *api.RekeyRequest, tr http.RoundTripper) (*api.SignResponse, error) {\n\tvar retried bool\n\tbody, err := json.Marshal(req)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"error marshaling request\")\n\t}\n\tu := c.endpoint.ResolveReference(&url.URL{Path: \"/rekey\"})\n\thttpClient := &http.Client{Transport: tr}\nretry:\n\thttpReq, err := http.NewRequestWithContext(ctx, \"POST\", u.String(), bytes.NewReader(body))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\thttpReq.Header.Set(\"Content-Type\", \"application/json\")\n\tresp, err := httpClient.Do(httpReq)\n\tif err != nil {\n\t\treturn nil, clientError(err)\n\t}\n\tif resp.StatusCode >= 400 {\n\t\tif !retried && c.retryOnError(resp) { //nolint:contextcheck // deeply nested context; retry using the same context\n\t\t\tretried = true\n\t\t\tgoto retry\n\t\t}\n\t\treturn nil, readError(resp)\n\t}\n\tvar sign api.SignResponse\n\tif err := readJSON(resp.Body, &sign); err != nil {\n\t\treturn nil, errs.Wrapf(http.StatusInternalServerError, err, \"client.Rekey; error reading %s\", u)\n\t}\n\treturn &sign, nil\n}\n\n// Revoke performs the revoke request to the CA with an empty context and returns\n// the api.RevokeResponse struct.\nfunc (c *Client) Revoke(req *api.RevokeRequest, tr http.RoundTripper) (*api.RevokeResponse, error) {\n\treturn c.RevokeWithContext(context.Background(), req, tr)\n}\n\n// RevokeWithContext performs the revoke request to the CA with the provided context and\n// returns the api.RevokeResponse struct.\nfunc (c *Client) RevokeWithContext(ctx context.Context, req *api.RevokeRequest, tr http.RoundTripper) (*api.RevokeResponse, error) {\n\tvar retried bool\n\tbody, err := json.Marshal(req)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"error marshaling request\")\n\t}\n\tvar uaClient *uaClient\nretry:\n\tif tr != nil {\n\t\tuaClient = newClient(tr, c.timeout)\n\t} else {\n\t\tuaClient = c.client\n\t}\n\n\tu := c.endpoint.ResolveReference(&url.URL{Path: \"/revoke\"})\n\tresp, err := uaClient.PostWithContext(ctx, u.String(), \"application/json\", bytes.NewReader(body))\n\tif err != nil {\n\t\treturn nil, clientError(err)\n\t}\n\tif resp.StatusCode >= 400 {\n\t\tif !retried && c.retryOnError(resp) { //nolint:contextcheck // deeply nested context; retry using the same context\n\t\t\tretried = true\n\t\t\tgoto retry\n\t\t}\n\t\treturn nil, readError(resp)\n\t}\n\tvar revoke api.RevokeResponse\n\tif err := readJSON(resp.Body, &revoke); err != nil {\n\t\treturn nil, errors.Wrapf(err, \"error reading %s\", u)\n\t}\n\treturn &revoke, nil\n}\n\n// Provisioners performs the provisioners request to the CA with an empty context\n// and returns the api.ProvisionersResponse struct with a map of provisioners.\n//\n// ProvisionerOption WithProvisionerCursor and WithProvisionLimit can be used to\n// paginate the provisioners.\nfunc (c *Client) Provisioners(opts ...ProvisionerOption) (*api.ProvisionersResponse, error) {\n\treturn c.ProvisionersWithContext(context.Background(), opts...)\n}\n\n// ProvisionersWithContext performs the provisioners request to the CA with the provided context\n// and returns the api.ProvisionersResponse struct with a map of provisioners.\n//\n// ProvisionerOption WithProvisionerCursor and WithProvisionLimit can be used to\n// paginate the provisioners.\nfunc (c *Client) ProvisionersWithContext(ctx context.Context, opts ...ProvisionerOption) (*api.ProvisionersResponse, error) {\n\tvar retried bool\n\to := new(ProvisionerOptions)\n\tif err := o.Apply(opts); err != nil {\n\t\treturn nil, err\n\t}\n\tu := c.endpoint.ResolveReference(&url.URL{\n\t\tPath:     \"/provisioners\",\n\t\tRawQuery: o.rawQuery(),\n\t})\nretry:\n\tresp, err := c.client.GetWithContext(ctx, u.String())\n\tif err != nil {\n\t\treturn nil, clientError(err)\n\t}\n\tif resp.StatusCode >= 400 {\n\t\tif !retried && c.retryOnError(resp) { //nolint:contextcheck // deeply nested context; retry using the same context\n\t\t\tretried = true\n\t\t\tgoto retry\n\t\t}\n\t\treturn nil, readError(resp)\n\t}\n\tvar provisioners api.ProvisionersResponse\n\tif err := readJSON(resp.Body, &provisioners); err != nil {\n\t\treturn nil, errors.Wrapf(err, \"error reading %s\", u)\n\t}\n\treturn &provisioners, nil\n}\n\n// ProvisionerKey performs the request to the CA with an empty context to get\n// the encrypted key for the given provisioner kid and returns the api.ProvisionerKeyResponse\n// struct with the encrypted key.\nfunc (c *Client) ProvisionerKey(kid string) (*api.ProvisionerKeyResponse, error) {\n\treturn c.ProvisionerKeyWithContext(context.Background(), kid)\n}\n\n// ProvisionerKeyWithContext performs the request to the CA with the provided context to get\n// the encrypted key for the given provisioner kid and returns the api.ProvisionerKeyResponse\n// struct with the encrypted key.\nfunc (c *Client) ProvisionerKeyWithContext(ctx context.Context, kid string) (*api.ProvisionerKeyResponse, error) {\n\tvar retried bool\n\tu := c.endpoint.ResolveReference(&url.URL{Path: \"/provisioners/\" + kid + \"/encrypted-key\"})\nretry:\n\tresp, err := c.client.GetWithContext(ctx, u.String())\n\tif err != nil {\n\t\treturn nil, clientError(err)\n\t}\n\tif resp.StatusCode >= 400 {\n\t\tif !retried && c.retryOnError(resp) { //nolint:contextcheck // deeply nested context; retry using the same context\n\t\t\tretried = true\n\t\t\tgoto retry\n\t\t}\n\t\treturn nil, readError(resp)\n\t}\n\tvar key api.ProvisionerKeyResponse\n\tif err := readJSON(resp.Body, &key); err != nil {\n\t\treturn nil, errors.Wrapf(err, \"error reading %s\", u)\n\t}\n\treturn &key, nil\n}\n\n// Roots performs the get roots request to the CA with an empty context\n// and returns the api.RootsResponse struct.\nfunc (c *Client) Roots() (*api.RootsResponse, error) {\n\treturn c.RootsWithContext(context.Background())\n}\n\n// RootsWithContext performs the get roots request to the CA with the provided context\n// and returns the api.RootsResponse struct.\nfunc (c *Client) RootsWithContext(ctx context.Context) (*api.RootsResponse, error) {\n\tvar retried bool\n\tu := c.endpoint.ResolveReference(&url.URL{Path: \"/roots\"})\nretry:\n\tresp, err := c.client.GetWithContext(ctx, u.String())\n\tif err != nil {\n\t\treturn nil, clientError(err)\n\t}\n\tif resp.StatusCode >= 400 {\n\t\tif !retried && c.retryOnError(resp) { //nolint:contextcheck // deeply nested context; retry using the same context\n\t\t\tretried = true\n\t\t\tgoto retry\n\t\t}\n\t\treturn nil, readError(resp)\n\t}\n\tvar roots api.RootsResponse\n\tif err := readJSON(resp.Body, &roots); err != nil {\n\t\treturn nil, errors.Wrapf(err, \"error reading %s\", u)\n\t}\n\treturn &roots, nil\n}\n\n// Federation performs the get federation request to the CA with an empty context\n// and returns the api.FederationResponse struct.\nfunc (c *Client) Federation() (*api.FederationResponse, error) {\n\treturn c.FederationWithContext(context.Background())\n}\n\n// FederationWithContext performs the get federation request to the CA with the provided context\n// and returns the api.FederationResponse struct.\nfunc (c *Client) FederationWithContext(ctx context.Context) (*api.FederationResponse, error) {\n\tvar retried bool\n\tu := c.endpoint.ResolveReference(&url.URL{Path: \"/federation\"})\nretry:\n\tresp, err := c.client.GetWithContext(ctx, u.String())\n\tif err != nil {\n\t\treturn nil, clientError(err)\n\t}\n\tif resp.StatusCode >= 400 {\n\t\tif !retried && c.retryOnError(resp) { //nolint:contextcheck // deeply nested context; retry using the same context\n\t\t\tretried = true\n\t\t\tgoto retry\n\t\t}\n\t\treturn nil, readError(resp)\n\t}\n\tvar federation api.FederationResponse\n\tif err := readJSON(resp.Body, &federation); err != nil {\n\t\treturn nil, errors.Wrapf(err, \"error reading %s\", u)\n\t}\n\treturn &federation, nil\n}\n\n// SSHSign performs the POST /ssh/sign request to the CA with an empty context\n// and returns the api.SSHSignResponse struct.\nfunc (c *Client) SSHSign(req *api.SSHSignRequest) (*api.SSHSignResponse, error) {\n\treturn c.SSHSignWithContext(context.Background(), req)\n}\n\n// SSHSignWithContext performs the POST /ssh/sign request to the CA with the provided context\n// and returns the api.SSHSignResponse struct.\nfunc (c *Client) SSHSignWithContext(ctx context.Context, req *api.SSHSignRequest) (*api.SSHSignResponse, error) {\n\tvar retried bool\n\tbody, err := json.Marshal(req)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"error marshaling request\")\n\t}\n\tu := c.endpoint.ResolveReference(&url.URL{Path: \"/ssh/sign\"})\nretry:\n\tresp, err := c.client.PostWithContext(ctx, u.String(), \"application/json\", bytes.NewReader(body))\n\tif err != nil {\n\t\treturn nil, clientError(err)\n\t}\n\tif resp.StatusCode >= 400 {\n\t\tif !retried && c.retryOnError(resp) { //nolint:contextcheck // deeply nested context; retry using the same context\n\t\t\tretried = true\n\t\t\tgoto retry\n\t\t}\n\t\treturn nil, readError(resp)\n\t}\n\tvar sign api.SSHSignResponse\n\tif err := readJSON(resp.Body, &sign); err != nil {\n\t\treturn nil, errors.Wrapf(err, \"error reading %s\", u)\n\t}\n\treturn &sign, nil\n}\n\n// SSHRenew performs the POST /ssh/renew request to the CA with an empty context\n// and returns the api.SSHRenewResponse struct.\nfunc (c *Client) SSHRenew(req *api.SSHRenewRequest) (*api.SSHRenewResponse, error) {\n\treturn c.SSHRenewWithContext(context.Background(), req)\n}\n\n// SSHRenewWithContext performs the POST /ssh/renew request to the CA with the provided context\n// and returns the api.SSHRenewResponse struct.\nfunc (c *Client) SSHRenewWithContext(ctx context.Context, req *api.SSHRenewRequest) (*api.SSHRenewResponse, error) {\n\tvar retried bool\n\tbody, err := json.Marshal(req)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"error marshaling request\")\n\t}\n\tu := c.endpoint.ResolveReference(&url.URL{Path: \"/ssh/renew\"})\nretry:\n\tresp, err := c.client.PostWithContext(ctx, u.String(), \"application/json\", bytes.NewReader(body))\n\tif err != nil {\n\t\treturn nil, clientError(err)\n\t}\n\tif resp.StatusCode >= 400 {\n\t\tif !retried && c.retryOnError(resp) { //nolint:contextcheck // deeply nested context; retry using the same context\n\t\t\tretried = true\n\t\t\tgoto retry\n\t\t}\n\t\treturn nil, readError(resp)\n\t}\n\tvar renew api.SSHRenewResponse\n\tif err := readJSON(resp.Body, &renew); err != nil {\n\t\treturn nil, errors.Wrapf(err, \"error reading %s\", u)\n\t}\n\treturn &renew, nil\n}\n\n// SSHRekey performs the POST /ssh/rekey request to the CA with an empty context\n// and returns the api.SSHRekeyResponse struct.\nfunc (c *Client) SSHRekey(req *api.SSHRekeyRequest) (*api.SSHRekeyResponse, error) {\n\treturn c.SSHRekeyWithContext(context.Background(), req)\n}\n\n// SSHRekeyWithContext performs the POST /ssh/rekey request to the CA with the provided context\n// and returns the api.SSHRekeyResponse struct.\nfunc (c *Client) SSHRekeyWithContext(ctx context.Context, req *api.SSHRekeyRequest) (*api.SSHRekeyResponse, error) {\n\tvar retried bool\n\tbody, err := json.Marshal(req)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"error marshaling request\")\n\t}\n\tu := c.endpoint.ResolveReference(&url.URL{Path: \"/ssh/rekey\"})\nretry:\n\tresp, err := c.client.PostWithContext(ctx, u.String(), \"application/json\", bytes.NewReader(body))\n\tif err != nil {\n\t\treturn nil, clientError(err)\n\t}\n\tif resp.StatusCode >= 400 {\n\t\tif !retried && c.retryOnError(resp) { //nolint:contextcheck // deeply nested context; retry using the same context\n\t\t\tretried = true\n\t\t\tgoto retry\n\t\t}\n\t\treturn nil, readError(resp)\n\t}\n\tvar rekey api.SSHRekeyResponse\n\tif err := readJSON(resp.Body, &rekey); err != nil {\n\t\treturn nil, errors.Wrapf(err, \"error reading %s\", u)\n\t}\n\treturn &rekey, nil\n}\n\n// SSHRevoke performs the POST /ssh/revoke request to the CA with an empty context\n// and returns the api.SSHRevokeResponse struct.\nfunc (c *Client) SSHRevoke(req *api.SSHRevokeRequest) (*api.SSHRevokeResponse, error) {\n\treturn c.SSHRevokeWithContext(context.Background(), req)\n}\n\n// SSHRevokeWithContext performs the POST /ssh/revoke request to the CA with the provided context\n// and returns the api.SSHRevokeResponse struct.\nfunc (c *Client) SSHRevokeWithContext(ctx context.Context, req *api.SSHRevokeRequest) (*api.SSHRevokeResponse, error) {\n\tvar retried bool\n\tbody, err := json.Marshal(req)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"error marshaling request\")\n\t}\n\tu := c.endpoint.ResolveReference(&url.URL{Path: \"/ssh/revoke\"})\nretry:\n\tresp, err := c.client.PostWithContext(ctx, u.String(), \"application/json\", bytes.NewReader(body))\n\tif err != nil {\n\t\treturn nil, clientError(err)\n\t}\n\tif resp.StatusCode >= 400 {\n\t\tif !retried && c.retryOnError(resp) { //nolint:contextcheck // deeply nested context; retry using the same context\n\t\t\tretried = true\n\t\t\tgoto retry\n\t\t}\n\t\treturn nil, readError(resp)\n\t}\n\tvar revoke api.SSHRevokeResponse\n\tif err := readJSON(resp.Body, &revoke); err != nil {\n\t\treturn nil, errors.Wrapf(err, \"error reading %s\", u)\n\t}\n\treturn &revoke, nil\n}\n\n// SSHRoots performs the GET /ssh/roots request to the CA with an empty context\n// and returns the api.SSHRootsResponse struct.\nfunc (c *Client) SSHRoots() (*api.SSHRootsResponse, error) {\n\treturn c.SSHRootsWithContext(context.Background())\n}\n\n// SSHRootsWithContext performs the GET /ssh/roots request to the CA with the provided context\n// and returns the api.SSHRootsResponse struct.\nfunc (c *Client) SSHRootsWithContext(ctx context.Context) (*api.SSHRootsResponse, error) {\n\tvar retried bool\n\tu := c.endpoint.ResolveReference(&url.URL{Path: \"/ssh/roots\"})\nretry:\n\tresp, err := c.client.GetWithContext(ctx, u.String())\n\tif err != nil {\n\t\treturn nil, clientError(err)\n\t}\n\tif resp.StatusCode >= 400 {\n\t\tif !retried && c.retryOnError(resp) { //nolint:contextcheck // deeply nested context; retry using the same context\n\t\t\tretried = true\n\t\t\tgoto retry\n\t\t}\n\t\treturn nil, readError(resp)\n\t}\n\tvar keys api.SSHRootsResponse\n\tif err := readJSON(resp.Body, &keys); err != nil {\n\t\treturn nil, errors.Wrapf(err, \"error reading %s\", u)\n\t}\n\treturn &keys, nil\n}\n\n// SSHFederation performs the get /ssh/federation request to the CA with an empty context\n// and returns the api.SSHRootsResponse struct.\nfunc (c *Client) SSHFederation() (*api.SSHRootsResponse, error) {\n\treturn c.SSHFederationWithContext(context.Background())\n}\n\n// SSHFederationWithContext performs the get /ssh/federation request to the CA with the provided context\n// and returns the api.SSHRootsResponse struct.\nfunc (c *Client) SSHFederationWithContext(ctx context.Context) (*api.SSHRootsResponse, error) {\n\tvar retried bool\n\tu := c.endpoint.ResolveReference(&url.URL{Path: \"/ssh/federation\"})\nretry:\n\tresp, err := c.client.GetWithContext(ctx, u.String())\n\tif err != nil {\n\t\treturn nil, clientError(err)\n\t}\n\tif resp.StatusCode >= 400 {\n\t\tif !retried && c.retryOnError(resp) { //nolint:contextcheck // deeply nested context; retry using the same context\n\t\t\tretried = true\n\t\t\tgoto retry\n\t\t}\n\t\treturn nil, readError(resp)\n\t}\n\tvar keys api.SSHRootsResponse\n\tif err := readJSON(resp.Body, &keys); err != nil {\n\t\treturn nil, errors.Wrapf(err, \"error reading %s\", u)\n\t}\n\treturn &keys, nil\n}\n\n// SSHConfig performs the POST /ssh/config request to the CA with an empty context\n// to get the ssh configuration templates.\nfunc (c *Client) SSHConfig(req *api.SSHConfigRequest) (*api.SSHConfigResponse, error) {\n\treturn c.SSHConfigWithContext(context.Background(), req)\n}\n\n// SSHConfigWithContext performs the POST /ssh/config request to the CA with the provided context\n// to get the ssh configuration templates.\nfunc (c *Client) SSHConfigWithContext(ctx context.Context, req *api.SSHConfigRequest) (*api.SSHConfigResponse, error) {\n\tvar retried bool\n\tbody, err := json.Marshal(req)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"error marshaling request\")\n\t}\n\tu := c.endpoint.ResolveReference(&url.URL{Path: \"/ssh/config\"})\nretry:\n\tresp, err := c.client.PostWithContext(ctx, u.String(), \"application/json\", bytes.NewReader(body))\n\tif err != nil {\n\t\treturn nil, clientError(err)\n\t}\n\tif resp.StatusCode >= 400 {\n\t\tif !retried && c.retryOnError(resp) { //nolint:contextcheck // deeply nested context; retry using the same context\n\t\t\tretried = true\n\t\t\tgoto retry\n\t\t}\n\t\treturn nil, readError(resp)\n\t}\n\tvar cfg api.SSHConfigResponse\n\tif err := readJSON(resp.Body, &cfg); err != nil {\n\t\treturn nil, errors.Wrapf(err, \"error reading %s\", u)\n\t}\n\treturn &cfg, nil\n}\n\n// SSHCheckHost performs the POST /ssh/check-host request to the CA with an empty context,\n// the principal and a token and returns the api.SSHCheckPrincipalResponse.\nfunc (c *Client) SSHCheckHost(principal, token string) (*api.SSHCheckPrincipalResponse, error) {\n\treturn c.SSHCheckHostWithContext(context.Background(), principal, token)\n}\n\n// SSHCheckHostWithContext performs the POST /ssh/check-host request to the CA with the provided context,\n// principal and token and returns the api.SSHCheckPrincipalResponse.\nfunc (c *Client) SSHCheckHostWithContext(ctx context.Context, principal, token string) (*api.SSHCheckPrincipalResponse, error) {\n\tvar retried bool\n\tbody, err := json.Marshal(&api.SSHCheckPrincipalRequest{\n\t\tType:      provisioner.SSHHostCert,\n\t\tPrincipal: principal,\n\t\tToken:     token,\n\t})\n\tif err != nil {\n\t\treturn nil, errs.Wrap(http.StatusInternalServerError, err, \"error marshaling request\",\n\t\t\terrs.WithMessage(\"Failed to marshal the check-host request\"))\n\t}\n\tu := c.endpoint.ResolveReference(&url.URL{Path: \"/ssh/check-host\"})\nretry:\n\tresp, err := c.client.PostWithContext(ctx, u.String(), \"application/json\", bytes.NewReader(body))\n\tif err != nil {\n\t\treturn nil, clientError(err)\n\t}\n\tif resp.StatusCode >= 400 {\n\t\tif !retried && c.retryOnError(resp) { //nolint:contextcheck // deeply nested context; retry using the same context\n\t\t\tretried = true\n\t\t\tgoto retry\n\t\t}\n\t\treturn nil, readError(resp)\n\t}\n\tvar check api.SSHCheckPrincipalResponse\n\tif err := readJSON(resp.Body, &check); err != nil {\n\t\treturn nil, errs.Wrapf(http.StatusInternalServerError, err, \"error reading %s response\",\n\t\t\t[]any{u, errs.WithMessage(\"Failed to parse response from /ssh/check-host endpoint\")}...)\n\t}\n\treturn &check, nil\n}\n\n// SSHGetHosts performs the GET /ssh/get-hosts request to the CA with an empty context.\nfunc (c *Client) SSHGetHosts() (*api.SSHGetHostsResponse, error) {\n\treturn c.SSHGetHostsWithContext(context.Background())\n}\n\n// SSHGetHostsWithContext performs the GET /ssh/get-hosts request to the CA with the provided context.\nfunc (c *Client) SSHGetHostsWithContext(ctx context.Context) (*api.SSHGetHostsResponse, error) {\n\tvar retried bool\n\tu := c.endpoint.ResolveReference(&url.URL{Path: \"/ssh/hosts\"})\nretry:\n\tresp, err := c.client.GetWithContext(ctx, u.String())\n\tif err != nil {\n\t\treturn nil, clientError(err)\n\t}\n\tif resp.StatusCode >= 400 {\n\t\tif !retried && c.retryOnError(resp) { //nolint:contextcheck // deeply nested context; retry using the same context\n\t\t\tretried = true\n\t\t\tgoto retry\n\t\t}\n\t\treturn nil, readError(resp)\n\t}\n\tvar hosts api.SSHGetHostsResponse\n\tif err := readJSON(resp.Body, &hosts); err != nil {\n\t\treturn nil, errors.Wrapf(err, \"error reading %s\", u)\n\t}\n\treturn &hosts, nil\n}\n\n// SSHBastion performs the POST /ssh/bastion request to the CA with an empty context.\nfunc (c *Client) SSHBastion(req *api.SSHBastionRequest) (*api.SSHBastionResponse, error) {\n\treturn c.SSHBastionWithContext(context.Background(), req)\n}\n\n// SSHBastionWithContext performs the POST /ssh/bastion request to the CA with the provided context.\nfunc (c *Client) SSHBastionWithContext(ctx context.Context, req *api.SSHBastionRequest) (*api.SSHBastionResponse, error) {\n\tvar retried bool\n\tbody, err := json.Marshal(req)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"client.SSHBastion; error marshaling request\")\n\t}\n\tu := c.endpoint.ResolveReference(&url.URL{Path: \"/ssh/bastion\"})\nretry:\n\tresp, err := c.client.PostWithContext(ctx, u.String(), \"application/json\", bytes.NewReader(body))\n\tif err != nil {\n\t\treturn nil, clientError(err)\n\t}\n\tif resp.StatusCode >= 400 {\n\t\tif !retried && c.retryOnError(resp) { //nolint:contextcheck // deeply nested context; retry using the same context\n\t\t\tretried = true\n\t\t\tgoto retry\n\t\t}\n\t\treturn nil, readError(resp)\n\t}\n\tvar bastion api.SSHBastionResponse\n\tif err := readJSON(resp.Body, &bastion); err != nil {\n\t\treturn nil, errors.Wrapf(err, \"client.SSHBastion; error reading %s\", u)\n\t}\n\treturn &bastion, nil\n}\n\n// RootFingerprint is a helper method that returns the current root fingerprint.\n// It does an health connection and gets the fingerprint from the TLS verified chains.\nfunc (c *Client) RootFingerprint() (string, error) {\n\treturn c.RootFingerprintWithContext(context.Background())\n}\n\n// RootFingerprintWithContext is a helper method that returns the current root fingerprint.\n// It does an health connection and gets the fingerprint from the TLS verified chains.\nfunc (c *Client) RootFingerprintWithContext(ctx context.Context) (string, error) {\n\tu := c.endpoint.ResolveReference(&url.URL{Path: \"/health\"})\n\tresp, err := c.client.GetWithContext(ctx, u.String())\n\tif err != nil {\n\t\treturn \"\", clientError(err)\n\t}\n\tdefer resp.Body.Close()\n\tif resp.TLS == nil || len(resp.TLS.VerifiedChains) == 0 {\n\t\treturn \"\", errors.New(\"missing verified chains\")\n\t}\n\tlastChain := resp.TLS.VerifiedChains[len(resp.TLS.VerifiedChains)-1]\n\tif len(lastChain) == 0 {\n\t\treturn \"\", errors.New(\"missing verified chains\")\n\t}\n\treturn x509util.Fingerprint(lastChain[len(lastChain)-1]), nil\n}\n\n// CreateSignRequest is a helper function that given an x509 OTT returns a\n// simple but secure sign request as well as the private key used.\nfunc CreateSignRequest(ott string) (*api.SignRequest, crypto.PrivateKey, error) {\n\ttoken, err := jose.ParseSigned(ott)\n\tif err != nil {\n\t\treturn nil, nil, errors.Wrap(err, \"error parsing ott\")\n\t}\n\tvar claims authority.Claims\n\tif err := token.UnsafeClaimsWithoutVerification(&claims); err != nil {\n\t\treturn nil, nil, errors.Wrap(err, \"error parsing ott\")\n\t}\n\n\tpk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)\n\tif err != nil {\n\t\treturn nil, nil, errors.Wrap(err, \"error generating key\")\n\t}\n\n\tdnsNames, ips, emails, uris := x509util.SplitSANs(claims.SANs)\n\tif claims.Email != \"\" {\n\t\temails = append(emails, claims.Email)\n\t}\n\n\ttemplate := &x509.CertificateRequest{\n\t\tSubject: pkix.Name{\n\t\t\tCommonName: claims.Subject,\n\t\t},\n\t\tSignatureAlgorithm: x509.ECDSAWithSHA256,\n\t\tDNSNames:           dnsNames,\n\t\tIPAddresses:        ips,\n\t\tEmailAddresses:     emails,\n\t\tURIs:               uris,\n\t}\n\n\tcsr, err := x509.CreateCertificateRequest(rand.Reader, template, pk)\n\tif err != nil {\n\t\treturn nil, nil, errors.Wrap(err, \"error creating certificate request\")\n\t}\n\tcr, err := x509.ParseCertificateRequest(csr)\n\tif err != nil {\n\t\treturn nil, nil, errors.Wrap(err, \"error parsing certificate request\")\n\t}\n\tif err := cr.CheckSignature(); err != nil {\n\t\treturn nil, nil, errors.Wrap(err, \"error signing certificate request\")\n\t}\n\treturn &api.SignRequest{\n\t\tCsrPEM: api.CertificateRequest{CertificateRequest: cr},\n\t\tOTT:    ott,\n\t}, pk, nil\n}\n\n// CreateCertificateRequest creates a new CSR with the given common name and\n// SANs. If no san is provided the commonName will set also a SAN.\nfunc CreateCertificateRequest(commonName string, sans ...string) (*api.CertificateRequest, crypto.PrivateKey, error) {\n\tkey, err := keyutil.GenerateDefaultKey()\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\treturn createCertificateRequest(commonName, sans, key)\n}\n\n// CreateIdentityRequest returns a new CSR to create the identity. If an\n// identity was already present it reuses the private key.\nfunc CreateIdentityRequest(commonName string, sans ...string) (*api.CertificateRequest, crypto.PrivateKey, error) {\n\tvar identityKey crypto.PrivateKey\n\tif i, err := identity.LoadDefaultIdentity(); err == nil && i.Key != \"\" {\n\t\tif k, err := pemutil.Read(i.Key); err == nil {\n\t\t\tidentityKey = k\n\t\t}\n\t}\n\tif identityKey == nil {\n\t\treturn CreateCertificateRequest(commonName, sans...)\n\t}\n\treturn createCertificateRequest(commonName, sans, identityKey)\n}\n\n// LoadDefaultIdentity is a wrapper for identity.LoadDefaultIdentity.\nfunc LoadDefaultIdentity() (*identity.Identity, error) {\n\treturn identity.LoadDefaultIdentity()\n}\n\n// WriteDefaultIdentity is a wrapper for identity.WriteDefaultIdentity.\nfunc WriteDefaultIdentity(certChain []api.Certificate, key crypto.PrivateKey) error {\n\treturn identity.WriteDefaultIdentity(certChain, key)\n}\n\nfunc createCertificateRequest(commonName string, sans []string, key crypto.PrivateKey) (*api.CertificateRequest, crypto.PrivateKey, error) {\n\tif len(sans) == 0 {\n\t\tsans = []string{commonName}\n\t}\n\tdnsNames, ips, emails, uris := x509util.SplitSANs(sans)\n\ttemplate := &x509.CertificateRequest{\n\t\tSubject: pkix.Name{\n\t\t\tCommonName: commonName,\n\t\t},\n\t\tDNSNames:       dnsNames,\n\t\tIPAddresses:    ips,\n\t\tEmailAddresses: emails,\n\t\tURIs:           uris,\n\t}\n\tcsr, err := x509.CreateCertificateRequest(rand.Reader, template, key)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\tcr, err := x509.ParseCertificateRequest(csr)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\tif err := cr.CheckSignature(); err != nil {\n\t\treturn nil, nil, err\n\t}\n\n\treturn &api.CertificateRequest{CertificateRequest: cr}, key, nil\n}\n\n// getRootCAPath returns the path where the root CA is stored based on the\n// STEPPATH environment variable.\nfunc getRootCAPath() string {\n\treturn filepath.Join(step.Path(), \"certs\", \"root_ca.crt\")\n}\n\nfunc readJSON(r io.ReadCloser, v interface{}) error {\n\tdefer r.Close()\n\treturn json.NewDecoder(r).Decode(v)\n}\n\nfunc readProtoJSON(r io.ReadCloser, m proto.Message) error {\n\tdefer r.Close()\n\tdata, err := io.ReadAll(r)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn protojson.Unmarshal(data, m)\n}\n\nfunc readError(r *http.Response) error {\n\tdefer r.Body.Close()\n\tapiErr := new(errs.Error)\n\tif err := json.NewDecoder(r.Body).Decode(apiErr); err != nil {\n\t\treturn fmt.Errorf(\"failed decoding CA error response: %w\", err)\n\t}\n\tapiErr.RequestID = r.Header.Get(\"X-Request-Id\")\n\treturn apiErr\n}\n\nfunc clientError(err error) error {\n\tvar uerr *url.Error\n\tif errors.As(err, &uerr) {\n\t\treturn fmt.Errorf(\"client %s %s failed: %w\",\n\t\t\tstrings.ToUpper(uerr.Op), uerr.URL, uerr.Err)\n\t}\n\treturn fmt.Errorf(\"client request failed: %w\", err)\n}\n\nfunc decorateRoundTripper(tr http.RoundTripper, td TransportDecorator) http.RoundTripper {\n\tif td != nil {\n\t\treturn td(tr)\n\t}\n\treturn tr\n}\n"
  },
  {
    "path": "ca/client_test.go",
    "content": "package ca\n\nimport (\n\t\"bytes\"\n\t\"crypto/ecdsa\"\n\t\"crypto/elliptic\"\n\t\"crypto/rand\"\n\t\"crypto/x509\"\n\t\"encoding/json\"\n\t\"encoding/pem\"\n\t\"errors\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"reflect\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/google/uuid\"\n\t\"github.com/smallstep/certificates/api\"\n\t\"github.com/smallstep/certificates/api/read\"\n\t\"github.com/smallstep/certificates/api/render\"\n\t\"github.com/smallstep/certificates/authority\"\n\t\"github.com/smallstep/certificates/authority/provisioner\"\n\t\"github.com/smallstep/certificates/ca/client\"\n\t\"github.com/smallstep/certificates/errs\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"go.step.sm/crypto/x509util\"\n\t\"golang.org/x/crypto/ssh\"\n)\n\nconst (\n\trootPEM = `-----BEGIN CERTIFICATE-----\nMIIEBDCCAuygAwIBAgIDAjppMA0GCSqGSIb3DQEBBQUAMEIxCzAJBgNVBAYTAlVT\nMRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMRswGQYDVQQDExJHZW9UcnVzdCBHbG9i\nYWwgQ0EwHhcNMTMwNDA1MTUxNTU1WhcNMTUwNDA0MTUxNTU1WjBJMQswCQYDVQQG\nEwJVUzETMBEGA1UEChMKR29vZ2xlIEluYzElMCMGA1UEAxMcR29vZ2xlIEludGVy\nbmV0IEF1dGhvcml0eSBHMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB\nAJwqBHdc2FCROgajguDYUEi8iT/xGXAaiEZ+4I/F8YnOIe5a/mENtzJEiaB0C1NP\nVaTOgmKV7utZX8bhBYASxF6UP7xbSDj0U/ck5vuR6RXEz/RTDfRK/J9U3n2+oGtv\nh8DQUB8oMANA2ghzUWx//zo8pzcGjr1LEQTrfSTe5vn8MXH7lNVg8y5Kr0LSy+rE\nahqyzFPdFUuLH8gZYR/Nnag+YyuENWllhMgZxUYi+FOVvuOAShDGKuy6lyARxzmZ\nEASg8GF6lSWMTlJ14rbtCMoU/M4iarNOz0YDl5cDfsCx3nuvRTPPuj5xt970JSXC\nDTWJnZ37DhF5iR43xa+OcmkCAwEAAaOB+zCB+DAfBgNVHSMEGDAWgBTAephojYn7\nqwVkDBF9qn1luMrMTjAdBgNVHQ4EFgQUSt0GFhu89mi1dvWBtrtiGrpagS8wEgYD\nVR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAQYwOgYDVR0fBDMwMTAvoC2g\nK4YpaHR0cDovL2NybC5nZW90cnVzdC5jb20vY3Jscy9ndGdsb2JhbC5jcmwwPQYI\nKwYBBQUHAQEEMTAvMC0GCCsGAQUFBzABhiFodHRwOi8vZ3RnbG9iYWwtb2NzcC5n\nZW90cnVzdC5jb20wFwYDVR0gBBAwDjAMBgorBgEEAdZ5AgUBMA0GCSqGSIb3DQEB\nBQUAA4IBAQA21waAESetKhSbOHezI6B1WLuxfoNCunLaHtiONgaX4PCVOzf9G0JY\n/iLIa704XtE7JW4S615ndkZAkNoUyHgN7ZVm2o6Gb4ChulYylYbc3GrKBIxbf/a/\nzG+FA1jDaFETzf3I93k9mTXwVqO94FntT0QJo544evZG0R0SnU++0ED8Vf4GXjza\nHFa9llF7b1cq26KqltyMdMKVvvBulRP/F/A8rLIQjcxz++iPAsbw+zOzlTvjwsto\nWHPbqCRiOwY1nQ2pM714A5AuTHhdUDqB1O6gyHA43LL5Z/qHQF1hwFGPa4NrzQU6\nyuGnBXj8ytqU0CwIPX4WecigUCAkVDNx\n-----END CERTIFICATE-----`\n\n\tcertPEM = `-----BEGIN CERTIFICATE-----\nMIIDujCCAqKgAwIBAgIIE31FZVaPXTUwDQYJKoZIhvcNAQEFBQAwSTELMAkGA1UE\nBhMCVVMxEzARBgNVBAoTCkdvb2dsZSBJbmMxJTAjBgNVBAMTHEdvb2dsZSBJbnRl\ncm5ldCBBdXRob3JpdHkgRzIwHhcNMTQwMTI5MTMyNzQzWhcNMTQwNTI5MDAwMDAw\nWjBpMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwN\nTW91bnRhaW4gVmlldzETMBEGA1UECgwKR29vZ2xlIEluYzEYMBYGA1UEAwwPbWFp\nbC5nb29nbGUuY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEfRrObuSW5T7q\n5CnSEqefEmtH4CCv6+5EckuriNr1CjfVvqzwfAhopXkLrq45EQm8vkmf7W96XJhC\n7ZM0dYi1/qOCAU8wggFLMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAa\nBgNVHREEEzARgg9tYWlsLmdvb2dsZS5jb20wCwYDVR0PBAQDAgeAMGgGCCsGAQUF\nBwEBBFwwWjArBggrBgEFBQcwAoYfaHR0cDovL3BraS5nb29nbGUuY29tL0dJQUcy\nLmNydDArBggrBgEFBQcwAYYfaHR0cDovL2NsaWVudHMxLmdvb2dsZS5jb20vb2Nz\ncDAdBgNVHQ4EFgQUiJxtimAuTfwb+aUtBn5UYKreKvMwDAYDVR0TAQH/BAIwADAf\nBgNVHSMEGDAWgBRK3QYWG7z2aLV29YG2u2IaulqBLzAXBgNVHSAEEDAOMAwGCisG\nAQQB1nkCBQEwMAYDVR0fBCkwJzAloCOgIYYfaHR0cDovL3BraS5nb29nbGUuY29t\nL0dJQUcyLmNybDANBgkqhkiG9w0BAQUFAAOCAQEAH6RYHxHdcGpMpFE3oxDoFnP+\ngtuBCHan2yE2GRbJ2Cw8Lw0MmuKqHlf9RSeYfd3BXeKkj1qO6TVKwCh+0HdZk283\nTZZyzmEOyclm3UGFYe82P/iDFt+CeQ3NpmBg+GoaVCuWAARJN/KfglbLyyYygcQq\n0SgeDh8dRKUiaW3HQSoYvTvdTuqzwK4CXsr3b5/dAOY8uMuG/IAR3FgwTbZ1dtoW\nRvOTa8hYiU6A475WuZKyEHcwnGYe57u2I2KbMgcKjPniocj4QzgYsVAVKW3IwaOh\nyE+vPxsiUkvQHdO2fojCkY8jg70jxM+gu59tPDNbw3Uh/2Ij310FgTHsnGQMyA==\n-----END CERTIFICATE-----`\n\n\tcsrPEM = `-----BEGIN CERTIFICATE REQUEST-----\nMIIEYjCCAkoCAQAwHTEbMBkGA1UEAxMSdGVzdC5zbWFsbHN0ZXAuY29tMIICIjAN\nBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAuCpifZfoZhYNywfpnPa21NezXgtn\nwrWBFE6xhVzE7YDSIqtIsj8aR7R8zwEymxfv5j5298LUy/XSmItVH31CsKyfcGqN\nQM0PZr9XY3z5V6qchGMqjzt/jqlYMBHujcxIFBfz4HATxSgKyvHqvw14ESsS2huu\n7jowx+XTKbFYgKcXrjBkvOej5FXD3ehkg0jDA2UAJNdfKmrc1BBEaaqOtfh7eyU2\nHU7+5gxH8C27IiCAmNj719E0B99Nu2MUw6aLFIM4xAcRga33Avevx6UuXZZIEepe\nV1sihrkcnDK9Vsxkme5erXzvAoOiRusiC2iIomJHJrdRM5ReEU+N+Tl1Kxq+rk7H\n/qAq78wVm07M1/GGi9SUMObZS4WuJpM6whlikIAEbv9iV+CK0sv/Jr/AADdGMmQU\nlwk+Q0ZNE8p4ZuWILv/dtLDtDVBpnrrJ9e8duBtB0lGcG8MdaUCQ346EI4T0Sgx0\nhJ+wMq8zYYFfPIZEHC8o9p1ywWN9ySpJ8Zj/5ubmx9v2bY67GbuVFEa8iAp+S00x\n/Z8nD6/JsoKtexuHyGr3ixWFzlBqXDuugukIDFUOVDCbuGw4Io4/hEMu4Zz0TIFk\nUu/wf2z75Tt8EkosKLu2wieKcY7n7Vhog/0tqexqWlWtJH0tvq4djsGoSvA62WPs\n0iXXj+aZIARPNhECAwEAAaAAMA0GCSqGSIb3DQEBCwUAA4ICAQA0vyHIndAkIs/I\nNnz5yZWCokRjokoKv3Aj4VilyjncL+W0UIPULLU/47ZyoHVSUj2t8gknr9xu/Kd+\ng/2z0RiF3CIp8IUH49w/HYWaR95glzVNAAzr8qD9UbUqloLVQW3lObSRGtezhdZO\nsspw5dC+inhAb1LZhx8PVxB3SAeJ8h11IEBr0s2Hxt9viKKd7YPtIFZkZdOkVx4R\nif1DMawj1P6fEomf8z7m+dmbUYTqqosbCbRL01mzEga/kF6JyH/OzpNlcsAiyM8e\nBxPWH6TtPqwmyy4y7j1outmM0RnyUw5A0HmIbWh+rHpXiHVsnNqse0XfzmaxM8+z\ndxYeDax8aMWZKfvY1Zew+xIxl7DtEy1BpxrZcawumJYt5+LL+bwF/OtL0inQLnw8\nzyqydsXNdrpIQJnfmWPld7ThWbQw2FBE70+nFSxHeG2ULnpF3M9xf6ZNAF4gqaNE\nQ7vMNPBWrJWu+A++vHY61WGET+h4lY3GFr2I8OE4IiHPQi1D7Y0+fwOmStwuRPM4\n2rARcJChNdiYBkkuvs4kixKTTjdXhB8RQtuBSrJ0M1tzq2qMbm7F8G01rOg4KlXU\n58jHzJwr1K7cx0lpWfGTtc5bseCGtTKmDBXTziw04yl8eE1+ZFOganixGwCtl4Tt\nDCbKzWTW8lqVdp9Kyf7XEhhc2R8C5w==\n-----END CERTIFICATE REQUEST-----`\n)\n\nfunc mustKey(t *testing.T) *ecdsa.PrivateKey {\n\tt.Helper()\n\tpriv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)\n\trequire.NoError(t, err)\n\treturn priv\n}\n\nfunc parseCertificate(t *testing.T, data string) *x509.Certificate {\n\tt.Helper()\n\tblock, _ := pem.Decode([]byte(data))\n\tif block == nil {\n\t\trequire.Fail(t, \"failed to parse certificate PEM\")\n\t\treturn nil\n\t}\n\tcert, err := x509.ParseCertificate(block.Bytes)\n\trequire.NoError(t, err, \"failed to parse certificate\")\n\treturn cert\n}\n\nfunc parseCertificateRequest(t *testing.T, csrPEM string) *x509.CertificateRequest {\n\tt.Helper()\n\tblock, _ := pem.Decode([]byte(csrPEM))\n\tif block == nil {\n\t\trequire.Fail(t, \"failed to parse certificate request PEM\")\n\t\treturn nil\n\t}\n\tcsr, err := x509.ParseCertificateRequest(block.Bytes)\n\trequire.NoError(t, err, \"failed to parse certificate request\")\n\treturn csr\n}\n\nfunc equalJSON(t *testing.T, a, b interface{}) bool {\n\tt.Helper()\n\tif reflect.DeepEqual(a, b) {\n\t\treturn true\n\t}\n\n\tab, err := json.Marshal(a)\n\trequire.NoError(t, err)\n\n\tbb, err := json.Marshal(b)\n\trequire.NoError(t, err)\n\n\treturn bytes.Equal(ab, bb)\n}\n\nfunc TestClient_Version(t *testing.T) {\n\tok := &api.VersionResponse{Version: \"test\"}\n\n\ttests := []struct {\n\t\tname         string\n\t\tresponse     interface{}\n\t\tresponseCode int\n\t\twantErr      bool\n\t\texpectedErr  error\n\t}{\n\t\t{\"ok\", ok, 200, false, nil},\n\t\t{\"500\", errs.InternalServer(\"force\"), 500, true, errors.New(errs.InternalServerErrorDefaultMsg)},\n\t\t{\"404\", errs.NotFound(\"force\"), 404, true, errors.New(errs.NotFoundDefaultMsg)},\n\t}\n\n\tsrv := httptest.NewServer(nil)\n\tdefer srv.Close()\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tc, err := NewClient(srv.URL, WithTransport(http.DefaultTransport))\n\t\t\trequire.NoError(t, err)\n\n\t\t\tsrv.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\trender.JSONStatus(w, r, tt.response, tt.responseCode)\n\t\t\t})\n\n\t\t\tgot, err := c.Version()\n\t\t\tif tt.wantErr {\n\t\t\t\tif assert.Error(t, err) {\n\t\t\t\t\tassert.EqualError(t, err, tt.expectedErr.Error())\n\t\t\t\t}\n\t\t\t\tassert.Nil(t, got)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, tt.response, got)\n\t\t})\n\t}\n}\n\nfunc TestClient_Health(t *testing.T) {\n\tok := &api.HealthResponse{Status: \"ok\"}\n\n\ttests := []struct {\n\t\tname         string\n\t\tresponse     interface{}\n\t\tresponseCode int\n\t\twantErr      bool\n\t\texpectedErr  error\n\t}{\n\t\t{\"ok\", ok, 200, false, nil},\n\t\t{\"not ok\", errs.InternalServer(\"force\"), 500, true, errors.New(errs.InternalServerErrorDefaultMsg)},\n\t}\n\n\tsrv := httptest.NewServer(nil)\n\tdefer srv.Close()\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tc, err := NewClient(srv.URL, WithTransport(http.DefaultTransport))\n\t\t\trequire.NoError(t, err)\n\n\t\t\tsrv.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\trender.JSONStatus(w, r, tt.response, tt.responseCode)\n\t\t\t})\n\n\t\t\tgot, err := c.Health()\n\t\t\tif tt.wantErr {\n\t\t\t\tif assert.Error(t, err) {\n\t\t\t\t\tassert.EqualError(t, err, tt.expectedErr.Error())\n\t\t\t\t}\n\t\t\t\tassert.Nil(t, got)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, tt.response, got)\n\t\t})\n\t}\n}\n\nfunc TestClient_Root(t *testing.T) {\n\tok := &api.RootResponse{\n\t\tRootPEM: api.Certificate{Certificate: parseCertificate(t, rootPEM)},\n\t}\n\n\ttests := []struct {\n\t\tname         string\n\t\tshasum       string\n\t\tresponse     interface{}\n\t\tresponseCode int\n\t\twantErr      bool\n\t\texpectedErr  error\n\t}{\n\t\t{\"ok\", \"a047a37fa2d2e118a4f5095fe074d6cfe0e352425a7632bf8659c03919a6c81d\", ok, 200, false, nil},\n\t\t{\"not found\", \"invalid\", errs.NotFound(\"force\"), 404, true, errors.New(errs.NotFoundDefaultMsg)},\n\t}\n\n\tsrv := httptest.NewServer(nil)\n\tdefer srv.Close()\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tc, err := NewClient(srv.URL, WithTransport(http.DefaultTransport))\n\t\t\trequire.NoError(t, err)\n\n\t\t\tsrv.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\texpected := \"/root/\" + tt.shasum\n\t\t\t\tif r.RequestURI != expected {\n\t\t\t\t\tt.Errorf(\"RequestURI = %s, want %s\", r.RequestURI, expected)\n\t\t\t\t}\n\t\t\t\trender.JSONStatus(w, r, tt.response, tt.responseCode)\n\t\t\t})\n\n\t\t\tgot, err := c.Root(tt.shasum)\n\t\t\tif tt.wantErr {\n\t\t\t\tif assert.Error(t, err) {\n\t\t\t\t\tassert.EqualError(t, err, tt.expectedErr.Error())\n\t\t\t\t}\n\t\t\t\tassert.Nil(t, got)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, tt.response, got)\n\t\t})\n\t}\n}\n\nfunc TestClient_Sign(t *testing.T) {\n\tok := &api.SignResponse{\n\t\tServerPEM: api.Certificate{Certificate: parseCertificate(t, certPEM)},\n\t\tCaPEM:     api.Certificate{Certificate: parseCertificate(t, rootPEM)},\n\t\tCertChainPEM: []api.Certificate{\n\t\t\t{Certificate: parseCertificate(t, certPEM)},\n\t\t\t{Certificate: parseCertificate(t, rootPEM)},\n\t\t},\n\t}\n\trequest := &api.SignRequest{\n\t\tCsrPEM:    api.CertificateRequest{CertificateRequest: parseCertificateRequest(t, csrPEM)},\n\t\tOTT:       \"the-ott\",\n\t\tNotBefore: api.NewTimeDuration(time.Now()),\n\t\tNotAfter:  api.NewTimeDuration(time.Now().AddDate(0, 1, 0)),\n\t}\n\n\ttests := []struct {\n\t\tname         string\n\t\trequest      *api.SignRequest\n\t\tresponse     interface{}\n\t\tresponseCode int\n\t\twantErr      bool\n\t\texpectedErr  error\n\t}{\n\t\t{\"ok\", request, ok, 200, false, nil},\n\t\t{\"unauthorized\", request, errs.Unauthorized(\"force\"), 401, true, errors.New(errs.UnauthorizedDefaultMsg)},\n\t\t{\"empty request\", &api.SignRequest{}, errs.BadRequest(\"force\"), 400, true, errors.New(errs.BadRequestPrefix + \"force.\")},\n\t\t{\"nil request\", nil, errs.BadRequest(\"force\"), 400, true, errors.New(errs.BadRequestPrefix + \"force.\")},\n\t}\n\n\tsrv := httptest.NewServer(nil)\n\tdefer srv.Close()\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tc, err := NewClient(srv.URL, WithTransport(http.DefaultTransport))\n\t\t\trequire.NoError(t, err)\n\n\t\t\tsrv.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\tbody := new(api.SignRequest)\n\t\t\t\tif err := read.JSON(r.Body, body); err != nil {\n\t\t\t\t\te, ok := tt.response.(error)\n\t\t\t\t\trequire.True(t, ok, \"response expected to be error type\")\n\t\t\t\t\trender.Error(w, r, e)\n\t\t\t\t\treturn\n\t\t\t\t} else if !equalJSON(t, body, tt.request) {\n\t\t\t\t\tif tt.request == nil {\n\t\t\t\t\t\tif !reflect.DeepEqual(body, &api.SignRequest{}) {\n\t\t\t\t\t\t\tt.Errorf(\"Client.Sign() request = %v, wants %v\", body, tt.request)\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\tt.Errorf(\"Client.Sign() request = %v, wants %v\", body, tt.request)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\trender.JSONStatus(w, r, tt.response, tt.responseCode)\n\t\t\t})\n\n\t\t\tgot, err := c.Sign(tt.request)\n\t\t\tif tt.wantErr {\n\t\t\t\tif assert.Error(t, err) {\n\t\t\t\t\tassert.EqualError(t, err, tt.expectedErr.Error())\n\t\t\t\t}\n\t\t\t\tassert.Nil(t, got)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, tt.response, got)\n\t\t})\n\t}\n}\n\nfunc TestClient_Revoke(t *testing.T) {\n\tok := &api.RevokeResponse{Status: \"ok\"}\n\trequest := &api.RevokeRequest{\n\t\tSerial:     \"sn\",\n\t\tOTT:        \"the-ott\",\n\t\tReasonCode: 4,\n\t}\n\ttests := []struct {\n\t\tname         string\n\t\trequest      *api.RevokeRequest\n\t\tresponse     interface{}\n\t\tresponseCode int\n\t\twantErr      bool\n\t\texpectedErr  error\n\t}{\n\t\t{\"ok\", request, ok, 200, false, nil},\n\t\t{\"unauthorized\", request, errs.Unauthorized(\"force\"), 401, true, errors.New(errs.UnauthorizedDefaultMsg)},\n\t\t{\"nil request\", nil, errs.BadRequest(\"force\"), 400, true, errors.New(errs.BadRequestPrefix)},\n\t}\n\n\tsrv := httptest.NewServer(nil)\n\tdefer srv.Close()\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tc, err := NewClient(srv.URL, WithTransport(http.DefaultTransport))\n\t\t\trequire.NoError(t, err)\n\n\t\t\tsrv.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\tbody := new(api.RevokeRequest)\n\t\t\t\tif err := read.JSON(r.Body, body); err != nil {\n\t\t\t\t\te, ok := tt.response.(error)\n\t\t\t\t\trequire.True(t, ok, \"response expected to be error type\")\n\t\t\t\t\trender.Error(w, r, e)\n\t\t\t\t\treturn\n\t\t\t\t} else if !equalJSON(t, body, tt.request) {\n\t\t\t\t\tif tt.request == nil {\n\t\t\t\t\t\tif !reflect.DeepEqual(body, &api.RevokeRequest{}) {\n\t\t\t\t\t\t\tt.Errorf(\"Client.Revoke() request = %v, wants %v\", body, tt.request)\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\tt.Errorf(\"Client.Revoke() request = %v, wants %v\", body, tt.request)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\trender.JSONStatus(w, r, tt.response, tt.responseCode)\n\t\t\t})\n\n\t\t\tgot, err := c.Revoke(tt.request, nil)\n\t\t\tif tt.wantErr {\n\t\t\t\tif assert.Error(t, err) {\n\t\t\t\t\tassert.True(t, strings.HasPrefix(err.Error(), tt.expectedErr.Error()))\n\t\t\t\t}\n\t\t\t\tassert.Nil(t, got)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, tt.response, got)\n\t\t})\n\t}\n}\n\nfunc TestClient_Renew(t *testing.T) {\n\tok := &api.SignResponse{\n\t\tServerPEM: api.Certificate{Certificate: parseCertificate(t, certPEM)},\n\t\tCaPEM:     api.Certificate{Certificate: parseCertificate(t, rootPEM)},\n\t\tCertChainPEM: []api.Certificate{\n\t\t\t{Certificate: parseCertificate(t, certPEM)},\n\t\t\t{Certificate: parseCertificate(t, rootPEM)},\n\t\t},\n\t}\n\n\ttests := []struct {\n\t\tname         string\n\t\tresponse     interface{}\n\t\tresponseCode int\n\t\twantErr      bool\n\t\terr          error\n\t}{\n\t\t{\"ok\", ok, 200, false, nil},\n\t\t{\"unauthorized\", errs.Unauthorized(\"force\"), 401, true, errors.New(errs.UnauthorizedDefaultMsg)},\n\t\t{\"empty request\", errs.BadRequest(\"force\"), 400, true, errors.New(errs.BadRequestPrefix)},\n\t\t{\"nil request\", errs.BadRequest(\"force\"), 400, true, errors.New(errs.BadRequestPrefix)},\n\t}\n\n\tsrv := httptest.NewServer(nil)\n\tdefer srv.Close()\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tc, err := NewClient(srv.URL, WithTransport(http.DefaultTransport))\n\t\t\trequire.NoError(t, err)\n\n\t\t\tsrv.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\trender.JSONStatus(w, r, tt.response, tt.responseCode)\n\t\t\t})\n\n\t\t\tgot, err := c.Renew(nil)\n\t\t\tif tt.wantErr {\n\t\t\t\tif assert.Error(t, err) {\n\t\t\t\t\tvar sc render.StatusCodedError\n\t\t\t\t\tif assert.ErrorAs(t, err, &sc) {\n\t\t\t\t\t\tassert.Equal(t, tt.responseCode, sc.StatusCode())\n\t\t\t\t\t}\n\t\t\t\t\tassert.True(t, strings.HasPrefix(err.Error(), tt.err.Error()))\n\t\t\t\t}\n\t\t\t\tassert.Nil(t, got)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, tt.response, got)\n\t\t})\n\t}\n}\n\nfunc TestClient_RenewWithToken(t *testing.T) {\n\tok := &api.SignResponse{\n\t\tServerPEM: api.Certificate{Certificate: parseCertificate(t, certPEM)},\n\t\tCaPEM:     api.Certificate{Certificate: parseCertificate(t, rootPEM)},\n\t\tCertChainPEM: []api.Certificate{\n\t\t\t{Certificate: parseCertificate(t, certPEM)},\n\t\t\t{Certificate: parseCertificate(t, rootPEM)},\n\t\t},\n\t}\n\n\ttests := []struct {\n\t\tname         string\n\t\tresponse     interface{}\n\t\tresponseCode int\n\t\twantErr      bool\n\t\terr          error\n\t}{\n\t\t{\"ok\", ok, 200, false, nil},\n\t\t{\"unauthorized\", errs.Unauthorized(\"force\"), 401, true, errors.New(errs.UnauthorizedDefaultMsg)},\n\t\t{\"empty request\", errs.BadRequest(\"force\"), 400, true, errors.New(errs.BadRequestPrefix)},\n\t\t{\"nil request\", errs.BadRequest(\"force\"), 400, true, errors.New(errs.BadRequestPrefix)},\n\t}\n\n\tsrv := httptest.NewServer(nil)\n\tdefer srv.Close()\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tc, err := NewClient(srv.URL, WithTransport(http.DefaultTransport))\n\t\t\trequire.NoError(t, err)\n\n\t\t\tsrv.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\tif r.Header.Get(\"Authorization\") != \"Bearer token\" {\n\t\t\t\t\trender.JSONStatus(w, r, errs.InternalServer(\"force\"), 500)\n\t\t\t\t} else {\n\t\t\t\t\trender.JSONStatus(w, r, tt.response, tt.responseCode)\n\t\t\t\t}\n\t\t\t})\n\n\t\t\tgot, err := c.RenewWithToken(\"token\")\n\t\t\tif tt.wantErr {\n\t\t\t\tif assert.Error(t, err) {\n\t\t\t\t\tvar sc render.StatusCodedError\n\t\t\t\t\tif assert.ErrorAs(t, err, &sc) {\n\t\t\t\t\t\tassert.Equal(t, tt.responseCode, sc.StatusCode())\n\t\t\t\t\t}\n\t\t\t\t\tassert.True(t, strings.HasPrefix(err.Error(), tt.err.Error()))\n\t\t\t\t}\n\t\t\t\tassert.Nil(t, got)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, tt.response, got)\n\t\t})\n\t}\n}\n\nfunc TestClient_Rekey(t *testing.T) {\n\tok := &api.SignResponse{\n\t\tServerPEM: api.Certificate{Certificate: parseCertificate(t, certPEM)},\n\t\tCaPEM:     api.Certificate{Certificate: parseCertificate(t, rootPEM)},\n\t\tCertChainPEM: []api.Certificate{\n\t\t\t{Certificate: parseCertificate(t, certPEM)},\n\t\t\t{Certificate: parseCertificate(t, rootPEM)},\n\t\t},\n\t}\n\n\trequest := &api.RekeyRequest{\n\t\tCsrPEM: api.CertificateRequest{CertificateRequest: parseCertificateRequest(t, csrPEM)},\n\t}\n\n\ttests := []struct {\n\t\tname         string\n\t\trequest      *api.RekeyRequest\n\t\tresponse     interface{}\n\t\tresponseCode int\n\t\twantErr      bool\n\t\terr          error\n\t}{\n\t\t{\"ok\", request, ok, 200, false, nil},\n\t\t{\"unauthorized\", request, errs.Unauthorized(\"force\"), 401, true, errors.New(errs.UnauthorizedDefaultMsg)},\n\t\t{\"empty request\", &api.RekeyRequest{}, errs.BadRequest(\"force\"), 400, true, errors.New(errs.BadRequestPrefix)},\n\t\t{\"nil request\", nil, errs.BadRequest(\"force\"), 400, true, errors.New(errs.BadRequestPrefix)},\n\t}\n\n\tsrv := httptest.NewServer(nil)\n\tdefer srv.Close()\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tc, err := NewClient(srv.URL, WithTransport(http.DefaultTransport))\n\t\t\trequire.NoError(t, err)\n\n\t\t\tsrv.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\trender.JSONStatus(w, r, tt.response, tt.responseCode)\n\t\t\t})\n\n\t\t\tgot, err := c.Rekey(tt.request, nil)\n\t\t\tif tt.wantErr {\n\t\t\t\tif assert.Error(t, err) {\n\t\t\t\t\tvar sc render.StatusCodedError\n\t\t\t\t\tif assert.ErrorAs(t, err, &sc) {\n\t\t\t\t\t\tassert.Equal(t, tt.responseCode, sc.StatusCode())\n\t\t\t\t\t}\n\t\t\t\t\tassert.True(t, strings.HasPrefix(err.Error(), tt.err.Error()))\n\t\t\t\t}\n\t\t\t\tassert.Nil(t, got)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, tt.response, got)\n\t\t})\n\t}\n}\n\nfunc TestClient_Provisioners(t *testing.T) {\n\tok := &api.ProvisionersResponse{\n\t\tProvisioners: provisioner.List{},\n\t}\n\tinternalServerError := errs.InternalServer(\"Internal Server Error\")\n\n\ttests := []struct {\n\t\tname         string\n\t\targs         []ProvisionerOption\n\t\texpectedURI  string\n\t\tresponse     interface{}\n\t\tresponseCode int\n\t\twantErr      bool\n\t}{\n\t\t{\"ok\", nil, \"/provisioners\", ok, 200, false},\n\t\t{\"ok with cursor\", []ProvisionerOption{WithProvisionerCursor(\"abc\")}, \"/provisioners?cursor=abc\", ok, 200, false},\n\t\t{\"ok with limit\", []ProvisionerOption{WithProvisionerLimit(10)}, \"/provisioners?limit=10\", ok, 200, false},\n\t\t{\"ok with cursor+limit\", []ProvisionerOption{WithProvisionerCursor(\"abc\"), WithProvisionerLimit(10)}, \"/provisioners?cursor=abc&limit=10\", ok, 200, false},\n\t\t{\"fail\", nil, \"/provisioners\", internalServerError, 500, true},\n\t}\n\n\tsrv := httptest.NewServer(nil)\n\tdefer srv.Close()\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tc, err := NewClient(srv.URL, WithTransport(http.DefaultTransport))\n\t\t\trequire.NoError(t, err)\n\n\t\t\tsrv.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\tif r.RequestURI != tt.expectedURI {\n\t\t\t\t\tt.Errorf(\"RequestURI = %s, want %s\", r.RequestURI, tt.expectedURI)\n\t\t\t\t}\n\t\t\t\trender.JSONStatus(w, r, tt.response, tt.responseCode)\n\t\t\t})\n\n\t\t\tgot, err := c.Provisioners(tt.args...)\n\t\t\tif tt.wantErr {\n\t\t\t\tif assert.Error(t, err) {\n\t\t\t\t\tassert.True(t, strings.HasPrefix(err.Error(), errs.InternalServerErrorDefaultMsg))\n\t\t\t\t}\n\t\t\t\tassert.Nil(t, got)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, tt.response, got)\n\t\t})\n\t}\n}\n\nfunc TestClient_ProvisionerKey(t *testing.T) {\n\tok := &api.ProvisionerKeyResponse{\n\t\tKey: \"an encrypted key\",\n\t}\n\n\ttests := []struct {\n\t\tname         string\n\t\tkid          string\n\t\tresponse     interface{}\n\t\tresponseCode int\n\t\twantErr      bool\n\t\terr          error\n\t}{\n\t\t{\"ok\", \"kid\", ok, 200, false, nil},\n\t\t{\"fail\", \"invalid\", errs.NotFound(\"force\"), 404, true, errors.New(errs.NotFoundDefaultMsg)},\n\t}\n\n\tsrv := httptest.NewServer(nil)\n\tdefer srv.Close()\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tc, err := NewClient(srv.URL, WithTransport(http.DefaultTransport))\n\t\t\trequire.NoError(t, err)\n\n\t\t\tsrv.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\texpected := \"/provisioners/\" + tt.kid + \"/encrypted-key\"\n\t\t\t\tif r.RequestURI != expected {\n\t\t\t\t\tt.Errorf(\"RequestURI = %s, want %s\", r.RequestURI, expected)\n\t\t\t\t}\n\t\t\t\trender.JSONStatus(w, r, tt.response, tt.responseCode)\n\t\t\t})\n\n\t\t\tgot, err := c.ProvisionerKey(tt.kid)\n\t\t\tif tt.wantErr {\n\t\t\t\tif assert.Error(t, err) {\n\t\t\t\t\tvar sc render.StatusCodedError\n\t\t\t\t\tif assert.ErrorAs(t, err, &sc) {\n\t\t\t\t\t\tassert.Equal(t, tt.responseCode, sc.StatusCode())\n\t\t\t\t\t}\n\t\t\t\t\tassert.True(t, strings.HasPrefix(err.Error(), tt.err.Error()))\n\t\t\t\t}\n\t\t\t\tassert.Nil(t, got)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, tt.response, got)\n\t\t})\n\t}\n}\n\nfunc TestClient_Roots(t *testing.T) {\n\tok := &api.RootsResponse{\n\t\tCertificates: []api.Certificate{\n\t\t\t{Certificate: parseCertificate(t, rootPEM)},\n\t\t},\n\t}\n\n\ttests := []struct {\n\t\tname         string\n\t\tresponse     interface{}\n\t\tresponseCode int\n\t\twantErr      bool\n\t\terr          error\n\t}{\n\t\t{\"ok\", ok, 200, false, nil},\n\t\t{\"unauthorized\", errs.Unauthorized(\"force\"), 401, true, errors.New(errs.UnauthorizedDefaultMsg)},\n\t\t{\"bad-request\", errs.BadRequest(\"force\"), 400, true, errors.New(errs.BadRequestPrefix)},\n\t}\n\n\tsrv := httptest.NewServer(nil)\n\tdefer srv.Close()\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tc, err := NewClient(srv.URL, WithTransport(http.DefaultTransport))\n\t\t\trequire.NoError(t, err)\n\n\t\t\tsrv.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\trender.JSONStatus(w, r, tt.response, tt.responseCode)\n\t\t\t})\n\n\t\t\tgot, err := c.Roots()\n\t\t\tif tt.wantErr {\n\t\t\t\tif assert.Error(t, err) {\n\t\t\t\t\tvar sc render.StatusCodedError\n\t\t\t\t\tif assert.ErrorAs(t, err, &sc) {\n\t\t\t\t\t\tassert.Equal(t, tt.responseCode, sc.StatusCode())\n\t\t\t\t\t}\n\t\t\t\t\tassert.True(t, strings.HasPrefix(err.Error(), tt.err.Error()))\n\t\t\t\t}\n\t\t\t\tassert.Nil(t, got)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, tt.response, got)\n\t\t})\n\t}\n}\n\nfunc TestClient_Federation(t *testing.T) {\n\tok := &api.FederationResponse{\n\t\tCertificates: []api.Certificate{\n\t\t\t{Certificate: parseCertificate(t, rootPEM)},\n\t\t},\n\t}\n\n\ttests := []struct {\n\t\tname         string\n\t\tresponse     interface{}\n\t\tresponseCode int\n\t\twantErr      bool\n\t\terr          error\n\t}{\n\t\t{\"ok\", ok, 200, false, nil},\n\t\t{\"unauthorized\", errs.Unauthorized(\"force\"), 401, true, errors.New(errs.UnauthorizedDefaultMsg)},\n\t}\n\n\tsrv := httptest.NewServer(nil)\n\tdefer srv.Close()\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tc, err := NewClient(srv.URL, WithTransport(http.DefaultTransport))\n\t\t\trequire.NoError(t, err)\n\n\t\t\tsrv.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\trender.JSONStatus(w, r, tt.response, tt.responseCode)\n\t\t\t})\n\n\t\t\tgot, err := c.Federation()\n\t\t\tif tt.wantErr {\n\t\t\t\tif assert.Error(t, err) {\n\t\t\t\t\tvar sc render.StatusCodedError\n\t\t\t\t\tif assert.ErrorAs(t, err, &sc) {\n\t\t\t\t\t\tassert.Equal(t, tt.responseCode, sc.StatusCode())\n\t\t\t\t\t}\n\t\t\t\t\tassert.True(t, strings.HasPrefix(err.Error(), tt.err.Error()))\n\t\t\t\t}\n\t\t\t\tassert.Nil(t, got)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, tt.response, got)\n\t\t})\n\t}\n}\n\nfunc TestClient_SSHRoots(t *testing.T) {\n\tkey, err := ssh.NewPublicKey(mustKey(t).Public())\n\trequire.NoError(t, err)\n\n\tok := &api.SSHRootsResponse{\n\t\tHostKeys: []api.SSHPublicKey{{PublicKey: key}},\n\t\tUserKeys: []api.SSHPublicKey{{PublicKey: key}},\n\t}\n\n\ttests := []struct {\n\t\tname         string\n\t\tresponse     interface{}\n\t\tresponseCode int\n\t\twantErr      bool\n\t\terr          error\n\t}{\n\t\t{\"ok\", ok, 200, false, nil},\n\t\t{\"not found\", errs.NotFound(\"force\"), 404, true, errors.New(errs.NotFoundDefaultMsg)},\n\t}\n\n\tsrv := httptest.NewServer(nil)\n\tdefer srv.Close()\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tc, err := NewClient(srv.URL, WithTransport(http.DefaultTransport))\n\t\t\trequire.NoError(t, err)\n\n\t\t\tsrv.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\trender.JSONStatus(w, r, tt.response, tt.responseCode)\n\t\t\t})\n\n\t\t\tgot, err := c.SSHRoots()\n\t\t\tif tt.wantErr {\n\t\t\t\tif assert.Error(t, err) {\n\t\t\t\t\tvar sc render.StatusCodedError\n\t\t\t\t\tif assert.ErrorAs(t, err, &sc) {\n\t\t\t\t\t\tassert.Equal(t, tt.responseCode, sc.StatusCode())\n\t\t\t\t\t}\n\t\t\t\t\tassert.True(t, strings.HasPrefix(err.Error(), tt.err.Error()))\n\t\t\t\t}\n\t\t\t\tassert.Nil(t, got)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, tt.response, got)\n\t\t})\n\t}\n}\n\nfunc Test_parseEndpoint(t *testing.T) {\n\texpected1 := &url.URL{Scheme: \"https\", Host: \"ca.smallstep.com\"}\n\texpected2 := &url.URL{Scheme: \"https\", Host: \"ca.smallstep.com\", Path: \"/1.0/sign\"}\n\ttype args struct {\n\t\tendpoint string\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\targs    args\n\t\twant    *url.URL\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", args{\"https://ca.smallstep.com\"}, expected1, false},\n\t\t{\"ok no scheme\", args{\"//ca.smallstep.com\"}, expected1, false},\n\t\t{\"ok only host\", args{\"ca.smallstep.com\"}, expected1, false},\n\t\t{\"ok no bars\", args{\"https://ca.smallstep.com\"}, expected1, false},\n\t\t{\"ok schema, host and path\", args{\"https://ca.smallstep.com/1.0/sign\"}, expected2, false},\n\t\t{\"ok no bars with path\", args{\"https://ca.smallstep.com/1.0/sign\"}, expected2, false},\n\t\t{\"ok host and path\", args{\"ca.smallstep.com/1.0/sign\"}, expected2, false},\n\t\t{\"ok host and port\", args{\"ca.smallstep.com:443\"}, &url.URL{Scheme: \"https\", Host: \"ca.smallstep.com:443\"}, false},\n\t\t{\"ok host, path and port\", args{\"ca.smallstep.com:443/1.0/sign\"}, &url.URL{Scheme: \"https\", Host: \"ca.smallstep.com:443\", Path: \"/1.0/sign\"}, false},\n\t\t{\"fail bad url\", args{\"://ca.smallstep.com\"}, nil, true},\n\t\t{\"fail no host\", args{\"https://\"}, nil, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := parseEndpoint(tt.args.endpoint)\n\t\t\tif tt.wantErr {\n\t\t\t\tassert.Error(t, err)\n\t\t\t\tassert.Nil(t, got)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, tt.want, got)\n\t\t})\n\t}\n}\n\nfunc TestClient_RootFingerprint(t *testing.T) {\n\tok := &api.HealthResponse{Status: \"ok\"}\n\tnok := errs.InternalServer(\"Internal Server Error\")\n\n\thttpsServer := httptest.NewTLSServer(nil)\n\tdefer httpsServer.Close()\n\thttpsServerFingerprint := x509util.Fingerprint(httpsServer.Certificate())\n\n\thttpServer := httptest.NewServer(nil)\n\tdefer httpServer.Close()\n\n\ttests := []struct {\n\t\tname         string\n\t\tserver       *httptest.Server\n\t\tresponse     interface{}\n\t\tresponseCode int\n\t\twant         string\n\t\twantErr      bool\n\t}{\n\t\t{\"ok\", httpsServer, ok, 200, httpsServerFingerprint, false},\n\t\t{\"ok with error\", httpsServer, nok, 500, httpsServerFingerprint, false},\n\t\t{\"fail\", httpServer, ok, 200, \"\", true},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ttr := tt.server.Client().Transport\n\t\t\tc, err := NewClient(tt.server.URL, WithTransport(tr))\n\t\t\trequire.NoError(t, err)\n\n\t\t\ttt.server.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\trender.JSONStatus(w, r, tt.response, tt.responseCode)\n\t\t\t})\n\n\t\t\tgot, err := c.RootFingerprint()\n\t\t\tif tt.wantErr {\n\t\t\t\tassert.Error(t, err)\n\t\t\t\tassert.Empty(t, got)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, tt.want, got)\n\t\t})\n\t}\n}\n\nfunc TestClient_RootFingerprintWithServer(t *testing.T) {\n\tsrv := startCABootstrapServer()\n\tdefer srv.Close()\n\n\tcaClient, err := NewClient(srv.URL+\"/sign\", WithRootFile(\"testdata/secrets/root_ca.crt\"))\n\trequire.NoError(t, err)\n\n\tfp, err := caClient.RootFingerprint()\n\tassert.NoError(t, err)\n\tassert.Equal(t, \"ef742f95dc0d8aa82d3cca4017af6dac3fce84290344159891952d18c53eefe7\", fp)\n}\n\nfunc TestClient_SSHBastion(t *testing.T) {\n\tok := &api.SSHBastionResponse{\n\t\tHostname: \"host.local\",\n\t\tBastion: &authority.Bastion{\n\t\t\tHostname: \"bastion.local\",\n\t\t},\n\t}\n\n\ttests := []struct {\n\t\tname         string\n\t\trequest      *api.SSHBastionRequest\n\t\tresponse     interface{}\n\t\tresponseCode int\n\t\twantErr      bool\n\t\terr          error\n\t}{\n\t\t{\"ok\", &api.SSHBastionRequest{Hostname: \"host.local\"}, ok, 200, false, nil},\n\t\t{\"bad-response\", &api.SSHBastionRequest{Hostname: \"host.local\"}, \"bad json\", 200, true, nil},\n\t\t{\"bad-request\", &api.SSHBastionRequest{}, errs.BadRequest(\"force\"), 400, true, errors.New(errs.BadRequestPrefix)},\n\t}\n\n\tsrv := httptest.NewServer(nil)\n\tdefer srv.Close()\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tc, err := NewClient(srv.URL, WithTransport(http.DefaultTransport))\n\t\t\trequire.NoError(t, err)\n\n\t\t\tsrv.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\trender.JSONStatus(w, r, tt.response, tt.responseCode)\n\t\t\t})\n\n\t\t\tgot, err := c.SSHBastion(tt.request)\n\t\t\tif tt.wantErr {\n\t\t\t\tif assert.Error(t, err) {\n\t\t\t\t\tif tt.responseCode != 200 {\n\t\t\t\t\t\tvar sc render.StatusCodedError\n\t\t\t\t\t\tif assert.ErrorAs(t, err, &sc) {\n\t\t\t\t\t\t\tassert.Equal(t, tt.responseCode, sc.StatusCode())\n\t\t\t\t\t\t}\n\t\t\t\t\t\tassert.True(t, strings.HasPrefix(err.Error(), tt.err.Error()))\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tassert.Nil(t, got)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, tt.response, got)\n\t\t})\n\t}\n}\n\nfunc TestClient_GetCaURL(t *testing.T) {\n\ttests := []struct {\n\t\tname  string\n\t\tcaURL string\n\t\twant  string\n\t}{\n\t\t{\"ok\", \"https://ca.com\", \"https://ca.com\"},\n\t\t{\"ok no schema\", \"ca.com\", \"https://ca.com\"},\n\t\t{\"ok with port\", \"https://ca.com:9000\", \"https://ca.com:9000\"},\n\t\t{\"ok with version\", \"https://ca.com/1.0\", \"https://ca.com/1.0\"},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tc, err := NewClient(tt.caURL, WithTransport(http.DefaultTransport))\n\t\t\trequire.NoError(t, err)\n\n\t\t\tgot := c.GetCaURL()\n\t\t\tassert.Equal(t, tt.want, got)\n\t\t})\n\t}\n}\n\nfunc TestClient_WithTimeout(t *testing.T) {\n\tsrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\ttime.Sleep(200 * time.Millisecond)\n\t\trender.JSONStatus(w, r, api.HealthResponse{Status: \"ok\"}, 200)\n\t}))\n\tdefer srv.Close()\n\n\ttests := []struct {\n\t\tname      string\n\t\toptions   []ClientOption\n\t\tassertion assert.ErrorAssertionFunc\n\t}{\n\t\t{\"ok\", []ClientOption{WithTransport(http.DefaultTransport)}, assert.NoError},\n\t\t{\"ok with timeout\", []ClientOption{WithTransport(http.DefaultTransport), WithTimeout(5 * time.Second)}, assert.NoError},\n\t\t{\"fail with timeout\", []ClientOption{WithTransport(http.DefaultTransport), WithTimeout(10 * time.Millisecond)}, assert.Error},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tc, err := NewClient(srv.URL, tt.options...)\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.NotZero(t, c.timeout)\n\t\t\t_, err = c.Health()\n\t\t\ttt.assertion(t, err)\n\t\t})\n\t}\n}\n\ntype decoratedRoundTripper func(*http.Request) (*http.Response, error)\n\nfunc (rt decoratedRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {\n\treturn rt(req)\n}\n\nfunc TestClient_WithTransportDecorator(t *testing.T) {\n\tvar srv *httptest.Server\n\tsrv = httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tif strings.HasPrefix(r.RequestURI, \"/root\") {\n\t\t\trender.JSONStatus(w, r, api.RootResponse{\n\t\t\t\tRootPEM: api.NewCertificate(srv.Certificate()),\n\t\t\t}, 200)\n\t\t\treturn\n\t\t}\n\n\t\tif s := r.Header.Get(\"X-Test-Header\"); s != \"\" {\n\t\t\trender.JSONStatus(w, r, api.HealthResponse{Status: s}, 200)\n\t\t} else {\n\t\t\trender.JSONStatus(w, r, api.HealthResponse{Status: \"ok\"}, 200)\n\t\t}\n\t}))\n\tdefer srv.Close()\n\n\tfp := x509util.Fingerprint(srv.Certificate())\n\tc, err := NewClient(srv.URL, WithRootSHA256(fp), WithTransportDecorator(func(tr http.RoundTripper) http.RoundTripper {\n\t\treturn decoratedRoundTripper(func(r *http.Request) (*http.Response, error) {\n\t\t\tr.Header.Add(\"X-Test-Header\", \"some-data\")\n\t\t\treturn tr.RoundTrip(r)\n\t\t})\n\t}))\n\trequire.NoError(t, err)\n\tresp, err := c.Health()\n\trequire.NoError(t, err)\n\tassert.Equal(t, \"some-data\", resp.Status)\n}\n\nfunc Test_enforceRequestID(t *testing.T) {\n\tset := httptest.NewRequest(http.MethodGet, \"https://example.com\", http.NoBody)\n\tset.Header.Set(\"X-Request-Id\", \"already-set\")\n\tinContext := httptest.NewRequest(http.MethodGet, \"https://example.com\", http.NoBody)\n\tinContext = inContext.WithContext(client.NewRequestIDContext(inContext.Context(), \"from-context\"))\n\tnewRequestID := httptest.NewRequest(http.MethodGet, \"https://example.com\", http.NoBody)\n\n\ttests := []struct {\n\t\tname string\n\t\tr    *http.Request\n\t\twant string\n\t}{\n\t\t{\n\t\t\tname: \"set\",\n\t\t\tr:    set,\n\t\t\twant: \"already-set\",\n\t\t},\n\t\t{\n\t\t\tname: \"context\",\n\t\t\tr:    inContext,\n\t\t\twant: \"from-context\",\n\t\t},\n\t\t{\n\t\t\tname: \"new\",\n\t\t\tr:    newRequestID,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tenforceRequestID(tt.r)\n\n\t\t\tv := tt.r.Header.Get(\"X-Request-Id\")\n\t\t\tif assert.NotEmpty(t, v) {\n\t\t\t\tif tt.want != \"\" {\n\t\t\t\t\tassert.Equal(t, tt.want, v)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_newRequestID(t *testing.T) {\n\trequestID := newRequestID()\n\tu, err := uuid.Parse(requestID)\n\tassert.NoError(t, err)\n\tassert.Equal(t, uuid.Version(0x4), u.Version())\n\tassert.Equal(t, uuid.RFC4122, u.Variant())\n\tassert.Equal(t, requestID, u.String())\n}\n"
  },
  {
    "path": "ca/identity/client.go",
    "content": "package identity\n\nimport (\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\n\t\"github.com/pkg/errors\"\n\t\"github.com/smallstep/certificates/internal/httptransport\"\n)\n\n// Client wraps http.Client with a transport using the step root and identity.\ntype Client struct {\n\tCaURL *url.URL\n\t*http.Client\n}\n\n// ResolveReference resolves the given reference from the CaURL.\nfunc (c *Client) ResolveReference(ref *url.URL) *url.URL {\n\treturn c.CaURL.ResolveReference(ref)\n}\n\n// LoadClient configures an http.Client with the root in\n// $STEPPATH/config/defaults.json and the identity defined in\n// $STEPPATH/config/identity.json\nfunc LoadClient() (*Client, error) {\n\tdefaultsFile := DefaultsFile()\n\tb, err := os.ReadFile(defaultsFile)\n\tif err != nil {\n\t\treturn nil, errors.Wrapf(err, \"error reading %s\", defaultsFile)\n\t}\n\n\tvar defaults defaultsConfig\n\tif err := json.Unmarshal(b, &defaults); err != nil {\n\t\treturn nil, errors.Wrapf(err, \"error unmarshaling %s\", defaultsFile)\n\t}\n\tif err := defaults.Validate(); err != nil {\n\t\treturn nil, errors.Wrapf(err, \"error validating %s\", defaultsFile)\n\t}\n\tcaURL, err := url.Parse(defaults.CaURL)\n\tif err != nil {\n\t\treturn nil, errors.Wrapf(err, \"error validating %s\", defaultsFile)\n\t}\n\tif caURL.Scheme == \"\" {\n\t\tcaURL.Scheme = \"https\"\n\t}\n\n\tidentity, err := LoadDefaultIdentity()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif err := identity.Validate(); err != nil {\n\t\treturn nil, errors.Wrapf(err, \"error validating %s\", IdentityFile())\n\t}\n\tif kind := identity.Kind(); kind != MutualTLS {\n\t\treturn nil, errors.Errorf(\"unsupported identity %s: only mTLS is currently supported\", kind)\n\t}\n\n\t// Prepare transport with information in defaults.json and identity.json\n\ttr := httptransport.New()\n\ttr.TLSClientConfig = &tls.Config{\n\t\tMinVersion:           tls.VersionTLS12,\n\t\tGetClientCertificate: identity.GetClientCertificateFunc(),\n\t}\n\n\t// RootCAs\n\tb, err = os.ReadFile(defaults.Root)\n\tif err != nil {\n\t\treturn nil, errors.Wrapf(err, \"error loading %s\", defaults.Root)\n\t}\n\tpool := x509.NewCertPool()\n\tif pool.AppendCertsFromPEM(b) {\n\t\ttr.TLSClientConfig.RootCAs = pool\n\t}\n\n\treturn &Client{\n\t\tCaURL: caURL,\n\t\tClient: &http.Client{\n\t\t\tTransport: tr,\n\t\t},\n\t}, nil\n}\n\ntype defaultsConfig struct {\n\tCaURL string `json:\"ca-url\"`\n\tRoot  string `json:\"root\"`\n}\n\nfunc (c *defaultsConfig) Validate() error {\n\tswitch {\n\tcase c.CaURL == \"\":\n\t\treturn fmt.Errorf(\"missing or invalid `ca-url` property\")\n\tcase c.Root == \"\":\n\t\treturn fmt.Errorf(\"missing or invalid `root` property\")\n\tdefault:\n\t\treturn nil\n\t}\n}\n"
  },
  {
    "path": "ca/identity/client_test.go",
    "content": "package identity\n\nimport (\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"os\"\n\t\"reflect\"\n\t\"sort\"\n\t\"testing\"\n\n\t\"github.com/smallstep/certificates/internal/httptransport\"\n)\n\nfunc returnInput(val string) func() string {\n\treturn func() string {\n\t\treturn val\n\t}\n}\n\nfunc TestClient(t *testing.T) {\n\toldIdentityFile := IdentityFile\n\toldDefaultsFile := DefaultsFile\n\tdefer func() {\n\t\tIdentityFile = oldIdentityFile\n\t\tDefaultsFile = oldDefaultsFile\n\t}()\n\n\tIdentityFile = returnInput(\"testdata/config/identity.json\")\n\tDefaultsFile = returnInput(\"testdata/config/defaults.json\")\n\n\tclient, err := LoadClient()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tokServer := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tif r.TLS == nil || len(r.TLS.PeerCertificates) == 0 {\n\t\t\tw.WriteHeader(http.StatusUnauthorized)\n\t\t} else {\n\t\t\tw.WriteHeader(http.StatusOK)\n\t\t}\n\t}))\n\tdefer okServer.Close()\n\n\tcrt, err := tls.LoadX509KeyPair(\"testdata/certs/server.crt\", \"testdata/secrets/server_key\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tb, err := os.ReadFile(\"testdata/certs/root_ca.crt\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tpool := x509.NewCertPool()\n\tpool.AppendCertsFromPEM(b)\n\n\tokServer.TLS = &tls.Config{\n\t\tCertificates: []tls.Certificate{crt},\n\t\tClientCAs:    pool,\n\t\tClientAuth:   tls.VerifyClientCertIfGiven,\n\t\tMinVersion:   tls.VersionTLS12,\n\t}\n\tokServer.StartTLS()\n\n\tbadServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tw.Write([]byte(\"ok\"))\n\t}))\n\tdefer badServer.Close()\n\n\tif resp, err := client.Get(okServer.URL); err != nil {\n\t\tt.Errorf(\"client.Get() error = %v\", err)\n\t} else {\n\t\tresp.Body.Close()\n\t\tif resp.StatusCode != http.StatusOK {\n\t\t\tt.Errorf(\"client.Get() = %d, want %d\", resp.StatusCode, http.StatusOK)\n\t\t}\n\t}\n\n\tif _, err := client.Get(badServer.URL); err == nil {\n\t\tt.Errorf(\"client.Get() error = %v, wantErr true\", err)\n\t}\n}\n\nfunc TestClient_ResolveReference(t *testing.T) {\n\ttype fields struct {\n\t\tCaURL *url.URL\n\t}\n\ttype args struct {\n\t\tref *url.URL\n\t}\n\ttests := []struct {\n\t\tname   string\n\t\tfields fields\n\t\targs   args\n\t\twant   *url.URL\n\t}{\n\t\t{\"ok\", fields{&url.URL{Scheme: \"https\", Host: \"localhost\"}}, args{&url.URL{Path: \"/foo\"}}, &url.URL{Scheme: \"https\", Host: \"localhost\", Path: \"/foo\"}},\n\t\t{\"ok\", fields{&url.URL{Scheme: \"https\", Host: \"localhost\", Path: \"/bar\"}}, args{&url.URL{Path: \"/foo\"}}, &url.URL{Scheme: \"https\", Host: \"localhost\", Path: \"/foo\"}},\n\t\t{\"ok\", fields{&url.URL{Scheme: \"https\", Host: \"localhost\"}}, args{&url.URL{Path: \"/foo\", RawQuery: \"foo=bar\"}}, &url.URL{Scheme: \"https\", Host: \"localhost\", Path: \"/foo\", RawQuery: \"foo=bar\"}},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tc := &Client{\n\t\t\t\tCaURL: tt.fields.CaURL,\n\t\t\t}\n\t\t\tif got := c.ResolveReference(tt.args.ref); !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"Client.ResolveReference() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLoadClient(t *testing.T) {\n\toldIdentityFile := IdentityFile\n\toldDefaultsFile := DefaultsFile\n\tdefer func() {\n\t\tIdentityFile = oldIdentityFile\n\t\tDefaultsFile = oldDefaultsFile\n\t}()\n\n\tcrt, err := tls.LoadX509KeyPair(\"testdata/identity/identity.crt\", \"testdata/identity/identity_key\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tb, err := os.ReadFile(\"testdata/certs/root_ca.crt\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tpool := x509.NewCertPool()\n\tpool.AppendCertsFromPEM(b)\n\n\ttr := httptransport.New()\n\ttr.TLSClientConfig = &tls.Config{\n\t\tCertificates: []tls.Certificate{crt},\n\t\tRootCAs:      pool,\n\t\tMinVersion:   tls.VersionTLS12,\n\t}\n\texpected := &Client{\n\t\tCaURL: &url.URL{Scheme: \"https\", Host: \"127.0.0.1\"},\n\t\tClient: &http.Client{\n\t\t\tTransport: tr,\n\t\t},\n\t}\n\n\ttests := []struct {\n\t\tname    string\n\t\tprepare func()\n\t\twant    *Client\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", func() {\n\t\t\tIdentityFile = returnInput(\"testdata/config/identity.json\")\n\t\t\tDefaultsFile = returnInput(\"testdata/config/defaults.json\")\n\t\t}, expected, false},\n\t\t{\"fail identity\", func() {\n\t\t\tIdentityFile = returnInput(\"testdata/config/missing.json\")\n\t\t\tDefaultsFile = returnInput(\"testdata/config/defaults.json\")\n\t\t}, nil, true},\n\t\t{\"fail identity\", func() {\n\t\t\tIdentityFile = returnInput(\"testdata/config/fail.json\")\n\t\t\tDefaultsFile = returnInput(\"testdata/config/defaults.json\")\n\t\t}, nil, true},\n\t\t{\"fail defaults\", func() {\n\t\t\tIdentityFile = returnInput(\"testdata/config/identity.json\")\n\t\t\tDefaultsFile = returnInput(\"testdata/config/missing.json\")\n\t\t}, nil, true},\n\t\t{\"fail defaults\", func() {\n\t\t\tIdentityFile = returnInput(\"testdata/config/identity.json\")\n\t\t\tDefaultsFile = returnInput(\"testdata/config/fail.json\")\n\t\t}, nil, true},\n\t\t{\"fail ca\", func() {\n\t\t\tIdentityFile = returnInput(\"testdata/config/identity.json\")\n\t\t\tDefaultsFile = returnInput(\"testdata/config/badca.json\")\n\t\t}, nil, true},\n\t\t{\"fail root\", func() {\n\t\t\tIdentityFile = returnInput(\"testdata/config/identity.json\")\n\t\t\tDefaultsFile = returnInput(\"testdata/config/badroot.json\")\n\t\t}, nil, true},\n\t\t{\"fail type\", func() {\n\t\t\tIdentityFile = returnInput(\"testdata/config/badIdentity.json\")\n\t\t\tDefaultsFile = returnInput(\"testdata/config/defaults.json\")\n\t\t}, nil, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ttt.prepare()\n\t\t\tgot, err := LoadClient()\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"LoadClient() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif tt.want == nil {\n\t\t\t\tif !reflect.DeepEqual(got, tt.want) {\n\t\t\t\t\tt.Errorf(\"LoadClient() = %#v, want %#v\", got, tt.want)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tgotTransport := got.Client.Transport.(*http.Transport)\n\t\t\t\twantTransport := tt.want.Client.Transport.(*http.Transport)\n\t\t\t\tswitch {\n\t\t\t\tcase gotTransport.TLSClientConfig.GetClientCertificate == nil:\n\t\t\t\t\tt.Error(\"LoadClient() transport does not define GetClientCertificate\")\n\t\t\t\tcase !reflect.DeepEqual(got.CaURL, tt.want.CaURL) || !equalPools(gotTransport.TLSClientConfig.RootCAs, wantTransport.TLSClientConfig.RootCAs):\n\t\t\t\t\tt.Errorf(\"LoadClient() = %#v, want %#v\", got, tt.want)\n\t\t\t\tdefault:\n\t\t\t\t\tcrt, err := gotTransport.TLSClientConfig.GetClientCertificate(nil)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tt.Errorf(\"LoadClient() GetClientCertificate error = %v\", err)\n\t\t\t\t\t} else if !reflect.DeepEqual(*crt, wantTransport.TLSClientConfig.Certificates[0]) {\n\t\t\t\t\t\tt.Errorf(\"LoadClient() GetClientCertificate crt = %#v, want %#v\", *crt, wantTransport.TLSClientConfig.Certificates[0])\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_defaultsConfig_Validate(t *testing.T) {\n\ttype fields struct {\n\t\tCaURL string\n\t\tRoot  string\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tfields  fields\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", fields{\"https://127.0.0.1\", \"root_ca.crt\"}, false},\n\t\t{\"fail ca-url\", fields{\"\", \"root_ca.crt\"}, true},\n\t\t{\"fail root\", fields{\"https://127.0.0.1\", \"\"}, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tc := &defaultsConfig{\n\t\t\t\tCaURL: tt.fields.CaURL,\n\t\t\t\tRoot:  tt.fields.Root,\n\t\t\t}\n\t\t\tif err := c.Validate(); (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"defaultsConfig.Validate() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\t\t})\n\t}\n}\n\n//nolint:staticcheck,gocritic\nfunc equalPools(a, b *x509.CertPool) bool {\n\tif reflect.DeepEqual(a, b) {\n\t\treturn true\n\t}\n\tsubjects := a.Subjects()\n\tsA := make([]string, len(subjects))\n\tfor i := range subjects {\n\t\tsA[i] = string(subjects[i])\n\t}\n\tsubjects = b.Subjects()\n\tsB := make([]string, len(subjects))\n\tfor i := range subjects {\n\t\tsB[i] = string(subjects[i])\n\t}\n\tsort.Strings(sA)\n\tsort.Strings(sB)\n\treturn reflect.DeepEqual(sA, sB)\n}\n"
  },
  {
    "path": "ca/identity/identity.go",
    "content": "package identity\n\nimport (\n\t\"bytes\"\n\t\"crypto\"\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"encoding/json\"\n\t\"encoding/pem\"\n\t\"net/http\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n\n\t\"github.com/smallstep/cli-utils/step\"\n\t\"go.step.sm/crypto/pemutil\"\n\n\t\"github.com/smallstep/certificates/api\"\n\t\"github.com/smallstep/certificates/internal/httptransport\"\n)\n\n// Type represents the different types of identity files.\ntype Type string\n\n// Disabled represents a disabled identity type\nconst Disabled Type = \"\"\n\n// MutualTLS represents the identity using mTLS.\nconst MutualTLS Type = \"mTLS\"\n\n// TunnelTLS represents an identity using a (m)TLS tunnel.\n//\n// TunnelTLS can be optionally configured with client certificates and a root\n// file with the CAs to trust. By default it will use the system truststore\n// instead of the CA truststore.\nconst TunnelTLS Type = \"tTLS\"\n\n// DefaultLeeway is the duration for matching not before claims.\nconst DefaultLeeway = 1 * time.Minute\n\nvar (\n\tidentityDir = step.IdentityPath\n\tconfigDir   = step.ConfigPath\n\n\t// IdentityFile contains a pointer to a function that outputs the location of\n\t// the identity file.\n\tIdentityFile = step.IdentityFile\n\n\t// DefaultsFile contains a prointer a function that outputs the location of the\n\t// defaults configuration file.\n\tDefaultsFile = step.DefaultsFile\n)\n\n// Identity represents the identity file that can be used to authenticate with\n// the CA.\ntype Identity struct {\n\tType        string `json:\"type\"`\n\tCertificate string `json:\"crt\"`\n\tKey         string `json:\"key\"`\n\n\t// Host is the tunnel host for a TunnelTLS (tTLS) identity.\n\tHost string `json:\"host,omitempty\"`\n\t// Root is the CA bundle of root CAs used in TunnelTLS to trust the\n\t// certificate of the host.\n\tRoot string `json:\"root,omitempty\"`\n}\n\n// LoadIdentity loads an identity present in the given filename.\nfunc LoadIdentity(filename string) (*Identity, error) {\n\tb, err := os.ReadFile(filename)\n\tif err != nil {\n\t\treturn nil, errors.Wrapf(err, \"error reading %s\", filename)\n\t}\n\tidentity := new(Identity)\n\tif err := json.Unmarshal(b, &identity); err != nil {\n\t\treturn nil, errors.Wrapf(err, \"error unmarshaling %s\", filename)\n\t}\n\treturn identity, nil\n}\n\n// LoadDefaultIdentity loads the default identity.\nfunc LoadDefaultIdentity() (*Identity, error) {\n\treturn LoadIdentity(IdentityFile())\n}\n\n// WriteDefaultIdentity writes the given certificates and key and the\n// identity.json pointing to the new files.\nfunc WriteDefaultIdentity(certChain []api.Certificate, key crypto.PrivateKey) error {\n\tif err := os.MkdirAll(configDir(), 0700); err != nil {\n\t\treturn errors.Wrap(err, \"error creating config directory\")\n\t}\n\n\tidentityDir := identityDir()\n\tif err := os.MkdirAll(identityDir, 0700); err != nil {\n\t\treturn errors.Wrap(err, \"error creating identity directory\")\n\t}\n\n\tcertFilename := filepath.Join(identityDir, \"identity.crt\")\n\tkeyFilename := filepath.Join(identityDir, \"identity_key\")\n\n\t// Write certificate\n\tif err := writeCertificate(certFilename, certChain); err != nil {\n\t\treturn err\n\t}\n\n\t// Write key\n\tbuf := new(bytes.Buffer)\n\tblock, err := pemutil.Serialize(key)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif err := pem.Encode(buf, block); err != nil {\n\t\treturn errors.Wrap(err, \"error encoding identity key\")\n\t}\n\tif err := os.WriteFile(keyFilename, buf.Bytes(), 0600); err != nil {\n\t\treturn errors.Wrap(err, \"error writing identity certificate\")\n\t}\n\n\t// Write identity.json\n\tbuf.Reset()\n\tenc := json.NewEncoder(buf)\n\tenc.SetIndent(\"\", \"   \")\n\tif err := enc.Encode(Identity{\n\t\tType:        string(MutualTLS),\n\t\tCertificate: certFilename,\n\t\tKey:         keyFilename,\n\t}); err != nil {\n\t\treturn errors.Wrap(err, \"error writing identity json\")\n\t}\n\tif err := os.WriteFile(IdentityFile(), buf.Bytes(), 0600); err != nil {\n\t\treturn errors.Wrap(err, \"error writing identity certificate\")\n\t}\n\n\treturn nil\n}\n\n// WriteIdentityCertificate writes the identity certificate to disk.\nfunc WriteIdentityCertificate(certChain []api.Certificate) error {\n\tfilename := filepath.Join(identityDir(), \"identity.crt\")\n\treturn writeCertificate(filename, certChain)\n}\n\n// writeCertificate writes the given certificate on disk.\nfunc writeCertificate(filename string, certChain []api.Certificate) error {\n\tbuf := new(bytes.Buffer)\n\tfor _, crt := range certChain {\n\t\tblock := &pem.Block{\n\t\t\tType:  \"CERTIFICATE\",\n\t\t\tBytes: crt.Raw,\n\t\t}\n\t\tif err := pem.Encode(buf, block); err != nil {\n\t\t\treturn errors.Wrap(err, \"error encoding certificate\")\n\t\t}\n\t}\n\n\tif err := os.WriteFile(filename, buf.Bytes(), 0600); err != nil {\n\t\treturn errors.Wrap(err, \"error writing certificate\")\n\t}\n\n\treturn nil\n}\n\n// Kind returns the type for the given identity.\nfunc (i *Identity) Kind() Type {\n\tswitch strings.ToLower(i.Type) {\n\tcase \"\":\n\t\treturn Disabled\n\tcase \"mtls\":\n\t\treturn MutualTLS\n\tcase \"ttls\":\n\t\treturn TunnelTLS\n\tdefault:\n\t\treturn Type(i.Type)\n\t}\n}\n\n// Validate validates the identity object.\nfunc (i *Identity) Validate() error {\n\tswitch i.Kind() {\n\tcase Disabled:\n\t\treturn nil\n\tcase MutualTLS:\n\t\tif i.Certificate == \"\" {\n\t\t\treturn errors.New(\"identity.crt cannot be empty\")\n\t\t}\n\t\tif i.Key == \"\" {\n\t\t\treturn errors.New(\"identity.key cannot be empty\")\n\t\t}\n\t\tif err := fileExists(i.Certificate); err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn fileExists(i.Key)\n\tcase TunnelTLS:\n\t\tif i.Host == \"\" {\n\t\t\treturn errors.New(\"tunnel.host cannot be empty\")\n\t\t}\n\t\tif i.Certificate != \"\" {\n\t\t\tif err := fileExists(i.Certificate); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif i.Key == \"\" {\n\t\t\t\treturn errors.New(\"tunnel.key cannot be empty\")\n\t\t\t}\n\t\t\tif err := fileExists(i.Key); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\tif i.Root != \"\" {\n\t\t\tif err := fileExists(i.Root); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\treturn nil\n\tdefault:\n\t\treturn errors.Errorf(\"unsupported identity type %s\", i.Type)\n\t}\n}\n\n// TLSCertificate returns a tls.Certificate for the identity.\nfunc (i *Identity) TLSCertificate() (tls.Certificate, error) {\n\tfail := func(err error) (tls.Certificate, error) { return tls.Certificate{}, err }\n\tswitch i.Kind() {\n\tcase Disabled:\n\t\treturn tls.Certificate{}, nil\n\tcase MutualTLS, TunnelTLS:\n\t\tcrt, err := tls.LoadX509KeyPair(i.Certificate, i.Key)\n\t\tif err != nil {\n\t\t\treturn fail(errors.Wrap(err, \"error creating identity certificate\"))\n\t\t}\n\n\t\t// Check if certificate is expired.\n\t\tx509Cert, err := x509.ParseCertificate(crt.Certificate[0])\n\t\tif err != nil {\n\t\t\treturn fail(errors.Wrap(err, \"error creating identity certificate\"))\n\t\t}\n\t\tnow := time.Now().Truncate(time.Second)\n\t\tif now.Add(DefaultLeeway).Before(x509Cert.NotBefore) {\n\t\t\treturn fail(errors.New(\"certificate is not yet valid\"))\n\t\t}\n\t\tif now.After(x509Cert.NotAfter) {\n\t\t\treturn fail(errors.New(\"certificate is already expired\"))\n\t\t}\n\t\treturn crt, nil\n\tdefault:\n\t\treturn fail(errors.Errorf(\"unsupported identity type %s\", i.Type))\n\t}\n}\n\n// GetClientCertificateFunc returns a method that can be used as the\n// GetClientCertificate property in a tls.Config.\nfunc (i *Identity) GetClientCertificateFunc() func(*tls.CertificateRequestInfo) (*tls.Certificate, error) {\n\treturn func(*tls.CertificateRequestInfo) (*tls.Certificate, error) {\n\t\tcrt, err := tls.LoadX509KeyPair(i.Certificate, i.Key)\n\t\tif err != nil {\n\t\t\treturn nil, errors.Wrap(err, \"error loading identity certificate\")\n\t\t}\n\t\treturn &crt, nil\n\t}\n}\n\n// GetCertPool returns a x509.CertPool if the identity defines a custom root.\nfunc (i *Identity) GetCertPool() (*x509.CertPool, error) {\n\tif i.Root == \"\" {\n\t\t//nolint:nilnil // legacy\n\t\treturn nil, nil\n\t}\n\tb, err := os.ReadFile(i.Root)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"error reading identity root\")\n\t}\n\tpool := x509.NewCertPool()\n\tif !pool.AppendCertsFromPEM(b) {\n\t\treturn nil, errors.Errorf(\"error pasing identity root: %s does not contain any certificate\", i.Root)\n\t}\n\treturn pool, nil\n}\n\n// Renewer is that interface that a renew client must implement.\ntype Renewer interface {\n\tGetRootCAs() *x509.CertPool\n\tRenew(tr http.RoundTripper) (*api.SignResponse, error)\n}\n\n// Renew renews the current identity certificate using a client with a renew\n// method.\nfunc (i *Identity) Renew(client Renewer) error {\n\tswitch i.Kind() {\n\tcase Disabled:\n\t\treturn nil\n\tcase MutualTLS, TunnelTLS:\n\t\tcert, err := i.TLSCertificate()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\ttr := httptransport.New()\n\t\ttr.TLSClientConfig = &tls.Config{\n\t\t\tCertificates:             []tls.Certificate{cert},\n\t\t\tRootCAs:                  client.GetRootCAs(),\n\t\t\tMinVersion:               tls.VersionTLS12,\n\t\t\tPreferServerCipherSuites: true,\n\t\t}\n\n\t\tsign, err := client.Renew(tr)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif len(sign.CertChainPEM) == 0 {\n\t\t\tsign.CertChainPEM = []api.Certificate{sign.ServerPEM, sign.CaPEM}\n\t\t}\n\n\t\t// Write certificate\n\t\tbuf := new(bytes.Buffer)\n\t\tfor _, crt := range sign.CertChainPEM {\n\t\t\tblock := &pem.Block{\n\t\t\t\tType:  \"CERTIFICATE\",\n\t\t\t\tBytes: crt.Raw,\n\t\t\t}\n\t\t\tif err := pem.Encode(buf, block); err != nil {\n\t\t\t\treturn errors.Wrap(err, \"error encoding identity certificate\")\n\t\t\t}\n\t\t}\n\t\tcertFilename := filepath.Join(identityDir(), \"identity.crt\")\n\t\tif err := os.WriteFile(certFilename, buf.Bytes(), 0600); err != nil {\n\t\t\treturn errors.Wrap(err, \"error writing identity certificate\")\n\t\t}\n\n\t\treturn nil\n\tdefault:\n\t\treturn errors.Errorf(\"unsupported identity type %s\", i.Type)\n\t}\n}\n\nfunc fileExists(filename string) error {\n\tinfo, err := os.Stat(filename)\n\tif err != nil {\n\t\treturn errors.Wrapf(err, \"error reading %s\", filename)\n\t}\n\tif info.IsDir() {\n\t\treturn errors.Errorf(\"error reading %s: file is a directory\", filename)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "ca/identity/identity_test.go",
    "content": "package identity\n\nimport (\n\t\"crypto\"\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"reflect\"\n\t\"testing\"\n\n\t\"github.com/smallstep/certificates/api\"\n\t\"go.step.sm/crypto/pemutil\"\n)\n\nfunc TestLoadDefaultIdentity(t *testing.T) {\n\toldFile := IdentityFile\n\tdefer func() {\n\t\tIdentityFile = oldFile\n\t}()\n\n\texpected := &Identity{\n\t\tType:        \"mTLS\",\n\t\tCertificate: \"testdata/identity/identity.crt\",\n\t\tKey:         \"testdata/identity/identity_key\",\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tprepare func()\n\t\twant    *Identity\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", func() { IdentityFile = returnInput(\"testdata/config/identity.json\") }, expected, false},\n\t\t{\"fail read\", func() { IdentityFile = returnInput(\"testdata/config/missing.json\") }, nil, true},\n\t\t{\"fail unmarshal\", func() { IdentityFile = returnInput(\"testdata/config/fail.json\") }, nil, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ttt.prepare()\n\t\t\tgot, err := LoadDefaultIdentity()\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"LoadDefaultIdentity() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"LoadDefaultIdentity() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestIdentity_Kind(t *testing.T) {\n\ttype fields struct {\n\t\tType string\n\t}\n\ttests := []struct {\n\t\tname   string\n\t\tfields fields\n\t\twant   Type\n\t}{\n\t\t{\"disabled\", fields{\"\"}, Disabled},\n\t\t{\"mutualTLS\", fields{\"mTLS\"}, MutualTLS},\n\t\t{\"tunnelTLS\", fields{\"tTLS\"}, TunnelTLS},\n\t\t{\"unknown\", fields{\"unknown\"}, Type(\"unknown\")},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ti := &Identity{\n\t\t\t\tType: tt.fields.Type,\n\t\t\t}\n\t\t\tif got := i.Kind(); got != tt.want {\n\t\t\t\tt.Errorf(\"Identity.Kind() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestIdentity_Validate(t *testing.T) {\n\ttype fields struct {\n\t\tType        string\n\t\tCertificate string\n\t\tKey         string\n\t\tHost        string\n\t\tRoot        string\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tfields  fields\n\t\twantErr bool\n\t}{\n\t\t{\"ok mTLS\", fields{\"mTLS\", \"testdata/identity/identity.crt\", \"testdata/identity/identity_key\", \"\", \"\"}, false},\n\t\t{\"ok tTLS\", fields{\"tTLS\", \"testdata/identity/identity.crt\", \"testdata/identity/identity_key\", \"tunnel:443\", \"testdata/certs/root_ca.crt\"}, false},\n\t\t{\"ok disabled\", fields{}, false},\n\t\t{\"fail type\", fields{\"foo\", \"testdata/identity/identity.crt\", \"testdata/identity/identity_key\", \"\", \"\"}, true},\n\t\t{\"fail certificate\", fields{\"mTLS\", \"\", \"testdata/identity/identity_key\", \"\", \"\"}, true},\n\t\t{\"fail key\", fields{\"mTLS\", \"testdata/identity/identity.crt\", \"\", \"\", \"\"}, true},\n\t\t{\"fail key\", fields{\"tTLS\", \"testdata/identity/identity.crt\", \"\", \"tunnel:443\", \"testdata/certs/root_ca.crt\"}, true},\n\t\t{\"fail missing certificate\", fields{\"mTLS\", \"testdata/identity/missing.crt\", \"testdata/identity/identity_key\", \"\", \"\"}, true},\n\t\t{\"fail missing certificate\", fields{\"tTLS\", \"testdata/identity/missing.crt\", \"testdata/identity/identity_key\", \"tunnel:443\", \"testdata/certs/root_ca.crt\"}, true},\n\t\t{\"fail missing key\", fields{\"mTLS\", \"testdata/identity/identity.crt\", \"testdata/identity/missing_key\", \"\", \"\"}, true},\n\t\t{\"fail missing key\", fields{\"tTLS\", \"testdata/identity/identity.crt\", \"testdata/identity/missing_key\", \"tunnel:443\", \"testdata/certs/root_ca.crt\"}, true},\n\t\t{\"fail host\", fields{\"tTLS\", \"testdata/identity/identity.crt\", \"testdata/identity/missing_key\", \"\", \"testdata/certs/root_ca.crt\"}, true},\n\t\t{\"fail root\", fields{\"tTLS\", \"testdata/identity/identity.crt\", \"testdata/identity/identity_key\", \"tunnel:443\", \"testdata/certs/missing.crt\"}, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ti := &Identity{\n\t\t\t\tType:        tt.fields.Type,\n\t\t\t\tCertificate: tt.fields.Certificate,\n\t\t\t\tKey:         tt.fields.Key,\n\t\t\t\tHost:        tt.fields.Host,\n\t\t\t\tRoot:        tt.fields.Root,\n\t\t\t}\n\t\t\tif err := i.Validate(); (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"Identity.Validate() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestIdentity_TLSCertificate(t *testing.T) {\n\texpected, err := tls.LoadX509KeyPair(\"testdata/identity/identity.crt\", \"testdata/identity/identity_key\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\ttype fields struct {\n\t\tType        string\n\t\tCertificate string\n\t\tKey         string\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tfields  fields\n\t\twant    tls.Certificate\n\t\twantErr bool\n\t}{\n\t\t{\"ok mTLS\", fields{\"mTLS\", \"testdata/identity/identity.crt\", \"testdata/identity/identity_key\"}, expected, false},\n\t\t{\"ok tTLS\", fields{\"tTLS\", \"testdata/identity/identity.crt\", \"testdata/identity/identity_key\"}, expected, false},\n\t\t{\"ok disabled\", fields{}, tls.Certificate{}, false},\n\t\t{\"fail type\", fields{\"foo\", \"testdata/identity/identity.crt\", \"testdata/identity/identity_key\"}, tls.Certificate{}, true},\n\t\t{\"fail certificate\", fields{\"mTLS\", \"testdata/certs/server.crt\", \"testdata/identity/identity_key\"}, tls.Certificate{}, true},\n\t\t{\"fail not after\", fields{\"mTLS\", \"testdata/identity/expired.crt\", \"testdata/identity/identity_key\"}, tls.Certificate{}, true},\n\t\t{\"fail not before\", fields{\"mTLS\", \"testdata/identity/not_before.crt\", \"testdata/identity/identity_key\"}, tls.Certificate{}, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ti := &Identity{\n\t\t\t\tType:        tt.fields.Type,\n\t\t\t\tCertificate: tt.fields.Certificate,\n\t\t\t\tKey:         tt.fields.Key,\n\t\t\t}\n\t\t\tgot, err := i.TLSCertificate()\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"Identity.TLSCertificate() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"Identity.TLSCertificate() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_fileExists(t *testing.T) {\n\ttype args struct {\n\t\tfilename string\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\targs    args\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", args{\"testdata/identity/identity.crt\"}, false},\n\t\t{\"missing\", args{\"testdata/identity/missing.crt\"}, true},\n\t\t{\"directory\", args{\"testdata/identity\"}, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif err := fileExists(tt.args.filename); (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"fileExists() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestWriteDefaultIdentity(t *testing.T) {\n\ttmpDir := t.TempDir()\n\n\toldConfigDir := configDir\n\toldIdentityDir := identityDir\n\toldIdentityFile := IdentityFile\n\tdefer func() {\n\t\tconfigDir = oldConfigDir\n\t\tidentityDir = oldIdentityDir\n\t\tIdentityFile = oldIdentityFile\n\t\tos.RemoveAll(tmpDir)\n\t}()\n\n\tcerts, err := pemutil.ReadCertificateBundle(\"testdata/identity/identity.crt\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tkey, err := pemutil.Read(\"testdata/identity/identity_key\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tvar certChain []api.Certificate\n\tfor _, c := range certs {\n\t\tcertChain = append(certChain, api.Certificate{Certificate: c})\n\t}\n\n\tconfigDir = returnInput(filepath.Join(tmpDir, \"config\"))\n\tidentityDir = returnInput(filepath.Join(tmpDir, \"identity\"))\n\tIdentityFile = returnInput(filepath.Join(tmpDir, \"config\", \"identity.json\"))\n\n\ttype args struct {\n\t\tcertChain []api.Certificate\n\t\tkey       crypto.PrivateKey\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tprepare func()\n\t\targs    args\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", func() {}, args{certChain, key}, false},\n\t\t{\"fail mkdir config\", func() {\n\t\t\tconfigDir = returnInput(filepath.Join(tmpDir, \"identity\", \"identity.crt\"))\n\t\t\tidentityDir = returnInput(filepath.Join(tmpDir, \"identity\"))\n\t\t}, args{certChain, key}, true},\n\t\t{\"fail mkdir identity\", func() {\n\t\t\tconfigDir = returnInput(filepath.Join(tmpDir, \"config\"))\n\t\t\tidentityDir = returnInput(filepath.Join(tmpDir, \"identity\", \"identity.crt\"))\n\t\t}, args{certChain, key}, true},\n\t\t{\"fail certificate\", func() {\n\t\t\tconfigDir = returnInput(filepath.Join(tmpDir, \"config\"))\n\t\t\tidentityDir = returnInput(filepath.Join(tmpDir, \"bad-dir\"))\n\t\t\tos.MkdirAll(identityDir(), 0600)\n\t\t}, args{certChain, key}, true},\n\t\t{\"fail key\", func() {\n\t\t\tconfigDir = returnInput(filepath.Join(tmpDir, \"config\"))\n\t\t\tidentityDir = returnInput(filepath.Join(tmpDir, \"identity\"))\n\t\t}, args{certChain, \"badKey\"}, true},\n\t\t{\"fail write identity\", func() {\n\t\t\tconfigDir = returnInput(filepath.Join(tmpDir, \"bad-dir\"))\n\t\t\tidentityDir = returnInput(filepath.Join(tmpDir, \"identity\"))\n\t\t\tIdentityFile = returnInput(filepath.Join(configDir(), \"identity.json\"))\n\t\t\tos.MkdirAll(configDir(), 0600)\n\t\t}, args{certChain, key}, true},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ttt.prepare()\n\t\t\tif err := WriteDefaultIdentity(tt.args.certChain, tt.args.key); (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"WriteDefaultIdentity() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestIdentity_GetClientCertificateFunc(t *testing.T) {\n\texpected, err := tls.LoadX509KeyPair(\"testdata/identity/identity.crt\", \"testdata/identity/identity_key\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\ttype fields struct {\n\t\tType        string\n\t\tCertificate string\n\t\tKey         string\n\t\tHost        string\n\t\tRoot        string\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tfields  fields\n\t\twant    *tls.Certificate\n\t\twantErr bool\n\t}{\n\t\t{\"ok mTLS\", fields{\"mtls\", \"testdata/identity/identity.crt\", \"testdata/identity/identity_key\", \"\", \"\"}, &expected, false},\n\t\t{\"ok tTLS\", fields{\"ttls\", \"testdata/identity/identity.crt\", \"testdata/identity/identity_key\", \"tunnel:443\", \"testdata/certs/root_ca.crt\"}, &expected, false},\n\t\t{\"fail missing cert\", fields{\"mTLS\", \"testdata/identity/missing.crt\", \"testdata/identity/identity_key\", \"\", \"\"}, nil, true},\n\t\t{\"fail missing key\", fields{\"tTLS\", \"testdata/identity/identity.crt\", \"testdata/identity/missing_key\", \"tunnel:443\", \"testdata/certs/root_ca.crt\"}, nil, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ti := &Identity{\n\t\t\t\tType:        tt.fields.Type,\n\t\t\t\tCertificate: tt.fields.Certificate,\n\t\t\t\tKey:         tt.fields.Key,\n\t\t\t\tHost:        tt.fields.Host,\n\t\t\t\tRoot:        tt.fields.Root,\n\t\t\t}\n\t\t\tfn := i.GetClientCertificateFunc()\n\t\t\tgot, err := fn(&tls.CertificateRequestInfo{})\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"Identity.GetClientCertificateFunc() = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"Identity.GetClientCertificateFunc() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestIdentity_GetCertPool(t *testing.T) {\n\ttype fields struct {\n\t\tType        string\n\t\tCertificate string\n\t\tKey         string\n\t\tHost        string\n\t\tRoot        string\n\t}\n\ttests := []struct {\n\t\tname         string\n\t\tfields       fields\n\t\twantSubjects [][]byte\n\t\twantErr      bool\n\t}{\n\t\t{\"ok\", fields{\"ttls\", \"testdata/identity/identity.crt\", \"testdata/identity/identity_key\", \"tunnel:443\", \"testdata/certs/root_ca.crt\"}, [][]byte{[]byte(\"0\\x1c1\\x1a0\\x18\\x06\\x03U\\x04\\x03\\x13\\x11Smallstep Root CA\")}, false},\n\t\t{\"ok nil\", fields{\"ttls\", \"testdata/identity/identity.crt\", \"testdata/identity/identity_key\", \"tunnel:443\", \"\"}, nil, false},\n\t\t{\"fail missing\", fields{\"ttls\", \"testdata/identity/identity.crt\", \"testdata/identity/identity_key\", \"tunnel:443\", \"testdata/certs/missing.crt\"}, nil, true},\n\t\t{\"fail no cert\", fields{\"ttls\", \"testdata/identity/identity.crt\", \"testdata/identity/identity_key\", \"tunnel:443\", \"testdata/secrets/root_ca_key\"}, nil, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ti := &Identity{\n\t\t\t\tType:        tt.fields.Type,\n\t\t\t\tCertificate: tt.fields.Certificate,\n\t\t\t\tKey:         tt.fields.Key,\n\t\t\t\tHost:        tt.fields.Host,\n\t\t\t\tRoot:        tt.fields.Root,\n\t\t\t}\n\t\t\tgot, err := i.GetCertPool()\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"Identity.GetCertPool() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif got != nil {\n\t\t\t\t//nolint:staticcheck // we don't have a different way to check\n\t\t\t\t// the certificates in the pool.\n\t\t\t\tsubjects := got.Subjects()\n\t\t\t\tif !reflect.DeepEqual(subjects, tt.wantSubjects) {\n\t\t\t\t\tt.Errorf(\"Identity.GetCertPool() = %x, want %x\", subjects, tt.wantSubjects)\n\t\t\t\t}\n\t\t\t}\n\n\t\t})\n\t}\n}\n\ntype renewer struct {\n\tpool *x509.CertPool\n\tsign *api.SignResponse\n\terr  error\n}\n\nfunc (r *renewer) GetRootCAs() *x509.CertPool {\n\treturn r.pool\n}\n\nfunc (r *renewer) Renew(http.RoundTripper) (*api.SignResponse, error) {\n\treturn r.sign, r.err\n}\n\nfunc TestIdentity_Renew(t *testing.T) {\n\ttmpDir := t.TempDir()\n\n\toldIdentityDir := identityDir\n\tidentityDir = returnInput(\"testdata/identity\")\n\tdefer func() {\n\t\tidentityDir = oldIdentityDir\n\t\tos.RemoveAll(tmpDir)\n\t}()\n\n\tcerts, err := pemutil.ReadCertificateBundle(\"testdata/identity/identity.crt\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tok := &renewer{\n\t\tsign: &api.SignResponse{\n\t\t\tServerPEM: api.Certificate{Certificate: certs[0]},\n\t\t\tCaPEM:     api.Certificate{Certificate: certs[1]},\n\t\t\tCertChainPEM: []api.Certificate{\n\t\t\t\t{Certificate: certs[0]},\n\t\t\t\t{Certificate: certs[1]},\n\t\t\t},\n\t\t},\n\t}\n\n\tokOld := &renewer{\n\t\tsign: &api.SignResponse{\n\t\t\tServerPEM: api.Certificate{Certificate: certs[0]},\n\t\t\tCaPEM:     api.Certificate{Certificate: certs[1]},\n\t\t},\n\t}\n\n\tfail := &renewer{\n\t\terr: fmt.Errorf(\"an error\"),\n\t}\n\n\ttype fields struct {\n\t\tType        string\n\t\tCertificate string\n\t\tKey         string\n\t}\n\ttype args struct {\n\t\tclient Renewer\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tprepare func()\n\t\tfields  fields\n\t\targs    args\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", func() {}, fields{\"mTLS\", \"testdata/identity/identity.crt\", \"testdata/identity/identity_key\"}, args{ok}, false},\n\t\t{\"ok old\", func() {}, fields{\"mTLS\", \"testdata/identity/identity.crt\", \"testdata/identity/identity_key\"}, args{okOld}, false},\n\t\t{\"ok disabled\", func() {}, fields{}, args{nil}, false},\n\t\t{\"fail type\", func() {}, fields{\"foo\", \"testdata/identity/identity.crt\", \"testdata/identity/identity_key\"}, args{ok}, true},\n\t\t{\"fail renew\", func() {}, fields{\"mTLS\", \"testdata/identity/identity.crt\", \"testdata/identity/identity_key\"}, args{fail}, true},\n\t\t{\"fail certificate\", func() {}, fields{\"mTLS\", \"testdata/certs/server.crt\", \"testdata/identity/identity_key\"}, args{ok}, true},\n\t\t{\"fail write identity\", func() {\n\t\t\tidentityDir = returnInput(filepath.Join(tmpDir, \"bad-dir\"))\n\t\t\tos.MkdirAll(identityDir(), 0600)\n\t\t}, fields{\"mTLS\", \"testdata/identity/identity.crt\", \"testdata/identity/identity_key\"}, args{ok}, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ttt.prepare()\n\t\t\ti := &Identity{\n\t\t\t\tType:        tt.fields.Type,\n\t\t\t\tCertificate: tt.fields.Certificate,\n\t\t\t\tKey:         tt.fields.Key,\n\t\t\t}\n\t\t\tif err := i.Renew(tt.args.client); (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"Identity.Renew() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "ca/identity/testdata/certs/intermediate_ca.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIBozCCAUqgAwIBAgIQF4UYp5uEiuq/BO0cOWTq9DAKBggqhkjOPQQDAjAcMRow\nGAYDVQQDExFTbWFsbHN0ZXAgUm9vdCBDQTAeFw0xOTEyMTIwMjQ1MThaFw0yOTEy\nMDkwMjQ1MThaMCQxIjAgBgNVBAMTGVNtYWxsc3RlcCBJbnRlcm1lZGlhdGUgQ0Ew\nWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQGECLvDj+ZSqW78DRmUaugh0EU4NQ5\nPoZxsLpB0gUsvNDGE0V5/2Q85GmsYzlBjBuoM+RfvF2fSP+dDTs3Hwjgo2YwZDAO\nBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQU95Au\nB82vrt2UJyDTNBQH3B8sePUwHwYDVR0jBBgwFoAUgwZucvb+H/1chTPLQ1GYTJwK\nCXQwCgYIKoZIzj0EAwIDRwAwRAIgSaHuI61rNsFf1ke5WSUyuqy51DIE/ONCSWKT\nVQgTVJMCIAMsE+Eibk43hL4qQi5vBJiFLfGQDDN/9HUi6w4w5EZ7\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "ca/identity/testdata/certs/root_ca.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIBfDCCASGgAwIBAgIQE8W0gyMruWxRDfegdPHrdDAKBggqhkjOPQQDAjAcMRow\nGAYDVQQDExFTbWFsbHN0ZXAgUm9vdCBDQTAeFw0xOTEyMTIwMjQ1MThaFw0yOTEy\nMDkwMjQ1MThaMBwxGjAYBgNVBAMTEVNtYWxsc3RlcCBSb290IENBMFkwEwYHKoZI\nzj0CAQYIKoZIzj0DAQcDQgAEgd74QbUDcEj3aV5Oxv5eAMzwnejj7S/iDFAp89t9\nkEb+Ux4NZC3Pay+92yRL//dBUI5WOopLXBniYomH4SFJg6NFMEMwDgYDVR0PAQH/\nBAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQEwHQYDVR0OBBYEFIMGbnL2/h/9XIUz\ny0NRmEycCgl0MAoGCCqGSM49BAMCA0kAMEYCIQD3/IUBL5/9Hpdp2+t4XnA42cwQ\nj5WkGY5hJIhdQ5P8qgIhAMf19nAIUlSbXKPf21Gv6eYEoNuuLfpcqnfBt5NJX64M\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "ca/identity/testdata/certs/server.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIICHDCCAcKgAwIBAgIQQ4n25nGGKm6uGyVQ4cDNCTAKBggqhkjOPQQDAjAkMSIw\nIAYDVQQDExlTbWFsbHN0ZXAgSW50ZXJtZWRpYXRlIENBMB4XDTE5MTIxMjAyNTAz\nOVoXDTI5MTIwOTAyNTAzOVowFjEUMBIGA1UEAxMLdGVzdCBzZXJ2ZXIwWTATBgcq\nhkjOPQIBBggqhkjOPQMBBwNCAATmQRMCzRP1hBcYhAXlbiyR9QtsQosQfCZTS+en\ng6TtL9VjWsQXqd1SSStfi0grPyiTQLIPhPbSho/VJzSpf59Do4HjMIHgMA4GA1Ud\nDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwHQYDVR0O\nBBYEFBvz34jDFrb3G4qiGkZZj99BnabAMB8GA1UdIwQYMBaAFPeQLgfNr67dlCcg\n0zQUB9wfLHj1MBoGA1UdEQQTMBGCCWxvY2FsaG9zdIcEfwAAATBTBgwrBgEEAYKk\nZMYoQAEEQzBBAgEBBA9qb2VAZXhhbXBsZS5jb20EKzJ3U05fQ21leFhXaWdfRG5w\nVlpzWUZkTUgxU3RjODZCSUJ6TjBydDVpcEUwCgYIKoZIzj0EAwIDSAAwRQIhAOt6\n/x9LWQyBtx3RcyyALF2//OCfGjAx0zLGmUsXIHGIAiAZGVwTxbhxiYU95AXncS3F\n3tXNaaIJyyO7atiVPhCR1A==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIBozCCAUqgAwIBAgIQF4UYp5uEiuq/BO0cOWTq9DAKBggqhkjOPQQDAjAcMRow\nGAYDVQQDExFTbWFsbHN0ZXAgUm9vdCBDQTAeFw0xOTEyMTIwMjQ1MThaFw0yOTEy\nMDkwMjQ1MThaMCQxIjAgBgNVBAMTGVNtYWxsc3RlcCBJbnRlcm1lZGlhdGUgQ0Ew\nWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQGECLvDj+ZSqW78DRmUaugh0EU4NQ5\nPoZxsLpB0gUsvNDGE0V5/2Q85GmsYzlBjBuoM+RfvF2fSP+dDTs3Hwjgo2YwZDAO\nBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQU95Au\nB82vrt2UJyDTNBQH3B8sePUwHwYDVR0jBBgwFoAUgwZucvb+H/1chTPLQ1GYTJwK\nCXQwCgYIKoZIzj0EAwIDRwAwRAIgSaHuI61rNsFf1ke5WSUyuqy51DIE/ONCSWKT\nVQgTVJMCIAMsE+Eibk43hL4qQi5vBJiFLfGQDDN/9HUi6w4w5EZ7\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "ca/identity/testdata/config/badIdentity.json",
    "content": "{\n    \"type\": \"\",\n    \"crt\": \"testdata/identity/identity.crt\",\n    \"key\": \"testdata/identity/identity_key\"\n}"
  },
  {
    "path": "ca/identity/testdata/config/badca.json",
    "content": "{\n   \"ca-url\": \":\",\n   \"ca-config\": \"testdata/config/ca.json\",\n   \"fingerprint\": \"9dc35eef23a234b2520516a3169090d7ec2fc61323bdd6e4fde08bcfec5d0931\",\n   \"root\": \"testdata/certs/root_ca.crt\"\n}"
  },
  {
    "path": "ca/identity/testdata/config/badroot.json",
    "content": "{\n   \"ca-url\": \"https://127.0.0.1\",\n   \"ca-config\": \"testdata/config/ca.json\",\n   \"fingerprint\": \"9dc35eef23a234b2520516a3169090d7ec2fc61323bdd6e4fde08bcfec5d0931\",\n   \"root\": \"testdata/certs/missing.crt\"\n}"
  },
  {
    "path": "ca/identity/testdata/config/ca.json",
    "content": "{\n   \"root\": \"testdata/certs/root_ca.crt\",\n   \"federatedRoots\": [],\n   \"crt\": \"testdata/certs/intermediate_ca.crt\",\n   \"key\": \"testdata/secrets/intermediate_ca_key\",\n   \"address\": \":443\",\n   \"dnsNames\": [\n      \"127.0.0.1\",\n      \"localhost\"\n   ],\n   \"logger\": {\n      \"format\": \"text\"\n   },\n   \"authority\": {\n      \"provisioners\": [\n         {\n            \"type\": \"jwk\",\n            \"name\": \"joe@example.com\",\n            \"key\": {\n               \"use\": \"sig\",\n               \"kty\": \"EC\",\n               \"kid\": \"2wSN_CmexXWig_DnpVZsYFdMH1Stc86BIBzN0rt5ipE\",\n               \"crv\": \"P-256\",\n               \"alg\": \"ES256\",\n               \"x\": \"QqYaIULUQqP0EOmogorCcQIxEtI7-zCRcUVFxyNwq4Q\",\n               \"y\": \"YeIMipM7uMHjlxpFIUbfCBC1xEXczXNYRzJCMyrGcH0\"\n            },\n            \"encryptedKey\": \"eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJjdHkiOiJqd2sranNvbiIsImVuYyI6IkEyNTZHQ00iLCJwMmMiOjEwMDAwMCwicDJzIjoiSVQ3MVNUMTNNMTd1S3Y4VHRDczYyUSJ9.TXShNLPcITS0bFvQeMjjCDhQLICQs1ShECkgUkUsAm9ZWpSq6Yu03w.SWxtxscivS3L5Yo5.O-XY9YKK8wEJgVs7X1-FxiM_6w4s7iJQNXRD2JrZRsXtDqUz7diPfXuBOFPUFsNzykvob1qCsU4B23Ek2nbaS2HqPrIOGbOvOsR8Pt6kNoraH1QDp3Hyzkv0S-VGM0MCGYDDmmH33PZmsdS36Aw8v9xBnDHlwlMg4NjTskxpqggfQl01433B0lCJqJdrmeBeGL1ZCKixvc-wAQxU8GH5iiD925ViLY7RlVo-tmIBXpxRgheLgKiuMxmgPvf15qCdgU5TRqeuJbYJLzvPpoai0W4WHjpM1zLjjmp5OYRFW4m4ZRZf5g1Cm4lstFPUlTn85fkMZFdBh4_bFbjAv7k.epXp8DZKHj_dxP9EohwDIg\"\n         }\n      ]\n   },\n   \"tls\": {\n      \"cipherSuites\": [\n         \"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305\",\n         \"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256\"\n      ],\n      \"minVersion\": 1.2,\n      \"maxVersion\": 1.2,\n      \"renegotiation\": false\n   }\n}"
  },
  {
    "path": "ca/identity/testdata/config/defaults.json",
    "content": "{\n   \"ca-url\": \"https://127.0.0.1\",\n   \"ca-config\": \"testdata/config/ca.json\",\n   \"fingerprint\": \"9dc35eef23a234b2520516a3169090d7ec2fc61323bdd6e4fde08bcfec5d0931\",\n   \"root\": \"testdata/certs/root_ca.crt\"\n}"
  },
  {
    "path": "ca/identity/testdata/config/fail.json",
    "content": "This is not a json file"
  },
  {
    "path": "ca/identity/testdata/config/identity.json",
    "content": "{\n    \"type\": \"mTLS\",\n    \"crt\": \"testdata/identity/identity.crt\",\n    \"key\": \"testdata/identity/identity_key\"\n}"
  },
  {
    "path": "ca/identity/testdata/config/tunnel.json",
    "content": "{\n    \"type\": \"mTLS\",\n    \"crt\": \"testdata/identity/identity.crt\",\n    \"key\": \"testdata/identity/identity_key\",\n    \"host\": \"tunnel:443\",\n    \"root\": \"testdata/certs/root_ca.crt\"\n}"
  },
  {
    "path": "ca/identity/testdata/identity/expired.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIICIDCCAcegAwIBAgIRAM1GK1TLmvWLVOjP0dqVCiEwCgYIKoZIzj0EAwIwJDEi\nMCAGA1UEAxMZU21hbGxzdGVwIEludGVybWVkaWF0ZSBDQTAeFw0xODEyMTIwMzI2\nMzZaFw0xODEyMTMwMzI2MzZaMBoxGDAWBgNVBAMMD2pvZUBleGFtcGxlLmNvbTBZ\nMBMGByqGSM49AgEGCCqGSM49AwEHA0IABI0+NSjg3+vGhAeZGrxPksrXFqq0AIUB\nD3nQPmGPuUWIEmbt6qp3EVF/o+KwzWgDv5fzBmDlBkdBRz9xc3XIcQ2jgeMwgeAw\nHwYDVR0jBBgwFoAU95AuB82vrt2UJyDTNBQH3B8sePUwDgYDVR0PAQH/BAQDAgWg\nMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAdBgNVHQ4EFgQU1Ht6zX2M\neVXcnxhM4hxU0RCblNowGgYDVR0RBBMwEYEPam9lQGV4YW1wbGUuY29tMFMGDCsG\nAQQBgqRkxihAAQRDMEECAQEED2pvZUBleGFtcGxlLmNvbQQrMndTTl9DbWV4WFdp\nZ19EbnBWWnNZRmRNSDFTdGM4NkJJQnpOMHJ0NWlwRTAKBggqhkjOPQQDAgNHADBE\nAiBgoPACCRJ6s+C5Yz3BWeyM6VnWewctnaMsVJKyPdb98AIgV/7HRZsc5Xgi8iVt\nD4XxVOZDu/y1V4VIH5W4INfg6JA=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIBozCCAUqgAwIBAgIQF4UYp5uEiuq/BO0cOWTq9DAKBggqhkjOPQQDAjAcMRow\nGAYDVQQDExFTbWFsbHN0ZXAgUm9vdCBDQTAeFw0xOTEyMTIwMjQ1MThaFw0yOTEy\nMDkwMjQ1MThaMCQxIjAgBgNVBAMTGVNtYWxsc3RlcCBJbnRlcm1lZGlhdGUgQ0Ew\nWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQGECLvDj+ZSqW78DRmUaugh0EU4NQ5\nPoZxsLpB0gUsvNDGE0V5/2Q85GmsYzlBjBuoM+RfvF2fSP+dDTs3Hwjgo2YwZDAO\nBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQU95Au\nB82vrt2UJyDTNBQH3B8sePUwHwYDVR0jBBgwFoAUgwZucvb+H/1chTPLQ1GYTJwK\nCXQwCgYIKoZIzj0EAwIDRwAwRAIgSaHuI61rNsFf1ke5WSUyuqy51DIE/ONCSWKT\nVQgTVJMCIAMsE+Eibk43hL4qQi5vBJiFLfGQDDN/9HUi6w4w5EZ7\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "ca/identity/testdata/identity/identity.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIICHzCCAcagAwIBAgIQfVgJ4dZ2AhS88uthvlIzyjAKBggqhkjOPQQDAjAkMSIw\nIAYDVQQDExlTbWFsbHN0ZXAgSW50ZXJtZWRpYXRlIENBMB4XDTE5MTIxMjAyNDgy\nMVoXDTI5MTIwOTAyNDgyMVowGjEYMBYGA1UEAwwPam9lQGV4YW1wbGUuY29tMFkw\nEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEjT41KODf68aEB5kavE+SytcWqrQAhQEP\nedA+YY+5RYgSZu3qqncRUX+j4rDNaAO/l/MGYOUGR0FHP3FzdchxDaOB4zCB4DAO\nBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMB0G\nA1UdDgQWBBTUe3rNfYx5VdyfGEziHFTREJuU2jAfBgNVHSMEGDAWgBT3kC4Hza+u\n3ZQnINM0FAfcHyx49TAaBgNVHREEEzARgQ9qb2VAZXhhbXBsZS5jb20wUwYMKwYB\nBAGCpGTGKEABBEMwQQIBAQQPam9lQGV4YW1wbGUuY29tBCsyd1NOX0NtZXhYV2ln\nX0RucFZac1lGZE1IMVN0Yzg2QklCek4wcnQ1aXBFMAoGCCqGSM49BAMCA0cAMEQC\nIHkYnKUBrXc/GIosKgnhHqVeRMi2O1JhnZdTE1uoy2C0AiA9ZrmGqPvpQ86f5yq5\nllsieqBTzIum6A45q0/4XeN3QA==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIBozCCAUqgAwIBAgIQF4UYp5uEiuq/BO0cOWTq9DAKBggqhkjOPQQDAjAcMRow\nGAYDVQQDExFTbWFsbHN0ZXAgUm9vdCBDQTAeFw0xOTEyMTIwMjQ1MThaFw0yOTEy\nMDkwMjQ1MThaMCQxIjAgBgNVBAMTGVNtYWxsc3RlcCBJbnRlcm1lZGlhdGUgQ0Ew\nWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQGECLvDj+ZSqW78DRmUaugh0EU4NQ5\nPoZxsLpB0gUsvNDGE0V5/2Q85GmsYzlBjBuoM+RfvF2fSP+dDTs3Hwjgo2YwZDAO\nBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQU95Au\nB82vrt2UJyDTNBQH3B8sePUwHwYDVR0jBBgwFoAUgwZucvb+H/1chTPLQ1GYTJwK\nCXQwCgYIKoZIzj0EAwIDRwAwRAIgSaHuI61rNsFf1ke5WSUyuqy51DIE/ONCSWKT\nVQgTVJMCIAMsE+Eibk43hL4qQi5vBJiFLfGQDDN/9HUi6w4w5EZ7\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "ca/identity/testdata/identity/identity_key",
    "content": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIJ4A5QcJioS5I89uT/hkuWPy/nlW5qy8vM8Tm2sgUCDyoAoGCCqGSM49\nAwEHoUQDQgAEjT41KODf68aEB5kavE+SytcWqrQAhQEPedA+YY+5RYgSZu3qqncR\nUX+j4rDNaAO/l/MGYOUGR0FHP3FzdchxDQ==\n-----END EC PRIVATE KEY-----\n"
  },
  {
    "path": "ca/identity/testdata/identity/not_before.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIICIDCCAcagAwIBAgIQHRUI8eJv55I9/5IHi1mpmjAKBggqhkjOPQQDAjAkMSIw\nIAYDVQQDExlTbWFsbHN0ZXAgSW50ZXJtZWRpYXRlIENBMB4XDTI5MTIwOTAzMzAx\nNFoXDTI5MTIxMDAzMzAxNFowGjEYMBYGA1UEAwwPam9lQGV4YW1wbGUuY29tMFkw\nEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEjT41KODf68aEB5kavE+SytcWqrQAhQEP\nedA+YY+5RYgSZu3qqncRUX+j4rDNaAO/l/MGYOUGR0FHP3FzdchxDaOB4zCB4DAf\nBgNVHSMEGDAWgBT3kC4Hza+u3ZQnINM0FAfcHyx49TAOBgNVHQ8BAf8EBAMCBaAw\nHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMB0GA1UdDgQWBBTUe3rNfYx5\nVdyfGEziHFTREJuU2jAaBgNVHREEEzARgQ9qb2VAZXhhbXBsZS5jb20wUwYMKwYB\nBAGCpGTGKEABBEMwQQIBAQQPam9lQGV4YW1wbGUuY29tBCsyd1NOX0NtZXhYV2ln\nX0RucFZac1lGZE1IMVN0Yzg2QklCek4wcnQ1aXBFMAoGCCqGSM49BAMCA0gAMEUC\nIQDJVzxQ0lY9+haZLs5qxhbaWoTmXwCbYdkwhThDfM/izwIgRZCmshc1flfimIPO\neblT85Gk16ND/diV6pmtUaMT73I=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIBozCCAUqgAwIBAgIQF4UYp5uEiuq/BO0cOWTq9DAKBggqhkjOPQQDAjAcMRow\nGAYDVQQDExFTbWFsbHN0ZXAgUm9vdCBDQTAeFw0xOTEyMTIwMjQ1MThaFw0yOTEy\nMDkwMjQ1MThaMCQxIjAgBgNVBAMTGVNtYWxsc3RlcCBJbnRlcm1lZGlhdGUgQ0Ew\nWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQGECLvDj+ZSqW78DRmUaugh0EU4NQ5\nPoZxsLpB0gUsvNDGE0V5/2Q85GmsYzlBjBuoM+RfvF2fSP+dDTs3Hwjgo2YwZDAO\nBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQU95Au\nB82vrt2UJyDTNBQH3B8sePUwHwYDVR0jBBgwFoAUgwZucvb+H/1chTPLQ1GYTJwK\nCXQwCgYIKoZIzj0EAwIDRwAwRAIgSaHuI61rNsFf1ke5WSUyuqy51DIE/ONCSWKT\nVQgTVJMCIAMsE+Eibk43hL4qQi5vBJiFLfGQDDN/9HUi6w4w5EZ7\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "ca/identity/testdata/secrets/intermediate_ca_key",
    "content": "-----BEGIN EC PRIVATE KEY-----\nProc-Type: 4,ENCRYPTED\nDEK-Info: AES-256-CBC,37e3019a1aa420225bbd4f342a3ce330\n\n3SNIIXzE11cGKTPnErv8S1HIrd2lbQo+lsMT9GrU33GAi/MTvp0hx0txy7E3CsrU\nDbuPXs3zLCjgoNLOeyAWLqGjPLRt4YNnZGVDi3F/dFUAWxgXH8gZQ2d9ZqAXwxdd\nbhT4ZcRFgFzCPlHExtxBrJe+Tmeuq1HqD+8gpOSYbt0=\n-----END EC PRIVATE KEY-----\n"
  },
  {
    "path": "ca/identity/testdata/secrets/root_ca_key",
    "content": "-----BEGIN EC PRIVATE KEY-----\nProc-Type: 4,ENCRYPTED\nDEK-Info: AES-256-CBC,48fc92ab6885b2377d8bbac5b035bde2\n\nBE07EXlLmJbAfjt2c9GwQoTT07DzjLWgiGWqxMKC0bOLQdmHe2pFudeQldDhTOme\nxnr9rRj9h+GRWV+sIzp+ilGd4/F6lfzWMl44GA5y7uBNWKhnI1uB9m9oo69hBNRg\ndQuDmAx5EWXvg7Mgg1MQZIPY8539RXWJdAs+uRSI12g=\n-----END EC PRIVATE KEY-----\n"
  },
  {
    "path": "ca/identity/testdata/secrets/server_key",
    "content": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIIGgfuMfx7h1VaCYzzEPZhrbTLsAr6dtyuQ2RLl6jKqBoAoGCCqGSM49\nAwEHoUQDQgAE5kETAs0T9YQXGIQF5W4skfULbEKLEHwmU0vnp4Ok7S/VY1rEF6nd\nUkkrX4tIKz8ok0CyD4T20oaP1Sc0qX+fQw==\n-----END EC PRIVATE KEY-----\n"
  },
  {
    "path": "ca/mutable_tls_config.go",
    "content": "package ca\n\nimport (\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"sync\"\n\n\t\"github.com/smallstep/certificates/api\"\n)\n\n// mutableTLSConfig allows to use a tls.Config with mutable cert pools.\ntype mutableTLSConfig struct {\n\tsync.RWMutex\n\tconfig         *tls.Config\n\tclientCerts    []*x509.Certificate\n\trootCerts      []*x509.Certificate\n\tmutClientCerts []*x509.Certificate\n\tmutRootCerts   []*x509.Certificate\n}\n\n// newMutableTLSConfig creates a new mutableTLSConfig that will be later\n// initialized with a tls.Config.\nfunc newMutableTLSConfig() *mutableTLSConfig {\n\treturn &mutableTLSConfig{\n\t\tclientCerts:    []*x509.Certificate{},\n\t\trootCerts:      []*x509.Certificate{},\n\t\tmutClientCerts: []*x509.Certificate{},\n\t\tmutRootCerts:   []*x509.Certificate{},\n\t}\n}\n\n// Init initializes the mutable tls.Config with the given tls.Config.\nfunc (c *mutableTLSConfig) Init(base *tls.Config) {\n\tc.Lock()\n\tc.config = base.Clone()\n\tc.Unlock()\n}\n\n// TLSConfig returns the updated tls.Config it it has changed. It's used in the\n// tls.Config GetConfigForClient.\nfunc (c *mutableTLSConfig) TLSConfig() (config *tls.Config) {\n\tc.RLock()\n\tconfig = c.config.Clone()\n\tc.RUnlock()\n\treturn\n}\n\n// Reload reloads the tls.Config with the new CAs.\nfunc (c *mutableTLSConfig) Reload() {\n\t// Prepare new pools\n\tc.RLock()\n\trootCAs := x509.NewCertPool()\n\tclientCAs := x509.NewCertPool()\n\t// Fixed certs\n\tfor _, cert := range c.rootCerts {\n\t\trootCAs.AddCert(cert)\n\t}\n\tfor _, cert := range c.clientCerts {\n\t\tclientCAs.AddCert(cert)\n\t}\n\t// Mutable certs\n\tfor _, cert := range c.mutRootCerts {\n\t\trootCAs.AddCert(cert)\n\t}\n\tfor _, cert := range c.mutClientCerts {\n\t\tclientCAs.AddCert(cert)\n\t}\n\tc.RUnlock()\n\n\t// Set new pool\n\tc.Lock()\n\tc.config.RootCAs = rootCAs\n\tc.config.ClientCAs = clientCAs\n\tc.mutRootCerts = []*x509.Certificate{}\n\tc.mutClientCerts = []*x509.Certificate{}\n\tc.Unlock()\n}\n\n// AddImmutableClientCACert add an immutable cert to ClientCAs.\nfunc (c *mutableTLSConfig) AddImmutableClientCACert(cert *x509.Certificate) {\n\tc.Lock()\n\tc.clientCerts = append(c.clientCerts, cert)\n\tc.Unlock()\n}\n\n// AddImmutableRootCACert add an immutable cert to RootCas.\nfunc (c *mutableTLSConfig) AddImmutableRootCACert(cert *x509.Certificate) {\n\tc.Lock()\n\tc.rootCerts = append(c.rootCerts, cert)\n\tc.Unlock()\n}\n\n// AddClientCAs add mutable certs to ClientCAs.\nfunc (c *mutableTLSConfig) AddClientCAs(certs []api.Certificate) {\n\tc.Lock()\n\tfor _, cert := range certs {\n\t\tc.mutClientCerts = append(c.mutClientCerts, cert.Certificate)\n\t}\n\tc.Unlock()\n}\n\n// AddRootCAs add mutable certs to RootCAs.\nfunc (c *mutableTLSConfig) AddRootCAs(certs []api.Certificate) {\n\tc.Lock()\n\tfor _, cert := range certs {\n\t\tc.mutRootCerts = append(c.mutRootCerts, cert.Certificate)\n\t}\n\tc.Unlock()\n}\n"
  },
  {
    "path": "ca/provisioner.go",
    "content": "package ca\n\nimport (\n\t\"encoding/json\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n\n\t\"github.com/smallstep/cli-utils/token\"\n\t\"github.com/smallstep/cli-utils/token/provision\"\n\t\"go.step.sm/crypto/jose\"\n\t\"go.step.sm/crypto/randutil\"\n\n\t\"github.com/smallstep/certificates/authority/provisioner\"\n)\n\nconst tokenLifetime = 5 * time.Minute\n\n// Provisioner is an authorized entity that can sign tokens necessary for\n// signature requests.\ntype Provisioner struct {\n\t*Client\n\tname          string\n\tkid           string\n\taudience      string\n\tsshAudience   string\n\tfingerprint   string\n\tjwk           *jose.JSONWebKey\n\ttokenLifetime time.Duration\n}\n\n// NewProvisioner loads and decrypts key material from the CA for the named\n// provisioner. The key identified by `kid` will be used if specified. If `kid`\n// is the empty string we'll use the first key for the named provisioner that\n// decrypts using `password`.\nfunc NewProvisioner(name, kid, caURL string, password []byte, opts ...ClientOption) (*Provisioner, error) {\n\tclient, err := NewClient(caURL, opts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Get the fingerprint of the current connection\n\tfp, err := client.RootFingerprint()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar jwk *jose.JSONWebKey\n\tswitch {\n\tcase name == \"\":\n\t\treturn nil, errors.New(\"provisioner name cannot be empty\")\n\tcase kid == \"\":\n\t\tjwk, err = loadProvisionerJWKByName(client, name, password)\n\tdefault:\n\t\tjwk, err = loadProvisionerJWKByKid(client, kid, password)\n\t}\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &Provisioner{\n\t\tClient:        client,\n\t\tname:          name,\n\t\tkid:           jwk.KeyID,\n\t\taudience:      client.endpoint.ResolveReference(&url.URL{Path: \"/1.0/sign\"}).String(),\n\t\tsshAudience:   client.endpoint.ResolveReference(&url.URL{Path: \"/1.0/ssh/sign\"}).String(),\n\t\tfingerprint:   fp,\n\t\tjwk:           jwk,\n\t\ttokenLifetime: tokenLifetime,\n\t}, nil\n}\n\n// Name returns the provisioner's name.\nfunc (p *Provisioner) Name() string {\n\treturn p.name\n}\n\n// Kid returns the provisioners key ID.\nfunc (p *Provisioner) Kid() string {\n\treturn p.kid\n}\n\n// Fingerprint root certificate fingerprint.\nfunc (p *Provisioner) Fingerprint() string {\n\treturn p.fingerprint\n}\n\n// Audience returns the audience for tokens used with X.509 certificates.\nfunc (p *Provisioner) Audience() string {\n\treturn p.audience\n}\n\n// SSHAudience returns audience used with SSH certificates.\nfunc (p *Provisioner) SSHAudience() string {\n\treturn p.sshAudience\n}\n\n// SetFingerprint overwrites the default fingerprint used.\nfunc (p *Provisioner) SetFingerprint(sum string) {\n\tp.fingerprint = sum\n}\n\n// SetAudience overwrites the default audience used with X.509 certificates.\nfunc (p *Provisioner) SetAudience(s string) {\n\tp.audience = s\n}\n\n// SetSSHAudience overwrites the default audience used with SSH certificates.\nfunc (p *Provisioner) SetSSHAudience(s string) {\n\tp.sshAudience = s\n}\n\n// Token generates a bootstrap token for a subject.\nfunc (p *Provisioner) Token(subject string, sans ...string) (string, error) {\n\tif len(sans) == 0 {\n\t\tsans = []string{subject}\n\t}\n\n\t// A random jwt id will be used to identify duplicated tokens\n\tjwtID, err := randutil.Hex(64) // 256 bits\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tnotBefore := time.Now()\n\tnotAfter := notBefore.Add(tokenLifetime)\n\ttokOptions := []token.Options{\n\t\ttoken.WithJWTID(jwtID),\n\t\ttoken.WithKid(p.kid),\n\t\ttoken.WithIssuer(p.name),\n\t\ttoken.WithAudience(p.audience),\n\t\ttoken.WithValidity(notBefore, notAfter),\n\t\ttoken.WithSANS(sans),\n\t}\n\n\tif p.fingerprint != \"\" {\n\t\ttokOptions = append(tokOptions, token.WithSHA(p.fingerprint))\n\t}\n\n\ttok, err := provision.New(subject, tokOptions...)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn tok.SignedString(p.jwk.Algorithm, p.jwk.Key)\n}\n\n// SSHToken generates a SSH token.\nfunc (p *Provisioner) SSHToken(certType, keyID string, principals []string) (string, error) {\n\tjwtID, err := randutil.Hex(64)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tnotBefore := time.Now()\n\tnotAfter := notBefore.Add(tokenLifetime)\n\ttokOptions := []token.Options{\n\t\ttoken.WithJWTID(jwtID),\n\t\ttoken.WithKid(p.kid),\n\t\ttoken.WithIssuer(p.name),\n\t\ttoken.WithAudience(p.sshAudience),\n\t\ttoken.WithValidity(notBefore, notAfter),\n\t\ttoken.WithSSH(provisioner.SignSSHOptions{\n\t\t\tCertType:   certType,\n\t\t\tPrincipals: principals,\n\t\t\tKeyID:      keyID,\n\t\t}),\n\t}\n\n\tif p.fingerprint != \"\" {\n\t\ttokOptions = append(tokOptions, token.WithSHA(p.fingerprint))\n\t}\n\n\ttok, err := provision.New(keyID, tokOptions...)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn tok.SignedString(p.jwk.Algorithm, p.jwk.Key)\n}\n\nfunc decryptProvisionerJWK(encryptedKey string, password []byte) (*jose.JSONWebKey, error) {\n\tenc, err := jose.ParseEncrypted(encryptedKey)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"error parsing provisioner encrypted key\")\n\t}\n\tdata, err := enc.Decrypt(password)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"error decrypting provisioner key with provided password\")\n\t}\n\tjwk := new(jose.JSONWebKey)\n\tif err := json.Unmarshal(data, jwk); err != nil {\n\t\treturn nil, errors.Wrap(err, \"error unmarshaling provisioning key\")\n\t}\n\treturn jwk, nil\n}\n\n// loadProvisionerJWKByKid retrieves a provisioner key from the CA by key ID and\n// decrypts it using the specified password.\nfunc loadProvisionerJWKByKid(client *Client, kid string, password []byte) (*jose.JSONWebKey, error) {\n\tencrypted, err := getProvisionerKey(client, kid)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn decryptProvisionerJWK(encrypted, password)\n}\n\n// loadProvisionerJWKByName retrieves the list of provisioners and encrypted key then\n// returns the key of the first provisioner with a matching name that can be successfully\n// decrypted with the specified password.\nfunc loadProvisionerJWKByName(client *Client, name string, password []byte) (*jose.JSONWebKey, error) {\n\tprovisioners, err := getProvisioners(client)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"error getting the provisioners\")\n\t}\n\n\tfor _, provisioner := range provisioners {\n\t\tif provisioner.GetName() == name {\n\t\t\tif _, encryptedKey, ok := provisioner.GetEncryptedKey(); ok {\n\t\t\t\tif key, err := decryptProvisionerJWK(encryptedKey, password); err == nil {\n\t\t\t\t\treturn key, nil\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn nil, errors.Errorf(\"provisioner '%s' not found (or your password is wrong)\", name)\n}\n\n// getProvisioners returns the list of provisioners using the configured client.\nfunc getProvisioners(client *Client) (provisioner.List, error) {\n\tvar cursor string\n\tvar provisioners provisioner.List\n\tfor {\n\t\tresp, err := client.Provisioners(WithProvisionerCursor(cursor), WithProvisionerLimit(100))\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tprovisioners = append(provisioners, resp.Provisioners...)\n\t\tif resp.NextCursor == \"\" {\n\t\t\treturn provisioners, nil\n\t\t}\n\t\tcursor = resp.NextCursor\n\t}\n}\n\n// getProvisionerKey returns the encrypted provisioner key for the given kid.\nfunc getProvisionerKey(client *Client, kid string) (string, error) {\n\tresp, err := client.ProvisionerKey(kid)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn resp.Key, nil\n}\n"
  },
  {
    "path": "ca/provisioner_test.go",
    "content": "package ca\n\nimport (\n\t\"net/url\"\n\t\"os\"\n\t\"reflect\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"go.step.sm/crypto/jose\"\n\t\"go.step.sm/crypto/pemutil\"\n\t\"go.step.sm/crypto/x509util\"\n)\n\nfunc getTestProvisioner(t *testing.T, caURL string) *Provisioner {\n\tjwk, err := jose.ReadKey(\"testdata/secrets/ott_mariano_priv.jwk\", jose.WithPassword([]byte(\"password\")))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tcert, err := pemutil.ReadCertificate(\"testdata/secrets/root_ca.crt\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tclient, err := NewClient(caURL, WithRootFile(\"testdata/secrets/root_ca.crt\"))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\treturn &Provisioner{\n\t\tClient:        client,\n\t\tname:          \"mariano\",\n\t\tkid:           \"FLIV7q23CXHrg75J2OSbvzwKJJqoxCYixjmsJirneOg\",\n\t\taudience:      client.endpoint.ResolveReference(&url.URL{Path: \"/1.0/sign\"}).String(),\n\t\tsshAudience:   client.endpoint.ResolveReference(&url.URL{Path: \"/1.0/ssh/sign\"}).String(),\n\t\tfingerprint:   x509util.Fingerprint(cert),\n\t\tjwk:           jwk,\n\t\ttokenLifetime: 5 * time.Minute,\n\t}\n}\n\nfunc mustParseSigned(t *testing.T, tok string, key, dest any) {\n\tt.Helper()\n\n\tjwt, err := jose.ParseSigned(tok)\n\trequire.NoError(t, err)\n\trequire.NoError(t, jwt.Claims(key, dest))\n}\n\nfunc TestNewProvisioner(t *testing.T) {\n\tca := startCATestServer(t)\n\tdefer ca.Close()\n\twant := getTestProvisioner(t, ca.URL)\n\n\tcaBundle, err := os.ReadFile(\"testdata/secrets/root_ca.crt\")\n\trequire.NoError(t, err)\n\n\ttype args struct {\n\t\tname         string\n\t\tkid          string\n\t\tcaURL        string\n\t\tpassword     []byte\n\t\tclientOption ClientOption\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\targs    args\n\t\twant    *Provisioner\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", args{want.name, want.kid, ca.URL, []byte(\"password\"), WithRootFile(\"testdata/secrets/root_ca.crt\")}, want, false},\n\t\t{\"ok-by-name\", args{want.name, \"\", ca.URL, []byte(\"password\"), WithRootFile(\"testdata/secrets/root_ca.crt\")}, want, false},\n\t\t{\"ok-with-bundle\", args{want.name, want.kid, ca.URL, []byte(\"password\"), WithCABundle(caBundle)}, want, false},\n\t\t{\"ok-with-fingerprint\", args{want.name, want.kid, ca.URL, []byte(\"password\"), WithRootSHA256(want.fingerprint)}, want, false},\n\t\t{\"fail-bad-kid\", args{want.name, \"bad-kid\", ca.URL, []byte(\"password\"), WithRootFile(\"testdata/secrets/root_ca.crt\")}, nil, true},\n\t\t{\"fail-empty-name\", args{\"\", want.kid, ca.URL, []byte(\"password\"), WithRootFile(\"testdata/secrets/root_ca.crt\")}, nil, true},\n\t\t{\"fail-bad-name\", args{\"bad-name\", \"\", ca.URL, []byte(\"password\"), WithRootFile(\"testdata/secrets/root_ca.crt\")}, nil, true},\n\t\t{\"fail-by-password\", args{want.name, want.kid, ca.URL, []byte(\"bad-password\"), WithRootFile(\"testdata/secrets/root_ca.crt\")}, nil, true},\n\t\t{\"fail-by-password-no-kid\", args{want.name, \"\", ca.URL, []byte(\"bad-password\"), WithRootFile(\"testdata/secrets/root_ca.crt\")}, nil, true},\n\t\t{\"fail-bad-certificate\", args{want.name, want.kid, ca.URL, []byte(\"password\"), WithRootFile(\"testdata/secrets/federated_ca.crt\")}, nil, true},\n\t\t{\"fail-not-found-certificate\", args{want.name, want.kid, ca.URL, []byte(\"password\"), WithRootFile(\"testdata/secrets/missing.crt\")}, nil, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := NewProvisioner(tt.args.name, tt.args.kid, tt.args.caURL, tt.args.password, tt.args.clientOption)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"NewProvisioner() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\t// Client won't match.\n\t\t\t// Make sure it does.\n\t\t\tif got != nil {\n\t\t\t\tgot.Client = want.Client\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"NewProvisioner() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestProvisioner_Getters(t *testing.T) {\n\tp := getTestProvisioner(t, \"https://127.0.0.1:9000\")\n\tif got := p.Name(); got != p.name {\n\t\tt.Errorf(\"Provisioner.Name() = %v, want %v\", got, p.name)\n\t}\n\tif got := p.Kid(); got != p.kid {\n\t\tt.Errorf(\"Provisioner.Kid() = %v, want %v\", got, p.kid)\n\t}\n\tif got := p.Fingerprint(); got != p.fingerprint {\n\t\tt.Errorf(\"Provisioner.Fingerprint() = %v, want %v\", got, p.kid)\n\t}\n\tif got := p.Audience(); got != p.audience {\n\t\tt.Errorf(\"Provisioner.Audience() = %v, want %v\", got, p.kid)\n\t}\n\tif got := p.SSHAudience(); got != p.sshAudience {\n\t\tt.Errorf(\"Provisioner.SSHAudience() = %v, want %v\", got, p.kid)\n\t}\n}\n\nfunc TestProvisioner_Setters(t *testing.T) {\n\tp := getTestProvisioner(t, \"https://127.0.0.1:9000\")\n\tu, err := url.Parse(p.GetCaURL())\n\trequire.NoError(t, err)\n\n\tp.SetFingerprint(\"71498a347624fe99e1baff52d57d04a75be9c695c67bd6b0a08903e809f7497d\")\n\tp.SetAudience(u.ResolveReference(&url.URL{Path: \"/1.0/revoke\"}).String())\n\tp.SetSSHAudience(u.ResolveReference(&url.URL{Path: \"/1.0/ssh/revoke\"}).String())\n\n\ttok, err := p.Token(\"test@example.com\")\n\trequire.NoError(t, err)\n\tclaims := make(map[string]any)\n\tmustParseSigned(t, tok, p.jwk.Public(), &claims)\n\tassert.Equal(t, \"71498a347624fe99e1baff52d57d04a75be9c695c67bd6b0a08903e809f7497d\", claims[\"sha\"])\n\tassert.Equal(t, \"https://127.0.0.1:9000/1.0/revoke\", claims[\"aud\"])\n\n\ttok, err = p.SSHToken(\"user\", \"test@example.com\", []string{\"test\"})\n\trequire.NoError(t, err)\n\tclaims = make(map[string]any)\n\tmustParseSigned(t, tok, p.jwk.Public(), &claims)\n\tassert.Equal(t, \"71498a347624fe99e1baff52d57d04a75be9c695c67bd6b0a08903e809f7497d\", claims[\"sha\"])\n\tassert.Equal(t, \"https://127.0.0.1:9000/1.0/ssh/revoke\", claims[\"aud\"])\n}\n\nfunc TestProvisioner_Token(t *testing.T) {\n\tp := getTestProvisioner(t, \"https://127.0.0.1:9000\")\n\tsha := \"ef742f95dc0d8aa82d3cca4017af6dac3fce84290344159891952d18c53eefe7\"\n\n\ttype fields struct {\n\t\tname          string\n\t\tkid           string\n\t\tfingerprint   string\n\t\tjwk           *jose.JSONWebKey\n\t\ttokenLifetime time.Duration\n\t}\n\ttype args struct {\n\t\tsubject string\n\t\tsans    []string\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tfields  fields\n\t\targs    args\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", fields{p.name, p.kid, sha, p.jwk, p.tokenLifetime}, args{\"subject\", nil}, false},\n\t\t{\"ok-with-san\", fields{p.name, p.kid, sha, p.jwk, p.tokenLifetime}, args{\"subject\", []string{\"foo.smallstep.com\"}}, false},\n\t\t{\"ok-with-sans\", fields{p.name, p.kid, sha, p.jwk, p.tokenLifetime}, args{\"subject\", []string{\"foo.smallstep.com\", \"127.0.0.1\"}}, false},\n\t\t{\"fail-no-subject\", fields{p.name, p.kid, sha, p.jwk, p.tokenLifetime}, args{\"\", []string{\"foo.smallstep.com\"}}, true},\n\t\t{\"fail-no-key\", fields{p.name, p.kid, sha, &jose.JSONWebKey{}, p.tokenLifetime}, args{\"subject\", nil}, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tp := &Provisioner{\n\t\t\t\tname:          tt.fields.name,\n\t\t\t\tkid:           tt.fields.kid,\n\t\t\t\taudience:      \"https://127.0.0.1:9000/1.0/sign\",\n\t\t\t\tfingerprint:   tt.fields.fingerprint,\n\t\t\t\tjwk:           tt.fields.jwk,\n\t\t\t\ttokenLifetime: tt.fields.tokenLifetime,\n\t\t\t}\n\t\t\tgot, err := p.Token(tt.args.subject, tt.args.sans...)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"Provisioner.Token() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif tt.wantErr == false {\n\t\t\t\tjwt, err := jose.ParseSigned(got)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Error(err)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tvar claims jose.Claims\n\t\t\t\tif err := jwt.Claims(tt.fields.jwk.Public(), &claims); err != nil {\n\t\t\t\t\tt.Error(err)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tif err := claims.ValidateWithLeeway(jose.Expected{\n\t\t\t\t\tAudience: []string{\"https://127.0.0.1:9000/1.0/sign\"},\n\t\t\t\t\tIssuer:   tt.fields.name,\n\t\t\t\t\tSubject:  tt.args.subject,\n\t\t\t\t\tTime:     time.Now().UTC(),\n\t\t\t\t}, time.Minute); err != nil {\n\t\t\t\t\tt.Error(err)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tlifetime := claims.Expiry.Time().Sub(claims.NotBefore.Time())\n\t\t\t\tif lifetime != tt.fields.tokenLifetime {\n\t\t\t\t\tt.Errorf(\"Claims token life time = %s, want %s\", lifetime, tt.fields.tokenLifetime)\n\t\t\t\t}\n\t\t\t\tallClaims := make(map[string]interface{})\n\t\t\t\tif err := jwt.Claims(tt.fields.jwk.Public(), &allClaims); err != nil {\n\t\t\t\t\tt.Error(err)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tif v, ok := allClaims[\"sha\"].(string); !ok || v != sha {\n\t\t\t\t\tt.Errorf(\"Claim sha = %s, want %s\", v, sha)\n\t\t\t\t}\n\t\t\t\tif len(tt.args.sans) == 0 {\n\t\t\t\t\tif v, ok := allClaims[\"sans\"].([]interface{}); !ok || !reflect.DeepEqual(v, []interface{}{tt.args.subject}) {\n\t\t\t\t\t\tt.Errorf(\"Claim sans = %s, want %s\", v, []interface{}{tt.args.subject})\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\twant := []interface{}{}\n\t\t\t\t\tfor _, s := range tt.args.sans {\n\t\t\t\t\t\twant = append(want, s)\n\t\t\t\t\t}\n\t\t\t\t\tif v, ok := allClaims[\"sans\"].([]interface{}); !ok || !reflect.DeepEqual(v, want) {\n\t\t\t\t\t\tt.Errorf(\"Claim sans = %s, want %s\", v, want)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif v, ok := allClaims[\"jti\"].(string); !ok || v == \"\" {\n\t\t\t\t\tt.Errorf(\"Claim jti = %s, want not blank\", v)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestProvisioner_IPv6Token(t *testing.T) {\n\tp := getTestProvisioner(t, \"https://[::1]:9000\")\n\tsha := \"ef742f95dc0d8aa82d3cca4017af6dac3fce84290344159891952d18c53eefe7\"\n\n\ttype fields struct {\n\t\tname          string\n\t\tkid           string\n\t\tfingerprint   string\n\t\tjwk           *jose.JSONWebKey\n\t\ttokenLifetime time.Duration\n\t}\n\ttype args struct {\n\t\tsubject string\n\t\tsans    []string\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tfields  fields\n\t\targs    args\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", fields{p.name, p.kid, sha, p.jwk, p.tokenLifetime}, args{\"subject\", nil}, false},\n\t\t{\"ok-with-san\", fields{p.name, p.kid, sha, p.jwk, p.tokenLifetime}, args{\"subject\", []string{\"foo.smallstep.com\"}}, false},\n\t\t{\"ok-with-sans\", fields{p.name, p.kid, sha, p.jwk, p.tokenLifetime}, args{\"subject\", []string{\"foo.smallstep.com\", \"127.0.0.1\"}}, false},\n\t\t{\"fail-no-subject\", fields{p.name, p.kid, sha, p.jwk, p.tokenLifetime}, args{\"\", []string{\"foo.smallstep.com\"}}, true},\n\t\t{\"fail-no-key\", fields{p.name, p.kid, sha, &jose.JSONWebKey{}, p.tokenLifetime}, args{\"subject\", nil}, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tp := &Provisioner{\n\t\t\t\tname:          tt.fields.name,\n\t\t\t\tkid:           tt.fields.kid,\n\t\t\t\taudience:      \"https://[::1]:9000/1.0/sign\",\n\t\t\t\tfingerprint:   tt.fields.fingerprint,\n\t\t\t\tjwk:           tt.fields.jwk,\n\t\t\t\ttokenLifetime: tt.fields.tokenLifetime,\n\t\t\t}\n\t\t\tgot, err := p.Token(tt.args.subject, tt.args.sans...)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"Provisioner.Token() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif tt.wantErr == false {\n\t\t\t\tjwt, err := jose.ParseSigned(got)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Error(err)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tvar claims jose.Claims\n\t\t\t\tif err := jwt.Claims(tt.fields.jwk.Public(), &claims); err != nil {\n\t\t\t\t\tt.Error(err)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tif err := claims.ValidateWithLeeway(jose.Expected{\n\t\t\t\t\tAudience: []string{\"https://[::1]:9000/1.0/sign\"},\n\t\t\t\t\tIssuer:   tt.fields.name,\n\t\t\t\t\tSubject:  tt.args.subject,\n\t\t\t\t\tTime:     time.Now().UTC(),\n\t\t\t\t}, time.Minute); err != nil {\n\t\t\t\t\tt.Error(err)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tlifetime := claims.Expiry.Time().Sub(claims.NotBefore.Time())\n\t\t\t\tif lifetime != tt.fields.tokenLifetime {\n\t\t\t\t\tt.Errorf(\"Claims token life time = %s, want %s\", lifetime, tt.fields.tokenLifetime)\n\t\t\t\t}\n\t\t\t\tallClaims := make(map[string]interface{})\n\t\t\t\tif err := jwt.Claims(tt.fields.jwk.Public(), &allClaims); err != nil {\n\t\t\t\t\tt.Error(err)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tif v, ok := allClaims[\"sha\"].(string); !ok || v != sha {\n\t\t\t\t\tt.Errorf(\"Claim sha = %s, want %s\", v, sha)\n\t\t\t\t}\n\t\t\t\tif len(tt.args.sans) == 0 {\n\t\t\t\t\tif v, ok := allClaims[\"sans\"].([]interface{}); !ok || !reflect.DeepEqual(v, []interface{}{tt.args.subject}) {\n\t\t\t\t\t\tt.Errorf(\"Claim sans = %s, want %s\", v, []interface{}{tt.args.subject})\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\twant := []interface{}{}\n\t\t\t\t\tfor _, s := range tt.args.sans {\n\t\t\t\t\t\twant = append(want, s)\n\t\t\t\t\t}\n\t\t\t\t\tif v, ok := allClaims[\"sans\"].([]interface{}); !ok || !reflect.DeepEqual(v, want) {\n\t\t\t\t\t\tt.Errorf(\"Claim sans = %s, want %s\", v, want)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif v, ok := allClaims[\"jti\"].(string); !ok || v == \"\" {\n\t\t\t\t\tt.Errorf(\"Claim jti = %s, want not blank\", v)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestProvisioner_SSHToken(t *testing.T) {\n\tp := getTestProvisioner(t, \"https://127.0.0.1:9000\")\n\tsha := \"ef742f95dc0d8aa82d3cca4017af6dac3fce84290344159891952d18c53eefe7\"\n\n\ttype fields struct {\n\t\tname          string\n\t\tkid           string\n\t\tfingerprint   string\n\t\tjwk           *jose.JSONWebKey\n\t\ttokenLifetime time.Duration\n\t}\n\ttype args struct {\n\t\tcertType   string\n\t\tkeyID      string\n\t\tprincipals []string\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tfields  fields\n\t\targs    args\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", fields{p.name, p.kid, sha, p.jwk, p.tokenLifetime}, args{\"user\", \"foo@smallstep.com\", []string{\"foo\"}}, false},\n\t\t{\"ok host\", fields{p.name, p.kid, sha, p.jwk, p.tokenLifetime}, args{\"host\", \"foo.smallstep.com\", []string{\"foo.smallstep.com\"}}, false},\n\t\t{\"ok multiple principals\", fields{p.name, p.kid, sha, p.jwk, p.tokenLifetime}, args{\"user\", \"foo@smallstep.com\", []string{\"foo\", \"bar\"}}, false},\n\t\t{\"fail-no-subject\", fields{p.name, p.kid, sha, p.jwk, p.tokenLifetime}, args{\"user\", \"\", []string{\"foo\"}}, true},\n\t\t{\"fail-no-key\", fields{p.name, p.kid, sha, &jose.JSONWebKey{}, p.tokenLifetime}, args{\"user\", \"foo@smallstep.com\", []string{\"foo\"}}, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tp := &Provisioner{\n\t\t\t\tname:          tt.fields.name,\n\t\t\t\tkid:           tt.fields.kid,\n\t\t\t\taudience:      \"https://127.0.0.1:9000/1.0/sign\",\n\t\t\t\tsshAudience:   \"https://127.0.0.1:9000/1.0/ssh/sign\",\n\t\t\t\tfingerprint:   tt.fields.fingerprint,\n\t\t\t\tjwk:           tt.fields.jwk,\n\t\t\t\ttokenLifetime: tt.fields.tokenLifetime,\n\t\t\t}\n\t\t\tgot, err := p.SSHToken(tt.args.certType, tt.args.keyID, tt.args.principals)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"Provisioner.SSHToken() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif tt.wantErr == false {\n\t\t\t\tjwt, err := jose.ParseSigned(got)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Error(err)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tvar claims jose.Claims\n\t\t\t\tif err := jwt.Claims(tt.fields.jwk.Public(), &claims); err != nil {\n\t\t\t\t\tt.Error(err)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tif err := claims.ValidateWithLeeway(jose.Expected{\n\t\t\t\t\tAudience: []string{\"https://127.0.0.1:9000/1.0/ssh/sign\"},\n\t\t\t\t\tIssuer:   tt.fields.name,\n\t\t\t\t\tSubject:  tt.args.keyID,\n\t\t\t\t\tTime:     time.Now().UTC(),\n\t\t\t\t}, time.Minute); err != nil {\n\t\t\t\t\tt.Error(err)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tlifetime := claims.Expiry.Time().Sub(claims.NotBefore.Time())\n\t\t\t\tif lifetime != tt.fields.tokenLifetime {\n\t\t\t\t\tt.Errorf(\"Claims token life time = %s, want %s\", lifetime, tt.fields.tokenLifetime)\n\t\t\t\t}\n\t\t\t\tallClaims := make(map[string]interface{})\n\t\t\t\tif err := jwt.Claims(tt.fields.jwk.Public(), &allClaims); err != nil {\n\t\t\t\t\tt.Error(err)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tif v, ok := allClaims[\"sha\"].(string); !ok || v != sha {\n\t\t\t\t\tt.Errorf(\"Claim sha = %s, want %s\", v, sha)\n\t\t\t\t}\n\n\t\t\t\tprincipals := make([]interface{}, len(tt.args.principals))\n\t\t\t\tfor i, p := range tt.args.principals {\n\t\t\t\t\tprincipals[i] = p\n\t\t\t\t}\n\t\t\t\twant := map[string]interface{}{\n\t\t\t\t\t\"ssh\": map[string]interface{}{\n\t\t\t\t\t\t\"certType\":    tt.args.certType,\n\t\t\t\t\t\t\"keyID\":       tt.args.keyID,\n\t\t\t\t\t\t\"principals\":  principals,\n\t\t\t\t\t\t\"validAfter\":  \"\",\n\t\t\t\t\t\t\"validBefore\": \"\",\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\tif !reflect.DeepEqual(allClaims[\"step\"], want) {\n\t\t\t\t\tt.Errorf(\"Claim step = %s, want %s\", allClaims[\"step\"], want)\n\t\t\t\t}\n\t\t\t\tif v, ok := allClaims[\"jti\"].(string); !ok || v == \"\" {\n\t\t\t\t\tt.Errorf(\"Claim jti = %s, want not blank\", v)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "ca/renew.go",
    "content": "package ca\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"math/rand\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n)\n\n// RenewFunc defines the type of the functions used to get a new tls\n// certificate.\ntype RenewFunc func() (*tls.Certificate, error)\n\nvar minCertDuration = time.Minute\n\n// TLSRenewer automatically renews a tls certificate using a RenewFunc.\ntype TLSRenewer struct {\n\trenewMutex       sync.RWMutex\n\tRenewCertificate RenewFunc\n\tcert             *tls.Certificate\n\ttimer            *time.Timer\n\trenewBefore      time.Duration\n\trenewJitter      time.Duration\n\tcertNotAfter     time.Time\n}\n\ntype tlsRenewerOptions func(r *TLSRenewer) error\n\n// WithRenewBefore modifies a tlsRenewer by setting the renewBefore attribute.\nfunc WithRenewBefore(b time.Duration) func(r *TLSRenewer) error {\n\treturn func(r *TLSRenewer) error {\n\t\tr.renewBefore = b\n\t\treturn nil\n\t}\n}\n\n// WithRenewJitter modifies a tlsRenewer by setting the renewJitter attribute.\nfunc WithRenewJitter(j time.Duration) func(r *TLSRenewer) error {\n\treturn func(r *TLSRenewer) error {\n\t\tr.renewJitter = j\n\t\treturn nil\n\t}\n}\n\n// NewTLSRenewer creates a TLSRenewer for the given cert. It will use the given\n// RenewFunc to get a new certificate when required.\nfunc NewTLSRenewer(cert *tls.Certificate, fn RenewFunc, opts ...tlsRenewerOptions) (*TLSRenewer, error) {\n\tr := &TLSRenewer{\n\t\tRenewCertificate: fn,\n\t\tcert:             cert,\n\t\tcertNotAfter:     cert.Leaf.NotAfter.Add(-1 * time.Minute),\n\t}\n\n\tfor _, f := range opts {\n\t\tif err := f(r); err != nil {\n\t\t\treturn nil, errors.Wrap(err, \"error applying options\")\n\t\t}\n\t}\n\n\t// Use the current time to calculate the initial period. Using a notBefore\n\t// in the past might set a renewBefore too large, causing continuous\n\t// renewals due to the negative values in nextRenewDuration.\n\tperiod := cert.Leaf.NotAfter.Sub(time.Now().Truncate(time.Second))\n\tif period < minCertDuration {\n\t\treturn nil, errors.Errorf(\"period must be greater than or equal to %s, but got %v.\", minCertDuration, period)\n\t}\n\t// By default we will try to renew the cert before 2/3 of the validity\n\t// period have expired.\n\tif r.renewBefore == 0 {\n\t\tr.renewBefore = period / 3\n\t}\n\t// By default we set the jitter to 1/20th of the validity period.\n\tif r.renewJitter == 0 {\n\t\tr.renewJitter = period / 20\n\t}\n\n\treturn r, nil\n}\n\n// Run starts the certificate renewer for the given certificate.\nfunc (r *TLSRenewer) Run() {\n\tcert := r.getCertificate()\n\tnext := r.nextRenewDuration(cert.Leaf.NotAfter)\n\tr.renewMutex.Lock()\n\tr.timer = time.AfterFunc(next, r.renewCertificate)\n\tr.renewMutex.Unlock()\n}\n\n// RunContext starts the certificate renewer for the given certificate.\nfunc (r *TLSRenewer) RunContext(ctx context.Context) {\n\tr.Run()\n\tgo func() {\n\t\t<-ctx.Done()\n\t\tr.Stop()\n\t}()\n}\n\n// Stop prevents the renew timer from firing.\nfunc (r *TLSRenewer) Stop() bool {\n\tif r.timer != nil {\n\t\treturn r.timer.Stop()\n\t}\n\treturn true\n}\n\n// GetCertificate returns the current server certificate.\n//\n// This method is set in the tls.Config GetCertificate property.\nfunc (r *TLSRenewer) GetCertificate(*tls.ClientHelloInfo) (*tls.Certificate, error) {\n\treturn r.getCertificate(), nil\n}\n\n// GetCertificateForCA returns the current server certificate. It can only be\n// used if the renew function creates the new certificate and do not uses a TLS\n// request. It's intended to be use by the certificate authority server.\n//\n// This method is set in the tls.Config GetCertificate property.\nfunc (r *TLSRenewer) GetCertificateForCA(*tls.ClientHelloInfo) (*tls.Certificate, error) {\n\treturn r.getCertificateForCA(), nil\n}\n\n// GetClientCertificate returns the current client certificate.\n//\n// This method is set in the tls.Config GetClientCertificate property.\nfunc (r *TLSRenewer) GetClientCertificate(*tls.CertificateRequestInfo) (*tls.Certificate, error) {\n\treturn r.getCertificate(), nil\n}\n\n// getCertificate returns the certificate using a read-only lock.\n//\n// Known issue: It cannot renew an expired certificate because the /renew\n// endpoint requires a valid client certificate. The certificate can expire\n// if the timer does not fire e.g. when the CA is run from a laptop that\n// enters sleep mode.\nfunc (r *TLSRenewer) getCertificate() *tls.Certificate {\n\tr.renewMutex.RLock()\n\tcert := r.cert\n\tr.renewMutex.RUnlock()\n\treturn cert\n}\n\n// getCertificateForCA returns the certificate using a read-only lock. It will\n// automatically renew the certificate if it has expired.\nfunc (r *TLSRenewer) getCertificateForCA() *tls.Certificate {\n\tr.renewMutex.RLock()\n\t// Force certificate renewal if the timer didn't run.\n\t// This is an special case that can happen after a computer sleep.\n\tif time.Now().After(r.certNotAfter) {\n\t\tr.renewMutex.RUnlock()\n\t\tr.renewCertificate()\n\t\tr.renewMutex.RLock()\n\t}\n\tcert := r.cert\n\tr.renewMutex.RUnlock()\n\treturn cert\n}\n\n// setCertificate updates the certificate using a read-write lock. It also\n// updates certNotAfter with 1m of delta; this will force the renewal of the\n// certificate if it is about to expire.\nfunc (r *TLSRenewer) setCertificate(cert *tls.Certificate) {\n\tr.renewMutex.Lock()\n\tr.cert = cert\n\tr.certNotAfter = cert.Leaf.NotAfter.Add(-1 * time.Minute)\n\tr.renewMutex.Unlock()\n}\n\nfunc (r *TLSRenewer) renewCertificate() {\n\tvar next time.Duration\n\tcert, err := r.RenewCertificate()\n\tif err != nil {\n\t\tnext = r.renewJitter / 2\n\t\tnext += time.Duration(mathRandInt63n(int64(next)))\n\t} else {\n\t\tr.setCertificate(cert)\n\t\tnext = r.nextRenewDuration(cert.Leaf.NotAfter)\n\t}\n\tr.renewMutex.Lock()\n\tr.timer.Reset(next)\n\tr.renewMutex.Unlock()\n}\n\nfunc (r *TLSRenewer) nextRenewDuration(notAfter time.Time) time.Duration {\n\td := time.Until(notAfter).Truncate(time.Second) - r.renewBefore\n\tn := mathRandInt63n(int64(r.renewJitter))\n\td -= time.Duration(n)\n\tif d < 0 {\n\t\td = 0\n\t}\n\treturn d\n}\n\n//nolint:gosec // not used for cryptographic security\nfunc mathRandInt63n(n int64) int64 {\n\treturn rand.Int63n(n)\n}\n"
  },
  {
    "path": "ca/signal.go",
    "content": "package ca\n\nimport (\n\t\"log\"\n\t\"os\"\n\t\"os/signal\"\n\t\"syscall\"\n)\n\n// Stopper is the interface that external commands can implement to stop the\n// server.\ntype Stopper interface {\n\tStop() error\n}\n\n// StopReloader is the interface that external commands can implement to stop\n// the server and reload the configuration while running.\ntype StopReloader interface {\n\tStop() error\n\tReload() error\n}\n\n// StopHandler watches SIGINT, SIGTERM on a list of servers implementing the\n// Stopper interface, and when one of those signals is caught we'll run Stop\n// (SIGINT, SIGTERM) on all servers.\nfunc StopHandler(servers ...Stopper) {\n\tsignals := make(chan os.Signal, 1)\n\tsignal.Notify(signals, syscall.SIGINT, syscall.SIGTERM)\n\tdefer signal.Stop(signals)\n\n\tfor sig := range signals {\n\t\tswitch sig {\n\t\tcase syscall.SIGINT, syscall.SIGTERM:\n\t\t\tlog.Println(\"shutting down ...\")\n\t\t\tfor _, server := range servers {\n\t\t\t\terr := server.Stop()\n\t\t\t\tif err != nil {\n\t\t\t\t\tlog.Printf(\"error stopping server: %s\", err.Error())\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn\n\t\t}\n\t}\n}\n\n// StopReloaderHandler watches SIGINT, SIGTERM and SIGHUP on a list of servers\n// implementing the StopReloader interface, and when one of those signals is\n// caught we'll run Stop (SIGINT, SIGTERM) or Reload (SIGHUP) on all servers.\nfunc StopReloaderHandler(servers ...StopReloader) {\n\tsignals := make(chan os.Signal, 1)\n\tsignal.Notify(signals, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)\n\tdefer signal.Stop(signals)\n\n\tfor sig := range signals {\n\t\tswitch sig {\n\t\tcase syscall.SIGHUP:\n\t\t\tlog.Println(\"reloading ...\")\n\t\t\tfor _, server := range servers {\n\t\t\t\terr := server.Reload()\n\t\t\t\tif err != nil {\n\t\t\t\t\tlog.Printf(\"error reloading server: %+v\", err)\n\t\t\t\t}\n\t\t\t}\n\t\tcase syscall.SIGINT, syscall.SIGTERM:\n\t\t\tlog.Println(\"shutting down ...\")\n\t\t\tfor _, server := range servers {\n\t\t\t\terr := server.Stop()\n\t\t\t\tif err != nil {\n\t\t\t\t\tlog.Printf(\"error stopping server: %s\", err.Error())\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "ca/testdata/ca.json",
    "content": "{\n    \"root\": \"../ca/testdata/secrets/root_ca.crt\",\n    \"federatedRoots\": [\"../ca/testdata/secrets/federated_ca.crt\"],\n    \"crt\": \"../ca/testdata/secrets/intermediate_ca.crt\",\n    \"key\": \"../ca/testdata/secrets/intermediate_ca_key\",\n    \"password\": \"password\",\n    \"address\": \"127.0.0.1:0\",\n    \"dnsNames\": [\"127.0.0.1\"],\n    \"_logger\": {\"format\": \"text\"},\n    \"tls\": {\n        \"minVersion\": 1.2,\n        \"maxVersion\": 1.3,\n        \"renegotiation\": false,\n        \"cipherSuites\": [\n            \"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256\",\n\t\t\t\"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256\"\n        ]\n    },\n    \"authority\": {\n        \"backdate\": \"0s\",\n        \"provisioners\": [\n            {\n                \"name\": \"max\",\n                \"type\": \"jwk\",\n                \"encryptedKey\": \"eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJlbmMiOiJBMTI4R0NNIiwicDJjIjoxMDAwMDAsInAycyI6IkpsNkZLWUp4V1UwdGRIbG9UanA1aGcifQ.Qy0EP6u5-t0ggOweoc3Z1DCzR5BllsQi.KUkviZ_TJKY4c0Mi.h7QZqgh_Fl2MZpmVy4h375yC0DORjB1dQULbNqc6MuUCW2iweWVRysFImUXiXMUKRarJC5adwWy1GhyAqUj6Xj1iOZDGLjYnqMETGWcI0rKDBwcSU7y7Y-2VYBRDSM2b7aWtTBfz3_kvEaw_vc3b5CEPJ86UlZc-jhKFRr_IcGWU-vXX5-bppoH15IPreyzi55YdjCll338lYpDecB_Paym3XBXotyd2iGXXUwoA1npEFwuyRMMEhl9zLp7rVcMW6A_32EzB8cZANEnA0C4FXGHQalY6u_2UeqxcC8_FuXPay6VIYODyRqcABvvkft3nwOcrI0pYDGBdk2w2Euk.kOAFq3Tg6s4vBGS_plMpSw\",\n                \"key\": {\n                    \"use\": \"sig\",\n                    \"kty\": \"EC\",\n                    \"kid\": \"IMi94WBNI6gP5cNHXlZYNUzvMjGdHyBRmFoo-lCEaqk\",\n                    \"crv\": \"P-256\",\n                    \"alg\": \"ES256\",\n                    \"x\": \"XmaY0c9Cc_kjfn9uhimiDiKnKn00gmFzzsvElg4KxoE\",\n                    \"y\": \"ZhYcFQBqtErdC_pA7sOXrO7AboCEPIKP9Ik4CHJqANk\"\n                }\n            }, {\n                \"name\": \"mike\",\n                \"type\": \"jwk\",\n                \"encryptedKey\": \"eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJlbmMiOiJBMTI4R0NNIiwicDJjIjoxMDAwMDAsInAycyI6IlZsWnl0dUxrWTR5enlqZXJybnN0aGcifQ.QP15wQYjZ12BLgl-XTq2Vb12G3OHAfic.X35QqAaXwnlmeCUU._2qIUp0TI8yDI7c2e9upIRdrnmB5OvtLfrYN-Su2NLBpaoYtr9O55Wo0Iryc0W2pYqnVDPvgPPes4P4nQAnzw5WhFYc1Xf1ZEetfdNhwi1x2FNwPbACBAgxm5AW40O5AAlbLcWushYASfeMBZocTGXuSGUzwFqoWD-5EDJ80TWQ7cAj3ttHrJ_3QV9hi4O9KJUCiXngN-Yz2zXrhBL4NOH2fmRbaf5c0rF8xUJIIW-TcyYJeX_Fbx1IzzKKPd9USUwkDhxD4tLa51I345xVqjuwG1PEn6nF8JKqLRVUKEKFin-ShXrfE61KceyAvm4YhWKrbJWIm3bH5Hxaphy4.TexIrIhsRxJStpE3EJ925Q\",\n                \"key\": {\n                    \"use\": \"sig\",\n                    \"kty\": \"EC\",\n                    \"kid\": \"DC06fatJ5nALkfEubR3VVgQ2XNy_DXSKZhwGoRO8cWU\",\n                    \"crv\": \"P-256\",\n                    \"alg\": \"ES256\",\n                    \"x\": \"SuaL-GJ3LmgBF43Da9ZCY-BzmvlkMJ61MAZ1UELPpTw\",\n                    \"y\": \"wnqZSMuXpmUxORq20t83LyY4BDYmqDGV9P7FGR6mw84\"\n                }\n            }, {\n                \"name\": \"step-cli\",\n                \"type\": \"jwk\",\n                \"encryptedKey\": \"eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJlbmMiOiJBMTI4R0NNIiwicDJjIjoxMDAwMDAsInAycyI6IlhOdmYxQjgxSUlLMFA2NUkwcmtGTGcifQ.XaN9zcPQeWt49zchUDm34FECUTHfQTn_.tmNHPQDqR3ebsWfd.9WZr3YVdeOyJh36vvx0VlRtluhvYp4K7jJ1KGDr1qypwZ3ziBVSNbYYQ71du7fTtrnfG1wgGTVR39tWSzBU-zwQ5hdV3rpMAaEbod5zeW6SHd95H3Bvcb43YiiqJFNL5sGZzFb7FqzVmpsZ1efiv6sZaGDHtnCAL6r12UG5EZuqGfM0jGCZitUz2m9TUKXJL5DJ7MOYbFfkCEsUBPDm_TInliSVn2kMJhFa0VOe5wZk5YOuYM3lNYW64HGtbf-llN2Xk-4O9TfeSPizBx9ZqGpeu8pz13efUDT2WL9tWo6-0UE-CrG0bScm8lFTncTkHcu49_a5NaUBkYlBjEiw.thPcx3t1AUcWuEygXIY3Fg\",\n                \"key\": {\n                    \"use\": \"sig\",\n                    \"kty\": \"EC\",\n                    \"kid\": \"4UELJx8e0aS9m0CH3fZ0EB7D5aUPICb759zALHFejvc\",\n                    \"crv\": \"P-256\",\n                    \"alg\": \"ES256\",\n                    \"x\": \"7ZdAAMZCFU4XwgblI5RfZouBi8lYmF6DlZusNNnsbm8\",\n                    \"y\": \"sQr2JdzwD2fgyrymBEXWsxDxFNjjqN64qLLSbLdLZ9Y\"\n                }\n            }, {\n                \"name\": \"mariano\",\n                \"type\": \"jwk\",\n                \"encryptedKey\": \"eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJlbmMiOiJBMTI4R0NNIiwicDJjIjoxMDAwMDAsInAycyI6IlB1UnJVQ1RZZkR1T2F5MEh2cGl6bncifQ.7a-OP5xWGbFra8m2MN9YuLGt6v4y0wmB.u-54daK2y-0UO9na.3GQy6E52-fOSUu5NJ_sEbxj_T3CTyWb7wOPFv2oI2PBWXp5CLpiWJbCFpF4v2oD9fN5XbxMP14ootbrFjATnoMWfWgyLwG-KOj9BqMGNxhG2v37yC7Wrris6s30nrPa3uyNEYZ12AOQW1K04cU2X0u_qJM3vzMCle548ZFTWs6_d6L8lp3o0F9MEbCmJ4p6CLqQxjxYtn1aD79lM91NbIXpRP3iUFQRly-y_iC2mSkXCdd_cQ6-dqLUchXwWRyVO5nBHb4J87aZ91VApw7ldTLtwRZ2ZGJpqGQGgjTwi4sgjEcMuGg0_83XGk2ubdlKDpmGFedOHS5rYCbxotts.vSYfxsi2UU9LQeySDjAnnQ\",\n                \"key\": {\n                    \"use\": \"sig\",\n                    \"kty\": \"EC\",\n                    \"kid\": \"FLIV7q23CXHrg75J2OSbvzwKJJqoxCYixjmsJirneOg\",\n                    \"crv\": \"P-256\",\n                    \"alg\": \"ES256\",\n                    \"x\": \"tTKthEHN7RuybhkaC43J2oLfBG995FNSWbtahLAiK7Y\",\n                    \"y\": \"e3wycXwVB366F0wLE5J9gIpq8EIQ4900nHBNpIGebEA\"\n                },\n                \"claims\": {\n                    \"minTLSCertDuration\": \"1s\"\n                }\n            }, {\n                \"name\": \"maxey\",\n                \"type\": \"jwk\",\n                \"encryptedKey\": \"eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJlbmMiOiJBMTI4R0NNIiwicDJjIjoxMDAwMDAsInAycyI6Ik5SLTk5ZkVMSm1CLW1FZGllUlFFc3cifQ.Fr314BEUGTda4ICJl2uxFdjpEUGGqJEV.gBbu_DZE1ONDu14r.X-7MKMyokZIF1HTCVqqL0tTWgaC1ZGZBLLltd11ZUhQTswo_8kvgiTv3cFShj7ATF0tAY8HStyJmzLO8mKPVOPDXSwjdNsPriZclI6JWGi9iOu8pEiN9pZM6-itxan1JMcDUNg2U-P1BmKppHRbDKsOTivymfRyeUk51dBIlS54p5xNK1HFLc1YtWC1Rc_ngYVqOgqlhIrCHArAEBe3jrfUaH2ym-8fkVdwVqtxmte3XXK9g8FchsygRNnOKtRcr0TyzTUV-7bPi8_t02Zi-EHLFaSawVXWV_Qk1GeLYJR22Rp74beo-b5-lCNVp10btO0xdGySUWmCJ4v4_QZw.c8unwWycwtfdJMM_0b0fuA\",\n                \"key\": {\n                    \"use\": \"sig\",\n                    \"kty\": \"EC\",\n                    \"kid\": \"kA5qxq_k8VFc2vzriBUU1FdzHpRfQ5Uq4W3803l1m5U\",\n                    \"crv\": \"P-256\",\n                    \"alg\": \"ES256\",\n                    \"x\": \"qGXXrT1vgRKVpqLoVwdgIut5VjvxrHa_V4xhh2kQvY0\",\n                    \"y\": \"8YHQPb031kQ9gMG8ue-YRy0Fm8Gc-v6TnYYLxRGcSjw\"\n                }\n            }\n        ],\n        \"template\": {\n            \"country\": \"US\",\n            \"locality\": \"San Francisco\",\n            \"organization\": \"Smallstep\"\n        }\n    }\n}\n"
  },
  {
    "path": "ca/testdata/federated-ca.json",
    "content": "{\n    \"root\": \"testdata/rotated/root_ca.crt\",\n    \"federatedRoots\": [\"testdata/secrets/root_ca.crt\"],\n    \"crt\": \"testdata/rotated/intermediate_ca.crt\",\n    \"key\": \"testdata/rotated/intermediate_ca_key\",\n    \"password\": \"asdf\",\n    \"address\": \"127.0.0.1:0\",\n    \"dnsNames\": [\"127.0.0.1\"],\n    \"_logger\": {\"format\": \"text\"},\n    \"tls\": {\n        \"minVersion\": 1.2,\n        \"maxVersion\": 1.2,\n        \"renegotiation\": false,\n        \"cipherSuites\": [\n            \"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305\",\n            \"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256\",\n            \"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384\"\n        ]\n    },\n    \"authority\": {\n        \"provisioners\": [\n            {\n                \"name\": \"mariano\",\n                \"type\": \"jwk\",\n                \"encryptedKey\": \"eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJlbmMiOiJBMTI4R0NNIiwicDJjIjoxMDAwMDAsInAycyI6IlB1UnJVQ1RZZkR1T2F5MEh2cGl6bncifQ.7a-OP5xWGbFra8m2MN9YuLGt6v4y0wmB.u-54daK2y-0UO9na.3GQy6E52-fOSUu5NJ_sEbxj_T3CTyWb7wOPFv2oI2PBWXp5CLpiWJbCFpF4v2oD9fN5XbxMP14ootbrFjATnoMWfWgyLwG-KOj9BqMGNxhG2v37yC7Wrris6s30nrPa3uyNEYZ12AOQW1K04cU2X0u_qJM3vzMCle548ZFTWs6_d6L8lp3o0F9MEbCmJ4p6CLqQxjxYtn1aD79lM91NbIXpRP3iUFQRly-y_iC2mSkXCdd_cQ6-dqLUchXwWRyVO5nBHb4J87aZ91VApw7ldTLtwRZ2ZGJpqGQGgjTwi4sgjEcMuGg0_83XGk2ubdlKDpmGFedOHS5rYCbxotts.vSYfxsi2UU9LQeySDjAnnQ\",\n                \"key\": {\n                    \"use\": \"sig\",\n                    \"kty\": \"EC\",\n                    \"kid\": \"FLIV7q23CXHrg75J2OSbvzwKJJqoxCYixjmsJirneOg\",\n                    \"crv\": \"P-256\",\n                    \"alg\": \"ES256\",\n                    \"x\": \"tTKthEHN7RuybhkaC43J2oLfBG995FNSWbtahLAiK7Y\",\n                    \"y\": \"e3wycXwVB366F0wLE5J9gIpq8EIQ4900nHBNpIGebEA\"\n                },\n                \"claims\": {\n                    \"minTLSCertDuration\": \"1s\",\n                    \"defaultTLSCertDuration\": \"5s\"\n                }\n            }\n        ],\n        \"template\": {\n            \"country\": \"US\",\n            \"locality\": \"San Francisco\",\n            \"organization\": \"Smallstep\"\n        }\n    }\n}\n"
  },
  {
    "path": "ca/testdata/rotate-ca-0.json",
    "content": "{\n    \"root\": \"testdata/secrets/root_ca.crt\",\n    \"crt\": \"testdata/secrets/intermediate_ca.crt\",\n    \"key\": \"testdata/secrets/intermediate_ca_key\",\n    \"password\": \"password\",\n    \"address\": \"127.0.0.1:0\",\n    \"dnsNames\": [\"127.0.0.1\"],\n    \"_logger\": {\"format\": \"text\"},\n    \"tls\": {\n        \"minVersion\": 1.2,\n        \"maxVersion\": 1.2,\n        \"renegotiation\": false,\n        \"cipherSuites\": [\n            \"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305\",\n            \"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256\",\n            \"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384\"\n        ]\n    },\n    \"authority\": {\n        \"provisioners\": [\n            {\n                \"name\": \"mariano\",\n                \"type\": \"jwk\",\n                \"encryptedKey\": \"eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJlbmMiOiJBMTI4R0NNIiwicDJjIjoxMDAwMDAsInAycyI6IlB1UnJVQ1RZZkR1T2F5MEh2cGl6bncifQ.7a-OP5xWGbFra8m2MN9YuLGt6v4y0wmB.u-54daK2y-0UO9na.3GQy6E52-fOSUu5NJ_sEbxj_T3CTyWb7wOPFv2oI2PBWXp5CLpiWJbCFpF4v2oD9fN5XbxMP14ootbrFjATnoMWfWgyLwG-KOj9BqMGNxhG2v37yC7Wrris6s30nrPa3uyNEYZ12AOQW1K04cU2X0u_qJM3vzMCle548ZFTWs6_d6L8lp3o0F9MEbCmJ4p6CLqQxjxYtn1aD79lM91NbIXpRP3iUFQRly-y_iC2mSkXCdd_cQ6-dqLUchXwWRyVO5nBHb4J87aZ91VApw7ldTLtwRZ2ZGJpqGQGgjTwi4sgjEcMuGg0_83XGk2ubdlKDpmGFedOHS5rYCbxotts.vSYfxsi2UU9LQeySDjAnnQ\",\n                \"key\": {\n                    \"use\": \"sig\",\n                    \"kty\": \"EC\",\n                    \"kid\": \"FLIV7q23CXHrg75J2OSbvzwKJJqoxCYixjmsJirneOg\",\n                    \"crv\": \"P-256\",\n                    \"alg\": \"ES256\",\n                    \"x\": \"tTKthEHN7RuybhkaC43J2oLfBG995FNSWbtahLAiK7Y\",\n                    \"y\": \"e3wycXwVB366F0wLE5J9gIpq8EIQ4900nHBNpIGebEA\"\n                },\n                \"claims\": {\n                    \"minTLSCertDuration\": \"1s\",\n                    \"defaultTLSCertDuration\": \"5s\"\n                }\n            }\n        ],\n        \"template\": {\n            \"country\": \"US\",\n            \"locality\": \"San Francisco\",\n            \"organization\": \"Smallstep\"\n        }\n    }\n}\n"
  },
  {
    "path": "ca/testdata/rotate-ca-1.json",
    "content": "{\n    \"root\": [\"testdata/secrets/root_ca.crt\", \"testdata/rotated/root_ca.crt\"],\n    \"crt\": \"testdata/secrets/intermediate_ca.crt\",\n    \"key\": \"testdata/secrets/intermediate_ca_key\",\n    \"password\": \"password\",\n    \"address\": \"127.0.0.1:0\",\n    \"dnsNames\": [\"127.0.0.1\"],\n    \"_logger\": {\"format\": \"text\"},\n    \"tls\": {\n        \"minVersion\": 1.2,\n        \"maxVersion\": 1.2,\n        \"renegotiation\": false,\n        \"cipherSuites\": [\n            \"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305\",\n            \"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256\",\n            \"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384\"\n        ]\n    },\n    \"authority\": {\n        \"provisioners\": [\n            {\n                \"name\": \"mariano\",\n                \"type\": \"jwk\",\n                \"encryptedKey\": \"eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJlbmMiOiJBMTI4R0NNIiwicDJjIjoxMDAwMDAsInAycyI6IlB1UnJVQ1RZZkR1T2F5MEh2cGl6bncifQ.7a-OP5xWGbFra8m2MN9YuLGt6v4y0wmB.u-54daK2y-0UO9na.3GQy6E52-fOSUu5NJ_sEbxj_T3CTyWb7wOPFv2oI2PBWXp5CLpiWJbCFpF4v2oD9fN5XbxMP14ootbrFjATnoMWfWgyLwG-KOj9BqMGNxhG2v37yC7Wrris6s30nrPa3uyNEYZ12AOQW1K04cU2X0u_qJM3vzMCle548ZFTWs6_d6L8lp3o0F9MEbCmJ4p6CLqQxjxYtn1aD79lM91NbIXpRP3iUFQRly-y_iC2mSkXCdd_cQ6-dqLUchXwWRyVO5nBHb4J87aZ91VApw7ldTLtwRZ2ZGJpqGQGgjTwi4sgjEcMuGg0_83XGk2ubdlKDpmGFedOHS5rYCbxotts.vSYfxsi2UU9LQeySDjAnnQ\",\n                \"key\": {\n                    \"use\": \"sig\",\n                    \"kty\": \"EC\",\n                    \"kid\": \"FLIV7q23CXHrg75J2OSbvzwKJJqoxCYixjmsJirneOg\",\n                    \"crv\": \"P-256\",\n                    \"alg\": \"ES256\",\n                    \"x\": \"tTKthEHN7RuybhkaC43J2oLfBG995FNSWbtahLAiK7Y\",\n                    \"y\": \"e3wycXwVB366F0wLE5J9gIpq8EIQ4900nHBNpIGebEA\"\n                },\n                \"claims\": {\n                    \"minTLSCertDuration\": \"1s\",\n                    \"defaultTLSCertDuration\": \"5s\"\n                }\n            }\n        ],\n        \"template\": {\n            \"country\": \"US\",\n            \"locality\": \"San Francisco\",\n            \"organization\": \"Smallstep\"\n        }\n    }\n}\n"
  },
  {
    "path": "ca/testdata/rotate-ca-2.json",
    "content": "{\n    \"root\": [\"testdata/rotated/root_ca.crt\", \"testdata/secrets/root_ca.crt\"],\n    \"crt\": \"testdata/rotated/intermediate_ca.crt\",\n    \"key\": \"testdata/rotated/intermediate_ca_key\",\n    \"password\": \"asdf\",\n    \"address\": \"127.0.0.1:0\",\n    \"dnsNames\": [\"127.0.0.1\"],\n    \"_logger\": {\"format\": \"text\"},\n    \"tls\": {\n        \"minVersion\": 1.2,\n        \"maxVersion\": 1.2,\n        \"renegotiation\": false,\n        \"cipherSuites\": [\n            \"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305\",\n            \"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256\",\n            \"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384\"\n        ]\n    },\n    \"authority\": {\n        \"provisioners\": [\n            {\n                \"name\": \"mariano\",\n                \"type\": \"jwk\",\n                \"encryptedKey\": \"eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJlbmMiOiJBMTI4R0NNIiwicDJjIjoxMDAwMDAsInAycyI6IlB1UnJVQ1RZZkR1T2F5MEh2cGl6bncifQ.7a-OP5xWGbFra8m2MN9YuLGt6v4y0wmB.u-54daK2y-0UO9na.3GQy6E52-fOSUu5NJ_sEbxj_T3CTyWb7wOPFv2oI2PBWXp5CLpiWJbCFpF4v2oD9fN5XbxMP14ootbrFjATnoMWfWgyLwG-KOj9BqMGNxhG2v37yC7Wrris6s30nrPa3uyNEYZ12AOQW1K04cU2X0u_qJM3vzMCle548ZFTWs6_d6L8lp3o0F9MEbCmJ4p6CLqQxjxYtn1aD79lM91NbIXpRP3iUFQRly-y_iC2mSkXCdd_cQ6-dqLUchXwWRyVO5nBHb4J87aZ91VApw7ldTLtwRZ2ZGJpqGQGgjTwi4sgjEcMuGg0_83XGk2ubdlKDpmGFedOHS5rYCbxotts.vSYfxsi2UU9LQeySDjAnnQ\",\n                \"key\": {\n                    \"use\": \"sig\",\n                    \"kty\": \"EC\",\n                    \"kid\": \"FLIV7q23CXHrg75J2OSbvzwKJJqoxCYixjmsJirneOg\",\n                    \"crv\": \"P-256\",\n                    \"alg\": \"ES256\",\n                    \"x\": \"tTKthEHN7RuybhkaC43J2oLfBG995FNSWbtahLAiK7Y\",\n                    \"y\": \"e3wycXwVB366F0wLE5J9gIpq8EIQ4900nHBNpIGebEA\"\n                },\n                \"claims\": {\n                    \"minTLSCertDuration\": \"1s\",\n                    \"defaultTLSCertDuration\": \"5s\"\n                }\n            }\n        ],\n        \"template\": {\n            \"country\": \"US\",\n            \"locality\": \"San Francisco\",\n            \"organization\": \"Smallstep\"\n        }\n    }\n}\n"
  },
  {
    "path": "ca/testdata/rotate-ca-3.json",
    "content": "{\n    \"root\": \"testdata/rotated/root_ca.crt\",\n    \"crt\": \"testdata/rotated/intermediate_ca.crt\",\n    \"key\": \"testdata/rotated/intermediate_ca_key\",\n    \"password\": \"asdf\",\n    \"address\": \"127.0.0.1:0\",\n    \"dnsNames\": [\"127.0.0.1\"],\n    \"_logger\": {\"format\": \"text\"},\n    \"tls\": {\n        \"minVersion\": 1.2,\n        \"maxVersion\": 1.2,\n        \"renegotiation\": false,\n        \"cipherSuites\": [\n            \"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305\",\n            \"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256\",\n            \"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384\"\n        ]\n    },\n    \"authority\": {\n        \"provisioners\": [\n            {\n                \"name\": \"mariano\",\n                \"type\": \"jwk\",\n                \"encryptedKey\": \"eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJlbmMiOiJBMTI4R0NNIiwicDJjIjoxMDAwMDAsInAycyI6IlB1UnJVQ1RZZkR1T2F5MEh2cGl6bncifQ.7a-OP5xWGbFra8m2MN9YuLGt6v4y0wmB.u-54daK2y-0UO9na.3GQy6E52-fOSUu5NJ_sEbxj_T3CTyWb7wOPFv2oI2PBWXp5CLpiWJbCFpF4v2oD9fN5XbxMP14ootbrFjATnoMWfWgyLwG-KOj9BqMGNxhG2v37yC7Wrris6s30nrPa3uyNEYZ12AOQW1K04cU2X0u_qJM3vzMCle548ZFTWs6_d6L8lp3o0F9MEbCmJ4p6CLqQxjxYtn1aD79lM91NbIXpRP3iUFQRly-y_iC2mSkXCdd_cQ6-dqLUchXwWRyVO5nBHb4J87aZ91VApw7ldTLtwRZ2ZGJpqGQGgjTwi4sgjEcMuGg0_83XGk2ubdlKDpmGFedOHS5rYCbxotts.vSYfxsi2UU9LQeySDjAnnQ\",\n                \"key\": {\n                    \"use\": \"sig\",\n                    \"kty\": \"EC\",\n                    \"kid\": \"FLIV7q23CXHrg75J2OSbvzwKJJqoxCYixjmsJirneOg\",\n                    \"crv\": \"P-256\",\n                    \"alg\": \"ES256\",\n                    \"x\": \"tTKthEHN7RuybhkaC43J2oLfBG995FNSWbtahLAiK7Y\",\n                    \"y\": \"e3wycXwVB366F0wLE5J9gIpq8EIQ4900nHBNpIGebEA\"\n                },\n                \"claims\": {\n                    \"minTLSCertDuration\": \"1s\",\n                    \"defaultTLSCertDuration\": \"5s\"\n                }\n            }\n        ],\n        \"template\": {\n            \"country\": \"US\",\n            \"locality\": \"San Francisco\",\n            \"organization\": \"Smallstep\"\n        }\n    }\n}\n"
  },
  {
    "path": "ca/testdata/rotated/intermediate_ca.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIBxTCCAWugAwIBAgIQLIY6MR/1fBRQY4ZTTsPAJjAKBggqhkjOPQQDAjAcMRow\nGAYDVQQDExFTbWFsbHN0ZXAgUm9vdCBDQTAeFw0xOTAxMDcyMDExMzBaFw0yOTAx\nMDQyMDExMzBaMCQxIjAgBgNVBAMTGVNtYWxsc3RlcCBJbnRlcm1lZGlhdGUgQ0Ew\nWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARgtjL/KLNpdq81YYWaek1lrkPM/QF1\nm+ujwv5jya21fAXljdBLh6m2xco1GPfwPBbwUGlNOdEqE9Nq3Qx3ngPKo4GGMIGD\nMA4GA1UdDwEB/wQEAwIBpjAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIw\nEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUqixeZ/K1HW9N6SVw7ONya98S\nu8UwHwYDVR0jBBgwFoAUgIzlCLxh/RlwEany4JQHOorLAIEwCgYIKoZIzj0EAwID\nSAAwRQIgdGX6lxThrKlt3v+3HJZlaWdmoeQ3vYwpJb9uHExZdVYCIQDCxsdI8EnB\nbxjnJscbT4zvqVsq6AmycdbFwgy8RIeVzg==\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "ca/testdata/rotated/intermediate_ca_key",
    "content": "-----BEGIN EC PRIVATE KEY-----\nProc-Type: 4,ENCRYPTED\nDEK-Info: AES-256-CBC,7dcc0a8c1d73c8d438184e0928875329\n\nr6yrQrHg6zBZRSjQpe8RzyQALEfiT3/8lMvvPu3BX6yign5skMfCVMXZhzbmAwmR\nBJBIX+5hkudR2VN+hrsOyuU7FvIk4gx2c8buIlFObfYXIml0mpuThfm52ciAtOTE\nS0hkfYvPcOAjzaDZ+8Po/mYhkODgyvijogn4ioTF/Ss=\n-----END EC PRIVATE KEY-----\n"
  },
  {
    "path": "ca/testdata/rotated/root_ca.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIBfTCCASKgAwIBAgIRAJPUE0MTA+fMz6f6i/XYmTwwCgYIKoZIzj0EAwIwHDEa\nMBgGA1UEAxMRU21hbGxzdGVwIFJvb3QgQ0EwHhcNMTkwMTA3MjAxMTMwWhcNMjkw\nMTA0MjAxMTMwWjAcMRowGAYDVQQDExFTbWFsbHN0ZXAgUm9vdCBDQTBZMBMGByqG\nSM49AgEGCCqGSM49AwEHA0IABCOH/PGThn0cMOGDeqDxb22olsdCm8hVdyW9cHQL\njfIYAqpWNh9f7E5umlnxkOy6OEROTtpq7etzfBbzb52loVWjRTBDMA4GA1UdDwEB\n/wQEAwIBpjASBgNVHRMBAf8ECDAGAQH/AgEBMB0GA1UdDgQWBBSAjOUIvGH9GXAR\nqfLglAc6issAgTAKBggqhkjOPQQDAgNJADBGAiEAjs0yjbQ/9dmGoUn7JS3lE83z\nYlnXZ0fHdeNakkIKhQICIQCUENhGZp63pMtm3ipgwp91EM0T7YtKgrFNvDekqufc\nSw==\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "ca/testdata/rotated/root_ca_key",
    "content": "-----BEGIN EC PRIVATE KEY-----\nProc-Type: 4,ENCRYPTED\nDEK-Info: AES-256-CBC,8ce79d28601b9809905ef7c362a20749\n\nH+pTTL3B5fLYycgHLxFOW0fZsayr7Y+BW8THKf12h8dk0/eOE1wNoX2TuMtpbZgO\nlMJdFPL+SAPCCmuZOZIcQDejRHVcYBq1wvrrnw/yfVawXC4xze+J4Y+q0J2WY+rM\nxcLGlEOIRZkvdDVGmSitEZBl0Ibk0p9tG++7QGqAvnk=\n-----END EC PRIVATE KEY-----\n"
  },
  {
    "path": "ca/testdata/rsaca.json",
    "content": "{\n    \"root\": \"../ca/testdata/secrets/rsa_root_ca.crt\",\n    \"federatedRoots\": [],\n    \"crt\": \"../ca/testdata/secrets/rsa_intermediate_ca.crt\",\n    \"key\": \"../ca/testdata/secrets/rsa_intermediate_ca_key\",\n    \"password\": \"1234\",\n    \"address\": \"127.0.0.1:0\",\n    \"dnsNames\": [\"127.0.0.1\"],\n    \"_logger\": {\"format\": \"text\"},\n    \"tls\": {\n        \"minVersion\": 1.2,\n        \"maxVersion\": 1.3,\n        \"renegotiation\": false,\n        \"cipherSuites\": [\n            \"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256\",\n\t\t\t\"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256\"\n        ]\n    },\n    \"authority\": {\n        \"backdate\": \"0s\",\n        \"provisioners\": [\n            {\n                \"name\": \"scep\",\n                \"type\": \"scep\",\n                \"challenge\": \"not-so-secret\"\n            }, {\n                \"name\": \"step-cli\",\n                \"type\": \"jwk\",\n                \"encryptedKey\": \"eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJlbmMiOiJBMTI4R0NNIiwicDJjIjoxMDAwMDAsInAycyI6IlhOdmYxQjgxSUlLMFA2NUkwcmtGTGcifQ.XaN9zcPQeWt49zchUDm34FECUTHfQTn_.tmNHPQDqR3ebsWfd.9WZr3YVdeOyJh36vvx0VlRtluhvYp4K7jJ1KGDr1qypwZ3ziBVSNbYYQ71du7fTtrnfG1wgGTVR39tWSzBU-zwQ5hdV3rpMAaEbod5zeW6SHd95H3Bvcb43YiiqJFNL5sGZzFb7FqzVmpsZ1efiv6sZaGDHtnCAL6r12UG5EZuqGfM0jGCZitUz2m9TUKXJL5DJ7MOYbFfkCEsUBPDm_TInliSVn2kMJhFa0VOe5wZk5YOuYM3lNYW64HGtbf-llN2Xk-4O9TfeSPizBx9ZqGpeu8pz13efUDT2WL9tWo6-0UE-CrG0bScm8lFTncTkHcu49_a5NaUBkYlBjEiw.thPcx3t1AUcWuEygXIY3Fg\",\n                \"key\": {\n                    \"use\": \"sig\",\n                    \"kty\": \"EC\",\n                    \"kid\": \"4UELJx8e0aS9m0CH3fZ0EB7D5aUPICb759zALHFejvc\",\n                    \"crv\": \"P-256\",\n                    \"alg\": \"ES256\",\n                    \"x\": \"7ZdAAMZCFU4XwgblI5RfZouBi8lYmF6DlZusNNnsbm8\",\n                    \"y\": \"sQr2JdzwD2fgyrymBEXWsxDxFNjjqN64qLLSbLdLZ9Y\"\n                }\n            }\n        ],\n        \"template\": {\n            \"country\": \"US\",\n            \"locality\": \"San Francisco\",\n            \"organization\": \"Smallstep\"\n        }\n    }\n}\n"
  },
  {
    "path": "ca/testdata/secrets/federated_ca.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIBfTCCASKgAwIBAgIRAJPUE0MTA+fMz6f6i/XYmTwwCgYIKoZIzj0EAwIwHDEa\nMBgGA1UEAxMRU21hbGxzdGVwIFJvb3QgQ0EwHhcNMTkwMTA3MjAxMTMwWhcNMjkw\nMTA0MjAxMTMwWjAcMRowGAYDVQQDExFTbWFsbHN0ZXAgUm9vdCBDQTBZMBMGByqG\nSM49AgEGCCqGSM49AwEHA0IABCOH/PGThn0cMOGDeqDxb22olsdCm8hVdyW9cHQL\njfIYAqpWNh9f7E5umlnxkOy6OEROTtpq7etzfBbzb52loVWjRTBDMA4GA1UdDwEB\n/wQEAwIBpjASBgNVHRMBAf8ECDAGAQH/AgEBMB0GA1UdDgQWBBSAjOUIvGH9GXAR\nqfLglAc6issAgTAKBggqhkjOPQQDAgNJADBGAiEAjs0yjbQ/9dmGoUn7JS3lE83z\nYlnXZ0fHdeNakkIKhQICIQCUENhGZp63pMtm3ipgwp91EM0T7YtKgrFNvDekqufc\nSw==\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "ca/testdata/secrets/intermediate_ca.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIB0DCCAXWgAwIBAgIQaYEAv6hTHRU+ZEnIJ6VB7zAKBggqhkjOPQQDAjAhMR8w\nHQYDVQQDExZTbWFsbHN0ZXAgVGVzdCBSb290IENBMB4XDTE4MDkyNzE4MTgwOVoX\nDTI4MDkyNDE4MTgwOVowKTEnMCUGA1UEAxMeU21hbGxzdGVwIFRlc3QgSW50ZXJt\nZWRpYXRlIENBMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEUnFoY688av7AhSsP\nvAMXHuA66zdzujzw/Wx0F/ZkWagbo52zskTxElrTt/Qkiotv33EKTUaJ7mSV/ZhW\nDaI6TqOBhjCBgzAOBgNVHQ8BAf8EBAMCAaYwHQYDVR0lBBYwFAYIKwYBBQUHAwEG\nCCsGAQUFBwMCMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFAKELAm5/V3t\n40xrDbKcDn5VWYThMB8GA1UdIwQYMBaAFAdgQF1Ej2WxY52Olc2wKVePE596MAoG\nCCqGSM49BAMCA0kAMEYCIQCoCUGx0W5wv3iQjlGIhux/zWZiDkyIbGj3ASeUL5v9\nQgIhAJ8dVOcqW3oq2TF9hHv8tXjhwmK44krO/FMK4gHljo4i\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "ca/testdata/secrets/intermediate_ca_key",
    "content": "-----BEGIN EC PRIVATE KEY-----\nProc-Type: 4,ENCRYPTED\nDEK-Info: AES-256-CBC,62bb1ccb9ed22ed553a479e34a4a0765\n\n6lqTXwNel3jJjj+LdkA1E3Xr7bbeSukQLouFq2cbjh9Zyqb2xuhS2goxWZw0DDmG\nrhCCKyiQnR+ImuHAwZnKBouWvp6po8CR4C1STNAX45wPfIhPV3UA49xbiA1sM+AE\nQrlwCWVk9x/JhkZURK0T/3TWtdk9llcnhSKfAXnekAA=\n-----END EC PRIVATE KEY-----\n"
  },
  {
    "path": "ca/testdata/secrets/ott_key",
    "content": "-----BEGIN EC PRIVATE KEY-----\nProc-Type: 4,ENCRYPTED\nDEK-Info: AES-256-CBC,f6870a50902e9397844faaf37f6196fc\n\nBVotbStC8KUiRyR6azjNu5nM1ER3/DtrdS/DxzDWJdWCPfayvQAU47DwoZdZ8Id2\nCu92bfKB0gQsgckPSfQhMC6sCd9JEiV7NqyLztDLnJJBmhml6fPMhoQaHAZy+qgW\nRiVrBaYXR92DTbtzFuYb03nmHeUVCjAT/R8Q21SCAfE=\n-----END EC PRIVATE KEY-----\n"
  },
  {
    "path": "ca/testdata/secrets/ott_key.public",
    "content": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEtTKthEHN7RuybhkaC43J2oLfBG99\n5FNSWbtahLAiK7Z7fDJxfBUHfroXTAsTkn2AimrwQhDj3TSccE2kgZ5sQA==\n-----END PUBLIC KEY-----\n"
  },
  {
    "path": "ca/testdata/secrets/ott_mariano_priv.jwk",
    "content": "{\n  \"protected\": \"eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJlbmMiOiJBMTI4R0NNIiwicDJjIjoxMDAwMDAsInAycyI6IlB1UnJVQ1RZZkR1T2F5MEh2cGl6bncifQ\",\n  \"encrypted_key\": \"7a-OP5xWGbFra8m2MN9YuLGt6v4y0wmB\",\n  \"iv\": \"u-54daK2y-0UO9na\",\n  \"ciphertext\": \"3GQy6E52-fOSUu5NJ_sEbxj_T3CTyWb7wOPFv2oI2PBWXp5CLpiWJbCFpF4v2oD9fN5XbxMP14ootbrFjATnoMWfWgyLwG-KOj9BqMGNxhG2v37yC7Wrris6s30nrPa3uyNEYZ12AOQW1K04cU2X0u_qJM3vzMCle548ZFTWs6_d6L8lp3o0F9MEbCmJ4p6CLqQxjxYtn1aD79lM91NbIXpRP3iUFQRly-y_iC2mSkXCdd_cQ6-dqLUchXwWRyVO5nBHb4J87aZ91VApw7ldTLtwRZ2ZGJpqGQGgjTwi4sgjEcMuGg0_83XGk2ubdlKDpmGFedOHS5rYCbxotts\",\n  \"tag\": \"vSYfxsi2UU9LQeySDjAnnQ\"\n}"
  },
  {
    "path": "ca/testdata/secrets/ott_mariano_pub.jwk",
    "content": "{\n  \"use\": \"sig\",\n  \"kty\": \"EC\",\n  \"kid\": \"FLIV7q23CXHrg75J2OSbvzwKJJqoxCYixjmsJirneOg\",\n  \"crv\": \"P-256\",\n  \"alg\": \"ES256\",\n  \"x\": \"tTKthEHN7RuybhkaC43J2oLfBG995FNSWbtahLAiK7Y\",\n  \"y\": \"e3wycXwVB366F0wLE5J9gIpq8EIQ4900nHBNpIGebEA\"\n}"
  },
  {
    "path": "ca/testdata/secrets/root_ca.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIBhzCCASygAwIBAgIRANJiwPnM38wWznkJGOcIyIYwCgYIKoZIzj0EAwIwITEf\nMB0GA1UEAxMWU21hbGxzdGVwIFRlc3QgUm9vdCBDQTAeFw0xODA5MjcxODE4MDla\nFw0yODA5MjQxODE4MDlaMCExHzAdBgNVBAMTFlNtYWxsc3RlcCBUZXN0IFJvb3Qg\nQ0EwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAS15w7dx9zPjCnQ7+RlRkvUXQJN\nFjk5Hg5K9nCoiiNQQhcQMw63/pXQxHNsugiMshcN59XJC8195KJPm25nXN8co0Uw\nQzAOBgNVHQ8BAf8EBAMCAaYwEgYDVR0TAQH/BAgwBgEB/wIBATAdBgNVHQ4EFgQU\nB2BAXUSPZbFjnY6VzbApV48Tn3owCgYIKoZIzj0EAwIDSQAwRgIhAJRTVmc2xW8c\nESx4oIp2d/OX9KBZzpcNi9fHnnJCS0FXAiEA7OpFb2+b8KBzg1c02x21PS7pHoET\n/A8LXNH4M06A7vE=\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "ca/testdata/secrets/root_ca_key",
    "content": "-----BEGIN EC PRIVATE KEY-----\nProc-Type: 4,ENCRYPTED\nDEK-Info: AES-256-CBC,3e0252253bf2ca8a21087f2f36c3bb4d\n\nYlSY9zZ7jEMEWqgk3IT3B+WuJrnAMn9OBtMeWMo9FL1eQFLfAJBwKiKdEUYyeAwi\nqi4nxx4MvfpkN02B53rmObUmAWQsxOPlMY3/KVkwQ1ovT/+eC/BGieBMvm/1aOYu\n7/rnNAvI/3gWrbQ59mW6pr2qjK2eHr08s6S6GUx3C2E=\n-----END EC PRIVATE KEY-----\n"
  },
  {
    "path": "ca/testdata/secrets/rsa_intermediate_ca.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIFJTCCAw2gAwIBAgIRAMBEHdXQtHUla+J13aUn/0gwDQYJKoZIhvcNAQELBQAw\nFjEUMBIGA1UEAxMLcnNhLXJvb3QtY2EwHhcNMjIxMjAyMTE0MzE2WhcNMzIxMTI5\nMTE0MzE2WjAeMRwwGgYDVQQDExNyc2EtaW50ZXJtZWRpYXRlLWNhMIICIjANBgkq\nhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEArxVkidtUrM6KIdGZ8a2QtJWezrTxTiEM\nlDeYqLd4CKp1bjQ7JOi1uc0mBG0Y4u5NwQRDk3L2aulLrENsPx4PMsPwMPXZgw67\nzTTuug1/uec8phW9IvEqu8FDQhFCMzZZMmc/0UTLmhJq5NZhIU8SQ6XYF/5s11Gm\nzBbBG1CEV6KcwVul8+T/GcHr60h2/X4uRkibEdUsDy0jHFLMPOWMeKQXoA8hVWHc\nQRYInRS5q+aFZ79YqMTUFT2tKdgSCiDsm6MqAPhFVB20ZrxMU6zco67+DBKAzSGy\nqO0H6fxkStN4RBrCFTgUdyUPwSe5xCOVfR4JbF8pXMI9cA7iCT0Mw9ZgbTncKVdn\nepwIZfqqYMP0C3EL+BZOSfEQeXIq7qlmHKwRRkc010ZaLmbKB9Kug/HcsS3CevU2\nJ0Efosi2xfMcfhi11rAfKvZpyAuOVap7BONro3yYXjv6Co9sDWtyK6VkLsczp2MM\nNHxhzjGXAcQdnU79UbGxO67imZm6FYLTwcg/6SVrfh+slLJ5nCyXqC/LaQ+Mc7Q+\nmdibgOzHSYg/QHVamic0uqn4BLw8QjICIZAnWWJHYjVgCieZrvK/7BGOjQ8+LT/8\nNhjI6MSuNMcXLxyOciiPw1r8fT/NUbJZblMDhibGTaOFCoMc3niY/fwxPb3p1J8I\ntmOLoK8HCysCAwEAAaNmMGQwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYB\nAf8CAQAwHQYDVR0OBBYEFO0ULj6Dt1RakbRqV4rVFUdRHK3KMB8GA1UdIwQYMBaA\nFCd9WZYMPpfDLBKjySFENwIXJpuzMA0GCSqGSIb3DQEBCwUAA4ICAQCiWxrj4HqV\nJ9tGj59Ea2cMZUcBfGPYh4dZ0af6IlNZnqW9ZlmNNF/h0VvCpd28STZlkW7hp2Xb\nRcJ0tXs3MvnU0Sqzw8ZTevJgIIbiOIwndfmi4apfSC63JXftBkThP0xpR5LI/4pH\nUPYyeGA13fynH4YmO4QBsGEXlKMKSYSjwrheYKkSB73AYlc7r8OqE/NAVHc1xzov\n9GT4p7w+tF6vrgzUtwqpAEVM/3USmSx4rgSdkI4DPkrYb1HEqT8ixOIH/3IG42ag\nUZgICckBPqcki8UbnU4nbxWVGJd18FE2n4wC2erewlBL+1PJFTmgDEKmOlcabot8\nQEk/YOpMThCm79VGuFB7frXoFefLCl5q1K5yV1eDsmr79ZFIy2WM2alnVk2Cvk/9\noJQQ42AWRVHGFuaIrG+hLLtwq17MnoeyQ/A2IRlpWu7DpaCVfuPA+3yQC06qo98u\nA3vGpifN8eohTSEMYNGQAsUsArYPwMEp/QrP4EwK8YnaJtd2HCnG4VS3D+RenRIF\n04b8EXX64ePD07uzPh7dKpWfmdJf1xj8GSndw2vk14KYDOvjrXirVkNCXFxgU9jp\nuTLGU/7Panm81xQgjeNwRaXxWvvDSrQaKMZ1QL6i0U7OTso0Q4VHivGG7IDhYSkA\nzNRdjmJnuap8XWGs/4xKjMJcv12UtnaMgw==\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "ca/testdata/secrets/rsa_intermediate_ca_key",
    "content": "-----BEGIN RSA PRIVATE KEY-----\nProc-Type: 4,ENCRYPTED\nDEK-Info: AES-256-CBC,03e26f42f8642e55946bcad62fef0c2e\n\n54jydVXdnixOccnF90L9pkfsy4mrRC9xyl4BbZMaYwplZC+LE+U80GAdXOqSxBEo\nsQBz+OTYaq2bmT2MDnoHty8I4vdDTVmxovc+NdtCJdC+etc2bSEKt68K57BPEqa0\no7SE5Lk39zSDIFkyltQeYNII8sCX7H26kRsfZhmDYPoFXGCfnxrEQoASaF8S9n3l\n9yERxk4untsVpvOPPde6Vn3b40ALqg0J0PaqzIbWifbWL8Uu3IeP27VHJLS4AH23\nemkWaZiT1bjWNevwWiU0REZ1CxyShaggJa4YwXPJJyRcQlvnVMZ8+DjXoQ1EdSGA\nEGMfG6i5zDrRAdRDRgbJM56wZqIWup+/Kd0WyVGOteGFhzyl8Pad65NGYP9saPE/\nP0/Wi51t30KllF6i6XHATeAKPgGAMkl8E9x9KCQVqGEWi8Ceu3w5AMxC1tcwB0Xy\n1X9NBipHaDh0DneTTdRRpwGCEIkZefDwy0z4rgsxrbKyY0YP1NKsFt+rNFkdNSnK\nRevnejtYHSDjOyGImnLRJ0c2nxwet93hfY1g3yzagKtWUp/TXOO7EkggqUPObQhC\nn9U5tkPxvHTCXSzeK3QqrbReyb3AlEay8Th8R8roxcClV83E4vcjjuvitcJ0MbSW\n+/jCU1WhCanat67je749MB19msA95XYxNsAmCn17vJIVRI/QBS9HQCkf7UW1Jptm\nhU06/7sytuOFboXh/xhfoQUomlx8Hl/GqV2yGZyL7SsH4sxoT9cVCO2vXCnr/r63\nUo1nkEHQNddbBCR7yvjoeeq5PypGxZibC7YzWx87Hwcr8dEhBwzoqeIFkhnEVMyq\nY3xFIilqqRaLwG1c77wy5jReTv/OTJ2OU2VFDu3Pf7zAOcGtcQazcNv7PMkiqunK\nBp/vDLL+LiaWO/5Zl49DFPTGkRj5kNK/aNajfHyw0hvYYytiaGaH9DM3L+7kC7YG\n2le8eLbUgZ7tqw3P2KueCK1F6Ef5X2It2sjxv/w5hz6lDtGfEIVXJuOamSUEewkY\n9xM9njmqFhQjb71Khm3+/HUoxvmOebpuQ884xORfvzJ1rl8IHA84VTo8/XKp3EST\nyMC39rGhtVuADHvNz3Y/WAWIbrJkkdZvMXyYKoOTosNVeFjJxyfKlz8ZMYmy6cM6\nmjOcsaI8xYUslYtpj/7vAjtcF4tJv94cQB/KGdUc/Z5JQ3r8zooG8ghEPt/5jiEr\n4ECCK7btew0mexVv+HY3rX7UiPCHugfX6+XEIxQ8+AsM27FNFaKxjxTE2r9h95mP\njmcWO7YqyqyeEZmKoxNo5oLMKXIDKxzK6ianJYg65xMnT+cH5vcnVaQKaC2QcnMI\nTiLOz/+ZdJSz2FiyE4myjnp9COKQhsDOfQA/1xzPF/4dqWMyWijGnlcozCHlU0i+\n2oG7izmDl9zn79v8VH6y0WjeEywoH5XlrF5eKBA2g7AtB8MCJTpIRVazTRbvhjaP\nEXr+Zk6vPVlDS0KOIUJ4V8iYcatdoaJz1fM3XjVZ6Wwy8TaYd9EBwWlWdFDx6r3s\n1aT5fDDyZNjnTx80OHyWT2IS/+/FrColWGc9s/t5raFm3KEnvVpFc+7/AKOV3keB\n+3KVSg4ILLDYf7PfMrT2IPrWObuUXZ2InZPEG3T7BOtbbdO8BDbDng1xLxPGDFgQ\nzKUFngsPO90PoDmNUZ9dBZ/oOI54e38hqUGB7vdTsNlX+VTK4n+qb8w7GzNhGgnR\nfTP927HeuFBdq8Y2ngxt2i6vg9yo7Ojd+nG5OLj2T7uyNraKdaaBx4Nd9ZUZbNGt\n4EueDSHCALKBsimLl4DfnMDnUK3G79dsoazs/nUr5y7kaUlkBGNZ/iSuoqpgeTKU\njsTmVjRj4W5opC+UUBiY/tE7qHGczLDw/mw/NP14nQ5iFdwi6EJv3viprYL+zL/A\nzRTkcQ0KqBfc1ChVWhvxIg7QCsnPT6+y0yn2k6n4a9cUvQXcOQKqF5eOJWPE3ZeC\n7fgIwt7ZdqHPZHyMxAnwWbmsj1Tn09SBW1b7S4t50aAPTRjDmrp5iC4vK59L7qPZ\nekoft0VduaJlKqq90Bh5ouRvTO6ytDI261bbEIGQqH1nJVt12bhNA6h3xI3Iwn/E\nqlMLAN1M36LenUEp9l77AfiFU1f+d8ZP2U6bJo4FKTnRR33R6+89sezUmVEuqozt\nqONJo0DE9XSAVhxpVX1QF91RjrJSiQtNyRkaOEyTsw2VvJpQNI5GAQN0TbcqgCVD\naUqPUuwntC7Wx7PkF6OR07rVxSIvhXs1NlG09nPZVByVCRJf/zKp9jMcRVJSXp16\n+Sqw4qifz/INEPGPgM3vr0GdvEN27S1IEFUZDU0M+e6KcHeIoLEPnhQPnZfO/kPT\n69gRFOZAcONvnGyP+Fj74fRWpWWIIN6b8oIzPN8tez9g+DdmXHf/LnD0fGIfhqPI\nGjjZcNJ8oa2F2qZfmwtrYs8UIChJxfZXK/lV7Jgf48ZDSF73war8nGHA/Sir4NsF\n9cp3TxTSpXo2iXqb8ZH679q7OJ3UE7OiVKr2XzVEo7T/QSPnV4l9eiq9lDb/1cnS\nAFfm3m0+Zqy+uE+Qfkigt5jWXBLQ3DbJEUNriumsit5dMeh2zCMwtYsWC8fumJw1\n6kJVZ7yEFXhFggTkHrgTZCI/9ym8FxCcz7W9qNy47h3aDOMs+yRidyl279FsKMR3\ngkjZmvGyAuZRqNttqldexMGwH1qVPIwDtCHdwesdefAydr/9h/ElDzAyBG31u3zN\n7Bp5/JkN9OycTvUB7SIMR80Q7wwPJngovRu1wdKQVZC+y/snJR6tQx9u+OuSHrB+\nX0J4LFuxSj5PjsTH5y2o3UFbuKzxaIwbEibPvUc7FqW7O9/N4gYZaANgcodo0ozb\nZjhcL+oE90AGQyKSKGna5bZWdokLQBOUyro442gKXAOVARMzEHwIIWwD3bm6Mj0a\nAmaMta3/LoCj54ESPFqRm7lCTmTj4gR6t5TED810hEimbxE8CBB6yrGTyj+vn+nH\n9Wn1D+Pgo0QuHp1yBZI5xrFtX2Dm6TW7cKuv0oohgjd2WFKNIqzDhOeIslk3K9TL\nkcBqeYMDJ5xi/R5/dfE5yLg7WhsPcH5QcMO2I6Sm+smXWytB8zo3NkX5UXUTdWNp\n-----END RSA PRIVATE KEY-----\n"
  },
  {
    "path": "ca/testdata/secrets/rsa_root_ca.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIE/DCCAuSgAwIBAgIRAOFB5q6CzRilW0ERurTeSQ4wDQYJKoZIhvcNAQELBQAw\nFjEUMBIGA1UEAxMLcnNhLXJvb3QtY2EwHhcNMjIxMjAyMTE0MjI1WhcNMzIxMTI5\nMTE0MjI1WjAWMRQwEgYDVQQDEwtyc2Etcm9vdC1jYTCCAiIwDQYJKoZIhvcNAQEB\nBQADggIPADCCAgoCggIBALIcD6VfJ6NZLWOhrLHr9au3WhKOmvt2gp+l53rjmwP3\nPLApSnFi3PGE9gvwzdGd0XeIIithgj+FiZEk/gdWfjx3abjpNM4uTsjBweQ4d3uT\nzgH5h/AmGbSVUweqOCvmK5cingcvc2UGVbDo5VOP50bZR8O9NY2OQNgFHig7Z+xT\neZSkGF7Sxm1zNMNU7BZqBNofFcwYDIaR/sBFuE9Im2qXj0duHbC1GXuVivE+iTDI\nir52qsuobnXwEQyGe3EOwIAD9AMPsmmJ/vZSaVLFO0dIbSwTqB3nXaNC8+hA/dyX\na9gEdVsSzKUiXfsk5awAOHOAEpCusywyJzZhhIyqot4rr3A3nuVOmg5utvJX2jMr\nwtGT7n7YhJWJVIcB/ahx/G7qwkcphEM7jnfweVgdDGTjcJ2tZchqx4U0axo+5wQy\nhebLz6z9QLkmfIMW0qjV6JcrYz2U1T4xSFmyNBhOrJQw4OFufSEWqYSJxoUHHOBn\nDy4V98AhoIkK5UDTeTrQea5QJRGRhiCfl6VpuO1YAP/4oNrJa+rWrzYPU5bq3FF2\nz2aCb9MAxnDQmfHfCSn6avioM2BcRQ8SfVVj1XsI4JtS7i7kqsHzuezJp28Jvll5\nsOTGp6CNASLJg2zRE3LZbNuuZ3JlVDZPDHqOqci7Gw8xwNXZv1SNNVDBDLsN3sSd\nAgMBAAGjRTBDMA4GA1UdDwEB/wQEAwIBBjASBgNVHRMBAf8ECDAGAQH/AgEBMB0G\nA1UdDgQWBBQnfVmWDD6XwywSo8khRDcCFyabszANBgkqhkiG9w0BAQsFAAOCAgEA\nmV/q1xjM9k+2Z9MhC7RXT0a/9bMVry9RiWp4xD09bPLRso+T9Pys/m222DxTjW6+\nJAM1fwm6HKESeWHToIBnB1htIG2jMSC5wn2/oKfEFnJU16f4lE7aoFMHP6Pxhf9w\ndGXvb7Pbze1MHNtNabx5x2uVp5DLTjOjL2o7pufSXNpB3djx20jADx5KqqXQiIqk\nrMDi1rpWRnNT/IqkkmDdGbG9WyKp28z8HPW2Iyq80zp1d3diJvtRZTeDTBrc8NGk\n96RpK1IVY0c8Z56UfecILuthm18ChSxm8DTXdc1CA1e89fiZ/pfEXPrbYLdcq8/b\nWQjA39z0zTiGC6gjd0g5hGeXZ5ThuW0s1EwpWmcF5bvHOxK2SOtYzxxy6bhbOzU7\n4J0uCj+GIR7eKtdrHdRv0cHFPE4/XDEI/93UCJjOphNekSKGUiQKzTZhjP7g6DdM\nbBtsdEwkVckqFTrOlHy1aDfoUzuOB8DDwSs/59h/0a2MtGBq1MAjLaZUlDAUUbYO\nx8VbloQHxcEdrUYmIGEhoI+zPz6Bm2xsaIs72R10y9PfFV5xY9JcsnA9AvJ2KOHo\nRH7gmqh7GyqCNcQf7bfhC2SLMa8luEn0tQFVx7F/vbO1rzpvsEvtsvHka1SEEqS/\nctNS8RyWPh92jaJQ4U9nWMHOJJZ4LYW38gsMc/om7+k=\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "ca/testdata/secrets/rsa_root_ca_key",
    "content": "-----BEGIN RSA PRIVATE KEY-----\nProc-Type: 4,ENCRYPTED\nDEK-Info: AES-256-CBC,2b2138d58dc4fe659251306226ee53ef\n\nf6H0Rvs3FmbjNk31qTe/CikGW6oFT3p+6A/g6E7gnloHuxVv4HdM0RDHOUvSMG49\nhb2kLYfbztJ7+RyGdc5JhozgfwRJsSP+iT0JbDQyHlTWzG5YasMnGrMbeLayn0Bw\nfhawiDOaBzK36hBFnx3aE5D3MOEbJM81/tZ7SoAovvgLZmhTH5w6cGYSJle6Fgey\n47skoiuRX4JJ1Us6aiME203l6AdPEs01XVPRQFZHMdbTCQ5ZVeH/BS2GHn2vfosg\nPLJ6RUQIILuBytRwiWZIXoVZDI7T0d6eiUizj2cyIi58rypkGDDeDvN6Uzq/r8Or\nepwo28YlIDRz4H40XIGVDnD8LcIbAmcfe2FTz+TbTTcQcBySnuQQJhJ6aGDnE2LG\n5QPSZOLAStlMP6ceGB6oeo7nBYLnqxUbDyeNeeIfBmf2NDgFpIwjgjs+QMXg1XFP\n/Z0BnKm/bmKKc94w3BwsAsZ0RwZTS+WyK+xKoXpQNVRoECKZt2oDTIPUX5no7dQO\nCQPOvJoYjGd+IS1jykvViYZYW4Lae08thWOMbWVTyV882/wpR7DN697w38VFLw2x\nq9dhd4wFZfzwGndO6xUq5h3qHGXg5xPS/ArvK5KGRXFusHI0HKqK5TeJwW2NBFky\nAhYPr/wdGdyL+mjU4ynjG4AekdAUi8t2Jpxf7+NuWGIbD/J00GPExOUuM68gqmuw\n9wGXj0EaPEyBSWc7Sq95o5+eg5VjwLsGnEKLtYLJRuwOnQ+6LZddOsaNNHgafP3N\nyrDN4Xu2NBowzrbAPv4nFxQF6pNkAJTtTOimRQA5qwf04Er1KSw3SAs0s8WXTDnp\nkySMpvSibBo4CQE+XvjGISg+yTY7/Uj6lZrJFzwrl4Nne1k960qofY7B6D8sGSxk\n8DZpsNkfY86juIBUri9pp2+nqEmj8NcK0gGpNgomYbPQHoQuudfWKER8JEX5hp9g\nik3RZIpes3yKJYbzEKpeAOMRy2yS75B9DOpvIO7YPfUsjGVWnV55Cqni+z2QUOWR\nlaRnRReQRQ/C3sinoFCEDZNmw5W5ex+iaGxj7d88tolFzvN6P7JdJTrq9kZ6pEIV\nyJnWT6dxoabxtyArpOAIwsEbeVXyFq1o0UF5x8Y1xOJOvlWallj0cZo/mHO+sFVT\nVLR1Ijh+klcKjJnU1s7yk/Ls/eRMJzSnk3iAV9WJuuOFyvpzmO26uTQh0f1rSrk/\nk4DA9Klywo9OFlCvGU5xuRhISDEBBrxtKQMkefFQRBxclqZldDmbss2Zr4vfmhjx\n5JdETi7q40Nt0kWsXi/XXIriEILvVIShYuER84aYSG2LQw3kOREA2BLOfJXYvRxc\ng3UzHviOYRpzPb7fJmOsSa1sRMWTKbZn1eBwFZbmqZmFboVzUmYUiFqFFCMGaPq0\nafhkZGmM/dPgStruKEyXCcAHnsIFruNZGICnDUbQyAwXlw66fJG/IwL0FbTdhr3i\n68wlLKA3uAAdTPkNQvef9Ed5b2xu9Yazt3ub93sKTbSzv1PZU+VVyrfmCVXXRrku\nybRoLd4HAeuMKZ7jF4dLNzPDvJ6SfdMP7Qw/NoeCBogbtsstsHe+3hOEmBYlPZpT\n+AyXV/BNEvli9uBUlwy9B7B6s0hj1bxMnxHTCxEuCBf3EYgRlwcIRqSCi7EV6FuT\n1ScpROJP3U1+FSF8b2pP7W23xAGtXUOBSoGvMlZcxF3+xB4L3zVMqfqwlbLXvSix\nQoXKYtESBmVVLT5jc+sWUelEynXowG+YaDVUyEBx99vlXAznQ3D99rD1dzvzx9Um\nTI0aV2IjeUgOXWP5b7rVs+GJ82DDUBBsZYEYK6JIpiBtdhAYhWbplusJCwdOAmyj\n+9JtfLrdJTohAn1smp285wHrHgdhLECEotaHqh8Cubrw5u66couCw7ibVSDrmshi\n8xiPL3hp0jWbE10Lah4MK5pMLfjq2wOta435RuD3HNJu1nGGvEIb/+Malef8JzBW\ny7iABGlAHPNhcOheNQX/nXuTnUOwv69N03/i+/hWzGHIjYH/nI02EZ+CHtuCbeUd\nJCP3Ia1xCDEZJEtb2GmswgB5P06U7z2rZemb2HIWC6/Sors72WlEZhtis1y9/mRF\n1pmGFsqmQCHk7XNrdZB56KjB4Kkj7eOE5xO01ALdZXs7nIhB7S9Sqk6Rtf+Th85N\n1BT/esB3d30ORVu3TbV1uashC71ThtdNEpYNi441Yfs8u/c/c+7NtUoxBcIIvMEs\nFMCLs4Nqt1y2UxocWGQtii2EvjwStAgtNIGhq+/6SZVRIU3CyYm3RRx5eQ1VdfNh\ni+bOJlf5l8/gZXsaWwD2tBOCibml9GbFJeGPQi9Rc6AUUeTGmNRnA1PUcwbs96uz\nF/lmo1dms5jiV2+d+SFQgAujrJSRsST4GxpqDlU3T/anIknusTkOyyuP3Z3EY6eA\nLiY6sdKYj40IFdpM3aLl6LAIgkTXS1ji4nvfu5CAdBAsntTRVRB2Ew3ux8+ZsShg\nRg/LMEmEP8oMq1JFrx9q2rlBghWyUdY5M+ZY/e8hGheMuaUGs8SeqWlI513+CvLw\nsWOUwnox+j9rjvj43Q3ac9mbqjwjykMpBDAMhAeJkW5FSK5gc6LPmRvUhfyv0De7\nbgA6dpQYh6+l3yKoWmNQdFZ0YtuEc+wzzgbyUE1s/BOTB3WDLaBnUAw7R3nkTUyX\n05t5b1NCcrj2fpe0DhRa7KqNQTVazEgZIkd0nPVGP8bmfMEMCXw2ri0wls0F4KkB\nY52Ctx+/kQkP8HYJMV79RURNvI9204C8a+w09++w9rmHuUlGXfJ7/iVADRaXI1pM\nE+N4q7KrhcQYlRWthmwsol2unqtnTHjSyHiYtHeagNTt2eNkAqG61E+mtYsjQ6Al\n+aL3vi73hJ6oNLpT8Cb2S4XYDziIlKTtX4biZYJgkc/P4Ado0Z5ZhXqLnt+BsrDv\nFuqpZoHp0BA9qaCPuocL7Ne6cVTY1PGKS+Gkh9u+QWmrp1QGltNQNUiNUiuSKP79\n41tdta3UYstwtuTydQPGbg71YPSXM6CqEUuYINP5yVSiO3k1aPA82Uxr3TYdnym7\nD54ctp9HHk3SYpA/zdT5clNwyNiTv/bZ2Wa0DUpBRK3epvLVB6fyGlmSFnOtyelP\n-----END RSA PRIVATE KEY-----\n"
  },
  {
    "path": "ca/testdata/secrets/step_cli_key",
    "content": "-----BEGIN EC PRIVATE KEY-----\nProc-Type: 4,ENCRYPTED\nDEK-Info: AES-128-CBC,e2c9c7cdad45b5032f1990b929cf83fd\n\nk3Yd307VgDrdllCBGN7PP8dOMQvEAUkq1lYtyxAWa7u/DuxeDP7SYlDB+xEk/UL8\nbgoYYCProydEElYFzGg8Z98WYAzbNoP2p6PPPpAhOZsxJjc5OfTHf/OQleR8PjD5\nryN4woGuq7Tiq5xritlyhluPc91ODqMsm4P98X1sPYA=\n-----END EC PRIVATE KEY-----\n"
  },
  {
    "path": "ca/testdata/secrets/step_cli_key.public",
    "content": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE7ZdAAMZCFU4XwgblI5RfZouBi8lY\nmF6DlZusNNnsbm+xCvYl3PAPZ+DKvKYERdazEPEU2OOo3riostJst0tn1g==\n-----END PUBLIC KEY-----\n"
  },
  {
    "path": "ca/testdata/secrets/step_cli_key_priv.jwk",
    "content": "{\n  \"protected\": \"eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJlbmMiOiJBMTI4R0NNIiwicDJjIjoxMDAwMDAsInAycyI6IlhOdmYxQjgxSUlLMFA2NUkwcmtGTGcifQ\",\n  \"encrypted_key\": \"XaN9zcPQeWt49zchUDm34FECUTHfQTn_\",\n  \"iv\": \"tmNHPQDqR3ebsWfd\",\n  \"ciphertext\": \"9WZr3YVdeOyJh36vvx0VlRtluhvYp4K7jJ1KGDr1qypwZ3ziBVSNbYYQ71du7fTtrnfG1wgGTVR39tWSzBU-zwQ5hdV3rpMAaEbod5zeW6SHd95H3Bvcb43YiiqJFNL5sGZzFb7FqzVmpsZ1efiv6sZaGDHtnCAL6r12UG5EZuqGfM0jGCZitUz2m9TUKXJL5DJ7MOYbFfkCEsUBPDm_TInliSVn2kMJhFa0VOe5wZk5YOuYM3lNYW64HGtbf-llN2Xk-4O9TfeSPizBx9ZqGpeu8pz13efUDT2WL9tWo6-0UE-CrG0bScm8lFTncTkHcu49_a5NaUBkYlBjEiw\",\n  \"tag\": \"thPcx3t1AUcWuEygXIY3Fg\"\n}"
  },
  {
    "path": "ca/testdata/secrets/step_cli_key_pub.jwk",
    "content": "{\n  \"use\": \"sig\",\n  \"kty\": \"EC\",\n  \"kid\": \"4UELJx8e0aS9m0CH3fZ0EB7D5aUPICb759zALHFejvc\",\n  \"crv\": \"P-256\",\n  \"alg\": \"ES256\",\n  \"x\": \"7ZdAAMZCFU4XwgblI5RfZouBi8lYmF6DlZusNNnsbm8\",\n  \"y\": \"sQr2JdzwD2fgyrymBEXWsxDxFNjjqN64qLLSbLdLZ9Y\"\n}"
  },
  {
    "path": "ca/tls.go",
    "content": "package ca\n\nimport (\n\t\"context\"\n\t\"crypto\"\n\t\"crypto/ecdsa\"\n\t\"crypto/ed25519\"\n\t\"crypto/rsa\"\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"encoding/pem\"\n\t\"net\"\n\t\"net/http\"\n\t\"os\"\n\t\"runtime\"\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n\t\"github.com/smallstep/certificates/api\"\n\t\"github.com/smallstep/certificates/ca/identity\"\n)\n\n// mTLSDialContext will hold the dial context function to use in\n// getDefaultTransport.\nvar mTLSDialContext func() func(ctx context.Context, network, address string) (net.Conn, error)\n\n// localAddr is the local address to use when dialing an address. This address\n// is defined by the environment variable STEP_CLIENT_ADDR.\nvar localAddr net.Addr\n\nfunc init() {\n\t// STEP_TLS_TUNNEL is an environment variable that can be set to do an TLS\n\t// over (m)TLS tunnel to step-ca using identity-like credentials. The value\n\t// is a path to a json file with the tunnel host, certificate, key and root\n\t// used to create the (m)TLS tunnel.\n\t//\n\t// The configuration should look like:\n\t//  {\n\t//      \"type\": \"tTLS\",\n\t//      \"host\": \"tunnel.example.com:443\"\n\t//      \"crt\": \"/path/to/tunnel.crt\",\n\t//      \"key\": \"/path/to/tunnel.key\",\n\t//      \"root\": \"/path/to/tunnel-root.crt\"\n\t//  }\n\t//\n\t// This feature is EXPERIMENTAL and might change at any time.\n\tif path := os.Getenv(\"STEP_TLS_TUNNEL\"); path != \"\" {\n\t\tid, err := identity.LoadIdentity(path)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t\tif err := id.Validate(); err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t\thost, port, err := net.SplitHostPort(id.Host)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t\tpool, err := id.GetCertPool()\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t\tmTLSDialContext = func() func(ctx context.Context, network, address string) (net.Conn, error) {\n\t\t\td := &tls.Dialer{\n\t\t\t\tNetDialer: createDefaultDialer(),\n\t\t\t\tConfig: &tls.Config{\n\t\t\t\t\tMinVersion:           tls.VersionTLS12,\n\t\t\t\t\tRootCAs:              pool,\n\t\t\t\t\tGetClientCertificate: id.GetClientCertificateFunc(),\n\t\t\t\t},\n\t\t\t}\n\t\t\treturn func(ctx context.Context, _, _ string) (net.Conn, error) {\n\t\t\t\treturn d.DialContext(ctx, \"tcp\", net.JoinHostPort(host, port))\n\t\t\t}\n\t\t}\n\t}\n\n\t// STEP_CLIENT_ADDR is an environment variable that can be set to define the\n\t// local address to use when dialing an address. This can be useful when\n\t// step is run behind a CIDR-based ACL.\n\t//\n\t// STEP_CLIENT_ADDR can be set to an IP (\"127.0.0.1\", \"[::1]\"), a hostname\n\t// (\"localhost\"), or a host:port (\"[::1]:0\"). If the port is set to\n\t// something other than \":0\" and the dialer is created multiple times it\n\t// will fail with an \"address already in use\" error.\n\t//\n\t// See https://github.com/smallstep/cli/issues/730\n\tif v := os.Getenv(\"STEP_CLIENT_ADDR\"); v != \"\" {\n\t\t_, _, err := net.SplitHostPort(v)\n\t\tif err != nil {\n\t\t\t// assuming that the error is a missing port, if it's not it will\n\t\t\t// panic below.\n\t\t\tv += \":0\"\n\t\t}\n\t\tlocalAddr, err = net.ResolveTCPAddr(\"tcp\", v)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t}\n}\n\n// GetClientTLSConfig returns a tls.Config for client use configured with the\n// sign certificate, and a new certificate pool with the sign root certificate.\n// The client certificate will automatically rotate before expiring.\nfunc (c *Client) GetClientTLSConfig(ctx context.Context, sign *api.SignResponse, pk crypto.PrivateKey, options ...TLSOption) (*tls.Config, error) {\n\ttlsConfig, _, err := c.getClientTLSConfig(ctx, sign, pk, options)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn tlsConfig, nil\n}\n\nfunc (c *Client) getClientTLSConfig(ctx context.Context, sign *api.SignResponse, pk crypto.PrivateKey, options []TLSOption) (*tls.Config, http.RoundTripper, error) {\n\tcert, err := TLSCertificate(sign, pk)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\trenewer, err := NewTLSRenewer(cert, nil)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\n\ttlsConfig := getDefaultTLSConfig(sign)\n\t// Note that with GetClientCertificate tlsConfig.Certificates is not used.\n\t// Without tlsConfig.Certificates there's not need to use tlsConfig.BuildNameToCertificate()\n\ttlsConfig.GetClientCertificate = renewer.GetClientCertificate\n\n\t// Apply options and initialize mutable tls.Config\n\ttlsCtx := newTLSOptionCtx(c, tlsConfig, sign)\n\tif err := tlsCtx.apply(options); err != nil {\n\t\treturn nil, nil, err\n\t}\n\n\ttr := getDefaultTransport(tlsConfig)\n\ttr.DialTLSContext = c.buildDialTLSContext(tlsCtx)\n\n\t// Add decorator if available, and use the resulting [http.RoundTripper]\n\t// going forward\n\trt := decorateRoundTripper(tr, c.transportDecorator)\n\trenewer.RenewCertificate = getRenewFunc(tlsCtx, c, rt, pk) //nolint:contextcheck // deeply nested context\n\n\t// Update client transport\n\tc.SetTransport(rt)\n\n\t// Start renewer\n\trenewer.RunContext(ctx)\n\treturn tlsConfig, rt, nil\n}\n\n// GetServerTLSConfig returns a tls.Config for server use configured with the\n// sign certificate, and a new certificate pool with the sign root certificate.\n// The returned tls.Config will only verify the client certificate if provided.\n// The server certificate will automatically rotate before expiring.\nfunc (c *Client) GetServerTLSConfig(ctx context.Context, sign *api.SignResponse, pk crypto.PrivateKey, options ...TLSOption) (*tls.Config, error) {\n\tcert, err := TLSCertificate(sign, pk)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\trenewer, err := NewTLSRenewer(cert, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\ttlsConfig := getDefaultTLSConfig(sign)\n\t// Note that GetCertificate will only be called if the client supplies SNI\n\t// information or if tlsConfig.Certificates is empty.\n\t// Without tlsConfig.Certificates there's not need to use tlsConfig.BuildNameToCertificate()\n\ttlsConfig.GetCertificate = renewer.GetCertificate\n\ttlsConfig.GetClientCertificate = renewer.GetClientCertificate\n\ttlsConfig.ClientAuth = tls.RequireAndVerifyClientCert\n\n\t// Apply options and initialize mutable tls.Config\n\ttlsCtx := newTLSOptionCtx(c, tlsConfig, sign)\n\tif err := tlsCtx.apply(options); err != nil {\n\t\treturn nil, err\n\t}\n\n\t// GetConfigForClient allows seamless root and federated roots rotation.\n\t// If the return of the callback is not-nil, it will use the returned\n\t// tls.Config instead of the default one.\n\ttlsConfig.GetConfigForClient = c.buildGetConfigForClient(tlsCtx)\n\n\t// Update renew function with transport\n\ttr := getDefaultTransport(tlsConfig)\n\ttr.DialTLSContext = c.buildDialTLSContext(tlsCtx)\n\n\t// Add decorator if available, and use the resulting [http.RoundTripper]\n\t// going forward\n\trt := decorateRoundTripper(tr, c.transportDecorator)\n\trenewer.RenewCertificate = getRenewFunc(tlsCtx, c, rt, pk) //nolint:contextcheck // deeply nested context\n\n\t// Update client transport\n\tc.SetTransport(rt)\n\n\t// Start renewer\n\trenewer.RunContext(ctx)\n\treturn tlsConfig, nil\n}\n\n// Transport returns an [http.RoundTripper] configured to use the client\n// certificate from the sign response.\nfunc (c *Client) Transport(ctx context.Context, sign *api.SignResponse, pk crypto.PrivateKey, options ...TLSOption) (http.RoundTripper, error) {\n\t_, tr, err := c.getClientTLSConfig(ctx, sign, pk, options)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn tr, nil\n}\n\n// buildGetConfigForClient returns an implementation of GetConfigForClient\n// callback in tls.Config.\n//\n// If the implementation returns a nil tls.Config, the original Config will be\n// used, but if it's non-nil, the returned Config will be used to handle this\n// connection.\nfunc (c *Client) buildGetConfigForClient(ctx *TLSOptionCtx) func(*tls.ClientHelloInfo) (*tls.Config, error) {\n\treturn func(*tls.ClientHelloInfo) (*tls.Config, error) {\n\t\treturn ctx.mutableConfig.TLSConfig(), nil\n\t}\n}\n\n// buildDialTLSContext returns an implementation of DialTLSContext callback in http.Transport.\nfunc (c *Client) buildDialTLSContext(tlsCtx *TLSOptionCtx) func(ctx context.Context, network, addr string) (net.Conn, error) {\n\treturn func(ctx context.Context, network, addr string) (net.Conn, error) {\n\t\td := createDefaultDialer()\n\t\t// TLS dialers do not support context, but we can use the context\n\t\t// deadline if it is set.\n\t\tif t, ok := ctx.Deadline(); ok {\n\t\t\td.Deadline = t\n\t\t}\n\t\treturn tls.DialWithDialer(d, network, addr, tlsCtx.mutableConfig.TLSConfig())\n\t}\n}\n\n// Certificate returns the server or client certificate from the sign response.\nfunc Certificate(sign *api.SignResponse) (*x509.Certificate, error) {\n\tif sign.ServerPEM.Certificate == nil {\n\t\treturn nil, errors.New(\"ca: certificate does not exist\")\n\t}\n\treturn sign.ServerPEM.Certificate, nil\n}\n\n// IntermediateCertificate returns the CA intermediate certificate from the sign\n// response.\nfunc IntermediateCertificate(sign *api.SignResponse) (*x509.Certificate, error) {\n\tif sign.CaPEM.Certificate == nil {\n\t\treturn nil, errors.New(\"ca: certificate does not exist\")\n\t}\n\treturn sign.CaPEM.Certificate, nil\n}\n\n// RootCertificate returns the root certificate from the sign response.\nfunc RootCertificate(sign *api.SignResponse) (*x509.Certificate, error) {\n\tif sign == nil || sign.TLS == nil || len(sign.TLS.VerifiedChains) == 0 {\n\t\treturn nil, errors.New(\"ca: certificate does not exist\")\n\t}\n\tlastChain := sign.TLS.VerifiedChains[len(sign.TLS.VerifiedChains)-1]\n\tif len(lastChain) == 0 {\n\t\treturn nil, errors.New(\"ca: certificate does not exist\")\n\t}\n\treturn lastChain[len(lastChain)-1], nil\n}\n\n// TLSCertificate creates a new TLS certificate from the sign response and the\n// private key used.\nfunc TLSCertificate(sign *api.SignResponse, pk crypto.PrivateKey) (*tls.Certificate, error) {\n\tcertPEM, err := getPEM(sign.ServerPEM)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tcaPEM, err := getPEM(sign.CaPEM)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tkeyPEM, err := getPEM(pk)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t//nolint:gocritic // using a new variable for clarity\n\tchain := append(certPEM, caPEM...)\n\tcert, err := tls.X509KeyPair(chain, keyPEM)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"error creating tls certificate\")\n\t}\n\tleaf, err := x509.ParseCertificate(cert.Certificate[0])\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"error parsing tls certificate\")\n\t}\n\tcert.Leaf = leaf\n\treturn &cert, nil\n}\n\nfunc getDefaultTLSConfig(sign *api.SignResponse) *tls.Config {\n\tif sign.TLSOptions != nil {\n\t\treturn sign.TLSOptions.TLSConfig()\n\t}\n\treturn &tls.Config{\n\t\tMinVersion: tls.VersionTLS12,\n\t}\n}\n\n// createDefaultDialer returns a new dialer with the default configuration.\nfunc createDefaultDialer() *net.Dialer {\n\t// With the KeepAlive parameter set to 0, it will be use Golang's default.\n\treturn &net.Dialer{\n\t\tTimeout:   30 * time.Second,\n\t\tLocalAddr: localAddr,\n\t}\n}\n\n// getDefaultTransport returns an http.Transport with the same parameters than\n// http.DefaultTransport, but adds the given tls.Config and configures the\n// transport for HTTP/2.\nfunc getDefaultTransport(tlsConfig *tls.Config) *http.Transport {\n\tvar dialContext func(ctx context.Context, network string, addr string) (net.Conn, error)\n\tswitch {\n\tcase runtime.GOOS == \"js\" && runtime.GOARCH == \"wasm\":\n\t\t// when running in js/wasm and using the default dialer context all requests\n\t\t// performed by the CA client resulted in a \"protocol not supported\" error.\n\t\t// By setting the dial context to nil requests will be handled by the browser\n\t\t// fetch API instead. Currently this will always set the dial context to nil,\n\t\t// but we could implement some additional logic similar to what's found in\n\t\t// https://github.com/golang/go/pull/46923/files to support a different dial\n\t\t// context if it is available, required and expected to work.\n\t\tdialContext = nil\n\tcase mTLSDialContext == nil:\n\t\td := createDefaultDialer()\n\t\tdialContext = d.DialContext\n\tdefault:\n\t\tdialContext = mTLSDialContext()\n\t}\n\treturn &http.Transport{\n\t\tProxy:                 http.ProxyFromEnvironment,\n\t\tDialContext:           dialContext,\n\t\tForceAttemptHTTP2:     true,\n\t\tMaxIdleConns:          100,\n\t\tIdleConnTimeout:       90 * time.Second,\n\t\tTLSHandshakeTimeout:   10 * time.Second,\n\t\tExpectContinueTimeout: 1 * time.Second,\n\t\tTLSClientConfig:       tlsConfig,\n\t}\n}\n\nfunc getPEM(i interface{}) ([]byte, error) {\n\tblock := new(pem.Block)\n\tswitch i := i.(type) {\n\tcase api.Certificate:\n\t\tblock.Type = \"CERTIFICATE\"\n\t\tblock.Bytes = i.Raw\n\tcase *x509.Certificate:\n\t\tblock.Type = \"CERTIFICATE\"\n\t\tblock.Bytes = i.Raw\n\tcase *rsa.PrivateKey:\n\t\tblock.Type = \"RSA PRIVATE KEY\"\n\t\tblock.Bytes = x509.MarshalPKCS1PrivateKey(i)\n\tcase *ecdsa.PrivateKey:\n\t\tvar err error\n\t\tblock.Type = \"EC PRIVATE KEY\"\n\t\tblock.Bytes, err = x509.MarshalECPrivateKey(i)\n\t\tif err != nil {\n\t\t\treturn nil, errors.Wrap(err, \"error marshaling private key\")\n\t\t}\n\tcase ed25519.PrivateKey:\n\t\tvar err error\n\t\tblock.Type = \"PRIVATE KEY\"\n\t\tblock.Bytes, err = x509.MarshalPKCS8PrivateKey(i)\n\t\tif err != nil {\n\t\t\treturn nil, errors.Wrap(err, \"error marshaling private key\")\n\t\t}\n\tdefault:\n\t\treturn nil, errors.Errorf(\"unsupported key type %T\", i)\n\t}\n\treturn pem.EncodeToMemory(block), nil\n}\n\nfunc getRenewFunc(ctx *TLSOptionCtx, client *Client, tr http.RoundTripper, pk crypto.PrivateKey) RenewFunc {\n\treturn func() (*tls.Certificate, error) {\n\t\t// Close connections in keep-alive state\n\t\tdefer client.CloseIdleConnections()\n\n\t\t// Get updated list of roots\n\t\tif err := ctx.applyRenew(); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\t// Get new certificate\n\t\tsign, err := client.Renew(tr)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn TLSCertificate(sign, pk)\n\t}\n}\n"
  },
  {
    "path": "ca/tls_options.go",
    "content": "package ca\n\nimport (\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\n\t\"github.com/smallstep/certificates/api\"\n)\n\n// TLSOption defines the type of a function that modifies a tls.Config.\ntype TLSOption func(ctx *TLSOptionCtx) error\n\n// TLSOptionCtx is the context modified on TLSOption methods.\ntype TLSOptionCtx struct {\n\tClient        *Client\n\tConfig        *tls.Config\n\tSign          *api.SignResponse\n\tOnRenewFunc   []TLSOption\n\tmutableConfig *mutableTLSConfig\n\thasRootCA     bool\n\thasClientCA   bool\n}\n\n// newTLSOptionCtx creates the TLSOption context.\nfunc newTLSOptionCtx(c *Client, config *tls.Config, sign *api.SignResponse) *TLSOptionCtx {\n\treturn &TLSOptionCtx{\n\t\tClient:        c,\n\t\tConfig:        config,\n\t\tSign:          sign,\n\t\tmutableConfig: newMutableTLSConfig(),\n\t}\n}\n\nfunc (ctx *TLSOptionCtx) apply(options []TLSOption) error {\n\tfor _, fn := range options {\n\t\tif err := fn(ctx); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\t// Initialize mutable config with the fully configured tls.Config\n\tctx.mutableConfig.Init(ctx.Config)\n\n\t// Build RootCAs and ClientCAs with given root certificate if necessary\n\tif root, err := RootCertificate(ctx.Sign); err == nil {\n\t\tif !ctx.hasRootCA {\n\t\t\tif ctx.Config.RootCAs == nil {\n\t\t\t\tctx.Config.RootCAs = x509.NewCertPool()\n\t\t\t}\n\t\t\tctx.Config.RootCAs.AddCert(root)\n\t\t\tctx.mutableConfig.AddImmutableRootCACert(root)\n\t\t}\n\n\t\tif !ctx.hasClientCA && ctx.Config.ClientAuth != tls.NoClientCert {\n\t\t\tif ctx.Config.ClientCAs == nil {\n\t\t\t\tctx.Config.ClientCAs = x509.NewCertPool()\n\t\t\t}\n\t\t\tctx.Config.ClientCAs.AddCert(root)\n\t\t\tctx.mutableConfig.AddImmutableClientCACert(root)\n\t\t}\n\t}\n\n\t// Update tls.Config with mutable data\n\tif ctx.Config.RootCAs == nil && len(ctx.mutableConfig.mutRootCerts) > 0 {\n\t\tctx.Config.RootCAs = x509.NewCertPool()\n\t}\n\tif ctx.Config.ClientCAs == nil && len(ctx.mutableConfig.mutClientCerts) > 0 {\n\t\tctx.Config.ClientCAs = x509.NewCertPool()\n\t}\n\t// Add mutable certificates\n\tfor _, cert := range ctx.mutableConfig.mutRootCerts {\n\t\tctx.Config.RootCAs.AddCert(cert)\n\t}\n\tfor _, cert := range ctx.mutableConfig.mutClientCerts {\n\t\tctx.Config.ClientCAs.AddCert(cert)\n\t}\n\tctx.mutableConfig.Reload()\n\treturn nil\n}\n\nfunc (ctx *TLSOptionCtx) applyRenew() error {\n\tfor _, fn := range ctx.OnRenewFunc {\n\t\tif err := fn(ctx); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\t// Reload mutable config with the changes\n\tctx.mutableConfig.Reload()\n\treturn nil\n}\n\n// RequireAndVerifyClientCert is a tls.Config option used on servers to enforce\n// a valid TLS client certificate. This is the default option for mTLS servers.\nfunc RequireAndVerifyClientCert() TLSOption {\n\treturn func(ctx *TLSOptionCtx) error {\n\t\tctx.Config.ClientAuth = tls.RequireAndVerifyClientCert\n\t\treturn nil\n\t}\n}\n\n// VerifyClientCertIfGiven is a tls.Config option used on on servers to validate\n// a TLS client certificate if it is provided. It does not requires a certificate.\nfunc VerifyClientCertIfGiven() TLSOption {\n\treturn func(ctx *TLSOptionCtx) error {\n\t\tctx.Config.ClientAuth = tls.VerifyClientCertIfGiven\n\t\treturn nil\n\t}\n}\n\n// AddRootCA adds to the tls.Config RootCAs the given certificate. RootCAs\n// defines the set of root certificate authorities that clients use when\n// verifying server certificates.\nfunc AddRootCA(cert *x509.Certificate) TLSOption {\n\treturn func(ctx *TLSOptionCtx) error {\n\t\tif ctx.Config.RootCAs == nil {\n\t\t\tctx.Config.RootCAs = x509.NewCertPool()\n\t\t}\n\t\tctx.hasRootCA = true\n\t\tctx.Config.RootCAs.AddCert(cert)\n\t\tctx.mutableConfig.AddImmutableRootCACert(cert)\n\t\treturn nil\n\t}\n}\n\n// AddClientCA adds to the tls.Config ClientCAs the given certificate. ClientCAs\n// defines the set of root certificate authorities that servers use if required\n// to verify a client certificate by the policy in ClientAuth.\nfunc AddClientCA(cert *x509.Certificate) TLSOption {\n\treturn func(ctx *TLSOptionCtx) error {\n\t\tif ctx.Config.ClientCAs == nil {\n\t\t\tctx.Config.ClientCAs = x509.NewCertPool()\n\t\t}\n\t\tctx.hasClientCA = true\n\t\tctx.Config.ClientCAs.AddCert(cert)\n\t\tctx.mutableConfig.AddImmutableClientCACert(cert)\n\t\treturn nil\n\t}\n}\n\n// AddRootsToRootCAs does a roots request and adds to the tls.Config RootCAs all\n// the certificates in the response. RootCAs defines the set of root certificate\n// authorities that clients use when verifying server certificates.\n//\n// BootstrapServer and BootstrapClient methods include this option by default.\nfunc AddRootsToRootCAs() TLSOption {\n\t// var once sync.Once\n\tfn := func(ctx *TLSOptionCtx) error {\n\t\tcerts, err := ctx.Client.Roots()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tctx.hasRootCA = true\n\t\tctx.mutableConfig.AddRootCAs(certs.Certificates)\n\t\treturn nil\n\t}\n\treturn func(ctx *TLSOptionCtx) error {\n\t\tctx.OnRenewFunc = append(ctx.OnRenewFunc, fn)\n\t\treturn fn(ctx)\n\t}\n}\n\n// AddRootsToClientCAs does a roots request and adds to the tls.Config ClientCAs\n// all the certificates in the response. ClientCAs defines the set of root\n// certificate authorities that servers use if required to verify a client\n// certificate by the policy in ClientAuth.\n//\n// BootstrapServer method includes this option by default.\nfunc AddRootsToClientCAs() TLSOption {\n\t// var once sync.Once\n\tfn := func(ctx *TLSOptionCtx) error {\n\t\tcerts, err := ctx.Client.Roots()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tctx.hasClientCA = true\n\t\tctx.mutableConfig.AddClientCAs(certs.Certificates)\n\t\treturn nil\n\t}\n\treturn func(ctx *TLSOptionCtx) error {\n\t\tctx.OnRenewFunc = append(ctx.OnRenewFunc, fn)\n\t\treturn fn(ctx)\n\t}\n}\n\n// AddFederationToRootCAs does a federation request and adds to the tls.Config\n// RootCAs all the certificates in the response. RootCAs defines the set of root\n// certificate authorities that clients use when verifying server certificates.\nfunc AddFederationToRootCAs() TLSOption {\n\tfn := func(ctx *TLSOptionCtx) error {\n\t\tcerts, err := ctx.Client.Federation()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tctx.mutableConfig.AddRootCAs(certs.Certificates)\n\t\treturn nil\n\t}\n\treturn func(ctx *TLSOptionCtx) error {\n\t\tctx.OnRenewFunc = append(ctx.OnRenewFunc, fn)\n\t\treturn fn(ctx)\n\t}\n}\n\n// AddFederationToClientCAs does a federation request and adds to the tls.Config\n// ClientCAs all the certificates in the response. ClientCAs defines the set of\n// root certificate authorities that servers use if required to verify a client\n// certificate by the policy in ClientAuth.\nfunc AddFederationToClientCAs() TLSOption {\n\tfn := func(ctx *TLSOptionCtx) error {\n\t\tcerts, err := ctx.Client.Federation()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tctx.mutableConfig.AddClientCAs(certs.Certificates)\n\t\treturn nil\n\t}\n\treturn func(ctx *TLSOptionCtx) error {\n\t\tctx.OnRenewFunc = append(ctx.OnRenewFunc, fn)\n\t\treturn fn(ctx)\n\t}\n}\n\n// AddRootsToCAs does a roots request and adds the resulting certs to the\n// tls.Config RootCAs and ClientCAs. Combines the functionality of\n// AddRootsToRootCAs and AddRootsToClientCAs.\nfunc AddRootsToCAs() TLSOption {\n\tfn := func(ctx *TLSOptionCtx) error {\n\t\tcerts, err := ctx.Client.Roots()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tctx.hasRootCA = true\n\t\tctx.hasClientCA = true\n\t\tctx.mutableConfig.AddRootCAs(certs.Certificates)\n\t\tctx.mutableConfig.AddClientCAs(certs.Certificates)\n\t\treturn nil\n\t}\n\treturn func(ctx *TLSOptionCtx) error {\n\t\tctx.OnRenewFunc = append(ctx.OnRenewFunc, fn)\n\t\treturn fn(ctx)\n\t}\n}\n\n// AddFederationToCAs does a federation request and adds the resulting certs to the\n// tls.Config RootCAs and ClientCAs. Combines the functionality of\n// AddFederationToRootCAs and AddFederationToClientCAs.\nfunc AddFederationToCAs() TLSOption {\n\tfn := func(ctx *TLSOptionCtx) error {\n\t\tcerts, err := ctx.Client.Federation()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif ctx.mutableConfig == nil {\n\t\t\tif ctx.Config.RootCAs == nil {\n\t\t\t\tctx.Config.RootCAs = x509.NewCertPool()\n\t\t\t}\n\t\t\tif ctx.Config.ClientCAs == nil {\n\t\t\t\tctx.Config.ClientCAs = x509.NewCertPool()\n\t\t\t}\n\t\t\tfor _, cert := range certs.Certificates {\n\t\t\t\tctx.Config.RootCAs.AddCert(cert.Certificate)\n\t\t\t\tctx.Config.ClientCAs.AddCert(cert.Certificate)\n\t\t\t}\n\t\t} else {\n\t\t\tctx.mutableConfig.AddRootCAs(certs.Certificates)\n\t\t\tctx.mutableConfig.AddClientCAs(certs.Certificates)\n\t\t}\n\t\treturn nil\n\t}\n\treturn func(ctx *TLSOptionCtx) error {\n\t\tctx.OnRenewFunc = append(ctx.OnRenewFunc, fn)\n\t\treturn fn(ctx)\n\t}\n}\n"
  },
  {
    "path": "ca/tls_options_test.go",
    "content": "package ca\n\nimport (\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"os\"\n\t\"reflect\"\n\t\"sort\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/smallstep/certificates/api\"\n)\n\n//nolint:gosec // test tls config\nfunc Test_newTLSOptionCtx(t *testing.T) {\n\tclient, err := NewClient(\"https://ca.smallstep.com\", WithTransport(http.DefaultTransport))\n\tif err != nil {\n\t\tt.Fatalf(\"NewClient() error = %v\", err)\n\t}\n\n\ttype args struct {\n\t\tc      *Client\n\t\tconfig *tls.Config\n\t\tsign   *api.SignResponse\n\t}\n\ttests := []struct {\n\t\tname string\n\t\targs args\n\t\twant *TLSOptionCtx\n\t}{\n\t\t{\"ok\", args{client, &tls.Config{}, &api.SignResponse{}}, &TLSOptionCtx{Client: client, Config: &tls.Config{}, Sign: &api.SignResponse{}, mutableConfig: newMutableTLSConfig()}},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif got := newTLSOptionCtx(tt.args.c, tt.args.config, tt.args.sign); !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"newTLSOptionCtx() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\n//nolint:gosec // test tls config\nfunc TestTLSOptionCtx_apply(t *testing.T) {\n\tfail := func() TLSOption {\n\t\treturn func(ctx *TLSOptionCtx) error {\n\t\t\treturn fmt.Errorf(\"an error\")\n\t\t}\n\t}\n\n\ttype fields struct {\n\t\tConfig *tls.Config\n\t}\n\ttype args struct {\n\t\toptions []TLSOption\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tfields  fields\n\t\targs    args\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", fields{&tls.Config{}}, args{[]TLSOption{RequireAndVerifyClientCert()}}, false},\n\t\t{\"ok\", fields{&tls.Config{}}, args{[]TLSOption{VerifyClientCertIfGiven()}}, false},\n\t\t{\"fail\", fields{&tls.Config{}}, args{[]TLSOption{VerifyClientCertIfGiven(), fail()}}, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tctx := &TLSOptionCtx{\n\t\t\t\tConfig:        tt.fields.Config,\n\t\t\t\tmutableConfig: newMutableTLSConfig(),\n\t\t\t}\n\t\t\tif err := ctx.apply(tt.args.options); (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"TLSOptionCtx.apply() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\t\t})\n\t}\n}\n\n//nolint:gosec // test tls config\nfunc TestRequireAndVerifyClientCert(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\twant *tls.Config\n\t}{\n\t\t{\"ok\", &tls.Config{ClientAuth: tls.RequireAndVerifyClientCert}},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tctx := &TLSOptionCtx{\n\t\t\t\tConfig:        &tls.Config{},\n\t\t\t\tmutableConfig: newMutableTLSConfig(),\n\t\t\t}\n\t\t\tif err := RequireAndVerifyClientCert()(ctx); err != nil {\n\t\t\t\tt.Errorf(\"RequireAndVerifyClientCert() error = %v\", err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(ctx.Config, tt.want) {\n\t\t\t\tt.Errorf(\"RequireAndVerifyClientCert() = %v, want %v\", ctx.Config, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\n//nolint:gosec // test tls config\nfunc TestVerifyClientCertIfGiven(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\twant *tls.Config\n\t}{\n\t\t{\"ok\", &tls.Config{ClientAuth: tls.VerifyClientCertIfGiven}},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tctx := &TLSOptionCtx{\n\t\t\t\tConfig:        &tls.Config{},\n\t\t\t\tmutableConfig: newMutableTLSConfig(),\n\t\t\t}\n\t\t\tif err := VerifyClientCertIfGiven()(ctx); err != nil {\n\t\t\t\tt.Errorf(\"VerifyClientCertIfGiven() error = %v\", err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(ctx.Config, tt.want) {\n\t\t\t\tt.Errorf(\"VerifyClientCertIfGiven() = %v, want %v\", ctx.Config, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\n//nolint:gosec // test tls config\nfunc TestAddRootCA(t *testing.T) {\n\tcert := parseCertificate(t, rootPEM)\n\tpool := x509.NewCertPool()\n\tpool.AddCert(cert)\n\n\ttype args struct {\n\t\tcert *x509.Certificate\n\t}\n\ttests := []struct {\n\t\tname string\n\t\targs args\n\t\twant *tls.Config\n\t}{\n\t\t{\"ok\", args{cert}, &tls.Config{RootCAs: pool}},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tctx := &TLSOptionCtx{\n\t\t\t\tConfig:        &tls.Config{},\n\t\t\t\tmutableConfig: newMutableTLSConfig(),\n\t\t\t}\n\t\t\tif err := AddRootCA(tt.args.cert)(ctx); err != nil {\n\t\t\t\tt.Errorf(\"AddRootCA() error = %v\", err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(ctx.Config, tt.want) && !equalPools(ctx.Config.RootCAs, tt.want.RootCAs) {\n\t\t\t\tt.Errorf(\"AddRootCA() = %v, want %v\", ctx.Config, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\n//nolint:gosec // test tls config\nfunc TestAddClientCA(t *testing.T) {\n\tcert := parseCertificate(t, rootPEM)\n\tpool := x509.NewCertPool()\n\tpool.AddCert(cert)\n\n\ttype args struct {\n\t\tcert *x509.Certificate\n\t}\n\ttests := []struct {\n\t\tname string\n\t\targs args\n\t\twant *tls.Config\n\t}{\n\t\t{\"ok\", args{cert}, &tls.Config{ClientCAs: pool}},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tctx := &TLSOptionCtx{\n\t\t\t\tConfig:        &tls.Config{},\n\t\t\t\tmutableConfig: newMutableTLSConfig(),\n\t\t\t}\n\t\t\tif err := AddClientCA(tt.args.cert)(ctx); err != nil {\n\t\t\t\tt.Errorf(\"AddClientCA() error = %v\", err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(ctx.Config, tt.want) && !equalPools(ctx.Config.ClientCAs, tt.want.ClientCAs) {\n\t\t\t\tt.Errorf(\"AddClientCA() = %v, want %v\", ctx.Config, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\n//nolint:gosec // test tls config\nfunc TestAddRootsToRootCAs(t *testing.T) {\n\tca := startCATestServer(t)\n\tdefer ca.Close()\n\n\tclient, err := NewClient(ca.URL, WithRootFile(\"testdata/secrets/root_ca.crt\"))\n\trequire.NoError(t, err)\n\n\tclientFail, err := NewClient(ca.URL, WithTransport(http.DefaultTransport))\n\trequire.NoError(t, err)\n\n\troot, err := os.ReadFile(\"testdata/secrets/root_ca.crt\")\n\trequire.NoError(t, err)\n\n\tcert := parseCertificate(t, string(root))\n\tpool := x509.NewCertPool()\n\tpool.AddCert(cert)\n\n\ttype args struct {\n\t\tclient *Client\n\t\tconfig *tls.Config\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\targs    args\n\t\twant    *tls.Config\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", args{client, &tls.Config{}}, &tls.Config{RootCAs: pool}, false},\n\t\t{\"fail\", args{clientFail, &tls.Config{}}, &tls.Config{}, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tctx := &TLSOptionCtx{\n\t\t\t\tClient:        tt.args.client,\n\t\t\t\tConfig:        tt.args.config,\n\t\t\t\tmutableConfig: newMutableTLSConfig(),\n\t\t\t}\n\t\t\tif err := ctx.apply([]TLSOption{AddRootsToRootCAs()}); (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"AddRootsToRootCAs() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !equalPools(ctx.Config.RootCAs, tt.want.RootCAs) {\n\t\t\t\tt.Errorf(\"AddRootsToRootCAs() = %v, want %v\", ctx.Config, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\n//nolint:gosec // test tls config\nfunc TestAddRootsToClientCAs(t *testing.T) {\n\tca := startCATestServer(t)\n\tdefer ca.Close()\n\n\tclient, err := NewClient(ca.URL, WithRootFile(\"testdata/secrets/root_ca.crt\"))\n\trequire.NoError(t, err)\n\n\tclientFail, err := NewClient(ca.URL, WithTransport(http.DefaultTransport))\n\trequire.NoError(t, err)\n\n\troot, err := os.ReadFile(\"testdata/secrets/root_ca.crt\")\n\trequire.NoError(t, err)\n\n\tcert := parseCertificate(t, string(root))\n\tpool := x509.NewCertPool()\n\tpool.AddCert(cert)\n\n\ttype args struct {\n\t\tclient *Client\n\t\tconfig *tls.Config\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\targs    args\n\t\twant    *tls.Config\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", args{client, &tls.Config{}}, &tls.Config{ClientCAs: pool}, false},\n\t\t{\"fail\", args{clientFail, &tls.Config{}}, &tls.Config{}, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tctx := &TLSOptionCtx{\n\t\t\t\tClient:        tt.args.client,\n\t\t\t\tConfig:        tt.args.config,\n\t\t\t\tmutableConfig: newMutableTLSConfig(),\n\t\t\t}\n\t\t\tif err := ctx.apply([]TLSOption{AddRootsToClientCAs()}); (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"AddRootsToClientCAs() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !equalPools(ctx.Config.ClientCAs, tt.want.ClientCAs) {\n\t\t\t\tt.Errorf(\"AddRootsToClientCAs() = %v, want %v\", ctx.Config, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\n//nolint:gosec // test tls config\nfunc TestAddFederationToRootCAs(t *testing.T) {\n\tca := startCATestServer(t)\n\tdefer ca.Close()\n\n\tclient, err := NewClient(ca.URL, WithRootFile(\"testdata/secrets/root_ca.crt\"))\n\trequire.NoError(t, err)\n\n\tclientFail, err := NewClient(ca.URL, WithTransport(http.DefaultTransport))\n\trequire.NoError(t, err)\n\n\troot, err := os.ReadFile(\"testdata/secrets/root_ca.crt\")\n\trequire.NoError(t, err)\n\n\tfederated, err := os.ReadFile(\"testdata/secrets/federated_ca.crt\")\n\trequire.NoError(t, err)\n\n\tcrt1 := parseCertificate(t, string(root))\n\tcrt2 := parseCertificate(t, string(federated))\n\tpool := x509.NewCertPool()\n\tpool.AddCert(crt1)\n\tpool.AddCert(crt2)\n\n\ttype args struct {\n\t\tclient *Client\n\t\tconfig *tls.Config\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\targs    args\n\t\twant    *tls.Config\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", args{client, &tls.Config{}}, &tls.Config{RootCAs: pool}, false},\n\t\t{\"fail\", args{clientFail, &tls.Config{}}, &tls.Config{}, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tctx := &TLSOptionCtx{\n\t\t\t\tClient:        tt.args.client,\n\t\t\t\tConfig:        tt.args.config,\n\t\t\t\tmutableConfig: newMutableTLSConfig(),\n\t\t\t}\n\t\t\tif err := ctx.apply([]TLSOption{AddFederationToRootCAs()}); (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"AddFederationToRootCAs() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(ctx.Config, tt.want) {\n\t\t\t\t// Federated roots are randomly sorted\n\t\t\t\tif !equalPools(ctx.Config.RootCAs, tt.want.RootCAs) || ctx.Config.ClientCAs != nil {\n\t\t\t\t\tt.Errorf(\"AddFederationToRootCAs() = %v, want %v\", ctx.Config, tt.want)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\n//nolint:gosec // test tls config\nfunc TestAddFederationToClientCAs(t *testing.T) {\n\tca := startCATestServer(t)\n\tdefer ca.Close()\n\n\tclient, err := NewClient(ca.URL, WithRootFile(\"testdata/secrets/root_ca.crt\"))\n\trequire.NoError(t, err)\n\n\tclientFail, err := NewClient(ca.URL, WithTransport(http.DefaultTransport))\n\trequire.NoError(t, err)\n\n\troot, err := os.ReadFile(\"testdata/secrets/root_ca.crt\")\n\trequire.NoError(t, err)\n\n\tfederated, err := os.ReadFile(\"testdata/secrets/federated_ca.crt\")\n\trequire.NoError(t, err)\n\n\tcrt1 := parseCertificate(t, string(root))\n\tcrt2 := parseCertificate(t, string(federated))\n\tpool := x509.NewCertPool()\n\tpool.AddCert(crt1)\n\tpool.AddCert(crt2)\n\n\ttype args struct {\n\t\tclient *Client\n\t\tconfig *tls.Config\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\targs    args\n\t\twant    *tls.Config\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", args{client, &tls.Config{}}, &tls.Config{ClientCAs: pool}, false},\n\t\t{\"fail\", args{clientFail, &tls.Config{}}, &tls.Config{}, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tctx := &TLSOptionCtx{\n\t\t\t\tClient:        tt.args.client,\n\t\t\t\tConfig:        tt.args.config,\n\t\t\t\tmutableConfig: newMutableTLSConfig(),\n\t\t\t}\n\t\t\tif err := ctx.apply([]TLSOption{AddFederationToClientCAs()}); (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"AddFederationToClientCAs() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(ctx.Config, tt.want) {\n\t\t\t\t// Federated roots are randomly sorted\n\t\t\t\tif !equalPools(ctx.Config.ClientCAs, tt.want.ClientCAs) || ctx.Config.RootCAs != nil {\n\t\t\t\t\tt.Errorf(\"AddFederationToClientCAs() = %v, want %v\", ctx.Config, tt.want)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\n//nolint:gosec // test tls config\nfunc TestAddRootsToCAs(t *testing.T) {\n\tca := startCATestServer(t)\n\tdefer ca.Close()\n\n\tclient, err := NewClient(ca.URL, WithRootFile(\"testdata/secrets/root_ca.crt\"))\n\trequire.NoError(t, err)\n\n\tclientFail, err := NewClient(ca.URL, WithTransport(http.DefaultTransport))\n\trequire.NoError(t, err)\n\n\troot, err := os.ReadFile(\"testdata/secrets/root_ca.crt\")\n\trequire.NoError(t, err)\n\n\tcert := parseCertificate(t, string(root))\n\tpool := x509.NewCertPool()\n\tpool.AddCert(cert)\n\n\ttype args struct {\n\t\tclient *Client\n\t\tconfig *tls.Config\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\targs    args\n\t\twant    *tls.Config\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", args{client, &tls.Config{}}, &tls.Config{ClientCAs: pool, RootCAs: pool}, false},\n\t\t{\"fail\", args{clientFail, &tls.Config{}}, &tls.Config{}, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tctx := &TLSOptionCtx{\n\t\t\t\tClient:        tt.args.client,\n\t\t\t\tConfig:        tt.args.config,\n\t\t\t\tmutableConfig: newMutableTLSConfig(),\n\t\t\t}\n\t\t\tif err := ctx.apply([]TLSOption{AddRootsToCAs()}); (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"AddRootsToCAs() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !equalPools(ctx.Config.RootCAs, tt.want.RootCAs) || !equalPools(ctx.Config.ClientCAs, tt.want.ClientCAs) {\n\t\t\t\tt.Errorf(\"AddRootsToCAs() = %v, want %v\", ctx.Config, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\n//nolint:gosec // test tls config\nfunc TestAddFederationToCAs(t *testing.T) {\n\tca := startCATestServer(t)\n\tdefer ca.Close()\n\n\tclient, err := NewClient(ca.URL, WithRootFile(\"testdata/secrets/root_ca.crt\"))\n\trequire.NoError(t, err)\n\n\tclientFail, err := NewClient(ca.URL, WithTransport(http.DefaultTransport))\n\trequire.NoError(t, err)\n\n\troot, err := os.ReadFile(\"testdata/secrets/root_ca.crt\")\n\trequire.NoError(t, err)\n\n\tfederated, err := os.ReadFile(\"testdata/secrets/federated_ca.crt\")\n\trequire.NoError(t, err)\n\n\tcrt1 := parseCertificate(t, string(root))\n\tcrt2 := parseCertificate(t, string(federated))\n\tpool := x509.NewCertPool()\n\tpool.AddCert(crt1)\n\tpool.AddCert(crt2)\n\n\ttype args struct {\n\t\tclient *Client\n\t\tconfig *tls.Config\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\targs    args\n\t\twant    *tls.Config\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", args{client, &tls.Config{}}, &tls.Config{ClientCAs: pool, RootCAs: pool}, false},\n\t\t{\"fail\", args{clientFail, &tls.Config{}}, &tls.Config{}, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tctx := &TLSOptionCtx{\n\t\t\t\tClient:        tt.args.client,\n\t\t\t\tConfig:        tt.args.config,\n\t\t\t\tmutableConfig: newMutableTLSConfig(),\n\t\t\t}\n\t\t\tif err := ctx.apply([]TLSOption{AddFederationToCAs()}); (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"AddFederationToCAs() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(ctx.Config, tt.want) {\n\t\t\t\t// Federated roots are randomly sorted\n\t\t\t\tif !equalPools(ctx.Config.ClientCAs, tt.want.ClientCAs) || !equalPools(ctx.Config.RootCAs, tt.want.RootCAs) {\n\t\t\t\t\tt.Errorf(\"AddFederationToCAs() = %v, want %v\", ctx.Config, tt.want)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\n//nolint:staticcheck,gocritic\nfunc equalPools(a, b *x509.CertPool) bool {\n\tif reflect.DeepEqual(a, b) {\n\t\treturn true\n\t}\n\tsubjects := a.Subjects()\n\tsA := make([]string, len(subjects))\n\tfor i := range subjects {\n\t\tsA[i] = string(subjects[i])\n\t}\n\tsubjects = b.Subjects()\n\tsB := make([]string, len(subjects))\n\tfor i := range subjects {\n\t\tsB[i] = string(subjects[i])\n\t}\n\tsort.Strings(sA)\n\tsort.Strings(sB)\n\treturn reflect.DeepEqual(sA, sB)\n}\n"
  },
  {
    "path": "ca/tls_test.go",
    "content": "package ca\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto\"\n\t\"crypto/sha256\"\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"encoding/hex\"\n\t\"io\"\n\t\"log\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"reflect\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/require\"\n\n\t\"go.step.sm/crypto/jose\"\n\t\"go.step.sm/crypto/randutil\"\n\n\t\"github.com/smallstep/certificates/api\"\n\t\"github.com/smallstep/certificates/authority\"\n)\n\nfunc generateOTT(t *testing.T, subject string) string {\n\tt.Helper()\n\tnow := time.Now()\n\tjwk, err := jose.ReadKey(\"testdata/secrets/ott_mariano_priv.jwk\", jose.WithPassword([]byte(\"password\")))\n\trequire.NoError(t, err)\n\n\topts := new(jose.SignerOptions).WithType(\"JWT\").WithHeader(\"kid\", jwk.KeyID)\n\tsig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: jwk.Key}, opts)\n\trequire.NoError(t, err)\n\n\tid, err := randutil.ASCII(64)\n\trequire.NoError(t, err)\n\n\tcl := struct {\n\t\tjose.Claims\n\t\tSANS []string `json:\"sans\"`\n\t}{\n\t\tClaims: jose.Claims{\n\t\t\tID:        id,\n\t\t\tSubject:   subject,\n\t\t\tIssuer:    \"mariano\",\n\t\t\tNotBefore: jose.NewNumericDate(now),\n\t\t\tExpiry:    jose.NewNumericDate(now.Add(time.Minute)),\n\t\t\tAudience:  []string{\"https://127.0.0.1:0/sign\"},\n\t\t},\n\t\tSANS: []string{subject},\n\t}\n\traw, err := jose.Signed(sig).Claims(cl).CompactSerialize()\n\trequire.NoError(t, err)\n\n\treturn raw\n}\n\nfunc startTestServer(baseContext context.Context, tlsConfig *tls.Config, handler http.Handler) *httptest.Server {\n\tsrv := httptest.NewUnstartedServer(handler)\n\tsrv.TLS = tlsConfig\n\t// Base context MUST be set before the start of the server\n\tsrv.Config.BaseContext = func(l net.Listener) context.Context {\n\t\treturn baseContext\n\t}\n\tsrv.StartTLS()\n\t// Force the use of GetCertificate on IPs\n\tsrv.TLS.Certificates = nil\n\treturn srv\n}\n\nfunc startCATestServer(t *testing.T) *httptest.Server {\n\tconfig, err := authority.LoadConfiguration(\"testdata/ca.json\")\n\trequire.NoError(t, err)\n\tca, err := New(config)\n\trequire.NoError(t, err)\n\t// Use a httptest.Server instead\n\tbaseContext := buildContext(ca.auth, nil, nil, nil)\n\tsrv := startTestServer(baseContext, ca.srv.TLSConfig, ca.srv.Handler)\n\treturn srv\n}\n\nfunc sign(t *testing.T, domain string) (*Client, *api.SignResponse, crypto.PrivateKey) {\n\tt.Helper()\n\tsrv := startCATestServer(t)\n\tdefer srv.Close()\n\treturn signDuration(t, srv, domain, 0)\n}\n\nfunc signDuration(t *testing.T, srv *httptest.Server, domain string, duration time.Duration) (*Client, *api.SignResponse, crypto.PrivateKey) {\n\tt.Helper()\n\treq, pk, err := CreateSignRequest(generateOTT(t, domain))\n\trequire.NoError(t, err)\n\n\tif duration > 0 {\n\t\treq.NotBefore = api.NewTimeDuration(time.Now())\n\t\treq.NotAfter = api.NewTimeDuration(req.NotBefore.Time().Add(duration))\n\t}\n\n\tclient, err := NewClient(srv.URL, WithRootFile(\"testdata/secrets/root_ca.crt\"))\n\trequire.NoError(t, err)\n\n\tsr, err := client.Sign(req)\n\trequire.NoError(t, err)\n\n\treturn client, sr, pk\n}\n\nfunc serverHandler(t *testing.T, clientDomain string) http.Handler {\n\treturn http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {\n\t\tif req.RequestURI != \"/no-cert\" {\n\t\t\tif req.TLS == nil || len(req.TLS.PeerCertificates) == 0 {\n\t\t\t\tw.Write([]byte(\"fail\"))\n\t\t\t\tt.Error(\"http.Request.TLS does not have peer certificates\")\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif req.TLS.PeerCertificates[0].Subject.CommonName != clientDomain {\n\t\t\t\tw.Write([]byte(\"fail\"))\n\t\t\t\tt.Errorf(\"http.Request.TLS.PeerCertificates[0].Subject.CommonName = %s, wants %s\", req.TLS.PeerCertificates[0].Subject.CommonName, clientDomain)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(req.TLS.PeerCertificates[0].DNSNames, []string{clientDomain}) {\n\t\t\t\tw.Write([]byte(\"fail\"))\n\t\t\t\tt.Errorf(\"http.Request.TLS.PeerCertificates[0].DNSNames %v, wants %v\", req.TLS.PeerCertificates[0].DNSNames, []string{clientDomain})\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t// Add serial number to check rotation\n\t\t\tsum := sha256.Sum256(req.TLS.PeerCertificates[0].Raw)\n\t\t\tw.Header().Set(\"x-fingerprint\", hex.EncodeToString(sum[:]))\n\t\t}\n\n\t\tw.Write([]byte(\"ok\"))\n\t})\n}\n\nfunc TestClient_GetServerTLSConfig_http(t *testing.T) {\n\tclientDomain := \"test.domain\"\n\tclient, sr, pk := sign(t, \"127.0.0.1\")\n\n\t// Create mTLS server\n\tctx, cancel := context.WithCancel(context.Background())\n\tdefer cancel()\n\ttlsConfig, err := client.GetServerTLSConfig(ctx, sr, pk)\n\tif err != nil {\n\t\tt.Fatalf(\"Client.GetServerTLSConfig() error = %v\", err)\n\t}\n\tsrvMTLS := startTestServer(context.Background(), tlsConfig, serverHandler(t, clientDomain))\n\tdefer srvMTLS.Close()\n\n\t// Create TLS server\n\tctx, cancel = context.WithCancel(context.Background())\n\tdefer cancel()\n\ttlsConfig, err = client.GetServerTLSConfig(ctx, sr, pk, VerifyClientCertIfGiven())\n\tif err != nil {\n\t\tt.Fatalf(\"Client.GetServerTLSConfig() error = %v\", err)\n\t}\n\tsrvTLS := startTestServer(context.Background(), tlsConfig, serverHandler(t, clientDomain))\n\tdefer srvTLS.Close()\n\n\ttests := []struct {\n\t\tname      string\n\t\tgetClient func(*testing.T, *Client, *api.SignResponse, crypto.PrivateKey) *http.Client\n\t\twantErr   map[string]bool\n\t}{\n\t\t{\"with transport\", func(t *testing.T, client *Client, sr *api.SignResponse, pk crypto.PrivateKey) *http.Client {\n\t\t\ttr, err := client.Transport(context.Background(), sr, pk)\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"Client.Transport() error = %v\", err)\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\treturn &http.Client{\n\t\t\t\tTransport: tr,\n\t\t\t}\n\t\t}, map[string]bool{srvTLS.URL: false, srvMTLS.URL: false}},\n\t\t{\"with tlsConfig\", func(t *testing.T, client *Client, sr *api.SignResponse, pk crypto.PrivateKey) *http.Client {\n\t\t\ttlsConfig, err := client.GetClientTLSConfig(context.Background(), sr, pk)\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"Client.GetClientTLSConfig() error = %v\", err)\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\treturn &http.Client{\n\t\t\t\tTransport: getDefaultTransport(tlsConfig),\n\t\t\t}\n\t\t}, map[string]bool{srvTLS.URL: false, srvMTLS.URL: false}},\n\t\t{\"with no ClientCert\", func(t *testing.T, client *Client, sr *api.SignResponse, pk crypto.PrivateKey) *http.Client {\n\t\t\troot, err := RootCertificate(sr)\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"RootCertificate() error = %v\", err)\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\ttlsConfig := getDefaultTLSConfig(sr)\n\t\t\ttlsConfig.RootCAs = x509.NewCertPool()\n\t\t\ttlsConfig.RootCAs.AddCert(root)\n\t\t\treturn &http.Client{\n\t\t\t\tTransport: getDefaultTransport(tlsConfig),\n\t\t\t}\n\t\t}, map[string]bool{srvTLS.URL + \"/no-cert\": false, srvMTLS.URL + \"/no-cert\": true}},\n\t\t{\"fail with default\", func(t *testing.T, client *Client, sr *api.SignResponse, pk crypto.PrivateKey) *http.Client {\n\t\t\treturn &http.Client{}\n\t\t}, map[string]bool{srvTLS.URL + \"/no-cert\": true, srvMTLS.URL + \"/no-cert\": true}},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tclient, sr, pk := sign(t, clientDomain)\n\t\t\tcli := tt.getClient(t, client, sr, pk)\n\t\t\tif cli == nil {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tfor path, wantErr := range tt.wantErr {\n\t\t\t\tt.Run(path, func(t *testing.T) {\n\t\t\t\t\tresp, err := cli.Get(path)\n\t\t\t\t\tif (err != nil) != wantErr {\n\t\t\t\t\t\tt.Errorf(\"http.Client.Get() error = %v, wantErr %v\", err, wantErr)\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\tif wantErr {\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\tdefer resp.Body.Close()\n\t\t\t\t\tb, err := io.ReadAll(resp.Body)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tt.Fatalf(\"io.ReadAll() error = %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tif !bytes.Equal(b, []byte(\"ok\")) {\n\t\t\t\t\t\tt.Errorf(\"response body unexpected, got %s, want ok\", b)\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestClient_GetServerTLSConfig_renew(t *testing.T) {\n\treset := setMinCertDuration(1 * time.Second)\n\tdefer reset()\n\n\t// Start CA\n\tca := startCATestServer(t)\n\tdefer ca.Close()\n\n\tclientDomain := \"test.domain\"\n\tclient, sr, pk := signDuration(t, ca, \"127.0.0.1\", 5*time.Second)\n\n\t// Start mTLS server\n\tctx, cancel := context.WithCancel(context.Background())\n\tdefer cancel()\n\ttlsConfig, err := client.GetServerTLSConfig(ctx, sr, pk)\n\trequire.NoError(t, err)\n\n\tsrvMTLS := startTestServer(context.Background(), tlsConfig, serverHandler(t, clientDomain))\n\tdefer srvMTLS.Close()\n\n\t// Start TLS server\n\tctx, cancel = context.WithCancel(context.Background())\n\tdefer cancel()\n\ttlsConfig, err = client.GetServerTLSConfig(ctx, sr, pk, VerifyClientCertIfGiven())\n\trequire.NoError(t, err)\n\n\tsrvTLS := startTestServer(context.Background(), tlsConfig, serverHandler(t, clientDomain))\n\tdefer srvTLS.Close()\n\n\t// Transport\n\tclient, sr, pk = signDuration(t, ca, clientDomain, 5*time.Second)\n\ttr, err := client.Transport(context.Background(), sr, pk)\n\trequire.NoError(t, err)\n\ttr1, ok := tr.(*http.Transport)\n\trequire.True(t, ok)\n\n\t// Transport with tlsConfig\n\tclient, sr, pk = signDuration(t, ca, clientDomain, 5*time.Second)\n\ttlsConfig, err = client.GetClientTLSConfig(context.Background(), sr, pk)\n\trequire.NoError(t, err)\n\n\ttr2 := getDefaultTransport(tlsConfig)\n\t// No client cert\n\troot, err := RootCertificate(sr)\n\trequire.NoError(t, err)\n\n\ttlsConfig = getDefaultTLSConfig(sr)\n\ttlsConfig.RootCAs = x509.NewCertPool()\n\ttlsConfig.RootCAs.AddCert(root)\n\ttr3 := getDefaultTransport(tlsConfig)\n\n\t// Disable keep alives to force TLS handshake\n\ttr1.DisableKeepAlives = true\n\ttr2.DisableKeepAlives = true\n\ttr3.DisableKeepAlives = true\n\n\ttests := []struct {\n\t\tname    string\n\t\tclient  *http.Client\n\t\twantErr map[string]bool\n\t}{\n\t\t{\"with transport\", &http.Client{Transport: tr1}, map[string]bool{\n\t\t\tsrvTLS.URL:  false,\n\t\t\tsrvMTLS.URL: false,\n\t\t}},\n\t\t{\"with tlsConfig\", &http.Client{Transport: tr2}, map[string]bool{\n\t\t\tsrvTLS.URL:  false,\n\t\t\tsrvMTLS.URL: false,\n\t\t}},\n\t\t{\"with no ClientCert\", &http.Client{Transport: tr3}, map[string]bool{\n\t\t\tsrvTLS.URL + \"/no-cert\":  false,\n\t\t\tsrvMTLS.URL + \"/no-cert\": true,\n\t\t}},\n\t\t{\"fail with default\", &http.Client{}, map[string]bool{\n\t\t\tsrvTLS.URL + \"/no-cert\":  true,\n\t\t\tsrvMTLS.URL + \"/no-cert\": true,\n\t\t}},\n\t}\n\n\t// To count different cert fingerprints\n\tfingerprints := map[string]struct{}{}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tfor path, wantErr := range tt.wantErr {\n\t\t\t\tt.Run(path, func(t *testing.T) {\n\t\t\t\t\tresp, err := tt.client.Get(path)\n\t\t\t\t\tif (err != nil) != wantErr {\n\t\t\t\t\t\tt.Errorf(\"http.Client.Get() error = %v\", err)\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\tif wantErr {\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\tif fp := resp.Header.Get(\"x-fingerprint\"); fp != \"\" {\n\t\t\t\t\t\tfingerprints[fp] = struct{}{}\n\t\t\t\t\t}\n\n\t\t\t\t\tdefer resp.Body.Close()\n\t\t\t\t\tb, err := io.ReadAll(resp.Body)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tt.Errorf(\"io.ReadAll() error = %v\", err)\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\tif !bytes.Equal(b, []byte(\"ok\")) {\n\t\t\t\t\t\tt.Errorf(\"response body unexpected, got %s, want ok\", b)\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t}\n\t\t})\n\t}\n\n\tif l := len(fingerprints); l != 2 {\n\t\tt.Errorf(\"number of fingerprints unexpected, got %d, want 2\", l)\n\t}\n\n\t// Wait for renewal\n\tlog.Printf(\"Sleeping for %s ...\\n\", 5*time.Second)\n\ttime.Sleep(5 * time.Second)\n\n\tfor _, tt := range tests {\n\t\tt.Run(\"renewed \"+tt.name, func(t *testing.T) {\n\t\t\tfor path, wantErr := range tt.wantErr {\n\t\t\t\tt.Run(path, func(t *testing.T) {\n\t\t\t\t\tresp, err := tt.client.Get(path)\n\t\t\t\t\tif (err != nil) != wantErr {\n\t\t\t\t\t\tt.Errorf(\"http.Client.Get() error = %v\", err)\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\tif wantErr {\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\tif fp := resp.Header.Get(\"x-fingerprint\"); fp != \"\" {\n\t\t\t\t\t\tfingerprints[fp] = struct{}{}\n\t\t\t\t\t}\n\n\t\t\t\t\tdefer resp.Body.Close()\n\t\t\t\t\tb, err := io.ReadAll(resp.Body)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tt.Errorf(\"io.ReadAll() error = %v\", err)\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\tif !bytes.Equal(b, []byte(\"ok\")) {\n\t\t\t\t\t\tt.Errorf(\"response body unexpected, got %s, want ok\", b)\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t}\n\t\t})\n\t}\n\n\tif l := len(fingerprints); l != 4 {\n\t\tt.Errorf(\"number of fingerprints unexpected, got %d, want 4\", l)\n\t}\n}\n\nfunc TestCertificate(t *testing.T) {\n\tcert := parseCertificate(t, certPEM)\n\tok := &api.SignResponse{\n\t\tServerPEM: api.Certificate{Certificate: cert},\n\t\tCaPEM:     api.Certificate{Certificate: parseCertificate(t, rootPEM)},\n\t\tCertChainPEM: []api.Certificate{\n\t\t\t{Certificate: cert},\n\t\t\t{Certificate: parseCertificate(t, rootPEM)},\n\t\t},\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tsign    *api.SignResponse\n\t\twant    *x509.Certificate\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", ok, cert, false},\n\t\t{\"fail\", &api.SignResponse{}, nil, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := Certificate(tt.sign)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"Certificate() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"Certificate() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestIntermediateCertificate(t *testing.T) {\n\tintermediate := parseCertificate(t, rootPEM)\n\tok := &api.SignResponse{\n\t\tServerPEM: api.Certificate{Certificate: parseCertificate(t, certPEM)},\n\t\tCaPEM:     api.Certificate{Certificate: intermediate},\n\t\tCertChainPEM: []api.Certificate{\n\t\t\t{Certificate: parseCertificate(t, certPEM)},\n\t\t\t{Certificate: intermediate},\n\t\t},\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tsign    *api.SignResponse\n\t\twant    *x509.Certificate\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", ok, intermediate, false},\n\t\t{\"fail\", &api.SignResponse{}, nil, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := IntermediateCertificate(tt.sign)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"IntermediateCertificate() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"IntermediateCertificate() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestRootCertificateCertificate(t *testing.T) {\n\troot := parseCertificate(t, rootPEM)\n\tok := &api.SignResponse{\n\t\tServerPEM: api.Certificate{Certificate: parseCertificate(t, certPEM)},\n\t\tCaPEM:     api.Certificate{Certificate: parseCertificate(t, rootPEM)},\n\t\tCertChainPEM: []api.Certificate{\n\t\t\t{Certificate: parseCertificate(t, certPEM)},\n\t\t\t{Certificate: parseCertificate(t, rootPEM)},\n\t\t},\n\t\tTLS: &tls.ConnectionState{VerifiedChains: [][]*x509.Certificate{\n\t\t\t{root, root},\n\t\t}},\n\t}\n\tnoTLS := &api.SignResponse{\n\t\tServerPEM: api.Certificate{Certificate: parseCertificate(t, certPEM)},\n\t\tCaPEM:     api.Certificate{Certificate: parseCertificate(t, rootPEM)},\n\t\tCertChainPEM: []api.Certificate{\n\t\t\t{Certificate: parseCertificate(t, certPEM)},\n\t\t\t{Certificate: parseCertificate(t, rootPEM)},\n\t\t},\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tsign    *api.SignResponse\n\t\twant    *x509.Certificate\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", ok, root, false},\n\t\t{\"fail\", &api.SignResponse{}, nil, true},\n\t\t{\"no tls\", noTLS, nil, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := RootCertificate(tt.sign)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"RootCertificate() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"RootCertificate() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "cas/apiv1/extension.go",
    "content": "package apiv1\n\nimport (\n\t\"crypto/x509\"\n\t\"crypto/x509/pkix\"\n\t\"encoding/asn1\"\n\n\t\"github.com/pkg/errors\"\n)\n\nvar (\n\toidStepRoot                 = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 37476, 9000, 64}\n\toidStepCertificateAuthority = append(asn1.ObjectIdentifier(nil), append(oidStepRoot, 2)...)\n)\n\n// CertificateAuthorityExtension type is used to encode the certificate\n// authority extension.\ntype CertificateAuthorityExtension struct {\n\tType          string\n\tCertificateID string   `asn1:\"optional,omitempty\"`\n\tKeyValuePairs []string `asn1:\"optional,omitempty\"`\n}\n\n// CreateCertificateAuthorityExtension returns a X.509 extension that shows the\n// CAS type, id and a list of optional key value pairs.\nfunc CreateCertificateAuthorityExtension(typ Type, certificateID string, keyValuePairs ...string) (pkix.Extension, error) {\n\tb, err := asn1.Marshal(CertificateAuthorityExtension{\n\t\tType:          typ.String(),\n\t\tCertificateID: certificateID,\n\t\tKeyValuePairs: keyValuePairs,\n\t})\n\tif err != nil {\n\t\treturn pkix.Extension{}, errors.Wrapf(err, \"error marshaling certificate id extension\")\n\t}\n\treturn pkix.Extension{\n\t\tId:       oidStepCertificateAuthority,\n\t\tCritical: false,\n\t\tValue:    b,\n\t}, nil\n}\n\n// FindCertificateAuthorityExtension returns the certificate authority extension\n// from a signed certificate.\nfunc FindCertificateAuthorityExtension(cert *x509.Certificate) (pkix.Extension, bool) {\n\tfor _, ext := range cert.Extensions {\n\t\tif ext.Id.Equal(oidStepCertificateAuthority) {\n\t\t\treturn ext, true\n\t\t}\n\t}\n\treturn pkix.Extension{}, false\n}\n\n// RemoveCertificateAuthorityExtension removes the certificate authority\n// extension from a certificate template.\nfunc RemoveCertificateAuthorityExtension(cert *x509.Certificate) {\n\tfor i, ext := range cert.ExtraExtensions {\n\t\tif ext.Id.Equal(oidStepCertificateAuthority) {\n\t\t\tcert.ExtraExtensions = append(cert.ExtraExtensions[:i], cert.ExtraExtensions[i+1:]...)\n\t\t\treturn\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "cas/apiv1/extension_test.go",
    "content": "package apiv1\n\nimport (\n\t\"crypto/x509\"\n\t\"crypto/x509/pkix\"\n\t\"reflect\"\n\t\"testing\"\n)\n\nfunc TestCreateCertificateAuthorityExtension(t *testing.T) {\n\ttype args struct {\n\t\ttyp           Type\n\t\tcertificateID string\n\t\tkeyValuePairs []string\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\targs    args\n\t\twant    pkix.Extension\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", args{Type(CloudCAS), \"1ac75689-cd3f-482e-a695-8a13daf39dc4\", nil}, pkix.Extension{\n\t\t\tId:       oidStepCertificateAuthority,\n\t\t\tCritical: false,\n\t\t\tValue: []byte{\n\t\t\t\t0x30, 0x30, 0x13, 0x08, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x63, 0x61, 0x73, 0x13, 0x24, 0x31, 0x61,\n\t\t\t\t0x63, 0x37, 0x35, 0x36, 0x38, 0x39, 0x2d, 0x63, 0x64, 0x33, 0x66, 0x2d, 0x34, 0x38, 0x32, 0x65,\n\t\t\t\t0x2d, 0x61, 0x36, 0x39, 0x35, 0x2d, 0x38, 0x61, 0x31, 0x33, 0x64, 0x61, 0x66, 0x33, 0x39, 0x64,\n\t\t\t\t0x63, 0x34,\n\t\t\t},\n\t\t}, false},\n\t\t{\"ok\", args{Type(CloudCAS), \"1ac75689-cd3f-482e-a695-8a13daf39dc4\", []string{\"foo\", \"bar\"}}, pkix.Extension{\n\t\t\tId:       oidStepCertificateAuthority,\n\t\t\tCritical: false,\n\t\t\tValue: []byte{\n\t\t\t\t0x30, 0x3c, 0x13, 0x08, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x63, 0x61, 0x73, 0x13, 0x24, 0x31, 0x61,\n\t\t\t\t0x63, 0x37, 0x35, 0x36, 0x38, 0x39, 0x2d, 0x63, 0x64, 0x33, 0x66, 0x2d, 0x34, 0x38, 0x32, 0x65,\n\t\t\t\t0x2d, 0x61, 0x36, 0x39, 0x35, 0x2d, 0x38, 0x61, 0x31, 0x33, 0x64, 0x61, 0x66, 0x33, 0x39, 0x64,\n\t\t\t\t0x63, 0x34, 0x30, 0x0a, 0x13, 0x03, 0x66, 0x6f, 0x6f, 0x13, 0x03, 0x62, 0x61, 0x72,\n\t\t\t},\n\t\t}, false},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := CreateCertificateAuthorityExtension(tt.args.typ, tt.args.certificateID, tt.args.keyValuePairs...)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"CreateCertificateAuthorityExtension() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"CreateCertificateAuthorityExtension() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestFindCertificateAuthorityExtension(t *testing.T) {\n\texpected := pkix.Extension{\n\t\tId:    oidStepCertificateAuthority,\n\t\tValue: []byte(\"fake data\"),\n\t}\n\ttype args struct {\n\t\tcert *x509.Certificate\n\t}\n\ttests := []struct {\n\t\tname  string\n\t\targs  args\n\t\twant  pkix.Extension\n\t\twant1 bool\n\t}{\n\t\t{\"first\", args{&x509.Certificate{Extensions: []pkix.Extension{\n\t\t\texpected,\n\t\t\t{Id: []int{1, 2, 3, 4}},\n\t\t}}}, expected, true},\n\t\t{\"last\", args{&x509.Certificate{Extensions: []pkix.Extension{\n\t\t\t{Id: []int{1, 2, 3, 4}},\n\t\t\t{Id: []int{2, 3, 4, 5}},\n\t\t\texpected,\n\t\t}}}, expected, true},\n\t\t{\"fail\", args{&x509.Certificate{Extensions: []pkix.Extension{\n\t\t\t{Id: []int{1, 2, 3, 4}},\n\t\t}}}, pkix.Extension{}, false},\n\t\t{\"fail ExtraExtensions\", args{&x509.Certificate{ExtraExtensions: []pkix.Extension{\n\t\t\texpected,\n\t\t\t{Id: []int{1, 2, 3, 4}},\n\t\t}}}, pkix.Extension{}, false},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, got1 := FindCertificateAuthorityExtension(tt.args.cert)\n\t\t\tif !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"FindCertificateAuthorityExtension() got = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t\tif got1 != tt.want1 {\n\t\t\t\tt.Errorf(\"FindCertificateAuthorityExtension() got1 = %v, want %v\", got1, tt.want1)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestRemoveCertificateAuthorityExtension(t *testing.T) {\n\tcaExt := pkix.Extension{\n\t\tId:    oidStepCertificateAuthority,\n\t\tValue: []byte(\"fake data\"),\n\t}\n\ttype args struct {\n\t\tcert *x509.Certificate\n\t}\n\ttests := []struct {\n\t\tname string\n\t\targs args\n\t\twant *x509.Certificate\n\t}{\n\t\t{\"first\", args{&x509.Certificate{ExtraExtensions: []pkix.Extension{\n\t\t\tcaExt,\n\t\t\t{Id: []int{1, 2, 3, 4}},\n\t\t}}}, &x509.Certificate{ExtraExtensions: []pkix.Extension{\n\t\t\t{Id: []int{1, 2, 3, 4}},\n\t\t}}},\n\t\t{\"last\", args{&x509.Certificate{ExtraExtensions: []pkix.Extension{\n\t\t\t{Id: []int{1, 2, 3, 4}},\n\t\t\tcaExt,\n\t\t}}}, &x509.Certificate{ExtraExtensions: []pkix.Extension{\n\t\t\t{Id: []int{1, 2, 3, 4}},\n\t\t}}},\n\t\t{\"missing\", args{&x509.Certificate{ExtraExtensions: []pkix.Extension{\n\t\t\t{Id: []int{1, 2, 3, 4}},\n\t\t}}}, &x509.Certificate{ExtraExtensions: []pkix.Extension{\n\t\t\t{Id: []int{1, 2, 3, 4}},\n\t\t}}},\n\t\t{\"extensions\", args{&x509.Certificate{Extensions: []pkix.Extension{\n\t\t\tcaExt,\n\t\t\t{Id: []int{1, 2, 3, 4}},\n\t\t}}}, &x509.Certificate{Extensions: []pkix.Extension{\n\t\t\tcaExt,\n\t\t\t{Id: []int{1, 2, 3, 4}},\n\t\t}}},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tRemoveCertificateAuthorityExtension(tt.args.cert)\n\t\t\tif !reflect.DeepEqual(tt.args.cert, tt.want) {\n\t\t\t\tt.Errorf(\"RemoveCertificateAuthorityExtension() cert = %v, want %v\", tt.args.cert, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "cas/apiv1/options.go",
    "content": "package apiv1\n\nimport (\n\t\"crypto\"\n\t\"crypto/x509\"\n\t\"encoding/json\"\n\n\t\"github.com/pkg/errors\"\n\n\t\"go.step.sm/crypto/kms\"\n)\n\n// Options represents the configuration options used to select and configure the\n// CertificateAuthorityService (CAS) to use.\ntype Options struct {\n\t// AuthorityID is the the id oc the current authority. This is used on\n\t// StepCAS to add information about the origin of a certificate.\n\tAuthorityID string `json:\"-\"`\n\n\t// The type of the CAS to use.\n\tType string `json:\"type\"`\n\n\t// CertificateAuthority reference:\n\t// In StepCAS the value is the CA url, e.g., \"https://ca.smallstep.com:9000\".\n\t// In CloudCAS the format is \"projects/*/locations/*/certificateAuthorities/*\".\n\t// In VaultCAS the value is the url, e.g., \"https://vault.smallstep.com\".\n\tCertificateAuthority string `json:\"certificateAuthority,omitempty\"`\n\n\t// CertificateAuthorityFingerprint is the root fingerprint used to\n\t// authenticate the connection to the CA when using StepCAS.\n\tCertificateAuthorityFingerprint string `json:\"certificateAuthorityFingerprint,omitempty\"`\n\n\t// CertificateIssuer contains the configuration used in StepCAS.\n\tCertificateIssuer *CertificateIssuer `json:\"certificateIssuer,omitempty\"`\n\n\t// Path to the credentials file used in CloudCAS. If not defined the default\n\t// authentication mechanism provided by Google SDK will be used. See\n\t// https://cloud.google.com/docs/authentication.\n\tCredentialsFile string `json:\"credentialsFile,omitempty\"`\n\n\t// CertificateChain contains the issuer certificate, along with any other\n\t// bundled certificates to be returned in the chain to consumers. It is used\n\t// used in SoftCAS and it is configured in the crt property of the ca.json.\n\tCertificateChain []*x509.Certificate `json:\"-\"`\n\n\t// Signer is the private key or a KMS signer for the issuer certificate. It\n\t// is used in SoftCAS and it is configured in the key property of the\n\t// ca.json.\n\tSigner crypto.Signer `json:\"-\"`\n\n\t// CertificateSigner combines CertificateChain and Signer in a callback that\n\t// returns the chain of certificate and signer used to sign X.509\n\t// certificates in SoftCAS.\n\tCertificateSigner func() ([]*x509.Certificate, crypto.Signer, error) `json:\"-\"`\n\n\t// IsCreator is set to true when we're creating a certificate authority. It\n\t// is used to skip some validations when initializing a\n\t// CertificateAuthority. This option is used on SoftCAS and CloudCAS.\n\tIsCreator bool `json:\"-\"`\n\n\t// IsCAGetter is set to true when we're just using the\n\t// CertificateAuthorityGetter interface to retrieve the root certificate. It\n\t// is used to skip some validations when initializing a\n\t// CertificateAuthority. This option is used on StepCAS.\n\tIsCAGetter bool `json:\"-\"`\n\n\t// KeyManager is the KMS used to generate keys in SoftCAS.\n\tKeyManager kms.KeyManager `json:\"-\"`\n\n\t// Project, Location, CaPool and GCSBucket are parameters used in CloudCAS\n\t// to create a new certificate authority. If a CaPool does not exist it will\n\t// be created. GCSBucket is optional, if not provided GCloud will create a\n\t// managed bucket.\n\tProject    string `json:\"-\"`\n\tLocation   string `json:\"-\"`\n\tCaPool     string `json:\"-\"`\n\tCaPoolTier string `json:\"-\"`\n\tGCSBucket  string `json:\"-\"`\n\n\t// Generic structure to configure any CAS\n\tConfig json.RawMessage `json:\"config,omitempty\"`\n}\n\n// CertificateIssuer contains the properties used to use the StepCAS certificate\n// authority service.\ntype CertificateIssuer struct {\n\tType        string `json:\"type\"`\n\tProvisioner string `json:\"provisioner,omitempty\"`\n\tCertificate string `json:\"crt,omitempty\"`\n\tKey         string `json:\"key,omitempty\"`\n\tPassword    string `json:\"password,omitempty\"`\n}\n\n// Validate checks the fields in Options.\nfunc (o *Options) Validate() error {\n\tvar typ Type\n\tif o == nil {\n\t\ttyp = Type(SoftCAS)\n\t} else {\n\t\ttyp = Type(o.Type)\n\t}\n\t// Check that the type can be loaded.\n\tif _, ok := LoadCertificateAuthorityServiceNewFunc(typ); !ok {\n\t\treturn errors.Errorf(\"unsupported cas type %s\", typ)\n\t}\n\treturn nil\n}\n\n// Is returns if the options have the given type.\nfunc (o *Options) Is(t Type) bool {\n\tif o == nil {\n\t\treturn t.String() == SoftCAS\n\t}\n\treturn Type(o.Type).String() == t.String()\n}\n"
  },
  {
    "path": "cas/apiv1/options_test.go",
    "content": "package apiv1\n\nimport (\n\t\"context\"\n\t\"crypto\"\n\t\"crypto/x509\"\n\t\"sync\"\n\t\"testing\"\n)\n\ntype testCAS struct {\n\tname string\n}\n\nfunc (t *testCAS) CreateCertificate(*CreateCertificateRequest) (*CreateCertificateResponse, error) {\n\treturn nil, nil\n}\n\nfunc (t *testCAS) RenewCertificate(*RenewCertificateRequest) (*RenewCertificateResponse, error) {\n\treturn nil, nil\n}\n\nfunc (t *testCAS) RevokeCertificate(*RevokeCertificateRequest) (*RevokeCertificateResponse, error) {\n\treturn nil, nil\n}\n\n//nolint:gocritic // ignore sloppy test func name\nfunc mockRegister(t *testing.T) {\n\tt.Helper()\n\tRegister(SoftCAS, func(ctx context.Context, opts Options) (CertificateAuthorityService, error) {\n\t\treturn &testCAS{name: SoftCAS}, nil\n\t})\n\tRegister(CloudCAS, func(ctx context.Context, opts Options) (CertificateAuthorityService, error) {\n\t\treturn &testCAS{name: CloudCAS}, nil\n\t})\n\tt.Cleanup(func() {\n\t\tregistry = new(sync.Map)\n\t})\n}\n\nfunc TestOptions_Validate(t *testing.T) {\n\tmockRegister(t)\n\ttype fields struct {\n\t\tType                 string\n\t\tCredentialsFile      string\n\t\tCertificateAuthority string\n\t\tIssuer               *x509.Certificate\n\t\tSigner               crypto.Signer\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tfields  fields\n\t\twantErr bool\n\t}{\n\t\t{\"empty\", fields{}, false},\n\t\t{\"SoftCAS\", fields{SoftCAS, \"\", \"\", nil, nil}, false},\n\t\t{\"CloudCAS\", fields{CloudCAS, \"\", \"\", nil, nil}, false},\n\t\t{\"softcas\", fields{\"softcas\", \"\", \"\", nil, nil}, false},\n\t\t{\"CLOUDCAS\", fields{\"CLOUDCAS\", \"\", \"\", nil, nil}, false},\n\t\t{\"fail\", fields{\"FailCAS\", \"\", \"\", nil, nil}, true},\n\t}\n\tt.Run(\"nil\", func(t *testing.T) {\n\t\tvar o *Options\n\t\tif err := o.Validate(); err != nil {\n\t\t\tt.Errorf(\"Options.Validate() error = %v, wantErr %v\", err, false)\n\t\t}\n\t})\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\to := &Options{\n\t\t\t\tType:                 tt.fields.Type,\n\t\t\t\tCredentialsFile:      tt.fields.CredentialsFile,\n\t\t\t\tCertificateAuthority: tt.fields.CertificateAuthority,\n\t\t\t\tCertificateChain:     []*x509.Certificate{tt.fields.Issuer},\n\t\t\t\tSigner:               tt.fields.Signer,\n\t\t\t}\n\t\t\tif err := o.Validate(); (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"Options.Validate() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestOptions_Is(t *testing.T) {\n\tmockRegister(t)\n\n\ttype fields struct {\n\t\tType                 string\n\t\tCredentialsFile      string\n\t\tCertificateAuthority string\n\t\tIssuer               *x509.Certificate\n\t\tSigner               crypto.Signer\n\t}\n\ttype args struct {\n\t\tt Type\n\t}\n\ttests := []struct {\n\t\tname   string\n\t\tfields fields\n\t\targs   args\n\t\twant   bool\n\t}{\n\t\t{\"empty\", fields{}, args{}, true},\n\t\t{\"SoftCAS\", fields{SoftCAS, \"\", \"\", nil, nil}, args{\"SoftCAS\"}, true},\n\t\t{\"CloudCAS\", fields{CloudCAS, \"\", \"\", nil, nil}, args{\"CloudCAS\"}, true},\n\t\t{\"softcas\", fields{\"softcas\", \"\", \"\", nil, nil}, args{SoftCAS}, true},\n\t\t{\"CLOUDCAS\", fields{\"CLOUDCAS\", \"\", \"\", nil, nil}, args{CloudCAS}, true},\n\t\t{\"UnknownCAS\", fields{\"UnknownCAS\", \"\", \"\", nil, nil}, args{\"UnknownCAS\"}, true},\n\t\t{\"fail\", fields{CloudCAS, \"\", \"\", nil, nil}, args{\"SoftCAS\"}, false},\n\t\t{\"fail\", fields{SoftCAS, \"\", \"\", nil, nil}, args{\"CloudCAS\"}, false},\n\t}\n\tt.Run(\"nil\", func(t *testing.T) {\n\t\tvar o *Options\n\t\tif got := o.Is(SoftCAS); got != true {\n\t\t\tt.Errorf(\"Options.Is() = %v, want %v\", got, true)\n\t\t}\n\t})\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\to := &Options{\n\t\t\t\tType:                 tt.fields.Type,\n\t\t\t\tCredentialsFile:      tt.fields.CredentialsFile,\n\t\t\t\tCertificateAuthority: tt.fields.CertificateAuthority,\n\t\t\t\tCertificateChain:     []*x509.Certificate{tt.fields.Issuer},\n\t\t\t\tSigner:               tt.fields.Signer,\n\t\t\t}\n\t\t\tif got := o.Is(tt.args.t); got != tt.want {\n\t\t\t\tt.Errorf(\"Options.Is() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "cas/apiv1/registry.go",
    "content": "package apiv1\n\nimport (\n\t\"context\"\n\t\"sync\"\n)\n\nvar (\n\tregistry = new(sync.Map)\n)\n\n// CertificateAuthorityServiceNewFunc is the type that represents the method to initialize a new\n// CertificateAuthorityService.\ntype CertificateAuthorityServiceNewFunc func(ctx context.Context, opts Options) (CertificateAuthorityService, error)\n\n// Register adds to the registry a method to create a KeyManager of type t.\nfunc Register(t Type, fn CertificateAuthorityServiceNewFunc) {\n\tregistry.Store(t.String(), fn)\n}\n\n// LoadCertificateAuthorityServiceNewFunc returns the function to initialize a KeyManager.\nfunc LoadCertificateAuthorityServiceNewFunc(t Type) (CertificateAuthorityServiceNewFunc, bool) {\n\tv, ok := registry.Load(t.String())\n\tif !ok {\n\t\treturn nil, false\n\t}\n\tfn, ok := v.(CertificateAuthorityServiceNewFunc)\n\treturn fn, ok\n}\n"
  },
  {
    "path": "cas/apiv1/registry_test.go",
    "content": "package apiv1\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"reflect\"\n\t\"sync\"\n\t\"testing\"\n)\n\nfunc TestRegister(t *testing.T) {\n\tt.Cleanup(func() {\n\t\tregistry = new(sync.Map)\n\t})\n\ttype args struct {\n\t\tt  Type\n\t\tfn CertificateAuthorityServiceNewFunc\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\targs    args\n\t\twant    CertificateAuthorityService\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", args{\"TestCAS\", func(ctx context.Context, opts Options) (CertificateAuthorityService, error) {\n\t\t\treturn &testCAS{}, nil\n\t\t}}, &testCAS{}, false},\n\t\t{\"error\", args{\"ErrorCAS\", func(ctx context.Context, opts Options) (CertificateAuthorityService, error) {\n\t\t\treturn nil, fmt.Errorf(\"an error\")\n\t\t}}, nil, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tRegister(tt.args.t, tt.args.fn)\n\t\t\tfmt.Println(registry)\n\t\t\tfn, ok := registry.Load(tt.args.t.String())\n\t\t\tif !ok {\n\t\t\t\tt.Errorf(\"Register() failed\")\n\t\t\t\treturn\n\t\t\t}\n\t\t\tgot, err := fn.(CertificateAuthorityServiceNewFunc)(context.Background(), Options{})\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"CertificateAuthorityServiceNewFunc() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"CertificateAuthorityServiceNewFunc() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLoadCertificateAuthorityServiceNewFunc(t *testing.T) {\n\tmockRegister(t)\n\ttype args struct {\n\t\tt Type\n\t}\n\ttests := []struct {\n\t\tname   string\n\t\targs   args\n\t\twant   CertificateAuthorityService\n\t\twantOk bool\n\t}{\n\t\t{\"default\", args{\"\"}, &testCAS{name: SoftCAS}, true},\n\t\t{\"SoftCAS\", args{\"SoftCAS\"}, &testCAS{name: SoftCAS}, true},\n\t\t{\"CloudCAS\", args{\"CloudCAS\"}, &testCAS{name: CloudCAS}, true},\n\t\t{\"softcas\", args{\"softcas\"}, &testCAS{name: SoftCAS}, true},\n\t\t{\"cloudcas\", args{\"cloudcas\"}, &testCAS{name: CloudCAS}, true},\n\t\t{\"FailCAS\", args{\"FailCAS\"}, nil, false},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tfn, ok := LoadCertificateAuthorityServiceNewFunc(tt.args.t)\n\t\t\tif ok != tt.wantOk {\n\t\t\t\tt.Errorf(\"LoadCertificateAuthorityServiceNewFunc() ok = %v, want %v\", ok, tt.wantOk)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif ok {\n\t\t\t\tgot, err := fn(context.Background(), Options{})\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Errorf(\"CertificateAuthorityServiceNewFunc() error = %v\", err)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tif !reflect.DeepEqual(got, tt.want) {\n\t\t\t\t\tt.Errorf(\"CertificateAuthorityServiceNewFunc() = %v, want %v\", got, tt.want)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "cas/apiv1/requests.go",
    "content": "package apiv1\n\nimport (\n\t\"crypto\"\n\t\"crypto/x509\"\n\t\"time\"\n\n\t\"go.step.sm/crypto/kms/apiv1\"\n)\n\n// CertificateAuthorityType indicates the type of Certificate Authority to\n// create.\ntype CertificateAuthorityType int\n\nconst (\n\t// RootCA is the type used to create a self-signed certificate suitable for\n\t// use as a root CA.\n\tRootCA CertificateAuthorityType = iota + 1\n\n\t// IntermediateCA is the type used to create a subordinated certificate that\n\t// can be used to sign additional leaf certificates.\n\tIntermediateCA\n)\n\n// SignatureAlgorithm used for cryptographic signing.\ntype SignatureAlgorithm int\n\nconst (\n\t// Not specified.\n\tUnspecifiedSignAlgorithm SignatureAlgorithm = iota\n\t// RSASSA-PKCS1-v1_5 key and a SHA256 digest.\n\tSHA256WithRSA\n\t// RSASSA-PKCS1-v1_5 key and a SHA384 digest.\n\tSHA384WithRSA\n\t// RSASSA-PKCS1-v1_5 key and a SHA512 digest.\n\tSHA512WithRSA\n\t// RSASSA-PSS key with a SHA256 digest.\n\tSHA256WithRSAPSS\n\t// RSASSA-PSS key with a SHA384 digest.\n\tSHA384WithRSAPSS\n\t// RSASSA-PSS key with a SHA512 digest.\n\tSHA512WithRSAPSS\n\t// ECDSA on the NIST P-256 curve with a SHA256 digest.\n\tECDSAWithSHA256\n\t// ECDSA on the NIST P-384 curve with a SHA384 digest.\n\tECDSAWithSHA384\n\t// ECDSA on the NIST P-521 curve with a SHA512 digest.\n\tECDSAWithSHA512\n\t// EdDSA on Curve25519 with a SHA512 digest.\n\tPureEd25519\n)\n\n// CreateCertificateRequest is the request used to sign a new certificate.\ntype CreateCertificateRequest struct {\n\tTemplate       *x509.Certificate\n\tCSR            *x509.CertificateRequest\n\tLifetime       time.Duration\n\tBackdate       time.Duration\n\tRequestID      string\n\tProvisioner    *ProvisionerInfo\n\tIsCAServerCert bool\n}\n\n// ProvisionerInfo contains information of the provisioner used to authorize a\n// certificate.\ntype ProvisionerInfo struct {\n\tID   string\n\tType string\n\tName string\n}\n\n// CreateCertificateResponse is the response to a create certificate request.\ntype CreateCertificateResponse struct {\n\tCertificate      *x509.Certificate\n\tCertificateChain []*x509.Certificate\n}\n\n// RenewCertificateRequest is the request used to re-sign a certificate.\ntype RenewCertificateRequest struct {\n\tTemplate  *x509.Certificate\n\tCSR       *x509.CertificateRequest\n\tLifetime  time.Duration\n\tBackdate  time.Duration\n\tToken     string\n\tRequestID string\n}\n\n// RenewCertificateResponse is the response to a renew certificate request.\ntype RenewCertificateResponse struct {\n\tCertificate      *x509.Certificate\n\tCertificateChain []*x509.Certificate\n}\n\n// RevokeCertificateRequest is the request used to revoke a certificate.\ntype RevokeCertificateRequest struct {\n\tCertificate  *x509.Certificate\n\tSerialNumber string\n\tReason       string\n\tReasonCode   int\n\tPassiveOnly  bool\n\tRequestID    string\n}\n\n// RevokeCertificateResponse is the response to a revoke certificate request.\ntype RevokeCertificateResponse struct {\n\tCertificate      *x509.Certificate\n\tCertificateChain []*x509.Certificate\n}\n\n// GetCertificateAuthorityRequest is the request used to get the root\n// certificate from a CAS.\ntype GetCertificateAuthorityRequest struct {\n\tName string\n}\n\n// GetCertificateAuthorityResponse is the response that contains\n// the root certificate.\ntype GetCertificateAuthorityResponse struct {\n\tRootCertificate          *x509.Certificate\n\tIntermediateCertificates []*x509.Certificate\n}\n\n// CreateKeyRequest is the request used to generate a new key using a KMS.\ntype CreateKeyRequest = apiv1.CreateKeyRequest\n\n// CreateCertificateAuthorityRequest is the request used to generate a root or\n// intermediate certificate.\ntype CreateCertificateAuthorityRequest struct {\n\tName      string\n\tType      CertificateAuthorityType\n\tTemplate  *x509.Certificate\n\tLifetime  time.Duration\n\tBackdate  time.Duration\n\tRequestID string\n\tProject   string\n\tLocation  string\n\n\t// Parent is the signer of the new CertificateAuthority.\n\tParent *CreateCertificateAuthorityResponse\n\n\t// CreateKey defines the KMS CreateKeyRequest to use when creating a new\n\t// CertificateAuthority. If CreateKey is nil, a default algorithm will be\n\t// used.\n\tCreateKey *CreateKeyRequest\n}\n\n// CreateCertificateAuthorityResponse is the response for\n// CreateCertificateAuthority method and contains the root or intermediate\n// certificate generated as well as the CA chain.\ntype CreateCertificateAuthorityResponse struct {\n\tName             string\n\tCertificate      *x509.Certificate\n\tCertificateChain []*x509.Certificate\n\tKeyName          string\n\tPublicKey        crypto.PublicKey\n\tPrivateKey       crypto.PrivateKey\n\tSigner           crypto.Signer\n}\n\n// CreateCRLRequest is the request to create a Certificate Revocation List.\ntype CreateCRLRequest struct {\n\tRevocationList *x509.RevocationList\n}\n\n// CreateCRLResponse is the response to a Certificate Revocation List request.\ntype CreateCRLResponse struct {\n\tCRL []byte //the CRL in DER format\n}\n"
  },
  {
    "path": "cas/apiv1/services.go",
    "content": "package apiv1\n\nimport (\n\t\"crypto\"\n\t\"crypto/x509\"\n\t\"net/http\"\n\t\"strings\"\n)\n\n// CertificateAuthorityService is the interface implemented to support external\n// certificate authorities.\ntype CertificateAuthorityService interface {\n\tCreateCertificate(req *CreateCertificateRequest) (*CreateCertificateResponse, error)\n\tRenewCertificate(req *RenewCertificateRequest) (*RenewCertificateResponse, error)\n\tRevokeCertificate(req *RevokeCertificateRequest) (*RevokeCertificateResponse, error)\n}\n\n// CertificateAuthorityCRLGenerator is an optional interface implemented by CertificateAuthorityService\n// that has a method to create a CRL\ntype CertificateAuthorityCRLGenerator interface {\n\tCreateCRL(req *CreateCRLRequest) (*CreateCRLResponse, error)\n}\n\n// CertificateAuthorityGetter is an interface implemented by a\n// CertificateAuthorityService that has a method to get the root certificate.\ntype CertificateAuthorityGetter interface {\n\tGetCertificateAuthority(req *GetCertificateAuthorityRequest) (*GetCertificateAuthorityResponse, error)\n}\n\n// CertificateAuthorityCreator is an interface implemented by a\n// CertificateAuthorityService that has a method to create a new certificate\n// authority.\ntype CertificateAuthorityCreator interface {\n\tCreateCertificateAuthority(req *CreateCertificateAuthorityRequest) (*CreateCertificateAuthorityResponse, error)\n}\n\n// CertificateAuthoritySigner is an optional interface implemented by a\n// CertificateAuthorityService that has a method that returns a [crypto.Signer]\n// using the same key used to issue certificates.\ntype CertificateAuthoritySigner interface {\n\tGetSigner() (crypto.Signer, error)\n}\n\n// SignatureAlgorithmGetter is an optional implementation in a crypto.Signer\n// that returns the SignatureAlgorithm to use.\ntype SignatureAlgorithmGetter interface {\n\tSignatureAlgorithm() x509.SignatureAlgorithm\n}\n\n// Type represents the CAS type used.\ntype Type string\n\nconst (\n\t// DefaultCAS is a CertificateAuthorityService using software.\n\tDefaultCAS = \"\"\n\t// SoftCAS is a CertificateAuthorityService using software.\n\tSoftCAS = \"softcas\"\n\t// CloudCAS is a CertificateAuthorityService using Google Cloud CAS.\n\tCloudCAS = \"cloudcas\"\n\t// StepCAS is a CertificateAuthorityService using another step-ca instance.\n\tStepCAS = \"stepcas\"\n\t// VaultCAS is a CertificateAuthorityService using Hasicorp Vault PKI.\n\tVaultCAS = \"vaultcas\"\n\t// ExternalCAS is a CertificateAuthorityService using an external injected CA implementation\n\tExternalCAS = \"externalcas\"\n)\n\n// String returns a string from the type. It will always return the lower case\n// version of the Type, as we need a standard type to compare and use as the\n// registry key.\nfunc (t Type) String() string {\n\tif t == \"\" {\n\t\treturn SoftCAS\n\t}\n\treturn strings.ToLower(string(t))\n}\n\n// TypeOf returns the type of the given CertificateAuthorityService.\nfunc TypeOf(c CertificateAuthorityService) Type {\n\tif ct, ok := c.(interface{ Type() Type }); ok {\n\t\treturn ct.Type()\n\t}\n\treturn ExternalCAS\n}\n\n// NotImplementedError is the type of error returned if an operation is not implemented.\ntype NotImplementedError struct {\n\tMessage string\n}\n\n// Error implements the error interface.\nfunc (e NotImplementedError) Error() string {\n\tif e.Message != \"\" {\n\t\treturn e.Message\n\t}\n\treturn \"not implemented\"\n}\n\n// StatusCode implements the StatusCoder interface and returns the HTTP 501\n// error.\nfunc (e NotImplementedError) StatusCode() int {\n\treturn http.StatusNotImplemented\n}\n\n// ValidationError is the type of error returned if request is not properly\n// validated.\ntype ValidationError struct {\n\tMessage string\n}\n\n// NotImplementedError implements the error interface.\nfunc (e ValidationError) Error() string {\n\tif e.Message != \"\" {\n\t\treturn e.Message\n\t}\n\treturn \"bad request\"\n}\n\n// StatusCode implements the StatusCoder interface and returns the HTTP 400\n// error.\nfunc (e ValidationError) StatusCode() int {\n\treturn http.StatusBadRequest\n}\n"
  },
  {
    "path": "cas/apiv1/services_test.go",
    "content": "package apiv1\n\nimport (\n\t\"testing\"\n)\n\ntype simpleCAS struct{}\n\nfunc (*simpleCAS) CreateCertificate(req *CreateCertificateRequest) (*CreateCertificateResponse, error) {\n\treturn nil, NotImplementedError{}\n}\nfunc (*simpleCAS) RenewCertificate(req *RenewCertificateRequest) (*RenewCertificateResponse, error) {\n\treturn nil, NotImplementedError{}\n}\nfunc (*simpleCAS) RevokeCertificate(req *RevokeCertificateRequest) (*RevokeCertificateResponse, error) {\n\treturn nil, NotImplementedError{}\n}\n\ntype fakeCAS struct {\n\tsimpleCAS\n}\n\nfunc (*fakeCAS) Type() Type { return SoftCAS }\n\nfunc TestType_String(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tt    Type\n\t\twant string\n\t}{\n\t\t{\"default\", \"\", \"softcas\"},\n\t\t{\"SoftCAS\", SoftCAS, \"softcas\"},\n\t\t{\"CloudCAS\", CloudCAS, \"cloudcas\"},\n\t\t{\"ExternalCAS\", ExternalCAS, \"externalcas\"},\n\t\t{\"UnknownCAS\", \"UnknownCAS\", \"unknowncas\"},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif got := tt.t.String(); got != tt.want {\n\t\t\t\tt.Errorf(\"Type.String() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestTypeOf(t *testing.T) {\n\ttype args struct {\n\t\tc CertificateAuthorityService\n\t}\n\ttests := []struct {\n\t\tname string\n\t\targs args\n\t\twant Type\n\t}{\n\t\t{\"ok\", args{&simpleCAS{}}, ExternalCAS},\n\t\t{\"ok with type\", args{&fakeCAS{}}, SoftCAS},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif got := TypeOf(tt.args.c); got != tt.want {\n\t\t\t\tt.Errorf(\"TypeOf() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNotImplementedError_Error(t *testing.T) {\n\ttype fields struct {\n\t\tMessage string\n\t}\n\ttests := []struct {\n\t\tname   string\n\t\tfields fields\n\t\twant   string\n\t}{\n\t\t{\"default\", fields{\"\"}, \"not implemented\"},\n\t\t{\"with message\", fields{\"method not supported\"}, \"method not supported\"},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\te := NotImplementedError{\n\t\t\t\tMessage: tt.fields.Message,\n\t\t\t}\n\t\t\tif got := e.Error(); got != tt.want {\n\t\t\t\tt.Errorf(\"NotImplementedError.Error() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNotImplementedError_StatusCode(t *testing.T) {\n\ttype fields struct {\n\t\tMessage string\n\t}\n\ttests := []struct {\n\t\tname   string\n\t\tfields fields\n\t\twant   int\n\t}{\n\t\t{\"default\", fields{\"\"}, 501},\n\t\t{\"with message\", fields{\"method not supported\"}, 501},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ts := NotImplementedError{\n\t\t\t\tMessage: tt.fields.Message,\n\t\t\t}\n\t\t\tif got := s.StatusCode(); got != tt.want {\n\t\t\t\tt.Errorf(\"NotImplementedError.StatusCode() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestValidationError_Error(t *testing.T) {\n\ttype fields struct {\n\t\tMessage string\n\t}\n\ttests := []struct {\n\t\tname   string\n\t\tfields fields\n\t\twant   string\n\t}{\n\t\t{\"default\", fields{\"\"}, \"bad request\"},\n\t\t{\"with message\", fields{\"token is empty\"}, \"token is empty\"},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\te := ValidationError{\n\t\t\t\tMessage: tt.fields.Message,\n\t\t\t}\n\t\t\tif got := e.Error(); got != tt.want {\n\t\t\t\tt.Errorf(\"ValidationError.Error() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestValidationError_StatusCode(t *testing.T) {\n\ttype fields struct {\n\t\tMessage string\n\t}\n\ttests := []struct {\n\t\tname   string\n\t\tfields fields\n\t\twant   int\n\t}{\n\t\t{\"default\", fields{\"\"}, 400},\n\t\t{\"with message\", fields{\"token is empty\"}, 400},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\te := ValidationError{\n\t\t\t\tMessage: tt.fields.Message,\n\t\t\t}\n\t\t\tif got := e.StatusCode(); got != tt.want {\n\t\t\t\tt.Errorf(\"ValidationError.StatusCode() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "cas/cas.go",
    "content": "package cas\n\nimport (\n\t\"context\"\n\t\"strings\"\n\n\t\"github.com/pkg/errors\"\n\t\"github.com/smallstep/certificates/cas/apiv1\"\n\n\t// Enable default implementation\n\t_ \"github.com/smallstep/certificates/cas/softcas\"\n)\n\n// CertificateAuthorityService is the interface implemented by all the CAS.\ntype CertificateAuthorityService = apiv1.CertificateAuthorityService\n\n// CertificateAuthorityCreator is the interface implemented by all CAS that can create a new authority.\ntype CertificateAuthorityCreator = apiv1.CertificateAuthorityCreator\n\n// New creates a new CertificateAuthorityService using the given options.\nfunc New(ctx context.Context, opts apiv1.Options) (CertificateAuthorityService, error) {\n\tif err := opts.Validate(); err != nil {\n\t\treturn nil, err\n\t}\n\n\tt := apiv1.Type(strings.ToLower(opts.Type))\n\tif t == apiv1.DefaultCAS {\n\t\tt = apiv1.SoftCAS\n\t}\n\n\tfn, ok := apiv1.LoadCertificateAuthorityServiceNewFunc(t)\n\tif !ok {\n\t\treturn nil, errors.Errorf(\"unsupported cas type '%s'\", t)\n\t}\n\treturn fn(ctx, opts)\n}\n\n// NewCreator creates a new CertificateAuthorityCreator using the given options.\nfunc NewCreator(ctx context.Context, opts apiv1.Options) (CertificateAuthorityCreator, error) {\n\topts.IsCreator = true\n\n\tt := apiv1.Type(strings.ToLower(opts.Type))\n\tif t == apiv1.DefaultCAS {\n\t\tt = apiv1.SoftCAS\n\t}\n\n\tsvc, err := New(ctx, opts)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tcreator, ok := svc.(CertificateAuthorityCreator)\n\tif !ok {\n\t\treturn nil, errors.Errorf(\"cas type '%s' does not implements CertificateAuthorityCreator\", t)\n\t}\n\n\treturn creator, nil\n}\n"
  },
  {
    "path": "cas/cas_test.go",
    "content": "package cas\n\nimport (\n\t\"context\"\n\t\"crypto/ed25519\"\n\t\"crypto/x509\"\n\t\"crypto/x509/pkix\"\n\t\"fmt\"\n\t\"reflect\"\n\t\"testing\"\n\n\t\"go.step.sm/crypto/kms\"\n\tkmsapi \"go.step.sm/crypto/kms/apiv1\"\n\n\t\"github.com/smallstep/certificates/cas/apiv1\"\n\t\"github.com/smallstep/certificates/cas/softcas\"\n)\n\ntype mockCAS struct{}\n\nfunc (m *mockCAS) CreateCertificate(*apiv1.CreateCertificateRequest) (*apiv1.CreateCertificateResponse, error) {\n\tpanic(\"not implemented\")\n}\n\nfunc (m *mockCAS) RenewCertificate(*apiv1.RenewCertificateRequest) (*apiv1.RenewCertificateResponse, error) {\n\tpanic(\"not implemented\")\n}\n\nfunc (m *mockCAS) RevokeCertificate(*apiv1.RevokeCertificateRequest) (*apiv1.RevokeCertificateResponse, error) {\n\tpanic(\"not implemented\")\n}\n\nfunc TestNew(t *testing.T) {\n\texpected := &softcas.SoftCAS{\n\t\tCertificateChain: []*x509.Certificate{{Subject: pkix.Name{CommonName: \"Test Issuer\"}}},\n\t\tSigner:           ed25519.PrivateKey{},\n\t}\n\n\tapiv1.Register(apiv1.Type(\"nockCAS\"), func(ctx context.Context, opts apiv1.Options) (apiv1.CertificateAuthorityService, error) {\n\t\treturn nil, fmt.Errorf(\"an error\")\n\t})\n\n\ttype args struct {\n\t\tctx  context.Context\n\t\topts apiv1.Options\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\targs    args\n\t\twant    CertificateAuthorityService\n\t\twantErr bool\n\t}{\n\t\t{\"ok default\", args{context.Background(), apiv1.Options{\n\t\t\tCertificateChain: []*x509.Certificate{{Subject: pkix.Name{CommonName: \"Test Issuer\"}}},\n\t\t\tSigner:           ed25519.PrivateKey{},\n\t\t}}, expected, false},\n\t\t{\"ok softcas\", args{context.Background(), apiv1.Options{\n\t\t\tType:             \"softcas\",\n\t\t\tCertificateChain: []*x509.Certificate{{Subject: pkix.Name{CommonName: \"Test Issuer\"}}},\n\t\t\tSigner:           ed25519.PrivateKey{},\n\t\t}}, expected, false},\n\t\t{\"ok SoftCAS\", args{context.Background(), apiv1.Options{\n\t\t\tType:             \"SoftCAS\",\n\t\t\tCertificateChain: []*x509.Certificate{{Subject: pkix.Name{CommonName: \"Test Issuer\"}}},\n\t\t\tSigner:           ed25519.PrivateKey{},\n\t\t}}, expected, false},\n\t\t{\"fail empty\", args{context.Background(), apiv1.Options{}}, (*softcas.SoftCAS)(nil), true},\n\t\t{\"fail type\", args{context.Background(), apiv1.Options{Type: \"FailCAS\"}}, nil, true},\n\t\t{\"fail load\", args{context.Background(), apiv1.Options{Type: \"nockCAS\"}}, nil, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := New(tt.args.ctx, tt.args.opts)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"New() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"New() = %#v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewCreator(t *testing.T) {\n\tkeyManager, err := kms.New(context.Background(), kmsapi.Options{})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tapiv1.Register(apiv1.Type(\"nockCAS\"), func(ctx context.Context, opts apiv1.Options) (apiv1.CertificateAuthorityService, error) {\n\t\treturn &mockCAS{}, nil\n\t})\n\n\ttype args struct {\n\t\tctx  context.Context\n\t\topts apiv1.Options\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\targs    args\n\t\twant    CertificateAuthorityCreator\n\t\twantErr bool\n\t}{\n\t\t{\"ok empty\", args{context.Background(), apiv1.Options{}}, &softcas.SoftCAS{}, false},\n\t\t{\"ok softcas\", args{context.Background(), apiv1.Options{\n\t\t\tType: \"softcas\",\n\t\t}}, &softcas.SoftCAS{}, false},\n\t\t{\"ok SoftCAS\", args{context.Background(), apiv1.Options{\n\t\t\tType:       \"SoftCAS\",\n\t\t\tKeyManager: keyManager,\n\t\t}}, &softcas.SoftCAS{KeyManager: keyManager}, false},\n\t\t{\"fail type\", args{context.Background(), apiv1.Options{Type: \"FailCAS\"}}, nil, true},\n\t\t{\"fail no creator\", args{context.Background(), apiv1.Options{Type: \"nockCAS\"}}, nil, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := NewCreator(tt.args.ctx, tt.args.opts)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"NewCreator() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"NewCreator() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "cas/cloudcas/certificate.go",
    "content": "package cloudcas\n\nimport (\n\t\"crypto\"\n\t\"crypto/ecdsa\"\n\t\"crypto/rsa\"\n\t\"crypto/x509\"\n\t\"crypto/x509/pkix\"\n\t\"encoding/asn1\"\n\t\"encoding/pem\"\n\t\"fmt\"\n\n\tpb \"cloud.google.com/go/security/privateca/apiv1/privatecapb\"\n\t\"github.com/pkg/errors\"\n\n\tkmsapi \"go.step.sm/crypto/kms/apiv1\"\n\n\t\"github.com/smallstep/certificates/internal/cast\"\n)\n\nvar (\n\toidExtensionSubjectKeyID          = []int{2, 5, 29, 14}\n\toidExtensionKeyUsage              = []int{2, 5, 29, 15}\n\toidExtensionExtendedKeyUsage      = []int{2, 5, 29, 37}\n\toidExtensionAuthorityKeyID        = []int{2, 5, 29, 35}\n\toidExtensionBasicConstraints      = []int{2, 5, 29, 19}\n\toidExtensionSubjectAltName        = []int{2, 5, 29, 17}\n\toidExtensionCRLDistributionPoints = []int{2, 5, 29, 31}\n\toidExtensionCertificatePolicies   = []int{2, 5, 29, 32}\n\toidExtensionAuthorityInfoAccess   = []int{1, 3, 6, 1, 5, 5, 7, 1, 1}\n)\n\nvar extraExtensions = [...]asn1.ObjectIdentifier{\n\toidExtensionSubjectKeyID,          // Added by CAS\n\toidExtensionKeyUsage,              // Added in CertificateConfig.ReusableConfig\n\toidExtensionExtendedKeyUsage,      // Added in CertificateConfig.ReusableConfig\n\toidExtensionAuthorityKeyID,        // Added by CAS\n\toidExtensionBasicConstraints,      // Added in CertificateConfig.ReusableConfig\n\toidExtensionSubjectAltName,        // Added in CertificateConfig.SubjectConfig.SubjectAltName\n\toidExtensionCRLDistributionPoints, // Added by CAS\n\toidExtensionCertificatePolicies,   // Added in CertificateConfig.ReusableConfig\n\toidExtensionAuthorityInfoAccess,   // Added in CertificateConfig.ReusableConfig and by CAS\n}\n\nvar (\n\toidExtKeyUsageAny                            = asn1.ObjectIdentifier{2, 5, 29, 37, 0}\n\toidExtKeyUsageIPSECEndSystem                 = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 5}\n\toidExtKeyUsageIPSECTunnel                    = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 6}\n\toidExtKeyUsageIPSECUser                      = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 7}\n\toidExtKeyUsageMicrosoftServerGatedCrypto     = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 311, 10, 3, 3}\n\toidExtKeyUsageNetscapeServerGatedCrypto      = asn1.ObjectIdentifier{2, 16, 840, 1, 113730, 4, 1}\n\toidExtKeyUsageMicrosoftCommercialCodeSigning = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 311, 2, 1, 22}\n\toidExtKeyUsageMicrosoftKernelCodeSigning     = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 311, 61, 1, 1}\n)\n\nconst (\n\tnameTypeEmail = 1\n\tnameTypeDNS   = 2\n\tnameTypeURI   = 6\n\tnameTypeIP    = 7\n)\n\nfunc createCertificateConfig(tpl *x509.Certificate) (*pb.Certificate_Config, error) {\n\tpk, err := createPublicKey(tpl.PublicKey)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tconfig := &pb.CertificateConfig{\n\t\tSubjectConfig: &pb.CertificateConfig_SubjectConfig{\n\t\t\tSubject:        createSubject(tpl),\n\t\t\tSubjectAltName: createSubjectAlternativeNames(tpl),\n\t\t},\n\t\tX509Config: createX509Parameters(tpl),\n\t\tPublicKey:  pk,\n\t}\n\treturn &pb.Certificate_Config{\n\t\tConfig: config,\n\t}, nil\n}\n\nfunc createPublicKey(key crypto.PublicKey) (*pb.PublicKey, error) {\n\tswitch key := key.(type) {\n\tcase *ecdsa.PublicKey:\n\t\tasn1Bytes, err := x509.MarshalPKIXPublicKey(key)\n\t\tif err != nil {\n\t\t\treturn nil, errors.Wrap(err, \"error marshaling public key\")\n\t\t}\n\t\treturn &pb.PublicKey{\n\t\t\tFormat: pb.PublicKey_PEM,\n\t\t\tKey: pem.EncodeToMemory(&pem.Block{\n\t\t\t\tType:  \"PUBLIC KEY\",\n\t\t\t\tBytes: asn1Bytes,\n\t\t\t}),\n\t\t}, nil\n\tcase *rsa.PublicKey:\n\t\treturn &pb.PublicKey{\n\t\t\tFormat: pb.PublicKey_PEM,\n\t\t\tKey: pem.EncodeToMemory(&pem.Block{\n\t\t\t\tType:  \"RSA PUBLIC KEY\",\n\t\t\t\tBytes: x509.MarshalPKCS1PublicKey(key),\n\t\t\t}),\n\t\t}, nil\n\tdefault:\n\t\treturn nil, errors.Errorf(\"unsupported public key type: %T\", key)\n\t}\n}\n\nfunc createSubject(cert *x509.Certificate) *pb.Subject {\n\tsub := cert.Subject\n\tret := &pb.Subject{\n\t\tCommonName: sub.CommonName,\n\t}\n\tif len(sub.Country) > 0 {\n\t\tret.CountryCode = sub.Country[0]\n\t}\n\tif len(sub.Organization) > 0 {\n\t\tret.Organization = sub.Organization[0]\n\t}\n\tif len(sub.OrganizationalUnit) > 0 {\n\t\tret.OrganizationalUnit = sub.OrganizationalUnit[0]\n\t}\n\tif len(sub.Locality) > 0 {\n\t\tret.Locality = sub.Locality[0]\n\t}\n\tif len(sub.Province) > 0 {\n\t\tret.Province = sub.Province[0]\n\t}\n\tif len(sub.StreetAddress) > 0 {\n\t\tret.StreetAddress = sub.StreetAddress[0]\n\t}\n\tif len(sub.PostalCode) > 0 {\n\t\tret.PostalCode = sub.PostalCode[0]\n\t}\n\treturn ret\n}\n\nfunc createSubjectAlternativeNames(cert *x509.Certificate) *pb.SubjectAltNames {\n\tret := new(pb.SubjectAltNames)\n\tret.DnsNames = cert.DNSNames\n\tret.EmailAddresses = cert.EmailAddresses\n\tif n := len(cert.IPAddresses); n > 0 {\n\t\tret.IpAddresses = make([]string, n)\n\t\tfor i, ip := range cert.IPAddresses {\n\t\t\tret.IpAddresses[i] = ip.String()\n\t\t}\n\t}\n\tif n := len(cert.URIs); n > 0 {\n\t\tret.Uris = make([]string, n)\n\t\tfor i, u := range cert.URIs {\n\t\t\tret.Uris[i] = u.String()\n\t\t}\n\t}\n\n\t// Add extra SANs coming from the extensions\n\tif ext, ok := findExtraExtension(cert, oidExtensionSubjectAltName); ok {\n\t\tvar rawValues []asn1.RawValue\n\t\tif _, err := asn1.Unmarshal(ext.Value, &rawValues); err == nil {\n\t\t\tvar newValues []asn1.RawValue\n\n\t\t\tfor _, v := range rawValues {\n\t\t\t\tif v.Class == asn1.ClassContextSpecific {\n\t\t\t\t\tswitch v.Tag {\n\t\t\t\t\tcase nameTypeDNS:\n\t\t\t\t\t\tif len(ret.DnsNames) == 0 {\n\t\t\t\t\t\t\tnewValues = append(newValues, v)\n\t\t\t\t\t\t}\n\t\t\t\t\tcase nameTypeEmail:\n\t\t\t\t\t\tif len(ret.EmailAddresses) == 0 {\n\t\t\t\t\t\t\tnewValues = append(newValues, v)\n\t\t\t\t\t\t}\n\t\t\t\t\tcase nameTypeIP:\n\t\t\t\t\t\tif len(ret.IpAddresses) == 0 {\n\t\t\t\t\t\t\tnewValues = append(newValues, v)\n\t\t\t\t\t\t}\n\t\t\t\t\tcase nameTypeURI:\n\t\t\t\t\t\tif len(ret.Uris) == 0 {\n\t\t\t\t\t\t\tnewValues = append(newValues, v)\n\t\t\t\t\t\t}\n\t\t\t\t\tdefault:\n\t\t\t\t\t\tnewValues = append(newValues, v)\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tnewValues = append(newValues, v)\n\t\t\t\t}\n\t\t\t}\n\t\t\tif len(newValues) > 0 {\n\t\t\t\tif b, err := asn1.Marshal(newValues); err == nil {\n\t\t\t\t\tret.CustomSans = []*pb.X509Extension{{\n\t\t\t\t\t\tObjectId: createObjectID(ext.Id),\n\t\t\t\t\t\tCritical: ext.Critical,\n\t\t\t\t\t\tValue:    b,\n\t\t\t\t\t}}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn ret\n}\n\nfunc createX509Parameters(cert *x509.Certificate) *pb.X509Parameters {\n\tvar unknownEKUs []*pb.ObjectId\n\tvar ekuOptions = &pb.KeyUsage_ExtendedKeyUsageOptions{}\n\tfor _, eku := range cert.ExtKeyUsage {\n\t\tswitch eku {\n\t\tcase x509.ExtKeyUsageAny:\n\t\t\tunknownEKUs = append(unknownEKUs, createObjectID(oidExtKeyUsageAny))\n\t\tcase x509.ExtKeyUsageServerAuth:\n\t\t\tekuOptions.ServerAuth = true\n\t\tcase x509.ExtKeyUsageClientAuth:\n\t\t\tekuOptions.ClientAuth = true\n\t\tcase x509.ExtKeyUsageCodeSigning:\n\t\t\tekuOptions.CodeSigning = true\n\t\tcase x509.ExtKeyUsageEmailProtection:\n\t\t\tekuOptions.EmailProtection = true\n\t\tcase x509.ExtKeyUsageIPSECEndSystem:\n\t\t\tunknownEKUs = append(unknownEKUs, createObjectID(oidExtKeyUsageIPSECEndSystem))\n\t\tcase x509.ExtKeyUsageIPSECTunnel:\n\t\t\tunknownEKUs = append(unknownEKUs, createObjectID(oidExtKeyUsageIPSECTunnel))\n\t\tcase x509.ExtKeyUsageIPSECUser:\n\t\t\tunknownEKUs = append(unknownEKUs, createObjectID(oidExtKeyUsageIPSECUser))\n\t\tcase x509.ExtKeyUsageTimeStamping:\n\t\t\tekuOptions.TimeStamping = true\n\t\tcase x509.ExtKeyUsageOCSPSigning:\n\t\t\tekuOptions.OcspSigning = true\n\t\tcase x509.ExtKeyUsageMicrosoftServerGatedCrypto:\n\t\t\tunknownEKUs = append(unknownEKUs, createObjectID(oidExtKeyUsageMicrosoftServerGatedCrypto))\n\t\tcase x509.ExtKeyUsageNetscapeServerGatedCrypto:\n\t\t\tunknownEKUs = append(unknownEKUs, createObjectID(oidExtKeyUsageNetscapeServerGatedCrypto))\n\t\tcase x509.ExtKeyUsageMicrosoftCommercialCodeSigning:\n\t\t\tunknownEKUs = append(unknownEKUs, createObjectID(oidExtKeyUsageMicrosoftCommercialCodeSigning))\n\t\tcase x509.ExtKeyUsageMicrosoftKernelCodeSigning:\n\t\t\tunknownEKUs = append(unknownEKUs, createObjectID(oidExtKeyUsageMicrosoftKernelCodeSigning))\n\t\t}\n\t}\n\n\tfor _, oid := range cert.UnknownExtKeyUsage {\n\t\tunknownEKUs = append(unknownEKUs, createObjectID(oid))\n\t}\n\n\tvar policyIDs []*pb.ObjectId\n\tfor _, oid := range cert.PolicyIdentifiers {\n\t\tpolicyIDs = append(policyIDs, createObjectID(oid))\n\t}\n\n\tvar caOptions *pb.X509Parameters_CaOptions\n\tif cert.BasicConstraintsValid {\n\t\tcaOptions = new(pb.X509Parameters_CaOptions)\n\t\tvar maxPathLength int32\n\t\tswitch {\n\t\tcase cert.MaxPathLenZero:\n\t\t\tmaxPathLength = 0\n\t\t\tcaOptions.MaxIssuerPathLength = &maxPathLength\n\t\tcase cert.MaxPathLen > 0:\n\t\t\tmaxPathLength = cast.Int32(cert.MaxPathLen)\n\t\t\tcaOptions.MaxIssuerPathLength = &maxPathLength\n\t\t}\n\t\tcaOptions.IsCa = &cert.IsCA\n\t}\n\n\tvar extraExtensions []*pb.X509Extension\n\tfor _, ext := range cert.ExtraExtensions {\n\t\tif isExtraExtension(ext.Id) {\n\t\t\textraExtensions = append(extraExtensions, &pb.X509Extension{\n\t\t\t\tObjectId: createObjectID(ext.Id),\n\t\t\t\tCritical: ext.Critical,\n\t\t\t\tValue:    ext.Value,\n\t\t\t})\n\t\t}\n\t}\n\n\treturn &pb.X509Parameters{\n\t\tKeyUsage: &pb.KeyUsage{\n\t\t\tBaseKeyUsage: &pb.KeyUsage_KeyUsageOptions{\n\t\t\t\tDigitalSignature:  cert.KeyUsage&x509.KeyUsageDigitalSignature > 0,\n\t\t\t\tContentCommitment: cert.KeyUsage&x509.KeyUsageContentCommitment > 0,\n\t\t\t\tKeyEncipherment:   cert.KeyUsage&x509.KeyUsageKeyEncipherment > 0,\n\t\t\t\tDataEncipherment:  cert.KeyUsage&x509.KeyUsageDataEncipherment > 0,\n\t\t\t\tKeyAgreement:      cert.KeyUsage&x509.KeyUsageKeyAgreement > 0,\n\t\t\t\tCertSign:          cert.KeyUsage&x509.KeyUsageCertSign > 0,\n\t\t\t\tCrlSign:           cert.KeyUsage&x509.KeyUsageCRLSign > 0,\n\t\t\t\tEncipherOnly:      cert.KeyUsage&x509.KeyUsageEncipherOnly > 0,\n\t\t\t\tDecipherOnly:      cert.KeyUsage&x509.KeyUsageDecipherOnly > 0,\n\t\t\t},\n\t\t\tExtendedKeyUsage:         ekuOptions,\n\t\t\tUnknownExtendedKeyUsages: unknownEKUs,\n\t\t},\n\t\tCaOptions:            caOptions,\n\t\tPolicyIds:            policyIDs,\n\t\tAiaOcspServers:       cert.OCSPServer,\n\t\tAdditionalExtensions: extraExtensions,\n\t}\n}\n\n// isExtraExtension returns true if the extension oid is not managed in a\n// different way.\nfunc isExtraExtension(oid asn1.ObjectIdentifier) bool {\n\tfor _, id := range extraExtensions {\n\t\tif id.Equal(oid) {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\nfunc createObjectID(oid asn1.ObjectIdentifier) *pb.ObjectId {\n\tret := make([]int32, len(oid))\n\tfor i, v := range oid {\n\t\tret[i] = cast.Int32(v)\n\t}\n\treturn &pb.ObjectId{\n\t\tObjectIdPath: ret,\n\t}\n}\n\nfunc findExtraExtension(cert *x509.Certificate, oid asn1.ObjectIdentifier) (pkix.Extension, bool) {\n\tfor _, ext := range cert.ExtraExtensions {\n\t\tif ext.Id.Equal(oid) {\n\t\t\treturn ext, true\n\t\t}\n\t}\n\treturn pkix.Extension{}, false\n}\n\nfunc createKeyVersionSpec(alg kmsapi.SignatureAlgorithm, bits int) (*pb.CertificateAuthority_KeyVersionSpec, error) {\n\tswitch alg {\n\tcase kmsapi.UnspecifiedSignAlgorithm, kmsapi.ECDSAWithSHA256:\n\t\treturn &pb.CertificateAuthority_KeyVersionSpec{\n\t\t\tKeyVersion: &pb.CertificateAuthority_KeyVersionSpec_Algorithm{\n\t\t\t\tAlgorithm: pb.CertificateAuthority_EC_P256_SHA256,\n\t\t\t},\n\t\t}, nil\n\tcase kmsapi.ECDSAWithSHA384:\n\t\treturn &pb.CertificateAuthority_KeyVersionSpec{\n\t\t\tKeyVersion: &pb.CertificateAuthority_KeyVersionSpec_Algorithm{\n\t\t\t\tAlgorithm: pb.CertificateAuthority_EC_P384_SHA384,\n\t\t\t},\n\t\t}, nil\n\tcase kmsapi.SHA256WithRSA:\n\t\talgo, err := getRSAPKCS1Algorithm(bits)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn &pb.CertificateAuthority_KeyVersionSpec{\n\t\t\tKeyVersion: &pb.CertificateAuthority_KeyVersionSpec_Algorithm{\n\t\t\t\tAlgorithm: algo,\n\t\t\t},\n\t\t}, nil\n\tcase kmsapi.SHA256WithRSAPSS:\n\t\talgo, err := getRSAPSSAlgorithm(bits)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn &pb.CertificateAuthority_KeyVersionSpec{\n\t\t\tKeyVersion: &pb.CertificateAuthority_KeyVersionSpec_Algorithm{\n\t\t\t\tAlgorithm: algo,\n\t\t\t},\n\t\t}, nil\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unknown or unsupported signature algorithm '%s'\", alg)\n\t}\n}\n\nfunc getRSAPKCS1Algorithm(bits int) (pb.CertificateAuthority_SignHashAlgorithm, error) {\n\tswitch bits {\n\tcase 0, 3072:\n\t\treturn pb.CertificateAuthority_RSA_PKCS1_3072_SHA256, nil\n\tcase 2048:\n\t\treturn pb.CertificateAuthority_RSA_PKCS1_2048_SHA256, nil\n\tcase 4096:\n\t\treturn pb.CertificateAuthority_RSA_PKCS1_4096_SHA256, nil\n\tdefault:\n\t\treturn 0, fmt.Errorf(\"unsupported RSA PKCS #1 key size '%d'\", bits)\n\t}\n}\n\nfunc getRSAPSSAlgorithm(bits int) (pb.CertificateAuthority_SignHashAlgorithm, error) {\n\tswitch bits {\n\tcase 0, 3072:\n\t\treturn pb.CertificateAuthority_RSA_PSS_3072_SHA256, nil\n\tcase 2048:\n\t\treturn pb.CertificateAuthority_RSA_PSS_2048_SHA256, nil\n\tcase 4096:\n\t\treturn pb.CertificateAuthority_RSA_PSS_4096_SHA256, nil\n\tdefault:\n\t\treturn 0, fmt.Errorf(\"unsupported RSA-PSS key size '%d'\", bits)\n\t}\n}\n"
  },
  {
    "path": "cas/cloudcas/certificate_test.go",
    "content": "package cloudcas\n\nimport (\n\t\"crypto\"\n\t\"crypto/ecdsa\"\n\t\"crypto/ed25519\"\n\t\"crypto/elliptic\"\n\t\"crypto/rand\"\n\t\"crypto/x509\"\n\t\"crypto/x509/pkix\"\n\t\"encoding/asn1\"\n\t\"net\"\n\t\"net/url\"\n\t\"reflect\"\n\t\"testing\"\n\n\tpb \"cloud.google.com/go/security/privateca/apiv1/privatecapb\"\n\tkmsapi \"go.step.sm/crypto/kms/apiv1\"\n)\n\nvar (\n\ttestLeafPublicKey = `-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEAdUSRBrpgHFilN4eaGlNnX2+xfjX\na1Iwk2/+AensjFTXJi1UAIB0e+4pqi7Sen5E2QVBhntEHCrA3xOf7czgPw==\n-----END PUBLIC KEY-----\n`\n\ttestRSACertificate = `-----BEGIN CERTIFICATE-----\nMIICozCCAkmgAwIBAgIRANNhMpODj7ThgviZCoF6kj8wCgYIKoZIzj0EAwIwKjEo\nMCYGA1UEAxMfR29vZ2xlIENBUyBUZXN0IEludGVybWVkaWF0ZSBDQTAeFw0yMDA5\nMTUwMTUxMDdaFw0zMDA5MTMwMTUxMDNaMB0xGzAZBgNVBAMTEnRlc3Quc21hbGxz\ndGVwLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANPRjuIlsP5Z\n672syAsHlbILFabG/xmrlsO0UdcLo4Yjf9WPAFA+7q+CsVDFh4dQbMv96fsHtdYP\nE9wlWyMqYG+5E8QT2i0WNFEoYcXOGZuXdyD/TA5Aucu1RuYLrZXQrXWDnvaWOgvr\nEZ6s9VsPCzzkL8KBejIMQIMY0KXEJfB/HgXZNn8V2trZkWT5CzxbcOF3s3UC1Z6F\nJa6zjpxhSyRkqgknJxv6yK4t7HEwdhrDI8uyxJYHPQWKNRjWecHWE9E+MtoS7D08\nmTh8qlAKoBbkGolR2nJSXffU09F3vSg+MIfjPiRqjf6394cQ3T9D5yZK//rCrxWU\n8KKBQMEmdKcCAwEAAaOBkTCBjjAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0lBBYwFAYI\nKwYBBQUHAwEGCCsGAQUFBwMCMB0GA1UdDgQWBBQffuoYvH1+IF1cipl35gXJxSJE\nSjAfBgNVHSMEGDAWgBRIOVqyLDSlErJLuWWEvRm5UU1r1TAdBgNVHREEFjAUghJ0\nZXN0LnNtYWxsc3RlcC5jb20wCgYIKoZIzj0EAwIDSAAwRQIhAL9AAw/LVLvvxBkM\nsJnHd+RIk7ZblkgcArwpIS2+Z5xNAiBtUED4zyimz9b4aQiXdw4IMd2CKxVyW8eE\n6x1vSZMvzQ==\n-----END CERTIFICATE-----`\n\ttestRSAPublicKey = `-----BEGIN RSA PUBLIC KEY-----\nMIIBCgKCAQEA09GO4iWw/lnrvazICweVsgsVpsb/GauWw7RR1wujhiN/1Y8AUD7u\nr4KxUMWHh1Bsy/3p+we11g8T3CVbIypgb7kTxBPaLRY0UShhxc4Zm5d3IP9MDkC5\ny7VG5gutldCtdYOe9pY6C+sRnqz1Ww8LPOQvwoF6MgxAgxjQpcQl8H8eBdk2fxXa\n2tmRZPkLPFtw4XezdQLVnoUlrrOOnGFLJGSqCScnG/rIri3scTB2GsMjy7LElgc9\nBYo1GNZ5wdYT0T4y2hLsPTyZOHyqUAqgFuQaiVHaclJd99TT0Xe9KD4wh+M+JGqN\n/rf3hxDdP0PnJkr/+sKvFZTwooFAwSZ0pwIDAQAB\n-----END RSA PUBLIC KEY-----\n`\n)\n\nfunc Test_createCertificateConfig(t *testing.T) {\n\tcert := mustParseCertificate(t, testLeafCertificate)\n\ttype args struct {\n\t\ttpl *x509.Certificate\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\targs    args\n\t\twant    *pb.Certificate_Config\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", args{cert}, &pb.Certificate_Config{\n\t\t\tConfig: &pb.CertificateConfig{\n\t\t\t\tSubjectConfig: &pb.CertificateConfig_SubjectConfig{\n\t\t\t\t\tSubject: &pb.Subject{\n\t\t\t\t\t\tCommonName: \"test.smallstep.com\",\n\t\t\t\t\t},\n\t\t\t\t\tSubjectAltName: &pb.SubjectAltNames{\n\t\t\t\t\t\tDnsNames: []string{\"test.smallstep.com\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tX509Config: &pb.X509Parameters{\n\t\t\t\t\tKeyUsage: &pb.KeyUsage{\n\t\t\t\t\t\tBaseKeyUsage: &pb.KeyUsage_KeyUsageOptions{\n\t\t\t\t\t\t\tDigitalSignature: true,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tExtendedKeyUsage: &pb.KeyUsage_ExtendedKeyUsageOptions{\n\t\t\t\t\t\t\tClientAuth: true,\n\t\t\t\t\t\t\tServerAuth: true,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tPublicKey: &pb.PublicKey{\n\t\t\t\t\tKey:    []byte(testLeafPublicKey),\n\t\t\t\t\tFormat: pb.PublicKey_PEM,\n\t\t\t\t},\n\t\t\t},\n\t\t}, false},\n\t\t{\"fail\", args{&x509.Certificate{}}, nil, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := createCertificateConfig(tt.args.tpl)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"createCertificateConfig() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"createCertificateConfig() = %v, want %v\", got.Config, tt.want.Config)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_createPublicKey(t *testing.T) {\n\tedpub, _, err := ed25519.GenerateKey(rand.Reader)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tecCert := mustParseCertificate(t, testLeafCertificate)\n\tecCertPublicKey := ecCert.PublicKey.(*ecdsa.PublicKey)\n\trsaCert := mustParseCertificate(t, testRSACertificate)\n\ttype args struct {\n\t\tkey crypto.PublicKey\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\targs    args\n\t\twant    *pb.PublicKey\n\t\twantErr bool\n\t}{\n\t\t{\"ok ec\", args{ecCert.PublicKey}, &pb.PublicKey{\n\t\t\tFormat: pb.PublicKey_PEM,\n\t\t\tKey:    []byte(testLeafPublicKey),\n\t\t}, false},\n\t\t{\"ok rsa\", args{rsaCert.PublicKey}, &pb.PublicKey{\n\t\t\tFormat: pb.PublicKey_PEM,\n\t\t\tKey:    []byte(testRSAPublicKey),\n\t\t}, false},\n\t\t{\"fail ed25519\", args{edpub}, nil, true},\n\t\t{\"fail ec marshal\", args{&ecdsa.PublicKey{\n\t\t\tCurve: &elliptic.CurveParams{\n\t\t\t\tName:    \"FOO\",\n\t\t\t\tBitSize: 256,\n\t\t\t\tP:       ecCertPublicKey.Params().P,\n\t\t\t\tB:       ecCertPublicKey.Params().B,\n\t\t\t},\n\t\t\tX: ecCertPublicKey.X,\n\t\t\tY: ecCertPublicKey.Y,\n\t\t}}, nil, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := createPublicKey(tt.args.key)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"createPublicKey() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"createPublicKey() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_createSubject(t *testing.T) {\n\ttype args struct {\n\t\tcert *x509.Certificate\n\t}\n\ttests := []struct {\n\t\tname string\n\t\targs args\n\t\twant *pb.Subject\n\t}{\n\t\t{\"ok empty\", args{&x509.Certificate{}}, &pb.Subject{}},\n\t\t{\"ok all\", args{&x509.Certificate{\n\t\t\tSubject: pkix.Name{\n\t\t\t\tCountry:            []string{\"US\"},\n\t\t\t\tOrganization:       []string{\"Smallstep Labs\"},\n\t\t\t\tOrganizationalUnit: []string{\"Engineering\"},\n\t\t\t\tLocality:           []string{\"San Francisco\"},\n\t\t\t\tProvince:           []string{\"California\"},\n\t\t\t\tStreetAddress:      []string{\"1 A St.\"},\n\t\t\t\tPostalCode:         []string{\"12345\"},\n\t\t\t\tSerialNumber:       \"1234567890\",\n\t\t\t\tCommonName:         \"test.smallstep.com\",\n\t\t\t},\n\t\t}}, &pb.Subject{\n\t\t\tCountryCode:        \"US\",\n\t\t\tOrganization:       \"Smallstep Labs\",\n\t\t\tOrganizationalUnit: \"Engineering\",\n\t\t\tLocality:           \"San Francisco\",\n\t\t\tProvince:           \"California\",\n\t\t\tStreetAddress:      \"1 A St.\",\n\t\t\tPostalCode:         \"12345\",\n\t\t\tCommonName:         \"test.smallstep.com\",\n\t\t}},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif got := createSubject(tt.args.cert); !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"createSubject() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_createSubjectAlternativeNames(t *testing.T) {\n\tmarshalRawValues := func(rawValues []asn1.RawValue) []byte {\n\t\tb, err := asn1.Marshal(rawValues)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\treturn b\n\t}\n\n\turi := func(s string) *url.URL {\n\t\tu, err := url.Parse(s)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\treturn u\n\t}\n\ttype args struct {\n\t\tcert *x509.Certificate\n\t}\n\ttests := []struct {\n\t\tname string\n\t\targs args\n\t\twant *pb.SubjectAltNames\n\t}{\n\t\t{\"ok empty\", args{&x509.Certificate{}}, &pb.SubjectAltNames{}},\n\t\t{\"ok dns\", args{&x509.Certificate{DNSNames: []string{\n\t\t\t\"doe.com\", \"doe.org\",\n\t\t}}}, &pb.SubjectAltNames{DnsNames: []string{\"doe.com\", \"doe.org\"}}},\n\t\t{\"ok emails\", args{&x509.Certificate{EmailAddresses: []string{\n\t\t\t\"john@doe.com\", \"jane@doe.com\",\n\t\t}}}, &pb.SubjectAltNames{EmailAddresses: []string{\"john@doe.com\", \"jane@doe.com\"}}},\n\t\t{\"ok ips\", args{&x509.Certificate{IPAddresses: []net.IP{\n\t\t\tnet.ParseIP(\"127.0.0.1\"), net.ParseIP(\"1.2.3.4\"),\n\t\t\tnet.ParseIP(\"::1\"), net.ParseIP(\"2001:0db8:85a3:a0b:12f0:8a2e:0370:7334\"), net.ParseIP(\"2001:0db8:85a3:0000:0000:8a2e:0370:7334\"),\n\t\t}}}, &pb.SubjectAltNames{IpAddresses: []string{\"127.0.0.1\", \"1.2.3.4\", \"::1\", \"2001:db8:85a3:a0b:12f0:8a2e:370:7334\", \"2001:db8:85a3::8a2e:370:7334\"}}},\n\t\t{\"ok uris\", args{&x509.Certificate{URIs: []*url.URL{\n\t\t\turi(\"mailto:john@doe.com\"), uri(\"https://john@doe.com/hello\"),\n\t\t}}}, &pb.SubjectAltNames{Uris: []string{\"mailto:john@doe.com\", \"https://john@doe.com/hello\"}}},\n\t\t{\"ok extensions\", args{&x509.Certificate{\n\t\t\tExtraExtensions: []pkix.Extension{{\n\t\t\t\tId: []int{2, 5, 29, 17}, Critical: true, Value: []byte{\n\t\t\t\t\t0x30, 0x48, 0x82, 0x0b, 0x77, 0x77, 0x77, 0x2e, 0x64, 0x6f, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x81,\n\t\t\t\t\t0x0c, 0x6a, 0x61, 0x6e, 0x65, 0x40, 0x64, 0x6f, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x87, 0x04, 0x01,\n\t\t\t\t\t0x02, 0x03, 0x04, 0x87, 0x10, 0x20, 0x01, 0x0d, 0xb8, 0x85, 0xa3, 0x0a, 0x0b, 0x12, 0xf0, 0x8a,\n\t\t\t\t\t0x2e, 0x03, 0x70, 0x73, 0x34, 0x86, 0x13, 0x6d, 0x61, 0x69, 0x6c, 0x74, 0x6f, 0x3a, 0x6a, 0x61,\n\t\t\t\t\t0x6e, 0x65, 0x40, 0x64, 0x6f, 0x65, 0x2e, 0x63, 0x6f, 0x6d,\n\t\t\t\t},\n\t\t\t}},\n\t\t}}, &pb.SubjectAltNames{\n\t\t\tCustomSans: []*pb.X509Extension{{\n\t\t\t\tObjectId: &pb.ObjectId{ObjectIdPath: []int32{2, 5, 29, 17}},\n\t\t\t\tCritical: true,\n\t\t\t\tValue: []byte{\n\t\t\t\t\t0x30, 0x48, 0x82, 0x0b, 0x77, 0x77, 0x77, 0x2e, 0x64, 0x6f, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x81,\n\t\t\t\t\t0x0c, 0x6a, 0x61, 0x6e, 0x65, 0x40, 0x64, 0x6f, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x87, 0x04, 0x01,\n\t\t\t\t\t0x02, 0x03, 0x04, 0x87, 0x10, 0x20, 0x01, 0x0d, 0xb8, 0x85, 0xa3, 0x0a, 0x0b, 0x12, 0xf0, 0x8a,\n\t\t\t\t\t0x2e, 0x03, 0x70, 0x73, 0x34, 0x86, 0x13, 0x6d, 0x61, 0x69, 0x6c, 0x74, 0x6f, 0x3a, 0x6a, 0x61,\n\t\t\t\t\t0x6e, 0x65, 0x40, 0x64, 0x6f, 0x65, 0x2e, 0x63, 0x6f, 0x6d,\n\t\t\t\t},\n\t\t\t}},\n\t\t}},\n\t\t{\"ok extra extensions\", args{&x509.Certificate{\n\t\t\tDNSNames: []string{\"doe.com\"},\n\t\t\tExtraExtensions: []pkix.Extension{{\n\t\t\t\tId: []int{2, 5, 29, 17}, Critical: true, Value: marshalRawValues([]asn1.RawValue{\n\t\t\t\t\t{Class: asn1.ClassApplication, Tag: 2, IsCompound: true, Bytes: []byte{}},\n\t\t\t\t\t{Class: asn1.ClassContextSpecific, Tag: nameTypeDNS, Bytes: []byte(\"doe.com\")},\n\t\t\t\t\t{Class: asn1.ClassContextSpecific, Tag: nameTypeEmail, Bytes: []byte(\"jane@doe.com\")},\n\t\t\t\t\t{Class: asn1.ClassContextSpecific, Tag: 8, Bytes: []byte(\"foo.bar\")},\n\t\t\t\t}),\n\t\t\t}},\n\t\t}}, &pb.SubjectAltNames{\n\t\t\tDnsNames: []string{\"doe.com\"},\n\t\t\tCustomSans: []*pb.X509Extension{{\n\t\t\t\tObjectId: &pb.ObjectId{ObjectIdPath: []int32{2, 5, 29, 17}},\n\t\t\t\tCritical: true,\n\t\t\t\tValue: marshalRawValues([]asn1.RawValue{\n\t\t\t\t\t{Class: asn1.ClassApplication, Tag: 2, IsCompound: true, Bytes: []byte{}},\n\t\t\t\t\t{Class: asn1.ClassContextSpecific, Tag: nameTypeEmail, Bytes: []byte(\"jane@doe.com\")},\n\t\t\t\t\t{Class: asn1.ClassContextSpecific, Tag: 8, Bytes: []byte(\"foo.bar\")},\n\t\t\t\t}),\n\t\t\t}},\n\t\t}},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif got := createSubjectAlternativeNames(tt.args.cert); !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"createSubjectAlternativeNames() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_createX509Parameters(t *testing.T) {\n\twithKU := func(ku *pb.KeyUsage) *pb.X509Parameters {\n\t\tif ku.BaseKeyUsage == nil {\n\t\t\tku.BaseKeyUsage = &pb.KeyUsage_KeyUsageOptions{}\n\t\t}\n\t\tif ku.ExtendedKeyUsage == nil {\n\t\t\tku.ExtendedKeyUsage = &pb.KeyUsage_ExtendedKeyUsageOptions{}\n\t\t}\n\t\treturn &pb.X509Parameters{\n\t\t\tKeyUsage: ku,\n\t\t}\n\t}\n\twithRCV := func(rcv *pb.X509Parameters) *pb.X509Parameters {\n\t\tif rcv.KeyUsage == nil {\n\t\t\trcv.KeyUsage = &pb.KeyUsage{\n\t\t\t\tBaseKeyUsage:     &pb.KeyUsage_KeyUsageOptions{},\n\t\t\t\tExtendedKeyUsage: &pb.KeyUsage_ExtendedKeyUsageOptions{},\n\t\t\t}\n\t\t}\n\t\treturn rcv\n\t}\n\n\tvTrue := true\n\tvFalse := false\n\tvZero := int32(0)\n\tvOne := int32(1)\n\n\ttype args struct {\n\t\tcert *x509.Certificate\n\t}\n\ttests := []struct {\n\t\tname string\n\t\targs args\n\t\twant *pb.X509Parameters\n\t}{\n\t\t{\"keyUsageDigitalSignature\", args{&x509.Certificate{\n\t\t\tKeyUsage: x509.KeyUsageDigitalSignature,\n\t\t}}, &pb.X509Parameters{\n\t\t\tKeyUsage: &pb.KeyUsage{\n\t\t\t\tBaseKeyUsage: &pb.KeyUsage_KeyUsageOptions{\n\t\t\t\t\tDigitalSignature: true,\n\t\t\t\t},\n\t\t\t\tExtendedKeyUsage:         &pb.KeyUsage_ExtendedKeyUsageOptions{},\n\t\t\t\tUnknownExtendedKeyUsages: nil,\n\t\t\t},\n\t\t\tCaOptions:            nil,\n\t\t\tPolicyIds:            nil,\n\t\t\tAiaOcspServers:       nil,\n\t\t\tAdditionalExtensions: nil,\n\t\t}},\n\t\t// KeyUsage\n\t\t{\"KeyUsageDigitalSignature\", args{&x509.Certificate{KeyUsage: x509.KeyUsageDigitalSignature}}, withKU(&pb.KeyUsage{\n\t\t\tBaseKeyUsage: &pb.KeyUsage_KeyUsageOptions{\n\t\t\t\tDigitalSignature: true,\n\t\t\t},\n\t\t})},\n\t\t{\"KeyUsageContentCommitment\", args{&x509.Certificate{KeyUsage: x509.KeyUsageContentCommitment}}, withKU(&pb.KeyUsage{\n\t\t\tBaseKeyUsage: &pb.KeyUsage_KeyUsageOptions{\n\t\t\t\tContentCommitment: true,\n\t\t\t},\n\t\t})},\n\t\t{\"KeyUsageKeyEncipherment\", args{&x509.Certificate{KeyUsage: x509.KeyUsageKeyEncipherment}}, withKU(&pb.KeyUsage{\n\t\t\tBaseKeyUsage: &pb.KeyUsage_KeyUsageOptions{\n\t\t\t\tKeyEncipherment: true,\n\t\t\t},\n\t\t})},\n\t\t{\"KeyUsageDataEncipherment\", args{&x509.Certificate{KeyUsage: x509.KeyUsageDataEncipherment}}, withKU(&pb.KeyUsage{\n\t\t\tBaseKeyUsage: &pb.KeyUsage_KeyUsageOptions{\n\t\t\t\tDataEncipherment: true,\n\t\t\t},\n\t\t})},\n\t\t{\"KeyUsageKeyAgreement\", args{&x509.Certificate{KeyUsage: x509.KeyUsageKeyAgreement}}, withKU(&pb.KeyUsage{\n\t\t\tBaseKeyUsage: &pb.KeyUsage_KeyUsageOptions{\n\t\t\t\tKeyAgreement: true,\n\t\t\t},\n\t\t})},\n\t\t{\"KeyUsageCertSign\", args{&x509.Certificate{KeyUsage: x509.KeyUsageCertSign}}, withKU(&pb.KeyUsage{\n\t\t\tBaseKeyUsage: &pb.KeyUsage_KeyUsageOptions{\n\t\t\t\tCertSign: true,\n\t\t\t},\n\t\t})},\n\t\t{\"KeyUsageCRLSign\", args{&x509.Certificate{KeyUsage: x509.KeyUsageCRLSign}}, withKU(&pb.KeyUsage{\n\t\t\tBaseKeyUsage: &pb.KeyUsage_KeyUsageOptions{\n\t\t\t\tCrlSign: true,\n\t\t\t},\n\t\t})},\n\t\t{\"KeyUsageEncipherOnly\", args{&x509.Certificate{KeyUsage: x509.KeyUsageEncipherOnly}}, withKU(&pb.KeyUsage{\n\t\t\tBaseKeyUsage: &pb.KeyUsage_KeyUsageOptions{\n\t\t\t\tEncipherOnly: true,\n\t\t\t},\n\t\t})},\n\t\t{\"KeyUsageDecipherOnly\", args{&x509.Certificate{KeyUsage: x509.KeyUsageDecipherOnly}}, withKU(&pb.KeyUsage{\n\t\t\tBaseKeyUsage: &pb.KeyUsage_KeyUsageOptions{\n\t\t\t\tDecipherOnly: true,\n\t\t\t},\n\t\t})},\n\t\t// ExtKeyUsage\n\t\t{\"ExtKeyUsageAny\", args{&x509.Certificate{ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageAny}}}, withKU(&pb.KeyUsage{\n\t\t\tUnknownExtendedKeyUsages: []*pb.ObjectId{{ObjectIdPath: []int32{2, 5, 29, 37, 0}}},\n\t\t})},\n\t\t{\"ExtKeyUsageServerAuth\", args{&x509.Certificate{ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}}}, withKU(&pb.KeyUsage{\n\t\t\tExtendedKeyUsage: &pb.KeyUsage_ExtendedKeyUsageOptions{\n\t\t\t\tServerAuth: true,\n\t\t\t},\n\t\t})},\n\t\t{\"ExtKeyUsageClientAuth\", args{&x509.Certificate{ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}}}, withKU(&pb.KeyUsage{\n\t\t\tExtendedKeyUsage: &pb.KeyUsage_ExtendedKeyUsageOptions{\n\t\t\t\tClientAuth: true,\n\t\t\t},\n\t\t})},\n\t\t{\"ExtKeyUsageCodeSigning\", args{&x509.Certificate{ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageCodeSigning}}}, withKU(&pb.KeyUsage{\n\t\t\tExtendedKeyUsage: &pb.KeyUsage_ExtendedKeyUsageOptions{\n\t\t\t\tCodeSigning: true,\n\t\t\t},\n\t\t})},\n\t\t{\"ExtKeyUsageEmailProtection\", args{&x509.Certificate{ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageEmailProtection}}}, withKU(&pb.KeyUsage{\n\t\t\tExtendedKeyUsage: &pb.KeyUsage_ExtendedKeyUsageOptions{\n\t\t\t\tEmailProtection: true,\n\t\t\t},\n\t\t})},\n\t\t{\"ExtKeyUsageIPSECEndSystem\", args{&x509.Certificate{ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageIPSECEndSystem}}}, withKU(&pb.KeyUsage{\n\t\t\tUnknownExtendedKeyUsages: []*pb.ObjectId{{ObjectIdPath: []int32{1, 3, 6, 1, 5, 5, 7, 3, 5}}},\n\t\t})},\n\t\t{\"ExtKeyUsageIPSECTunnel\", args{&x509.Certificate{ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageIPSECTunnel}}}, withKU(&pb.KeyUsage{\n\t\t\tUnknownExtendedKeyUsages: []*pb.ObjectId{{ObjectIdPath: []int32{1, 3, 6, 1, 5, 5, 7, 3, 6}}},\n\t\t})},\n\t\t{\"ExtKeyUsageIPSECUser\", args{&x509.Certificate{ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageIPSECUser}}}, withKU(&pb.KeyUsage{\n\t\t\tUnknownExtendedKeyUsages: []*pb.ObjectId{{ObjectIdPath: []int32{1, 3, 6, 1, 5, 5, 7, 3, 7}}},\n\t\t})},\n\t\t{\"ExtKeyUsageTimeStamping\", args{&x509.Certificate{ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageTimeStamping}}}, withKU(&pb.KeyUsage{\n\t\t\tExtendedKeyUsage: &pb.KeyUsage_ExtendedKeyUsageOptions{\n\t\t\t\tTimeStamping: true,\n\t\t\t},\n\t\t})},\n\t\t{\"ExtKeyUsageOCSPSigning\", args{&x509.Certificate{ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageOCSPSigning}}}, withKU(&pb.KeyUsage{\n\t\t\tExtendedKeyUsage: &pb.KeyUsage_ExtendedKeyUsageOptions{\n\t\t\t\tOcspSigning: true,\n\t\t\t},\n\t\t})},\n\t\t{\"ExtKeyUsageMicrosoftServerGatedCrypto\", args{&x509.Certificate{ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageMicrosoftServerGatedCrypto}}}, withKU(&pb.KeyUsage{\n\t\t\tUnknownExtendedKeyUsages: []*pb.ObjectId{{ObjectIdPath: []int32{1, 3, 6, 1, 4, 1, 311, 10, 3, 3}}},\n\t\t})},\n\t\t{\"ExtKeyUsageNetscapeServerGatedCrypto\", args{&x509.Certificate{ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageNetscapeServerGatedCrypto}}}, withKU(&pb.KeyUsage{\n\t\t\tUnknownExtendedKeyUsages: []*pb.ObjectId{{ObjectIdPath: []int32{2, 16, 840, 1, 113730, 4, 1}}},\n\t\t})},\n\t\t{\"ExtKeyUsageMicrosoftCommercialCodeSigning\", args{&x509.Certificate{ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageMicrosoftCommercialCodeSigning}}}, withKU(&pb.KeyUsage{\n\t\t\tUnknownExtendedKeyUsages: []*pb.ObjectId{{ObjectIdPath: []int32{1, 3, 6, 1, 4, 1, 311, 2, 1, 22}}},\n\t\t})},\n\t\t{\"ExtKeyUsageMicrosoftKernelCodeSigning\", args{&x509.Certificate{ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageMicrosoftKernelCodeSigning}}}, withKU(&pb.KeyUsage{\n\t\t\tUnknownExtendedKeyUsages: []*pb.ObjectId{{ObjectIdPath: []int32{1, 3, 6, 1, 4, 1, 311, 61, 1, 1}}},\n\t\t})},\n\t\t// UnknownExtendedKeyUsages\n\t\t{\"UnknownExtKeyUsage\", args{&x509.Certificate{UnknownExtKeyUsage: []asn1.ObjectIdentifier{{1, 2, 3, 4}, {4, 3, 2, 1}}}}, withKU(&pb.KeyUsage{\n\t\t\tUnknownExtendedKeyUsages: []*pb.ObjectId{\n\t\t\t\t{ObjectIdPath: []int32{1, 2, 3, 4}},\n\t\t\t\t{ObjectIdPath: []int32{4, 3, 2, 1}},\n\t\t\t},\n\t\t})},\n\t\t// BasicCre\n\t\t{\"BasicConstraintsCAMax0\", args{&x509.Certificate{BasicConstraintsValid: true, IsCA: true, MaxPathLen: 0, MaxPathLenZero: true}}, withRCV(&pb.X509Parameters{\n\t\t\tCaOptions: &pb.X509Parameters_CaOptions{\n\t\t\t\tIsCa:                &vTrue,\n\t\t\t\tMaxIssuerPathLength: &vZero,\n\t\t\t},\n\t\t})},\n\t\t{\"BasicConstraintsCAMax1\", args{&x509.Certificate{BasicConstraintsValid: true, IsCA: true, MaxPathLen: 1, MaxPathLenZero: false}}, withRCV(&pb.X509Parameters{\n\t\t\tCaOptions: &pb.X509Parameters_CaOptions{\n\t\t\t\tIsCa:                &vTrue,\n\t\t\t\tMaxIssuerPathLength: &vOne,\n\t\t\t},\n\t\t})},\n\t\t{\"BasicConstraintsCANoMax\", args{&x509.Certificate{BasicConstraintsValid: true, IsCA: true, MaxPathLen: -1, MaxPathLenZero: false}}, withRCV(&pb.X509Parameters{\n\t\t\tCaOptions: &pb.X509Parameters_CaOptions{\n\t\t\t\tIsCa:                &vTrue,\n\t\t\t\tMaxIssuerPathLength: nil,\n\t\t\t},\n\t\t})},\n\t\t{\"BasicConstraintsCANoMax0\", args{&x509.Certificate{BasicConstraintsValid: true, IsCA: true, MaxPathLen: 0, MaxPathLenZero: false}}, withRCV(&pb.X509Parameters{\n\t\t\tCaOptions: &pb.X509Parameters_CaOptions{\n\t\t\t\tIsCa:                &vTrue,\n\t\t\t\tMaxIssuerPathLength: nil,\n\t\t\t},\n\t\t})},\n\t\t{\"BasicConstraintsNoCA\", args{&x509.Certificate{BasicConstraintsValid: true, IsCA: false, MaxPathLen: 0, MaxPathLenZero: false}}, withRCV(&pb.X509Parameters{\n\t\t\tCaOptions: &pb.X509Parameters_CaOptions{\n\t\t\t\tIsCa:                &vFalse,\n\t\t\t\tMaxIssuerPathLength: nil,\n\t\t\t},\n\t\t})},\n\t\t{\"BasicConstraintsNoValid\", args{&x509.Certificate{BasicConstraintsValid: false, IsCA: false, MaxPathLen: 0, MaxPathLenZero: false}}, withRCV(&pb.X509Parameters{\n\t\t\tCaOptions: nil,\n\t\t})},\n\t\t// PolicyIdentifiers\n\t\t{\"PolicyIdentifiers\", args{&x509.Certificate{PolicyIdentifiers: []asn1.ObjectIdentifier{{1, 2, 3, 4}, {4, 3, 2, 1}}}}, withRCV(&pb.X509Parameters{\n\t\t\tPolicyIds: []*pb.ObjectId{\n\t\t\t\t{ObjectIdPath: []int32{1, 2, 3, 4}},\n\t\t\t\t{ObjectIdPath: []int32{4, 3, 2, 1}},\n\t\t\t},\n\t\t})},\n\t\t// OCSPServer\n\t\t{\"OCPServers\", args{&x509.Certificate{OCSPServer: []string{\"https://oscp.doe.com\", \"https://doe.com/ocsp\"}}}, withRCV(&pb.X509Parameters{\n\t\t\tAiaOcspServers: []string{\"https://oscp.doe.com\", \"https://doe.com/ocsp\"},\n\t\t})},\n\t\t// Extensions\n\t\t{\"Extensions\", args{&x509.Certificate{ExtraExtensions: []pkix.Extension{\n\t\t\t{Id: []int{1, 2, 3, 4}, Critical: true, Value: []byte(\"foobar\")},\n\t\t\t{Id: []int{2, 5, 29, 17}, Critical: true, Value: []byte(\"SANs\")}, //\n\t\t\t{Id: []int{4, 3, 2, 1}, Critical: false, Value: []byte(\"zoobar\")},\n\t\t\t{Id: []int{2, 5, 29, 31}, Critical: false, Value: []byte(\"CRL Distribution points\")},\n\t\t}}}, withRCV(&pb.X509Parameters{\n\t\t\tAdditionalExtensions: []*pb.X509Extension{\n\t\t\t\t{ObjectId: &pb.ObjectId{ObjectIdPath: []int32{1, 2, 3, 4}}, Critical: true, Value: []byte(\"foobar\")},\n\t\t\t\t{ObjectId: &pb.ObjectId{ObjectIdPath: []int32{4, 3, 2, 1}}, Critical: false, Value: []byte(\"zoobar\")},\n\t\t\t},\n\t\t})},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif got := createX509Parameters(tt.args.cert); !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"createX509Parameters() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_isExtraExtension(t *testing.T) {\n\ttype args struct {\n\t\toid asn1.ObjectIdentifier\n\t}\n\ttests := []struct {\n\t\tname string\n\t\targs args\n\t\twant bool\n\t}{\n\t\t{\"oidExtensionSubjectKeyID\", args{oidExtensionSubjectKeyID}, false},\n\t\t{\"oidExtensionKeyUsage\", args{oidExtensionKeyUsage}, false},\n\t\t{\"oidExtensionExtendedKeyUsage\", args{oidExtensionExtendedKeyUsage}, false},\n\t\t{\"oidExtensionAuthorityKeyID\", args{oidExtensionAuthorityKeyID}, false},\n\t\t{\"oidExtensionBasicConstraints\", args{oidExtensionBasicConstraints}, false},\n\t\t{\"oidExtensionSubjectAltName\", args{oidExtensionSubjectAltName}, false},\n\t\t{\"oidExtensionCRLDistributionPoints\", args{oidExtensionCRLDistributionPoints}, false},\n\t\t{\"oidExtensionCertificatePolicies\", args{oidExtensionCertificatePolicies}, false},\n\t\t{\"oidExtensionAuthorityInfoAccess\", args{oidExtensionAuthorityInfoAccess}, false},\n\t\t{\"other\", args{[]int{1, 2, 3, 4}}, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif got := isExtraExtension(tt.args.oid); got != tt.want {\n\t\t\t\tt.Errorf(\"isExtraExtension() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_createKeyVersionSpec(t *testing.T) {\n\ttype args struct {\n\t\talg  kmsapi.SignatureAlgorithm\n\t\tbits int\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\targs    args\n\t\twant    *pb.CertificateAuthority_KeyVersionSpec\n\t\twantErr bool\n\t}{\n\t\t{\"ok P256\", args{0, 0}, &pb.CertificateAuthority_KeyVersionSpec{\n\t\t\tKeyVersion: &pb.CertificateAuthority_KeyVersionSpec_Algorithm{\n\t\t\t\tAlgorithm: pb.CertificateAuthority_EC_P256_SHA256,\n\t\t\t}}, false},\n\t\t{\"ok P256\", args{kmsapi.ECDSAWithSHA256, 0}, &pb.CertificateAuthority_KeyVersionSpec{\n\t\t\tKeyVersion: &pb.CertificateAuthority_KeyVersionSpec_Algorithm{\n\t\t\t\tAlgorithm: pb.CertificateAuthority_EC_P256_SHA256,\n\t\t\t}}, false},\n\t\t{\"ok P384\", args{kmsapi.ECDSAWithSHA384, 0}, &pb.CertificateAuthority_KeyVersionSpec{\n\t\t\tKeyVersion: &pb.CertificateAuthority_KeyVersionSpec_Algorithm{\n\t\t\t\tAlgorithm: pb.CertificateAuthority_EC_P384_SHA384,\n\t\t\t}}, false},\n\t\t{\"ok RSA default\", args{kmsapi.SHA256WithRSA, 0}, &pb.CertificateAuthority_KeyVersionSpec{\n\t\t\tKeyVersion: &pb.CertificateAuthority_KeyVersionSpec_Algorithm{\n\t\t\t\tAlgorithm: pb.CertificateAuthority_RSA_PKCS1_3072_SHA256,\n\t\t\t}}, false},\n\t\t{\"ok RSA 2048\", args{kmsapi.SHA256WithRSA, 2048}, &pb.CertificateAuthority_KeyVersionSpec{\n\t\t\tKeyVersion: &pb.CertificateAuthority_KeyVersionSpec_Algorithm{\n\t\t\t\tAlgorithm: pb.CertificateAuthority_RSA_PKCS1_2048_SHA256,\n\t\t\t}}, false},\n\t\t{\"ok RSA 3072\", args{kmsapi.SHA256WithRSA, 3072}, &pb.CertificateAuthority_KeyVersionSpec{\n\t\t\tKeyVersion: &pb.CertificateAuthority_KeyVersionSpec_Algorithm{\n\t\t\t\tAlgorithm: pb.CertificateAuthority_RSA_PKCS1_3072_SHA256,\n\t\t\t}}, false},\n\t\t{\"ok RSA 4096\", args{kmsapi.SHA256WithRSA, 4096}, &pb.CertificateAuthority_KeyVersionSpec{\n\t\t\tKeyVersion: &pb.CertificateAuthority_KeyVersionSpec_Algorithm{\n\t\t\t\tAlgorithm: pb.CertificateAuthority_RSA_PKCS1_4096_SHA256,\n\t\t\t}}, false},\n\t\t{\"ok RSA-PSS default\", args{kmsapi.SHA256WithRSAPSS, 0}, &pb.CertificateAuthority_KeyVersionSpec{\n\t\t\tKeyVersion: &pb.CertificateAuthority_KeyVersionSpec_Algorithm{\n\t\t\t\tAlgorithm: pb.CertificateAuthority_RSA_PSS_3072_SHA256,\n\t\t\t}}, false},\n\t\t{\"ok RSA-PSS 2048\", args{kmsapi.SHA256WithRSAPSS, 2048}, &pb.CertificateAuthority_KeyVersionSpec{\n\t\t\tKeyVersion: &pb.CertificateAuthority_KeyVersionSpec_Algorithm{\n\t\t\t\tAlgorithm: pb.CertificateAuthority_RSA_PSS_2048_SHA256,\n\t\t\t}}, false},\n\t\t{\"ok RSA-PSS 3072\", args{kmsapi.SHA256WithRSAPSS, 3072}, &pb.CertificateAuthority_KeyVersionSpec{\n\t\t\tKeyVersion: &pb.CertificateAuthority_KeyVersionSpec_Algorithm{\n\t\t\t\tAlgorithm: pb.CertificateAuthority_RSA_PSS_3072_SHA256,\n\t\t\t}}, false},\n\t\t{\"ok RSA-PSS 4096\", args{kmsapi.SHA256WithRSAPSS, 4096}, &pb.CertificateAuthority_KeyVersionSpec{\n\t\t\tKeyVersion: &pb.CertificateAuthority_KeyVersionSpec_Algorithm{\n\t\t\t\tAlgorithm: pb.CertificateAuthority_RSA_PSS_4096_SHA256,\n\t\t\t}}, false},\n\t\t{\"fail Ed25519\", args{kmsapi.PureEd25519, 0}, nil, true},\n\t\t{\"fail RSA size\", args{kmsapi.SHA256WithRSA, 1024}, nil, true},\n\t\t{\"fail RSA-PSS size\", args{kmsapi.SHA256WithRSAPSS, 1024}, nil, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := createKeyVersionSpec(tt.args.alg, tt.args.bits)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"createKeyVersionSpec() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"createKeyVersionSpec() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "cas/cloudcas/cloudcas.go",
    "content": "package cloudcas\n\nimport (\n\t\"context\"\n\t\"crypto/rand\"\n\t\"crypto/x509\"\n\t\"encoding/asn1\"\n\t\"encoding/pem\"\n\t\"regexp\"\n\t\"strings\"\n\t\"time\"\n\n\tprivateca \"cloud.google.com/go/security/privateca/apiv1\"\n\tpb \"cloud.google.com/go/security/privateca/apiv1/privatecapb\"\n\t\"github.com/google/uuid\"\n\tgax \"github.com/googleapis/gax-go/v2\"\n\t\"github.com/pkg/errors\"\n\t\"google.golang.org/api/option\"\n\t\"google.golang.org/grpc/codes\"\n\t\"google.golang.org/grpc/status\"\n\tdurationpb \"google.golang.org/protobuf/types/known/durationpb\"\n\n\t\"go.step.sm/crypto/x509util\"\n\n\t\"github.com/smallstep/certificates/cas/apiv1\"\n)\n\nfunc init() {\n\tapiv1.Register(apiv1.CloudCAS, func(ctx context.Context, opts apiv1.Options) (apiv1.CertificateAuthorityService, error) {\n\t\treturn New(ctx, opts)\n\t})\n}\n\nvar now = time.Now\n\n// The actual regular expression that matches a certificate authority is:\n//\n//\t^projects/[a-z][a-z0-9-]{4,28}[a-z0-9]/locations/[a-z0-9-]+/caPools/[a-zA-Z0-9-_]+/certificateAuthorities/[a-zA-Z0-9-_]+$\n//\n// But we will allow a more flexible one to fail if this changes.\nvar caRegexp = regexp.MustCompile(\"^projects/[^/]+/locations/[^/]+/caPools/[^/]+/certificateAuthorities/[^/]+$\")\n\n// CertificateAuthorityClient is the interface implemented by the Google CAS\n// client.\ntype CertificateAuthorityClient interface {\n\tCreateCertificate(ctx context.Context, req *pb.CreateCertificateRequest, opts ...gax.CallOption) (*pb.Certificate, error)\n\tRevokeCertificate(ctx context.Context, req *pb.RevokeCertificateRequest, opts ...gax.CallOption) (*pb.Certificate, error)\n\tGetCertificateAuthority(ctx context.Context, req *pb.GetCertificateAuthorityRequest, opts ...gax.CallOption) (*pb.CertificateAuthority, error)\n\tCreateCertificateAuthority(ctx context.Context, req *pb.CreateCertificateAuthorityRequest, opts ...gax.CallOption) (*privateca.CreateCertificateAuthorityOperation, error)\n\tFetchCertificateAuthorityCsr(ctx context.Context, req *pb.FetchCertificateAuthorityCsrRequest, opts ...gax.CallOption) (*pb.FetchCertificateAuthorityCsrResponse, error)\n\tActivateCertificateAuthority(ctx context.Context, req *pb.ActivateCertificateAuthorityRequest, opts ...gax.CallOption) (*privateca.ActivateCertificateAuthorityOperation, error)\n\tEnableCertificateAuthority(ctx context.Context, req *pb.EnableCertificateAuthorityRequest, opts ...gax.CallOption) (*privateca.EnableCertificateAuthorityOperation, error)\n\tGetCaPool(ctx context.Context, req *pb.GetCaPoolRequest, opts ...gax.CallOption) (*pb.CaPool, error)\n\tCreateCaPool(ctx context.Context, req *pb.CreateCaPoolRequest, opts ...gax.CallOption) (*privateca.CreateCaPoolOperation, error)\n}\n\n// recocationCodeMap maps revocation reason codes from RFC 5280, to Google CAS\n// revocation reasons. Revocation reason 7 is not used, and revocation reason 8\n// (removeFromCRL) is not supported by Google CAS.\nvar revocationCodeMap = map[int]pb.RevocationReason{\n\t0:  pb.RevocationReason_REVOCATION_REASON_UNSPECIFIED,\n\t1:  pb.RevocationReason_KEY_COMPROMISE,\n\t2:  pb.RevocationReason_CERTIFICATE_AUTHORITY_COMPROMISE,\n\t3:  pb.RevocationReason_AFFILIATION_CHANGED,\n\t4:  pb.RevocationReason_SUPERSEDED,\n\t5:  pb.RevocationReason_CESSATION_OF_OPERATION,\n\t6:  pb.RevocationReason_CERTIFICATE_HOLD,\n\t9:  pb.RevocationReason_PRIVILEGE_WITHDRAWN,\n\t10: pb.RevocationReason_ATTRIBUTE_AUTHORITY_COMPROMISE,\n}\n\n// caPoolTierMap contains the map between apiv1.Options.Tier and the pb type.\nvar caPoolTierMap = map[string]pb.CaPool_Tier{\n\t\"\":           pb.CaPool_DEVOPS,\n\t\"ENTERPRISE\": pb.CaPool_ENTERPRISE,\n\t\"DEVOPS\":     pb.CaPool_DEVOPS,\n}\n\n// CloudCAS implements a Certificate Authority Service using Google Cloud CAS.\ntype CloudCAS struct {\n\tclient               CertificateAuthorityClient\n\tcertificateAuthority string\n\tproject              string\n\tlocation             string\n\tcaPool               string\n\tcaPoolTier           pb.CaPool_Tier\n\tgcsBucket            string\n}\n\n// newCertificateAuthorityClient creates the certificate authority client. This\n// function is used for testing purposes.\nvar newCertificateAuthorityClient = func(ctx context.Context, credentialsFile string) (CertificateAuthorityClient, error) {\n\tvar cloudOpts []option.ClientOption\n\tif credentialsFile != \"\" {\n\t\tcloudOpts = append(cloudOpts, option.WithAuthCredentialsFile(option.ServiceAccount, credentialsFile))\n\t}\n\tclient, err := privateca.NewCertificateAuthorityClient(ctx, cloudOpts...)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"error creating client\")\n\t}\n\treturn client, nil\n}\n\n// New creates a new CertificateAuthorityService implementation using Google\n// Cloud CAS.\nfunc New(ctx context.Context, opts apiv1.Options) (*CloudCAS, error) {\n\tvar caPoolTier pb.CaPool_Tier\n\tif opts.IsCreator && opts.CertificateAuthority == \"\" {\n\t\tswitch {\n\t\tcase opts.Project == \"\":\n\t\t\treturn nil, errors.New(\"cloudCAS 'project' cannot be empty\")\n\t\tcase opts.Location == \"\":\n\t\t\treturn nil, errors.New(\"cloudCAS 'location' cannot be empty\")\n\t\tcase opts.CaPool == \"\":\n\t\t\treturn nil, errors.New(\"cloudCAS 'caPool' cannot be empty\")\n\t\t}\n\t\tvar ok bool\n\t\tif caPoolTier, ok = caPoolTierMap[strings.ToUpper(opts.CaPoolTier)]; !ok {\n\t\t\treturn nil, errors.New(\"cloudCAS 'caPoolTier' is not a valid tier\")\n\t\t}\n\t} else {\n\t\tif opts.CertificateAuthority == \"\" {\n\t\t\treturn nil, errors.New(\"cloudCAS 'certificateAuthority' cannot be empty\")\n\t\t}\n\t\tif !caRegexp.MatchString(opts.CertificateAuthority) {\n\t\t\treturn nil, errors.New(\"cloudCAS 'certificateAuthority' is not valid certificate authority resource\")\n\t\t}\n\t\t// Extract project and location from CertificateAuthority\n\t\tif parts := strings.Split(opts.CertificateAuthority, \"/\"); len(parts) == 8 {\n\t\t\tif opts.Project == \"\" {\n\t\t\t\topts.Project = parts[1]\n\t\t\t}\n\t\t\tif opts.Location == \"\" {\n\t\t\t\topts.Location = parts[3]\n\t\t\t}\n\t\t\tif opts.CaPool == \"\" {\n\t\t\t\topts.CaPool = parts[5]\n\t\t\t}\n\t\t}\n\t}\n\n\tclient, err := newCertificateAuthorityClient(ctx, opts.CredentialsFile)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// GCSBucket is the the bucket name or empty for a managed bucket.\n\treturn &CloudCAS{\n\t\tclient:               client,\n\t\tcertificateAuthority: opts.CertificateAuthority,\n\t\tproject:              opts.Project,\n\t\tlocation:             opts.Location,\n\t\tcaPool:               opts.CaPool,\n\t\tgcsBucket:            opts.GCSBucket,\n\t\tcaPoolTier:           caPoolTier,\n\t}, nil\n}\n\n// Type returns the type of this CertificateAuthorityService.\nfunc (c *CloudCAS) Type() apiv1.Type {\n\treturn apiv1.CloudCAS\n}\n\n// GetCertificateAuthority returns the root certificate for the given\n// certificate authority. It implements apiv1.CertificateAuthorityGetter\n// interface.\nfunc (c *CloudCAS) GetCertificateAuthority(req *apiv1.GetCertificateAuthorityRequest) (*apiv1.GetCertificateAuthorityResponse, error) {\n\tname := req.Name\n\tif name == \"\" {\n\t\tname = c.certificateAuthority\n\t}\n\n\tctx, cancel := defaultContext()\n\tdefer cancel()\n\n\tresp, err := c.client.GetCertificateAuthority(ctx, &pb.GetCertificateAuthorityRequest{\n\t\tName: name,\n\t})\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"cloudCAS GetCertificateAuthority failed\")\n\t}\n\tif len(resp.PemCaCertificates) == 0 {\n\t\treturn nil, errors.New(\"cloudCAS GetCertificateAuthority: PemCACertificate should not be empty\")\n\t}\n\n\t// Parse intermediate certificates\n\tvar intermediates = make([]*x509.Certificate, len(resp.PemCaCertificates)-1)\n\tfor i := 0; i < len(resp.PemCaCertificates)-1; i++ {\n\t\tintermediate, err := parseCertificate(resp.PemCaCertificates[i])\n\t\tif err != nil {\n\t\t\treturn nil, errors.Wrap(err, \"parsing cloudCAS intermediates failed\")\n\t\t}\n\t\tintermediates[i] = intermediate\n\t}\n\n\t// Last certificate in the chain is the root\n\troot, err := parseCertificate(resp.PemCaCertificates[len(resp.PemCaCertificates)-1])\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"parsing cloudCAS root failed\")\n\t}\n\n\treturn &apiv1.GetCertificateAuthorityResponse{\n\t\tRootCertificate:          root,\n\t\tIntermediateCertificates: intermediates,\n\t}, nil\n}\n\n// CreateCertificate signs a new certificate using Google Cloud CAS.\nfunc (c *CloudCAS) CreateCertificate(req *apiv1.CreateCertificateRequest) (*apiv1.CreateCertificateResponse, error) {\n\tswitch {\n\tcase req.Template == nil:\n\t\treturn nil, errors.New(\"createCertificateRequest `template` cannot be nil\")\n\tcase req.Lifetime == 0:\n\t\treturn nil, errors.New(\"createCertificateRequest `lifetime` cannot be 0\")\n\t}\n\n\tcert, chain, err := c.createCertificate(req.Template, req.Lifetime, req.RequestID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &apiv1.CreateCertificateResponse{\n\t\tCertificate:      cert,\n\t\tCertificateChain: chain,\n\t}, nil\n}\n\n// RenewCertificate renews the given certificate using Google Cloud CAS.\n// Google's CAS does not support the renew operation, so this method uses\n// CreateCertificate.\nfunc (c *CloudCAS) RenewCertificate(req *apiv1.RenewCertificateRequest) (*apiv1.RenewCertificateResponse, error) {\n\tswitch {\n\tcase req.Template == nil:\n\t\treturn nil, errors.New(\"renewCertificateRequest `template` cannot be nil\")\n\tcase req.Lifetime == 0:\n\t\treturn nil, errors.New(\"renewCertificateRequest `lifetime` cannot be 0\")\n\t}\n\n\tcert, chain, err := c.createCertificate(req.Template, req.Lifetime, req.RequestID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &apiv1.RenewCertificateResponse{\n\t\tCertificate:      cert,\n\t\tCertificateChain: chain,\n\t}, nil\n}\n\n// RevokeCertificate revokes a certificate using Google Cloud CAS.\nfunc (c *CloudCAS) RevokeCertificate(req *apiv1.RevokeCertificateRequest) (*apiv1.RevokeCertificateResponse, error) {\n\treason, ok := revocationCodeMap[req.ReasonCode]\n\tswitch {\n\tcase !ok:\n\t\treturn nil, errors.Errorf(\"revokeCertificate 'reasonCode=%d' is invalid or not supported\", req.ReasonCode)\n\tcase req.Certificate == nil:\n\t\treturn nil, errors.New(\"revokeCertificateRequest `certificate` cannot be nil\")\n\t}\n\n\text, ok := apiv1.FindCertificateAuthorityExtension(req.Certificate)\n\tif !ok {\n\t\treturn nil, errors.New(\"error revoking certificate: certificate authority extension was not found\")\n\t}\n\n\tvar cae apiv1.CertificateAuthorityExtension\n\tif _, err := asn1.Unmarshal(ext.Value, &cae); err != nil {\n\t\treturn nil, errors.Wrap(err, \"error unmarshaling certificate authority extension\")\n\t}\n\n\tctx, cancel := defaultContext()\n\tdefer cancel()\n\n\tcertpb, err := c.client.RevokeCertificate(ctx, &pb.RevokeCertificateRequest{\n\t\tName:      c.certificateAuthority + \"/certificates/\" + cae.CertificateID,\n\t\tReason:    reason,\n\t\tRequestId: req.RequestID,\n\t})\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"cloudCAS RevokeCertificate failed\")\n\t}\n\n\tcert, chain, err := getCertificateAndChain(certpb)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &apiv1.RevokeCertificateResponse{\n\t\tCertificate:      cert,\n\t\tCertificateChain: chain,\n\t}, nil\n}\n\n// CreateCertificateAuthority creates a new root or intermediate certificate\n// using Google Cloud CAS.\nfunc (c *CloudCAS) CreateCertificateAuthority(req *apiv1.CreateCertificateAuthorityRequest) (*apiv1.CreateCertificateAuthorityResponse, error) {\n\tswitch {\n\tcase c.project == \"\":\n\t\treturn nil, errors.New(\"cloudCAS `project` cannot be empty\")\n\tcase c.location == \"\":\n\t\treturn nil, errors.New(\"cloudCAS `location` cannot be empty\")\n\tcase c.caPool == \"\":\n\t\treturn nil, errors.New(\"cloudCAS `caPool` cannot be empty\")\n\tcase c.caPoolTier == 0:\n\t\treturn nil, errors.New(\"cloudCAS `caPoolTier` cannot be empty\")\n\tcase req.Template == nil:\n\t\treturn nil, errors.New(\"createCertificateAuthorityRequest `template` cannot be nil\")\n\tcase req.Lifetime == 0:\n\t\treturn nil, errors.New(\"createCertificateAuthorityRequest `lifetime` cannot be 0\")\n\tcase req.Type == apiv1.IntermediateCA && req.Parent == nil:\n\t\treturn nil, errors.New(\"createCertificateAuthorityRequest `parent` cannot be nil\")\n\tcase req.Type == apiv1.IntermediateCA && req.Parent.Name == \"\" && (req.Parent.Certificate == nil || req.Parent.Signer == nil):\n\t\treturn nil, errors.New(\"createCertificateAuthorityRequest `parent.name` cannot be empty\")\n\t}\n\n\tvar caType pb.CertificateAuthority_Type\n\tswitch req.Type {\n\tcase apiv1.RootCA:\n\t\tcaType = pb.CertificateAuthority_SELF_SIGNED\n\tcase apiv1.IntermediateCA:\n\t\tcaType = pb.CertificateAuthority_SUBORDINATE\n\tdefault:\n\t\treturn nil, errors.Errorf(\"createCertificateAuthorityRequest `type=%d' is invalid or not supported\", req.Type)\n\t}\n\n\t// Select key and signature algorithm to use\n\tvar err error\n\tvar keySpec *pb.CertificateAuthority_KeyVersionSpec\n\tif req.CreateKey == nil {\n\t\tif keySpec, err = createKeyVersionSpec(0, 0); err != nil {\n\t\t\treturn nil, errors.Wrap(err, \"createCertificateAuthorityRequest `createKey` is not valid\")\n\t\t}\n\t} else {\n\t\tif keySpec, err = createKeyVersionSpec(req.CreateKey.SignatureAlgorithm, req.CreateKey.Bits); err != nil {\n\t\t\treturn nil, errors.Wrap(err, \"createCertificateAuthorityRequest `createKey` is not valid\")\n\t\t}\n\t}\n\n\t// Normalize or generate id.\n\tcaID := normalizeCertificateAuthorityName(req.Name)\n\tif caID == \"\" {\n\t\tid, err := createCertificateID()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tcaID = id\n\t}\n\n\t// Add CertificateAuthority extension\n\tcasExtension, err := apiv1.CreateCertificateAuthorityExtension(apiv1.CloudCAS, caID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treq.Template.ExtraExtensions = append(req.Template.ExtraExtensions, casExtension)\n\n\t// Create the caPool if necessary\n\tparent, err := c.createCaPoolIfNecessary()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Prepare CreateCertificateAuthorityRequest\n\tpbReq := &pb.CreateCertificateAuthorityRequest{\n\t\tParent:                 parent,\n\t\tCertificateAuthorityId: caID,\n\t\tRequestId:              req.RequestID,\n\t\tCertificateAuthority: &pb.CertificateAuthority{\n\t\t\tType: caType,\n\t\t\tConfig: &pb.CertificateConfig{\n\t\t\t\tSubjectConfig: &pb.CertificateConfig_SubjectConfig{\n\t\t\t\t\tSubject:        createSubject(req.Template),\n\t\t\t\t\tSubjectAltName: createSubjectAlternativeNames(req.Template),\n\t\t\t\t},\n\t\t\t\tX509Config: createX509Parameters(req.Template),\n\t\t\t},\n\t\t\tLifetime:  durationpb.New(req.Lifetime),\n\t\t\tKeySpec:   keySpec,\n\t\t\tGcsBucket: c.gcsBucket,\n\t\t\tLabels:    map[string]string{},\n\t\t},\n\t}\n\n\t// Create certificate authority.\n\tctx, cancel := defaultContext()\n\tdefer cancel()\n\n\tresp, err := c.client.CreateCertificateAuthority(ctx, pbReq)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"cloudCAS CreateCertificateAuthority failed\")\n\t}\n\n\t// Wait for the long-running operation.\n\tctx, cancel = defaultInitiatorContext()\n\tdefer cancel()\n\n\tca, err := resp.Wait(ctx)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"cloudCAS CreateCertificateAuthority failed\")\n\t}\n\n\t// Sign Intermediate CAs with the parent.\n\tif req.Type == apiv1.IntermediateCA {\n\t\tca, err = c.signIntermediateCA(parent, ca.Name, req)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\t// Enable Certificate Authority.\n\tca, err = c.enableCertificateAuthority(ca)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif len(ca.PemCaCertificates) == 0 {\n\t\treturn nil, errors.New(\"cloudCAS CreateCertificateAuthority failed: PemCaCertificates is empty\")\n\t}\n\n\tcert, err := parseCertificate(ca.PemCaCertificates[0])\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar chain []*x509.Certificate\n\tif pemChain := ca.PemCaCertificates[1:]; len(pemChain) > 0 {\n\t\tchain = make([]*x509.Certificate, len(pemChain))\n\t\tfor i, s := range pemChain {\n\t\t\tif chain[i], err = parseCertificate(s); err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\t}\n\n\treturn &apiv1.CreateCertificateAuthorityResponse{\n\t\tName:             ca.Name,\n\t\tCertificate:      cert,\n\t\tCertificateChain: chain,\n\t}, nil\n}\n\nfunc (c *CloudCAS) createCaPoolIfNecessary() (string, error) {\n\tctx, cancel := defaultContext()\n\tdefer cancel()\n\n\tpool, err := c.client.GetCaPool(ctx, &pb.GetCaPoolRequest{\n\t\tName: \"projects/\" + c.project + \"/locations/\" + c.location + \"/caPools/\" + c.caPool,\n\t})\n\tif err == nil {\n\t\treturn pool.Name, nil\n\t}\n\n\tif status.Code(err) != codes.NotFound {\n\t\treturn \"\", errors.Wrap(err, \"cloudCAS GetCaPool failed\")\n\t}\n\n\t// PublishCrl is only supported by the enterprise tier\n\tvar publishCrl bool\n\tif c.caPoolTier == pb.CaPool_ENTERPRISE {\n\t\tpublishCrl = true\n\t}\n\n\tctx, cancel = defaultContext()\n\tdefer cancel()\n\n\top, err := c.client.CreateCaPool(ctx, &pb.CreateCaPoolRequest{\n\t\tParent:   \"projects/\" + c.project + \"/locations/\" + c.location,\n\t\tCaPoolId: c.caPool,\n\t\tCaPool: &pb.CaPool{\n\t\t\tTier:           c.caPoolTier,\n\t\t\tIssuancePolicy: nil,\n\t\t\tPublishingOptions: &pb.CaPool_PublishingOptions{\n\t\t\t\tPublishCaCert: true,\n\t\t\t\tPublishCrl:    publishCrl,\n\t\t\t},\n\t\t},\n\t})\n\tif err != nil {\n\t\treturn \"\", errors.Wrap(err, \"cloudCAS CreateCaPool failed\")\n\t}\n\n\tctx, cancel = defaultInitiatorContext()\n\tdefer cancel()\n\n\tpool, err = op.Wait(ctx)\n\tif err != nil {\n\t\treturn \"\", errors.Wrap(err, \"cloudCAS CreateCaPool failed\")\n\t}\n\n\treturn pool.Name, nil\n}\n\nfunc (c *CloudCAS) enableCertificateAuthority(ca *pb.CertificateAuthority) (*pb.CertificateAuthority, error) {\n\tif ca.State == pb.CertificateAuthority_ENABLED {\n\t\treturn ca, nil\n\t}\n\n\tctx, cancel := defaultContext()\n\tdefer cancel()\n\n\tresp, err := c.client.EnableCertificateAuthority(ctx, &pb.EnableCertificateAuthorityRequest{\n\t\tName: ca.Name,\n\t})\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"cloudCAS EnableCertificateAuthority failed\")\n\t}\n\n\tctx, cancel = defaultInitiatorContext()\n\tdefer cancel()\n\n\tca, err = resp.Wait(ctx)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"cloudCAS EnableCertificateAuthority failed\")\n\t}\n\n\treturn ca, nil\n}\n\nfunc (c *CloudCAS) createCertificate(tpl *x509.Certificate, lifetime time.Duration, requestID string) (*x509.Certificate, []*x509.Certificate, error) {\n\t// Removes the CAS extension if it exists.\n\tapiv1.RemoveCertificateAuthorityExtension(tpl)\n\n\t// Create new CAS extension with the certificate id.\n\tid, err := createCertificateID()\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\tcasExtension, err := apiv1.CreateCertificateAuthorityExtension(apiv1.CloudCAS, id)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\ttpl.ExtraExtensions = append(tpl.ExtraExtensions, casExtension)\n\n\t// Create and submit certificate\n\tcertConfig, err := createCertificateConfig(tpl)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\n\tctx, cancel := defaultContext()\n\tdefer cancel()\n\n\tcert, err := c.client.CreateCertificate(ctx, &pb.CreateCertificateRequest{\n\t\tParent:        \"projects/\" + c.project + \"/locations/\" + c.location + \"/caPools/\" + c.caPool,\n\t\tCertificateId: id,\n\t\tCertificate: &pb.Certificate{\n\t\t\tCertificateConfig: certConfig,\n\t\t\tLifetime:          durationpb.New(lifetime),\n\t\t\tLabels:            map[string]string{},\n\t\t},\n\t\tIssuingCertificateAuthorityId: getResourceName(c.certificateAuthority),\n\t\tRequestId:                     requestID,\n\t})\n\tif err != nil {\n\t\treturn nil, nil, errors.Wrap(err, \"cloudCAS CreateCertificate failed\")\n\t}\n\n\t// Return certificate and certificate chain\n\treturn getCertificateAndChain(cert)\n}\n\nfunc (c *CloudCAS) signIntermediateCA(parent, name string, req *apiv1.CreateCertificateAuthorityRequest) (*pb.CertificateAuthority, error) {\n\tid, err := createCertificateID()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Fetch intermediate CSR\n\tctx, cancel := defaultInitiatorContext()\n\tdefer cancel()\n\n\tcsr, err := c.client.FetchCertificateAuthorityCsr(ctx, &pb.FetchCertificateAuthorityCsrRequest{\n\t\tName: name,\n\t})\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"cloudCAS FetchCertificateAuthorityCsr failed\")\n\t}\n\n\t// Sign the CSR with the ca.\n\tvar cert *pb.Certificate\n\tif req.Parent.Certificate != nil && req.Parent.Signer != nil {\n\t\t// Using a local certificate and key.\n\t\tcr, err := parseCertificateRequest(csr.PemCsr)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\ttemplate, err := x509util.CreateCertificateTemplate(cr)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tt := now()\n\t\ttemplate.NotBefore = t.Add(-1 * req.Backdate)\n\t\ttemplate.NotAfter = t.Add(req.Lifetime)\n\n\t\t// Sign certificate\n\t\tcrt, err := x509util.CreateCertificate(template, req.Parent.Certificate, template.PublicKey, req.Parent.Signer)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\t// Build pb.Certificate for activaion\n\t\tchain := []string{\n\t\t\tencodeCertificate(req.Parent.Certificate),\n\t\t}\n\t\tfor _, c := range req.Parent.CertificateChain {\n\t\t\tchain = append(chain, encodeCertificate(c))\n\t\t}\n\t\tcert = &pb.Certificate{\n\t\t\tPemCertificate:      encodeCertificate(crt),\n\t\t\tPemCertificateChain: chain,\n\t\t}\n\t} else {\n\t\t// Using the parent in CloudCAS.\n\t\tctx, cancel = defaultInitiatorContext()\n\t\tdefer cancel()\n\n\t\tcert, err = c.client.CreateCertificate(ctx, &pb.CreateCertificateRequest{\n\t\t\tParent:        parent,\n\t\t\tCertificateId: id,\n\t\t\tCertificate: &pb.Certificate{\n\t\t\t\tCertificateConfig: &pb.Certificate_PemCsr{\n\t\t\t\t\tPemCsr: csr.PemCsr,\n\t\t\t\t},\n\t\t\t\tLifetime: durationpb.New(req.Lifetime),\n\t\t\t\tLabels:   map[string]string{},\n\t\t\t},\n\t\t\tIssuingCertificateAuthorityId: getResourceName(req.Parent.Name),\n\t\t\tRequestId:                     req.RequestID,\n\t\t})\n\t\tif err != nil {\n\t\t\treturn nil, errors.Wrap(err, \"cloudCAS CreateCertificate failed\")\n\t\t}\n\t}\n\n\t// Activate the intermediate certificate.\n\tctx, cancel = defaultInitiatorContext()\n\tdefer cancel()\n\tresp, err := c.client.ActivateCertificateAuthority(ctx, &pb.ActivateCertificateAuthorityRequest{\n\t\tName:             name,\n\t\tPemCaCertificate: cert.PemCertificate,\n\t\tSubordinateConfig: &pb.SubordinateConfig{\n\t\t\tSubordinateConfig: &pb.SubordinateConfig_PemIssuerChain{\n\t\t\t\tPemIssuerChain: &pb.SubordinateConfig_SubordinateConfigChain{\n\t\t\t\t\tPemCertificates: cert.PemCertificateChain,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tRequestId: req.RequestID,\n\t})\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"cloudCAS ActivateCertificateAuthority1 failed\")\n\t}\n\n\t// Wait for the long-running operation.\n\tctx, cancel = defaultInitiatorContext()\n\tdefer cancel()\n\n\tca, err := resp.Wait(ctx)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"cloudCAS ActivateCertificateAuthority failed\")\n\t}\n\n\treturn ca, nil\n}\n\nfunc defaultContext() (context.Context, context.CancelFunc) {\n\treturn context.WithTimeout(context.Background(), 15*time.Second)\n}\n\nfunc defaultInitiatorContext() (context.Context, context.CancelFunc) {\n\treturn context.WithTimeout(context.Background(), 60*time.Second)\n}\n\nfunc createCertificateID() (string, error) {\n\tid, err := uuid.NewRandomFromReader(rand.Reader)\n\tif err != nil {\n\t\treturn \"\", errors.Wrap(err, \"error creating certificate id\")\n\t}\n\treturn id.String(), nil\n}\n\nfunc parseCertificate(pemCert string) (*x509.Certificate, error) {\n\tblock, _ := pem.Decode([]byte(pemCert))\n\tif block == nil {\n\t\treturn nil, errors.New(\"error decoding certificate: not a valid PEM encoded block\")\n\t}\n\tcert, err := x509.ParseCertificate(block.Bytes)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"error parsing certificate\")\n\t}\n\treturn cert, nil\n}\n\nfunc parseCertificateRequest(pemCsr string) (*x509.CertificateRequest, error) {\n\tblock, _ := pem.Decode([]byte(pemCsr))\n\tif block == nil {\n\t\treturn nil, errors.New(\"error decoding certificate request: not a valid PEM encoded block\")\n\t}\n\tcr, err := x509.ParseCertificateRequest(block.Bytes)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"error parsing certificate request\")\n\t}\n\treturn cr, nil\n}\n\nfunc encodeCertificate(cert *x509.Certificate) string {\n\treturn string(pem.EncodeToMemory(&pem.Block{\n\t\tType:  \"CERTIFICATE\",\n\t\tBytes: cert.Raw,\n\t}))\n}\n\nfunc getCertificateAndChain(certpb *pb.Certificate) (*x509.Certificate, []*x509.Certificate, error) {\n\tcert, err := parseCertificate(certpb.PemCertificate)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\n\tpemChain := certpb.PemCertificateChain[:len(certpb.PemCertificateChain)-1]\n\tchain := make([]*x509.Certificate, len(pemChain))\n\tfor i := range pemChain {\n\t\tchain[i], err = parseCertificate(pemChain[i])\n\t\tif err != nil {\n\t\t\treturn nil, nil, err\n\t\t}\n\t}\n\n\treturn cert, chain, nil\n}\n\n// getResourceName returns the last part of a resource.\nfunc getResourceName(name string) string {\n\tparts := strings.Split(name, \"/\")\n\treturn parts[len(parts)-1]\n}\n\n// Normalize a certificate authority name to comply with [a-zA-Z0-9-_].\nfunc normalizeCertificateAuthorityName(name string) string {\n\treturn strings.Map(func(r rune) rune {\n\t\tswitch {\n\t\tcase r >= 'a' && r <= 'z':\n\t\t\treturn r\n\t\tcase r >= 'A' && r <= 'Z':\n\t\t\treturn r\n\t\tcase r >= '0' && r <= '9':\n\t\t\treturn r\n\t\tcase r == '-':\n\t\t\treturn r\n\t\tcase r == '_':\n\t\t\treturn r\n\t\tdefault:\n\t\t\treturn '-'\n\t\t}\n\t}, name)\n}\n"
  },
  {
    "path": "cas/cloudcas/cloudcas_test.go",
    "content": "//go:generate go run go.uber.org/mock/mockgen -package cloudcas -mock_names=CertificateAuthorityClient=MockCertificateAuthorityClient -destination mock_client_test.go github.com/smallstep/certificates/cas/cloudcas CertificateAuthorityClient\n//go:generate go run go.uber.org/mock/mockgen -package cloudcas -mock_names=OperationsServer=MockOperationsServer -destination mock_operation_server_test.go cloud.google.com/go/longrunning/autogen/longrunningpb OperationsServer\n\npackage cloudcas\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto\"\n\t\"crypto/ecdsa\"\n\t\"crypto/ed25519\"\n\t\"crypto/rand\"\n\t\"crypto/x509\"\n\t\"encoding/asn1\"\n\t\"encoding/pem\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"reflect\"\n\t\"testing\"\n\t\"time\"\n\n\tlroauto \"cloud.google.com/go/longrunning/autogen\"\n\t\"cloud.google.com/go/longrunning/autogen/longrunningpb\"\n\tprivateca \"cloud.google.com/go/security/privateca/apiv1\"\n\tpb \"cloud.google.com/go/security/privateca/apiv1/privatecapb\"\n\t\"github.com/google/uuid\"\n\t\"github.com/googleapis/gax-go/v2\"\n\t\"github.com/pkg/errors\"\n\t\"go.uber.org/mock/gomock\"\n\t\"google.golang.org/api/option\"\n\t\"google.golang.org/grpc\"\n\t\"google.golang.org/grpc/codes\"\n\t\"google.golang.org/grpc/credentials/insecure\"\n\t\"google.golang.org/grpc/status\"\n\t\"google.golang.org/grpc/test/bufconn\"\n\t\"google.golang.org/protobuf/types/known/anypb\"\n\n\tkmsapi \"go.step.sm/crypto/kms/apiv1\"\n\n\t\"github.com/smallstep/certificates/cas/apiv1\"\n)\n\nvar (\n\terrTest             = errors.New(\"test error\")\n\ttestCaPoolName      = \"projects/test-project/locations/us-west1/caPools/test-capool\"\n\ttestAuthorityName   = \"projects/test-project/locations/us-west1/caPools/test-capool/certificateAuthorities/test-ca\"\n\ttestCertificateName = \"projects/test-project/locations/us-west1/caPools/test-capool/certificateAuthorities/test-ca/certificates/test-certificate\"\n\ttestProject         = \"test-project\"\n\ttestLocation        = \"us-west1\"\n\ttestCaPool          = \"test-capool\"\n\ttestRootCertificate = `-----BEGIN CERTIFICATE-----\nMIIBeDCCAR+gAwIBAgIQcXWWjtSZ/PAyH8D1Ou4L9jAKBggqhkjOPQQDAjAbMRkw\nFwYDVQQDExBDbG91ZENBUyBSb290IENBMB4XDTIwMTAyNzIyNTM1NFoXDTMwMTAy\nNzIyNTM1NFowGzEZMBcGA1UEAxMQQ2xvdWRDQVMgUm9vdCBDQTBZMBMGByqGSM49\nAgEGCCqGSM49AwEHA0IABIySHA4b78Yu4LuGhZIlv/PhNwXz4ZoV1OUZQ0LrK3vj\nB13O12DLZC5uj1z3kxdQzXUttSbtRv49clMpBiTpsZKjRTBDMA4GA1UdDwEB/wQE\nAwIBBjASBgNVHRMBAf8ECDAGAQH/AgEBMB0GA1UdDgQWBBSZ+t9RMHbFTl5BatM3\n5bJlHPOu3DAKBggqhkjOPQQDAgNHADBEAiASah6gg0tVM3WI0meCQ4SEKk7Mjhbv\n+SmhuZHWV1QlXQIgRXNyWcpVUrAoG6Uy1KQg07LDpF5dFeK9InrDxSJAkVo=\n-----END CERTIFICATE-----`\n\ttestIntermediateCertificate = `-----BEGIN CERTIFICATE-----\nMIIBpDCCAUmgAwIBAgIRALLKxnxyl0GBeKevIcbx02wwCgYIKoZIzj0EAwIwGzEZ\nMBcGA1UEAxMQQ2xvdWRDQVMgUm9vdCBDQTAeFw0yMDEwMjcyMjUzNTRaFw0zMDEw\nMjcyMjUzNTRaMCMxITAfBgNVBAMTGENsb3VkQ0FTIEludGVybWVkaWF0ZSBDQTBZ\nMBMGByqGSM49AgEGCCqGSM49AwEHA0IABPLuqxgBY+QmaXc8zKIC8FMgjJ6dF/cL\nb+Dig0XKc5GH/T1ORrhgOkRayrQcjPMu+jkjg25qn6vvp43LRtUKPXOjZjBkMA4G\nA1UdDwEB/wQEAwIBBjASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBQ8RVQI\nVgXAmRNDX8qItalVpSBEGjAfBgNVHSMEGDAWgBSZ+t9RMHbFTl5BatM35bJlHPOu\n3DAKBggqhkjOPQQDAgNJADBGAiEA70MVYVqjm8SBHJf5cOlWfiXXOfHUsctTJ+/F\npLsKBogCIQDJJkoQqYl9B59Dq3zydl8bpJevQxsoaa4Wqg+ZBMkvbQ==\n-----END CERTIFICATE-----`\n\ttestLeafCertificate = `-----BEGIN CERTIFICATE-----\nMIIB1jCCAX2gAwIBAgIQQfOn+COMeuD8VYF1TiDkEzAKBggqhkjOPQQDAjAqMSgw\nJgYDVQQDEx9Hb29nbGUgQ0FTIFRlc3QgSW50ZXJtZWRpYXRlIENBMB4XDTIwMDkx\nNDIyNTE1NVoXDTMwMDkxMjIyNTE1MlowHTEbMBkGA1UEAxMSdGVzdC5zbWFsbHN0\nZXAuY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEAdUSRBrpgHFilN4eaGlN\nnX2+xfjXa1Iwk2/+AensjFTXJi1UAIB0e+4pqi7Sen5E2QVBhntEHCrA3xOf7czg\nP6OBkTCBjjAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsG\nAQUFBwMCMB0GA1UdDgQWBBSYPbu4Tmm7Zze/hCePeZH1Avoj+jAfBgNVHSMEGDAW\ngBRIOVqyLDSlErJLuWWEvRm5UU1r1TAdBgNVHREEFjAUghJ0ZXN0LnNtYWxsc3Rl\ncC5jb20wCgYIKoZIzj0EAwIDRwAwRAIgY+nTc+RHn31/BOhht4JpxCmJPHxqFT3S\nojnictBudV0CIB87ipY5HV3c8FLVEzTA0wFwdDZvQraQYsthwbg2kQFb\n-----END CERTIFICATE-----`\n\ttestSignedCertificate = `-----BEGIN CERTIFICATE-----\nMIIB/DCCAaKgAwIBAgIQHHFuGMz0cClfde5kqP5prTAKBggqhkjOPQQDAjAqMSgw\nJgYDVQQDEx9Hb29nbGUgQ0FTIFRlc3QgSW50ZXJtZWRpYXRlIENBMB4XDTIwMDkx\nNTAwMDQ0M1oXDTMwMDkxMzAwMDQ0MFowHTEbMBkGA1UEAxMSdGVzdC5zbWFsbHN0\nZXAuY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEMqNCiXMvbn74LsHzRv+8\n17m9vEzH6RHrg3m82e0uEc36+fZWV/zJ9SKuONmnl5VP79LsjL5SVH0RDj73U2XO\nDKOBtjCBszAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsG\nAQUFBwMCMB0GA1UdDgQWBBRTA2cTs7PCNjnps/+T0dS8diqv0DAfBgNVHSMEGDAW\ngBRIOVqyLDSlErJLuWWEvRm5UU1r1TBCBgwrBgEEAYKkZMYoQAIEMjAwEwhjbG91\nZGNhcxMkZDhkMThhNjgtNTI5Ni00YWYzLWFlNGItMmY4NzdkYTNmYmQ5MAoGCCqG\nSM49BAMCA0gAMEUCIGxl+pqJ50WYWUqK2l4V1FHoXSi0Nht5kwTxFxnWZu1xAiEA\nzemu3bhWLFaGg3s8i+HTEhw4RqkHP74vF7AVYp88bAw=\n-----END CERTIFICATE-----`\n\ttestIntermediateCsr = `-----BEGIN CERTIFICATE REQUEST-----\nMIHeMIGFAgEAMCMxITAfBgNVBAMTGENsb3VkQ0FTIEludGVybWVkaWF0ZSBDQTBZ\nMBMGByqGSM49AgEGCCqGSM49AwEHA0IABPLuqxgBY+QmaXc8zKIC8FMgjJ6dF/cL\nb+Dig0XKc5GH/T1ORrhgOkRayrQcjPMu+jkjg25qn6vvp43LRtUKPXOgADAKBggq\nhkjOPQQDAgNIADBFAiEAn3pkYXb2OzoQZ+AExFqd7qZ7pg2nyP2kBZZ01Pl8KfcC\nIHKplBXDR79/i7kjOtv1iWfgf5S/XQHrz178gXA0YQe7\n-----END CERTIFICATE REQUEST-----`\n\ttestRootKey = `-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIN51Rgg6YcQVLeCRzumdw4pjM3VWqFIdCbnsV3Up1e/goAoGCCqGSM49\nAwEHoUQDQgAEjJIcDhvvxi7gu4aFkiW/8+E3BfPhmhXU5RlDQusre+MHXc7XYMtk\nLm6PXPeTF1DNdS21Ju1G/j1yUykGJOmxkg==\n-----END EC PRIVATE KEY-----`\n\t//nolint:unused,gocritic,varcheck\n\ttestIntermediateKey = `-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIMMX/XkXGnRDD4fYu7Z4rHACdJn/iyOy2UTwsv+oZ0C+oAoGCCqGSM49\nAwEHoUQDQgAE8u6rGAFj5CZpdzzMogLwUyCMnp0X9wtv4OKDRcpzkYf9PU5GuGA6\nRFrKtByM8y76OSODbmqfq++njctG1Qo9cw==\n-----END EC PRIVATE KEY-----`\n)\n\ntype testClient struct {\n\tcredentialsFile      string\n\tcertificate          *pb.Certificate\n\tcertificateAuthority *pb.CertificateAuthority\n\terr                  error\n}\n\nfunc newTestClient(credentialsFile string) (CertificateAuthorityClient, error) {\n\tif credentialsFile == \"testdata/error.json\" {\n\t\treturn nil, errTest\n\t}\n\treturn &testClient{\n\t\tcredentialsFile: credentialsFile,\n\t}, nil\n}\n\nfunc okTestClient() *testClient {\n\treturn &testClient{\n\t\tcredentialsFile: \"testdata/credentials.json\",\n\t\tcertificate: &pb.Certificate{\n\t\t\tName:                testCertificateName,\n\t\t\tPemCertificate:      testSignedCertificate,\n\t\t\tPemCertificateChain: []string{testIntermediateCertificate, testRootCertificate},\n\t\t},\n\t\tcertificateAuthority: &pb.CertificateAuthority{\n\t\t\tPemCaCertificates: []string{testIntermediateCertificate, testRootCertificate},\n\t\t},\n\t}\n}\n\nfunc okTestClientRootOnly() *testClient {\n\treturn &testClient{\n\t\tcredentialsFile: \"testdata/credentials.json\",\n\t\tcertificate: &pb.Certificate{\n\t\t\tName:                testCertificateName,\n\t\t\tPemCertificate:      testSignedCertificate,\n\t\t\tPemCertificateChain: []string{testRootCertificate},\n\t\t},\n\t\tcertificateAuthority: &pb.CertificateAuthority{\n\t\t\tPemCaCertificates: []string{testRootCertificate},\n\t\t},\n\t}\n}\n\nfunc okTestClientWithMultipleIntermediates() *testClient {\n\treturn &testClient{\n\t\tcredentialsFile: \"testdata/credentials.json\",\n\t\tcertificate: &pb.Certificate{\n\t\t\tName:                testCertificateName,\n\t\t\tPemCertificate:      testSignedCertificate,\n\t\t\tPemCertificateChain: []string{testIntermediateCertificate, testIntermediateCertificate, testIntermediateCertificate, testRootCertificate},\n\t\t},\n\t\tcertificateAuthority: &pb.CertificateAuthority{\n\t\t\tPemCaCertificates: []string{testIntermediateCertificate, testIntermediateCertificate, testIntermediateCertificate, testRootCertificate},\n\t\t},\n\t}\n}\n\nfunc failTestClient() *testClient {\n\treturn &testClient{\n\t\tcredentialsFile: \"testdata/credentials.json\",\n\t\terr:             errTest,\n\t}\n}\n\nfunc badRootTestClient() *testClient {\n\treturn &testClient{\n\t\tcredentialsFile: \"testdata/credentials.json\",\n\t\tcertificate: &pb.Certificate{\n\t\t\tName:                testCertificateName,\n\t\t\tPemCertificate:      \"not a pem cert\",\n\t\t\tPemCertificateChain: []string{testIntermediateCertificate, testRootCertificate},\n\t\t},\n\t\tcertificateAuthority: &pb.CertificateAuthority{\n\t\t\tPemCaCertificates: []string{testIntermediateCertificate, \"not a pem cert\"},\n\t\t},\n\t}\n}\n\nfunc badIntermediateTestClient() *testClient {\n\treturn &testClient{\n\t\tcredentialsFile: \"testdata/credentials.json\",\n\t\tcertificate: &pb.Certificate{\n\t\t\tName:                testCertificateName,\n\t\t\tPemCertificate:      \"this is not a pem\",\n\t\t\tPemCertificateChain: []string{testIntermediateCertificate, testRootCertificate},\n\t\t},\n\t\tcertificateAuthority: &pb.CertificateAuthority{\n\t\t\tPemCaCertificates: []string{\"this intermediate is not a pem\", testRootCertificate},\n\t\t},\n\t}\n}\n\nfunc setTeeReader(t *testing.T, w *bytes.Buffer) {\n\tt.Helper()\n\treader := rand.Reader\n\tt.Cleanup(func() {\n\t\trand.Reader = reader\n\t})\n\trand.Reader = io.TeeReader(reader, w)\n}\n\ntype badSigner struct {\n\tpub crypto.PublicKey\n}\n\nfunc createBadSigner(t *testing.T) *badSigner {\n\tt.Helper()\n\tpub, _, err := ed25519.GenerateKey(rand.Reader)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn &badSigner{\n\t\tpub: pub,\n\t}\n}\n\nfunc (b *badSigner) Public() crypto.PublicKey {\n\treturn b.pub\n}\n\nfunc (b *badSigner) Sign(io.Reader, []byte, crypto.SignerOpts) ([]byte, error) {\n\treturn nil, fmt.Errorf(\"💥\")\n}\n\nfunc (c *testClient) CreateCertificate(context.Context, *pb.CreateCertificateRequest, ...gax.CallOption) (*pb.Certificate, error) {\n\treturn c.certificate, c.err\n}\n\nfunc (c *testClient) RevokeCertificate(context.Context, *pb.RevokeCertificateRequest, ...gax.CallOption) (*pb.Certificate, error) {\n\treturn c.certificate, c.err\n}\n\nfunc (c *testClient) GetCertificateAuthority(context.Context, *pb.GetCertificateAuthorityRequest, ...gax.CallOption) (*pb.CertificateAuthority, error) {\n\treturn c.certificateAuthority, c.err\n}\n\nfunc (c *testClient) CreateCertificateAuthority(context.Context, *pb.CreateCertificateAuthorityRequest, ...gax.CallOption) (*privateca.CreateCertificateAuthorityOperation, error) {\n\treturn nil, errors.New(\"use NewMockCertificateAuthorityClient\")\n}\n\nfunc (c *testClient) FetchCertificateAuthorityCsr(context.Context, *pb.FetchCertificateAuthorityCsrRequest, ...gax.CallOption) (*pb.FetchCertificateAuthorityCsrResponse, error) {\n\treturn nil, errors.New(\"use NewMockCertificateAuthorityClient\")\n}\n\nfunc (c *testClient) ActivateCertificateAuthority(context.Context, *pb.ActivateCertificateAuthorityRequest, ...gax.CallOption) (*privateca.ActivateCertificateAuthorityOperation, error) {\n\treturn nil, errors.New(\"use NewMockCertificateAuthorityClient\")\n}\n\nfunc (c *testClient) EnableCertificateAuthority(context.Context, *pb.EnableCertificateAuthorityRequest, ...gax.CallOption) (*privateca.EnableCertificateAuthorityOperation, error) {\n\treturn nil, errors.New(\"use NewMockCertificateAuthorityClient\")\n}\n\nfunc (c *testClient) GetCaPool(context.Context, *pb.GetCaPoolRequest, ...gax.CallOption) (*pb.CaPool, error) {\n\treturn nil, errors.New(\"use NewMockCertificateAuthorityClient\")\n}\n\nfunc (c *testClient) CreateCaPool(context.Context, *pb.CreateCaPoolRequest, ...gax.CallOption) (*privateca.CreateCaPoolOperation, error) {\n\treturn nil, errors.New(\"use NewMockCertificateAuthorityClient\")\n}\n\nfunc mustParseCertificate(t *testing.T, pemCert string) *x509.Certificate {\n\tt.Helper()\n\tcrt, err := parseCertificate(pemCert)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn crt\n}\n\nfunc mustParseECKey(t *testing.T, pemKey string) *ecdsa.PrivateKey {\n\tt.Helper()\n\tblock, _ := pem.Decode([]byte(pemKey))\n\tif block == nil {\n\t\tt.Fatal(\"failed to parse key\")\n\t\treturn nil\n\t}\n\tkey, err := x509.ParseECPrivateKey(block.Bytes)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn key\n}\n\nfunc TestNew(t *testing.T) {\n\ttmp := newCertificateAuthorityClient\n\tnewCertificateAuthorityClient = func(ctx context.Context, credentialsFile string) (CertificateAuthorityClient, error) {\n\t\treturn newTestClient(credentialsFile)\n\t}\n\tt.Cleanup(func() {\n\t\tnewCertificateAuthorityClient = tmp\n\t})\n\n\ttype args struct {\n\t\tctx  context.Context\n\t\topts apiv1.Options\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\targs    args\n\t\twant    *CloudCAS\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", args{context.Background(), apiv1.Options{\n\t\t\tCertificateAuthority: testAuthorityName,\n\t\t}}, &CloudCAS{\n\t\t\tclient:               &testClient{},\n\t\t\tcertificateAuthority: testAuthorityName,\n\t\t\tproject:              testProject,\n\t\t\tlocation:             testLocation,\n\t\t\tcaPool:               testCaPool,\n\t\t\tcaPoolTier:           0,\n\t\t}, false},\n\t\t{\"ok authority and creator\", args{context.Background(), apiv1.Options{\n\t\t\tCertificateAuthority: testAuthorityName, IsCreator: true,\n\t\t}}, &CloudCAS{\n\t\t\tclient:               &testClient{},\n\t\t\tcertificateAuthority: testAuthorityName,\n\t\t\tproject:              testProject,\n\t\t\tlocation:             testLocation,\n\t\t\tcaPool:               testCaPool,\n\t\t\tcaPoolTier:           0,\n\t\t}, false},\n\t\t{\"ok with credentials\", args{context.Background(), apiv1.Options{\n\t\t\tCertificateAuthority: testAuthorityName, CredentialsFile: \"testdata/credentials.json\",\n\t\t}}, &CloudCAS{\n\t\t\tclient:               &testClient{credentialsFile: \"testdata/credentials.json\"},\n\t\t\tcertificateAuthority: testAuthorityName,\n\t\t\tproject:              testProject,\n\t\t\tlocation:             testLocation,\n\t\t\tcaPool:               testCaPool,\n\t\t\tcaPoolTier:           0,\n\t\t}, false},\n\t\t{\"ok creator\", args{context.Background(), apiv1.Options{\n\t\t\tIsCreator: true, Project: testProject, Location: testLocation, CaPool: testCaPool,\n\t\t}}, &CloudCAS{\n\t\t\tclient:     &testClient{},\n\t\t\tproject:    testProject,\n\t\t\tlocation:   testLocation,\n\t\t\tcaPool:     testCaPool,\n\t\t\tcaPoolTier: pb.CaPool_DEVOPS,\n\t\t}, false},\n\t\t{\"ok creator devops\", args{context.Background(), apiv1.Options{\n\t\t\tIsCreator: true, Project: testProject, Location: testLocation, CaPool: testCaPool, CaPoolTier: \"DevOps\",\n\t\t}}, &CloudCAS{\n\t\t\tclient:     &testClient{},\n\t\t\tproject:    testProject,\n\t\t\tlocation:   testLocation,\n\t\t\tcaPool:     testCaPool,\n\t\t\tcaPoolTier: pb.CaPool_DEVOPS,\n\t\t}, false},\n\t\t{\"ok creator enterprise\", args{context.Background(), apiv1.Options{\n\t\t\tIsCreator: true, Project: testProject, Location: testLocation, CaPool: testCaPool, CaPoolTier: \"ENTERPRISE\",\n\t\t}}, &CloudCAS{\n\t\t\tclient:     &testClient{},\n\t\t\tproject:    testProject,\n\t\t\tlocation:   testLocation,\n\t\t\tcaPool:     testCaPool,\n\t\t\tcaPoolTier: pb.CaPool_ENTERPRISE,\n\t\t}, false},\n\t\t{\"fail certificate authority\", args{context.Background(), apiv1.Options{\n\t\t\tCertificateAuthority: \"projects/ok1234/locations/ok1234/caPools/ok1234/certificateAuthorities/ok1234/bad\",\n\t\t}}, nil, true},\n\t\t{\"fail certificate authority regex\", args{context.Background(), apiv1.Options{}}, nil, true},\n\t\t{\"fail with credentials\", args{context.Background(), apiv1.Options{\n\t\t\tCertificateAuthority: testAuthorityName, CredentialsFile: \"testdata/error.json\",\n\t\t}}, nil, true},\n\t\t{\"fail creator project\", args{context.Background(), apiv1.Options{\n\t\t\tIsCreator: true, Project: \"\", Location: testLocation,\n\t\t}}, nil, true},\n\t\t{\"fail creator location\", args{context.Background(), apiv1.Options{\n\t\t\tIsCreator: true, Project: testProject, Location: \"\",\n\t\t}}, nil, true},\n\t\t{\"fail caPool\", args{context.Background(), apiv1.Options{\n\t\t\tIsCreator: true, Project: testProject, Location: testLocation, CaPool: \"\",\n\t\t}}, nil, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := New(tt.args.ctx, tt.args.opts)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"New() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"New() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNew_register(t *testing.T) {\n\ttmp := newCertificateAuthorityClient\n\tnewCertificateAuthorityClient = func(ctx context.Context, credentialsFile string) (CertificateAuthorityClient, error) {\n\t\treturn newTestClient(credentialsFile)\n\t}\n\tt.Cleanup(func() {\n\t\tnewCertificateAuthorityClient = tmp\n\t})\n\n\twant := &CloudCAS{\n\t\tclient:               &testClient{credentialsFile: \"testdata/credentials.json\"},\n\t\tcertificateAuthority: testAuthorityName,\n\t\tproject:              testProject,\n\t\tlocation:             testLocation,\n\t\tcaPool:               testCaPool,\n\t}\n\n\tnewFn, ok := apiv1.LoadCertificateAuthorityServiceNewFunc(apiv1.CloudCAS)\n\tif !ok {\n\t\tt.Error(\"apiv1.LoadCertificateAuthorityServiceNewFunc(apiv1.CloudCAS) was not found\")\n\t\treturn\n\t}\n\n\tgot, err := newFn(context.Background(), apiv1.Options{\n\t\tCertificateAuthority: testAuthorityName, CredentialsFile: \"testdata/credentials.json\",\n\t})\n\tif err != nil {\n\t\tt.Errorf(\"New() error = %v\", err)\n\t\treturn\n\t}\n\tif !reflect.DeepEqual(got, want) {\n\t\tt.Errorf(\"New() = %v, want %v\", got, want)\n\t}\n}\n\nfunc TestNew_real(t *testing.T) {\n\tif v, ok := os.LookupEnv(\"GOOGLE_APPLICATION_CREDENTIALS\"); ok {\n\t\tos.Unsetenv(\"GOOGLE_APPLICATION_CREDENTIALS\")\n\t\tt.Cleanup(func() {\n\t\t\tt.Setenv(\"GOOGLE_APPLICATION_CREDENTIALS\", v)\n\t\t})\n\t}\n\n\tfailDefaultCredentials := true\n\tif home, err := os.UserHomeDir(); err == nil {\n\t\tfile := filepath.Join(home, \".config\", \"gcloud\", \"application_default_credentials.json\")\n\t\tif _, err := os.Stat(file); err == nil {\n\t\t\tfailDefaultCredentials = false\n\t\t}\n\t}\n\n\ttype args struct {\n\t\tctx  context.Context\n\t\topts apiv1.Options\n\t}\n\ttests := []struct {\n\t\tname     string\n\t\tskipOnCI bool\n\t\targs     args\n\t\twantErr  bool\n\t}{\n\t\t{\"fail default credentials\", true, args{context.Background(), apiv1.Options{CertificateAuthority: testAuthorityName}}, failDefaultCredentials},\n\t\t{\"fail certificate authority\", false, args{context.Background(), apiv1.Options{}}, true},\n\t\t{\"fail with credentials\", false, args{context.Background(), apiv1.Options{\n\t\t\tCertificateAuthority: testAuthorityName, CredentialsFile: \"testdata/missing.json\",\n\t\t}}, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif tt.skipOnCI && os.Getenv(\"CI\") == \"true\" {\n\t\t\t\tt.SkipNow()\n\t\t\t}\n\t\t\t_, err := New(tt.args.ctx, tt.args.opts)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"New() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestCloudCAS_Type(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\twant apiv1.Type\n\t}{\n\t\t{\"ok\", apiv1.CloudCAS},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tc := &CloudCAS{}\n\t\t\tif got := c.Type(); got != tt.want {\n\t\t\t\tt.Errorf(\"CloudCAS.Type() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestCloudCAS_GetCertificateAuthority(t *testing.T) {\n\tintermediate := mustParseCertificate(t, testIntermediateCertificate)\n\troot := mustParseCertificate(t, testRootCertificate)\n\ttype fields struct {\n\t\tclient               CertificateAuthorityClient\n\t\tcertificateAuthority string\n\t}\n\ttype args struct {\n\t\treq *apiv1.GetCertificateAuthorityRequest\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tfields  fields\n\t\targs    args\n\t\twant    *apiv1.GetCertificateAuthorityResponse\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", fields{okTestClient(), testCertificateName}, args{&apiv1.GetCertificateAuthorityRequest{}}, &apiv1.GetCertificateAuthorityResponse{\n\t\t\tRootCertificate:          root,\n\t\t\tIntermediateCertificates: []*x509.Certificate{intermediate},\n\t\t}, false},\n\t\t{\"ok with name\", fields{okTestClient(), testCertificateName}, args{&apiv1.GetCertificateAuthorityRequest{\n\t\t\tName: testCertificateName,\n\t\t}}, &apiv1.GetCertificateAuthorityResponse{\n\t\t\tRootCertificate:          root,\n\t\t\tIntermediateCertificates: []*x509.Certificate{intermediate},\n\t\t}, false},\n\t\t{\"ok with root only\", fields{okTestClientRootOnly(), testCertificateName}, args{&apiv1.GetCertificateAuthorityRequest{}}, &apiv1.GetCertificateAuthorityResponse{\n\t\t\tRootCertificate:          root,\n\t\t\tIntermediateCertificates: []*x509.Certificate{},\n\t\t}, false},\n\t\t{\"ok with multiple intermediates\", fields{okTestClientWithMultipleIntermediates(), testCertificateName}, args{&apiv1.GetCertificateAuthorityRequest{}}, &apiv1.GetCertificateAuthorityResponse{\n\t\t\tRootCertificate:          root,\n\t\t\tIntermediateCertificates: []*x509.Certificate{intermediate, intermediate, intermediate},\n\t\t}, false},\n\t\t{\"fail GetCertificateAuthority\", fields{failTestClient(), testCertificateName}, args{&apiv1.GetCertificateAuthorityRequest{}}, nil, true},\n\t\t{\"fail bad root\", fields{badRootTestClient(), testCertificateName}, args{&apiv1.GetCertificateAuthorityRequest{}}, nil, true},\n\t\t{\"fail bad intermediate\", fields{badIntermediateTestClient(), testCertificateName}, args{&apiv1.GetCertificateAuthorityRequest{}}, nil, true},\n\t\t{\"fail no pems\", fields{&testClient{certificateAuthority: &pb.CertificateAuthority{}}, testCertificateName}, args{&apiv1.GetCertificateAuthorityRequest{}}, nil, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tc := &CloudCAS{\n\t\t\t\tclient:               tt.fields.client,\n\t\t\t\tcertificateAuthority: tt.fields.certificateAuthority,\n\t\t\t}\n\t\t\tgot, err := c.GetCertificateAuthority(tt.args.req)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"CloudCAS.GetCertificateAuthority() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"CloudCAS.GetCertificateAuthority() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestCloudCAS_CreateCertificate(t *testing.T) {\n\ttype fields struct {\n\t\tclient               CertificateAuthorityClient\n\t\tcertificateAuthority string\n\t}\n\ttype args struct {\n\t\treq *apiv1.CreateCertificateRequest\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tfields  fields\n\t\targs    args\n\t\twant    *apiv1.CreateCertificateResponse\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", fields{okTestClient(), testCertificateName}, args{&apiv1.CreateCertificateRequest{\n\t\t\tTemplate: mustParseCertificate(t, testLeafCertificate),\n\t\t\tLifetime: 24 * time.Hour,\n\t\t}}, &apiv1.CreateCertificateResponse{\n\t\t\tCertificate:      mustParseCertificate(t, testSignedCertificate),\n\t\t\tCertificateChain: []*x509.Certificate{mustParseCertificate(t, testIntermediateCertificate)},\n\t\t}, false},\n\t\t{\"fail Template\", fields{okTestClient(), testCertificateName}, args{&apiv1.CreateCertificateRequest{\n\t\t\tLifetime: 24 * time.Hour,\n\t\t}}, nil, true},\n\t\t{\"fail Lifetime\", fields{okTestClient(), testCertificateName}, args{&apiv1.CreateCertificateRequest{\n\t\t\tTemplate: mustParseCertificate(t, testLeafCertificate),\n\t\t}}, nil, true},\n\t\t{\"fail CreateCertificate\", fields{failTestClient(), testCertificateName}, args{&apiv1.CreateCertificateRequest{\n\t\t\tTemplate: mustParseCertificate(t, testLeafCertificate),\n\t\t\tLifetime: 24 * time.Hour,\n\t\t}}, nil, true},\n\t\t{\"fail Certificate\", fields{badRootTestClient(), testCertificateName}, args{&apiv1.CreateCertificateRequest{\n\t\t\tTemplate: mustParseCertificate(t, testLeafCertificate),\n\t\t\tLifetime: 24 * time.Hour,\n\t\t}}, nil, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tc := &CloudCAS{\n\t\t\t\tclient:               tt.fields.client,\n\t\t\t\tcertificateAuthority: tt.fields.certificateAuthority,\n\t\t\t}\n\t\t\tgot, err := c.CreateCertificate(tt.args.req)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"CloudCAS.CreateCertificate() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"CloudCAS.CreateCertificate() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestCloudCAS_createCertificate(t *testing.T) {\n\tleaf := mustParseCertificate(t, testLeafCertificate)\n\tsigned := mustParseCertificate(t, testSignedCertificate)\n\tchain := []*x509.Certificate{mustParseCertificate(t, testIntermediateCertificate)}\n\n\ttype fields struct {\n\t\tclient               CertificateAuthorityClient\n\t\tcertificateAuthority string\n\t}\n\ttype args struct {\n\t\ttpl       *x509.Certificate\n\t\tlifetime  time.Duration\n\t\trequestID string\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tfields  fields\n\t\targs    args\n\t\twant    *x509.Certificate\n\t\twant1   []*x509.Certificate\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", fields{okTestClient(), testAuthorityName}, args{leaf, 24 * time.Hour, \"request-id\"}, signed, chain, false},\n\t\t{\"fail CertificateConfig\", fields{okTestClient(), testAuthorityName}, args{&x509.Certificate{}, 24 * time.Hour, \"request-id\"}, nil, nil, true},\n\t\t{\"fail CreateCertificate\", fields{failTestClient(), testAuthorityName}, args{leaf, 24 * time.Hour, \"request-id\"}, nil, nil, true},\n\t\t{\"fail ParseCertificates\", fields{badRootTestClient(), testAuthorityName}, args{leaf, 24 * time.Hour, \"request-id\"}, nil, nil, true},\n\t\t{\"fail create id\", fields{okTestClient(), testAuthorityName}, args{leaf, 24 * time.Hour, \"request-id\"}, nil, nil, true},\n\t}\n\n\t// Pre-calculate rand.Random\n\tbuf := new(bytes.Buffer)\n\tsetTeeReader(t, buf)\n\tfor i := 0; i < len(tests)-1; i++ {\n\t\t_, err := uuid.NewRandomFromReader(rand.Reader)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t}\n\trand.Reader = buf\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tc := &CloudCAS{\n\t\t\t\tclient:               tt.fields.client,\n\t\t\t\tcertificateAuthority: tt.fields.certificateAuthority,\n\t\t\t}\n\t\t\tgot, got1, err := c.createCertificate(tt.args.tpl, tt.args.lifetime, tt.args.requestID)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"CloudCAS.createCertificate() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"CloudCAS.createCertificate() got = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(got1, tt.want1) {\n\t\t\t\tt.Errorf(\"CloudCAS.createCertificate() got1 = %v, want %v\", got1, tt.want1)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestCloudCAS_RenewCertificate(t *testing.T) {\n\ttype fields struct {\n\t\tclient               CertificateAuthorityClient\n\t\tcertificateAuthority string\n\t}\n\ttype args struct {\n\t\treq *apiv1.RenewCertificateRequest\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tfields  fields\n\t\targs    args\n\t\twant    *apiv1.RenewCertificateResponse\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", fields{okTestClient(), testCertificateName}, args{&apiv1.RenewCertificateRequest{\n\t\t\tTemplate: mustParseCertificate(t, testLeafCertificate),\n\t\t\tLifetime: 24 * time.Hour,\n\t\t}}, &apiv1.RenewCertificateResponse{\n\t\t\tCertificate:      mustParseCertificate(t, testSignedCertificate),\n\t\t\tCertificateChain: []*x509.Certificate{mustParseCertificate(t, testIntermediateCertificate)},\n\t\t}, false},\n\t\t{\"fail Template\", fields{okTestClient(), testCertificateName}, args{&apiv1.RenewCertificateRequest{\n\t\t\tLifetime: 24 * time.Hour,\n\t\t}}, nil, true},\n\t\t{\"fail Lifetime\", fields{okTestClient(), testCertificateName}, args{&apiv1.RenewCertificateRequest{\n\t\t\tTemplate: mustParseCertificate(t, testLeafCertificate),\n\t\t}}, nil, true},\n\t\t{\"fail CreateCertificate\", fields{failTestClient(), testCertificateName}, args{&apiv1.RenewCertificateRequest{\n\t\t\tTemplate: mustParseCertificate(t, testLeafCertificate),\n\t\t\tLifetime: 24 * time.Hour,\n\t\t}}, nil, true},\n\t\t{\"fail Certificate\", fields{badRootTestClient(), testCertificateName}, args{&apiv1.RenewCertificateRequest{\n\t\t\tTemplate: mustParseCertificate(t, testLeafCertificate),\n\t\t\tLifetime: 24 * time.Hour,\n\t\t}}, nil, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tc := &CloudCAS{\n\t\t\t\tclient:               tt.fields.client,\n\t\t\t\tcertificateAuthority: tt.fields.certificateAuthority,\n\t\t\t}\n\t\t\tgot, err := c.RenewCertificate(tt.args.req)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"CloudCAS.RenewCertificate() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"CloudCAS.RenewCertificate() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestCloudCAS_RevokeCertificate(t *testing.T) {\n\tbadExtensionCert := mustParseCertificate(t, testSignedCertificate)\n\tfor i, ext := range badExtensionCert.Extensions {\n\t\tif ext.Id.Equal(asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 37476, 9000, 64, 2}) {\n\t\t\tbadExtensionCert.Extensions[i].Value = []byte(\"bad-data\")\n\t\t}\n\t}\n\n\ttype fields struct {\n\t\tclient               CertificateAuthorityClient\n\t\tcertificateAuthority string\n\t}\n\ttype args struct {\n\t\treq *apiv1.RevokeCertificateRequest\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tfields  fields\n\t\targs    args\n\t\twant    *apiv1.RevokeCertificateResponse\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", fields{okTestClient(), testCertificateName}, args{&apiv1.RevokeCertificateRequest{\n\t\t\tCertificate: mustParseCertificate(t, testSignedCertificate),\n\t\t\tReasonCode:  1,\n\t\t}}, &apiv1.RevokeCertificateResponse{\n\t\t\tCertificate:      mustParseCertificate(t, testSignedCertificate),\n\t\t\tCertificateChain: []*x509.Certificate{mustParseCertificate(t, testIntermediateCertificate)},\n\t\t}, false},\n\t\t{\"fail Extension\", fields{okTestClient(), testCertificateName}, args{&apiv1.RevokeCertificateRequest{\n\t\t\tCertificate: mustParseCertificate(t, testLeafCertificate),\n\t\t\tReasonCode:  1,\n\t\t}}, nil, true},\n\t\t{\"fail Extension Value\", fields{okTestClient(), testCertificateName}, args{&apiv1.RevokeCertificateRequest{\n\t\t\tCertificate: badExtensionCert,\n\t\t\tReasonCode:  1,\n\t\t}}, nil, true},\n\t\t{\"fail Certificate\", fields{okTestClient(), testCertificateName}, args{&apiv1.RevokeCertificateRequest{\n\t\t\tReasonCode: 2,\n\t\t}}, nil, true},\n\t\t{\"fail ReasonCode\", fields{okTestClient(), testCertificateName}, args{&apiv1.RevokeCertificateRequest{\n\t\t\tCertificate: mustParseCertificate(t, testSignedCertificate),\n\t\t\tReasonCode:  100,\n\t\t}}, nil, true},\n\t\t{\"fail ReasonCode 7\", fields{okTestClient(), testCertificateName}, args{&apiv1.RevokeCertificateRequest{\n\t\t\tCertificate: mustParseCertificate(t, testSignedCertificate),\n\t\t\tReasonCode:  7,\n\t\t}}, nil, true},\n\t\t{\"fail ReasonCode 8\", fields{okTestClient(), testCertificateName}, args{&apiv1.RevokeCertificateRequest{\n\t\t\tCertificate: mustParseCertificate(t, testSignedCertificate),\n\t\t\tReasonCode:  8,\n\t\t}}, nil, true},\n\t\t{\"fail RevokeCertificate\", fields{failTestClient(), testCertificateName}, args{&apiv1.RevokeCertificateRequest{\n\t\t\tCertificate: mustParseCertificate(t, testSignedCertificate),\n\t\t\tReasonCode:  1,\n\t\t}}, nil, true},\n\t\t{\"fail ParseCertificate\", fields{badRootTestClient(), testCertificateName}, args{&apiv1.RevokeCertificateRequest{\n\t\t\tCertificate: mustParseCertificate(t, testSignedCertificate),\n\t\t\tReasonCode:  1,\n\t\t}}, nil, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tc := &CloudCAS{\n\t\t\t\tclient:               tt.fields.client,\n\t\t\t\tcertificateAuthority: tt.fields.certificateAuthority,\n\t\t\t}\n\t\t\tgot, err := c.RevokeCertificate(tt.args.req)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"CloudCAS.RevokeCertificate() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"CloudCAS.RevokeCertificate() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_createCertificateID(t *testing.T) {\n\tbuf := new(bytes.Buffer)\n\tsetTeeReader(t, buf)\n\tid, err := uuid.NewRandomFromReader(rand.Reader)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\trand.Reader = buf\n\n\ttests := []struct {\n\t\tname    string\n\t\twant    string\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", id.String(), false},\n\t\t{\"fail\", \"\", true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := createCertificateID()\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"createCertificateID() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif got != tt.want {\n\t\t\t\tt.Errorf(\"createCertificateID() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_parseCertificate(t *testing.T) {\n\ttype args struct {\n\t\tpemCert string\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\targs    args\n\t\twant    *x509.Certificate\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", args{testLeafCertificate}, mustParseCertificate(t, testLeafCertificate), false},\n\t\t{\"ok intermediate\", args{testIntermediateCertificate}, mustParseCertificate(t, testIntermediateCertificate), false},\n\t\t{\"fail pem\", args{\"not pem\"}, nil, true},\n\t\t{\"fail parseCertificate\", args{\"-----BEGIN CERTIFICATE-----\\nZm9vYmFyCg==\\n-----END CERTIFICATE-----\\n\"}, nil, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := parseCertificate(tt.args.pemCert)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"parseCertificate() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"parseCertificate() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_getCertificateAndChain(t *testing.T) {\n\ttype args struct {\n\t\tcertpb *pb.Certificate\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\targs    args\n\t\twant    *x509.Certificate\n\t\twant1   []*x509.Certificate\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", args{&pb.Certificate{\n\t\t\tName:                testCertificateName,\n\t\t\tPemCertificate:      testSignedCertificate,\n\t\t\tPemCertificateChain: []string{testIntermediateCertificate, testRootCertificate},\n\t\t}}, mustParseCertificate(t, testSignedCertificate), []*x509.Certificate{mustParseCertificate(t, testIntermediateCertificate)}, false},\n\t\t{\"fail PemCertificate\", args{&pb.Certificate{\n\t\t\tName:                testCertificateName,\n\t\t\tPemCertificate:      \"foobar\",\n\t\t\tPemCertificateChain: []string{testIntermediateCertificate, testRootCertificate},\n\t\t}}, nil, nil, true},\n\t\t{\"fail PemCertificateChain\", args{&pb.Certificate{\n\t\t\tName:                testCertificateName,\n\t\t\tPemCertificate:      testSignedCertificate,\n\t\t\tPemCertificateChain: []string{\"foobar\", testRootCertificate},\n\t\t}}, nil, nil, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, got1, err := getCertificateAndChain(tt.args.certpb)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"getCertificateAndChain() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"getCertificateAndChain() got = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(got1, tt.want1) {\n\t\t\t\tt.Errorf(\"getCertificateAndChain() got1 = %v, want %v\", got1, tt.want1)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestCloudCAS_CreateCertificateAuthority(t *testing.T) {\n\tmust := func(a, _ any) any {\n\t\treturn a\n\t}\n\n\tctrl := gomock.NewController(t)\n\tdefer ctrl.Finish()\n\tmosCtrl := gomock.NewController(t)\n\tdefer mosCtrl.Finish()\n\n\tm := NewMockCertificateAuthorityClient(ctrl)\n\tmos := NewMockOperationsServer(mosCtrl)\n\n\t// Create operation server\n\tsrv := grpc.NewServer()\n\tlongrunningpb.RegisterOperationsServer(srv, mos)\n\n\tlis := bufconn.Listen(2)\n\tgo srv.Serve(lis)\n\tdefer srv.Stop()\n\n\t// Create fake privateca client\n\tconn, err := grpc.NewClient(\"localhost\", grpc.WithTransportCredentials(insecure.NewCredentials()),\n\t\tgrpc.WithContextDialer(func(context.Context, string) (net.Conn, error) {\n\t\t\treturn lis.Dial()\n\t\t}))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tclient, err := lroauto.NewOperationsClient(context.Background(), option.WithGRPCConn(conn))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tfake, err := privateca.NewCertificateAuthorityClient(context.Background(), option.WithGRPCConn(conn))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tfake.LROClient = client\n\n\t// Configure mocks\n\tanee := gomock.Any()\n\n\t// ok root\n\tm.EXPECT().GetCaPool(anee, anee).Return(nil, status.Error(codes.NotFound, \"not found\"))\n\tm.EXPECT().CreateCaPool(anee, anee).Return(fake.CreateCaPoolOperation(\"CreateCaPool\"), nil)\n\tmos.EXPECT().GetOperation(anee, anee).Return(&longrunningpb.Operation{\n\t\tName: \"CreateCaPool\",\n\t\tDone: true,\n\t\tResult: &longrunningpb.Operation_Response{\n\t\t\tResponse: must(anypb.New(&pb.CaPool{\n\t\t\t\tName: testCaPoolName,\n\t\t\t})).(*anypb.Any),\n\t\t},\n\t}, nil)\n\tm.EXPECT().CreateCertificateAuthority(anee, anee).Return(fake.CreateCertificateAuthorityOperation(\"CreateCertificateAuthority\"), nil)\n\tmos.EXPECT().GetOperation(anee, anee).Return(&longrunningpb.Operation{\n\t\tName: \"CreateCertificateAuthority\",\n\t\tDone: true,\n\t\tResult: &longrunningpb.Operation_Response{\n\t\t\tResponse: must(anypb.New(&pb.CertificateAuthority{\n\t\t\t\tName:              testAuthorityName,\n\t\t\t\tPemCaCertificates: []string{testRootCertificate},\n\t\t\t})).(*anypb.Any),\n\t\t},\n\t}, nil)\n\tm.EXPECT().EnableCertificateAuthority(anee, anee).Return(fake.EnableCertificateAuthorityOperation(\"EnableCertificateAuthorityOperation\"), nil)\n\tmos.EXPECT().GetOperation(anee, anee).Return(&longrunningpb.Operation{\n\t\tName: \"EnableCertificateAuthority\",\n\t\tDone: true,\n\t\tResult: &longrunningpb.Operation_Response{\n\t\t\tResponse: must(anypb.New(&pb.CertificateAuthority{\n\t\t\t\tName:              testAuthorityName,\n\t\t\t\tPemCaCertificates: []string{testRootCertificate},\n\t\t\t})).(*anypb.Any),\n\t\t},\n\t}, nil)\n\n\t// ok intermediate\n\tm.EXPECT().GetCaPool(anee, anee).Return(&pb.CaPool{Name: testCaPoolName}, nil)\n\tm.EXPECT().CreateCertificateAuthority(anee, anee).Return(fake.CreateCertificateAuthorityOperation(\"CreateCertificateAuthority\"), nil)\n\tmos.EXPECT().GetOperation(anee, anee).Return(&longrunningpb.Operation{\n\t\tName: \"CreateCertificateAuthority\",\n\t\tDone: true,\n\t\tResult: &longrunningpb.Operation_Response{\n\t\t\tResponse: must(anypb.New(&pb.CertificateAuthority{\n\t\t\t\tName: testAuthorityName,\n\t\t\t})).(*anypb.Any),\n\t\t},\n\t}, nil)\n\tm.EXPECT().FetchCertificateAuthorityCsr(anee, anee).Return(&pb.FetchCertificateAuthorityCsrResponse{\n\t\tPemCsr: testIntermediateCsr,\n\t}, nil)\n\tm.EXPECT().CreateCertificate(anee, anee).Return(&pb.Certificate{\n\t\tPemCertificate:      testIntermediateCertificate,\n\t\tPemCertificateChain: []string{testRootCertificate},\n\t}, nil)\n\tm.EXPECT().ActivateCertificateAuthority(anee, anee).Return(fake.ActivateCertificateAuthorityOperation(\"ActivateCertificateAuthority\"), nil)\n\tmos.EXPECT().GetOperation(anee, anee).Return(&longrunningpb.Operation{\n\t\tName: \"ActivateCertificateAuthority\",\n\t\tDone: true,\n\t\tResult: &longrunningpb.Operation_Response{\n\t\t\tResponse: must(anypb.New(&pb.CertificateAuthority{\n\t\t\t\tName:              testAuthorityName,\n\t\t\t\tPemCaCertificates: []string{testIntermediateCertificate, testRootCertificate},\n\t\t\t})).(*anypb.Any),\n\t\t},\n\t}, nil)\n\tm.EXPECT().EnableCertificateAuthority(anee, anee).Return(fake.EnableCertificateAuthorityOperation(\"EnableCertificateAuthorityOperation\"), nil)\n\tmos.EXPECT().GetOperation(anee, anee).Return(&longrunningpb.Operation{\n\t\tName: \"EnableCertificateAuthority\",\n\t\tDone: true,\n\t\tResult: &longrunningpb.Operation_Response{\n\t\t\tResponse: must(anypb.New(&pb.CertificateAuthority{\n\t\t\t\tName:              testAuthorityName,\n\t\t\t\tPemCaCertificates: []string{testIntermediateCertificate, testRootCertificate},\n\t\t\t})).(*anypb.Any),\n\t\t},\n\t}, nil)\n\n\t// ok intermediate local signer\n\tm.EXPECT().GetCaPool(anee, anee).Return(&pb.CaPool{Name: testCaPoolName}, nil)\n\tm.EXPECT().CreateCertificateAuthority(anee, anee).Return(fake.CreateCertificateAuthorityOperation(\"CreateCertificateAuthority\"), nil)\n\tmos.EXPECT().GetOperation(anee, anee).Return(&longrunningpb.Operation{\n\t\tName: \"CreateCertificateAuthority\",\n\t\tDone: true,\n\t\tResult: &longrunningpb.Operation_Response{\n\t\t\tResponse: must(anypb.New(&pb.CertificateAuthority{\n\t\t\t\tName: testAuthorityName,\n\t\t\t})).(*anypb.Any),\n\t\t},\n\t}, nil)\n\tm.EXPECT().FetchCertificateAuthorityCsr(anee, anee).Return(&pb.FetchCertificateAuthorityCsrResponse{\n\t\tPemCsr: testIntermediateCsr,\n\t}, nil)\n\tm.EXPECT().ActivateCertificateAuthority(anee, anee).Return(fake.ActivateCertificateAuthorityOperation(\"ActivateCertificateAuthority\"), nil)\n\tmos.EXPECT().GetOperation(anee, anee).Return(&longrunningpb.Operation{\n\t\tName: \"ActivateCertificateAuthority\",\n\t\tDone: true,\n\t\tResult: &longrunningpb.Operation_Response{\n\t\t\tResponse: must(anypb.New(&pb.CertificateAuthority{\n\t\t\t\tName:              testAuthorityName,\n\t\t\t\tPemCaCertificates: []string{testIntermediateCertificate, testRootCertificate},\n\t\t\t})).(*anypb.Any),\n\t\t},\n\t}, nil)\n\tm.EXPECT().EnableCertificateAuthority(anee, anee).Return(fake.EnableCertificateAuthorityOperation(\"EnableCertificateAuthorityOperation\"), nil)\n\tmos.EXPECT().GetOperation(anee, anee).Return(&longrunningpb.Operation{\n\t\tName: \"EnableCertificateAuthority\",\n\t\tDone: true,\n\t\tResult: &longrunningpb.Operation_Response{\n\t\t\tResponse: must(anypb.New(&pb.CertificateAuthority{\n\t\t\t\tName:              testAuthorityName,\n\t\t\t\tPemCaCertificates: []string{testIntermediateCertificate, testRootCertificate},\n\t\t\t})).(*anypb.Any),\n\t\t},\n\t}, nil)\n\n\t// ok create key\n\tm.EXPECT().GetCaPool(anee, anee).Return(&pb.CaPool{Name: testCaPoolName}, nil)\n\tm.EXPECT().CreateCertificateAuthority(anee, anee).Return(fake.CreateCertificateAuthorityOperation(\"CreateCertificateAuthority\"), nil)\n\tmos.EXPECT().GetOperation(anee, anee).Return(&longrunningpb.Operation{\n\t\tName: \"CreateCertificateAuthority\",\n\t\tDone: true,\n\t\tResult: &longrunningpb.Operation_Response{\n\t\t\tResponse: must(anypb.New(&pb.CertificateAuthority{\n\t\t\t\tName:              testAuthorityName,\n\t\t\t\tPemCaCertificates: []string{testRootCertificate},\n\t\t\t})).(*anypb.Any),\n\t\t},\n\t}, nil)\n\tm.EXPECT().EnableCertificateAuthority(anee, anee).Return(fake.EnableCertificateAuthorityOperation(\"EnableCertificateAuthorityOperation\"), nil)\n\tmos.EXPECT().GetOperation(anee, anee).Return(&longrunningpb.Operation{\n\t\tName: \"EnableCertificateAuthority\",\n\t\tDone: true,\n\t\tResult: &longrunningpb.Operation_Response{\n\t\t\tResponse: must(anypb.New(&pb.CertificateAuthority{\n\t\t\t\tName:              testAuthorityName,\n\t\t\t\tPemCaCertificates: []string{testRootCertificate},\n\t\t\t})).(*anypb.Any),\n\t\t},\n\t}, nil)\n\n\t// fail GetCaPool\n\tm.EXPECT().GetCaPool(anee, anee).Return(nil, errTest)\n\n\t// fail CreateCaPool\n\tm.EXPECT().GetCaPool(anee, anee).Return(nil, status.Error(codes.NotFound, \"not found\"))\n\tm.EXPECT().CreateCaPool(anee, anee).Return(nil, errTest)\n\n\t// fail CreateCaPool.Wait\n\tm.EXPECT().GetCaPool(anee, anee).Return(nil, status.Error(codes.NotFound, \"not found\"))\n\tm.EXPECT().CreateCaPool(anee, anee).Return(fake.CreateCaPoolOperation(\"CreateCaPool\"), nil)\n\tmos.EXPECT().GetOperation(anee, anee).Return(nil, errTest)\n\n\t// fail CreateCertificateAuthority\n\tm.EXPECT().GetCaPool(anee, anee).Return(&pb.CaPool{Name: testCaPoolName}, nil)\n\tm.EXPECT().CreateCertificateAuthority(anee, anee).Return(nil, errTest)\n\n\t// fail CreateCertificateAuthority.Wait\n\tm.EXPECT().GetCaPool(anee, anee).Return(&pb.CaPool{Name: testCaPoolName}, nil)\n\tm.EXPECT().CreateCertificateAuthority(anee, anee).Return(fake.CreateCertificateAuthorityOperation(\"CreateCertificateAuthority\"), nil)\n\tmos.EXPECT().GetOperation(anee, anee).Return(nil, errTest)\n\n\t// fail EnableCertificateAuthority\n\tm.EXPECT().GetCaPool(anee, anee).Return(&pb.CaPool{Name: testCaPoolName}, nil)\n\tm.EXPECT().CreateCertificateAuthority(anee, anee).Return(fake.CreateCertificateAuthorityOperation(\"CreateCertificateAuthority\"), nil)\n\tmos.EXPECT().GetOperation(anee, anee).Return(&longrunningpb.Operation{\n\t\tName: \"CreateCertificateAuthority\",\n\t\tDone: true,\n\t\tResult: &longrunningpb.Operation_Response{\n\t\t\tResponse: must(anypb.New(&pb.CertificateAuthority{\n\t\t\t\tName:              testAuthorityName,\n\t\t\t\tPemCaCertificates: []string{testRootCertificate},\n\t\t\t})).(*anypb.Any),\n\t\t},\n\t}, nil)\n\tm.EXPECT().EnableCertificateAuthority(anee, anee).Return(nil, errTest)\n\n\t// fail EnableCertificateAuthority.Wait\n\tm.EXPECT().GetCaPool(anee, anee).Return(&pb.CaPool{Name: testCaPoolName}, nil)\n\tm.EXPECT().CreateCertificateAuthority(anee, anee).Return(fake.CreateCertificateAuthorityOperation(\"CreateCertificateAuthority\"), nil)\n\tmos.EXPECT().GetOperation(anee, anee).Return(&longrunningpb.Operation{\n\t\tName: \"CreateCertificateAuthority\",\n\t\tDone: true,\n\t\tResult: &longrunningpb.Operation_Response{\n\t\t\tResponse: must(anypb.New(&pb.CertificateAuthority{\n\t\t\t\tName:              testAuthorityName,\n\t\t\t\tPemCaCertificates: []string{testRootCertificate},\n\t\t\t})).(*anypb.Any),\n\t\t},\n\t}, nil)\n\tm.EXPECT().EnableCertificateAuthority(anee, anee).Return(fake.EnableCertificateAuthorityOperation(\"EnableCertificateAuthorityOperation\"), nil)\n\tmos.EXPECT().GetOperation(anee, anee).Return(nil, errTest)\n\n\t// fail EnableCertificateAuthority intermediate\n\tm.EXPECT().GetCaPool(anee, anee).Return(&pb.CaPool{Name: testCaPoolName}, nil)\n\tm.EXPECT().CreateCertificateAuthority(anee, anee).Return(fake.CreateCertificateAuthorityOperation(\"CreateCertificateAuthority\"), nil)\n\tmos.EXPECT().GetOperation(anee, anee).Return(&longrunningpb.Operation{\n\t\tName: \"CreateCertificateAuthority\",\n\t\tDone: true,\n\t\tResult: &longrunningpb.Operation_Response{\n\t\t\tResponse: must(anypb.New(&pb.CertificateAuthority{\n\t\t\t\tName: testAuthorityName,\n\t\t\t})).(*anypb.Any),\n\t\t},\n\t}, nil)\n\tm.EXPECT().FetchCertificateAuthorityCsr(anee, anee).Return(&pb.FetchCertificateAuthorityCsrResponse{\n\t\tPemCsr: testIntermediateCsr,\n\t}, nil)\n\tm.EXPECT().CreateCertificate(anee, anee).Return(&pb.Certificate{\n\t\tPemCertificate:      testIntermediateCertificate,\n\t\tPemCertificateChain: []string{testRootCertificate},\n\t}, nil)\n\tm.EXPECT().ActivateCertificateAuthority(anee, anee).Return(fake.ActivateCertificateAuthorityOperation(\"ActivateCertificateAuthority\"), nil)\n\tmos.EXPECT().GetOperation(anee, anee).Return(&longrunningpb.Operation{\n\t\tName: \"ActivateCertificateAuthority\",\n\t\tDone: true,\n\t\tResult: &longrunningpb.Operation_Response{\n\t\t\tResponse: must(anypb.New(&pb.CertificateAuthority{\n\t\t\t\tName:              testAuthorityName,\n\t\t\t\tPemCaCertificates: []string{testIntermediateCertificate, testRootCertificate},\n\t\t\t})).(*anypb.Any),\n\t\t},\n\t}, nil)\n\tm.EXPECT().EnableCertificateAuthority(anee, anee).Return(nil, errTest)\n\n\t// fail EnableCertificateAuthority.Wait intermediate\n\tm.EXPECT().GetCaPool(anee, anee).Return(&pb.CaPool{Name: testCaPoolName}, nil)\n\tm.EXPECT().CreateCertificateAuthority(anee, anee).Return(fake.CreateCertificateAuthorityOperation(\"CreateCertificateAuthority\"), nil)\n\tmos.EXPECT().GetOperation(anee, anee).Return(&longrunningpb.Operation{\n\t\tName: \"CreateCertificateAuthority\",\n\t\tDone: true,\n\t\tResult: &longrunningpb.Operation_Response{\n\t\t\tResponse: must(anypb.New(&pb.CertificateAuthority{\n\t\t\t\tName: testAuthorityName,\n\t\t\t})).(*anypb.Any),\n\t\t},\n\t}, nil)\n\tm.EXPECT().FetchCertificateAuthorityCsr(anee, anee).Return(&pb.FetchCertificateAuthorityCsrResponse{\n\t\tPemCsr: testIntermediateCsr,\n\t}, nil)\n\tm.EXPECT().CreateCertificate(anee, anee).Return(&pb.Certificate{\n\t\tPemCertificate:      testIntermediateCertificate,\n\t\tPemCertificateChain: []string{testRootCertificate},\n\t}, nil)\n\tm.EXPECT().ActivateCertificateAuthority(anee, anee).Return(fake.ActivateCertificateAuthorityOperation(\"ActivateCertificateAuthority\"), nil)\n\tmos.EXPECT().GetOperation(anee, anee).Return(&longrunningpb.Operation{\n\t\tName: \"ActivateCertificateAuthority\",\n\t\tDone: true,\n\t\tResult: &longrunningpb.Operation_Response{\n\t\t\tResponse: must(anypb.New(&pb.CertificateAuthority{\n\t\t\t\tName:              testAuthorityName,\n\t\t\t\tPemCaCertificates: []string{testIntermediateCertificate, testRootCertificate},\n\t\t\t})).(*anypb.Any),\n\t\t},\n\t}, nil)\n\tm.EXPECT().EnableCertificateAuthority(anee, anee).Return(fake.EnableCertificateAuthorityOperation(\"EnableCertificateAuthorityOperation\"), nil)\n\tmos.EXPECT().GetOperation(anee, anee).Return(nil, errTest)\n\n\t// fail FetchCertificateAuthorityCsr\n\tm.EXPECT().GetCaPool(anee, anee).Return(&pb.CaPool{Name: testCaPoolName}, nil)\n\tm.EXPECT().CreateCertificateAuthority(anee, anee).Return(fake.CreateCertificateAuthorityOperation(\"CreateCertificateAuthority\"), nil)\n\tmos.EXPECT().GetOperation(anee, anee).Return(&longrunningpb.Operation{\n\t\tName: \"CreateCertificateAuthority\",\n\t\tDone: true,\n\t\tResult: &longrunningpb.Operation_Response{\n\t\t\tResponse: must(anypb.New(&pb.CertificateAuthority{\n\t\t\t\tName: testAuthorityName,\n\t\t\t})).(*anypb.Any),\n\t\t},\n\t}, nil)\n\tm.EXPECT().FetchCertificateAuthorityCsr(anee, anee).Return(nil, errTest)\n\n\t// fail CreateCertificate\n\tm.EXPECT().GetCaPool(anee, anee).Return(&pb.CaPool{Name: testCaPoolName}, nil)\n\tm.EXPECT().CreateCertificateAuthority(anee, anee).Return(fake.CreateCertificateAuthorityOperation(\"CreateCertificateAuthority\"), nil)\n\tmos.EXPECT().GetOperation(anee, anee).Return(&longrunningpb.Operation{\n\t\tName: \"CreateCertificateAuthority\",\n\t\tDone: true,\n\t\tResult: &longrunningpb.Operation_Response{\n\t\t\tResponse: must(anypb.New(&pb.CertificateAuthority{\n\t\t\t\tName: testAuthorityName,\n\t\t\t})).(*anypb.Any),\n\t\t},\n\t}, nil)\n\tm.EXPECT().FetchCertificateAuthorityCsr(anee, anee).Return(&pb.FetchCertificateAuthorityCsrResponse{\n\t\tPemCsr: testIntermediateCsr,\n\t}, nil)\n\tm.EXPECT().CreateCertificate(anee, anee).Return(nil, errTest)\n\n\t// fail ActivateCertificateAuthority\n\tm.EXPECT().GetCaPool(anee, anee).Return(&pb.CaPool{Name: testCaPoolName}, nil)\n\tm.EXPECT().CreateCertificateAuthority(anee, anee).Return(fake.CreateCertificateAuthorityOperation(\"CreateCertificateAuthority\"), nil)\n\tmos.EXPECT().GetOperation(anee, anee).Return(&longrunningpb.Operation{\n\t\tName: \"CreateCertificateAuthority\",\n\t\tDone: true,\n\t\tResult: &longrunningpb.Operation_Response{\n\t\t\tResponse: must(anypb.New(&pb.CertificateAuthority{\n\t\t\t\tName: testAuthorityName,\n\t\t\t})).(*anypb.Any),\n\t\t},\n\t}, nil)\n\tm.EXPECT().FetchCertificateAuthorityCsr(anee, anee).Return(&pb.FetchCertificateAuthorityCsrResponse{\n\t\tPemCsr: testIntermediateCsr,\n\t}, nil)\n\tm.EXPECT().CreateCertificate(anee, anee).Return(&pb.Certificate{\n\t\tPemCertificate:      testIntermediateCertificate,\n\t\tPemCertificateChain: []string{testRootCertificate},\n\t}, nil)\n\tm.EXPECT().ActivateCertificateAuthority(anee, anee).Return(nil, errTest)\n\n\t// fail ActivateCertificateAuthority.Wait\n\tm.EXPECT().GetCaPool(anee, anee).Return(&pb.CaPool{Name: testCaPoolName}, nil)\n\tm.EXPECT().CreateCertificateAuthority(anee, anee).Return(fake.CreateCertificateAuthorityOperation(\"CreateCertificateAuthority\"), nil)\n\tmos.EXPECT().GetOperation(anee, anee).Return(&longrunningpb.Operation{\n\t\tName: \"CreateCertificateAuthority\",\n\t\tDone: true,\n\t\tResult: &longrunningpb.Operation_Response{\n\t\t\tResponse: must(anypb.New(&pb.CertificateAuthority{\n\t\t\t\tName: testAuthorityName,\n\t\t\t})).(*anypb.Any),\n\t\t},\n\t}, nil)\n\tm.EXPECT().FetchCertificateAuthorityCsr(anee, anee).Return(&pb.FetchCertificateAuthorityCsrResponse{\n\t\tPemCsr: testIntermediateCsr,\n\t}, nil)\n\tm.EXPECT().CreateCertificate(anee, anee).Return(&pb.Certificate{\n\t\tPemCertificate:      testIntermediateCertificate,\n\t\tPemCertificateChain: []string{testRootCertificate},\n\t}, nil)\n\tm.EXPECT().ActivateCertificateAuthority(anee, anee).Return(fake.ActivateCertificateAuthorityOperation(\"ActivateCertificateAuthority\"), nil)\n\tmos.EXPECT().GetOperation(anee, anee).Return(nil, errTest)\n\n\t// fail x509util.CreateCertificate\n\tm.EXPECT().GetCaPool(anee, anee).Return(&pb.CaPool{Name: testCaPoolName}, nil)\n\tm.EXPECT().CreateCertificateAuthority(anee, anee).Return(fake.CreateCertificateAuthorityOperation(\"CreateCertificateAuthority\"), nil)\n\tmos.EXPECT().GetOperation(anee, anee).Return(&longrunningpb.Operation{\n\t\tName: \"CreateCertificateAuthority\",\n\t\tDone: true,\n\t\tResult: &longrunningpb.Operation_Response{\n\t\t\tResponse: must(anypb.New(&pb.CertificateAuthority{\n\t\t\t\tName: testAuthorityName,\n\t\t\t})).(*anypb.Any),\n\t\t},\n\t}, nil)\n\tm.EXPECT().FetchCertificateAuthorityCsr(anee, anee).Return(&pb.FetchCertificateAuthorityCsrResponse{\n\t\tPemCsr: testIntermediateCsr,\n\t}, nil)\n\n\t// fail parseCertificateRequest\n\tm.EXPECT().GetCaPool(anee, anee).Return(&pb.CaPool{Name: testCaPoolName}, nil)\n\tm.EXPECT().CreateCertificateAuthority(anee, anee).Return(fake.CreateCertificateAuthorityOperation(\"CreateCertificateAuthority\"), nil)\n\tmos.EXPECT().GetOperation(anee, anee).Return(&longrunningpb.Operation{\n\t\tName: \"CreateCertificateAuthority\",\n\t\tDone: true,\n\t\tResult: &longrunningpb.Operation_Response{\n\t\t\tResponse: must(anypb.New(&pb.CertificateAuthority{\n\t\t\t\tName: testAuthorityName,\n\t\t\t})).(*anypb.Any),\n\t\t},\n\t}, nil)\n\tm.EXPECT().FetchCertificateAuthorityCsr(anee, anee).Return(&pb.FetchCertificateAuthorityCsrResponse{\n\t\tPemCsr: \"Not a CSR\",\n\t}, nil)\n\n\trootCrt := mustParseCertificate(t, testRootCertificate)\n\tintCrt := mustParseCertificate(t, testIntermediateCertificate)\n\n\ttype fields struct {\n\t\tclient               CertificateAuthorityClient\n\t\tcertificateAuthority string\n\t\tproject              string\n\t\tlocation             string\n\t\tcaPool               string\n\t\tcaPoolTier           pb.CaPool_Tier\n\t}\n\ttype args struct {\n\t\treq *apiv1.CreateCertificateAuthorityRequest\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tfields  fields\n\t\targs    args\n\t\twant    *apiv1.CreateCertificateAuthorityResponse\n\t\twantErr bool\n\t}{\n\t\t{\"ok root\", fields{m, \"\", testProject, testLocation, testCaPool, pb.CaPool_ENTERPRISE}, args{&apiv1.CreateCertificateAuthorityRequest{\n\t\t\tType:     apiv1.RootCA,\n\t\t\tTemplate: mustParseCertificate(t, testRootCertificate),\n\t\t\tLifetime: 24 * time.Hour,\n\t\t}}, &apiv1.CreateCertificateAuthorityResponse{\n\t\t\tName:        testAuthorityName,\n\t\t\tCertificate: rootCrt,\n\t\t}, false},\n\t\t{\"ok intermediate\", fields{m, \"\", testProject, testLocation, testCaPool, pb.CaPool_DEVOPS}, args{&apiv1.CreateCertificateAuthorityRequest{\n\t\t\tType:     apiv1.IntermediateCA,\n\t\t\tTemplate: mustParseCertificate(t, testIntermediateCertificate),\n\t\t\tLifetime: 24 * time.Hour,\n\t\t\tParent: &apiv1.CreateCertificateAuthorityResponse{\n\t\t\t\tName:        testAuthorityName,\n\t\t\t\tCertificate: rootCrt,\n\t\t\t},\n\t\t}}, &apiv1.CreateCertificateAuthorityResponse{\n\t\t\tName:             testAuthorityName,\n\t\t\tCertificate:      intCrt,\n\t\t\tCertificateChain: []*x509.Certificate{rootCrt},\n\t\t}, false},\n\t\t{\"ok intermediate local signer\", fields{m, \"\", testProject, testLocation, testCaPool, pb.CaPool_ENTERPRISE}, args{&apiv1.CreateCertificateAuthorityRequest{\n\t\t\tType:     apiv1.IntermediateCA,\n\t\t\tTemplate: mustParseCertificate(t, testIntermediateCertificate),\n\t\t\tLifetime: 24 * time.Hour,\n\t\t\tParent: &apiv1.CreateCertificateAuthorityResponse{\n\t\t\t\tCertificate: rootCrt,\n\t\t\t\tSigner:      mustParseECKey(t, testRootKey),\n\t\t\t},\n\t\t}}, &apiv1.CreateCertificateAuthorityResponse{\n\t\t\tName:             testAuthorityName,\n\t\t\tCertificate:      intCrt,\n\t\t\tCertificateChain: []*x509.Certificate{rootCrt},\n\t\t}, false},\n\t\t{\"ok create key\", fields{m, \"\", testProject, testLocation, testCaPool, pb.CaPool_DEVOPS}, args{&apiv1.CreateCertificateAuthorityRequest{\n\t\t\tType:     apiv1.RootCA,\n\t\t\tTemplate: mustParseCertificate(t, testRootCertificate),\n\t\t\tLifetime: 24 * time.Hour,\n\t\t\tCreateKey: &kmsapi.CreateKeyRequest{\n\t\t\t\tSignatureAlgorithm: kmsapi.ECDSAWithSHA256,\n\t\t\t},\n\t\t}}, &apiv1.CreateCertificateAuthorityResponse{\n\t\t\tName:        testAuthorityName,\n\t\t\tCertificate: rootCrt,\n\t\t}, false},\n\t\t{\"fail project\", fields{m, \"\", \"\", testLocation, testCaPool, pb.CaPool_DEVOPS}, args{&apiv1.CreateCertificateAuthorityRequest{\n\t\t\tType:     apiv1.RootCA,\n\t\t\tTemplate: mustParseCertificate(t, testRootCertificate),\n\t\t\tLifetime: 24 * time.Hour,\n\t\t}}, nil, true},\n\t\t{\"fail location\", fields{m, \"\", testProject, \"\", testCaPool, pb.CaPool_DEVOPS}, args{&apiv1.CreateCertificateAuthorityRequest{\n\t\t\tType:     apiv1.RootCA,\n\t\t\tTemplate: mustParseCertificate(t, testRootCertificate),\n\t\t\tLifetime: 24 * time.Hour,\n\t\t}}, nil, true},\n\t\t{\"fail caPool\", fields{m, \"\", testProject, testLocation, \"\", pb.CaPool_DEVOPS}, args{&apiv1.CreateCertificateAuthorityRequest{\n\t\t\tType:     apiv1.RootCA,\n\t\t\tTemplate: mustParseCertificate(t, testRootCertificate),\n\t\t\tLifetime: 24 * time.Hour,\n\t\t}}, nil, true},\n\t\t{\"fail template\", fields{m, \"\", testProject, testLocation, testCaPool, pb.CaPool_DEVOPS}, args{&apiv1.CreateCertificateAuthorityRequest{\n\t\t\tType:     apiv1.RootCA,\n\t\t\tLifetime: 24 * time.Hour,\n\t\t}}, nil, true},\n\t\t{\"fail lifetime\", fields{m, \"\", testProject, testLocation, testCaPool, pb.CaPool_DEVOPS}, args{&apiv1.CreateCertificateAuthorityRequest{\n\t\t\tType:     apiv1.RootCA,\n\t\t\tTemplate: mustParseCertificate(t, testRootCertificate),\n\t\t}}, nil, true},\n\t\t{\"fail parent\", fields{m, \"\", testProject, testLocation, testCaPool, pb.CaPool_DEVOPS}, args{&apiv1.CreateCertificateAuthorityRequest{\n\t\t\tType:     apiv1.IntermediateCA,\n\t\t\tTemplate: mustParseCertificate(t, testRootCertificate),\n\t\t\tLifetime: 24 * time.Hour,\n\t\t}}, nil, true},\n\t\t{\"fail parent name\", fields{m, \"\", testProject, testLocation, testCaPool, pb.CaPool_DEVOPS}, args{&apiv1.CreateCertificateAuthorityRequest{\n\t\t\tType:     apiv1.IntermediateCA,\n\t\t\tTemplate: mustParseCertificate(t, testRootCertificate),\n\t\t\tLifetime: 24 * time.Hour,\n\t\t\tParent:   &apiv1.CreateCertificateAuthorityResponse{},\n\t\t}}, nil, true},\n\t\t{\"fail type\", fields{m, \"\", testProject, testLocation, testCaPool, pb.CaPool_DEVOPS}, args{&apiv1.CreateCertificateAuthorityRequest{\n\t\t\tType:     0,\n\t\t\tTemplate: mustParseCertificate(t, testRootCertificate),\n\t\t\tLifetime: 24 * time.Hour,\n\t\t}}, nil, true},\n\t\t{\"fail create key\", fields{m, \"\", testProject, testLocation, testCaPool, pb.CaPool_DEVOPS}, args{&apiv1.CreateCertificateAuthorityRequest{\n\t\t\tType:     apiv1.RootCA,\n\t\t\tTemplate: mustParseCertificate(t, testRootCertificate),\n\t\t\tLifetime: 24 * time.Hour,\n\t\t\tCreateKey: &kmsapi.CreateKeyRequest{\n\t\t\t\tSignatureAlgorithm: kmsapi.PureEd25519,\n\t\t\t},\n\t\t}}, nil, true},\n\t\t{\"fail GetCaPool\", fields{m, \"\", testProject, testLocation, testCaPool, pb.CaPool_DEVOPS}, args{&apiv1.CreateCertificateAuthorityRequest{\n\t\t\tType:     apiv1.RootCA,\n\t\t\tTemplate: mustParseCertificate(t, testRootCertificate),\n\t\t\tLifetime: 24 * time.Hour,\n\t\t}}, nil, true},\n\t\t{\"fail CreateCaPool\", fields{m, \"\", testProject, testLocation, testCaPool, pb.CaPool_DEVOPS}, args{&apiv1.CreateCertificateAuthorityRequest{\n\t\t\tType:     apiv1.RootCA,\n\t\t\tTemplate: mustParseCertificate(t, testRootCertificate),\n\t\t\tLifetime: 24 * time.Hour,\n\t\t}}, nil, true},\n\t\t{\"fail CreateCaPool.Wait\", fields{m, \"\", testProject, testLocation, testCaPool, pb.CaPool_DEVOPS}, args{&apiv1.CreateCertificateAuthorityRequest{\n\t\t\tType:     apiv1.RootCA,\n\t\t\tTemplate: mustParseCertificate(t, testRootCertificate),\n\t\t\tLifetime: 24 * time.Hour,\n\t\t}}, nil, true},\n\t\t{\"fail CreateCertificateAuthority\", fields{m, \"\", testProject, testLocation, testCaPool, pb.CaPool_DEVOPS}, args{&apiv1.CreateCertificateAuthorityRequest{\n\t\t\tType:     apiv1.RootCA,\n\t\t\tTemplate: mustParseCertificate(t, testRootCertificate),\n\t\t\tLifetime: 24 * time.Hour,\n\t\t}}, nil, true},\n\t\t{\"fail CreateCertificateAuthority.Wait\", fields{m, \"\", testProject, testLocation, testCaPool, pb.CaPool_DEVOPS}, args{&apiv1.CreateCertificateAuthorityRequest{\n\t\t\tType:     apiv1.RootCA,\n\t\t\tTemplate: mustParseCertificate(t, testRootCertificate),\n\t\t\tLifetime: 24 * time.Hour,\n\t\t}}, nil, true},\n\t\t{\"fail EnableCertificateAuthority\", fields{m, \"\", testProject, testLocation, testCaPool, pb.CaPool_DEVOPS}, args{&apiv1.CreateCertificateAuthorityRequest{\n\t\t\tType:     apiv1.RootCA,\n\t\t\tTemplate: mustParseCertificate(t, testRootCertificate),\n\t\t\tLifetime: 24 * time.Hour,\n\t\t}}, nil, true},\n\t\t{\"fail EnableCertificateAuthority.Wait\", fields{m, \"\", testProject, testLocation, testCaPool, pb.CaPool_DEVOPS}, args{&apiv1.CreateCertificateAuthorityRequest{\n\t\t\tType:     apiv1.RootCA,\n\t\t\tTemplate: mustParseCertificate(t, testRootCertificate),\n\t\t\tLifetime: 24 * time.Hour,\n\t\t}}, nil, true},\n\n\t\t{\"fail EnableCertificateAuthority intermediate\", fields{m, \"\", testProject, testLocation, testCaPool, pb.CaPool_DEVOPS}, args{&apiv1.CreateCertificateAuthorityRequest{\n\t\t\tType:     apiv1.IntermediateCA,\n\t\t\tTemplate: mustParseCertificate(t, testIntermediateCertificate),\n\t\t\tLifetime: 24 * time.Hour,\n\t\t\tParent: &apiv1.CreateCertificateAuthorityResponse{\n\t\t\t\tName:        testAuthorityName,\n\t\t\t\tCertificate: rootCrt,\n\t\t\t},\n\t\t}}, nil, true},\n\t\t{\"fail EnableCertificateAuthority.Wait intermediate\", fields{m, \"\", testProject, testLocation, testCaPool, pb.CaPool_DEVOPS}, args{&apiv1.CreateCertificateAuthorityRequest{\n\t\t\tType:     apiv1.IntermediateCA,\n\t\t\tTemplate: mustParseCertificate(t, testIntermediateCertificate),\n\t\t\tLifetime: 24 * time.Hour,\n\t\t\tParent: &apiv1.CreateCertificateAuthorityResponse{\n\t\t\t\tName:        testAuthorityName,\n\t\t\t\tCertificate: rootCrt,\n\t\t\t},\n\t\t}}, nil, true},\n\n\t\t{\"fail FetchCertificateAuthorityCsr\", fields{m, \"\", testProject, testLocation, testCaPool, pb.CaPool_DEVOPS}, args{&apiv1.CreateCertificateAuthorityRequest{\n\t\t\tType:     apiv1.IntermediateCA,\n\t\t\tTemplate: mustParseCertificate(t, testIntermediateCertificate),\n\t\t\tLifetime: 24 * time.Hour,\n\t\t\tParent: &apiv1.CreateCertificateAuthorityResponse{\n\t\t\t\tName:        testAuthorityName,\n\t\t\t\tCertificate: rootCrt,\n\t\t\t},\n\t\t}}, nil, true},\n\t\t{\"fail CreateCertificate\", fields{m, \"\", testProject, testLocation, testCaPool, pb.CaPool_DEVOPS}, args{&apiv1.CreateCertificateAuthorityRequest{\n\t\t\tType:     apiv1.IntermediateCA,\n\t\t\tTemplate: mustParseCertificate(t, testIntermediateCertificate),\n\t\t\tLifetime: 24 * time.Hour,\n\t\t\tParent: &apiv1.CreateCertificateAuthorityResponse{\n\t\t\t\tName:        testAuthorityName,\n\t\t\t\tCertificate: rootCrt,\n\t\t\t},\n\t\t}}, nil, true},\n\t\t{\"fail ActivateCertificateAuthority\", fields{m, \"\", testProject, testLocation, testCaPool, pb.CaPool_DEVOPS}, args{&apiv1.CreateCertificateAuthorityRequest{\n\t\t\tType:     apiv1.IntermediateCA,\n\t\t\tTemplate: mustParseCertificate(t, testIntermediateCertificate),\n\t\t\tLifetime: 24 * time.Hour,\n\t\t\tParent: &apiv1.CreateCertificateAuthorityResponse{\n\t\t\t\tName:        testAuthorityName,\n\t\t\t\tCertificate: rootCrt,\n\t\t\t},\n\t\t}}, nil, true},\n\t\t{\"fail ActivateCertificateAuthority.Wait\", fields{m, \"\", testProject, testLocation, testCaPool, pb.CaPool_DEVOPS}, args{&apiv1.CreateCertificateAuthorityRequest{\n\t\t\tType:     apiv1.IntermediateCA,\n\t\t\tTemplate: mustParseCertificate(t, testIntermediateCertificate),\n\t\t\tLifetime: 24 * time.Hour,\n\t\t\tParent: &apiv1.CreateCertificateAuthorityResponse{\n\t\t\t\tName:        testAuthorityName,\n\t\t\t\tCertificate: rootCrt,\n\t\t\t},\n\t\t}}, nil, true},\n\t\t{\"fail x509util.CreateCertificate\", fields{m, \"\", testProject, testLocation, testCaPool, pb.CaPool_DEVOPS}, args{&apiv1.CreateCertificateAuthorityRequest{\n\t\t\tType:     apiv1.IntermediateCA,\n\t\t\tTemplate: mustParseCertificate(t, testIntermediateCertificate),\n\t\t\tLifetime: 24 * time.Hour,\n\t\t\tParent: &apiv1.CreateCertificateAuthorityResponse{\n\t\t\t\tCertificate: rootCrt,\n\t\t\t\tSigner:      createBadSigner(t),\n\t\t\t},\n\t\t}}, nil, true},\n\t\t{\"fail parseCertificateRequest\", fields{m, \"\", testProject, testLocation, testCaPool, pb.CaPool_DEVOPS}, args{&apiv1.CreateCertificateAuthorityRequest{\n\t\t\tType:     apiv1.IntermediateCA,\n\t\t\tTemplate: mustParseCertificate(t, testIntermediateCertificate),\n\t\t\tLifetime: 24 * time.Hour,\n\t\t\tParent: &apiv1.CreateCertificateAuthorityResponse{\n\t\t\t\tCertificate: rootCrt,\n\t\t\t\tSigner:      createBadSigner(t),\n\t\t\t},\n\t\t}}, nil, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tc := &CloudCAS{\n\t\t\t\tclient:               tt.fields.client,\n\t\t\t\tcertificateAuthority: tt.fields.certificateAuthority,\n\t\t\t\tproject:              tt.fields.project,\n\t\t\t\tlocation:             tt.fields.location,\n\t\t\t\tcaPool:               tt.fields.caPool,\n\t\t\t\tcaPoolTier:           tt.fields.caPoolTier,\n\t\t\t}\n\t\t\tgot, err := c.CreateCertificateAuthority(tt.args.req)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"CloudCAS.CreateCertificateAuthority() error = %+v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"CloudCAS.CreateCertificateAuthority() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_normalizeCertificateAuthorityName(t *testing.T) {\n\ttype args struct {\n\t\tname string\n\t}\n\ttests := []struct {\n\t\tname string\n\t\targs args\n\t\twant string\n\t}{\n\t\t{\"ok\", args{\"Test-CA-Name_1234\"}, \"Test-CA-Name_1234\"},\n\t\t{\"change\", args{\"💥 CA\"}, \"--CA\"},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif got := normalizeCertificateAuthorityName(tt.args.name); got != tt.want {\n\t\t\t\tt.Errorf(\"normalizeCertificateAuthorityName() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "cas/cloudcas/mock_client_test.go",
    "content": "// Code generated by MockGen. DO NOT EDIT.\n// Source: github.com/smallstep/certificates/cas/cloudcas (interfaces: CertificateAuthorityClient)\n//\n// Generated by this command:\n//\n//\tmockgen -package cloudcas -mock_names=CertificateAuthorityClient=MockCertificateAuthorityClient -destination mock_client_test.go github.com/smallstep/certificates/cas/cloudcas CertificateAuthorityClient\n//\n\n// Package cloudcas is a generated GoMock package.\npackage cloudcas\n\nimport (\n\tcontext \"context\"\n\treflect \"reflect\"\n\n\tprivateca \"cloud.google.com/go/security/privateca/apiv1\"\n\tprivatecapb \"cloud.google.com/go/security/privateca/apiv1/privatecapb\"\n\tgax \"github.com/googleapis/gax-go/v2\"\n\tgomock \"go.uber.org/mock/gomock\"\n)\n\n// MockCertificateAuthorityClient is a mock of CertificateAuthorityClient interface.\ntype MockCertificateAuthorityClient struct {\n\tctrl     *gomock.Controller\n\trecorder *MockCertificateAuthorityClientMockRecorder\n\tisgomock struct{}\n}\n\n// MockCertificateAuthorityClientMockRecorder is the mock recorder for MockCertificateAuthorityClient.\ntype MockCertificateAuthorityClientMockRecorder struct {\n\tmock *MockCertificateAuthorityClient\n}\n\n// NewMockCertificateAuthorityClient creates a new mock instance.\nfunc NewMockCertificateAuthorityClient(ctrl *gomock.Controller) *MockCertificateAuthorityClient {\n\tmock := &MockCertificateAuthorityClient{ctrl: ctrl}\n\tmock.recorder = &MockCertificateAuthorityClientMockRecorder{mock}\n\treturn mock\n}\n\n// EXPECT returns an object that allows the caller to indicate expected use.\nfunc (m *MockCertificateAuthorityClient) EXPECT() *MockCertificateAuthorityClientMockRecorder {\n\treturn m.recorder\n}\n\n// ActivateCertificateAuthority mocks base method.\nfunc (m *MockCertificateAuthorityClient) ActivateCertificateAuthority(ctx context.Context, req *privatecapb.ActivateCertificateAuthorityRequest, opts ...gax.CallOption) (*privateca.ActivateCertificateAuthorityOperation, error) {\n\tm.ctrl.T.Helper()\n\tvarargs := []any{ctx, req}\n\tfor _, a := range opts {\n\t\tvarargs = append(varargs, a)\n\t}\n\tret := m.ctrl.Call(m, \"ActivateCertificateAuthority\", varargs...)\n\tret0, _ := ret[0].(*privateca.ActivateCertificateAuthorityOperation)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// ActivateCertificateAuthority indicates an expected call of ActivateCertificateAuthority.\nfunc (mr *MockCertificateAuthorityClientMockRecorder) ActivateCertificateAuthority(ctx, req any, opts ...any) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\tvarargs := append([]any{ctx, req}, opts...)\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"ActivateCertificateAuthority\", reflect.TypeOf((*MockCertificateAuthorityClient)(nil).ActivateCertificateAuthority), varargs...)\n}\n\n// CreateCaPool mocks base method.\nfunc (m *MockCertificateAuthorityClient) CreateCaPool(ctx context.Context, req *privatecapb.CreateCaPoolRequest, opts ...gax.CallOption) (*privateca.CreateCaPoolOperation, error) {\n\tm.ctrl.T.Helper()\n\tvarargs := []any{ctx, req}\n\tfor _, a := range opts {\n\t\tvarargs = append(varargs, a)\n\t}\n\tret := m.ctrl.Call(m, \"CreateCaPool\", varargs...)\n\tret0, _ := ret[0].(*privateca.CreateCaPoolOperation)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// CreateCaPool indicates an expected call of CreateCaPool.\nfunc (mr *MockCertificateAuthorityClientMockRecorder) CreateCaPool(ctx, req any, opts ...any) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\tvarargs := append([]any{ctx, req}, opts...)\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"CreateCaPool\", reflect.TypeOf((*MockCertificateAuthorityClient)(nil).CreateCaPool), varargs...)\n}\n\n// CreateCertificate mocks base method.\nfunc (m *MockCertificateAuthorityClient) CreateCertificate(ctx context.Context, req *privatecapb.CreateCertificateRequest, opts ...gax.CallOption) (*privatecapb.Certificate, error) {\n\tm.ctrl.T.Helper()\n\tvarargs := []any{ctx, req}\n\tfor _, a := range opts {\n\t\tvarargs = append(varargs, a)\n\t}\n\tret := m.ctrl.Call(m, \"CreateCertificate\", varargs...)\n\tret0, _ := ret[0].(*privatecapb.Certificate)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// CreateCertificate indicates an expected call of CreateCertificate.\nfunc (mr *MockCertificateAuthorityClientMockRecorder) CreateCertificate(ctx, req any, opts ...any) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\tvarargs := append([]any{ctx, req}, opts...)\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"CreateCertificate\", reflect.TypeOf((*MockCertificateAuthorityClient)(nil).CreateCertificate), varargs...)\n}\n\n// CreateCertificateAuthority mocks base method.\nfunc (m *MockCertificateAuthorityClient) CreateCertificateAuthority(ctx context.Context, req *privatecapb.CreateCertificateAuthorityRequest, opts ...gax.CallOption) (*privateca.CreateCertificateAuthorityOperation, error) {\n\tm.ctrl.T.Helper()\n\tvarargs := []any{ctx, req}\n\tfor _, a := range opts {\n\t\tvarargs = append(varargs, a)\n\t}\n\tret := m.ctrl.Call(m, \"CreateCertificateAuthority\", varargs...)\n\tret0, _ := ret[0].(*privateca.CreateCertificateAuthorityOperation)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// CreateCertificateAuthority indicates an expected call of CreateCertificateAuthority.\nfunc (mr *MockCertificateAuthorityClientMockRecorder) CreateCertificateAuthority(ctx, req any, opts ...any) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\tvarargs := append([]any{ctx, req}, opts...)\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"CreateCertificateAuthority\", reflect.TypeOf((*MockCertificateAuthorityClient)(nil).CreateCertificateAuthority), varargs...)\n}\n\n// EnableCertificateAuthority mocks base method.\nfunc (m *MockCertificateAuthorityClient) EnableCertificateAuthority(ctx context.Context, req *privatecapb.EnableCertificateAuthorityRequest, opts ...gax.CallOption) (*privateca.EnableCertificateAuthorityOperation, error) {\n\tm.ctrl.T.Helper()\n\tvarargs := []any{ctx, req}\n\tfor _, a := range opts {\n\t\tvarargs = append(varargs, a)\n\t}\n\tret := m.ctrl.Call(m, \"EnableCertificateAuthority\", varargs...)\n\tret0, _ := ret[0].(*privateca.EnableCertificateAuthorityOperation)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// EnableCertificateAuthority indicates an expected call of EnableCertificateAuthority.\nfunc (mr *MockCertificateAuthorityClientMockRecorder) EnableCertificateAuthority(ctx, req any, opts ...any) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\tvarargs := append([]any{ctx, req}, opts...)\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"EnableCertificateAuthority\", reflect.TypeOf((*MockCertificateAuthorityClient)(nil).EnableCertificateAuthority), varargs...)\n}\n\n// FetchCertificateAuthorityCsr mocks base method.\nfunc (m *MockCertificateAuthorityClient) FetchCertificateAuthorityCsr(ctx context.Context, req *privatecapb.FetchCertificateAuthorityCsrRequest, opts ...gax.CallOption) (*privatecapb.FetchCertificateAuthorityCsrResponse, error) {\n\tm.ctrl.T.Helper()\n\tvarargs := []any{ctx, req}\n\tfor _, a := range opts {\n\t\tvarargs = append(varargs, a)\n\t}\n\tret := m.ctrl.Call(m, \"FetchCertificateAuthorityCsr\", varargs...)\n\tret0, _ := ret[0].(*privatecapb.FetchCertificateAuthorityCsrResponse)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// FetchCertificateAuthorityCsr indicates an expected call of FetchCertificateAuthorityCsr.\nfunc (mr *MockCertificateAuthorityClientMockRecorder) FetchCertificateAuthorityCsr(ctx, req any, opts ...any) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\tvarargs := append([]any{ctx, req}, opts...)\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"FetchCertificateAuthorityCsr\", reflect.TypeOf((*MockCertificateAuthorityClient)(nil).FetchCertificateAuthorityCsr), varargs...)\n}\n\n// GetCaPool mocks base method.\nfunc (m *MockCertificateAuthorityClient) GetCaPool(ctx context.Context, req *privatecapb.GetCaPoolRequest, opts ...gax.CallOption) (*privatecapb.CaPool, error) {\n\tm.ctrl.T.Helper()\n\tvarargs := []any{ctx, req}\n\tfor _, a := range opts {\n\t\tvarargs = append(varargs, a)\n\t}\n\tret := m.ctrl.Call(m, \"GetCaPool\", varargs...)\n\tret0, _ := ret[0].(*privatecapb.CaPool)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// GetCaPool indicates an expected call of GetCaPool.\nfunc (mr *MockCertificateAuthorityClientMockRecorder) GetCaPool(ctx, req any, opts ...any) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\tvarargs := append([]any{ctx, req}, opts...)\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"GetCaPool\", reflect.TypeOf((*MockCertificateAuthorityClient)(nil).GetCaPool), varargs...)\n}\n\n// GetCertificateAuthority mocks base method.\nfunc (m *MockCertificateAuthorityClient) GetCertificateAuthority(ctx context.Context, req *privatecapb.GetCertificateAuthorityRequest, opts ...gax.CallOption) (*privatecapb.CertificateAuthority, error) {\n\tm.ctrl.T.Helper()\n\tvarargs := []any{ctx, req}\n\tfor _, a := range opts {\n\t\tvarargs = append(varargs, a)\n\t}\n\tret := m.ctrl.Call(m, \"GetCertificateAuthority\", varargs...)\n\tret0, _ := ret[0].(*privatecapb.CertificateAuthority)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// GetCertificateAuthority indicates an expected call of GetCertificateAuthority.\nfunc (mr *MockCertificateAuthorityClientMockRecorder) GetCertificateAuthority(ctx, req any, opts ...any) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\tvarargs := append([]any{ctx, req}, opts...)\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"GetCertificateAuthority\", reflect.TypeOf((*MockCertificateAuthorityClient)(nil).GetCertificateAuthority), varargs...)\n}\n\n// RevokeCertificate mocks base method.\nfunc (m *MockCertificateAuthorityClient) RevokeCertificate(ctx context.Context, req *privatecapb.RevokeCertificateRequest, opts ...gax.CallOption) (*privatecapb.Certificate, error) {\n\tm.ctrl.T.Helper()\n\tvarargs := []any{ctx, req}\n\tfor _, a := range opts {\n\t\tvarargs = append(varargs, a)\n\t}\n\tret := m.ctrl.Call(m, \"RevokeCertificate\", varargs...)\n\tret0, _ := ret[0].(*privatecapb.Certificate)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// RevokeCertificate indicates an expected call of RevokeCertificate.\nfunc (mr *MockCertificateAuthorityClientMockRecorder) RevokeCertificate(ctx, req any, opts ...any) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\tvarargs := append([]any{ctx, req}, opts...)\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"RevokeCertificate\", reflect.TypeOf((*MockCertificateAuthorityClient)(nil).RevokeCertificate), varargs...)\n}\n"
  },
  {
    "path": "cas/cloudcas/mock_operation_server_test.go",
    "content": "// Code generated by MockGen. DO NOT EDIT.\n// Source: cloud.google.com/go/longrunning/autogen/longrunningpb (interfaces: OperationsServer)\n//\n// Generated by this command:\n//\n//\tmockgen -package cloudcas -mock_names=OperationsServer=MockOperationsServer -destination mock_operation_server_test.go cloud.google.com/go/longrunning/autogen/longrunningpb OperationsServer\n//\n\n// Package cloudcas is a generated GoMock package.\npackage cloudcas\n\nimport (\n\tcontext \"context\"\n\treflect \"reflect\"\n\n\tlongrunningpb \"cloud.google.com/go/longrunning/autogen/longrunningpb\"\n\tgomock \"go.uber.org/mock/gomock\"\n\temptypb \"google.golang.org/protobuf/types/known/emptypb\"\n)\n\n// MockOperationsServer is a mock of OperationsServer interface.\ntype MockOperationsServer struct {\n\tctrl     *gomock.Controller\n\trecorder *MockOperationsServerMockRecorder\n\tisgomock struct{}\n}\n\n// MockOperationsServerMockRecorder is the mock recorder for MockOperationsServer.\ntype MockOperationsServerMockRecorder struct {\n\tmock *MockOperationsServer\n}\n\n// NewMockOperationsServer creates a new mock instance.\nfunc NewMockOperationsServer(ctrl *gomock.Controller) *MockOperationsServer {\n\tmock := &MockOperationsServer{ctrl: ctrl}\n\tmock.recorder = &MockOperationsServerMockRecorder{mock}\n\treturn mock\n}\n\n// EXPECT returns an object that allows the caller to indicate expected use.\nfunc (m *MockOperationsServer) EXPECT() *MockOperationsServerMockRecorder {\n\treturn m.recorder\n}\n\n// CancelOperation mocks base method.\nfunc (m *MockOperationsServer) CancelOperation(arg0 context.Context, arg1 *longrunningpb.CancelOperationRequest) (*emptypb.Empty, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"CancelOperation\", arg0, arg1)\n\tret0, _ := ret[0].(*emptypb.Empty)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// CancelOperation indicates an expected call of CancelOperation.\nfunc (mr *MockOperationsServerMockRecorder) CancelOperation(arg0, arg1 any) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"CancelOperation\", reflect.TypeOf((*MockOperationsServer)(nil).CancelOperation), arg0, arg1)\n}\n\n// DeleteOperation mocks base method.\nfunc (m *MockOperationsServer) DeleteOperation(arg0 context.Context, arg1 *longrunningpb.DeleteOperationRequest) (*emptypb.Empty, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"DeleteOperation\", arg0, arg1)\n\tret0, _ := ret[0].(*emptypb.Empty)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// DeleteOperation indicates an expected call of DeleteOperation.\nfunc (mr *MockOperationsServerMockRecorder) DeleteOperation(arg0, arg1 any) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"DeleteOperation\", reflect.TypeOf((*MockOperationsServer)(nil).DeleteOperation), arg0, arg1)\n}\n\n// GetOperation mocks base method.\nfunc (m *MockOperationsServer) GetOperation(arg0 context.Context, arg1 *longrunningpb.GetOperationRequest) (*longrunningpb.Operation, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"GetOperation\", arg0, arg1)\n\tret0, _ := ret[0].(*longrunningpb.Operation)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// GetOperation indicates an expected call of GetOperation.\nfunc (mr *MockOperationsServerMockRecorder) GetOperation(arg0, arg1 any) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"GetOperation\", reflect.TypeOf((*MockOperationsServer)(nil).GetOperation), arg0, arg1)\n}\n\n// ListOperations mocks base method.\nfunc (m *MockOperationsServer) ListOperations(arg0 context.Context, arg1 *longrunningpb.ListOperationsRequest) (*longrunningpb.ListOperationsResponse, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"ListOperations\", arg0, arg1)\n\tret0, _ := ret[0].(*longrunningpb.ListOperationsResponse)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// ListOperations indicates an expected call of ListOperations.\nfunc (mr *MockOperationsServerMockRecorder) ListOperations(arg0, arg1 any) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"ListOperations\", reflect.TypeOf((*MockOperationsServer)(nil).ListOperations), arg0, arg1)\n}\n\n// WaitOperation mocks base method.\nfunc (m *MockOperationsServer) WaitOperation(arg0 context.Context, arg1 *longrunningpb.WaitOperationRequest) (*longrunningpb.Operation, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"WaitOperation\", arg0, arg1)\n\tret0, _ := ret[0].(*longrunningpb.Operation)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// WaitOperation indicates an expected call of WaitOperation.\nfunc (mr *MockOperationsServerMockRecorder) WaitOperation(arg0, arg1 any) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"WaitOperation\", reflect.TypeOf((*MockOperationsServer)(nil).WaitOperation), arg0, arg1)\n}\n"
  },
  {
    "path": "cas/softcas/softcas.go",
    "content": "package softcas\n\nimport (\n\t\"context\"\n\t\"crypto\"\n\t\"crypto/rand\"\n\t\"crypto/rsa\"\n\t\"crypto/x509\"\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n\n\t\"go.step.sm/crypto/kms\"\n\tkmsapi \"go.step.sm/crypto/kms/apiv1\"\n\t\"go.step.sm/crypto/x509util\"\n\n\t\"github.com/smallstep/certificates/cas/apiv1\"\n)\n\nfunc init() {\n\tapiv1.Register(apiv1.SoftCAS, func(ctx context.Context, opts apiv1.Options) (apiv1.CertificateAuthorityService, error) {\n\t\treturn New(ctx, opts)\n\t})\n}\n\nvar now = time.Now\n\n// SoftCAS implements a Certificate Authority Service using Golang or KMS\n// crypto. This is the default CAS used in step-ca.\ntype SoftCAS struct {\n\tCertificateChain  []*x509.Certificate\n\tSigner            crypto.Signer\n\tCertificateSigner func() ([]*x509.Certificate, crypto.Signer, error)\n\tKeyManager        kms.KeyManager\n}\n\n// New creates a new CertificateAuthorityService implementation using Golang or KMS\n// crypto.\nfunc New(_ context.Context, opts apiv1.Options) (*SoftCAS, error) {\n\tif !opts.IsCreator {\n\t\tswitch {\n\t\tcase len(opts.CertificateChain) == 0 && opts.CertificateSigner == nil:\n\t\t\treturn nil, errors.New(\"softCAS 'CertificateChain' cannot be nil\")\n\t\tcase opts.Signer == nil && opts.CertificateSigner == nil:\n\t\t\treturn nil, errors.New(\"softCAS 'signer' cannot be nil\")\n\t\t}\n\t}\n\treturn &SoftCAS{\n\t\tCertificateChain:  opts.CertificateChain,\n\t\tSigner:            opts.Signer,\n\t\tCertificateSigner: opts.CertificateSigner,\n\t\tKeyManager:        opts.KeyManager,\n\t}, nil\n}\n\n// Type returns the type of this CertificateAuthorityService.\nfunc (c *SoftCAS) Type() apiv1.Type {\n\treturn apiv1.SoftCAS\n}\n\n// GetSigner implements [apiv1.CertificateAuthoritySigner] and returns a\n// [crypto.Signer] with the intermediate key.\nfunc (c *SoftCAS) GetSigner() (crypto.Signer, error) {\n\t_, signer, err := c.getCertSigner()\n\treturn signer, err\n}\n\n// CreateCertificate signs a new certificate using Golang or KMS crypto.\nfunc (c *SoftCAS) CreateCertificate(req *apiv1.CreateCertificateRequest) (*apiv1.CreateCertificateResponse, error) {\n\tswitch {\n\tcase req.Template == nil:\n\t\treturn nil, errors.New(\"createCertificateRequest `template` cannot be nil\")\n\tcase req.Lifetime == 0:\n\t\treturn nil, errors.New(\"createCertificateRequest `lifetime` cannot be 0\")\n\t}\n\n\tt := now()\n\n\t// Provisioners can also set specific values.\n\tif req.Template.NotBefore.IsZero() {\n\t\treq.Template.NotBefore = t.Add(-1 * req.Backdate)\n\t}\n\tif req.Template.NotAfter.IsZero() {\n\t\treq.Template.NotAfter = t.Add(req.Lifetime)\n\t}\n\n\tchain, signer, err := c.getCertSigner()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treq.Template.Issuer = chain[0].Subject\n\n\tcert, err := createCertificate(req.Template, chain[0], req.Template.PublicKey, signer)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &apiv1.CreateCertificateResponse{\n\t\tCertificate:      cert,\n\t\tCertificateChain: chain,\n\t}, nil\n}\n\n// RenewCertificate signs the given certificate template using Golang or KMS crypto.\nfunc (c *SoftCAS) RenewCertificate(req *apiv1.RenewCertificateRequest) (*apiv1.RenewCertificateResponse, error) {\n\tswitch {\n\tcase req.Template == nil:\n\t\treturn nil, errors.New(\"createCertificateRequest `template` cannot be nil\")\n\tcase req.Lifetime == 0:\n\t\treturn nil, errors.New(\"createCertificateRequest `lifetime` cannot be 0\")\n\t}\n\n\tt := now()\n\treq.Template.NotBefore = t.Add(-1 * req.Backdate)\n\treq.Template.NotAfter = t.Add(req.Lifetime)\n\n\tchain, signer, err := c.getCertSigner()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treq.Template.Issuer = chain[0].Subject\n\n\tcert, err := createCertificate(req.Template, chain[0], req.Template.PublicKey, signer)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &apiv1.RenewCertificateResponse{\n\t\tCertificate:      cert,\n\t\tCertificateChain: chain,\n\t}, nil\n}\n\n// RevokeCertificate revokes the given certificate in step-ca. In SoftCAS this\n// operation is a no-op as the actual revoke will happen when we store the entry\n// in the db.\nfunc (c *SoftCAS) RevokeCertificate(req *apiv1.RevokeCertificateRequest) (*apiv1.RevokeCertificateResponse, error) {\n\tchain, _, err := c.getCertSigner()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &apiv1.RevokeCertificateResponse{\n\t\tCertificate:      req.Certificate,\n\t\tCertificateChain: chain,\n\t}, nil\n}\n\n// CreateCRL will create a new CRL based on the RevocationList passed to it\nfunc (c *SoftCAS) CreateCRL(req *apiv1.CreateCRLRequest) (*apiv1.CreateCRLResponse, error) {\n\tcertChain, signer, err := c.getCertSigner()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\trevocationListBytes, err := x509.CreateRevocationList(rand.Reader, req.RevocationList, certChain[0], signer)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &apiv1.CreateCRLResponse{CRL: revocationListBytes}, nil\n}\n\n// CreateCertificateAuthority creates a root or an intermediate certificate.\nfunc (c *SoftCAS) CreateCertificateAuthority(req *apiv1.CreateCertificateAuthorityRequest) (*apiv1.CreateCertificateAuthorityResponse, error) {\n\tswitch {\n\tcase req.Template == nil:\n\t\treturn nil, errors.New(\"createCertificateAuthorityRequest `template` cannot be nil\")\n\tcase req.Lifetime == 0:\n\t\treturn nil, errors.New(\"createCertificateAuthorityRequest `lifetime` cannot be 0\")\n\tcase req.Type == apiv1.IntermediateCA && req.Parent == nil:\n\t\treturn nil, errors.New(\"createCertificateAuthorityRequest `parent` cannot be nil\")\n\tcase req.Type == apiv1.IntermediateCA && req.Parent.Certificate == nil:\n\t\treturn nil, errors.New(\"createCertificateAuthorityRequest `parent.template` cannot be nil\")\n\tcase req.Type == apiv1.IntermediateCA && req.Parent.Signer == nil:\n\t\treturn nil, errors.New(\"createCertificateAuthorityRequest `parent.signer` cannot be nil\")\n\t}\n\n\tkey, err := c.createKey(req.CreateKey)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tsigner, err := c.createSigner(&key.CreateSignerRequest)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tt := now()\n\tif req.Template.NotBefore.IsZero() {\n\t\treq.Template.NotBefore = t.Add(-1 * req.Backdate)\n\t}\n\tif req.Template.NotAfter.IsZero() {\n\t\treq.Template.NotAfter = t.Add(req.Lifetime)\n\t}\n\n\tvar cert *x509.Certificate\n\tswitch req.Type {\n\tcase apiv1.RootCA:\n\t\tcert, err = createCertificate(req.Template, req.Template, signer.Public(), signer)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\tcase apiv1.IntermediateCA:\n\t\tcert, err = createCertificate(req.Template, req.Parent.Certificate, signer.Public(), req.Parent.Signer)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\tdefault:\n\t\treturn nil, errors.Errorf(\"createCertificateAuthorityRequest `type=%d' is invalid or not supported\", req.Type)\n\t}\n\n\t// Add the parent\n\tvar chain []*x509.Certificate\n\tif req.Parent != nil {\n\t\tchain = append(chain, req.Parent.Certificate)\n\t\tchain = append(chain, req.Parent.CertificateChain...)\n\t}\n\n\treturn &apiv1.CreateCertificateAuthorityResponse{\n\t\tName:             cert.Subject.CommonName,\n\t\tCertificate:      cert,\n\t\tCertificateChain: chain,\n\t\tKeyName:          key.Name,\n\t\tPublicKey:        key.PublicKey,\n\t\tPrivateKey:       key.PrivateKey,\n\t\tSigner:           signer,\n\t}, nil\n}\n\n// initializeKeyManager initializes the default key manager if was not given.\nfunc (c *SoftCAS) initializeKeyManager() (err error) {\n\tif c.KeyManager == nil {\n\t\tc.KeyManager, err = kms.New(context.Background(), kmsapi.Options{\n\t\t\tType: kmsapi.DefaultKMS,\n\t\t})\n\t}\n\treturn\n}\n\n// getCertSigner returns the certificate chain and signer to use.\nfunc (c *SoftCAS) getCertSigner() ([]*x509.Certificate, crypto.Signer, error) {\n\tif c.CertificateSigner != nil {\n\t\treturn c.CertificateSigner()\n\t}\n\treturn c.CertificateChain, c.Signer, nil\n}\n\n// createKey uses the configured kms to create a key.\nfunc (c *SoftCAS) createKey(req *kmsapi.CreateKeyRequest) (*kmsapi.CreateKeyResponse, error) {\n\tif err := c.initializeKeyManager(); err != nil {\n\t\treturn nil, err\n\t}\n\tif req == nil {\n\t\treq = &kmsapi.CreateKeyRequest{\n\t\t\tSignatureAlgorithm: kmsapi.ECDSAWithSHA256,\n\t\t}\n\t}\n\treturn c.KeyManager.CreateKey(req)\n}\n\n// createSigner uses the configured kms to create a singer\nfunc (c *SoftCAS) createSigner(req *kmsapi.CreateSignerRequest) (crypto.Signer, error) {\n\tif err := c.initializeKeyManager(); err != nil {\n\t\treturn nil, err\n\t}\n\treturn c.KeyManager.CreateSigner(req)\n}\n\n// createCertificate sets the SignatureAlgorithm of the template if necessary\n// and calls x509util.CreateCertificate.\nfunc createCertificate(template, parent *x509.Certificate, pub crypto.PublicKey, signer crypto.Signer) (*x509.Certificate, error) {\n\t// Signers can specify the signature algorithm. This is especially important\n\t// when x509.CreateCertificate attempts to validate a RSAPSS signature.\n\tif template.SignatureAlgorithm == 0 {\n\t\tif sa, ok := signer.(apiv1.SignatureAlgorithmGetter); ok {\n\t\t\ttemplate.SignatureAlgorithm = sa.SignatureAlgorithm()\n\t\t} else if _, ok := parent.PublicKey.(*rsa.PublicKey); ok {\n\t\t\t// For RSA issuers, only overwrite the default algorithm is the\n\t\t\t// intermediate is signed with an RSA signature scheme.\n\t\t\tif isRSA(parent.SignatureAlgorithm) {\n\t\t\t\ttemplate.SignatureAlgorithm = parent.SignatureAlgorithm\n\t\t\t}\n\t\t}\n\t}\n\treturn x509util.CreateCertificate(template, parent, pub, signer)\n}\n\nfunc isRSA(sa x509.SignatureAlgorithm) bool {\n\tswitch sa {\n\tcase x509.SHA256WithRSA, x509.SHA384WithRSA, x509.SHA512WithRSA:\n\t\treturn true\n\tcase x509.SHA256WithRSAPSS, x509.SHA384WithRSAPSS, x509.SHA512WithRSAPSS:\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n"
  },
  {
    "path": "cas/softcas/softcas_test.go",
    "content": "package softcas\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto\"\n\t\"crypto/ecdsa\"\n\t\"crypto/elliptic\"\n\t\"crypto/rand\"\n\t\"crypto/rsa\"\n\t\"crypto/x509\"\n\t\"crypto/x509/pkix\"\n\t\"fmt\"\n\t\"io\"\n\t\"math/big\"\n\t\"reflect\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n\t\"github.com/smallstep/certificates/cas/apiv1\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"go.step.sm/crypto/kms\"\n\tkmsapi \"go.step.sm/crypto/kms/apiv1\"\n\t\"go.step.sm/crypto/minica\"\n\t\"go.step.sm/crypto/pemutil\"\n\t\"go.step.sm/crypto/x509util\"\n)\n\nvar (\n\ttestIntermediatePem = `-----BEGIN CERTIFICATE-----\nMIIBPjCB8aADAgECAhAk4aPIlsVvQg3gveApc3mIMAUGAytlcDAeMRwwGgYDVQQD\nExNTbWFsbHN0ZXAgVW5pdCBUZXN0MB4XDTIwMDkxNjAyMDgwMloXDTMwMDkxNDAy\nMDgwMlowHjEcMBoGA1UEAxMTU21hbGxzdGVwIFVuaXQgVGVzdDAqMAUGAytlcAMh\nANLs3JCzECR29biut0NDsaLnh0BGij5eJx6VkdJPfS/ko0UwQzAOBgNVHQ8BAf8E\nBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBATAdBgNVHQ4EFgQUup5qpZFMAFdgK7RB\nxNzmUaQM8YwwBQYDK2VwA0EAAwcW25E/6bchyKwp3RRK1GXiPMDCc+hsTJxuOLWy\nYM7ga829dU8X4pRcEEAcBndqCED/502excjEK7U9vCkFCg==\n-----END CERTIFICATE-----`\n\n\ttestIntermediateKeyPem = `-----BEGIN PRIVATE KEY-----\nMC4CAQAwBQYDK2VwBCIEII9ZckcrDKlbhZKR0jp820Uz6mOMLFsq2JhI+Tl7WJwH\n-----END PRIVATE KEY-----`\n)\n\nvar (\n\terrTest      = errors.New(\"test error\")\n\ttestIssuer   = mustIssuer()\n\ttestSigner   = mustSigner()\n\ttestTemplate = &x509.Certificate{\n\t\tSubject:      pkix.Name{CommonName: \"test.smallstep.com\"},\n\t\tDNSNames:     []string{\"test.smallstep.com\"},\n\t\tKeyUsage:     x509.KeyUsageDigitalSignature,\n\t\tExtKeyUsage:  []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},\n\t\tPublicKey:    testSigner.Public(),\n\t\tSerialNumber: big.NewInt(1234),\n\t}\n\ttestRootTemplate = &x509.Certificate{\n\t\tSubject:               pkix.Name{CommonName: \"Test Root CA\"},\n\t\tKeyUsage:              x509.KeyUsageCRLSign | x509.KeyUsageCertSign,\n\t\tPublicKey:             testSigner.Public(),\n\t\tBasicConstraintsValid: true,\n\t\tIsCA:                  true,\n\t\tMaxPathLen:            1,\n\t\tSerialNumber:          big.NewInt(1234),\n\t}\n\ttestIntermediateTemplate = &x509.Certificate{\n\t\tSubject:               pkix.Name{CommonName: \"Test Intermediate CA\"},\n\t\tKeyUsage:              x509.KeyUsageCRLSign | x509.KeyUsageCertSign,\n\t\tPublicKey:             testSigner.Public(),\n\t\tBasicConstraintsValid: true,\n\t\tIsCA:                  true,\n\t\tMaxPathLen:            0,\n\t\tMaxPathLenZero:        true,\n\t\tSerialNumber:          big.NewInt(1234),\n\t}\n\ttestNow                        = time.Now()\n\ttestSignedTemplate             = mustSign(testTemplate, testIssuer, testNow, testNow.Add(24*time.Hour))\n\ttestSignedRootTemplate         = mustSign(testRootTemplate, testRootTemplate, testNow, testNow.Add(24*time.Hour))\n\ttestSignedIntermediateTemplate = mustSign(testIntermediateTemplate, testSignedRootTemplate, testNow, testNow.Add(24*time.Hour))\n\ttestCertificateSigner          = func() ([]*x509.Certificate, crypto.Signer, error) {\n\t\treturn []*x509.Certificate{testIssuer}, testSigner, nil\n\t}\n\ttestFailCertificateSigner = func() ([]*x509.Certificate, crypto.Signer, error) {\n\t\treturn nil, nil, errTest\n\t}\n)\n\ntype signatureAlgorithmSigner struct {\n\tcrypto.Signer\n\talgorithm x509.SignatureAlgorithm\n}\n\nfunc (s *signatureAlgorithmSigner) SignatureAlgorithm() x509.SignatureAlgorithm {\n\treturn s.algorithm\n}\n\ntype mockKeyManager struct {\n\tsigner          crypto.Signer\n\terrGetPublicKey error\n\terrCreateKey    error\n\terrCreatesigner error\n\terrClose        error\n}\n\nfunc (m *mockKeyManager) GetPublicKey(*kmsapi.GetPublicKeyRequest) (crypto.PublicKey, error) {\n\tsigner := testSigner\n\tif m.signer != nil {\n\t\tsigner = m.signer\n\t}\n\treturn signer.Public(), m.errGetPublicKey\n}\n\nfunc (m *mockKeyManager) CreateKey(req *kmsapi.CreateKeyRequest) (*kmsapi.CreateKeyResponse, error) {\n\tsigner := testSigner\n\tif m.signer != nil {\n\t\tsigner = m.signer\n\t}\n\treturn &kmsapi.CreateKeyResponse{\n\t\tName:       req.Name,\n\t\tPrivateKey: signer,\n\t\tPublicKey:  signer.Public(),\n\t}, m.errCreateKey\n}\n\nfunc (m *mockKeyManager) CreateSigner(*kmsapi.CreateSignerRequest) (crypto.Signer, error) {\n\tsigner := testSigner\n\tif m.signer != nil {\n\t\tsigner = m.signer\n\t}\n\treturn signer, m.errCreatesigner\n}\n\nfunc (m *mockKeyManager) CreateDecrypter(*kmsapi.CreateDecrypterRequest) (crypto.Decrypter, error) {\n\treturn nil, nil\n}\n\nfunc (m *mockKeyManager) Close() error {\n\treturn m.errClose\n}\n\ntype badSigner struct{}\n\nfunc (b *badSigner) Public() crypto.PublicKey {\n\treturn testSigner.Public()\n}\n\nfunc (b *badSigner) Sign(_ io.Reader, _ []byte, _ crypto.SignerOpts) ([]byte, error) {\n\treturn nil, fmt.Errorf(\"💥\")\n}\n\n//nolint:gocritic // ignore sloppy test func name\nfunc mockNow(t *testing.T) {\n\ttmp := now\n\tnow = func() time.Time {\n\t\treturn testNow\n\t}\n\tt.Cleanup(func() {\n\t\tnow = tmp\n\t})\n}\n\nfunc mustIssuer() *x509.Certificate {\n\tv, err := pemutil.Parse([]byte(testIntermediatePem))\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn v.(*x509.Certificate)\n}\n\nfunc mustSigner() crypto.Signer {\n\tv, err := pemutil.Parse([]byte(testIntermediateKeyPem))\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn v.(crypto.Signer)\n}\n\nfunc mustSign(template, parent *x509.Certificate, notBefore, notAfter time.Time) *x509.Certificate {\n\ttmpl := *template\n\ttmpl.NotBefore = notBefore\n\ttmpl.NotAfter = notAfter\n\ttmpl.Issuer = parent.Subject\n\tcert, err := x509util.CreateCertificate(&tmpl, parent, tmpl.PublicKey, testSigner)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn cert\n}\n\nfunc setTeeReader(t *testing.T, w *bytes.Buffer) {\n\tt.Helper()\n\treader := rand.Reader\n\tt.Cleanup(func() {\n\t\trand.Reader = reader\n\t})\n\trand.Reader = io.TeeReader(reader, w)\n}\n\nfunc TestNew(t *testing.T) {\n\tassertEqual := func(x, y interface{}) bool {\n\t\treturn reflect.DeepEqual(x, y) || fmt.Sprintf(\"%#v\", x) == fmt.Sprintf(\"%#v\", y)\n\t}\n\n\ttype args struct {\n\t\tctx  context.Context\n\t\topts apiv1.Options\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\targs    args\n\t\twant    *SoftCAS\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", args{context.Background(), apiv1.Options{CertificateChain: []*x509.Certificate{testIssuer}, Signer: testSigner}}, &SoftCAS{CertificateChain: []*x509.Certificate{testIssuer}, Signer: testSigner}, false},\n\t\t{\"ok with callback\", args{context.Background(), apiv1.Options{CertificateSigner: testCertificateSigner}}, &SoftCAS{CertificateSigner: testCertificateSigner}, false},\n\t\t{\"fail no issuer\", args{context.Background(), apiv1.Options{Signer: testSigner}}, nil, true},\n\t\t{\"fail no signer\", args{context.Background(), apiv1.Options{CertificateChain: []*x509.Certificate{testIssuer}}}, nil, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := New(tt.args.ctx, tt.args.opts)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"New() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !assertEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"New() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNew_register(t *testing.T) {\n\tnewFn, ok := apiv1.LoadCertificateAuthorityServiceNewFunc(apiv1.SoftCAS)\n\tif !ok {\n\t\tt.Error(\"apiv1.LoadCertificateAuthorityServiceNewFunc(apiv1.SoftCAS) was not found\")\n\t\treturn\n\t}\n\n\twant := &SoftCAS{\n\t\tCertificateChain: []*x509.Certificate{testIssuer},\n\t\tSigner:           testSigner,\n\t}\n\n\tgot, err := newFn(context.Background(), apiv1.Options{CertificateChain: []*x509.Certificate{testIssuer}, Signer: testSigner})\n\tif err != nil {\n\t\tt.Errorf(\"New() error = %v\", err)\n\t\treturn\n\t}\n\n\tif !reflect.DeepEqual(got, want) {\n\t\tt.Errorf(\"New() = %v, want %v\", got, want)\n\t}\n}\n\nfunc TestSoftCAS_Type(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\twant apiv1.Type\n\t}{\n\t\t{\"ok\", apiv1.SoftCAS},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tc := &SoftCAS{}\n\t\t\tif got := c.Type(); got != tt.want {\n\t\t\t\tt.Errorf(\"SoftCAS.Type() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSoftCAS_GetSigner(t *testing.T) {\n\tca, err := minica.New()\n\trequire.NoError(t, err)\n\n\ttype fields struct {\n\t\tCertificateChain  []*x509.Certificate\n\t\tSigner            crypto.Signer\n\t\tCertificateSigner func() ([]*x509.Certificate, crypto.Signer, error)\n\t\tKeyManager        kms.KeyManager\n\t}\n\ttests := []struct {\n\t\tname      string\n\t\tfields    fields\n\t\twant      crypto.Signer\n\t\tassertion assert.ErrorAssertionFunc\n\t}{\n\t\t{\"ok signer\", fields{[]*x509.Certificate{ca.Intermediate}, ca.Signer, nil, nil}, ca.Signer, assert.NoError},\n\t\t{\"ok certificateSigner\", fields{[]*x509.Certificate{ca.Intermediate}, nil, func() ([]*x509.Certificate, crypto.Signer, error) {\n\t\t\treturn []*x509.Certificate{ca.Intermediate}, ca.Signer, nil\n\t\t}, nil}, ca.Signer, assert.NoError},\n\t\t{\"fail certificateSigner\", fields{[]*x509.Certificate{ca.Intermediate}, nil, func() ([]*x509.Certificate, crypto.Signer, error) {\n\t\t\treturn nil, nil, apiv1.NotImplementedError{}\n\t\t}, nil}, nil, assert.Error},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tc := &SoftCAS{\n\t\t\t\tCertificateChain:  tt.fields.CertificateChain,\n\t\t\t\tSigner:            tt.fields.Signer,\n\t\t\t\tCertificateSigner: tt.fields.CertificateSigner,\n\t\t\t\tKeyManager:        tt.fields.KeyManager,\n\t\t\t}\n\t\t\tgot, err := c.GetSigner()\n\t\t\ttt.assertion(t, err)\n\t\t\tassert.Equal(t, tt.want, got)\n\t\t})\n\t}\n}\n\nfunc TestSoftCAS_CreateCertificate(t *testing.T) {\n\tmockNow(t)\n\t// Set rand.Reader to EOF\n\tbuf := new(bytes.Buffer)\n\tsetTeeReader(t, buf)\n\trand.Reader = buf\n\n\ttmplNotBefore := *testTemplate\n\ttmplNotBefore.NotBefore = testNow\n\n\ttmplWithLifetime := *testTemplate\n\ttmplWithLifetime.NotBefore = testNow\n\ttmplWithLifetime.NotAfter = testNow.Add(24 * time.Hour)\n\n\ttmplNoSerial := *testTemplate\n\ttmplNoSerial.SerialNumber = nil\n\n\tsaTemplate := *testSignedTemplate\n\tsaTemplate.SignatureAlgorithm = 0\n\tsaSigner := &signatureAlgorithmSigner{\n\t\tSigner:    testSigner,\n\t\talgorithm: x509.PureEd25519,\n\t}\n\n\ttype fields struct {\n\t\tIssuer            *x509.Certificate\n\t\tSigner            crypto.Signer\n\t\tCertificateSigner func() ([]*x509.Certificate, crypto.Signer, error)\n\t}\n\ttype args struct {\n\t\treq *apiv1.CreateCertificateRequest\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tfields  fields\n\t\targs    args\n\t\twant    *apiv1.CreateCertificateResponse\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", fields{testIssuer, testSigner, nil}, args{&apiv1.CreateCertificateRequest{\n\t\t\tTemplate: testTemplate, Lifetime: 24 * time.Hour,\n\t\t}}, &apiv1.CreateCertificateResponse{\n\t\t\tCertificate:      testSignedTemplate,\n\t\t\tCertificateChain: []*x509.Certificate{testIssuer},\n\t\t}, false},\n\t\t{\"ok signature algorithm\", fields{testIssuer, saSigner, nil}, args{&apiv1.CreateCertificateRequest{\n\t\t\tTemplate: &saTemplate, Lifetime: 24 * time.Hour,\n\t\t}}, &apiv1.CreateCertificateResponse{\n\t\t\tCertificate:      testSignedTemplate,\n\t\t\tCertificateChain: []*x509.Certificate{testIssuer},\n\t\t}, false},\n\t\t{\"ok with notBefore\", fields{testIssuer, testSigner, nil}, args{&apiv1.CreateCertificateRequest{\n\t\t\tTemplate: &tmplNotBefore, Lifetime: 24 * time.Hour,\n\t\t}}, &apiv1.CreateCertificateResponse{\n\t\t\tCertificate:      testSignedTemplate,\n\t\t\tCertificateChain: []*x509.Certificate{testIssuer},\n\t\t}, false},\n\t\t{\"ok with notBefore+notAfter\", fields{testIssuer, testSigner, nil}, args{&apiv1.CreateCertificateRequest{\n\t\t\tTemplate: &tmplWithLifetime, Lifetime: 24 * time.Hour,\n\t\t}}, &apiv1.CreateCertificateResponse{\n\t\t\tCertificate:      testSignedTemplate,\n\t\t\tCertificateChain: []*x509.Certificate{testIssuer},\n\t\t}, false},\n\t\t{\"ok with callback\", fields{nil, nil, testCertificateSigner}, args{&apiv1.CreateCertificateRequest{\n\t\t\tTemplate: testTemplate, Lifetime: 24 * time.Hour,\n\t\t}}, &apiv1.CreateCertificateResponse{\n\t\t\tCertificate:      testSignedTemplate,\n\t\t\tCertificateChain: []*x509.Certificate{testIssuer},\n\t\t}, false},\n\t\t{\"fail template\", fields{testIssuer, testSigner, nil}, args{&apiv1.CreateCertificateRequest{Lifetime: 24 * time.Hour}}, nil, true},\n\t\t{\"fail lifetime\", fields{testIssuer, testSigner, nil}, args{&apiv1.CreateCertificateRequest{Template: testTemplate}}, nil, true},\n\t\t{\"fail CreateCertificate\", fields{testIssuer, testSigner, nil}, args{&apiv1.CreateCertificateRequest{\n\t\t\tTemplate: &tmplNoSerial,\n\t\t\tLifetime: 24 * time.Hour,\n\t\t}}, nil, true},\n\t\t{\"fail with callback\", fields{nil, nil, testFailCertificateSigner}, args{&apiv1.CreateCertificateRequest{\n\t\t\tTemplate: testTemplate, Lifetime: 24 * time.Hour,\n\t\t}}, nil, true},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tc := &SoftCAS{\n\t\t\t\tCertificateChain:  []*x509.Certificate{tt.fields.Issuer},\n\t\t\t\tSigner:            tt.fields.Signer,\n\t\t\t\tCertificateSigner: tt.fields.CertificateSigner,\n\t\t\t}\n\t\t\tgot, err := c.CreateCertificate(tt.args.req)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"SoftCAS.CreateCertificate() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"SoftCAS.CreateCertificate() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSoftCAS_CreateCertificate_pss(t *testing.T) {\n\tsigner, err := rsa.GenerateKey(rand.Reader, 2048)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tnow := time.Now()\n\ttemplate := &x509.Certificate{\n\t\tSubject:               pkix.Name{CommonName: \"Test Root CA\"},\n\t\tKeyUsage:              x509.KeyUsageCRLSign | x509.KeyUsageCertSign,\n\t\tPublicKey:             signer.Public(),\n\t\tBasicConstraintsValid: true,\n\t\tIsCA:                  true,\n\t\tMaxPathLen:            0,\n\t\tSerialNumber:          big.NewInt(1234),\n\t\tSignatureAlgorithm:    x509.SHA256WithRSAPSS,\n\t\tNotBefore:             now,\n\t\tNotAfter:              now.Add(24 * time.Hour),\n\t}\n\n\tiss, err := x509util.CreateCertificate(template, template, signer.Public(), signer)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif iss.SignatureAlgorithm != x509.SHA256WithRSAPSS {\n\t\tt.Errorf(\"Certificate.SignatureAlgorithm = %v, want %v\", iss.SignatureAlgorithm, x509.SHA256WithRSAPSS)\n\t}\n\n\tc := &SoftCAS{\n\t\tCertificateChain: []*x509.Certificate{iss},\n\t\tSigner:           signer,\n\t}\n\tcert, err := c.CreateCertificate(&apiv1.CreateCertificateRequest{\n\t\tTemplate: &x509.Certificate{\n\t\t\tSubject:      pkix.Name{CommonName: \"test.smallstep.com\"},\n\t\t\tDNSNames:     []string{\"test.smallstep.com\"},\n\t\t\tKeyUsage:     x509.KeyUsageDigitalSignature,\n\t\t\tExtKeyUsage:  []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},\n\t\t\tPublicKey:    testSigner.Public(),\n\t\t\tSerialNumber: big.NewInt(1234),\n\t\t},\n\t\tLifetime: time.Hour, Backdate: time.Minute,\n\t})\n\tif err != nil {\n\t\tt.Fatalf(\"SoftCAS.CreateCertificate() error = %v\", err)\n\t}\n\tif cert.Certificate.SignatureAlgorithm != x509.SHA256WithRSAPSS {\n\t\tt.Errorf(\"Certificate.SignatureAlgorithm = %v, want %v\", iss.SignatureAlgorithm, x509.SHA256WithRSAPSS)\n\t}\n\n\tpool := x509.NewCertPool()\n\tpool.AddCert(iss)\n\tif _, err = cert.Certificate.Verify(x509.VerifyOptions{\n\t\tCurrentTime: time.Now(),\n\t\tRoots:       pool,\n\t\tKeyUsages:   []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},\n\t}); err != nil {\n\t\tt.Errorf(\"Certificate.Verify() error = %v\", err)\n\t}\n}\n\nfunc TestSoftCAS_CreateCertificate_ec_rsa(t *testing.T) {\n\trootSigner, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tintSigner, err := rsa.GenerateKey(rand.Reader, 2048)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tnow := time.Now()\n\n\t// Root template\n\ttemplate := &x509.Certificate{\n\t\tSubject:               pkix.Name{CommonName: \"Test Root CA\"},\n\t\tKeyUsage:              x509.KeyUsageCRLSign | x509.KeyUsageCertSign,\n\t\tPublicKey:             rootSigner.Public(),\n\t\tBasicConstraintsValid: true,\n\t\tIsCA:                  true,\n\t\tMaxPathLen:            0,\n\t\tSerialNumber:          big.NewInt(1234),\n\t\tNotBefore:             now,\n\t\tNotAfter:              now.Add(24 * time.Hour),\n\t}\n\n\troot, err := x509util.CreateCertificate(template, template, rootSigner.Public(), rootSigner)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\t// Intermediate template\n\ttemplate = &x509.Certificate{\n\t\tSubject:               pkix.Name{CommonName: \"Test Intermediate CA\"},\n\t\tKeyUsage:              x509.KeyUsageCRLSign | x509.KeyUsageCertSign,\n\t\tPublicKey:             intSigner.Public(),\n\t\tBasicConstraintsValid: true,\n\t\tIsCA:                  true,\n\t\tMaxPathLen:            0,\n\t\tSerialNumber:          big.NewInt(1234),\n\t\tNotBefore:             now,\n\t\tNotAfter:              now.Add(24 * time.Hour),\n\t}\n\n\tiss, err := x509util.CreateCertificate(template, root, intSigner.Public(), rootSigner)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tif iss.SignatureAlgorithm != x509.ECDSAWithSHA256 {\n\t\tt.Errorf(\"Certificate.SignatureAlgorithm = %v, want %v\", iss.SignatureAlgorithm, x509.ECDSAWithSHA256)\n\t}\n\n\tc := &SoftCAS{\n\t\tCertificateChain: []*x509.Certificate{iss},\n\t\tSigner:           intSigner,\n\t}\n\tcert, err := c.CreateCertificate(&apiv1.CreateCertificateRequest{\n\t\tTemplate: &x509.Certificate{\n\t\t\tSubject:      pkix.Name{CommonName: \"test.smallstep.com\"},\n\t\t\tDNSNames:     []string{\"test.smallstep.com\"},\n\t\t\tKeyUsage:     x509.KeyUsageDigitalSignature,\n\t\t\tExtKeyUsage:  []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},\n\t\t\tPublicKey:    testSigner.Public(),\n\t\t\tSerialNumber: big.NewInt(1234),\n\t\t},\n\t\tLifetime: time.Hour, Backdate: time.Minute,\n\t})\n\tif err != nil {\n\t\tt.Fatalf(\"SoftCAS.CreateCertificate() error = %v\", err)\n\t}\n\tif cert.Certificate.SignatureAlgorithm != x509.SHA256WithRSA {\n\t\tt.Errorf(\"Certificate.SignatureAlgorithm = %v, want %v\", iss.SignatureAlgorithm, x509.SHA256WithRSAPSS)\n\t}\n\n\troots := x509.NewCertPool()\n\troots.AddCert(root)\n\tintermediates := x509.NewCertPool()\n\tintermediates.AddCert(iss)\n\tif _, err = cert.Certificate.Verify(x509.VerifyOptions{\n\t\tCurrentTime:   time.Now(),\n\t\tRoots:         roots,\n\t\tIntermediates: intermediates,\n\t\tKeyUsages:     []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},\n\t}); err != nil {\n\t\tt.Errorf(\"Certificate.Verify() error = %v\", err)\n\t}\n}\n\nfunc TestSoftCAS_RenewCertificate(t *testing.T) {\n\tmockNow(t)\n\n\t// Set rand.Reader to EOF\n\tbuf := new(bytes.Buffer)\n\tsetTeeReader(t, buf)\n\trand.Reader = buf\n\n\ttmplNoSerial := *testTemplate\n\ttmplNoSerial.SerialNumber = nil\n\n\tsaSigner := &signatureAlgorithmSigner{\n\t\tSigner:    testSigner,\n\t\talgorithm: x509.PureEd25519,\n\t}\n\n\ttype fields struct {\n\t\tIssuer            *x509.Certificate\n\t\tSigner            crypto.Signer\n\t\tCertificateSigner func() ([]*x509.Certificate, crypto.Signer, error)\n\t}\n\ttype args struct {\n\t\treq *apiv1.RenewCertificateRequest\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tfields  fields\n\t\targs    args\n\t\twant    *apiv1.RenewCertificateResponse\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", fields{testIssuer, testSigner, nil}, args{&apiv1.RenewCertificateRequest{\n\t\t\tTemplate: testTemplate, Lifetime: 24 * time.Hour,\n\t\t}}, &apiv1.RenewCertificateResponse{\n\t\t\tCertificate:      testSignedTemplate,\n\t\t\tCertificateChain: []*x509.Certificate{testIssuer},\n\t\t}, false},\n\t\t{\"ok signature algorithm\", fields{testIssuer, saSigner, nil}, args{&apiv1.RenewCertificateRequest{\n\t\t\tTemplate: testTemplate, Lifetime: 24 * time.Hour,\n\t\t}}, &apiv1.RenewCertificateResponse{\n\t\t\tCertificate:      testSignedTemplate,\n\t\t\tCertificateChain: []*x509.Certificate{testIssuer},\n\t\t}, false},\n\t\t{\"ok with callback\", fields{nil, nil, testCertificateSigner}, args{&apiv1.RenewCertificateRequest{\n\t\t\tTemplate: testTemplate, Lifetime: 24 * time.Hour,\n\t\t}}, &apiv1.RenewCertificateResponse{\n\t\t\tCertificate:      testSignedTemplate,\n\t\t\tCertificateChain: []*x509.Certificate{testIssuer},\n\t\t}, false},\n\t\t{\"fail template\", fields{testIssuer, testSigner, nil}, args{&apiv1.RenewCertificateRequest{Lifetime: 24 * time.Hour}}, nil, true},\n\t\t{\"fail lifetime\", fields{testIssuer, testSigner, nil}, args{&apiv1.RenewCertificateRequest{Template: testTemplate}}, nil, true},\n\t\t{\"fail CreateCertificate\", fields{testIssuer, testSigner, nil}, args{&apiv1.RenewCertificateRequest{\n\t\t\tTemplate: &tmplNoSerial,\n\t\t\tLifetime: 24 * time.Hour,\n\t\t}}, nil, true},\n\t\t{\"fail with callback\", fields{nil, nil, testFailCertificateSigner}, args{&apiv1.RenewCertificateRequest{\n\t\t\tTemplate: testTemplate, Lifetime: 24 * time.Hour,\n\t\t}}, nil, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tc := &SoftCAS{\n\t\t\t\tCertificateChain:  []*x509.Certificate{tt.fields.Issuer},\n\t\t\t\tSigner:            tt.fields.Signer,\n\t\t\t\tCertificateSigner: tt.fields.CertificateSigner,\n\t\t\t}\n\t\t\tgot, err := c.RenewCertificate(tt.args.req)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"SoftCAS.RenewCertificate() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"SoftCAS.RenewCertificate() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSoftCAS_RevokeCertificate(t *testing.T) {\n\ttype fields struct {\n\t\tIssuer            *x509.Certificate\n\t\tSigner            crypto.Signer\n\t\tCertificateSigner func() ([]*x509.Certificate, crypto.Signer, error)\n\t}\n\ttype args struct {\n\t\treq *apiv1.RevokeCertificateRequest\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tfields  fields\n\t\targs    args\n\t\twant    *apiv1.RevokeCertificateResponse\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", fields{testIssuer, testSigner, nil}, args{&apiv1.RevokeCertificateRequest{\n\t\t\tCertificate: &x509.Certificate{Subject: pkix.Name{CommonName: \"fake\"}},\n\t\t\tReason:      \"test reason\",\n\t\t\tReasonCode:  1,\n\t\t}}, &apiv1.RevokeCertificateResponse{\n\t\t\tCertificate:      &x509.Certificate{Subject: pkix.Name{CommonName: \"fake\"}},\n\t\t\tCertificateChain: []*x509.Certificate{testIssuer},\n\t\t}, false},\n\t\t{\"ok no cert\", fields{testIssuer, testSigner, nil}, args{&apiv1.RevokeCertificateRequest{\n\t\t\tReason:     \"test reason\",\n\t\t\tReasonCode: 1,\n\t\t}}, &apiv1.RevokeCertificateResponse{\n\t\t\tCertificate:      nil,\n\t\t\tCertificateChain: []*x509.Certificate{testIssuer},\n\t\t}, false},\n\t\t{\"ok empty\", fields{testIssuer, testSigner, nil}, args{&apiv1.RevokeCertificateRequest{}}, &apiv1.RevokeCertificateResponse{\n\t\t\tCertificate:      nil,\n\t\t\tCertificateChain: []*x509.Certificate{testIssuer},\n\t\t}, false},\n\t\t{\"ok with callback\", fields{nil, nil, testCertificateSigner}, args{&apiv1.RevokeCertificateRequest{\n\t\t\tCertificate: &x509.Certificate{Subject: pkix.Name{CommonName: \"fake\"}},\n\t\t\tReason:      \"test reason\",\n\t\t\tReasonCode:  1,\n\t\t}}, &apiv1.RevokeCertificateResponse{\n\t\t\tCertificate:      &x509.Certificate{Subject: pkix.Name{CommonName: \"fake\"}},\n\t\t\tCertificateChain: []*x509.Certificate{testIssuer},\n\t\t}, false},\n\t\t{\"fail with callback\", fields{nil, nil, testFailCertificateSigner}, args{&apiv1.RevokeCertificateRequest{\n\t\t\tCertificate: &x509.Certificate{Subject: pkix.Name{CommonName: \"fake\"}},\n\t\t\tReason:      \"test reason\",\n\t\t\tReasonCode:  1,\n\t\t}}, nil, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tc := &SoftCAS{\n\t\t\t\tCertificateChain:  []*x509.Certificate{tt.fields.Issuer},\n\t\t\t\tSigner:            tt.fields.Signer,\n\t\t\t\tCertificateSigner: tt.fields.CertificateSigner,\n\t\t\t}\n\t\t\tgot, err := c.RevokeCertificate(tt.args.req)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"SoftCAS.RevokeCertificate() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"SoftCAS.RevokeCertificate() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_now(t *testing.T) {\n\tt0 := time.Now()\n\tt1 := now()\n\tif t1.Sub(t0) > time.Second {\n\t\tt.Errorf(\"now() = %s, want ~%s\", t1, t0)\n\t}\n}\n\nfunc TestSoftCAS_CreateCertificateAuthority(t *testing.T) {\n\tmockNow(t)\n\n\tsaSigner := &signatureAlgorithmSigner{\n\t\tSigner:    testSigner,\n\t\talgorithm: x509.PureEd25519,\n\t}\n\n\ttype fields struct {\n\t\tIssuer     *x509.Certificate\n\t\tSigner     crypto.Signer\n\t\tKeyManager kms.KeyManager\n\t}\n\ttype args struct {\n\t\treq *apiv1.CreateCertificateAuthorityRequest\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tfields  fields\n\t\targs    args\n\t\twant    *apiv1.CreateCertificateAuthorityResponse\n\t\twantErr bool\n\t}{\n\t\t{\"ok root\", fields{nil, nil, &mockKeyManager{}}, args{&apiv1.CreateCertificateAuthorityRequest{\n\t\t\tType:     apiv1.RootCA,\n\t\t\tTemplate: testRootTemplate,\n\t\t\tLifetime: 24 * time.Hour,\n\t\t}}, &apiv1.CreateCertificateAuthorityResponse{\n\t\t\tName:        \"Test Root CA\",\n\t\t\tCertificate: testSignedRootTemplate,\n\t\t\tPublicKey:   testSignedRootTemplate.PublicKey,\n\t\t\tPrivateKey:  testSigner,\n\t\t\tSigner:      testSigner,\n\t\t}, false},\n\t\t{\"ok intermediate\", fields{nil, nil, &mockKeyManager{}}, args{&apiv1.CreateCertificateAuthorityRequest{\n\t\t\tType:     apiv1.IntermediateCA,\n\t\t\tTemplate: testIntermediateTemplate,\n\t\t\tLifetime: 24 * time.Hour,\n\t\t\tParent: &apiv1.CreateCertificateAuthorityResponse{\n\t\t\t\tCertificate: testSignedRootTemplate,\n\t\t\t\tSigner:      testSigner,\n\t\t\t},\n\t\t}}, &apiv1.CreateCertificateAuthorityResponse{\n\t\t\tName:             \"Test Intermediate CA\",\n\t\t\tCertificate:      testSignedIntermediateTemplate,\n\t\t\tCertificateChain: []*x509.Certificate{testSignedRootTemplate},\n\t\t\tPublicKey:        testSignedIntermediateTemplate.PublicKey,\n\t\t\tPrivateKey:       testSigner,\n\t\t\tSigner:           testSigner,\n\t\t}, false},\n\t\t{\"ok signature algorithm\", fields{nil, nil, &mockKeyManager{signer: saSigner}}, args{&apiv1.CreateCertificateAuthorityRequest{\n\t\t\tType:     apiv1.RootCA,\n\t\t\tTemplate: testRootTemplate,\n\t\t\tLifetime: 24 * time.Hour,\n\t\t}}, &apiv1.CreateCertificateAuthorityResponse{\n\t\t\tName:        \"Test Root CA\",\n\t\t\tCertificate: testSignedRootTemplate,\n\t\t\tPublicKey:   testSignedRootTemplate.PublicKey,\n\t\t\tPrivateKey:  saSigner,\n\t\t\tSigner:      saSigner,\n\t\t}, false},\n\t\t{\"ok createKey\", fields{nil, nil, &mockKeyManager{}}, args{&apiv1.CreateCertificateAuthorityRequest{\n\t\t\tType:     apiv1.RootCA,\n\t\t\tTemplate: testRootTemplate,\n\t\t\tLifetime: 24 * time.Hour,\n\t\t\tCreateKey: &kmsapi.CreateKeyRequest{\n\t\t\t\tName:               \"root_ca.crt\",\n\t\t\t\tSignatureAlgorithm: kmsapi.ECDSAWithSHA256,\n\t\t\t},\n\t\t}}, &apiv1.CreateCertificateAuthorityResponse{\n\t\t\tName:        \"Test Root CA\",\n\t\t\tCertificate: testSignedRootTemplate,\n\t\t\tPublicKey:   testSignedRootTemplate.PublicKey,\n\t\t\tKeyName:     \"root_ca.crt\",\n\t\t\tPrivateKey:  testSigner,\n\t\t\tSigner:      testSigner,\n\t\t}, false},\n\t\t{\"fail template\", fields{nil, nil, &mockKeyManager{}}, args{&apiv1.CreateCertificateAuthorityRequest{\n\t\t\tType:     apiv1.RootCA,\n\t\t\tLifetime: 24 * time.Hour,\n\t\t}}, nil, true},\n\t\t{\"fail lifetime\", fields{nil, nil, &mockKeyManager{}}, args{&apiv1.CreateCertificateAuthorityRequest{\n\t\t\tType:     apiv1.RootCA,\n\t\t\tTemplate: testIntermediateTemplate,\n\t\t}}, nil, true},\n\t\t{\"fail type\", fields{nil, nil, &mockKeyManager{}}, args{&apiv1.CreateCertificateAuthorityRequest{\n\t\t\tTemplate: testIntermediateTemplate,\n\t\t\tLifetime: 24 * time.Hour,\n\t\t}}, nil, true},\n\t\t{\"fail parent\", fields{nil, nil, &mockKeyManager{}}, args{&apiv1.CreateCertificateAuthorityRequest{\n\t\t\tType:     apiv1.IntermediateCA,\n\t\t\tTemplate: testIntermediateTemplate,\n\t\t\tLifetime: 24 * time.Hour,\n\t\t}}, nil, true},\n\t\t{\"fail parent.certificate\", fields{nil, nil, &mockKeyManager{}}, args{&apiv1.CreateCertificateAuthorityRequest{\n\t\t\tType:     apiv1.IntermediateCA,\n\t\t\tTemplate: testIntermediateTemplate,\n\t\t\tLifetime: 24 * time.Hour,\n\t\t\tParent: &apiv1.CreateCertificateAuthorityResponse{\n\t\t\t\tSigner: testSigner,\n\t\t\t},\n\t\t}}, nil, true},\n\t\t{\"fail parent.signer\", fields{nil, nil, &mockKeyManager{}}, args{&apiv1.CreateCertificateAuthorityRequest{\n\t\t\tType:     apiv1.IntermediateCA,\n\t\t\tTemplate: testIntermediateTemplate,\n\t\t\tLifetime: 24 * time.Hour,\n\t\t\tParent: &apiv1.CreateCertificateAuthorityResponse{\n\t\t\t\tCertificate: testSignedRootTemplate,\n\t\t\t},\n\t\t}}, nil, true},\n\t\t{\"fail createKey\", fields{nil, nil, &mockKeyManager{errCreateKey: errTest}}, args{&apiv1.CreateCertificateAuthorityRequest{\n\t\t\tType:     apiv1.RootCA,\n\t\t\tTemplate: testIntermediateTemplate,\n\t\t\tLifetime: 24 * time.Hour,\n\t\t}}, nil, true},\n\t\t{\"fail createSigner\", fields{nil, nil, &mockKeyManager{errCreatesigner: errTest}}, args{&apiv1.CreateCertificateAuthorityRequest{\n\t\t\tType:     apiv1.RootCA,\n\t\t\tTemplate: testIntermediateTemplate,\n\t\t\tLifetime: 24 * time.Hour,\n\t\t}}, nil, true},\n\t\t{\"fail sign root\", fields{nil, nil, &mockKeyManager{signer: &badSigner{}}}, args{&apiv1.CreateCertificateAuthorityRequest{\n\t\t\tType:     apiv1.RootCA,\n\t\t\tTemplate: testIntermediateTemplate,\n\t\t\tLifetime: 24 * time.Hour,\n\t\t}}, nil, true},\n\t\t{\"fail sign intermediate\", fields{nil, nil, &mockKeyManager{}}, args{&apiv1.CreateCertificateAuthorityRequest{\n\t\t\tType:     apiv1.IntermediateCA,\n\t\t\tTemplate: testIntermediateTemplate,\n\t\t\tLifetime: 24 * time.Hour,\n\t\t\tParent: &apiv1.CreateCertificateAuthorityResponse{\n\t\t\t\tCertificate: testSignedRootTemplate,\n\t\t\t\tSigner:      &badSigner{},\n\t\t\t},\n\t\t}}, nil, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tc := &SoftCAS{\n\t\t\t\tCertificateChain: []*x509.Certificate{tt.fields.Issuer},\n\t\t\t\tSigner:           tt.fields.Signer,\n\t\t\t\tKeyManager:       tt.fields.KeyManager,\n\t\t\t}\n\t\t\tgot, err := c.CreateCertificateAuthority(tt.args.req)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"SoftCAS.CreateCertificateAuthority() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"SoftCAS.CreateCertificateAuthority() = \\n%#v, want \\n%#v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSoftCAS_defaultKeyManager(t *testing.T) {\n\tmockNow(t)\n\ttype args struct {\n\t\treq *apiv1.CreateCertificateAuthorityRequest\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\targs    args\n\t\twantErr bool\n\t}{\n\t\t{\"ok root\", args{&apiv1.CreateCertificateAuthorityRequest{\n\t\t\tType: apiv1.RootCA,\n\t\t\tTemplate: &x509.Certificate{\n\t\t\t\tSubject:               pkix.Name{CommonName: \"Test Root CA\"},\n\t\t\t\tKeyUsage:              x509.KeyUsageCRLSign | x509.KeyUsageCertSign,\n\t\t\t\tBasicConstraintsValid: true,\n\t\t\t\tIsCA:                  true,\n\t\t\t\tMaxPathLen:            1,\n\t\t\t\tSerialNumber:          big.NewInt(1234),\n\t\t\t},\n\t\t\tLifetime: 24 * time.Hour,\n\t\t}}, false},\n\t\t{\"ok intermediate\", args{&apiv1.CreateCertificateAuthorityRequest{\n\t\t\tType:     apiv1.IntermediateCA,\n\t\t\tTemplate: testIntermediateTemplate,\n\t\t\tLifetime: 24 * time.Hour,\n\t\t\tParent: &apiv1.CreateCertificateAuthorityResponse{\n\t\t\t\tCertificate: testSignedRootTemplate,\n\t\t\t\tSigner:      testSigner,\n\t\t\t},\n\t\t}}, false},\n\t\t{\"fail with default key manager\", args{&apiv1.CreateCertificateAuthorityRequest{\n\t\t\tType:     apiv1.IntermediateCA,\n\t\t\tTemplate: testIntermediateTemplate,\n\t\t\tLifetime: 24 * time.Hour,\n\t\t\tParent: &apiv1.CreateCertificateAuthorityResponse{\n\t\t\t\tCertificate: testSignedRootTemplate,\n\t\t\t\tSigner:      &badSigner{},\n\t\t\t},\n\t\t}}, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tc := &SoftCAS{}\n\t\t\t_, err := c.CreateCertificateAuthority(tt.args.req)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"SoftCAS.CreateCertificateAuthority() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_isRSA(t *testing.T) {\n\ttype args struct {\n\t\tsa x509.SignatureAlgorithm\n\t}\n\ttests := []struct {\n\t\tname string\n\t\targs args\n\t\twant bool\n\t}{\n\t\t{\"SHA256WithRSA\", args{x509.SHA256WithRSA}, true},\n\t\t{\"SHA384WithRSA\", args{x509.SHA384WithRSA}, true},\n\t\t{\"SHA512WithRSA\", args{x509.SHA512WithRSA}, true},\n\t\t{\"SHA256WithRSAPSS\", args{x509.SHA256WithRSAPSS}, true},\n\t\t{\"SHA384WithRSAPSS\", args{x509.SHA384WithRSAPSS}, true},\n\t\t{\"SHA512WithRSAPSS\", args{x509.SHA512WithRSAPSS}, true},\n\t\t{\"ECDSAWithSHA256\", args{x509.ECDSAWithSHA256}, false},\n\t\t{\"ECDSAWithSHA384\", args{x509.ECDSAWithSHA384}, false},\n\t\t{\"ECDSAWithSHA512\", args{x509.ECDSAWithSHA512}, false},\n\t\t{\"PureEd25519\", args{x509.PureEd25519}, false},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif got := isRSA(tt.args.sa); got != tt.want {\n\t\t\t\tt.Errorf(\"isRSA() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "cas/stepcas/issuer.go",
    "content": "package stepcas\n\nimport (\n\t\"context\"\n\t\"net/url\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/google/uuid\"\n\t\"github.com/pkg/errors\"\n\t\"github.com/smallstep/certificates/ca\"\n\t\"github.com/smallstep/certificates/cas/apiv1\"\n)\n\n// raAuthorityNS is a custom namespace used to generate endpoint ids based on\n// the authority id.\nvar raAuthorityNS = uuid.MustParse(\"d6f14c1f-2f92-47bf-a04f-7b2c11382edd\")\n\n// newServerEndpointID returns a uuid v5 using raAuthorityNS as the namespace.\n// The return uuid will be used as the server endpoint id, it will be unique per\n// authority.\nfunc newServerEndpointID(data string) uuid.UUID {\n\treturn uuid.NewSHA1(raAuthorityNS, []byte(data))\n}\n\ntype raInfo struct {\n\tAuthorityID     string `json:\"authorityId,omitempty\"`\n\tEndpointID      string `json:\"endpointId,omitempty\"`\n\tProvisionerID   string `json:\"provisionerId,omitempty\"`\n\tProvisionerType string `json:\"provisionerType,omitempty\"`\n\tProvisionerName string `json:\"provisionerName,omitempty\"`\n}\n\ntype stepIssuer interface {\n\tSignToken(subject string, sans []string, info *raInfo) (string, error)\n\tRevokeToken(subject string) (string, error)\n\tLifetime(d time.Duration) time.Duration\n}\n\n// newStepIssuer returns the configured step issuer.\nfunc newStepIssuer(ctx context.Context, caURL *url.URL, client *ca.Client, iss *apiv1.CertificateIssuer) (stepIssuer, error) {\n\tif err := validateCertificateIssuer(iss); err != nil {\n\t\treturn nil, err\n\t}\n\n\tswitch strings.ToLower(iss.Type) {\n\tcase \"x5c\":\n\t\treturn newX5CIssuer(caURL, iss)\n\tcase \"jwk\":\n\t\treturn newJWKIssuer(ctx, caURL, client, iss)\n\tdefault:\n\t\treturn nil, errors.Errorf(\"stepCAS `certificateIssuer.type` %s is not supported\", iss.Type)\n\t}\n}\n\n// validateCertificateIssuer validates the configuration of the certificate\n// issuer.\nfunc validateCertificateIssuer(iss *apiv1.CertificateIssuer) error {\n\tswitch {\n\tcase iss == nil:\n\t\treturn errors.New(\"stepCAS 'certificateIssuer' cannot be nil\")\n\tcase iss.Type == \"\":\n\t\treturn errors.New(\"stepCAS `certificateIssuer.type` cannot be empty\")\n\t}\n\n\tswitch strings.ToLower(iss.Type) {\n\tcase \"x5c\":\n\t\treturn validateX5CIssuer(iss)\n\tcase \"jwk\":\n\t\treturn validateJWKIssuer(iss)\n\tdefault:\n\t\treturn errors.Errorf(\"stepCAS `certificateIssuer.type` %s is not supported\", iss.Type)\n\t}\n}\n\n// validateX5CIssuer validates the configuration of x5c issuer.\nfunc validateX5CIssuer(iss *apiv1.CertificateIssuer) error {\n\tswitch {\n\tcase iss.Certificate == \"\":\n\t\treturn errors.New(\"stepCAS `certificateIssuer.crt` cannot be empty\")\n\tcase iss.Key == \"\":\n\t\treturn errors.New(\"stepCAS `certificateIssuer.key` cannot be empty\")\n\tcase iss.Provisioner == \"\":\n\t\treturn errors.New(\"stepCAS `certificateIssuer.provisioner` cannot be empty\")\n\tdefault:\n\t\treturn nil\n\t}\n}\n\n// validateJWKIssuer validates the configuration of jwk issuer. If the key is\n// not given, then it will download it from the CA. If the password is not set\n// it will be prompted.\nfunc validateJWKIssuer(iss *apiv1.CertificateIssuer) error {\n\tswitch iss.Provisioner {\n\tcase \"\":\n\t\treturn errors.New(\"stepCAS `certificateIssuer.provisioner` cannot be empty\")\n\tdefault:\n\t\treturn nil\n\t}\n}\n"
  },
  {
    "path": "cas/stepcas/issuer_test.go",
    "content": "package stepcas\n\nimport (\n\t\"context\"\n\t\"net/url\"\n\t\"reflect\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/google/uuid\"\n\t\"github.com/smallstep/certificates/ca\"\n\t\"github.com/smallstep/certificates/cas/apiv1\"\n\t\"go.step.sm/crypto/jose\"\n)\n\ntype mockErrIssuer struct{}\n\nfunc (m mockErrIssuer) SignToken(string, []string, *raInfo) (string, error) {\n\treturn \"\", apiv1.NotImplementedError{}\n}\n\nfunc (m mockErrIssuer) RevokeToken(string) (string, error) {\n\treturn \"\", apiv1.NotImplementedError{}\n}\n\nfunc (m mockErrIssuer) Lifetime(d time.Duration) time.Duration {\n\treturn d\n}\n\ntype mockErrSigner struct{}\n\nfunc (s *mockErrSigner) Sign([]byte) (*jose.JSONWebSignature, error) {\n\treturn nil, apiv1.NotImplementedError{}\n}\n\nfunc (s *mockErrSigner) Options() jose.SignerOptions {\n\treturn jose.SignerOptions{}\n}\n\nfunc Test_newServerEndpointID(t *testing.T) {\n\ttype args struct {\n\t\tname string\n\t}\n\ttests := []struct {\n\t\tname string\n\t\targs args\n\t\twant []byte\n\t}{\n\t\t{\"ok\", args{\"foo\"}, []byte{\n\t\t\t0x8f, 0x63, 0x69, 0x20, 0x8a, 0x7a, 0x57, 0x0c, 0xbe, 0x4c, 0x46, 0x66, 0x77, 0xf8, 0x54, 0xe7,\n\t\t}},\n\t\t{\"ok uuid\", args{\"e4fa6d2d-fa9c-4fdc-913e-7484cc9516e4\"}, []byte{\n\t\t\t0x8d, 0x8d, 0x7f, 0x04, 0x73, 0xd4, 0x5f, 0x2f, 0xa8, 0xe1, 0x28, 0x9a, 0xd1, 0xa8, 0xcf, 0x7e,\n\t\t}},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tvar want uuid.UUID\n\t\t\tcopy(want[:], tt.want)\n\t\t\tgot := newServerEndpointID(tt.args.name)\n\t\t\tif !reflect.DeepEqual(got, want) {\n\t\t\t\tt.Errorf(\"newServerEndpointID() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t\t// Check version\n\t\t\tif v := (got[6] & 0xf0) >> 4; v != 5 {\n\t\t\t\tt.Errorf(\"newServerEndpointID() version = %d, want 5\", v)\n\t\t\t}\n\t\t\t// Check variant\n\t\t\tif v := (got[8] & 0x80) >> 6; v != 2 {\n\t\t\t\tt.Errorf(\"newServerEndpointID() variant = %d, want 2\", v)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_newStepIssuer(t *testing.T) {\n\tcaURL, client := testCAHelper(t)\n\tsigner, err := newJWKSignerFromEncryptedKey(testKeyID, testEncryptedJWKKey, testPassword)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\ttype args struct {\n\t\tcaURL  *url.URL\n\t\tclient *ca.Client\n\t\tiss    *apiv1.CertificateIssuer\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\targs    args\n\t\twant    stepIssuer\n\t\twantErr bool\n\t}{\n\t\t{\"x5c\", args{caURL, client, &apiv1.CertificateIssuer{\n\t\t\tType:        \"x5c\",\n\t\t\tProvisioner: \"X5C\",\n\t\t\tCertificate: testX5CPath,\n\t\t\tKey:         testX5CKeyPath,\n\t\t}}, &x5cIssuer{\n\t\t\tcaURL:    caURL,\n\t\t\tcertFile: testX5CPath,\n\t\t\tkeyFile:  testX5CKeyPath,\n\t\t\tissuer:   \"X5C\",\n\t\t}, false},\n\t\t{\"jwk\", args{caURL, client, &apiv1.CertificateIssuer{\n\t\t\tType:        \"jwk\",\n\t\t\tProvisioner: \"ra@doe.org\",\n\t\t\tKey:         testX5CKeyPath,\n\t\t}}, &jwkIssuer{\n\t\t\tcaURL:  caURL,\n\t\t\tissuer: \"ra@doe.org\",\n\t\t\tsigner: signer,\n\t\t}, false},\n\t\t{\"fail\", args{caURL, client, &apiv1.CertificateIssuer{\n\t\t\tType:        \"unknown\",\n\t\t\tProvisioner: \"ra@doe.org\",\n\t\t\tKey:         testX5CKeyPath,\n\t\t}}, nil, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := newStepIssuer(context.TODO(), tt.args.caURL, tt.args.client, tt.args.iss)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"newStepIssuer() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif tt.args.iss.Type == \"jwk\" && got != nil && tt.want != nil {\n\t\t\t\tgot.(*jwkIssuer).signer = tt.want.(*jwkIssuer).signer\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"newStepIssuer() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "cas/stepcas/jwk_issuer.go",
    "content": "package stepcas\n\nimport (\n\t\"context\"\n\t\"crypto\"\n\t\"encoding/json\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n\n\t\"github.com/smallstep/cli-utils/ui\"\n\t\"go.step.sm/crypto/jose\"\n\t\"go.step.sm/crypto/randutil\"\n\n\t\"github.com/smallstep/certificates/authority/provisioner\"\n\t\"github.com/smallstep/certificates/ca\"\n\t\"github.com/smallstep/certificates/cas/apiv1\"\n)\n\ntype jwkIssuer struct {\n\tcaURL  *url.URL\n\tissuer string\n\tsigner jose.Signer\n}\n\nfunc newJWKIssuer(ctx context.Context, caURL *url.URL, client *ca.Client, cfg *apiv1.CertificateIssuer) (*jwkIssuer, error) {\n\tvar err error\n\tvar signer jose.Signer\n\t// Read the key from the CA if not provided.\n\t// Or read it from a PEM file.\n\tif cfg.Key == \"\" {\n\t\tp, err := findProvisioner(ctx, client, provisioner.TypeJWK, cfg.Provisioner)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tkid, key, ok := p.GetEncryptedKey()\n\t\tif !ok {\n\t\t\treturn nil, errors.Errorf(\"provisioner with name %s does not have an encrypted key\", cfg.Provisioner)\n\t\t}\n\t\tsigner, err = newJWKSignerFromEncryptedKey(kid, key, cfg.Password)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t} else {\n\t\tsigner, err = newJWKSigner(cfg.Key, cfg.Password)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn &jwkIssuer{\n\t\tcaURL:  caURL,\n\t\tissuer: cfg.Provisioner,\n\t\tsigner: signer,\n\t}, nil\n}\n\nfunc (i *jwkIssuer) SignToken(subject string, sans []string, info *raInfo) (string, error) {\n\taud := i.caURL.ResolveReference(&url.URL{\n\t\tPath: \"/1.0/sign\",\n\t}).String()\n\treturn i.createToken(aud, subject, sans, info)\n}\n\nfunc (i *jwkIssuer) RevokeToken(subject string) (string, error) {\n\taud := i.caURL.ResolveReference(&url.URL{\n\t\tPath: \"/1.0/revoke\",\n\t}).String()\n\treturn i.createToken(aud, subject, nil, nil)\n}\n\nfunc (i *jwkIssuer) Lifetime(d time.Duration) time.Duration {\n\treturn d\n}\n\nfunc (i *jwkIssuer) createToken(aud, sub string, sans []string, info *raInfo) (string, error) {\n\tid, err := randutil.Hex(64) // 256 bits\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tclaims := defaultClaims(i.issuer, sub, aud, id)\n\tbuilder := jose.Signed(i.signer).Claims(claims)\n\tif len(sans) > 0 {\n\t\tbuilder = builder.Claims(map[string]interface{}{\n\t\t\t\"sans\": sans,\n\t\t})\n\t}\n\tif info != nil {\n\t\tbuilder = builder.Claims(map[string]interface{}{\n\t\t\t\"step\": map[string]interface{}{\n\t\t\t\t\"ra\": info,\n\t\t\t},\n\t\t})\n\t}\n\n\ttok, err := builder.CompactSerialize()\n\tif err != nil {\n\t\treturn \"\", errors.Wrap(err, \"error signing token\")\n\t}\n\n\treturn tok, nil\n}\n\nfunc newJWKSigner(keyFile, password string) (jose.Signer, error) {\n\tsigner, err := readKey(keyFile, password)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tkid, err := jose.Thumbprint(&jose.JSONWebKey{Key: signer.Public()})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tso := new(jose.SignerOptions)\n\tso.WithType(\"JWT\")\n\tso.WithHeader(\"kid\", kid)\n\treturn newJoseSigner(signer, so)\n}\n\nfunc newJWKSignerFromEncryptedKey(kid, key, password string) (jose.Signer, error) {\n\tvar jwk jose.JSONWebKey\n\n\t// If the password is empty it will use the password prompter.\n\tb, err := jose.Decrypt([]byte(key),\n\t\tjose.WithPassword([]byte(password)),\n\t\tjose.WithPasswordPrompter(\"Please enter the password to decrypt the provisioner key\", func(msg string) ([]byte, error) {\n\t\t\treturn ui.PromptPassword(msg)\n\t\t}))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Decrypt returns the JSON representation of the JWK.\n\tif err := json.Unmarshal(b, &jwk); err != nil {\n\t\treturn nil, errors.Wrap(err, \"error parsing provisioner key\")\n\t}\n\n\tsigner, ok := jwk.Key.(crypto.Signer)\n\tif !ok {\n\t\treturn nil, errors.New(\"error parsing provisioner key: key is not a crypto.Signer\")\n\t}\n\n\tso := new(jose.SignerOptions)\n\tso.WithType(\"JWT\")\n\tso.WithHeader(\"kid\", kid)\n\treturn newJoseSigner(signer, so)\n}\n\nfunc findProvisioner(ctx context.Context, client *ca.Client, typ provisioner.Type, name string) (provisioner.Interface, error) {\n\tcursor := \"\"\n\tfor {\n\t\tps, err := client.ProvisionersWithContext(ctx, ca.WithProvisionerCursor(cursor))\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tfor _, p := range ps.Provisioners {\n\t\t\tif p.GetType() == typ && p.GetName() == name {\n\t\t\t\treturn p, nil\n\t\t\t}\n\t\t}\n\t\tif ps.NextCursor == \"\" {\n\t\t\treturn nil, errors.Errorf(\"provisioner with name %s was not found\", name)\n\t\t}\n\t\tcursor = ps.NextCursor\n\t}\n}\n"
  },
  {
    "path": "cas/stepcas/jwk_issuer_test.go",
    "content": "package stepcas\n\nimport (\n\t\"net/url\"\n\t\"reflect\"\n\t\"testing\"\n\t\"time\"\n\n\t\"go.step.sm/crypto/jose\"\n)\n\nfunc Test_jwkIssuer_SignToken(t *testing.T) {\n\tcaURL, err := url.Parse(\"https://ca.smallstep.com\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tsigner, err := newJWKSignerFromEncryptedKey(testKeyID, testEncryptedJWKKey, testPassword)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\ttype fields struct {\n\t\tcaURL  *url.URL\n\t\tissuer string\n\t\tsigner jose.Signer\n\t}\n\ttype args struct {\n\t\tsubject string\n\t\tsans    []string\n\t\tinfo    *raInfo\n\t}\n\ttype stepClaims struct {\n\t\tRA *raInfo `json:\"ra\"`\n\t}\n\ttype claims struct {\n\t\tAud  jose.Audience `json:\"aud\"`\n\t\tSub  string        `json:\"sub\"`\n\t\tSans []string      `json:\"sans\"`\n\t\tStep stepClaims    `json:\"step\"`\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tfields  fields\n\t\targs    args\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", fields{caURL, \"ra@doe.org\", signer}, args{\"doe\", []string{\"doe.org\"}, nil}, false},\n\t\t{\"ok ra\", fields{caURL, \"ra@doe.org\", signer}, args{\"doe\", []string{\"doe.org\"}, &raInfo{\n\t\t\tAuthorityID: \"authority-id\", ProvisionerID: \"provisioner-id\", ProvisionerType: \"provisioner-type\",\n\t\t}}, false},\n\t\t{\"ok ra endpoint id\", fields{caURL, \"ra@doe.org\", signer}, args{\"doe\", []string{\"doe.org\"}, &raInfo{\n\t\t\tAuthorityID: \"authority-id\", EndpointID: \"endpoint-id\",\n\t\t}}, false},\n\t\t{\"fail\", fields{caURL, \"ra@doe.org\", &mockErrSigner{}}, args{\"doe\", []string{\"doe.org\"}, nil}, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ti := &jwkIssuer{\n\t\t\t\tcaURL:  tt.fields.caURL,\n\t\t\t\tissuer: tt.fields.issuer,\n\t\t\t\tsigner: tt.fields.signer,\n\t\t\t}\n\t\t\tgot, err := i.SignToken(tt.args.subject, tt.args.sans, tt.args.info)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"jwkIssuer.SignToken() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !tt.wantErr {\n\t\t\t\tjwt, err := jose.ParseSigned(got)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Errorf(\"jose.ParseSigned() error = %v\", err)\n\t\t\t\t}\n\t\t\t\tvar c claims\n\t\t\t\twant := claims{\n\t\t\t\t\tAud:  jose.Audience{tt.fields.caURL.String() + \"/1.0/sign\"},\n\t\t\t\t\tSub:  tt.args.subject,\n\t\t\t\t\tSans: tt.args.sans,\n\t\t\t\t}\n\t\t\t\tif tt.args.info != nil {\n\t\t\t\t\twant.Step.RA = tt.args.info\n\t\t\t\t}\n\t\t\t\tif err := jwt.Claims(testX5CKey.Public(), &c); err != nil {\n\t\t\t\t\tt.Log(got)\n\t\t\t\t\tt.Errorf(\"jwt.Claims() error = %v\", err)\n\t\t\t\t}\n\t\t\t\tif !reflect.DeepEqual(c, want) {\n\t\t\t\t\tt.Errorf(\"jwt.Claims() claims = %#v, want %#v\", c, want)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_jwkIssuer_RevokeToken(t *testing.T) {\n\tcaURL, err := url.Parse(\"https://ca.smallstep.com\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tsigner, err := newJWKSignerFromEncryptedKey(testKeyID, testEncryptedJWKKey, testPassword)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\ttype fields struct {\n\t\tcaURL  *url.URL\n\t\tissuer string\n\t\tsigner jose.Signer\n\t}\n\ttype args struct {\n\t\tsubject string\n\t}\n\ttype claims struct {\n\t\tAud  jose.Audience `json:\"aud\"`\n\t\tSub  string        `json:\"sub\"`\n\t\tSans []string      `json:\"sans\"`\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tfields  fields\n\t\targs    args\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", fields{caURL, \"ra@doe.org\", signer}, args{\"doe\"}, false},\n\t\t{\"ok\", fields{caURL, \"ra@doe.org\", &mockErrSigner{}}, args{\"doe\"}, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ti := &jwkIssuer{\n\t\t\t\tcaURL:  tt.fields.caURL,\n\t\t\t\tissuer: tt.fields.issuer,\n\t\t\t\tsigner: tt.fields.signer,\n\t\t\t}\n\t\t\tgot, err := i.RevokeToken(tt.args.subject)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"jwkIssuer.RevokeToken() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !tt.wantErr {\n\t\t\t\tjwt, err := jose.ParseSigned(got)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Errorf(\"jose.ParseSigned() error = %v\", err)\n\t\t\t\t}\n\t\t\t\tvar c claims\n\t\t\t\twant := claims{\n\t\t\t\t\tAud: []string{tt.fields.caURL.String() + \"/1.0/revoke\"},\n\t\t\t\t\tSub: tt.args.subject,\n\t\t\t\t}\n\t\t\t\tif err := jwt.Claims(testX5CKey.Public(), &c); err != nil {\n\t\t\t\t\tt.Errorf(\"jwt.Claims() error = %v\", err)\n\t\t\t\t}\n\t\t\t\tif !reflect.DeepEqual(c, want) {\n\t\t\t\t\tt.Errorf(\"jwt.Claims() claims = %#v, want %#v\", c, want)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_jwkIssuer_Lifetime(t *testing.T) {\n\tcaURL, err := url.Parse(\"https://ca.smallstep.com\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tsigner, err := newJWKSignerFromEncryptedKey(testKeyID, testEncryptedJWKKey, testPassword)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\ttype fields struct {\n\t\tcaURL  *url.URL\n\t\tissuer string\n\t\tsigner jose.Signer\n\t}\n\ttype args struct {\n\t\td time.Duration\n\t}\n\ttests := []struct {\n\t\tname   string\n\t\tfields fields\n\t\targs   args\n\t\twant   time.Duration\n\t}{\n\t\t{\"ok\", fields{caURL, \"ra@smallstep.com\", signer}, args{time.Second}, time.Second},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ti := &jwkIssuer{\n\t\t\t\tcaURL:  tt.fields.caURL,\n\t\t\t\tissuer: tt.fields.issuer,\n\t\t\t\tsigner: tt.fields.signer,\n\t\t\t}\n\t\t\tif got := i.Lifetime(tt.args.d); got != tt.want {\n\t\t\t\tt.Errorf(\"jwkIssuer.Lifetime() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_newJWKSignerFromEncryptedKey(t *testing.T) {\n\tencrypt := func(plaintext string) string {\n\t\trecipient := jose.Recipient{\n\t\t\tAlgorithm:  jose.PBES2_HS256_A128KW,\n\t\t\tKey:        testPassword,\n\t\t\tPBES2Count: jose.PBKDF2Iterations,\n\t\t\tPBES2Salt:  []byte{0x01, 0x02},\n\t\t}\n\n\t\topts := new(jose.EncrypterOptions)\n\t\topts.WithContentType(jose.ContentType(\"jwk+json\"))\n\n\t\tencrypter, err := jose.NewEncrypter(jose.DefaultEncAlgorithm, recipient, opts)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\tjwe, err := encrypter.Encrypt([]byte(plaintext))\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tret, err := jwe.CompactSerialize()\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\treturn ret\n\t}\n\n\ttype args struct {\n\t\tkid      string\n\t\tkey      string\n\t\tpassword string\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\targs    args\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", args{testKeyID, testEncryptedJWKKey, testPassword}, false},\n\t\t{\"fail decrypt\", args{testKeyID, testEncryptedJWKKey, \"bad-password\"}, true},\n\t\t{\"fail unmarshal\", args{testKeyID, encrypt(`{not a json}`), testPassword}, true},\n\t\t{\"fail not signer\", args{testKeyID, encrypt(`{\"kty\":\"oct\",\"k\":\"password\"}`), testPassword}, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\t_, err := newJWKSignerFromEncryptedKey(tt.args.kid, tt.args.key, tt.args.password)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"newJWKSignerFromEncryptedKey() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "cas/stepcas/stepcas.go",
    "content": "package stepcas\n\nimport (\n\t\"context\"\n\t\"crypto/x509\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n\t\"github.com/smallstep/certificates/api\"\n\t\"github.com/smallstep/certificates/ca\"\n\t\"github.com/smallstep/certificates/cas/apiv1\"\n)\n\nfunc init() {\n\tapiv1.Register(apiv1.StepCAS, func(ctx context.Context, opts apiv1.Options) (apiv1.CertificateAuthorityService, error) {\n\t\treturn New(ctx, opts)\n\t})\n}\n\n// StepCAS implements the cas.CertificateAuthorityService interface using\n// another step-ca instance.\ntype StepCAS struct {\n\tiss         stepIssuer\n\tclient      *ca.Client\n\tauthorityID string\n\tfingerprint string\n}\n\n// New creates a new CertificateAuthorityService implementation using another\n// step-ca instance.\nfunc New(ctx context.Context, opts apiv1.Options) (*StepCAS, error) {\n\tswitch {\n\tcase opts.CertificateAuthority == \"\":\n\t\treturn nil, errors.New(\"stepCAS 'certificateAuthority' cannot be empty\")\n\tcase opts.CertificateAuthorityFingerprint == \"\":\n\t\treturn nil, errors.New(\"stepCAS 'certificateAuthorityFingerprint' cannot be empty\")\n\t}\n\n\tcaURL, err := url.Parse(opts.CertificateAuthority)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"stepCAS `certificateAuthority` is not valid\")\n\t}\n\n\t// Create client.\n\tclient, err := ca.NewClient(opts.CertificateAuthority, ca.WithRootSHA256(opts.CertificateAuthorityFingerprint)) //nolint:contextcheck // deeply nested context\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar iss stepIssuer\n\t// Create configured issuer unless we only want to use GetCertificateAuthority.\n\t// This avoid the request for the password if not provided.\n\tif !opts.IsCAGetter {\n\t\tif iss, err = newStepIssuer(ctx, caURL, client, opts.CertificateIssuer); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn &StepCAS{\n\t\tiss:         iss,\n\t\tclient:      client,\n\t\tauthorityID: opts.AuthorityID,\n\t\tfingerprint: opts.CertificateAuthorityFingerprint,\n\t}, nil\n}\n\n// Type returns the type of this CertificateAuthorityService.\nfunc (s *StepCAS) Type() apiv1.Type {\n\treturn apiv1.StepCAS\n}\n\n// CreateCertificate uses the step-ca sign request with the configured\n// provisioner to get a new certificate from the certificate authority.\nfunc (s *StepCAS) CreateCertificate(req *apiv1.CreateCertificateRequest) (*apiv1.CreateCertificateResponse, error) {\n\tswitch {\n\tcase req.CSR == nil:\n\t\treturn nil, errors.New(\"createCertificateRequest `csr` cannot be nil\")\n\tcase req.Template == nil:\n\t\treturn nil, errors.New(\"createCertificateRequest `template` cannot be nil\")\n\tcase req.Lifetime < 0:\n\t\treturn nil, errors.New(\"createCertificateRequest `lifetime` cannot be less than 0\")\n\t}\n\n\tinfo := &raInfo{\n\t\tAuthorityID: s.authorityID,\n\t}\n\tif req.IsCAServerCert {\n\t\tinfo.EndpointID = newServerEndpointID(s.authorityID).String()\n\t}\n\tif p := req.Provisioner; p != nil {\n\t\tinfo.ProvisionerID = p.ID\n\t\tinfo.ProvisionerType = p.Type\n\t\tinfo.ProvisionerName = p.Name\n\t}\n\n\tcert, chain, err := s.createCertificate(req.CSR, req.Template, req.Lifetime, info)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &apiv1.CreateCertificateResponse{\n\t\tCertificate:      cert,\n\t\tCertificateChain: chain,\n\t}, nil\n}\n\n// RenewCertificate will always return a non-implemented error as mTLS renewals\n// are not supported yet.\nfunc (s *StepCAS) RenewCertificate(req *apiv1.RenewCertificateRequest) (*apiv1.RenewCertificateResponse, error) {\n\tif req.Token == \"\" {\n\t\treturn nil, apiv1.ValidationError{Message: \"renewCertificateRequest `token` cannot be empty\"}\n\t}\n\n\tresp, err := s.client.RenewWithToken(req.Token)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar chain []*x509.Certificate\n\tcert := resp.CertChainPEM[0].Certificate\n\tfor _, c := range resp.CertChainPEM[1:] {\n\t\tchain = append(chain, c.Certificate)\n\t}\n\n\treturn &apiv1.RenewCertificateResponse{\n\t\tCertificate:      cert,\n\t\tCertificateChain: chain,\n\t}, nil\n}\n\n// RevokeCertificate revokes a certificate.\nfunc (s *StepCAS) RevokeCertificate(req *apiv1.RevokeCertificateRequest) (*apiv1.RevokeCertificateResponse, error) {\n\tif req.SerialNumber == \"\" && req.Certificate == nil {\n\t\treturn nil, errors.New(\"revokeCertificateRequest `serialNumber` or `certificate` are required\")\n\t}\n\n\tserialNumber := req.SerialNumber\n\tif req.Certificate != nil {\n\t\tserialNumber = req.Certificate.SerialNumber.String()\n\t}\n\n\ttoken, err := s.iss.RevokeToken(serialNumber)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t_, err = s.client.Revoke(&api.RevokeRequest{\n\t\tSerial:     serialNumber,\n\t\tReasonCode: req.ReasonCode,\n\t\tReason:     req.Reason,\n\t\tOTT:        token,\n\t\tPassive:    req.PassiveOnly,\n\t}, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &apiv1.RevokeCertificateResponse{\n\t\tCertificate:      req.Certificate,\n\t\tCertificateChain: nil,\n\t}, nil\n}\n\n// GetCertificateAuthority returns the root certificate of the certificate\n// authority using the configured fingerprint.\nfunc (s *StepCAS) GetCertificateAuthority(*apiv1.GetCertificateAuthorityRequest) (*apiv1.GetCertificateAuthorityResponse, error) {\n\tresp, err := s.client.Root(s.fingerprint)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &apiv1.GetCertificateAuthorityResponse{\n\t\tRootCertificate: resp.RootPEM.Certificate,\n\t}, nil\n}\n\nfunc (s *StepCAS) createCertificate(cr *x509.CertificateRequest, template *x509.Certificate, lifetime time.Duration, raInfo *raInfo) (*x509.Certificate, []*x509.Certificate, error) {\n\tsans := make([]string, 0, len(template.DNSNames)+len(template.EmailAddresses)+len(template.IPAddresses)+len(template.URIs))\n\tsans = append(sans, template.DNSNames...)\n\tsans = append(sans, template.EmailAddresses...)\n\tfor _, ip := range template.IPAddresses {\n\t\tsans = append(sans, ip.String())\n\t}\n\tfor _, u := range template.URIs {\n\t\tsans = append(sans, u.String())\n\t}\n\n\tcommonName := template.Subject.CommonName\n\tif commonName == \"\" && len(sans) > 0 {\n\t\tcommonName = sans[0]\n\t}\n\n\ttoken, err := s.iss.SignToken(commonName, sans, raInfo)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\n\tresp, err := s.client.Sign(&api.SignRequest{\n\t\tCsrPEM:   api.CertificateRequest{CertificateRequest: cr},\n\t\tOTT:      token,\n\t\tNotAfter: s.lifetime(lifetime),\n\t})\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\n\tvar chain []*x509.Certificate\n\tcert := resp.CertChainPEM[0].Certificate\n\tfor _, c := range resp.CertChainPEM[1:] {\n\t\tchain = append(chain, c.Certificate)\n\t}\n\n\treturn cert, chain, nil\n}\n\nfunc (s *StepCAS) lifetime(d time.Duration) api.TimeDuration {\n\tvar td api.TimeDuration\n\ttd.SetDuration(s.iss.Lifetime(d))\n\treturn td\n}\n"
  },
  {
    "path": "cas/stepcas/stepcas_test.go",
    "content": "package stepcas\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto\"\n\t\"crypto/ed25519\"\n\t\"crypto/rand\"\n\t\"crypto/x509\"\n\t\"encoding/json\"\n\t\"encoding/pem\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"reflect\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/smallstep/certificates/api\"\n\t\"github.com/smallstep/certificates/authority/provisioner\"\n\t\"github.com/smallstep/certificates/ca\"\n\t\"github.com/smallstep/certificates/cas/apiv1\"\n\t\"github.com/stretchr/testify/require\"\n\t\"go.step.sm/crypto/jose\"\n\t\"go.step.sm/crypto/pemutil\"\n\t\"go.step.sm/crypto/randutil\"\n\t\"go.step.sm/crypto/x509util\"\n)\n\nvar (\n\ttestRootCrt                   *x509.Certificate\n\ttestRootKey                   crypto.Signer\n\ttestRootPath, testRootKeyPath string\n\ttestRootFingerprint           string\n\n\ttestIssCrt                  *x509.Certificate\n\ttestIssKey                  crypto.Signer\n\ttestIssPath, testIssKeyPath string\n\n\ttestX5CCrt                         *x509.Certificate\n\ttestX5CKey                         crypto.Signer\n\ttestX5CPath, testX5CKeyPath        string\n\ttestPassword, testEncryptedKeyPath string\n\ttestKeyID, testEncryptedJWKKey     string\n\n\ttestCR     *x509.CertificateRequest\n\ttestCrt    *x509.Certificate\n\ttestKey    crypto.Signer\n\ttestFailCR *x509.CertificateRequest\n)\n\nfunc mustSignCertificate(subject string, sans []string, template string, parent *x509.Certificate, signer crypto.Signer) (*x509.Certificate, crypto.Signer) {\n\tpub, priv, err := ed25519.GenerateKey(rand.Reader)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tcr, err := x509util.CreateCertificateRequest(subject, sans, priv)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tcert, err := x509util.NewCertificate(cr, x509util.WithTemplate(template, x509util.CreateTemplateData(subject, sans)))\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tcrt := cert.GetCertificate()\n\tcrt.NotBefore = time.Now()\n\tcrt.NotAfter = crt.NotBefore.Add(time.Hour)\n\tif parent == nil {\n\t\tparent = crt\n\t}\n\tif signer == nil {\n\t\tsigner = priv\n\t}\n\tif crt, err = x509util.CreateCertificate(crt, parent, pub, signer); err != nil {\n\t\tpanic(err)\n\t}\n\treturn crt, priv\n}\n\nfunc mustSerializeCrt(filename string, certs ...*x509.Certificate) {\n\tbuf := new(bytes.Buffer)\n\tfor _, c := range certs {\n\t\tif err := pem.Encode(buf, &pem.Block{\n\t\t\tType:  \"CERTIFICATE\",\n\t\t\tBytes: c.Raw,\n\t\t}); err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t}\n\tif err := os.WriteFile(filename, buf.Bytes(), 0600); err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc mustSerializeKey(filename string, key crypto.Signer) {\n\tb, err := x509.MarshalPKCS8PrivateKey(key)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tb = pem.EncodeToMemory(&pem.Block{\n\t\tType:  \"PRIVATE KEY\",\n\t\tBytes: b,\n\t})\n\tif err := os.WriteFile(filename, b, 0600); err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc mustEncryptKey(filename string, key crypto.Signer) {\n\t_, err := pemutil.Serialize(key,\n\t\tpemutil.ToFile(filename, 0600),\n\t\tpemutil.WithPKCS8(true),\n\t\tpemutil.WithPassword([]byte(testPassword)))\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc testCAHelper(t *testing.T) (*url.URL, *ca.Client) {\n\tt.Helper()\n\n\twriteJSON := func(w http.ResponseWriter, v interface{}) {\n\t\t_ = json.NewEncoder(w).Encode(v)\n\t}\n\tparseJSON := func(r *http.Request, v interface{}) {\n\t\t_ = json.NewDecoder(r.Body).Decode(v)\n\t}\n\tsrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tswitch r.RequestURI {\n\t\tcase \"/root/\" + testRootFingerprint:\n\t\t\tw.WriteHeader(http.StatusOK)\n\t\t\twriteJSON(w, api.RootResponse{\n\t\t\t\tRootPEM: api.NewCertificate(testRootCrt),\n\t\t\t})\n\t\tcase \"/sign\":\n\t\t\tvar msg api.SignRequest\n\t\t\tparseJSON(r, &msg)\n\t\t\tif msg.CsrPEM.DNSNames[0] == \"fail.doe.org\" {\n\t\t\t\tw.WriteHeader(http.StatusBadRequest)\n\t\t\t\tfmt.Fprintf(w, `{\"error\":\"fail\",\"message\":\"fail\"}`)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tw.WriteHeader(http.StatusOK)\n\t\t\twriteJSON(w, api.SignResponse{\n\t\t\t\tCertChainPEM: []api.Certificate{api.NewCertificate(testCrt), api.NewCertificate(testIssCrt)},\n\t\t\t})\n\t\tcase \"/renew\":\n\t\t\tif r.Header.Get(\"Authorization\") == \"Bearer fail\" {\n\t\t\t\tw.WriteHeader(http.StatusBadRequest)\n\t\t\t\tfmt.Fprintf(w, `{\"error\":\"fail\",\"message\":\"fail\"}`)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tw.WriteHeader(http.StatusOK)\n\t\t\twriteJSON(w, api.SignResponse{\n\t\t\t\tCertChainPEM: []api.Certificate{api.NewCertificate(testCrt), api.NewCertificate(testIssCrt)},\n\t\t\t})\n\t\tcase \"/revoke\":\n\t\t\tvar msg api.RevokeRequest\n\t\t\tparseJSON(r, &msg)\n\t\t\tif msg.Serial == \"fail\" {\n\t\t\t\tw.WriteHeader(http.StatusBadRequest)\n\t\t\t\tfmt.Fprintf(w, `{\"error\":\"fail\",\"message\":\"fail\"}`)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tw.WriteHeader(http.StatusOK)\n\t\t\twriteJSON(w, api.RevokeResponse{\n\t\t\t\tStatus: \"ok\",\n\t\t\t})\n\t\tcase \"/provisioners\":\n\t\t\tw.WriteHeader(http.StatusOK)\n\t\t\twriteJSON(w, api.ProvisionersResponse{\n\t\t\t\tNextCursor: \"cursor\",\n\t\t\t\tProvisioners: []provisioner.Interface{\n\t\t\t\t\t&provisioner.JWK{\n\t\t\t\t\t\tType:         \"JWK\",\n\t\t\t\t\t\tName:         \"ra@doe.org\",\n\t\t\t\t\t\tKey:          &jose.JSONWebKey{KeyID: testKeyID, Key: testX5CKey.Public()},\n\t\t\t\t\t\tEncryptedKey: testEncryptedJWKKey,\n\t\t\t\t\t},\n\t\t\t\t\t&provisioner.JWK{\n\t\t\t\t\t\tType: \"JWK\",\n\t\t\t\t\t\tName: \"empty@doe.org\",\n\t\t\t\t\t\tKey:  &jose.JSONWebKey{KeyID: testKeyID, Key: testX5CKey.Public()},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t})\n\t\tcase \"/provisioners?cursor=cursor\":\n\t\t\tw.WriteHeader(http.StatusOK)\n\t\t\twriteJSON(w, api.ProvisionersResponse{})\n\t\tdefault:\n\t\t\tw.WriteHeader(http.StatusNotFound)\n\t\t\tfmt.Fprintf(w, `{\"error\":\"not found\"}`)\n\t\t}\n\t}))\n\tt.Cleanup(func() {\n\t\tsrv.Close()\n\t})\n\tu, err := url.Parse(srv.URL)\n\tif err != nil {\n\t\tsrv.Close()\n\t\tt.Fatal(err)\n\t}\n\n\tclient, err := ca.NewClient(srv.URL, ca.WithTransport(http.DefaultTransport))\n\tif err != nil {\n\t\tsrv.Close()\n\t\tt.Fatal(err)\n\t}\n\n\treturn u, client\n}\n\nfunc testX5CIssuer(t *testing.T, caURL *url.URL, password string) *x5cIssuer {\n\tt.Helper()\n\tkey, givenPassword := testX5CKeyPath, password\n\tif password != \"\" {\n\t\tkey = testEncryptedKeyPath\n\t\tpassword = testPassword\n\t}\n\tx5c, err := newX5CIssuer(caURL, &apiv1.CertificateIssuer{\n\t\tType:        \"x5c\",\n\t\tProvisioner: \"X5C\",\n\t\tCertificate: testX5CPath,\n\t\tKey:         key,\n\t\tPassword:    password,\n\t})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tx5c.password = givenPassword\n\treturn x5c\n}\n\nfunc testJWKIssuer(t *testing.T, caURL *url.URL, password string) *jwkIssuer {\n\tt.Helper()\n\tclient, err := ca.NewClient(caURL.String(), ca.WithTransport(http.DefaultTransport))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tkey := testX5CKeyPath\n\tif password != \"\" {\n\t\tkey = testEncryptedKeyPath\n\t\tpassword = testPassword\n\t}\n\tjwk, err := newJWKIssuer(context.TODO(), caURL, client, &apiv1.CertificateIssuer{\n\t\tType:        \"jwk\",\n\t\tProvisioner: \"ra@doe.org\",\n\t\tKey:         key,\n\t\tPassword:    password,\n\t})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\treturn jwk\n}\n\nfunc TestMain(m *testing.M) {\n\ttestRootCrt, testRootKey = mustSignCertificate(\"Test Root Certificate\", nil, x509util.DefaultRootTemplate, nil, nil)\n\ttestIssCrt, testIssKey = mustSignCertificate(\"Test Intermediate Certificate\", nil, x509util.DefaultIntermediateTemplate, testRootCrt, testRootKey)\n\ttestX5CCrt, testX5CKey = mustSignCertificate(\"Test X5C Certificate\", nil, x509util.DefaultLeafTemplate, testIssCrt, testIssKey)\n\ttestRootFingerprint = x509util.Fingerprint(testRootCrt)\n\n\t// Final certificate.\n\tvar err error\n\tsans := []string{\"doe.org\", \"jane@doe.org\", \"127.0.0.1\", \"::1\", \"localhost\", \"uuid:f81d4fae-7dec-11d0-a765-00a0c91e6bf6;name=value\"}\n\ttestCrt, testKey = mustSignCertificate(\"Test Certificate\", sans, x509util.DefaultLeafTemplate, testIssCrt, testIssKey)\n\ttestCR, err = x509util.CreateCertificateRequest(\"Test Certificate\", sans, testKey)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// CR used in errors.\n\ttestFailCR, err = x509util.CreateCertificateRequest(\"\", []string{\"fail.doe.org\"}, testKey)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// Password used to encrypt the key.\n\ttestPassword, err = randutil.Hex(32)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// Encrypted JWK key used when the key is downloaded from the CA.\n\tjwe, err := jose.EncryptJWK(&jose.JSONWebKey{Key: testX5CKey}, []byte(testPassword))\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\ttestEncryptedJWKKey, err = jwe.CompactSerialize()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\ttestKeyID, err = jose.Thumbprint(&jose.JSONWebKey{Key: testX5CKey})\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// Create test files.\n\tpath, err := os.MkdirTemp(os.TempDir(), \"stepcas\")\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\ttestRootPath = filepath.Join(path, \"root_ca.crt\")\n\ttestRootKeyPath = filepath.Join(path, \"root_ca.key\")\n\tmustSerializeCrt(testRootPath, testRootCrt)\n\tmustSerializeKey(testRootKeyPath, testRootKey)\n\n\ttestIssPath = filepath.Join(path, \"intermediate_ca.crt\")\n\ttestIssKeyPath = filepath.Join(path, \"intermediate_ca.key\")\n\tmustSerializeCrt(testIssPath, testIssCrt)\n\tmustSerializeKey(testIssKeyPath, testIssKey)\n\n\ttestX5CPath = filepath.Join(path, \"x5c.crt\")\n\ttestX5CKeyPath = filepath.Join(path, \"x5c.key\")\n\tmustSerializeCrt(testX5CPath, testX5CCrt, testIssCrt)\n\tmustSerializeKey(testX5CKeyPath, testX5CKey)\n\n\ttestEncryptedKeyPath = filepath.Join(path, \"x5c.enc.key\")\n\tmustEncryptKey(testEncryptedKeyPath, testX5CKey)\n\n\tcode := m.Run()\n\tif err := os.RemoveAll(path); err != nil {\n\t\tpanic(err)\n\t}\n\tos.Exit(code)\n}\n\nfunc Test_init(t *testing.T) {\n\tcaURL, _ := testCAHelper(t)\n\n\tfn, ok := apiv1.LoadCertificateAuthorityServiceNewFunc(apiv1.StepCAS)\n\tif !ok {\n\t\tt.Errorf(\"apiv1.Register() ok = %v, want true\", ok)\n\t\treturn\n\t}\n\tfn(context.Background(), apiv1.Options{\n\t\tCertificateAuthority:            caURL.String(),\n\t\tCertificateAuthorityFingerprint: testRootFingerprint,\n\t\tCertificateIssuer: &apiv1.CertificateIssuer{\n\t\t\tType:        \"x5c\",\n\t\t\tProvisioner: \"X5C\",\n\t\t\tCertificate: testX5CPath,\n\t\t\tKey:         testX5CKeyPath,\n\t\t},\n\t})\n}\n\nfunc TestNew(t *testing.T) {\n\tcaURL, client := testCAHelper(t)\n\tsigner, err := newJWKSignerFromEncryptedKey(testKeyID, testEncryptedJWKKey, testPassword)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\ttype args struct {\n\t\tctx  context.Context\n\t\topts apiv1.Options\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\targs    args\n\t\twant    *StepCAS\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", args{context.TODO(), apiv1.Options{\n\t\t\tCertificateAuthority:            caURL.String(),\n\t\t\tCertificateAuthorityFingerprint: testRootFingerprint,\n\t\t\tCertificateIssuer: &apiv1.CertificateIssuer{\n\t\t\t\tType:        \"x5c\",\n\t\t\t\tProvisioner: \"X5C\",\n\t\t\t\tCertificate: testX5CPath,\n\t\t\t\tKey:         testX5CKeyPath,\n\t\t\t},\n\t\t}}, &StepCAS{\n\t\t\tiss: &x5cIssuer{\n\t\t\t\tcaURL:    caURL,\n\t\t\t\tcertFile: testX5CPath,\n\t\t\t\tkeyFile:  testX5CKeyPath,\n\t\t\t\tissuer:   \"X5C\",\n\t\t\t},\n\t\t\tclient:      client,\n\t\t\tfingerprint: testRootFingerprint,\n\t\t}, false},\n\t\t{\"ok jwk\", args{context.TODO(), apiv1.Options{\n\t\t\tCertificateAuthority:            caURL.String(),\n\t\t\tCertificateAuthorityFingerprint: testRootFingerprint,\n\t\t\tCertificateIssuer: &apiv1.CertificateIssuer{\n\t\t\t\tType:        \"jwk\",\n\t\t\t\tProvisioner: \"ra@doe.org\",\n\t\t\t\tKey:         testX5CKeyPath,\n\t\t\t},\n\t\t}}, &StepCAS{\n\t\t\tiss: &jwkIssuer{\n\t\t\t\tcaURL:  caURL,\n\t\t\t\tissuer: \"ra@doe.org\",\n\t\t\t\tsigner: signer,\n\t\t\t},\n\t\t\tclient:      client,\n\t\t\tfingerprint: testRootFingerprint,\n\t\t}, false},\n\t\t{\"ok jwk provisioners\", args{context.TODO(), apiv1.Options{\n\t\t\tCertificateAuthority:            caURL.String(),\n\t\t\tCertificateAuthorityFingerprint: testRootFingerprint,\n\t\t\tCertificateIssuer: &apiv1.CertificateIssuer{\n\t\t\t\tType:        \"jwk\",\n\t\t\t\tProvisioner: \"ra@doe.org\",\n\t\t\t\tPassword:    testPassword,\n\t\t\t},\n\t\t}}, &StepCAS{\n\t\t\tiss: &jwkIssuer{\n\t\t\t\tcaURL:  caURL,\n\t\t\t\tissuer: \"ra@doe.org\",\n\t\t\t\tsigner: signer,\n\t\t\t},\n\t\t\tclient:      client,\n\t\t\tfingerprint: testRootFingerprint,\n\t\t}, false},\n\t\t{\"ok ca getter\", args{context.TODO(), apiv1.Options{\n\t\t\tIsCAGetter:                      true,\n\t\t\tCertificateAuthority:            caURL.String(),\n\t\t\tCertificateAuthorityFingerprint: testRootFingerprint,\n\t\t\tCertificateIssuer: &apiv1.CertificateIssuer{\n\t\t\t\tType:        \"jwk\",\n\t\t\t\tProvisioner: \"ra@doe.org\",\n\t\t\t},\n\t\t}}, &StepCAS{\n\t\t\tiss:         nil,\n\t\t\tclient:      client,\n\t\t\tfingerprint: testRootFingerprint,\n\t\t}, false},\n\t\t{\"fail authority\", args{context.TODO(), apiv1.Options{\n\t\t\tCertificateAuthority:            \"\",\n\t\t\tCertificateAuthorityFingerprint: testRootFingerprint,\n\t\t\tCertificateIssuer: &apiv1.CertificateIssuer{\n\t\t\t\tType:        \"x5c\",\n\t\t\t\tProvisioner: \"X5C\",\n\t\t\t\tCertificate: testX5CPath,\n\t\t\t\tKey:         testX5CKeyPath,\n\t\t\t},\n\t\t}}, nil, true},\n\t\t{\"fail fingerprint\", args{context.TODO(), apiv1.Options{\n\t\t\tCertificateAuthority:            caURL.String(),\n\t\t\tCertificateAuthorityFingerprint: \"\",\n\t\t\tCertificateIssuer: &apiv1.CertificateIssuer{\n\t\t\t\tType:        \"x5c\",\n\t\t\t\tProvisioner: \"X5C\",\n\t\t\t\tCertificate: testX5CPath,\n\t\t\t\tKey:         testX5CKeyPath,\n\t\t\t},\n\t\t}}, nil, true},\n\t\t{\"fail type\", args{context.TODO(), apiv1.Options{\n\t\t\tCertificateAuthority:            caURL.String(),\n\t\t\tCertificateAuthorityFingerprint: testRootFingerprint,\n\t\t\tCertificateIssuer: &apiv1.CertificateIssuer{\n\t\t\t\tType:        \"\",\n\t\t\t\tProvisioner: \"X5C\",\n\t\t\t\tCertificate: testX5CPath,\n\t\t\t\tKey:         testX5CKeyPath,\n\t\t\t},\n\t\t}}, nil, true},\n\t\t{\"fail provisioner\", args{context.TODO(), apiv1.Options{\n\t\t\tCertificateAuthority:            caURL.String(),\n\t\t\tCertificateAuthorityFingerprint: testRootFingerprint,\n\t\t\tCertificateIssuer: &apiv1.CertificateIssuer{\n\t\t\t\tType:        \"x5c\",\n\t\t\t\tProvisioner: \"\",\n\t\t\t\tCertificate: testX5CPath,\n\t\t\t\tKey:         testX5CKeyPath,\n\t\t\t},\n\t\t}}, nil, true},\n\t\t{\"fail provisioner jwk\", args{context.TODO(), apiv1.Options{\n\t\t\tCertificateAuthority:            caURL.String(),\n\t\t\tCertificateAuthorityFingerprint: testRootFingerprint,\n\t\t\tCertificateIssuer: &apiv1.CertificateIssuer{\n\t\t\t\tType:        \"jwk\",\n\t\t\t\tProvisioner: \"\",\n\t\t\t\tKey:         testX5CKeyPath,\n\t\t\t},\n\t\t}}, nil, true},\n\t\t{\"fail provisioner not found\", args{context.TODO(), apiv1.Options{\n\t\t\tCertificateAuthority:            caURL.String(),\n\t\t\tCertificateAuthorityFingerprint: testRootFingerprint,\n\t\t\tCertificateIssuer: &apiv1.CertificateIssuer{\n\t\t\t\tType:        \"jwk\",\n\t\t\t\tProvisioner: \"notfound@doe.org\",\n\t\t\t\tPassword:    testPassword,\n\t\t\t},\n\t\t}}, nil, true},\n\t\t{\"fail invalid password\", args{context.TODO(), apiv1.Options{\n\t\t\tCertificateAuthority:            caURL.String(),\n\t\t\tCertificateAuthorityFingerprint: testRootFingerprint,\n\t\t\tCertificateIssuer: &apiv1.CertificateIssuer{\n\t\t\t\tType:        \"jwk\",\n\t\t\t\tProvisioner: \"ra@doe.org\",\n\t\t\t\tPassword:    \"bad-password\",\n\t\t\t},\n\t\t}}, nil, true},\n\t\t{\"fail no key\", args{context.TODO(), apiv1.Options{\n\t\t\tCertificateAuthority:            caURL.String(),\n\t\t\tCertificateAuthorityFingerprint: testRootFingerprint,\n\t\t\tCertificateIssuer: &apiv1.CertificateIssuer{\n\t\t\t\tType:        \"jwk\",\n\t\t\t\tProvisioner: \"empty@doe.org\",\n\t\t\t\tPassword:    testPassword,\n\t\t\t},\n\t\t}}, nil, true},\n\t\t{\"fail certificate\", args{context.TODO(), apiv1.Options{\n\t\t\tCertificateAuthority:            caURL.String(),\n\t\t\tCertificateAuthorityFingerprint: testRootFingerprint,\n\t\t\tCertificateIssuer: &apiv1.CertificateIssuer{\n\t\t\t\tType:        \"x5c\",\n\t\t\t\tProvisioner: \"X5C\",\n\t\t\t\tCertificate: \"\",\n\t\t\t\tKey:         testX5CKeyPath,\n\t\t\t},\n\t\t}}, nil, true},\n\t\t{\"fail key\", args{context.TODO(), apiv1.Options{\n\t\t\tCertificateAuthority:            caURL.String(),\n\t\t\tCertificateAuthorityFingerprint: testRootFingerprint,\n\t\t\tCertificateIssuer: &apiv1.CertificateIssuer{\n\t\t\t\tType:        \"x5c\",\n\t\t\t\tProvisioner: \"X5C\",\n\t\t\t\tCertificate: testX5CPath,\n\t\t\t\tKey:         \"\",\n\t\t\t},\n\t\t}}, nil, true},\n\t\t{\"fail key jwk\", args{context.TODO(), apiv1.Options{\n\t\t\tCertificateAuthority:            caURL.String(),\n\t\t\tCertificateAuthorityFingerprint: testRootFingerprint,\n\t\t\tCertificateIssuer: &apiv1.CertificateIssuer{\n\t\t\t\tType:        \"jwk\",\n\t\t\t\tProvisioner: \"ra@smallstep.com\",\n\t\t\t\tKey:         \"\",\n\t\t\t},\n\t\t}}, nil, true},\n\t\t{\"bad authority\", args{context.TODO(), apiv1.Options{\n\t\t\tCertificateAuthority:            \"https://foobar\",\n\t\t\tCertificateAuthorityFingerprint: testRootFingerprint,\n\t\t\tCertificateIssuer: &apiv1.CertificateIssuer{\n\t\t\t\tType:        \"x5c\",\n\t\t\t\tProvisioner: \"X5C\",\n\t\t\t\tCertificate: testX5CPath,\n\t\t\t\tKey:         testX5CKeyPath,\n\t\t\t},\n\t\t}}, nil, true},\n\t\t{\"fail parse url\", args{context.TODO(), apiv1.Options{\n\t\t\tCertificateAuthority:            \"::failparse\",\n\t\t\tCertificateAuthorityFingerprint: testRootFingerprint,\n\t\t\tCertificateIssuer: &apiv1.CertificateIssuer{\n\t\t\t\tType:        \"x5c\",\n\t\t\t\tProvisioner: \"X5C\",\n\t\t\t\tCertificate: testX5CPath,\n\t\t\t\tKey:         testX5CKeyPath,\n\t\t\t},\n\t\t}}, nil, true},\n\t\t{\"fail new client\", args{context.TODO(), apiv1.Options{\n\t\t\tCertificateAuthority:            caURL.String(),\n\t\t\tCertificateAuthorityFingerprint: \"foobar\",\n\t\t\tCertificateIssuer: &apiv1.CertificateIssuer{\n\t\t\t\tType:        \"x5c\",\n\t\t\t\tProvisioner: \"X5C\",\n\t\t\t\tCertificate: testX5CPath,\n\t\t\t\tKey:         testX5CKeyPath,\n\t\t\t},\n\t\t}}, nil, true},\n\t\t{\"fail new x5c issuer\", args{context.TODO(), apiv1.Options{\n\t\t\tCertificateAuthority:            caURL.String(),\n\t\t\tCertificateAuthorityFingerprint: testRootFingerprint,\n\t\t\tCertificateIssuer: &apiv1.CertificateIssuer{\n\t\t\t\tType:        \"x5c\",\n\t\t\t\tProvisioner: \"X5C\",\n\t\t\t\tCertificate: testX5CPath + \".missing\",\n\t\t\t\tKey:         testX5CKeyPath,\n\t\t\t},\n\t\t}}, nil, true},\n\t\t{\"fail new jwk issuer\", args{context.TODO(), apiv1.Options{\n\t\t\tCertificateAuthority:            caURL.String(),\n\t\t\tCertificateAuthorityFingerprint: testRootFingerprint,\n\t\t\tCertificateIssuer: &apiv1.CertificateIssuer{\n\t\t\t\tType:        \"jwk\",\n\t\t\t\tProvisioner: \"ra@doe.org\",\n\t\t\t\tKey:         testX5CKeyPath + \".missing\",\n\t\t\t},\n\t\t}}, nil, true},\n\t\t{\"bad issuer\", args{context.TODO(), apiv1.Options{\n\t\t\tCertificateAuthority:            caURL.String(),\n\t\t\tCertificateAuthorityFingerprint: testRootFingerprint,\n\t\t\tCertificateIssuer:               nil}}, nil, true},\n\t\t{\"bad issuer type\", args{context.TODO(), apiv1.Options{\n\t\t\tCertificateAuthority:            caURL.String(),\n\t\t\tCertificateAuthorityFingerprint: testRootFingerprint,\n\t\t\tCertificateIssuer: &apiv1.CertificateIssuer{\n\t\t\t\tType:        \"fail\",\n\t\t\t\tProvisioner: \"X5C\",\n\t\t\t\tCertificate: testX5CPath,\n\t\t\t\tKey:         testX5CKeyPath,\n\t\t\t},\n\t\t}}, nil, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := New(tt.args.ctx, tt.args.opts)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"New() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\t// We cannot compare neither the client nor the signer.\n\t\t\tif got != nil && tt.want != nil {\n\t\t\t\tgot.client = tt.want.client\n\t\t\t\tif jwk, ok := got.iss.(*jwkIssuer); ok {\n\t\t\t\t\tjwk.signer = signer\n\t\t\t\t}\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"New() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestStepCAS_Type(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\twant apiv1.Type\n\t}{\n\t\t{\"ok\", apiv1.StepCAS},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tc := &StepCAS{}\n\t\t\tif got := c.Type(); got != tt.want {\n\t\t\t\tt.Errorf(\"StepCAS.Type() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestStepCAS_CreateCertificate(t *testing.T) {\n\tcaURL, client := testCAHelper(t)\n\tx5c := testX5CIssuer(t, caURL, \"\")\n\tjwk := testJWKIssuer(t, caURL, \"\")\n\tx5cEnc := testX5CIssuer(t, caURL, testPassword)\n\tjwkEnc := testJWKIssuer(t, caURL, testPassword)\n\tx5cBad := testX5CIssuer(t, caURL, \"bad-password\")\n\n\ttestTemplate := &x509.Certificate{\n\t\tSubject:        testCR.Subject,\n\t\tDNSNames:       testCR.DNSNames,\n\t\tEmailAddresses: testCR.EmailAddresses,\n\t\tIPAddresses:    testCR.IPAddresses,\n\t\tURIs:           testCR.URIs,\n\t}\n\n\ttestOtherCR, err := x509util.CreateCertificateRequest(\"Test Certificate\", []string{\"test.example.com\"}, testKey)\n\trequire.NoError(t, err)\n\n\ttype fields struct {\n\t\tiss         stepIssuer\n\t\tclient      *ca.Client\n\t\tfingerprint string\n\t}\n\ttype args struct {\n\t\treq *apiv1.CreateCertificateRequest\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tfields  fields\n\t\targs    args\n\t\twant    *apiv1.CreateCertificateResponse\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", fields{x5c, client, testRootFingerprint}, args{&apiv1.CreateCertificateRequest{\n\t\t\tCSR:      testCR,\n\t\t\tTemplate: testTemplate,\n\t\t\tLifetime: time.Hour,\n\t\t}}, &apiv1.CreateCertificateResponse{\n\t\t\tCertificate:      testCrt,\n\t\t\tCertificateChain: []*x509.Certificate{testIssCrt},\n\t\t}, false},\n\t\t{\"ok with different CSR\", fields{x5c, client, testRootFingerprint}, args{&apiv1.CreateCertificateRequest{\n\t\t\tCSR:      testOtherCR,\n\t\t\tTemplate: testTemplate,\n\t\t\tLifetime: time.Hour,\n\t\t}}, &apiv1.CreateCertificateResponse{\n\t\t\tCertificate:      testCrt,\n\t\t\tCertificateChain: []*x509.Certificate{testIssCrt},\n\t\t}, false},\n\t\t{\"ok with password\", fields{x5cEnc, client, testRootFingerprint}, args{&apiv1.CreateCertificateRequest{\n\t\t\tCSR:      testCR,\n\t\t\tTemplate: testTemplate,\n\t\t\tLifetime: time.Hour,\n\t\t}}, &apiv1.CreateCertificateResponse{\n\t\t\tCertificate:      testCrt,\n\t\t\tCertificateChain: []*x509.Certificate{testIssCrt},\n\t\t}, false},\n\t\t{\"ok jwk\", fields{jwk, client, testRootFingerprint}, args{&apiv1.CreateCertificateRequest{\n\t\t\tCSR:      testCR,\n\t\t\tTemplate: testTemplate,\n\t\t\tLifetime: time.Hour,\n\t\t}}, &apiv1.CreateCertificateResponse{\n\t\t\tCertificate:      testCrt,\n\t\t\tCertificateChain: []*x509.Certificate{testIssCrt},\n\t\t}, false},\n\t\t{\"ok jwk with password\", fields{jwkEnc, client, testRootFingerprint}, args{&apiv1.CreateCertificateRequest{\n\t\t\tCSR:      testCR,\n\t\t\tTemplate: testTemplate,\n\t\t\tLifetime: time.Hour,\n\t\t}}, &apiv1.CreateCertificateResponse{\n\t\t\tCertificate:      testCrt,\n\t\t\tCertificateChain: []*x509.Certificate{testIssCrt},\n\t\t}, false},\n\t\t{\"ok with provisioner\", fields{jwk, client, testRootFingerprint}, args{&apiv1.CreateCertificateRequest{\n\t\t\tCSR:         testCR,\n\t\t\tTemplate:    testTemplate,\n\t\t\tLifetime:    time.Hour,\n\t\t\tProvisioner: &apiv1.ProvisionerInfo{ID: \"provisioner-id\", Type: \"ACME\"},\n\t\t}}, &apiv1.CreateCertificateResponse{\n\t\t\tCertificate:      testCrt,\n\t\t\tCertificateChain: []*x509.Certificate{testIssCrt},\n\t\t}, false},\n\t\t{\"ok with server cert\", fields{jwk, client, testRootFingerprint}, args{&apiv1.CreateCertificateRequest{\n\t\t\tCSR:            testCR,\n\t\t\tTemplate:       testTemplate,\n\t\t\tLifetime:       time.Hour,\n\t\t\tIsCAServerCert: true,\n\t\t}}, &apiv1.CreateCertificateResponse{\n\t\t\tCertificate:      testCrt,\n\t\t\tCertificateChain: []*x509.Certificate{testIssCrt},\n\t\t}, false},\n\t\t{\"fail CSR\", fields{x5c, client, testRootFingerprint}, args{&apiv1.CreateCertificateRequest{\n\t\t\tCSR:      nil,\n\t\t\tTemplate: testTemplate,\n\t\t\tLifetime: time.Hour,\n\t\t}}, nil, true},\n\t\t{\"fail Template\", fields{x5c, client, testRootFingerprint}, args{&apiv1.CreateCertificateRequest{\n\t\t\tCSR:      testCR,\n\t\t\tTemplate: nil,\n\t\t\tLifetime: time.Hour,\n\t\t}}, nil, true},\n\t\t{\"fail lifetime\", fields{x5c, client, testRootFingerprint}, args{&apiv1.CreateCertificateRequest{\n\t\t\tCSR:      testCR,\n\t\t\tLifetime: 0,\n\t\t}}, nil, true},\n\t\t{\"fail sign token\", fields{mockErrIssuer{}, client, testRootFingerprint}, args{&apiv1.CreateCertificateRequest{\n\t\t\tCSR:      testCR,\n\t\t\tLifetime: time.Hour,\n\t\t}}, nil, true},\n\t\t{\"fail client sign\", fields{x5c, client, testRootFingerprint}, args{&apiv1.CreateCertificateRequest{\n\t\t\tCSR:      testFailCR,\n\t\t\tLifetime: time.Hour,\n\t\t}}, nil, true},\n\t\t{\"fail password\", fields{x5cBad, client, testRootFingerprint}, args{&apiv1.CreateCertificateRequest{\n\t\t\tCSR:      testCR,\n\t\t\tLifetime: time.Hour,\n\t\t}}, nil, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ts := &StepCAS{\n\t\t\t\tiss:         tt.fields.iss,\n\t\t\t\tclient:      tt.fields.client,\n\t\t\t\tauthorityID: \"authority-id\",\n\t\t\t\tfingerprint: tt.fields.fingerprint,\n\t\t\t}\n\t\t\tgot, err := s.CreateCertificate(tt.args.req)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"StepCAS.CreateCertificate() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"StepCAS.CreateCertificate() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestStepCAS_RenewCertificate(t *testing.T) {\n\tcaURL, client := testCAHelper(t)\n\tjwk := testJWKIssuer(t, caURL, \"\")\n\n\ttokenIssuer := testX5CIssuer(t, caURL, \"\")\n\ttoken, err := tokenIssuer.SignToken(\"test\", []string{\"test.example.com\"}, nil)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\ttype fields struct {\n\t\tiss         stepIssuer\n\t\tclient      *ca.Client\n\t\tfingerprint string\n\t}\n\ttype args struct {\n\t\treq *apiv1.RenewCertificateRequest\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tfields  fields\n\t\targs    args\n\t\twant    *apiv1.RenewCertificateResponse\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", fields{jwk, client, testRootFingerprint}, args{&apiv1.RenewCertificateRequest{\n\t\t\tTemplate: &x509.Certificate{},\n\t\t\tBackdate: time.Minute,\n\t\t\tLifetime: time.Hour,\n\t\t\tToken:    token,\n\t\t}}, &apiv1.RenewCertificateResponse{\n\t\t\tCertificate:      testCrt,\n\t\t\tCertificateChain: []*x509.Certificate{testIssCrt},\n\t\t}, false},\n\t\t{\"fail no token\", fields{jwk, client, testRootFingerprint}, args{&apiv1.RenewCertificateRequest{\n\t\t\tTemplate: &x509.Certificate{},\n\t\t\tBackdate: time.Minute,\n\t\t\tLifetime: time.Hour,\n\t\t}}, nil, true},\n\t\t{\"fail bad token\", fields{jwk, client, testRootFingerprint}, args{&apiv1.RenewCertificateRequest{\n\t\t\tTemplate: &x509.Certificate{},\n\t\t\tBackdate: time.Minute,\n\t\t\tLifetime: time.Hour,\n\t\t\tToken:    \"fail\",\n\t\t}}, nil, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ts := &StepCAS{\n\t\t\t\tiss:         tt.fields.iss,\n\t\t\t\tclient:      tt.fields.client,\n\t\t\t\tfingerprint: tt.fields.fingerprint,\n\t\t\t}\n\t\t\tgot, err := s.RenewCertificate(tt.args.req)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"StepCAS.RenewCertificate() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Error(reflect.DeepEqual(got.Certificate, tt.want.Certificate))\n\t\t\t\tt.Error(reflect.DeepEqual(got.CertificateChain, tt.want.CertificateChain))\n\n\t\t\t\tt.Errorf(\"StepCAS.RenewCertificate() = %v, want %v\", got.Certificate.Subject, tt.want.Certificate.Subject)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestStepCAS_RevokeCertificate(t *testing.T) {\n\tcaURL, client := testCAHelper(t)\n\tx5c := testX5CIssuer(t, caURL, \"\")\n\tjwk := testJWKIssuer(t, caURL, \"\")\n\tx5cEnc := testX5CIssuer(t, caURL, testPassword)\n\tjwkEnc := testJWKIssuer(t, caURL, testPassword)\n\tx5cBad := testX5CIssuer(t, caURL, \"bad-password\")\n\n\ttype fields struct {\n\t\tiss         stepIssuer\n\t\tclient      *ca.Client\n\t\tfingerprint string\n\t}\n\ttype args struct {\n\t\treq *apiv1.RevokeCertificateRequest\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tfields  fields\n\t\targs    args\n\t\twant    *apiv1.RevokeCertificateResponse\n\t\twantErr bool\n\t}{\n\t\t{\"ok serial number\", fields{x5c, client, testRootFingerprint}, args{&apiv1.RevokeCertificateRequest{\n\t\t\tSerialNumber: \"ok\",\n\t\t\tCertificate:  nil,\n\t\t}}, &apiv1.RevokeCertificateResponse{}, false},\n\t\t{\"ok certificate\", fields{x5c, client, testRootFingerprint}, args{&apiv1.RevokeCertificateRequest{\n\t\t\tSerialNumber: \"\",\n\t\t\tCertificate:  testCrt,\n\t\t}}, &apiv1.RevokeCertificateResponse{\n\t\t\tCertificate: testCrt,\n\t\t}, false},\n\t\t{\"ok both\", fields{x5c, client, testRootFingerprint}, args{&apiv1.RevokeCertificateRequest{\n\t\t\tSerialNumber: \"ok\",\n\t\t\tCertificate:  testCrt,\n\t\t}}, &apiv1.RevokeCertificateResponse{\n\t\t\tCertificate: testCrt,\n\t\t}, false},\n\t\t{\"ok with password\", fields{x5cEnc, client, testRootFingerprint}, args{&apiv1.RevokeCertificateRequest{\n\t\t\tSerialNumber: \"ok\",\n\t\t\tCertificate:  nil,\n\t\t}}, &apiv1.RevokeCertificateResponse{}, false},\n\t\t{\"ok serial number jwk\", fields{jwk, client, testRootFingerprint}, args{&apiv1.RevokeCertificateRequest{\n\t\t\tSerialNumber: \"ok\",\n\t\t\tCertificate:  nil,\n\t\t}}, &apiv1.RevokeCertificateResponse{}, false},\n\t\t{\"ok certificate jwk\", fields{jwk, client, testRootFingerprint}, args{&apiv1.RevokeCertificateRequest{\n\t\t\tSerialNumber: \"\",\n\t\t\tCertificate:  testCrt,\n\t\t}}, &apiv1.RevokeCertificateResponse{\n\t\t\tCertificate: testCrt,\n\t\t}, false},\n\t\t{\"ok both jwk\", fields{jwk, client, testRootFingerprint}, args{&apiv1.RevokeCertificateRequest{\n\t\t\tSerialNumber: \"ok\",\n\t\t\tCertificate:  testCrt,\n\t\t}}, &apiv1.RevokeCertificateResponse{\n\t\t\tCertificate: testCrt,\n\t\t}, false},\n\t\t{\"ok jwk with password\", fields{jwkEnc, client, testRootFingerprint}, args{&apiv1.RevokeCertificateRequest{\n\t\t\tSerialNumber: \"ok\",\n\t\t\tCertificate:  nil,\n\t\t}}, &apiv1.RevokeCertificateResponse{}, false},\n\t\t{\"fail request\", fields{x5c, client, testRootFingerprint}, args{&apiv1.RevokeCertificateRequest{\n\t\t\tSerialNumber: \"\",\n\t\t\tCertificate:  nil,\n\t\t}}, nil, true},\n\t\t{\"fail revoke token\", fields{mockErrIssuer{}, client, testRootFingerprint}, args{&apiv1.RevokeCertificateRequest{\n\t\t\tSerialNumber: \"ok\",\n\t\t}}, nil, true},\n\t\t{\"fail client revoke\", fields{x5c, client, testRootFingerprint}, args{&apiv1.RevokeCertificateRequest{\n\t\t\tSerialNumber: \"fail\",\n\t\t}}, nil, true},\n\t\t{\"fail password\", fields{x5cBad, client, testRootFingerprint}, args{&apiv1.RevokeCertificateRequest{\n\t\t\tSerialNumber: \"ok\",\n\t\t\tCertificate:  nil,\n\t\t}}, nil, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ts := &StepCAS{\n\t\t\t\tiss:         tt.fields.iss,\n\t\t\t\tclient:      tt.fields.client,\n\t\t\t\tfingerprint: tt.fields.fingerprint,\n\t\t\t}\n\t\t\tgot, err := s.RevokeCertificate(tt.args.req)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"StepCAS.RevokeCertificate() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"StepCAS.RevokeCertificate() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestStepCAS_GetCertificateAuthority(t *testing.T) {\n\tcaURL, client := testCAHelper(t)\n\tx5c := testX5CIssuer(t, caURL, \"\")\n\tjwk := testJWKIssuer(t, caURL, \"\")\n\n\ttype fields struct {\n\t\tiss         stepIssuer\n\t\tclient      *ca.Client\n\t\tfingerprint string\n\t}\n\ttype args struct {\n\t\treq *apiv1.GetCertificateAuthorityRequest\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tfields  fields\n\t\targs    args\n\t\twant    *apiv1.GetCertificateAuthorityResponse\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", fields{x5c, client, testRootFingerprint}, args{&apiv1.GetCertificateAuthorityRequest{\n\t\t\tName: caURL.String(),\n\t\t}}, &apiv1.GetCertificateAuthorityResponse{\n\t\t\tRootCertificate: testRootCrt,\n\t\t}, false},\n\t\t{\"ok jwk\", fields{jwk, client, testRootFingerprint}, args{&apiv1.GetCertificateAuthorityRequest{\n\t\t\tName: caURL.String(),\n\t\t}}, &apiv1.GetCertificateAuthorityResponse{\n\t\t\tRootCertificate: testRootCrt,\n\t\t}, false},\n\t\t{\"fail fingerprint\", fields{x5c, client, \"fail\"}, args{&apiv1.GetCertificateAuthorityRequest{\n\t\t\tName: caURL.String(),\n\t\t}}, nil, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ts := &StepCAS{\n\t\t\t\tiss:         tt.fields.iss,\n\t\t\t\tclient:      tt.fields.client,\n\t\t\t\tfingerprint: tt.fields.fingerprint,\n\t\t\t}\n\t\t\tgot, err := s.GetCertificateAuthority(tt.args.req)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"StepCAS.GetCertificateAuthority() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"StepCAS.GetCertificateAuthority() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "cas/stepcas/x5c_issuer.go",
    "content": "package stepcas\n\nimport (\n\t\"crypto\"\n\t\"crypto/ecdsa\"\n\t\"crypto/ed25519\"\n\t\"crypto/rsa\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n\t\"github.com/smallstep/certificates/cas/apiv1\"\n\t\"go.step.sm/crypto/jose\"\n\t\"go.step.sm/crypto/pemutil\"\n\t\"go.step.sm/crypto/randutil\"\n)\n\nconst defaultValidity = 5 * time.Minute\n\n// timeNow returns the current time.\n// This method is used for unit testing purposes.\nvar timeNow = time.Now\n\ntype x5cIssuer struct {\n\tcaURL    *url.URL\n\tissuer   string\n\tcertFile string\n\tkeyFile  string\n\tpassword string\n}\n\n// newX5CIssuer create a new x5c token issuer. The given configuration should be\n// already validate.\nfunc newX5CIssuer(caURL *url.URL, cfg *apiv1.CertificateIssuer) (*x5cIssuer, error) {\n\t_, err := newX5CSigner(cfg.Certificate, cfg.Key, cfg.Password)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &x5cIssuer{\n\t\tcaURL:    caURL,\n\t\tissuer:   cfg.Provisioner,\n\t\tcertFile: cfg.Certificate,\n\t\tkeyFile:  cfg.Key,\n\t\tpassword: cfg.Password,\n\t}, nil\n}\n\nfunc (i *x5cIssuer) SignToken(subject string, sans []string, info *raInfo) (string, error) {\n\taud := i.caURL.ResolveReference(&url.URL{\n\t\tPath:     \"/1.0/sign\",\n\t\tFragment: \"x5c/\" + i.issuer,\n\t}).String()\n\n\treturn i.createToken(aud, subject, sans, info)\n}\n\nfunc (i *x5cIssuer) RevokeToken(subject string) (string, error) {\n\taud := i.caURL.ResolveReference(&url.URL{\n\t\tPath:     \"/1.0/revoke\",\n\t\tFragment: \"x5c/\" + i.issuer,\n\t}).String()\n\n\treturn i.createToken(aud, subject, nil, nil)\n}\n\nfunc (i *x5cIssuer) Lifetime(d time.Duration) time.Duration {\n\tcert, err := pemutil.ReadCertificate(i.certFile, pemutil.WithFirstBlock())\n\tif err != nil {\n\t\treturn d\n\t}\n\tnow := timeNow()\n\tif now.Add(d + time.Minute).After(cert.NotAfter) {\n\t\treturn cert.NotAfter.Sub(now) - time.Minute\n\t}\n\treturn d\n}\n\nfunc (i *x5cIssuer) createToken(aud, sub string, sans []string, info *raInfo) (string, error) {\n\tsigner, err := newX5CSigner(i.certFile, i.keyFile, i.password)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tid, err := randutil.Hex(64) // 256 bits\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tclaims := defaultClaims(i.issuer, sub, aud, id)\n\tbuilder := jose.Signed(signer).Claims(claims)\n\tif len(sans) > 0 {\n\t\tbuilder = builder.Claims(map[string]interface{}{\n\t\t\t\"sans\": sans,\n\t\t})\n\t}\n\tif info != nil {\n\t\tbuilder = builder.Claims(map[string]interface{}{\n\t\t\t\"step\": map[string]interface{}{\n\t\t\t\t\"ra\": info,\n\t\t\t},\n\t\t})\n\t}\n\n\ttok, err := builder.CompactSerialize()\n\tif err != nil {\n\t\treturn \"\", errors.Wrap(err, \"error signing token\")\n\t}\n\n\treturn tok, nil\n}\n\nfunc defaultClaims(iss, sub, aud, id string) jose.Claims {\n\tnow := timeNow()\n\treturn jose.Claims{\n\t\tID:        id,\n\t\tIssuer:    iss,\n\t\tSubject:   sub,\n\t\tAudience:  jose.Audience{aud},\n\t\tExpiry:    jose.NewNumericDate(now.Add(defaultValidity)),\n\t\tNotBefore: jose.NewNumericDate(now),\n\t\tIssuedAt:  jose.NewNumericDate(now),\n\t}\n}\n\nfunc readKey(keyFile, password string) (crypto.Signer, error) {\n\tvar opts []pemutil.Options\n\tif password != \"\" {\n\t\topts = append(opts, pemutil.WithPassword([]byte(password)))\n\t}\n\tkey, err := pemutil.Read(keyFile, opts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tsigner, ok := key.(crypto.Signer)\n\tif !ok {\n\t\treturn nil, errors.New(\"key is not a crypto.Signer\")\n\t}\n\treturn signer, nil\n}\n\nfunc newX5CSigner(certFile, keyFile, password string) (jose.Signer, error) {\n\tsigner, err := readKey(keyFile, password)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tkid, err := jose.Thumbprint(&jose.JSONWebKey{Key: signer.Public()})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tcerts, err := pemutil.ReadCertificateBundle(certFile)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"error reading x5c certificate chain\")\n\t}\n\tcertStrs, err := jose.ValidateX5C(certs, signer)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"error validating x5c certificate chain and key\")\n\t}\n\n\tso := new(jose.SignerOptions)\n\tso.WithType(\"JWT\")\n\tso.WithHeader(\"kid\", kid)\n\tso.WithHeader(\"x5c\", certStrs)\n\treturn newJoseSigner(signer, so)\n}\n\nfunc newJoseSigner(key crypto.Signer, so *jose.SignerOptions) (jose.Signer, error) {\n\tvar alg jose.SignatureAlgorithm\n\tswitch k := key.Public().(type) {\n\tcase *ecdsa.PublicKey:\n\t\tswitch k.Curve.Params().Name {\n\t\tcase \"P-256\":\n\t\t\talg = jose.ES256\n\t\tcase \"P-384\":\n\t\t\talg = jose.ES384\n\t\tcase \"P-521\":\n\t\t\talg = jose.ES512\n\t\tdefault:\n\t\t\treturn nil, errors.Errorf(\"unsupported elliptic curve %s\", k.Curve.Params().Name)\n\t\t}\n\tcase ed25519.PublicKey:\n\t\talg = jose.EdDSA\n\tcase *rsa.PublicKey:\n\t\talg = jose.DefaultRSASigAlgorithm\n\tdefault:\n\t\treturn nil, errors.Errorf(\"unsupported key type %T\", k)\n\t}\n\n\tsigner, err := jose.NewSigner(jose.SigningKey{Algorithm: alg, Key: key}, so)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"error creating jose.Signer\")\n\t}\n\treturn signer, nil\n}\n"
  },
  {
    "path": "cas/stepcas/x5c_issuer_test.go",
    "content": "package stepcas\n\nimport (\n\t\"crypto\"\n\t\"crypto/ecdsa\"\n\t\"crypto/ed25519\"\n\t\"crypto/elliptic\"\n\t\"crypto/rand\"\n\t\"crypto/rsa\"\n\t\"io\"\n\t\"net/url\"\n\t\"reflect\"\n\t\"testing\"\n\t\"time\"\n\n\t\"go.step.sm/crypto/jose\"\n)\n\ntype noneSigner []byte\n\nfunc (b noneSigner) Public() crypto.PublicKey {\n\treturn []byte(b)\n}\n\nfunc (b noneSigner) Sign(_ io.Reader, digest []byte, _ crypto.SignerOpts) (signature []byte, err error) {\n\treturn digest, nil\n}\n\n//nolint:gocritic // ignore sloppy test func name\nfunc fakeTime(t *testing.T) {\n\tt.Helper()\n\ttmp := timeNow\n\tt.Cleanup(func() {\n\t\ttimeNow = tmp\n\t})\n\ttimeNow = func() time.Time {\n\t\treturn testX5CCrt.NotBefore\n\t}\n}\n\nfunc Test_x5cIssuer_SignToken(t *testing.T) {\n\tcaURL, err := url.Parse(\"https://ca.smallstep.com\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\ttype fields struct {\n\t\tcaURL    *url.URL\n\t\tcertFile string\n\t\tkeyFile  string\n\t\tissuer   string\n\t}\n\ttype args struct {\n\t\tsubject string\n\t\tsans    []string\n\t\tinfo    *raInfo\n\t}\n\ttype stepClaims struct {\n\t\tRA *raInfo `json:\"ra\"`\n\t}\n\ttype claims struct {\n\t\tAud  jose.Audience `json:\"aud\"`\n\t\tSub  string        `json:\"sub\"`\n\t\tSans []string      `json:\"sans\"`\n\t\tStep stepClaims    `json:\"step\"`\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tfields  fields\n\t\targs    args\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", fields{caURL, testX5CPath, testX5CKeyPath, \"X5C\"}, args{\"doe\", []string{\"doe.org\"}, nil}, false},\n\t\t{\"ok ra\", fields{caURL, testX5CPath, testX5CKeyPath, \"X5C\"}, args{\"doe\", []string{\"doe.org\"}, &raInfo{\n\t\t\tAuthorityID: \"authority-id\", ProvisionerID: \"provisioner-id\", ProvisionerType: \"provisioner-type\",\n\t\t}}, false},\n\t\t{\"ok ra endpoint id\", fields{caURL, testX5CPath, testX5CKeyPath, \"X5C\"}, args{\"doe\", []string{\"doe.org\"}, &raInfo{\n\t\t\tAuthorityID: \"authority-id\", EndpointID: \"endpoint-id\",\n\t\t}}, false},\n\t\t{\"fail crt\", fields{caURL, \"\", testX5CKeyPath, \"X5C\"}, args{\"doe\", []string{\"doe.org\"}, nil}, true},\n\t\t{\"fail key\", fields{caURL, testX5CPath, \"\", \"X5C\"}, args{\"doe\", []string{\"doe.org\"}, nil}, true},\n\t\t{\"fail no signer\", fields{caURL, testIssKeyPath, testIssPath, \"X5C\"}, args{\"doe\", []string{\"doe.org\"}, nil}, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ti := &x5cIssuer{\n\t\t\t\tcaURL:    tt.fields.caURL,\n\t\t\t\tcertFile: tt.fields.certFile,\n\t\t\t\tkeyFile:  tt.fields.keyFile,\n\t\t\t\tissuer:   tt.fields.issuer,\n\t\t\t}\n\t\t\tgot, err := i.SignToken(tt.args.subject, tt.args.sans, tt.args.info)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"x5cIssuer.SignToken() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\t\t\tif !tt.wantErr {\n\t\t\t\tjwt, err := jose.ParseSigned(got)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Errorf(\"jose.ParseSigned() error = %v\", err)\n\t\t\t\t}\n\t\t\t\tvar c claims\n\t\t\t\twant := claims{\n\t\t\t\t\tAud:  []string{tt.fields.caURL.String() + \"/1.0/sign#x5c/X5C\"},\n\t\t\t\t\tSub:  tt.args.subject,\n\t\t\t\t\tSans: tt.args.sans,\n\t\t\t\t}\n\t\t\t\tif tt.args.info != nil {\n\t\t\t\t\twant.Step.RA = tt.args.info\n\t\t\t\t}\n\t\t\t\tif err := jwt.Claims(testX5CKey.Public(), &c); err != nil {\n\t\t\t\t\tt.Errorf(\"jwt.Claims() error = %v\", err)\n\t\t\t\t}\n\t\t\t\tif !reflect.DeepEqual(c, want) {\n\t\t\t\t\tt.Errorf(\"jwt.Claims() claims = %#v, want %#v\", c, want)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_x5cIssuer_RevokeToken(t *testing.T) {\n\tcaURL, err := url.Parse(\"https://ca.smallstep.com\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\ttype fields struct {\n\t\tcaURL    *url.URL\n\t\tcertFile string\n\t\tkeyFile  string\n\t\tissuer   string\n\t}\n\ttype args struct {\n\t\tsubject string\n\t}\n\ttype claims struct {\n\t\tAud  jose.Audience `json:\"aud\"`\n\t\tSub  string        `json:\"sub\"`\n\t\tSans []string      `json:\"sans\"`\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tfields  fields\n\t\targs    args\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", fields{caURL, testX5CPath, testX5CKeyPath, \"X5C\"}, args{\"doe\"}, false},\n\t\t{\"fail crt\", fields{caURL, \"\", testX5CKeyPath, \"X5C\"}, args{\"doe\"}, true},\n\t\t{\"fail key\", fields{caURL, testX5CPath, \"\", \"X5C\"}, args{\"doe\"}, true},\n\t\t{\"fail no signer\", fields{caURL, testIssKeyPath, testIssPath, \"X5C\"}, args{\"doe\"}, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ti := &x5cIssuer{\n\t\t\t\tcaURL:    tt.fields.caURL,\n\t\t\t\tcertFile: tt.fields.certFile,\n\t\t\t\tkeyFile:  tt.fields.keyFile,\n\t\t\t\tissuer:   tt.fields.issuer,\n\t\t\t}\n\t\t\tgot, err := i.RevokeToken(tt.args.subject)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"x5cIssuer.RevokeToken() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !tt.wantErr {\n\t\t\t\tjwt, err := jose.ParseSigned(got)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Errorf(\"jose.ParseSigned() error = %v\", err)\n\t\t\t\t}\n\t\t\t\tvar c claims\n\t\t\t\twant := claims{\n\t\t\t\t\tAud: []string{tt.fields.caURL.String() + \"/1.0/revoke#x5c/X5C\"},\n\t\t\t\t\tSub: tt.args.subject,\n\t\t\t\t}\n\t\t\t\tif err := jwt.Claims(testX5CKey.Public(), &c); err != nil {\n\t\t\t\t\tt.Errorf(\"jwt.Claims() error = %v\", err)\n\t\t\t\t}\n\t\t\t\tif !reflect.DeepEqual(c, want) {\n\t\t\t\t\tt.Errorf(\"jwt.Claims() claims = %#v, want %#v\", c, want)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_x5cIssuer_Lifetime(t *testing.T) {\n\tfakeTime(t)\n\tcaURL, err := url.Parse(\"https://ca.smallstep.com\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\t// With a leeway of 1m the max duration will be 59m.\n\tmaxDuration := testX5CCrt.NotAfter.Sub(timeNow()) - time.Minute\n\n\ttype fields struct {\n\t\tcaURL    *url.URL\n\t\tcertFile string\n\t\tkeyFile  string\n\t\tissuer   string\n\t}\n\ttype args struct {\n\t\td time.Duration\n\t}\n\ttests := []struct {\n\t\tname   string\n\t\tfields fields\n\t\targs   args\n\t\twant   time.Duration\n\t}{\n\t\t{\"ok 0s\", fields{caURL, testX5CPath, testX5CKeyPath, \"X5C\"}, args{0}, 0},\n\t\t{\"ok 1m\", fields{caURL, testX5CPath, testX5CKeyPath, \"X5C\"}, args{time.Minute}, time.Minute},\n\t\t{\"ok max-1m\", fields{caURL, testX5CPath, testX5CKeyPath, \"X5C\"}, args{maxDuration - time.Minute}, maxDuration - time.Minute},\n\t\t{\"ok max\", fields{caURL, testX5CPath, testX5CKeyPath, \"X5C\"}, args{maxDuration}, maxDuration},\n\t\t{\"ok max+1m\", fields{caURL, testX5CPath, testX5CKeyPath, \"X5C\"}, args{maxDuration + time.Minute}, maxDuration},\n\t\t{\"ok fail\", fields{caURL, testX5CPath + \".missing\", testX5CKeyPath, \"X5C\"}, args{maxDuration + time.Minute}, maxDuration + time.Minute},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ti := &x5cIssuer{\n\t\t\t\tcaURL:    tt.fields.caURL,\n\t\t\t\tcertFile: tt.fields.certFile,\n\t\t\t\tkeyFile:  tt.fields.keyFile,\n\t\t\t\tissuer:   tt.fields.issuer,\n\t\t\t}\n\t\t\tif got := i.Lifetime(tt.args.d); got != tt.want {\n\t\t\t\tt.Errorf(\"x5cIssuer.Lifetime() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_newJoseSigner(t *testing.T) {\n\tmustSigner := func(args ...interface{}) crypto.Signer {\n\t\tif err := args[len(args)-1]; err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tfor _, a := range args {\n\t\t\tif s, ok := a.(crypto.Signer); ok {\n\t\t\t\treturn s\n\t\t\t}\n\t\t}\n\t\tt.Fatal(\"signer not found\")\n\t\treturn nil\n\t}\n\n\tp224 := mustSigner(ecdsa.GenerateKey(elliptic.P224(), rand.Reader))\n\tp256 := mustSigner(ecdsa.GenerateKey(elliptic.P256(), rand.Reader))\n\tp384 := mustSigner(ecdsa.GenerateKey(elliptic.P384(), rand.Reader))\n\tp521 := mustSigner(ecdsa.GenerateKey(elliptic.P521(), rand.Reader))\n\tedKey := mustSigner(ed25519.GenerateKey(rand.Reader))\n\trsaKey := mustSigner(rsa.GenerateKey(rand.Reader, 2048))\n\n\ttype args struct {\n\t\tkey crypto.Signer\n\t\tso  *jose.SignerOptions\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\targs    args\n\t\twant    []jose.Header\n\t\twantErr bool\n\t}{\n\t\t{\"p256\", args{p256, nil}, []jose.Header{{Algorithm: \"ES256\"}}, false},\n\t\t{\"p384\", args{p384, new(jose.SignerOptions).WithType(\"JWT\")}, []jose.Header{{Algorithm: \"ES384\", ExtraHeaders: map[jose.HeaderKey]interface{}{\"typ\": \"JWT\"}}}, false},\n\t\t{\"p521\", args{p521, new(jose.SignerOptions).WithHeader(\"kid\", \"the-kid\")}, []jose.Header{{Algorithm: \"ES512\", KeyID: \"the-kid\"}}, false},\n\t\t{\"ed25519\", args{edKey, nil}, []jose.Header{{Algorithm: \"EdDSA\"}}, false},\n\t\t{\"rsa\", args{rsaKey, nil}, []jose.Header{{Algorithm: \"RS256\"}}, false},\n\t\t{\"fail p224\", args{p224, nil}, nil, true},\n\t\t{\"fail signer\", args{noneSigner{1, 2, 3}, nil}, nil, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := newJoseSigner(tt.args.key, tt.args.so)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"newJoseSigner() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !tt.wantErr {\n\t\t\t\tjws, err := got.Sign([]byte(\"{}\"))\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Errorf(\"jose.Signer.Sign() err = %v\", err)\n\t\t\t\t}\n\t\t\t\tjwt, err := jose.ParseSigned(jws.FullSerialize())\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Errorf(\"jose.ParseSigned() err = %v\", err)\n\t\t\t\t}\n\t\t\t\tif !reflect.DeepEqual(jwt.Headers, tt.want) {\n\t\t\t\t\tt.Errorf(\"jose.Header got = %v, want = %v\", jwt.Headers, tt.want)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "cas/vaultcas/auth/approle/approle.go",
    "content": "package approle\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\n\t\"github.com/hashicorp/vault/api/auth/approle\"\n)\n\n// AuthOptions defines the configuration options added using the\n// VaultOptions.AuthOptions field when AuthType is approle\ntype AuthOptions struct {\n\tRoleID          string `json:\"roleID,omitempty\"`\n\tSecretID        string `json:\"secretID,omitempty\"`\n\tSecretIDFile    string `json:\"secretIDFile,omitempty\"`\n\tSecretIDEnv     string `json:\"secretIDEnv,omitempty\"`\n\tIsWrappingToken bool   `json:\"isWrappingToken,omitempty\"`\n}\n\nfunc NewApproleAuthMethod(mountPath string, options json.RawMessage) (*approle.AppRoleAuth, error) {\n\tvar opts *AuthOptions\n\n\terr := json.Unmarshal(options, &opts)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error decoding AppRole auth options: %w\", err)\n\t}\n\n\tvar approleAuth *approle.AppRoleAuth\n\n\tvar loginOptions []approle.LoginOption\n\tif mountPath != \"\" {\n\t\tloginOptions = append(loginOptions, approle.WithMountPath(mountPath))\n\t}\n\tif opts.IsWrappingToken {\n\t\tloginOptions = append(loginOptions, approle.WithWrappingToken())\n\t}\n\n\tif opts.RoleID == \"\" {\n\t\treturn nil, errors.New(\"you must set roleID\")\n\t}\n\n\tvar sid approle.SecretID\n\tswitch {\n\tcase opts.SecretID != \"\" && opts.SecretIDFile == \"\" && opts.SecretIDEnv == \"\":\n\t\tsid = approle.SecretID{\n\t\t\tFromString: opts.SecretID,\n\t\t}\n\tcase opts.SecretIDFile != \"\" && opts.SecretID == \"\" && opts.SecretIDEnv == \"\":\n\t\tsid = approle.SecretID{\n\t\t\tFromFile: opts.SecretIDFile,\n\t\t}\n\tcase opts.SecretIDEnv != \"\" && opts.SecretIDFile == \"\" && opts.SecretID == \"\":\n\t\tsid = approle.SecretID{\n\t\t\tFromEnv: opts.SecretIDEnv,\n\t\t}\n\tdefault:\n\t\treturn nil, errors.New(\"you must set one of secretID, secretIDFile or secretIDEnv\")\n\t}\n\n\tapproleAuth, err = approle.NewAppRoleAuth(opts.RoleID, &sid, loginOptions...)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to initialize Kubernetes auth method: %w\", err)\n\t}\n\n\treturn approleAuth, nil\n}\n"
  },
  {
    "path": "cas/vaultcas/auth/approle/approle_test.go",
    "content": "package approle\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\tvault \"github.com/hashicorp/vault/api\"\n)\n\nfunc testCAHelper(t *testing.T) (*url.URL, *vault.Client) {\n\tt.Helper()\n\n\tsrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tswitch r.RequestURI {\n\t\tcase \"/v1/auth/approle/login\":\n\t\t\tw.WriteHeader(http.StatusOK)\n\t\t\tfmt.Fprintf(w, `{\n\t\t\t\t  \"auth\": {\n\t\t\t\t\t\"client_token\": \"hvs.0000\"\n\t\t\t\t  }\n\t\t\t\t}`)\n\t\tcase \"/v1/auth/custom-approle/login\":\n\t\t\tw.WriteHeader(http.StatusOK)\n\t\t\tfmt.Fprintf(w, `{\n\t\t\t\t  \"auth\": {\n\t\t\t\t\t\"client_token\": \"hvs.9999\"\n\t\t\t\t  }\n\t\t\t\t}`)\n\t\tdefault:\n\t\t\tw.WriteHeader(http.StatusNotFound)\n\t\t\tfmt.Fprintf(w, `{\"error\":\"not found\"}`)\n\t\t}\n\t}))\n\tt.Cleanup(func() {\n\t\tsrv.Close()\n\t})\n\tu, err := url.Parse(srv.URL)\n\tif err != nil {\n\t\tsrv.Close()\n\t\tt.Fatal(err)\n\t}\n\n\tconfig := vault.DefaultConfig()\n\tconfig.Address = srv.URL\n\n\tclient, err := vault.NewClient(config)\n\tif err != nil {\n\t\tsrv.Close()\n\t\tt.Fatal(err)\n\t}\n\n\treturn u, client\n}\n\nfunc TestApprole_LoginMountPaths(t *testing.T) {\n\tcaURL, _ := testCAHelper(t)\n\n\tconfig := vault.DefaultConfig()\n\tconfig.Address = caURL.String()\n\tclient, _ := vault.NewClient(config)\n\n\ttests := []struct {\n\t\tname      string\n\t\tmountPath string\n\t\ttoken     string\n\t}{\n\t\t{\n\t\t\tname:      \"ok default mount path\",\n\t\t\tmountPath: \"\",\n\t\t\ttoken:     \"hvs.0000\",\n\t\t},\n\t\t{\n\t\t\tname:      \"ok explicit mount path\",\n\t\t\tmountPath: \"approle\",\n\t\t\ttoken:     \"hvs.0000\",\n\t\t},\n\t\t{\n\t\t\tname:      \"ok custom mount path\",\n\t\t\tmountPath: \"custom-approle\",\n\t\t\ttoken:     \"hvs.9999\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tmethod, err := NewApproleAuthMethod(tt.mountPath, json.RawMessage(`{\"RoleID\":\"roleID\",\"SecretID\":\"secretID\",\"IsWrappingToken\":false}`))\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"NewApproleAuthMethod() error = %v\", err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tsecret, err := client.Auth().Login(context.Background(), method)\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"Login() error = %v\", err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\ttoken, _ := secret.TokenID()\n\t\t\tif token != tt.token {\n\t\t\t\tt.Errorf(\"Token error got %v, expected %v\", token, tt.token)\n\t\t\t\treturn\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestApprole_NewApproleAuthMethod(t *testing.T) {\n\ttests := []struct {\n\t\tname      string\n\t\tmountPath string\n\t\traw       string\n\t\twantErr   bool\n\t}{\n\t\t{\n\t\t\t\"ok secret-id string\",\n\t\t\t\"\",\n\t\t\t`{\"RoleID\": \"0000-0000-0000-0000\", \"SecretID\": \"0000-0000-0000-0000\"}`,\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"ok secret-id string and wrapped\",\n\t\t\t\"\",\n\t\t\t`{\"RoleID\": \"0000-0000-0000-0000\", \"SecretID\": \"0000-0000-0000-0000\", \"isWrappedToken\": true}`,\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"ok secret-id string and wrapped with custom mountPath\",\n\t\t\t\"approle2\",\n\t\t\t`{\"RoleID\": \"0000-0000-0000-0000\", \"SecretID\": \"0000-0000-0000-0000\", \"isWrappedToken\": true}`,\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"ok secret-id file\",\n\t\t\t\"\",\n\t\t\t`{\"RoleID\": \"0000-0000-0000-0000\", \"SecretIDFile\": \"./secret-id\"}`,\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"ok secret-id env\",\n\t\t\t\"\",\n\t\t\t`{\"RoleID\": \"0000-0000-0000-0000\", \"SecretIDEnv\": \"VAULT_APPROLE_SECRETID\"}`,\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"fail mandatory role-id\",\n\t\t\t\"\",\n\t\t\t`{}`,\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"fail mandatory secret-id any\",\n\t\t\t\"\",\n\t\t\t`{\"RoleID\": \"0000-0000-0000-0000\"}`,\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"fail multiple secret-id types id and env\",\n\t\t\t\"\",\n\t\t\t`{\"RoleID\": \"0000-0000-0000-0000\", \"SecretID\": \"0000-0000-0000-0000\", \"SecretIDEnv\": \"VAULT_APPROLE_SECRETID\"}`,\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"fail multiple secret-id types id and file\",\n\t\t\t\"\",\n\t\t\t`{\"RoleID\": \"0000-0000-0000-0000\", \"SecretID\": \"0000-0000-0000-0000\", \"SecretIDFile\": \"./secret-id\"}`,\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"fail multiple secret-id types env and file\",\n\t\t\t\"\",\n\t\t\t`{\"RoleID\": \"0000-0000-0000-0000\", \"SecretIDFile\": \"./secret-id\", \"SecretIDEnv\": \"VAULT_APPROLE_SECRETID\"}`,\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"fail multiple secret-id types all\",\n\t\t\t\"\",\n\t\t\t`{\"RoleID\": \"0000-0000-0000-0000\", \"SecretID\": \"0000-0000-0000-0000\", \"SecretIDFile\": \"./secret-id\", \"SecretIDEnv\": \"VAULT_APPROLE_SECRETID\"}`,\n\t\t\ttrue,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\t_, err := NewApproleAuthMethod(tt.mountPath, json.RawMessage(tt.raw))\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"Approle.NewApproleAuthMethod() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "cas/vaultcas/auth/aws/aws.go",
    "content": "package aws\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\n\t\"github.com/hashicorp/vault/api/auth/aws\"\n)\n\n// AuthOptions defines the configuration options added using the\n// VaultOptions.AuthOptions field when AuthType is aws.\n// This maps directly to Vault's AWS Login options,\n// see: https://developer.hashicorp.com/vault/api-docs/auth/aws#login\ntype AuthOptions struct {\n\tRole        string `json:\"role,omitempty\"`\n\tRegion      string `json:\"region,omitempty\"`\n\tAwsAuthType string `json:\"awsAuthType,omitempty\"`\n\n\t// options specific to 'iam' auth type\n\tIamServerIDHeader string `json:\"iamServerIdHeader\"`\n\n\t// options specific to 'ec2' auth type\n\tSignatureType string `json:\"signatureType,omitempty\"`\n\tNonce         string `json:\"nonce,omitempty\"`\n}\n\nfunc NewAwsAuthMethod(mountPath string, options json.RawMessage) (*aws.AWSAuth, error) {\n\tvar opts *AuthOptions\n\n\terr := json.Unmarshal(options, &opts)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error decoding AWS auth options: %w\", err)\n\t}\n\n\tvar awsAuth *aws.AWSAuth\n\n\tvar loginOptions []aws.LoginOption\n\tif mountPath != \"\" {\n\t\tloginOptions = append(loginOptions, aws.WithMountPath(mountPath))\n\t}\n\tif opts.Role != \"\" {\n\t\tloginOptions = append(loginOptions, aws.WithRole(opts.Role))\n\t}\n\tif opts.Region != \"\" {\n\t\tloginOptions = append(loginOptions, aws.WithRegion(opts.Region))\n\t}\n\n\tswitch opts.AwsAuthType {\n\tcase \"iam\":\n\t\tloginOptions = append(loginOptions, aws.WithIAMAuth())\n\n\t\tif opts.IamServerIDHeader != \"\" {\n\t\t\tloginOptions = append(loginOptions, aws.WithIAMServerIDHeader(opts.IamServerIDHeader))\n\t\t}\n\tcase \"ec2\":\n\t\tloginOptions = append(loginOptions, aws.WithEC2Auth())\n\n\t\tswitch opts.SignatureType {\n\t\tcase \"pkcs7\":\n\t\t\tloginOptions = append(loginOptions, aws.WithPKCS7Signature())\n\t\tcase \"identity\":\n\t\t\tloginOptions = append(loginOptions, aws.WithIdentitySignature())\n\t\tcase \"rsa2048\":\n\t\t\tloginOptions = append(loginOptions, aws.WithRSA2048Signature())\n\t\tcase \"\":\n\t\t\t// no-op\n\t\tdefault:\n\t\t\treturn nil, fmt.Errorf(\"unknown SignatureType type %q; valid options are 'pkcs7', 'identity' and 'rsa2048'\", opts.SignatureType)\n\t\t}\n\n\t\tif opts.Nonce != \"\" {\n\t\t\tloginOptions = append(loginOptions, aws.WithNonce(opts.Nonce))\n\t\t}\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unknown awsAuthType %q; valid options are 'iam' and 'ec2'\", opts.AwsAuthType)\n\t}\n\n\tawsAuth, err = aws.NewAWSAuth(loginOptions...)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to initialize AWS auth method: %w\", err)\n\t}\n\n\treturn awsAuth, nil\n}\n"
  },
  {
    "path": "cas/vaultcas/auth/aws/aws_test.go",
    "content": "package aws\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\tvault \"github.com/hashicorp/vault/api\"\n)\n\nfunc testCAHelper(t *testing.T) (*url.URL, *vault.Client) {\n\tt.Helper()\n\n\tsrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tswitch r.RequestURI {\n\t\tcase \"/v1/auth/aws/login\":\n\t\t\tw.WriteHeader(http.StatusOK)\n\t\t\tfmt.Fprintf(w, `{\n\t\t\t\t  \"auth\": {\n\t\t\t\t\t\"client_token\": \"hvs.0000\"\n\t\t\t\t  }\n\t\t\t\t}`)\n\t\tcase \"/v1/auth/custom-aws/login\":\n\t\t\tw.WriteHeader(http.StatusOK)\n\t\t\tfmt.Fprintf(w, `{\n\t\t\t\t  \"auth\": {\n\t\t\t\t\t\"client_token\": \"hvs.9999\"\n\t\t\t\t  }\n\t\t\t\t}`)\n\t\tdefault:\n\t\t\tw.WriteHeader(http.StatusNotFound)\n\t\t\tfmt.Fprintf(w, `{\"error\":\"not found\"}`)\n\t\t}\n\t}))\n\tt.Cleanup(func() {\n\t\tsrv.Close()\n\t})\n\tu, err := url.Parse(srv.URL)\n\tif err != nil {\n\t\tsrv.Close()\n\t\tt.Fatal(err)\n\t}\n\n\tconfig := vault.DefaultConfig()\n\tconfig.Address = srv.URL\n\n\tclient, err := vault.NewClient(config)\n\tif err != nil {\n\t\tsrv.Close()\n\t\tt.Fatal(err)\n\t}\n\n\treturn u, client\n}\n\nfunc TestAws_LoginMountPaths(t *testing.T) {\n\t_, client := testCAHelper(t)\n\n\t// Dummy AWS credentials is needed for Vault client to sign the STS request\n\tt.Setenv(\"AWS_ACCESS_KEY_ID\", \"AKIAIOSFODNN7EXAMPLE\")\n\tt.Setenv(\"AWS_SECRET_ACCESS_KEY\", \"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY\")\n\n\ttests := []struct {\n\t\tname      string\n\t\tmountPath string\n\t\ttoken     string\n\t}{\n\t\t{\n\t\t\tname:      \"ok default mount path\",\n\t\t\tmountPath: \"\",\n\t\t\ttoken:     \"hvs.0000\",\n\t\t},\n\t\t{\n\t\t\tname:      \"ok explicit mount path\",\n\t\t\tmountPath: \"aws\",\n\t\t\ttoken:     \"hvs.0000\",\n\t\t},\n\t\t{\n\t\t\tname:      \"ok custom mount path\",\n\t\t\tmountPath: \"custom-aws\",\n\t\t\ttoken:     \"hvs.9999\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tmethod, err := NewAwsAuthMethod(tt.mountPath, json.RawMessage(`{\"role\":\"test-role\",\"awsAuthType\":\"iam\"}`))\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"NewAwsAuthMethod() error = %v\", err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tsecret, err := client.Auth().Login(context.Background(), method)\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"Login() error = %v\", err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\ttoken, _ := secret.TokenID()\n\t\t\tif token != tt.token {\n\t\t\t\tt.Errorf(\"Token error got %v, expected %v\", token, tt.token)\n\t\t\t\treturn\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestAws_NewAwsAuthMethod(t *testing.T) {\n\ttests := []struct {\n\t\tname      string\n\t\tmountPath string\n\t\traw       string\n\t\twantErr   bool\n\t}{\n\t\t{\n\t\t\t\"ok iam\",\n\t\t\t\"\",\n\t\t\t`{\"role\":\"test-role\",\"awsAuthType\":\"iam\"}`,\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"ok iam with region\",\n\t\t\t\"\",\n\t\t\t`{\"role\":\"test-role\",\"awsAuthType\":\"iam\",\"region\":\"us-east-1\"}`,\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"ok iam with header\",\n\t\t\t\"\",\n\t\t\t`{\"role\":\"test-role\",\"awsAuthType\":\"iam\",\"iamServerIdHeader\":\"vault.example.com\"}`,\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"ok ec2\",\n\t\t\t\"\",\n\t\t\t`{\"role\":\"test-role\",\"awsAuthType\":\"ec2\"}`,\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"ok ec2 with nonce\",\n\t\t\t\"\",\n\t\t\t`{\"role\":\"test-role\",\"awsAuthType\":\"ec2\",\"nonce\": \"0000-0000-0000-0000\"}`,\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"ok ec2 with signature type\",\n\t\t\t\"\",\n\t\t\t`{\"role\":\"test-role\",\"awsAuthType\":\"ec2\",\"signatureType\":\"rsa2048\"}`,\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"fail mandatory role\",\n\t\t\t\"\",\n\t\t\t`{}`,\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"fail mandatory auth type\",\n\t\t\t\"\",\n\t\t\t`{\"role\":\"test-role\"}`,\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"fail invalid auth type\",\n\t\t\t\"\",\n\t\t\t`{\"role\":\"test-role\",\"awsAuthType\":\"test\"}`,\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"fail invalid ec2 signature type\",\n\t\t\t\"\",\n\t\t\t`{\"role\":\"test-role\",\"awsAuthType\":\"test\",\"signatureType\":\"test\"}`,\n\t\t\ttrue,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\t_, err := NewAwsAuthMethod(tt.mountPath, json.RawMessage(tt.raw))\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"Aws.NewAwsAuthMethod() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "cas/vaultcas/auth/kubernetes/kubernetes.go",
    "content": "package kubernetes\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\n\t\"github.com/hashicorp/vault/api/auth/kubernetes\"\n)\n\n// AuthOptions defines the configuration options added using the\n// VaultOptions.AuthOptions field when AuthType is kubernetes\ntype AuthOptions struct {\n\tRole      string `json:\"role,omitempty\"`\n\tTokenPath string `json:\"tokenPath,omitempty\"`\n}\n\nfunc NewKubernetesAuthMethod(mountPath string, options json.RawMessage) (*kubernetes.KubernetesAuth, error) {\n\tvar opts *AuthOptions\n\n\terr := json.Unmarshal(options, &opts)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error decoding Kubernetes auth options: %w\", err)\n\t}\n\n\tvar kubernetesAuth *kubernetes.KubernetesAuth\n\n\tvar loginOptions []kubernetes.LoginOption\n\tif mountPath != \"\" {\n\t\tloginOptions = append(loginOptions, kubernetes.WithMountPath(mountPath))\n\t}\n\tif opts.TokenPath != \"\" {\n\t\tloginOptions = append(loginOptions, kubernetes.WithServiceAccountTokenPath(opts.TokenPath))\n\t}\n\n\tif opts.Role == \"\" {\n\t\treturn nil, errors.New(\"you must set role\")\n\t}\n\n\tkubernetesAuth, err = kubernetes.NewKubernetesAuth(\n\t\topts.Role,\n\t\tloginOptions...,\n\t)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to initialize Kubernetes auth method: %w\", err)\n\t}\n\n\treturn kubernetesAuth, nil\n}\n"
  },
  {
    "path": "cas/vaultcas/auth/kubernetes/kubernetes_test.go",
    "content": "package kubernetes\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"testing\"\n\n\tvault \"github.com/hashicorp/vault/api\"\n)\n\nfunc testCAHelper(t *testing.T) (*url.URL, *vault.Client) {\n\tt.Helper()\n\n\tsrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tswitch r.RequestURI {\n\t\tcase \"/v1/auth/kubernetes/login\":\n\t\t\tw.WriteHeader(http.StatusOK)\n\t\t\tfmt.Fprintf(w, `{\n\t\t\t\t  \"auth\": {\n\t\t\t\t\t\"client_token\": \"hvs.0000\"\n\t\t\t\t  }\n\t\t\t\t}`)\n\t\tcase \"/v1/auth/custom-kubernetes/login\":\n\t\t\tw.WriteHeader(http.StatusOK)\n\t\t\tfmt.Fprintf(w, `{\n\t\t\t\t  \"auth\": {\n\t\t\t\t\t\"client_token\": \"hvs.9999\"\n\t\t\t\t  }\n\t\t\t\t}`)\n\t\tdefault:\n\t\t\tw.WriteHeader(http.StatusNotFound)\n\t\t\tfmt.Fprintf(w, `{\"error\":\"not found\"}`)\n\t\t}\n\t}))\n\tt.Cleanup(func() {\n\t\tsrv.Close()\n\t})\n\tu, err := url.Parse(srv.URL)\n\tif err != nil {\n\t\tsrv.Close()\n\t\tt.Fatal(err)\n\t}\n\n\tconfig := vault.DefaultConfig()\n\tconfig.Address = srv.URL\n\n\tclient, err := vault.NewClient(config)\n\tif err != nil {\n\t\tsrv.Close()\n\t\tt.Fatal(err)\n\t}\n\n\treturn u, client\n}\n\nfunc TestApprole_LoginMountPaths(t *testing.T) {\n\tcaURL, _ := testCAHelper(t)\n\t_, filename, _, _ := runtime.Caller(0)\n\ttokenPath := filepath.Join(path.Dir(filename), \"token\")\n\n\tconfig := vault.DefaultConfig()\n\tconfig.Address = caURL.String()\n\tclient, _ := vault.NewClient(config)\n\n\ttests := []struct {\n\t\tname      string\n\t\tmountPath string\n\t\ttoken     string\n\t}{\n\t\t{\n\t\t\tname:      \"ok default mount path\",\n\t\t\tmountPath: \"\",\n\t\t\ttoken:     \"hvs.0000\",\n\t\t},\n\t\t{\n\t\t\tname:      \"ok explicit mount path\",\n\t\t\tmountPath: \"kubernetes\",\n\t\t\ttoken:     \"hvs.0000\",\n\t\t},\n\t\t{\n\t\t\tname:      \"ok custom mount path\",\n\t\t\tmountPath: \"custom-kubernetes\",\n\t\t\ttoken:     \"hvs.9999\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tmethod, err := NewKubernetesAuthMethod(tt.mountPath, json.RawMessage(`{\"role\": \"SomeRoleName\", \"tokenPath\": \"`+tokenPath+`\"}`))\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"NewApproleAuthMethod() error = %v\", err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tsecret, err := client.Auth().Login(context.Background(), method)\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"Login() error = %v\", err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\ttoken, _ := secret.TokenID()\n\t\t\tif token != tt.token {\n\t\t\t\tt.Errorf(\"Token error got %v, expected %v\", token, tt.token)\n\t\t\t\treturn\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestApprole_NewApproleAuthMethod(t *testing.T) {\n\t_, filename, _, _ := runtime.Caller(0)\n\ttokenPath := filepath.Join(path.Dir(filename), \"token\")\n\n\ttests := []struct {\n\t\tname      string\n\t\tmountPath string\n\t\traw       string\n\t\twantErr   bool\n\t}{\n\t\t{\n\t\t\t\"ok secret-id string\",\n\t\t\t\"\",\n\t\t\t`{\"role\": \"SomeRoleName\", \"tokenPath\": \"` + tokenPath + `\"}`,\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"fail mandatory role\",\n\t\t\t\"\",\n\t\t\t`{}`,\n\t\t\ttrue,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\t_, err := NewKubernetesAuthMethod(tt.mountPath, json.RawMessage(tt.raw))\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"Kubernetes.NewKubernetesAuthMethod() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "cas/vaultcas/auth/kubernetes/token",
    "content": "token"
  },
  {
    "path": "cas/vaultcas/vaultcas.go",
    "content": "package vaultcas\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/sha256\"\n\t\"crypto/x509\"\n\t\"encoding/hex\"\n\t\"encoding/json\"\n\t\"encoding/pem\"\n\t\"errors\"\n\t\"fmt\"\n\t\"math/big\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/smallstep/certificates/cas/apiv1\"\n\t\"github.com/smallstep/certificates/cas/vaultcas/auth/approle\"\n\t\"github.com/smallstep/certificates/cas/vaultcas/auth/aws\"\n\t\"github.com/smallstep/certificates/cas/vaultcas/auth/kubernetes\"\n\n\tvault \"github.com/hashicorp/vault/api\"\n)\n\nfunc init() {\n\tapiv1.Register(apiv1.VaultCAS, func(ctx context.Context, opts apiv1.Options) (apiv1.CertificateAuthorityService, error) {\n\t\treturn New(ctx, opts)\n\t})\n}\n\n// VaultOptions defines the configuration options added using the\n// apiv1.Options.Config field.\ntype VaultOptions struct {\n\tPKIMountPath   string          `json:\"pkiMountPath,omitempty\"`\n\tPKIRoleDefault string          `json:\"pkiRoleDefault,omitempty\"`\n\tPKIRoleRSA     string          `json:\"pkiRoleRSA,omitempty\"`\n\tPKIRoleEC      string          `json:\"pkiRoleEC,omitempty\"`\n\tPKIRoleEd25519 string          `json:\"pkiRoleEd25519,omitempty\"`\n\tAuthType       string          `json:\"authType,omitempty\"`\n\tAuthMountPath  string          `json:\"authMountPath,omitempty\"`\n\tNamespace      string          `json:\"namespace,omitempty\"`\n\tAuthOptions    json.RawMessage `json:\"authOptions,omitempty\"`\n}\n\n// VaultCAS implements a Certificate Authority Service using Hashicorp Vault.\ntype VaultCAS struct {\n\tclient      *vault.Client\n\tconfig      VaultOptions\n\tfingerprint string\n}\n\ntype certBundle struct {\n\tleaf          *x509.Certificate\n\tintermediates []*x509.Certificate\n\troot          *x509.Certificate\n}\n\n// New creates a new CertificateAuthorityService implementation\n// using Hashicorp Vault\nfunc New(ctx context.Context, opts apiv1.Options) (*VaultCAS, error) {\n\tif opts.CertificateAuthority == \"\" {\n\t\treturn nil, errors.New(\"vaultCAS 'certificateAuthority' cannot be empty\")\n\t}\n\n\tif opts.CertificateAuthorityFingerprint == \"\" {\n\t\treturn nil, errors.New(\"vaultCAS 'certificateAuthorityFingerprint' cannot be empty\")\n\t}\n\n\tvc, err := loadOptions(opts.Config)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tconfig := vault.DefaultConfig()\n\tconfig.Address = opts.CertificateAuthority\n\n\tclient, err := vault.NewClient(config)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to initialize vault client: %w\", err)\n\t}\n\n\tvar method vault.AuthMethod\n\tswitch vc.AuthType {\n\tcase \"kubernetes\":\n\t\tmethod, err = kubernetes.NewKubernetesAuthMethod(vc.AuthMountPath, vc.AuthOptions)\n\tcase \"approle\":\n\t\tmethod, err = approle.NewApproleAuthMethod(vc.AuthMountPath, vc.AuthOptions)\n\tcase \"aws\":\n\t\tmethod, err = aws.NewAwsAuthMethod(vc.AuthMountPath, vc.AuthOptions)\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unknown auth type: %s, only 'kubernetes' and 'approle' currently supported\", vc.AuthType)\n\t}\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to configure %s auth method: %w\", vc.AuthType, err)\n\t}\n\n\tif vc.Namespace != \"\" {\n\t\tclient.SetNamespace(vc.Namespace)\n\t}\n\n\tauthInfo, err := client.Auth().Login(ctx, method)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to login to %s auth method: %w\", vc.AuthType, err)\n\t}\n\tif authInfo == nil {\n\t\treturn nil, errors.New(\"no auth info was returned after login\")\n\t}\n\n\treturn &VaultCAS{\n\t\tclient:      client,\n\t\tconfig:      *vc,\n\t\tfingerprint: opts.CertificateAuthorityFingerprint,\n\t}, nil\n}\n\n// Type returns the type of this CertificateAuthorityService.\nfunc (v *VaultCAS) Type() apiv1.Type {\n\treturn apiv1.VaultCAS\n}\n\n// CreateCertificate signs a new certificate using Hashicorp Vault.\nfunc (v *VaultCAS) CreateCertificate(req *apiv1.CreateCertificateRequest) (*apiv1.CreateCertificateResponse, error) {\n\tswitch {\n\tcase req.CSR == nil:\n\t\treturn nil, errors.New(\"createCertificate `csr` cannot be nil\")\n\tcase req.Lifetime == 0:\n\t\treturn nil, errors.New(\"createCertificate `lifetime` cannot be 0\")\n\t}\n\n\tcert, chain, err := v.createCertificate(req.CSR, req.Lifetime)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &apiv1.CreateCertificateResponse{\n\t\tCertificate:      cert,\n\t\tCertificateChain: chain,\n\t}, nil\n}\n\n// GetCertificateAuthority returns the root certificate of the certificate\n// authority using the configured fingerprint.\nfunc (v *VaultCAS) GetCertificateAuthority(*apiv1.GetCertificateAuthorityRequest) (*apiv1.GetCertificateAuthorityResponse, error) {\n\tsecret, err := v.client.Logical().Read(v.config.PKIMountPath + \"/cert/ca_chain\")\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error reading ca chain: %w\", err)\n\t}\n\tif secret == nil {\n\t\treturn nil, errors.New(\"error reading ca chain: response is empty\")\n\t}\n\n\tchain, ok := secret.Data[\"certificate\"].(string)\n\tif !ok {\n\t\treturn nil, errors.New(\"error unmarshaling vault response: certificate not found\")\n\t}\n\n\tcert, err := getCertificateBundle(chain)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif cert.root == nil {\n\t\treturn nil, errors.New(\"error unmarshaling vault response: root certificate not found\")\n\t}\n\n\tsum := sha256.Sum256(cert.root.Raw)\n\tif !strings.EqualFold(v.fingerprint, strings.ToLower(hex.EncodeToString(sum[:]))) {\n\t\treturn nil, errors.New(\"error verifying vault root: fingerprint does not match\")\n\t}\n\n\treturn &apiv1.GetCertificateAuthorityResponse{\n\t\tRootCertificate:          cert.root,\n\t\tIntermediateCertificates: cert.intermediates,\n\t}, nil\n}\n\n// RenewCertificate will always return a non-implemented error as renewals\n// are not supported yet.\nfunc (v *VaultCAS) RenewCertificate(*apiv1.RenewCertificateRequest) (*apiv1.RenewCertificateResponse, error) {\n\treturn nil, apiv1.NotImplementedError{Message: \"vaultCAS does not support renewals\"}\n}\n\n// RevokeCertificate revokes a certificate by serial number.\nfunc (v *VaultCAS) RevokeCertificate(req *apiv1.RevokeCertificateRequest) (*apiv1.RevokeCertificateResponse, error) {\n\tif req.SerialNumber == \"\" && req.Certificate == nil {\n\t\treturn nil, errors.New(\"revokeCertificate `serialNumber` or `certificate` are required\")\n\t}\n\n\tvar sn *big.Int\n\tif req.SerialNumber != \"\" {\n\t\tvar ok bool\n\t\tif sn, ok = new(big.Int).SetString(req.SerialNumber, 10); !ok {\n\t\t\treturn nil, fmt.Errorf(\"error parsing serialNumber: %v cannot be converted to big.Int\", req.SerialNumber)\n\t\t}\n\t} else {\n\t\tsn = req.Certificate.SerialNumber\n\t}\n\n\tvaultReq := map[string]interface{}{\n\t\t\"serial_number\": formatSerialNumber(sn),\n\t}\n\t_, err := v.client.Logical().Write(v.config.PKIMountPath+\"/revoke/\", vaultReq)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error revoking certificate: %w\", err)\n\t}\n\n\treturn &apiv1.RevokeCertificateResponse{\n\t\tCertificate:      req.Certificate,\n\t\tCertificateChain: nil,\n\t}, nil\n}\n\nfunc (v *VaultCAS) createCertificate(cr *x509.CertificateRequest, lifetime time.Duration) (*x509.Certificate, []*x509.Certificate, error) {\n\tvar vaultPKIRole string\n\n\tswitch cr.PublicKeyAlgorithm {\n\tcase x509.RSA:\n\t\tvaultPKIRole = v.config.PKIRoleRSA\n\tcase x509.ECDSA:\n\t\tvaultPKIRole = v.config.PKIRoleEC\n\tcase x509.Ed25519:\n\t\tvaultPKIRole = v.config.PKIRoleEd25519\n\tdefault:\n\t\treturn nil, nil, fmt.Errorf(\"unsupported public key algorithm %v\", cr.PublicKeyAlgorithm)\n\t}\n\n\tvaultReq := map[string]interface{}{\n\t\t\"csr\": string(pem.EncodeToMemory(&pem.Block{\n\t\t\tType:  \"CERTIFICATE REQUEST\",\n\t\t\tBytes: cr.Raw,\n\t\t})),\n\t\t\"format\": \"pem_bundle\",\n\t\t\"ttl\":    lifetime.String(),\n\t}\n\n\tsecret, err := v.client.Logical().Write(v.config.PKIMountPath+\"/sign/\"+vaultPKIRole, vaultReq)\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"error signing certificate: %w\", err)\n\t}\n\tif secret == nil {\n\t\treturn nil, nil, errors.New(\"error signing certificate: response is empty\")\n\t}\n\n\tchain, ok := secret.Data[\"certificate\"].(string)\n\tif !ok {\n\t\treturn nil, nil, errors.New(\"error unmarshaling vault response: certificate not found\")\n\t}\n\n\tcert, err := getCertificateBundle(chain)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\n\t// Return certificate and certificate chain\n\treturn cert.leaf, cert.intermediates, nil\n}\n\nfunc loadOptions(config json.RawMessage) (*VaultOptions, error) {\n\t// setup default values\n\tvc := VaultOptions{\n\t\tPKIMountPath:   \"pki\",\n\t\tPKIRoleDefault: \"default\",\n\t}\n\n\terr := json.Unmarshal(config, &vc)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error decoding vaultCAS config: %w\", err)\n\t}\n\n\tif vc.PKIRoleRSA == \"\" {\n\t\tvc.PKIRoleRSA = vc.PKIRoleDefault\n\t}\n\tif vc.PKIRoleEC == \"\" {\n\t\tvc.PKIRoleEC = vc.PKIRoleDefault\n\t}\n\tif vc.PKIRoleEd25519 == \"\" {\n\t\tvc.PKIRoleEd25519 = vc.PKIRoleDefault\n\t}\n\n\treturn &vc, nil\n}\n\nfunc parseCertificates(pemCert string) []*x509.Certificate {\n\tvar certs []*x509.Certificate\n\trest := []byte(pemCert)\n\tvar block *pem.Block\n\tfor {\n\t\tblock, rest = pem.Decode(rest)\n\t\tif block == nil {\n\t\t\tbreak\n\t\t}\n\t\tcert, err := x509.ParseCertificate(block.Bytes)\n\t\tif err != nil {\n\t\t\tbreak\n\t\t}\n\t\tcerts = append(certs, cert)\n\t}\n\treturn certs\n}\n\nfunc getCertificateBundle(chain string) (*certBundle, error) {\n\tvar root *x509.Certificate\n\tvar leaf *x509.Certificate\n\tvar intermediates []*x509.Certificate\n\tfor _, cert := range parseCertificates(chain) {\n\t\tswitch {\n\t\tcase isRoot(cert):\n\t\t\troot = cert\n\t\tcase cert.BasicConstraintsValid && cert.IsCA:\n\t\t\tintermediates = append(intermediates, cert)\n\t\tdefault:\n\t\t\tleaf = cert\n\t\t}\n\t}\n\n\tcertificate := &certBundle{\n\t\troot:          root,\n\t\tleaf:          leaf,\n\t\tintermediates: intermediates,\n\t}\n\n\treturn certificate, nil\n}\n\n// isRoot returns true if the given certificate is a root certificate.\nfunc isRoot(cert *x509.Certificate) bool {\n\tif cert.BasicConstraintsValid && cert.IsCA {\n\t\treturn cert.CheckSignatureFrom(cert) == nil\n\t}\n\treturn false\n}\n\n// formatSerialNumber formats a serial number to a dash-separated hexadecimal\n// string.\nfunc formatSerialNumber(sn *big.Int) string {\n\tvar ret bytes.Buffer\n\tfor _, b := range sn.Bytes() {\n\t\tif ret.Len() > 0 {\n\t\t\tret.WriteString(\"-\")\n\t\t}\n\t\tret.WriteString(hex.EncodeToString([]byte{b}))\n\t}\n\treturn ret.String()\n}\n"
  },
  {
    "path": "cas/vaultcas/vaultcas_test.go",
    "content": "package vaultcas\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/x509\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"reflect\"\n\t\"testing\"\n\t\"time\"\n\n\tvault \"github.com/hashicorp/vault/api\"\n\t\"github.com/smallstep/certificates/cas/apiv1\"\n\t\"go.step.sm/crypto/pemutil\"\n)\n\nvar (\n\ttestCertificateSigned = `-----BEGIN CERTIFICATE-----\nMIIB/DCCAaKgAwIBAgIQHHFuGMz0cClfde5kqP5prTAKBggqhkjOPQQDAjAqMSgw\nJgYDVQQDEx9Hb29nbGUgQ0FTIFRlc3QgSW50ZXJtZWRpYXRlIENBMB4XDTIwMDkx\nNTAwMDQ0M1oXDTMwMDkxMzAwMDQ0MFowHTEbMBkGA1UEAxMSdGVzdC5zbWFsbHN0\nZXAuY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEMqNCiXMvbn74LsHzRv+8\n17m9vEzH6RHrg3m82e0uEc36+fZWV/zJ9SKuONmnl5VP79LsjL5SVH0RDj73U2XO\nDKOBtjCBszAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsG\nAQUFBwMCMB0GA1UdDgQWBBRTA2cTs7PCNjnps/+T0dS8diqv0DAfBgNVHSMEGDAW\ngBRIOVqyLDSlErJLuWWEvRm5UU1r1TBCBgwrBgEEAYKkZMYoQAIEMjAwEwhjbG91\nZGNhcxMkZDhkMThhNjgtNTI5Ni00YWYzLWFlNGItMmY4NzdkYTNmYmQ5MAoGCCqG\nSM49BAMCA0gAMEUCIGxl+pqJ50WYWUqK2l4V1FHoXSi0Nht5kwTxFxnWZu1xAiEA\nzemu3bhWLFaGg3s8i+HTEhw4RqkHP74vF7AVYp88bAw=\n-----END CERTIFICATE-----`\n\ttestCertificateCsrEc = `-----BEGIN CERTIFICATE REQUEST-----\nMIHoMIGPAgEAMA0xCzAJBgNVBAMTAkVDMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcD\nQgAEUVVVZGD6eUrB20T/qrjKZoYzseQ18AIm9jtUNpQn5hIClpdk2zKy5bja3iUa\nnmqRKCIz/B/MU55zuNDeckqqX6AgMB4GCSqGSIb3DQEJDjERMA8wDQYDVR0RBAYw\nBIICRUMwCgYIKoZIzj0EAwIDSAAwRQIhAJxpWyH7cctbzcnK1JBWDAmc/G61bq9y\notHrQDfYvS8bAiBVGQz2cfO2SqhvkkQbOqWUFjk1wHzISvlTjyc3IJ7FLw==\n-----END CERTIFICATE REQUEST-----`\n\ttestCertificateCsrRsa = `-----BEGIN CERTIFICATE REQUEST-----\nMIICdDCCAVwCAQAwDjEMMAoGA1UEAxMDUlNBMIIBIjANBgkqhkiG9w0BAQEFAAOC\nAQ8AMIIBCgKCAQEAxe5XLSZrTCzzH0FJCXvZwghAY5XztzjseSRcm0jL8Q7nvNWi\nVpu1n7EmfVU9b8sbvtVYqMQV+hMdj2C/NIw4Yal4Wg+BgunYOrRqfY7oDm4csG0R\ng5v0h2yQw14kqVrftNyojX0Nv/CPboCGl64PA9zsEXQTB3Y1AUWrUGPiBWNACYIH\nmjv70Ay9JKBBAqov38I7nka/RgYAl5DCHzU2vvODriBYFWagnzycA4Ni5EKTz93W\nSPdDEhkWi3ugUqal3SvgHl8re+8d7ghLn85Y3TFuyU2nSMDPHaymsiNFw1mRwOw3\nlAseidHJkPQs7q6FiYXaeqetf1j/gw0n23ZogwIDAQABoCEwHwYJKoZIhvcNAQkO\nMRIwEDAOBgNVHREEBzAFggNSU0EwDQYJKoZIhvcNAQELBQADggEBALnO5vcDkgGO\nGQoSINa2NmNFxAtYQGYHok5KXYX+S+etmOmDrmrhsl/pSjN3GPCPlThFlbLStB70\noJw67nEjGf0hPEBVlm+qFUsYQ1KGRZFAWDSMQ//pU225XFDCmlzHfV7gZjSkP9GN\nGc5VECOzx6hAFR+IEL/l/1GG5HHkPPrr/8OvuIfm2V5ofYmhsXMVVYH52qPofMAV\nB8UdNnZK3nyLdUqVd+PYUUJmN4bJ8YfxofKKgbLkhvkKp4OZ9vkwUi2+61NdHTf2\nwIauOyxEoTlJpU6oA/sxu/2Ht2DP+8y6mognLBuKklE/VH3/2iqQWyg1NV5hyg3b\nloVSdLsIh5Y=\n-----END CERTIFICATE REQUEST-----`\n\ttestCertificateCsrEd25519 = `-----BEGIN CERTIFICATE REQUEST-----\nMIGuMGICAQAwDjEMMAoGA1UEAxMDT0tQMCowBQYDK2VwAyEAopc6daK4zYR6BDAM\npV/v53oR/ewbtrkHZQkN/amFMLagITAfBgkqhkiG9w0BCQ4xEjAQMA4GA1UdEQQH\nMAWCA09LUDAFBgMrZXADQQDJi47MAgl/WKAz+V/kDu1k/zbKk1nrHHAUonbofHUW\nM6ihSD43+awq3BPeyPbToeH5orSH9l3MuTfbxPb5BVEH\n-----END CERTIFICATE REQUEST-----`\n\ttestRootCertificate = `-----BEGIN CERTIFICATE-----\nMIIBeDCCAR+gAwIBAgIQcXWWjtSZ/PAyH8D1Ou4L9jAKBggqhkjOPQQDAjAbMRkw\nFwYDVQQDExBDbG91ZENBUyBSb290IENBMB4XDTIwMTAyNzIyNTM1NFoXDTMwMTAy\nNzIyNTM1NFowGzEZMBcGA1UEAxMQQ2xvdWRDQVMgUm9vdCBDQTBZMBMGByqGSM49\nAgEGCCqGSM49AwEHA0IABIySHA4b78Yu4LuGhZIlv/PhNwXz4ZoV1OUZQ0LrK3vj\nB13O12DLZC5uj1z3kxdQzXUttSbtRv49clMpBiTpsZKjRTBDMA4GA1UdDwEB/wQE\nAwIBBjASBgNVHRMBAf8ECDAGAQH/AgEBMB0GA1UdDgQWBBSZ+t9RMHbFTl5BatM3\n5bJlHPOu3DAKBggqhkjOPQQDAgNHADBEAiASah6gg0tVM3WI0meCQ4SEKk7Mjhbv\n+SmhuZHWV1QlXQIgRXNyWcpVUrAoG6Uy1KQg07LDpF5dFeK9InrDxSJAkVo=\n-----END CERTIFICATE-----`\n\ttestRootFingerprint = `62e816cbac5c501b7705e18415503852798dfbcd67062f06bcb4af67c290e3c8`\n)\n\nfunc mustParseCertificate(t *testing.T, pemCert string) *x509.Certificate {\n\tt.Helper()\n\tcrt := parseCertificates(pemCert)[0]\n\treturn crt\n}\n\nfunc mustParseCertificateRequest(t *testing.T, pemData string) *x509.CertificateRequest {\n\tt.Helper()\n\tcsr, err := pemutil.ParseCertificateRequest([]byte(pemData))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn csr\n}\n\nfunc testCAHelper(t *testing.T) (*url.URL, *vault.Client) {\n\tt.Helper()\n\n\twriteJSON := func(w http.ResponseWriter, v interface{}) {\n\t\t_ = json.NewEncoder(w).Encode(v)\n\t}\n\n\tsrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tswitch r.RequestURI {\n\t\tcase \"/v1/auth/approle/login\":\n\t\t\tw.WriteHeader(http.StatusOK)\n\t\t\tfmt.Fprintf(w, `{\n\t\t\t\t  \"auth\": {\n\t\t\t\t\t\"client_token\": \"98a4c7ab-b1fe-361b-ba0b-e307aacfd587\"\n\t\t\t\t  }\n\t\t\t\t}`)\n\t\tcase \"/v1/pki/sign/ec\":\n\t\t\tw.WriteHeader(http.StatusOK)\n\t\t\tcert := map[string]interface{}{\"data\": map[string]interface{}{\"certificate\": testCertificateSigned + \"\\n\" + testRootCertificate}}\n\t\t\twriteJSON(w, cert)\n\t\t\treturn\n\t\tcase \"/v1/pki/sign/rsa\":\n\t\t\tw.WriteHeader(http.StatusOK)\n\t\t\tcert := map[string]interface{}{\"data\": map[string]interface{}{\"certificate\": testCertificateSigned + \"\\n\" + testRootCertificate}}\n\t\t\twriteJSON(w, cert)\n\t\t\treturn\n\t\tcase \"/v1/pki/sign/ed25519\":\n\t\t\tw.WriteHeader(http.StatusOK)\n\t\t\tcert := map[string]interface{}{\"data\": map[string]interface{}{\"certificate\": testCertificateSigned + \"\\n\" + testRootCertificate}}\n\t\t\twriteJSON(w, cert)\n\t\t\treturn\n\t\tcase \"/v1/pki/cert/ca_chain\":\n\t\t\tw.WriteHeader(http.StatusOK)\n\t\t\tcert := map[string]interface{}{\"data\": map[string]interface{}{\"certificate\": testCertificateSigned + \"\\n\" + testRootCertificate}}\n\t\t\twriteJSON(w, cert)\n\t\t\treturn\n\t\tcase \"/v1/pki/revoke\":\n\t\t\tbuf := new(bytes.Buffer)\n\t\t\tbuf.ReadFrom(r.Body)\n\t\t\tm := make(map[string]string)\n\t\t\tjson.Unmarshal(buf.Bytes(), &m)\n\t\t\tswitch m[\"serial_number\"] {\n\t\t\tcase \"1c-71-6e-18-cc-f4-70-29-5f-75-ee-64-a8-fe-69-ad\":\n\t\t\t\tw.WriteHeader(http.StatusOK)\n\t\t\t\treturn\n\t\t\tcase \"01-e2-40\":\n\t\t\t\tw.WriteHeader(http.StatusOK)\n\t\t\t\treturn\n\t\t\t// both\n\t\t\tcase \"01-34-3e\":\n\t\t\t\tw.WriteHeader(http.StatusOK)\n\t\t\t\treturn\n\t\t\tdefault:\n\t\t\t\tw.WriteHeader(http.StatusNotFound)\n\t\t\t}\n\t\tdefault:\n\t\t\tw.WriteHeader(http.StatusNotFound)\n\t\t\tfmt.Fprintf(w, `{\"error\":\"not found\"}`)\n\t\t}\n\t}))\n\tt.Cleanup(func() {\n\t\tsrv.Close()\n\t})\n\tu, err := url.Parse(srv.URL)\n\tif err != nil {\n\t\tsrv.Close()\n\t\tt.Fatal(err)\n\t}\n\n\tconfig := vault.DefaultConfig()\n\tconfig.Address = srv.URL\n\n\tclient, err := vault.NewClient(config)\n\tif err != nil {\n\t\tsrv.Close()\n\t\tt.Fatal(err)\n\t}\n\n\treturn u, client\n}\n\nfunc TestNew_register(t *testing.T) {\n\tcaURL, _ := testCAHelper(t)\n\n\tfn, ok := apiv1.LoadCertificateAuthorityServiceNewFunc(apiv1.VaultCAS)\n\tif !ok {\n\t\tt.Errorf(\"apiv1.Register() ok = %v, want true\", ok)\n\t\treturn\n\t}\n\t_, err := fn(context.Background(), apiv1.Options{\n\t\tCertificateAuthority:            caURL.String(),\n\t\tCertificateAuthorityFingerprint: testRootFingerprint,\n\t\tConfig: json.RawMessage(`{\n\t\t\t\"AuthType\": \"approle\",\n\t\t\t\"AuthOptions\": {\"RoleID\":\"roleID\",\"SecretID\":\"secretID\",\"IsWrappingToken\":false}\n\t\t}`),\n\t})\n\n\tif err != nil {\n\t\tt.Errorf(\"New() error = %v\", err)\n\t\treturn\n\t}\n}\n\nfunc TestVaultCAS_Type(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\twant apiv1.Type\n\t}{\n\t\t{\"ok\", apiv1.VaultCAS},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tc := &VaultCAS{}\n\t\t\tif got := c.Type(); got != tt.want {\n\t\t\t\tt.Errorf(\"VaultCAS.Type() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestVaultCAS_CreateCertificate(t *testing.T) {\n\t_, client := testCAHelper(t)\n\n\toptions := VaultOptions{\n\t\tPKIMountPath:   \"pki\",\n\t\tPKIRoleDefault: \"role\",\n\t\tPKIRoleRSA:     \"rsa\",\n\t\tPKIRoleEC:      \"ec\",\n\t\tPKIRoleEd25519: \"ed25519\",\n\t}\n\n\ttype fields struct {\n\t\tclient  *vault.Client\n\t\toptions VaultOptions\n\t}\n\n\ttype args struct {\n\t\treq *apiv1.CreateCertificateRequest\n\t}\n\n\ttests := []struct {\n\t\tname    string\n\t\tfields  fields\n\t\targs    args\n\t\twant    *apiv1.CreateCertificateResponse\n\t\twantErr bool\n\t}{\n\t\t{\"ok ec\", fields{client, options}, args{&apiv1.CreateCertificateRequest{\n\t\t\tCSR:      mustParseCertificateRequest(t, testCertificateCsrEc),\n\t\t\tLifetime: time.Hour,\n\t\t}}, &apiv1.CreateCertificateResponse{\n\t\t\tCertificate:      mustParseCertificate(t, testCertificateSigned),\n\t\t\tCertificateChain: nil,\n\t\t}, false},\n\t\t{\"ok rsa\", fields{client, options}, args{&apiv1.CreateCertificateRequest{\n\t\t\tCSR:      mustParseCertificateRequest(t, testCertificateCsrRsa),\n\t\t\tLifetime: time.Hour,\n\t\t}}, &apiv1.CreateCertificateResponse{\n\t\t\tCertificate:      mustParseCertificate(t, testCertificateSigned),\n\t\t\tCertificateChain: nil,\n\t\t}, false},\n\t\t{\"ok ed25519\", fields{client, options}, args{&apiv1.CreateCertificateRequest{\n\t\t\tCSR:      mustParseCertificateRequest(t, testCertificateCsrEd25519),\n\t\t\tLifetime: time.Hour,\n\t\t}}, &apiv1.CreateCertificateResponse{\n\t\t\tCertificate:      mustParseCertificate(t, testCertificateSigned),\n\t\t\tCertificateChain: nil,\n\t\t}, false},\n\t\t{\"fail CSR\", fields{client, options}, args{&apiv1.CreateCertificateRequest{\n\t\t\tCSR:      nil,\n\t\t\tLifetime: time.Hour,\n\t\t}}, nil, true},\n\t\t{\"fail lifetime\", fields{client, options}, args{&apiv1.CreateCertificateRequest{\n\t\t\tCSR:      mustParseCertificateRequest(t, testCertificateCsrEc),\n\t\t\tLifetime: 0,\n\t\t}}, nil, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tc := &VaultCAS{\n\t\t\t\tclient: tt.fields.client,\n\t\t\t\tconfig: tt.fields.options,\n\t\t\t}\n\t\t\tgot, err := c.CreateCertificate(tt.args.req)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"VaultCAS.CreateCertificate() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"VaultCAS.CreateCertificate() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestVaultCAS_GetCertificateAuthority(t *testing.T) {\n\tcaURL, client := testCAHelper(t)\n\n\ttype fields struct {\n\t\tclient      *vault.Client\n\t\toptions     VaultOptions\n\t\tfingerprint string\n\t}\n\n\ttype args struct {\n\t\treq *apiv1.GetCertificateAuthorityRequest\n\t}\n\n\toptions := VaultOptions{\n\t\tPKIMountPath: \"pki\",\n\t}\n\n\trootCert := parseCertificates(testRootCertificate)[0]\n\n\ttests := []struct {\n\t\tname    string\n\t\tfields  fields\n\t\targs    args\n\t\twant    *apiv1.GetCertificateAuthorityResponse\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", fields{client, options, testRootFingerprint}, args{&apiv1.GetCertificateAuthorityRequest{\n\t\t\tName: caURL.String(),\n\t\t}}, &apiv1.GetCertificateAuthorityResponse{\n\t\t\tRootCertificate: rootCert,\n\t\t}, false},\n\t\t{\"fail fingerprint\", fields{client, options, \"fail\"}, args{&apiv1.GetCertificateAuthorityRequest{\n\t\t\tName: caURL.String(),\n\t\t}}, nil, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ts := &VaultCAS{\n\t\t\t\tclient:      tt.fields.client,\n\t\t\t\tfingerprint: tt.fields.fingerprint,\n\t\t\t\tconfig:      tt.fields.options,\n\t\t\t}\n\t\t\tgot, err := s.GetCertificateAuthority(tt.args.req)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"VaultCAS.GetCertificateAuthority() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"VaultCAS.GetCertificateAuthority() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestVaultCAS_RevokeCertificate(t *testing.T) {\n\t_, client := testCAHelper(t)\n\n\toptions := VaultOptions{\n\t\tPKIMountPath:   \"pki\",\n\t\tPKIRoleDefault: \"role\",\n\t\tPKIRoleRSA:     \"rsa\",\n\t\tPKIRoleEC:      \"ec\",\n\t\tPKIRoleEd25519: \"ed25519\",\n\t}\n\n\ttype fields struct {\n\t\tclient  *vault.Client\n\t\toptions VaultOptions\n\t}\n\n\ttype args struct {\n\t\treq *apiv1.RevokeCertificateRequest\n\t}\n\n\ttestCrt := parseCertificates(testCertificateSigned)[0]\n\n\ttests := []struct {\n\t\tname    string\n\t\tfields  fields\n\t\targs    args\n\t\twant    *apiv1.RevokeCertificateResponse\n\t\twantErr bool\n\t}{\n\t\t{\"ok serial number\", fields{client, options}, args{&apiv1.RevokeCertificateRequest{\n\t\t\tSerialNumber: \"123456\",\n\t\t\tCertificate:  nil,\n\t\t}}, &apiv1.RevokeCertificateResponse{}, false},\n\t\t{\"ok certificate\", fields{client, options}, args{&apiv1.RevokeCertificateRequest{\n\t\t\tSerialNumber: \"\",\n\t\t\tCertificate:  testCrt,\n\t\t}}, &apiv1.RevokeCertificateResponse{\n\t\t\tCertificate: testCrt,\n\t\t}, false},\n\t\t{\"ok both\", fields{client, options}, args{&apiv1.RevokeCertificateRequest{\n\t\t\tSerialNumber: \"78910\",\n\t\t\tCertificate:  testCrt,\n\t\t}}, &apiv1.RevokeCertificateResponse{\n\t\t\tCertificate: testCrt,\n\t\t}, false},\n\t\t{\"fail serial string\", fields{client, options}, args{&apiv1.RevokeCertificateRequest{\n\t\t\tSerialNumber: \"fail\",\n\t\t\tCertificate:  nil,\n\t\t}}, nil, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ts := &VaultCAS{\n\t\t\t\tclient: tt.fields.client,\n\t\t\t\tconfig: tt.fields.options,\n\t\t\t}\n\t\t\tgot, err := s.RevokeCertificate(tt.args.req)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"VaultCAS.RevokeCertificate() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"VaultCAS.RevokeCertificate() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestVaultCAS_RenewCertificate(t *testing.T) {\n\t_, client := testCAHelper(t)\n\n\toptions := VaultOptions{\n\t\tPKIMountPath:   \"pki\",\n\t\tPKIRoleDefault: \"role\",\n\t\tPKIRoleRSA:     \"rsa\",\n\t\tPKIRoleEC:      \"ec\",\n\t\tPKIRoleEd25519: \"ed25519\",\n\t}\n\n\ttype fields struct {\n\t\tclient  *vault.Client\n\t\toptions VaultOptions\n\t}\n\n\ttype args struct {\n\t\treq *apiv1.RenewCertificateRequest\n\t}\n\n\ttests := []struct {\n\t\tname    string\n\t\tfields  fields\n\t\targs    args\n\t\twant    *apiv1.RenewCertificateResponse\n\t\twantErr bool\n\t}{\n\t\t{\"not implemented\", fields{client, options}, args{&apiv1.RenewCertificateRequest{\n\t\t\tCSR:      mustParseCertificateRequest(t, testCertificateCsrEc),\n\t\t\tLifetime: time.Hour,\n\t\t}}, nil, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ts := &VaultCAS{\n\t\t\t\tclient: tt.fields.client,\n\t\t\t\tconfig: tt.fields.options,\n\t\t\t}\n\t\t\tgot, err := s.RenewCertificate(tt.args.req)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"VaultCAS.RenewCertificate() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"VaultCAS.RenewCertificate() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestVaultCAS_loadOptions(t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\traw     string\n\t\twant    *VaultOptions\n\t\twantErr bool\n\t}{\n\t\t{\n\t\t\t\"ok mandatory PKIRole PKIRoleEd25519\",\n\t\t\t`{\"PKIRoleDefault\": \"role\", \"PKIRoleEd25519\": \"ed25519\"}`,\n\t\t\t&VaultOptions{\n\t\t\t\tPKIMountPath:   \"pki\",\n\t\t\t\tPKIRoleDefault: \"role\",\n\t\t\t\tPKIRoleRSA:     \"role\",\n\t\t\t\tPKIRoleEC:      \"role\",\n\t\t\t\tPKIRoleEd25519: \"ed25519\",\n\t\t\t},\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"ok mandatory PKIRole PKIRoleEC\",\n\t\t\t`{\"PKIRoleDefault\": \"role\", \"PKIRoleEC\": \"ec\"}`,\n\t\t\t&VaultOptions{\n\t\t\t\tPKIMountPath:   \"pki\",\n\t\t\t\tPKIRoleDefault: \"role\",\n\t\t\t\tPKIRoleRSA:     \"role\",\n\t\t\t\tPKIRoleEC:      \"ec\",\n\t\t\t\tPKIRoleEd25519: \"role\",\n\t\t\t},\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"ok mandatory PKIRole PKIRoleRSA\",\n\t\t\t`{\"PKIRoleDefault\": \"role\", \"PKIRoleRSA\": \"rsa\"}`,\n\t\t\t&VaultOptions{\n\t\t\t\tPKIMountPath:   \"pki\",\n\t\t\t\tPKIRoleDefault: \"role\",\n\t\t\t\tPKIRoleRSA:     \"rsa\",\n\t\t\t\tPKIRoleEC:      \"role\",\n\t\t\t\tPKIRoleEd25519: \"role\",\n\t\t\t},\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"ok mandatory PKIRoleRSA PKIRoleEC PKIRoleEd25519\",\n\t\t\t`{\"PKIRoleRSA\": \"rsa\", \"PKIRoleEC\": \"ec\", \"PKIRoleEd25519\": \"ed25519\"}`,\n\t\t\t&VaultOptions{\n\t\t\t\tPKIMountPath:   \"pki\",\n\t\t\t\tPKIRoleDefault: \"default\",\n\t\t\t\tPKIRoleRSA:     \"rsa\",\n\t\t\t\tPKIRoleEC:      \"ec\",\n\t\t\t\tPKIRoleEd25519: \"ed25519\",\n\t\t\t},\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"ok mandatory PKIRoleRSA PKIRoleEC PKIRoleEd25519 with useless PKIRoleDefault\",\n\t\t\t`{\"PKIRoleDefault\": \"role\", \"PKIRoleRSA\": \"rsa\", \"PKIRoleEC\": \"ec\", \"PKIRoleEd25519\": \"ed25519\"}`,\n\t\t\t&VaultOptions{\n\t\t\t\tPKIMountPath:   \"pki\",\n\t\t\t\tPKIRoleDefault: \"role\",\n\t\t\t\tPKIRoleRSA:     \"rsa\",\n\t\t\t\tPKIRoleEC:      \"ec\",\n\t\t\t\tPKIRoleEd25519: \"ed25519\",\n\t\t\t},\n\t\t\tfalse,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := loadOptions(json.RawMessage(tt.raw))\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"VaultCAS.loadOptions() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"VaultCAS.loadOptions() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "cmd/step-ca/main.go",
    "content": "package main\n\nimport (\n\t\"flag\"\n\t\"fmt\"\n\t\"html\"\n\t\"log\"\n\t\"net/http\"\n\t\"os\"\n\t\"reflect\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"time\"\n\n\t// Server profiler\n\t//nolint:gosec // profile server, if enabled runs on a different port\n\t_ \"net/http/pprof\"\n\n\t\"github.com/urfave/cli\"\n\n\t\"github.com/smallstep/certificates/authority\"\n\t\"github.com/smallstep/certificates/commands\"\n\t\"github.com/smallstep/cli-utils/command\"\n\t\"github.com/smallstep/cli-utils/command/version\"\n\t\"github.com/smallstep/cli-utils/step\"\n\t\"github.com/smallstep/cli-utils/ui\"\n\t\"github.com/smallstep/cli-utils/usage\"\n\t\"go.step.sm/crypto/pemutil\"\n\n\t// Enabled kms interfaces.\n\t_ \"go.step.sm/crypto/kms/awskms\"\n\t_ \"go.step.sm/crypto/kms/azurekms\"\n\t_ \"go.step.sm/crypto/kms/cloudkms\"\n\t_ \"go.step.sm/crypto/kms/pkcs11\"\n\t_ \"go.step.sm/crypto/kms/softkms\"\n\t_ \"go.step.sm/crypto/kms/sshagentkms\"\n\t_ \"go.step.sm/crypto/kms/tpmkms\"\n\t_ \"go.step.sm/crypto/kms/yubikey\"\n\n\t// Enabled cas interfaces.\n\t_ \"github.com/smallstep/certificates/cas/cloudcas\"\n\t_ \"github.com/smallstep/certificates/cas/softcas\"\n\t_ \"github.com/smallstep/certificates/cas/stepcas\"\n\t_ \"github.com/smallstep/certificates/cas/vaultcas\"\n)\n\n// commit and buildTime are filled in during build by the Makefile\nvar (\n\tBuildTime = \"N/A\"\n\tVersion   = \"N/A\"\n)\n\nfunc init() {\n\tstep.Set(\"Smallstep CA\", Version, BuildTime)\n\tauthority.GlobalVersion.Version = Version\n\t// Add support for asking passwords\n\tpemutil.PromptPassword = func(msg string) ([]byte, error) {\n\t\treturn ui.PromptPassword(msg)\n\t}\n}\n\nfunc exit(code int) {\n\tui.Reset()\n\tos.Exit(code)\n}\n\n// appHelpTemplate contains the modified template for the main app\nvar appHelpTemplate = `## NAME\n**{{.HelpName}}** -- {{.Usage}}\n## USAGE\n{{if .UsageText}}{{.UsageText}}{{else}}**{{.HelpName}}**{{if .Commands}} <command>{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}_[arguments]_{{end}}{{end}}{{if .Description}}\n## DESCRIPTION\n{{.Description}}{{end}}{{if .VisibleCommands}}\n## COMMANDS\n{{range .VisibleCategories}}{{if .Name}}{{.Name}}:{{end}}\n|||\n|---|---|{{range .VisibleCommands}}\n| **{{join .Names \", \"}}** | {{.Usage}} |{{end}}\n{{end}}{{if .VisibleFlags}}{{end}}\n## OPTIONS\n\n{{range $index, $option := .VisibleFlags}}{{if $index}}\n{{end}}{{$option}}\n{{end}}{{end}}{{if .Copyright}}{{if len .Authors}}\n## AUTHOR{{with $length := len .Authors}}{{if ne 1 $length}}S{{end}}{{end}}:\n{{range $index, $author := .Authors}}{{if $index}}\n{{end}}{{$author}}{{end}}{{end}}{{if .Version}}{{if not .HideVersion}}\n## ONLINE\nThis documentation is available online at https://smallstep.com/docs/certificates\n## VERSION\n{{.Version}}{{end}}{{end}}\n## COPYRIGHT\n{{.Copyright}}\n## FEEDBACK ` +\n\thtml.UnescapeString(\"&#\"+strconv.Itoa(128525)+\";\") + \" \" +\n\thtml.UnescapeString(\"&#\"+strconv.Itoa(127867)+\";\") +\n\t`\nThe **step-ca** utility is not instrumented for usage statistics. It does not phone home.\nBut your feedback is extremely valuable. Any information you can provide regarding how you’re using **step-ca** helps.\nPlease send us a sentence or two, good or bad: **feedback@smallstep.com** or https://github.com/smallstep/certificates/discussions.\n{{end}}\n`\n\nfunc main() {\n\t// initialize step environment.\n\tif err := step.Init(); err != nil {\n\t\tfmt.Fprintln(os.Stderr, err.Error())\n\t\tos.Exit(1)\n\t}\n\n\t// Initialize windows terminal\n\tui.Init()\n\n\t// Override global framework components\n\tcli.VersionPrinter = func(c *cli.Context) {\n\t\tversion.Command(c)\n\t}\n\tcli.AppHelpTemplate = appHelpTemplate\n\tcli.SubcommandHelpTemplate = usage.SubcommandHelpTemplate\n\tcli.CommandHelpTemplate = usage.CommandHelpTemplate\n\tcli.HelpPrinter = usage.HelpPrinter\n\tcli.FlagNamePrefixer = usage.FlagNamePrefixer\n\tcli.FlagStringer = stringifyFlag\n\n\t// Configure cli app\n\tapp := cli.NewApp()\n\tapp.Name = \"step-ca\"\n\tapp.HelpName = \"step-ca\"\n\tapp.Version = step.Version()\n\tapp.Usage = \"an online certificate authority for secure automated certificate management\"\n\tapp.UsageText = `**step-ca** [config] [**--context**=<name>] [**--password-file**=<file>]\n[**--ssh-host-password-file**=<file>] [**--ssh-user-password-file**=<file>]\n[**--issuer-password-file**=<file>] [**--resolver**=<addr>] [**--help**] [**--version**]`\n\tapp.Description = `**step-ca** runs the Step Online Certificate Authority\n(Step CA) using the given configuration.\nSee the README.md for more detailed configuration documentation.\n## POSITIONAL ARGUMENTS\n<config>\n: File that configures the operation of the Step CA; this file is generated\nwhen you initialize the Step CA using 'step ca init'\n## EXIT CODES\nThis command will run indefinitely on success and return \\>0 if any error occurs.\n## EXAMPLES\nThese examples assume that you have already initialized your PKI by running\n'step ca init'. If you have not completed this step please see the 'Getting Started'\nsection of the README.\n\nRun the Step CA and prompt for password:\n'''\n$ step-ca $STEPPATH/config/ca.json\n'''\nRun the Step CA and read the password from a file - this is useful for\nautomating deployment:\n'''\n$ step-ca $STEPPATH/config/ca.json --password-file ./password.txt\n'''\nRun the Step CA for the context selected with step and a custom password file:\n'''\n$ step context select ssh\n$ step-ca --password-file ./password.txt\n'''\nRun the Step CA for the context named _mybiz_ and prompt for password:\n'''\n$ step-ca --context=mybiz\n'''\nRun the Step CA for the context named _mybiz_ and an alternate ca.json file:\n'''\n$ step-ca --context=mybiz other-ca.json\n'''\nRun the Step CA for the context named _mybiz_ and read the password from a file - this is useful for\nautomating deployment:\n'''\n$ step-ca --context=mybiz --password-file ./password.txt\n'''\n`\n\tapp.Flags = append(app.Flags, commands.AppCommand.Flags...)\n\tapp.Flags = append(app.Flags, cli.HelpFlag)\n\tapp.Copyright = fmt.Sprintf(\"(c) 2018-%d Smallstep Labs, Inc.\", time.Now().Year())\n\n\t// All non-successful output should be written to stderr\n\tapp.Writer = os.Stdout\n\tapp.ErrWriter = os.Stderr\n\tapp.Commands = command.Retrieve()\n\n\t// Start the golang debug logger if environment variable is set.\n\t// See https://golang.org/pkg/net/http/pprof/\n\tdebugProfAddr := os.Getenv(\"STEP_PROF_ADDR\")\n\tif debugProfAddr != \"\" {\n\t\tgo func() {\n\t\t\tsrv := http.Server{\n\t\t\t\tAddr:              debugProfAddr,\n\t\t\t\tReadHeaderTimeout: 15 * time.Second,\n\t\t\t}\n\t\t\tlog.Println(srv.ListenAndServe())\n\t\t}()\n\t}\n\n\tapp.Action = func(_ *cli.Context) error {\n\t\t// Hack to be able to run a the top action as a subcommand\n\t\tset := flag.NewFlagSet(app.Name, flag.ContinueOnError)\n\t\tset.Parse(os.Args)\n\t\tctx := cli.NewContext(app, set, nil)\n\t\treturn commands.AppCommand.Run(ctx)\n\t}\n\n\tif err := app.Run(os.Args); err != nil {\n\t\tif os.Getenv(\"STEPDEBUG\") == \"1\" {\n\t\t\tfmt.Fprintf(os.Stderr, \"%+v\\n\", err)\n\t\t} else {\n\t\t\tfmt.Fprintln(os.Stderr, err)\n\t\t}\n\t\texit(1)\n\t}\n\n\texit(0)\n}\n\nfunc flagValue(f cli.Flag) reflect.Value {\n\tfv := reflect.ValueOf(f)\n\tfor fv.Kind() == reflect.Ptr {\n\t\tfv = reflect.Indirect(fv)\n\t}\n\treturn fv\n}\n\nvar placeholderString = regexp.MustCompile(`<.*?>`)\n\nfunc stringifyFlag(f cli.Flag) string {\n\tfv := flagValue(f)\n\tusg := fv.FieldByName(\"Usage\").String()\n\tplaceholder := placeholderString.FindString(usg)\n\tif placeholder == \"\" {\n\t\tswitch f.(type) {\n\t\tcase cli.BoolFlag, cli.BoolTFlag:\n\t\tdefault:\n\t\t\tplaceholder = \"<value>\"\n\t\t}\n\t}\n\treturn cli.FlagNamePrefixer(fv.FieldByName(\"Name\").String(), placeholder) + \"\\t\" + usg\n}\n"
  },
  {
    "path": "commands/app.go",
    "content": "package commands\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/http\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"strings\"\n\t\"unicode\"\n\n\t\"github.com/pkg/errors\"\n\t\"github.com/urfave/cli\"\n\n\t\"github.com/smallstep/cli-utils/errs\"\n\t\"github.com/smallstep/cli-utils/step\"\n\n\t\"github.com/smallstep/certificates/acme\"\n\t\"github.com/smallstep/certificates/authority/config\"\n\t\"github.com/smallstep/certificates/authority/provisioner\"\n\t\"github.com/smallstep/certificates/ca\"\n\t\"github.com/smallstep/certificates/db\"\n\t\"github.com/smallstep/certificates/pki\"\n)\n\n// AppCommand is the action used as the top action.\nvar AppCommand = cli.Command{\n\tName:   \"start\",\n\tAction: appAction,\n\tUsageText: `**step-ca** <config> [**--password-file**=<file>]\n[**--ssh-host-password-file**=<file>] [**--ssh-user-password-file**=<file>]\n[**--issuer-password-file**=<file>] [**--pidfile**=<file>] [**--resolver**=<addr>]`,\n\tFlags: []cli.Flag{\n\t\tcli.StringFlag{\n\t\t\tName: \"password-file\",\n\t\t\tUsage: `path to the <file> containing the password to decrypt the\nintermediate private key.`,\n\t\t},\n\t\tcli.StringFlag{\n\t\t\tName: \"ssh-host-password-file\",\n\t\t\tUsage: `path to the <file> containing the password to decrypt the\nprivate key used to sign SSH host certificates. If the flag is not passed it\nwill default to --password-file.`,\n\t\t},\n\t\tcli.StringFlag{\n\t\t\tName: \"ssh-user-password-file\",\n\t\t\tUsage: `path to the <file> containing the password to decrypt the\nprivate key used to sign SSH user certificates. If the flag is not passed it\nwill default to --password-file.`,\n\t\t},\n\t\tcli.StringFlag{\n\t\t\tName: \"issuer-password-file\",\n\t\t\tUsage: `path to the <file> containing the password to decrypt the\ncertificate issuer private key used in the RA mode.`,\n\t\t},\n\t\tcli.StringFlag{\n\t\t\tName:  \"resolver\",\n\t\t\tUsage: \"address of a DNS resolver to be used instead of the default.\",\n\t\t},\n\t\tcli.StringFlag{\n\t\t\tName:   \"token\",\n\t\t\tUsage:  \"token used to enable the linked ca.\",\n\t\t\tEnvVar: \"STEP_CA_TOKEN\",\n\t\t},\n\t\tcli.BoolFlag{\n\t\t\tName:   \"quiet\",\n\t\t\tUsage:  \"disable startup information\",\n\t\t\tEnvVar: \"STEP_CA_QUIET\",\n\t\t},\n\t\tcli.StringFlag{\n\t\t\tName:   \"context\",\n\t\t\tUsage:  \"the <name> of the authority's context.\",\n\t\t\tEnvVar: \"STEP_CA_CONTEXT\",\n\t\t},\n\t\tcli.IntFlag{\n\t\t\tName: \"acme-http-port\",\n\t\t\tUsage: `the <port> used on http-01 challenges. It can be changed for testing purposes.\nRequires **--insecure** flag.`,\n\t\t},\n\t\tcli.IntFlag{\n\t\t\tName: \"acme-tls-port\",\n\t\t\tUsage: `the <port> used on tls-alpn-01 challenges. It can be changed for testing purposes.\nRequires **--insecure** flag.`,\n\t\t},\n\t\tcli.BoolFlag{\n\t\t\tName:  \"acme-strict-fqdn\",\n\t\t\tUsage: `enable strict DNS resolution using a fully qualified domain name.`,\n\t\t},\n\t\tcli.StringFlag{\n\t\t\tName:  \"pidfile\",\n\t\t\tUsage: \"the path to the <file> to write the process ID.\",\n\t\t},\n\t\tcli.BoolFlag{\n\t\t\tName:  \"insecure\",\n\t\t\tUsage: \"enable insecure flags.\",\n\t\t},\n\t},\n}\n\nvar pidfile string\n\n// AppAction is the action used when the top command runs.\nfunc appAction(ctx *cli.Context) error {\n\tpassFile := ctx.String(\"password-file\")\n\tsshHostPassFile := ctx.String(\"ssh-host-password-file\")\n\tsshUserPassFile := ctx.String(\"ssh-user-password-file\")\n\tissuerPassFile := ctx.String(\"issuer-password-file\")\n\tresolver := ctx.String(\"resolver\")\n\ttoken := ctx.String(\"token\")\n\tquiet := ctx.Bool(\"quiet\")\n\n\tif ctx.NArg() > 1 {\n\t\treturn errs.TooManyArguments(ctx)\n\t}\n\n\t// Allow custom ACME ports with insecure\n\tif acmePort := ctx.Int(\"acme-http-port\"); acmePort != 0 {\n\t\tif ctx.Bool(\"insecure\") {\n\t\t\tacme.InsecurePortHTTP01 = acmePort\n\t\t} else {\n\t\t\treturn fmt.Errorf(\"flag '--acme-http-port' requires the '--insecure' flag\")\n\t\t}\n\t}\n\tif acmePort := ctx.Int(\"acme-tls-port\"); acmePort != 0 {\n\t\tif ctx.Bool(\"insecure\") {\n\t\t\tacme.InsecurePortTLSALPN01 = acmePort\n\t\t} else {\n\t\t\treturn fmt.Errorf(\"flag '--acme-tls-port' requires the '--insecure' flag\")\n\t\t}\n\t}\n\n\t// Set the strict DNS resolution on ACME challenges. Defaults to false.\n\tacme.StrictFQDN = ctx.Bool(\"acme-strict-fqdn\")\n\n\t// Allow custom contexts.\n\tif caCtx := ctx.String(\"context\"); caCtx != \"\" {\n\t\tif _, ok := step.Contexts().Get(caCtx); ok {\n\t\t\tif err := step.Contexts().SetCurrent(caCtx); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t} else if token == \"\" {\n\t\t\treturn fmt.Errorf(\"context %q not found\", caCtx)\n\t\t} else if err := createContext(caCtx); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tvar configFile string\n\tif ctx.NArg() > 0 {\n\t\tconfigFile = ctx.Args().Get(0)\n\t} else {\n\t\tconfigFile = step.CaConfigFile()\n\t}\n\n\tcfg, err := config.LoadConfiguration(configFile)\n\tif err != nil && token == \"\" {\n\t\tvar pathErr *os.PathError\n\t\tif errors.As(err, &pathErr) {\n\t\t\tfmt.Println(\"step-ca can't find or open the configuration file for your CA.\")\n\t\t\tfmt.Println(\"You may need to create a CA first by running `step ca init`.\")\n\t\t\tfmt.Println(\"Documentation: https://u.step.sm/docs/ca\")\n\t\t\tos.Exit(1)\n\t\t}\n\t\tfatal(err)\n\t}\n\n\t// Initialize a basic configuration to be used with an automatically\n\t// configured linked RA. Default configuration includes:\n\t//  * badgerv2 on $(step path)/db\n\t//  * JSON logger\n\t//  * Default TLS options\n\tif cfg == nil {\n\t\tcfg = &config.Config{\n\t\t\tSkipValidation: true,\n\t\t\tLogger:         []byte(`{\"format\":\"json\"}`),\n\t\t\tDB: &db.Config{\n\t\t\t\tType:       \"badgerv2\",\n\t\t\t\tDataSource: filepath.Join(step.Path(), \"db\"),\n\t\t\t},\n\t\t\tAuthorityConfig: &config.AuthConfig{\n\t\t\t\tDeploymentType: pki.LinkedDeployment.String(),\n\t\t\t\tProvisioners:   provisioner.List{},\n\t\t\t\tTemplate:       &config.ASN1DN{},\n\t\t\t\tBackdate: &provisioner.Duration{\n\t\t\t\t\tDuration: config.DefaultBackdate,\n\t\t\t\t},\n\t\t\t},\n\t\t\tTLS: &config.DefaultTLSOptions,\n\t\t}\n\t}\n\n\tif cfg.AuthorityConfig != nil {\n\t\tif token == \"\" && strings.EqualFold(cfg.AuthorityConfig.DeploymentType, pki.LinkedDeployment.String()) {\n\t\t\treturn errors.New(`'step-ca' requires the '--token' flag for linked deploy type.\n\nTo get a linked authority token:\n  1. Contact us at ` + \"\\033[1mhttps://u.step.sm/cm\\033[0m\" + ` to create a new Certificate Manager account\n  2. Add a new authority and select \"Link a step-ca instance\"\n  3. Follow instructions in browser to start 'step-ca' using the '--token' flag\n`)\n\t\t}\n\t}\n\n\tvar password []byte\n\tif passFile != \"\" {\n\t\tif password, err = os.ReadFile(passFile); err != nil {\n\t\t\tfatal(errors.Wrapf(err, \"error reading %s\", passFile))\n\t\t}\n\t\tpassword = bytes.TrimRightFunc(password, unicode.IsSpace)\n\t}\n\n\tvar sshHostPassword []byte\n\tif sshHostPassFile != \"\" {\n\t\tif sshHostPassword, err = os.ReadFile(sshHostPassFile); err != nil {\n\t\t\tfatal(errors.Wrapf(err, \"error reading %s\", sshHostPassFile))\n\t\t}\n\t\tsshHostPassword = bytes.TrimRightFunc(sshHostPassword, unicode.IsSpace)\n\t}\n\n\tvar sshUserPassword []byte\n\tif sshUserPassFile != \"\" {\n\t\tif sshUserPassword, err = os.ReadFile(sshUserPassFile); err != nil {\n\t\t\tfatal(errors.Wrapf(err, \"error reading %s\", sshUserPassFile))\n\t\t}\n\t\tsshUserPassword = bytes.TrimRightFunc(sshUserPassword, unicode.IsSpace)\n\t}\n\n\tvar issuerPassword []byte\n\tif issuerPassFile != \"\" {\n\t\tif issuerPassword, err = os.ReadFile(issuerPassFile); err != nil {\n\t\t\tfatal(errors.Wrapf(err, \"error reading %s\", issuerPassFile))\n\t\t}\n\t\tissuerPassword = bytes.TrimRightFunc(issuerPassword, unicode.IsSpace)\n\t}\n\n\tif filename := ctx.String(\"pidfile\"); filename != \"\" {\n\t\tpid := []byte(strconv.Itoa(os.Getpid()) + \"\\n\")\n\t\t//nolint:gosec // 0644 (-rw-r--r--) are common permissions for a pid file\n\t\tif err := os.WriteFile(filename, pid, 0644); err != nil {\n\t\t\tfatal(errors.Wrap(err, \"error writing pidfile\"))\n\t\t}\n\t\tpidfile = filename\n\t}\n\n\t// replace resolver if requested\n\tif resolver != \"\" {\n\t\tnet.DefaultResolver.PreferGo = true\n\t\tnet.DefaultResolver.Dial = func(_ context.Context, network, _ string) (net.Conn, error) {\n\t\t\treturn net.Dial(network, resolver)\n\t\t}\n\t}\n\n\tsrv, err := ca.New(cfg,\n\t\tca.WithConfigFile(configFile),\n\t\tca.WithPassword(password),\n\t\tca.WithSSHHostPassword(sshHostPassword),\n\t\tca.WithSSHUserPassword(sshUserPassword),\n\t\tca.WithIssuerPassword(issuerPassword),\n\t\tca.WithLinkedCAToken(token),\n\t\tca.WithQuiet(quiet),\n\t)\n\tif err != nil {\n\t\tfatal(err)\n\t}\n\n\tgo ca.StopReloaderHandler(srv)\n\tif err = srv.Run(); err != nil && !errors.Is(err, http.ErrServerClosed) {\n\t\tfatal(err)\n\t}\n\n\tif pidfile != \"\" {\n\t\tos.Remove(pidfile)\n\t}\n\n\treturn nil\n}\n\n// createContext creates a new context using the given name for the context,\n// authority and profile.\nfunc createContext(name string) error {\n\tif err := step.Contexts().Add(&step.Context{\n\t\tName: name, Authority: name, Profile: name,\n\t}); err != nil {\n\t\treturn fmt.Errorf(\"error adding context: %w\", err)\n\t}\n\tif err := step.Contexts().SaveCurrent(name); err != nil {\n\t\treturn fmt.Errorf(\"error saving context: %w\", err)\n\t}\n\tif err := step.Contexts().SetCurrent(name); err != nil {\n\t\treturn fmt.Errorf(\"error setting context: %w\", err)\n\t}\n\tif err := os.MkdirAll(step.Path(), 0700); err != nil {\n\t\treturn fmt.Errorf(\"error creating directory: %w\", err)\n\t}\n\treturn nil\n}\n\n// fatal writes the passed error on the standard error and exits with the exit\n// code 1. If the environment variable STEPDEBUG is set to 1 it shows the\n// stack trace of the error.\nfunc fatal(err error) {\n\tif os.Getenv(\"STEPDEBUG\") == \"1\" {\n\t\tfmt.Fprintf(os.Stderr, \"%+v\\n\", err)\n\t} else {\n\t\tfmt.Fprintln(os.Stderr, err)\n\t}\n\tif pidfile != \"\" {\n\t\tos.Remove(pidfile)\n\t}\n\tos.Exit(2)\n}\n"
  },
  {
    "path": "commands/export.go",
    "content": "package commands\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"unicode\"\n\n\t\"github.com/pkg/errors\"\n\t\"github.com/urfave/cli\"\n\t\"google.golang.org/protobuf/encoding/protojson\"\n\n\t\"github.com/smallstep/cli-utils/command\"\n\t\"github.com/smallstep/cli-utils/errs\"\n\n\t\"github.com/smallstep/certificates/authority\"\n\t\"github.com/smallstep/certificates/authority/config\"\n)\n\nfunc init() {\n\tcommand.Register(cli.Command{\n\t\tName:      \"export\",\n\t\tUsage:     \"export the current configuration of step-ca\",\n\t\tUsageText: \"**step-ca export** <config>\",\n\t\tAction:    exportAction,\n\t\tDescription: `**step-ca export** exports the current configuration of step-ca.\n\nNote that neither the PKI password nor the certificate issuer password will be\nincluded in the export file.\n\n## POSITIONAL ARGUMENTS\n\n<config>\n:  The ca.json that contains the step-ca configuration.\n\n## EXAMPLES\n\nExport the current configuration:\n'''\n$ step-ca export $(step path)/config/ca.json\n'''`,\n\t\tFlags: []cli.Flag{\n\t\t\tcli.StringFlag{\n\t\t\t\tName: \"password-file\",\n\t\t\t\tUsage: `path to the <file> containing the password to decrypt the\nintermediate private key.`,\n\t\t\t},\n\t\t\tcli.StringFlag{\n\t\t\t\tName: \"issuer-password-file\",\n\t\t\t\tUsage: `path to the <file> containing the password to decrypt the\ncertificate issuer private key used in the RA mode.`,\n\t\t\t},\n\t\t},\n\t})\n}\n\nfunc exportAction(ctx *cli.Context) error {\n\tif err := errs.NumberOfArguments(ctx, 1); err != nil {\n\t\treturn err\n\t}\n\n\tconfigFile := ctx.Args().Get(0)\n\tpasswordFile := ctx.String(\"password-file\")\n\tissuerPasswordFile := ctx.String(\"issuer-password-file\")\n\n\tcfg, err := config.LoadConfiguration(configFile)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif err := cfg.Validate(); err != nil {\n\t\treturn err\n\t}\n\n\tif passwordFile != \"\" {\n\t\tb, err := os.ReadFile(passwordFile)\n\t\tif err != nil {\n\t\t\treturn errors.Wrapf(err, \"error reading %s\", passwordFile)\n\t\t}\n\t\tcfg.Password = string(bytes.TrimRightFunc(b, unicode.IsSpace))\n\t}\n\tif issuerPasswordFile != \"\" {\n\t\tb, err := os.ReadFile(issuerPasswordFile)\n\t\tif err != nil {\n\t\t\treturn errors.Wrapf(err, \"error reading %s\", issuerPasswordFile)\n\t\t}\n\t\tif cfg.AuthorityConfig.CertificateIssuer != nil {\n\t\t\tcfg.AuthorityConfig.CertificateIssuer.Password = string(bytes.TrimRightFunc(b, unicode.IsSpace))\n\t\t}\n\t}\n\n\tauth, err := authority.New(cfg)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\texport, err := auth.Export()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tb, err := protojson.Marshal(export)\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"error marshaling export\")\n\t}\n\n\tvar buf bytes.Buffer\n\tif err := json.Indent(&buf, b, \"\", \"\\t\"); err != nil {\n\t\treturn errors.Wrap(err, \"error indenting export\")\n\t}\n\n\tfmt.Println(buf.String())\n\treturn nil\n}\n"
  },
  {
    "path": "commands/onboard.go",
    "content": "package commands\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\n\t\"github.com/pkg/errors\"\n\t\"github.com/urfave/cli\"\n\n\t\"github.com/smallstep/cli-utils/command\"\n\t\"github.com/smallstep/cli-utils/errs\"\n\t\"github.com/smallstep/cli-utils/fileutil\"\n\t\"github.com/smallstep/cli-utils/ui\"\n\t\"go.step.sm/crypto/randutil\"\n\n\t\"github.com/smallstep/certificates/authority/config\"\n\t\"github.com/smallstep/certificates/ca\"\n\t\"github.com/smallstep/certificates/cas/apiv1\"\n\t\"github.com/smallstep/certificates/pki\"\n)\n\n// defaultOnboardingURL is the production onboarding url, to use a development\n// url use:\n//\n//\texport STEP_CA_ONBOARDING_URL=http://localhost:3002/onboarding/\nconst defaultOnboardingURL = \"https://api.smallstep.com/onboarding/\"\n\ntype onboardingConfiguration struct {\n\tName     string `json:\"name\"`\n\tDNS      string `json:\"dns\"`\n\tAddress  string `json:\"address\"`\n\tpassword []byte\n}\n\ntype onboardingPayload struct {\n\tFingerprint string `json:\"fingerprint\"`\n}\n\ntype onboardingError struct {\n\tStatusCode int    `json:\"statusCode\"`\n\tMessage    string `json:\"message\"`\n}\n\nfunc (e onboardingError) Error() string {\n\treturn e.Message\n}\n\nfunc init() {\n\tcommand.Register(cli.Command{\n\t\tName:      \"onboard\",\n\t\tUsage:     \"configure and run step-ca from the onboarding guide\",\n\t\tUsageText: \"**step-ca onboard** <token>\",\n\t\tAction:    onboardAction,\n\t\tDescription: `**step-ca onboard** configures step certificates using the onboarding guide.\n\nOpen https://smallstep.com/onboarding in your browser and start the CA with the\ngiven token:\n'''\n$ step-ca onboard <token>\n'''\n\n## POSITIONAL ARGUMENTS\n\n<token>\n:  The token string provided by the onboarding guide.`,\n\t})\n}\n\nfunc onboardAction(ctx *cli.Context) error {\n\tif ctx.NArg() == 0 {\n\t\treturn cli.ShowCommandHelp(ctx, \"onboard\")\n\t}\n\tif err := errs.NumberOfArguments(ctx, 1); err != nil {\n\t\treturn err\n\t}\n\n\t// Get onboarding url\n\tonboarding := defaultOnboardingURL\n\tif v := os.Getenv(\"STEP_CA_ONBOARDING_URL\"); v != \"\" {\n\t\tonboarding = v\n\t}\n\n\tu, err := url.Parse(onboarding)\n\tif err != nil {\n\t\treturn errors.Wrapf(err, \"error parsing %s\", onboarding)\n\t}\n\n\tui.Println(\"Connecting to onboarding guide...\")\n\n\ttoken := ctx.Args().Get(0)\n\tonboardingURL := u.ResolveReference(&url.URL{Path: token}).String()\n\n\t//nolint:gosec // onboarding url\n\tres, err := http.Get(onboardingURL)\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"error connecting onboarding guide\")\n\t}\n\tdefer res.Body.Close()\n\tif res.StatusCode >= 400 {\n\t\tvar msg onboardingError\n\t\tif err := readJSON(res.Body, &msg); err != nil {\n\t\t\treturn errors.Wrap(err, \"error unmarshaling response\")\n\t\t}\n\t\treturn errors.Wrap(msg, \"error receiving onboarding guide\")\n\t}\n\n\tvar cfg onboardingConfiguration\n\tif err := readJSON(res.Body, &cfg); err != nil {\n\t\treturn errors.Wrap(err, \"error unmarshaling response\")\n\t}\n\n\tpassword, err := randutil.ASCII(32)\n\tif err != nil {\n\t\treturn err\n\t}\n\tcfg.password = []byte(password)\n\n\tui.Println(\"Initializing step-ca with the following configuration:\")\n\tui.PrintSelected(\"Name\", cfg.Name)\n\tui.PrintSelected(\"DNS\", cfg.DNS)\n\tui.PrintSelected(\"Address\", cfg.Address)\n\tui.PrintSelected(\"Password\", password)\n\tui.Println()\n\n\tcaConfig, fp, err := onboardPKI(cfg)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tpayload, err := json.Marshal(onboardingPayload{Fingerprint: fp})\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"error marshaling payload\")\n\t}\n\n\t//nolint:gosec // onboarding url\n\tresp, err := http.Post(onboardingURL, \"application/json\", bytes.NewBuffer(payload))\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"error connecting onboarding guide\")\n\t}\n\tif resp.StatusCode >= 400 {\n\t\tvar msg onboardingError\n\t\tif err := readJSON(resp.Body, &msg); err != nil {\n\t\t\tui.Printf(\"%s {{ \\\"error unmarshalling response: %v\\\" | yellow }}\\n\", ui.IconWarn, err)\n\t\t} else {\n\t\t\tui.Printf(\"%s {{ \\\"error posting fingerprint: %s\\\" | yellow }}\\n\", ui.IconWarn, msg.Message)\n\t\t}\n\t} else {\n\t\tresp.Body.Close()\n\t}\n\n\tui.Println(\"Initialized!\")\n\tui.Println(\"Step CA is starting. Please return to the onboarding guide in your browser to continue.\")\n\n\tsrv, err := ca.New(caConfig, ca.WithPassword(cfg.password))\n\tif err != nil {\n\t\tfatal(err)\n\t}\n\n\tgo ca.StopReloaderHandler(srv)\n\tif err := srv.Run(); err != nil && !errors.Is(err, http.ErrServerClosed) {\n\t\tfatal(err)\n\t}\n\n\treturn nil\n}\n\nfunc onboardPKI(cfg onboardingConfiguration) (*config.Config, string, error) {\n\tvar opts = []pki.Option{\n\t\tpki.WithAddress(cfg.Address),\n\t\tpki.WithDNSNames([]string{cfg.DNS}),\n\t\tpki.WithProvisioner(\"admin\"),\n\t}\n\n\tp, err := pki.New(apiv1.Options{\n\t\tType:      apiv1.SoftCAS,\n\t\tIsCreator: true,\n\t}, opts...)\n\tif err != nil {\n\t\treturn nil, \"\", err\n\t}\n\n\t// Generate pki\n\tui.Println(\"Generating root certificate...\")\n\troot, err := p.GenerateRootCertificate(cfg.Name, cfg.Name, cfg.Name, cfg.password)\n\tif err != nil {\n\t\treturn nil, \"\", err\n\t}\n\n\tui.Println(\"Generating intermediate certificate...\")\n\terr = p.GenerateIntermediateCertificate(cfg.Name, cfg.Name, cfg.Name, root, cfg.password)\n\tif err != nil {\n\t\treturn nil, \"\", err\n\t}\n\n\t// Write files to disk\n\tif err := p.WriteFiles(); err != nil {\n\t\treturn nil, \"\", err\n\t}\n\n\t// Generate provisioner\n\tui.Println(\"Generating admin provisioner...\")\n\tif err := p.GenerateKeyPairs(cfg.password); err != nil {\n\t\treturn nil, \"\", err\n\t}\n\n\t// Generate and write configuration\n\tcaConfig, err := p.GenerateConfig()\n\tif err != nil {\n\t\treturn nil, \"\", err\n\t}\n\n\tb, err := json.MarshalIndent(caConfig, \"\", \"   \") //nolint:gosec // config struct contains password field by design\n\tif err != nil {\n\t\treturn nil, \"\", errors.Wrapf(err, \"error marshaling %s\", p.GetCAConfigPath())\n\t}\n\tif err := fileutil.WriteFile(p.GetCAConfigPath(), b, 0666); err != nil {\n\t\treturn nil, \"\", errs.FileError(err, p.GetCAConfigPath())\n\t}\n\n\treturn caConfig, p.GetRootFingerprint(), nil\n}\n\nfunc readJSON(r io.ReadCloser, v interface{}) error {\n\tdefer r.Close()\n\treturn json.NewDecoder(r).Decode(v)\n}\n"
  },
  {
    "path": "cosign.pub",
    "content": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEs+6THbAiXx4bja5ARQFNZmPwZjlD\nGRvt5H+9ZFDhrcFPR1E7eB2rt1B/DhobANdHGKjvEBZEf0v4X/7S+SHrIw==\n-----END PUBLIC KEY-----\n"
  },
  {
    "path": "db/db.go",
    "content": "package db\n\nimport (\n\t\"context\"\n\t\"crypto/x509\"\n\t\"encoding/json\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n\t\"golang.org/x/crypto/ssh\"\n\n\t\"github.com/smallstep/nosql\"\n\t\"github.com/smallstep/nosql/database\"\n\n\t\"github.com/smallstep/certificates/authority/provisioner\"\n\t\"github.com/smallstep/certificates/internal/cast\"\n)\n\nvar (\n\tcertsTable             = []byte(\"x509_certs\")\n\tcertsDataTable         = []byte(\"x509_certs_data\")\n\trevokedCertsTable      = []byte(\"revoked_x509_certs\")\n\tcrlTable               = []byte(\"x509_crl\")\n\trevokedSSHCertsTable   = []byte(\"revoked_ssh_certs\")\n\tusedOTTTable           = []byte(\"used_ott\")\n\tsshCertsTable          = []byte(\"ssh_certs\")\n\tsshHostsTable          = []byte(\"ssh_hosts\")\n\tsshUsersTable          = []byte(\"ssh_users\")\n\tsshHostPrincipalsTable = []byte(\"ssh_host_principals\")\n)\n\n// TODO: at the moment we store a single CRL in the database, in a dedicated table.\n// is this acceptable? probably not....\nvar crlKey = []byte(\"crl\")\n\n// ErrAlreadyExists can be returned if the DB attempts to set a key that has\n// been previously set.\nvar ErrAlreadyExists = errors.New(\"already exists\")\n\n// Config represents the JSON attributes used for configuring a step-ca DB.\ntype Config struct {\n\tType       string `json:\"type\"`\n\tDataSource string `json:\"dataSource\"`\n\tValueDir   string `json:\"valueDir,omitempty\"`\n\tDatabase   string `json:\"database,omitempty\"`\n\n\t// BadgerFileLoadingMode can be set to 'FileIO' (instead of the default\n\t// 'MemoryMap') to avoid memory-mapping log files. This can be useful\n\t// in environments with low RAM\n\tBadgerFileLoadingMode string `json:\"badgerFileLoadingMode\"`\n}\n\n// AuthDB is an interface over an Authority DB client that implements a nosql.DB interface.\ntype AuthDB interface {\n\tIsRevoked(sn string) (bool, error)\n\tIsSSHRevoked(sn string) (bool, error)\n\tRevoke(rci *RevokedCertificateInfo) error\n\tRevokeSSH(rci *RevokedCertificateInfo) error\n\tGetCertificate(serialNumber string) (*x509.Certificate, error)\n\tUseToken(id, tok string) (bool, error)\n\tIsSSHHost(name string) (bool, error)\n\tGetSSHHostPrincipals() ([]string, error)\n\tShutdown() error\n}\n\ntype dbKey struct{}\n\n// NewContext adds the given authority database to the context.\nfunc NewContext(ctx context.Context, db AuthDB) context.Context {\n\treturn context.WithValue(ctx, dbKey{}, db)\n}\n\n// FromContext returns the current authority database from the given context.\nfunc FromContext(ctx context.Context) (db AuthDB, ok bool) {\n\tdb, ok = ctx.Value(dbKey{}).(AuthDB)\n\treturn\n}\n\n// MustFromContext returns the current database from the given context. It\n// will panic if it's not in the context.\nfunc MustFromContext(ctx context.Context) AuthDB {\n\tvar (\n\t\tdb AuthDB\n\t\tok bool\n\t)\n\tif db, ok = FromContext(ctx); !ok {\n\t\tpanic(\"authority database is not in the context\")\n\t}\n\treturn db\n}\n\n// CertificateStorer is an extension of AuthDB that allows to store\n// certificates.\ntype CertificateStorer interface {\n\tStoreCertificate(crt *x509.Certificate) error\n\tStoreSSHCertificate(crt *ssh.Certificate) error\n}\n\n// CertificateRevocationListDB is an interface to indicate whether the DB supports CRL generation\ntype CertificateRevocationListDB interface {\n\tGetRevokedCertificates() (*[]RevokedCertificateInfo, error)\n\tGetCRL() (*CertificateRevocationListInfo, error)\n\tStoreCRL(*CertificateRevocationListInfo) error\n}\n\n// DB is a wrapper over the nosql.DB interface.\ntype DB struct {\n\tnosql.DB\n\tisUp bool\n}\n\n// New returns a new database client that implements the AuthDB interface.\nfunc New(c *Config) (AuthDB, error) {\n\tif c == nil {\n\t\treturn newSimpleDB(c)\n\t}\n\n\topts := []nosql.Option{nosql.WithDatabase(c.Database),\n\t\tnosql.WithValueDir(c.ValueDir)}\n\tif c.BadgerFileLoadingMode != \"\" {\n\t\topts = append(opts, nosql.WithBadgerFileLoadingMode(c.BadgerFileLoadingMode))\n\t}\n\n\tdb, err := nosql.New(c.Type, c.DataSource, opts...)\n\tif err != nil {\n\t\treturn nil, errors.Wrapf(err, \"Error opening database of Type %s\", c.Type)\n\t}\n\n\ttables := [][]byte{\n\t\trevokedCertsTable, certsTable, usedOTTTable,\n\t\tsshCertsTable, sshHostsTable, sshHostPrincipalsTable, sshUsersTable,\n\t\trevokedSSHCertsTable, certsDataTable, crlTable,\n\t}\n\tfor _, b := range tables {\n\t\tif err := db.CreateTable(b); err != nil {\n\t\t\treturn nil, errors.Wrapf(err, \"error creating table %s\",\n\t\t\t\tstring(b))\n\t\t}\n\t}\n\n\treturn &DB{db, true}, nil\n}\n\n// RevokedCertificateInfo contains information regarding the certificate\n// revocation action.\ntype RevokedCertificateInfo struct {\n\tSerial        string\n\tProvisionerID string\n\tReasonCode    int\n\tReason        string\n\tRevokedAt     time.Time\n\tExpiresAt     time.Time\n\tTokenID       string\n\tMTLS          bool\n\tACME          bool\n}\n\n// CertificateRevocationListInfo contains a CRL in DER format and associated\n// metadata to allow a decision on whether to regenerate the CRL or not easier\ntype CertificateRevocationListInfo struct {\n\tNumber    int64\n\tExpiresAt time.Time\n\tDuration  time.Duration\n\tDER       []byte\n}\n\n// IsRevoked returns whether or not a certificate with the given identifier\n// has been revoked.\n// In the case of an X509 Certificate the `id` should be the Serial Number of\n// the Certificate.\nfunc (db *DB) IsRevoked(sn string) (bool, error) {\n\t// If the DB is nil then act as pass through.\n\tif db == nil {\n\t\treturn false, nil\n\t}\n\n\t// If the error is `Not Found` then the certificate has not been revoked.\n\t// Any other error should be propagated to the caller.\n\tif _, err := db.Get(revokedCertsTable, []byte(sn)); err != nil {\n\t\tif nosql.IsErrNotFound(err) {\n\t\t\treturn false, nil\n\t\t}\n\t\treturn false, errors.Wrap(err, \"error checking revocation bucket\")\n\t}\n\n\t// This certificate has been revoked.\n\treturn true, nil\n}\n\n// IsSSHRevoked returns whether or not a certificate with the given identifier\n// has been revoked.\n// In the case of an X509 Certificate the `id` should be the Serial Number of\n// the Certificate.\nfunc (db *DB) IsSSHRevoked(sn string) (bool, error) {\n\t// If the DB is nil then act as pass through.\n\tif db == nil {\n\t\treturn false, nil\n\t}\n\n\t// If the error is `Not Found` then the certificate has not been revoked.\n\t// Any other error should be propagated to the caller.\n\tif _, err := db.Get(revokedSSHCertsTable, []byte(sn)); err != nil {\n\t\tif nosql.IsErrNotFound(err) {\n\t\t\treturn false, nil\n\t\t}\n\t\treturn false, errors.Wrap(err, \"error checking revocation bucket\")\n\t}\n\n\t// This certificate has been revoked.\n\treturn true, nil\n}\n\n// Revoke adds a certificate to the revocation table.\nfunc (db *DB) Revoke(rci *RevokedCertificateInfo) error {\n\trcib, err := json.Marshal(rci)\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"error marshaling revoked certificate info\")\n\t}\n\n\t_, swapped, err := db.CmpAndSwap(revokedCertsTable, []byte(rci.Serial), nil, rcib)\n\tswitch {\n\tcase err != nil:\n\t\treturn errors.Wrap(err, \"error AuthDB CmpAndSwap\")\n\tcase !swapped:\n\t\treturn ErrAlreadyExists\n\tdefault:\n\t\treturn nil\n\t}\n}\n\n// RevokeSSH adds a SSH certificate to the revocation table.\nfunc (db *DB) RevokeSSH(rci *RevokedCertificateInfo) error {\n\trcib, err := json.Marshal(rci)\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"error marshaling revoked certificate info\")\n\t}\n\n\t_, swapped, err := db.CmpAndSwap(revokedSSHCertsTable, []byte(rci.Serial), nil, rcib)\n\tswitch {\n\tcase err != nil:\n\t\treturn errors.Wrap(err, \"error AuthDB CmpAndSwap\")\n\tcase !swapped:\n\t\treturn ErrAlreadyExists\n\tdefault:\n\t\treturn nil\n\t}\n}\n\n// GetRevokedCertificates gets a list of all revoked certificates.\nfunc (db *DB) GetRevokedCertificates() (*[]RevokedCertificateInfo, error) {\n\tentries, err := db.List(revokedCertsTable)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvar revokedCerts []RevokedCertificateInfo\n\tfor _, e := range entries {\n\t\tvar data RevokedCertificateInfo\n\t\tif err := json.Unmarshal(e.Value, &data); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\trevokedCerts = append(revokedCerts, data)\n\t}\n\treturn &revokedCerts, nil\n}\n\n// StoreCRL stores a CRL in the DB\nfunc (db *DB) StoreCRL(crlInfo *CertificateRevocationListInfo) error {\n\tcrlInfoBytes, err := json.Marshal(crlInfo)\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"json Marshal error\")\n\t}\n\n\tif err := db.Set(crlTable, crlKey, crlInfoBytes); err != nil {\n\t\treturn errors.Wrap(err, \"database Set error\")\n\t}\n\treturn nil\n}\n\n// GetCRL gets the existing CRL from the database\nfunc (db *DB) GetCRL() (*CertificateRevocationListInfo, error) {\n\tcrlInfoBytes, err := db.Get(crlTable, crlKey)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"database Get error\")\n\t}\n\n\tvar crlInfo CertificateRevocationListInfo\n\terr = json.Unmarshal(crlInfoBytes, &crlInfo)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"json Unmarshal error\")\n\t}\n\treturn &crlInfo, err\n}\n\n// GetCertificate retrieves a certificate by the serial number.\nfunc (db *DB) GetCertificate(serialNumber string) (*x509.Certificate, error) {\n\tasn1Data, err := db.Get(certsTable, []byte(serialNumber))\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"database Get error\")\n\t}\n\tcert, err := x509.ParseCertificate(asn1Data)\n\tif err != nil {\n\t\treturn nil, errors.Wrapf(err, \"error parsing certificate with serial number %s\", serialNumber)\n\t}\n\treturn cert, nil\n}\n\n// GetCertificateData returns the data stored for a provisioner\nfunc (db *DB) GetCertificateData(serialNumber string) (*CertificateData, error) {\n\tb, err := db.Get(certsDataTable, []byte(serialNumber))\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"database Get error\")\n\t}\n\tvar data CertificateData\n\tif err := json.Unmarshal(b, &data); err != nil {\n\t\treturn nil, errors.Wrap(err, \"error unmarshaling json\")\n\t}\n\treturn &data, nil\n}\n\n// StoreCertificate stores a certificate PEM.\nfunc (db *DB) StoreCertificate(crt *x509.Certificate) error {\n\tif err := db.Set(certsTable, []byte(crt.SerialNumber.String()), crt.Raw); err != nil {\n\t\treturn errors.Wrap(err, \"database Set error\")\n\t}\n\treturn nil\n}\n\n// CertificateData is the JSON representation of the data stored in\n// x509_certs_data table.\ntype CertificateData struct {\n\tProvisioner *ProvisionerData    `json:\"provisioner,omitempty\"`\n\tRaInfo      *provisioner.RAInfo `json:\"ra,omitempty\"`\n}\n\n// ProvisionerData is the JSON representation of the provisioner stored in the\n// x509_certs_data table.\ntype ProvisionerData struct {\n\tID   string `json:\"id\"`\n\tName string `json:\"name\"`\n\tType string `json:\"type\"`\n}\n\ntype raProvisioner interface {\n\tRAInfo() *provisioner.RAInfo\n}\n\n// StoreCertificateChain stores the leaf certificate and the provisioner that\n// authorized the certificate.\nfunc (db *DB) StoreCertificateChain(p provisioner.Interface, chain ...*x509.Certificate) error {\n\tleaf := chain[0]\n\tserialNumber := []byte(leaf.SerialNumber.String())\n\tdata := &CertificateData{}\n\tif p != nil {\n\t\tdata.Provisioner = &ProvisionerData{\n\t\t\tID:   p.GetID(),\n\t\t\tName: p.GetName(),\n\t\t\tType: p.GetType().String(),\n\t\t}\n\t\tif rap, ok := p.(raProvisioner); ok {\n\t\t\tdata.RaInfo = rap.RAInfo()\n\t\t}\n\t}\n\tb, err := json.Marshal(data)\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"error marshaling json\")\n\t}\n\t// Add certificate and certificate data in one transaction.\n\ttx := new(database.Tx)\n\ttx.Set(certsTable, serialNumber, leaf.Raw)\n\ttx.Set(certsDataTable, serialNumber, b)\n\tif err := db.Update(tx); err != nil {\n\t\treturn errors.Wrap(err, \"database Update error\")\n\t}\n\treturn nil\n}\n\n// StoreRenewedCertificate stores the leaf certificate and the provisioner that\n// authorized the old certificate if available.\nfunc (db *DB) StoreRenewedCertificate(oldCert *x509.Certificate, chain ...*x509.Certificate) error {\n\tvar certificateData []byte\n\tif data, err := db.GetCertificateData(oldCert.SerialNumber.String()); err == nil {\n\t\tif b, err := json.Marshal(data); err == nil {\n\t\t\tcertificateData = b\n\t\t}\n\t}\n\n\tleaf := chain[0]\n\tserialNumber := []byte(leaf.SerialNumber.String())\n\n\t// Add certificate and certificate data in one transaction.\n\ttx := new(database.Tx)\n\ttx.Set(certsTable, serialNumber, leaf.Raw)\n\tif certificateData != nil {\n\t\ttx.Set(certsDataTable, serialNumber, certificateData)\n\t}\n\tif err := db.Update(tx); err != nil {\n\t\treturn errors.Wrap(err, \"database Update error\")\n\t}\n\treturn nil\n}\n\n// UseToken returns true if we were able to successfully store the token for\n// for the first time, false otherwise.\nfunc (db *DB) UseToken(id, tok string) (bool, error) {\n\t_, swapped, err := db.CmpAndSwap(usedOTTTable, []byte(id), nil, []byte(tok))\n\tif err != nil {\n\t\treturn false, errors.Wrapf(err, \"error storing used token %s/%s\",\n\t\t\tstring(usedOTTTable), id)\n\t}\n\treturn swapped, nil\n}\n\n// IsSSHHost returns if a principal is present in the ssh hosts table.\nfunc (db *DB) IsSSHHost(principal string) (bool, error) {\n\tif _, err := db.Get(sshHostsTable, []byte(strings.ToLower(principal))); err != nil {\n\t\tif database.IsErrNotFound(err) {\n\t\t\treturn false, nil\n\t\t}\n\t\treturn false, errors.Wrap(err, \"database Get error\")\n\t}\n\treturn true, nil\n}\n\ntype sshHostPrincipalData struct {\n\tSerial string\n\tExpiry uint64\n}\n\n// StoreSSHCertificate stores an SSH certificate.\nfunc (db *DB) StoreSSHCertificate(crt *ssh.Certificate) error {\n\tserial := strconv.FormatUint(crt.Serial, 10)\n\ttx := new(database.Tx)\n\ttx.Set(sshCertsTable, []byte(serial), crt.Marshal())\n\tif crt.CertType == ssh.HostCert {\n\t\tfor _, p := range crt.ValidPrincipals {\n\t\t\thostPrincipalData, err := json.Marshal(sshHostPrincipalData{\n\t\t\t\tSerial: serial,\n\t\t\t\tExpiry: crt.ValidBefore,\n\t\t\t})\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\ttx.Set(sshHostsTable, []byte(strings.ToLower(p)), []byte(serial))\n\t\t\ttx.Set(sshHostPrincipalsTable, []byte(strings.ToLower(p)), hostPrincipalData)\n\t\t}\n\t} else {\n\t\tfor _, p := range crt.ValidPrincipals {\n\t\t\ttx.Set(sshUsersTable, []byte(strings.ToLower(p)), []byte(serial))\n\t\t}\n\t}\n\tif err := db.Update(tx); err != nil {\n\t\treturn errors.Wrap(err, \"database Update error\")\n\t}\n\treturn nil\n}\n\n// GetSSHHostPrincipals gets a list of all valid host principals.\nfunc (db *DB) GetSSHHostPrincipals() ([]string, error) {\n\tentries, err := db.List(sshHostPrincipalsTable)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvar principals []string\n\tfor _, e := range entries {\n\t\tvar data sshHostPrincipalData\n\t\tif err := json.Unmarshal(e.Value, &data); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif time.Unix(cast.Int64(data.Expiry), 0).After(time.Now()) {\n\t\t\tprincipals = append(principals, string(e.Key))\n\t\t}\n\t}\n\treturn principals, nil\n}\n\n// Shutdown sends a shutdown message to the database.\nfunc (db *DB) Shutdown() error {\n\tif db.isUp {\n\t\tif err := db.Close(); err != nil {\n\t\t\treturn errors.Wrap(err, \"database shutdown error\")\n\t\t}\n\t\tdb.isUp = false\n\t}\n\treturn nil\n}\n\n// MockAuthDB mocks the AuthDB interface. //\ntype MockAuthDB struct {\n\tErr                     error\n\tRet1                    interface{}\n\tMIsRevoked              func(string) (bool, error)\n\tMIsSSHRevoked           func(string) (bool, error)\n\tMRevoke                 func(rci *RevokedCertificateInfo) error\n\tMRevokeSSH              func(rci *RevokedCertificateInfo) error\n\tMGetCertificate         func(serialNumber string) (*x509.Certificate, error)\n\tMGetCertificateData     func(serialNumber string) (*CertificateData, error)\n\tMStoreCertificate       func(crt *x509.Certificate) error\n\tMUseToken               func(id, tok string) (bool, error)\n\tMIsSSHHost              func(principal string) (bool, error)\n\tMStoreSSHCertificate    func(crt *ssh.Certificate) error\n\tMGetSSHHostPrincipals   func() ([]string, error)\n\tMShutdown               func() error\n\tMGetRevokedCertificates func() (*[]RevokedCertificateInfo, error)\n\tMGetCRL                 func() (*CertificateRevocationListInfo, error)\n\tMStoreCRL               func(*CertificateRevocationListInfo) error\n}\n\nfunc (m *MockAuthDB) GetRevokedCertificates() (*[]RevokedCertificateInfo, error) {\n\tif m.MGetRevokedCertificates != nil {\n\t\treturn m.MGetRevokedCertificates()\n\t}\n\treturn m.Ret1.(*[]RevokedCertificateInfo), m.Err\n}\n\nfunc (m *MockAuthDB) GetCRL() (*CertificateRevocationListInfo, error) {\n\tif m.MGetCRL != nil {\n\t\treturn m.MGetCRL()\n\t}\n\treturn m.Ret1.(*CertificateRevocationListInfo), m.Err\n}\n\nfunc (m *MockAuthDB) StoreCRL(info *CertificateRevocationListInfo) error {\n\tif m.MStoreCRL != nil {\n\t\treturn m.MStoreCRL(info)\n\t}\n\treturn m.Err\n}\n\n// IsRevoked mock.\nfunc (m *MockAuthDB) IsRevoked(sn string) (bool, error) {\n\tif m.MIsRevoked != nil {\n\t\treturn m.MIsRevoked(sn)\n\t}\n\treturn m.Ret1.(bool), m.Err\n}\n\n// IsSSHRevoked mock.\nfunc (m *MockAuthDB) IsSSHRevoked(sn string) (bool, error) {\n\tif m.MIsSSHRevoked != nil {\n\t\treturn m.MIsSSHRevoked(sn)\n\t}\n\treturn m.Ret1.(bool), m.Err\n}\n\n// UseToken mock.\nfunc (m *MockAuthDB) UseToken(id, tok string) (bool, error) {\n\tif m.MUseToken != nil {\n\t\treturn m.MUseToken(id, tok)\n\t}\n\tif m.Ret1 == nil {\n\t\treturn false, m.Err\n\t}\n\treturn m.Ret1.(bool), m.Err\n}\n\n// Revoke mock.\nfunc (m *MockAuthDB) Revoke(rci *RevokedCertificateInfo) error {\n\tif m.MRevoke != nil {\n\t\treturn m.MRevoke(rci)\n\t}\n\treturn m.Err\n}\n\n// RevokeSSH mock.\nfunc (m *MockAuthDB) RevokeSSH(rci *RevokedCertificateInfo) error {\n\tif m.MRevokeSSH != nil {\n\t\treturn m.MRevokeSSH(rci)\n\t}\n\treturn m.Err\n}\n\n// GetCertificate mock.\nfunc (m *MockAuthDB) GetCertificate(serialNumber string) (*x509.Certificate, error) {\n\tif m.MGetCertificate != nil {\n\t\treturn m.MGetCertificate(serialNumber)\n\t}\n\treturn m.Ret1.(*x509.Certificate), m.Err\n}\n\n// GetCertificateData mock.\nfunc (m *MockAuthDB) GetCertificateData(serialNumber string) (*CertificateData, error) {\n\tif m.MGetCertificateData != nil {\n\t\treturn m.MGetCertificateData(serialNumber)\n\t}\n\tif cd, ok := m.Ret1.(*CertificateData); ok {\n\t\treturn cd, m.Err\n\t}\n\treturn nil, m.Err\n}\n\n// StoreCertificate mock.\nfunc (m *MockAuthDB) StoreCertificate(crt *x509.Certificate) error {\n\tif m.MStoreCertificate != nil {\n\t\treturn m.MStoreCertificate(crt)\n\t}\n\treturn m.Err\n}\n\n// IsSSHHost mock.\nfunc (m *MockAuthDB) IsSSHHost(principal string) (bool, error) {\n\tif m.MIsSSHHost != nil {\n\t\treturn m.MIsSSHHost(principal)\n\t}\n\treturn m.Ret1.(bool), m.Err\n}\n\n// StoreSSHCertificate mock.\nfunc (m *MockAuthDB) StoreSSHCertificate(crt *ssh.Certificate) error {\n\tif m.MStoreSSHCertificate != nil {\n\t\treturn m.MStoreSSHCertificate(crt)\n\t}\n\treturn m.Err\n}\n\n// GetSSHHostPrincipals mock.\nfunc (m *MockAuthDB) GetSSHHostPrincipals() ([]string, error) {\n\tif m.MGetSSHHostPrincipals != nil {\n\t\treturn m.MGetSSHHostPrincipals()\n\t}\n\treturn m.Ret1.([]string), m.Err\n}\n\n// Shutdown mock.\nfunc (m *MockAuthDB) Shutdown() error {\n\tif m.MShutdown != nil {\n\t\treturn m.MShutdown()\n\t}\n\treturn m.Err\n}\n\n// MockNoSQLDB //\ntype MockNoSQLDB struct {\n\tErr          error\n\tRet1, Ret2   interface{}\n\tMGet         func(bucket, key []byte) ([]byte, error)\n\tMSet         func(bucket, key, value []byte) error\n\tMOpen        func(dataSourceName string, opt ...database.Option) error\n\tMClose       func() error\n\tMCreateTable func(bucket []byte) error\n\tMDeleteTable func(bucket []byte) error\n\tMDel         func(bucket, key []byte) error\n\tMList        func(bucket []byte) ([]*database.Entry, error)\n\tMUpdate      func(tx *database.Tx) error\n\tMCmpAndSwap  func(bucket, key, old, newval []byte) ([]byte, bool, error)\n}\n\n// CmpAndSwap mock\nfunc (m *MockNoSQLDB) CmpAndSwap(bucket, key, old, newval []byte) ([]byte, bool, error) {\n\tif m.MCmpAndSwap != nil {\n\t\treturn m.MCmpAndSwap(bucket, key, old, newval)\n\t}\n\tif m.Ret1 == nil {\n\t\treturn nil, false, m.Err\n\t}\n\treturn m.Ret1.([]byte), m.Ret2.(bool), m.Err\n}\n\n// Get mock\nfunc (m *MockNoSQLDB) Get(bucket, key []byte) ([]byte, error) {\n\tif m.MGet != nil {\n\t\treturn m.MGet(bucket, key)\n\t}\n\tif m.Ret1 == nil {\n\t\treturn nil, m.Err\n\t}\n\treturn m.Ret1.([]byte), m.Err\n}\n\n// Set mock\nfunc (m *MockNoSQLDB) Set(bucket, key, value []byte) error {\n\tif m.MSet != nil {\n\t\treturn m.MSet(bucket, key, value)\n\t}\n\treturn m.Err\n}\n\n// Open mock\nfunc (m *MockNoSQLDB) Open(dataSourceName string, opt ...database.Option) error {\n\tif m.MOpen != nil {\n\t\treturn m.MOpen(dataSourceName, opt...)\n\t}\n\treturn m.Err\n}\n\n// Close mock\nfunc (m *MockNoSQLDB) Close() error {\n\tif m.MClose != nil {\n\t\treturn m.MClose()\n\t}\n\treturn m.Err\n}\n\n// CreateTable mock\nfunc (m *MockNoSQLDB) CreateTable(bucket []byte) error {\n\tif m.MCreateTable != nil {\n\t\treturn m.MCreateTable(bucket)\n\t}\n\treturn m.Err\n}\n\n// DeleteTable mock\nfunc (m *MockNoSQLDB) DeleteTable(bucket []byte) error {\n\tif m.MDeleteTable != nil {\n\t\treturn m.MDeleteTable(bucket)\n\t}\n\treturn m.Err\n}\n\n// Del mock\nfunc (m *MockNoSQLDB) Del(bucket, key []byte) error {\n\tif m.MDel != nil {\n\t\treturn m.MDel(bucket, key)\n\t}\n\treturn m.Err\n}\n\n// List mock\nfunc (m *MockNoSQLDB) List(bucket []byte) ([]*database.Entry, error) {\n\tif m.MList != nil {\n\t\treturn m.MList(bucket)\n\t}\n\treturn m.Ret1.([]*database.Entry), m.Err\n}\n\n// Update mock\nfunc (m *MockNoSQLDB) Update(tx *database.Tx) error {\n\tif m.MUpdate != nil {\n\t\treturn m.MUpdate(tx)\n\t}\n\treturn m.Err\n}\n"
  },
  {
    "path": "db/db_test.go",
    "content": "package db\n\nimport (\n\t\"bytes\"\n\t\"crypto/x509\"\n\t\"errors\"\n\t\"math/big\"\n\t\"reflect\"\n\t\"testing\"\n\n\t\"github.com/smallstep/assert\"\n\t\"github.com/smallstep/certificates/authority/provisioner\"\n\t\"github.com/smallstep/nosql\"\n\t\"github.com/smallstep/nosql/database\"\n)\n\nfunc TestIsRevoked(t *testing.T) {\n\ttests := map[string]struct {\n\t\tkey       string\n\t\tdb        *DB\n\t\tisRevoked bool\n\t\terr       error\n\t}{\n\t\t\"false/nil db\": {\n\t\t\tkey: \"sn\",\n\t\t},\n\t\t\"false/ErrNotFound\": {\n\t\t\tkey: \"sn\",\n\t\t\tdb:  &DB{&MockNoSQLDB{Err: database.ErrNotFound, Ret1: nil}, true},\n\t\t},\n\t\t\"error/checking bucket\": {\n\t\t\tkey: \"sn\",\n\t\t\tdb:  &DB{&MockNoSQLDB{Err: errors.New(\"force\"), Ret1: nil}, true},\n\t\t\terr: errors.New(\"error checking revocation bucket: force\"),\n\t\t},\n\t\t\"true\": {\n\t\t\tkey:       \"sn\",\n\t\t\tdb:        &DB{&MockNoSQLDB{Ret1: []byte(\"value\")}, true},\n\t\t\tisRevoked: true,\n\t\t},\n\t}\n\tfor name, tc := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tisRevoked, err := tc.db.IsRevoked(tc.key)\n\t\t\tif err != nil {\n\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\tassert.HasPrefix(t, tc.err.Error(), err.Error())\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tassert.Nil(t, tc.err)\n\t\t\t\tassert.Fatal(t, isRevoked == tc.isRevoked)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestRevoke(t *testing.T) {\n\ttests := map[string]struct {\n\t\trci *RevokedCertificateInfo\n\t\tdb  *DB\n\t\terr error\n\t}{\n\t\t\"error/force isRevoked\": {\n\t\t\trci: &RevokedCertificateInfo{Serial: \"sn\"},\n\t\t\tdb: &DB{&MockNoSQLDB{\n\t\t\t\tMCmpAndSwap: func(bucket, sn, old, newval []byte) ([]byte, bool, error) {\n\t\t\t\t\treturn nil, false, errors.New(\"force\")\n\t\t\t\t},\n\t\t\t}, true},\n\t\t\terr: errors.New(\"error AuthDB CmpAndSwap: force\"),\n\t\t},\n\t\t\"error/was already revoked\": {\n\t\t\trci: &RevokedCertificateInfo{Serial: \"sn\"},\n\t\t\tdb: &DB{&MockNoSQLDB{\n\t\t\t\tMCmpAndSwap: func(bucket, sn, old, newval []byte) ([]byte, bool, error) {\n\t\t\t\t\treturn []byte(\"foo\"), false, nil\n\t\t\t\t},\n\t\t\t}, true},\n\t\t\terr: ErrAlreadyExists,\n\t\t},\n\t\t\"ok\": {\n\t\t\trci: &RevokedCertificateInfo{Serial: \"sn\"},\n\t\t\tdb: &DB{&MockNoSQLDB{\n\t\t\t\tMCmpAndSwap: func(bucket, sn, old, newval []byte) ([]byte, bool, error) {\n\t\t\t\t\treturn []byte(\"foo\"), true, nil\n\t\t\t\t},\n\t\t\t}, true},\n\t\t},\n\t}\n\tfor name, tc := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tif err := tc.db.Revoke(tc.rci); err != nil {\n\t\t\t\tif assert.NotNil(t, tc.err) {\n\t\t\t\t\tassert.HasPrefix(t, tc.err.Error(), err.Error())\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tassert.Nil(t, tc.err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestUseToken(t *testing.T) {\n\ttype result struct {\n\t\terr error\n\t\tok  bool\n\t}\n\ttests := map[string]struct {\n\t\tid, tok string\n\t\tdb      *DB\n\t\twant    result\n\t}{\n\t\t\"fail/force-CmpAndSwap-error\": {\n\t\t\tid:  \"id\",\n\t\t\ttok: \"token\",\n\t\t\tdb: &DB{&MockNoSQLDB{\n\t\t\t\tMCmpAndSwap: func(bucket, key, old, newval []byte) ([]byte, bool, error) {\n\t\t\t\t\treturn nil, false, errors.New(\"force\")\n\t\t\t\t},\n\t\t\t}, true},\n\t\t\twant: result{\n\t\t\t\tok:  false,\n\t\t\t\terr: errors.New(\"error storing used token used_ott/id\"),\n\t\t\t},\n\t\t},\n\t\t\"fail/CmpAndSwap-already-exists\": {\n\t\t\tid:  \"id\",\n\t\t\ttok: \"token\",\n\t\t\tdb: &DB{&MockNoSQLDB{\n\t\t\t\tMCmpAndSwap: func(bucket, key, old, newval []byte) ([]byte, bool, error) {\n\t\t\t\t\treturn []byte(\"foo\"), false, nil\n\t\t\t\t},\n\t\t\t}, true},\n\t\t\twant: result{\n\t\t\t\tok: false,\n\t\t\t},\n\t\t},\n\t\t\"ok/cmpAndSwap-success\": {\n\t\t\tid:  \"id\",\n\t\t\ttok: \"token\",\n\t\t\tdb: &DB{&MockNoSQLDB{\n\t\t\t\tMCmpAndSwap: func(bucket, key, old, newval []byte) ([]byte, bool, error) {\n\t\t\t\t\treturn []byte(\"bar\"), true, nil\n\t\t\t\t},\n\t\t\t}, true},\n\t\t\twant: result{\n\t\t\t\tok: true,\n\t\t\t},\n\t\t},\n\t}\n\tfor name, tc := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tswitch ok, err := tc.db.UseToken(tc.id, tc.tok); {\n\t\t\tcase err != nil:\n\t\t\t\tif assert.NotNil(t, tc.want.err) {\n\t\t\t\t\tassert.HasPrefix(t, err.Error(), tc.want.err.Error())\n\t\t\t\t}\n\t\t\t\tassert.False(t, ok)\n\t\t\tcase ok:\n\t\t\t\tassert.True(t, tc.want.ok)\n\t\t\tdefault:\n\t\t\t\tassert.False(t, tc.want.ok)\n\t\t\t}\n\t\t})\n\t}\n}\n\n// wrappedProvisioner implements raProvisioner and attProvisioner.\ntype wrappedProvisioner struct {\n\tprovisioner.Interface\n\traInfo *provisioner.RAInfo\n}\n\nfunc (p *wrappedProvisioner) RAInfo() *provisioner.RAInfo {\n\treturn p.raInfo\n}\n\nfunc TestDB_StoreCertificateChain(t *testing.T) {\n\tp := &provisioner.JWK{\n\t\tID:   \"some-id\",\n\t\tName: \"admin\",\n\t\tType: \"JWK\",\n\t}\n\trap := &wrappedProvisioner{\n\t\tInterface: p,\n\t\traInfo: &provisioner.RAInfo{\n\t\t\tProvisionerID:   \"ra-id\",\n\t\t\tProvisionerType: \"JWK\",\n\t\t\tProvisionerName: \"ra\",\n\t\t},\n\t}\n\tchain := []*x509.Certificate{\n\t\t{Raw: []byte(\"the certificate\"), SerialNumber: big.NewInt(1234)},\n\t}\n\ttype fields struct {\n\t\tDB   nosql.DB\n\t\tisUp bool\n\t}\n\ttype args struct {\n\t\tp     provisioner.Interface\n\t\tchain []*x509.Certificate\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tfields  fields\n\t\targs    args\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", fields{&MockNoSQLDB{\n\t\t\tMUpdate: func(tx *database.Tx) error {\n\t\t\t\tif len(tx.Operations) != 2 {\n\t\t\t\t\tt.Fatal(\"unexpected number of operations\")\n\t\t\t\t}\n\t\t\t\tassert.Equals(t, []byte(\"x509_certs\"), tx.Operations[0].Bucket)\n\t\t\t\tassert.Equals(t, []byte(\"1234\"), tx.Operations[0].Key)\n\t\t\t\tassert.Equals(t, []byte(\"the certificate\"), tx.Operations[0].Value)\n\t\t\t\tassert.Equals(t, []byte(\"x509_certs_data\"), tx.Operations[1].Bucket)\n\t\t\t\tassert.Equals(t, []byte(\"1234\"), tx.Operations[1].Key)\n\t\t\t\tassert.Equals(t, []byte(`{\"provisioner\":{\"id\":\"some-id\",\"name\":\"admin\",\"type\":\"JWK\"}}`), tx.Operations[1].Value)\n\t\t\t\treturn nil\n\t\t\t},\n\t\t}, true}, args{p, chain}, false},\n\t\t{\"ok ra provisioner\", fields{&MockNoSQLDB{\n\t\t\tMUpdate: func(tx *database.Tx) error {\n\t\t\t\tif len(tx.Operations) != 2 {\n\t\t\t\t\tt.Fatal(\"unexpected number of operations\")\n\t\t\t\t}\n\t\t\t\tassert.Equals(t, []byte(\"x509_certs\"), tx.Operations[0].Bucket)\n\t\t\t\tassert.Equals(t, []byte(\"1234\"), tx.Operations[0].Key)\n\t\t\t\tassert.Equals(t, []byte(\"the certificate\"), tx.Operations[0].Value)\n\t\t\t\tassert.Equals(t, []byte(\"x509_certs_data\"), tx.Operations[1].Bucket)\n\t\t\t\tassert.Equals(t, []byte(\"1234\"), tx.Operations[1].Key)\n\t\t\t\tassert.Equals(t, []byte(`{\"provisioner\":{\"id\":\"some-id\",\"name\":\"admin\",\"type\":\"JWK\"},\"ra\":{\"provisionerId\":\"ra-id\",\"provisionerType\":\"JWK\",\"provisionerName\":\"ra\"}}`), tx.Operations[1].Value)\n\t\t\t\tassert.Equals(t, `{\"provisioner\":{\"id\":\"some-id\",\"name\":\"admin\",\"type\":\"JWK\"},\"ra\":{\"provisionerId\":\"ra-id\",\"provisionerType\":\"JWK\",\"provisionerName\":\"ra\"}}`, string(tx.Operations[1].Value))\n\t\t\t\treturn nil\n\t\t\t},\n\t\t}, true}, args{rap, chain}, false},\n\t\t{\"ok no provisioner\", fields{&MockNoSQLDB{\n\t\t\tMUpdate: func(tx *database.Tx) error {\n\t\t\t\tif len(tx.Operations) != 2 {\n\t\t\t\t\tt.Fatal(\"unexpected number of operations\")\n\t\t\t\t}\n\t\t\t\tassert.Equals(t, []byte(\"x509_certs\"), tx.Operations[0].Bucket)\n\t\t\t\tassert.Equals(t, []byte(\"1234\"), tx.Operations[0].Key)\n\t\t\t\tassert.Equals(t, []byte(\"the certificate\"), tx.Operations[0].Value)\n\t\t\t\tassert.Equals(t, []byte(\"x509_certs_data\"), tx.Operations[1].Bucket)\n\t\t\t\tassert.Equals(t, []byte(\"1234\"), tx.Operations[1].Key)\n\t\t\t\tassert.Equals(t, []byte(`{}`), tx.Operations[1].Value)\n\t\t\t\treturn nil\n\t\t\t},\n\t\t}, true}, args{nil, chain}, false},\n\t\t{\"fail store certificate\", fields{&MockNoSQLDB{\n\t\t\tMUpdate: func(tx *database.Tx) error {\n\t\t\t\treturn errors.New(\"test error\")\n\t\t\t},\n\t\t}, true}, args{p, chain}, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\td := &DB{\n\t\t\t\tDB:   tt.fields.DB,\n\t\t\t\tisUp: tt.fields.isUp,\n\t\t\t}\n\t\t\tif err := d.StoreCertificateChain(tt.args.p, tt.args.chain...); (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"DB.StoreCertificateChain() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDB_GetCertificateData(t *testing.T) {\n\ttype fields struct {\n\t\tDB   nosql.DB\n\t\tisUp bool\n\t}\n\ttype args struct {\n\t\tserialNumber string\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tfields  fields\n\t\targs    args\n\t\twant    *CertificateData\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", fields{&MockNoSQLDB{\n\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\tassert.Equals(t, bucket, []byte(\"x509_certs_data\"))\n\t\t\t\tassert.Equals(t, key, []byte(\"1234\"))\n\t\t\t\treturn []byte(`{\"provisioner\":{\"id\":\"some-id\",\"name\":\"admin\",\"type\":\"JWK\"}}`), nil\n\t\t\t},\n\t\t}, true}, args{\"1234\"}, &CertificateData{\n\t\t\tProvisioner: &ProvisionerData{\n\t\t\t\tID: \"some-id\", Name: \"admin\", Type: \"JWK\",\n\t\t\t},\n\t\t}, false},\n\t\t{\"fail not found\", fields{&MockNoSQLDB{\n\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\treturn nil, database.ErrNotFound\n\t\t\t},\n\t\t}, true}, args{\"1234\"}, nil, true},\n\t\t{\"fail db\", fields{&MockNoSQLDB{\n\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\treturn nil, errors.New(\"an error\")\n\t\t\t},\n\t\t}, true}, args{\"1234\"}, nil, true},\n\t\t{\"fail unmarshal\", fields{&MockNoSQLDB{\n\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\treturn []byte(`{\"bad-json\"}`), nil\n\t\t\t},\n\t\t}, true}, args{\"1234\"}, nil, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tdb := &DB{\n\t\t\t\tDB:   tt.fields.DB,\n\t\t\t\tisUp: tt.fields.isUp,\n\t\t\t}\n\t\t\tgot, err := db.GetCertificateData(tt.args.serialNumber)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"DB.GetCertificateData() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"DB.GetCertificateData() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDB_StoreRenewedCertificate(t *testing.T) {\n\toldCert := &x509.Certificate{SerialNumber: big.NewInt(1)}\n\tchain := []*x509.Certificate{\n\t\t&x509.Certificate{SerialNumber: big.NewInt(2), Raw: []byte(\"raw\")},\n\t\t&x509.Certificate{SerialNumber: big.NewInt(0)},\n\t}\n\n\ttestErr := errors.New(\"test error\")\n\tcertsData := []byte(`{\"provisioner\":{\"id\":\"p\",\"name\":\"name\",\"type\":\"JWK\"},\"ra\":{\"provisionerId\":\"rap\",\"provisionerType\":\"JWK\",\"provisionerName\":\"rapname\"}}`)\n\tmatchOperation := func(op *database.TxEntry, bucket, key, value []byte) bool {\n\t\treturn bytes.Equal(op.Bucket, bucket) && bytes.Equal(op.Key, key) && bytes.Equal(op.Value, value)\n\t}\n\n\ttype fields struct {\n\t\tDB   nosql.DB\n\t\tisUp bool\n\t}\n\ttype args struct {\n\t\toldCert *x509.Certificate\n\t\tchain   []*x509.Certificate\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tfields  fields\n\t\targs    args\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", fields{&MockNoSQLDB{\n\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\tif bytes.Equal(bucket, certsDataTable) && bytes.Equal(key, []byte(\"1\")) {\n\t\t\t\t\treturn certsData, nil\n\t\t\t\t}\n\t\t\t\tt.Error(\"ok failed: unexpected get\")\n\t\t\t\treturn nil, testErr\n\t\t\t},\n\t\t\tMUpdate: func(tx *database.Tx) error {\n\t\t\t\tif len(tx.Operations) != 2 {\n\t\t\t\t\tt.Error(\"ok failed: unexpected number of operations\")\n\t\t\t\t\treturn testErr\n\t\t\t\t}\n\t\t\t\top0, op1 := tx.Operations[0], tx.Operations[1]\n\t\t\t\tif !matchOperation(op0, certsTable, []byte(\"2\"), []byte(\"raw\")) {\n\t\t\t\t\tt.Errorf(\"ok failed: unexpected entry 0, %s[%s]=%s\", op0.Bucket, op0.Key, op0.Value)\n\t\t\t\t\treturn testErr\n\t\t\t\t}\n\t\t\t\tif !matchOperation(op1, certsDataTable, []byte(\"2\"), certsData) {\n\t\t\t\t\tt.Errorf(\"ok failed: unexpected entry 1, %s[%s]=%s\", op1.Bucket, op1.Key, op1.Value)\n\t\t\t\t\treturn testErr\n\t\t\t\t}\n\t\t\t\treturn nil\n\t\t\t},\n\t\t}, true}, args{oldCert, chain}, false},\n\t\t{\"ok no data\", fields{&MockNoSQLDB{\n\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\treturn nil, database.ErrNotFound\n\t\t\t},\n\t\t\tMUpdate: func(tx *database.Tx) error {\n\t\t\t\tif len(tx.Operations) != 1 {\n\t\t\t\t\tt.Error(\"ok failed: unexpected number of operations\")\n\t\t\t\t\treturn testErr\n\t\t\t\t}\n\t\t\t\top0 := tx.Operations[0]\n\t\t\t\tif !matchOperation(op0, certsTable, []byte(\"2\"), []byte(\"raw\")) {\n\t\t\t\t\tt.Errorf(\"ok failed: unexpected entry 0, %s[%s]=%s\", op0.Bucket, op0.Key, op0.Value)\n\t\t\t\t\treturn testErr\n\t\t\t\t}\n\t\t\t\treturn nil\n\t\t\t},\n\t\t}, true}, args{oldCert, chain}, false},\n\t\t{\"ok fail marshal\", fields{&MockNoSQLDB{\n\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\treturn []byte(`{\"bad\":\"json\"`), nil\n\t\t\t},\n\t\t\tMUpdate: func(tx *database.Tx) error {\n\t\t\t\tif len(tx.Operations) != 1 {\n\t\t\t\t\tt.Error(\"ok failed: unexpected number of operations\")\n\t\t\t\t\treturn testErr\n\t\t\t\t}\n\t\t\t\top0 := tx.Operations[0]\n\t\t\t\tif !matchOperation(op0, certsTable, []byte(\"2\"), []byte(\"raw\")) {\n\t\t\t\t\tt.Errorf(\"ok failed: unexpected entry 0, %s[%s]=%s\", op0.Bucket, op0.Key, op0.Value)\n\t\t\t\t\treturn testErr\n\t\t\t\t}\n\t\t\t\treturn nil\n\t\t\t},\n\t\t}, true}, args{oldCert, chain}, false},\n\t\t{\"fail\", fields{&MockNoSQLDB{\n\t\t\tMGet: func(bucket, key []byte) ([]byte, error) {\n\t\t\t\treturn certsData, nil\n\t\t\t},\n\t\t\tMUpdate: func(tx *database.Tx) error {\n\t\t\t\treturn testErr\n\t\t\t},\n\t\t}, true}, args{oldCert, chain}, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tdb := &DB{\n\t\t\t\tDB:   tt.fields.DB,\n\t\t\t\tisUp: tt.fields.isUp,\n\t\t\t}\n\t\t\tif err := db.StoreRenewedCertificate(tt.args.oldCert, tt.args.chain...); (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"DB.StoreRenewedCertificate() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "db/simple.go",
    "content": "package db\n\nimport (\n\t\"crypto/x509\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n\t\"github.com/smallstep/nosql/database\"\n\t\"golang.org/x/crypto/ssh\"\n)\n\n// ErrNotImplemented is an error returned when an operation is Not Implemented.\nvar ErrNotImplemented = errors.Errorf(\"not implemented\")\n\n// SimpleDB is a barebones implementation of the DB interface. It is NOT an\n// in memory implementation of the DB, but rather the bare minimum of\n// functionality that the CA requires to operate securely.\ntype SimpleDB struct {\n\tusedTokens *sync.Map\n}\n\nfunc newSimpleDB(*Config) (*SimpleDB, error) {\n\tdb := &SimpleDB{}\n\tdb.usedTokens = new(sync.Map)\n\treturn db, nil\n}\n\n// IsRevoked noop\nfunc (s *SimpleDB) IsRevoked(string) (bool, error) {\n\treturn false, nil\n}\n\n// IsSSHRevoked noop\nfunc (s *SimpleDB) IsSSHRevoked(string) (bool, error) {\n\treturn false, nil\n}\n\n// Revoke returns a \"NotImplemented\" error.\nfunc (s *SimpleDB) Revoke(*RevokedCertificateInfo) error {\n\treturn ErrNotImplemented\n}\n\n// GetRevokedCertificates returns a \"NotImplemented\" error.\nfunc (s *SimpleDB) GetRevokedCertificates() (*[]RevokedCertificateInfo, error) {\n\treturn nil, ErrNotImplemented\n}\n\n// GetCRL returns a \"NotImplemented\" error.\nfunc (s *SimpleDB) GetCRL() (*CertificateRevocationListInfo, error) {\n\treturn nil, ErrNotImplemented\n}\n\n// StoreCRL returns a \"NotImplemented\" error.\nfunc (s *SimpleDB) StoreCRL(*CertificateRevocationListInfo) error {\n\treturn ErrNotImplemented\n}\n\n// RevokeSSH returns a \"NotImplemented\" error.\nfunc (s *SimpleDB) RevokeSSH(*RevokedCertificateInfo) error {\n\treturn ErrNotImplemented\n}\n\n// GetCertificate returns a \"NotImplemented\" error.\nfunc (s *SimpleDB) GetCertificate(string) (*x509.Certificate, error) {\n\treturn nil, ErrNotImplemented\n}\n\n// StoreCertificate returns a \"NotImplemented\" error.\nfunc (s *SimpleDB) StoreCertificate(*x509.Certificate) error {\n\treturn ErrNotImplemented\n}\n\ntype usedToken struct {\n\tUsedAt int64  `json:\"ua,omitempty\"`\n\tToken  string `json:\"tok,omitempty\"`\n}\n\n// UseToken returns a \"NotImplemented\" error.\nfunc (s *SimpleDB) UseToken(id, tok string) (bool, error) {\n\tif _, ok := s.usedTokens.LoadOrStore(id, &usedToken{\n\t\tUsedAt: time.Now().Unix(),\n\t\tToken:  tok,\n\t}); ok {\n\t\t// Token already exists in DB.\n\t\treturn false, nil\n\t}\n\t// Successfully stored token.\n\treturn true, nil\n}\n\n// IsSSHHost returns a \"NotImplemented\" error.\nfunc (s *SimpleDB) IsSSHHost(string) (bool, error) {\n\treturn false, ErrNotImplemented\n}\n\n// StoreSSHCertificate returns a \"NotImplemented\" error.\nfunc (s *SimpleDB) StoreSSHCertificate(*ssh.Certificate) error {\n\treturn ErrNotImplemented\n}\n\n// GetSSHHostPrincipals returns a \"NotImplemented\" error.\nfunc (s *SimpleDB) GetSSHHostPrincipals() ([]string, error) {\n\treturn nil, ErrNotImplemented\n}\n\n// Shutdown returns nil\nfunc (s *SimpleDB) Shutdown() error {\n\treturn nil\n}\n\n// nosql.DB interface implementation //\n\n// Open opens the database available with the given options.\nfunc (s *SimpleDB) Open(string, ...database.Option) error {\n\treturn ErrNotImplemented\n}\n\n// Close closes the current database.\nfunc (s *SimpleDB) Close() error {\n\treturn ErrNotImplemented\n}\n\n// Get returns the value stored in the given table/bucket and key.\nfunc (s *SimpleDB) Get([]byte, []byte) ([]byte, error) {\n\treturn nil, ErrNotImplemented\n}\n\n// Set sets the given value in the given table/bucket and key.\nfunc (s *SimpleDB) Set([]byte, []byte, []byte) error {\n\treturn ErrNotImplemented\n}\n\n// CmpAndSwap swaps the value at the given bucket and key if the current\n// value is equivalent to the oldValue input. Returns 'true' if the\n// swap was successful and 'false' otherwise.\nfunc (s *SimpleDB) CmpAndSwap([]byte, []byte, []byte, []byte) ([]byte, bool, error) {\n\treturn nil, false, ErrNotImplemented\n}\n\n// Del deletes the data in the given table/bucket and key.\nfunc (s *SimpleDB) Del([]byte, []byte) error {\n\treturn ErrNotImplemented\n}\n\n// List returns a list of all the entries in a given table/bucket.\nfunc (s *SimpleDB) List([]byte) ([]*database.Entry, error) {\n\treturn nil, ErrNotImplemented\n}\n\n// Update performs a transaction with multiple read-write commands.\nfunc (s *SimpleDB) Update(*database.Tx) error {\n\treturn ErrNotImplemented\n}\n\n// CreateTable creates a table or a bucket in the database.\nfunc (s *SimpleDB) CreateTable([]byte) error {\n\treturn ErrNotImplemented\n}\n\n// DeleteTable deletes a table or a bucket in the database.\nfunc (s *SimpleDB) DeleteTable([]byte) error {\n\treturn ErrNotImplemented\n}\n"
  },
  {
    "path": "db/simple_test.go",
    "content": "package db\n\nimport (\n\t\"testing\"\n\n\t\"github.com/smallstep/assert\"\n)\n\nfunc TestSimpleDB(t *testing.T) {\n\tdb, err := newSimpleDB(nil)\n\tassert.FatalError(t, err)\n\n\t// Revoke\n\tassert.Equals(t, ErrNotImplemented, db.Revoke(nil))\n\n\t// IsRevoked -- verify noop\n\tisRevoked, err := db.IsRevoked(\"foo\")\n\tassert.False(t, isRevoked)\n\tassert.Nil(t, err)\n\n\t// StoreCertificate\n\tassert.Equals(t, ErrNotImplemented, db.StoreCertificate(nil))\n\n\t// UseToken\n\tok, err := db.UseToken(\"foo\", \"bar\")\n\tassert.True(t, ok)\n\tassert.Nil(t, err)\n\tok, err = db.UseToken(\"foo\", \"cat\")\n\tassert.False(t, ok)\n\tassert.Nil(t, err)\n\n\t// Shutdown -- verify noop\n\tassert.FatalError(t, db.Shutdown())\n\tok, err = db.UseToken(\"foo\", \"cat\")\n\tassert.False(t, ok)\n\tassert.Nil(t, err)\n}\n"
  },
  {
    "path": "debian/copyright",
    "content": "Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/\nUpstream-Name: step-ca\nSource: https://github.com/smallstep/certificates\n\nFiles: *\nCopyright: 2021 Smallstep Labs, Inc.\nLicense: Apache 2.0\n\nLicense: Apache 2.0\n Copyright (c) 2021 Smallstep Labs, Inc.\n .\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n .\n     http://www.apache.org/licenses/LICENSE-2.0\n .\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n"
  },
  {
    "path": "docker/Dockerfile",
    "content": "FROM golang:alpine AS builder\n\nWORKDIR /src\nCOPY . .\n\nRUN apk add --no-cache curl git make libcap\nRUN make V=1 bin/step-ca\nRUN setcap CAP_NET_BIND_SERVICE=+eip bin/step-ca\n\nFROM smallstep/step-kms-plugin:cloud AS kms\n\nFROM smallstep/step-cli:latest\n\nCOPY --from=builder /src/bin/step-ca /usr/local/bin/step-ca\nCOPY --from=kms /usr/local/bin/step-kms-plugin /usr/local/bin/step-kms-plugin\n\nUSER step\n\nENV CONFIGPATH=\"/home/step/config/ca.json\"\nENV PWDPATH=\"/home/step/secrets/password\"\n\nVOLUME [\"/home/step\"]\nSTOPSIGNAL SIGTERM\nHEALTHCHECK CMD step ca health 2>/dev/null | grep \"^ok\" >/dev/null\n\nCOPY docker/entrypoint.sh /entrypoint.sh\n\nENTRYPOINT [\"/bin/bash\", \"/entrypoint.sh\"]\nCMD [\"/usr/local/bin/step-ca\", \"--password-file\", \"/home/step/secrets/password\", \"/home/step/config/ca.json\"]\n"
  },
  {
    "path": "docker/Dockerfile.hsm",
    "content": "FROM golang:trixie AS builder\n\nWORKDIR /src\nCOPY . .\n\nRUN apt-get update\nRUN apt-get install -y --no-install-recommends \\\n        gcc pkgconf libpcsclite-dev libcap2-bin\nRUN make V=1 GO_ENVS=\"CGO_ENABLED=1\" bin/step-ca\nRUN setcap CAP_NET_BIND_SERVICE=+eip bin/step-ca\n\nFROM smallstep/step-kms-plugin:trixie AS kms\n\nFROM smallstep/step-cli:trixie\n\nCOPY --from=builder /src/bin/step-ca /usr/local/bin/step-ca\nCOPY --from=kms /usr/local/bin/step-kms-plugin /usr/local/bin/step-kms-plugin\n\nUSER root\nRUN apt-get update\nRUN apt-get install -y --no-install-recommends opensc opensc-pkcs11 pcscd gnutls-bin libpcsclite1 p11-kit yubihsm-pkcs11\nRUN mkdir -p /run/pcscd\nRUN chown step:step /run/pcscd\nUSER step\n\nENV CONFIGPATH=\"/home/step/config/ca.json\"\nENV PWDPATH=\"/home/step/secrets/password\"\n\nVOLUME [\"/home/step\"]\nSTOPSIGNAL SIGTERM\nHEALTHCHECK CMD step ca health 2>/dev/null | grep \"^ok\" >/dev/null\n\nCOPY docker/entrypoint.sh /entrypoint.sh\n\nENTRYPOINT [\"/bin/bash\", \"/entrypoint.sh\"]\nCMD [\"/usr/local/bin/step-ca\", \"--password-file\", \"/home/step/secrets/password\", \"/home/step/config/ca.json\"]\n"
  },
  {
    "path": "docker/entrypoint.sh",
    "content": "#!/bin/bash\nset -eo pipefail\n\n# Paraphrased from:\n# https://github.com/influxdata/influxdata-docker/blob/0d341f18067c4652dfa8df7dcb24d69bf707363d/influxdb/2.0/entrypoint.sh\n# (a repo with no LICENSE.md)\n\nexport STEPPATH=$(step path)\n\n# List of env vars required for step ca init\ndeclare -ra REQUIRED_INIT_VARS=(DOCKER_STEPCA_INIT_NAME DOCKER_STEPCA_INIT_DNS_NAMES)\n\n# Ensure all env vars required to run step ca init are set.\nfunction init_if_possible () {\n    local missing_vars=0\n    for var in \"${REQUIRED_INIT_VARS[@]}\"; do\n        if [ -z \"${!var}\" ]; then\n            missing_vars=1\n        fi\n    done\n    if [ ${missing_vars} = 1 ]; then\n        >&2 echo \"there is no ca.json config file; please run step ca init, or provide config parameters via DOCKER_STEPCA_INIT_ vars\"\n    else\n        step_ca_init \"${@}\"\n    fi\n}\n\nfunction generate_password () {\n    set +o pipefail\n    < /dev/urandom tr -dc A-Za-z0-9 | head -c40\n    echo\n    set -o pipefail\n}\n\n# Initialize a CA if not already initialized\nfunction step_ca_init () {\n    DOCKER_STEPCA_INIT_PROVISIONER_NAME=\"${DOCKER_STEPCA_INIT_PROVISIONER_NAME:-admin}\"\n    DOCKER_STEPCA_INIT_ADMIN_SUBJECT=\"${DOCKER_STEPCA_INIT_ADMIN_SUBJECT:-step}\"\n    DOCKER_STEPCA_INIT_ADDRESS=\"${DOCKER_STEPCA_INIT_ADDRESS:-:9000}\"\n    DOCKER_STEPCA_INIT_ROOT_FILE=\"${DOCKER_STEPCA_INIT_ROOT_FILE:-\"/run/secrets/root_ca.crt\"}\"\n    DOCKER_STEPCA_INIT_KEY_FILE=\"${DOCKER_STEPCA_INIT_KEY_FILE:-\"/run/secrets/root_ca_key\"}\"\n    DOCKER_STEPCA_INIT_KEY_PASSWORD_FILE=\"${DOCKER_STEPCA_INIT_KEY_PASSWORD_FILE:-\"/run/secrets/root_ca_key_password\"}\"\n\n    local -a setup_args=(\n        --name \"${DOCKER_STEPCA_INIT_NAME}\"\n        --dns \"${DOCKER_STEPCA_INIT_DNS_NAMES}\"\n        --provisioner \"${DOCKER_STEPCA_INIT_PROVISIONER_NAME}\"\n        --password-file \"${STEPPATH}/password\"\n        --provisioner-password-file \"${STEPPATH}/provisioner_password\"\n        --address \"${DOCKER_STEPCA_INIT_ADDRESS}\"\n    )\n    if [ -n \"${DOCKER_STEPCA_INIT_PASSWORD_FILE}\" ]; then\n        cat < \"${DOCKER_STEPCA_INIT_PASSWORD_FILE}\" > \"${STEPPATH}/password\"\n        cat < \"${DOCKER_STEPCA_INIT_PASSWORD_FILE}\" > \"${STEPPATH}/provisioner_password\"\n    elif [ -n \"${DOCKER_STEPCA_INIT_PASSWORD}\" ]; then\n        echo \"${DOCKER_STEPCA_INIT_PASSWORD}\" > \"${STEPPATH}/password\"\n        echo \"${DOCKER_STEPCA_INIT_PASSWORD}\" > \"${STEPPATH}/provisioner_password\"\n    else\n        generate_password > \"${STEPPATH}/password\"\n        generate_password > \"${STEPPATH}/provisioner_password\"\n    fi\n    if [ -f \"${DOCKER_STEPCA_INIT_ROOT_FILE}\" ]; then\n        setup_args=(\"${setup_args[@]}\" --root \"${DOCKER_STEPCA_INIT_ROOT_FILE}\")\n    fi\n    if [ -f \"${DOCKER_STEPCA_INIT_KEY_FILE}\" ]; then\n        setup_args=(\"${setup_args[@]}\" --key \"${DOCKER_STEPCA_INIT_KEY_FILE}\")\n    fi\n    if [ -f \"${DOCKER_STEPCA_INIT_KEY_PASSWORD_FILE}\" ]; then\n        setup_args=(\"${setup_args[@]}\" --key-password-file \"${DOCKER_STEPCA_INIT_KEY_PASSWORD_FILE}\")\n    fi\n    if [ -n \"${DOCKER_STEPCA_INIT_DEPLOYMENT_TYPE}\" ]; then\n        setup_args=(\"${setup_args[@]}\" --deployment-type \"${DOCKER_STEPCA_INIT_DEPLOYMENT_TYPE}\")\n    fi\n    if [ -n \"${DOCKER_STEPCA_INIT_WITH_CA_URL}\" ]; then\n        setup_args=(\"${setup_args[@]}\" --with-ca-url \"${DOCKER_STEPCA_INIT_WITH_CA_URL}\")\n    fi\n    if [ \"${DOCKER_STEPCA_INIT_SSH}\" == \"true\" ]; then\n        setup_args=(\"${setup_args[@]}\" --ssh)\n    fi\n    if [ \"${DOCKER_STEPCA_INIT_ACME}\" == \"true\" ]; then\n        setup_args=(\"${setup_args[@]}\" --acme)\n    fi\n    if [ \"${DOCKER_STEPCA_INIT_REMOTE_MANAGEMENT}\" == \"true\" ]; then\n        setup_args=(\"${setup_args[@]}\" --remote-management\n                       --admin-subject \"${DOCKER_STEPCA_INIT_ADMIN_SUBJECT}\"\n        )\n    fi\n    step ca init \"${setup_args[@]}\"\n   \techo \"\"\n    if [ \"${DOCKER_STEPCA_INIT_REMOTE_MANAGEMENT}\" == \"true\" ]; then\n        echo \"👉 Your CA administrative username is: ${DOCKER_STEPCA_INIT_ADMIN_SUBJECT}\"\n    fi\n    echo \"👉 Your CA administrative password is: $(< $STEPPATH/provisioner_password )\"\n    echo \"🤫 This will only be displayed once.\"\n    shred -u $STEPPATH/provisioner_password\n    mv $STEPPATH/password $PWDPATH\n}\n\nif [ -f /usr/sbin/pcscd ]; then\n    /usr/sbin/pcscd\nfi\n\nif [ ! -f \"${STEPPATH}/config/ca.json\" ]; then\n    init_if_possible\nfi\n\nexec \"${@}\"\n"
  },
  {
    "path": "errs/error.go",
    "content": "package errs\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\n\t\"github.com/pkg/errors\"\n\n\t\"github.com/smallstep/certificates/api/log\"\n\t\"github.com/smallstep/certificates/api/render\"\n)\n\n// Option modifies the Error type.\ntype Option func(e *Error) error\n\n// withDefaultMessage returns an Option that modifies the error by overwriting\n// the message only if it is empty. Having withDefaultMessage and\n// withFormattedMessage avoid vet errors when the \"format\" passed to\n// \"fmt.Sprintf\" is not a constant.\nfunc withDefaultMessage(message string) Option {\n\treturn func(e *Error) error {\n\t\tif e.Msg != \"\" {\n\t\t\treturn e\n\t\t}\n\t\te.Msg = message\n\t\treturn e\n\t}\n}\n\n// withFormattedMessage returns an Option that modifies the error by overwriting\n// the formatted message only if it is empty.\nfunc withFormattedMessage(format string, args ...interface{}) Option {\n\treturn func(e *Error) error {\n\t\tif e.Msg != \"\" {\n\t\t\treturn e\n\t\t}\n\t\te.Msg = fmt.Sprintf(format, args...)\n\t\treturn e\n\t}\n}\n\n// WithMessage returns an Option that modifies the error by overwriting the\n// message with the formatted string.\nfunc WithMessage(format string, args ...interface{}) Option {\n\treturn func(e *Error) error {\n\t\te.Msg = fmt.Sprintf(format, args...)\n\t\treturn e\n\t}\n}\n\n// WithErrorMessage returns an Option that modifies the error by overwriting the\n// message with the error string.\nfunc WithErrorMessage() Option {\n\treturn func(e *Error) error {\n\t\te.Msg = e.Error()\n\t\treturn e\n\t}\n}\n\n// WithKeyVal returns an Option that adds the given key-value pair to the\n// Error details. This is helpful for debugging errors.\nfunc WithKeyVal(key string, val interface{}) Option {\n\treturn func(e *Error) error {\n\t\tif e.Details == nil {\n\t\t\te.Details = make(map[string]interface{})\n\t\t}\n\t\te.Details[key] = val\n\t\treturn e\n\t}\n}\n\n// Error represents the CA API errors.\ntype Error struct {\n\tStatus    int\n\tErr       error\n\tMsg       string\n\tDetails   map[string]interface{}\n\tRequestID string `json:\"-\"`\n}\n\n// ErrorResponse represents an error in JSON format.\ntype ErrorResponse struct {\n\tStatus  int    `json:\"status\"`\n\tMessage string `json:\"message\"`\n}\n\n// Unwrap implements the Unwrap interface and returns the original error.\nfunc (e *Error) Unwrap() error {\n\treturn e.Err\n}\n\n// Cause implements the errors.Causer interface and returns the original error.\nfunc (e *Error) Cause() error {\n\treturn e.Err\n}\n\n// Error implements the error interface and returns the error string.\nfunc (e *Error) Error() string {\n\treturn e.Err.Error()\n}\n\n// StatusCode implements the StatusCoder interface and returns the HTTP response\n// code.\nfunc (e *Error) StatusCode() int {\n\treturn e.Status\n}\n\n// Message returns a user friendly error, if one is set.\nfunc (e *Error) Message() string {\n\tif e.Msg != \"\" {\n\t\treturn e.Msg\n\t}\n\treturn e.Err.Error()\n}\n\n// Wrap returns an error annotating err with a stack trace at the point Wrap is\n// called, and the supplied message. If err is nil, Wrap returns nil.\nfunc Wrap(status int, e error, m string, args ...interface{}) error {\n\tif e == nil {\n\t\treturn nil\n\t}\n\t_, opts := splitOptionArgs(args)\n\tvar err *Error\n\tif errors.As(e, &err) {\n\t\terr.Err = errors.Wrap(err.Err, m)\n\t\te = err\n\t} else {\n\t\te = errors.Wrap(e, m)\n\t}\n\treturn StatusCodeError(status, e, opts...)\n}\n\n// Wrapf returns an error annotating err with a stack trace at the point Wrap is\n// called, and the supplied message. If err is nil, Wrap returns nil.\nfunc Wrapf(status int, e error, format string, args ...interface{}) error {\n\tif e == nil {\n\t\treturn nil\n\t}\n\tas, opts := splitOptionArgs(args)\n\tvar err *Error\n\tif errors.As(e, &err) {\n\t\terr.Err = errors.Wrapf(err.Err, format, args...)\n\t\te = err\n\t} else {\n\t\te = errors.Wrapf(e, format, as...)\n\t}\n\treturn StatusCodeError(status, e, opts...)\n}\n\n// MarshalJSON implements json.Marshaller interface for the Error struct.\nfunc (e *Error) MarshalJSON() ([]byte, error) {\n\tvar msg string\n\tif e.Msg != \"\" {\n\t\tmsg = e.Msg\n\t} else {\n\t\tmsg = http.StatusText(e.Status)\n\t}\n\treturn json.Marshal(&ErrorResponse{Status: e.Status, Message: msg})\n}\n\n// UnmarshalJSON implements json.Unmarshaler interface for the Error struct.\nfunc (e *Error) UnmarshalJSON(data []byte) error {\n\tvar er ErrorResponse\n\tif err := json.Unmarshal(data, &er); err != nil {\n\t\treturn err\n\t}\n\te.Status = er.Status\n\te.Err = fmt.Errorf(\"%s\", er.Message)\n\treturn nil\n}\n\n// Format implements the fmt.Formatter interface.\nfunc (e *Error) Format(f fmt.State, c rune) {\n\tvar fe fmt.Formatter\n\tif errors.As(e.Err, &fe) {\n\t\tfe.Format(f, c)\n\t\treturn\n\t}\n\tfmt.Fprint(f, e.Err.Error())\n}\n\n// Messenger is a friendly message interface that errors can implement.\ntype Messenger interface {\n\tMessage() string\n}\n\n// StatusCodeError selects the proper error based on the status code.\nfunc StatusCodeError(code int, e error, opts ...Option) error {\n\tswitch code {\n\tcase http.StatusBadRequest:\n\t\topts = append(opts, withDefaultMessage(BadRequestDefaultMsg))\n\t\treturn NewErr(http.StatusBadRequest, e, opts...)\n\tcase http.StatusUnauthorized:\n\t\treturn UnauthorizedErr(e, opts...)\n\tcase http.StatusForbidden:\n\t\topts = append(opts, withDefaultMessage(ForbiddenDefaultMsg))\n\t\treturn NewErr(http.StatusForbidden, e, opts...)\n\tcase http.StatusInternalServerError:\n\t\treturn InternalServerErr(e, opts...)\n\tcase http.StatusNotImplemented:\n\t\treturn NotImplementedErr(e, opts...)\n\tdefault:\n\t\treturn UnexpectedErr(code, e, opts...)\n\t}\n}\n\nconst (\n\tseeLogs    = \"Please see the certificate authority logs for more info.\"\n\tdefaultMsg = \"The requested could not be completed. \" + seeLogs\n\t// BadRequestDefaultMsg 400 default msg\n\tBadRequestDefaultMsg = \"The request could not be completed; malformed or missing data. \" + seeLogs\n\t// UnauthorizedDefaultMsg 401 default msg\n\tUnauthorizedDefaultMsg = \"The request lacked necessary authorization to be completed. \" + seeLogs\n\t// ForbiddenDefaultMsg 403 default msg\n\tForbiddenDefaultMsg = \"The request was forbidden by the certificate authority. \" + seeLogs\n\t// NotFoundDefaultMsg 404 default msg\n\tNotFoundDefaultMsg = \"The requested resource could not be found. \" + seeLogs\n\t// InternalServerErrorDefaultMsg 500 default msg\n\tInternalServerErrorDefaultMsg = \"The certificate authority encountered an Internal Server Error. \" + seeLogs\n\t// NotImplementedDefaultMsg 501 default msg\n\tNotImplementedDefaultMsg = \"The requested method is not implemented by the certificate authority. \" + seeLogs\n)\n\nfunc defaultMessage(status int) string {\n\tswitch status {\n\tcase http.StatusBadRequest:\n\t\treturn BadRequestDefaultMsg\n\tcase http.StatusUnauthorized:\n\t\treturn UnauthorizedDefaultMsg\n\tcase http.StatusForbidden:\n\t\treturn ForbiddenDefaultMsg\n\tcase http.StatusNotFound:\n\t\treturn NotFoundDefaultMsg\n\tcase http.StatusInternalServerError:\n\t\treturn InternalServerErrorDefaultMsg\n\tcase http.StatusNotImplemented:\n\t\treturn NotImplementedDefaultMsg\n\tdefault:\n\t\treturn defaultMsg\n\t}\n}\n\nconst (\n\t// BadRequestPrefix is the prefix added to the bad request messages that are\n\t// directly sent to the cli.\n\tBadRequestPrefix = \"The request could not be completed: \"\n\n\t// ForbiddenPrefix is the prefix added to the forbidden messates that are\n\t// sent to the cli.\n\tForbiddenPrefix = \"The request was forbidden by the certificate authority: \"\n)\n\nfunc formatMessage(status int, msg string) string {\n\tswitch status {\n\tcase http.StatusBadRequest:\n\t\treturn BadRequestPrefix + msg + \".\"\n\tcase http.StatusForbidden:\n\t\treturn ForbiddenPrefix + msg + \".\"\n\tdefault:\n\t\treturn msg\n\t}\n}\n\n// splitOptionArgs splits the variadic length args into string formatting args\n// and Option(s) to apply to an Error.\nfunc splitOptionArgs(args []interface{}) ([]interface{}, []Option) {\n\tindexOptionStart := -1\n\tfor i, a := range args {\n\t\tif _, ok := a.(Option); ok {\n\t\t\tindexOptionStart = i\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif indexOptionStart < 0 {\n\t\treturn args, []Option{}\n\t}\n\topts := []Option{}\n\t// Ignore any non-Option args that come after the first Option.\n\tfor _, o := range args[indexOptionStart:] {\n\t\tif opt, ok := o.(Option); ok {\n\t\t\topts = append(opts, opt)\n\t\t}\n\t}\n\treturn args[:indexOptionStart], opts\n}\n\n// New creates a new http error with the given status and message.\nfunc New(status int, format string, args ...interface{}) error {\n\tmsg := fmt.Sprintf(format, args...)\n\treturn &Error{\n\t\tStatus: status,\n\t\tMsg:    formatMessage(status, msg),\n\t\tErr:    errors.New(msg),\n\t}\n}\n\n// NewError creates a new http error with the given error and message.\nfunc NewError(status int, err error, format string, args ...interface{}) error {\n\tvar e *Error\n\tif errors.As(err, &e) {\n\t\treturn err\n\t}\n\tmsg := fmt.Sprintf(format, args...)\n\tvar ste log.StackTracedError\n\tif !errors.As(err, &ste) {\n\t\terr = errors.Wrap(err, msg)\n\t}\n\treturn &Error{\n\t\tStatus: status,\n\t\tMsg:    formatMessage(status, msg),\n\t\tErr:    err,\n\t}\n}\n\n// NewErr returns a new Error. If the given error implements the StatusCoder\n// interface we will ignore the given status.\nfunc NewErr(status int, err error, opts ...Option) error {\n\tvar e *Error\n\tif !errors.As(err, &e) {\n\t\tvar ste render.StatusCodedError\n\t\tif errors.As(err, &ste) {\n\t\t\te = &Error{Status: ste.StatusCode(), Err: err}\n\t\t} else {\n\t\t\te = &Error{Status: status, Err: err}\n\t\t}\n\t}\n\tfor _, o := range opts {\n\t\to(e)\n\t}\n\treturn e\n}\n\n// Errorf creates a new error using the given format and status code.\nfunc Errorf(code int, format string, args ...interface{}) error {\n\tas, opts := splitOptionArgs(args)\n\topts = append(opts, withDefaultMessage(defaultMessage(code)))\n\te := &Error{Status: code, Err: fmt.Errorf(format, as...)}\n\tfor _, o := range opts {\n\t\to(e)\n\t}\n\treturn e\n}\n\n// ApplyOptions applies the given options to the error if is the type *Error.\n// TODO(mariano): try to get rid of this.\nfunc ApplyOptions(err error, opts ...interface{}) error {\n\tvar e *Error\n\tif errors.As(err, &e) {\n\t\t_, o := splitOptionArgs(opts)\n\t\tfor _, fn := range o {\n\t\t\tfn(e)\n\t\t}\n\t}\n\treturn err\n}\n\n// InternalServer creates a 500 error with the given format and arguments.\nfunc InternalServer(format string, args ...interface{}) error {\n\targs = append(args, withDefaultMessage(InternalServerErrorDefaultMsg))\n\treturn Errorf(http.StatusInternalServerError, format, args...)\n}\n\n// InternalServerErr returns a 500 error with the given error.\nfunc InternalServerErr(err error, opts ...Option) error {\n\topts = append(opts, withDefaultMessage(InternalServerErrorDefaultMsg))\n\treturn NewErr(http.StatusInternalServerError, err, opts...)\n}\n\n// NotImplemented creates a 501 error with the given format and arguments.\nfunc NotImplemented(format string, args ...interface{}) error {\n\targs = append(args, withDefaultMessage(NotImplementedDefaultMsg))\n\treturn Errorf(http.StatusNotImplemented, format, args...)\n}\n\n// NotImplementedErr returns a 501 error with the given error.\nfunc NotImplementedErr(err error, opts ...Option) error {\n\topts = append(opts, withDefaultMessage(NotImplementedDefaultMsg))\n\treturn NewErr(http.StatusNotImplemented, err, opts...)\n}\n\n// BadRequest creates a 400 error with the given format and arguments.\nfunc BadRequest(format string, args ...interface{}) error {\n\treturn New(http.StatusBadRequest, format, args...)\n}\n\n// BadRequestErr returns an 400 error with the given error.\nfunc BadRequestErr(err error, format string, args ...interface{}) error {\n\treturn NewError(http.StatusBadRequest, err, format, args...)\n}\n\n// Unauthorized creates a 401 error with the given format and arguments.\nfunc Unauthorized(format string, args ...interface{}) error {\n\targs = append(args, withDefaultMessage(UnauthorizedDefaultMsg))\n\treturn Errorf(http.StatusUnauthorized, format, args...)\n}\n\n// UnauthorizedErr returns an 401 error with the given error.\nfunc UnauthorizedErr(err error, opts ...Option) error {\n\topts = append(opts, withDefaultMessage(UnauthorizedDefaultMsg))\n\treturn NewErr(http.StatusUnauthorized, err, opts...)\n}\n\n// Forbidden creates a 403 error with the given format and arguments.\nfunc Forbidden(format string, args ...interface{}) error {\n\treturn New(http.StatusForbidden, format, args...)\n}\n\n// ForbiddenErr returns an 403 error with the given error.\nfunc ForbiddenErr(err error, format string, args ...interface{}) error {\n\treturn NewError(http.StatusForbidden, err, format, args...)\n}\n\n// NotFound creates a 404 error with the given format and arguments.\nfunc NotFound(format string, args ...interface{}) error {\n\targs = append(args, withDefaultMessage(NotFoundDefaultMsg))\n\treturn Errorf(http.StatusNotFound, format, args...)\n}\n\n// NotFoundErr returns an 404 error with the given error.\nfunc NotFoundErr(err error, opts ...Option) error {\n\topts = append(opts, withDefaultMessage(NotFoundDefaultMsg))\n\treturn NewErr(http.StatusNotFound, err, opts...)\n}\n\n// UnexpectedErr will be used when the certificate authority makes an outgoing\n// request and receives an unhandled status code.\nfunc UnexpectedErr(code int, err error, opts ...Option) error {\n\topts = append(opts, withFormattedMessage(\"The certificate authority received an unexpected HTTP status code - '%d'. \"+seeLogs, code))\n\treturn NewErr(code, err, opts...)\n}\n"
  },
  {
    "path": "errs/errors_test.go",
    "content": "package errs\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestError_MarshalJSON(t *testing.T) {\n\ttype fields struct {\n\t\tStatus int\n\t\tErr    error\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tfields  fields\n\t\twant    []byte\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", fields{400, fmt.Errorf(\"bad request\")}, []byte(`{\"status\":400,\"message\":\"Bad Request\"}`), false},\n\t\t{\"ok no error\", fields{500, nil}, []byte(`{\"status\":500,\"message\":\"Internal Server Error\"}`), false},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\te := &Error{\n\t\t\t\tStatus: tt.fields.Status,\n\t\t\t\tErr:    tt.fields.Err,\n\t\t\t}\n\t\t\tgot, err := e.MarshalJSON()\n\t\t\tif tt.wantErr {\n\t\t\t\tassert.Error(t, err)\n\t\t\t\tassert.Empty(t, got)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, tt.want, got)\n\t\t})\n\t}\n}\n\nfunc TestError_UnmarshalJSON(t *testing.T) {\n\ttype args struct {\n\t\tdata []byte\n\t}\n\ttests := []struct {\n\t\tname     string\n\t\targs     args\n\t\texpected *Error\n\t\twantErr  bool\n\t}{\n\t\t{\"ok\", args{[]byte(`{\"status\":400,\"message\":\"bad request\"}`)}, &Error{Status: 400, Err: fmt.Errorf(\"bad request\")}, false},\n\t\t{\"fail\", args{[]byte(`{\"status\":\"400\",\"message\":\"bad request\"}`)}, &Error{}, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\te := new(Error)\n\t\t\terr := e.UnmarshalJSON(tt.args.data)\n\t\t\tif tt.wantErr {\n\t\t\t\tassert.Error(t, err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, tt.expected, e)\n\t\t})\n\t}\n}\n\nfunc TestError_Unwrap(t *testing.T) {\n\terr := errors.New(\"wrapped error\")\n\ttests := []struct {\n\t\tname  string\n\t\terror error\n\t\twant  string\n\t}{\n\t\t{\"ok New\", New(http.StatusBadRequest, \"some error\"), \"some error\"},\n\t\t{\"ok New v-wrap\", New(http.StatusBadRequest, \"some error: %v\", err), \"some error: wrapped error\"},\n\t\t{\"ok NewError\", NewError(http.StatusBadRequest, err, \"some error\"), \"some error: wrapped error\"},\n\t\t{\"ok NewErr\", NewErr(http.StatusBadRequest, err), \"wrapped error\"},\n\t\t{\"ok NewErr wit message\", NewErr(http.StatusBadRequest, err, WithMessage(\"some message\")), \"wrapped error\"},\n\t\t{\"ok Errorf\", Errorf(http.StatusBadRequest, \"some error: %w\", err), \"some error: wrapped error\"},\n\t\t{\"ok Errorf v-wrap\", Errorf(http.StatusBadRequest, \"some error: %v\", err), \"some error: wrapped error\"},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot := errors.Unwrap(tt.error)\n\t\t\tassert.EqualError(t, got, tt.want)\n\t\t})\n\t}\n}\n\ntype customError struct {\n\tMessage string\n}\n\nfunc (e *customError) Error() string {\n\treturn e.Message\n}\n\nfunc TestError_Unwrap_As(t *testing.T) {\n\terr := &customError{Message: \"wrapped error\"}\n\n\ttests := []struct {\n\t\tname    string\n\t\terror   error\n\t\twant    bool\n\t\twantErr *customError\n\t}{\n\t\t{\"ok NewError\", NewError(http.StatusBadRequest, err, \"some error\"), true, err},\n\t\t{\"ok NewErr\", NewErr(http.StatusBadRequest, err), true, err},\n\t\t{\"ok NewErr wit message\", NewErr(http.StatusBadRequest, err, WithMessage(\"some message\")), true, err},\n\t\t{\"ok Errorf\", Errorf(http.StatusBadRequest, \"some error: %w\", err), true, err},\n\t\t{\"fail New\", New(http.StatusBadRequest, \"some error\"), false, nil},\n\t\t{\"fail New v-wrap\", New(http.StatusBadRequest, \"some error: %v\", err), false, nil},\n\t\t{\"fail Errorf\", Errorf(http.StatusBadRequest, \"some error\"), false, nil},\n\t\t{\"fail Errorf v-wrap\", Errorf(http.StatusBadRequest, \"some error: %v\", err), false, nil},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tvar cerr *customError\n\t\t\tassert.Equal(t, tt.want, errors.As(tt.error, &cerr))\n\t\t\tassert.Equal(t, tt.wantErr, cerr)\n\t\t})\n\t}\n}\n\nfunc TestErrorf(t *testing.T) {\n\ttests := []struct {\n\t\tname   string\n\t\tcode   int\n\t\tformat string\n\t\targs   []any\n\t\twant   error\n\t}{\n\t\t{\"bad request\", 400, \"test error string\", nil, &Error{\n\t\t\tStatus: 400,\n\t\t\tErr:    errors.New(\"test error string\"),\n\t\t\tMsg:    BadRequestDefaultMsg,\n\t\t}},\n\t\t{\"unauthorized\", 401, \"test error string\", nil, &Error{\n\t\t\tStatus: 401,\n\t\t\tErr:    errors.New(\"test error string\"),\n\t\t\tMsg:    UnauthorizedDefaultMsg,\n\t\t}},\n\t\t{\"forbidden\", 403, \"test error string\", nil, &Error{\n\t\t\tStatus: 403,\n\t\t\tErr:    errors.New(\"test error string\"),\n\t\t\tMsg:    ForbiddenDefaultMsg,\n\t\t}},\n\t\t{\"not found\", 404, \"test error string\", nil, &Error{\n\t\t\tStatus: 404,\n\t\t\tErr:    errors.New(\"test error string\"),\n\t\t\tMsg:    NotFoundDefaultMsg,\n\t\t}},\n\t\t{\"internal server error\", 500, \"test error string\", nil, &Error{\n\t\t\tStatus: 500,\n\t\t\tErr:    errors.New(\"test error string\"),\n\t\t\tMsg:    InternalServerErrorDefaultMsg,\n\t\t}},\n\t\t{\"not implemented\", 501, \"test error string\", nil, &Error{\n\t\t\tStatus: 501,\n\t\t\tErr:    errors.New(\"test error string\"),\n\t\t\tMsg:    NotImplementedDefaultMsg,\n\t\t}},\n\t\t{\"other\", 502, \"test error string\", nil, &Error{\n\t\t\tStatus: 502,\n\t\t\tErr:    errors.New(\"test error string\"),\n\t\t\tMsg:    defaultMsg,\n\t\t}},\n\t\t{\"formatted args\", 401, \"test error string: %s\", []any{\"some reason\"}, &Error{\n\t\t\tStatus: 401,\n\t\t\tErr:    errors.New(\"test error string: some reason\"),\n\t\t\tMsg:    UnauthorizedDefaultMsg,\n\t\t}},\n\t\t{\"WithMessage\", 403, \"test error string\", []any{WithMessage(\"%s failed\", \"something\")}, &Error{\n\t\t\tStatus: 403,\n\t\t\tErr:    errors.New(\"test error string\"),\n\t\t\tMsg:    \"something failed\",\n\t\t}},\n\t\t{\"WithErrorMessage\", 404, \"test error string\", []any{WithErrorMessage()}, &Error{\n\t\t\tStatus: 404,\n\t\t\tErr:    errors.New(\"test error string\"),\n\t\t\tMsg:    \"test error string\",\n\t\t}},\n\t\t{\"WithKeyValue\", 500, \"test error string\", []any{WithKeyVal(\"foo\", 1), WithKeyVal(\"bar\", \"zar\")}, &Error{\n\t\t\tStatus:  500,\n\t\t\tErr:     errors.New(\"test error string\"),\n\t\t\tMsg:     InternalServerErrorDefaultMsg,\n\t\t\tDetails: map[string]interface{}{\"foo\": 1, \"bar\": \"zar\"},\n\t\t}},\n\t\t{\"withDefaultMessage\", 501, \"test error string\", []any{withDefaultMessage(\"some message\")}, &Error{\n\t\t\tStatus: 501,\n\t\t\tErr:    errors.New(\"test error string\"),\n\t\t\tMsg:    \"some message\",\n\t\t}},\n\t\t{\"withFormattedMessage\", 502, \"test error string\", []any{withFormattedMessage(\"some message: %s\", \"the reason\")}, &Error{\n\t\t\tStatus: 502,\n\t\t\tErr:    errors.New(\"test error string\"),\n\t\t\tMsg:    \"some message: the reason\",\n\t\t}},\n\t\t{\"WithMessage and withDefaultMessage\", 500, \"test error string\", []any{WithMessage(\"the message\"), withDefaultMessage(\"some message\")}, &Error{\n\t\t\tStatus: 500,\n\t\t\tErr:    errors.New(\"test error string\"),\n\t\t\tMsg:    \"the message\",\n\t\t}},\n\t\t{\"WithErrorMessage and withFormattedMessage\", 500, \"test error string\", []any{WithErrorMessage(), withFormattedMessage(\"some message: %s\", \"the reason\")}, &Error{\n\t\t\tStatus: 500,\n\t\t\tErr:    errors.New(\"test error string\"),\n\t\t\tMsg:    \"test error string\",\n\t\t}},\n\t\t{\"formatted args and withMessage\", 500, \"test error string: %s, code %d\", []any{\"reason\", 1234, WithMessage(\"the message\")}, &Error{\n\t\t\tStatus: 500,\n\t\t\tErr:    errors.New(\"test error string: reason, code 1234\"),\n\t\t\tMsg:    \"the message\",\n\t\t}},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgotErr := Errorf(tt.code, tt.format, tt.args...)\n\t\t\tassert.Equal(t, tt.want, gotErr)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "examples/README.md",
    "content": "# Examples\n\n## Basic client usage\n\nThe basic-client example shows the functionality of the `ca.Client` type. The\nmethods work as an SDK for integrating services with the Certificate Authority (CA).\n\nIn [basic-client/client.go](/examples/basic-client/client.go) we see\nthe initialization of a client:\n\n```go\nclient, err := ca.NewClient(\"https://localhost:9000\", ca.WithRootSHA256(\"84a033e84196f73bd593fad7a63e509e57fd982f02084359c4e8c5c864efc27d\"))\n```\n\nThe previous code uses the CA address and the root certificate fingerprint.\nThe CA url will be present in the token, and the root fingerprint can be present\ntoo if the `--root root_ca.crt` option is used in the creation of the token. If\nthe token does contain the root fingerprint then it is simpler to use:\n\n```go\nclient, err := ca.Bootstrap(token)\n```\n\nAfter the initialization, there are examples of all the client methods. These\nmethods are a convenient way to use the CA API. The first method, `Health`,\nreturns the status of the CA server. If the server is up it will return\n`{\"status\":\"ok\"}`.\n\n```go\nhealth, err := client.Health()\n// Health is a struct created from the JSON response {\"status\": \"ok\"}\n```\n\nThe next method `Root` is used to get and verify the root certificate. We\npass a fingerprint and it downloads the root certificate from the CA and\nverifies that the fingerprint matches. This method uses an insecure HTTP\nclient as it might be used in the initialization of the client, but the response\nis considered secure because we have compared against the expected digest.\n\n```go\nroot, err := client.Root(\"84a033e84196f73bd593fad7a63e509e57fd982f02084359c4e8c5c864efc27d\")\n```\n\nNext we have the most important method; `Sign`. `Sign` will authorize and sign a\nCSR (Certificate Signing Request) that we provide. To authorize this request we use\na provisioning token issued by an authorized provisioner.\nYou can build your own certificate request and add it in\nthe `*api.SignRequest`, but our CA SDK contains a method that will generate a\nsecure random key and create a CSR - combining the key with the information\nprovided in the provisioning token.\n\n```go\n// Create a CSR from a token and return the SignRequest, the private key,  and an\n// error if something failed.\nreq, pk, err := ca.CreateSignRequest(token)\nif err != nil { ... }\n\n// Do the Sign request and return the signed certificate.\nsign, err := client.Sign(req)\nif err != nil { ... }\n```\n\nNext is the `Renew` method which is used to (you guessed it!) renew certificates.\nCertificate renewal relies on a mTLS connection with using an existing certificate.\nSo, as input we will need to pass a transport with the current certificate.\n\n```go\n// Get a cancelable context to stop the renewal goroutines and timers.\nctx, cancel := context.WithCancel(context.Background())\ndefer cancel()\n// Create a transport with the sign response and the private key.\ntr, err := client.Transport(ctx, sign, pk)\nif err != nil { ... }\n// Renew the certificate. The return type is equivalent to the Sign method.\nrenew, err := client.Renew(tr)\nif err != nil { ... }\n```\n\nThe following methods are for inpsecting Provisioners.\nOne method that returns a list of provisioners or an encrypted key of one provisioner.\n\n```go\n// Without options it will return the first 20 provisioners.\nprovisioners, err := client.Provisioners()\n// We can also set a limit up to 100.\nprovisioners, err := client.Provisioners(ca.WithProvisionerLimit(100))\n// With a pagination cursor.\nprovisioners, err := client.Provisioners(ca.WithProvisionerCursor(\"1f18c1ecffe54770e9107ce7b39b39735\"))\n// Or combine both.\nprovisioners, err := client.Provisioners(\n    ca.WithProvisionerCursor(\"1f18c1ecffe54770e9107ce7b39b39735\"),\n    ca.WithProvisionerLimit(100),\n)\n\n// Return the encrypted key of one of the returned provisioners. The key\n// returned is an encrypted JWE with the private key used to sign tokens.\nkey, err := client.ProvisionerKey(\"DmAtZt2EhmZr_iTJJ387fr4Md2NbzMXGdXQNW1UWPXk\")\n```\n\nThe following example shows how to create a\ntls.Config object that can be injected into servers and clients. By default, these\nmethods will spin off Go routines that auto-renew a certificate once (approximately)\ntwo thirds of the duration of the certificate has passed.\n\n```go\n// Get a cancelable context to stop the renewal goroutines and timers.\nctx, cancel := context.WithCancel(context.Background())\ndefer cancel()\n// Get tls.Config for a server.\ntlsConfig, err := client.GetServerTLSConfig(ctx, sign, pk)\n// Get tls.Config for a client.\ntlsConfig, err := client.GetClientTLSConfig(ctx, sign, pk)\n// Get an http.Transport for a client; this can be used as a http.RoundTripper\n// in an http.Client.\ntr, err := client.Transport(ctx, sign, pk)\n```\n\nTo run the example you need to start the certificate authority:\n\n```sh\ncertificates $ bin/step-ca examples/pki/config/ca.json\n2018/11/02 18:29:25 Serving HTTPS on :9000 ...\n```\n\nThen run client.go with a new token:\n```sh\ncertificates $ export STEPPATH=examples/pki\ncertificates $ export STEP_CA_URL=https://localhost:9000\ncertificates $ go run examples/basic-client/client.go $(step ca token client.smallstep.com)\n```\n\n## Bootstrap Client & Server\n\nIn this example we are going run the CA alongside a simple Server using TLS and\na simple client making TLS requests to the server.\n\nThe examples directory already contains a sample pki configuration with the\npassword `password` hardcoded, but you can create your own using `step ca init`.\n\nThese examples show the use of some other helper methods - simple ways to\ncreate TLS configured http.Server and http.Client objects. The methods are\n`BootstrapServer` and `BootstrapClient`.\n\n```go\n// Get a cancelable context to stop the renewal goroutines and timers.\nctx, cancel := context.WithCancel(context.Background())\ndefer cancel()\n// Create an http.Server that requires a client certificate\nsrv, err := ca.BootstrapServer(ctx, token, &http.Server{\n    Addr: \":8443\",\n    Handler: handler,\n})\nif err != nil {\n    panic(err)\n}\nsrv.ListenAndServeTLS(\"\", \"\")\n```\n\n```go\n// Get a cancelable context to stop the renewal goroutines and timers.\nctx, cancel := context.WithCancel(context.Background())\ndefer cancel()\n// Create an http.Server that does not require a client certificate\nsrv, err := ca.BootstrapServerWithMTLS(ctx, token, &http.Server{\n    Addr: \":8443\",\n    Handler: handler,\n}, ca.VerifyClientCertIfGiven())\nif err != nil {\n    panic(err)\n}\nsrv.ListenAndServeTLS(\"\", \"\")\n```\n\n```go\n// Get a cancelable context to stop the renewal goroutines and timers.\nctx, cancel := context.WithCancel(context.Background())\ndefer cancel()\n// Create an http.Client\nclient, err := ca.BootstrapClient(ctx, token)\nif err != nil {\n    panic(err)\n}\nresp, err := client.Get(\"https://localhost:8443\")\n```\n\nWe will demonstrate the mTLS configuration in a different example. In this\nexample we will configure the server to only verify client certificates\nif they are provided.\n\nTo being with let's start the Step CA:\n\n```sh\ncertificates $ bin/step-ca examples/pki/config/ca.json\n2018/11/02 18:29:25 Serving HTTPS on :9000 ...\n```\n\nNext we will start the bootstrap-tls-server and enter `password` prompted for the\nprovisioner password:\n\n```sh\ncertificates $ export STEPPATH=examples/pki\ncertificates $ export STEP_CA_URL=https://localhost:9000\ncertificates $ go run examples/bootstrap-tls-server/server.go $(step ca token localhost)\n✔ Key ID: DmAtZt2EhmZr_iTJJ387fr4Md2NbzMXGdXQNW1UWPXk (mariano@smallstep.com)\nPlease enter the password to decrypt the provisioner key:\nListening on :8443 ...\n```\n\nLet's try to cURL our new bootstrap server with the system certificates bundle\nas our root. It should fail.\n```\ncertificates $ curl https://localhost:8443\ncurl: (60) SSL certificate problem: unable to get local issuer certificate\nMore details here: https://curl.haxx.se/docs/sslcerts.html\n\ncurl performs SSL certificate verification by default, using a \"bundle\"\n of Certificate Authority (CA) public keys (CA certs). If the default\n bundle file isn't adequate, you can specify an alternate file\n using the --cacert option.\nIf this HTTPS server uses a certificate signed by a CA represented in\n the bundle, the certificate verification probably failed due to a\n problem with the certificate (it might be expired, or the name might\n not match the domain name in the URL).\nIf you'd like to turn off curl's verification of the certificate, use\n the -k (or --insecure) option.\nHTTPS-proxy has similar options --proxy-cacert and --proxy-insecure.\n```\n\nNow let's use the root certificate generated for the Step PKI. It should work.\n\n```sh\ncertificates $ curl --cacert examples/pki/secrets/root_ca.crt https://localhost:8443\nHello nobody at 2018-11-03 01:49:25.66912 +0000 UTC!!!\n```\n\nNotice that in the response we see `nobody`. This is because the server did not\ndetected a TLS client configuration.\n\nBut if we create a client with its own certificate (generated by the Step CA),\nwe should see the Common Name of the client certificate:\n\n```sh\ncertificates $ export STEPPATH=examples/pki\ncertificates $ export STEP_CA_URL=https://localhost:9000\ncertificates $ go run examples/bootstrap-client/client.go $(step ca token Mike)\n✔ Key ID: DmAtZt2EhmZr_iTJJ387fr4Md2NbzMXGdXQNW1UWPXk (mariano@smallstep.com)\nPlease enter the password to decrypt the provisioner key:\nServer responded: Hello Mike at 2018-11-03 01:52:52.678215 +0000 UTC!!!\nServer responded: Hello Mike at 2018-11-03 01:52:53.681563 +0000 UTC!!!\nServer responded: Hello Mike at 2018-11-03 01:52:54.682787 +0000 UTC!!!\n...\n```\n\n## Bootstrap mTLS Client & Server\n\nThis example demonstrates a stricter configuration of the bootstrap-server. Here\nwe configure the server to require mTLS (mutual TLS) with a valid client certificate.\n\nAs always, we begin by starting the CA:\n\n```sh\ncertificates $ bin/step-ca examples/pki/config/ca.json\n2018/11/02 18:29:25 Serving HTTPS on :9000 ...\n```\n\nNext we start the mTLS server and we enter `password` when prompted for the\nprovisioner password:\n\n```sh\ncertificates $ export STEPPATH=examples/pki\ncertificates $ export STEP_CA_URL=https://localhost:9000\ncertificates $ go run examples/bootstrap-mtls-server/server.go $(step ca token localhost)\n✔ Key ID: DmAtZt2EhmZr_iTJJ387fr4Md2NbzMXGdXQNW1UWPXk (mariano@smallstep.com)\nPlease enter the password to decrypt the provisioner key:\nListening on :8443 ...\n```\n\nNow that the server is configured to require mTLS cURL-ing should fail even\nif we use the correct root certificate bundle.\n\n```sh\ncertificates $ curl --cacert examples/pki/secrets/root_ca.crt https://localhost:8443\ncurl: (35) error:1401E412:SSL routines:CONNECT_CR_FINISHED:sslv3 alert bad certificate\n```\n\nHowever, if we use our client (which requests a certificate from the Step CA\nwhen it starts):\n\n```sh\ncertificates $ export STEPPATH=examples/pki\ncertificates $ export STEP_CA_URL=https://localhost:9000\ncertificates $ go run examples/bootstrap-client/client.go $(step ca token Mike)\n✔ Key ID: DmAtZt2EhmZr_iTJJ387fr4Md2NbzMXGdXQNW1UWPXk (mariano@smallstep.com)\nPlease enter the password to decrypt the provisioner key:\nServer responded: Hello Mike at 2018-11-07 21:54:00.140022 +0000 UTC!!!\nServer responded: Hello Mike at 2018-11-07 21:54:01.140827 +0000 UTC!!!\nServer responded: Hello Mike at 2018-11-07 21:54:02.141578 +0000 UTC!!!\n...\n```\n\n## Certificate rotation\n\nWe can use the bootstrap-server to demonstrate certificate rotation. We've\nadded a second provisioner, named `mike@smallstep.com`, to the CA configuration.\nThis provisioner is has a default certificate duration of 2 minutes.\nLet's run the server, and inspect the certificate. We can should be able to\nsee the certificate rotate once approximately 2/3rds of its lifespan has passed.\n\n```sh\ncertificates $ export STEPPATH=examples/pki\ncertificates $ export STEP_CA_URL=https://localhost:9000\ncertificates $ go run examples/bootstrap-server/server.go $(step ca token localhost)\n✔ Key ID: YYNxZ0rq0WsT2MlqLCWvgme3jszkmt99KjoGEJJwAKs (mike@smallstep.com)\nPlease enter the password to decrypt the provisioner key:\nListening on :8443 ...\n```\n\nIn this case, the certificate will rotate after 74-80 seconds.\nThe exact formula is `<duration>-<duration>/3-rand(<duration>/20)` (`duration=120`\nin our example).\n\nWe can use the following command to check the certificate expiration and to make\nsure the certificate changes after 74-80 seconds.\n\n```sh\ncertificates $ step certificate inspect --insecure https://localhost:8443\n```\n\n## NGINX with Step CA certificates\n\nThe example under the `docker` directory shows how to combine the Step CA\nwith NGINX to serve or proxy services using certificates created by the\nStep CA.\n\nThis example creates 3 different docker images:\n\n* nginx-test: docker image with NGINX and a script using inotify-tools to watch\n  for changes in the certificate to reload NGINX.\n* step-ca-test: docker image with the Step CA\n* step-renewer-test: docker image with the step cli tool - it creates the\n  certificate and sets a cron that renews the certificate (the cron\n  runs every minute for testing purposes).\n\nTo run this test you need to have the docker daemon running. With docker running\nswith to the `examples/docker directory` and run `make`:\n\n```\ncertificates $ cd examples/docker/\ndocker $ make\nGOOS=linux go build -o ca/step-ca github.com/smallstep/certificates/cmd/step-ca\nGOOS=linux go build -o renewer/step github.com/smallstep/cli/cmd/step\ndocker build -t nginx-test:latest nginx\n...\ndocker-compose up\nWARNING: The Docker Engine you're using is running in swarm mode.\n\nCompose does not use swarm mode to deploy services to multiple nodes in a swarm. All containers will be scheduled on the current node.\n\nTo deploy your application across the swarm, use `docker stack deploy`.\n\nCreating network \"docker_default\" with the default driver\nCreating docker_ca_1 ... done\nCreating docker_renewer_1 ... done\nCreating docker_nginx_1   ... done\nAttaching to docker_ca_1, docker_renewer_1, docker_nginx_1\nca_1       | 2018/11/12 19:39:16 Serving HTTPS on :443 ...\nnginx_1    | Setting up watches.\nnginx_1    | Watches established.\n...\n```\n\nMake will build the binaries for step and step-ca, create the images, create the\ncontainers and start them using docker composer.\n\nNGINX will be listening on your local machine on https://localhost:4443, but to\nmake sure the cert is right we need to add the following entry to `/etc/hosts`:\n\n```\n127.0.0.1   nginx\n```\n\nNow we can use cURL to verify:\n\n```sh\ndocker $ curl --cacert ca/pki/secrets/root_ca.crt https://nginx:4443/\n<!DOCTYPE html>\n<html>\n<head>\n<title>Welcome to nginx!</title>\n<style>\n    body {\n        width: 35em;\n        margin: 0 auto;\n        font-family: Tahoma, Verdana, Arial, sans-serif;\n    }\n</style>\n</head>\n<body>\n<h1>Welcome to nginx!</h1>\n<p>If you see this page, the nginx web server is successfully installed and\nworking. Further configuration is required.</p>\n\n<p>For online documentation and support please refer to\n<a href=\"http://nginx.org/\">nginx.org</a>.<br/>\nCommercial support is available at\n<a href=\"http://nginx.com/\">nginx.com</a>.</p>\n\n<p><em>Thank you for using nginx.</em></p>\n</body>\n</html>\n```\n\nWe can use `make inspect` to witness the certificate being rotated every minute.\n\n```sh\ndocker $ make inspect | head\nstep certificate inspect https://localhost:4443 --insecure\nCertificate:\n    Data:\n        Version: 3 (0x2)\n        Serial Number: 220353801925419530569669982276277771655 (0xa5c6993a7e110e6f009c83c79edc1d87)\n    Signature Algorithm: ECDSA-SHA256\n        Issuer: CN=Smallstep Intermediate CA\n        Validity\n            Not Before: Nov 10 02:13:00 2018 UTC\n            Not After : Nov 11 02:13:00 2018 UTC\ndocker $ make inspect | head\nstep certificate inspect https://localhost:4443 --insecure\nCertificate:\n    Data:\n        Version: 3 (0x2)\n        Serial Number: 207756171799719353821615361892302471392 (0x9c4c621c04d3e8be401ff0d14c5440e0)\n    Signature Algorithm: ECDSA-SHA256\n        Issuer: CN=Smallstep Intermediate CA\n        Validity\n            Not Before: Nov 10 02:14:00 2018 UTC\n            Not After : Nov 11 02:14:00 2018 UTC\n```\n\nFinally, to cleanup the containers and volumes created in this demo use `make down`:\n\n```sh\ndocker $ make down\ndocker-compose down\nStopping docker_nginx_1   ... done\nStopping docker_renewer_1 ... done\nStopping docker_ca_1      ... done\nRemoving docker_nginx_1   ... done\nRemoving docker_renewer_1 ... done\nRemoving docker_ca_1      ... done\nRemoving network docker_default\n```\n\n## Basic Federation\n\nThe [basic-federation example](basic-federation) showcases how to securely facilitate communication between relying parties of multiple autonomous certificate authorities. Federation is what's required when services are spread between multiple independent Kubernetes clusters, public clouds, and/or serverless cloud functions to enable service communication across boundaries.\n\nThis example uses a pre-generated PKI (public/private key material). Do not use pre-generated PKIs for dev, staging, or production purposes outside of this example.\n\n### Launch Online CAs\n\nBring up two online CAs; `Cloud CA` and `Kubernetes CA`.\n\n```bash\n$ step-ca ./pki/cloud/config/ca.federated.json\nPlease enter the password to decrypt intermediate_ca_key: password\n2019/01/22 13:38:52 Serving HTTPS on :1443 ...\n```\n\n```bash\n$ step-ca ./pki/kubernetes/config/ca.federated.json\nPlease enter the password to decrypt intermediate_ca_key: password\n2019/01/22 13:39:44 Serving HTTPS on :2443 ...\n```\n\nNotice the difference between the two configuration options below. `Cloud CA` will list `Kubernetes CA` in the `federatedRoots` section and vice versa for the federated options.\n\n```bash\n$ diff pki/cloud/config/ca.json pki/cloud/config/ca.federated.json\n3c3\n<    \"federatedRoots\": [],\n---\n>    \"federatedRoots\": [\"pki/cloud/certs/kubernetes_root_ca.crt\"],\n```\n\n### Bring up Demo Server\n\nThis demo server leverages step's [SDK](https://godoc.org/github.com/smallstep/certificates/ca) to obtain certs, automatically renew them, and fetch a bundle of trusted roots. When it starts up it will report what root certificates it will use to authenticate client certs.\n\n```bash\ngo run server/main.go $(step ca token \\\n  --ca-url https://localhost:1443 \\\n  --root ./pki/cloud/certs/root_ca.crt \\\n  127.0.0.1)\n✔ Key ID: EE1ZiqkMaxsUdpz8SCSkRBzwK9TWUoidQnMnJ8Eryn8 (sebastian@smallstep.com)\n✔ Please enter the password to decrypt the provisioner key: password\nServer is using federated root certificates\nAccepting certs anchored in CN=Smallstep Public Cloud Root CA\nAccepting certs anchored in CN=Smallstep Kubernetes Root CA\nListening on :8443 ...\n```\n\n### Run Demo Client\n\nSimilarly step's [SDK](https://godoc.org/github.com/smallstep/certificates/ca) provides a client option to mutually authenticate connections to servers. It automatically handles cert bootstrapping, renewal, and fetches a bundle of trusted roots. The demo client will send HTTP requests to the demo server periodically (every 5s).\n\n```bash\n$ go run client/main.go $(step ca token sdk_client \\\n  --ca-url https://localhost:2443 \\\n  --root ./pki/kubernetes/certs/root_ca.crt)\n✔ Key ID: S5gYgpeqcIAgc1Zr4myZXpgJ_Ao4ryS6F6wqg9o8RYo (sebastian@smallstep.com)\n✔ Please enter the password to decrypt the provisioner key: password\nServer responded: Hello sdk_client (cert issued by 'Smallstep Kubernetes Root CA') at 2019-01-23 00:51:38.576648 +0000 UTC\n```\n\n### Curl as Client\n\nWhile the demo client provides a convenient way to periodically send requests to the demo server curl in combination with a client cert from `Kubernetes CA` can be used to hit the server instead:\n\n```bash\n$ step ca certificate kube_client kube_client.crt kube_client.key \\\n  --ca-url https://localhost:2443 \\\n  --root pki/kubernetes/certs/root_ca.crt\n✔ Key ID: S5gYgpeqcIAgc1Zr4myZXpgJ_Ao4ryS6F6wqg9o8RYo (sebastian@smallstep.com)\n✔ Please enter the password to decrypt the provisioner key:\n✔ CA: https://localhost:2443/1.0/sign\n✔ Certificate: kube_client.crt\n✔ Private Key: kube_client.key\n```\n\nFederation relies on a bundle of multiple trusted roots which need to be fetched before passed into curl.\n\n```bash\n$ step ca federation --ca-url https://localhost:1443 \\\n  --root pki/cloud/certs/root_ca.crt \\\n  federated.pem\nThe federation certificate bundle has been saved in federated.pem.\n```\n\nPassing the cert (issued by `Kubernetes CA`) into curl using the appropriate command line flags:\n\n```bash\n$ curl -i --cacert federated.pem \\\n  --cert kube_client.crt \\\n  --key kube_client.key \\\n  https://127.0.0.1:8443\n\nHTTP/2 200\ncontent-type: text/plain; charset=utf-8\ncontent-length: 105\ndate: Mon, 28 Jan 2019 15:24:54 GMT\n\nHello kube_client (cert issued by 'Smallstep Kubernetes Root CA') at 2019-01-28 15:24:54.864373 +0000 UTC\n```\n\nSince the demo server is enrolled with the federated `Cloud CA` that trusts certs issued by the `Kubernetes CA` through federation the connection is successfully established.\n\n## Custom certificate validity periods using Custom Claims\n\nBring up the certificate authority with the example:\n\n```sh\ncertificates $ step-ca examples/pki/config/ca.json\n2019/03/11 13:37:03 Serving HTTPS on :9000 ...\n```\n\nThe example comes with multiple provisioner options, two of which have custom claims to expand the validity of certificates:\n\n```sh\n$ step ca provisioner list | jq '.[] | \"\\(.name): \\(.claims.defaultTLSCertDuration)\"'\n# null means step default of 24h for cert validity\n\"mariano@smallstep.com: null\"\n\"mike@smallstep.com: 2m0s\"\n\"decade: 87600h0m0s\"\n\"90days: 2160h0m0s\"\n```\n\nA closer look at a duration-bound provisioner, `90days` for instance, reveals the custom configuration for certificate validity.\n\n```sh\n$ step ca provisioner list | jq '.[3].claims'\n{\n  \"maxTLSCertDuration\": \"2160h0m0s\",\n  \"defaultTLSCertDuration\": \"2160h0m0s\"\n}\n```\n\nCertificates with different validity periods can be generated using the respective provisioners.\nThe durations are strings which are a sequence of decimal numbers, each with optional fraction and a unit suffix, such as \"300ms\" or \"2h45m\". Valid time units are \"ns\", \"us\" (or \"µs\"), \"ms\", \"s\", \"m\", \"h\".\n\nPlease see [Getting Started](https://github.com/smallstep/certificates/blob/master/docs/GETTING_STARTED.md) in the docs directory to learn what custom claims configuration options are available and how to use them.\n\n```sh\n$ step ca certificate decade decade.crt decade.key\n✔ Key ID: iu7VZxKUcquv1BCWuvEUOyRy4zYyCmgt61OpRW5VbRE (decade)\n✔ Please enter the password to decrypt the provisioner key: password\n✔ CA: https://localhost:9000/1.0/sign\n✔ Certificate: decade.crt\n✔ Private Key: decade.key\n$ step certificate inspect --format json decade.crt | jq .validity\n{\n  \"start\": \"2019-03-11T22:34:30Z\",\n  \"end\": \"2029-03-08T22:34:30Z\",\n  \"length\": 315360000\n}\n\n$ step ca certificate 90days 90days.crt 90days.key\n✔ Key ID: 2LgjIvfirblnFMC6FjUr8jYkO8nOqz4rKoarCc8kiGU (90days)\n✔ Please enter the password to decrypt the provisioner key: password\n✔ CA: https://localhost:9000/1.0/sign\n✔ Certificate: 90days.crt\n✔ Private Key: 90days.key\n$ step certificate inspect --format json 90days.crt | jq .validity\n{\n  \"start\": \"2019-03-11T22:35:39Z\",\n  \"end\": \"2019-06-09T22:35:39Z\",\n  \"length\": 7776000\n}\n```\n\n## Configuration Management Tools\n\nConfiguration management tools such as Puppet, Chef, Ansible, Salt, etc. make\nautomation and deployment a whole lot easier and more manageable. Step CLI and\nCA are built with automation in mind and are easy to configure using your\nfavorite tools\n\n# Puppet\n\nThe following are snippets and files that users can add to their puppet\nmanifests to easily instrument services with TLS.\n\n** [step.pp](./puppet/step.pp) ** - Install `step` from source and configure the `step` user, group,\nand home directory for use by the Step CLI and CA.\n** [step_ca.pp](./puppet/step_ca.pp) ** - Install `step-ca` from source. Configure\ncertificates and secrets and run the Step CA.\n** [tls_server.pp](./puppet/tls_server.pp) ** - This is your service, instrumented\nwith the Step CA SDK to request, receive, and renew TLS certificates. See\n[the bootstrap-tls-server](./bootstrap-tls-server/server.go) for a\nsimple integration example.\n\n**Note:** This is a significantly oversimplified example that will not work standalone.\nA complete Puppet configuration should use a service manager (like\n[systemctl](https://www.digitalocean.com/community/tutorials/how-to-use-systemctl-to-manage-systemd-services-and-units))\nand a secret store (like [Hiera](https://puppet.com/docs/puppet/6.0/hiera_intro.html)).\nIf you are interested in seeing a more complete example please let us know and we'll\nmake one available.\n\n"
  },
  {
    "path": "examples/ansible/smallstep-certs/defaults/main.yml",
    "content": "\n\n\n# Root cert for each will be saved in /etc/ssl/smallstep/ca/{{ ca_name }}/certs/root_ca.crt\nsmallstep_root_certs: []\n#  -\n#    ca_name: your_ca\n#    ca_url: \"https://certs.your_ca.ca.smallstep.com\"\n#    ca_fingerprint: \"56092...2200\"\n\n# Each leaf cert will be saved in /etc/ssl/smallstep/leaf/{{ cert_subject }}/{{ cert_subject }}.crt|key\nsmallstep_leaf_certs: []\n#  -\n#    ca_name: your_ca\n#    cert_subject: \"{{ inventory_hostname }}\"\n#    provisioner_name: \"admin\"\n#    provisioner_password: \"{{ smallstep_ssh_provisioner_password }}\"\n\n"
  },
  {
    "path": "examples/ansible/smallstep-certs/tasks/main.yml",
    "content": "\n- name: \"Ensure provisioners directories exist\"\n  file:\n    path: \"/etc/ssl/smallstep/provisioners/{{ item.context }}/{{ item.provisioner_name }}\"\n    state: directory\n    mode: 0600\n    owner: root\n    group: root\n  with_items: \"{{ smallstep_leaf_certs }}\"\n  no_log: true\n\n- name: \"Ensure provisioner passwords are up to date\"\n  copy:\n    dest: \"/etc/ssl/smallstep/provisioners/{{ item.context }}/{{ item.provisioner_name }}/provisioner-pass.txt\"\n    content: \"{{ item.provisioner_password }}\"\n    mode: 0700\n    owner: root\n    group: root\n  with_items: \"{{ smallstep_leaf_certs }}\"\n  no_log: true\n\n- name: \"Get root certs for CAs\"\n  command:\n    cmd: \"step ca bootstrap --context {{ item.context }} --ca-url {{ item.ca_url }} --fingerprint {{ item.ca_fingerprint }}\"\n  with_items: \"{{ smallstep_root_certs }}\"\n  no_log: true\n\n- name: \"Get leaf certs\"\n  command:\n    cmd: \"step ca certificate --context {{ item.context }} {{ item.cert_subject }} {{ item.cert_path }} {{ item.key_path }} --force --console --provisioner {{ item.provisioner_name }} --provisioner-password-file /etc/ssl/smallstep/provisioners/{{ item.context }}/{{ item.provisioner_name }}/provisioner-pass.txt\"\n  with_items: \"{{ smallstep_leaf_certs }}\"\n  no_log: true\n\n- name: Ensure cron to renew leaf certs is up to date\n  cron:\n    user: \"root\"\n    name: \"renew leaf cert {{ item.cert_subject }}\"\n    cron_file: smallstep\n    job: \"step ca renew --context {{ item.context }} {{ item.cert_path }} {{ item.key_path }} --expires-in 6h --force >> /var/log/smallstep-{{ item.cert_subject }}.log 2>&1\"\n    state: present\n    minute: \"*/30\"\n  with_items: \"{{ smallstep_leaf_certs }}\"\n  when: \"{{ item.cron_renew }}\"\n  no_log: true\n"
  },
  {
    "path": "examples/ansible/smallstep-install/defaults/main.yml",
    "content": "smallstep_install_step_version: 0.15.3\nsmallstep_install_step_ssh_version: 0.19.1-1\n"
  },
  {
    "path": "examples/ansible/smallstep-install/tasks/main.yml",
    "content": "\n# These steps automate the installation guide here:\n#  https://smallstep.com/docs/sso-ssh/hosts/\n\n- name: Download step binary\n  get_url:\n    url: \"https://files.smallstep.com/step-linux-{{ smallstep_install_step_version }}\"\n    dest: \"/usr/local/bin/step-{{ smallstep_install_step_version }}\"\n    mode: '0755'\n\n- name: Link binaries to correct version\n  file:\n    src: \"/usr/local/bin/step-{{ smallstep_install_step_version }}\"\n    dest: \"{{ item }}\"\n    state: link\n  with_items:\n    - /usr/bin/step\n    - /usr/local/bin/step\n\n- name: Link /usr/local/bin/step to correct binary version\n  file:\n    src: \"/usr/local/bin/step-{{ smallstep_install_step_version }}\"\n    dest: /usr/local/bin/step\n    state: link\n\n- name: Ensure step-ssh is installed\n  apt:\n    deb: \"https://files.smallstep.com/step-ssh_{{ smallstep_install_step_ssh_version }}_amd64.deb\"\n    state: present\n"
  },
  {
    "path": "examples/ansible/smallstep-ssh/defaults/main.yml",
    "content": "# If this host is behind a bastion this variable should contain the hostname of the bastion\nsmallstep_ssh_host_behind_bastion_name: \"\"\nsmallstep_ssh_host_is_bastion: false\nsmallstep_ssh_ca_url: \"https://ssh.mycompany.ca.smallstep.com\"\nsmallstep_ssh_ca_fingerprint: \"XXXXXXXXXXXXXXX\"\n\n# Whether or not to reinitialize the host even if it's already been installed\nsmallstep_ssh_force_reinit: true\n"
  },
  {
    "path": "examples/ansible/smallstep-ssh/tasks/main.yml",
    "content": "\n# These steps automate the installation guide here:\n#  https://smallstep.com/docs/sso-ssh/hosts/\n\n# TODO: Figure out how to make this idempotent instead of reinstalling on each run\n\n- name: Bootstrap node to connect to CA\n  command: \"step ca bootstrap --context ssh --ca-url {{ smallstep_ssh_ca_url }} --fingerprint {{ smallstep_ssh_ca_fingerprint }} --force\"\n#  when: smallstep_ssh_installed.changed or smallstep_ssh_force_reinit\n\n- name: Get a host SSH certificate\n  command: \"step ssh certificate --context ssh {{ inventory_hostname }} /etc/ssh/ssh_host_ecdsa_key.pub --host --sign --provisioner=\\\"Service Account\\\" --token=\\\"{{ smallstep_ssh_enrollment_token }}\\\" --force\"\n#  when: smallstep_ssh_installed.changed or smallstep_ssh_force_reinit\n\n- name: Configure SSHD (will be overwriten by the sshd template in Ansible later)\n  command: \"step ssh config --context ssh --host --set Certificate=ssh_host_ecdsa_key-cert.pub --set Key=ssh_host_ecdsa_key\"\n#  when: smallstep_ssh_installed.changed or smallstep_ssh_force_reinit\n\n- name: Activate SmallStep PAM/NSS modules and nohup sshd\n  command: \"step-ssh activate {{ inventory_hostname }}\"\n#  when: smallstep_ssh_installed.changed or smallstep_ssh_force_reinit\n\n- name: Generate host tags list\n  set_fact:\n    smallstep_ssh_host_tags_string: \"{{ smallstep_ssh_host_tags | to_json | regex_replace('\\\\:\\\\ ','=') | regex_replace('\\\\{\\\\\\\"|,\\\\ \\\\\\\"', ' --tag \\\"') | regex_replace('[\\\\[\\\\]{}]') }}\"\n\n- name: Generate command to register\n  set_fact:\n    smallstep_ssh_register_string: |\n          step-ssh-ctl register\n                --hostname {{ inventory_hostname }}\n                {% if not smallstep_ssh_host_is_bastion %}--bastion '{{ smallstep_ssh_host_behind_bastion_name|default(\"\") }}'{% endif %}\n                {% if smallstep_ssh_host_is_bastion %}--is-bastion{% endif %}\n                {{ smallstep_ssh_host_tags_string }}\n\n- debug: var=smallstep_ssh_register_string\n\n- name: Register host with smallstep\n  command: \"{{ smallstep_ssh_register_string }}\"\n#  when: smallstep_ssh_installed.changed or smallstep_ssh_force_reinit\n\n"
  },
  {
    "path": "examples/basic-client/client.go",
    "content": "//nolint:govet // example code; allow unused variables\npackage main\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/http\"\n\t\"os\"\n\t\"time\"\n\n\t\"github.com/smallstep/certificates/ca\"\n)\n\nfunc printResponse(name string, v interface{}) {\n\tb, err := json.MarshalIndent(v, \"\", \"  \")\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tfmt.Printf(\"%s response:\\n%s\\n\\n\", name, b)\n}\n\nfunc main() {\n\tif len(os.Args) != 2 {\n\t\tfmt.Fprintf(os.Stderr, \"Usage: %s <token>\\n\", os.Args[0])\n\t\tos.Exit(1)\n\t}\n\n\ttoken := os.Args[1]\n\n\t// To create the client using ca.NewClient we need:\n\t// * The CA address \"https://localhost:9000\"\n\t// * The root certificate fingerprint\n\t// 84a033e84196f73bd593fad7a63e509e57fd982f02084359c4e8c5c864efc27d to get\n\t// the root fingerprint we can use `step certificate fingerprint root_ca.crt`\n\tclient, err := ca.NewClient(\"https://localhost:9000\", ca.WithRootSHA256(\"84a033e84196f73bd593fad7a63e509e57fd982f02084359c4e8c5c864efc27d\"))\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// Other ways to initialize the client would be:\n\t// * With the Bootstrap functionality (recommended):\n\t//   client, err := ca.Bootstrap(token)\n\t// * Using the root certificate instead of the fingerprint:\n\t//   client, err := ca.NewClient(\"https://localhost:9000\", ca.WithRootFile(\"../pki/secrets/root_ca.crt\"))\n\n\t// Get the health of the CA\n\thealth, err := client.Health()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tprintResponse(\"Health\", health)\n\n\t// Get and verify a root CA\n\troot, err := client.Root(\"84a033e84196f73bd593fad7a63e509e57fd982f02084359c4e8c5c864efc27d\")\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tprintResponse(\"Root\", root)\n\n\t// We can use ca.CreateSignRequest to generate a new sign request with a\n\t// randomly generated key.\n\treq, pk, err := ca.CreateSignRequest(token)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tsign, err := client.Sign(req)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tprintResponse(\"Sign\", sign)\n\n\t// Renew a certificate with a transport that contains the previous\n\t// certificate. We should created a context that allows us to finish the\n\t// renewal goroutine.∑\n\tctx, cancel := context.WithCancel(context.Background())\n\tdefer cancel() // Finish the renewal goroutine\n\ttr, err := client.Transport(ctx, sign, pk)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\trenew, err := client.Renew(tr)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tprintResponse(\"Renew\", renew)\n\n\t// Get tls.Config for a server\n\tctxServer, cancelServer := context.WithCancel(context.Background())\n\tdefer cancelServer()\n\ttlsConfig, err := client.GetServerTLSConfig(ctxServer, sign, pk)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\t// An http server will use the tls.Config like:\n\t_ = &http.Server{\n\t\tAddr: \":443\",\n\t\tHandler: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {\n\t\t\tw.Write([]byte(\"Hello world\"))\n\t\t}),\n\t\tTLSConfig:         tlsConfig,\n\t\tReadHeaderTimeout: 30 * time.Second,\n\t}\n\n\t// Get tls.Config for a client\n\tctxClient, cancelClient := context.WithCancel(context.Background())\n\tdefer cancelClient()\n\ttlsConfig, err = client.GetClientTLSConfig(ctxClient, sign, pk)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\t// An http.Client will need to create a transport first\n\t_ = &http.Client{\n\t\tTransport: &http.Transport{\n\t\t\tTLSClientConfig: tlsConfig,\n\t\t\t// Options set in http.DefaultTransport\n\t\t\tProxy: http.ProxyFromEnvironment,\n\t\t\tDialContext: (&net.Dialer{\n\t\t\t\tTimeout:   30 * time.Second,\n\t\t\t\tDualStack: true,\n\t\t\t}).DialContext,\n\t\t\tMaxIdleConns:          100,\n\t\t\tIdleConnTimeout:       90 * time.Second,\n\t\t\tTLSHandshakeTimeout:   10 * time.Second,\n\t\t\tExpectContinueTimeout: 1 * time.Second,\n\t\t},\n\t}\n\n\t// But we can just use client.Transport to get the default configuration\n\tctxTransport, cancelTransport := context.WithCancel(context.Background())\n\tdefer cancelTransport()\n\ttr, err = client.Transport(ctxTransport, sign, pk)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\t// And http.Client will use the transport like\n\t_ = &http.Client{\n\t\tTransport: tr,\n\t}\n\n\t// Get provisioners and provisioner keys. In this example we add two\n\t// optional arguments with the initial cursor and a limit.\n\t//\n\t// A server or a client should not need this functionality, they are used to\n\t// sign (private key) and verify (public key) tokens. The step cli can be\n\t// used for this purpose.\n\tprovisioners, err := client.Provisioners(ca.WithProvisionerCursor(\"\"), ca.WithProvisionerLimit(100))\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tprintResponse(\"Provisioners\", provisioners)\n\t// Get encrypted key\n\tkey, err := client.ProvisionerKey(\"DmAtZt2EhmZr_iTJJ387fr4Md2NbzMXGdXQNW1UWPXk\")\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tprintResponse(\"Provisioner Key\", key)\n}\n"
  },
  {
    "path": "examples/basic-federation/client/main.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"time\"\n\n\t\"github.com/smallstep/certificates/ca\"\n)\n\nfunc main() {\n\tif len(os.Args) != 2 {\n\t\tfmt.Fprintf(os.Stderr, \"Usage: %s <token>\\n\", os.Args[0])\n\t\tos.Exit(1)\n\t}\n\n\ttoken := os.Args[1]\n\n\t// make sure to cancel the renew goroutine\n\tctx, cancel := context.WithCancel(context.Background())\n\tdefer cancel()\n\n\tclient, err := ca.BootstrapClient(ctx, token, ca.AddFederationToRootCAs())\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfor {\n\t\tresp, err := client.Get(\"https://127.0.0.1:8443\")\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t\tb, err := io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\n\t\tfmt.Printf(\"Server responded: %s\\n\", b)\n\t\ttime.Sleep(5 * time.Second)\n\t}\n}\n"
  },
  {
    "path": "examples/basic-federation/pki/cloud/certs/intermediate_ca.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIBvjCCAWSgAwIBAgIQPDSG4MCReDzPu96+Cb5e0TAKBggqhkjOPQQDAjApMScw\nJQYDVQQDEx5TbWFsbHN0ZXAgUHVibGljIENsb3VkIFJvb3QgQ0EwHhcNMTkwMTE4\nMjEwNjE2WhcNMjkwMTE1MjEwNjE2WjAxMS8wLQYDVQQDEyZTbWFsbHN0ZXAgUHVi\nbGljIENsb3VkIEludGVybWVkaWF0ZSBDQTBZMBMGByqGSM49AgEGCCqGSM49AwEH\nA0IABCQRZefT8U34OiW3o+R2/Ob2DUWrcL87jj9D7IGtAgOjulbfoJaH3rnJumG2\nDtMImBJ1hPa2mXMjThnjOXSlGA+jZjBkMA4GA1UdDwEB/wQEAwIBBjASBgNVHRMB\nAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBSCHPvFHHqEJU82OtFPZVHhstA/rDAfBgNV\nHSMEGDAWgBSMOU1vYot1qfDaTG2uY9l2nQNSADAKBggqhkjOPQQDAgNIADBFAiEA\nhojptJQvmTlu9Ybyr9UCL6Akiks8U1RPF2NS+YKZm+8CIDjbipMuz5AXCez57/5r\nZrEv0JxcWpK6AxfitwyYg34e\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "examples/basic-federation/pki/cloud/certs/kubernetes_root_ca.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIBkjCCATigAwIBAgIRAKqKZhDGVx8dcxTDGpowzNEwCgYIKoZIzj0EAwIwJzEl\nMCMGA1UEAxMcU21hbGxzdGVwIEt1YmVybmV0ZXMgUm9vdCBDQTAeFw0xOTAxMTgy\nMTA2NDdaFw0yOTAxMTUyMTA2NDdaMCcxJTAjBgNVBAMTHFNtYWxsc3RlcCBLdWJl\ncm5ldGVzIFJvb3QgQ0EwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASst212A8a9\nh1DBFEzCgIaoZEWWf0JlBkSmnlHCHZLK2ookNKY6k8UAki4o1xpYjeLtlL4xn4WL\nmMEafC2tPQvxo0UwQzAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIB\nATAdBgNVHQ4EFgQU1+ia0R8GNWYXgs7qkbXHzinghlEwCgYIKoZIzj0EAwIDSAAw\nRQIgDQlbDQxnNxRsR8d/lQiBSy6v0u6BOmftfbB3y0CcGI4CIQC2dxkUvi6GsfHs\nzRgU5ZPIT7sVEfNi9G3GZABj0vOnvQ==\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "examples/basic-federation/pki/cloud/certs/root_ca.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIBlTCCATygAwIBAgIRAPykhdlDneUGU9rI1g+Y40MwCgYIKoZIzj0EAwIwKTEn\nMCUGA1UEAxMeU21hbGxzdGVwIFB1YmxpYyBDbG91ZCBSb290IENBMB4XDTE5MDEx\nODIxMDYxM1oXDTI5MDExNTIxMDYxM1owKTEnMCUGA1UEAxMeU21hbGxzdGVwIFB1\nYmxpYyBDbG91ZCBSb290IENBMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAESc9z\nY77gf4XhCCOzsAhvMThV3Wro6EVnfBaSlmmnq15VaONG6FP7kVyJEM+XD75Thu10\nAsxwB0w4WxKIJ63TNaNFMEMwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYB\nAf8CAQEwHQYDVR0OBBYEFIw5TW9ii3Wp8NpMba5j2XadA1IAMAoGCCqGSM49BAMC\nA0cAMEQCICVylyEZfBBilwKN1nvS4j9Lbt6/nhF5DH9K/wjPIBhiAiBZojDvMZhj\nmreuuFfRC4kWE/OUG5Iz2qVUtlvL/NaXXQ==\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "examples/basic-federation/pki/cloud/config/ca.federated.json",
    "content": "{\n   \"root\": \"pki/cloud/certs/root_ca.crt\",\n   \"federatedRoots\": [\"pki/cloud/certs/kubernetes_root_ca.crt\"],\n   \"crt\": \"pki/cloud/certs/intermediate_ca.crt\",\n   \"key\": \"pki/cloud/secrets/intermediate_ca_key\",\n   \"address\": \":1443\",\n   \"dnsNames\": [\n      \"localhost\"\n   ],\n   \"logger\": {\n      \"format\": \"text\"\n   },\n   \"authority\": {\n      \"provisioners\": [\n         {\n            \"name\": \"sebastian@smallstep.com\",\n            \"type\": \"jwk\",\n            \"key\": {\n               \"use\": \"sig\",\n               \"kty\": \"EC\",\n               \"kid\": \"EE1ZiqkMaxsUdpz8SCSkRBzwK9TWUoidQnMnJ8Eryn8\",\n               \"crv\": \"P-256\",\n               \"alg\": \"ES256\",\n               \"x\": \"BcoXteWHdYxXUrckEQwEQDol2nM97J8KIg7GiXc3AMc\",\n               \"y\": \"8QkL41tl7BZ5uIf_VTOEypp8vsKUnDGpNdrfk0FNt0Q\"\n            },\n            \"encryptedKey\": \"eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJjdHkiOiJqd2sranNvbiIsImVuYyI6IkEyNTZHQ00iLCJwMmMiOjEwMDAwMCwicDJzIjoicTV2MkZ2bmRjNnF0ZWlEZkVBNWI5ZyJ9.MZCVUhU1yuYkhKbQqJDJX0Y6d1X6ranvIeGHIpWLc_STHAgta8c0tA.4o-sw0jTV064OtuL.QVqIo2l0Qf_MRXVghjFYUFkWlK-3VomqzskLLDfLz1norWQa-wEdV56_CIZ7gAPxiLj2N6VOlpEg-sKA2xL3w9b-2WovH_o93iN2MziiWajFf9uq-41LVyEeROd_6Gs4TQbxyz5rk_iMsZeRNRKTpYvW1E2lA4YlMTm4QLV7s7xkGaWsL_-pATfb24bnDMrjRyAVLR61rPHxUQ2wcK_hRG272xoSAsNOWRrUcnDYzjylj-YfmhQZy77Rf38Rxy3UlhB4iMB-y7wMoMuseRKTvBEncL-c0wrllKWUP_KjCl6VeanKWAGUilbmgIpEa1Y_QbNZTD9t0rw2TJSkjx0.CNtEZWZrfp542E65F2oi4w\"\n         }\n      ]\n   },\n   \"tls\": {\n      \"cipherSuites\": [\n         \"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305\",\n         \"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256\"\n      ],\n      \"minVersion\": 1.2,\n      \"maxVersion\": 1.2,\n      \"renegotiation\": false\n   }\n}"
  },
  {
    "path": "examples/basic-federation/pki/cloud/config/ca.json",
    "content": "{\n   \"root\": \"pki/cloud/certs/root_ca.crt\",\n   \"federatedRoots\": [],\n   \"crt\": \"pki/cloud/certs/intermediate_ca.crt\",\n   \"key\": \"pki/cloud/secrets/intermediate_ca_key\",\n   \"address\": \":1443\",\n   \"dnsNames\": [\n      \"localhost\"\n   ],\n   \"logger\": {\n      \"format\": \"text\"\n   },\n   \"authority\": {\n      \"provisioners\": [\n         {\n            \"name\": \"sebastian@smallstep.com\",\n            \"type\": \"jwk\",\n            \"key\": {\n               \"use\": \"sig\",\n               \"kty\": \"EC\",\n               \"kid\": \"EE1ZiqkMaxsUdpz8SCSkRBzwK9TWUoidQnMnJ8Eryn8\",\n               \"crv\": \"P-256\",\n               \"alg\": \"ES256\",\n               \"x\": \"BcoXteWHdYxXUrckEQwEQDol2nM97J8KIg7GiXc3AMc\",\n               \"y\": \"8QkL41tl7BZ5uIf_VTOEypp8vsKUnDGpNdrfk0FNt0Q\"\n            },\n            \"encryptedKey\": \"eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJjdHkiOiJqd2sranNvbiIsImVuYyI6IkEyNTZHQ00iLCJwMmMiOjEwMDAwMCwicDJzIjoicTV2MkZ2bmRjNnF0ZWlEZkVBNWI5ZyJ9.MZCVUhU1yuYkhKbQqJDJX0Y6d1X6ranvIeGHIpWLc_STHAgta8c0tA.4o-sw0jTV064OtuL.QVqIo2l0Qf_MRXVghjFYUFkWlK-3VomqzskLLDfLz1norWQa-wEdV56_CIZ7gAPxiLj2N6VOlpEg-sKA2xL3w9b-2WovH_o93iN2MziiWajFf9uq-41LVyEeROd_6Gs4TQbxyz5rk_iMsZeRNRKTpYvW1E2lA4YlMTm4QLV7s7xkGaWsL_-pATfb24bnDMrjRyAVLR61rPHxUQ2wcK_hRG272xoSAsNOWRrUcnDYzjylj-YfmhQZy77Rf38Rxy3UlhB4iMB-y7wMoMuseRKTvBEncL-c0wrllKWUP_KjCl6VeanKWAGUilbmgIpEa1Y_QbNZTD9t0rw2TJSkjx0.CNtEZWZrfp542E65F2oi4w\"\n         }\n      ]\n   },\n   \"tls\": {\n      \"cipherSuites\": [\n         \"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305\",\n         \"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256\"\n      ],\n      \"minVersion\": 1.2,\n      \"maxVersion\": 1.2,\n      \"renegotiation\": false\n   }\n}"
  },
  {
    "path": "examples/basic-federation/pki/cloud/secrets/intermediate_ca_key",
    "content": "-----BEGIN EC PRIVATE KEY-----\nProc-Type: 4,ENCRYPTED\nDEK-Info: AES-256-CBC,61d1fda0c56e8b8fcf2eb8ba5e20903f\n\npFdzP/NSsBCJ3LiVNd6Qg8BRZrNO9+MOcXeg93LAOFQVOswPRya+tfTojfv6t4Lm\nqJYJlENAAKCjM5+GuMeslTpyqlgDvUYi36v4FAxvLNp49r+nzKXqq0chIPhlLwGb\nMr8rEtX97vosyYEtsYhbjjzgCJXMhBXzi9tbLkkiRxw=\n-----END EC PRIVATE KEY-----\n"
  },
  {
    "path": "examples/basic-federation/pki/cloud/secrets/root_ca_key",
    "content": "-----BEGIN EC PRIVATE KEY-----\nProc-Type: 4,ENCRYPTED\nDEK-Info: AES-256-CBC,78223655d6ee4b458ff2d5b79dadbd06\n\n+bHgP06x5ScVppDSysFN/1xVPnC5MrrUhG7zLG/eX6yJrj4YegthwnDrCuWSCc9U\n0fv0jJ0QGGm8YL5NdH1EpOF7f3+KhOaCv3KLUFJqDWy7HC9anvLHRZPxEeGgndfZ\n5EJz043Lg7xYSDDaH1a0ZVcHPpyiUJNhS9lxmOQP+Ec=\n-----END EC PRIVATE KEY-----\n"
  },
  {
    "path": "examples/basic-federation/pki/kubernetes/certs/cloud_root_ca.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIBlTCCATygAwIBAgIRAPykhdlDneUGU9rI1g+Y40MwCgYIKoZIzj0EAwIwKTEn\nMCUGA1UEAxMeU21hbGxzdGVwIFB1YmxpYyBDbG91ZCBSb290IENBMB4XDTE5MDEx\nODIxMDYxM1oXDTI5MDExNTIxMDYxM1owKTEnMCUGA1UEAxMeU21hbGxzdGVwIFB1\nYmxpYyBDbG91ZCBSb290IENBMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAESc9z\nY77gf4XhCCOzsAhvMThV3Wro6EVnfBaSlmmnq15VaONG6FP7kVyJEM+XD75Thu10\nAsxwB0w4WxKIJ63TNaNFMEMwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYB\nAf8CAQEwHQYDVR0OBBYEFIw5TW9ii3Wp8NpMba5j2XadA1IAMAoGCCqGSM49BAMC\nA0cAMEQCICVylyEZfBBilwKN1nvS4j9Lbt6/nhF5DH9K/wjPIBhiAiBZojDvMZhj\nmreuuFfRC4kWE/OUG5Iz2qVUtlvL/NaXXQ==\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "examples/basic-federation/pki/kubernetes/certs/intermediate_ca.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIBuzCCAWGgAwIBAgIRAK/si8hkdtyDLAjg6ZiRxIgwCgYIKoZIzj0EAwIwJzEl\nMCMGA1UEAxMcU21hbGxzdGVwIEt1YmVybmV0ZXMgUm9vdCBDQTAeFw0xOTAxMTgy\nMTA2NDlaFw0yOTAxMTUyMTA2NDlaMC8xLTArBgNVBAMTJFNtYWxsc3RlcCBLdWJl\ncm5ldGVzIEludGVybWVkaWF0ZSBDQTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IA\nBBIuNImYDPC6BIGm0/C97su9GPsrGoS2uSHDuPIORbaGPtGI8A2SHInw7pHXRqnW\n5TvrbCOcLI2Ao2RQvAd/9jCjZjBkMA4GA1UdDwEB/wQEAwIBBjASBgNVHRMBAf8E\nCDAGAQH/AgEAMB0GA1UdDgQWBBT3w/Rtaw3B4mjhqwDb+/tCFLQTozAfBgNVHSME\nGDAWgBTX6JrRHwY1ZheCzuqRtcfOKeCGUTAKBggqhkjOPQQDAgNIADBFAiEAz8Lo\nGPyrFRGvQ6Eie/qIjJByNEmN3FCOJOJr0J7csy0CIBLxKfMhsT719con+/ZMBNRt\nHEV9xTWpPhUvnlsTPIJl\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "examples/basic-federation/pki/kubernetes/certs/root_ca.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIBkjCCATigAwIBAgIRAKqKZhDGVx8dcxTDGpowzNEwCgYIKoZIzj0EAwIwJzEl\nMCMGA1UEAxMcU21hbGxzdGVwIEt1YmVybmV0ZXMgUm9vdCBDQTAeFw0xOTAxMTgy\nMTA2NDdaFw0yOTAxMTUyMTA2NDdaMCcxJTAjBgNVBAMTHFNtYWxsc3RlcCBLdWJl\ncm5ldGVzIFJvb3QgQ0EwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASst212A8a9\nh1DBFEzCgIaoZEWWf0JlBkSmnlHCHZLK2ookNKY6k8UAki4o1xpYjeLtlL4xn4WL\nmMEafC2tPQvxo0UwQzAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIB\nATAdBgNVHQ4EFgQU1+ia0R8GNWYXgs7qkbXHzinghlEwCgYIKoZIzj0EAwIDSAAw\nRQIgDQlbDQxnNxRsR8d/lQiBSy6v0u6BOmftfbB3y0CcGI4CIQC2dxkUvi6GsfHs\nzRgU5ZPIT7sVEfNi9G3GZABj0vOnvQ==\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "examples/basic-federation/pki/kubernetes/config/ca.federated.json",
    "content": "{\n   \"root\": \"pki/kubernetes/certs/root_ca.crt\",\n   \"federatedRoots\": [\"pki/kubernetes/certs/cloud_root_ca.crt\"],\n   \"crt\": \"pki/kubernetes/certs/intermediate_ca.crt\",\n   \"key\": \"pki/kubernetes/secrets/intermediate_ca_key\",\n   \"address\": \":2443\",\n   \"dnsNames\": [\n      \"localhost\"\n   ],\n   \"logger\": {\n      \"format\": \"text\"\n   },\n   \"authority\": {\n      \"provisioners\": [\n         {\n            \"name\": \"sebastian@smallstep.com\",\n            \"type\": \"jwk\",\n            \"key\": {\n               \"use\": \"sig\",\n               \"kty\": \"EC\",\n               \"kid\": \"S5gYgpeqcIAgc1Zr4myZXpgJ_Ao4ryS6F6wqg9o8RYo\",\n               \"crv\": \"P-256\",\n               \"alg\": \"ES256\",\n               \"x\": \"uYecJyfa3pKHrO36zVsPKCHAcCUoYKOic2M7_qv9Jes\",\n               \"y\": \"WXzgS-36_BcXSi-G86RcLmLaHJEjmcmkhzR9ajrhhGo\"\n            },\n            \"encryptedKey\": \"eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJjdHkiOiJqd2sranNvbiIsImVuYyI6IkEyNTZHQ00iLCJwMmMiOjEwMDAwMCwicDJzIjoiRlBVMHBJclNYbUU5VzhFSzFaMlQ2USJ9.wTT9VpKIOKzoiaBJ2fIcOYwypkKTwuKauwkf4fcfmJ6_VVQ6TykIrA.y_YTjVhmpztcOqYq.9j_pppHHcJ3_VEbt-WfxSOks05QMXNI862uYWcFGc7EVCoD1qEiKIDKEuoUSTG-_WcgkaXrag9UUQPKfDpuYi6UJcyaLkehO2DBX26DJ1T-qEYlhPxPGpx8r7p84zcg_AftypD6PNheiCLe6HOQWWuPtuPrfyUpvyfMWkJC14NZjR4iJysKP5dndxFbSTI2XCw1X-zBDVD9xMnVPlRtezIIuDi2cLEYnNaIlr5NMNQBOrzQaSo1LQaOOoSa_OrZzTdjg7HaUU5DaAA6YEDQPfFxLIJdKshvj5sxDlH_LLY58mzsGECrUx396zjvN8FD-bFSrdbCNlJJ4xhtt34c.FhRAcFIg-5k2srdtdsq2VQ\"\n         }\n      ]\n   },\n   \"tls\": {\n      \"cipherSuites\": [\n         \"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305\",\n         \"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256\"\n      ],\n      \"minVersion\": 1.2,\n      \"maxVersion\": 1.2,\n      \"renegotiation\": false\n   }\n}"
  },
  {
    "path": "examples/basic-federation/pki/kubernetes/config/ca.json",
    "content": "{\n   \"root\": \"pki/kubernetes/certs/root_ca.crt\",\n   \"federatedRoots\": [],\n   \"crt\": \"pki/kubernetes/certs/intermediate_ca.crt\",\n   \"key\": \"pki/kubernetes/secrets/intermediate_ca_key\",\n   \"address\": \":2443\",\n   \"dnsNames\": [\n      \"localhost\"\n   ],\n   \"logger\": {\n      \"format\": \"text\"\n   },\n   \"authority\": {\n      \"provisioners\": [\n         {\n            \"name\": \"sebastian@smallstep.com\",\n            \"type\": \"jwk\",\n            \"key\": {\n               \"use\": \"sig\",\n               \"kty\": \"EC\",\n               \"kid\": \"S5gYgpeqcIAgc1Zr4myZXpgJ_Ao4ryS6F6wqg9o8RYo\",\n               \"crv\": \"P-256\",\n               \"alg\": \"ES256\",\n               \"x\": \"uYecJyfa3pKHrO36zVsPKCHAcCUoYKOic2M7_qv9Jes\",\n               \"y\": \"WXzgS-36_BcXSi-G86RcLmLaHJEjmcmkhzR9ajrhhGo\"\n            },\n            \"encryptedKey\": \"eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJjdHkiOiJqd2sranNvbiIsImVuYyI6IkEyNTZHQ00iLCJwMmMiOjEwMDAwMCwicDJzIjoiRlBVMHBJclNYbUU5VzhFSzFaMlQ2USJ9.wTT9VpKIOKzoiaBJ2fIcOYwypkKTwuKauwkf4fcfmJ6_VVQ6TykIrA.y_YTjVhmpztcOqYq.9j_pppHHcJ3_VEbt-WfxSOks05QMXNI862uYWcFGc7EVCoD1qEiKIDKEuoUSTG-_WcgkaXrag9UUQPKfDpuYi6UJcyaLkehO2DBX26DJ1T-qEYlhPxPGpx8r7p84zcg_AftypD6PNheiCLe6HOQWWuPtuPrfyUpvyfMWkJC14NZjR4iJysKP5dndxFbSTI2XCw1X-zBDVD9xMnVPlRtezIIuDi2cLEYnNaIlr5NMNQBOrzQaSo1LQaOOoSa_OrZzTdjg7HaUU5DaAA6YEDQPfFxLIJdKshvj5sxDlH_LLY58mzsGECrUx396zjvN8FD-bFSrdbCNlJJ4xhtt34c.FhRAcFIg-5k2srdtdsq2VQ\"\n         }\n      ]\n   },\n   \"tls\": {\n      \"cipherSuites\": [\n         \"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305\",\n         \"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256\"\n      ],\n      \"minVersion\": 1.2,\n      \"maxVersion\": 1.2,\n      \"renegotiation\": false\n   }\n}"
  },
  {
    "path": "examples/basic-federation/pki/kubernetes/secrets/intermediate_ca_key",
    "content": "-----BEGIN EC PRIVATE KEY-----\nProc-Type: 4,ENCRYPTED\nDEK-Info: AES-256-CBC,fb0860a45dbae164b2657f27c2a3f1cf\n\n6nFswxVAct5zIqsThsJ1uYY6gzkhMzAmurih+mhlhANfl6SHURdzk8AtutIQrFfV\njr/vTPyr3aR+dqldt/wg1L9pJFc/OoWVlOFbltmLTkWMPk+VHKxR5V6A7IVYoKvm\nEUFzVb+aHj6M9R7ecBf0IslGQ0nsqYa54WppGHmAA4A=\n-----END EC PRIVATE KEY-----\n"
  },
  {
    "path": "examples/basic-federation/pki/kubernetes/secrets/root_ca_key",
    "content": "-----BEGIN EC PRIVATE KEY-----\nProc-Type: 4,ENCRYPTED\nDEK-Info: AES-256-CBC,965ede7ef96d4640932c18bdf1795645\n\ntreDpMX0uYFlWMiPvjYfnv6K9jmT4f8pTG6AkzZB0eeaNj04tt4FuIgrabHoZFNx\nIC1mFIRZvhJaiOXNIvQbo/Wnweu8nVV/xn73xNKBramgfXDo9WCvIsffjRg1xtsq\ns3SuONddo4IdpmrG7iEZTkDe2IzSV5NYhGsKiwvDvaU=\n-----END EC PRIVATE KEY-----\n"
  },
  {
    "path": "examples/basic-federation/server/main.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"os\"\n\t\"time\"\n\n\t\"github.com/smallstep/certificates/ca\"\n)\n\nfunc main() {\n\tif len(os.Args) != 2 {\n\t\tfmt.Fprintf(os.Stderr, \"Usage: %s <token>\\n\", os.Args[0])\n\t\tos.Exit(1)\n\t}\n\n\ttoken := os.Args[1]\n\n\t// make sure to cancel the renew goroutine\n\tctx, cancel := context.WithCancel(context.Background())\n\tdefer cancel()\n\n\tsrv, err := ca.BootstrapServer(ctx, token, &http.Server{\n\t\tAddr: \":8443\",\n\t\tHandler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tname := \"nobody\"\n\t\t\tissuer := \"none\"\n\t\t\tif r.TLS != nil && len(r.TLS.PeerCertificates) > 0 {\n\t\t\t\tname = r.TLS.PeerCertificates[0].Subject.CommonName\n\t\t\t\tissuer = r.TLS.PeerCertificates[len(r.TLS.PeerCertificates)-1].Issuer.CommonName\n\t\t\t}\n\t\t\tfmt.Fprintf(w, \"Hello %s (cert issued by '%s') at %s\", name, issuer, time.Now().UTC()) //nolint:gosec // example code for demonstration\n\t\t}),\n\t\tReadHeaderTimeout: 30 * time.Second,\n\t}, ca.AddFederationToClientCAs(), ListTrustedRoots())\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(\"Listening on :8443 ...\")\n\tif err := srv.ListenAndServeTLS(\"\", \"\"); err != nil {\n\t\tpanic(err)\n\t}\n}\n\n// ListTrustedRoots prints list of trusted roots for illustration purposes\nfunc ListTrustedRoots() ca.TLSOption {\n\tfn := func(ctx *ca.TLSOptionCtx) error {\n\t\tcerts, err := ctx.Client.Federation()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\troots, err := ctx.Client.Roots()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif len(certs.Certificates) > len(roots.Certificates) {\n\t\t\tfmt.Println(\"Server is using federated root certificates\")\n\t\t}\n\t\tfor _, cert := range certs.Certificates {\n\t\t\tfmt.Printf(\"Accepting certs anchored in %s\\n\", cert.Certificate.Subject)\n\t\t}\n\n\t\treturn nil\n\t}\n\treturn func(ctx *ca.TLSOptionCtx) error {\n\t\tctx.OnRenewFunc = append(ctx.OnRenewFunc, fn)\n\t\treturn fn(ctx)\n\t}\n}\n"
  },
  {
    "path": "examples/bootstrap-client/client.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"time\"\n\n\t\"github.com/smallstep/certificates/ca\"\n)\n\nfunc main() {\n\tif len(os.Args) != 2 {\n\t\tfmt.Fprintf(os.Stderr, \"Usage: %s <token>\\n\", os.Args[0])\n\t\tos.Exit(1)\n\t}\n\n\ttoken := os.Args[1]\n\n\t// make sure to cancel the renew goroutine\n\tctx, cancel := context.WithCancel(context.Background())\n\tdefer cancel()\n\n\tclient, err := ca.BootstrapClient(ctx, token)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfor {\n\t\tresp, err := client.Get(\"https://localhost:8443\")\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t\tb, err := io.ReadAll(resp.Body)\n\t\tresp.Body.Close()\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\n\t\tfmt.Printf(\"Server responded: %s\\n\", b)\n\t\ttime.Sleep(1 * time.Second)\n\t}\n}\n"
  },
  {
    "path": "examples/bootstrap-mtls-server/server.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"os\"\n\t\"time\"\n\n\t\"github.com/smallstep/certificates/ca\"\n)\n\nfunc main() {\n\tif len(os.Args) != 2 {\n\t\tfmt.Fprintf(os.Stderr, \"Usage: %s <token>\\n\", os.Args[0])\n\t\tos.Exit(1)\n\t}\n\n\ttoken := os.Args[1]\n\n\t// make sure to cancel the renew goroutine\n\tctx, cancel := context.WithCancel(context.Background())\n\tdefer cancel()\n\n\tsrv, err := ca.BootstrapServer(ctx, token, &http.Server{\n\t\tAddr: \":8443\",\n\t\tHandler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tname := \"nobody\"\n\t\t\tif r.TLS != nil && len(r.TLS.PeerCertificates) > 0 {\n\t\t\t\tname = r.TLS.PeerCertificates[0].Subject.CommonName\n\t\t\t}\n\t\t\tfmt.Fprintf(w, \"Hello %s at %s!!!\", name, time.Now().UTC()) //nolint:gosec // example code for demonstration\n\t\t}),\n\t\tReadHeaderTimeout: 30 * time.Second,\n\t})\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(\"Listening on :8443 ...\")\n\tif err := srv.ListenAndServeTLS(\"\", \"\"); err != nil {\n\t\tpanic(err)\n\t}\n}\n"
  },
  {
    "path": "examples/bootstrap-tls-server/server.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"os\"\n\t\"time\"\n\n\t\"github.com/smallstep/certificates/ca\"\n)\n\nfunc main() {\n\tif len(os.Args) != 2 {\n\t\tfmt.Fprintf(os.Stderr, \"Usage: %s <token>\\n\", os.Args[0])\n\t\tos.Exit(1)\n\t}\n\n\ttoken := os.Args[1]\n\n\t// make sure to cancel the renew goroutine\n\tctx, cancel := context.WithCancel(context.Background())\n\tdefer cancel()\n\n\tsrv, err := ca.BootstrapServer(ctx, token, &http.Server{\n\t\tAddr: \":8443\",\n\t\tHandler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tname := \"nobody\"\n\t\t\tif r.TLS != nil && len(r.TLS.PeerCertificates) > 0 {\n\t\t\t\tname = r.TLS.PeerCertificates[0].Subject.CommonName\n\t\t\t}\n\t\t\tfmt.Fprintf(w, \"Hello %s at %s!!!\", name, time.Now().UTC()) //nolint:gosec // example code for demonstration\n\t\t}),\n\t\tReadHeaderTimeout: 30 * time.Second,\n\t}, ca.VerifyClientCertIfGiven())\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(\"Listening on :8443 ...\")\n\tif err := srv.ListenAndServeTLS(\"\", \"\"); err != nil {\n\t\tpanic(err)\n\t}\n}\n"
  },
  {
    "path": "examples/docker/Makefile",
    "content": "all: binaries build up\n\nbinaries:\n\tCGO_ENABLED=0 GOOS=linux go build -o ca/step-ca github.com/smallstep/certificates/cmd/step-ca\n\nbuild: build-nginx build-ca build-renewer\nbuild-nginx:\n\tdocker build -t nginx-test:latest nginx\nbuild-ca:\n\tdocker build -t step-ca-test:latest ca\nbuild-renewer:\n\tdocker build -t step-renewer-test:latest renewer\n\nup:\n\tdocker-compose up\n\ndown:\n\tdocker-compose down\n\ninspect:\n\tstep certificate inspect https://localhost:4443 --insecure\n\n.PHONY: all binaries up down inspect\n.PHONY: build build-nginx build-ca build-renewer\n"
  },
  {
    "path": "examples/docker/ca/Dockerfile",
    "content": "FROM alpine\n\nADD step-ca /usr/local/bin/step-ca\nCOPY pki /run\n\n# Smallstep CA\nCMD [\"step-ca\", \"/run/config/ca.json\"]\n"
  },
  {
    "path": "examples/docker/ca/pki/config/ca.json",
    "content": "{\n    \"root\": \"/run/secrets/root_ca.crt\",\n    \"crt\": \"/run/secrets/intermediate_ca.crt\",\n    \"key\": \"/run/secrets/intermediate_ca_key\",\n    \"password\": \"password\",\n    \"address\": \":443\",\n    \"dnsNames\": [\n        \"ca\"\n    ],\n    \"logger\": {\n        \"format\": \"text\"\n    },\n    \"authority\": {\n        \"provisioners\": [\n            {\n                \"name\": \"mariano@smallstep.com\",\n                \"type\": \"jwk\",\n                \"key\": {\n                    \"use\": \"sig\",\n                    \"kty\": \"EC\",\n                    \"kid\": \"DmAtZt2EhmZr_iTJJ387fr4Md2NbzMXGdXQNW1UWPXk\",\n                    \"crv\": \"P-256\",\n                    \"alg\": \"ES256\",\n                    \"x\": \"jXoO1j4CXxoTC32pNzkVC8l6k2LfP0k5ndhJZmcdVbk\",\n                    \"y\": \"c3JDL4GTFxJWHa8EaHdMh4QgwMh64P2_AGWrD0ADXcI\"\n                },\n                \"encryptedKey\": \"eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJjdHkiOiJqd2sranNvbiIsImVuYyI6IkEyNTZHQ00iLCJwMmMiOjEwMDAwMCwicDJzIjoiOTFVWjdzRGw3RlNXcldfX1I1NUh3USJ9.FcWtrBDNgrkA33G9Ll9sXh1cPF-3jVXeYe1FLmSDc_Q2PmfLOPvJOA.0ZoN32ayaRWnufJb.WrkffMmDLWiq1-2kn-w7-kVBGW12gjNCBHNHB1hyEdED0rWH1YWpKd8FjoOACdJyLhSn4kAS3Lw5AH7fvO27A48zzvoxZU5EgSm5HG9IjkIH-LBJ-v79ShkpmPylchgjkFhxa5epD11OIK4rFmI7s-0BCjmJokLR_DZBhDMw2khGnsr_MEOfAz9UnqXaQ4MIy8eT52xUpx68gpWFlz2YP3EqiYyNEv0PpjMtyP5lO2i8-p8BqvuJdus9H3fO5Dg-1KVto1wuqh4BQ2JKTauv60QAnM_4sdxRHku3F_nV64SCrZfDvnN2ve21raFROtyXaqHZhN6lyoPxDncy8v4.biaOblEe0N-gMpJyFZ-3-A\"\n            },\n            {\n                \"name\": \"mike@smallstep.com\",\n                \"type\": \"jwk\",\n                \"key\": {\n                    \"use\": \"sig\",\n                    \"kty\": \"EC\",\n                    \"kid\": \"YYNxZ0rq0WsT2MlqLCWvgme3jszkmt99KjoGEJJwAKs\",\n                    \"crv\": \"P-256\",\n                    \"alg\": \"ES256\",\n                    \"x\": \"LsI8nHBflc-mrCbRqhl8d3hSl5sYuSM1AbXBmRfznyg\",\n                    \"y\": \"F99LoOvi7z-ZkumsgoHIhodP8q9brXe4bhF3szK-c_w\"\n                },\n                \"encryptedKey\": \"eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJjdHkiOiJqd2sranNvbiIsImVuYyI6IkEyNTZHQ00iLCJwMmMiOjEwMDAwMCwicDJzIjoiVERQS2dzcEItTUR4ZDJxTGo0VlpwdyJ9.2_j0cZgTm2eFkZ-hrtr1hBIvLxN0w3TZhbX0Jrrq7vBMaywhgFcGTA.mCasZCbZJ-JT7vjA.bW052WDKSf_ueEXq1dyxLq0n3qXWRO-LXr7OzBLdUKWKSBGQrzqS5KJWqdUCPoMIHTqpwYvm-iD6uFlcxKBYxnsAG_hoq_V3icvvwNQQSd_q7Thxr2_KtPIDJWNuX1t5qXp11hkgb-8d5HO93CmN7xNDG89pzSUepT6RYXOZ483mP5fre9qzkfnrjx3oPROCnf3SnIVUvqk7fwfXuniNsg3NrNqncHYUQNReiq3e9I1R60w0ZQTvIReY7-zfiq7iPgVqmu5I7XGgFK4iBv0L7UOEora65b4hRWeLxg5t7OCfUqrS9yxAk8FdjFb9sEfjopWViPRepB0dYPH8dVI.fb6-7XWqp0j6CR9Li0NI-Q\",\n                \"claims\": {\n                    \"minTLSCertDuration\": \"60s\",\n                    \"defaultTLSCertDuration\": \"120s\"\n                }\n            }\n        ]\n    },\n    \"tls\": {\n        \"cipherSuites\": [\n            \"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305\",\n            \"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256\"\n        ],\n        \"minVersion\": 1.2,\n        \"maxVersion\": 1.2,\n        \"renegotiation\": false\n    }\n}"
  },
  {
    "path": "examples/docker/ca/pki/secrets/intermediate_ca.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIBxjCCAWugAwIBAgIQAYoOWhdChUmmKzlc0DWcWDAKBggqhkjOPQQDAjAcMRow\nGAYDVQQDExFTbWFsbHN0ZXAgUm9vdCBDQTAeFw0xODExMDIyMzU0MTNaFw0yODEw\nMzAyMzU0MTNaMCQxIjAgBgNVBAMTGVNtYWxsc3RlcCBJbnRlcm1lZGlhdGUgQ0Ew\nWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASxvIWme8/yDAxkR63KgSYkpN7mHKBH\nk5c8S+uzba4xWbaxZtEZ9NNhEIAgYFZ9/3ThrzLOsuGwRCvPTaD5iycQo4GGMIGD\nMA4GA1UdDwEB/wQEAwIBpjAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIw\nEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQU8dKIy5ZLH2h3ihWgqjcpoo5e\nq3YwHwYDVR0jBBgwFoAU0IpOvAyBnn9UhDqOQzXnfEU3aYMwCgYIKoZIzj0EAwID\nSQAwRgIhANXlcktuaEvORhgRvzQ6vVNgvpqCEXW3CcCHjUl1xSdaAiEAmakkpfFq\nVsT5PqPnTRgOWlFESRhQ9btl6nQ+2Lt/S5A=\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "examples/docker/ca/pki/secrets/intermediate_ca_key",
    "content": "-----BEGIN EC PRIVATE KEY-----\nProc-Type: 4,ENCRYPTED\nDEK-Info: AES-256-CBC,4c7758e66df1884f6560839de64d4dd3\n\nS8Ha8uA+bA3IGPurYODwd9VaJZ6FHI2tlznHXCOxT1MlGqyEAc4aWS11QBUz0Ucp\nexcwlqM8kfh5BcN5a+vvInHnv74ZiNPdpt/apzz2LIx52pApzASiKVXRsAUmR4Pv\n3MsO1/cVHkilpee1uC+axL32d5YmyP0URpSNJK9BhZo=\n-----END EC PRIVATE KEY-----\n"
  },
  {
    "path": "examples/docker/ca/pki/secrets/root_ca.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIBfDCCASGgAwIBAgIQY0CXerxuM+EhTbpVxxLRKjAKBggqhkjOPQQDAjAcMRow\nGAYDVQQDExFTbWFsbHN0ZXAgUm9vdCBDQTAeFw0xODExMDIyMzU0MTNaFw0yODEw\nMzAyMzU0MTNaMBwxGjAYBgNVBAMTEVNtYWxsc3RlcCBSb290IENBMFkwEwYHKoZI\nzj0CAQYIKoZIzj0DAQcDQgAEEGa7ZeL4WVIfPFDS7glJkIVsITVQgjfyz+AhcYaS\nrkJZlWOGZ60br9uE/wEfUcX1zavrX1Wz+bSJzTvT0AVBNqNFMEMwDgYDVR0PAQH/\nBAQDAgGmMBIGA1UdEwEB/wQIMAYBAf8CAQEwHQYDVR0OBBYEFNCKTrwMgZ5/VIQ6\njkM153xFN2mDMAoGCCqGSM49BAMCA0kAMEYCIQCRA4EdlTTMhs2Zd1cT75ZgxeGa\nmjVPl1vqBxLkHqEO+QIhAPKVm7E452ZBe2o5rQRxGwa94MI+CyuEIH9md3nTgWWX\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "examples/docker/ca/pki/secrets/root_ca_key",
    "content": "-----BEGIN EC PRIVATE KEY-----\nProc-Type: 4,ENCRYPTED\nDEK-Info: AES-256-CBC,98fdc560ba714aebb9fd4b714395d8ce\n\n2bFn8yRb8lMvDR6oh22PocfhXdaoVNt4QwHCJNy0K0fG8CMokwDfEec//LseP6rA\n7/EV11+ZgoN9xyTNe1kB6zFv7/kzCpRm23sqtyio+8xXWnLZNYKBRYYEeJWBUqqd\nGAfazg4ZFzoIH5TEPWCEAp7M9CVvtiw1SeA/zjewp2k=\n-----END EC PRIVATE KEY-----\n"
  },
  {
    "path": "examples/docker/docker-compose.yml",
    "content": "version: '3.3'\n\nservices:\n  ca:\n    image: step-ca-test:latest\n    ports:\n      - \"8443:443\"\n    restart: always\n\n  renewer:\n    depends_on:\n      - ca\n    image: step-renewer-test:latest\n    volumes:\n      - certificates:/var/local/step\n    secrets:\n      - password\n    environment:\n      STEPPATH: /home/step\n      STEP_CA_URL: https://ca\n      STEP_FINGERPRINT: 84a033e84196f73bd593fad7a63e509e57fd982f02084359c4e8c5c864efc27d\n      STEP_ROOT: /var/local/step/root_ca.crt\n      STEP_KID: DmAtZt2EhmZr_iTJJ387fr4Md2NbzMXGdXQNW1UWPXk\n      STEP_PASSWORD_FILE: /run/secrets/password\n      COMMON_NAME: nginx\n\n  nginx:\n    depends_on:\n      - renewer\n    image: nginx-test:latest\n    ports:\n      - \"4443:443\"\n    volumes:\n      - certificates:/var/local/step:ro\n    restart: always\n\nvolumes:\n  certificates:\n\nsecrets:\n  password:\n    file: ./password.txt\n"
  },
  {
    "path": "examples/docker/nginx/Dockerfile",
    "content": "FROM nginx:alpine\n\nRUN apk add inotify-tools\nRUN mkdir -p /var/local/step\nCOPY site.conf /etc/nginx/conf.d/\nCOPY certwatch.sh /\nCOPY entrypoint.sh /\n\n# Certificate watcher and nginx\nENTRYPOINT [\"/entrypoint.sh\"]\nCMD [\"nginx\", \"-g\", \"daemon off;\"]\n"
  },
  {
    "path": "examples/docker/nginx/certwatch.sh",
    "content": "#!/bin/sh\n\nwhile true; do\n    inotifywait -e modify /var/local/step/site.crt\n    nginx -s reload\ndone\n"
  },
  {
    "path": "examples/docker/nginx/entrypoint.sh",
    "content": "#!/bin/sh\n\n# Wait for renewer\nsleep 10\n\n# watch for the update of the cert and reload nginx\n/certwatch.sh &\n\n# Run docker CMD\nexec \"$@\""
  },
  {
    "path": "examples/docker/nginx/site.conf",
    "content": "server {\n    listen                443 ssl;\n    server_name           localhost;\n    ssl_certificate       /var/local/step/site.crt;\n    ssl_certificate_key   /var/local/step/site.key;\n\n    location / {\n        root   /usr/share/nginx/html;\n        index  index.html index.htm;\n    }\n}"
  },
  {
    "path": "examples/docker/password.txt",
    "content": "password\n"
  },
  {
    "path": "examples/docker/renewer/Dockerfile",
    "content": "FROM smallstep/step-cli\n\nUSER root\nRUN mkdir -p /var/local/step\nADD crontab /var/spool/cron/crontabs/root\nRUN chmod 0644 /var/spool/cron/crontabs/root\n\nCOPY entrypoint.sh /\nENTRYPOINT [\"/entrypoint.sh\"]\nCMD [\"/usr/sbin/crond\", \"-l\", \"2\", \"-f\"]\n"
  },
  {
    "path": "examples/docker/renewer/crontab",
    "content": "# min\thour\tday\tmonth\tweekday\tcommand\n*\t*\t*\t*\t*\tstep ca renew --force /var/local/step/site.crt /var/local/step/site.key\n"
  },
  {
    "path": "examples/docker/renewer/entrypoint.sh",
    "content": "#!/bin/sh\n\n# Wait for CA\nsleep 5\n\n# Clean old certificates\nrm -f /var/local/step/root_ca.crt\nrm -f /var/local/step/site.crt /var/local/step/site.key\n\n# Download the root certificate\nstep ca root /var/local/step/root_ca.crt\n\n# Get token\nSTEP_TOKEN=$(step ca token $COMMON_NAME)\n# Download the root certificate\nstep ca certificate --token $STEP_TOKEN $COMMON_NAME /var/local/step/site.crt /var/local/step/site.key\n\nexec \"$@\"\n"
  },
  {
    "path": "examples/pki/config/ca.json",
    "content": "{\n\t\"root\": \"examples/pki/secrets/root_ca.crt\",\n\t\"federatedRoots\": null,\n\t\"crt\": \"examples/pki/secrets/intermediate_ca.crt\",\n\t\"key\": \"examples/pki/secrets/intermediate_ca_key\",\n\t\"address\": \":9000\",\n\t\"dnsNames\": [\n\t\t\"localhost\"\n\t],\n\t\"logger\": {\n\t\t\"format\": \"text\"\n\t},\n\t\"authority\": {\n\t\t\"provisioners\": [\n\t\t\t{\n\t\t\t\t\"type\": \"jwk\",\n\t\t\t\t\"name\": \"mariano@smallstep.com\",\n\t\t\t\t\"key\": {\n\t\t\t\t\t\"use\": \"sig\",\n\t\t\t\t\t\"kty\": \"EC\",\n\t\t\t\t\t\"kid\": \"DmAtZt2EhmZr_iTJJ387fr4Md2NbzMXGdXQNW1UWPXk\",\n\t\t\t\t\t\"crv\": \"P-256\",\n\t\t\t\t\t\"alg\": \"ES256\",\n\t\t\t\t\t\"x\": \"jXoO1j4CXxoTC32pNzkVC8l6k2LfP0k5ndhJZmcdVbk\",\n\t\t\t\t\t\"y\": \"c3JDL4GTFxJWHa8EaHdMh4QgwMh64P2_AGWrD0ADXcI\"\n\t\t\t\t},\n\t\t\t\t\"encryptedKey\": \"eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJjdHkiOiJqd2sranNvbiIsImVuYyI6IkEyNTZHQ00iLCJwMmMiOjEwMDAwMCwicDJzIjoiOTFVWjdzRGw3RlNXcldfX1I1NUh3USJ9.FcWtrBDNgrkA33G9Ll9sXh1cPF-3jVXeYe1FLmSDc_Q2PmfLOPvJOA.0ZoN32ayaRWnufJb.WrkffMmDLWiq1-2kn-w7-kVBGW12gjNCBHNHB1hyEdED0rWH1YWpKd8FjoOACdJyLhSn4kAS3Lw5AH7fvO27A48zzvoxZU5EgSm5HG9IjkIH-LBJ-v79ShkpmPylchgjkFhxa5epD11OIK4rFmI7s-0BCjmJokLR_DZBhDMw2khGnsr_MEOfAz9UnqXaQ4MIy8eT52xUpx68gpWFlz2YP3EqiYyNEv0PpjMtyP5lO2i8-p8BqvuJdus9H3fO5Dg-1KVto1wuqh4BQ2JKTauv60QAnM_4sdxRHku3F_nV64SCrZfDvnN2ve21raFROtyXaqHZhN6lyoPxDncy8v4.biaOblEe0N-gMpJyFZ-3-A\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"type\": \"jwk\",\n\t\t\t\t\"name\": \"mike@smallstep.com\",\n\t\t\t\t\"key\": {\n\t\t\t\t\t\"use\": \"sig\",\n\t\t\t\t\t\"kty\": \"EC\",\n\t\t\t\t\t\"kid\": \"YYNxZ0rq0WsT2MlqLCWvgme3jszkmt99KjoGEJJwAKs\",\n\t\t\t\t\t\"crv\": \"P-256\",\n\t\t\t\t\t\"alg\": \"ES256\",\n\t\t\t\t\t\"x\": \"LsI8nHBflc-mrCbRqhl8d3hSl5sYuSM1AbXBmRfznyg\",\n\t\t\t\t\t\"y\": \"F99LoOvi7z-ZkumsgoHIhodP8q9brXe4bhF3szK-c_w\"\n\t\t\t\t},\n\t\t\t\t\"encryptedKey\": \"eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJjdHkiOiJqd2sranNvbiIsImVuYyI6IkEyNTZHQ00iLCJwMmMiOjEwMDAwMCwicDJzIjoiVERQS2dzcEItTUR4ZDJxTGo0VlpwdyJ9.2_j0cZgTm2eFkZ-hrtr1hBIvLxN0w3TZhbX0Jrrq7vBMaywhgFcGTA.mCasZCbZJ-JT7vjA.bW052WDKSf_ueEXq1dyxLq0n3qXWRO-LXr7OzBLdUKWKSBGQrzqS5KJWqdUCPoMIHTqpwYvm-iD6uFlcxKBYxnsAG_hoq_V3icvvwNQQSd_q7Thxr2_KtPIDJWNuX1t5qXp11hkgb-8d5HO93CmN7xNDG89pzSUepT6RYXOZ483mP5fre9qzkfnrjx3oPROCnf3SnIVUvqk7fwfXuniNsg3NrNqncHYUQNReiq3e9I1R60w0ZQTvIReY7-zfiq7iPgVqmu5I7XGgFK4iBv0L7UOEora65b4hRWeLxg5t7OCfUqrS9yxAk8FdjFb9sEfjopWViPRepB0dYPH8dVI.fb6-7XWqp0j6CR9Li0NI-Q\",\n\t\t\t\t\"claims\": {\n\t\t\t\t\t\"minTLSCertDuration\": \"1m0s\",\n\t\t\t\t\t\"defaultTLSCertDuration\": \"2m0s\"\n\t\t\t\t}\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"type\": \"jwk\",\n\t\t\t\t\"name\": \"decade\",\n\t\t\t\t\"key\": {\n\t\t\t\t\t\"use\": \"sig\",\n\t\t\t\t\t\"kty\": \"EC\",\n\t\t\t\t\t\"kid\": \"iu7VZxKUcquv1BCWuvEUOyRy4zYyCmgt61OpRW5VbRE\",\n\t\t\t\t\t\"crv\": \"P-256\",\n\t\t\t\t\t\"alg\": \"ES256\",\n\t\t\t\t\t\"x\": \"PExnlmHxnnfpvp4bznMKbA6L_9Bk9ZhtsmvbOwh9Kys\",\n\t\t\t\t\t\"y\": \"rrMPGvxscRzDdOYtZ1wsxeQjuuFl0nSzkwTHV_P-K-Y\"\n                },\n                \"claims\": {\n                    \"maxTLSCertDuration\": \"87600h\",\n                    \"defaultTLSCertDuration\": \"87600h\"\n                },\n\t\t\t\t\"encryptedKey\": \"eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJjdHkiOiJqd2sranNvbiIsImVuYyI6IkEyNTZHQ00iLCJwMmMiOjEwMDAwMCwicDJzIjoiZS1OVzRaZlBUNjFCUmR1bjJyNk9OZyJ9.zjToJ_Od6RIzVmo0cnmLZ69am410ftfBW594qNt60KmKX6JEWUufhA.kSrC74fKK3CkqiNS.G-oUqQhYMFIKuSj8thg9B5TeiaIMsQ-o_PTxIZE-Qb8TDU15ehPAsuIQmnbM6dSpkSGCmZgHTscp3xgLyv6QEBBjUHBpLwciWyipj1KBZDKSgLKeV6G2NiVBMETOaD1DsX3DxrHM-K3T1chXJFMJfkDSx1OEtaVfzqVYLyvNb5y_26oeRNSNYuTLzOrk6Ebr6KJE6lSWpvu1dtOrDAhTErouC56EQu2fTeDCa9eN50iRs4OjmF6FtBlR63h6FkvbmjJWC3zbIOe2RXRQx0Po6_dnKXSIqs7JMZSBerlgw6jzHme8YvqBqc2Ccy4Y4gJ23nwLkcsOVuFNdk6Nb7s.SB296DDrS-Wi4a9x_TGv4A\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"type\": \"jwk\",\n\t\t\t\t\"name\": \"90days\",\n\t\t\t\t\"key\": {\n\t\t\t\t\t\"use\": \"sig\",\n\t\t\t\t\t\"kty\": \"EC\",\n\t\t\t\t\t\"kid\": \"2LgjIvfirblnFMC6FjUr8jYkO8nOqz4rKoarCc8kiGU\",\n\t\t\t\t\t\"crv\": \"P-256\",\n\t\t\t\t\t\"alg\": \"ES256\",\n\t\t\t\t\t\"x\": \"iHFHMN91iFUDLh2LweFj6o0gDJ-pdmBY4IFIBNfUqd4\",\n\t\t\t\t\t\"y\": \"Yfym7KtzZQaQc1gQoT81ggNBPvAdV_0CW0A5mQgOsOc\"\n\t\t\t\t},\n                \"claims\": {\n                    \"maxTLSCertDuration\": \"2160h\",\n                    \"defaultTLSCertDuration\": \"2160h\"\n                },\n\t\t\t\t\"encryptedKey\": \"eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJjdHkiOiJqd2sranNvbiIsImVuYyI6IkEyNTZHQ00iLCJwMmMiOjEwMDAwMCwicDJzIjoiYk9XV0ZUN29uZldtZTdvbzdCMFZOdyJ9.p3gs2xd-Bdtwz1WGzQUZrcZeA8mpaMn_R_wTInpzZ9G1vIeRk-9T4g.RQNXmZP8uAzF1n8b.WpLqmNV_I0RIetdID2ag-igZryM8ekSimaHrXKoEpRAlBdBDZC-9qkbrJPNcTPRUi-29iZiBxKQ-0GX7ytiyulrQl7UfxUSrtT5vjhJEthSOGYXAOerUAnodGjpLCtIueTwVl6KJA2bXUapUd9xFn3DXfVgFagwqo1MrXKuIR0R5A4sjmEx8d2Kn_KQr0ZNnSOaAod2os4tmh3A87u9Jb51FMxhP-8Qbn7ff-RXwT_015C64Ux1zzS-ok89XbTgyfGxkah0-fVFAgS0zosHLI3C_pvumcglmFXZz7otH596BAU_QkqME6X-PGte6j6eldFobP_96tBxOhIRgVKw.Ky4xLbQZEGaBPjGJnKurng\"\n\t\t\t}\n\t\t]\n\t},\n\t\"tls\": {\n\t\t\"cipherSuites\": [\n\t\t\t\"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305\",\n\t\t\t\"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256\"\n\t\t],\n\t\t\"minVersion\": 1.2,\n\t\t\"maxVersion\": 1.2,\n\t\t\"renegotiation\": false\n\t},\n\t\"password\": \"password\"\n}\n"
  },
  {
    "path": "examples/pki/secrets/intermediate_ca.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIBxjCCAWugAwIBAgIQAYoOWhdChUmmKzlc0DWcWDAKBggqhkjOPQQDAjAcMRow\nGAYDVQQDExFTbWFsbHN0ZXAgUm9vdCBDQTAeFw0xODExMDIyMzU0MTNaFw0yODEw\nMzAyMzU0MTNaMCQxIjAgBgNVBAMTGVNtYWxsc3RlcCBJbnRlcm1lZGlhdGUgQ0Ew\nWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASxvIWme8/yDAxkR63KgSYkpN7mHKBH\nk5c8S+uzba4xWbaxZtEZ9NNhEIAgYFZ9/3ThrzLOsuGwRCvPTaD5iycQo4GGMIGD\nMA4GA1UdDwEB/wQEAwIBpjAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIw\nEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQU8dKIy5ZLH2h3ihWgqjcpoo5e\nq3YwHwYDVR0jBBgwFoAU0IpOvAyBnn9UhDqOQzXnfEU3aYMwCgYIKoZIzj0EAwID\nSQAwRgIhANXlcktuaEvORhgRvzQ6vVNgvpqCEXW3CcCHjUl1xSdaAiEAmakkpfFq\nVsT5PqPnTRgOWlFESRhQ9btl6nQ+2Lt/S5A=\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "examples/pki/secrets/intermediate_ca_key",
    "content": "-----BEGIN EC PRIVATE KEY-----\nProc-Type: 4,ENCRYPTED\nDEK-Info: AES-256-CBC,4c7758e66df1884f6560839de64d4dd3\n\nS8Ha8uA+bA3IGPurYODwd9VaJZ6FHI2tlznHXCOxT1MlGqyEAc4aWS11QBUz0Ucp\nexcwlqM8kfh5BcN5a+vvInHnv74ZiNPdpt/apzz2LIx52pApzASiKVXRsAUmR4Pv\n3MsO1/cVHkilpee1uC+axL32d5YmyP0URpSNJK9BhZo=\n-----END EC PRIVATE KEY-----\n"
  },
  {
    "path": "examples/pki/secrets/root_ca.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIBfDCCASGgAwIBAgIQY0CXerxuM+EhTbpVxxLRKjAKBggqhkjOPQQDAjAcMRow\nGAYDVQQDExFTbWFsbHN0ZXAgUm9vdCBDQTAeFw0xODExMDIyMzU0MTNaFw0yODEw\nMzAyMzU0MTNaMBwxGjAYBgNVBAMTEVNtYWxsc3RlcCBSb290IENBMFkwEwYHKoZI\nzj0CAQYIKoZIzj0DAQcDQgAEEGa7ZeL4WVIfPFDS7glJkIVsITVQgjfyz+AhcYaS\nrkJZlWOGZ60br9uE/wEfUcX1zavrX1Wz+bSJzTvT0AVBNqNFMEMwDgYDVR0PAQH/\nBAQDAgGmMBIGA1UdEwEB/wQIMAYBAf8CAQEwHQYDVR0OBBYEFNCKTrwMgZ5/VIQ6\njkM153xFN2mDMAoGCCqGSM49BAMCA0kAMEYCIQCRA4EdlTTMhs2Zd1cT75ZgxeGa\nmjVPl1vqBxLkHqEO+QIhAPKVm7E452ZBe2o5rQRxGwa94MI+CyuEIH9md3nTgWWX\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "examples/pki/secrets/root_ca_key",
    "content": "-----BEGIN EC PRIVATE KEY-----\nProc-Type: 4,ENCRYPTED\nDEK-Info: AES-256-CBC,98fdc560ba714aebb9fd4b714395d8ce\n\n2bFn8yRb8lMvDR6oh22PocfhXdaoVNt4QwHCJNy0K0fG8CMokwDfEec//LseP6rA\n7/EV11+ZgoN9xyTNe1kB6zFv7/kzCpRm23sqtyio+8xXWnLZNYKBRYYEeJWBUqqd\nGAfazg4ZFzoIH5TEPWCEAp7M9CVvtiw1SeA/zjewp2k=\n-----END EC PRIVATE KEY-----\n"
  },
  {
    "path": "examples/puppet/ca.json.erb",
    "content": "{\n    \"root\": \"/usr/local/lib/step/.step/secrets/root_ca.crt\",\n    \"crt\": \"/usr/local/lib/step/.step/secrets/intermediate_ca.crt\",\n    \"key\": \"/usr/local/lib/step/.step/secrets/intermediate_ca_key\",\n    \"password\": \"password\",\n    \"address\": \":9000\",\n    \"dnsNames\": [\n        \"localhost\"\n    ],\n    \"logger\": {\n        \"format\": \"text\"\n    },\n    \"authority\": {\n        \"provisioners\": [\n            {\n                \"name\": \"mariano@smallstep.com\",\n                \"type\": \"jwk\",\n                \"key\": {\n                    \"use\": \"sig\",\n                    \"kty\": \"EC\",\n                    \"kid\": \"DmAtZt2EhmZr_iTJJ387fr4Md2NbzMXGdXQNW1UWPXk\",\n                    \"crv\": \"P-256\",\n                    \"alg\": \"ES256\",\n                    \"x\": \"jXoO1j4CXxoTC32pNzkVC8l6k2LfP0k5ndhJZmcdVbk\",\n                    \"y\": \"c3JDL4GTFxJWHa8EaHdMh4QgwMh64P2_AGWrD0ADXcI\"\n                },\n                \"encryptedKey\": \"eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJjdHkiOiJqd2sranNvbiIsImVuYyI6IkEyNTZHQ00iLCJwMmMiOjEwMDAwMCwicDJzIjoiOTFVWjdzRGw3RlNXcldfX1I1NUh3USJ9.FcWtrBDNgrkA33G9Ll9sXh1cPF-3jVXeYe1FLmSDc_Q2PmfLOPvJOA.0ZoN32ayaRWnufJb.WrkffMmDLWiq1-2kn-w7-kVBGW12gjNCBHNHB1hyEdED0rWH1YWpKd8FjoOACdJyLhSn4kAS3Lw5AH7fvO27A48zzvoxZU5EgSm5HG9IjkIH-LBJ-v79ShkpmPylchgjkFhxa5epD11OIK4rFmI7s-0BCjmJokLR_DZBhDMw2khGnsr_MEOfAz9UnqXaQ4MIy8eT52xUpx68gpWFlz2YP3EqiYyNEv0PpjMtyP5lO2i8-p8BqvuJdus9H3fO5Dg-1KVto1wuqh4BQ2JKTauv60QAnM_4sdxRHku3F_nV64SCrZfDvnN2ve21raFROtyXaqHZhN6lyoPxDncy8v4.biaOblEe0N-gMpJyFZ-3-A\"\n            },\n            {\n                \"name\": \"mike@smallstep.com\",\n                \"type\": \"jwk\",\n                \"key\": {\n                    \"use\": \"sig\",\n                    \"kty\": \"EC\",\n                    \"kid\": \"YYNxZ0rq0WsT2MlqLCWvgme3jszkmt99KjoGEJJwAKs\",\n                    \"crv\": \"P-256\",\n                    \"alg\": \"ES256\",\n                    \"x\": \"LsI8nHBflc-mrCbRqhl8d3hSl5sYuSM1AbXBmRfznyg\",\n                    \"y\": \"F99LoOvi7z-ZkumsgoHIhodP8q9brXe4bhF3szK-c_w\"\n                },\n                \"encryptedKey\": \"eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJjdHkiOiJqd2sranNvbiIsImVuYyI6IkEyNTZHQ00iLCJwMmMiOjEwMDAwMCwicDJzIjoiVERQS2dzcEItTUR4ZDJxTGo0VlpwdyJ9.2_j0cZgTm2eFkZ-hrtr1hBIvLxN0w3TZhbX0Jrrq7vBMaywhgFcGTA.mCasZCbZJ-JT7vjA.bW052WDKSf_ueEXq1dyxLq0n3qXWRO-LXr7OzBLdUKWKSBGQrzqS5KJWqdUCPoMIHTqpwYvm-iD6uFlcxKBYxnsAG_hoq_V3icvvwNQQSd_q7Thxr2_KtPIDJWNuX1t5qXp11hkgb-8d5HO93CmN7xNDG89pzSUepT6RYXOZ483mP5fre9qzkfnrjx3oPROCnf3SnIVUvqk7fwfXuniNsg3NrNqncHYUQNReiq3e9I1R60w0ZQTvIReY7-zfiq7iPgVqmu5I7XGgFK4iBv0L7UOEora65b4hRWeLxg5t7OCfUqrS9yxAk8FdjFb9sEfjopWViPRepB0dYPH8dVI.fb6-7XWqp0j6CR9Li0NI-Q\",\n                \"claims\": {\n                    \"minTLSCertDuration\": \"60s\",\n                    \"defaultTLSCertDuration\": \"120s\"\n                }\n            }\n        ]\n    },\n    \"tls\": {\n        \"cipherSuites\": [\n            \"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305\",\n            \"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256\"\n        ],\n        \"minVersion\": 1.2,\n        \"maxVersion\": 1.2,\n        \"renegotiation\": false\n    }\n}\n"
  },
  {
    "path": "examples/puppet/defaults.json.erb",
    "content": "{\n    \"ca-url\": \"ca.smallstep.com:8080\",\n    \"root\": \"/usr/local/lib/step/.step/secrets/root_ca.crt\"\n}\n"
  },
  {
    "path": "examples/puppet/step.pp",
    "content": "# smallstep package configuration\nclass step(\n  $version = false,\n) {\n  if !$version {\n    fail(\"class ${name}: version cannot be empty\")\n  }\n\n  $pkg = \"step_${version}_linux_amd64.tar.gz\"\n  $download_url = \"https://github.com/smallstep/cli/releases/download/v${version}/step_${version}_linux_amd64.tar.gz\"\n  $step_exec = '/opt/smallstep/bin/step'\n\n  exec {\n    'download/update smallstep':\n      command => \"/usr/bin/curl --fail -o /tmp/${pkg} ${download_url} && /bin/tar -xzvf /tmp/${pkg} -C /opt\",\n      unless  => \"/usr/bin/which ${step_exec} && ${step_exec} version | grep ${version}\",\n      user    => 'step',\n      require => File['/opt/smallstep'];\n  }\n\n  file {\n    '/opt/smallstep':\n      ensure  => directory,\n      mode    => '0755',\n      owner   => 'step';\n    '/usr/local/lib/step':\n      ensure  => directory,\n      mode    => '0755',\n      owner   => 'step';\n    '/usr/local/lib/step/.step':\n      ensure  => directory,\n      mode    => '0755',\n      owner   => 'step';\n    '/usr/local/lib/step/.step/secrets':\n      ensure  => directory,\n      mode    => '0644',\n      owner   => 'step';\n    '/usr/local/lib/step/.step/config':\n      ensure  => directory,\n      mode    => '0755',\n      owner   => 'step';\n  }\n\n  group { 'step':\n      ensure => present,\n      gid    => $::step_id,\n  }\n\n  user { 'step':\n      ensure     => present,\n      gid        => 'puppet',\n      home       => '/usr/local/lib/step',\n      managehome => false,\n      uid        => $::step_id,\n  }\n}\n"
  },
  {
    "path": "examples/puppet/step_ca.pp",
    "content": "# step_ca package configuration\nclass step_ca(\n  $version = false,\n) {\n  if !$version {\n    fail(\"class ${name}: version cannot be empty\")\n  }\n\n  $pkg = \"step_${version}_linux_amd64.tar.gz\"\n  $download_url = \"https://github.com/smallstep/certificates/releases/download/v${version}/step-certificates_${version}_linux_amd64.tar.gz\"\n  $step_ca_exec = '/opt/smallstep/bin/step-ca'\n\n  exec {\n    'download/update smallstep':\n      command => \"/usr/bin/curl --fail -o /tmp/${pkg} ${download_url} && /bin/tar -xzvf /tmp/${pkg} -C /opt\",\n      unless  => \"/usr/bin/which ${step_exec} && ${step_exec} version | grep ${version}\",\n      user    => 'step',\n      require => File['/opt/smallstep'];\n  }\n\n  file {\n    '/usr/local/lib/step/.step':\n      ensure  => directory,\n      mode    => '0755',\n      owner   => 'step';\n    '/usr/local/lib/step/.step/secrets':\n      ensure  => directory,\n      mode    => '0644',\n      owner   => 'step';\n    '/usr/local/lib/step/.step/secrets/root_ca.crt': # Get this from Hiera.\n      ensure  => file,\n      mode    => '0644',\n      owner   => 'step';\n    '/usr/local/lib/step/.step/secrets/intermediate_ca.crt': # Get this from Hiera.\n      ensure  => file,\n      mode    => '0644',\n      owner   => 'step';\n    '/usr/local/lib/step/.step/secrets/intermediate_ca_key': # Get this from Hiera.\n      ensure  => file,\n      mode    => '0644',\n      owner   => 'step';\n    '/usr/local/lib/step/.step/secrets/intermediate_pass': # Get this from Hiera.\n      ensure  => file,\n      mode    => '0644',\n      owner   => 'step';\n    '/usr/local/lib/step/.step/config':\n      ensure  => directory,\n      mode    => '0755',\n      owner   => 'step';\n    '/usr/local/lib/step/.step/config/ca.json': # Fill from template in repo.\n      ensure  => file,\n      content => template('ca.json.erb'),\n      mode    => '0755',\n      owner   => 'step';\n    '/usr/local/lib/step/.step/config/ca.json': # Fill from template in repo.\n      ensure  => file,\n      content => template('defaults.json.erb'),\n      mode    => '0755',\n      owner   => 'step';\n  }\n\n  service { $name:\n    ensure    => running,\n    start => \"${step_ca_exec} /usr/local/lib/step/.step/config/ca.json --password-file /usr/local/lib/step/.step/secrets/intermediate_pass\",\n    provider  => 'systemd',\n  }\n}\n"
  },
  {
    "path": "examples/puppet/tls_server.pp",
    "content": "# step package configuration\nclass tls_server(\n  $version = false,\n) {\n  if !$version {\n    fail(\"class ${name}: version cannot be empty\")\n  }\n\n  file {\n    '/usr/local/lib/step/.step/secrets/provisioner_pupppet_pass': # Get this from Hiera.\n      ensure  => file,\n      mode    => '0644',\n      owner   => 'step';\n  }\n\n  $step = \"/opt/smallstep/bin/step\"\n  $step_path = \"/usr/local/lib/step/.step\"\n  $secrets = \"${step_path}/usr/local/lib/step/.step\"\n  service { $name:\n    ensure    => running,\n    start => \"/usr/local/bin/tls_server --token $(${step} token foo.com --ca-url=ca.smallstep.com --root=${secrets}/root_ca.crt --password-file=${secrets}/intermediate_pass)\",\n    provider  => 'systemd',\n  }\n}\n"
  },
  {
    "path": "go.mod",
    "content": "module github.com/smallstep/certificates\n\ngo 1.25.0\n\nrequire (\n\tcloud.google.com/go/longrunning v0.8.0\n\tcloud.google.com/go/security v1.19.2\n\tgithub.com/Masterminds/sprig/v3 v3.3.0\n\tgithub.com/ccoveille/go-safecast/v2 v2.0.0\n\tgithub.com/coreos/go-oidc/v3 v3.17.0\n\tgithub.com/coreos/go-systemd/v22 v22.7.0\n\tgithub.com/dgraph-io/badger v1.6.2\n\tgithub.com/dgraph-io/badger/v2 v2.2007.4\n\tgithub.com/fxamacker/cbor/v2 v2.9.0\n\tgithub.com/go-chi/chi/v5 v5.2.5\n\tgithub.com/go-jose/go-jose/v3 v3.0.4\n\tgithub.com/google/go-cmp v0.7.0\n\tgithub.com/google/go-tpm v0.9.8\n\tgithub.com/google/uuid v1.6.0\n\tgithub.com/googleapis/gax-go/v2 v2.18.0\n\tgithub.com/hashicorp/vault/api v1.22.0\n\tgithub.com/hashicorp/vault/api/auth/approle v0.11.0\n\tgithub.com/hashicorp/vault/api/auth/aws v0.11.0\n\tgithub.com/hashicorp/vault/api/auth/kubernetes v0.10.0\n\tgithub.com/newrelic/go-agent/v3 v3.42.0\n\tgithub.com/pkg/errors v0.9.1\n\tgithub.com/prometheus/client_golang v1.23.2\n\tgithub.com/rs/xid v1.6.0\n\tgithub.com/sirupsen/logrus v1.9.4\n\tgithub.com/slackhq/nebula v1.10.3\n\tgithub.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262\n\tgithub.com/smallstep/cli-utils v0.12.2\n\tgithub.com/smallstep/go-attestation v0.4.4-0.20241119153605-2306d5b464ca\n\tgithub.com/smallstep/linkedca v0.25.0\n\tgithub.com/smallstep/nosql v0.8.0\n\tgithub.com/smallstep/pkcs7 v0.2.1\n\tgithub.com/smallstep/scep v0.0.0-20250318231241-a25cabb69492\n\tgithub.com/stretchr/testify v1.11.1\n\tgithub.com/urfave/cli v1.22.17\n\tgo.step.sm/crypto v0.77.1\n\tgo.uber.org/mock v0.6.0\n\tgolang.org/x/crypto v0.49.0\n\tgolang.org/x/net v0.52.0\n\tgolang.org/x/sync v0.20.0\n\tgoogle.golang.org/api v0.271.0\n\tgoogle.golang.org/grpc v1.79.3\n\tgoogle.golang.org/protobuf v1.36.11\n)\n\nrequire (\n\tcloud.google.com/go v0.123.0 // indirect\n\tcloud.google.com/go/auth v0.18.2 // indirect\n\tcloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect\n\tcloud.google.com/go/compute/metadata v0.9.0 // indirect\n\tcloud.google.com/go/iam v1.5.3 // indirect\n\tcloud.google.com/go/kms v1.26.0 // indirect\n\tdario.cat/mergo v1.0.2 // indirect\n\tfilippo.io/bigmod v0.1.0 // indirect\n\tfilippo.io/edwards25519 v1.2.0 // indirect\n\tgithub.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect\n\tgithub.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 // indirect\n\tgithub.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 // indirect\n\tgithub.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect\n\tgithub.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.4.0 // indirect\n\tgithub.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0 // indirect\n\tgithub.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 // indirect\n\tgithub.com/Masterminds/goutils v1.1.1 // indirect\n\tgithub.com/Masterminds/semver/v3 v3.3.1 // indirect\n\tgithub.com/ThalesIgnite/crypto11 v1.2.5 // indirect\n\tgithub.com/aws/aws-sdk-go v1.55.7 // indirect\n\tgithub.com/aws/aws-sdk-go-v2 v1.41.4 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/config v1.32.12 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/credentials v1.19.12 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/kms v1.50.3 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/signin v1.0.8 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/sso v1.30.13 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/sts v1.41.9 // indirect\n\tgithub.com/aws/smithy-go v1.24.2 // indirect\n\tgithub.com/beorn7/perks v1.0.1 // indirect\n\tgithub.com/cenkalti/backoff/v4 v4.3.0 // indirect\n\tgithub.com/cespare/xxhash v1.1.0 // indirect\n\tgithub.com/cespare/xxhash/v2 v2.3.0 // indirect\n\tgithub.com/chzyer/readline v1.5.1 // indirect\n\tgithub.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect\n\tgithub.com/davecgh/go-spew v1.1.1 // indirect\n\tgithub.com/dgraph-io/ristretto v0.1.0 // indirect\n\tgithub.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect\n\tgithub.com/dustin/go-humanize v1.0.1 // indirect\n\tgithub.com/fatih/color v1.18.0 // indirect\n\tgithub.com/felixge/httpsnoop v1.0.4 // indirect\n\tgithub.com/go-jose/go-jose/v4 v4.1.3 // indirect\n\tgithub.com/go-logr/logr v1.4.3 // indirect\n\tgithub.com/go-logr/stdr v1.2.2 // indirect\n\tgithub.com/go-piv/piv-go/v2 v2.5.0 // indirect\n\tgithub.com/go-sql-driver/mysql v1.9.3 // indirect\n\tgithub.com/golang-jwt/jwt/v5 v5.3.0 // indirect\n\tgithub.com/golang/glog v1.2.5 // indirect\n\tgithub.com/golang/protobuf v1.5.4 // indirect\n\tgithub.com/golang/snappy v0.0.4 // indirect\n\tgithub.com/google/btree v1.1.2 // indirect\n\tgithub.com/google/certificate-transparency-go v1.1.7 // indirect\n\tgithub.com/google/go-tpm-tools v0.4.7 // indirect\n\tgithub.com/google/go-tspi v0.3.0 // indirect\n\tgithub.com/google/s2a-go v0.1.9 // indirect\n\tgithub.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect\n\tgithub.com/hashicorp/errwrap v1.1.0 // indirect\n\tgithub.com/hashicorp/go-cleanhttp v0.5.2 // indirect\n\tgithub.com/hashicorp/go-hclog v1.6.3 // indirect\n\tgithub.com/hashicorp/go-multierror v1.1.1 // indirect\n\tgithub.com/hashicorp/go-retryablehttp v0.7.8 // indirect\n\tgithub.com/hashicorp/go-rootcerts v1.0.2 // indirect\n\tgithub.com/hashicorp/go-secure-stdlib/awsutil v0.3.0 // indirect\n\tgithub.com/hashicorp/go-secure-stdlib/parseutil v0.2.0 // indirect\n\tgithub.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect\n\tgithub.com/hashicorp/go-sockaddr v1.0.7 // indirect\n\tgithub.com/hashicorp/go-uuid v1.0.2 // indirect\n\tgithub.com/hashicorp/hcl v1.0.1-vault-7 // indirect\n\tgithub.com/huandu/xstrings v1.5.0 // indirect\n\tgithub.com/jackc/pgpassfile v1.0.0 // indirect\n\tgithub.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect\n\tgithub.com/jackc/pgx/v5 v5.8.0 // indirect\n\tgithub.com/jackc/puddle/v2 v2.2.2 // indirect\n\tgithub.com/jmespath/go-jmespath v0.4.0 // indirect\n\tgithub.com/klauspost/compress v1.18.0 // indirect\n\tgithub.com/kylelemons/godebug v1.1.0 // indirect\n\tgithub.com/manifoldco/promptui v0.9.0 // indirect\n\tgithub.com/mattn/go-colorable v0.1.14 // indirect\n\tgithub.com/mattn/go-isatty v0.0.20 // indirect\n\tgithub.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect\n\tgithub.com/miekg/pkcs11 v1.1.2 // indirect\n\tgithub.com/mitchellh/copystructure v1.2.0 // indirect\n\tgithub.com/mitchellh/go-homedir v1.1.0 // indirect\n\tgithub.com/mitchellh/mapstructure v1.5.0 // indirect\n\tgithub.com/mitchellh/reflectwalk v1.0.2 // indirect\n\tgithub.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect\n\tgithub.com/peterbourgon/diskv/v3 v3.0.1 // indirect\n\tgithub.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect\n\tgithub.com/pmezard/go-difflib v1.0.0 // indirect\n\tgithub.com/prometheus/client_model v0.6.2 // indirect\n\tgithub.com/prometheus/common v0.67.5 // indirect\n\tgithub.com/prometheus/procfs v0.19.2 // indirect\n\tgithub.com/russross/blackfriday/v2 v2.1.0 // indirect\n\tgithub.com/ryanuber/go-glob v1.0.0 // indirect\n\tgithub.com/schollz/jsonstore v1.1.0 // indirect\n\tgithub.com/shopspring/decimal v1.4.0 // indirect\n\tgithub.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect\n\tgithub.com/spf13/cast v1.7.0 // indirect\n\tgithub.com/thales-e-security/pool v0.0.2 // indirect\n\tgithub.com/x448/float16 v0.8.4 // indirect\n\tgo.etcd.io/bbolt v1.4.3 // indirect\n\tgo.opentelemetry.io/auto/sdk v1.2.1 // indirect\n\tgo.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 // indirect\n\tgo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect\n\tgo.opentelemetry.io/otel v1.39.0 // indirect\n\tgo.opentelemetry.io/otel/metric v1.39.0 // indirect\n\tgo.opentelemetry.io/otel/trace v1.39.0 // indirect\n\tgo.yaml.in/yaml/v2 v2.4.3 // indirect\n\tgolang.org/x/mod v0.33.0 // indirect\n\tgolang.org/x/oauth2 v0.36.0 // indirect\n\tgolang.org/x/sys v0.42.0 // indirect\n\tgolang.org/x/term v0.41.0 // indirect\n\tgolang.org/x/text v0.35.0 // indirect\n\tgolang.org/x/time v0.15.0 // indirect\n\tgolang.org/x/tools v0.42.0 // indirect\n\tgoogle.golang.org/genproto v0.0.0-20260217215200-42d3e9bedb6d // indirect\n\tgoogle.golang.org/genproto/googleapis/api v0.0.0-20260217215200-42d3e9bedb6d // indirect\n\tgoogle.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect\n\tgoogle.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 // indirect\n\tgopkg.in/yaml.v3 v3.0.1 // indirect\n)\n"
  },
  {
    "path": "go.sum",
    "content": "cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE=\ncloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU=\ncloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM=\ncloud.google.com/go/auth v0.18.2/go.mod h1:xD+oY7gcahcu7G2SG2DsBerfFxgPAJz17zz2joOFF3M=\ncloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=\ncloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=\ncloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=\ncloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=\ncloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc=\ncloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU=\ncloud.google.com/go/kms v1.26.0 h1:cK9mN2cf+9V63D3H1f6koxTatWy39aTI/hCjz1I+adU=\ncloud.google.com/go/kms v1.26.0/go.mod h1:pHKOdFJm63hxBsiPkYtowZPltu9dW0MWvBa6IA4HM58=\ncloud.google.com/go/longrunning v0.8.0 h1:LiKK77J3bx5gDLi4SMViHixjD2ohlkwBi+mKA7EhfW8=\ncloud.google.com/go/longrunning v0.8.0/go.mod h1:UmErU2Onzi+fKDg2gR7dusz11Pe26aknR4kHmJJqIfk=\ncloud.google.com/go/security v1.19.2 h1:cF3FkCRRbRC1oXuaGZFl3qU2sdu2gP3iOAHKzL5y04Y=\ncloud.google.com/go/security v1.19.2/go.mod h1:KXmf64mnOsLVKe8mk/bZpU1Rsvxqc0Ej0A6tgCeN93w=\ndario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=\ndario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=\nfilippo.io/bigmod v0.1.0 h1:UNzDk7y9ADKST+axd9skUpBQeW7fG2KrTZyOE4uGQy8=\nfilippo.io/bigmod v0.1.0/go.mod h1:OjOXDNlClLblvXdwgFFOQFJEocLhhtai8vGLy0JCZlI=\nfilippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=\nfilippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=\ngithub.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 h1:cTp8I5+VIoKjsnZuH8vjyaysT/ses3EvZeaV/1UkF2M=\ngithub.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8=\ngithub.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 h1:fou+2+WFTib47nS+nz/ozhEBnvU96bKHy6LjRsY4E28=\ngithub.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0/go.mod h1:t76Ruy8AHvUAC8GfMWJMa0ElSbuIcO03NLpynfbgsPA=\ngithub.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4=\ngithub.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1/go.mod h1:IYus9qsFobWIc2YVwe/WPjcnyCkPKtnHAqUYeebc8z0=\ngithub.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY=\ngithub.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8=\ngithub.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA=\ngithub.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI=\ngithub.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.4.0 h1:E4MgwLBGeVB5f2MdcIVD3ELVAWpr+WD6MUe1i+tM/PA=\ngithub.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.4.0/go.mod h1:Y2b/1clN4zsAoUd/pgNAQHjLDnTis/6ROkUfyob6psM=\ngithub.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0 h1:nCYfgcSyHZXJI8J0IWE5MsCGlb2xp9fJiXyxWgmOFg4=\ngithub.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0/go.mod h1:ucUjca2JtSZboY8IoUqyQyuuXvwbMBVwFOm0vdQPNhA=\ngithub.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM=\ngithub.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE=\ngithub.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs=\ngithub.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk=\ngithub.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=\ngithub.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=\ngithub.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=\ngithub.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=\ngithub.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4=\ngithub.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=\ngithub.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs=\ngithub.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0=\ngithub.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=\ngithub.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=\ngithub.com/ThalesIgnite/crypto11 v1.2.5 h1:1IiIIEqYmBvUYFeMnHqRft4bwf/O36jryEUpY+9ef8E=\ngithub.com/ThalesIgnite/crypto11 v1.2.5/go.mod h1:ILDKtnCKiQ7zRoNxcp36Y1ZR8LBPmR2E23+wTQe/MlE=\ngithub.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=\ngithub.com/aws/aws-sdk-go v1.34.0/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0=\ngithub.com/aws/aws-sdk-go v1.55.7 h1:UJrkFq7es5CShfBwlWAC8DA077vp8PyVbQd3lqLiztE=\ngithub.com/aws/aws-sdk-go v1.55.7/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=\ngithub.com/aws/aws-sdk-go-v2 v1.41.4 h1:10f50G7WyU02T56ox1wWXq+zTX9I1zxG46HYuG1hH/k=\ngithub.com/aws/aws-sdk-go-v2 v1.41.4/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o=\ngithub.com/aws/aws-sdk-go-v2/config v1.32.12 h1:O3csC7HUGn2895eNrLytOJQdoL2xyJy0iYXhoZ1OmP0=\ngithub.com/aws/aws-sdk-go-v2/config v1.32.12/go.mod h1:96zTvoOFR4FURjI+/5wY1vc1ABceROO4lWgWJuxgy0g=\ngithub.com/aws/aws-sdk-go-v2/credentials v1.19.12 h1:oqtA6v+y5fZg//tcTWahyN9PEn5eDU/Wpvc2+kJ4aY8=\ngithub.com/aws/aws-sdk-go-v2/credentials v1.19.12/go.mod h1:U3R1RtSHx6NB0DvEQFGyf/0sbrpJrluENHdPy1j/3TE=\ngithub.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20 h1:zOgq3uezl5nznfoK3ODuqbhVg1JzAGDUhXOsU0IDCAo=\ngithub.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20/go.mod h1:z/MVwUARehy6GAg/yQ1GO2IMl0k++cu1ohP9zo887wE=\ngithub.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20 h1:CNXO7mvgThFGqOFgbNAP2nol2qAWBOGfqR/7tQlvLmc=\ngithub.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20/go.mod h1:oydPDJKcfMhgfcgBUZaG+toBbwy8yPWubJXBVERtI4o=\ngithub.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20 h1:tN6W/hg+pkM+tf9XDkWUbDEjGLb+raoBMFsTodcoYKw=\ngithub.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20/go.mod h1:YJ898MhD067hSHA6xYCx5ts/jEd8BSOLtQDL3iZsvbc=\ngithub.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 h1:qYQ4pzQ2Oz6WpQ8T3HvGHnZydA72MnLuFK9tJwmrbHw=\ngithub.com/aws/aws-sdk-go-v2/internal/ini v1.8.6/go.mod h1:O3h0IK87yXci+kg6flUKzJnWeziQUKciKrLjcatSNcY=\ngithub.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 h1:5EniKhLZe4xzL7a+fU3C2tfUN4nWIqlLesfrjkuPFTY=\ngithub.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI=\ngithub.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20 h1:2HvVAIq+YqgGotK6EkMf+KIEqTISmTYh5zLpYyeTo1Y=\ngithub.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20/go.mod h1:V4X406Y666khGa8ghKmphma/7C0DAtEQYhkq9z4vpbk=\ngithub.com/aws/aws-sdk-go-v2/service/kms v1.50.3 h1:s/zDSG/a/Su9aX+v0Ld9cimUCdkr5FWPmBV8owaEbZY=\ngithub.com/aws/aws-sdk-go-v2/service/kms v1.50.3/go.mod h1:/iSgiUor15ZuxFGQSTf3lA2FmKxFsQoc2tADOarQBSw=\ngithub.com/aws/aws-sdk-go-v2/service/signin v1.0.8 h1:0GFOLzEbOyZABS3PhYfBIx2rNBACYcKty+XGkTgw1ow=\ngithub.com/aws/aws-sdk-go-v2/service/signin v1.0.8/go.mod h1:LXypKvk85AROkKhOG6/YEcHFPoX+prKTowKnVdcaIxE=\ngithub.com/aws/aws-sdk-go-v2/service/sso v1.30.13 h1:kiIDLZ005EcKomYYITtfsjn7dtOwHDOFy7IbPXKek2o=\ngithub.com/aws/aws-sdk-go-v2/service/sso v1.30.13/go.mod h1:2h/xGEowcW/g38g06g3KpRWDlT+OTfxxI0o1KqayAB8=\ngithub.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17 h1:jzKAXIlhZhJbnYwHbvUQZEB8KfgAEuG0dc08Bkda7NU=\ngithub.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17/go.mod h1:Al9fFsXjv4KfbzQHGe6V4NZSZQXecFcvaIF4e70FoRA=\ngithub.com/aws/aws-sdk-go-v2/service/sts v1.41.9 h1:Cng+OOwCHmFljXIxpEVXAGMnBia8MSU6Ch5i9PgBkcU=\ngithub.com/aws/aws-sdk-go-v2/service/sts v1.41.9/go.mod h1:LrlIndBDdjA/EeXeyNBle+gyCwTlizzW5ycgWnvIxkk=\ngithub.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng=\ngithub.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=\ngithub.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=\ngithub.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=\ngithub.com/ccoveille/go-safecast/v2 v2.0.0 h1:+5eyITXAUj3wMjad6cRVJKGnC7vDS55zk0INzJagub0=\ngithub.com/ccoveille/go-safecast/v2 v2.0.0/go.mod h1:JIYA4CAR33blIDuE6fSwCp2sz1oOBahXnvmdBhOAABs=\ngithub.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=\ngithub.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=\ngithub.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=\ngithub.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=\ngithub.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=\ngithub.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=\ngithub.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM=\ngithub.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=\ngithub.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=\ngithub.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI=\ngithub.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk=\ngithub.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=\ngithub.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04=\ngithub.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=\ngithub.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 h1:6xNmx7iTtyBRev0+D/Tv1FZd4SCg8axKApyNyRsAt/w=\ngithub.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI=\ngithub.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=\ngithub.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=\ngithub.com/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc=\ngithub.com/coreos/go-oidc/v3 v3.17.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8=\ngithub.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=\ngithub.com/coreos/go-systemd/v22 v22.7.0 h1:LAEzFkke61DFROc7zNLX/WA2i5J8gYqe0rSj9KI28KA=\ngithub.com/coreos/go-systemd/v22 v22.7.0/go.mod h1:xNUYtjHu2EDXbsxz1i41wouACIwT7Ybq9o0BQhMwD0w=\ngithub.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=\ngithub.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/dgraph-io/badger v1.6.2 h1:mNw0qs90GVgGGWylh0umH5iag1j6n/PeJtNvL6KY/x8=\ngithub.com/dgraph-io/badger v1.6.2/go.mod h1:JW2yswe3V058sS0kZ2h/AXeDSqFjxnZcRrVH//y2UQE=\ngithub.com/dgraph-io/badger/v2 v2.2007.4 h1:TRWBQg8UrlUhaFdco01nO2uXwzKS7zd+HVdwV/GHc4o=\ngithub.com/dgraph-io/badger/v2 v2.2007.4/go.mod h1:vSw/ax2qojzbN6eXHIx6KPKtCSHJN/Uz0X0VPruTIhk=\ngithub.com/dgraph-io/ristretto v0.0.2/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E=\ngithub.com/dgraph-io/ristretto v0.0.3-0.20200630154024-f66de99634de/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E=\ngithub.com/dgraph-io/ristretto v0.1.0 h1:Jv3CGQHp9OjuMBSne1485aDpUkTKEcUqF+jm/LuerPI=\ngithub.com/dgraph-io/ristretto v0.1.0/go.mod h1:fux0lOrBhrVCJd3lcTHsIJhq1T2rokOu6v9Vcb3Q9ug=\ngithub.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=\ngithub.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y=\ngithub.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=\ngithub.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=\ngithub.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=\ngithub.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=\ngithub.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA=\ngithub.com/envoyproxy/go-control-plane/envoy v1.36.0 h1:yg/JjO5E7ubRyKX3m07GF3reDNEnfOboJ0QySbH736g=\ngithub.com/envoyproxy/go-control-plane/envoy v1.36.0/go.mod h1:ty89S1YCCVruQAm9OtKeEkQLTb+Lkz0k8v9W0Oxsv98=\ngithub.com/envoyproxy/protoc-gen-validate v1.3.0 h1:TvGH1wof4H33rezVKWSpqKz5NXWg5VPuZ0uONDT6eb4=\ngithub.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA=\ngithub.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=\ngithub.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=\ngithub.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=\ngithub.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=\ngithub.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=\ngithub.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=\ngithub.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=\ngithub.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=\ngithub.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=\ngithub.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=\ngithub.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=\ngithub.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=\ngithub.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY=\ngithub.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=\ngithub.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=\ngithub.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=\ngithub.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=\ngithub.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=\ngithub.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=\ngithub.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=\ngithub.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=\ngithub.com/go-piv/piv-go/v2 v2.5.0 h1:w4KZ3GytEGZt8zm+S7olcIHZk0giL23xVqCa2HgwuqA=\ngithub.com/go-piv/piv-go/v2 v2.5.0/go.mod h1:ShZi74nnrWNQEdWzRUd/3cSig3uNOcEZp+EWl0oewnI=\ngithub.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=\ngithub.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=\ngithub.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=\ngithub.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=\ngithub.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=\ngithub.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=\ngithub.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=\ngithub.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=\ngithub.com/golang/glog v1.2.5 h1:DrW6hGnjIhtvhOIiAKT6Psh/Kd/ldepEa81DKeiRJ5I=\ngithub.com/golang/glog v1.2.5/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=\ngithub.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=\ngithub.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=\ngithub.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=\ngithub.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=\ngithub.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=\ngithub.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=\ngithub.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=\ngithub.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=\ngithub.com/google/certificate-transparency-go v1.0.21/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg=\ngithub.com/google/certificate-transparency-go v1.1.7 h1:IASD+NtgSTJLPdzkthwvAG1ZVbF2WtFg4IvoA68XGSw=\ngithub.com/google/certificate-transparency-go v1.1.7/go.mod h1:FSSBo8fyMVgqptbfF6j5p/XNdgQftAhSmXcIxV9iphE=\ngithub.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=\ngithub.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=\ngithub.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=\ngithub.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=\ngithub.com/google/go-configfs-tsm v0.3.3-0.20240919001351-b4b5b84fdcbc h1:SG12DWUUM5igxm+//YX5Yq4vhdoRnOG9HkCodkOn+YU=\ngithub.com/google/go-configfs-tsm v0.3.3-0.20240919001351-b4b5b84fdcbc/go.mod h1:EL1GTDFMb5PZQWDviGfZV9n87WeGTR/JUg13RfwkgRo=\ngithub.com/google/go-sev-guest v0.14.0 h1:dCb4F3YrHTtrDX3cYIPTifEDz7XagZmXQioxRBW4wOo=\ngithub.com/google/go-sev-guest v0.14.0/go.mod h1:SK9vW+uyfuzYdVN0m8BShL3OQCtXZe/JPF7ZkpD3760=\ngithub.com/google/go-tdx-guest v0.3.2-0.20241009005452-097ee70d0843 h1:+MoPobRN9HrDhGyn6HnF5NYo4uMBKaiFqAtf/D/OB4A=\ngithub.com/google/go-tdx-guest v0.3.2-0.20241009005452-097ee70d0843/go.mod h1:g/n8sKITIT9xRivBUbizo34DTsUm2nN2uU3A662h09g=\ngithub.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=\ngithub.com/google/go-tpm v0.9.8/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=\ngithub.com/google/go-tpm-tools v0.4.7 h1:J3ycC8umYxM9A4eF73EofRZu4BxY0jjQnUnkhIBbvws=\ngithub.com/google/go-tpm-tools v0.4.7/go.mod h1:gSyXTZHe3fgbzb6WEGd90QucmsnT1SRdlye82gH8QjQ=\ngithub.com/google/go-tspi v0.3.0 h1:ADtq8RKfP+jrTyIWIZDIYcKOMecRqNJFOew2IT0Inus=\ngithub.com/google/go-tspi v0.3.0/go.mod h1:xfMGI3G0PhxCdNVcYr1C4C+EizojDg/TXuX5by8CiHI=\ngithub.com/google/logger v1.1.1 h1:+6Z2geNxc9G+4D4oDO9njjjn2d0wN5d7uOo0vOIW1NQ=\ngithub.com/google/logger v1.1.1/go.mod h1:BkeJZ+1FhQ+/d087r4dzojEg1u2ZX+ZqG1jTUrLM+zQ=\ngithub.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=\ngithub.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/googleapis/enterprise-certificate-proxy v0.3.14 h1:yh8ncqsbUY4shRD5dA6RlzjJaT4hi3kII+zYw8wmLb8=\ngithub.com/googleapis/enterprise-certificate-proxy v0.3.14/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg=\ngithub.com/googleapis/gax-go/v2 v2.18.0 h1:jxP5Uuo3bxm3M6gGtV94P4lliVetoCB4Wk2x8QA86LI=\ngithub.com/googleapis/gax-go/v2 v2.18.0/go.mod h1:uSzZN4a356eRG985CzJ3WfbFSpqkLTjsnhWGJR6EwrE=\ngithub.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=\ngithub.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=\ngithub.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=\ngithub.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=\ngithub.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=\ngithub.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=\ngithub.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=\ngithub.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=\ngithub.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=\ngithub.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=\ngithub.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48=\ngithub.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw=\ngithub.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc=\ngithub.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=\ngithub.com/hashicorp/go-secure-stdlib/awsutil v0.3.0 h1:I8bynUKMh9I7JdwtW9voJ0xmHvBpxQtLjrMFDYmhOxY=\ngithub.com/hashicorp/go-secure-stdlib/awsutil v0.3.0/go.mod h1:oKHSQs4ivIfZ3fbXGQOop1XuDfdSb8RIsWTGaAanSfg=\ngithub.com/hashicorp/go-secure-stdlib/parseutil v0.2.0 h1:U+kC2dOhMFQctRfhK0gRctKAPTloZdMU5ZJxaesJ/VM=\ngithub.com/hashicorp/go-secure-stdlib/parseutil v0.2.0/go.mod h1:Ll013mhdmsVDuoIXVfBtvgGJsXDYkTw1kooNcoCXuE0=\ngithub.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts=\ngithub.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4=\ngithub.com/hashicorp/go-sockaddr v1.0.7 h1:G+pTkSO01HpR5qCxg7lxfsFEZaG+C0VssTy/9dbT+Fw=\ngithub.com/hashicorp/go-sockaddr v1.0.7/go.mod h1:FZQbEYa1pxkQ7WLpyXJ6cbjpT8q0YgQaK/JakXqGyWw=\ngithub.com/hashicorp/go-uuid v1.0.2 h1:cfejS+Tpcp13yd5nYHWDI6qVCny6wyX2Mt5SGur2IGE=\ngithub.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=\ngithub.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=\ngithub.com/hashicorp/hcl v1.0.1-vault-7 h1:ag5OxFVy3QYTFTJODRzTKVZ6xvdfLLCA1cy/Y6xGI0I=\ngithub.com/hashicorp/hcl v1.0.1-vault-7/go.mod h1:XYhtn6ijBSAj6n4YqAaf7RBPS4I06AItNorpy+MoQNM=\ngithub.com/hashicorp/vault/api v1.22.0 h1:+HYFquE35/B74fHoIeXlZIP2YADVboaPjaSicHEZiH0=\ngithub.com/hashicorp/vault/api v1.22.0/go.mod h1:IUZA2cDvr4Ok3+NtK2Oq/r+lJeXkeCrHRmqdyWfpmGM=\ngithub.com/hashicorp/vault/api/auth/approle v0.11.0 h1:ViUvgqoSTqHkMi1L1Rr/LnQ+PWiRaGUBGvx4UPfmKOw=\ngithub.com/hashicorp/vault/api/auth/approle v0.11.0/go.mod h1:v8ZqBRw+GP264ikIw2sEBKF0VT72MEhLWnZqWt3xEG8=\ngithub.com/hashicorp/vault/api/auth/aws v0.11.0 h1:lWdUxrzvPotg6idNr62al4w97BgI9xTDdzMCTViNH2s=\ngithub.com/hashicorp/vault/api/auth/aws v0.11.0/go.mod h1:PWqdH/xqaudapmnnGP9ip2xbxT/kRW2qEgpqiQff6Gc=\ngithub.com/hashicorp/vault/api/auth/kubernetes v0.10.0 h1:5rqWmUFxnu3S7XYq9dafURwBgabYDFzo2Wv+AMopPHs=\ngithub.com/hashicorp/vault/api/auth/kubernetes v0.10.0/go.mod h1:cZZmhF6xboMDmDbMY52oj2DKW6gS0cQ9g0pJ5XIXQ5U=\ngithub.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=\ngithub.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=\ngithub.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=\ngithub.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=\ngithub.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=\ngithub.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=\ngithub.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=\ngithub.com/jackc/pgx/v5 v5.8.0 h1:TYPDoleBBme0xGSAX3/+NujXXtpZn9HBONkQC7IEZSo=\ngithub.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw=\ngithub.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=\ngithub.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=\ngithub.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik=\ngithub.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=\ngithub.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=\ngithub.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=\ngithub.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=\ngithub.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU=\ngithub.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k=\ngithub.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=\ngithub.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=\ngithub.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=\ngithub.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=\ngithub.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=\ngithub.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=\ngithub.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=\ngithub.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=\ngithub.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=\ngithub.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\ngithub.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=\ngithub.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=\ngithub.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=\ngithub.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA=\ngithub.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg=\ngithub.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=\ngithub.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=\ngithub.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=\ngithub.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=\ngithub.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=\ngithub.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=\ngithub.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=\ngithub.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=\ngithub.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=\ngithub.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=\ngithub.com/miekg/pkcs11 v1.0.3-0.20190429190417-a667d056470f/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=\ngithub.com/miekg/pkcs11 v1.1.2 h1:/VxmeAX5qU6Q3EwafypogwWbYryHFmF2RpkJmw3m4MQ=\ngithub.com/miekg/pkcs11 v1.1.2/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=\ngithub.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=\ngithub.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=\ngithub.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=\ngithub.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=\ngithub.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=\ngithub.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=\ngithub.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=\ngithub.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=\ngithub.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=\ngithub.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=\ngithub.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=\ngithub.com/newrelic/go-agent/v3 v3.42.0 h1:aA2Ea1RT5eD59LtOS1KGFXSmaDs6kM3Jeqo7PpuQoFQ=\ngithub.com/newrelic/go-agent/v3 v3.42.0/go.mod h1:sCgxDCVydoKD/C4S8BFxDtmFHvdWHtaIz/a3kiyNB/k=\ngithub.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=\ngithub.com/peterbourgon/diskv/v3 v3.0.1 h1:x06SQA46+PKIUftmEujdwSEpIx8kR+M9eLYsUxeYveU=\ngithub.com/peterbourgon/diskv/v3 v3.0.1/go.mod h1:kJ5Ny7vLdARGU3WUuy6uzO6T0nb/2gWcT1JiBvRmb5o=\ngithub.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=\ngithub.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=\ngithub.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=\ngithub.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=\ngithub.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=\ngithub.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=\ngithub.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=\ngithub.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=\ngithub.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=\ngithub.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=\ngithub.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=\ngithub.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=\ngithub.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=\ngithub.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=\ngithub.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=\ngithub.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=\ngithub.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=\ngithub.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=\ngithub.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=\ngithub.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk=\ngithub.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=\ngithub.com/schollz/jsonstore v1.1.0 h1:WZBDjgezFS34CHI+myb4s8GGpir3UMpy7vWoCeO0n6E=\ngithub.com/schollz/jsonstore v1.1.0/go.mod h1:15c6+9guw8vDRyozGjN3FoILt0wpruJk9Pi66vjaZfg=\ngithub.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=\ngithub.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=\ngithub.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=\ngithub.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=\ngithub.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=\ngithub.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=\ngithub.com/slackhq/nebula v1.10.3 h1:EstYj8ODEcv6T0R9X5BVq1zgWZnyU5gtPzk99QF1PMU=\ngithub.com/slackhq/nebula v1.10.3/go.mod h1:IL5TUQm4x9IFx2kCKPYm1gP47pwd5b8QGnnBH2RHnvs=\ngithub.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262 h1:unQFBIznI+VYD1/1fApl1A+9VcBk+9dcqGfnePY87LY=\ngithub.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262/go.mod h1:MyOHs9Po2fbM1LHej6sBUT8ozbxmMOFG+E+rx/GSGuc=\ngithub.com/smallstep/cli-utils v0.12.2 h1:lGzM9PJrH/qawbzMC/s2SvgLdJPKDWKwKzx9doCVO+k=\ngithub.com/smallstep/cli-utils v0.12.2/go.mod h1:uCPqefO29goHLGqFnwk0i8W7XJu18X3WHQFRtOm/00Y=\ngithub.com/smallstep/go-attestation v0.4.4-0.20241119153605-2306d5b464ca h1:VX8L0r8vybH0bPeaIxh4NQzafKQiqvlOn8pmOXbFLO4=\ngithub.com/smallstep/go-attestation v0.4.4-0.20241119153605-2306d5b464ca/go.mod h1:vNAduivU014fubg6ewygkAvQC0IQVXqdc8vaGl/0er4=\ngithub.com/smallstep/linkedca v0.25.0 h1:txT9QHGbCsJq0MhAghBq7qhurGY727tQuqUi+n4BVBo=\ngithub.com/smallstep/linkedca v0.25.0/go.mod h1:Q3jVAauFKNlF86W5/RFtgQeyDKz98GL/KN3KG4mJOvc=\ngithub.com/smallstep/nosql v0.8.0 h1:FBTCUfKPmWYbrozW+RBKu+fnvbn+zr5rVli/XB4Jp4A=\ngithub.com/smallstep/nosql v0.8.0/go.mod h1:5dUpNotHLHhOUapP0PLBVVfp3tG1DFC31VRccg+Cqwo=\ngithub.com/smallstep/pkcs7 v0.2.1 h1:6Kfzr/QizdIuB6LSv8y1LJdZ3aPSfTNhTLqAx9CTLfA=\ngithub.com/smallstep/pkcs7 v0.2.1/go.mod h1:RcXHsMfL+BzH8tRhmrF1NkkpebKpq3JEM66cOFxanf0=\ngithub.com/smallstep/scep v0.0.0-20250318231241-a25cabb69492 h1:k23+s51sgYix4Zgbvpmy+1ZgXLjr4ZTkBTqXmpnImwA=\ngithub.com/smallstep/scep v0.0.0-20250318231241-a25cabb69492/go.mod h1:QQhwLqCS13nhv8L5ov7NgusowENUtXdEzdytjmJHdZQ=\ngithub.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=\ngithub.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=\ngithub.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=\ngithub.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=\ngithub.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=\ngithub.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w=\ngithub.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=\ngithub.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=\ngithub.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=\ngithub.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=\ngithub.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=\ngithub.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=\ngithub.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=\ngithub.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=\ngithub.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=\ngithub.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=\ngithub.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=\ngithub.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=\ngithub.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=\ngithub.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=\ngithub.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=\ngithub.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngithub.com/thales-e-security/pool v0.0.2 h1:RAPs4q2EbWsTit6tpzuvTFlgFRJ3S8Evf5gtvVDbmPg=\ngithub.com/thales-e-security/pool v0.0.2/go.mod h1:qtpMm2+thHtqhLzTwgDBj/OuNnMpupY8mv0Phz0gjhU=\ngithub.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=\ngithub.com/urfave/cli v1.22.17 h1:SYzXoiPfQjHBbkYxbew5prZHS1TOLT3ierW8SYLqtVQ=\ngithub.com/urfave/cli v1.22.17/go.mod h1:b0ht0aqgH/6pBYzzxURyrM4xXNgsoT/n2ZzwQiEhNVo=\ngithub.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=\ngithub.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=\ngithub.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=\ngithub.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=\ngo.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=\ngo.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=\ngo.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=\ngo.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=\ngo.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ=\ngo.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo=\ngo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=\ngo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=\ngo.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=\ngo.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=\ngo.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=\ngo.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=\ngo.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=\ngo.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=\ngo.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=\ngo.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=\ngo.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=\ngo.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=\ngo.step.sm/crypto v0.77.1 h1:4EEqfKdv0egQ1lqz2RhnU8Jv6QgXZfrgoxWMqJF9aDs=\ngo.step.sm/crypto v0.77.1/go.mod h1:U/SsmEm80mNnfD5WIkbhuW/B1eFp3fgFvdXyDLpU1AQ=\ngo.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=\ngo.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=\ngo.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=\ngo.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=\ngo.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=\ngo.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=\ngo.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=\ngo.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=\ngolang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=\ngolang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=\ngolang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=\ngolang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=\ngolang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=\ngolang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=\ngolang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=\ngolang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=\ngolang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=\ngolang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=\ngolang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=\ngolang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=\ngolang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=\ngolang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=\ngolang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=\ngolang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=\ngolang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=\ngolang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=\ngolang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=\ngolang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=\ngolang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=\ngolang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=\ngolang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=\ngolang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=\ngolang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=\ngolang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=\ngolang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=\ngolang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=\ngolang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\ngolang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\ngolang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\ngolang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=\ngolang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=\ngolang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=\ngolang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=\ngolang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=\ngolang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=\ngolang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=\ngolang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=\ngolang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=\ngolang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=\ngolang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=\ngolang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=\ngolang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=\ngolang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=\ngolang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=\ngolang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=\ngolang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=\ngolang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=\ngolang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=\ngolang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=\ngolang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=\ngolang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=\ngolang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=\ngolang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=\ngolang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=\ngolang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=\ngolang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=\ngolang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=\ngonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=\ngoogle.golang.org/api v0.271.0 h1:cIPN4qcUc61jlh7oXu6pwOQqbJW2GqYh5PS6rB2C/JY=\ngoogle.golang.org/api v0.271.0/go.mod h1:CGT29bhwkbF+i11qkRUJb2KMKqcJ1hdFceEIRd9u64Q=\ngoogle.golang.org/genproto v0.0.0-20260217215200-42d3e9bedb6d h1:vsOm753cOAMkt76efriTCDKjpCbK18XGHMJHo0JUKhc=\ngoogle.golang.org/genproto v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:0oz9d7g9QLSdv9/lgbIjowW1JoxMbxmBVNe8i6tORJI=\ngoogle.golang.org/genproto/googleapis/api v0.0.0-20260217215200-42d3e9bedb6d h1:EocjzKLywydp5uZ5tJ79iP6Q0UjDnyiHkGRWxuPBP8s=\ngoogle.golang.org/genproto/googleapis/api v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:48U2I+QQUYhsFrg2SY6r+nJzeOtjey7j//WBESw+qyQ=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=\ngoogle.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=\ngoogle.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=\ngoogle.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 h1:F29+wU6Ee6qgu9TddPgooOdaqsxTMunOoj8KA5yuS5A=\ngoogle.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1/go.mod h1:5KF+wpkbTSbGcR9zteSqZV6fqFOWBl4Yde8En8MryZA=\ngoogle.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=\ngoogle.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=\ngopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=\ngopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=\ngopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "internal/cast/cast.go",
    "content": "package cast\n\nimport (\n\t\"github.com/ccoveille/go-safecast/v2\"\n)\n\ntype signed interface {\n\t~int | ~int8 | ~int16 | ~int32 | ~int64\n}\n\ntype unsigned interface {\n\t~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64\n}\n\ntype number interface {\n\tsigned | unsigned\n}\n\nfunc SafeUint(x int) (uint, error) {\n\treturn safecast.Convert[uint](x)\n}\n\nfunc Uint(x int) uint {\n\tu, err := SafeUint(x)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn u\n}\n\nfunc SafeInt64[T number](x T) (int64, error) {\n\treturn safecast.Convert[int64](x)\n}\n\nfunc Int64[T number](x T) int64 {\n\ti64, err := SafeInt64(x)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn i64\n}\n\nfunc SafeUint64[T signed](x T) (uint64, error) {\n\treturn safecast.Convert[uint64](x)\n}\n\nfunc Uint64[T signed](x T) uint64 {\n\tu64, err := SafeUint64(x)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn u64\n}\n\nfunc SafeInt32[T signed](x T) (int32, error) {\n\treturn safecast.Convert[int32](x)\n}\n\nfunc Int32[T signed](x T) int32 {\n\ti32, err := SafeInt32(x)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn i32\n}\n\nfunc SafeUint32[T signed](x T) (uint32, error) {\n\treturn safecast.Convert[uint32](x)\n}\n\nfunc Uint32[T signed](x T) uint32 {\n\tu32, err := SafeUint32(x)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn u32\n}\n\nfunc SafeUint16(x int) (uint16, error) {\n\treturn safecast.Convert[uint16](x)\n}\n\nfunc Uint16(x int) uint16 {\n\tu16, err := SafeUint16(x)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn u16\n}\n\nfunc SafeUint8[T number](x T) (uint8, error) {\n\treturn safecast.Convert[uint8](x)\n}\n\nfunc Uint8[T number](x T) uint8 {\n\tu8, err := SafeUint8(x)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn u8\n}\n"
  },
  {
    "path": "internal/cast/cast_test.go",
    "content": "package cast\n\nimport (\n\t\"math\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestUintConvertsValues(t *testing.T) {\n\trequire.Equal(t, uint(0), Uint(0))\n\trequire.Equal(t, uint(math.MaxInt), Uint(math.MaxInt))\n\trequire.Equal(t, uint(42), Uint(42))\n}\n\nfunc TestUintPanicsOnNegativeValue(t *testing.T) {\n\trequire.Panics(t, func() { Uint(-1) })\n}\n\nfunc TestInt64ConvertsValues(t *testing.T) {\n\trequire.Equal(t, int64(0), Int64(0))\n\trequire.Equal(t, int64(math.MaxInt), Int64(math.MaxInt))\n\trequire.Equal(t, int64(42), Int64(42))\n}\n\nfunc TestInt64PanicsOnLargeValue(t *testing.T) {\n\trequire.Panics(t, func() { Int64(uint64(math.MaxInt64 + 1)) })\n}\n\nfunc TestUint64ConvertsValues(t *testing.T) {\n\trequire.Equal(t, uint64(0), Uint64(0))\n\trequire.Equal(t, uint64(math.MaxInt), Uint64((math.MaxInt)))\n\trequire.Equal(t, uint64(42), Uint64(42))\n}\n\nfunc TestUint64PanicsOnNegativeValue(t *testing.T) {\n\trequire.Panics(t, func() { Uint64(-1) })\n}\n\nfunc TestInt32ConvertsValues(t *testing.T) {\n\trequire.Equal(t, int32(0), Int32(0))\n\trequire.Equal(t, int32(math.MaxInt32), Int32(math.MaxInt32))\n\trequire.Equal(t, int32(42), Int32(42))\n}\n\nfunc TestInt32PanicsOnTooSmallValue(t *testing.T) {\n\trequire.Panics(t, func() { Int32(int64(math.MinInt32 - 1)) })\n}\n\nfunc TestInt32PanicsOnLargeValue(t *testing.T) {\n\trequire.Panics(t, func() { Int32(int64(math.MaxInt32 + 1)) })\n}\n\nfunc TestUint32ConvertsValues(t *testing.T) {\n\trequire.Equal(t, uint32(0), Uint32(0))\n\trequire.Equal(t, uint32(math.MaxUint32), Uint32(int64(math.MaxUint32)))\n\trequire.Equal(t, uint32(42), Uint32(42))\n}\n\nfunc TestUint32PanicsOnNegativeValue(t *testing.T) {\n\trequire.Panics(t, func() { Uint32(-1) })\n}\n\nfunc TestUint32PanicsOnLargeValue(t *testing.T) {\n\trequire.Panics(t, func() { Uint32(int64(math.MaxUint32 + 1)) })\n}\n\nfunc TestUint16ConvertsValues(t *testing.T) {\n\trequire.Equal(t, uint16(0), Uint16(0))\n\trequire.Equal(t, uint16(math.MaxUint16), Uint16(math.MaxUint16))\n\trequire.Equal(t, uint16(42), Uint16(42))\n}\n\nfunc TestUint16PanicsOnNegativeValue(t *testing.T) {\n\trequire.Panics(t, func() { Uint16(-1) })\n}\n\nfunc TestUint16PanicsOnLargeValue(t *testing.T) {\n\trequire.Panics(t, func() { Uint16(math.MaxUint16 + 1) })\n}\n\nfunc TestUint8ConvertsValues(t *testing.T) {\n\trequire.Equal(t, uint8(0), Uint8(0))\n\trequire.Equal(t, uint8(math.MaxUint8), Uint8(math.MaxUint8))\n\trequire.Equal(t, uint8(42), Uint8(42))\n}\n\nfunc TestUint8PanicsOnNegativeValue(t *testing.T) {\n\trequire.Panics(t, func() { Uint8(-1) })\n}\n\nfunc TestUint8PanicsOnLargeValue(t *testing.T) {\n\trequire.Panics(t, func() { Uint8(math.MaxUint8 + 1) })\n}\n"
  },
  {
    "path": "internal/httptransport/httptransport.go",
    "content": "// Package httptransport implements initialization of [http.Transport] instances and related\n// functionality.\npackage httptransport\n\nimport (\n\t\"net\"\n\t\"net/http\"\n\t\"time\"\n)\n\n// Wrapper wraps the set of functions mapping [http.Transport] references to [http.RoundTripper].\ntype Wrapper func(*http.Transport) http.RoundTripper\n\n// NoopWrapper returns a [Wrapper] that simply casts its provided [http.Transport] to an\n// [http.RoundTripper].\nfunc NoopWrapper() Wrapper {\n\treturn func(t *http.Transport) http.RoundTripper {\n\t\treturn t\n\t}\n}\n\n// New returns a reference to an [http.Transport] that's initialized just like the\n// [http.DefaultTransport] is by the standard library.\nfunc New() *http.Transport {\n\treturn &http.Transport{\n\t\tProxy: http.ProxyFromEnvironment,\n\t\tDialContext: (&net.Dialer{\n\t\t\tTimeout:   30 * time.Second,\n\t\t\tKeepAlive: 30 * time.Second,\n\t\t}).DialContext,\n\t\tForceAttemptHTTP2:     true,\n\t\tMaxIdleConns:          100,\n\t\tIdleConnTimeout:       90 * time.Second,\n\t\tTLSHandshakeTimeout:   10 * time.Second,\n\t\tExpectContinueTimeout: 1 * time.Second,\n\t}\n}\n"
  },
  {
    "path": "internal/metrix/meter.go",
    "content": "// Package metrix implements stats-related functionality.\npackage metrix\n\nimport (\n\t\"crypto/x509\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/smallstep/certificates/authority/provisioner\"\n\t\"golang.org/x/crypto/ssh\"\n\n\t\"github.com/prometheus/client_golang/prometheus\"\n\t\"github.com/prometheus/client_golang/prometheus/promhttp\"\n)\n\n// New initializes and returns a new [Meter].\nfunc New() (m *Meter) {\n\tinitializedAt := time.Now()\n\tdefaultLabels := []string{\"provisioner\", \"success\"}\n\tsshSignLabels := []string{\"provisioner\", \"success\", \"type\"}\n\n\tm = &Meter{\n\t\tuptime: prometheus.NewGaugeFunc(\n\t\t\tprometheus.GaugeOpts(opts(\n\t\t\t\t\"\",\n\t\t\t\t\"uptime_seconds\",\n\t\t\t\t\"Number of seconds since service start\",\n\t\t\t)),\n\t\t\tfunc() float64 {\n\t\t\t\treturn float64(time.Since(initializedAt) / time.Second)\n\t\t\t},\n\t\t),\n\t\tssh:  newProvisionerInstruments(\"ssh\", sshSignLabels, defaultLabels),\n\t\tx509: newProvisionerInstruments(\"x509\", defaultLabels, defaultLabels),\n\t\tkms: &kms{\n\t\t\tsigned: prometheus.NewCounter(prometheus.CounterOpts(opts(\"kms\", \"signed\", \"Number of KMS-backed signatures\"))),\n\t\t\terrors: prometheus.NewCounter(prometheus.CounterOpts(opts(\"kms\", \"errors\", \"Number of KMS-related errors\"))),\n\t\t},\n\t}\n\n\treg := prometheus.NewRegistry()\n\n\treg.MustRegister(\n\t\tm.uptime,\n\t\tm.ssh.rekeyed,\n\t\tm.ssh.renewed,\n\t\tm.ssh.signed,\n\t\tm.ssh.webhookAuthorized,\n\t\tm.ssh.webhookEnriched,\n\t\tm.x509.rekeyed,\n\t\tm.x509.renewed,\n\t\tm.x509.signed,\n\t\tm.x509.webhookAuthorized,\n\t\tm.x509.webhookEnriched,\n\t\tm.kms.signed,\n\t\tm.kms.errors,\n\t)\n\n\th := promhttp.HandlerFor(reg, promhttp.HandlerOpts{\n\t\tRegistry:            reg,\n\t\tTimeout:             5 * time.Second,\n\t\tMaxRequestsInFlight: 10,\n\t})\n\n\tmux := http.NewServeMux()\n\tmux.Handle(\"/metrics\", h)\n\tm.Handler = mux\n\n\treturn\n}\n\n// Meter wraps the functionality of a Prometheus-compatible HTTP handler.\ntype Meter struct {\n\thttp.Handler\n\n\tuptime prometheus.GaugeFunc\n\tssh    *provisionerInstruments\n\tx509   *provisionerInstruments\n\tkms    *kms\n}\n\n// SSHRekeyed implements [authority.Meter] for [Meter].\nfunc (m *Meter) SSHRekeyed(cert *ssh.Certificate, p provisioner.Interface, err error) {\n\tincrProvisionerCounter(m.ssh.rekeyed, p, err, sshCertValues(cert)...)\n}\n\n// SSHRenewed implements [authority.Meter] for [Meter].\nfunc (m *Meter) SSHRenewed(cert *ssh.Certificate, p provisioner.Interface, err error) {\n\tincrProvisionerCounter(m.ssh.renewed, p, err, sshCertValues(cert)...)\n}\n\n// SSHSigned implements [authority.Meter] for [Meter].\nfunc (m *Meter) SSHSigned(cert *ssh.Certificate, p provisioner.Interface, err error) {\n\tincrProvisionerCounter(m.ssh.signed, p, err, sshCertValues(cert)...)\n}\n\n// SSHWebhookAuthorized implements [authority.Meter] for [Meter].\nfunc (m *Meter) SSHWebhookAuthorized(p provisioner.Interface, err error) {\n\tincrProvisionerCounter(m.ssh.webhookAuthorized, p, err)\n}\n\n// SSHWebhookEnriched implements [authority.Meter] for [Meter].\nfunc (m *Meter) SSHWebhookEnriched(p provisioner.Interface, err error) {\n\tincrProvisionerCounter(m.ssh.webhookEnriched, p, err)\n}\n\n// X509Rekeyed implements [authority.Meter] for [Meter].\nfunc (m *Meter) X509Rekeyed(_ []*x509.Certificate, p provisioner.Interface, err error) {\n\tincrProvisionerCounter(m.x509.rekeyed, p, err)\n}\n\n// X509Renewed implements [authority.Meter] for [Meter].\nfunc (m *Meter) X509Renewed(_ []*x509.Certificate, p provisioner.Interface, err error) {\n\tincrProvisionerCounter(m.x509.renewed, p, err)\n}\n\n// X509Signed implements [authority.Meter] for [Meter].\nfunc (m *Meter) X509Signed(_ []*x509.Certificate, p provisioner.Interface, err error) {\n\tincrProvisionerCounter(m.x509.signed, p, err)\n}\n\n// X509WebhookAuthorized implements [authority.Meter] for [Meter].\nfunc (m *Meter) X509WebhookAuthorized(p provisioner.Interface, err error) {\n\tincrProvisionerCounter(m.x509.webhookAuthorized, p, err)\n}\n\n// X509WebhookEnriched implements [authority.Meter] for [Meter].\nfunc (m *Meter) X509WebhookEnriched(p provisioner.Interface, err error) {\n\tincrProvisionerCounter(m.x509.webhookEnriched, p, err)\n}\n\nfunc sshCertValues(cert *ssh.Certificate) []string {\n\tswitch cert.CertType {\n\tcase ssh.UserCert:\n\t\treturn []string{\"user\"}\n\tcase ssh.HostCert:\n\t\treturn []string{\"host\"}\n\tdefault:\n\t\treturn []string{\"unknown\"}\n\t}\n}\n\nfunc incrProvisionerCounter(cv *prometheus.CounterVec, p provisioner.Interface, err error, extraValues ...string) {\n\tvar name string\n\tif p != nil {\n\t\tname = p.GetName()\n\t}\n\n\tvalues := append([]string{\n\t\tname, strconv.FormatBool(err == nil),\n\t}, extraValues...)\n\tcv.WithLabelValues(values...).Inc()\n}\n\n// KMSSigned implements [authority.Meter] for [Meter].\nfunc (m *Meter) KMSSigned(err error) {\n\tif err == nil {\n\t\tm.kms.signed.Inc()\n\t} else {\n\t\tm.kms.errors.Inc()\n\t}\n}\n\n// provisionerInstruments wraps the counters exported by provisioners.\ntype provisionerInstruments struct {\n\trekeyed *prometheus.CounterVec\n\trenewed *prometheus.CounterVec\n\tsigned  *prometheus.CounterVec\n\n\twebhookAuthorized *prometheus.CounterVec\n\twebhookEnriched   *prometheus.CounterVec\n}\n\nfunc newProvisionerInstruments(subsystem string, signLabels, webhookLabels []string) *provisionerInstruments {\n\treturn &provisionerInstruments{\n\t\trekeyed:           newCounterVec(subsystem, \"rekeyed_total\", \"Number of certificates rekeyed\", signLabels...),\n\t\trenewed:           newCounterVec(subsystem, \"renewed_total\", \"Number of certificates renewed\", signLabels...),\n\t\tsigned:            newCounterVec(subsystem, \"signed_total\", \"Number of certificates signed\", signLabels...),\n\t\twebhookAuthorized: newCounterVec(subsystem, \"webhook_authorized_total\", \"Number of authorizing webhooks called\", webhookLabels...),\n\t\twebhookEnriched:   newCounterVec(subsystem, \"webhook_enriched_total\", \"Number of enriching webhooks called\", webhookLabels...),\n\t}\n}\n\ntype kms struct {\n\tsigned prometheus.Counter\n\terrors prometheus.Counter\n}\n\nfunc newCounterVec(subsystem, name, help string, labels ...string) *prometheus.CounterVec {\n\topts := opts(subsystem, name, help)\n\n\treturn prometheus.NewCounterVec(prometheus.CounterOpts(opts), labels)\n}\n\nfunc opts(subsystem, name, help string) prometheus.Opts {\n\treturn prometheus.Opts{\n\t\tNamespace: \"step_ca\",\n\t\tSubsystem: subsystem,\n\t\tName:      name,\n\t\tHelp:      help,\n\t}\n}\n"
  },
  {
    "path": "internal/userid/userid.go",
    "content": "package userid\n\nimport \"context\"\n\ntype contextKey struct{}\n\n// NewContext returns a new context with the given user ID added to the\n// context.\n// TODO(hs): this doesn't seem to be used / set currently; implement\n// when/where it makes sense.\nfunc NewContext(ctx context.Context, userID string) context.Context {\n\treturn context.WithValue(ctx, contextKey{}, userID)\n}\n\n// FromContext returns the user ID from the context if it exists\n// and is not empty.\nfunc FromContext(ctx context.Context) (string, bool) {\n\tv, ok := ctx.Value(contextKey{}).(string)\n\treturn v, ok && v != \"\"\n}\n"
  },
  {
    "path": "logging/clf.go",
    "content": "package logging\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/sirupsen/logrus\"\n)\n\nvar clfFields = [...]string{\n\t\"request-id\", \"remote-address\", \"name\", \"user-id\", \"time\", \"duration\", \"method\", \"path\", \"protocol\", \"status\", \"size\",\n}\n\n// CommonLogFormat implements the logrus.Formatter interface it writes logrus\n// entries using a CLF format prepended by the request-id.\ntype CommonLogFormat struct{}\n\n// Format implements the logrus.Formatter interface. It returns the given\n// logrus entry as a CLF line with the following format:\n//\n//\t<request-id> <remote-address> <name> <user-id> <time> <duration> \"<method> <path> <protocol>\" <status> <size>\n//\n// If a field is not known, the hyphen symbol (-) will be used.\nfunc (f *CommonLogFormat) Format(entry *logrus.Entry) ([]byte, error) {\n\tdata := make([]string, len(clfFields))\n\tfor i, name := range clfFields {\n\t\tif v, ok := entry.Data[name]; ok {\n\t\t\tswitch v := v.(type) {\n\t\t\tcase error:\n\t\t\t\tdata[i] = v.Error()\n\t\t\tcase string:\n\t\t\t\tif v == \"\" {\n\t\t\t\t\tdata[i] = \"-\"\n\t\t\t\t} else {\n\t\t\t\t\tdata[i] = v\n\t\t\t\t}\n\t\t\tcase time.Time:\n\t\t\t\tdata[i] = v.Format(time.RFC3339)\n\t\t\tcase time.Duration:\n\t\t\t\tdata[i] = strconv.FormatInt(int64(v/time.Millisecond), 10)\n\t\t\tcase int:\n\t\t\t\tdata[i] = strconv.FormatInt(int64(v), 10)\n\t\t\tcase int64:\n\t\t\t\tdata[i] = strconv.FormatInt(v, 10)\n\t\t\tdefault:\n\t\t\t\tdata[i] = fmt.Sprintf(\"%v\", v)\n\t\t\t}\n\t\t} else {\n\t\t\tdata[i] = \"-\"\n\t\t}\n\t}\n\n\tvar buf bytes.Buffer\n\tbuf.WriteString(data[0])\n\tbuf.WriteByte(' ')\n\tbuf.WriteString(data[1])\n\tbuf.WriteByte(' ')\n\tbuf.WriteString(data[2])\n\tbuf.WriteByte(' ')\n\tbuf.WriteString(data[3])\n\tbuf.WriteByte(' ')\n\tbuf.WriteString(data[4])\n\tbuf.WriteByte(' ')\n\tbuf.WriteString(data[5])\n\tbuf.WriteString(\" \\\"\")\n\tbuf.WriteString(data[6])\n\tbuf.WriteByte(' ')\n\tbuf.WriteString(data[7])\n\tbuf.WriteByte(' ')\n\tbuf.WriteString(data[8])\n\tbuf.WriteString(\"\\\" \")\n\tbuf.WriteString(data[9])\n\tbuf.WriteByte(' ')\n\tbuf.WriteString(data[10])\n\tbuf.WriteByte('\\n')\n\treturn buf.Bytes(), nil\n}\n"
  },
  {
    "path": "logging/handler.go",
    "content": "package logging\n\nimport (\n\t\"net\"\n\t\"net/http\"\n\t\"os\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/sirupsen/logrus\"\n\n\t\"github.com/smallstep/certificates/internal/userid\"\n\t\"github.com/smallstep/certificates/middleware/requestid\"\n)\n\n// Common headers used for identifying the originating IP address of a client\n// connecting to a web server through a proxy server\nvar (\n\ttrueClientIP  = http.CanonicalHeaderKey(\"True-Client-IP\")\n\txRealIP       = http.CanonicalHeaderKey(\"X-Real-IP\")\n\txForwardedFor = http.CanonicalHeaderKey(\"X-Forwarded-For\")\n)\n\n// LoggerHandler creates a logger handler\ntype LoggerHandler struct {\n\tname    string\n\tlogger  *logrus.Logger\n\toptions options\n\tnext    http.Handler\n}\n\n// options encapsulates any overriding parameters for the logger handler\ntype options struct {\n\t// onlyTraceHealthEndpoint determines if the kube-probe requests to the /health\n\t// endpoint should only be logged at the TRACE level in the (expected) HTTP\n\t// 200 case\n\tonlyTraceHealthEndpoint bool\n\n\t// logRealIP determines if the real IP address of the client should be logged\n\t// instead of the IP address of the proxy\n\tlogRealIP bool\n}\n\n// NewLoggerHandler returns the given http.Handler with the logger integrated.\nfunc NewLoggerHandler(name string, logger *Logger, next http.Handler) http.Handler {\n\tonlyTraceHealthEndpoint, _ := strconv.ParseBool(os.Getenv(\"STEP_LOGGER_ONLY_TRACE_HEALTH_ENDPOINT\"))\n\tlogRealIP, _ := strconv.ParseBool(os.Getenv(\"STEP_LOGGER_LOG_REAL_IP\"))\n\n\treturn &LoggerHandler{\n\t\tname:   name,\n\t\tlogger: logger.GetImpl(),\n\t\toptions: options{\n\t\t\tonlyTraceHealthEndpoint: onlyTraceHealthEndpoint,\n\t\t\tlogRealIP:               logRealIP,\n\t\t},\n\t\tnext: next,\n\t}\n}\n\n// ServeHTTP implements the http.Handler and call to the handler to log with a\n// custom http.ResponseWriter that records the response code and the number of\n// bytes sent.\nfunc (l *LoggerHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {\n\tt := time.Now()\n\trw := NewResponseLogger(w)\n\tl.next.ServeHTTP(rw, r)\n\td := time.Since(t)\n\tl.writeEntry(rw, r, t, d)\n}\n\n// writeEntry writes to the Logger writer the request information in the logger.\nfunc (l *LoggerHandler) writeEntry(w ResponseLogger, r *http.Request, t time.Time, d time.Duration) {\n\tvar requestID, userID string\n\n\tctx := r.Context()\n\tif v, ok := requestid.FromContext(ctx); ok {\n\t\trequestID = v\n\t}\n\tif v, ok := userid.FromContext(ctx); ok {\n\t\tuserID = v\n\t}\n\n\t// Remote hostname\n\taddr := r.RemoteAddr\n\tif l.options.logRealIP {\n\t\taddr = realIP(r)\n\t}\n\tif host, _, err := net.SplitHostPort(addr); err == nil {\n\t\taddr = host\n\t}\n\n\t// From https://github.com/gorilla/handlers\n\turi := r.RequestURI\n\t// Requests using the CONNECT method over HTTP/2.0 must use\n\t// the authority field (aka r.Host) to identify the target.\n\t// Refer: https://httpwg.github.io/specs/rfc7540.html#CONNECT\n\tif r.ProtoMajor == 2 && r.Method == \"CONNECT\" {\n\t\turi = r.Host\n\t}\n\tif uri == \"\" {\n\t\turi = sanitizeLogEntry(r.URL.RequestURI())\n\t}\n\n\tstatus := w.StatusCode()\n\n\tfields := logrus.Fields{\n\t\t\"request-id\":     requestID,\n\t\t\"remote-address\": addr,\n\t\t\"name\":           l.name,\n\t\t\"user-id\":        userID,\n\t\t\"time\":           t.Format(time.RFC3339),\n\t\t\"duration-ns\":    d.Nanoseconds(),\n\t\t\"duration\":       d.String(),\n\t\t\"method\":         r.Method,\n\t\t\"path\":           uri,\n\t\t\"protocol\":       r.Proto,\n\t\t\"status\":         status,\n\t\t\"size\":           w.Size(),\n\t\t\"referer\":        sanitizeLogEntry(r.Referer()),\n\t\t\"user-agent\":     sanitizeLogEntry(r.UserAgent()),\n\t}\n\n\tfor k, v := range w.Fields() {\n\t\tfields[k] = v\n\t}\n\n\tswitch {\n\tcase status < http.StatusBadRequest:\n\t\tif l.options.onlyTraceHealthEndpoint && uri == \"/health\" {\n\t\t\tl.logger.WithFields(fields).Trace()\n\t\t} else {\n\t\t\tl.logger.WithFields(fields).Info()\n\t\t}\n\tcase status < http.StatusInternalServerError:\n\t\tl.logger.WithFields(fields).Warn()\n\tdefault:\n\t\tl.logger.WithFields(fields).Error()\n\t}\n}\n\nfunc sanitizeLogEntry(s string) string {\n\tescaped := strings.ReplaceAll(s, \"\\n\", \"\")\n\treturn strings.ReplaceAll(escaped, \"\\r\", \"\")\n}\n\n// realIP returns the real IP address of the client connecting to the server by\n// parsing either the True-Client-IP, X-Real-IP or the X-Forwarded-For headers\n// (in that order). If the headers are not set or set to an invalid IP, it\n// returns the RemoteAddr of the request.\nfunc realIP(r *http.Request) string {\n\tvar ip string\n\n\tif tcip := r.Header.Get(trueClientIP); tcip != \"\" {\n\t\tip = tcip\n\t} else if xrip := r.Header.Get(xRealIP); xrip != \"\" {\n\t\tip = xrip\n\t} else if xff := r.Header.Get(xForwardedFor); xff != \"\" {\n\t\ti := strings.Index(xff, \",\")\n\t\tif i == -1 {\n\t\t\ti = len(xff)\n\t\t}\n\t\tip = xff[:i]\n\t}\n\tif ip == \"\" || net.ParseIP(ip) == nil {\n\t\treturn r.RemoteAddr\n\t}\n\treturn ip\n}\n"
  },
  {
    "path": "logging/handler_test.go",
    "content": "package logging\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/smallstep/assert\"\n\n\t\"github.com/sirupsen/logrus\"\n\t\"github.com/sirupsen/logrus/hooks/test\"\n)\n\n// TestHealthOKHandling ensures that http requests from the Kubernetes\n// liveness/readiness probes are only logged at Trace level if they are HTTP\n// 200 (which is normal operation) and the user has opted-in. If the user has\n// not opted-in then they continue to be logged at Info level.\nfunc TestHealthOKHandling(t *testing.T) {\n\tstatusHandler := func(statusCode int) http.HandlerFunc {\n\t\treturn func(w http.ResponseWriter, _ *http.Request) {\n\t\t\tw.WriteHeader(statusCode)\n\t\t\tfmt.Fprint(w, \"{}\")\n\t\t}\n\t}\n\n\ttests := []struct {\n\t\tname    string\n\t\tpath    string\n\t\toptions options\n\t\thandler http.HandlerFunc\n\t\twant    logrus.Level\n\t}{\n\t\t{\n\t\t\tname:    \"200 should be logged at Info level for /health request without explicit opt-in\",\n\t\t\tpath:    \"/health\",\n\t\t\thandler: statusHandler(http.StatusOK),\n\t\t\twant:    logrus.InfoLevel,\n\t\t},\n\t\t{\n\t\t\tname: \"200 should be logged only at Trace level for /health request if opt-in\",\n\t\t\tpath: \"/health\",\n\t\t\toptions: options{\n\t\t\t\tonlyTraceHealthEndpoint: true,\n\t\t\t},\n\t\t\thandler: statusHandler(http.StatusOK),\n\t\t\twant:    logrus.TraceLevel,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tlogger, hook := test.NewNullLogger()\n\t\t\tlogger.SetLevel(logrus.TraceLevel)\n\t\t\tl := &LoggerHandler{\n\t\t\t\tlogger:  logger,\n\t\t\t\toptions: tt.options,\n\t\t\t\tnext:    tt.handler,\n\t\t\t}\n\n\t\t\tr := httptest.NewRequest(\"GET\", tt.path, http.NoBody)\n\t\t\tw := httptest.NewRecorder()\n\t\t\tl.ServeHTTP(w, r)\n\n\t\t\tif assert.Equals(t, 1, len(hook.AllEntries())) {\n\t\t\t\tassert.Equals(t, tt.want, hook.LastEntry().Level)\n\t\t\t}\n\t\t})\n\t}\n}\n\n// TestHandlingRegardlessOfOptions ensures that http requests are treated like\n// any other request if they are for a non-health uri or fall within the\n// warn/error ranges of the http status codes, regardless of the\n// \"onlyTraceHealthEndpoint\" option.\nfunc TestHandlingRegardlessOfOptions(t *testing.T) {\n\tstatusHandler := func(statusCode int) http.HandlerFunc {\n\t\treturn func(w http.ResponseWriter, _ *http.Request) {\n\t\t\tw.WriteHeader(statusCode)\n\t\t\tfmt.Fprint(w, \"{}\")\n\t\t}\n\t}\n\n\ttests := []struct {\n\t\tname    string\n\t\tpath    string\n\t\thandler http.HandlerFunc\n\t\twant    logrus.Level\n\t}{\n\t\t{\n\t\t\tname:    \"200 should be logged at Info level for non-health requests\",\n\t\t\tpath:    \"/info\",\n\t\t\thandler: statusHandler(http.StatusOK),\n\t\t\twant:    logrus.InfoLevel,\n\t\t},\n\t\t{\n\t\t\tname:    \"400 should be logged at Warn level for non-health requests\",\n\t\t\tpath:    \"/info\",\n\t\t\thandler: statusHandler(http.StatusBadRequest),\n\t\t\twant:    logrus.WarnLevel,\n\t\t},\n\t\t{\n\t\t\tname:    \"500 should be logged at Error level for non-health requests\",\n\t\t\tpath:    \"/info\",\n\t\t\thandler: statusHandler(http.StatusInternalServerError),\n\t\t\twant:    logrus.ErrorLevel,\n\t\t},\n\t\t{\n\t\t\tname:    \"400 should be logged at Warn level even for /health requests\",\n\t\t\tpath:    \"/health\",\n\t\t\thandler: statusHandler(http.StatusBadRequest),\n\t\t\twant:    logrus.WarnLevel,\n\t\t},\n\t\t{\n\t\t\tname:    \"500 should be logged at Error level even for /health requests\",\n\t\t\tpath:    \"/health\",\n\t\t\thandler: statusHandler(http.StatusInternalServerError),\n\t\t\twant:    logrus.ErrorLevel,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tfor _, b := range []bool{true, false} {\n\t\t\t\tlogger, hook := test.NewNullLogger()\n\t\t\t\tlogger.SetLevel(logrus.TraceLevel)\n\t\t\t\tl := &LoggerHandler{\n\t\t\t\t\tlogger: logger,\n\t\t\t\t\toptions: options{\n\t\t\t\t\t\tonlyTraceHealthEndpoint: b,\n\t\t\t\t\t},\n\t\t\t\t\tnext: tt.handler,\n\t\t\t\t}\n\n\t\t\t\tr := httptest.NewRequest(\"GET\", tt.path, http.NoBody)\n\t\t\t\tw := httptest.NewRecorder()\n\t\t\t\tl.ServeHTTP(w, r)\n\n\t\t\t\tif assert.Equals(t, 1, len(hook.AllEntries())) {\n\t\t\t\t\tassert.Equals(t, tt.want, hook.LastEntry().Level)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\n// TestLogRealIP ensures that the real originating IP is logged instead of the\n// proxy IP when STEP_LOGGER_LOG_REAL_IP is set to true and specific headers are\n// present.\nfunc TestLogRealIP(t *testing.T) {\n\tstatusHandler := func(statusCode int) http.HandlerFunc {\n\t\treturn func(w http.ResponseWriter, _ *http.Request) {\n\t\t\tw.WriteHeader(statusCode)\n\t\t\tw.Write([]byte(\"{}\"))\n\t\t}\n\t}\n\n\tproxyIP := \"1.1.1.1\"\n\n\ttests := []struct {\n\t\tname      string\n\t\tlogRealIP string\n\t\theaders   map[string]string\n\t\texpected  string\n\t}{\n\t\t{\n\t\t\tname:      \"setting is turned on, no header is set\",\n\t\t\tlogRealIP: \"true\",\n\t\t\texpected:  \"1.1.1.1\",\n\t\t\theaders:   map[string]string{},\n\t\t},\n\t\t{\n\t\t\tname:      \"setting is turned on, True-Client-IP header is set\",\n\t\t\tlogRealIP: \"true\",\n\t\t\theaders: map[string]string{\n\t\t\t\t\"True-Client-IP\": \"2.2.2.2\",\n\t\t\t},\n\t\t\texpected: \"2.2.2.2\",\n\t\t},\n\t\t{\n\t\t\tname:      \"setting is turned on, True-Client-IP header is set with invalid value\",\n\t\t\tlogRealIP: \"true\",\n\t\t\theaders: map[string]string{\n\t\t\t\t\"True-Client-IP\": \"a.b.c.d\",\n\t\t\t},\n\t\t\texpected: \"1.1.1.1\",\n\t\t},\n\t\t{\n\t\t\tname:      \"setting is turned on, X-Real-IP header is set\",\n\t\t\tlogRealIP: \"true\",\n\t\t\theaders: map[string]string{\n\t\t\t\t\"X-Real-IP\": \"3.3.3.3\",\n\t\t\t},\n\t\t\texpected: \"3.3.3.3\",\n\t\t},\n\t\t{\n\t\t\tname:      \"setting is turned on, X-Forwarded-For header is set\",\n\t\t\tlogRealIP: \"true\",\n\t\t\theaders: map[string]string{\n\t\t\t\t\"X-Forwarded-For\": \"4.4.4.4\",\n\t\t\t},\n\t\t\texpected: \"4.4.4.4\",\n\t\t},\n\t\t{\n\t\t\tname:      \"setting is turned on, X-Forwarded-For header is set with multiple IPs\",\n\t\t\tlogRealIP: \"true\",\n\t\t\theaders: map[string]string{\n\t\t\t\t\"X-Forwarded-For\": \"4.4.4.4, 5.5.5.5, 6.6.6.6\",\n\t\t\t},\n\t\t\texpected: \"4.4.4.4\",\n\t\t},\n\t\t{\n\t\t\tname:      \"setting is turned on, all headers are set\",\n\t\t\tlogRealIP: \"true\",\n\t\t\theaders: map[string]string{\n\t\t\t\t\"True-Client-IP\":  \"2.2.2.2\",\n\t\t\t\t\"X-Real-IP\":       \"3.3.3.3\",\n\t\t\t\t\"X-Forwarded-For\": \"4.4.4.4\",\n\t\t\t},\n\t\t\texpected: \"2.2.2.2\",\n\t\t},\n\t\t{\n\t\t\tname:      \"setting is turned off, True-Client-IP header is set\",\n\t\t\tlogRealIP: \"false\",\n\t\t\texpected:  \"1.1.1.1\",\n\t\t\theaders: map[string]string{\n\t\t\t\t\"True-Client-IP\": \"2.2.2.2\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:      \"setting is turned off, no header is set\",\n\t\t\tlogRealIP: \"false\",\n\t\t\texpected:  \"1.1.1.1\",\n\t\t\theaders:   map[string]string{},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Setenv(\"STEP_LOGGER_LOG_REAL_IP\", tt.logRealIP)\n\n\t\t\tbaseLogger, hook := test.NewNullLogger()\n\t\t\tlogger := &Logger{\n\t\t\t\tLogger: baseLogger,\n\t\t\t}\n\t\t\tl := NewLoggerHandler(\"test\", logger, statusHandler(http.StatusOK))\n\n\t\t\tr := httptest.NewRequest(\"GET\", \"/test\", http.NoBody)\n\t\t\tr.RemoteAddr = proxyIP\n\t\t\tfor k, v := range tt.headers {\n\t\t\t\tr.Header.Set(k, v)\n\t\t\t}\n\t\t\tw := httptest.NewRecorder()\n\t\t\tl.ServeHTTP(w, r)\n\n\t\t\tif assert.Equals(t, 1, len(hook.AllEntries())) {\n\t\t\t\tentry := hook.LastEntry()\n\t\t\t\tassert.Equals(t, tt.expected, entry.Data[\"remote-address\"])\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "logging/logger.go",
    "content": "package logging\n\nimport (\n\t\"encoding/json\"\n\t\"net/http\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/pkg/errors\"\n\t\"github.com/sirupsen/logrus\"\n)\n\n// defaultTraceIdHeader is the default header used as a trace id.\nconst defaultTraceIDHeader = \"X-Smallstep-Id\"\n\n// ErrorKey defines the key used to log errors.\nvar ErrorKey = logrus.ErrorKey\n\n// Logger is an alias of logrus.Logger.\ntype Logger struct {\n\t*logrus.Logger\n\tname        string\n\ttraceHeader string\n}\n\n// loggerConfig represents the configuration options for the logger.\ntype loggerConfig struct {\n\tFormat      string `json:\"format\"`\n\tTraceHeader string `json:\"traceHeader\"`\n}\n\n// New initializes the logger with the given options.\nfunc New(name string, raw json.RawMessage) (*Logger, error) {\n\tvar config loggerConfig\n\tif err := json.Unmarshal(raw, &config); err != nil {\n\t\treturn nil, errors.Wrap(err, \"error unmarshalling logging attribute\")\n\t}\n\n\tvar formatter logrus.Formatter\n\tswitch strings.ToLower(config.Format) {\n\tcase \"\", \"text\":\n\t\t_, noColor := os.LookupEnv(\"NO_COLOR\")\n\t\t// With EnvironmentOverrideColors set, logrus looks at CLICOLOR and\n\t\t// CLICOLOR_FORCE\n\t\tformatter = &logrus.TextFormatter{\n\t\t\tDisableColors:             noColor,\n\t\t\tEnvironmentOverrideColors: true,\n\t\t}\n\tcase \"json\":\n\t\tformatter = new(logrus.JSONFormatter)\n\tcase \"common\":\n\t\tformatter = new(CommonLogFormat)\n\tdefault:\n\t\treturn nil, errors.Errorf(\"unsupported logger.format '%s'\", config.Format)\n\t}\n\n\tlogger := &Logger{\n\t\tLogger:      logrus.New(),\n\t\tname:        name,\n\t\ttraceHeader: config.TraceHeader,\n\t}\n\tif formatter != nil {\n\t\tlogger.Formatter = formatter\n\t}\n\treturn logger, nil\n}\n\n// GetImpl returns the real implementation of the logger.\nfunc (l *Logger) GetImpl() *logrus.Logger {\n\treturn l.Logger\n}\n\n// GetTraceHeader returns the trace header configured\nfunc (l *Logger) GetTraceHeader() string {\n\tif l.traceHeader == \"\" {\n\t\treturn defaultTraceIDHeader\n\t}\n\treturn l.traceHeader\n}\n\n// Middleware returns the logger middleware that will trace the request of the\n// given handler.\nfunc (l *Logger) Middleware(next http.Handler) http.Handler {\n\treturn NewLoggerHandler(l.name, l, next)\n}\n"
  },
  {
    "path": "logging/responselogger.go",
    "content": "package logging\n\nimport (\n\t\"bufio\"\n\t\"net\"\n\t\"net/http\"\n)\n\n// ResponseLogger defines an interface that a responseWrite can implement to\n// support the capture of the status code, the number of bytes written and\n// extra log entry fields.\ntype ResponseLogger interface {\n\thttp.ResponseWriter\n\tSize() int\n\tStatusCode() int\n\tFields() map[string]interface{}\n\tWithFields(map[string]interface{})\n}\n\n// NewResponseLogger wraps the given response writer with methods to capture\n// the status code, the number of bytes written, and methods to add new log\n// entries. It won't wrap the response writer if it's already a\n// ResponseLogger.\nfunc NewResponseLogger(w http.ResponseWriter) ResponseLogger {\n\tif rw, ok := w.(ResponseLogger); ok {\n\t\treturn rw\n\t}\n\treturn wrapLogger(w)\n}\n\nfunc wrapLogger(w http.ResponseWriter) (rw ResponseLogger) {\n\trw = &rwDefault{w, 200, 0, nil}\n\tif f, ok := w.(http.Flusher); ok {\n\t\trw = &rwFlusher{rw, f}\n\t}\n\tif h, ok := w.(http.Hijacker); ok {\n\t\trw = &rwHijacker{rw, h}\n\t}\n\tif p, ok := w.(http.Pusher); ok {\n\t\trw = &rwPusher{rw, p}\n\t}\n\treturn\n}\n\ntype rwDefault struct {\n\thttp.ResponseWriter\n\tcode   int\n\tsize   int\n\tfields map[string]interface{}\n}\n\nfunc (r *rwDefault) Header() http.Header {\n\treturn r.ResponseWriter.Header()\n}\n\nfunc (r *rwDefault) Write(p []byte) (n int, err error) {\n\tn, err = r.ResponseWriter.Write(p)\n\tr.size += n\n\treturn\n}\n\nfunc (r *rwDefault) WriteHeader(code int) {\n\tr.ResponseWriter.WriteHeader(code)\n\tr.code = code\n}\n\nfunc (r *rwDefault) Size() int {\n\treturn r.size\n}\n\nfunc (r *rwDefault) StatusCode() int {\n\treturn r.code\n}\n\nfunc (r *rwDefault) Fields() map[string]interface{} {\n\treturn r.fields\n}\n\nfunc (r *rwDefault) WithFields(fields map[string]interface{}) {\n\tif r.fields == nil {\n\t\tr.fields = make(map[string]interface{}, len(fields))\n\t}\n\tfor k, v := range fields {\n\t\tr.fields[k] = v\n\t}\n}\n\ntype rwFlusher struct {\n\tResponseLogger\n\tf http.Flusher\n}\n\nfunc (r *rwFlusher) Flush() {\n\tr.f.Flush()\n}\n\ntype rwHijacker struct {\n\tResponseLogger\n\th http.Hijacker\n}\n\nfunc (r *rwHijacker) Hijack() (net.Conn, *bufio.ReadWriter, error) {\n\treturn r.h.Hijack()\n}\n\ntype rwPusher struct {\n\tResponseLogger\n\tp http.Pusher\n}\n\nfunc (rw *rwPusher) Push(target string, opts *http.PushOptions) error {\n\treturn rw.p.Push(target, opts)\n}\n"
  },
  {
    "path": "middleware/requestid/requestid.go",
    "content": "// Package requestid provides HTTP request ID functionality\npackage requestid\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\n\t\"github.com/rs/xid\"\n\n\t\"go.step.sm/crypto/randutil\"\n)\n\nconst (\n\t// requestIDHeader is the header name used for propagating request IDs. If\n\t// available in an HTTP request, it'll be used instead of the X-Smallstep-Id\n\t// header. It'll always be used in response and set to the request ID.\n\trequestIDHeader = \"X-Request-Id\"\n\n\t// defaultTraceHeader is the default Smallstep tracing header that's currently\n\t// in use. It is used as a fallback to retrieve a request ID from, if the\n\t// \"X-Request-Id\" request header is not set.\n\tdefaultTraceHeader = \"X-Smallstep-Id\"\n)\n\ntype Handler struct {\n\tlegacyTraceHeader string\n}\n\n// New creates a new request ID [handler]. It takes a trace header,\n// which is used keep the legacy behavior intact, which relies on the\n// X-Smallstep-Id header instead of X-Request-Id.\nfunc New(legacyTraceHeader string) *Handler {\n\tif legacyTraceHeader == \"\" {\n\t\tlegacyTraceHeader = defaultTraceHeader\n\t}\n\n\treturn &Handler{legacyTraceHeader: legacyTraceHeader}\n}\n\n// Middleware wraps an [http.Handler] with request ID extraction\n// from the X-Reqeust-Id header by default, or from the X-Smallstep-Id\n// header if not set. If both are not set, a new request ID is generated.\n// In all cases, the request ID is added to the request context, and\n// set to be reflected in the response.\nfunc (h *Handler) Middleware(next http.Handler) http.Handler {\n\tfn := func(w http.ResponseWriter, req *http.Request) {\n\t\trequestID := req.Header.Get(requestIDHeader)\n\t\tif requestID == \"\" {\n\t\t\trequestID = req.Header.Get(h.legacyTraceHeader)\n\t\t}\n\n\t\tif requestID == \"\" {\n\t\t\trequestID = newRequestID()\n\t\t\treq.Header.Set(h.legacyTraceHeader, requestID) // legacy behavior\n\t\t}\n\n\t\t// immediately set the request ID to be reflected in the response\n\t\tw.Header().Set(requestIDHeader, requestID)\n\n\t\t// continue down the handler chain\n\t\tctx := NewContext(req.Context(), requestID)\n\t\tnext.ServeHTTP(w, req.WithContext(ctx))\n\t}\n\treturn http.HandlerFunc(fn)\n}\n\n// newRequestID generates a new random UUIDv4 request ID. If UUIDv4\n// generation fails, it'll fallback to generating a random ID using\n// github.com/rs/xid.\nfunc newRequestID() string {\n\trequestID, err := randutil.UUIDv4()\n\tif err != nil {\n\t\trequestID = xid.New().String()\n\t}\n\n\treturn requestID\n}\n\ntype contextKey struct{}\n\n// NewContext returns a new context with the given request ID added to the\n// context.\nfunc NewContext(ctx context.Context, requestID string) context.Context {\n\treturn context.WithValue(ctx, contextKey{}, requestID)\n}\n\n// FromContext returns the request ID from the context if it exists and\n// is not the empty value.\nfunc FromContext(ctx context.Context) (string, bool) {\n\tv, ok := ctx.Value(contextKey{}).(string)\n\treturn v, ok && v != \"\"\n}\n"
  },
  {
    "path": "middleware/requestid/requestid_test.go",
    "content": "package requestid\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc newRequest(t *testing.T) *http.Request {\n\tt.Helper()\n\tr, err := http.NewRequest(http.MethodGet, \"https://example.com\", http.NoBody)\n\trequire.NoError(t, err)\n\treturn r\n}\n\nfunc Test_Middleware(t *testing.T) {\n\trequestWithID := newRequest(t)\n\trequestWithID.Header.Set(\"X-Request-Id\", \"reqID\")\n\n\trequestWithoutID := newRequest(t)\n\n\trequestWithEmptyHeader := newRequest(t)\n\trequestWithEmptyHeader.Header.Set(\"X-Request-Id\", \"\")\n\n\trequestWithSmallstepID := newRequest(t)\n\trequestWithSmallstepID.Header.Set(\"X-Smallstep-Id\", \"smallstepID\")\n\n\ttests := []struct {\n\t\tname        string\n\t\ttraceHeader string\n\t\tnext        http.HandlerFunc\n\t\treq         *http.Request\n\t}{\n\t\t{\n\t\t\tname:        \"default-request-id\",\n\t\t\ttraceHeader: defaultTraceHeader,\n\t\t\tnext: func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\tassert.Empty(t, r.Header.Get(\"X-Smallstep-Id\"))\n\t\t\t\tassert.Equal(t, \"reqID\", r.Header.Get(\"X-Request-Id\"))\n\t\t\t\treqID, ok := FromContext(r.Context())\n\t\t\t\tif assert.True(t, ok) {\n\t\t\t\t\tassert.Equal(t, \"reqID\", reqID)\n\t\t\t\t}\n\t\t\t\tassert.Equal(t, \"reqID\", w.Header().Get(\"X-Request-Id\"))\n\t\t\t},\n\t\t\treq: requestWithID,\n\t\t},\n\t\t{\n\t\t\tname:        \"no-request-id\",\n\t\t\ttraceHeader: \"X-Request-Id\",\n\t\t\tnext: func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\tassert.Empty(t, r.Header.Get(\"X-Smallstep-Id\"))\n\t\t\t\tvalue := r.Header.Get(\"X-Request-Id\")\n\t\t\t\tassert.NotEmpty(t, value)\n\t\t\t\treqID, ok := FromContext(r.Context())\n\t\t\t\tif assert.True(t, ok) {\n\t\t\t\t\tassert.Equal(t, value, reqID)\n\t\t\t\t}\n\t\t\t\tassert.Equal(t, value, w.Header().Get(\"X-Request-Id\"))\n\t\t\t},\n\t\t\treq: requestWithoutID,\n\t\t},\n\t\t{\n\t\t\tname:        \"empty-header\",\n\t\t\ttraceHeader: \"\",\n\t\t\tnext: func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\tassert.Empty(t, r.Header.Get(\"X-Request-Id\"))\n\t\t\t\tvalue := r.Header.Get(\"X-Smallstep-Id\")\n\t\t\t\tassert.NotEmpty(t, value)\n\t\t\t\treqID, ok := FromContext(r.Context())\n\t\t\t\tif assert.True(t, ok) {\n\t\t\t\t\tassert.Equal(t, value, reqID)\n\t\t\t\t}\n\t\t\t\tassert.Equal(t, value, w.Header().Get(\"X-Request-Id\"))\n\t\t\t},\n\t\t\treq: requestWithEmptyHeader,\n\t\t},\n\t\t{\n\t\t\tname:        \"fallback-header-name\",\n\t\t\ttraceHeader: defaultTraceHeader,\n\t\t\tnext: func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\tassert.Empty(t, r.Header.Get(\"X-Request-Id\"))\n\t\t\t\tassert.Equal(t, \"smallstepID\", r.Header.Get(\"X-Smallstep-Id\"))\n\t\t\t\treqID, ok := FromContext(r.Context())\n\t\t\t\tif assert.True(t, ok) {\n\t\t\t\t\tassert.Equal(t, \"smallstepID\", reqID)\n\t\t\t\t}\n\t\t\t\tassert.Equal(t, \"smallstepID\", w.Header().Get(\"X-Request-Id\"))\n\t\t\t},\n\t\t\treq: requestWithSmallstepID,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\thandler := New(tt.traceHeader).Middleware(tt.next)\n\n\t\t\tw := httptest.NewRecorder()\n\t\t\thandler.ServeHTTP(w, tt.req)\n\t\t\tassert.NotEmpty(t, w.Header().Get(\"X-Request-Id\"))\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "monitoring/monitoring.go",
    "content": "package monitoring\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/newrelic/go-agent/v3/newrelic\"\n\t\"github.com/pkg/errors\"\n\n\t\"github.com/smallstep/certificates/logging\"\n\t\"github.com/smallstep/certificates/middleware/requestid\"\n)\n\n// Middleware is a function returns another http.Handler that wraps the given\n// handler.\ntype Middleware func(next http.Handler) http.Handler\n\n// Monitoring is the type holding a middleware that traces the request to an\n// application.\ntype Monitoring struct {\n\tmiddleware Middleware\n}\n\n// monitoring config represents the JSON attributes used for configuration. At\n// this moment only fields for NewRelic are supported.\ntype monitoringConfig struct {\n\tType string `json:\"type,omitempty\"`\n\tName string `json:\"name\"`\n\tKey  string `json:\"key\"`\n}\n\n// New initializes the monitoring with the given configuration.\n// Right now it only supports newrelic as the monitoring backend.\nfunc New(raw json.RawMessage) (*Monitoring, error) {\n\tvar config monitoringConfig\n\tif err := json.Unmarshal(raw, &config); err != nil {\n\t\treturn nil, errors.Wrap(err, \"error unmarshalling monitoring attribute\")\n\t}\n\n\tm := new(Monitoring)\n\tswitch strings.ToLower(config.Type) {\n\tcase \"\", \"newrelic\":\n\t\tapp, err := newrelic.NewApplication(\n\t\t\tnewrelic.ConfigAppName(config.Name),\n\t\t\tnewrelic.ConfigLicense(config.Key),\n\t\t)\n\t\tif err != nil {\n\t\t\treturn nil, errors.Wrap(err, \"error loading New Relic application\")\n\t\t}\n\t\tm.middleware = newRelicMiddleware(app)\n\tdefault:\n\t\treturn nil, errors.Errorf(\"unsupported monitoring.type '%s'\", config.Type)\n\t}\n\treturn m, nil\n}\n\n// Middleware is an HTTP middleware that traces the request with the configured\n// monitoring backednd.\nfunc (m *Monitoring) Middleware(next http.Handler) http.Handler {\n\treturn m.middleware(next)\n}\n\nfunc newRelicMiddleware(app *newrelic.Application) Middleware {\n\treturn func(next http.Handler) http.Handler {\n\t\treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\t// Start transaction\n\t\t\ttxn := app.StartTransaction(transactionName(r))\n\t\t\tdefer txn.End()\n\n\t\t\tw = txn.SetWebResponse(w)\n\t\t\ttxn.SetWebRequestHTTP(r)\n\n\t\t\t// Wrap request writer if necessary\n\t\t\trw := logging.NewResponseLogger(w)\n\n\t\t\t// Call next handler\n\t\t\tnext.ServeHTTP(rw, r)\n\n\t\t\t// Report status (using same key NewRelic uses by default)\n\t\t\tstatus := rw.StatusCode()\n\t\t\ttxn.AddAttribute(\"httpResponseCode\", strconv.Itoa(status))\n\n\t\t\t// Add custom attributes\n\t\t\tif v, ok := requestid.FromContext(r.Context()); ok {\n\t\t\t\ttxn.AddAttribute(\"request.id\", v)\n\t\t\t}\n\n\t\t\t// Report errors if necessary\n\t\t\tif status >= http.StatusBadRequest {\n\t\t\t\tvar errorNoticed bool\n\t\t\t\tif fields := rw.Fields(); fields != nil {\n\t\t\t\t\tif v, ok := fields[\"error\"]; ok {\n\t\t\t\t\t\tif err, ok := v.(error); ok {\n\t\t\t\t\t\t\ttxn.NoticeError(err)\n\t\t\t\t\t\t\terrorNoticed = true\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif !errorNoticed {\n\t\t\t\t\ttxn.NoticeError(fmt.Errorf(\"request failed with status code %d\", status))\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc transactionName(r *http.Request) string {\n\t// From https://github.com/gorilla/handlers\n\turi := r.RequestURI\n\t// Requests using the CONNECT method over HTTP/2.0 must use\n\t// the authority field (aka r.Host) to identify the target.\n\t// Refer: https://httpwg.github.io/specs/rfc7540.html#CONNECT\n\tif r.ProtoMajor == 2 && r.Method == \"CONNECT\" {\n\t\turi = r.Host\n\t}\n\tif uri == \"\" {\n\t\turi = r.URL.RequestURI()\n\t}\n\treturn uri\n}\n"
  },
  {
    "path": "pki/helm.go",
    "content": "package pki\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"text/template\"\n\n\t\"github.com/pkg/errors\"\n\t\"github.com/smallstep/certificates/authority\"\n\tauthconfig \"github.com/smallstep/certificates/authority/config\"\n\t\"github.com/smallstep/certificates/authority/provisioner\"\n\t\"github.com/smallstep/certificates/templates\"\n\t\"github.com/smallstep/linkedca\"\n)\n\ntype helmVariables struct {\n\t*linkedca.Configuration\n\tDefaults     *linkedca.Defaults\n\tPassword     string\n\tEnableSSH    bool\n\tEnableAdmin  bool\n\tTLS          authconfig.TLSOptions\n\tProvisioners []provisioner.Interface\n}\n\n// WriteHelmTemplate a helm template to configure the\n// smallstep/step-certificates helm chart.\nfunc (p *PKI) WriteHelmTemplate(w io.Writer) error {\n\ttmpl, err := template.New(\"helm\").Funcs(templates.StepFuncMap()).Parse(helmTemplate)\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"error writing helm template\")\n\t}\n\n\t// Delete ssh section if it is not enabled\n\tif !p.options.enableSSH {\n\t\tp.Ssh = nil\n\t}\n\n\t// Convert provisioners to ca.json representation\n\tprovisioners := []provisioner.Interface{}\n\tfor _, p := range p.Authority.Provisioners {\n\t\tpp, err := authority.ProvisionerToCertificates(p)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tprovisioners = append(provisioners, pp)\n\t}\n\n\t// Add default ACME provisioner if enabled. Note that this logic is similar\n\t// to what's in p.GenerateConfig(), but that codepath isn't taken when\n\t// writing the Helm template. The default JWK provisioner is added earlier in\n\t// the process and that's part of the provisioners above.\n\t//\n\t// To prevent name clashes for the default ACME provisioner, we append \"-1\" to\n\t// the name if it already exists. See https://github.com/smallstep/cli/issues/1018\n\t// for the reason.\n\t//\n\t// TODO(hs): consider refactoring the initialization, so that this becomes\n\t// easier to reason about and maintain.\n\tif p.options.enableACME {\n\t\tacmeProvisionerName := \"acme\"\n\t\tfor _, prov := range provisioners {\n\t\t\tif prov.GetName() == acmeProvisionerName {\n\t\t\t\tacmeProvisionerName = fmt.Sprintf(\"%s-1\", acmeProvisionerName)\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tprovisioners = append(provisioners, &provisioner.ACME{\n\t\t\tType: \"ACME\",\n\t\t\tName: acmeProvisionerName,\n\t\t})\n\t}\n\n\t// Add default SSHPOP provisioner if enabled. Similar to the above, this is\n\t// the same as what happens in p.GenerateConfig(). To prevent name clashes for the\n\t// default SSHPOP provisioner, we append \"-1\" to it if it already exists. See\n\t// https://github.com/smallstep/cli/issues/1018 for the reason.\n\tif p.options.enableSSH {\n\t\tsshProvisionerName := \"sshpop\"\n\t\tfor _, prov := range provisioners {\n\t\t\tif prov.GetName() == sshProvisionerName {\n\t\t\t\tsshProvisionerName = fmt.Sprintf(\"%s-1\", sshProvisionerName)\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tprovisioners = append(provisioners, &provisioner.SSHPOP{\n\t\t\tType: \"SSHPOP\",\n\t\t\tName: sshProvisionerName,\n\t\t\tClaims: &provisioner.Claims{\n\t\t\t\tEnableSSHCA: &p.options.enableSSH,\n\t\t\t},\n\t\t})\n\t}\n\n\tif err := tmpl.Execute(w, helmVariables{\n\t\tConfiguration: &p.Configuration,\n\t\tDefaults:      &p.Defaults,\n\t\tPassword:      \"\",\n\t\tEnableSSH:     p.options.enableSSH,\n\t\tEnableAdmin:   p.options.enableAdmin,\n\t\tTLS:           authconfig.DefaultTLSOptions,\n\t\tProvisioners:  provisioners,\n\t}); err != nil {\n\t\treturn errors.Wrap(err, \"error executing helm template\")\n\t}\n\treturn nil\n}\n\nconst helmTemplate = `# Helm template\ninject:\n  enabled: true\n  # Config contains the configuration files ca.json and defaults.json\n  config:\n    files:\n      ca.json:\n        root: {{ first .Root }}\n        federateRoots: []\n        crt: {{ .Intermediate }}\n        key: {{ .IntermediateKey }}\n        {{- if .Kms }}\n        kms:\n          type: {{ lower (.Kms.Type | toString) }}\n        {{- end }}\n        {{- if .EnableSSH }}\n        ssh:\n          hostKey: {{ .Ssh.HostKey }}\n          userKey: {{ .Ssh.UserKey }}\n        {{- end }}\n        address: {{ .Address }}\n        dnsNames:\n        {{- range .DnsNames }}\n          - {{ . }}\n        {{- end }}\n        logger:\n          format: json\n        db:\n          type: badgerv2\n          dataSource: /home/step/db\n        authority:\n          enableAdmin: {{ .EnableAdmin }}\n          provisioners:\n          {{- range .Provisioners }}\n            - {{ . | toJson }}\n          {{- end }}\n        tls:\n          cipherSuites:\n          {{- range .TLS.CipherSuites }}\n            - {{ . }}\n          {{- end }}\n          minVersion: {{ .TLS.MinVersion }}\n          maxVersion: {{ .TLS.MaxVersion }}\n          renegotiation: {{ .TLS.Renegotiation }}\n\n      defaults.json:\n        ca-url: {{ .Defaults.CaUrl }}\n        ca-config: {{ .Defaults.CaConfig }}\n        fingerprint: {{ .Defaults.Fingerprint }}\n        root: {{ .Defaults.Root }}\n\n  # Certificates contains the root and intermediate certificate and \n  # optionally the SSH host and user public keys\n  certificates:\n    # intermediate_ca contains the text of the intermediate CA Certificate\n    intermediate_ca: |\n      {{- index .Files .Intermediate | toString | nindent 6 }}\n      \n    # root_ca contains the text of the root CA Certificate\n    root_ca: |\n      {{- first .Root | index .Files | toString | nindent 6 }}\n\n    {{- if .Ssh }}\n    # ssh_host_ca contains the text of the public ssh key for the SSH root CA\n    ssh_host_ca: {{ index .Files .Ssh.HostPublicKey | toString }}\n\n    # ssh_user_ca contains the text of the public ssh key for the SSH root CA\n    ssh_user_ca: {{ index .Files .Ssh.UserPublicKey | toString }}\n    {{- end }}\n\n  # Secrets contains the root and intermediate keys and optionally the SSH\n  # private keys\n  secrets:\n    # ca_password contains the password used to encrypt x509.intermediate_ca_key, ssh.host_ca_key and ssh.user_ca_key\n    # This value must be base64 encoded.\n    ca_password: {{ .Password | b64enc }}\n    provisioner_password: {{ .Password | b64enc}}\n\n    x509:\n      # intermediate_ca_key contains the contents of your encrypted intermediate CA key\n      intermediate_ca_key: |\n        {{- index .Files .IntermediateKey | toString | nindent 8 }}\n\n      # root_ca_key contains the contents of your encrypted root CA key\n      # Note that this value can be omitted without impacting the functionality of step-certificates\n      # If supplied, this should be encrypted using a unique password that is not used for encrypting\n      # the intermediate_ca_key, ssh.host_ca_key or ssh.user_ca_key.\n      root_ca_key: |\n        {{- first .RootKey | index .Files | toString | nindent 8 }}\n\n    {{- if .Ssh }}\n    ssh:\n      # ssh_host_ca_key contains the contents of your encrypted SSH Host CA key\n      host_ca_key: |\n        {{- index .Files .Ssh.HostKey | toString | nindent 8 }}\n\n      # ssh_user_ca_key contains the contents of your encrypted SSH User CA key\n      user_ca_key: |\n        {{- index .Files .Ssh.UserKey | toString | nindent 8 }}\n    {{- end }}\n`\n"
  },
  {
    "path": "pki/helm_test.go",
    "content": "package pki\n\nimport (\n\t\"bytes\"\n\t\"crypto/sha256\"\n\t\"crypto/x509\"\n\t\"encoding/hex\"\n\t\"encoding/json\"\n\t\"encoding/pem\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/stretchr/testify/assert\"\n\n\t\"github.com/smallstep/linkedca\"\n\t\"go.step.sm/crypto/jose\"\n\n\t\"github.com/smallstep/certificates/cas/apiv1\"\n)\n\nfunc TestPKI_WriteHelmTemplate(t *testing.T) {\n\tvar preparePKI = func(t *testing.T, opts ...Option) *PKI {\n\t\to := apiv1.Options{\n\t\t\tType:      \"softcas\",\n\t\t\tIsCreator: true,\n\t\t}\n\n\t\t// Add default WithHelm option\n\t\topts = append(opts, WithHelm())\n\n\t\t// TODO(hs): invoking `New` doesn't perform all operations that are executed\n\t\t// when `ca init --helm` is executed. Ideally this logic should be handled\n\t\t// in one place and probably inside of the PKI initialization. For testing\n\t\t// purposes the missing operations to fill a Helm template fully are faked\n\t\t// by `setKeyPair`, `setCertificates` and `setSSHSigningKeys`\n\t\tp, err := New(o, opts...)\n\t\tassert.NoError(t, err)\n\n\t\t// setKeyPair sets a predefined JWK and a default JWK provisioner. This is one\n\t\t// of the things performed in the `ca init` code that's not part of `New`, but\n\t\t// performed after that in p.GenerateKeyPairs`. We're currently using the same\n\t\t// JWK for every test to keep test variance small: we're not testing JWK generation\n\t\t// here after all. It's a bit dangerous to redefine the function here, but it's\n\t\t// the simplest way to make this fully testable without refactoring the init now.\n\t\t// The password for the predefined encrypted key is \\x01\\x03\\x03\\x07.\n\t\tsetKeyPair(t, p)\n\n\t\t// setCertificates sets some static intermediate and root CA certificate bytes. It\n\t\t// replaces the logic executed in `p.GenerateRootCertificate`, `p.WriteRootCertificate`,\n\t\t// and `p.GenerateIntermediateCertificate`.\n\t\tsetCertificates(t, p)\n\n\t\t// setSSHSigningKeys sets predefined SSH user and host certificate and key bytes.\n\t\t// This replaces the logic in `p.GenerateSSHSigningKeys`\n\t\tsetSSHSigningKeys(t, p)\n\n\t\treturn p\n\t}\n\ttype test struct {\n\t\tpki      *PKI\n\t\ttestFile string\n\t\twantErr  bool\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"ok/simple\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tpki:      preparePKI(t),\n\t\t\t\ttestFile: \"testdata/helm/simple.yml\",\n\t\t\t\twantErr:  false,\n\t\t\t}\n\t\t},\n\t\t\"ok/with-provisioner\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tpki:      preparePKI(t, WithProvisioner(\"a-provisioner\")),\n\t\t\t\ttestFile: \"testdata/helm/with-provisioner.yml\",\n\t\t\t\twantErr:  false,\n\t\t\t}\n\t\t},\n\t\t\"ok/with-acme\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tpki:      preparePKI(t, WithACME()),\n\t\t\t\ttestFile: \"testdata/helm/with-acme.yml\",\n\t\t\t\twantErr:  false,\n\t\t\t}\n\t\t},\n\t\t\"ok/with-acme-and-duplicate-provisioner-name\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tpki:      preparePKI(t, WithProvisioner(\"acme\"), WithACME()),\n\t\t\t\ttestFile: \"testdata/helm/with-acme-and-duplicate-provisioner-name.yml\",\n\t\t\t\twantErr:  false,\n\t\t\t}\n\t\t},\n\t\t\"ok/with-admin\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tpki:      preparePKI(t, WithAdmin()),\n\t\t\t\ttestFile: \"testdata/helm/with-admin.yml\",\n\t\t\t\twantErr:  false,\n\t\t\t}\n\t\t},\n\t\t\"ok/with-ssh\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tpki:      preparePKI(t, WithSSH()),\n\t\t\t\ttestFile: \"testdata/helm/with-ssh.yml\",\n\t\t\t\twantErr:  false,\n\t\t\t}\n\t\t},\n\t\t\"ok/with-ssh-and-duplicate-provisioner-name\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tpki:      preparePKI(t, WithProvisioner(\"sshpop\"), WithSSH()),\n\t\t\t\ttestFile: \"testdata/helm/with-ssh-and-duplicate-provisioner-name.yml\",\n\t\t\t\twantErr:  false,\n\t\t\t}\n\t\t},\n\t\t\"ok/with-ssh-and-acme\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\tpki:      preparePKI(t, WithSSH(), WithACME()),\n\t\t\t\ttestFile: \"testdata/helm/with-ssh-and-acme.yml\",\n\t\t\t\twantErr:  false,\n\t\t\t}\n\t\t},\n\t\t\"fail/authority.ProvisionerToCertificates\": func(t *testing.T) test {\n\t\t\tpki := preparePKI(t)\n\t\t\tpki.Authority.Provisioners = append(pki.Authority.Provisioners,\n\t\t\t\t&linkedca.Provisioner{\n\t\t\t\t\tType:    linkedca.Provisioner_JWK,\n\t\t\t\t\tName:    \"Broken JWK\",\n\t\t\t\t\tDetails: nil,\n\t\t\t\t},\n\t\t\t)\n\t\t\treturn test{\n\t\t\t\tpki:     pki,\n\t\t\t\twantErr: true,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\ttc := run(t)\n\t\tt.Run(name, func(t *testing.T) {\n\n\t\t\tw := &bytes.Buffer{}\n\t\t\tif err := tc.pki.WriteHelmTemplate(w); (err != nil) != tc.wantErr {\n\t\t\t\tt.Errorf(\"PKI.WriteHelmTemplate() error = %v, wantErr %v\", err, tc.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif tc.wantErr {\n\t\t\t\t// don't compare output if an error was expected on output\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\twantBytes, err := os.ReadFile(tc.testFile)\n\t\t\tassert.NoError(t, err)\n\t\t\tif diff := cmp.Diff(wantBytes, w.Bytes()); diff != \"\" {\n\t\t\t\tt.Logf(\"Generated Helm template did not match reference %q\\n\", tc.testFile)\n\t\t\t\tt.Errorf(\"Diff follows:\\n%s\\n\", diff)\n\t\t\t\tt.Errorf(\"Full output:\\n%s\\n\", w.Bytes())\n\t\t\t}\n\t\t})\n\t}\n}\n\n// setKeyPair sets a predefined JWK and a default JWK provisioner.\nfunc setKeyPair(t *testing.T, p *PKI) {\n\tt.Helper()\n\n\tvar err error\n\n\tp.ottPublicKey, err = jose.ParseKey([]byte(`{\"use\":\"sig\",\"kty\":\"EC\",\"kid\":\"zsUmysmDVoGJ71YoPHyZ-68tNihDaDaO5Mu7xX3M-_I\",\"crv\":\"P-256\",\"alg\":\"ES256\",\"x\":\"Pqnua4CzqKz6ua41J3yeWZ1sRkGt0UlCkbHv8H2DGuY\",\"y\":\"UhoZ_2ItDen9KQTcjay-ph-SBXH0mwqhHyvrrqIFDOI\"}`))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tp.ottPrivateKey, err = jose.ParseEncrypted(\"eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJjdHkiOiJqd2sranNvbiIsImVuYyI6IkEyNTZHQ00iLCJwMmMiOjEwMDAwMCwicDJzIjoiZjVvdGVRS2hvOXl4MmQtSGlMZi05QSJ9.eYA6tt3fNuUpoxKWDT7P0Lbn2juxhEbTxEnwEMbjlYLLQ3sxL-dYTA.ven-FhmdjlC9itH0.a2jRTarN9vPd6F_mWnNBlOn6KbfMjCApmci2t65XbAsLzYFzhI_79Ykm5ueMYTupWLTjBJctl-g51ZHmsSB55pStbpoyyLNAsUX2E1fTmHe-Ni8bRrspwLv15FoN1Xo1g0mpR-ufWIFxOsW-QIfnMmMIIkygVuHFXmg2tFpzTNNG5aS29K3dN2nyk0WJrdIq79hZSTqVkkBU25Yu3A46sgjcM86XcIJJ2XUEih_KWEa6T1YrkixGu96pebjVqbO0R6dbDckfPF7FqNnwPHVtb1ACFpEYoOJVIbUCMaARBpWsxYhjJZlEM__XA46l8snFQDkNY3CdN0p1_gF3ckA.JLmq9nmu1h9oUi1S8ZxYjA\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tvar claims *linkedca.Claims\n\tif p.options.enableSSH {\n\t\tclaims = &linkedca.Claims{\n\t\t\tSsh: &linkedca.SSHClaims{\n\t\t\t\tEnabled: true,\n\t\t\t},\n\t\t}\n\t}\n\n\tpublicKey, err := json.Marshal(p.ottPublicKey)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tencryptedKey, err := p.ottPrivateKey.CompactSerialize()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tp.Authority.Provisioners = append(p.Authority.Provisioners, &linkedca.Provisioner{\n\t\tType:   linkedca.Provisioner_JWK,\n\t\tName:   p.options.provisioner,\n\t\tClaims: claims,\n\t\tDetails: &linkedca.ProvisionerDetails{\n\t\t\tData: &linkedca.ProvisionerDetails_JWK{\n\t\t\t\tJWK: &linkedca.JWKProvisioner{\n\t\t\t\t\tPublicKey:           publicKey,\n\t\t\t\t\tEncryptedPrivateKey: []byte(encryptedKey),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t})\n}\n\n// setCertificates sets some static, gibberish intermediate and root CA certificate and key bytes.\nfunc setCertificates(_ *testing.T, p *PKI) {\n\traw := []byte(\"these are just some fake root CA cert bytes\")\n\tp.Files[p.Root[0]] = encodeCertificate(&x509.Certificate{Raw: raw})\n\tp.Files[p.RootKey[0]] = pem.EncodeToMemory(&pem.Block{\n\t\tType:  \"EC PRIVATE KEY\",\n\t\tBytes: []byte(\"these are just some fake root CA key bytes\"),\n\t})\n\tp.Files[p.Intermediate] = encodeCertificate(&x509.Certificate{Raw: []byte(\"these are just some fake intermediate CA cert bytes\")})\n\tp.Files[p.IntermediateKey] = pem.EncodeToMemory(&pem.Block{\n\t\tType:  \"EC PRIVATE KEY\",\n\t\tBytes: []byte(\"these are just some fake intermediate CA key bytes\"),\n\t})\n\tsum := sha256.Sum256(raw)\n\tp.Defaults.Fingerprint = strings.ToLower(hex.EncodeToString(sum[:]))\n}\n\n// setSSHSigningKeys sets some static, gibberish ssh user and host CA certificate and key bytes.\nfunc setSSHSigningKeys(_ *testing.T, p *PKI) {\n\tif !p.options.enableSSH {\n\t\treturn\n\t}\n\n\tp.Files[p.Ssh.HostKey] = pem.EncodeToMemory(&pem.Block{\n\t\tType:  \"EC PRIVATE KEY\",\n\t\tBytes: []byte(\"fake ssh host key bytes\"),\n\t})\n\tp.Files[p.Ssh.HostPublicKey] = []byte(\"ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBJ0IdS5sZm6KITBMZLEJD6b5ROVraYHcAOr3feFel8r1Wp4DRPR1oU0W00J/zjNBRBbANlJoYN4x/8WNNVZ49Ms=\")\n\tp.Files[p.Ssh.UserKey] = pem.EncodeToMemory(&pem.Block{\n\t\tType:  \"EC PRIVATE KEY\",\n\t\tBytes: []byte(\"fake ssh user key bytes\"),\n\t})\n\tp.Files[p.Ssh.UserPublicKey] = []byte(\"ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEWA1qUxaGwVNErsvEOGe2d6TvLMF+aiVpuOiIEvpMJ3JeJmecLQctjWqeIbpSvy6/gRa7c82Ge5rLlapYmOChs=\")\n}\n"
  },
  {
    "path": "pki/pki.go",
    "content": "package pki\n\nimport (\n\t\"context\"\n\t\"crypto\"\n\t\"crypto/sha256\"\n\t\"crypto/x509\"\n\t\"crypto/x509/pkix\"\n\t\"encoding/hex\"\n\t\"encoding/json\"\n\t\"encoding/pem\"\n\t\"fmt\"\n\t\"net\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n\t\"golang.org/x/crypto/ssh\"\n\n\t\"github.com/smallstep/cli-utils/errs\"\n\t\"github.com/smallstep/cli-utils/fileutil\"\n\t\"github.com/smallstep/cli-utils/step\"\n\t\"github.com/smallstep/cli-utils/ui\"\n\t\"github.com/smallstep/linkedca\"\n\t\"github.com/smallstep/nosql\"\n\t\"go.step.sm/crypto/jose\"\n\t\"go.step.sm/crypto/kms\"\n\tkmsapi \"go.step.sm/crypto/kms/apiv1\"\n\t\"go.step.sm/crypto/pemutil\"\n\n\t\"github.com/smallstep/certificates/authority\"\n\t\"github.com/smallstep/certificates/authority/admin\"\n\tadmindb \"github.com/smallstep/certificates/authority/admin/db/nosql\"\n\tauthconfig \"github.com/smallstep/certificates/authority/config\"\n\t\"github.com/smallstep/certificates/authority/provisioner\"\n\t\"github.com/smallstep/certificates/ca\"\n\t\"github.com/smallstep/certificates/cas\"\n\t\"github.com/smallstep/certificates/cas/apiv1\"\n\t\"github.com/smallstep/certificates/db\"\n)\n\n// DeploymentType defines witch type of deployment a user is initializing\ntype DeploymentType int\n\nconst (\n\t// StandaloneDeployment is a deployment where all the components like keys,\n\t// provisioners, admins, certificates and others are managed by the user.\n\tStandaloneDeployment DeploymentType = iota\n\t// LinkedDeployment is a deployment where the keys are managed by the user,\n\t// but provisioners, admins and the record of certificates are managed in\n\t// the cloud.\n\tLinkedDeployment\n\t// HostedDeployment is a deployment where all the components are managed in\n\t// the cloud by smallstep.com/certificate-manager.\n\tHostedDeployment\n)\n\n// String returns the string version of the deployment type.\nfunc (d DeploymentType) String() string {\n\tswitch d {\n\tcase StandaloneDeployment:\n\t\treturn \"standalone\"\n\tcase LinkedDeployment:\n\t\treturn \"linked\"\n\tcase HostedDeployment:\n\t\treturn \"hosted\"\n\tdefault:\n\t\treturn \"unknown\"\n\t}\n}\n\nconst (\n\t// ConfigPath is the directory name under the step path where the configuration\n\t// files will be stored.\n\tconfigPath = \"config\"\n\t// PublicPath is the directory name under the step path where the public keys\n\t// will be stored.\n\tpublicPath = \"certs\"\n\t// PublicPath is the directory name under the step path where the private keys\n\t// will be stored.\n\tprivatePath = \"secrets\"\n\t// DBPath is the directory name under the step path where the private keys\n\t// will be stored.\n\tdbPath = \"db\"\n\t// templatesPath is the directory to store templates\n\ttemplatesPath = \"templates\"\n)\n\n// GetDBPath returns the path where the file-system persistence is stored\n// based on the $(step path).\nfunc GetDBPath() string {\n\treturn filepath.Join(step.Path(), dbPath)\n}\n\n// GetConfigPath returns the directory where the configuration files are stored\n// based on the $(step path).\nfunc GetConfigPath() string {\n\treturn filepath.Join(step.Path(), configPath)\n}\n\n// GetProfileConfigPath returns the directory where the profile configuration\n// files are stored based on the $(step path).\nfunc GetProfileConfigPath() string {\n\treturn filepath.Join(step.ProfilePath(), configPath)\n}\n\n// GetPublicPath returns the directory where the public keys are stored based on\n// the $(step path).\nfunc GetPublicPath() string {\n\treturn filepath.Join(step.Path(), publicPath)\n}\n\n// GetSecretsPath returns the directory where the private keys are stored based\n// on the $(step path).\nfunc GetSecretsPath() string {\n\treturn filepath.Join(step.Path(), privatePath)\n}\n\n// GetRootCAPath returns the path where the root CA is stored based on the\n// $(step path).\nfunc GetRootCAPath() string {\n\treturn filepath.Join(step.Path(), publicPath, \"root_ca.crt\")\n}\n\n// GetOTTKeyPath returns the path where the one-time token key is stored based\n// on the $(step path).\nfunc GetOTTKeyPath() string {\n\treturn filepath.Join(step.Path(), privatePath, \"ott_key\")\n}\n\n// GetTemplatesPath returns the path where the templates are stored.\nfunc GetTemplatesPath() string {\n\treturn filepath.Join(step.Path(), templatesPath)\n}\n\n// GetProvisioners returns the map of provisioners on the given CA.\nfunc GetProvisioners(caURL, rootFile string) (provisioner.List, error) {\n\tif rootFile == \"\" {\n\t\trootFile = GetRootCAPath()\n\t}\n\tclient, err := ca.NewClient(caURL, ca.WithRootFile(rootFile))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tcursor := \"\"\n\tprovisioners := provisioner.List{}\n\tfor {\n\t\tresp, err := client.Provisioners(ca.WithProvisionerCursor(cursor), ca.WithProvisionerLimit(100))\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tprovisioners = append(provisioners, resp.Provisioners...)\n\t\tif resp.NextCursor == \"\" {\n\t\t\treturn provisioners, nil\n\t\t}\n\t\tcursor = resp.NextCursor\n\t}\n}\n\n// GetProvisionerKey returns the encrypted provisioner key with the for the\n// given kid.\nfunc GetProvisionerKey(caURL, rootFile, kid string) (string, error) {\n\tif rootFile == \"\" {\n\t\trootFile = GetRootCAPath()\n\t}\n\tclient, err := ca.NewClient(caURL, ca.WithRootFile(rootFile))\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tresp, err := client.ProvisionerKey(kid)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn resp.Key, nil\n}\n\ntype options struct {\n\tprovisioner        string\n\tsuperAdminSubject  string\n\tpkiOnly            bool\n\tenableACME         bool\n\tenableSSH          bool\n\tenableAdmin        bool\n\tnoDB               bool\n\tisHelm             bool\n\tdeploymentType     DeploymentType\n\trootKeyURI         string\n\tintermediateKeyURI string\n\thostKeyURI         string\n\tuserKeyURI         string\n}\n\n// Option is the type of a configuration option on the pki constructor.\ntype Option func(p *PKI)\n\n// WithAddress sets the listen address of step-ca.\nfunc WithAddress(s string) Option {\n\treturn func(p *PKI) {\n\t\tp.Address = s\n\t}\n}\n\n// WithCaURL sets the default ca-url of step-ca.\nfunc WithCaURL(s string) Option {\n\treturn func(p *PKI) {\n\t\tp.Defaults.CaUrl = s\n\t}\n}\n\n// WithDNSNames sets the SANs of step-ca.\nfunc WithDNSNames(s []string) Option {\n\treturn func(p *PKI) {\n\t\tp.DnsNames = s\n\t}\n}\n\n// WithProvisioner defines the name of the default provisioner.\nfunc WithProvisioner(s string) Option {\n\treturn func(p *PKI) {\n\t\tp.options.provisioner = s\n\t}\n}\n\n// WithSuperAdminSubject defines the subject of the first\n// super admin for use with the Admin API. The admin will belong\n// to the first JWK provisioner.\nfunc WithSuperAdminSubject(s string) Option {\n\treturn func(p *PKI) {\n\t\tp.options.superAdminSubject = s\n\t}\n}\n\n// WithPKIOnly will only generate the PKI without the step-ca config files.\nfunc WithPKIOnly() Option {\n\treturn func(p *PKI) {\n\t\tp.options.pkiOnly = true\n\t}\n}\n\n// WithACME enables acme provisioner in step-ca.\nfunc WithACME() Option {\n\treturn func(p *PKI) {\n\t\tp.options.enableACME = true\n\t}\n}\n\n// WithSSH enables ssh in step-ca.\nfunc WithSSH() Option {\n\treturn func(p *PKI) {\n\t\tp.options.enableSSH = true\n\t}\n}\n\n// WithAdmin enables the admin api in step-ca.\nfunc WithAdmin() Option {\n\treturn func(p *PKI) {\n\t\tp.options.enableAdmin = true\n\t}\n}\n\n// WithNoDB disables the db in step-ca.\nfunc WithNoDB() Option {\n\treturn func(p *PKI) {\n\t\tp.options.noDB = true\n\t}\n}\n\n// WithHelm configures the pki to create a helm values.yaml.\nfunc WithHelm() Option {\n\treturn func(p *PKI) {\n\t\tp.options.isHelm = true\n\t}\n}\n\n// WithDeploymentType defines the deployment type of step-ca.\nfunc WithDeploymentType(dt DeploymentType) Option {\n\treturn func(p *PKI) {\n\t\tp.options.deploymentType = dt\n\t}\n}\n\n// WithKMS enables the kms with the given name.\nfunc WithKMS(name string) Option {\n\treturn func(p *PKI) {\n\t\ttyp := linkedca.KMS_Type_value[strings.ToUpper(name)]\n\t\tp.Configuration.Kms = &linkedca.KMS{\n\t\t\tType: linkedca.KMS_Type(typ),\n\t\t}\n\t}\n}\n\n// WithKeyURIs defines the key uris for X.509 and SSH keys.\nfunc WithKeyURIs(rootKey, intermediateKey, hostKey, userKey string) Option {\n\treturn func(p *PKI) {\n\t\tp.options.rootKeyURI = rootKey\n\t\tp.options.intermediateKeyURI = intermediateKey\n\t\tp.options.hostKeyURI = hostKey\n\t\tp.options.userKeyURI = userKey\n\t}\n}\n\n// PKI represents the Public Key Infrastructure used by a certificate authority.\ntype PKI struct {\n\tlinkedca.Configuration\n\tDefaults        linkedca.Defaults\n\tcasOptions      apiv1.Options\n\tcaService       apiv1.CertificateAuthorityService\n\tcaCreator       apiv1.CertificateAuthorityCreator\n\tkeyManager      kmsapi.KeyManager\n\tconfig          string\n\tdefaults        string\n\tprofileDefaults string\n\tottPublicKey    *jose.JSONWebKey\n\tottPrivateKey   *jose.JSONWebEncryption\n\toptions         *options\n}\n\n// New creates a new PKI configuration.\nfunc New(o apiv1.Options, opts ...Option) (*PKI, error) {\n\t// TODO(hs): invoking `New` with a context active will use values from\n\t// that CA context while generating the context. Thay may or may not\n\t// be fully expected and/or what we want. This specific behavior was\n\t// changed after not relying on the `init` inside of `step`, resulting in\n\t// the default context being active if `step.Init` isn't called explicitly.\n\t// It can still result in surprising results, though.\n\tcurrentCtx := step.Contexts().GetCurrent()\n\tcaService, err := cas.New(context.Background(), o)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar caCreator apiv1.CertificateAuthorityCreator\n\tif o.IsCreator {\n\t\tcreator, ok := caService.(apiv1.CertificateAuthorityCreator)\n\t\tif !ok {\n\t\t\treturn nil, errors.Errorf(\"cas type %q does not implement CertificateAuthorityCreator\", o.Type)\n\t\t}\n\t\tcaCreator = creator\n\t}\n\n\t// get absolute path for dir/name\n\tgetPath := func(dir string, name string) (string, error) {\n\t\ts, err := filepath.Abs(filepath.Join(dir, name))\n\t\treturn s, errors.Wrapf(err, \"error getting absolute path for %s\", name)\n\t}\n\n\tp := &PKI{\n\t\tConfiguration: linkedca.Configuration{\n\t\t\tAddress:   \"127.0.0.1:9000\",\n\t\t\tDnsNames:  []string{\"127.0.0.1\"},\n\t\t\tSsh:       &linkedca.SSH{},\n\t\t\tAuthority: &linkedca.Authority{},\n\t\t\tFiles:     make(map[string][]byte),\n\t\t},\n\t\tcasOptions: o,\n\t\tcaService:  caService,\n\t\tcaCreator:  caCreator,\n\t\tkeyManager: o.KeyManager,\n\t\toptions: &options{\n\t\t\tprovisioner: \"step-cli\",\n\t\t},\n\t}\n\tfor _, fn := range opts {\n\t\tfn(p)\n\t}\n\n\t// Use default key manager\n\tif p.keyManager == nil {\n\t\tp.keyManager = kms.Default\n\t}\n\n\t// Use /home/step as the step path in helm configurations.\n\t// Use the current step path when creating pki in files.\n\tvar public, private, cfg string\n\tif p.options.isHelm {\n\t\tpublic = \"/home/step/certs\"\n\t\tprivate = \"/home/step/secrets\"\n\t\tcfg = \"/home/step/config\"\n\t} else {\n\t\tpublic = GetPublicPath()\n\t\tprivate = GetSecretsPath()\n\t\tcfg = GetConfigPath()\n\t\t// Create directories\n\t\tdirs := []string{public, private, cfg, GetTemplatesPath()}\n\t\tif currentCtx != nil {\n\t\t\tdirs = append(dirs, GetProfileConfigPath())\n\t\t}\n\t\tfor _, name := range dirs {\n\t\t\tif _, err := os.Stat(name); os.IsNotExist(err) {\n\t\t\t\tif err = os.MkdirAll(name, 0700); err != nil {\n\t\t\t\t\treturn nil, errs.FileError(err, name)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tif p.Defaults.CaUrl == \"\" {\n\t\tp.Defaults.CaUrl = p.DnsNames[0]\n\t\t_, port, err := net.SplitHostPort(p.Address)\n\t\tif err != nil {\n\t\t\treturn nil, errors.Wrapf(err, \"error parsing %s\", p.Address)\n\t\t}\n\t\t// On k8s we usually access through a service, and this is configured on\n\t\t// port 443 by default.\n\t\tif port == \"443\" || p.options.isHelm {\n\t\t\tp.Defaults.CaUrl = fmt.Sprintf(\"https://%s\", p.Defaults.CaUrl)\n\t\t} else {\n\t\t\tp.Defaults.CaUrl = fmt.Sprintf(\"https://%s\", net.JoinHostPort(p.Defaults.CaUrl, port))\n\t\t}\n\t}\n\n\troot, err := getPath(public, \"root_ca.crt\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\trootKey, err := getPath(private, \"root_ca_key\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tp.Root = []string{root}\n\tp.RootKey = []string{rootKey}\n\tp.Defaults.Root = root\n\n\tif p.Intermediate, err = getPath(public, \"intermediate_ca.crt\"); err != nil {\n\t\treturn nil, err\n\t}\n\tif p.IntermediateKey, err = getPath(private, \"intermediate_ca_key\"); err != nil {\n\t\treturn nil, err\n\t}\n\tif p.Ssh.HostPublicKey, err = getPath(public, \"ssh_host_ca_key.pub\"); err != nil {\n\t\treturn nil, err\n\t}\n\tif p.Ssh.UserPublicKey, err = getPath(public, \"ssh_user_ca_key.pub\"); err != nil {\n\t\treturn nil, err\n\t}\n\tif p.Ssh.HostKey, err = getPath(private, \"ssh_host_ca_key\"); err != nil {\n\t\treturn nil, err\n\t}\n\tif p.Ssh.UserKey, err = getPath(private, \"ssh_user_ca_key\"); err != nil {\n\t\treturn nil, err\n\t}\n\tif p.defaults, err = getPath(cfg, \"defaults.json\"); err != nil {\n\t\treturn nil, err\n\t}\n\tif currentCtx != nil {\n\t\tp.profileDefaults = currentCtx.ProfileDefaultsFile()\n\t}\n\n\tif p.config, err = getPath(cfg, \"ca.json\"); err != nil {\n\t\treturn nil, err\n\t}\n\tp.Defaults.CaConfig = p.config\n\n\treturn p, nil\n}\n\n// GetCAConfigPath returns the path of the CA configuration file.\nfunc (p *PKI) GetCAConfigPath() string {\n\treturn p.config\n}\n\n// GetRootFingerprint returns the root fingerprint.\nfunc (p *PKI) GetRootFingerprint() string {\n\treturn p.Defaults.Fingerprint\n}\n\n// GenerateKeyPairs generates the key pairs used by the certificate authority.\nfunc (p *PKI) GenerateKeyPairs(pass []byte) error {\n\tvar err error\n\t// Create OTT key pair, the user doesn't need to know about this.\n\tp.ottPublicKey, p.ottPrivateKey, err = jose.GenerateDefaultKeyPair(pass)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar claims *linkedca.Claims\n\tif p.options.enableSSH {\n\t\tclaims = &linkedca.Claims{\n\t\t\tSsh: &linkedca.SSHClaims{\n\t\t\t\tEnabled: true,\n\t\t\t},\n\t\t}\n\t}\n\n\t// Add JWK provisioner to the configuration.\n\tpublicKey, err := json.Marshal(p.ottPublicKey)\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"error marshaling public key\")\n\t}\n\tencryptedKey, err := p.ottPrivateKey.CompactSerialize()\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"error serializing private key\")\n\t}\n\tp.Authority.Provisioners = append(p.Authority.Provisioners, &linkedca.Provisioner{\n\t\tType:   linkedca.Provisioner_JWK,\n\t\tName:   p.options.provisioner,\n\t\tClaims: claims,\n\t\tDetails: &linkedca.ProvisionerDetails{\n\t\t\tData: &linkedca.ProvisionerDetails_JWK{\n\t\t\t\tJWK: &linkedca.JWKProvisioner{\n\t\t\t\t\tPublicKey:           publicKey,\n\t\t\t\t\tEncryptedPrivateKey: []byte(encryptedKey),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t})\n\n\treturn nil\n}\n\n// GenerateRootCertificate generates a root certificate with the given name\n// and using the default key type.\nfunc (p *PKI) GenerateRootCertificate(name, org, resource string, pass []byte) (*apiv1.CreateCertificateAuthorityResponse, error) {\n\tif uri := p.options.rootKeyURI; uri != \"\" {\n\t\tp.RootKey[0] = uri\n\t}\n\n\tresp, err := p.caCreator.CreateCertificateAuthority(&apiv1.CreateCertificateAuthorityRequest{\n\t\tName:     resource + \"-Root-CA\",\n\t\tType:     apiv1.RootCA,\n\t\tLifetime: 10 * 365 * 24 * time.Hour,\n\t\tCreateKey: &apiv1.CreateKeyRequest{\n\t\t\tName:               p.RootKey[0],\n\t\t\tSignatureAlgorithm: kmsapi.UnspecifiedSignAlgorithm,\n\t\t},\n\t\tTemplate: &x509.Certificate{\n\t\t\tSubject: pkix.Name{\n\t\t\t\tCommonName:   name + \" Root CA\",\n\t\t\t\tOrganization: []string{org},\n\t\t\t},\n\t\t\tKeyUsage:              x509.KeyUsageCertSign | x509.KeyUsageCRLSign,\n\t\t\tBasicConstraintsValid: true,\n\t\t\tIsCA:                  true,\n\t\t\tMaxPathLen:            1,\n\t\t\tMaxPathLenZero:        false,\n\t\t},\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Replace key name with the one from the key manager if available. On\n\t// softcas this will be the original filename, on any other kms will be the\n\t// uri to the key.\n\tif resp.KeyName != \"\" {\n\t\tp.RootKey[0] = resp.KeyName\n\t}\n\n\t// PrivateKey will only be set if we have access to it (SoftCAS).\n\tif err := p.WriteRootCertificate(resp.Certificate, resp.PrivateKey, pass); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn resp, nil\n}\n\n// WriteRootCertificate writes to the buffer the given certificate and key if given.\nfunc (p *PKI) WriteRootCertificate(rootCrt *x509.Certificate, rootKey interface{}, pass []byte) error {\n\tp.Files[p.Root[0]] = encodeCertificate(rootCrt)\n\tif rootKey != nil {\n\t\tvar err error\n\t\tp.Files[p.RootKey[0]], err = encodePrivateKey(rootKey, pass)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tsum := sha256.Sum256(rootCrt.Raw)\n\tp.Defaults.Fingerprint = strings.ToLower(hex.EncodeToString(sum[:]))\n\treturn nil\n}\n\n// GenerateIntermediateCertificate generates an intermediate certificate with\n// the given name and using the default key type.\nfunc (p *PKI) GenerateIntermediateCertificate(name, org, resource string, parent *apiv1.CreateCertificateAuthorityResponse, pass []byte) error {\n\tif uri := p.options.intermediateKeyURI; uri != \"\" {\n\t\tp.IntermediateKey = uri\n\t}\n\n\tresp, err := p.caCreator.CreateCertificateAuthority(&apiv1.CreateCertificateAuthorityRequest{\n\t\tName:     resource + \"-Intermediate-CA\",\n\t\tType:     apiv1.IntermediateCA,\n\t\tLifetime: 10 * 365 * 24 * time.Hour,\n\t\tCreateKey: &apiv1.CreateKeyRequest{\n\t\t\tName:               p.IntermediateKey,\n\t\t\tSignatureAlgorithm: kmsapi.UnspecifiedSignAlgorithm,\n\t\t},\n\t\tTemplate: &x509.Certificate{\n\t\t\tSubject: pkix.Name{\n\t\t\t\tCommonName:   name + \" Intermediate CA\",\n\t\t\t\tOrganization: []string{org},\n\t\t\t},\n\t\t\tKeyUsage:              x509.KeyUsageCertSign | x509.KeyUsageCRLSign,\n\t\t\tBasicConstraintsValid: true,\n\t\t\tIsCA:                  true,\n\t\t\tMaxPathLen:            0,\n\t\t\tMaxPathLenZero:        true,\n\t\t},\n\t\tParent: parent,\n\t})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tp.casOptions.CertificateAuthority = resp.Name\n\tp.Files[p.Intermediate] = encodeCertificate(resp.Certificate)\n\n\t// Replace the key name with the one from the key manager. On softcas this\n\t// will be the original filename, on any other kms will be the uri to the\n\t// key.\n\tif resp.KeyName != \"\" {\n\t\tp.IntermediateKey = resp.KeyName\n\t}\n\n\t// If a kms is used it will not have the private key\n\tif resp.PrivateKey != nil {\n\t\tp.Files[p.IntermediateKey], err = encodePrivateKey(resp.PrivateKey, pass)\n\t}\n\n\treturn err\n}\n\n// CreateCertificateAuthorityResponse returns a\n// CreateCertificateAuthorityResponse that can be used as a parent of a\n// CreateCertificateAuthority request.\nfunc (p *PKI) CreateCertificateAuthorityResponse(cert *x509.Certificate, key crypto.PrivateKey) *apiv1.CreateCertificateAuthorityResponse {\n\tsigner, _ := key.(crypto.Signer)\n\treturn &apiv1.CreateCertificateAuthorityResponse{\n\t\tCertificate: cert,\n\t\tPrivateKey:  key,\n\t\tSigner:      signer,\n\t}\n}\n\n// GetCertificateAuthority attempts to load the certificate authority from the\n// RA.\nfunc (p *PKI) GetCertificateAuthority() error {\n\tsrv, ok := p.caService.(apiv1.CertificateAuthorityGetter)\n\tif !ok {\n\t\treturn nil\n\t}\n\n\tresp, err := srv.GetCertificateAuthority(&apiv1.GetCertificateAuthorityRequest{\n\t\tName: p.casOptions.CertificateAuthority,\n\t})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif err := p.WriteRootCertificate(resp.RootCertificate, nil, nil); err != nil {\n\t\treturn err\n\t}\n\n\t// Issuer is in the RA\n\tp.Intermediate = \"\"\n\tp.IntermediateKey = \"\"\n\n\treturn nil\n}\n\n// GenerateSSHSigningKeys generates and encrypts a private key used for signing\n// SSH user certificates and a private key used for signing host certificates.\nfunc (p *PKI) GenerateSSHSigningKeys(password []byte) error {\n\t// Enable SSH\n\tp.options.enableSSH = true // TODO(hs): change this function to not mutate configuration state\n\n\t// Create SSH key used to sign host certificates. Using\n\t// kmsapi.UnspecifiedSignAlgorithm will default to the default algorithm.\n\tname := p.Ssh.HostKey\n\tif uri := p.options.hostKeyURI; uri != \"\" {\n\t\tname = uri\n\t}\n\tresp, err := p.keyManager.CreateKey(&kmsapi.CreateKeyRequest{\n\t\tName:               name,\n\t\tSignatureAlgorithm: kmsapi.UnspecifiedSignAlgorithm,\n\t})\n\tif err != nil {\n\t\treturn err\n\t}\n\tsshKey, err := ssh.NewPublicKey(resp.PublicKey)\n\tif err != nil {\n\t\treturn errors.Wrapf(err, \"error converting public key\")\n\t}\n\tp.Files[p.Ssh.HostPublicKey] = ssh.MarshalAuthorizedKey(sshKey)\n\n\t// On softkms we will have the private key\n\tif resp.PrivateKey != nil {\n\t\tp.Files[p.Ssh.HostKey], err = encodePrivateKey(resp.PrivateKey, password)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t} else {\n\t\tp.Ssh.HostKey = resp.Name\n\t}\n\n\t// Create SSH key used to sign user certificates. Using\n\t// kmsapi.UnspecifiedSignAlgorithm will default to the default algorithm.\n\tname = p.Ssh.UserKey\n\tif uri := p.options.userKeyURI; uri != \"\" {\n\t\tname = uri\n\t}\n\tresp, err = p.keyManager.CreateKey(&kmsapi.CreateKeyRequest{\n\t\tName:               name,\n\t\tSignatureAlgorithm: kmsapi.UnspecifiedSignAlgorithm,\n\t})\n\tif err != nil {\n\t\treturn err\n\t}\n\tsshKey, err = ssh.NewPublicKey(resp.PublicKey)\n\tif err != nil {\n\t\treturn errors.Wrapf(err, \"error converting public key\")\n\t}\n\tp.Files[p.Ssh.UserPublicKey] = ssh.MarshalAuthorizedKey(sshKey)\n\n\t// On softkms we will have the private key\n\tif resp.PrivateKey != nil {\n\t\tp.Files[p.Ssh.UserKey], err = encodePrivateKey(resp.PrivateKey, password)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t} else {\n\t\tp.Ssh.UserKey = resp.Name\n\t}\n\n\treturn nil\n}\n\n// WriteFiles writes on disk the previously generated files.\nfunc (p *PKI) WriteFiles() error {\n\tfor fn, b := range p.Files {\n\t\tif err := fileutil.WriteFile(fn, b, 0600); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (p *PKI) askFeedback() {\n\tui.Println()\n\tui.Println(\"\\033[1mFEEDBACK\\033[0m 😍 🍻\")\n\tui.Println(\"  The \\033[1mstep\\033[0m utility is not instrumented for usage statistics. It does not phone\")\n\tui.Println(\"  home. But your feedback is extremely valuable. Any information you can provide\")\n\tui.Println(\"  regarding how you’re using `step` helps. Please send us a sentence or two,\")\n\tui.Println(\"  good or bad at \\033[1mfeedback@smallstep.com\\033[0m or join GitHub Discussions\")\n\tui.Println(\"  \\033[1mhttps://github.com/smallstep/certificates/discussions\\033[0m and our Discord \")\n\tui.Println(\"  \\033[1mhttps://u.step.sm/discord\\033[0m.\")\n\n\tif p.options.deploymentType == LinkedDeployment {\n\t\tui.Println()\n\t\tui.Println(\"\\033[1mNEXT STEPS\\033[0m\")\n\t\tui.Println(\"  1. Contact us at \\033[1mhttps://u.step.sm/cm\\033[0m to create a Certificate Manager account\")\n\t\tui.Println(\"  2. Add a new authority and select \\\"Link a step-ca instance\\\"\")\n\t\tui.Println(\"  3. Follow instructions in browser to start `step-ca` using the `--token` flag\")\n\t\tui.Println()\n\t}\n}\n\nfunc (p *PKI) tellPKI() {\n\tui.Println()\n\tswitch {\n\tcase p.casOptions.Is(apiv1.SoftCAS):\n\t\tui.PrintSelected(\"Root certificate\", p.Root[0])\n\t\tui.PrintSelected(\"Root private key\", p.RootKey[0])\n\t\tui.PrintSelected(\"Root fingerprint\", p.Defaults.Fingerprint)\n\t\tui.PrintSelected(\"Intermediate certificate\", p.Intermediate)\n\t\tui.PrintSelected(\"Intermediate private key\", p.IntermediateKey)\n\tcase p.Defaults.Fingerprint != \"\":\n\t\tui.PrintSelected(\"Root certificate\", p.Root[0])\n\t\tui.PrintSelected(\"Root fingerprint\", p.Defaults.Fingerprint)\n\tdefault:\n\t\tui.Printf(`{{ \"%s\" | red }} {{ \"Root certificate:\" | bold }} failed to retrieve it from RA`+\"\\n\", ui.IconBad)\n\t}\n\tif p.options.enableSSH {\n\t\tui.PrintSelected(\"SSH user public key\", p.Ssh.UserPublicKey)\n\t\tui.PrintSelected(\"SSH user private key\", p.Ssh.UserKey)\n\t\tui.PrintSelected(\"SSH host public key\", p.Ssh.HostPublicKey)\n\t\tui.PrintSelected(\"SSH host private key\", p.Ssh.HostKey)\n\t}\n}\n\ntype caDefaults struct {\n\tCAUrl       string `json:\"ca-url\"`\n\tCAConfig    string `json:\"ca-config\"`\n\tFingerprint string `json:\"fingerprint\"`\n\tRoot        string `json:\"root\"`\n}\n\n// ConfigOption is the type for modifiers over the auth config object.\ntype ConfigOption func(c *authconfig.Config) error\n\n// GenerateConfig returns the step certificates configuration.\nfunc (p *PKI) GenerateConfig(opt ...ConfigOption) (*authconfig.Config, error) {\n\tvar authorityOptions *apiv1.Options\n\tif !p.casOptions.Is(apiv1.SoftCAS) {\n\t\tauthorityOptions = &p.casOptions\n\t}\n\n\tcfg := &authconfig.Config{\n\t\tRoot:             p.Root,\n\t\tFederatedRoots:   p.FederatedRoots,\n\t\tIntermediateCert: p.Intermediate,\n\t\tIntermediateKey:  p.IntermediateKey,\n\t\tAddress:          p.Address,\n\t\tDNSNames:         p.DnsNames,\n\t\tLogger:           []byte(`{\"format\": \"text\"}`),\n\t\tDB: &db.Config{\n\t\t\tType:       \"badgerv2\",\n\t\t\tDataSource: GetDBPath(),\n\t\t},\n\t\tAuthorityConfig: &authconfig.AuthConfig{\n\t\t\tOptions:              authorityOptions,\n\t\t\tDisableIssuedAtCheck: false,\n\t\t\tEnableAdmin:          false,\n\t\t},\n\t\tTLS:       &authconfig.DefaultTLSOptions,\n\t\tTemplates: p.getTemplates(),\n\t}\n\n\t// Disable the database when WithNoDB() option is passed.\n\tif p.options.noDB {\n\t\tcfg.DB = nil\n\t}\n\n\t// Add linked as a deployment type to detect it on start and provide a\n\t// message if the token is not given.\n\tif p.options.deploymentType == LinkedDeployment {\n\t\tcfg.AuthorityConfig.DeploymentType = LinkedDeployment.String()\n\t}\n\n\t// Enable KMS if necessary\n\tif p.Kms != nil {\n\t\ttyp := strings.ToLower(p.Kms.Type.String())\n\t\tcfg.KMS = &kmsapi.Options{\n\t\t\tType: kmsapi.Type(typ),\n\t\t}\n\t}\n\n\t// On standalone deployments add the provisioners to either the ca.json or\n\t// the database.\n\tvar provisioners []provisioner.Interface\n\tif p.options.deploymentType == StandaloneDeployment {\n\t\tkey, err := p.ottPrivateKey.CompactSerialize()\n\t\tif err != nil {\n\t\t\treturn nil, errors.Wrap(err, \"error serializing private key\")\n\t\t}\n\n\t\tprov := &provisioner.JWK{\n\t\t\tName:         p.options.provisioner,\n\t\t\tType:         \"JWK\",\n\t\t\tKey:          p.ottPublicKey,\n\t\t\tEncryptedKey: key,\n\t\t}\n\t\tprovisioners = append(provisioners, prov)\n\n\t\t// Add default ACME provisioner if enabled\n\t\tif p.options.enableACME {\n\t\t\t// To prevent name clashes for the default ACME provisioner, we append \"-1\" to\n\t\t\t// the name if it already exists. See https://github.com/smallstep/cli/issues/1018\n\t\t\t// for the reason.\n\t\t\tacmeProvisionerName := \"acme\"\n\t\t\tif p.options.provisioner == acmeProvisionerName {\n\t\t\t\tacmeProvisionerName = fmt.Sprintf(\"%s-1\", acmeProvisionerName)\n\t\t\t}\n\t\t\tprovisioners = append(provisioners, &provisioner.ACME{\n\t\t\t\tType: \"ACME\",\n\t\t\t\tName: acmeProvisionerName,\n\t\t\t})\n\t\t}\n\n\t\tif p.options.enableSSH {\n\t\t\tenableSSHCA := true\n\t\t\tcfg.SSH = &authconfig.SSHConfig{\n\t\t\t\tHostKey: p.Ssh.HostKey,\n\t\t\t\tUserKey: p.Ssh.UserKey,\n\t\t\t}\n\t\t\t// Enable SSH authorization for default JWK provisioner\n\t\t\tprov.Claims = &provisioner.Claims{\n\t\t\t\tEnableSSHCA: &enableSSHCA,\n\t\t\t}\n\n\t\t\t// Add default SSHPOP provisioner. To prevent name clashes for the default\n\t\t\t// SSHPOP provisioner, we append \"-1\" to the name if it already exists.\n\t\t\t// See https://github.com/smallstep/cli/issues/1018 for the reason.\n\t\t\tsshProvisionerName := \"sshpop\"\n\t\t\tif p.options.provisioner == sshProvisionerName {\n\t\t\t\tsshProvisionerName = fmt.Sprintf(\"%s-1\", sshProvisionerName)\n\t\t\t}\n\t\t\tprovisioners = append(provisioners, &provisioner.SSHPOP{\n\t\t\t\tType: \"SSHPOP\",\n\t\t\t\tName: sshProvisionerName,\n\t\t\t\tClaims: &provisioner.Claims{\n\t\t\t\t\tEnableSSHCA: &enableSSHCA,\n\t\t\t\t},\n\t\t\t})\n\t\t}\n\t}\n\n\t// Apply configuration modifiers\n\tfor _, o := range opt {\n\t\tif err := o(cfg); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\t// Set authority.enableAdmin to true\n\tif p.options.enableAdmin {\n\t\tcfg.AuthorityConfig.EnableAdmin = true\n\t}\n\n\tif p.options.deploymentType == StandaloneDeployment {\n\t\tif !cfg.AuthorityConfig.EnableAdmin {\n\t\t\tcfg.AuthorityConfig.Provisioners = provisioners\n\t\t} else {\n\t\t\t// At this moment this code path is never used because `step ca\n\t\t\t// init` will always set enableAdmin to false for a standalone\n\t\t\t// deployment. Once we move `step beta` commands out of the beta we\n\t\t\t// should probably default to this route.\n\t\t\t//\n\t\t\t// Note that we might want to be able to define the database as a\n\t\t\t// flag in `step ca init` so we can write to the proper place.\n\t\t\t//\n\t\t\t// TODO(hs): the logic for creating the provisioners and the super admin\n\t\t\t// is similar to what's done when automatically migrating the provisioners.\n\t\t\t// This is related to the existing comment above. Refactor this to exist in\n\t\t\t// a single place and ensure it happens only once.\n\t\t\t_db, err := db.New(cfg.DB)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tdefer _db.Shutdown() // free DB resources; unlock BadgerDB file\n\n\t\t\tadminDB, err := admindb.New(_db.(nosql.DB), admin.DefaultAuthorityID)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\t// Add all the provisioners to the db.\n\t\t\tvar adminID string\n\t\t\tfor i, p := range provisioners {\n\t\t\t\tprov, err := authority.ProvisionerToLinkedca(p)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t\tif err := adminDB.CreateProvisioner(context.Background(), prov); err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t\tif i == 0 {\n\t\t\t\t\tadminID = prov.Id\n\t\t\t\t}\n\t\t\t}\n\t\t\t// Add the first provisioner as an admin.\n\t\t\tsuperAdminSubject := \"step\"\n\t\t\tif p.options.superAdminSubject != \"\" {\n\t\t\t\tsuperAdminSubject = p.options.superAdminSubject\n\t\t\t}\n\t\t\tif err := adminDB.CreateAdmin(context.Background(), &linkedca.Admin{\n\t\t\t\tAuthorityId:   admin.DefaultAuthorityID,\n\t\t\t\tSubject:       superAdminSubject,\n\t\t\t\tType:          linkedca.Admin_SUPER_ADMIN,\n\t\t\t\tProvisionerId: adminID,\n\t\t\t}); err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\t}\n\n\treturn cfg, nil\n}\n\n// Save stores the pki on a json file that will be used as the certificate\n// authority configuration.\nfunc (p *PKI) Save(opt ...ConfigOption) error {\n\t// Write generated files\n\tif err := p.WriteFiles(); err != nil {\n\t\treturn err\n\t}\n\n\t// Display the files written\n\tp.tellPKI()\n\n\t// Generate and write ca.json\n\tif !p.options.pkiOnly {\n\t\tcfg, err := p.GenerateConfig(opt...)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tb, err := json.MarshalIndent(cfg, \"\", \"\\t\") //nolint:gosec // config struct contains password field by design\n\t\tif err != nil {\n\t\t\treturn errors.Wrapf(err, \"error marshaling %s\", p.config)\n\t\t}\n\t\tif err = fileutil.WriteFile(p.config, b, 0644); err != nil {\n\t\t\treturn errs.FileError(err, p.config)\n\t\t}\n\n\t\t// Generate and write defaults.json\n\t\tdefaults := &caDefaults{\n\t\t\tRoot:        p.Defaults.Root,\n\t\t\tCAConfig:    p.Defaults.CaConfig,\n\t\t\tCAUrl:       p.Defaults.CaUrl,\n\t\t\tFingerprint: p.Defaults.Fingerprint,\n\t\t}\n\t\tb, err = json.MarshalIndent(defaults, \"\", \"\\t\")\n\t\tif err != nil {\n\t\t\treturn errors.Wrapf(err, \"error marshaling %s\", p.defaults)\n\t\t}\n\t\tif err = fileutil.WriteFile(p.defaults, b, 0644); err != nil {\n\t\t\treturn errs.FileError(err, p.defaults)\n\t\t}\n\t\t// If we're using contexts then write a blank object to the default profile\n\t\t// configuration location.\n\t\tif p.profileDefaults != \"\" {\n\t\t\tif _, err := os.Stat(p.profileDefaults); os.IsNotExist(err) {\n\t\t\t\t// Write with 0600 to be consistent with directories structure.\n\t\t\t\tif err = fileutil.WriteFile(p.profileDefaults, []byte(\"{}\"), 0600); err != nil {\n\t\t\t\t\treturn errs.FileError(err, p.profileDefaults)\n\t\t\t\t}\n\t\t\t} else if err != nil {\n\t\t\t\treturn errs.FileError(err, p.profileDefaults)\n\t\t\t}\n\t\t}\n\n\t\t// Generate and write templates\n\t\tif err := generateTemplates(cfg.Templates); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif cfg.DB != nil {\n\t\t\tos.MkdirAll(cfg.DB.DataSource, 0700)\n\t\t\tui.PrintSelected(\"Database folder\", cfg.DB.DataSource)\n\t\t}\n\t\tif cfg.Templates != nil {\n\t\t\tui.PrintSelected(\"Templates folder\", GetTemplatesPath())\n\t\t}\n\n\t\tui.PrintSelected(\"Default configuration\", p.defaults)\n\t\tif p.profileDefaults != \"\" {\n\t\t\tui.PrintSelected(\"Default profile configuration\", p.profileDefaults)\n\t\t}\n\t\tui.PrintSelected(\"Certificate Authority configuration\", p.config)\n\t\tif cfg.AuthorityConfig.EnableAdmin && p.options.deploymentType != LinkedDeployment {\n\t\t\t// TODO(hs): we may want to get this information from the DB, because that's\n\t\t\t// where the admin and provisioner are stored in this case. Requires some\n\t\t\t// refactoring.\n\t\t\tsuperAdminSubject := \"step\"\n\t\t\tif p.options.superAdminSubject != \"\" {\n\t\t\t\tsuperAdminSubject = p.options.superAdminSubject\n\t\t\t}\n\t\t\tui.PrintSelected(\"Admin provisioner\", fmt.Sprintf(\"%s (JWK)\", p.options.provisioner))\n\t\t\tui.PrintSelected(\"Super admin subject\", superAdminSubject)\n\t\t}\n\n\t\tif p.options.deploymentType != LinkedDeployment {\n\t\t\tui.Println()\n\t\t\tif p.casOptions.Is(apiv1.SoftCAS) {\n\t\t\t\tui.Println(\"Your PKI is ready to go. To generate certificates for individual services see 'step help ca'.\")\n\t\t\t} else {\n\t\t\t\tui.Println(\"Your registration authority is ready to go. To generate certificates for individual services see 'step help ca'.\")\n\t\t\t}\n\t\t}\n\t}\n\n\tp.askFeedback()\n\treturn nil\n}\n\nfunc encodeCertificate(c *x509.Certificate) []byte {\n\treturn pem.EncodeToMemory(&pem.Block{\n\t\tType:  \"CERTIFICATE\",\n\t\tBytes: c.Raw,\n\t})\n}\n\nfunc encodePrivateKey(key crypto.PrivateKey, pass []byte) ([]byte, error) {\n\tblock, err := pemutil.Serialize(key, pemutil.WithPassword(pass))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn pem.EncodeToMemory(block), nil\n}\n"
  },
  {
    "path": "pki/pki_test.go",
    "content": "package pki\n\nimport (\n\t\"context\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/smallstep/cli-utils/step\"\n\t\"github.com/smallstep/nosql\"\n\n\t\"github.com/smallstep/certificates/authority/admin\"\n\tadmindb \"github.com/smallstep/certificates/authority/admin/db/nosql\"\n\tauthconfig \"github.com/smallstep/certificates/authority/config\"\n\t\"github.com/smallstep/certificates/authority/provisioner\"\n\t\"github.com/smallstep/certificates/cas/apiv1\"\n\t\"github.com/smallstep/certificates/db\"\n)\n\nfunc withDBDataSource(t *testing.T, dataSource string) func(c *authconfig.Config) error {\n\treturn func(c *authconfig.Config) error {\n\t\tif c == nil || c.DB == nil {\n\t\t\trequire.Fail(t, \"withDBDataSource prerequisites not met\")\n\t\t}\n\t\tc.DB.DataSource = dataSource\n\t\treturn nil\n\t}\n}\n\nfunc TestPKI_GenerateConfig(t *testing.T) {\n\tvar preparePKI = func(t *testing.T, opts ...Option) *PKI {\n\t\to := apiv1.Options{\n\t\t\tType:      \"softcas\",\n\t\t\tIsCreator: true,\n\t\t}\n\n\t\t// TODO(hs): invoking `New` doesn't perform all operations that are executed\n\t\t// when `ca init` is executed. Ideally this logic should be handled in one\n\t\t// place and probably inside of the PKI initialization. For testing purposes\n\t\t// the missing operations are faked by `setKeyPair`.\n\t\tp, err := New(o, opts...)\n\t\trequire.NoError(t, err)\n\n\t\t// setKeyPair sets a predefined JWK and a default JWK provisioner. This is one\n\t\t// of the things performed in the `ca init` code that's not part of `New`, but\n\t\t// performed after that in p.GenerateKeyPairs`. We're currently using the same\n\t\t// JWK for every test to keep test variance small: we're not testing JWK generation\n\t\t// here after all. It's a bit dangerous to redefine the function here, but it's\n\t\t// the simplest way to make this fully testable without refactoring the init now.\n\t\t// The password for the predefined encrypted key is \\x01\\x03\\x03\\x07.\n\t\tsetKeyPair(t, p)\n\n\t\treturn p\n\t}\n\ttype args struct {\n\t\topt []ConfigOption\n\t}\n\ttype test struct {\n\t\tpki     *PKI\n\t\targs    args\n\t\twant    *authconfig.Config\n\t\twantErr bool\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"ok/simple\": func(t *testing.T) test {\n\t\t\tpki := preparePKI(t)\n\t\t\tpki.options.deploymentType = StandaloneDeployment\n\t\t\tpki.options.provisioner = \"default-prov\"\n\t\t\treturn test{\n\t\t\t\tpki: pki,\n\t\t\t\targs: args{\n\t\t\t\t\t[]ConfigOption{},\n\t\t\t\t},\n\t\t\t\twant: &authconfig.Config{\n\t\t\t\t\tAddress:         \"127.0.0.1:9000\",\n\t\t\t\t\tInsecureAddress: \"\",\n\t\t\t\t\tDNSNames:        []string{\"127.0.0.1\"},\n\t\t\t\t\tAuthorityConfig: &authconfig.AuthConfig{\n\t\t\t\t\t\tDeploymentType: \"\", // TODO(hs): (why is) this is not set to standalone?\n\t\t\t\t\t\tEnableAdmin:    false,\n\t\t\t\t\t\tProvisioners: provisioner.List{\n\t\t\t\t\t\t\t&provisioner.JWK{\n\t\t\t\t\t\t\t\tType: \"JWK\",\n\t\t\t\t\t\t\t\tName: \"default-prov\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tDB: &db.Config{\n\t\t\t\t\t\tType:       \"badgerv2\",\n\t\t\t\t\t\tDataSource: filepath.Join(step.Path(), \"db\"),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\twantErr: false,\n\t\t\t}\n\t\t},\n\t\t\"ok/with-acme\": func(t *testing.T) test {\n\t\t\tpki := preparePKI(t)\n\t\t\tpki.options.deploymentType = StandaloneDeployment\n\t\t\tpki.options.provisioner = \"default-prov\"\n\t\t\tpki.options.enableACME = true\n\t\t\treturn test{\n\t\t\t\tpki: pki,\n\t\t\t\targs: args{\n\t\t\t\t\t[]ConfigOption{},\n\t\t\t\t},\n\t\t\t\twant: &authconfig.Config{\n\t\t\t\t\tAddress:         \"127.0.0.1:9000\",\n\t\t\t\t\tInsecureAddress: \"\",\n\t\t\t\t\tDNSNames:        []string{\"127.0.0.1\"},\n\t\t\t\t\tAuthorityConfig: &authconfig.AuthConfig{\n\t\t\t\t\t\tDeploymentType: \"\", // TODO(hs): (why is) this is not set to standalone?\n\t\t\t\t\t\tEnableAdmin:    false,\n\t\t\t\t\t\tProvisioners: provisioner.List{\n\t\t\t\t\t\t\t&provisioner.JWK{\n\t\t\t\t\t\t\t\tType: \"JWK\",\n\t\t\t\t\t\t\t\tName: \"default-prov\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t&provisioner.ACME{\n\t\t\t\t\t\t\t\tType: \"ACME\",\n\t\t\t\t\t\t\t\tName: \"acme\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tDB: &db.Config{\n\t\t\t\t\t\tType:       \"badgerv2\",\n\t\t\t\t\t\tDataSource: filepath.Join(step.Path(), \"db\"),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\twantErr: false,\n\t\t\t}\n\t\t},\n\t\t\"ok/with-acme-and-double-provisioner-name\": func(t *testing.T) test {\n\t\t\tpki := preparePKI(t)\n\t\t\tpki.options.deploymentType = StandaloneDeployment\n\t\t\tpki.options.provisioner = \"acme\"\n\t\t\tpki.options.enableACME = true\n\t\t\treturn test{\n\t\t\t\tpki: pki,\n\t\t\t\targs: args{\n\t\t\t\t\t[]ConfigOption{},\n\t\t\t\t},\n\t\t\t\twant: &authconfig.Config{\n\t\t\t\t\tAddress:         \"127.0.0.1:9000\",\n\t\t\t\t\tInsecureAddress: \"\",\n\t\t\t\t\tDNSNames:        []string{\"127.0.0.1\"},\n\t\t\t\t\tAuthorityConfig: &authconfig.AuthConfig{\n\t\t\t\t\t\tDeploymentType: \"\", // TODO(hs): (why is) this is not set to standalone?\n\t\t\t\t\t\tEnableAdmin:    false,\n\t\t\t\t\t\tProvisioners: provisioner.List{\n\t\t\t\t\t\t\t&provisioner.JWK{\n\t\t\t\t\t\t\t\tType: \"JWK\",\n\t\t\t\t\t\t\t\tName: \"acme\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t&provisioner.ACME{\n\t\t\t\t\t\t\t\tType: \"ACME\",\n\t\t\t\t\t\t\t\tName: \"acme-1\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tDB: &db.Config{\n\t\t\t\t\t\tType:       \"badgerv2\",\n\t\t\t\t\t\tDataSource: filepath.Join(step.Path(), \"db\"),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\twantErr: false,\n\t\t\t}\n\t\t},\n\t\t\"ok/with-ssh\": func(t *testing.T) test {\n\t\t\tpki := preparePKI(t)\n\t\t\tpki.options.deploymentType = StandaloneDeployment\n\t\t\tpki.options.provisioner = \"default-prov\"\n\t\t\tpki.options.enableSSH = true\n\t\t\treturn test{\n\t\t\t\tpki: pki,\n\t\t\t\targs: args{\n\t\t\t\t\t[]ConfigOption{},\n\t\t\t\t},\n\t\t\t\twant: &authconfig.Config{\n\t\t\t\t\tAddress:         \"127.0.0.1:9000\",\n\t\t\t\t\tInsecureAddress: \"\",\n\t\t\t\t\tDNSNames:        []string{\"127.0.0.1\"},\n\t\t\t\t\tAuthorityConfig: &authconfig.AuthConfig{\n\t\t\t\t\t\tDeploymentType: \"\", // TODO(hs): (why is) this is not set to standalone?\n\t\t\t\t\t\tEnableAdmin:    false,\n\t\t\t\t\t\tProvisioners: provisioner.List{\n\t\t\t\t\t\t\t&provisioner.JWK{\n\t\t\t\t\t\t\t\tType: \"JWK\",\n\t\t\t\t\t\t\t\tName: \"default-prov\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t&provisioner.SSHPOP{\n\t\t\t\t\t\t\t\tType: \"SSHPOP\",\n\t\t\t\t\t\t\t\tName: \"sshpop\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tDB: &db.Config{\n\t\t\t\t\t\tType:       \"badgerv2\",\n\t\t\t\t\t\tDataSource: filepath.Join(step.Path(), \"db\"),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\twantErr: false,\n\t\t\t}\n\t\t},\n\t\t\"ok/with-ssh-and-double-provisioner-name\": func(t *testing.T) test {\n\t\t\tpki := preparePKI(t)\n\t\t\tpki.options.deploymentType = StandaloneDeployment\n\t\t\tpki.options.provisioner = \"sshpop\"\n\t\t\tpki.options.enableSSH = true\n\t\t\treturn test{\n\t\t\t\tpki: pki,\n\t\t\t\targs: args{\n\t\t\t\t\t[]ConfigOption{},\n\t\t\t\t},\n\t\t\t\twant: &authconfig.Config{\n\t\t\t\t\tAddress:         \"127.0.0.1:9000\",\n\t\t\t\t\tInsecureAddress: \"\",\n\t\t\t\t\tDNSNames:        []string{\"127.0.0.1\"},\n\t\t\t\t\tAuthorityConfig: &authconfig.AuthConfig{\n\t\t\t\t\t\tDeploymentType: \"\", // TODO(hs): (why is) this is not set to standalone?\n\t\t\t\t\t\tEnableAdmin:    false,\n\t\t\t\t\t\tProvisioners: provisioner.List{\n\t\t\t\t\t\t\t&provisioner.JWK{\n\t\t\t\t\t\t\t\tType: \"JWK\",\n\t\t\t\t\t\t\t\tName: \"sshpop\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t&provisioner.SSHPOP{\n\t\t\t\t\t\t\t\tType: \"SSHPOP\",\n\t\t\t\t\t\t\t\tName: \"sshpop-1\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tDB: &db.Config{\n\t\t\t\t\t\tType:       \"badgerv2\",\n\t\t\t\t\t\tDataSource: filepath.Join(step.Path(), \"db\"),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\twantErr: false,\n\t\t\t}\n\t\t},\n\t\t\"ok/with-admin\": func(t *testing.T) test {\n\t\t\tpki := preparePKI(t)\n\t\t\tpki.options.deploymentType = StandaloneDeployment\n\t\t\tpki.options.provisioner = \"default-prov\"\n\t\t\tpki.options.enableAdmin = true\n\t\t\ttempDir := t.TempDir()\n\t\t\treturn test{\n\t\t\t\tpki: pki,\n\t\t\t\targs: args{\n\t\t\t\t\t[]ConfigOption{withDBDataSource(t, filepath.Join(tempDir, \"db\"))},\n\t\t\t\t},\n\t\t\t\twant: &authconfig.Config{\n\t\t\t\t\tAddress:         \"127.0.0.1:9000\",\n\t\t\t\t\tInsecureAddress: \"\",\n\t\t\t\t\tDNSNames:        []string{\"127.0.0.1\"},\n\t\t\t\t\tAuthorityConfig: &authconfig.AuthConfig{\n\t\t\t\t\t\tDeploymentType: \"\", // TODO(hs): (why is) this is not set to standalone?\n\t\t\t\t\t\tEnableAdmin:    true,\n\t\t\t\t\t\tProvisioners:   provisioner.List{}, // when admin is enabled, provisioner list is expected to be empty\n\t\t\t\t\t},\n\t\t\t\t\tDB: &db.Config{\n\t\t\t\t\t\tType:       \"badgerv2\",\n\t\t\t\t\t\tDataSource: filepath.Join(tempDir, \"db\"),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\twantErr: false,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, run := range tests {\n\t\ttc := run(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tgot, err := tc.pki.GenerateConfig(tc.args.opt...)\n\t\t\tif tc.wantErr {\n\t\t\t\tassert.NotNil(t, err)\n\t\t\t\tassert.Nil(t, got)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.Nil(t, err)\n\t\t\tif assert.NotNil(t, got) {\n\t\t\t\tassert.Equal(t, tc.want.Address, got.Address)\n\t\t\t\tassert.Equal(t, tc.want.InsecureAddress, got.InsecureAddress)\n\t\t\t\tassert.Equal(t, tc.want.DNSNames, got.DNSNames)\n\t\t\t\tassert.Equal(t, tc.want.DB, got.DB)\n\t\t\t\tif assert.NotNil(t, tc.want.AuthorityConfig) {\n\t\t\t\t\tassert.Equal(t, tc.want.AuthorityConfig.DeploymentType, got.AuthorityConfig.DeploymentType)\n\t\t\t\t\tassert.Equal(t, tc.want.AuthorityConfig.EnableAdmin, got.AuthorityConfig.EnableAdmin)\n\t\t\t\t\tif numberOfProvisioners := len(tc.want.AuthorityConfig.Provisioners); numberOfProvisioners > 0 {\n\t\t\t\t\t\tif assert.Len(t, got.AuthorityConfig.Provisioners, numberOfProvisioners) {\n\t\t\t\t\t\t\tfor i, p := range tc.want.AuthorityConfig.Provisioners {\n\t\t\t\t\t\t\t\tassert.Equal(t, p.GetType(), got.AuthorityConfig.Provisioners[i].GetType())\n\t\t\t\t\t\t\t\tassert.Equal(t, p.GetName(), got.AuthorityConfig.Provisioners[i].GetName())\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif tc.want.AuthorityConfig.EnableAdmin {\n\t\t\t\t\t\t_db, err := db.New(tc.want.DB)\n\t\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t\t\tdefer _db.Shutdown()\n\n\t\t\t\t\t\tadminDB, err := admindb.New(_db.(nosql.DB), admin.DefaultAuthorityID)\n\t\t\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\t\t\tprovs, err := adminDB.GetProvisioners(context.Background())\n\t\t\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\t\t\tassert.NotEmpty(t, provs) // currently about the best we can do in terms of checks\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "pki/templates.go",
    "content": "package pki\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/pkg/errors\"\n\n\t\"github.com/smallstep/cli-utils/errs\"\n\t\"github.com/smallstep/cli-utils/fileutil\"\n\t\"github.com/smallstep/cli-utils/step\"\n\n\t\"github.com/smallstep/certificates/templates\"\n)\n\n// getTemplates returns all the templates enabled\nfunc (p *PKI) getTemplates() *templates.Templates {\n\tif !p.options.enableSSH {\n\t\treturn nil\n\t}\n\treturn &templates.Templates{\n\t\tSSH:  &templates.DefaultSSHTemplates,\n\t\tData: map[string]interface{}{},\n\t}\n}\n\n// generateTemplates generates given templates.\nfunc generateTemplates(t *templates.Templates) error {\n\tif t == nil {\n\t\treturn nil\n\t}\n\n\tbase := GetTemplatesPath()\n\t// Generate SSH templates\n\tif t.SSH != nil {\n\t\t// all ssh templates are under ssh:\n\t\tsshDir := filepath.Join(base, \"ssh\")\n\t\tif _, err := os.Stat(sshDir); os.IsNotExist(err) {\n\t\t\tif err = os.MkdirAll(sshDir, 0700); err != nil {\n\t\t\t\treturn errs.FileError(err, sshDir)\n\t\t\t}\n\t\t}\n\t\t// Create all templates\n\t\tfor _, t := range t.SSH.User {\n\t\t\tdata, ok := templates.DefaultSSHTemplateData[t.Name]\n\t\t\tif !ok {\n\t\t\t\treturn errors.Errorf(\"template %s does not exists\", t.Name)\n\t\t\t}\n\t\t\tif err := fileutil.WriteFile(step.Abs(t.TemplatePath), []byte(data), 0644); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\tfor _, t := range t.SSH.Host {\n\t\t\tdata, ok := templates.DefaultSSHTemplateData[t.Name]\n\t\t\tif !ok {\n\t\t\t\treturn errors.Errorf(\"template %s does not exists\", t.Name)\n\t\t\t}\n\t\t\tif err := fileutil.WriteFile(step.Abs(t.TemplatePath), []byte(data), 0644); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "pki/testdata/helm/simple.yml",
    "content": "# Helm template\ninject:\n  enabled: true\n  # Config contains the configuration files ca.json and defaults.json\n  config:\n    files:\n      ca.json:\n        root: /home/step/certs/root_ca.crt\n        federateRoots: []\n        crt: /home/step/certs/intermediate_ca.crt\n        key: /home/step/secrets/intermediate_ca_key\n        address: 127.0.0.1:9000\n        dnsNames:\n          - 127.0.0.1\n        logger:\n          format: json\n        db:\n          type: badgerv2\n          dataSource: /home/step/db\n        authority:\n          enableAdmin: false\n          provisioners:\n            - {\"type\":\"JWK\",\"name\":\"step-cli\",\"key\":{\"use\":\"sig\",\"kty\":\"EC\",\"kid\":\"zsUmysmDVoGJ71YoPHyZ-68tNihDaDaO5Mu7xX3M-_I\",\"crv\":\"P-256\",\"alg\":\"ES256\",\"x\":\"Pqnua4CzqKz6ua41J3yeWZ1sRkGt0UlCkbHv8H2DGuY\",\"y\":\"UhoZ_2ItDen9KQTcjay-ph-SBXH0mwqhHyvrrqIFDOI\"},\"encryptedKey\":\"eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJjdHkiOiJqd2sranNvbiIsImVuYyI6IkEyNTZHQ00iLCJwMmMiOjEwMDAwMCwicDJzIjoiZjVvdGVRS2hvOXl4MmQtSGlMZi05QSJ9.eYA6tt3fNuUpoxKWDT7P0Lbn2juxhEbTxEnwEMbjlYLLQ3sxL-dYTA.ven-FhmdjlC9itH0.a2jRTarN9vPd6F_mWnNBlOn6KbfMjCApmci2t65XbAsLzYFzhI_79Ykm5ueMYTupWLTjBJctl-g51ZHmsSB55pStbpoyyLNAsUX2E1fTmHe-Ni8bRrspwLv15FoN1Xo1g0mpR-ufWIFxOsW-QIfnMmMIIkygVuHFXmg2tFpzTNNG5aS29K3dN2nyk0WJrdIq79hZSTqVkkBU25Yu3A46sgjcM86XcIJJ2XUEih_KWEa6T1YrkixGu96pebjVqbO0R6dbDckfPF7FqNnwPHVtb1ACFpEYoOJVIbUCMaARBpWsxYhjJZlEM__XA46l8snFQDkNY3CdN0p1_gF3ckA.JLmq9nmu1h9oUi1S8ZxYjA\",\"options\":{\"x509\":{},\"ssh\":{}}}\n        tls:\n          cipherSuites:\n            - TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256\n            - TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256\n          minVersion: 1.2\n          maxVersion: 1.3\n          renegotiation: false\n\n      defaults.json:\n        ca-url: https://127.0.0.1\n        ca-config: /home/step/config/ca.json\n        fingerprint: e543cad8e9f6417076bb5aed3471c588152118aac1e0ca7984a43ee7f76da5e3\n        root: /home/step/certs/root_ca.crt\n\n  # Certificates contains the root and intermediate certificate and \n  # optionally the SSH host and user public keys\n  certificates:\n    # intermediate_ca contains the text of the intermediate CA Certificate\n    intermediate_ca: |\n      -----BEGIN CERTIFICATE-----\n      dGhlc2UgYXJlIGp1c3Qgc29tZSBmYWtlIGludGVybWVkaWF0ZSBDQSBjZXJ0IGJ5\n      dGVz\n      -----END CERTIFICATE-----\n      \n      \n    # root_ca contains the text of the root CA Certificate\n    root_ca: |\n      -----BEGIN CERTIFICATE-----\n      dGhlc2UgYXJlIGp1c3Qgc29tZSBmYWtlIHJvb3QgQ0EgY2VydCBieXRlcw==\n      -----END CERTIFICATE-----\n      \n\n  # Secrets contains the root and intermediate keys and optionally the SSH\n  # private keys\n  secrets:\n    # ca_password contains the password used to encrypt x509.intermediate_ca_key, ssh.host_ca_key and ssh.user_ca_key\n    # This value must be base64 encoded.\n    ca_password: \n    provisioner_password: \n\n    x509:\n      # intermediate_ca_key contains the contents of your encrypted intermediate CA key\n      intermediate_ca_key: |\n        -----BEGIN EC PRIVATE KEY-----\n        dGhlc2UgYXJlIGp1c3Qgc29tZSBmYWtlIGludGVybWVkaWF0ZSBDQSBrZXkgYnl0\n        ZXM=\n        -----END EC PRIVATE KEY-----\n        \n\n      # root_ca_key contains the contents of your encrypted root CA key\n      # Note that this value can be omitted without impacting the functionality of step-certificates\n      # If supplied, this should be encrypted using a unique password that is not used for encrypting\n      # the intermediate_ca_key, ssh.host_ca_key or ssh.user_ca_key.\n      root_ca_key: |\n        -----BEGIN EC PRIVATE KEY-----\n        dGhlc2UgYXJlIGp1c3Qgc29tZSBmYWtlIHJvb3QgQ0Ega2V5IGJ5dGVz\n        -----END EC PRIVATE KEY-----\n        \n"
  },
  {
    "path": "pki/testdata/helm/with-acme-and-duplicate-provisioner-name.yml",
    "content": "# Helm template\ninject:\n  enabled: true\n  # Config contains the configuration files ca.json and defaults.json\n  config:\n    files:\n      ca.json:\n        root: /home/step/certs/root_ca.crt\n        federateRoots: []\n        crt: /home/step/certs/intermediate_ca.crt\n        key: /home/step/secrets/intermediate_ca_key\n        address: 127.0.0.1:9000\n        dnsNames:\n          - 127.0.0.1\n        logger:\n          format: json\n        db:\n          type: badgerv2\n          dataSource: /home/step/db\n        authority:\n          enableAdmin: false\n          provisioners:\n            - {\"type\":\"JWK\",\"name\":\"acme\",\"key\":{\"use\":\"sig\",\"kty\":\"EC\",\"kid\":\"zsUmysmDVoGJ71YoPHyZ-68tNihDaDaO5Mu7xX3M-_I\",\"crv\":\"P-256\",\"alg\":\"ES256\",\"x\":\"Pqnua4CzqKz6ua41J3yeWZ1sRkGt0UlCkbHv8H2DGuY\",\"y\":\"UhoZ_2ItDen9KQTcjay-ph-SBXH0mwqhHyvrrqIFDOI\"},\"encryptedKey\":\"eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJjdHkiOiJqd2sranNvbiIsImVuYyI6IkEyNTZHQ00iLCJwMmMiOjEwMDAwMCwicDJzIjoiZjVvdGVRS2hvOXl4MmQtSGlMZi05QSJ9.eYA6tt3fNuUpoxKWDT7P0Lbn2juxhEbTxEnwEMbjlYLLQ3sxL-dYTA.ven-FhmdjlC9itH0.a2jRTarN9vPd6F_mWnNBlOn6KbfMjCApmci2t65XbAsLzYFzhI_79Ykm5ueMYTupWLTjBJctl-g51ZHmsSB55pStbpoyyLNAsUX2E1fTmHe-Ni8bRrspwLv15FoN1Xo1g0mpR-ufWIFxOsW-QIfnMmMIIkygVuHFXmg2tFpzTNNG5aS29K3dN2nyk0WJrdIq79hZSTqVkkBU25Yu3A46sgjcM86XcIJJ2XUEih_KWEa6T1YrkixGu96pebjVqbO0R6dbDckfPF7FqNnwPHVtb1ACFpEYoOJVIbUCMaARBpWsxYhjJZlEM__XA46l8snFQDkNY3CdN0p1_gF3ckA.JLmq9nmu1h9oUi1S8ZxYjA\",\"options\":{\"x509\":{},\"ssh\":{}}}\n            - {\"type\":\"ACME\",\"name\":\"acme-1\"}\n        tls:\n          cipherSuites:\n            - TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256\n            - TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256\n          minVersion: 1.2\n          maxVersion: 1.3\n          renegotiation: false\n\n      defaults.json:\n        ca-url: https://127.0.0.1\n        ca-config: /home/step/config/ca.json\n        fingerprint: e543cad8e9f6417076bb5aed3471c588152118aac1e0ca7984a43ee7f76da5e3\n        root: /home/step/certs/root_ca.crt\n\n  # Certificates contains the root and intermediate certificate and \n  # optionally the SSH host and user public keys\n  certificates:\n    # intermediate_ca contains the text of the intermediate CA Certificate\n    intermediate_ca: |\n      -----BEGIN CERTIFICATE-----\n      dGhlc2UgYXJlIGp1c3Qgc29tZSBmYWtlIGludGVybWVkaWF0ZSBDQSBjZXJ0IGJ5\n      dGVz\n      -----END CERTIFICATE-----\n      \n      \n    # root_ca contains the text of the root CA Certificate\n    root_ca: |\n      -----BEGIN CERTIFICATE-----\n      dGhlc2UgYXJlIGp1c3Qgc29tZSBmYWtlIHJvb3QgQ0EgY2VydCBieXRlcw==\n      -----END CERTIFICATE-----\n      \n\n  # Secrets contains the root and intermediate keys and optionally the SSH\n  # private keys\n  secrets:\n    # ca_password contains the password used to encrypt x509.intermediate_ca_key, ssh.host_ca_key and ssh.user_ca_key\n    # This value must be base64 encoded.\n    ca_password: \n    provisioner_password: \n\n    x509:\n      # intermediate_ca_key contains the contents of your encrypted intermediate CA key\n      intermediate_ca_key: |\n        -----BEGIN EC PRIVATE KEY-----\n        dGhlc2UgYXJlIGp1c3Qgc29tZSBmYWtlIGludGVybWVkaWF0ZSBDQSBrZXkgYnl0\n        ZXM=\n        -----END EC PRIVATE KEY-----\n        \n\n      # root_ca_key contains the contents of your encrypted root CA key\n      # Note that this value can be omitted without impacting the functionality of step-certificates\n      # If supplied, this should be encrypted using a unique password that is not used for encrypting\n      # the intermediate_ca_key, ssh.host_ca_key or ssh.user_ca_key.\n      root_ca_key: |\n        -----BEGIN EC PRIVATE KEY-----\n        dGhlc2UgYXJlIGp1c3Qgc29tZSBmYWtlIHJvb3QgQ0Ega2V5IGJ5dGVz\n        -----END EC PRIVATE KEY-----\n        \n"
  },
  {
    "path": "pki/testdata/helm/with-acme.yml",
    "content": "# Helm template\ninject:\n  enabled: true\n  # Config contains the configuration files ca.json and defaults.json\n  config:\n    files:\n      ca.json:\n        root: /home/step/certs/root_ca.crt\n        federateRoots: []\n        crt: /home/step/certs/intermediate_ca.crt\n        key: /home/step/secrets/intermediate_ca_key\n        address: 127.0.0.1:9000\n        dnsNames:\n          - 127.0.0.1\n        logger:\n          format: json\n        db:\n          type: badgerv2\n          dataSource: /home/step/db\n        authority:\n          enableAdmin: false\n          provisioners:\n            - {\"type\":\"JWK\",\"name\":\"step-cli\",\"key\":{\"use\":\"sig\",\"kty\":\"EC\",\"kid\":\"zsUmysmDVoGJ71YoPHyZ-68tNihDaDaO5Mu7xX3M-_I\",\"crv\":\"P-256\",\"alg\":\"ES256\",\"x\":\"Pqnua4CzqKz6ua41J3yeWZ1sRkGt0UlCkbHv8H2DGuY\",\"y\":\"UhoZ_2ItDen9KQTcjay-ph-SBXH0mwqhHyvrrqIFDOI\"},\"encryptedKey\":\"eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJjdHkiOiJqd2sranNvbiIsImVuYyI6IkEyNTZHQ00iLCJwMmMiOjEwMDAwMCwicDJzIjoiZjVvdGVRS2hvOXl4MmQtSGlMZi05QSJ9.eYA6tt3fNuUpoxKWDT7P0Lbn2juxhEbTxEnwEMbjlYLLQ3sxL-dYTA.ven-FhmdjlC9itH0.a2jRTarN9vPd6F_mWnNBlOn6KbfMjCApmci2t65XbAsLzYFzhI_79Ykm5ueMYTupWLTjBJctl-g51ZHmsSB55pStbpoyyLNAsUX2E1fTmHe-Ni8bRrspwLv15FoN1Xo1g0mpR-ufWIFxOsW-QIfnMmMIIkygVuHFXmg2tFpzTNNG5aS29K3dN2nyk0WJrdIq79hZSTqVkkBU25Yu3A46sgjcM86XcIJJ2XUEih_KWEa6T1YrkixGu96pebjVqbO0R6dbDckfPF7FqNnwPHVtb1ACFpEYoOJVIbUCMaARBpWsxYhjJZlEM__XA46l8snFQDkNY3CdN0p1_gF3ckA.JLmq9nmu1h9oUi1S8ZxYjA\",\"options\":{\"x509\":{},\"ssh\":{}}}\n            - {\"type\":\"ACME\",\"name\":\"acme\"}\n        tls:\n          cipherSuites:\n            - TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256\n            - TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256\n          minVersion: 1.2\n          maxVersion: 1.3\n          renegotiation: false\n\n      defaults.json:\n        ca-url: https://127.0.0.1\n        ca-config: /home/step/config/ca.json\n        fingerprint: e543cad8e9f6417076bb5aed3471c588152118aac1e0ca7984a43ee7f76da5e3\n        root: /home/step/certs/root_ca.crt\n\n  # Certificates contains the root and intermediate certificate and \n  # optionally the SSH host and user public keys\n  certificates:\n    # intermediate_ca contains the text of the intermediate CA Certificate\n    intermediate_ca: |\n      -----BEGIN CERTIFICATE-----\n      dGhlc2UgYXJlIGp1c3Qgc29tZSBmYWtlIGludGVybWVkaWF0ZSBDQSBjZXJ0IGJ5\n      dGVz\n      -----END CERTIFICATE-----\n      \n      \n    # root_ca contains the text of the root CA Certificate\n    root_ca: |\n      -----BEGIN CERTIFICATE-----\n      dGhlc2UgYXJlIGp1c3Qgc29tZSBmYWtlIHJvb3QgQ0EgY2VydCBieXRlcw==\n      -----END CERTIFICATE-----\n      \n\n  # Secrets contains the root and intermediate keys and optionally the SSH\n  # private keys\n  secrets:\n    # ca_password contains the password used to encrypt x509.intermediate_ca_key, ssh.host_ca_key and ssh.user_ca_key\n    # This value must be base64 encoded.\n    ca_password: \n    provisioner_password: \n\n    x509:\n      # intermediate_ca_key contains the contents of your encrypted intermediate CA key\n      intermediate_ca_key: |\n        -----BEGIN EC PRIVATE KEY-----\n        dGhlc2UgYXJlIGp1c3Qgc29tZSBmYWtlIGludGVybWVkaWF0ZSBDQSBrZXkgYnl0\n        ZXM=\n        -----END EC PRIVATE KEY-----\n        \n\n      # root_ca_key contains the contents of your encrypted root CA key\n      # Note that this value can be omitted without impacting the functionality of step-certificates\n      # If supplied, this should be encrypted using a unique password that is not used for encrypting\n      # the intermediate_ca_key, ssh.host_ca_key or ssh.user_ca_key.\n      root_ca_key: |\n        -----BEGIN EC PRIVATE KEY-----\n        dGhlc2UgYXJlIGp1c3Qgc29tZSBmYWtlIHJvb3QgQ0Ega2V5IGJ5dGVz\n        -----END EC PRIVATE KEY-----\n        \n"
  },
  {
    "path": "pki/testdata/helm/with-admin.yml",
    "content": "# Helm template\ninject:\n  enabled: true\n  # Config contains the configuration files ca.json and defaults.json\n  config:\n    files:\n      ca.json:\n        root: /home/step/certs/root_ca.crt\n        federateRoots: []\n        crt: /home/step/certs/intermediate_ca.crt\n        key: /home/step/secrets/intermediate_ca_key\n        address: 127.0.0.1:9000\n        dnsNames:\n          - 127.0.0.1\n        logger:\n          format: json\n        db:\n          type: badgerv2\n          dataSource: /home/step/db\n        authority:\n          enableAdmin: true\n          provisioners:\n            - {\"type\":\"JWK\",\"name\":\"step-cli\",\"key\":{\"use\":\"sig\",\"kty\":\"EC\",\"kid\":\"zsUmysmDVoGJ71YoPHyZ-68tNihDaDaO5Mu7xX3M-_I\",\"crv\":\"P-256\",\"alg\":\"ES256\",\"x\":\"Pqnua4CzqKz6ua41J3yeWZ1sRkGt0UlCkbHv8H2DGuY\",\"y\":\"UhoZ_2ItDen9KQTcjay-ph-SBXH0mwqhHyvrrqIFDOI\"},\"encryptedKey\":\"eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJjdHkiOiJqd2sranNvbiIsImVuYyI6IkEyNTZHQ00iLCJwMmMiOjEwMDAwMCwicDJzIjoiZjVvdGVRS2hvOXl4MmQtSGlMZi05QSJ9.eYA6tt3fNuUpoxKWDT7P0Lbn2juxhEbTxEnwEMbjlYLLQ3sxL-dYTA.ven-FhmdjlC9itH0.a2jRTarN9vPd6F_mWnNBlOn6KbfMjCApmci2t65XbAsLzYFzhI_79Ykm5ueMYTupWLTjBJctl-g51ZHmsSB55pStbpoyyLNAsUX2E1fTmHe-Ni8bRrspwLv15FoN1Xo1g0mpR-ufWIFxOsW-QIfnMmMIIkygVuHFXmg2tFpzTNNG5aS29K3dN2nyk0WJrdIq79hZSTqVkkBU25Yu3A46sgjcM86XcIJJ2XUEih_KWEa6T1YrkixGu96pebjVqbO0R6dbDckfPF7FqNnwPHVtb1ACFpEYoOJVIbUCMaARBpWsxYhjJZlEM__XA46l8snFQDkNY3CdN0p1_gF3ckA.JLmq9nmu1h9oUi1S8ZxYjA\",\"options\":{\"x509\":{},\"ssh\":{}}}\n        tls:\n          cipherSuites:\n            - TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256\n            - TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256\n          minVersion: 1.2\n          maxVersion: 1.3\n          renegotiation: false\n\n      defaults.json:\n        ca-url: https://127.0.0.1\n        ca-config: /home/step/config/ca.json\n        fingerprint: e543cad8e9f6417076bb5aed3471c588152118aac1e0ca7984a43ee7f76da5e3\n        root: /home/step/certs/root_ca.crt\n\n  # Certificates contains the root and intermediate certificate and \n  # optionally the SSH host and user public keys\n  certificates:\n    # intermediate_ca contains the text of the intermediate CA Certificate\n    intermediate_ca: |\n      -----BEGIN CERTIFICATE-----\n      dGhlc2UgYXJlIGp1c3Qgc29tZSBmYWtlIGludGVybWVkaWF0ZSBDQSBjZXJ0IGJ5\n      dGVz\n      -----END CERTIFICATE-----\n      \n      \n    # root_ca contains the text of the root CA Certificate\n    root_ca: |\n      -----BEGIN CERTIFICATE-----\n      dGhlc2UgYXJlIGp1c3Qgc29tZSBmYWtlIHJvb3QgQ0EgY2VydCBieXRlcw==\n      -----END CERTIFICATE-----\n      \n\n  # Secrets contains the root and intermediate keys and optionally the SSH\n  # private keys\n  secrets:\n    # ca_password contains the password used to encrypt x509.intermediate_ca_key, ssh.host_ca_key and ssh.user_ca_key\n    # This value must be base64 encoded.\n    ca_password: \n    provisioner_password: \n\n    x509:\n      # intermediate_ca_key contains the contents of your encrypted intermediate CA key\n      intermediate_ca_key: |\n        -----BEGIN EC PRIVATE KEY-----\n        dGhlc2UgYXJlIGp1c3Qgc29tZSBmYWtlIGludGVybWVkaWF0ZSBDQSBrZXkgYnl0\n        ZXM=\n        -----END EC PRIVATE KEY-----\n        \n\n      # root_ca_key contains the contents of your encrypted root CA key\n      # Note that this value can be omitted without impacting the functionality of step-certificates\n      # If supplied, this should be encrypted using a unique password that is not used for encrypting\n      # the intermediate_ca_key, ssh.host_ca_key or ssh.user_ca_key.\n      root_ca_key: |\n        -----BEGIN EC PRIVATE KEY-----\n        dGhlc2UgYXJlIGp1c3Qgc29tZSBmYWtlIHJvb3QgQ0Ega2V5IGJ5dGVz\n        -----END EC PRIVATE KEY-----\n        \n"
  },
  {
    "path": "pki/testdata/helm/with-provisioner.yml",
    "content": "# Helm template\ninject:\n  enabled: true\n  # Config contains the configuration files ca.json and defaults.json\n  config:\n    files:\n      ca.json:\n        root: /home/step/certs/root_ca.crt\n        federateRoots: []\n        crt: /home/step/certs/intermediate_ca.crt\n        key: /home/step/secrets/intermediate_ca_key\n        address: 127.0.0.1:9000\n        dnsNames:\n          - 127.0.0.1\n        logger:\n          format: json\n        db:\n          type: badgerv2\n          dataSource: /home/step/db\n        authority:\n          enableAdmin: false\n          provisioners:\n            - {\"type\":\"JWK\",\"name\":\"a-provisioner\",\"key\":{\"use\":\"sig\",\"kty\":\"EC\",\"kid\":\"zsUmysmDVoGJ71YoPHyZ-68tNihDaDaO5Mu7xX3M-_I\",\"crv\":\"P-256\",\"alg\":\"ES256\",\"x\":\"Pqnua4CzqKz6ua41J3yeWZ1sRkGt0UlCkbHv8H2DGuY\",\"y\":\"UhoZ_2ItDen9KQTcjay-ph-SBXH0mwqhHyvrrqIFDOI\"},\"encryptedKey\":\"eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJjdHkiOiJqd2sranNvbiIsImVuYyI6IkEyNTZHQ00iLCJwMmMiOjEwMDAwMCwicDJzIjoiZjVvdGVRS2hvOXl4MmQtSGlMZi05QSJ9.eYA6tt3fNuUpoxKWDT7P0Lbn2juxhEbTxEnwEMbjlYLLQ3sxL-dYTA.ven-FhmdjlC9itH0.a2jRTarN9vPd6F_mWnNBlOn6KbfMjCApmci2t65XbAsLzYFzhI_79Ykm5ueMYTupWLTjBJctl-g51ZHmsSB55pStbpoyyLNAsUX2E1fTmHe-Ni8bRrspwLv15FoN1Xo1g0mpR-ufWIFxOsW-QIfnMmMIIkygVuHFXmg2tFpzTNNG5aS29K3dN2nyk0WJrdIq79hZSTqVkkBU25Yu3A46sgjcM86XcIJJ2XUEih_KWEa6T1YrkixGu96pebjVqbO0R6dbDckfPF7FqNnwPHVtb1ACFpEYoOJVIbUCMaARBpWsxYhjJZlEM__XA46l8snFQDkNY3CdN0p1_gF3ckA.JLmq9nmu1h9oUi1S8ZxYjA\",\"options\":{\"x509\":{},\"ssh\":{}}}\n        tls:\n          cipherSuites:\n            - TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256\n            - TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256\n          minVersion: 1.2\n          maxVersion: 1.3\n          renegotiation: false\n\n      defaults.json:\n        ca-url: https://127.0.0.1\n        ca-config: /home/step/config/ca.json\n        fingerprint: e543cad8e9f6417076bb5aed3471c588152118aac1e0ca7984a43ee7f76da5e3\n        root: /home/step/certs/root_ca.crt\n\n  # Certificates contains the root and intermediate certificate and \n  # optionally the SSH host and user public keys\n  certificates:\n    # intermediate_ca contains the text of the intermediate CA Certificate\n    intermediate_ca: |\n      -----BEGIN CERTIFICATE-----\n      dGhlc2UgYXJlIGp1c3Qgc29tZSBmYWtlIGludGVybWVkaWF0ZSBDQSBjZXJ0IGJ5\n      dGVz\n      -----END CERTIFICATE-----\n      \n      \n    # root_ca contains the text of the root CA Certificate\n    root_ca: |\n      -----BEGIN CERTIFICATE-----\n      dGhlc2UgYXJlIGp1c3Qgc29tZSBmYWtlIHJvb3QgQ0EgY2VydCBieXRlcw==\n      -----END CERTIFICATE-----\n      \n\n  # Secrets contains the root and intermediate keys and optionally the SSH\n  # private keys\n  secrets:\n    # ca_password contains the password used to encrypt x509.intermediate_ca_key, ssh.host_ca_key and ssh.user_ca_key\n    # This value must be base64 encoded.\n    ca_password: \n    provisioner_password: \n\n    x509:\n      # intermediate_ca_key contains the contents of your encrypted intermediate CA key\n      intermediate_ca_key: |\n        -----BEGIN EC PRIVATE KEY-----\n        dGhlc2UgYXJlIGp1c3Qgc29tZSBmYWtlIGludGVybWVkaWF0ZSBDQSBrZXkgYnl0\n        ZXM=\n        -----END EC PRIVATE KEY-----\n        \n\n      # root_ca_key contains the contents of your encrypted root CA key\n      # Note that this value can be omitted without impacting the functionality of step-certificates\n      # If supplied, this should be encrypted using a unique password that is not used for encrypting\n      # the intermediate_ca_key, ssh.host_ca_key or ssh.user_ca_key.\n      root_ca_key: |\n        -----BEGIN EC PRIVATE KEY-----\n        dGhlc2UgYXJlIGp1c3Qgc29tZSBmYWtlIHJvb3QgQ0Ega2V5IGJ5dGVz\n        -----END EC PRIVATE KEY-----\n        \n"
  },
  {
    "path": "pki/testdata/helm/with-ssh-and-acme.yml",
    "content": "# Helm template\ninject:\n  enabled: true\n  # Config contains the configuration files ca.json and defaults.json\n  config:\n    files:\n      ca.json:\n        root: /home/step/certs/root_ca.crt\n        federateRoots: []\n        crt: /home/step/certs/intermediate_ca.crt\n        key: /home/step/secrets/intermediate_ca_key\n        ssh:\n          hostKey: /home/step/secrets/ssh_host_ca_key\n          userKey: /home/step/secrets/ssh_user_ca_key\n        address: 127.0.0.1:9000\n        dnsNames:\n          - 127.0.0.1\n        logger:\n          format: json\n        db:\n          type: badgerv2\n          dataSource: /home/step/db\n        authority:\n          enableAdmin: false\n          provisioners:\n            - {\"type\":\"JWK\",\"name\":\"step-cli\",\"key\":{\"use\":\"sig\",\"kty\":\"EC\",\"kid\":\"zsUmysmDVoGJ71YoPHyZ-68tNihDaDaO5Mu7xX3M-_I\",\"crv\":\"P-256\",\"alg\":\"ES256\",\"x\":\"Pqnua4CzqKz6ua41J3yeWZ1sRkGt0UlCkbHv8H2DGuY\",\"y\":\"UhoZ_2ItDen9KQTcjay-ph-SBXH0mwqhHyvrrqIFDOI\"},\"encryptedKey\":\"eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJjdHkiOiJqd2sranNvbiIsImVuYyI6IkEyNTZHQ00iLCJwMmMiOjEwMDAwMCwicDJzIjoiZjVvdGVRS2hvOXl4MmQtSGlMZi05QSJ9.eYA6tt3fNuUpoxKWDT7P0Lbn2juxhEbTxEnwEMbjlYLLQ3sxL-dYTA.ven-FhmdjlC9itH0.a2jRTarN9vPd6F_mWnNBlOn6KbfMjCApmci2t65XbAsLzYFzhI_79Ykm5ueMYTupWLTjBJctl-g51ZHmsSB55pStbpoyyLNAsUX2E1fTmHe-Ni8bRrspwLv15FoN1Xo1g0mpR-ufWIFxOsW-QIfnMmMIIkygVuHFXmg2tFpzTNNG5aS29K3dN2nyk0WJrdIq79hZSTqVkkBU25Yu3A46sgjcM86XcIJJ2XUEih_KWEa6T1YrkixGu96pebjVqbO0R6dbDckfPF7FqNnwPHVtb1ACFpEYoOJVIbUCMaARBpWsxYhjJZlEM__XA46l8snFQDkNY3CdN0p1_gF3ckA.JLmq9nmu1h9oUi1S8ZxYjA\",\"claims\":{\"enableSSHCA\":true,\"disableRenewal\":false,\"allowRenewalAfterExpiry\":false,\"disableSmallstepExtensions\":false},\"options\":{\"x509\":{},\"ssh\":{}}}\n            - {\"type\":\"ACME\",\"name\":\"acme\"}\n            - {\"type\":\"SSHPOP\",\"name\":\"sshpop\",\"claims\":{\"enableSSHCA\":true}}\n        tls:\n          cipherSuites:\n            - TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256\n            - TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256\n          minVersion: 1.2\n          maxVersion: 1.3\n          renegotiation: false\n\n      defaults.json:\n        ca-url: https://127.0.0.1\n        ca-config: /home/step/config/ca.json\n        fingerprint: e543cad8e9f6417076bb5aed3471c588152118aac1e0ca7984a43ee7f76da5e3\n        root: /home/step/certs/root_ca.crt\n\n  # Certificates contains the root and intermediate certificate and \n  # optionally the SSH host and user public keys\n  certificates:\n    # intermediate_ca contains the text of the intermediate CA Certificate\n    intermediate_ca: |\n      -----BEGIN CERTIFICATE-----\n      dGhlc2UgYXJlIGp1c3Qgc29tZSBmYWtlIGludGVybWVkaWF0ZSBDQSBjZXJ0IGJ5\n      dGVz\n      -----END CERTIFICATE-----\n      \n      \n    # root_ca contains the text of the root CA Certificate\n    root_ca: |\n      -----BEGIN CERTIFICATE-----\n      dGhlc2UgYXJlIGp1c3Qgc29tZSBmYWtlIHJvb3QgQ0EgY2VydCBieXRlcw==\n      -----END CERTIFICATE-----\n      \n    # ssh_host_ca contains the text of the public ssh key for the SSH root CA\n    ssh_host_ca: ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBJ0IdS5sZm6KITBMZLEJD6b5ROVraYHcAOr3feFel8r1Wp4DRPR1oU0W00J/zjNBRBbANlJoYN4x/8WNNVZ49Ms=\n\n    # ssh_user_ca contains the text of the public ssh key for the SSH root CA\n    ssh_user_ca: ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEWA1qUxaGwVNErsvEOGe2d6TvLMF+aiVpuOiIEvpMJ3JeJmecLQctjWqeIbpSvy6/gRa7c82Ge5rLlapYmOChs=\n\n  # Secrets contains the root and intermediate keys and optionally the SSH\n  # private keys\n  secrets:\n    # ca_password contains the password used to encrypt x509.intermediate_ca_key, ssh.host_ca_key and ssh.user_ca_key\n    # This value must be base64 encoded.\n    ca_password: \n    provisioner_password: \n\n    x509:\n      # intermediate_ca_key contains the contents of your encrypted intermediate CA key\n      intermediate_ca_key: |\n        -----BEGIN EC PRIVATE KEY-----\n        dGhlc2UgYXJlIGp1c3Qgc29tZSBmYWtlIGludGVybWVkaWF0ZSBDQSBrZXkgYnl0\n        ZXM=\n        -----END EC PRIVATE KEY-----\n        \n\n      # root_ca_key contains the contents of your encrypted root CA key\n      # Note that this value can be omitted without impacting the functionality of step-certificates\n      # If supplied, this should be encrypted using a unique password that is not used for encrypting\n      # the intermediate_ca_key, ssh.host_ca_key or ssh.user_ca_key.\n      root_ca_key: |\n        -----BEGIN EC PRIVATE KEY-----\n        dGhlc2UgYXJlIGp1c3Qgc29tZSBmYWtlIHJvb3QgQ0Ega2V5IGJ5dGVz\n        -----END EC PRIVATE KEY-----\n        \n    ssh:\n      # ssh_host_ca_key contains the contents of your encrypted SSH Host CA key\n      host_ca_key: |\n        -----BEGIN EC PRIVATE KEY-----\n        ZmFrZSBzc2ggaG9zdCBrZXkgYnl0ZXM=\n        -----END EC PRIVATE KEY-----\n        \n\n      # ssh_user_ca_key contains the contents of your encrypted SSH User CA key\n      user_ca_key: |\n        -----BEGIN EC PRIVATE KEY-----\n        ZmFrZSBzc2ggdXNlciBrZXkgYnl0ZXM=\n        -----END EC PRIVATE KEY-----\n        \n"
  },
  {
    "path": "pki/testdata/helm/with-ssh-and-duplicate-provisioner-name.yml",
    "content": "# Helm template\ninject:\n  enabled: true\n  # Config contains the configuration files ca.json and defaults.json\n  config:\n    files:\n      ca.json:\n        root: /home/step/certs/root_ca.crt\n        federateRoots: []\n        crt: /home/step/certs/intermediate_ca.crt\n        key: /home/step/secrets/intermediate_ca_key\n        ssh:\n          hostKey: /home/step/secrets/ssh_host_ca_key\n          userKey: /home/step/secrets/ssh_user_ca_key\n        address: 127.0.0.1:9000\n        dnsNames:\n          - 127.0.0.1\n        logger:\n          format: json\n        db:\n          type: badgerv2\n          dataSource: /home/step/db\n        authority:\n          enableAdmin: false\n          provisioners:\n            - {\"type\":\"JWK\",\"name\":\"sshpop\",\"key\":{\"use\":\"sig\",\"kty\":\"EC\",\"kid\":\"zsUmysmDVoGJ71YoPHyZ-68tNihDaDaO5Mu7xX3M-_I\",\"crv\":\"P-256\",\"alg\":\"ES256\",\"x\":\"Pqnua4CzqKz6ua41J3yeWZ1sRkGt0UlCkbHv8H2DGuY\",\"y\":\"UhoZ_2ItDen9KQTcjay-ph-SBXH0mwqhHyvrrqIFDOI\"},\"encryptedKey\":\"eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJjdHkiOiJqd2sranNvbiIsImVuYyI6IkEyNTZHQ00iLCJwMmMiOjEwMDAwMCwicDJzIjoiZjVvdGVRS2hvOXl4MmQtSGlMZi05QSJ9.eYA6tt3fNuUpoxKWDT7P0Lbn2juxhEbTxEnwEMbjlYLLQ3sxL-dYTA.ven-FhmdjlC9itH0.a2jRTarN9vPd6F_mWnNBlOn6KbfMjCApmci2t65XbAsLzYFzhI_79Ykm5ueMYTupWLTjBJctl-g51ZHmsSB55pStbpoyyLNAsUX2E1fTmHe-Ni8bRrspwLv15FoN1Xo1g0mpR-ufWIFxOsW-QIfnMmMIIkygVuHFXmg2tFpzTNNG5aS29K3dN2nyk0WJrdIq79hZSTqVkkBU25Yu3A46sgjcM86XcIJJ2XUEih_KWEa6T1YrkixGu96pebjVqbO0R6dbDckfPF7FqNnwPHVtb1ACFpEYoOJVIbUCMaARBpWsxYhjJZlEM__XA46l8snFQDkNY3CdN0p1_gF3ckA.JLmq9nmu1h9oUi1S8ZxYjA\",\"claims\":{\"enableSSHCA\":true,\"disableRenewal\":false,\"allowRenewalAfterExpiry\":false,\"disableSmallstepExtensions\":false},\"options\":{\"x509\":{},\"ssh\":{}}}\n            - {\"type\":\"SSHPOP\",\"name\":\"sshpop-1\",\"claims\":{\"enableSSHCA\":true}}\n        tls:\n          cipherSuites:\n            - TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256\n            - TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256\n          minVersion: 1.2\n          maxVersion: 1.3\n          renegotiation: false\n\n      defaults.json:\n        ca-url: https://127.0.0.1\n        ca-config: /home/step/config/ca.json\n        fingerprint: e543cad8e9f6417076bb5aed3471c588152118aac1e0ca7984a43ee7f76da5e3\n        root: /home/step/certs/root_ca.crt\n\n  # Certificates contains the root and intermediate certificate and \n  # optionally the SSH host and user public keys\n  certificates:\n    # intermediate_ca contains the text of the intermediate CA Certificate\n    intermediate_ca: |\n      -----BEGIN CERTIFICATE-----\n      dGhlc2UgYXJlIGp1c3Qgc29tZSBmYWtlIGludGVybWVkaWF0ZSBDQSBjZXJ0IGJ5\n      dGVz\n      -----END CERTIFICATE-----\n      \n      \n    # root_ca contains the text of the root CA Certificate\n    root_ca: |\n      -----BEGIN CERTIFICATE-----\n      dGhlc2UgYXJlIGp1c3Qgc29tZSBmYWtlIHJvb3QgQ0EgY2VydCBieXRlcw==\n      -----END CERTIFICATE-----\n      \n    # ssh_host_ca contains the text of the public ssh key for the SSH root CA\n    ssh_host_ca: ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBJ0IdS5sZm6KITBMZLEJD6b5ROVraYHcAOr3feFel8r1Wp4DRPR1oU0W00J/zjNBRBbANlJoYN4x/8WNNVZ49Ms=\n\n    # ssh_user_ca contains the text of the public ssh key for the SSH root CA\n    ssh_user_ca: ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEWA1qUxaGwVNErsvEOGe2d6TvLMF+aiVpuOiIEvpMJ3JeJmecLQctjWqeIbpSvy6/gRa7c82Ge5rLlapYmOChs=\n\n  # Secrets contains the root and intermediate keys and optionally the SSH\n  # private keys\n  secrets:\n    # ca_password contains the password used to encrypt x509.intermediate_ca_key, ssh.host_ca_key and ssh.user_ca_key\n    # This value must be base64 encoded.\n    ca_password: \n    provisioner_password: \n\n    x509:\n      # intermediate_ca_key contains the contents of your encrypted intermediate CA key\n      intermediate_ca_key: |\n        -----BEGIN EC PRIVATE KEY-----\n        dGhlc2UgYXJlIGp1c3Qgc29tZSBmYWtlIGludGVybWVkaWF0ZSBDQSBrZXkgYnl0\n        ZXM=\n        -----END EC PRIVATE KEY-----\n        \n\n      # root_ca_key contains the contents of your encrypted root CA key\n      # Note that this value can be omitted without impacting the functionality of step-certificates\n      # If supplied, this should be encrypted using a unique password that is not used for encrypting\n      # the intermediate_ca_key, ssh.host_ca_key or ssh.user_ca_key.\n      root_ca_key: |\n        -----BEGIN EC PRIVATE KEY-----\n        dGhlc2UgYXJlIGp1c3Qgc29tZSBmYWtlIHJvb3QgQ0Ega2V5IGJ5dGVz\n        -----END EC PRIVATE KEY-----\n        \n    ssh:\n      # ssh_host_ca_key contains the contents of your encrypted SSH Host CA key\n      host_ca_key: |\n        -----BEGIN EC PRIVATE KEY-----\n        ZmFrZSBzc2ggaG9zdCBrZXkgYnl0ZXM=\n        -----END EC PRIVATE KEY-----\n        \n\n      # ssh_user_ca_key contains the contents of your encrypted SSH User CA key\n      user_ca_key: |\n        -----BEGIN EC PRIVATE KEY-----\n        ZmFrZSBzc2ggdXNlciBrZXkgYnl0ZXM=\n        -----END EC PRIVATE KEY-----\n        \n"
  },
  {
    "path": "pki/testdata/helm/with-ssh.yml",
    "content": "# Helm template\ninject:\n  enabled: true\n  # Config contains the configuration files ca.json and defaults.json\n  config:\n    files:\n      ca.json:\n        root: /home/step/certs/root_ca.crt\n        federateRoots: []\n        crt: /home/step/certs/intermediate_ca.crt\n        key: /home/step/secrets/intermediate_ca_key\n        ssh:\n          hostKey: /home/step/secrets/ssh_host_ca_key\n          userKey: /home/step/secrets/ssh_user_ca_key\n        address: 127.0.0.1:9000\n        dnsNames:\n          - 127.0.0.1\n        logger:\n          format: json\n        db:\n          type: badgerv2\n          dataSource: /home/step/db\n        authority:\n          enableAdmin: false\n          provisioners:\n            - {\"type\":\"JWK\",\"name\":\"step-cli\",\"key\":{\"use\":\"sig\",\"kty\":\"EC\",\"kid\":\"zsUmysmDVoGJ71YoPHyZ-68tNihDaDaO5Mu7xX3M-_I\",\"crv\":\"P-256\",\"alg\":\"ES256\",\"x\":\"Pqnua4CzqKz6ua41J3yeWZ1sRkGt0UlCkbHv8H2DGuY\",\"y\":\"UhoZ_2ItDen9KQTcjay-ph-SBXH0mwqhHyvrrqIFDOI\"},\"encryptedKey\":\"eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJjdHkiOiJqd2sranNvbiIsImVuYyI6IkEyNTZHQ00iLCJwMmMiOjEwMDAwMCwicDJzIjoiZjVvdGVRS2hvOXl4MmQtSGlMZi05QSJ9.eYA6tt3fNuUpoxKWDT7P0Lbn2juxhEbTxEnwEMbjlYLLQ3sxL-dYTA.ven-FhmdjlC9itH0.a2jRTarN9vPd6F_mWnNBlOn6KbfMjCApmci2t65XbAsLzYFzhI_79Ykm5ueMYTupWLTjBJctl-g51ZHmsSB55pStbpoyyLNAsUX2E1fTmHe-Ni8bRrspwLv15FoN1Xo1g0mpR-ufWIFxOsW-QIfnMmMIIkygVuHFXmg2tFpzTNNG5aS29K3dN2nyk0WJrdIq79hZSTqVkkBU25Yu3A46sgjcM86XcIJJ2XUEih_KWEa6T1YrkixGu96pebjVqbO0R6dbDckfPF7FqNnwPHVtb1ACFpEYoOJVIbUCMaARBpWsxYhjJZlEM__XA46l8snFQDkNY3CdN0p1_gF3ckA.JLmq9nmu1h9oUi1S8ZxYjA\",\"claims\":{\"enableSSHCA\":true,\"disableRenewal\":false,\"allowRenewalAfterExpiry\":false,\"disableSmallstepExtensions\":false},\"options\":{\"x509\":{},\"ssh\":{}}}\n            - {\"type\":\"SSHPOP\",\"name\":\"sshpop\",\"claims\":{\"enableSSHCA\":true}}\n        tls:\n          cipherSuites:\n            - TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256\n            - TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256\n          minVersion: 1.2\n          maxVersion: 1.3\n          renegotiation: false\n\n      defaults.json:\n        ca-url: https://127.0.0.1\n        ca-config: /home/step/config/ca.json\n        fingerprint: e543cad8e9f6417076bb5aed3471c588152118aac1e0ca7984a43ee7f76da5e3\n        root: /home/step/certs/root_ca.crt\n\n  # Certificates contains the root and intermediate certificate and \n  # optionally the SSH host and user public keys\n  certificates:\n    # intermediate_ca contains the text of the intermediate CA Certificate\n    intermediate_ca: |\n      -----BEGIN CERTIFICATE-----\n      dGhlc2UgYXJlIGp1c3Qgc29tZSBmYWtlIGludGVybWVkaWF0ZSBDQSBjZXJ0IGJ5\n      dGVz\n      -----END CERTIFICATE-----\n      \n      \n    # root_ca contains the text of the root CA Certificate\n    root_ca: |\n      -----BEGIN CERTIFICATE-----\n      dGhlc2UgYXJlIGp1c3Qgc29tZSBmYWtlIHJvb3QgQ0EgY2VydCBieXRlcw==\n      -----END CERTIFICATE-----\n      \n    # ssh_host_ca contains the text of the public ssh key for the SSH root CA\n    ssh_host_ca: ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBJ0IdS5sZm6KITBMZLEJD6b5ROVraYHcAOr3feFel8r1Wp4DRPR1oU0W00J/zjNBRBbANlJoYN4x/8WNNVZ49Ms=\n\n    # ssh_user_ca contains the text of the public ssh key for the SSH root CA\n    ssh_user_ca: ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEWA1qUxaGwVNErsvEOGe2d6TvLMF+aiVpuOiIEvpMJ3JeJmecLQctjWqeIbpSvy6/gRa7c82Ge5rLlapYmOChs=\n\n  # Secrets contains the root and intermediate keys and optionally the SSH\n  # private keys\n  secrets:\n    # ca_password contains the password used to encrypt x509.intermediate_ca_key, ssh.host_ca_key and ssh.user_ca_key\n    # This value must be base64 encoded.\n    ca_password: \n    provisioner_password: \n\n    x509:\n      # intermediate_ca_key contains the contents of your encrypted intermediate CA key\n      intermediate_ca_key: |\n        -----BEGIN EC PRIVATE KEY-----\n        dGhlc2UgYXJlIGp1c3Qgc29tZSBmYWtlIGludGVybWVkaWF0ZSBDQSBrZXkgYnl0\n        ZXM=\n        -----END EC PRIVATE KEY-----\n        \n\n      # root_ca_key contains the contents of your encrypted root CA key\n      # Note that this value can be omitted without impacting the functionality of step-certificates\n      # If supplied, this should be encrypted using a unique password that is not used for encrypting\n      # the intermediate_ca_key, ssh.host_ca_key or ssh.user_ca_key.\n      root_ca_key: |\n        -----BEGIN EC PRIVATE KEY-----\n        dGhlc2UgYXJlIGp1c3Qgc29tZSBmYWtlIHJvb3QgQ0Ega2V5IGJ5dGVz\n        -----END EC PRIVATE KEY-----\n        \n    ssh:\n      # ssh_host_ca_key contains the contents of your encrypted SSH Host CA key\n      host_ca_key: |\n        -----BEGIN EC PRIVATE KEY-----\n        ZmFrZSBzc2ggaG9zdCBrZXkgYnl0ZXM=\n        -----END EC PRIVATE KEY-----\n        \n\n      # ssh_user_ca_key contains the contents of your encrypted SSH User CA key\n      user_ca_key: |\n        -----BEGIN EC PRIVATE KEY-----\n        ZmFrZSBzc2ggdXNlciBrZXkgYnl0ZXM=\n        -----END EC PRIVATE KEY-----\n        \n"
  },
  {
    "path": "policy/engine.go",
    "content": "package policy\n\nimport (\n\t\"crypto/x509\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/url\"\n\n\t\"go.step.sm/crypto/x509util\"\n\t\"golang.org/x/crypto/ssh\"\n\n\t\"github.com/smallstep/certificates/errs\"\n)\n\ntype NamePolicyReason int\n\nconst (\n\t// NotAllowed results when an instance of NamePolicyEngine\n\t// determines that there's a constraint which doesn't permit\n\t// a DNS or another type of SAN to be signed (or otherwise used).\n\tNotAllowed NamePolicyReason = iota + 1\n\t// CannotParseDomain is returned when an error occurs\n\t// when parsing the domain part of SAN or subject.\n\tCannotParseDomain\n\t// CannotParseRFC822Name is returned when an error\n\t// occurs when parsing an email address.\n\tCannotParseRFC822Name\n\t// CannotMatch is the type of error returned when\n\t// an error happens when matching SAN types.\n\tCannotMatchNameToConstraint\n)\n\ntype NameType string\n\nconst (\n\tCNNameType        NameType = \"cn\"\n\tDNSNameType       NameType = \"dns\"\n\tIPNameType        NameType = \"ip\"\n\tEmailNameType     NameType = \"email\"\n\tURINameType       NameType = \"uri\"\n\tPrincipalNameType NameType = \"principal\"\n)\n\ntype NamePolicyError struct {\n\tReason   NamePolicyReason\n\tNameType NameType\n\tName     string\n\tdetail   string\n}\n\nfunc (e *NamePolicyError) Error() string {\n\tswitch e.Reason {\n\tcase NotAllowed:\n\t\treturn fmt.Sprintf(\"%s name %q not allowed\", e.NameType, e.Name)\n\tcase CannotParseDomain:\n\t\treturn fmt.Sprintf(\"cannot parse %s domain %q\", e.NameType, e.Name)\n\tcase CannotParseRFC822Name:\n\t\treturn fmt.Sprintf(\"cannot parse %s rfc822Name %q\", e.NameType, e.Name)\n\tcase CannotMatchNameToConstraint:\n\t\treturn fmt.Sprintf(\"error matching %s name %q to constraint\", e.NameType, e.Name)\n\tdefault:\n\t\treturn fmt.Sprintf(\"unknown error reason (%d): %s\", e.Reason, e.detail)\n\t}\n}\n\n// As implements the As(any) bool interface and allows to use \"errors.As()\" to\n// convert a NotAllowed NamePolicyError to an errs.Error.\nfunc (e *NamePolicyError) As(v any) bool {\n\tif e.Reason == NotAllowed {\n\t\tif err, ok := v.(**errs.Error); ok {\n\t\t\t*err = &errs.Error{\n\t\t\t\tStatus: http.StatusForbidden,\n\t\t\t\tMsg:    fmt.Sprintf(\"The request was forbidden by the certificate authority: %s\", e.Error()),\n\t\t\t\tErr:    e,\n\t\t\t}\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc (e *NamePolicyError) Detail() string {\n\treturn e.detail\n}\n\n// NamePolicyEngine can be used to check that a CSR or Certificate meets all allowed and\n// denied names before a CA creates and/or signs the Certificate.\n// TODO(hs): the X509 RFC also defines name checks on directory name; support that?\n// TODO(hs): implement Stringer interface: describe the contents of the NamePolicyEngine?\n// TODO(hs): implement matching URI schemes, paths, etc; not just the domain part of URI domains\n\ntype NamePolicyEngine struct {\n\t// verifySubjectCommonName is set when Subject Common Name must be verified\n\tverifySubjectCommonName bool\n\t// allowLiteralWildcardNames allows literal wildcard DNS domains\n\tallowLiteralWildcardNames bool\n\n\t// permitted and exluded constraints similar to x509 Name Constraints\n\tpermittedCommonNames    []string\n\texcludedCommonNames     []string\n\tpermittedDNSDomains     []string\n\texcludedDNSDomains      []string\n\tpermittedIPRanges       []*net.IPNet\n\texcludedIPRanges        []*net.IPNet\n\tpermittedEmailAddresses []string\n\texcludedEmailAddresses  []string\n\tpermittedURIDomains     []string\n\texcludedURIDomains      []string\n\tpermittedPrincipals     []string\n\texcludedPrincipals      []string\n\n\t// some internal counts for housekeeping\n\tnumberOfCommonNameConstraints     int\n\tnumberOfDNSDomainConstraints      int\n\tnumberOfIPRangeConstraints        int\n\tnumberOfEmailAddressConstraints   int\n\tnumberOfURIDomainConstraints      int\n\tnumberOfPrincipalConstraints      int\n\ttotalNumberOfPermittedConstraints int\n\ttotalNumberOfExcludedConstraints  int\n\ttotalNumberOfConstraints          int\n}\n\n// NewNamePolicyEngine creates a new NamePolicyEngine with NamePolicyOptions\nfunc New(opts ...NamePolicyOption) (*NamePolicyEngine, error) {\n\te := &NamePolicyEngine{}\n\tfor _, option := range opts {\n\t\tif err := option(e); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\te.permittedCommonNames = removeDuplicates(e.permittedCommonNames)\n\te.permittedDNSDomains = removeDuplicates(e.permittedDNSDomains)\n\te.permittedIPRanges = removeDuplicateIPNets(e.permittedIPRanges)\n\te.permittedEmailAddresses = removeDuplicates(e.permittedEmailAddresses)\n\te.permittedURIDomains = removeDuplicates(e.permittedURIDomains)\n\te.permittedPrincipals = removeDuplicates(e.permittedPrincipals)\n\n\te.excludedCommonNames = removeDuplicates(e.excludedCommonNames)\n\te.excludedDNSDomains = removeDuplicates(e.excludedDNSDomains)\n\te.excludedIPRanges = removeDuplicateIPNets(e.excludedIPRanges)\n\te.excludedEmailAddresses = removeDuplicates(e.excludedEmailAddresses)\n\te.excludedURIDomains = removeDuplicates(e.excludedURIDomains)\n\te.excludedPrincipals = removeDuplicates(e.excludedPrincipals)\n\n\te.numberOfCommonNameConstraints = len(e.permittedCommonNames) + len(e.excludedCommonNames)\n\te.numberOfDNSDomainConstraints = len(e.permittedDNSDomains) + len(e.excludedDNSDomains)\n\te.numberOfIPRangeConstraints = len(e.permittedIPRanges) + len(e.excludedIPRanges)\n\te.numberOfEmailAddressConstraints = len(e.permittedEmailAddresses) + len(e.excludedEmailAddresses)\n\te.numberOfURIDomainConstraints = len(e.permittedURIDomains) + len(e.excludedURIDomains)\n\te.numberOfPrincipalConstraints = len(e.permittedPrincipals) + len(e.excludedPrincipals)\n\n\te.totalNumberOfPermittedConstraints = len(e.permittedCommonNames) + len(e.permittedDNSDomains) +\n\t\tlen(e.permittedIPRanges) + len(e.permittedEmailAddresses) + len(e.permittedURIDomains) +\n\t\tlen(e.permittedPrincipals)\n\n\te.totalNumberOfExcludedConstraints = len(e.excludedCommonNames) + len(e.excludedDNSDomains) +\n\t\tlen(e.excludedIPRanges) + len(e.excludedEmailAddresses) + len(e.excludedURIDomains) +\n\t\tlen(e.excludedPrincipals)\n\n\te.totalNumberOfConstraints = e.totalNumberOfPermittedConstraints + e.totalNumberOfExcludedConstraints\n\n\treturn e, nil\n}\n\n// removeDuplicates returns a new slice of strings with\n// duplicate values removed. It retains the order of elements\n// in the source slice.\nfunc removeDuplicates(items []string) (ret []string) {\n\t// no need to remove dupes; return original\n\tif len(items) <= 1 {\n\t\treturn items\n\t}\n\n\tkeys := make(map[string]struct{}, len(items))\n\n\tret = make([]string, 0, len(items))\n\tfor _, item := range items {\n\t\tif _, ok := keys[item]; ok {\n\t\t\tcontinue\n\t\t}\n\n\t\tkeys[item] = struct{}{}\n\t\tret = append(ret, item)\n\t}\n\n\treturn\n}\n\n// removeDuplicateIPNets returns a new slice of net.IPNets with\n// duplicate values removed. It retains the order of elements in\n// the source slice. An IPNet is considered duplicate if its CIDR\n// notation exists multiple times in the slice.\nfunc removeDuplicateIPNets(items []*net.IPNet) (ret []*net.IPNet) {\n\t// no need to remove dupes; return original\n\tif len(items) <= 1 {\n\t\treturn items\n\t}\n\n\tkeys := make(map[string]struct{}, len(items))\n\n\tret = make([]*net.IPNet, 0, len(items))\n\tfor _, item := range items {\n\t\tkey := item.String() // use CIDR notation as key\n\t\tif _, ok := keys[key]; ok {\n\t\t\tcontinue\n\t\t}\n\n\t\tkeys[key] = struct{}{}\n\t\tret = append(ret, item)\n\t}\n\n\t// TODO(hs): implement filter of fully overlapping ranges,\n\t// so that the smaller ones are automatically removed?\n\n\treturn\n}\n\n// IsX509CertificateAllowed verifies that all SANs in a Certificate are allowed.\nfunc (e *NamePolicyEngine) IsX509CertificateAllowed(cert *x509.Certificate) error {\n\tif err := e.validateNames(cert.DNSNames, cert.IPAddresses, cert.EmailAddresses, cert.URIs, []string{}); err != nil {\n\t\treturn err\n\t}\n\n\tif e.verifySubjectCommonName {\n\t\treturn e.validateCommonName(cert.Subject.CommonName)\n\t}\n\n\treturn nil\n}\n\n// IsX509CertificateRequestAllowed verifies that all names in the CSR are allowed.\nfunc (e *NamePolicyEngine) IsX509CertificateRequestAllowed(csr *x509.CertificateRequest) error {\n\tif err := e.validateNames(csr.DNSNames, csr.IPAddresses, csr.EmailAddresses, csr.URIs, []string{}); err != nil {\n\t\treturn err\n\t}\n\n\tif e.verifySubjectCommonName {\n\t\treturn e.validateCommonName(csr.Subject.CommonName)\n\t}\n\n\treturn nil\n}\n\n// AreSANsAllowed verifies that all names in the slice of SANs are allowed.\n// The SANs are first split into DNS names, IPs, email addresses and URIs.\nfunc (e *NamePolicyEngine) AreSANsAllowed(sans []string) error {\n\tdnsNames, ips, emails, uris := x509util.SplitSANs(sans)\n\treturn e.validateNames(dnsNames, ips, emails, uris, []string{})\n}\n\n// IsDNSAllowed verifies a single DNS domain is allowed.\nfunc (e *NamePolicyEngine) IsDNSAllowed(dns string) error {\n\treturn e.validateNames([]string{dns}, []net.IP{}, []string{}, []*url.URL{}, []string{})\n}\n\n// IsIPAllowed verifies a single IP domain is allowed.\nfunc (e *NamePolicyEngine) IsIPAllowed(ip net.IP) error {\n\treturn e.validateNames([]string{}, []net.IP{ip}, []string{}, []*url.URL{}, []string{})\n}\n\n// IsSSHCertificateAllowed verifies that all principals in an SSH certificate are allowed.\nfunc (e *NamePolicyEngine) IsSSHCertificateAllowed(cert *ssh.Certificate) error {\n\tdnsNames, ips, emails, principals, err := splitSSHPrincipals(cert)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn e.validateNames(dnsNames, ips, emails, []*url.URL{}, principals)\n}\n\n// splitSSHPrincipals splits SSH certificate principals into DNS names, emails and usernames.\nfunc splitSSHPrincipals(cert *ssh.Certificate) (dnsNames []string, ips []net.IP, emails, principals []string, err error) {\n\tdnsNames = []string{}\n\tips = []net.IP{}\n\temails = []string{}\n\tprincipals = []string{}\n\tvar uris []*url.URL\n\tswitch cert.CertType {\n\tcase ssh.HostCert:\n\t\tdnsNames, ips, emails, uris = x509util.SplitSANs(cert.ValidPrincipals)\n\t\tif len(uris) > 0 {\n\t\t\terr = fmt.Errorf(\"URL principals %v not expected in SSH host certificate \", uris)\n\t\t}\n\tcase ssh.UserCert:\n\t\t// re-using SplitSANs results in anything that can't be parsed as an IP, URI or email\n\t\t// to be considered a username principal. This allows usernames like h.slatman to be present\n\t\t// in the SSH certificate. We're exluding URIs, because they can be confusing\n\t\t// when used in a SSH user certificate.\n\t\tprincipals, ips, emails, uris = x509util.SplitSANs(cert.ValidPrincipals)\n\t\tif len(ips) > 0 {\n\t\t\terr = fmt.Errorf(\"IP principals %v not expected in SSH user certificate \", ips)\n\t\t}\n\t\tif len(uris) > 0 {\n\t\t\terr = fmt.Errorf(\"URL principals %v not expected in SSH user certificate \", uris)\n\t\t}\n\tdefault:\n\t\terr = fmt.Errorf(\"unexpected SSH certificate type %d\", cert.CertType)\n\t}\n\n\treturn\n}\n"
  },
  {
    "path": "policy/engine_test.go",
    "content": "package policy\n\nimport (\n\t\"crypto/x509\"\n\t\"crypto/x509/pkix\"\n\t\"errors\"\n\t\"net\"\n\t\"net/url\"\n\t\"reflect\"\n\t\"testing\"\n\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"golang.org/x/crypto/ssh\"\n)\n\n// TODO(hs): the functionality in the policy engine is a nice candidate for trying fuzzing on\n// TODO(hs): more complex test use cases that combine multiple names and permitted/excluded entries?\n\nfunc TestNamePolicyEngine_matchDomainConstraint(t *testing.T) {\n\ttests := []struct {\n\t\tname                      string\n\t\tallowLiteralWildcardNames bool\n\t\tdomain                    string\n\t\tconstraint                string\n\t\twant                      bool\n\t\twantErr                   bool\n\t}{\n\t\t{\n\t\t\tname:       \"fail/wildcard\",\n\t\t\tdomain:     \"host.local\",\n\t\t\tconstraint: \".example.com\", // internally we're using the x509 period prefix as the indicator for exactly one subdomain\n\t\t\twant:       false,\n\t\t\twantErr:    false,\n\t\t},\n\t\t{\n\t\t\tname:       \"fail/wildcard-literal\",\n\t\t\tdomain:     \"*.example.com\",\n\t\t\tconstraint: \".example.com\", // internally we're using the x509 period prefix as the indicator for exactly one subdomain\n\t\t\twant:       false,\n\t\t\twantErr:    false,\n\t\t},\n\t\t{\n\t\t\tname:       \"fail/specific-domain\",\n\t\t\tdomain:     \"www.example.com\",\n\t\t\tconstraint: \"host.example.com\",\n\t\t\twant:       false,\n\t\t\twantErr:    false,\n\t\t},\n\t\t{\n\t\t\tname:       \"fail/single-whitespace-domain\",\n\t\t\tdomain:     \" \",\n\t\t\tconstraint: \"host.example.com\",\n\t\t\twant:       false,\n\t\t\twantErr:    false,\n\t\t},\n\t\t{\n\t\t\tname:       \"fail/period-domain\",\n\t\t\tdomain:     \".host.example.com\",\n\t\t\tconstraint: \".example.com\",\n\t\t\twant:       false,\n\t\t\twantErr:    false,\n\t\t},\n\t\t{\n\t\t\tname:       \"fail/wrong-asterisk-prefix\",\n\t\t\tdomain:     \"*Xexample.com\",\n\t\t\tconstraint: \".example.com\",\n\t\t\twant:       false,\n\t\t\twantErr:    false,\n\t\t},\n\t\t{\n\t\t\tname:       \"fail/asterisk-in-domain\",\n\t\t\tdomain:     \"e*ample.com\",\n\t\t\tconstraint: \".com\",\n\t\t\twant:       false,\n\t\t\twantErr:    false,\n\t\t},\n\t\t{\n\t\t\tname:       \"fail/asterisk-label\",\n\t\t\tdomain:     \"example.*.local\",\n\t\t\tconstraint: \".local\",\n\t\t\twant:       false,\n\t\t\twantErr:    false,\n\t\t},\n\t\t{\n\t\t\tname:       \"fail/multiple-periods\",\n\t\t\tdomain:     \"example.local\",\n\t\t\tconstraint: \"..local\",\n\t\t\twant:       false,\n\t\t\twantErr:    false,\n\t\t},\n\t\t{\n\t\t\tname:       \"fail/error-parsing-domain\",\n\t\t\tdomain:     string(byte(0)),\n\t\t\tconstraint: \".local\",\n\t\t\twant:       false,\n\t\t\twantErr:    true,\n\t\t},\n\t\t{\n\t\t\tname:       \"fail/error-parsing-constraint\",\n\t\t\tdomain:     \"example.local\",\n\t\t\tconstraint: string(byte(0)),\n\t\t\twant:       false,\n\t\t\twantErr:    true,\n\t\t},\n\t\t{\n\t\t\tname:       \"fail/no-subdomain\",\n\t\t\tdomain:     \"local\",\n\t\t\tconstraint: \".local\",\n\t\t\twant:       false,\n\t\t\twantErr:    false,\n\t\t},\n\t\t{\n\t\t\tname:       \"fail/too-many-subdomains\",\n\t\t\tdomain:     \"www.example.local\",\n\t\t\tconstraint: \".local\",\n\t\t\twant:       false,\n\t\t\twantErr:    false,\n\t\t},\n\t\t{\n\t\t\tname:       \"fail/wrong-domain\",\n\t\t\tdomain:     \"example.notlocal\",\n\t\t\tconstraint: \".local\",\n\t\t\twant:       false,\n\t\t\twantErr:    false,\n\t\t},\n\t\t{\n\t\t\tname:       \"false/idna-internationalized-domain-name\",\n\t\t\tdomain:     \"JP納豆.例.jp\", // Example value from https://www.w3.org/International/articles/idn-and-iri/\n\t\t\tconstraint: \".例.jp\",\n\t\t\twant:       false,\n\t\t\twantErr:    true,\n\t\t},\n\t\t{\n\t\t\tname:       \"false/idna-internationalized-domain-name-constraint\",\n\t\t\tdomain:     \"xn--jp-cd2fp15c.xn--fsq.jp\", // Example value from https://www.w3.org/International/articles/idn-and-iri/\n\t\t\tconstraint: \".例.jp\",\n\t\t\twant:       false,\n\t\t\twantErr:    true,\n\t\t},\n\t\t{\n\t\t\tname:       \"ok/empty-constraint\",\n\t\t\tdomain:     \"www.example.com\",\n\t\t\tconstraint: \"\",\n\t\t\twant:       true,\n\t\t\twantErr:    false,\n\t\t},\n\t\t{\n\t\t\tname:       \"ok/wildcard\",\n\t\t\tdomain:     \"www.example.com\",\n\t\t\tconstraint: \".example.com\", // internally we're using the x509 period prefix as the indicator for exactly one subdomain\n\t\t\twant:       true,\n\t\t\twantErr:    false,\n\t\t},\n\t\t{\n\t\t\tname:                      \"ok/wildcard-literal\",\n\t\t\tallowLiteralWildcardNames: true,\n\t\t\tdomain:                    \"*.example.com\", // specifically allowed using an option on the NamePolicyEngine\n\t\t\tconstraint:                \".example.com\",  // internally we're using the x509 period prefix as the indicator for exactly one subdomain\n\t\t\twant:                      true,\n\t\t\twantErr:                   false,\n\t\t},\n\t\t{\n\t\t\tname:       \"ok/specific-domain\",\n\t\t\tdomain:     \"www.example.com\",\n\t\t\tconstraint: \"www.example.com\",\n\t\t\twant:       true,\n\t\t\twantErr:    false,\n\t\t},\n\t\t{\n\t\t\tname:       \"ok/different-case\",\n\t\t\tdomain:     \"WWW.EXAMPLE.com\",\n\t\t\tconstraint: \"www.example.com\",\n\t\t\twant:       true,\n\t\t\twantErr:    false,\n\t\t},\n\t\t{\n\t\t\tname:       \"ok/idna-internationalized-domain-name-punycode\",\n\t\t\tdomain:     \"xn--jp-cd2fp15c.xn--fsq.jp\", // Example value from https://www.w3.org/International/articles/idn-and-iri/\n\t\t\tconstraint: \".xn--fsq.jp\",\n\t\t\twant:       true,\n\t\t\twantErr:    false,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tengine := NamePolicyEngine{\n\t\t\t\tallowLiteralWildcardNames: tt.allowLiteralWildcardNames,\n\t\t\t}\n\t\t\tgot, err := engine.matchDomainConstraint(tt.domain, tt.constraint)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"NamePolicyEngine.matchDomainConstraint() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif got != tt.want {\n\t\t\t\tt.Errorf(\"NamePolicyEngine.matchDomainConstraint() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_matchIPConstraint(t *testing.T) {\n\tnat64IP, nat64Net, err := net.ParseCIDR(\"64:ff9b::/96\")\n\tassert.NoError(t, err)\n\ttests := []struct {\n\t\tname       string\n\t\tip         net.IP\n\t\tconstraint *net.IPNet\n\t\twant       bool\n\t\twantErr    bool\n\t}{\n\t\t{\n\t\t\tname:       \"false/ipv4-in-ipv6-nat64\",\n\t\t\tip:         net.ParseIP(\"192.0.2.128\"),\n\t\t\tconstraint: nat64Net,\n\t\t\twant:       false,\n\t\t\twantErr:    false,\n\t\t},\n\t\t{\n\t\t\tname: \"ok/ipv4\",\n\t\t\tip:   net.ParseIP(\"127.0.0.1\"),\n\t\t\tconstraint: &net.IPNet{\n\t\t\t\tIP:   net.ParseIP(\"127.0.0.0\"),\n\t\t\t\tMask: net.IPv4Mask(255, 255, 255, 0),\n\t\t\t},\n\t\t\twant:    true,\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"ok/ipv6\",\n\t\t\tip:   net.ParseIP(\"2001:0db8:85a3:0000:0000:8a2e:0370:7335\"),\n\t\t\tconstraint: &net.IPNet{\n\t\t\t\tIP:   net.ParseIP(\"2001:0db8:85a3:0000:0000:8a2e:0370:7334\"),\n\t\t\t\tMask: net.CIDRMask(120, 128),\n\t\t\t},\n\t\t\twant:    true,\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"ok/ipv4-in-ipv6\", // ipv4 in ipv6 addresses are considered the same in the current implementation, because Go parses them as IPv4\n\t\t\tip:   net.ParseIP(\"::ffff:192.0.2.128\"),\n\t\t\tconstraint: &net.IPNet{\n\t\t\t\tIP:   net.ParseIP(\"192.0.2.0\"),\n\t\t\t\tMask: net.IPv4Mask(255, 255, 255, 0),\n\t\t\t},\n\t\t\twant:    true,\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:       \"ok/ipv4-in-ipv6-nat64-fixed-ip\",\n\t\t\tip:         nat64IP,\n\t\t\tconstraint: nat64Net,\n\t\t\twant:       true,\n\t\t\twantErr:    false,\n\t\t},\n\t\t{\n\t\t\tname:       \"ok/ipv4-in-ipv6-nat64\",\n\t\t\tip:         net.ParseIP(\"64:ff9b::192.0.2.129\"),\n\t\t\tconstraint: nat64Net,\n\t\t\twant:       true,\n\t\t\twantErr:    false,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := matchIPConstraint(tt.ip, tt.constraint)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"matchIPConstraint() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif got != tt.want {\n\t\t\t\tt.Errorf(\"matchIPConstraint() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNamePolicyEngine_matchEmailConstraint(t *testing.T) {\n\n\ttests := []struct {\n\t\tname       string\n\t\tengine     *NamePolicyEngine\n\t\tmailbox    rfc2821Mailbox\n\t\tconstraint string\n\t\twant       bool\n\t\twantErr    bool\n\t}{\n\t\t{\n\t\t\tname:   \"fail/asterisk-label\",\n\t\t\tengine: &NamePolicyEngine{},\n\t\t\tmailbox: rfc2821Mailbox{\n\t\t\t\tlocal:  \"mail\",\n\t\t\t\tdomain: \"local\",\n\t\t\t},\n\t\t\tconstraint: \"@host.*.example.com\",\n\t\t\twant:       false,\n\t\t\twantErr:    true,\n\t\t},\n\t\t{\n\t\t\tname:   \"fail/asterisk-inside-domain\",\n\t\t\tengine: &NamePolicyEngine{},\n\t\t\tmailbox: rfc2821Mailbox{\n\t\t\t\tlocal:  \"mail\",\n\t\t\t\tdomain: \"local\",\n\t\t\t},\n\t\t\tconstraint: \"@h*st.example.com\",\n\t\t\twant:       false,\n\t\t\twantErr:    true,\n\t\t},\n\t\t{\n\t\t\tname:   \"fail/parse-email\",\n\t\t\tengine: &NamePolicyEngine{},\n\t\t\tmailbox: rfc2821Mailbox{\n\t\t\t\tlocal:  \"mail\",\n\t\t\t\tdomain: \"local\",\n\t\t\t},\n\t\t\tconstraint: \"@example.com\",\n\t\t\twant:       false,\n\t\t\twantErr:    true,\n\t\t},\n\t\t{\n\t\t\tname:   \"fail/wildcard\",\n\t\t\tengine: &NamePolicyEngine{},\n\t\t\tmailbox: rfc2821Mailbox{\n\t\t\t\tlocal:  \"mail\",\n\t\t\t\tdomain: \"local\",\n\t\t\t},\n\t\t\tconstraint: \"example.com\",\n\t\t\twant:       false,\n\t\t\twantErr:    false,\n\t\t},\n\t\t{\n\t\t\tname:   \"fail/wildcard-x509-period\",\n\t\t\tengine: &NamePolicyEngine{},\n\t\t\tmailbox: rfc2821Mailbox{\n\t\t\t\tlocal:  \"mail\",\n\t\t\t\tdomain: \"local\",\n\t\t\t},\n\t\t\tconstraint: \".local\",\n\t\t\twant:       false,\n\t\t\twantErr:    false,\n\t\t},\n\t\t{\n\t\t\tname:   \"fail/specific-mail-wrong-domain\",\n\t\t\tengine: &NamePolicyEngine{},\n\t\t\tmailbox: rfc2821Mailbox{\n\t\t\t\tlocal:  \"mail\",\n\t\t\t\tdomain: \"local\",\n\t\t\t},\n\t\t\tconstraint: \"mail@example.com\",\n\t\t\twant:       false,\n\t\t\twantErr:    false,\n\t\t},\n\t\t{\n\t\t\tname:   \"fail/specific-mail-wrong-local\",\n\t\t\tengine: &NamePolicyEngine{},\n\t\t\tmailbox: rfc2821Mailbox{\n\t\t\t\tlocal:  \"root\",\n\t\t\t\tdomain: \"example.com\",\n\t\t\t},\n\t\t\tconstraint: \"mail@example.com\",\n\t\t\twant:       false,\n\t\t\twantErr:    false,\n\t\t},\n\t\t{\n\t\t\tname:   \"ok/wildcard\",\n\t\t\tengine: &NamePolicyEngine{},\n\t\t\tmailbox: rfc2821Mailbox{\n\t\t\t\tlocal:  \"mail\",\n\t\t\t\tdomain: \"local\",\n\t\t\t},\n\t\t\tconstraint: \"local\", // \"wildcard\" for the local domain\n\t\t\twant:       true,\n\t\t\twantErr:    false,\n\t\t},\n\t\t{\n\t\t\tname:   \"ok/wildcard-x509-period\",\n\t\t\tengine: &NamePolicyEngine{},\n\t\t\tmailbox: rfc2821Mailbox{\n\t\t\t\tlocal:  \"mail\",\n\t\t\t\tdomain: \"example.local\",\n\t\t\t},\n\t\t\tconstraint: \".local\", // \"wildcard\" for the local domain; requires exactly 1 subdomain\n\t\t\twant:       true,\n\t\t\twantErr:    false,\n\t\t},\n\t\t{\n\t\t\tname:   \"ok/asterisk-prefix\",\n\t\t\tengine: &NamePolicyEngine{},\n\t\t\tmailbox: rfc2821Mailbox{\n\t\t\t\tlocal:  \"mail\",\n\t\t\t\tdomain: \"local\",\n\t\t\t},\n\t\t\tconstraint: \"*@example.com\",\n\t\t\twant:       false,\n\t\t\twantErr:    false,\n\t\t},\n\t\t{\n\t\t\tname:   \"ok/asterisk-prefix-match\",\n\t\t\tengine: &NamePolicyEngine{},\n\t\t\tmailbox: rfc2821Mailbox{\n\t\t\t\tlocal:  \"*\",\n\t\t\t\tdomain: \"example.com\",\n\t\t\t},\n\t\t\tconstraint: \"*@example.com\",\n\t\t\twant:       true,\n\t\t\twantErr:    false,\n\t\t},\n\t\t{\n\t\t\tname:   \"ok/asterisk-inside-local\",\n\t\t\tengine: &NamePolicyEngine{},\n\t\t\tmailbox: rfc2821Mailbox{\n\t\t\t\tlocal:  \"mail\",\n\t\t\t\tdomain: \"local\",\n\t\t\t},\n\t\t\tconstraint: \"m*il@local\",\n\t\t\twant:       false,\n\t\t\twantErr:    false,\n\t\t},\n\t\t{\n\t\t\tname:   \"ok/asterisk-inside-local-match\",\n\t\t\tengine: &NamePolicyEngine{},\n\t\t\tmailbox: rfc2821Mailbox{\n\t\t\t\tlocal:  \"m*il\",\n\t\t\t\tdomain: \"local\",\n\t\t\t},\n\t\t\tconstraint: \"m*il@local\",\n\t\t\twant:       true,\n\t\t\twantErr:    false,\n\t\t},\n\t\t{\n\t\t\tname:   \"ok/specific-mail\",\n\t\t\tengine: &NamePolicyEngine{},\n\t\t\tmailbox: rfc2821Mailbox{\n\t\t\t\tlocal:  \"mail\",\n\t\t\t\tdomain: \"local\",\n\t\t\t},\n\t\t\tconstraint: \"mail@local\",\n\t\t\twant:       true,\n\t\t\twantErr:    false,\n\t\t},\n\t\t{\n\t\t\tname:   \"ok/wildcard-tld\",\n\t\t\tengine: &NamePolicyEngine{},\n\t\t\tmailbox: rfc2821Mailbox{\n\t\t\t\tlocal:  \"mail\",\n\t\t\t\tdomain: \"example.com\",\n\t\t\t},\n\t\t\tconstraint: \"example.com\", // \"wildcard\" for 'example.com'\n\t\t\twant:       true,\n\t\t\twantErr:    false,\n\t\t},\n\t\t{\n\t\t\tname:   \"ok/different-case\",\n\t\t\tengine: &NamePolicyEngine{},\n\t\t\tmailbox: rfc2821Mailbox{\n\t\t\t\tlocal:  \"mail\",\n\t\t\t\tdomain: \"EXAMPLE.com\",\n\t\t\t},\n\t\t\tconstraint: \"example.com\", // \"wildcard\" for 'example.com'\n\t\t\twant:       true,\n\t\t\twantErr:    false,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := tt.engine.matchEmailConstraint(tt.mailbox, tt.constraint)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"NamePolicyEngine.matchEmailConstraint() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif got != tt.want {\n\t\t\t\tt.Errorf(\"NamePolicyEngine.matchEmailConstraint() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNamePolicyEngine_matchURIConstraint(t *testing.T) {\n\ttests := []struct {\n\t\tname       string\n\t\tengine     *NamePolicyEngine\n\t\turi        *url.URL\n\t\tconstraint string\n\t\twant       bool\n\t\twantErr    bool\n\t}{\n\t\t{\n\t\t\tname:   \"fail/empty-host\",\n\t\t\tengine: &NamePolicyEngine{},\n\t\t\turi: &url.URL{\n\t\t\t\tScheme: \"https\",\n\t\t\t\tHost:   \"\",\n\t\t\t},\n\t\t\tconstraint: \".local\",\n\t\t\twant:       false,\n\t\t\twantErr:    true,\n\t\t},\n\t\t{\n\t\t\tname:   \"fail/host-with-asterisk-prefix\",\n\t\t\tengine: &NamePolicyEngine{},\n\t\t\turi: &url.URL{\n\t\t\t\tScheme: \"https\",\n\t\t\t\tHost:   \"*.local\",\n\t\t\t},\n\t\t\tconstraint: \".local\",\n\t\t\twant:       false,\n\t\t\twantErr:    true,\n\t\t},\n\t\t{\n\t\t\tname:   \"fail/host-with-asterisk-label\",\n\t\t\tengine: &NamePolicyEngine{},\n\t\t\turi: &url.URL{\n\t\t\t\tScheme: \"https\",\n\t\t\t\tHost:   \"host.*.local\",\n\t\t\t},\n\t\t\tconstraint: \".local\",\n\t\t\twant:       false,\n\t\t\twantErr:    true,\n\t\t},\n\t\t{\n\t\t\tname:   \"fail/host-with-asterisk-inside\",\n\t\t\tengine: &NamePolicyEngine{},\n\t\t\turi: &url.URL{\n\t\t\t\tScheme: \"https\",\n\t\t\t\tHost:   \"h*st.local\",\n\t\t\t},\n\t\t\tconstraint: \".local\",\n\t\t\twant:       false,\n\t\t\twantErr:    true,\n\t\t},\n\t\t{\n\t\t\tname:   \"fail/wildcard\",\n\t\t\tengine: &NamePolicyEngine{},\n\t\t\turi: &url.URL{\n\t\t\t\tScheme: \"https\",\n\t\t\t\tHost:   \"www.example.notlocal\",\n\t\t\t},\n\t\t\tconstraint: \".example.local\", // using x509 period as the \"wildcard\"; expects a single subdomain\n\t\t\twant:       false,\n\t\t\twantErr:    false,\n\t\t},\n\t\t{\n\t\t\tname:   \"fail/wildcard-subdomains-too-deep\",\n\t\t\tengine: &NamePolicyEngine{},\n\t\t\turi: &url.URL{\n\t\t\t\tScheme: \"https\",\n\t\t\t\tHost:   \"www.sub.example.local\",\n\t\t\t},\n\t\t\tconstraint: \".example.local\", // using x509 period as the \"wildcard\"; expects a single subdomain\n\t\t\twant:       false,\n\t\t\twantErr:    false,\n\t\t},\n\t\t{\n\t\t\tname:   \"fail/host-with-port-split-error\",\n\t\t\tengine: &NamePolicyEngine{},\n\t\t\turi: &url.URL{\n\t\t\t\tScheme: \"https\",\n\t\t\t\tHost:   \"www.example.local::8080\",\n\t\t\t},\n\t\t\tconstraint: \".example.local\", // using x509 period as the \"wildcard\"; expects a single subdomain\n\t\t\twant:       false,\n\t\t\twantErr:    true,\n\t\t},\n\t\t{\n\t\t\tname:   \"fail/host-with-ipv4\",\n\t\t\tengine: &NamePolicyEngine{},\n\t\t\turi: &url.URL{\n\t\t\t\tScheme: \"https\",\n\t\t\t\tHost:   \"127.0.0.1\",\n\t\t\t},\n\t\t\tconstraint: \".example.local\", // using x509 period as the \"wildcard\"; expects a single subdomain\n\t\t\twant:       false,\n\t\t\twantErr:    true,\n\t\t},\n\t\t{\n\t\t\tname:   \"fail/host-with-ipv6\",\n\t\t\tengine: &NamePolicyEngine{},\n\t\t\turi: &url.URL{\n\t\t\t\tScheme: \"https\",\n\t\t\t\tHost:   \"[2001:0db8:85a3:0000:0000:8a2e:0370:7334]\",\n\t\t\t},\n\t\t\tconstraint: \".example.local\", // using x509 period as the \"wildcard\"; expects a single subdomain\n\t\t\twant:       false,\n\t\t\twantErr:    true,\n\t\t},\n\t\t{\n\t\t\tname:   \"ok/wildcard\",\n\t\t\tengine: &NamePolicyEngine{},\n\t\t\turi: &url.URL{\n\t\t\t\tScheme: \"https\",\n\t\t\t\tHost:   \"www.example.local\",\n\t\t\t},\n\t\t\tconstraint: \".example.local\", // using x509 period as the \"wildcard\"; expects a single subdomain\n\t\t\twant:       true,\n\t\t\twantErr:    false,\n\t\t},\n\t\t{\n\t\t\tname:   \"ok/host-with-port\",\n\t\t\tengine: &NamePolicyEngine{},\n\t\t\turi: &url.URL{\n\t\t\t\tScheme: \"https\",\n\t\t\t\tHost:   \"www.example.local:8080\",\n\t\t\t},\n\t\t\tconstraint: \".example.local\", // using x509 period as the \"wildcard\"; expects a single subdomain\n\t\t\twant:       true,\n\t\t\twantErr:    false,\n\t\t},\n\t\t{\n\t\t\tname:   \"ok/different-case\",\n\t\t\tengine: &NamePolicyEngine{},\n\t\t\turi: &url.URL{\n\t\t\t\tScheme: \"https\",\n\t\t\t\tHost:   \"www.EXAMPLE.local\",\n\t\t\t},\n\t\t\tconstraint: \".example.local\", // using x509 period as the \"wildcard\"; expects a single subdomain\n\t\t\twant:       true,\n\t\t\twantErr:    false,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := tt.engine.matchURIConstraint(tt.uri, tt.constraint)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"NamePolicyEngine.matchURIConstraint() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif got != tt.want {\n\t\t\t\tt.Errorf(\"NamePolicyEngine.matchURIConstraint() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNamePolicyEngine_X509_AllAllowed(t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\toptions []NamePolicyOption\n\t\tcert    *x509.Certificate\n\t\twant    bool\n\t\twantErr *NamePolicyError\n\t}{\n\t\t// SINGLE SAN TYPE PERMITTED FAILURE TESTS\n\t\t{\n\t\t\tname: \"fail/dns-permitted\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithPermittedDNSDomains(\"*.local\"),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tDNSNames: []string{\"www.example.com\"},\n\t\t\t},\n\t\t\twant: false,\n\t\t\twantErr: &NamePolicyError{\n\t\t\t\tReason:   NotAllowed,\n\t\t\t\tNameType: DNSNameType,\n\t\t\t\tName:     \"www.example.com\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"fail/dns-permitted-wildcard-literal-x509\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithPermittedDNSDomains(\"*.x509local\"),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tDNSNames: []string{\n\t\t\t\t\t\"*.x509local\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: false,\n\t\t\twantErr: &NamePolicyError{\n\t\t\t\tReason:   NotAllowed,\n\t\t\t\tNameType: DNSNameType,\n\t\t\t\tName:     \"*.x509local\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"fail/dns-permitted-single-host\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithPermittedDNSDomains(\"host.local\"),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tDNSNames: []string{\"differenthost.local\"},\n\t\t\t},\n\t\t\twant: false,\n\t\t\twantErr: &NamePolicyError{\n\t\t\t\tReason:   NotAllowed,\n\t\t\t\tNameType: DNSNameType,\n\t\t\t\tName:     \"differenthost.local\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"fail/dns-permitted-no-label\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithPermittedDNSDomains(\"*.local\"),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tDNSNames: []string{\"local\"},\n\t\t\t},\n\t\t\twant: false,\n\t\t\twantErr: &NamePolicyError{\n\t\t\t\tReason:   NotAllowed,\n\t\t\t\tNameType: DNSNameType,\n\t\t\t\tName:     \"local\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"fail/dns-permitted-empty-label\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithPermittedDNSDomains(\"*.local\"),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tDNSNames: []string{\"www..local\"},\n\t\t\t},\n\t\t\twant: false,\n\t\t\twantErr: &NamePolicyError{\n\t\t\t\tReason:   CannotParseDomain,\n\t\t\t\tNameType: DNSNameType,\n\t\t\t\tName:     \"www..local\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"fail/dns-permitted-dot-domain\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithPermittedDNSDomains(\"*.local\"),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tDNSNames: []string{\n\t\t\t\t\t\".local\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: false,\n\t\t\twantErr: &NamePolicyError{\n\t\t\t\tReason:   NotAllowed,\n\t\t\t\tNameType: DNSNameType,\n\t\t\t\tName:     \".local\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"fail/dns-permitted-wildcard-multiple-subdomains\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithPermittedDNSDomains(\"*.local\"),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tDNSNames: []string{\n\t\t\t\t\t\"sub.example.local\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: false,\n\t\t\twantErr: &NamePolicyError{\n\t\t\t\tReason:   NotAllowed,\n\t\t\t\tNameType: DNSNameType,\n\t\t\t\tName:     \"sub.example.local\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"fail/dns-permitted-wildcard-literal\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithPermittedDNSDomains(\"*.local\"),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tDNSNames: []string{\n\t\t\t\t\t\"*.local\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: false,\n\t\t\twantErr: &NamePolicyError{\n\t\t\t\tReason:   NotAllowed,\n\t\t\t\tNameType: DNSNameType,\n\t\t\t\tName:     \"*.local\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"fail/dns-permitted-idna-internationalized-domain\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithPermittedDNSDomains(\"*.豆.jp\"),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tDNSNames: []string{\n\t\t\t\t\tstring(byte(0)) + \".例.jp\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: false,\n\t\t\twantErr: &NamePolicyError{\n\t\t\t\tReason:   CannotParseDomain,\n\t\t\t\tNameType: DNSNameType,\n\t\t\t\tName:     string(byte(0)) + \".例.jp\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"fail/ipv4-permitted\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithPermittedIPRanges(\n\t\t\t\t\t&net.IPNet{\n\t\t\t\t\t\tIP:   net.ParseIP(\"127.0.0.1\"),\n\t\t\t\t\t\tMask: net.IPv4Mask(255, 255, 255, 0),\n\t\t\t\t\t},\n\t\t\t\t),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tIPAddresses: []net.IP{net.ParseIP(\"1.1.1.1\")},\n\t\t\t},\n\t\t\twant: false,\n\t\t\twantErr: &NamePolicyError{\n\t\t\t\tReason:   NotAllowed,\n\t\t\t\tNameType: IPNameType,\n\t\t\t\tName:     \"1.1.1.1\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"fail/ipv6-permitted\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithPermittedIPRanges(\n\t\t\t\t\t&net.IPNet{\n\t\t\t\t\t\tIP:   net.ParseIP(\"2001:0db8:85a3:0000:0000:8a2e:0370:7334\"),\n\t\t\t\t\t\tMask: net.CIDRMask(120, 128),\n\t\t\t\t\t},\n\t\t\t\t),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tIPAddresses: []net.IP{net.ParseIP(\"3001:0db8:85a3:0000:0000:8a2e:0370:7334\")},\n\t\t\t},\n\t\t\twant: false,\n\t\t\twantErr: &NamePolicyError{\n\t\t\t\tReason:   NotAllowed,\n\t\t\t\tNameType: IPNameType,\n\t\t\t\tName:     \"3001:db8:85a3::8a2e:370:7334\", // IPv6 is shortened internally\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"fail/mail-permitted-wildcard\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithPermittedEmailAddresses(\"@example.com\"),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tEmailAddresses: []string{\n\t\t\t\t\t\"test@local.com\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: false,\n\t\t\twantErr: &NamePolicyError{\n\t\t\t\tReason:   NotAllowed,\n\t\t\t\tNameType: EmailNameType,\n\t\t\t\tName:     \"test@local.com\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"fail/mail-permitted-wildcard-x509\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithPermittedEmailAddresses(\"example.com\"),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tEmailAddresses: []string{\n\t\t\t\t\t\"test@local.com\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: false,\n\t\t\twantErr: &NamePolicyError{\n\t\t\t\tReason:   NotAllowed,\n\t\t\t\tNameType: EmailNameType,\n\t\t\t\tName:     \"test@local.com\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"fail/mail-permitted-specific-mailbox\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithPermittedEmailAddresses(\"test@local.com\"),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tEmailAddresses: []string{\n\t\t\t\t\t\"root@local.com\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: false,\n\t\t\twantErr: &NamePolicyError{\n\t\t\t\tReason:   NotAllowed,\n\t\t\t\tNameType: EmailNameType,\n\t\t\t\tName:     \"root@local.com\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"fail/mail-permitted-wildcard-subdomain\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithPermittedEmailAddresses(\"@example.com\"),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tEmailAddresses: []string{\n\t\t\t\t\t\"test@sub.example.com\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: false,\n\t\t\twantErr: &NamePolicyError{\n\t\t\t\tReason:   NotAllowed,\n\t\t\t\tNameType: EmailNameType,\n\t\t\t\tName:     \"test@sub.example.com\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"fail/mail-permitted-idna-internationalized-domain\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithPermittedEmailAddresses(\"@例.jp\"),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tEmailAddresses: []string{\"bücher@例.jp\"},\n\t\t\t},\n\t\t\twant: false,\n\t\t\twantErr: &NamePolicyError{\n\t\t\t\tReason:   CannotParseRFC822Name,\n\t\t\t\tNameType: EmailNameType,\n\t\t\t\tName:     \"bücher@例.jp\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"fail/mail-permitted-idna-internationalized-domain-rfc822\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithPermittedEmailAddresses(\"@例.jp\"),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tEmailAddresses: []string{\"bücher@例.jp\" + string(byte(0))},\n\t\t\t},\n\t\t\twant: false,\n\t\t\twantErr: &NamePolicyError{\n\t\t\t\tReason:   CannotParseRFC822Name,\n\t\t\t\tNameType: EmailNameType,\n\t\t\t\tName:     \"bücher@例.jp\" + string(byte(0)),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"fail/mail-permitted-idna-internationalized-domain-ascii\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithPermittedEmailAddresses(\"@例.jp\"),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tEmailAddresses: []string{\"mail@xn---bla.jp\"},\n\t\t\t},\n\t\t\twant: false,\n\t\t\twantErr: &NamePolicyError{\n\t\t\t\tReason:   CannotParseDomain,\n\t\t\t\tNameType: EmailNameType,\n\t\t\t\tName:     \"mail@xn---bla.jp\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"fail/uri-permitted-domain-wildcard\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithPermittedURIDomains(\"*.local\"),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tURIs: []*url.URL{\n\t\t\t\t\t{\n\t\t\t\t\t\tScheme: \"https\",\n\t\t\t\t\t\tHost:   \"example.com\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: false,\n\t\t\twantErr: &NamePolicyError{\n\t\t\t\tReason:   NotAllowed,\n\t\t\t\tNameType: URINameType,\n\t\t\t\tName:     \"https://example.com\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"fail/uri-permitted\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithPermittedURIDomains(\"test.local\"),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tURIs: []*url.URL{\n\t\t\t\t\t{\n\t\t\t\t\t\tScheme: \"https\",\n\t\t\t\t\t\tHost:   \"bad.local\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: false,\n\t\t\twantErr: &NamePolicyError{\n\t\t\t\tReason:   NotAllowed,\n\t\t\t\tNameType: URINameType,\n\t\t\t\tName:     \"https://bad.local\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"fail/uri-permitted-with-literal-wildcard\", // don't allow literal wildcard in URI, e.g. xxxx://*.domain.tld\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithPermittedURIDomains(\"*.local\"),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tURIs: []*url.URL{\n\t\t\t\t\t{\n\t\t\t\t\t\tScheme: \"https\",\n\t\t\t\t\t\tHost:   \"*.local\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: false,\n\t\t\twantErr: &NamePolicyError{\n\t\t\t\tReason:   CannotMatchNameToConstraint,\n\t\t\t\tNameType: URINameType,\n\t\t\t\tName:     \"https://*.local\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"fail/uri-permitted-idna-internationalized-domain\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithPermittedURIDomains(\"*.bücher.example.com\"),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tURIs: []*url.URL{\n\t\t\t\t\t{\n\t\t\t\t\t\tScheme: \"https\",\n\t\t\t\t\t\tHost:   \"abc.bücher.example.com\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: false,\n\t\t\twantErr: &NamePolicyError{\n\t\t\t\tReason:   CannotMatchNameToConstraint,\n\t\t\t\tNameType: URINameType,\n\t\t\t\tName:     \"https://abc.b%C3%BCcher.example.com\",\n\t\t\t},\n\t\t},\n\t\t// SINGLE SAN TYPE EXCLUDED FAILURE TESTS\n\t\t{\n\t\t\tname: \"fail/dns-excluded\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithExcludedDNSDomains(\"*.example.com\"),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tDNSNames: []string{\"www.example.com\"},\n\t\t\t},\n\t\t\twant: false,\n\t\t\twantErr: &NamePolicyError{\n\t\t\t\tReason:   NotAllowed,\n\t\t\t\tNameType: DNSNameType,\n\t\t\t\tName:     \"www.example.com\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"fail/dns-excluded-single-host\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithExcludedDNSDomains(\"host.example.com\"),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tDNSNames: []string{\"host.example.com\"},\n\t\t\t},\n\t\t\twant: false,\n\t\t\twantErr: &NamePolicyError{\n\t\t\t\tReason:   NotAllowed,\n\t\t\t\tNameType: DNSNameType,\n\t\t\t\tName:     \"host.example.com\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"fail/ipv4-excluded\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithExcludedIPRanges(\n\t\t\t\t\t&net.IPNet{\n\t\t\t\t\t\tIP:   net.ParseIP(\"127.0.0.1\"),\n\t\t\t\t\t\tMask: net.IPv4Mask(255, 255, 255, 0),\n\t\t\t\t\t},\n\t\t\t\t),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tIPAddresses: []net.IP{net.ParseIP(\"127.0.0.1\")},\n\t\t\t},\n\t\t\twant: false,\n\t\t\twantErr: &NamePolicyError{\n\t\t\t\tReason:   NotAllowed,\n\t\t\t\tNameType: IPNameType,\n\t\t\t\tName:     \"127.0.0.1\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"fail/ipv6-excluded\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithExcludedIPRanges(\n\t\t\t\t\t&net.IPNet{\n\t\t\t\t\t\tIP:   net.ParseIP(\"2001:0db8:85a3:0000:0000:8a2e:0370:7334\"),\n\t\t\t\t\t\tMask: net.CIDRMask(120, 128),\n\t\t\t\t\t},\n\t\t\t\t),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tIPAddresses: []net.IP{net.ParseIP(\"2001:0db8:85a3:0000:0000:8a2e:0370:7334\")},\n\t\t\t},\n\t\t\twant: false,\n\t\t\twantErr: &NamePolicyError{\n\t\t\t\tReason:   NotAllowed,\n\t\t\t\tNameType: IPNameType,\n\t\t\t\tName:     \"2001:db8:85a3::8a2e:370:7334\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"fail/mail-excluded\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithExcludedEmailAddresses(\"@example.com\"),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tEmailAddresses: []string{\"mail@example.com\"},\n\t\t\t},\n\t\t\twant: false,\n\t\t\twantErr: &NamePolicyError{\n\t\t\t\tReason:   NotAllowed,\n\t\t\t\tNameType: EmailNameType,\n\t\t\t\tName:     \"mail@example.com\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"fail/uri-excluded\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithExcludedURIDomains(\"*.example.com\"),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tURIs: []*url.URL{\n\t\t\t\t\t{\n\t\t\t\t\t\tScheme: \"https\",\n\t\t\t\t\t\tHost:   \"www.example.com\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: false,\n\t\t\twantErr: &NamePolicyError{\n\t\t\t\tReason:   NotAllowed,\n\t\t\t\tNameType: URINameType,\n\t\t\t\tName:     \"https://www.example.com\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"fail/uri-excluded-with-literal-wildcard\", // don't allow literal wildcard in URI, e.g. xxxx://*.domain.tld\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithExcludedURIDomains(\"*.local\"),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tURIs: []*url.URL{\n\t\t\t\t\t{\n\t\t\t\t\t\tScheme: \"https\",\n\t\t\t\t\t\tHost:   \"*.local\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: false,\n\t\t\twantErr: &NamePolicyError{\n\t\t\t\tReason:   CannotMatchNameToConstraint,\n\t\t\t\tNameType: URINameType,\n\t\t\t\tName:     \"https://*.local\",\n\t\t\t},\n\t\t},\n\t\t// SUBJECT FAILURE TESTS\n\t\t{\n\t\t\tname: \"fail/subject-permitted-no-match\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithSubjectCommonNameVerification(),\n\t\t\t\tWithPermittedCommonNames(\"this name is allowed\", \"and this one too\"),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tSubject: pkix.Name{\n\t\t\t\t\tCommonName: \"some certificate name\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: false,\n\t\t\twantErr: &NamePolicyError{\n\t\t\t\tReason:   NotAllowed, // only permitted names allowed\n\t\t\t\tNameType: CNNameType,\n\t\t\t\tName:     \"some certificate name\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"fail/subject-excluded-match\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithSubjectCommonNameVerification(),\n\t\t\t\tWithExcludedCommonNames(\"this name is not allowed\"),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tSubject: pkix.Name{\n\t\t\t\t\tCommonName: \"this name is not allowed\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: false,\n\t\t\twantErr: &NamePolicyError{\n\t\t\t\tReason:   CannotParseDomain, // CN cannot be parsed as DNS in this case\n\t\t\t\tNameType: CNNameType,\n\t\t\t\tName:     \"this name is not allowed\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"fail/subject-dns-no-domain\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithSubjectCommonNameVerification(),\n\t\t\t\tWithPermittedDNSDomains(\"*.local\"),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tSubject: pkix.Name{\n\t\t\t\t\tCommonName: \"name with space.local\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: false,\n\t\t\twantErr: &NamePolicyError{\n\t\t\t\tReason:   CannotParseDomain,\n\t\t\t\tNameType: CNNameType,\n\t\t\t\tName:     \"name with space.local\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"fail/subject-dns-permitted\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithSubjectCommonNameVerification(),\n\t\t\t\tWithPermittedDNSDomains(\"*.local\"),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tSubject: pkix.Name{\n\t\t\t\t\tCommonName: \"example.notlocal\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: false,\n\t\t\twantErr: &NamePolicyError{\n\t\t\t\tReason:   NotAllowed,\n\t\t\t\tNameType: CNNameType,\n\t\t\t\tName:     \"example.notlocal\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"fail/subject-dns-excluded\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithSubjectCommonNameVerification(),\n\t\t\t\tWithExcludedDNSDomains(\"*.local\"),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tSubject: pkix.Name{\n\t\t\t\t\tCommonName: \"example.local\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: false,\n\t\t\twantErr: &NamePolicyError{\n\t\t\t\tReason:   NotAllowed,\n\t\t\t\tNameType: CNNameType,\n\t\t\t\tName:     \"example.local\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"fail/subject-ipv4-permitted\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithSubjectCommonNameVerification(),\n\t\t\t\tWithPermittedIPRanges(\n\t\t\t\t\t&net.IPNet{\n\t\t\t\t\t\tIP:   net.ParseIP(\"127.0.0.1\"),\n\t\t\t\t\t\tMask: net.IPv4Mask(255, 255, 255, 0),\n\t\t\t\t\t},\n\t\t\t\t),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tSubject: pkix.Name{\n\t\t\t\t\tCommonName: \"10.10.10.10\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: false,\n\t\t\twantErr: &NamePolicyError{\n\t\t\t\tReason:   NotAllowed,\n\t\t\t\tNameType: CNNameType,\n\t\t\t\tName:     \"10.10.10.10\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"fail/subject-ipv4-excluded\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithSubjectCommonNameVerification(),\n\t\t\t\tWithExcludedIPRanges(\n\t\t\t\t\t&net.IPNet{\n\t\t\t\t\t\tIP:   net.ParseIP(\"127.0.0.1\"),\n\t\t\t\t\t\tMask: net.IPv4Mask(255, 255, 255, 0),\n\t\t\t\t\t},\n\t\t\t\t),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tSubject: pkix.Name{\n\t\t\t\t\tCommonName: \"127.0.0.30\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: false,\n\t\t\twantErr: &NamePolicyError{\n\t\t\t\tReason:   NotAllowed,\n\t\t\t\tNameType: CNNameType,\n\t\t\t\tName:     \"127.0.0.30\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"fail/subject-ipv6-permitted\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithSubjectCommonNameVerification(),\n\t\t\t\tWithPermittedIPRanges(\n\t\t\t\t\t&net.IPNet{\n\t\t\t\t\t\tIP:   net.ParseIP(\"2001:0db8:85a3:0000:0000:8a2e:0370:7334\"),\n\t\t\t\t\t\tMask: net.CIDRMask(120, 128),\n\t\t\t\t\t},\n\t\t\t\t),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tSubject: pkix.Name{\n\t\t\t\t\tCommonName: \"2002:0db8:85a3:0000:0000:8a2e:0370:7339\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: false,\n\t\t\twantErr: &NamePolicyError{\n\t\t\t\tReason:   NotAllowed,\n\t\t\t\tNameType: CNNameType,\n\t\t\t\tName:     \"2002:db8:85a3::8a2e:370:7339\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"fail/subject-ipv6-excluded\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithSubjectCommonNameVerification(),\n\t\t\t\tWithExcludedIPRanges(\n\t\t\t\t\t&net.IPNet{\n\t\t\t\t\t\tIP:   net.ParseIP(\"2001:0db8:85a3:0000:0000:8a2e:0370:7334\"),\n\t\t\t\t\t\tMask: net.CIDRMask(120, 128),\n\t\t\t\t\t},\n\t\t\t\t),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tSubject: pkix.Name{\n\t\t\t\t\tCommonName: \"2001:0db8:85a3:0000:0000:8a2e:0370:7339\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: false,\n\t\t\twantErr: &NamePolicyError{\n\t\t\t\tReason:   NotAllowed,\n\t\t\t\tNameType: CNNameType,\n\t\t\t\tName:     \"2001:db8:85a3::8a2e:370:7339\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"fail/subject-email-permitted\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithSubjectCommonNameVerification(),\n\t\t\t\tWithPermittedEmailAddresses(\"@example.local\"),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tSubject: pkix.Name{\n\t\t\t\t\tCommonName: \"mail@smallstep.com\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: false,\n\t\t\twantErr: &NamePolicyError{\n\t\t\t\tReason:   NotAllowed,\n\t\t\t\tNameType: CNNameType,\n\t\t\t\tName:     \"mail@smallstep.com\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"fail/subject-email-excluded\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithSubjectCommonNameVerification(),\n\t\t\t\tWithExcludedEmailAddresses(\"@example.local\"),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tSubject: pkix.Name{\n\t\t\t\t\tCommonName: \"mail@example.local\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: false,\n\t\t\twantErr: &NamePolicyError{\n\t\t\t\tReason:   NotAllowed,\n\t\t\t\tNameType: CNNameType,\n\t\t\t\tName:     \"mail@example.local\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"fail/subject-uri-permitted\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithSubjectCommonNameVerification(),\n\t\t\t\tWithPermittedURIDomains(\"*.example.com\"),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tSubject: pkix.Name{\n\t\t\t\t\tCommonName: \"https://www.google.com\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: false,\n\t\t\twantErr: &NamePolicyError{\n\t\t\t\tReason:   NotAllowed,\n\t\t\t\tNameType: CNNameType,\n\t\t\t\tName:     \"https://www.google.com\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"fail/subject-uri-excluded\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithSubjectCommonNameVerification(),\n\t\t\t\tWithExcludedURIDomains(\"*.example.com\"),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tSubject: pkix.Name{\n\t\t\t\t\tCommonName: \"https://www.example.com\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: false,\n\t\t\twantErr: &NamePolicyError{\n\t\t\t\tReason:   NotAllowed,\n\t\t\t\tNameType: CNNameType,\n\t\t\t\tName:     \"https://www.example.com\",\n\t\t\t},\n\t\t},\n\t\t// DIFFERENT SAN PERMITTED FAILURE TESTS\n\t\t{\n\t\t\tname: \"fail/dns-permitted-with-ip-name\", // when only DNS is permitted, IPs are not allowed.\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithPermittedDNSDomains(\"*.local\"),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tIPAddresses: []net.IP{net.ParseIP(\"127.0.0.1\")},\n\t\t\t},\n\t\t\twant: false,\n\t\t\twantErr: &NamePolicyError{\n\t\t\t\tReason:   NotAllowed,\n\t\t\t\tNameType: IPNameType,\n\t\t\t\tName:     \"127.0.0.1\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"fail/dns-permitted-with-mail\", // when only DNS is permitted, mails are not allowed.\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithPermittedDNSDomains(\"*.local\"),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tEmailAddresses: []string{\"mail@smallstep.com\"},\n\t\t\t},\n\t\t\twant: false,\n\t\t\twantErr: &NamePolicyError{\n\t\t\t\tReason:   NotAllowed,\n\t\t\t\tNameType: EmailNameType,\n\t\t\t\tName:     \"mail@smallstep.com\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"fail/dns-permitted-with-uri\", // when only DNS is permitted, URIs are not allowed.\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithPermittedDNSDomains(\"*.local\"),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tURIs: []*url.URL{\n\t\t\t\t\t{\n\t\t\t\t\t\tScheme: \"https\",\n\t\t\t\t\t\tHost:   \"www.example.com\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: false,\n\t\t\twantErr: &NamePolicyError{\n\t\t\t\tReason:   NotAllowed,\n\t\t\t\tNameType: URINameType,\n\t\t\t\tName:     \"https://www.example.com\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"fail/ip-permitted-with-dns-name\", // when only IP is permitted, DNS names are not allowed.\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithPermittedIPRanges(\n\t\t\t\t\t&net.IPNet{\n\t\t\t\t\t\tIP:   net.ParseIP(\"127.0.0.1\"),\n\t\t\t\t\t\tMask: net.IPv4Mask(255, 255, 255, 0),\n\t\t\t\t\t},\n\t\t\t\t),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tDNSNames: []string{\"www.example.com\"},\n\t\t\t},\n\t\t\twant: false,\n\t\t\twantErr: &NamePolicyError{\n\t\t\t\tReason:   NotAllowed,\n\t\t\t\tNameType: DNSNameType,\n\t\t\t\tName:     \"www.example.com\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"fail/ip-permitted-with-mail\", // when only IP is permitted, mails are not allowed.\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithPermittedIPRanges(\n\t\t\t\t\t&net.IPNet{\n\t\t\t\t\t\tIP:   net.ParseIP(\"127.0.0.1\"),\n\t\t\t\t\t\tMask: net.IPv4Mask(255, 255, 255, 0),\n\t\t\t\t\t},\n\t\t\t\t),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tEmailAddresses: []string{\"mail@smallstep.com\"},\n\t\t\t},\n\t\t\twant: false,\n\t\t\twantErr: &NamePolicyError{\n\t\t\t\tReason:   NotAllowed,\n\t\t\t\tNameType: EmailNameType,\n\t\t\t\tName:     \"mail@smallstep.com\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"fail/ip-permitted-with-uri\", // when only IP is permitted, URIs are not allowed.\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithPermittedIPRanges(\n\t\t\t\t\t&net.IPNet{\n\t\t\t\t\t\tIP:   net.ParseIP(\"127.0.0.1\"),\n\t\t\t\t\t\tMask: net.IPv4Mask(255, 255, 255, 0),\n\t\t\t\t\t},\n\t\t\t\t),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tURIs: []*url.URL{\n\t\t\t\t\t{\n\t\t\t\t\t\tScheme: \"https\",\n\t\t\t\t\t\tHost:   \"www.example.com\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: false,\n\t\t\twantErr: &NamePolicyError{\n\t\t\t\tReason:   NotAllowed,\n\t\t\t\tNameType: URINameType,\n\t\t\t\tName:     \"https://www.example.com\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"fail/mail-permitted-with-dns-name\", // when only mail is permitted, DNS names are not allowed.\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithPermittedEmailAddresses(\"@example.com\"),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tDNSNames: []string{\"www.example.com\"},\n\t\t\t},\n\t\t\twant: false,\n\t\t\twantErr: &NamePolicyError{\n\t\t\t\tReason:   NotAllowed,\n\t\t\t\tNameType: DNSNameType,\n\t\t\t\tName:     \"www.example.com\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"fail/mail-permitted-with-ip\", // when only mail is permitted, IPs are not allowed.\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithPermittedEmailAddresses(\"@example.com\"),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tIPAddresses: []net.IP{\n\t\t\t\t\tnet.ParseIP(\"127.0.0.1\"),\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: false,\n\t\t\twantErr: &NamePolicyError{\n\t\t\t\tReason:   NotAllowed,\n\t\t\t\tNameType: IPNameType,\n\t\t\t\tName:     \"127.0.0.1\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"fail/mail-permitted-with-uri\", // when only mail is permitted, URIs are not allowed.\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithPermittedEmailAddresses(\"@example.com\"),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tURIs: []*url.URL{\n\t\t\t\t\t{\n\t\t\t\t\t\tScheme: \"https\",\n\t\t\t\t\t\tHost:   \"www.example.com\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: false,\n\t\t\twantErr: &NamePolicyError{\n\t\t\t\tReason:   NotAllowed,\n\t\t\t\tNameType: URINameType,\n\t\t\t\tName:     \"https://www.example.com\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"fail/uri-permitted-with-dns-name\", // when only URI is permitted, DNS names are not allowed.\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithPermittedURIDomains(\"*.local\"),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tDNSNames: []string{\"host.local\"},\n\t\t\t},\n\t\t\twant: false,\n\t\t\twantErr: &NamePolicyError{\n\t\t\t\tReason:   NotAllowed,\n\t\t\t\tNameType: DNSNameType,\n\t\t\t\tName:     \"host.local\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"fail/uri-permitted-with-ip-name\", // when only URI is permitted, IPs are not allowed.\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithPermittedURIDomains(\"*.local\"),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tIPAddresses: []net.IP{\n\t\t\t\t\tnet.ParseIP(\"2001:0db8:85a3:0000:0000:8a2e:0370:7334\"),\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: false,\n\t\t\twantErr: &NamePolicyError{\n\t\t\t\tReason:   NotAllowed,\n\t\t\t\tNameType: IPNameType,\n\t\t\t\tName:     \"2001:db8:85a3::8a2e:370:7334\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"fail/uri-permitted-with-ip-name\", // when only URI is permitted, mails are not allowed.\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithPermittedURIDomains(\"*.local\"),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tEmailAddresses: []string{\"mail@smallstep.com\"},\n\t\t\t},\n\t\t\twant: false,\n\t\t\twantErr: &NamePolicyError{\n\t\t\t\tReason:   NotAllowed,\n\t\t\t\tNameType: EmailNameType,\n\t\t\t\tName:     \"mail@smallstep.com\",\n\t\t\t},\n\t\t},\n\t\t// COMBINED FAILURE TESTS\n\t\t{\n\t\t\tname: \"fail/combined-simple-all-badhost.local-common-name\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithSubjectCommonNameVerification(),\n\t\t\t\tWithPermittedDNSDomains(\"*.local\"),\n\t\t\t\tWithPermittedCIDRs(\"127.0.0.1/24\"),\n\t\t\t\tWithPermittedEmailAddresses(\"@example.local\"),\n\t\t\t\tWithPermittedURIDomains(\"*.example.local\"),\n\t\t\t\tWithExcludedDNSDomains(\"badhost.local\"),\n\t\t\t\tWithExcludedCIDRs(\"127.0.0.128/25\"),\n\t\t\t\tWithExcludedEmailAddresses(\"badmail@example.local\"),\n\t\t\t\tWithExcludedURIDomains(\"badwww.example.local\"),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tSubject: pkix.Name{\n\t\t\t\t\tCommonName: \"badhost.local\",\n\t\t\t\t},\n\t\t\t\tDNSNames:       []string{\"example.local\"},\n\t\t\t\tIPAddresses:    []net.IP{net.ParseIP(\"127.0.0.40\")},\n\t\t\t\tEmailAddresses: []string{\"mail@example.local\"},\n\t\t\t\tURIs: []*url.URL{\n\t\t\t\t\t{\n\t\t\t\t\t\tScheme: \"https\",\n\t\t\t\t\t\tHost:   \"www.example.local\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: false,\n\t\t\twantErr: &NamePolicyError{\n\t\t\t\tReason:   NotAllowed,\n\t\t\t\tNameType: CNNameType,\n\t\t\t\tName:     \"badhost.local\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"fail/combined-simple-all-anotherbadhost.local-dns\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithPermittedDNSDomains(\"*.local\"),\n\t\t\t\tWithPermittedCIDRs(\"127.0.0.1/24\"),\n\t\t\t\tWithPermittedEmailAddresses(\"@example.local\"),\n\t\t\t\tWithPermittedURIDomains(\"*.example.local\"),\n\t\t\t\tWithExcludedDNSDomains(\"anotherbadhost.local\"),\n\t\t\t\tWithExcludedCIDRs(\"127.0.0.128/25\"),\n\t\t\t\tWithExcludedEmailAddresses(\"badmail@example.local\"),\n\t\t\t\tWithExcludedURIDomains(\"badwww.example.local\"),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tSubject: pkix.Name{\n\t\t\t\t\tCommonName: \"badhost.local\",\n\t\t\t\t},\n\t\t\t\tDNSNames:       []string{\"anotherbadhost.local\"},\n\t\t\t\tIPAddresses:    []net.IP{net.ParseIP(\"127.0.0.40\")},\n\t\t\t\tEmailAddresses: []string{\"mail@example.local\"},\n\t\t\t\tURIs: []*url.URL{\n\t\t\t\t\t{\n\t\t\t\t\t\tScheme: \"https\",\n\t\t\t\t\t\tHost:   \"www.example.local\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: false,\n\t\t\twantErr: &NamePolicyError{\n\t\t\t\tReason:   NotAllowed,\n\t\t\t\tNameType: DNSNameType,\n\t\t\t\tName:     \"anotherbadhost.local\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"fail/combined-simple-all-badmail@example.local\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithPermittedDNSDomains(\"*.local\"),\n\t\t\t\tWithPermittedCIDRs(\"127.0.0.1/24\"),\n\t\t\t\tWithPermittedEmailAddresses(\"@example.local\"),\n\t\t\t\tWithPermittedURIDomains(\"*.example.local\"),\n\t\t\t\tWithExcludedDNSDomains(\"badhost.local\"),\n\t\t\t\tWithExcludedCIDRs(\"127.0.0.128/25\"),\n\t\t\t\tWithExcludedEmailAddresses(\"badmail@example.local\"),\n\t\t\t\tWithExcludedURIDomains(\"badwww.example.local\"),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tSubject: pkix.Name{\n\t\t\t\t\tCommonName: \"badhost.local\",\n\t\t\t\t},\n\t\t\t\tDNSNames:       []string{\"example.local\"},\n\t\t\t\tIPAddresses:    []net.IP{net.ParseIP(\"127.0.0.40\")},\n\t\t\t\tEmailAddresses: []string{\"mail@example.local\", \"badmail@example.local\"},\n\t\t\t\tURIs: []*url.URL{\n\t\t\t\t\t{\n\t\t\t\t\t\tScheme: \"https\",\n\t\t\t\t\t\tHost:   \"www.example.local\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: false,\n\t\t\twantErr: &NamePolicyError{\n\t\t\t\tReason:   NotAllowed,\n\t\t\t\tNameType: EmailNameType,\n\t\t\t\tName:     \"badmail@example.local\",\n\t\t\t},\n\t\t},\n\t\t// NO CONSTRAINT SUCCESS TESTS\n\t\t{\n\t\t\tname:    \"ok/dns-no-constraints\",\n\t\t\toptions: []NamePolicyOption{},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tDNSNames: []string{\"www.example.com\"},\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname:    \"ok/ipv4-no-constraints\",\n\t\t\toptions: []NamePolicyOption{},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tIPAddresses: []net.IP{\n\t\t\t\t\tnet.ParseIP(\"127.0.0.1\"),\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname:    \"ok/ipv6-no-constraints\",\n\t\t\toptions: []NamePolicyOption{},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tIPAddresses: []net.IP{\n\t\t\t\t\tnet.ParseIP(\"2001:0db8:85a3:0000:0000:8a2e:0370:7334\"),\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname:    \"ok/mail-no-constraints\",\n\t\t\toptions: []NamePolicyOption{},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tEmailAddresses: []string{\"mail@smallstep.com\"},\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname:    \"ok/uri-no-constraints\",\n\t\t\toptions: []NamePolicyOption{},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tURIs: []*url.URL{\n\t\t\t\t\t{\n\t\t\t\t\t\tScheme: \"https\",\n\t\t\t\t\t\tHost:   \"www.example.com\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"ok/subject-no-constraints\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithSubjectCommonNameVerification(),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tSubject: pkix.Name{\n\t\t\t\t\tCommonName: \"www.example.com\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"ok/subject-empty-no-constraints\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithSubjectCommonNameVerification(),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tSubject: pkix.Name{\n\t\t\t\t\tCommonName: \"\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"ok/subject-permitted-match\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithSubjectCommonNameVerification(),\n\t\t\t\tWithPermittedCommonNames(\"this name is allowed\"),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tSubject: pkix.Name{\n\t\t\t\t\tCommonName: \"this name is allowed\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"ok/subject-excluded-match\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithSubjectCommonNameVerification(),\n\t\t\t\tWithExcludedCommonNames(\"this name is not allowed\"),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tSubject: pkix.Name{\n\t\t\t\t\tCommonName: \"some other name\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t// SINGLE SAN TYPE PERMITTED SUCCESS TESTS\n\t\t{\n\t\t\tname: \"ok/dns-permitted\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithPermittedDNSDomains(\"*.local\"),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tDNSNames: []string{\"example.local\"},\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"ok/dns-permitted-wildcard\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithPermittedDNSDomains(\"*.local\", \"*.x509local\"),\n\t\t\t\tWithAllowLiteralWildcardNames(),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tDNSNames: []string{\n\t\t\t\t\t\"host.local\",\n\t\t\t\t\t\"test.x509local\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"ok/dns-permitted-wildcard-literal\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithPermittedDNSDomains(\"*.local\", \"*.x509local\"),\n\t\t\t\tWithAllowLiteralWildcardNames(),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tDNSNames: []string{\n\t\t\t\t\t\"*.local\",\n\t\t\t\t\t\"*.x509local\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"ok/dns-permitted-combined\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithPermittedDNSDomains(\"*.local\", \"*.x509local\", \"host.example.com\"),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tDNSNames: []string{\n\t\t\t\t\t\"example.local\",\n\t\t\t\t\t\"example.x509local\",\n\t\t\t\t\t\"host.example.com\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"ok/dns-permitted-idna-internationalized-domain\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithPermittedDNSDomains(\"*.例.jp\"),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tDNSNames: []string{\n\t\t\t\t\t\"JP納豆.例.jp\", // Example value from https://www.w3.org/International/articles/idn-and-iri/\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"ok/ipv4-permitted\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithPermittedCIDRs(\"127.0.0.1/24\"),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tIPAddresses: []net.IP{net.ParseIP(\"127.0.0.20\")},\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"ok/ipv6-permitted\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithPermittedCIDRs(\"2001:0db8:85a3:0000:0000:8a2e:0370:7334/120\"),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tIPAddresses: []net.IP{net.ParseIP(\"2001:0db8:85a3:0000:0000:8a2e:0370:7339\")},\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"ok/mail-permitted-wildcard\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithPermittedEmailAddresses(\"@example.com\"),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tEmailAddresses: []string{\n\t\t\t\t\t\"test@example.com\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"ok/mail-permitted-plain-domain\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithPermittedEmailAddresses(\"example.com\"),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tEmailAddresses: []string{\n\t\t\t\t\t\"test@example.com\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"ok/mail-permitted-specific-mailbox\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithPermittedEmailAddresses(\"test@local.com\"),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tEmailAddresses: []string{\n\t\t\t\t\t\"test@local.com\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"ok/mail-permitted-idna-internationalized-domain\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithPermittedEmailAddresses(\"@例.jp\"),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tEmailAddresses: []string{},\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"ok/uri-permitted-domain-wildcard\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithPermittedURIDomains(\"*.local\"),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tURIs: []*url.URL{\n\t\t\t\t\t{\n\t\t\t\t\t\tScheme: \"https\",\n\t\t\t\t\t\tHost:   \"example.local\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"ok/uri-permitted-specific-uri\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithPermittedURIDomains(\"test.local\"),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tURIs: []*url.URL{\n\t\t\t\t\t{\n\t\t\t\t\t\tScheme: \"https\",\n\t\t\t\t\t\tHost:   \"test.local\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"ok/uri-permitted-with-port\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithPermittedURIDomains(\"*.example.com\"),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tURIs: []*url.URL{\n\t\t\t\t\t{\n\t\t\t\t\t\tScheme: \"https\",\n\t\t\t\t\t\tHost:   \"www.example.com:8080\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"ok/uri-permitted-idna-internationalized-domain\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithPermittedURIDomains(\"*.bücher.example.com\"),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tURIs: []*url.URL{\n\t\t\t\t\t{\n\t\t\t\t\t\tScheme: \"https\",\n\t\t\t\t\t\tHost:   \"abc.xn--bcher-kva.example.com\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"ok/uri-permitted-idna-internationalized-domain\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithPermittedURIDomains(\"bücher.example.com\"),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tURIs: []*url.URL{\n\t\t\t\t\t{\n\t\t\t\t\t\tScheme: \"https\",\n\t\t\t\t\t\tHost:   \"xn--bcher-kva.example.com\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t// SINGLE SAN TYPE EXCLUDED SUCCESS TESTS\n\t\t{\n\t\t\tname: \"ok/dns-excluded\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithExcludedDNSDomains(\"*.notlocal\"),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tDNSNames: []string{\"example.local\"},\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"ok/ipv4-excluded\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithExcludedIPRanges(\n\t\t\t\t\t&net.IPNet{\n\t\t\t\t\t\tIP:   net.ParseIP(\"127.0.0.1\"),\n\t\t\t\t\t\tMask: net.IPv4Mask(255, 255, 255, 0),\n\t\t\t\t\t},\n\t\t\t\t),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tIPAddresses: []net.IP{net.ParseIP(\"10.10.10.10\")},\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"ok/ipv6-excluded\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithExcludedCIDRs(\"2001:0db8:85a3:0000:0000:8a2e:0370:7334/120\"),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tIPAddresses: []net.IP{net.ParseIP(\"2003:0db8:85a3:0000:0000:8a2e:0370:7334\")},\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"ok/mail-excluded\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithExcludedEmailAddresses(\"@notlocal\"),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tEmailAddresses: []string{\"mail@local\"},\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"ok/mail-excluded-with-subdomain\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithExcludedEmailAddresses(\"@local\"),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tEmailAddresses: []string{\"mail@example.local\"},\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"ok/uri-excluded\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithExcludedURIDomains(\"*.google.com\"),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tURIs: []*url.URL{\n\t\t\t\t\t{\n\t\t\t\t\t\tScheme: \"https\",\n\t\t\t\t\t\tHost:   \"www.example.com\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t// SUBJECT SUCCESS TESTS\n\t\t{\n\t\t\tname: \"ok/subject-empty\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithSubjectCommonNameVerification(),\n\t\t\t\tWithPermittedDNSDomains(\"*.local\"),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tSubject: pkix.Name{\n\t\t\t\t\tCommonName: \"\",\n\t\t\t\t},\n\t\t\t\tDNSNames: []string{\"example.local\"},\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"ok/subject-dns-permitted\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithSubjectCommonNameVerification(),\n\t\t\t\tWithPermittedDNSDomains(\"*.local\"),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tSubject: pkix.Name{\n\t\t\t\t\tCommonName: \"example.local\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"ok/subject-dns-excluded\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithSubjectCommonNameVerification(),\n\t\t\t\tWithExcludedDNSDomains(\"*.notlocal\"),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tSubject: pkix.Name{\n\t\t\t\t\tCommonName: \"example.local\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"ok/subject-ipv4-permitted\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithSubjectCommonNameVerification(),\n\t\t\t\tWithPermittedIPRanges(\n\t\t\t\t\t&net.IPNet{\n\t\t\t\t\t\tIP:   net.ParseIP(\"127.0.0.1\"),\n\t\t\t\t\t\tMask: net.IPv4Mask(255, 255, 255, 0),\n\t\t\t\t\t},\n\t\t\t\t),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tSubject: pkix.Name{\n\t\t\t\t\tCommonName: \"127.0.0.20\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"ok/subject-ipv4-excluded\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithSubjectCommonNameVerification(),\n\t\t\t\tWithExcludedIPRanges(\n\t\t\t\t\t&net.IPNet{\n\t\t\t\t\t\tIP:   net.ParseIP(\"128.0.0.1\"),\n\t\t\t\t\t\tMask: net.IPv4Mask(255, 255, 255, 0),\n\t\t\t\t\t},\n\t\t\t\t),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tSubject: pkix.Name{\n\t\t\t\t\tCommonName: \"127.0.0.1\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"ok/subject-ipv6-permitted\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithSubjectCommonNameVerification(),\n\t\t\t\tWithPermittedIPRanges(\n\t\t\t\t\t&net.IPNet{\n\t\t\t\t\t\tIP:   net.ParseIP(\"2001:0db8:85a3:0000:0000:8a2e:0370:7334\"),\n\t\t\t\t\t\tMask: net.CIDRMask(120, 128),\n\t\t\t\t\t},\n\t\t\t\t),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tSubject: pkix.Name{\n\t\t\t\t\tCommonName: \"2001:0db8:85a3:0000:0000:8a2e:0370:7339\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"ok/subject-ipv6-excluded\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithSubjectCommonNameVerification(),\n\t\t\t\tWithExcludedIPRanges(\n\t\t\t\t\t&net.IPNet{\n\t\t\t\t\t\tIP:   net.ParseIP(\"2001:0db8:85a3:0000:0000:8a2e:0370:7334\"),\n\t\t\t\t\t\tMask: net.CIDRMask(120, 128),\n\t\t\t\t\t},\n\t\t\t\t),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tSubject: pkix.Name{\n\t\t\t\t\tCommonName: \"2009:0db8:85a3:0000:0000:8a2e:0370:7339\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"ok/subject-email-permitted\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithSubjectCommonNameVerification(),\n\t\t\t\tWithPermittedEmailAddresses(\"@example.local\"),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tSubject: pkix.Name{\n\t\t\t\t\tCommonName: \"mail@example.local\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"ok/subject-email-excluded\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithSubjectCommonNameVerification(),\n\t\t\t\tWithExcludedEmailAddresses(\"@example.notlocal\"),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tSubject: pkix.Name{\n\t\t\t\t\tCommonName: \"mail@example.local\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"ok/subject-uri-permitted\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithSubjectCommonNameVerification(),\n\t\t\t\tWithPermittedURIDomains(\"*.example.com\"),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tSubject: pkix.Name{\n\t\t\t\t\tCommonName: \"https://www.example.com\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"ok/subject-uri-excluded\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithSubjectCommonNameVerification(),\n\t\t\t\tWithExcludedURIDomains(\"*.smallstep.com\"),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tSubject: pkix.Name{\n\t\t\t\t\tCommonName: \"https://www.example.com\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t// DIFFERENT SAN TYPE EXCLUDED SUCCESS TESTS\n\t\t{\n\t\t\tname: \"ok/dns-excluded-with-ip-name\", // when only DNS is exluded, we allow anything else\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithExcludedDNSDomains(\"*.local\"),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tIPAddresses: []net.IP{net.ParseIP(\"127.0.0.1\")},\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"ok/dns-excluded-with-mail\", // when only DNS is exluded, we allow anything else\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithExcludedDNSDomains(\"*.local\"),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tEmailAddresses: []string{\"mail@example.com\"},\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"ok/dns-excluded-with-mail\", // when only DNS is exluded, we allow anything else\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithExcludedDNSDomains(\"*.local\"),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tURIs: []*url.URL{\n\t\t\t\t\t{\n\t\t\t\t\t\tScheme: \"https\",\n\t\t\t\t\t\tHost:   \"www.example.com\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"ok/ip-excluded-with-dns\", // when only IP is exluded, we allow anything else\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithExcludedCIDRs(\"127.0.0.1/24\"),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tDNSNames: []string{\"test.local\"},\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"ok/ip-excluded-with-mail\", // when only IP is exluded, we allow anything else\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithExcludedCIDRs(\"127.0.0.1/24\"),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tEmailAddresses: []string{\"mail@example.com\"},\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"ok/ip-excluded-with-mail\", // when only IP is exluded, we allow anything else\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithExcludedCIDRs(\"127.0.0.1/24\"),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tURIs: []*url.URL{\n\t\t\t\t\t{\n\t\t\t\t\t\tScheme: \"https\",\n\t\t\t\t\t\tHost:   \"www.example.com\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"ok/mail-excluded-with-dns\", // when only mail is exluded, we allow anything else\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithExcludedEmailAddresses(\"@example.com\"),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tDNSNames: []string{\"test.local\"},\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"ok/mail-excluded-with-ip\", // when only mail is exluded, we allow anything else\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithExcludedEmailAddresses(\"@example.com\"),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tIPAddresses: []net.IP{net.ParseIP(\"127.0.0.1\")},\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"ok/mail-excluded-with-uri\", // when only mail is exluded, we allow anything else\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithExcludedEmailAddresses(\"@example.com\"),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tURIs: []*url.URL{\n\t\t\t\t\t{\n\t\t\t\t\t\tScheme: \"https\",\n\t\t\t\t\t\tHost:   \"www.example.com\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"ok/uri-excluded-with-dns\", // when only URI is exluded, we allow anything else\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithExcludedURIDomains(\"*.example.local\"),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tDNSNames: []string{\"test.example.local\"},\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"ok/uri-excluded-with-dns\", // when only URI is exluded, we allow anything else\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithExcludedURIDomains(\"*.example.local\"),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tIPAddresses: []net.IP{net.ParseIP(\"127.0.0.1\")},\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"ok/uri-excluded-with-mail\", // when only URI is exluded, we allow anything else\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithExcludedURIDomains(\"*.example.local\"),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tEmailAddresses: []string{\"mail@example.local\"},\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"ok/dns-excluded-with-subject-ip-name\", // when only DNS is exluded, we allow anything else\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithSubjectCommonNameVerification(),\n\t\t\t\tWithExcludedDNSDomains(\"*.local\"),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tSubject: pkix.Name{\n\t\t\t\t\tCommonName: \"127.0.0.1\",\n\t\t\t\t},\n\t\t\t\tIPAddresses: []net.IP{net.ParseIP(\"127.0.0.1\")},\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t// COMBINED SUCCESS TESTS\n\t\t{\n\t\t\tname: \"ok/combined-simple-permitted\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithSubjectCommonNameVerification(),\n\t\t\t\tWithPermittedDNSDomains(\"*.local\"),\n\t\t\t\tWithPermittedCIDRs(\"127.0.0.1/24\"),\n\t\t\t\tWithPermittedEmailAddresses(\"@example.local\"),\n\t\t\t\tWithPermittedURIDomains(\"*.example.local\"),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tSubject: pkix.Name{\n\t\t\t\t\tCommonName: \"somehost.local\",\n\t\t\t\t},\n\t\t\t\tDNSNames:       []string{\"example.local\"},\n\t\t\t\tIPAddresses:    []net.IP{net.ParseIP(\"127.0.0.15\")},\n\t\t\t\tEmailAddresses: []string{\"mail@example.local\"},\n\t\t\t\tURIs: []*url.URL{\n\t\t\t\t\t{\n\t\t\t\t\t\tScheme: \"https\",\n\t\t\t\t\t\tHost:   \"www.example.local\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"ok/combined-simple-permitted-without-subject-verification\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithPermittedDNSDomains(\"*.local\"),\n\t\t\t\tWithPermittedCIDRs(\"127.0.0.1/24\"),\n\t\t\t\tWithPermittedEmailAddresses(\"@example.local\"),\n\t\t\t\tWithPermittedURIDomains(\"*.example.local\"),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tSubject: pkix.Name{\n\t\t\t\t\tCommonName: \"forbidden-but-non-verified-domain.example.com\",\n\t\t\t\t},\n\t\t\t\tDNSNames:       []string{\"example.local\"},\n\t\t\t\tIPAddresses:    []net.IP{net.ParseIP(\"127.0.0.1\")},\n\t\t\t\tEmailAddresses: []string{\"mail@example.local\"},\n\t\t\t\tURIs: []*url.URL{\n\t\t\t\t\t{\n\t\t\t\t\t\tScheme: \"https\",\n\t\t\t\t\t\tHost:   \"www.example.local\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"ok/combined-simple-all\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithSubjectCommonNameVerification(),\n\t\t\t\tWithPermittedDNSDomains(\"*.local\"),\n\t\t\t\tWithPermittedCIDRs(\"127.0.0.1/24\"),\n\t\t\t\tWithPermittedEmailAddresses(\"@example.local\"),\n\t\t\t\tWithPermittedURIDomains(\"*.example.local\"),\n\t\t\t\tWithExcludedDNSDomains(\"badhost.local\"),\n\t\t\t\tWithExcludedCIDRs(\"127.0.0.128/25\"),\n\t\t\t\tWithExcludedEmailAddresses(\"badmail@example.local\"),\n\t\t\t\tWithExcludedURIDomains(\"badwww.example.local\"),\n\t\t\t},\n\t\t\tcert: &x509.Certificate{\n\t\t\t\tSubject: pkix.Name{\n\t\t\t\t\tCommonName: \"somehost.local\",\n\t\t\t\t},\n\t\t\t\tDNSNames:       []string{\"example.local\"},\n\t\t\t\tIPAddresses:    []net.IP{net.ParseIP(\"127.0.0.1\")},\n\t\t\t\tEmailAddresses: []string{\"mail@example.local\"},\n\t\t\t\tURIs: []*url.URL{\n\t\t\t\t\t{\n\t\t\t\t\t\tScheme: \"https\",\n\t\t\t\t\t\tHost:   \"www.example.local\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tengine, err := New(tt.options...)\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.NotNil(t, engine)\n\t\t\tgotErr := engine.IsX509CertificateAllowed(tt.cert)\n\t\t\twantErr := tt.wantErr != nil\n\n\t\t\tif (gotErr != nil) != wantErr {\n\t\t\t\tt.Errorf(\"NamePolicyEngine.IsX509CertificateAllowed() error = %v, wantErr %v\", gotErr, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif gotErr != nil {\n\t\t\t\tvar npe *NamePolicyError\n\t\t\t\tassert.True(t, errors.As(gotErr, &npe))\n\t\t\t\tassert.NotEqual(t, \"\", npe.Error())\n\t\t\t\tassert.Equal(t, tt.wantErr.Reason, npe.Reason)\n\t\t\t\tassert.Equal(t, tt.wantErr.NameType, npe.NameType)\n\t\t\t\tassert.Equal(t, tt.wantErr.Name, npe.Name)\n\t\t\t\tassert.NotEqual(t, \"\", npe.Detail())\n\t\t\t\t//assert.Equals(t, tt.err.Reason, npe.Reason) // NOTE: reason detail is skipped; it's a detail\n\t\t\t}\n\n\t\t\t// Perform the same tests for a CSR, which are similar to Certificates\n\t\t\tcsr := &x509.CertificateRequest{\n\t\t\t\tSubject:        tt.cert.Subject,\n\t\t\t\tDNSNames:       tt.cert.DNSNames,\n\t\t\t\tEmailAddresses: tt.cert.EmailAddresses,\n\t\t\t\tIPAddresses:    tt.cert.IPAddresses,\n\t\t\t\tURIs:           tt.cert.URIs,\n\t\t\t}\n\t\t\tgotErr = engine.IsX509CertificateRequestAllowed(csr)\n\t\t\twantErr = tt.wantErr != nil\n\t\t\tif (gotErr != nil) != wantErr {\n\t\t\t\tt.Errorf(\"NamePolicyEngine.AreCSRNamesAllowed() error = %v, wantErr %v\", gotErr, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif gotErr != nil {\n\t\t\t\tvar npe *NamePolicyError\n\t\t\t\tassert.True(t, errors.As(gotErr, &npe))\n\t\t\t\tassert.NotEqual(t, \"\", npe.Error())\n\t\t\t\tassert.Equal(t, tt.wantErr.Reason, npe.Reason)\n\t\t\t\tassert.Equal(t, tt.wantErr.NameType, npe.NameType)\n\t\t\t\tassert.Equal(t, tt.wantErr.Name, npe.Name)\n\t\t\t\tassert.NotEqual(t, \"\", npe.Detail())\n\t\t\t\t//assert.Equals(t, tt.err.Reason, npe.Reason) // NOTE: reason detail is skipped; it's a detail\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNamePolicyEngine_SSH_ArePrincipalsAllowed(t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\toptions []NamePolicyOption\n\t\tcert    *ssh.Certificate\n\t\twant    bool\n\t\twantErr *NamePolicyError\n\t}{\n\t\t{\n\t\t\tname: \"fail/host-with-permitted-dns-domain\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithPermittedDNSDomains(\"*.local\"),\n\t\t\t},\n\t\t\tcert: &ssh.Certificate{\n\t\t\t\tCertType: ssh.HostCert,\n\t\t\t\tValidPrincipals: []string{\n\t\t\t\t\t\"host.example.com\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: false,\n\t\t\twantErr: &NamePolicyError{\n\t\t\t\tReason:   NotAllowed,\n\t\t\t\tNameType: DNSNameType,\n\t\t\t\tName:     \"host.example.com\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"fail/host-with-excluded-dns-domain\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithExcludedDNSDomains(\"*.local\"),\n\t\t\t},\n\t\t\tcert: &ssh.Certificate{\n\t\t\t\tCertType: ssh.HostCert,\n\t\t\t\tValidPrincipals: []string{\n\t\t\t\t\t\"host.local\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: false,\n\t\t\twantErr: &NamePolicyError{\n\t\t\t\tReason:   NotAllowed,\n\t\t\t\tNameType: DNSNameType,\n\t\t\t\tName:     \"host.local\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"fail/host-with-permitted-cidr\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithPermittedCIDRs(\"127.0.0.1/24\"),\n\t\t\t},\n\t\t\tcert: &ssh.Certificate{\n\t\t\t\tCertType: ssh.HostCert,\n\t\t\t\tValidPrincipals: []string{\n\t\t\t\t\t\"192.168.0.22\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: false,\n\t\t\twantErr: &NamePolicyError{\n\t\t\t\tReason:   NotAllowed,\n\t\t\t\tNameType: IPNameType,\n\t\t\t\tName:     \"192.168.0.22\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"fail/host-with-excluded-cidr\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithExcludedCIDRs(\"127.0.0.1/24\"),\n\t\t\t},\n\t\t\tcert: &ssh.Certificate{\n\t\t\t\tCertType: ssh.HostCert,\n\t\t\t\tValidPrincipals: []string{\n\t\t\t\t\t\"127.0.0.0\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: false,\n\t\t\twantErr: &NamePolicyError{\n\t\t\t\tReason:   NotAllowed,\n\t\t\t\tNameType: IPNameType,\n\t\t\t\tName:     \"127.0.0.0\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"fail/user-with-permitted-email\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithPermittedEmailAddresses(\"@example.com\"),\n\t\t\t},\n\t\t\tcert: &ssh.Certificate{\n\t\t\t\tCertType: ssh.UserCert,\n\t\t\t\tValidPrincipals: []string{\n\t\t\t\t\t\"mail@local\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: false,\n\t\t\twantErr: &NamePolicyError{\n\t\t\t\tReason:   NotAllowed,\n\t\t\t\tNameType: EmailNameType,\n\t\t\t\tName:     \"mail@local\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"fail/user-with-excluded-email\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithExcludedEmailAddresses(\"@example.com\"),\n\t\t\t},\n\t\t\tcert: &ssh.Certificate{\n\t\t\t\tCertType: ssh.UserCert,\n\t\t\t\tValidPrincipals: []string{\n\t\t\t\t\t\"mail@example.com\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: false,\n\t\t\twantErr: &NamePolicyError{\n\t\t\t\tReason:   NotAllowed,\n\t\t\t\tNameType: EmailNameType,\n\t\t\t\tName:     \"mail@example.com\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"fail/host-with-permitted-principals\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithPermittedPrincipals(\"localhost\"),\n\t\t\t},\n\t\t\tcert: &ssh.Certificate{\n\t\t\t\tCertType: ssh.HostCert,\n\t\t\t\tValidPrincipals: []string{\n\t\t\t\t\t\"host\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: false,\n\t\t\twantErr: &NamePolicyError{\n\t\t\t\tReason:   NotAllowed,\n\t\t\t\tNameType: DNSNameType,\n\t\t\t\tName:     \"host\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"fail/user-with-permitted-principals\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithPermittedPrincipals(\"user\"),\n\t\t\t},\n\t\t\tcert: &ssh.Certificate{\n\t\t\t\tCertType: ssh.UserCert,\n\t\t\t\tValidPrincipals: []string{\n\t\t\t\t\t\"root\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: false,\n\t\t\twantErr: &NamePolicyError{\n\t\t\t\tReason:   NotAllowed,\n\t\t\t\tNameType: PrincipalNameType,\n\t\t\t\tName:     \"root\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"fail/user-with-excluded-principals\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithExcludedPrincipals(\"user\"),\n\t\t\t},\n\t\t\tcert: &ssh.Certificate{\n\t\t\t\tCertType: ssh.UserCert,\n\t\t\t\tValidPrincipals: []string{\n\t\t\t\t\t\"user\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: false,\n\t\t\twantErr: &NamePolicyError{\n\t\t\t\tReason:   NotAllowed,\n\t\t\t\tNameType: PrincipalNameType,\n\t\t\t\tName:     \"user\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"fail/user-with-permitted-principal-as-mail\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithPermittedPrincipals(\"ops\"),\n\t\t\t},\n\t\t\tcert: &ssh.Certificate{\n\t\t\t\tCertType: ssh.UserCert,\n\t\t\t\tValidPrincipals: []string{\n\t\t\t\t\t\"ops@work\", // this is (currently) parsed as an email-like principal; not allowed with just \"ops\" as the permitted principal\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: false,\n\t\t\twantErr: &NamePolicyError{\n\t\t\t\tReason:   NotAllowed,\n\t\t\t\tNameType: EmailNameType,\n\t\t\t\tName:     \"ops@work\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"fail/host-principal-with-permitted-dns-domain\", // when only DNS is permitted, username principals are not allowed.\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithPermittedDNSDomains(\"*.local\"),\n\t\t\t},\n\t\t\tcert: &ssh.Certificate{\n\t\t\t\tCertType: ssh.HostCert,\n\t\t\t\tValidPrincipals: []string{\n\t\t\t\t\t\"user\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: false,\n\t\t\twantErr: &NamePolicyError{\n\t\t\t\tReason:   NotAllowed,\n\t\t\t\tNameType: DNSNameType,\n\t\t\t\tName:     \"user\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"fail/host-principal-with-permitted-ip-range\", // when only IPs are permitted, username principals are not allowed.\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithPermittedCIDRs(\"127.0.0.1/24\"),\n\t\t\t},\n\t\t\tcert: &ssh.Certificate{\n\t\t\t\tCertType: ssh.HostCert,\n\t\t\t\tValidPrincipals: []string{\n\t\t\t\t\t\"user\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: false,\n\t\t\twantErr: &NamePolicyError{\n\t\t\t\tReason:   NotAllowed,\n\t\t\t\tNameType: DNSNameType,\n\t\t\t\tName:     \"user\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"fail/user-principal-with-permitted-email\", // when only emails are permitted, username principals are not allowed.\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithPermittedEmailAddresses(\"@example.com\"),\n\t\t\t},\n\t\t\tcert: &ssh.Certificate{\n\t\t\t\tCertType: ssh.UserCert,\n\t\t\t\tValidPrincipals: []string{\n\t\t\t\t\t\"user\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: false,\n\t\t\twantErr: &NamePolicyError{\n\t\t\t\tReason:   NotAllowed,\n\t\t\t\tNameType: PrincipalNameType,\n\t\t\t\tName:     \"user\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"fail/combined-user\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithPermittedEmailAddresses(\"@smallstep.com\"),\n\t\t\t\tWithExcludedEmailAddresses(\"root@smallstep.com\"),\n\t\t\t},\n\t\t\tcert: &ssh.Certificate{\n\t\t\t\tCertType: ssh.UserCert,\n\t\t\t\tValidPrincipals: []string{\n\t\t\t\t\t\"someone@smallstep.com\",\n\t\t\t\t\t\"someone\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: false,\n\t\t\twantErr: &NamePolicyError{\n\t\t\t\tReason:   NotAllowed,\n\t\t\t\tNameType: PrincipalNameType,\n\t\t\t\tName:     \"someone\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"fail/combined-user-with-excluded-user-principal\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithPermittedEmailAddresses(\"@smallstep.com\"),\n\t\t\t\tWithExcludedPrincipals(\"root\"),\n\t\t\t},\n\t\t\tcert: &ssh.Certificate{\n\t\t\t\tCertType: ssh.UserCert,\n\t\t\t\tValidPrincipals: []string{\n\t\t\t\t\t\"someone@smallstep.com\",\n\t\t\t\t\t\"root\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: false,\n\t\t\twantErr: &NamePolicyError{\n\t\t\t\tReason:   NotAllowed,\n\t\t\t\tNameType: PrincipalNameType,\n\t\t\t\tName:     \"root\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"fail/host-with-permitted-user-principals\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithPermittedEmailAddresses(\"@work\"),\n\t\t\t},\n\t\t\tcert: &ssh.Certificate{\n\t\t\t\tCertType: ssh.HostCert,\n\t\t\t\tValidPrincipals: []string{\n\t\t\t\t\t\"example.work\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: false,\n\t\t\twantErr: &NamePolicyError{\n\t\t\t\tReason:   NotAllowed,\n\t\t\t\tNameType: DNSNameType,\n\t\t\t\tName:     \"example.work\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"fail/user-with-permitted-user-principals\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithPermittedDNSDomains(\"*.local\"),\n\t\t\t},\n\t\t\tcert: &ssh.Certificate{\n\t\t\t\tCertType: ssh.UserCert,\n\t\t\t\tValidPrincipals: []string{\n\t\t\t\t\t\"herman@work\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: false,\n\t\t\twantErr: &NamePolicyError{\n\t\t\t\tReason:   NotAllowed,\n\t\t\t\tNameType: EmailNameType,\n\t\t\t\tName:     \"herman@work\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"ok/host-with-permitted-dns-domain\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithPermittedDNSDomains(\"*.local\"),\n\t\t\t},\n\t\t\tcert: &ssh.Certificate{\n\t\t\t\tCertType: ssh.HostCert,\n\t\t\t\tValidPrincipals: []string{\n\t\t\t\t\t\"host.local\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"ok/host-with-excluded-dns-domain\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithExcludedDNSDomains(\"*.example.com\"),\n\t\t\t},\n\t\t\tcert: &ssh.Certificate{\n\t\t\t\tCertType: ssh.HostCert,\n\t\t\t\tValidPrincipals: []string{\n\t\t\t\t\t\"host.local\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"ok/host-with-permitted-ip\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithPermittedCIDRs(\"127.0.0.1/24\"),\n\t\t\t},\n\t\t\tcert: &ssh.Certificate{\n\t\t\t\tCertType: ssh.HostCert,\n\t\t\t\tValidPrincipals: []string{\n\t\t\t\t\t\"127.0.0.33\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"ok/host-with-excluded-ip\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithExcludedCIDRs(\"127.0.0.1/24\"),\n\t\t\t},\n\t\t\tcert: &ssh.Certificate{\n\t\t\t\tCertType: ssh.HostCert,\n\t\t\t\tValidPrincipals: []string{\n\t\t\t\t\t\"192.168.0.35\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"ok/host-with-excluded-principals\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithExcludedPrincipals(\"localhost\"),\n\t\t\t},\n\t\t\tcert: &ssh.Certificate{\n\t\t\t\tCertType: ssh.HostCert,\n\t\t\t\tValidPrincipals: []string{\n\t\t\t\t\t\"localhost\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"ok/user-with-permitted-email\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithPermittedEmailAddresses(\"@example.com\"),\n\t\t\t},\n\t\t\tcert: &ssh.Certificate{\n\t\t\t\tCertType: ssh.UserCert,\n\t\t\t\tValidPrincipals: []string{\n\t\t\t\t\t\"mail@example.com\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"ok/user-with-excluded-email\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithExcludedEmailAddresses(\"@example.com\"),\n\t\t\t},\n\t\t\tcert: &ssh.Certificate{\n\t\t\t\tCertType: ssh.UserCert,\n\t\t\t\tValidPrincipals: []string{\n\t\t\t\t\t\"mail@local\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"ok/user-with-permitted-principals\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithPermittedPrincipals(\"*\"),\n\t\t\t},\n\t\t\tcert: &ssh.Certificate{\n\t\t\t\tCertType: ssh.UserCert,\n\t\t\t\tValidPrincipals: []string{\n\t\t\t\t\t\"user\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"ok/user-with-excluded-principals\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithExcludedPrincipals(\"user\"),\n\t\t\t},\n\t\t\tcert: &ssh.Certificate{\n\t\t\t\tCertType: ssh.UserCert,\n\t\t\t\tValidPrincipals: []string{\n\t\t\t\t\t\"root\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"ok/combined-user\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithPermittedEmailAddresses(\"@smallstep.com\"),\n\t\t\t\tWithPermittedPrincipals(\"*\"), // without specifying the wildcard, \"someone\" would not be allowed.\n\t\t\t\tWithExcludedEmailAddresses(\"root@smallstep.com\"),\n\t\t\t},\n\t\t\tcert: &ssh.Certificate{\n\t\t\t\tCertType: ssh.UserCert,\n\t\t\t\tValidPrincipals: []string{\n\t\t\t\t\t\"someone@smallstep.com\",\n\t\t\t\t\t\"someone\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"ok/combined-user-with-excluded-user-principal\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithPermittedEmailAddresses(\"@smallstep.com\"),\n\t\t\t\tWithExcludedEmailAddresses(\"root@smallstep.com\"),\n\t\t\t\tWithExcludedPrincipals(\"root\"), // unlike the previous test, this implicitly allows any other username principal\n\t\t\t},\n\t\t\tcert: &ssh.Certificate{\n\t\t\t\tCertType: ssh.UserCert,\n\t\t\t\tValidPrincipals: []string{\n\t\t\t\t\t\"someone@smallstep.com\",\n\t\t\t\t\t\"someone\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"ok/combined-host\",\n\t\t\toptions: []NamePolicyOption{\n\t\t\t\tWithPermittedDNSDomains(\"*.local\"),\n\t\t\t\tWithPermittedCIDRs(\"127.0.0.1/24\"),\n\t\t\t\tWithExcludedDNSDomains(\"badhost.local\"),\n\t\t\t\tWithExcludedCIDRs(\"127.0.0.128/25\"),\n\t\t\t},\n\t\t\tcert: &ssh.Certificate{\n\t\t\t\tCertType: ssh.HostCert,\n\t\t\t\tValidPrincipals: []string{\n\t\t\t\t\t\"example.local\",\n\t\t\t\t\t\"127.0.0.31\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tengine, err := New(tt.options...)\n\t\t\tassert.NoError(t, err)\n\t\t\tgotErr := engine.IsSSHCertificateAllowed(tt.cert)\n\t\t\twantErr := tt.wantErr != nil\n\t\t\tif (gotErr != nil) != wantErr {\n\t\t\t\tt.Errorf(\"NamePolicyEngine.IsSSHCertificateAllowed() error = %v, wantErr %v\", gotErr, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif gotErr != nil {\n\t\t\t\tvar npe *NamePolicyError\n\t\t\t\tassert.True(t, errors.As(gotErr, &npe))\n\t\t\t\tassert.NotEqual(t, \"\", npe.Error())\n\t\t\t\tassert.Equal(t, tt.wantErr.Reason, npe.Reason)\n\t\t\t\tassert.Equal(t, tt.wantErr.NameType, npe.NameType)\n\t\t\t\tassert.Equal(t, tt.wantErr.Name, npe.Name)\n\t\t\t\tassert.NotEqual(t, \"\", npe.Detail())\n\t\t\t\t//assert.Equals(t, tt.err.Reason, npe.Reason) // NOTE: reason detail is skipped; it's a detail\n\t\t\t}\n\t\t})\n\t}\n}\n\ntype result struct {\n\twantDNSNames  []string\n\twantIps       []net.IP\n\twantEmails    []string\n\twantUsernames []string\n}\n\nfunc emptyResult() result {\n\treturn result{\n\t\twantDNSNames:  []string{},\n\t\twantIps:       []net.IP{},\n\t\twantEmails:    []string{},\n\t\twantUsernames: []string{},\n\t}\n}\n\nfunc Test_splitSSHPrincipals(t *testing.T) {\n\ttype test struct {\n\t\tcert    *ssh.Certificate\n\t\tr       result\n\t\twantErr bool\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/unexpected-cert-type\": func(t *testing.T) test {\n\t\t\tr := emptyResult()\n\t\t\treturn test{\n\t\t\t\tcert: &ssh.Certificate{\n\t\t\t\t\tCertType: uint32(0),\n\t\t\t\t},\n\t\t\t\tr:       r,\n\t\t\t\twantErr: true,\n\t\t\t}\n\t\t},\n\t\t\"fail/user-ip\": func(t *testing.T) test {\n\t\t\tr := emptyResult()\n\t\t\tr.wantIps = []net.IP{net.ParseIP(\"127.0.0.1\")}\n\t\t\treturn test{\n\t\t\t\tcert: &ssh.Certificate{\n\t\t\t\t\tCertType:        ssh.UserCert,\n\t\t\t\t\tValidPrincipals: []string{\"127.0.0.1\"},\n\t\t\t\t},\n\t\t\t\tr:       r,\n\t\t\t\twantErr: true,\n\t\t\t}\n\t\t},\n\t\t\"fail/user-uri\": func(t *testing.T) test {\n\t\t\tr := emptyResult()\n\t\t\treturn test{\n\t\t\t\tcert: &ssh.Certificate{\n\t\t\t\t\tCertType:        ssh.UserCert,\n\t\t\t\t\tValidPrincipals: []string{\"https://host.local/\"},\n\t\t\t\t},\n\t\t\t\tr:       r,\n\t\t\t\twantErr: true,\n\t\t\t}\n\t\t},\n\t\t\"fail/host-uri\": func(t *testing.T) test {\n\t\t\tr := emptyResult()\n\t\t\treturn test{\n\t\t\t\tcert: &ssh.Certificate{\n\t\t\t\t\tCertType:        ssh.HostCert,\n\t\t\t\t\tValidPrincipals: []string{\"https://host.local/\"},\n\t\t\t\t},\n\t\t\t\tr:       r,\n\t\t\t\twantErr: true,\n\t\t\t}\n\t\t},\n\t\t\"ok/host-dns\": func(t *testing.T) test {\n\t\t\tr := emptyResult()\n\t\t\tr.wantDNSNames = []string{\"host.example.com\"}\n\t\t\treturn test{\n\t\t\t\tcert: &ssh.Certificate{\n\t\t\t\t\tCertType:        ssh.HostCert,\n\t\t\t\t\tValidPrincipals: []string{\"host.example.com\"},\n\t\t\t\t},\n\t\t\t\tr:       r,\n\t\t\t\twantErr: false,\n\t\t\t}\n\t\t},\n\t\t\"ok/host-ip\": func(t *testing.T) test {\n\t\t\tr := emptyResult()\n\t\t\tr.wantIps = []net.IP{net.ParseIP(\"127.0.0.1\")}\n\t\t\treturn test{\n\t\t\t\tcert: &ssh.Certificate{\n\t\t\t\t\tCertType:        ssh.HostCert,\n\t\t\t\t\tValidPrincipals: []string{\"127.0.0.1\"},\n\t\t\t\t},\n\t\t\t\tr:       r,\n\t\t\t\twantErr: false,\n\t\t\t}\n\t\t},\n\t\t\"ok/host-email\": func(t *testing.T) test {\n\t\t\tr := emptyResult()\n\t\t\tr.wantEmails = []string{\"ops@work\"}\n\t\t\treturn test{\n\t\t\t\tcert: &ssh.Certificate{\n\t\t\t\t\tCertType:        ssh.HostCert,\n\t\t\t\t\tValidPrincipals: []string{\"ops@work\"},\n\t\t\t\t},\n\t\t\t\tr:       r,\n\t\t\t\twantErr: false,\n\t\t\t}\n\t\t},\n\t\t\"ok/user-localhost\": func(t *testing.T) test {\n\t\t\tr := emptyResult()\n\t\t\tr.wantUsernames = []string{\"localhost\"} // when type is User cert, this is considered a username; not a DNS\n\t\t\treturn test{\n\t\t\t\tcert: &ssh.Certificate{\n\t\t\t\t\tCertType:        ssh.UserCert,\n\t\t\t\t\tValidPrincipals: []string{\"localhost\"},\n\t\t\t\t},\n\t\t\t\tr:       r,\n\t\t\t\twantErr: false,\n\t\t\t}\n\t\t},\n\t\t\"ok/user-username-with-period\": func(t *testing.T) test {\n\t\t\tr := emptyResult()\n\t\t\tr.wantUsernames = []string{\"x.joe\"}\n\t\t\treturn test{\n\t\t\t\tcert: &ssh.Certificate{\n\t\t\t\t\tCertType:        ssh.UserCert,\n\t\t\t\t\tValidPrincipals: []string{\"x.joe\"},\n\t\t\t\t},\n\t\t\t\tr:       r,\n\t\t\t\twantErr: false,\n\t\t\t}\n\t\t},\n\t\t\"ok/user-maillike\": func(t *testing.T) test {\n\t\t\tr := emptyResult()\n\t\t\tr.wantEmails = []string{\"ops@work\"}\n\t\t\treturn test{\n\t\t\t\tcert: &ssh.Certificate{\n\t\t\t\t\tCertType:        ssh.UserCert,\n\t\t\t\t\tValidPrincipals: []string{\"ops@work\"},\n\t\t\t\t},\n\t\t\t\tr:       r,\n\t\t\t\twantErr: false,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, prep := range tests {\n\t\ttt := prep(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tgotDNSNames, gotIps, gotEmails, gotUsernames, err := splitSSHPrincipals(tt.cert)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"splitSSHPrincipals() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !cmp.Equal(tt.r.wantDNSNames, gotDNSNames) {\n\t\t\t\tt.Errorf(\"splitSSHPrincipals() DNS names diff =\\n%s\", cmp.Diff(tt.r.wantDNSNames, gotDNSNames))\n\t\t\t}\n\t\t\tif !cmp.Equal(tt.r.wantIps, gotIps) {\n\t\t\t\tt.Errorf(\"splitSSHPrincipals() IPs diff =\\n%s\", cmp.Diff(tt.r.wantIps, gotIps))\n\t\t\t}\n\t\t\tif !cmp.Equal(tt.r.wantEmails, gotEmails) {\n\t\t\t\tt.Errorf(\"splitSSHPrincipals() Emails diff =\\n%s\", cmp.Diff(tt.r.wantEmails, gotEmails))\n\t\t\t}\n\t\t\tif !cmp.Equal(tt.r.wantUsernames, gotUsernames) {\n\t\t\t\tt.Errorf(\"splitSSHPrincipals() Usernames diff =\\n%s\", cmp.Diff(tt.r.wantUsernames, gotUsernames))\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_removeDuplicates(t *testing.T) {\n\ttests := []struct {\n\t\tname  string\n\t\tinput []string\n\t\twant  []string\n\t}{\n\t\t{\n\t\t\tname:  \"empty-slice\",\n\t\t\tinput: []string{},\n\t\t\twant:  []string{},\n\t\t},\n\t\t{\n\t\t\tname:  \"single-item\",\n\t\t\tinput: []string{\"x\"},\n\t\t\twant:  []string{\"x\"},\n\t\t},\n\t\t{\n\t\t\tname:  \"ok\",\n\t\t\tinput: []string{\"x\", \"y\", \"x\", \"z\", \"x\", \"z\", \"y\"},\n\t\t\twant:  []string{\"x\", \"y\", \"z\"},\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif got := removeDuplicates(tt.input); !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"removeDuplicates() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_removeDuplicateIPNets(t *testing.T) {\n\ttests := []struct {\n\t\tname  string\n\t\tinput []*net.IPNet\n\t\twant  []*net.IPNet\n\t}{\n\t\t{\n\t\t\tname:  \"empty-slice\",\n\t\t\tinput: []*net.IPNet{},\n\t\t\twant:  []*net.IPNet{},\n\t\t},\n\t\t{\n\t\t\tname: \"single-item\",\n\t\t\tinput: []*net.IPNet{\n\t\t\t\t{\n\t\t\t\t\tIP:   net.ParseIP(\"127.0.0.1\"),\n\t\t\t\t\tMask: net.IPv4Mask(255, 255, 255, 255),\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: []*net.IPNet{\n\t\t\t\t{\n\t\t\t\t\tIP:   net.ParseIP(\"127.0.0.1\"),\n\t\t\t\t\tMask: net.IPv4Mask(255, 255, 255, 255),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"multiple\",\n\t\t\tinput: []*net.IPNet{\n\t\t\t\t{\n\t\t\t\t\tIP:   net.ParseIP(\"127.0.0.1\"),\n\t\t\t\t\tMask: net.IPv4Mask(255, 255, 255, 255),\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tIP:   net.ParseIP(\"192.168.0.1\"),\n\t\t\t\t\tMask: net.IPv4Mask(255, 255, 255, 0),\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tIP:   net.ParseIP(\"127.0.0.1\"),\n\t\t\t\t\tMask: net.IPv4Mask(255, 255, 255, 255),\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tIP:   net.ParseIP(\"10.10.0.0\"),\n\t\t\t\t\tMask: net.IPv4Mask(255, 255, 0, 0),\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tIP:   net.ParseIP(\"192.168.0.1\"),\n\t\t\t\t\tMask: net.IPv4Mask(255, 255, 255, 0),\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tIP:   net.ParseIP(\"127.0.0.1\"),\n\t\t\t\t\tMask: net.IPv4Mask(255, 255, 255, 255),\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tIP:   net.ParseIP(\"192.168.0.1\"),\n\t\t\t\t\tMask: net.IPv4Mask(255, 255, 255, 0),\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: []*net.IPNet{\n\t\t\t\t{\n\t\t\t\t\tIP:   net.ParseIP(\"127.0.0.1\"),\n\t\t\t\t\tMask: net.IPv4Mask(255, 255, 255, 255),\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tIP:   net.ParseIP(\"192.168.0.1\"),\n\t\t\t\t\tMask: net.IPv4Mask(255, 255, 255, 0),\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tIP:   net.ParseIP(\"10.10.0.0\"),\n\t\t\t\t\tMask: net.IPv4Mask(255, 255, 0, 0),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif gotRet := removeDuplicateIPNets(tt.input); !reflect.DeepEqual(gotRet, tt.want) {\n\t\t\t\tt.Errorf(\"removeDuplicateIPNets() = %v, want %v\", gotRet, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNamePolicyError_Error(t *testing.T) {\n\ttype fields struct {\n\t\tReason   NamePolicyReason\n\t\tNameType NameType\n\t\tName     string\n\t\tdetail   string\n\t}\n\ttests := []struct {\n\t\tname   string\n\t\tfields fields\n\t\twant   string\n\t}{\n\t\t{\n\t\t\tname: \"dns-not-allowed\",\n\t\t\tfields: fields{\n\t\t\t\tReason:   NotAllowed,\n\t\t\t\tNameType: DNSNameType,\n\t\t\t\tName:     \"www.example.com\",\n\t\t\t},\n\t\t\twant: \"dns name \\\"www.example.com\\\" not allowed\",\n\t\t},\n\t\t{\n\t\t\tname: \"dns-cannot-parse-domain\",\n\t\t\tfields: fields{\n\t\t\t\tReason:   CannotParseDomain,\n\t\t\t\tNameType: DNSNameType,\n\t\t\t\tName:     \"www.example.com\",\n\t\t\t},\n\t\t\twant: \"cannot parse dns domain \\\"www.example.com\\\"\",\n\t\t},\n\t\t{\n\t\t\tname: \"email-cannot-parse\",\n\t\t\tfields: fields{\n\t\t\t\tReason:   CannotParseRFC822Name,\n\t\t\t\tNameType: EmailNameType,\n\t\t\t\tName:     \"mail@example.com\",\n\t\t\t},\n\t\t\twant: \"cannot parse email rfc822Name \\\"mail@example.com\\\"\",\n\t\t},\n\t\t{\n\t\t\tname: \"uri-cannot-match\",\n\t\t\tfields: fields{\n\t\t\t\tReason:   CannotMatchNameToConstraint,\n\t\t\t\tNameType: URINameType,\n\t\t\t\tName:     \"https://*.local\",\n\t\t\t},\n\t\t\twant: \"error matching uri name \\\"https://*.local\\\" to constraint\",\n\t\t},\n\t\t{\n\t\t\tname: \"unknown\",\n\t\t\tfields: fields{\n\t\t\t\tReason:   -1,\n\t\t\t\tNameType: DNSNameType,\n\t\t\t\tName:     \"some name\",\n\t\t\t\tdetail:   \"detail string\",\n\t\t\t},\n\t\t\twant: \"unknown error reason (-1): detail string\",\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\te := &NamePolicyError{\n\t\t\t\tReason:   tt.fields.Reason,\n\t\t\t\tNameType: tt.fields.NameType,\n\t\t\t\tName:     tt.fields.Name,\n\t\t\t\tdetail:   tt.fields.detail,\n\t\t\t}\n\t\t\tif got := e.Error(); got != tt.want {\n\t\t\t\tt.Errorf(\"NamePolicyError.Error() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "policy/options.go",
    "content": "package policy\n\nimport (\n\t\"fmt\"\n\t\"net\"\n\t\"strings\"\n\n\t\"golang.org/x/net/idna\"\n)\n\ntype NamePolicyOption func(e *NamePolicyEngine) error\n\n// TODO: wrap (more) errors; and prove a set of known (exported) errors\n\nfunc WithSubjectCommonNameVerification() NamePolicyOption {\n\treturn func(e *NamePolicyEngine) error {\n\t\te.verifySubjectCommonName = true\n\t\treturn nil\n\t}\n}\n\nfunc WithAllowLiteralWildcardNames() NamePolicyOption {\n\treturn func(e *NamePolicyEngine) error {\n\t\te.allowLiteralWildcardNames = true\n\t\treturn nil\n\t}\n}\n\nfunc WithPermittedCommonNames(commonNames ...string) NamePolicyOption {\n\treturn func(g *NamePolicyEngine) error {\n\t\tnormalizedCommonNames := make([]string, len(commonNames))\n\t\tfor i, commonName := range commonNames {\n\t\t\tnormalizedCommonName, err := normalizeAndValidateCommonName(commonName)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"cannot parse permitted common name constraint %q: %w\", commonName, err)\n\t\t\t}\n\t\t\tnormalizedCommonNames[i] = normalizedCommonName\n\t\t}\n\t\tg.permittedCommonNames = normalizedCommonNames\n\t\treturn nil\n\t}\n}\n\nfunc WithExcludedCommonNames(commonNames ...string) NamePolicyOption {\n\treturn func(g *NamePolicyEngine) error {\n\t\tnormalizedCommonNames := make([]string, len(commonNames))\n\t\tfor i, commonName := range commonNames {\n\t\t\tnormalizedCommonName, err := normalizeAndValidateCommonName(commonName)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"cannot parse excluded common name constraint %q: %w\", commonName, err)\n\t\t\t}\n\t\t\tnormalizedCommonNames[i] = normalizedCommonName\n\t\t}\n\t\tg.excludedCommonNames = normalizedCommonNames\n\t\treturn nil\n\t}\n}\n\nfunc WithPermittedDNSDomains(domains ...string) NamePolicyOption {\n\treturn func(e *NamePolicyEngine) error {\n\t\tnormalizedDomains := make([]string, len(domains))\n\t\tfor i, domain := range domains {\n\t\t\tnormalizedDomain, err := normalizeAndValidateDNSDomainConstraint(domain)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"cannot parse permitted domain constraint %q: %w\", domain, err)\n\t\t\t}\n\t\t\tnormalizedDomains[i] = normalizedDomain\n\t\t}\n\t\te.permittedDNSDomains = normalizedDomains\n\t\treturn nil\n\t}\n}\n\nfunc WithExcludedDNSDomains(domains ...string) NamePolicyOption {\n\treturn func(e *NamePolicyEngine) error {\n\t\tnormalizedDomains := make([]string, len(domains))\n\t\tfor i, domain := range domains {\n\t\t\tnormalizedDomain, err := normalizeAndValidateDNSDomainConstraint(domain)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"cannot parse excluded domain constraint %q: %w\", domain, err)\n\t\t\t}\n\t\t\tnormalizedDomains[i] = normalizedDomain\n\t\t}\n\t\te.excludedDNSDomains = normalizedDomains\n\t\treturn nil\n\t}\n}\n\nfunc WithPermittedIPRanges(ipRanges ...*net.IPNet) NamePolicyOption {\n\treturn func(e *NamePolicyEngine) error {\n\t\te.permittedIPRanges = ipRanges\n\t\treturn nil\n\t}\n}\n\nfunc WithPermittedCIDRs(cidrs ...string) NamePolicyOption {\n\treturn func(e *NamePolicyEngine) error {\n\t\tnetworks := make([]*net.IPNet, len(cidrs))\n\t\tfor i, cidr := range cidrs {\n\t\t\t_, nw, err := net.ParseCIDR(cidr)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"cannot parse permitted CIDR constraint %q\", cidr)\n\t\t\t}\n\t\t\tnetworks[i] = nw\n\t\t}\n\t\te.permittedIPRanges = networks\n\t\treturn nil\n\t}\n}\n\nfunc WithExcludedCIDRs(cidrs ...string) NamePolicyOption {\n\treturn func(e *NamePolicyEngine) error {\n\t\tnetworks := make([]*net.IPNet, len(cidrs))\n\t\tfor i, cidr := range cidrs {\n\t\t\t_, nw, err := net.ParseCIDR(cidr)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"cannot parse excluded CIDR constraint %q\", cidr)\n\t\t\t}\n\t\t\tnetworks[i] = nw\n\t\t}\n\t\te.excludedIPRanges = networks\n\t\treturn nil\n\t}\n}\n\nfunc WithPermittedIPsOrCIDRs(ipsOrCIDRs ...string) NamePolicyOption {\n\treturn func(e *NamePolicyEngine) error {\n\t\tnetworks := make([]*net.IPNet, len(ipsOrCIDRs))\n\t\tfor i, ipOrCIDR := range ipsOrCIDRs {\n\t\t\t_, nw, err := net.ParseCIDR(ipOrCIDR)\n\t\t\tif err == nil {\n\t\t\t\tnetworks[i] = nw\n\t\t\t} else if ip := net.ParseIP(ipOrCIDR); ip != nil {\n\t\t\t\tnetworks[i] = networkFor(ip)\n\t\t\t} else {\n\t\t\t\treturn fmt.Errorf(\"cannot parse permitted constraint %q as IP nor CIDR\", ipOrCIDR)\n\t\t\t}\n\t\t}\n\t\te.permittedIPRanges = networks\n\t\treturn nil\n\t}\n}\n\nfunc WithExcludedIPsOrCIDRs(ipsOrCIDRs ...string) NamePolicyOption {\n\treturn func(e *NamePolicyEngine) error {\n\t\tnetworks := make([]*net.IPNet, len(ipsOrCIDRs))\n\t\tfor i, ipOrCIDR := range ipsOrCIDRs {\n\t\t\t_, nw, err := net.ParseCIDR(ipOrCIDR)\n\t\t\tif err == nil {\n\t\t\t\tnetworks[i] = nw\n\t\t\t} else if ip := net.ParseIP(ipOrCIDR); ip != nil {\n\t\t\t\tnetworks[i] = networkFor(ip)\n\t\t\t} else {\n\t\t\t\treturn fmt.Errorf(\"cannot parse excluded constraint %q as IP nor CIDR\", ipOrCIDR)\n\t\t\t}\n\t\t}\n\t\te.excludedIPRanges = networks\n\t\treturn nil\n\t}\n}\n\nfunc WithExcludedIPRanges(ipRanges ...*net.IPNet) NamePolicyOption {\n\treturn func(e *NamePolicyEngine) error {\n\t\te.excludedIPRanges = ipRanges\n\t\treturn nil\n\t}\n}\n\nfunc WithPermittedEmailAddresses(emailAddresses ...string) NamePolicyOption {\n\treturn func(e *NamePolicyEngine) error {\n\t\tnormalizedEmailAddresses := make([]string, len(emailAddresses))\n\t\tfor i, email := range emailAddresses {\n\t\t\tnormalizedEmailAddress, err := normalizeAndValidateEmailConstraint(email)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"cannot parse permitted email constraint %q: %w\", email, err)\n\t\t\t}\n\t\t\tnormalizedEmailAddresses[i] = normalizedEmailAddress\n\t\t}\n\t\te.permittedEmailAddresses = normalizedEmailAddresses\n\t\treturn nil\n\t}\n}\n\nfunc WithExcludedEmailAddresses(emailAddresses ...string) NamePolicyOption {\n\treturn func(e *NamePolicyEngine) error {\n\t\tnormalizedEmailAddresses := make([]string, len(emailAddresses))\n\t\tfor i, email := range emailAddresses {\n\t\t\tnormalizedEmailAddress, err := normalizeAndValidateEmailConstraint(email)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"cannot parse excluded email constraint %q: %w\", email, err)\n\t\t\t}\n\t\t\tnormalizedEmailAddresses[i] = normalizedEmailAddress\n\t\t}\n\t\te.excludedEmailAddresses = normalizedEmailAddresses\n\t\treturn nil\n\t}\n}\n\nfunc WithPermittedURIDomains(uriDomains ...string) NamePolicyOption {\n\treturn func(e *NamePolicyEngine) error {\n\t\tnormalizedURIDomains := make([]string, len(uriDomains))\n\t\tfor i, domain := range uriDomains {\n\t\t\tnormalizedURIDomain, err := normalizeAndValidateURIDomainConstraint(domain)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"cannot parse permitted URI domain constraint %q: %w\", domain, err)\n\t\t\t}\n\t\t\tnormalizedURIDomains[i] = normalizedURIDomain\n\t\t}\n\t\te.permittedURIDomains = normalizedURIDomains\n\t\treturn nil\n\t}\n}\n\nfunc WithExcludedURIDomains(domains ...string) NamePolicyOption {\n\treturn func(e *NamePolicyEngine) error {\n\t\tnormalizedURIDomains := make([]string, len(domains))\n\t\tfor i, domain := range domains {\n\t\t\tnormalizedURIDomain, err := normalizeAndValidateURIDomainConstraint(domain)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"cannot parse excluded URI domain constraint %q: %w\", domain, err)\n\t\t\t}\n\t\t\tnormalizedURIDomains[i] = normalizedURIDomain\n\t\t}\n\t\te.excludedURIDomains = normalizedURIDomains\n\t\treturn nil\n\t}\n}\n\nfunc WithPermittedPrincipals(principals ...string) NamePolicyOption {\n\treturn func(g *NamePolicyEngine) error {\n\t\tg.permittedPrincipals = principals\n\t\treturn nil\n\t}\n}\n\nfunc WithExcludedPrincipals(principals ...string) NamePolicyOption {\n\treturn func(g *NamePolicyEngine) error {\n\t\tg.excludedPrincipals = principals\n\t\treturn nil\n\t}\n}\n\nfunc networkFor(ip net.IP) *net.IPNet {\n\tvar mask net.IPMask\n\tif !isIPv4(ip) {\n\t\tmask = net.CIDRMask(128, 128)\n\t} else {\n\t\tmask = net.CIDRMask(32, 32)\n\t}\n\tnw := &net.IPNet{\n\t\tIP:   ip,\n\t\tMask: mask,\n\t}\n\treturn nw\n}\n\nfunc isIPv4(ip net.IP) bool {\n\treturn ip.To4() != nil\n}\n\nfunc normalizeAndValidateCommonName(constraint string) (string, error) {\n\tnormalizedConstraint := strings.ToLower(strings.TrimSpace(constraint))\n\tif normalizedConstraint == \"\" {\n\t\treturn \"\", fmt.Errorf(\"contraint %q can not be empty or white space string\", constraint)\n\t}\n\tif normalizedConstraint == \"*\" {\n\t\treturn \"\", fmt.Errorf(\"wildcard constraint %q is not supported\", constraint)\n\t}\n\treturn normalizedConstraint, nil\n}\n\nfunc normalizeAndValidateDNSDomainConstraint(constraint string) (string, error) {\n\tnormalizedConstraint := strings.ToLower(strings.TrimSpace(constraint))\n\tif normalizedConstraint == \"\" {\n\t\treturn \"\", fmt.Errorf(\"contraint %q can not be empty or white space string\", constraint)\n\t}\n\tif strings.Contains(normalizedConstraint, \"..\") {\n\t\treturn \"\", fmt.Errorf(\"domain constraint %q cannot have empty labels\", constraint)\n\t}\n\tif strings.HasPrefix(normalizedConstraint, \".\") {\n\t\treturn \"\", fmt.Errorf(\"domain constraint %q with wildcard should start with *\", constraint)\n\t}\n\tif strings.LastIndex(normalizedConstraint, \"*\") > 0 {\n\t\treturn \"\", fmt.Errorf(\"domain constraint %q can only have wildcard as starting character\", constraint)\n\t}\n\tif len(normalizedConstraint) >= 2 && normalizedConstraint[0] == '*' && normalizedConstraint[1] != '.' {\n\t\treturn \"\", fmt.Errorf(\"wildcard character in domain constraint %q can only be used to match (full) labels\", constraint)\n\t}\n\tif strings.HasPrefix(normalizedConstraint, \"*.\") {\n\t\tnormalizedConstraint = normalizedConstraint[1:] // cut off wildcard character; keep the period\n\t}\n\tnormalizedConstraint, err := idna.Lookup.ToASCII(normalizedConstraint)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"domain constraint %q can not be converted to ASCII: %w\", constraint, err)\n\t}\n\tif _, ok := domainToReverseLabels(normalizedConstraint); !ok {\n\t\treturn \"\", fmt.Errorf(\"cannot parse domain constraint %q\", constraint)\n\t}\n\treturn normalizedConstraint, nil\n}\n\nfunc normalizeAndValidateEmailConstraint(constraint string) (string, error) {\n\tnormalizedConstraint := strings.ToLower(strings.TrimSpace(constraint))\n\tif normalizedConstraint == \"\" {\n\t\treturn \"\", fmt.Errorf(\"email contraint %q can not be empty or white space string\", constraint)\n\t}\n\tif strings.Contains(normalizedConstraint, \"*\") {\n\t\treturn \"\", fmt.Errorf(\"email constraint %q cannot contain asterisk wildcard\", constraint)\n\t}\n\tif strings.Count(normalizedConstraint, \"@\") > 1 {\n\t\treturn \"\", fmt.Errorf(\"email constraint %q contains too many @ characters\", constraint)\n\t}\n\tif normalizedConstraint[0] == '@' {\n\t\tnormalizedConstraint = normalizedConstraint[1:] // remove the leading @ as wildcard for emails\n\t}\n\tif normalizedConstraint[0] == '.' {\n\t\treturn \"\", fmt.Errorf(\"email constraint %q cannot start with period\", constraint)\n\t}\n\tif strings.Contains(normalizedConstraint, \"@\") {\n\t\tmailbox, ok := parseRFC2821Mailbox(normalizedConstraint)\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"cannot parse email constraint %q as RFC 2821 mailbox\", constraint)\n\t\t}\n\t\t// According to RFC 5280, section 7.5, emails are considered to match if the local part is\n\t\t// an exact match and the host (domain) part matches the ASCII representation (case-insensitive):\n\t\t// https://datatracker.ietf.org/doc/html/rfc5280#section-7.5\n\t\tdomainASCII, err := idna.Lookup.ToASCII(mailbox.domain)\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"email constraint %q domain part %q cannot be converted to ASCII: %w\", constraint, mailbox.domain, err)\n\t\t}\n\t\tnormalizedConstraint = mailbox.local + \"@\" + domainASCII\n\t} else {\n\t\tvar err error\n\t\tnormalizedConstraint, err = idna.Lookup.ToASCII(normalizedConstraint)\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"email constraint %q cannot be converted to ASCII: %w\", constraint, err)\n\t\t}\n\t}\n\tif _, ok := domainToReverseLabels(normalizedConstraint); !ok {\n\t\treturn \"\", fmt.Errorf(\"cannot parse email domain constraint %q\", constraint)\n\t}\n\treturn normalizedConstraint, nil\n}\n\nfunc normalizeAndValidateURIDomainConstraint(constraint string) (string, error) {\n\tnormalizedConstraint := strings.ToLower(strings.TrimSpace(constraint))\n\tif normalizedConstraint == \"\" {\n\t\treturn \"\", fmt.Errorf(\"URI domain contraint %q cannot be empty or white space string\", constraint)\n\t}\n\tif strings.Contains(normalizedConstraint, \"://\") {\n\t\treturn \"\", fmt.Errorf(\"URI domain constraint %q contains scheme (not supported yet)\", constraint)\n\t}\n\tif strings.Contains(normalizedConstraint, \"..\") {\n\t\treturn \"\", fmt.Errorf(\"URI domain constraint %q cannot have empty labels\", constraint)\n\t}\n\tif strings.HasPrefix(normalizedConstraint, \".\") {\n\t\treturn \"\", fmt.Errorf(\"URI domain constraint %q with wildcard should start with *\", constraint)\n\t}\n\tif strings.LastIndex(normalizedConstraint, \"*\") > 0 {\n\t\treturn \"\", fmt.Errorf(\"URI domain constraint %q can only have wildcard as starting character\", constraint)\n\t}\n\tif strings.HasPrefix(normalizedConstraint, \"*.\") {\n\t\tnormalizedConstraint = normalizedConstraint[1:] // cut off wildcard character; keep the period\n\t}\n\t// we're being strict with square brackets in domains; we don't allow them, no matter what\n\tif strings.Contains(normalizedConstraint, \"[\") || strings.Contains(normalizedConstraint, \"]\") {\n\t\treturn \"\", fmt.Errorf(\"URI domain constraint %q contains invalid square brackets\", constraint)\n\t}\n\tif _, _, err := net.SplitHostPort(normalizedConstraint); err == nil {\n\t\t// a successful split (likely) with host and port; we don't currently allow ports in the config\n\t\treturn \"\", fmt.Errorf(\"URI domain constraint %q cannot contain port\", constraint)\n\t}\n\t// check if the host part of the URI domain constraint is an IP\n\tif net.ParseIP(normalizedConstraint) != nil {\n\t\treturn \"\", fmt.Errorf(\"URI domain constraint %q cannot be an IP\", constraint)\n\t}\n\tnormalizedConstraint, err := idna.Lookup.ToASCII(normalizedConstraint)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"URI domain constraint %q cannot be converted to ASCII: %w\", constraint, err)\n\t}\n\t_, ok := domainToReverseLabels(normalizedConstraint)\n\tif !ok {\n\t\treturn \"\", fmt.Errorf(\"cannot parse URI domain constraint %q\", constraint)\n\t}\n\treturn normalizedConstraint, nil\n}\n"
  },
  {
    "path": "policy/options_test.go",
    "content": "package policy\n\nimport (\n\t\"net\"\n\t\"testing\"\n\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc Test_normalizeAndValidateCommonName(t *testing.T) {\n\ttests := []struct {\n\t\tname       string\n\t\tconstraint string\n\t\twant       string\n\t\twantErr    bool\n\t}{\n\t\t{\n\t\t\tname:       \"fail/empty-constraint\",\n\t\t\tconstraint: \"\",\n\t\t\twant:       \"\",\n\t\t\twantErr:    true,\n\t\t},\n\t\t{\n\t\t\tname:       \"fail/wildcard\",\n\t\t\tconstraint: \"*\",\n\t\t\twant:       \"\",\n\t\t\twantErr:    true,\n\t\t},\n\t\t{\n\t\t\tname:       \"ok\",\n\t\t\tconstraint: \"step\",\n\t\t\twant:       \"step\",\n\t\t\twantErr:    false,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := normalizeAndValidateCommonName(tt.constraint)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"normalizeAndValidateCommonName() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif got != tt.want {\n\t\t\t\tt.Errorf(\"normalizeAndValidateCommonName() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_normalizeAndValidateDNSDomainConstraint(t *testing.T) {\n\ttests := []struct {\n\t\tname       string\n\t\tconstraint string\n\t\twant       string\n\t\twantErr    bool\n\t}{\n\t\t{\n\t\t\tname:       \"fail/empty-constraint\",\n\t\t\tconstraint: \"\",\n\t\t\twant:       \"\",\n\t\t\twantErr:    true,\n\t\t},\n\t\t{\n\t\t\tname:       \"fail/wildcard-partial-label\",\n\t\t\tconstraint: \"*xxxx.local\",\n\t\t\twant:       \"\",\n\t\t\twantErr:    true,\n\t\t},\n\t\t{\n\t\t\tname:       \"fail/wildcard-in-the-middle\",\n\t\t\tconstraint: \"x.*.local\",\n\t\t\twant:       \"\",\n\t\t\twantErr:    true,\n\t\t},\n\t\t{\n\t\t\tname:       \"fail/empty-label\",\n\t\t\tconstraint: \"..local\",\n\t\t\twant:       \"\",\n\t\t\twantErr:    true,\n\t\t},\n\t\t{\n\t\t\tname:       \"fail/empty-reverse\",\n\t\t\tconstraint: \".\",\n\t\t\twant:       \"\",\n\t\t\twantErr:    true,\n\t\t},\n\t\t{\n\t\t\tname:       \"fail/no-asterisk\",\n\t\t\tconstraint: \".example.com\",\n\t\t\twant:       \"\",\n\t\t\twantErr:    true,\n\t\t},\n\t\t{\n\t\t\tname:       \"fail/idna-internationalized-domain-name-lookup\",\n\t\t\tconstraint: `\\00.local`, // invalid IDNA ASCII character\n\t\t\twant:       \"\",\n\t\t\twantErr:    true,\n\t\t},\n\t\t{\n\t\t\tname:       \"ok/wildcard\",\n\t\t\tconstraint: \"*.local\",\n\t\t\twant:       \".local\",\n\t\t\twantErr:    false,\n\t\t},\n\t\t{\n\t\t\tname:       \"ok/specific-domain\",\n\t\t\tconstraint: \"example.local\",\n\t\t\twant:       \"example.local\",\n\t\t\twantErr:    false,\n\t\t},\n\t\t{\n\t\t\tname:       \"ok/idna-internationalized-domain-name-punycode\",\n\t\t\tconstraint: \"*.xn--fsq.jp\", // Example value from https://www.w3.org/International/articles/idn-and-iri/\n\t\t\twant:       \".xn--fsq.jp\",\n\t\t\twantErr:    false,\n\t\t},\n\t\t{\n\t\t\tname:       \"ok/idna-internationalized-domain-name-lookup-transformed\",\n\t\t\tconstraint: \"*.例.jp\", // Example value from https://www.w3.org/International/articles/idn-and-iri/\n\t\t\twant:       \".xn--fsq.jp\",\n\t\t\twantErr:    false,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := normalizeAndValidateDNSDomainConstraint(tt.constraint)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"normalizeAndValidateDNSDomainConstraint() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\t\t\tif got != tt.want {\n\t\t\t\tt.Errorf(\"normalizeAndValidateDNSDomainConstraint() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_normalizeAndValidateEmailConstraint(t *testing.T) {\n\ttests := []struct {\n\t\tname       string\n\t\tconstraint string\n\t\twant       string\n\t\twantErr    bool\n\t}{\n\t\t{\n\t\t\tname:       \"fail/empty-constraint\",\n\t\t\tconstraint: \"\",\n\t\t\twant:       \"\",\n\t\t\twantErr:    true,\n\t\t},\n\t\t{\n\t\t\tname:       \"fail/asterisk\",\n\t\t\tconstraint: \"*.local\",\n\t\t\twant:       \"\",\n\t\t\twantErr:    true,\n\t\t},\n\t\t{\n\t\t\tname:       \"fail/period\",\n\t\t\tconstraint: \".local\",\n\t\t\twant:       \"\",\n\t\t\twantErr:    true,\n\t\t},\n\t\t{\n\t\t\tname:       \"fail/@period\",\n\t\t\tconstraint: \"@.local\",\n\t\t\twant:       \"\",\n\t\t\twantErr:    true,\n\t\t},\n\t\t{\n\t\t\tname:       \"fail/too-many-@s\",\n\t\t\tconstraint: \"@local@example.com\",\n\t\t\twant:       \"\",\n\t\t\twantErr:    true,\n\t\t},\n\t\t{\n\t\t\tname:       \"fail/parse-mailbox\",\n\t\t\tconstraint: \"mail@example.com\" + string(byte(0)),\n\t\t\twant:       \"\",\n\t\t\twantErr:    true,\n\t\t},\n\t\t{\n\t\t\tname:       \"fail/idna-internationalized-domain\",\n\t\t\tconstraint: \"mail@xn--bla.local\",\n\t\t\twant:       \"\",\n\t\t\twantErr:    true,\n\t\t},\n\t\t{\n\t\t\tname:       \"fail/idna-internationalized-domain-name-lookup\",\n\t\t\tconstraint: `\\00local`,\n\t\t\twant:       \"\",\n\t\t\twantErr:    true,\n\t\t},\n\t\t{\n\t\t\tname:       \"fail/parse-domain\",\n\t\t\tconstraint: \"x..example.com\",\n\t\t\twant:       \"\",\n\t\t\twantErr:    true,\n\t\t},\n\t\t{\n\t\t\tname:       \"ok/wildcard\",\n\t\t\tconstraint: \"@local\",\n\t\t\twant:       \"local\",\n\t\t\twantErr:    false,\n\t\t},\n\t\t{\n\t\t\tname:       \"ok/specific-mail\",\n\t\t\tconstraint: \"mail@local\",\n\t\t\twant:       \"mail@local\",\n\t\t\twantErr:    false,\n\t\t},\n\t\t// TODO(hs): fix the below; doesn't get past parseRFC2821Mailbox; I think it should be allowed.\n\t\t// {\n\t\t// \tname:       \"ok/idna-internationalized-local\",\n\t\t// \tconstraint: `bücher@local`,\n\t\t// \twant:       \"bücher@local\",\n\t\t// \twantErr:    false,\n\t\t// },\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := normalizeAndValidateEmailConstraint(tt.constraint)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"normalizeAndValidateEmailConstraint() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\t\t\tif got != tt.want {\n\t\t\t\tt.Errorf(\"normalizeAndValidateEmailConstraint() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNew(t *testing.T) {\n\ttype test struct {\n\t\toptions []NamePolicyOption\n\t\twant    *NamePolicyEngine\n\t\twantErr bool\n\t}\n\tvar tests = map[string]func(t *testing.T) test{\n\t\t\"fail/with-permitted-common-name\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\toptions: []NamePolicyOption{\n\t\t\t\t\tWithPermittedCommonNames(\"*\"),\n\t\t\t\t},\n\t\t\t\twant:    nil,\n\t\t\t\twantErr: true,\n\t\t\t}\n\t\t},\n\t\t\"fail/with-excluded-common-name\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\toptions: []NamePolicyOption{\n\t\t\t\t\tWithExcludedCommonNames(\"\"),\n\t\t\t\t},\n\t\t\t\twant:    nil,\n\t\t\t\twantErr: true,\n\t\t\t}\n\t\t},\n\t\t\"fail/with-permitted-dns-domains\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\toptions: []NamePolicyOption{\n\t\t\t\t\tWithPermittedDNSDomains(\"**.local\"),\n\t\t\t\t},\n\t\t\t\twant:    nil,\n\t\t\t\twantErr: true,\n\t\t\t}\n\t\t},\n\t\t\"fail/with-excluded-dns-domains\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\toptions: []NamePolicyOption{\n\t\t\t\t\tWithExcludedDNSDomains(\"**.local\"),\n\t\t\t\t},\n\t\t\t\twant:    nil,\n\t\t\t\twantErr: true,\n\t\t\t}\n\t\t},\n\t\t\"fail/with-permitted-cidrs\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\toptions: []NamePolicyOption{\n\t\t\t\t\tWithPermittedCIDRs(\"127.0.0.1//24\"),\n\t\t\t\t},\n\t\t\t\twant:    nil,\n\t\t\t\twantErr: true,\n\t\t\t}\n\t\t},\n\t\t\"fail/with-excluded-cidrs\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\toptions: []NamePolicyOption{\n\t\t\t\t\tWithExcludedCIDRs(\"127.0.0.1//24\"),\n\t\t\t\t},\n\t\t\t\twant:    nil,\n\t\t\t\twantErr: true,\n\t\t\t}\n\t\t},\n\t\t\"fail/with-permitted-ipsOrCIDRs-cidr\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\toptions: []NamePolicyOption{\n\t\t\t\t\tWithPermittedIPsOrCIDRs(\"127.0.0.1//24\"),\n\t\t\t\t},\n\t\t\t\twant:    nil,\n\t\t\t\twantErr: true,\n\t\t\t}\n\t\t},\n\t\t\"fail/with-permitted-ipsOrCIDRs-ip\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\toptions: []NamePolicyOption{\n\t\t\t\t\tWithPermittedIPsOrCIDRs(\"127.0.0:1\"),\n\t\t\t\t},\n\t\t\t\twant:    nil,\n\t\t\t\twantErr: true,\n\t\t\t}\n\t\t},\n\t\t\"fail/with-excluded-ipsOrCIDRs-cidr\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\toptions: []NamePolicyOption{\n\t\t\t\t\tWithExcludedIPsOrCIDRs(\"127.0.0.1//24\"),\n\t\t\t\t},\n\t\t\t\twant:    nil,\n\t\t\t\twantErr: true,\n\t\t\t}\n\t\t},\n\t\t\"fail/with-excluded-ipsOrCIDRs-ip\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\toptions: []NamePolicyOption{\n\t\t\t\t\tWithExcludedIPsOrCIDRs(\"127.0.0:1\"),\n\t\t\t\t},\n\t\t\t\twant:    nil,\n\t\t\t\twantErr: true,\n\t\t\t}\n\t\t},\n\t\t\"fail/with-permitted-emails\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\toptions: []NamePolicyOption{\n\t\t\t\t\tWithPermittedEmailAddresses(\"*.local\"),\n\t\t\t\t},\n\t\t\t\twant:    nil,\n\t\t\t\twantErr: true,\n\t\t\t}\n\t\t},\n\t\t\"fail/with-excluded-emails\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\toptions: []NamePolicyOption{\n\t\t\t\t\tWithExcludedEmailAddresses(\"*.local\"),\n\t\t\t\t},\n\t\t\t\twant:    nil,\n\t\t\t\twantErr: true,\n\t\t\t}\n\t\t},\n\t\t\"fail/with-permitted-uris\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\toptions: []NamePolicyOption{\n\t\t\t\t\tWithPermittedURIDomains(\"**.local\"),\n\t\t\t\t},\n\t\t\t\twant:    nil,\n\t\t\t\twantErr: true,\n\t\t\t}\n\t\t},\n\t\t\"fail/with-excluded-uris\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\toptions: []NamePolicyOption{\n\t\t\t\t\tWithExcludedURIDomains(\"**.local\"),\n\t\t\t\t},\n\t\t\t\twant:    nil,\n\t\t\t\twantErr: true,\n\t\t\t}\n\t\t},\n\t\t\"ok/default\": func(t *testing.T) test {\n\t\t\treturn test{\n\t\t\t\toptions: []NamePolicyOption{},\n\t\t\t\twant:    &NamePolicyEngine{},\n\t\t\t\twantErr: false,\n\t\t\t}\n\t\t},\n\t\t\"ok/subject-verification\": func(t *testing.T) test {\n\t\t\toptions := []NamePolicyOption{\n\t\t\t\tWithSubjectCommonNameVerification(),\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\toptions: options,\n\t\t\t\twant: &NamePolicyEngine{\n\t\t\t\t\tverifySubjectCommonName: true,\n\t\t\t\t},\n\t\t\t\twantErr: false,\n\t\t\t}\n\t\t},\n\t\t\"ok/literal-wildcards\": func(t *testing.T) test {\n\t\t\toptions := []NamePolicyOption{\n\t\t\t\tWithAllowLiteralWildcardNames(),\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\toptions: options,\n\t\t\t\twant: &NamePolicyEngine{\n\t\t\t\t\tallowLiteralWildcardNames: true,\n\t\t\t\t},\n\t\t\t\twantErr: false,\n\t\t\t}\n\t\t},\n\t\t\"ok/with-permitted-dns-wildcard-domains\": func(t *testing.T) test {\n\t\t\toptions := []NamePolicyOption{\n\t\t\t\tWithPermittedDNSDomains(\"*.local\", \"*.example.com\"),\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\toptions: options,\n\t\t\t\twant: &NamePolicyEngine{\n\t\t\t\t\tpermittedDNSDomains:               []string{\".local\", \".example.com\"},\n\t\t\t\t\tnumberOfDNSDomainConstraints:      2,\n\t\t\t\t\ttotalNumberOfPermittedConstraints: 2,\n\t\t\t\t\ttotalNumberOfConstraints:          2,\n\t\t\t\t},\n\t\t\t\twantErr: false,\n\t\t\t}\n\t\t},\n\t\t\"ok/with-excluded-dns-domains\": func(t *testing.T) test {\n\t\t\toptions := []NamePolicyOption{\n\t\t\t\tWithExcludedDNSDomains(\"*.local\", \"*.example.com\"),\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\toptions: options,\n\t\t\t\twant: &NamePolicyEngine{\n\t\t\t\t\texcludedDNSDomains:               []string{\".local\", \".example.com\"},\n\t\t\t\t\tnumberOfDNSDomainConstraints:     2,\n\t\t\t\t\ttotalNumberOfExcludedConstraints: 2,\n\t\t\t\t\ttotalNumberOfConstraints:         2,\n\t\t\t\t},\n\t\t\t\twantErr: false,\n\t\t\t}\n\t\t},\n\t\t\"ok/with-permitted-ip-ranges\": func(t *testing.T) test {\n\t\t\t_, nw1, err := net.ParseCIDR(\"127.0.0.1/24\")\n\t\t\tassert.NoError(t, err)\n\t\t\t_, nw2, err := net.ParseCIDR(\"192.168.0.1/24\")\n\t\t\tassert.NoError(t, err)\n\t\t\toptions := []NamePolicyOption{\n\t\t\t\tWithPermittedIPRanges(nw1, nw2),\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\toptions: options,\n\t\t\t\twant: &NamePolicyEngine{\n\t\t\t\t\tpermittedIPRanges: []*net.IPNet{\n\t\t\t\t\t\tnw1, nw2,\n\t\t\t\t\t},\n\t\t\t\t\tnumberOfIPRangeConstraints:        2,\n\t\t\t\t\ttotalNumberOfPermittedConstraints: 2,\n\t\t\t\t\ttotalNumberOfConstraints:          2,\n\t\t\t\t},\n\t\t\t\twantErr: false,\n\t\t\t}\n\t\t},\n\t\t\"ok/with-excluded-ip-ranges\": func(t *testing.T) test {\n\t\t\t_, nw1, err := net.ParseCIDR(\"127.0.0.1/24\")\n\t\t\tassert.NoError(t, err)\n\t\t\t_, nw2, err := net.ParseCIDR(\"192.168.0.1/24\")\n\t\t\tassert.NoError(t, err)\n\t\t\toptions := []NamePolicyOption{\n\t\t\t\tWithExcludedIPRanges(nw1, nw2),\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\toptions: options,\n\t\t\t\twant: &NamePolicyEngine{\n\t\t\t\t\texcludedIPRanges: []*net.IPNet{\n\t\t\t\t\t\tnw1, nw2,\n\t\t\t\t\t},\n\t\t\t\t\tnumberOfIPRangeConstraints:       2,\n\t\t\t\t\ttotalNumberOfExcludedConstraints: 2,\n\t\t\t\t\ttotalNumberOfConstraints:         2,\n\t\t\t\t},\n\t\t\t\twantErr: false,\n\t\t\t}\n\t\t},\n\t\t\"ok/with-permitted-cidrs\": func(t *testing.T) test {\n\t\t\t_, nw1, err := net.ParseCIDR(\"127.0.0.1/24\")\n\t\t\tassert.NoError(t, err)\n\t\t\t_, nw2, err := net.ParseCIDR(\"192.168.0.1/24\")\n\t\t\tassert.NoError(t, err)\n\t\t\toptions := []NamePolicyOption{\n\t\t\t\tWithPermittedCIDRs(\"127.0.0.1/24\", \"192.168.0.1/24\"),\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\toptions: options,\n\t\t\t\twant: &NamePolicyEngine{\n\t\t\t\t\tpermittedIPRanges: []*net.IPNet{\n\t\t\t\t\t\tnw1, nw2,\n\t\t\t\t\t},\n\t\t\t\t\tnumberOfIPRangeConstraints:        2,\n\t\t\t\t\ttotalNumberOfPermittedConstraints: 2,\n\t\t\t\t\ttotalNumberOfConstraints:          2,\n\t\t\t\t},\n\t\t\t\twantErr: false,\n\t\t\t}\n\t\t},\n\t\t\"ok/with-excluded-cidrs\": func(t *testing.T) test {\n\t\t\t_, nw1, err := net.ParseCIDR(\"127.0.0.1/24\")\n\t\t\tassert.NoError(t, err)\n\t\t\t_, nw2, err := net.ParseCIDR(\"192.168.0.1/24\")\n\t\t\tassert.NoError(t, err)\n\t\t\toptions := []NamePolicyOption{\n\t\t\t\tWithExcludedCIDRs(\"127.0.0.1/24\", \"192.168.0.1/24\"),\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\toptions: options,\n\t\t\t\twant: &NamePolicyEngine{\n\t\t\t\t\texcludedIPRanges: []*net.IPNet{\n\t\t\t\t\t\tnw1, nw2,\n\t\t\t\t\t},\n\t\t\t\t\tnumberOfIPRangeConstraints:       2,\n\t\t\t\t\ttotalNumberOfExcludedConstraints: 2,\n\t\t\t\t\ttotalNumberOfConstraints:         2,\n\t\t\t\t},\n\t\t\t\twantErr: false,\n\t\t\t}\n\t\t},\n\t\t\"ok/with-permitted-ipsOrCIDRs-cidr\": func(t *testing.T) test {\n\t\t\t_, nw1, err := net.ParseCIDR(\"127.0.0.1/24\")\n\t\t\tassert.NoError(t, err)\n\t\t\t_, nw2, err := net.ParseCIDR(\"192.168.0.31/32\")\n\t\t\tassert.NoError(t, err)\n\t\t\t_, nw3, err := net.ParseCIDR(\"2001:0db8:85a3:0000:0000:8a2e:0370:7334/128\")\n\t\t\tassert.NoError(t, err)\n\t\t\toptions := []NamePolicyOption{\n\t\t\t\tWithPermittedIPsOrCIDRs(\"127.0.0.1/24\", \"192.168.0.31\", \"2001:0db8:85a3:0000:0000:8a2e:0370:7334\"),\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\toptions: options,\n\t\t\t\twant: &NamePolicyEngine{\n\t\t\t\t\tpermittedIPRanges: []*net.IPNet{\n\t\t\t\t\t\tnw1, nw2, nw3,\n\t\t\t\t\t},\n\t\t\t\t\tnumberOfIPRangeConstraints:        3,\n\t\t\t\t\ttotalNumberOfPermittedConstraints: 3,\n\t\t\t\t\ttotalNumberOfConstraints:          3,\n\t\t\t\t},\n\t\t\t\twantErr: false,\n\t\t\t}\n\t\t},\n\t\t\"ok/with-excluded-ipsOrCIDRs-cidr\": func(t *testing.T) test {\n\t\t\t_, nw1, err := net.ParseCIDR(\"127.0.0.1/24\")\n\t\t\tassert.NoError(t, err)\n\t\t\t_, nw2, err := net.ParseCIDR(\"192.168.0.31/32\")\n\t\t\tassert.NoError(t, err)\n\t\t\t_, nw3, err := net.ParseCIDR(\"2001:0db8:85a3:0000:0000:8a2e:0370:7334/128\")\n\t\t\tassert.NoError(t, err)\n\t\t\toptions := []NamePolicyOption{\n\t\t\t\tWithExcludedIPsOrCIDRs(\"127.0.0.1/24\", \"192.168.0.31\", \"2001:0db8:85a3:0000:0000:8a2e:0370:7334\"),\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\toptions: options,\n\t\t\t\twant: &NamePolicyEngine{\n\t\t\t\t\texcludedIPRanges: []*net.IPNet{\n\t\t\t\t\t\tnw1, nw2, nw3,\n\t\t\t\t\t},\n\t\t\t\t\tnumberOfIPRangeConstraints:       3,\n\t\t\t\t\ttotalNumberOfExcludedConstraints: 3,\n\t\t\t\t\ttotalNumberOfConstraints:         3,\n\t\t\t\t},\n\t\t\t\twantErr: false,\n\t\t\t}\n\t\t},\n\t\t\"ok/with-permitted-emails\": func(t *testing.T) test {\n\t\t\toptions := []NamePolicyOption{\n\t\t\t\tWithPermittedEmailAddresses(\"mail@local\", \"@example.com\"),\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\toptions: options,\n\t\t\t\twant: &NamePolicyEngine{\n\t\t\t\t\tpermittedEmailAddresses:           []string{\"mail@local\", \"example.com\"},\n\t\t\t\t\tnumberOfEmailAddressConstraints:   2,\n\t\t\t\t\ttotalNumberOfPermittedConstraints: 2,\n\t\t\t\t\ttotalNumberOfConstraints:          2,\n\t\t\t\t},\n\t\t\t\twantErr: false,\n\t\t\t}\n\t\t},\n\t\t\"ok/with-excluded-emails\": func(t *testing.T) test {\n\t\t\toptions := []NamePolicyOption{\n\t\t\t\tWithExcludedEmailAddresses(\"mail@local\", \"@example.com\"),\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\toptions: options,\n\t\t\t\twant: &NamePolicyEngine{\n\t\t\t\t\texcludedEmailAddresses:           []string{\"mail@local\", \"example.com\"},\n\t\t\t\t\tnumberOfEmailAddressConstraints:  2,\n\t\t\t\t\ttotalNumberOfExcludedConstraints: 2,\n\t\t\t\t\ttotalNumberOfConstraints:         2,\n\t\t\t\t},\n\t\t\t\twantErr: false,\n\t\t\t}\n\t\t},\n\t\t\"ok/with-permitted-uris\": func(t *testing.T) test {\n\t\t\toptions := []NamePolicyOption{\n\t\t\t\tWithPermittedURIDomains(\"host.local\", \"*.example.com\"),\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\toptions: options,\n\t\t\t\twant: &NamePolicyEngine{\n\t\t\t\t\tpermittedURIDomains:               []string{\"host.local\", \".example.com\"},\n\t\t\t\t\tnumberOfURIDomainConstraints:      2,\n\t\t\t\t\ttotalNumberOfPermittedConstraints: 2,\n\t\t\t\t\ttotalNumberOfConstraints:          2,\n\t\t\t\t},\n\t\t\t\twantErr: false,\n\t\t\t}\n\t\t},\n\t\t\"ok/with-excluded-uris\": func(t *testing.T) test {\n\t\t\toptions := []NamePolicyOption{\n\t\t\t\tWithExcludedURIDomains(\"host.local\", \"*.example.com\"),\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\toptions: options,\n\t\t\t\twant: &NamePolicyEngine{\n\t\t\t\t\texcludedURIDomains:               []string{\"host.local\", \".example.com\"},\n\t\t\t\t\tnumberOfURIDomainConstraints:     2,\n\t\t\t\t\ttotalNumberOfExcludedConstraints: 2,\n\t\t\t\t\ttotalNumberOfConstraints:         2,\n\t\t\t\t},\n\t\t\t\twantErr: false,\n\t\t\t}\n\t\t},\n\t\t\"ok/with-permitted-principals\": func(t *testing.T) test {\n\t\t\toptions := []NamePolicyOption{\n\t\t\t\tWithPermittedPrincipals(\"root\", \"ops\"),\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\toptions: options,\n\t\t\t\twant: &NamePolicyEngine{\n\t\t\t\t\tpermittedPrincipals:               []string{\"root\", \"ops\"},\n\t\t\t\t\tnumberOfPrincipalConstraints:      2,\n\t\t\t\t\ttotalNumberOfPermittedConstraints: 2,\n\t\t\t\t\ttotalNumberOfConstraints:          2,\n\t\t\t\t},\n\t\t\t\twantErr: false,\n\t\t\t}\n\t\t},\n\t\t\"ok/with-excluded-principals\": func(t *testing.T) test {\n\t\t\toptions := []NamePolicyOption{\n\t\t\t\tWithExcludedPrincipals(\"root\", \"ops\"),\n\t\t\t}\n\t\t\treturn test{\n\t\t\t\toptions: options,\n\t\t\t\twant: &NamePolicyEngine{\n\t\t\t\t\texcludedPrincipals:               []string{\"root\", \"ops\"},\n\t\t\t\t\tnumberOfPrincipalConstraints:     2,\n\t\t\t\t\ttotalNumberOfExcludedConstraints: 2,\n\t\t\t\t\ttotalNumberOfConstraints:         2,\n\t\t\t\t},\n\t\t\t\twantErr: false,\n\t\t\t}\n\t\t},\n\t}\n\tfor name, prep := range tests {\n\t\ttc := prep(t)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tgot, err := New(tc.options...)\n\t\t\tif (err != nil) != tc.wantErr {\n\t\t\t\tt.Errorf(\"New() error = %v, wantErr %v\", err, tc.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !cmp.Equal(tc.want, got, cmp.AllowUnexported(NamePolicyEngine{})) {\n\t\t\t\tt.Errorf(\"New() diff =\\n %s\", cmp.Diff(tc.want, got, cmp.AllowUnexported(NamePolicyEngine{})))\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_normalizeAndValidateURIDomainConstraint(t *testing.T) {\n\ttests := []struct {\n\t\tname       string\n\t\tconstraint string\n\t\twant       string\n\t\twantErr    bool\n\t}{\n\t\t{\n\t\t\tname:       \"fail/empty-constraint\",\n\t\t\tconstraint: \"\",\n\t\t\twant:       \"\",\n\t\t\twantErr:    true,\n\t\t},\n\t\t{\n\t\t\tname:       \"fail/scheme-https\",\n\t\t\tconstraint: `https://*.local`,\n\t\t\twant:       \"\",\n\t\t\twantErr:    true,\n\t\t},\n\t\t{\n\t\t\tname:       \"fail/too-many-asterisks\",\n\t\t\tconstraint: \"**.local\",\n\t\t\twant:       \"\",\n\t\t\twantErr:    true,\n\t\t},\n\t\t{\n\t\t\tname:       \"fail/empty-label\",\n\t\t\tconstraint: \"..local\",\n\t\t\twant:       \"\",\n\t\t\twantErr:    true,\n\t\t},\n\t\t{\n\t\t\tname:       \"fail/empty-reverse\",\n\t\t\tconstraint: \".\",\n\t\t\twant:       \"\",\n\t\t\twantErr:    true,\n\t\t},\n\t\t{\n\t\t\tname:       \"fail/domain-with-port\",\n\t\t\tconstraint: \"host.local:8443\",\n\t\t\twant:       \"\",\n\t\t\twantErr:    true,\n\t\t},\n\t\t{\n\t\t\tname:       \"fail/no-asterisk\",\n\t\t\tconstraint: \".example.com\",\n\t\t\twant:       \"\",\n\t\t\twantErr:    true,\n\t\t},\n\t\t{\n\t\t\tname:       \"fail/ipv4\",\n\t\t\tconstraint: \"127.0.0.1\",\n\t\t\twant:       \"\",\n\t\t\twantErr:    true,\n\t\t},\n\t\t{\n\t\t\tname:       \"fail/ipv6-brackets\",\n\t\t\tconstraint: \"[::1]\",\n\t\t\twant:       \"\",\n\t\t\twantErr:    true,\n\t\t},\n\t\t{\n\t\t\tname:       \"fail/ipv6-no-brackets\",\n\t\t\tconstraint: \"::1\",\n\t\t\twant:       \"\",\n\t\t\twantErr:    true,\n\t\t},\n\t\t{\n\t\t\tname:       \"fail/ipv6-no-brackets\",\n\t\t\tconstraint: \"[::1\",\n\t\t\twant:       \"\",\n\t\t\twantErr:    true,\n\t\t},\n\t\t{\n\t\t\tname:       \"fail/idna-internationalized-domain-name-lookup\",\n\t\t\tconstraint: `\\00local`,\n\t\t\twant:       \"\",\n\t\t\twantErr:    true,\n\t\t},\n\t\t{\n\t\t\tname:       \"ok/wildcard\",\n\t\t\tconstraint: \"*.local\",\n\t\t\twant:       \".local\",\n\t\t\twantErr:    false,\n\t\t},\n\t\t{\n\t\t\tname:       \"ok/specific-domain\",\n\t\t\tconstraint: \"example.local\",\n\t\t\twant:       \"example.local\",\n\t\t\twantErr:    false,\n\t\t},\n\t\t{\n\t\t\tname:       \"ok/idna-internationalized-domain-name-lookup\",\n\t\t\tconstraint: `*.bücher.example.com`,\n\t\t\twant:       \".xn--bcher-kva.example.com\",\n\t\t\twantErr:    false,\n\t\t},\n\t\t{\n\t\t\t// IDNA2003 vs. 2008 deviation: https://unicode.org/reports/tr46/#Deviations results\n\t\t\t// in a difference between Go 1.18 and lower versions. Go 1.18 expects \".xn--fa-hia.de\"; not .fass.de.\n\t\t\tname:       \"ok/idna-internationalized-domain-name-lookup-deviation\",\n\t\t\tconstraint: `*.faß.de`,\n\t\t\twant:       \".xn--fa-hia.de\",\n\t\t\twantErr:    false,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := normalizeAndValidateURIDomainConstraint(tt.constraint)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"normalizeAndValidateURIDomainConstraint() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\t\t\tif got != tt.want {\n\t\t\t\tt.Errorf(\"normalizeAndValidateURIDomainConstraint() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "policy/ssh.go",
    "content": "package policy\n\nimport (\n\t\"golang.org/x/crypto/ssh\"\n)\n\ntype SSHNamePolicyEngine interface {\n\tIsSSHCertificateAllowed(cert *ssh.Certificate) error\n}\n"
  },
  {
    "path": "policy/validate.go",
    "content": "// Copyright 2011 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n//\n// The code in this file is an adapted version of the code in\n// https://cs.opensource.google/go/go/+/refs/tags/go1.17.5:src/crypto/x509/verify.go\npackage policy\n\nimport (\n\t\"bytes\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/url\"\n\t\"reflect\"\n\t\"strings\"\n\n\t\"golang.org/x/net/idna\"\n\n\t\"go.step.sm/crypto/x509util\"\n)\n\n// validateNames verifies that all names are allowed.\nfunc (e *NamePolicyEngine) validateNames(dnsNames []string, ips []net.IP, emailAddresses []string, uris []*url.URL, principals []string) error {\n\t// nothing to compare against; return early\n\tif e.totalNumberOfConstraints == 0 {\n\t\treturn nil\n\t}\n\n\t// TODO: set limit on total of all names validated? In x509 there's a limit on the number of comparisons\n\t// that protects the CA from a DoS (i.e. many heavy comparisons). The x509 implementation takes\n\t// this number as a total of all checks and keeps a (pointer to a) counter of the number of checks\n\t// executed so far.\n\n\t// TODO: gather all errors, or return early? Currently we return early on the first wrong name; check might fail for multiple names.\n\t// Perhaps make that an option?\n\tfor _, dns := range dnsNames {\n\t\t// if there are DNS names to check, no DNS constraints set, but there are other permitted constraints,\n\t\t// then return error, because DNS should be explicitly configured to be allowed in that case. In case there are\n\t\t// (other) excluded constraints, we'll allow a DNS (implicit allow; currently).\n\t\tif e.numberOfDNSDomainConstraints == 0 && e.totalNumberOfPermittedConstraints > 0 {\n\t\t\treturn &NamePolicyError{\n\t\t\t\tReason:   NotAllowed,\n\t\t\t\tNameType: DNSNameType,\n\t\t\t\tName:     dns,\n\t\t\t\tdetail:   fmt.Sprintf(\"dns %q is not explicitly permitted by any constraint\", dns),\n\t\t\t}\n\t\t}\n\t\tdidCutWildcard := false\n\t\tparsedDNS := dns\n\t\tif strings.HasPrefix(parsedDNS, \"*.\") {\n\t\t\tparsedDNS = parsedDNS[1:]\n\t\t\tdidCutWildcard = true\n\t\t}\n\t\t// TODO(hs): fix this above; we need separate rule for Subject Common Name?\n\t\tparsedDNS, err := idna.Lookup.ToASCII(parsedDNS)\n\t\tif err != nil {\n\t\t\treturn &NamePolicyError{\n\t\t\t\tReason:   CannotParseDomain,\n\t\t\t\tNameType: DNSNameType,\n\t\t\t\tName:     dns,\n\t\t\t\tdetail:   fmt.Sprintf(\"dns %q cannot be converted to ASCII\", dns),\n\t\t\t}\n\t\t}\n\t\tif didCutWildcard {\n\t\t\tparsedDNS = \"*\" + parsedDNS\n\t\t}\n\t\tif _, ok := domainToReverseLabels(parsedDNS); !ok { // TODO(hs): this also fails with spaces\n\t\t\treturn &NamePolicyError{\n\t\t\t\tReason:   CannotParseDomain,\n\t\t\t\tNameType: DNSNameType,\n\t\t\t\tName:     dns,\n\t\t\t\tdetail:   fmt.Sprintf(\"cannot parse dns %q\", dns),\n\t\t\t}\n\t\t}\n\t\tif err := checkNameConstraints(DNSNameType, dns, parsedDNS,\n\t\t\tfunc(parsedName, constraint interface{}) (bool, error) {\n\t\t\t\treturn e.matchDomainConstraint(parsedName.(string), constraint.(string))\n\t\t\t}, e.permittedDNSDomains, e.excludedDNSDomains); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tfor _, ip := range ips {\n\t\tif e.numberOfIPRangeConstraints == 0 && e.totalNumberOfPermittedConstraints > 0 {\n\t\t\treturn &NamePolicyError{\n\t\t\t\tReason:   NotAllowed,\n\t\t\t\tNameType: IPNameType,\n\t\t\t\tName:     ip.String(),\n\t\t\t\tdetail:   fmt.Sprintf(\"ip %q is not explicitly permitted by any constraint\", ip.String()),\n\t\t\t}\n\t\t}\n\t\tif err := checkNameConstraints(IPNameType, ip.String(), ip,\n\t\t\tfunc(parsedName, constraint interface{}) (bool, error) {\n\t\t\t\treturn matchIPConstraint(parsedName.(net.IP), constraint.(*net.IPNet))\n\t\t\t}, e.permittedIPRanges, e.excludedIPRanges); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tfor _, email := range emailAddresses {\n\t\tif e.numberOfEmailAddressConstraints == 0 && e.totalNumberOfPermittedConstraints > 0 {\n\t\t\treturn &NamePolicyError{\n\t\t\t\tReason:   NotAllowed,\n\t\t\t\tNameType: EmailNameType,\n\t\t\t\tName:     email,\n\t\t\t\tdetail:   fmt.Sprintf(\"email %q is not explicitly permitted by any constraint\", email),\n\t\t\t}\n\t\t}\n\t\tmailbox, ok := parseRFC2821Mailbox(email)\n\t\tif !ok {\n\t\t\treturn &NamePolicyError{\n\t\t\t\tReason:   CannotParseRFC822Name,\n\t\t\t\tNameType: EmailNameType,\n\t\t\t\tName:     email,\n\t\t\t\tdetail:   fmt.Sprintf(\"invalid rfc822Name %q\", mailbox),\n\t\t\t}\n\t\t}\n\t\t// According to RFC 5280, section 7.5, emails are considered to match if the local part is\n\t\t// an exact match and the host (domain) part matches the ASCII representation (case-insensitive):\n\t\t// https://datatracker.ietf.org/doc/html/rfc5280#section-7.5\n\t\tdomainASCII, err := idna.ToASCII(mailbox.domain)\n\t\tif err != nil {\n\t\t\treturn &NamePolicyError{\n\t\t\t\tReason:   CannotParseDomain,\n\t\t\t\tNameType: EmailNameType,\n\t\t\t\tName:     email,\n\t\t\t\tdetail:   fmt.Errorf(\"cannot parse email domain %q: %w\", email, err).Error(),\n\t\t\t}\n\t\t}\n\t\tmailbox.domain = domainASCII\n\t\tif err := checkNameConstraints(EmailNameType, email, mailbox,\n\t\t\tfunc(parsedName, constraint interface{}) (bool, error) {\n\t\t\t\treturn e.matchEmailConstraint(parsedName.(rfc2821Mailbox), constraint.(string))\n\t\t\t}, e.permittedEmailAddresses, e.excludedEmailAddresses); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\t// TODO(hs): fix internationalization for URIs (IRIs)\n\n\tfor _, uri := range uris {\n\t\tif e.numberOfURIDomainConstraints == 0 && e.totalNumberOfPermittedConstraints > 0 {\n\t\t\treturn &NamePolicyError{\n\t\t\t\tReason:   NotAllowed,\n\t\t\t\tNameType: URINameType,\n\t\t\t\tName:     uri.String(),\n\t\t\t\tdetail:   fmt.Sprintf(\"uri %q is not explicitly permitted by any constraint\", uri.String()),\n\t\t\t}\n\t\t}\n\t\t// TODO(hs): ideally we'd like the uri.String() to be the original contents; now\n\t\t// it's transformed into ASCII. Prevent that here?\n\t\tif err := checkNameConstraints(URINameType, uri.String(), uri,\n\t\t\tfunc(parsedName, constraint interface{}) (bool, error) {\n\t\t\t\treturn e.matchURIConstraint(parsedName.(*url.URL), constraint.(string))\n\t\t\t}, e.permittedURIDomains, e.excludedURIDomains); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tfor _, principal := range principals {\n\t\tif e.numberOfPrincipalConstraints == 0 && e.totalNumberOfPermittedConstraints > 0 {\n\t\t\treturn &NamePolicyError{\n\t\t\t\tReason:   NotAllowed,\n\t\t\t\tNameType: PrincipalNameType,\n\t\t\t\tName:     principal,\n\t\t\t\tdetail:   fmt.Sprintf(\"username principal %q is not explicitly permitted by any constraint\", principal),\n\t\t\t}\n\t\t}\n\t\t// TODO: some validation? I.e. allowed characters?\n\t\tif err := checkNameConstraints(PrincipalNameType, principal, principal,\n\t\t\tfunc(parsedName, constraint interface{}) (bool, error) {\n\t\t\t\treturn matchPrincipalConstraint(parsedName.(string), constraint.(string))\n\t\t\t}, e.permittedPrincipals, e.excludedPrincipals); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\t// if all checks out, all SANs are allowed\n\treturn nil\n}\n\n// validateCommonName verifies that the Subject Common Name is allowed\nfunc (e *NamePolicyEngine) validateCommonName(commonName string) error {\n\t// nothing to compare against; return early\n\tif e.totalNumberOfConstraints == 0 {\n\t\treturn nil\n\t}\n\n\t// empty common names are not validated\n\tif commonName == \"\" {\n\t\treturn nil\n\t}\n\n\tif e.numberOfCommonNameConstraints > 0 {\n\t\t// Check the Common Name using its dedicated matcher if constraints have been\n\t\t// configured. If no error is returned from matching, the Common Name was\n\t\t// explicitly allowed and nil is returned immediately.\n\t\tif err := checkNameConstraints(CNNameType, commonName, commonName,\n\t\t\tfunc(parsedName, constraint interface{}) (bool, error) {\n\t\t\t\treturn matchCommonNameConstraint(parsedName.(string), constraint.(string))\n\t\t\t}, e.permittedCommonNames, e.excludedCommonNames); err == nil {\n\t\t\treturn nil\n\t\t}\n\t}\n\n\t// When an error was returned or when no constraints were configured for Common Names,\n\t// the Common Name should be validated against the other types of constraints too,\n\t// according to what type it is.\n\tdnsNames, ips, emails, uris := x509util.SplitSANs([]string{commonName})\n\n\terr := e.validateNames(dnsNames, ips, emails, uris, []string{})\n\n\tvar pe *NamePolicyError\n\tif errors.As(err, &pe) {\n\t\t// override the name type with CN\n\t\tpe.NameType = CNNameType\n\t}\n\n\treturn err\n}\n\n// checkNameConstraints checks that a name, of type nameType is permitted.\n// The argument parsedName contains the parsed form of name, suitable for passing\n// to the match function.\nfunc checkNameConstraints(\n\tnameType NameType,\n\tname string,\n\tparsedName interface{},\n\tmatch func(parsedName, constraint interface{}) (match bool, err error),\n\tpermitted, excluded interface{}) error {\n\texcludedValue := reflect.ValueOf(excluded)\n\n\tfor i := 0; i < excludedValue.Len(); i++ {\n\t\tconstraint := excludedValue.Index(i).Interface()\n\t\tmatch, err := match(parsedName, constraint)\n\t\tif err != nil {\n\t\t\treturn &NamePolicyError{\n\t\t\t\tReason:   CannotMatchNameToConstraint,\n\t\t\t\tNameType: nameType,\n\t\t\t\tName:     name,\n\t\t\t\tdetail:   err.Error(),\n\t\t\t}\n\t\t}\n\n\t\tif match {\n\t\t\treturn &NamePolicyError{\n\t\t\t\tReason:   NotAllowed,\n\t\t\t\tNameType: nameType,\n\t\t\t\tName:     name,\n\t\t\t\tdetail:   fmt.Sprintf(\"%s %q is excluded by constraint %q\", nameType, name, constraint),\n\t\t\t}\n\t\t}\n\t}\n\n\tpermittedValue := reflect.ValueOf(permitted)\n\n\tok := true\n\tfor i := 0; i < permittedValue.Len(); i++ {\n\t\tconstraint := permittedValue.Index(i).Interface()\n\t\tvar err error\n\t\tif ok, err = match(parsedName, constraint); err != nil {\n\t\t\treturn &NamePolicyError{\n\t\t\t\tReason:   CannotMatchNameToConstraint,\n\t\t\t\tNameType: nameType,\n\t\t\t\tName:     name,\n\t\t\t\tdetail:   err.Error(),\n\t\t\t}\n\t\t}\n\n\t\tif ok {\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif !ok {\n\t\treturn &NamePolicyError{\n\t\t\tReason:   NotAllowed,\n\t\t\tNameType: nameType,\n\t\t\tName:     name,\n\t\t\tdetail:   fmt.Sprintf(\"%s %q is not permitted by any constraint\", nameType, name),\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// domainToReverseLabels converts a textual domain name like foo.example.com to\n// the list of labels in reverse order, e.g. [\"com\", \"example\", \"foo\"].\nfunc domainToReverseLabels(domain string) (reverseLabels []string, ok bool) {\n\tfor domain != \"\" {\n\t\tif i := strings.LastIndexByte(domain, '.'); i == -1 {\n\t\t\treverseLabels = append(reverseLabels, domain)\n\t\t\tdomain = \"\"\n\t\t} else {\n\t\t\treverseLabels = append(reverseLabels, domain[i+1:])\n\t\t\tdomain = domain[:i]\n\t\t}\n\t}\n\n\tif len(reverseLabels) > 0 && reverseLabels[0] == \"\" {\n\t\t// An empty label at the end indicates an absolute value.\n\t\treturn nil, false\n\t}\n\n\tfor _, label := range reverseLabels {\n\t\tif label == \"\" {\n\t\t\t// Empty labels are otherwise invalid.\n\t\t\treturn nil, false\n\t\t}\n\n\t\tfor _, c := range label {\n\t\t\tif c < 33 || c > 126 {\n\t\t\t\t// Invalid character.\n\t\t\t\treturn nil, false\n\t\t\t}\n\t\t}\n\t}\n\n\treturn reverseLabels, true\n}\n\n// rfc2821Mailbox represents a “mailbox” (which is an email address to most\n// people) by breaking it into the “local” (i.e. before the '@') and “domain”\n// parts.\ntype rfc2821Mailbox struct {\n\tlocal, domain string\n}\n\n// parseRFC2821Mailbox parses an email address into local and domain parts,\n// based on the ABNF for a “Mailbox” from RFC 2821. According to RFC 5280,\n// Section 4.2.1.6 that's correct for an rfc822Name from a certificate: “The\n// format of an rfc822Name is a \"Mailbox\" as defined in RFC 2821, Section 4.1.2”.\nfunc parseRFC2821Mailbox(in string) (mailbox rfc2821Mailbox, ok bool) {\n\tif in == \"\" {\n\t\treturn mailbox, false\n\t}\n\n\tlocalPartBytes := make([]byte, 0, len(in)/2)\n\n\tif in[0] == '\"' {\n\t\t// Quoted-string = DQUOTE *qcontent DQUOTE\n\t\t// non-whitespace-control = %d1-8 / %d11 / %d12 / %d14-31 / %d127\n\t\t// qcontent = qtext / quoted-pair\n\t\t// qtext = non-whitespace-control /\n\t\t//         %d33 / %d35-91 / %d93-126\n\t\t// quoted-pair = (\"\\\" text) / obs-qp\n\t\t// text = %d1-9 / %d11 / %d12 / %d14-127 / obs-text\n\t\t//\n\t\t// (Names beginning with “obs-” are the obsolete syntax from RFC 2822,\n\t\t// Section 4. Since it has been 16 years, we no longer accept that.)\n\t\tin = in[1:]\n\tQuotedString:\n\t\tfor {\n\t\t\tif in == \"\" {\n\t\t\t\treturn mailbox, false\n\t\t\t}\n\t\t\tc := in[0]\n\t\t\tin = in[1:]\n\n\t\t\tswitch {\n\t\t\tcase c == '\"':\n\t\t\t\tbreak QuotedString\n\n\t\t\tcase c == '\\\\':\n\t\t\t\t// quoted-pair\n\t\t\t\tif in == \"\" {\n\t\t\t\t\treturn mailbox, false\n\t\t\t\t}\n\t\t\t\tif in[0] == 11 ||\n\t\t\t\t\tin[0] == 12 ||\n\t\t\t\t\t(1 <= in[0] && in[0] <= 9) ||\n\t\t\t\t\t(14 <= in[0] && in[0] <= 127) {\n\t\t\t\t\tlocalPartBytes = append(localPartBytes, in[0])\n\t\t\t\t\tin = in[1:]\n\t\t\t\t} else {\n\t\t\t\t\treturn mailbox, false\n\t\t\t\t}\n\n\t\t\tcase c == 11 ||\n\t\t\t\tc == 12 ||\n\t\t\t\t// Space (char 32) is not allowed based on the\n\t\t\t\t// BNF, but RFC 3696 gives an example that\n\t\t\t\t// assumes that it is. Several “verified”\n\t\t\t\t// errata continue to argue about this point.\n\t\t\t\t// We choose to accept it.\n\t\t\t\tc == 32 ||\n\t\t\t\tc == 33 ||\n\t\t\t\tc == 127 ||\n\t\t\t\t(1 <= c && c <= 8) ||\n\t\t\t\t(14 <= c && c <= 31) ||\n\t\t\t\t(35 <= c && c <= 91) ||\n\t\t\t\t(93 <= c && c <= 126):\n\t\t\t\t// qtext\n\t\t\t\tlocalPartBytes = append(localPartBytes, c)\n\n\t\t\tdefault:\n\t\t\t\treturn mailbox, false\n\t\t\t}\n\t\t}\n\t} else {\n\t\t// Atom (\".\" Atom)*\n\tNextChar:\n\t\tfor in != \"\" {\n\t\t\t// atext from RFC 2822, Section 3.2.4\n\t\t\tc := in[0]\n\n\t\t\tswitch {\n\t\t\tcase c == '\\\\':\n\t\t\t\t// Examples given in RFC 3696 suggest that\n\t\t\t\t// escaped characters can appear outside of a\n\t\t\t\t// quoted string. Several “verified” errata\n\t\t\t\t// continue to argue the point. We choose to\n\t\t\t\t// accept it.\n\t\t\t\tin = in[1:]\n\t\t\t\tif in == \"\" {\n\t\t\t\t\treturn mailbox, false\n\t\t\t\t}\n\t\t\t\tfallthrough\n\n\t\t\tcase ('0' <= c && c <= '9') ||\n\t\t\t\t('a' <= c && c <= 'z') ||\n\t\t\t\t('A' <= c && c <= 'Z') ||\n\t\t\t\tc == '!' || c == '#' || c == '$' || c == '%' ||\n\t\t\t\tc == '&' || c == '\\'' || c == '*' || c == '+' ||\n\t\t\t\tc == '-' || c == '/' || c == '=' || c == '?' ||\n\t\t\t\tc == '^' || c == '_' || c == '`' || c == '{' ||\n\t\t\t\tc == '|' || c == '}' || c == '~' || c == '.':\n\t\t\t\tlocalPartBytes = append(localPartBytes, in[0])\n\t\t\t\tin = in[1:]\n\n\t\t\tdefault:\n\t\t\t\tbreak NextChar\n\t\t\t}\n\t\t}\n\n\t\tif len(localPartBytes) == 0 {\n\t\t\treturn mailbox, false\n\t\t}\n\n\t\t// From RFC 3696, Section 3:\n\t\t// “period (\".\") may also appear, but may not be used to start\n\t\t// or end the local part, nor may two or more consecutive\n\t\t// periods appear.”\n\t\ttwoDots := []byte{'.', '.'}\n\t\tif localPartBytes[0] == '.' ||\n\t\t\tlocalPartBytes[len(localPartBytes)-1] == '.' ||\n\t\t\tbytes.Contains(localPartBytes, twoDots) {\n\t\t\treturn mailbox, false\n\t\t}\n\t}\n\n\tif in == \"\" || in[0] != '@' {\n\t\treturn mailbox, false\n\t}\n\tin = in[1:]\n\n\t// The RFC species a format for domains, but that's known to be\n\t// violated in practice so we accept that anything after an '@' is the\n\t// domain part.\n\tif _, ok := domainToReverseLabels(in); !ok {\n\t\treturn mailbox, false\n\t}\n\n\tmailbox.local = string(localPartBytes)\n\tmailbox.domain = in\n\treturn mailbox, true\n}\n\n// matchDomainConstraint matches a domain against the given constraint\nfunc (e *NamePolicyEngine) matchDomainConstraint(domain, constraint string) (bool, error) {\n\t// The meaning of zero length constraints is not specified, but this\n\t// code follows NSS and accepts them as matching everything.\n\tif constraint == \"\" {\n\t\treturn true, nil\n\t}\n\n\t// A single whitespace seems to be considered a valid domain, but we don't allow it.\n\tif domain == \" \" {\n\t\treturn false, nil\n\t}\n\n\t// Block domains that start with just a period\n\tif domain[0] == '.' {\n\t\treturn false, nil\n\t}\n\n\t// Block wildcard domains that don't start with exactly \"*.\" (i.e. double wildcards and such)\n\tif domain[0] == '*' && domain[1] != '.' {\n\t\treturn false, nil\n\t}\n\n\t// Check if the domain starts with a wildcard and return early if not allowed\n\tif strings.HasPrefix(domain, \"*.\") && !e.allowLiteralWildcardNames {\n\t\treturn false, nil\n\t}\n\n\t// Only allow asterisk at the start of the domain; we don't allow them as part of a domain label or as a (sub)domain label (currently)\n\tif strings.LastIndex(domain, \"*\") > 0 {\n\t\treturn false, nil\n\t}\n\n\t// Don't allow constraints with empty labels in any position\n\tif strings.Contains(constraint, \"..\") {\n\t\treturn false, nil\n\t}\n\n\tdomainLabels, ok := domainToReverseLabels(domain)\n\tif !ok {\n\t\treturn false, fmt.Errorf(\"cannot parse domain %q\", domain)\n\t}\n\n\t// RFC 5280 says that a leading period in a domain name means that at\n\t// least one label must be prepended, but only for URI and email\n\t// constraints, not DNS constraints. The code also supports that\n\t// behavior for DNS constraints. In our adaptation of the original\n\t// Go stdlib x509 Name Constraint implementation we look for exactly\n\t// one subdomain, currently.\n\n\tmustHaveSubdomains := false\n\tif constraint[0] == '.' {\n\t\tmustHaveSubdomains = true\n\t\tconstraint = constraint[1:]\n\t}\n\n\tconstraintLabels, ok := domainToReverseLabels(constraint)\n\tif !ok {\n\t\treturn false, fmt.Errorf(\"cannot parse domain constraint %q\", constraint)\n\t}\n\n\texpectedNumberOfLabels := len(constraintLabels)\n\tif mustHaveSubdomains {\n\t\t// we expect exactly one more label if it starts with the \"canonical\" x509 \"wildcard\": \".\"\n\t\t// in the future we could extend this to support multiple additional labels and/or more\n\t\t// complex matching.\n\t\texpectedNumberOfLabels++\n\t}\n\n\tif len(domainLabels) != expectedNumberOfLabels {\n\t\treturn false, nil\n\t}\n\n\tfor i, constraintLabel := range constraintLabels {\n\t\tif !strings.EqualFold(constraintLabel, domainLabels[i]) {\n\t\t\treturn false, nil\n\t\t}\n\t}\n\n\treturn true, nil\n}\n\n// SOURCE: https://cs.opensource.google/go/go/+/refs/tags/go1.17.5:src/crypto/x509/verify.go\nfunc matchIPConstraint(ip net.IP, constraint *net.IPNet) (bool, error) {\n\t// TODO(hs): this is code from Go library, but I got some unexpected result:\n\t// with permitted net 127.0.0.0/24, 127.0.0.1 is NOT allowed. When parsing 127.0.0.1 as net.IP\n\t// which is in the IPAddresses slice, the underlying length is 16. The contraint.IP has a length\n\t// of 4 instead. I currently don't believe that this is a bug in Go now, but why is it like that?\n\t// Is there a difference because we're not operating on a sans []string slice? Or is the Go\n\t// implementation stricter regarding IPv4 vs. IPv6? I've been bitten by some unfortunate differences\n\t// between the two before (i.e. IPv4 in IPv6; IP SANS in ACME)\n\t// if len(ip) != len(constraint.IP) {\n\t// \treturn false, nil\n\t// }\n\n\t// for i := range ip {\n\t// \tif mask := constraint.Mask[i]; ip[i]&mask != constraint.IP[i]&mask {\n\t// \t\treturn false, nil\n\t// \t}\n\t// }\n\n\tcontained := constraint.Contains(ip) // TODO(hs): validate that this is the correct behavior; also check IPv4-in-IPv6 (again)\n\n\treturn contained, nil\n}\n\n// SOURCE: https://cs.opensource.google/go/go/+/refs/tags/go1.17.5:src/crypto/x509/verify.go\nfunc (e *NamePolicyEngine) matchEmailConstraint(mailbox rfc2821Mailbox, constraint string) (bool, error) {\n\tif strings.Contains(constraint, \"@\") {\n\t\tconstraintMailbox, ok := parseRFC2821Mailbox(constraint)\n\t\tif !ok {\n\t\t\treturn false, fmt.Errorf(\"cannot parse constraint %q\", constraint)\n\t\t}\n\t\treturn mailbox.local == constraintMailbox.local && strings.EqualFold(mailbox.domain, constraintMailbox.domain), nil\n\t}\n\n\t// Otherwise the constraint is like a DNS constraint of the domain part\n\t// of the mailbox.\n\treturn e.matchDomainConstraint(mailbox.domain, constraint)\n}\n\n// matchURIConstraint matches an URL against a constraint\nfunc (e *NamePolicyEngine) matchURIConstraint(uri *url.URL, constraint string) (bool, error) {\n\t// From RFC 5280, Section 4.2.1.10:\n\t// “a uniformResourceIdentifier that does not include an authority\n\t// component with a host name specified as a fully qualified domain\n\t// name (e.g., if the URI either does not include an authority\n\t// component or includes an authority component in which the host name\n\t// is specified as an IP address), then the application MUST reject the\n\t// certificate.”\n\n\thost := uri.Host\n\tif host == \"\" {\n\t\treturn false, fmt.Errorf(\"URI with empty host (%q) cannot be matched against constraints\", uri.String())\n\t}\n\n\t// Block hosts with the wildcard character; no exceptions, also not when wildcards allowed.\n\tif strings.Contains(host, \"*\") {\n\t\treturn false, fmt.Errorf(\"URI host %q cannot contain asterisk\", uri.String())\n\t}\n\n\tif strings.Contains(host, \":\") && !strings.HasSuffix(host, \"]\") {\n\t\tvar err error\n\t\thost, _, err = net.SplitHostPort(host)\n\t\tif err != nil {\n\t\t\treturn false, err\n\t\t}\n\t}\n\n\tif strings.HasPrefix(host, \"[\") && strings.HasSuffix(host, \"]\") ||\n\t\tnet.ParseIP(host) != nil {\n\t\treturn false, fmt.Errorf(\"URI with IP %q cannot be matched against constraints\", uri.String())\n\t}\n\n\t// TODO(hs): add checks for scheme, path, etc.; either here, or in a different constraint matcher (to keep this one simple)\n\n\treturn e.matchDomainConstraint(host, constraint)\n}\n\n// matchPrincipalConstraint performs a string literal equality check against a constraint.\nfunc matchPrincipalConstraint(principal, constraint string) (bool, error) {\n\t// allow any plain principal when wildcard constraint is used\n\tif constraint == \"*\" {\n\t\treturn true, nil\n\t}\n\treturn strings.EqualFold(principal, constraint), nil\n}\n\n// matchCommonNameConstraint performs a string literal equality check against constraint.\nfunc matchCommonNameConstraint(commonName, constraint string) (bool, error) {\n\t// wildcard constraint is (currently) not supported for common names\n\tif constraint == \"*\" {\n\t\treturn false, nil\n\t}\n\treturn strings.EqualFold(commonName, constraint), nil\n}\n"
  },
  {
    "path": "policy/x509.go",
    "content": "package policy\n\nimport (\n\t\"crypto/x509\"\n\t\"net\"\n)\n\ntype X509NamePolicyEngine interface {\n\tIsX509CertificateAllowed(cert *x509.Certificate) error\n\tIsX509CertificateRequestAllowed(csr *x509.CertificateRequest) error\n\tAreSANsAllowed(sans []string) error\n\tIsDNSAllowed(dns string) error\n\tIsIPAllowed(ip net.IP) error\n}\n"
  },
  {
    "path": "scep/api/api.go",
    "content": "// Package api implements a SCEP HTTP server.\npackage api\n\nimport (\n\t\"context\"\n\t\"crypto/x509\"\n\t\"encoding/base64\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n\n\t\"github.com/go-chi/chi/v5\"\n\t\"github.com/smallstep/pkcs7\"\n\tsmallscep \"github.com/smallstep/scep\"\n\n\t\"github.com/smallstep/certificates/api\"\n\t\"github.com/smallstep/certificates/api/log\"\n\t\"github.com/smallstep/certificates/authority\"\n\t\"github.com/smallstep/certificates/authority/provisioner\"\n\t\"github.com/smallstep/certificates/scep\"\n)\n\nconst (\n\topnGetCACert    = \"GetCACert\"\n\topnGetCACaps    = \"GetCACaps\"\n\topnPKIOperation = \"PKIOperation\"\n\n\t// TODO: add other (more optional) operations and handling\n)\n\nconst maxPayloadSize = 2 << 20\n\n// request is a SCEP server request.\ntype request struct {\n\tOperation string\n\tMessage   []byte\n}\n\n// Response is a SCEP server Response.\ntype Response struct {\n\tOperation   string\n\tCACertNum   int\n\tData        []byte\n\tCertificate *x509.Certificate\n\tError       error\n}\n\n// handler is the SCEP request handler.\ntype handler struct {\n\tauth *scep.Authority\n}\n\n// Route traffic and implement the Router interface.\n//\n// Deprecated: use scep.Route(r api.Router)\nfunc (h *handler) Route(r api.Router) {\n\troute(r, func(next http.HandlerFunc) http.HandlerFunc {\n\t\treturn func(w http.ResponseWriter, r *http.Request) {\n\t\t\tctx := scep.NewContext(r.Context(), h.auth)\n\t\t\tnext(w, r.WithContext(ctx))\n\t\t}\n\t})\n}\n\n// New returns a new SCEP API router.\n//\n// Deprecated: use scep.Route(r api.Router)\nfunc New(auth *scep.Authority) api.RouterHandler {\n\treturn &handler{auth: auth}\n}\n\n// Route traffic and implement the Router interface.\nfunc Route(r api.Router) {\n\troute(r, nil)\n}\n\nfunc route(r api.Router, middleware func(next http.HandlerFunc) http.HandlerFunc) {\n\tgetHandler := lookupProvisioner(Get)\n\tpostHandler := lookupProvisioner(Post)\n\n\t// For backward compatibility.\n\tif middleware != nil {\n\t\tgetHandler = middleware(getHandler)\n\t\tpostHandler = middleware(postHandler)\n\t}\n\n\tr.MethodFunc(http.MethodGet, \"/{provisionerName}/*\", getHandler)\n\tr.MethodFunc(http.MethodGet, \"/{provisionerName}\", getHandler)\n\tr.MethodFunc(http.MethodPost, \"/{provisionerName}/*\", postHandler)\n\tr.MethodFunc(http.MethodPost, \"/{provisionerName}\", postHandler)\n}\n\n// Get handles all SCEP GET requests\nfunc Get(w http.ResponseWriter, r *http.Request) {\n\treq, err := decodeRequest(r)\n\tif err != nil {\n\t\tfail(w, r, fmt.Errorf(\"invalid scep get request: %w\", err))\n\t\treturn\n\t}\n\n\tctx := r.Context()\n\tvar res Response\n\n\tswitch req.Operation {\n\tcase opnGetCACert:\n\t\tres, err = GetCACert(ctx)\n\tcase opnGetCACaps:\n\t\tres, err = GetCACaps(ctx)\n\tcase opnPKIOperation:\n\t\tres, err = PKIOperation(ctx, req)\n\tdefault:\n\t\terr = fmt.Errorf(\"unknown operation: %s\", req.Operation)\n\t}\n\n\tif err != nil {\n\t\tfail(w, r, fmt.Errorf(\"scep get request failed: %w\", err))\n\t\treturn\n\t}\n\n\twriteResponse(w, r, res)\n}\n\n// Post handles all SCEP POST requests\nfunc Post(w http.ResponseWriter, r *http.Request) {\n\treq, err := decodeRequest(r)\n\tif err != nil {\n\t\tfail(w, r, fmt.Errorf(\"invalid scep post request: %w\", err))\n\t\treturn\n\t}\n\n\tvar res Response\n\tswitch req.Operation {\n\tcase opnPKIOperation:\n\t\tres, err = PKIOperation(r.Context(), req)\n\tdefault:\n\t\terr = fmt.Errorf(\"unknown operation: %s\", req.Operation)\n\t}\n\n\tif err != nil {\n\t\tfail(w, r, fmt.Errorf(\"scep post request failed: %w\", err))\n\t\treturn\n\t}\n\n\twriteResponse(w, r, res)\n}\n\nfunc decodeRequest(r *http.Request) (request, error) {\n\tdefer r.Body.Close()\n\n\tmethod := r.Method\n\tquery, err := url.ParseQuery(r.URL.RawQuery)\n\tif err != nil {\n\t\treturn request{}, fmt.Errorf(\"failed parsing URL query: %w\", err)\n\t}\n\n\toperation := query.Get(\"operation\")\n\tif operation == \"\" {\n\t\treturn request{}, errors.New(\"no operation provided\")\n\t}\n\n\tswitch method {\n\tcase http.MethodGet:\n\t\tswitch operation {\n\t\tcase opnGetCACert, opnGetCACaps:\n\t\t\treturn request{\n\t\t\t\tOperation: operation,\n\t\t\t\tMessage:   []byte{},\n\t\t\t}, nil\n\t\tcase opnPKIOperation:\n\t\t\tmessage := query.Get(\"message\")\n\t\t\tdecodedMessage, err := decodeMessage(message, r)\n\t\t\tif err != nil {\n\t\t\t\treturn request{}, fmt.Errorf(\"failed decoding message: %w\", err)\n\t\t\t}\n\t\t\treturn request{\n\t\t\t\tOperation: operation,\n\t\t\t\tMessage:   decodedMessage,\n\t\t\t}, nil\n\t\tdefault:\n\t\t\treturn request{}, fmt.Errorf(\"unsupported operation: %s\", operation)\n\t\t}\n\tcase http.MethodPost:\n\t\tbody, err := io.ReadAll(io.LimitReader(r.Body, maxPayloadSize))\n\t\tif err != nil {\n\t\t\treturn request{}, fmt.Errorf(\"failed reading request body: %w\", err)\n\t\t}\n\t\treturn request{\n\t\t\tOperation: operation,\n\t\t\tMessage:   body,\n\t\t}, nil\n\tdefault:\n\t\treturn request{}, fmt.Errorf(\"unsupported method: %s\", method)\n\t}\n}\n\nfunc decodeMessage(message string, r *http.Request) ([]byte, error) {\n\tif message == \"\" {\n\t\treturn nil, errors.New(\"message must not be empty\")\n\t}\n\n\t// decode the message, which should be base64 standard encoded. Any characters that\n\t// were escaped in the original query, were unescaped as part of url.ParseQuery, so\n\t// that doesn't need to be performed here. Return early if successful.\n\tdecodedMessage, err := base64.StdEncoding.DecodeString(message)\n\tif err == nil {\n\t\treturn decodedMessage, nil\n\t}\n\n\t// only interested in corrupt input errors below this. This type of error is the\n\t// most likely to return, but better safe than sorry.\n\tvar cie base64.CorruptInputError\n\tif !errors.As(err, &cie) {\n\t\treturn nil, fmt.Errorf(\"failed base64 decoding message: %w\", err)\n\t}\n\n\t// the below code is a workaround for macOS when it sends a GET PKIOperation, which seems to result\n\t// in a query with the '+' and '/' not being percent encoded; only the padding ('=') is encoded.\n\t// When that is unescaped in the code before this, this results in invalid base64. The workaround\n\t// is to obtain the original query, extract the message, apply transformation(s) to make it valid\n\t// base64 and try decoding it again. If it succeeds, the happy path can be followed with the patched\n\t// message. Otherwise we still return an error.\n\trawQuery, err := parseRawQuery(r.URL.RawQuery)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to parse raw query: %w\", err)\n\t}\n\n\trawMessage := rawQuery.Get(\"message\")\n\tif rawMessage == \"\" {\n\t\treturn nil, errors.New(\"no message in raw query\")\n\t}\n\n\trawMessage = strings.ReplaceAll(rawMessage, \"%3D\", \"=\") // apparently the padding arrives encoded; the others (+, /) not?\n\tdecodedMessage, err = base64.StdEncoding.DecodeString(rawMessage)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed base64 decoding raw message: %w\", err)\n\t}\n\n\treturn decodedMessage, nil\n}\n\n// parseRawQuery parses a URL query into url.Values. It skips\n// unescaping keys and values. This code is based on url.ParseQuery.\nfunc parseRawQuery(query string) (url.Values, error) {\n\tm := make(url.Values)\n\terr := parseRawQueryWithoutUnescaping(m, query)\n\treturn m, err\n}\n\n// parseRawQueryWithoutUnescaping parses the raw query into url.Values, skipping\n// unescaping of the parts. This code is based on url.parseQuery.\nfunc parseRawQueryWithoutUnescaping(m url.Values, query string) (err error) {\n\tfor query != \"\" {\n\t\tvar key string\n\t\tkey, query, _ = strings.Cut(query, \"&\")\n\t\tif strings.Contains(key, \";\") {\n\t\t\treturn errors.New(\"invalid semicolon separator in query\")\n\t\t}\n\t\tif key == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tkey, value, _ := strings.Cut(key, \"=\")\n\t\tm[key] = append(m[key], value)\n\t}\n\treturn err\n}\n\n// lookupProvisioner loads the provisioner associated with the request.\n// Responds 404 if the provisioner does not exist.\nfunc lookupProvisioner(next http.HandlerFunc) http.HandlerFunc {\n\treturn func(w http.ResponseWriter, r *http.Request) {\n\t\tname := chi.URLParam(r, \"provisionerName\")\n\t\tprovisionerName, err := url.PathUnescape(name)\n\t\tif err != nil {\n\t\t\tfail(w, r, fmt.Errorf(\"error url unescaping provisioner name '%s'\", name))\n\t\t\treturn\n\t\t}\n\n\t\tctx := r.Context()\n\t\tauth := authority.MustFromContext(ctx)\n\t\tp, err := auth.LoadProvisionerByName(provisionerName)\n\t\tif err != nil {\n\t\t\tfail(w, r, err)\n\t\t\treturn\n\t\t}\n\n\t\tprov, ok := p.(*provisioner.SCEP)\n\t\tif !ok {\n\t\t\tfail(w, r, errors.New(\"provisioner must be of type SCEP\"))\n\t\t\treturn\n\t\t}\n\n\t\tctx = scep.NewProvisionerContext(ctx, scep.Provisioner(prov))\n\t\tnext(w, r.WithContext(ctx))\n\t}\n}\n\n// GetCACert returns the CA certificates in a SCEP response\nfunc GetCACert(ctx context.Context) (Response, error) {\n\tauth := scep.MustFromContext(ctx)\n\tcerts, err := auth.GetCACertificates(ctx)\n\tif err != nil {\n\t\treturn Response{}, err\n\t}\n\n\tif len(certs) == 0 {\n\t\treturn Response{}, errors.New(\"missing CA cert\")\n\t}\n\n\tres := Response{\n\t\tOperation: opnGetCACert,\n\t\tCACertNum: len(certs),\n\t}\n\n\tif len(certs) == 1 {\n\t\tres.Data = certs[0].Raw\n\t} else {\n\t\t// create degenerate pkcs7 certificate structure, according to\n\t\t// https://tools.ietf.org/html/rfc8894#section-4.2.1.2, because\n\t\t// not signed or encrypted data has to be returned.\n\t\tdata, err := smallscep.DegenerateCertificates(certs)\n\t\tif err != nil {\n\t\t\treturn Response{}, err\n\t\t}\n\t\tres.Data = data\n\t}\n\n\treturn res, nil\n}\n\n// GetCACaps returns the CA capabilities in a SCEP response\nfunc GetCACaps(ctx context.Context) (Response, error) {\n\tauth := scep.MustFromContext(ctx)\n\tcaps := auth.GetCACaps(ctx)\n\n\tres := Response{\n\t\tOperation: opnGetCACaps,\n\t\tData:      formatCapabilities(caps),\n\t}\n\n\treturn res, nil\n}\n\n// PKIOperation performs PKI operations and returns a SCEP response\nfunc PKIOperation(ctx context.Context, req request) (Response, error) {\n\t// parse the message using smallscep implementation\n\tmicroMsg, err := smallscep.ParsePKIMessage(req.Message)\n\tif err != nil {\n\t\t// return the error, because we can't use the msg for creating a CertRep\n\t\treturn Response{}, fmt.Errorf(\"failed parsing SCEP request: %w\", err)\n\t}\n\n\t// this is essentially doing the same as smallscep.ParsePKIMessage, but\n\t// gives us access to the p7 itself in scep.PKIMessage. Essentially a small\n\t// wrapper for the smallscep implementation.\n\tp7, err := pkcs7.Parse(microMsg.Raw)\n\tif err != nil {\n\t\treturn Response{}, err\n\t}\n\n\t// copy over properties to our internal PKIMessage\n\tmsg := &scep.PKIMessage{\n\t\tTransactionID: microMsg.TransactionID,\n\t\tMessageType:   microMsg.MessageType,\n\t\tSenderNonce:   microMsg.SenderNonce,\n\t\tRaw:           microMsg.Raw,\n\t\tP7:            p7,\n\t}\n\n\tauth := scep.MustFromContext(ctx)\n\tif err := auth.DecryptPKIEnvelope(ctx, msg); err != nil {\n\t\treturn Response{}, err\n\t}\n\n\t// NOTE: at this point we have sufficient information for returning nicely signed CertReps\n\tcsr := msg.CSRReqMessage.CSR\n\ttransactionID := string(msg.TransactionID)\n\tchallengePassword := msg.CSRReqMessage.ChallengePassword\n\n\t// NOTE: we're blocking the RenewalReq if the challenge does not match, because otherwise we don't have any authentication.\n\t// The macOS SCEP client performs renewals using PKCSreq. The CertNanny SCEP client will use PKCSreq with challenge too, it seems,\n\t// even if using the renewal flow as described in the README.md. MicroMDM SCEP client also only does PKCSreq by default, unless\n\t// a certificate exists; then it will use RenewalReq. Adding the challenge check here may be a small breaking change for clients.\n\t// We'll have to see how it works out.\n\tvar signCSROpts []provisioner.SignCSROption\n\tif msg.MessageType == smallscep.PKCSReq || msg.MessageType == smallscep.RenewalReq {\n\t\tchallengeOptions, err := auth.ValidateChallenge(ctx, csr, challengePassword, transactionID)\n\t\tif err != nil {\n\t\t\tif errors.Is(err, provisioner.ErrSCEPChallengeInvalid) {\n\t\t\t\treturn createFailureResponse(ctx, csr, msg, smallscep.BadRequest, err.Error(), err)\n\t\t\t}\n\t\t\tscepErr := errors.New(\"failed validating challenge password\")\n\t\t\treturn createFailureResponse(ctx, csr, msg, smallscep.BadRequest, scepErr.Error(), scepErr)\n\t\t}\n\t\tsignCSROpts = append(signCSROpts, challengeOptions...)\n\t} else {\n\t\tscepErr := fmt.Errorf(\"unexpected message type: (%s)\", string(msg.MessageType))\n\t\treturn createFailureResponse(ctx, csr, msg, smallscep.BadRequest, scepErr.Error(), scepErr)\n\t}\n\n\t// TODO: authorize renewal: we can authorize renewals with the challenge password (if reusable secrets are used).\n\t// Renewals OPTIONALLY include the challenge if the existing cert is used as authentication, but client SHOULD omit the challenge.\n\t// This means that for renewal requests we should check the certificate provided to be signed before by the CA. We could\n\t// enforce use of the challenge if we want too. That way we could be more flexible in terms of authentication scheme (i.e. reusing\n\t// tokens from other provisioners, calling a webhook, storing multiple secrets, allowing them to be multi-use, etc).\n\t// Authentication by the (self-signed) certificate with an optional challenge is required; supporting renewals incl. verification\n\t// of the client cert is not.\n\n\tcertRep, err := auth.SignCSR(ctx, csr, msg, signCSROpts...)\n\tif err != nil {\n\t\tif notifyErr := auth.NotifyFailure(ctx, csr, transactionID, 0, err.Error()); notifyErr != nil {\n\t\t\t// TODO(hs): ignore this error case? It's not critical if the notification fails; but logging it might be good\n\t\t\t_ = notifyErr\n\t\t}\n\t\treturn createFailureResponse(ctx, csr, msg, smallscep.BadRequest, \"internal server error; please see the certificate authority logs for more info\", fmt.Errorf(\"error when signing new certificate: %w\", err))\n\t}\n\n\tif notifyErr := auth.NotifySuccess(ctx, csr, certRep.Certificate, transactionID); notifyErr != nil {\n\t\t// TODO(hs): ignore this error case? It's not critical if the notification fails; but logging it might be good\n\t\t_ = notifyErr\n\t}\n\n\tres := Response{\n\t\tOperation:   opnPKIOperation,\n\t\tData:        certRep.Raw,\n\t\tCertificate: certRep.Certificate,\n\t}\n\n\treturn res, nil\n}\n\nfunc formatCapabilities(caps []string) []byte {\n\treturn []byte(strings.Join(caps, \"\\r\\n\"))\n}\n\n// writeResponse writes a SCEP response back to the SCEP client.\nfunc writeResponse(w http.ResponseWriter, r *http.Request, res Response) {\n\tif res.Error != nil {\n\t\tlog.Error(w, r, res.Error)\n\t}\n\n\tif res.Certificate != nil {\n\t\tapi.LogCertificate(w, res.Certificate)\n\t}\n\n\tw.Header().Set(\"Content-Type\", contentHeader(res))\n\t_, _ = w.Write(res.Data)\n}\n\nfunc fail(w http.ResponseWriter, r *http.Request, err error) {\n\tlog.Error(w, r, err)\n\n\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n}\n\nfunc createFailureResponse(ctx context.Context, csr *x509.CertificateRequest, msg *scep.PKIMessage, info smallscep.FailInfo, infoText string, failError error) (Response, error) {\n\tauth := scep.MustFromContext(ctx)\n\tcertRepMsg, err := auth.CreateFailureResponse(ctx, csr, msg, scep.FailInfoName(info), infoText)\n\tif err != nil {\n\t\treturn Response{}, err\n\t}\n\treturn Response{\n\t\tOperation: opnPKIOperation,\n\t\tData:      certRepMsg.Raw,\n\t\tError:     failError,\n\t}, nil\n}\n\nfunc contentHeader(r Response) string {\n\tswitch r.Operation {\n\tdefault:\n\t\treturn \"text/plain\"\n\tcase opnGetCACert:\n\t\tif r.CACertNum > 1 {\n\t\t\treturn \"application/x-x509-ca-ra-cert\"\n\t\t}\n\t\treturn \"application/x-x509-ca-cert\"\n\tcase opnPKIOperation:\n\t\treturn \"application/x-pki-message\"\n\t}\n}\n"
  },
  {
    "path": "scep/api/api_test.go",
    "content": "// Package api implements a SCEP HTTP server.\npackage api\n\nimport (\n\t\"bytes\"\n\t\"encoding/base64\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"strings\"\n\t\"testing\"\n\t\"testing/iotest\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc Test_decodeRequest(t *testing.T) {\n\trandomB64 := \"wx/1mQ49TpdLRfvVjQhXNSe8RB3hjZEarqYp5XVIxpSbvOhQSs8hP2TgucID1IputbA8JC6CbsUpcVae3+8hRNqs5pTsSHP2aNxsw8AHGSX9dZVymSclkUV8irk+ztfEfs7aLA==\"\n\texpectedRandom, err := base64.StdEncoding.DecodeString(randomB64)\n\trequire.NoError(t, err)\n\tweirdMacOSCase := \"wx/1mQ49TpdLRfvVjQhXNSe8RB3hjZEarqYp5XVIxpSbvOhQSs8hP2TgucID1IputbA8JC6CbsUpcVae3+8hRNqs5pTsSHP2aNxsw8AHGSX9dZVymSclkUV8irk+ztfEfs7aLA%3D%3D\"\n\texpectedWeirdMacOSCase, err := base64.StdEncoding.DecodeString(strings.ReplaceAll(weirdMacOSCase, \"%3D\", \"=\"))\n\trequire.NoError(t, err)\n\ttype args struct {\n\t\tr *http.Request\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\targs    args\n\t\twant    request\n\t\twantErr bool\n\t}{\n\t\t{\n\t\t\tname: \"fail/invalid-query\",\n\t\t\targs: args{\n\t\t\t\tr: httptest.NewRequest(http.MethodGet, \"http://scep:8080/?operation=bla;message=invalid-separator\", http.NoBody),\n\t\t\t},\n\t\t\twant:    request{},\n\t\t\twantErr: true,\n\t\t},\n\t\t{\n\t\t\tname: \"fail/empty-operation\",\n\t\t\targs: args{\n\t\t\t\tr: httptest.NewRequest(http.MethodGet, \"http://scep:8080/?operation=\", http.NoBody),\n\t\t\t},\n\t\t\twant:    request{},\n\t\t\twantErr: true,\n\t\t},\n\t\t{\n\t\t\tname: \"fail/unsupported-method\",\n\t\t\targs: args{\n\t\t\t\tr: httptest.NewRequest(http.MethodPatch, \"http://scep:8080/?operation=AnUnsupportOperation\", http.NoBody),\n\t\t\t},\n\t\t\twant:    request{},\n\t\t\twantErr: true,\n\t\t},\n\t\t{\n\t\t\tname: \"fail/get-unsupported-operation\",\n\t\t\targs: args{\n\t\t\t\tr: httptest.NewRequest(http.MethodGet, \"http://scep:8080/?operation=AnUnsupportOperation\", http.NoBody),\n\t\t\t},\n\t\t\twant:    request{},\n\t\t\twantErr: true,\n\t\t},\n\t\t{\n\t\t\tname: \"fail/get-PKIOperation-empty-message\",\n\t\t\targs: args{\n\t\t\t\tr: httptest.NewRequest(http.MethodGet, \"http://scep:8080/?operation=PKIOperation&message=\", http.NoBody),\n\t\t\t},\n\t\t\twant:    request{},\n\t\t\twantErr: true,\n\t\t},\n\t\t{\n\t\t\tname: \"fail/get-PKIOperation\",\n\t\t\targs: args{\n\t\t\t\tr: httptest.NewRequest(http.MethodGet, \"http://scep:8080/?operation=PKIOperation&message='somewronginput'\", http.NoBody),\n\t\t\t},\n\t\t\twant:    request{},\n\t\t\twantErr: true,\n\t\t},\n\t\t{\n\t\t\tname: \"fail/post-PKIOperation\",\n\t\t\targs: args{\n\t\t\t\tr: httptest.NewRequest(http.MethodPost, \"http://scep:8080/?operation=PKIOperation\", iotest.ErrReader(errors.New(\"a read error\"))),\n\t\t\t},\n\t\t\twant:    request{},\n\t\t\twantErr: true,\n\t\t},\n\t\t{\n\t\t\tname: \"ok/get-GetCACert\",\n\t\t\targs: args{\n\t\t\t\tr: httptest.NewRequest(http.MethodGet, \"http://scep:8080/?operation=GetCACert\", http.NoBody),\n\t\t\t},\n\t\t\twant: request{\n\t\t\t\tOperation: \"GetCACert\",\n\t\t\t\tMessage:   []byte{},\n\t\t\t},\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"ok/get-GetCACaps\",\n\t\t\targs: args{\n\t\t\t\tr: httptest.NewRequest(http.MethodGet, \"http://scep:8080/?operation=GetCACaps\", http.NoBody),\n\t\t\t},\n\t\t\twant: request{\n\t\t\t\tOperation: \"GetCACaps\",\n\t\t\t\tMessage:   []byte{},\n\t\t\t},\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"ok/get-PKIOperation\",\n\t\t\targs: args{\n\t\t\t\tr: httptest.NewRequest(http.MethodGet, \"http://scep:8080/?operation=PKIOperation&message=MTIzNA==\", http.NoBody),\n\t\t\t},\n\t\t\twant: request{\n\t\t\t\tOperation: \"PKIOperation\",\n\t\t\t\tMessage:   []byte(\"1234\"),\n\t\t\t},\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"ok/get-PKIOperation-escaped\",\n\t\t\targs: args{\n\t\t\t\tr: httptest.NewRequest(http.MethodGet, fmt.Sprintf(\"http://scep:8080/?operation=PKIOperation&message=%s\", url.QueryEscape(randomB64)), http.NoBody),\n\t\t\t},\n\t\t\twant: request{\n\t\t\t\tOperation: \"PKIOperation\",\n\t\t\t\tMessage:   expectedRandom,\n\t\t\t},\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"ok/get-PKIOperation-not-escaped\", // bit of a special case, but this is supported because of the macOS case now\n\t\t\targs: args{\n\t\t\t\tr: httptest.NewRequest(http.MethodGet, fmt.Sprintf(\"http://scep:8080/?operation=PKIOperation&message=%s\", randomB64), http.NoBody),\n\t\t\t},\n\t\t\twant: request{\n\t\t\t\tOperation: \"PKIOperation\",\n\t\t\t\tMessage:   expectedRandom,\n\t\t\t},\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"ok/get-PKIOperation-weird-macos-case\", // a special case for macOS, which seems to result in the message not arriving fully percent-encoded\n\t\t\targs: args{\n\t\t\t\tr: httptest.NewRequest(http.MethodGet, fmt.Sprintf(\"http://scep:8080/?operation=PKIOperation&message=%s\", weirdMacOSCase), http.NoBody),\n\t\t\t},\n\t\t\twant: request{\n\t\t\t\tOperation: \"PKIOperation\",\n\t\t\t\tMessage:   expectedWeirdMacOSCase,\n\t\t\t},\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"ok/post-PKIOperation\",\n\t\t\targs: args{\n\t\t\t\tr: httptest.NewRequest(http.MethodPost, \"http://scep:8080/?operation=PKIOperation\", bytes.NewBufferString(\"1234\")),\n\t\t\t},\n\t\t\twant: request{\n\t\t\t\tOperation: \"PKIOperation\",\n\t\t\t\tMessage:   []byte(\"1234\"),\n\t\t\t},\n\t\t\twantErr: false,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := decodeRequest(tt.args.r)\n\t\t\tif tt.wantErr {\n\t\t\t\tassert.Error(t, err)\n\t\t\t\tassert.Equal(t, tt.want, got)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, tt.want, got)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "scep/authority.go",
    "content": "package scep\n\nimport (\n\t\"context\"\n\t\"crypto\"\n\t\"crypto/x509\"\n\t\"errors\"\n\t\"fmt\"\n\t\"sync\"\n\n\t\"github.com/smallstep/pkcs7\"\n\tsmallscep \"github.com/smallstep/scep\"\n\tsmallscepx509util \"github.com/smallstep/scep/x509util\"\n\n\t\"go.step.sm/crypto/x509util\"\n\n\t\"github.com/smallstep/certificates/authority/provisioner\"\n)\n\n// Authority is the layer that handles all SCEP interactions.\ntype Authority struct {\n\tsignAuth             SignAuthority\n\troots                []*x509.Certificate\n\tintermediates        []*x509.Certificate\n\tdefaultSigner        crypto.Signer\n\tsignerCertificate    *x509.Certificate\n\tdefaultDecrypter     crypto.Decrypter\n\tdecrypterCertificate *x509.Certificate\n\tscepProvisionerNames []string\n\n\tprovisionersMutex        sync.RWMutex\n\tencryptionAlgorithmMutex sync.Mutex\n}\n\ntype authorityKey struct{}\n\n// NewContext adds the given authority to the context.\nfunc NewContext(ctx context.Context, a *Authority) context.Context {\n\treturn context.WithValue(ctx, authorityKey{}, a)\n}\n\n// FromContext returns the current authority from the given context.\nfunc FromContext(ctx context.Context) (a *Authority, ok bool) {\n\ta, ok = ctx.Value(authorityKey{}).(*Authority)\n\treturn\n}\n\n// MustFromContext returns the current authority from the given context. It will\n// panic if the authority is not in the context.\nfunc MustFromContext(ctx context.Context) *Authority {\n\tvar (\n\t\ta  *Authority\n\t\tok bool\n\t)\n\tif a, ok = FromContext(ctx); !ok {\n\t\tpanic(\"scep authority is not in the context\")\n\t}\n\treturn a\n}\n\n// SignAuthority is the interface for a signing authority\ntype SignAuthority interface {\n\tSignWithContext(ctx context.Context, cr *x509.CertificateRequest, opts provisioner.SignOptions, signOpts ...provisioner.SignOption) ([]*x509.Certificate, error)\n\tLoadProvisionerByName(string) (provisioner.Interface, error)\n}\n\n// New returns a new Authority that implements the SCEP interface.\nfunc New(signAuth SignAuthority, opts Options) (*Authority, error) {\n\tif err := opts.Validate(); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &Authority{\n\t\tsignAuth:             signAuth, // TODO: provide signAuth through context instead?\n\t\troots:                opts.Roots,\n\t\tintermediates:        opts.Intermediates,\n\t\tdefaultSigner:        opts.Signer,\n\t\tsignerCertificate:    opts.SignerCert,\n\t\tdefaultDecrypter:     opts.Decrypter,\n\t\tdecrypterCertificate: opts.SignerCert, // the intermediate signer cert is also the decrypter cert (if RSA)\n\t\tscepProvisionerNames: opts.SCEPProvisionerNames,\n\t}, nil\n}\n\n// Validate validates if the SCEP Authority has a valid configuration.\n// The validation includes a check if a decrypter is available, either\n// an authority wide decrypter, or a provisioner specific decrypter.\nfunc (a *Authority) Validate() error {\n\tif a == nil {\n\t\treturn nil\n\t}\n\n\ta.provisionersMutex.RLock()\n\tdefer a.provisionersMutex.RUnlock()\n\n\tnoDefaultDecrypterAvailable := a.defaultDecrypter == nil\n\tfor _, name := range a.scepProvisionerNames {\n\t\tp, err := a.LoadProvisionerByName(name)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed loading provisioner %q: %w\", name, err)\n\t\t}\n\t\tif scepProv, ok := p.(*provisioner.SCEP); ok {\n\t\t\tcert, decrypter := scepProv.GetDecrypter()\n\t\t\t// TODO(hs): return sentinel/typed error, to be able to ignore/log these cases during init?\n\t\t\tif cert == nil && noDefaultDecrypterAvailable {\n\t\t\t\treturn fmt.Errorf(\"SCEP provisioner %q does not have a decrypter certificate\", name)\n\t\t\t}\n\t\t\tif decrypter == nil && noDefaultDecrypterAvailable {\n\t\t\t\treturn fmt.Errorf(\"SCEP provisioner %q does not have decrypter\", name)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// UpdateProvisioners updates the SCEP Authority with the new, and hopefully\n// current SCEP provisioners configured. This allows the Authority to be\n// validated with the latest data.\nfunc (a *Authority) UpdateProvisioners(scepProvisionerNames []string) {\n\tif a == nil {\n\t\treturn\n\t}\n\n\ta.provisionersMutex.Lock()\n\tdefer a.provisionersMutex.Unlock()\n\n\ta.scepProvisionerNames = scepProvisionerNames\n}\n\nvar (\n\t// TODO: check the default capabilities; https://tools.ietf.org/html/rfc8894#section-3.5.2\n\tdefaultCapabilities = []string{\n\t\t\"Renewal\", // NOTE: removing this will result in macOS SCEP client stating the server doesn't support renewal, but it uses PKCSreq to do so.\n\t\t\"SHA-1\",\n\t\t\"SHA-256\",\n\t\t\"AES\",\n\t\t\"DES3\",\n\t\t\"SCEPStandard\",\n\t\t\"POSTPKIOperation\",\n\t}\n)\n\n// LoadProvisionerByName calls out to the SignAuthority interface to load a\n// provisioner by name.\nfunc (a *Authority) LoadProvisionerByName(name string) (provisioner.Interface, error) {\n\treturn a.signAuth.LoadProvisionerByName(name)\n}\n\n// GetCACertificates returns the certificate (chain) for the CA.\n//\n// This methods returns the \"SCEP Server (RA)\" certificate, the issuing CA up to and excl. the root.\n// Some clients do need the root certificate however; also see: https://github.com/openxpki/openxpki/issues/73\n//\n// In case a provisioner specific decrypter is available, this is used as the \"SCEP Server (RA)\" certificate\n// instead of the CA intermediate directly. This uses a distinct instance of a KMS for doing the SCEP key\n// operations, so that RSA can be used for just SCEP.\n//\n// Using an RA does not seem to exist in https://tools.ietf.org/html/rfc8894, but is mentioned in\n// https://tools.ietf.org/id/draft-nourse-scep-21.html.\nfunc (a *Authority) GetCACertificates(ctx context.Context) (certs []*x509.Certificate, err error) {\n\tp := provisionerFromContext(ctx)\n\n\t// if a provisioner specific RSA decrypter is available, it is returned as\n\t// the first certificate.\n\tif decrypterCertificate, _ := p.GetDecrypter(); decrypterCertificate != nil {\n\t\tcerts = append(certs, decrypterCertificate)\n\t}\n\n\t// the CA intermediate is added to the chain by default. It's possible to\n\t// exclude it from being added through configuration. This can be useful in\n\t// environments where the SCEP client doesn't select the right RSA decrypter\n\t// certificate, resulting in the wrong recipient in the PKCS7 message.\n\tif p.ShouldIncludeIntermediateInChain() || len(certs) == 0 {\n\t\t// TODO(hs): ensure logic is in place that checks the signer is the first\n\t\t// intermediate and that there are no double certificates.\n\t\tcerts = append(certs, a.intermediates...)\n\t}\n\n\t// the CA roots are added for completeness when configured to do so. Clients\n\t// are responsible to select the right cert(s) to store and use.\n\tif p.ShouldIncludeRootInChain() {\n\t\tcerts = append(certs, a.roots...)\n\t}\n\n\treturn certs, nil\n}\n\n// DecryptPKIEnvelope decrypts an enveloped message\nfunc (a *Authority) DecryptPKIEnvelope(ctx context.Context, msg *PKIMessage) error {\n\tp7c, err := pkcs7.Parse(msg.P7.Content)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error parsing pkcs7 content: %w\", err)\n\t}\n\n\tcert, decrypter, err := a.selectDecrypter(ctx)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed selecting decrypter: %w\", err)\n\t}\n\n\tenvelope, err := p7c.Decrypt(cert, decrypter)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error decrypting encrypted pkcs7 content: %w\", err)\n\t}\n\n\tmsg.pkiEnvelope = envelope\n\n\tswitch msg.MessageType {\n\tcase smallscep.CertRep:\n\t\tcerts, err := smallscep.CACerts(msg.pkiEnvelope)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error extracting CA certs from pkcs7 degenerate data: %w\", err)\n\t\t}\n\t\tmsg.CertRepMessage.Certificate = certs[0]\n\t\treturn nil\n\tcase smallscep.PKCSReq, smallscep.RenewalReq:\n\t\tcsr, err := x509.ParseCertificateRequest(msg.pkiEnvelope)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"parse CSR from pkiEnvelope: %w\", err)\n\t\t}\n\t\tif err := csr.CheckSignature(); err != nil {\n\t\t\treturn fmt.Errorf(\"invalid CSR signature; %w\", err)\n\t\t}\n\t\t// extract the challenge password\n\t\tcp, err := smallscepx509util.ParseChallengePassword(msg.pkiEnvelope)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"parse challenge password in pkiEnvelope: %w\", err)\n\t\t}\n\t\tmsg.CSRReqMessage = &smallscep.CSRReqMessage{\n\t\t\tRawDecrypted:      msg.pkiEnvelope,\n\t\t\tCSR:               csr,\n\t\t\tChallengePassword: cp,\n\t\t}\n\t\treturn nil\n\tdefault:\n\t\t// this is for e.g. GetCRL, GetCert and CertPoll\n\t\treturn errors.New(\"not implemented\")\n\t}\n}\n\n// SignCSR creates an x509.Certificate based on a CSR template and Cert Authority credentials\n// returns a new PKIMessage with CertRep data\nfunc (a *Authority) SignCSR(ctx context.Context, csr *x509.CertificateRequest, msg *PKIMessage, signCSROpts ...provisioner.SignCSROption) (*PKIMessage, error) {\n\t// TODO: intermediate storage of the request? In SCEP it's possible to request a csr/certificate\n\t// to be signed, which can be performed asynchronously / out-of-band. In that case a client can\n\t// poll for the status. It seems to be similar as what can happen in ACME, so might want to model\n\t// the implementation after the one in the ACME authority. Requires storage, etc.\n\n\tp := provisionerFromContext(ctx)\n\n\t// check if CSRReqMessage has already been decrypted\n\tif msg.CSRReqMessage.CSR == nil {\n\t\tif err := a.DecryptPKIEnvelope(ctx, msg); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tcsr = msg.CSRReqMessage.CSR\n\t}\n\n\t// Template data\n\tsans := []string{}\n\tsans = append(sans, csr.DNSNames...)\n\tsans = append(sans, csr.EmailAddresses...)\n\tfor _, v := range csr.IPAddresses {\n\t\tsans = append(sans, v.String())\n\t}\n\tfor _, v := range csr.URIs {\n\t\tsans = append(sans, v.String())\n\t}\n\tif len(sans) == 0 {\n\t\tsans = append(sans, csr.Subject.CommonName)\n\t}\n\tdata := x509util.CreateTemplateData(csr.Subject.CommonName, sans)\n\tdata.SetCertificateRequest(csr)\n\tdata.SetSubject(x509util.Subject{\n\t\tCountry:            csr.Subject.Country,\n\t\tOrganization:       csr.Subject.Organization,\n\t\tOrganizationalUnit: csr.Subject.OrganizationalUnit,\n\t\tLocality:           csr.Subject.Locality,\n\t\tProvince:           csr.Subject.Province,\n\t\tStreetAddress:      csr.Subject.StreetAddress,\n\t\tPostalCode:         csr.Subject.PostalCode,\n\t\tSerialNumber:       csr.Subject.SerialNumber,\n\t\tCommonName:         csr.Subject.CommonName,\n\t})\n\n\t// Apply CSR options. Currently only one option is defined.\n\tfor _, o := range signCSROpts {\n\t\tif m, ok := o.(provisioner.TemplateDataModifier); ok {\n\t\t\tm.Modify(data)\n\t\t}\n\t}\n\n\t// Get authorizations from the SCEP provisioner.\n\tctx = provisioner.NewContextWithMethod(ctx, provisioner.SignMethod)\n\tsignOps, err := p.AuthorizeSign(ctx, \"\")\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error retrieving authorization options from SCEP provisioner: %w\", err)\n\t}\n\t// Unlike most of the provisioners, scep's AuthorizeSign method doesn't\n\t// define the templates, and the template data used in WebHooks is not\n\t// available.\n\tfor _, signOp := range signOps {\n\t\tif wc, ok := signOp.(*provisioner.WebhookController); ok {\n\t\t\twc.TemplateData = data\n\t\t}\n\t}\n\n\topts := provisioner.SignOptions{}\n\ttemplateOptions, err := provisioner.TemplateOptions(p.GetOptions(), data)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error creating template options from SCEP provisioner: %w\", err)\n\t}\n\tsignOps = append(signOps, templateOptions)\n\n\tcertChain, err := a.signAuth.SignWithContext(ctx, csr, opts, signOps...)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error generating certificate: %w\", err)\n\t}\n\n\t// take the issued certificate (only); https://tools.ietf.org/html/rfc8894#section-3.3.2\n\tcert := certChain[0]\n\n\t// and create a degenerate cert structure\n\tdeg, err := smallscep.DegenerateCertificates([]*x509.Certificate{cert})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed generating degenerate certificate: %w\", err)\n\t}\n\n\te7, err := a.encrypt(deg, msg.P7.Certificates, p.GetContentEncryptionAlgorithm())\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed encrypting degenerate certificate: %w\", err)\n\t}\n\n\t// PKIMessageAttributes to be signed\n\tconfig := pkcs7.SignerInfoConfig{\n\t\tExtraSignedAttributes: []pkcs7.Attribute{\n\t\t\t{\n\t\t\t\tType:  oidSCEPtransactionID,\n\t\t\t\tValue: msg.TransactionID,\n\t\t\t},\n\t\t\t{\n\t\t\t\tType:  oidSCEPpkiStatus,\n\t\t\t\tValue: smallscep.SUCCESS,\n\t\t\t},\n\t\t\t{\n\t\t\t\tType:  oidSCEPmessageType,\n\t\t\t\tValue: smallscep.CertRep,\n\t\t\t},\n\t\t\t{\n\t\t\t\tType:  oidSCEPrecipientNonce,\n\t\t\t\tValue: msg.SenderNonce,\n\t\t\t},\n\t\t\t{\n\t\t\t\tType:  oidSCEPsenderNonce,\n\t\t\t\tValue: msg.SenderNonce,\n\t\t\t},\n\t\t},\n\t}\n\n\tsignedData, err := pkcs7.NewSignedData(e7)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// add the certificate into the signed data type\n\t// this cert must be added before the signedData because the recipient will expect it\n\t// as the first certificate in the array\n\tsignedData.AddCertificate(cert)\n\n\tsignerCert, signer, err := a.selectSigner(ctx)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed selecting signer: %w\", err)\n\t}\n\n\t// sign the attributes\n\tif err := signedData.AddSigner(signerCert, signer, config); err != nil {\n\t\treturn nil, err\n\t}\n\n\tcertRepBytes, err := signedData.Finish()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tcr := &CertRepMessage{\n\t\tPKIStatus:      smallscep.SUCCESS,\n\t\tRecipientNonce: smallscep.RecipientNonce(msg.SenderNonce),\n\t\tCertificate:    cert,\n\t\tdegenerate:     deg,\n\t}\n\n\t// create a CertRep message from the original\n\tcrepMsg := &PKIMessage{\n\t\tRaw:            certRepBytes,\n\t\tTransactionID:  msg.TransactionID,\n\t\tMessageType:    smallscep.CertRep,\n\t\tCertRepMessage: cr,\n\t}\n\n\treturn crepMsg, nil\n}\n\nfunc (a *Authority) encrypt(content []byte, recipients []*x509.Certificate, algorithm int) ([]byte, error) {\n\t// apparently the pkcs7 library uses a global default setting for the content encryption\n\t// algorithm to use when en- or decrypting data. We need to restore the current setting after\n\t// the cryptographic operation, so that other usages of the library are not influenced by\n\t// this call to Encrypt(). We are not required to use the same algorithm the SCEP client uses.\n\ta.encryptionAlgorithmMutex.Lock()\n\tdefer a.encryptionAlgorithmMutex.Unlock()\n\n\tencryptionAlgorithmToRestore := pkcs7.ContentEncryptionAlgorithm\n\tdefer func() {\n\t\tpkcs7.ContentEncryptionAlgorithm = encryptionAlgorithmToRestore\n\t}()\n\n\tpkcs7.ContentEncryptionAlgorithm = algorithm\n\te7, err := pkcs7.Encrypt(content, recipients)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn e7, nil\n}\n\n// CreateFailureResponse creates an appropriately signed reply for PKI operations\nfunc (a *Authority) CreateFailureResponse(ctx context.Context, _ *x509.CertificateRequest, msg *PKIMessage, info FailInfoName, infoText string) (*PKIMessage, error) {\n\tconfig := pkcs7.SignerInfoConfig{\n\t\tExtraSignedAttributes: []pkcs7.Attribute{\n\t\t\t{\n\t\t\t\tType:  oidSCEPtransactionID,\n\t\t\t\tValue: msg.TransactionID,\n\t\t\t},\n\t\t\t{\n\t\t\t\tType:  oidSCEPpkiStatus,\n\t\t\t\tValue: smallscep.FAILURE,\n\t\t\t},\n\t\t\t{\n\t\t\t\tType:  oidSCEPfailInfo,\n\t\t\t\tValue: info,\n\t\t\t},\n\t\t\t{\n\t\t\t\tType:  oidSCEPfailInfoText,\n\t\t\t\tValue: infoText,\n\t\t\t},\n\t\t\t{\n\t\t\t\tType:  oidSCEPmessageType,\n\t\t\t\tValue: smallscep.CertRep,\n\t\t\t},\n\t\t\t{\n\t\t\t\tType:  oidSCEPsenderNonce,\n\t\t\t\tValue: msg.SenderNonce,\n\t\t\t},\n\t\t\t{\n\t\t\t\tType:  oidSCEPrecipientNonce,\n\t\t\t\tValue: msg.SenderNonce,\n\t\t\t},\n\t\t},\n\t}\n\n\tsignedData, err := pkcs7.NewSignedData(nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tsignerCert, signer, err := a.selectSigner(ctx)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed selecting signer: %w\", err)\n\t}\n\n\t// sign the attributes\n\tif err := signedData.AddSigner(signerCert, signer, config); err != nil {\n\t\treturn nil, err\n\t}\n\n\tcertRepBytes, err := signedData.Finish()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tcr := &CertRepMessage{\n\t\tPKIStatus:      smallscep.FAILURE,\n\t\tFailInfo:       smallscep.FailInfo(info),\n\t\tRecipientNonce: smallscep.RecipientNonce(msg.SenderNonce),\n\t}\n\n\t// create a CertRep message from the original\n\tcrepMsg := &PKIMessage{\n\t\tRaw:            certRepBytes,\n\t\tTransactionID:  msg.TransactionID,\n\t\tMessageType:    smallscep.CertRep,\n\t\tCertRepMessage: cr,\n\t}\n\n\treturn crepMsg, nil\n}\n\n// GetCACaps returns the CA capabilities\nfunc (a *Authority) GetCACaps(ctx context.Context) []string {\n\tp := provisionerFromContext(ctx)\n\n\tcaps := p.GetCapabilities()\n\tif len(caps) == 0 {\n\t\treturn defaultCapabilities\n\t}\n\n\t// TODO: validate the caps? Ensure they are the right format according to RFC?\n\t// TODO: ensure that the capabilities are actually \"enforced\"/\"verified\" in code too:\n\t// check that only parts of the spec are used in the implementation belonging to the capabilities.\n\t// For example for renewals, which we could disable in the provisioner, should then also\n\t// not be reported in cacaps operation.\n\n\treturn caps\n}\n\nfunc (a *Authority) ValidateChallenge(ctx context.Context, csr *x509.CertificateRequest, challenge, transactionID string) ([]provisioner.SignCSROption, error) {\n\tp := provisionerFromContext(ctx)\n\treturn p.ValidateChallenge(ctx, csr, challenge, transactionID)\n}\n\nfunc (a *Authority) NotifySuccess(ctx context.Context, csr *x509.CertificateRequest, cert *x509.Certificate, transactionID string) error {\n\tp := provisionerFromContext(ctx)\n\treturn p.NotifySuccess(ctx, csr, cert, transactionID)\n}\n\nfunc (a *Authority) NotifyFailure(ctx context.Context, csr *x509.CertificateRequest, transactionID string, errorCode int, errorDescription string) error {\n\tp := provisionerFromContext(ctx)\n\treturn p.NotifyFailure(ctx, csr, transactionID, errorCode, errorDescription)\n}\n\nfunc (a *Authority) selectDecrypter(ctx context.Context) (cert *x509.Certificate, decrypter crypto.Decrypter, err error) {\n\tp := provisionerFromContext(ctx)\n\tcert, decrypter = p.GetDecrypter()\n\tswitch {\n\tcase cert != nil && decrypter != nil:\n\t\treturn\n\tcase cert == nil && decrypter != nil:\n\t\treturn nil, nil, fmt.Errorf(\"provisioner %q does not have a decrypter certificate available\", p.GetName())\n\tcase cert != nil && decrypter == nil:\n\t\treturn nil, nil, fmt.Errorf(\"provisioner %q does not have a decrypter available\", p.GetName())\n\t}\n\n\tcert, decrypter = a.decrypterCertificate, a.defaultDecrypter\n\tswitch {\n\tcase cert == nil && decrypter != nil:\n\t\treturn nil, nil, fmt.Errorf(\"provisioner %q does not have a default decrypter certificate available\", p.GetName())\n\tcase cert != nil && decrypter == nil:\n\t\treturn nil, nil, fmt.Errorf(\"provisioner %q does not have a default decrypter available\", p.GetName())\n\t}\n\n\treturn\n}\n\nfunc (a *Authority) selectSigner(ctx context.Context) (cert *x509.Certificate, signer crypto.Signer, err error) {\n\tp := provisionerFromContext(ctx)\n\tcert, signer = p.GetSigner()\n\tswitch {\n\tcase cert != nil && signer != nil:\n\t\treturn\n\tcase cert == nil && signer != nil:\n\t\treturn nil, nil, fmt.Errorf(\"provisioner %q does not have a signer certificate available\", p.GetName())\n\tcase cert != nil && signer == nil:\n\t\treturn nil, nil, fmt.Errorf(\"provisioner %q does not have a signer available\", p.GetName())\n\t}\n\n\tcert, signer = a.signerCertificate, a.defaultSigner\n\tswitch {\n\tcase cert == nil && signer != nil:\n\t\treturn nil, nil, fmt.Errorf(\"provisioner %q does not have a default signer certificate available\", p.GetName())\n\tcase cert != nil && signer == nil:\n\t\treturn nil, nil, fmt.Errorf(\"provisioner %q does not have a default signer available\", p.GetName())\n\t}\n\n\treturn\n}\n"
  },
  {
    "path": "scep/authority_test.go",
    "content": "package scep\n\nimport (\n\t\"context\"\n\t\"crypto\"\n\t\"crypto/rand\"\n\t\"crypto/rsa\"\n\t\"crypto/x509\"\n\t\"crypto/x509/pkix\"\n\t\"encoding/pem\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/smallstep/certificates/authority/config\"\n\t\"github.com/smallstep/certificates/authority/provisioner\"\n\t\"github.com/smallstep/linkedca\"\n\t\"github.com/smallstep/pkcs7\"\n\t\"github.com/smallstep/scep\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"go.step.sm/crypto/keyutil\"\n\t\"go.step.sm/crypto/minica\"\n\t\"go.step.sm/crypto/randutil\"\n\t\"go.step.sm/crypto/x509util\"\n)\n\nfunc generateContent(t *testing.T, size int) []byte {\n\tt.Helper()\n\tb, err := randutil.Bytes(size)\n\trequire.NoError(t, err)\n\treturn b\n}\n\nfunc generateRecipients(t *testing.T) []*x509.Certificate {\n\tca, err := minica.New()\n\trequire.NoError(t, err)\n\ts, err := keyutil.GenerateSigner(\"RSA\", \"\", 2048)\n\trequire.NoError(t, err)\n\ttmpl := &x509.Certificate{\n\t\tPublicKey: s.Public(),\n\t\tSubject:   pkix.Name{CommonName: \"Test PKCS#7 Encryption\"},\n\t}\n\tcert, err := ca.Sign(tmpl)\n\trequire.NoError(t, err)\n\treturn []*x509.Certificate{cert}\n}\n\nfunc TestAuthority_encrypt(t *testing.T) {\n\tt.Parallel()\n\ta := &Authority{}\n\trecipients := generateRecipients(t)\n\ttype args struct {\n\t\tcontent    []byte\n\t\trecipients []*x509.Certificate\n\t\talgorithm  int\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\targs    args\n\t\twantErr bool\n\t}{\n\t\t{\"alg-0\", args{generateContent(t, 32), recipients, pkcs7.EncryptionAlgorithmDESCBC}, false},\n\t\t{\"alg-1\", args{generateContent(t, 32), recipients, pkcs7.EncryptionAlgorithmAES128CBC}, false},\n\t\t{\"alg-2\", args{generateContent(t, 32), recipients, pkcs7.EncryptionAlgorithmAES256CBC}, false},\n\t\t{\"alg-3\", args{generateContent(t, 32), recipients, pkcs7.EncryptionAlgorithmAES128GCM}, false},\n\t\t{\"alg-4\", args{generateContent(t, 32), recipients, pkcs7.EncryptionAlgorithmAES256GCM}, false},\n\t\t{\"alg-unknown\", args{generateContent(t, 32), recipients, 42}, true},\n\t}\n\tfor _, tt := range tests {\n\t\ttc := tt\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tgot, err := a.encrypt(tc.args.content, tc.args.recipients, tc.args.algorithm)\n\t\t\tif tc.wantErr {\n\t\t\t\tassert.Error(t, err)\n\t\t\t\tassert.Nil(t, got)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.NotEmpty(t, got)\n\t\t})\n\t}\n}\n\ntype signAuthority struct {\n\tca       *minica.CA\n\twebhooks []*provisioner.Webhook\n\ttemplate string\n}\n\nfunc (s *signAuthority) SignWithContext(ctx context.Context, cr *x509.CertificateRequest, opts provisioner.SignOptions, signOpts ...provisioner.SignOption) ([]*x509.Certificate, error) {\n\tvar certOptions []x509util.Option\n\tfor _, so := range signOpts {\n\t\tif co, ok := so.(provisioner.CertificateOptions); ok {\n\t\t\tcertOptions = append(certOptions, co.Options(opts)...)\n\t\t}\n\t}\n\tc, err := x509util.NewCertificate(cr, certOptions...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tcrt, err := s.ca.Sign(c.GetCertificate())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn []*x509.Certificate{crt, s.ca.Intermediate}, nil\n}\n\nfunc (s *signAuthority) LoadProvisionerByName(string) (provisioner.Interface, error) {\n\tp := &provisioner.SCEP{\n\t\tName:              \"scep\",\n\t\tType:              \"SCEP\",\n\t\tChallengePassword: \"password\",\n\t\tDecrypterCertificate: pem.EncodeToMemory(&pem.Block{\n\t\t\tType:  \"CERTIFICATE\",\n\t\t\tBytes: s.ca.Intermediate.Raw,\n\t\t}),\n\t\tDecrypterKeyPEM: pem.EncodeToMemory(&pem.Block{\n\t\t\tType:  \"RSA PRIVATE KEY\",\n\t\t\tBytes: x509.MarshalPKCS1PrivateKey(s.ca.Signer.(*rsa.PrivateKey)),\n\t\t}),\n\t\tOptions: &provisioner.Options{\n\t\t\tWebhooks: s.webhooks,\n\t\t\tX509: &provisioner.X509Options{\n\t\t\t\tTemplate: s.template,\n\t\t\t},\n\t\t},\n\t}\n\tif err := p.Init(provisioner.Config{\n\t\tClaims: config.GlobalProvisionerClaims,\n\t}); err != nil {\n\t\treturn nil, err\n\t}\n\treturn p, nil\n}\n\nfunc TestAuthority_SignCSR(t *testing.T) {\n\tca, err := minica.New(minica.WithGetSignerFunc(func() (crypto.Signer, error) {\n\t\treturn rsa.GenerateKey(rand.Reader, 2048)\n\t}))\n\trequire.NoError(t, err)\n\n\tsa := &signAuthority{\n\t\tca: ca,\n\t\twebhooks: []*provisioner.Webhook{{\n\t\t\tID:       \"1f81b7ed-62c4-4dd5-b63a-348e92b2e25d\",\n\t\t\tName:     \"ScepChallenge\",\n\t\t\tKind:     linkedca.Webhook_SCEPCHALLENGE.String(),\n\t\t\tCertType: linkedca.Webhook_X509.String(),\n\t\t\tURL:      \"https://not.used\",\n\t\t\tSecret:   \"MTIzNAo=\",\n\t\t}},\n\t\ttemplate: `{\n{{- with .Webhooks.ScepChallenge.CommonName }}\n\t\"subject\": {\"commonName\" : {{ . | toJson }}},\n{{- else }}\n\t\"subject\": {{ toJson .Subject }},\n{{- end }}\n{{- with .Webhooks.ScepChallenge.Email }}\n\t\"emailAddresses\" : [ {{ . | toJson }} ],\n{{- else }}\n\t\"sans\": {{ toJson .SANs }},\n{{- end }}\n{{- if typeIs \"*rsa.PublicKey\" .Insecure.CR.PublicKey }}\n\t\"keyUsage\": [\"keyEncipherment\", \"digitalSignature\"],\n{{- else }}\n\t\"keyUsage\": [\"digitalSignature\"],\n{{- end }}\n\t\"extKeyUsage\": [\"serverAuth\", \"clientAuth\"]\n}`,\n\t}\n\n\ta1, err := New(sa, Options{\n\t\tRoots:                []*x509.Certificate{ca.Root},\n\t\tIntermediates:        []*x509.Certificate{ca.Intermediate},\n\t\tSignerCert:           ca.Intermediate,\n\t\tSigner:               ca.Signer,\n\t\tDecrypter:            ca.Signer.(*rsa.PrivateKey),\n\t\tDecrypterCert:        ca.Intermediate,\n\t\tSCEPProvisionerNames: []string{\"scep\"},\n\t})\n\trequire.NoError(t, err)\n\n\tp1, err := a1.LoadProvisionerByName(\"scep\")\n\trequire.NoError(t, err)\n\n\tctx := NewProvisionerContext(context.Background(), p1.(*provisioner.SCEP))\n\n\tsigner, err := keyutil.GenerateDefaultSigner()\n\trequire.NoError(t, err)\n\tcsr, err := x509util.CreateCertificateRequest(\"jane@example.com\", []string{\"urn:uuid:81d19787-cfe8-4a04-82b2-8827f3727235\"}, signer)\n\trequire.NoError(t, err)\n\n\ttype args struct {\n\t\tctx         context.Context\n\t\tcsr         *x509.CertificateRequest\n\t\tmsg         *PKIMessage\n\t\tsignCSROpts []provisioner.SignCSROption\n\t}\n\ttests := []struct {\n\t\tname      string\n\t\tauthority *Authority\n\t\targs      args\n\t\tvalidate  func(*testing.T, *PKIMessage)\n\t\tassertion assert.ErrorAssertionFunc\n\t}{\n\t\t{\"ok\", a1, args{ctx, csr, &PKIMessage{\n\t\t\tCSRReqMessage: &scep.CSRReqMessage{CSR: csr},\n\t\t\tP7: &pkcs7.PKCS7{\n\t\t\t\tCertificates: []*x509.Certificate{ca.Intermediate},\n\t\t\t},\n\t\t}, []provisioner.SignCSROption{\n\t\t\tprovisioner.TemplateDataModifierFunc(func(data x509util.TemplateData) {\n\t\t\t\tdata.SetWebhook(\"ScepChallenge\", map[string]any{\n\t\t\t\t\t\"CommonName\": \"Jane C.\",\n\t\t\t\t\t\"Email\":      \"jane@example.com\",\n\t\t\t\t})\n\t\t\t}),\n\t\t}}, func(t *testing.T, p *PKIMessage) {\n\t\t\trequire.NotNil(t, p.CertRepMessage)\n\t\t\tcert, err := x509.ParseCertificate(p.Certificate.Raw)\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, \"Jane C.\", cert.Subject.CommonName)\n\t\t\tassert.Equal(t, []string{\"jane@example.com\"}, cert.EmailAddresses)\n\t\t\tassert.Nil(t, cert.URIs)\n\t\t}, assert.NoError},\n\t\t{\"ok no sign options\", a1, args{ctx, csr, &PKIMessage{\n\t\t\tCSRReqMessage: &scep.CSRReqMessage{CSR: csr},\n\t\t\tP7: &pkcs7.PKCS7{\n\t\t\t\tCertificates: []*x509.Certificate{ca.Intermediate},\n\t\t\t},\n\t\t}, []provisioner.SignCSROption{}}, func(t *testing.T, p *PKIMessage) {\n\t\t\trequire.NotNil(t, p.CertRepMessage)\n\t\t\tcert, err := x509.ParseCertificate(p.Certificate.Raw)\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, \"jane@example.com\", cert.Subject.CommonName)\n\t\t\tassert.Nil(t, cert.EmailAddresses)\n\t\t\tassert.Equal(t, []*url.URL{{Scheme: \"urn\", Opaque: \"uuid:81d19787-cfe8-4a04-82b2-8827f3727235\"}}, cert.URIs)\n\t\t}, assert.NoError},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := tt.authority.SignCSR(tt.args.ctx, tt.args.csr, tt.args.msg, tt.args.signCSROpts...)\n\t\t\ttt.assertion(t, err)\n\t\t\ttt.validate(t, got)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "scep/options.go",
    "content": "package scep\n\nimport (\n\t\"crypto\"\n\t\"crypto/rsa\"\n\t\"crypto/x509\"\n\t\"errors\"\n)\n\ntype Options struct {\n\t// Roots contains the (federated) CA roots certificate(s)\n\tRoots []*x509.Certificate `json:\"-\"`\n\t// Intermediates points issuer certificate, along with any other bundled certificates\n\t// to be returned in the chain for consumers.\n\tIntermediates []*x509.Certificate `json:\"-\"`\n\t// SignerCert points to the certificate of the CA signer. It usually is the same as the\n\t// first certificate in the CertificateChain.\n\tSignerCert *x509.Certificate `json:\"-\"`\n\t// Signer signs CSRs in SCEP. Configured in the ca.json key property.\n\tSigner crypto.Signer `json:\"-\"`\n\t// Decrypter decrypts encrypted SCEP messages. Configured in the ca.json key property.\n\tDecrypter crypto.Decrypter `json:\"-\"`\n\t// DecrypterCert points to the certificate of the CA decrypter.\n\tDecrypterCert *x509.Certificate `json:\"-\"`\n\t// SCEPProvisionerNames contains the currently configured SCEP provioner names. These\n\t// are used to be able to load the provisioners when the SCEP authority is being\n\t// validated.\n\tSCEPProvisionerNames []string\n}\n\ntype comparablePublicKey interface {\n\tEqual(crypto.PublicKey) bool\n}\n\n// Validate checks the fields in Options.\nfunc (o *Options) Validate() error {\n\tswitch {\n\tcase len(o.Intermediates) == 0:\n\t\treturn errors.New(\"no intermediate certificate available for SCEP authority\")\n\tcase o.SignerCert == nil:\n\t\treturn errors.New(\"no signer certificate available for SCEP authority\")\n\t}\n\n\t// the signer is optional, but if it's set, its public key must match the signer\n\t// certificate public key.\n\tif o.Signer != nil {\n\t\t// check if the signer (intermediate CA) certificate has the same public key as\n\t\t// the signer. According to the RFC it seems valid to have different keys for\n\t\t// the intermediate and the CA signing new certificates, so this might change\n\t\t// in the future.\n\t\tsignerPublicKey := o.Signer.Public().(comparablePublicKey)\n\t\tif !signerPublicKey.Equal(o.SignerCert.PublicKey) {\n\t\t\treturn errors.New(\"mismatch between signer certificate and public key\")\n\t\t}\n\t}\n\n\t// decrypter can be nil in case a signing only key is used; validation complete.\n\tif o.Decrypter == nil {\n\t\treturn nil\n\t}\n\n\t// If a decrypter is available, check that it's backed by an RSA key. According to the\n\t// RFC: https://tools.ietf.org/html/rfc8894#section-3.1, SCEP can be used with something\n\t// different than RSA, but requires the encryption to be performed using the challenge\n\t// password in that case. An older version of specification states that only RSA is\n\t// supported: https://tools.ietf.org/html/draft-nourse-scep-23#section-2.1.1. Other\n\t// algorithms do not seem to be supported in certnanny/sscep, but it might work\n\t// in micromdm/scep. Currently only RSA is allowed, but it might be an option\n\t// to try other algorithms in the future.\n\tdecrypterPublicKey, ok := o.Decrypter.Public().(*rsa.PublicKey)\n\tif !ok {\n\t\treturn errors.New(\"only RSA keys are (currently) supported as decrypters\")\n\t}\n\n\t// check if intermediate public key is the same as the decrypter public key.\n\t// In certnanny/sscep it's mentioned that the signing key can be different\n\t// from the decrypting (and encrypting) key. These options are only used and\n\t// validated when the intermediate CA is also used as the decrypter, though,\n\t// so they should match.\n\tif !decrypterPublicKey.Equal(o.SignerCert.PublicKey) {\n\t\treturn errors.New(\"mismatch between certificate chain and decrypter public keys\")\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "scep/provisioner.go",
    "content": "package scep\n\nimport (\n\t\"context\"\n\t\"crypto\"\n\t\"crypto/x509\"\n\n\t\"github.com/smallstep/certificates/authority/provisioner\"\n)\n\n// Provisioner is an interface that embeds the\n// provisioner.Interface and adds some SCEP specific\n// functions.\ntype Provisioner interface {\n\tprovisioner.Interface\n\tGetOptions() *provisioner.Options\n\tGetCapabilities() []string\n\tShouldIncludeRootInChain() bool\n\tShouldIncludeIntermediateInChain() bool\n\tGetDecrypter() (*x509.Certificate, crypto.Decrypter)\n\tGetSigner() (*x509.Certificate, crypto.Signer)\n\tGetContentEncryptionAlgorithm() int\n\tValidateChallenge(ctx context.Context, csr *x509.CertificateRequest, challenge, transactionID string) ([]provisioner.SignCSROption, error)\n\tNotifySuccess(ctx context.Context, csr *x509.CertificateRequest, cert *x509.Certificate, transactionID string) error\n\tNotifyFailure(ctx context.Context, csr *x509.CertificateRequest, transactionID string, errorCode int, errorDescription string) error\n}\n\n// provisionerKey is the key type for storing and searching a\n// SCEP provisioner in the context.\ntype provisionerKey struct{}\n\n// provisionerFromContext searches the context for a SCEP provisioner.\n// Returns the provisioner or panics if no SCEP provisioner is found.\nfunc provisionerFromContext(ctx context.Context) Provisioner {\n\tp, ok := ctx.Value(provisionerKey{}).(Provisioner)\n\tif !ok {\n\t\tpanic(\"SCEP provisioner expected in request context\")\n\t}\n\treturn p\n}\n\nfunc NewProvisionerContext(ctx context.Context, p Provisioner) context.Context {\n\treturn context.WithValue(ctx, provisionerKey{}, p)\n}\n"
  },
  {
    "path": "scep/scep.go",
    "content": "// Package scep implements Simple Certificate Enrollment Protocol related functionality.\npackage scep\n\nimport (\n\t\"crypto/x509\"\n\t\"encoding/asn1\"\n\n\t\"github.com/smallstep/pkcs7\"\n\tsmallscep \"github.com/smallstep/scep\"\n)\n\nfunc init() {\n\t// enable the fallback X509 certificate parser to support parsing\n\t// Windows SCEP enrollment certificates that contain a critical\n\t// authority key identifier extension. Starting with Go 1.23 those\n\t// fail to be parsed by crypto/x509. Enabling the legacy fallback\n\t// parser is a workaround for that.\n\tpkcs7.SetFallbackLegacyX509CertificateParserEnabled(true)\n}\n\n// FailInfoName models the name/value of failInfo\ntype FailInfoName smallscep.FailInfo\n\n// FailInfo models a failInfo object consisting of a\n// name/identifier and a failInfoText, the latter of\n// which can be more descriptive and is intended to be\n// read by humans.\ntype FailInfo struct {\n\tName FailInfoName\n\tText string\n}\n\n// SCEP OIDs\nvar (\n\toidSCEPmessageType    = asn1.ObjectIdentifier{2, 16, 840, 1, 113733, 1, 9, 2}\n\toidSCEPpkiStatus      = asn1.ObjectIdentifier{2, 16, 840, 1, 113733, 1, 9, 3}\n\toidSCEPfailInfo       = asn1.ObjectIdentifier{2, 16, 840, 1, 113733, 1, 9, 4}\n\toidSCEPsenderNonce    = asn1.ObjectIdentifier{2, 16, 840, 1, 113733, 1, 9, 5}\n\toidSCEPrecipientNonce = asn1.ObjectIdentifier{2, 16, 840, 1, 113733, 1, 9, 6}\n\toidSCEPtransactionID  = asn1.ObjectIdentifier{2, 16, 840, 1, 113733, 1, 9, 7}\n\toidSCEPfailInfoText   = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 24, 1}\n\t//oidChallengePassword  = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 7}\n)\n\n// PKIMessage defines the possible SCEP message types\ntype PKIMessage struct {\n\tsmallscep.TransactionID\n\tsmallscep.MessageType\n\tsmallscep.SenderNonce\n\t*smallscep.CSRReqMessage\n\n\t*CertRepMessage\n\n\t// DER Encoded PKIMessage\n\tRaw []byte\n\n\t// parsed\n\tP7 *pkcs7.PKCS7\n\n\t// decrypted enveloped content\n\tpkiEnvelope []byte\n\n\t// Used to sign message\n\tRecipients []*x509.Certificate\n}\n\n// CertRepMessage is a type of PKIMessage\ntype CertRepMessage struct {\n\tsmallscep.PKIStatus\n\tsmallscep.RecipientNonce\n\tsmallscep.FailInfo\n\n\tCertificate *x509.Certificate\n\n\tdegenerate []byte\n}\n"
  },
  {
    "path": "scripts/README.md",
    "content": "# Scripts folder\n\nPlease note that `install-step-ra.sh` is referenced on the `files.smallstep.com` S3 website bucket as a redirect to `raw.githubusercontent.com`. If you move it, please update the S3 redirect.\n\n## badger-migration\n\nbadger-migration is a tool that allows migrating data from BadgerDB (v1 or\nv2) to MySQL or PostgreSQL.\n"
  },
  {
    "path": "scripts/badger-migration/main.go",
    "content": "package main\n\nimport (\n\t\"bytes\"\n\t\"encoding/base64\"\n\t\"encoding/binary\"\n\t\"errors\"\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\n\tbadgerv1 \"github.com/dgraph-io/badger\"\n\tbadgerv2 \"github.com/dgraph-io/badger/v2\"\n\n\t\"github.com/smallstep/certificates/internal/cast\"\n\t\"github.com/smallstep/nosql\"\n)\n\nvar (\n\tauthorityTables = []string{\n\t\t\"x509_certs\",\n\t\t\"x509_certs_data\",\n\t\t\"revoked_x509_certs\",\n\t\t\"x509_crl\",\n\t\t\"revoked_ssh_certs\",\n\t\t\"used_ott\",\n\t\t\"ssh_certs\",\n\t\t\"ssh_hosts\",\n\t\t\"ssh_users\",\n\t\t\"ssh_host_principals\",\n\t}\n\tacmeTables = []string{\n\t\t\"acme_accounts\",\n\t\t\"acme_keyID_accountID_index\",\n\t\t\"acme_authzs\",\n\t\t\"acme_challenges\",\n\t\t\"nonces\",\n\t\t\"acme_orders\",\n\t\t\"acme_account_orders_index\",\n\t\t\"acme_certs\",\n\t\t\"acme_serial_certs_index\",\n\t\t\"acme_external_account_keys\",\n\t\t\"acme_external_account_keyID_reference_index\",\n\t\t\"acme_external_account_keyID_provisionerID_index\",\n\t}\n\tadminTables = []string{\n\t\t\"admins\",\n\t\t\"provisioners\",\n\t\t\"authority_policies\",\n\t}\n)\n\ntype DB interface {\n\tCreateTable([]byte) error\n\tSet(bucket, key, value []byte) error\n}\n\ntype dryRunDB struct{}\n\nfunc (*dryRunDB) CreateTable([]byte) error { return nil }\n\nfunc (*dryRunDB) Set(bucket, key, value []byte) error { return nil }\n\nfunc usage(fs *flag.FlagSet) {\n\tname := filepath.Base(os.Args[0])\n\tfmt.Fprintf(os.Stderr, \"%s is a tool to migrate data from BadgerDB to MySQL or PostgreSQL.\\n\", name)\n\tfmt.Fprintln(os.Stderr, \"\\nUsage:\")\n\tfmt.Fprintf(os.Stderr, \"  %s [-v1|-v2] -dir=<path> [-value-dir=<path>] -type=type -database=<source>\\n\", name)\n\tfmt.Fprintln(os.Stderr, \"\\nExamples:\")\n\tfmt.Fprintf(os.Stderr, \"  %s -v1 -dir /var/lib/step-ca/db -type=mysql -database \\\"user@unix/step_ca\\\"\\n\", name)\n\tfmt.Fprintf(os.Stderr, \"  %s -v1 -dir /var/lib/step-ca/db -type=mysql -database \\\"user:password@tcp(localhost:3306)/step_ca\\\"\\n\", name)\n\tfmt.Fprintf(os.Stderr, \"  %s -v2 -dir /var/lib/step-ca/db -type=postgresql -database \\\"user=postgres dbname=step_ca\\\"\\n\", name)\n\tfmt.Fprintf(os.Stderr, \"  %s -v2 -dir /var/lib/step-ca/db -dry-run\\\"\\n\", name)\n\tfmt.Fprintln(os.Stderr, \"\\nOptions:\")\n\tfs.PrintDefaults()\n}\n\nfunc main() {\n\tvar v1, v2, dryRun bool\n\tvar dir, valueDir string\n\tvar typ, database string\n\tvar key string\n\n\tfs := flag.NewFlagSet(os.Args[0], flag.ExitOnError)\n\n\tfs.BoolVar(&v1, \"v1\", false, \"use badger v1 as the source database\")\n\tfs.BoolVar(&v2, \"v2\", false, \"use badger v2 as the source database\")\n\tfs.StringVar(&dir, \"dir\", \"\", \"badger database directory\")\n\tfs.StringVar(&valueDir, \"value-dir\", \"\", \"badger database value directory\")\n\tfs.StringVar(&typ, \"type\", \"\", \"the destination database type to use\")\n\tfs.StringVar(&database, \"database\", \"\", \"the destination driver-specific data source name\")\n\tfs.StringVar(&key, \"key\", \"\", \"the key used to resume the migration\")\n\tfs.BoolVar(&dryRun, \"dry-run\", false, \"runs the migration scripts without writing anything\")\n\tfs.Usage = func() { usage(fs) }\n\tfs.Parse(os.Args[1:])\n\n\tswitch {\n\tcase v1 == v2:\n\t\tfatal(\"flag -v1 or -v2 are required\")\n\tcase dir == \"\":\n\t\tfatal(\"flag -dir is required\")\n\tcase typ != \"postgresql\" && typ != \"mysql\" && !dryRun:\n\t\tfatal(`flag -type must be \"postgresql\" or \"mysql\"`)\n\tcase database == \"\" && !dryRun:\n\t\tfatal(\"flag --database required\")\n\t}\n\n\tvar (\n\t\terr     error\n\t\tv1DB    *badgerv1.DB\n\t\tv2DB    *badgerv2.DB\n\t\tlastKey []byte\n\t)\n\n\tif key != \"\" {\n\t\tif lastKey, err = base64.StdEncoding.DecodeString(key); err != nil {\n\t\t\tfatal(\"error decoding key: %v\", err)\n\t\t}\n\t}\n\n\tif v1 {\n\t\tif v1DB, err = badgerV1Open(dir, valueDir); err != nil {\n\t\t\tfatal(\"error opening badger v1 database: %v\", err)\n\t\t}\n\t} else {\n\t\tif v2DB, err = badgerV2Open(dir, valueDir); err != nil {\n\t\t\tfatal(\"error opening badger v2 database: %v\", err)\n\t\t}\n\t}\n\n\tvar db DB\n\tif dryRun {\n\t\tdb = &dryRunDB{}\n\t} else {\n\t\tdb, err = nosql.New(typ, database)\n\t\tif err != nil {\n\t\t\tfatal(\"error opening %s database: %v\", typ, err)\n\t\t}\n\t}\n\n\tallTables := append([]string{}, authorityTables...)\n\tallTables = append(allTables, acmeTables...)\n\tallTables = append(allTables, adminTables...)\n\n\t// Convert prefix names to badger key prefixes\n\tbadgerKeys := make([][]byte, len(allTables))\n\tfor i, name := range allTables {\n\t\tbadgerKeys[i], err = badgerEncode([]byte(name))\n\t\tif err != nil {\n\t\t\tfatal(\"error encoding table %s: %v\", name, err)\n\t\t}\n\t}\n\n\tfor i, prefix := range badgerKeys {\n\t\ttable := allTables[i]\n\n\t\t// With a key flag, resume from that table and prefix\n\t\tif lastKey != nil {\n\t\t\tbucket, _ := parseBadgerEncode(lastKey)\n\t\t\tif table != string(bucket) {\n\t\t\t\tfmt.Printf(\"skipping table %s\\n\", table)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\t// Continue with a new prefix\n\t\t\tprefix = lastKey\n\t\t\tlastKey = nil\n\t\t}\n\n\t\tvar n int64\n\t\tfmt.Printf(\"migrating %s ...\", table)\n\t\tif err := db.CreateTable([]byte(table)); err != nil {\n\t\t\tfatal(\"error creating table %s: %v\", table, err)\n\t\t}\n\n\t\tif v1 {\n\t\t\tif badgerKey, err := badgerV1Iterate(v1DB, prefix, func(bucket, key, value []byte) error {\n\t\t\t\tn++\n\t\t\t\treturn db.Set(bucket, key, value)\n\t\t\t}); err != nil {\n\t\t\t\tfmt.Println()\n\t\t\t\tfatal(\"error inserting into %s: %v\\nLast key: %s\", table, err, base64.StdEncoding.EncodeToString(badgerKey))\n\t\t\t}\n\t\t} else {\n\t\t\tif badgerKey, err := badgerV2Iterate(v2DB, prefix, func(bucket, key, value []byte) error {\n\t\t\t\tn++\n\t\t\t\treturn db.Set(bucket, key, value)\n\t\t\t}); err != nil {\n\t\t\t\tfmt.Println()\n\t\t\t\tfatal(\"error inserting into %s: %v\\nLast key: %s\", table, err, base64.StdEncoding.EncodeToString(badgerKey))\n\t\t\t}\n\t\t}\n\n\t\tfmt.Printf(\" %d rows\\n\", n)\n\t}\n}\n\nfunc fatal(format string, args ...any) {\n\tfmt.Fprintf(os.Stderr, format, args...)\n\tfmt.Fprintln(os.Stderr)\n\tos.Exit(1)\n}\n\nfunc badgerV1Open(dir, valueDir string) (*badgerv1.DB, error) {\n\topts := badgerv1.DefaultOptions(dir)\n\tif valueDir != \"\" {\n\t\topts.ValueDir = valueDir\n\t}\n\treturn badgerv1.Open(opts)\n}\n\nfunc badgerV2Open(dir, valueDir string) (*badgerv2.DB, error) {\n\topts := badgerv2.DefaultOptions(dir)\n\tif valueDir != \"\" {\n\t\topts.ValueDir = valueDir\n\t}\n\treturn badgerv2.Open(opts)\n}\n\ntype Iterator interface {\n\tSeek([]byte)\n\tValidForPrefix([]byte) bool\n\tNext()\n}\n\ntype Item interface {\n\tKeyCopy([]byte) []byte\n\tValueCopy([]byte) ([]byte, error)\n}\n\nfunc badgerV1Iterate(db *badgerv1.DB, prefix []byte, fn func(bucket, key, value []byte) error) (badgerKey []byte, err error) {\n\terr = db.View(func(txn *badgerv1.Txn) error {\n\t\tit := txn.NewIterator(badgerv1.DefaultIteratorOptions)\n\t\tdefer it.Close()\n\t\tbadgerKey, err = badgerIterate(it, prefix, fn)\n\t\treturn err\n\t})\n\treturn\n}\n\nfunc badgerV2Iterate(db *badgerv2.DB, prefix []byte, fn func(bucket, key, value []byte) error) (badgerKey []byte, err error) {\n\terr = db.View(func(txn *badgerv2.Txn) error {\n\t\tit := txn.NewIterator(badgerv2.DefaultIteratorOptions)\n\t\tdefer it.Close()\n\t\tbadgerKey, err = badgerIterate(it, prefix, fn)\n\t\treturn err\n\t})\n\treturn\n}\n\nfunc badgerIterate(it Iterator, prefix []byte, fn func(bucket, key, value []byte) error) ([]byte, error) {\n\tvar badgerKey []byte\n\tfor it.Seek(prefix); it.ValidForPrefix(prefix); it.Next() {\n\t\tvar item Item\n\t\tswitch itt := it.(type) {\n\t\tcase *badgerv1.Iterator:\n\t\t\titem = itt.Item()\n\t\tcase *badgerv2.Iterator:\n\t\t\titem = itt.Item()\n\t\tdefault:\n\t\t\treturn badgerKey, fmt.Errorf(\"unexpected iterator type %T\", it)\n\t\t}\n\n\t\tbadgerKey = item.KeyCopy(nil)\n\t\tif isBadgerTable(badgerKey) {\n\t\t\tcontinue\n\t\t}\n\n\t\tbucket, key, err := fromBadgerKey(badgerKey)\n\t\tif err != nil {\n\t\t\treturn badgerKey, fmt.Errorf(\"error converting from badger key %s\", badgerKey)\n\t\t}\n\t\tvalue, err := item.ValueCopy(nil)\n\t\tif err != nil {\n\t\t\treturn badgerKey, fmt.Errorf(\"error retrieving contents from database value: %w\", err)\n\t\t}\n\n\t\tif err := fn(bucket, key, value); err != nil {\n\t\t\treturn badgerKey, fmt.Errorf(\"error exporting %s[%s]=%x\", bucket, key, value)\n\t\t}\n\t}\n\n\treturn badgerKey, nil\n}\n\n// badgerEncode encodes a byte slice into a section of a BadgerKey. See\n// documentation for toBadgerKey.\nfunc badgerEncode(val []byte) ([]byte, error) {\n\tl := len(val)\n\tswitch {\n\tcase l == 0:\n\t\treturn nil, errors.New(\"input cannot be empty\")\n\tcase l > 65535:\n\t\treturn nil, errors.New(\"length of input cannot be greater than 65535\")\n\tdefault:\n\t\tlb := new(bytes.Buffer)\n\t\tif err := binary.Write(lb, binary.LittleEndian, uint16(l)); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"error doing binary Write: %w\", err)\n\t\t}\n\t\treturn append(lb.Bytes(), val...), nil\n\t}\n}\n\n// parseBadgerEncode decodes the badger key and returns the bucket and the rest.\nfunc parseBadgerEncode(bk []byte) (value, rest []byte) {\n\tvar (\n\t\tkeyLen uint16\n\t\tstart  = uint16(2)\n\t\tlength = cast.Uint16(len(bk))\n\t)\n\tif cast.Uint16(len(bk)) < start {\n\t\treturn nil, bk\n\t}\n\t// First 2 bytes stores the length of the value.\n\tif err := binary.Read(bytes.NewReader(bk[:2]), binary.LittleEndian, &keyLen); err != nil {\n\t\treturn nil, bk\n\t}\n\tend := start + keyLen\n\tswitch {\n\tcase length < end:\n\t\treturn nil, bk\n\tcase length == end:\n\t\treturn bk[start:end], nil\n\tdefault:\n\t\treturn bk[start:end], bk[end:]\n\t}\n}\n\n// isBadgerTable returns True if the slice is a badgerTable token, false\n// otherwise. badgerTable means that the slice contains only the [size|value] of\n// one section of a badgerKey and no remainder. A badgerKey is [bucket|key],\n// while a badgerTable is only the bucket section.\nfunc isBadgerTable(bk []byte) bool {\n\tif k, rest := parseBadgerEncode(bk); len(k) > 0 && len(rest) == 0 {\n\t\treturn true\n\t}\n\treturn false\n}\n\n// fromBadgerKey returns the bucket and key encoded in a BadgerKey. See\n// documentation for toBadgerKey.\nfunc fromBadgerKey(bk []byte) ([]byte, []byte, error) {\n\tbucket, rest := parseBadgerEncode(bk)\n\tif len(bucket) == 0 || len(rest) == 0 {\n\t\treturn nil, nil, fmt.Errorf(\"invalid badger key: %v\", bk)\n\t}\n\n\tkey, rest2 := parseBadgerEncode(rest)\n\tif len(key) == 0 || len(rest2) != 0 {\n\t\treturn nil, nil, fmt.Errorf(\"invalid badger key: %v\", bk)\n\t}\n\n\treturn bucket, key, nil\n}\n"
  },
  {
    "path": "scripts/install-step-ra.sh",
    "content": "#!/bin/bash\nset -e\n\n# TODO:\n# - Parse params using argbash (argbash.io). Here's a template that I have tested but have not implemented yet:\n# \n# ARG_OPTIONAL_SINGLE([ca-url], , [the URL of the upstream (issuing) step-ca server])\n# ARG_OPTIONAL_SINGLE([fingerprint], , [the SHA256 fingerprint of the upstream peer step-ca server])\n# ARG_OPTIONAL_SINGLE([provisioner-name], , [the name of a JWK provisioner on the upstream CA that this RA will use])\n# ARG_OPTIONAL_SINGLE([provisioner-password-file], , [the name a file containing the upstream JWK provisioner password])\n# ARG_OPTIONAL_REPEATED([dns-name], , [DNS name of this RA that will appear on its TLS certificate; you may pass this flag multiple times])\n# ARG_OPTIONAL_SINGLE([listen-address], , [the address (and port #) this RA will listen on, eg. :443 or 127.0.0.1:4443])\n# ARG_HELP([This script will install and configure a Registration Authority that connects to an upstream CA running step-ca.])\n# ARGBASH_GO\n\necho \"This script will install and start a step-ca server running in Registration Authority (RA) mode.\"\necho \"\"\necho \"You will need an upstream CA (URL and fingerprint)\"\necho \"Don't have a CA? Sign up for a hosted CA at smallstep.com — or run your own.\"\necho \"\"\n\n# Fail if this script is not run as root.\nif ! [ $(id -u) = 0 ]; then\n  echo \"This script must be run as root\"\n  exit 1\nfi\n\n# Architecture detection\narch=$(uname -m)\ncase $arch in\n  x86_64) arch=\"amd64\" ;;\n  x86) arch=\"386\" ;;\n  i686) arch=\"386\" ;;\n  i386) arch=\"386\" ;;\n  aarch64) arch=\"arm64\" ;;\n  armv5*) arch=\"armv5\" ;;\n  armv6*) arch=\"armv6\" ;;\n  armv7*) arch=\"armv7\" ;;\nesac\n\nif ! hash jq &> /dev/null; then\n  echo \"This script requires the jq commmand; please install it.\"\n  exit 1\nfi\n\nif ! hash curl &> /dev/null; then\n  echo \"This script requires the curl commmand; please install it.\"\n  exit 1\nfi\n\nif ! hash tar &> /dev/null; then\n  echo \"This script requires the tar commmand; please install it.\"\n  exit 1\nfi\n\nwhile [ $# -gt 0 ]; do\n  case \"$1\" in\n    --ca-url)\n      CA_URL=\"$2\"\n      shift\n      shift\n      ;;\n    --fingerprint)\n      CA_FINGERPRINT=\"$2\"\n      shift\n      shift\n      ;;\n    --provisioner-name)\n      CA_PROVISIONER_NAME=\"$2\"\n      shift\n      shift\n      ;;\n    --provisioner-password-file)\n      CA_PROVISIONER_JWK_PASSWORD_FILE=\"$2\"\n      shift\n      shift\n      ;;\n    --dns-names)\n      RA_DNS_NAMES=\"$2\"\n      shift\n      shift\n      ;;\n    --listen-address)\n      RA_ADDRESS=\"$2\"\n      shift\n      shift\n      ;;\n    *)\n      shift\n      ;;\n  esac\ndone\n\n# Install step\nif ! hash step &> /dev/null; then\n  echo \"Installing 'step' in /usr/bin...\"\n  STEP_VERSION=$(curl -s https://api.github.com/repos/smallstep/cli/releases/latest | jq -r '.tag_name')\n\n  curl -sLO https://github.com/smallstep/cli/releases/download/$STEP_VERSION/step_linux_${STEP_VERSION:1}_$arch.tar.gz\n  tar xvzf step_linux_${STEP_VERSION:1}_$arch.tar.gz\n  install -m 0755 -t /usr/bin step_${STEP_VERSION:1}/bin/step\n\n  rm step_linux_${STEP_VERSION:1}_$arch.tar.gz\n  rm -rf step_${STEP_VERSION:1}\nfi\n\n# Prompt for required parameters\nif [ -z \"$CA_URL\" ]; then\n  CA_URL=\"\"\n  while [[ $CA_URL = \"\" ]]; do\n    read -p \"Issuing CA URL: \" CA_URL < /dev/tty\n  done\nfi\n\nif [ -z \"$CA_FINGERPRINT\" ]; then\n  CA_FINGERPRINT=\"\"\n  while [[ $CA_FINGERPRINT = \"\" ]]; do\n    read -p \"Issuing CA Fingerprint: \" CA_FINGERPRINT < /dev/tty\n  done\nfi\n\necho \"Bootstrapping with the CA...\"\nexport STEPPATH=$(mktemp -d)\n\nstep ca bootstrap --ca-url $CA_URL --fingerprint $CA_FINGERPRINT\n\nif [ -z \"$CA_PROVISIONER_NAME\" ]; then\n  declare -a provisioners\n  readarray -t provisioners < <(step ca provisioner list | jq -r '.[] | select(.type == \"JWK\") | .name')\n  printf '%s\\n' \"${provisioners[@]}\"\n\n  printf \"%b\" \"\\nSelect a JWK provisioner:\\n\" >&2\n  select provisioner in \"${provisioners[@]}\"; do\n    if [ -n \"$provisioner\" ]; then\n      echo \"Using existing provisioner $provisioner.\"\n      CA_PROVISIONER_NAME=$provisioner\n      break\n    else\n      echo \"Invalid selection!\"\n    fi\n  done\nfi\n\nif [ -z \"$RA_DNS_NAMES\" ]; then\n  RA_DNS_NAMES=\"\"\n  while [[ $RA_DNS_NAMES = \"\" ]]; do\n    echo \"What DNS names or IP addresses will your RA use?\"\n    read -p \"(e.g. acme.example.com[,1.1.1.1,etc.]): \" RA_DNS_NAMES < /dev/tty\n  done\nfi\n\n\ncount=0\nra_dns_names_quoted=\"\"\n\nfor i in ${RA_DNS_NAMES//,/ }\ndo\n  if [ \"$count\" = \"0\" ]; then\n    ra_dns_names_quoted=\"\\\"$i\\\"\"\n  else \n    ra_dns_names_quoted=\"${ra_dns_names_quoted}, \\\"$i\\\"\"\n  fi\n  count=$((count+1))\ndone\n\nif [ \"$count\" = \"0\" ]; then\n  echo \"You must supply at least one RA DNS name\"\n  exit 1\nfi\n\necho \"Got here\"\n\nif [ -z \"$RA_ADDRESS\" ]; then\n  RA_ADDRESS=\"\"\n  while [[ $RA_ADDRESS = \"\" ]] ; do\n    echo \"What address should your RA listen on?\"\n    read -p \"(e.g. :443 or 10.2.1.201:4430): \" RA_ADDRESS < /dev/tty\n  done\nfi\n\nif [ -z \"$CA_PROVISIONER_JWK_PASSWORD_FILE\" ]; then\n    read -s -p \"Enter the CA Provisioner Password: \" CA_PROVISIONER_JWK_PASSWORD < /dev/tty\n    printf \"%b\" \"\\n\"\nfi\n\necho \"Installing 'step-ca' in /usr/bin...\"\ncurl -sLO https://dl.smallstep.com/certificates/ra-installer/latest/step-ca_linux_$arch.tar.gz\ntar -xf step-ca_linux_$arch.tar.gz\ninstall -m 0755 -t /usr/bin step-ca_linux_$arch/step-ca\nsetcap CAP_NET_BIND_SERVICE=+eip $(which step-ca)\nrm step-ca_linux_$arch.tar.gz\nrm -rf step-ca_linux_$arch\n\necho \"Creating 'step' user...\"\nexport STEPPATH=/etc/step-ca\n\nuseradd --system --home $(step path) --shell /bin/false step\n\necho \"Creating RA configuration...\"\nmkdir -p $(step path)/db\nmkdir -p $(step path)/config\n\ncat <<EOF > $(step path)/config/ca.json\n{\n  \"address\": \"$RA_ADDRESS\",\n  \"dnsNames\": [$ra_dns_names_quoted],\n  \"db\": {\n    \"type\": \"badgerV2\",\n    \"dataSource\": \"/etc/step-ca/db\"\n  },\n  \"logger\": {\"format\": \"text\"},\n  \"authority\": {\n    \"type\": \"stepcas\",\n    \"certificateAuthority\": \"$CA_URL\",\n    \"certificateAuthorityFingerprint\": \"$CA_FINGERPRINT\",\n    \"certificateIssuer\": {\n      \"type\" : \"jwk\",\n      \"provisioner\": \"$CA_PROVISIONER_NAME\"\n    },\n    \"provisioners\": [{\n      \"type\": \"ACME\",\n      \"name\": \"acme\"\n    }]\n  },\n  \"tls\": {\n    \"cipherSuites\": [\n      \"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305\",\n       \"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256\"\n    ],\n    \"minVersion\": 1.2,\n    \"maxVersion\": 1.3,\n    \"renegotiation\": false\n  }\n}\nEOF\n\nif ! [ -z \"$CA_PROVISIONER_JWK_PASSWORD\" ]; then\n  echo \"Saving provisoiner password to $(step path)/password.txt...\"\n  echo $CA_PROVISIONER_JWK_PASSWORD > $(step path)/password.txt\nelse\n  echo \"Copying provisioner password file to $(step path)/password.txt...\"\n  cp $CA_PROVISIONER_JWK_PASSWORD_FILE $(step path)/password.txt\nfi\nchmod 440 $(step path)/password.txt\n\n# Add a service to systemd for the RA.\necho \"Creating systemd service step-ca.service...\"\ncurl -sL https://raw.githubusercontent.com/smallstep/certificates/master/systemd/step-ca.service \\\n     -o /etc/systemd/system/step-ca.service\n\necho \"Creating RA mode override /etc/systemd/system/step-ca.service.d/local.conf...\"\nmkdir /etc/systemd/system/step-ca.service.d\ncat <<EOF > /etc/systemd/system/step-ca.service.d/local.conf \n[Service]\n; The empty ExecStart= clears the inherited ExecStart= value\nExecStart=\nExecStart=/usr/bin/step-ca config/ca.json --issuer-password-file password.txt\nEOF\n\necho \"Starting step-ca.service...\"\nsystemctl daemon-reload\n\nchown -R step:step $(step path)\n\nsystemctl enable --now step-ca\n\necho \"Adding STEPPATH export to /root/.bash_profile...\"\necho \"export STEPPATH=$STEPPATH\" >> /root/.bash_profile\n\necho \"Finished. Check the journal with journalctl -fu step-ca.service\"\n\n"
  },
  {
    "path": "scripts/package-repo-import.sh",
    "content": "#!/usr/bin/env bash\n\nset -e\n\n: ${GCLOUD_LOCATION:=us-central1}\n: ${GCLOUD_RPM_REPO:=rpms}\n: ${GCLOUD_DEB_REPO:=debs}\n\nPACKAGE=\"${1}\"\nVERSION=\"${2}\"\nRELEASE=\"1\"\nEPOCH=\"0\"\nGORELEASER_PHASE=${GORELEASER_PHASE:-release}\n\necho \"Package: ${PACKAGE}\"\necho \"Version: ${VERSION}\"\n\ncheck_package() {\n  local EXITCODE=0\n  local REPO=\"${1}\"\n  local VER=\"${2}\"\n  if [ ! -f /tmp/version-deleted.stamp ]; then\n    gcloud artifacts versions list \\\n       --repository \"${REPO}\" \\\n       --location \"${GCLOUD_LOCATION}\" \\\n       --package \"${PACKAGE}\" \\\n       --filter \"VERSION:${VER}\" \\\n       --format json  2> /dev/null \\\n       | jq -re '.[].name?' >/dev/null 2>&1 \\\n       || EXITCODE=$?\n    if [[ \"${EXITCODE}\" -eq 0 ]]; then\n      echo \"Package version already exists. Removing it...\"\n      gcloud artifacts versions delete \\\n      --quiet \"${VER}\" \\\n      --package \"${PACKAGE}\" \\\n      --repository \"${REPO}\" \\\n      --location \"${GCLOUD_LOCATION}\"\n      touch /tmp/version-deleted.stamp\n    fi\n  fi\n}\n\nif [[ ${IS_PRERELEASE} == \"true\" ]]; then\n  echo \"Skipping artifact import; IS_PRERELEASE is 'true'\"\n  exit 0;\nfi\n\ncheck_package \"${GCLOUD_RPM_REPO}\" \"${EPOCH}:${VERSION}-${RELEASE}\"\ngcloud artifacts yum import \"${GCLOUD_RPM_REPO}\" \\\n  --location \"${GCLOUD_LOCATION}\" \\\n  --gcs-source \"gs://artifacts-outgoing/${PACKAGE}/rpm/${VERSION}/*\"\n\ncheck_package ${GCLOUD_DEB_REPO} \"${VERSION}-${RELEASE}\"}\ngcloud artifacts apt import \"${GCLOUD_DEB_REPO}\" \\\n  --location \"${GCLOUD_LOCATION}\" \\\n  --gcs-source \"gs://artifacts-outgoing/${PACKAGE}/deb/${VERSION}/*\"\n"
  },
  {
    "path": "scripts/package-upload.sh",
    "content": "#!/usr/bin/env bash\n\nset -e\nset -x\n\nFILE=\"${1}\"\nPACKAGE=\"${2}\"\nVERSION=\"${3}\"\n\necho \"Package File: ${FILE}\"\necho \"Package: ${PACKAGE}\"\necho \"Version: ${VERSION}\"\necho \"Release: ${RELEASE}\"\necho \"Location: ${GCLOUD_LOCATION}\"\n\nif [ \"${FILE: -4}\" == \".deb\" ]; then\n  if [[ \"${FILE}\" =~ \"armhf6\" ]]; then\n    echo \"Skipping ${FILE} due to GCP Artifact Registry armhf conflict!\"\n  else\n    gcloud storage cp ${FILE} gs://artifacts-outgoing/${PACKAGE}/deb/${VERSION}/\n  fi\nelse\n  gcloud storage cp ${FILE} gs://artifacts-outgoing/${PACKAGE}/rpm/${VERSION}/\nfi\n"
  },
  {
    "path": "server/server.go",
    "content": "package server\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"log\"\n\t\"net\"\n\t\"net/http\"\n\t\"os\"\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n)\n\n// ServerShutdownTimeout is the default time to wait before closing\n// connections on shutdown.\nconst ServerShutdownTimeout = 60 * time.Second\n\n// Server is a incomplete component that implements a basic HTTP/HTTPS\n// server.\ntype Server struct {\n\t*http.Server\n\tlistener   *net.TCPListener\n\treloadCh   chan net.Listener\n\tshutdownCh chan struct{}\n}\n\n// New creates a new HTTP/HTTPS server configured with the passed\n// address, http.Handler and tls.Config.\nfunc New(addr string, handler http.Handler, tlsConfig *tls.Config) *Server {\n\treturn &Server{\n\t\treloadCh:   make(chan net.Listener),\n\t\tshutdownCh: make(chan struct{}),\n\t\tServer:     newHTTPServer(addr, handler, tlsConfig),\n\t}\n}\n\n// newHTTPServer creates a new http.Server with the TCP address, handler and\n// tls.Config.\nfunc newHTTPServer(addr string, handler http.Handler, tlsConfig *tls.Config) *http.Server {\n\treturn &http.Server{\n\t\tAddr:              addr,\n\t\tHandler:           handler,\n\t\tTLSConfig:         tlsConfig,\n\t\tWriteTimeout:      15 * time.Second,\n\t\tReadTimeout:       15 * time.Second,\n\t\tReadHeaderTimeout: 15 * time.Second,\n\t\tIdleTimeout:       15 * time.Second,\n\t\tErrorLog:          log.New(os.Stderr, \"\", log.Ldate|log.Ltime|log.Llongfile),\n\t}\n}\n\n// ListenAndServe listens on the TCP network address srv.Addr and then calls\n// Serve to handle requests on incoming connections.\nfunc (srv *Server) ListenAndServe() error {\n\tln, err := net.Listen(\"tcp\", srv.Addr)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn srv.Serve(ln)\n}\n\n// Serve runs Serve or ServeTLS on the underlying http.Server and listen to\n// channels to reload or shutdown the server.\nfunc (srv *Server) Serve(ln net.Listener) error {\n\tvar err error\n\t// Store the current listener.\n\t// In reloads we'll create a copy of the underlying os.File so the close of the server one does not affect the copy.\n\tsrv.listener = ln.(*net.TCPListener)\n\n\tfor {\n\t\t// Start server\n\t\tif srv.TLSConfig == nil || (len(srv.TLSConfig.Certificates) == 0 && srv.TLSConfig.GetCertificate == nil) {\n\t\t\tlog.Printf(\"Serving HTTP on %s ...\", srv.Addr)\n\t\t\terr = srv.Server.Serve(ln)\n\t\t} else {\n\t\t\tlog.Printf(\"Serving HTTPS on %s ...\", srv.Addr)\n\t\t\terr = srv.Server.ServeTLS(ln, \"\", \"\")\n\t\t}\n\n\t\t// log unexpected errors\n\t\tif err != http.ErrServerClosed {\n\t\t\tlog.Println(errors.Wrap(err, \"unexpected error\"))\n\t\t}\n\n\t\tselect {\n\t\tcase ln = <-srv.reloadCh:\n\t\t\tsrv.listener = ln.(*net.TCPListener)\n\t\tcase <-srv.shutdownCh:\n\t\t\treturn http.ErrServerClosed\n\t\t}\n\t}\n}\n\n// Shutdown gracefully shuts down the server without interrupting any active\n// connections.\nfunc (srv *Server) Shutdown() error {\n\tctx, cancel := context.WithTimeout(context.Background(), ServerShutdownTimeout)\n\tdefer cancel()              // release resources if Shutdown ends before the timeout\n\tdefer close(srv.shutdownCh) // close shutdown channel\n\treturn srv.Server.Shutdown(ctx)\n}\n\nfunc (srv *Server) reloadShutdown() error {\n\tctx, cancel := context.WithTimeout(context.Background(), ServerShutdownTimeout)\n\tdefer cancel() // release resources if Shutdown ends before the timeout\n\treturn srv.Server.Shutdown(ctx)\n}\n\n// Reload reloads the current server with the configuration of the passed\n// server.\nfunc (srv *Server) Reload(ns *Server) error {\n\tvar err error\n\tvar ln net.Listener\n\n\tif srv.Addr != ns.Addr {\n\t\t// Open new address\n\t\tln, err = net.Listen(\"tcp\", ns.Addr)\n\t\tif err != nil {\n\t\t\treturn errors.WithStack(err)\n\t\t}\n\t} else {\n\t\t// Get a copy of the underlying os.File\n\t\tfd, err := srv.listener.File()\n\t\tif err != nil {\n\t\t\treturn errors.WithStack(err)\n\t\t}\n\t\t// Make sure to close the copy\n\t\tdefer fd.Close()\n\n\t\t// Creates a new listener copying fd\n\t\tln, err = net.FileListener(fd)\n\t\tif err != nil {\n\t\t\treturn errors.WithStack(err)\n\t\t}\n\t}\n\n\t// Close old server without sending a signal\n\tif err := srv.reloadShutdown(); err != nil {\n\t\treturn err\n\t}\n\n\t// Update old server\n\tsrv.Server = ns.Server\n\tsrv.reloadCh <- ln\n\treturn nil\n}\n\n// Forbidden writes on the http.ResponseWriter a text/plain forbidden\n// response.\nfunc (srv *Server) Forbidden(w http.ResponseWriter) {\n\theader := w.Header()\n\theader.Set(\"Content-Type\", \"text/plain; charset=utf-8\")\n\theader.Set(\"Content-Length\", \"11\")\n\tw.WriteHeader(http.StatusForbidden)\n\tw.Write([]byte(\"Forbidden.\\n\"))\n}\n"
  },
  {
    "path": "systemd/README.md",
    "content": "### Systemd unit files for `step-ca`\n\nFor documentation on `step-ca.service`, see [Running `step-ca` As A Daemon](https://smallstep.com/docs/step-ca/certificate-authority-server-production#running-step-ca-as-a-daemon).\n\nSee also: There is a systemd certificate renewal timer, in the [`systemd` directory of `smallstep/cli`](https://github.com/smallstep/cli/tree/master/systemd).\n"
  },
  {
    "path": "systemd/step-ca.service",
    "content": "[Unit]\nDescription=step-ca service\nDocumentation=https://smallstep.com/docs/step-ca\nDocumentation=https://smallstep.com/docs/step-ca/certificate-authority-server-production\nAfter=network-online.target\nWants=network-online.target\nStartLimitIntervalSec=30\nStartLimitBurst=3\nConditionFileNotEmpty=/etc/step-ca/config/ca.json\nConditionFileNotEmpty=/etc/step-ca/password.txt\n\n[Service]\nType=simple\nUser=step\nGroup=step\nEnvironment=STEPPATH=/etc/step-ca\nWorkingDirectory=/etc/step-ca\nExecStart=/usr/bin/step-ca config/ca.json --password-file password.txt\nExecReload=/bin/kill --signal HUP $MAINPID\nRestart=on-failure\nRestartSec=5\nTimeoutStopSec=30\nStartLimitInterval=30\nStartLimitBurst=3\n\n; Process capabilities & privileges\nAmbientCapabilities=CAP_NET_BIND_SERVICE\nCapabilityBoundingSet=CAP_NET_BIND_SERVICE\nSecureBits=keep-caps\nNoNewPrivileges=yes\n\n; Sandboxing\n; This sandboxing works with YubiKey PIV (via pcscd HTTP API), but it is likely\n; too restrictive for PKCS#11 HSMs.\n;\n; NOTE: Comment out the rest of this section for troubleshooting.\nProtectSystem=full\nProtectHome=true\nRestrictNamespaces=true\nRestrictAddressFamilies=AF_UNIX AF_INET AF_INET6\nPrivateTmp=true\nProtectClock=true\nProtectControlGroups=true\nProtectKernelTunables=true\nProtectKernelLogs=true\nProtectKernelModules=true\nLockPersonality=true\nRestrictSUIDSGID=true\nRemoveIPC=true\nRestrictRealtime=true\nPrivateDevices=true\nSystemCallFilter=@system-service\nSystemCallArchitectures=native\nMemoryDenyWriteExecute=true\nReadWriteDirectories=/etc/step-ca/db\n\n[Install]\nWantedBy=multi-user.target\n"
  },
  {
    "path": "templates/templates.go",
    "content": "package templates\n\nimport (\n\t\"bytes\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"sync\"\n\t\"text/template\"\n\n\t\"github.com/Masterminds/sprig/v3\"\n\t\"github.com/pkg/errors\"\n\n\t\"github.com/smallstep/cli-utils/fileutil\"\n\t\"github.com/smallstep/cli-utils/step\"\n)\n\n// TemplateType defines how a template will be written in disk.\ntype TemplateType string\n\nconst (\n\t// Snippet will mark a template as a part of a file.\n\tSnippet TemplateType = \"snippet\"\n\t// PrependLine is a template for prepending a single line to a file. If the\n\t// line already exists in the file it will be removed first.\n\tPrependLine TemplateType = \"prepend-line\"\n\t// File will mark a templates as a full file.\n\tFile TemplateType = \"file\"\n\t// Directory will mark a template as a directory.\n\tDirectory TemplateType = \"directory\"\n)\n\nvar (\n\tstepFuncMap  template.FuncMap\n\tstepFuncOnce sync.Once\n)\n\n// StepFuncMap returns sprig.TxtFuncMap but removing the \"env\" and \"expandenv\"\n// functions to avoid any leak of information.\nfunc StepFuncMap() template.FuncMap {\n\tstepFuncOnce.Do(func() {\n\t\tstepFuncMap = sprig.TxtFuncMap()\n\t\tdelete(stepFuncMap, \"env\")\n\t\tdelete(stepFuncMap, \"expandenv\")\n\t})\n\treturn stepFuncMap\n}\n\n// Templates is a collection of templates and variables.\ntype Templates struct {\n\tSSH  *SSHTemplates          `json:\"ssh,omitempty\"`\n\tData map[string]interface{} `json:\"data,omitempty\"`\n}\n\n// Validate returns an error if a template is not valid.\nfunc (t *Templates) Validate() (err error) {\n\tif t == nil {\n\t\treturn nil\n\t}\n\n\t// Validate members\n\tif err = t.SSH.Validate(); err != nil {\n\t\treturn\n\t}\n\n\t// Do not allow \"Step\" and \"User\"\n\tif t.Data != nil {\n\t\tif _, ok := t.Data[\"Step\"]; ok {\n\t\t\treturn errors.New(\"templates variables cannot contain 'Step' as a property\")\n\t\t}\n\t\tif _, ok := t.Data[\"User\"]; ok {\n\t\t\treturn errors.New(\"templates variables cannot contain 'User' as a property\")\n\t\t}\n\t}\n\treturn nil\n}\n\n// LoadAll preloads all templates in memory. It returns an error if an error is\n// found parsing at least one template.\nfunc LoadAll(t *Templates) (err error) {\n\tif t != nil {\n\t\tif t.SSH != nil {\n\t\t\tfor _, tt := range t.SSH.User {\n\t\t\t\tif err = tt.Load(); err != nil {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\t\t\tfor _, tt := range t.SSH.Host {\n\t\t\t\tif err = tt.Load(); err != nil {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\n// SSHTemplates contains the templates defining ssh configuration files.\ntype SSHTemplates struct {\n\tUser []Template `json:\"user\"`\n\tHost []Template `json:\"host\"`\n}\n\n// Validate returns an error if a template is not valid.\nfunc (t *SSHTemplates) Validate() (err error) {\n\tif t == nil {\n\t\treturn nil\n\t}\n\tfor _, tt := range t.User {\n\t\tif err = tt.Validate(); err != nil {\n\t\t\treturn\n\t\t}\n\t}\n\tfor _, tt := range t.Host {\n\t\tif err = tt.Validate(); err != nil {\n\t\t\treturn\n\t\t}\n\t}\n\treturn\n}\n\n// Template represents a template file.\ntype Template struct {\n\t*template.Template\n\tName         string       `json:\"name\"`\n\tType         TemplateType `json:\"type\"`\n\tTemplatePath string       `json:\"template\"`\n\tPath         string       `json:\"path\"`\n\tComment      string       `json:\"comment\"`\n\tRequiredData []string     `json:\"requires,omitempty\"`\n\tContent      []byte       `json:\"-\"`\n}\n\n// Validate returns an error if the template is not valid.\nfunc (t *Template) Validate() error {\n\tswitch {\n\tcase t == nil:\n\t\treturn nil\n\tcase t.Name == \"\":\n\t\treturn errors.New(\"template name cannot be empty\")\n\tcase t.Type != Snippet && t.Type != File && t.Type != Directory && t.Type != PrependLine:\n\t\treturn errors.Errorf(\"invalid template type %s, it must be %s, %s, %s, or %s\", t.Type, Snippet, PrependLine, File, Directory)\n\tcase t.TemplatePath == \"\" && t.Type != Directory && len(t.Content) == 0:\n\t\treturn errors.New(\"template template cannot be empty\")\n\tcase t.TemplatePath != \"\" && t.Type == Directory:\n\t\treturn errors.New(\"template template must be empty with directory type\")\n\tcase t.TemplatePath != \"\" && len(t.Content) > 0:\n\t\treturn errors.New(\"template template must be empty with content\")\n\tcase t.Path == \"\":\n\t\treturn errors.New(\"template path cannot be empty\")\n\t}\n\n\tif t.TemplatePath != \"\" {\n\t\t// Check for file\n\t\tst, err := os.Stat(step.Abs(t.TemplatePath))\n\t\tif err != nil {\n\t\t\treturn errors.Wrapf(err, \"error reading %s\", t.TemplatePath)\n\t\t}\n\t\tif st.IsDir() {\n\t\t\treturn errors.Errorf(\"error reading %s: is not a file\", t.TemplatePath)\n\t\t}\n\n\t\t// Defaults\n\t\tif t.Comment == \"\" {\n\t\t\tt.Comment = \"#\"\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// ValidateRequiredData checks that the given data contains all the keys\n// required.\nfunc (t *Template) ValidateRequiredData(data map[string]string) error {\n\tfor _, key := range t.RequiredData {\n\t\tif _, ok := data[key]; !ok {\n\t\t\treturn errors.Errorf(\"required variable '%s' is missing\", key)\n\t\t}\n\t}\n\treturn nil\n}\n\n// Load loads the template in memory, returns an error if the parsing of the\n// template fails.\nfunc (t *Template) Load() error {\n\tif t.Template == nil && t.Type != Directory {\n\t\tswitch {\n\t\tcase t.TemplatePath != \"\":\n\t\t\tfilename := step.Abs(t.TemplatePath)\n\t\t\tb, err := os.ReadFile(filename)\n\t\t\tif err != nil {\n\t\t\t\treturn errors.Wrapf(err, \"error reading %s\", filename)\n\t\t\t}\n\t\t\treturn t.LoadBytes(b)\n\t\tdefault:\n\t\t\treturn t.LoadBytes(t.Content)\n\t\t}\n\t}\n\treturn nil\n}\n\n// LoadBytes loads the template in memory, returns an error if the parsing of\n// the template fails.\nfunc (t *Template) LoadBytes(b []byte) error {\n\tt.backfill(b)\n\ttmpl, err := template.New(t.Name).Funcs(StepFuncMap()).Parse(string(b))\n\tif err != nil {\n\t\treturn errors.Wrapf(err, \"error parsing template %s\", t.Name)\n\t}\n\tt.Template = tmpl\n\treturn nil\n}\n\n// Render executes the template with the given data and returns the rendered\n// version.\nfunc (t *Template) Render(data interface{}) ([]byte, error) {\n\tif t.Type == Directory {\n\t\treturn nil, nil\n\t}\n\n\tif err := t.Load(); err != nil {\n\t\treturn nil, err\n\t}\n\n\tbuf := new(bytes.Buffer)\n\tif err := t.Execute(buf, data); err != nil {\n\t\treturn nil, errors.Wrapf(err, \"error executing %s\", t.TemplatePath)\n\t}\n\treturn buf.Bytes(), nil\n}\n\n// Output renders the template and returns a template.Output struct or an error.\nfunc (t *Template) Output(data interface{}) (Output, error) {\n\tb, err := t.Render(data)\n\tif err != nil {\n\t\treturn Output{}, err\n\t}\n\n\treturn Output{\n\t\tName:    t.Name,\n\t\tType:    t.Type,\n\t\tPath:    t.Path,\n\t\tComment: t.Comment,\n\t\tContent: b,\n\t}, nil\n}\n\n// backfill updates old templates with the required data.\nfunc (t *Template) backfill(b []byte) {\n\tif strings.EqualFold(t.Name, \"sshd_config.tpl\") && len(t.RequiredData) == 0 {\n\t\ta := bytes.TrimSpace(b)\n\t\tb := bytes.TrimSpace([]byte(DefaultSSHTemplateData[t.Name]))\n\t\tif bytes.Equal(a, b) {\n\t\t\tt.RequiredData = []string{\"Certificate\", \"Key\"}\n\t\t}\n\t}\n}\n\n// Output represents the text representation of a rendered template.\ntype Output struct {\n\tName    string       `json:\"name\"`\n\tType    TemplateType `json:\"type\"`\n\tPath    string       `json:\"path\"`\n\tComment string       `json:\"comment\"`\n\tContent []byte       `json:\"content\"`\n}\n\n// Write writes the Output to the filesystem as a directory, file or snippet.\nfunc (o *Output) Write() error {\n\t// Replace ${STEPPATH} with the base step path.\n\to.Path = strings.ReplaceAll(o.Path, \"${STEPPATH}\", step.BasePath())\n\n\tpath := step.Abs(o.Path)\n\tif o.Type == Directory {\n\t\treturn mkdir(path, 0700)\n\t}\n\n\tdir := filepath.Dir(path)\n\tif err := mkdir(dir, 0700); err != nil {\n\t\treturn err\n\t}\n\n\tswitch o.Type {\n\tcase File:\n\t\treturn fileutil.WriteFile(path, o.Content, 0600)\n\tcase Snippet:\n\t\treturn fileutil.WriteSnippet(path, o.Content, 0600)\n\tcase PrependLine:\n\t\treturn fileutil.PrependLine(path, o.Content, 0600)\n\tdefault:\n\t\t// Default to using a Snippet type if the type is not known.\n\t\treturn fileutil.WriteSnippet(path, o.Content, 0600)\n\t}\n}\n\nfunc mkdir(path string, perm os.FileMode) error {\n\tif err := os.MkdirAll(path, perm); err != nil {\n\t\treturn errors.Wrapf(err, \"error creating %s\", path)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "templates/templates_test.go",
    "content": "package templates\n\nimport (\n\t\"crypto/ecdsa\"\n\t\"crypto/elliptic\"\n\t\"crypto/rand\"\n\t\"encoding/base64\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"reflect\"\n\t\"testing\"\n\n\t\"github.com/smallstep/assert\"\n\t\"golang.org/x/crypto/ssh\"\n)\n\nfunc TestTemplates_Validate(t *testing.T) {\n\tsshTemplates := &SSHTemplates{\n\t\tUser: []Template{\n\t\t\t{Name: \"known_host.tpl\", Type: File, TemplatePath: \"../authority/testdata/templates/known_hosts.tpl\", Path: \"ssh/known_host\", Comment: \"#\"},\n\t\t},\n\t\tHost: []Template{\n\t\t\t{Name: \"ca.tpl\", Type: File, TemplatePath: \"../authority/testdata/templates/ca.tpl\", Path: \"/etc/ssh/ca.pub\", Comment: \"#\"},\n\t\t},\n\t}\n\ttype fields struct {\n\t\tSSH  *SSHTemplates\n\t\tData map[string]interface{}\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tfields  fields\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", fields{sshTemplates, nil}, false},\n\t\t{\"okWithData\", fields{sshTemplates, map[string]interface{}{\"Foo\": \"Bar\"}}, false},\n\t\t{\"badSSH\", fields{&SSHTemplates{User: []Template{{}}}, nil}, true},\n\t\t{\"badDataUser\", fields{sshTemplates, map[string]interface{}{\"User\": \"Bar\"}}, true},\n\t\t{\"badDataStep\", fields{sshTemplates, map[string]interface{}{\"Step\": \"Bar\"}}, true},\n\t}\n\tvar nilValue *Templates\n\tassert.NoError(t, nilValue.Validate())\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ttmpl := &Templates{\n\t\t\t\tSSH:  tt.fields.SSH,\n\t\t\t\tData: tt.fields.Data,\n\t\t\t}\n\t\t\tif err := tmpl.Validate(); (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"Templates.Validate() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSSHTemplates_Validate(t *testing.T) {\n\tuser := []Template{\n\t\t{Name: \"include.tpl\", Type: Snippet, TemplatePath: \"../authority/testdata/templates/include.tpl\", Path: \"~/.ssh/config\", Comment: \"#\"},\n\t}\n\thost := []Template{\n\t\t{Name: \"ca.tpl\", Type: File, TemplatePath: \"../authority/testdata/templates/ca.tpl\", Path: \"/etc/ssh/ca.pub\", Comment: \"#\"},\n\t}\n\tcontent := []Template{\n\t\t{Name: \"test.tpl\", Type: File, Content: []byte(\"some content\"), Path: \"/test.pub\", Comment: \"#\"},\n\t}\n\tbadContent := []Template{\n\t\t{Name: \"ca.tpl\", Type: File, TemplatePath: \"../authority/testdata/templates/ca.tpl\", Content: []byte(\"some content\"), Path: \"/etc/ssh/ca.pub\", Comment: \"#\"},\n\t}\n\n\ttype fields struct {\n\t\tUser []Template\n\t\tHost []Template\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tfields  fields\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", fields{user, host}, false},\n\t\t{\"user\", fields{user, nil}, false},\n\t\t{\"host\", fields{nil, host}, false},\n\t\t{\"content\", fields{content, nil}, false},\n\t\t{\"badUser\", fields{[]Template{{}}, nil}, true},\n\t\t{\"badHost\", fields{nil, []Template{{}}}, true},\n\t\t{\"badContent\", fields{badContent, nil}, true},\n\t}\n\tvar nilValue *SSHTemplates\n\tassert.NoError(t, nilValue.Validate())\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ttmpl := &SSHTemplates{\n\t\t\t\tUser: tt.fields.User,\n\t\t\t\tHost: tt.fields.Host,\n\t\t\t}\n\t\t\tif err := tmpl.Validate(); (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"SSHTemplates.Validate() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestTemplate_Validate(t *testing.T) {\n\tokPath := \"~/.ssh/config\"\n\tokTmplPath := \"../authority/testdata/templates/include.tpl\"\n\n\ttype fields struct {\n\t\tName         string\n\t\tType         TemplateType\n\t\tTemplatePath string\n\t\tPath         string\n\t\tComment      string\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tfields  fields\n\t\twantErr bool\n\t}{\n\t\t{\"okSnippet\", fields{\"include.tpl\", Snippet, okTmplPath, okPath, \"#\"}, false},\n\t\t{\"okFile\", fields{\"file.tpl\", File, okTmplPath, okPath, \"#\"}, false},\n\t\t{\"okDirectory\", fields{\"dir.tpl\", Directory, \"\", \"/tmp/dir\", \"#\"}, false},\n\t\t{\"badName\", fields{\"\", Snippet, okTmplPath, okPath, \"#\"}, true},\n\t\t{\"badType\", fields{\"include.tpl\", \"\", okTmplPath, okPath, \"#\"}, true},\n\t\t{\"badType\", fields{\"include.tpl\", \"foo\", okTmplPath, okPath, \"#\"}, true},\n\t\t{\"badTemplatePath\", fields{\"include.tpl\", Snippet, \"\", okPath, \"#\"}, true},\n\t\t{\"badTemplatePath\", fields{\"include.tpl\", File, \"\", okPath, \"#\"}, true},\n\t\t{\"badTemplatePath\", fields{\"include.tpl\", Directory, okTmplPath, okPath, \"#\"}, true},\n\t\t{\"badPath\", fields{\"include.tpl\", Snippet, okTmplPath, \"\", \"#\"}, true},\n\t\t{\"missingTemplate\", fields{\"include.tpl\", Snippet, \"./testdata/include.tpl\", okTmplPath, \"#\"}, true},\n\t\t{\"directoryTemplate\", fields{\"include.tpl\", File, \"../authority/testdata\", okTmplPath, \"#\"}, true},\n\t}\n\tvar nilValue *Template\n\tassert.NoError(t, nilValue.Validate())\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ttmpl := &Template{\n\t\t\t\tName:         tt.fields.Name,\n\t\t\t\tType:         tt.fields.Type,\n\t\t\t\tTemplatePath: tt.fields.TemplatePath,\n\t\t\t\tPath:         tt.fields.Path,\n\t\t\t\tComment:      tt.fields.Comment,\n\t\t\t}\n\t\t\tif err := tmpl.Validate(); (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"Template.Validate() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLoadAll(t *testing.T) {\n\ttmpl := &Templates{\n\t\tSSH: &SSHTemplates{\n\t\t\tUser: []Template{\n\t\t\t\t{Name: \"include.tpl\", Type: Snippet, TemplatePath: \"../authority/testdata/templates/include.tpl\", Path: \"~/.ssh/config\", Comment: \"#\"},\n\t\t\t},\n\t\t\tHost: []Template{\n\t\t\t\t{Name: \"ca.tpl\", Type: File, TemplatePath: \"../authority/testdata/templates/ca.tpl\", Path: \"/etc/ssh/ca.pub\", Comment: \"#\"},\n\t\t\t},\n\t\t},\n\t}\n\n\ttype args struct {\n\t\tt *Templates\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\targs    args\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", args{tmpl}, false},\n\t\t{\"empty\", args{&Templates{}}, false},\n\t\t{\"nil\", args{nil}, false},\n\t\t{\"badUser\", args{&Templates{SSH: &SSHTemplates{User: []Template{{TemplatePath: \"missing\"}}}}}, true},\n\t\t{\"badHost\", args{&Templates{SSH: &SSHTemplates{Host: []Template{{TemplatePath: \"missing\"}}}}}, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif err := LoadAll(tt.args.t); (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"LoadAll() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestTemplate_Load(t *testing.T) {\n\ttype fields struct {\n\t\tName         string\n\t\tType         TemplateType\n\t\tTemplatePath string\n\t\tPath         string\n\t\tComment      string\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tfields  fields\n\t\twantErr bool\n\t}{\n\t\t{\"ok\", fields{\"include.tpl\", Snippet, \"../authority/testdata/templates/include.tpl\", \"~/.ssh/config\", \"#\"}, false},\n\t\t{\"ok backfill\", fields{\"sshd_config.tpl\", Snippet, \"../authority/testdata/templates/sshd_config.tpl\", \"/etc/ssh/sshd_config\", \"#\"}, false},\n\t\t{\"error\", fields{\"error.tpl\", Snippet, \"../authority/testdata/templates/error.tpl\", \"/tmp/error\", \"#\"}, true},\n\t\t{\"missing\", fields{\"include.tpl\", Snippet, \"./testdata/include.tpl\", \"~/.ssh/config\", \"#\"}, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ttmpl := &Template{\n\t\t\t\tName:         tt.fields.Name,\n\t\t\t\tType:         tt.fields.Type,\n\t\t\t\tTemplatePath: tt.fields.TemplatePath,\n\t\t\t\tPath:         tt.fields.Path,\n\t\t\t\tComment:      tt.fields.Comment,\n\t\t\t}\n\t\t\tif err := tmpl.Load(); (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"Template.Load() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestTemplate_Render(t *testing.T) {\n\tkey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)\n\tassert.FatalError(t, err)\n\tuser, err := ssh.NewPublicKey(key.Public())\n\tassert.FatalError(t, err)\n\tuserB64 := base64.StdEncoding.EncodeToString(user.Marshal())\n\n\tkey, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)\n\tassert.FatalError(t, err)\n\thost, err := ssh.NewPublicKey(key.Public())\n\tassert.FatalError(t, err)\n\thostB64 := base64.StdEncoding.EncodeToString(host.Marshal())\n\n\tdata := map[string]interface{}{\n\t\t\"Step\": &Step{\n\t\t\tSSH: StepSSH{\n\t\t\t\tUserKey: user,\n\t\t\t\tHostKey: host,\n\t\t\t},\n\t\t},\n\t\t\"User\": map[string]string{\n\t\t\t\"StepPath\": \"/tmp/.step\",\n\t\t\t\"User\":     \"john\",\n\t\t\t\"GOOS\":     \"linux\",\n\t\t},\n\t}\n\n\ttype fields struct {\n\t\tName         string\n\t\tType         TemplateType\n\t\tTemplatePath string\n\t\tPath         string\n\t\tComment      string\n\t}\n\ttype args struct {\n\t\tdata interface{}\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tfields  fields\n\t\targs    args\n\t\twant    []byte\n\t\twantErr bool\n\t}{\n\t\t{\"snippet\", fields{\"include.tpl\", Snippet, \"../authority/testdata/templates/include.tpl\", \"~/.ssh/config\", \"#\"}, args{data}, []byte(\"Host *\\n\\tInclude /tmp/.step/ssh/config\"), false},\n\t\t{\"file\", fields{\"known_hosts.tpl\", File, \"../authority/testdata/templates/known_hosts.tpl\", \"ssh/known_hosts\", \"#\"}, args{data}, []byte(fmt.Sprintf(\"@cert-authority * %s %s\", host.Type(), hostB64)), false},\n\t\t{\"file\", fields{\"ca.tpl\", File, \"../authority/testdata/templates/ca.tpl\", \"/etc/ssh/ca.pub\", \"#\"}, args{data}, []byte(fmt.Sprintf(\"%s %s\", user.Type(), userB64)), false},\n\t\t{\"directory\", fields{\"dir.tpl\", Directory, \"\", \"/tmp/dir\", \"\"}, args{data}, nil, false},\n\t\t{\"error\", fields{\"error.tpl\", File, \"../authority/testdata/templates/error.tpl\", \"/tmp/error\", \"#\"}, args{data}, nil, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ttmpl := &Template{\n\t\t\t\tName:         tt.fields.Name,\n\t\t\t\tType:         tt.fields.Type,\n\t\t\t\tTemplatePath: tt.fields.TemplatePath,\n\t\t\t\tPath:         tt.fields.Path,\n\t\t\t\tComment:      tt.fields.Comment,\n\t\t\t}\n\t\t\tgot, err := tmpl.Render(tt.args.data)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"Template.Render() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"Template.Render() = %v, want %v\", string(got), string(tt.want))\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestTemplate_Output(t *testing.T) {\n\tkey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)\n\tassert.FatalError(t, err)\n\tuser, err := ssh.NewPublicKey(key.Public())\n\tassert.FatalError(t, err)\n\tuserB64 := base64.StdEncoding.EncodeToString(user.Marshal())\n\n\tkey, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)\n\tassert.FatalError(t, err)\n\thost, err := ssh.NewPublicKey(key.Public())\n\tassert.FatalError(t, err)\n\thostB64 := base64.StdEncoding.EncodeToString(host.Marshal())\n\n\tdata := map[string]interface{}{\n\t\t\"Step\": &Step{\n\t\t\tSSH: StepSSH{\n\t\t\t\tUserKey: user,\n\t\t\t\tHostKey: host,\n\t\t\t},\n\t\t},\n\t\t\"User\": map[string]string{\n\t\t\t\"StepPath\": \"/tmp/.step\",\n\t\t},\n\t}\n\n\ttype fields struct {\n\t\tName         string\n\t\tType         TemplateType\n\t\tTemplatePath string\n\t\tPath         string\n\t\tComment      string\n\t}\n\ttype args struct {\n\t\tdata interface{}\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tfields  fields\n\t\targs    args\n\t\twant    []byte\n\t\twantErr bool\n\t}{\n\t\t{\"snippet\", fields{\"include.tpl\", Snippet, \"../authority/testdata/templates/include.tpl\", \"~/.ssh/config\", \"#\"}, args{data}, []byte(\"Host *\\n\\tInclude /tmp/.step/ssh/config\"), false},\n\t\t{\"file\", fields{\"known_hosts.tpl\", File, \"../authority/testdata/templates/known_hosts.tpl\", \"ssh/known_hosts\", \"#\"}, args{data}, []byte(fmt.Sprintf(\"@cert-authority * %s %s\", host.Type(), hostB64)), false},\n\t\t{\"file\", fields{\"ca.tpl\", File, \"../authority/testdata/templates/ca.tpl\", \"/etc/ssh/ca.pub\", \"#\"}, args{data}, []byte(fmt.Sprintf(\"%s %s\", user.Type(), userB64)), false},\n\t\t{\"directory\", fields{\"dir.tpl\", Directory, \"\", \"/tmp/dir\", \"\"}, args{data}, nil, false},\n\t\t{\"error\", fields{\"error.tpl\", File, \"../authority/testdata/templates/error.tpl\", \"/tmp/error\", \"#\"}, args{data}, nil, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tvar want Output\n\t\t\tif !tt.wantErr {\n\t\t\t\twant = Output{\n\t\t\t\t\tName:    tt.fields.Name,\n\t\t\t\t\tType:    tt.fields.Type,\n\t\t\t\t\tPath:    tt.fields.Path,\n\t\t\t\t\tComment: tt.fields.Comment,\n\t\t\t\t\tContent: tt.want,\n\t\t\t\t}\n\t\t\t}\n\n\t\t\ttmpl := &Template{\n\t\t\t\tName:         tt.fields.Name,\n\t\t\t\tType:         tt.fields.Type,\n\t\t\t\tTemplatePath: tt.fields.TemplatePath,\n\t\t\t\tPath:         tt.fields.Path,\n\t\t\t\tComment:      tt.fields.Comment,\n\t\t\t}\n\t\t\tgot, err := tmpl.Output(tt.args.data)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"Template.Output() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(got, want) {\n\t\t\t\tt.Errorf(\"Template.Output() = %v, want %v\", got, want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestOutput_Write(t *testing.T) {\n\tdir := t.TempDir()\n\n\tjoin := func(elem ...string) string {\n\t\telems := append([]string{dir}, elem...)\n\t\treturn filepath.Join(elems...)\n\t}\n\tassert.FatalError(t, os.Mkdir(join(\"bad\"), 0644))\n\n\ttype fields struct {\n\t\tName    string\n\t\tType    TemplateType\n\t\tPath    string\n\t\tComment string\n\t\tContent []byte\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tfields  fields\n\t\twantErr bool\n\t}{\n\t\t{\"snippet\", fields{\"snippet\", Snippet, join(\"snippet\"), \"#\", []byte(\"some content\")}, false},\n\t\t{\"file\", fields{\"file\", File, join(\"file\"), \"#\", []byte(\"some content\")}, false},\n\t\t{\"snippetInDir\", fields{\"file\", Snippet, join(\"dir\", \"snippets\", \"snippet\"), \"#\", []byte(\"some content\")}, false},\n\t\t{\"fileInDir\", fields{\"file\", File, join(\"dir\", \"files\", \"file\"), \"#\", []byte(\"some content\")}, false},\n\t\t{\"directory\", fields{\"directory\", Directory, join(\"directory\"), \"\", nil}, false},\n\t\t{\"snippetErr\", fields{\"snippet\", Snippet, join(\"bad\", \"snippet\"), \"#\", []byte(\"some content\")}, true},\n\t\t{\"fileErr\", fields{\"file\", File, join(\"bad\", \"file\"), \"#\", []byte(\"some content\")}, true},\n\t\t{\"directoryErr\", fields{\"directory\", Directory, join(\"bad\", \"directory\"), \"\", nil}, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\to := &Output{\n\t\t\t\tName:    tt.fields.Name,\n\t\t\t\tType:    tt.fields.Type,\n\t\t\t\tComment: tt.fields.Comment,\n\t\t\t\tPath:    tt.fields.Path,\n\t\t\t\tContent: tt.fields.Content,\n\t\t\t}\n\t\t\tif err := o.Write(); (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"Output.Write() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\t\t\tif !tt.wantErr {\n\t\t\t\tst, err := os.Stat(o.Path)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Errorf(\"os.Stat(%s) error = %v\", o.Path, err)\n\t\t\t\t} else {\n\t\t\t\t\tif o.Type == Directory {\n\t\t\t\t\t\tassert.True(t, st.IsDir())\n\t\t\t\t\t\tassert.Equals(t, os.ModeDir|os.FileMode(0700), st.Mode())\n\t\t\t\t\t} else {\n\t\t\t\t\t\tassert.False(t, st.IsDir())\n\t\t\t\t\t\tassert.Equals(t, os.FileMode(0600), st.Mode())\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestTemplate_ValidateRequiredData(t *testing.T) {\n\tdata := map[string]string{\n\t\t\"key1\": \"value1\",\n\t\t\"key2\": \"value2\",\n\t}\n\ttype fields struct {\n\t\tRequiredData []string\n\t}\n\ttype args struct {\n\t\tdata map[string]string\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tfields  fields\n\t\targs    args\n\t\twantErr bool\n\t}{\n\t\t{\"ok nil\", fields{nil}, args{nil}, false},\n\t\t{\"ok empty\", fields{[]string{}}, args{data}, false},\n\t\t{\"ok one\", fields{[]string{\"key1\"}}, args{data}, false},\n\t\t{\"ok multiple\", fields{[]string{\"key1\", \"key2\"}}, args{data}, false},\n\t\t{\"fail nil\", fields{[]string{\"missing\"}}, args{nil}, true},\n\t\t{\"fail missing\", fields{[]string{\"missing\"}}, args{data}, true},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ttmpl := &Template{\n\t\t\t\tRequiredData: tt.fields.RequiredData,\n\t\t\t}\n\t\t\tif err := tmpl.ValidateRequiredData(tt.args.data); (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"Template.ValidateRequiredData() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "templates/values.go",
    "content": "package templates\n\nimport (\n\t\"golang.org/x/crypto/ssh\"\n)\n\n// SSHTemplateVersionKey is a key that can be submitted by a client to select\n// the template version that will be returned by the server.\nvar SSHTemplateVersionKey = \"StepSSHTemplateVersion\"\n\n// Step represents the default variables available in the CA.\ntype Step struct {\n\tSSH StepSSH\n}\n\n// StepSSH holds SSH-related values for the CA.\ntype StepSSH struct {\n\tHostKey           ssh.PublicKey\n\tUserKey           ssh.PublicKey\n\tHostFederatedKeys []ssh.PublicKey\n\tUserFederatedKeys []ssh.PublicKey\n}\n\n// DefaultSSHTemplates contains the configuration of default templates used on ssh.\n// Relative paths are relative to the StepPath.\nvar DefaultSSHTemplates = SSHTemplates{\n\tUser: []Template{\n\t\t{\n\t\t\tName:         \"config.tpl\",\n\t\t\tType:         Snippet,\n\t\t\tTemplatePath: \"templates/ssh/config.tpl\",\n\t\t\tPath:         \"~/.ssh/config\",\n\t\t\tComment:      \"#\",\n\t\t},\n\t\t{\n\t\t\tName:         \"step_includes.tpl\",\n\t\t\tType:         PrependLine,\n\t\t\tTemplatePath: \"templates/ssh/step_includes.tpl\",\n\t\t\tPath:         \"${STEPPATH}/ssh/includes\",\n\t\t\tComment:      \"#\",\n\t\t},\n\t\t{\n\t\t\tName:         \"step_config.tpl\",\n\t\t\tType:         File,\n\t\t\tTemplatePath: \"templates/ssh/step_config.tpl\",\n\t\t\tPath:         \"ssh/config\",\n\t\t\tComment:      \"#\",\n\t\t},\n\t\t{\n\t\t\tName:         \"known_hosts.tpl\",\n\t\t\tType:         File,\n\t\t\tTemplatePath: \"templates/ssh/known_hosts.tpl\",\n\t\t\tPath:         \"ssh/known_hosts\",\n\t\t\tComment:      \"#\",\n\t\t},\n\t},\n\tHost: []Template{\n\t\t{\n\t\t\tName:         \"sshd_config.tpl\",\n\t\t\tType:         Snippet,\n\t\t\tTemplatePath: \"templates/ssh/sshd_config.tpl\",\n\t\t\tPath:         \"/etc/ssh/sshd_config\",\n\t\t\tComment:      \"#\",\n\t\t\tRequiredData: []string{\"Certificate\", \"Key\"},\n\t\t},\n\t\t{\n\t\t\tName:         \"ca.tpl\",\n\t\t\tType:         Snippet,\n\t\t\tTemplatePath: \"templates/ssh/ca.tpl\",\n\t\t\tPath:         \"/etc/ssh/ca.pub\",\n\t\t\tComment:      \"#\",\n\t\t},\n\t},\n}\n\n// DefaultSSHTemplateData contains the data of the default templates used on ssh.\nvar DefaultSSHTemplateData = map[string]string{\n\t// config.tpl adds the step ssh config file.\n\t//\n\t// Note: on windows `Include C:\\...` is treated as a relative path.\n\t\"config.tpl\": `Host *\n{{- if or .User.GOOS \"none\" | eq \"windows\" }}\n{{- if .User.StepBasePath }}\n\tInclude \"{{ .User.StepBasePath | replace \"\\\\\" \"/\" | trimPrefix \"C:\" }}/ssh/includes\"\n{{- else }}\n\tInclude \"{{ .User.StepPath | replace \"\\\\\" \"/\" | trimPrefix \"C:\" }}/ssh/includes\"\n{{- end }}\n{{- else }}\n{{- if .User.StepBasePath }}\n\tInclude \"{{.User.StepBasePath}}/ssh/includes\"\n{{- else }}\n\tInclude \"{{.User.StepPath}}/ssh/includes\"\n{{- end }}\n{{- end }}`,\n\n\t// step_includes.tpl adds the step ssh config file.\n\t//\n\t// Note: on windows `Include C:\\...` is treated as a relative path.\n\t\"step_includes.tpl\": `{{- if or .User.GOOS \"none\" | eq \"windows\" }}Include \"{{ .User.StepPath | replace \"\\\\\" \"/\" | trimPrefix \"C:\" }}/ssh/config\"{{- else }}Include \"{{.User.StepPath}}/ssh/config\"{{- end }}`,\n\n\t// step_config.tpl is the step ssh config file, it includes the Match rule and\n\t// references the step known_hosts file.\n\t//\n\t// Note: on windows ProxyCommand requires the full path\n\t\"step_config.tpl\": `Match exec \"step ssh check-host{{- if .User.Context }} --context {{ .User.Context }}{{- end }} %h\"\n{{- if .User.User }}\n\tUser {{.User.User}}\n{{- end }}\n{{- if or .User.GOOS \"none\" | eq \"windows\" }}\n\tUserKnownHostsFile \"{{.User.StepPath}}\\ssh\\known_hosts\"\n\tProxyCommand C:\\Windows\\System32\\cmd.exe /c step ssh proxycommand{{- if .User.Context }} --context {{ .User.Context }}{{- end }}{{- if .User.Console}} --console {{- end }}{{- if .User.Provisioner }} --provisioner {{ .User.Provisioner }}{{- end }} %r %h %p\n{{- else }}\n\tUserKnownHostsFile \"{{.User.StepPath}}/ssh/known_hosts\"\n\tProxyCommand step ssh proxycommand{{- if .User.Context }} --context {{ .User.Context }}{{- end }}{{- if .User.Console}} --console {{- end }}{{- if .User.Provisioner }} --provisioner {{ .User.Provisioner }}{{- end }} %r %h %p\n{{- end }}\n`,\n\n\t// known_hosts.tpl authorizes the ssh hosts key\n\t\"known_hosts.tpl\": `@cert-authority * {{.Step.SSH.HostKey.Type}} {{.Step.SSH.HostKey.Marshal | toString | b64enc}}\n{{- range .Step.SSH.HostFederatedKeys}}\n@cert-authority * {{.Type}} {{.Marshal | toString | b64enc}}\n{{- end }}\n`,\n\n\t// sshd_config.tpl adds the configuration to support certificates\n\t\"sshd_config.tpl\": `Match all\n\tTrustedUserCAKeys /etc/ssh/ca.pub\n\tHostCertificate /etc/ssh/{{.User.Certificate}}\n\tHostKey /etc/ssh/{{.User.Key}}`,\n\n\t// ca.tpl contains the public key used to authorized clients\n\t\"ca.tpl\": `{{.Step.SSH.UserKey.Type}} {{.Step.SSH.UserKey.Marshal | toString | b64enc}}\n{{- range .Step.SSH.UserFederatedKeys}}\n{{.Type}} {{.Marshal | toString | b64enc}}\n{{- end }}\n`,\n}\n\n// DefaultTemplates returns the default templates.\nfunc DefaultTemplates() *Templates {\n\tsshTemplates := DefaultSSHTemplates\n\tfor i, t := range sshTemplates.User {\n\t\tsshTemplates.User[i].TemplatePath = \"\"\n\t\tsshTemplates.User[i].Content = []byte(DefaultSSHTemplateData[t.Name])\n\t}\n\tfor i, t := range sshTemplates.Host {\n\t\tsshTemplates.Host[i].TemplatePath = \"\"\n\t\tsshTemplates.Host[i].Content = []byte(DefaultSSHTemplateData[t.Name])\n\t}\n\treturn &Templates{\n\t\tSSH:  &sshTemplates,\n\t\tData: map[string]interface{}{},\n\t}\n}\n"
  },
  {
    "path": "templates/values_test.go",
    "content": "package templates\n\nimport (\n\t\"reflect\"\n\t\"testing\"\n)\n\nfunc TestDefaultTemplates(t *testing.T) {\n\tsshTemplates := DefaultSSHTemplates\n\tsshTemplatesData := DefaultSSHTemplateData\n\tt.Cleanup(func() {\n\t\tDefaultSSHTemplates = sshTemplates\n\t\tDefaultSSHTemplateData = sshTemplatesData\n\t})\n\n\tDefaultSSHTemplates = SSHTemplates{\n\t\tUser: []Template{\n\t\t\t{Name: \"foo.tpl\", Type: Snippet, TemplatePath: \"templates/ssh/foo.tpl\", Path: \"/tmp/foo\", Comment: \"#\"},\n\t\t},\n\t\tHost: []Template{\n\t\t\t{Name: \"bar.tpl\", Type: Snippet, TemplatePath: \"templates/ssh/bar.tpl\", Path: \"/tmp/bar\", Comment: \"#\"},\n\t\t},\n\t}\n\tDefaultSSHTemplateData = map[string]string{\n\t\t\"foo.tpl\": \"foo\",\n\t\t\"bar.tpl\": \"bar\",\n\t}\n\n\ttests := []struct {\n\t\tname string\n\t\twant *Templates\n\t}{\n\t\t{\"ok\", &Templates{\n\t\t\tSSH: &SSHTemplates{\n\t\t\t\tUser: []Template{\n\t\t\t\t\t{Name: \"foo.tpl\", Type: Snippet, Content: []byte(\"foo\"), Path: \"/tmp/foo\", Comment: \"#\"},\n\t\t\t\t},\n\t\t\t\tHost: []Template{\n\t\t\t\t\t{Name: \"bar.tpl\", Type: Snippet, Content: []byte(\"bar\"), Path: \"/tmp/bar\", Comment: \"#\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tData: map[string]interface{}{},\n\t\t}},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif got := DefaultTemplates(); !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"DefaultTemplates() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "test/integration/requestid_test.go",
    "content": "package integration\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"path/filepath\"\n\t\"sync\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"go.step.sm/crypto/jose\"\n\t\"go.step.sm/crypto/keyutil\"\n\t\"go.step.sm/crypto/minica\"\n\t\"go.step.sm/crypto/pemutil\"\n\t\"go.step.sm/crypto/randutil\"\n\t\"go.step.sm/crypto/x509util\"\n\n\t\"github.com/smallstep/certificates/api\"\n\t\"github.com/smallstep/certificates/authority/config\"\n\t\"github.com/smallstep/certificates/authority/provisioner\"\n\t\"github.com/smallstep/certificates/ca\"\n\t\"github.com/smallstep/certificates/ca/client\"\n\t\"github.com/smallstep/certificates/errs\"\n)\n\n// reservePort \"reserves\" a TCP port by opening a listener on a random\n// port and immediately closing it. The port can then be assumed to be\n// available for running a server on.\nfunc reservePort(t *testing.T) (host, port string) {\n\tt.Helper()\n\tl, err := net.Listen(\"tcp\", \":0\")\n\trequire.NoError(t, err)\n\n\taddress := l.Addr().String()\n\terr = l.Close()\n\trequire.NoError(t, err)\n\n\thost, port, err = net.SplitHostPort(address)\n\trequire.NoError(t, err)\n\n\treturn\n}\n\nfunc Test_reflectRequestID(t *testing.T) {\n\tctx := context.Background()\n\n\tdir := t.TempDir()\n\tt.Setenv(\"STEPPATH\", dir)\n\n\tm, err := minica.New(minica.WithName(\"Step E2E\"))\n\trequire.NoError(t, err)\n\n\trootFilepath := filepath.Join(dir, \"root.crt\")\n\t_, err = pemutil.Serialize(m.Root, pemutil.WithFilename(rootFilepath))\n\trequire.NoError(t, err)\n\n\tintermediateCertFilepath := filepath.Join(dir, \"intermediate.crt\")\n\t_, err = pemutil.Serialize(m.Intermediate, pemutil.WithFilename(intermediateCertFilepath))\n\trequire.NoError(t, err)\n\n\tintermediateKeyFilepath := filepath.Join(dir, \"intermediate.key\")\n\t_, err = pemutil.Serialize(m.Signer, pemutil.WithFilename(intermediateKeyFilepath))\n\trequire.NoError(t, err)\n\n\t// get a random address to listen on and connect to; currently no nicer way to get one before starting the server\n\t// TODO(hs): find/implement a nicer way to expose the CA URL, similar to how e.g. httptest.Server exposes it?\n\thost, port := reservePort(t)\n\n\tauthorizingSrv := newAuthorizingServer(t, m)\n\tdefer authorizingSrv.Close()\n\tauthorizingSrv.StartTLS()\n\n\tpassword := []byte(\"1234\")\n\tjwk, jwe, err := jose.GenerateDefaultKeyPair(password)\n\trequire.NoError(t, err)\n\tencryptedKey, err := jwe.CompactSerialize()\n\trequire.NoError(t, err)\n\tprov := &provisioner.JWK{\n\t\tID:           \"jwk\",\n\t\tName:         \"jwk\",\n\t\tType:         \"JWK\",\n\t\tKey:          jwk,\n\t\tEncryptedKey: encryptedKey,\n\t\tClaims:       &config.GlobalProvisionerClaims,\n\t\tOptions: &provisioner.Options{\n\t\t\tWebhooks: []*provisioner.Webhook{\n\t\t\t\t{\n\t\t\t\t\tID:       \"webhook\",\n\t\t\t\t\tName:     \"webhook-test\",\n\t\t\t\t\tURL:      fmt.Sprintf(\"%s/authorize\", authorizingSrv.URL),\n\t\t\t\t\tKind:     \"AUTHORIZING\",\n\t\t\t\t\tCertType: \"X509\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\terr = prov.Init(provisioner.Config{})\n\trequire.NoError(t, err)\n\n\tcfg := &config.Config{\n\t\tRoot:             []string{rootFilepath},\n\t\tIntermediateCert: intermediateCertFilepath,\n\t\tIntermediateKey:  intermediateKeyFilepath,\n\t\tAddress:          net.JoinHostPort(host, port), // reuse the address that was just \"reserved\"\n\t\tDNSNames:         []string{\"127.0.0.1\", \"[::1]\", \"localhost\"},\n\t\tAuthorityConfig: &config.AuthConfig{\n\t\t\tAuthorityID:    \"stepca-test\",\n\t\t\tDeploymentType: \"standalone-test\",\n\t\t\tProvisioners:   provisioner.List{prov},\n\t\t},\n\t\tLogger: json.RawMessage(`{\"format\": \"text\"}`),\n\t}\n\tc, err := ca.New(cfg)\n\trequire.NoError(t, err)\n\n\t// instantiate a client for the CA running at the random address\n\tcaClient, err := ca.NewClient(\n\t\tfmt.Sprintf(\"https://localhost:%s\", port),\n\t\tca.WithRootFile(rootFilepath),\n\t)\n\trequire.NoError(t, err)\n\n\tvar wg sync.WaitGroup\n\twg.Add(1)\n\n\tgo func() {\n\t\tdefer wg.Done()\n\t\terr = c.Run()\n\t\trequire.ErrorIs(t, err, http.ErrServerClosed)\n\t}()\n\n\t// require the CA server to be available within 10 seconds,\n\t// failing the test if it doesn't.\n\trequireCAServerToBeAvailable(t, net.JoinHostPort(\"localhost\", port), 10*time.Second)\n\n\t// require OK health response as the baseline\n\thealthResponse, err := caClient.HealthWithContext(ctx)\n\trequire.NoError(t, err)\n\tif assert.NotNil(t, healthResponse) {\n\t\trequire.Equal(t, \"ok\", healthResponse.Status)\n\t}\n\n\t// expect an error when retrieving an invalid root\n\trootResponse, err := caClient.RootWithContext(ctx, \"invalid\")\n\tvar firstErr *errs.Error\n\tif assert.ErrorAs(t, err, &firstErr) {\n\t\tassert.Equal(t, 404, firstErr.StatusCode())\n\t\tassert.Equal(t, `root certificate with fingerprint \"invalid\" was not found`, firstErr.Err.Error())\n\t\tassert.NotEmpty(t, firstErr.RequestID)\n\n\t}\n\tassert.Nil(t, rootResponse)\n\n\t// expect an error when retrieving an invalid root and provided request ID\n\trootResponse, err = caClient.RootWithContext(client.NewRequestIDContext(ctx, \"reqID\"), \"invalid\")\n\tvar secondErr *errs.Error\n\tif assert.ErrorAs(t, err, &secondErr) {\n\t\tassert.Equal(t, 404, secondErr.StatusCode())\n\t\tassert.Equal(t, `root certificate with fingerprint \"invalid\" was not found`, secondErr.Err.Error())\n\t\tassert.Equal(t, \"reqID\", secondErr.RequestID)\n\t}\n\tassert.Nil(t, rootResponse)\n\n\t// prepare a Sign request\n\tsubject := \"test\"\n\tdecryptedJWK := decryptPrivateKey(t, jwe, password)\n\tott := generateOTT(t, decryptedJWK, subject)\n\n\tsigner, err := keyutil.GenerateDefaultSigner()\n\trequire.NoError(t, err)\n\n\tcsr, err := x509util.CreateCertificateRequest(subject, []string{subject}, signer)\n\trequire.NoError(t, err)\n\n\t// perform the Sign request using the OTT and CSR\n\tsignResponse, err := caClient.SignWithContext(client.NewRequestIDContext(ctx, \"signRequestID\"), &api.SignRequest{\n\t\tCsrPEM:    api.CertificateRequest{CertificateRequest: csr},\n\t\tOTT:       ott,\n\t\tNotAfter:  api.NewTimeDuration(time.Now().Add(1 * time.Hour)),\n\t\tNotBefore: api.NewTimeDuration(time.Now().Add(-1 * time.Hour)),\n\t})\n\tassert.NoError(t, err)\n\n\t// assert a certificate was returned for the subject \"test\"\n\tif assert.NotNil(t, signResponse) {\n\t\tassert.Len(t, signResponse.CertChainPEM, 2)\n\t\tcert, err := x509.ParseCertificate(signResponse.CertChainPEM[0].Raw)\n\t\tassert.NoError(t, err)\n\t\tif assert.NotNil(t, cert) {\n\t\t\tassert.Equal(t, \"test\", cert.Subject.CommonName)\n\t\t\tassert.Contains(t, cert.DNSNames, \"test\")\n\t\t}\n\t}\n\n\t// done testing; stop and wait for the server to quit\n\terr = c.Stop()\n\trequire.NoError(t, err)\n\n\twg.Wait()\n}\n\nfunc decryptPrivateKey(t *testing.T, jwe *jose.JSONWebEncryption, pass []byte) *jose.JSONWebKey {\n\tt.Helper()\n\td, err := jwe.Decrypt(pass)\n\trequire.NoError(t, err)\n\n\tjwk := &jose.JSONWebKey{}\n\terr = json.Unmarshal(d, jwk)\n\trequire.NoError(t, err)\n\n\treturn jwk\n}\n\nfunc generateOTT(t *testing.T, jwk *jose.JSONWebKey, subject string) string {\n\tt.Helper()\n\tnow := time.Now()\n\n\tkeyID, err := jose.Thumbprint(jwk)\n\trequire.NoError(t, err)\n\n\topts := new(jose.SignerOptions).WithType(\"JWT\").WithHeader(\"kid\", keyID)\n\tsigner, err := jose.NewSigner(jose.SigningKey{Key: jwk.Key}, opts)\n\trequire.NoError(t, err)\n\n\tid, err := randutil.ASCII(64)\n\trequire.NoError(t, err)\n\n\tcl := struct {\n\t\tjose.Claims\n\t\tSANS []string `json:\"sans\"`\n\t}{\n\t\tClaims: jose.Claims{\n\t\t\tID:        id,\n\t\t\tSubject:   subject,\n\t\t\tIssuer:    \"jwk\",\n\t\t\tNotBefore: jose.NewNumericDate(now),\n\t\t\tExpiry:    jose.NewNumericDate(now.Add(time.Minute)),\n\t\t\tAudience:  []string{\"https://127.0.0.1/1.0/sign\"},\n\t\t},\n\t\tSANS: []string{subject},\n\t}\n\traw, err := jose.Signed(signer).Claims(cl).CompactSerialize()\n\trequire.NoError(t, err)\n\n\treturn raw\n}\n\nfunc newAuthorizingServer(t *testing.T, mca *minica.CA) *httptest.Server {\n\tt.Helper()\n\n\tkey, err := keyutil.GenerateDefaultSigner()\n\trequire.NoError(t, err)\n\n\tcsr, err := x509util.CreateCertificateRequest(\"127.0.0.1\", []string{\"127.0.0.1\"}, key)\n\trequire.NoError(t, err)\n\n\tcrt, err := mca.SignCSR(csr)\n\trequire.NoError(t, err)\n\n\tsrv := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tif assert.Equal(t, \"signRequestID\", r.Header.Get(\"X-Request-Id\")) {\n\t\t\terr := json.NewEncoder(w).Encode(struct{ Allow bool }{Allow: true})\n\t\t\trequire.NoError(t, err)\n\t\t\treturn\n\t\t}\n\n\t\tw.WriteHeader(http.StatusBadRequest)\n\t}))\n\ttrustedRoots := x509.NewCertPool()\n\ttrustedRoots.AddCert(mca.Root)\n\n\tsrv.TLS = &tls.Config{\n\t\tCertificates: []tls.Certificate{\n\t\t\t{\n\t\t\t\tCertificate: [][]byte{crt.Raw, mca.Intermediate.Raw},\n\t\t\t\tPrivateKey:  key,\n\t\t\t\tLeaf:        crt,\n\t\t\t},\n\t\t},\n\t\tClientCAs:  trustedRoots,\n\t\tClientAuth: tls.RequireAndVerifyClientCert,\n\t\tServerName: \"localhost\",\n\t}\n\n\treturn srv\n}\n\n// requireCAServerToBeAvailable tries to connect to address to check a server\n// is available. It will retry the connection every ~100ms, until timeout occurs.\n// If no connection can be made, the test is failed.\nfunc requireCAServerToBeAvailable(t *testing.T, address string, timeout time.Duration) {\n\tt.Helper()\n\n\tctx, cancel := context.WithTimeout(context.Background(), timeout)\n\tdefer cancel()\n\n\tfor !canConnect(ctx, address) {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\trequire.FailNow(t, fmt.Sprintf(\"CA server failed to start at https://%s within %s\", address, timeout.String()))\n\t\tcase <-time.After(100 * time.Millisecond):\n\t\t}\n\t}\n}\n\nfunc canConnect(ctx context.Context, address string) bool {\n\td := net.Dialer{}\n\tconn, err := d.DialContext(ctx, \"tcp\", address)\n\tif err != nil {\n\t\treturn false\n\t}\n\n\tconn.Close()\n\n\treturn true\n}\n"
  },
  {
    "path": "test/integration/scep/common_test.go",
    "content": "package sceptest\n\nimport (\n\t\"context\"\n\t\"crypto\"\n\t\"crypto/rand\"\n\t\"crypto/rsa\"\n\t\"crypto/sha1\"\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"crypto/x509/pkix\"\n\t\"encoding/asn1\"\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"math/big\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"path/filepath\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/smallstep/pkcs7\"\n\t\"github.com/smallstep/scep\"\n\tscepx509util \"github.com/smallstep/scep/x509util\"\n\t\"go.step.sm/crypto/keyutil\"\n\t\"go.step.sm/crypto/minica\"\n\t\"go.step.sm/crypto/pemutil\"\n\t\"go.step.sm/crypto/x509util\"\n\n\t\"github.com/smallstep/certificates/authority/config\"\n\t\"github.com/smallstep/certificates/authority/provisioner\"\n\t\"github.com/smallstep/certificates/ca\"\n\t\"github.com/smallstep/certificates/cas/apiv1\"\n\t\"github.com/smallstep/certificates/internal/httptransport\"\n)\n\nfunc newCAClient(t *testing.T, caURL, rootFilepath string) *ca.Client {\n\tt.Helper()\n\tcaClient, err := ca.NewClient(\n\t\tcaURL,\n\t\tca.WithRootFile(rootFilepath),\n\t)\n\trequire.NoError(t, err)\n\treturn caClient\n}\n\nfunc requireHealthyCA(t *testing.T, caClient *ca.Client) {\n\tt.Helper()\n\t// Wait for CA\n\ttime.Sleep(time.Second)\n\n\tctx := context.Background()\n\thealthResponse, err := caClient.HealthWithContext(ctx)\n\trequire.NoError(t, err)\n\tif assert.NotNil(t, healthResponse) {\n\t\trequire.Equal(t, \"ok\", healthResponse.Status)\n\t}\n}\n\n// reservePort \"reserves\" a TCP port by opening a listener on a random\n// port and immediately closing it. The port can then be assumed to be\n// available for running a server on.\nfunc reservePort(t *testing.T) (host, port string) {\n\tt.Helper()\n\tl, err := net.Listen(\"tcp\", \":0\")\n\trequire.NoError(t, err)\n\n\taddress := l.Addr().String()\n\terr = l.Close()\n\trequire.NoError(t, err)\n\n\thost, port, err = net.SplitHostPort(address)\n\trequire.NoError(t, err)\n\n\treturn\n}\n\ntype testCA struct {\n\tca           *ca.CA\n\tcaURL        string\n\trootFilepath string\n\troot         *x509.Certificate\n}\n\nfunc (t *testCA) run() error {\n\treturn t.ca.Run()\n}\n\nfunc (t *testCA) stop() error {\n\treturn t.ca.Stop()\n}\n\nfunc newTestCA(t *testing.T, name string) *testCA {\n\tt.Helper()\n\n\tsigner, err := keyutil.GenerateSigner(\"RSA\", \"\", 2048)\n\trequire.NoError(t, err)\n\n\tdir := t.TempDir()\n\tt.Setenv(\"STEPPATH\", dir)\n\n\tm, err := minica.New(minica.WithName(name), minica.WithGetSignerFunc(func() (crypto.Signer, error) {\n\t\treturn signer, nil\n\t}))\n\trequire.NoError(t, err)\n\n\trootFilepath := filepath.Join(dir, \"root.crt\")\n\t_, err = pemutil.Serialize(m.Root, pemutil.WithFilename(rootFilepath))\n\trequire.NoError(t, err)\n\n\tintermediateCertFilepath := filepath.Join(dir, \"intermediate.crt\")\n\t_, err = pemutil.Serialize(m.Intermediate, pemutil.WithFilename(intermediateCertFilepath))\n\trequire.NoError(t, err)\n\n\tintermediateKeyFilepath := filepath.Join(dir, \"intermediate.key\")\n\t_, err = pemutil.Serialize(m.Signer, pemutil.WithFilename(intermediateKeyFilepath))\n\trequire.NoError(t, err)\n\n\t// get a random address to listen on and connect to; currently no nicer way to get one before starting the server\n\thost, port := reservePort(t)\n\n\tprov := &provisioner.SCEP{\n\t\tID:                            \"scep\",\n\t\tName:                          \"scep\",\n\t\tType:                          \"SCEP\",\n\t\tForceCN:                       false,\n\t\tChallengePassword:             \"the-challenge\",\n\t\tEncryptionAlgorithmIdentifier: 2,\n\t\tMinimumPublicKeyLength:        2048,\n\t\tClaims:                        &config.GlobalProvisionerClaims,\n\t}\n\n\terr = prov.Init(provisioner.Config{})\n\trequire.NoError(t, err)\n\n\tcfg := &config.Config{\n\t\tRoot:             []string{rootFilepath},\n\t\tIntermediateCert: intermediateCertFilepath,\n\t\tIntermediateKey:  intermediateKeyFilepath,\n\t\tAddress:          net.JoinHostPort(host, port), // reuse the address that was just \"reserved\"\n\t\tDNSNames:         []string{\"127.0.0.1\", \"[::1]\", \"localhost\"},\n\t\tAuthorityConfig: &config.AuthConfig{\n\t\t\tAuthorityID:    \"stepca-test-scep\",\n\t\t\tDeploymentType: \"standalone-test\",\n\t\t\tProvisioners:   provisioner.List{prov},\n\t\t},\n\t\tLogger: json.RawMessage(`{\"format\": \"text\"}`),\n\t}\n\tc, err := ca.New(cfg)\n\trequire.NoError(t, err)\n\n\treturn &testCA{\n\t\tca:           c,\n\t\tcaURL:        fmt.Sprintf(\"https://localhost:%s\", port),\n\t\trootFilepath: rootFilepath,\n\t\troot:         m.Root,\n\t}\n}\n\ntype client struct {\n\tcaURL      string\n\tcaCert     *x509.Certificate\n\thttpClient *http.Client\n}\n\nfunc createSCEPClient(t *testing.T, caURL string, root *x509.Certificate) *client {\n\tt.Helper()\n\ttrustedRoots := x509.NewCertPool()\n\ttrustedRoots.AddCert(root)\n\ttransport := httptransport.New()\n\ttransport.TLSClientConfig = &tls.Config{\n\t\tRootCAs: trustedRoots,\n\t}\n\thttpClient := &http.Client{\n\t\tTransport: transport,\n\t}\n\treturn &client{\n\t\tcaURL:      fmt.Sprintf(\"%s/scep/scep\", caURL),\n\t\thttpClient: httpClient,\n\t}\n}\n\nfunc (c *client) getCACert(t *testing.T) error {\n\t// return early if CA certificate already available\n\tif c.caCert != nil {\n\t\treturn nil\n\t}\n\n\tresp, err := c.httpClient.Get(fmt.Sprintf(\"%s?operation=GetCACert&message=test\", c.caURL))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed get request: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\tbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed reading response body: %w\", err)\n\t}\n\n\tt.Log(string(body))\n\n\t// SCEP CA/RA certificate selection. If there's only a single certificate, it will\n\t// be used as the CA certificate at all times. If there's multiple, the first certificate\n\t// is assumed to be the certificate of the recipient to encrypt messages to.\n\tswitch ct := resp.Header.Get(\"Content-Type\"); ct {\n\tcase \"application/x-x509-ca-cert\":\n\t\tcert, err := x509.ParseCertificate(body)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed parsing response body: %w\", err)\n\t\t}\n\t\tif _, ok := cert.PublicKey.(*rsa.PublicKey); !ok {\n\t\t\treturn fmt.Errorf(\"certificate has unexpected public key type %T\", cert.PublicKey)\n\t\t}\n\t\tc.caCert = cert\n\tcase \"application/x-x509-ca-ra-cert\":\n\t\tcerts, err := scep.CACerts(body)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed parsing response body: %w\", err)\n\t\t}\n\t\tcert := certs[0]\n\t\tif _, ok := cert.PublicKey.(*rsa.PublicKey); !ok {\n\t\t\treturn fmt.Errorf(\"certificate has unexpected public key type %T\", cert.PublicKey)\n\t\t}\n\t\tc.caCert = cert\n\tdefault:\n\t\treturn fmt.Errorf(\"unexpected content-type value %q\", ct)\n\t}\n\n\treturn nil\n}\n\ntype certificateParserFunc = func(der []byte) (*x509.Certificate, error)\n\ntype option func(o *options)\n\ntype options struct {\n\tcommonName        string\n\tsans              []string\n\tchallenge         string\n\ttemplate          *x509.Certificate\n\tsigner            crypto.Signer\n\tmessageType       scep.MessageType\n\tcertificateParser certificateParserFunc\n}\n\nfunc withChallenge(challenge string) option {\n\treturn func(o *options) {\n\t\to.challenge = challenge\n\t}\n}\n\nfunc withTemplate(tmpl *x509.Certificate) option {\n\treturn func(o *options) {\n\t\to.template = tmpl\n\t}\n}\n\nfunc withSigner(signer crypto.Signer) option {\n\treturn func(o *options) {\n\t\to.signer = signer\n\t}\n}\n\nfunc withMessageType(messageType scep.MessageType) option {\n\treturn func(o *options) {\n\t\to.messageType = messageType\n\t}\n}\n\nfunc withCertificateParser(certificateParser certificateParserFunc) option {\n\treturn func(o *options) {\n\t\to.certificateParser = certificateParser\n\t}\n}\n\nfunc (c *client) requestCertificate(t *testing.T, opts ...option) (*x509.Certificate, error) {\n\to := &options{\n\t\tcommonName:        \"test.localhost\",\n\t\tsans:              []string{\"test.localhost\"},\n\t\tchallenge:         \"the-challenge\",\n\t\tmessageType:       scep.PKCSReq,\n\t\tcertificateParser: x509.ParseCertificate,\n\t}\n\tfor _, applyTo := range opts {\n\t\tapplyTo(o)\n\t}\n\n\tif err := c.getCACert(t); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed getting CA certificate: %w\", err)\n\t}\n\n\tvar (\n\t\tsigner = o.signer\n\t\ttmpl   = o.template\n\t\terr    error\n\t)\n\tif signer == nil {\n\t\tsigner, err = rsa.GenerateKey(rand.Reader, 2048)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed creating SCEP private key: %w\", err)\n\t\t}\n\t}\n\n\tcsr, err := x509util.CreateCertificateRequest(o.commonName, o.sans, signer)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed creating CSR: %w\", err)\n\t}\n\n\tif tmpl == nil {\n\t\ttmpl = &x509.Certificate{\n\t\t\tSubject:        csr.Subject,\n\t\t\tPublicKey:      signer.Public(),\n\t\t\tSerialNumber:   big.NewInt(1),\n\t\t\tNotBefore:      time.Now().Add(-1 * time.Hour),\n\t\t\tNotAfter:       time.Now().Add(1 * time.Hour),\n\t\t\tDNSNames:       csr.DNSNames,\n\t\t\tIPAddresses:    csr.IPAddresses,\n\t\t\tEmailAddresses: csr.EmailAddresses,\n\t\t\tURIs:           csr.URIs,\n\t\t}\n\t}\n\n\tcrTmpl := &scepx509util.CertificateRequest{\n\t\tCertificateRequest: *csr,\n\t\tChallengePassword:  o.challenge,\n\t}\n\n\tnewCSR, err := scepx509util.CreateCertificateRequest(rand.Reader, crTmpl, signer)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed creating csr: %w\", err)\n\t}\n\n\tcr, err := x509.ParseCertificateRequest(newCSR)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed parsing certificate request: %w\", err)\n\t}\n\n\tselfSigned, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, signer.Public(), signer)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed creating self signed certificate: %w\", err)\n\t}\n\n\tselfSignedCertificate, err := o.certificateParser(selfSigned)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed parsing self signed certificate: %w\", err)\n\t}\n\n\tmsgTmpl := &scep.PKIMessage{\n\t\tTransactionID: \"test-1\",\n\t\tMessageType:   o.messageType,\n\t\tSenderNonce:   []byte(\"test-nonce-1\"),\n\t\tRecipients:    []*x509.Certificate{c.caCert},\n\t\tSignerCert:    selfSignedCertificate,\n\t\tSignerKey:     signer,\n\t}\n\n\tmsg, err := scep.NewCSRRequest(cr, msgTmpl)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed creating SCEP PKCSReq message: %w\", err)\n\t}\n\n\tt.Log(string(msg.Raw))\n\n\tu, err := url.Parse(c.caURL)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed parsing CA URL: %w\", err)\n\t}\n\n\topURL := u.ResolveReference(&url.URL{RawQuery: fmt.Sprintf(\"operation=PKIOperation&message=%s\", url.QueryEscape(base64.StdEncoding.EncodeToString(msg.Raw)))})\n\tresp, err := c.httpClient.Get(opURL.String())\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed get request: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\tif ct := resp.Header.Get(\"Content-Type\"); ct != \"application/x-pki-message\" {\n\t\treturn nil, fmt.Errorf(\"received unexpected content type %q\", ct)\n\t}\n\n\tbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed reading response body: %w\", err)\n\t}\n\n\tt.Log(string(body))\n\n\tsignedData, err := pkcs7.Parse(body)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed parsing response body: %w\", err)\n\t}\n\n\t// TODO: verify the signature?\n\n\tp7, err := pkcs7.Parse(signedData.Content)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed decrypting inner p7: %w\", err)\n\t}\n\n\tcontent, err := p7.Decrypt(selfSignedCertificate, signer)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed decrypting response: %w\", err)\n\t}\n\n\tp7, err = pkcs7.Parse(content)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed parsing p7 content: %w\", err)\n\t}\n\n\tcert := p7.Certificates[0]\n\n\treturn cert, nil\n}\n\nvar (\n\toidExtensionAuthorityKeyID = asn1.ObjectIdentifier{2, 5, 29, 35}\n\toidExtensionSubjectKeyID   = asn1.ObjectIdentifier{2, 5, 29, 14}\n)\n\ntype authorityKeyID struct {\n\tID []byte `asn1:\"optional,tag:0\"`\n}\n\ntype pkcs1PublicKey struct {\n\tN *big.Int\n\tE int\n}\n\nfunc createWindowsTemplate(t *testing.T, signer *rsa.PrivateKey) *x509.Certificate {\n\tt.Helper()\n\n\t// on Windows the self-signed certificate contains an authority key identifier\n\t// extension that is marked critical\n\tvalue, err := asn1.Marshal(authorityKeyID{[]byte(\"bla\")}) // fake value\n\trequire.NoError(t, err)\n\n\tauthorityKeyIDExtension := pkix.Extension{\n\t\tId:       oidExtensionAuthorityKeyID,\n\t\tCritical: true,\n\t\tValue:    value,\n\t}\n\n\t// determine the subject key ID\n\tpublicKeyBytes, err := asn1.Marshal(pkcs1PublicKey{\n\t\tN: signer.N,\n\t\tE: signer.E,\n\t})\n\trequire.NoError(t, err)\n\n\th := sha1.Sum(publicKeyBytes)\n\tsubjectKeyID := h[:]\n\n\t// create subject key ID extension\n\tvalue, err = asn1.Marshal(subjectKeyID)\n\trequire.NoError(t, err)\n\n\tsubjectKeyIDExtension := pkix.Extension{\n\t\tId:    oidExtensionSubjectKeyID,\n\t\tValue: value,\n\t}\n\n\treturn &x509.Certificate{\n\t\tSubject:            pkix.Name{CommonName: \"SCEP Protocol Certificate\"},\n\t\tSignatureAlgorithm: x509.SHA1WithRSA,\n\t\tPublicKey:          signer.Public(),\n\t\tSerialNumber:       big.NewInt(1),\n\t\tNotBefore:          time.Now().Add(-1 * time.Hour),\n\t\tNotAfter:           time.Now().Add(365 * 24 * time.Hour),\n\t\tExtraExtensions:    []pkix.Extension{authorityKeyIDExtension, subjectKeyIDExtension},\n\t}\n}\n\ntype testCAS struct {\n\tca *minica.CA\n}\n\nfunc (c *testCAS) CreateCertificate(req *apiv1.CreateCertificateRequest) (*apiv1.CreateCertificateResponse, error) {\n\tcert, err := c.ca.SignCSR(req.CSR)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed signing CSR: %w\", err)\n\t}\n\n\treturn &apiv1.CreateCertificateResponse{\n\t\tCertificate:      cert,\n\t\tCertificateChain: []*x509.Certificate{cert, c.ca.Intermediate},\n\t}, nil\n}\n\nfunc (c *testCAS) RenewCertificate(req *apiv1.RenewCertificateRequest) (*apiv1.RenewCertificateResponse, error) {\n\treturn nil, errors.New(\"not implemented\")\n}\n\nfunc (c *testCAS) RevokeCertificate(req *apiv1.RevokeCertificateRequest) (*apiv1.RevokeCertificateResponse, error) {\n\treturn nil, errors.New(\"not implemented\")\n}\n\nfunc (c *testCAS) GetCertificateAuthority(req *apiv1.GetCertificateAuthorityRequest) (*apiv1.GetCertificateAuthorityResponse, error) {\n\treturn &apiv1.GetCertificateAuthorityResponse{\n\t\tRootCertificate:          c.ca.Root,\n\t\tIntermediateCertificates: []*x509.Certificate{c.ca.Intermediate},\n\t}, nil\n}\n\nvar _ apiv1.CertificateAuthorityService = (*testCAS)(nil)\nvar _ apiv1.CertificateAuthorityGetter = (*testCAS)(nil)\n"
  },
  {
    "path": "test/integration/scep/decrypter_cas_test.go",
    "content": "package sceptest\n\nimport (\n\t\"context\"\n\t\"crypto\"\n\t\"crypto/x509\"\n\t\"crypto/x509/pkix\"\n\t\"encoding/json\"\n\t\"encoding/pem\"\n\t\"fmt\"\n\t\"math/big\"\n\t\"net\"\n\t\"net/http\"\n\t\"path/filepath\"\n\t\"sync\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"go.step.sm/crypto/keyutil\"\n\t\"go.step.sm/crypto/minica\"\n\t\"go.step.sm/crypto/pemutil\"\n\n\t\"github.com/smallstep/certificates/authority/config\"\n\t\"github.com/smallstep/certificates/authority/provisioner\"\n\t\"github.com/smallstep/certificates/ca\"\n\t\"github.com/smallstep/certificates/cas/apiv1\"\n)\n\nfunc TestIssuesCertificateUsingSCEPWithDecrypterAndUpstreamCAS(t *testing.T) {\n\tsigner, err := keyutil.GenerateSigner(\"EC\", \"P-256\", 0)\n\trequire.NoError(t, err)\n\n\tdir := t.TempDir()\n\tt.Setenv(\"STEPPATH\", dir)\n\n\tm, err := minica.New(minica.WithName(\"Step E2E | SCEP Decrypter w/ Upstream CAS\"), minica.WithGetSignerFunc(func() (crypto.Signer, error) {\n\t\treturn signer, nil\n\t}))\n\trequire.NoError(t, err)\n\n\trootFilepath := filepath.Join(dir, \"root.crt\")\n\t_, err = pemutil.Serialize(m.Root, pemutil.WithFilename(rootFilepath))\n\trequire.NoError(t, err)\n\n\tintermediateCertFilepath := filepath.Join(dir, \"intermediate.crt\")\n\t_, err = pemutil.Serialize(m.Intermediate, pemutil.WithFilename(intermediateCertFilepath))\n\trequire.NoError(t, err)\n\n\tintermediateKeyFilepath := filepath.Join(dir, \"intermediate.key\")\n\t_, err = pemutil.Serialize(m.Signer, pemutil.WithFilename(intermediateKeyFilepath))\n\trequire.NoError(t, err)\n\n\tdecrypterKey, err := keyutil.GenerateKey(\"RSA\", \"\", 2048)\n\trequire.NoError(t, err)\n\n\tdecrypter, ok := decrypterKey.(crypto.Decrypter)\n\trequire.True(t, ok)\n\n\tdecrypterCertifiate, err := m.Sign(&x509.Certificate{\n\t\tSubject:      pkix.Name{CommonName: \"decrypter\"},\n\t\tPublicKey:    decrypter.Public(),\n\t\tSerialNumber: big.NewInt(1),\n\t\tNotBefore:    time.Now().Add(-1 * time.Hour),\n\t\tNotAfter:     time.Now().Add(1 * time.Hour),\n\t\tDNSNames:     []string{\"decrypter\"},\n\t})\n\trequire.NoError(t, err)\n\n\tb, err := pemutil.Serialize(decrypterCertifiate)\n\trequire.NoError(t, err)\n\tdecrypterCertificatePEMBytes := pem.EncodeToMemory(b)\n\n\tb, err = pemutil.Serialize(decrypter, pemutil.WithPassword([]byte(\"1234\")))\n\trequire.NoError(t, err)\n\tdecrypterKeyPEMBytes := pem.EncodeToMemory(b)\n\n\t// get a random address to listen on and connect to; currently no nicer way to get one before starting the server\n\t// TODO(hs): find/implement a nicer way to expose the CA URL, similar to how e.g. httptest.Server exposes it?\n\thost, port := reservePort(t)\n\n\tprov := &provisioner.SCEP{\n\t\tID:                            \"scep\",\n\t\tName:                          \"scep\",\n\t\tType:                          \"SCEP\",\n\t\tForceCN:                       false,\n\t\tChallengePassword:             \"the-challenge\",\n\t\tEncryptionAlgorithmIdentifier: 2,\n\t\tMinimumPublicKeyLength:        2048,\n\t\tClaims:                        &config.GlobalProvisionerClaims,\n\t\tDecrypterCertificate:          decrypterCertificatePEMBytes,\n\t\tDecrypterKeyPEM:               decrypterKeyPEMBytes,\n\t\tDecrypterKeyPassword:          \"1234\",\n\t}\n\n\terr = prov.Init(provisioner.Config{})\n\trequire.NoError(t, err)\n\n\tapiv1.Register(\"test-scep-cas\", func(_ context.Context, opts apiv1.Options) (apiv1.CertificateAuthorityService, error) {\n\t\treturn &testCAS{\n\t\t\tca: m,\n\t\t}, nil\n\t})\n\n\tcfg := &config.Config{\n\t\tAddress:  net.JoinHostPort(host, port), // reuse the address that was just \"reserved\"\n\t\tDNSNames: []string{\"127.0.0.1\", \"[::1]\", \"localhost\"},\n\t\tAuthorityConfig: &config.AuthConfig{\n\t\t\tOptions: &apiv1.Options{\n\t\t\t\tAuthorityID:          \"stepca-test-scep\",\n\t\t\t\tType:                 \"test-scep-cas\",\n\t\t\t\tCertificateAuthority: \"test-cas\",\n\t\t\t},\n\t\t\tAuthorityID:    \"stepca-test-scep\",\n\t\t\tDeploymentType: \"standalone-test\",\n\t\t\tProvisioners:   provisioner.List{prov},\n\t\t},\n\t\tLogger: json.RawMessage(`{\"format\": \"text\"}`),\n\t}\n\tc, err := ca.New(cfg)\n\trequire.NoError(t, err)\n\n\tvar wg sync.WaitGroup\n\twg.Add(1)\n\n\tgo func() {\n\t\tdefer wg.Done()\n\t\terr = c.Run()\n\t\trequire.ErrorIs(t, err, http.ErrServerClosed)\n\t}()\n\n\t// instantiate a client for the CA running at the random address\n\tcaClient := newCAClient(t, fmt.Sprintf(\"https://localhost:%s\", port), rootFilepath)\n\trequireHealthyCA(t, caClient)\n\n\tscepClient := createSCEPClient(t, fmt.Sprintf(\"https://localhost:%s/scep/scep\", port), m.Root)\n\tcert, err := scepClient.requestCertificate(t)\n\tassert.NoError(t, err)\n\trequire.NotNil(t, cert)\n\n\tassert.Equal(t, \"test.localhost\", cert.Subject.CommonName)\n\tassert.Equal(t, \"Step E2E | SCEP Decrypter w/ Upstream CAS Intermediate CA\", cert.Issuer.CommonName)\n\n\t// done testing; stop and wait for the server to quit\n\terr = c.Stop()\n\trequire.NoError(t, err)\n\n\twg.Wait()\n}\n"
  },
  {
    "path": "test/integration/scep/decrypter_test.go",
    "content": "package sceptest\n\nimport (\n\t\"crypto\"\n\t\"crypto/x509\"\n\t\"crypto/x509/pkix\"\n\t\"encoding/json\"\n\t\"encoding/pem\"\n\t\"fmt\"\n\t\"math/big\"\n\t\"net\"\n\t\"net/http\"\n\t\"path/filepath\"\n\t\"sync\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"go.step.sm/crypto/keyutil\"\n\t\"go.step.sm/crypto/minica\"\n\t\"go.step.sm/crypto/pemutil\"\n\n\t\"github.com/smallstep/certificates/authority/config\"\n\t\"github.com/smallstep/certificates/authority/provisioner\"\n\t\"github.com/smallstep/certificates/ca\"\n)\n\nfunc TestIssuesCertificateUsingSCEPWithDecrypter(t *testing.T) {\n\tsigner, err := keyutil.GenerateSigner(\"EC\", \"P-256\", 0)\n\trequire.NoError(t, err)\n\n\tdir := t.TempDir()\n\tt.Setenv(\"STEPPATH\", dir)\n\n\tm, err := minica.New(minica.WithName(\"Step E2E | SCEP Decrypter\"), minica.WithGetSignerFunc(func() (crypto.Signer, error) {\n\t\treturn signer, nil\n\t}))\n\trequire.NoError(t, err)\n\n\trootFilepath := filepath.Join(dir, \"root.crt\")\n\t_, err = pemutil.Serialize(m.Root, pemutil.WithFilename(rootFilepath))\n\trequire.NoError(t, err)\n\n\tintermediateCertFilepath := filepath.Join(dir, \"intermediate.crt\")\n\t_, err = pemutil.Serialize(m.Intermediate, pemutil.WithFilename(intermediateCertFilepath))\n\trequire.NoError(t, err)\n\n\tintermediateKeyFilepath := filepath.Join(dir, \"intermediate.key\")\n\t_, err = pemutil.Serialize(m.Signer, pemutil.WithFilename(intermediateKeyFilepath))\n\trequire.NoError(t, err)\n\n\tdecrypterKey, err := keyutil.GenerateKey(\"RSA\", \"\", 2048)\n\trequire.NoError(t, err)\n\n\tdecrypter, ok := decrypterKey.(crypto.Decrypter)\n\trequire.True(t, ok)\n\n\tdecrypterCertifiate, err := m.Sign(&x509.Certificate{\n\t\tSubject:      pkix.Name{CommonName: \"decrypter\"},\n\t\tPublicKey:    decrypter.Public(),\n\t\tSerialNumber: big.NewInt(1),\n\t\tNotBefore:    time.Now().Add(-1 * time.Hour),\n\t\tNotAfter:     time.Now().Add(1 * time.Hour),\n\t\tDNSNames:     []string{\"decrypter\"},\n\t})\n\trequire.NoError(t, err)\n\n\tb, err := pemutil.Serialize(decrypterCertifiate)\n\trequire.NoError(t, err)\n\tdecrypterCertificatePEMBytes := pem.EncodeToMemory(b)\n\n\tb, err = pemutil.Serialize(decrypter, pemutil.WithPassword([]byte(\"1234\")))\n\trequire.NoError(t, err)\n\tdecrypterKeyPEMBytes := pem.EncodeToMemory(b)\n\n\t// get a random address to listen on and connect to; currently no nicer way to get one before starting the server\n\t// TODO(hs): find/implement a nicer way to expose the CA URL, similar to how e.g. httptest.Server exposes it?\n\thost, port := reservePort(t)\n\n\tprov := &provisioner.SCEP{\n\t\tID:                            \"scep\",\n\t\tName:                          \"scep\",\n\t\tType:                          \"SCEP\",\n\t\tForceCN:                       false,\n\t\tChallengePassword:             \"the-challenge\",\n\t\tEncryptionAlgorithmIdentifier: 2,\n\t\tMinimumPublicKeyLength:        2048,\n\t\tClaims:                        &config.GlobalProvisionerClaims,\n\t\tDecrypterCertificate:          decrypterCertificatePEMBytes,\n\t\tDecrypterKeyPEM:               decrypterKeyPEMBytes,\n\t\tDecrypterKeyPassword:          \"1234\",\n\t}\n\n\terr = prov.Init(provisioner.Config{})\n\trequire.NoError(t, err)\n\n\tcfg := &config.Config{\n\t\tRoot:             []string{rootFilepath},\n\t\tIntermediateCert: intermediateCertFilepath,\n\t\tIntermediateKey:  intermediateKeyFilepath,\n\t\tAddress:          net.JoinHostPort(host, port), // reuse the address that was just \"reserved\"\n\t\tDNSNames:         []string{\"127.0.0.1\", \"[::1]\", \"localhost\"},\n\t\tAuthorityConfig: &config.AuthConfig{\n\t\t\tAuthorityID:    \"stepca-test-scep\",\n\t\t\tDeploymentType: \"standalone-test\",\n\t\t\tProvisioners:   provisioner.List{prov},\n\t\t},\n\t\tLogger: json.RawMessage(`{\"format\": \"text\"}`),\n\t}\n\tc, err := ca.New(cfg)\n\trequire.NoError(t, err)\n\n\tvar wg sync.WaitGroup\n\twg.Add(1)\n\n\tgo func() {\n\t\tdefer wg.Done()\n\t\terr = c.Run()\n\t\trequire.ErrorIs(t, err, http.ErrServerClosed)\n\t}()\n\n\t// instantiate a client for the CA running at the random address\n\tcaClient := newCAClient(t, fmt.Sprintf(\"https://localhost:%s\", port), rootFilepath)\n\trequireHealthyCA(t, caClient)\n\n\tscepClient := createSCEPClient(t, fmt.Sprintf(\"https://localhost:%s/scep/scep\", port), m.Root)\n\tcert, err := scepClient.requestCertificate(t)\n\tassert.NoError(t, err)\n\trequire.NotNil(t, cert)\n\n\tassert.Equal(t, \"test.localhost\", cert.Subject.CommonName)\n\tassert.Equal(t, \"Step E2E | SCEP Decrypter Intermediate CA\", cert.Issuer.CommonName)\n\n\t// done testing; stop and wait for the server to quit\n\terr = c.Stop()\n\trequire.NoError(t, err)\n\n\twg.Wait()\n}\n"
  },
  {
    "path": "test/integration/scep/internal/x509/debug.go",
    "content": "package legacyx509\n\nimport \"fmt\"\n\n// legacyGodebugSetting is a type mimicking Go's internal godebug package\n// settings, which are used to enable / disable certain functionalities at\n// build time.\ntype legacyGodebugSetting int\n\nfunc (s legacyGodebugSetting) Value() string {\n\treturn fmt.Sprintf(\"%d\", s)\n}\n\nfunc (s legacyGodebugSetting) IncNonDefault() {}\n"
  },
  {
    "path": "test/integration/scep/internal/x509/doc.go",
    "content": "/*\nPackage legacyx509 is a copy of certain parts of Go's crypto/x509 package.\nIt is based on Go 1.23, and has just the parts copied over required for\nparsing X509 certificates.\n\nThe copy in this repository is intended to be used for preparing a SCEP request\nemulating a Windows SCEP client. The Windows SCEP client marks the authority\nkey identifier as critical in the self-signed SCEP enrolment certificate, which\nfails to parse using the standard X509 parser in Go 1.23 and later.\n\nThis is itself a copy from the copy in our PKCS7 package. We currently don't\nintend to maintain that in an importable package, since we only need these copies\nfor testing purposes, hence needing another copy of the code.\n*/\npackage legacyx509\n"
  },
  {
    "path": "test/integration/scep/internal/x509/oid.go",
    "content": "// Copyright 2023 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\n//nolint:staticcheck,gocritic // code copied from crypto/x509\npackage legacyx509\n\nimport (\n\t\"bytes\"\n\t\"encoding/asn1\"\n\t\"errors\"\n\t\"math\"\n\t\"math/big\"\n\t\"math/bits\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/smallstep/certificates/internal/cast\"\n)\n\nvar errInvalidOID = errors.New(\"invalid oid\")\n\n// An OID represents an ASN.1 OBJECT IDENTIFIER.\ntype OID struct {\n\tder []byte\n}\n\n// ParseOID parses a Object Identifier string, represented by ASCII numbers separated by dots.\nfunc ParseOID(oid string) (OID, error) {\n\tvar o OID\n\treturn o, o.unmarshalOIDText(oid)\n}\n\nfunc newOIDFromDER(der []byte) (OID, bool) {\n\tif len(der) == 0 || der[len(der)-1]&0x80 != 0 {\n\t\treturn OID{}, false\n\t}\n\n\tstart := 0\n\tfor i, v := range der {\n\t\t// ITU-T X.690, section 8.19.2:\n\t\t// The subidentifier shall be encoded in the fewest possible octets,\n\t\t// that is, the leading octet of the subidentifier shall not have the value 0x80.\n\t\tif i == start && v == 0x80 {\n\t\t\treturn OID{}, false\n\t\t}\n\t\tif v&0x80 == 0 {\n\t\t\tstart = i + 1\n\t\t}\n\t}\n\n\treturn OID{der}, true\n}\n\n// OIDFromInts creates a new OID using ints, each integer is a separate component.\nfunc OIDFromInts(oid []uint64) (OID, error) {\n\tif len(oid) < 2 || oid[0] > 2 || (oid[0] < 2 && oid[1] >= 40) {\n\t\treturn OID{}, errInvalidOID\n\t}\n\n\tlength := base128IntLength(oid[0]*40 + oid[1])\n\tfor _, v := range oid[2:] {\n\t\tlength += base128IntLength(v)\n\t}\n\n\tder := make([]byte, 0, length)\n\tder = appendBase128Int(der, oid[0]*40+oid[1])\n\tfor _, v := range oid[2:] {\n\t\tder = appendBase128Int(der, v)\n\t}\n\treturn OID{der}, nil\n}\n\nfunc base128IntLength(n uint64) int {\n\tif n == 0 {\n\t\treturn 1\n\t}\n\treturn (bits.Len64(n) + 6) / 7\n}\n\nfunc appendBase128Int(dst []byte, n uint64) []byte {\n\tfor i := base128IntLength(n) - 1; i >= 0; i-- {\n\t\to := byte(n >> cast.Uint(i*7)) //nolint:gosec // masked to 7 bits below\n\t\to &= 0x7f\n\t\tif i != 0 {\n\t\t\to |= 0x80\n\t\t}\n\t\tdst = append(dst, o)\n\t}\n\treturn dst\n}\n\nfunc base128BigIntLength(n *big.Int) int {\n\tif n.Cmp(big.NewInt(0)) == 0 {\n\t\treturn 1\n\t}\n\treturn (n.BitLen() + 6) / 7\n}\n\nfunc appendBase128BigInt(dst []byte, n *big.Int) []byte {\n\tif n.Cmp(big.NewInt(0)) == 0 {\n\t\treturn append(dst, 0)\n\t}\n\n\tfor i := base128BigIntLength(n) - 1; i >= 0; i-- {\n\t\to := byte(big.NewInt(0).Rsh(n, uint(i)*7).Bits()[0]) //nolint:gosec // intentional truncation, masked to 0x7f immediately after\n\t\to &= 0x7f\n\t\tif i != 0 {\n\t\t\to |= 0x80\n\t\t}\n\t\tdst = append(dst, o)\n\t}\n\treturn dst\n}\n\n// AppendText implements [encoding.TextAppender]\nfunc (o OID) AppendText(b []byte) ([]byte, error) {\n\treturn append(b, o.String()...), nil\n}\n\n// MarshalText implements [encoding.TextMarshaler]\nfunc (o OID) MarshalText() ([]byte, error) {\n\treturn o.AppendText(nil)\n}\n\n// UnmarshalText implements [encoding.TextUnmarshaler]\nfunc (o *OID) UnmarshalText(text []byte) error {\n\treturn o.unmarshalOIDText(string(text))\n}\n\n// cutString slices s around the first instance of sep,\n// returning the text before and after sep.\n// The found result reports whether sep appears in s.\n// If sep does not appear in s, cut returns s, \"\", false.\nfunc cutString(s, sep string) (before, after string, found bool) {\n\tif i := strings.Index(s, sep); i >= 0 {\n\t\treturn s[:i], s[i+len(sep):], true\n\t}\n\treturn s, \"\", false\n}\n\nfunc (o *OID) unmarshalOIDText(oid string) error {\n\t// (*big.Int).SetString allows +/- signs, but we don't want\n\t// to allow them in the string representation of Object Identifier, so\n\t// reject such encodings.\n\tfor _, c := range oid {\n\t\tisDigit := c >= '0' && c <= '9'\n\t\tif !isDigit && c != '.' {\n\t\t\treturn errInvalidOID\n\t\t}\n\t}\n\n\tvar (\n\t\tfirstNum  string\n\t\tsecondNum string\n\t)\n\n\tvar nextComponentExists bool\n\tfirstNum, oid, nextComponentExists = cutString(oid, \".\")\n\tif !nextComponentExists {\n\t\treturn errInvalidOID\n\t}\n\tsecondNum, oid, nextComponentExists = cutString(oid, \".\")\n\n\tvar (\n\t\tfirst  = big.NewInt(0)\n\t\tsecond = big.NewInt(0)\n\t)\n\n\tif _, ok := first.SetString(firstNum, 10); !ok {\n\t\treturn errInvalidOID\n\t}\n\tif _, ok := second.SetString(secondNum, 10); !ok {\n\t\treturn errInvalidOID\n\t}\n\n\tif first.Cmp(big.NewInt(2)) > 0 || (first.Cmp(big.NewInt(2)) < 0 && second.Cmp(big.NewInt(40)) >= 0) {\n\t\treturn errInvalidOID\n\t}\n\n\tfirstComponent := first.Mul(first, big.NewInt(40))\n\tfirstComponent.Add(firstComponent, second)\n\n\tder := appendBase128BigInt(make([]byte, 0, 32), firstComponent)\n\n\tfor nextComponentExists {\n\t\tvar strNum string\n\t\tstrNum, oid, nextComponentExists = cutString(oid, \".\")\n\t\tb, ok := big.NewInt(0).SetString(strNum, 10)\n\t\tif !ok {\n\t\t\treturn errInvalidOID\n\t\t}\n\t\tder = appendBase128BigInt(der, b)\n\t}\n\n\to.der = der\n\treturn nil\n}\n\n// AppendBinary implements [encoding.BinaryAppender]\nfunc (o OID) AppendBinary(b []byte) ([]byte, error) {\n\treturn append(b, o.der...), nil\n}\n\n// MarshalBinary implements [encoding.BinaryMarshaler]\nfunc (o OID) MarshalBinary() ([]byte, error) {\n\treturn o.AppendBinary(nil)\n}\n\n// cloneBytes returns a copy of b[:len(b)].\n// The result may have additional unused capacity.\n// Clone(nil) returns nil.\nfunc cloneBytes(b []byte) []byte {\n\tif b == nil {\n\t\treturn nil\n\t}\n\treturn append([]byte{}, b...)\n}\n\n// UnmarshalBinary implements [encoding.BinaryUnmarshaler]\nfunc (o *OID) UnmarshalBinary(b []byte) error {\n\toid, ok := newOIDFromDER(cloneBytes(b))\n\tif !ok {\n\t\treturn errInvalidOID\n\t}\n\t*o = oid\n\treturn nil\n}\n\n// Equal returns true when oid and other represents the same Object Identifier.\nfunc (oid OID) Equal(other OID) bool {\n\t// There is only one possible DER encoding of\n\t// each unique Object Identifier.\n\treturn bytes.Equal(oid.der, other.der)\n}\n\nfunc parseBase128Int(bytes []byte, initOffset int) (ret, offset int, failed bool) {\n\toffset = initOffset\n\tvar ret64 int64\n\tfor shifted := 0; offset < len(bytes); shifted++ {\n\t\t// 5 * 7 bits per byte == 35 bits of data\n\t\t// Thus the representation is either non-minimal or too large for an int32\n\t\tif shifted == 5 {\n\t\t\tfailed = true\n\t\t\treturn\n\t\t}\n\t\tret64 <<= 7\n\t\tb := bytes[offset]\n\t\t// integers should be minimally encoded, so the leading octet should\n\t\t// never be 0x80\n\t\tif shifted == 0 && b == 0x80 {\n\t\t\tfailed = true\n\t\t\treturn\n\t\t}\n\t\tret64 |= int64(b & 0x7f)\n\t\toffset++\n\t\tif b&0x80 == 0 {\n\t\t\tret = int(ret64)\n\t\t\t// Ensure that the returned value fits in an int on all platforms\n\t\t\tif ret64 > math.MaxInt32 {\n\t\t\t\tfailed = true\n\t\t\t}\n\t\t\treturn\n\t\t}\n\t}\n\tfailed = true\n\treturn\n}\n\n// EqualASN1OID returns whether an OID equals an asn1.ObjectIdentifier. If\n// asn1.ObjectIdentifier cannot represent the OID specified by oid, because\n// a component of OID requires more than 31 bits, it returns false.\nfunc (oid OID) EqualASN1OID(other asn1.ObjectIdentifier) bool {\n\tif len(other) < 2 {\n\t\treturn false\n\t}\n\tv, offset, failed := parseBase128Int(oid.der, 0)\n\tif failed {\n\t\t// This should never happen, since we've already parsed the OID,\n\t\t// but just in case.\n\t\treturn false\n\t}\n\tif v < 80 {\n\t\ta, b := v/40, v%40\n\t\tif other[0] != a || other[1] != b {\n\t\t\treturn false\n\t\t}\n\t} else {\n\t\ta, b := 2, v-80\n\t\tif other[0] != a || other[1] != b {\n\t\t\treturn false\n\t\t}\n\t}\n\n\ti := 2\n\tfor ; offset < len(oid.der); i++ {\n\t\tv, offset, failed = parseBase128Int(oid.der, offset)\n\t\tif failed {\n\t\t\t// Again, shouldn't happen, since we've already parsed\n\t\t\t// the OID, but better safe than sorry.\n\t\t\treturn false\n\t\t}\n\t\tif i >= len(other) || v != other[i] {\n\t\t\treturn false\n\t\t}\n\t}\n\n\treturn i == len(other)\n}\n\n// Strings returns the string representation of the Object Identifier.\nfunc (oid OID) String() string {\n\tvar b strings.Builder\n\tb.Grow(32)\n\tconst (\n\t\tvalSize         = 64 // size in bits of val.\n\t\tbitsPerByte     = 7\n\t\tmaxValSafeShift = (1 << (valSize - bitsPerByte)) - 1\n\t)\n\tvar (\n\t\tstart    = 0\n\t\tval      = uint64(0)\n\t\tnumBuf   = make([]byte, 0, 21)\n\t\tbigVal   *big.Int\n\t\toverflow bool\n\t)\n\tfor i, v := range oid.der {\n\t\tcurVal := v & 0x7F\n\t\tvalEnd := v&0x80 == 0\n\t\tif valEnd {\n\t\t\tif start != 0 {\n\t\t\t\tb.WriteByte('.')\n\t\t\t}\n\t\t}\n\t\tif !overflow && val > maxValSafeShift {\n\t\t\tif bigVal == nil {\n\t\t\t\tbigVal = new(big.Int)\n\t\t\t}\n\t\t\tbigVal = bigVal.SetUint64(val)\n\t\t\toverflow = true\n\t\t}\n\t\tif overflow {\n\t\t\tbigVal = bigVal.Lsh(bigVal, bitsPerByte).Or(bigVal, big.NewInt(int64(curVal)))\n\t\t\tif valEnd {\n\t\t\t\tif start == 0 {\n\t\t\t\t\tb.WriteString(\"2.\")\n\t\t\t\t\tbigVal = bigVal.Sub(bigVal, big.NewInt(80))\n\t\t\t\t}\n\t\t\t\tnumBuf = bigVal.Append(numBuf, 10)\n\t\t\t\tb.Write(numBuf)\n\t\t\t\tnumBuf = numBuf[:0]\n\t\t\t\tval = 0\n\t\t\t\tstart = i + 1\n\t\t\t\toverflow = false\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tval <<= bitsPerByte\n\t\tval |= uint64(curVal)\n\t\tif valEnd {\n\t\t\tif start == 0 {\n\t\t\t\tif val < 80 {\n\t\t\t\t\tb.Write(strconv.AppendUint(numBuf, val/40, 10))\n\t\t\t\t\tb.WriteByte('.')\n\t\t\t\t\tb.Write(strconv.AppendUint(numBuf, val%40, 10))\n\t\t\t\t} else {\n\t\t\t\t\tb.WriteString(\"2.\")\n\t\t\t\t\tb.Write(strconv.AppendUint(numBuf, val-80, 10))\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tb.Write(strconv.AppendUint(numBuf, val, 10))\n\t\t\t}\n\t\t\tval = 0\n\t\t\tstart = i + 1\n\t\t}\n\t}\n\treturn b.String()\n}\n"
  },
  {
    "path": "test/integration/scep/internal/x509/parser.go",
    "content": "// Copyright 2021 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\n//nolint:gocritic,errorlint,unconvert,staticcheck // code copied from crypto/x509\npackage legacyx509\n\nimport (\n\t\"bytes\"\n\t\"crypto/dsa\"\n\t\"crypto/ecdsa\"\n\t\"crypto/ed25519\"\n\t\"crypto/elliptic\"\n\t\"crypto/rsa\"\n\t\"crypto/x509/pkix\"\n\t\"encoding/asn1\"\n\t\"errors\"\n\t\"fmt\"\n\t\"math/big\"\n\t\"net\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\t\"unicode/utf16\"\n\t\"unicode/utf8\"\n\n\t\"golang.org/x/crypto/cryptobyte\"\n\tcryptobyte_asn1 \"golang.org/x/crypto/cryptobyte/asn1\"\n\n\tstdx509 \"crypto/x509\"\n)\n\n// ParseCertificates parses one or more certificates from the given ASN.1 DER\n// data. The certificates must be concatenated with no intermediate padding.\nfunc ParseCertificates(der []byte) ([]*stdx509.Certificate, error) {\n\tvar certs []*stdx509.Certificate\n\tfor len(der) > 0 {\n\t\tcert, err := parseCertificate(der)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tcerts = append(certs, cert)\n\t\tder = der[len(cert.Raw):]\n\t}\n\treturn certs, nil\n}\n\n// isPrintable reports whether the given b is in the ASN.1 PrintableString set.\n// This is a simplified version of encoding/asn1.isPrintable.\nfunc isPrintable(b byte) bool {\n\treturn 'a' <= b && b <= 'z' ||\n\t\t'A' <= b && b <= 'Z' ||\n\t\t'0' <= b && b <= '9' ||\n\t\t'\\'' <= b && b <= ')' ||\n\t\t'+' <= b && b <= '/' ||\n\t\tb == ' ' ||\n\t\tb == ':' ||\n\t\tb == '=' ||\n\t\tb == '?' ||\n\t\t// This is technically not allowed in a PrintableString.\n\t\t// However, x509 certificates with wildcard strings don't\n\t\t// always use the correct string type so we permit it.\n\t\tb == '*' ||\n\t\t// This is not technically allowed either. However, not\n\t\t// only is it relatively common, but there are also a\n\t\t// handful of CA certificates that contain it. At least\n\t\t// one of which will not expire until 2027.\n\t\tb == '&'\n}\n\n// parseASN1String parses the ASN.1 string types T61String, PrintableString,\n// UTF8String, BMPString, IA5String, and NumericString. This is mostly copied\n// from the respective encoding/asn1.parse... methods, rather than just\n// increasing the API surface of that package.\nfunc parseASN1String(tag cryptobyte_asn1.Tag, value []byte) (string, error) {\n\tswitch tag {\n\tcase cryptobyte_asn1.T61String:\n\t\treturn string(value), nil\n\tcase cryptobyte_asn1.PrintableString:\n\t\tfor _, b := range value {\n\t\t\tif !isPrintable(b) {\n\t\t\t\treturn \"\", errors.New(\"invalid PrintableString\")\n\t\t\t}\n\t\t}\n\t\treturn string(value), nil\n\tcase cryptobyte_asn1.UTF8String:\n\t\tif !utf8.Valid(value) {\n\t\t\treturn \"\", errors.New(\"invalid UTF-8 string\")\n\t\t}\n\t\treturn string(value), nil\n\tcase cryptobyte_asn1.Tag(asn1.TagBMPString):\n\t\tif len(value)%2 != 0 {\n\t\t\treturn \"\", errors.New(\"invalid BMPString\")\n\t\t}\n\n\t\t// Strip terminator if present.\n\t\tif l := len(value); l >= 2 && value[l-1] == 0 && value[l-2] == 0 {\n\t\t\tvalue = value[:l-2]\n\t\t}\n\n\t\ts := make([]uint16, 0, len(value)/2)\n\t\tfor len(value) > 0 {\n\t\t\ts = append(s, uint16(value[0])<<8+uint16(value[1]))\n\t\t\tvalue = value[2:]\n\t\t}\n\n\t\treturn string(utf16.Decode(s)), nil\n\tcase cryptobyte_asn1.IA5String:\n\t\ts := string(value)\n\t\tif isIA5String(s) != nil {\n\t\t\treturn \"\", errors.New(\"invalid IA5String\")\n\t\t}\n\t\treturn s, nil\n\tcase cryptobyte_asn1.Tag(asn1.TagNumericString):\n\t\tfor _, b := range value {\n\t\t\tif !('0' <= b && b <= '9' || b == ' ') {\n\t\t\t\treturn \"\", errors.New(\"invalid NumericString\")\n\t\t\t}\n\t\t}\n\t\treturn string(value), nil\n\t}\n\treturn \"\", fmt.Errorf(\"unsupported string type: %v\", tag)\n}\n\n// parseName parses a DER encoded Name as defined in RFC 5280. We may\n// want to export this function in the future for use in crypto/tls.\nfunc parseName(raw cryptobyte.String) (*pkix.RDNSequence, error) {\n\tif !raw.ReadASN1(&raw, cryptobyte_asn1.SEQUENCE) {\n\t\treturn nil, errors.New(\"x509: invalid RDNSequence\")\n\t}\n\n\tvar rdnSeq pkix.RDNSequence\n\tfor !raw.Empty() {\n\t\tvar rdnSet pkix.RelativeDistinguishedNameSET\n\t\tvar set cryptobyte.String\n\t\tif !raw.ReadASN1(&set, cryptobyte_asn1.SET) {\n\t\t\treturn nil, errors.New(\"x509: invalid RDNSequence\")\n\t\t}\n\t\tfor !set.Empty() {\n\t\t\tvar atav cryptobyte.String\n\t\t\tif !set.ReadASN1(&atav, cryptobyte_asn1.SEQUENCE) {\n\t\t\t\treturn nil, errors.New(\"x509: invalid RDNSequence: invalid attribute\")\n\t\t\t}\n\t\t\tvar attr pkix.AttributeTypeAndValue\n\t\t\tif !atav.ReadASN1ObjectIdentifier(&attr.Type) {\n\t\t\t\treturn nil, errors.New(\"x509: invalid RDNSequence: invalid attribute type\")\n\t\t\t}\n\t\t\tvar rawValue cryptobyte.String\n\t\t\tvar valueTag cryptobyte_asn1.Tag\n\t\t\tif !atav.ReadAnyASN1(&rawValue, &valueTag) {\n\t\t\t\treturn nil, errors.New(\"x509: invalid RDNSequence: invalid attribute value\")\n\t\t\t}\n\t\t\tvar err error\n\t\t\tattr.Value, err = parseASN1String(valueTag, rawValue)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"x509: invalid RDNSequence: invalid attribute value: %s\", err)\n\t\t\t}\n\t\t\trdnSet = append(rdnSet, attr)\n\t\t}\n\n\t\trdnSeq = append(rdnSeq, rdnSet)\n\t}\n\n\treturn &rdnSeq, nil\n}\n\nfunc parseAI(der cryptobyte.String) (pkix.AlgorithmIdentifier, error) {\n\tai := pkix.AlgorithmIdentifier{}\n\tif !der.ReadASN1ObjectIdentifier(&ai.Algorithm) {\n\t\treturn ai, errors.New(\"x509: malformed OID\")\n\t}\n\tif der.Empty() {\n\t\treturn ai, nil\n\t}\n\tvar params cryptobyte.String\n\tvar tag cryptobyte_asn1.Tag\n\tif !der.ReadAnyASN1Element(&params, &tag) {\n\t\treturn ai, errors.New(\"x509: malformed parameters\")\n\t}\n\tai.Parameters.Tag = int(tag)\n\tai.Parameters.FullBytes = params\n\treturn ai, nil\n}\n\nfunc parseTime(der *cryptobyte.String) (time.Time, error) {\n\tvar t time.Time\n\tswitch {\n\tcase der.PeekASN1Tag(cryptobyte_asn1.UTCTime):\n\t\tif !der.ReadASN1UTCTime(&t) {\n\t\t\treturn t, errors.New(\"x509: malformed UTCTime\")\n\t\t}\n\tcase der.PeekASN1Tag(cryptobyte_asn1.GeneralizedTime):\n\t\tif !der.ReadASN1GeneralizedTime(&t) {\n\t\t\treturn t, errors.New(\"x509: malformed GeneralizedTime\")\n\t\t}\n\tdefault:\n\t\treturn t, errors.New(\"x509: unsupported time format\")\n\t}\n\treturn t, nil\n}\n\nfunc parseValidity(der cryptobyte.String) (time.Time, time.Time, error) {\n\tnotBefore, err := parseTime(&der)\n\tif err != nil {\n\t\treturn time.Time{}, time.Time{}, err\n\t}\n\tnotAfter, err := parseTime(&der)\n\tif err != nil {\n\t\treturn time.Time{}, time.Time{}, err\n\t}\n\n\treturn notBefore, notAfter, nil\n}\n\nfunc parseExtension(der cryptobyte.String) (pkix.Extension, error) {\n\tvar ext pkix.Extension\n\tif !der.ReadASN1ObjectIdentifier(&ext.Id) {\n\t\treturn ext, errors.New(\"x509: malformed extension OID field\")\n\t}\n\tif der.PeekASN1Tag(cryptobyte_asn1.BOOLEAN) {\n\t\tif !der.ReadASN1Boolean(&ext.Critical) {\n\t\t\treturn ext, errors.New(\"x509: malformed extension critical field\")\n\t\t}\n\t}\n\tvar val cryptobyte.String\n\tif !der.ReadASN1(&val, cryptobyte_asn1.OCTET_STRING) {\n\t\treturn ext, errors.New(\"x509: malformed extension value field\")\n\t}\n\text.Value = val\n\treturn ext, nil\n}\n\nfunc parsePublicKey(keyData *publicKeyInfo) (interface{}, error) {\n\toid := keyData.Algorithm.Algorithm\n\tparams := keyData.Algorithm.Parameters\n\tder := cryptobyte.String(keyData.PublicKey.RightAlign())\n\tswitch {\n\tcase oid.Equal(oidPublicKeyRSA):\n\t\t// RSA public keys must have a NULL in the parameters.\n\t\t// See RFC 3279, Section 2.3.1.\n\t\tif !bytes.Equal(params.FullBytes, asn1.NullBytes) {\n\t\t\treturn nil, errors.New(\"x509: RSA key missing NULL parameters\")\n\t\t}\n\n\t\tp := &pkcs1PublicKey{N: new(big.Int)}\n\t\tif !der.ReadASN1(&der, cryptobyte_asn1.SEQUENCE) {\n\t\t\treturn nil, errors.New(\"x509: invalid RSA public key\")\n\t\t}\n\t\tif !der.ReadASN1Integer(p.N) {\n\t\t\treturn nil, errors.New(\"x509: invalid RSA modulus\")\n\t\t}\n\t\tif !der.ReadASN1Integer(&p.E) {\n\t\t\treturn nil, errors.New(\"x509: invalid RSA public exponent\")\n\t\t}\n\n\t\tif p.N.Sign() <= 0 {\n\t\t\treturn nil, errors.New(\"x509: RSA modulus is not a positive number\")\n\t\t}\n\t\tif p.E <= 0 {\n\t\t\treturn nil, errors.New(\"x509: RSA public exponent is not a positive number\")\n\t\t}\n\n\t\tpub := &rsa.PublicKey{\n\t\t\tE: p.E,\n\t\t\tN: p.N,\n\t\t}\n\t\treturn pub, nil\n\tcase oid.Equal(oidPublicKeyECDSA):\n\t\tparamsDer := cryptobyte.String(params.FullBytes)\n\t\tnamedCurveOID := new(asn1.ObjectIdentifier)\n\t\tif !paramsDer.ReadASN1ObjectIdentifier(namedCurveOID) {\n\t\t\treturn nil, errors.New(\"x509: invalid ECDSA parameters\")\n\t\t}\n\t\tnamedCurve := namedCurveFromOID(*namedCurveOID)\n\t\tif namedCurve == nil {\n\t\t\treturn nil, errors.New(\"x509: unsupported elliptic curve\")\n\t\t}\n\t\tx, y := elliptic.Unmarshal(namedCurve, der)\n\t\tif x == nil {\n\t\t\treturn nil, errors.New(\"x509: failed to unmarshal elliptic curve point\")\n\t\t}\n\t\tpub := &ecdsa.PublicKey{\n\t\t\tCurve: namedCurve,\n\t\t\tX:     x,\n\t\t\tY:     y,\n\t\t}\n\t\treturn pub, nil\n\tcase oid.Equal(oidPublicKeyEd25519):\n\t\t// RFC 8410, Section 3\n\t\t// > For all of the OIDs, the parameters MUST be absent.\n\t\tif len(params.FullBytes) != 0 {\n\t\t\treturn nil, errors.New(\"x509: Ed25519 key encoded with illegal parameters\")\n\t\t}\n\t\tif len(der) != ed25519.PublicKeySize {\n\t\t\treturn nil, errors.New(\"x509: wrong Ed25519 public key size\")\n\t\t}\n\t\treturn ed25519.PublicKey(der), nil\n\t// case oid.Equal(oidPublicKeyX25519):\n\t// \t// RFC 8410, Section 3\n\t// \t// > For all of the OIDs, the parameters MUST be absent.\n\t// \tif len(params.FullBytes) != 0 {\n\t// \t\treturn nil, errors.New(\"x509: X25519 key encoded with illegal parameters\")\n\t// \t}\n\t// \treturn ecdh.X25519().NewPublicKey(der)\n\tcase oid.Equal(oidPublicKeyDSA):\n\t\ty := new(big.Int)\n\t\tif !der.ReadASN1Integer(y) {\n\t\t\treturn nil, errors.New(\"x509: invalid DSA public key\")\n\t\t}\n\t\tpub := &dsa.PublicKey{\n\t\t\tY: y,\n\t\t\tParameters: dsa.Parameters{\n\t\t\t\tP: new(big.Int),\n\t\t\t\tQ: new(big.Int),\n\t\t\t\tG: new(big.Int),\n\t\t\t},\n\t\t}\n\t\tparamsDer := cryptobyte.String(params.FullBytes)\n\t\tif !paramsDer.ReadASN1(&paramsDer, cryptobyte_asn1.SEQUENCE) ||\n\t\t\t!paramsDer.ReadASN1Integer(pub.Parameters.P) ||\n\t\t\t!paramsDer.ReadASN1Integer(pub.Parameters.Q) ||\n\t\t\t!paramsDer.ReadASN1Integer(pub.Parameters.G) {\n\t\t\treturn nil, errors.New(\"x509: invalid DSA parameters\")\n\t\t}\n\t\tif pub.Y.Sign() <= 0 || pub.Parameters.P.Sign() <= 0 ||\n\t\t\tpub.Parameters.Q.Sign() <= 0 || pub.Parameters.G.Sign() <= 0 {\n\t\t\treturn nil, errors.New(\"x509: zero or negative DSA parameter\")\n\t\t}\n\t\treturn pub, nil\n\tdefault:\n\t\treturn nil, errors.New(\"x509: unknown public key algorithm\")\n\t}\n}\n\nfunc parseKeyUsageExtension(der cryptobyte.String) (stdx509.KeyUsage, error) {\n\tvar usageBits asn1.BitString\n\tif !der.ReadASN1BitString(&usageBits) {\n\t\treturn 0, errors.New(\"x509: invalid key usage\")\n\t}\n\n\tvar usage int\n\tfor i := 0; i < 9; i++ {\n\t\tif usageBits.At(i) != 0 {\n\t\t\tusage |= 1 << uint(i)\n\t\t}\n\t}\n\treturn stdx509.KeyUsage(usage), nil\n}\n\nfunc parseBasicConstraintsExtension(der cryptobyte.String) (bool, int, error) {\n\tvar isCA bool\n\tif !der.ReadASN1(&der, cryptobyte_asn1.SEQUENCE) {\n\t\treturn false, 0, errors.New(\"x509: invalid basic constraints\")\n\t}\n\tif der.PeekASN1Tag(cryptobyte_asn1.BOOLEAN) {\n\t\tif !der.ReadASN1Boolean(&isCA) {\n\t\t\treturn false, 0, errors.New(\"x509: invalid basic constraints\")\n\t\t}\n\t}\n\tmaxPathLen := -1\n\tif der.PeekASN1Tag(cryptobyte_asn1.INTEGER) {\n\t\tif !der.ReadASN1Integer(&maxPathLen) {\n\t\t\treturn false, 0, errors.New(\"x509: invalid basic constraints\")\n\t\t}\n\t}\n\n\t// TODO: map out.MaxPathLen to 0 if it has the -1 default value? (Issue 19285)\n\treturn isCA, maxPathLen, nil\n}\n\nfunc forEachSAN(der cryptobyte.String, callback func(tag int, data []byte) error) error {\n\tif !der.ReadASN1(&der, cryptobyte_asn1.SEQUENCE) {\n\t\treturn errors.New(\"x509: invalid subject alternative names\")\n\t}\n\tfor !der.Empty() {\n\t\tvar san cryptobyte.String\n\t\tvar tag cryptobyte_asn1.Tag\n\t\tif !der.ReadAnyASN1(&san, &tag) {\n\t\t\treturn errors.New(\"x509: invalid subject alternative name\")\n\t\t}\n\t\tif err := callback(int(tag^0x80), san); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc parseSANExtension(der cryptobyte.String) (dnsNames, emailAddresses []string, ipAddresses []net.IP, uris []*url.URL, err error) {\n\terr = forEachSAN(der, func(tag int, data []byte) error {\n\t\tswitch tag {\n\t\tcase nameTypeEmail:\n\t\t\temail := string(data)\n\t\t\tif err := isIA5String(email); err != nil {\n\t\t\t\treturn errors.New(\"x509: SAN rfc822Name is malformed\")\n\t\t\t}\n\t\t\temailAddresses = append(emailAddresses, email)\n\t\tcase nameTypeDNS:\n\t\t\tname := string(data)\n\t\t\tif err := isIA5String(name); err != nil {\n\t\t\t\treturn errors.New(\"x509: SAN dNSName is malformed\")\n\t\t\t}\n\t\t\tdnsNames = append(dnsNames, string(name))\n\t\tcase nameTypeURI:\n\t\t\turiStr := string(data)\n\t\t\tif err := isIA5String(uriStr); err != nil {\n\t\t\t\treturn errors.New(\"x509: SAN uniformResourceIdentifier is malformed\")\n\t\t\t}\n\t\t\turi, err := url.Parse(uriStr)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"x509: cannot parse URI %q: %s\", uriStr, err)\n\t\t\t}\n\t\t\tif len(uri.Host) > 0 {\n\t\t\t\tif _, ok := domainToReverseLabels(uri.Host); !ok {\n\t\t\t\t\treturn fmt.Errorf(\"x509: cannot parse URI %q: invalid domain\", uriStr)\n\t\t\t\t}\n\t\t\t}\n\t\t\turis = append(uris, uri)\n\t\tcase nameTypeIP:\n\t\t\tswitch len(data) {\n\t\t\tcase net.IPv4len, net.IPv6len:\n\t\t\t\tipAddresses = append(ipAddresses, data)\n\t\t\tdefault:\n\t\t\t\treturn errors.New(\"x509: cannot parse IP address of length \" + strconv.Itoa(len(data)))\n\t\t\t}\n\t\t}\n\n\t\treturn nil\n\t})\n\n\treturn\n}\n\nfunc parseAuthorityKeyIdentifier(e pkix.Extension) ([]byte, error) {\n\t// RFC 5280, Section 4.2.1.1\n\t// if e.Critical {\n\t// \t// Conforming CAs MUST mark this extension as non-critical\n\t// \treturn nil, errors.New(\"x509: authority key identifier incorrectly marked critical\")\n\t// }\n\tval := cryptobyte.String(e.Value)\n\tvar akid cryptobyte.String\n\tif !val.ReadASN1(&akid, cryptobyte_asn1.SEQUENCE) {\n\t\treturn nil, errors.New(\"x509: invalid authority key identifier\")\n\t}\n\tif akid.PeekASN1Tag(cryptobyte_asn1.Tag(0).ContextSpecific()) {\n\t\tif !akid.ReadASN1(&akid, cryptobyte_asn1.Tag(0).ContextSpecific()) {\n\t\t\treturn nil, errors.New(\"x509: invalid authority key identifier\")\n\t\t}\n\t\treturn akid, nil\n\t}\n\treturn nil, nil\n}\n\nfunc parseExtKeyUsageExtension(der cryptobyte.String) ([]stdx509.ExtKeyUsage, []asn1.ObjectIdentifier, error) {\n\tvar extKeyUsages []stdx509.ExtKeyUsage\n\tvar unknownUsages []asn1.ObjectIdentifier\n\tif !der.ReadASN1(&der, cryptobyte_asn1.SEQUENCE) {\n\t\treturn nil, nil, errors.New(\"x509: invalid extended key usages\")\n\t}\n\tfor !der.Empty() {\n\t\tvar eku asn1.ObjectIdentifier\n\t\tif !der.ReadASN1ObjectIdentifier(&eku) {\n\t\t\treturn nil, nil, errors.New(\"x509: invalid extended key usages\")\n\t\t}\n\t\tif extKeyUsage, ok := extKeyUsageFromOID(eku); ok {\n\t\t\textKeyUsages = append(extKeyUsages, stdx509.ExtKeyUsage(extKeyUsage))\n\t\t} else {\n\t\t\tunknownUsages = append(unknownUsages, eku)\n\t\t}\n\t}\n\treturn extKeyUsages, unknownUsages, nil\n}\n\n// func parseCertificatePoliciesExtension(der cryptobyte.String) ([]OID, error) {\n// \tvar oids []OID\n// \tif !der.ReadASN1(&der, cryptobyte_asn1.SEQUENCE) {\n// \t\treturn nil, errors.New(\"x509: invalid certificate policies\")\n// \t}\n// \tfor !der.Empty() {\n// \t\tvar cp cryptobyte.String\n// \t\tvar OIDBytes cryptobyte.String\n// \t\tif !der.ReadASN1(&cp, cryptobyte_asn1.SEQUENCE) || !cp.ReadASN1(&OIDBytes, cryptobyte_asn1.OBJECT_IDENTIFIER) {\n// \t\t\treturn nil, errors.New(\"x509: invalid certificate policies\")\n// \t\t}\n// \t\toid, ok := newOIDFromDER(OIDBytes)\n// \t\tif !ok {\n// \t\t\treturn nil, errors.New(\"x509: invalid certificate policies\")\n// \t\t}\n// \t\toids = append(oids, oid)\n// \t}\n// \treturn oids, nil\n// }\n\n// isValidIPMask reports whether mask consists of zero or more 1 bits, followed by zero bits.\nfunc isValidIPMask(mask []byte) bool {\n\tseenZero := false\n\n\tfor _, b := range mask {\n\t\tif seenZero {\n\t\t\tif b != 0 {\n\t\t\t\treturn false\n\t\t\t}\n\n\t\t\tcontinue\n\t\t}\n\n\t\tswitch b {\n\t\tcase 0x00, 0x80, 0xc0, 0xe0, 0xf0, 0xf8, 0xfc, 0xfe:\n\t\t\tseenZero = true\n\t\tcase 0xff:\n\t\tdefault:\n\t\t\treturn false\n\t\t}\n\t}\n\n\treturn true\n}\n\nfunc parseNameConstraintsExtension(out *stdx509.Certificate, e pkix.Extension) (unhandled bool, err error) {\n\t// RFC 5280, 4.2.1.10\n\n\t// NameConstraints ::= SEQUENCE {\n\t//      permittedSubtrees       [0]     GeneralSubtrees OPTIONAL,\n\t//      excludedSubtrees        [1]     GeneralSubtrees OPTIONAL }\n\t//\n\t// GeneralSubtrees ::= SEQUENCE SIZE (1..MAX) OF GeneralSubtree\n\t//\n\t// GeneralSubtree ::= SEQUENCE {\n\t//      base                    GeneralName,\n\t//      minimum         [0]     BaseDistance DEFAULT 0,\n\t//      maximum         [1]     BaseDistance OPTIONAL }\n\t//\n\t// BaseDistance ::= INTEGER (0..MAX)\n\n\touter := cryptobyte.String(e.Value)\n\tvar toplevel, permitted, excluded cryptobyte.String\n\tvar havePermitted, haveExcluded bool\n\tif !outer.ReadASN1(&toplevel, cryptobyte_asn1.SEQUENCE) ||\n\t\t!outer.Empty() ||\n\t\t!toplevel.ReadOptionalASN1(&permitted, &havePermitted, cryptobyte_asn1.Tag(0).ContextSpecific().Constructed()) ||\n\t\t!toplevel.ReadOptionalASN1(&excluded, &haveExcluded, cryptobyte_asn1.Tag(1).ContextSpecific().Constructed()) ||\n\t\t!toplevel.Empty() {\n\t\treturn false, errors.New(\"x509: invalid NameConstraints extension\")\n\t}\n\n\tif !havePermitted && !haveExcluded || len(permitted) == 0 && len(excluded) == 0 {\n\t\t// From RFC 5280, Section 4.2.1.10:\n\t\t//   “either the permittedSubtrees field\n\t\t//   or the excludedSubtrees MUST be\n\t\t//   present”\n\t\treturn false, errors.New(\"x509: empty name constraints extension\")\n\t}\n\n\tgetValues := func(subtrees cryptobyte.String) (dnsNames []string, ips []*net.IPNet, emails, uriDomains []string, err error) {\n\t\tfor !subtrees.Empty() {\n\t\t\tvar seq, value cryptobyte.String\n\t\t\tvar tag cryptobyte_asn1.Tag\n\t\t\tif !subtrees.ReadASN1(&seq, cryptobyte_asn1.SEQUENCE) ||\n\t\t\t\t!seq.ReadAnyASN1(&value, &tag) {\n\t\t\t\treturn nil, nil, nil, nil, fmt.Errorf(\"x509: invalid NameConstraints extension\")\n\t\t\t}\n\n\t\t\tvar (\n\t\t\t\tdnsTag   = cryptobyte_asn1.Tag(2).ContextSpecific()\n\t\t\t\temailTag = cryptobyte_asn1.Tag(1).ContextSpecific()\n\t\t\t\tipTag    = cryptobyte_asn1.Tag(7).ContextSpecific()\n\t\t\t\turiTag   = cryptobyte_asn1.Tag(6).ContextSpecific()\n\t\t\t)\n\n\t\t\tswitch tag {\n\t\t\tcase dnsTag:\n\t\t\t\tdomain := string(value)\n\t\t\t\tif err := isIA5String(domain); err != nil {\n\t\t\t\t\treturn nil, nil, nil, nil, errors.New(\"x509: invalid constraint value: \" + err.Error())\n\t\t\t\t}\n\n\t\t\t\ttrimmedDomain := domain\n\t\t\t\tif len(trimmedDomain) > 0 && trimmedDomain[0] == '.' {\n\t\t\t\t\t// constraints can have a leading\n\t\t\t\t\t// period to exclude the domain\n\t\t\t\t\t// itself, but that's not valid in a\n\t\t\t\t\t// normal domain name.\n\t\t\t\t\ttrimmedDomain = trimmedDomain[1:]\n\t\t\t\t}\n\t\t\t\tif _, ok := domainToReverseLabels(trimmedDomain); !ok {\n\t\t\t\t\treturn nil, nil, nil, nil, fmt.Errorf(\"x509: failed to parse dnsName constraint %q\", domain)\n\t\t\t\t}\n\t\t\t\tdnsNames = append(dnsNames, domain)\n\n\t\t\tcase ipTag:\n\t\t\t\tl := len(value)\n\t\t\t\tvar ip, mask []byte\n\n\t\t\t\tswitch l {\n\t\t\t\tcase 8:\n\t\t\t\t\tip = value[:4]\n\t\t\t\t\tmask = value[4:]\n\n\t\t\t\tcase 32:\n\t\t\t\t\tip = value[:16]\n\t\t\t\t\tmask = value[16:]\n\n\t\t\t\tdefault:\n\t\t\t\t\treturn nil, nil, nil, nil, fmt.Errorf(\"x509: IP constraint contained value of length %d\", l)\n\t\t\t\t}\n\n\t\t\t\tif !isValidIPMask(mask) {\n\t\t\t\t\treturn nil, nil, nil, nil, fmt.Errorf(\"x509: IP constraint contained invalid mask %x\", mask)\n\t\t\t\t}\n\n\t\t\t\tips = append(ips, &net.IPNet{IP: net.IP(ip), Mask: net.IPMask(mask)})\n\n\t\t\tcase emailTag:\n\t\t\t\tconstraint := string(value)\n\t\t\t\tif err := isIA5String(constraint); err != nil {\n\t\t\t\t\treturn nil, nil, nil, nil, errors.New(\"x509: invalid constraint value: \" + err.Error())\n\t\t\t\t}\n\n\t\t\t\t// If the constraint contains an @ then\n\t\t\t\t// it specifies an exact mailbox name.\n\t\t\t\tif strings.Contains(constraint, \"@\") {\n\t\t\t\t\tif _, ok := parseRFC2821Mailbox(constraint); !ok {\n\t\t\t\t\t\treturn nil, nil, nil, nil, fmt.Errorf(\"x509: failed to parse rfc822Name constraint %q\", constraint)\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// Otherwise it's a domain name.\n\t\t\t\t\tdomain := constraint\n\t\t\t\t\tif len(domain) > 0 && domain[0] == '.' {\n\t\t\t\t\t\tdomain = domain[1:]\n\t\t\t\t\t}\n\t\t\t\t\tif _, ok := domainToReverseLabels(domain); !ok {\n\t\t\t\t\t\treturn nil, nil, nil, nil, fmt.Errorf(\"x509: failed to parse rfc822Name constraint %q\", constraint)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\temails = append(emails, constraint)\n\n\t\t\tcase uriTag:\n\t\t\t\tdomain := string(value)\n\t\t\t\tif err := isIA5String(domain); err != nil {\n\t\t\t\t\treturn nil, nil, nil, nil, errors.New(\"x509: invalid constraint value: \" + err.Error())\n\t\t\t\t}\n\n\t\t\t\tif net.ParseIP(domain) != nil {\n\t\t\t\t\treturn nil, nil, nil, nil, fmt.Errorf(\"x509: failed to parse URI constraint %q: cannot be IP address\", domain)\n\t\t\t\t}\n\n\t\t\t\ttrimmedDomain := domain\n\t\t\t\tif len(trimmedDomain) > 0 && trimmedDomain[0] == '.' {\n\t\t\t\t\t// constraints can have a leading\n\t\t\t\t\t// period to exclude the domain itself,\n\t\t\t\t\t// but that's not valid in a normal\n\t\t\t\t\t// domain name.\n\t\t\t\t\ttrimmedDomain = trimmedDomain[1:]\n\t\t\t\t}\n\t\t\t\tif _, ok := domainToReverseLabels(trimmedDomain); !ok {\n\t\t\t\t\treturn nil, nil, nil, nil, fmt.Errorf(\"x509: failed to parse URI constraint %q\", domain)\n\t\t\t\t}\n\t\t\t\turiDomains = append(uriDomains, domain)\n\n\t\t\tdefault:\n\t\t\t\tunhandled = true\n\t\t\t}\n\t\t}\n\n\t\treturn dnsNames, ips, emails, uriDomains, nil\n\t}\n\n\tif out.PermittedDNSDomains, out.PermittedIPRanges, out.PermittedEmailAddresses, out.PermittedURIDomains, err = getValues(permitted); err != nil {\n\t\treturn false, err\n\t}\n\tif out.ExcludedDNSDomains, out.ExcludedIPRanges, out.ExcludedEmailAddresses, out.ExcludedURIDomains, err = getValues(excluded); err != nil {\n\t\treturn false, err\n\t}\n\tout.PermittedDNSDomainsCritical = e.Critical\n\n\treturn unhandled, nil\n}\n\nfunc processExtensions(out *stdx509.Certificate) error {\n\tvar err error\n\tfor _, e := range out.Extensions {\n\t\tunhandled := false\n\n\t\tif len(e.Id) == 4 && e.Id[0] == 2 && e.Id[1] == 5 && e.Id[2] == 29 {\n\t\t\tswitch e.Id[3] {\n\t\t\tcase 15:\n\t\t\t\tout.KeyUsage, err = parseKeyUsageExtension(e.Value)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\tcase 19:\n\t\t\t\tout.IsCA, out.MaxPathLen, err = parseBasicConstraintsExtension(e.Value)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tout.BasicConstraintsValid = true\n\t\t\t\tout.MaxPathLenZero = out.MaxPathLen == 0\n\t\t\tcase 17:\n\t\t\t\tout.DNSNames, out.EmailAddresses, out.IPAddresses, out.URIs, err = parseSANExtension(e.Value)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tif len(out.DNSNames) == 0 && len(out.EmailAddresses) == 0 && len(out.IPAddresses) == 0 && len(out.URIs) == 0 {\n\t\t\t\t\t// If we didn't parse anything then we do the critical check, below.\n\t\t\t\t\tunhandled = true\n\t\t\t\t}\n\n\t\t\tcase 30:\n\t\t\t\tunhandled, err = parseNameConstraintsExtension(out, e)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\tcase 31:\n\t\t\t\t// RFC 5280, 4.2.1.13\n\n\t\t\t\t// CRLDistributionPoints ::= SEQUENCE SIZE (1..MAX) OF DistributionPoint\n\t\t\t\t//\n\t\t\t\t// DistributionPoint ::= SEQUENCE {\n\t\t\t\t//     distributionPoint       [0]     DistributionPointName OPTIONAL,\n\t\t\t\t//     reasons                 [1]     ReasonFlags OPTIONAL,\n\t\t\t\t//     cRLIssuer               [2]     GeneralNames OPTIONAL }\n\t\t\t\t//\n\t\t\t\t// DistributionPointName ::= CHOICE {\n\t\t\t\t//     fullName                [0]     GeneralNames,\n\t\t\t\t//     nameRelativeToCRLIssuer [1]     RelativeDistinguishedName }\n\t\t\t\tval := cryptobyte.String(e.Value)\n\t\t\t\tif !val.ReadASN1(&val, cryptobyte_asn1.SEQUENCE) {\n\t\t\t\t\treturn errors.New(\"x509: invalid CRL distribution points\")\n\t\t\t\t}\n\t\t\t\tfor !val.Empty() {\n\t\t\t\t\tvar dpDER cryptobyte.String\n\t\t\t\t\tif !val.ReadASN1(&dpDER, cryptobyte_asn1.SEQUENCE) {\n\t\t\t\t\t\treturn errors.New(\"x509: invalid CRL distribution point\")\n\t\t\t\t\t}\n\t\t\t\t\tvar dpNameDER cryptobyte.String\n\t\t\t\t\tvar dpNamePresent bool\n\t\t\t\t\tif !dpDER.ReadOptionalASN1(&dpNameDER, &dpNamePresent, cryptobyte_asn1.Tag(0).Constructed().ContextSpecific()) {\n\t\t\t\t\t\treturn errors.New(\"x509: invalid CRL distribution point\")\n\t\t\t\t\t}\n\t\t\t\t\tif !dpNamePresent {\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t\tif !dpNameDER.ReadASN1(&dpNameDER, cryptobyte_asn1.Tag(0).Constructed().ContextSpecific()) {\n\t\t\t\t\t\treturn errors.New(\"x509: invalid CRL distribution point\")\n\t\t\t\t\t}\n\t\t\t\t\tfor !dpNameDER.Empty() {\n\t\t\t\t\t\tif !dpNameDER.PeekASN1Tag(cryptobyte_asn1.Tag(6).ContextSpecific()) {\n\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t}\n\t\t\t\t\t\tvar uri cryptobyte.String\n\t\t\t\t\t\tif !dpNameDER.ReadASN1(&uri, cryptobyte_asn1.Tag(6).ContextSpecific()) {\n\t\t\t\t\t\t\treturn errors.New(\"x509: invalid CRL distribution point\")\n\t\t\t\t\t\t}\n\t\t\t\t\t\tout.CRLDistributionPoints = append(out.CRLDistributionPoints, string(uri))\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\tcase 35:\n\t\t\t\tout.AuthorityKeyId, err = parseAuthorityKeyIdentifier(e)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\tcase 37:\n\t\t\t\tout.ExtKeyUsage, out.UnknownExtKeyUsage, err = parseExtKeyUsageExtension(e.Value)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\tcase 14:\n\t\t\t\t// RFC 5280, 4.2.1.2\n\t\t\t\tif e.Critical {\n\t\t\t\t\t// Conforming CAs MUST mark this extension as non-critical\n\t\t\t\t\treturn errors.New(\"x509: subject key identifier incorrectly marked critical\")\n\t\t\t\t}\n\t\t\t\tval := cryptobyte.String(e.Value)\n\t\t\t\tvar skid cryptobyte.String\n\t\t\t\tif !val.ReadASN1(&skid, cryptobyte_asn1.OCTET_STRING) {\n\t\t\t\t\treturn errors.New(\"x509: invalid subject key identifier\")\n\t\t\t\t}\n\t\t\t\tout.SubjectKeyId = skid\n\t\t\t// case 32:\n\t\t\t// \tout.Policies, err = parseCertificatePoliciesExtension(e.Value)\n\t\t\t// \tif err != nil {\n\t\t\t// \t\treturn err\n\t\t\t// \t}\n\t\t\t// \tout.PolicyIdentifiers = make([]asn1.ObjectIdentifier, 0, len(out.Policies))\n\t\t\t// \tfor _, oid := range out.Policies {\n\t\t\t// \t\tif oid, ok := oid.toASN1OID(); ok {\n\t\t\t// \t\t\tout.PolicyIdentifiers = append(out.PolicyIdentifiers, oid)\n\t\t\t// \t\t}\n\t\t\t// \t}\n\t\t\tdefault:\n\t\t\t\t// Unknown extensions are recorded if critical.\n\t\t\t\tunhandled = true\n\t\t\t}\n\t\t} else if e.Id.Equal(oidExtensionAuthorityInfoAccess) {\n\t\t\t// RFC 5280 4.2.2.1: Authority Information Access\n\t\t\tif e.Critical {\n\t\t\t\t// Conforming CAs MUST mark this extension as non-critical\n\t\t\t\treturn errors.New(\"x509: authority info access incorrectly marked critical\")\n\t\t\t}\n\t\t\tval := cryptobyte.String(e.Value)\n\t\t\tif !val.ReadASN1(&val, cryptobyte_asn1.SEQUENCE) {\n\t\t\t\treturn errors.New(\"x509: invalid authority info access\")\n\t\t\t}\n\t\t\tfor !val.Empty() {\n\t\t\t\tvar aiaDER cryptobyte.String\n\t\t\t\tif !val.ReadASN1(&aiaDER, cryptobyte_asn1.SEQUENCE) {\n\t\t\t\t\treturn errors.New(\"x509: invalid authority info access\")\n\t\t\t\t}\n\t\t\t\tvar method asn1.ObjectIdentifier\n\t\t\t\tif !aiaDER.ReadASN1ObjectIdentifier(&method) {\n\t\t\t\t\treturn errors.New(\"x509: invalid authority info access\")\n\t\t\t\t}\n\t\t\t\tif !aiaDER.PeekASN1Tag(cryptobyte_asn1.Tag(6).ContextSpecific()) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tif !aiaDER.ReadASN1(&aiaDER, cryptobyte_asn1.Tag(6).ContextSpecific()) {\n\t\t\t\t\treturn errors.New(\"x509: invalid authority info access\")\n\t\t\t\t}\n\t\t\t\tswitch {\n\t\t\t\tcase method.Equal(oidAuthorityInfoAccessOcsp):\n\t\t\t\t\tout.OCSPServer = append(out.OCSPServer, string(aiaDER))\n\t\t\t\tcase method.Equal(oidAuthorityInfoAccessIssuers):\n\t\t\t\t\tout.IssuingCertificateURL = append(out.IssuingCertificateURL, string(aiaDER))\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\t// Unknown extensions are recorded if critical.\n\t\t\tunhandled = true\n\t\t}\n\n\t\tif e.Critical && unhandled {\n\t\t\tout.UnhandledCriticalExtensions = append(out.UnhandledCriticalExtensions, e.Id)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nvar x509negativeserial = legacyGodebugSetting(0) // replaces godebug.New(\"x509negativeserial\")\n\nfunc parseCertificate(der []byte) (*stdx509.Certificate, error) {\n\tcert := &stdx509.Certificate{}\n\n\tinput := cryptobyte.String(der)\n\t// we read the SEQUENCE including length and tag bytes so that\n\t// we can populate Certificate.Raw, before unwrapping the\n\t// SEQUENCE so it can be operated on\n\tif !input.ReadASN1Element(&input, cryptobyte_asn1.SEQUENCE) {\n\t\treturn nil, errors.New(\"x509: malformed certificate\")\n\t}\n\tcert.Raw = input\n\tif !input.ReadASN1(&input, cryptobyte_asn1.SEQUENCE) {\n\t\treturn nil, errors.New(\"x509: malformed certificate\")\n\t}\n\n\tvar tbs cryptobyte.String\n\t// do the same trick again as above to extract the raw\n\t// bytes for Certificate.RawTBSCertificate\n\tif !input.ReadASN1Element(&tbs, cryptobyte_asn1.SEQUENCE) {\n\t\treturn nil, errors.New(\"x509: malformed tbs certificate\")\n\t}\n\tcert.RawTBSCertificate = tbs\n\tif !tbs.ReadASN1(&tbs, cryptobyte_asn1.SEQUENCE) {\n\t\treturn nil, errors.New(\"x509: malformed tbs certificate\")\n\t}\n\n\tif !tbs.ReadOptionalASN1Integer(&cert.Version, cryptobyte_asn1.Tag(0).Constructed().ContextSpecific(), 0) {\n\t\treturn nil, errors.New(\"x509: malformed version\")\n\t}\n\tif cert.Version < 0 {\n\t\treturn nil, errors.New(\"x509: malformed version\")\n\t}\n\t// for backwards compat reasons Version is one-indexed,\n\t// rather than zero-indexed as defined in 5280\n\tcert.Version++\n\tif cert.Version > 3 {\n\t\treturn nil, errors.New(\"x509: invalid version\")\n\t}\n\n\tserial := new(big.Int)\n\tif !tbs.ReadASN1Integer(serial) {\n\t\treturn nil, errors.New(\"x509: malformed serial number\")\n\t}\n\tif serial.Sign() == -1 {\n\t\tif x509negativeserial.Value() != \"1\" {\n\t\t\treturn nil, errors.New(\"x509: negative serial number\")\n\t\t} else {\n\t\t\tx509negativeserial.IncNonDefault()\n\t\t}\n\t}\n\tcert.SerialNumber = serial\n\n\tvar sigAISeq cryptobyte.String\n\tif !tbs.ReadASN1(&sigAISeq, cryptobyte_asn1.SEQUENCE) {\n\t\treturn nil, errors.New(\"x509: malformed signature algorithm identifier\")\n\t}\n\t// Before parsing the inner algorithm identifier, extract\n\t// the outer algorithm identifier and make sure that they\n\t// match.\n\tvar outerSigAISeq cryptobyte.String\n\tif !input.ReadASN1(&outerSigAISeq, cryptobyte_asn1.SEQUENCE) {\n\t\treturn nil, errors.New(\"x509: malformed algorithm identifier\")\n\t}\n\tif !bytes.Equal(outerSigAISeq, sigAISeq) {\n\t\treturn nil, errors.New(\"x509: inner and outer signature algorithm identifiers don't match\")\n\t}\n\tsigAI, err := parseAI(sigAISeq)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tcert.SignatureAlgorithm = getSignatureAlgorithmFromAI(sigAI)\n\n\tvar issuerSeq cryptobyte.String\n\tif !tbs.ReadASN1Element(&issuerSeq, cryptobyte_asn1.SEQUENCE) {\n\t\treturn nil, errors.New(\"x509: malformed issuer\")\n\t}\n\tcert.RawIssuer = issuerSeq\n\tissuerRDNs, err := parseName(issuerSeq)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tcert.Issuer.FillFromRDNSequence(issuerRDNs)\n\n\tvar validity cryptobyte.String\n\tif !tbs.ReadASN1(&validity, cryptobyte_asn1.SEQUENCE) {\n\t\treturn nil, errors.New(\"x509: malformed validity\")\n\t}\n\tcert.NotBefore, cert.NotAfter, err = parseValidity(validity)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar subjectSeq cryptobyte.String\n\tif !tbs.ReadASN1Element(&subjectSeq, cryptobyte_asn1.SEQUENCE) {\n\t\treturn nil, errors.New(\"x509: malformed issuer\")\n\t}\n\tcert.RawSubject = subjectSeq\n\tsubjectRDNs, err := parseName(subjectSeq)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tcert.Subject.FillFromRDNSequence(subjectRDNs)\n\n\tvar spki cryptobyte.String\n\tif !tbs.ReadASN1Element(&spki, cryptobyte_asn1.SEQUENCE) {\n\t\treturn nil, errors.New(\"x509: malformed spki\")\n\t}\n\tcert.RawSubjectPublicKeyInfo = spki\n\tif !spki.ReadASN1(&spki, cryptobyte_asn1.SEQUENCE) {\n\t\treturn nil, errors.New(\"x509: malformed spki\")\n\t}\n\tvar pkAISeq cryptobyte.String\n\tif !spki.ReadASN1(&pkAISeq, cryptobyte_asn1.SEQUENCE) {\n\t\treturn nil, errors.New(\"x509: malformed public key algorithm identifier\")\n\t}\n\tpkAI, err := parseAI(pkAISeq)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tcert.PublicKeyAlgorithm = getPublicKeyAlgorithmFromOID(pkAI.Algorithm)\n\tvar spk asn1.BitString\n\tif !spki.ReadASN1BitString(&spk) {\n\t\treturn nil, errors.New(\"x509: malformed subjectPublicKey\")\n\t}\n\tif cert.PublicKeyAlgorithm != stdx509.UnknownPublicKeyAlgorithm {\n\t\tcert.PublicKey, err = parsePublicKey(&publicKeyInfo{\n\t\t\tAlgorithm: pkAI,\n\t\t\tPublicKey: spk,\n\t\t})\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\tif cert.Version > 1 {\n\t\tif !tbs.SkipOptionalASN1(cryptobyte_asn1.Tag(1).ContextSpecific()) {\n\t\t\treturn nil, errors.New(\"x509: malformed issuerUniqueID\")\n\t\t}\n\t\tif !tbs.SkipOptionalASN1(cryptobyte_asn1.Tag(2).ContextSpecific()) {\n\t\t\treturn nil, errors.New(\"x509: malformed subjectUniqueID\")\n\t\t}\n\t\tif cert.Version == 3 {\n\t\t\tvar extensions cryptobyte.String\n\t\t\tvar present bool\n\t\t\tif !tbs.ReadOptionalASN1(&extensions, &present, cryptobyte_asn1.Tag(3).Constructed().ContextSpecific()) {\n\t\t\t\treturn nil, errors.New(\"x509: malformed extensions\")\n\t\t\t}\n\t\t\tif present {\n\t\t\t\tseenExts := make(map[string]bool)\n\t\t\t\tif !extensions.ReadASN1(&extensions, cryptobyte_asn1.SEQUENCE) {\n\t\t\t\t\treturn nil, errors.New(\"x509: malformed extensions\")\n\t\t\t\t}\n\t\t\t\tfor !extensions.Empty() {\n\t\t\t\t\tvar extension cryptobyte.String\n\t\t\t\t\tif !extensions.ReadASN1(&extension, cryptobyte_asn1.SEQUENCE) {\n\t\t\t\t\t\treturn nil, errors.New(\"x509: malformed extension\")\n\t\t\t\t\t}\n\t\t\t\t\text, err := parseExtension(extension)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn nil, err\n\t\t\t\t\t}\n\t\t\t\t\toidStr := ext.Id.String()\n\t\t\t\t\tif seenExts[oidStr] {\n\t\t\t\t\t\treturn nil, fmt.Errorf(\"x509: certificate contains duplicate extension with OID %q\", oidStr)\n\t\t\t\t\t}\n\t\t\t\t\tseenExts[oidStr] = true\n\t\t\t\t\tcert.Extensions = append(cert.Extensions, ext)\n\t\t\t\t}\n\t\t\t\terr = processExtensions(cert)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tvar signature asn1.BitString\n\tif !input.ReadASN1BitString(&signature) {\n\t\treturn nil, errors.New(\"x509: malformed signature\")\n\t}\n\tcert.Signature = signature.RightAlign()\n\n\treturn cert, nil\n}\n"
  },
  {
    "path": "test/integration/scep/internal/x509/pkcs1.go",
    "content": "// Copyright 2011 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage legacyx509\n\nimport (\n\t\"math/big\"\n)\n\n// pkcs1PublicKey reflects the ASN.1 structure of a PKCS #1 public key.\ntype pkcs1PublicKey struct {\n\tN *big.Int\n\tE int\n}\n"
  },
  {
    "path": "test/integration/scep/internal/x509/verify.go",
    "content": "// Copyright 2011 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\n//nolint:gocritic // code copied from crypto/x509\npackage legacyx509\n\nimport (\n\t\"bytes\"\n\t\"strings\"\n)\n\n// rfc2821Mailbox represents a “mailbox” (which is an email address to most\n// people) by breaking it into the “local” (i.e. before the '@') and “domain”\n// parts.\ntype rfc2821Mailbox struct {\n\tlocal, domain string\n}\n\n// parseRFC2821Mailbox parses an email address into local and domain parts,\n// based on the ABNF for a “Mailbox” from RFC 2821. According to RFC 5280,\n// Section 4.2.1.6 that's correct for an rfc822Name from a certificate: “The\n// format of an rfc822Name is a \"Mailbox\" as defined in RFC 2821, Section 4.1.2”.\nfunc parseRFC2821Mailbox(in string) (mailbox rfc2821Mailbox, ok bool) {\n\tif len(in) == 0 {\n\t\treturn mailbox, false\n\t}\n\n\tlocalPartBytes := make([]byte, 0, len(in)/2)\n\n\tif in[0] == '\"' {\n\t\t// Quoted-string = DQUOTE *qcontent DQUOTE\n\t\t// non-whitespace-control = %d1-8 / %d11 / %d12 / %d14-31 / %d127\n\t\t// qcontent = qtext / quoted-pair\n\t\t// qtext = non-whitespace-control /\n\t\t//         %d33 / %d35-91 / %d93-126\n\t\t// quoted-pair = (\"\\\" text) / obs-qp\n\t\t// text = %d1-9 / %d11 / %d12 / %d14-127 / obs-text\n\t\t//\n\t\t// (Names beginning with “obs-” are the obsolete syntax from RFC 2822,\n\t\t// Section 4. Since it has been 16 years, we no longer accept that.)\n\t\tin = in[1:]\n\tQuotedString:\n\t\tfor {\n\t\t\tif len(in) == 0 {\n\t\t\t\treturn mailbox, false\n\t\t\t}\n\t\t\tc := in[0]\n\t\t\tin = in[1:]\n\n\t\t\tswitch {\n\t\t\tcase c == '\"':\n\t\t\t\tbreak QuotedString\n\n\t\t\tcase c == '\\\\':\n\t\t\t\t// quoted-pair\n\t\t\t\tif len(in) == 0 {\n\t\t\t\t\treturn mailbox, false\n\t\t\t\t}\n\t\t\t\tif in[0] == 11 ||\n\t\t\t\t\tin[0] == 12 ||\n\t\t\t\t\t(1 <= in[0] && in[0] <= 9) ||\n\t\t\t\t\t(14 <= in[0] && in[0] <= 127) {\n\t\t\t\t\tlocalPartBytes = append(localPartBytes, in[0])\n\t\t\t\t\tin = in[1:]\n\t\t\t\t} else {\n\t\t\t\t\treturn mailbox, false\n\t\t\t\t}\n\n\t\t\tcase c == 11 ||\n\t\t\t\tc == 12 ||\n\t\t\t\t// Space (char 32) is not allowed based on the\n\t\t\t\t// BNF, but RFC 3696 gives an example that\n\t\t\t\t// assumes that it is. Several “verified”\n\t\t\t\t// errata continue to argue about this point.\n\t\t\t\t// We choose to accept it.\n\t\t\t\tc == 32 ||\n\t\t\t\tc == 33 ||\n\t\t\t\tc == 127 ||\n\t\t\t\t(1 <= c && c <= 8) ||\n\t\t\t\t(14 <= c && c <= 31) ||\n\t\t\t\t(35 <= c && c <= 91) ||\n\t\t\t\t(93 <= c && c <= 126):\n\t\t\t\t// qtext\n\t\t\t\tlocalPartBytes = append(localPartBytes, c)\n\n\t\t\tdefault:\n\t\t\t\treturn mailbox, false\n\t\t\t}\n\t\t}\n\t} else {\n\t\t// Atom (\".\" Atom)*\n\tNextChar:\n\t\tfor len(in) > 0 {\n\t\t\t// atext from RFC 2822, Section 3.2.4\n\t\t\tc := in[0]\n\n\t\t\tswitch {\n\t\t\tcase c == '\\\\':\n\t\t\t\t// Examples given in RFC 3696 suggest that\n\t\t\t\t// escaped characters can appear outside of a\n\t\t\t\t// quoted string. Several “verified” errata\n\t\t\t\t// continue to argue the point. We choose to\n\t\t\t\t// accept it.\n\t\t\t\tin = in[1:]\n\t\t\t\tif len(in) == 0 {\n\t\t\t\t\treturn mailbox, false\n\t\t\t\t}\n\t\t\t\tfallthrough\n\n\t\t\tcase ('0' <= c && c <= '9') ||\n\t\t\t\t('a' <= c && c <= 'z') ||\n\t\t\t\t('A' <= c && c <= 'Z') ||\n\t\t\t\tc == '!' || c == '#' || c == '$' || c == '%' ||\n\t\t\t\tc == '&' || c == '\\'' || c == '*' || c == '+' ||\n\t\t\t\tc == '-' || c == '/' || c == '=' || c == '?' ||\n\t\t\t\tc == '^' || c == '_' || c == '`' || c == '{' ||\n\t\t\t\tc == '|' || c == '}' || c == '~' || c == '.':\n\t\t\t\tlocalPartBytes = append(localPartBytes, in[0])\n\t\t\t\tin = in[1:]\n\n\t\t\tdefault:\n\t\t\t\tbreak NextChar\n\t\t\t}\n\t\t}\n\n\t\tif len(localPartBytes) == 0 {\n\t\t\treturn mailbox, false\n\t\t}\n\n\t\t// From RFC 3696, Section 3:\n\t\t// “period (\".\") may also appear, but may not be used to start\n\t\t// or end the local part, nor may two or more consecutive\n\t\t// periods appear.”\n\t\ttwoDots := []byte{'.', '.'}\n\t\tif localPartBytes[0] == '.' ||\n\t\t\tlocalPartBytes[len(localPartBytes)-1] == '.' ||\n\t\t\tbytes.Contains(localPartBytes, twoDots) {\n\t\t\treturn mailbox, false\n\t\t}\n\t}\n\n\tif len(in) == 0 || in[0] != '@' {\n\t\treturn mailbox, false\n\t}\n\tin = in[1:]\n\n\t// The RFC species a format for domains, but that's known to be\n\t// violated in practice so we accept that anything after an '@' is the\n\t// domain part.\n\tif _, ok := domainToReverseLabels(in); !ok {\n\t\treturn mailbox, false\n\t}\n\n\tmailbox.local = string(localPartBytes)\n\tmailbox.domain = in\n\treturn mailbox, true\n}\n\n// domainToReverseLabels converts a textual domain name like foo.example.com to\n// the list of labels in reverse order, e.g. [\"com\", \"example\", \"foo\"].\nfunc domainToReverseLabels(domain string) (reverseLabels []string, ok bool) {\n\tfor len(domain) > 0 {\n\t\tif i := strings.LastIndexByte(domain, '.'); i == -1 {\n\t\t\treverseLabels = append(reverseLabels, domain)\n\t\t\tdomain = \"\"\n\t\t} else {\n\t\t\treverseLabels = append(reverseLabels, domain[i+1:])\n\t\t\tdomain = domain[:i]\n\t\t\tif i == 0 { // domain == \"\"\n\t\t\t\t// domain is prefixed with an empty label, append an empty\n\t\t\t\t// string to reverseLabels to indicate this.\n\t\t\t\treverseLabels = append(reverseLabels, \"\")\n\t\t\t}\n\t\t}\n\t}\n\n\tif len(reverseLabels) > 0 && len(reverseLabels[0]) == 0 {\n\t\t// An empty label at the end indicates an absolute value.\n\t\treturn nil, false\n\t}\n\n\tfor _, label := range reverseLabels {\n\t\tif len(label) == 0 {\n\t\t\t// Empty labels are otherwise invalid.\n\t\t\treturn nil, false\n\t\t}\n\n\t\tfor _, c := range label {\n\t\t\tif c < 33 || c > 126 {\n\t\t\t\t// Invalid character.\n\t\t\t\treturn nil, false\n\t\t\t}\n\t\t}\n\t}\n\n\treturn reverseLabels, true\n}\n"
  },
  {
    "path": "test/integration/scep/internal/x509/x509.go",
    "content": "// Copyright 2009 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\n// Package x509 implements a subset of the X.509 standard.\n//\n// It allows parsing and generating certificates, certificate signing\n// requests, certificate revocation lists, and encoded public and private keys.\n// It provides a certificate verifier, complete with a chain builder.\n//\n// The package targets the X.509 technical profile defined by the IETF (RFC\n// 2459/3280/5280), and as further restricted by the CA/Browser Forum Baseline\n// Requirements. There is minimal support for features outside of these\n// profiles, as the primary goal of the package is to provide compatibility\n// with the publicly trusted TLS certificate ecosystem and its policies and\n// constraints.\n//\n// On macOS and Windows, certificate verification is handled by system APIs, but\n// the package aims to apply consistent validation rules across operating\n// systems.\n\n//nolint:gosec,unused // code copied from crypto/x509\npackage legacyx509\n\nimport (\n\t\"bytes\"\n\t\"crypto\"\n\t\"crypto/elliptic\"\n\tstdx509 \"crypto/x509\"\n\t\"crypto/x509/pkix\"\n\t\"encoding/asn1\"\n\t\"fmt\"\n\t\"strconv\"\n\t\"unicode\"\n\n\t// Explicitly import these for their crypto.RegisterHash init side-effects.\n\t// Keep these as blank imports, even if they're imported above.\n\t_ \"crypto/sha1\"\n\t_ \"crypto/sha256\"\n\t_ \"crypto/sha512\"\n)\n\ntype publicKeyInfo struct {\n\tRaw       asn1.RawContent\n\tAlgorithm pkix.AlgorithmIdentifier\n\tPublicKey asn1.BitString\n}\n\ntype SignatureAlgorithm int\n\nconst (\n\tUnknownSignatureAlgorithm SignatureAlgorithm = iota\n\n\tMD2WithRSA  // Unsupported.\n\tMD5WithRSA  // Only supported for signing, not verification.\n\tSHA1WithRSA // Only supported for signing, and verification of CRLs, CSRs, and OCSP responses.\n\tSHA256WithRSA\n\tSHA384WithRSA\n\tSHA512WithRSA\n\tDSAWithSHA1   // Unsupported.\n\tDSAWithSHA256 // Unsupported.\n\tECDSAWithSHA1 // Only supported for signing, and verification of CRLs, CSRs, and OCSP responses.\n\tECDSAWithSHA256\n\tECDSAWithSHA384\n\tECDSAWithSHA512\n\tSHA256WithRSAPSS\n\tSHA384WithRSAPSS\n\tSHA512WithRSAPSS\n\tPureEd25519\n)\n\nfunc (algo SignatureAlgorithm) String() string {\n\tfor _, details := range signatureAlgorithmDetails {\n\t\tif details.algo == algo {\n\t\t\treturn details.name\n\t\t}\n\t}\n\treturn strconv.Itoa(int(algo))\n}\n\ntype PublicKeyAlgorithm int\n\nconst (\n\tUnknownPublicKeyAlgorithm PublicKeyAlgorithm = iota\n\tRSA\n\tDSA // Only supported for parsing.\n\tECDSA\n\tEd25519\n)\n\nvar publicKeyAlgoName = [...]string{\n\tRSA:     \"RSA\",\n\tDSA:     \"DSA\",\n\tECDSA:   \"ECDSA\",\n\tEd25519: \"Ed25519\",\n}\n\nfunc (algo PublicKeyAlgorithm) String() string {\n\tif 0 < algo && int(algo) < len(publicKeyAlgoName) {\n\t\treturn publicKeyAlgoName[algo]\n\t}\n\treturn strconv.Itoa(int(algo))\n}\n\n// OIDs for signature algorithms\n//\n//\tpkcs-1 OBJECT IDENTIFIER ::= {\n//\t\tiso(1) member-body(2) us(840) rsadsi(113549) pkcs(1) 1 }\n//\n// RFC 3279 2.2.1 RSA Signature Algorithms\n//\n//\tmd5WithRSAEncryption OBJECT IDENTIFIER ::= { pkcs-1 4 }\n//\n//\tsha-1WithRSAEncryption OBJECT IDENTIFIER ::= { pkcs-1 5 }\n//\n//\tdsaWithSha1 OBJECT IDENTIFIER ::= {\n//\t\tiso(1) member-body(2) us(840) x9-57(10040) x9cm(4) 3 }\n//\n// RFC 3279 2.2.3 ECDSA Signature Algorithm\n//\n//\tecdsa-with-SHA1 OBJECT IDENTIFIER ::= {\n//\t\tiso(1) member-body(2) us(840) ansi-x962(10045)\n//\t\tsignatures(4) ecdsa-with-SHA1(1)}\n//\n// RFC 4055 5 PKCS #1 Version 1.5\n//\n//\tsha256WithRSAEncryption OBJECT IDENTIFIER ::= { pkcs-1 11 }\n//\n//\tsha384WithRSAEncryption OBJECT IDENTIFIER ::= { pkcs-1 12 }\n//\n//\tsha512WithRSAEncryption OBJECT IDENTIFIER ::= { pkcs-1 13 }\n//\n// RFC 5758 3.1 DSA Signature Algorithms\n//\n//\tdsaWithSha256 OBJECT IDENTIFIER ::= {\n//\t\tjoint-iso-ccitt(2) country(16) us(840) organization(1) gov(101)\n//\t\tcsor(3) algorithms(4) id-dsa-with-sha2(3) 2}\n//\n// RFC 5758 3.2 ECDSA Signature Algorithm\n//\n//\tecdsa-with-SHA256 OBJECT IDENTIFIER ::= { iso(1) member-body(2)\n//\t\tus(840) ansi-X9-62(10045) signatures(4) ecdsa-with-SHA2(3) 2 }\n//\n//\tecdsa-with-SHA384 OBJECT IDENTIFIER ::= { iso(1) member-body(2)\n//\t\tus(840) ansi-X9-62(10045) signatures(4) ecdsa-with-SHA2(3) 3 }\n//\n//\tecdsa-with-SHA512 OBJECT IDENTIFIER ::= { iso(1) member-body(2)\n//\t\tus(840) ansi-X9-62(10045) signatures(4) ecdsa-with-SHA2(3) 4 }\n//\n// RFC 8410 3 Curve25519 and Curve448 Algorithm Identifiers\n//\n//\tid-Ed25519   OBJECT IDENTIFIER ::= { 1 3 101 112 }\nvar (\n\toidSignatureMD5WithRSA      = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 4}\n\toidSignatureSHA1WithRSA     = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 5}\n\toidSignatureSHA256WithRSA   = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 11}\n\toidSignatureSHA384WithRSA   = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 12}\n\toidSignatureSHA512WithRSA   = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 13}\n\toidSignatureRSAPSS          = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 10}\n\toidSignatureDSAWithSHA1     = asn1.ObjectIdentifier{1, 2, 840, 10040, 4, 3}\n\toidSignatureDSAWithSHA256   = asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 3, 2}\n\toidSignatureECDSAWithSHA1   = asn1.ObjectIdentifier{1, 2, 840, 10045, 4, 1}\n\toidSignatureECDSAWithSHA256 = asn1.ObjectIdentifier{1, 2, 840, 10045, 4, 3, 2}\n\toidSignatureECDSAWithSHA384 = asn1.ObjectIdentifier{1, 2, 840, 10045, 4, 3, 3}\n\toidSignatureECDSAWithSHA512 = asn1.ObjectIdentifier{1, 2, 840, 10045, 4, 3, 4}\n\toidSignatureEd25519         = asn1.ObjectIdentifier{1, 3, 101, 112}\n\n\toidSHA256 = asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 2, 1}\n\toidSHA384 = asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 2, 2}\n\toidSHA512 = asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 2, 3}\n\n\toidMGF1 = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 8}\n\n\t// oidISOSignatureSHA1WithRSA means the same as oidSignatureSHA1WithRSA\n\t// but it's specified by ISO. Microsoft's makecert.exe has been known\n\t// to produce certificates with this OID.\n\toidISOSignatureSHA1WithRSA = asn1.ObjectIdentifier{1, 3, 14, 3, 2, 29}\n)\n\nvar signatureAlgorithmDetails = []struct {\n\talgo       SignatureAlgorithm\n\tname       string\n\toid        asn1.ObjectIdentifier\n\tparams     asn1.RawValue\n\tpubKeyAlgo PublicKeyAlgorithm\n\thash       crypto.Hash\n\tisRSAPSS   bool\n}{\n\t{MD5WithRSA, \"MD5-RSA\", oidSignatureMD5WithRSA, asn1.NullRawValue, RSA, crypto.MD5, false},\n\t{SHA1WithRSA, \"SHA1-RSA\", oidSignatureSHA1WithRSA, asn1.NullRawValue, RSA, crypto.SHA1, false},\n\t{SHA1WithRSA, \"SHA1-RSA\", oidISOSignatureSHA1WithRSA, asn1.NullRawValue, RSA, crypto.SHA1, false},\n\t{SHA256WithRSA, \"SHA256-RSA\", oidSignatureSHA256WithRSA, asn1.NullRawValue, RSA, crypto.SHA256, false},\n\t{SHA384WithRSA, \"SHA384-RSA\", oidSignatureSHA384WithRSA, asn1.NullRawValue, RSA, crypto.SHA384, false},\n\t{SHA512WithRSA, \"SHA512-RSA\", oidSignatureSHA512WithRSA, asn1.NullRawValue, RSA, crypto.SHA512, false},\n\t{SHA256WithRSAPSS, \"SHA256-RSAPSS\", oidSignatureRSAPSS, pssParametersSHA256, RSA, crypto.SHA256, true},\n\t{SHA384WithRSAPSS, \"SHA384-RSAPSS\", oidSignatureRSAPSS, pssParametersSHA384, RSA, crypto.SHA384, true},\n\t{SHA512WithRSAPSS, \"SHA512-RSAPSS\", oidSignatureRSAPSS, pssParametersSHA512, RSA, crypto.SHA512, true},\n\t{DSAWithSHA1, \"DSA-SHA1\", oidSignatureDSAWithSHA1, emptyRawValue, DSA, crypto.SHA1, false},\n\t{DSAWithSHA256, \"DSA-SHA256\", oidSignatureDSAWithSHA256, emptyRawValue, DSA, crypto.SHA256, false},\n\t{ECDSAWithSHA1, \"ECDSA-SHA1\", oidSignatureECDSAWithSHA1, emptyRawValue, ECDSA, crypto.SHA1, false},\n\t{ECDSAWithSHA256, \"ECDSA-SHA256\", oidSignatureECDSAWithSHA256, emptyRawValue, ECDSA, crypto.SHA256, false},\n\t{ECDSAWithSHA384, \"ECDSA-SHA384\", oidSignatureECDSAWithSHA384, emptyRawValue, ECDSA, crypto.SHA384, false},\n\t{ECDSAWithSHA512, \"ECDSA-SHA512\", oidSignatureECDSAWithSHA512, emptyRawValue, ECDSA, crypto.SHA512, false},\n\t{PureEd25519, \"Ed25519\", oidSignatureEd25519, emptyRawValue, Ed25519, crypto.Hash(0) /* no pre-hashing */, false},\n}\n\nvar emptyRawValue = asn1.RawValue{}\n\n// DER encoded RSA PSS parameters for the\n// SHA256, SHA384, and SHA512 hashes as defined in RFC 3447, Appendix A.2.3.\n// The parameters contain the following values:\n//   - hashAlgorithm contains the associated hash identifier with NULL parameters\n//   - maskGenAlgorithm always contains the default mgf1SHA1 identifier\n//   - saltLength contains the length of the associated hash\n//   - trailerField always contains the default trailerFieldBC value\nvar (\n\tpssParametersSHA256 = asn1.RawValue{FullBytes: []byte{48, 52, 160, 15, 48, 13, 6, 9, 96, 134, 72, 1, 101, 3, 4, 2, 1, 5, 0, 161, 28, 48, 26, 6, 9, 42, 134, 72, 134, 247, 13, 1, 1, 8, 48, 13, 6, 9, 96, 134, 72, 1, 101, 3, 4, 2, 1, 5, 0, 162, 3, 2, 1, 32}}\n\tpssParametersSHA384 = asn1.RawValue{FullBytes: []byte{48, 52, 160, 15, 48, 13, 6, 9, 96, 134, 72, 1, 101, 3, 4, 2, 2, 5, 0, 161, 28, 48, 26, 6, 9, 42, 134, 72, 134, 247, 13, 1, 1, 8, 48, 13, 6, 9, 96, 134, 72, 1, 101, 3, 4, 2, 2, 5, 0, 162, 3, 2, 1, 48}}\n\tpssParametersSHA512 = asn1.RawValue{FullBytes: []byte{48, 52, 160, 15, 48, 13, 6, 9, 96, 134, 72, 1, 101, 3, 4, 2, 3, 5, 0, 161, 28, 48, 26, 6, 9, 42, 134, 72, 134, 247, 13, 1, 1, 8, 48, 13, 6, 9, 96, 134, 72, 1, 101, 3, 4, 2, 3, 5, 0, 162, 3, 2, 1, 64}}\n)\n\n// pssParameters reflects the parameters in an AlgorithmIdentifier that\n// specifies RSA PSS. See RFC 3447, Appendix A.2.3.\ntype pssParameters struct {\n\t// The following three fields are not marked as\n\t// optional because the default values specify SHA-1,\n\t// which is no longer suitable for use in signatures.\n\tHash         pkix.AlgorithmIdentifier `asn1:\"explicit,tag:0\"`\n\tMGF          pkix.AlgorithmIdentifier `asn1:\"explicit,tag:1\"`\n\tSaltLength   int                      `asn1:\"explicit,tag:2\"`\n\tTrailerField int                      `asn1:\"optional,explicit,tag:3,default:1\"`\n}\n\nfunc getSignatureAlgorithmFromAI(ai pkix.AlgorithmIdentifier) stdx509.SignatureAlgorithm {\n\tif ai.Algorithm.Equal(oidSignatureEd25519) {\n\t\t// RFC 8410, Section 3\n\t\t// > For all of the OIDs, the parameters MUST be absent.\n\t\tif len(ai.Parameters.FullBytes) != 0 {\n\t\t\treturn stdx509.UnknownSignatureAlgorithm\n\t\t}\n\t}\n\n\tif !ai.Algorithm.Equal(oidSignatureRSAPSS) {\n\t\tfor _, details := range signatureAlgorithmDetails {\n\t\t\tif ai.Algorithm.Equal(details.oid) {\n\t\t\t\treturn stdx509.SignatureAlgorithm(details.algo)\n\t\t\t}\n\t\t}\n\t\treturn stdx509.UnknownSignatureAlgorithm\n\t}\n\n\t// RSA PSS is special because it encodes important parameters\n\t// in the Parameters.\n\n\tvar params pssParameters\n\tif _, err := asn1.Unmarshal(ai.Parameters.FullBytes, &params); err != nil {\n\t\treturn stdx509.UnknownSignatureAlgorithm\n\t}\n\n\tvar mgf1HashFunc pkix.AlgorithmIdentifier\n\tif _, err := asn1.Unmarshal(params.MGF.Parameters.FullBytes, &mgf1HashFunc); err != nil {\n\t\treturn stdx509.UnknownSignatureAlgorithm\n\t}\n\n\t// PSS is greatly overburdened with options. This code forces them into\n\t// three buckets by requiring that the MGF1 hash function always match the\n\t// message hash function (as recommended in RFC 3447, Section 8.1), that the\n\t// salt length matches the hash length, and that the trailer field has the\n\t// default value.\n\tif (len(params.Hash.Parameters.FullBytes) != 0 && !bytes.Equal(params.Hash.Parameters.FullBytes, asn1.NullBytes)) ||\n\t\t!params.MGF.Algorithm.Equal(oidMGF1) ||\n\t\t!mgf1HashFunc.Algorithm.Equal(params.Hash.Algorithm) ||\n\t\t(len(mgf1HashFunc.Parameters.FullBytes) != 0 && !bytes.Equal(mgf1HashFunc.Parameters.FullBytes, asn1.NullBytes)) ||\n\t\tparams.TrailerField != 1 {\n\t\treturn stdx509.UnknownSignatureAlgorithm\n\t}\n\n\tswitch {\n\tcase params.Hash.Algorithm.Equal(oidSHA256) && params.SaltLength == 32:\n\t\treturn stdx509.SHA256WithRSAPSS\n\tcase params.Hash.Algorithm.Equal(oidSHA384) && params.SaltLength == 48:\n\t\treturn stdx509.SHA384WithRSAPSS\n\tcase params.Hash.Algorithm.Equal(oidSHA512) && params.SaltLength == 64:\n\t\treturn stdx509.SHA512WithRSAPSS\n\t}\n\n\treturn stdx509.UnknownSignatureAlgorithm\n}\n\nvar (\n\t// RFC 3279, 2.3 Public Key Algorithms\n\t//\n\t//\tpkcs-1 OBJECT IDENTIFIER ::== { iso(1) member-body(2) us(840)\n\t//\t\trsadsi(113549) pkcs(1) 1 }\n\t//\n\t// rsaEncryption OBJECT IDENTIFIER ::== { pkcs1-1 1 }\n\t//\n\t//\tid-dsa OBJECT IDENTIFIER ::== { iso(1) member-body(2) us(840)\n\t//\t\tx9-57(10040) x9cm(4) 1 }\n\toidPublicKeyRSA = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 1}\n\toidPublicKeyDSA = asn1.ObjectIdentifier{1, 2, 840, 10040, 4, 1}\n\t// RFC 5480, 2.1.1 Unrestricted Algorithm Identifier and Parameters\n\t//\n\t//\tid-ecPublicKey OBJECT IDENTIFIER ::= {\n\t//\t\tiso(1) member-body(2) us(840) ansi-X9-62(10045) keyType(2) 1 }\n\toidPublicKeyECDSA = asn1.ObjectIdentifier{1, 2, 840, 10045, 2, 1}\n\t// RFC 8410, Section 3\n\t//\n\t//\tid-X25519    OBJECT IDENTIFIER ::= { 1 3 101 110 }\n\t//\tid-Ed25519   OBJECT IDENTIFIER ::= { 1 3 101 112 }\n\toidPublicKeyX25519  = asn1.ObjectIdentifier{1, 3, 101, 110}\n\toidPublicKeyEd25519 = asn1.ObjectIdentifier{1, 3, 101, 112}\n)\n\n// getPublicKeyAlgorithmFromOID returns the exposed PublicKeyAlgorithm\n// identifier for public key types supported in certificates and CSRs. Marshal\n// and Parse functions may support a different set of public key types.\nfunc getPublicKeyAlgorithmFromOID(oid asn1.ObjectIdentifier) stdx509.PublicKeyAlgorithm {\n\tswitch {\n\tcase oid.Equal(oidPublicKeyRSA):\n\t\treturn stdx509.RSA\n\tcase oid.Equal(oidPublicKeyDSA):\n\t\treturn stdx509.DSA\n\tcase oid.Equal(oidPublicKeyECDSA):\n\t\treturn stdx509.ECDSA\n\tcase oid.Equal(oidPublicKeyEd25519):\n\t\treturn stdx509.Ed25519\n\t}\n\treturn stdx509.UnknownPublicKeyAlgorithm\n}\n\n// RFC 5480, 2.1.1.1. Named Curve\n//\n//\tsecp224r1 OBJECT IDENTIFIER ::= {\n//\t  iso(1) identified-organization(3) certicom(132) curve(0) 33 }\n//\n//\tsecp256r1 OBJECT IDENTIFIER ::= {\n//\t  iso(1) member-body(2) us(840) ansi-X9-62(10045) curves(3)\n//\t  prime(1) 7 }\n//\n//\tsecp384r1 OBJECT IDENTIFIER ::= {\n//\t  iso(1) identified-organization(3) certicom(132) curve(0) 34 }\n//\n//\tsecp521r1 OBJECT IDENTIFIER ::= {\n//\t  iso(1) identified-organization(3) certicom(132) curve(0) 35 }\n//\n// NB: secp256r1 is equivalent to prime256v1\nvar (\n\toidNamedCurveP224 = asn1.ObjectIdentifier{1, 3, 132, 0, 33}\n\toidNamedCurveP256 = asn1.ObjectIdentifier{1, 2, 840, 10045, 3, 1, 7}\n\toidNamedCurveP384 = asn1.ObjectIdentifier{1, 3, 132, 0, 34}\n\toidNamedCurveP521 = asn1.ObjectIdentifier{1, 3, 132, 0, 35}\n)\n\nfunc namedCurveFromOID(oid asn1.ObjectIdentifier) elliptic.Curve {\n\tswitch {\n\tcase oid.Equal(oidNamedCurveP224):\n\t\treturn elliptic.P224()\n\tcase oid.Equal(oidNamedCurveP256):\n\t\treturn elliptic.P256()\n\tcase oid.Equal(oidNamedCurveP384):\n\t\treturn elliptic.P384()\n\tcase oid.Equal(oidNamedCurveP521):\n\t\treturn elliptic.P521()\n\t}\n\treturn nil\n}\n\n// KeyUsage represents the set of actions that are valid for a given key. It's\n// a bitmap of the KeyUsage* constants.\ntype KeyUsage int\n\nconst (\n\tKeyUsageDigitalSignature KeyUsage = 1 << iota\n\tKeyUsageContentCommitment\n\tKeyUsageKeyEncipherment\n\tKeyUsageDataEncipherment\n\tKeyUsageKeyAgreement\n\tKeyUsageCertSign\n\tKeyUsageCRLSign\n\tKeyUsageEncipherOnly\n\tKeyUsageDecipherOnly\n)\n\n// RFC 5280, 4.2.1.12  Extended Key Usage\n//\n//\tanyExtendedKeyUsage OBJECT IDENTIFIER ::= { id-ce-extKeyUsage 0 }\n//\n//\tid-kp OBJECT IDENTIFIER ::= { id-pkix 3 }\n//\n//\tid-kp-serverAuth             OBJECT IDENTIFIER ::= { id-kp 1 }\n//\tid-kp-clientAuth             OBJECT IDENTIFIER ::= { id-kp 2 }\n//\tid-kp-codeSigning            OBJECT IDENTIFIER ::= { id-kp 3 }\n//\tid-kp-emailProtection        OBJECT IDENTIFIER ::= { id-kp 4 }\n//\tid-kp-timeStamping           OBJECT IDENTIFIER ::= { id-kp 8 }\n//\tid-kp-OCSPSigning            OBJECT IDENTIFIER ::= { id-kp 9 }\nvar (\n\toidExtKeyUsageAny                            = asn1.ObjectIdentifier{2, 5, 29, 37, 0}\n\toidExtKeyUsageServerAuth                     = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 1}\n\toidExtKeyUsageClientAuth                     = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 2}\n\toidExtKeyUsageCodeSigning                    = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 3}\n\toidExtKeyUsageEmailProtection                = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 4}\n\toidExtKeyUsageIPSECEndSystem                 = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 5}\n\toidExtKeyUsageIPSECTunnel                    = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 6}\n\toidExtKeyUsageIPSECUser                      = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 7}\n\toidExtKeyUsageTimeStamping                   = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 8}\n\toidExtKeyUsageOCSPSigning                    = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 9}\n\toidExtKeyUsageMicrosoftServerGatedCrypto     = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 311, 10, 3, 3}\n\toidExtKeyUsageNetscapeServerGatedCrypto      = asn1.ObjectIdentifier{2, 16, 840, 1, 113730, 4, 1}\n\toidExtKeyUsageMicrosoftCommercialCodeSigning = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 311, 2, 1, 22}\n\toidExtKeyUsageMicrosoftKernelCodeSigning     = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 311, 61, 1, 1}\n)\n\n// ExtKeyUsage represents an extended set of actions that are valid for a given key.\n// Each of the ExtKeyUsage* constants define a unique action.\ntype ExtKeyUsage int\n\nconst (\n\tExtKeyUsageAny ExtKeyUsage = iota\n\tExtKeyUsageServerAuth\n\tExtKeyUsageClientAuth\n\tExtKeyUsageCodeSigning\n\tExtKeyUsageEmailProtection\n\tExtKeyUsageIPSECEndSystem\n\tExtKeyUsageIPSECTunnel\n\tExtKeyUsageIPSECUser\n\tExtKeyUsageTimeStamping\n\tExtKeyUsageOCSPSigning\n\tExtKeyUsageMicrosoftServerGatedCrypto\n\tExtKeyUsageNetscapeServerGatedCrypto\n\tExtKeyUsageMicrosoftCommercialCodeSigning\n\tExtKeyUsageMicrosoftKernelCodeSigning\n)\n\n// extKeyUsageOIDs contains the mapping between an ExtKeyUsage and its OID.\nvar extKeyUsageOIDs = []struct {\n\textKeyUsage ExtKeyUsage\n\toid         asn1.ObjectIdentifier\n}{\n\t{ExtKeyUsageAny, oidExtKeyUsageAny},\n\t{ExtKeyUsageServerAuth, oidExtKeyUsageServerAuth},\n\t{ExtKeyUsageClientAuth, oidExtKeyUsageClientAuth},\n\t{ExtKeyUsageCodeSigning, oidExtKeyUsageCodeSigning},\n\t{ExtKeyUsageEmailProtection, oidExtKeyUsageEmailProtection},\n\t{ExtKeyUsageIPSECEndSystem, oidExtKeyUsageIPSECEndSystem},\n\t{ExtKeyUsageIPSECTunnel, oidExtKeyUsageIPSECTunnel},\n\t{ExtKeyUsageIPSECUser, oidExtKeyUsageIPSECUser},\n\t{ExtKeyUsageTimeStamping, oidExtKeyUsageTimeStamping},\n\t{ExtKeyUsageOCSPSigning, oidExtKeyUsageOCSPSigning},\n\t{ExtKeyUsageMicrosoftServerGatedCrypto, oidExtKeyUsageMicrosoftServerGatedCrypto},\n\t{ExtKeyUsageNetscapeServerGatedCrypto, oidExtKeyUsageNetscapeServerGatedCrypto},\n\t{ExtKeyUsageMicrosoftCommercialCodeSigning, oidExtKeyUsageMicrosoftCommercialCodeSigning},\n\t{ExtKeyUsageMicrosoftKernelCodeSigning, oidExtKeyUsageMicrosoftKernelCodeSigning},\n}\n\nfunc extKeyUsageFromOID(oid asn1.ObjectIdentifier) (eku ExtKeyUsage, ok bool) {\n\tfor _, pair := range extKeyUsageOIDs {\n\t\tif oid.Equal(pair.oid) {\n\t\t\treturn pair.extKeyUsage, true\n\t\t}\n\t}\n\treturn\n}\n\nconst (\n\tnameTypeEmail = 1\n\tnameTypeDNS   = 2\n\tnameTypeURI   = 6\n\tnameTypeIP    = 7\n)\n\nvar (\n\toidExtensionAuthorityInfoAccess = []int{1, 3, 6, 1, 5, 5, 7, 1, 1}\n)\n\nvar (\n\toidAuthorityInfoAccessOcsp    = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 48, 1}\n\toidAuthorityInfoAccessIssuers = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 48, 2}\n)\n\nfunc isIA5String(s string) error {\n\tfor _, r := range s {\n\t\t// Per RFC5280 \"IA5String is limited to the set of ASCII characters\"\n\t\tif r > unicode.MaxASCII {\n\t\t\treturn fmt.Errorf(\"x509: %q cannot be encoded as an IA5String\", s)\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "test/integration/scep/regular_cas_test.go",
    "content": "package sceptest\n\nimport (\n\t\"context\"\n\t\"crypto\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/http\"\n\t\"path/filepath\"\n\t\"sync\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"go.step.sm/crypto/keyutil\"\n\t\"go.step.sm/crypto/minica\"\n\t\"go.step.sm/crypto/pemutil\"\n\n\t\"github.com/smallstep/certificates/authority/config\"\n\t\"github.com/smallstep/certificates/authority/provisioner\"\n\t\"github.com/smallstep/certificates/ca\"\n\t\"github.com/smallstep/certificates/cas/apiv1\"\n)\n\nfunc TestFailsIssuingCertificateUsingRegularSCEPWithUpstreamCAS(t *testing.T) {\n\tsigner, err := keyutil.GenerateSigner(\"RSA\", \"\", 2048)\n\trequire.NoError(t, err)\n\n\tdir := t.TempDir()\n\tt.Setenv(\"STEPPATH\", dir)\n\n\tm, err := minica.New(minica.WithName(\"Step E2E | SCEP Regular w/ Upstream CAS\"), minica.WithGetSignerFunc(func() (crypto.Signer, error) {\n\t\treturn signer, nil\n\t}))\n\trequire.NoError(t, err)\n\n\trootFilepath := filepath.Join(dir, \"root.crt\")\n\t_, err = pemutil.Serialize(m.Root, pemutil.WithFilename(rootFilepath))\n\trequire.NoError(t, err)\n\n\tintermediateCertFilepath := filepath.Join(dir, \"intermediate.crt\")\n\t_, err = pemutil.Serialize(m.Intermediate, pemutil.WithFilename(intermediateCertFilepath))\n\trequire.NoError(t, err)\n\n\tintermediateKeyFilepath := filepath.Join(dir, \"intermediate.key\")\n\t_, err = pemutil.Serialize(m.Signer, pemutil.WithFilename(intermediateKeyFilepath))\n\trequire.NoError(t, err)\n\n\t// get a random address to listen on and connect to; currently no nicer way to get one before starting the server\n\t// TODO(hs): find/implement a nicer way to expose the CA URL, similar to how e.g. httptest.Server exposes it?\n\thost, port := reservePort(t)\n\n\tprov := &provisioner.SCEP{\n\t\tID:                            \"scep\",\n\t\tName:                          \"scep\",\n\t\tType:                          \"SCEP\",\n\t\tForceCN:                       false,\n\t\tChallengePassword:             \"the-challenge\",\n\t\tEncryptionAlgorithmIdentifier: 2,\n\t\tMinimumPublicKeyLength:        2048,\n\t\tClaims:                        &config.GlobalProvisionerClaims,\n\t}\n\n\terr = prov.Init(provisioner.Config{})\n\trequire.NoError(t, err)\n\n\tapiv1.Register(\"test-scep-cas\", func(_ context.Context, opts apiv1.Options) (apiv1.CertificateAuthorityService, error) {\n\t\treturn &testCAS{\n\t\t\tca: m,\n\t\t}, nil\n\t})\n\n\tcfg := &config.Config{\n\t\tAddress:  net.JoinHostPort(host, port), // reuse the address that was just \"reserved\"\n\t\tDNSNames: []string{\"127.0.0.1\", \"[::1]\", \"localhost\"},\n\t\tAuthorityConfig: &config.AuthConfig{\n\t\t\tOptions: &apiv1.Options{\n\t\t\t\tAuthorityID:          \"stepca-test-scep\",\n\t\t\t\tType:                 \"test-scep-cas\",\n\t\t\t\tCertificateAuthority: \"test-cas\",\n\t\t\t},\n\t\t\tAuthorityID:    \"stepca-test-scep\",\n\t\t\tDeploymentType: \"standalone-test\",\n\t\t\tProvisioners:   provisioner.List{prov},\n\t\t},\n\t\tLogger: json.RawMessage(`{\"format\": \"text\"}`),\n\t}\n\tc, err := ca.New(cfg)\n\trequire.NoError(t, err)\n\n\tvar wg sync.WaitGroup\n\twg.Add(1)\n\n\tgo func() {\n\t\tdefer wg.Done()\n\t\terr = c.Run()\n\t\trequire.ErrorIs(t, err, http.ErrServerClosed)\n\t}()\n\n\t// instantiate a client for the CA running at the random address\n\tcaClient := newCAClient(t, fmt.Sprintf(\"https://localhost:%s\", port), rootFilepath)\n\trequireHealthyCA(t, caClient)\n\n\t// issuance is expected to fail when an upstream CAS is configured, as the current\n\t// CAS interfaces do not support providing a decrypter.\n\tscepClient := createSCEPClient(t, fmt.Sprintf(\"https://localhost:%s/scep/scep\", port), m.Root)\n\tcert, err := scepClient.requestCertificate(t)\n\tassert.Error(t, err)\n\tassert.Nil(t, cert)\n\n\t// done testing; stop and wait for the server to quit\n\terr = c.Stop()\n\trequire.NoError(t, err)\n\n\twg.Wait()\n}\n"
  },
  {
    "path": "test/integration/scep/regular_test.go",
    "content": "package sceptest\n\nimport (\n\t\"net/http\"\n\t\"sync\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/smallstep/scep\"\n)\n\nfunc TestIssuesCertificateUsingRegularSCEPConfiguration(t *testing.T) {\n\tc := newTestCA(t, \"Step E2E | SCEP Regular\")\n\n\tvar wg sync.WaitGroup\n\twg.Add(1)\n\n\tgo func() {\n\t\tdefer wg.Done()\n\t\terr := c.run()\n\t\trequire.ErrorIs(t, err, http.ErrServerClosed)\n\t}()\n\n\t// instantiate a client for the CA running at the random address\n\tcaClient := newCAClient(t, c.caURL, c.rootFilepath)\n\trequireHealthyCA(t, caClient)\n\n\tscepClient := createSCEPClient(t, c.caURL, c.root)\n\tcert, err := scepClient.requestCertificate(t)\n\trequire.NoError(t, err)\n\trequire.NotNil(t, cert)\n\n\tassert.Equal(t, \"test.localhost\", cert.Subject.CommonName)\n\tassert.Equal(t, \"Step E2E | SCEP Regular Intermediate CA\", cert.Issuer.CommonName)\n\n\t// done testing; stop and wait for the server to quit\n\terr = c.stop()\n\trequire.NoError(t, err)\n\n\twg.Wait()\n}\n\nfunc TestBlocksCertificateRequestUsingInvalidChallenge(t *testing.T) {\n\tc := newTestCA(t, \"Step E2E | SCEP Regular w/ invalid challenge\")\n\n\tvar wg sync.WaitGroup\n\twg.Add(1)\n\n\tgo func() {\n\t\tdefer wg.Done()\n\t\terr := c.run()\n\t\trequire.ErrorIs(t, err, http.ErrServerClosed)\n\t}()\n\n\t// instantiate a client for the CA running at the random address\n\tcaClient := newCAClient(t, c.caURL, c.rootFilepath)\n\trequireHealthyCA(t, caClient)\n\n\tscepClient := createSCEPClient(t, c.caURL, c.root)\n\tcert, err := scepClient.requestCertificate(t, withChallenge(\"an-invalid-challenge\"))\n\trequire.Error(t, err)\n\trequire.Nil(t, cert)\n\n\t// done testing; stop and wait for the server to quit\n\terr = c.stop()\n\trequire.NoError(t, err)\n\n\twg.Wait()\n}\n\nfunc TestBlocksUnsupportedMessageType(t *testing.T) {\n\tc := newTestCA(t, \"Step E2E | SCEP Regular w/ unsupported message type\")\n\n\tvar wg sync.WaitGroup\n\twg.Add(1)\n\n\tgo func() {\n\t\tdefer wg.Done()\n\t\terr := c.run()\n\t\trequire.ErrorIs(t, err, http.ErrServerClosed)\n\t}()\n\n\t// instantiate a client for the CA running at the random address\n\tcaClient := newCAClient(t, c.caURL, c.rootFilepath)\n\trequireHealthyCA(t, caClient)\n\n\tscepClient := createSCEPClient(t, c.caURL, c.root)\n\tcert, err := scepClient.requestCertificate(t, withMessageType(scep.CertPoll))\n\trequire.Error(t, err)\n\trequire.Nil(t, cert)\n\n\t// done testing; stop and wait for the server to quit\n\terr = c.stop()\n\trequire.NoError(t, err)\n\n\twg.Wait()\n}\n"
  },
  {
    "path": "test/integration/scep/windows_go1.23_test.go",
    "content": "//go:build go1.23\n\npackage sceptest\n\nimport (\n\t\"crypto/rand\"\n\t\"crypto/rsa\"\n\t\"crypto/x509\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"sync\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\tlegacyx509 \"github.com/smallstep/certificates/test/integration/scep/internal/x509\"\n)\n\nfunc legacyCertificateParser(der []byte) (*x509.Certificate, error) {\n\tcerts, err := legacyx509.ParseCertificates(der)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed parsing self signed certificate: %w\", err)\n\t}\n\n\treturn certs[0], nil\n}\n\nfunc TestIssuesCertificateToEmulatedWindowsClientGo123(t *testing.T) {\n\tc := newTestCA(t, \"Step E2E | SCEP Regular w/ Windows Client\")\n\n\tvar wg sync.WaitGroup\n\twg.Add(1)\n\n\tgo func() {\n\t\tdefer wg.Done()\n\t\terr := c.run()\n\t\trequire.ErrorIs(t, err, http.ErrServerClosed)\n\t}()\n\n\t// instantiate a client for the CA running at the random address\n\tcaClient := newCAClient(t, c.caURL, c.rootFilepath)\n\trequireHealthyCA(t, caClient)\n\n\tscepClient := createSCEPClient(t, c.caURL, c.root)\n\n\tsigner, err := rsa.GenerateKey(rand.Reader, 2048)\n\trequire.NoError(t, err)\n\n\ttmpl := createWindowsTemplate(t, signer)\n\tcert, err := scepClient.requestCertificate(t, withTemplate(tmpl), withSigner(signer), withCertificateParser(legacyCertificateParser))\n\trequire.NoError(t, err)\n\trequire.NotNil(t, cert)\n\n\tassert.Equal(t, \"test.localhost\", cert.Subject.CommonName)\n\tassert.Equal(t, \"Step E2E | SCEP Regular w/ Windows Client Intermediate CA\", cert.Issuer.CommonName)\n\n\t// done testing; stop and wait for the server to quit\n\terr = c.stop()\n\trequire.NoError(t, err)\n\n\twg.Wait()\n}\n"
  },
  {
    "path": "test/integration/scep/windows_test.go",
    "content": "//go:build !go1.23\n\npackage sceptest\n\nimport (\n\t\"crypto/rand\"\n\t\"crypto/rsa\"\n\t\"net/http\"\n\t\"sync\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestIssuesCertificateToEmulatedWindowsClient(t *testing.T) {\n\tc := newTestCA(t, \"Step E2E | SCEP Regular w/ Windows Client\")\n\n\tvar wg sync.WaitGroup\n\twg.Add(1)\n\tgo func() {\n\t\tdefer wg.Done()\n\t\terr := c.run()\n\t\trequire.ErrorIs(t, err, http.ErrServerClosed)\n\t}()\n\n\t// instantiate a client for the CA running at the random address\n\tcaClient := newCAClient(t, c.caURL, c.rootFilepath)\n\trequireHealthyCA(t, caClient)\n\n\tscepClient := createSCEPClient(t, c.caURL, c.root)\n\n\tsigner, err := rsa.GenerateKey(rand.Reader, 2048)\n\trequire.NoError(t, err)\n\n\ttmpl := createWindowsTemplate(t, signer)\n\tcert, err := scepClient.requestCertificate(t, withTemplate(tmpl), withSigner(signer))\n\trequire.NoError(t, err)\n\trequire.NotNil(t, cert)\n\n\tassert.Equal(t, \"test.localhost\", cert.Subject.CommonName)\n\tassert.Equal(t, \"Step E2E | SCEP Regular w/ Windows Client Intermediate CA\", cert.Issuer.CommonName)\n\n\t// done testing; stop and wait for the server to quit\n\terr = c.stop()\n\trequire.NoError(t, err)\n\n\twg.Wait()\n}\n"
  },
  {
    "path": "tools.go",
    "content": "//go:build tools\n\npackage certificates\n\nimport (\n\t_ \"go.uber.org/mock/mockgen\"\n)\n"
  },
  {
    "path": "webhook/options.go",
    "content": "package webhook\n\nimport (\n\t\"crypto/x509\"\n\n\t\"go.step.sm/crypto/sshutil\"\n\t\"go.step.sm/crypto/x509util\"\n\t\"golang.org/x/crypto/ssh\"\n)\n\ntype RequestBodyOption func(*RequestBody) error\n\nfunc NewRequestBody(options ...RequestBodyOption) (*RequestBody, error) {\n\trb := &RequestBody{}\n\n\tfor _, fn := range options {\n\t\tif err := fn(rb); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn rb, nil\n}\n\nfunc WithX509CertificateRequest(cr *x509.CertificateRequest) RequestBodyOption {\n\treturn func(rb *RequestBody) error {\n\t\trb.X509CertificateRequest = &X509CertificateRequest{\n\t\t\tCertificateRequest: x509util.NewCertificateRequestFromX509(cr),\n\t\t\tPublicKeyAlgorithm: cr.PublicKeyAlgorithm.String(),\n\t\t\tRaw:                cr.Raw,\n\t\t}\n\t\tif cr.PublicKey != nil {\n\t\t\tkey, err := x509.MarshalPKIXPublicKey(cr.PublicKey)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\trb.X509CertificateRequest.PublicKey = key\n\t\t}\n\n\t\treturn nil\n\t}\n}\n\nfunc WithX509Certificate(cert *x509util.Certificate, leaf *x509.Certificate) RequestBodyOption {\n\treturn func(rb *RequestBody) error {\n\t\trb.X509Certificate = &X509Certificate{\n\t\t\tCertificate:        cert,\n\t\t\tPublicKeyAlgorithm: leaf.PublicKeyAlgorithm.String(),\n\t\t\tNotBefore:          leaf.NotBefore,\n\t\t\tNotAfter:           leaf.NotAfter,\n\t\t}\n\t\tif leaf.PublicKey != nil {\n\t\t\tkey, err := x509.MarshalPKIXPublicKey(leaf.PublicKey)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\trb.X509Certificate.PublicKey = key\n\t\t}\n\n\t\treturn nil\n\t}\n}\n\nfunc WithAttestationData(data *AttestationData) RequestBodyOption {\n\treturn func(rb *RequestBody) error {\n\t\trb.AttestationData = data\n\t\treturn nil\n\t}\n}\n\nfunc WithAuthorizationPrincipal(p string) RequestBodyOption {\n\treturn func(rb *RequestBody) error {\n\t\trb.AuthorizationPrincipal = p\n\t\treturn nil\n\t}\n}\n\nfunc WithSSHCertificateRequest(cr sshutil.CertificateRequest) RequestBodyOption {\n\treturn func(rb *RequestBody) error {\n\t\trb.SSHCertificateRequest = &SSHCertificateRequest{\n\t\t\tType:       cr.Type,\n\t\t\tKeyID:      cr.KeyID,\n\t\t\tPrincipals: cr.Principals,\n\t\t}\n\t\tif cr.Key != nil {\n\t\t\trb.SSHCertificateRequest.PublicKey = cr.Key.Marshal()\n\t\t}\n\t\treturn nil\n\t}\n}\n\nfunc WithSSHCertificate(cert *sshutil.Certificate, certTpl *ssh.Certificate) RequestBodyOption {\n\treturn func(rb *RequestBody) error {\n\t\trb.SSHCertificate = &SSHCertificate{\n\t\t\tCertificate: cert,\n\t\t\tValidBefore: certTpl.ValidBefore,\n\t\t\tValidAfter:  certTpl.ValidAfter,\n\t\t}\n\t\tif certTpl.Key != nil {\n\t\t\trb.SSHCertificate.PublicKey = certTpl.Key.Marshal()\n\t\t}\n\t\treturn nil\n\t}\n}\n\nfunc WithX5CCertificate(leaf *x509.Certificate) RequestBodyOption {\n\treturn func(rb *RequestBody) error {\n\t\trb.X5CCertificate = &X5CCertificate{\n\t\t\tRaw:                leaf.Raw,\n\t\t\tPublicKeyAlgorithm: leaf.PublicKeyAlgorithm.String(),\n\t\t\tNotBefore:          leaf.NotBefore,\n\t\t\tNotAfter:           leaf.NotAfter,\n\t\t}\n\t\tif leaf.PublicKey != nil {\n\t\t\tkey, err := x509.MarshalPKIXPublicKey(leaf.PublicKey)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\trb.X5CCertificate.PublicKey = key\n\t\t}\n\n\t\treturn nil\n\t}\n}\n"
  },
  {
    "path": "webhook/options_test.go",
    "content": "package webhook\n\nimport (\n\t\"crypto/x509\"\n\t\"crypto/x509/pkix\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/smallstep/assert\"\n\t\"go.step.sm/crypto/keyutil\"\n\t\"go.step.sm/crypto/sshutil\"\n\t\"go.step.sm/crypto/x509util\"\n\t\"golang.org/x/crypto/ssh\"\n)\n\nfunc TestNewRequestBody(t *testing.T) {\n\tt1 := time.Now()\n\tt2 := t1.Add(time.Hour)\n\n\tkey, err := keyutil.GenerateDefaultSigner()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tkeyBytes, err := x509.MarshalPKIXPublicKey(key.Public())\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\ttype test struct {\n\t\toptions []RequestBodyOption\n\t\twant    *RequestBody\n\t\twantErr bool\n\t}\n\ttests := map[string]test{\n\t\t\"Permanent Identifier\": {\n\t\t\toptions: []RequestBodyOption{WithAttestationData(&AttestationData{PermanentIdentifier: \"mydevice123\"})},\n\t\t\twant: &RequestBody{\n\t\t\t\tAttestationData: &AttestationData{\n\t\t\t\t\tPermanentIdentifier: \"mydevice123\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twantErr: false,\n\t\t},\n\t\t\"X509 Certificate Request\": {\n\t\t\toptions: []RequestBodyOption{\n\t\t\t\tWithX509CertificateRequest(&x509.CertificateRequest{\n\t\t\t\t\tPublicKeyAlgorithm: x509.ECDSA,\n\t\t\t\t\tSubject:            pkix.Name{CommonName: \"foo\"},\n\t\t\t\t\tRaw:                []byte(\"csr der\"),\n\t\t\t\t}),\n\t\t\t},\n\t\t\twant: &RequestBody{\n\t\t\t\tX509CertificateRequest: &X509CertificateRequest{\n\t\t\t\t\tCertificateRequest: &x509util.CertificateRequest{\n\t\t\t\t\t\tPublicKeyAlgorithm: x509.ECDSA,\n\t\t\t\t\t\tSubject:            x509util.Subject{CommonName: \"foo\"},\n\t\t\t\t\t},\n\t\t\t\t\tPublicKeyAlgorithm: \"ECDSA\",\n\t\t\t\t\tRaw:                []byte(\"csr der\"),\n\t\t\t\t},\n\t\t\t},\n\t\t\twantErr: false,\n\t\t},\n\t\t\"X509 Certificate\": {\n\t\t\toptions: []RequestBodyOption{\n\t\t\t\tWithX509Certificate(&x509util.Certificate{}, &x509.Certificate{\n\t\t\t\t\tNotBefore:          t1,\n\t\t\t\t\tNotAfter:           t2,\n\t\t\t\t\tPublicKeyAlgorithm: x509.ECDSA,\n\t\t\t\t}),\n\t\t\t},\n\t\t\twant: &RequestBody{\n\t\t\t\tX509Certificate: &X509Certificate{\n\t\t\t\t\tCertificate:        &x509util.Certificate{},\n\t\t\t\t\tPublicKeyAlgorithm: \"ECDSA\",\n\t\t\t\t\tNotBefore:          t1,\n\t\t\t\t\tNotAfter:           t2,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t\"SSH Certificate Request\": {\n\t\t\toptions: []RequestBodyOption{\n\t\t\t\tWithSSHCertificateRequest(sshutil.CertificateRequest{\n\t\t\t\t\tType:       \"User\",\n\t\t\t\t\tKeyID:      \"key1\",\n\t\t\t\t\tPrincipals: []string{\"areed\", \"other\"},\n\t\t\t\t})},\n\t\t\twant: &RequestBody{\n\t\t\t\tSSHCertificateRequest: &SSHCertificateRequest{\n\t\t\t\t\tType:       \"User\",\n\t\t\t\t\tKeyID:      \"key1\",\n\t\t\t\t\tPrincipals: []string{\"areed\", \"other\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\twantErr: false,\n\t\t},\n\t\t\"SSH Certificate\": {\n\t\t\toptions: []RequestBodyOption{\n\t\t\t\tWithSSHCertificate(\n\t\t\t\t\t&sshutil.Certificate{},\n\t\t\t\t\t&ssh.Certificate{\n\t\t\t\t\t\tValidAfter:  uint64(t1.Unix()),\n\t\t\t\t\t\tValidBefore: uint64(t2.Unix()),\n\t\t\t\t\t},\n\t\t\t\t),\n\t\t\t},\n\t\t\twant: &RequestBody{\n\t\t\t\tSSHCertificate: &SSHCertificate{\n\t\t\t\t\tCertificate: &sshutil.Certificate{},\n\t\t\t\t\tValidAfter:  uint64(t1.Unix()),\n\t\t\t\t\tValidBefore: uint64(t2.Unix()),\n\t\t\t\t},\n\t\t\t},\n\t\t\twantErr: false,\n\t\t},\n\t\t\"X5C Certificate\": {\n\t\t\toptions: []RequestBodyOption{\n\t\t\t\tWithX5CCertificate(&x509.Certificate{\n\t\t\t\t\tRaw:                []byte(\"some raw data\"),\n\t\t\t\t\tNotBefore:          t1,\n\t\t\t\t\tNotAfter:           t2,\n\t\t\t\t\tPublicKeyAlgorithm: x509.ECDSA,\n\t\t\t\t\tPublicKey:          key.Public(),\n\t\t\t\t}),\n\t\t\t},\n\t\t\twant: &RequestBody{\n\t\t\t\tX5CCertificate: &X5CCertificate{\n\t\t\t\t\tRaw:                []byte(\"some raw data\"),\n\t\t\t\t\tPublicKeyAlgorithm: \"ECDSA\",\n\t\t\t\t\tNotBefore:          t1,\n\t\t\t\t\tNotAfter:           t2,\n\t\t\t\t\tPublicKey:          keyBytes,\n\t\t\t\t},\n\t\t\t},\n\t\t\twantErr: false,\n\t\t},\n\t\t\"fail/X5C Certificate\": {\n\t\t\toptions: []RequestBodyOption{\n\t\t\t\tWithX5CCertificate(&x509.Certificate{\n\t\t\t\t\tRaw:                []byte(\"some raw data\"),\n\t\t\t\t\tNotBefore:          t1,\n\t\t\t\t\tNotAfter:           t2,\n\t\t\t\t\tPublicKeyAlgorithm: x509.ECDSA,\n\t\t\t\t\tPublicKey:          []byte(\"fail\"),\n\t\t\t\t}),\n\t\t\t},\n\t\t\twant:    nil,\n\t\t\twantErr: true,\n\t\t},\n\t}\n\tfor name, test := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tgot, err := NewRequestBody(test.options...)\n\t\t\tif (err != nil) != test.wantErr {\n\t\t\t\tt.Fatalf(\"Got err %v, wanted %t\", err, test.wantErr)\n\t\t\t}\n\t\t\tassert.Equals(t, test.want, got)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "webhook/types.go",
    "content": "package webhook\n\nimport (\n\t\"fmt\"\n\t\"time\"\n\n\t\"go.step.sm/crypto/sshutil\"\n\t\"go.step.sm/crypto/x509util\"\n)\n\n// ResponseBody is the body returned by webhook servers.\ntype ResponseBody struct {\n\tData  any    `json:\"data\"`\n\tAllow bool   `json:\"allow\"`\n\tError *Error `json:\"error,omitempty\"`\n}\n\n// Error provides details explaining why the webhook was not permitted.\ntype Error struct {\n\tCode    string `json:\"code\"`\n\tMessage string `json:\"message\"`\n}\n\nfunc (e *Error) Error() string {\n\treturn fmt.Sprintf(\"%s (%s)\", e.Message, e.Code)\n}\n\n// X509CertificateRequest is the certificate request sent to webhook servers for\n// enriching webhooks when signing x509 certificates\ntype X509CertificateRequest struct {\n\t*x509util.CertificateRequest\n\tPublicKey          []byte `json:\"publicKey\"`\n\tPublicKeyAlgorithm string `json:\"publicKeyAlgorithm\"`\n\tRaw                []byte `json:\"raw\"`\n}\n\n// X509Certificate is the certificate sent to webhook servers for authorizing\n// webhooks when signing x509 certificates\ntype X509Certificate struct {\n\t*x509util.Certificate\n\tPublicKey          []byte    `json:\"publicKey\"`\n\tPublicKeyAlgorithm string    `json:\"publicKeyAlgorithm\"`\n\tNotBefore          time.Time `json:\"notBefore\"`\n\tNotAfter           time.Time `json:\"notAfter\"`\n\tRaw                []byte    `json:\"raw\"`\n}\n\n// SSHCertificateRequest is the certificate request sent to webhook servers for\n// enriching webhooks when signing SSH certificates\ntype SSHCertificateRequest struct {\n\tPublicKey  []byte   `json:\"publicKey\"`\n\tType       string   `json:\"type\"`\n\tKeyID      string   `json:\"keyID\"`\n\tPrincipals []string `json:\"principals\"`\n}\n\n// SSHCertificate is the certificate sent to webhook servers for authorizing\n// webhooks when signing SSH certificates\ntype SSHCertificate struct {\n\t*sshutil.Certificate\n\tPublicKey    []byte `json:\"publicKey\"`\n\tSignatureKey []byte `json:\"signatureKey\"`\n\tValidBefore  uint64 `json:\"validBefore\"`\n\tValidAfter   uint64 `json:\"validAfter\"`\n}\n\n// AttestationData is data validated by acme device-attest-01 challenge\ntype AttestationData struct {\n\tPermanentIdentifier string `json:\"permanentIdentifier\"`\n}\n\n// X5CCertificate is the authorization certificate sent to webhook servers for\n// enriching or authorizing webhooks when signing X509 or SSH certificates using\n// the X5C provisioner.\ntype X5CCertificate struct {\n\tRaw                []byte    `json:\"raw\"`\n\tPublicKey          []byte    `json:\"publicKey\"`\n\tPublicKeyAlgorithm string    `json:\"publicKeyAlgorithm\"`\n\tNotBefore          time.Time `json:\"notBefore\"`\n\tNotAfter           time.Time `json:\"notAfter\"`\n}\n\n// RequestBody is the body sent to webhook servers.\ntype RequestBody struct {\n\tTimestamp       time.Time `json:\"timestamp\"`\n\tProvisionerName string    `json:\"provisionerName,omitempty\"`\n\t// Only set after successfully completing acme device-attest-01 challenge\n\tAttestationData *AttestationData `json:\"attestationData,omitempty\"`\n\t// Set for most provisioners, but not acme or scep\n\t// Token any `json:\"token,omitempty\"`\n\t// Exactly one of the remaining fields should be set\n\tX509CertificateRequest *X509CertificateRequest `json:\"x509CertificateRequest,omitempty\"`\n\tX509Certificate        *X509Certificate        `json:\"x509Certificate,omitempty\"`\n\tSSHCertificateRequest  *SSHCertificateRequest  `json:\"sshCertificateRequest,omitempty\"`\n\tSSHCertificate         *SSHCertificate         `json:\"sshCertificate,omitempty\"`\n\t// Only set for SCEP webhook requests\n\tSCEPChallenge        string `json:\"scepChallenge,omitempty\"`\n\tSCEPTransactionID    string `json:\"scepTransactionID,omitempty\"`\n\tSCEPErrorCode        int    `json:\"scepErrorCode,omitempty\"`\n\tSCEPErrorDescription string `json:\"scepErrorDescription,omitempty\"`\n\t// Only set for X5C provisioners\n\tX5CCertificate *X5CCertificate `json:\"x5cCertificate,omitempty\"`\n\t// Set for X5C, AWS, GCP, and Azure provisioners\n\tAuthorizationPrincipal string `json:\"authorizationPrincipal,omitempty\"`\n}\n"
  }
]