├── .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 |
--------------------------------------------------------------------------------