├── .github ├── ISSUE_TEMPLATE │ ├── bug-report.md │ ├── config.yml │ └── feature_request.md ├── arch.png ├── grafana-dashboard.png ├── logo.svg └── workflows │ ├── go.yml │ ├── issues.yaml │ └── release.yml ├── .gitignore ├── .golangci.yml ├── .goreleaser.yml ├── CONTRIBUTING.md ├── CREDITS ├── Dockerfile ├── Dockerfile.dev ├── LICENSE ├── MAINTAINERS.md ├── README.md ├── SECURITY.md ├── api_test.go ├── audit.go ├── auth.go ├── auth_test.go ├── cmd └── kes │ ├── autocomplete.go │ ├── color-option.go │ ├── flags.go │ ├── identity.go │ ├── key.go │ ├── log.go │ ├── ls.go │ ├── main.go │ ├── metric.go │ ├── migrate.go │ ├── mlock_linux.go │ ├── mlock_ref.go │ ├── policy.go │ ├── server.go │ ├── status.go │ └── update.go ├── code_of_conduct.md ├── config.go ├── example_test.go ├── examples └── grafana │ └── dashboard.json ├── go.mod ├── go.sum ├── internal ├── api │ ├── api.go │ ├── error.go │ ├── multicast.go │ ├── request.go │ └── response.go ├── cache │ ├── barrier.go │ ├── barrier_test.go │ ├── cow.go │ └── cow_test.go ├── cli │ ├── buffer.go │ ├── env.go │ ├── exit.go │ ├── fmt.go │ └── term.go ├── cpu │ └── aes.go ├── crypto │ ├── ciphertext.go │ ├── key.go │ └── key_test.go ├── fips │ ├── api.go │ ├── fips.go │ └── nofips.go ├── headers │ ├── header.go │ └── header_test.go ├── http │ ├── close.go │ └── retry.go ├── https │ ├── certificate.go │ ├── certificate_test.go │ ├── certpool.go │ ├── certpool_test.go │ ├── flush.go │ ├── proxy.go │ ├── proxy_test.go │ └── testdata │ │ ├── ca │ │ └── single.pem │ │ ├── certificates │ │ ├── single.pem │ │ ├── with_privatekey.pem │ │ └── with_whitespaces.pem │ │ └── privatekeys │ │ ├── encrypted.pem │ │ └── plaintext.pem ├── keystore │ ├── aws │ │ └── secrets-manager.go │ ├── azure │ │ ├── client.go │ │ ├── create-keyvault.sh │ │ ├── error.go │ │ ├── key-vault.go │ │ └── key-vault_test.go │ ├── entrust │ │ └── keycontrol.go │ ├── fortanix │ │ └── keystore.go │ ├── fs │ │ ├── fs.go │ │ └── fs_test.go │ ├── gcp │ │ ├── config.go │ │ ├── config_test.go │ │ └── secret-manager.go │ ├── gemalto │ │ ├── client.go │ │ └── key-secure.go │ ├── keystore.go │ ├── keystore_test.go │ └── vault │ │ ├── client.go │ │ ├── config.go │ │ ├── config_test.go │ │ └── vault.go ├── metric │ └── metric.go ├── protobuf │ ├── crypto.pb.go │ ├── crypto.proto │ └── proto.go └── sys │ └── build.go ├── kesconf ├── aws_test.go ├── azure_test.go ├── config.go ├── config_test.go ├── edge_test.go ├── file.go ├── fortanix_test.go ├── fs_test.go ├── gcp_test.go ├── gemalto_test.go ├── keycontrol_test.go ├── testdata │ ├── aws-no-credentials.yml │ ├── aws.yml │ ├── custom-api.yml │ ├── fs.yml │ ├── vault-approle.yml │ ├── vault-k8s-service-account │ ├── vault-k8s-with-service-account-file.yml │ ├── vault-k8s.yml │ └── vault.yml └── vault_test.go ├── keystore.go ├── log.go ├── minisign.pub ├── root.cert ├── root.key ├── server-config.yaml ├── server.go ├── server_test.go └── state.go /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Please use this template if you want to report a bug. 4 | title: '' 5 | labels: '' 6 | assignees: aead 7 | 8 | --- 9 | 10 | #### Bug describtion 11 | 15 | 16 | #### Expected behavior 17 | 18 | 19 | #### Additional context 20 | 21 | 1. What version of Go are you using (`go version`)? 22 | 23 | 2. What operating system and processor architecture are you using (`go env`)? 24 | 25 | 3. Anything else that is important? 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: MinIO Community Support 4 | url: https://slack.min.io 5 | about: Join here for Community Support. 6 | - name: MinIO SUBNET Support 7 | url: https://min.io/pricing 8 | about: Join here for Enterprise Support. 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Please use this template if you want to suggest an idea. 4 | title: '' 5 | labels: feature-request 6 | assignees: aead 7 | 8 | --- 9 | 10 | #### What is the problem you want to solve? 11 | 12 | 13 | #### How do you want to solve it? 14 | 15 | #### Additional context 16 | 17 | 1. Are there alternative solutions? 18 | 19 | 20 | 2. Would your solution cause a major breaking API change? 21 | 22 | 23 | 3. Anything else that is important? 24 | -------------------------------------------------------------------------------- /.github/arch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minio/kes/63649d0ea9be882887631cf315c4655f3c090d8f/.github/arch.png -------------------------------------------------------------------------------- /.github/grafana-dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minio/kes/63649d0ea9be882887631cf315c4655f3c090d8f/.github/grafana-dashboard.png -------------------------------------------------------------------------------- /.github/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master 7 | push: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | build: 13 | name: Build 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Set up Go 17 | uses: actions/setup-go@v5 18 | with: 19 | go-version: 1.24.2 20 | id: go 21 | - name: Check out code 22 | uses: actions/checkout@v4 23 | - name: Build and Lint 24 | run: | 25 | go build ./... 26 | go vet ./... 27 | lint: 28 | name: Lint 29 | runs-on: ubuntu-latest 30 | steps: 31 | - name: "Set up Go" 32 | uses: actions/setup-go@v5 33 | with: 34 | go-version: 1.24.2 35 | id: go 36 | - name: Check out code 37 | uses: actions/checkout@v4 38 | - name: Lint 39 | uses: golangci/golangci-lint-action@v6 40 | with: 41 | version: v1.64.5 42 | args: --config ./.golangci.yml --timeout=5m 43 | test: 44 | name: Test ${{ matrix.os }} 45 | needs: Lint 46 | runs-on: ${{ matrix.os }} 47 | strategy: 48 | matrix: 49 | os: [ubuntu-latest, windows-latest, macos-latest] 50 | steps: 51 | - name: Set up Go 52 | uses: actions/setup-go@v5 53 | with: 54 | go-version: 1.24.2 55 | id: go 56 | - name: Check out code 57 | uses: actions/checkout@v4 58 | - name: Test 59 | env: 60 | GO111MODULE: on 61 | run: | 62 | go test ./... 63 | 64 | vulncheck: 65 | name: Vulncheck ${{ matrix.go-version }} 66 | runs-on: ubuntu-latest 67 | steps: 68 | - name: Set up Go ${{ matrix.go-version }} 69 | uses: actions/setup-go@v5 70 | with: 71 | go-version: 1.24.2 72 | - name: Check out code into the Go module directory 73 | uses: actions/checkout@v4 74 | - name: Get govulncheck 75 | run: go install golang.org/x/vuln/cmd/govulncheck@latest 76 | shell: bash 77 | - name: Run govulncheck 78 | run: govulncheck ./... 79 | shell: bash 80 | -------------------------------------------------------------------------------- /.github/workflows/issues.yaml: -------------------------------------------------------------------------------- 1 | # @format 2 | 3 | name: Issue Workflow 4 | 5 | on: 6 | issues: 7 | types: 8 | - opened 9 | 10 | jobs: 11 | add-to-project: 12 | name: Add issue to project 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/add-to-project@v0.5.0 16 | with: 17 | project-url: https://github.com/orgs/miniohq/projects/2 18 | github-token: ${{ secrets.BOT_PAT }} 19 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master 7 | push: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | goreleaser: 13 | name: Build 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v4 18 | - name: Unshallow 19 | run: git fetch --prune --unshallow 20 | - name: Set up Go 21 | uses: actions/setup-go@v5 22 | with: 23 | go-version: 1.24.0 24 | - name: Set up QEMU 25 | uses: docker/setup-qemu-action@v1 26 | - name: Set up Docker Buildx 27 | uses: docker/setup-buildx-action@v1 28 | - name: Run GoReleaser 29 | uses: goreleaser/goreleaser-action@v3 30 | with: 31 | version: latest 32 | args: release --skip=publish,sign,before --clean --snapshot 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.cert 3 | *.key 4 | *.crt 5 | .idea 6 | kes 7 | !kes/ 8 | *.test 9 | dist/ -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | linters-settings: 2 | misspell: 3 | locale: US 4 | 5 | staticcheck: 6 | checks: ["all", "-SA1019"] 7 | 8 | linters: 9 | disable-all: true 10 | enable: 11 | - typecheck 12 | - goimports 13 | - misspell 14 | - staticcheck 15 | - govet 16 | - revive 17 | - ineffassign 18 | - gosimple 19 | - unused 20 | - prealloc 21 | - unconvert 22 | - gofumpt 23 | 24 | issues: 25 | exclude-use-default: false 26 | exclude: 27 | - "var-naming: don't use ALL_CAPS in Go names; use CamelCase" 28 | - "package-comments: should have a package comment" 29 | - "exitAfterDefer:" 30 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | project_name: kes 3 | 4 | release: 5 | name_template: Release {{ replace .CommitDate ":" "-" }} 6 | 7 | github: 8 | owner: minio 9 | name: kes 10 | 11 | before: 12 | hooks: 13 | - go mod tidy 14 | - bash -c 'git diff --quiet || (echo "Repo contains modified files. Run git checkout" && exit 1)' 15 | - bash -c '[[ {{ .Tag }} == {{ replace .CommitDate ":" "-" }} ]] || (echo Invalid release tag. Run git tag {{ replace .CommitDate ":" "-" }} && exit 1)' 16 | 17 | builds: 18 | - 19 | main: ./cmd/kes 20 | goos: 21 | - linux 22 | - darwin 23 | - windows 24 | goarch: 25 | - amd64 26 | - arm64 27 | ignore: 28 | - goos: windows 29 | goarch: arm64 30 | - goos: darwin 31 | goarch: amd64 32 | env: 33 | - CGO_ENABLED=0 34 | flags: 35 | - -trimpath 36 | - -buildvcs=true 37 | ldflags: 38 | - "-s -w" 39 | 40 | archives: 41 | - 42 | name_template: "{{ .ProjectName }}-{{ .Os }}-{{ .Arch }}" 43 | format: binary 44 | 45 | nfpms: 46 | - 47 | vendor: MinIO, Inc. 48 | homepage: https://github.com/minio/kes 49 | maintainer: MinIO Development 50 | description: KES is a stateless and distributed key-management system for high-performance applications 51 | license: GNU Affero General Public License v3.0 52 | formats: 53 | - deb 54 | - rpm 55 | 56 | signs: 57 | - 58 | signature: "${artifact}.minisig" 59 | cmd: "sh" 60 | args: 61 | - '-c' 62 | - 'minisign -s /media/${USER}/minio/minisign.key -Sm ${artifact} < /media/${USER}/minio/minisign-passphrase' 63 | artifacts: all 64 | 65 | snapshot: 66 | version_template: '{{ replace .CommitDate ":" "-" }}' 67 | 68 | changelog: 69 | sort: asc 70 | 71 | dockers: 72 | - image_templates: 73 | - minio/kes:{{ replace .CommitDate ":" "-" }}-amd64 74 | use: buildx 75 | dockerfile: Dockerfile 76 | extra_files: 77 | - LICENSE 78 | - CREDITS 79 | build_flag_templates: 80 | - '--platform=linux/amd64' 81 | - '--build-arg=TAG={{ replace .CommitDate ":" "-" }}' 82 | - image_templates: 83 | - quay.io/minio/kes:{{ replace .CommitDate ":" "-" }}-amd64 84 | use: buildx 85 | dockerfile: Dockerfile 86 | extra_files: 87 | - LICENSE 88 | - CREDITS 89 | build_flag_templates: 90 | - '--platform=linux/amd64' 91 | - '--build-arg=TAG={{ replace .CommitDate ":" "-" }}' 92 | - image_templates: 93 | - minio/kes:{{ replace .CommitDate ":" "-" }}-arm64 94 | use: buildx 95 | goarch: arm64 96 | dockerfile: Dockerfile 97 | extra_files: 98 | - LICENSE 99 | - CREDITS 100 | build_flag_templates: 101 | - '--platform=linux/arm64' 102 | - '--build-arg=TAG={{ replace .CommitDate ":" "-" }}' 103 | - image_templates: 104 | - quay.io/minio/kes:{{ replace .CommitDate ":" "-" }}-arm64 105 | use: buildx 106 | goarch: arm64 107 | dockerfile: Dockerfile 108 | extra_files: 109 | - LICENSE 110 | - CREDITS 111 | build_flag_templates: 112 | - '--platform=linux/arm64' 113 | - '--build-arg=TAG={{ replace .CommitDate ":" "-" }}' 114 | docker_manifests: 115 | - name_template: minio/kes:{{ replace .CommitDate ":" "-" }} 116 | image_templates: 117 | - minio/kes:{{ replace .CommitDate ":" "-" }}-amd64 118 | - minio/kes:{{ replace .CommitDate ":" "-" }}-arm64 119 | - name_template: quay.io/minio/kes:{{ replace .CommitDate ":" "-" }} 120 | image_templates: 121 | - quay.io/minio/kes:{{ replace .CommitDate ":" "-" }}-amd64 122 | - quay.io/minio/kes:{{ replace .CommitDate ":" "-" }}-arm64 123 | - name_template: minio/kes:latest 124 | image_templates: 125 | - minio/kes:{{ replace .CommitDate ":" "-" }}-amd64 126 | - minio/kes:{{ replace .CommitDate ":" "-" }}-arm64 127 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | **The KES project welcomes all contributors.** 2 | 3 | This document is a guide to help you getting your pull request merged more 4 | quickly. 5 | 6 | ### Source Code 7 | 8 | KES is (mostly) written in Go. The following resources may be valuable 9 | to reduce code-review iterations: 10 | - [Code Formatting](https://github.com/mvdan/gofumpt) - KES uses `gofumpt` instead of `gofmt` or `goimports`. 11 | - [Go Style Guide](https://google.github.io/styleguide/go/decisions) 12 | - [Code Comments and Documentation](https://go.dev/doc/comment) 13 | - [Go Best Practices](https://github.com/golang/go/wiki/CodeReviewComments) 14 | 15 | ### Commit Message 16 | 17 | The commit message explains **what** a change does and **why** it was needed. 18 | 19 | Others, including yourself, may refer to the commit message to understand the 20 | purpose of a commit. A good commit message can safe many hours or days of work 21 | in the future. 22 | 23 | For example: 24 | ``` 25 | a one line summary of the commit 26 | 27 | First, explain why a change is needed if it isn't self-describing. 28 | Then talk about what a change does and its potential side-effects 29 | before explaining the the design decisions. For example, explain 30 | why you have chosen approach A instead of B. 31 | 32 | List assumptions / invariants that your change relies on. For example, 33 | that some initialization logic assumes that it operations on a clean 34 | state. 35 | 36 | Include benchmarks when claiming a performance gain or loss. 37 | 38 | Reference related commits, pull requests or issues. For example: 39 | 40 | Ref: #101 41 | Ref: a2b1987 42 | 43 | When fixing an issue, include the issue number. The following directive 44 | automatically references and automatically closes the issue on merge: 45 | 46 | Fixes #102 47 | ``` 48 | 49 | ### License 50 | 51 | KES is an opensource project licensed under AGPLv3. The license file 52 | can be found [here](https://github.com/minio/kes/blob/master/LICENSE). 53 | 54 | By contributing to KES, you agree to assign the copyright to MinIO. 55 | Any contributed source file must include the following license 56 | header: 57 | ``` 58 | // Copyright - MinIO, Inc. All rights reserved. 59 | // Use of this source code is governed by the AGPLv3 60 | // license that can be found in the LICENSE file. 61 | ``` 62 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM registry.access.redhat.com/ubi9/ubi-minimal:9.5 as build 2 | 3 | RUN microdnf update -y --nodocs && microdnf install ca-certificates --nodocs 4 | 5 | FROM registry.access.redhat.com/ubi9/ubi-micro:9.5 6 | 7 | ARG TAG 8 | 9 | LABEL name="MinIO" \ 10 | vendor="MinIO Inc " \ 11 | maintainer="MinIO Inc " \ 12 | version="${TAG}" \ 13 | release="${TAG}" \ 14 | summary="KES is a cloud-native distributed key management and encryption server designed to build zero-trust infrastructures at scale." 15 | 16 | # On RHEL the certificate bundle is located at: 17 | # - /etc/pki/tls/certs/ca-bundle.crt (RHEL 6) 18 | # - /etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem (RHEL 7) 19 | COPY --from=build /etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem /etc/pki/ca-trust/extracted/pem/ 20 | 21 | COPY LICENSE /LICENSE 22 | COPY CREDITS /CREDITS 23 | COPY kes /kes 24 | 25 | EXPOSE 7373 26 | 27 | ENTRYPOINT ["/kes"] 28 | CMD ["kes"] 29 | -------------------------------------------------------------------------------- /Dockerfile.dev: -------------------------------------------------------------------------------- 1 | FROM golang:1.21-alpine as build 2 | 3 | LABEL maintainer="MinIO Inc " 4 | 5 | ENV GOPATH /go 6 | ENV CGO_ENABLED 0 7 | 8 | RUN \ 9 | apk add -U --no-cache ca-certificates && \ 10 | apk add --no-cache git && \ 11 | git clone https://github.com/minio/kes && cd kes && \ 12 | go install -v -trimpath -buildvcs=true -ldflags "-s -w" ./cmd/kes 13 | 14 | FROM scratch 15 | 16 | COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ 17 | COPY --from=build /go/bin/kes /kes 18 | 19 | EXPOSE 7373 20 | 21 | ENTRYPOINT ["/kes"] 22 | -------------------------------------------------------------------------------- /MAINTAINERS.md: -------------------------------------------------------------------------------- 1 | # Release versioning 2 | 3 | KES server is released with time based version tags, similar to MinIO server. 4 | 5 | To get the release name in the appropriate format, run the following with the code checked out at the desired revision: 6 | 7 | ```shell 8 | TZ=UTC0 git show --quiet --date='format-local:%Y-%m-%dT%H-%M-%SZ' --format="%cd" 9 | ``` 10 | 11 | # Making a release 12 | 13 | Set the GITHUB_TOKEN environment variable to the token for the account making the release and run goreleaser: 14 | 15 | ```shell 16 | export GITHUB_TOKEN=mytokenvalue 17 | goreleaser --clean 18 | 19 | ``` 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | We always provide security updates for the [latest release](https://github.com/minio/kes/releases/latest). 6 | Whenever there is a security update you just need to upgrade to the latest version. 7 | 8 | ## Reporting a Vulnerability 9 | 10 | All security bugs in [minio/kes](https://github,com/minio/kes) should be reported by email to security@min.io. 11 | Your email will be acknowledged within 48 hours, and you'll receive a more detailed response to your email 12 | within 72 hours indicating the next steps in handling your report. 13 | 14 | Please, provide a detailed explanation of the issue. In particular, outline the type of the security 15 | issue (DoS, authentication bypass, information disclose, ...) and the assumptions you're making (e.g. do 16 | you need access credentials for a successful exploit). 17 | 18 | If you have not received a reply to your email within 48 hours or you have not heard from the security team 19 | for the past five days please contact the security team directly: 20 | - Primary security coordinator: aead@min.io 21 | - Secondary coordinator: harsha@min.io 22 | - If you receive no response: dev@min.io 23 | 24 | ### Disclosure Process 25 | 26 | MinIO uses the following disclosure process: 27 | 28 | 1. Once the security report is received one member of the security team tries to verify and reproduce 29 | the issue and determines the impact it has. 30 | 2. A member of the security team will respond and either confirm or reject the security report. 31 | If the report is rejected the response explains why. 32 | 3. Code is audited to find any potential similar problems. 33 | 4. Fixes are prepared for the latest release. 34 | 5. On the date that the fixes are applied a security advisory will be published on https://github.com/minio/kes/security/advisories. 35 | Please inform us in your report email whether MinIO should mention your contribution w.r.t. fixing 36 | the security issue. By default MinIO will **not** publish this information to protect your privacy. 37 | 38 | This process can take some time, especially when coordination is required with maintainers of other projects. 39 | Every effort will be made to handle the bug in as timely a manner as possible, however it's important that we 40 | follow the process described above to ensure that disclosures are handled consistently. 41 | -------------------------------------------------------------------------------- /audit.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 - MinIO, Inc. All rights reserved. 2 | // Use of this source code is governed by the AGPLv3 3 | // license that can be found in the LICENSE file. 4 | 5 | package kes 6 | 7 | import ( 8 | "context" 9 | "encoding/json" 10 | "log/slog" 11 | "net/netip" 12 | "time" 13 | 14 | "github.com/minio/kes/internal/api" 15 | "github.com/minio/kms-go/kes" 16 | ) 17 | 18 | // AuditRecord describes an audit event logged by a KES server. 19 | type AuditRecord struct { 20 | // Point in time when the audit event happened. 21 | Time time.Time 22 | 23 | // The request HTTP method. (GET, PUT, ...) 24 | Method string 25 | 26 | // Request URL path. Always starts with a '/'. 27 | Path string 28 | 29 | // Identity that send the request. 30 | Identity kes.Identity 31 | 32 | // IP address of the client that sent the request. 33 | RemoteIP netip.Addr 34 | 35 | // Status code the KES server responded with. 36 | StatusCode int 37 | 38 | // Amount of time the server took to process the 39 | // request and generate a response. 40 | ResponseTime time.Duration 41 | 42 | // The log level of this event. 43 | Level slog.Level 44 | 45 | // The log message describing the event. 46 | Message string 47 | } 48 | 49 | // An AuditHandler handles audit records produced by a Server. 50 | // 51 | // A typical handler may print audit records to standard error, 52 | // or write them to a file or database. 53 | // 54 | // Any of the AuditHandler's methods may be called concurrently 55 | // with itself or with other methods. It is the responsibility 56 | // of the Handler to manage this concurrency. 57 | type AuditHandler interface { 58 | // Enabled reports whether the handler handles records at 59 | // the given level. The handler ignores records whose level 60 | // is lower. It is called early, before an audit record is 61 | // created, to safe effort if the audit event should be 62 | // discarded. 63 | // 64 | // The Server will pass the request context as the first 65 | // argument, or context.Background() if no context is 66 | // available. Enabled may use the context to make a 67 | // decision. 68 | Enabled(context.Context, slog.Level) bool 69 | 70 | // Handle handles the AuditRecord. It will only be called when 71 | // Enabled returns true. 72 | // 73 | // The context is present for providing AuditHandlers access 74 | // to the context's values and to potentially pass it to an 75 | // underlying slog.Handler. Canceling the context should not 76 | // affect record processing. 77 | Handle(context.Context, AuditRecord) error 78 | } 79 | 80 | // AuditLogHandler is an AuditHandler adapter that wraps 81 | // an slog.Handler. It converts AuditRecords to slog.Records 82 | // and passes them to the slog.Handler. An AuditLogHandler 83 | // acts as a bridge between AuditHandlers and slog.Handlers. 84 | type AuditLogHandler struct { 85 | Handler slog.Handler 86 | } 87 | 88 | // Enabled reports whether the AuditLogHandler handles records 89 | // at the given level. It returns true if the underlying handler 90 | // returns true. 91 | func (a *AuditLogHandler) Enabled(ctx context.Context, level slog.Level) bool { 92 | return a.Handler.Enabled(ctx, level) 93 | } 94 | 95 | // Handle converts the AuditRecord to an slog.Record and 96 | // passes it to the underlying handler. 97 | func (a *AuditLogHandler) Handle(ctx context.Context, r AuditRecord) error { 98 | rec := slog.Record{ 99 | Time: r.Time, 100 | Message: r.Message, 101 | Level: r.Level, 102 | } 103 | rec.AddAttrs( 104 | slog.Attr{Key: "req", Value: slog.GroupValue( 105 | slog.String("method", r.Method), 106 | slog.String("path", r.Path), 107 | slog.String("ip", r.RemoteIP.String()), 108 | slog.String("identity", r.Identity.String()), 109 | )}, 110 | slog.Attr{Key: "res", Value: slog.GroupValue( 111 | slog.Int("code", r.StatusCode), 112 | slog.Duration("time", r.ResponseTime), 113 | )}, 114 | ) 115 | return a.Handler.Handle(ctx, rec) 116 | } 117 | 118 | // An auditLogger records information about a request/response 119 | // handled by the Server. 120 | // 121 | // For each call of its Log method, it creates an AuditRecord and 122 | // passes it to its AuditHandler. If clients have subscribed to 123 | // the AuditLog API, the logger also sends the AuditRecord to these 124 | // clients. 125 | type auditLogger struct { 126 | h AuditHandler 127 | level slog.Leveler 128 | 129 | out *api.Multicast // clients subscribed to the AuditLog API 130 | } 131 | 132 | // newAuditLogger returns a new auditLogger passing AuditRecords to h. 133 | // A record is only sent to clients subscribed to the AuditLog API if 134 | // its log level is >= level. 135 | func newAuditLogger(h AuditHandler, level slog.Leveler) *auditLogger { 136 | return &auditLogger{ 137 | h: h, 138 | level: level, 139 | out: &api.Multicast{}, 140 | } 141 | } 142 | 143 | // Log emits an audit record with the current time, log message, 144 | // response status code and request information. 145 | func (a *auditLogger) Log(msg string, statusCode int, req *api.Request) { 146 | const Level = slog.LevelInfo 147 | if Level < a.level.Level() { 148 | return 149 | } 150 | 151 | hEnabled, oEnabled := a.h.Enabled(req.Context(), Level), a.out.Num() > 0 152 | if !hEnabled && !oEnabled { 153 | return 154 | } 155 | 156 | now := time.Now() 157 | remoteIP, _ := netip.ParseAddrPort(req.RemoteAddr) 158 | r := AuditRecord{ 159 | Time: time.Now(), 160 | Method: req.Method, 161 | Path: req.URL.Path, 162 | Identity: req.Identity, 163 | RemoteIP: remoteIP.Addr(), 164 | StatusCode: statusCode, 165 | ResponseTime: now.Sub(req.Received), 166 | Level: Level, 167 | Message: msg, 168 | } 169 | if hEnabled { 170 | a.h.Handle(req.Context(), r) 171 | } 172 | 173 | if !oEnabled { 174 | return 175 | } 176 | json.NewEncoder(a.out).Encode(api.AuditLogEvent{ 177 | Time: r.Time, 178 | Request: api.AuditLogRequest{ 179 | IP: r.RemoteIP.String(), 180 | APIPath: r.Path, 181 | Identity: r.Identity.String(), 182 | }, 183 | Response: api.AuditLogResponse{ 184 | StatusCode: r.StatusCode, 185 | Time: r.ResponseTime.Milliseconds(), 186 | }, 187 | }) 188 | } 189 | -------------------------------------------------------------------------------- /auth.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 - MinIO, Inc. All rights reserved. 2 | // Use of this source code is governed by the AGPLv3 3 | // license that can be found in the LICENSE file. 4 | 5 | package kes 6 | 7 | import ( 8 | "crypto/sha256" 9 | "crypto/tls" 10 | "crypto/x509" 11 | "encoding/hex" 12 | "fmt" 13 | "net/http" 14 | "sync/atomic" 15 | 16 | "github.com/minio/kes/internal/api" 17 | "github.com/minio/kms-go/kes" 18 | ) 19 | 20 | // verifyIdentity authenticates client requests by verifying that 21 | // the client provides a certificate during the TLS handshake (mTLS) 22 | // and that the identity of the certificate public key matches either 23 | // the admin identity or an identity with an assigned policy. 24 | // 25 | // A request is accepted if the identity matches the admin identity 26 | // or the policy associated to the identity allows the request. The 27 | // later is the case if none of the policy's deny rules and at least 28 | // one of the policy's allow rules apply. Otherwise, the request is 29 | // rejected. 30 | type verifyIdentity atomic.Pointer[serverState] 31 | 32 | // Authenticate verifies that the request is either sent by the 33 | // server admin or passes the policy assigned to the identity. 34 | // Otherwise, it returns an error. 35 | func (v *verifyIdentity) Authenticate(req *http.Request) (*api.Request, api.Error) { 36 | s := (*atomic.Pointer[serverState])(v).Load() 37 | identity, err := identifyRequest(req.TLS) 38 | if err != nil { 39 | s.Log.DebugContext(req.Context(), err.Error(), "req", req) 40 | return nil, err 41 | } 42 | if identity == s.Admin { 43 | return &api.Request{ 44 | Request: req, 45 | Identity: identity, 46 | }, nil 47 | } 48 | 49 | policy, ok := s.Identities[identity] 50 | if !ok { 51 | s.Log.DebugContext(req.Context(), "access denied: identity not found", "req", req) 52 | return nil, kes.ErrNotAllowed 53 | } 54 | if err := policy.Verify(req); err != nil { 55 | s.Log.DebugContext(req.Context(), fmt.Sprintf("access denied: rejected by policy '%s'", policy.Name), "req", req) 56 | return nil, kes.ErrNotAllowed 57 | } 58 | 59 | return &api.Request{ 60 | Request: req, 61 | Identity: identity, 62 | }, nil 63 | } 64 | 65 | // insecureIdentifyOnly does not authenticate client requests but 66 | // computes the certificate public key identity, if provided. 67 | // It does not return an error if the client did not provide a 68 | // certificate, or an invalid one, during the TLS handshake. In 69 | // such a case, the identity of the returned request is empty. 70 | type insecureIdentifyOnly struct{} 71 | 72 | func (insecureIdentifyOnly) Authenticate(req *http.Request) (*api.Request, api.Error) { 73 | identity, _ := identifyRequest(req.TLS) 74 | return &api.Request{ 75 | Request: req, 76 | Identity: identity, 77 | }, nil 78 | } 79 | 80 | func identifyRequest(state *tls.ConnectionState) (kes.Identity, api.Error) { 81 | if state == nil { 82 | return "", api.NewError(http.StatusBadRequest, "insecure connection: TLS is required") 83 | } 84 | 85 | var cert *x509.Certificate 86 | for _, c := range state.PeerCertificates { 87 | if c.IsCA { 88 | continue 89 | } 90 | if cert != nil { 91 | return "", api.NewError(http.StatusBadRequest, "tls: received more than one client certificate") 92 | } 93 | cert = c 94 | } 95 | if cert == nil { 96 | return "", api.NewError(http.StatusBadRequest, "tls: client certificate is required") 97 | } 98 | 99 | h := sha256.Sum256(cert.RawSubjectPublicKeyInfo) 100 | return kes.Identity(hex.EncodeToString(h[:])), nil 101 | } 102 | 103 | // validName reports whether s is a valid {policy|identity|key} name. 104 | // 105 | // Valid names only contain the characters: 106 | // - 0-9 107 | // - A-Z 108 | // - a-z 109 | // - '-' (hyphen, must not be first/last character) 110 | // - '_' (underscore, must not be the only character) 111 | // 112 | // More characters may be allowed in the future. 113 | func validName(s string) bool { 114 | const MaxLength = 80 // Some arbitrary but reasonable limit 115 | 116 | if s == "" || s == "_" || len(s) > MaxLength { 117 | return false 118 | } 119 | 120 | n := len(s) - 1 121 | for i, r := range s { 122 | switch { 123 | case r >= '0' && r <= '9': 124 | case r >= 'A' && r <= 'Z': 125 | case r >= 'a' && r <= 'z': 126 | case r == '-' && i > 0 && i < n: 127 | case r == '_': 128 | default: 129 | return false 130 | } 131 | } 132 | return true 133 | } 134 | 135 | // validPattern reports whether s is a valid pattern for 136 | // listing {policy|identity|key} names. 137 | // 138 | // Valid patterns only contain the characters: 139 | // - 0-9 140 | // - A-Z 141 | // - a-z 142 | // - '-' (hyphen, must not be first/last character) 143 | // - '_' (underscore, must not be the only character) 144 | // - '*' (only as last character) 145 | // 146 | // More characters may be allowed in the future. 147 | func validPattern(s string) bool { 148 | const MaxLength = 80 // Some arbitrary but reasonable limit 149 | 150 | if s == "*" { // fast path 151 | return true 152 | } 153 | if s == "_" || len(s) > MaxLength { 154 | return false 155 | } 156 | 157 | n := len(s) - 1 158 | for i, r := range s { 159 | switch { 160 | case r >= '0' && r <= '9': 161 | case r >= 'A' && r <= 'Z': 162 | case r >= 'a' && r <= 'z': 163 | case r == '-' && i > 0 && i < n: 164 | case r == '_': 165 | case r == '*' && i == n: 166 | default: 167 | return false 168 | } 169 | } 170 | return true 171 | } 172 | -------------------------------------------------------------------------------- /auth_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 - MinIO, Inc. All rights reserved. 2 | // Use of this source code is governed by the AGPLv3 3 | // license that can be found in the LICENSE file. 4 | 5 | package kes 6 | 7 | import ( 8 | "strings" 9 | "testing" 10 | ) 11 | 12 | func TestValidName(t *testing.T) { 13 | t.Parallel() 14 | for i, test := range validNameTests { 15 | if valid := validName(test.Name); valid != !test.ShouldFail { 16 | t.Errorf("Test %d: got 'valid=%v' - want 'fail=%v' for name '%s'", i, valid, test.ShouldFail, test.Name) 17 | } 18 | } 19 | } 20 | 21 | func TestValidPattern(t *testing.T) { 22 | t.Parallel() 23 | for i, test := range validPatternTests { 24 | if valid := validPattern(test.Pattern); valid != !test.ShouldFail { 25 | t.Errorf("Test %d: got 'valid=%v' - want 'fail=%v' for pattern '%s'", i, valid, test.ShouldFail, test.Pattern) 26 | } 27 | } 28 | } 29 | 30 | func BenchmarkValidName(b *testing.B) { 31 | const ( 32 | EmptyName = "" 33 | ValidName = "my-minio-key" 34 | InvalidName = "my-minio-key*" 35 | ) 36 | 37 | b.Run("empty", func(b *testing.B) { 38 | for i := 0; i < b.N; i++ { 39 | validName(EmptyName) 40 | } 41 | }) 42 | b.Run("valid", func(b *testing.B) { 43 | for i := 0; i < b.N; i++ { 44 | validName(ValidName) 45 | } 46 | }) 47 | b.Run("invalid", func(b *testing.B) { 48 | for i := 0; i < b.N; i++ { 49 | validName(InvalidName) 50 | } 51 | }) 52 | } 53 | 54 | func BenchmarkValidPattern(b *testing.B) { 55 | const ( 56 | MatchAll = "*" 57 | ValidPattern = "my-minio-key*" 58 | InvalidPattern = "my-minio-key/" 59 | ) 60 | 61 | b.Run("matchall", func(b *testing.B) { 62 | for i := 0; i < b.N; i++ { 63 | validPattern(MatchAll) 64 | } 65 | }) 66 | b.Run("valid", func(b *testing.B) { 67 | for i := 0; i < b.N; i++ { 68 | validPattern(ValidPattern) 69 | } 70 | }) 71 | b.Run("invalid", func(b *testing.B) { 72 | for i := 0; i < b.N; i++ { 73 | validPattern(InvalidPattern) 74 | } 75 | }) 76 | } 77 | 78 | var ( 79 | validNameTests = []struct { 80 | Name string 81 | ShouldFail bool 82 | }{ 83 | {Name: "my-key"}, // 0 84 | {Name: "abc123"}, // 1 85 | {Name: "0"}, // 2 86 | {Name: "123ABC321"}, // 3 87 | {Name: "_-___---_"}, // 4 88 | {Name: "_0"}, // 5 89 | {Name: "0-Z"}, // 6 90 | {Name: "my_key-0"}, // 7 91 | 92 | {Name: "", ShouldFail: true}, // 8 93 | {Name: "my.key", ShouldFail: true}, // 9 94 | {Name: "key/", ShouldFail: true}, // 10 95 | {Name: "", ShouldFail: true}, // 11 96 | {Name: "☰", ShouldFail: true}, // 12 97 | {Name: "hel len(match) { 75 | match = key 76 | } 77 | } 78 | if candidates, ok := completion[match]; ok { 79 | line = strings.TrimSpace(strings.TrimPrefix(line, match)) 80 | for _, candidate := range candidates { 81 | if strings.HasPrefix(candidate, line) { 82 | fmt.Println(candidate) 83 | } 84 | } 85 | } 86 | return true 87 | } 88 | 89 | func installAutoCompletion() { 90 | if runtime.GOOS == "windows" { 91 | cli.Fatal("auto-completion is not available for windows") 92 | } 93 | 94 | shell, ok := os.LookupEnv("SHELL") 95 | if !ok { 96 | cli.Fatal("failed to detect shell. The env variable $SHELL is not defined") 97 | } 98 | 99 | var filename string 100 | var isZsh bool 101 | switch { 102 | case strings.HasSuffix(shell, "zsh"): 103 | filename = ".zshrc" 104 | isZsh = true 105 | case strings.HasSuffix(shell, "bash"): 106 | filename = ".bashrc" 107 | default: 108 | cli.Fatalf("auto-completion for '%s' is not available", shell) 109 | } 110 | 111 | home, err := os.UserHomeDir() 112 | if err != nil { 113 | cli.Fatalf("failed to detect home directory: %v", home) 114 | } 115 | if home == "" { 116 | home = "~" 117 | } 118 | filename = filepath.Join(home, filename) 119 | 120 | binaryPath, err := os.Executable() 121 | if err != nil { 122 | cli.Fatalf("failed to detect binary path: %v", err) 123 | } 124 | binaryPath, err = filepath.Abs(binaryPath) 125 | if err != nil { 126 | cli.Fatalf("failed to turn binary path into an absolute path: %v", err) 127 | } 128 | 129 | var ( 130 | autoloadCmd = "autoload -U +X bashcompinit && bashcompinit" 131 | completeCmd = fmt.Sprintf("complete -o default -C %s %s", binaryPath, os.Args[0]) 132 | ) 133 | 134 | hasAutoloadLine, hasCompleteLine := isCompletionInstalled(filename, autoloadCmd, completeCmd) 135 | if isZsh && (hasAutoloadLine && hasCompleteLine) { 136 | cli.Println("Completion is already installed.") 137 | return 138 | } 139 | 140 | if !isZsh && hasCompleteLine { 141 | cli.Println("Completion is already installed.") 142 | return 143 | } 144 | 145 | file, err := os.OpenFile(filename, os.O_WRONLY|os.O_APPEND|os.O_SYNC, 0o600) 146 | if err != nil { 147 | cli.Fatal(err) 148 | } 149 | defer file.Close() 150 | 151 | if isZsh && !hasAutoloadLine { 152 | if _, err = file.WriteString(autoloadCmd + "\n"); err != nil { 153 | cli.Fatalf("failed to add '%s' to '%s': %v", autoloadCmd, filename, err) 154 | } 155 | } 156 | 157 | if !hasCompleteLine { 158 | if _, err = file.WriteString(completeCmd + "\n"); err != nil { 159 | cli.Fatalf("failed to add '%s' to '%s': %v", completeCmd, filename, err) 160 | } 161 | } 162 | if err = file.Close(); err != nil { 163 | cli.Fatal(err) 164 | } 165 | 166 | cli.Printf("Added completion to '%s'\n", filename) 167 | cli.Println() 168 | cli.Printf("To uninstall completion remove the following lines from '%s':\n", filename) 169 | if isZsh && !hasAutoloadLine { 170 | cli.Println(" ", autoloadCmd) 171 | } 172 | if !hasCompleteLine { 173 | cli.Println(" ", completeCmd) 174 | } 175 | } 176 | 177 | func isCompletionInstalled(filename, autoloadCmd, completeCmd string) (autoload, complete bool) { 178 | file, err := os.Open(filename) 179 | if err != nil { 180 | cli.Fatal(err) 181 | } 182 | defer file.Close() 183 | 184 | scanner := bufio.NewScanner(file) 185 | for scanner.Scan() { 186 | if strings.HasPrefix(scanner.Text(), autoloadCmd) { 187 | autoload = true 188 | } 189 | if strings.HasPrefix(scanner.Text(), completeCmd) { 190 | complete = true 191 | } 192 | } 193 | if err = scanner.Err(); err != nil { 194 | cli.Fatalf("failed to read '%s': %v", filename, err) 195 | } 196 | if err = file.Close(); err != nil { 197 | cli.Fatal(err) 198 | } 199 | return 200 | } 201 | -------------------------------------------------------------------------------- /cmd/kes/color-option.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 - MinIO, Inc. All rights reserved. 2 | // Use of this source code is governed by the AGPLv3 3 | // license that can be found in the LICENSE file. 4 | 5 | package main 6 | 7 | import ( 8 | "errors" 9 | "strings" 10 | 11 | tui "github.com/charmbracelet/lipgloss" 12 | "github.com/minio/kes/internal/cli" 13 | "github.com/muesli/termenv" 14 | flag "github.com/spf13/pflag" 15 | ) 16 | 17 | // colorOption is a CLI Flag that controls 18 | // terminal output colorization. It can be 19 | // set to one of the following values: 20 | // 21 | // · always 22 | // · auto (default) 23 | // · never 24 | type colorOption struct { 25 | value string 26 | } 27 | 28 | var _ flag.Value = (*colorOption)(nil) 29 | 30 | func (c *colorOption) Colorize() bool { 31 | v := strings.ToLower(c.value) 32 | return v == "always" || ((v == "auto" || v == "") && cli.IsTerminal()) 33 | } 34 | 35 | func (c *colorOption) String() string { return c.value } 36 | 37 | func (c *colorOption) Set(value string) error { 38 | switch strings.ToLower(value) { 39 | case "always": 40 | if p := tui.ColorProfile(); p == termenv.Ascii { 41 | tui.SetColorProfile(termenv.ANSI256) 42 | } 43 | c.value = value 44 | return nil 45 | case "auto", "": 46 | c.value = value 47 | return nil 48 | case "never": 49 | tui.SetColorProfile(termenv.Ascii) 50 | c.value = value 51 | return nil 52 | default: 53 | return errors.New("invalid color option") 54 | } 55 | } 56 | 57 | func (c *colorOption) Type() string { return "color option" } 58 | -------------------------------------------------------------------------------- /cmd/kes/flags.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 - MinIO, Inc. All rights reserved. 2 | // Use of this source code is governed by the AGPLv3 3 | // license that can be found in the LICENSE file. 4 | 5 | package main 6 | 7 | import ( 8 | "github.com/minio/kes/internal/cli" 9 | flag "github.com/spf13/pflag" 10 | ) 11 | 12 | // Use register functions for common flags exposed by 13 | // many commands. Common flags should have common names 14 | // to make command usage consistent. 15 | 16 | // flagsInsecureSkipVerify adds a bool flag '-k, --insecure' 17 | // that sets insecureSkipVerify to true if provided on the 18 | // command line. 19 | func flagsInsecureSkipVerify(f *flag.FlagSet, insecureSkipVerify *bool) { 20 | f.BoolVarP(insecureSkipVerify, "insecure", "k", false, "Skip server certificate verification") 21 | } 22 | 23 | func flagsAPIKey(f *flag.FlagSet, apiKey *string) { 24 | f.StringVarP(apiKey, "api-key", "a", cli.Env(cli.EnvAPIKey), "API key to authenticate to the KES server") 25 | } 26 | 27 | func flagsOutputJSON(f *flag.FlagSet, jsonOutput *bool) { 28 | f.BoolVar(jsonOutput, "json", false, "Print output in JSON format") 29 | } 30 | 31 | func flagsServer(f *flag.FlagSet, host *string) { 32 | f.StringVarP(host, "server", "s", cli.Env(cli.EnvServer), "Use the server HOST[:PORT]") 33 | } 34 | -------------------------------------------------------------------------------- /cmd/kes/log.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 - MinIO, Inc. All rights reserved. 2 | // Use of this source code is governed by the AGPLv3 3 | // license that can be found in the LICENSE file. 4 | 5 | package main 6 | 7 | import ( 8 | "context" 9 | "errors" 10 | "fmt" 11 | "net/http" 12 | "os" 13 | "os/signal" 14 | "strconv" 15 | "time" 16 | 17 | tui "github.com/charmbracelet/lipgloss" 18 | "github.com/minio/kes/internal/cli" 19 | "github.com/minio/kms-go/kes" 20 | 21 | flag "github.com/spf13/pflag" 22 | ) 23 | 24 | const logCmdUsage = `Usage: 25 | kes log 26 | 27 | Options: 28 | --audit Print audit logs. (default) 29 | --error Print error logs. 30 | --json Print log events as JSON. 31 | 32 | -k, --insecure Skip TLS certificate validation. 33 | -h, --help Print command line options. 34 | 35 | Examples: 36 | $ kes log 37 | $ kes log --error 38 | ` 39 | 40 | func logCmd(args []string) { 41 | cmd := flag.NewFlagSet(args[0], flag.ContinueOnError) 42 | cmd.Usage = func() { fmt.Fprint(os.Stderr, logCmdUsage) } 43 | 44 | var ( 45 | auditFlag bool 46 | errorFlag bool 47 | jsonFlag bool 48 | insecureSkipVerify bool 49 | ) 50 | cmd.BoolVar(&auditFlag, "audit", true, "Print audit logs") 51 | cmd.BoolVar(&errorFlag, "error", false, "Print error logs") 52 | cmd.BoolVar(&jsonFlag, "json", false, "Print log events as JSON") 53 | cmd.BoolVarP(&insecureSkipVerify, "insecure", "k", false, "Skip TLS certificate validation") 54 | if err := cmd.Parse(args[1:]); err != nil { 55 | if errors.Is(err, flag.ErrHelp) { 56 | os.Exit(2) 57 | } 58 | cli.Fatalf("%v. See 'kes log --help'", err) 59 | } 60 | if cmd.NArg() > 0 { 61 | cli.Fatal("too many arguments. See 'kes key import --help'") 62 | } 63 | if auditFlag && errorFlag && cmd.Changed("audit") { 64 | cli.Fatal("cannot display audit and error logs at the same time") 65 | } 66 | if auditFlag && errorFlag { // Unset (default) audit flag if error flag has been set 67 | auditFlag = !auditFlag 68 | } 69 | 70 | client := newClient(config{ 71 | InsecureSkipVerify: insecureSkipVerify, 72 | }) 73 | ctx, cancelCtx := signal.NotifyContext(context.Background(), os.Interrupt, os.Kill) 74 | defer cancelCtx() 75 | 76 | switch { 77 | case auditFlag: 78 | stream, err := client.AuditLog(ctx) 79 | if err != nil { 80 | if errors.Is(err, context.Canceled) { 81 | os.Exit(1) 82 | } 83 | cli.Fatalf("failed to connect to error log: %v", err) 84 | } 85 | defer stream.Close() 86 | 87 | if jsonFlag { 88 | if _, err = stream.WriteTo(os.Stdout); err != nil { 89 | cli.Fatal(err) 90 | } 91 | } else { 92 | printAuditLog(stream) 93 | } 94 | case errorFlag: 95 | stream, err := client.ErrorLog(ctx) 96 | if err != nil { 97 | if errors.Is(err, context.Canceled) { 98 | os.Exit(1) 99 | } 100 | cli.Fatalf("failed to connect to error log: %v", err) 101 | } 102 | defer stream.Close() 103 | 104 | if jsonFlag { 105 | if _, err = stream.WriteTo(os.Stdout); err != nil { 106 | cli.Fatal(err) 107 | } 108 | } else { 109 | printErrorLog(stream) 110 | } 111 | default: 112 | cmd.Usage() 113 | os.Exit(2) 114 | } 115 | } 116 | 117 | func printAuditLog(stream *kes.AuditStream) { 118 | var ( 119 | statStyleFail = tui.NewStyle().Foreground(tui.Color("#ff0000")).Width(5) 120 | statStyleSuccess = tui.NewStyle().Foreground(tui.Color("#00ff00")).Width(5) 121 | identityStyle = tui.NewStyle().Foreground(tui.AdaptiveColor{Light: "#D1BD2E", Dark: "#C6A18C"}).MaxWidth(20).Inline(true) 122 | apiStyle = tui.NewStyle().Foreground(tui.AdaptiveColor{Light: "#2E42D1", Dark: "#2e8bc0"}).Width(30).Inline(true) 123 | ipStyle = tui.NewStyle().Width(15).Inline(true) 124 | ) 125 | const ( 126 | header = "Time Status Identity IP API Latency" 127 | format = "%02d:%02d:%02d %s %s %s %s %s\n" 128 | ) 129 | 130 | if cli.IsTerminal() { 131 | fmt.Println(tui.NewStyle().Bold(true).Underline(true).Render(header)) 132 | } else { 133 | fmt.Println(header) 134 | } 135 | for stream.Next() { 136 | event := stream.Event() 137 | var ( 138 | hour, minute, sec = event.Timestamp.Clock() 139 | status = strconv.Itoa(event.StatusCode) 140 | identity = identityStyle.Render(event.ClientIdentity.String()) 141 | apiPath = apiStyle.Render(event.APIPath) 142 | latency = event.ResponseTime 143 | ) 144 | 145 | if event.StatusCode == http.StatusOK { 146 | status = statStyleSuccess.Render(status) 147 | } else { 148 | status = statStyleFail.Render(status) 149 | } 150 | 151 | switch { 152 | case latency >= time.Second: 153 | latency = latency.Round(100 * time.Millisecond) 154 | case latency >= 10*time.Millisecond: 155 | latency = latency.Round(time.Millisecond) 156 | case latency >= time.Millisecond: 157 | latency = latency.Round(100 * time.Microsecond) 158 | case latency >= 10*time.Microsecond: 159 | latency = latency.Round(time.Microsecond) 160 | } 161 | 162 | var ipAddr string 163 | if len(event.ClientIP) == 0 { 164 | ipAddr = "" 165 | } else { 166 | ipAddr = event.ClientIP.String() 167 | } 168 | ipAddr = ipStyle.Render(ipAddr) 169 | 170 | fmt.Printf(format, hour, minute, sec, status, identity, ipAddr, apiPath, latency) 171 | } 172 | if err := stream.Close(); err != nil { 173 | if errors.Is(err, context.Canceled) { 174 | os.Exit(1) 175 | } 176 | cli.Fatal(err) 177 | } 178 | } 179 | 180 | func printErrorLog(stream *kes.ErrorStream) { 181 | for stream.Next() { 182 | fmt.Println(stream.Event().Message) 183 | } 184 | if err := stream.Close(); err != nil { 185 | if errors.Is(err, context.Canceled) { 186 | os.Exit(1) 187 | } 188 | cli.Fatal(err) 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /cmd/kes/ls.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 - MinIO, Inc. All rights reserved. 2 | // Use of this source code is governed by the AGPLv3 3 | // license that can be found in the LICENSE file. 4 | 5 | package main 6 | 7 | import ( 8 | "context" 9 | "encoding/json" 10 | "fmt" 11 | "io" 12 | "os" 13 | "os/signal" 14 | "slices" 15 | "strings" 16 | 17 | tui "github.com/charmbracelet/lipgloss" 18 | "github.com/minio/kes/internal/cli" 19 | "github.com/minio/kms-go/kes" 20 | flag "github.com/spf13/pflag" 21 | ) 22 | 23 | const lsUsage = `Usage: 24 | kes ls [-a KEY] [-k] [--json] [-i] [-p] [-s HOST[:PORT]] [PREFIX] 25 | 26 | Options: 27 | -a, --api-key KEY API key to authenticate to the KES server. 28 | Defaults to $MINIO_KES_API_KEY. 29 | -s, --server HOST[:PORT] Use the server HOST[:PORT] instead of 30 | $MINIO_KES_SERVER. 31 | --json Print output in JSON format. 32 | -i, --identity List identities. 33 | -p, --policy List policy names. 34 | -k, --insecure Skip server certificate verification. 35 | ` 36 | 37 | func ls(args []string) { 38 | var ( 39 | apiKey string 40 | skipVerify bool 41 | jsonOutput bool 42 | host string 43 | policies bool 44 | identities bool 45 | ) 46 | 47 | flags := flag.NewFlagSet(args[0], flag.ContinueOnError) 48 | flagsAPIKey(flags, &apiKey) 49 | flagsInsecureSkipVerify(flags, &skipVerify) 50 | flagsOutputJSON(flags, &jsonOutput) 51 | flagsServer(flags, &host) 52 | flags.BoolVarP(&policies, "policy", "p", false, "") 53 | flags.BoolVarP(&identities, "identity", "i", false, "") 54 | flags.Usage = func() { fmt.Fprint(os.Stderr, lsUsage) } 55 | 56 | if err := flags.Parse(args[1:]); err != nil { 57 | cli.Exit(err) 58 | } 59 | if flags.NArg() > 1 { 60 | cli.Exit("too many arguments") 61 | } 62 | if identities && policies { 63 | cli.Exit("'-p / --policy' and '-i / --identity' must not be used at the same time") 64 | } 65 | 66 | // Define functions for listing keys, identities and policies. 67 | // All a []string since we want to print the elements anyway. 68 | listKeys := func(ctx context.Context, client *kes.Client, prefix string) ([]string, error) { 69 | iter := kes.ListIter[string]{ 70 | NextFunc: client.ListKeys, 71 | } 72 | var names []string 73 | for name, err := iter.SeekTo(ctx, prefix); err != io.EOF; name, err = iter.Next(ctx) { 74 | if err != nil { 75 | return nil, err 76 | } 77 | names = append(names, name) 78 | } 79 | return names, nil 80 | } 81 | listIdentities := func(ctx context.Context, client *kes.Client, prefix string) ([]string, error) { 82 | iter := kes.ListIter[kes.Identity]{ 83 | NextFunc: client.ListIdentities, 84 | } 85 | var names []string 86 | for id, err := iter.SeekTo(ctx, prefix); err != io.EOF; id, err = iter.Next(ctx) { 87 | if err != nil { 88 | return nil, err 89 | } 90 | names = append(names, id.String()) 91 | } 92 | return names, nil 93 | } 94 | listPolicies := func(ctx context.Context, client *kes.Client, prefix string) ([]string, error) { 95 | iter := kes.ListIter[string]{ 96 | NextFunc: client.ListPolicies, 97 | } 98 | var names []string 99 | for name, err := iter.SeekTo(ctx, prefix); err != io.EOF; name, err = iter.Next(ctx) { 100 | if err != nil { 101 | return nil, err 102 | } 103 | names = append(names, name) 104 | } 105 | return names, nil 106 | } 107 | 108 | var prefix string 109 | if flags.NArg() == 1 { 110 | prefix = flags.Arg(0) 111 | } 112 | 113 | client := newClient(config{ 114 | Endpoint: host, 115 | APIKey: apiKey, 116 | InsecureSkipVerify: skipVerify, 117 | }) 118 | ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt) 119 | defer cancel() 120 | 121 | var ( 122 | names []string 123 | err error 124 | ) 125 | switch { 126 | case identities: 127 | names, err = listIdentities(ctx, client, prefix) 128 | case policies: 129 | names, err = listPolicies(ctx, client, prefix) 130 | default: 131 | names, err = listKeys(ctx, client, prefix) 132 | } 133 | if err != nil { 134 | cli.Exit(err) 135 | } 136 | slices.Sort(names) 137 | 138 | if jsonOutput { 139 | if err := json.NewEncoder(os.Stdout).Encode(names); err != nil { 140 | cli.Exit(err) 141 | } 142 | return 143 | } 144 | if len(names) == 0 { 145 | return 146 | } 147 | 148 | buf := &strings.Builder{} 149 | switch s := tui.NewStyle().Underline(true); { 150 | case identities: 151 | fmt.Fprintln(buf, s.Render("Identity")) 152 | case policies: 153 | fmt.Fprintln(buf, s.Render("Policy")) 154 | default: 155 | fmt.Fprintln(buf, s.Render("Key")) 156 | } 157 | for _, name := range names { 158 | fmt.Fprintln(buf, name) 159 | } 160 | fmt.Print(buf) 161 | } 162 | -------------------------------------------------------------------------------- /cmd/kes/metric.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 - MinIO, Inc. All rights reserved. 2 | // Use of this source code is governed by the AGPLv3 3 | // license that can be found in the LICENSE file. 4 | 5 | package main 6 | 7 | import ( 8 | "context" 9 | "encoding/json" 10 | "errors" 11 | "fmt" 12 | "os" 13 | "os/signal" 14 | "sort" 15 | "sync" 16 | "time" 17 | 18 | "aead.dev/mem" 19 | tui "github.com/charmbracelet/lipgloss" 20 | "github.com/minio/kes/internal/cli" 21 | "github.com/minio/kms-go/kes" 22 | flag "github.com/spf13/pflag" 23 | ) 24 | 25 | const metricCmdUsage = `Usage: 26 | kes metric [options] 27 | 28 | Options: 29 | --rate Scrap rate when monitoring metrics. (default: 5s) 30 | 31 | -k, --insecure Skip TLS certificate validation 32 | -h, --help Print command line options. 33 | ` 34 | 35 | func metricCmd(args []string) { 36 | cmd := flag.NewFlagSet(args[0], flag.ContinueOnError) 37 | cmd.Usage = func() { fmt.Fprint(os.Stderr, metricCmdUsage) } 38 | 39 | var ( 40 | rate time.Duration 41 | insecureSkipVerify bool 42 | ) 43 | cmd.DurationVar(&rate, "rate", 5*time.Second, "Scrap rate when monitoring metrics") 44 | cmd.BoolVarP(&insecureSkipVerify, "insecure", "k", false, "Skip TLS certificate validation") 45 | if err := cmd.Parse(args[1:]); err != nil { 46 | if errors.Is(err, flag.ErrHelp) { 47 | os.Exit(2) 48 | } 49 | cli.Fatalf("%v. See 'kes metric --help'", err) 50 | } 51 | if cmd.NArg() > 0 { 52 | cli.Fatal("too many arguments. See 'kes metric --help'") 53 | } 54 | 55 | client := newClient(config{ 56 | InsecureSkipVerify: insecureSkipVerify, 57 | }) 58 | ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, os.Kill) 59 | defer cancel() 60 | 61 | if cli.IsTerminal() { 62 | traceMetricsWithUI(ctx, client, rate) 63 | return 64 | } 65 | 66 | ticker := time.NewTicker(rate) 67 | defer ticker.Stop() 68 | 69 | encoder := json.NewEncoder(os.Stdout) 70 | for { 71 | metrics, err := client.Metrics(ctx) 72 | if err != nil { 73 | cli.Fatalf("failed to fetch metrics: %v", err) 74 | } 75 | encoder.Encode(metrics) 76 | select { 77 | case <-ticker.C: 78 | case <-ctx.Done(): 79 | return 80 | } 81 | } 82 | } 83 | 84 | // traceMetricsWithUI iterates scraps the KES metrics 85 | // and prints a table-like UI to STDOUT. 86 | func traceMetricsWithUI(ctx context.Context, client *kes.Client, rate time.Duration) { 87 | const ( 88 | EraseLine = "\033[2K" + "\033[F" + "\r" 89 | ShowCursor = "\x1b[?25h" 90 | HideCursor = "\x1b[?25l" 91 | ) 92 | var ( 93 | header = tui.NewStyle().Bold(true).Faint(true) 94 | green = tui.NewStyle().Bold(true).Foreground(tui.Color("#00fe00")) 95 | yellow = tui.NewStyle().Bold(true).Foreground(tui.Color("#fede00")) 96 | red = tui.NewStyle().Bold(true).Foreground(tui.Color("#fe0000")) 97 | ) 98 | draw := func(metric *kes.Metric, reqRate float64) { 99 | fmt.Println(header.Render("\nRequest OK [2xx] Err [4xx] Err [5xx] Req/s Latency")) 100 | fmt.Printf("%s%s%s%s%s\n", 101 | green.Render(fmt.Sprintf("%19d", metric.RequestOK)), 102 | yellow.Render(fmt.Sprintf("%16d", metric.RequestErr)), 103 | red.Render(fmt.Sprintf("%16d", metric.RequestFail)), 104 | fmt.Sprintf("%11.2f", reqRate), 105 | fmt.Sprintf("%15s", avgLatency(metric.LatencyHistogram).Round(time.Millisecond)), 106 | ) 107 | fmt.Printf("%35s%33s%33s\n\n", 108 | green.Render(fmt.Sprintf("%.2f%%", 100*float64(metric.RequestOK)/float64(metric.RequestN()))), 109 | yellow.Render(fmt.Sprintf("%.2f%%", 100*float64(metric.RequestErr)/float64(metric.RequestN()))), 110 | red.Render(fmt.Sprintf("%.2f%%", 100*float64(metric.RequestFail)/float64(metric.RequestN()))), 111 | ) 112 | fmt.Println(header.Render("System UpTime Heap Stack CPUs Threads")) 113 | fmt.Printf( 114 | "%19s%16s%16s%11d%15d\n\n", 115 | metric.UpTime, 116 | mem.FormatSize(mem.Size(metric.HeapAlloc), 'D', 1), 117 | mem.FormatSize(mem.Size(metric.StackAlloc), 'D', 1), 118 | metric.UsableCPUs, 119 | metric.Threads, 120 | ) 121 | } 122 | clearScreen := func() { 123 | fmt.Print(EraseLine, EraseLine, EraseLine, EraseLine, EraseLine, EraseLine, EraseLine, EraseLine) 124 | } 125 | 126 | var ( 127 | metric kes.Metric 128 | requestN uint64 129 | reqRate float64 130 | drawn bool 131 | ) 132 | fmt.Print(HideCursor) 133 | defer fmt.Print(ShowCursor) 134 | 135 | var wg sync.WaitGroup 136 | wg.Add(1) 137 | go func() { 138 | defer wg.Done() 139 | ticker := time.NewTicker(rate) 140 | for { 141 | var err error 142 | metric, err = client.Metrics(ctx) 143 | if err != nil { 144 | continue 145 | } 146 | 147 | // Compute the current request rate 148 | if requestN == 0 { 149 | requestN = metric.RequestN() 150 | } 151 | reqRate = float64(metric.RequestN()-requestN) / rate.Seconds() 152 | requestN = metric.RequestN() 153 | 154 | if drawn { 155 | clearScreen() 156 | } 157 | draw(&metric, reqRate) 158 | drawn = true 159 | select { 160 | case <-ctx.Done(): 161 | return 162 | case <-ticker.C: 163 | } 164 | } 165 | }() 166 | wg.Wait() 167 | } 168 | 169 | // avgLatency computes the arithmetic mean latency o 170 | func avgLatency(histogram map[time.Duration]uint64) time.Duration { 171 | latencies := make([]time.Duration, 0, len(histogram)) 172 | for l := range histogram { 173 | latencies = append(latencies, l) 174 | } 175 | sort.Slice(latencies, func(i, j int) bool { return latencies[i] < latencies[j] }) 176 | 177 | // Compute the total number of requests in the histogram 178 | var N uint64 179 | for _, l := range latencies { 180 | N += histogram[l] - N 181 | } 182 | 183 | var ( 184 | avg float64 185 | n uint64 186 | ) 187 | for _, l := range latencies { 188 | avg += float64(l) * (float64(histogram[l]-n) / float64(N)) 189 | n += histogram[l] - n 190 | } 191 | return time.Duration(avg) 192 | } 193 | -------------------------------------------------------------------------------- /cmd/kes/mlock_linux.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 - MinIO, Inc. All rights reserved. 2 | // Use of this source code is governed by the AGPLv3 3 | // license that can be found in the LICENSE file. 4 | 5 | package main 6 | 7 | import ( 8 | "syscall" 9 | 10 | "golang.org/x/sys/unix" 11 | ) 12 | 13 | func mlockall() error { return unix.Mlockall(syscall.MCL_CURRENT | syscall.MCL_FUTURE) } 14 | 15 | func munlockall() error { return unix.Munlockall() } 16 | -------------------------------------------------------------------------------- /cmd/kes/mlock_ref.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 - MinIO, Inc. All rights reserved. 2 | // Use of this source code is governed by the AGPLv3 3 | // license that can be found in the LICENSE file. 4 | 5 | //go:build !linux 6 | // +build !linux 7 | 8 | package main 9 | 10 | func mlockall() error { 11 | // We only support locking memory pages 12 | // on linux at the moment. 13 | return nil 14 | } 15 | 16 | func munlockall() error { 17 | // We only support locking memory pages 18 | // on linux at the moment. 19 | return nil 20 | } 21 | -------------------------------------------------------------------------------- /cmd/kes/status.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 - MinIO, Inc. All rights reserved. 2 | // Use of this source code is governed by the AGPLv3 3 | // license that can be found in the LICENSE file. 4 | 5 | package main 6 | 7 | import ( 8 | "context" 9 | "encoding/json" 10 | "errors" 11 | "fmt" 12 | "math" 13 | "os" 14 | "os/signal" 15 | "strconv" 16 | "strings" 17 | "time" 18 | 19 | "aead.dev/mem" 20 | tui "github.com/charmbracelet/lipgloss" 21 | "github.com/minio/kes/internal/cli" 22 | "github.com/minio/kms-go/kes" 23 | flag "github.com/spf13/pflag" 24 | ) 25 | 26 | const statusCmdUsage = `Usage: 27 | kes status [options] 28 | 29 | Options: 30 | -k, --insecure Skip TLS certificate validation. 31 | -s, --short Print status information in a short summary format. 32 | --api List all server APIs. 33 | --json Print status information in JSON format. 34 | --color Specify when to use colored output. The automatic 35 | mode only enables colors if an interactive terminal 36 | is detected - colors are automatically disabled if 37 | the output goes to a pipe. 38 | Possible values: *auto*, never, always. 39 | 40 | -h, --help Print command line options. 41 | ` 42 | 43 | func statusCmd(args []string) { 44 | cmd := flag.NewFlagSet(args[0], flag.ContinueOnError) 45 | cmd.Usage = func() { fmt.Fprint(os.Stderr, statusCmdUsage) } 46 | 47 | var ( 48 | jsonFlag bool 49 | shortFlag bool 50 | apiFlag bool 51 | colorFlag colorOption 52 | insecureSkipVerify bool 53 | ) 54 | cmd.BoolVar(&jsonFlag, "json", false, "Print status information in JSON format") 55 | cmd.BoolVar(&apiFlag, "api", false, "List all server APIs") 56 | cmd.Var(&colorFlag, "color", "Specify when to use colored output") 57 | cmd.BoolVarP(&shortFlag, "short", "s", false, "Print status information in a short summary format") 58 | cmd.BoolVarP(&insecureSkipVerify, "insecure", "k", false, "Skip TLS certificate validation") 59 | if err := cmd.Parse(args[1:]); err != nil { 60 | if errors.Is(err, flag.ErrHelp) { 61 | os.Exit(2) 62 | } 63 | cli.Fatalf("%v. See 'kes status --help'", err) 64 | } 65 | if cmd.NArg() > 0 { 66 | cli.Fatal("too many arguments. See 'kes status --help'") 67 | } 68 | 69 | client := newClient(config{ 70 | InsecureSkipVerify: insecureSkipVerify, 71 | }) 72 | ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, os.Kill) 73 | defer cancel() 74 | 75 | start := time.Now() 76 | status, err := client.Status(ctx) 77 | if err != nil { 78 | if errors.Is(err, context.Canceled) { 79 | os.Exit(1) 80 | } 81 | cli.Fatal(err) 82 | } 83 | latency := time.Since(start) 84 | 85 | var APIs []kes.API 86 | if apiFlag { 87 | APIs, err = client.APIs(ctx) 88 | if err != nil { 89 | cli.Fatal(err) 90 | } 91 | } 92 | 93 | if jsonFlag { 94 | encoder := json.NewEncoder(os.Stdout) 95 | if cli.IsTerminal() && !shortFlag { 96 | encoder.SetIndent("", " ") 97 | } 98 | if apiFlag { 99 | if err = encoder.Encode(APIs); err != nil { 100 | cli.Fatal(err) 101 | } 102 | } else { 103 | if err = encoder.Encode(status); err != nil { 104 | cli.Fatal(err) 105 | } 106 | } 107 | return 108 | } 109 | 110 | faint := tui.NewStyle() 111 | dotStyle := tui.NewStyle() 112 | endpointStyle := tui.NewStyle() 113 | if colorFlag.Colorize() { 114 | const ( 115 | ColorDot = tui.Color("#00f700") 116 | ColorEndpoint = tui.Color("#00afaf") 117 | ) 118 | faint = faint.Faint(true) 119 | dotStyle = dotStyle.Foreground(ColorDot).Bold(true) 120 | endpointStyle = endpointStyle.Foreground(ColorEndpoint).Bold(true) 121 | } 122 | 123 | fmt.Println(dotStyle.Render("●"), endpointStyle.Render(strings.TrimPrefix(client.Endpoints[0], "https://"))) 124 | if !shortFlag { 125 | fmt.Println( 126 | faint.Render(fmt.Sprintf(" %-8s", "Version")), 127 | status.Version, 128 | ) 129 | switch { 130 | case status.UpTime > 24*time.Hour: 131 | fmt.Println( 132 | faint.Render(fmt.Sprintf(" %-8s", "Uptime")), 133 | fmt.Sprintf("%.f days %.f hours", status.UpTime.Hours()/24, math.Mod(status.UpTime.Hours(), 24)), 134 | ) 135 | case status.UpTime > 1*time.Hour: 136 | fmt.Println( 137 | faint.Render(fmt.Sprintf(" %-8s", "Uptime")), 138 | fmt.Sprintf("%.f hours", status.UpTime.Hours()), 139 | ) 140 | case status.UpTime > 1*time.Minute: 141 | fmt.Println( 142 | faint.Render(fmt.Sprintf(" %-8s", "Uptime")), 143 | fmt.Sprintf("%.f minutes", status.UpTime.Minutes()), 144 | ) 145 | default: 146 | fmt.Println( 147 | faint.Render(fmt.Sprintf(" %-8s", "Uptime")), 148 | fmt.Sprintf("%.f seconds", status.UpTime.Seconds()), 149 | ) 150 | } 151 | fmt.Println( 152 | faint.Render(fmt.Sprintf(" %-8s", "Latency")), 153 | latency.Round(time.Millisecond), 154 | ) 155 | fmt.Println( 156 | faint.Render(fmt.Sprintf(" %-8s", "OS")), 157 | status.OS, 158 | ) 159 | fmt.Println( 160 | faint.Render(fmt.Sprintf(" %-8s", "CPUs")), 161 | strconv.Itoa(status.UsableCPUs), 162 | status.Arch, 163 | ) 164 | fmt.Println(faint.Render(fmt.Sprintf(" %-8s", "Memory"))) 165 | fmt.Println( 166 | faint.Render(fmt.Sprintf("%3s %-6s", "·", "Heap")), 167 | mem.FormatSize(mem.Size(status.HeapAlloc), 'D', 1), 168 | ) 169 | fmt.Println( 170 | faint.Render(fmt.Sprintf("%3s %-6s", "·", "Stack")), 171 | mem.FormatSize(mem.Size(status.StackAlloc), 'D', 1), 172 | ) 173 | } 174 | 175 | if apiFlag { 176 | header := tui.NewStyle() 177 | pathStyle := tui.NewStyle() 178 | if colorFlag.Colorize() { 179 | header = header.Faint(true).Underline(true).UnderlineSpaces(false) 180 | pathStyle = pathStyle.Foreground(tui.AdaptiveColor{Light: "#2E42D1", Dark: "#2e8bc0"}).Inline(true) 181 | } 182 | fmt.Println() 183 | fmt.Println( 184 | " ", 185 | header.Render(fmt.Sprintf("%-7s", "Method")), 186 | header.Render(fmt.Sprintf("%-28s", "API")), 187 | header.Render("Timeout"), 188 | ) 189 | 190 | for _, api := range APIs { 191 | timeout := "Inf" 192 | if api.Timeout > 0 { 193 | timeout = api.Timeout.String() 194 | } 195 | fmt.Println( 196 | " ", 197 | fmt.Sprintf("%-7s", api.Method), 198 | pathStyle.Render(fmt.Sprintf("%-28s", api.Path)), 199 | timeout, 200 | ) 201 | } 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /code_of_conduct.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior, in compliance with the 39 | licensing terms applying to the Project developments. 40 | 41 | Project maintainers have the right and responsibility to remove, edit, or 42 | reject comments, commits, code, wiki edits, issues, and other contributions 43 | that are not aligned to this Code of Conduct, or to ban temporarily or 44 | permanently any contributor for other behaviors that they deem inappropriate, 45 | threatening, offensive, or harmful. However, these actions shall respect the 46 | licensing terms of the Project Developments that will always supersede such 47 | Code of Conduct. 48 | 49 | ## Scope 50 | 51 | This Code of Conduct applies both within project spaces and in public spaces 52 | when an individual is representing the project or its community. Examples of 53 | representing a project or community include using an official project e-mail 54 | address, posting via an official social media account, or acting as an appointed 55 | representative at an online or offline event. Representation of a project may be 56 | further defined and clarified by project maintainers. 57 | 58 | ## Enforcement 59 | 60 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 61 | reported by contacting the project team at dev@min.io. The project team 62 | will review and investigate all complaints, and will respond in a way that it deems 63 | appropriate to the circumstances. The project team is obligated to maintain 64 | confidentiality with regard to the reporter of an incident. 65 | Further details of specific enforcement policies may be posted separately. 66 | 67 | Project maintainers who do not follow or enforce the Code of Conduct in good 68 | faith may face temporary or permanent repercussions as determined by other 69 | members of the project's leadership. 70 | 71 | ## Attribution 72 | 73 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 74 | available at [http://contributor-covenant.org/version/1/4][version] 75 | 76 | This version includes a clarification to ensure that the code of conduct is in 77 | compliance with the free software licensing terms of the project. 78 | 79 | [homepage]: http://contributor-covenant.org 80 | [version]: http://contributor-covenant.org/version/1/4/ 81 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 - MinIO, Inc. All rights reserved. 2 | // Use of this source code is governed by the AGPLv3 3 | // license that can be found in the LICENSE file. 4 | 5 | package kes 6 | 7 | import ( 8 | "crypto/tls" 9 | "errors" 10 | "log/slog" 11 | "time" 12 | 13 | "github.com/minio/kms-go/kes" 14 | ) 15 | 16 | // Config is a structure that holds configuration for a KES server. 17 | type Config struct { 18 | // Admin is the KES server admin identity. It must not be empty. 19 | // To disable admin access set it to a non-hex value. For example, 20 | // "disabled". 21 | Admin kes.Identity 22 | 23 | // TLS contains the KES server's TLS configuration. 24 | // 25 | // A KES server requires a TLS certificate. Therefore, either 26 | // Config.Certificates, Config.GetCertificate or 27 | // Config.GetConfigForClient must be set. 28 | // 29 | // Further, the KES server has to request client certificates 30 | // for mTLS authentication. Hence, Config.ClientAuth must be 31 | // at least tls.RequestClientCert. 32 | TLS *tls.Config 33 | 34 | // Cache specifies how long the KES server caches keys from the 35 | // KeyStore. If nil, caching is disabled. 36 | Cache *CacheConfig 37 | 38 | // Policies is a set of policies and identities. Each identity 39 | // must be assigned to a policy only once. 40 | Policies map[string]Policy 41 | 42 | // Predefined Keys to be added to KeyStore before server starts. 43 | PredefinedKeys []Key 44 | 45 | // Keys is the KeyStore the KES server fetches keys from. 46 | Keys KeyStore 47 | 48 | // Routes allows customization of the KES server API routes. It 49 | // contains a set of API route paths, for example "/v1/status", 50 | // and the corresponding route configuration. 51 | // 52 | // The KES server uses sane defaults for all its API routes. 53 | Routes map[string]RouteConfig 54 | 55 | // ErrorLog is an optional handler for handling the server's 56 | // error log events. If nil, defaults to a slog.TextHandler 57 | // writing to os.Stderr. The server's error log level is 58 | // controlled by Server.ErrLevel. 59 | ErrorLog slog.Handler 60 | 61 | // AuditLog is an optional handler for handling the server's 62 | // audit log events. If nil, defaults to a slog.TextHandler 63 | // writing to os.Stdout. The server's audit log level is 64 | // controlled by Server.AuditLevel. 65 | AuditLog AuditHandler 66 | } 67 | 68 | // Policy is a KES policy with associated identities. 69 | // 70 | // A policy contains a set of allow and deny rules. 71 | type Policy struct { 72 | Allow map[string]kes.Rule // Set of allow rules 73 | 74 | Deny map[string]kes.Rule // Set of deny rules 75 | 76 | Identities []kes.Identity 77 | } 78 | 79 | // CacheConfig is a structure containing the KES server 80 | // key store cache configuration. 81 | type CacheConfig struct { 82 | // Expiry controls how long a particular key resides 83 | // in the cache. If zero or negative, keys remain in 84 | // the cache as long as the KES server has sufficient 85 | // memory. 86 | Expiry time.Duration 87 | 88 | // ExpiryUnused is the interval in which a particular 89 | // key must be accessed to remain in the cache. Keys 90 | // that haven't been accessed get evicted from the 91 | // cache. The general cache expiry still applies. 92 | // 93 | // ExpiryUnused does nothing if <= 0 or greater than 94 | // Expiry. 95 | ExpiryUnused time.Duration 96 | 97 | // ExpiryOffline controls how long a particular key 98 | // resides in the cache once the key store becomes 99 | // unavailable. It overwrites Expiry and ExpiryUnused 100 | // if the key store is not available. Once the key 101 | // store is available again, Expiry and ExpiryUnused, 102 | // if set, apply. 103 | // 104 | // A common use of ExpiryOffline is reducing the impact 105 | // of a key store outage, and therefore, improving 106 | // availability. 107 | // 108 | // Offline caching is disabled if ExpiryOffline <= 0. 109 | ExpiryOffline time.Duration 110 | } 111 | 112 | // RouteConfig is a structure holding API route configuration. 113 | type RouteConfig struct { 114 | // Timeout specifies when the API handler times out. 115 | // 116 | // A handler times out when it fails to send the 117 | // *entire* response body to the client within the 118 | // given time period. 119 | // 120 | // If <= 0, timeouts are disabled for the API route. 121 | // 122 | // Disabling timeouts may leave client/server connections 123 | // hung or allow certain types of denial-of-service (DOS) 124 | // attacks. 125 | Timeout time.Duration 126 | 127 | // InsecureSkipAuth, if set, disables authentication for the 128 | // API route. It allows anyone that can send HTTPS requests 129 | // to the KES server to invoke the API. 130 | // 131 | // For example the KES readiness API authentication may be 132 | // disabled when the probing clients do not support mTLS 133 | // client authentication 134 | // 135 | // If setting InsecureSkipAuth for any API then clients that 136 | // do not send a client certificate during the TLS handshake 137 | // no longer encounter a TLS handshake error but receive a 138 | // HTTP error instead. In particular, when the server's TLS 139 | // client auth type has been set tls.RequireAnyClientCert 140 | // or tls.RequireAndVerifyClientCert. 141 | InsecureSkipAuth bool 142 | } 143 | 144 | // verifyConfig reports whether the c is a valid Config 145 | // and contains at least a TLS certificate for the server 146 | // and a key store. 147 | func verifyConfig(c *Config) error { 148 | if c == nil || c.TLS == nil || (len(c.TLS.Certificates) == 0 && c.TLS.GetCertificate == nil && c.TLS.GetConfigForClient == nil) { 149 | return errors.New("kes: tls config contains no server certificate") 150 | } 151 | if c.TLS.ClientAuth == tls.NoClientCert { 152 | return errors.New("kes: tls client auth must request client certificate") 153 | } 154 | if c.Keys == nil { 155 | return errors.New("kes: config contains no key store") 156 | } 157 | return nil 158 | } 159 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 - MinIO, Inc. All rights reserved. 2 | // Use of this source code is governed by the AGPLv3 3 | // license that can be found in the LICENSE file. 4 | 5 | package kes_test 6 | 7 | import ( 8 | "context" 9 | "log/slog" 10 | "net/http" 11 | "net/netip" 12 | "os" 13 | "time" 14 | 15 | "github.com/minio/kes" 16 | ) 17 | 18 | // This example shows how to connect an AuditHandler 19 | // to any slog.Handler, here an TextHandler writing 20 | // to stdout. 21 | func ExampleAuditLogHandler() { 22 | audit := &kes.AuditLogHandler{ 23 | Handler: slog.NewTextHandler(os.Stdout, nil), 24 | } 25 | conf := &kes.Config{ 26 | AuditLog: audit, 27 | } 28 | _ = conf 29 | 30 | // Handle will be called by the KES server internally 31 | audit.Handle(context.Background(), kes.AuditRecord{ 32 | Time: time.Date(2023, time.October, 19, 8, 44, 0, 0, time.UTC), 33 | Method: http.MethodPut, 34 | Path: "/v1/key/create/my-key", 35 | Identity: "2ecb8804e7702a6b768e89b7bba5933044c9d071e4f4035235269b919e56e691", 36 | RemoteIP: netip.MustParseAddr("10.1.2.3"), 37 | StatusCode: http.StatusOK, 38 | ResponseTime: 200 * time.Millisecond, 39 | Level: slog.LevelInfo, 40 | Message: "secret key 'my-key' created", 41 | }) 42 | // Output: 43 | // time=2023-10-19T08:44:00.000Z level=INFO msg="secret key 'my-key' created" req.method=PUT req.path=/v1/key/create/my-key req.ip=10.1.2.3 req.identity=2ecb8804e7702a6b768e89b7bba5933044c9d071e4f4035235269b919e56e691 res.code=200 res.time=200ms 44 | } 45 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/minio/kes 2 | 3 | go 1.24.0 4 | 5 | toolchain go1.24.1 6 | 7 | require ( 8 | aead.dev/mem v0.2.0 9 | aead.dev/minisign v0.3.0 10 | cloud.google.com/go/secretmanager v1.14.3 11 | github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0 12 | github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.1 13 | github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.3.0 14 | github.com/aws/aws-sdk-go v1.55.6 15 | github.com/charmbracelet/lipgloss v1.0.0 16 | github.com/hashicorp/vault/api v1.15.0 17 | github.com/minio/kms-go/kes v0.3.1 18 | github.com/minio/kms-go/kms v0.5.0 19 | github.com/minio/selfupdate v0.6.0 20 | github.com/muesli/termenv v0.15.2 21 | github.com/prometheus/client_golang v1.20.5 22 | github.com/prometheus/common v0.62.0 23 | github.com/spf13/pflag v1.0.6 24 | github.com/tinylib/msgp v1.2.5 25 | golang.org/x/crypto v0.35.0 26 | golang.org/x/sys v0.30.0 27 | golang.org/x/term v0.29.0 28 | google.golang.org/api v0.219.0 29 | google.golang.org/grpc v1.70.0 30 | google.golang.org/protobuf v1.36.4 31 | gopkg.in/yaml.v3 v3.0.1 32 | ) 33 | 34 | require ( 35 | cloud.google.com/go/auth v0.14.1 // indirect 36 | cloud.google.com/go/auth/oauth2adapt v0.2.7 // indirect 37 | cloud.google.com/go/compute/metadata v0.6.0 // indirect 38 | cloud.google.com/go/iam v1.3.1 // indirect 39 | github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect 40 | github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.0 // indirect 41 | github.com/AzureAD/microsoft-authentication-library-for-go v1.3.3 // indirect 42 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect 43 | github.com/beorn7/perks v1.0.1 // indirect 44 | github.com/cenkalti/backoff/v4 v4.3.0 // indirect 45 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 46 | github.com/charmbracelet/x/ansi v0.8.0 // indirect 47 | github.com/felixge/httpsnoop v1.0.4 // indirect 48 | github.com/go-jose/go-jose/v4 v4.0.5 // indirect 49 | github.com/go-logr/logr v1.4.2 // indirect 50 | github.com/go-logr/stdr v1.2.2 // indirect 51 | github.com/golang-jwt/jwt/v5 v5.2.1 // indirect 52 | github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect 53 | github.com/google/s2a-go v0.1.9 // indirect 54 | github.com/google/uuid v1.6.0 // indirect 55 | github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect 56 | github.com/googleapis/gax-go/v2 v2.14.1 // indirect 57 | github.com/hashicorp/errwrap v1.1.0 // indirect 58 | github.com/hashicorp/go-cleanhttp v0.5.2 // indirect 59 | github.com/hashicorp/go-multierror v1.1.1 // indirect 60 | github.com/hashicorp/go-retryablehttp v0.7.7 // indirect 61 | github.com/hashicorp/go-rootcerts v1.0.2 // indirect 62 | github.com/hashicorp/go-secure-stdlib/parseutil v0.1.9 // indirect 63 | github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect 64 | github.com/hashicorp/go-sockaddr v1.0.7 // indirect 65 | github.com/hashicorp/hcl v1.0.0 // indirect 66 | github.com/jmespath/go-jmespath v0.4.0 // indirect 67 | github.com/kylelemons/godebug v1.1.0 // indirect 68 | github.com/lucasb-eyer/go-colorful v1.2.0 // indirect 69 | github.com/mattn/go-isatty v0.0.20 // indirect 70 | github.com/mattn/go-runewidth v0.0.16 // indirect 71 | github.com/mitchellh/go-homedir v1.1.0 // indirect 72 | github.com/mitchellh/mapstructure v1.5.0 // indirect 73 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 74 | github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect 75 | github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect 76 | github.com/prometheus/client_model v0.6.1 // indirect 77 | github.com/prometheus/procfs v0.15.1 // indirect 78 | github.com/rivo/uniseg v0.4.7 // indirect 79 | github.com/ryanuber/go-glob v1.0.0 // indirect 80 | go.opentelemetry.io/auto/sdk v1.1.0 // indirect 81 | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 // indirect 82 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 // indirect 83 | go.opentelemetry.io/otel v1.34.0 // indirect 84 | go.opentelemetry.io/otel/metric v1.34.0 // indirect 85 | go.opentelemetry.io/otel/trace v1.34.0 // indirect 86 | golang.org/x/net v0.34.0 // indirect 87 | golang.org/x/oauth2 v0.27.0 // indirect 88 | golang.org/x/sync v0.11.0 // indirect 89 | golang.org/x/text v0.22.0 // indirect 90 | golang.org/x/time v0.9.0 // indirect 91 | google.golang.org/genproto v0.0.0-20250127172529-29210b9bc287 // indirect 92 | google.golang.org/genproto/googleapis/api v0.0.0-20250127172529-29210b9bc287 // indirect 93 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250127172529-29210b9bc287 // indirect 94 | ) 95 | -------------------------------------------------------------------------------- /internal/api/error.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 - MinIO, Inc. All rights reserved. 2 | // Use of this source code is governed by the AGPLv3 3 | // license that can be found in the LICENSE file. 4 | 5 | package api 6 | 7 | import ( 8 | "bytes" 9 | "encoding/json" 10 | "fmt" 11 | "io" 12 | "net/http" 13 | "strconv" 14 | "strings" 15 | 16 | "aead.dev/mem" 17 | "github.com/minio/kes/internal/headers" 18 | ) 19 | 20 | // Failr responds to the client with err. The response 21 | // status code is set to err.Status. The error encoding 22 | // format is selected automatically based on the response 23 | // content type. Handlers should return after calling Failr. 24 | func Failr(r *Response, err Error) error { 25 | return Fail(r, err.Status(), err.Error()) 26 | } 27 | 28 | // Failf responds to the client with the given status code 29 | // and formatted error message. The message encoding format 30 | // is selected automatically based on the response content 31 | // type. Handlers should return after calling Failf. 32 | func Failf(r *Response, code int, format string, a ...any) error { 33 | return Fail(r, code, fmt.Sprintf(format, a...)) 34 | } 35 | 36 | // Fail responds to the client with the given status code 37 | // and error message. The message encoding format is selected 38 | // automatically based on the response content type. Handlers 39 | // should return after calling Fail. 40 | func Fail(r *Response, code int, msg string) error { 41 | var buf bytes.Buffer 42 | buf.WriteString(`{"message":`) 43 | if err := json.NewEncoder(&buf).Encode(msg); err != nil { 44 | return err 45 | } 46 | buf.WriteByte('}') 47 | 48 | r.Header().Set(headers.ContentType, headers.ContentTypeJSON) 49 | r.Header().Set(headers.ContentLength, strconv.Itoa(buf.Len())) 50 | r.WriteHeader(code) 51 | _, err := r.Write(buf.Bytes()) 52 | return err 53 | } 54 | 55 | // Error is an API error. 56 | // 57 | // Status codes should be within 400 (inclusive) and 600 (exclusive). 58 | // HTTP clients treat status codes between 400 and 499 as client 59 | // errors and status codes between 500 and 599 as server errors. 60 | // 61 | // Refer to the net/http package for a list of HTTP status codes. 62 | type Error interface { 63 | error 64 | 65 | // Status returns the Error's HTTP status code. 66 | Status() int 67 | } 68 | 69 | // NewError returns a new Error from the given status code 70 | // and error message. 71 | func NewError(code int, msg string) Error { 72 | return &codeError{ 73 | code: code, 74 | msg: msg, 75 | } 76 | } 77 | 78 | // IsError reports whether any error in err's tree is an 79 | // Error. It returns the first error that implements Error, 80 | // if any. 81 | // 82 | // The tree consists of err itself, followed by the errors 83 | // obtained by repeatedly unwrapping the error. When err 84 | // wraps multiple errors, IsError examines err followed by 85 | // a depth-first traversal of its children. 86 | func IsError(err error) (Error, bool) { 87 | if err == nil { 88 | return nil, false 89 | } 90 | 91 | for { 92 | switch e := err.(type) { 93 | case Error: 94 | return e, true 95 | case interface{ Unwrap() error }: 96 | if err = e.Unwrap(); err == nil { 97 | return nil, false 98 | } 99 | case interface{ Unwrap() []error }: 100 | for _, err := range e.Unwrap() { 101 | if err, ok := IsError(err); ok { 102 | return err, true 103 | } 104 | } 105 | return nil, false 106 | default: 107 | return nil, false 108 | } 109 | } 110 | } 111 | 112 | // ReadError reads the response body into an Error using 113 | // the response content encoding. It limits the response 114 | // body to a reasonable size for typical error messages. 115 | func ReadError(resp *http.Response) Error { 116 | const MaxSize = 5 * mem.KB // An error message should not exceed 5 KB. 117 | 118 | msg, err := readErrorMessage(resp, MaxSize) 119 | if err != nil { 120 | return NewError(resp.StatusCode, err.Error()) 121 | } 122 | return NewError(resp.StatusCode, msg) 123 | } 124 | 125 | func readErrorMessage(resp *http.Response, maxSize mem.Size) (string, error) { 126 | size := mem.Size(resp.ContentLength) 127 | if size <= 0 || size > maxSize { 128 | size = maxSize 129 | } 130 | body := mem.LimitReader(resp.Body, size) 131 | 132 | switch resp.Header.Get(headers.ContentType) { 133 | case headers.ContentTypeHTML, headers.ContentTypeText: 134 | var sb strings.Builder 135 | if _, err := io.Copy(&sb, body); err != nil { 136 | return "", err 137 | } 138 | return sb.String(), nil 139 | default: 140 | type ErrResponse struct { 141 | Message string `json:"error"` 142 | } 143 | var response ErrResponse 144 | if err := json.NewDecoder(body).Decode(&response); err != nil { 145 | return "", err 146 | } 147 | return response.Message, nil 148 | } 149 | } 150 | 151 | type codeError struct { 152 | code int 153 | msg string 154 | } 155 | 156 | func (e *codeError) Error() string { return e.msg } 157 | 158 | func (e *codeError) Status() int { return e.code } 159 | -------------------------------------------------------------------------------- /internal/api/multicast.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 - MinIO, Inc. All rights reserved. 2 | // Use of this source code is governed by the AGPLv3 3 | // license that can be found in the LICENSE file. 4 | 5 | package api 6 | 7 | import ( 8 | "encoding/json" 9 | "io" 10 | "net/http" 11 | "slices" 12 | "sync/atomic" 13 | ) 14 | 15 | // Multicast is a one-to-many io.Writer. It is similar 16 | // to io.MultiWriter but writers can be added and removed 17 | // dynamically. A Multicast may be modified by multiple 18 | // go routines concurrently. 19 | // 20 | // Its zero value is an empty group of io.Writers and 21 | // ready for use. 22 | type Multicast struct { 23 | group atomic.Pointer[[]io.Writer] 24 | } 25 | 26 | // Num returns how many connections are part of this Multicast. 27 | func (m *Multicast) Num() int { 28 | if p := m.group.Load(); p != nil { 29 | return len(*p) 30 | } 31 | return 0 32 | } 33 | 34 | // Add adds w to m. Future writes to m will also reach w. 35 | // If w is already part of m, Add does nothing. 36 | func (m *Multicast) Add(w io.Writer) { 37 | if w == nil { 38 | return 39 | } 40 | 41 | for { 42 | old := m.group.Load() 43 | if old == nil && m.group.CompareAndSwap(nil, &[]io.Writer{w}) { 44 | return 45 | } 46 | if slices.Contains(*old, w) { // avoid adding an io.Writer twice 47 | return 48 | } 49 | 50 | group := make([]io.Writer, 0, len(*old)+1) 51 | group = append(group, w) 52 | group = append(group, *old...) 53 | if m.group.CompareAndSwap(old, &group) { 54 | return 55 | } 56 | } 57 | } 58 | 59 | // Remove removes w from m. Future writes to m will no longer 60 | // reach w. 61 | func (m *Multicast) Remove(w io.Writer) { 62 | if w == nil { 63 | return 64 | } 65 | 66 | for { 67 | old := m.group.Load() 68 | if old == nil || len(*old) == 0 || !slices.Contains(*old, w) { 69 | return 70 | } 71 | 72 | group := make([]io.Writer, 0, len(*old)-1) 73 | for _, wr := range *old { 74 | if wr != w { 75 | group = append(group, wr) 76 | } 77 | } 78 | if m.group.CompareAndSwap(old, &group) { 79 | return 80 | } 81 | } 82 | } 83 | 84 | // Write writes p to all io.Writers that are currently part of m. 85 | // It returns the first error encountered, if any, but writes to 86 | // all io.Writers before returning. 87 | func (m *Multicast) Write(p []byte) (n int, err error) { 88 | ptr := m.group.Load() 89 | if ptr == nil { 90 | return 0, nil 91 | } 92 | group := *ptr 93 | if len(group) == 0 { 94 | return 0, nil 95 | } 96 | 97 | for _, w := range group { 98 | nn, wErr := w.Write(p) 99 | if wErr == nil && nn < len(p) { 100 | wErr = io.ErrShortWrite 101 | } 102 | if err == nil && wErr != nil { 103 | err = wErr 104 | n = nn 105 | } 106 | } 107 | if n == 0 && err == nil { 108 | n = len(p) 109 | } 110 | return n, err 111 | } 112 | 113 | // LogWriter wraps an io.Writer and encodes each 114 | // write operation as ErrorLogEvent. 115 | // 116 | // It's intended to be used as adapter to send 117 | // API error logs to an http.ResponseWriter. 118 | type LogWriter struct { 119 | encoder *json.Encoder 120 | flusher http.Flusher 121 | } 122 | 123 | // NewLogWriter returns a new LogWriter wrapping w. 124 | func NewLogWriter(w io.Writer) *LogWriter { 125 | flusher, _ := w.(http.Flusher) 126 | return &LogWriter{ 127 | encoder: json.NewEncoder(w), 128 | flusher: flusher, 129 | } 130 | } 131 | 132 | // Write encodes p as ErrorLogEvent and 133 | // writes it to the underlying io.Writer. 134 | func (w *LogWriter) Write(p []byte) (int, error) { 135 | if len(p) == 0 { 136 | return 0, nil 137 | } 138 | 139 | n := len(p) 140 | if p[n-1] == '\n' { // Remove trailing newline added by logger 141 | p = p[:n-1] 142 | } 143 | 144 | if err := w.encoder.Encode(ErrorLogEvent{ 145 | Message: string(p), 146 | }); err != nil { 147 | return 0, err 148 | } 149 | if w.flusher != nil { 150 | w.flusher.Flush() 151 | } 152 | return n, nil 153 | } 154 | -------------------------------------------------------------------------------- /internal/api/request.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 - MinIO, Inc. All rights reserved. 2 | // Use of this source code is governed by the AGPLv3 3 | // license that can be found in the LICENSE file. 4 | 5 | package api 6 | 7 | // ImportKeyRequest is the request sent by clients when calling the ImportKey API. 8 | type ImportKeyRequest struct { 9 | Bytes []byte `json:"key"` 10 | Cipher string `json:"cipher"` 11 | } 12 | 13 | // EncryptKeyRequest is the request sent by clients when calling the EncryptKey API. 14 | type EncryptKeyRequest struct { 15 | Plaintext []byte `json:"plaintext"` 16 | Context []byte `json:"context"` // optional 17 | Version string `json:"version"` // optional 18 | } 19 | 20 | // GenerateKeyRequest is the request sent by clients when calling the GenerateKey API. 21 | type GenerateKeyRequest struct { 22 | Context []byte `json:"context"` // optional 23 | Version string `json:"version"` // optional 24 | } 25 | 26 | // DecryptKeyRequest is the request sent by clients when calling the DecryptKey API. 27 | type DecryptKeyRequest struct { 28 | Ciphertext []byte `json:"ciphertext"` 29 | Context []byte `json:"context"` // optional 30 | Version string `json:"version"` // optional 31 | } 32 | 33 | // HMACRequest is the request sent by clients when calling the HMAC API. 34 | type HMACRequest struct { 35 | Message []byte `json:"message"` 36 | Version string `json:"version"` // optional 37 | } 38 | -------------------------------------------------------------------------------- /internal/api/response.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 - MinIO, Inc. All rights reserved. 2 | // Use of this source code is governed by the AGPLv3 3 | // license that can be found in the LICENSE file. 4 | 5 | package api 6 | 7 | import ( 8 | "time" 9 | ) 10 | 11 | // VersionResponse is the response sent to clients by the Version API. 12 | type VersionResponse struct { 13 | Version string `json:"version"` 14 | Commit string `json:"commit"` 15 | } 16 | 17 | // StatusResponse is the response sent to clients by the Status API. 18 | type StatusResponse struct { 19 | Version string `json:"version"` 20 | OS string `json:"os"` 21 | Arch string `json:"arch"` 22 | UpTime uint64 `json:"uptime"` // in seconds 23 | CPUs int `json:"num_cpu"` 24 | UsableCPUs int `json:"num_cpu_used"` 25 | HeapAlloc uint64 `json:"mem_heap_used"` 26 | StackAlloc uint64 `json:"mem_stack_used"` 27 | 28 | KeyStoreLatency int64 `json:"keystore_latency,omitempty"` // In microseconds 29 | KeyStoreUnreachable bool `json:"keystore_unreachable,omitempty"` 30 | } 31 | 32 | // DescribeRouteResponse describes a single API route. It is part of 33 | // a List API response. 34 | type DescribeRouteResponse struct { 35 | Method string `json:"method"` 36 | Path string `json:"path"` 37 | MaxBody int64 `json:"max_body"` 38 | Timeout int64 `json:"timeout"` // in seconds 39 | } 40 | 41 | // ListAPIsResponse is the response sent to clients by the List APIs API. 42 | type ListAPIsResponse []DescribeRouteResponse 43 | 44 | // DescribeKeyResponse is the response sent to clients by the DescribeKey API. 45 | type DescribeKeyResponse struct { 46 | Name string `json:"name"` 47 | Algorithm string `json:"algorithm,omitempty"` 48 | CreatedAt time.Time `json:"created_at,omitempty"` 49 | CreatedBy string `json:"created_by,omitempty"` 50 | } 51 | 52 | // ListKeysResponse is the response sent to clients by the ListKeys API. 53 | type ListKeysResponse struct { 54 | Names []string `json:"names"` 55 | ContinueAt string `json:"continue_at,omitempty"` 56 | } 57 | 58 | // EncryptKeyResponse is the response sent to clients by the EncryptKey API. 59 | type EncryptKeyResponse struct { 60 | Ciphertext []byte `json:"ciphertext"` 61 | Version string `json:"version,omitempty"` 62 | } 63 | 64 | // GenerateKeyResponse is the response sent to clients by the GenerateKey API. 65 | type GenerateKeyResponse struct { 66 | Plaintext []byte `json:"plaintext"` 67 | Ciphertext []byte `json:"ciphertext"` 68 | Version string `json:"version,omitempty"` 69 | } 70 | 71 | // DecryptKeyResponse is the response sent to clients by the DecryptKey API. 72 | type DecryptKeyResponse struct { 73 | Plaintext []byte `json:"plaintext"` 74 | } 75 | 76 | // HMACResponse is the response sent to clients by the HMAC API. 77 | type HMACResponse struct { 78 | Sum []byte `json:"hmac"` 79 | Version string `json:"version,omitempty"` 80 | } 81 | 82 | // ReadPolicyResponse is the response sent to clients by the ReadPolicy API. 83 | type ReadPolicyResponse struct { 84 | Name string `json:"name"` 85 | Allow map[string]struct{} `json:"allow,omitempty"` 86 | Deny map[string]struct{} `json:"deny,omitempty"` 87 | CreatedAt time.Time `json:"created_at"` 88 | CreatedBy string `json:"created_by"` 89 | } 90 | 91 | // DescribePolicyResponse is the response sent to clients by the DescribePolicy API. 92 | type DescribePolicyResponse struct { 93 | Name string `json:"name"` 94 | CreatedAt time.Time `json:"created_at"` 95 | CreatedBy string `json:"created_by"` 96 | } 97 | 98 | // ListPoliciesResponse is the response sent to clients by the ListPolicies API. 99 | type ListPoliciesResponse struct { 100 | Names []string `json:"names"` 101 | ContinueAt string `json:"continue_at"` 102 | } 103 | 104 | // DescribeIdentityResponse is the response sent to clients by the DescribeIdentity API. 105 | type DescribeIdentityResponse struct { 106 | IsAdmin bool `json:"admin,omitempty"` 107 | Policy string `json:"policy,omitempty"` 108 | CreatedAt time.Time `json:"created_at"` 109 | CreatedBy string `json:"created_by,omitempty"` 110 | } 111 | 112 | // ListIdentitiesResponse is the response sent to clients by the ListIdentities API. 113 | type ListIdentitiesResponse struct { 114 | Identities []string `json:"identities"` 115 | ContinueAt string `json:"continue_at"` 116 | } 117 | 118 | // SelfDescribeIdentityResponse is the response sent to clients by the SelfDescribeIdentity API. 119 | type SelfDescribeIdentityResponse struct { 120 | Identity string `json:"identity"` 121 | IsAdmin bool `json:"admin,omitempty"` 122 | CreatedAt time.Time `json:"created_at"` 123 | CreatedBy string `json:"created_by,omitempty"` 124 | 125 | Policy *ReadPolicyResponse `json:"policy,omitempty"` 126 | } 127 | 128 | // AuditLogEvent is sent to clients (as stream of events) when they subscribe to the AuditLog API. 129 | type AuditLogEvent struct { 130 | Time time.Time `json:"time"` 131 | Request AuditLogRequest `json:"request"` 132 | Response AuditLogResponse `json:"response"` 133 | } 134 | 135 | // AuditLogRequest describes a client request in an AuditLogEvent. 136 | type AuditLogRequest struct { 137 | IP string `json:"ip,omitempty"` 138 | APIPath string `json:"path"` 139 | Identity string `json:"identity,omitempty"` 140 | } 141 | 142 | // AuditLogResponse describes a server response in an AuditLogEvent. 143 | type AuditLogResponse struct { 144 | StatusCode int `json:"code"` 145 | Time int64 `json:"time"` // In microseconds 146 | } 147 | 148 | // ErrorLogEvent is sent to clients (as stream of events) when they subscribe to the ErrorLog API. 149 | type ErrorLogEvent struct { 150 | Message string `json:"message"` 151 | } 152 | -------------------------------------------------------------------------------- /internal/cache/barrier.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 - MinIO, Inc. All rights reserved. 2 | // Use of this source code is governed by the AGPLv3 3 | // license that can be found in the LICENSE file. 4 | 5 | package cache 6 | 7 | import ( 8 | "sync" 9 | ) 10 | 11 | // A Barrier is a mutual exclusion lock per key K. 12 | // 13 | // The zero value for a Barrier is an unlocked mutex. 14 | // 15 | // A Barrier must not be copied after first use. 16 | type Barrier[K comparable] struct { 17 | mu sync.Mutex 18 | keys map[K]*barrier 19 | } 20 | 21 | // Lock locks the key. 22 | // 23 | // If the key is already in use, the calling goroutine 24 | // blocks until the key is available. 25 | func (b *Barrier[K]) Lock(key K) { b.add(key).Lock() } 26 | 27 | // Unlock unlocks the key. 28 | // It is a run-time error if the key is not locked on entry 29 | // to Unlock. 30 | // 31 | // A Barrier is not associated with a particular goroutine. 32 | // It is allowed for one goroutine to lock one Barrier key 33 | // and then arrange for another goroutine to unlock this key. 34 | func (b *Barrier[K]) Unlock(key K) { b.remove(key).Unlock() } 35 | 36 | // add adds a new barrier for the given key, if non exist, 37 | // or returns the existing barrier. 38 | func (b *Barrier[K]) add(key K) *barrier { 39 | b.mu.Lock() 40 | defer b.mu.Unlock() 41 | 42 | m, ok := b.keys[key] 43 | if !ok { 44 | if b.keys == nil { 45 | b.keys = make(map[K]*barrier) 46 | } 47 | m = new(barrier) 48 | b.keys[key] = m 49 | } 50 | 51 | m.N++ 52 | return m 53 | } 54 | 55 | func (b *Barrier[K]) remove(key K) *barrier { 56 | b.mu.Lock() 57 | defer b.mu.Unlock() 58 | 59 | m, ok := b.keys[key] 60 | if !ok { 61 | // This is the equivalent of calling Unlock 62 | // on a unlocked sync.Mutex. 63 | panic("cache: unlock of unlocked Barrier key") 64 | } 65 | m.N-- 66 | 67 | if m.N == 0 { 68 | // Once N reaches 0, the barrier can be removed 69 | // since no goroutine is trying to acquire a lock 70 | // for this key. 71 | delete(b.keys, key) 72 | } 73 | return m 74 | } 75 | 76 | type barrier struct { 77 | sync.Mutex 78 | 79 | // N is the number of goroutines that have 80 | // acquired / are trying to acquire the lock. 81 | N uint 82 | } 83 | -------------------------------------------------------------------------------- /internal/cache/barrier_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 - MinIO, Inc. All rights reserved. 2 | // Use of this source code is governed by the AGPLv3 3 | // license that can be found in the LICENSE file. 4 | 5 | package cache 6 | 7 | import ( 8 | "runtime" 9 | "sync" 10 | "sync/atomic" 11 | "testing" 12 | "time" 13 | ) 14 | 15 | func TestBarrierZeroValue(t *testing.T) { 16 | t.Run("Lock", func(*testing.T) { 17 | var b Barrier[int] 18 | b.Lock(0) 19 | b.Unlock(0) 20 | }) 21 | t.Run("Unlock", func(t *testing.T) { 22 | defer func() { 23 | const Msg = "cache: unlock of unlocked Barrier key" 24 | 25 | switch err := recover(); { 26 | case err == nil: 27 | t.Fatal("Unlock should have panic'ed") 28 | case err != Msg: 29 | t.Fatalf("Panic should be '%v' - got '%v'", Msg, err) 30 | } 31 | }() 32 | 33 | var b Barrier[int] 34 | b.Lock(0) 35 | b.Unlock(1) // Panic 36 | }) 37 | } 38 | 39 | func TestBarrierLock(t *testing.T) { 40 | const N = 3 41 | var ( 42 | b Barrier[int] 43 | ctr [N]atomic.Uint32 44 | wg sync.WaitGroup 45 | ) 46 | for i := 0; i < 100; i++ { 47 | wg.Add(1) 48 | go func(i int) { 49 | runtime.Gosched() // make a potential race condition more likely 50 | 51 | defer wg.Done() 52 | 53 | b.Lock(i % N) 54 | defer b.Unlock(i % N) 55 | 56 | defer ctr[i%N].Add(^uint32(0)) // ctr = ctr - 1 | Ref: sync/atomic docs 57 | 58 | if ctr[i%N].Load() != 0 { 59 | t.Errorf("Concurrent access to counter detected: Barrier allows concurrent access to %d", i) 60 | } 61 | ctr[i%N].Add(1) 62 | time.Sleep(10 * time.Microsecond) // make a potential race condition more likely 63 | }(i) 64 | } 65 | wg.Wait() 66 | } 67 | -------------------------------------------------------------------------------- /internal/cache/cow.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 - MinIO, Inc. All rights reserved. 2 | // Use of this source code is governed by the AGPLv3 3 | // license that can be found in the LICENSE file. 4 | 5 | package cache 6 | 7 | import ( 8 | "sync" 9 | "sync/atomic" 10 | ) 11 | 12 | // NewCow returns a new copy-on-write cache with the 13 | // given capacity that can hold at most N entries at 14 | // the same time; N being the capacity. 15 | func NewCow[K comparable, V any](capacity int) *Cow[K, V] { 16 | c := &Cow[K, V]{ 17 | ptr: atomic.Pointer[map[K]V]{}, 18 | capacity: capacity, 19 | } 20 | c.ptr.Store(&map[K]V{}) 21 | return c 22 | } 23 | 24 | // Cow is a copy-on-write cache. 25 | // 26 | // A Cow is optimized for many concurrent reads 27 | // since any read operation does not require a 28 | // lock. 29 | // 30 | // However, a Cow is not well suited for frequent 31 | // updates since it applies changes to a new copy. 32 | // 33 | // The zero Cow is empty and ready for use. 34 | // A Cow must not be copied after first use. 35 | type Cow[K comparable, V any] struct { 36 | mu sync.Mutex 37 | ptr atomic.Pointer[map[K]V] 38 | capacity int 39 | } 40 | 41 | // Get returns the value associated with the given 42 | // key, if any, and reports whether a value has 43 | // been found. 44 | func (c *Cow[K, V]) Get(key K) (v V, ok bool) { 45 | m := c.ptr.Load() 46 | if m == nil { 47 | return 48 | } 49 | v, ok = (*m)[key] 50 | return 51 | } 52 | 53 | // Set adds the key value pair, or replaces an 54 | // existing value. It reports whether the 55 | // given value has been stored. 56 | // 57 | // If the Cow has reached its capacity limit, 58 | // if set, Set does not add the value and 59 | // returns false. However, it still replaces 60 | // existing values, since this does not increase 61 | // the size of the Cow. 62 | func (c *Cow[K, V]) Set(key K, value V) bool { 63 | c.mu.Lock() 64 | defer c.mu.Unlock() 65 | 66 | p := c.ptr.Load() 67 | if p == nil { 68 | c.ptr.Store(&map[K]V{key: value}) 69 | return true 70 | } 71 | 72 | r := *p 73 | size := len(r) 74 | if c.capacity > 0 && size >= c.capacity { 75 | if _, ok := r[key]; !ok { 76 | return false 77 | } 78 | } else { 79 | size++ 80 | } 81 | 82 | w := make(map[K]V, size) 83 | for k, v := range r { 84 | w[k] = v 85 | } 86 | w[key] = value 87 | 88 | c.ptr.Store(&w) 89 | return true 90 | } 91 | 92 | // Add adds the value if and only if no 93 | // such entry already exists, and reports 94 | // whether the value has been added. 95 | // 96 | // As long as the Cow has reached its 97 | // capacity limit, if set, Add does not 98 | // add the value and returns false. 99 | func (c *Cow[K, V]) Add(key K, value V) bool { 100 | c.mu.Lock() 101 | defer c.mu.Unlock() 102 | 103 | p := c.ptr.Load() 104 | if p == nil { 105 | c.ptr.Store(&map[K]V{key: value}) 106 | return true 107 | } 108 | 109 | r := *p 110 | if c.capacity > 0 && len(r) >= c.capacity { 111 | return false 112 | } 113 | if _, ok := r[key]; ok { 114 | return false 115 | } 116 | 117 | w := make(map[K]V, len(r)+1) 118 | for k, v := range r { 119 | w[k] = v 120 | } 121 | w[key] = value 122 | 123 | c.ptr.Store(&w) 124 | return true 125 | } 126 | 127 | // Delete removes the given entry and reports 128 | // whether it was present. 129 | func (c *Cow[K, V]) Delete(key K) bool { 130 | c.mu.Lock() 131 | defer c.mu.Unlock() 132 | 133 | p := c.ptr.Load() 134 | if p == nil { 135 | return false 136 | } 137 | 138 | r := *p 139 | if len(r) == 0 { 140 | return false 141 | } 142 | if _, ok := r[key]; !ok { 143 | return false 144 | } 145 | 146 | w := make(map[K]V, len(r)) 147 | for k, v := range r { 148 | w[k] = v 149 | } 150 | delete(w, key) 151 | 152 | c.ptr.Store(&w) 153 | return true 154 | } 155 | 156 | // DeleteAll removes all entries. 157 | func (c *Cow[K, V]) DeleteAll() { 158 | c.mu.Lock() 159 | defer c.mu.Unlock() 160 | 161 | if c.ptr.Load() != nil { 162 | c.ptr.Store(new(map[K]V)) 163 | } 164 | } 165 | 166 | // DeleteFunc calls f for each entry and removes any 167 | // entry for which f returns true 168 | func (c *Cow[K, V]) DeleteFunc(f func(K, V) bool) { 169 | c.mu.Lock() 170 | defer c.mu.Unlock() 171 | 172 | p := c.ptr.Load() 173 | if p == nil { 174 | return 175 | } 176 | 177 | r := *p 178 | w := make(map[K]V, len(r)/2) 179 | for k, v := range r { 180 | if !f(k, v) { 181 | w[k] = v 182 | } 183 | } 184 | 185 | c.ptr.Store(&w) 186 | } 187 | 188 | // Clone returns a copy of the Cow. 189 | func (c *Cow[K, V]) Clone() *Cow[K, V] { 190 | c.mu.Lock() 191 | defer c.mu.Unlock() 192 | 193 | p := c.ptr.Load() 194 | if p == nil { 195 | return &Cow[K, V]{ 196 | ptr: atomic.Pointer[map[K]V]{}, 197 | capacity: c.capacity, 198 | } 199 | } 200 | 201 | r := *p 202 | w := make(map[K]V, len(r)) 203 | for k, v := range r { 204 | w[k] = v 205 | } 206 | 207 | cc := &Cow[K, V]{ 208 | ptr: atomic.Pointer[map[K]V]{}, 209 | capacity: c.capacity, 210 | } 211 | cc.ptr.Store(&w) 212 | return cc 213 | } 214 | 215 | // Keys returns a slice of all keys of the Cow. 216 | // It never returns nil. 217 | func (c *Cow[K, _]) Keys() []K { 218 | p := c.ptr.Load() 219 | if p == nil || len(*p) == 0 { 220 | return []K{} 221 | } 222 | 223 | keys := make([]K, 0, len(*p)) 224 | for k := range *p { 225 | keys = append(keys, k) 226 | } 227 | return keys 228 | } 229 | -------------------------------------------------------------------------------- /internal/cache/cow_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 - MinIO, Inc. All rights reserved. 2 | // Use of this source code is governed by the AGPLv3 3 | // license that can be found in the LICENSE file. 4 | 5 | package cache 6 | 7 | import "testing" 8 | 9 | func TestCowZeroValue(t *testing.T) { 10 | t.Run("Get", func(t *testing.T) { 11 | var cow Cow[int, string] 12 | if v, ok := cow.Get(0); ok { 13 | t.Fatalf("Empty Cow contains value: %v", v) 14 | } 15 | }) 16 | t.Run("Delete", func(t *testing.T) { 17 | var cow Cow[int, string] 18 | if cow.Delete(0) { 19 | t.Fatal("Empty Cow contains value") 20 | } 21 | }) 22 | t.Run("DeleteAll", func(*testing.T) { 23 | var cow Cow[int, string] 24 | cow.DeleteAll() // Check whether this panics for an empty Cow 25 | }) 26 | t.Run("DeleteFunc", func(*testing.T) { 27 | var cow Cow[int, string] 28 | cow.DeleteFunc(func(_ int, _ string) bool { return true }) // Check whether this panics for an empty Cow 29 | }) 30 | t.Run("Set", func(t *testing.T) { 31 | var cow Cow[int, string] 32 | if !cow.Set(0, "Hello") { 33 | t.Fatal("Failed to insert value into empty Cow") 34 | } 35 | }) 36 | t.Run("Add", func(t *testing.T) { 37 | var cow Cow[int, string] 38 | if !cow.Add(0, "Hello") { 39 | t.Fatal("Failed to add value to empty Cow") 40 | } 41 | if cow.Add(0, "World") { 42 | t.Fatal("Added the same key to an empty Cow twice") 43 | } 44 | }) 45 | } 46 | 47 | func TestCowCapacity(t *testing.T) { 48 | const Cap = 3 49 | 50 | c := NewCow[int, string](Cap) 51 | if !c.Add(0, "Hello") { 52 | t.Fatalf("Failed to add '%d'", 0) 53 | } 54 | if !c.Add(1, "World") { 55 | t.Fatalf("Failed to add '%d'", 1) 56 | } 57 | if !c.Add(2, "!") { 58 | t.Fatalf("Failed to add '%d'", 2) 59 | } 60 | if c.Add(3, "") { 61 | t.Fatalf("Added more than '%d' keys to Cow", Cap) 62 | } 63 | if c.Set(3, "") { 64 | t.Fatalf("Added more than '%d' keys to Cow", Cap) 65 | } 66 | if !c.Set(2, "") { 67 | t.Fatalf("Failed to replace existing entry even though capacity limited has been reached") 68 | } 69 | 70 | if !c.Delete(2) { 71 | t.Fatalf("Failed to delete '%d'", 2) 72 | } 73 | if !c.Add(3, "") { 74 | t.Fatalf("Failed to add '%d'", 3) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /internal/cli/buffer.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | tui "github.com/charmbracelet/lipgloss" 8 | ) 9 | 10 | // A Buffer is used to efficiently build a string 11 | // to display on a terminal. 12 | type Buffer struct { 13 | s strings.Builder 14 | } 15 | 16 | // Sprint appends to the Buffer. 17 | // Arguments are handled in the manner 18 | // of fmt.Print. 19 | func (b *Buffer) Sprint(v ...any) *Buffer { 20 | b.s.WriteString(fmt.Sprint(v...)) 21 | return b 22 | } 23 | 24 | // Sprintf appends to the Buffer. 25 | // Arguments are handled in the manner 26 | // of fmt.Printf. 27 | func (b *Buffer) Sprintf(format string, v ...any) *Buffer { 28 | b.s.WriteString(fmt.Sprintf(format, v...)) 29 | return b 30 | } 31 | 32 | // Sprintln appends to the Buffer. 33 | // Arguments are handled in the manner 34 | // of fmt.Println. 35 | func (b *Buffer) Sprintln(v ...any) *Buffer { 36 | b.s.WriteString(fmt.Sprintln(v...)) 37 | return b 38 | } 39 | 40 | // Stylef appends the styled string to the Buffer. 41 | // Arguments are handled in the manner of fmt.Printf 42 | // before styling. 43 | func (b *Buffer) Stylef(style tui.Style, format string, v ...any) *Buffer { 44 | b.s.WriteString(style.Render(fmt.Sprintf(format, v...))) 45 | return b 46 | } 47 | 48 | // Styleln appends the styled string to the Buffer. 49 | // Arguments are handled in the manner of fmt.Println 50 | // before styling. 51 | func (b *Buffer) Styleln(style tui.Style, v ...any) *Buffer { 52 | b.s.WriteString(style.Render(fmt.Sprint(v...))) 53 | b.s.WriteByte('\n') 54 | return b 55 | } 56 | 57 | // WriteByte appends the byte c to b's buffer. 58 | // The returned error is always nil. 59 | func (b *Buffer) WriteByte(v byte) error { return b.s.WriteByte(v) } 60 | 61 | // WriteRune appends the UTF-8 encoding of Unicode code point r to b's buffer. 62 | // It returns the length of r and a nil error. 63 | func (b *Buffer) WriteRune(r rune) (int, error) { return b.s.WriteRune(r) } 64 | 65 | // Write appends the contents of p to b's buffer. 66 | // Write always returns len(p), nil. 67 | func (b *Buffer) Write(p []byte) (int, error) { return b.s.Write(p) } 68 | 69 | // WriteString appends the contents of s to b's buffer. 70 | // It returns the length of s and a nil error. 71 | func (b *Buffer) WriteString(s string) (int, error) { return b.s.WriteString(s) } 72 | 73 | // String returns the accumulated string. 74 | func (b *Buffer) String() string { return b.s.String() } 75 | -------------------------------------------------------------------------------- /internal/cli/env.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 - MinIO, Inc. All rights reserved. 2 | // Use of this source code is governed by the AGPLv3 3 | // license that can be found in the LICENSE file. 4 | 5 | package cli 6 | 7 | import ( 8 | "os" 9 | ) 10 | 11 | // Environment variable used by the KES CLI. 12 | const ( 13 | // EnvServer is the server endpoint the client uses. If not set, 14 | // clients will use '127.0.0.1:7373'. 15 | EnvServer = "MINIO_KES_SERVER" 16 | 17 | // EnvAPIKey is used by the client to authenticate to the server. 18 | EnvAPIKey = "MINIO_KES_API_KEY" 19 | 20 | EnvPrivateKey = "MINIO_KES_KEY_FILE" 21 | 22 | EnvCertificate = "MINIO_KES_CERT_FILE" 23 | ) 24 | 25 | // Env retrieves the value of the environment variable named by the key. 26 | // It returns the value, which will be empty if the variable is not present. 27 | func Env(key string) string { 28 | switch key { 29 | default: 30 | return os.Getenv(key) 31 | case EnvServer: 32 | const ( 33 | EnvServerLegacy = "KES_SERVER" 34 | EnvServerMinIO = "MINIO_KMS_KES_ENDPOINT" 35 | DefaultServer = "127.0.0.1:7373" 36 | ) 37 | if s, ok := os.LookupEnv(EnvServer); ok { 38 | return s 39 | } 40 | if s, ok := os.LookupEnv(EnvServerLegacy); ok { 41 | return s 42 | } 43 | if s, ok := os.LookupEnv(EnvServerMinIO); ok { 44 | return s 45 | } 46 | return DefaultServer 47 | 48 | case EnvAPIKey: 49 | const ( 50 | EnvAPIKeyLegacy = "KES_API_KEY" 51 | EnvAPIKeyMinIO = "MINIO_KMS_KES_API_KEY" 52 | ) 53 | if s, ok := os.LookupEnv(EnvAPIKey); ok { 54 | return s 55 | } 56 | if s, ok := os.LookupEnv(EnvAPIKeyLegacy); ok { 57 | return s 58 | } 59 | if s, ok := os.LookupEnv(EnvAPIKeyMinIO); ok { 60 | return s 61 | } 62 | return "" 63 | 64 | case EnvPrivateKey: 65 | const ( 66 | EnvPrivateKeyLegacy = "KES_CLIENT_KEY" 67 | EnvPrivateKeyMinIO = "MINIO_KES_CLIENT_KEY" 68 | ) 69 | if s, ok := os.LookupEnv(EnvPrivateKey); ok { 70 | return s 71 | } 72 | if s, ok := os.LookupEnv(EnvPrivateKeyLegacy); ok { 73 | return s 74 | } 75 | if s, ok := os.LookupEnv(EnvPrivateKeyMinIO); ok { 76 | return s 77 | } 78 | return "" 79 | 80 | case EnvCertificate: 81 | const ( 82 | EnvCertificateLegacy = "KES_CLIENT_CERT" 83 | EnvCertificateMinIO = "MINIO_KES_CLIENT_CERT" 84 | ) 85 | if s, ok := os.LookupEnv(EnvCertificate); ok { 86 | return s 87 | } 88 | if s, ok := os.LookupEnv(EnvCertificateLegacy); ok { 89 | return s 90 | } 91 | if s, ok := os.LookupEnv(EnvCertificateMinIO); ok { 92 | return s 93 | } 94 | return "" 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /internal/cli/exit.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 - MinIO, Inc. All rights reserved. 2 | // Use of this source code is governed by the AGPLv3 3 | // license that can be found in the LICENSE file. 4 | 5 | package cli 6 | 7 | import ( 8 | "fmt" 9 | "os" 10 | 11 | tui "github.com/charmbracelet/lipgloss" 12 | ) 13 | 14 | // Exit prints args as error message and aborts with exit code 1. 15 | func Exit(args ...any) { 16 | const FG tui.Color = "#ac0000" 17 | s := tui.NewStyle().Foreground(FG).Render("Error: ") 18 | 19 | fmt.Fprintln(os.Stderr, s+fmt.Sprint(args...)) 20 | os.Exit(1) 21 | } 22 | 23 | // Exitf formats args as error message and aborts with exit code 1. 24 | func Exitf(format string, args ...any) { 25 | const FG tui.Color = "#ac0000" 26 | s := tui.NewStyle().Foreground(FG).Render("Error: ") 27 | 28 | fmt.Fprintln(os.Stderr, s+fmt.Sprintf(format, args...)) 29 | os.Exit(1) 30 | } 31 | 32 | // Assert calls Exit if the statement is false. 33 | func Assert(statement bool, args ...any) { 34 | if !statement { 35 | Exit(args...) 36 | } 37 | } 38 | 39 | // Assertf calls Exitf if the statement is false. 40 | func Assertf(statement bool, format string, args ...any) { 41 | if !statement { 42 | Exitf(format, args...) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /internal/cli/fmt.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 - MinIO, Inc. All rights reserved. 2 | // Use of this source code is governed by the AGPLv3 3 | // license that can be found in the LICENSE file. 4 | 5 | package cli 6 | 7 | import ( 8 | "fmt" 9 | ) 10 | 11 | // Fatal writes an error prefix and the operands 12 | // to OS stderr. Then, Fatal terminates the program by 13 | // calling os.Exit(1). 14 | func Fatal(v ...any) { Exit(v...) } 15 | 16 | // Fatalf writes an error prefix and the operands, 17 | // formatted according to the format specifier, to OS stderr. 18 | // Then, Fatalf terminates the program by calling os.Exit(1). 19 | func Fatalf(format string, v ...any) { Exitf(format, v...) } 20 | 21 | // Print formats using the default formats for its operands and 22 | // writes to standard output. Spaces are added between operands 23 | // when neither is a string. 24 | // It returns the number of bytes written and any write error 25 | // encountered. 26 | func Print(v ...any) (int, error) { return fmt.Print(v...) } 27 | 28 | // Printf formats according to a format specifier and writes to 29 | // standard output. It returns the number of bytes written and 30 | // any write error encountered. 31 | func Printf(format string, v ...any) (int, error) { return fmt.Printf(format, v...) } 32 | 33 | // Println formats using the default formats for its operands and writes to 34 | // standard output. Spaces are always added between operands and a newline 35 | // is appended. 36 | // It returns the number of bytes written and any write error encountered. 37 | func Println(v ...any) (int, error) { return fmt.Println(v...) } 38 | -------------------------------------------------------------------------------- /internal/cli/term.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 - MinIO, Inc. All rights reserved. 2 | // Use of this source code is governed by the AGPLv3 3 | // license that can be found in the LICENSE file. 4 | 5 | package cli 6 | 7 | import ( 8 | "os" 9 | 10 | tui "github.com/charmbracelet/lipgloss" 11 | "golang.org/x/term" 12 | ) 13 | 14 | var isTerm = term.IsTerminal(int(os.Stdout.Fd())) || term.IsTerminal(int(os.Stderr.Fd())) 15 | 16 | // IsTerminal reports whether stdout is a terminal. 17 | func IsTerminal() bool { return isTerm } 18 | 19 | // Fg returns a new style with the given foreground 20 | // color. All strings s are rendered with the style. 21 | // For example: 22 | // 23 | // fmt.Println(cli.Fg(tui.ANSIColor(2), "Hello World")) 24 | func Fg(c tui.TerminalColor, s ...string) tui.Style { 25 | return tui.NewStyle().Foreground(c).SetString(s...) 26 | } 27 | -------------------------------------------------------------------------------- /internal/cpu/aes.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 - MinIO, Inc. All rights reserved. 2 | // Use of this source code is governed by the AGPLv3 3 | // license that can be found in the LICENSE file. 4 | 5 | package cpu 6 | 7 | import ( 8 | "runtime" 9 | 10 | "golang.org/x/sys/cpu" 11 | ) 12 | 13 | // HasAESGCM returns true if and only if the CPU 14 | // provides native hardware instructions for AES-GCM. 15 | func HasAESGCM() bool { 16 | // Go 1.14 introduced an AES-GCM asm implementation for PPC64-le. 17 | // PPC64 always provides hardware support for AES-GCM. 18 | // Ref: https://go.dev/src/crypto/aes/gcm_ppc64le.go 19 | if runtime.GOARCH == "ppc64le" { 20 | return true 21 | } 22 | 23 | if !cpu.Initialized { 24 | return false 25 | } 26 | if cpu.X86.HasAES && cpu.X86.HasPCLMULQDQ { 27 | return true 28 | } 29 | 30 | // ARM CPUs may provide AES and PMULL instructions 31 | // as well. However, the Go STL does not provide 32 | // an ARM asm implementation. It provides only an 33 | // ARM64 implementation. 34 | if cpu.ARM64.HasAES && cpu.ARM64.HasPMULL { 35 | return true 36 | } 37 | 38 | // On S390X, AES-GCM is only enabled when all 39 | // AES CPU features (CBC, CTR and GHASH / GCM) 40 | // are available. 41 | // Ref: https://golang.org/src/crypto/aes/cipher_s390x.go#L39 42 | if cpu.S390X.HasAES && cpu.S390X.HasAESCBC && cpu.S390X.HasAESCTR && (cpu.S390X.HasGHASH || cpu.S390X.HasAESGCM) { 43 | return true 44 | } 45 | return false 46 | } 47 | -------------------------------------------------------------------------------- /internal/crypto/ciphertext.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 - MinIO, Inc. All rights reserved. 2 | // Use of this source code is governed by the AGPLv3 3 | // license that can be found in the LICENSE file. 4 | 5 | package crypto 6 | 7 | import ( 8 | "encoding/json" 9 | "slices" 10 | 11 | "github.com/minio/kms-go/kes" 12 | "github.com/tinylib/msgp/msgp" 13 | ) 14 | 15 | // parseCiphertext parses and converts a ciphertext into 16 | // the format expected by a SecretKey. 17 | // 18 | // Previous implementations of a SecretKey produced a structured 19 | // ciphertext. parseCiphertext converts all previously generated 20 | // formats into the one that SecretKey.Decrypt expects. 21 | func parseCiphertext(b []byte) []byte { 22 | if len(b) == 0 { 23 | return b 24 | } 25 | 26 | var c ciphertext 27 | switch b[0] { 28 | case 0x95: // msgp first byte 29 | if err := c.UnmarshalBinary(b); err != nil { 30 | return b 31 | } 32 | 33 | b = b[:0] 34 | b = append(b, c.Bytes...) 35 | b = append(b, c.IV...) 36 | b = append(b, c.Nonce...) 37 | case 0x7b: // JSON first byte 38 | if err := c.UnmarshalJSON(b); err != nil { 39 | return b 40 | } 41 | 42 | b = b[:0] 43 | b = append(b, c.Bytes...) 44 | b = append(b, c.IV...) 45 | b = append(b, c.Nonce...) 46 | } 47 | return b 48 | } 49 | 50 | // ciphertext is a structure that contains the encrypted 51 | // bytes and all relevant information to decrypt these 52 | // bytes again with a cryptographic key. 53 | type ciphertext struct { 54 | Algorithm kes.KeyAlgorithm 55 | ID string 56 | IV []byte 57 | Nonce []byte 58 | Bytes []byte 59 | } 60 | 61 | // UnmarshalBinary parses b as binary-encoded ciphertext. 62 | func (c *ciphertext) UnmarshalBinary(b []byte) error { 63 | const ( 64 | Items = 5 65 | IVSize = 16 66 | NonceSize = 12 67 | ) 68 | 69 | items, b, err := msgp.ReadArrayHeaderBytes(b) 70 | if err != nil { 71 | return kes.ErrDecrypt 72 | } 73 | if items != Items { 74 | return kes.ErrDecrypt 75 | } 76 | algorithm, b, err := msgp.ReadStringBytes(b) 77 | if err != nil { 78 | return kes.ErrDecrypt 79 | } 80 | id, b, err := msgp.ReadStringBytes(b) 81 | if err != nil { 82 | return kes.ErrDecrypt 83 | } 84 | var iv [IVSize]byte 85 | b, err = msgp.ReadExactBytes(b, iv[:]) 86 | if err != nil { 87 | return kes.ErrDecrypt 88 | } 89 | var nonce [NonceSize]byte 90 | b, err = msgp.ReadExactBytes(b, nonce[:]) 91 | if err != nil { 92 | return kes.ErrDecrypt 93 | } 94 | bytes, b, err := msgp.ReadBytesZC(b) 95 | if err != nil { 96 | return kes.ErrDecrypt 97 | } 98 | if len(b) != 0 { 99 | return kes.ErrDecrypt 100 | } 101 | 102 | var alg kes.KeyAlgorithm 103 | if err = alg.UnmarshalText([]byte(algorithm)); err != nil { 104 | return kes.ErrDecrypt 105 | } 106 | 107 | c.Algorithm = alg 108 | c.ID = id 109 | c.IV = iv[:] 110 | c.Nonce = nonce[:] 111 | c.Bytes = slices.Clone(bytes) 112 | return nil 113 | } 114 | 115 | // UnmarshalJSON parses the given text as JSON-encoded 116 | // ciphertext. 117 | // 118 | // UnmarshalJSON provides backward-compatible unmarsahaling 119 | // of existing ciphertext. In the past, ciphertexts were 120 | // JSON-encoded. Now, ciphertexts are binary-encoded. 121 | // Therefore, there is no MarshalJSON implementation. 122 | func (c *ciphertext) UnmarshalJSON(text []byte) error { 123 | const ( 124 | IVSize = 16 125 | NonceSize = 12 126 | 127 | AES256GCM = "AES-256-GCM-HMAC-SHA-256" 128 | CHACHA20POLY1305 = "ChaCha20Poly1305" 129 | ) 130 | 131 | type JSON struct { 132 | Algorithm string `json:"aead"` 133 | ID string `json:"id,omitempty"` 134 | IV []byte `json:"iv"` 135 | Nonce []byte `json:"nonce"` 136 | Bytes []byte `json:"bytes"` 137 | } 138 | var value JSON 139 | if err := json.Unmarshal(text, &value); err != nil { 140 | return kes.ErrDecrypt 141 | } 142 | 143 | if value.Algorithm != AES256GCM && value.Algorithm != CHACHA20POLY1305 { 144 | return kes.ErrDecrypt 145 | } 146 | if len(value.IV) != IVSize { 147 | return kes.ErrDecrypt 148 | } 149 | if len(value.Nonce) != NonceSize { 150 | return kes.ErrDecrypt 151 | } 152 | 153 | if value.Algorithm == AES256GCM { 154 | c.Algorithm = kes.AES256 155 | } else { 156 | c.Algorithm = kes.ChaCha20 157 | } 158 | c.ID = value.ID 159 | c.IV = value.IV 160 | c.Nonce = value.Nonce 161 | c.Bytes = value.Bytes 162 | return nil 163 | } 164 | -------------------------------------------------------------------------------- /internal/fips/api.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 - MinIO, Inc. All rights reserved. 2 | // Use of this source code is governed by the AGPLv3 3 | // license that can be found in the LICENSE file. 4 | 5 | package fips 6 | 7 | import "crypto/tls" 8 | 9 | // Enabled indicates whether cryptographic primitives, 10 | // like AES or SHA-256, are implemented using a FIPS 140 11 | // certified module. 12 | // 13 | // If FIPS-140 is enabled no non-NIST/FIPS approved 14 | // primitives must be used. 15 | const Enabled = enabled 16 | 17 | // TLSCiphers returns a list of supported TLS transport 18 | // cipher suite IDs. 19 | func TLSCiphers() []uint16 { 20 | if Enabled { 21 | return []uint16{ 22 | tls.TLS_AES_128_GCM_SHA256, // TLS 1.3 23 | tls.TLS_AES_256_GCM_SHA384, 24 | 25 | tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, // TLS 1.2 26 | tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, 27 | tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, 28 | tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, 29 | } 30 | } 31 | return []uint16{ 32 | tls.TLS_AES_128_GCM_SHA256, // TLS 1.3 33 | tls.TLS_AES_256_GCM_SHA384, 34 | tls.TLS_CHACHA20_POLY1305_SHA256, 35 | 36 | tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, // TLS 1.2 37 | tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, 38 | tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, 39 | tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, 40 | tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, 41 | tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, 42 | } 43 | } 44 | 45 | // TLSCurveIDs returns a list of supported elliptic curve IDs 46 | // in preference order. 47 | func TLSCurveIDs() []tls.CurveID { 48 | if Enabled { 49 | return []tls.CurveID{ 50 | tls.CurveP256, 51 | tls.CurveP384, // Contant time since Go 1.18 52 | tls.CurveP521, // Constat time since Go 1.18 53 | } 54 | } 55 | return []tls.CurveID{ 56 | tls.X25519, 57 | tls.CurveP256, 58 | tls.CurveP384, // Contant time since Go 1.18 59 | tls.CurveP521, // Contant time since Go 1.18 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /internal/fips/fips.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 - MinIO, Inc. All rights reserved. 2 | // Use of this source code is governed by the AGPLv3 3 | // license that can be found in the LICENSE file. 4 | 5 | //go:build fips && linux && amd64 6 | // +build fips,linux,amd64 7 | 8 | package fips 9 | 10 | const enabled = 0 == 0 11 | -------------------------------------------------------------------------------- /internal/fips/nofips.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 - MinIO, Inc. All rights reserved. 2 | // Use of this source code is governed by the AGPLv3 3 | // license that can be found in the LICENSE file. 4 | 5 | //go:build !fips 6 | // +build !fips 7 | 8 | package fips 9 | 10 | const enabled = 0 == 1 11 | -------------------------------------------------------------------------------- /internal/headers/header.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 - MinIO, Inc. All rights reserved. 2 | // Use of this source code is governed by the AGPLv3 3 | // license that can be found in the LICENSE file. 4 | 5 | // Package headers defines common HTTP headers. 6 | package headers 7 | 8 | import ( 9 | "net/http" 10 | "slices" 11 | "strings" 12 | ) 13 | 14 | // Commonly used HTTP headers. 15 | const ( 16 | Accept = "Accept" // RFC 2616 17 | Authorization = "Authorization" // RFC 2616 18 | ContentType = "Content-Type" // RFC 2616 19 | ContentLength = "Content-Length" // RFC 2616 20 | ETag = "ETag" // RFC 2616 21 | TransferEncoding = "Transfer-Encoding" // RFC 2616 22 | ) 23 | 24 | // Commonly used HTTP headers for forwarding originating 25 | // IP addresses of clients connecting through an reverse 26 | // proxy or load balancer. 27 | const ( 28 | Forwarded = "Forwarded" // RFC 7239 29 | XForwardedFor = "X-Forwarded-For" // Non-standard 30 | XFrameOptions = "X-Frame-Options" // Non-standard 31 | ) 32 | 33 | // Commonly used HTTP content type values. 34 | const ( 35 | ContentTypeBinary = "application/octet-stream" 36 | ContentTypeJSON = "application/json" 37 | ContentTypeJSONLines = "application/x-ndjson" 38 | ContentTypeText = "text/plain" 39 | ContentTypeHTML = "text/html" 40 | ) 41 | 42 | // Accepts reports whether h contains an "Accept" header 43 | // that includes s. 44 | func Accepts(h http.Header, s string) bool { 45 | values := h[Accept] 46 | if len(values) == 0 { 47 | return false 48 | } 49 | 50 | return slices.ContainsFunc(values, func(v string) bool { 51 | if v == "*/*" { // matches any MIME type 52 | return true 53 | } 54 | if v == s { 55 | return true 56 | } 57 | if i := strings.IndexByte(v, '*'); i > 0 { // MIME patterns, like application/* 58 | if v[i-1] == '/' { 59 | return strings.HasPrefix(s, v[:i]) 60 | } 61 | } 62 | return false 63 | }) 64 | } 65 | -------------------------------------------------------------------------------- /internal/headers/header_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 - MinIO, Inc. All rights reserved. 2 | // Use of this source code is governed by the AGPLv3 3 | // license that can be found in the LICENSE file. 4 | 5 | package headers 6 | 7 | import ( 8 | "net/http" 9 | "testing" 10 | ) 11 | 12 | func TestAccepts(t *testing.T) { 13 | for i, test := range acceptsTests { 14 | if accept := Accepts(test.Headers, test.ContentType); accept != test.Accept { 15 | t.Errorf("Test %d: got '%v' - want '%v' for content type '%s'", i, accept, test.Accept, test.ContentType) 16 | } 17 | } 18 | } 19 | 20 | var acceptsTests = []struct { 21 | Headers http.Header 22 | ContentType string 23 | Accept bool 24 | }{ 25 | {http.Header{}, "", false}, // 0 26 | {http.Header{Accept: []string{}}, "", false}, // 1 27 | {http.Header{Accept: []string{ContentTypeJSON}}, ContentTypeHTML, false}, // 2 28 | {http.Header{Accept: []string{ContentTypeHTML, ContentTypeBinary}}, ContentTypeBinary, true}, // 3 29 | 30 | {http.Header{Accept: []string{"*/*"}}, ContentTypeBinary, true}, // 4 31 | {http.Header{Accept: []string{"*/*"}}, ContentTypeHTML, true}, // 5 32 | {http.Header{Accept: []string{"*/*"}}, "", true}, // 6 33 | {http.Header{Accept: []string{"*"}}, ContentTypeHTML, false}, // 7 34 | 35 | {http.Header{Accept: []string{"text/*"}}, ContentTypeHTML, true}, // 8 36 | {http.Header{Accept: []string{"text/*"}}, ContentTypeJSON, false}, // 9 37 | {http.Header{Accept: []string{"text*"}}, ContentTypeHTML, false}, // 10 38 | {http.Header{Accept: []string{"application/*"}}, ContentTypeBinary, true}, // 11 39 | {http.Header{Accept: []string{"application/*"}}, ContentTypeJSON, true}, // 12 40 | } 41 | -------------------------------------------------------------------------------- /internal/http/close.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 - MinIO, Inc. All rights reserved. 2 | // Use of this source code is governed by the AGPLv3 3 | // license that can be found in the LICENSE file. 4 | 5 | package http 6 | 7 | import ( 8 | "io" 9 | ) 10 | 11 | // DrainBody close non nil response with any response Body. 12 | // convenient wrapper to drain any remaining data on response body. 13 | // 14 | // Subsequently this allows golang http RoundTripper 15 | // to reuse the same connection for future requests. 16 | func DrainBody(respBody io.ReadCloser) { 17 | // Callers should close resp.Body when done reading from it. 18 | // If resp.Body is not closed, the Client's underlying RoundTripper 19 | // (typically Transport) may not be able to reuse a persistent TCP 20 | // connection to the server for a subsequent "keep-alive" request. 21 | if respBody != nil { 22 | // Drain any remaining Body and then close the connection. 23 | // Without this closing connection would disallow re-using 24 | // the same connection for future uses. 25 | // - http://stackoverflow.com/a/17961593/4465767 26 | io.Copy(io.Discard, respBody) 27 | respBody.Close() 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /internal/https/certificate.go: -------------------------------------------------------------------------------- 1 | package https 2 | 3 | import ( 4 | "bytes" 5 | "crypto/tls" 6 | "crypto/x509" 7 | "encoding/pem" 8 | "errors" 9 | "os" 10 | "strings" 11 | ) 12 | 13 | // CertificateFromFile reads and parses the PEM-encoded private key from 14 | // the keyFile and the X.509 certificate from the given certFile. 15 | // 16 | // If the private key is an encrypted PEM block, it uses the given password 17 | // to decrypt the private key. However, PEM encryption as specified in RFC 18 | // 1423 is insecure by design. Since it does not authenticate the ciphertext, 19 | // it is vulnerable to padding oracle attacks that can let an attacker recover 20 | // the plaintext. 21 | func CertificateFromFile(certFile, keyFile, password string) (tls.Certificate, error) { 22 | certBytes, err := readCertificate(certFile) 23 | if err != nil { 24 | return tls.Certificate{}, err 25 | } 26 | keyBytes, err := readPrivateKey(keyFile, password) 27 | if err != nil { 28 | return tls.Certificate{}, err 29 | } 30 | certificate, err := tls.X509KeyPair(certBytes, keyBytes) 31 | if err != nil { 32 | return tls.Certificate{}, err 33 | } 34 | if certificate.Leaf == nil { 35 | certificate.Leaf, err = x509.ParseCertificate(certificate.Certificate[0]) 36 | if err != nil { 37 | return tls.Certificate{}, err 38 | } 39 | } 40 | return certificate, nil 41 | } 42 | 43 | // FilterPEM applies the filter function on each PEM block 44 | // in pemBlocks and returns an error if at least one PEM 45 | // block does not pass the filter. 46 | func FilterPEM(pemBlocks []byte, filter func(*pem.Block) bool) ([]byte, error) { 47 | pemBlocks = bytes.TrimSpace(pemBlocks) 48 | 49 | b := pemBlocks 50 | for len(b) > 0 { 51 | next, rest := pem.Decode(b) 52 | if next == nil { 53 | return nil, errors.New("https: no valid PEM data") 54 | } 55 | if !filter(next) { 56 | return nil, errors.New("https: unsupported PEM data block") 57 | } 58 | b = rest 59 | } 60 | return pemBlocks, nil 61 | } 62 | 63 | // readCertificate reads the TLS certificate from 64 | // the given file path. 65 | func readCertificate(certFile string) ([]byte, error) { 66 | data, err := os.ReadFile(certFile) 67 | if err != nil { 68 | return nil, err 69 | } 70 | return FilterPEM(data, func(b *pem.Block) bool { return b.Type == "CERTIFICATE" }) 71 | } 72 | 73 | // readPrivateKey reads the TLS private key from the 74 | // given file path. 75 | // 76 | // It decrypts the private key using the given password 77 | // if the private key is an encrypted PEM block. 78 | func readPrivateKey(keyFile, password string) ([]byte, error) { 79 | pemBlock, err := os.ReadFile(keyFile) 80 | if err != nil { 81 | return nil, err 82 | } 83 | pemBlock, err = FilterPEM(pemBlock, func(b *pem.Block) bool { 84 | return b.Type == "CERTIFICATE" || b.Type == "PRIVATE KEY" || strings.HasSuffix(b.Type, " PRIVATE KEY") 85 | }) 86 | if err != nil { 87 | return nil, err 88 | } 89 | 90 | for len(pemBlock) > 0 { 91 | next, rest := pem.Decode(pemBlock) 92 | if next == nil { 93 | return nil, errors.New("https: no PEM-encoded private key found") 94 | } 95 | if next.Type != "PRIVATE KEY" && !strings.HasSuffix(next.Type, " PRIVATE KEY") { 96 | pemBlock = rest 97 | continue 98 | } 99 | 100 | if x509.IsEncryptedPEMBlock(next) { 101 | if password == "" { 102 | return nil, errors.New("https: private key is encrypted: password required") 103 | } 104 | plaintext, err := x509.DecryptPEMBlock(next, []byte(password)) 105 | if err != nil { 106 | return nil, err 107 | } 108 | return pem.EncodeToMemory(&pem.Block{Type: next.Type, Bytes: plaintext}), nil 109 | } 110 | return pem.EncodeToMemory(next), nil 111 | } 112 | return nil, errors.New("https: no PEM-encoded private key found") 113 | } 114 | -------------------------------------------------------------------------------- /internal/https/certificate_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 - MinIO, Inc. All rights reserved. 2 | // Use of this source code is governed by the AGPLv3 3 | // license that can be found in the LICENSE file. 4 | 5 | package https 6 | 7 | import ( 8 | "testing" 9 | ) 10 | 11 | var readPrivateKeyTests = []struct { 12 | FilePath string 13 | Password string 14 | ShouldFail bool 15 | }{ 16 | {FilePath: "testdata/privatekeys/plaintext.pem", Password: ""}, // 0 17 | {FilePath: "testdata/privatekeys/plaintext.pem", Password: "ignored_password"}, // 1 18 | {FilePath: "testdata/privatekeys/encrypted.pem", Password: "correct_password"}, // 2 19 | {FilePath: "testdata/privatekeys/encrypted.pem", Password: "", ShouldFail: true}, // 3 20 | {FilePath: "testdata/privatekeys/encrypted.pem", Password: "incorrect_password", ShouldFail: true}, // 4 21 | } 22 | 23 | func TestReadPrivateKey(t *testing.T) { 24 | for i, test := range readPrivateKeyTests { 25 | _, err := readPrivateKey(test.FilePath, test.Password) 26 | if err != nil && !test.ShouldFail { 27 | t.Fatalf("Test %d: failed to read private key %q: %v", i, test.FilePath, err) 28 | } 29 | if err == nil && test.ShouldFail { 30 | t.Fatalf("Test %d: reading private key %q should have failed", i, test.FilePath) 31 | } 32 | } 33 | } 34 | 35 | var readCertificateTests = []struct { 36 | FilePath string 37 | ShouldFail bool 38 | }{ 39 | {FilePath: "testdata/certificates/single.pem"}, 40 | {FilePath: "testdata/certificates/with_whitespaces.pem"}, 41 | {FilePath: "testdata/certificates/with_privatekey.pem", ShouldFail: true}, 42 | } 43 | 44 | func TestReadCertificate(t *testing.T) { 45 | for i, test := range readCertificateTests { 46 | _, err := readCertificate(test.FilePath) 47 | if err != nil && !test.ShouldFail { 48 | t.Fatalf("Test %d: failed to read certificate %q: %v", i, test.FilePath, err) 49 | } 50 | if err == nil && test.ShouldFail { 51 | t.Fatalf("Test %d: reading certificate %q should have failed", i, test.FilePath) 52 | } 53 | } 54 | } 55 | 56 | var loadCertPoolTests = []struct { 57 | CAPath string 58 | ShouldFail bool 59 | }{ 60 | {CAPath: "testdata/certificates/single.pem"}, 61 | {CAPath: "testdata/certificates/with_whitespaces.pem"}, 62 | {CAPath: "testdata/certificates/with_privatekey.pem", ShouldFail: true}, 63 | {CAPath: "testdata/certificates", ShouldFail: true}, 64 | } 65 | 66 | func TestLoadCertPool(t *testing.T) { 67 | for i, test := range loadCertPoolTests { 68 | _, err := CertPoolFromFile(test.CAPath) 69 | if err != nil && !test.ShouldFail { 70 | t.Fatalf("Test %d: failed to load certificate pool %s: %v", i, test.CAPath, err) 71 | } 72 | if err == nil && test.ShouldFail { 73 | t.Fatalf("Test %d: reading certificate %s should have failed", i, test.CAPath) 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /internal/https/certpool.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 - MinIO, Inc. All rights reserved. 2 | // Use of this source code is governed by the AGPLv3 3 | // license that can be found in the LICENSE file. 4 | 5 | package https 6 | 7 | import ( 8 | "crypto/x509" 9 | "errors" 10 | "os" 11 | "path/filepath" 12 | ) 13 | 14 | // CertPoolFromFile returns a X.509 certificate pool that contains 15 | // all system root certificates from x509.SystemCertPool and 16 | // the certificates loaded from the given filename. 17 | // 18 | // If filename is a directory LoadCertPool parses all files inside 19 | // as PEM-encoded X.509 certificate and adds them to the certificate 20 | // pool. 21 | // Otherwise, LoadCertPool parses filename as PEM-encoded X.509 22 | // certificate file and adds it to the certificate pool. 23 | // 24 | // It returns the first error it encounters, if any, when parsing 25 | // a X.509 certificate file. 26 | func CertPoolFromFile(filename string) (*x509.CertPool, error) { 27 | stat, err := os.Stat(filename) 28 | if err != nil { 29 | return nil, err 30 | } 31 | 32 | pool, _ := x509.SystemCertPool() 33 | if pool == nil { 34 | pool = x509.NewCertPool() 35 | } 36 | if !stat.IsDir() { 37 | if err = appendCertificate(pool, filename); err != nil { 38 | return nil, err 39 | } 40 | return pool, nil 41 | } 42 | 43 | files, err := os.ReadDir(filename) 44 | if err != nil { 45 | return nil, err 46 | } 47 | for _, file := range files { 48 | if file.IsDir() { 49 | continue 50 | } 51 | if err = appendCertificate(pool, filepath.Join(filename, file.Name())); err != nil { 52 | return nil, err 53 | } 54 | } 55 | return pool, nil 56 | } 57 | 58 | // appendCertificate parses the given file as X.509 59 | // certificate and adds it to the given pool. 60 | // 61 | // It returns an error if the certificate couldn't 62 | // be added. 63 | func appendCertificate(pool *x509.CertPool, filename string) error { 64 | b, err := readCertificate(filename) 65 | if err != nil { 66 | return err 67 | } 68 | if !pool.AppendCertsFromPEM(b) { 69 | return errors.New("https: failed to add '" + filename + "' as CA certificate") 70 | } 71 | return nil 72 | } 73 | -------------------------------------------------------------------------------- /internal/https/certpool_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 - MinIO, Inc. All rights reserved. 2 | // Use of this source code is governed by the AGPLv3 3 | // license that can be found in the LICENSE file. 4 | 5 | package https 6 | 7 | import "testing" 8 | 9 | var certpoolFromFileTests = []struct { 10 | Filename string 11 | }{ 12 | {Filename: "testdata/ca/single.pem"}, 13 | } 14 | 15 | func TestCertPoolFromFile(t *testing.T) { 16 | for i, test := range certpoolFromFileTests { 17 | _, err := CertPoolFromFile(test.Filename) 18 | if err != nil { 19 | t.Fatalf("Test %d: failed to load CA certificates: %v", i, err) 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /internal/https/flush.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 - MinIO, Inc. All rights reserved. 2 | // Use of this source code is governed by the AGPLv3 3 | // license that can be found in the LICENSE file. 4 | 5 | package https 6 | 7 | import ( 8 | "net/http" 9 | ) 10 | 11 | // FlushOnWrite returns an ResponseWriter that wraps w and 12 | // flushes after every Write if w implements the Flusher 13 | // interface. 14 | func FlushOnWrite(w http.ResponseWriter) http.ResponseWriter { 15 | f, _ := w.(http.Flusher) 16 | return &flushWriter{ 17 | w: w, 18 | f: f, 19 | } 20 | } 21 | 22 | type flushWriter struct { 23 | w http.ResponseWriter 24 | f http.Flusher 25 | } 26 | 27 | var ( // compiler checks 28 | _ http.ResponseWriter = (*flushWriter)(nil) 29 | _ http.Flusher = (*flushWriter)(nil) 30 | ) 31 | 32 | // Unwrap returns the underlying ResponseWriter. 33 | // 34 | // This method is mainly used in the context of ResponseController. 35 | func (fw *flushWriter) Unwrap() http.ResponseWriter { return fw.w } 36 | 37 | func (fw *flushWriter) WriteHeader(status int) { fw.w.WriteHeader(status) } 38 | 39 | func (fw *flushWriter) Header() http.Header { return fw.w.Header() } 40 | 41 | func (fw *flushWriter) Write(p []byte) (int, error) { 42 | n, err := fw.w.Write(p) 43 | if fw.f != nil && err == nil { 44 | fw.f.Flush() 45 | } 46 | return n, err 47 | } 48 | 49 | func (fw *flushWriter) Flush() { 50 | if fw.f != nil { 51 | fw.f.Flush() 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /internal/https/proxy_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 - MinIO, Inc. All rights reserved. 2 | // Use of this source code is governed by the AGPLv3 3 | // license that can be found in the LICENSE file. 4 | 5 | package https 6 | 7 | import ( 8 | "net/http" 9 | "net/url" 10 | "testing" 11 | 12 | "github.com/minio/kms-go/kes" 13 | ) 14 | 15 | var tlsProxyAddTests = []struct { 16 | Identities []kes.Identity 17 | }{ 18 | { 19 | Identities: nil, 20 | }, 21 | { 22 | Identities: []kes.Identity{ 23 | "57eb2da320a48ebe2750e95c50b3d64240aef4cd5d54c28a4f25155e88c98580", 24 | }, 25 | }, 26 | { 27 | Identities: []kes.Identity{ 28 | kes.IdentityUnknown, 29 | "57eb2da320a48ebe2750e95c50b3d64240aef4cd5d54c28a4f25155e88c98580", 30 | }, 31 | }, 32 | { 33 | Identities: []kes.Identity{ 34 | kes.IdentityUnknown, 35 | "57eb2da320a48ebe2750e95c50b3d64240aef4cd5d54c28a4f25155e88c98580", 36 | "163d766f3e88f2a02b15a46bc541cc679c4cbb0a060405f298d5fc0d9d876bb3", 37 | }, 38 | }, 39 | } 40 | 41 | func TestTLSProxyAdd(t *testing.T) { 42 | for i, test := range tlsProxyAddTests { 43 | var proxy TLSProxy 44 | for j, identity := range test.Identities { 45 | proxy.Add(identity) 46 | if !identity.IsUnknown() && !proxy.Is(identity) { 47 | t.Fatalf("Test %d: %d-th identity '%s' should be a proxy but is not", i, j, identity) 48 | } 49 | } 50 | } 51 | } 52 | 53 | var tlsProxyGetClientCertificateTests = []struct { 54 | Proxy *TLSProxy 55 | Header http.Header 56 | Err error 57 | }{ 58 | { 59 | Proxy: &TLSProxy{}, 60 | Header: http.Header{}, 61 | Err: kes.NewError(http.StatusBadRequest, "no client certificate is present"), 62 | }, 63 | { 64 | Proxy: &TLSProxy{CertHeader: "X-Forwarded-Ssl-Client-Cert"}, 65 | Header: http.Header{ 66 | "X-Forwarded-Ssl-Client-Cert": []string{url.QueryEscape(clientCert)}, 67 | }, 68 | Err: nil, 69 | }, 70 | { 71 | Proxy: &TLSProxy{CertHeader: "X-Forwarded-Ssl-Client-Cert"}, 72 | Header: http.Header{ 73 | "X-Forwarded-Ssl-Client-Cert": []string{url.QueryEscape(clientCert), url.QueryEscape(clientCert)}, 74 | }, 75 | Err: kes.NewError(http.StatusBadRequest, "too many client certificates are present"), 76 | }, 77 | { 78 | Proxy: &TLSProxy{CertHeader: "X-Forwarded-Ssl-Client-Cert"}, 79 | Header: http.Header{ 80 | "X-Ssl-Cert": []string{url.QueryEscape(clientCert)}, 81 | }, 82 | Err: kes.NewError(http.StatusBadRequest, "no client certificate is present"), 83 | }, 84 | { 85 | Proxy: &TLSProxy{CertHeader: "X-Tls-Client-Cert"}, 86 | Header: http.Header{ 87 | "X-Tls-Client-Cert": []string{url.QueryEscape(noPEMTypeClientCert)}, 88 | }, 89 | Err: kes.NewError(http.StatusBadRequest, "invalid client certificate"), 90 | }, 91 | { 92 | Proxy: &TLSProxy{CertHeader: "X-Tls-Client-Cert"}, 93 | Header: http.Header{ 94 | "X-Tls-Client-Cert": []string{unescapedClientCert}, 95 | }, 96 | Err: kes.NewError(http.StatusBadRequest, "invalid client certificate"), 97 | }, 98 | } 99 | 100 | func TestTLSProxyGetClientCertificate(t *testing.T) { 101 | for i, test := range tlsProxyGetClientCertificateTests { 102 | _, err := test.Proxy.getClientCertificate(test.Header) 103 | if err != test.Err { 104 | t.Fatalf("Test %d: got error %v - want error %v", i, err, test.Err) 105 | } 106 | } 107 | } 108 | 109 | const clientCert = `-----BEGIN CERTIFICATE----- 110 | MIIBETCBxKADAgECAhEAwNfpyTO85V8w7ecjWU8CdDAFBgMrZXAwDzENMAsGA1UE 111 | AxMEcm9vdDAeFw0xOTEyMTYyMjQ2NDdaFw0yMDAxMTUyMjQ2NDdaMA8xDTALBgNV 112 | BAMTBHJvb3QwKjAFBgMrZXADIQDNKcY+Mv84QGUEyC/NIvJefLjt9NGGQ9kj5eEX 113 | e2QNGaM1MDMwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMCMAwG 114 | A1UdEwEB/wQCMAAwBQYDK2VwA0EAqUvabyUgcQYp+dPFZpPBycx9+2sWEwwBsybk 115 | JPbwv+fAB2l3rjHt2u9iWL6a2C9xzLh8ni+o2YIWLCGhMSfqBA== 116 | -----END CERTIFICATE-----` 117 | 118 | const noPEMTypeClientCert = `MIIBETCBxKADAgECAhEAwNfpyTO85V8w7ecjWU8CdDAFBgMrZXAwDzENMAsGA1UE 119 | AxMEcm9vdDAeFw0xOTEyMTYyMjQ2NDdaFw0yMDAxMTUyMjQ2NDdaMA8xDTALBgNV 120 | BAMTBHJvb3QwKjAFBgMrZXADIQDNKcY+Mv84QGUEyC/NIvJefLjt9NGGQ9kj5eEX 121 | e2QNGaM1MDMwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMCMAwG 122 | A1UdEwEB/wQCMAAwBQYDK2VwA0EAqUvabyUgcQYp+dPFZpPBycx9+2sWEwwBsybk 123 | JPbwv+fAB2l3rjHt2u9iWL6a2C9xzLh8ni+o2YIWLCGhMSfqBA==` 124 | 125 | const unescapedClientCert = `%A-----BEGIN CERTIFICATE----- 126 | MIIBETCBxKADAgECAhEAwNfpyTO85V8w7ecjWU8CdDAFBgMrZXAwDzENMAsGA1UE 127 | AxMEcm9vdDAeFw0xOTEyMTYyMjQ2NDdaFw0yMDAxMTUyMjQ2NDdaMA8xDTALBgNV 128 | BAMTBHJvb3QwKjAFBgMrZXADIQDNKcY+Mv84QGUEyC/NIvJefLjt9NGGQ9kj5eEX 129 | e2QNGaM1MDMwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMCMAwG 130 | A1UdEwEB/wQCMAAwBQYDK2VwA0EAqUvabyUgcQYp+dPFZpPBycx9+2sWEwwBsybk 131 | JPbwv+fAB2l3rjHt2u9iWL6a2C9xzLh8ni+o2YIWLCGhMSfqBA== 132 | -----END CERTIFICATE-----` 133 | -------------------------------------------------------------------------------- /internal/https/testdata/ca/single.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIBNTCB6KADAgECAhBkrKl4noz7zpfoPPC97FZRMAUGAytlcDAUMRIwEAYDVQQD 3 | Ewlsb2NhbGhvc3QwHhcNMjIwMzA0MTEzNTIyWhcNMjIwNDAzMTEzNTIyWjAUMRIw 4 | EAYDVQQDEwlsb2NhbGhvc3QwKjAFBgMrZXADIQA2dEviao8sJbhFSwwFA+QsoxB1 5 | Mctx7gBFUxALsHfU1qNQME4wDgYDVR0PAQH/BAQDAgeAMB0GA1UdJQQWMBQGCCsG 6 | AQUFBwMBBggrBgEFBQcDAjAMBgNVHRMBAf8EAjAAMA8GA1UdEQQIMAaHBH8AAAEw 7 | BQYDK2VwA0EAFzH90435nyoM9IXMlMvGpZaC7NP2GEhpdEm8nDTXc+aMtov75ws2 8 | aARu9BXAALvoEInVzNq9ZKNWeQjfi0hTCQ== 9 | -----END CERTIFICATE----- 10 | -------------------------------------------------------------------------------- /internal/https/testdata/certificates/single.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIBNTCB6KADAgECAhBkrKl4noz7zpfoPPC97FZRMAUGAytlcDAUMRIwEAYDVQQD 3 | Ewlsb2NhbGhvc3QwHhcNMjIwMzA0MTEzNTIyWhcNMjIwNDAzMTEzNTIyWjAUMRIw 4 | EAYDVQQDEwlsb2NhbGhvc3QwKjAFBgMrZXADIQA2dEviao8sJbhFSwwFA+QsoxB1 5 | Mctx7gBFUxALsHfU1qNQME4wDgYDVR0PAQH/BAQDAgeAMB0GA1UdJQQWMBQGCCsG 6 | AQUFBwMBBggrBgEFBQcDAjAMBgNVHRMBAf8EAjAAMA8GA1UdEQQIMAaHBH8AAAEw 7 | BQYDK2VwA0EAFzH90435nyoM9IXMlMvGpZaC7NP2GEhpdEm8nDTXc+aMtov75ws2 8 | aARu9BXAALvoEInVzNq9ZKNWeQjfi0hTCQ== 9 | -----END CERTIFICATE----- 10 | -------------------------------------------------------------------------------- /internal/https/testdata/certificates/with_privatekey.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MC4CAQAwBQYDK2VwBCIEIKTLLoTTHsF2NhpVJA1AeedZJza2J/VgSEjGYoUPpKEh 3 | -----END PRIVATE KEY----- 4 | -----BEGIN CERTIFICATE----- 5 | MIIBNTCB6KADAgECAhBkrKl4noz7zpfoPPC97FZRMAUGAytlcDAUMRIwEAYDVQQD 6 | Ewlsb2NhbGhvc3QwHhcNMjIwMzA0MTEzNTIyWhcNMjIwNDAzMTEzNTIyWjAUMRIw 7 | EAYDVQQDEwlsb2NhbGhvc3QwKjAFBgMrZXADIQA2dEviao8sJbhFSwwFA+QsoxB1 8 | Mctx7gBFUxALsHfU1qNQME4wDgYDVR0PAQH/BAQDAgeAMB0GA1UdJQQWMBQGCCsG 9 | AQUFBwMBBggrBgEFBQcDAjAMBgNVHRMBAf8EAjAAMA8GA1UdEQQIMAaHBH8AAAEw 10 | BQYDK2VwA0EAFzH90435nyoM9IXMlMvGpZaC7NP2GEhpdEm8nDTXc+aMtov75ws2 11 | aARu9BXAALvoEInVzNq9ZKNWeQjfi0hTCQ== 12 | -----END CERTIFICATE---- 13 | 14 | -------------------------------------------------------------------------------- /internal/https/testdata/certificates/with_whitespaces.pem: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -----BEGIN CERTIFICATE----- 5 | MIIBNTCB6KADAgECAhBkrKl4noz7zpfoPPC97FZRMAUGAytlcDAUMRIwEAYDVQQD 6 | Ewlsb2NhbGhvc3QwHhcNMjIwMzA0MTEzNTIyWhcNMjIwNDAzMTEzNTIyWjAUMRIw 7 | EAYDVQQDEwlsb2NhbGhvc3QwKjAFBgMrZXADIQA2dEviao8sJbhFSwwFA+QsoxB1 8 | Mctx7gBFUxALsHfU1qNQME4wDgYDVR0PAQH/BAQDAgeAMB0GA1UdJQQWMBQGCCsG 9 | AQUFBwMBBggrBgEFBQcDAjAMBgNVHRMBAf8EAjAAMA8GA1UdEQQIMAaHBH8AAAEw 10 | BQYDK2VwA0EAFzH90435nyoM9IXMlMvGpZaC7NP2GEhpdEm8nDTXc+aMtov75ws2 11 | aARu9BXAALvoEInVzNq9ZKNWeQjfi0hTCQ== 12 | -----END CERTIFICATE----- 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /internal/https/testdata/privatekeys/encrypted.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN EC PRIVATE KEY----- 2 | Proc-Type: 4,ENCRYPTED 3 | DEK-Info: AES-256-CBC,E1CF19C5B05C92E1D8D88867779455F8 4 | 5 | hX5Pwa91m/+DGSsE4ON34/fsFyXOakpjnYK7IrbVWHagbMcDgDGTgwkeCqlEX7U/ 6 | BQiX8oGFR/ff+R+TcLRI6tGfmns9B4QS3RVMeY965F1zHysMn/jjIgzQyfmo8YNI 7 | K/11h1SW8SEmbMzmCIhnagYmt/JQrjJVbgGlQgxHV0w= 8 | -----END EC PRIVATE KEY----- 9 | -------------------------------------------------------------------------------- /internal/https/testdata/privatekeys/plaintext.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN EC PRIVATE KEY----- 2 | MHcCAQEEIB4u/+f//HoqSAdVd9DN91JRhQoHLml+/gbvK91SYbK5oAoGCCqGSM49 3 | AwEHoUQDQgAEZwWOkBVrkeLKYE5QFwDzDDHwkBjiVFJ+BgXOaXHTkRcjclh7k85r 4 | wx4zTr/x27oWtlDusD/JTa8dSqJADEF3HA== 5 | -----END EC PRIVATE KEY----- 6 | -------------------------------------------------------------------------------- /internal/keystore/azure/create-keyvault.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | RESOURCE_GROUP=minio-kes 3 | SERVICE_PRINCIPAL=minio-kes 4 | LOCATION=westus 5 | 6 | AZ_SUBSCRIPTION_NAME=$(az account show -o tsv --query 'name') 7 | AZ_SUBSCRIPTION_ID=$(az account show -o tsv --query 'id') 8 | echo "Running in subscription '$AZ_SUBSCRIPTION_NAME' ($AZ_SUBSCRIPTION_ID)" 9 | 10 | # Create the resource-group (if not exists) 11 | AZ_RESOURCE_GROUP=$(az group show --resource-group $RESOURCE_GROUP 2>/dev/null || echo -n) 12 | if [ -z "$AZ_RESOURCE_GROUP" ]; then 13 | echo "Creating resource group '$RESOURCE_GROUP'" 14 | AZ_RESOURCE_GROUP=$(az group create --resource-group $RESOURCE_GROUP -l $LOCATION || echo -n) 15 | else 16 | echo "Using resource group '$RESOURCE_GROUP'" 17 | fi 18 | 19 | # Create a random key-vault-name (should be globally unique) 20 | KEYVAULT_NAME=$(az keyvault list -g $RESOURCE_GROUP --query '[0].name' -o tsv 2>/dev/null || echo -n) 21 | if [ -z "$KEYVAULT_NAME" ]; then 22 | KEYVAULT_NAME=minio-kes-$(tr -dc a-z /dev/null 25 | else 26 | echo "Using existing key-vault '$KEYVAULT_NAME'" 27 | fi 28 | 29 | # Add admin privileges to the keyvault 30 | IAM_ID=$(az ad signed-in-user show --query 'id' -o tsv) 31 | VAULT_ID=$(az keyvault show -g $RESOURCE_GROUP -n $KEYVAULT_NAME --query 'id' -o tsv) 32 | az role assignment create --role "Key Vault Administrator" --scope $VAULT_ID --assignee $IAM_ID > /dev/null 33 | 34 | # Show command to run 35 | VAULT_URI=$(az keyvault show -g $RESOURCE_GROUP -n $KEYVAULT_NAME --query 'properties.vaultUri' -o tsv) 36 | echo "Run: EndPoint=$VAULT_URI go test ." 37 | -------------------------------------------------------------------------------- /internal/keystore/azure/error.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 - MinIO, Inc. All rights reserved. 2 | // Use of this source code is governed by the AGPLv3 3 | // license that can be found in the LICENSE file. 4 | 5 | package azure 6 | 7 | import ( 8 | "encoding/json" 9 | "errors" 10 | 11 | "github.com/Azure/azure-sdk-for-go/sdk/azcore" 12 | ) 13 | 14 | // errorResponse is a KeyVault secrets API error response. 15 | type errorResponse struct { 16 | Error struct { 17 | Code string `json:"code"` 18 | Message string `json:"message"` 19 | Inner struct { 20 | Code string `json:"code"` 21 | } `json:"innererror"` 22 | } `json:"error"` 23 | } 24 | 25 | // transportErrToStatus converts a transport error to a Status. 26 | func transportErrToStatus(err error) (status, error) { 27 | var rerr *azcore.ResponseError 28 | if errors.As(err, &rerr) { 29 | var errorResponse errorResponse 30 | if rerr.RawResponse != nil { 31 | jsonErr := json.NewDecoder(rerr.RawResponse.Body).Decode(&errorResponse) 32 | if jsonErr != nil { 33 | return status{}, err 34 | } 35 | } 36 | return status{ 37 | ErrorCode: errorResponse.Error.Inner.Code, 38 | StatusCode: rerr.StatusCode, 39 | Message: errorResponse.Error.Message, 40 | }, nil 41 | } 42 | return status{}, err 43 | } 44 | -------------------------------------------------------------------------------- /internal/keystore/azure/key-vault_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 - MinIO, Inc. All rights reserved. 2 | // Use of this source code is governed by the AGPLv3 3 | // license that can be found in the LICENSE file. 4 | 5 | package azure 6 | 7 | import ( 8 | "context" 9 | "fmt" 10 | "math/rand" 11 | "os" 12 | "testing" 13 | "time" 14 | 15 | "github.com/Azure/azure-sdk-for-go/sdk/azidentity" 16 | ) 17 | 18 | var ( 19 | prefix = fmt.Sprintf("%04d-", rand.Intn(10000)) 20 | keyName = fmt.Sprintf("%skey", prefix) 21 | ) 22 | 23 | func TestConnectWithCredentials(t *testing.T) { 24 | EndPoint := os.Getenv("EndPoint") 25 | if EndPoint == "" { 26 | t.Skip("Skipping test due to missing Keyvault endpoint") 27 | } 28 | 29 | c, err := azidentity.NewDefaultAzureCredential(nil) 30 | if err != nil { 31 | t.Fatalf("unable to determine Azure credentials: %v", err) 32 | } 33 | 34 | c1, err := ConnectWithCredentials(EndPoint, c) 35 | if err != nil { 36 | return 37 | } 38 | 39 | ctx := context.Background() 40 | 41 | // create key 42 | keyValue := time.Now().Format(time.RFC3339Nano) 43 | err = c1.Create(ctx, keyName, []byte(keyValue)) 44 | if err != nil { 45 | t.Fatalf("error creating key: %s", err) 46 | } 47 | 48 | // delete key upon termination 49 | defer c1.Delete(ctx, keyName) 50 | 51 | // fetch key and check if the value is correct 52 | data, err := c1.Get(ctx, keyName) 53 | if err != nil { 54 | t.Fatalf("error fetching key: %v", err) 55 | } 56 | if string(data) != keyValue { 57 | t.Fatalf("got %q, but expected %q", string(data), keyValue) 58 | } 59 | 60 | // list keys 61 | list, next, err := c1.List(ctx, prefix, 25) 62 | if err != nil { 63 | t.Fatalf("error listing keys: %v", err) 64 | } 65 | if len(list) != 1 || next != "" { 66 | t.Log("got the following keys:\n") 67 | for _, key := range list { 68 | t.Logf("- %s", key) 69 | t.Fatalf("got %d keys, but only expected key %q", len(list), keyName) 70 | } 71 | } 72 | if list[0] != keyName { 73 | t.Fatalf("got key %q, but expected key %q", list[0], keyName) 74 | } 75 | 76 | // delete the key 77 | err = c1.Delete(ctx, keyName) 78 | if err != nil { 79 | t.Fatalf("error deleting key: %v", err) 80 | } 81 | 82 | // recreate the key (deleted secret should be purged automatically) 83 | keyValue = time.Now().Format(time.RFC3339Nano) 84 | err = c1.Create(ctx, keyName, []byte(keyValue)) 85 | if err != nil { 86 | t.Fatalf("error (re)creating the key: %v", err) 87 | } 88 | 89 | // fetch key and check if the value is correct 90 | data, err = c1.Get(ctx, keyName) 91 | if err != nil { 92 | t.Fatalf("error fetching key %q: %v", keyName, err) 93 | } 94 | if string(data) != keyValue { 95 | t.Errorf("Got value %q, but expected value %q", string(data), keyValue) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /internal/keystore/fs/fs.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 - MinIO, Inc. All rights reserved. 2 | // Use of this source code is governed by the AGPLv3 3 | // license that can be found in the LICENSE file. 4 | 5 | // Package fs implements a key-value store that 6 | // stores keys as file names and values as file 7 | // content. 8 | package fs 9 | 10 | import ( 11 | "context" 12 | "errors" 13 | "io" 14 | "os" 15 | "path/filepath" 16 | "strings" 17 | "sync" 18 | "time" 19 | 20 | "aead.dev/mem" 21 | "github.com/minio/kes" 22 | "github.com/minio/kes/internal/keystore" 23 | kesdk "github.com/minio/kms-go/kes" 24 | ) 25 | 26 | // NewStore returns a new Store that reads 27 | // from and writes to the given directory. 28 | // 29 | // If the directory or any parent directory 30 | // does not exist, NewStore creates them all. 31 | // 32 | // It returns an error if dir exists but is 33 | // not a directory. 34 | func NewStore(dir string) (*Store, error) { 35 | switch file, err := os.Stat(dir); { 36 | case errors.Is(err, os.ErrNotExist): 37 | if err = os.MkdirAll(dir, 0o755); err != nil { 38 | return nil, err 39 | } 40 | case err != nil: 41 | return nil, err 42 | default: 43 | if !file.Mode().IsDir() { 44 | return nil, errors.New("fs: '" + dir + "' is not a directory") 45 | } 46 | } 47 | return &Store{dir: dir}, nil 48 | } 49 | 50 | // Store is a connection to a directory on 51 | // the filesystem. 52 | // 53 | // It implements the kms.Store interface and 54 | // acts as KMS abstraction over a filesystem. 55 | type Store struct { 56 | dir string 57 | lock sync.RWMutex 58 | } 59 | 60 | func (s *Store) String() string { return "Filesystem: " + s.dir } 61 | 62 | // Status returns the current state of the Conn. 63 | // 64 | // In particular, it reports whether the underlying 65 | // filesystem is accessible. 66 | func (s *Store) Status(context.Context) (kes.KeyStoreState, error) { 67 | start := time.Now() 68 | if _, err := os.Stat(s.dir); err != nil { 69 | return kes.KeyStoreState{}, &keystore.ErrUnreachable{Err: err} 70 | } 71 | return kes.KeyStoreState{ 72 | Latency: time.Since(start), 73 | }, nil 74 | } 75 | 76 | // Create creates a new file with the given name inside 77 | // the Conn directory if and only if no such file exists. 78 | // 79 | // It returns kes.ErrKeyExists if such a file already exists. 80 | func (s *Store) Create(_ context.Context, name string, value []byte) error { 81 | if err := validName(name); err != nil { 82 | return err 83 | } 84 | s.lock.Lock() 85 | defer s.lock.Unlock() 86 | 87 | filename := filepath.Join(s.dir, name) 88 | switch err := s.create(filename, value); { 89 | case errors.Is(err, os.ErrExist): 90 | return kesdk.ErrKeyExists 91 | case err != nil: 92 | os.Remove(filename) 93 | return err 94 | } 95 | return nil 96 | } 97 | 98 | // Get reads the content of the named file within the Conn 99 | // directory. It returns kes.ErrKeyNotFound if no such file 100 | // exists. 101 | func (s *Store) Get(_ context.Context, name string) ([]byte, error) { 102 | const MaxSize = 1 * mem.MiB 103 | 104 | if err := validName(name); err != nil { 105 | return nil, err 106 | } 107 | s.lock.RLock() 108 | defer s.lock.RUnlock() 109 | 110 | file, err := os.Open(filepath.Join(s.dir, name)) 111 | if errors.Is(err, os.ErrNotExist) { 112 | return nil, kesdk.ErrKeyNotFound 113 | } 114 | if err != nil { 115 | return nil, err 116 | } 117 | defer file.Close() 118 | 119 | value, err := io.ReadAll(mem.LimitReader(file, MaxSize)) 120 | if err != nil { 121 | return nil, err 122 | } 123 | if err = file.Close(); err != nil { 124 | return nil, err 125 | } 126 | return value, nil 127 | } 128 | 129 | // Delete deletes the named file within the Conn directory if 130 | // and only if it exists. It returns kes.ErrKeyNotFound if 131 | // no such file exists. 132 | func (s *Store) Delete(_ context.Context, name string) error { 133 | if err := validName(name); err != nil { 134 | return err 135 | } 136 | switch err := os.Remove(filepath.Join(s.dir, name)); { 137 | case errors.Is(err, os.ErrNotExist): 138 | return kesdk.ErrKeyNotFound 139 | default: 140 | return err 141 | } 142 | } 143 | 144 | // List returns a new Iterator over the names of 145 | // all stored keys. 146 | // List returns the first n key names, that start with the given 147 | // prefix, and the next prefix from which the listing should 148 | // continue. 149 | // 150 | // It returns all keys with the prefix if n < 0 and less than n 151 | // names if n is greater than the number of keys with the prefix. 152 | // 153 | // An empty prefix matches any key name. At the end of the listing 154 | // or when there are no (more) keys starting with the prefix, the 155 | // returned prefix is empty 156 | func (s *Store) List(ctx context.Context, prefix string, n int) ([]string, string, error) { 157 | dir, err := os.Open(s.dir) 158 | if err != nil { 159 | return nil, "", err 160 | } 161 | defer dir.Close() 162 | 163 | names, err := dir.Readdirnames(-1) 164 | if err != nil { 165 | return nil, "", err 166 | } 167 | select { 168 | case <-ctx.Done(): 169 | if err := ctx.Err(); err != nil { 170 | return nil, "", err 171 | } 172 | return nil, "", context.Canceled 173 | default: 174 | return keystore.List(names, prefix, n) 175 | } 176 | } 177 | 178 | // Close closes the Store. 179 | func (s *Store) Close() error { return nil } 180 | 181 | func (s *Store) create(filename string, value []byte) error { 182 | file, err := os.OpenFile(filename, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o600) 183 | if err != nil { 184 | return err 185 | } 186 | defer file.Close() 187 | 188 | n, err := file.Write(value) 189 | if err != nil { 190 | return err 191 | } 192 | if n != len(value) { 193 | return io.ErrShortWrite 194 | } 195 | if err = file.Sync(); err != nil { 196 | return err 197 | } 198 | return file.Close() 199 | } 200 | 201 | func validName(name string) error { 202 | if name == "" || strings.IndexFunc(name, func(c rune) bool { 203 | return c == '/' || c == '\\' || c == '.' 204 | }) >= 0 { 205 | return errors.New("fs: key name contains invalid character") 206 | } 207 | return nil 208 | } 209 | -------------------------------------------------------------------------------- /internal/keystore/fs/fs_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 - MinIO, Inc. All rights reserved. 2 | // Use of this source code is governed by the AGPLv3 3 | // license that can be found in the LICENSE file.ackage fs 4 | 5 | package fs 6 | 7 | import "testing" 8 | 9 | var validNameTests = []struct { 10 | Name string 11 | Valid bool 12 | }{ 13 | {Name: "", Valid: false}, 14 | {Name: ".", Valid: false}, 15 | {Name: "..", Valid: false}, 16 | {Name: ".my-key", Valid: false}, 17 | {Name: "my.key", Valid: false}, 18 | {Name: "/my-key", Valid: false}, 19 | {Name: "\\my-key", Valid: false}, 20 | {Name: "my-key/", Valid: false}, 21 | {Name: "my/key", Valid: false}, 22 | {Name: "./my-key", Valid: false}, 23 | {Name: "./../my-key", Valid: false}, 24 | {Name: "my-key", Valid: true}, 25 | } 26 | 27 | func TestValidName(t *testing.T) { 28 | for i, test := range validNameTests { 29 | if valid := validName(test.Name) == nil; valid != test.Valid { 30 | t.Fatalf("Test %d: got '%v' - wanted: '%v'", i, valid, test.Valid) 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /internal/keystore/gcp/config.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 - MinIO, Inc. All rights reserved. 2 | // Use of this source code is governed by the AGPLv3 3 | // license that can be found in the LICENSE file. 4 | 5 | package gcp 6 | 7 | import ( 8 | "encoding/json" 9 | "log" 10 | "net/url" 11 | "strings" 12 | "sync" 13 | ) 14 | 15 | // Credentials represent GCP service account credentials. 16 | type Credentials struct { 17 | projectID string // not exported - set by the SecretManager 18 | 19 | // ClientID is the client ID of the GCP service account. 20 | ClientID string 21 | 22 | // Client is the client email of the GCP service account. 23 | Client string 24 | 25 | // Key is the private key ID of the GCP service account. 26 | KeyID string 27 | 28 | // Key is the encoded private key of the GCP service account. 29 | Key string 30 | } 31 | 32 | // MarshalJSON returns a JSON representation of the GCP credentials. 33 | // 34 | // The returned JSON contains extra fields to match the JSON credentials 35 | // returned by GCP. Those additional fields are set to default values. 36 | func (c Credentials) MarshalJSON() ([]byte, error) { 37 | type CredentialsJSON struct { 38 | Type string `json:"type"` 39 | ProjectID string `json:"project_id"` 40 | PrivateKeyID string `json:"private_key_id"` 41 | PrivateKey string `json:"private_key"` 42 | ClientEmail string `json:"client_email"` 43 | ClientID string `json:"client_id"` 44 | 45 | AuthURI string `json:"auth_uri"` 46 | TokenURI string `json:"token_uri"` 47 | AuthProviderCertURL string `json:"auth_provider_x509_cert_url"` 48 | ClientCertURL string `json:"client_x509_cert_url"` 49 | } 50 | clientCertURL, err := url.JoinPath("https://www.googleapis.com/robot/v1/metadata/x509", url.QueryEscape(c.Client)) 51 | if err != nil { 52 | return nil, err 53 | } 54 | // A single-quoted YAML string ('foo') will represent newline characters as 55 | // two runes (i.e. a "\" followed by "n"). Typically a private key contains 56 | // newline characters. Hence, we replace the two rune string "\\n" with the 57 | // newline character '\n'. Otherwise, the GCP SDK will fail to parse the private 58 | // key. 59 | return json.Marshal(CredentialsJSON{ 60 | Type: "service_account", 61 | ProjectID: c.projectID, 62 | PrivateKeyID: c.KeyID, 63 | PrivateKey: strings.ReplaceAll(c.Key, "\\n", "\n"), 64 | ClientEmail: c.Client, 65 | ClientID: c.ClientID, 66 | AuthURI: "https://accounts.google.com/o/oauth2/auth", 67 | TokenURI: "https://accounts.google.com/o/oauth2/token", 68 | AuthProviderCertURL: "https://www.googleapis.com/oauth2/v1/certs", 69 | ClientCertURL: clientCertURL, 70 | }) 71 | } 72 | 73 | // Config is a structure containing configuration 74 | // options for connecting to a KeySecure server. 75 | type Config struct { 76 | // Endpoint is the GCP SecretManager endpoint. 77 | Endpoint string 78 | 79 | // ProjectID is the ID of the GCP project. 80 | ProjectID string 81 | 82 | // Credentials are the GCP credentials to 83 | // access the SecretManager. 84 | Credentials Credentials 85 | 86 | // Scopes are GCP OAuth2 scopes for accessing GCP APIs. 87 | // If not set, defaults to the GCP default scopes. 88 | // 89 | // Ref: https://developers.google.com/identity/protocols/oauth2/scopes 90 | Scopes []string 91 | 92 | // ErrorLog is an optional logger for errors 93 | // that may occur when interacting with GCP 94 | // SecretManager. 95 | ErrorLog *log.Logger 96 | 97 | lock sync.RWMutex 98 | } 99 | 100 | // Clone returns a shallow clone of c or nil if c is 101 | // nil. It is safe to clone a Config that is being used 102 | // concurrently. 103 | func (c *Config) Clone() *Config { 104 | if c == nil { 105 | return nil 106 | } 107 | 108 | c.lock.RLock() 109 | defer c.lock.RUnlock() 110 | clone := &Config{ 111 | Endpoint: c.Endpoint, 112 | ProjectID: c.ProjectID, 113 | Credentials: c.Credentials, 114 | ErrorLog: c.ErrorLog, 115 | } 116 | if len(c.Scopes) > 0 { 117 | clone.Scopes = make([]string, 0, len(c.Scopes)) 118 | clone.Scopes = append(clone.Scopes, c.Scopes...) 119 | } 120 | return clone 121 | } 122 | -------------------------------------------------------------------------------- /internal/keystore/gemalto/client.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 - MinIO, Inc. All rights reserved. 2 | // Use of this source code is governed by the AGPLv3 3 | // license that can be found in the LICENSE file. 4 | 5 | package gemalto 6 | 7 | import ( 8 | "bytes" 9 | "context" 10 | "encoding/json" 11 | "errors" 12 | "fmt" 13 | "net/http" 14 | "sync" 15 | "time" 16 | 17 | "aead.dev/mem" 18 | xhttp "github.com/minio/kes/internal/http" 19 | ) 20 | 21 | // authToken is a KeySecure authentication token. 22 | // It can be used to authenticate API requests. 23 | type authToken struct { 24 | Type string 25 | Value string 26 | Expiry time.Duration 27 | } 28 | 29 | // String returns the string representation of 30 | // the authentication token. 31 | func (t *authToken) String() string { return fmt.Sprintf("%s %s", t.Type, t.Value) } 32 | 33 | // client is a KeySecure REST API client 34 | // responsible for fetching and renewing 35 | // authentication tokens. 36 | type client struct { 37 | xhttp.Retry 38 | 39 | lock sync.Mutex 40 | token authToken 41 | } 42 | 43 | // Authenticate tries to obtain a new authentication token 44 | // from the given KeySecure endpoint via the given refresh 45 | // token. 46 | // 47 | // Authenticate should be called to obtain the first authentication 48 | // token. This token can then be renewed via RenewAuthToken. 49 | func (c *client) Authenticate(ctx context.Context, endpoint string, login Credentials) error { 50 | type Request struct { 51 | Type string `json:"grant_type"` 52 | Token string `json:"refresh_token"` 53 | Domain string `json:"domain"` 54 | } 55 | type Response struct { 56 | Type string `json:"token_type"` 57 | Token string `json:"jwt"` 58 | Expiry uint64 `json:"duration"` // KeySecure returns expiry in seconds 59 | } 60 | 61 | body, err := json.Marshal(Request{ 62 | Type: "refresh_token", 63 | Token: login.Token, 64 | Domain: login.Domain, 65 | }) 66 | if err != nil { 67 | return err 68 | } 69 | 70 | url := fmt.Sprintf("%s/api/v1/auth/tokens", endpoint) 71 | req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, xhttp.RetryReader(bytes.NewReader(body))) 72 | if err != nil { 73 | return err 74 | } 75 | req.Header.Set("Content-Type", "application/json") 76 | 77 | resp, err := c.Do(req) 78 | if err != nil { 79 | return err 80 | } 81 | defer xhttp.DrainBody(resp.Body) 82 | 83 | if resp.StatusCode != http.StatusOK { 84 | response, err := parseServerError(resp) 85 | if err != nil { 86 | return fmt.Errorf("%s: %v", resp.Status, err) 87 | } 88 | return fmt.Errorf("%s: %s (%d)", resp.Status, response.Message, response.Code) 89 | } 90 | 91 | const MaxSize = 1 * mem.MiB // An auth. token response should not exceed 1 MiB 92 | var response Response 93 | if err = json.NewDecoder(mem.LimitReader(resp.Body, MaxSize)).Decode(&response); err != nil { 94 | return err 95 | } 96 | if response.Token == "" { 97 | return errors.New("server response does not contain an auth token") 98 | } 99 | if response.Type != "Bearer" { 100 | return fmt.Errorf("unexpected auth token type '%s'", response.Type) 101 | } 102 | if response.Expiry <= 0 { 103 | return fmt.Errorf("invalid auth token expiry '%d'", response.Expiry) 104 | } 105 | 106 | c.lock.Lock() 107 | c.token = authToken{ 108 | Type: response.Type, 109 | Value: response.Token, 110 | Expiry: time.Duration(response.Expiry) * time.Second, 111 | } 112 | c.lock.Unlock() 113 | return nil 114 | } 115 | 116 | // RenewAuthToken tries to renew the client's authentication 117 | // token before it expires. It blocks until <-ctx.Done() completes. 118 | // 119 | // Before calling RenewAuthToken the client should already have a 120 | // authentication token. Therefore, RenewAuthToken should be called 121 | // only after a Authenticate. 122 | // 123 | // RenewAuthToken tries get a new authentication token from the given 124 | // KeySecure endpoint by presenting the given refresh token. 125 | // It continuously tries to renew the authentication before it expires. 126 | // 127 | // If RenewAuthToken fails to request or renew the client's authentication 128 | // token then it keeps retrying and waits for the given login.Retry delay 129 | // between each retry attempt. 130 | // 131 | // If login.Retry is 0 then RenewAuthToken uses a reasonable default retry delay. 132 | func (c *client) RenewAuthToken(ctx context.Context, endpoint string, login Credentials) { 133 | if login.Retry == 0 { 134 | login.Retry = 5 * time.Second 135 | } 136 | var ( 137 | timer *time.Timer 138 | err error 139 | ) 140 | for { 141 | if err != nil { 142 | timer = time.NewTimer(login.Retry) 143 | } else { 144 | c.lock.Lock() 145 | timer = time.NewTimer(c.token.Expiry / 2) 146 | c.lock.Unlock() 147 | } 148 | 149 | select { 150 | case <-ctx.Done(): 151 | timer.Stop() 152 | return 153 | case <-timer.C: 154 | err = c.Authenticate(ctx, endpoint, login) 155 | timer.Stop() 156 | } 157 | } 158 | } 159 | 160 | // AuthToken returns an authentication token that can be 161 | // used to authenticate API requests to a KeySecure instance. 162 | // 163 | // Typically, it is a JWT token and should be used as HTTP 164 | // Authorization header value. 165 | func (c *client) AuthToken() string { 166 | c.lock.Lock() 167 | defer c.lock.Unlock() 168 | 169 | return c.token.String() 170 | } 171 | -------------------------------------------------------------------------------- /internal/keystore/keystore.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 - MinIO, Inc. All rights reserved. 2 | // Use of this source code is governed by the AGPLv3 3 | // license that can be found in the LICENSE file. 4 | 5 | package keystore 6 | 7 | import ( 8 | "errors" 9 | "slices" 10 | "strings" 11 | ) 12 | 13 | // List sorts the names lexicographically and returns the 14 | // first n, if n > 0, names that match the given prefix. 15 | // If n <= 0, List limits the returned slice to a reasonable 16 | // default. If len(names) is greater than n then List returns 17 | // the next name from which to continue. 18 | func List(names []string, prefix string, n int) ([]string, string, error) { 19 | const N = 1024 20 | 21 | slices.Sort(names) 22 | if prefix != "" { 23 | i := slices.IndexFunc(names, func(name string) bool { 24 | return strings.HasPrefix(name, prefix) 25 | }) 26 | if i < 0 { 27 | return []string{}, "", nil 28 | } 29 | names = names[i:] 30 | 31 | for i, name := range names { 32 | if !strings.HasPrefix(name, prefix) { 33 | return names[:i], "", nil 34 | } 35 | if (n > 0 && i > n) || i == N { 36 | if i == len(names)-1 { 37 | return names, "", nil 38 | } 39 | return names[:i], names[i], nil 40 | } 41 | } 42 | } 43 | 44 | switch { 45 | case (n <= 0 && len(names) <= N) || len(names) <= n: 46 | return names, "", nil 47 | case n <= 0: 48 | return names[:N], names[N], nil 49 | default: 50 | return names[:n], names[n], nil 51 | } 52 | } 53 | 54 | // ErrUnreachable is an error that indicates that the 55 | // Store is not reachable - for example due to a 56 | // a network error. 57 | type ErrUnreachable struct { 58 | Err error 59 | } 60 | 61 | func (e *ErrUnreachable) Error() string { 62 | if e.Err == nil { 63 | return "kes: keystore unreachable" 64 | } 65 | return "kes: keystore unreachable: " + e.Err.Error() 66 | } 67 | 68 | // IsUnreachable reports whether err is an Unreachable 69 | // error. If IsUnreachable returns true it returns err 70 | // as Unreachable error. 71 | func IsUnreachable(err error) (*ErrUnreachable, bool) { 72 | var u *ErrUnreachable 73 | if errors.As(err, &u) { 74 | return u, true 75 | } 76 | return nil, false 77 | } 78 | -------------------------------------------------------------------------------- /internal/keystore/keystore_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 - MinIO, Inc. All rights reserved. 2 | // Use of this source code is governed by the AGPLv3 3 | // license that can be found in the LICENSE file. 4 | 5 | package keystore 6 | 7 | import ( 8 | "slices" 9 | "testing" 10 | ) 11 | 12 | func TestList(t *testing.T) { 13 | for i, test := range listTests { 14 | list, continueAt, err := List(test.Names, test.Prefix, test.N) 15 | if err != nil { 16 | t.Fatalf("Test %d: failed to list: %v", i, err) 17 | } 18 | 19 | if !slices.Equal(list, test.List) { 20 | t.Fatalf("Test %d: listing does not match: got '%v' - want '%v'", i, list, test.List) 21 | } 22 | if continueAt != test.ContinueAt { 23 | t.Fatalf("Test %d: continue at does not match: got '%s' - want '%s'", i, continueAt, test.ContinueAt) 24 | } 25 | } 26 | } 27 | 28 | var listTests = []struct { 29 | Names []string 30 | Prefix string 31 | N int 32 | 33 | List []string 34 | ContinueAt string 35 | }{ 36 | { 37 | Names: []string{}, 38 | List: []string{}, 39 | }, 40 | { 41 | Names: []string{"my-key", "my-key2", "0-key", "1-key"}, 42 | List: []string{"0-key", "1-key", "my-key", "my-key2"}, 43 | }, 44 | { 45 | Names: []string{"my-key", "my-key2", "0-key", "1-key"}, 46 | Prefix: "my", 47 | List: []string{"my-key", "my-key2"}, 48 | }, 49 | { 50 | Names: []string{"my-key", "my-key2", "0-key", "1-key"}, 51 | Prefix: "my", 52 | N: 1, 53 | List: []string{"my-key"}, 54 | ContinueAt: "my-key2", 55 | }, 56 | } 57 | -------------------------------------------------------------------------------- /internal/keystore/vault/config_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 - MinIO, Inc. All rights reserved. 2 | // Use of this source code is governed by the AGPLv3 3 | // license that can be found in the LICENSE file. 4 | 5 | package vault 6 | 7 | import ( 8 | "reflect" 9 | "testing" 10 | "time" 11 | ) 12 | 13 | func TestCloneConfig(t *testing.T) { 14 | for i, a := range cloneConfigTests { 15 | if b := a.Clone(); !reflect.DeepEqual(a, b) { 16 | t.Fatalf("Test %d: cloned config does not match original", i) 17 | } 18 | } 19 | } 20 | 21 | var cloneConfigTests = []*Config{ 22 | { 23 | Endpoint: "https://vault.cluster.local:8200", 24 | Engine: "secrets", 25 | APIVersion: APIv2, 26 | Namespace: "ns-1", 27 | Prefix: "my-prefix", 28 | AppRole: &AppRole{ 29 | Engine: "auth", 30 | ID: "be7f3c83-9733-4d65-adaa-7eeb6e14e922", 31 | Secret: "ba8d68af-23c4-4199-a516-e37cebdaab48", 32 | }, 33 | K8S: &Kubernetes{ 34 | Engine: "auth", 35 | Role: "kes", 36 | JWT: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c", 37 | }, 38 | Transit: &Transit{ 39 | Engine: "transit", 40 | KeyName: "my-key", 41 | }, 42 | StatusPingAfter: 15 * time.Second, 43 | PrivateKey: "/tmp/kes/vault.key", 44 | Certificate: "/tmp/kes/vault.crt", 45 | CAPath: "/tmp/kes/vautl.ca", 46 | }, 47 | } 48 | -------------------------------------------------------------------------------- /internal/protobuf/crypto.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2024 - MinIO, Inc. All rights reserved. 2 | // Use of this source code is governed by the AGPLv3 3 | // license that can be found in the LICENSE file. 4 | 5 | // Generate the Go protobuf code by running the protobuf compiler 6 | // from the repository root: 7 | // 8 | // $ protoc -I=./internal/protobuf --go_out=. ./internal/protobuf/*.proto 9 | 10 | syntax = "proto3"; 11 | 12 | package miniohq.kms; 13 | 14 | import "google/protobuf/timestamp.proto"; 15 | 16 | option go_package = "internal/protobuf"; 17 | 18 | message SecretKey { 19 | bytes Key = 1 [ json_name = "key" ]; 20 | uint32 Type = 2 [ json_name = "type" ]; 21 | } 22 | 23 | message HMACKey { 24 | bytes Key = 1 [ json_name = "key" ]; 25 | uint32 Hash = 2 [ json_name = "hash" ]; 26 | } 27 | 28 | message KeyVersion { 29 | SecretKey Key = 1 [ json_name = "key" ]; 30 | HMACKey HMACKey = 2 [ json_name = "hmac_key" ]; 31 | google.protobuf.Timestamp CreatedAt = 3 [ json_name = "created_at" ]; 32 | string CreatedBy = 4 [ json_name = "created_by" ]; 33 | } 34 | -------------------------------------------------------------------------------- /internal/protobuf/proto.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 - MinIO, Inc. All rights reserved. 2 | // Use of this source code is governed by the AGPLv3 3 | // license that can be found in the LICENSE file. 4 | 5 | package protobuf 6 | 7 | import ( 8 | "time" 9 | 10 | "google.golang.org/protobuf/proto" 11 | pbt "google.golang.org/protobuf/types/known/timestamppb" 12 | ) 13 | 14 | // Marshaler is an interface implemented by types that 15 | // know how to marshal themselves into their protobuf 16 | // representation T. 17 | type Marshaler[T proto.Message] interface { 18 | MarshalPB(T) error 19 | } 20 | 21 | // Unmarshaler is an interface implemented by types that 22 | // know how to unmarshal themselves from their protobuf 23 | // representation T. 24 | type Unmarshaler[T proto.Message] interface { 25 | UnmarshalPB(T) error 26 | } 27 | 28 | // Marshal returns v's protobuf binary data by first converting 29 | // v into its protobuf representation type M and then marshaling 30 | // M into the protobuf wire format. 31 | func Marshal[M any, P Pointer[M], T Marshaler[P]](v T) ([]byte, error) { 32 | var m M 33 | if err := v.MarshalPB(&m); err != nil { 34 | return nil, err 35 | } 36 | 37 | var p P = &m 38 | return proto.Marshal(p) 39 | } 40 | 41 | // Unmarshal unmarshales v from b by first decoding b into v's 42 | // protobuf representation M before converting M to v. It returns 43 | // an error if b is not a valid protobuf representation of v. 44 | func Unmarshal[M any, P Pointer[M], T Unmarshaler[P]](b []byte, v T) error { 45 | var m M 46 | var p P = &m 47 | if err := proto.Unmarshal(b, p); err != nil { 48 | return err 49 | } 50 | return v.UnmarshalPB(p) 51 | } 52 | 53 | // Time returns a new protobuf timestamp from the given t. 54 | func Time(t time.Time) *pbt.Timestamp { return pbt.New(t) } 55 | 56 | // Pointer is a type constraint used to express that some 57 | // type P is a pointer of some other type T such that: 58 | // 59 | // var t T 60 | // var p P = &t 61 | // 62 | // This proposition is useful when unmarshalling data into types 63 | // without additional dynamic dispatch or heap allocations. 64 | // 65 | // A generic function that wants to use the default value of 66 | // some type T but also wants to call pointer receiver methods 67 | // on instances of T has to have two type parameters: 68 | // 69 | // func foo[T any, P pointer[T]]() { 70 | // var t T 71 | // var p P = &t 72 | // } 73 | // 74 | // This functionality cannot be achieved with a single type 75 | // parameter because: 76 | // 77 | // func foo[T proto.Message]() { 78 | // var t T // compiles but t is nil if T is a pointer type 79 | // var t2 T = *new(T) // compiles but t2 is nil if T is a pointer type 80 | // var t3 = T{} // compiler error - e.g. T may be a pointer type 81 | // } 82 | type Pointer[M any] interface { 83 | proto.Message 84 | *M // Anything implementing Pointer must also be a pointer type of M 85 | } 86 | -------------------------------------------------------------------------------- /internal/sys/build.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 - MinIO, Inc. All rights reserved. 2 | // Use of this source code is governed by the AGPLv3 3 | // license that can be found in the LICENSE file. 4 | 5 | package sys 6 | 7 | import ( 8 | "errors" 9 | "runtime" 10 | "runtime/debug" 11 | "strings" 12 | "sync" 13 | ) 14 | 15 | // BinaryInfo contains build information about a Go binary. 16 | type BinaryInfo struct { 17 | Version string // The version of this binary 18 | CommitID string // The git commit hash 19 | Runtime string // The Go runtime version, e.g. go1.21.0 20 | Compiler string // The Go compiler used to build this binary 21 | } 22 | 23 | // ReadBinaryInfo returns the ReadBinaryInfo about this program. 24 | func ReadBinaryInfo() (BinaryInfo, error) { return readBinaryInfo() } 25 | 26 | var readBinaryInfo = sync.OnceValues[BinaryInfo, error](func() (BinaryInfo, error) { 27 | const ( 28 | DefaultVersion = "" 29 | DefaultCommitID = "" 30 | DefaultCompiler = "" 31 | ) 32 | binaryInfo := BinaryInfo{ 33 | Version: DefaultVersion, 34 | CommitID: DefaultCommitID, 35 | Runtime: runtime.Version(), 36 | Compiler: DefaultCompiler, 37 | } 38 | 39 | info, ok := debug.ReadBuildInfo() 40 | if !ok { 41 | return binaryInfo, errors.New("sys: binary does not contain build info") 42 | } 43 | 44 | const ( 45 | GitTimeKey = "vcs.time" 46 | GitRevisionKey = "vcs.revision" 47 | CompilerKey = "-compiler" 48 | ) 49 | for _, setting := range info.Settings { 50 | switch setting.Key { 51 | case GitTimeKey: 52 | binaryInfo.Version = strings.ReplaceAll(setting.Value, ":", "-") 53 | case GitRevisionKey: 54 | binaryInfo.CommitID = setting.Value 55 | case CompilerKey: 56 | binaryInfo.Compiler = setting.Value 57 | } 58 | } 59 | return binaryInfo, nil 60 | }) 61 | -------------------------------------------------------------------------------- /kesconf/aws_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 - MinIO, Inc. All rights reserved. 2 | // Use of this source code is governed by the AGPLv3 3 | // license that can be found in the LICENSE file. 4 | 5 | package kesconf_test 6 | 7 | import ( 8 | "flag" 9 | "testing" 10 | 11 | "github.com/minio/kes/kesconf" 12 | ) 13 | 14 | var awsConfigFile = flag.String("aws.config", "", "Path to a KES config file with AWS SecretsManager config") 15 | 16 | func TestAWS(t *testing.T) { 17 | if *awsConfigFile == "" { 18 | t.Skip("AWS SecretsManager tests disabled. Use -aws.config= to enable them") 19 | } 20 | 21 | config, err := kesconf.ReadFile(*awsConfigFile) 22 | if err != nil { 23 | t.Fatal(err) 24 | } 25 | if _, ok := config.KeyStore.(*kesconf.AWSSecretsManagerKeyStore); !ok { 26 | t.Fatalf("Invalid Keystore: want %T - got %T", config.KeyStore, &kesconf.AWSSecretsManagerKeyStore{}) 27 | } 28 | 29 | ctx, cancel := testingContext(t) 30 | defer cancel() 31 | 32 | store, err := config.KeyStore.Connect(ctx) 33 | if err != nil { 34 | t.Fatal(err) 35 | } 36 | 37 | t.Run("Create", func(t *testing.T) { testCreate(ctx, store, t, RandString(ranStringLength)) }) 38 | t.Run("Get", func(t *testing.T) { testGet(ctx, store, t, RandString(ranStringLength)) }) 39 | t.Run("Status", func(t *testing.T) { testStatus(ctx, store, t) }) 40 | } 41 | -------------------------------------------------------------------------------- /kesconf/azure_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 - MinIO, Inc. All rights reserved. 2 | // Use of this source code is governed by the AGPLv3 3 | // license that can be found in the LICENSE file. 4 | 5 | package kesconf_test 6 | 7 | import ( 8 | "flag" 9 | "os" 10 | "testing" 11 | 12 | "github.com/minio/kes/kesconf" 13 | ) 14 | 15 | var azureConfigFile = flag.String("azure.config", "", "Path to a KES config file with Azure KeyVault config") 16 | 17 | func TestAzure(t *testing.T) { 18 | if *azureConfigFile == "" { 19 | t.Skip("Azure KeyVault tests disabled. Use -azure.config= to enable them") 20 | } 21 | file, err := os.Open(*azureConfigFile) 22 | if err != nil { 23 | t.Fatal(err) 24 | } 25 | defer file.Close() 26 | 27 | config, err := kesconf.ReadFile(*azureConfigFile) 28 | if err != nil { 29 | t.Fatal(err) 30 | } 31 | if _, ok := config.KeyStore.(*kesconf.AzureKeyVaultKeyStore); !ok { 32 | t.Fatalf("Invalid Keystore: want %T - got %T", config.KeyStore, &kesconf.AzureKeyVaultKeyStore{}) 33 | } 34 | 35 | ctx, cancel := testingContext(t) 36 | defer cancel() 37 | 38 | store, err := config.KeyStore.Connect(ctx) 39 | if err != nil { 40 | t.Fatal(err) 41 | } 42 | 43 | t.Run("Create", func(t *testing.T) { testCreate(ctx, store, t, RandString(ranStringLength)) }) 44 | t.Run("Get", func(t *testing.T) { testGet(ctx, store, t, RandString(ranStringLength)) }) 45 | t.Run("Status", func(t *testing.T) { testStatus(ctx, store, t) }) 46 | } 47 | -------------------------------------------------------------------------------- /kesconf/edge_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 - MinIO, Inc. All rights reserved. 2 | // Use of this source code is governed by the AGPLv3 3 | // license that can be found in the LICENSE file. 4 | 5 | package kesconf_test 6 | 7 | import ( 8 | "bytes" 9 | "context" 10 | "errors" 11 | "fmt" 12 | "io" 13 | "math/rand" 14 | "os" 15 | "os/signal" 16 | "testing" 17 | 18 | "github.com/minio/kes" 19 | kesdk "github.com/minio/kms-go/kes" 20 | ) 21 | 22 | type SetupFunc func(context.Context, kes.KeyStore, string) error 23 | 24 | const ranStringLength = 8 25 | 26 | var createTests = []struct { 27 | Args map[string][]byte 28 | Setup SetupFunc 29 | ShouldFail bool 30 | }{ 31 | { // 0 32 | Args: map[string][]byte{"edge-test": []byte("edge-test-value")}, 33 | }, 34 | { // 1 35 | Args: map[string][]byte{"edge-test": []byte("edge-test-value")}, 36 | Setup: func(ctx context.Context, s kes.KeyStore, suffix string) error { 37 | return s.Create(ctx, "edge-test-"+suffix, []byte("t")) 38 | }, 39 | ShouldFail: true, 40 | }, 41 | } 42 | 43 | func testCreate(ctx context.Context, store kes.KeyStore, t *testing.T, seed string) { 44 | defer clean(ctx, store, t) 45 | for i, test := range createTests { 46 | if test.Setup != nil { 47 | if err := test.Setup(ctx, store, fmt.Sprintf("%s-%d", seed, i)); err != nil { 48 | t.Fatalf("Test %d: failed to setup: %v", i, err) 49 | } 50 | } 51 | 52 | for key, value := range test.Args { 53 | secretKet := fmt.Sprintf("%s-%s-%d", key, seed, i) 54 | err := store.Create(ctx, secretKet, value) 55 | if err != nil && !test.ShouldFail { 56 | t.Errorf("Test %d: failed to create key '%s': %v", i, secretKet, err) 57 | } 58 | if err == nil && test.ShouldFail { 59 | t.Errorf("Test %d: creating key '%s' should have failed: %v", i, secretKet, err) 60 | } 61 | } 62 | } 63 | } 64 | 65 | var getTests = []struct { 66 | Args map[string][]byte 67 | Setup SetupFunc 68 | ShouldFail bool 69 | }{ 70 | { // 0 71 | Args: map[string][]byte{"edge-test": []byte("edge-test-value")}, 72 | Setup: func(ctx context.Context, s kes.KeyStore, suffix string) error { 73 | return s.Create(ctx, "edge-test-"+suffix, []byte("edge-test-value")) 74 | }, 75 | }, 76 | { // 1 77 | Args: map[string][]byte{"edge-test": []byte("edge-test-value")}, 78 | ShouldFail: true, 79 | }, 80 | { // 1 81 | Args: map[string][]byte{"edge-test": []byte("edge-test-value")}, 82 | Setup: func(ctx context.Context, s kes.KeyStore, suffix string) error { 83 | return s.Create(ctx, "edge-test-"+suffix, []byte("edge-test-value2")) 84 | }, 85 | ShouldFail: true, 86 | }, 87 | } 88 | 89 | func testGet(ctx context.Context, store kes.KeyStore, t *testing.T, seed string) { 90 | defer clean(ctx, store, t) 91 | for i, test := range getTests { 92 | if test.Setup != nil { 93 | if err := test.Setup(ctx, store, fmt.Sprintf("%s-%d", seed, i)); err != nil { 94 | t.Fatalf("Test %d: failed to setup: %v", i, err) 95 | } 96 | } 97 | 98 | for key, value := range test.Args { 99 | secretKet := fmt.Sprintf("%s-%s-%d", key, seed, i) 100 | v, err := store.Get(ctx, secretKet) 101 | if !test.ShouldFail { 102 | if err != nil { 103 | t.Errorf("Test %d: failed to get key '%s': %v", i, secretKet, err) 104 | } 105 | if !bytes.Equal(v, value) { 106 | t.Errorf("Test %d: failed to get key: got '%s' - want '%s'", i, string(v), string(value)) 107 | } 108 | } 109 | if test.ShouldFail && err == nil && bytes.Equal(v, value) { 110 | t.Errorf("Test %d: getting key '%s' should have failed: %v", i, secretKet, err) 111 | } 112 | } 113 | } 114 | } 115 | 116 | func testStatus(ctx context.Context, store kes.KeyStore, t *testing.T) { 117 | if _, err := store.Status(ctx); err != nil { 118 | t.Fatalf("Failed to fetch status: %v", err) 119 | } 120 | } 121 | 122 | var osCtx, _ = signal.NotifyContext(context.Background(), os.Interrupt, os.Kill) 123 | 124 | func testingContext(t *testing.T) (context.Context, context.CancelFunc) { 125 | d, ok := t.Deadline() 126 | if !ok { 127 | return osCtx, func() {} 128 | } 129 | return context.WithDeadline(osCtx, d) 130 | } 131 | 132 | func clean(ctx context.Context, store kes.KeyStore, t *testing.T) { 133 | iter := kesdk.ListIter[string]{ 134 | NextFunc: store.List, 135 | } 136 | 137 | var names []string 138 | for next, err := iter.Next(ctx); err != io.EOF; next, err = iter.Next(ctx) { 139 | if err != nil { 140 | t.Errorf("Cleanup: failed to list: %v", err) 141 | } 142 | names = append(names, next) 143 | } 144 | for _, name := range names { 145 | if err := store.Delete(ctx, name); err != nil && !errors.Is(err, kesdk.ErrKeyNotFound) { 146 | t.Errorf("Cleanup: failed to delete '%s': %v", name, err) 147 | } 148 | } 149 | } 150 | 151 | const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" 152 | 153 | func RandString(n int) string { 154 | b := make([]byte, n) 155 | for i := range b { 156 | b[i] = letters[rand.Intn(len(letters))] 157 | } 158 | return string(b) 159 | } 160 | -------------------------------------------------------------------------------- /kesconf/fortanix_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 - MinIO, Inc. All rights reserved. 2 | // Use of this source code is governed by the AGPLv3 3 | // license that can be found in the LICENSE file. 4 | 5 | package kesconf_test 6 | 7 | import ( 8 | "flag" 9 | "testing" 10 | 11 | "github.com/minio/kes/kesconf" 12 | ) 13 | 14 | var fortanixConfigFile = flag.String("fortanix.config", "", "Path to a KES config file with Fortanix SDKMS config") 15 | 16 | func TestFortanix(t *testing.T) { 17 | if *fortanixConfigFile == "" { 18 | t.Skip("Fortanix tests disabled. Use -fortanix.config= to enable them") 19 | } 20 | 21 | config, err := kesconf.ReadFile(*fortanixConfigFile) 22 | if err != nil { 23 | t.Fatal(err) 24 | } 25 | 26 | if _, ok := config.KeyStore.(*kesconf.FortanixKeyStore); !ok { 27 | t.Fatalf("Invalid Keystore: want %T - got %T", config.KeyStore, &kesconf.FortanixKeyStore{}) 28 | } 29 | 30 | ctx, cancel := testingContext(t) 31 | defer cancel() 32 | 33 | store, err := config.KeyStore.Connect(ctx) 34 | if err != nil { 35 | t.Fatal(err) 36 | } 37 | 38 | t.Run("Create", func(t *testing.T) { testCreate(ctx, store, t, RandString(ranStringLength)) }) 39 | t.Run("Get", func(t *testing.T) { testGet(ctx, store, t, RandString(ranStringLength)) }) 40 | t.Run("Status", func(t *testing.T) { testStatus(ctx, store, t) }) 41 | } 42 | -------------------------------------------------------------------------------- /kesconf/fs_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 - MinIO, Inc. All rights reserved. 2 | // Use of this source code is governed by the AGPLv3 3 | // license that can be found in the LICENSE file. 4 | 5 | package kesconf_test 6 | 7 | import ( 8 | "flag" 9 | "testing" 10 | 11 | "github.com/minio/kes/kesconf" 12 | ) 13 | 14 | var FSPath = flag.String("fs.path", "", "Path used for FS tests") 15 | 16 | func TestFS(t *testing.T) { 17 | if *FSPath == "" { 18 | t.Skip("FS tests disabled. Use -fs.path= to enable them") 19 | } 20 | config := kesconf.FSKeyStore{ 21 | Path: *FSPath, 22 | } 23 | 24 | ctx, cancel := testingContext(t) 25 | defer cancel() 26 | 27 | store, err := config.Connect(ctx) 28 | if err != nil { 29 | t.Fatal(err) 30 | } 31 | 32 | t.Run("Create", func(t *testing.T) { testCreate(ctx, store, t, RandString(ranStringLength)) }) 33 | t.Run("Get", func(t *testing.T) { testGet(ctx, store, t, RandString(ranStringLength)) }) 34 | t.Run("Status", func(t *testing.T) { testStatus(ctx, store, t) }) 35 | } 36 | -------------------------------------------------------------------------------- /kesconf/gcp_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 - MinIO, Inc. All rights reserved. 2 | // Use of this source code is governed by the AGPLv3 3 | // license that can be found in the LICENSE file. 4 | 5 | package kesconf_test 6 | 7 | import ( 8 | "flag" 9 | "testing" 10 | 11 | "github.com/minio/kes/kesconf" 12 | ) 13 | 14 | var gcpConfigFile = flag.String("gcp.config", "", "Path to a KES config file with GCP SecretManager config") 15 | 16 | func TestGCP(t *testing.T) { 17 | if *gcpConfigFile == "" { 18 | t.Skip("GCP tests disabled. Use -gcp.config= to enable them") 19 | } 20 | 21 | config, err := kesconf.ReadFile(*gcpConfigFile) 22 | if err != nil { 23 | t.Fatal(err) 24 | } 25 | 26 | if _, ok := config.KeyStore.(*kesconf.GCPSecretManagerKeyStore); !ok { 27 | t.Fatalf("Invalid Keystore: want %T - got %T", config.KeyStore, &kesconf.GCPSecretManagerKeyStore{}) 28 | } 29 | 30 | ctx, cancel := testingContext(t) 31 | defer cancel() 32 | 33 | store, err := config.KeyStore.Connect(ctx) 34 | if err != nil { 35 | t.Fatal(err) 36 | } 37 | 38 | t.Run("Create", func(t *testing.T) { testCreate(ctx, store, t, RandString(ranStringLength)) }) 39 | t.Run("Get", func(t *testing.T) { testGet(ctx, store, t, RandString(ranStringLength)) }) 40 | t.Run("Status", func(t *testing.T) { testStatus(ctx, store, t) }) 41 | } 42 | -------------------------------------------------------------------------------- /kesconf/gemalto_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 - MinIO, Inc. All rights reserved. 2 | // Use of this source code is governed by the AGPLv3 3 | // license that can be found in the LICENSE file. 4 | 5 | package kesconf_test 6 | 7 | import ( 8 | "flag" 9 | "testing" 10 | 11 | "github.com/minio/kes/kesconf" 12 | ) 13 | 14 | var gemaltoConfigFile = flag.String("gemalto.config", "", "Path to a KES config file with Gemalto KeySecure config") 15 | 16 | func TestGemalto(t *testing.T) { 17 | if *gemaltoConfigFile == "" { 18 | t.Skip("Gemalto tests disabled. Use -gemalto.config= to enable them") 19 | } 20 | 21 | config, err := kesconf.ReadFile(*gemaltoConfigFile) 22 | if err != nil { 23 | t.Fatal(err) 24 | } 25 | 26 | if _, ok := config.KeyStore.(*kesconf.KeySecureKeyStore); !ok { 27 | t.Fatalf("Invalid Keystore: want %T - got %T", config.KeyStore, &kesconf.KeySecureKeyStore{}) 28 | } 29 | 30 | ctx, cancel := testingContext(t) 31 | defer cancel() 32 | 33 | store, err := config.KeyStore.Connect(ctx) 34 | if err != nil { 35 | t.Fatal(err) 36 | } 37 | 38 | t.Run("Create", func(t *testing.T) { testCreate(ctx, store, t, RandString(ranStringLength)) }) 39 | t.Run("Get", func(t *testing.T) { testGet(ctx, store, t, RandString(ranStringLength)) }) 40 | t.Run("Status", func(t *testing.T) { testStatus(ctx, store, t) }) 41 | } 42 | -------------------------------------------------------------------------------- /kesconf/keycontrol_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 - MinIO, Inc. All rights reserved. 2 | // Use of this source code is governed by the AGPLv3 3 | // license that can be found in the LICENSE file. 4 | 5 | package kesconf_test 6 | 7 | import ( 8 | "flag" 9 | "testing" 10 | 11 | "github.com/minio/kes/kesconf" 12 | ) 13 | 14 | var keyControlConfigFile = flag.String("entrust.config", "", "Path to a KES config file with Entrust KeyControl config") 15 | 16 | func TestKeyControl(t *testing.T) { 17 | if *keyControlConfigFile == "" { 18 | t.Skip("KeyControl tests disabled. Use -entrust.config= to enable them") 19 | } 20 | 21 | config, err := kesconf.ReadFile(*keyControlConfigFile) 22 | if err != nil { 23 | t.Fatal(err) 24 | } 25 | 26 | if _, ok := config.KeyStore.(*kesconf.EntrustKeyControlKeyStore); !ok { 27 | t.Fatalf("Invalid Keystore: want %T - got %T", config.KeyStore, &kesconf.EntrustKeyControlKeyStore{}) 28 | } 29 | 30 | ctx, cancel := testingContext(t) 31 | defer cancel() 32 | 33 | store, err := config.KeyStore.Connect(ctx) 34 | if err != nil { 35 | t.Fatal(err) 36 | } 37 | 38 | t.Run("Create", func(t *testing.T) { testCreate(ctx, store, t, RandString(ranStringLength)) }) 39 | t.Run("Get", func(t *testing.T) { testGet(ctx, store, t, RandString(ranStringLength)) }) 40 | t.Run("Status", func(t *testing.T) { testStatus(ctx, store, t) }) 41 | } 42 | -------------------------------------------------------------------------------- /kesconf/testdata/aws-no-credentials.yml: -------------------------------------------------------------------------------- 1 | version: v1 2 | 3 | address: 0.0.0.0:7373 4 | 5 | admin: 6 | identity: c84cc9b91ae2399b043da7eca616048d4b4200edf2ff418d8af3835911db945d 7 | 8 | tls: 9 | key: ./server.key 10 | cert: ./server.cert 11 | 12 | keystore: 13 | aws: 14 | secretsmanager: 15 | endpoint: secretsmanager.us-east-2.amazonaws.com 16 | region: us-east-2 17 | -------------------------------------------------------------------------------- /kesconf/testdata/aws.yml: -------------------------------------------------------------------------------- 1 | version: v1 2 | 3 | address: 0.0.0.0:7373 4 | 5 | admin: 6 | identity: c84cc9b91ae2399b043da7eca616048d4b4200edf2ff418d8af3835911db945d 7 | 8 | tls: 9 | key: ./server.key 10 | cert: ./server.cert 11 | 12 | keystore: 13 | aws: 14 | secretsmanager: 15 | endpoint: secretsmanager.us-east-2.amazonaws.com 16 | region: us-east-2 17 | credentials: 18 | accesskey: AKIAIOSFODNN7EXAMPLE 19 | secretkey: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY 20 | -------------------------------------------------------------------------------- /kesconf/testdata/custom-api.yml: -------------------------------------------------------------------------------- 1 | 2 | address: 0.0.0.0:7373 3 | admin: 4 | identity: disabled 5 | 6 | tls: 7 | key: ./private.key 8 | cert: ./public.crt 9 | 10 | cache: 11 | expiry: 12 | any: 5m0s 13 | unused: 30s 14 | offline: 0s 15 | 16 | api: 17 | /v1/status: 18 | timeout: 17s 19 | skip_auth: true 20 | /v1/metrics: 21 | timeout: 22s 22 | skip_auth: true 23 | 24 | keystore: 25 | fs: 26 | path: /tmp/kes -------------------------------------------------------------------------------- /kesconf/testdata/fs.yml: -------------------------------------------------------------------------------- 1 | version: v1 2 | 3 | address: 0.0.0.0:7373 4 | 5 | admin: 6 | identity: c84cc9b91ae2399b043da7eca616048d4b4200edf2ff418d8af3835911db945d 7 | 8 | tls: 9 | key: ./server.key 10 | cert: ./server.cert 11 | 12 | keystore: 13 | fs: 14 | path: "/tmp/keys" 15 | -------------------------------------------------------------------------------- /kesconf/testdata/vault-approle.yml: -------------------------------------------------------------------------------- 1 | version: v1 2 | 3 | address: 0.0.0.0:7373 4 | 5 | admin: 6 | identity: c84cc9b91ae2399b043da7eca616048d4b4200edf2ff418d8af3835911db945d 7 | 8 | tls: 9 | key: ./server.key 10 | cert: ./server.cert 11 | 12 | keystore: 13 | vault: 14 | endpoint: https://127.0.0.1:8200 15 | engine: kv 16 | version: v2 17 | namespace: ns1 18 | prefix: tenant-1 19 | approle: 20 | engine: approle 21 | id: db02de05-fa39-4855-059b-67221c5c2f63 22 | secret: 6a174c20-f6de-a53c-74d2-6018fcceff64 23 | 24 | -------------------------------------------------------------------------------- /kesconf/testdata/vault-k8s-service-account: -------------------------------------------------------------------------------- 1 | eyJhbGciOiJSUzI1NiIsImtpZCI6IkJQbGNNeTdBeXdLQmZMaGw2N1dFZkJvUmtsdnVvdkxXWGsteTc5TmJPeGMifQ.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJteS1uYW1lc3BhY2UiLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlY3JldC5uYW1lIjoibXktc2VydmljZS1hY2NvdW50LXRva2VuLXA5NWRyIiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZXJ2aWNlLWFjY291bnQubmFtZSI6Im15LXNlcnZpY2UtYWNjb3VudCIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50LnVpZCI6IjdiYmViZGE2LTViMDUtNGFlNC05Yjg2LTBkODE0NWMwNzdhNSIsInN1YiI6InN5c3RlbTpzZXJ2aWNlYWNjb3VudDpteS1uYW1lc3BhY2U6bXktc2VydmljZS1hY2NvdW50In0.dnvJE3LU7L8XxsIOwea3lUZAULdwAjV9_crHFLKBGNxEu70lk3MQmUbGTEFvawryArmxMa1bWF9wbK1GHEsNipDgWAmc0rmBYByP_ahlf9bI2EEzpaGU5s194csB_eG7kvfi1AHED_nkVTfvCjIJM-9oGICCjDJcoNOP1NAXICFmqvWfXl6SY3UoZvtzUOcH9-0hbARY3p6V5pPecW4Dm-yGub9PKZLJNzv7GxChM-uvBvHAt6o0UBIL4iSy6Bx2l91ojB-RSkm_oy0W9gKi9ZFQPgyvcvQnEfjoGdvNGlOEdFEdXvl-dP6iLBPnZ5xwhAk8lK0oOONWvQg6VDNd9w -------------------------------------------------------------------------------- /kesconf/testdata/vault-k8s-with-service-account-file.yml: -------------------------------------------------------------------------------- 1 | version: v1 2 | 3 | address: 0.0.0.0:7373 4 | 5 | admin: 6 | identity: c84cc9b91ae2399b043da7eca616048d4b4200edf2ff418d8af3835911db945d 7 | 8 | tls: 9 | key: ./server.key 10 | cert: ./server.cert 11 | 12 | keystore: 13 | vault: 14 | endpoint: https://127.0.0.1:8201 15 | engine: secrets 16 | version: v1 17 | namespace: ns2 18 | prefix: tenant-2 19 | kubernetes: 20 | engine: kubernetes 21 | role: default 22 | jwt: "./testdata/vault-k8s-service-account" 23 | 24 | -------------------------------------------------------------------------------- /kesconf/testdata/vault-k8s.yml: -------------------------------------------------------------------------------- 1 | version: v1 2 | 3 | address: 0.0.0.0:7373 4 | 5 | admin: 6 | identity: c84cc9b91ae2399b043da7eca616048d4b4200edf2ff418d8af3835911db945d 7 | 8 | tls: 9 | key: ./server.key 10 | cert: ./server.cert 11 | 12 | keystore: 13 | vault: 14 | endpoint: https://127.0.0.1:8201 15 | engine: secrets 16 | version: v1 17 | namespace: ns2 18 | prefix: tenant-2 19 | kubernetes: 20 | engine: kubernetes 21 | role: default 22 | jwt: eyJhbGciOiJSUzI1NiIsImtpZCI6IkJQbGNNeTdBeXdLQmZMaGw2N1dFZkJvUmtsdnVvdkxXWGsteTc5TmJPeGMifQ.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJteS1uYW1lc3BhY2UiLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlY3JldC5uYW1lIjoibXktc2VydmljZS1hY2NvdW50LXRva2VuLXA5NWRyIiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZXJ2aWNlLWFjY291bnQubmFtZSI6Im15LXNlcnZpY2UtYWNjb3VudCIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50LnVpZCI6IjdiYmViZGE2LTViMDUtNGFlNC05Yjg2LTBkODE0NWMwNzdhNSIsInN1YiI6InN5c3RlbTpzZXJ2aWNlYWNjb3VudDpteS1uYW1lc3BhY2U6bXktc2VydmljZS1hY2NvdW50In0.dnvJE3LU7L8XxsIOwea3lUZAULdwAjV9_crHFLKBGNxEu70lk3MQmUbGTEFvawryArmxMa1bWF9wbK1GHEsNipDgWAmc0rmBYByP_ahlf9bI2EEzpaGU5s194csB_eG7kvfi1AHED_nkVTfvCjIJM-9oGICCjDJcoNOP1NAXICFmqvWfXl6SY3UoZvtzUOcH9-0hbARY3p6V5pPecW4Dm-yGub9PKZLJNzv7GxChM-uvBvHAt6o0UBIL4iSy6Bx2l91ojB-RSkm_oy0W9gKi9ZFQPgyvcvQnEfjoGdvNGlOEdFEdXvl-dP6iLBPnZ5xwhAk8lK0oOONWvQg6VDNd9w 23 | 24 | -------------------------------------------------------------------------------- /kesconf/testdata/vault.yml: -------------------------------------------------------------------------------- 1 | version: v1 2 | 3 | address: 0.0.0.0:7373 4 | 5 | admin: 6 | identity: c84cc9b91ae2399b043da7eca616048d4b4200edf2ff418d8af3835911db945d 7 | 8 | tls: 9 | key: ./server.key 10 | cert: ./server.cert 11 | 12 | keystore: 13 | vault: 14 | endpoint: https://127.0.0.1:8200 15 | engine: kv 16 | version: v2 17 | namespace: ns1 18 | prefix: tenant-1 19 | approle: 20 | engine: approle 21 | id: db02de05-fa39-4855-059b-67221c5c2f63 22 | secret: 6a174c20-f6de-a53c-74d2-6018fcceff64 23 | 24 | -------------------------------------------------------------------------------- /kesconf/vault_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 - MinIO, Inc. All rights reserved. 2 | // Use of this source code is governed by the AGPLv3 3 | // license that can be found in the LICENSE file. 4 | 5 | package kesconf_test 6 | 7 | import ( 8 | "flag" 9 | "testing" 10 | 11 | "github.com/minio/kes/kesconf" 12 | ) 13 | 14 | var vaultConfigFile = flag.String("vault.config", "", "Path to a KES config file with Hashicorp Vault config") 15 | 16 | func TestVault(t *testing.T) { 17 | if *vaultConfigFile == "" { 18 | t.Skip("Vault tests disabled. Use -vault.config= to enable them") 19 | } 20 | 21 | config, err := kesconf.ReadFile(*vaultConfigFile) 22 | if err != nil { 23 | t.Fatal(err) 24 | } 25 | 26 | if _, ok := config.KeyStore.(*kesconf.VaultKeyStore); !ok { 27 | t.Fatalf("Invalid Keystore: want %T - got %T", config.KeyStore, &kesconf.VaultKeyStore{}) 28 | } 29 | 30 | ctx, cancel := testingContext(t) 31 | defer cancel() 32 | 33 | store, err := config.KeyStore.Connect(ctx) 34 | if err != nil { 35 | t.Fatal(err) 36 | } 37 | 38 | t.Run("Create", func(t *testing.T) { testCreate(ctx, store, t, RandString(ranStringLength)) }) 39 | t.Run("Get", func(t *testing.T) { testGet(ctx, store, t, RandString(ranStringLength)) }) 40 | t.Run("Status", func(t *testing.T) { testStatus(ctx, store, t) }) 41 | } 42 | -------------------------------------------------------------------------------- /log.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 - MinIO, Inc. All rights reserved. 2 | // Use of this source code is governed by the AGPLv3 3 | // license that can be found in the LICENSE file. 4 | 5 | package kes 6 | 7 | import ( 8 | "context" 9 | "log/slog" 10 | 11 | "github.com/minio/kes/internal/api" 12 | ) 13 | 14 | // logHandler is an slog.Handler that handles Server log records. 15 | // 16 | // It wraps a custom slog.Handlers provided by Config.ErrorLog. If 17 | // Config.ErrorLog is nil, a slog.TextHandler to os.Stderr is used 18 | // as default. 19 | // 20 | // Log records may be handled twice. First, they are passed to the 21 | // custom/default handler. For example to write to standard error. 22 | // Second, they are sent to clients, that have subscribed to the 23 | // ErrorLog API, if any. 24 | type logHandler struct { 25 | h slog.Handler 26 | level slog.Leveler 27 | 28 | text slog.Handler 29 | out *api.Multicast // clients subscribed to the ErrorLog API 30 | } 31 | 32 | // newLogHandler returns a new logHandler that passing records to h. 33 | // 34 | // A record is only sent to clients subscribed to the ErrorLog API if 35 | // its log level is >= level. 36 | func newLogHandler(h slog.Handler, level slog.Leveler) *logHandler { 37 | handler := &logHandler{ 38 | h: h, 39 | level: level, 40 | out: &api.Multicast{}, 41 | } 42 | handler.text = slog.NewTextHandler(handler.out, &slog.HandlerOptions{ 43 | Level: level, 44 | }) 45 | return handler 46 | } 47 | 48 | // Enabled reports whether h handles records at the given level. 49 | func (h *logHandler) Enabled(ctx context.Context, level slog.Level) bool { 50 | return level >= h.level.Level() && h.h.Enabled(ctx, level) || 51 | (h.text.Enabled(ctx, level) && h.out.Num() > 0) 52 | } 53 | 54 | // Handle handles r by passing it first to the custom/default handler and 55 | // then sending it to all clients subscribed to the ErrorLog API. 56 | func (h *logHandler) Handle(ctx context.Context, r slog.Record) error { 57 | var err error 58 | if r.Level >= h.level.Level() { 59 | err = h.h.Handle(ctx, r) 60 | } 61 | if h.out.Num() > 0 && h.text.Enabled(ctx, r.Level) { 62 | if tErr := h.text.Handle(ctx, r); err == nil { 63 | err = tErr 64 | } 65 | } 66 | return err 67 | } 68 | 69 | // WithAttrs returns a new Handler whose attributes consist of 70 | // both the receiver's attributes and the arguments. 71 | // The Handler owns the slice: it may retain, modify or discard it. 72 | func (h *logHandler) WithAttrs(attrs []slog.Attr) slog.Handler { 73 | return &logHandler{ 74 | h: h.h.WithAttrs(attrs), 75 | text: h.text.WithAttrs(attrs), 76 | out: h.out, // Share all connections to clients 77 | } 78 | } 79 | 80 | // WithGroup returns a new Handler with the given group appended to 81 | // the receiver's existing groups. 82 | func (h *logHandler) WithGroup(name string) slog.Handler { 83 | return &logHandler{ 84 | h: h.h.WithGroup(name), 85 | text: h.text.WithGroup(name), 86 | out: h.out, // Share all connections to clients 87 | } 88 | } 89 | 90 | // Handler returns the underlying custom/default slog.Handler. 91 | func (h *logHandler) Handler() slog.Handler { return h.h } 92 | -------------------------------------------------------------------------------- /minisign.pub: -------------------------------------------------------------------------------- 1 | untrusted comment: minisign public key 2FD021B6F59AE5F1 2 | RWTx5Zr1tiHQLwG9keckT0c45M3AGeHD6IvimQHpyRywVWGbP1aVSGav 3 | -------------------------------------------------------------------------------- /root.cert: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIBKDCB26ADAgECAhB6vebGMUfKnmBKyqoApRSOMAUGAytlcDAbMRkwFwYDVQQD 3 | DBByb290QHBsYXkubWluLmlvMB4XDTIwMDQzMDE1MjIyNVoXDTI1MDQyOTE1MjIy 4 | NVowGzEZMBcGA1UEAwwQcm9vdEBwbGF5Lm1pbi5pbzAqMAUGAytlcAMhALzn735W 5 | fmSH/ghKs+4iPWziZMmWdiWr/sqvqeW+WwSxozUwMzAOBgNVHQ8BAf8EBAMCB4Aw 6 | EwYDVR0lBAwwCgYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADAFBgMrZXADQQDZOrGK 7 | b2ATkDlu2pTcP3LyhSBDpYh7V4TvjRkBTRgjkacCzwFLm+mh+7US8V4dBpIDsJ4u 8 | uWoF0y6vbLVGIlkG 9 | -----END CERTIFICATE----- 10 | -------------------------------------------------------------------------------- /root.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MC4CAQAwBQYDK2VwBCIEID9E7FSYWrMD+VjhI6q545cYT9YOyFxZb7UnjEepYDRc 3 | -----END PRIVATE KEY----- 4 | -------------------------------------------------------------------------------- /server_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 - MinIO, Inc. All rights reserved. 2 | // Use of this source code is governed by the AGPLv3 3 | // license that can be found in the LICENSE file. 4 | 5 | package kes 6 | 7 | import ( 8 | "context" 9 | "crypto/tls" 10 | "crypto/x509" 11 | "fmt" 12 | "log/slog" 13 | "net" 14 | "sync" 15 | "testing" 16 | "time" 17 | 18 | "github.com/minio/kms-go/kes" 19 | ) 20 | 21 | // Self-signed, valid from Oct. 10 2023 until Oct 10 2050 22 | const ( 23 | srvCertificate = `-----BEGIN CERTIFICATE----- 24 | MIIBlTCCATugAwIBAgIQVBb0Y6QgG4y/Uhsqr15ixDAKBggqhkjOPQQDAjAUMRIw 25 | EAYDVQQDEwlsb2NhbGhvc3QwIBcNMjMxMDEwMDAwMDAwWhgPMjA1MDEwMTAwMDAw 26 | MDBaMBQxEjAQBgNVBAMTCWxvY2FsaG9zdDBZMBMGByqGSM49AgEGCCqGSM49AwEH 27 | A0IABGSF1/2rUFcQSfd1SY3jBF82BY0MH77fDn7+aR7V8L1M5joDHBqR+TAoqS04 28 | GVIFrMC9vKSYuNVx5Pn0hfQ+Z92jbTBrMA4GA1UdDwEB/wQEAwIChDAdBgNVHSUE 29 | FjAUBggrBgEFBQcDAgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADAsBgNVHREEJTAj 30 | gglsb2NhbGhvc3SHBH8AAAGHEAAAAAAAAAAAAAAAAAAAAAEwCgYIKoZIzj0EAwID 31 | SAAwRQIhAPXQ9LRiCQZJruplDQnrRUt3OJxd9vhZQmmhbWC8zKMPAiB7sy46Fgrg 32 | DB5wr8jkeZpC5Inb1yjbyoHOD6sfQUdm9g== 33 | -----END CERTIFICATE-----` 34 | 35 | srvPrivateKey = `-----BEGIN PRIVATE KEY----- 36 | MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgj0xKJXLMx/S9dc5w 37 | dJ9Dm4+lX7qYfHRNGoJiF+DAbtKhRANCAARkhdf9q1BXEEn3dUmN4wRfNgWNDB++ 38 | 3w5+/mke1fC9TOY6AxwakfkwKKktOBlSBazAvbykmLjVceT59IX0Pmfd 39 | -----END PRIVATE KEY-----` 40 | ) 41 | 42 | const ( 43 | defaultAPIKey = "kes:v1:AD9E7FSYWrMD+VjhI6q545cYT9YOyFxZb7UnjEepYDRc" 44 | defaultIdentity = "3ecfcdf38fcbe141ae26a1030f81e96b753365a46760ae6b578698a97c59fd22" 45 | ) 46 | 47 | func startServer(ctx context.Context, conf *Config) (*Server, string) { 48 | ln := newLocalListener() 49 | 50 | if conf == nil { 51 | conf = &Config{} 52 | } 53 | if conf.Admin == "" { 54 | conf.Admin = defaultIdentity 55 | } 56 | if conf.TLS == nil { 57 | conf.TLS = &tls.Config{ 58 | MinVersion: tls.VersionTLS12, 59 | ClientAuth: tls.RequestClientCert, 60 | Certificates: []tls.Certificate{defaultServerCertificate()}, 61 | NextProtos: []string{"h2", "http/1.1"}, 62 | } 63 | } 64 | if conf.Cache == nil { 65 | conf.Cache = &CacheConfig{ 66 | Expiry: 5 * time.Minute, 67 | ExpiryUnused: 30 * time.Second, 68 | ExpiryOffline: 0, 69 | } 70 | } 71 | if conf.Keys == nil { 72 | conf.Keys = &MemKeyStore{} 73 | } 74 | if conf.ErrorLog == nil { 75 | conf.ErrorLog = discardLog{} 76 | } 77 | if conf.AuditLog == nil { 78 | conf.AuditLog = discardAudit{} 79 | } 80 | 81 | srv := &Server{ 82 | ShutdownTimeout: -1, // wait for all requests to finish 83 | } 84 | 85 | var wg sync.WaitGroup 86 | wg.Add(1) 87 | go func() { 88 | wg.Done() 89 | if err := srv.Start(ctx, ln, conf); err != nil { 90 | panic(fmt.Sprintf("serve failed: %v", err)) 91 | } 92 | }() 93 | wg.Wait() 94 | 95 | for srv.Addr() == "" { 96 | time.Sleep(5 * time.Microsecond) 97 | } 98 | return srv, "https://" + ln.Addr().String() 99 | } 100 | 101 | func testContext(t *testing.T) context.Context { 102 | if deadline, ok := t.Deadline(); ok { 103 | ctx, cancel := context.WithDeadline(context.Background(), deadline) 104 | t.Cleanup(cancel) 105 | return ctx 106 | 107 | } 108 | 109 | ctx, cancel := context.WithCancel(context.Background()) 110 | t.Cleanup(cancel) 111 | return ctx 112 | } 113 | 114 | func defaultClient(endpoint string) *kes.Client { 115 | adminKey, err := kes.ParseAPIKey(defaultAPIKey) 116 | if err != nil { 117 | panic(fmt.Sprintf("kes: failed to parse API key '%s': %v", defaultAPIKey, err)) 118 | } 119 | clientCert, err := kes.GenerateCertificate(adminKey) 120 | if err != nil { 121 | panic(fmt.Sprintf("kes: failed to generate client certificate: %v", err)) 122 | } 123 | 124 | rootCAs := x509.NewCertPool() 125 | rootCAs.AddCert(defaultServerCertificate().Leaf) 126 | 127 | return kes.NewClientWithConfig(endpoint, &tls.Config{ 128 | MinVersion: tls.VersionTLS12, 129 | RootCAs: rootCAs, 130 | GetClientCertificate: func(*tls.CertificateRequestInfo) (*tls.Certificate, error) { 131 | return &clientCert, nil 132 | }, 133 | }) 134 | } 135 | 136 | func newLocalListener() net.Listener { 137 | l, err := net.Listen("tcp", "127.0.0.1:0") 138 | if err != nil { 139 | if l, err = net.Listen("tcp6", "[::1]:0"); err != nil { 140 | panic(fmt.Sprintf("kes: failed to listen on a port: %v", err)) 141 | } 142 | } 143 | return l 144 | } 145 | 146 | func defaultServerCertificate() tls.Certificate { 147 | cert, err := tls.X509KeyPair([]byte(srvCertificate), []byte(srvPrivateKey)) 148 | if err != nil { 149 | panic(fmt.Sprintf("kes: failed to parse server certificate: %v", err)) 150 | } 151 | cert.Leaf, _ = x509.ParseCertificate(cert.Certificate[0]) 152 | return cert 153 | } 154 | 155 | type discardLog struct{} 156 | 157 | func (discardLog) Enabled(context.Context, slog.Level) bool { return false } 158 | 159 | func (discardLog) Handle(context.Context, slog.Record) error { return nil } 160 | 161 | func (h discardLog) WithAttrs([]slog.Attr) slog.Handler { return h } 162 | 163 | func (h discardLog) WithGroup(string) slog.Handler { return h } 164 | 165 | type discardAudit struct{} 166 | 167 | func (discardAudit) Enabled(context.Context, slog.Level) bool { return false } 168 | 169 | func (discardAudit) Handle(context.Context, AuditRecord) error { return nil } 170 | --------------------------------------------------------------------------------