diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..c3e90fe --- /dev/null +++ b/.dockerignore @@ -0,0 +1,13 @@ +config/*.yml +docs/ +examples/ +pkg/ +test/ +terraform/ + +*.md + +.dockerignore +.git +.gitignore +.github/ \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..c975c1d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,45 @@ +name: Bug Report +description: File a Bug Report +title: "[Bug]: " +labels: ["bug", "triage"] +assignees: + - yangkenneth +body: + - type: input + id: contact + attributes: + label: Contact Details + description: How can we get in touch with you if we need more info? + placeholder: ex. email@example.com + validations: + required: false + - type: textarea + id: explanation + attributes: + label: What happened? + description: Also tell us, what did you expect to happen? + placeholder: Tell us what you see! + validations: + required: true + - type: input + id: version + attributes: + label: Version + description: What release of baseca are you running? + placeholder: ex. 1.0.0 + validations: + required: true + - type: textarea + id: logs + attributes: + label: Relevant Log Output + description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. + render: shell + - type: checkboxes + id: terms + attributes: + label: Code of Conduct + description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/coinbase/code-of-conduct) + options: + - label: I agree to follow this project's Code of Conduct + required: true \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..73c952d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,54 @@ +name: Feature Request +description: Request a Feature +title: "[Feature]: " +labels: ["enhancement"] +assignees: + - yangkenneth +body: + - type: input + id: contact + attributes: + label: Contact Details + description: How can we get in touch with you if we need more info? + placeholder: ex. email@example.com + validations: + required: false + - type: textarea + id: background + attributes: + label: Background + description: Is your feature request related to a problem? Please describe. + placeholder: A clear and concise description of what the problem is. + validations: + required: true + - type: textarea + id: description + attributes: + label: Description + description: Describe the solution that you'd like implemented. + placeholder: A clear and concise description of what you want to happen. + validations: + required: true + - type: textarea + id: alternate + attributes: + label: Alternative Solution + description: Describe any alternatives that you've considered. + placeholder: A clear and concise description of any alternative solutions or features you've considered. + validations: + required: false + - type: textarea + id: addition + attributes: + label: Additional Context + description: Add any other context or screenshots about the feature request here. + validations: + required: false + - type: checkboxes + id: terms + attributes: + label: Code of Conduct + description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/coinbase/code-of-conduct) + options: + - label: I agree to follow this project's Code of Conduct + required: true \ No newline at end of file diff --git a/.github/workflows/pr_build.yml b/.github/workflows/pr_build.yml new file mode 100644 index 0000000..08ba97e --- /dev/null +++ b/.github/workflows/pr_build.yml @@ -0,0 +1,38 @@ +name: PR Build + +on: + pull_request: {} + + +jobs: + unit-test: + runs-on: ubuntu-latest + permissions: + contents: read + + steps: + - uses: actions/checkout@v3 + + - name: Setup Golang + uses: actions/setup-go@v4 + with: + go-version: '1.21' + + - name: Install Dependencies + run: | + sudo apt-get update + sudo apt-get install -y make + + - name: Build baseca Executable + run: make build + + - name: baseca Unit Test + run: make test + + - name: gosec Scanner + uses: securego/gosec@master + with: + args: -exclude-dir=examples ./... + + - name: Build baseca Container + run: docker build -f Dockerfile -t baseca . \ No newline at end of file diff --git a/.github/workflows/release_build.yml b/.github/workflows/release_build.yml new file mode 100644 index 0000000..50e06bd --- /dev/null +++ b/.github/workflows/release_build.yml @@ -0,0 +1,60 @@ +name: Release Build + +on: + push: + tags: [ 'v*' ] + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + + +jobs: + build: + + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + # Workaround: https://github.com/docker/build-push-action/issues/461 + - name: Setup Docker buildx + uses: docker/setup-buildx-action@79abd3f86f79a9d68a23c75a09a9a85889262adf + + # Login against a Docker registry except on PR + # https://github.com/docker/login-action + - name: Log into registry ${{ env.REGISTRY }} + if: github.event_name != 'pull_request' + uses: docker/login-action@28218f9b04b4f3f62068d7b6ce6ca5b26e35336c + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + # Extract metadata (tags, labels) for Docker + # https://github.com/docker/metadata-action + - name: Extract Docker Metadata + id: meta + uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=tag + type=sha + + # Build and push Docker image with Buildx (don't push on PR) + # https://github.com/docker/build-push-action + - name: Build and Push Docker Image + id: build-and-push + uses: docker/build-push-action@ac9327eae2b366085ac7f6a2d02df8aa8ead720a + with: + context: . + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..2445538 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,53 @@ +# Contributing to baseca + +## Code of Conduct + +All interactions with this project follow our [Code of Conduct][code-of-conduct]. +By participating, you are expected to honor this code. Violators can be banned +from further participation in this project, or potentially all Coinbase projects. + +[code-of-conduct]: https://github.com/coinbase/code-of-conduct + +## Bug Reports + +* Ensure your issue [has not already been reported][1]. It may already be fixed! +* Include the steps you carried out to produce the problem. +* Include the behavior you observed along with the behavior you expected, and + why you expected it. +* Include any relevant stack traces or debugging output. + +## Feature Requests + +We welcome feedback with or without pull requests. If you have an idea for how +to improve the project, great! All we ask is that you take the time to write a +clear and concise explanation of what need you are trying to solve. If you have +thoughts on _how_ it can be solved, include those too! + +The best way to see a feature added, however, is to submit a pull request. + +## Pull Requests + +* Before creating your pull request, it's usually worth asking if the code + you're planning on writing will actually be considered for merging. You can + do this by [opening an issue][1] and asking. It may also help give the + maintainers context for when the time comes to review your code. + +* Ensure your [commit messages are well-written][2]. This can double as your + pull request message, so it pays to take the time to write a clear message. + +* Add tests for your feature. You should be able to look at other tests for + examples. If you're unsure, don't hesitate to [open an issue][1] and ask! + +* Submit your pull request! + +## Support Requests + +For security reasons, any communication referencing support tickets for Coinbase +products will be ignored. The request will have its content redacted and will +be locked to prevent further discussion. + +All support requests must be made via [our support team][3]. + +[1]: https://github.com/coinbase/baseca/issues +[2]: https://medium.com/brigade-engineering/the-secrets-to-great-commit-messages-106fc0a92a25 +[3]: https://support.coinbase.com/customer/en/portal/articles/2288496-how-can-i-contact-coinbase-support \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..782d916 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,24 @@ +# Build Image +FROM golang:1.20 as builder + +ENV CGO_ENABLED=0 +WORKDIR /baseca +COPY . /baseca +RUN apt update && apt clean && make build + +# Deploy Image +FROM alpine:3.17 + +RUN adduser --home /home/baseca baseca --gecos "baseca" --disabled-password && \ + apk --no-cache add ca-certificates && \ + rm -rf /var/cache/apk/* + +COPY --from=builder /baseca/target/bin/linux/baseca /home/baseca/baseca +COPY --from=builder /baseca/config /home/baseca/config + +RUN chown -R baseca:baseca /home/baseca + +USER baseca +WORKDIR /home/baseca + +CMD ["/home/baseca/baseca"] \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..b963f81 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..d6c7316 --- /dev/null +++ b/Makefile @@ -0,0 +1,63 @@ +SERVICE := baseca +BUILD := $(shell git rev-parse --short HEAD) +GITHUB_REPO := github.com/coinbase/baseca + +TARGET=target +BIN=$(TARGET)/bin +LDFLAGS=-ldflags "-X=main.Version=$(VERSION) -X=main.Build=$(BUILD)" + +.PHONY: usage +usage: + @ echo "Usage: make [`cat Makefile | grep "^[A-z\%\-]*:" | awk '{print $$1}' | sed "s/://g" | sed "s/%/[1-3]/g" | xargs`]" + +.PHONY: info +info: + @ echo SERVICE: $(SERVICE) + @ echo BUILD: $(BUILD) + +.PHONY: clean +clean: info + @ rm -rf target + +.PHONY: dependencies +dependencies: info clean + @ go mod tidy + +.PHONY: test +test: info clean dependencies + @ go test -v -cover -short $$(go list ./... | grep -v /examples) + +.PHONY: build +build: info clean + @ GOOS=darwin GOARCH=amd64 go build $(LDFLAGS) -o $(BIN)/darwin/$(SERVICE) cmd/baseca/server.go + @ GOOS=linux GOARCH=amd64 go build $(LDFLAGS) -o $(BIN)/linux/$(SERVICE) cmd/baseca/server.go + +.PHONY: sqlc +sqlc: + @ sqlc generate -f db/sqlc.yaml + +.PHONY: mock +mock: + @ mockgen --build_flags=--mod=mod -package mock -destination db/mock/store.go ${GITHUB_REPO}/db/sqlc Store + +.PHONY: gen +gen: info clean + @ buf generate protos --template protos/buf.gen.yaml + +.PHONY: server +server: + @ database_credentials=${DATABASE_CREDENTIALS} \ + go run cmd/baseca/server.go + +.PHONY: lint +lint: + @ golangci-lint run + +.PHONY: tools +tools: + @ go install go.uber.org/mock/mockgen@latest + @ go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.28 + @ go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.2 + @ go install google.golang.org/protobuf/cmd/protoc-gen-go@latest + @ which buf || (go install github.com/bufbuild/buf/cmd/buf@latest) + diff --git a/README.md b/README.md index e88576d..d37f0d2 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,34 @@

coinbase/baseca

+ +[![Go Report Card](https://goreportcard.com/badge/github.com/coinbase/baseca)](https://goreportcard.com/report/github.com/coinbase/baseca) [![PR Build](https://github.com/coinbase/baseca/actions/workflows/pr_build.yml/badge.svg)](https://github.com/coinbase/baseca/actions/workflows/pr_build.yml) [![Release Build](https://github.com/coinbase/baseca/actions/workflows/release_build.yml/badge.svg)](https://github.com/coinbase/baseca/actions/workflows/release_build.yml) + +## Overview + +`baseca` is a `gRPC` service that serves as a Public Key Infrastructure (PKI) control plane intended to provide a safe and scalable approach to issue short-lived end-entities certificates. + +### Use Cases + +`baseca` extends the `pathlen` constraint from AWS Private CA and acts as an Intermediate CA; instead of issuing leaf certificates directly from Private CA, `baseca` manages many Subordinate CAs and signs requests in-memory depending on the [`scope`](docs/SCOPE.md) of the service account. + +- Client Authentication +- Server Authentication +- Code Signing +- SSH Certificates (Pending) + +### Running `baseca` + +- [`Architecture`](docs/ARCHITECTURE.md) +- [`Getting Started`](docs/GETTING_STARTED.md) +- [`Production Deployment`](docs/PRODUCTION_DEPLOYMENT.md) +- [`baseca gRPC Methods`](docs/ENDPOINTS.md) + +### Benefits + +- Short-Lived Certificates with Ephemeral Private Key Material +- No Quotas on Quantity of Issued Certificates +- Supports Issuance from On-Prem and Multi-Cloud +- Protects Issuance of Certificates on Scope +- Supports Node Attestation +- Cost Savings diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..76f7883 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,5 @@ +# Security Policy + +Please report your findings to our [HackerOne][1] program. + +[1]: https://hackerone.com/coinbase diff --git a/cmd/baseca/server.go b/cmd/baseca/server.go new file mode 100644 index 0000000..d876e9a --- /dev/null +++ b/cmd/baseca/server.go @@ -0,0 +1,44 @@ +package main + +import ( + "context" + "log" + "time" + + "github.com/coinbase/baseca/internal/config" + "github.com/coinbase/baseca/internal/gateway" + "github.com/coinbase/baseca/internal/lib/util/validator" + _ "github.com/lib/pq" + "go.uber.org/fx" + "go.uber.org/fx/fxevent" +) + +func main() { + + app := fx.New( + fx.Options( + config.Module, + gateway.Module, + validator.Module, + ), + fx.WithLogger( + func() fxevent.Logger { + return fxevent.NopLogger + }, + ), + ) + + startCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if err := app.Start(startCtx); err != nil { + log.Fatal(err) + } + + <-app.Done() + + stopCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if err := app.Stop(stopCtx); err != nil { + log.Fatal(err) + } +} diff --git a/config/certificate_authority/rds.global.bundle.pem b/config/certificate_authority/rds.global.bundle.pem new file mode 100644 index 0000000..dd5f955 --- /dev/null +++ b/config/certificate_authority/rds.global.bundle.pem @@ -0,0 +1,2876 @@ +-----BEGIN CERTIFICATE----- +MIIEEjCCAvqgAwIBAgIJAM2ZN/+nPi27MA0GCSqGSIb3DQEBCwUAMIGVMQswCQYD +VQQGEwJVUzEQMA4GA1UEBwwHU2VhdHRsZTETMBEGA1UECAwKV2FzaGluZ3RvbjEi +MCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywgSW5jLjETMBEGA1UECwwKQW1h +em9uIFJEUzEmMCQGA1UEAwwdQW1hem9uIFJEUyBhZi1zb3V0aC0xIFJvb3QgQ0Ew +HhcNMTkxMDI4MTgwNTU4WhcNMjQxMDI2MTgwNTU4WjCBlTELMAkGA1UEBhMCVVMx +EDAOBgNVBAcMB1NlYXR0bGUxEzARBgNVBAgMCldhc2hpbmd0b24xIjAgBgNVBAoM +GUFtYXpvbiBXZWIgU2VydmljZXMsIEluYy4xEzARBgNVBAsMCkFtYXpvbiBSRFMx +JjAkBgNVBAMMHUFtYXpvbiBSRFMgYWYtc291dGgtMSBSb290IENBMIIBIjANBgkq +hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwR2351uPMZaJk2gMGT+1sk8HE9MQh2rc +/sCnbxGn2p1c7Oi9aBbd/GiFijeJb2BXvHU+TOq3d3Jjqepq8tapXVt4ojbTJNyC +J5E7r7KjTktKdLxtBE1MK25aY+IRJjtdU6vG3KiPKUT1naO3xs3yt0F76WVuFivd +9OHv2a+KHvPkRUWIxpmAHuMY9SIIMmEZtVE7YZGx5ah0iO4JzItHcbVR0y0PBH55 +arpFBddpIVHCacp1FUPxSEWkOpI7q0AaU4xfX0fe1BV5HZYRKpBOIp1TtZWvJD+X +jGUtL1BEsT5vN5g9MkqdtYrC+3SNpAk4VtpvJrdjraI/hhvfeXNnAwIDAQABo2Mw +YTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUEEi/ +WWMcBJsoGXg+EZwkQ0MscZQwHwYDVR0jBBgwFoAUEEi/WWMcBJsoGXg+EZwkQ0Ms +cZQwDQYJKoZIhvcNAQELBQADggEBAGDZ5js5Pc/gC58LJrwMPXFhJDBS8QuDm23C +FFUdlqucskwOS3907ErK1ZkmVJCIqFLArHqskFXMAkRZ2PNR7RjWLqBs+0znG5yH +hRKb4DXzhUFQ18UBRcvT6V6zN97HTRsEEaNhM/7k8YLe7P8vfNZ28VIoJIGGgv9D +wQBBvkxQ71oOmAG0AwaGD0ORGUfbYry9Dz4a4IcUsZyRWRMADixgrFv6VuETp26s +/+z+iqNaGWlELBKh3iQCT6Y/1UnkPLO42bxrCSyOvshdkYN58Q2gMTE1SVTqyo8G +Lw8lLAz9bnvUSgHzB3jRrSx6ggF/WRMRYlR++y6LXP4SAsSAaC0= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEEjCCAvqgAwIBAgIJAJYM4LxvTZA6MA0GCSqGSIb3DQEBCwUAMIGVMQswCQYD +VQQGEwJVUzEQMA4GA1UEBwwHU2VhdHRsZTETMBEGA1UECAwKV2FzaGluZ3RvbjEi +MCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywgSW5jLjETMBEGA1UECwwKQW1h +em9uIFJEUzEmMCQGA1UEAwwdQW1hem9uIFJEUyBldS1zb3V0aC0xIFJvb3QgQ0Ew +HhcNMTkxMDMwMjAyMDM2WhcNMjQxMDI4MjAyMDM2WjCBlTELMAkGA1UEBhMCVVMx +EDAOBgNVBAcMB1NlYXR0bGUxEzARBgNVBAgMCldhc2hpbmd0b24xIjAgBgNVBAoM +GUFtYXpvbiBXZWIgU2VydmljZXMsIEluYy4xEzARBgNVBAsMCkFtYXpvbiBSRFMx +JjAkBgNVBAMMHUFtYXpvbiBSRFMgZXUtc291dGgtMSBSb290IENBMIIBIjANBgkq +hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqM921jXCXeqpRNCS9CBPOe5N7gMaEt+D +s5uR3riZbqzRlHGiF1jZihkXfHAIQewDwy+Yz+Oec1aEZCQMhUHxZJPusuX0cJfj +b+UluFqHIijL2TfXJ3D0PVLLoNTQJZ8+GAPECyojAaNuoHbdVqxhOcznMsXIXVFq +yVLKDGvyKkJjai/iSPDrQMXufg3kWt0ISjNLvsG5IFXgP4gttsM8i0yvRd4QcHoo +DjvH7V3cS+CQqW5SnDrGnHToB0RLskE1ET+oNOfeN9PWOxQprMOX/zmJhnJQlTqD +QP7jcf7SddxrKFjuziFiouskJJyNDsMjt1Lf60+oHZhed2ogTeifGwIDAQABo2Mw +YTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUFBAF +cgJe/BBuZiGeZ8STfpkgRYQwHwYDVR0jBBgwFoAUFBAFcgJe/BBuZiGeZ8STfpkg +RYQwDQYJKoZIhvcNAQELBQADggEBAKAYUtlvDuX2UpZW9i1QgsjFuy/ErbW0dLHU +e/IcFtju2z6RLZ+uF+5A8Kme7IKG1hgt8s+w9TRVQS/7ukQzoK3TaN6XKXRosjtc +o9Rm4gYWM8bmglzY1TPNaiI4HC7546hSwJhubjN0bXCuj/0sHD6w2DkiGuwKNAef +yTu5vZhPkeNyXLykxkzz7bNp2/PtMBnzIp+WpS7uUDmWyScGPohKMq5PqvL59z+L +ZI3CYeMZrJ5VpXUg3fNNIz/83N3G0sk7wr0ohs/kHTP7xPOYB0zD7Ku4HA0Q9Swf +WX0qr6UQgTPMjfYDLffI7aEId0gxKw1eGYc6Cq5JAZ3ipi/cBFc= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEEjCCAvqgAwIBAgIJANew34ehz5l8MA0GCSqGSIb3DQEBCwUAMIGVMQswCQYD +VQQGEwJVUzEQMA4GA1UEBwwHU2VhdHRsZTETMBEGA1UECAwKV2FzaGluZ3RvbjEi +MCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywgSW5jLjETMBEGA1UECwwKQW1h +em9uIFJEUzEmMCQGA1UEAwwdQW1hem9uIFJEUyBtZS1zb3V0aC0xIFJvb3QgQ0Ew +HhcNMTkwNTEwMjE0ODI3WhcNMjQwNTA4MjE0ODI3WjCBlTELMAkGA1UEBhMCVVMx +EDAOBgNVBAcMB1NlYXR0bGUxEzARBgNVBAgMCldhc2hpbmd0b24xIjAgBgNVBAoM +GUFtYXpvbiBXZWIgU2VydmljZXMsIEluYy4xEzARBgNVBAsMCkFtYXpvbiBSRFMx +JjAkBgNVBAMMHUFtYXpvbiBSRFMgbWUtc291dGgtMSBSb290IENBMIIBIjANBgkq +hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAp7BYV88MukcY+rq0r79+C8UzkT30fEfT +aPXbx1d6M7uheGN4FMaoYmL+JE1NZPaMRIPTHhFtLSdPccInvenRDIatcXX+jgOk +UA6lnHQ98pwN0pfDUyz/Vph4jBR9LcVkBbe0zdoKKp+HGbMPRU0N2yNrog9gM5O8 +gkU/3O2csJ/OFQNnj4c2NQloGMUpEmedwJMOyQQfcUyt9CvZDfIPNnheUS29jGSw +ERpJe/AENu8Pxyc72jaXQuD+FEi2Ck6lBkSlWYQFhTottAeGvVFNCzKszCntrtqd +rdYUwurYsLTXDHv9nW2hfDUQa0mhXf9gNDOBIVAZugR9NqNRNyYLHQIDAQABo2Mw +YTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU54cf +DjgwBx4ycBH8+/r8WXdaiqYwHwYDVR0jBBgwFoAU54cfDjgwBx4ycBH8+/r8WXda +iqYwDQYJKoZIhvcNAQELBQADggEBAIIMTSPx/dR7jlcxggr+O6OyY49Rlap2laKA +eC/XI4ySP3vQkIFlP822U9Kh8a9s46eR0uiwV4AGLabcu0iKYfXjPkIprVCqeXV7 +ny9oDtrbflyj7NcGdZLvuzSwgl9SYTJp7PVCZtZutsPYlbJrBPHwFABvAkMvRtDB +hitIg4AESDGPoCl94sYHpfDfjpUDMSrAMDUyO6DyBdZH5ryRMAs3lGtsmkkNUrso +aTW6R05681Z0mvkRdb+cdXtKOSuDZPoe2wJJIaz3IlNQNSrB5TImMYgmt6iAsFhv +3vfTSTKrZDNTJn4ybG6pq1zWExoXsktZPylJly6R3RBwV6nwqBM= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEBjCCAu6gAwIBAgIJAMc0ZzaSUK51MA0GCSqGSIb3DQEBCwUAMIGPMQswCQYD +VQQGEwJVUzEQMA4GA1UEBwwHU2VhdHRsZTETMBEGA1UECAwKV2FzaGluZ3RvbjEi +MCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywgSW5jLjETMBEGA1UECwwKQW1h +em9uIFJEUzEgMB4GA1UEAwwXQW1hem9uIFJEUyBSb290IDIwMTkgQ0EwHhcNMTkw +ODIyMTcwODUwWhcNMjQwODIyMTcwODUwWjCBjzELMAkGA1UEBhMCVVMxEDAOBgNV +BAcMB1NlYXR0bGUxEzARBgNVBAgMCldhc2hpbmd0b24xIjAgBgNVBAoMGUFtYXpv +biBXZWIgU2VydmljZXMsIEluYy4xEzARBgNVBAsMCkFtYXpvbiBSRFMxIDAeBgNV +BAMMF0FtYXpvbiBSRFMgUm9vdCAyMDE5IENBMIIBIjANBgkqhkiG9w0BAQEFAAOC +AQ8AMIIBCgKCAQEArXnF/E6/Qh+ku3hQTSKPMhQQlCpoWvnIthzX6MK3p5a0eXKZ +oWIjYcNNG6UwJjp4fUXl6glp53Jobn+tWNX88dNH2n8DVbppSwScVE2LpuL+94vY +0EYE/XxN7svKea8YvlrqkUBKyxLxTjh+U/KrGOaHxz9v0l6ZNlDbuaZw3qIWdD/I +6aNbGeRUVtpM6P+bWIoxVl/caQylQS6CEYUk+CpVyJSkopwJlzXT07tMoDL5WgX9 +O08KVgDNz9qP/IGtAcRduRcNioH3E9v981QO1zt/Gpb2f8NqAjUUCUZzOnij6mx9 +McZ+9cWX88CRzR0vQODWuZscgI08NvM69Fn2SQIDAQABo2MwYTAOBgNVHQ8BAf8E +BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUc19g2LzLA5j0Kxc0LjZa +pmD/vB8wHwYDVR0jBBgwFoAUc19g2LzLA5j0Kxc0LjZapmD/vB8wDQYJKoZIhvcN +AQELBQADggEBAHAG7WTmyjzPRIM85rVj+fWHsLIvqpw6DObIjMWokpliCeMINZFV +ynfgBKsf1ExwbvJNzYFXW6dihnguDG9VMPpi2up/ctQTN8tm9nDKOy08uNZoofMc +NUZxKCEkVKZv+IL4oHoeayt8egtv3ujJM6V14AstMQ6SwvwvA93EP/Ug2e4WAXHu +cbI1NAbUgVDqp+DRdfvZkgYKryjTWd/0+1fS8X1bBZVWzl7eirNVnHbSH2ZDpNuY +0SBd8dj5F6ld3t58ydZbrTHze7JJOd8ijySAp4/kiu9UfZWuTPABzDa/DSdz9Dk/ +zPW4CXXvhLmE02TA9/HeCw3KEHIwicNuEfw= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEEDCCAvigAwIBAgIJAKFMXyltvuRdMA0GCSqGSIb3DQEBCwUAMIGUMQswCQYD +VQQGEwJVUzEQMA4GA1UEBwwHU2VhdHRsZTETMBEGA1UECAwKV2FzaGluZ3RvbjEi +MCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywgSW5jLjETMBEGA1UECwwKQW1h +em9uIFJEUzElMCMGA1UEAwwcQW1hem9uIFJEUyBCZXRhIFJvb3QgMjAxOSBDQTAe +Fw0xOTA4MTkxNzM4MjZaFw0yNDA4MTkxNzM4MjZaMIGUMQswCQYDVQQGEwJVUzEQ +MA4GA1UEBwwHU2VhdHRsZTETMBEGA1UECAwKV2FzaGluZ3RvbjEiMCAGA1UECgwZ +QW1hem9uIFdlYiBTZXJ2aWNlcywgSW5jLjETMBEGA1UECwwKQW1hem9uIFJEUzEl +MCMGA1UEAwwcQW1hem9uIFJEUyBCZXRhIFJvb3QgMjAxOSBDQTCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAMkZdnIH9ndatGAcFo+DppGJ1HUt4x+zeO+0 +ZZ29m0sfGetVulmTlv2d5b66e+QXZFWpcPQMouSxxYTW08TbrQiZngKr40JNXftA +atvzBqIImD4II0ZX5UEVj2h98qe/ypW5xaDN7fEa5e8FkYB1TEemPaWIbNXqchcL +tV7IJPr3Cd7Z5gZJlmujIVDPpMuSiNaal9/6nT9oqN+JSM1fx5SzrU5ssg1Vp1vv +5Xab64uOg7wCJRB9R2GC9XD04odX6VcxUAGrZo6LR64ZSifupo3l+R5sVOc5i8NH +skdboTzU9H7+oSdqoAyhIU717PcqeDum23DYlPE2nGBWckE+eT8CAwEAAaNjMGEw +DgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFK2hDBWl +sbHzt/EHd0QYOooqcFPhMB8GA1UdIwQYMBaAFK2hDBWlsbHzt/EHd0QYOooqcFPh +MA0GCSqGSIb3DQEBCwUAA4IBAQAO/718k8EnOqJDx6wweUscGTGL/QdKXUzTVRAx +JUsjNUv49mH2HQVEW7oxszfH6cPCaupNAddMhQc4C/af6GHX8HnqfPDk27/yBQI+ +yBBvIanGgxv9c9wBbmcIaCEWJcsLp3HzXSYHmjiqkViXwCpYfkoV3Ns2m8bp+KCO +y9XmcCKRaXkt237qmoxoh2sGmBHk2UlQtOsMC0aUQ4d7teAJG0q6pbyZEiPyKZY1 +XR/UVxMJL0Q4iVpcRS1kaNCMfqS2smbLJeNdsan8pkw1dvPhcaVTb7CvjhJtjztF +YfDzAI5794qMlWxwilKMmUvDlPPOTen8NNHkLwWvyFCH7Doh +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEFjCCAv6gAwIBAgIJAMzYZJ+R9NBVMA0GCSqGSIb3DQEBCwUAMIGXMQswCQYD +VQQGEwJVUzEQMA4GA1UEBwwHU2VhdHRsZTETMBEGA1UECAwKV2FzaGluZ3RvbjEi +MCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywgSW5jLjETMBEGA1UECwwKQW1h +em9uIFJEUzEoMCYGA1UEAwwfQW1hem9uIFJEUyBQcmV2aWV3IFJvb3QgMjAxOSBD +QTAeFw0xOTA4MjEyMjI5NDlaFw0yNDA4MjEyMjI5NDlaMIGXMQswCQYDVQQGEwJV +UzEQMA4GA1UEBwwHU2VhdHRsZTETMBEGA1UECAwKV2FzaGluZ3RvbjEiMCAGA1UE +CgwZQW1hem9uIFdlYiBTZXJ2aWNlcywgSW5jLjETMBEGA1UECwwKQW1hem9uIFJE +UzEoMCYGA1UEAwwfQW1hem9uIFJEUyBQcmV2aWV3IFJvb3QgMjAxOSBDQTCCASIw +DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAM7kkS6vjgKKQTPynC2NjdN5aPPV +O71G0JJS/2ARVBVJd93JLiGovVJilfWYfwZCs4gTRSSjrUD4D4HyqCd6A+eEEtJq +M0DEC7i0dC+9WNTsPszuB206Jy2IUmxZMIKJAA1NHSbIMjB+b6/JhbSUi7nKdbR/ +brj83bF+RoSA+ogrgX7mQbxhmFcoZN9OGaJgYKsKWUt5Wqv627KkGodUK8mDepgD +S3ZfoRQRx3iceETpcmHJvaIge6+vyDX3d9Z22jmvQ4AKv3py2CmU2UwuhOltFDwB +0ddtb39vgwrJxaGfiMRHpEP1DfNLWHAnA69/pgZPwIggidS+iBPUhgucMp8CAwEA +AaNjMGEwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYE +FGnTGpQuQ2H/DZlXMQijZEhjs7TdMB8GA1UdIwQYMBaAFGnTGpQuQ2H/DZlXMQij +ZEhjs7TdMA0GCSqGSIb3DQEBCwUAA4IBAQC3xz1vQvcXAfpcZlngiRWeqU8zQAMQ +LZPCFNv7PVk4pmqX+ZiIRo4f9Zy7TrOVcboCnqmP/b/mNq0gVF4O+88jwXJZD+f8 +/RnABMZcnGU+vK0YmxsAtYU6TIb1uhRFmbF8K80HHbj9vSjBGIQdPCbvmR2zY6VJ +BYM+w9U9hp6H4DVMLKXPc1bFlKA5OBTgUtgkDibWJKFOEPW3UOYwp9uq6pFoN0AO +xMTldqWFsOF3bJIlvOY0c/1EFZXu3Ns6/oCP//Ap9vumldYMUZWmbK+gK33FPOXV +8BQ6jNC29icv7lLDpRPwjibJBXX+peDR5UK4FdYcswWEB1Tix5X8dYu6 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIECTCCAvGgAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwgZUxCzAJBgNVBAYTAlVT +MRAwDgYDVQQHDAdTZWF0dGxlMRMwEQYDVQQIDApXYXNoaW5ndG9uMSIwIAYDVQQK +DBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJbmMuMRMwEQYDVQQLDApBbWF6b24gUkRT +MSYwJAYDVQQDDB1BbWF6b24gUkRTIGFmLXNvdXRoLTEgUm9vdCBDQTAeFw0xOTEw +MjgxODA2NTNaFw0yNDEwMjgxODA2NTNaMIGQMQswCQYDVQQGEwJVUzETMBEGA1UE +CAwKV2FzaGluZ3RvbjEQMA4GA1UEBwwHU2VhdHRsZTEiMCAGA1UECgwZQW1hem9u +IFdlYiBTZXJ2aWNlcywgSW5jLjETMBEGA1UECwwKQW1hem9uIFJEUzEhMB8GA1UE +AwwYQW1hem9uIFJEUyBhZi1zb3V0aC0xIENBMIIBIjANBgkqhkiG9w0BAQEFAAOC +AQ8AMIIBCgKCAQEAvtV1OqmFa8zCVQSKOvPUJERLVFtd4rZmDpImc5rIoeBk7w/P +9lcKUJjO8R/w1a2lJXx3oQ81tiY0Piw6TpT62YWVRMWrOw8+Vxq1dNaDSFp9I8d0 +UHillSSbOk6FOrPDp+R6AwbGFqUDebbN5LFFoDKbhNmH1BVS0a6YNKpGigLRqhka +cClPslWtPqtjbaP3Jbxl26zWzLo7OtZl98dR225pq8aApNBwmtgA7Gh60HK/cX0t +32W94n8D+GKSg6R4MKredVFqRTi9hCCNUu0sxYPoELuM+mHiqB5NPjtm92EzCWs+ ++vgWhMc6GxG+82QSWx1Vj8sgLqtE/vLrWddf5QIDAQABo2YwZDAOBgNVHQ8BAf8E +BAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUuLB4gYVJrSKJj/Gz +pqc6yeA+RcAwHwYDVR0jBBgwFoAUEEi/WWMcBJsoGXg+EZwkQ0MscZQwDQYJKoZI +hvcNAQELBQADggEBABauYOZxUhe9/RhzGJ8MsWCz8eKcyDVd4FCnY6Qh+9wcmYNT +LtnD88LACtJKb/b81qYzcB0Em6+zVJ3Z9jznfr6buItE6es9wAoja22Xgv44BTHL +rimbgMwpTt3uEMXDffaS0Ww6YWb3pSE0XYI2ISMWz+xRERRf+QqktSaL39zuiaW5 +tfZMre+YhohRa/F0ZQl3RCd6yFcLx4UoSPqQsUl97WhYzwAxZZfwvLJXOc4ATt3u +VlCUylNDkaZztDJc/yN5XQoK9W5nOt2cLu513MGYKbuarQr8f+gYU8S+qOyuSRSP +NRITzwCRVnsJE+2JmcRInn/NcanB7uOGqTvJ9+c= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIECTCCAvGgAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwgZUxCzAJBgNVBAYTAlVT +MRAwDgYDVQQHDAdTZWF0dGxlMRMwEQYDVQQIDApXYXNoaW5ndG9uMSIwIAYDVQQK +DBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJbmMuMRMwEQYDVQQLDApBbWF6b24gUkRT +MSYwJAYDVQQDDB1BbWF6b24gUkRTIGV1LXNvdXRoLTEgUm9vdCBDQTAeFw0xOTEw +MzAyMDIxMzBaFw0yNDEwMzAyMDIxMzBaMIGQMQswCQYDVQQGEwJVUzETMBEGA1UE +CAwKV2FzaGluZ3RvbjEQMA4GA1UEBwwHU2VhdHRsZTEiMCAGA1UECgwZQW1hem9u +IFdlYiBTZXJ2aWNlcywgSW5jLjETMBEGA1UECwwKQW1hem9uIFJEUzEhMB8GA1UE +AwwYQW1hem9uIFJEUyBldS1zb3V0aC0xIENBMIIBIjANBgkqhkiG9w0BAQEFAAOC +AQ8AMIIBCgKCAQEAtEyjYcajx6xImJn8Vz1zjdmL4ANPgQXwF7+tF7xccmNAZETb +bzb3I9i5fZlmrRaVznX+9biXVaGxYzIUIR3huQ3Q283KsDYnVuGa3mk690vhvJbB +QIPgKa5mVwJppnuJm78KqaSpi0vxyCPe3h8h6LLFawVyWrYNZ4okli1/U582eef8 +RzJp/Ear3KgHOLIiCdPDF0rjOdCG1MOlDLixVnPn9IYOciqO+VivXBg+jtfc5J+L +AaPm0/Yx4uELt1tkbWkm4BvTU/gBOODnYziITZM0l6Fgwvbwgq5duAtKW+h031lC +37rEvrclqcp4wrsUYcLAWX79ZyKIlRxcAdvEhQIDAQABo2YwZDAOBgNVHQ8BAf8E +BAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQU7zPyc0azQxnBCe7D +b9KAadH1QSEwHwYDVR0jBBgwFoAUFBAFcgJe/BBuZiGeZ8STfpkgRYQwDQYJKoZI +hvcNAQELBQADggEBAFGaNiYxg7yC/xauXPlaqLCtwbm2dKyK9nIFbF/7be8mk7Q3 +MOA0of1vGHPLVQLr6bJJpD9MAbUcm4cPAwWaxwcNpxOjYOFDaq10PCK4eRAxZWwF +NJRIRmGsl8NEsMNTMCy8X+Kyw5EzH4vWFl5Uf2bGKOeFg0zt43jWQVOX6C+aL3Cd +pRS5MhmYpxMG8irrNOxf4NVFE2zpJOCm3bn0STLhkDcV/ww4zMzObTJhiIb5wSWn +EXKKWhUXuRt7A2y1KJtXpTbSRHQxE++69Go1tWhXtRiULCJtf7wF2Ksm0RR/AdXT +1uR1vKyH5KBJPX3ppYkQDukoHTFR0CpB+G84NLo= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIECTCCAvGgAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwgZUxCzAJBgNVBAYTAlVT +MRAwDgYDVQQHDAdTZWF0dGxlMRMwEQYDVQQIDApXYXNoaW5ndG9uMSIwIAYDVQQK +DBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJbmMuMRMwEQYDVQQLDApBbWF6b24gUkRT +MSYwJAYDVQQDDB1BbWF6b24gUkRTIG1lLXNvdXRoLTEgUm9vdCBDQTAeFw0xOTA1 +MTAyMTU4NDNaFw0yNTA2MDExMjAwMDBaMIGQMQswCQYDVQQGEwJVUzETMBEGA1UE +CAwKV2FzaGluZ3RvbjEQMA4GA1UEBwwHU2VhdHRsZTEiMCAGA1UECgwZQW1hem9u +IFdlYiBTZXJ2aWNlcywgSW5jLjETMBEGA1UECwwKQW1hem9uIFJEUzEhMB8GA1UE +AwwYQW1hem9uIFJEUyBtZS1zb3V0aC0xIENBMIIBIjANBgkqhkiG9w0BAQEFAAOC +AQ8AMIIBCgKCAQEAudOYPZH+ihJAo6hNYMB5izPVBe3TYhnZm8+X3IoaaYiKtsp1 +JJhkTT0CEejYIQ58Fh4QrMUyWvU8qsdK3diNyQRoYLbctsBPgxBR1u07eUJDv38/ +C1JlqgHmMnMi4y68Iy7ymv50QgAMuaBqgEBRI1R6Lfbyrb2YvH5txjJyTVMwuCfd +YPAtZVouRz0JxmnfsHyxjE+So56uOKTDuw++Ho4HhZ7Qveej7XB8b+PIPuroknd3 +FQB5RVbXRvt5ZcVD4F2fbEdBniF7FAF4dEiofVCQGQ2nynT7dZdEIPfPdH3n7ZmE +lAOmwHQ6G83OsiHRBLnbp+QZRgOsjkHJxT20bQIDAQABo2YwZDAOBgNVHQ8BAf8E +BAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUOEVDM7VomRH4HVdA +QvIMNq2tXOcwHwYDVR0jBBgwFoAU54cfDjgwBx4ycBH8+/r8WXdaiqYwDQYJKoZI +hvcNAQELBQADggEBAHhvMssj+Th8IpNePU6RH0BiL6o9c437R3Q4IEJeFdYL+nZz +PW/rELDPvLRUNMfKM+KzduLZ+l29HahxefejYPXtvXBlq/E/9czFDD4fWXg+zVou +uDXhyrV4kNmP4S0eqsAP/jQHPOZAMFA4yVwO9hlqmePhyDnszCh9c1PfJSBh49+b +4w7i/L3VBOMt8j3EKYvqz0gVfpeqhJwL4Hey8UbVfJRFJMJzfNHpePqtDRAY7yjV +PYquRaV2ab/E+/7VFkWMM4tazYz/qsYA2jSH+4xDHvYk8LnsbcrF9iuidQmEc5sb +FgcWaSKG4DJjcI5k7AJLWcXyTDt21Ci43LE+I9Q= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIECDCCAvCgAwIBAgICVIYwDQYJKoZIhvcNAQELBQAwgY8xCzAJBgNVBAYTAlVT +MRAwDgYDVQQHDAdTZWF0dGxlMRMwEQYDVQQIDApXYXNoaW5ndG9uMSIwIAYDVQQK +DBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJbmMuMRMwEQYDVQQLDApBbWF6b24gUkRT +MSAwHgYDVQQDDBdBbWF6b24gUkRTIFJvb3QgMjAxOSBDQTAeFw0xOTA5MDQxNzEz +MDRaFw0yNDA4MjIxNzA4NTBaMIGVMQswCQYDVQQGEwJVUzETMBEGA1UECAwKV2Fz +aGluZ3RvbjEQMA4GA1UEBwwHU2VhdHRsZTEiMCAGA1UECgwZQW1hem9uIFdlYiBT +ZXJ2aWNlcywgSW5jLjETMBEGA1UECwwKQW1hem9uIFJEUzEmMCQGA1UEAwwdQW1h +em9uIFJEUyBhcC1zb3V0aC0xIDIwMTkgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IB +DwAwggEKAoIBAQDUYOz1hGL42yUCrcsMSOoU8AeD/3KgZ4q7gP+vAz1WnY9K/kim +eWN/2Qqzlo3+mxSFQFyD4MyV3+CnCPnBl9Sh1G/F6kThNiJ7dEWSWBQGAB6HMDbC +BaAsmUc1UIz8sLTL3fO+S9wYhA63Wun0Fbm/Rn2yk/4WnJAaMZcEtYf6e0KNa0LM +p/kN/70/8cD3iz3dDR8zOZFpHoCtf0ek80QqTich0A9n3JLxR6g6tpwoYviVg89e +qCjQ4axxOkWWeusLeTJCcY6CkVyFvDAKvcUl1ytM5AiaUkXblE7zDFXRM4qMMRdt +lPm8d3pFxh0fRYk8bIKnpmtOpz3RIctDrZZxAgMBAAGjZjBkMA4GA1UdDwEB/wQE +AwIBBjASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBT99wKJftD3jb4sHoHG +i3uGlH6W6TAfBgNVHSMEGDAWgBRzX2DYvMsDmPQrFzQuNlqmYP+8HzANBgkqhkiG +9w0BAQsFAAOCAQEAZ17hhr3dII3hUfuHQ1hPWGrpJOX/G9dLzkprEIcCidkmRYl+ +hu1Pe3caRMh/17+qsoEErmnVq5jNY9X1GZL04IZH8YbHc7iRHw3HcWAdhN8633+K +jYEB2LbJ3vluCGnCejq9djDb6alOugdLMJzxOkHDhMZ6/gYbECOot+ph1tQuZXzD +tZ7prRsrcuPBChHlPjmGy8M9z8u+kF196iNSUGC4lM8vLkHM7ycc1/ZOwRq9aaTe +iOghbQQyAEe03MWCyDGtSmDfr0qEk+CHN+6hPiaL8qKt4s+V9P7DeK4iW08ny8Ox +AVS7u0OK/5+jKMAMrKwpYrBydOjTUTHScocyNw== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEBzCCAu+gAwIBAgICQ2QwDQYJKoZIhvcNAQELBQAwgY8xCzAJBgNVBAYTAlVT +MRAwDgYDVQQHDAdTZWF0dGxlMRMwEQYDVQQIDApXYXNoaW5ndG9uMSIwIAYDVQQK +DBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJbmMuMRMwEQYDVQQLDApBbWF6b24gUkRT +MSAwHgYDVQQDDBdBbWF6b24gUkRTIFJvb3QgMjAxOSBDQTAeFw0xOTA5MDUxODQ2 +MjlaFw0yNDA4MjIxNzA4NTBaMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECAwKV2Fz +aGluZ3RvbjEQMA4GA1UEBwwHU2VhdHRsZTEiMCAGA1UECgwZQW1hem9uIFdlYiBT +ZXJ2aWNlcywgSW5jLjETMBEGA1UECwwKQW1hem9uIFJEUzElMCMGA1UEAwwcQW1h +em9uIFJEUyBzYS1lYXN0LTEgMjAxOSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBAMMvR+ReRnOzqJzoaPipNTt1Z2VA968jlN1+SYKUrYM3No+Vpz0H +M6Tn0oYB66ByVsXiGc28ulsqX1HbHsxqDPwvQTKvO7SrmDokoAkjJgLocOLUAeld +5AwvUjxGRP6yY90NV7X786MpnYb2Il9DIIaV9HjCmPt+rjy2CZjS0UjPjCKNfB8J +bFjgW6GGscjeyGb/zFwcom5p4j0rLydbNaOr9wOyQrtt3ZQWLYGY9Zees/b8pmcc +Jt+7jstZ2UMV32OO/kIsJ4rMUn2r/uxccPwAc1IDeRSSxOrnFKhW3Cu69iB3bHp7 +JbawY12g7zshE4I14sHjv3QoXASoXjx4xgMCAwEAAaNmMGQwDgYDVR0PAQH/BAQD +AgEGMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFI1Fc/Ql2jx+oJPgBVYq +ccgP0pQ8MB8GA1UdIwQYMBaAFHNfYNi8ywOY9CsXNC42WqZg/7wfMA0GCSqGSIb3 +DQEBCwUAA4IBAQB4VVVabVp70myuYuZ3vltQIWqSUMhkaTzehMgGcHjMf9iLoZ/I +93KiFUSGnek5cRePyS9wcpp0fcBT3FvkjpUdCjVtdttJgZFhBxgTd8y26ImdDDMR +4+BUuhI5msvjL08f+Vkkpu1GQcGmyFVPFOy/UY8iefu+QyUuiBUnUuEDd49Hw0Fn +/kIPII6Vj82a2mWV/Q8e+rgN8dIRksRjKI03DEoP8lhPlsOkhdwU6Uz9Vu6NOB2Q +Ls1kbcxAc7cFSyRVJEhh12Sz9d0q/CQSTFsVJKOjSNQBQfVnLz1GwO/IieUEAr4C +jkTntH0r1LX5b/GwN4R887LvjAEdTbg1his7 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIECDCCAvCgAwIBAgIDAIkHMA0GCSqGSIb3DQEBCwUAMIGPMQswCQYDVQQGEwJV +UzEQMA4GA1UEBwwHU2VhdHRsZTETMBEGA1UECAwKV2FzaGluZ3RvbjEiMCAGA1UE +CgwZQW1hem9uIFdlYiBTZXJ2aWNlcywgSW5jLjETMBEGA1UECwwKQW1hem9uIFJE +UzEgMB4GA1UEAwwXQW1hem9uIFJEUyBSb290IDIwMTkgQ0EwHhcNMTkwOTA2MTc0 +MDIxWhcNMjQwODIyMTcwODUwWjCBlDELMAkGA1UEBhMCVVMxEzARBgNVBAgMCldh +c2hpbmd0b24xEDAOBgNVBAcMB1NlYXR0bGUxIjAgBgNVBAoMGUFtYXpvbiBXZWIg +U2VydmljZXMsIEluYy4xEzARBgNVBAsMCkFtYXpvbiBSRFMxJTAjBgNVBAMMHEFt +YXpvbiBSRFMgdXMtd2VzdC0xIDIwMTkgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IB +DwAwggEKAoIBAQDD2yzbbAl77OofTghDMEf624OvU0eS9O+lsdO0QlbfUfWa1Kd6 +0WkgjkLZGfSRxEHMCnrv4UPBSK/Qwn6FTjkDLgemhqBtAnplN4VsoDL+BkRX4Wwq +/dSQJE2b+0hm9w9UMVGFDEq1TMotGGTD2B71eh9HEKzKhGzqiNeGsiX4VV+LJzdH +uM23eGisNqmd4iJV0zcAZ+Gbh2zK6fqTOCvXtm7Idccv8vZZnyk1FiWl3NR4WAgK +AkvWTIoFU3Mt7dIXKKClVmvssG8WHCkd3Xcb4FHy/G756UZcq67gMMTX/9fOFM/v +l5C0+CHl33Yig1vIDZd+fXV1KZD84dEJfEvHAgMBAAGjZjBkMA4GA1UdDwEB/wQE +AwIBBjASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBR+ap20kO/6A7pPxo3+ +T3CfqZpQWjAfBgNVHSMEGDAWgBRzX2DYvMsDmPQrFzQuNlqmYP+8HzANBgkqhkiG +9w0BAQsFAAOCAQEAHCJky2tPjPttlDM/RIqExupBkNrnSYnOK4kr9xJ3sl8UF2DA +PAnYsjXp3rfcjN/k/FVOhxwzi3cXJF/2Tjj39Bm/OEfYTOJDNYtBwB0VVH4ffa/6 +tZl87jaIkrxJcreeeHqYMnIxeN0b/kliyA+a5L2Yb0VPjt9INq34QDc1v74FNZ17 +4z8nr1nzg4xsOWu0Dbjo966lm4nOYIGBRGOKEkHZRZ4mEiMgr3YLkv8gSmeitx57 +Z6dVemNtUic/LVo5Iqw4n3TBS0iF2C1Q1xT/s3h+0SXZlfOWttzSluDvoMv5PvCd +pFjNn+aXLAALoihL1MJSsxydtsLjOBro5eK0Vw== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEDDCCAvSgAwIBAgICOFAwDQYJKoZIhvcNAQELBQAwgY8xCzAJBgNVBAYTAlVT +MRAwDgYDVQQHDAdTZWF0dGxlMRMwEQYDVQQIDApXYXNoaW5ndG9uMSIwIAYDVQQK +DBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJbmMuMRMwEQYDVQQLDApBbWF6b24gUkRT +MSAwHgYDVQQDDBdBbWF6b24gUkRTIFJvb3QgMjAxOSBDQTAeFw0xOTA5MTAxNzQ2 +MjFaFw0yNDA4MjIxNzA4NTBaMIGZMQswCQYDVQQGEwJVUzETMBEGA1UECAwKV2Fz +aGluZ3RvbjEQMA4GA1UEBwwHU2VhdHRsZTEiMCAGA1UECgwZQW1hem9uIFdlYiBT +ZXJ2aWNlcywgSW5jLjETMBEGA1UECwwKQW1hem9uIFJEUzEqMCgGA1UEAwwhQW1h +em9uIFJEUyBhcC1ub3J0aGVhc3QtMiAyMDE5IENBMIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEAzU72e6XbaJbi4HjJoRNjKxzUEuChKQIt7k3CWzNnmjc5 +8I1MjCpa2W1iw1BYVysXSNSsLOtUsfvBZxi/1uyMn5ZCaf9aeoA9UsSkFSZBjOCN +DpKPCmfV1zcEOvJz26+1m8WDg+8Oa60QV0ou2AU1tYcw98fOQjcAES0JXXB80P2s +3UfkNcnDz+l4k7j4SllhFPhH6BQ4lD2NiFAP4HwoG6FeJUn45EPjzrydxjq6v5Fc +cQ8rGuHADVXotDbEhaYhNjIrsPL+puhjWfhJjheEw8c4whRZNp6gJ/b6WEes/ZhZ +h32DwsDsZw0BfRDUMgUn8TdecNexHUw8vQWeC181hwIDAQABo2YwZDAOBgNVHQ8B +Af8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUwW9bWgkWkr0U +lrOsq2kvIdrECDgwHwYDVR0jBBgwFoAUc19g2LzLA5j0Kxc0LjZapmD/vB8wDQYJ +KoZIhvcNAQELBQADggEBAEugF0Gj7HVhX0ehPZoGRYRt3PBuI2YjfrrJRTZ9X5wc +9T8oHmw07mHmNy1qqWvooNJg09bDGfB0k5goC2emDiIiGfc/kvMLI7u+eQOoMKj6 +mkfCncyRN3ty08Po45vTLBFZGUvtQmjM6yKewc4sXiASSBmQUpsMbiHRCL72M5qV +obcJOjGcIdDTmV1BHdWT+XcjynsGjUqOvQWWhhLPrn4jWe6Xuxll75qlrpn3IrIx +CRBv/5r7qbcQJPOgwQsyK4kv9Ly8g7YT1/vYBlR3cRsYQjccw5ceWUj2DrMVWhJ4 +prf+E3Aa4vYmLLOUUvKnDQ1k3RGNu56V0tonsQbfsaM= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIECjCCAvKgAwIBAgICEzUwDQYJKoZIhvcNAQELBQAwgY8xCzAJBgNVBAYTAlVT +MRAwDgYDVQQHDAdTZWF0dGxlMRMwEQYDVQQIDApXYXNoaW5ndG9uMSIwIAYDVQQK +DBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJbmMuMRMwEQYDVQQLDApBbWF6b24gUkRT +MSAwHgYDVQQDDBdBbWF6b24gUkRTIFJvb3QgMjAxOSBDQTAeFw0xOTA5MTAyMDUy +MjVaFw0yNDA4MjIxNzA4NTBaMIGXMQswCQYDVQQGEwJVUzETMBEGA1UECAwKV2Fz +aGluZ3RvbjEQMA4GA1UEBwwHU2VhdHRsZTEiMCAGA1UECgwZQW1hem9uIFdlYiBT +ZXJ2aWNlcywgSW5jLjETMBEGA1UECwwKQW1hem9uIFJEUzEoMCYGA1UEAwwfQW1h +em9uIFJEUyBjYS1jZW50cmFsLTEgMjAxOSBDQTCCASIwDQYJKoZIhvcNAQEBBQAD +ggEPADCCAQoCggEBAOxHqdcPSA2uBjsCP4DLSlqSoPuQ/X1kkJLusVRKiQE2zayB +viuCBt4VB9Qsh2rW3iYGM+usDjltGnI1iUWA5KHcvHszSMkWAOYWLiMNKTlg6LCp +XnE89tvj5dIH6U8WlDvXLdjB/h30gW9JEX7S8supsBSci2GxEzb5mRdKaDuuF/0O +qvz4YE04pua3iZ9QwmMFuTAOYzD1M72aOpj+7Ac+YLMM61qOtU+AU6MndnQkKoQi +qmUN2A9IFaqHFzRlSdXwKCKUA4otzmz+/N3vFwjb5F4DSsbsrMfjeHMo6o/nb6Nh +YDb0VJxxPee6TxSuN7CQJ2FxMlFUezcoXqwqXD0CAwEAAaNmMGQwDgYDVR0PAQH/ +BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFDGGpon9WfIpsggE +CxHq8hZ7E2ESMB8GA1UdIwQYMBaAFHNfYNi8ywOY9CsXNC42WqZg/7wfMA0GCSqG +SIb3DQEBCwUAA4IBAQAvpeQYEGZvoTVLgV9rd2+StPYykMsmFjWQcyn3dBTZRXC2 +lKq7QhQczMAOhEaaN29ZprjQzsA2X/UauKzLR2Uyqc2qOeO9/YOl0H3qauo8C/W9 +r8xqPbOCDLEXlOQ19fidXyyEPHEq5WFp8j+fTh+s8WOx2M7IuC0ANEetIZURYhSp +xl9XOPRCJxOhj7JdelhpweX0BJDNHeUFi0ClnFOws8oKQ7sQEv66d5ddxqqZ3NVv +RbCvCtEutQMOUMIuaygDlMn1anSM8N7Wndx8G6+Uy67AnhjGx7jw/0YPPxopEj6x +JXP8j0sJbcT9K/9/fPVLNT25RvQ/93T2+IQL4Ca2 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEBzCCAu+gAwIBAgICYpgwDQYJKoZIhvcNAQELBQAwgY8xCzAJBgNVBAYTAlVT +MRAwDgYDVQQHDAdTZWF0dGxlMRMwEQYDVQQIDApXYXNoaW5ndG9uMSIwIAYDVQQK +DBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJbmMuMRMwEQYDVQQLDApBbWF6b24gUkRT +MSAwHgYDVQQDDBdBbWF6b24gUkRTIFJvb3QgMjAxOSBDQTAeFw0xOTA5MTExNzMx +NDhaFw0yNDA4MjIxNzA4NTBaMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECAwKV2Fz +aGluZ3RvbjEQMA4GA1UEBwwHU2VhdHRsZTEiMCAGA1UECgwZQW1hem9uIFdlYiBT +ZXJ2aWNlcywgSW5jLjETMBEGA1UECwwKQW1hem9uIFJEUzElMCMGA1UEAwwcQW1h +em9uIFJEUyBldS13ZXN0LTEgMjAxOSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBAMk3YdSZ64iAYp6MyyKtYJtNzv7zFSnnNf6vv0FB4VnfITTMmOyZ +LXqKAT2ahZ00hXi34ewqJElgU6eUZT/QlzdIu359TEZyLVPwURflL6SWgdG01Q5X +O++7fSGcBRyIeuQWs9FJNIIqK8daF6qw0Rl5TXfu7P9dBc3zkgDXZm2DHmxGDD69 +7liQUiXzoE1q2Z9cA8+jirDioJxN9av8hQt12pskLQumhlArsMIhjhHRgF03HOh5 +tvi+RCfihVOxELyIRTRpTNiIwAqfZxxTWFTgfn+gijTmd0/1DseAe82aYic8JbuS +EMbrDduAWsqrnJ4GPzxHKLXX0JasCUcWyMECAwEAAaNmMGQwDgYDVR0PAQH/BAQD +AgEGMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFPLtsq1NrwJXO13C9eHt +sLY11AGwMB8GA1UdIwQYMBaAFHNfYNi8ywOY9CsXNC42WqZg/7wfMA0GCSqGSIb3 +DQEBCwUAA4IBAQAnWBKj5xV1A1mYd0kIgDdkjCwQkiKF5bjIbGkT3YEFFbXoJlSP +0lZZ/hDaOHI8wbLT44SzOvPEEmWF9EE7SJzkvSdQrUAWR9FwDLaU427ALI3ngNHy +lGJ2hse1fvSRNbmg8Sc9GBv8oqNIBPVuw+AJzHTacZ1OkyLZrz1c1QvwvwN2a+Jd +vH0V0YIhv66llKcYDMUQJAQi4+8nbRxXWv6Gq3pvrFoorzsnkr42V3JpbhnYiK+9 +nRKd4uWl62KRZjGkfMbmsqZpj2fdSWMY1UGyN1k+kDmCSWYdrTRDP0xjtIocwg+A +J116n4hV/5mbA0BaPiS2krtv17YAeHABZcvz +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIECjCCAvKgAwIBAgICV2YwDQYJKoZIhvcNAQELBQAwgY8xCzAJBgNVBAYTAlVT +MRAwDgYDVQQHDAdTZWF0dGxlMRMwEQYDVQQIDApXYXNoaW5ndG9uMSIwIAYDVQQK +DBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJbmMuMRMwEQYDVQQLDApBbWF6b24gUkRT +MSAwHgYDVQQDDBdBbWF6b24gUkRTIFJvb3QgMjAxOSBDQTAeFw0xOTA5MTExOTM2 +MjBaFw0yNDA4MjIxNzA4NTBaMIGXMQswCQYDVQQGEwJVUzETMBEGA1UECAwKV2Fz +aGluZ3RvbjEQMA4GA1UEBwwHU2VhdHRsZTEiMCAGA1UECgwZQW1hem9uIFdlYiBT +ZXJ2aWNlcywgSW5jLjETMBEGA1UECwwKQW1hem9uIFJEUzEoMCYGA1UEAwwfQW1h +em9uIFJEUyBldS1jZW50cmFsLTEgMjAxOSBDQTCCASIwDQYJKoZIhvcNAQEBBQAD +ggEPADCCAQoCggEBAMEx54X2pHVv86APA0RWqxxRNmdkhAyp2R1cFWumKQRofoFv +n+SPXdkpIINpMuEIGJANozdiEz7SPsrAf8WHyD93j/ZxrdQftRcIGH41xasetKGl +I67uans8d+pgJgBKGb/Z+B5m+UsIuEVekpvgpwKtmmaLFC/NCGuSsJoFsRqoa6Gh +m34W6yJoY87UatddCqLY4IIXaBFsgK9Q/wYzYLbnWM6ZZvhJ52VMtdhcdzeTHNW0 +5LGuXJOF7Ahb4JkEhoo6TS2c0NxB4l4MBfBPgti+O7WjR3FfZHpt18A6Zkq6A2u6 +D/oTSL6c9/3sAaFTFgMyL3wHb2YlW0BPiljZIqECAwEAAaNmMGQwDgYDVR0PAQH/ +BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFOcAToAc6skWffJa +TnreaswAfrbcMB8GA1UdIwQYMBaAFHNfYNi8ywOY9CsXNC42WqZg/7wfMA0GCSqG +SIb3DQEBCwUAA4IBAQA1d0Whc1QtspK496mFWfFEQNegLh0a9GWYlJm+Htcj5Nxt +DAIGXb+8xrtOZFHmYP7VLCT5Zd2C+XytqseK/+s07iAr0/EPF+O2qcyQWMN5KhgE +cXw2SwuP9FPV3i+YAm11PBVeenrmzuk9NrdHQ7TxU4v7VGhcsd2C++0EisrmquWH +mgIfmVDGxphwoES52cY6t3fbnXmTkvENvR+h3rj+fUiSz0aSo+XZUGHPgvuEKM/W +CBD9Smc9CBoBgvy7BgHRgRUmwtABZHFUIEjHI5rIr7ZvYn+6A0O6sogRfvVYtWFc +qpyrW1YX8mD0VlJ8fGKM3G+aCOsiiPKDV/Uafrm+ +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIECDCCAvCgAwIBAgICGAcwDQYJKoZIhvcNAQELBQAwgY8xCzAJBgNVBAYTAlVT +MRAwDgYDVQQHDAdTZWF0dGxlMRMwEQYDVQQIDApXYXNoaW5ndG9uMSIwIAYDVQQK +DBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJbmMuMRMwEQYDVQQLDApBbWF6b24gUkRT +MSAwHgYDVQQDDBdBbWF6b24gUkRTIFJvb3QgMjAxOSBDQTAeFw0xOTA5MTIxODE5 +NDRaFw0yNDA4MjIxNzA4NTBaMIGVMQswCQYDVQQGEwJVUzETMBEGA1UECAwKV2Fz +aGluZ3RvbjEQMA4GA1UEBwwHU2VhdHRsZTEiMCAGA1UECgwZQW1hem9uIFdlYiBT +ZXJ2aWNlcywgSW5jLjETMBEGA1UECwwKQW1hem9uIFJEUzEmMCQGA1UEAwwdQW1h +em9uIFJEUyBldS1ub3J0aC0xIDIwMTkgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IB +DwAwggEKAoIBAQCiIYnhe4UNBbdBb/nQxl5giM0XoVHWNrYV5nB0YukA98+TPn9v +Aoj1RGYmtryjhrf01Kuv8SWO+Eom95L3zquoTFcE2gmxCfk7bp6qJJ3eHOJB+QUO +XsNRh76fwDzEF1yTeZWH49oeL2xO13EAx4PbZuZpZBttBM5zAxgZkqu4uWQczFEs +JXfla7z2fvWmGcTagX10O5C18XaFroV0ubvSyIi75ue9ykg/nlFAeB7O0Wxae88e +uhiBEFAuLYdqWnsg3459NfV8Yi1GnaitTym6VI3tHKIFiUvkSiy0DAlAGV2iiyJE +q+DsVEO4/hSINJEtII4TMtysOsYPpINqeEzRAgMBAAGjZjBkMA4GA1UdDwEB/wQE +AwIBBjASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBRR0UpnbQyjnHChgmOc +hnlc0PogzTAfBgNVHSMEGDAWgBRzX2DYvMsDmPQrFzQuNlqmYP+8HzANBgkqhkiG +9w0BAQsFAAOCAQEAKJD4xVzSf4zSGTBJrmamo86jl1NHQxXUApAZuBZEc8tqC6TI +T5CeoSr9CMuVC8grYyBjXblC4OsM5NMvmsrXl/u5C9dEwtBFjo8mm53rOOIm1fxl +I1oYB/9mtO9ANWjkykuLzWeBlqDT/i7ckaKwalhLODsRDO73vRhYNjsIUGloNsKe +pxw3dzHwAZx4upSdEVG4RGCZ1D0LJ4Gw40OfD69hfkDfRVVxKGrbEzqxXRvovmDc +tKLdYZO/6REoca36v4BlgIs1CbUXJGLSXUwtg7YXGLSVBJ/U0+22iGJmBSNcoyUN +cjPFD9JQEhDDIYYKSGzIYpvslvGc4T5ISXFiuQ== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEBzCCAu+gAwIBAgICZIEwDQYJKoZIhvcNAQELBQAwgY8xCzAJBgNVBAYTAlVT +MRAwDgYDVQQHDAdTZWF0dGxlMRMwEQYDVQQIDApXYXNoaW5ndG9uMSIwIAYDVQQK +DBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJbmMuMRMwEQYDVQQLDApBbWF6b24gUkRT +MSAwHgYDVQQDDBdBbWF6b24gUkRTIFJvb3QgMjAxOSBDQTAeFw0xOTA5MTIyMTMy +MzJaFw0yNDA4MjIxNzA4NTBaMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECAwKV2Fz +aGluZ3RvbjEQMA4GA1UEBwwHU2VhdHRsZTEiMCAGA1UECgwZQW1hem9uIFdlYiBT +ZXJ2aWNlcywgSW5jLjETMBEGA1UECwwKQW1hem9uIFJEUzElMCMGA1UEAwwcQW1h +em9uIFJEUyBldS13ZXN0LTIgMjAxOSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBALGiwqjiF7xIjT0Sx7zB3764K2T2a1DHnAxEOr+/EIftWKxWzT3u +PFwS2eEZcnKqSdRQ+vRzonLBeNLO4z8aLjQnNbkizZMBuXGm4BqRm1Kgq3nlLDQn +7YqdijOq54SpShvR/8zsO4sgMDMmHIYAJJOJqBdaus2smRt0NobIKc0liy7759KB +6kmQ47Gg+kfIwxrQA5zlvPLeQImxSoPi9LdbRoKvu7Iot7SOa+jGhVBh3VdqndJX +7tm/saj4NE375csmMETFLAOXjat7zViMRwVorX4V6AzEg1vkzxXpA9N7qywWIT5Y +fYaq5M8i6vvLg0CzrH9fHORtnkdjdu1y+0MCAwEAAaNmMGQwDgYDVR0PAQH/BAQD +AgEGMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFFOhOx1yt3Z7mvGB9jBv +2ymdZwiOMB8GA1UdIwQYMBaAFHNfYNi8ywOY9CsXNC42WqZg/7wfMA0GCSqGSIb3 +DQEBCwUAA4IBAQBehqY36UGDvPVU9+vtaYGr38dBbp+LzkjZzHwKT1XJSSUc2wqM +hnCIQKilonrTIvP1vmkQi8qHPvDRtBZKqvz/AErW/ZwQdZzqYNFd+BmOXaeZWV0Q +oHtDzXmcwtP8aUQpxN0e1xkWb1E80qoy+0uuRqb/50b/R4Q5qqSfJhkn6z8nwB10 +7RjLtJPrK8igxdpr3tGUzfAOyiPrIDncY7UJaL84GFp7WWAkH0WG3H8Y8DRcRXOU +mqDxDLUP3rNuow3jnGxiUY+gGX5OqaZg4f4P6QzOSmeQYs6nLpH0PiN00+oS1BbD +bpWdZEttILPI+vAYkU4QuBKKDjJL6HbSd+cn +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIECDCCAvCgAwIBAgIDAIVCMA0GCSqGSIb3DQEBCwUAMIGPMQswCQYDVQQGEwJV +UzEQMA4GA1UEBwwHU2VhdHRsZTETMBEGA1UECAwKV2FzaGluZ3RvbjEiMCAGA1UE +CgwZQW1hem9uIFdlYiBTZXJ2aWNlcywgSW5jLjETMBEGA1UECwwKQW1hem9uIFJE +UzEgMB4GA1UEAwwXQW1hem9uIFJEUyBSb290IDIwMTkgQ0EwHhcNMTkwOTEzMTcw +NjQxWhcNMjQwODIyMTcwODUwWjCBlDELMAkGA1UEBhMCVVMxEzARBgNVBAgMCldh +c2hpbmd0b24xEDAOBgNVBAcMB1NlYXR0bGUxIjAgBgNVBAoMGUFtYXpvbiBXZWIg +U2VydmljZXMsIEluYy4xEzARBgNVBAsMCkFtYXpvbiBSRFMxJTAjBgNVBAMMHEFt +YXpvbiBSRFMgdXMtZWFzdC0yIDIwMTkgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IB +DwAwggEKAoIBAQDE+T2xYjUbxOp+pv+gRA3FO24+1zCWgXTDF1DHrh1lsPg5k7ht +2KPYzNc+Vg4E+jgPiW0BQnA6jStX5EqVh8BU60zELlxMNvpg4KumniMCZ3krtMUC +au1NF9rM7HBh+O+DYMBLK5eSIVt6lZosOb7bCi3V6wMLA8YqWSWqabkxwN4w0vXI +8lu5uXXFRemHnlNf+yA/4YtN4uaAyd0ami9+klwdkZfkrDOaiy59haOeBGL8EB/c +dbJJlguHH5CpCscs3RKtOOjEonXnKXldxarFdkMzi+aIIjQ8GyUOSAXHtQHb3gZ4 +nS6Ey0CMlwkB8vUObZU9fnjKJcL5QCQqOfwvAgMBAAGjZjBkMA4GA1UdDwEB/wQE +AwIBBjASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBQUPuRHohPxx4VjykmH +6usGrLL1ETAfBgNVHSMEGDAWgBRzX2DYvMsDmPQrFzQuNlqmYP+8HzANBgkqhkiG +9w0BAQsFAAOCAQEAUdR9Vb3y33Yj6X6KGtuthZ08SwjImVQPtknzpajNE5jOJAh8 +quvQnU9nlnMO85fVDU1Dz3lLHGJ/YG1pt1Cqq2QQ200JcWCvBRgdvH6MjHoDQpqZ +HvQ3vLgOGqCLNQKFuet9BdpsHzsctKvCVaeBqbGpeCtt3Hh/26tgx0rorPLw90A2 +V8QSkZJjlcKkLa58N5CMM8Xz8KLWg3MZeT4DmlUXVCukqK2RGuP2L+aME8dOxqNv +OnOz1zrL5mR2iJoDpk8+VE/eBDmJX40IJk6jBjWoxAO/RXq+vBozuF5YHN1ujE92 +tO8HItgTp37XT8bJBAiAnt5mxw+NLSqtxk2QdQ== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEDDCCAvSgAwIBAgICY4kwDQYJKoZIhvcNAQELBQAwgY8xCzAJBgNVBAYTAlVT +MRAwDgYDVQQHDAdTZWF0dGxlMRMwEQYDVQQIDApXYXNoaW5ndG9uMSIwIAYDVQQK +DBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJbmMuMRMwEQYDVQQLDApBbWF6b24gUkRT +MSAwHgYDVQQDDBdBbWF6b24gUkRTIFJvb3QgMjAxOSBDQTAeFw0xOTA5MTMyMDEx +NDJaFw0yNDA4MjIxNzA4NTBaMIGZMQswCQYDVQQGEwJVUzETMBEGA1UECAwKV2Fz +aGluZ3RvbjEQMA4GA1UEBwwHU2VhdHRsZTEiMCAGA1UECgwZQW1hem9uIFdlYiBT +ZXJ2aWNlcywgSW5jLjETMBEGA1UECwwKQW1hem9uIFJEUzEqMCgGA1UEAwwhQW1h +em9uIFJEUyBhcC1zb3V0aGVhc3QtMSAyMDE5IENBMIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEAr5u9OuLL/OF/fBNUX2kINJLzFl4DnmrhnLuSeSnBPgbb +qddjf5EFFJBfv7IYiIWEFPDbDG5hoBwgMup5bZDbas+ZTJTotnnxVJTQ6wlhTmns +eHECcg2pqGIKGrxZfbQhlj08/4nNAPvyYCTS0bEcmQ1emuDPyvJBYDDLDU6AbCB5 +6Z7YKFQPTiCBblvvNzchjLWF9IpkqiTsPHiEt21sAdABxj9ityStV3ja/W9BfgxH +wzABSTAQT6FbDwmQMo7dcFOPRX+hewQSic2Rn1XYjmNYzgEHisdUsH7eeXREAcTw +61TRvaLH8AiOWBnTEJXPAe6wYfrcSd1pD0MXpoB62wIDAQABo2YwZDAOBgNVHQ8B +Af8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUytwMiomQOgX5 +Ichd+2lDWRUhkikwHwYDVR0jBBgwFoAUc19g2LzLA5j0Kxc0LjZapmD/vB8wDQYJ +KoZIhvcNAQELBQADggEBACf6lRDpfCD7BFRqiWM45hqIzffIaysmVfr+Jr+fBTjP +uYe/ba1omSrNGG23bOcT9LJ8hkQJ9d+FxUwYyICQNWOy6ejicm4z0C3VhphbTPqj +yjpt9nG56IAcV8BcRJh4o/2IfLNzC/dVuYJV8wj7XzwlvjysenwdrJCoLadkTr1h +eIdG6Le07sB9IxrGJL9e04afk37h7c8ESGSE4E+oS4JQEi3ATq8ne1B9DQ9SasXi +IRmhNAaISDzOPdyLXi9N9V9Lwe/DHcja7hgLGYx3UqfjhLhOKwp8HtoZORixAmOI +HfILgNmwyugAbuZoCazSKKBhQ0wgO0WZ66ZKTMG8Oho= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEBzCCAu+gAwIBAgICUYkwDQYJKoZIhvcNAQELBQAwgY8xCzAJBgNVBAYTAlVT +MRAwDgYDVQQHDAdTZWF0dGxlMRMwEQYDVQQIDApXYXNoaW5ndG9uMSIwIAYDVQQK +DBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJbmMuMRMwEQYDVQQLDApBbWF6b24gUkRT +MSAwHgYDVQQDDBdBbWF6b24gUkRTIFJvb3QgMjAxOSBDQTAeFw0xOTA5MTYxODIx +MTVaFw0yNDA4MjIxNzA4NTBaMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECAwKV2Fz +aGluZ3RvbjEQMA4GA1UEBwwHU2VhdHRsZTEiMCAGA1UECgwZQW1hem9uIFdlYiBT +ZXJ2aWNlcywgSW5jLjETMBEGA1UECwwKQW1hem9uIFJEUzElMCMGA1UEAwwcQW1h +em9uIFJEUyB1cy13ZXN0LTIgMjAxOSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBANCEZBZyu6yJQFZBJmSUZfSZd3Ui2gitczMKC4FLr0QzkbxY+cLa +uVONIOrPt4Rwi+3h/UdnUg917xao3S53XDf1TDMFEYp4U8EFPXqCn/GXBIWlU86P +PvBN+gzw3nS+aco7WXb+woTouvFVkk8FGU7J532llW8o/9ydQyDIMtdIkKTuMfho +OiNHSaNc+QXQ32TgvM9A/6q7ksUoNXGCP8hDOkSZ/YOLiI5TcdLh/aWj00ziL5bj +pvytiMZkilnc9dLY9QhRNr0vGqL0xjmWdoEXz9/OwjmCihHqJq+20MJPsvFm7D6a +2NKybR9U+ddrjb8/iyLOjURUZnj5O+2+OPcCAwEAAaNmMGQwDgYDVR0PAQH/BAQD +AgEGMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFEBxMBdv81xuzqcK5TVu +pHj+Aor8MB8GA1UdIwQYMBaAFHNfYNi8ywOY9CsXNC42WqZg/7wfMA0GCSqGSIb3 +DQEBCwUAA4IBAQBZkfiVqGoJjBI37aTlLOSjLcjI75L5wBrwO39q+B4cwcmpj58P +3sivv+jhYfAGEbQnGRzjuFoyPzWnZ1DesRExX+wrmHsLLQbF2kVjLZhEJMHF9eB7 +GZlTPdTzHErcnuXkwA/OqyXMpj9aghcQFuhCNguEfnROY9sAoK2PTfnTz9NJHL+Q +UpDLEJEUfc0GZMVWYhahc0x38ZnSY2SKacIPECQrTI0KpqZv/P+ijCEcMD9xmYEb +jL4en+XKS1uJpw5fIU5Sj0MxhdGstH6S84iAE5J3GM3XHklGSFwwqPYvuTXvANH6 +uboynxRgSae59jIlAK6Jrr6GWMwQRbgcaAlW +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEDDCCAvSgAwIBAgICEkYwDQYJKoZIhvcNAQELBQAwgY8xCzAJBgNVBAYTAlVT +MRAwDgYDVQQHDAdTZWF0dGxlMRMwEQYDVQQIDApXYXNoaW5ndG9uMSIwIAYDVQQK +DBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJbmMuMRMwEQYDVQQLDApBbWF6b24gUkRT +MSAwHgYDVQQDDBdBbWF6b24gUkRTIFJvb3QgMjAxOSBDQTAeFw0xOTA5MTYxOTUz +NDdaFw0yNDA4MjIxNzA4NTBaMIGZMQswCQYDVQQGEwJVUzETMBEGA1UECAwKV2Fz +aGluZ3RvbjEQMA4GA1UEBwwHU2VhdHRsZTEiMCAGA1UECgwZQW1hem9uIFdlYiBT +ZXJ2aWNlcywgSW5jLjETMBEGA1UECwwKQW1hem9uIFJEUzEqMCgGA1UEAwwhQW1h +em9uIFJEUyBhcC1zb3V0aGVhc3QtMiAyMDE5IENBMIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEAufodI2Flker8q7PXZG0P0vmFSlhQDw907A6eJuF/WeMo +GHnll3b4S6nC3oRS3nGeRMHbyU2KKXDwXNb3Mheu+ox+n5eb/BJ17eoj9HbQR1cd +gEkIciiAltf8gpMMQH4anP7TD+HNFlZnP7ii3geEJB2GGXSxgSWvUzH4etL67Zmn +TpGDWQMB0T8lK2ziLCMF4XAC/8xDELN/buHCNuhDpxpPebhct0T+f6Arzsiswt2j +7OeNeLLZwIZvVwAKF7zUFjC6m7/VmTQC8nidVY559D6l0UhhU0Co/txgq3HVsMOH +PbxmQUwJEKAzQXoIi+4uZzHFZrvov/nDTNJUhC6DqwIDAQABo2YwZDAOBgNVHQ8B +Af8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUwaZpaCme+EiV +M5gcjeHZSTgOn4owHwYDVR0jBBgwFoAUc19g2LzLA5j0Kxc0LjZapmD/vB8wDQYJ +KoZIhvcNAQELBQADggEBAAR6a2meCZuXO2TF9bGqKGtZmaah4pH2ETcEVUjkvXVz +sl+ZKbYjrun+VkcMGGKLUjS812e7eDF726ptoku9/PZZIxlJB0isC/0OyixI8N4M +NsEyvp52XN9QundTjkl362bomPnHAApeU0mRbMDRR2JdT70u6yAzGLGsUwMkoNnw +1VR4XKhXHYGWo7KMvFrZ1KcjWhubxLHxZWXRulPVtGmyWg/MvE6KF+2XMLhojhUL ++9jB3Fpn53s6KMx5tVq1x8PukHmowcZuAF8k+W4gk8Y68wIwynrdZrKRyRv6CVtR +FZ8DeJgoNZT3y/GT254VqMxxfuy2Ccb/RInd16tEvVk= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEDDCCAvSgAwIBAgICOYIwDQYJKoZIhvcNAQELBQAwgY8xCzAJBgNVBAYTAlVT +MRAwDgYDVQQHDAdTZWF0dGxlMRMwEQYDVQQIDApXYXNoaW5ndG9uMSIwIAYDVQQK +DBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJbmMuMRMwEQYDVQQLDApBbWF6b24gUkRT +MSAwHgYDVQQDDBdBbWF6b24gUkRTIFJvb3QgMjAxOSBDQTAeFw0xOTA5MTcyMDA1 +MjlaFw0yNDA4MjIxNzA4NTBaMIGZMQswCQYDVQQGEwJVUzETMBEGA1UECAwKV2Fz +aGluZ3RvbjEQMA4GA1UEBwwHU2VhdHRsZTEiMCAGA1UECgwZQW1hem9uIFdlYiBT +ZXJ2aWNlcywgSW5jLjETMBEGA1UECwwKQW1hem9uIFJEUzEqMCgGA1UEAwwhQW1h +em9uIFJEUyBhcC1ub3J0aGVhc3QtMyAyMDE5IENBMIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEA4dMak8W+XW8y/2F6nRiytFiA4XLwePadqWebGtlIgyCS +kbug8Jv5w7nlMkuxOxoUeD4WhI6A9EkAn3r0REM/2f0aYnd2KPxeqS2MrtdxxHw1 +xoOxk2x0piNSlOz6yog1idsKR5Wurf94fvM9FdTrMYPPrDabbGqiBMsZZmoHLvA3 +Z+57HEV2tU0Ei3vWeGIqnNjIekS+E06KhASxrkNU5vi611UsnYZlSi0VtJsH4UGV +LhnHl53aZL0YFO5mn/fzuNG/51qgk/6EFMMhaWInXX49Dia9FnnuWXwVwi6uX1Wn +7kjoHi5VtmC8ZlGEHroxX2DxEr6bhJTEpcLMnoQMqwIDAQABo2YwZDAOBgNVHQ8B +Af8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUsUI5Cb3SWB8+ +gv1YLN/ABPMdxSAwHwYDVR0jBBgwFoAUc19g2LzLA5j0Kxc0LjZapmD/vB8wDQYJ +KoZIhvcNAQELBQADggEBAJAF3E9PM1uzVL8YNdzb6fwJrxxqI2shvaMVmC1mXS+w +G0zh4v2hBZOf91l1EO0rwFD7+fxoI6hzQfMxIczh875T6vUXePKVOCOKI5wCrDad +zQbVqbFbdhsBjF4aUilOdtw2qjjs9JwPuB0VXN4/jY7m21oKEOcnpe36+7OiSPjN +xngYewCXKrSRqoj3mw+0w/+exYj3Wsush7uFssX18av78G+ehKPIVDXptOCP/N7W +8iKVNeQ2QGTnu2fzWsGUSvMGyM7yqT+h1ILaT//yQS8er511aHMLc142bD4D9VSy +DgactwPDTShK/PXqhvNey9v/sKXm4XatZvwcc8KYlW4= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEDDCCAvSgAwIBAgICcEUwDQYJKoZIhvcNAQELBQAwgY8xCzAJBgNVBAYTAlVT +MRAwDgYDVQQHDAdTZWF0dGxlMRMwEQYDVQQIDApXYXNoaW5ndG9uMSIwIAYDVQQK +DBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJbmMuMRMwEQYDVQQLDApBbWF6b24gUkRT +MSAwHgYDVQQDDBdBbWF6b24gUkRTIFJvb3QgMjAxOSBDQTAeFw0xOTA5MTgxNjU2 +MjBaFw0yNDA4MjIxNzA4NTBaMIGZMQswCQYDVQQGEwJVUzETMBEGA1UECAwKV2Fz +aGluZ3RvbjEQMA4GA1UEBwwHU2VhdHRsZTEiMCAGA1UECgwZQW1hem9uIFdlYiBT +ZXJ2aWNlcywgSW5jLjETMBEGA1UECwwKQW1hem9uIFJEUzEqMCgGA1UEAwwhQW1h +em9uIFJEUyBhcC1ub3J0aGVhc3QtMSAyMDE5IENBMIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEAndtkldmHtk4TVQAyqhAvtEHSMb6pLhyKrIFved1WO3S7 ++I+bWwv9b2W/ljJxLq9kdT43bhvzonNtI4a1LAohS6bqyirmk8sFfsWT3akb+4Sx +1sjc8Ovc9eqIWJCrUiSvv7+cS7ZTA9AgM1PxvHcsqrcUXiK3Jd/Dax9jdZE1e15s +BEhb2OEPE+tClFZ+soj8h8Pl2Clo5OAppEzYI4LmFKtp1X/BOf62k4jviXuCSst3 +UnRJzE/CXtjmN6oZySVWSe0rQYuyqRl6//9nK40cfGKyxVnimB8XrrcxUN743Vud +QQVU0Esm8OVTX013mXWQXJHP2c0aKkog8LOga0vobQIDAQABo2YwZDAOBgNVHQ8B +Af8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQULmoOS1mFSjj+ +snUPx4DgS3SkLFYwHwYDVR0jBBgwFoAUc19g2LzLA5j0Kxc0LjZapmD/vB8wDQYJ +KoZIhvcNAQELBQADggEBAAkVL2P1M2/G9GM3DANVAqYOwmX0Xk58YBHQu6iiQg4j +b4Ky/qsZIsgT7YBsZA4AOcPKQFgGTWhe9pvhmXqoN3RYltN8Vn7TbUm/ZVDoMsrM +gwv0+TKxW1/u7s8cXYfHPiTzVSJuOogHx99kBW6b2f99GbP7O1Sv3sLq4j6lVvBX +Fiacf5LAWC925nvlTzLlBgIc3O9xDtFeAGtZcEtxZJ4fnGXiqEnN4539+nqzIyYq +nvlgCzyvcfRAxwltrJHuuRu6Maw5AGcd2Y0saMhqOVq9KYKFKuD/927BTrbd2JVf +2sGWyuPZPCk3gq+5pCjbD0c6DkhcMGI6WwxvM5V/zSM= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEBzCCAu+gAwIBAgICJDQwDQYJKoZIhvcNAQELBQAwgY8xCzAJBgNVBAYTAlVT +MRAwDgYDVQQHDAdTZWF0dGxlMRMwEQYDVQQIDApXYXNoaW5ndG9uMSIwIAYDVQQK +DBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJbmMuMRMwEQYDVQQLDApBbWF6b24gUkRT +MSAwHgYDVQQDDBdBbWF6b24gUkRTIFJvb3QgMjAxOSBDQTAeFw0xOTA5MTgxNzAz +MTVaFw0yNDA4MjIxNzA4NTBaMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECAwKV2Fz +aGluZ3RvbjEQMA4GA1UEBwwHU2VhdHRsZTEiMCAGA1UECgwZQW1hem9uIFdlYiBT +ZXJ2aWNlcywgSW5jLjETMBEGA1UECwwKQW1hem9uIFJEUzElMCMGA1UEAwwcQW1h +em9uIFJEUyBldS13ZXN0LTMgMjAxOSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBAL9bL7KE0n02DLVtlZ2PL+g/BuHpMYFq2JnE2RgompGurDIZdjmh +1pxfL3nT+QIVMubuAOy8InRfkRxfpxyjKYdfLJTPJG+jDVL+wDcPpACFVqoV7Prg +pVYEV0lc5aoYw4bSeYFhdzgim6F8iyjoPnObjll9mo4XsHzSoqJLCd0QC+VG9Fw2 +q+GDRZrLRmVM2oNGDRbGpGIFg77aRxRapFZa8SnUgs2AqzuzKiprVH5i0S0M6dWr +i+kk5epmTtkiDHceX+dP/0R1NcnkCPoQ9TglyXyPdUdTPPRfKCq12dftqll+u4mV +ARdN6WFjovxax8EAP2OAUTi1afY+1JFMj+sCAwEAAaNmMGQwDgYDVR0PAQH/BAQD +AgEGMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFLfhrbrO5exkCVgxW0x3 +Y2mAi8lNMB8GA1UdIwQYMBaAFHNfYNi8ywOY9CsXNC42WqZg/7wfMA0GCSqGSIb3 +DQEBCwUAA4IBAQAigQ5VBNGyw+OZFXwxeJEAUYaXVoP/qrhTOJ6mCE2DXUVEoJeV +SxScy/TlFA9tJXqmit8JH8VQ/xDL4ubBfeMFAIAo4WzNWDVoeVMqphVEcDWBHsI1 +AETWzfsapRS9yQekOMmxg63d/nV8xewIl8aNVTHdHYXMqhhik47VrmaVEok1UQb3 +O971RadLXIEbVd9tjY5bMEHm89JsZDnDEw1hQXBb67Elu64OOxoKaHBgUH8AZn/2 +zFsL1ynNUjOhCSAA15pgd1vjwc0YsBbAEBPcHBWYBEyME6NLNarjOzBl4FMtATSF +wWCKRGkvqN8oxYhwR2jf2rR5Mu4DWkK5Q8Ep +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEBzCCAu+gAwIBAgICJVUwDQYJKoZIhvcNAQELBQAwgY8xCzAJBgNVBAYTAlVT +MRAwDgYDVQQHDAdTZWF0dGxlMRMwEQYDVQQIDApXYXNoaW5ndG9uMSIwIAYDVQQK +DBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJbmMuMRMwEQYDVQQLDApBbWF6b24gUkRT +MSAwHgYDVQQDDBdBbWF6b24gUkRTIFJvb3QgMjAxOSBDQTAeFw0xOTA5MTkxODE2 +NTNaFw0yNDA4MjIxNzA4NTBaMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECAwKV2Fz +aGluZ3RvbjEQMA4GA1UEBwwHU2VhdHRsZTEiMCAGA1UECgwZQW1hem9uIFdlYiBT +ZXJ2aWNlcywgSW5jLjETMBEGA1UECwwKQW1hem9uIFJEUzElMCMGA1UEAwwcQW1h +em9uIFJEUyB1cy1lYXN0LTEgMjAxOSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBAM3i/k2u6cqbMdcISGRvh+m+L0yaSIoOXjtpNEoIftAipTUYoMhL +InXGlQBVA4shkekxp1N7HXe1Y/iMaPEyb3n+16pf3vdjKl7kaSkIhjdUz3oVUEYt +i8Z/XeJJ9H2aEGuiZh3kHixQcZczn8cg3dA9aeeyLSEnTkl/npzLf//669Ammyhs +XcAo58yvT0D4E0D/EEHf2N7HRX7j/TlyWvw/39SW0usiCrHPKDLxByLojxLdHzso +QIp/S04m+eWn6rmD+uUiRteN1hI5ncQiA3wo4G37mHnUEKo6TtTUh+sd/ku6a8HK +glMBcgqudDI90s1OpuIAWmuWpY//8xEG2YECAwEAAaNmMGQwDgYDVR0PAQH/BAQD +AgEGMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFPqhoWZcrVY9mU7tuemR +RBnQIj1jMB8GA1UdIwQYMBaAFHNfYNi8ywOY9CsXNC42WqZg/7wfMA0GCSqGSIb3 +DQEBCwUAA4IBAQB6zOLZ+YINEs72heHIWlPZ8c6WY8MDU+Be5w1M+BK2kpcVhCUK +PJO4nMXpgamEX8DIiaO7emsunwJzMSvavSPRnxXXTKIc0i/g1EbiDjnYX9d85DkC +E1LaAUCmCZBVi9fIe0H2r9whIh4uLWZA41oMnJx/MOmo3XyMfQoWcqaSFlMqfZM4 +0rNoB/tdHLNuV4eIdaw2mlHxdWDtF4oH+HFm+2cVBUVC1jXKrFv/euRVtsTT+A6i +h2XBHKxQ1Y4HgAn0jACP2QSPEmuoQEIa57bEKEcZsBR8SDY6ZdTd2HLRIApcCOSF +MRM8CKLeF658I0XgF8D5EsYoKPsA+74Z+jDH +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEETCCAvmgAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwgZQxCzAJBgNVBAYTAlVT +MRAwDgYDVQQHDAdTZWF0dGxlMRMwEQYDVQQIDApXYXNoaW5ndG9uMSIwIAYDVQQK +DBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJbmMuMRMwEQYDVQQLDApBbWF6b24gUkRT +MSUwIwYDVQQDDBxBbWF6b24gUkRTIEJldGEgUm9vdCAyMDE5IENBMB4XDTE5MDgy +MDE3MTAwN1oXDTI0MDgxOTE3MzgyNlowgZkxCzAJBgNVBAYTAlVTMRMwEQYDVQQI +DApXYXNoaW5ndG9uMRAwDgYDVQQHDAdTZWF0dGxlMSIwIAYDVQQKDBlBbWF6b24g +V2ViIFNlcnZpY2VzLCBJbmMuMRMwEQYDVQQLDApBbWF6b24gUkRTMSowKAYDVQQD +DCFBbWF6b24gUkRTIEJldGEgdXMtZWFzdC0xIDIwMTkgQ0EwggEiMA0GCSqGSIb3 +DQEBAQUAA4IBDwAwggEKAoIBAQDTNCOlotQcLP8TP82U2+nk0bExVuuMVOgFeVMx +vbUHZQeIj9ikjk+jm6eTDnnkhoZcmJiJgRy+5Jt69QcRbb3y3SAU7VoHgtraVbxF +QDh7JEHI9tqEEVOA5OvRrDRcyeEYBoTDgh76ROco2lR+/9uCvGtHVrMCtG7BP7ZB +sSVNAr1IIRZZqKLv2skKT/7mzZR2ivcw9UeBBTUf8xsfiYVBvMGoEsXEycjYdf6w +WV+7XS7teNOc9UgsFNN+9AhIBc1jvee5E//72/4F8pAttAg/+mmPUyIKtekNJ4gj +OAR2VAzGx1ybzWPwIgOudZFHXFduxvq4f1hIRPH0KbQ/gkRrAgMBAAGjZjBkMA4G +A1UdDwEB/wQEAwIBBjASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBTkvpCD +6C43rar9TtJoXr7q8dkrrjAfBgNVHSMEGDAWgBStoQwVpbGx87fxB3dEGDqKKnBT +4TANBgkqhkiG9w0BAQsFAAOCAQEAJd9fOSkwB3uVdsS+puj6gCER8jqmhd3g/J5V +Zjk9cKS8H0e8pq/tMxeJ8kpurPAzUk5RkCspGt2l0BSwmf3ahr8aJRviMX6AuW3/ +g8aKplTvq/WMNGKLXONa3Sq8591J+ce8gtOX/1rDKmFI4wQ/gUzOSYiT991m7QKS +Fr6HMgFuz7RNJbb3Fy5cnurh8eYWA7mMv7laiLwTNsaro5qsqErD5uXuot6o9beT +a+GiKinEur35tNxAr47ax4IRubuIzyfCrezjfKc5raVV2NURJDyKP0m0CCaffAxE +qn2dNfYc3v1D8ypg3XjHlOzRo32RB04o8ALHMD9LSwsYDLpMag== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEFzCCAv+gAwIBAgICFSUwDQYJKoZIhvcNAQELBQAwgZcxCzAJBgNVBAYTAlVT +MRAwDgYDVQQHDAdTZWF0dGxlMRMwEQYDVQQIDApXYXNoaW5ndG9uMSIwIAYDVQQK +DBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJbmMuMRMwEQYDVQQLDApBbWF6b24gUkRT +MSgwJgYDVQQDDB9BbWF6b24gUkRTIFByZXZpZXcgUm9vdCAyMDE5IENBMB4XDTE5 +MDgyMTIyMzk0N1oXDTI0MDgyMTIyMjk0OVowgZwxCzAJBgNVBAYTAlVTMRMwEQYD +VQQIDApXYXNoaW5ndG9uMRAwDgYDVQQHDAdTZWF0dGxlMSIwIAYDVQQKDBlBbWF6 +b24gV2ViIFNlcnZpY2VzLCBJbmMuMRMwEQYDVQQLDApBbWF6b24gUkRTMS0wKwYD +VQQDDCRBbWF6b24gUkRTIFByZXZpZXcgdXMtZWFzdC0yIDIwMTkgQ0EwggEiMA0G +CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQD0dB/U7qRnSf05wOi7m10Pa2uPMTJv +r6U/3Y17a5prq5Zr4++CnSUYarG51YuIf355dKs+7Lpzs782PIwCmLpzAHKWzix6 +pOaTQ+WZ0+vUMTxyqgqWbsBgSCyP7pVBiyqnmLC/L4az9XnscrbAX4pNaoJxsuQe +mzBo6yofjQaAzCX69DuqxFkVTRQnVy7LCFkVaZtjNAftnAHJjVgQw7lIhdGZp9q9 +IafRt2gteihYfpn+EAQ/t/E4MnhrYs4CPLfS7BaYXBycEKC5Muj1l4GijNNQ0Efo +xG8LSZz7SNgUvfVwiNTaqfLP3AtEAWiqxyMyh3VO+1HpCjT7uNBFtmF3AgMBAAGj +ZjBkMA4GA1UdDwEB/wQEAwIBBjASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQW +BBQtinkdrj+0B2+qdXngV2tgHnPIujAfBgNVHSMEGDAWgBRp0xqULkNh/w2ZVzEI +o2RIY7O03TANBgkqhkiG9w0BAQsFAAOCAQEAtJdqbCxDeMc8VN1/RzCabw9BIL/z +73Auh8eFTww/sup26yn8NWUkfbckeDYr1BrXa+rPyLfHpg06kwR8rBKyrs5mHwJx +bvOzXD/5WTdgreB+2Fb7mXNvWhenYuji1MF+q1R2DXV3I05zWHteKX6Dajmx+Uuq +Yq78oaCBSV48hMxWlp8fm40ANCL1+gzQ122xweMFN09FmNYFhwuW+Ao+Vv90ZfQG +PYwTvN4n/gegw2TYcifGZC2PNX74q3DH03DXe5fvNgRW5plgz/7f+9mS+YHd5qa9 +tYTPUvoRbi169ou6jicsMKUKPORHWhiTpSCWR1FMMIbsAcsyrvtIsuaGCQ== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIID/jCCAuagAwIBAgIQdOCSuA9psBpQd8EI368/0DANBgkqhkiG9w0BAQsFADCB +lzELMAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIElu +Yy4xEzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTAwLgYDVQQDDCdB +bWF6b24gUkRTIHNhLWVhc3QtMSBSb290IENBIFJTQTIwNDggRzExEDAOBgNVBAcM +B1NlYXR0bGUwIBcNMjEwNTE5MTgwNjI2WhgPMjA2MTA1MTkxOTA2MjZaMIGXMQsw +CQYDVQQGEwJVUzEiMCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywgSW5jLjET +MBEGA1UECwwKQW1hem9uIFJEUzELMAkGA1UECAwCV0ExMDAuBgNVBAMMJ0FtYXpv +biBSRFMgc2EtZWFzdC0xIFJvb3QgQ0EgUlNBMjA0OCBHMTEQMA4GA1UEBwwHU2Vh +dHRsZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAN6ftL6w8v3dB2yW +LjCxSP1D7ZsOTeLZOSCz1Zv0Gkd0XLhil5MdHOHBvwH/DrXqFU2oGzCRuAy+aZis +DardJU6ChyIQIciXCO37f0K23edhtpXuruTLLwUwzeEPdcnLPCX+sWEn9Y5FPnVm +pCd6J8edH2IfSGoa9LdErkpuESXdidLym/w0tWG/O2By4TabkNSmpdrCL00cqI+c +prA8Bx1jX8/9sY0gpAovtuFaRN+Ivg3PAnWuhqiSYyQ5nC2qDparOWuDiOhpY56E +EgmTvjwqMMjNtExfYx6Rv2Ndu50TriiNKEZBzEtkekwXInTupmYTvc7U83P/959V +UiQ+WSMCAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU4uYHdH0+ +bUeh81Eq2l5/RJbW+vswDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4IB +AQBhxcExJ+w74bvDknrPZDRgTeMLYgbVJjx2ExH7/Ac5FZZWcpUpFwWMIJJxtewI +AnhryzM3tQYYd4CG9O+Iu0+h/VVfW7e4O3joWVkxNMb820kQSEwvZfA78aItGwOY +WSaFNVRyloVicZRNJSyb1UL9EiJ9ldhxm4LTT0ax+4ontI7zTx6n6h8Sr6r/UOvX +d9T5aUUENWeo6M9jGupHNn3BobtL7BZm2oS8wX8IVYj4tl0q5T89zDi2x0MxbsIV +5ZjwqBQ5JWKv7ASGPb+z286RjPA9R2knF4lJVZrYuNV90rHvI/ECyt/JrDqeljGL +BLl1W/UsvZo6ldLIpoMbbrb5 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEBDCCAuygAwIBAgIQUfVbqapkLYpUqcLajpTJWzANBgkqhkiG9w0BAQsFADCB +mjELMAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIElu +Yy4xEzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTMwMQYDVQQDDCpB +bWF6b24gUkRTIG1lLWNlbnRyYWwtMSBSb290IENBIFJTQTIwNDggRzExEDAOBgNV +BAcMB1NlYXR0bGUwIBcNMjIwNTA2MjMyMDA5WhgPMjA2MjA1MDcwMDIwMDlaMIGa +MQswCQYDVQQGEwJVUzEiMCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywgSW5j +LjETMBEGA1UECwwKQW1hem9uIFJEUzELMAkGA1UECAwCV0ExMzAxBgNVBAMMKkFt +YXpvbiBSRFMgbWUtY2VudHJhbC0xIFJvb3QgQ0EgUlNBMjA0OCBHMTEQMA4GA1UE +BwwHU2VhdHRsZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJIeovu3 +ewI9FVitXMQzvkh34aQ6WyI4NO3YepfJaePiv3cnyFGYHN2S1cR3UQcLWgypP5va +j6bfroqwGbCbZZcb+6cyOB4ceKO9Ws1UkcaGHnNDcy5gXR7aCW2OGTUfinUuhd2d +5bOGgV7JsPbpw0bwJ156+MwfOK40OLCWVbzy8B1kITs4RUPNa/ZJnvIbiMu9rdj4 +8y7GSFJLnKCjlOFUkNI5LcaYvI1+ybuNgphT3nuu5ZirvTswGakGUT/Q0J3dxP0J +pDfg5Sj/2G4gXiaM0LppVOoU5yEwVewhQ250l0eQAqSrwPqAkdTg9ng360zqCFPE +JPPcgI1tdGUgneECAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU +/2AJVxWdZxc8eJgdpbwpW7b0f7IwDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEB +CwUAA4IBAQBYm63jTu2qYKJ94gKnqc+oUgqmb1mTXmgmp/lXDbxonjszJDOXFbri +3CCO7xB2sg9bd5YWY8sGKHaWmENj3FZpCmoefbUx++8D7Mny95Cz8R32rNcwsPTl +ebpd9A/Oaw5ug6M0x/cNr0qzF8Wk9Dx+nFEimp8RYQdKvLDfNFZHjPa1itnTiD8M +TorAqj+VwnUGHOYBsT/0NY12tnwXdD+ATWfpEHdOXV+kTMqFFwDyhfgRVNpTc+os +ygr8SwhnSCpJPB/EYl2S7r+tgAbJOkuwUvGT4pTqrzDQEhwE7swgepnHC87zhf6l +qN6mVpSnQKQLm6Ob5TeCEFgcyElsF5bH +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICrjCCAjSgAwIBAgIRAOxu0I1QuMAhIeszB3fJIlkwCgYIKoZIzj0EAwMwgZYx +CzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJbmMu +MRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTEvMC0GA1UEAwwmQW1h +em9uIFJEUyB1cy13ZXN0LTIgUm9vdCBDQSBFQ0MzODQgRzExEDAOBgNVBAcMB1Nl +YXR0bGUwIBcNMjEwNTI0MjIwNjU5WhgPMjEyMTA1MjQyMzA2NTlaMIGWMQswCQYD +VQQGEwJVUzEiMCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywgSW5jLjETMBEG +A1UECwwKQW1hem9uIFJEUzELMAkGA1UECAwCV0ExLzAtBgNVBAMMJkFtYXpvbiBS +RFMgdXMtd2VzdC0yIFJvb3QgQ0EgRUNDMzg0IEcxMRAwDgYDVQQHDAdTZWF0dGxl +MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEz4bylRcGqqDWdP7gQIIoTHdBK6FNtKH1 +4SkEIXRXkYDmRvL9Bci1MuGrwuvrka5TDj4b7e+csY0llEzHpKfq6nJPFljoYYP9 +uqHFkv77nOpJJ633KOr8IxmeHW5RXgrZo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0G +A1UdDgQWBBQQikVz8wmjd9eDFRXzBIU8OseiGzAOBgNVHQ8BAf8EBAMCAYYwCgYI +KoZIzj0EAwMDaAAwZQIwf06Mcrpw1O0EBLBBrp84m37NYtOkE/0Z0O+C7D41wnXi +EQdn6PXUVgdD23Gj82SrAjEAklhKs+liO1PtN15yeZR1Io98nFve+lLptaLakZcH ++hfFuUtCqMbaI8CdvJlKnPqT +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIGCTCCA/GgAwIBAgIRALyWMTyCebLZOGcZZQmkmfcwDQYJKoZIhvcNAQEMBQAw +gZwxCzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJ +bmMuMRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTE1MDMGA1UEAwws +QW1hem9uIFJEUyBhcC1ub3J0aGVhc3QtMyBSb290IENBIFJTQTQwOTYgRzExEDAO +BgNVBAcMB1NlYXR0bGUwIBcNMjEwNTI0MjAyODAzWhgPMjEyMTA1MjQyMTI4MDNa +MIGcMQswCQYDVQQGEwJVUzEiMCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywg +SW5jLjETMBEGA1UECwwKQW1hem9uIFJEUzELMAkGA1UECAwCV0ExNTAzBgNVBAMM +LEFtYXpvbiBSRFMgYXAtbm9ydGhlYXN0LTMgUm9vdCBDQSBSU0E0MDk2IEcxMRAw +DgYDVQQHDAdTZWF0dGxlMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA +wGFiyDyCrGqgdn4fXG12cxKAAfVvhMea1mw5h9CVRoavkPqhzQpAitSOuMB9DeiP +wQyqcsiGl/cTEau4L+AUBG8b9v26RlY48exUYBXj8CieYntOT9iNw5WtdYJa3kF/ +JxgI+HDMzE9cmHDs5DOO3S0uwZVyra/xE1ymfSlpOeUIOTpHRJv97CBUEpaZMUW5 +Sr6GruuOwFVpO5FX3A/jQlcS+UN4GjSRgDUJuqg6RRQldEZGCVCCmodbByvI2fGm +reGpsPJD54KkmAX08nOR8e5hkGoHxq0m2DLD4SrOFmt65vG47qnuwplWJjtk9B3Z +9wDoopwZLBOtlkPIkUllWm1P8EuHC1IKOA+wSP6XdT7cy8S77wgyHzR0ynxv7q/l +vlZtH30wnNqFI0y9FeogD0TGMCHcnGqfBSicJXPy9T4fU6f0r1HwqKwPp2GArwe7 +dnqLTj2D7M9MyVtFjEs6gfGWXmu1y5uDrf+CszurE8Cycoma+OfjjuVQgWOCy7Nd +jJswPxAroTzVfpgoxXza4ShUY10woZu0/J+HmNmqK7lh4NS75q1tz75in8uTZDkV +be7GK+SEusTrRgcf3tlgPjSTWG3veNzFDF2Vn1GLJXmuZfhdlVQDBNXW4MNREExS +dG57kJjICpT+r8X+si+5j51gRzkSnMYs7VHulpxfcwECAwEAAaNCMEAwDwYDVR0T +AQH/BAUwAwEB/zAdBgNVHQ4EFgQU4JWOpDBmUBuWKvGPZelw87ezhL8wDgYDVR0P +AQH/BAQDAgGGMA0GCSqGSIb3DQEBDAUAA4ICAQBRNLMql7itvXSEFQRAnyOjivHz +l5IlWVQjAbOUr6ogZcwvK6YpxNAFW5zQr8F+fdkiypLz1kk5irx9TIpff0BWC9hQ +/odMPO8Gxn8+COlSvc+dLsF2Dax3Hvz0zLeKMo+cYisJOzpdR/eKd0/AmFdkvQoM +AOK9n0yYvVJU2IrSgeJBiiCarpKSeAktEVQ4rvyacQGr+QAPkkjRwm+5LHZKK43W +nNnggRli9N/27qYtc5bgr3AaQEhEXMI4RxPRXCLsod0ehMGWyRRK728a+6PMMJAJ +WHOU0x7LCEMPP/bvpLj3BdvSGqNor4ZtyXEbwREry1uzsgODeRRns5acPwTM6ff+ +CmxO2NZ0OktIUSYRmf6H/ZFlZrIhV8uWaIwEJDz71qvj7buhQ+RFDZ9CNL64C0X6 +mf0zJGEpddjANHaaVky+F4gYMtEy2K2Lcm4JGTdyIzUoIe+atzCnRp0QeIcuWtF+ +s8AjDYCVFNypcMmqbRmNpITSnOoCHSRuVkY3gutVoYyMLbp8Jm9SJnCIlEWTA6Rm +wADOMGZJVn5/XRTRuetVOB3KlQDjs9OO01XN5NzGSZO2KT9ngAUfh9Eqhf1iRWSP +nZlRbQ2NRCuY/oJ5N59mLGxnNJSE7giEKEBRhTQ/XEPIUYAUPD5fca0arKRJwbol +l9Se1Hsq0ZU5f+OZKQ== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIGATCCA+mgAwIBAgIRAK7vlRrGVEePJpW1VHMXdlIwDQYJKoZIhvcNAQEMBQAw +gZgxCzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJ +bmMuMRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTExMC8GA1UEAwwo +QW1hem9uIFJEUyBhZi1zb3V0aC0xIFJvb3QgQ0EgUlNBNDA5NiBHMTEQMA4GA1UE +BwwHU2VhdHRsZTAgFw0yMTA1MTkxOTI4NDNaGA8yMTIxMDUxOTIwMjg0M1owgZgx +CzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJbmMu +MRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTExMC8GA1UEAwwoQW1h +em9uIFJEUyBhZi1zb3V0aC0xIFJvb3QgQ0EgUlNBNDA5NiBHMTEQMA4GA1UEBwwH +U2VhdHRsZTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMZiHOQC6x4o +eC7vVOMCGiN5EuLqPYHdceFPm4h5k/ZejXTf7kryk6aoKZKsDIYihkaZwXVS7Y/y +7Ig1F1ABi2jD+CYprj7WxXbhpysmN+CKG7YC3uE4jSvfvUnpzionkQbjJsRJcrPO +cZJM4FVaVp3mlHHtvnM+K3T+ni4a38nAd8xrv1na4+B8ZzZwWZXarfg8lJoGskSn +ou+3rbGQ0r+XlUP03zWujHoNlVK85qUIQvDfTB7n3O4s1XNGvkfv3GNBhYRWJYlB +4p8T+PFN8wG+UOByp1gV7BD64RnpuZ8V3dRAlO6YVAmINyG5UGrPzkIbLtErUNHO +4iSp4UqYvztDqJWWHR/rA84ef+I9RVwwZ8FQbjKq96OTnPrsr63A5mXTC9dXKtbw +XNJPQY//FEdyM3K8sqM0IdCzxCA1MXZ8+QapWVjwyTjUwFvL69HYky9H8eAER59K +5I7u/CWWeCy2R1SYUBINc3xxLr0CGGukcWPEZW2aPo5ibW5kepU1P/pzdMTaTfao +F42jSFXbc7gplLcSqUgWwzBnn35HLTbiZOFBPKf6vRRu8aRX9atgHw/EjCebi2xP +xIYr5Ub8u0QVHIqcnF1/hVzO/Xz0chj3E6VF/yTXnsakm+W1aM2QkZbFGpga+LMy +mFCtdPrELjea2CfxgibaJX1Q4rdEpc8DAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMB +Af8wHQYDVR0OBBYEFDSaycEyuspo/NOuzlzblui8KotFMA4GA1UdDwEB/wQEAwIB +hjANBgkqhkiG9w0BAQwFAAOCAgEAbosemjeTRsL9o4v0KadBUNS3V7gdAH+X4vH2 +Ee1Jc91VOGLdd/s1L9UX6bhe37b9WjUD69ur657wDW0RzxMYgQdZ27SUl0tEgGGp +cCmVs1ky3zEN+Hwnhkz+OTmIg1ufq0W2hJgJiluAx2r1ib1GB+YI3Mo3rXSaBYUk +bgQuujYPctf0PA153RkeICE5GI3OaJ7u6j0caYEixBS3PDHt2MJWexITvXGwHWwc +CcrC05RIrTUNOJaetQw8smVKYOfRImEzLLPZ5kf/H3Cbj8BNAFNsa10wgvlPuGOW +XLXqzNXzrG4V3sjQU5YtisDMagwYaN3a6bBf1wFwFIHQoAPIgt8q5zaQ9WI+SBns +Il6rd4zfvjq/BPmt0uI7rVg/cgbaEg/JDL2neuM9CJAzmKxYxLQuHSX2i3Fy4Y1B +cnxnRQETCRZNPGd00ADyxPKVoYBC45/t+yVusArFt+2SVLEGiFBr23eG2CEZu+HS +nDEgIfQ4V3YOTUNa86wvbAss1gbbnT/v1XCnNGClEWCWNCSRjwV2ZmQ/IVTmNHPo +7axTTBBJbKJbKzFndCnuxnDXyytdYRgFU7Ly3sa27WS2KFyFEDebLFRHQEfoYqCu +IupSqBSbXsR3U10OTjc9z6EPo1nuV6bdz+gEDthmxKa1NI+Qb1kvyliXQHL2lfhr +5zT5+Bs= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIF/zCCA+egAwIBAgIRAOLV6zZcL4IV2xmEneN1GwswDQYJKoZIhvcNAQEMBQAw +gZcxCzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJ +bmMuMRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTEwMC4GA1UEAwwn +QW1hem9uIFJEUyB1cy13ZXN0LTEgUm9vdCBDQSBSU0E0MDk2IEcxMRAwDgYDVQQH +DAdTZWF0dGxlMCAXDTIxMDUxOTE5MDg1OFoYDzIxMjEwNTE5MjAwODU4WjCBlzEL +MAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIEluYy4x +EzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTAwLgYDVQQDDCdBbWF6 +b24gUkRTIHVzLXdlc3QtMSBSb290IENBIFJTQTQwOTYgRzExEDAOBgNVBAcMB1Nl +YXR0bGUwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC7koAKGXXlLixN +fVjhuqvz0WxDeTQfhthPK60ekRpftkfE5QtnYGzeovaUAiS58MYVzqnnTACDwcJs +IGTFE6Wd7sB6r8eI/3CwI1pyJfxepubiQNVAQG0zJETOVkoYKe/5KnteKtnEER3X +tCBRdV/rfbxEDG9ZAsYfMl6zzhEWKF88G6xhs2+VZpDqwJNNALvQuzmTx8BNbl5W +RUWGq9CQ9GK9GPF570YPCuURW7kl35skofudE9bhURNz51pNoNtk2Z3aEeRx3ouT +ifFJlzh+xGJRHqBG7nt5NhX8xbg+vw4xHCeq1aAe6aVFJ3Uf9E2HzLB4SfIT9bRp +P7c9c0ySGt+3n+KLSHFf/iQ3E4nft75JdPjeSt0dnyChi1sEKDi0tnWGiXaIg+J+ +r1ZtcHiyYpCB7l29QYMAdD0TjfDwwPayLmq//c20cPmnSzw271VwqjUT0jYdrNAm +gV+JfW9t4ixtE3xF2jaUh/NzL3bAmN5v8+9k/aqPXlU1BgE3uPwMCjrfn7V0I7I1 +WLpHyd9jF3U/Ysci6H6i8YKgaPiOfySimQiDu1idmPld659qerutUSemQWmPD3bE +dcjZolmzS9U0Ujq/jDF1YayN3G3xvry1qWkTci0qMRMu2dZu30Herugh9vsdTYkf +00EqngPbqtIVLDrDjEQLqPcb8QvWFQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/ +MB0GA1UdDgQWBBQBqg8Za/L0YMHURGExHfvPyfLbOTAOBgNVHQ8BAf8EBAMCAYYw +DQYJKoZIhvcNAQEMBQADggIBACAGPMa1QL7P/FIO7jEtMelJ0hQlQepKnGtbKz4r +Xq1bUX1jnLvnAieR9KZmeQVuKi3g3CDU6b0mDgygS+FL1KDDcGRCSPh238Ou8KcG +HIxtt3CMwMHMa9gmdcMlR5fJF9vhR0C56KM2zvyelUY51B/HJqHwGvWuexryXUKa +wq1/iK2/d9mNeOcjDvEIj0RCMI8dFQCJv3PRCTC36XS36Tzr6F47TcTw1c3mgKcs +xpcwt7ezrXMUunzHS4qWAA5OGdzhYlcv+P5GW7iAA7TDNrBF+3W4a/6s9v2nQAnX +UvXd9ul0ob71377UhZbJ6SOMY56+I9cJOOfF5QvaL83Sz29Ij1EKYw/s8TYdVqAq ++dCyQZBkMSnDFLVe3J1KH2SUSfm3O98jdPORQrUlORQVYCHPls19l2F6lCmU7ICK +hRt8EVSpXm4sAIA7zcnR2nU00UH8YmMQLnx5ok9YGhuh3Ehk6QlTQLJux6LYLskd +9YHOLGW/t6knVtV78DgPqDeEx/Wu/5A8R0q7HunpWxr8LCPBK6hksZnOoUhhb8IP +vl46Ve5Tv/FlkyYr1RTVjETmg7lb16a8J0At14iLtpZWmwmuv4agss/1iBVMXfFk ++ZGtx5vytWU5XJmsfKA51KLsMQnhrLxb3X3zC+JRCyJoyc8++F3YEcRi2pkRYE3q +Hing +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIECTCCAvGgAwIBAgIRANxgyBbnxgTEOpDul2ZnC0UwDQYJKoZIhvcNAQELBQAw +gZwxCzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJ +bmMuMRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTE1MDMGA1UEAwws +QW1hem9uIFJEUyBhcC1zb3V0aGVhc3QtMyBSb290IENBIFJTQTIwNDggRzExEDAO +BgNVBAcMB1NlYXR0bGUwIBcNMjEwNjEwMTgxOTA3WhgPMjA2MTA2MTAxOTE5MDda +MIGcMQswCQYDVQQGEwJVUzEiMCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywg +SW5jLjETMBEGA1UECwwKQW1hem9uIFJEUzELMAkGA1UECAwCV0ExNTAzBgNVBAMM +LEFtYXpvbiBSRFMgYXAtc291dGhlYXN0LTMgUm9vdCBDQSBSU0EyMDQ4IEcxMRAw +DgYDVQQHDAdTZWF0dGxlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA +xnwSDAChrMkfk5TA4Dk8hKzStDlSlONzmd3fTG0Wqr5+x3EmFT6Ksiu/WIwEl9J2 +K98UI7vYyuZfCxUKb1iMPeBdVGqk0zb92GpURd+Iz/+K1ps9ZLeGBkzR8mBmAi1S +OfpwKiTBzIv6E8twhEn4IUpHsdcuX/2Y78uESpJyM8O5CpkG0JaV9FNEbDkJeBUQ +Ao2qqNcH4R0Qcr5pyeqA9Zto1RswgL06BQMI9dTpfwSP5VvkvcNUaLl7Zv5WzLQE +JzORWePvdPzzvWEkY/3FPjxBypuYwssKaERW0fkPDmPtykktP9W/oJolKUFI6pXp +y+Y6p6/AVdnQD2zZjW5FhQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1Ud +DgQWBBT+jEKs96LC+/X4BZkUYUkzPfXdqTAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZI +hvcNAQELBQADggEBAIGQqgqcQ6XSGkmNebzR6DhadTbfDmbYeN5N0Vuzv+Tdmufb +tMGjdjnYMg4B+IVnTKQb+Ox3pL9gbX6KglGK8HupobmIRtwKVth+gYYz3m0SL/Nk +haWPYzOm0x3tJm8jSdufJcEob4/ATce9JwseLl76pSWdl5A4lLjnhPPKudUDfH+1 +BLNUi3lxpp6GkC8aWUPtupnhZuXddolTLOuA3GwTZySI44NfaFRm+o83N1jp+EwD +6e94M4cTRzjUv6J3MZmSbdtQP/Tk1uz2K4bQZGP0PZC3bVpqiesdE/xr+wbu8uHr +cM1JXH0AmXf1yIkTgyWzmvt0k1/vgcw5ixAqvvE= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEATCCAumgAwIBAgIRAMhw98EQU18mIji+unM2YH8wDQYJKoZIhvcNAQELBQAw +gZgxCzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJ +bmMuMRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTExMC8GA1UEAwwo +QW1hem9uIFJEUyBhcC1zb3V0aC0yIFJvb3QgQ0EgUlNBMjA0OCBHMTEQMA4GA1UE +BwwHU2VhdHRsZTAgFw0yMjA2MDYyMTQyMjJaGA8yMDYyMDYwNjIyNDIyMlowgZgx +CzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJbmMu +MRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTExMC8GA1UEAwwoQW1h +em9uIFJEUyBhcC1zb3V0aC0yIFJvb3QgQ0EgUlNBMjA0OCBHMTEQMA4GA1UEBwwH +U2VhdHRsZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAIeeRoLfTm+7 +vqm7ZlFSx+1/CGYHyYrOOryM4/Z3dqYVHFMgWTR7V3ziO8RZ6yUanrRcWVX3PZbF +AfX0KFE8OgLsXEZIX8odSrq86+/Th5eZOchB2fDBsUB7GuN2rvFBbM8lTI9ivVOU +lbuTnYyb55nOXN7TpmH2bK+z5c1y9RVC5iQsNAl6IJNvSN8VCqXh31eK5MlKB4DT ++Y3OivCrSGsjM+UR59uZmwuFB1h+icE+U0p9Ct3Mjq3MzSX5tQb6ElTNGlfmyGpW +Kh7GQ5XU1KaKNZXoJ37H53woNSlq56bpVrKI4uv7ATpdpFubOnSLtpsKlpLdR3sy +Ws245200pC8CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUp0ki +6+eWvsnBjQhMxwMW5pwn7DgwDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUA +A4IBAQB2V8lv0aqbYQpj/bmVv/83QfE4vOxKCJAHv7DQ35cJsTyBdF+8pBczzi3t +3VNL5IUgW6WkyuUOWnE0eqAFOUVj0yTS1jSAtfl3vOOzGJZmWBbqm9BKEdu1D8O6 +sB8bnomwiab2tNDHPmUslpdDqdabbkWwNWzLJ97oGFZ7KNODMEPXWKWNxg33iHfS +/nlmnrTVI3XgaNK9qLZiUrxu9Yz5gxi/1K+sG9/Dajd32ZxjRwDipOLiZbiXQrsd +qzIMY4GcWf3g1gHL5mCTfk7dG22h/rhPyGV0svaDnsb+hOt6sv1McMN6Y3Ou0mtM +/UaAXojREmJmTSCNvs2aBny3/2sy +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICrjCCAjSgAwIBAgIRAMnRxsKLYscJV8Qv5pWbL7swCgYIKoZIzj0EAwMwgZYx +CzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJbmMu +MRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTEvMC0GA1UEAwwmQW1h +em9uIFJEUyBzYS1lYXN0LTEgUm9vdCBDQSBFQ0MzODQgRzExEDAOBgNVBAcMB1Nl +YXR0bGUwIBcNMjEwNTE5MTgxNjAxWhgPMjEyMTA1MTkxOTE2MDFaMIGWMQswCQYD +VQQGEwJVUzEiMCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywgSW5jLjETMBEG +A1UECwwKQW1hem9uIFJEUzELMAkGA1UECAwCV0ExLzAtBgNVBAMMJkFtYXpvbiBS +RFMgc2EtZWFzdC0xIFJvb3QgQ0EgRUNDMzg0IEcxMRAwDgYDVQQHDAdTZWF0dGxl +MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEjFOCZgTNVKxLKhUxffiDEvTLFhrmIqdO +dKqVdgDoELEzIHWDdC+19aDPitbCYtBVHl65ITu/9pn6mMUl5hhUNtfZuc6A+Iw1 +sBe0v0qI3y9Q9HdQYrGgeHDh8M5P7E2ho0IwQDAPBgNVHRMBAf8EBTADAQH/MB0G +A1UdDgQWBBS5L7/8M0TzoBZk39Ps7BkfTB4yJTAOBgNVHQ8BAf8EBAMCAYYwCgYI +KoZIzj0EAwMDaAAwZQIwI43O0NtWKTgnVv9z0LO5UMZYgSve7GvGTwqktZYCMObE +rUI4QerXM9D6JwLy09mqAjEAypfkdLyVWtaElVDUyHFkihAS1I1oUxaaDrynLNQK +Ou/Ay+ns+J+GyvyDUjBpVVW1 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIF/jCCA+agAwIBAgIQR71Z8lTO5Sj+as2jB7IWXzANBgkqhkiG9w0BAQwFADCB +lzELMAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIElu +Yy4xEzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTAwLgYDVQQDDCdB +bWF6b24gUkRTIHVzLXdlc3QtMiBSb290IENBIFJTQTQwOTYgRzExEDAOBgNVBAcM +B1NlYXR0bGUwIBcNMjEwNTI0MjIwMzIwWhgPMjEyMTA1MjQyMzAzMjBaMIGXMQsw +CQYDVQQGEwJVUzEiMCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywgSW5jLjET +MBEGA1UECwwKQW1hem9uIFJEUzELMAkGA1UECAwCV0ExMDAuBgNVBAMMJ0FtYXpv +biBSRFMgdXMtd2VzdC0yIFJvb3QgQ0EgUlNBNDA5NiBHMTEQMA4GA1UEBwwHU2Vh +dHRsZTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAM977bHIs1WJijrS +XQMfUOhmlJjr2v0K0UjPl52sE1TJ76H8umo1yR4T7Whkd9IwBHNGKXCJtJmMr9zp +fB38eLTu+5ydUAXdFuZpRMKBWwPVe37AdJRKqn5beS8HQjd3JXAgGKUNNuE92iqF +qi2fIqFMpnJXWo0FIW6s2Dl2zkORd7tH0DygcRi7lgVxCsw1BJQhFJon3y+IV8/F +bnbUXSNSDUnDW2EhvWSD8L+t4eiXYsozhDAzhBvojpxhPH9OB7vqFYw5qxFx+G0t +lSLX5iWi1jzzc3XyGnB6WInZDVbvnvJ4BGZ+dTRpOCvsoMIn9bz4EQTvu243c7aU +HbS/kvnCASNt+zk7C6lbmaq0AGNztwNj85Opn2enFciWZVnnJ/4OeefUWQxD0EPp +SjEd9Cn2IHzkBZrHCg+lWZJQBKbUVS0lLIMSsLQQ6WvR38jY7D2nxM1A93xWxwpt +ZtQnYRCVXH6zt2OwDAFePInWwxUjR5t/wu3XxPgpSfrmTi3WYtr1wFypAJ811e/P +yBtswWUQ6BNJQvy+KnOEeGfOwmtdDFYR+GOCfvCihzrKJrxOtHIieehR5Iw3cbXG +sm4pDzfMUVvDDz6C2M6PRlJhhClbatHCjik9hxFYEsAlqtVVK9pxaz9i8hOqSFQq +kJSQsgWw+oM/B2CyjcSqkSQEu8RLAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8w +HQYDVR0OBBYEFPmrdxpRRgu3IcaB5BTqlprcKdTsMA4GA1UdDwEB/wQEAwIBhjAN +BgkqhkiG9w0BAQwFAAOCAgEAVdlxWjPvVKky3kn8ZizeM4D+EsLw9dWLau2UD/ls +zwDCFoT6euagVeCknrn+YEl7g20CRYT9iaonGoMUPuMR/cdtPL1W/Rf40PSrGf9q +QuxavWiHLEXOQTCtCaVZMokkvjuuLNDXyZnstgECuiZECTwhexUF4oiuhyGk9o01 +QMaiz4HX4lgk0ozALUvEzaNd9gWEwD2qe+rq9cQMTVq3IArUkvTIftZUaVUMzr0O +ed1+zAsNa9nJhURJ/6anJPJjbQgb5qA1asFcp9UaMT1ku36U3gnR1T/BdgG2jX3X +Um0UcaGNVPrH1ukInWW743pxWQb7/2sumEEMVh+jWbB18SAyLI4WIh4lkurdifzS +IuTFp8TEx+MouISFhz/vJDWZ84tqoLVjkEcP6oDypq9lFoEzHDJv3V1CYcIgOusT +k1jm9P7BXdTG7TYzUaTb9USb6bkqkD9EwJAOSs7DI94aE6rsSws2yAHavjAMfuMZ +sDAZvkqS2Qg2Z2+CI6wUZn7mzkJXbZoqRjDvChDXEB1mIhzVXhiNW/CR5WKVDvlj +9v1sdGByh2pbxcLQtVaq/5coM4ANgphoNz3pOYUPWHS+JUrIivBZ+JobjXcxr3SN +9iDzcu5/FVVNbq7+KN/nvPMngT+gduEN5m+EBjm8GukJymFG0m6BENRA0QSDqZ7k +zDY= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIECTCCAvGgAwIBAgIRAK5EYG3iHserxMqgg+0EFjgwDQYJKoZIhvcNAQELBQAw +gZwxCzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJ +bmMuMRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTE1MDMGA1UEAwws +QW1hem9uIFJEUyBhcC1ub3J0aGVhc3QtMyBSb290IENBIFJTQTIwNDggRzExEDAO +BgNVBAcMB1NlYXR0bGUwIBcNMjEwNTI0MjAyMzE2WhgPMjA2MTA1MjQyMTIzMTZa +MIGcMQswCQYDVQQGEwJVUzEiMCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywg +SW5jLjETMBEGA1UECwwKQW1hem9uIFJEUzELMAkGA1UECAwCV0ExNTAzBgNVBAMM +LEFtYXpvbiBSRFMgYXAtbm9ydGhlYXN0LTMgUm9vdCBDQSBSU0EyMDQ4IEcxMRAw +DgYDVQQHDAdTZWF0dGxlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA +s1L6TtB84LGraLHVC+rGPhLBW2P0oN/91Rq3AnYwqDOuTom7agANwEjvLq7dSRG/ +sIfZsSV/ABTgArZ5sCmLjHFZAo8Kd45yA9byx20RcYtAG8IZl+q1Cri+s0XefzyO +U6mlfXZkVe6lzjlfXBkrlE/+5ifVbJK4dqOS1t9cWIpgKqv5fbE6Qbq4LVT+5/WM +Vd2BOljuBMGMzdZubqFKFq4mzTuIYfnBm7SmHlZfTdfBYPP1ScNuhpjuzw4n3NCR +EdU6dQv04Q6th4r7eiOCwbWI9LkmVbvBe3ylhH63lApC7MiiPYLlB13xBubVHVhV +q1NHoNTi+zA3MN9HWicRxQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1Ud +DgQWBBSuxoqm0/wjNiZLvqv+JlQwsDvTPDAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZI +hvcNAQELBQADggEBAFfTK/j5kv90uIbM8VaFdVbr/6weKTwehafT0pAk1bfLVX+7 +uf8oHgYiyKTTl0DFQicXejghXTeyzwoEkWSR8c6XkhD5vYG3oESqmt/RGvvoxz11 +rHHy7yHYu7RIUc3VQG60c4qxXv/1mWySGwVwJrnuyNT9KZXPevu3jVaWOVHEILaK +HvzQ2YEcWBPmde/zEseO2QeeGF8FL45Q1d66wqIP4nNUd2pCjeTS5SpB0MMx7yi9 +ki1OH1pv8tOuIdimtZ7wkdB8+JSZoaJ81b8sRrydRwJyvB88rftuI3YB4WwGuONT +ZezUPsmaoK69B0RChB0ofDpAaviF9V3xOWvVZfo= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIGDzCCA/egAwIBAgIRAI0sMNG2XhaBMRN3zD7ZyoEwDQYJKoZIhvcNAQEMBQAw +gZ8xCzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJ +bmMuMRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTE4MDYGA1UEAwwv +QW1hem9uIFJEUyBQcmV2aWV3IHVzLWVhc3QtMiBSb290IENBIFJTQTQwOTYgRzEx +EDAOBgNVBAcMB1NlYXR0bGUwIBcNMjEwNTE4MjA1NzUwWhgPMjEyMTA1MTgyMTU3 +NTBaMIGfMQswCQYDVQQGEwJVUzEiMCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNl +cywgSW5jLjETMBEGA1UECwwKQW1hem9uIFJEUzELMAkGA1UECAwCV0ExODA2BgNV +BAMML0FtYXpvbiBSRFMgUHJldmlldyB1cy1lYXN0LTIgUm9vdCBDQSBSU0E0MDk2 +IEcxMRAwDgYDVQQHDAdTZWF0dGxlMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC +CgKCAgEAh/otSiCu4Uw3hu7OJm0PKgLsLRqBmUS6jihcrkxfN2SHmp2zuRflkweU +BhMkebzL+xnNvC8okzbgPWtUxSmDnIRhE8J7bvSKFlqs/tmEdiI/LMqe/YIKcdsI +20UYmvyLIjtDaJIh598SHHlF9P8DB5jD8snJfhxWY+9AZRN+YVTltgQAAgayxkWp +M1BbvxpOnz4CC00rE0eqkguXIUSuobb1vKqdKIenlYBNxm2AmtgvQfpsBIQ0SB+8 +8Zip8Ef5rtjSw5J3s2Rq0aYvZPfCVIsKYepIboVwXtD7E9J31UkB5onLBQlaHaA6 +XlH4srsMmrew5d2XejQGy/lGZ1nVWNsKO0x/Az2QzY5Kjd6AlXZ8kq6H68hscA5i +OMbNlXzeEQsZH0YkId3+UsEns35AAjZv4qfFoLOu8vDotWhgVNT5DfdbIWZW3ZL8 +qbmra3JnCHuaTwXMnc25QeKgVq7/rG00YB69tCIDwcf1P+tFJWxvaGtV0g2NthtB +a+Xo09eC0L53gfZZ3hZw1pa3SIF5dIZ6RFRUQ+lFOux3Q/I3u+rYstYw7Zxc4Zeo +Y8JiedpQXEAnbw2ECHix/L6mVWgiWCiDzBnNLLdbmXjJRnafNSndSfFtHCnY1SiP +aCrNpzwZIJejoV1zDlWAMO+gyS28EqzuIq3WJK/TFE7acHkdKIcCAwEAAaNCMEAw +DwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUrmV1YASnuudfmqAZP4sKGTvScaEw +DgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBDAUAA4ICAQBGpEKeQoPvE85tN/25 +qHFkys9oHDl93DZ62EnOqAUKLd6v0JpCyEiop4nlrJe+4KrBYVBPyKOJDcIqE2Sp +3cvgJXLhY4i46VM3Qxe8yuYF1ElqBpg3jJVj/sCQnYz9dwoAMWIJFaDWOvmU2E7M +MRaKx+sPXFkIjiDA6Bv0m+VHef7aedSYIY7IDltEQHuXoqNacGrYo3I50R+fZs88 +/mB3e/V7967e99D6565yf9Lcjw4oQf2Hy7kl/6P9AuMz0LODnGITwh2TKk/Zo3RU +Vgq25RDrT4xJK6nFHyjUF6+4cOBxVpimmFw/VP1zaXT8DN5r4HyJ9p4YuSK8ha5N +2pJc/exvU8Nv2+vS/efcDZWyuEdZ7eh1IJWQZlOZKIAONfRDRTpeQHJ3zzv3QVYy +t78pYp/eWBHyVIfEE8p2lFKD4279WYe+Uvdb8c4Jm4TJwqkSJV8ifID7Ub80Lsir +lPAU3OCVTBeVRFPXT2zpC4PB4W6KBSuj6OOcEu2y/HgWcoi7Cnjvp0vFTUhDFdus +Wz3ucmJjfVsrkEO6avDKu4SwdbVHsk30TVAwPd6srIdi9U6MOeOQSOSE4EsrrS7l +SVmu2QIDUVFpm8QAHYplkyWIyGkupyl3ashH9mokQhixIU/Pzir0byePxHLHrwLu +1axqeKpI0F5SBUPsaVNYY2uNFg== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIECDCCAvCgAwIBAgIQCREfzzVyDTMcNME+gWnTCTANBgkqhkiG9w0BAQsFADCB +nDELMAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIElu +Yy4xEzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTUwMwYDVQQDDCxB +bWF6b24gUkRTIGFwLXNvdXRoZWFzdC0yIFJvb3QgQ0EgUlNBMjA0OCBHMTEQMA4G +A1UEBwwHU2VhdHRsZTAgFw0yMTA1MjQyMDQyMzNaGA8yMDYxMDUyNDIxNDIzM1ow +gZwxCzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJ +bmMuMRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTE1MDMGA1UEAwws +QW1hem9uIFJEUyBhcC1zb3V0aGVhc3QtMiBSb290IENBIFJTQTIwNDggRzExEDAO +BgNVBAcMB1NlYXR0bGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDL +1MT6br3L/4Pq87DPXtcjlXN3cnbNk2YqRAZHJayStTz8VtsFcGPJOpk14geRVeVk +e9uKFHRbcyr/RM4owrJTj5X4qcEuATYZbo6ou/rW2kYzuWFZpFp7lqm0vasV4Z9F +fChlhwkNks0UbM3G+psCSMNSoF19ERunj7w2c4E62LwujkeYLvKGNepjnaH10TJL +2krpERd+ZQ4jIpObtRcMH++bTrvklc+ei8W9lqrVOJL+89v2piN3Ecdd389uphst +qQdb1BBVXbhUrtuGHgVf7zKqN1SkCoktoWxVuOprVWhSvr7akaWeq0UmlvbEsujU +vADqxGMcJFyCzxx3CkJjAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0O +BBYEFFk8UJmlhoxFT3PP12PvhvazHjT4MA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG +9w0BAQsFAAOCAQEAfFtr2lGoWVXmWAsIo2NYre7kzL8Xb9Tx7desKxCCz5HOOvIr +8JMB1YK6A7IOvQsLJQ/f1UnKRh3X3mJZjKIywfrMSh0FiDf+rjcEzXxw2dGtUem4 +A+WMvIA3jwxnJ90OQj5rQ8bg3iPtE6eojzo9vWQGw/Vu48Dtw1DJo9210Lq/6hze +hPhNkFh8fMXNT7Q1Wz/TJqJElyAQGNOXhyGpHKeb0jHMMhsy5UNoW5hLeMS5ffao +TBFWEJ1gVfxIU9QRxSh+62m46JIg+dwDlWv8Aww14KgepspRbMqDuaM2cinoejv6 +t3dyOyHHrsOyv3ffZUKtQhQbQr+sUcL89lARsg== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIID/zCCAuegAwIBAgIRAIJLTMpzGNxqHZ4t+c1MlCIwDQYJKoZIhvcNAQELBQAw +gZcxCzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJ +bmMuMRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTEwMC4GA1UEAwwn +QW1hem9uIFJEUyBhcC1lYXN0LTEgUm9vdCBDQSBSU0EyMDQ4IEcxMRAwDgYDVQQH +DAdTZWF0dGxlMCAXDTIxMDUyNTIxMzAzM1oYDzIwNjEwNTI1MjIzMDMzWjCBlzEL +MAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIEluYy4x +EzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTAwLgYDVQQDDCdBbWF6 +b24gUkRTIGFwLWVhc3QtMSBSb290IENBIFJTQTIwNDggRzExEDAOBgNVBAcMB1Nl +YXR0bGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDtdHut0ZhJ9Nn2 +MpVafFcwHdoEzx06okmmhjJsNy4l9QYVeh0UUoek0SufRNMRF4d5ibzpgZol0Y92 +/qKWNe0jNxhEj6sXyHsHPeYtNBPuDMzThfbvsLK8z7pBP7vVyGPGuppqW/6m4ZBB +lcc9fsf7xpZ689iSgoyjiT6J5wlVgmCx8hFYc/uvcRtfd8jAHvheug7QJ3zZmIye +V4htOW+fRVWnBjf40Q+7uTv790UAqs0Zboj4Yil+hER0ibG62y1g71XcCyvcVpto +2/XW7Y9NCgMNqQ7fGN3wR1gjtSYPd7DO32LTzYhutyvfbpAZjsAHnoObmoljcgXI +QjfBcCFpAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFJI3aWLg +CS5xqU5WYVaeT5s8lpO0MA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOC +AQEAUwATpJOcGVOs3hZAgJwznWOoTzOVJKfrqBum7lvkVH1vBwxBl9CahaKj3ZOt +YYp2qJzhDUWludL164DL4ZjS6eRedLRviyy5cRy0581l1MxPWTThs27z+lCC14RL +PJZNVYYdl7Jy9Q5NsQ0RBINUKYlRY6OqGDySWyuMPgno2GPbE8aynMdKP+f6G/uE +YHOf08gFDqTsbyfa70ztgVEJaRooVf5JJq4UQtpDvVswW2reT96qi6tXPKHN5qp3 +3wI0I1Mp4ePmiBKku2dwYzPfrJK/pQlvu0Gu5lKOQ65QdotwLAAoaFqrf9za1yYs +INUkHLWIxDds+4OHNYcerGp5Dw== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIGCTCCA/GgAwIBAgIRAIO6ldra1KZvNWJ0TA1ihXEwDQYJKoZIhvcNAQEMBQAw +gZwxCzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJ +bmMuMRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTE1MDMGA1UEAwws +QW1hem9uIFJEUyBhcC1zb3V0aGVhc3QtMSBSb290IENBIFJTQTQwOTYgRzExEDAO +BgNVBAcMB1NlYXR0bGUwIBcNMjEwNTIxMjE0NTA1WhgPMjEyMTA1MjEyMjQ1MDVa +MIGcMQswCQYDVQQGEwJVUzEiMCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywg +SW5jLjETMBEGA1UECwwKQW1hem9uIFJEUzELMAkGA1UECAwCV0ExNTAzBgNVBAMM +LEFtYXpvbiBSRFMgYXAtc291dGhlYXN0LTEgUm9vdCBDQSBSU0E0MDk2IEcxMRAw +DgYDVQQHDAdTZWF0dGxlMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA +sDN52Si9pFSyZ1ruh3xAN0nVqEs960o2IK5CPu/ZfshFmzAwnx/MM8EHt/jMeZtj +SM58LADAsNDL01ELpFZATjgZQ6xNAyXRXE7RiTRUvNkK7O3o2qAGbLnJq/UqF7Sw +LRnB8V6hYOv+2EjVnohtGCn9SUFGZtYDjWXsLd4ML4Zpxv0a5LK7oEC7AHzbUR7R +jsjkrXqSv7GE7bvhSOhMkmgxgj1F3J0b0jdQdtyyj109aO0ATUmIvf+Bzadg5AI2 +A9UA+TUcGeebhpHu8AP1Hf56XIlzPpaQv3ZJ4vzoLaVNUC7XKzAl1dlvCl7Klg/C +84qmbD/tjZ6GHtzpLKgg7kQEV7mRoXq8X4wDX2AFPPQl2fv+Kbe+JODqm5ZjGegm +uskABBi8IFv1hYx9jEulZPxC6uD/09W2+niFm3pirnlWS83BwVDTUBzF+CooUIMT +jhWkIIZGDDgMJTzouBHfoSJtS1KpUZi99m2WyVs21MNKHeWAbs+zmI6TO5iiMC+T +uB8spaOiHFO1573Fmeer4sy3YA6qVoqVl6jjTQqOdy3frAMbCkwH22/crV8YA+08 +hLeHXrMK+6XUvU+EtHAM3VzcrLbuYJUI2XJbzTj5g0Eb8I8JWsHvWHR5K7Z7gceR +78AzxQmoGEfV6KABNWKsgoCQnfb1BidDJIe3BsI0A6UCAwEAAaNCMEAwDwYDVR0T +AQH/BAUwAwEB/zAdBgNVHQ4EFgQUABp0MlB14MSHgAcuNSOhs3MOlUcwDgYDVR0P +AQH/BAQDAgGGMA0GCSqGSIb3DQEBDAUAA4ICAQCv4CIOBSQi/QR9NxdRgVAG/pAh +tFJhV7OWb/wqwsNKFDtg6tTxwaahdCfWpGWId15OUe7G9LoPiKiwM9C92n0ZeHRz +4ewbrQVo7Eu1JI1wf0rnZJISL72hVYKmlvaWaacHhWxvsbKLrB7vt6Cknxa+S993 +Kf8i2Psw8j5886gaxhiUtzMTBwoDWak8ZaK7m3Y6C6hXQk08+3pnIornVSFJ9dlS +PAqt5UPwWmrEfF+0uIDORlT+cvrAwgSp7nUF1q8iasledycZ/BxFgQqzNwnkBDwQ +Z/aM52ArGsTzfMhkZRz9HIEhz1/0mJw8gZtDVQroD8778h8zsx2SrIz7eWQ6uWsD +QEeSWXpcheiUtEfzkDImjr2DLbwbA23c9LoexUD10nwohhoiQQg77LmvBVxeu7WU +E63JqaYUlOLOzEmNJp85zekIgR8UTkO7Gc+5BD7P4noYscI7pPOL5rP7YLg15ZFi +ega+G53NTckRXz4metsd8XFWloDjZJJq4FfD60VuxgXzoMNT9wpFTNSH42PR2s9L +I1vcl3w8yNccs9se2utM2nLsItZ3J0m/+QSRiw9hbrTYTcM9sXki0DtH2kyIOwYf +lOrGJDiYOIrXSQK36H0gQ+8omlrUTvUj4msvkXuQjlfgx6sgp2duOAfnGxE7uHnc +UhnJzzoe6M+LfGHkVQ== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICuDCCAj2gAwIBAgIQSAG6j2WHtWUUuLGJTPb1nTAKBggqhkjOPQQDAzCBmzEL +MAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIEluYy4x +EzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTQwMgYDVQQDDCtBbWF6 +b24gUkRTIGFwLW5vcnRoZWFzdC0yIFJvb3QgQ0EgRUNDMzg0IEcxMRAwDgYDVQQH +DAdTZWF0dGxlMCAXDTIxMDUyMDE2MzgyNloYDzIxMjEwNTIwMTczODI2WjCBmzEL +MAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIEluYy4x +EzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTQwMgYDVQQDDCtBbWF6 +b24gUkRTIGFwLW5vcnRoZWFzdC0yIFJvb3QgQ0EgRUNDMzg0IEcxMRAwDgYDVQQH +DAdTZWF0dGxlMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE2eqwU4FOzW8RV1W381Bd +olhDOrqoMqzWli21oDUt7y8OnXM/lmAuOS6sr8Nt61BLVbONdbr+jgCYw75KabrK +ZGg3siqvMOgabIKkKuXO14wtrGyGDt7dnKXg5ERGYOZlo0IwQDAPBgNVHRMBAf8E +BTADAQH/MB0GA1UdDgQWBBS1Acp2WYxOcblv5ikZ3ZIbRCCW+zAOBgNVHQ8BAf8E +BAMCAYYwCgYIKoZIzj0EAwMDaQAwZgIxAJL84J08PBprxmsAKPTotBuVI3MyW1r8 +xQ0i8lgCQUf8GcmYjQ0jI4oZyv+TuYJAcwIxAP9Xpzq0Docxb+4N1qVhpiOfWt1O +FnemFiy9m1l+wv6p3riQMPV7mBVpklmijkIv3Q== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIECTCCAvGgAwIBAgIRALZLcqCVIJ25maDPE3sbPCIwDQYJKoZIhvcNAQELBQAw +gZwxCzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJ +bmMuMRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTE1MDMGA1UEAwws +QW1hem9uIFJEUyBhcC1zb3V0aGVhc3QtMSBSb290IENBIFJTQTIwNDggRzExEDAO +BgNVBAcMB1NlYXR0bGUwIBcNMjEwNTIxMjEzOTM5WhgPMjA2MTA1MjEyMjM5Mzla +MIGcMQswCQYDVQQGEwJVUzEiMCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywg +SW5jLjETMBEGA1UECwwKQW1hem9uIFJEUzELMAkGA1UECAwCV0ExNTAzBgNVBAMM +LEFtYXpvbiBSRFMgYXAtc291dGhlYXN0LTEgUm9vdCBDQSBSU0EyMDQ4IEcxMRAw +DgYDVQQHDAdTZWF0dGxlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA +ypKc+6FfGx6Gl6fQ78WYS29QoKgQiur58oxR3zltWeg5fqh9Z85K5S3UbRSTqWWu +Xcfnkz0/FS07qHX+nWAGU27JiQb4YYqhjZNOAq8q0+ptFHJ6V7lyOqXBq5xOzO8f ++0DlbJSsy7GEtJp7d7QCM3M5KVY9dENVZUKeJwa8PC5StvwPx4jcLeZRJC2rAVDG +SW7NAInbATvr9ssSh03JqjXb+HDyywiqoQ7EVLtmtXWimX+0b3/2vhqcH5jgcKC9 +IGFydrjPbv4kwMrKnm6XlPZ9L0/3FMzanXPGd64LQVy51SI4d5Xymn0Mw2kMX8s6 +Nf05OsWcDzJ1n6/Q1qHSxQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1Ud +DgQWBBRmaIc8eNwGP7i6P7AJrNQuK6OpFzAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZI +hvcNAQELBQADggEBAIBeHfGwz3S2zwIUIpqEEI5/sMySDeS+3nJR+woWAHeO0C8i +BJdDh+kzzkP0JkWpr/4NWz84/IdYo1lqASd1Kopz9aT1+iROXaWr43CtbzjXb7/X +Zv7eZZFC8/lS5SROq42pPWl4ekbR0w8XGQElmHYcWS41LBfKeHCUwv83ATF0XQ6I +4t+9YSqZHzj4vvedrvcRInzmwWJaal9s7Z6GuwTGmnMsN3LkhZ+/GD6oW3pU/Pyh +EtWqffjsLhfcdCs3gG8x9BbkcJPH5aPAVkPn4wc8wuXg6xxb9YGsQuY930GWTYRf +schbgjsuqznW4HHakq4WNhs1UdTSTKkRdZz7FUQ= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEDzCCAvegAwIBAgIRAM2zAbhyckaqRim63b+Tib8wDQYJKoZIhvcNAQELBQAw +gZ8xCzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJ +bmMuMRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTE4MDYGA1UEAwwv +QW1hem9uIFJEUyBQcmV2aWV3IHVzLWVhc3QtMiBSb290IENBIFJTQTIwNDggRzEx +EDAOBgNVBAcMB1NlYXR0bGUwIBcNMjEwNTE4MjA0OTQ1WhgPMjA2MTA1MTgyMTQ5 +NDVaMIGfMQswCQYDVQQGEwJVUzEiMCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNl +cywgSW5jLjETMBEGA1UECwwKQW1hem9uIFJEUzELMAkGA1UECAwCV0ExODA2BgNV +BAMML0FtYXpvbiBSRFMgUHJldmlldyB1cy1lYXN0LTIgUm9vdCBDQSBSU0EyMDQ4 +IEcxMRAwDgYDVQQHDAdTZWF0dGxlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB +CgKCAQEA1ybjQMH1MkbvfKsWJaCTXeCSN1SG5UYid+Twe+TjuSqaXWonyp4WRR5z +tlkqq+L2MWUeQQAX3S17ivo/t84mpZ3Rla0cx39SJtP3BiA2BwfUKRjhPwOjmk7j +3zrcJjV5k1vSeLNOfFFSlwyDiVyLAE61lO6onBx+cRjelu0egMGq6WyFVidTdCmT +Q9Zw3W6LTrnPvPmEyjHy2yCHzH3E50KSd/5k4MliV4QTujnxYexI2eR8F8YQC4m3 +DYjXt/MicbqA366SOoJA50JbgpuVv62+LSBu56FpzY12wubmDZsdn4lsfYKiWxUy +uc83a2fRXsJZ1d3whxrl20VFtLFHFQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/ +MB0GA1UdDgQWBBRC0ytKmDYbfz0Bz0Psd4lRQV3aNTAOBgNVHQ8BAf8EBAMCAYYw +DQYJKoZIhvcNAQELBQADggEBAGv8qZu4uaeoF6zsbumauz6ea6tdcWt+hGFuwGrb +tRbI85ucAmVSX06x59DJClsb4MPhL1XmqO3RxVMIVVfRwRHWOsZQPnXm8OYQ2sny +rYuFln1COOz1U/KflZjgJmxbn8x4lYiTPZRLarG0V/OsCmnLkQLPtEl/spMu8Un7 +r3K8SkbWN80gg17Q8EV5mnFwycUx9xsTAaFItuG0en9bGsMgMmy+ZsDmTRbL+lcX +Fq8r4LT4QjrFz0shrzCwuuM4GmcYtBSxlacl+HxYEtAs5k10tmzRf6OYlY33tGf6 +1tkYvKryxDPF/EDgGp/LiBwx6ixYMBfISoYASt4V/ylAlHA= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICtTCCAjqgAwIBAgIRAK9BSZU6nIe6jqfODmuVctYwCgYIKoZIzj0EAwMwgZkx +CzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJbmMu +MRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTEyMDAGA1UEAwwpQW1h +em9uIFJEUyBjYS1jZW50cmFsLTEgUm9vdCBDQSBFQ0MzODQgRzExEDAOBgNVBAcM +B1NlYXR0bGUwIBcNMjEwNTIxMjIxMzA5WhgPMjEyMTA1MjEyMzEzMDlaMIGZMQsw +CQYDVQQGEwJVUzEiMCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywgSW5jLjET +MBEGA1UECwwKQW1hem9uIFJEUzELMAkGA1UECAwCV0ExMjAwBgNVBAMMKUFtYXpv +biBSRFMgY2EtY2VudHJhbC0xIFJvb3QgQ0EgRUNDMzg0IEcxMRAwDgYDVQQHDAdT +ZWF0dGxlMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEUkEERcgxneT5H+P+fERcbGmf +bVx+M7rNWtgWUr6w+OBENebQA9ozTkeSg4c4M+qdYSObFqjxITdYxT1z/nHz1gyx +OKAhLjWu+nkbRefqy3RwXaWT680uUaAP6ccnkZOMo0IwQDAPBgNVHRMBAf8EBTAD +AQH/MB0GA1UdDgQWBBSN6fxlg0s5Wny08uRBYZcQ3TUoyzAOBgNVHQ8BAf8EBAMC +AYYwCgYIKoZIzj0EAwMDaQAwZgIxAORaz+MBVoFBTmZ93j2G2vYTwA6T5hWzBWrx +CrI54pKn5g6At56DBrkjrwZF5T1enAIxAJe/LZ9xpDkAdxDgGJFN8gZYLRWc0NRy +Rb4hihy5vj9L+w9uKc9VfEBIFuhT7Z3ljg== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEADCCAuigAwIBAgIQB/57HSuaqUkLaasdjxUdPjANBgkqhkiG9w0BAQsFADCB +mDELMAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIElu +Yy4xEzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTEwLwYDVQQDDChB +bWF6b24gUkRTIGFwLXNvdXRoLTEgUm9vdCBDQSBSU0EyMDQ4IEcxMRAwDgYDVQQH +DAdTZWF0dGxlMCAXDTIxMDUxOTE3NDAzNFoYDzIwNjEwNTE5MTg0MDM0WjCBmDEL +MAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIEluYy4x +EzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTEwLwYDVQQDDChBbWF6 +b24gUkRTIGFwLXNvdXRoLTEgUm9vdCBDQSBSU0EyMDQ4IEcxMRAwDgYDVQQHDAdT +ZWF0dGxlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtbkaoVsUS76o +TgLFmcnaB8cswBk1M3Bf4IVRcwWT3a1HeJSnaJUqWHCJ+u3ip/zGVOYl0gN1MgBb +MuQRIJiB95zGVcIa6HZtx00VezDTr3jgGWRHmRjNVCCHGmxOZWvJjsIE1xavT/1j +QYV/ph4EZEIZ/qPq7e3rHohJaHDe23Z7QM9kbyqp2hANG2JtU/iUhCxqgqUHNozV +Zd0l5K6KnltZQoBhhekKgyiHqdTrH8fWajYl5seD71bs0Axowb+Oh0rwmrws3Db2 +Dh+oc2PwREnjHeca9/1C6J2vhY+V0LGaJmnnIuOANrslx2+bgMlyhf9j0Bv8AwSi +dSWsobOhNQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBQb7vJT +VciLN72yJGhaRKLn6Krn2TAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZIhvcNAQELBQAD +ggEBAAxEj8N9GslReAQnNOBpGl8SLgCMTejQ6AW/bapQvzxrZrfVOZOYwp/5oV0f +9S1jcGysDM+DrmfUJNzWxq2Y586R94WtpH4UpJDGqZp+FuOVJL313te4609kopzO +lDdmd+8z61+0Au93wB1rMiEfnIMkOEyt7D2eTFJfJRKNmnPrd8RjimRDlFgcLWJA +3E8wca67Lz/G0eAeLhRHIXv429y8RRXDtKNNz0wA2RwURWIxyPjn1fHjA9SPDkeW +E1Bq7gZj+tBnrqz+ra3yjZ2blss6Ds3/uRY6NYqseFTZWmQWT7FolZEnT9vMUitW +I0VynUbShVpGf6946e0vgaaKw20= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIID/jCCAuagAwIBAgIQGyUVTaVjYJvWhroVEiHPpDANBgkqhkiG9w0BAQsFADCB +lzELMAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIElu +Yy4xEzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTAwLgYDVQQDDCdB +bWF6b24gUkRTIHVzLXdlc3QtMSBSb290IENBIFJTQTIwNDggRzExEDAOBgNVBAcM +B1NlYXR0bGUwIBcNMjEwNTE5MTkwNDA2WhgPMjA2MTA1MTkyMDA0MDZaMIGXMQsw +CQYDVQQGEwJVUzEiMCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywgSW5jLjET +MBEGA1UECwwKQW1hem9uIFJEUzELMAkGA1UECAwCV0ExMDAuBgNVBAMMJ0FtYXpv +biBSRFMgdXMtd2VzdC0xIFJvb3QgQ0EgUlNBMjA0OCBHMTEQMA4GA1UEBwwHU2Vh +dHRsZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANhyXpJ0t4nigRDZ +EwNtFOem1rM1k8k5XmziHKDvDk831p7QsX9ZOxl/BT59Pu/P+6W6SvasIyKls1sW +FJIjFF+6xRQcpoE5L5evMgN/JXahpKGeQJPOX9UEXVW5B8yi+/dyUitFT7YK5LZA +MqWBN/LtHVPa8UmE88RCDLiKkqiv229tmwZtWT7nlMTTCqiAHMFcryZHx0pf9VPh +x/iPV8p2gBJnuPwcz7z1kRKNmJ8/cWaY+9w4q7AYlAMaq/rzEqDaN2XXevdpsYAK +TMMj2kji4x1oZO50+VPNfBl5ZgJc92qz1ocF95SAwMfOUsP8AIRZkf0CILJYlgzk +/6u6qZECAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUm5jfcS9o ++LwL517HpB6hG+PmpBswDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4IB +AQAcQ6lsqxi63MtpGk9XK8mCxGRLCad51+MF6gcNz6i6PAqhPOoKCoFqdj4cEQTF +F8dCfa3pvfJhxV6RIh+t5FCk/y6bWT8Ls/fYKVo6FhHj57bcemWsw/Z0XnROdVfK +Yqbc7zvjCPmwPHEqYBhjU34NcY4UF9yPmlLOL8uO1JKXa3CAR0htIoW4Pbmo6sA4 +6P0co/clW+3zzsQ92yUCjYmRNeSbdXbPfz3K/RtFfZ8jMtriRGuO7KNxp8MqrUho +HK8O0mlSUxGXBZMNicfo7qY8FD21GIPH9w5fp5oiAl7lqFzt3E3sCLD3IiVJmxbf +fUwpGd1XZBBSdIxysRLM6j48 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICrTCCAjOgAwIBAgIQU+PAILXGkpoTcpF200VD/jAKBggqhkjOPQQDAzCBljEL +MAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIEluYy4x +EzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMS8wLQYDVQQDDCZBbWF6 +b24gUkRTIGFwLWVhc3QtMSBSb290IENBIEVDQzM4NCBHMTEQMA4GA1UEBwwHU2Vh +dHRsZTAgFw0yMTA1MjUyMTQ1MTFaGA8yMTIxMDUyNTIyNDUxMVowgZYxCzAJBgNV +BAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJbmMuMRMwEQYD +VQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTEvMC0GA1UEAwwmQW1hem9uIFJE +UyBhcC1lYXN0LTEgUm9vdCBDQSBFQ0MzODQgRzExEDAOBgNVBAcMB1NlYXR0bGUw +djAQBgcqhkjOPQIBBgUrgQQAIgNiAAT3tFKE8Kw1sGQAvNLlLhd8OcGhlc7MiW/s +NXm3pOiCT4vZpawKvHBzD76Kcv+ZZzHRxQEmG1/muDzZGlKR32h8AAj+NNO2Wy3d +CKTtYMiVF6Z2zjtuSkZQdjuQbe4eQ7qjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYD +VR0OBBYEFAiSQOp16Vv0Ohpvqcbd2j5RmhYNMA4GA1UdDwEB/wQEAwIBhjAKBggq +hkjOPQQDAwNoADBlAjBVsi+5Ape0kOhMt/WFkANkslD4qXA5uqhrfAtH29Xzz2NV +tR7akiA771OaIGB/6xsCMQCZt2egCtbX7J0WkuZ2KivTh66jecJr5DHvAP4X2xtS +F/5pS+AUhcKTEGjI9jDH3ew= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICuDCCAj2gAwIBAgIQT5mGlavQzFHsB7hV6Mmy6TAKBggqhkjOPQQDAzCBmzEL +MAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIEluYy4x +EzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTQwMgYDVQQDDCtBbWF6 +b24gUkRTIGFwLXNvdXRoZWFzdC0yIFJvb3QgQ0EgRUNDMzg0IEcxMRAwDgYDVQQH +DAdTZWF0dGxlMCAXDTIxMDUyNDIwNTAxNVoYDzIxMjEwNTI0MjE1MDE1WjCBmzEL +MAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIEluYy4x +EzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTQwMgYDVQQDDCtBbWF6 +b24gUkRTIGFwLXNvdXRoZWFzdC0yIFJvb3QgQ0EgRUNDMzg0IEcxMRAwDgYDVQQH +DAdTZWF0dGxlMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEcm4BBBjYK7clwm0HJRWS +flt3iYwoJbIXiXn9c1y3E+Vb7bmuyKhS4eO8mwO4GefUcXObRfoHY2TZLhMJLVBQ +7MN2xDc0RtZNj07BbGD3VAIFRTDX0mH9UNYd0JQM3t/Oo0IwQDAPBgNVHRMBAf8E +BTADAQH/MB0GA1UdDgQWBBRrd5ITedfAwrGo4FA9UaDaGFK3rjAOBgNVHQ8BAf8E +BAMCAYYwCgYIKoZIzj0EAwMDaQAwZgIxAPBNqmVv1IIA3EZyQ6XuVf4gj79/DMO8 +bkicNS1EcBpUqbSuU4Zwt2BYc8c/t7KVOQIxAOHoWkoKZPiKyCxfMtJpCZySUG+n +sXgB/LOyWE5BJcXUfm+T1ckeNoWeUUMOLmnJjg== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIECTCCAvGgAwIBAgIRAJcDeinvdNrDQBeJ8+t38WQwDQYJKoZIhvcNAQELBQAw +gZwxCzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJ +bmMuMRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTE1MDMGA1UEAwws +QW1hem9uIFJEUyBhcC1zb3V0aGVhc3QtNCBSb290IENBIFJTQTIwNDggRzExEDAO +BgNVBAcMB1NlYXR0bGUwIBcNMjIwNTI1MTY0OTE2WhgPMjA2MjA1MjUxNzQ5MTZa +MIGcMQswCQYDVQQGEwJVUzEiMCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywg +SW5jLjETMBEGA1UECwwKQW1hem9uIFJEUzELMAkGA1UECAwCV0ExNTAzBgNVBAMM +LEFtYXpvbiBSRFMgYXAtc291dGhlYXN0LTQgUm9vdCBDQSBSU0EyMDQ4IEcxMRAw +DgYDVQQHDAdTZWF0dGxlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA +k8DBNkr9tMoIM0NHoFiO7cQfSX0cOMhEuk/CHt0fFx95IBytx7GHCnNzpM27O5z6 +x6iRhfNnx+B6CrGyCzOjxvPizneY+h+9zfvNz9jj7L1I2uYMuiNyOKR6FkHR46CT +1CiArfVLLPaTqgD/rQjS0GL2sLHS/0dmYipzynnZcs613XT0rAWdYDYgxDq7r/Yi +Xge5AkWQFkMUq3nOYDLCyGGfQqWKkwv6lZUHLCDKf+Y0Uvsrj8YGCI1O8mF0qPCQ +lmlfaDvbuBu1AV+aabmkvyFj3b8KRIlNLEtQ4N8KGYR2Jdb82S4YUGIOAt4wuuFt +1B7AUDLk3V/u+HTWiwfoLQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1Ud +DgQWBBSNpcjz6ArWBtAA+Gz6kyyZxrrgdDAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZI +hvcNAQELBQADggEBAGJEd7UgOzHYIcQRSF7nSYyjLROyalaIV9AX4WXW/Cqlul1c +MblP5etDZm7A/thliZIWAuyqv2bNicmS3xKvNy6/QYi1YgxZyy/qwJ3NdFl067W0 +t8nGo29B+EVK94IPjzFHWShuoktIgp+dmpijB7wkTIk8SmIoe9yuY4+hzgqk+bo4 +ms2SOXSN1DoQ75Xv+YmztbnZM8MuWhL1T7hA4AMorzTQLJ9Pof8SpSdMHeDsHp0R +01jogNFkwy25nw7cL62nufSuH2fPYGWXyNDg+y42wKsKWYXLRgUQuDVEJ2OmTFMB +T0Vf7VuNijfIA9hkN2d3K53m/9z5WjGPSdOjGhg= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIID/jCCAuagAwIBAgIQRiwspKyrO0xoxDgSkqLZczANBgkqhkiG9w0BAQsFADCB +lzELMAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIElu +Yy4xEzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTAwLgYDVQQDDCdB +bWF6b24gUkRTIHVzLXdlc3QtMiBSb290IENBIFJTQTIwNDggRzExEDAOBgNVBAcM +B1NlYXR0bGUwIBcNMjEwNTI0MjE1OTAwWhgPMjA2MTA1MjQyMjU5MDBaMIGXMQsw +CQYDVQQGEwJVUzEiMCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywgSW5jLjET +MBEGA1UECwwKQW1hem9uIFJEUzELMAkGA1UECAwCV0ExMDAuBgNVBAMMJ0FtYXpv +biBSRFMgdXMtd2VzdC0yIFJvb3QgQ0EgUlNBMjA0OCBHMTEQMA4GA1UEBwwHU2Vh +dHRsZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL53Jk3GsKiu+4bx +jDfsevWbwPCNJ3H08Zp7GWhvI3Tgi39opfHYv2ku2BKFjK8N2L6RvNPSR8yplv5j +Y0tK0U+XVNl8o0ibhqRDhbTuh6KL8CFINWYzAajuxFS+CF0U6c1Q3tXLBdALxA7l +FlXJ71QrP06W31kRe7kvgrvO7qWU3/OzUf9qYw4LSiR1/VkvvRCTqcVNw09clw/M +Jbw6FSgweN65M9j7zPbjGAXSHkXyxH1Erin2fa+B9PE4ZDgX9cp2C1DHewYJQL/g +SepwwcudVNRN1ibKH7kpMrgPnaNIVNx5sXVsTjk6q2ZqYw3SVHegltJpLy/cZReP +mlivF2kCAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUmTcQd6o1 +CuS65MjBrMwQ9JJjmBwwDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4IB +AQAKSDSIzl956wVddPThf2VAzI8syw9ngSwsEHZvxVGHBvu5gg618rDyguVCYX9L +4Kw/xJrk6S3qxOS2ZDyBcOpsrBskgahDFIunzoRP3a18ARQVq55LVgfwSDQiunch +Bd05cnFGLoiLkR5rrkgYaP2ftn3gRBRaf0y0S3JXZ2XB3sMZxGxavYq9mfiEcwB0 +LMTMQ1NYzahIeG6Jm3LqRqR8HkzP/Ztq4dT2AtSLvFebbNMiWqeqT7OcYp94HTYT +zqrtaVdUg9bwyAUCDgy0GV9RHDIdNAOInU/4LEETovrtuBU7Z1q4tcHXvN6Hd1H8 +gMb0mCG5I393qW5hFsA/diFb +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIECTCCAvGgAwIBAgIRAPQAvihfjBg/JDbj6U64K98wDQYJKoZIhvcNAQELBQAw +gZwxCzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJ +bmMuMRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTE1MDMGA1UEAwws +QW1hem9uIFJEUyBhcC1ub3J0aGVhc3QtMiBSb290IENBIFJTQTIwNDggRzExEDAO +BgNVBAcMB1NlYXR0bGUwIBcNMjEwNTIwMTYyODQxWhgPMjA2MTA1MjAxNzI4NDFa +MIGcMQswCQYDVQQGEwJVUzEiMCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywg +SW5jLjETMBEGA1UECwwKQW1hem9uIFJEUzELMAkGA1UECAwCV0ExNTAzBgNVBAMM +LEFtYXpvbiBSRFMgYXAtbm9ydGhlYXN0LTIgUm9vdCBDQSBSU0EyMDQ4IEcxMRAw +DgYDVQQHDAdTZWF0dGxlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA +vJ9lgyksCxkBlY40qOzI1TCj/Q0FVGuPL/Z1Mw2YN0l+41BDv0FHApjTUkIKOeIP +nwDwpXTa3NjYbk3cOZ/fpH2rYJ++Fte6PNDGPgKppVCUh6x3jiVZ1L7wOgnTdK1Q +Trw8440IDS5eLykRHvz8OmwvYDl0iIrt832V0QyOlHTGt6ZJ/aTQKl12Fy3QBLv7 +stClPzvHTrgWqVU6uidSYoDtzHbU7Vda7YH0wD9IUoMBf7Tu0rqcE4uH47s2XYkc +SdLEoOg/Ngs7Y9B1y1GCyj3Ux7hnyvCoRTw014QyNB7dTatFMDvYlrRDGG14KeiU +UL7Vo/+EejWI31eXNLw84wIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1Ud +DgQWBBQkgTWFsNg6wA3HbbihDQ4vpt1E2zAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZI +hvcNAQELBQADggEBAGz1Asiw7hn5WYUj8RpOCzpE0h/oBZcnxP8wulzZ5Xd0YxWO +0jYUcUk3tTQy1QvoY+Q5aCjg6vFv+oFBAxkib/SmZzp4xLisZIGlzpJQuAgRkwWA +6BVMgRS+AaOMQ6wKPgz1x4v6T0cIELZEPq3piGxvvqkcLZKdCaeC3wCS6sxuafzZ +4qA3zMwWuLOzRftgX2hQto7d/2YkRXga7jSvQl3id/EI+xrYoH6zIWgjdU1AUaNq +NGT7DIo47vVMfnd9HFZNhREsd4GJE83I+JhTqIxiKPNxrKgESzyADmNPt0gXDnHo +tbV1pMZz5HpJtjnP/qVZhEK5oB0tqlKPv9yx074= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICuTCCAj6gAwIBAgIRAKp1Rn3aL/g/6oiHVIXtCq8wCgYIKoZIzj0EAwMwgZsx +CzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJbmMu +MRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTE0MDIGA1UEAwwrQW1h +em9uIFJEUyBhcC1ub3J0aGVhc3QtMyBSb290IENBIEVDQzM4NCBHMTEQMA4GA1UE +BwwHU2VhdHRsZTAgFw0yMTA1MjQyMDMyMTdaGA8yMTIxMDUyNDIxMzIxN1owgZsx +CzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJbmMu +MRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTE0MDIGA1UEAwwrQW1h +em9uIFJEUyBhcC1ub3J0aGVhc3QtMyBSb290IENBIEVDQzM4NCBHMTEQMA4GA1UE +BwwHU2VhdHRsZTB2MBAGByqGSM49AgEGBSuBBAAiA2IABGTYWPILeBJXfcL3Dz4z +EWMUq78xB1HpjBwHoTURYfcMd5r96BTVG6yaUBWnAVCMeeD6yTG9a1eVGNhG14Hk +ZAEjgLiNB7RRbEG5JZ/XV7W/vODh09WCst2y9SLKsdgeAaNCMEAwDwYDVR0TAQH/ +BAUwAwEB/zAdBgNVHQ4EFgQUoE0qZHmDCDB+Bnm8GUa/evpfPwgwDgYDVR0PAQH/ +BAQDAgGGMAoGCCqGSM49BAMDA2kAMGYCMQCnil5MMwhY3qoXv0xvcKZGxGPaBV15 +0CCssCKn0oVtdJQfJQ3Jrf3RSaEyijXIJsoCMQC35iJi4cWoNX3N/qfgnHohW52O +B5dg0DYMqy5cNZ40+UcAanRMyqNQ6P7fy3umGco= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICtzCCAj2gAwIBAgIQPXnDTPegvJrI98qz8WxrMjAKBggqhkjOPQQDAzCBmzEL +MAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIEluYy4x +EzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTQwMgYDVQQDDCtBbWF6 +b24gUkRTIEJldGEgdXMtZWFzdC0xIFJvb3QgQ0EgRUNDMzg0IEcxMRAwDgYDVQQH +DAdTZWF0dGxlMCAXDTIxMDUxODIxNDAxMloYDzIxMjEwNTE4MjI0MDEyWjCBmzEL +MAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIEluYy4x +EzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTQwMgYDVQQDDCtBbWF6 +b24gUkRTIEJldGEgdXMtZWFzdC0xIFJvb3QgQ0EgRUNDMzg0IEcxMRAwDgYDVQQH +DAdTZWF0dGxlMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEI0sR7gwutK5AB46hM761 +gcLTGBIYlURSEoM1jcBwy56CL+3CJKZwLLyJ7qoOKfWbu5GsVLUTWS8MV6Nw33cx +2KQD2svb694wi+Px2f4n9+XHkEFQw8BbiodDD7RZA70fo0IwQDAPBgNVHRMBAf8E +BTADAQH/MB0GA1UdDgQWBBTQSioOvnVLEMXwNSDg+zgln/vAkjAOBgNVHQ8BAf8E +BAMCAYYwCgYIKoZIzj0EAwMDaAAwZQIxAMwu1hqm5Bc98uE/E0B5iMYbBQ4kpMxO +tP8FTfz5UR37HUn26nXE0puj6S/Ffj4oJgIwXI7s2c26tFQeqzq6u3lrNJHp5jC9 +Uxlo/hEJOLoDj5jnpxo8dMAtCNoQPaHdfL0P +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICrjCCAjWgAwIBAgIQGKVv+5VuzEZEBzJ+bVfx2zAKBggqhkjOPQQDAzCBlzEL +MAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIEluYy4x +EzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTAwLgYDVQQDDCdBbWF6 +b24gUkRTIGFwLXNvdXRoLTEgUm9vdCBDQSBFQ0MzODQgRzExEDAOBgNVBAcMB1Nl +YXR0bGUwIBcNMjEwNTE5MTc1MDU5WhgPMjEyMTA1MTkxODUwNTlaMIGXMQswCQYD +VQQGEwJVUzEiMCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywgSW5jLjETMBEG +A1UECwwKQW1hem9uIFJEUzELMAkGA1UECAwCV0ExMDAuBgNVBAMMJ0FtYXpvbiBS +RFMgYXAtc291dGgtMSBSb290IENBIEVDQzM4NCBHMTEQMA4GA1UEBwwHU2VhdHRs +ZTB2MBAGByqGSM49AgEGBSuBBAAiA2IABMqdLJ0tZF/DGFZTKZDrGRJZID8ivC2I +JRCYTWweZKCKSCAzoiuGGHzJhr5RlLHQf/QgmFcgXsdmO2n3CggzhA4tOD9Ip7Lk +P05eHd2UPInyPCHRgmGjGb0Z+RdQ6zkitKNCMEAwDwYDVR0TAQH/BAUwAwEB/zAd +BgNVHQ4EFgQUC1yhRgVqU5bR8cGzOUCIxRpl4EYwDgYDVR0PAQH/BAQDAgGGMAoG +CCqGSM49BAMDA2cAMGQCMG0c/zLGECRPzGKJvYCkpFTCUvdP4J74YP0v/dPvKojL +t/BrR1Tg4xlfhaib7hPc7wIwFvgqHes20CubQnZmswbTKLUrgSUW4/lcKFpouFd2 +t2/ewfi/0VhkeUW+IiHhOMdU +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIGCTCCA/GgAwIBAgIRAOXxJuyXVkbfhZCkS/dOpfEwDQYJKoZIhvcNAQEMBQAw +gZwxCzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJ +bmMuMRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTE1MDMGA1UEAwws +QW1hem9uIFJEUyBhcC1ub3J0aGVhc3QtMSBSb290IENBIFJTQTQwOTYgRzExEDAO +BgNVBAcMB1NlYXR0bGUwIBcNMjEwNTI1MjE1OTEwWhgPMjEyMTA1MjUyMjU5MTBa +MIGcMQswCQYDVQQGEwJVUzEiMCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywg +SW5jLjETMBEGA1UECwwKQW1hem9uIFJEUzELMAkGA1UECAwCV0ExNTAzBgNVBAMM +LEFtYXpvbiBSRFMgYXAtbm9ydGhlYXN0LTEgUm9vdCBDQSBSU0E0MDk2IEcxMRAw +DgYDVQQHDAdTZWF0dGxlMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA +xiP4RDYm4tIS12hGgn1csfO8onQDmK5SZDswUpl0HIKXOUVVWkHNlINkVxbdqpqH +FhbyZmNN6F/EWopotMDKe1B+NLrjNQf4zefv2vyKvPHJXhxoKmfyuTd5Wk8k1F7I +lNwLQzznB+ElhrLIDJl9Ro8t31YBBNFRGAGEnxyACFGcdkjlsa52UwfYrwreEg2l +gW5AzqHgjFfj9QRLydeU/n4bHm0F1adMsV7P3rVwilcUlqsENDwXnWyPEyv3sw6F +wNemLEs1129mB77fwvySb+lLNGsnzr8w4wdioZ74co+T9z2ca+eUiP+EQccVw1Is +D4Fh57IjPa6Wuc4mwiUYKkKY63+38aCfEWb0Qoi+zW+mE9nek6MOQ914cN12u5LX +dBoYopphRO5YmubSN4xcBy405nIdSdbrAVWwxXnVVyjqjknmNeqQsPZaxAhdoKhV +AqxNr8AUAdOAO6Sz3MslmcLlDXFihrEEOeUbpg/m1mSUUHGbu966ajTG1FuEHHwS +7WB52yxoJo/tHvt9nAWnh3uH5BHmS8zn6s6CGweWKbX5yICnZ1QFR1e4pogxX39v +XD6YcNOO+Vn+HY4nXmjgSYVC7l+eeP8eduMg1xJujzjrbmrXU+d+cBObgdTOAlpa +JFHaGwYw1osAwPCo9cZ2f04yitBfj9aPFia8ASKldakCAwEAAaNCMEAwDwYDVR0T +AQH/BAUwAwEB/zAdBgNVHQ4EFgQUqKS+ltlior0SyZKYAkJ/efv55towDgYDVR0P +AQH/BAQDAgGGMA0GCSqGSIb3DQEBDAUAA4ICAQAdElvp8bW4B+Cv+1WSN87dg6TN +wGyIjJ14/QYURgyrZiYpUmZpj+/pJmprSWXu4KNyqHftmaidu7cdjL5nCAvAfnY5 +/6eDDbX4j8Gt9fb/6H9y0O0dn3mUPSEKG0crR+JRFAtPhn/2FNvst2P82yguWLv0 +pHjHVUVcq+HqDMtUIJsTPYjSh9Iy77Q6TOZKln9dyDOWJpCSkiUWQtMAKbCSlvzd +zTs/ahqpT+zLfGR1SR+T3snZHgQnbnemmz/XtlKl52NxccARwfcEEKaCRQyGq/pR +0PVZasyJS9JY4JfQs4YOdeOt4UMZ8BmW1+BQWGSkkb0QIRl8CszoKofucAlqdPcO +IT/ZaMVhI580LFGWiQIizWFskX6lqbCyHqJB3LDl8gJISB5vNTHOHpvpMOMs5PYt +cRl5Mrksx5MKMqG7y5R734nMlZxQIHjL5FOoOxTBp9KeWIL/Ib89T2QDaLw1SQ+w +ihqWBJ4ZdrIMWYpP3WqM+MXWk7WAem+xsFJdR+MDgOOuobVQTy5dGBlPks/6gpjm +rO9TjfQ36ppJ3b7LdKUPeRfnYmlR5RU4oyYJ//uLbClI443RZAgxaCXX/nyc12lr +eVLUMNF2abLX4/VF63m2/Z9ACgMRfqGshPssn1NN33OonrotQoj4S3N9ZrjvzKt8 +iHcaqd60QKpfiH2A3A== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICuDCCAj2gAwIBAgIQPaVGRuu86nh/ylZVCLB0MzAKBggqhkjOPQQDAzCBmzEL +MAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIEluYy4x +EzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTQwMgYDVQQDDCtBbWF6 +b24gUkRTIGFwLW5vcnRoZWFzdC0xIFJvb3QgQ0EgRUNDMzg0IEcxMRAwDgYDVQQH +DAdTZWF0dGxlMCAXDTIxMDUyNTIyMDMxNloYDzIxMjEwNTI1MjMwMzE2WjCBmzEL +MAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIEluYy4x +EzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTQwMgYDVQQDDCtBbWF6 +b24gUkRTIGFwLW5vcnRoZWFzdC0xIFJvb3QgQ0EgRUNDMzg0IEcxMRAwDgYDVQQH +DAdTZWF0dGxlMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEexNURoB9KE93MEtEAlJG +obz4LS/pD2hc8Gczix1WhVvpJ8bN5zCDXaKdnDMCebetyRQsmQ2LYlfmCwpZwSDu +0zowB11Pt3I5Avu2EEcuKTlKIDMBeZ1WWuOd3Tf7MEAMo0IwQDAPBgNVHRMBAf8E +BTADAQH/MB0GA1UdDgQWBBSaYbZPBvFLikSAjpa8mRJvyArMxzAOBgNVHQ8BAf8E +BAMCAYYwCgYIKoZIzj0EAwMDaQAwZgIxAOEJkuh3Zjb7Ih/zuNRd1RBqmIYcnyw0 +nwUZczKXry+9XebYj3VQxSRNadrarPWVqgIxAMg1dyGoDAYjY/L/9YElyMnvHltO +PwpJShmqHvCLc/mXMgjjYb/akK7yGthvW6j/uQ== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIGCDCCA/CgAwIBAgIQChu3v5W1Doil3v6pgRIcVzANBgkqhkiG9w0BAQwFADCB +nDELMAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIElu +Yy4xEzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTUwMwYDVQQDDCxB +bWF6b24gUkRTIEJldGEgdXMtZWFzdC0xIFJvb3QgQ0EgUlNBNDA5NiBHMTEQMA4G +A1UEBwwHU2VhdHRsZTAgFw0yMTA1MTgyMTM0MTVaGA8yMTIxMDUxODIyMzQxNVow +gZwxCzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJ +bmMuMRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTE1MDMGA1UEAwws +QW1hem9uIFJEUyBCZXRhIHVzLWVhc3QtMSBSb290IENBIFJTQTQwOTYgRzExEDAO +BgNVBAcMB1NlYXR0bGUwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC1 +FUGQ5tf3OwpDR6hGBxhUcrkwKZhaXP+1St1lSOQvjG8wXT3RkKzRGMvb7Ee0kzqI +mzKKe4ASIhtV3UUWdlNmP0EA3XKnif6N79MismTeGkDj75Yzp5A6tSvqByCgxIjK +JqpJrch3Dszoyn8+XhwDxMZtkUa5nQVdJgPzJ6ltsQ8E4SWLyLtTu0S63jJDkqYY +S7cQblk7y7fel+Vn+LS5dGTdRRhMvSzEnb6mkVBaVzRyVX90FNUED06e8q+gU8Ob +htvQlf9/kRzHwRAdls2YBhH40ZeyhpUC7vdtPwlmIyvW5CZ/QiG0yglixnL6xahL +pbmTuTSA/Oqz4UGQZv2WzHe1lD2gRHhtFX2poQZeNQX8wO9IcUhrH5XurW/G9Xwl +Sat9CMPERQn4KC3HSkat4ir2xaEUrjfg6c4XsGyh2Pk/LZ0gLKum0dyWYpWP4JmM +RQNjrInXPbMhzQObozCyFT7jYegS/3cppdyy+K1K7434wzQGLU1gYXDKFnXwkX8R +bRKgx2pHNbH5lUddjnNt75+e8m83ygSq/ZNBUz2Ur6W2s0pl6aBjwaDES4VfWYlI +jokcmrGvJNDfQWygb1k00eF2bzNeNCHwgWsuo3HSxVgc/WGsbcGrTlDKfz+g3ich +bXUeUidPhRiv5UQIVCLIHpHuin3bj9lQO/0t6p+tAQIDAQABo0IwQDAPBgNVHRMB +Af8EBTADAQH/MB0GA1UdDgQWBBSFmMBgm5IsRv3hLrvDPIhcPweXYTAOBgNVHQ8B +Af8EBAMCAYYwDQYJKoZIhvcNAQEMBQADggIBAAa2EuozymOsQDJlEi7TqnyA2OhT +GXPfYqCyMJVkfrqNgcnsNpCAiNEiZbb+8sIPXnT8Ay8hrwJYEObJ5b7MHXpLuyft +z0Pu1oFLKnQxKjNxrIsCvaB4CRRdYjm1q7EqGhMGv76se9stOxkOqO9it31w/LoU +ENDk7GLsSqsV1OzYLhaH8t+MaNP6rZTSNuPrHwbV3CtBFl2TAZ7iKgKOhdFz1Hh9 +Pez0lG+oKi4mHZ7ajov6PD0W7njn5KqzCAkJR6OYmlNVPjir+c/vUtEs0j+owsMl +g7KE5g4ZpTRShyh5BjCFRK2tv0tkqafzNtxrKC5XNpEkqqVTCnLcKG+OplIEadtr +C7UWf4HyhCiR+xIyxFyR05p3uY/QQU/5uza7GlK0J+U1sBUytx7BZ+Fo8KQfPPqV +CqDCaYUksoJcnJE/KeoksyqNQys7sDGJhkd0NeUGDrFLKHSLhIwAMbEWnqGxvhli +E7sP2E5rI/I9Y9zTbLIiI8pfeZlFF8DBdoP/Hzg8pqsiE/yiXSFTKByDwKzGwNqz +F0VoFdIZcIbLdDbzlQitgGpJtvEL7HseB0WH7B2PMMD8KPJlYvPveO3/6OLzCsav ++CAkvk47NQViKMsUTKOA0JDCW+u981YRozxa3K081snhSiSe83zIPBz1ikldXxO9 +6YYLNPRrj3mi9T/f +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICrjCCAjSgAwIBAgIRAMkvdFnVDb0mWWFiXqnKH68wCgYIKoZIzj0EAwMwgZYx +CzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJbmMu +MRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTEvMC0GA1UEAwwmQW1h +em9uIFJEUyB1cy13ZXN0LTEgUm9vdCBDQSBFQ0MzODQgRzExEDAOBgNVBAcMB1Nl +YXR0bGUwIBcNMjEwNTE5MTkxMzI0WhgPMjEyMTA1MTkyMDEzMjRaMIGWMQswCQYD +VQQGEwJVUzEiMCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywgSW5jLjETMBEG +A1UECwwKQW1hem9uIFJEUzELMAkGA1UECAwCV0ExLzAtBgNVBAMMJkFtYXpvbiBS +RFMgdXMtd2VzdC0xIFJvb3QgQ0EgRUNDMzg0IEcxMRAwDgYDVQQHDAdTZWF0dGxl +MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEy86DB+9th/0A5VcWqMSWDxIUblWTt/R0 +ao6Z2l3vf2YDF2wt1A2NIOGpfQ5+WAOJO/IQmnV9LhYo+kacB8sOnXdQa6biZZkR +IyouUfikVQAKWEJnh1Cuo5YMM4E2sUt5o0IwQDAPBgNVHRMBAf8EBTADAQH/MB0G +A1UdDgQWBBQ8u3OnecANmG8OoT7KLWDuFzZwBTAOBgNVHQ8BAf8EBAMCAYYwCgYI +KoZIzj0EAwMDaAAwZQIwQ817qkb7mWJFnieRAN+m9W3E0FLVKaV3zC5aYJUk2fcZ +TaUx3oLp3jPLGvY5+wgeAjEA6wAicAki4ZiDfxvAIuYiIe1OS/7H5RA++R8BH6qG +iRzUBM/FItFpnkus7u/eTkvo +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICrzCCAjWgAwIBAgIQS/+Ryfgb/IOVEa1pWoe8oTAKBggqhkjOPQQDAzCBlzEL +MAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIEluYy4x +EzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTAwLgYDVQQDDCdBbWF6 +b24gUkRTIGFwLXNvdXRoLTIgUm9vdCBDQSBFQ0MzODQgRzExEDAOBgNVBAcMB1Nl +YXR0bGUwIBcNMjIwNjA2MjE1NDQyWhgPMjEyMjA2MDYyMjU0NDJaMIGXMQswCQYD +VQQGEwJVUzEiMCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywgSW5jLjETMBEG +A1UECwwKQW1hem9uIFJEUzELMAkGA1UECAwCV0ExMDAuBgNVBAMMJ0FtYXpvbiBS +RFMgYXAtc291dGgtMiBSb290IENBIEVDQzM4NCBHMTEQMA4GA1UEBwwHU2VhdHRs +ZTB2MBAGByqGSM49AgEGBSuBBAAiA2IABDsX6fhdUWBQpYTdseBD/P3s96Dtw2Iw +OrXKNToCnmX5nMkUGdRn9qKNiz1pw3EPzaPxShbYwQ7LYP09ENK/JN4QQjxMihxC +jLFxS85nhBQQQGRCWikDAe38mD8fSvREQKNCMEAwDwYDVR0TAQH/BAUwAwEB/zAd +BgNVHQ4EFgQUIh1xZiseQYFjPYKJmGbruAgRH+AwDgYDVR0PAQH/BAQDAgGGMAoG +CCqGSM49BAMDA2gAMGUCMFudS4zLy+UUGrtgNLtRMcu/DZ9BUzV4NdHxo0bkG44O +thnjl4+wTKI6VbyAbj2rkgIxAOHps8NMITU5DpyiMnKTxV8ubb/WGHrLl0BjB8Lw +ETVJk5DNuZvsIIcm7ykk6iL4Tw== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIGBDCCA+ygAwIBAgIQDcEmNIAVrDpUw5cH5ynutDANBgkqhkiG9w0BAQwFADCB +mjELMAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIElu +Yy4xEzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTMwMQYDVQQDDCpB +bWF6b24gUkRTIG1lLWNlbnRyYWwtMSBSb290IENBIFJTQTQwOTYgRzExEDAOBgNV +BAcMB1NlYXR0bGUwIBcNMjIwNTA3MDA0MDIzWhgPMjEyMjA1MDcwMTQwMjNaMIGa +MQswCQYDVQQGEwJVUzEiMCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywgSW5j +LjETMBEGA1UECwwKQW1hem9uIFJEUzELMAkGA1UECAwCV0ExMzAxBgNVBAMMKkFt +YXpvbiBSRFMgbWUtY2VudHJhbC0xIFJvb3QgQ0EgUlNBNDA5NiBHMTEQMA4GA1UE +BwwHU2VhdHRsZTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAKvADk8t +Fl9bFlU5sajLPPDSOUpPAkKs6iPlz+27o1GJC88THcOvf3x0nVAcu9WYe9Qaas+4 +j4a0vv51agqyODRD/SNi2HnqW7DbtLPAm6KBHe4twl28ItB/JD5g7u1oPAHFoXMS +cH1CZEAs5RtlZGzJhcBXLFsHNv/7+SCLyZ7+2XFh9OrtgU4wMzkHoRNndhfwV5bu +17bPTwuH+VxH37zXf1mQ/KjhuJos0C9dL0FpjYBAuyZTAWhZKs8dpSe4DI544z4w +gkwUB4bC2nA1TBzsywEAHyNuZ/xRjNpWvx0ToWAA2iFJqC3VO3iKcnBplMvaUuMt +jwzVSNBnKcoabXCZL2XDLt4YTZR8FSwz05IvsmwcPB7uNTBXq3T9sjejW8QQK3vT +tzyfLq4jKmQE7PoS6cqYm+hEPm2hDaC/WP9bp3FdEJxZlPH26fq1b7BWYWhQ9pBA +Nv9zTnzdR1xohTyOJBUFQ81ybEzabqXqVXUIANqIOaNcTB09/sLJ7+zuMhp3mwBu +LtjfJv8PLuT1r63bU3seROhKA98b5KfzjvbvPSg3vws78JQyoYGbqNyDfyjVjg3U +v//AdVuPie6PNtdrW3upZY4Qti5IjP9e3kimaJ+KAtTgMRG56W0WxD3SP7+YGGbG +KhntDOkKsN39hLpn9UOafTIqFu7kIaueEy/NAgMBAAGjQjBAMA8GA1UdEwEB/wQF +MAMBAf8wHQYDVR0OBBYEFHAems86dTwdZbLe8AaPy3kfIUVoMA4GA1UdDwEB/wQE +AwIBhjANBgkqhkiG9w0BAQwFAAOCAgEAOBHpp0ICx81kmeoBcZTrMdJs2gnhcd85 +FoSCjXx9H5XE5rmN/lQcxxOgj8hr3uPuLdLHu+i6THAyzjrl2NA1FWiqpfeECGmy +0jm7iZsYORgGQYp/VKnDrwnKNSqlZvOuRr0kfUexwFlr34Y4VmupvEOK/RdGsd3S ++3hiemcHse9ST/sJLHx962AWMkN86UHPscJEe4+eT3f2Wyzg6La8ARwdWZSNS+WH +ZfybrncMmuiXuUdHv9XspPsqhKgtHhcYeXOGUtrwQPLe3+VJZ0LVxhlTWr9951GZ +GfmWwTV/9VsyKVaCFIXeQ6L+gjcKyEzYF8wpMtQlSc7FFqwgC4bKxvMBSaRy88Nr +lV2+tJD/fr8zGUeBK44Emon0HKDBWGX+/Hq1ZIv0Da0S+j6LbA4fusWxtGfuGha+ +luhHgVInCpALIOamiBEdGhILkoTtx7JrYppt3/Raqg9gUNCOOYlCvGhqX7DXeEfL +DGabooiY2FNWot6h04JE9nqGj5QqT8D6t/TL1nzxhRPzbcSDIHUd/b5R+a0bAA+7 +YTU6JqzEVCWKEIEynYmqikgLMGB/OzWsgyEL6822QW6hJAQ78XpbNeCzrICF4+GC +7KShLnwuWoWpAb26268lvOEvCTFM47VC6jNQl97md+2SA9Ma81C9wflid2M83Wle +cuLMVcQZceE= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEADCCAuigAwIBAgIQAhAteLRCvizAElaWORFU2zANBgkqhkiG9w0BAQsFADCB +mDELMAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIElu +Yy4xEzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTEwLwYDVQQDDChB +bWF6b24gUkRTIG1lLXNvdXRoLTEgUm9vdCBDQSBSU0EyMDQ4IEcxMRAwDgYDVQQH +DAdTZWF0dGxlMCAXDTIxMDUyMDE3MDkxNloYDzIwNjEwNTIwMTgwOTE2WjCBmDEL +MAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIEluYy4x +EzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTEwLwYDVQQDDChBbWF6 +b24gUkRTIG1lLXNvdXRoLTEgUm9vdCBDQSBSU0EyMDQ4IEcxMRAwDgYDVQQHDAdT +ZWF0dGxlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA+qg7JAcOVKjh +N83SACnBFZPyB63EusfDr/0V9ZdL8lKcmZX9sv/CqoBo3N0EvBqHQqUUX6JvFb7F +XrMUZ740kr28gSRALfXTFgNODjXeDsCtEkKRTkac/UM8xXHn+hR7UFRPHS3e0GzI +iLiwQWDkr0Op74W8aM0CfaVKvh2bp4BI1jJbdDnQ9OKXpOxNHGUf0ZGb7TkNPkgI +b2CBAc8J5o3H9lfw4uiyvl6Fz5JoP+A+zPELAioYBXDrbE7wJeqQDJrETWqR9VEK +BXURCkVnHeaJy123MpAX2ozf4pqk0V0LOEOZRS29I+USF5DcWr7QIXR/w2I8ws1Q +7ys+qbE+kQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBQFJ16n +1EcCMOIhoZs/F9sR+Jy++zAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZIhvcNAQELBQAD +ggEBAOc5nXbT3XTDEZsxX2iD15YrQvmL5m13B3ImZWpx/pqmObsgx3/dg75rF2nQ +qS+Vl+f/HLh516pj2BPP/yWCq12TRYigGav8UH0qdT3CAClYy2o+zAzUJHm84oiB +ud+6pFVGkbqpsY+QMpJUbZWu52KViBpJMYsUEy+9cnPSFRVuRAHjYynSiLk2ZEjb +Wkdc4x0nOZR5tP0FgrX0Ve2KcjFwVQJVZLgOUqmFYQ/G0TIIGTNh9tcmR7yp+xJR +A2tbPV2Z6m9Yxx4E8lLEPNuoeouJ/GR4CkMEmF8cLwM310t174o3lKKUXJ4Vs2HO +Wj2uN6R9oI+jGLMSswTzCNV1vgc= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICuDCCAj6gAwIBAgIRAOocLeZWjYkG/EbHmscuy8gwCgYIKoZIzj0EAwMwgZsx +CzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJbmMu +MRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTE0MDIGA1UEAwwrQW1h +em9uIFJEUyBhcC1zb3V0aGVhc3QtMSBSb290IENBIEVDQzM4NCBHMTEQMA4GA1UE +BwwHU2VhdHRsZTAgFw0yMTA1MjEyMTUwMDFaGA8yMTIxMDUyMTIyNTAwMVowgZsx +CzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJbmMu +MRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTE0MDIGA1UEAwwrQW1h +em9uIFJEUyBhcC1zb3V0aGVhc3QtMSBSb290IENBIEVDQzM4NCBHMTEQMA4GA1UE +BwwHU2VhdHRsZTB2MBAGByqGSM49AgEGBSuBBAAiA2IABCEr3jq1KtRncnZfK5cq +btY0nW6ZG3FMbh7XwBIR6Ca0f8llGZ4vJEC1pXgiM/4Dh045B9ZIzNrR54rYOIfa +2NcYZ7mk06DjIQML64hbAxbQzOAuNzLPx268MrlL2uW2XaNCMEAwDwYDVR0TAQH/ +BAUwAwEB/zAdBgNVHQ4EFgQUln75pChychwN4RfHl+tOinMrfVowDgYDVR0PAQH/ +BAQDAgGGMAoGCCqGSM49BAMDA2gAMGUCMGiyPINRU1mwZ4Crw01vpuPvxZxb2IOr +yX3RNlOIu4We1H+5dQk5tIvH8KGYFbWEpAIxAO9NZ6/j9osMhLgZ0yj0WVjb+uZx +YlZR9fyFisY/jNfX7QhSk+nrc3SFLRUNtpXrng== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEBTCCAu2gAwIBAgIRAKiaRZatN8eiz9p0s0lu0rQwDQYJKoZIhvcNAQELBQAw +gZoxCzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJ +bmMuMRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTEzMDEGA1UEAwwq +QW1hem9uIFJEUyBjYS1jZW50cmFsLTEgUm9vdCBDQSBSU0EyMDQ4IEcxMRAwDgYD +VQQHDAdTZWF0dGxlMCAXDTIxMDUyMTIyMDIzNVoYDzIwNjEwNTIxMjMwMjM1WjCB +mjELMAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIElu +Yy4xEzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTMwMQYDVQQDDCpB +bWF6b24gUkRTIGNhLWNlbnRyYWwtMSBSb290IENBIFJTQTIwNDggRzExEDAOBgNV +BAcMB1NlYXR0bGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCygVMf +qB865IR9qYRBRFHn4eAqGJOCFx+UbraQZmjr/mnRqSkY+nhbM7Pn/DWOrRnxoh+w +q5F9ZxdZ5D5T1v6kljVwxyfFgHItyyyIL0YS7e2h7cRRscCM+75kMedAP7icb4YN +LfWBqfKHbHIOqvvQK8T6+Emu/QlG2B5LvuErrop9K0KinhITekpVIO4HCN61cuOe +CADBKF/5uUJHwS9pWw3uUbpGUwsLBuhJzCY/OpJlDqC8Y9aToi2Ivl5u3/Q/sKjr +6AZb9lx4q3J2z7tJDrm5MHYwV74elGSXoeoG8nODUqjgklIWAPrt6lQ3WJpO2kug +8RhCdSbWkcXHfX95AgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYE +FOIxhqTPkKVqKBZvMWtKewKWDvDBMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0B +AQsFAAOCAQEAqoItII89lOl4TKvg0I1EinxafZLXIheLcdGCxpjRxlZ9QMQUN3yb +y/8uFKBL0otbQgJEoGhxm4h0tp54g28M6TN1U0332dwkjYxUNwvzrMaV5Na55I2Z +1hq4GB3NMXW+PvdtsgVOZbEN+zOyOZ5MvJHEQVkT3YRnf6avsdntltcRzHJ16pJc +Y8rR7yWwPXh1lPaPkxddrCtwayyGxNbNmRybjR48uHRhwu7v2WuAMdChL8H8bp89 +TQLMrMHgSbZfee9hKhO4Zebelf1/cslRSrhkG0ESq6G5MUINj6lMg2g6F0F7Xz2v +ncD/vuRN5P+vT8th/oZ0Q2Gc68Pun0cn/g== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIID/zCCAuegAwIBAgIRAJYlnmkGRj4ju/2jBQsnXJYwDQYJKoZIhvcNAQELBQAw +gZcxCzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJ +bmMuMRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTEwMC4GA1UEAwwn +QW1hem9uIFJEUyB1cy1lYXN0LTIgUm9vdCBDQSBSU0EyMDQ4IEcxMRAwDgYDVQQH +DAdTZWF0dGxlMCAXDTIxMDUyMTIzMDQ0NFoYDzIwNjEwNTIyMDAwNDQ0WjCBlzEL +MAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIEluYy4x +EzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTAwLgYDVQQDDCdBbWF6 +b24gUkRTIHVzLWVhc3QtMiBSb290IENBIFJTQTIwNDggRzExEDAOBgNVBAcMB1Nl +YXR0bGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC74V3eigv+pCj5 +nqDBqplY0Jp16pTeNB06IKbzb4MOTvNde6QjsZxrE1xUmprT8LxQqN9tI3aDYEYk +b9v4F99WtQVgCv3Y34tYKX9NwWQgwS1vQwnIR8zOFBYqsAsHEkeJuSqAB12AYUSd +Zv2RVFjiFmYJho2X30IrSLQfS/IE3KV7fCyMMm154+/K1Z2IJlcissydEAwgsUHw +edrE6CxJVkkJ3EvIgG4ugK/suxd8eEMztaQYJwSdN8TdfT59LFuSPl7zmF3fIBdJ +//WexcQmGabaJ7Xnx+6o2HTfkP8Zzzzaq8fvjAcvA7gyFH5EP26G2ZqMG+0y4pTx +SPVTrQEXAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFIWWuNEF +sUMOC82XlfJeqazzrkPDMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOC +AQEAgClmxcJaQTGpEZmjElL8G2Zc8lGc+ylGjiNlSIw8X25/bcLRptbDA90nuP+q +zXAMhEf0ccbdpwxG/P5a8JipmHgqQLHfpkvaXx+0CuP++3k+chAJ3Gk5XtY587jX ++MJfrPgjFt7vmMaKmynndf+NaIJAYczjhJj6xjPWmGrjM3MlTa9XesmelMwP3jep +bApIWAvCYVjGndbK9byyMq1nyj0TUzB8oJZQooaR3MMjHTmADuVBylWzkRMxbKPl +4Nlsk4Ef1JvIWBCzsMt+X17nuKfEatRfp3c9tbpGlAE/DSP0W2/Lnayxr4RpE9ds +ICF35uSis/7ZlsftODUe8wtpkQ== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIF/zCCA+egAwIBAgIRAPvvd+MCcp8E36lHziv0xhMwDQYJKoZIhvcNAQEMBQAw +gZcxCzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJ +bmMuMRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTEwMC4GA1UEAwwn +QW1hem9uIFJEUyB1cy1lYXN0LTIgUm9vdCBDQSBSU0E0MDk2IEcxMRAwDgYDVQQH +DAdTZWF0dGxlMCAXDTIxMDUyMTIzMTEwNloYDzIxMjEwNTIyMDAxMTA2WjCBlzEL +MAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIEluYy4x +EzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTAwLgYDVQQDDCdBbWF6 +b24gUkRTIHVzLWVhc3QtMiBSb290IENBIFJTQTQwOTYgRzExEDAOBgNVBAcMB1Nl +YXR0bGUwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDbvwekKIKGcV/s +lDU96a71ZdN2pTYkev1X2e2/ICb765fw/i1jP9MwCzs8/xHBEQBJSxdfO4hPeNx3 +ENi0zbM+TrMKliS1kFVe1trTTEaHYjF8BMK9yTY0VgSpWiGxGwg4tshezIA5lpu8 +sF6XMRxosCEVCxD/44CFqGZTzZaREIvvFPDTXKJ6yOYnuEkhH3OcoOajHN2GEMMQ +ShuyRFDQvYkqOC/Q5icqFbKg7eGwfl4PmimdV7gOVsxSlw2s/0EeeIILXtHx22z3 +8QBhX25Lrq2rMuaGcD3IOMBeBo2d//YuEtd9J+LGXL9AeOXHAwpvInywJKAtXTMq +Wsy3LjhuANFrzMlzjR2YdjkGVzeQVx3dKUzJ2//Qf7IXPSPaEGmcgbxuatxjnvfT +H85oeKr3udKnXm0Kh7CLXeqJB5ITsvxI+Qq2iXtYCc+goHNR01QJwtGDSzuIMj3K +f+YMrqBXZgYBwU2J/kCNTH31nfw96WTbOfNGwLwmVRDgguzFa+QzmQsJW4FTDMwc +7cIjwdElQQVA+Gqa67uWmyDKAnoTkudmgAP+OTBkhnmc6NJuZDcy6f/iWUdl0X0u +/tsfgXXR6ZovnHonM13ANiN7VmEVqFlEMa0VVmc09m+2FYjjlk8F9sC7Rc4wt214 +7u5YvCiCsFZwx44baP5viyRZgkJVpQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/ +MB0GA1UdDgQWBBQgCZCsc34nVTRbWsniXBPjnUTQ2DAOBgNVHQ8BAf8EBAMCAYYw +DQYJKoZIhvcNAQEMBQADggIBAAQas3x1G6OpsIvQeMS9BbiHG3+kU9P/ba6Rrg+E +lUz8TmL04Bcd+I+R0IyMBww4NznT+K60cFdk+1iSmT8Q55bpqRekyhcdWda1Qu0r +JiTi7zz+3w2v66akofOnGevDpo/ilXGvCUJiLOBnHIF0izUqzvfczaMZGJT6xzKq +PcEVRyAN1IHHf5KnGzUlVFv9SGy47xJ9I1vTk24JU0LWkSLzMMoxiUudVmHSqJtN +u0h+n/x3Q6XguZi1/C1KOntH56ewRh8n5AF7c+9LJJSRM9wunb0Dzl7BEy21Xe9q +03xRYjf5wn8eDELB8FZPa1PrNKXIOLYM9egdctbKEcpSsse060+tkyBrl507+SJT +04lvJ4tcKjZFqxn+bUkDQvXYj0D3WK+iJ7a8kZJPRvz8BDHfIqancY8Tgw+69SUn +WqIb+HNZqFuRs16WFSzlMksqzXv6wcDSyI7aZOmCGGEcYW9NHk8EuOnOQ+1UMT9C +Qb1GJcipjRzry3M4KN/t5vN3hIetB+/PhmgTO4gKhBETTEyPC3HC1QbdVfRndB6e +U/NF2U/t8U2GvD26TTFLK4pScW7gyw4FQyXWs8g8FS8f+R2yWajhtS9++VDJQKom +fAUISoCH+PlPRJpu/nHd1Zrddeiiis53rBaLbXu2J1Q3VqjWOmtj0HjxJJxWnYmz +Pqj2 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIGATCCA+mgAwIBAgIRAI/U4z6+GF8/znpHM8Dq8G0wDQYJKoZIhvcNAQEMBQAw +gZgxCzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJ +bmMuMRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTExMC8GA1UEAwwo +QW1hem9uIFJEUyBhcC1zb3V0aC0yIFJvb3QgQ0EgUlNBNDA5NiBHMTEQMA4GA1UE +BwwHU2VhdHRsZTAgFw0yMjA2MDYyMTQ4MThaGA8yMTIyMDYwNjIyNDgxOFowgZgx +CzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJbmMu +MRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTExMC8GA1UEAwwoQW1h +em9uIFJEUyBhcC1zb3V0aC0yIFJvb3QgQ0EgUlNBNDA5NiBHMTEQMA4GA1UEBwwH +U2VhdHRsZTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK5WqMvyq888 +3uuOtEj1FcP6iZhqO5kJurdJF59Otp2WCg+zv6I+QwaAspEWHQsKD405XfFsTGKV +SKTCwoMxwBniuChSmyhlagQGKSnRY9+znOWq0v7hgmJRwp6FqclTbubmr+K6lzPy +hs86mEp68O5TcOTYWUlPZDqfKwfNTbtCl5YDRr8Gxb5buHmkp6gUSgDkRsXiZ5VV +b3GBmXRqbnwo5ZRNAzQeM6ylXCn4jKs310lQGUrFbrJqlyxUdfxzqdlaIRn2X+HY +xRSYbHox3LVNPpJxYSBRvpQVFSy9xbX8d1v6OM8+xluB31cbLBtm08KqPFuqx+cO +I2H5F0CYqYzhyOSKJsiOEJT6/uH4ewryskZzncx9ae62SC+bB5n3aJLmOSTkKLFY +YS5IsmDT2m3iMgzsJNUKVoCx2zihAzgBanFFBsG+Xmoq0aKseZUI6vd2qpd5tUST +/wS1sNk0Ph7teWB2ACgbFE6etnJ6stwjHFZOj/iTYhlnR2zDRU8akunFdGb6CB4/ +hMxGJxaqXSJeGtHm7FpadlUTf+2ESbYcVW+ui/F8sdBJseQdKZf3VdZZMgM0bcaX +NE47cauDTy72WdU9YJX/YXKYMLDE0iFHTnGpfVGsuWGPYhlwZ3dFIO07mWnCRM6X +u5JXRB1oy5n5HRluMsmpSN/R92MeBxKFAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMB +Af8wHQYDVR0OBBYEFNtH0F0xfijSLHEyIkRGD9gW6NazMA4GA1UdDwEB/wQEAwIB +hjANBgkqhkiG9w0BAQwFAAOCAgEACo+5jFeY3ygxoDDzL3xpfe5M0U1WxdKk+az4 +/OfjZvkoma7WfChi3IIMtwtKLYC2/seKWA4KjlB3rlTsCVNPnK6D+gAnybcfTKk/ +IRSPk92zagwQkSUWtAk80HpVfWJzpkSU16ejiajhedzOBRtg6BwsbSqLCDXb8hXr +eXWC1S9ZceGc+LcKRHewGWPu31JDhHE9bNcl9BFSAS0lYVZqxIRWxivZ+45j5uQv +wPrC8ggqsdU3K8quV6dblUQzzA8gKbXJpCzXZihkPrYpQHTH0szvXvgebh+CNUAG +rUxm8+yTS0NFI3U+RLbcLFVzSvjMOnEwCX0SPj5XZRYYXs5ajtQCoZhTUkkwpDV8 +RxXk8qGKiXwUxDO8GRvmvM82IOiXz5w2jy/h7b7soyIgdYiUydMq4Ja4ogB/xPZa +gf4y0o+bremO15HFf1MkaU2UxPK5FFVUds05pKvpSIaQWbF5lw4LHHj4ZtVup7zF +CLjPWs4Hs/oUkxLMqQDw0FBwlqa4uot8ItT8uq5BFpz196ZZ+4WXw5PVzfSxZibI +C/nwcj0AS6qharXOs8yPnPFLPSZ7BbmWzFDgo3tpglRqo3LbSPsiZR+sLeivqydr +0w4RK1btRda5Ws88uZMmW7+2aufposMKcbAdrApDEAVzHijbB/nolS5nsnFPHZoA +KDPtFEk= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICtzCCAj2gAwIBAgIQVZ5Y/KqjR4XLou8MCD5pOjAKBggqhkjOPQQDAzCBmzEL +MAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIEluYy4x +EzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTQwMgYDVQQDDCtBbWF6 +b24gUkRTIGFwLXNvdXRoZWFzdC00IFJvb3QgQ0EgRUNDMzg0IEcxMRAwDgYDVQQH +DAdTZWF0dGxlMCAXDTIyMDUyNTE2NTgzM1oYDzIxMjIwNTI1MTc1ODMzWjCBmzEL +MAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIEluYy4x +EzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTQwMgYDVQQDDCtBbWF6 +b24gUkRTIGFwLXNvdXRoZWFzdC00IFJvb3QgQ0EgRUNDMzg0IEcxMRAwDgYDVQQH +DAdTZWF0dGxlMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEbo473OmpD5vkckdJajXg +brhmNFyoSa0WCY1njuZC2zMFp3zP6rX4I1r3imrYnJd9pFH/aSiV/r6L5ACE5RPx +4qdg5SQ7JJUaZc3DWsTOiOed7BCZSzM+KTYK/2QzDMApo0IwQDAPBgNVHRMBAf8E +BTADAQH/MB0GA1UdDgQWBBTmogc06+1knsej1ltKUOdWFvwgsjAOBgNVHQ8BAf8E +BAMCAYYwCgYIKoZIzj0EAwMDaAAwZQIxAIs7TlLMbGTWNXpGiKf9DxaM07d/iDHe +F/Vv/wyWSTGdobxBL6iArQNVXz0Gr4dvPAIwd0rsoa6R0x5mtvhdRPtM37FYrbHJ +pbV+OMusQqcSLseunLBoCHenvJW0QOCQ8EDY +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICvTCCAkOgAwIBAgIQCIY7E/bFvFN2lK9Kckb0dTAKBggqhkjOPQQDAzCBnjEL +MAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIEluYy4x +EzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTcwNQYDVQQDDC5BbWF6 +b24gUkRTIFByZXZpZXcgdXMtZWFzdC0yIFJvb3QgQ0EgRUNDMzg0IEcxMRAwDgYD +VQQHDAdTZWF0dGxlMCAXDTIxMDUxODIxMDUxMFoYDzIxMjEwNTE4MjIwNTEwWjCB +njELMAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIElu +Yy4xEzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTcwNQYDVQQDDC5B +bWF6b24gUkRTIFByZXZpZXcgdXMtZWFzdC0yIFJvb3QgQ0EgRUNDMzg0IEcxMRAw +DgYDVQQHDAdTZWF0dGxlMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEMI0hzf1JCEOI +Eue4+DmcNnSs2i2UaJxHMrNGGfU7b42a7vwP53F7045ffHPBGP4jb9q02/bStZzd +VHqfcgqkSRI7beBKjD2mfz82hF/wJSITTgCLs+NRpS6zKMFOFHUNo0IwQDAPBgNV +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBS8uF/6hk5mPLH4qaWv9NVZaMmyTjAOBgNV +HQ8BAf8EBAMCAYYwCgYIKoZIzj0EAwMDaAAwZQIxAO7Pu9wzLyM0X7Q08uLIL+vL +qaxe3UFuzFTWjM16MLJHbzLf1i9IDFKz+Q4hXCSiJwIwClMBsqT49BPUxVsJnjGr +EbyEk6aOOVfY1p2yQL649zh3M4h8okLnwf+bYIb1YpeU +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEADCCAuigAwIBAgIQY+JhwFEQTe36qyRlUlF8ozANBgkqhkiG9w0BAQsFADCB +mDELMAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIElu +Yy4xEzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTEwLwYDVQQDDChB +bWF6b24gUkRTIGFmLXNvdXRoLTEgUm9vdCBDQSBSU0EyMDQ4IEcxMRAwDgYDVQQH +DAdTZWF0dGxlMCAXDTIxMDUxOTE5MjQxNloYDzIwNjEwNTE5MjAyNDE2WjCBmDEL +MAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIEluYy4x +EzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTEwLwYDVQQDDChBbWF6 +b24gUkRTIGFmLXNvdXRoLTEgUm9vdCBDQSBSU0EyMDQ4IEcxMRAwDgYDVQQHDAdT +ZWF0dGxlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnIye77j6ev40 +8wRPyN2OdKFSUfI9jB20Or2RLO+RDoL43+USXdrze0Wv4HMRLqaen9BcmCfaKMp0 +E4SFo47bXK/O17r6G8eyq1sqnHE+v288mWtYH9lAlSamNFRF6YwA7zncmE/iKL8J +0vePHMHP/B6svw8LULZCk+nZk3tgxQn2+r0B4FOz+RmpkoVddfqqUPMbKUxhM2wf +fO7F6bJaUXDNMBPhCn/3ayKCjYr49ErmnpYV2ZVs1i34S+LFq39J7kyv6zAgbHv9 ++/MtRMoRB1CjpqW0jIOZkHBdYcd1o9p1zFn591Do1wPkmMsWdjIYj+6e7UXcHvOB +2+ScIRAcnwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBQGtq2W +YSyMMxpdQ3IZvcGE+nyZqTAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZIhvcNAQELBQAD +ggEBAEgoP3ixJsKSD5FN8dQ01RNHERl/IFbA7TRXfwC+L1yFocKnQh4Mp/msPRSV ++OeHIvemPW/wtZDJzLTOFJ6eTolGekHK1GRTQ6ZqsWiU2fmiOP8ks4oSpI+tQ9Lw +VrfZqTiEcS5wEIqyfUAZZfKDo7W1xp+dQWzfczSBuZJZwI5iaha7+ILM0r8Ckden +TVTapc5pLSoO15v0ziRuQ2bT3V3nwu/U0MRK44z+VWOJdSiKxdnOYDs8hFNnKhfe +klbTZF7kW7WbiNYB43OaAQBJ6BALZsIskEaqfeZT8FD71uN928TcEQyBDXdZpRN+ +iGQZDGhht0r0URGMDSs9waJtTfA= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIF/jCCA+agAwIBAgIQXY/dmS+72lZPranO2JM9jjANBgkqhkiG9w0BAQwFADCB +lzELMAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIElu +Yy4xEzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTAwLgYDVQQDDCdB +bWF6b24gUkRTIGFwLWVhc3QtMSBSb290IENBIFJTQTQwOTYgRzExEDAOBgNVBAcM +B1NlYXR0bGUwIBcNMjEwNTI1MjEzNDUxWhgPMjEyMTA1MjUyMjM0NTFaMIGXMQsw +CQYDVQQGEwJVUzEiMCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywgSW5jLjET +MBEGA1UECwwKQW1hem9uIFJEUzELMAkGA1UECAwCV0ExMDAuBgNVBAMMJ0FtYXpv +biBSRFMgYXAtZWFzdC0xIFJvb3QgQ0EgUlNBNDA5NiBHMTEQMA4GA1UEBwwHU2Vh +dHRsZTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMyW9kBJjD/hx8e8 +b5E1sF42bp8TXsz1htSYE3Tl3T1Aq379DfEhB+xa/ASDZxt7/vwa81BkNo4M6HYq +okYIXeE7cu5SnSgjWXqcERhgPevtAwgmhdE3yREe8oz2DyOi2qKKZqah+1gpPaIQ +fK0uAqoeQlyHosye3KZZKkDHBatjBsQ5kf8lhuf7wVulEZVRHY2bP2X7N98PfbpL +QdH7mWXzDtJJ0LiwFwds47BrkgK1pkHx2p1mTo+HMkfX0P6Fq1atkVC2RHHtbB/X +iYyH7paaHBzviFrhr679zNqwXIOKlbf74w3mS11P76rFn9rS1BAH2Qm6eY5S/Fxe +HEKXm4kjPN63Zy0p3yE5EjPt54yPkvumOnT+RqDGJ2HCI9k8Ehcbve0ogfdRKNqQ +VHWYTy8V33ndQRHZlx/CuU1yN61TH4WSoMly1+q1ihTX9sApmlQ14B2pJi/9DnKW +cwECrPy1jAowC2UJ45RtC8UC05CbP9yrIy/7Noj8gQDiDOepm+6w1g6aNlWoiuQS +kyI6nzz1983GcnOHya73ga7otXo0Qfg9jPghlYiMomrgshlSLDHZG0Ib/3hb8cnR +1OcN9FpzNmVK2Ll1SmTMLrIhuCkyNYX9O/bOknbcf706XeESxGduSkHEjIw/k1+2 +Atteoq5dT6cwjnJ9hyhiueVlVkiDAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8w +HQYDVR0OBBYEFLUI+DD7RJs+0nRnjcwIVWzzYSsFMA4GA1UdDwEB/wQEAwIBhjAN +BgkqhkiG9w0BAQwFAAOCAgEAb1mcCHv4qMQetLGTBH9IxsB2YUUhr5dda0D2BcHr +UtDbfd0VQs4tux6h/6iKwHPx0Ew8fuuYj99WknG0ffgJfNc5/fMspxR/pc1jpdyU +5zMQ+B9wi0lOZPO9uH7/pr+d2odcNEy8zAwqdv/ihsTwLmGP54is9fVbsgzNW1cm +HKAVL2t/Ope+3QnRiRilKCN1lzhav4HHdLlN401TcWRWKbEuxF/FgxSO2Hmx86pj +e726lweCTMmnq/cTsPOVY0WMjs0or3eHDVlyLgVeV5ldyN+ptg3Oit60T05SRa58 +AJPTaVKIcGQ/gKkKZConpu7GDofT67P/ox0YNY57LRbhsx9r5UY4ROgz7WMQ1yoS +Y+19xizm+mBm2PyjMUbfwZUyCxsdKMwVdOq5/UmTmdms+TR8+m1uBHPOTQ2vKR0s +Pd/THSzPuu+d3dbzRyDSLQbHFFneG760CUlD/ZmzFlQjJ89/HmAmz8IyENq+Sjhx +Jgzy+FjVZb8aRUoYLlnffpUpej1n87Ynlr1GrvC4GsRpNpOHlwuf6WD4W0qUTsC/ +C9JO+fBzUj/aWlJzNcLEW6pte1SB+EdkR2sZvWH+F88TxemeDrV0jKJw5R89CDf8 +ZQNfkxJYjhns+YeV0moYjqQdc7tq4i04uggEQEtVzEhRLU5PE83nlh/K2NZZm8Kj +dIA= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIID/zCCAuegAwIBAgIRAPVSMfFitmM5PhmbaOFoGfUwDQYJKoZIhvcNAQELBQAw +gZcxCzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJ +bmMuMRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTEwMC4GA1UEAwwn +QW1hem9uIFJEUyB1cy1lYXN0LTEgUm9vdCBDQSBSU0EyMDQ4IEcxMRAwDgYDVQQH +DAdTZWF0dGxlMCAXDTIxMDUyNTIyMzQ1N1oYDzIwNjEwNTI1MjMzNDU3WjCBlzEL +MAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIEluYy4x +EzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTAwLgYDVQQDDCdBbWF6 +b24gUkRTIHVzLWVhc3QtMSBSb290IENBIFJTQTIwNDggRzExEDAOBgNVBAcMB1Nl +YXR0bGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDu9H7TBeGoDzMr +dxN6H8COntJX4IR6dbyhnj5qMD4xl/IWvp50lt0VpmMd+z2PNZzx8RazeGC5IniV +5nrLg0AKWRQ2A/lGGXbUrGXCSe09brMQCxWBSIYe1WZZ1iU1IJ/6Bp4D2YEHpXrW +bPkOq5x3YPcsoitgm1Xh8ygz6vb7PsvJvPbvRMnkDg5IqEThapPjmKb8ZJWyEFEE +QRrkCIRueB1EqQtJw0fvP4PKDlCJAKBEs/y049FoOqYpT3pRy0WKqPhWve+hScMd +6obq8kxTFy1IHACjHc51nrGII5Bt76/MpTWhnJIJrCnq1/Uc3Qs8IVeb+sLaFC8K +DI69Sw6bAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFE7PCopt +lyOgtXX0Y1lObBUxuKaCMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOC +AQEAFj+bX8gLmMNefr5jRJfHjrL3iuZCjf7YEZgn89pS4z8408mjj9z6Q5D1H7yS +jNETVV8QaJip1qyhh5gRzRaArgGAYvi2/r0zPsy+Tgf7v1KGL5Lh8NT8iCEGGXwF +g3Ir+Nl3e+9XUp0eyyzBIjHtjLBm6yy8rGk9p6OtFDQnKF5OxwbAgip42CD75r/q +p421maEDDvvRFR4D+99JZxgAYDBGqRRceUoe16qDzbMvlz0A9paCZFclxeftAxv6 +QlR5rItMz/XdzpBJUpYhdzM0gCzAzdQuVO5tjJxmXhkSMcDP+8Q+Uv6FA9k2VpUV +E/O5jgpqUJJ2Hc/5rs9VkAPXeA== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICrzCCAjWgAwIBAgIQW0yuFCle3uj4vWiGU0SaGzAKBggqhkjOPQQDAzCBlzEL +MAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIEluYy4x +EzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTAwLgYDVQQDDCdBbWF6 +b24gUkRTIGFmLXNvdXRoLTEgUm9vdCBDQSBFQ0MzODQgRzExEDAOBgNVBAcMB1Nl +YXR0bGUwIBcNMjEwNTE5MTkzNTE2WhgPMjEyMTA1MTkyMDM1MTZaMIGXMQswCQYD +VQQGEwJVUzEiMCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywgSW5jLjETMBEG +A1UECwwKQW1hem9uIFJEUzELMAkGA1UECAwCV0ExMDAuBgNVBAMMJ0FtYXpvbiBS +RFMgYWYtc291dGgtMSBSb290IENBIEVDQzM4NCBHMTEQMA4GA1UEBwwHU2VhdHRs +ZTB2MBAGByqGSM49AgEGBSuBBAAiA2IABDPiKNZSaXs3Un/J/v+LTsFDANHpi7en +oL2qh0u0DoqNzEBTbBjvO23bLN3k599zh6CY3HKW0r2k1yaIdbWqt4upMCRCcUFi +I4iedAmubgzh56wJdoMZztjXZRwDthTkJKNCMEAwDwYDVR0TAQH/BAUwAwEB/zAd +BgNVHQ4EFgQUWbYkcrvVSnAWPR5PJhIzppcAnZIwDgYDVR0PAQH/BAQDAgGGMAoG +CCqGSM49BAMDA2gAMGUCMCESGqpat93CjrSEjE7z+Hbvz0psZTHwqaxuiH64GKUm +mYynIiwpKHyBrzjKBmeDoQIxANGrjIo6/b8Jl6sdIZQI18V0pAyLfLiZjlHVOnhM +MOTVgr82ZuPoEHTX78MxeMnYlw== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIECTCCAvGgAwIBAgIRAIbsx8XOl0sgTNiCN4O+18QwDQYJKoZIhvcNAQELBQAw +gZwxCzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJ +bmMuMRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTE1MDMGA1UEAwws +QW1hem9uIFJEUyBhcC1ub3J0aGVhc3QtMSBSb290IENBIFJTQTIwNDggRzExEDAO +BgNVBAcMB1NlYXR0bGUwIBcNMjEwNTI1MjE1NDU4WhgPMjA2MTA1MjUyMjU0NTha +MIGcMQswCQYDVQQGEwJVUzEiMCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywg +SW5jLjETMBEGA1UECwwKQW1hem9uIFJEUzELMAkGA1UECAwCV0ExNTAzBgNVBAMM +LEFtYXpvbiBSRFMgYXAtbm9ydGhlYXN0LTEgUm9vdCBDQSBSU0EyMDQ4IEcxMRAw +DgYDVQQHDAdTZWF0dGxlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA +tROxwXWCgn5R9gI/2Ivjzaxc0g95ysBjoJsnhPdJEHQb7w3y2kWrVWU3Y9fOitgb +CEsnEC3PrhRnzNVW0fPsK6kbvOeCmjvY30rdbxbc8h+bjXfGmIOgAkmoULEr6Hc7 +G1Q/+tvv4lEwIs7bEaf+abSZxRJbZ0MBxhbHn7UHHDiMZYvzK+SV1MGCxx7JVhrm +xWu3GC1zZCsGDhB9YqY9eR6PmjbqA5wy8vqbC57dZZa1QVtWIQn3JaRXn+faIzHx +nLMN5CEWihsdmHBXhnRboXprE/OS4MFv1UrQF/XM/h5RBeCywpHePpC+Oe1T3LNC +iP8KzRFrjC1MX/WXJnmOVQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1Ud +DgQWBBS33XbXAUMs1znyZo4B0+B3D68WFTAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZI +hvcNAQELBQADggEBADuadd2EmlpueY2VlrIIPC30QkoA1EOSoCmZgN6124apkoY1 +HiV4r+QNPljN4WP8gmcARnNkS7ZeR4fvWi8xPh5AxQCpiaBMw4gcbTMCuKDV68Pw +P2dZCTMspvR3CDfM35oXCufdtFnxyU6PAyINUqF/wyTHguO3owRFPz64+sk3r2pT +WHmJjG9E7V+KOh0s6REgD17Gqn6C5ijLchSrPUHB0wOIkeLJZndHxN/76h7+zhMt +fFeNxPWHY2MfpcaLjz4UREzZPSB2U9k+y3pW1omCIcl6MQU9itGx/LpQE+H3ZeX2 +M2bdYd5L+ow+bdbGtsVKOuN+R9Dm17YpswF+vyQ= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIGATCCA+mgAwIBAgIRAKlQ+3JX9yHXyjP/Ja6kZhkwDQYJKoZIhvcNAQEMBQAw +gZgxCzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJ +bmMuMRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTExMC8GA1UEAwwo +QW1hem9uIFJEUyBhcC1zb3V0aC0xIFJvb3QgQ0EgUlNBNDA5NiBHMTEQMA4GA1UE +BwwHU2VhdHRsZTAgFw0yMTA1MTkxNzQ1MjBaGA8yMTIxMDUxOTE4NDUyMFowgZgx +CzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJbmMu +MRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTExMC8GA1UEAwwoQW1h +em9uIFJEUyBhcC1zb3V0aC0xIFJvb3QgQ0EgUlNBNDA5NiBHMTEQMA4GA1UEBwwH +U2VhdHRsZTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAKtahBrpUjQ6 +H2mni05BAKU6Z5USPZeSKmBBJN3YgD17rJ93ikJxSgzJ+CupGy5rvYQ0xznJyiV0 +91QeQN4P+G2MjGQR0RGeUuZcfcZitJro7iAg3UBvw8WIGkcDUg+MGVpRv/B7ry88 +7E4OxKb8CPNoa+a9j6ABjOaaxaI22Bb7j3OJ+JyMICs6CU2bgkJaj3VUV9FCNUOc +h9PxD4jzT9yyGYm/sK9BAT1WOTPG8XQUkpcFqy/IerZDfiQkf1koiSd4s5VhBkUn +aQHOdri/stldT7a+HJFVyz2AXDGPDj+UBMOuLq0K6GAT6ThpkXCb2RIf4mdTy7ox +N5BaJ+ih+Ro3ZwPkok60egnt/RN98jgbm+WstgjJWuLqSNInnMUgkuqjyBWwePqX +Kib+wdpyx/LOzhKPEFpeMIvHQ3A0sjlulIjnh+j+itezD+dp0UNxMERlW4Bn/IlS +sYQVNfYutWkRPRLErXOZXtlxxkI98JWQtLjvGzQr+jywxTiw644FSLWdhKa6DtfU +2JWBHqQPJicMElfZpmfaHZjtXuCZNdZQXWg7onZYohe281ZrdFPOqC4rUq7gYamL +T+ZB+2P+YCPOLJ60bj/XSvcB7mesAdg8P0DNddPhHUFWx2dFqOs1HxIVB4FZVA9U +Ppbv4a484yxjTgG7zFZNqXHKTqze6rBBAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMB +Af8wHQYDVR0OBBYEFCEAqjighncv/UnWzBjqu1Ka2Yb4MA4GA1UdDwEB/wQEAwIB +hjANBgkqhkiG9w0BAQwFAAOCAgEAYyvumblckIXlohzi3QiShkZhqFzZultbFIu9 +GhA5CDar1IFMhJ9vJpO9nUK/camKs1VQRs8ZsBbXa0GFUM2p8y2cgUfLwFULAiC/ +sWETyW5lcX/xc4Pyf6dONhqFJt/ovVBxNZtcmMEWv/1D6Tf0nLeEb0P2i/pnSRR4 +Oq99LVFjossXtyvtaq06OSiUUZ1zLPvV6AQINg8dWeBOWRcQYhYcEcC2wQ06KShZ +0ahuu7ar5Gym3vuLK6nH+eQrkUievVomN/LpASrYhK32joQ5ypIJej3sICIgJUEP +UoeswJ+Z16f3ECoL1OSnq4A0riiLj1ZGmVHNhM6m/gotKaHNMxsK9zsbqmuU6IT/ +P6cR0S+vdigQG8ZNFf5vEyVNXhl8KcaJn6lMD/gMB2rY0qpaeTg4gPfU5wcg8S4Y +C9V//tw3hv0f2n+8kGNmqZrylOQDQWSSo8j8M2SRSXiwOHDoTASd1fyBEIqBAwzn +LvXVg8wQd1WlmM3b0Vrsbzltyh6y4SuKSkmgufYYvC07NknQO5vqvZcNoYbLNea3 +76NkFaMHUekSbwVejZgG5HGwbaYBgNdJEdpbWlA3X4yGRVxknQSUyt4dZRnw/HrX +k8x6/wvtw7wht0/DOqz1li7baSsMazqxx+jDdSr1h9xML416Q4loFCLgqQhil8Jq +Em4Hy3A= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIGBTCCA+2gAwIBAgIRAJfKe4Zh4aWNt3bv6ZjQwogwDQYJKoZIhvcNAQEMBQAw +gZoxCzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJ +bmMuMRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTEzMDEGA1UEAwwq +QW1hem9uIFJEUyBjYS1jZW50cmFsLTEgUm9vdCBDQSBSU0E0MDk2IEcxMRAwDgYD +VQQHDAdTZWF0dGxlMCAXDTIxMDUyMTIyMDg1M1oYDzIxMjEwNTIxMjMwODUzWjCB +mjELMAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIElu +Yy4xEzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTMwMQYDVQQDDCpB +bWF6b24gUkRTIGNhLWNlbnRyYWwtMSBSb290IENBIFJTQTQwOTYgRzExEDAOBgNV +BAcMB1NlYXR0bGUwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCpgUH6 +Crzd8cOw9prAh2rkQqAOx2vtuI7xX4tmBG4I/um28eBjyVmgwQ1fpq0Zg2nCKS54 +Nn0pCmT7f3h6Bvopxn0J45AzXEtajFqXf92NQ3iPth95GVfAJSD7gk2LWMhpmID9 +JGQyoGuDPg+hYyr292X6d0madzEktVVGO4mKTF989qEg+tY8+oN0U2fRTrqa2tZp +iYsmg350ynNopvntsJAfpCO/srwpsqHHLNFZ9jvhTU8uW90wgaKO9i31j/mHggCE ++CAOaJCM3g+L8DPl/2QKsb6UkBgaaIwKyRgKSj1IlgrK+OdCBCOgM9jjId4Tqo2j +ZIrrPBGl6fbn1+etZX+2/tf6tegz+yV0HHQRAcKCpaH8AXF44bny9andslBoNjGx +H6R/3ib4FhPrnBMElzZ5i4+eM/cuPC2huZMBXb/jKgRC/QN1Wm3/nah5FWq+yn+N +tiAF10Ga0BYzVhHDEwZzN7gn38bcY5yi/CjDUNpY0OzEe2+dpaBKPlXTaFfn9Nba +CBmXPRF0lLGGtPeTAgjcju+NEcVa82Ht1pqxyu2sDtbu3J5bxp4RKtj+ShwN8nut +Tkf5Ea9rSmHEY13fzgibZlQhXaiFSKA2ASUwgJP19Putm0XKlBCNSGCoECemewxL ++7Y8FszS4Uu4eaIwvXVqUEE2yf+4ex0hqQ1acQIDAQABo0IwQDAPBgNVHRMBAf8E +BTADAQH/MB0GA1UdDgQWBBSeUnXIRxNbYsZLtKomIz4Y1nOZEzAOBgNVHQ8BAf8E +BAMCAYYwDQYJKoZIhvcNAQEMBQADggIBAIpRvxVS0dzoosBh/qw65ghPUGSbP2D4 +dm6oYCv5g/zJr4fR7NzEbHOXX5aOQnHbQL4M/7veuOCLNPOW1uXwywMg6gY+dbKe +YtPVA1as8G9sUyadeXyGh2uXGsziMFXyaESwiAXZyiYyKChS3+g26/7jwECFo5vC +XGhWpIO7Hp35Yglp8AnwnEAo/PnuXgyt2nvyTSrxlEYa0jus6GZEZd77pa82U1JH +qFhIgmKPWWdvELA3+ra1nKnvpWM/xX0pnMznMej5B3RT3Y+k61+kWghJE81Ix78T ++tG4jSotgbaL53BhtQWBD1yzbbilqsGE1/DXPXzHVf9yD73fwh2tGWSaVInKYinr +a4tcrB3KDN/PFq0/w5/21lpZjVFyu/eiPj6DmWDuHW73XnRwZpHo/2OFkei5R7cT +rn/YdDD6c1dYtSw5YNnS6hdCQ3sOiB/xbPRN9VWJa6se79uZ9NLz6RMOr73DNnb2 +bhIR9Gf7XAA5lYKqQk+A+stoKbIT0F65RnkxrXi/6vSiXfCh/bV6B41cf7MY/6YW +ehserSdjhQamv35rTFdM+foJwUKz1QN9n9KZhPxeRmwqPitAV79PloksOnX25ElN +SlyxdndIoA1wia1HRd26EFm2pqfZ2vtD2EjU3wD42CXX4H8fKVDna30nNFSYF0yn +jGKc3k6UNxpg +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIF/jCCA+agAwIBAgIQaRHaEqqacXN20e8zZJtmDDANBgkqhkiG9w0BAQwFADCB +lzELMAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIElu +Yy4xEzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTAwLgYDVQQDDCdB +bWF6b24gUkRTIHVzLWVhc3QtMSBSb290IENBIFJTQTQwOTYgRzExEDAOBgNVBAcM +B1NlYXR0bGUwIBcNMjEwNTI1MjIzODM1WhgPMjEyMTA1MjUyMzM4MzVaMIGXMQsw +CQYDVQQGEwJVUzEiMCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywgSW5jLjET +MBEGA1UECwwKQW1hem9uIFJEUzELMAkGA1UECAwCV0ExMDAuBgNVBAMMJ0FtYXpv +biBSRFMgdXMtZWFzdC0xIFJvb3QgQ0EgUlNBNDA5NiBHMTEQMA4GA1UEBwwHU2Vh +dHRsZTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAInfBCaHuvj6Rb5c +L5Wmn1jv2PHtEGMHm+7Z8dYosdwouG8VG2A+BCYCZfij9lIGszrTXkY4O7vnXgru +JUNdxh0Q3M83p4X+bg+gODUs3jf+Z3Oeq7nTOk/2UYvQLcxP4FEXILxDInbQFcIx +yen1ESHggGrjEodgn6nbKQNRfIhjhW+TKYaewfsVWH7EF2pfj+cjbJ6njjgZ0/M9 +VZifJFBgat6XUTOf3jwHwkCBh7T6rDpgy19A61laImJCQhdTnHKvzTpxcxiLRh69 +ZObypR7W04OAUmFS88V7IotlPmCL8xf7kwxG+gQfvx31+A9IDMsiTqJ1Cc4fYEKg +bL+Vo+2Ii4W2esCTGVYmHm73drznfeKwL+kmIC/Bq+DrZ+veTqKFYwSkpHRyJCEe +U4Zym6POqQ/4LBSKwDUhWLJIlq99bjKX+hNTJykB+Lbcx0ScOP4IAZQoxmDxGWxN +S+lQj+Cx2pwU3S/7+OxlRndZAX/FKgk7xSMkg88HykUZaZ/ozIiqJqSnGpgXCtED +oQ4OJw5ozAr+/wudOawaMwUWQl5asD8fuy/hl5S1nv9XxIc842QJOtJFxhyeMIXt +LVECVw/dPekhMjS3Zo3wwRgYbnKG7YXXT5WMxJEnHu8+cYpMiRClzq2BEP6/MtI2 +AZQQUFu2yFjRGL2OZA6IYjxnXYiRAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8w +HQYDVR0OBBYEFADCcQCPX2HmkqQcmuHfiQ2jjqnrMA4GA1UdDwEB/wQEAwIBhjAN +BgkqhkiG9w0BAQwFAAOCAgEASXkGQ2eUmudIKPeOIF7RBryCoPmMOsqP0+1qxF8l +pGkwmrgNDGpmd9s0ArfIVBTc1jmpgB3oiRW9c6n2OmwBKL4UPuQ8O3KwSP0iD2sZ +KMXoMEyphCEzW1I2GRvYDugL3Z9MWrnHkoaoH2l8YyTYvszTvdgxBPpM2x4pSkp+ +76d4/eRpJ5mVuQ93nC+YG0wXCxSq63hX4kyZgPxgCdAA+qgFfKIGyNqUIqWgeyTP +n5OgKaboYk2141Rf2hGMD3/hsGm0rrJh7g3C0ZirPws3eeJfulvAOIy2IZzqHUSY +jkFzraz6LEH3IlArT3jUPvWKqvh2lJWnnp56aqxBR7qHH5voD49UpJWY1K0BjGnS +OHcurpp0Yt/BIs4VZeWdCZwI7JaSeDcPMaMDBvND3Ia5Fga0thgYQTG6dE+N5fgF +z+hRaujXO2nb0LmddVyvE8prYlWRMuYFv+Co8hcMdJ0lEZlfVNu0jbm9/GmwAZ+l +9umeYO9yz/uC7edC8XJBglMAKUmVK9wNtOckUWAcCfnPWYLbYa/PqtXBYcxrso5j +iaS/A7iEW51uteHBGrViCy1afGG+hiUWwFlesli+Rq4dNstX3h6h2baWABaAxEVJ +y1RnTQSz6mROT1VmZSgSVO37rgIyY0Hf0872ogcTS+FfvXgBxCxsNWEbiQ/XXva4 +0Ws= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICtDCCAjqgAwIBAgIRAMyaTlVLN0ndGp4ffwKAfoMwCgYIKoZIzj0EAwMwgZkx +CzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJbmMu +MRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTEyMDAGA1UEAwwpQW1h +em9uIFJEUyBtZS1jZW50cmFsLTEgUm9vdCBDQSBFQ0MzODQgRzExEDAOBgNVBAcM +B1NlYXR0bGUwIBcNMjIwNTA3MDA0NDM3WhgPMjEyMjA1MDcwMTQ0MzdaMIGZMQsw +CQYDVQQGEwJVUzEiMCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywgSW5jLjET +MBEGA1UECwwKQW1hem9uIFJEUzELMAkGA1UECAwCV0ExMjAwBgNVBAMMKUFtYXpv +biBSRFMgbWUtY2VudHJhbC0xIFJvb3QgQ0EgRUNDMzg0IEcxMRAwDgYDVQQHDAdT +ZWF0dGxlMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE19nCV1nsI6CohSor13+B25cr +zg+IHdi9Y3L7ziQnHWI6yjBazvnKD+oC71aRRlR8b5YXsYGUQxWzPLHN7EGPcSGv +bzA9SLG1KQYCJaQ0m9Eg/iGrwKWOgylbhVw0bCxoo0IwQDAPBgNVHRMBAf8EBTAD +AQH/MB0GA1UdDgQWBBS4KsknsJXM9+QPEkBdZxUPaLr11zAOBgNVHQ8BAf8EBAMC +AYYwCgYIKoZIzj0EAwMDaAAwZQIxAJaRgrYIEfXQMZQQDxMTYS0azpyWSseQooXo +L3nYq4OHGBgYyQ9gVjvRYWU85PXbfgIwdi82DtANQFkCu+j+BU0JBY/uRKPEeYzo +JG92igKIcXPqCoxIJ7lJbbzmuf73gQu5 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIGATCCA+mgAwIBAgIRAJwCobx0Os8F7ihbJngxrR8wDQYJKoZIhvcNAQEMBQAw +gZgxCzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJ +bmMuMRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTExMC8GA1UEAwwo +QW1hem9uIFJEUyBtZS1zb3V0aC0xIFJvb3QgQ0EgUlNBNDA5NiBHMTEQMA4GA1UE +BwwHU2VhdHRsZTAgFw0yMTA1MjAxNzE1MzNaGA8yMTIxMDUyMDE4MTUzM1owgZgx +CzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJbmMu +MRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTExMC8GA1UEAwwoQW1h +em9uIFJEUyBtZS1zb3V0aC0xIFJvb3QgQ0EgUlNBNDA5NiBHMTEQMA4GA1UEBwwH +U2VhdHRsZTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANukKwlm+ZaI +Y5MkWGbEVLApEyLmlrHLEg8PfiiEa9ts7jssQcin3bzEPdTqGr5jo91ONoZ3ccWq +xJgg1W3bLu5CAO2CqIOXTXHRyCO/u0Ch1FGgWB8xETPSi3UHt/Vn1ltdO6DYdbDU +mYgwzYrvLBdRCwxsb9o+BuYQHVFzUYonqk/y9ujz3gotzFq7r55UwDTA1ita3vb4 +eDKjIb4b1M4Wr81M23WHonpje+9qkkrAkdQcHrkgvSCV046xsq/6NctzwCUUNsgF +7Q1a8ut5qJEYpz5ta8vI1rqFqAMBqCbFjRYlmAoTTpFPOmzAVxV+YoqTrW5A16su +/2SXlMYfJ/n/ad/QfBNPPAAQMpyOr2RCL/YiL/PFZPs7NxYjnZHNWxMLSPgFyI+/ +t2klnn5jR76KJK2qimmaXedB90EtFsMRUU1e4NxH9gDuyrihKPJ3aVnZ35mSipvR +/1KB8t8gtFXp/VQaz2sg8+uxPMKB81O37fL4zz6Mg5K8+aq3ejBiyHucpFGnsnVB +3kQWeD36ONkybngmgWoyPceuSWm1hQ0Z7VRAQX+KlxxSaHmSaIk1XxZu9h9riQHx +fMuev6KXjRn/CjCoUTn+7eFrt0dT5GryQEIZP+nA0oq0LKxogigHNZlwAT4flrqb +JUfZJrqgoce5HjZSXl10APbtPjJi0fW9AgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMB +Af8wHQYDVR0OBBYEFEfV+LztI29OVDRm0tqClP3NrmEWMA4GA1UdDwEB/wQEAwIB +hjANBgkqhkiG9w0BAQwFAAOCAgEAvSNe+0wuk53KhWlRlRf2x/97H2Q76X3anzF0 +5fOSVm022ldALzXMzqOfdnoKIhAu2oVKiHHKs7mMas+T6TL+Mkphx0CYEVxFE3PG +061q3CqJU+wMm9W9xsB79oB2XG47r1fIEywZZ3GaRsatAbjcNOT8uBaATPQAfJFN +zjFe4XyN+rA4cFrYNvfHTeu5ftrYmvks7JlRaJgEGWsz+qXux7uvaEEVPqEumd2H +uYeaRNOZ2V23R009X5lbgBFx9tq5VDTnKhQiTQ2SeT0rc1W3Dz5ik6SbQQNP3nSR +0Ywy7r/sZ3fcDyfFiqnrVY4Ympfvb4YW2PZ6OsQJbzH6xjdnTG2HtzEU30ngxdp1 +WUEF4zt6rjJCp7QBUqXgdlHvJqYu6949qtWjEPiFN9uSsRV2i1YDjJqN52dLjAPn +AipJKo8x1PHTwUzuITqnB9BdP+5TlTl8biJfkEf/+08eWDTLlDHr2VrZLOLompTh +bS5OrhDmqA2Q+O+EWrTIhMflwwlCpR9QYM/Xwvlbad9H0FUHbJsCVNaru3wGOgWo +tt3dNSK9Lqnv/Ej9K9v6CRr36in4ylJKivhJ5B9E7ABHg7EpBJ1xi7O5eNDkNoJG ++pFyphJq3AkBR2U4ni2tUaTAtSW2tks7IaiDV+UMtqZyGabT5ISQfWLLtLHSWn2F +Tspdjbg= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIECTCCAvGgAwIBAgIRAJZFh4s9aZGzKaTMLrSb4acwDQYJKoZIhvcNAQELBQAw +gZwxCzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJ +bmMuMRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTE1MDMGA1UEAwws +QW1hem9uIFJEUyBCZXRhIHVzLWVhc3QtMSBSb290IENBIFJTQTIwNDggRzExEDAO +BgNVBAcMB1NlYXR0bGUwIBcNMjEwNTE4MjEyODQxWhgPMjA2MTA1MTgyMjI4NDFa +MIGcMQswCQYDVQQGEwJVUzEiMCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywg +SW5jLjETMBEGA1UECwwKQW1hem9uIFJEUzELMAkGA1UECAwCV0ExNTAzBgNVBAMM +LEFtYXpvbiBSRFMgQmV0YSB1cy1lYXN0LTEgUm9vdCBDQSBSU0EyMDQ4IEcxMRAw +DgYDVQQHDAdTZWF0dGxlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA +17i2yoU6diep+WrqxIn2CrDEO2NdJVwWTSckx4WMZlLpkQDoymSmkNHjq9ADIApD +A31Cx+843apL7wub8QkFZD0Tk7/ThdHWJOzcAM3ov98QBPQfOC1W5zYIIRP2F+vQ +TRETHQnLcW3rLv0NMk5oQvIKpJoC9ett6aeVrzu+4cU4DZVWYlJUoC/ljWzCluau +8blfW0Vwin6OB7s0HCG5/wijQWJBU5SrP/KAIPeQi1GqG5efbqAXDr/ple0Ipwyo +Xjjl73LenGUgqpANlC9EAT4i7FkJcllLPeK3NcOHjuUG0AccLv1lGsHAxZLgjk/x +z9ZcnVV9UFWZiyJTKxeKPwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1Ud +DgQWBBRWyMuZUo4gxCR3Luf9/bd2AqZ7CjAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZI +hvcNAQELBQADggEBAIqN2DlIKlvDFPO0QUZQVFbsi/tLdYM98/vvzBpttlTGVMyD +gJuQeHVz+MnhGIwoCGOlGU3OOUoIlLAut0+WG74qYczn43oA2gbMd7HoD7oL/IGg +njorBwJVcuuLv2G//SqM3nxGcLRtkRnQ+lvqPxMz9+0fKFUn6QcIDuF0QSfthLs2 +WSiGEPKO9c9RSXdRQ4pXA7c3hXng8P4A2ZmdciPne5Nu4I4qLDGZYRrRLRkNTrOi +TyS6r2HNGUfgF7eOSeKt3NWL+mNChcYj71/Vycf5edeczpUgfnWy9WbPrK1svKyl +aAs2xg+X6O8qB+Mnj2dNBzm+lZIS3sIlm+nO9sg= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICrjCCAjSgAwIBAgIRAPAlEk8VJPmEzVRRaWvTh2AwCgYIKoZIzj0EAwMwgZYx +CzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJbmMu +MRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTEvMC0GA1UEAwwmQW1h +em9uIFJEUyB1cy1lYXN0LTEgUm9vdCBDQSBFQ0MzODQgRzExEDAOBgNVBAcMB1Nl +YXR0bGUwIBcNMjEwNTI1MjI0MTU1WhgPMjEyMTA1MjUyMzQxNTVaMIGWMQswCQYD +VQQGEwJVUzEiMCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywgSW5jLjETMBEG +A1UECwwKQW1hem9uIFJEUzELMAkGA1UECAwCV0ExLzAtBgNVBAMMJkFtYXpvbiBS +RFMgdXMtZWFzdC0xIFJvb3QgQ0EgRUNDMzg0IEcxMRAwDgYDVQQHDAdTZWF0dGxl +MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEx5xjrup8II4HOJw15NTnS3H5yMrQGlbj +EDA5MMGnE9DmHp5dACIxmPXPMe/99nO7wNdl7G71OYPCgEvWm0FhdvVUeTb3LVnV +BnaXt32Ek7/oxGk1T+Df03C+W0vmuJ+wo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0G +A1UdDgQWBBTGXmqBWN/1tkSea4pNw0oHrjk2UDAOBgNVHQ8BAf8EBAMCAYYwCgYI +KoZIzj0EAwMDaAAwZQIxAIqqZWCSrIkZ7zsv/FygtAusW6yvlL935YAWYPVXU30m +jkMFLM+/RJ9GMvnO8jHfCgIwB+whlkcItzE9CRQ6CsMo/d5cEHDUu/QW6jSIh9BR +OGh9pTYPVkUbBiKPA7lVVhre +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIF/zCCA+egAwIBAgIRAJGY9kZITwfSRaAS/bSBOw8wDQYJKoZIhvcNAQEMBQAw +gZcxCzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJ +bmMuMRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTEwMC4GA1UEAwwn +QW1hem9uIFJEUyBzYS1lYXN0LTEgUm9vdCBDQSBSU0E0MDk2IEcxMRAwDgYDVQQH +DAdTZWF0dGxlMCAXDTIxMDUxOTE4MTEyMFoYDzIxMjEwNTE5MTkxMTIwWjCBlzEL +MAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIEluYy4x +EzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTAwLgYDVQQDDCdBbWF6 +b24gUkRTIHNhLWVhc3QtMSBSb290IENBIFJTQTQwOTYgRzExEDAOBgNVBAcMB1Nl +YXR0bGUwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDe2vlDp6Eo4WQi +Wi32YJOgdXHhxTFrLjB9SRy22DYoMaWfginJIwJcSR8yse8ZDQuoNhERB9LRggAE +eng23mhrfvtL1yQkMlZfBu4vG1nOb22XiPFzk7X2wqz/WigdYNBCqa1kK3jrLqPx +YUy7jk2oZle4GLVRTNGuMfcid6S2hs3UCdXfkJuM2z2wc3WUlvHoVNk37v2/jzR/ +hSCHZv5YHAtzL/kLb/e64QkqxKll5QmKhyI6d7vt6Lr1C0zb+DmwxUoJhseAS0hI +dRk5DklMb4Aqpj6KN0ss0HAYqYERGRIQM7KKA4+hxDMUkJmt8KqWKZkAlCZgflzl +m8NZ31o2cvBzf6g+VFHx+6iVrSkohVQydkCxx7NJ743iPKsh8BytSM4qU7xx4OnD +H2yNXcypu+D5bZnVZr4Pywq0w0WqbTM2bpYthG9IC4JeVUvZ2mDc01lqOlbMeyfT +og5BRPLDXdZK8lapo7se2teh64cIfXtCmM2lDSwm1wnH2iSK+AWZVIM3iE45WSGc +vZ+drHfVgjJJ5u1YrMCWNL5C2utFbyF9Obw9ZAwm61MSbPQL9JwznhNlCh7F2ANW +ZHWQPNcOAJqzE4uVcJB1ZeVl28ORYY1668lx+s9yYeMXk3QQdj4xmdnvoBFggqRB +ZR6Z0D7ZohADXe024RzEo1TukrQgKQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/ +MB0GA1UdDgQWBBT7Vs4Y5uG/9aXnYGNMEs6ycPUT3jAOBgNVHQ8BAf8EBAMCAYYw +DQYJKoZIhvcNAQEMBQADggIBACN4Htp2PvGcQA0/sAS+qUVWWJoAXSsu8Pgc6Gar +7tKVlNJ/4W/a6pUV2Xo/Tz3msg4yiE8sMESp2k+USosD5n9Alai5s5qpWDQjrqrh +76AGyF2nzve4kIN19GArYhm4Mz/EKEG1QHYvBDGgXi3kNvL/a2Zbybp+3LevG+q7 +xtx4Sz9yIyMzuT/6Y7ijtiMZ9XbuxGf5wab8UtwT3Xq1UradJy0KCkzRJAz/Wy/X +HbTkEvKSaYKExH6sLo0jqdIjV/d2Io31gt4e0Ly1ER2wPyFa+pc/swu7HCzrN+iz +A2ZM4+KX9nBvFyfkHLix4rALg+WTYJa/dIsObXkdZ3z8qPf5A9PXlULiaa1mcP4+ +rokw74IyLEYooQ8iSOjxumXhnkTS69MAdGzXYE5gnHokABtGD+BB5qLhtLt4fqAp +8AyHpQWMyV42M9SJLzQ+iOz7kAgJOBOaVtJI3FV/iAg/eqWVm3yLuUTWDxSHrKuL +N19+pSjF6TNvUSFXwEa2LJkfDqIOCE32iOuy85QY//3NsgrSQF6UkSPa95eJrSGI +3hTRYYh3Up2GhBGl1KUy7/o0k3KRZTk4s38fylY8bZ3TakUOH5iIGoHyFVVcp361 +Pyy25SzFSmNalWoQd9wZVc/Cps2ldxhcttM+WLkFNzprd0VJa8qTz8vYtHP0ouDN +nWS0 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIGCTCCA/GgAwIBAgIRAOY7gfcBZgR2tqfBzMbFQCUwDQYJKoZIhvcNAQEMBQAw +gZwxCzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJ +bmMuMRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTE1MDMGA1UEAwws +QW1hem9uIFJEUyBhcC1zb3V0aGVhc3QtNCBSb290IENBIFJTQTQwOTYgRzExEDAO +BgNVBAcMB1NlYXR0bGUwIBcNMjIwNTI1MTY1NDU5WhgPMjEyMjA1MjUxNzU0NTla +MIGcMQswCQYDVQQGEwJVUzEiMCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywg +SW5jLjETMBEGA1UECwwKQW1hem9uIFJEUzELMAkGA1UECAwCV0ExNTAzBgNVBAMM +LEFtYXpvbiBSRFMgYXAtc291dGhlYXN0LTQgUm9vdCBDQSBSU0E0MDk2IEcxMRAw +DgYDVQQHDAdTZWF0dGxlMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA +lfxER43FuLRdL08bddF0YhbCP+XXKj1A/TFMXmd2My8XDei8rPXFYyyjMig9+xZw +uAsIxLwz8uiA26CKA8bCZKg5VG2kTeOJAfvBJaLv1CZefs3Z4Uf1Sjvm6MF2yqEj +GoORfyfL9HiZFTDuF/hcjWoKYCfMuG6M/wO8IbdICrX3n+BiYQJu/pFO660Mg3h/ +8YBBWYDbHoCiH/vkqqJugQ5BM3OI5nsElW51P1icEEqti4AZ7JmtSv9t7fIFBVyR +oaEyOgpp0sm193F/cDJQdssvjoOnaubsSYm1ep3awZAUyGN/X8MBrPY95d0hLhfH +Ehc5Icyg+hsosBljlAyksmt4hFQ9iBnWIz/ZTfGMck+6p3HVL9RDgvluez+rWv59 +8q7omUGsiPApy5PDdwI/Wt/KtC34/2sjslIJfvgifdAtkRPkhff1WEwER00ADrN9 +eGGInaCpJfb1Rq8cV2n00jxg7DcEd65VR3dmIRb0bL+jWK62ni/WdEyomAOMfmGj +aWf78S/4rasHllWJ+QwnaUYY3u6N8Cgio0/ep4i34FxMXqMV3V0/qXdfhyabi/LM +wCxNo1Dwt+s6OtPJbwO92JL+829QAxydfmaMTeHBsgMPkG7RwAekeuatKGHNsc2Z +x2Q4C2wVvOGAhcHwxfM8JfZs3nDSZJndtVVnFlUY0UECAwEAAaNCMEAwDwYDVR0T +AQH/BAUwAwEB/zAdBgNVHQ4EFgQUpnG7mWazy6k97/tb5iduRB3RXgQwDgYDVR0P +AQH/BAQDAgGGMA0GCSqGSIb3DQEBDAUAA4ICAQCDLqq1Wwa9Tkuv7vxBnIeVvvFF +ecTn+P+wJxl9Qa2ortzqTHZsBDyJO62d04AgBwiDXkJ9a+bthgG0H1J7Xee8xqv1 +xyX2yKj24ygHjspLotKP4eDMdDi5TYq+gdkbPmm9Q69B1+W6e049JVGXvWG8/7kU +igxeuCYwtCCdUPRLf6D8y+1XMGgVv3/DSOHWvTg3MJ1wJ3n3+eve3rjGdRYWZeJu +k21HLSZYzVrCtUsh2YAeLnUbSxVuT2Xr4JehYe9zW5HEQ8Je/OUfnCy9vzoN/ITw +osAH+EBJQey7RxEDqMwCaRefH0yeHFcnOll0OXg/urnQmwbEYzQ1uutJaBPsjU0J +Qf06sMxI7GiB5nPE+CnI2sM6A9AW9kvwexGXpNJiLxF8dvPQthpOKGcYu6BFvRmt +6ctfXd9b7JJoVqMWuf5cCY6ihpk1e9JTlAqu4Eb/7JNyGiGCR40iSLvV28un9wiE +plrdYxwcNYq851BEu3r3AyYWw/UW1AKJ5tM+/Gtok+AphMC9ywT66o/Kfu44mOWm +L3nSLSWEcgfUVgrikpnyGbUnGtgCmHiMlUtNVexcE7OtCIZoVAlCGKNu7tyuJf10 +Qlk8oIIzfSIlcbHpOYoN79FkLoDNc2er4Gd+7w1oPQmdAB0jBJnA6t0OUBPKdDdE +Ufff2jrbfbzECn1ELg== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIGCDCCA/CgAwIBAgIQIuO1A8LOnmc7zZ/vMm3TrDANBgkqhkiG9w0BAQwFADCB +nDELMAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIElu +Yy4xEzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTUwMwYDVQQDDCxB +bWF6b24gUkRTIGFwLXNvdXRoZWFzdC0yIFJvb3QgQ0EgUlNBNDA5NiBHMTEQMA4G +A1UEBwwHU2VhdHRsZTAgFw0yMTA1MjQyMDQ2MThaGA8yMTIxMDUyNDIxNDYxOFow +gZwxCzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJ +bmMuMRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTE1MDMGA1UEAwws +QW1hem9uIFJEUyBhcC1zb3V0aGVhc3QtMiBSb290IENBIFJTQTQwOTYgRzExEDAO +BgNVBAcMB1NlYXR0bGUwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDq +qRHKbG8ZK6/GkGm2cenznEF06yHwI1gD5sdsHjTgekDZ2Dl9RwtDmUH2zFuIQwGj +SeC7E2iKwrJRA5wYzL9/Vk8NOILEKQOP8OIKUHbc7q8rEtjs401KcU6pFBBEdO9G +CTiRhogq+8mhC13AM/UriZJbKhwgM2UaDOzAneGMhQAGjH8z83NsNcPxpYVE7tqM +sch5yLtIJLkJRusrmQQTeHUev16YNqyUa+LuFclFL0FzFCimkcxUhXlbfEKXbssS +yPzjiv8wokGyo7+gA0SueceMO2UjfGfute3HlXZDcNvBbkSY+ver41jPydyRD6Qq +oEkh0tyIbPoa3oU74kwipJtz6KBEA3u3iq61OUR0ENhR2NeP7CSKrC24SnQJZ/92 +qxusrbyV/0w+U4m62ug/o4hWNK1lUcc2AqiBOvCSJ7qpdteTFxcEIzDwYfERDx6a +d9+3IPvzMb0ZCxBIIUFMxLTF7yAxI9s6KZBBXSZ6tDcCCYIgEysEPRWMRAcG+ye/ +fZVn9Vnzsj4/2wchC2eQrYpb1QvG4eMXA4M5tFHKi+/8cOPiUzJRgwS222J8YuDj +yEBval874OzXk8H8Mj0JXJ/jH66WuxcBbh5K7Rp5oJn7yju9yqX6qubY8gVeMZ1i +u4oXCopefDqa35JplQNUXbWwSebi0qJ4EK0V8F9Q+QIDAQABo0IwQDAPBgNVHRMB +Af8EBTADAQH/MB0GA1UdDgQWBBT4ysqCxaPe7y+g1KUIAenqu8PAgzAOBgNVHQ8B +Af8EBAMCAYYwDQYJKoZIhvcNAQEMBQADggIBALU8WN35KAjPZEX65tobtCDQFkIO +uJjv0alD7qLB0i9eY80C+kD87HKqdMDJv50a5fZdqOta8BrHutgFtDm+xo5F/1M3 +u5/Vva5lV4xy5DqPajcF4Mw52czYBmeiLRTnyPJsU93EQIC2Bp4Egvb6LI4cMOgm +4pY2hL8DojOC5PXt4B1/7c1DNcJX3CMzHDm4SMwiv2MAxSuC/cbHXcWMk+qXdrVx ++ayLUSh8acaAOy3KLs1MVExJ6j9iFIGsDVsO4vr4ZNsYQiyHjp+L8ops6YVBO5AT +k/pI+axHIVsO5qiD4cFWvkGqmZ0gsVtgGUchZaacboyFsVmo6QPrl28l6LwxkIEv +GGJYvIBW8sfqtGRspjfX5TlNy5IgW/VOwGBdHHsvg/xpRo31PR3HOFw7uPBi7cAr +FiZRLJut7af98EB2UvovZnOh7uIEGPeecQWeOTQfJeWet2FqTzFYd0NUMgqPuJx1 +vLKferP+ajAZLJvVnW1J7Vccx/pm0rMiUJEf0LRb/6XFxx7T2RGjJTi0EzXODTYI +gnLfBBjnolQqw+emf4pJ4pAtly0Gq1KoxTG2QN+wTd4lsCMjnelklFDjejwnl7Uy +vtxzRBAu/hi/AqDkDFf94m6j+edIrjbi9/JDFtQ9EDlyeqPgw0qwi2fwtJyMD45V +fejbXelUSJSzDIdY +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIGCTCCA/GgAwIBAgIRAN7Y9G9i4I+ZaslPobE7VL4wDQYJKoZIhvcNAQEMBQAw +gZwxCzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJ +bmMuMRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTE1MDMGA1UEAwws +QW1hem9uIFJEUyBhcC1ub3J0aGVhc3QtMiBSb290IENBIFJTQTQwOTYgRzExEDAO +BgNVBAcMB1NlYXR0bGUwIBcNMjEwNTIwMTYzMzIzWhgPMjEyMTA1MjAxNzMzMjNa +MIGcMQswCQYDVQQGEwJVUzEiMCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywg +SW5jLjETMBEGA1UECwwKQW1hem9uIFJEUzELMAkGA1UECAwCV0ExNTAzBgNVBAMM +LEFtYXpvbiBSRFMgYXAtbm9ydGhlYXN0LTIgUm9vdCBDQSBSU0E0MDk2IEcxMRAw +DgYDVQQHDAdTZWF0dGxlMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA +4BEPCiIfiK66Q/qa8k+eqf1Q3qsa6Xuu/fPkpuStXVBShhtXd3eqrM0iT4Xxs420 +Va0vSB3oZ7l86P9zYfa60n6PzRxdYFckYX330aI7L/oFIdaodB/C9szvROI0oLG+ +6RwmIF2zcprH0cTby8MiM7G3v9ykpq27g4WhDC1if2j8giOQL3oHpUaByekZNIHF +dIllsI3RkXmR3xmmxoOxJM1B9MZi7e1CvuVtTGOnSGpNCQiqofehTGwxCN2wFSK8 +xysaWlw48G0VzZs7cbxoXMH9QbMpb4tpk0d+T8JfAPu6uWO9UwCLWWydf0CkmA/+ +D50/xd1t33X9P4FEaPSg5lYbHXzSLWn7oLbrN2UqMLaQrkoEBg/VGvzmfN0mbflw ++T87bJ/VEOVNlG+gepyCTf89qIQVWOjuYMox4sK0PjzZGsYEuYiq1+OUT3vk/e5K +ag1fCcq2Isy4/iwB2xcXrsQ6ljwdk1fc+EmOnjGKrhuOHJY3S+RFv4ToQBsVyYhC +XGaC3EkqIX0xaCpDimxYhFjWhpDXAjG/zJ+hRLDAMCMhl/LPGRk/D1kzSbPmdjpl +lEMK5695PeBvEBTQdBQdOiYgOU3vWU6tzwwHfiM2/wgvess/q0FDAHfJhppbgbb9 +3vgsIUcsvoC5o29JvMsUxsDRvsAfEmMSDGkJoA/X6GECAwEAAaNCMEAwDwYDVR0T +AQH/BAUwAwEB/zAdBgNVHQ4EFgQUgEWm1mZCbGD6ytbwk2UU1aLaOUUwDgYDVR0P +AQH/BAQDAgGGMA0GCSqGSIb3DQEBDAUAA4ICAQBb4+ABTGBGwxK1U/q4g8JDqTQM +1Wh8Oz8yAk4XtPJMAmCctxbd81cRnSnePWw/hxViLVtkZ/GsemvXfqAQyOn1coN7 +QeYSw+ZOlu0j2jEJVynmgsR7nIRqE7QkCyZAU+d2FTJUfmee+IiBiGyFGgxz9n7A +JhBZ/eahBbiuoOik/APW2JWLh0xp0W0GznfJ8lAlaQTyDa8iDXmVtbJg9P9qzkvl +FgPXQttzEOyooF8Pb2LCZO4kUz+1sbU7tHdr2YE+SXxt6D3SBv+Yf0FlvyWLiqVk +GDEOlPPTDSjAWgKnqST8UJ0RDcZK/v1ixs7ayqQJU0GUQm1I7LGTErWXHMnCuHKe +UKYuiSZwmTcJ06NgdhcCnGZgPq13ryMDqxPeltQc3n5eO7f1cL9ERYLDLOzm6A9P +oQ3MfcVOsbHgGHZWaPSeNrQRN9xefqBXH0ZPasgcH9WJdsLlEjVUXoultaHOKx3b +UCCb+d3EfqF6pRT488ippOL6bk7zNubwhRa/+y4wjZtwe3kAX78ACJVcjPobH9jZ +ErySads5zdQeaoee5wRKdp3TOfvuCe4bwLRdhOLCHWzEcXzY3g/6+ppLvNom8o+h +Bh5X26G6KSfr9tqhQ3O9IcbARjnuPbvtJnoPY0gz3EHHGPhy0RNW8i2gl3nUp0ah +PtjwbKW0hYAhIttT0Q== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICtzCCAj2gAwIBAgIQQRBQTs6Y3H1DDbpHGta3lzAKBggqhkjOPQQDAzCBmzEL +MAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIEluYy4x +EzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTQwMgYDVQQDDCtBbWF6 +b24gUkRTIGFwLXNvdXRoZWFzdC0zIFJvb3QgQ0EgRUNDMzg0IEcxMRAwDgYDVQQH +DAdTZWF0dGxlMCAXDTIxMDYxMTAwMTI0M1oYDzIxMjEwNjExMDExMjQzWjCBmzEL +MAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIEluYy4x +EzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTQwMgYDVQQDDCtBbWF6 +b24gUkRTIGFwLXNvdXRoZWFzdC0zIFJvb3QgQ0EgRUNDMzg0IEcxMRAwDgYDVQQH +DAdTZWF0dGxlMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEs0942Xj4m/gKA+WA6F5h +AHYuek9eGpzTRoLJddM4rEV1T3eSueytMVKOSlS3Ub9IhyQrH2D8EHsLYk9ktnGR +pATk0kCYTqFbB7onNo070lmMJmGT/Q7NgwC8cySChFxbo0IwQDAPBgNVHRMBAf8E +BTADAQH/MB0GA1UdDgQWBBQ20iKBKiNkcbIZRu0y1uoF1yJTEzAOBgNVHQ8BAf8E +BAMCAYYwCgYIKoZIzj0EAwMDaAAwZQIwYv0wTSrpQTaPaarfLN8Xcqrqu3hzl07n +FrESIoRw6Cx77ZscFi2/MV6AFyjCV/TlAjEAhpwJ3tpzPXpThRML8DMJYZ3YgMh3 +CMuLqhPpla3cL0PhybrD27hJWl29C4el6aMO +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICrDCCAjOgAwIBAgIQGcztRyV40pyMKbNeSN+vXTAKBggqhkjOPQQDAzCBljEL +MAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIEluYy4x +EzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMS8wLQYDVQQDDCZBbWF6 +b24gUkRTIHVzLWVhc3QtMiBSb290IENBIEVDQzM4NCBHMTEQMA4GA1UEBwwHU2Vh +dHRsZTAgFw0yMTA1MjEyMzE1NTZaGA8yMTIxMDUyMjAwMTU1NlowgZYxCzAJBgNV +BAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJbmMuMRMwEQYD +VQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTEvMC0GA1UEAwwmQW1hem9uIFJE +UyB1cy1lYXN0LTIgUm9vdCBDQSBFQ0MzODQgRzExEDAOBgNVBAcMB1NlYXR0bGUw +djAQBgcqhkjOPQIBBgUrgQQAIgNiAAQfDcv+GGRESD9wT+I5YIPRsD3L+/jsiIis +Tr7t9RSbFl+gYpO7ZbDXvNbV5UGOC5lMJo/SnqFRTC6vL06NF7qOHfig3XO8QnQz +6T5uhhrhnX2RSY3/10d2kTyHq3ZZg3+jQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYD +VR0OBBYEFLDyD3PRyNXpvKHPYYxjHXWOgfPnMA4GA1UdDwEB/wQEAwIBhjAKBggq +hkjOPQQDAwNnADBkAjB20HQp6YL7CqYD82KaLGzgw305aUKw2aMrdkBR29J183jY +6Ocj9+Wcif9xnRMS+7oCMAvrt03rbh4SU9BohpRUcQ2Pjkh7RoY0jDR4Xq4qzjNr +5UFr3BXpFvACxXF51BksGQ== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICrjCCAjWgAwIBAgIQeKbS5zvtqDvRtwr5H48cAjAKBggqhkjOPQQDAzCBlzEL +MAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIEluYy4x +EzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTAwLgYDVQQDDCdBbWF6 +b24gUkRTIG1lLXNvdXRoLTEgUm9vdCBDQSBFQ0MzODQgRzExEDAOBgNVBAcMB1Nl +YXR0bGUwIBcNMjEwNTIwMTcxOTU1WhgPMjEyMTA1MjAxODE5NTVaMIGXMQswCQYD +VQQGEwJVUzEiMCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywgSW5jLjETMBEG +A1UECwwKQW1hem9uIFJEUzELMAkGA1UECAwCV0ExMDAuBgNVBAMMJ0FtYXpvbiBS +RFMgbWUtc291dGgtMSBSb290IENBIEVDQzM4NCBHMTEQMA4GA1UEBwwHU2VhdHRs +ZTB2MBAGByqGSM49AgEGBSuBBAAiA2IABEKjgUaAPmUlRMEQdBC7BScAGosJ1zRV +LDd38qTBjzgmwBfQJ5ZfGIvyEK5unB09MB4e/3qqK5I/L6Qn5Px/n5g4dq0c7MQZ +u7G9GBYm90U3WRJBf7lQrPStXaRnS4A/O6NCMEAwDwYDVR0TAQH/BAUwAwEB/zAd +BgNVHQ4EFgQUNKcAbGEIn03/vkwd8g6jNyiRdD4wDgYDVR0PAQH/BAQDAgGGMAoG +CCqGSM49BAMDA2cAMGQCMHIeTrjenCSYuGC6txuBt/0ZwnM/ciO9kHGWVCoK8QLs +jGghb5/YSFGZbmQ6qpGlSAIwVOQgdFfTpEfe5i+Vs9frLJ4QKAfc27cTNYzRIM0I +E+AJgK4C4+DiyyMzOpiCfmvq +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIGCDCCA/CgAwIBAgIQSFkEUzu9FYgC5dW+5lnTgjANBgkqhkiG9w0BAQwFADCB +nDELMAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIElu +Yy4xEzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTUwMwYDVQQDDCxB +bWF6b24gUkRTIGFwLXNvdXRoZWFzdC0zIFJvb3QgQ0EgUlNBNDA5NiBHMTEQMA4G +A1UEBwwHU2VhdHRsZTAgFw0yMTA2MTEwMDA4MzZaGA8yMTIxMDYxMTAxMDgzNlow +gZwxCzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJ +bmMuMRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTE1MDMGA1UEAwws +QW1hem9uIFJEUyBhcC1zb3V0aGVhc3QtMyBSb290IENBIFJTQTQwOTYgRzExEDAO +BgNVBAcMB1NlYXR0bGUwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDx +my5Qmd8zdwaI/KOKV9Xar9oNbhJP5ED0JCiigkuvCkg5qM36klszE8JhsUj40xpp +vQw9wkYW4y+C8twBpzKGBvakqMnoaVUV7lOCKx0RofrnNwkZCboTBB4X/GCZ3fIl +YTybS7Ehi1UuiaZspIT5A2jidoA8HiBPk+mTg1UUkoWS9h+MEAPa8L4DY6fGf4pO +J1Gk2cdePuNzzIrpm2yPto+I8MRROwZ3ha7ooyymOXKtz2c7jEHHJ314boCXAv9G +cdo27WiebewZkHHH7Zx9iTIVuuk2abyVSzvLVeGv7Nuy4lmSqa5clWYqWsGXxvZ2 +0fZC5Gd+BDUMW1eSpW7QDTk3top6x/coNoWuLSfXiC5ZrJkIKimSp9iguULgpK7G +abMMN4PR+O+vhcB8E879hcwmS2yd3IwcPTl3QXxufqeSV58/h2ibkqb/W4Bvggf6 +5JMHQPlPHOqMCVFIHP1IffIo+Of7clb30g9FD2j3F4qgV3OLwEDNg/zuO1DiAvH1 +L+OnmGHkfbtYz+AVApkAZrxMWwoYrwpauyBusvSzwRE24vLTd2i80ZDH422QBLXG +rN7Zas8rwIiBKacJLYtBYETw8mfsNt8gb72aIQX6cZOsphqp6hUtKaiMTVgGazl7 +tBXqbB+sIv3S9X6bM4cZJKkMJOXbnyCCLZFYv8TurwIDAQABo0IwQDAPBgNVHRMB +Af8EBTADAQH/MB0GA1UdDgQWBBTOVtaS1b/lz6yJDvNk65vEastbQTAOBgNVHQ8B +Af8EBAMCAYYwDQYJKoZIhvcNAQEMBQADggIBABEONg+TmMZM/PrYGNAfB4S41zp1 +3CVjslZswh/pC4kgXSf8cPJiUOzMwUevuFQj7tCqxQtJEygJM2IFg4ViInIah2kh +xlRakEGGw2dEVlxZAmmLWxlL1s1lN1565t5kgVwM0GVfwYM2xEvUaby6KDVJIkD3 +aM6sFDBshvVA70qOggM6kU6mwTbivOROzfoIQDnVaT+LQjHqY/T+ok6IN0YXXCWl +Favai8RDjzLDFwXSRvgIK+1c49vlFFY4W9Efp7Z9tPSZU1TvWUcKdAtV8P2fPHAS +vAZ+g9JuNfeawhEibjXkwg6Z/yFUueQCQOs9TRXYogzp5CMMkfdNJF8byKYqHscs +UosIcETnHwqwban99u35sWcoDZPr6aBIrz7LGKTJrL8Nis8qHqnqQBXu/fsQEN8u +zJ2LBi8sievnzd0qI0kaWmg8GzZmYH1JCt1GXSqOFkI8FMy2bahP7TUQR1LBUKQ3 +hrOSqldkhN+cSAOnvbQcFzLr+iEYEk34+NhcMIFVE+51KJ1n6+zISOinr6mI3ckX +6p2tmiCD4Shk2Xx/VTY/KGvQWKFcQApWezBSvDNlGe0yV71LtLf3dr1pr4ofo7cE +rYucCJ40bfxEU/fmzYdBF32xP7AOD9U0FbOR3Mcthc6Z6w20WFC+zru8FGY08gPf +WT1QcNdw7ntUJP/w +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICrzCCAjWgAwIBAgIQARky6+5PNFRkFVOp3Ob1CTAKBggqhkjOPQQDAzCBlzEL +MAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIEluYy4x +EzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTAwLgYDVQQDDCdBbWF6 +b24gUkRTIGV1LXNvdXRoLTIgUm9vdCBDQSBFQ0MzODQgRzExEDAOBgNVBAcMB1Nl +YXR0bGUwIBcNMjIwNTIzMTg0MTI4WhgPMjEyMjA1MjMxOTQxMjdaMIGXMQswCQYD +VQQGEwJVUzEiMCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywgSW5jLjETMBEG +A1UECwwKQW1hem9uIFJEUzELMAkGA1UECAwCV0ExMDAuBgNVBAMMJ0FtYXpvbiBS +RFMgZXUtc291dGgtMiBSb290IENBIEVDQzM4NCBHMTEQMA4GA1UEBwwHU2VhdHRs +ZTB2MBAGByqGSM49AgEGBSuBBAAiA2IABNVGL5oF7cfIBxKyWd2PVK/S5yQfaJY3 +QFHWvEdt6951n9JhiiPrHzfVHsxZp1CBjILRMzjgRbYWmc8qRoLkgGE7htGdwudJ +Fa/WuKzO574Prv4iZXUnVGTboC7JdvKbh6NCMEAwDwYDVR0TAQH/BAUwAwEB/zAd +BgNVHQ4EFgQUgDeIIEKynwUbNXApdIPnmRWieZwwDgYDVR0PAQH/BAQDAgGGMAoG +CCqGSM49BAMDA2gAMGUCMEOOJfucrST+FxuqJkMZyCM3gWGZaB+/w6+XUAJC6hFM +uSTY0F44/bERkA4XhH+YGAIxAIpJQBakCA1/mXjsTnQ+0El9ty+LODp8ibkn031c +8DKDS7pR9UK7ZYdR6zFg3ZCjQw== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICrjCCAjOgAwIBAgIQJvkWUcYLbnxtuwnyjMmntDAKBggqhkjOPQQDAzCBljEL +MAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIEluYy4x +EzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMS8wLQYDVQQDDCZBbWF6 +b24gUkRTIGV1LXdlc3QtMyBSb290IENBIEVDQzM4NCBHMTEQMA4GA1UEBwwHU2Vh +dHRsZTAgFw0yMTA1MjUyMjI2MTJaGA8yMTIxMDUyNTIzMjYxMlowgZYxCzAJBgNV +BAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJbmMuMRMwEQYD +VQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTEvMC0GA1UEAwwmQW1hem9uIFJE +UyBldS13ZXN0LTMgUm9vdCBDQSBFQ0MzODQgRzExEDAOBgNVBAcMB1NlYXR0bGUw +djAQBgcqhkjOPQIBBgUrgQQAIgNiAARENn8uHCyjn1dFax4OeXxvbV861qsXFD9G +DshumTmFzWWHN/69WN/AOsxy9XN5S7Cgad4gQgeYYYgZ5taw+tFo/jQvCLY//uR5 +uihcLuLJ78opvRPvD9kbWZ6oXfBtFkWjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYD +VR0OBBYEFKiK3LpoF+gDnqPldGSwChBPCYciMA4GA1UdDwEB/wQEAwIBhjAKBggq +hkjOPQQDAwNpADBmAjEA+7qfvRlnvF1Aosyp9HzxxCbN7VKu+QXXPhLEBWa5oeWW +UOcifunf/IVLC4/FGCsLAjEAte1AYp+iJyOHDB8UYkhBE/1sxnFaTiEPbvQBU0wZ +SuwWVLhu2wWDuSW+K7tTuL8p +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIID/zCCAuegAwIBAgIRAKeDpqX5WFCGNo94M4v69sUwDQYJKoZIhvcNAQELBQAw +gZcxCzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJ +bmMuMRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTEwMC4GA1UEAwwn +QW1hem9uIFJEUyBldS13ZXN0LTMgUm9vdCBDQSBSU0EyMDQ4IEcxMRAwDgYDVQQH +DAdTZWF0dGxlMCAXDTIxMDUyNTIyMTgzM1oYDzIwNjEwNTI1MjMxODMzWjCBlzEL +MAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIEluYy4x +EzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTAwLgYDVQQDDCdBbWF6 +b24gUkRTIGV1LXdlc3QtMyBSb290IENBIFJTQTIwNDggRzExEDAOBgNVBAcMB1Nl +YXR0bGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCcKOTEMTfzvs4H +WtJR8gI7GXN6xesulWtZPv21oT+fLGwJ+9Bv8ADCGDDrDxfeH/HxJmzG9hgVAzVn +4g97Bn7q07tGZM5pVi96/aNp11velZT7spOJKfJDZTlGns6DPdHmx48whpdO+dOb +6+eR0VwCIv+Vl1fWXgoACXYCoKjhxJs+R+fwY//0JJ1YG8yjZ+ghLCJmvlkOJmE1 +TCPUyIENaEONd6T+FHGLVYRRxC2cPO65Jc4yQjsXvvQypoGgx7FwD5voNJnFMdyY +754JGPOOe/SZdepN7Tz7UEq8kn7NQSbhmCsgA/Hkjkchz96qN/YJ+H/okiQUTNB0 +eG9ogiVFAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFFjayw9Y +MjbxfF14XAhMM2VPl0PfMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOC +AQEAAtmx6d9+9CWlMoU0JCirtp4dSS41bBfb9Oor6GQ8WIr2LdfZLL6uES/ubJPE +1Sh5Vu/Zon5/MbqLMVrfniv3UpQIof37jKXsjZJFE1JVD/qQfRzG8AlBkYgHNEiS +VtD4lFxERmaCkY1tjKB4Dbd5hfhdrDy29618ZjbSP7NwAfnwb96jobCmMKgxVGiH +UqsLSiEBZ33b2hI7PJ6iTJnYBWGuiDnsWzKRmheA4nxwbmcQSfjbrNwa93w3caL2 +v/4u54Kcasvcu3yFsUwJygt8z43jsGAemNZsS7GWESxVVlW93MJRn6M+MMakkl9L +tWaXdHZ+KUV7LhfYLb0ajvb40w== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEBDCCAuygAwIBAgIQJ5oxPEjefCsaESSwrxk68DANBgkqhkiG9w0BAQsFADCB +mjELMAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIElu +Yy4xEzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTMwMQYDVQQDDCpB +bWF6b24gUkRTIGV1LWNlbnRyYWwtMiBSb290IENBIFJTQTIwNDggRzExEDAOBgNV +BAcMB1NlYXR0bGUwIBcNMjIwNjA2MjExNzA1WhgPMjA2MjA2MDYyMjE3MDVaMIGa +MQswCQYDVQQGEwJVUzEiMCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywgSW5j +LjETMBEGA1UECwwKQW1hem9uIFJEUzELMAkGA1UECAwCV0ExMzAxBgNVBAMMKkFt +YXpvbiBSRFMgZXUtY2VudHJhbC0yIFJvb3QgQ0EgUlNBMjA0OCBHMTEQMA4GA1UE +BwwHU2VhdHRsZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALTQt5eX +g+VP3BjO9VBkWJhE0GfLrU/QIk32I6WvrnejayTrlup9H1z4QWlXF7GNJrqScRMY +KhJHlcP05aPsx1lYco6pdFOf42ybXyWHHJdShj4A5glU81GTT+VrXGzHSarLmtua +eozkQgPpDsSlPt0RefyTyel7r3Cq+5K/4vyjCTcIqbfgaGwTU36ffjM1LaPCuE4O +nINMeD6YuImt2hU/mFl20FZ+IZQUIFZZU7pxGLqTRz/PWcH8tDDxnkYg7tNuXOeN +JbTpXrw7St50/E9ZQ0llGS+MxJD8jGRAa/oL4G/cwnV8P2OEPVVkgN9xDDQeieo0 +3xkzolkDkmeKOnUCAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU +bwu8635iQGQMRanekesORM8Hkm4wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEB +CwUAA4IBAQAgN6LE9mUgjsj6xGCX1afYE69fnmCjjb0rC6eEe1mb/QZNcyw4XBIW +6+zTXo4mjZ4ffoxb//R0/+vdTE7IvaLgfAZgFsLKJCtYDDstXZj8ujQnGR9Pig3R +W+LpNacvOOSJSawNQq0Xrlcu55AU4buyD5VjcICnfF1dqBMnGTnh27m/scd/ZMx/ +kapHZ/fMoK2mAgSX/NvUKF3UkhT85vSSM2BTtET33DzCPDQTZQYxFBa4rFRmFi4c +BLlmIReiCGyh3eJhuUUuYAbK6wLaRyPsyEcIOLMQmZe1+gAFm1+1/q5Ke9ugBmjf +PbTWjsi/lfZ5CdVAhc5lmZj/l5aKqwaS +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICrjCCAjSgAwIBAgIRAKKPTYKln9L4NTx9dpZGUjowCgYIKoZIzj0EAwMwgZYx +CzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJbmMu +MRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTEvMC0GA1UEAwwmQW1h +em9uIFJEUyBldS13ZXN0LTIgUm9vdCBDQSBFQ0MzODQgRzExEDAOBgNVBAcMB1Nl +YXR0bGUwIBcNMjEwNTIxMjI1NTIxWhgPMjEyMTA1MjEyMzU1MjFaMIGWMQswCQYD +VQQGEwJVUzEiMCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywgSW5jLjETMBEG +A1UECwwKQW1hem9uIFJEUzELMAkGA1UECAwCV0ExLzAtBgNVBAMMJkFtYXpvbiBS +RFMgZXUtd2VzdC0yIFJvb3QgQ0EgRUNDMzg0IEcxMRAwDgYDVQQHDAdTZWF0dGxl +MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE/owTReDvaRqdmbtTzXbyRmEpKCETNj6O +hZMKH0F8oU9Tmn8RU7kQQj6xUKEyjLPrFBN7c+26TvrVO1KmJAvbc8bVliiJZMbc +C0yV5PtJTalvlMZA1NnciZuhxaxrzlK1o0IwQDAPBgNVHRMBAf8EBTADAQH/MB0G +A1UdDgQWBBT4i5HaoHtrs7Mi8auLhMbKM1XevDAOBgNVHQ8BAf8EBAMCAYYwCgYI +KoZIzj0EAwMDaAAwZQIxAK9A+8/lFdX4XJKgfP+ZLy5ySXC2E0Spoy12Gv2GdUEZ +p1G7c1KbWVlyb1d6subzkQIwKyH0Naf/3usWfftkmq8SzagicKz5cGcEUaULq4tO +GzA/AMpr63IDBAqkZbMDTCmH +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICrzCCAjWgAwIBAgIQTgIvwTDuNWQo0Oe1sOPQEzAKBggqhkjOPQQDAzCBlzEL +MAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIEluYy4x +EzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTAwLgYDVQQDDCdBbWF6 +b24gUkRTIGV1LW5vcnRoLTEgUm9vdCBDQSBFQ0MzODQgRzExEDAOBgNVBAcMB1Nl +YXR0bGUwIBcNMjEwNTI0MjEwNjM4WhgPMjEyMTA1MjQyMjA2MzhaMIGXMQswCQYD +VQQGEwJVUzEiMCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywgSW5jLjETMBEG +A1UECwwKQW1hem9uIFJEUzELMAkGA1UECAwCV0ExMDAuBgNVBAMMJ0FtYXpvbiBS +RFMgZXUtbm9ydGgtMSBSb290IENBIEVDQzM4NCBHMTEQMA4GA1UEBwwHU2VhdHRs +ZTB2MBAGByqGSM49AgEGBSuBBAAiA2IABJuzXLU8q6WwSKXBvx8BbdIi3mPhb7Xo +rNJBfuMW1XRj5BcKH1ZoGaDGw+BIIwyBJg8qNmCK8kqIb4cH8/Hbo3Y+xBJyoXq/ +cuk8aPrxiNoRsKWwiDHCsVxaK9L7GhHHAqNCMEAwDwYDVR0TAQH/BAUwAwEB/zAd +BgNVHQ4EFgQUYgcsdU4fm5xtuqLNppkfTHM2QMYwDgYDVR0PAQH/BAQDAgGGMAoG +CCqGSM49BAMDA2gAMGUCMQDz/Rm89+QJOWJecYAmYcBWCcETASyoK1kbr4vw7Hsg +7Ew3LpLeq4IRmTyuiTMl0gMCMAa0QSjfAnxBKGhAnYxcNJSntUyyMpaXzur43ec0 +3D8npJghwC4DuICtKEkQiI5cSg== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIGATCCA+mgAwIBAgIRAORIGqQXLTcbbYT2upIsSnQwDQYJKoZIhvcNAQEMBQAw +gZgxCzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJ +bmMuMRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTExMC8GA1UEAwwo +QW1hem9uIFJEUyBldS1zb3V0aC0yIFJvb3QgQ0EgUlNBNDA5NiBHMTEQMA4GA1UE +BwwHU2VhdHRsZTAgFw0yMjA1MjMxODM0MjJaGA8yMTIyMDUyMzE5MzQyMlowgZgx +CzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJbmMu +MRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTExMC8GA1UEAwwoQW1h +em9uIFJEUyBldS1zb3V0aC0yIFJvb3QgQ0EgUlNBNDA5NiBHMTEQMA4GA1UEBwwH +U2VhdHRsZTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAPKukwsW2s/h +1k+Hf65pOP0knVBnOnMQyT1mopp2XHGdXznj9xS49S30jYoUnWccyXgD983A1bzu +w4fuJRHg4MFdz/NWTgXvy+zy0Roe83OPIJjUmXnnzwUHQcBa9vl6XUO65iQ3pbSi +fQfNDFXD8cvuXbkezeADoy+iFAlzhXTzV9MD44GTuo9Z3qAXNGHQCrgRSCL7uRYt +t1nfwboCbsVRnElopn2cTigyVXE62HzBUmAw1GTbAZeFAqCn5giBWYAfHwTUldRL +6eEa6atfsS2oPNus4ZENa1iQxXq7ft+pMdNt0qKXTCZiiCZjmLkY0V9kWwHTRRF8 +r+75oSL//3di43QnuSCgjwMRIeWNtMud5jf3eQzSBci+9njb6DrrSUbx7blP0srg +94/C/fYOp/0/EHH34w99Th14VVuGWgDgKahT9/COychLOubXUT6vD1As47S9KxTv +yYleVKwJnF9cVjepODN72fNlEf74BwzgSIhUmhksmZSeJBabrjSUj3pdyo/iRZN/ +CiYz9YPQ29eXHPQjBZVIUqWbOVfdwsx0/Xu5T1e7yyXByQ3/oDulahtcoKPAFQ3J +ee6NJK655MdS7pM9hJnU2Rzu3qZ/GkM6YK7xTlMXVouPUZov/VbiaCKbqYDs8Dg+ +UKdeNXAT6+BMleGQzly1X7vjhgeA8ugVAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMB +Af8wHQYDVR0OBBYEFJdaPwpCf78UolFTEn6GO85/QwUIMA4GA1UdDwEB/wQEAwIB +hjANBgkqhkiG9w0BAQwFAAOCAgEAWkxHIT3mers5YnZRSVjmpxCLivGj1jMB9VYC +iKqTAeIvD0940L0YaZgivQll5pue8UUcQ6M2uCdVVAsNJdmQ5XHIYiGOknYPtxzO +aO+bnZp7VIZw/vJ49hvH6RreA2bbxYMZO/ossYdcWsWbOKHFrRmAw0AhtK/my51g +obV7eQg+WmlE5Iqc75ycUsoZdc3NimkjBi7LQoNP1HMvlLHlF71UZhQDdq+/WdV7 +0zmg+epkki1LjgMmuPyb+xWuYkFKT1/faX+Xs62hIm5BY+aI4if4RuQ+J//0pOSs +UajrjTo+jLGB8A96jAe8HaFQenbwMjlaHRDAF0wvbkYrMr5a6EbneAB37V05QD0Y +Rh4L4RrSs9DX2hbSmS6iLDuPEjanHKzglF5ePEvnItbRvGGkynqDVlwF+Bqfnw8l +0i8Hr1f1/LP1c075UjkvsHlUnGgPbLqA0rDdcxF8Fdlv1BunUjX0pVlz10Ha5M6P +AdyWUOneOfaA5G7jjv7i9qg3r99JNs1/Lmyg/tV++gnWTAsSPFSSEte81kmPhlK3 +2UtAO47nOdTtk+q4VIRAwY1MaOR7wTFZPfer1mWs4RhKNu/odp8urEY87iIzbMWT +QYO/4I6BGj9rEWNGncvR5XTowwIthMCj2KWKM3Z/JxvjVFylSf+s+FFfO1bNIm6h +u3UBpZI= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICtDCCAjmgAwIBAgIQenQbcP/Zbj9JxvZ+jXbRnTAKBggqhkjOPQQDAzCBmTEL +MAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIEluYy4x +EzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTIwMAYDVQQDDClBbWF6 +b24gUkRTIGV1LWNlbnRyYWwtMSBSb290IENBIEVDQzM4NCBHMTEQMA4GA1UEBwwH +U2VhdHRsZTAgFw0yMTA1MjEyMjMzMjRaGA8yMTIxMDUyMTIzMzMyNFowgZkxCzAJ +BgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJbmMuMRMw +EQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTEyMDAGA1UEAwwpQW1hem9u +IFJEUyBldS1jZW50cmFsLTEgUm9vdCBDQSBFQ0MzODQgRzExEDAOBgNVBAcMB1Nl +YXR0bGUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAATlBHiEM9LoEb1Hdnd5j2VpCDOU +5nGuFoBD8ROUCkFLFh5mHrHfPXwBc63heW9WrP3qnDEm+UZEUvW7ROvtWCTPZdLz +Z4XaqgAlSqeE2VfUyZOZzBSgUUJk7OlznXfkCMOjQjBAMA8GA1UdEwEB/wQFMAMB +Af8wHQYDVR0OBBYEFDT/ThjQZl42Nv/4Z/7JYaPNMly2MA4GA1UdDwEB/wQEAwIB +hjAKBggqhkjOPQQDAwNpADBmAjEAnZWmSgpEbmq+oiCa13l5aGmxSlfp9h12Orvw +Dq/W5cENJz891QD0ufOsic5oGq1JAjEAp5kSJj0MxJBTHQze1Aa9gG4sjHBxXn98 +4MP1VGsQuhfndNHQb4V0Au7OWnOeiobq +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIID/zCCAuegAwIBAgIRAMgnyikWz46xY6yRgiYwZ3swDQYJKoZIhvcNAQELBQAw +gZcxCzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJ +bmMuMRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTEwMC4GA1UEAwwn +QW1hem9uIFJEUyBldS13ZXN0LTEgUm9vdCBDQSBSU0EyMDQ4IEcxMRAwDgYDVQQH +DAdTZWF0dGxlMCAXDTIxMDUyMDE2NDkxMloYDzIwNjEwNTIwMTc0OTEyWjCBlzEL +MAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIEluYy4x +EzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTAwLgYDVQQDDCdBbWF6 +b24gUkRTIGV1LXdlc3QtMSBSb290IENBIFJTQTIwNDggRzExEDAOBgNVBAcMB1Nl +YXR0bGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCi8JYOc9cYSgZH +gYPxLk6Xcc7HqzamvsnjYU98Dcb98y6iDqS46Ra2Ne02MITtU5MDL+qjxb8WGDZV +RUA9ZS69tkTO3gldW8QdiSh3J6hVNJQW81F0M7ZWgV0gB3n76WCmfT4IWos0AXHM +5v7M/M4tqVmCPViQnZb2kdVlM3/Xc9GInfSMCgNfwHPTXl+PXX+xCdNBePaP/A5C +5S0oK3HiXaKGQAy3K7VnaQaYdiv32XUatlM4K2WS4AMKt+2cw3hTCjlmqKRHvYFQ +veWCXAuc+U5PQDJ9SuxB1buFJZhT4VP3JagOuZbh5NWpIbOTxlAJOb5pGEDuJTKi +1gQQQVEFAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFNXm+N87 +OFxK9Af/bjSxDCiulGUzMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOC +AQEAkqIbkgZ45spvrgRQ6n9VKzDLvNg+WciLtmVrqyohwwJbj4pYvWwnKQCkVc7c +hUOSBmlSBa5REAPbH5o8bdt00FPRrD6BdXLXhaECKgjsHe1WW08nsequRKD8xVmc +8bEX6sw/utBeBV3mB+3Zv7ejYAbDFM4vnRsWtO+XqgReOgrl+cwdA6SNQT9oW3e5 +rSQ+VaXgJtl9NhkiIysq9BeYigxqS/A13pHQp0COMwS8nz+kBPHhJTsajHCDc8F4 +HfLi6cgs9G0gaRhT8FCH66OdGSqn196sE7Y3bPFFFs/3U+vxvmQgoZC6jegQXAg5 +Prxd+VNXtNI/azitTysQPumH7A== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEBTCCAu2gAwIBAgIRAO8bekN7rUReuNPG8pSTKtEwDQYJKoZIhvcNAQELBQAw +gZoxCzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJ +bmMuMRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTEzMDEGA1UEAwwq +QW1hem9uIFJEUyBldS1jZW50cmFsLTEgUm9vdCBDQSBSU0EyMDQ4IEcxMRAwDgYD +VQQHDAdTZWF0dGxlMCAXDTIxMDUyMTIyMjM0N1oYDzIwNjEwNTIxMjMyMzQ3WjCB +mjELMAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIElu +Yy4xEzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTMwMQYDVQQDDCpB +bWF6b24gUkRTIGV1LWNlbnRyYWwtMSBSb290IENBIFJTQTIwNDggRzExEDAOBgNV +BAcMB1NlYXR0bGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCTTYds +Tray+Q9VA5j5jTh5TunHKFQzn68ZbOzdqaoi/Rq4ohfC0xdLrxCpfqn2TGDHN6Zi +2qGK1tWJZEd1H0trhzd9d1CtGK+3cjabUmz/TjSW/qBar7e9MA67/iJ74Gc+Ww43 +A0xPNIWcL4aLrHaLm7sHgAO2UCKsrBUpxErOAACERScVYwPAfu79xeFcX7DmcX+e +lIqY16pQAvK2RIzrekSYfLFxwFq2hnlgKHaVgZ3keKP+nmXcXmRSHQYUUr72oYNZ +HcNYl2+gxCc9ccPEHM7xncVEKmb5cWEWvVoaysgQ+osi5f5aQdzgC2X2g2daKbyA +XL/z5FM9GHpS5BJjAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYE +FBDAiJ7Py9/A9etNa/ebOnx5l5MGMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0B +AQsFAAOCAQEALMh/+81fFPdJV/RrJUeoUvFCGMp8iaANu97NpeJyKitNOv7RoeVP +WjivS0KcCqZaDBs+p6IZ0sLI5ZH098LDzzytcfZg0PsGqUAb8a0MiU/LfgDCI9Ee +jsOiwaFB8k0tfUJK32NPcIoQYApTMT2e26lPzYORSkfuntme2PTHUnuC7ikiQrZk +P+SZjWgRuMcp09JfRXyAYWIuix4Gy0eZ4rpRuaTK6mjAb1/LYoNK/iZ/gTeIqrNt +l70OWRsWW8jEmSyNTIubGK/gGGyfuZGSyqoRX6OKHESkP6SSulbIZHyJ5VZkgtXo +2XvyRyJ7w5pFyoofrL3Wv0UF8yt/GDszmg== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIF/zCCA+egAwIBAgIRAMDk/F+rrhdn42SfE+ghPC8wDQYJKoZIhvcNAQEMBQAw +gZcxCzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJ +bmMuMRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTEwMC4GA1UEAwwn +QW1hem9uIFJEUyBldS13ZXN0LTIgUm9vdCBDQSBSU0E0MDk2IEcxMRAwDgYDVQQH +DAdTZWF0dGxlMCAXDTIxMDUyMTIyNTEyMloYDzIxMjEwNTIxMjM1MTIyWjCBlzEL +MAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIEluYy4x +EzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTAwLgYDVQQDDCdBbWF6 +b24gUkRTIGV1LXdlc3QtMiBSb290IENBIFJTQTQwOTYgRzExEDAOBgNVBAcMB1Nl +YXR0bGUwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC2twMALVg9vRVu +VNqsr6N8thmp3Dy8jEGTsm3GCQ+C5P2YcGlD/T/5icfWW84uF7Sx3ezcGlvsqFMf +Ukj9sQyqtz7qfFFugyy7pa/eH9f48kWFHLbQYm9GEgbYBIrWMp1cy3vyxuMCwQN4 +DCncqU+yNpy0CprQJEha3PzY+3yJOjDQtc3zr99lyECCFJTDUucxHzyQvX89eL74 +uh8la0lKH3v9wPpnEoftbrwmm5jHNFdzj7uXUHUJ41N7af7z7QUfghIRhlBDiKtx +5lYZemPCXajTc3ryDKUZC/b+B6ViXZmAeMdmQoPE0jwyEp/uaUcdp+FlUQwCfsBk +ayPFEApTWgPiku2isjdeTVmEgL8bJTDUZ6FYFR7ZHcYAsDzcwHgIu3GGEMVRS3Uf +ILmioiyly9vcK4Sa01ondARmsi/I0s7pWpKflaekyv5boJKD/xqwz9lGejmJHelf +8Od2TyqJScMpB7Q8c2ROxBwqwB72jMCEvYigB+Wnbb8RipliqNflIGx938FRCzKL +UQUBmNAznR/yRRL0wHf9UAE/8v9a09uZABeiznzOFAl/frHpgdAbC00LkFlnwwgX +g8YfEFlkp4fLx5B7LtoO6uVNFVimLxtwirpyKoj3G4M/kvSTux8bTw0heBCmWmKR +57MS6k7ODzbv+Kpeht2hqVZCNFMxoQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/ +MB0GA1UdDgQWBBRuMnDhJjoj7DcKALj+HbxEqj3r6jAOBgNVHQ8BAf8EBAMCAYYw +DQYJKoZIhvcNAQEMBQADggIBALSnXfx72C3ldhBP5kY4Mo2DDaGQ8FGpTOOiD95d +0rf7I9LrsBGVqu/Nir+kqqP80PB70+Jy9fHFFigXwcPBX3MpKGxK8Cel7kVf8t1B +4YD6A6bqlzP+OUL0uGWfZpdpDxwMDI2Flt4NEldHgXWPjvN1VblEKs0+kPnKowyg +jhRMgBbD/y+8yg0fIcjXUDTAw/+INcp21gWaMukKQr/8HswqC1yoqW9in2ijQkpK +2RB9vcQ0/gXR0oJUbZQx0jn0OH8Agt7yfMAnJAdnHO4M3gjvlJLzIC5/4aGrRXZl +JoZKfJ2fZRnrFMi0nhAYDeInoS+Rwx+QzaBk6fX5VPyCj8foZ0nmqvuYoydzD8W5 +mMlycgxFqS+DUmO+liWllQC4/MnVBlHGB1Cu3wTj5kgOvNs/k+FW3GXGzD3+rpv0 +QTLuwSbMr+MbEThxrSZRSXTCQzKfehyC+WZejgLb+8ylLJUA10e62o7H9PvCrwj+ +ZDVmN7qj6amzvndCP98sZfX7CFZPLfcBd4wVIjHsFjSNEwWHOiFyLPPG7cdolGKA +lOFvonvo4A1uRc13/zFeP0Xi5n5OZ2go8aOOeGYdI2vB2sgH9R2IASH/jHmr0gvY +0dfBCcfXNgrS0toq0LX/y+5KkKOxh52vEYsJLdhqrveuZhQnsFEm/mFwjRXkyO7c +2jpC +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIGADCCA+igAwIBAgIQYe0HgSuFFP9ivYM2vONTrTANBgkqhkiG9w0BAQwFADCB +mDELMAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIElu +Yy4xEzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTEwLwYDVQQDDChB +bWF6b24gUkRTIGV1LXNvdXRoLTEgUm9vdCBDQSBSU0E0MDk2IEcxMRAwDgYDVQQH +DAdTZWF0dGxlMCAXDTIxMDUxOTE4MzMyMVoYDzIxMjEwNTE5MTkzMzIxWjCBmDEL +MAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIEluYy4x +EzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTEwLwYDVQQDDChBbWF6 +b24gUkRTIGV1LXNvdXRoLTEgUm9vdCBDQSBSU0E0MDk2IEcxMRAwDgYDVQQHDAdT +ZWF0dGxlMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAuO7QPKfPMTo2 +POQWvzDLwi5f++X98hGjORI1zkN9kotCYH5pAzSBwBPoMNaIfedgmsIxGHj2fq5G +4oXagNhNuGP79Zl6uKW5H7S74W7aWM8C0s8zuxMOI4GZy5h2IfQk3m/3AzZEX5w8 +UtNPkzo2feDVOkerHT+j+vjXgAxZ4wHnuMDcRT+K4r9EXlAH6X9b/RO0JlfEwmNz +xlqqGxocq9qRC66N6W0HF2fNEAKP84n8H80xcZBOBthQORRi8HSmKcPdmrvwCuPz +M+L+j18q6RAVaA0ABbD0jMWcTf0UvjUfBStn5mvu/wGlLjmmRkZsppUTRukfwqXK +yltUsTq0tOIgCIpne5zA4v+MebbR5JBnsvd4gdh5BI01QH470yB7BkUefZ9bobOm +OseAAVXcYFJKe4DAA6uLDrqOfFSxV+CzVvEp3IhLRaik4G5MwI/h2c/jEYDqkg2J +HMflxc2gcSMdk7E5ByLz5f6QrFfSDFk02ZJTs4ssbbUEYohht9znPMQEaWVqATWE +3n0VspqZyoBNkH/agE5GiGZ/k/QyeqzMNj+c9kr43Upu8DpLrz8v2uAp5xNj3YVg +ihaeD6GW8+PQoEjZ3mrCmH7uGLmHxh7Am59LfEyNrDn+8Rq95WvkmbyHSVxZnBmo +h/6O3Jk+0/QhIXZ2hryMflPcYWeRGH0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB +/zAdBgNVHQ4EFgQU2eFK7+R3x/me8roIBNxBrplkM6EwDgYDVR0PAQH/BAQDAgGG +MA0GCSqGSIb3DQEBDAUAA4ICAQB5gWFe5s7ObQFj1fTO9L6gYgtFhnwdmxU0q8Ke +HWCrdFmyXdC39qdAFOwM5/7fa9zKmiMrZvy9HNvCXEp4Z7z9mHhBmuqPZQx0qPgU +uLdP8wGRuWryzp3g2oqkX9t31Z0JnkbIdp7kfRT6ME4I4VQsaY5Y3mh+hIHOUvcy +p+98i3UuEIcwJnVAV9wTTzrWusZl9iaQ1nSYbmkX9bBssJ2GmtW+T+VS/1hJ/Q4f +AlE3dOQkLFoPPb3YRWBHr2n1LPIqMVwDNAuWavRA2dSfaLl+kzbn/dua7HTQU5D4 +b2Fu2vLhGirwRJe+V7zdef+tI7sngXqjgObyOeG5O2BY3s+um6D4fS0Th3QchMO7 +0+GwcIgSgcjIjlrt6/xJwJLE8cRkUUieYKq1C4McpZWTF30WnzOPUzRzLHkcNzNA +0A7sKMK6QoYWo5Rmo8zewUxUqzc9oQSrYADP7PEwGncLtFe+dlRFx+PA1a+lcIgo +1ZGfXigYtQ3VKkcknyYlJ+hN4eCMBHtD81xDy9iP2MLE41JhLnoB2rVEtewO5diF +7o95Mwl84VMkLhhHPeGKSKzEbBtYYBifHNct+Bst8dru8UumTltgfX6urH3DN+/8 +JF+5h3U8oR2LL5y76cyeb+GWDXXy9zoQe2QvTyTy88LwZq1JzujYi2k8QiLLhFIf +FEv9Bg== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICsDCCAjagAwIBAgIRAMgApnfGYPpK/fD0dbN2U4YwCgYIKoZIzj0EAwMwgZcx +CzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJbmMu +MRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTEwMC4GA1UEAwwnQW1h +em9uIFJEUyBldS1zb3V0aC0xIFJvb3QgQ0EgRUNDMzg0IEcxMRAwDgYDVQQHDAdT +ZWF0dGxlMCAXDTIxMDUxOTE4MzgxMVoYDzIxMjEwNTE5MTkzODExWjCBlzELMAkG +A1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIEluYy4xEzAR +BgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTAwLgYDVQQDDCdBbWF6b24g +UkRTIGV1LXNvdXRoLTEgUm9vdCBDQSBFQ0MzODQgRzExEDAOBgNVBAcMB1NlYXR0 +bGUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQfEWl6d4qSuIoECdZPp+39LaKsfsX7 +THs3/RrtT0+h/jl3bjZ7Qc68k16x+HGcHbaayHfqD0LPdzH/kKtNSfQKqemdxDQh +Z4pwkixJu8T1VpXZ5zzCvBXCl75UqgEFS92jQjBAMA8GA1UdEwEB/wQFMAMBAf8w +HQYDVR0OBBYEFFPrSNtWS5JU+Tvi6ABV231XbjbEMA4GA1UdDwEB/wQEAwIBhjAK +BggqhkjOPQQDAwNoADBlAjEA+a7hF1IrNkBd2N/l7IQYAQw8chnRZDzh4wiGsZsC +6A83maaKFWUKIb3qZYXFSi02AjAbp3wxH3myAmF8WekDHhKcC2zDvyOiKLkg9Y6v +ZVmyMR043dscQbcsVoacOYv198c= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICtDCCAjqgAwIBAgIRAPhVkIsQ51JFhD2kjFK5uAkwCgYIKoZIzj0EAwMwgZkx +CzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJbmMu +MRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTEyMDAGA1UEAwwpQW1h +em9uIFJEUyBldS1jZW50cmFsLTIgUm9vdCBDQSBFQ0MzODQgRzExEDAOBgNVBAcM +B1NlYXR0bGUwIBcNMjIwNjA2MjEyOTE3WhgPMjEyMjA2MDYyMjI5MTdaMIGZMQsw +CQYDVQQGEwJVUzEiMCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywgSW5jLjET +MBEGA1UECwwKQW1hem9uIFJEUzELMAkGA1UECAwCV0ExMjAwBgNVBAMMKUFtYXpv +biBSRFMgZXUtY2VudHJhbC0yIFJvb3QgQ0EgRUNDMzg0IEcxMRAwDgYDVQQHDAdT +ZWF0dGxlMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEA5xnIEBtG5b2nmbj49UEwQza +yX0844fXjccYzZ8xCDUe9dS2XOUi0aZlGblgSe/3lwjg8fMcKXLObGGQfgIx1+5h +AIBjORis/dlyN5q/yH4U5sjS8tcR0GDGVHrsRUZCo0IwQDAPBgNVHRMBAf8EBTAD +AQH/MB0GA1UdDgQWBBRK+lSGutXf4DkTjR3WNfv4+KeNFTAOBgNVHQ8BAf8EBAMC +AYYwCgYIKoZIzj0EAwMDaAAwZQIxAJ4NxQ1Gerqr70ZrnUqc62Vl8NNqTzInamCG +Kce3FTsMWbS9qkgrjZkO9QqOcGIw/gIwSLrwUT+PKr9+H9eHyGvpq9/3AIYSnFkb +Cf3dyWPiLKoAtLFwjzB/CkJlsAS1c8dS +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIF/jCCA+agAwIBAgIQGZH12Q7x41qIh9vDu9ikTjANBgkqhkiG9w0BAQwFADCB +lzELMAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIElu +Yy4xEzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTAwLgYDVQQDDCdB +bWF6b24gUkRTIGV1LXdlc3QtMyBSb290IENBIFJTQTQwOTYgRzExEDAOBgNVBAcM +B1NlYXR0bGUwIBcNMjEwNTI1MjIyMjMzWhgPMjEyMTA1MjUyMzIyMzNaMIGXMQsw +CQYDVQQGEwJVUzEiMCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywgSW5jLjET +MBEGA1UECwwKQW1hem9uIFJEUzELMAkGA1UECAwCV0ExMDAuBgNVBAMMJ0FtYXpv +biBSRFMgZXUtd2VzdC0zIFJvb3QgQ0EgUlNBNDA5NiBHMTEQMA4GA1UEBwwHU2Vh +dHRsZTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMqE47sHXWzdpuqj +JHb+6jM9tDbQLDFnYjDWpq4VpLPZhb7xPNh9gnYYTPKG4avG421EblAHqzy9D2pN +1z90yKbIfUb/Sy2MhQbmZomsObhONEra06fJ0Dydyjswf1iYRp2kwpx5AgkVoNo7 +3dlws73zFjD7ImKvUx2C7B75bhnw2pJWkFnGcswl8fZt9B5Yt95sFOKEz2MSJE91 +kZlHtya19OUxZ/cSGci4MlOySzqzbGwUqGxEIDlY8I39VMwXaYQ8uXUN4G780VcL +u46FeyRGxZGz2n3hMc805WAA1V5uir87vuirTvoSVREET97HVRGVVNJJ/FM6GXr1 +VKtptybbo81nefYJg9KBysxAa2Ao2x2ry/2ZxwhS6VZ6v1+90bpZA1BIYFEDXXn/ +dW07HSCFnYSlgPtSc+Muh15mdr94LspYeDqNIierK9i4tB6ep7llJAnq0BU91fM2 +JPeqyoTtc3m06QhLf68ccSxO4l8Hmq9kLSHO7UXgtdjfRVaffngopTNk8qK7bIb7 +LrgkqhiQw/PRCZjUdyXL153/fUcsj9nFNe25gM4vcFYwH6c5trd2tUl31NTi1MfG +Mgp3d2dqxQBIYANkEjtBDMy3SqQLIo9EymqmVP8xx2A/gCBgaxvMAsI6FSWRoC7+ +hqJ8XH4mFnXSHKtYMe6WPY+/XZgtAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8w +HQYDVR0OBBYEFIkXqTnllT/VJnI2NqipA4XV8rh1MA4GA1UdDwEB/wQEAwIBhjAN +BgkqhkiG9w0BAQwFAAOCAgEAKjSle8eenGeHgT8pltWCw/HzWyQruVKhfYIBfKJd +MhV4EnH5BK7LxBIvpXGsFUrb0ThzSw0fn0zoA9jBs3i/Sj6KyeZ9qUF6b8ycDXd+ +wHonmJiQ7nk7UuMefaYAfs06vosgl1rI7eBHC0itexIQmKh0aX+821l4GEgEoSMf +loMFTLXv2w36fPHHCsZ67ODldgcZbKNnpCTX0YrCwEYO3Pz/L398btiRcWGrewrK +jdxAAyietra8DRno1Zl87685tfqc6HsL9v8rVw58clAo9XAQvT+fmSOFw/PogRZ7 +OMHUat3gu/uQ1M5S64nkLLFsKu7jzudBuoNmcJysPlzIbqJ7vYc82OUGe9ucF3wi +3tbKQ983hdJiTExVRBLX/fYjPsGbG3JtPTv89eg2tjWHlPhCDMMxyRKl6isu2RTq +6VT489Z2zQrC33MYF8ZqO1NKjtyMAMIZwxVu4cGLkVsqFmEV2ScDHa5RadDyD3Ok +m+mqybhvEVm5tPgY6p0ILPMN3yvJsMSPSvuBXhO/X5ppNnpw9gnxpwbjQKNhkFaG +M5pkADZ14uRguOLM4VthSwUSEAr5VQYCFZhEwK+UOyJAGiB/nJz6IxL5XBNUXmRM +Hl8Xvz4riq48LMQbjcVQj0XvH941yPh+P8xOi00SGaQRaWp55Vyr4YKGbV0mEDz1 +r1o= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIF/zCCA+egAwIBAgIRAKwYju1QWxUZpn6D1gOtwgQwDQYJKoZIhvcNAQEMBQAw +gZcxCzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJ +bmMuMRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTEwMC4GA1UEAwwn +QW1hem9uIFJEUyBldS13ZXN0LTEgUm9vdCBDQSBSU0E0MDk2IEcxMRAwDgYDVQQH +DAdTZWF0dGxlMCAXDTIxMDUyMDE2NTM1NFoYDzIxMjEwNTIwMTc1MzU0WjCBlzEL +MAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIEluYy4x +EzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTAwLgYDVQQDDCdBbWF6 +b24gUkRTIGV1LXdlc3QtMSBSb290IENBIFJTQTQwOTYgRzExEDAOBgNVBAcMB1Nl +YXR0bGUwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCKdBP1U4lqWWkc +Cb25/BKRTsvNVnISiKocva8GAzJyKfcGRa85gmgu41U+Hz6+39K+XkRfM0YS4BvQ +F1XxWT0bNyypuvwCvmYShSTjN1TY0ltncDddahTajE/4MdSOZb/c98u0yt03cH+G +hVwRyT50h0v/UEol50VfwcVAEZEgcQQYhf1IFUFlIvKpmDOqLuFakOnc7c9akK+i +ivST+JO1tgowbnNkn2iLlSSgUWgb1gjaOsNfysagv1RXdlyPw3EyfwkFifAQvF2P +Q0ayYZfYS640cccv7efM1MSVyFHR9PrrDsF/zr2S2sGPbeHr7R/HwLl+S5J/l9N9 +y0rk6IHAWV4dEkOvgpnuJKURwA48iu1Hhi9e4moNS6eqoK2KmY3VFpuiyWcA73nH +GSmyaH+YuMrF7Fnuu7GEHZL/o6+F5cL3mj2SJJhL7sz0ryf5Cs5R4yN9BIEj/f49 +wh84pM6nexoI0Q4wiSFCxWiBpjSmOK6h7z6+2utaB5p20XDZHhxAlmlx4vMuWtjh +XckgRFxc+ZpVMU3cAHUpVEoO49e/+qKEpPzp8Xg4cToKw2+AfTk3cmyyXQfGwXMQ +ZUHNZ3w9ILMWihGCM2aGUsLcGDRennvNmnmin/SENsOQ8Ku0/a3teEzwV9cmmdYz +5iYs1YtgPvKFobY6+T2RXXh+A5kprwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/ +MB0GA1UdDgQWBBSyUrsQVnKmA8z6/2Ech0rCvqpNmTAOBgNVHQ8BAf8EBAMCAYYw +DQYJKoZIhvcNAQEMBQADggIBAFlj3IFmgiFz5lvTzFTRizhVofhTJsGr14Yfkuc7 +UrXPuXOwJomd4uot2d/VIeGJpfnuS84qGdmQyGewGTJ9inatHsGZgHl9NHNWRwKZ +lTKTbBiq7aqgtUSFa06v202wpzU+1kadxJJePrbABxiXVfOmIW/a1a4hPNcT3syH +FIEg1+CGsp71UNjBuwg3JTKWna0sLSKcxLOSOvX1fzxK5djzVpEsvQMB4PSAzXca +vENgg2ErTwgTA+4s6rRtiBF9pAusN1QVuBahYP3ftrY6f3ycS4K65GnqscyfvKt5 +YgjtEKO3ZeeX8NpubMbzC+0Z6tVKfPFk/9TXuJtwvVeqow0YMrLLyRiYvK7EzJ97 +rrkxoKnHYQSZ+rH2tZ5SE392/rfk1PJL0cdHnkpDkUDO+8cKsFjjYKAQSNC52sKX +74AVh6wMwxYwVZZJf2/2XxkjMWWhKNejsZhUkTISSmiLs+qPe3L67IM7GyKm9/m6 +R3r8x6NGjhTsKH64iYJg7AeKeax4b2e4hBb6GXFftyOs7unpEOIVkJJgM6gh3mwn +R7v4gwFbLKADKt1vHuerSZMiTuNTGhSfCeDM53XI/mjZl2HeuCKP1mCDLlaO+gZR +Q/G+E0sBKgEX4xTkAc3kgkuQGfExdGtnN2U2ehF80lBHB8+2y2E+xWWXih/ZyIcW +wOx+ +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIGBDCCA+ygAwIBAgIQM4C8g5iFRucSWdC8EdqHeDANBgkqhkiG9w0BAQwFADCB +mjELMAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIElu +Yy4xEzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTMwMQYDVQQDDCpB +bWF6b24gUkRTIGV1LWNlbnRyYWwtMSBSb290IENBIFJTQTQwOTYgRzExEDAOBgNV +BAcMB1NlYXR0bGUwIBcNMjEwNTIxMjIyODI2WhgPMjEyMTA1MjEyMzI4MjZaMIGa +MQswCQYDVQQGEwJVUzEiMCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywgSW5j +LjETMBEGA1UECwwKQW1hem9uIFJEUzELMAkGA1UECAwCV0ExMzAxBgNVBAMMKkFt +YXpvbiBSRFMgZXUtY2VudHJhbC0xIFJvb3QgQ0EgUlNBNDA5NiBHMTEQMA4GA1UE +BwwHU2VhdHRsZTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANeTsD/u +6saPiY4Sg0GlJlMXMBltnrcGAEkwq34OKQ0bCXqcoNJ2rcAMmuFC5x9Ho1Y3YzB7 +NO2GpIh6bZaO76GzSv4cnimcv9n/sQSYXsGbPD+bAtnN/RvNW1avt4C0q0/ghgF1 +VFS8JihIrgPYIArAmDtGNEdl5PUrdi9y6QGggbRfidMDdxlRdZBe1C18ZdgERSEv +UgSTPRlVczONG5qcQkUGCH83MMqL5MKQiby/Br5ZyPq6rxQMwRnQ7tROuElzyYzL +7d6kke+PNzG1mYy4cbYdjebwANCtZ2qYRSUHAQsOgybRcSoarv2xqcjO9cEsDiRU +l97ToadGYa4VVERuTaNZxQwrld4mvzpyKuirqZltOqg0eoy8VUsaRPL3dc5aChR0 +dSrBgRYmSAClcR2/2ZCWpXemikwgt031Dsc0A/+TmVurrsqszwbr0e5xqMow9LzO +MI/JtLd0VFtoOkL/7GG2tN8a+7gnLFxpv+AQ0DH5n4k/BY/IyS+H1erqSJhOTQ11 +vDOFTM5YplB9hWV9fp5PRs54ILlHTlZLpWGs3I2BrJwzRtg/rOlvsosqcge9ryai +AKm2j+JBg5wJ19R8oxRy8cfrNTftZePpISaLTyV2B16w/GsSjqixjTQe9LRN2DHk +cC+HPqYyzW2a3pUVyTGHhW6a7YsPBs9yzt6hAgMBAAGjQjBAMA8GA1UdEwEB/wQF +MAMBAf8wHQYDVR0OBBYEFIqA8QkOs2cSirOpCuKuOh9VDfJfMA4GA1UdDwEB/wQE +AwIBhjANBgkqhkiG9w0BAQwFAAOCAgEAOUI90mEIsa+vNJku0iUwdBMnHiO4gm7E +5JloP7JG0xUr7d0hypDorMM3zVDAL+aZRHsq8n934Cywj7qEp1304UF6538ByGdz +tkfacJsUSYfdlNJE9KbA4T+U+7SNhj9jvePpVjdQbhgzxITE9f8CxY/eM40yluJJ +PhbaWvOiRagzo74wttlcDerzLT6Y/JrVpWhnB7IY8HvzK+BwAdaCsBUPC3HF+kth +CIqLq7J3YArTToejWZAp5OOI6DLPM1MEudyoejL02w0jq0CChmZ5i55ElEMnapRX +7GQTARHmjgAOqa95FjbHEZzRPqZ72AtZAWKFcYFNk+grXSeWiDgPFOsq6mDg8DDB +0kfbYwKLFFCC9YFmYzR2YrWw2NxAScccUc2chOWAoSNHiqBbHR8ofrlJSWrtmKqd +YRCXzn8wqXnTS3NNHNccqJ6dN+iMr9NGnytw8zwwSchiev53Fpc1mGrJ7BKTWH0t +ZrA6m32wzpMymtKozlOPYoE5mtZEzrzHEXfa44Rns7XIHxVQSXVWyBHLtIsZOrvW +U5F41rQaFEpEeUQ7sQvqUoISfTUVRNDn6GK6YaccEhCji14APLFIvhRQUDyYMIiM +4vll0F/xgVRHTgDVQ8b8sxdhSYlqB4Wc2Ym41YRz+X2yPqk3typEZBpc4P5Tt1/N +89cEIGdbjsA= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEADCCAuigAwIBAgIQYjbPSg4+RNRD3zNxO1fuKDANBgkqhkiG9w0BAQsFADCB +mDELMAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIElu +Yy4xEzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTEwLwYDVQQDDChB +bWF6b24gUkRTIGV1LW5vcnRoLTEgUm9vdCBDQSBSU0EyMDQ4IEcxMRAwDgYDVQQH +DAdTZWF0dGxlMCAXDTIxMDUyNDIwNTkyMVoYDzIwNjEwNTI0MjE1OTIxWjCBmDEL +MAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIEluYy4x +EzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTEwLwYDVQQDDChBbWF6 +b24gUkRTIGV1LW5vcnRoLTEgUm9vdCBDQSBSU0EyMDQ4IEcxMRAwDgYDVQQHDAdT +ZWF0dGxlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA179eQHxcV0YL +XMkqEmhSBazHhnRVd8yICbMq82PitE3BZcnv1Z5Zs/oOgNmMkOKae4tCXO/41JCX +wAgbs/eWWi+nnCfpQ/FqbLPg0h3dqzAgeszQyNl9IzTzX4Nd7JFRBVJXPIIKzlRf ++GmFsAhi3rYgDgO27pz3ciahVSN+CuACIRYnA0K0s9lhYdddmrW/SYeWyoB7jPa2 +LmWpAs7bDOgS4LlP2H3eFepBPgNufRytSQUVA8f58lsE5w25vNiUSnrdlvDrIU5n +Qwzc7NIZCx4qJpRbSKWrUtbyJriWfAkGU7i0IoainHLn0eHp9bWkwb9D+C/tMk1X +ERZw2PDGkwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSFmR7s +dAblusFN+xhf1ae0KUqhWTAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZIhvcNAQELBQAD +ggEBAHsXOpjPMyH9lDhPM61zYdja1ebcMVgfUvsDvt+w0xKMKPhBzYDMs/cFOi1N +Q8LV79VNNfI2NuvFmGygcvTIR+4h0pqqZ+wjWl3Kk5jVxCrbHg3RBX02QLumKd/i +kwGcEtTUvTssn3SM8bgM0/1BDXgImZPC567ciLvWDo0s/Fe9dJJC3E0G7d/4s09n +OMdextcxFuWBZrBm/KK3QF0ByA8MG3//VXaGO9OIeeOJCpWn1G1PjT1UklYhkg61 +EbsTiZVA2DLd1BGzfU4o4M5mo68l0msse/ndR1nEY6IywwpgIFue7+rEleDh6b9d +PYkG1rHVw2I0XDG4o17aOn5E94I= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEADCCAuigAwIBAgIQC6W4HFghUkkgyQw14a6JljANBgkqhkiG9w0BAQsFADCB +mDELMAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIElu +Yy4xEzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTEwLwYDVQQDDChB +bWF6b24gUkRTIGV1LXNvdXRoLTIgUm9vdCBDQSBSU0EyMDQ4IEcxMRAwDgYDVQQH +DAdTZWF0dGxlMCAXDTIyMDUyMzE4MTYzMloYDzIwNjIwNTIzMTkxNjMyWjCBmDEL +MAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIEluYy4x +EzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTEwLwYDVQQDDChBbWF6 +b24gUkRTIGV1LXNvdXRoLTIgUm9vdCBDQSBSU0EyMDQ4IEcxMRAwDgYDVQQHDAdT +ZWF0dGxlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAiM/t4FV2R9Nx +UQG203UY83jInTa/6TMq0SPyg617FqYZxvz2kkx09x3dmxepUg9ttGMlPgjsRZM5 +LCFEi1FWk+hxHzt7vAdhHES5tdjwds3aIkgNEillmRDVrUsbrDwufLaa+MMDO2E1 +wQ/JYFXw16WBCCi2g1EtyQ2Xp+tZDX5IWOTnvhZpW8vVDptZ2AcJ5rMhfOYO3OsK +5EF0GGA5ldzuezP+BkrBYGJ4wVKGxeaq9+5AT8iVZrypjwRkD7Y5CurywK3+aBwm +s9Q5Nd8t45JCOUzYp92rFKsCriD86n/JnEvgDfdP6Hvtm0/DkwXK40Wz2q0Zrd0k +mjP054NRPwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRR7yqd +SfKcX2Q8GzhcVucReIpewTAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZIhvcNAQELBQAD +ggEBAEszBRDwXcZyNm07VcFwI1Im94oKwKccuKYeJEsizTBsVon8VpEiMwDs+yGu +3p8kBhvkLwWybkD/vv6McH7T5b9jDX2DoOudqYnnaYeypsPH/00Vh3LvKagqzQza +orWLx+0tLo8xW4BtU+Wrn3JId8LvAhxyYXTn9bm+EwPcStp8xGLwu53OPD1RXYuy +uu+3ps/2piP7GVfou7H6PRaqbFHNfiGg6Y+WA0HGHiJzn8uLmrRJ5YRdIOOG9/xi +qTmAZloUNM7VNuurcMM2hWF494tQpsQ6ysg2qPjbBqzlGoOt3GfBTOZmqmwmqtam +K7juWM/mdMQAJ3SMlE5wI8nVdx4= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICrjCCAjSgAwIBAgIRAL9SdzVPcpq7GOpvdGoM80IwCgYIKoZIzj0EAwMwgZYx +CzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJbmMu +MRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTEvMC0GA1UEAwwmQW1h +em9uIFJEUyBldS13ZXN0LTEgUm9vdCBDQSBFQ0MzODQgRzExEDAOBgNVBAcMB1Nl +YXR0bGUwIBcNMjEwNTIwMTY1ODA3WhgPMjEyMTA1MjAxNzU4MDdaMIGWMQswCQYD +VQQGEwJVUzEiMCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywgSW5jLjETMBEG +A1UECwwKQW1hem9uIFJEUzELMAkGA1UECAwCV0ExLzAtBgNVBAMMJkFtYXpvbiBS +RFMgZXUtd2VzdC0xIFJvb3QgQ0EgRUNDMzg0IEcxMRAwDgYDVQQHDAdTZWF0dGxl +MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEJWDgXebvwjR+Ce+hxKOLbnsfN5W5dOlP +Zn8kwWnD+SLkU81Eac/BDJsXGrMk6jFD1vg16PEkoSevsuYWlC8xR6FmT6F6pmeh +fsMGOyJpfK4fyoEPhKeQoT23lFIc5Orjo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0G +A1UdDgQWBBSVNAN1CHAz0eZ77qz2adeqjm31TzAOBgNVHQ8BAf8EBAMCAYYwCgYI +KoZIzj0EAwMDaAAwZQIxAMlQeHbcjor49jqmcJ9gRLWdEWpXG8thIf6zfYQ/OEAg +d7GDh4fR/OUk0VfjsBUN/gIwZB0bGdXvK38s6AAE/9IT051cz/wMe9GIrX1MnL1T +1F5OqnXJdiwfZRRTHsRQ/L00 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIGBDCCA+ygAwIBAgIQalr16vDfX4Rsr+gfQ4iVFDANBgkqhkiG9w0BAQwFADCB +mjELMAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIElu +Yy4xEzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTMwMQYDVQQDDCpB +bWF6b24gUkRTIGV1LWNlbnRyYWwtMiBSb290IENBIFJTQTQwOTYgRzExEDAOBgNV +BAcMB1NlYXR0bGUwIBcNMjIwNjA2MjEyNTIzWhgPMjEyMjA2MDYyMjI1MjNaMIGa +MQswCQYDVQQGEwJVUzEiMCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywgSW5j +LjETMBEGA1UECwwKQW1hem9uIFJEUzELMAkGA1UECAwCV0ExMzAxBgNVBAMMKkFt +YXpvbiBSRFMgZXUtY2VudHJhbC0yIFJvb3QgQ0EgUlNBNDA5NiBHMTEQMA4GA1UE +BwwHU2VhdHRsZTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANbHbFg7 +2VhZor1YNtez0VlNFaobS3PwOMcEn45BE3y7HONnElIIWXGQa0811M8V2FnyqnE8 +Z5aO1EuvijvWf/3D8DPZkdmAkIfh5hlZYY6Aatr65kEOckwIAm7ZZzrwFogYuaFC +z/q0CW+8gxNK+98H/zeFx+IxiVoPPPX6UlrLvn+R6XYNERyHMLNgoZbbS5gGHk43 +KhENVv3AWCCcCc85O4rVd+DGb2vMVt6IzXdTQt6Kih28+RGph+WDwYmf+3txTYr8 +xMcCBt1+whyCPlMbC+Yn/ivtCO4LRf0MPZDRQrqTTrFf0h/V0BGEUmMGwuKgmzf5 +Kl9ILdWv6S956ioZin2WgAxhcn7+z//sN++zkqLreSf90Vgv+A7xPRqIpTdJ/nWG +JaAOUofBfsDsk4X4SUFE7xJa1FZAiu2lqB/E+y7jnWOvFRalzxVJ2Y+D/ZfUfrnK +4pfKtyD1C6ni1celrZrAwLrJ3PoXPSg4aJKh8+CHex477SRsGj8KP19FG8r0P5AG +8lS1V+enFCNvT5KqEBpDZ/Y5SQAhAYFUX+zH4/n4ql0l/emS+x23kSRrF+yMkB9q +lhC/fMk6Pi3tICBjrDQ8XAxv56hfud9w6+/ljYB2uQ1iUYtlE3JdIiuE+3ws26O8 +i7PLMD9zQmo+sVi12pLHfBHQ6RRHtdVRXbXRAgMBAAGjQjBAMA8GA1UdEwEB/wQF +MAMBAf8wHQYDVR0OBBYEFBFot08ipEL9ZUXCG4lagmF53C0/MA4GA1UdDwEB/wQE +AwIBhjANBgkqhkiG9w0BAQwFAAOCAgEAi2mcZi6cpaeqJ10xzMY0F3L2eOKYnlEQ +h6QyhmNKCUF05q5u+cok5KtznzqMwy7TFOZtbVHl8uUX+xvgq/MQCxqFAnuStBXm +gr2dg1h509ZwvTdk7TDxGdftvPCfnPNJBFbMSq4CZtNcOFBg9Rj8c3Yj+Qvwd56V +zWs65BUkDNJrXmxdvhJZjUkMa9vi/oFN+M84xXeZTaC5YDYNZZeW9706QqDbAVES +5ulvKLavB8waLI/lhRBK5/k0YykCMl0A8Togt8D1QsQ0eWWbIM8/HYJMPVFhJ8Wj +vT1p/YVeDA3Bo1iKDOttgC5vILf5Rw1ZEeDxjf/r8A7VS13D3OLjBmc31zxRTs3n +XvHKP9MieQHn9GE44tEYPjK3/yC6BDFzCBlvccYHmqGb+jvDEXEBXKzimdC9mcDl +f4BBQWGJBH5jkbU9p6iti19L/zHhz7qU6UJWbxY40w92L9jS9Utljh4A0LCTjlnR +NQUgjnGC6K+jkw8hj0LTC5Ip87oqoT9w7Av5EJ3VJ4hcnmNMXJJ1DkWYdnytcGpO +DMVITQzzDZRwhbitCVPHagTN2wdi9TEuYE33J0VmFeTc6FSI50wP2aOAZ0Q1/8Aj +bxeM5jS25eaHc2CQAuhrc/7GLnxOcPwdWQb2XWT8eHudhMnoRikVv/KSK3mf6om4 +1YfpdH2jp30= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIID/jCCAuagAwIBAgIQTDc+UgTRtYO7ZGTQ8UWKDDANBgkqhkiG9w0BAQsFADCB +lzELMAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIElu +Yy4xEzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTAwLgYDVQQDDCdB +bWF6b24gUkRTIGV1LXdlc3QtMiBSb290IENBIFJTQTIwNDggRzExEDAOBgNVBAcM +B1NlYXR0bGUwIBcNMjEwNTIxMjI0NjI0WhgPMjA2MTA1MjEyMzQ2MjRaMIGXMQsw +CQYDVQQGEwJVUzEiMCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywgSW5jLjET +MBEGA1UECwwKQW1hem9uIFJEUzELMAkGA1UECAwCV0ExMDAuBgNVBAMMJ0FtYXpv +biBSRFMgZXUtd2VzdC0yIFJvb3QgQ0EgUlNBMjA0OCBHMTEQMA4GA1UEBwwHU2Vh +dHRsZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAM1oGtthQ1YiVIC2 +i4u4swMAGxAjc/BZp0yq0eP5ZQFaxnxs7zFAPabEWsrjeDzrRhdVO0h7zskrertP +gblGhfD20JfjvCHdP1RUhy/nzG+T+hn6Takan/GIgs8grlBMRHMgBYHW7tklhjaH +3F7LujhceAHhhgp6IOrpb6YTaTTaJbF3GTmkqxSJ3l1LtEoWz8Al/nL/Ftzxrtez +Vs6ebpvd7sw37sxmXBWX2OlvUrPCTmladw9OrllGXtCFw4YyLe3zozBlZ3cHzQ0q +lINhpRcajTMfZrsiGCkQtoJT+AqVJPS2sHjqsEH8yiySW9Jbq4zyMbM1yqQ2vnnx +MJgoYMcCAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUaQG88UnV +JPTI+Pcti1P+q3H7pGYwDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4IB +AQBAkgr75V0sEJimC6QRiTVWEuj2Khy7unjSfudbM6zumhXEU2/sUaVLiYy6cA/x +3v0laDle6T07x9g64j5YastE/4jbzrGgIINFlY0JnaYmR3KZEjgi1s1fkRRf3llL +PJm9u4Q1mbwAMQK/ZjLuuRcL3uRIHJek18nRqT5h43GB26qXyvJqeYYpYfIjL9+/ +YiZAbSRRZG+Li23cmPWrbA1CJY121SB+WybCbysbOXzhD3Sl2KSZRwSw4p2HrFtV +1Prk0dOBtZxCG9luf87ultuDZpfS0w6oNBAMXocgswk24ylcADkkFxBWW+7BETn1 +EpK+t1Lm37mU4sxtuha00XAi +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEADCCAuigAwIBAgIQcY44/8NUvBwr6LlHfRy7KjANBgkqhkiG9w0BAQsFADCB +mDELMAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIElu +Yy4xEzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTEwLwYDVQQDDChB +bWF6b24gUkRTIGV1LXNvdXRoLTEgUm9vdCBDQSBSU0EyMDQ4IEcxMRAwDgYDVQQH +DAdTZWF0dGxlMCAXDTIxMDUxOTE4MjcxOFoYDzIwNjEwNTE5MTkyNzE4WjCBmDEL +MAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIEluYy4x +EzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTEwLwYDVQQDDChBbWF6 +b24gUkRTIGV1LXNvdXRoLTEgUm9vdCBDQSBSU0EyMDQ4IEcxMRAwDgYDVQQHDAdT +ZWF0dGxlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0UaBeC+Usalu +EtXnV7+PnH+gi7/71tI/jkKVGKuhD2JDVvqLVoqbMHRh3+wGMvqKCjbHPcC2XMWv +566fpAj4UZ9CLB5fVzss+QVNTl+FH2XhEzigopp+872ajsNzcZxrMkifxGb4i0U+ +t0Zi+UrbL5tsfP2JonKR1crOrbS6/DlzHBjIiJazGOQcMsJjNuTOItLbMohLpraA +/nApa3kOvI7Ufool1/34MG0+wL3UUA4YkZ6oBJVxjZvvs6tI7Lzz/SnhK2widGdc +snbLqBpHNIZQSorVoiwcFaRBGYX/uzYkiw44Yfa4cK2V/B5zgu1Fbr0gbI2am4eh +yVYyg4jPawIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBS9gM1m +IIjyh9O5H/7Vj0R/akI7UzAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZIhvcNAQELBQAD +ggEBAF0Sm9HC2AUyedBVnwgkVXMibnYChOzz7T+0Y+fOLXYAEXex2s8oqGeZdGYX +JHkjBn7JXu7LM+TpTbPbFFDoc1sgMguD/ls+8XsqAl1CssW+amryIL+jfcfbgQ+P +ICwEUD9hGdjBgJ5WcuS+qqxHsEIlFNci3HxcxfBa9VsWs5TjI7Vsl4meL5lf7ZyL +wDV7dHRuU+cImqG1MIvPRIlvPnT7EghrCYi2VCPhP2pM/UvShuwVnkz4MJ29ebIk +WR9kpblFxFdE92D5UUvMCjC2kmtgzNiErvTcwIvOO9YCbBHzRB1fFiWrXUHhJWq9 +IkaxR5icb/IpAV0A1lYZEWMVsfQ= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIGATCCA+mgAwIBAgIRAMa0TPL+QgbWfUPpYXQkf8wwDQYJKoZIhvcNAQEMBQAw +gZgxCzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJ +bmMuMRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTExMC8GA1UEAwwo +QW1hem9uIFJEUyBldS1ub3J0aC0xIFJvb3QgQ0EgUlNBNDA5NiBHMTEQMA4GA1UE +BwwHU2VhdHRsZTAgFw0yMTA1MjQyMTAzMjBaGA8yMTIxMDUyNDIyMDMyMFowgZgx +CzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJbmMu +MRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTExMC8GA1UEAwwoQW1h +em9uIFJEUyBldS1ub3J0aC0xIFJvb3QgQ0EgUlNBNDA5NiBHMTEQMA4GA1UEBwwH +U2VhdHRsZTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANhS9LJVJyWp +6Rudy9t47y6kzvgnFYDrvJVtgEK0vFn5ifdlHE7xqMz4LZqWBFTnS+3oidwVRqo7 +tqsuuElsouStO8m315/YUzKZEPmkw8h5ufWt/lg3NTCoUZNkB4p4skr7TspyMUwE +VdlKQuWTCOLtofwmWT+BnFF3To6xTh3XPlT3ssancw27Gob8kJegD7E0TSMVsecP +B8je65+3b8CGwcD3QB3kCTGLy87tXuS2+07pncHvjMRMBdDQQQqhXWsRSeUNg0IP +xdHTWcuwMldYPWK5zus9M4dCNBDlmZjKdcZZVUOKeBBAm7Uo7CbJCk8r/Fvfr6mw +nXXDtuWhqn/WhJiI/y0QU27M+Hy5CQMxBwFsfAjJkByBpdXmyYxUgTmMpLf43p7H +oWfH1xN0cT0OQEVmAQjMakauow4AQLNkilV+X6uAAu3STQVFRSrpvMen9Xx3EPC3 +G9flHueTa71bU65Xe8ZmEmFhGeFYHY0GrNPAFhq9RThPRY0IPyCZe0Th8uGejkek +jQjm0FHPOqs5jc8CD8eJs4jSEFt9lasFLVDcAhx0FkacLKQjGHvKAnnbRwhN/dF3 +xt4oL8Z4JGPCLau056gKnYaEyviN7PgO+IFIVOVIdKEBu2ASGE8/+QJB5bcHefNj +04hEkDW0UYJbSfPpVbGAR0gFI/QpycKnAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMB +Af8wHQYDVR0OBBYEFFMXvvjoaGGUcul8GA3FT05DLbZcMA4GA1UdDwEB/wQEAwIB +hjANBgkqhkiG9w0BAQwFAAOCAgEAQLwFhd2JKn4K/6salLyIA4mP58qbA/9BTB/r +D9l0bEwDlVPSdY7R3gZCe6v7SWLfA9RjE5tdWDrQMi5IU6W2OVrVsZS/yGJfwnwe +a/9iUAYprA5QYKDg37h12XhVsDKlYCekHdC+qa5WwB1SL3YUprDLPWeaIQdg+Uh2 ++LxvpZGoxoEbca0fc7flwq9ke/3sXt/3V4wJDyY6AL2YNdjFzC+FtYjHHx8rYxHs +aesP7yunuN17KcfOZBBnSFRrx96k+Xm95VReTEEpwiBqAECqEpMbd+R0mFAayMb1 +cE77GaK5yeC2f67NLYGpkpIoPbO9p9rzoXLE5GpSizMjimnz6QCbXPFAFBDfSzim +u6azp40kEUO6kWd7rBhqRwLc43D3TtNWQYxMve5mTRG4Od+eMKwYZmQz89BQCeqm +aZiJP9y9uwJw4p/A5V3lYHTDQqzmbOyhGUk6OdpdE8HXs/1ep1xTT20QDYOx3Ekt +r4mmNYfH/8v9nHNRlYJOqFhmoh1i85IUl5IHhg6OT5ZTTwsGTSxvgQQXrmmHVrgZ +rZIqyBKllCgVeB9sMEsntn4bGLig7CS/N1y2mYdW/745yCLZv2gj0NXhPqgEIdVV +f9DhFD4ohE1C63XP0kOQee+LYg/MY5vH8swpCSWxQgX5icv5jVDz8YTdCKgUc5u8 +rM2p0kk= +-----END CERTIFICATE----- diff --git a/config/config.primary.development.aws.yml b/config/config.primary.development.aws.yml new file mode 100644 index 0000000..a8394ea --- /dev/null +++ b/config/config.primary.development.aws.yml @@ -0,0 +1,63 @@ +grpc_server_address: 0.0.0.0:9090 + +database: + database_driver: postgres + database_table: baseca + database_endpoint: xxxxxx.cluster.xxxxxx.us-east-1.rds.amazonaws.com + database_reader_endpoint: xxxxxx.cluster-ro.xxxxxx.us-east-1.rds.amazonaws.com + database_user: baseca + database_port: 5432 + region: us-east-1 + ssl_mode: verify-full + +redis: + cluster_endpoint: xxxxxx.xxxxxx.0001.use1.cache.amazonaws.com + port: 6379 + rate_limit: 20 + +domains: + - example.com + +acm_pca: + development_use1: + region: us-east-1 + ca_arn: arn:aws:acm-pca:us-east-1:123456789012:certificate-authority/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + ca_active_day: 90 + assume_role: false + root_ca: false + default: true + development_usw1: + region: us-west-1 + ca_arn: arn:aws:acm-pca:us-west-1:123456789012:certificate-authority/yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy + ca_active_day: 90 + assume_role: false + default: false + +firehose: + stream: baseca-development + region: us-east-1 + +kms: + key_id: 12345678-1234-1234-1234-123456789012 + signing_algorithm: RSASSA_PSS_SHA_512 + region: us-east-1 + auth_validity: 5 + +secrets_manager: + secret_id: baseca-xxxxxxxxxxxx + +subordinate_ca_metadata: + country: "US" + province: "CA" + locality: "San Francisco" + organization: "Example" + organization_unit: "Security" + email: "example@example.com" + signing_algorithm: SHA512WITHRSA + key_algorithm: "RSA" + key_size: 4096 + +certificate_authority: + development: + - development_use1 + - development_usw1 \ No newline at end of file diff --git a/config/config.primary.production.aws.yml b/config/config.primary.production.aws.yml new file mode 100644 index 0000000..b2d2d3a --- /dev/null +++ b/config/config.primary.production.aws.yml @@ -0,0 +1,65 @@ +grpc_server_address: 0.0.0.0:9090 + +ocsp_server: + - production.ocsp.example.com + +database: + database_driver: postgres + database_table: baseca + database_endpoint: xxxxxx.cluster.xxxxxx.us-east-1.rds.amazonaws.com + database_reader_endpoint: xxxxxx.cluster-ro.xxxxxx.us-east-1.rds.amazonaws.com + database_user: baseca + database_port: 5432 + region: us-east-1 + ssl_mode: verify-full + +redis: + cluster_endpoint: xxxxxx.xxxxxx.0001.use1.cache.amazonaws.com + port: 6379 + rate_limit: 20 + +domains: + - example.com + +acm_pca: + production_use1: + region: us-east-1 + ca_arn: arn:aws:acm-pca:us-east-1:123456789012:certificate-authority/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + ca_active_day: 90 + assume_role: false + root_ca: false + default: true + + # Configure Additional Certificate Authorities (development_use1, staging_use1, etc) + +firehose: + stream: baseca-production + region: us-east-1 + +kms: + key_id: 12345678-1234-1234-1234-123456789012 + signing_algorithm: RSASSA_PSS_SHA_512 + region: us-east-1 + auth_validity: 5 + +secrets_manager: + secret_id: baseca-xxxxxxxxxxxx + +subordinate_ca_metadata: + country: "US" + province: "CA" + locality: "San Francisco" + organization: "Example" + organization_unit: "Security" + email: "example@example.com" + signing_algorithm: SHA512WITHRSA + key_algorithm: "RSA" + key_size: 4096 + +certificate_authority: + production: + - production_use1 + pre_production: + - pre_production_use1 + corporate: + - corporate_use1 \ No newline at end of file diff --git a/config/permissions/model.conf b/config/permissions/model.conf new file mode 100644 index 0000000..ed9fb1e --- /dev/null +++ b/config/permissions/model.conf @@ -0,0 +1,11 @@ +[request_definition] +r = sub, obj + +[policy_definition] +p = sub, obj + +[policy_effect] +e = some(where (p.eft == allow)) + +[matchers] +m = r.sub == p.sub && keyMatch(r.obj, p.obj) diff --git a/config/permissions/policy.csv b/config/permissions/policy.csv new file mode 100644 index 0000000..24363e4 --- /dev/null +++ b/config/permissions/policy.csv @@ -0,0 +1,3 @@ +p, ADMIN, /baseca.v1.Account/* +p, ADMIN, /baseca.v1.Service/* +p, ADMIN, /baseca.v1.Certificate/* \ No newline at end of file diff --git a/db/init/init-docker.sql b/db/init/init-docker.sql new file mode 100644 index 0000000..b329e84 --- /dev/null +++ b/db/init/init-docker.sql @@ -0,0 +1,5 @@ +-- init-docker.sql +CREATE EXTENSION IF NOT EXISTS pgcrypto; +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; +INSERT INTO users (uuid, username, hashed_credential, full_name, email, permissions, credential_changed_at) +VALUES (uuid_generate_v4(), 'defaultuser', crypt('defaultpassword', gen_salt('bf')), 'Default User', 'defaultuser@example.com', 'ADMIN', now()); diff --git a/db/init/init.sql b/db/init/init.sql new file mode 100644 index 0000000..fa7420d --- /dev/null +++ b/db/init/init.sql @@ -0,0 +1,5 @@ +-- init.sql +CREATE EXTENSION IF NOT EXISTS pgcrypto; +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; +INSERT INTO users (uuid, username, hashed_credential, full_name, email, permissions, credential_changed_at) +VALUES (uuid_generate_v4(), 'defaultuser', crypt('defaultpassword', gen_salt('bf')), 'Default User', 'defaultuser@example.com', 'ADMIN', now()); diff --git a/db/migration/000001_init_schema.down.sql b/db/migration/000001_init_schema.down.sql new file mode 100644 index 0000000..85cf247 --- /dev/null +++ b/db/migration/000001_init_schema.down.sql @@ -0,0 +1,5 @@ +DROP TABLE IF EXISTS aws_attestation; +DROP TABLE IF EXISTS accounts; +DROP TABLE IF EXISTS provisioners; +DROP TABLE IF EXISTS certificates; +DROP TABLE IF EXISTS users; diff --git a/db/migration/000001_init_schema.up.sql b/db/migration/000001_init_schema.up.sql new file mode 100644 index 0000000..fa86766 --- /dev/null +++ b/db/migration/000001_init_schema.up.sql @@ -0,0 +1,74 @@ +CREATE TABLE "users" ( + "uuid" uuid UNIQUE PRIMARY KEY, + "username" varchar(100) UNIQUE NOT NULL, + "hashed_credential" varchar(100) NOT NULL, + "full_name" varchar(100) NOT NULL, + "email" varchar(100) UNIQUE NOT NULL, + "permissions" varchar(100) NOT NULL, + "credential_changed_at" timestamptz NOT NULL DEFAULT '0001-01-01 00:00:00Z', + "created_at" timestamptz NOT NULL DEFAULT (now()) +); + +CREATE TABLE "accounts" ( + "client_id" uuid UNIQUE PRIMARY KEY, + "api_token" varchar(100) NOT NULL, + "service_account" varchar(100) NOT NULL, + "environment" varchar(100) NOT NULL, + "team" varchar(100) NOT NULL, + "email" varchar(100) NOT NULL, + "regular_expression" varchar(100), + "valid_subject_alternate_name" varchar(100)[] NOT NULL, + "valid_certificate_authorities" varchar(100)[] NOT NULL, + "extended_key" varchar(100) NOT NULL, + "certificate_validity" smallserial NOT NULL, + "subordinate_ca" varchar(100) NOT NULL, + "provisioned" boolean NOT NULL, + "node_attestation" varchar(100)[], + "created_at" timestamptz NOT NULL DEFAULT (now()), + "created_by" uuid NOT NULL +); + +CREATE TABLE "certificates" ( + "serial_number" varchar(100) PRIMARY KEY, + "account" varchar(100) NOT NULL, + "environment" varchar(100) NOT NULL, + "extended_key" varchar(100) NOT NULL, + "common_name" varchar(100) NOT NULL, + "subject_alternative_name" varchar(100)[] NOT NULL, + "expiration_date" timestamptz NOT NULL DEFAULT (now()), + "issued_date" timestamptz NOT NULL DEFAULT (now()), + "revoked" boolean NOT NULL DEFAULT false, + "revoked_by" varchar(100), + "revoke_date" timestamptz, + "certificate_authority_arn" varchar(100) +); + +CREATE TABLE "aws_attestation" ( + "client_id" uuid UNIQUE PRIMARY KEY, + "role_arn" varchar(100), + "assume_role" varchar(100), + "security_group_id" varchar(100)[], + "region" varchar(100), + "instance_id" varchar(100), + "image_id" varchar(100), + "instance_tags" json +); + +CREATE TABLE "provisioners" ( + "client_id" uuid UNIQUE PRIMARY KEY, + "api_token" varchar(100) NOT NULL, + "provisioner_account" varchar(100) NOT NULL, + "environments" varchar(100)[] NOT NULL, + "team" varchar(100) NOT NULL, + "email" varchar(100) NOT NULL, + "regular_expression" varchar(100), + "valid_subject_alternate_names" varchar(100)[] NOT NULL, + "extended_keys" varchar(100)[] NOT NULL, + "max_certificate_validity" smallserial NOT NULL, + "node_attestation" varchar(100)[], + "created_at" timestamptz NOT NULL DEFAULT (now()), + "created_by" uuid NOT NULL +); + +ALTER TABLE "accounts" ADD FOREIGN KEY ("created_by") REFERENCES "users" ("uuid"); +ALTER TABLE "provisioners" ADD FOREIGN KEY ("created_by") REFERENCES "users" ("uuid"); \ No newline at end of file diff --git a/db/mock/store.go b/db/mock/store.go new file mode 100644 index 0000000..a633ee5 --- /dev/null +++ b/db/mock/store.go @@ -0,0 +1,545 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/coinbase/baseca/db/sqlc (interfaces: Store) +// +// Generated by this command: +// +// mockgen --build_flags=--mod=mod -package mock -destination db/mock/store.go github.com/coinbase/baseca/db/sqlc Store +// +// Package mock is a generated GoMock package. +package mock + +import ( + context "context" + reflect "reflect" + + db "github.com/coinbase/baseca/db/sqlc" + types "github.com/coinbase/baseca/internal/types" + uuid "github.com/google/uuid" + gomock "go.uber.org/mock/gomock" +) + +// MockStore is a mock of Store interface. +type MockStore struct { + ctrl *gomock.Controller + recorder *MockStoreMockRecorder +} + +// MockStoreMockRecorder is the mock recorder for MockStore. +type MockStoreMockRecorder struct { + mock *MockStore +} + +// NewMockStore creates a new mock instance. +func NewMockStore(ctrl *gomock.Controller) *MockStore { + mock := &MockStore{ctrl: ctrl} + mock.recorder = &MockStoreMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockStore) EXPECT() *MockStoreMockRecorder { + return m.recorder +} + +// CreateProvisionerAccount mocks base method. +func (m *MockStore) CreateProvisionerAccount(arg0 context.Context, arg1 db.CreateProvisionerAccountParams) (*db.Provisioner, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateProvisionerAccount", arg0, arg1) + ret0, _ := ret[0].(*db.Provisioner) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateProvisionerAccount indicates an expected call of CreateProvisionerAccount. +func (mr *MockStoreMockRecorder) CreateProvisionerAccount(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateProvisionerAccount", reflect.TypeOf((*MockStore)(nil).CreateProvisionerAccount), arg0, arg1) +} + +// CreateServiceAccount mocks base method. +func (m *MockStore) CreateServiceAccount(arg0 context.Context, arg1 db.CreateServiceAccountParams) (*db.Account, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateServiceAccount", arg0, arg1) + ret0, _ := ret[0].(*db.Account) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateServiceAccount indicates an expected call of CreateServiceAccount. +func (mr *MockStoreMockRecorder) CreateServiceAccount(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateServiceAccount", reflect.TypeOf((*MockStore)(nil).CreateServiceAccount), arg0, arg1) +} + +// CreateUser mocks base method. +func (m *MockStore) CreateUser(arg0 context.Context, arg1 db.CreateUserParams) (*db.User, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateUser", arg0, arg1) + ret0, _ := ret[0].(*db.User) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateUser indicates an expected call of CreateUser. +func (mr *MockStoreMockRecorder) CreateUser(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateUser", reflect.TypeOf((*MockStore)(nil).CreateUser), arg0, arg1) +} + +// DeleteInstanceIdentityDocument mocks base method. +func (m *MockStore) DeleteInstanceIdentityDocument(arg0 context.Context, arg1 uuid.UUID) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteInstanceIdentityDocument", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteInstanceIdentityDocument indicates an expected call of DeleteInstanceIdentityDocument. +func (mr *MockStoreMockRecorder) DeleteInstanceIdentityDocument(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteInstanceIdentityDocument", reflect.TypeOf((*MockStore)(nil).DeleteInstanceIdentityDocument), arg0, arg1) +} + +// DeleteProvisionerAccount mocks base method. +func (m *MockStore) DeleteProvisionerAccount(arg0 context.Context, arg1 uuid.UUID) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteProvisionerAccount", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteProvisionerAccount indicates an expected call of DeleteProvisionerAccount. +func (mr *MockStoreMockRecorder) DeleteProvisionerAccount(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteProvisionerAccount", reflect.TypeOf((*MockStore)(nil).DeleteProvisionerAccount), arg0, arg1) +} + +// DeleteServiceAccount mocks base method. +func (m *MockStore) DeleteServiceAccount(arg0 context.Context, arg1 uuid.UUID) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteServiceAccount", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteServiceAccount indicates an expected call of DeleteServiceAccount. +func (mr *MockStoreMockRecorder) DeleteServiceAccount(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteServiceAccount", reflect.TypeOf((*MockStore)(nil).DeleteServiceAccount), arg0, arg1) +} + +// DeleteUser mocks base method. +func (m *MockStore) DeleteUser(arg0 context.Context, arg1 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteUser", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteUser indicates an expected call of DeleteUser. +func (mr *MockStoreMockRecorder) DeleteUser(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteUser", reflect.TypeOf((*MockStore)(nil).DeleteUser), arg0, arg1) +} + +// GetCertificate mocks base method. +func (m *MockStore) GetCertificate(arg0 context.Context, arg1 string) (*db.Certificate, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetCertificate", arg0, arg1) + ret0, _ := ret[0].(*db.Certificate) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetCertificate indicates an expected call of GetCertificate. +func (mr *MockStoreMockRecorder) GetCertificate(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCertificate", reflect.TypeOf((*MockStore)(nil).GetCertificate), arg0, arg1) +} + +// GetInstanceIdentityDocument mocks base method. +func (m *MockStore) GetInstanceIdentityDocument(arg0 context.Context, arg1 uuid.UUID) (*db.AwsAttestation, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetInstanceIdentityDocument", arg0, arg1) + ret0, _ := ret[0].(*db.AwsAttestation) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetInstanceIdentityDocument indicates an expected call of GetInstanceIdentityDocument. +func (mr *MockStoreMockRecorder) GetInstanceIdentityDocument(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetInstanceIdentityDocument", reflect.TypeOf((*MockStore)(nil).GetInstanceIdentityDocument), arg0, arg1) +} + +// GetProvisionerUUID mocks base method. +func (m *MockStore) GetProvisionerUUID(arg0 context.Context, arg1 uuid.UUID) (*db.Provisioner, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetProvisionerUUID", arg0, arg1) + ret0, _ := ret[0].(*db.Provisioner) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetProvisionerUUID indicates an expected call of GetProvisionerUUID. +func (mr *MockStoreMockRecorder) GetProvisionerUUID(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetProvisionerUUID", reflect.TypeOf((*MockStore)(nil).GetProvisionerUUID), arg0, arg1) +} + +// GetServiceAccountByMetadata mocks base method. +func (m *MockStore) GetServiceAccountByMetadata(arg0 context.Context, arg1 db.GetServiceAccountByMetadataParams) ([]*db.Account, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetServiceAccountByMetadata", arg0, arg1) + ret0, _ := ret[0].([]*db.Account) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetServiceAccountByMetadata indicates an expected call of GetServiceAccountByMetadata. +func (mr *MockStoreMockRecorder) GetServiceAccountByMetadata(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetServiceAccountByMetadata", reflect.TypeOf((*MockStore)(nil).GetServiceAccountByMetadata), arg0, arg1) +} + +// GetServiceAccountBySAN mocks base method. +func (m *MockStore) GetServiceAccountBySAN(arg0 context.Context, arg1 []string) ([]*db.Account, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetServiceAccountBySAN", arg0, arg1) + ret0, _ := ret[0].([]*db.Account) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetServiceAccountBySAN indicates an expected call of GetServiceAccountBySAN. +func (mr *MockStoreMockRecorder) GetServiceAccountBySAN(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetServiceAccountBySAN", reflect.TypeOf((*MockStore)(nil).GetServiceAccountBySAN), arg0, arg1) +} + +// GetServiceAccounts mocks base method. +func (m *MockStore) GetServiceAccounts(arg0 context.Context, arg1 string) ([]*db.Account, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetServiceAccounts", arg0, arg1) + ret0, _ := ret[0].([]*db.Account) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetServiceAccounts indicates an expected call of GetServiceAccounts. +func (mr *MockStoreMockRecorder) GetServiceAccounts(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetServiceAccounts", reflect.TypeOf((*MockStore)(nil).GetServiceAccounts), arg0, arg1) +} + +// GetServiceUUID mocks base method. +func (m *MockStore) GetServiceUUID(arg0 context.Context, arg1 uuid.UUID) (*db.Account, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetServiceUUID", arg0, arg1) + ret0, _ := ret[0].(*db.Account) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetServiceUUID indicates an expected call of GetServiceUUID. +func (mr *MockStoreMockRecorder) GetServiceUUID(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetServiceUUID", reflect.TypeOf((*MockStore)(nil).GetServiceUUID), arg0, arg1) +} + +// GetSignedCertificateByMetadata mocks base method. +func (m *MockStore) GetSignedCertificateByMetadata(arg0 context.Context, arg1 db.GetSignedCertificateByMetadataParams) ([]*db.Certificate, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetSignedCertificateByMetadata", arg0, arg1) + ret0, _ := ret[0].([]*db.Certificate) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetSignedCertificateByMetadata indicates an expected call of GetSignedCertificateByMetadata. +func (mr *MockStoreMockRecorder) GetSignedCertificateByMetadata(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSignedCertificateByMetadata", reflect.TypeOf((*MockStore)(nil).GetSignedCertificateByMetadata), arg0, arg1) +} + +// GetUser mocks base method. +func (m *MockStore) GetUser(arg0 context.Context, arg1 string) (*db.User, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetUser", arg0, arg1) + ret0, _ := ret[0].(*db.User) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetUser indicates an expected call of GetUser. +func (mr *MockStoreMockRecorder) GetUser(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUser", reflect.TypeOf((*MockStore)(nil).GetUser), arg0, arg1) +} + +// ListCertificateSubjectAlternativeName mocks base method. +func (m *MockStore) ListCertificateSubjectAlternativeName(arg0 context.Context, arg1 db.ListCertificateSubjectAlternativeNameParams) ([]*db.Certificate, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListCertificateSubjectAlternativeName", arg0, arg1) + ret0, _ := ret[0].([]*db.Certificate) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListCertificateSubjectAlternativeName indicates an expected call of ListCertificateSubjectAlternativeName. +func (mr *MockStoreMockRecorder) ListCertificateSubjectAlternativeName(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListCertificateSubjectAlternativeName", reflect.TypeOf((*MockStore)(nil).ListCertificateSubjectAlternativeName), arg0, arg1) +} + +// ListCertificates mocks base method. +func (m *MockStore) ListCertificates(arg0 context.Context, arg1 db.ListCertificatesParams) ([]*db.Certificate, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListCertificates", arg0, arg1) + ret0, _ := ret[0].([]*db.Certificate) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListCertificates indicates an expected call of ListCertificates. +func (mr *MockStoreMockRecorder) ListCertificates(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListCertificates", reflect.TypeOf((*MockStore)(nil).ListCertificates), arg0, arg1) +} + +// ListProvisionerAccounts mocks base method. +func (m *MockStore) ListProvisionerAccounts(arg0 context.Context, arg1 db.ListProvisionerAccountsParams) ([]*db.Provisioner, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListProvisionerAccounts", arg0, arg1) + ret0, _ := ret[0].([]*db.Provisioner) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListProvisionerAccounts indicates an expected call of ListProvisionerAccounts. +func (mr *MockStoreMockRecorder) ListProvisionerAccounts(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListProvisionerAccounts", reflect.TypeOf((*MockStore)(nil).ListProvisionerAccounts), arg0, arg1) +} + +// ListServiceAccounts mocks base method. +func (m *MockStore) ListServiceAccounts(arg0 context.Context, arg1 db.ListServiceAccountsParams) ([]*db.Account, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListServiceAccounts", arg0, arg1) + ret0, _ := ret[0].([]*db.Account) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListServiceAccounts indicates an expected call of ListServiceAccounts. +func (mr *MockStoreMockRecorder) ListServiceAccounts(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListServiceAccounts", reflect.TypeOf((*MockStore)(nil).ListServiceAccounts), arg0, arg1) +} + +// ListUsers mocks base method. +func (m *MockStore) ListUsers(arg0 context.Context, arg1 db.ListUsersParams) ([]*db.User, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListUsers", arg0, arg1) + ret0, _ := ret[0].([]*db.User) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListUsers indicates an expected call of ListUsers. +func (mr *MockStoreMockRecorder) ListUsers(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListUsers", reflect.TypeOf((*MockStore)(nil).ListUsers), arg0, arg1) +} + +// ListValidCertificateAuthorityFromSubordinateCA mocks base method. +func (m *MockStore) ListValidCertificateAuthorityFromSubordinateCA(arg0 context.Context, arg1 db.ListValidCertificateAuthorityFromSubordinateCAParams) ([]any, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListValidCertificateAuthorityFromSubordinateCA", arg0, arg1) + ret0, _ := ret[0].([]any) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListValidCertificateAuthorityFromSubordinateCA indicates an expected call of ListValidCertificateAuthorityFromSubordinateCA. +func (mr *MockStoreMockRecorder) ListValidCertificateAuthorityFromSubordinateCA(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListValidCertificateAuthorityFromSubordinateCA", reflect.TypeOf((*MockStore)(nil).ListValidCertificateAuthorityFromSubordinateCA), arg0, arg1) +} + +// LogCertificate mocks base method. +func (m *MockStore) LogCertificate(arg0 context.Context, arg1 db.LogCertificateParams) (*db.Certificate, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "LogCertificate", arg0, arg1) + ret0, _ := ret[0].(*db.Certificate) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// LogCertificate indicates an expected call of LogCertificate. +func (mr *MockStoreMockRecorder) LogCertificate(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LogCertificate", reflect.TypeOf((*MockStore)(nil).LogCertificate), arg0, arg1) +} + +// RevokeIssuedCertificateSerialNumber mocks base method. +func (m *MockStore) RevokeIssuedCertificateSerialNumber(arg0 context.Context, arg1 db.RevokeIssuedCertificateSerialNumberParams) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RevokeIssuedCertificateSerialNumber", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// RevokeIssuedCertificateSerialNumber indicates an expected call of RevokeIssuedCertificateSerialNumber. +func (mr *MockStoreMockRecorder) RevokeIssuedCertificateSerialNumber(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RevokeIssuedCertificateSerialNumber", reflect.TypeOf((*MockStore)(nil).RevokeIssuedCertificateSerialNumber), arg0, arg1) +} + +// StoreInstanceIdentityDocument mocks base method. +func (m *MockStore) StoreInstanceIdentityDocument(arg0 context.Context, arg1 db.StoreInstanceIdentityDocumentParams) (*db.AwsAttestation, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "StoreInstanceIdentityDocument", arg0, arg1) + ret0, _ := ret[0].(*db.AwsAttestation) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// StoreInstanceIdentityDocument indicates an expected call of StoreInstanceIdentityDocument. +func (mr *MockStoreMockRecorder) StoreInstanceIdentityDocument(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StoreInstanceIdentityDocument", reflect.TypeOf((*MockStore)(nil).StoreInstanceIdentityDocument), arg0, arg1) +} + +// TxCreateProvisionerAccount mocks base method. +func (m *MockStore) TxCreateProvisionerAccount(arg0 context.Context, arg1 db.CreateProvisionerAccountParams, arg2 db.StoreInstanceIdentityDocumentParams) (*db.Provisioner, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "TxCreateProvisionerAccount", arg0, arg1, arg2) + ret0, _ := ret[0].(*db.Provisioner) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// TxCreateProvisionerAccount indicates an expected call of TxCreateProvisionerAccount. +func (mr *MockStoreMockRecorder) TxCreateProvisionerAccount(arg0, arg1, arg2 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TxCreateProvisionerAccount", reflect.TypeOf((*MockStore)(nil).TxCreateProvisionerAccount), arg0, arg1, arg2) +} + +// TxCreateServiceAccount mocks base method. +func (m *MockStore) TxCreateServiceAccount(arg0 context.Context, arg1 db.CreateServiceAccountParams, arg2 db.StoreInstanceIdentityDocumentParams) (*db.Account, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "TxCreateServiceAccount", arg0, arg1, arg2) + ret0, _ := ret[0].(*db.Account) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// TxCreateServiceAccount indicates an expected call of TxCreateServiceAccount. +func (mr *MockStoreMockRecorder) TxCreateServiceAccount(arg0, arg1, arg2 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TxCreateServiceAccount", reflect.TypeOf((*MockStore)(nil).TxCreateServiceAccount), arg0, arg1, arg2) +} + +// TxDeleteProvisionerAccount mocks base method. +func (m *MockStore) TxDeleteProvisionerAccount(arg0 context.Context, arg1 uuid.UUID) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "TxDeleteProvisionerAccount", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// TxDeleteProvisionerAccount indicates an expected call of TxDeleteProvisionerAccount. +func (mr *MockStoreMockRecorder) TxDeleteProvisionerAccount(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TxDeleteProvisionerAccount", reflect.TypeOf((*MockStore)(nil).TxDeleteProvisionerAccount), arg0, arg1) +} + +// TxDeleteServiceAccount mocks base method. +func (m *MockStore) TxDeleteServiceAccount(arg0 context.Context, arg1 uuid.UUID) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "TxDeleteServiceAccount", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// TxDeleteServiceAccount indicates an expected call of TxDeleteServiceAccount. +func (mr *MockStoreMockRecorder) TxDeleteServiceAccount(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TxDeleteServiceAccount", reflect.TypeOf((*MockStore)(nil).TxDeleteServiceAccount), arg0, arg1) +} + +// TxUpdateServiceAccount mocks base method. +func (m *MockStore) TxUpdateServiceAccount(arg0 context.Context, arg1 db.Account, arg2 types.NodeAttestation) (*db.Account, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "TxUpdateServiceAccount", arg0, arg1, arg2) + ret0, _ := ret[0].(*db.Account) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// TxUpdateServiceAccount indicates an expected call of TxUpdateServiceAccount. +func (mr *MockStoreMockRecorder) TxUpdateServiceAccount(arg0, arg1, arg2 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TxUpdateServiceAccount", reflect.TypeOf((*MockStore)(nil).TxUpdateServiceAccount), arg0, arg1, arg2) +} + +// UpdateInstanceIdentityNodeAttestor mocks base method. +func (m *MockStore) UpdateInstanceIdentityNodeAttestor(arg0 context.Context, arg1 db.UpdateInstanceIdentityNodeAttestorParams) (*db.Account, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateInstanceIdentityNodeAttestor", arg0, arg1) + ret0, _ := ret[0].(*db.Account) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UpdateInstanceIdentityNodeAttestor indicates an expected call of UpdateInstanceIdentityNodeAttestor. +func (mr *MockStoreMockRecorder) UpdateInstanceIdentityNodeAttestor(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateInstanceIdentityNodeAttestor", reflect.TypeOf((*MockStore)(nil).UpdateInstanceIdentityNodeAttestor), arg0, arg1) +} + +// UpdateServiceAccount mocks base method. +func (m *MockStore) UpdateServiceAccount(arg0 context.Context, arg1 db.UpdateServiceAccountParams) (*db.Account, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateServiceAccount", arg0, arg1) + ret0, _ := ret[0].(*db.Account) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UpdateServiceAccount indicates an expected call of UpdateServiceAccount. +func (mr *MockStoreMockRecorder) UpdateServiceAccount(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateServiceAccount", reflect.TypeOf((*MockStore)(nil).UpdateServiceAccount), arg0, arg1) +} + +// UpdateUserAuthentication mocks base method. +func (m *MockStore) UpdateUserAuthentication(arg0 context.Context, arg1 db.UpdateUserAuthenticationParams) (*db.User, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateUserAuthentication", arg0, arg1) + ret0, _ := ret[0].(*db.User) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UpdateUserAuthentication indicates an expected call of UpdateUserAuthentication. +func (mr *MockStoreMockRecorder) UpdateUserAuthentication(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUserAuthentication", reflect.TypeOf((*MockStore)(nil).UpdateUserAuthentication), arg0, arg1) +} + +// UpdateUserPermission mocks base method. +func (m *MockStore) UpdateUserPermission(arg0 context.Context, arg1 db.UpdateUserPermissionParams) (*db.User, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateUserPermission", arg0, arg1) + ret0, _ := ret[0].(*db.User) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UpdateUserPermission indicates an expected call of UpdateUserPermission. +func (mr *MockStoreMockRecorder) UpdateUserPermission(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUserPermission", reflect.TypeOf((*MockStore)(nil).UpdateUserPermission), arg0, arg1) +} diff --git a/db/query/accounts.sql b/db/query/accounts.sql new file mode 100644 index 0000000..d4c8473 --- /dev/null +++ b/db/query/accounts.sql @@ -0,0 +1,75 @@ +-- name: CreateServiceAccount :one +INSERT INTO accounts ( + client_id, + api_token, + service_account, + environment, + team, + email, + regular_expression, + valid_subject_alternate_name, + valid_certificate_authorities, + extended_key, + certificate_validity, + subordinate_ca, + provisioned, + node_attestation, + created_at, + created_by +) VALUES ( + $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16 +) RETURNING *; + +-- name: GetServiceUUID :one +SELECT * FROM accounts +WHERE client_id = $1; + +-- name: GetServiceAccounts :many +SELECT * FROM accounts +WHERE service_account = $1; + +-- name: ListServiceAccounts :many +SELECT * FROM accounts +ORDER BY service_account +LIMIT $1 +OFFSET $2; + +-- name: UpdateServiceAccount :one +UPDATE accounts +SET + environment = $2, + team = $3, + email = $4, + regular_expression = $5, + valid_subject_alternate_name = $6, + valid_certificate_authorities = $7, + extended_key = $8, + certificate_validity = $9, + subordinate_ca = $10, + node_attestation = $11 +WHERE client_id = $1 +RETURNING *; + +-- name: UpdateInstanceIdentityNodeAttestor :one +UPDATE accounts +SET + node_attestation = $2 +WHERE client_id = $1 +RETURNING *; + +-- name: DeleteServiceAccount :exec +DELETE FROM accounts +WHERE client_id = $1; + +-- name: GetServiceAccountBySAN :many +SELECT * FROM accounts +WHERE valid_subject_alternate_name = ANY($1::string[]); + +-- name: GetServiceAccountByMetadata :many +SELECT * FROM accounts +WHERE service_account LIKE $1 AND environment LIKE $2 AND extended_key LIKE $3; + +-- name: ListValidCertificateAuthorityFromSubordinateCA :many +SELECT DISTINCT unnest(valid_certificate_authorities) AS certificate_authorities +FROM accounts +WHERE subordinate_ca = $1 AND environment = $2; diff --git a/db/query/aws_attestation.sql b/db/query/aws_attestation.sql new file mode 100644 index 0000000..83e99f9 --- /dev/null +++ b/db/query/aws_attestation.sql @@ -0,0 +1,21 @@ +-- name: StoreInstanceIdentityDocument :one +INSERT into aws_attestation ( + client_id, + role_arn, + assume_role, + security_group_id, + region, + instance_id, + image_id, + instance_tags +) VALUES ( + $1, $2, $3, $4, $5, $6, $7, $8 +) RETURNING *; + +-- name: GetInstanceIdentityDocument :one +SELECT * from aws_attestation +WHERE client_id = $1; + +-- name: DeleteInstanceIdentityDocument :exec +DELETE FROM aws_attestation +WHERE client_id = $1; \ No newline at end of file diff --git a/db/query/certificate.sql b/db/query/certificate.sql new file mode 100644 index 0000000..6987027 --- /dev/null +++ b/db/query/certificate.sql @@ -0,0 +1,38 @@ +-- name: LogCertificate :one +INSERT INTO certificates ( + serial_number, + account, + environment, + extended_key, + common_name, + subject_alternative_name, + expiration_date, + issued_date, + certificate_authority_arn +) VALUES ( + $1, $2, $3, $4, $5, $6, $7, $8, $9 +) RETURNING *; + +-- name: GetCertificate :one +SELECT * FROM certificates +WHERE serial_number = $1; + +-- name: ListCertificateSubjectAlternativeName :many +SELECT * FROM certificates +WHERE common_name = $1 OR $1 = ANY(subject_alternative_name) +LIMIT $2 +OFFSET $3; + +-- name: ListCertificates :many +SELECT * FROM certificates +LIMIT $1 +OFFSET $2; + +-- name: RevokeIssuedCertificateSerialNumber :exec +UPDATE certificates +SET revoked = TRUE, revoke_date = $2, revoked_by = $3 +WHERE serial_number = $1; + +-- name: GetSignedCertificateByMetadata :many +SELECT * FROM certificates +WHERE serial_number LIKE $1 AND account LIKE $2 AND environment LIKE $3 AND extended_key LIKE $4; \ No newline at end of file diff --git a/db/query/provisioners.sql b/db/query/provisioners.sql new file mode 100644 index 0000000..0b1a622 --- /dev/null +++ b/db/query/provisioners.sql @@ -0,0 +1,31 @@ +-- name: CreateProvisionerAccount :one +INSERT INTO provisioners ( + client_id, + api_token, + provisioner_account, + environments, + team, + email, + regular_expression, + node_attestation, + valid_subject_alternate_names, + extended_keys, + max_certificate_validity, + created_at, + created_by +) VALUES ( + $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13 +) RETURNING *; + +-- name: GetProvisionerUUID :one +SELECT * FROM provisioners +WHERE client_id = $1; + +-- name: DeleteProvisionerAccount :exec +DELETE FROM provisioners +WHERE client_id = $1; + +-- name: ListProvisionerAccounts :many +SELECT * FROM provisioners +LIMIT $1 +OFFSET $2; \ No newline at end of file diff --git a/db/query/users.sql b/db/query/users.sql new file mode 100644 index 0000000..4788712 --- /dev/null +++ b/db/query/users.sql @@ -0,0 +1,37 @@ +-- name: CreateUser :one +INSERT INTO users ( + uuid, + username, + hashed_credential, + full_name, + email, + permissions +) VALUES ( + $1, $2, $3, $4, $5, $6 +) RETURNING *; + +-- name: GetUser :one +SELECT * FROM users +WHERE username = $1 LIMIT 1; + +-- name: ListUsers :many +SELECT * FROM users +ORDER BY username +LIMIT $1 +OFFSET $2; + +-- name: UpdateUserAuthentication :one +UPDATE users +SET hashed_credential = $2, credential_changed_at = now() +WHERE username = $1 +RETURNING *; + +-- name: UpdateUserPermission :one +UPDATE users +SET permissions = $2 +WHERE username = $1 +RETURNING *; + +-- name: DeleteUser :exec +DELETE FROM users +WHERE username = $1; \ No newline at end of file diff --git a/db/sqlc.yaml b/db/sqlc.yaml new file mode 100644 index 0000000..0377ada --- /dev/null +++ b/db/sqlc.yaml @@ -0,0 +1,13 @@ +version: '1' +packages: + - name: 'db' + path: './sqlc' + queries: './query/' + schema: './migration/' + engine: 'postgresql' + emit_json_tags: true + emit_prepared_queries: false + emit_interface: true + emit_exact_table_names: false + emit_empty_slices: true + emit_result_struct_pointers: true diff --git a/db/sqlc/accounts.sql.go b/db/sqlc/accounts.sql.go new file mode 100644 index 0000000..bdca389 --- /dev/null +++ b/db/sqlc/accounts.sql.go @@ -0,0 +1,468 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.21.0 +// source: accounts.sql + +package db + +import ( + "context" + "database/sql" + "time" + + "github.com/google/uuid" + "github.com/lib/pq" +) + +const createServiceAccount = `-- name: CreateServiceAccount :one +INSERT INTO accounts ( + client_id, + api_token, + service_account, + environment, + team, + email, + regular_expression, + valid_subject_alternate_name, + valid_certificate_authorities, + extended_key, + certificate_validity, + subordinate_ca, + provisioned, + node_attestation, + created_at, + created_by +) VALUES ( + $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16 +) RETURNING client_id, api_token, service_account, environment, team, email, regular_expression, valid_subject_alternate_name, valid_certificate_authorities, extended_key, certificate_validity, subordinate_ca, provisioned, node_attestation, created_at, created_by +` + +type CreateServiceAccountParams struct { + ClientID uuid.UUID `json:"client_id"` + ApiToken string `json:"api_token"` + ServiceAccount string `json:"service_account"` + Environment string `json:"environment"` + Team string `json:"team"` + Email string `json:"email"` + RegularExpression sql.NullString `json:"regular_expression"` + ValidSubjectAlternateName []string `json:"valid_subject_alternate_name"` + ValidCertificateAuthorities []string `json:"valid_certificate_authorities"` + ExtendedKey string `json:"extended_key"` + CertificateValidity int16 `json:"certificate_validity"` + SubordinateCa string `json:"subordinate_ca"` + Provisioned bool `json:"provisioned"` + NodeAttestation []string `json:"node_attestation"` + CreatedAt time.Time `json:"created_at"` + CreatedBy uuid.UUID `json:"created_by"` +} + +func (q *Queries) CreateServiceAccount(ctx context.Context, arg CreateServiceAccountParams) (*Account, error) { + row := q.db.QueryRowContext(ctx, createServiceAccount, + arg.ClientID, + arg.ApiToken, + arg.ServiceAccount, + arg.Environment, + arg.Team, + arg.Email, + arg.RegularExpression, + pq.Array(arg.ValidSubjectAlternateName), + pq.Array(arg.ValidCertificateAuthorities), + arg.ExtendedKey, + arg.CertificateValidity, + arg.SubordinateCa, + arg.Provisioned, + pq.Array(arg.NodeAttestation), + arg.CreatedAt, + arg.CreatedBy, + ) + var i Account + err := row.Scan( + &i.ClientID, + &i.ApiToken, + &i.ServiceAccount, + &i.Environment, + &i.Team, + &i.Email, + &i.RegularExpression, + pq.Array(&i.ValidSubjectAlternateName), + pq.Array(&i.ValidCertificateAuthorities), + &i.ExtendedKey, + &i.CertificateValidity, + &i.SubordinateCa, + &i.Provisioned, + pq.Array(&i.NodeAttestation), + &i.CreatedAt, + &i.CreatedBy, + ) + return &i, err +} + +const deleteServiceAccount = `-- name: DeleteServiceAccount :exec +DELETE FROM accounts +WHERE client_id = $1 +` + +func (q *Queries) DeleteServiceAccount(ctx context.Context, clientID uuid.UUID) error { + _, err := q.db.ExecContext(ctx, deleteServiceAccount, clientID) + return err +} + +const getServiceAccountByMetadata = `-- name: GetServiceAccountByMetadata :many +SELECT client_id, api_token, service_account, environment, team, email, regular_expression, valid_subject_alternate_name, valid_certificate_authorities, extended_key, certificate_validity, subordinate_ca, provisioned, node_attestation, created_at, created_by FROM accounts +WHERE service_account LIKE $1 AND environment LIKE $2 AND extended_key LIKE $3 +` + +type GetServiceAccountByMetadataParams struct { + ServiceAccount string `json:"service_account"` + Environment string `json:"environment"` + ExtendedKey string `json:"extended_key"` +} + +func (q *Queries) GetServiceAccountByMetadata(ctx context.Context, arg GetServiceAccountByMetadataParams) ([]*Account, error) { + rows, err := q.db.QueryContext(ctx, getServiceAccountByMetadata, arg.ServiceAccount, arg.Environment, arg.ExtendedKey) + if err != nil { + return nil, err + } + defer rows.Close() + items := []*Account{} + for rows.Next() { + var i Account + if err := rows.Scan( + &i.ClientID, + &i.ApiToken, + &i.ServiceAccount, + &i.Environment, + &i.Team, + &i.Email, + &i.RegularExpression, + pq.Array(&i.ValidSubjectAlternateName), + pq.Array(&i.ValidCertificateAuthorities), + &i.ExtendedKey, + &i.CertificateValidity, + &i.SubordinateCa, + &i.Provisioned, + pq.Array(&i.NodeAttestation), + &i.CreatedAt, + &i.CreatedBy, + ); err != nil { + return nil, err + } + items = append(items, &i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getServiceAccountBySAN = `-- name: GetServiceAccountBySAN :many +SELECT client_id, api_token, service_account, environment, team, email, regular_expression, valid_subject_alternate_name, valid_certificate_authorities, extended_key, certificate_validity, subordinate_ca, provisioned, node_attestation, created_at, created_by FROM accounts +WHERE valid_subject_alternate_name = ANY($1::string[]) +` + +func (q *Queries) GetServiceAccountBySAN(ctx context.Context, dollar_1 []string) ([]*Account, error) { + rows, err := q.db.QueryContext(ctx, getServiceAccountBySAN, pq.Array(dollar_1)) + if err != nil { + return nil, err + } + defer rows.Close() + items := []*Account{} + for rows.Next() { + var i Account + if err := rows.Scan( + &i.ClientID, + &i.ApiToken, + &i.ServiceAccount, + &i.Environment, + &i.Team, + &i.Email, + &i.RegularExpression, + pq.Array(&i.ValidSubjectAlternateName), + pq.Array(&i.ValidCertificateAuthorities), + &i.ExtendedKey, + &i.CertificateValidity, + &i.SubordinateCa, + &i.Provisioned, + pq.Array(&i.NodeAttestation), + &i.CreatedAt, + &i.CreatedBy, + ); err != nil { + return nil, err + } + items = append(items, &i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getServiceAccounts = `-- name: GetServiceAccounts :many +SELECT client_id, api_token, service_account, environment, team, email, regular_expression, valid_subject_alternate_name, valid_certificate_authorities, extended_key, certificate_validity, subordinate_ca, provisioned, node_attestation, created_at, created_by FROM accounts +WHERE service_account = $1 +` + +func (q *Queries) GetServiceAccounts(ctx context.Context, serviceAccount string) ([]*Account, error) { + rows, err := q.db.QueryContext(ctx, getServiceAccounts, serviceAccount) + if err != nil { + return nil, err + } + defer rows.Close() + items := []*Account{} + for rows.Next() { + var i Account + if err := rows.Scan( + &i.ClientID, + &i.ApiToken, + &i.ServiceAccount, + &i.Environment, + &i.Team, + &i.Email, + &i.RegularExpression, + pq.Array(&i.ValidSubjectAlternateName), + pq.Array(&i.ValidCertificateAuthorities), + &i.ExtendedKey, + &i.CertificateValidity, + &i.SubordinateCa, + &i.Provisioned, + pq.Array(&i.NodeAttestation), + &i.CreatedAt, + &i.CreatedBy, + ); err != nil { + return nil, err + } + items = append(items, &i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getServiceUUID = `-- name: GetServiceUUID :one +SELECT client_id, api_token, service_account, environment, team, email, regular_expression, valid_subject_alternate_name, valid_certificate_authorities, extended_key, certificate_validity, subordinate_ca, provisioned, node_attestation, created_at, created_by FROM accounts +WHERE client_id = $1 +` + +func (q *Queries) GetServiceUUID(ctx context.Context, clientID uuid.UUID) (*Account, error) { + row := q.db.QueryRowContext(ctx, getServiceUUID, clientID) + var i Account + err := row.Scan( + &i.ClientID, + &i.ApiToken, + &i.ServiceAccount, + &i.Environment, + &i.Team, + &i.Email, + &i.RegularExpression, + pq.Array(&i.ValidSubjectAlternateName), + pq.Array(&i.ValidCertificateAuthorities), + &i.ExtendedKey, + &i.CertificateValidity, + &i.SubordinateCa, + &i.Provisioned, + pq.Array(&i.NodeAttestation), + &i.CreatedAt, + &i.CreatedBy, + ) + return &i, err +} + +const listServiceAccounts = `-- name: ListServiceAccounts :many +SELECT client_id, api_token, service_account, environment, team, email, regular_expression, valid_subject_alternate_name, valid_certificate_authorities, extended_key, certificate_validity, subordinate_ca, provisioned, node_attestation, created_at, created_by FROM accounts +ORDER BY service_account +LIMIT $1 +OFFSET $2 +` + +type ListServiceAccountsParams struct { + Limit int32 `json:"limit"` + Offset int32 `json:"offset"` +} + +func (q *Queries) ListServiceAccounts(ctx context.Context, arg ListServiceAccountsParams) ([]*Account, error) { + rows, err := q.db.QueryContext(ctx, listServiceAccounts, arg.Limit, arg.Offset) + if err != nil { + return nil, err + } + defer rows.Close() + items := []*Account{} + for rows.Next() { + var i Account + if err := rows.Scan( + &i.ClientID, + &i.ApiToken, + &i.ServiceAccount, + &i.Environment, + &i.Team, + &i.Email, + &i.RegularExpression, + pq.Array(&i.ValidSubjectAlternateName), + pq.Array(&i.ValidCertificateAuthorities), + &i.ExtendedKey, + &i.CertificateValidity, + &i.SubordinateCa, + &i.Provisioned, + pq.Array(&i.NodeAttestation), + &i.CreatedAt, + &i.CreatedBy, + ); err != nil { + return nil, err + } + items = append(items, &i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const listValidCertificateAuthorityFromSubordinateCA = `-- name: ListValidCertificateAuthorityFromSubordinateCA :many +SELECT DISTINCT unnest(valid_certificate_authorities) AS certificate_authorities +FROM accounts +WHERE subordinate_ca = $1 AND environment = $2 +` + +type ListValidCertificateAuthorityFromSubordinateCAParams struct { + SubordinateCa string `json:"subordinate_ca"` + Environment string `json:"environment"` +} + +func (q *Queries) ListValidCertificateAuthorityFromSubordinateCA(ctx context.Context, arg ListValidCertificateAuthorityFromSubordinateCAParams) ([]interface{}, error) { + rows, err := q.db.QueryContext(ctx, listValidCertificateAuthorityFromSubordinateCA, arg.SubordinateCa, arg.Environment) + if err != nil { + return nil, err + } + defer rows.Close() + items := []interface{}{} + for rows.Next() { + var certificate_authorities interface{} + if err := rows.Scan(&certificate_authorities); err != nil { + return nil, err + } + items = append(items, certificate_authorities) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const updateInstanceIdentityNodeAttestor = `-- name: UpdateInstanceIdentityNodeAttestor :one +UPDATE accounts +SET + node_attestation = $2 +WHERE client_id = $1 +RETURNING client_id, api_token, service_account, environment, team, email, regular_expression, valid_subject_alternate_name, valid_certificate_authorities, extended_key, certificate_validity, subordinate_ca, provisioned, node_attestation, created_at, created_by +` + +type UpdateInstanceIdentityNodeAttestorParams struct { + ClientID uuid.UUID `json:"client_id"` + NodeAttestation []string `json:"node_attestation"` +} + +func (q *Queries) UpdateInstanceIdentityNodeAttestor(ctx context.Context, arg UpdateInstanceIdentityNodeAttestorParams) (*Account, error) { + row := q.db.QueryRowContext(ctx, updateInstanceIdentityNodeAttestor, arg.ClientID, pq.Array(arg.NodeAttestation)) + var i Account + err := row.Scan( + &i.ClientID, + &i.ApiToken, + &i.ServiceAccount, + &i.Environment, + &i.Team, + &i.Email, + &i.RegularExpression, + pq.Array(&i.ValidSubjectAlternateName), + pq.Array(&i.ValidCertificateAuthorities), + &i.ExtendedKey, + &i.CertificateValidity, + &i.SubordinateCa, + &i.Provisioned, + pq.Array(&i.NodeAttestation), + &i.CreatedAt, + &i.CreatedBy, + ) + return &i, err +} + +const updateServiceAccount = `-- name: UpdateServiceAccount :one +UPDATE accounts +SET + environment = $2, + team = $3, + email = $4, + regular_expression = $5, + valid_subject_alternate_name = $6, + valid_certificate_authorities = $7, + extended_key = $8, + certificate_validity = $9, + subordinate_ca = $10, + node_attestation = $11 +WHERE client_id = $1 +RETURNING client_id, api_token, service_account, environment, team, email, regular_expression, valid_subject_alternate_name, valid_certificate_authorities, extended_key, certificate_validity, subordinate_ca, provisioned, node_attestation, created_at, created_by +` + +type UpdateServiceAccountParams struct { + ClientID uuid.UUID `json:"client_id"` + Environment string `json:"environment"` + Team string `json:"team"` + Email string `json:"email"` + RegularExpression sql.NullString `json:"regular_expression"` + ValidSubjectAlternateName []string `json:"valid_subject_alternate_name"` + ValidCertificateAuthorities []string `json:"valid_certificate_authorities"` + ExtendedKey string `json:"extended_key"` + CertificateValidity int16 `json:"certificate_validity"` + SubordinateCa string `json:"subordinate_ca"` + NodeAttestation []string `json:"node_attestation"` +} + +func (q *Queries) UpdateServiceAccount(ctx context.Context, arg UpdateServiceAccountParams) (*Account, error) { + row := q.db.QueryRowContext(ctx, updateServiceAccount, + arg.ClientID, + arg.Environment, + arg.Team, + arg.Email, + arg.RegularExpression, + pq.Array(arg.ValidSubjectAlternateName), + pq.Array(arg.ValidCertificateAuthorities), + arg.ExtendedKey, + arg.CertificateValidity, + arg.SubordinateCa, + pq.Array(arg.NodeAttestation), + ) + var i Account + err := row.Scan( + &i.ClientID, + &i.ApiToken, + &i.ServiceAccount, + &i.Environment, + &i.Team, + &i.Email, + &i.RegularExpression, + pq.Array(&i.ValidSubjectAlternateName), + pq.Array(&i.ValidCertificateAuthorities), + &i.ExtendedKey, + &i.CertificateValidity, + &i.SubordinateCa, + &i.Provisioned, + pq.Array(&i.NodeAttestation), + &i.CreatedAt, + &i.CreatedBy, + ) + return &i, err +} diff --git a/db/sqlc/aws_attestation.sql.go b/db/sqlc/aws_attestation.sql.go new file mode 100644 index 0000000..c2eaa98 --- /dev/null +++ b/db/sqlc/aws_attestation.sql.go @@ -0,0 +1,97 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.21.0 +// source: aws_attestation.sql + +package db + +import ( + "context" + "database/sql" + + "github.com/google/uuid" + "github.com/lib/pq" + "github.com/sqlc-dev/pqtype" +) + +const deleteInstanceIdentityDocument = `-- name: DeleteInstanceIdentityDocument :exec +DELETE FROM aws_attestation +WHERE client_id = $1 +` + +func (q *Queries) DeleteInstanceIdentityDocument(ctx context.Context, clientID uuid.UUID) error { + _, err := q.db.ExecContext(ctx, deleteInstanceIdentityDocument, clientID) + return err +} + +const getInstanceIdentityDocument = `-- name: GetInstanceIdentityDocument :one +SELECT client_id, role_arn, assume_role, security_group_id, region, instance_id, image_id, instance_tags from aws_attestation +WHERE client_id = $1 +` + +func (q *Queries) GetInstanceIdentityDocument(ctx context.Context, clientID uuid.UUID) (*AwsAttestation, error) { + row := q.db.QueryRowContext(ctx, getInstanceIdentityDocument, clientID) + var i AwsAttestation + err := row.Scan( + &i.ClientID, + &i.RoleArn, + &i.AssumeRole, + pq.Array(&i.SecurityGroupID), + &i.Region, + &i.InstanceID, + &i.ImageID, + &i.InstanceTags, + ) + return &i, err +} + +const storeInstanceIdentityDocument = `-- name: StoreInstanceIdentityDocument :one +INSERT into aws_attestation ( + client_id, + role_arn, + assume_role, + security_group_id, + region, + instance_id, + image_id, + instance_tags +) VALUES ( + $1, $2, $3, $4, $5, $6, $7, $8 +) RETURNING client_id, role_arn, assume_role, security_group_id, region, instance_id, image_id, instance_tags +` + +type StoreInstanceIdentityDocumentParams struct { + ClientID uuid.UUID `json:"client_id"` + RoleArn sql.NullString `json:"role_arn"` + AssumeRole sql.NullString `json:"assume_role"` + SecurityGroupID []string `json:"security_group_id"` + Region sql.NullString `json:"region"` + InstanceID sql.NullString `json:"instance_id"` + ImageID sql.NullString `json:"image_id"` + InstanceTags pqtype.NullRawMessage `json:"instance_tags"` +} + +func (q *Queries) StoreInstanceIdentityDocument(ctx context.Context, arg StoreInstanceIdentityDocumentParams) (*AwsAttestation, error) { + row := q.db.QueryRowContext(ctx, storeInstanceIdentityDocument, + arg.ClientID, + arg.RoleArn, + arg.AssumeRole, + pq.Array(arg.SecurityGroupID), + arg.Region, + arg.InstanceID, + arg.ImageID, + arg.InstanceTags, + ) + var i AwsAttestation + err := row.Scan( + &i.ClientID, + &i.RoleArn, + &i.AssumeRole, + pq.Array(&i.SecurityGroupID), + &i.Region, + &i.InstanceID, + &i.ImageID, + &i.InstanceTags, + ) + return &i, err +} diff --git a/db/sqlc/certificate.sql.go b/db/sqlc/certificate.sql.go new file mode 100644 index 0000000..e278aa9 --- /dev/null +++ b/db/sqlc/certificate.sql.go @@ -0,0 +1,263 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.21.0 +// source: certificate.sql + +package db + +import ( + "context" + "database/sql" + "time" + + "github.com/lib/pq" +) + +const getCertificate = `-- name: GetCertificate :one +SELECT serial_number, account, environment, extended_key, common_name, subject_alternative_name, expiration_date, issued_date, revoked, revoked_by, revoke_date, certificate_authority_arn FROM certificates +WHERE serial_number = $1 +` + +func (q *Queries) GetCertificate(ctx context.Context, serialNumber string) (*Certificate, error) { + row := q.db.QueryRowContext(ctx, getCertificate, serialNumber) + var i Certificate + err := row.Scan( + &i.SerialNumber, + &i.Account, + &i.Environment, + &i.ExtendedKey, + &i.CommonName, + pq.Array(&i.SubjectAlternativeName), + &i.ExpirationDate, + &i.IssuedDate, + &i.Revoked, + &i.RevokedBy, + &i.RevokeDate, + &i.CertificateAuthorityArn, + ) + return &i, err +} + +const getSignedCertificateByMetadata = `-- name: GetSignedCertificateByMetadata :many +SELECT serial_number, account, environment, extended_key, common_name, subject_alternative_name, expiration_date, issued_date, revoked, revoked_by, revoke_date, certificate_authority_arn FROM certificates +WHERE serial_number LIKE $1 AND account LIKE $2 AND environment LIKE $3 AND extended_key LIKE $4 +` + +type GetSignedCertificateByMetadataParams struct { + SerialNumber string `json:"serial_number"` + Account string `json:"account"` + Environment string `json:"environment"` + ExtendedKey string `json:"extended_key"` +} + +func (q *Queries) GetSignedCertificateByMetadata(ctx context.Context, arg GetSignedCertificateByMetadataParams) ([]*Certificate, error) { + rows, err := q.db.QueryContext(ctx, getSignedCertificateByMetadata, + arg.SerialNumber, + arg.Account, + arg.Environment, + arg.ExtendedKey, + ) + if err != nil { + return nil, err + } + defer rows.Close() + items := []*Certificate{} + for rows.Next() { + var i Certificate + if err := rows.Scan( + &i.SerialNumber, + &i.Account, + &i.Environment, + &i.ExtendedKey, + &i.CommonName, + pq.Array(&i.SubjectAlternativeName), + &i.ExpirationDate, + &i.IssuedDate, + &i.Revoked, + &i.RevokedBy, + &i.RevokeDate, + &i.CertificateAuthorityArn, + ); err != nil { + return nil, err + } + items = append(items, &i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const listCertificateSubjectAlternativeName = `-- name: ListCertificateSubjectAlternativeName :many +SELECT serial_number, account, environment, extended_key, common_name, subject_alternative_name, expiration_date, issued_date, revoked, revoked_by, revoke_date, certificate_authority_arn FROM certificates +WHERE common_name = $1 OR $1 = ANY(subject_alternative_name) +LIMIT $2 +OFFSET $3 +` + +type ListCertificateSubjectAlternativeNameParams struct { + CommonName string `json:"common_name"` + Limit int32 `json:"limit"` + Offset int32 `json:"offset"` +} + +func (q *Queries) ListCertificateSubjectAlternativeName(ctx context.Context, arg ListCertificateSubjectAlternativeNameParams) ([]*Certificate, error) { + rows, err := q.db.QueryContext(ctx, listCertificateSubjectAlternativeName, arg.CommonName, arg.Limit, arg.Offset) + if err != nil { + return nil, err + } + defer rows.Close() + items := []*Certificate{} + for rows.Next() { + var i Certificate + if err := rows.Scan( + &i.SerialNumber, + &i.Account, + &i.Environment, + &i.ExtendedKey, + &i.CommonName, + pq.Array(&i.SubjectAlternativeName), + &i.ExpirationDate, + &i.IssuedDate, + &i.Revoked, + &i.RevokedBy, + &i.RevokeDate, + &i.CertificateAuthorityArn, + ); err != nil { + return nil, err + } + items = append(items, &i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const listCertificates = `-- name: ListCertificates :many +SELECT serial_number, account, environment, extended_key, common_name, subject_alternative_name, expiration_date, issued_date, revoked, revoked_by, revoke_date, certificate_authority_arn FROM certificates +LIMIT $1 +OFFSET $2 +` + +type ListCertificatesParams struct { + Limit int32 `json:"limit"` + Offset int32 `json:"offset"` +} + +func (q *Queries) ListCertificates(ctx context.Context, arg ListCertificatesParams) ([]*Certificate, error) { + rows, err := q.db.QueryContext(ctx, listCertificates, arg.Limit, arg.Offset) + if err != nil { + return nil, err + } + defer rows.Close() + items := []*Certificate{} + for rows.Next() { + var i Certificate + if err := rows.Scan( + &i.SerialNumber, + &i.Account, + &i.Environment, + &i.ExtendedKey, + &i.CommonName, + pq.Array(&i.SubjectAlternativeName), + &i.ExpirationDate, + &i.IssuedDate, + &i.Revoked, + &i.RevokedBy, + &i.RevokeDate, + &i.CertificateAuthorityArn, + ); err != nil { + return nil, err + } + items = append(items, &i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const logCertificate = `-- name: LogCertificate :one +INSERT INTO certificates ( + serial_number, + account, + environment, + extended_key, + common_name, + subject_alternative_name, + expiration_date, + issued_date, + certificate_authority_arn +) VALUES ( + $1, $2, $3, $4, $5, $6, $7, $8, $9 +) RETURNING serial_number, account, environment, extended_key, common_name, subject_alternative_name, expiration_date, issued_date, revoked, revoked_by, revoke_date, certificate_authority_arn +` + +type LogCertificateParams struct { + SerialNumber string `json:"serial_number"` + Account string `json:"account"` + Environment string `json:"environment"` + ExtendedKey string `json:"extended_key"` + CommonName string `json:"common_name"` + SubjectAlternativeName []string `json:"subject_alternative_name"` + ExpirationDate time.Time `json:"expiration_date"` + IssuedDate time.Time `json:"issued_date"` + CertificateAuthorityArn sql.NullString `json:"certificate_authority_arn"` +} + +func (q *Queries) LogCertificate(ctx context.Context, arg LogCertificateParams) (*Certificate, error) { + row := q.db.QueryRowContext(ctx, logCertificate, + arg.SerialNumber, + arg.Account, + arg.Environment, + arg.ExtendedKey, + arg.CommonName, + pq.Array(arg.SubjectAlternativeName), + arg.ExpirationDate, + arg.IssuedDate, + arg.CertificateAuthorityArn, + ) + var i Certificate + err := row.Scan( + &i.SerialNumber, + &i.Account, + &i.Environment, + &i.ExtendedKey, + &i.CommonName, + pq.Array(&i.SubjectAlternativeName), + &i.ExpirationDate, + &i.IssuedDate, + &i.Revoked, + &i.RevokedBy, + &i.RevokeDate, + &i.CertificateAuthorityArn, + ) + return &i, err +} + +const revokeIssuedCertificateSerialNumber = `-- name: RevokeIssuedCertificateSerialNumber :exec +UPDATE certificates +SET revoked = TRUE, revoke_date = $2, revoked_by = $3 +WHERE serial_number = $1 +` + +type RevokeIssuedCertificateSerialNumberParams struct { + SerialNumber string `json:"serial_number"` + RevokeDate sql.NullTime `json:"revoke_date"` + RevokedBy sql.NullString `json:"revoked_by"` +} + +func (q *Queries) RevokeIssuedCertificateSerialNumber(ctx context.Context, arg RevokeIssuedCertificateSerialNumberParams) error { + _, err := q.db.ExecContext(ctx, revokeIssuedCertificateSerialNumber, arg.SerialNumber, arg.RevokeDate, arg.RevokedBy) + return err +} diff --git a/db/sqlc/common.go b/db/sqlc/common.go new file mode 100644 index 0000000..36541dd --- /dev/null +++ b/db/sqlc/common.go @@ -0,0 +1,25 @@ +package db + +import "github.com/coinbase/baseca/internal/types" + +type CertificateResponseData struct { + Certificate string `json:"certificate"` + IntermediateCertificateChain string `json:"intermediate_certificate_chain,omitempty"` + RootCertificateChain string `json:"root_certificate_chain,omitempty"` + Metadata types.CertificateMetadata `json:"metadata"` +} + +type DatabaseEndpoints struct { + Writer Store + Reader Store +} + +type CachedServiceAccount struct { + ServiceAccount Account `json:"service_account"` + AwsIid AwsAttestation `json:"aws_iid"` +} + +type CachedProvisionerAccount struct { + ProvisionerAccount Provisioner `json:"provisioner_account"` + AwsIid AwsAttestation `json:"aws_iid"` +} diff --git a/db/sqlc/db.go b/db/sqlc/db.go new file mode 100644 index 0000000..46fda54 --- /dev/null +++ b/db/sqlc/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.21.0 + +package db + +import ( + "context" + "database/sql" +) + +type DBTX interface { + ExecContext(context.Context, string, ...interface{}) (sql.Result, error) + PrepareContext(context.Context, string) (*sql.Stmt, error) + QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...interface{}) *sql.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx *sql.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/db/sqlc/models.go b/db/sqlc/models.go new file mode 100644 index 0000000..c71cd07 --- /dev/null +++ b/db/sqlc/models.go @@ -0,0 +1,85 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.21.0 + +package db + +import ( + "database/sql" + "time" + + "github.com/google/uuid" + "github.com/sqlc-dev/pqtype" +) + +type Account struct { + ClientID uuid.UUID `json:"client_id"` + ApiToken string `json:"api_token"` + ServiceAccount string `json:"service_account"` + Environment string `json:"environment"` + Team string `json:"team"` + Email string `json:"email"` + RegularExpression sql.NullString `json:"regular_expression"` + ValidSubjectAlternateName []string `json:"valid_subject_alternate_name"` + ValidCertificateAuthorities []string `json:"valid_certificate_authorities"` + ExtendedKey string `json:"extended_key"` + CertificateValidity int16 `json:"certificate_validity"` + SubordinateCa string `json:"subordinate_ca"` + Provisioned bool `json:"provisioned"` + NodeAttestation []string `json:"node_attestation"` + CreatedAt time.Time `json:"created_at"` + CreatedBy uuid.UUID `json:"created_by"` +} + +type AwsAttestation struct { + ClientID uuid.UUID `json:"client_id"` + RoleArn sql.NullString `json:"role_arn"` + AssumeRole sql.NullString `json:"assume_role"` + SecurityGroupID []string `json:"security_group_id"` + Region sql.NullString `json:"region"` + InstanceID sql.NullString `json:"instance_id"` + ImageID sql.NullString `json:"image_id"` + InstanceTags pqtype.NullRawMessage `json:"instance_tags"` +} + +type Certificate struct { + SerialNumber string `json:"serial_number"` + Account string `json:"account"` + Environment string `json:"environment"` + ExtendedKey string `json:"extended_key"` + CommonName string `json:"common_name"` + SubjectAlternativeName []string `json:"subject_alternative_name"` + ExpirationDate time.Time `json:"expiration_date"` + IssuedDate time.Time `json:"issued_date"` + Revoked bool `json:"revoked"` + RevokedBy sql.NullString `json:"revoked_by"` + RevokeDate sql.NullTime `json:"revoke_date"` + CertificateAuthorityArn sql.NullString `json:"certificate_authority_arn"` +} + +type Provisioner struct { + ClientID uuid.UUID `json:"client_id"` + ApiToken string `json:"api_token"` + ProvisionerAccount string `json:"provisioner_account"` + Environments []string `json:"environments"` + Team string `json:"team"` + Email string `json:"email"` + RegularExpression sql.NullString `json:"regular_expression"` + ValidSubjectAlternateNames []string `json:"valid_subject_alternate_names"` + ExtendedKeys []string `json:"extended_keys"` + MaxCertificateValidity int16 `json:"max_certificate_validity"` + NodeAttestation []string `json:"node_attestation"` + CreatedAt time.Time `json:"created_at"` + CreatedBy uuid.UUID `json:"created_by"` +} + +type User struct { + Uuid uuid.UUID `json:"uuid"` + Username string `json:"username"` + HashedCredential string `json:"hashed_credential"` + FullName string `json:"full_name"` + Email string `json:"email"` + Permissions string `json:"permissions"` + CredentialChangedAt time.Time `json:"credential_changed_at"` + CreatedAt time.Time `json:"created_at"` +} diff --git a/db/sqlc/provisioners.sql.go b/db/sqlc/provisioners.sql.go new file mode 100644 index 0000000..ea547ed --- /dev/null +++ b/db/sqlc/provisioners.sql.go @@ -0,0 +1,170 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.21.0 +// source: provisioners.sql + +package db + +import ( + "context" + "database/sql" + "time" + + "github.com/google/uuid" + "github.com/lib/pq" +) + +const createProvisionerAccount = `-- name: CreateProvisionerAccount :one +INSERT INTO provisioners ( + client_id, + api_token, + provisioner_account, + environments, + team, + email, + regular_expression, + node_attestation, + valid_subject_alternate_names, + extended_keys, + max_certificate_validity, + created_at, + created_by +) VALUES ( + $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13 +) RETURNING client_id, api_token, provisioner_account, environments, team, email, regular_expression, valid_subject_alternate_names, extended_keys, max_certificate_validity, node_attestation, created_at, created_by +` + +type CreateProvisionerAccountParams struct { + ClientID uuid.UUID `json:"client_id"` + ApiToken string `json:"api_token"` + ProvisionerAccount string `json:"provisioner_account"` + Environments []string `json:"environments"` + Team string `json:"team"` + Email string `json:"email"` + RegularExpression sql.NullString `json:"regular_expression"` + NodeAttestation []string `json:"node_attestation"` + ValidSubjectAlternateNames []string `json:"valid_subject_alternate_names"` + ExtendedKeys []string `json:"extended_keys"` + MaxCertificateValidity int16 `json:"max_certificate_validity"` + CreatedAt time.Time `json:"created_at"` + CreatedBy uuid.UUID `json:"created_by"` +} + +func (q *Queries) CreateProvisionerAccount(ctx context.Context, arg CreateProvisionerAccountParams) (*Provisioner, error) { + row := q.db.QueryRowContext(ctx, createProvisionerAccount, + arg.ClientID, + arg.ApiToken, + arg.ProvisionerAccount, + pq.Array(arg.Environments), + arg.Team, + arg.Email, + arg.RegularExpression, + pq.Array(arg.NodeAttestation), + pq.Array(arg.ValidSubjectAlternateNames), + pq.Array(arg.ExtendedKeys), + arg.MaxCertificateValidity, + arg.CreatedAt, + arg.CreatedBy, + ) + var i Provisioner + err := row.Scan( + &i.ClientID, + &i.ApiToken, + &i.ProvisionerAccount, + pq.Array(&i.Environments), + &i.Team, + &i.Email, + &i.RegularExpression, + pq.Array(&i.ValidSubjectAlternateNames), + pq.Array(&i.ExtendedKeys), + &i.MaxCertificateValidity, + pq.Array(&i.NodeAttestation), + &i.CreatedAt, + &i.CreatedBy, + ) + return &i, err +} + +const deleteProvisionerAccount = `-- name: DeleteProvisionerAccount :exec +DELETE FROM provisioners +WHERE client_id = $1 +` + +func (q *Queries) DeleteProvisionerAccount(ctx context.Context, clientID uuid.UUID) error { + _, err := q.db.ExecContext(ctx, deleteProvisionerAccount, clientID) + return err +} + +const getProvisionerUUID = `-- name: GetProvisionerUUID :one +SELECT client_id, api_token, provisioner_account, environments, team, email, regular_expression, valid_subject_alternate_names, extended_keys, max_certificate_validity, node_attestation, created_at, created_by FROM provisioners +WHERE client_id = $1 +` + +func (q *Queries) GetProvisionerUUID(ctx context.Context, clientID uuid.UUID) (*Provisioner, error) { + row := q.db.QueryRowContext(ctx, getProvisionerUUID, clientID) + var i Provisioner + err := row.Scan( + &i.ClientID, + &i.ApiToken, + &i.ProvisionerAccount, + pq.Array(&i.Environments), + &i.Team, + &i.Email, + &i.RegularExpression, + pq.Array(&i.ValidSubjectAlternateNames), + pq.Array(&i.ExtendedKeys), + &i.MaxCertificateValidity, + pq.Array(&i.NodeAttestation), + &i.CreatedAt, + &i.CreatedBy, + ) + return &i, err +} + +const listProvisionerAccounts = `-- name: ListProvisionerAccounts :many +SELECT client_id, api_token, provisioner_account, environments, team, email, regular_expression, valid_subject_alternate_names, extended_keys, max_certificate_validity, node_attestation, created_at, created_by FROM provisioners +LIMIT $1 +OFFSET $2 +` + +type ListProvisionerAccountsParams struct { + Limit int32 `json:"limit"` + Offset int32 `json:"offset"` +} + +func (q *Queries) ListProvisionerAccounts(ctx context.Context, arg ListProvisionerAccountsParams) ([]*Provisioner, error) { + rows, err := q.db.QueryContext(ctx, listProvisionerAccounts, arg.Limit, arg.Offset) + if err != nil { + return nil, err + } + defer rows.Close() + items := []*Provisioner{} + for rows.Next() { + var i Provisioner + if err := rows.Scan( + &i.ClientID, + &i.ApiToken, + &i.ProvisionerAccount, + pq.Array(&i.Environments), + &i.Team, + &i.Email, + &i.RegularExpression, + pq.Array(&i.ValidSubjectAlternateNames), + pq.Array(&i.ExtendedKeys), + &i.MaxCertificateValidity, + pq.Array(&i.NodeAttestation), + &i.CreatedAt, + &i.CreatedBy, + ); err != nil { + return nil, err + } + items = append(items, &i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/db/sqlc/querier.go b/db/sqlc/querier.go new file mode 100644 index 0000000..561f2bd --- /dev/null +++ b/db/sqlc/querier.go @@ -0,0 +1,45 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.21.0 + +package db + +import ( + "context" + + "github.com/google/uuid" +) + +type Querier interface { + CreateProvisionerAccount(ctx context.Context, arg CreateProvisionerAccountParams) (*Provisioner, error) + CreateServiceAccount(ctx context.Context, arg CreateServiceAccountParams) (*Account, error) + CreateUser(ctx context.Context, arg CreateUserParams) (*User, error) + DeleteInstanceIdentityDocument(ctx context.Context, clientID uuid.UUID) error + DeleteProvisionerAccount(ctx context.Context, clientID uuid.UUID) error + DeleteServiceAccount(ctx context.Context, clientID uuid.UUID) error + DeleteUser(ctx context.Context, username string) error + GetCertificate(ctx context.Context, serialNumber string) (*Certificate, error) + GetInstanceIdentityDocument(ctx context.Context, clientID uuid.UUID) (*AwsAttestation, error) + GetProvisionerUUID(ctx context.Context, clientID uuid.UUID) (*Provisioner, error) + GetServiceAccountByMetadata(ctx context.Context, arg GetServiceAccountByMetadataParams) ([]*Account, error) + GetServiceAccountBySAN(ctx context.Context, dollar_1 []string) ([]*Account, error) + GetServiceAccounts(ctx context.Context, serviceAccount string) ([]*Account, error) + GetServiceUUID(ctx context.Context, clientID uuid.UUID) (*Account, error) + GetSignedCertificateByMetadata(ctx context.Context, arg GetSignedCertificateByMetadataParams) ([]*Certificate, error) + GetUser(ctx context.Context, username string) (*User, error) + ListCertificateSubjectAlternativeName(ctx context.Context, arg ListCertificateSubjectAlternativeNameParams) ([]*Certificate, error) + ListCertificates(ctx context.Context, arg ListCertificatesParams) ([]*Certificate, error) + ListProvisionerAccounts(ctx context.Context, arg ListProvisionerAccountsParams) ([]*Provisioner, error) + ListServiceAccounts(ctx context.Context, arg ListServiceAccountsParams) ([]*Account, error) + ListUsers(ctx context.Context, arg ListUsersParams) ([]*User, error) + ListValidCertificateAuthorityFromSubordinateCA(ctx context.Context, arg ListValidCertificateAuthorityFromSubordinateCAParams) ([]interface{}, error) + LogCertificate(ctx context.Context, arg LogCertificateParams) (*Certificate, error) + RevokeIssuedCertificateSerialNumber(ctx context.Context, arg RevokeIssuedCertificateSerialNumberParams) error + StoreInstanceIdentityDocument(ctx context.Context, arg StoreInstanceIdentityDocumentParams) (*AwsAttestation, error) + UpdateInstanceIdentityNodeAttestor(ctx context.Context, arg UpdateInstanceIdentityNodeAttestorParams) (*Account, error) + UpdateServiceAccount(ctx context.Context, arg UpdateServiceAccountParams) (*Account, error) + UpdateUserAuthentication(ctx context.Context, arg UpdateUserAuthenticationParams) (*User, error) + UpdateUserPermission(ctx context.Context, arg UpdateUserPermissionParams) (*User, error) +} + +var _ Querier = (*Queries)(nil) diff --git a/db/sqlc/store.go b/db/sqlc/store.go new file mode 100644 index 0000000..d02ee4e --- /dev/null +++ b/db/sqlc/store.go @@ -0,0 +1,54 @@ +package db + +import ( + "context" + "database/sql" + "fmt" + + "github.com/coinbase/baseca/internal/types" + "github.com/google/uuid" +) + +type Store interface { + Querier + TxCreateServiceAccount(ctx context.Context, arg CreateServiceAccountParams, iid StoreInstanceIdentityDocumentParams) (*Account, error) + TxDeleteServiceAccount(ctx context.Context, client_id uuid.UUID) error + TxUpdateServiceAccount(ctx context.Context, arg Account, attestation types.NodeAttestation) (*Account, error) + TxCreateProvisionerAccount(ctx context.Context, arg CreateProvisionerAccountParams, iid StoreInstanceIdentityDocumentParams) (*Provisioner, error) + TxDeleteProvisionerAccount(ctx context.Context, client_id uuid.UUID) error +} +type SQLStore struct { + db *sql.DB + *Queries +} + +func BuildDatastore(db *sql.DB) Store { + return &SQLStore{ + db: db, + Queries: New(db), + } +} + +func BuildReadDatastore(db *sql.DB) Store { + return &SQLStore{ + db: db, + Queries: New(db), + } +} + +func (store *SQLStore) execTx(ctx context.Context, fn func(*Queries) error) error { + tx, err := store.db.BeginTx(ctx, nil) + if err != nil { + return err + } + + query := New(tx) + err = fn(query) + if err != nil { + if rollbackErr := tx.Rollback(); rollbackErr != nil { + return fmt.Errorf("tx error: %v, rollback error: %v", err, rollbackErr) + } + return err + } + return tx.Commit() +} diff --git a/db/sqlc/tx_provisioner_account.go b/db/sqlc/tx_provisioner_account.go new file mode 100644 index 0000000..8bcc741 --- /dev/null +++ b/db/sqlc/tx_provisioner_account.go @@ -0,0 +1,51 @@ +package db + +import ( + "context" + + "github.com/google/uuid" +) + +func (store *SQLStore) TxCreateProvisionerAccount(ctx context.Context, arg CreateProvisionerAccountParams, iid StoreInstanceIdentityDocumentParams) (*Provisioner, error) { + var provisionerAccountResponse *Provisioner + + err := store.execTx(ctx, func(q *Queries) error { + var err error + + provisionerAccountResponse, err = store.CreateProvisionerAccount(ctx, arg) + if err != nil { + return err + } + + for _, node_attestation := range arg.NodeAttestation { + switch node_attestation { + case "AWS_IID": + // Add to AWS_IID Database + _, err = store.StoreInstanceIdentityDocument(ctx, iid) + if err != nil { + return err + } + } + } + return nil + }) + return provisionerAccountResponse, err +} + +func (store *SQLStore) TxDeleteProvisionerAccount(ctx context.Context, client_id uuid.UUID) error { + err := store.execTx(ctx, func(q *Queries) error { + var err error + + err = store.DeleteInstanceIdentityDocument(ctx, client_id) + if err != nil { + return err + } + + err = store.DeleteProvisionerAccount(ctx, client_id) + if err != nil { + return err + } + return err + }) + return err +} diff --git a/db/sqlc/tx_service_account.go b/db/sqlc/tx_service_account.go new file mode 100644 index 0000000..e00515b --- /dev/null +++ b/db/sqlc/tx_service_account.go @@ -0,0 +1,51 @@ +package db + +import ( + "context" + + "github.com/google/uuid" +) + +func (store *SQLStore) TxCreateServiceAccount(ctx context.Context, arg CreateServiceAccountParams, iid StoreInstanceIdentityDocumentParams) (*Account, error) { + var serviceAccountResponse *Account + + err := store.execTx(ctx, func(q *Queries) error { + var err error + + serviceAccountResponse, err = store.CreateServiceAccount(ctx, arg) + if err != nil { + return err + } + + for _, node_attestation := range arg.NodeAttestation { + switch node_attestation { + case "AWS_IID": + // Add to AWS_IID Database + _, err = store.StoreInstanceIdentityDocument(ctx, iid) + if err != nil { + return err + } + } + } + return nil + }) + return serviceAccountResponse, err +} + +func (store *SQLStore) TxDeleteServiceAccount(ctx context.Context, client_id uuid.UUID) error { + err := store.execTx(ctx, func(q *Queries) error { + var err error + + err = store.DeleteInstanceIdentityDocument(ctx, client_id) + if err != nil { + return err + } + + err = store.DeleteServiceAccount(ctx, client_id) + if err != nil { + return err + } + return err + }) + return err +} diff --git a/db/sqlc/tx_update_account.go b/db/sqlc/tx_update_account.go new file mode 100644 index 0000000..63615fa --- /dev/null +++ b/db/sqlc/tx_update_account.go @@ -0,0 +1,66 @@ +package db + +import ( + "context" + "database/sql" + + "github.com/coinbase/baseca/internal/lib/util/validator" + "github.com/coinbase/baseca/internal/types" +) + +func (store *SQLStore) TxUpdateServiceAccount(ctx context.Context, arg Account, attestation types.NodeAttestation) (*Account, error) { + var serviceAccountResponse *Account + // TODO: Input Validation for Service Account + + updateServiceAccountInput := UpdateServiceAccountParams{ + ClientID: arg.ClientID, + Environment: arg.Environment, + Team: arg.Team, + Email: arg.Email, + RegularExpression: arg.RegularExpression, + ValidSubjectAlternateName: arg.ValidCertificateAuthorities, + ValidCertificateAuthorities: arg.ValidCertificateAuthorities, + ExtendedKey: arg.ExtendedKey, + CertificateValidity: arg.CertificateValidity, + SubordinateCa: arg.SubordinateCa, + NodeAttestation: arg.NodeAttestation, + } + + raw_message, err := validator.MapToNullRawMessage(attestation.AWSInstanceIdentityDocument.InstanceTags) + if err != nil { + return nil, err + } + + iid := StoreInstanceIdentityDocumentParams{ + ClientID: arg.ClientID, + RoleArn: sql.NullString{String: attestation.AWSInstanceIdentityDocument.RoleArn, Valid: len(attestation.AWSInstanceIdentityDocument.RoleArn) != 0}, + AssumeRole: sql.NullString{String: attestation.AWSInstanceIdentityDocument.AssumeRole, Valid: len(attestation.AWSInstanceIdentityDocument.AssumeRole) != 0}, + SecurityGroupID: attestation.AWSInstanceIdentityDocument.SecurityGroups, + Region: sql.NullString{String: attestation.AWSInstanceIdentityDocument.Region, Valid: len(attestation.AWSInstanceIdentityDocument.Region) != 0}, + InstanceID: sql.NullString{String: attestation.AWSInstanceIdentityDocument.InstanceID, Valid: len(attestation.AWSInstanceIdentityDocument.InstanceID) != 0}, + ImageID: sql.NullString{String: attestation.AWSInstanceIdentityDocument.ImageID, Valid: len(attestation.AWSInstanceIdentityDocument.ImageID) != 0}, + InstanceTags: raw_message, + } + + err = store.execTx(ctx, func(q *Queries) error { + var err error + + serviceAccountResponse, err = store.UpdateServiceAccount(ctx, updateServiceAccountInput) + if err != nil { + return err + } + + for _, node_attestation := range arg.NodeAttestation { + switch node_attestation { + case types.Attestation.AWS_IID: + // Add to AWS_IID Database + _, err = store.StoreInstanceIdentityDocument(ctx, iid) + if err != nil { + return err + } + } + } + return nil + }) + return serviceAccountResponse, err +} diff --git a/db/sqlc/users.sql.go b/db/sqlc/users.sql.go new file mode 100644 index 0000000..7ddcfb5 --- /dev/null +++ b/db/sqlc/users.sql.go @@ -0,0 +1,188 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.21.0 +// source: users.sql + +package db + +import ( + "context" + + "github.com/google/uuid" +) + +const createUser = `-- name: CreateUser :one +INSERT INTO users ( + uuid, + username, + hashed_credential, + full_name, + email, + permissions +) VALUES ( + $1, $2, $3, $4, $5, $6 +) RETURNING uuid, username, hashed_credential, full_name, email, permissions, credential_changed_at, created_at +` + +type CreateUserParams struct { + Uuid uuid.UUID `json:"uuid"` + Username string `json:"username"` + HashedCredential string `json:"hashed_credential"` + FullName string `json:"full_name"` + Email string `json:"email"` + Permissions string `json:"permissions"` +} + +func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (*User, error) { + row := q.db.QueryRowContext(ctx, createUser, + arg.Uuid, + arg.Username, + arg.HashedCredential, + arg.FullName, + arg.Email, + arg.Permissions, + ) + var i User + err := row.Scan( + &i.Uuid, + &i.Username, + &i.HashedCredential, + &i.FullName, + &i.Email, + &i.Permissions, + &i.CredentialChangedAt, + &i.CreatedAt, + ) + return &i, err +} + +const deleteUser = `-- name: DeleteUser :exec +DELETE FROM users +WHERE username = $1 +` + +func (q *Queries) DeleteUser(ctx context.Context, username string) error { + _, err := q.db.ExecContext(ctx, deleteUser, username) + return err +} + +const getUser = `-- name: GetUser :one +SELECT uuid, username, hashed_credential, full_name, email, permissions, credential_changed_at, created_at FROM users +WHERE username = $1 LIMIT 1 +` + +func (q *Queries) GetUser(ctx context.Context, username string) (*User, error) { + row := q.db.QueryRowContext(ctx, getUser, username) + var i User + err := row.Scan( + &i.Uuid, + &i.Username, + &i.HashedCredential, + &i.FullName, + &i.Email, + &i.Permissions, + &i.CredentialChangedAt, + &i.CreatedAt, + ) + return &i, err +} + +const listUsers = `-- name: ListUsers :many +SELECT uuid, username, hashed_credential, full_name, email, permissions, credential_changed_at, created_at FROM users +ORDER BY username +LIMIT $1 +OFFSET $2 +` + +type ListUsersParams struct { + Limit int32 `json:"limit"` + Offset int32 `json:"offset"` +} + +func (q *Queries) ListUsers(ctx context.Context, arg ListUsersParams) ([]*User, error) { + rows, err := q.db.QueryContext(ctx, listUsers, arg.Limit, arg.Offset) + if err != nil { + return nil, err + } + defer rows.Close() + items := []*User{} + for rows.Next() { + var i User + if err := rows.Scan( + &i.Uuid, + &i.Username, + &i.HashedCredential, + &i.FullName, + &i.Email, + &i.Permissions, + &i.CredentialChangedAt, + &i.CreatedAt, + ); err != nil { + return nil, err + } + items = append(items, &i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const updateUserAuthentication = `-- name: UpdateUserAuthentication :one +UPDATE users +SET hashed_credential = $2, credential_changed_at = now() +WHERE username = $1 +RETURNING uuid, username, hashed_credential, full_name, email, permissions, credential_changed_at, created_at +` + +type UpdateUserAuthenticationParams struct { + Username string `json:"username"` + HashedCredential string `json:"hashed_credential"` +} + +func (q *Queries) UpdateUserAuthentication(ctx context.Context, arg UpdateUserAuthenticationParams) (*User, error) { + row := q.db.QueryRowContext(ctx, updateUserAuthentication, arg.Username, arg.HashedCredential) + var i User + err := row.Scan( + &i.Uuid, + &i.Username, + &i.HashedCredential, + &i.FullName, + &i.Email, + &i.Permissions, + &i.CredentialChangedAt, + &i.CreatedAt, + ) + return &i, err +} + +const updateUserPermission = `-- name: UpdateUserPermission :one +UPDATE users +SET permissions = $2 +WHERE username = $1 +RETURNING uuid, username, hashed_credential, full_name, email, permissions, credential_changed_at, created_at +` + +type UpdateUserPermissionParams struct { + Username string `json:"username"` + Permissions string `json:"permissions"` +} + +func (q *Queries) UpdateUserPermission(ctx context.Context, arg UpdateUserPermissionParams) (*User, error) { + row := q.db.QueryRowContext(ctx, updateUserPermission, arg.Username, arg.Permissions) + var i User + err := row.Scan( + &i.Uuid, + &i.Username, + &i.HashedCredential, + &i.FullName, + &i.Email, + &i.Permissions, + &i.CredentialChangedAt, + &i.CreatedAt, + ) + return &i, err +} diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..efb1b8d --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,34 @@ +# `baseca` Architecture + +At the core, `baseca` is a control plane used to issue certificates for service accounts within a granted scope. Instead of issuing [`end-entity certificates`](https://docs.aws.amazon.com/privateca/latest/userguide/UsingTemplates.html#EndEntityServerAuthCertificate-V1) directly from AWS, baseca issues [`subordinate certificate authorities`](https://docs.aws.amazon.com/privateca/latest/userguide/UsingTemplates.html#SubordinateCACertificate_PathLen0-V1) from upstream Root Certificate or Intermediate Certificate Authorities from Private CA. When certificate signing requests (CSR) are sent to `baseca`, the service authenticates and validates based off the scope of the service account and signs the CSR in memory within the control plane. + + + +Prior to client authentication, a service account must be provisioned for baseca. For detailed information on service accounts and their associated scopes, please refer to the [`SCOPE.md`](SCOPE.md) document. + +Upon receiving an RPC request, baseca validates the client's credentials against the stored service account within RDS. Successful authentication triggers a Node Attestation process which verifies that the request originates from the designated instance. If the request signature fails to match the scope outlined in the service account, authentication will be denied. + +Post-authentication, the Certificate Signing Request (CSR) attached to the request is verified, and an X.509 certificate is issued corresponding to the scopes of the service account. During the certificate signing process, baseca checks the `/tmp/baseca/ssl` directory for an existing Subordinate Certificate Authority (CA). If none exists, baseca communicates with AWS Private CA using the `SubordinateCACertificate_PathLen0/V1` template to generate a Subordinate CA. This Subordinate CA is then stored in memory and utilized for signing incoming requests that share the same `subordinate_ca` scope. + + +```sh +# Example of baseca Host Filesystem + +/tmp +└── baseca + └── ssl + ├── subordinate_a_development + │ ├── acm-ca.txt + │ ├── ca-intermediate.crt + │ ├── ca-root.crt + │ ├── ca-subordinate.crt + │ ├── ca-subordinate.key + │ └── serial.txt + ├── subordinate_a_production + ├── subordinate_b_development + └── subordinate_b_production +``` + +Because baseca performs signing, AWS Private CA does not have access to data about issued end-entity certificates as it only is able to track issued Subordinate CAs. However, it is essential to maintain a link between each Subordinate CA and the respective end-entity certificates. To achieve this, baseca streams this data via Kinesis Firehose to S3 after every successful request, thereby maintaining a record. + + diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md new file mode 100644 index 0000000..4b6979a --- /dev/null +++ b/docs/CONFIGURATION.md @@ -0,0 +1,113 @@ +# `baseca` Configuration File + +## Environment Variables + +Configurations are held the [`baseca/config`](../config/). The structure for the configuration files are `config.primary.CONFIGURATION.ENVIRONMENT.yml`. + +- If `ENVIRONMENT` is set to anything the value will be `aws`, if it is not set it will be `sandbox`. +- If `CONFIGURATION` is set, that same value will reflect within the configuration file. + +If `baseca` is run with the following environment variables then during start time it will look for the `config.primary.infrastructure-production.aws.yml` configuration file. + +```sh +export ENVIRONMENT=production +export CONFIGURATION=infrastructure-production +``` + +## Configuration File Parameters + +```yml +grpc_server_address: 0.0.0.0:9090 # baseca gRPC Server Port + +ocsp_server: # Optional + - production.ocsp.example.com # Custom OCSP Server URL + +database: + database_driver: postgres # Do Not Modify + database_table: baseca # Database Table Name + database_endpoint: xxxxxx.cluster.xxxxxx.us-east-1.rds.amazonaws.com # Database Writer Endpoint + database_reader_endpoint: xxxxxx.cluster-ro.xxxxxx.us-east-1.rds.amazonaws.com # Database Reader Endpoint + database_user: root # Database User + database_port: 5432 # Database Port + region: us-east-1 # RDS Region + ssl_mode: verify-full # RDS SSL Mode + +redis: + cluster_endpoint: xxxxxx.xxxxxx.0001.use1.cache.amazonaws.com # Redis Endpoint + port: 6379 # Redis Port + rate_limit: 20 # 5-minute Sliding Window Rate Limit per Subject Alternative Name (SAN) + +domains: + - example.com # Domains baseca Can Issue Certificates + +acm_pca: + # Alias for Certificate Authority (production_use1) + production_use1: + region: us-east-1 # Region Private CA is Deployed In + ca_arn: arn:aws:acm-pca:us-east-1:123456789012:certificate-authority/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx # Private CA ARN + ca_active_day: 90 # Days Subordinate CA Issued from baseca is Active + assume_role: false # baseca Supports Cross-Account Access to Private CA; if baseca is deployed in a different account than the Private CA set this to true. + root_ca: false # Root CA or Subordinate CA + + # Alias for Certificate Authority (production_usw1) + production_usw1: + region: us-west-1 + ca_arn: arn:aws:acm-pca:us-west-1:123456789012:certificate-authority/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + ca_active_day: 180 + assume_role: false + root_ca: false + + # Alias for Certificate Authority (development_use1) + development_use1: + region: us-east-1 + ca_arn: arn:aws:acm-pca:us-east-1:987654321098:certificate-authority/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + ca_active_day: 180 + assume_role: true + role_arn: arn:aws:iam::987654321098:role/[ROLE] + root_ca: false + +firehose: + stream: baseca-production # Kinesis Firehose Stream Name + region: us-east-1 # Kinesis Firehose Region + +kms: + key_id: 12345678-1234-1234-1234-123456789012 # KMS Key ID for Signing and Validating Requests + signing_algorithm: RSASSA_PSS_SHA_512 # [RSASSA_PKCS1_V1_5_SHA_256, RSASSA_PKCS1_V1_5_SHA_384, RSASSA_PKCS1_V1_5_SHA_512, RSASSA_PSS_SHA_256, RSASSA_PSS_SHA_384, RSASSA_PSS_SHA_512] + region: us-east-1 # KMS Region + auth_validity: 5 # User Authentication Token Validity (Minutes) + +secrets_manager: + secret_id: baseca-xxxxxxxxxxxx # AWS Secrets Manager ID + +subordinate_ca_metadata: + # baseca Subordinate CA Metadata + country: "US" + province: "CA" + locality: "San Francisco" + organization: "Example" + organization_unit: "Security" + email: "example@example.com" + + # [RSA, ECDSA] + key_algorithm: "RSA" + + # RSA [SHA256WITHRSA, SHA384WITHRSA, SHA512WITHRSA] + # ECDSA [SHA256WITHECDSA, SHA384WITHECDSA, SHA512WITHECDSA] + signing_algorithm: SHA512WITHRSA + + # RSA [2048, 4096] + # ECDSA [256, 384, 521] + key_size: 4096 + +certificate_authority: # CA Environments: [local, sandbox, development, staging, pre_production, production, corporate]. Each value within the environment maps to the CA configured within acm_pca. + + # List of Development Certificate Authority Alias + development: + - development_use1 + + # List of Production Certificate Authority Alias + production: + - production_use1 + - production_usw1 +``` + diff --git a/docs/ENDPOINTS.md b/docs/ENDPOINTS.md new file mode 100644 index 0000000..7fb008c --- /dev/null +++ b/docs/ENDPOINTS.md @@ -0,0 +1,1609 @@ +# baseca RPC Methods + +[`Certificate Endpoints`](#Certificate) + +```protobuf +service Certificate { + rpc SignCSR (CertificateSigningRequest) returns (SignedCertificate); + rpc GetCertificate (CertificateSerialNumber) returns (CertificateParameter); + rpc ListCertificates (ListCertificatesRequest) returns (CertificatesParameter); + rpc RevokeCertificate (RevokeCertificateRequest) returns (RevokeCertificateResponse); + rpc OperationsSignCSR (OperationsSignRequest) returns (SignedCertificate); + rpc QueryCertificateMetadata (QueryCertificateMetadataRequest) returns (CertificatesParameter); +} +``` + +[`Service Endpoints`](#Service) + +```protobuf +service Service { + rpc CreateServiceAccount (CreateServiceAccountRequest) returns (CreateServiceAccountResponse); + rpc CreateProvisionerAccount(CreateProvisionerAccountRequest) returns (CreateProvisionerAccountResponse); + rpc GetProvisionerAccount (AccountId) returns (ProvisionerAccount); + rpc GetServiceAccount (AccountId) returns (ServiceAccount); + rpc GetServiceAccountMetadata (GetServiceAccountMetadataRequest) returns (ServiceAccounts); + rpc DeleteServiceAccount (AccountId) returns (google.protobuf.Empty); + rpc DeleteProvisionedServiceAccount (AccountId) returns (google.protobuf.Empty); + rpc ProvisionServiceAccount (ProvisionServiceAccountRequest) returns (ProvisionServiceAccountResponse); + rpc ListServiceAccounts (QueryParameter) returns (ServiceAccounts); + rpc ListProvisionerAccounts (QueryParameter) returns (ProvisionerAccounts); +} +``` + +[`Account Endpoints`](#Account) + +```protobuf +service Account { + rpc LoginUser (LoginUserRequest) returns (LoginUserResponse); + rpc DeleteUser (UsernameRequest) returns (google.protobuf.Empty); + rpc GetUser (UsernameRequest) returns (User); + rpc ListUsers (QueryParameter) returns (Users); + rpc CreateUser (CreateUserRequest) returns (User); + rpc UpdateUserCredentials (UpdateCredentialsRequest) returns (User); + rpc UpdateUserPermissions (UpdatePermissionsRequest) returns (User); +} +``` + +## Certificate + +### **baseca.v1.Certificate/SignCSR** + +```protobuf +rpc SignCSR (CertificateSigningRequest) returns (SignedCertificate); +``` + +**Description** +Sign Certificate Signing Request (CSR) + +**Authentication** +Service Account Authentication + +**Client Example** +[sign_csr.go](../examples/certificate/baseca.v1.Certificate/sign_csr.go) + +**Request** + +```protobuf +message CertificateSigningRequest { + string certificate_signing_request = 1; +} +``` + +**Response** + +```protobuf +message SignedCertificate { + string certificate = 1; + string intermediate_certificate_chain = 2; + string root_certificate_chain = 3; + CertificateParameter metadata = 4; +} + +message CertificateParameter { + string serial_number = 1; + string common_name = 2; + repeated string subject_alternative_name = 3; + google.protobuf.Timestamp expiration_date = 4; + google.protobuf.Timestamp issued_date = 5; + bool revoked = 6; + string revoked_by = 7; + google.protobuf.Timestamp revoke_date = 8; + string ca_serial_number = 9; + string certificate_authority_arn = 10; +} +``` + +```json +{ + "certificate": "-----BEGIN CERTIFICATE-----", + "intermediate_certificate_chain": "-----BEGIN CERTIFICATE-----", + "root_certificate_chain": "-----BEGIN CERTIFICATE-----", + "metadata": { + "serial_number": "4c15b9ec07c2779f5319ab68ea16b7fc5f452f36", + "common_name": "sandbox.example.com", + "subject_alternative_name": ["test.example.com"], + "expiration_date": { + "seconds": 1685128356, + "nanos": 876043203 + }, + "issued_date": { + "seconds": 1682536356, + "nanos": 876043115 + }, + "revoke_date": { + "seconds": -62135596800 + }, + "ca_serial_number": "50919db8e90a157cb5d44f8b1eb87879", + "certificate_authority_arn": "arn:aws:acm-pca:us-east-1:123456789012:certificate-authority/11111111-22222-3333-4444-555555555555" + } +} +``` + +### **baseca.v1.Certificate/QueryCertificateMetadata** + +```protobuf +rpc QueryCertificateMetadata (QueryCertificateMetadataRequest) returns (CertificatesParameter); +``` + +**Description** +Query for Non-Ephemeral Certificate Issued from Provisioner, Certificates Issued from `baseca.v1.Certificate/SignCSR` are Excluded + +**Authentication** +Provisioner Account Authentication + +**Request** + +```protobuf +message QueryMetadataRequest { + string serial_number = 1; + string account = 2; + string environment = 3; + string extended_key = 4; + repeated string subject_alternative_name = 5; +} +``` + +**Response** + +```protobuf +message CertificatesParameter { + repeated CertificateParameter certificates = 1; +} + +message CertificateAuthorityParameter { + string region = 1; + string ca_arn = 2; + string sign_algorithm = 3; + bool assume_role = 4; + string role_arn = 5; + int32 validity = 6; +} +``` + +### **baseca.v1.Certificate/GetCertificate** + +```protobuf +rpc GetCertificate (CertificateSerialNumber) returns (CertificateParameter); +``` + +**Description** +Query Issued Certificate Metadata from Database + +**Authentication** +User Authentication + +**Request** + +```protobuf +message CertificateSerialNumber { + string serial_number = 1; +} +``` + +```bash +grpcurl -vv -plaintext -H "Authorization: Bearer ${AUTH_TOKEN}" \ + -d '{ + "serial_number": "4c15b9ec07c2779f5319ab68ea16b7fc5f452f36" + }' \ + localhost:9090 baseca.v1.Certificate/GetCertificate +``` + +**Response** + +```protobuf +message CertificateParameter { + string serial_number = 1; + string common_name = 2; + repeated string subject_alternative_name = 3; + google.protobuf.Timestamp expiration_date = 4; + google.protobuf.Timestamp issued_date = 5; + bool revoked = 6; + string revoked_by = 7; + google.protobuf.Timestamp revoke_date = 8; + string ca_serial_number = 9; + string certificate_authority_arn = 10; +} +``` + +```json +{ + "metadata": { + "serial_number": "4c15b9ec07c2779f5319ab68ea16b7fc5f452f36", + "common_name": "sandbox.example.com", + "subject_alternative_name": ["test.example.com"], + "expiration_date": "2023-09-25T03:30:11Z", + "issued_date": "2023-08-25T03:30:11Z", + "revoke_date": "0001-01-01T00:00:00Z", + "ca_serial_number": "50919db8e90a157cb5d44f8b1eb87879", + "certificate_authority_arn": "arn:aws:acm-pca:us-east-1:123456789012:certificate-authority/11111111-22222-3333-4444-555555555555" + } +} +``` + +### **baseca.v1.Certificate/ListCertificates** + +```protobuf +rpc ListCertificates (ListCertificatesRequest) returns (CertificatesParameter); +``` + +**Description** +List Issued Certificates via Common Name (CN) + +**Authentication** +User Authentication + +**Request** + +```protobuf +message ListCertificatesRequest { + string common_name = 1; + int32 page_id = 3; + int32 page_size = 4; +} +``` + +```sh +grpcurl -vv -plaintext -H "Authorization: Bearer ${AUTH_TOKEN}" \ + -d '{ + "common_name": "sandbox.example.com", + "page_id": 1, + "page_size": 20 + }' \ + localhost:9090 baseca.v1.Certificate/ListCertificates +``` + +**Response** + +```protobuf +message CertificatesParameter { + repeated CertificateParameter certificates = 1; +} +message CertificateAuthorityParameter { + string region = 1; + string ca_arn = 2; + string sign_algorithm = 3; + bool assume_role = 4; + string role_arn = 5; + int32 validity = 6; +} +``` + +### **baseca.v1.Certificate/RevokeCertificate** + +```protobuf +rpc RevokeCertificate (RevokeCertificateRequest) returns (RevokeCertificateResponse); +``` + +**Description** +Revoke Subordinate CA Certificate from ACM Private CA + +**Authentication** +User Authentication + +**Request** + +```protobuf +message RevokeCertificateRequest { + string serial_number = 1; + string revocation_reason = 2; +} +``` + +``` +AFFILIATION_CHANGED +CESSATION_OF_OPERATION +A_A_COMPROMISE +PRIVILEGE_WITHDRAWN +SUPERSEDED +UNSPECIFIED +KEY_COMPROMISE +CERTIFICATE_AUTHORITY_COMPROMISE +``` + +```sh +grpcurl -vv -plaintext -H "Authorization: Bearer ${AUTH_TOKEN}" \ + -d '{ + "serial_number": "50919db8e90a157cb5d44f8b1eb87879", + "revocation_reason": "PRIVILEGE_WITHDRAWN" + }' \ + localhost:9090 baseca.v1.Certificate/RevokeCertificate +``` + +**Response** + +```protobuf +message RevokeCertificateResponse { + string serial_number = 1; + google.protobuf.Timestamp revocation_date = 2; + string status = 3; +} +``` + +```json +{ + "serialNumber": "50919db8e90a157cb5d44f8b1eb87879", + "revocationDate": "2023-04-26T21:49:59.846588Z", + "status": "PRIVILEGE_WITHDRAWN" +} +``` + +### **baseca.v1.Certificate/OperationsSignCSR** + +```protobuf +rpc OperationsSignCSR (OperationsSignRequest) returns (SignedCertificate); +``` + +**Description** +Manual Sign Certificate Signing Request (CSR) + +**Authentication** +Provisioner Account Authentication + +**Client Example** +[operations_sign_csr.go](../examples/certificate/baseca.v1.Certificate/operations_sign_csr.go) + +**Request** + +```protobuf +message OperationsSignRequest { + string certificate_signing_request = 1; + optional CertificateAuthorityParameter certificate_authority = 2; + string service_account = 3; + string environment = 4; + string extended_key = 5; +} +``` + +**Response** + +```protobuf +message SignedCertificate { + string certificate = 1; + string intermediate_certificate_chain = 2; + string root_certificate_chain = 3; + CertificateParameter metadata = 4; +} + +message CertificateParameter { + string serial_number = 1; + string common_name = 2; + repeated string subject_alternative_name = 3; + google.protobuf.Timestamp expiration_date = 4; + google.protobuf.Timestamp issued_date = 5; + bool revoked = 6; + string revoked_by = 7; + google.protobuf.Timestamp revoke_date = 8; + string certificate_authority_arn = 9; + string account = 10; + string environment = 11; + string extended_key = 12; +``` + +## Service + +### **baseca.v1.Service/CreateServiceAccount** + +```protobuf +rpc CreateServiceAccount (CreateServiceAccountRequest) returns (CreateServiceAccountResponse); +``` + +**Description** +Create Service Account Record + +**Authentication** +User Authentication + +**Request** + +```protobuf +message CreateServiceAccountRequest { + string service_account = 1; + string environment = 2; + optional string regular_expression = 3; + repeated string subject_alternative_names = 4; + repeated string certificate_authorities = 5; + string extended_key = 6; + int32 certificate_validity = 7; + string subordinate_ca = 8; + optional NodeAttestation node_attestation = 9; + string team = 10; + string email = 11; + +} + +message NodeAttestation { + AWSInstanceIdentityDocument aws_iid = 1; +} + +message AWSInstanceIdentityDocument { + string role_arn = 1; + string assume_role = 2; + repeated string security_groups = 3; + string region = 4; + string instance_id = 5; + string image_id = 6; + map instance_tags = 7; +} +``` + +```sh +grpcurl -vv -plaintext -H "Authorization: Bearer ${AUTH_TOKEN}" \ + -d '{ + "service_account": "example", + "environment": "development", + "subject_alternative_names": [ + "development.coinbase.com" + ], + "extended_key": "EndEntityServerAuthCertificate", + "node_attestation": { + "aws_iid": { + "role_arn": "arn:aws:iam::123456789012:instance-profile/instance-profile-name", + "assume_role": "arn:aws:iam::123456789012:role/assumed-role", + "security_groups": ["sg-0123456789abcdef0"], + "region": "us-east-1" + } + }, + "certificate_authorities": [ + "development_use1" + ], + "certificate_validity": 30, + "subordinate_ca": "infrastructure", + "team": "Infrastructure Security", + "email": "security@coinbase.com" + }' \ + localhost:9090 baseca.v1.Service/CreateServiceAccount +``` + +**Response** + +```protobuf +message CreateServiceAccountResponse { + string client_id = 1; + string client_token = 2; + string service_account = 3; + string environment = 4; + string regular_expression = 5; + repeated string subject_alternative_names = 6; + repeated string certificate_authorities = 7; + string extended_key = 8; + NodeAttestation node_attestation = 9; + int32 certificate_validity = 10; + string subordinate_ca = 11; + string team = 12; + string email = 13; + google.protobuf.Timestamp created_at = 14; + string created_by = 15; +} +``` + +```json +{ + "clientId": "585c2f84-9a0e-4775-827a-a0a99c7dddcc", + "clientToken": "[CLIENT_TOKEN]", + "serviceAccount": "example", + "environment": "development", + "subjectAlternativeNames": ["development.coinbase.com"], + "certificateAuthorities": ["development_use1"], + "extendedKey": "EndEntityServerAuthCertificate", + "nodeAttestation": { + "awsIid": { + "roleArn": "arn:aws:iam::123456789012:instance-profile/instance-profile-name", + "assumeRole": "arn:aws:iam::123456789012:role/assumed-role", + "securityGroups": ["sg-0123456789abcdef0"], + "region": "us-east-1" + } + }, + "certificateValidity": 30, + "subordinateCa": "infrastructure", + "team": "Infrastructure Security", + "email": "security@coinbase.com", + "createdAt": "2023-04-26T22:24:42.873116Z", + "createdBy": "830b9b81-37c0-4180-9dba-9f21b1f6ae21" +} +``` + +### **baseca.v1.Service/ListServiceAccounts** + +```protobuf +rpc ListServiceAccounts (QueryParameter) returns (ServiceAccounts); +``` + +**Description** +Query Service Accounts from Database + +**Authentication** +User Authentication + +**Request** + +```protobuf +message QueryParameter { + int32 page_id = 2; + int32 page_size = 3; +} +``` + +```sh + grpcurl -vv -plaintext -H "Authorization: Bearer ${AUTH_TOKEN}" \ + -d '{ + "page_id": 1, + "page_size": 20 + }' \ + localhost:9090 baseca.v1.Service/ListServiceAccounts +``` + +**Response** + +```protobuf +message ServiceAccounts { + repeated ServiceAccount service_accounts = 1; +} + +message ServiceAccount { + string client_id = 1; + string service_account = 2; + string environment = 3; + string regular_expression = 4; + repeated string subject_alternative_names = 5; + repeated string certificate_authorities = 6; + string extended_key = 7; + NodeAttestation node_attestation = 8; + int32 certificate_validity = 9; + string subordinate_ca = 10; + bool provisioned = 11; + string team = 12; + string email = 13; + google.protobuf.Timestamp created_at = 14; + string created_by = 15; +} +``` + +```json +{ + "serviceAccounts": [ + { + "clientId": "585c2f84-9a0e-4775-827a-a0a99c7dddcc", + "serviceAccount": "example", + "environment": "development", + "subjectAlternativeNames": ["development.coinbase.com"], + "certificateAuthorities": ["development_use1"], + "extendedKey": "EndEntityServerAuthCertificate", + "nodeAttestation": { + "awsIid": { + "roleArn": "arn:aws:iam::123456789012:instance-profile/instance-profile-name", + "assumeRole": "arn:aws:iam::123456789012:role/assumed-role", + "securityGroups": ["sg-0123456789abcdef0"], + "region": "us-east-1" + } + }, + "certificateValidity": 30, + "subordinate_ca": "infrastructure", + "provisioned": false, + "team": "Infrastructure Security", + "email": "security@coinbase.com", + "createdAt": "2023-04-26T22:24:42.873116Z", + "createdBy": "830b9b81-37c0-4180-9dba-9f21b1f6ae21" + } + ] +} +``` + +### **baseca.v1.Service/ListProvisionerAccounts** + +```protobuf +rpc ListProvisionerAccounts (QueryParameter) returns (ProvisionerAccounts); +``` + +**Description** +List Provisioner Service Accounts + +**Request** + +```protobuf +message QueryParameter { + int32 page_id = 2; + int32 page_size = 3; +} +``` + +```sh +grpcurl -vv -plaintext -H "Authorization: Bearer ${AUTH_TOKEN}" \ + -d '{ + "page_id": 1, + "page_size": 25 + }' \ + localhost:9090 baseca.v1.Service/ListProvisionerAccounts +``` + +**Response** + +```protobuf +message ProvisionerAccounts { + repeated ProvisionerAccount provisioner_accounts = 1; +} + +message ProvisionerAccount { + string client_id = 1; + string provisioner_account = 2; + repeated string environments = 3; + string regular_expression = 4; + repeated string subject_alternative_names = 5; + repeated string extended_keys = 7; + NodeAttestation node_attestation = 8; + uint32 max_certificate_validity = 9; + string team = 10; + string email = 11; + google.protobuf.Timestamp created_at = 12; + string created_by = 13; +} + +message NodeAttestation { + AWSInstanceIdentityDocument aws_iid = 1; +} + +message AWSInstanceIdentityDocument { + string role_arn = 1; + string assume_role = 2; + repeated string security_groups = 3; + string region = 4; + string instance_id = 5; + string image_id = 6; + map instance_tags = 7; +} +``` + +```json +{ + "provisionerAccounts": [ + { + "clientId": "650b0ead-1c6b-49b3-810e-48d20a2890ac", + "provisionerAccount": "example", + "environments": ["development", "sandbox"], + "regularExpression": "[A-Za-z0-9.-]", + "subjectAlternativeNames": ["test.coinbase.com"], + "extendedKeys": ["EndEntityServerAuthCertificate"], + "nodeAttestation": { + "awsIid": { + "roleArn": "arn:aws:iam::123456789012:role/role", + "assumeRole": "arn:aws:iam::123456789012:role/assumed-role", + "securityGroups": ["sg-0123456789abcdef0"], + "region": "us-east-1" + } + }, + "maxCertificateValidity": 30, + "team": "Security", + "email": "example@coinbase.com", + "createdAt": "2023-04-26T22:24:42.873116Z", + "createdBy": "830b9b81-37c0-4180-9dba-9f21b1f6ae21" + } + ] +} +``` + +### **baseca.v1.Service/GetServiceAccountMetadata** + +```protobuf +rpc GetServiceAccountMetadata (GetServiceAccountMetadataRequest) returns (ServiceAccounts); +``` + +**Description** +Query Service Account from Database via Metadata + +**Authentication** +User Authentication + +**Request** + +```protobuf +message GetServiceAccountMetadataRequest{ + string service_account = 1; + string environment = 2; + string extended_key = 3; +} +``` + +```sh +grpcurl -vv -plaintext -H "Authorization: Bearer ${AUTH_TOKEN}" \ + -d '{ + "service_account": "example", + "environment": "development", + "extended_key": "EndEntityServerAuthCertificate" + }' \ + localhost:9090 baseca.v1.Service/GetServiceAccountMetadata +``` + +**Response** + +```protobuf +message ServiceAccounts { + repeated ServiceAccount service_accounts = 1; +} + +message ServiceAccount { + string client_id = 1; + string service_account = 2; + string environment = 3; + string regular_expression = 4; + repeated string subject_alternative_names = 5; + repeated string certificate_authorities = 6; + string extended_key = 7; + NodeAttestation node_attestation = 8; + int32 certificate_validity = 9; + string subordinate_ca = 10; + bool provisioned = 11; + string team = 12; + string email = 13; + google.protobuf.Timestamp created_at = 14; + string created_by = 15; +} +``` + +```json +{ + "serviceAccounts": [ + { + "clientId": "585c2f84-9a0e-4775-827a-a0a99c7dddcc", + "serviceAccount": "example", + "environment": "development", + "subjectAlternativeNames": ["development.coinbase.com"], + "certificateAuthorities": ["development_use1"], + "extendedKey": "EndEntityServerAuthCertificate", + "nodeAttestation": { + "awsIid": { + "roleArn": "arn:aws:iam::123456789012:instance-profile/instance-profile-name", + "assumeRole": "arn:aws:iam::123456789012:role/assumed-role", + "securityGroups": ["sg-0123456789abcdef0"], + "region": "us-east-1" + } + }, + "certificateValidity": 30, + "subordinateCa": "infrastructure", + "provisioned": false, + "team": "Infrastructure Security", + "email": "security@coinbase.com", + "createdAt": "2023-04-26T22:24:42.873116Z", + "createdBy": "830b9b81-37c0-4180-9dba-9f21b1f6ae21" + } + ] +} +``` + +### **baseca.v1.Service/GetServiceAccount** + +```protobuf +rpc GetServiceAccount (AccountId) returns (ServiceAccount); +``` + +**Description** +Query Service Account from Database By UUID + +**Authentication** +User Authentication + +**Request** + +```protobuf +message AccountId { + string uuid = 1; +} +``` + +```sh +grpcurl -vv -plaintext -H "Authorization: Bearer ${AUTH_TOKEN}" \ + -d '{ + "uuid": "585c2f84-9a0e-4775-827a-a0a99c7dddcc" + }' \ + localhost:9090 baseca.v1.Service/GetServiceAccount +``` + +**Response** + +```protobuf +message ServiceAccount { + string client_id = 1; + string service_account = 2; + string environment = 3; + string regular_expression = 4; + repeated string subject_alternative_names = 5; + repeated string certificate_authorities = 6; + string extended_key = 7; + NodeAttestation node_attestation = 8; + int32 certificate_validity = 9; + string subordinate_ca = 10; + bool provisioned = 11; + string team = 12; + string email = 13; + google.protobuf.Timestamp created_at = 14; + string created_by = 15; +} +``` + +```json +{ + "clientId": "585c2f84-9a0e-4775-827a-a0a99c7dddcc", + "serviceAccount": "example", + "environment": "development", + "subjectAlternativeNames": ["development.coinbase.com"], + "certificateAuthorities": ["development_use1"], + "extendedKey": "EndEntityServerAuthCertificate", + "nodeAttestation": { + "awsIid": { + "roleArn": "arn:aws:iam::123456789012:instance-profile/instance-profile-name", + "assumeRole": "arn:aws:iam::123456789012:role/assumed-role", + "securityGroups": ["sg-0123456789abcdef0"], + "region": "us-east-1" + } + }, + "certificateValidity": 30, + "subordinateCa": "infrastructure", + "provisioned": false, + "team": "Infrastructure Security", + "email": "security@coinbase.com", + "createdAt": "2023-04-26T22:24:42.873116Z", + "createdBy": "830b9b81-37c0-4180-9dba-9f21b1f6ae21" +} +``` + +### **baseca.v1.Service/DeleteServiceAccount** + +```protobuf +rpc DeleteServiceAccount (AccountId) returns (google.protobuf.Empty); +``` + +**Description** +Delete Service Account from Database + +**Authentication** +User Authentication + +**Request** + +```protobuf +message AccountId { + string uuid = 1; +} +``` + +```sh +grpcurl -vv -plaintext -H "Authorization: Bearer ${AUTH_TOKEN}" \ + -d '{ + "uuid": "585c2f84-9a0e-4775-827a-a0a99c7dddcc" + }' \ + localhost:9090 baseca.v1.Service/DeleteServiceAccount +``` + +**Response** + +```protobuf +google.protobuf.Empty +``` + +### **baseca.v1.Service/DeleteProvisionedServiceAccount** + +```protobuf +rpc DeleteProvisionedServiceAccount (AccountId) returns (google.protobuf.Empty); +``` + +**Description** +Delete Service Account Generated from Provisioner + +**Authentication** +Provisioner Account Authentication + +**Request** + +```protobuf +message AccountId { + string uuid = 1; +} +``` + +**Response** + +```protobuf +google.protobuf.Empty +``` + +### **baseca.v1.Service/CreateProvisionerAccount** + +```protobuf +rpc CreateProvisionerAccount (CreateProvisionerAccountRequest) returns (CreateProvisionerAccountResponse); +``` + +**Description** +Create Provisioner Account Record + +**Authentication** +User Authentication + +**Request** + +```protobuf +message CreateProvisionerAccountRequest { + string provisioner_account = 1; + repeated string environments = 2; + optional string regular_expression = 3; + repeated string subject_alternative_names = 4; + repeated string extended_keys = 5; + uint32 max_certificate_validity = 6; + optional NodeAttestation node_attestation = 7; + string team = 8; + string email = 9; +} + +message NodeAttestation { + AWSInstanceIdentityDocument aws_iid = 1; +} + +message AWSInstanceIdentityDocument { + string role_arn = 1; + string assume_role = 2; + repeated string security_groups = 3; + string region = 4; + string instance_id = 5; + string image_id = 6; + map instance_tags = 7; +} +``` + +```sh +grpcurl -vv -plaintext -H "Authorization: Bearer ${AUTH_TOKEN}" \ + -d '{ + "provisioner_account": "example", + "environments": ["development"], + "subject_alternative_names": [ + "development.coinbase.com" + ], + "extended_keys": ["EndEntityServerAuthCertificate"], + "node_attestation": { + "aws_iid": { + "role_arn": "arn:aws:iam::123456789012:instance-profile/instance-profile-name", + "assume_role": "arn:aws:iam::123456789012:role/assumed-role", + "security_groups": ["sg-0123456789abcdef0"], + "region": "us-east-1" + } + }, + "max_certificate_validity": 30, + "team": "Infrastructure Security", + "email": "security@coinbase.com" + }' \ + localhost:9090 baseca.v1.Service/CreateProvisionerAccount +``` + +**Response** + +```protobuf +message CreateProvisionerAccountResponse { + string client_id = 1; + string client_token = 2; + string provisioner_account = 3; + repeated string environments = 4; + string regular_expression = 5; + repeated string subject_alternative_names = 6; + repeated string extended_keys = 8; + NodeAttestation node_attestation = 9; + uint32 max_certificate_validity = 10; + string team = 11; + string email = 12; + google.protobuf.Timestamp created_at = 13; + string created_by = 14; +} +``` + +```json +{ + "clientId": "585c2f84-9a0e-4775-827a-a0a99c7dddcc", + "clientToken": "[CLIENT_TOKEN]", + "provisionerAccount": "example", + "environments": ["development"], + "subjectAlternativeNames": ["development.coinbase.com"], + "extendedKeys": ["EndEntityServerAuthCertificate"], + "nodeAttestation": { + "awsIid": { + "roleArn": "arn:aws:iam::123456789012:instance-profile/instance-profile-name", + "assumeRole": "arn:aws:iam::123456789012:role/assumed-role", + "securityGroups": ["sg-0123456789abcdef0"], + "region": "us-east-1" + } + }, + "maxCertificateValidity": 30, + "team": "Infrastructure Security", + "email": "security@coinbase.com", + "createdAt": "2023-04-26T22:24:42.873116Z", + "createdBy": "830b9b81-37c0-4180-9dba-9f21b1f6ae21" +} +``` + +### **baseca.v1.Service/GetProvisionerAccount** + +```protobuf +rpc GetProvisionerAccount (AccountId) returns (ProvisionerAccount); +``` + +**Description** +Query Provisioner Account + +**Authentication** +User Authentication + +**Request** + +```protobuf +message AccountId { + string uuid = 1; +} +``` + +```sh +grpcurl -vv -plaintext -H "Authorization: Bearer ${AUTH_TOKEN}" \ + -d '{ + "uuid": "ce337d41-f8b5-4630-b469-5ef896cf4fc1" + }' \ + localhost:9090 baseca.v1.Service/GetProvisionerAccount +``` + +**Response** + +```protobuf +message ProvisionerAccount { + string client_id = 1; + string provisioner_account = 2; + repeated string environments = 3; + string regular_expression = 4; + repeated string subject_alternative_names = 5; + repeated string extended_keys = 7; + NodeAttestation node_attestation = 8; + uint32 max_certificate_validity = 9; + string team = 10; + string email = 11; + google.protobuf.Timestamp created_at = 12; + string created_by = 13; +} +``` + +```json +{ + "clientId": "585c2f84-9a0e-4775-827a-a0a99c7dddcc", + "provisionerAccount": "example", + "environments": ["development"], + "subjectAlternativeNames": ["development.coinbase.com"], + "extendedKeys": ["EndEntityServerAuthCertificate"], + "nodeAttestation": { + "awsIid": { + "roleArn": "arn:aws:iam::123456789012:instance-profile/instance-profile-name", + "assumeRole": "arn:aws:iam::123456789012:role/assumed-role", + "securityGroups": ["sg-0123456789abcdef0"], + "region": "us-east-1" + } + }, + "maxCertificateValidity": 30, + "team": "Infrastructure Security", + "email": "security@coinbase.com", + "createdAt": "2023-04-26T22:24:42.873116Z", + "createdBy": "830b9b81-37c0-4180-9dba-9f21b1f6ae21" +} +``` + +### **baseca.v1.Service/ProvisionServiceAccount** + +```protobuf +rpc ProvisionServiceAccount (ProvisionServiceAccountRequest) returns (ProvisionServiceAccountResponse); +``` + +**Description** +Create Service Account Record + +**Authentication** +Provisioner Account Authentication + +**Request** + +```protobuf +message ProvisionServiceAccountRequest { + CreateServiceAccountRequest account = 1; +} + +message CreateServiceAccountRequest { + string service_account = 1; + string environment = 2; + optional string regular_expression = 3; + repeated string subject_alternative_names = 4; + repeated string certificate_authorities = 5; + string extended_key = 6; + int32 certificate_validity = 7; + string subordinate_ca = 8; + optional NodeAttestation node_attestation = 9; + string team = 10; + string email = 11; + optional string region = 12; +} +message NodeAttestation { + AWSInstanceIdentityDocument aws_iid = 1; +} +message AWSInstanceIdentityDocument { + string role_arn = 1; + string assume_role = 2; + repeated string security_groups = 3; + string region = 4; + string instance_id = 5; + string image_id = 6; + map instance_tags = 7; +} +``` + +**Response** + +```protobuf +message ProvisionServiceAccountResponse { + CreateServiceAccountResponse account = 1; +} +message CreateServiceAccountResponse { + string client_id = 1; + string client_token = 2; + string service_account = 3; + string environment = 4; + string regular_expression = 5; + repeated string subject_alternative_names = 6; + repeated string certificate_authorities = 7; + string extended_key = 8; + NodeAttestation node_attestation = 9; + int32 certificate_validity = 10; + string subordinate_ca = 11; + string team = 12; + string email = 13; + google.protobuf.Timestamp created_at = 14; + string created_by = 15; +} +``` + +```json +{ + "account": { + "clientId": "585c2f84-9a0e-4775-827a-a0a99c7dddcc", + "clientToken": "[CLIENT_TOKEN]", + "serviceAccount": "example", + "environment": "development", + "subjectAlternativeNames": ["development.coinbase.com"], + "certificateAuthorities": ["sandbox_use1"], + "extendedKey": "EndEntityServerAuthCertificate", + "nodeAttestation": { + "awsIid": { + "roleArn": "arn:aws:iam::123456789012:instance-profile/instance-profile-name", + "assumeRole": "arn:aws:iam::123456789012:role/assumed-role", + "securityGroups": ["sg-0123456789abcdef0"], + "region": "us-east-1" + } + }, + "certificateValidity": 30, + "subordinateCa": "infrastructure", + "team": "Infrastructure Security", + "email": "security@coinbase.com", + "createdAt": "2023-04-26T22:24:42.873116Z", + "createdBy": "830b9b81-37c0-4180-9dba-9f21b1f6ae21" + } +} +``` + +### **baseca.v1.Service/DeleteProvisionerAccount** + +```protobuf +rpc DeleteProvisionerAccount (AccountId) returns (google.protobuf.Empty); +``` + +**Description** +Delete Provisioner Account from Database + +**Authentication** +User Authentication + +**Request** + +```protobuf +message AccountId { + string uuid = 1; +} +``` + +```sh +grpcurl -vv -plaintext -H "Authorization: Bearer ${AUTH_TOKEN}" \ + -d '{ + "uuid": "ce337d41-f8b5-4630-b469-5ef896cf4fc1" + }' \ + localhost:9090 baseca.v1.Service/DeleteProvisionerAccount +``` + +**Response** + +```protobuf +google.protobuf.Empty +``` + +## Account + +### **baseca.v1.Account/LoginUser** + +```protobuf +rpc LoginUser (LoginUserRequest) returns (LoginUserResponse); +``` + +**Description** +Returns JSON Web Token (JWT) for User Authentication Endpoints + +**Authentication** +None + +**Request** + +```protobuf +message LoginUserRequest { + string username = 1; + string password = 2; +} +``` + +```sh +grpcurl -vv -plaintext \ + -d '{ + "username": "[USERNAME]", + "password": "[PASSWORD]" + }' \ + localhost:9090 baseca.v1.Account/LoginUser +``` + +**Response** + +```protobuf +message LoginUserResponse { + string access_token = 1; + User user = 2; +} + +message User { + string uuid = 1; + string username = 2; + string full_name = 3; + string email = 4; + string permissions = 5; + google.protobuf.Timestamp credential_changed_at = 6; + google.protobuf.Timestamp created_at = 7; +} +``` + +```json +{ + "accessToken": "[JSON_WEB_TOKEN_OUTPUT]", + "user": { + "uuid": "585c2f84-9a0e-4775-827a-a0a99c7dddcc", + "username": "security_operations", + "fullName": "Example User", + "email": "security@coinbase.com", + "permissions": "ADMIN", + "credentialChangedAt": "0001-01-01T00:00:00Z", + "createdAt": "2023-02-28T07:21:04.221349Z" + } +} +``` + +### **baseca.v1.Account/DeleteUser** + +```protobuf +rpc DeleteUser (UsernameRequest) returns (google.protobuf.Empty); +``` + +**Description** +Delete User from Database + +**Authentication** +User Authentication + +**Request** + +```protobuf +message UsernameRequest { + string username = 1; +} +``` + +```sh +grpcurl -vv -plaintext -H "Authorization: Bearer ${AUTH_TOKEN}" \ + -d '{ + "username": "example@coinbase.com" + }' \ + localhost:9090 baseca.v1.Account/DeleteUser +``` + +**Response** + +```protobuf +google.protobuf.Empty +``` + +### **baseca.v1.Account/GetUser** + +```protobuf +rpc GetUser (UsernameRequest) returns (User); +``` + +**Description** +Query User from Database + +**Authentication** +User Authentication + +**Request** + +```protobuf +message UsernameRequest { + string username = 1; +} +``` + +```sh +grpcurl -vv -plaintext -H "Authorization: Bearer ${AUTH_TOKEN}" \ + -d '{ + "username": "example@coinbase.com" + }' \ + localhost:9090 baseca.v1.Account/GetUser +``` + +**Response** + +```protobuf +message User { + string uuid = 1; + string username = 2; + string full_name = 3; + string email = 4; + string permissions = 5; + google.protobuf.Timestamp credential_changed_at = 6; + google.protobuf.Timestamp created_at = 7; +} +``` + +```json +{ + "uuid": "585c2f84-9a0e-4775-827a-a0a99c7dddcc", + "username": "example@coinbase.com", + "fullName": "Example User", + "email": "example@coinbase.com", + "permissions": "ADMIN", + "credentialChangedAt": "0001-01-01T00:00:00Z", + "createdAt": "2023-04-06T05:31:03.960297Z" +} +``` + +### **baseca.v1.Account/ListUsers** + +```protobuf +rpc ListUsers (QueryParameter) returns (Users); +``` + +**Description** +Query Users from Database + +**Authentication** +User Authentication + +**Request** + +```protobuf +message QueryParameter { + int32 page_id = 2; + int32 page_size = 3; +} +``` + +```sh +grpcurl -vv -plaintext -H "Authorization: Bearer ${AUTH_TOKEN}" \ + -d '{ + "page_id": 1, + "page_size": 20 + }' \ + localhost:9090 baseca.v1.Account/ListUsers +``` + +**Response** + +```protobuf +message Users { + repeated User users = 1; +} +message User { + string uuid = 1; + string username = 2; + string full_name = 3; + string email = 4; + string permissions = 5; + google.protobuf.Timestamp credential_changed_at = 6; + google.protobuf.Timestamp created_at = 7; +} +``` + +```json +{ + "users": [ + { + "uuid": "585c2f84-9a0e-4775-827a-a0a99c7dddcc", + "username": "example@coinbase.com", + "fullName": "Example User", + "email": "example@coinbase.com", + "permissions": "ADMIN", + "credentialChangedAt": "0001-01-01T00:00:00Z", + "createdAt": "2023-02-28T07:21:04.221349Z" + }, + { + "uuid": "bb8e9e1d-2a11-43ae-86f1-9a19b6c8f47e", + "username": "sample@coinbase.com", + "fullName": "Sample User", + "email": "sample@coinbase.com", + "permissions": "READ", + "credentialChangedAt": "0001-01-01T00:00:00Z", + "createdAt": "2023-03-28T05:58:07.822324Z" + } + ] +} +``` + +### **baseca.v1.Account/CreateUser** + +```protobuf +rpc CreateUser (CreateUserRequest) returns (User); +``` + +**Description** +Create User to Manually Interface with baseca Control Plane + +**Authentication** +User Authentication + +**Request** + +```protobuf +message CreateUserRequest { + string username = 1; + string password = 2; + string full_name = 3; + string email = 4; + string permissions = 5; +} +``` + +```sh +grpcurl -vv -plaintext -H "Authorization: Bearer ${AUTH_TOKEN}" \ + -d '{ + "username": "example@coinbase.com", + "password": "[PASSWORD]", + "full_name": "Example User", + "email": "example@coinbase.com", + "permissions": "READ" + }' \ + localhost:9090 baseca.v1.Account/CreateUser +``` + +**Response** + +```protobuf +message User { + string uuid = 1; + string username = 2; + string full_name = 3; + string email = 4; + string permissions = 5; + google.protobuf.Timestamp credential_changed_at = 6; + google.protobuf.Timestamp created_at = 7; +} +``` + +```json +{ + "uuid": "585c2f84-9a0e-4775-827a-a0a99c7dddcc", + "username": "example@coinbase.com", + "fullName": "Example User", + "email": "example@coinbase.com", + "permissions": "READ", + "credentialChangedAt": "0001-01-01T00:00:00Z", + "createdAt": "2023-04-27T00:38:22.776949Z" +} +``` + +### **baseca.v1.Account/UpdateUserCredentials** + +```protobuf +rpc UpdateUserCredentials (UpdateCredentialsRequest) returns (User); +``` + +**Description** +Update User Password + +**Authentication** +None + +**Request** + +```protobuf +message UpdateCredentialsRequest { + string username = 1; + string password = 2; + string updated_password = 3; +} +``` + +```sh +grpcurl -vv -plaintext \ + -d '{ + "username": "example@coinbase.com", + "password": "[PASSWORD]", + "updated_password": "[UPDATED_PASSWORD]" + }' \ + localhost:9090 baseca.v1.Account/UpdateUserCredentials +``` + +**Response** + +```protobuf +message User { + string uuid = 1; + string username = 2; + string full_name = 3; + string email = 4; + string permissions = 5; + google.protobuf.Timestamp credential_changed_at = 6; + google.protobuf.Timestamp created_at = 7; +} +``` + +### **baseca.v1.Account/UpdateUserPermissions** + +```protobuf +rpc UpdateUserPermissions (UpdatePermissionsRequest) returns (User); +``` + +**Description** +Update Permissions Parameter for User + +**Authentication** +User Authentication + +**Request** + +```protobuf +message UpdatePermissionsRequest { + string username = 1; + string permissions = 2; +} +``` + +```sh +grpcurl -vv -plaintext -H "Authorization: Bearer ${AUTH_TOKEN}" \ + -d '{ + "username": "example@coinbase.com", + "permissions": "ADMIN" + }' \ + localhost:9090 baseca.v1.Account/UpdateUserPermissions +``` + +**Response** + +```protobuf +message User { + string uuid = 1; + string username = 2; + string full_name = 3; + string email = 4; + string permissions = 5; + google.protobuf.Timestamp credential_changed_at = 6; + google.protobuf.Timestamp created_at = 7; +} +``` + +```json +{ + "uuid": "585c2f84-9a0e-4775-827a-a0a99c7dddcc", + "username": "example@coinbase.com", + "fullName": "Example User", + "email": "example@coinbase.com", + "permissions": "ADMIN", + "credentialChangedAt": "2023-06-04T01:17:48.919754Z", + "createdAt": "2023-06-04T00:53:45.946512Z" +} +``` diff --git a/docs/GETTING_STARTED.md b/docs/GETTING_STARTED.md new file mode 100644 index 0000000..88198a2 --- /dev/null +++ b/docs/GETTING_STARTED.md @@ -0,0 +1,366 @@ +# Getting Started + +`AWS` Dependencies: + +- [Build Public Key Infrastructure](#public-key-infrastructure) +- [Create baseca Infrastructure](#build-infrastructure) + +`baseca` Configuration: + +- [baseca Service Configuration](CONFIGURATION.md) + +`baseca` Database: + +- [Run and Create Local Database Container](#1-run-and-create-local-database-container) + +`baseca` gRPC Server: + +- Option 1: [Build and Run baseca as Container](#3a-run-baseca-as-container-option-a) +- Option 2: [Compile baseca as Executable (amd64)](#3b-compile-baseca-as-executable-option-b) +- Option 3: [Run baseca as One-Off Execution](#3c-run-baseca-as-one-off-execution-option-c) + +`Signing` x.509 Certificate: + +- [Create Admin User](#2-create-initial-admin-user) +- [Create Service Account](#2-create-service-account) +- [Issue x.509 Certificate with `baseca` Client](#3-issue-x509-certificate-with-baseca-client) + +## Public Key Infrastructure + +Each organization will have different Public Key Infrastructure topologies depending on its needs; for your PKI to be compatible with `baseca` (a) Certificate Authorities must be AWS Private CA and (b) there must be a minimum [PathLen](https://docs.aws.amazon.com/privateca/latest/userguide/PcaTerms.html#terms-pathlength) depending on where `baseca` issues the Subordinate CA from. Designing a Public Key Infrastructure is out of scope of this document, but we will take a look at topologies that `baseca` is compatible with below: + +- Option 1: Root CA (Self-Managed) → Intermediate CA (AWS): Minimum PathLen2 on Root CA, PathLen1 on Intermediate CA (Higher Complexity, Recommended) + +- Option 2: Root CA (AWS) → Intermediate CA (AWS): Minimum PathLen2 on Root CA, PathLen1 on Intermediate CA (Lower Complexity, Recommended) + +- Option 3: Root CA (AWS) → No AWS Intermediate CA: Minimum PathLen1 on Root CA (Not Recommended) + +_PKI Architecture Example: Option 1_ + + +## Build Infrastructure + +### 1. Install Terraform + +```sh +brew install tfenv +tfenv install 1.4.2 +tfenv use 1.4.2 +``` + +### 2. Configure Resource Module in `baseca/terraform/development` + +`baseca` Infrastructure [Documentation](../terraform/README.md) + +**NOTE:** Private CA(s) in `acm_pca_arns` must already exist within your infrastructure; refer to the [Public Key Infrastructure](#public-key-infrastructure) section if you need to design and deploy a Public Key Infrastructure. + +**DISCLAIMER**: `DO NOT` use Private CA(s) that are used within your organization's `PRODUCTION` environment for this `GETTING_STARTED.md` document, this is meant to build a local development environment. For production deployments please refer to [`PRODUCTION_DEPLOYMENT.md`](PRODUCTION_DEPLOYMENT.md). + +```sh +module "baseca" { + source = "./baseca" + service = "baseca" + environment = "development" + region = "us-east-1" + key_spec = "RSA_4096" + bucket = "baseca-firehose-example" +} +``` + +### 3. Deploy Terraform Resource Module + +```sh +cd /path/to/baseca/terraform/development + +terraform init +terraform apply +``` + +### 4. Terraform Outputs + +These outputs from Terraform will be utilized within the baseca `config.primary.local.sandbox.yml` configuration file. + +```sh +terraform output + +# Example Output + +kinesis_firehose_stream = "baseca-development" +kms_key_id = "xxxxxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" +``` + +## Configuration + +Create and update the configuration [`config/config.primary.local.sandbox.yml`](../config/config.primary.local.sandbox.yml) file using the outputs created from the Terraform infrastructure; an example configuration can be seen within [CONFIGURATION.md](CONFIGURATION.md). + +```yml +# Update config.primary.local.sandbox.yml +firehose: + stream: baseca-development + +kms: + key_id: xxxxxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx # kms_key_id +``` + +| Variable | Description | Update | +| ------------------------- | ----------------------------------------------------- | ---------------------------- | +| `grpc_server_address` | gRPC Server Address and Port | No | +| `ocsp_server` | Custom OCSP Server URL | Remove for Local Development | +| `database` | baseca RDS Database | No | +| `redis` | baseca Elasticache Redis Cluster | Remove for Local Development | +| `domains` | List of Valid Domains for `baseca` x.509 Certificates | Yes | +| `firehose` | baseca Kinesis Data Firehose | Yes | +| `kms` | baseca Customer Managed KMS Key | Yes | +| `acm_pca` | AWS Private Certificate Authorities | Yes | +| `secrets_manager` | AWS Secrets Manager | Remove for Local Development | +| `subordinate_ca_metadata` | baseca Subordinate CA Attributes | Optional | +| `certificate_authority` | Environment(s) for `acm_pca` Private CA(s) | Yes | + +## Local Deployment + +### 1. Run and Create Local Database Container + +Launch the PostgreSQL Container + +```sh +# Start Postgres Database and Mount db/init to Container +docker run --name baseca -p 5432:5432 -v /path/to/baseca/db/init:/db/init -e POSTGRES_USER=root -e POSTGRES_PASSWORD=secret -d postgres:latest + +# Create Initial baseca Database and Configure Root User Account +docker exec -it baseca createdb --username=root --owner=root baseca +``` + +[`golang-migrate Download Instructions`](https://github.com/golang-migrate/migrate/blob/master/cmd/migrate/README.md) + +```sh +brew install golang-migrate # Darwin (MacOS) + +migrate -path db/migration -database "postgresql://root:secret@localhost:5432/baseca?sslmode=disable" -verbose up +``` + +### 2. Create Initial Admin User + +```sh +# Update db/init/init-docker.sql for Admin User +VALUES (uuid_generate_v4(), 'example@example.com', crypt('ADMIN_CREDENTIALS', gen_salt('bf')), 'Example User', 'example@example.com', 'ADMIN', now()); + +# Run Database Init to Create Admin User +docker exec -it baseca psql -U root -d baseca -a -f db/init/init-docker.sql +``` + +### 3a. Run baseca as Container (Option A) + +_This step is recommended for production deployments using the standard Dockerfile that is provided for baseca._ + +Update the configuration file `config.primary.local.sandbox.yml` + +```yml +# IPv4_ADDRESS (Darwin) +ifconfig en0 | grep inet + +# Update config.primary.local.sandbox.yml +database_endpoint: IPv4_ADDRESS +database_reader_endpoint: IPv4_ADDRESS +ssl_mode: disable +``` + +Run the `baseca` Container + +**NOTE:** You must have AWS credentials stored locally within `~/.aws` with permissions to all infrastructure components created from Terraform and access to the Private CAs. + +**RELEASE:** Search for Latest [`baseca ghcr.io Published Release`](https://github.com/orgs/coinbase/packages/container/package/baseca) and update the `VERSION_SHA` container tag with the latest version. + +```sh +docker run -p 9090:9090 -e database_credentials=secret -v ~/.aws/:/home/baseca/.aws/:ro \ + -v /path/to/local/baseca/config:/home/baseca/config ghcr.io/coinbase/baseca:VERSION_SHA +``` + +### 3b. Compile `baseca` as Executable (Option B) + +_This step is recommended for users that may want build the binary and then deploy their own custom container._ + +Update the configuration file `config.primary.local.sandbox.yml` + +```yml +# Update config.primary.local.sandbox.yml +database_endpoint: localhost +database_reader_endpoint: localhost +ssl_mode: disable +``` + +Compile the Golang Binary `baseca` + +```sh +# Darwin AMD64 +GOOS=darwin GOARCH=amd64 go build -o target/bin/darwin/baseca cmd/baseca/server.go +database_credentials=secret ./target/bin/darwin/baseca + +# Linux AMD64 +GOOS=linux GOARCH=amd64 go build -o target/bin/linux/baseca cmd/baseca/server.go +database_credentials=secret ./target/bin/linux/baseca +``` + +### 3c. Run baseca as One-Off Execution (Option C) + +_This step is recommended for local testing and getting `baseca` running most quickly._ + +Update the configuration file `config.primary.local.sandbox.yml` + +```yml +# Update config.primary.local.sandbox.yml +database_endpoint: localhost +database_reader_endpoint: localhost +ssl_mode: disable +``` + +Start the Golang `baseca` gRPC Server + +```sh +database_credentials=secret go run cmd/baseca/server.go +``` + +## Signing x.509 Certificate + +Start the `baseca` gRPC server via the preferred method within the [Local Deployment](#local-deployment) section and then run the [`baseca.v1.Account/LoginUser`](ENDPOINTS.md#basecav1accountloginuser) RPC method. + +### 1. Generate Authentication Token + +Authenticate with the `ADMIN` user created from the [`Create Initial Admin User`](#2-create-initial-admin-user) section. + +```sh +grpcurl -vv -plaintext \ + -d '{ + "username": "[USERNAME]", + "password": "[PASSWORD]" + }' \ + localhost:9090 baseca.v1.Account/LoginUser + +# baseca.v1.Account/LoginUser Response +{ + "accessToken": "[AUTH_TOKEN]", + "user": { + "username": "example@coinbase.com", + "fullName": "Example User", + "email": "example@coinbase.com", + "permissions": "ADMIN", + "credentialChangedAt": "0001-01-01T00:00:00Z", + "createdAt": "2023-05-01T12:00:00.000000Z" + } +} + +export AUTH_TOKEN=[AUTH_TOKEN] +``` + +### 2. Create Service Account + +Build gRPC request to provision a service account; ensure that the environment and certificate authorities are mapped to the baseca configuration in `config/config.primary.local.sandbox.yml`. + +```sh +grpcurl -vv -plaintext -H "Authorization: Bearer ${AUTH_TOKEN}" \ + -d '{ + "service_account": "example", + "environment": "development", + "subject_alternative_names": [ + "development.coinbase.com" + ], + "extended_key": "EndEntityServerAuthCertificate", + "certificate_authorities": [ + "development_use1" + ], + "certificate_validity": 30, + "subordinate_ca": "infrastructure", + "team": "Infrastructure Security", + "email": "security@coinbase.com" + }' \ + localhost:9090 baseca.v1.Service/CreateServiceAccount + +# baseca.v1.Account/CreateServiceAccount Response + +{ + "clientId": "585c2f84-9a0e-4775-827a-a0a99c7dddcc", # Service Account UUID + "clientToken": "[CLIENT_TOKEN]", # Service Account Auth Token + "serviceAccount": "example", + "environment": "development", + "subjectAlternativeNames": ["development.coinbase.com"], + "certificateAuthorities": ["development_use1"], + "extendedKey": "EndEntityServerAuthCertificate", + "nodeAttestation": {}, # Node Attestation Not Applicable to Local Development + "certificateValidity": 30, + "subordinateCa": "infrastructure", + "team": "Infrastructure Security", + "email": "security@coinbase.com", + "createdAt": "2023-05-01T12:00:00.000000Z", + "createdBy": "830b9b81-37c0-4180-9dba-9f21b1f6ae21" # Admin User UUID +} +``` + +_Mapping Between `Service Account` and `baseca` Configuration_ + + + +### 3. Issue x.509 Certificate with `baseca` Client + +After using the `baseca` client to issue a certificate the private key material and signed certificate will be stored within the parameters defined within `baseca.Output`. The private key material is generated locally, and because we are running the `baseca` server on our local machine the Subordinate CA will also be generated and written in memory under the `/tmp/baseca/ssl/[SUBORDINATE_CA]` directory. + +```go +package main + +import ( + "crypto/x509" + "fmt" + "log" + + baseca "github.com/coinbase/baseca/pkg/client" +) + +func main() { + configuration := baseca.Configuration{ + URL: "localhost:9090", + Environment: baseca.Env.Local, + } + + authentication := baseca.Authentication{ + ClientId: "CLIENT_ID", + ClientToken: "CLIENT_TOKEN", + } + + client, err := baseca.LoadDefaultConfiguration(configuration, baseca.Attestation.Local, authentication) + if err != nil { + fmt.Println(err) + } + + if err != nil { + // Handle Error + log.Fatal(err) + } + + metadata := baseca.CertificateRequest{ + CommonName: "development.coinbase.com", + SubjectAlternateNames: []string{"development.coinbase.com"}, + SigningAlgorithm: x509.SHA512WithRSA, + PublicKeyAlgorithm: x509.RSA, + KeySize: 4096, + DistinguishedName: baseca.DistinguishedName{ + Organization: []string{"Coinbase"}, + // Additional Fields + }, + Output: baseca.Output{ + PrivateKey: "/tmp/private.key", // baseca Generate Private Key Output Location + Certificate: "/tmp/certificate.crt", // baseca Signed Leaf Certificate Output Location + IntermediateCertificateChain: "/tmp/intermediate_chain.crt", // baseca Signed Certificate Chain Up to Intermediate CA Output Location + RootCertificateChain: "/tmp/root_chain.crt", // baseca Signed Full Certificate Chain Output Location + CertificateSigningRequest: "/tmp/certificate_request.csr", // baseca CSR Output Location + }, + } + } + + response, err := client.IssueCertificate(metadata) + + if err != nil { + // Handle Error + log.Fatal(err) + } + + log.Printf("%+v", response) +} +``` diff --git a/docs/PRODUCTION_DEPLOYMENT.md b/docs/PRODUCTION_DEPLOYMENT.md new file mode 100644 index 0000000..53bd2cf --- /dev/null +++ b/docs/PRODUCTION_DEPLOYMENT.md @@ -0,0 +1,288 @@ +# Production Deployment + +`AWS` Dependencies + +- [Build Public Key Infrastructure](#public-key-infrastructure) +- [Create baseca Resource Infrastructure](#build-infrastructure) + +`baseca` Database Migration + +- [Bootstrap RDS Database](#bootstrap-rds-database) + +Build and Upload `baseca`: + +- [Build and Upload baseca to ECR](#build-and-upload-baseca-to-ecr) + +Deploy Compute for `baseca`: + +- [Create baseca Compute Infrastructure](#build-compute-infrastructure) + +## Public Key Infrastructure + +Please read the [`GETTING_STARTED.md`](GETTING_STARTED.md#public-key-infrastructure) documentation and set up a local development environment before proceeding and deploying the production infrastructure. + +## Build Infrastructure + +### 1. Install Terraform + +```sh +brew install tfenv +tfenv install 1.4.2 +tfenv use 1.4.2 +``` + +### 2. Configure Resource Module in `baseca/terraform/production` + +`baseca` Infrastructure [Documentation](../terraform/README.md#production-deployment-resources) + +**NOTE:** Private CA(s) in `acm_pca_arns` must already exist within your infrastructure; refer to the [Public Key Infrastructure](#public-key-infrastructure) section if you need to design and deploy a Public Key Infrastructure. + +```sh +# baseca/terraform/production/baseca.tf + +module "baseca" { + source = "./baseca" + service = "baseca" + environment = "production" + region = "us-east-1" + key_spec = "RSA_4096" + bucket = "baseca-firehose-example" + db_ingress = ["10.0.0.0/8"] + acm_pca_arns = [ + "arn:aws:acm-pca:us-east-1:123456789012:certificate-authority/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", + "arn:aws:acm-pca:us-east-1:987654321098:certificate-authority/yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy" + ] +} +``` + +### 3. Deploy Terraform Resource Module + +```sh +cd /path/to/baseca/terraform/production + +terraform init +terraform apply +``` + +### 4. Terraform Outputs + +These outputs from Terraform will be utilized within the baseca `config.primary.production.aws.yml` configuration file. + +```sh +terraform output + +# Example Output + +ecr_repository = "012345678901.dkr.ecr.us-east-1.amazonaws.com/baseca" +kinesis_firehose_stream = "baseca-production" +kms_key_id = "xxxxxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" +rds_reader_endpoint = "baseca-production-cluster.cluster-ro-xxxxxxxxxxxx.us-east-1.rds.amazonaws.com" +rds_writer_endpoint = "baseca-production-cluster.cluster-xxxxxxxxxxxx.us-east-1.rds.amazonaws.com" +redis_endpoint = "baseca-production.xxxxxx.0001.use1.cache.amazonaws.com" +``` + +## Configuration + +Update the [`config/config.primary.production.aws.yml`](../examples/config/config.primary.production.aws.yml) configuration file using the outputs from Terraform; an example configuration can be seen within [CONFIGURATION.md](CONFIGURATION.md). + +```yml +# Update config.primary.production.aws.yml +database: + database_endpoint: baseca-production-cluster.cluster-xxxxxxxxxxxx.us-east-1.rds.amazonaws.com # rds_writer_endpoint + database_reader_endpoint: baseca-production-cluster.cluster-ro-xxxxxxxxxxxx.us-east-1.rds.amazonaws.com # rds_reader_endpoint + +redis: + cluster_endpoint: baseca-production.xxxxxx.0001.use1.cache.amazonaws.com # redis_endpoint + +firehose: + stream: baseca-production + +kms: + key_id: xxxxxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx # kms_key_id +``` + +| Variable | Description | Update | +| ------------------------- | ----------------------------------------------------- | -------- | +| `grpc_server_address` | gRPC Server Address and Port | No | +| `ocsp_server` | Custom OCSP Server URL | Optional | +| `database` | baseca RDS Database | Yes | +| `redis` | baseca Elasticache Redis Cluster | Yes | +| `domains` | List of Valid Domains for `baseca` x.509 Certificates | Yes | +| `firehose` | baseca Kinesis Data Firehose | Yes | +| `kms` | baseca Customer Managed KMS Key | Yes | +| `acm_pca` | AWS Private Certificate Authorities | Yes | +| `secrets_manager` | AWS Secrets Manager | Yes | +| `subordinate_ca_metadata` | baseca Subordinate CA Attributes | Optional | +| `certificate_authority` | Environment(s) for `acm_pca` Private CA(s) | Yes | + +## Bootstrap RDS Database + +### 1. Search RDS Credentials from Secrets Manager + + + +```yml +# Update config.primary.production.aws.yml +secrets_manager: + secret_id: rds!cluster-bcc40600-5fa7-4877-aa35-529cae165937 +``` + +### 2. Perform Database Migration + +[`golang-migrate Download Instructions`](https://github.com/golang-migrate/migrate/blob/master/cmd/migrate/README.md) + +**NOTE:** The RDS Cluster should be deployed within a private subnet which means the database migration must be done through an EC2 instance which (a) has network connectivity to the database and (b) is allowed by the security group from the database over Port 5432. + +```sh +# Launch an Ubuntu EC2 Instance +# Documentation: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EC2_GetStarted.html + +cd /path/to/baseca + +# Copy Database Migration Files and RDS Certificate to EC2 +scp -i /path/to/your/key.pem -r db/migration ubuntu@[IP_ADDRESS]:/home/ubuntu +scp -i /path/to/your/key.pem internal/attestor/aws_iid/certificate/rds.global.bundle.pem ubuntu@[IP_ADDRESS]:/home/ubuntu + +# SSH to EC2 +ssh -i /path/to/your/key.pem ubuntu@[IP_ADDRESS] + +# Download golang-migrate +curl -L https://github.com/golang-migrate/migrate/releases/download/v4.16.1/migrate.linux-amd64.tar.gz | tar xvz # AMD64 Example + +# Execute Database Migration +./migrate -path migration -database "postgresql://[username]:[password]@[rds_writer_endpoint]:5432/baseca?sslmode=verify-full&sslrootcert=/home/ubuntu/rds.global.bundle.pem" -verbose up +``` + +**Note:** If you see the error below you will need to encode the credentials; please do this `locally` and not through an online encoder. + +``` +error: parse "postgresql://baseca:[PASSWORD]@[RDS_WRITER_ENDPOINT]:5432/baseca?sslmode=disable": net/url: invalid userinfo +``` + +### 3. Create Initial Admin User + +```sh +# Update db/init/init.sql for Admin User +VALUES (uuid_generate_v4(), 'example@example.com', crypt('ADMIN_CREDENTIALS', gen_salt('bf')), 'Example User', 'example@example.com', 'ADMIN', now()); + +# Copy db.init/init.sql to EC2 +scp -i /path/to/your/key.pem db/init/init.sql ubuntu@[IP_ADDRESS]:/home/ubuntu + +# SSH to EC2 +ssh -i /path/to/your/key.pem ubuntu@[IP_ADDRESS] + +# Download Postgres Client +sudo apt-get update +sudo apt-get install -y postgresql-client + +# Run Database Init to Create Admin User +psql -h [rds_writer_endpoint] -U [username] -d baseca -f /path/to/init.sql +``` + +**Note:** [`Terminate`](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/terminating-instances.html) the Migration EC2 Instance After Migration Completed + +## Build and Upload `baseca` to ECR + +### 1a. Build from Published `baseca` Image + +_Use this option by using the published image from `baseca` without any code changes or updates to your Dockerfile._ + +Create `Dockerfile` and Copy [`Configurations`](../config) to `/home/baseca/config` within the Docker Container. + +**RELEASE:** Search for Latest [`baseca ghcr.io Published Release`](https://github.com/orgs/coinbase/packages/container/package/baseca) and update the `VERSION_SHA` container tag with the latest version. + +```Dockerfile +# baseca/Dockerfile-production + +FROM ghcr.io/coinbase/baseca:VERSION_SHA + +COPY ./config /home/baseca/config +USER baseca + +CMD ["/home/baseca/baseca"] +``` + +```sh +cd /path/to/baseca/Dockerfile-production +docker build -t baseca -f Dockerfile-production . +``` + +Push Image to ECR Registry + +```sh +docker tag baseca .dkr.ecr..amazonaws.com/baseca:latest +aws ecr get-login-password --region | docker login --username AWS --password-stdin .dkr.ecr..amazonaws.com +docker push .dkr.ecr..amazonaws.com/baseca:latest +``` + +**NOTE:** This is an image built from [`examples/Dockerfile`](../examples/Dockerfile); there are alternative methods to get the config files onto the image, but you must copy your configurations within the `/home/baseca/config` path. + + + +### 1b. Local Build + +_Use this option if you have requirements to change the `baseca` image through either custom code changes or updates to the Dockerfile._ + +**NOTE:** If you intend to run a local build, within the current [`Dockerfile`](../Dockerfile) we do not copy the `config/` directory into the base image. Additionally within `.dockerignore` we ignore the `config/` directory as well. If you are running the image directly, the OS directory structure for the final image should be the same as the image in the previous option. + +```sh +cd /path/to/baseca +docker build -t baseca . +``` + +Push Image to ECR Registry + +```sh +docker tag baseca .dkr.ecr..amazonaws.com/baseca:latest +aws ecr get-login-password --region | docker login --username AWS --password-stdin .dkr.ecr..amazonaws.com +docker push .dkr.ecr..amazonaws.com/baseca:latest +``` + +## Build Compute Infrastructure + +**DISCLAIMER:** The compute deployment is a sample of how `baseca` can run within an environment; if you decide to run this ECS Service as is, be cautious as it has not been fully tested. `baseca` is not intended to run on ephemeral instances; if a design decision is made to run on ephemeral nodes you will need to have a shared volume such as EFS. The `recommended` method is to deploy the service directly on an EC2 instance that supports [`Nitro Enclaves`](https://docs.aws.amazon.com/enclaves/latest/user/using.html); this ensures that key material never leaves the host it was generated on and is never accessible by anything outside of the `baseca` process. + +`baseca` Infrastructure [Documentation](../terraform/README.md#production-compute-deployment) + +### 1. Configure Compute Module in `baseca/terraform/production` + +```sh +# baseca/terraform/production/baseca.tf + +module "compute" { + source = "./compute" + service = "baseca" + region = "us-east-1" + environment = "production" + configuration = "production" + + vpc_id = "vpc-xxxxxxxx" + subnet_ids = ["subnet-0123456789abcdef", "subnet-9876543210fedcba"] + + host_port = 9090 + network_ingress = ["10.0.0.0/8"] + public_ip = false + + baseca_iam_role = module.baseca.baseca_iam_role + ecr_repository = module.baseca.ecr_repository + depends_on = [ + module.baseca + ] +} +``` + +**NOTE:** `environment` and `configuration` are values used to concatenate and build the configuration file for `baseca`. Within Terraform we auto export these as environment variables within the ECS Task Definition, but if you are using your own deployment you will need to have these environment variables accessible by `baseca`. If the values below were exported then `baseca` would read the `config/config.primary.production.aws.yml` file. Changing `configuration` to `development` would read the `config/config.primary.development.aws.yml` file. + +```sh +export ENVIRONMENT=production +export CONFIGURATION=production +``` + +### 2. Deploy Terraform Compute Module + +```sh +cd /path/to/baseca/terraform/production + +terraform init +terraform apply +``` diff --git a/docs/SCOPE.md b/docs/SCOPE.md new file mode 100644 index 0000000..5c04552 --- /dev/null +++ b/docs/SCOPE.md @@ -0,0 +1,185 @@ +# Scope + +`Scope` is a concept for `service accounts` where attributes are defined to ensure that (a) the x.509 certificate issued should be unique to the service depending on the requirements, and (b) only the intended service is able to issue that x.509 certificate. + +- **`Certificate Authorities`** define which Certificate Authority a service is able to issue from; this scope goes together with the environment constraint to ensure that a service is not able to have a certificate authorities that cross environments (i.e. development_use1, production_use1). + +- **`Subject Alternative Names`** determine which Common Name (CN) and Subject Alternative Names (SAN) that are valid for the service account. + +- **`Extended Key`** dictates the type of x.509 certificate that will be issued whether it will be used for client authentication (EndEntityClientAuthCertificate), server authentication (EndEntityServerAuthCertificate), or code signing (CodeSigningCertificate). + +- **`Certificate Validity`** is a configurable value to determine when the expiration for an x.509 certificate will be; while we aim to have short-lived certificates for all of our services this is not always possible due to the nature of how often services are re-deployed. + +- **`Node Attestation`** is used to ensure that the request is originating from the node we expect it; currently we support attestation from AWS Instance Identity Document (IID) which validates the signature of the request and maps it to ensure the request is coming from the intended server. In the case the service account credentials are compromised a certificate would not be issued unless the request is originating from the underlying node (i.e. EC2 instance). + +- **`Subordinate CA`** is the and Subject Alternative Name (SAN) value of the Subordinate CA that will be issued from Private CA and used to sign requests downstream requests. For example, if the service account below made a request to baseca a Subordinate CA named `infrastructure_sandbox` would be created on the host and used to sign requests that also have a service account that uses the `infrastructure` subordinate_ca and is in the `sandbox` environment. + +```sh +grpcurl -vv -plaintext -H "Authorization: Bearer ${AUTH_TOKEN}" \ + -d '{ + "service_account": "example", + "environment": "sandbox", # << Environment Scope >> + "subject_alternative_names": [ # << Subject Alternative Names (SAN) Scope >> + "sandbox.coinbase.com" + ], + "extended_key": "EndEntityServerAuthCertificate", + "node_attestation": { # << Node Attestation Scope >> + "aws_iid": { + "role_arn": "arn:aws:iam::123456789012:instance-profile/instance-profile-name", + "assume_role": "arn:aws:iam::123456789012:role/assumed-role", + "security_groups": ["sg-0123456789abcdef0"], + "region": "us-east-1" + } + }, + "certificate_authorities": [ # << Certificate Authorities Scope >> + "sandbox_use1" + ], + "certificate_validity": 30, # << Certificate Validity Scope >> + "account_type": "SERVICE", + "subordinate_ca": "infrastructure", # << Subordinate CA Scope >> + "team": "Infrastructure Security", + "email": "security@coinbase.com" + }' \ + localhost:9090 baseca.v1.Service/CreateServiceAccount +``` + +Using the service account issued above, if we generated a CSR and sent it to [`baseca.v1.Certificate/SignCSR`](ENDPOINTS.md#basecav1certificatesigncsr) the output for the leaf certificate would be similar to the one below. + +``` +-----BEGIN CERTIFICATE----- +MIIF4TCCA8mgAwIBAgIUYKMFt0fz3ajjsiQ6O1VT0XTGuUEwDQYJKoZIhvcNAQEN +BQAwczELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAkNBMRYwFAYDVQQHEw1TYW4gRnJh +bmNpc2NvMQswCQYDVQQKEwJDQTERMA8GA1UECxMIU2VjdXJpdHkxHzAdBgNVBAMM +FmluZnJhc3RydWN0dXJlX3NhbmRib3gwHhcNMjMwNTI0MjIxNTMzWhcNMjMwNjIz +MjIxNTMzWjB3MQswCQYDVQQGEwJVUzELMAkGA1UECBMCQ0ExFjAUBgNVBAcTDVNh +biBGcmFuY2lzY28xETAPBgNVBAoTCENvaW5iYXNlMREwDwYDVQQLEwhTZWN1cml0 +eTEdMBsGA1UEAxMUc2FuZGJveC5jb2luYmFzZS5jb20wggIiMA0GCSqGSIb3DQEB +AQUAA4ICDwAwggIKAoICAQDLP6qvTSG+4qnfGCcY6ECEz4VBlaYI7cKjKarnxbUP +3RChJUsUUzDAohxol82LwBwN1zEkD2jtTFRnRN86DeTN9moQ2pDL4ePFm7tADnxc +wHd+xVBtvJjLQOKe5Oa21zGbRtfuXw/2nW1yeZPmVm3yhdOseoJjP2qEi8pnNyno +nP4mYvUYDHLJV5RY5IhjHkl6vQdenw9+9PiZUbLp1WGtgWYney9QhS5rGdy5IN5e +X3TVwKyUyNTApqt+dOgXsH8hOAyprgPB54bvjoH3q2oskJpHDD9QQtEtZ+8vod7v +vhdp62ywWrxLyjdE0Q+u5v8OyEXYKmVFzww2OSKVFPu6M5nj1SZvbYqZpfVnrYWz +RfEkJZ288tU0NKbOQEe0I4V5yMGT3DhFTE31h0CV7dop2qQpDZZCWtO5VusvL9p0 +/6RCta9jOfsBBHAwx/UCrgB9tW5hP7CDYnjz6a8+awK4NMV7BU0JLOTt9EgCFUSg +3I7qQFc8IuAGbMX4RGpo0iCvWkOK/SCegAv96lwMT1WvgYlN+hNxAuM0mn0PDFvn +4K1RlNCQnys30i4qgJ0dIANhiO64VRHC8a6NGJy+h1uQ/Yq/YbLvGYctTkMIqGqq +D4CnhDfCntidtfcNqhhVJrH5pFE3OAtxi6IgSqjHijZ6NRTNIdXSr5/3vSXjAF8o +aQIDAQABo2kwZzAOBgNVHQ8BAf8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEw +HwYDVR0jBBgwFoAUXq371v0v7mpEhQwZ5ZEl8ZXhSNgwHwYDVR0RBBgwFoIUc2Fu +ZGJveC5jb2luYmFzZS5jb20wDQYJKoZIhvcNAQENBQADggIBACdsEXqo7TLV0kDC +uCZgiNr0tdTrziKdex9vBJyxQCDJlDJRD2119kl4uZCP5B+uy1JTjOKDMCzzLkK8 +Fi1STvIXOpQyO2zlAR0id3UuJGkJ+DpxFAqIHRCiPGTdxoqqggRI5Lo+PCVzE8Ya +p+lOsY9seqcS0hUxtowwSNL29VnuSEECNQ3yV3Z9moReWkjLHt+mOyBJVCNHRmxx +u3CO73g/WgkjAUpH2qZaURtNFtaEh3hnQ7wPs8A9UOSMbmI8brdo586IFAGfJlAr +hnVdoZO+5uOmNU1IhhRfXlHCX8MRuN06ci1MQ2LdqCvVmw1MdtE9HRBj3kj824E+ +LyBZ1LP+hcz0LXfhU9lpLezRha+47d5TvseI9oijPvwdbwfPv2UfA2qQ9Wges1/o +y/LldJAzk6Wkk9vNrThBDRYCjWdoKxcROotWMYOzm+IZQOZ2+u+gV3WPO5U2r6mV +kxbVFaoLMA3485aXQBpnyFC/nqro6lf5SgtcTopFRCYQef+eVkuLsf+I7iMKObFE +t2dQy6vIXN1kHsAM9QKQDSKBYh3TjZi9KmY3e8gEFpXv0KzgOWS7JXCNZzZbegvL +XrHnWSowNC+VcpjTtLpI22cYFtYYPOlsSqgqYIS3VLecwdzBKsS/1qRQLvW4/pXN +jqXtvcRCofiEWOkOxBlz1JJvHV/N +-----END CERTIFICATE----- +``` + +Taking a further look, we can decode the certificate with `openssl x509 -in [file].crt -noout -text` which we can then correlate to the scopes that were defined within the service account. + +- x.509 Server Certificate +- Signed by Subordinate CA Generated from `sandbox_use1` +- Common Name (CN) and Subject Alternative Names (SAN) of `sandbox.coinbase.com` +- Certificate Validity of `30 Days` +- Request Must Come from EC2 with Attributes within `aws_iid` + +```sh +Certificate: + Data: + Version: 3 (0x2) + Serial Number: + 60:a3:05:b7:47:f3:dd:a8:e3:b2:24:3a:3b:55:53:d1:74:c6:b9:41 + Signature Algorithm: sha512WithRSAEncryption # Signing Algorithm (Configurable in baseca Client) + Issuer: C=US, ST=CA, L=San Francisco, O=CA, OU=Security, CN=infrastructure_sandbox # Subordinate CA Issued by baseca + Validity + Not Before: May 24 22:15:33 2023 GMT + Not After : Jun 23 22:15:33 2023 GMT # Certificate Validity + Subject: C=US, ST=CA, L=San Francisco, O=Coinbase, OU=Security, CN=sandbox.coinbase.com # Common Name (CN) + Subject Public Key Info: + Public Key Algorithm: rsaEncryption # Key Algorithm (Configurable in baseca Client) + RSA Public-Key: (4096 bit) # Key Size (Configurable in baseca Client) + Modulus: + 00:cb:3f:aa:af:4d:21:be:e2:a9:df:18:27:18:e8: + 40:84:cf:85:41:95:a6:08:ed:c2:a3:29:aa:e7:c5: + b5:0f:dd:10:a1:25:4b:14:53:30:c0:a2:1c:68:97: + cd:8b:c0:1c:0d:d7:31:24:0f:68:ed:4c:54:67:44: + df:3a:0d:e4:cd:f6:6a:10:da:90:cb:e1:e3:c5:9b: + bb:40:0e:7c:5c:c0:77:7e:c5:50:6d:bc:98:cb:40: + e2:9e:e4:e6:b6:d7:31:9b:46:d7:ee:5f:0f:f6:9d: + 6d:72:79:93:e6:56:6d:f2:85:d3:ac:7a:82:63:3f: + 6a:84:8b:ca:67:37:29:e8:9c:fe:26:62:f5:18:0c: + 72:c9:57:94:58:e4:88:63:1e:49:7a:bd:07:5e:9f: + 0f:7e:f4:f8:99:51:b2:e9:d5:61:ad:81:66:27:7b: + 2f:50:85:2e:6b:19:dc:b9:20:de:5e:5f:74:d5:c0: + ac:94:c8:d4:c0:a6:ab:7e:74:e8:17:b0:7f:21:38: + 0c:a9:ae:03:c1:e7:86:ef:8e:81:f7:ab:6a:2c:90: + 9a:47:0c:3f:50:42:d1:2d:67:ef:2f:a1:de:ef:be: + 17:69:eb:6c:b0:5a:bc:4b:ca:37:44:d1:0f:ae:e6: + ff:0e:c8:45:d8:2a:65:45:cf:0c:36:39:22:95:14: + fb:ba:33:99:e3:d5:26:6f:6d:8a:99:a5:f5:67:ad: + 85:b3:45:f1:24:25:9d:bc:f2:d5:34:34:a6:ce:40: + 47:b4:23:85:79:c8:c1:93:dc:38:45:4c:4d:f5:87: + 40:95:ed:da:29:da:a4:29:0d:96:42:5a:d3:b9:56: + eb:2f:2f:da:74:ff:a4:42:b5:af:63:39:fb:01:04: + 70:30:c7:f5:02:ae:00:7d:b5:6e:61:3f:b0:83:62: + 78:f3:e9:af:3e:6b:02:b8:34:c5:7b:05:4d:09:2c: + e4:ed:f4:48:02:15:44:a0:dc:8e:ea:40:57:3c:22: + e0:06:6c:c5:f8:44:6a:68:d2:20:af:5a:43:8a:fd: + 20:9e:80:0b:fd:ea:5c:0c:4f:55:af:81:89:4d:fa: + 13:71:02:e3:34:9a:7d:0f:0c:5b:e7:e0:ad:51:94: + d0:90:9f:2b:37:d2:2e:2a:80:9d:1d:20:03:61:88: + ee:b8:55:11:c2:f1:ae:8d:18:9c:be:87:5b:90:fd: + 8a:bf:61:b2:ef:19:87:2d:4e:43:08:a8:6a:aa:0f: + 80:a7:84:37:c2:9e:d8:9d:b5:f7:0d:aa:18:55:26: + b1:f9:a4:51:37:38:0b:71:8b:a2:20:4a:a8:c7:8a: + 36:7a:35:14:cd:21:d5:d2:af:9f:f7:bd:25:e3:00: + 5f:28:69 + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Key Usage: critical + Digital Signature, Key Encipherment + X509v3 Extended Key Usage: + TLS Web Server Authentication # Extended Key + X509v3 Authority Key Identifier: + keyid:5E:AD:FB:D6:FD:2F:EE:6A:44:85:0C:19:E5:91:25:F1:95:E1:48:D8 + + X509v3 Subject Alternative Name: + DNS:sandbox.coinbase.com # Subject Alternative Name (SAN) + Signature Algorithm: sha512WithRSAEncryption + 27:6c:11:7a:a8:ed:32:d5:d2:40:c2:b8:26:60:88:da:f4:b5: + d4:eb:ce:22:9d:7b:1f:6f:04:9c:b1:40:20:c9:94:32:51:0f: + 6d:75:f6:49:78:b9:90:8f:e4:1f:ae:cb:52:53:8c:e2:83:30: + 2c:f3:2e:42:bc:16:2d:52:4e:f2:17:3a:94:32:3b:6c:e5:01: + 1d:22:77:75:2e:24:69:09:f8:3a:71:14:0a:88:1d:10:a2:3c: + 64:dd:c6:8a:aa:82:04:48:e4:ba:3e:3c:25:73:13:c6:1a:a7: + e9:4e:b1:8f:6c:7a:a7:12:d2:15:31:b6:8c:30:48:d2:f6:f5: + 59:ee:48:41:02:35:0d:f2:57:76:7d:9a:84:5e:5a:48:cb:1e: + df:a6:3b:20:49:54:23:47:46:6c:71:bb:70:8e:ef:78:3f:5a: + 09:23:01:4a:47:da:a6:5a:51:1b:4d:16:d6:84:87:78:67:43: + bc:0f:b3:c0:3d:50:e4:8c:6e:62:3c:6e:b7:68:e7:ce:88:14: + 01:9f:26:50:2b:86:75:5d:a1:93:be:e6:e3:a6:35:4d:48:86: + 14:5f:5e:51:c2:5f:c3:11:b8:dd:3a:72:2d:4c:43:62:dd:a8: + 2b:d5:9b:0d:4c:76:d1:3d:1d:10:63:de:48:fc:db:81:3e:2f: + 20:59:d4:b3:fe:85:cc:f4:2d:77:e1:53:d9:69:2d:ec:d1:85: + af:b8:ed:de:53:be:c7:88:f6:88:a3:3e:fc:1d:6f:07:cf:bf: + 65:1f:03:6a:90:f5:68:1e:b3:5f:e8:cb:f2:e5:74:90:33:93: + a5:a4:93:db:cd:ad:38:41:0d:16:02:8d:67:68:2b:17:11:3a: + 8b:56:31:83:b3:9b:e2:19:40:e6:76:fa:ef:a0:57:75:8f:3b: + 95:36:af:a9:95:93:16:d5:15:aa:0b:30:0d:f8:f3:96:97:40: + 1a:67:c8:50:bf:9e:aa:e8:ea:57:f9:4a:0b:5c:4e:8a:45:44: + 26:10:79:ff:9e:56:4b:8b:b1:ff:88:ee:23:0a:39:b1:44:b7: + 67:50:cb:ab:c8:5c:dd:64:1e:c0:0c:f5:02:90:0d:22:81:62: + 1d:d3:8d:98:bd:2a:66:37:7b:c8:04:16:95:ef:d0:ac:e0:39: + 64:bb:25:70:8d:67:36:5b:7a:0b:cb:5e:b1:e7:59:2a:30:34: + 2f:95:72:98:d3:b4:ba:48:db:67:18:16:d6:18:3c:e9:6c:4a: + a8:2a:60:84:b7:54:b7:9c:c1:dc:c1:2a:c4:bf:d6:a4:50:2e: + f5:b8:fe:95:cd:8e:a5:ed:bd:c4:42:a1:f8:84:58:e9:0e:c4: + 19:73:d4:92:6f:1d:5f:cd +``` diff --git a/docs/images/architecture.png b/docs/images/architecture.png new file mode 100644 index 0000000..65b99b0 Binary files /dev/null and b/docs/images/architecture.png differ diff --git a/docs/images/baseca_image.png b/docs/images/baseca_image.png new file mode 100644 index 0000000..0c93a7b Binary files /dev/null and b/docs/images/baseca_image.png differ diff --git a/docs/images/configuration.png b/docs/images/configuration.png new file mode 100644 index 0000000..c68059e Binary files /dev/null and b/docs/images/configuration.png differ diff --git a/docs/images/provisioner.png b/docs/images/provisioner.png new file mode 100644 index 0000000..43d27e7 Binary files /dev/null and b/docs/images/provisioner.png differ diff --git a/docs/images/public_key_infrastructure.png b/docs/images/public_key_infrastructure.png new file mode 100644 index 0000000..255ec46 Binary files /dev/null and b/docs/images/public_key_infrastructure.png differ diff --git a/docs/images/secrets_manager.png b/docs/images/secrets_manager.png new file mode 100644 index 0000000..3223151 Binary files /dev/null and b/docs/images/secrets_manager.png differ diff --git a/docs/images/service_authentication.png b/docs/images/service_authentication.png new file mode 100644 index 0000000..fe3ec70 Binary files /dev/null and b/docs/images/service_authentication.png differ diff --git a/docs/images/sign_csr.png b/docs/images/sign_csr.png new file mode 100644 index 0000000..55d35f0 Binary files /dev/null and b/docs/images/sign_csr.png differ diff --git a/docs/images/user_authentication.png b/docs/images/user_authentication.png new file mode 100644 index 0000000..a38eb99 Binary files /dev/null and b/docs/images/user_authentication.png differ diff --git a/examples/Dockerfile b/examples/Dockerfile new file mode 100644 index 0000000..e1a9e91 --- /dev/null +++ b/examples/Dockerfile @@ -0,0 +1,6 @@ +FROM ghcr.io/coinbase/baseca:sha-xxxxx + +COPY ./config /home/baseca/config +USER baseca + +CMD ["/home/baseca/baseca"] diff --git a/examples/certificate/baseca.v1.Certificate/code_sign.go b/examples/certificate/baseca.v1.Certificate/code_sign.go new file mode 100644 index 0000000..1b3956f --- /dev/null +++ b/examples/certificate/baseca.v1.Certificate/code_sign.go @@ -0,0 +1,72 @@ +package examples + +import ( + "crypto/x509" + "fmt" + "os" + + baseca "github.com/coinbase/baseca/pkg/client" + "github.com/coinbase/baseca/pkg/types" +) + +func CodeSign() { + configuration := baseca.Configuration{ + URL: "localhost:9090", + Environment: baseca.Env.Local, + } + + authentication := baseca.Authentication{ + ClientId: "CLIENT_ID", + ClientToken: "CLIENT_TOKEN", + } + + client, err := baseca.LoadDefaultConfiguration(configuration, baseca.Attestation.Local, authentication) + if err != nil { + fmt.Println(err) + } + + metadata := baseca.CertificateRequest{ + CommonName: "sandbox.coinbase.com", + SubjectAlternateNames: []string{"sandbox.coinbase.com"}, + SigningAlgorithm: x509.ECDSAWithSHA384, + PublicKeyAlgorithm: x509.ECDSA, + KeySize: 256, + DistinguishedName: baseca.DistinguishedName{ + Organization: []string{"Coinbase"}, + // Additional Fields + }, + Output: baseca.Output{ + PrivateKey: "/tmp/private.key", + Certificate: "/tmp/certificate.crt", + IntermediateCertificateChain: "/tmp/intermediate_chain.crt", + RootCertificateChain: "/tmp/root_chain.crt", + CertificateSigningRequest: "/tmp/certificate_request.csr", + }, + } + + data, _ := os.ReadFile("/bin/chmod") + signature, chain, err := client.GenerateSignature(metadata, data) + if err != nil { + panic(err) + } + + // Validation Happens on Different Server + manifest := types.Manifest{ + CertificateChain: chain, + Signature: *signature, + Data: data, + SigningAlgorithm: x509.SHA256WithRSA, + } + + tc := types.TrustChain{ + CommonName: "sandbox.coinbase.com", + CertificateAuthorityFiles: []string{"/path/to/intermediate.pem"}, + } + + err = client.ValidateSignature(tc, manifest) + if err != nil { + panic(err) + } + + fmt.Println("Signature Verified") +} diff --git a/examples/certificate/baseca.v1.Certificate/operations_sign_csr.go b/examples/certificate/baseca.v1.Certificate/operations_sign_csr.go new file mode 100644 index 0000000..b1d0e99 --- /dev/null +++ b/examples/certificate/baseca.v1.Certificate/operations_sign_csr.go @@ -0,0 +1,62 @@ +package examples + +import ( + "crypto/x509" + "fmt" + "log" + + apiv1 "github.com/coinbase/baseca/gen/go/baseca/v1" + baseca "github.com/coinbase/baseca/pkg/client" +) + +func OperationsSignCSR() { + configuration := baseca.Configuration{ + URL: "localhost:9090", + Environment: baseca.Env.Local, + } + + authentication := baseca.Authentication{ + ClientId: "CLIENT_ID", + ClientToken: "CLIENT_TOKEN", + } + + client, err := baseca.LoadDefaultConfiguration(configuration, baseca.Attestation.Local, authentication) + if err != nil { + fmt.Println(err) + } + + certAuth := apiv1.CertificateAuthorityParameter{ + Region: "us-east-1", + CaArn: "arn:aws:acm-pca:us-east-1:1123331122:certificate-authority/112311-111231-1123131-11231", + SignAlgorithm: "SHA512WITHRSA", + AssumeRole: false, + Validity: 30, + } + + certificateRequest := baseca.CertificateRequest{ + CommonName: "example.coinbase.com", + SubjectAlternateNames: []string{"example.coinbase.com"}, + SigningAlgorithm: x509.SHA384WithRSA, + PublicKeyAlgorithm: x509.RSA, + KeySize: 4096, + DistinguishedName: baseca.DistinguishedName{ + Organization: []string{"Coinbase"}, + // Additional Fields + }, + Output: baseca.Output{ + PrivateKey: "/tmp/sandbox.key", + CertificateSigningRequest: "/tmp/sandbox.csr", + Certificate: "/tmp/sandbox.crt", + IntermediateCertificateChain: "/tmp/intermediate_chain.crt", + RootCertificateChain: "/tmp/root_chain.crt", + }, + } + + res, err := client.ProvisionIssueCertificate(certificateRequest, &certAuth, "example", "development", "EndEntityServerAuthCertificate") + + if err != nil { + log.Fatal(err) + } + log.Printf("%+v", res) + +} diff --git a/examples/certificate/baseca.v1.Certificate/sign_csr.go b/examples/certificate/baseca.v1.Certificate/sign_csr.go new file mode 100644 index 0000000..8597e1a --- /dev/null +++ b/examples/certificate/baseca.v1.Certificate/sign_csr.go @@ -0,0 +1,52 @@ +package examples + +import ( + "crypto/x509" + "fmt" + "log" + + baseca "github.com/coinbase/baseca/pkg/client" +) + +func SignCSR() { + configuration := baseca.Configuration{ + URL: "localhost:9090", + Environment: baseca.Env.Local, + } + + authentication := baseca.Authentication{ + ClientId: "CLIENT_ID", + ClientToken: "CLIENT_TOKEN", + } + + client, err := baseca.LoadDefaultConfiguration(configuration, baseca.Attestation.Local, authentication) + if err != nil { + fmt.Println(err) + } + + metadata := baseca.CertificateRequest{ + CommonName: "sandbox.coinbase.com", + SubjectAlternateNames: []string{"sandbox.coinbase.com"}, + SigningAlgorithm: x509.SHA384WithRSA, + PublicKeyAlgorithm: x509.RSA, + KeySize: 4096, + DistinguishedName: baseca.DistinguishedName{ + Organization: []string{"Coinbase"}, + // Additional Fields + }, + Output: baseca.Output{ + PrivateKey: "/tmp/private.key", + Certificate: "/tmp/certificate.crt", + IntermediateCertificateChain: "/tmp/intermediate_chain.crt", + RootCertificateChain: "/tmp/root_chain.crt", + CertificateSigningRequest: "/tmp/certificate_request.csr", + }, + } + + response, err := client.IssueCertificate(metadata) + + if err != nil { + log.Fatal(err) + } + log.Printf("%+v", response) +} diff --git a/examples/config/config.primary.development.aws.yml b/examples/config/config.primary.development.aws.yml new file mode 100644 index 0000000..a8394ea --- /dev/null +++ b/examples/config/config.primary.development.aws.yml @@ -0,0 +1,63 @@ +grpc_server_address: 0.0.0.0:9090 + +database: + database_driver: postgres + database_table: baseca + database_endpoint: xxxxxx.cluster.xxxxxx.us-east-1.rds.amazonaws.com + database_reader_endpoint: xxxxxx.cluster-ro.xxxxxx.us-east-1.rds.amazonaws.com + database_user: baseca + database_port: 5432 + region: us-east-1 + ssl_mode: verify-full + +redis: + cluster_endpoint: xxxxxx.xxxxxx.0001.use1.cache.amazonaws.com + port: 6379 + rate_limit: 20 + +domains: + - example.com + +acm_pca: + development_use1: + region: us-east-1 + ca_arn: arn:aws:acm-pca:us-east-1:123456789012:certificate-authority/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + ca_active_day: 90 + assume_role: false + root_ca: false + default: true + development_usw1: + region: us-west-1 + ca_arn: arn:aws:acm-pca:us-west-1:123456789012:certificate-authority/yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy + ca_active_day: 90 + assume_role: false + default: false + +firehose: + stream: baseca-development + region: us-east-1 + +kms: + key_id: 12345678-1234-1234-1234-123456789012 + signing_algorithm: RSASSA_PSS_SHA_512 + region: us-east-1 + auth_validity: 5 + +secrets_manager: + secret_id: baseca-xxxxxxxxxxxx + +subordinate_ca_metadata: + country: "US" + province: "CA" + locality: "San Francisco" + organization: "Example" + organization_unit: "Security" + email: "example@example.com" + signing_algorithm: SHA512WITHRSA + key_algorithm: "RSA" + key_size: 4096 + +certificate_authority: + development: + - development_use1 + - development_usw1 \ No newline at end of file diff --git a/examples/config/config.primary.local.sandbox.yml b/examples/config/config.primary.local.sandbox.yml new file mode 100644 index 0000000..2c3e1f4 --- /dev/null +++ b/examples/config/config.primary.local.sandbox.yml @@ -0,0 +1,48 @@ +grpc_server_address: 0.0.0.0:9090 + +database: + database_driver: postgres + database_table: baseca + database_endpoint: localhost + database_reader_endpoint: localhost + database_user: root + database_port: 5432 + region: us-east-1 + ssl_mode: disable + +domains: + - example.com + +acm_pca: + development_use1: + region: us-east-1 + ca_arn: arn:aws:acm-pca:us-east-1:123456789012:certificate-authority/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + ca_active_day: 90 + assume_role: false + root_ca: false + default: true + +firehose: + stream: baseca-development + region: us-east-1 + +kms: + key_id: 12345678-1234-1234-1234-123456789012 + signing_algorithm: RSASSA_PSS_SHA_512 + region: us-east-1 + auth_validity: 5 + +subordinate_ca_metadata: + country: "US" + province: "CA" + locality: "San Francisco" + organization: "Example" + organization_unit: "Security" + email: "example@example.com" + signing_algorithm: SHA512WITHRSA + key_algorithm: "RSA" + key_size: 4096 + +certificate_authority: + development: + - development_use1 diff --git a/examples/config/config.primary.production.aws.yml b/examples/config/config.primary.production.aws.yml new file mode 100644 index 0000000..b2d2d3a --- /dev/null +++ b/examples/config/config.primary.production.aws.yml @@ -0,0 +1,65 @@ +grpc_server_address: 0.0.0.0:9090 + +ocsp_server: + - production.ocsp.example.com + +database: + database_driver: postgres + database_table: baseca + database_endpoint: xxxxxx.cluster.xxxxxx.us-east-1.rds.amazonaws.com + database_reader_endpoint: xxxxxx.cluster-ro.xxxxxx.us-east-1.rds.amazonaws.com + database_user: baseca + database_port: 5432 + region: us-east-1 + ssl_mode: verify-full + +redis: + cluster_endpoint: xxxxxx.xxxxxx.0001.use1.cache.amazonaws.com + port: 6379 + rate_limit: 20 + +domains: + - example.com + +acm_pca: + production_use1: + region: us-east-1 + ca_arn: arn:aws:acm-pca:us-east-1:123456789012:certificate-authority/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + ca_active_day: 90 + assume_role: false + root_ca: false + default: true + + # Configure Additional Certificate Authorities (development_use1, staging_use1, etc) + +firehose: + stream: baseca-production + region: us-east-1 + +kms: + key_id: 12345678-1234-1234-1234-123456789012 + signing_algorithm: RSASSA_PSS_SHA_512 + region: us-east-1 + auth_validity: 5 + +secrets_manager: + secret_id: baseca-xxxxxxxxxxxx + +subordinate_ca_metadata: + country: "US" + province: "CA" + locality: "San Francisco" + organization: "Example" + organization_unit: "Security" + email: "example@example.com" + signing_algorithm: SHA512WITHRSA + key_algorithm: "RSA" + key_size: 4096 + +certificate_authority: + production: + - production_use1 + pre_production: + - pre_production_use1 + corporate: + - corporate_use1 \ No newline at end of file diff --git a/gen/go/baseca/v1/api.pb.go b/gen/go/baseca/v1/api.pb.go new file mode 100644 index 0000000..576f403 --- /dev/null +++ b/gen/go/baseca/v1/api.pb.go @@ -0,0 +1,4338 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.28.1 +// protoc (unknown) +// source: baseca/v1/api.proto + +package apiv1 + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + emptypb "google.golang.org/protobuf/types/known/emptypb" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type HealthCheckResponse_ServingStatus int32 + +const ( + HealthCheckResponse_UNKNOWN HealthCheckResponse_ServingStatus = 0 + HealthCheckResponse_SERVING HealthCheckResponse_ServingStatus = 1 + HealthCheckResponse_NOT_SERVING HealthCheckResponse_ServingStatus = 2 +) + +// Enum value maps for HealthCheckResponse_ServingStatus. +var ( + HealthCheckResponse_ServingStatus_name = map[int32]string{ + 0: "UNKNOWN", + 1: "SERVING", + 2: "NOT_SERVING", + } + HealthCheckResponse_ServingStatus_value = map[string]int32{ + "UNKNOWN": 0, + "SERVING": 1, + "NOT_SERVING": 2, + } +) + +func (x HealthCheckResponse_ServingStatus) Enum() *HealthCheckResponse_ServingStatus { + p := new(HealthCheckResponse_ServingStatus) + *p = x + return p +} + +func (x HealthCheckResponse_ServingStatus) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (HealthCheckResponse_ServingStatus) Descriptor() protoreflect.EnumDescriptor { + return file_baseca_v1_api_proto_enumTypes[0].Descriptor() +} + +func (HealthCheckResponse_ServingStatus) Type() protoreflect.EnumType { + return &file_baseca_v1_api_proto_enumTypes[0] +} + +func (x HealthCheckResponse_ServingStatus) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use HealthCheckResponse_ServingStatus.Descriptor instead. +func (HealthCheckResponse_ServingStatus) EnumDescriptor() ([]byte, []int) { + return file_baseca_v1_api_proto_rawDescGZIP(), []int{36, 0} +} + +type CertificateParameter struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + SerialNumber string `protobuf:"bytes,1,opt,name=serial_number,json=serialNumber,proto3" json:"serial_number,omitempty"` + CommonName string `protobuf:"bytes,2,opt,name=common_name,json=commonName,proto3" json:"common_name,omitempty"` + SubjectAlternativeName []string `protobuf:"bytes,3,rep,name=subject_alternative_name,json=subjectAlternativeName,proto3" json:"subject_alternative_name,omitempty"` + ExpirationDate *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=expiration_date,json=expirationDate,proto3" json:"expiration_date,omitempty"` + IssuedDate *timestamppb.Timestamp `protobuf:"bytes,5,opt,name=issued_date,json=issuedDate,proto3" json:"issued_date,omitempty"` + Revoked bool `protobuf:"varint,6,opt,name=revoked,proto3" json:"revoked,omitempty"` + RevokedBy string `protobuf:"bytes,7,opt,name=revoked_by,json=revokedBy,proto3" json:"revoked_by,omitempty"` + RevokeDate *timestamppb.Timestamp `protobuf:"bytes,8,opt,name=revoke_date,json=revokeDate,proto3" json:"revoke_date,omitempty"` + CertificateAuthorityArn string `protobuf:"bytes,9,opt,name=certificate_authority_arn,json=certificateAuthorityArn,proto3" json:"certificate_authority_arn,omitempty"` + Account string `protobuf:"bytes,10,opt,name=account,proto3" json:"account,omitempty"` + Environment string `protobuf:"bytes,11,opt,name=environment,proto3" json:"environment,omitempty"` + ExtendedKey string `protobuf:"bytes,12,opt,name=extended_key,json=extendedKey,proto3" json:"extended_key,omitempty"` +} + +func (x *CertificateParameter) Reset() { + *x = CertificateParameter{} + if protoimpl.UnsafeEnabled { + mi := &file_baseca_v1_api_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CertificateParameter) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CertificateParameter) ProtoMessage() {} + +func (x *CertificateParameter) ProtoReflect() protoreflect.Message { + mi := &file_baseca_v1_api_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CertificateParameter.ProtoReflect.Descriptor instead. +func (*CertificateParameter) Descriptor() ([]byte, []int) { + return file_baseca_v1_api_proto_rawDescGZIP(), []int{0} +} + +func (x *CertificateParameter) GetSerialNumber() string { + if x != nil { + return x.SerialNumber + } + return "" +} + +func (x *CertificateParameter) GetCommonName() string { + if x != nil { + return x.CommonName + } + return "" +} + +func (x *CertificateParameter) GetSubjectAlternativeName() []string { + if x != nil { + return x.SubjectAlternativeName + } + return nil +} + +func (x *CertificateParameter) GetExpirationDate() *timestamppb.Timestamp { + if x != nil { + return x.ExpirationDate + } + return nil +} + +func (x *CertificateParameter) GetIssuedDate() *timestamppb.Timestamp { + if x != nil { + return x.IssuedDate + } + return nil +} + +func (x *CertificateParameter) GetRevoked() bool { + if x != nil { + return x.Revoked + } + return false +} + +func (x *CertificateParameter) GetRevokedBy() string { + if x != nil { + return x.RevokedBy + } + return "" +} + +func (x *CertificateParameter) GetRevokeDate() *timestamppb.Timestamp { + if x != nil { + return x.RevokeDate + } + return nil +} + +func (x *CertificateParameter) GetCertificateAuthorityArn() string { + if x != nil { + return x.CertificateAuthorityArn + } + return "" +} + +func (x *CertificateParameter) GetAccount() string { + if x != nil { + return x.Account + } + return "" +} + +func (x *CertificateParameter) GetEnvironment() string { + if x != nil { + return x.Environment + } + return "" +} + +func (x *CertificateParameter) GetExtendedKey() string { + if x != nil { + return x.ExtendedKey + } + return "" +} + +type CertificatesParameter struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Certificates []*CertificateParameter `protobuf:"bytes,1,rep,name=certificates,proto3" json:"certificates,omitempty"` +} + +func (x *CertificatesParameter) Reset() { + *x = CertificatesParameter{} + if protoimpl.UnsafeEnabled { + mi := &file_baseca_v1_api_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CertificatesParameter) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CertificatesParameter) ProtoMessage() {} + +func (x *CertificatesParameter) ProtoReflect() protoreflect.Message { + mi := &file_baseca_v1_api_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CertificatesParameter.ProtoReflect.Descriptor instead. +func (*CertificatesParameter) Descriptor() ([]byte, []int) { + return file_baseca_v1_api_proto_rawDescGZIP(), []int{1} +} + +func (x *CertificatesParameter) GetCertificates() []*CertificateParameter { + if x != nil { + return x.Certificates + } + return nil +} + +type CertificateAuthorityParameter struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Region string `protobuf:"bytes,1,opt,name=region,proto3" json:"region,omitempty"` + CaArn string `protobuf:"bytes,2,opt,name=ca_arn,json=caArn,proto3" json:"ca_arn,omitempty"` + SignAlgorithm string `protobuf:"bytes,3,opt,name=sign_algorithm,json=signAlgorithm,proto3" json:"sign_algorithm,omitempty"` + AssumeRole bool `protobuf:"varint,4,opt,name=assume_role,json=assumeRole,proto3" json:"assume_role,omitempty"` + RoleArn string `protobuf:"bytes,5,opt,name=role_arn,json=roleArn,proto3" json:"role_arn,omitempty"` + Validity int32 `protobuf:"varint,6,opt,name=validity,proto3" json:"validity,omitempty"` +} + +func (x *CertificateAuthorityParameter) Reset() { + *x = CertificateAuthorityParameter{} + if protoimpl.UnsafeEnabled { + mi := &file_baseca_v1_api_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CertificateAuthorityParameter) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CertificateAuthorityParameter) ProtoMessage() {} + +func (x *CertificateAuthorityParameter) ProtoReflect() protoreflect.Message { + mi := &file_baseca_v1_api_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CertificateAuthorityParameter.ProtoReflect.Descriptor instead. +func (*CertificateAuthorityParameter) Descriptor() ([]byte, []int) { + return file_baseca_v1_api_proto_rawDescGZIP(), []int{2} +} + +func (x *CertificateAuthorityParameter) GetRegion() string { + if x != nil { + return x.Region + } + return "" +} + +func (x *CertificateAuthorityParameter) GetCaArn() string { + if x != nil { + return x.CaArn + } + return "" +} + +func (x *CertificateAuthorityParameter) GetSignAlgorithm() string { + if x != nil { + return x.SignAlgorithm + } + return "" +} + +func (x *CertificateAuthorityParameter) GetAssumeRole() bool { + if x != nil { + return x.AssumeRole + } + return false +} + +func (x *CertificateAuthorityParameter) GetRoleArn() string { + if x != nil { + return x.RoleArn + } + return "" +} + +func (x *CertificateAuthorityParameter) GetValidity() int32 { + if x != nil { + return x.Validity + } + return 0 +} + +type CertificateSigningRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + CertificateSigningRequest string `protobuf:"bytes,1,opt,name=certificate_signing_request,json=certificateSigningRequest,proto3" json:"certificate_signing_request,omitempty"` +} + +func (x *CertificateSigningRequest) Reset() { + *x = CertificateSigningRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_baseca_v1_api_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CertificateSigningRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CertificateSigningRequest) ProtoMessage() {} + +func (x *CertificateSigningRequest) ProtoReflect() protoreflect.Message { + mi := &file_baseca_v1_api_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CertificateSigningRequest.ProtoReflect.Descriptor instead. +func (*CertificateSigningRequest) Descriptor() ([]byte, []int) { + return file_baseca_v1_api_proto_rawDescGZIP(), []int{3} +} + +func (x *CertificateSigningRequest) GetCertificateSigningRequest() string { + if x != nil { + return x.CertificateSigningRequest + } + return "" +} + +type SignedCertificate struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Certificate string `protobuf:"bytes,1,opt,name=certificate,proto3" json:"certificate,omitempty"` + CertificateChain string `protobuf:"bytes,2,opt,name=certificate_chain,json=certificateChain,proto3" json:"certificate_chain,omitempty"` + Metadata *CertificateParameter `protobuf:"bytes,3,opt,name=metadata,proto3" json:"metadata,omitempty"` + IntermediateCertificateChain string `protobuf:"bytes,4,opt,name=intermediate_certificate_chain,json=intermediateCertificateChain,proto3" json:"intermediate_certificate_chain,omitempty"` +} + +func (x *SignedCertificate) Reset() { + *x = SignedCertificate{} + if protoimpl.UnsafeEnabled { + mi := &file_baseca_v1_api_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SignedCertificate) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SignedCertificate) ProtoMessage() {} + +func (x *SignedCertificate) ProtoReflect() protoreflect.Message { + mi := &file_baseca_v1_api_proto_msgTypes[4] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SignedCertificate.ProtoReflect.Descriptor instead. +func (*SignedCertificate) Descriptor() ([]byte, []int) { + return file_baseca_v1_api_proto_rawDescGZIP(), []int{4} +} + +func (x *SignedCertificate) GetCertificate() string { + if x != nil { + return x.Certificate + } + return "" +} + +func (x *SignedCertificate) GetCertificateChain() string { + if x != nil { + return x.CertificateChain + } + return "" +} + +func (x *SignedCertificate) GetMetadata() *CertificateParameter { + if x != nil { + return x.Metadata + } + return nil +} + +func (x *SignedCertificate) GetIntermediateCertificateChain() string { + if x != nil { + return x.IntermediateCertificateChain + } + return "" +} + +type CertificateSerialNumber struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + SerialNumber string `protobuf:"bytes,1,opt,name=serial_number,json=serialNumber,proto3" json:"serial_number,omitempty"` +} + +func (x *CertificateSerialNumber) Reset() { + *x = CertificateSerialNumber{} + if protoimpl.UnsafeEnabled { + mi := &file_baseca_v1_api_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CertificateSerialNumber) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CertificateSerialNumber) ProtoMessage() {} + +func (x *CertificateSerialNumber) ProtoReflect() protoreflect.Message { + mi := &file_baseca_v1_api_proto_msgTypes[5] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CertificateSerialNumber.ProtoReflect.Descriptor instead. +func (*CertificateSerialNumber) Descriptor() ([]byte, []int) { + return file_baseca_v1_api_proto_rawDescGZIP(), []int{5} +} + +func (x *CertificateSerialNumber) GetSerialNumber() string { + if x != nil { + return x.SerialNumber + } + return "" +} + +type ListCertificatesRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + CommonName string `protobuf:"bytes,1,opt,name=common_name,json=commonName,proto3" json:"common_name,omitempty"` + PageId int32 `protobuf:"varint,3,opt,name=page_id,json=pageId,proto3" json:"page_id,omitempty"` + PageSize int32 `protobuf:"varint,4,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"` +} + +func (x *ListCertificatesRequest) Reset() { + *x = ListCertificatesRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_baseca_v1_api_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ListCertificatesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListCertificatesRequest) ProtoMessage() {} + +func (x *ListCertificatesRequest) ProtoReflect() protoreflect.Message { + mi := &file_baseca_v1_api_proto_msgTypes[6] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListCertificatesRequest.ProtoReflect.Descriptor instead. +func (*ListCertificatesRequest) Descriptor() ([]byte, []int) { + return file_baseca_v1_api_proto_rawDescGZIP(), []int{6} +} + +func (x *ListCertificatesRequest) GetCommonName() string { + if x != nil { + return x.CommonName + } + return "" +} + +func (x *ListCertificatesRequest) GetPageId() int32 { + if x != nil { + return x.PageId + } + return 0 +} + +func (x *ListCertificatesRequest) GetPageSize() int32 { + if x != nil { + return x.PageSize + } + return 0 +} + +type RevokeCertificateRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + SerialNumber string `protobuf:"bytes,1,opt,name=serial_number,json=serialNumber,proto3" json:"serial_number,omitempty"` + RevocationReason string `protobuf:"bytes,2,opt,name=revocation_reason,json=revocationReason,proto3" json:"revocation_reason,omitempty"` +} + +func (x *RevokeCertificateRequest) Reset() { + *x = RevokeCertificateRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_baseca_v1_api_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RevokeCertificateRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RevokeCertificateRequest) ProtoMessage() {} + +func (x *RevokeCertificateRequest) ProtoReflect() protoreflect.Message { + mi := &file_baseca_v1_api_proto_msgTypes[7] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RevokeCertificateRequest.ProtoReflect.Descriptor instead. +func (*RevokeCertificateRequest) Descriptor() ([]byte, []int) { + return file_baseca_v1_api_proto_rawDescGZIP(), []int{7} +} + +func (x *RevokeCertificateRequest) GetSerialNumber() string { + if x != nil { + return x.SerialNumber + } + return "" +} + +func (x *RevokeCertificateRequest) GetRevocationReason() string { + if x != nil { + return x.RevocationReason + } + return "" +} + +type RevokeCertificateResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + SerialNumber string `protobuf:"bytes,1,opt,name=serial_number,json=serialNumber,proto3" json:"serial_number,omitempty"` + RevocationDate *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=revocation_date,json=revocationDate,proto3" json:"revocation_date,omitempty"` + Status string `protobuf:"bytes,3,opt,name=status,proto3" json:"status,omitempty"` +} + +func (x *RevokeCertificateResponse) Reset() { + *x = RevokeCertificateResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_baseca_v1_api_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RevokeCertificateResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RevokeCertificateResponse) ProtoMessage() {} + +func (x *RevokeCertificateResponse) ProtoReflect() protoreflect.Message { + mi := &file_baseca_v1_api_proto_msgTypes[8] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RevokeCertificateResponse.ProtoReflect.Descriptor instead. +func (*RevokeCertificateResponse) Descriptor() ([]byte, []int) { + return file_baseca_v1_api_proto_rawDescGZIP(), []int{8} +} + +func (x *RevokeCertificateResponse) GetSerialNumber() string { + if x != nil { + return x.SerialNumber + } + return "" +} + +func (x *RevokeCertificateResponse) GetRevocationDate() *timestamppb.Timestamp { + if x != nil { + return x.RevocationDate + } + return nil +} + +func (x *RevokeCertificateResponse) GetStatus() string { + if x != nil { + return x.Status + } + return "" +} + +type OperationsSignRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + CertificateSigningRequest string `protobuf:"bytes,1,opt,name=certificate_signing_request,json=certificateSigningRequest,proto3" json:"certificate_signing_request,omitempty"` + CertificateAuthority *CertificateAuthorityParameter `protobuf:"bytes,2,opt,name=certificate_authority,json=certificateAuthority,proto3,oneof" json:"certificate_authority,omitempty"` + ServiceAccount string `protobuf:"bytes,3,opt,name=service_account,json=serviceAccount,proto3" json:"service_account,omitempty"` + Environment string `protobuf:"bytes,4,opt,name=environment,proto3" json:"environment,omitempty"` + ExtendedKey string `protobuf:"bytes,5,opt,name=extended_key,json=extendedKey,proto3" json:"extended_key,omitempty"` +} + +func (x *OperationsSignRequest) Reset() { + *x = OperationsSignRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_baseca_v1_api_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *OperationsSignRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OperationsSignRequest) ProtoMessage() {} + +func (x *OperationsSignRequest) ProtoReflect() protoreflect.Message { + mi := &file_baseca_v1_api_proto_msgTypes[9] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OperationsSignRequest.ProtoReflect.Descriptor instead. +func (*OperationsSignRequest) Descriptor() ([]byte, []int) { + return file_baseca_v1_api_proto_rawDescGZIP(), []int{9} +} + +func (x *OperationsSignRequest) GetCertificateSigningRequest() string { + if x != nil { + return x.CertificateSigningRequest + } + return "" +} + +func (x *OperationsSignRequest) GetCertificateAuthority() *CertificateAuthorityParameter { + if x != nil { + return x.CertificateAuthority + } + return nil +} + +func (x *OperationsSignRequest) GetServiceAccount() string { + if x != nil { + return x.ServiceAccount + } + return "" +} + +func (x *OperationsSignRequest) GetEnvironment() string { + if x != nil { + return x.Environment + } + return "" +} + +func (x *OperationsSignRequest) GetExtendedKey() string { + if x != nil { + return x.ExtendedKey + } + return "" +} + +type Environment struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Environment string `protobuf:"bytes,1,opt,name=environment,proto3" json:"environment,omitempty"` +} + +func (x *Environment) Reset() { + *x = Environment{} + if protoimpl.UnsafeEnabled { + mi := &file_baseca_v1_api_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Environment) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Environment) ProtoMessage() {} + +func (x *Environment) ProtoReflect() protoreflect.Message { + mi := &file_baseca_v1_api_proto_msgTypes[10] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Environment.ProtoReflect.Descriptor instead. +func (*Environment) Descriptor() ([]byte, []int) { + return file_baseca_v1_api_proto_rawDescGZIP(), []int{10} +} + +func (x *Environment) GetEnvironment() string { + if x != nil { + return x.Environment + } + return "" +} + +type QueryCertificateMetadataRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + SerialNumber string `protobuf:"bytes,1,opt,name=serial_number,json=serialNumber,proto3" json:"serial_number,omitempty"` + Account string `protobuf:"bytes,2,opt,name=account,proto3" json:"account,omitempty"` + Environment string `protobuf:"bytes,3,opt,name=environment,proto3" json:"environment,omitempty"` + ExtendedKey string `protobuf:"bytes,4,opt,name=extended_key,json=extendedKey,proto3" json:"extended_key,omitempty"` + SubjectAlternativeName []string `protobuf:"bytes,5,rep,name=subject_alternative_name,json=subjectAlternativeName,proto3" json:"subject_alternative_name,omitempty"` +} + +func (x *QueryCertificateMetadataRequest) Reset() { + *x = QueryCertificateMetadataRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_baseca_v1_api_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *QueryCertificateMetadataRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*QueryCertificateMetadataRequest) ProtoMessage() {} + +func (x *QueryCertificateMetadataRequest) ProtoReflect() protoreflect.Message { + mi := &file_baseca_v1_api_proto_msgTypes[11] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use QueryCertificateMetadataRequest.ProtoReflect.Descriptor instead. +func (*QueryCertificateMetadataRequest) Descriptor() ([]byte, []int) { + return file_baseca_v1_api_proto_rawDescGZIP(), []int{11} +} + +func (x *QueryCertificateMetadataRequest) GetSerialNumber() string { + if x != nil { + return x.SerialNumber + } + return "" +} + +func (x *QueryCertificateMetadataRequest) GetAccount() string { + if x != nil { + return x.Account + } + return "" +} + +func (x *QueryCertificateMetadataRequest) GetEnvironment() string { + if x != nil { + return x.Environment + } + return "" +} + +func (x *QueryCertificateMetadataRequest) GetExtendedKey() string { + if x != nil { + return x.ExtendedKey + } + return "" +} + +func (x *QueryCertificateMetadataRequest) GetSubjectAlternativeName() []string { + if x != nil { + return x.SubjectAlternativeName + } + return nil +} + +type User struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Uuid string `protobuf:"bytes,1,opt,name=uuid,proto3" json:"uuid,omitempty"` + Username string `protobuf:"bytes,2,opt,name=username,proto3" json:"username,omitempty"` + FullName string `protobuf:"bytes,3,opt,name=full_name,json=fullName,proto3" json:"full_name,omitempty"` + Email string `protobuf:"bytes,4,opt,name=email,proto3" json:"email,omitempty"` + Permissions string `protobuf:"bytes,5,opt,name=permissions,proto3" json:"permissions,omitempty"` + CredentialChangedAt *timestamppb.Timestamp `protobuf:"bytes,6,opt,name=credential_changed_at,json=credentialChangedAt,proto3" json:"credential_changed_at,omitempty"` + CreatedAt *timestamppb.Timestamp `protobuf:"bytes,7,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` +} + +func (x *User) Reset() { + *x = User{} + if protoimpl.UnsafeEnabled { + mi := &file_baseca_v1_api_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *User) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*User) ProtoMessage() {} + +func (x *User) ProtoReflect() protoreflect.Message { + mi := &file_baseca_v1_api_proto_msgTypes[12] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use User.ProtoReflect.Descriptor instead. +func (*User) Descriptor() ([]byte, []int) { + return file_baseca_v1_api_proto_rawDescGZIP(), []int{12} +} + +func (x *User) GetUuid() string { + if x != nil { + return x.Uuid + } + return "" +} + +func (x *User) GetUsername() string { + if x != nil { + return x.Username + } + return "" +} + +func (x *User) GetFullName() string { + if x != nil { + return x.FullName + } + return "" +} + +func (x *User) GetEmail() string { + if x != nil { + return x.Email + } + return "" +} + +func (x *User) GetPermissions() string { + if x != nil { + return x.Permissions + } + return "" +} + +func (x *User) GetCredentialChangedAt() *timestamppb.Timestamp { + if x != nil { + return x.CredentialChangedAt + } + return nil +} + +func (x *User) GetCreatedAt() *timestamppb.Timestamp { + if x != nil { + return x.CreatedAt + } + return nil +} + +type Users struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Users []*User `protobuf:"bytes,1,rep,name=users,proto3" json:"users,omitempty"` +} + +func (x *Users) Reset() { + *x = Users{} + if protoimpl.UnsafeEnabled { + mi := &file_baseca_v1_api_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Users) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Users) ProtoMessage() {} + +func (x *Users) ProtoReflect() protoreflect.Message { + mi := &file_baseca_v1_api_proto_msgTypes[13] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Users.ProtoReflect.Descriptor instead. +func (*Users) Descriptor() ([]byte, []int) { + return file_baseca_v1_api_proto_rawDescGZIP(), []int{13} +} + +func (x *Users) GetUsers() []*User { + if x != nil { + return x.Users + } + return nil +} + +type LoginUserRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Username string `protobuf:"bytes,1,opt,name=username,proto3" json:"username,omitempty"` + Password string `protobuf:"bytes,2,opt,name=password,proto3" json:"password,omitempty"` +} + +func (x *LoginUserRequest) Reset() { + *x = LoginUserRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_baseca_v1_api_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *LoginUserRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LoginUserRequest) ProtoMessage() {} + +func (x *LoginUserRequest) ProtoReflect() protoreflect.Message { + mi := &file_baseca_v1_api_proto_msgTypes[14] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LoginUserRequest.ProtoReflect.Descriptor instead. +func (*LoginUserRequest) Descriptor() ([]byte, []int) { + return file_baseca_v1_api_proto_rawDescGZIP(), []int{14} +} + +func (x *LoginUserRequest) GetUsername() string { + if x != nil { + return x.Username + } + return "" +} + +func (x *LoginUserRequest) GetPassword() string { + if x != nil { + return x.Password + } + return "" +} + +type LoginUserResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + AccessToken string `protobuf:"bytes,1,opt,name=access_token,json=accessToken,proto3" json:"access_token,omitempty"` + User *User `protobuf:"bytes,2,opt,name=user,proto3" json:"user,omitempty"` +} + +func (x *LoginUserResponse) Reset() { + *x = LoginUserResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_baseca_v1_api_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *LoginUserResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LoginUserResponse) ProtoMessage() {} + +func (x *LoginUserResponse) ProtoReflect() protoreflect.Message { + mi := &file_baseca_v1_api_proto_msgTypes[15] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LoginUserResponse.ProtoReflect.Descriptor instead. +func (*LoginUserResponse) Descriptor() ([]byte, []int) { + return file_baseca_v1_api_proto_rawDescGZIP(), []int{15} +} + +func (x *LoginUserResponse) GetAccessToken() string { + if x != nil { + return x.AccessToken + } + return "" +} + +func (x *LoginUserResponse) GetUser() *User { + if x != nil { + return x.User + } + return nil +} + +type UsernameRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Username string `protobuf:"bytes,1,opt,name=username,proto3" json:"username,omitempty"` +} + +func (x *UsernameRequest) Reset() { + *x = UsernameRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_baseca_v1_api_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *UsernameRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UsernameRequest) ProtoMessage() {} + +func (x *UsernameRequest) ProtoReflect() protoreflect.Message { + mi := &file_baseca_v1_api_proto_msgTypes[16] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UsernameRequest.ProtoReflect.Descriptor instead. +func (*UsernameRequest) Descriptor() ([]byte, []int) { + return file_baseca_v1_api_proto_rawDescGZIP(), []int{16} +} + +func (x *UsernameRequest) GetUsername() string { + if x != nil { + return x.Username + } + return "" +} + +type QueryParameter struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + PageId int32 `protobuf:"varint,2,opt,name=page_id,json=pageId,proto3" json:"page_id,omitempty"` + PageSize int32 `protobuf:"varint,3,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"` +} + +func (x *QueryParameter) Reset() { + *x = QueryParameter{} + if protoimpl.UnsafeEnabled { + mi := &file_baseca_v1_api_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *QueryParameter) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*QueryParameter) ProtoMessage() {} + +func (x *QueryParameter) ProtoReflect() protoreflect.Message { + mi := &file_baseca_v1_api_proto_msgTypes[17] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use QueryParameter.ProtoReflect.Descriptor instead. +func (*QueryParameter) Descriptor() ([]byte, []int) { + return file_baseca_v1_api_proto_rawDescGZIP(), []int{17} +} + +func (x *QueryParameter) GetPageId() int32 { + if x != nil { + return x.PageId + } + return 0 +} + +func (x *QueryParameter) GetPageSize() int32 { + if x != nil { + return x.PageSize + } + return 0 +} + +type CreateUserRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Username string `protobuf:"bytes,1,opt,name=username,proto3" json:"username,omitempty"` + Password string `protobuf:"bytes,2,opt,name=password,proto3" json:"password,omitempty"` + FullName string `protobuf:"bytes,3,opt,name=full_name,json=fullName,proto3" json:"full_name,omitempty"` + Email string `protobuf:"bytes,4,opt,name=email,proto3" json:"email,omitempty"` + Permissions string `protobuf:"bytes,5,opt,name=permissions,proto3" json:"permissions,omitempty"` +} + +func (x *CreateUserRequest) Reset() { + *x = CreateUserRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_baseca_v1_api_proto_msgTypes[18] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CreateUserRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateUserRequest) ProtoMessage() {} + +func (x *CreateUserRequest) ProtoReflect() protoreflect.Message { + mi := &file_baseca_v1_api_proto_msgTypes[18] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateUserRequest.ProtoReflect.Descriptor instead. +func (*CreateUserRequest) Descriptor() ([]byte, []int) { + return file_baseca_v1_api_proto_rawDescGZIP(), []int{18} +} + +func (x *CreateUserRequest) GetUsername() string { + if x != nil { + return x.Username + } + return "" +} + +func (x *CreateUserRequest) GetPassword() string { + if x != nil { + return x.Password + } + return "" +} + +func (x *CreateUserRequest) GetFullName() string { + if x != nil { + return x.FullName + } + return "" +} + +func (x *CreateUserRequest) GetEmail() string { + if x != nil { + return x.Email + } + return "" +} + +func (x *CreateUserRequest) GetPermissions() string { + if x != nil { + return x.Permissions + } + return "" +} + +type UpdateCredentialsRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Username string `protobuf:"bytes,1,opt,name=username,proto3" json:"username,omitempty"` + Password string `protobuf:"bytes,2,opt,name=password,proto3" json:"password,omitempty"` + UpdatedPassword string `protobuf:"bytes,3,opt,name=updated_password,json=updatedPassword,proto3" json:"updated_password,omitempty"` +} + +func (x *UpdateCredentialsRequest) Reset() { + *x = UpdateCredentialsRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_baseca_v1_api_proto_msgTypes[19] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *UpdateCredentialsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateCredentialsRequest) ProtoMessage() {} + +func (x *UpdateCredentialsRequest) ProtoReflect() protoreflect.Message { + mi := &file_baseca_v1_api_proto_msgTypes[19] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpdateCredentialsRequest.ProtoReflect.Descriptor instead. +func (*UpdateCredentialsRequest) Descriptor() ([]byte, []int) { + return file_baseca_v1_api_proto_rawDescGZIP(), []int{19} +} + +func (x *UpdateCredentialsRequest) GetUsername() string { + if x != nil { + return x.Username + } + return "" +} + +func (x *UpdateCredentialsRequest) GetPassword() string { + if x != nil { + return x.Password + } + return "" +} + +func (x *UpdateCredentialsRequest) GetUpdatedPassword() string { + if x != nil { + return x.UpdatedPassword + } + return "" +} + +type UpdatePermissionsRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Username string `protobuf:"bytes,1,opt,name=username,proto3" json:"username,omitempty"` + Permissions string `protobuf:"bytes,2,opt,name=permissions,proto3" json:"permissions,omitempty"` +} + +func (x *UpdatePermissionsRequest) Reset() { + *x = UpdatePermissionsRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_baseca_v1_api_proto_msgTypes[20] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *UpdatePermissionsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdatePermissionsRequest) ProtoMessage() {} + +func (x *UpdatePermissionsRequest) ProtoReflect() protoreflect.Message { + mi := &file_baseca_v1_api_proto_msgTypes[20] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpdatePermissionsRequest.ProtoReflect.Descriptor instead. +func (*UpdatePermissionsRequest) Descriptor() ([]byte, []int) { + return file_baseca_v1_api_proto_rawDescGZIP(), []int{20} +} + +func (x *UpdatePermissionsRequest) GetUsername() string { + if x != nil { + return x.Username + } + return "" +} + +func (x *UpdatePermissionsRequest) GetPermissions() string { + if x != nil { + return x.Permissions + } + return "" +} + +type CreateServiceAccountRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + ServiceAccount string `protobuf:"bytes,1,opt,name=service_account,json=serviceAccount,proto3" json:"service_account,omitempty"` + Environment string `protobuf:"bytes,2,opt,name=environment,proto3" json:"environment,omitempty"` + RegularExpression *string `protobuf:"bytes,3,opt,name=regular_expression,json=regularExpression,proto3,oneof" json:"regular_expression,omitempty"` + SubjectAlternativeNames []string `protobuf:"bytes,4,rep,name=subject_alternative_names,json=subjectAlternativeNames,proto3" json:"subject_alternative_names,omitempty"` + CertificateAuthorities []string `protobuf:"bytes,5,rep,name=certificate_authorities,json=certificateAuthorities,proto3" json:"certificate_authorities,omitempty"` + ExtendedKey string `protobuf:"bytes,6,opt,name=extended_key,json=extendedKey,proto3" json:"extended_key,omitempty"` + CertificateValidity int32 `protobuf:"varint,7,opt,name=certificate_validity,json=certificateValidity,proto3" json:"certificate_validity,omitempty"` + SubordinateCa string `protobuf:"bytes,8,opt,name=subordinate_ca,json=subordinateCa,proto3" json:"subordinate_ca,omitempty"` + NodeAttestation *NodeAttestation `protobuf:"bytes,9,opt,name=node_attestation,json=nodeAttestation,proto3,oneof" json:"node_attestation,omitempty"` + Team string `protobuf:"bytes,10,opt,name=team,proto3" json:"team,omitempty"` + Email string `protobuf:"bytes,11,opt,name=email,proto3" json:"email,omitempty"` +} + +func (x *CreateServiceAccountRequest) Reset() { + *x = CreateServiceAccountRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_baseca_v1_api_proto_msgTypes[21] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CreateServiceAccountRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateServiceAccountRequest) ProtoMessage() {} + +func (x *CreateServiceAccountRequest) ProtoReflect() protoreflect.Message { + mi := &file_baseca_v1_api_proto_msgTypes[21] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateServiceAccountRequest.ProtoReflect.Descriptor instead. +func (*CreateServiceAccountRequest) Descriptor() ([]byte, []int) { + return file_baseca_v1_api_proto_rawDescGZIP(), []int{21} +} + +func (x *CreateServiceAccountRequest) GetServiceAccount() string { + if x != nil { + return x.ServiceAccount + } + return "" +} + +func (x *CreateServiceAccountRequest) GetEnvironment() string { + if x != nil { + return x.Environment + } + return "" +} + +func (x *CreateServiceAccountRequest) GetRegularExpression() string { + if x != nil && x.RegularExpression != nil { + return *x.RegularExpression + } + return "" +} + +func (x *CreateServiceAccountRequest) GetSubjectAlternativeNames() []string { + if x != nil { + return x.SubjectAlternativeNames + } + return nil +} + +func (x *CreateServiceAccountRequest) GetCertificateAuthorities() []string { + if x != nil { + return x.CertificateAuthorities + } + return nil +} + +func (x *CreateServiceAccountRequest) GetExtendedKey() string { + if x != nil { + return x.ExtendedKey + } + return "" +} + +func (x *CreateServiceAccountRequest) GetCertificateValidity() int32 { + if x != nil { + return x.CertificateValidity + } + return 0 +} + +func (x *CreateServiceAccountRequest) GetSubordinateCa() string { + if x != nil { + return x.SubordinateCa + } + return "" +} + +func (x *CreateServiceAccountRequest) GetNodeAttestation() *NodeAttestation { + if x != nil { + return x.NodeAttestation + } + return nil +} + +func (x *CreateServiceAccountRequest) GetTeam() string { + if x != nil { + return x.Team + } + return "" +} + +func (x *CreateServiceAccountRequest) GetEmail() string { + if x != nil { + return x.Email + } + return "" +} + +type CreateServiceAccountResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + ClientId string `protobuf:"bytes,1,opt,name=client_id,json=clientId,proto3" json:"client_id,omitempty"` + ClientToken string `protobuf:"bytes,2,opt,name=client_token,json=clientToken,proto3" json:"client_token,omitempty"` + ServiceAccount string `protobuf:"bytes,3,opt,name=service_account,json=serviceAccount,proto3" json:"service_account,omitempty"` + Environment string `protobuf:"bytes,4,opt,name=environment,proto3" json:"environment,omitempty"` + RegularExpression string `protobuf:"bytes,5,opt,name=regular_expression,json=regularExpression,proto3" json:"regular_expression,omitempty"` + SubjectAlternativeNames []string `protobuf:"bytes,6,rep,name=subject_alternative_names,json=subjectAlternativeNames,proto3" json:"subject_alternative_names,omitempty"` + CertificateAuthorities []string `protobuf:"bytes,7,rep,name=certificate_authorities,json=certificateAuthorities,proto3" json:"certificate_authorities,omitempty"` + ExtendedKey string `protobuf:"bytes,8,opt,name=extended_key,json=extendedKey,proto3" json:"extended_key,omitempty"` + NodeAttestation *NodeAttestation `protobuf:"bytes,9,opt,name=node_attestation,json=nodeAttestation,proto3" json:"node_attestation,omitempty"` + CertificateValidity int32 `protobuf:"varint,10,opt,name=certificate_validity,json=certificateValidity,proto3" json:"certificate_validity,omitempty"` + SubordinateCa string `protobuf:"bytes,11,opt,name=subordinate_ca,json=subordinateCa,proto3" json:"subordinate_ca,omitempty"` + Team string `protobuf:"bytes,12,opt,name=team,proto3" json:"team,omitempty"` + Email string `protobuf:"bytes,13,opt,name=email,proto3" json:"email,omitempty"` + CreatedAt *timestamppb.Timestamp `protobuf:"bytes,14,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` + CreatedBy string `protobuf:"bytes,15,opt,name=created_by,json=createdBy,proto3" json:"created_by,omitempty"` +} + +func (x *CreateServiceAccountResponse) Reset() { + *x = CreateServiceAccountResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_baseca_v1_api_proto_msgTypes[22] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CreateServiceAccountResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateServiceAccountResponse) ProtoMessage() {} + +func (x *CreateServiceAccountResponse) ProtoReflect() protoreflect.Message { + mi := &file_baseca_v1_api_proto_msgTypes[22] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateServiceAccountResponse.ProtoReflect.Descriptor instead. +func (*CreateServiceAccountResponse) Descriptor() ([]byte, []int) { + return file_baseca_v1_api_proto_rawDescGZIP(), []int{22} +} + +func (x *CreateServiceAccountResponse) GetClientId() string { + if x != nil { + return x.ClientId + } + return "" +} + +func (x *CreateServiceAccountResponse) GetClientToken() string { + if x != nil { + return x.ClientToken + } + return "" +} + +func (x *CreateServiceAccountResponse) GetServiceAccount() string { + if x != nil { + return x.ServiceAccount + } + return "" +} + +func (x *CreateServiceAccountResponse) GetEnvironment() string { + if x != nil { + return x.Environment + } + return "" +} + +func (x *CreateServiceAccountResponse) GetRegularExpression() string { + if x != nil { + return x.RegularExpression + } + return "" +} + +func (x *CreateServiceAccountResponse) GetSubjectAlternativeNames() []string { + if x != nil { + return x.SubjectAlternativeNames + } + return nil +} + +func (x *CreateServiceAccountResponse) GetCertificateAuthorities() []string { + if x != nil { + return x.CertificateAuthorities + } + return nil +} + +func (x *CreateServiceAccountResponse) GetExtendedKey() string { + if x != nil { + return x.ExtendedKey + } + return "" +} + +func (x *CreateServiceAccountResponse) GetNodeAttestation() *NodeAttestation { + if x != nil { + return x.NodeAttestation + } + return nil +} + +func (x *CreateServiceAccountResponse) GetCertificateValidity() int32 { + if x != nil { + return x.CertificateValidity + } + return 0 +} + +func (x *CreateServiceAccountResponse) GetSubordinateCa() string { + if x != nil { + return x.SubordinateCa + } + return "" +} + +func (x *CreateServiceAccountResponse) GetTeam() string { + if x != nil { + return x.Team + } + return "" +} + +func (x *CreateServiceAccountResponse) GetEmail() string { + if x != nil { + return x.Email + } + return "" +} + +func (x *CreateServiceAccountResponse) GetCreatedAt() *timestamppb.Timestamp { + if x != nil { + return x.CreatedAt + } + return nil +} + +func (x *CreateServiceAccountResponse) GetCreatedBy() string { + if x != nil { + return x.CreatedBy + } + return "" +} + +type ServiceAccount struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + ClientId string `protobuf:"bytes,1,opt,name=client_id,json=clientId,proto3" json:"client_id,omitempty"` + ServiceAccount string `protobuf:"bytes,2,opt,name=service_account,json=serviceAccount,proto3" json:"service_account,omitempty"` + Environment string `protobuf:"bytes,3,opt,name=environment,proto3" json:"environment,omitempty"` + RegularExpression string `protobuf:"bytes,4,opt,name=regular_expression,json=regularExpression,proto3" json:"regular_expression,omitempty"` + SubjectAlternativeNames []string `protobuf:"bytes,5,rep,name=subject_alternative_names,json=subjectAlternativeNames,proto3" json:"subject_alternative_names,omitempty"` + CertificateAuthorities []string `protobuf:"bytes,6,rep,name=certificate_authorities,json=certificateAuthorities,proto3" json:"certificate_authorities,omitempty"` + ExtendedKey string `protobuf:"bytes,7,opt,name=extended_key,json=extendedKey,proto3" json:"extended_key,omitempty"` + NodeAttestation *NodeAttestation `protobuf:"bytes,8,opt,name=node_attestation,json=nodeAttestation,proto3" json:"node_attestation,omitempty"` + CertificateValidity int32 `protobuf:"varint,9,opt,name=certificate_validity,json=certificateValidity,proto3" json:"certificate_validity,omitempty"` + SubordinateCa string `protobuf:"bytes,10,opt,name=subordinate_ca,json=subordinateCa,proto3" json:"subordinate_ca,omitempty"` + Provisioned bool `protobuf:"varint,11,opt,name=provisioned,proto3" json:"provisioned,omitempty"` + Team string `protobuf:"bytes,12,opt,name=team,proto3" json:"team,omitempty"` + Email string `protobuf:"bytes,13,opt,name=email,proto3" json:"email,omitempty"` + CreatedAt *timestamppb.Timestamp `protobuf:"bytes,14,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` + CreatedBy string `protobuf:"bytes,15,opt,name=created_by,json=createdBy,proto3" json:"created_by,omitempty"` +} + +func (x *ServiceAccount) Reset() { + *x = ServiceAccount{} + if protoimpl.UnsafeEnabled { + mi := &file_baseca_v1_api_proto_msgTypes[23] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ServiceAccount) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ServiceAccount) ProtoMessage() {} + +func (x *ServiceAccount) ProtoReflect() protoreflect.Message { + mi := &file_baseca_v1_api_proto_msgTypes[23] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ServiceAccount.ProtoReflect.Descriptor instead. +func (*ServiceAccount) Descriptor() ([]byte, []int) { + return file_baseca_v1_api_proto_rawDescGZIP(), []int{23} +} + +func (x *ServiceAccount) GetClientId() string { + if x != nil { + return x.ClientId + } + return "" +} + +func (x *ServiceAccount) GetServiceAccount() string { + if x != nil { + return x.ServiceAccount + } + return "" +} + +func (x *ServiceAccount) GetEnvironment() string { + if x != nil { + return x.Environment + } + return "" +} + +func (x *ServiceAccount) GetRegularExpression() string { + if x != nil { + return x.RegularExpression + } + return "" +} + +func (x *ServiceAccount) GetSubjectAlternativeNames() []string { + if x != nil { + return x.SubjectAlternativeNames + } + return nil +} + +func (x *ServiceAccount) GetCertificateAuthorities() []string { + if x != nil { + return x.CertificateAuthorities + } + return nil +} + +func (x *ServiceAccount) GetExtendedKey() string { + if x != nil { + return x.ExtendedKey + } + return "" +} + +func (x *ServiceAccount) GetNodeAttestation() *NodeAttestation { + if x != nil { + return x.NodeAttestation + } + return nil +} + +func (x *ServiceAccount) GetCertificateValidity() int32 { + if x != nil { + return x.CertificateValidity + } + return 0 +} + +func (x *ServiceAccount) GetSubordinateCa() string { + if x != nil { + return x.SubordinateCa + } + return "" +} + +func (x *ServiceAccount) GetProvisioned() bool { + if x != nil { + return x.Provisioned + } + return false +} + +func (x *ServiceAccount) GetTeam() string { + if x != nil { + return x.Team + } + return "" +} + +func (x *ServiceAccount) GetEmail() string { + if x != nil { + return x.Email + } + return "" +} + +func (x *ServiceAccount) GetCreatedAt() *timestamppb.Timestamp { + if x != nil { + return x.CreatedAt + } + return nil +} + +func (x *ServiceAccount) GetCreatedBy() string { + if x != nil { + return x.CreatedBy + } + return "" +} + +type ServiceAccounts struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + ServiceAccounts []*ServiceAccount `protobuf:"bytes,1,rep,name=service_accounts,json=serviceAccounts,proto3" json:"service_accounts,omitempty"` +} + +func (x *ServiceAccounts) Reset() { + *x = ServiceAccounts{} + if protoimpl.UnsafeEnabled { + mi := &file_baseca_v1_api_proto_msgTypes[24] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ServiceAccounts) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ServiceAccounts) ProtoMessage() {} + +func (x *ServiceAccounts) ProtoReflect() protoreflect.Message { + mi := &file_baseca_v1_api_proto_msgTypes[24] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ServiceAccounts.ProtoReflect.Descriptor instead. +func (*ServiceAccounts) Descriptor() ([]byte, []int) { + return file_baseca_v1_api_proto_rawDescGZIP(), []int{24} +} + +func (x *ServiceAccounts) GetServiceAccounts() []*ServiceAccount { + if x != nil { + return x.ServiceAccounts + } + return nil +} + +type CreateProvisionerAccountRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + ProvisionerAccount string `protobuf:"bytes,1,opt,name=provisioner_account,json=provisionerAccount,proto3" json:"provisioner_account,omitempty"` + Environments []string `protobuf:"bytes,2,rep,name=environments,proto3" json:"environments,omitempty"` + RegularExpression string `protobuf:"bytes,3,opt,name=regular_expression,json=regularExpression,proto3" json:"regular_expression,omitempty"` + SubjectAlternativeNames []string `protobuf:"bytes,4,rep,name=subject_alternative_names,json=subjectAlternativeNames,proto3" json:"subject_alternative_names,omitempty"` + ExtendedKeys []string `protobuf:"bytes,5,rep,name=extended_keys,json=extendedKeys,proto3" json:"extended_keys,omitempty"` + MaxCertificateValidity uint32 `protobuf:"varint,6,opt,name=max_certificate_validity,json=maxCertificateValidity,proto3" json:"max_certificate_validity,omitempty"` + NodeAttestation *NodeAttestation `protobuf:"bytes,7,opt,name=node_attestation,json=nodeAttestation,proto3,oneof" json:"node_attestation,omitempty"` + Team string `protobuf:"bytes,8,opt,name=team,proto3" json:"team,omitempty"` + Email string `protobuf:"bytes,9,opt,name=email,proto3" json:"email,omitempty"` +} + +func (x *CreateProvisionerAccountRequest) Reset() { + *x = CreateProvisionerAccountRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_baseca_v1_api_proto_msgTypes[25] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CreateProvisionerAccountRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateProvisionerAccountRequest) ProtoMessage() {} + +func (x *CreateProvisionerAccountRequest) ProtoReflect() protoreflect.Message { + mi := &file_baseca_v1_api_proto_msgTypes[25] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateProvisionerAccountRequest.ProtoReflect.Descriptor instead. +func (*CreateProvisionerAccountRequest) Descriptor() ([]byte, []int) { + return file_baseca_v1_api_proto_rawDescGZIP(), []int{25} +} + +func (x *CreateProvisionerAccountRequest) GetProvisionerAccount() string { + if x != nil { + return x.ProvisionerAccount + } + return "" +} + +func (x *CreateProvisionerAccountRequest) GetEnvironments() []string { + if x != nil { + return x.Environments + } + return nil +} + +func (x *CreateProvisionerAccountRequest) GetRegularExpression() string { + if x != nil { + return x.RegularExpression + } + return "" +} + +func (x *CreateProvisionerAccountRequest) GetSubjectAlternativeNames() []string { + if x != nil { + return x.SubjectAlternativeNames + } + return nil +} + +func (x *CreateProvisionerAccountRequest) GetExtendedKeys() []string { + if x != nil { + return x.ExtendedKeys + } + return nil +} + +func (x *CreateProvisionerAccountRequest) GetMaxCertificateValidity() uint32 { + if x != nil { + return x.MaxCertificateValidity + } + return 0 +} + +func (x *CreateProvisionerAccountRequest) GetNodeAttestation() *NodeAttestation { + if x != nil { + return x.NodeAttestation + } + return nil +} + +func (x *CreateProvisionerAccountRequest) GetTeam() string { + if x != nil { + return x.Team + } + return "" +} + +func (x *CreateProvisionerAccountRequest) GetEmail() string { + if x != nil { + return x.Email + } + return "" +} + +type CreateProvisionerAccountResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + ClientId string `protobuf:"bytes,1,opt,name=client_id,json=clientId,proto3" json:"client_id,omitempty"` + ClientToken string `protobuf:"bytes,2,opt,name=client_token,json=clientToken,proto3" json:"client_token,omitempty"` + ProvisionerAccount string `protobuf:"bytes,3,opt,name=provisioner_account,json=provisionerAccount,proto3" json:"provisioner_account,omitempty"` + Environments []string `protobuf:"bytes,4,rep,name=environments,proto3" json:"environments,omitempty"` + RegularExpression string `protobuf:"bytes,5,opt,name=regular_expression,json=regularExpression,proto3" json:"regular_expression,omitempty"` + SubjectAlternativeNames []string `protobuf:"bytes,6,rep,name=subject_alternative_names,json=subjectAlternativeNames,proto3" json:"subject_alternative_names,omitempty"` + ExtendedKeys []string `protobuf:"bytes,8,rep,name=extended_keys,json=extendedKeys,proto3" json:"extended_keys,omitempty"` + NodeAttestation *NodeAttestation `protobuf:"bytes,9,opt,name=node_attestation,json=nodeAttestation,proto3" json:"node_attestation,omitempty"` + MaxCertificateValidity uint32 `protobuf:"varint,10,opt,name=max_certificate_validity,json=maxCertificateValidity,proto3" json:"max_certificate_validity,omitempty"` + Team string `protobuf:"bytes,11,opt,name=team,proto3" json:"team,omitempty"` + Email string `protobuf:"bytes,12,opt,name=email,proto3" json:"email,omitempty"` + CreatedAt *timestamppb.Timestamp `protobuf:"bytes,13,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` + CreatedBy string `protobuf:"bytes,14,opt,name=created_by,json=createdBy,proto3" json:"created_by,omitempty"` +} + +func (x *CreateProvisionerAccountResponse) Reset() { + *x = CreateProvisionerAccountResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_baseca_v1_api_proto_msgTypes[26] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CreateProvisionerAccountResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateProvisionerAccountResponse) ProtoMessage() {} + +func (x *CreateProvisionerAccountResponse) ProtoReflect() protoreflect.Message { + mi := &file_baseca_v1_api_proto_msgTypes[26] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateProvisionerAccountResponse.ProtoReflect.Descriptor instead. +func (*CreateProvisionerAccountResponse) Descriptor() ([]byte, []int) { + return file_baseca_v1_api_proto_rawDescGZIP(), []int{26} +} + +func (x *CreateProvisionerAccountResponse) GetClientId() string { + if x != nil { + return x.ClientId + } + return "" +} + +func (x *CreateProvisionerAccountResponse) GetClientToken() string { + if x != nil { + return x.ClientToken + } + return "" +} + +func (x *CreateProvisionerAccountResponse) GetProvisionerAccount() string { + if x != nil { + return x.ProvisionerAccount + } + return "" +} + +func (x *CreateProvisionerAccountResponse) GetEnvironments() []string { + if x != nil { + return x.Environments + } + return nil +} + +func (x *CreateProvisionerAccountResponse) GetRegularExpression() string { + if x != nil { + return x.RegularExpression + } + return "" +} + +func (x *CreateProvisionerAccountResponse) GetSubjectAlternativeNames() []string { + if x != nil { + return x.SubjectAlternativeNames + } + return nil +} + +func (x *CreateProvisionerAccountResponse) GetExtendedKeys() []string { + if x != nil { + return x.ExtendedKeys + } + return nil +} + +func (x *CreateProvisionerAccountResponse) GetNodeAttestation() *NodeAttestation { + if x != nil { + return x.NodeAttestation + } + return nil +} + +func (x *CreateProvisionerAccountResponse) GetMaxCertificateValidity() uint32 { + if x != nil { + return x.MaxCertificateValidity + } + return 0 +} + +func (x *CreateProvisionerAccountResponse) GetTeam() string { + if x != nil { + return x.Team + } + return "" +} + +func (x *CreateProvisionerAccountResponse) GetEmail() string { + if x != nil { + return x.Email + } + return "" +} + +func (x *CreateProvisionerAccountResponse) GetCreatedAt() *timestamppb.Timestamp { + if x != nil { + return x.CreatedAt + } + return nil +} + +func (x *CreateProvisionerAccountResponse) GetCreatedBy() string { + if x != nil { + return x.CreatedBy + } + return "" +} + +type ProvisionerAccounts struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + ProvisionerAccounts []*ProvisionerAccount `protobuf:"bytes,1,rep,name=provisioner_accounts,json=provisionerAccounts,proto3" json:"provisioner_accounts,omitempty"` +} + +func (x *ProvisionerAccounts) Reset() { + *x = ProvisionerAccounts{} + if protoimpl.UnsafeEnabled { + mi := &file_baseca_v1_api_proto_msgTypes[27] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ProvisionerAccounts) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProvisionerAccounts) ProtoMessage() {} + +func (x *ProvisionerAccounts) ProtoReflect() protoreflect.Message { + mi := &file_baseca_v1_api_proto_msgTypes[27] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ProvisionerAccounts.ProtoReflect.Descriptor instead. +func (*ProvisionerAccounts) Descriptor() ([]byte, []int) { + return file_baseca_v1_api_proto_rawDescGZIP(), []int{27} +} + +func (x *ProvisionerAccounts) GetProvisionerAccounts() []*ProvisionerAccount { + if x != nil { + return x.ProvisionerAccounts + } + return nil +} + +type ProvisionerAccount struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + ClientId string `protobuf:"bytes,1,opt,name=client_id,json=clientId,proto3" json:"client_id,omitempty"` + ProvisionerAccount string `protobuf:"bytes,2,opt,name=provisioner_account,json=provisionerAccount,proto3" json:"provisioner_account,omitempty"` + Environments []string `protobuf:"bytes,3,rep,name=environments,proto3" json:"environments,omitempty"` + RegularExpression string `protobuf:"bytes,4,opt,name=regular_expression,json=regularExpression,proto3" json:"regular_expression,omitempty"` + SubjectAlternativeNames []string `protobuf:"bytes,5,rep,name=subject_alternative_names,json=subjectAlternativeNames,proto3" json:"subject_alternative_names,omitempty"` + ExtendedKeys []string `protobuf:"bytes,7,rep,name=extended_keys,json=extendedKeys,proto3" json:"extended_keys,omitempty"` + NodeAttestation *NodeAttestation `protobuf:"bytes,8,opt,name=node_attestation,json=nodeAttestation,proto3" json:"node_attestation,omitempty"` + MaxCertificateValidity uint32 `protobuf:"varint,9,opt,name=max_certificate_validity,json=maxCertificateValidity,proto3" json:"max_certificate_validity,omitempty"` + Team string `protobuf:"bytes,10,opt,name=team,proto3" json:"team,omitempty"` + Email string `protobuf:"bytes,11,opt,name=email,proto3" json:"email,omitempty"` + CreatedAt *timestamppb.Timestamp `protobuf:"bytes,12,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` + CreatedBy string `protobuf:"bytes,13,opt,name=created_by,json=createdBy,proto3" json:"created_by,omitempty"` +} + +func (x *ProvisionerAccount) Reset() { + *x = ProvisionerAccount{} + if protoimpl.UnsafeEnabled { + mi := &file_baseca_v1_api_proto_msgTypes[28] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ProvisionerAccount) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProvisionerAccount) ProtoMessage() {} + +func (x *ProvisionerAccount) ProtoReflect() protoreflect.Message { + mi := &file_baseca_v1_api_proto_msgTypes[28] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ProvisionerAccount.ProtoReflect.Descriptor instead. +func (*ProvisionerAccount) Descriptor() ([]byte, []int) { + return file_baseca_v1_api_proto_rawDescGZIP(), []int{28} +} + +func (x *ProvisionerAccount) GetClientId() string { + if x != nil { + return x.ClientId + } + return "" +} + +func (x *ProvisionerAccount) GetProvisionerAccount() string { + if x != nil { + return x.ProvisionerAccount + } + return "" +} + +func (x *ProvisionerAccount) GetEnvironments() []string { + if x != nil { + return x.Environments + } + return nil +} + +func (x *ProvisionerAccount) GetRegularExpression() string { + if x != nil { + return x.RegularExpression + } + return "" +} + +func (x *ProvisionerAccount) GetSubjectAlternativeNames() []string { + if x != nil { + return x.SubjectAlternativeNames + } + return nil +} + +func (x *ProvisionerAccount) GetExtendedKeys() []string { + if x != nil { + return x.ExtendedKeys + } + return nil +} + +func (x *ProvisionerAccount) GetNodeAttestation() *NodeAttestation { + if x != nil { + return x.NodeAttestation + } + return nil +} + +func (x *ProvisionerAccount) GetMaxCertificateValidity() uint32 { + if x != nil { + return x.MaxCertificateValidity + } + return 0 +} + +func (x *ProvisionerAccount) GetTeam() string { + if x != nil { + return x.Team + } + return "" +} + +func (x *ProvisionerAccount) GetEmail() string { + if x != nil { + return x.Email + } + return "" +} + +func (x *ProvisionerAccount) GetCreatedAt() *timestamppb.Timestamp { + if x != nil { + return x.CreatedAt + } + return nil +} + +func (x *ProvisionerAccount) GetCreatedBy() string { + if x != nil { + return x.CreatedBy + } + return "" +} + +type NodeAttestation struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + AwsIid *AWSInstanceIdentityDocument `protobuf:"bytes,1,opt,name=aws_iid,json=awsIid,proto3" json:"aws_iid,omitempty"` +} + +func (x *NodeAttestation) Reset() { + *x = NodeAttestation{} + if protoimpl.UnsafeEnabled { + mi := &file_baseca_v1_api_proto_msgTypes[29] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *NodeAttestation) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*NodeAttestation) ProtoMessage() {} + +func (x *NodeAttestation) ProtoReflect() protoreflect.Message { + mi := &file_baseca_v1_api_proto_msgTypes[29] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use NodeAttestation.ProtoReflect.Descriptor instead. +func (*NodeAttestation) Descriptor() ([]byte, []int) { + return file_baseca_v1_api_proto_rawDescGZIP(), []int{29} +} + +func (x *NodeAttestation) GetAwsIid() *AWSInstanceIdentityDocument { + if x != nil { + return x.AwsIid + } + return nil +} + +type AWSInstanceIdentityDocument struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + RoleArn string `protobuf:"bytes,1,opt,name=role_arn,json=roleArn,proto3" json:"role_arn,omitempty"` + AssumeRole string `protobuf:"bytes,2,opt,name=assume_role,json=assumeRole,proto3" json:"assume_role,omitempty"` + SecurityGroups []string `protobuf:"bytes,3,rep,name=security_groups,json=securityGroups,proto3" json:"security_groups,omitempty"` + Region string `protobuf:"bytes,4,opt,name=region,proto3" json:"region,omitempty"` + InstanceId string `protobuf:"bytes,5,opt,name=instance_id,json=instanceId,proto3" json:"instance_id,omitempty"` + ImageId string `protobuf:"bytes,6,opt,name=image_id,json=imageId,proto3" json:"image_id,omitempty"` + InstanceTags map[string]string `protobuf:"bytes,7,rep,name=instance_tags,json=instanceTags,proto3" json:"instance_tags,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` +} + +func (x *AWSInstanceIdentityDocument) Reset() { + *x = AWSInstanceIdentityDocument{} + if protoimpl.UnsafeEnabled { + mi := &file_baseca_v1_api_proto_msgTypes[30] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *AWSInstanceIdentityDocument) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AWSInstanceIdentityDocument) ProtoMessage() {} + +func (x *AWSInstanceIdentityDocument) ProtoReflect() protoreflect.Message { + mi := &file_baseca_v1_api_proto_msgTypes[30] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AWSInstanceIdentityDocument.ProtoReflect.Descriptor instead. +func (*AWSInstanceIdentityDocument) Descriptor() ([]byte, []int) { + return file_baseca_v1_api_proto_rawDescGZIP(), []int{30} +} + +func (x *AWSInstanceIdentityDocument) GetRoleArn() string { + if x != nil { + return x.RoleArn + } + return "" +} + +func (x *AWSInstanceIdentityDocument) GetAssumeRole() string { + if x != nil { + return x.AssumeRole + } + return "" +} + +func (x *AWSInstanceIdentityDocument) GetSecurityGroups() []string { + if x != nil { + return x.SecurityGroups + } + return nil +} + +func (x *AWSInstanceIdentityDocument) GetRegion() string { + if x != nil { + return x.Region + } + return "" +} + +func (x *AWSInstanceIdentityDocument) GetInstanceId() string { + if x != nil { + return x.InstanceId + } + return "" +} + +func (x *AWSInstanceIdentityDocument) GetImageId() string { + if x != nil { + return x.ImageId + } + return "" +} + +func (x *AWSInstanceIdentityDocument) GetInstanceTags() map[string]string { + if x != nil { + return x.InstanceTags + } + return nil +} + +type AccountId struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Uuid string `protobuf:"bytes,1,opt,name=uuid,proto3" json:"uuid,omitempty"` +} + +func (x *AccountId) Reset() { + *x = AccountId{} + if protoimpl.UnsafeEnabled { + mi := &file_baseca_v1_api_proto_msgTypes[31] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *AccountId) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AccountId) ProtoMessage() {} + +func (x *AccountId) ProtoReflect() protoreflect.Message { + mi := &file_baseca_v1_api_proto_msgTypes[31] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AccountId.ProtoReflect.Descriptor instead. +func (*AccountId) Descriptor() ([]byte, []int) { + return file_baseca_v1_api_proto_rawDescGZIP(), []int{31} +} + +func (x *AccountId) GetUuid() string { + if x != nil { + return x.Uuid + } + return "" +} + +type GetServiceAccountMetadataRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + ServiceAccount string `protobuf:"bytes,1,opt,name=service_account,json=serviceAccount,proto3" json:"service_account,omitempty"` + Environment string `protobuf:"bytes,2,opt,name=environment,proto3" json:"environment,omitempty"` + ExtendedKey string `protobuf:"bytes,3,opt,name=extended_key,json=extendedKey,proto3" json:"extended_key,omitempty"` +} + +func (x *GetServiceAccountMetadataRequest) Reset() { + *x = GetServiceAccountMetadataRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_baseca_v1_api_proto_msgTypes[32] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GetServiceAccountMetadataRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetServiceAccountMetadataRequest) ProtoMessage() {} + +func (x *GetServiceAccountMetadataRequest) ProtoReflect() protoreflect.Message { + mi := &file_baseca_v1_api_proto_msgTypes[32] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetServiceAccountMetadataRequest.ProtoReflect.Descriptor instead. +func (*GetServiceAccountMetadataRequest) Descriptor() ([]byte, []int) { + return file_baseca_v1_api_proto_rawDescGZIP(), []int{32} +} + +func (x *GetServiceAccountMetadataRequest) GetServiceAccount() string { + if x != nil { + return x.ServiceAccount + } + return "" +} + +func (x *GetServiceAccountMetadataRequest) GetEnvironment() string { + if x != nil { + return x.Environment + } + return "" +} + +func (x *GetServiceAccountMetadataRequest) GetExtendedKey() string { + if x != nil { + return x.ExtendedKey + } + return "" +} + +type ProvisionServiceAccountRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + ServiceAccount string `protobuf:"bytes,1,opt,name=service_account,json=serviceAccount,proto3" json:"service_account,omitempty"` + Environment string `protobuf:"bytes,2,opt,name=environment,proto3" json:"environment,omitempty"` + RegularExpression string `protobuf:"bytes,3,opt,name=regular_expression,json=regularExpression,proto3" json:"regular_expression,omitempty"` + SubjectAlternativeNames []string `protobuf:"bytes,4,rep,name=subject_alternative_names,json=subjectAlternativeNames,proto3" json:"subject_alternative_names,omitempty"` + CertificateAuthorities []string `protobuf:"bytes,5,rep,name=certificate_authorities,json=certificateAuthorities,proto3" json:"certificate_authorities,omitempty"` + ExtendedKey string `protobuf:"bytes,6,opt,name=extended_key,json=extendedKey,proto3" json:"extended_key,omitempty"` + CertificateValidity int32 `protobuf:"varint,7,opt,name=certificate_validity,json=certificateValidity,proto3" json:"certificate_validity,omitempty"` + SubordinateCa string `protobuf:"bytes,8,opt,name=subordinate_ca,json=subordinateCa,proto3" json:"subordinate_ca,omitempty"` + NodeAttestation *NodeAttestation `protobuf:"bytes,9,opt,name=node_attestation,json=nodeAttestation,proto3" json:"node_attestation,omitempty"` + Team string `protobuf:"bytes,10,opt,name=team,proto3" json:"team,omitempty"` + Email string `protobuf:"bytes,11,opt,name=email,proto3" json:"email,omitempty"` + Region *string `protobuf:"bytes,12,opt,name=region,proto3,oneof" json:"region,omitempty"` +} + +func (x *ProvisionServiceAccountRequest) Reset() { + *x = ProvisionServiceAccountRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_baseca_v1_api_proto_msgTypes[33] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ProvisionServiceAccountRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProvisionServiceAccountRequest) ProtoMessage() {} + +func (x *ProvisionServiceAccountRequest) ProtoReflect() protoreflect.Message { + mi := &file_baseca_v1_api_proto_msgTypes[33] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ProvisionServiceAccountRequest.ProtoReflect.Descriptor instead. +func (*ProvisionServiceAccountRequest) Descriptor() ([]byte, []int) { + return file_baseca_v1_api_proto_rawDescGZIP(), []int{33} +} + +func (x *ProvisionServiceAccountRequest) GetServiceAccount() string { + if x != nil { + return x.ServiceAccount + } + return "" +} + +func (x *ProvisionServiceAccountRequest) GetEnvironment() string { + if x != nil { + return x.Environment + } + return "" +} + +func (x *ProvisionServiceAccountRequest) GetRegularExpression() string { + if x != nil { + return x.RegularExpression + } + return "" +} + +func (x *ProvisionServiceAccountRequest) GetSubjectAlternativeNames() []string { + if x != nil { + return x.SubjectAlternativeNames + } + return nil +} + +func (x *ProvisionServiceAccountRequest) GetCertificateAuthorities() []string { + if x != nil { + return x.CertificateAuthorities + } + return nil +} + +func (x *ProvisionServiceAccountRequest) GetExtendedKey() string { + if x != nil { + return x.ExtendedKey + } + return "" +} + +func (x *ProvisionServiceAccountRequest) GetCertificateValidity() int32 { + if x != nil { + return x.CertificateValidity + } + return 0 +} + +func (x *ProvisionServiceAccountRequest) GetSubordinateCa() string { + if x != nil { + return x.SubordinateCa + } + return "" +} + +func (x *ProvisionServiceAccountRequest) GetNodeAttestation() *NodeAttestation { + if x != nil { + return x.NodeAttestation + } + return nil +} + +func (x *ProvisionServiceAccountRequest) GetTeam() string { + if x != nil { + return x.Team + } + return "" +} + +func (x *ProvisionServiceAccountRequest) GetEmail() string { + if x != nil { + return x.Email + } + return "" +} + +func (x *ProvisionServiceAccountRequest) GetRegion() string { + if x != nil && x.Region != nil { + return *x.Region + } + return "" +} + +type ProvisionServiceAccountResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + ClientId string `protobuf:"bytes,1,opt,name=client_id,json=clientId,proto3" json:"client_id,omitempty"` + ClientToken string `protobuf:"bytes,2,opt,name=client_token,json=clientToken,proto3" json:"client_token,omitempty"` + ServiceAccount string `protobuf:"bytes,3,opt,name=service_account,json=serviceAccount,proto3" json:"service_account,omitempty"` + Environment string `protobuf:"bytes,4,opt,name=environment,proto3" json:"environment,omitempty"` + RegularExpression string `protobuf:"bytes,5,opt,name=regular_expression,json=regularExpression,proto3" json:"regular_expression,omitempty"` + SubjectAlternativeNames []string `protobuf:"bytes,6,rep,name=subject_alternative_names,json=subjectAlternativeNames,proto3" json:"subject_alternative_names,omitempty"` + CertificateAuthorities []string `protobuf:"bytes,7,rep,name=certificate_authorities,json=certificateAuthorities,proto3" json:"certificate_authorities,omitempty"` + ExtendedKey string `protobuf:"bytes,8,opt,name=extended_key,json=extendedKey,proto3" json:"extended_key,omitempty"` + NodeAttestation *NodeAttestation `protobuf:"bytes,9,opt,name=node_attestation,json=nodeAttestation,proto3" json:"node_attestation,omitempty"` + CertificateValidity int32 `protobuf:"varint,10,opt,name=certificate_validity,json=certificateValidity,proto3" json:"certificate_validity,omitempty"` + SubordinateCa string `protobuf:"bytes,11,opt,name=subordinate_ca,json=subordinateCa,proto3" json:"subordinate_ca,omitempty"` + Team string `protobuf:"bytes,12,opt,name=team,proto3" json:"team,omitempty"` + Email string `protobuf:"bytes,13,opt,name=email,proto3" json:"email,omitempty"` + CreatedAt *timestamppb.Timestamp `protobuf:"bytes,14,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` + CreatedBy string `protobuf:"bytes,15,opt,name=created_by,json=createdBy,proto3" json:"created_by,omitempty"` +} + +func (x *ProvisionServiceAccountResponse) Reset() { + *x = ProvisionServiceAccountResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_baseca_v1_api_proto_msgTypes[34] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ProvisionServiceAccountResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProvisionServiceAccountResponse) ProtoMessage() {} + +func (x *ProvisionServiceAccountResponse) ProtoReflect() protoreflect.Message { + mi := &file_baseca_v1_api_proto_msgTypes[34] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ProvisionServiceAccountResponse.ProtoReflect.Descriptor instead. +func (*ProvisionServiceAccountResponse) Descriptor() ([]byte, []int) { + return file_baseca_v1_api_proto_rawDescGZIP(), []int{34} +} + +func (x *ProvisionServiceAccountResponse) GetClientId() string { + if x != nil { + return x.ClientId + } + return "" +} + +func (x *ProvisionServiceAccountResponse) GetClientToken() string { + if x != nil { + return x.ClientToken + } + return "" +} + +func (x *ProvisionServiceAccountResponse) GetServiceAccount() string { + if x != nil { + return x.ServiceAccount + } + return "" +} + +func (x *ProvisionServiceAccountResponse) GetEnvironment() string { + if x != nil { + return x.Environment + } + return "" +} + +func (x *ProvisionServiceAccountResponse) GetRegularExpression() string { + if x != nil { + return x.RegularExpression + } + return "" +} + +func (x *ProvisionServiceAccountResponse) GetSubjectAlternativeNames() []string { + if x != nil { + return x.SubjectAlternativeNames + } + return nil +} + +func (x *ProvisionServiceAccountResponse) GetCertificateAuthorities() []string { + if x != nil { + return x.CertificateAuthorities + } + return nil +} + +func (x *ProvisionServiceAccountResponse) GetExtendedKey() string { + if x != nil { + return x.ExtendedKey + } + return "" +} + +func (x *ProvisionServiceAccountResponse) GetNodeAttestation() *NodeAttestation { + if x != nil { + return x.NodeAttestation + } + return nil +} + +func (x *ProvisionServiceAccountResponse) GetCertificateValidity() int32 { + if x != nil { + return x.CertificateValidity + } + return 0 +} + +func (x *ProvisionServiceAccountResponse) GetSubordinateCa() string { + if x != nil { + return x.SubordinateCa + } + return "" +} + +func (x *ProvisionServiceAccountResponse) GetTeam() string { + if x != nil { + return x.Team + } + return "" +} + +func (x *ProvisionServiceAccountResponse) GetEmail() string { + if x != nil { + return x.Email + } + return "" +} + +func (x *ProvisionServiceAccountResponse) GetCreatedAt() *timestamppb.Timestamp { + if x != nil { + return x.CreatedAt + } + return nil +} + +func (x *ProvisionServiceAccountResponse) GetCreatedBy() string { + if x != nil { + return x.CreatedBy + } + return "" +} + +type HealthCheckRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Service string `protobuf:"bytes,1,opt,name=service,proto3" json:"service,omitempty"` +} + +func (x *HealthCheckRequest) Reset() { + *x = HealthCheckRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_baseca_v1_api_proto_msgTypes[35] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *HealthCheckRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HealthCheckRequest) ProtoMessage() {} + +func (x *HealthCheckRequest) ProtoReflect() protoreflect.Message { + mi := &file_baseca_v1_api_proto_msgTypes[35] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use HealthCheckRequest.ProtoReflect.Descriptor instead. +func (*HealthCheckRequest) Descriptor() ([]byte, []int) { + return file_baseca_v1_api_proto_rawDescGZIP(), []int{35} +} + +func (x *HealthCheckRequest) GetService() string { + if x != nil { + return x.Service + } + return "" +} + +type HealthCheckResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Status HealthCheckResponse_ServingStatus `protobuf:"varint,1,opt,name=status,proto3,enum=baseca.v1.HealthCheckResponse_ServingStatus" json:"status,omitempty"` +} + +func (x *HealthCheckResponse) Reset() { + *x = HealthCheckResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_baseca_v1_api_proto_msgTypes[36] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *HealthCheckResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HealthCheckResponse) ProtoMessage() {} + +func (x *HealthCheckResponse) ProtoReflect() protoreflect.Message { + mi := &file_baseca_v1_api_proto_msgTypes[36] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use HealthCheckResponse.ProtoReflect.Descriptor instead. +func (*HealthCheckResponse) Descriptor() ([]byte, []int) { + return file_baseca_v1_api_proto_rawDescGZIP(), []int{36} +} + +func (x *HealthCheckResponse) GetStatus() HealthCheckResponse_ServingStatus { + if x != nil { + return x.Status + } + return HealthCheckResponse_UNKNOWN +} + +var File_baseca_v1_api_proto protoreflect.FileDescriptor + +var file_baseca_v1_api_proto_rawDesc = []byte{ + 0x0a, 0x13, 0x62, 0x61, 0x73, 0x65, 0x63, 0x61, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x70, 0x69, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x09, 0x62, 0x61, 0x73, 0x65, 0x63, 0x61, 0x2e, 0x76, 0x31, + 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, + 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x1a, 0x1b, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, + 0x75, 0x66, 0x2f, 0x65, 0x6d, 0x70, 0x74, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xa9, + 0x04, 0x0a, 0x14, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x50, 0x61, + 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x12, 0x23, 0x0a, 0x0d, 0x73, 0x65, 0x72, 0x69, 0x61, + 0x6c, 0x5f, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, + 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x12, 0x1f, 0x0a, 0x0b, + 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0a, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x38, 0x0a, + 0x18, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x61, 0x6c, 0x74, 0x65, 0x72, 0x6e, 0x61, + 0x74, 0x69, 0x76, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, + 0x16, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x41, 0x6c, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x74, + 0x69, 0x76, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x43, 0x0a, 0x0f, 0x65, 0x78, 0x70, 0x69, 0x72, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x64, 0x61, 0x74, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, + 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0e, 0x65, 0x78, + 0x70, 0x69, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x44, 0x61, 0x74, 0x65, 0x12, 0x3b, 0x0a, 0x0b, + 0x69, 0x73, 0x73, 0x75, 0x65, 0x64, 0x5f, 0x64, 0x61, 0x74, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x69, + 0x73, 0x73, 0x75, 0x65, 0x64, 0x44, 0x61, 0x74, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x72, 0x65, 0x76, + 0x6f, 0x6b, 0x65, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x72, 0x65, 0x76, 0x6f, + 0x6b, 0x65, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x72, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x64, 0x5f, 0x62, + 0x79, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x72, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x64, + 0x42, 0x79, 0x12, 0x3b, 0x0a, 0x0b, 0x72, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x5f, 0x64, 0x61, 0x74, + 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, + 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x72, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x44, 0x61, 0x74, 0x65, 0x12, + 0x3a, 0x0a, 0x19, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x5f, 0x61, + 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x5f, 0x61, 0x72, 0x6e, 0x18, 0x09, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x17, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x41, + 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x41, 0x72, 0x6e, 0x12, 0x18, 0x0a, 0x07, 0x61, + 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x63, + 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x20, 0x0a, 0x0b, 0x65, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, + 0x6d, 0x65, 0x6e, 0x74, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x65, 0x6e, 0x76, 0x69, + 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x21, 0x0a, 0x0c, 0x65, 0x78, 0x74, 0x65, 0x6e, + 0x64, 0x65, 0x64, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x65, + 0x78, 0x74, 0x65, 0x6e, 0x64, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x22, 0x5c, 0x0a, 0x15, 0x43, 0x65, + 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x73, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, + 0x74, 0x65, 0x72, 0x12, 0x43, 0x0a, 0x0c, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, + 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x62, 0x61, 0x73, 0x65, + 0x63, 0x61, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, + 0x65, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x52, 0x0c, 0x63, 0x65, 0x72, 0x74, + 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x73, 0x22, 0xcd, 0x01, 0x0a, 0x1d, 0x43, 0x65, 0x72, + 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x74, + 0x79, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x65, + 0x67, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, 0x65, 0x67, 0x69, + 0x6f, 0x6e, 0x12, 0x15, 0x0a, 0x06, 0x63, 0x61, 0x5f, 0x61, 0x72, 0x6e, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x05, 0x63, 0x61, 0x41, 0x72, 0x6e, 0x12, 0x25, 0x0a, 0x0e, 0x73, 0x69, 0x67, + 0x6e, 0x5f, 0x61, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, 0x68, 0x6d, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0d, 0x73, 0x69, 0x67, 0x6e, 0x41, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, 0x68, 0x6d, + 0x12, 0x1f, 0x0a, 0x0b, 0x61, 0x73, 0x73, 0x75, 0x6d, 0x65, 0x5f, 0x72, 0x6f, 0x6c, 0x65, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x61, 0x73, 0x73, 0x75, 0x6d, 0x65, 0x52, 0x6f, 0x6c, + 0x65, 0x12, 0x19, 0x0a, 0x08, 0x72, 0x6f, 0x6c, 0x65, 0x5f, 0x61, 0x72, 0x6e, 0x18, 0x05, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x07, 0x72, 0x6f, 0x6c, 0x65, 0x41, 0x72, 0x6e, 0x12, 0x1a, 0x0a, 0x08, + 0x76, 0x61, 0x6c, 0x69, 0x64, 0x69, 0x74, 0x79, 0x18, 0x06, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, + 0x76, 0x61, 0x6c, 0x69, 0x64, 0x69, 0x74, 0x79, 0x22, 0x5b, 0x0a, 0x19, 0x43, 0x65, 0x72, 0x74, + 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x53, 0x69, 0x67, 0x6e, 0x69, 0x6e, 0x67, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x3e, 0x0a, 0x1b, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, + 0x63, 0x61, 0x74, 0x65, 0x5f, 0x73, 0x69, 0x67, 0x6e, 0x69, 0x6e, 0x67, 0x5f, 0x72, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x19, 0x63, 0x65, 0x72, 0x74, + 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x53, 0x69, 0x67, 0x6e, 0x69, 0x6e, 0x67, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xe5, 0x01, 0x0a, 0x11, 0x53, 0x69, 0x67, 0x6e, 0x65, 0x64, + 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x63, + 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0b, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x12, 0x2b, 0x0a, + 0x11, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x5f, 0x63, 0x68, 0x61, + 0x69, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, + 0x69, 0x63, 0x61, 0x74, 0x65, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x12, 0x3b, 0x0a, 0x08, 0x6d, 0x65, + 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x62, + 0x61, 0x73, 0x65, 0x63, 0x61, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, + 0x63, 0x61, 0x74, 0x65, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x52, 0x08, 0x6d, + 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x44, 0x0a, 0x1e, 0x69, 0x6e, 0x74, 0x65, 0x72, + 0x6d, 0x65, 0x64, 0x69, 0x61, 0x74, 0x65, 0x5f, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, + 0x61, 0x74, 0x65, 0x5f, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x1c, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6d, 0x65, 0x64, 0x69, 0x61, 0x74, 0x65, 0x43, 0x65, 0x72, + 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x22, 0x3e, 0x0a, + 0x17, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x53, 0x65, 0x72, 0x69, + 0x61, 0x6c, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x12, 0x23, 0x0a, 0x0d, 0x73, 0x65, 0x72, 0x69, + 0x61, 0x6c, 0x5f, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0c, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x22, 0x70, 0x0a, + 0x17, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, + 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x63, 0x6f, 0x6d, 0x6d, + 0x6f, 0x6e, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x63, + 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x17, 0x0a, 0x07, 0x70, 0x61, 0x67, + 0x65, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x70, 0x61, 0x67, 0x65, + 0x49, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x70, 0x61, 0x67, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x22, + 0x6c, 0x0a, 0x18, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, + 0x63, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x23, 0x0a, 0x0d, 0x73, + 0x65, 0x72, 0x69, 0x61, 0x6c, 0x5f, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0c, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, + 0x12, 0x2b, 0x0a, 0x11, 0x72, 0x65, 0x76, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x72, + 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x72, 0x65, 0x76, + 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x22, 0x9d, 0x01, + 0x0a, 0x19, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, + 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x73, + 0x65, 0x72, 0x69, 0x61, 0x6c, 0x5f, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0c, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, + 0x12, 0x43, 0x0a, 0x0f, 0x72, 0x65, 0x76, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x64, + 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, + 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, + 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0e, 0x72, 0x65, 0x76, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x44, 0x61, 0x74, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0xc3, 0x02, + 0x0a, 0x15, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x53, 0x69, 0x67, 0x6e, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x3e, 0x0a, 0x1b, 0x63, 0x65, 0x72, 0x74, 0x69, + 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x5f, 0x73, 0x69, 0x67, 0x6e, 0x69, 0x6e, 0x67, 0x5f, 0x72, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x19, 0x63, 0x65, + 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x53, 0x69, 0x67, 0x6e, 0x69, 0x6e, 0x67, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x62, 0x0a, 0x15, 0x63, 0x65, 0x72, 0x74, 0x69, + 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x74, 0x79, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x28, 0x2e, 0x62, 0x61, 0x73, 0x65, 0x63, 0x61, 0x2e, + 0x76, 0x31, 0x2e, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x41, 0x75, + 0x74, 0x68, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, + 0x48, 0x00, 0x52, 0x14, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x41, + 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x88, 0x01, 0x01, 0x12, 0x27, 0x0a, 0x0f, 0x73, + 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x41, 0x63, 0x63, + 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x20, 0x0a, 0x0b, 0x65, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, + 0x65, 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x65, 0x6e, 0x76, 0x69, 0x72, + 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x21, 0x0a, 0x0c, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x64, + 0x65, 0x64, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x65, 0x78, + 0x74, 0x65, 0x6e, 0x64, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x42, 0x18, 0x0a, 0x16, 0x5f, 0x63, 0x65, + 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, + 0x69, 0x74, 0x79, 0x22, 0x2f, 0x0a, 0x0b, 0x45, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, + 0x6e, 0x74, 0x12, 0x20, 0x0a, 0x0b, 0x65, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, + 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x65, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, + 0x6d, 0x65, 0x6e, 0x74, 0x22, 0xdf, 0x01, 0x0a, 0x1f, 0x51, 0x75, 0x65, 0x72, 0x79, 0x43, 0x65, + 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, + 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x23, 0x0a, 0x0d, 0x73, 0x65, 0x72, 0x69, + 0x61, 0x6c, 0x5f, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0c, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x12, 0x18, 0x0a, + 0x07, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, + 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x20, 0x0a, 0x0b, 0x65, 0x6e, 0x76, 0x69, 0x72, + 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x65, 0x6e, + 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x21, 0x0a, 0x0c, 0x65, 0x78, 0x74, + 0x65, 0x6e, 0x64, 0x65, 0x64, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0b, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x64, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x12, 0x38, 0x0a, 0x18, + 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x61, 0x6c, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x74, + 0x69, 0x76, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x16, + 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x41, 0x6c, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x74, 0x69, + 0x76, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x22, 0x96, 0x02, 0x0a, 0x04, 0x55, 0x73, 0x65, 0x72, 0x12, + 0x12, 0x0a, 0x04, 0x75, 0x75, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x75, + 0x75, 0x69, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x12, + 0x1b, 0x0a, 0x09, 0x66, 0x75, 0x6c, 0x6c, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x08, 0x66, 0x75, 0x6c, 0x6c, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, + 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x6d, 0x61, + 0x69, 0x6c, 0x12, 0x20, 0x0a, 0x0b, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, + 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, + 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x4e, 0x0a, 0x15, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, + 0x61, 0x6c, 0x5f, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x06, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, + 0x13, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x43, 0x68, 0x61, 0x6e, 0x67, + 0x65, 0x64, 0x41, 0x74, 0x12, 0x39, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, + 0x61, 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, + 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, + 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x22, + 0x2e, 0x0a, 0x05, 0x55, 0x73, 0x65, 0x72, 0x73, 0x12, 0x25, 0x0a, 0x05, 0x75, 0x73, 0x65, 0x72, + 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x62, 0x61, 0x73, 0x65, 0x63, 0x61, + 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x05, 0x75, 0x73, 0x65, 0x72, 0x73, 0x22, + 0x4a, 0x0a, 0x10, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x12, + 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, 0x5b, 0x0a, 0x11, 0x4c, + 0x6f, 0x67, 0x69, 0x6e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x21, 0x0a, 0x0c, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x54, 0x6f, + 0x6b, 0x65, 0x6e, 0x12, 0x23, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x0f, 0x2e, 0x62, 0x61, 0x73, 0x65, 0x63, 0x61, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x73, + 0x65, 0x72, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x22, 0x2d, 0x0a, 0x0f, 0x55, 0x73, 0x65, 0x72, + 0x6e, 0x61, 0x6d, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x75, + 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, + 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x46, 0x0a, 0x0e, 0x51, 0x75, 0x65, 0x72, 0x79, + 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x12, 0x17, 0x0a, 0x07, 0x70, 0x61, 0x67, + 0x65, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x70, 0x61, 0x67, 0x65, + 0x49, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x70, 0x61, 0x67, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x22, + 0xa0, 0x01, 0x0a, 0x11, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, + 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, + 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x1b, 0x0a, + 0x09, 0x66, 0x75, 0x6c, 0x6c, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x08, 0x66, 0x75, 0x6c, 0x6c, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x6d, + 0x61, 0x69, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, + 0x12, 0x20, 0x0a, 0x0b, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, + 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, + 0x6e, 0x73, 0x22, 0x7d, 0x0a, 0x18, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x43, 0x72, 0x65, 0x64, + 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, + 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, + 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x61, + 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x29, 0x0a, 0x10, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, + 0x64, 0x5f, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0f, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, + 0x64, 0x22, 0x58, 0x0a, 0x18, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x50, 0x65, 0x72, 0x6d, 0x69, + 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, + 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x70, 0x65, 0x72, + 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, + 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0xb0, 0x04, 0x0a, 0x1b, + 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x41, 0x63, 0x63, + 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x27, 0x0a, 0x0f, 0x73, + 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x41, 0x63, 0x63, + 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x20, 0x0a, 0x0b, 0x65, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, + 0x65, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x65, 0x6e, 0x76, 0x69, 0x72, + 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x32, 0x0a, 0x12, 0x72, 0x65, 0x67, 0x75, 0x6c, 0x61, + 0x72, 0x5f, 0x65, 0x78, 0x70, 0x72, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x09, 0x48, 0x00, 0x52, 0x11, 0x72, 0x65, 0x67, 0x75, 0x6c, 0x61, 0x72, 0x45, 0x78, 0x70, + 0x72, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x88, 0x01, 0x01, 0x12, 0x3a, 0x0a, 0x19, 0x73, 0x75, + 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x61, 0x6c, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x74, 0x69, 0x76, + 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x17, 0x73, + 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x41, 0x6c, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x74, 0x69, 0x76, + 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x12, 0x37, 0x0a, 0x17, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, + 0x69, 0x63, 0x61, 0x74, 0x65, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x74, 0x69, 0x65, + 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x16, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, + 0x63, 0x61, 0x74, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x74, 0x69, 0x65, 0x73, 0x12, + 0x21, 0x0a, 0x0c, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x64, 0x65, 0x64, 0x5f, 0x6b, 0x65, 0x79, 0x18, + 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x64, 0x65, 0x64, 0x4b, + 0x65, 0x79, 0x12, 0x31, 0x0a, 0x14, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, + 0x65, 0x5f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x69, 0x74, 0x79, 0x18, 0x07, 0x20, 0x01, 0x28, 0x05, + 0x52, 0x13, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x56, 0x61, 0x6c, + 0x69, 0x64, 0x69, 0x74, 0x79, 0x12, 0x25, 0x0a, 0x0e, 0x73, 0x75, 0x62, 0x6f, 0x72, 0x64, 0x69, + 0x6e, 0x61, 0x74, 0x65, 0x5f, 0x63, 0x61, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x73, + 0x75, 0x62, 0x6f, 0x72, 0x64, 0x69, 0x6e, 0x61, 0x74, 0x65, 0x43, 0x61, 0x12, 0x4a, 0x0a, 0x10, + 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x61, 0x74, 0x74, 0x65, 0x73, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x62, 0x61, 0x73, 0x65, 0x63, 0x61, 0x2e, + 0x76, 0x31, 0x2e, 0x4e, 0x6f, 0x64, 0x65, 0x41, 0x74, 0x74, 0x65, 0x73, 0x74, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x48, 0x01, 0x52, 0x0f, 0x6e, 0x6f, 0x64, 0x65, 0x41, 0x74, 0x74, 0x65, 0x73, 0x74, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x88, 0x01, 0x01, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x65, 0x61, 0x6d, + 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x65, 0x61, 0x6d, 0x12, 0x14, 0x0a, 0x05, + 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x6d, 0x61, + 0x69, 0x6c, 0x42, 0x15, 0x0a, 0x13, 0x5f, 0x72, 0x65, 0x67, 0x75, 0x6c, 0x61, 0x72, 0x5f, 0x65, + 0x78, 0x70, 0x72, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x42, 0x13, 0x0a, 0x11, 0x5f, 0x6e, 0x6f, + 0x64, 0x65, 0x5f, 0x61, 0x74, 0x74, 0x65, 0x73, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x95, + 0x05, 0x0a, 0x1c, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, + 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x1b, 0x0a, 0x09, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x08, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x21, 0x0a, 0x0c, + 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0b, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, + 0x27, 0x0a, 0x0f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x61, 0x63, 0x63, 0x6f, 0x75, + 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, + 0x65, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x20, 0x0a, 0x0b, 0x65, 0x6e, 0x76, 0x69, + 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x65, + 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x2d, 0x0a, 0x12, 0x72, 0x65, + 0x67, 0x75, 0x6c, 0x61, 0x72, 0x5f, 0x65, 0x78, 0x70, 0x72, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, + 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x72, 0x65, 0x67, 0x75, 0x6c, 0x61, 0x72, 0x45, + 0x78, 0x70, 0x72, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x3a, 0x0a, 0x19, 0x73, 0x75, 0x62, + 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x61, 0x6c, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x74, 0x69, 0x76, 0x65, + 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x09, 0x52, 0x17, 0x73, 0x75, + 0x62, 0x6a, 0x65, 0x63, 0x74, 0x41, 0x6c, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x74, 0x69, 0x76, 0x65, + 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x12, 0x37, 0x0a, 0x17, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, + 0x63, 0x61, 0x74, 0x65, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x74, 0x69, 0x65, 0x73, + 0x18, 0x07, 0x20, 0x03, 0x28, 0x09, 0x52, 0x16, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, + 0x61, 0x74, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x74, 0x69, 0x65, 0x73, 0x12, 0x21, + 0x0a, 0x0c, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x64, 0x65, 0x64, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x08, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x64, 0x65, 0x64, 0x4b, 0x65, + 0x79, 0x12, 0x45, 0x0a, 0x10, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x61, 0x74, 0x74, 0x65, 0x73, 0x74, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x62, 0x61, + 0x73, 0x65, 0x63, 0x61, 0x2e, 0x76, 0x31, 0x2e, 0x4e, 0x6f, 0x64, 0x65, 0x41, 0x74, 0x74, 0x65, + 0x73, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0f, 0x6e, 0x6f, 0x64, 0x65, 0x41, 0x74, 0x74, + 0x65, 0x73, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x31, 0x0a, 0x14, 0x63, 0x65, 0x72, 0x74, + 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x5f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x69, 0x74, 0x79, + 0x18, 0x0a, 0x20, 0x01, 0x28, 0x05, 0x52, 0x13, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, + 0x61, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x69, 0x74, 0x79, 0x12, 0x25, 0x0a, 0x0e, 0x73, + 0x75, 0x62, 0x6f, 0x72, 0x64, 0x69, 0x6e, 0x61, 0x74, 0x65, 0x5f, 0x63, 0x61, 0x18, 0x0b, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0d, 0x73, 0x75, 0x62, 0x6f, 0x72, 0x64, 0x69, 0x6e, 0x61, 0x74, 0x65, + 0x43, 0x61, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x65, 0x61, 0x6d, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x04, 0x74, 0x65, 0x61, 0x6d, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x18, + 0x0d, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x12, 0x39, 0x0a, 0x0a, + 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, + 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x63, 0x72, + 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, + 0x65, 0x64, 0x5f, 0x62, 0x79, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x72, 0x65, + 0x61, 0x74, 0x65, 0x64, 0x42, 0x79, 0x22, 0x86, 0x05, 0x0a, 0x0e, 0x53, 0x65, 0x72, 0x76, 0x69, + 0x63, 0x65, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6c, 0x69, + 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, 0x6c, + 0x69, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x27, 0x0a, 0x0f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, + 0x65, 0x5f, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, + 0x20, 0x0a, 0x0b, 0x65, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x65, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, + 0x74, 0x12, 0x2d, 0x0a, 0x12, 0x72, 0x65, 0x67, 0x75, 0x6c, 0x61, 0x72, 0x5f, 0x65, 0x78, 0x70, + 0x72, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x72, + 0x65, 0x67, 0x75, 0x6c, 0x61, 0x72, 0x45, 0x78, 0x70, 0x72, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, + 0x12, 0x3a, 0x0a, 0x19, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x61, 0x6c, 0x74, 0x65, + 0x72, 0x6e, 0x61, 0x74, 0x69, 0x76, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x18, 0x05, 0x20, + 0x03, 0x28, 0x09, 0x52, 0x17, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x41, 0x6c, 0x74, 0x65, + 0x72, 0x6e, 0x61, 0x74, 0x69, 0x76, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x12, 0x37, 0x0a, 0x17, + 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x5f, 0x61, 0x75, 0x74, 0x68, + 0x6f, 0x72, 0x69, 0x74, 0x69, 0x65, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x09, 0x52, 0x16, 0x63, + 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, + 0x69, 0x74, 0x69, 0x65, 0x73, 0x12, 0x21, 0x0a, 0x0c, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x64, 0x65, + 0x64, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x65, 0x78, 0x74, + 0x65, 0x6e, 0x64, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x12, 0x45, 0x0a, 0x10, 0x6e, 0x6f, 0x64, 0x65, + 0x5f, 0x61, 0x74, 0x74, 0x65, 0x73, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x08, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x62, 0x61, 0x73, 0x65, 0x63, 0x61, 0x2e, 0x76, 0x31, 0x2e, 0x4e, + 0x6f, 0x64, 0x65, 0x41, 0x74, 0x74, 0x65, 0x73, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0f, + 0x6e, 0x6f, 0x64, 0x65, 0x41, 0x74, 0x74, 0x65, 0x73, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, + 0x31, 0x0a, 0x14, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x5f, 0x76, + 0x61, 0x6c, 0x69, 0x64, 0x69, 0x74, 0x79, 0x18, 0x09, 0x20, 0x01, 0x28, 0x05, 0x52, 0x13, 0x63, + 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x69, + 0x74, 0x79, 0x12, 0x25, 0x0a, 0x0e, 0x73, 0x75, 0x62, 0x6f, 0x72, 0x64, 0x69, 0x6e, 0x61, 0x74, + 0x65, 0x5f, 0x63, 0x61, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x73, 0x75, 0x62, 0x6f, + 0x72, 0x64, 0x69, 0x6e, 0x61, 0x74, 0x65, 0x43, 0x61, 0x12, 0x20, 0x0a, 0x0b, 0x70, 0x72, 0x6f, + 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x64, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0b, + 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x74, + 0x65, 0x61, 0x6d, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x65, 0x61, 0x6d, 0x12, + 0x14, 0x0a, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, + 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x12, 0x39, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, + 0x5f, 0x61, 0x74, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, + 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, + 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, + 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x62, 0x79, 0x18, 0x0f, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x42, 0x79, 0x22, + 0x57, 0x0a, 0x0f, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, + 0x74, 0x73, 0x12, 0x44, 0x0a, 0x10, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x61, 0x63, + 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x62, + 0x61, 0x73, 0x65, 0x63, 0x61, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, + 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x0f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, + 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x22, 0xcb, 0x03, 0x0a, 0x1f, 0x43, 0x72, 0x65, + 0x61, 0x74, 0x65, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x41, 0x63, + 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2f, 0x0a, 0x13, + 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x5f, 0x61, 0x63, 0x63, 0x6f, + 0x75, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x70, 0x72, 0x6f, 0x76, 0x69, + 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x22, 0x0a, + 0x0c, 0x65, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x02, 0x20, + 0x03, 0x28, 0x09, 0x52, 0x0c, 0x65, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, + 0x73, 0x12, 0x2d, 0x0a, 0x12, 0x72, 0x65, 0x67, 0x75, 0x6c, 0x61, 0x72, 0x5f, 0x65, 0x78, 0x70, + 0x72, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x72, + 0x65, 0x67, 0x75, 0x6c, 0x61, 0x72, 0x45, 0x78, 0x70, 0x72, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, + 0x12, 0x3a, 0x0a, 0x19, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x61, 0x6c, 0x74, 0x65, + 0x72, 0x6e, 0x61, 0x74, 0x69, 0x76, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x18, 0x04, 0x20, + 0x03, 0x28, 0x09, 0x52, 0x17, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x41, 0x6c, 0x74, 0x65, + 0x72, 0x6e, 0x61, 0x74, 0x69, 0x76, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x12, 0x23, 0x0a, 0x0d, + 0x65, 0x78, 0x74, 0x65, 0x6e, 0x64, 0x65, 0x64, 0x5f, 0x6b, 0x65, 0x79, 0x73, 0x18, 0x05, 0x20, + 0x03, 0x28, 0x09, 0x52, 0x0c, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x64, 0x65, 0x64, 0x4b, 0x65, 0x79, + 0x73, 0x12, 0x38, 0x0a, 0x18, 0x6d, 0x61, 0x78, 0x5f, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, + 0x63, 0x61, 0x74, 0x65, 0x5f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x69, 0x74, 0x79, 0x18, 0x06, 0x20, + 0x01, 0x28, 0x0d, 0x52, 0x16, 0x6d, 0x61, 0x78, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, + 0x61, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x69, 0x74, 0x79, 0x12, 0x4a, 0x0a, 0x10, 0x6e, + 0x6f, 0x64, 0x65, 0x5f, 0x61, 0x74, 0x74, 0x65, 0x73, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, + 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x62, 0x61, 0x73, 0x65, 0x63, 0x61, 0x2e, 0x76, + 0x31, 0x2e, 0x4e, 0x6f, 0x64, 0x65, 0x41, 0x74, 0x74, 0x65, 0x73, 0x74, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x48, 0x00, 0x52, 0x0f, 0x6e, 0x6f, 0x64, 0x65, 0x41, 0x74, 0x74, 0x65, 0x73, 0x74, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x88, 0x01, 0x01, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x65, 0x61, 0x6d, 0x18, + 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x65, 0x61, 0x6d, 0x12, 0x14, 0x0a, 0x05, 0x65, + 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x6d, 0x61, 0x69, + 0x6c, 0x42, 0x13, 0x0a, 0x11, 0x5f, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x61, 0x74, 0x74, 0x65, 0x73, + 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0xcc, 0x04, 0x0a, 0x20, 0x43, 0x72, 0x65, 0x61, 0x74, + 0x65, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x41, 0x63, 0x63, 0x6f, + 0x75, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x63, + 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, + 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x6c, 0x69, 0x65, + 0x6e, 0x74, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, + 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x2f, 0x0a, 0x13, 0x70, + 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x5f, 0x61, 0x63, 0x63, 0x6f, 0x75, + 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, + 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x22, 0x0a, 0x0c, + 0x65, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x04, 0x20, 0x03, + 0x28, 0x09, 0x52, 0x0c, 0x65, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x73, + 0x12, 0x2d, 0x0a, 0x12, 0x72, 0x65, 0x67, 0x75, 0x6c, 0x61, 0x72, 0x5f, 0x65, 0x78, 0x70, 0x72, + 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x72, 0x65, + 0x67, 0x75, 0x6c, 0x61, 0x72, 0x45, 0x78, 0x70, 0x72, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, + 0x3a, 0x0a, 0x19, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x61, 0x6c, 0x74, 0x65, 0x72, + 0x6e, 0x61, 0x74, 0x69, 0x76, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x18, 0x06, 0x20, 0x03, + 0x28, 0x09, 0x52, 0x17, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x41, 0x6c, 0x74, 0x65, 0x72, + 0x6e, 0x61, 0x74, 0x69, 0x76, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x12, 0x23, 0x0a, 0x0d, 0x65, + 0x78, 0x74, 0x65, 0x6e, 0x64, 0x65, 0x64, 0x5f, 0x6b, 0x65, 0x79, 0x73, 0x18, 0x08, 0x20, 0x03, + 0x28, 0x09, 0x52, 0x0c, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x64, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x73, + 0x12, 0x45, 0x0a, 0x10, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x61, 0x74, 0x74, 0x65, 0x73, 0x74, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x62, 0x61, 0x73, + 0x65, 0x63, 0x61, 0x2e, 0x76, 0x31, 0x2e, 0x4e, 0x6f, 0x64, 0x65, 0x41, 0x74, 0x74, 0x65, 0x73, + 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0f, 0x6e, 0x6f, 0x64, 0x65, 0x41, 0x74, 0x74, 0x65, + 0x73, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x38, 0x0a, 0x18, 0x6d, 0x61, 0x78, 0x5f, 0x63, + 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x5f, 0x76, 0x61, 0x6c, 0x69, 0x64, + 0x69, 0x74, 0x79, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x16, 0x6d, 0x61, 0x78, 0x43, 0x65, + 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x69, 0x74, + 0x79, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x65, 0x61, 0x6d, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x04, 0x74, 0x65, 0x61, 0x6d, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x0c, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x12, 0x39, 0x0a, 0x0a, 0x63, + 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, + 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x63, 0x72, 0x65, + 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, + 0x64, 0x5f, 0x62, 0x79, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, + 0x74, 0x65, 0x64, 0x42, 0x79, 0x22, 0x67, 0x0a, 0x13, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, + 0x6f, 0x6e, 0x65, 0x72, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x12, 0x50, 0x0a, 0x14, + 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x5f, 0x61, 0x63, 0x63, 0x6f, + 0x75, 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x62, 0x61, 0x73, + 0x65, 0x63, 0x61, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, + 0x65, 0x72, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x13, 0x70, 0x72, 0x6f, 0x76, 0x69, + 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x22, 0x9b, + 0x04, 0x0a, 0x12, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x41, 0x63, + 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, + 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, + 0x49, 0x64, 0x12, 0x2f, 0x0a, 0x13, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, + 0x72, 0x5f, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x12, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x41, 0x63, 0x63, 0x6f, + 0x75, 0x6e, 0x74, 0x12, 0x22, 0x0a, 0x0c, 0x65, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, + 0x6e, 0x74, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0c, 0x65, 0x6e, 0x76, 0x69, 0x72, + 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x2d, 0x0a, 0x12, 0x72, 0x65, 0x67, 0x75, 0x6c, + 0x61, 0x72, 0x5f, 0x65, 0x78, 0x70, 0x72, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x11, 0x72, 0x65, 0x67, 0x75, 0x6c, 0x61, 0x72, 0x45, 0x78, 0x70, 0x72, + 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x3a, 0x0a, 0x19, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, + 0x74, 0x5f, 0x61, 0x6c, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x74, 0x69, 0x76, 0x65, 0x5f, 0x6e, 0x61, + 0x6d, 0x65, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x17, 0x73, 0x75, 0x62, 0x6a, 0x65, + 0x63, 0x74, 0x41, 0x6c, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x74, 0x69, 0x76, 0x65, 0x4e, 0x61, 0x6d, + 0x65, 0x73, 0x12, 0x23, 0x0a, 0x0d, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x64, 0x65, 0x64, 0x5f, 0x6b, + 0x65, 0x79, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0c, 0x65, 0x78, 0x74, 0x65, 0x6e, + 0x64, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x73, 0x12, 0x45, 0x0a, 0x10, 0x6e, 0x6f, 0x64, 0x65, 0x5f, + 0x61, 0x74, 0x74, 0x65, 0x73, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x08, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x1a, 0x2e, 0x62, 0x61, 0x73, 0x65, 0x63, 0x61, 0x2e, 0x76, 0x31, 0x2e, 0x4e, 0x6f, + 0x64, 0x65, 0x41, 0x74, 0x74, 0x65, 0x73, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0f, 0x6e, + 0x6f, 0x64, 0x65, 0x41, 0x74, 0x74, 0x65, 0x73, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x38, + 0x0a, 0x18, 0x6d, 0x61, 0x78, 0x5f, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, + 0x65, 0x5f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x69, 0x74, 0x79, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0d, + 0x52, 0x16, 0x6d, 0x61, 0x78, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, + 0x56, 0x61, 0x6c, 0x69, 0x64, 0x69, 0x74, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x65, 0x61, 0x6d, + 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x65, 0x61, 0x6d, 0x12, 0x14, 0x0a, 0x05, + 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x6d, 0x61, + 0x69, 0x6c, 0x12, 0x39, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, + 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, + 0x6d, 0x70, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, 0x1d, 0x0a, + 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x62, 0x79, 0x18, 0x0d, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x42, 0x79, 0x22, 0x52, 0x0a, 0x0f, + 0x4e, 0x6f, 0x64, 0x65, 0x41, 0x74, 0x74, 0x65, 0x73, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, + 0x3f, 0x0a, 0x07, 0x61, 0x77, 0x73, 0x5f, 0x69, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x26, 0x2e, 0x62, 0x61, 0x73, 0x65, 0x63, 0x61, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x57, 0x53, + 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, + 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x06, 0x61, 0x77, 0x73, 0x49, 0x69, 0x64, + 0x22, 0xf6, 0x02, 0x0a, 0x1b, 0x41, 0x57, 0x53, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, + 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, + 0x12, 0x19, 0x0a, 0x08, 0x72, 0x6f, 0x6c, 0x65, 0x5f, 0x61, 0x72, 0x6e, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x07, 0x72, 0x6f, 0x6c, 0x65, 0x41, 0x72, 0x6e, 0x12, 0x1f, 0x0a, 0x0b, 0x61, + 0x73, 0x73, 0x75, 0x6d, 0x65, 0x5f, 0x72, 0x6f, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0a, 0x61, 0x73, 0x73, 0x75, 0x6d, 0x65, 0x52, 0x6f, 0x6c, 0x65, 0x12, 0x27, 0x0a, 0x0f, + 0x73, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x5f, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x18, + 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0e, 0x73, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x47, + 0x72, 0x6f, 0x75, 0x70, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x12, 0x1f, 0x0a, + 0x0b, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0a, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x49, 0x64, 0x12, 0x19, + 0x0a, 0x08, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x07, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x49, 0x64, 0x12, 0x5d, 0x0a, 0x0d, 0x69, 0x6e, 0x73, + 0x74, 0x61, 0x6e, 0x63, 0x65, 0x5f, 0x74, 0x61, 0x67, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x38, 0x2e, 0x62, 0x61, 0x73, 0x65, 0x63, 0x61, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x57, 0x53, + 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, + 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, + 0x65, 0x54, 0x61, 0x67, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0c, 0x69, 0x6e, 0x73, 0x74, + 0x61, 0x6e, 0x63, 0x65, 0x54, 0x61, 0x67, 0x73, 0x1a, 0x3f, 0x0a, 0x11, 0x49, 0x6e, 0x73, 0x74, + 0x61, 0x6e, 0x63, 0x65, 0x54, 0x61, 0x67, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, + 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, + 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, + 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x1f, 0x0a, 0x09, 0x41, 0x63, 0x63, + 0x6f, 0x75, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x75, 0x69, 0x64, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x75, 0x75, 0x69, 0x64, 0x22, 0x90, 0x01, 0x0a, 0x20, 0x47, + 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, + 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, + 0x27, 0x0a, 0x0f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x61, 0x63, 0x63, 0x6f, 0x75, + 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, + 0x65, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x20, 0x0a, 0x0b, 0x65, 0x6e, 0x76, 0x69, + 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x65, + 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x21, 0x0a, 0x0c, 0x65, 0x78, + 0x74, 0x65, 0x6e, 0x64, 0x65, 0x64, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0b, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x64, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x22, 0xa5, 0x04, + 0x0a, 0x1e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, + 0x63, 0x65, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x12, 0x27, 0x0a, 0x0f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x61, 0x63, 0x63, 0x6f, + 0x75, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x73, 0x65, 0x72, 0x76, 0x69, + 0x63, 0x65, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x20, 0x0a, 0x0b, 0x65, 0x6e, 0x76, + 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, + 0x65, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x2d, 0x0a, 0x12, 0x72, + 0x65, 0x67, 0x75, 0x6c, 0x61, 0x72, 0x5f, 0x65, 0x78, 0x70, 0x72, 0x65, 0x73, 0x73, 0x69, 0x6f, + 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x72, 0x65, 0x67, 0x75, 0x6c, 0x61, 0x72, + 0x45, 0x78, 0x70, 0x72, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x3a, 0x0a, 0x19, 0x73, 0x75, + 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x61, 0x6c, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x74, 0x69, 0x76, + 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x17, 0x73, + 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x41, 0x6c, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x74, 0x69, 0x76, + 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x12, 0x37, 0x0a, 0x17, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, + 0x69, 0x63, 0x61, 0x74, 0x65, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x74, 0x69, 0x65, + 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x16, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, + 0x63, 0x61, 0x74, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x74, 0x69, 0x65, 0x73, 0x12, + 0x21, 0x0a, 0x0c, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x64, 0x65, 0x64, 0x5f, 0x6b, 0x65, 0x79, 0x18, + 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x64, 0x65, 0x64, 0x4b, + 0x65, 0x79, 0x12, 0x31, 0x0a, 0x14, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, + 0x65, 0x5f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x69, 0x74, 0x79, 0x18, 0x07, 0x20, 0x01, 0x28, 0x05, + 0x52, 0x13, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x56, 0x61, 0x6c, + 0x69, 0x64, 0x69, 0x74, 0x79, 0x12, 0x25, 0x0a, 0x0e, 0x73, 0x75, 0x62, 0x6f, 0x72, 0x64, 0x69, + 0x6e, 0x61, 0x74, 0x65, 0x5f, 0x63, 0x61, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x73, + 0x75, 0x62, 0x6f, 0x72, 0x64, 0x69, 0x6e, 0x61, 0x74, 0x65, 0x43, 0x61, 0x12, 0x45, 0x0a, 0x10, + 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x61, 0x74, 0x74, 0x65, 0x73, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x62, 0x61, 0x73, 0x65, 0x63, 0x61, 0x2e, + 0x76, 0x31, 0x2e, 0x4e, 0x6f, 0x64, 0x65, 0x41, 0x74, 0x74, 0x65, 0x73, 0x74, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x52, 0x0f, 0x6e, 0x6f, 0x64, 0x65, 0x41, 0x74, 0x74, 0x65, 0x73, 0x74, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x65, 0x61, 0x6d, 0x18, 0x0a, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x04, 0x74, 0x65, 0x61, 0x6d, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, + 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x12, 0x1b, 0x0a, + 0x06, 0x72, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, + 0x06, 0x72, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x88, 0x01, 0x01, 0x42, 0x09, 0x0a, 0x07, 0x5f, 0x72, + 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x22, 0x98, 0x05, 0x0a, 0x1f, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, + 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, + 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6c, 0x69, + 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, 0x6c, + 0x69, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, + 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x63, 0x6c, + 0x69, 0x65, 0x6e, 0x74, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x27, 0x0a, 0x0f, 0x73, 0x65, 0x72, + 0x76, 0x69, 0x63, 0x65, 0x5f, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x41, 0x63, 0x63, 0x6f, 0x75, + 0x6e, 0x74, 0x12, 0x20, 0x0a, 0x0b, 0x65, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, + 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x65, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, + 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x2d, 0x0a, 0x12, 0x72, 0x65, 0x67, 0x75, 0x6c, 0x61, 0x72, 0x5f, + 0x65, 0x78, 0x70, 0x72, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x11, 0x72, 0x65, 0x67, 0x75, 0x6c, 0x61, 0x72, 0x45, 0x78, 0x70, 0x72, 0x65, 0x73, 0x73, + 0x69, 0x6f, 0x6e, 0x12, 0x3a, 0x0a, 0x19, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x61, + 0x6c, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x74, 0x69, 0x76, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x73, + 0x18, 0x06, 0x20, 0x03, 0x28, 0x09, 0x52, 0x17, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x41, + 0x6c, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x74, 0x69, 0x76, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x12, + 0x37, 0x0a, 0x17, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x5f, 0x61, + 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x74, 0x69, 0x65, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x09, + 0x52, 0x16, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x41, 0x75, 0x74, + 0x68, 0x6f, 0x72, 0x69, 0x74, 0x69, 0x65, 0x73, 0x12, 0x21, 0x0a, 0x0c, 0x65, 0x78, 0x74, 0x65, + 0x6e, 0x64, 0x65, 0x64, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, + 0x65, 0x78, 0x74, 0x65, 0x6e, 0x64, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x12, 0x45, 0x0a, 0x10, 0x6e, + 0x6f, 0x64, 0x65, 0x5f, 0x61, 0x74, 0x74, 0x65, 0x73, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, + 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x62, 0x61, 0x73, 0x65, 0x63, 0x61, 0x2e, 0x76, + 0x31, 0x2e, 0x4e, 0x6f, 0x64, 0x65, 0x41, 0x74, 0x74, 0x65, 0x73, 0x74, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x52, 0x0f, 0x6e, 0x6f, 0x64, 0x65, 0x41, 0x74, 0x74, 0x65, 0x73, 0x74, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x12, 0x31, 0x0a, 0x14, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, + 0x65, 0x5f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x69, 0x74, 0x79, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x05, + 0x52, 0x13, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x56, 0x61, 0x6c, + 0x69, 0x64, 0x69, 0x74, 0x79, 0x12, 0x25, 0x0a, 0x0e, 0x73, 0x75, 0x62, 0x6f, 0x72, 0x64, 0x69, + 0x6e, 0x61, 0x74, 0x65, 0x5f, 0x63, 0x61, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x73, + 0x75, 0x62, 0x6f, 0x72, 0x64, 0x69, 0x6e, 0x61, 0x74, 0x65, 0x43, 0x61, 0x12, 0x12, 0x0a, 0x04, + 0x74, 0x65, 0x61, 0x6d, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x65, 0x61, 0x6d, + 0x12, 0x14, 0x0a, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x12, 0x39, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, + 0x64, 0x5f, 0x61, 0x74, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, + 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, + 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, + 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x62, 0x79, 0x18, + 0x0f, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x42, 0x79, + 0x22, 0x2e, 0x0a, 0x12, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, + 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, + 0x22, 0x97, 0x01, 0x0a, 0x13, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x44, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, + 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x2c, 0x2e, 0x62, 0x61, 0x73, 0x65, 0x63, + 0x61, 0x2e, 0x76, 0x31, 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, + 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x3a, + 0x0a, 0x0d, 0x53, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, + 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, + 0x53, 0x45, 0x52, 0x56, 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, 0x0f, 0x0a, 0x0b, 0x4e, 0x4f, 0x54, + 0x5f, 0x53, 0x45, 0x52, 0x56, 0x49, 0x4e, 0x47, 0x10, 0x02, 0x32, 0xac, 0x04, 0x0a, 0x0b, 0x43, + 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x12, 0x4d, 0x0a, 0x07, 0x53, 0x69, + 0x67, 0x6e, 0x43, 0x53, 0x52, 0x12, 0x24, 0x2e, 0x62, 0x61, 0x73, 0x65, 0x63, 0x61, 0x2e, 0x76, + 0x31, 0x2e, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x53, 0x69, 0x67, + 0x6e, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x62, 0x61, + 0x73, 0x65, 0x63, 0x61, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x43, 0x65, + 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x12, 0x55, 0x0a, 0x0e, 0x47, 0x65, 0x74, + 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x12, 0x22, 0x2e, 0x62, 0x61, + 0x73, 0x65, 0x63, 0x61, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, + 0x61, 0x74, 0x65, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x1a, + 0x1f, 0x2e, 0x62, 0x61, 0x73, 0x65, 0x63, 0x61, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x65, 0x72, 0x74, + 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, + 0x12, 0x58, 0x0a, 0x10, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, + 0x61, 0x74, 0x65, 0x73, 0x12, 0x22, 0x2e, 0x62, 0x61, 0x73, 0x65, 0x63, 0x61, 0x2e, 0x76, 0x31, + 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, + 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x62, 0x61, 0x73, 0x65, 0x63, + 0x61, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, + 0x73, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x12, 0x5e, 0x0a, 0x11, 0x52, 0x65, + 0x76, 0x6f, 0x6b, 0x65, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x12, + 0x23, 0x2e, 0x62, 0x61, 0x73, 0x65, 0x63, 0x61, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x76, 0x6f, + 0x6b, 0x65, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x24, 0x2e, 0x62, 0x61, 0x73, 0x65, 0x63, 0x61, 0x2e, 0x76, 0x31, + 0x2e, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, + 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x53, 0x0a, 0x11, 0x4f, 0x70, + 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x53, 0x69, 0x67, 0x6e, 0x43, 0x53, 0x52, 0x12, + 0x20, 0x2e, 0x62, 0x61, 0x73, 0x65, 0x63, 0x61, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x70, 0x65, 0x72, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x53, 0x69, 0x67, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x1c, 0x2e, 0x62, 0x61, 0x73, 0x65, 0x63, 0x61, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x69, + 0x67, 0x6e, 0x65, 0x64, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x12, + 0x68, 0x0a, 0x18, 0x51, 0x75, 0x65, 0x72, 0x79, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, + 0x61, 0x74, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x2a, 0x2e, 0x62, 0x61, + 0x73, 0x65, 0x63, 0x61, 0x2e, 0x76, 0x31, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x43, 0x65, 0x72, + 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x62, 0x61, 0x73, 0x65, 0x63, 0x61, + 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x73, + 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x32, 0xe0, 0x03, 0x0a, 0x07, 0x41, 0x63, + 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x46, 0x0a, 0x09, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x55, 0x73, + 0x65, 0x72, 0x12, 0x1b, 0x2e, 0x62, 0x61, 0x73, 0x65, 0x63, 0x61, 0x2e, 0x76, 0x31, 0x2e, 0x4c, + 0x6f, 0x67, 0x69, 0x6e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x1c, 0x2e, 0x62, 0x61, 0x73, 0x65, 0x63, 0x61, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x6f, 0x67, 0x69, + 0x6e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x40, 0x0a, + 0x0a, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x12, 0x1a, 0x2e, 0x62, 0x61, + 0x73, 0x65, 0x63, 0x61, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, + 0x36, 0x0a, 0x07, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x12, 0x1a, 0x2e, 0x62, 0x61, 0x73, + 0x65, 0x63, 0x61, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0f, 0x2e, 0x62, 0x61, 0x73, 0x65, 0x63, 0x61, 0x2e, + 0x76, 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x12, 0x38, 0x0a, 0x09, 0x4c, 0x69, 0x73, 0x74, 0x55, + 0x73, 0x65, 0x72, 0x73, 0x12, 0x19, 0x2e, 0x62, 0x61, 0x73, 0x65, 0x63, 0x61, 0x2e, 0x76, 0x31, + 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x1a, + 0x10, 0x2e, 0x62, 0x61, 0x73, 0x65, 0x63, 0x61, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, + 0x73, 0x12, 0x3b, 0x0a, 0x0a, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x12, + 0x1c, 0x2e, 0x62, 0x61, 0x73, 0x65, 0x63, 0x61, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, + 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0f, 0x2e, + 0x62, 0x61, 0x73, 0x65, 0x63, 0x61, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x12, 0x4d, + 0x0a, 0x15, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x43, 0x72, 0x65, 0x64, + 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x73, 0x12, 0x23, 0x2e, 0x62, 0x61, 0x73, 0x65, 0x63, 0x61, + 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, + 0x74, 0x69, 0x61, 0x6c, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0f, 0x2e, 0x62, + 0x61, 0x73, 0x65, 0x63, 0x61, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x12, 0x4d, 0x0a, + 0x15, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x50, 0x65, 0x72, 0x6d, 0x69, + 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x23, 0x2e, 0x62, 0x61, 0x73, 0x65, 0x63, 0x61, 0x2e, + 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, + 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0f, 0x2e, 0x62, 0x61, + 0x73, 0x65, 0x63, 0x61, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x32, 0xd8, 0x07, 0x0a, + 0x07, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x67, 0x0a, 0x14, 0x43, 0x72, 0x65, 0x61, + 0x74, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, + 0x12, 0x26, 0x2e, 0x62, 0x61, 0x73, 0x65, 0x63, 0x61, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, + 0x61, 0x74, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, + 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x62, 0x61, 0x73, 0x65, 0x63, + 0x61, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, + 0x63, 0x65, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x73, 0x0a, 0x18, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x50, 0x72, 0x6f, 0x76, 0x69, + 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x2a, 0x2e, + 0x62, 0x61, 0x73, 0x65, 0x63, 0x61, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, + 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x41, 0x63, 0x63, 0x6f, 0x75, + 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2b, 0x2e, 0x62, 0x61, 0x73, 0x65, + 0x63, 0x61, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x50, 0x72, 0x6f, 0x76, + 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4c, 0x0a, 0x15, 0x47, 0x65, 0x74, 0x50, 0x72, 0x6f, + 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, + 0x14, 0x2e, 0x62, 0x61, 0x73, 0x65, 0x63, 0x61, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x63, 0x63, 0x6f, + 0x75, 0x6e, 0x74, 0x49, 0x64, 0x1a, 0x1d, 0x2e, 0x62, 0x61, 0x73, 0x65, 0x63, 0x61, 0x2e, 0x76, + 0x31, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x41, 0x63, 0x63, + 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x44, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, + 0x63, 0x65, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x14, 0x2e, 0x62, 0x61, 0x73, 0x65, + 0x63, 0x61, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x49, 0x64, 0x1a, + 0x19, 0x2e, 0x62, 0x61, 0x73, 0x65, 0x63, 0x61, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x65, 0x72, 0x76, + 0x69, 0x63, 0x65, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x64, 0x0a, 0x19, 0x47, 0x65, + 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x4d, + 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x2b, 0x2e, 0x62, 0x61, 0x73, 0x65, 0x63, 0x61, + 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x41, 0x63, + 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x62, 0x61, 0x73, 0x65, 0x63, 0x61, 0x2e, 0x76, 0x31, + 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, + 0x12, 0x44, 0x0a, 0x14, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, + 0x65, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x14, 0x2e, 0x62, 0x61, 0x73, 0x65, 0x63, + 0x61, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x49, 0x64, 0x1a, 0x16, + 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, + 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x4f, 0x0a, 0x1f, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, + 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x64, 0x53, 0x65, 0x72, 0x76, 0x69, + 0x63, 0x65, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x14, 0x2e, 0x62, 0x61, 0x73, 0x65, + 0x63, 0x61, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x49, 0x64, 0x1a, + 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, + 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x48, 0x0a, 0x18, 0x44, 0x65, 0x6c, 0x65, 0x74, + 0x65, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x41, 0x63, 0x63, 0x6f, + 0x75, 0x6e, 0x74, 0x12, 0x14, 0x2e, 0x62, 0x61, 0x73, 0x65, 0x63, 0x61, 0x2e, 0x76, 0x31, 0x2e, + 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x49, 0x64, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, + 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, + 0x79, 0x12, 0x70, 0x0a, 0x17, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x53, 0x65, + 0x72, 0x76, 0x69, 0x63, 0x65, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x29, 0x2e, 0x62, + 0x61, 0x73, 0x65, 0x63, 0x61, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, + 0x6f, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, 0x2e, 0x62, 0x61, 0x73, 0x65, 0x63, 0x61, + 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x72, + 0x76, 0x69, 0x63, 0x65, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x4c, 0x0a, 0x13, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, + 0x63, 0x65, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x12, 0x19, 0x2e, 0x62, 0x61, 0x73, + 0x65, 0x63, 0x61, 0x2e, 0x76, 0x31, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x50, 0x61, 0x72, 0x61, + 0x6d, 0x65, 0x74, 0x65, 0x72, 0x1a, 0x1a, 0x2e, 0x62, 0x61, 0x73, 0x65, 0x63, 0x61, 0x2e, 0x76, + 0x31, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, + 0x73, 0x12, 0x54, 0x0a, 0x17, 0x4c, 0x69, 0x73, 0x74, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, + 0x6f, 0x6e, 0x65, 0x72, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x12, 0x19, 0x2e, 0x62, + 0x61, 0x73, 0x65, 0x63, 0x61, 0x2e, 0x76, 0x31, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x50, 0x61, + 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x1a, 0x1e, 0x2e, 0x62, 0x61, 0x73, 0x65, 0x63, 0x61, + 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x41, + 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x32, 0x50, 0x0a, 0x06, 0x48, 0x65, 0x61, 0x6c, 0x74, + 0x68, 0x12, 0x46, 0x0a, 0x05, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x12, 0x1d, 0x2e, 0x62, 0x61, 0x73, + 0x65, 0x63, 0x61, 0x2e, 0x76, 0x31, 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, + 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x62, 0x61, 0x73, 0x65, + 0x63, 0x61, 0x2e, 0x76, 0x31, 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, + 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x33, 0x5a, 0x31, 0x67, 0x69, 0x74, + 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, + 0x2f, 0x62, 0x61, 0x73, 0x65, 0x63, 0x61, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x73, 0x2f, 0x62, + 0x61, 0x73, 0x65, 0x63, 0x61, 0x2f, 0x76, 0x31, 0x3b, 0x61, 0x70, 0x69, 0x76, 0x31, 0x62, 0x06, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_baseca_v1_api_proto_rawDescOnce sync.Once + file_baseca_v1_api_proto_rawDescData = file_baseca_v1_api_proto_rawDesc +) + +func file_baseca_v1_api_proto_rawDescGZIP() []byte { + file_baseca_v1_api_proto_rawDescOnce.Do(func() { + file_baseca_v1_api_proto_rawDescData = protoimpl.X.CompressGZIP(file_baseca_v1_api_proto_rawDescData) + }) + return file_baseca_v1_api_proto_rawDescData +} + +var file_baseca_v1_api_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_baseca_v1_api_proto_msgTypes = make([]protoimpl.MessageInfo, 38) +var file_baseca_v1_api_proto_goTypes = []interface{}{ + (HealthCheckResponse_ServingStatus)(0), // 0: baseca.v1.HealthCheckResponse.ServingStatus + (*CertificateParameter)(nil), // 1: baseca.v1.CertificateParameter + (*CertificatesParameter)(nil), // 2: baseca.v1.CertificatesParameter + (*CertificateAuthorityParameter)(nil), // 3: baseca.v1.CertificateAuthorityParameter + (*CertificateSigningRequest)(nil), // 4: baseca.v1.CertificateSigningRequest + (*SignedCertificate)(nil), // 5: baseca.v1.SignedCertificate + (*CertificateSerialNumber)(nil), // 6: baseca.v1.CertificateSerialNumber + (*ListCertificatesRequest)(nil), // 7: baseca.v1.ListCertificatesRequest + (*RevokeCertificateRequest)(nil), // 8: baseca.v1.RevokeCertificateRequest + (*RevokeCertificateResponse)(nil), // 9: baseca.v1.RevokeCertificateResponse + (*OperationsSignRequest)(nil), // 10: baseca.v1.OperationsSignRequest + (*Environment)(nil), // 11: baseca.v1.Environment + (*QueryCertificateMetadataRequest)(nil), // 12: baseca.v1.QueryCertificateMetadataRequest + (*User)(nil), // 13: baseca.v1.User + (*Users)(nil), // 14: baseca.v1.Users + (*LoginUserRequest)(nil), // 15: baseca.v1.LoginUserRequest + (*LoginUserResponse)(nil), // 16: baseca.v1.LoginUserResponse + (*UsernameRequest)(nil), // 17: baseca.v1.UsernameRequest + (*QueryParameter)(nil), // 18: baseca.v1.QueryParameter + (*CreateUserRequest)(nil), // 19: baseca.v1.CreateUserRequest + (*UpdateCredentialsRequest)(nil), // 20: baseca.v1.UpdateCredentialsRequest + (*UpdatePermissionsRequest)(nil), // 21: baseca.v1.UpdatePermissionsRequest + (*CreateServiceAccountRequest)(nil), // 22: baseca.v1.CreateServiceAccountRequest + (*CreateServiceAccountResponse)(nil), // 23: baseca.v1.CreateServiceAccountResponse + (*ServiceAccount)(nil), // 24: baseca.v1.ServiceAccount + (*ServiceAccounts)(nil), // 25: baseca.v1.ServiceAccounts + (*CreateProvisionerAccountRequest)(nil), // 26: baseca.v1.CreateProvisionerAccountRequest + (*CreateProvisionerAccountResponse)(nil), // 27: baseca.v1.CreateProvisionerAccountResponse + (*ProvisionerAccounts)(nil), // 28: baseca.v1.ProvisionerAccounts + (*ProvisionerAccount)(nil), // 29: baseca.v1.ProvisionerAccount + (*NodeAttestation)(nil), // 30: baseca.v1.NodeAttestation + (*AWSInstanceIdentityDocument)(nil), // 31: baseca.v1.AWSInstanceIdentityDocument + (*AccountId)(nil), // 32: baseca.v1.AccountId + (*GetServiceAccountMetadataRequest)(nil), // 33: baseca.v1.GetServiceAccountMetadataRequest + (*ProvisionServiceAccountRequest)(nil), // 34: baseca.v1.ProvisionServiceAccountRequest + (*ProvisionServiceAccountResponse)(nil), // 35: baseca.v1.ProvisionServiceAccountResponse + (*HealthCheckRequest)(nil), // 36: baseca.v1.HealthCheckRequest + (*HealthCheckResponse)(nil), // 37: baseca.v1.HealthCheckResponse + nil, // 38: baseca.v1.AWSInstanceIdentityDocument.InstanceTagsEntry + (*timestamppb.Timestamp)(nil), // 39: google.protobuf.Timestamp + (*emptypb.Empty)(nil), // 40: google.protobuf.Empty +} +var file_baseca_v1_api_proto_depIdxs = []int32{ + 39, // 0: baseca.v1.CertificateParameter.expiration_date:type_name -> google.protobuf.Timestamp + 39, // 1: baseca.v1.CertificateParameter.issued_date:type_name -> google.protobuf.Timestamp + 39, // 2: baseca.v1.CertificateParameter.revoke_date:type_name -> google.protobuf.Timestamp + 1, // 3: baseca.v1.CertificatesParameter.certificates:type_name -> baseca.v1.CertificateParameter + 1, // 4: baseca.v1.SignedCertificate.metadata:type_name -> baseca.v1.CertificateParameter + 39, // 5: baseca.v1.RevokeCertificateResponse.revocation_date:type_name -> google.protobuf.Timestamp + 3, // 6: baseca.v1.OperationsSignRequest.certificate_authority:type_name -> baseca.v1.CertificateAuthorityParameter + 39, // 7: baseca.v1.User.credential_changed_at:type_name -> google.protobuf.Timestamp + 39, // 8: baseca.v1.User.created_at:type_name -> google.protobuf.Timestamp + 13, // 9: baseca.v1.Users.users:type_name -> baseca.v1.User + 13, // 10: baseca.v1.LoginUserResponse.user:type_name -> baseca.v1.User + 30, // 11: baseca.v1.CreateServiceAccountRequest.node_attestation:type_name -> baseca.v1.NodeAttestation + 30, // 12: baseca.v1.CreateServiceAccountResponse.node_attestation:type_name -> baseca.v1.NodeAttestation + 39, // 13: baseca.v1.CreateServiceAccountResponse.created_at:type_name -> google.protobuf.Timestamp + 30, // 14: baseca.v1.ServiceAccount.node_attestation:type_name -> baseca.v1.NodeAttestation + 39, // 15: baseca.v1.ServiceAccount.created_at:type_name -> google.protobuf.Timestamp + 24, // 16: baseca.v1.ServiceAccounts.service_accounts:type_name -> baseca.v1.ServiceAccount + 30, // 17: baseca.v1.CreateProvisionerAccountRequest.node_attestation:type_name -> baseca.v1.NodeAttestation + 30, // 18: baseca.v1.CreateProvisionerAccountResponse.node_attestation:type_name -> baseca.v1.NodeAttestation + 39, // 19: baseca.v1.CreateProvisionerAccountResponse.created_at:type_name -> google.protobuf.Timestamp + 29, // 20: baseca.v1.ProvisionerAccounts.provisioner_accounts:type_name -> baseca.v1.ProvisionerAccount + 30, // 21: baseca.v1.ProvisionerAccount.node_attestation:type_name -> baseca.v1.NodeAttestation + 39, // 22: baseca.v1.ProvisionerAccount.created_at:type_name -> google.protobuf.Timestamp + 31, // 23: baseca.v1.NodeAttestation.aws_iid:type_name -> baseca.v1.AWSInstanceIdentityDocument + 38, // 24: baseca.v1.AWSInstanceIdentityDocument.instance_tags:type_name -> baseca.v1.AWSInstanceIdentityDocument.InstanceTagsEntry + 30, // 25: baseca.v1.ProvisionServiceAccountRequest.node_attestation:type_name -> baseca.v1.NodeAttestation + 30, // 26: baseca.v1.ProvisionServiceAccountResponse.node_attestation:type_name -> baseca.v1.NodeAttestation + 39, // 27: baseca.v1.ProvisionServiceAccountResponse.created_at:type_name -> google.protobuf.Timestamp + 0, // 28: baseca.v1.HealthCheckResponse.status:type_name -> baseca.v1.HealthCheckResponse.ServingStatus + 4, // 29: baseca.v1.Certificate.SignCSR:input_type -> baseca.v1.CertificateSigningRequest + 6, // 30: baseca.v1.Certificate.GetCertificate:input_type -> baseca.v1.CertificateSerialNumber + 7, // 31: baseca.v1.Certificate.ListCertificates:input_type -> baseca.v1.ListCertificatesRequest + 8, // 32: baseca.v1.Certificate.RevokeCertificate:input_type -> baseca.v1.RevokeCertificateRequest + 10, // 33: baseca.v1.Certificate.OperationsSignCSR:input_type -> baseca.v1.OperationsSignRequest + 12, // 34: baseca.v1.Certificate.QueryCertificateMetadata:input_type -> baseca.v1.QueryCertificateMetadataRequest + 15, // 35: baseca.v1.Account.LoginUser:input_type -> baseca.v1.LoginUserRequest + 17, // 36: baseca.v1.Account.DeleteUser:input_type -> baseca.v1.UsernameRequest + 17, // 37: baseca.v1.Account.GetUser:input_type -> baseca.v1.UsernameRequest + 18, // 38: baseca.v1.Account.ListUsers:input_type -> baseca.v1.QueryParameter + 19, // 39: baseca.v1.Account.CreateUser:input_type -> baseca.v1.CreateUserRequest + 20, // 40: baseca.v1.Account.UpdateUserCredentials:input_type -> baseca.v1.UpdateCredentialsRequest + 21, // 41: baseca.v1.Account.UpdateUserPermissions:input_type -> baseca.v1.UpdatePermissionsRequest + 22, // 42: baseca.v1.Service.CreateServiceAccount:input_type -> baseca.v1.CreateServiceAccountRequest + 26, // 43: baseca.v1.Service.CreateProvisionerAccount:input_type -> baseca.v1.CreateProvisionerAccountRequest + 32, // 44: baseca.v1.Service.GetProvisionerAccount:input_type -> baseca.v1.AccountId + 32, // 45: baseca.v1.Service.GetServiceAccount:input_type -> baseca.v1.AccountId + 33, // 46: baseca.v1.Service.GetServiceAccountMetadata:input_type -> baseca.v1.GetServiceAccountMetadataRequest + 32, // 47: baseca.v1.Service.DeleteServiceAccount:input_type -> baseca.v1.AccountId + 32, // 48: baseca.v1.Service.DeleteProvisionedServiceAccount:input_type -> baseca.v1.AccountId + 32, // 49: baseca.v1.Service.DeleteProvisionerAccount:input_type -> baseca.v1.AccountId + 34, // 50: baseca.v1.Service.ProvisionServiceAccount:input_type -> baseca.v1.ProvisionServiceAccountRequest + 18, // 51: baseca.v1.Service.ListServiceAccounts:input_type -> baseca.v1.QueryParameter + 18, // 52: baseca.v1.Service.ListProvisionerAccounts:input_type -> baseca.v1.QueryParameter + 36, // 53: baseca.v1.Health.Check:input_type -> baseca.v1.HealthCheckRequest + 5, // 54: baseca.v1.Certificate.SignCSR:output_type -> baseca.v1.SignedCertificate + 1, // 55: baseca.v1.Certificate.GetCertificate:output_type -> baseca.v1.CertificateParameter + 2, // 56: baseca.v1.Certificate.ListCertificates:output_type -> baseca.v1.CertificatesParameter + 9, // 57: baseca.v1.Certificate.RevokeCertificate:output_type -> baseca.v1.RevokeCertificateResponse + 5, // 58: baseca.v1.Certificate.OperationsSignCSR:output_type -> baseca.v1.SignedCertificate + 2, // 59: baseca.v1.Certificate.QueryCertificateMetadata:output_type -> baseca.v1.CertificatesParameter + 16, // 60: baseca.v1.Account.LoginUser:output_type -> baseca.v1.LoginUserResponse + 40, // 61: baseca.v1.Account.DeleteUser:output_type -> google.protobuf.Empty + 13, // 62: baseca.v1.Account.GetUser:output_type -> baseca.v1.User + 14, // 63: baseca.v1.Account.ListUsers:output_type -> baseca.v1.Users + 13, // 64: baseca.v1.Account.CreateUser:output_type -> baseca.v1.User + 13, // 65: baseca.v1.Account.UpdateUserCredentials:output_type -> baseca.v1.User + 13, // 66: baseca.v1.Account.UpdateUserPermissions:output_type -> baseca.v1.User + 23, // 67: baseca.v1.Service.CreateServiceAccount:output_type -> baseca.v1.CreateServiceAccountResponse + 27, // 68: baseca.v1.Service.CreateProvisionerAccount:output_type -> baseca.v1.CreateProvisionerAccountResponse + 29, // 69: baseca.v1.Service.GetProvisionerAccount:output_type -> baseca.v1.ProvisionerAccount + 24, // 70: baseca.v1.Service.GetServiceAccount:output_type -> baseca.v1.ServiceAccount + 25, // 71: baseca.v1.Service.GetServiceAccountMetadata:output_type -> baseca.v1.ServiceAccounts + 40, // 72: baseca.v1.Service.DeleteServiceAccount:output_type -> google.protobuf.Empty + 40, // 73: baseca.v1.Service.DeleteProvisionedServiceAccount:output_type -> google.protobuf.Empty + 40, // 74: baseca.v1.Service.DeleteProvisionerAccount:output_type -> google.protobuf.Empty + 35, // 75: baseca.v1.Service.ProvisionServiceAccount:output_type -> baseca.v1.ProvisionServiceAccountResponse + 25, // 76: baseca.v1.Service.ListServiceAccounts:output_type -> baseca.v1.ServiceAccounts + 28, // 77: baseca.v1.Service.ListProvisionerAccounts:output_type -> baseca.v1.ProvisionerAccounts + 37, // 78: baseca.v1.Health.Check:output_type -> baseca.v1.HealthCheckResponse + 54, // [54:79] is the sub-list for method output_type + 29, // [29:54] is the sub-list for method input_type + 29, // [29:29] is the sub-list for extension type_name + 29, // [29:29] is the sub-list for extension extendee + 0, // [0:29] is the sub-list for field type_name +} + +func init() { file_baseca_v1_api_proto_init() } +func file_baseca_v1_api_proto_init() { + if File_baseca_v1_api_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_baseca_v1_api_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CertificateParameter); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_baseca_v1_api_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CertificatesParameter); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_baseca_v1_api_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CertificateAuthorityParameter); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_baseca_v1_api_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CertificateSigningRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_baseca_v1_api_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SignedCertificate); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_baseca_v1_api_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CertificateSerialNumber); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_baseca_v1_api_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ListCertificatesRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_baseca_v1_api_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RevokeCertificateRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_baseca_v1_api_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RevokeCertificateResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_baseca_v1_api_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*OperationsSignRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_baseca_v1_api_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Environment); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_baseca_v1_api_proto_msgTypes[11].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*QueryCertificateMetadataRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_baseca_v1_api_proto_msgTypes[12].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*User); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_baseca_v1_api_proto_msgTypes[13].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Users); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_baseca_v1_api_proto_msgTypes[14].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*LoginUserRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_baseca_v1_api_proto_msgTypes[15].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*LoginUserResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_baseca_v1_api_proto_msgTypes[16].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*UsernameRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_baseca_v1_api_proto_msgTypes[17].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*QueryParameter); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_baseca_v1_api_proto_msgTypes[18].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CreateUserRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_baseca_v1_api_proto_msgTypes[19].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*UpdateCredentialsRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_baseca_v1_api_proto_msgTypes[20].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*UpdatePermissionsRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_baseca_v1_api_proto_msgTypes[21].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CreateServiceAccountRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_baseca_v1_api_proto_msgTypes[22].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CreateServiceAccountResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_baseca_v1_api_proto_msgTypes[23].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ServiceAccount); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_baseca_v1_api_proto_msgTypes[24].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ServiceAccounts); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_baseca_v1_api_proto_msgTypes[25].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CreateProvisionerAccountRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_baseca_v1_api_proto_msgTypes[26].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CreateProvisionerAccountResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_baseca_v1_api_proto_msgTypes[27].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ProvisionerAccounts); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_baseca_v1_api_proto_msgTypes[28].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ProvisionerAccount); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_baseca_v1_api_proto_msgTypes[29].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*NodeAttestation); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_baseca_v1_api_proto_msgTypes[30].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*AWSInstanceIdentityDocument); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_baseca_v1_api_proto_msgTypes[31].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*AccountId); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_baseca_v1_api_proto_msgTypes[32].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GetServiceAccountMetadataRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_baseca_v1_api_proto_msgTypes[33].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ProvisionServiceAccountRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_baseca_v1_api_proto_msgTypes[34].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ProvisionServiceAccountResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_baseca_v1_api_proto_msgTypes[35].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*HealthCheckRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_baseca_v1_api_proto_msgTypes[36].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*HealthCheckResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + file_baseca_v1_api_proto_msgTypes[9].OneofWrappers = []interface{}{} + file_baseca_v1_api_proto_msgTypes[21].OneofWrappers = []interface{}{} + file_baseca_v1_api_proto_msgTypes[25].OneofWrappers = []interface{}{} + file_baseca_v1_api_proto_msgTypes[33].OneofWrappers = []interface{}{} + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_baseca_v1_api_proto_rawDesc, + NumEnums: 1, + NumMessages: 38, + NumExtensions: 0, + NumServices: 4, + }, + GoTypes: file_baseca_v1_api_proto_goTypes, + DependencyIndexes: file_baseca_v1_api_proto_depIdxs, + EnumInfos: file_baseca_v1_api_proto_enumTypes, + MessageInfos: file_baseca_v1_api_proto_msgTypes, + }.Build() + File_baseca_v1_api_proto = out.File + file_baseca_v1_api_proto_rawDesc = nil + file_baseca_v1_api_proto_goTypes = nil + file_baseca_v1_api_proto_depIdxs = nil +} diff --git a/gen/go/baseca/v1/api_grpc.pb.go b/gen/go/baseca/v1/api_grpc.pb.go new file mode 100644 index 0000000..aa99356 --- /dev/null +++ b/gen/go/baseca/v1/api_grpc.pb.go @@ -0,0 +1,1120 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.2.0 +// - protoc (unknown) +// source: baseca/v1/api.proto + +package apiv1 + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" + emptypb "google.golang.org/protobuf/types/known/emptypb" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.32.0 or later. +const _ = grpc.SupportPackageIsVersion7 + +// CertificateClient is the client API for Certificate service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type CertificateClient interface { + SignCSR(ctx context.Context, in *CertificateSigningRequest, opts ...grpc.CallOption) (*SignedCertificate, error) + GetCertificate(ctx context.Context, in *CertificateSerialNumber, opts ...grpc.CallOption) (*CertificateParameter, error) + ListCertificates(ctx context.Context, in *ListCertificatesRequest, opts ...grpc.CallOption) (*CertificatesParameter, error) + RevokeCertificate(ctx context.Context, in *RevokeCertificateRequest, opts ...grpc.CallOption) (*RevokeCertificateResponse, error) + OperationsSignCSR(ctx context.Context, in *OperationsSignRequest, opts ...grpc.CallOption) (*SignedCertificate, error) + QueryCertificateMetadata(ctx context.Context, in *QueryCertificateMetadataRequest, opts ...grpc.CallOption) (*CertificatesParameter, error) +} + +type certificateClient struct { + cc grpc.ClientConnInterface +} + +func NewCertificateClient(cc grpc.ClientConnInterface) CertificateClient { + return &certificateClient{cc} +} + +func (c *certificateClient) SignCSR(ctx context.Context, in *CertificateSigningRequest, opts ...grpc.CallOption) (*SignedCertificate, error) { + out := new(SignedCertificate) + err := c.cc.Invoke(ctx, "/baseca.v1.Certificate/SignCSR", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *certificateClient) GetCertificate(ctx context.Context, in *CertificateSerialNumber, opts ...grpc.CallOption) (*CertificateParameter, error) { + out := new(CertificateParameter) + err := c.cc.Invoke(ctx, "/baseca.v1.Certificate/GetCertificate", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *certificateClient) ListCertificates(ctx context.Context, in *ListCertificatesRequest, opts ...grpc.CallOption) (*CertificatesParameter, error) { + out := new(CertificatesParameter) + err := c.cc.Invoke(ctx, "/baseca.v1.Certificate/ListCertificates", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *certificateClient) RevokeCertificate(ctx context.Context, in *RevokeCertificateRequest, opts ...grpc.CallOption) (*RevokeCertificateResponse, error) { + out := new(RevokeCertificateResponse) + err := c.cc.Invoke(ctx, "/baseca.v1.Certificate/RevokeCertificate", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *certificateClient) OperationsSignCSR(ctx context.Context, in *OperationsSignRequest, opts ...grpc.CallOption) (*SignedCertificate, error) { + out := new(SignedCertificate) + err := c.cc.Invoke(ctx, "/baseca.v1.Certificate/OperationsSignCSR", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *certificateClient) QueryCertificateMetadata(ctx context.Context, in *QueryCertificateMetadataRequest, opts ...grpc.CallOption) (*CertificatesParameter, error) { + out := new(CertificatesParameter) + err := c.cc.Invoke(ctx, "/baseca.v1.Certificate/QueryCertificateMetadata", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// CertificateServer is the server API for Certificate service. +// All implementations must embed UnimplementedCertificateServer +// for forward compatibility +type CertificateServer interface { + SignCSR(context.Context, *CertificateSigningRequest) (*SignedCertificate, error) + GetCertificate(context.Context, *CertificateSerialNumber) (*CertificateParameter, error) + ListCertificates(context.Context, *ListCertificatesRequest) (*CertificatesParameter, error) + RevokeCertificate(context.Context, *RevokeCertificateRequest) (*RevokeCertificateResponse, error) + OperationsSignCSR(context.Context, *OperationsSignRequest) (*SignedCertificate, error) + QueryCertificateMetadata(context.Context, *QueryCertificateMetadataRequest) (*CertificatesParameter, error) + mustEmbedUnimplementedCertificateServer() +} + +// UnimplementedCertificateServer must be embedded to have forward compatible implementations. +type UnimplementedCertificateServer struct { +} + +func (UnimplementedCertificateServer) SignCSR(context.Context, *CertificateSigningRequest) (*SignedCertificate, error) { + return nil, status.Errorf(codes.Unimplemented, "method SignCSR not implemented") +} +func (UnimplementedCertificateServer) GetCertificate(context.Context, *CertificateSerialNumber) (*CertificateParameter, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetCertificate not implemented") +} +func (UnimplementedCertificateServer) ListCertificates(context.Context, *ListCertificatesRequest) (*CertificatesParameter, error) { + return nil, status.Errorf(codes.Unimplemented, "method ListCertificates not implemented") +} +func (UnimplementedCertificateServer) RevokeCertificate(context.Context, *RevokeCertificateRequest) (*RevokeCertificateResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method RevokeCertificate not implemented") +} +func (UnimplementedCertificateServer) OperationsSignCSR(context.Context, *OperationsSignRequest) (*SignedCertificate, error) { + return nil, status.Errorf(codes.Unimplemented, "method OperationsSignCSR not implemented") +} +func (UnimplementedCertificateServer) QueryCertificateMetadata(context.Context, *QueryCertificateMetadataRequest) (*CertificatesParameter, error) { + return nil, status.Errorf(codes.Unimplemented, "method QueryCertificateMetadata not implemented") +} +func (UnimplementedCertificateServer) mustEmbedUnimplementedCertificateServer() {} + +// UnsafeCertificateServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to CertificateServer will +// result in compilation errors. +type UnsafeCertificateServer interface { + mustEmbedUnimplementedCertificateServer() +} + +func RegisterCertificateServer(s grpc.ServiceRegistrar, srv CertificateServer) { + s.RegisterService(&Certificate_ServiceDesc, srv) +} + +func _Certificate_SignCSR_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CertificateSigningRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(CertificateServer).SignCSR(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/baseca.v1.Certificate/SignCSR", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(CertificateServer).SignCSR(ctx, req.(*CertificateSigningRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _Certificate_GetCertificate_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CertificateSerialNumber) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(CertificateServer).GetCertificate(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/baseca.v1.Certificate/GetCertificate", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(CertificateServer).GetCertificate(ctx, req.(*CertificateSerialNumber)) + } + return interceptor(ctx, in, info, handler) +} + +func _Certificate_ListCertificates_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListCertificatesRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(CertificateServer).ListCertificates(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/baseca.v1.Certificate/ListCertificates", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(CertificateServer).ListCertificates(ctx, req.(*ListCertificatesRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _Certificate_RevokeCertificate_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(RevokeCertificateRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(CertificateServer).RevokeCertificate(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/baseca.v1.Certificate/RevokeCertificate", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(CertificateServer).RevokeCertificate(ctx, req.(*RevokeCertificateRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _Certificate_OperationsSignCSR_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(OperationsSignRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(CertificateServer).OperationsSignCSR(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/baseca.v1.Certificate/OperationsSignCSR", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(CertificateServer).OperationsSignCSR(ctx, req.(*OperationsSignRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _Certificate_QueryCertificateMetadata_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(QueryCertificateMetadataRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(CertificateServer).QueryCertificateMetadata(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/baseca.v1.Certificate/QueryCertificateMetadata", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(CertificateServer).QueryCertificateMetadata(ctx, req.(*QueryCertificateMetadataRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// Certificate_ServiceDesc is the grpc.ServiceDesc for Certificate service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var Certificate_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "baseca.v1.Certificate", + HandlerType: (*CertificateServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "SignCSR", + Handler: _Certificate_SignCSR_Handler, + }, + { + MethodName: "GetCertificate", + Handler: _Certificate_GetCertificate_Handler, + }, + { + MethodName: "ListCertificates", + Handler: _Certificate_ListCertificates_Handler, + }, + { + MethodName: "RevokeCertificate", + Handler: _Certificate_RevokeCertificate_Handler, + }, + { + MethodName: "OperationsSignCSR", + Handler: _Certificate_OperationsSignCSR_Handler, + }, + { + MethodName: "QueryCertificateMetadata", + Handler: _Certificate_QueryCertificateMetadata_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "baseca/v1/api.proto", +} + +// AccountClient is the client API for Account service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type AccountClient interface { + LoginUser(ctx context.Context, in *LoginUserRequest, opts ...grpc.CallOption) (*LoginUserResponse, error) + DeleteUser(ctx context.Context, in *UsernameRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) + GetUser(ctx context.Context, in *UsernameRequest, opts ...grpc.CallOption) (*User, error) + ListUsers(ctx context.Context, in *QueryParameter, opts ...grpc.CallOption) (*Users, error) + CreateUser(ctx context.Context, in *CreateUserRequest, opts ...grpc.CallOption) (*User, error) + UpdateUserCredentials(ctx context.Context, in *UpdateCredentialsRequest, opts ...grpc.CallOption) (*User, error) + UpdateUserPermissions(ctx context.Context, in *UpdatePermissionsRequest, opts ...grpc.CallOption) (*User, error) +} + +type accountClient struct { + cc grpc.ClientConnInterface +} + +func NewAccountClient(cc grpc.ClientConnInterface) AccountClient { + return &accountClient{cc} +} + +func (c *accountClient) LoginUser(ctx context.Context, in *LoginUserRequest, opts ...grpc.CallOption) (*LoginUserResponse, error) { + out := new(LoginUserResponse) + err := c.cc.Invoke(ctx, "/baseca.v1.Account/LoginUser", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *accountClient) DeleteUser(ctx context.Context, in *UsernameRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, "/baseca.v1.Account/DeleteUser", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *accountClient) GetUser(ctx context.Context, in *UsernameRequest, opts ...grpc.CallOption) (*User, error) { + out := new(User) + err := c.cc.Invoke(ctx, "/baseca.v1.Account/GetUser", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *accountClient) ListUsers(ctx context.Context, in *QueryParameter, opts ...grpc.CallOption) (*Users, error) { + out := new(Users) + err := c.cc.Invoke(ctx, "/baseca.v1.Account/ListUsers", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *accountClient) CreateUser(ctx context.Context, in *CreateUserRequest, opts ...grpc.CallOption) (*User, error) { + out := new(User) + err := c.cc.Invoke(ctx, "/baseca.v1.Account/CreateUser", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *accountClient) UpdateUserCredentials(ctx context.Context, in *UpdateCredentialsRequest, opts ...grpc.CallOption) (*User, error) { + out := new(User) + err := c.cc.Invoke(ctx, "/baseca.v1.Account/UpdateUserCredentials", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *accountClient) UpdateUserPermissions(ctx context.Context, in *UpdatePermissionsRequest, opts ...grpc.CallOption) (*User, error) { + out := new(User) + err := c.cc.Invoke(ctx, "/baseca.v1.Account/UpdateUserPermissions", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// AccountServer is the server API for Account service. +// All implementations must embed UnimplementedAccountServer +// for forward compatibility +type AccountServer interface { + LoginUser(context.Context, *LoginUserRequest) (*LoginUserResponse, error) + DeleteUser(context.Context, *UsernameRequest) (*emptypb.Empty, error) + GetUser(context.Context, *UsernameRequest) (*User, error) + ListUsers(context.Context, *QueryParameter) (*Users, error) + CreateUser(context.Context, *CreateUserRequest) (*User, error) + UpdateUserCredentials(context.Context, *UpdateCredentialsRequest) (*User, error) + UpdateUserPermissions(context.Context, *UpdatePermissionsRequest) (*User, error) + mustEmbedUnimplementedAccountServer() +} + +// UnimplementedAccountServer must be embedded to have forward compatible implementations. +type UnimplementedAccountServer struct { +} + +func (UnimplementedAccountServer) LoginUser(context.Context, *LoginUserRequest) (*LoginUserResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method LoginUser not implemented") +} +func (UnimplementedAccountServer) DeleteUser(context.Context, *UsernameRequest) (*emptypb.Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method DeleteUser not implemented") +} +func (UnimplementedAccountServer) GetUser(context.Context, *UsernameRequest) (*User, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetUser not implemented") +} +func (UnimplementedAccountServer) ListUsers(context.Context, *QueryParameter) (*Users, error) { + return nil, status.Errorf(codes.Unimplemented, "method ListUsers not implemented") +} +func (UnimplementedAccountServer) CreateUser(context.Context, *CreateUserRequest) (*User, error) { + return nil, status.Errorf(codes.Unimplemented, "method CreateUser not implemented") +} +func (UnimplementedAccountServer) UpdateUserCredentials(context.Context, *UpdateCredentialsRequest) (*User, error) { + return nil, status.Errorf(codes.Unimplemented, "method UpdateUserCredentials not implemented") +} +func (UnimplementedAccountServer) UpdateUserPermissions(context.Context, *UpdatePermissionsRequest) (*User, error) { + return nil, status.Errorf(codes.Unimplemented, "method UpdateUserPermissions not implemented") +} +func (UnimplementedAccountServer) mustEmbedUnimplementedAccountServer() {} + +// UnsafeAccountServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to AccountServer will +// result in compilation errors. +type UnsafeAccountServer interface { + mustEmbedUnimplementedAccountServer() +} + +func RegisterAccountServer(s grpc.ServiceRegistrar, srv AccountServer) { + s.RegisterService(&Account_ServiceDesc, srv) +} + +func _Account_LoginUser_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(LoginUserRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AccountServer).LoginUser(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/baseca.v1.Account/LoginUser", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AccountServer).LoginUser(ctx, req.(*LoginUserRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _Account_DeleteUser_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UsernameRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AccountServer).DeleteUser(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/baseca.v1.Account/DeleteUser", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AccountServer).DeleteUser(ctx, req.(*UsernameRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _Account_GetUser_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UsernameRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AccountServer).GetUser(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/baseca.v1.Account/GetUser", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AccountServer).GetUser(ctx, req.(*UsernameRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _Account_ListUsers_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(QueryParameter) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AccountServer).ListUsers(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/baseca.v1.Account/ListUsers", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AccountServer).ListUsers(ctx, req.(*QueryParameter)) + } + return interceptor(ctx, in, info, handler) +} + +func _Account_CreateUser_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CreateUserRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AccountServer).CreateUser(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/baseca.v1.Account/CreateUser", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AccountServer).CreateUser(ctx, req.(*CreateUserRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _Account_UpdateUserCredentials_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UpdateCredentialsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AccountServer).UpdateUserCredentials(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/baseca.v1.Account/UpdateUserCredentials", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AccountServer).UpdateUserCredentials(ctx, req.(*UpdateCredentialsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _Account_UpdateUserPermissions_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UpdatePermissionsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AccountServer).UpdateUserPermissions(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/baseca.v1.Account/UpdateUserPermissions", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AccountServer).UpdateUserPermissions(ctx, req.(*UpdatePermissionsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// Account_ServiceDesc is the grpc.ServiceDesc for Account service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var Account_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "baseca.v1.Account", + HandlerType: (*AccountServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "LoginUser", + Handler: _Account_LoginUser_Handler, + }, + { + MethodName: "DeleteUser", + Handler: _Account_DeleteUser_Handler, + }, + { + MethodName: "GetUser", + Handler: _Account_GetUser_Handler, + }, + { + MethodName: "ListUsers", + Handler: _Account_ListUsers_Handler, + }, + { + MethodName: "CreateUser", + Handler: _Account_CreateUser_Handler, + }, + { + MethodName: "UpdateUserCredentials", + Handler: _Account_UpdateUserCredentials_Handler, + }, + { + MethodName: "UpdateUserPermissions", + Handler: _Account_UpdateUserPermissions_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "baseca/v1/api.proto", +} + +// ServiceClient is the client API for Service service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type ServiceClient interface { + CreateServiceAccount(ctx context.Context, in *CreateServiceAccountRequest, opts ...grpc.CallOption) (*CreateServiceAccountResponse, error) + CreateProvisionerAccount(ctx context.Context, in *CreateProvisionerAccountRequest, opts ...grpc.CallOption) (*CreateProvisionerAccountResponse, error) + GetProvisionerAccount(ctx context.Context, in *AccountId, opts ...grpc.CallOption) (*ProvisionerAccount, error) + GetServiceAccount(ctx context.Context, in *AccountId, opts ...grpc.CallOption) (*ServiceAccount, error) + GetServiceAccountMetadata(ctx context.Context, in *GetServiceAccountMetadataRequest, opts ...grpc.CallOption) (*ServiceAccounts, error) + DeleteServiceAccount(ctx context.Context, in *AccountId, opts ...grpc.CallOption) (*emptypb.Empty, error) + DeleteProvisionedServiceAccount(ctx context.Context, in *AccountId, opts ...grpc.CallOption) (*emptypb.Empty, error) + DeleteProvisionerAccount(ctx context.Context, in *AccountId, opts ...grpc.CallOption) (*emptypb.Empty, error) + ProvisionServiceAccount(ctx context.Context, in *ProvisionServiceAccountRequest, opts ...grpc.CallOption) (*ProvisionServiceAccountResponse, error) + ListServiceAccounts(ctx context.Context, in *QueryParameter, opts ...grpc.CallOption) (*ServiceAccounts, error) + ListProvisionerAccounts(ctx context.Context, in *QueryParameter, opts ...grpc.CallOption) (*ProvisionerAccounts, error) +} + +type serviceClient struct { + cc grpc.ClientConnInterface +} + +func NewServiceClient(cc grpc.ClientConnInterface) ServiceClient { + return &serviceClient{cc} +} + +func (c *serviceClient) CreateServiceAccount(ctx context.Context, in *CreateServiceAccountRequest, opts ...grpc.CallOption) (*CreateServiceAccountResponse, error) { + out := new(CreateServiceAccountResponse) + err := c.cc.Invoke(ctx, "/baseca.v1.Service/CreateServiceAccount", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *serviceClient) CreateProvisionerAccount(ctx context.Context, in *CreateProvisionerAccountRequest, opts ...grpc.CallOption) (*CreateProvisionerAccountResponse, error) { + out := new(CreateProvisionerAccountResponse) + err := c.cc.Invoke(ctx, "/baseca.v1.Service/CreateProvisionerAccount", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *serviceClient) GetProvisionerAccount(ctx context.Context, in *AccountId, opts ...grpc.CallOption) (*ProvisionerAccount, error) { + out := new(ProvisionerAccount) + err := c.cc.Invoke(ctx, "/baseca.v1.Service/GetProvisionerAccount", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *serviceClient) GetServiceAccount(ctx context.Context, in *AccountId, opts ...grpc.CallOption) (*ServiceAccount, error) { + out := new(ServiceAccount) + err := c.cc.Invoke(ctx, "/baseca.v1.Service/GetServiceAccount", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *serviceClient) GetServiceAccountMetadata(ctx context.Context, in *GetServiceAccountMetadataRequest, opts ...grpc.CallOption) (*ServiceAccounts, error) { + out := new(ServiceAccounts) + err := c.cc.Invoke(ctx, "/baseca.v1.Service/GetServiceAccountMetadata", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *serviceClient) DeleteServiceAccount(ctx context.Context, in *AccountId, opts ...grpc.CallOption) (*emptypb.Empty, error) { + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, "/baseca.v1.Service/DeleteServiceAccount", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *serviceClient) DeleteProvisionedServiceAccount(ctx context.Context, in *AccountId, opts ...grpc.CallOption) (*emptypb.Empty, error) { + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, "/baseca.v1.Service/DeleteProvisionedServiceAccount", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *serviceClient) DeleteProvisionerAccount(ctx context.Context, in *AccountId, opts ...grpc.CallOption) (*emptypb.Empty, error) { + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, "/baseca.v1.Service/DeleteProvisionerAccount", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *serviceClient) ProvisionServiceAccount(ctx context.Context, in *ProvisionServiceAccountRequest, opts ...grpc.CallOption) (*ProvisionServiceAccountResponse, error) { + out := new(ProvisionServiceAccountResponse) + err := c.cc.Invoke(ctx, "/baseca.v1.Service/ProvisionServiceAccount", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *serviceClient) ListServiceAccounts(ctx context.Context, in *QueryParameter, opts ...grpc.CallOption) (*ServiceAccounts, error) { + out := new(ServiceAccounts) + err := c.cc.Invoke(ctx, "/baseca.v1.Service/ListServiceAccounts", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *serviceClient) ListProvisionerAccounts(ctx context.Context, in *QueryParameter, opts ...grpc.CallOption) (*ProvisionerAccounts, error) { + out := new(ProvisionerAccounts) + err := c.cc.Invoke(ctx, "/baseca.v1.Service/ListProvisionerAccounts", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// ServiceServer is the server API for Service service. +// All implementations must embed UnimplementedServiceServer +// for forward compatibility +type ServiceServer interface { + CreateServiceAccount(context.Context, *CreateServiceAccountRequest) (*CreateServiceAccountResponse, error) + CreateProvisionerAccount(context.Context, *CreateProvisionerAccountRequest) (*CreateProvisionerAccountResponse, error) + GetProvisionerAccount(context.Context, *AccountId) (*ProvisionerAccount, error) + GetServiceAccount(context.Context, *AccountId) (*ServiceAccount, error) + GetServiceAccountMetadata(context.Context, *GetServiceAccountMetadataRequest) (*ServiceAccounts, error) + DeleteServiceAccount(context.Context, *AccountId) (*emptypb.Empty, error) + DeleteProvisionedServiceAccount(context.Context, *AccountId) (*emptypb.Empty, error) + DeleteProvisionerAccount(context.Context, *AccountId) (*emptypb.Empty, error) + ProvisionServiceAccount(context.Context, *ProvisionServiceAccountRequest) (*ProvisionServiceAccountResponse, error) + ListServiceAccounts(context.Context, *QueryParameter) (*ServiceAccounts, error) + ListProvisionerAccounts(context.Context, *QueryParameter) (*ProvisionerAccounts, error) + mustEmbedUnimplementedServiceServer() +} + +// UnimplementedServiceServer must be embedded to have forward compatible implementations. +type UnimplementedServiceServer struct { +} + +func (UnimplementedServiceServer) CreateServiceAccount(context.Context, *CreateServiceAccountRequest) (*CreateServiceAccountResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method CreateServiceAccount not implemented") +} +func (UnimplementedServiceServer) CreateProvisionerAccount(context.Context, *CreateProvisionerAccountRequest) (*CreateProvisionerAccountResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method CreateProvisionerAccount not implemented") +} +func (UnimplementedServiceServer) GetProvisionerAccount(context.Context, *AccountId) (*ProvisionerAccount, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetProvisionerAccount not implemented") +} +func (UnimplementedServiceServer) GetServiceAccount(context.Context, *AccountId) (*ServiceAccount, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetServiceAccount not implemented") +} +func (UnimplementedServiceServer) GetServiceAccountMetadata(context.Context, *GetServiceAccountMetadataRequest) (*ServiceAccounts, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetServiceAccountMetadata not implemented") +} +func (UnimplementedServiceServer) DeleteServiceAccount(context.Context, *AccountId) (*emptypb.Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method DeleteServiceAccount not implemented") +} +func (UnimplementedServiceServer) DeleteProvisionedServiceAccount(context.Context, *AccountId) (*emptypb.Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method DeleteProvisionedServiceAccount not implemented") +} +func (UnimplementedServiceServer) DeleteProvisionerAccount(context.Context, *AccountId) (*emptypb.Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method DeleteProvisionerAccount not implemented") +} +func (UnimplementedServiceServer) ProvisionServiceAccount(context.Context, *ProvisionServiceAccountRequest) (*ProvisionServiceAccountResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method ProvisionServiceAccount not implemented") +} +func (UnimplementedServiceServer) ListServiceAccounts(context.Context, *QueryParameter) (*ServiceAccounts, error) { + return nil, status.Errorf(codes.Unimplemented, "method ListServiceAccounts not implemented") +} +func (UnimplementedServiceServer) ListProvisionerAccounts(context.Context, *QueryParameter) (*ProvisionerAccounts, error) { + return nil, status.Errorf(codes.Unimplemented, "method ListProvisionerAccounts not implemented") +} +func (UnimplementedServiceServer) mustEmbedUnimplementedServiceServer() {} + +// UnsafeServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to ServiceServer will +// result in compilation errors. +type UnsafeServiceServer interface { + mustEmbedUnimplementedServiceServer() +} + +func RegisterServiceServer(s grpc.ServiceRegistrar, srv ServiceServer) { + s.RegisterService(&Service_ServiceDesc, srv) +} + +func _Service_CreateServiceAccount_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CreateServiceAccountRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ServiceServer).CreateServiceAccount(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/baseca.v1.Service/CreateServiceAccount", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ServiceServer).CreateServiceAccount(ctx, req.(*CreateServiceAccountRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _Service_CreateProvisionerAccount_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CreateProvisionerAccountRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ServiceServer).CreateProvisionerAccount(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/baseca.v1.Service/CreateProvisionerAccount", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ServiceServer).CreateProvisionerAccount(ctx, req.(*CreateProvisionerAccountRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _Service_GetProvisionerAccount_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(AccountId) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ServiceServer).GetProvisionerAccount(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/baseca.v1.Service/GetProvisionerAccount", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ServiceServer).GetProvisionerAccount(ctx, req.(*AccountId)) + } + return interceptor(ctx, in, info, handler) +} + +func _Service_GetServiceAccount_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(AccountId) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ServiceServer).GetServiceAccount(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/baseca.v1.Service/GetServiceAccount", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ServiceServer).GetServiceAccount(ctx, req.(*AccountId)) + } + return interceptor(ctx, in, info, handler) +} + +func _Service_GetServiceAccountMetadata_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetServiceAccountMetadataRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ServiceServer).GetServiceAccountMetadata(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/baseca.v1.Service/GetServiceAccountMetadata", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ServiceServer).GetServiceAccountMetadata(ctx, req.(*GetServiceAccountMetadataRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _Service_DeleteServiceAccount_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(AccountId) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ServiceServer).DeleteServiceAccount(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/baseca.v1.Service/DeleteServiceAccount", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ServiceServer).DeleteServiceAccount(ctx, req.(*AccountId)) + } + return interceptor(ctx, in, info, handler) +} + +func _Service_DeleteProvisionedServiceAccount_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(AccountId) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ServiceServer).DeleteProvisionedServiceAccount(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/baseca.v1.Service/DeleteProvisionedServiceAccount", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ServiceServer).DeleteProvisionedServiceAccount(ctx, req.(*AccountId)) + } + return interceptor(ctx, in, info, handler) +} + +func _Service_DeleteProvisionerAccount_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(AccountId) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ServiceServer).DeleteProvisionerAccount(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/baseca.v1.Service/DeleteProvisionerAccount", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ServiceServer).DeleteProvisionerAccount(ctx, req.(*AccountId)) + } + return interceptor(ctx, in, info, handler) +} + +func _Service_ProvisionServiceAccount_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ProvisionServiceAccountRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ServiceServer).ProvisionServiceAccount(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/baseca.v1.Service/ProvisionServiceAccount", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ServiceServer).ProvisionServiceAccount(ctx, req.(*ProvisionServiceAccountRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _Service_ListServiceAccounts_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(QueryParameter) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ServiceServer).ListServiceAccounts(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/baseca.v1.Service/ListServiceAccounts", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ServiceServer).ListServiceAccounts(ctx, req.(*QueryParameter)) + } + return interceptor(ctx, in, info, handler) +} + +func _Service_ListProvisionerAccounts_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(QueryParameter) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ServiceServer).ListProvisionerAccounts(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/baseca.v1.Service/ListProvisionerAccounts", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ServiceServer).ListProvisionerAccounts(ctx, req.(*QueryParameter)) + } + return interceptor(ctx, in, info, handler) +} + +// Service_ServiceDesc is the grpc.ServiceDesc for Service service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var Service_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "baseca.v1.Service", + HandlerType: (*ServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "CreateServiceAccount", + Handler: _Service_CreateServiceAccount_Handler, + }, + { + MethodName: "CreateProvisionerAccount", + Handler: _Service_CreateProvisionerAccount_Handler, + }, + { + MethodName: "GetProvisionerAccount", + Handler: _Service_GetProvisionerAccount_Handler, + }, + { + MethodName: "GetServiceAccount", + Handler: _Service_GetServiceAccount_Handler, + }, + { + MethodName: "GetServiceAccountMetadata", + Handler: _Service_GetServiceAccountMetadata_Handler, + }, + { + MethodName: "DeleteServiceAccount", + Handler: _Service_DeleteServiceAccount_Handler, + }, + { + MethodName: "DeleteProvisionedServiceAccount", + Handler: _Service_DeleteProvisionedServiceAccount_Handler, + }, + { + MethodName: "DeleteProvisionerAccount", + Handler: _Service_DeleteProvisionerAccount_Handler, + }, + { + MethodName: "ProvisionServiceAccount", + Handler: _Service_ProvisionServiceAccount_Handler, + }, + { + MethodName: "ListServiceAccounts", + Handler: _Service_ListServiceAccounts_Handler, + }, + { + MethodName: "ListProvisionerAccounts", + Handler: _Service_ListProvisionerAccounts_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "baseca/v1/api.proto", +} + +// HealthClient is the client API for Health service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type HealthClient interface { + Check(ctx context.Context, in *HealthCheckRequest, opts ...grpc.CallOption) (*HealthCheckResponse, error) +} + +type healthClient struct { + cc grpc.ClientConnInterface +} + +func NewHealthClient(cc grpc.ClientConnInterface) HealthClient { + return &healthClient{cc} +} + +func (c *healthClient) Check(ctx context.Context, in *HealthCheckRequest, opts ...grpc.CallOption) (*HealthCheckResponse, error) { + out := new(HealthCheckResponse) + err := c.cc.Invoke(ctx, "/baseca.v1.Health/Check", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// HealthServer is the server API for Health service. +// All implementations must embed UnimplementedHealthServer +// for forward compatibility +type HealthServer interface { + Check(context.Context, *HealthCheckRequest) (*HealthCheckResponse, error) + mustEmbedUnimplementedHealthServer() +} + +// UnimplementedHealthServer must be embedded to have forward compatible implementations. +type UnimplementedHealthServer struct { +} + +func (UnimplementedHealthServer) Check(context.Context, *HealthCheckRequest) (*HealthCheckResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method Check not implemented") +} +func (UnimplementedHealthServer) mustEmbedUnimplementedHealthServer() {} + +// UnsafeHealthServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to HealthServer will +// result in compilation errors. +type UnsafeHealthServer interface { + mustEmbedUnimplementedHealthServer() +} + +func RegisterHealthServer(s grpc.ServiceRegistrar, srv HealthServer) { + s.RegisterService(&Health_ServiceDesc, srv) +} + +func _Health_Check_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(HealthCheckRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(HealthServer).Check(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/baseca.v1.Health/Check", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(HealthServer).Check(ctx, req.(*HealthCheckRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// Health_ServiceDesc is the grpc.ServiceDesc for Health service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var Health_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "baseca.v1.Health", + HandlerType: (*HealthServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Check", + Handler: _Health_Check_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "baseca/v1/api.proto", +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..85a00d4 --- /dev/null +++ b/go.mod @@ -0,0 +1,79 @@ +module github.com/coinbase/baseca + +go 1.21 + +require ( + github.com/allegro/bigcache/v3 v3.1.0 + github.com/aws/aws-sdk-go v1.45.1 + github.com/aws/aws-sdk-go-v2 v1.21.0 + github.com/aws/aws-sdk-go-v2/config v1.18.38 + github.com/aws/aws-sdk-go-v2/credentials v1.13.36 + github.com/aws/aws-sdk-go-v2/service/acmpca v1.22.7 + github.com/aws/aws-sdk-go-v2/service/ec2 v1.115.0 + github.com/aws/aws-sdk-go-v2/service/firehose v1.17.5 + github.com/aws/aws-sdk-go-v2/service/kms v1.24.5 + github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.21.3 + github.com/aws/aws-sdk-go-v2/service/sts v1.21.5 + github.com/bazelbuild/rules_go v0.41.0 + github.com/casbin/casbin/v2 v2.77.1 + github.com/go-redis/redis/v8 v8.11.5 + github.com/gogo/status v1.1.1 + github.com/google/uuid v1.3.1 + github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 + github.com/lib/pq v1.10.9 + github.com/mitchellh/mapstructure v1.5.0 + github.com/rs/zerolog v1.30.0 + github.com/spf13/viper v1.16.0 + github.com/sqlc-dev/pqtype v0.2.0 + github.com/stretchr/testify v1.8.4 + github.com/wagslane/go-password-validator v0.3.0 + go.uber.org/fx v1.20.0 + go.uber.org/mock v0.3.0 + go.uber.org/zap v1.25.0 + golang.org/x/crypto v0.14.0 + golang.org/x/net v0.17.0 + google.golang.org/grpc v1.57.0 + google.golang.org/protobuf v1.31.0 +) + +require ( + github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.11 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.41 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.35 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.3.42 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.35 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.13.6 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.15.5 // indirect + github.com/aws/smithy-go v1.14.2 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/fsnotify/fsnotify v1.6.0 // indirect + github.com/gogo/googleapis v0.0.0-20180223154316-0cd9801be74a // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/jmespath/go-jmespath v0.4.0 // indirect + github.com/magiconair/properties v1.8.7 // indirect + github.com/mattn/go-colorable v0.1.12 // indirect + github.com/mattn/go-isatty v0.0.14 // indirect + github.com/pelletier/go-toml/v2 v2.0.8 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/spf13/afero v1.9.5 // indirect + github.com/spf13/cast v1.5.1 // indirect + github.com/spf13/jwalterweatherman v1.1.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/stretchr/objx v0.5.0 // indirect + github.com/subosito/gotenv v1.4.2 // indirect + github.com/tidwall/gjson v1.14.4 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.0 // indirect + go.uber.org/dig v1.17.0 // indirect + go.uber.org/multierr v1.10.0 // indirect + golang.org/x/sys v0.13.0 // indirect + golang.org/x/text v0.13.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..2718356 --- /dev/null +++ b/go.sum @@ -0,0 +1,658 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible h1:1G1pk05UrOh0NlF1oeaaix1x8XzrfjIDK47TY0Zehcw= +github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= +github.com/allegro/bigcache/v3 v3.1.0 h1:H2Vp8VOvxcrB91o86fUSVJFqeuz8kpyyB02eH3bSzwk= +github.com/allegro/bigcache/v3 v3.1.0/go.mod h1:aPyh7jEvrog9zAwx5N7+JUQX5dZTSGpxF1LAR4dr35I= +github.com/aws/aws-sdk-go v1.45.1 h1:PXuxDZIo/Y9Bvtg2t055+dY4hRwNAEcq6bUMv9fXcjk= +github.com/aws/aws-sdk-go v1.45.1/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= +github.com/aws/aws-sdk-go-v2 v1.21.0 h1:gMT0IW+03wtYJhRqTVYn0wLzwdnK9sRMcxmtfGzRdJc= +github.com/aws/aws-sdk-go-v2 v1.21.0/go.mod h1:/RfNgGmRxI+iFOB1OeJUyxiU+9s88k3pfHvDagGEp0M= +github.com/aws/aws-sdk-go-v2/config v1.18.38 h1:CByQCELMgm2tM1lAehx3XNg0R/pfeXsYzqn0Aq2chJQ= +github.com/aws/aws-sdk-go-v2/config v1.18.38/go.mod h1:vNm9Hf5VgG2fSUWhT3zFrqN/RosGcabFMYgiSoxKFU8= +github.com/aws/aws-sdk-go-v2/credentials v1.13.36 h1:ps0cPswZjpsOk6sLwG6fdXTzrYjCplgPEyG3OUbbdqE= +github.com/aws/aws-sdk-go-v2/credentials v1.13.36/go.mod h1:sY2phUzxbygoyDtTXhqi7GjGjCQ1S5a5Rj8u3ksBxCg= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.11 h1:uDZJF1hu0EVT/4bogChk8DyjSF6fof6uL/0Y26Ma7Fg= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.11/go.mod h1:TEPP4tENqBGO99KwVpV9MlOX4NSrSLP8u3KRy2CDwA8= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.41 h1:22dGT7PneFMx4+b3pz7lMTRyN8ZKH7M2cW4GP9yUS2g= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.41/go.mod h1:CrObHAuPneJBlfEJ5T3szXOUkLEThaGfvnhTf33buas= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.35 h1:SijA0mgjV8E+8G45ltVHs0fvKpTj8xmZJ3VwhGKtUSI= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.35/go.mod h1:SJC1nEVVva1g3pHAIdCp7QsRIkMmLAgoDquQ9Rr8kYw= +github.com/aws/aws-sdk-go-v2/internal/ini v1.3.42 h1:GPUcE/Yq7Ur8YSUk6lVkoIMWnJNO0HT18GUzCWCgCI0= +github.com/aws/aws-sdk-go-v2/internal/ini v1.3.42/go.mod h1:rzfdUlfA+jdgLDmPKjd3Chq9V7LVLYo1Nz++Wb91aRo= +github.com/aws/aws-sdk-go-v2/service/acmpca v1.22.7 h1:WPfAQECf66APeXIm/g7F/Y5Al40tNsSikDMuqpjrI/s= +github.com/aws/aws-sdk-go-v2/service/acmpca v1.22.7/go.mod h1:dyCrosYGFnhsjgxaKrqCzcZO4Lhqf1+U7tV0ErPkXGc= +github.com/aws/aws-sdk-go-v2/service/ec2 v1.115.0 h1:/OcX8Q9qehNdPQInuYifmcsTir62q6ulmZByy/VkoeE= +github.com/aws/aws-sdk-go-v2/service/ec2 v1.115.0/go.mod h1:0FhI2Rzcv5BNM3dNnbcCx2qa2naFZoAidJi11cQgzL0= +github.com/aws/aws-sdk-go-v2/service/firehose v1.17.5 h1:L9C2CDJ46zlS+T/Izi6YRQm2BDv+xzx+s1DFnm5ODLc= +github.com/aws/aws-sdk-go-v2/service/firehose v1.17.5/go.mod h1:LaKo3QD/FCMbX07y/33Y5HwlLxn64jHuyBnZRY381Jc= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.35 h1:CdzPW9kKitgIiLV1+MHobfR5Xg25iYnyzWZhyQuSlDI= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.35/go.mod h1:QGF2Rs33W5MaN9gYdEQOBBFPLwTZkEhRwI33f7KIG0o= +github.com/aws/aws-sdk-go-v2/service/kms v1.24.5 h1:VNEw+EdYDUdkICYAVQ6n9WoAq8ZuZr7dXKjyaOw94/Q= +github.com/aws/aws-sdk-go-v2/service/kms v1.24.5/go.mod h1:NZEhPgq+vvmM6L9w+xl78Vf7YxqUcpVULqFdrUhHg8I= +github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.21.3 h1:H6ZipEknzu7RkJW3w2PP75zd8XOdR35AEY5D57YrJtA= +github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.21.3/go.mod h1:5W2cYXDPabUmwULErlC92ffLhtTuyv4ai+5HhdbhfNo= +github.com/aws/aws-sdk-go-v2/service/sso v1.13.6 h1:2PylFCfKCEDv6PeSN09pC/VUiRd10wi1VfHG5FrW0/g= +github.com/aws/aws-sdk-go-v2/service/sso v1.13.6/go.mod h1:fIAwKQKBFu90pBxx07BFOMJLpRUGu8VOzLJakeY+0K4= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.15.5 h1:dnInJb4S0oy8aQuri1mV6ipLlnZPfnsDNB9BGO9PDNY= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.15.5/go.mod h1:yygr8ACQRY2PrEcy3xsUI357stq2AxnFM6DIsR9lij4= +github.com/aws/aws-sdk-go-v2/service/sts v1.21.5 h1:CQBFElb0LS8RojMJlxRSo/HXipvTZW2S44Lt9Mk2aYQ= +github.com/aws/aws-sdk-go-v2/service/sts v1.21.5/go.mod h1:VC7JDqsqiwXukYEDjoHh9U0fOJtNWh04FPQz4ct4GGU= +github.com/aws/smithy-go v1.14.2 h1:MJU9hqBGbvWZdApzpvoF2WAIJDbtjK2NDJSiJP7HblQ= +github.com/aws/smithy-go v1.14.2/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA= +github.com/bazelbuild/rules_go v0.41.0 h1:JzlRxsFNhlX+g4drDRPhIaU5H5LnI978wdMJ0vK4I+k= +github.com/bazelbuild/rules_go v0.41.0/go.mod h1:TMHmtfpvyfsxaqfL9WnahCsXMWDMICTw7XeK9yVb+YU= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= +github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/casbin/casbin/v2 v2.77.1 h1:+H46VamJCTlmCPcb0N99Zaj4tSorfuvBh3v5lyGopeU= +github.com/casbin/casbin/v2 v2.77.1/go.mod h1:mzGx0hYW9/ksOSpw3wNjk3NRAroq5VMFYUQ6G43iGPk= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= +github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= +github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI= +github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gogo/googleapis v0.0.0-20180223154316-0cd9801be74a h1:dR8+Q0uO5S2ZBcs2IH6VBKYwSxPo2vYCYq0ot0mu7xA= +github.com/gogo/googleapis v0.0.0-20180223154316-0cd9801be74a/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/gogo/status v1.1.1 h1:DuHXlSFHNKqTQ+/ACf5Vs6r4X/dH2EgIzR9Vr+H65kg= +github.com/gogo/status v1.1.1/go.mod h1:jpG3dM5QPcqu19Hg8lkUhBFBa3TcLs1DG7+2Jqci7oU= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= +github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= +github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI= +github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= +github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE= +github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs= +github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= +github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.30.0 h1:SymVODrcRsaRaSInD9yQtKbtWqwsfoPcRff/oRXLj4c= +github.com/rs/zerolog v1.30.0/go.mod h1:/tk+P47gFdPXq4QYjvCmT5/Gsug2nagsFWBWhAiSi1w= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM= +github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= +github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA= +github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48= +github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= +github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.16.0 h1:rGGH0XDZhdUOryiDWjmIvUSWpbNqisK8Wk0Vyefw8hc= +github.com/spf13/viper v1.16.0/go.mod h1:yg78JgCJcbrQOvV9YLXgkLaZqUidkY9K+Dd1FofRzQg= +github.com/sqlc-dev/pqtype v0.2.0 h1:zfzDpAxjCU0/GO7EgZ7ELUh0w28SrMSHzO3rH5Wd3is= +github.com/sqlc-dev/pqtype v0.2.0/go.mod h1:oyUjp5981ctiL9UYvj1bVvCKi8OXkCa0u645hce7CAs= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8= +github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= +github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM= +github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/wagslane/go-password-validator v0.3.0 h1:vfxOPzGHkz5S146HDpavl0cw1DSVP061Ry2PX0/ON6I= +github.com/wagslane/go-password-validator v0.3.0/go.mod h1:TI1XJ6T5fRdRnHqHt14pvy1tNVnrwe7m3/f1f2fDphQ= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/dig v1.17.0 h1:5Chju+tUvcC+N7N6EV08BJz41UZuO3BmHcN4A287ZLI= +go.uber.org/dig v1.17.0/go.mod h1:rTxpf7l5I0eBTlE6/9RL+lDybC7WFwY2QH55ZSjy1mU= +go.uber.org/fx v1.20.0 h1:ZMC/pnRvhsthOZh9MZjMq5U8Or3mA9zBSPaLnzs3ihQ= +go.uber.org/fx v1.20.0/go.mod h1:qCUj0btiR3/JnanEr1TYEePfSw6o/4qYJscgvzQ5Ub0= +go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= +go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk= +go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo= +go.uber.org/mock v0.3.0 h1:3mUxI1No2/60yUYax92Pt8eNOEecx2D3lcXZh2NEZJo= +go.uber.org/mock v0.3.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= +go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= +go.uber.org/zap v1.25.0 h1:4Hvk6GtkucQ790dqmj7l1eEnRdKm3k3ZUrUMS2d5+5c= +go.uber.org/zap v1.25.0/go.mod h1:JIAUzQIH94IC4fOJQm7gMmBJP5k7wQfdcnYdPoEXJYk= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= +golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180518175338-11a468237815/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19 h1:0nDDozoAU19Qb2HwhXadU8OcsiO/09cnTqhUtq2MEOM= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA= +google.golang.org/grpc v1.12.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.57.0 h1:kfzNeI/klCGD2YPMUlaGNT3pxvYfga7smW3Vth8Zsiw= +google.golang.org/grpc v1.57.0/go.mod h1:Sd+9RMTACXwmub0zcNY2c4arhtrbBYD1AUHI/dt16Mo= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/internal/attestation/aws_iid/iid.go b/internal/attestation/aws_iid/iid.go new file mode 100644 index 0000000..8ef18db --- /dev/null +++ b/internal/attestation/aws_iid/iid.go @@ -0,0 +1,214 @@ +package aws_iid + +import ( + "context" + "crypto/sha256" + "crypto/x509" + "encoding/base64" + "encoding/hex" + "encoding/json" + "encoding/pem" + "fmt" + "os" + "path/filepath" + "regexp" + + "github.com/allegro/bigcache/v3" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/credentials/stscreds" + "github.com/aws/aws-sdk-go-v2/service/ec2" + ec2types "github.com/aws/aws-sdk-go-v2/service/ec2/types" + "github.com/aws/aws-sdk-go-v2/service/sts" + db "github.com/coinbase/baseca/db/sqlc" + apiv1 "github.com/coinbase/baseca/gen/go/baseca/v1" + "github.com/coinbase/baseca/internal/types" + "github.com/gogo/status" + "github.com/google/uuid" + "google.golang.org/grpc/codes" +) + +const ( + // https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/verify-signature.html (Other AWS Regions) + aws_certificate_path = "internal/attestor/aws_iid/certificate/ec2.amazonaws.com.crt" +) + +type InstanceIdentityDocument struct { + AccountId string `json:"accountId"` + Architecture string `json:"architecture"` + AvailabilityZone string `json:"availabilityZone"` + ImageId string `json:"imageId"` + InstanceId string `json:"instanceId"` + InstanceType string `json:"instanceType"` + PrivateIp string `json:"privateIp"` + Region string `json:"region"` + Version string `json:"version"` +} + +var ( + instanceFilters = []ec2types.Filter{ + { + Name: aws.String("instance-state-name"), + Values: []string{ + "pending", + "running", + }, + }, + } +) + +func buildEC2Client(region string, roleARN string) (*ec2.Client, error) { + cfg, err := config.LoadDefaultConfig( + context.TODO(), + config.WithRegion(region), + ) + if err != nil { + return nil, err + } + + if isValidRoleArn(roleARN) { + stsSvc := sts.NewFromConfig(cfg) + cfg.Credentials = stscreds.NewAssumeRoleProvider(stsSvc, roleARN) + } + + svc := ec2.NewFromConfig(cfg) + return svc, nil +} + +func isValidRoleArn(arn string) bool { + pattern := `^arn:aws:iam::[0-9]{12}:role\/[a-zA-Z0-9+=,.@_-]{1,64}$` + re := regexp.MustCompile(pattern) + return re.MatchString(arn) +} + +func validateMetadataSignature(iid types.EC2InstanceMetadata) error { + certificate, err := os.ReadFile(filepath.Clean(aws_certificate_path)) + if err != nil { + return fmt.Errorf("error reading aws certificate for signature validation") + } + + rsa_certificate_pem, _ := pem.Decode([]byte(certificate)) + rsa_certificate, _ := x509.ParseCertificate(rsa_certificate_pem.Bytes) + signature, _ := base64.StdEncoding.DecodeString(string(iid.InstanceIdentitySignature)) + + err = rsa_certificate.CheckSignature(x509.SHA256WithRSA, iid.InstanceIdentityDocument, signature) + if err != nil { + return fmt.Errorf("invalid aws_iid signature") + } + + return nil +} + +func GetInstanceIdentityDocument(ctx context.Context, db_reader db.Store, client_id uuid.UUID) (*db.AwsAttestation, error) { + node_attestation, err := db_reader.GetInstanceIdentityDocument(ctx, client_id) + if err != nil { + return nil, fmt.Errorf("error retrieving aws_attestation from db, %s", err) + } + return node_attestation, nil +} + +func AWSIidNodeAttestation(client_uuid uuid.UUID, header_metadata string, iid db.AwsAttestation, cache *bigcache.BigCache) error { + var client *ec2.Client + var instance ec2types.Instance + var err error + + request_metadata_byte := []byte(header_metadata) + instance_metadata := types.EC2InstanceMetadata{} + instance_identity_document := InstanceIdentityDocument{} + + err = json.Unmarshal(request_metadata_byte, &instance_metadata) + if err != nil { + return fmt.Errorf("error unmarshal aws_instance metadata") + } + + err = validateMetadataSignature(instance_metadata) + if err != nil { + return err + } + + err = json.Unmarshal(instance_metadata.InstanceIdentityDocument, &instance_identity_document) + if err != nil { + return fmt.Errorf("error unmarshal aws_iid metadata") + } + + // Query Instance Metadata in Cache + hash := sha256.Sum256(instance_metadata.InstanceIdentityDocument) + hash_key := hex.EncodeToString(hash[:]) + + if value, cached := cache.Get(hash_key); cached != nil { + client, err = buildEC2Client(instance_identity_document.Region, iid.AssumeRole.String) + if err != nil { + return fmt.Errorf("error building ec2 client, %s", err) + } + + instancesDesc, err := client.DescribeInstances(context.Background(), &ec2.DescribeInstancesInput{ + InstanceIds: []string{instance_identity_document.InstanceId}, + Filters: instanceFilters, + }) + if err != nil { + return fmt.Errorf("ec2 describe instances failed, %s", err) + } + + instance, err = getEC2Instance(instancesDesc) + if err != nil { + return fmt.Errorf("error querying ec2 instance, %s", err) + } + + // IAM Role Arn Attestation + if iid.RoleArn.Valid { + if *instance.IamInstanceProfile.Arn != iid.RoleArn.String { + return fmt.Errorf("aws_iid role arn attestation error [client_id %s] [instance_identity_document %s]", client_uuid, instance_identity_document) + } + } + + data, err := json.Marshal(iid) + if err != nil { + return fmt.Errorf("error marshalling cached_service_account, %s", err) + } + err = cache.Set(hash_key, data) + if err != nil { + return fmt.Errorf("error setting hashed aws_iid in cache, %s", err) + } + } else { + // SHA-256 Instance Identity Document [Key], Client ID [Value]. Multiple Instances Map to Single Client ID. + err = json.Unmarshal(value, &iid) + if err != nil { + return fmt.Errorf("error unmarshal hashed iid in cached, %s", err) + } + attested_client_id := iid.ClientID + if attested_client_id != client_uuid { + return fmt.Errorf("request client id does not match attested node in cache") + } + } + return nil +} + +func getEC2Instance(instancesDesc *ec2.DescribeInstancesOutput) (ec2types.Instance, error) { + if len(instancesDesc.Reservations) < 1 { + return ec2types.Instance{}, status.Error(codes.Internal, "failed to query AWS via describe-instances: returned no reservations") + } + + if len(instancesDesc.Reservations[0].Instances) < 1 { + return ec2types.Instance{}, status.Error(codes.Internal, "failed to query AWS via describe-instances: returned no instances") + } + + return instancesDesc.Reservations[0].Instances[0], nil +} + +func GetNodeAttestation(node_attestation *apiv1.NodeAttestation) []string { + var valid_attestation []string + var iid = node_attestation.AwsIid + + // AWS Node Attestation + if iid != nil { + attestation := iid.RoleArn == "" && iid.AssumeRole == "" && len(iid.SecurityGroups) == 0 && + iid.Region == "" && iid.InstanceId == "" && iid.ImageId == "" && + len(iid.InstanceTags) == 0 + + if !attestation { + valid_attestation = append(valid_attestation, types.Attestation.AWS_IID) + } + } + + return valid_attestation +} diff --git a/internal/attestation/aws_iid/iid_test.go b/internal/attestation/aws_iid/iid_test.go new file mode 100644 index 0000000..262c914 --- /dev/null +++ b/internal/attestation/aws_iid/iid_test.go @@ -0,0 +1,33 @@ +package aws_iid + +import ( + "testing" +) + +func TestIsValidRoleArn(t *testing.T) { + tests := []struct { + name string + arn string + want bool + }{ + { + name: "Valid ARN", + arn: "arn:aws:iam::123456789012:role/Example", + want: true, + }, + { + name: "Invalid ARN", + arn: "invalid:arn:format", + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := isValidRoleArn(tt.arn) + if got != tt.want { + t.Errorf("isValidRoleArn() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/internal/client/acmpca/client.go b/internal/client/acmpca/client.go new file mode 100644 index 0000000..74c5a81 --- /dev/null +++ b/internal/client/acmpca/client.go @@ -0,0 +1,38 @@ +package acmpca + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/credentials/stscreds" + "github.com/aws/aws-sdk-go-v2/service/acmpca" + "github.com/aws/aws-sdk-go-v2/service/sts" + "github.com/coinbase/baseca/internal/types" +) + +type PrivateCaClientIface interface { + IssueCertificate(ctx context.Context, params *acmpca.IssueCertificateInput, optFns ...func(*acmpca.Options)) (*acmpca.IssueCertificateOutput, error) + GetCertificate(ctx context.Context, params *acmpca.GetCertificateInput, optFns ...func(*acmpca.Options)) (*acmpca.GetCertificateOutput, error) + RevokeCertificate(ctx context.Context, params *acmpca.RevokeCertificateInput, optFns ...func(*acmpca.Options)) (*acmpca.RevokeCertificateOutput, error) + GetCertificateAuthorityCertificate(ctx context.Context, params *acmpca.GetCertificateAuthorityCertificateInput, optFns ...func(*acmpca.Options)) (*acmpca.GetCertificateAuthorityCertificateOutput, error) +} + +type PrivateCaClient struct { + Client PrivateCaClientIface + waiter *acmpca.CertificateIssuedWaiter +} + +func NewPrivateCaClient(parameters types.CertificateParameters) (*PrivateCaClient, error) { + cfg, _ := config.LoadDefaultConfig(context.TODO()) + stsclient := sts.NewFromConfig(cfg) + + if parameters.AssumeRole { + cfg.Credentials = stscreds.NewAssumeRoleProvider(stsclient, parameters.RoleArn) + cfg.Region = parameters.Region + } + + client := acmpca.NewFromConfig(cfg) + return &PrivateCaClient{ + Client: client, + }, nil +} diff --git a/internal/client/acmpca/issue.go b/internal/client/acmpca/issue.go new file mode 100644 index 0000000..34512f9 --- /dev/null +++ b/internal/client/acmpca/issue.go @@ -0,0 +1,141 @@ +package acmpca + +import ( + "context" + "crypto/rand" + "crypto/x509" + "encoding/hex" + "encoding/pem" + "fmt" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/acmpca" + pca_types "github.com/aws/aws-sdk-go-v2/service/acmpca/types" + apiv1 "github.com/coinbase/baseca/gen/go/baseca/v1" + "github.com/coinbase/baseca/internal/types" +) + +const ( + _subordinateCACertificate_PathLen0_V1 = "arn:aws:acm-pca:::template/SubordinateCACertificate_PathLen0/V1" + _codeSigningCertificate_V1 = "arn:aws:acm-pca:::template/CodeSigningCertificate/V1" + _endEntityServerAuthCertificate_V1 = "arn:aws:acm-pca:::template/EndEntityServerAuthCertificate/V1" + _endEntityClientAuthCertificate_V1 = "arn:aws:acm-pca:::template/EndEntityClientAuthCertificate/V1" +) + +func (c *PrivateCaClient) IssueCertificateFromTemplate(parameters *apiv1.CertificateAuthorityParameter, csr []byte, template string) (*x509.Certificate, error) { + idempotencyToken, err := generateIdempotencyToken() + if err != nil { + return nil, err + } + + signingAlgorithm, ok := types.ValidSignatures[parameters.SignAlgorithm] + if !ok { + return nil, fmt.Errorf("signature algorithm %s invalid", parameters.SignAlgorithm) + } + + certReq := acmpca.IssueCertificateInput{ + CertificateAuthorityArn: aws.String(parameters.CaArn), + Csr: csr, + TemplateArn: aws.String(template), + SigningAlgorithm: signingAlgorithm.PCA, + Validity: &pca_types.Validity{ + Value: aws.Int64(int64(parameters.Validity)), + Type: pca_types.ValidityPeriodTypeDays, + }, + IdempotencyToken: aws.String(*idempotencyToken), + } + + certificateOutput, err := c.Client.IssueCertificate(context.Background(), &certReq) + if err != nil { + return nil, err + } + + certificateInput := acmpca.GetCertificateInput{ + CertificateArn: certificateOutput.CertificateArn, + CertificateAuthorityArn: ¶meters.CaArn, + } + + certificate, err := c.waitUntilCertificateIssued(certificateInput) + if err != nil { + return nil, err + } + + return certificate, nil +} + +func (c *PrivateCaClient) IssueSubordinateCertificate(parameters types.CertificateParameters, algorithm string, csr []byte) (*x509.Certificate, error) { + idempotencyToken, err := generateIdempotencyToken() + if err != nil { + return nil, err + } + + signingAlgorithm, ok := types.ValidSignatures[algorithm] + if !ok { + return nil, fmt.Errorf("signature algorithm %s invalid", algorithm) + } + + certReq := acmpca.IssueCertificateInput{ + CertificateAuthorityArn: aws.String(parameters.CaArn), + Csr: csr, + TemplateArn: aws.String(_subordinateCACertificate_PathLen0_V1), + SigningAlgorithm: signingAlgorithm.PCA, + Validity: &pca_types.Validity{ + Value: aws.Int64(int64(parameters.Validity)), + Type: pca_types.ValidityPeriodTypeDays, + }, + IdempotencyToken: aws.String(*idempotencyToken), + } + + certificateOutput, err := c.Client.IssueCertificate(context.Background(), &certReq) + if err != nil { + return nil, err + } + + certificateInput := acmpca.GetCertificateInput{ + CertificateArn: certificateOutput.CertificateArn, + CertificateAuthorityArn: ¶meters.CaArn, + } + + certificate, err := c.waitUntilCertificateIssued(certificateInput) + if err != nil { + return nil, err + } + + return certificate, nil +} + +func generateIdempotencyToken() (*string, error) { + idempotency := make([]byte, 16) + _, err := rand.Read(idempotency) + if err != nil { + return nil, fmt.Errorf("error generating idempotency token: %s", err) + } + + encoded := hex.EncodeToString(idempotency) + return &encoded, nil +} + +// AWS Removed WaitUntilCertificateIssued(input *GetCertificateInput) in the v2 SDK +func (c *PrivateCaClient) waitUntilCertificateIssued(request acmpca.GetCertificateInput) (*x509.Certificate, error) { + c.waiter = acmpca.NewCertificateIssuedWaiter(c.Client) + err := c.waiter.Wait(context.Background(), &request, time.Duration(30*time.Second)) + if err != nil { + return nil, err + } + certificatePem, err := c.Client.GetCertificate(context.Background(), &request) + if err != nil { + return nil, err + } + certPemBlock, _ := pem.Decode([]byte(*certificatePem.Certificate)) + if certPemBlock == nil { + return nil, err + } + + certificate, err := x509.ParseCertificate(certPemBlock.Bytes) + if err != nil { + return nil, err + } + + return certificate, nil +} diff --git a/internal/client/acmpca/query.go b/internal/client/acmpca/query.go new file mode 100644 index 0000000..58cb5d3 --- /dev/null +++ b/internal/client/acmpca/query.go @@ -0,0 +1,19 @@ +package acmpca + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/service/acmpca" +) + +func (c *PrivateCaClient) GetSubordinateCAChain(certificate_authority_arn string) (*acmpca.GetCertificateAuthorityCertificateOutput, error) { + input := acmpca.GetCertificateAuthorityCertificateInput{ + CertificateAuthorityArn: &certificate_authority_arn, + } + response, err := c.Client.GetCertificateAuthorityCertificate(context.Background(), &input) + if err != nil { + return nil, err + } + + return response, nil +} diff --git a/internal/client/acmpca/revoke.go b/internal/client/acmpca/revoke.go new file mode 100644 index 0000000..119e0eb --- /dev/null +++ b/internal/client/acmpca/revoke.go @@ -0,0 +1,18 @@ +package acmpca + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/acmpca" + "github.com/aws/aws-sdk-go-v2/service/acmpca/types" +) + +func (c *PrivateCaClient) RevokeCertificate(certificate_authority_arn string, serial_number string, revocation_reason string) (*acmpca.RevokeCertificateOutput, error) { + revokeCertificateInput := &acmpca.RevokeCertificateInput{ + CertificateAuthorityArn: aws.String(certificate_authority_arn), + CertificateSerial: aws.String(serial_number), + RevocationReason: types.RevocationReason(revocation_reason), + } + return c.Client.RevokeCertificate(context.Background(), revokeCertificateInput) +} diff --git a/internal/client/firehose/client.go b/internal/client/firehose/client.go new file mode 100644 index 0000000..fa9c88e --- /dev/null +++ b/internal/client/firehose/client.go @@ -0,0 +1,33 @@ +package firehose + +import ( + "context" + "fmt" + + config_v2 "github.com/aws/aws-sdk-go-v2/config" + firehose_v2 "github.com/aws/aws-sdk-go-v2/service/firehose" + "github.com/coinbase/baseca/internal/config" +) + +type FirehoseClientIface interface { + PutRecord(ctx context.Context, params *firehose_v2.PutRecordInput, optFns ...func(*firehose_v2.Options)) (*firehose_v2.PutRecordOutput, error) +} + +type FirehoseClient struct { + DataStream string + Service FirehoseClientIface +} + +func NewFirehoseClient(config *config.Config) (*FirehoseClient, error) { + cfg, err := config_v2.LoadDefaultConfig(context.TODO(), + config_v2.WithRegion(config.Firehose.Region), + ) + if err != nil { + return nil, fmt.Errorf("unable to create new session: %s", err) + } + + return &FirehoseClient{ + DataStream: config.Firehose.Stream, + Service: firehose_v2.NewFromConfig(cfg), + }, nil +} diff --git a/internal/client/firehose/stream.go b/internal/client/firehose/stream.go new file mode 100644 index 0000000..f8aa864 --- /dev/null +++ b/internal/client/firehose/stream.go @@ -0,0 +1,64 @@ +package firehose + +import ( + "context" + "crypto/rand" + "encoding/json" + "fmt" + "math/big" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + firehose_v2 "github.com/aws/aws-sdk-go-v2/service/firehose" + "github.com/aws/aws-sdk-go-v2/service/firehose/types" + "github.com/coinbase/baseca/internal/lib/util" +) + +type ForwardedEventUploadEvent struct { + SerialNumber string `json:"serial_number"` + Metadata Metadata `json:"metadata"` +} + +type Metadata struct { + CommonName string `json:"common_name"` + SubjectAlternateName []string `json:"subject_alternate_name"` + CertificateExpiration time.Time `json:"certificate_expiration"` + IssuedDate time.Time `json:"issued_date"` + CaSerialNumber string `json:"ca_serial_number"` + CertificateAuthorityArn string `json:"certificate_authority_arn"` + Timestamp time.Time `json:"timestamp"` +} + +func (c FirehoseClient) Stream(ctx context.Context, event ForwardedEventUploadEvent) (response *firehose_v2.PutRecordOutput, err error) { + var firehoseErr error + + batch, err := json.Marshal(event) + if err != nil { + return nil, fmt.Errorf("error marshalling firehose event: %s", err) + } + input := &firehose_v2.PutRecordInput{ + DeliveryStreamName: aws.String(c.DataStream), + Record: &types.Record{ + Data: append(batch, '\n'), + }, + } + + for _, backoff := range util.BackoffSchedule { + record, err := c.Service.PutRecord(ctx, input) + if err != nil { + firehoseErr = err + + jitterInt, err := rand.Int(rand.Reader, big.NewInt(int64(backoff))) + if err != nil { + return nil, fmt.Errorf("error generating jitter: %s", err) + } + + jitter := time.Duration(jitterInt.Int64()) + time.Sleep(backoff + jitter) + + continue + } + return record, nil + } + return nil, fmt.Errorf("error putting firehose record: %s", firehoseErr) +} diff --git a/internal/client/redis/client.go b/internal/client/redis/client.go new file mode 100644 index 0000000..fd05c8e --- /dev/null +++ b/internal/client/redis/client.go @@ -0,0 +1,65 @@ +package redis + +import ( + "context" + "fmt" + "time" + + "github.com/coinbase/baseca/internal/config" + "github.com/go-redis/redis/v8" +) + +const ( + // Redis Cache (Maximum Certificates Issued Within Default Time) 5 Minutes + _default_rate_limit = 0 + _default_window = 1 + _default_period = 5 +) + +type RedisIface interface { + HIncrBy(ctx context.Context, key, field string, incr int64) *redis.IntCmd + HGetAll(ctx context.Context, key string) *redis.StringStringMapCmd + HDel(ctx context.Context, key string, fields ...string) *redis.IntCmd + Expire(ctx context.Context, key string, expiration time.Duration) *redis.BoolCmd +} +type RedisClient struct { + Client RedisIface + Config *config.RedisConfig + + // Sliding Window + Limit int + Excluded []string + Period time.Duration + Window time.Duration +} + +func NewRedisClient(config *config.Config) (*RedisClient, error) { + redisConfig := &config.Redis + endpoint := fmt.Sprintf("%s:%s", redisConfig.Endpoint, redisConfig.Port) + client := redis.NewClient(&redis.Options{Addr: endpoint}) + + if redisConfig.Period == 0 { + redisConfig.Period = _default_period + } + + if redisConfig.Duration == 0 { + redisConfig.Duration = _default_window + } + + if redisConfig.RateLimit == 0 { + redisConfig.RateLimit = _default_rate_limit + } + + if redisConfig.Duration > redisConfig.Period { + return nil, fmt.Errorf("redis window duration [%d] must be greater than window period [%d]", redisConfig.Duration, redisConfig.Period) + } + + return &RedisClient{ + Client: client, + Config: redisConfig, + Limit: redisConfig.RateLimit, + Excluded: redisConfig.ExcludeRateLimit, + Period: time.Duration(redisConfig.Period) * time.Minute, + Window: time.Duration(redisConfig.Duration) * time.Minute, + }, nil +} diff --git a/internal/client/redis/increment.go b/internal/client/redis/increment.go new file mode 100644 index 0000000..6f31a82 --- /dev/null +++ b/internal/client/redis/increment.go @@ -0,0 +1,47 @@ +package redis + +import ( + "context" + "fmt" + "strconv" + "time" +) + +func (r *RedisClient) Increment(ctx context.Context, key string, increment int) error { + utc := time.Now().UTC() + timestamp := fmt.Sprint(utc.Truncate(r.Window).Unix()) + + value, err := r.Client.HIncrBy(ctx, key, timestamp, int64(increment)).Result() + if err != nil { + return err + } + + if value == 1 { + r.Client.Expire(ctx, key, r.Period) + } else if value >= int64(r.Limit) { + return fmt.Errorf("rate limit [%d], time period [%v], reset time: [%v], current limit: [%d]", r.Limit, r.Period, utc.Add(r.Period), value) + } + + values, err := r.Client.HGetAll(ctx, key).Result() + if err != nil { + return err + } + + threshold := fmt.Sprint(utc.Add(-r.Period).Unix()) + + aggregate := 0 + for time, count := range values { + if time > threshold { + i, _ := strconv.Atoi(count) + aggregate += i + } else { + r.Client.HDel(ctx, key, time) + } + } + + if aggregate >= r.Limit { + return fmt.Errorf("rate limit [%d], time period [%v], reset time: [%v], current aggregate: [%d]", r.Limit, r.Period, utc.Add(r.Period), aggregate) + } + + return nil +} diff --git a/internal/client/secretsmanager/client.go b/internal/client/secretsmanager/client.go new file mode 100644 index 0000000..c67b8c7 --- /dev/null +++ b/internal/client/secretsmanager/client.go @@ -0,0 +1,31 @@ +package secretsmanager + +import ( + "context" + "fmt" + + config_v2 "github.com/aws/aws-sdk-go-v2/config" + secretsmanager_v2 "github.com/aws/aws-sdk-go-v2/service/secretsmanager" + "github.com/coinbase/baseca/internal/config" +) + +type SecretsManagerClientIface interface { + GetSecretValue(ctx context.Context, params *secretsmanager_v2.GetSecretValueInput, optFns ...func(*secretsmanager_v2.Options)) (*secretsmanager_v2.GetSecretValueOutput, error) +} + +type SecretsManagerClient struct { + Client SecretsManagerClientIface +} + +func NewSecretsManagerClient(config *config.Config) (*SecretsManagerClient, error) { + cfg, err := config_v2.LoadDefaultConfig(context.TODO(), + config_v2.WithRegion(config.SecretsManager.Region), + ) + if err != nil { + return nil, fmt.Errorf("unable to create new session: %s", err) + } + + return &SecretsManagerClient{ + Client: secretsmanager_v2.NewFromConfig(cfg), + }, nil +} diff --git a/internal/client/secretsmanager/query.go b/internal/client/secretsmanager/query.go new file mode 100644 index 0000000..32cc297 --- /dev/null +++ b/internal/client/secretsmanager/query.go @@ -0,0 +1,45 @@ +package secretsmanager + +import ( + "context" + "encoding/json" + "fmt" + "os" + + secretsmanager_v2 "github.com/aws/aws-sdk-go-v2/service/secretsmanager" +) + +const ( + DATABASE_CREDENTIALS = "database_credentials" + AUTH_PRIVATE_KEY = "auth_private_key" + AUTH_PUBLIC_KEY = "auth_public_key" +) + +func (s *SecretsManagerClient) GetSecretValue(id string, key string) (*string, error) { + envValue, exists := os.LookupEnv(key) + if exists { + return &envValue, nil + } + + input := &secretsmanager_v2.GetSecretValueInput{ + SecretId: &id, + } + + result, err := s.Client.GetSecretValue(context.Background(), input) + if err != nil { + return nil, err + } + + var data map[string]string + err = json.Unmarshal([]byte(*result.SecretString), &data) + if err != nil { + return nil, fmt.Errorf("error unmarshal secrets manager data %s", id) + } + + value, ok := data[key] + if !ok { + return nil, fmt.Errorf("key %s not found in secrets manager %s", key, id) + } + + return &value, nil +} diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..181f7e5 --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,85 @@ +package config + +type SubordinateCertificate struct { + Region string `mapstructure:"region"` + CaArn string `mapstructure:"ca_arn"` + CaActiveDay int `mapstructure:"ca_active_day"` + AssumeRole bool `mapstructure:"assume_role"` + RoleArn string `mapstructure:"role_arn"` + RootCa bool `mapstructure:"root_ca"` + Default bool `mapstructure:"default"` +} + +type SubordinateCertificateAuthority struct { + BaseDirectory string `mapstructure:"directory"` + Country string `mapstructure:"country"` + Province string `mapstructure:"province"` + Locality string `mapstructure:"locality"` + Organization string `mapstructure:"organization"` + OrganizationUnit string `mapstructure:"organization_unit"` + Email string `mapstructure:"email"` + SigningAlgorithm string `mapstructure:"signing_algorithm"` + KeyAlgorithm string `mapstructure:"key_algorithm"` + KeySize int `mapstructure:"key_size"` +} + +type DatabaseConfig struct { + Driver string `mapstructure:"database_driver"` + Table string `mapstructure:"database_table"` + Endpoint string `mapstructure:"database_endpoint"` + ReaderEndpoint string `mapstructure:"database_reader_endpoint"` + User string `mapstructure:"database_user"` + Port int `mapstructure:"database_port"` + Region string `mapstructure:"region"` + SSLMode string `mapstructure:"ssl_mode"` +} + +type RedisConfig struct { + Endpoint string `mapstructure:"cluster_endpoint"` + Port string `mapstructure:"port"` + Duration int `mapstructure:"duration"` + Period int `mapstructure:"period"` + RateLimit int `mapstructure:"rate_limit"` + ExcludeRateLimit []string `mapstructure:"exclude_rate_limit"` +} + +type FirehoseConfig struct { + Stream string `mapstructure:"stream"` + Region string `mapstructure:"region"` +} + +type KMSConfig struct { + KeyId string `mapstructure:"key_id"` + SigningAlgorithm string `mapstructure:"signing_algorithm"` + Region string `mapstructure:"region"` + AuthValidity int `mapstructure:"auth_validity"` +} + +type SecretsManagerConfig struct { + SecretId string `mapstructure:"secret_id"` + Region string `mapstructure:"region"` +} + +type Stage struct { + Local []string `mapstructure:"local"` + Sandbox []string `mapstructure:"sandbox"` + Development []string `mapstructure:"development"` + Staging []string `mapstructure:"staging"` + PreProduction []string `mapstructure:"pre_production"` + Production []string `mapstructure:"production"` + Corporate []string `mapstructure:"corporate"` +} + +type Config struct { + GRPCServerAddress string `mapstructure:"grpc_server_address"` + OCSPServer []string `mapstructure:"ocsp_server"` + Database DatabaseConfig `mapstructure:"database"` + Redis RedisConfig `mapstructure:"redis"` + KMS KMSConfig `mapstructure:"kms"` + Firehose FirehoseConfig `mapstructure:"firehose"` + Domains []string `mapstructure:"domains"` + ACMPCA map[string]SubordinateCertificate `mapstructure:"acm_pca"` + SecretsManager SecretsManagerConfig `mapstructure:"secrets_manager"` + SubordinateMetadata SubordinateCertificateAuthority `mapstructure:"subordinate_ca_metadata"` + Environment Stage `mapstructure:"certificate_authority"` +} diff --git a/internal/config/environment.go b/internal/config/environment.go new file mode 100644 index 0000000..30297a5 --- /dev/null +++ b/internal/config/environment.go @@ -0,0 +1,51 @@ +package config + +import ( + "os" +) + +const ( + _Configuration = "CONFIGURATION" + _Environment = "ENVIRONMENT" + _AWSProvider = "aws" + _LocalProvider = "sandbox" + _LocalStage = "local" + _PrimaryConfiguration = "primary" +) + +type Environment struct { + Provider string + Stage string + Configuration string +} + +func ProvideEnvironment() Environment { + result := Environment{ + Provider: getProvider(), + Stage: getStage(), + Configuration: getConfiguration(), + } + return result +} + +func getProvider() string { + if os.Getenv(_Environment) != "" { + return _AWSProvider + } + return _LocalProvider +} + +func getStage() string { + configStage := os.Getenv(_Configuration) + if len(configStage) != 0 { + return configStage + } + return _LocalStage +} + +func getConfiguration() string { + switch os.Getenv(_Configuration) { + default: + return _PrimaryConfiguration + } +} diff --git a/internal/config/fx.go b/internal/config/fx.go new file mode 100644 index 0000000..f773335 --- /dev/null +++ b/internal/config/fx.go @@ -0,0 +1,65 @@ +package config + +import ( + "log" + + "github.com/coinbase/baseca/internal/logger" + "go.uber.org/fx" +) + +var Module = fx.Options( + fx.Provide( + ProvideConfigPathResolver, + ProvideConfig, + ProvideEnvironment, + ), +) + +var Configuration *Config + +type Parameter struct { + fx.In + + Environment Environment + PathResolver ConfigFilePathResolver +} + +type Result struct { + fx.Out + + RawConfig *Config + ConfigProvider ConfigProvider +} + +type ConfigProvider interface { + Get(path string, cfg any) error + Exists(path string) bool +} + +func ProvideConfig(p Parameter) (Result, error) { + var result Result + ctxLogger := logger.ContextLogger{Logger: logger.DefaultLogger} + + _, resolver := p.Environment, p.PathResolver + path, err := resolver.Resolve() + if err != nil { + log.Fatalf("configuration file does not exist [%s]", err.Error()) + } + ctxLogger.Info("Load Config From File, Config Path: " + path) + v, err := BuildViper(path) + if err != nil { + ctxLogger.Error(err.Error()) + } + + config, err := LoadConfig(v) + if err != nil { + return result, err + } + + Configuration = config // Set Globally + + result.RawConfig = config + result.ConfigProvider = NewConfigProviderFromViper(v) + + return result, nil +} diff --git a/internal/config/load.go b/internal/config/load.go new file mode 100644 index 0000000..338df25 --- /dev/null +++ b/internal/config/load.go @@ -0,0 +1,72 @@ +package config + +import ( + "errors" + "fmt" + + "github.com/coinbase/baseca/internal/logger" + "github.com/mitchellh/mapstructure" + "github.com/spf13/viper" + "go.uber.org/zap" +) + +type configProvider struct { + v *viper.Viper +} + +var _ ConfigProvider = (*configProvider)(nil) + +func BuildViper(path string) (*viper.Viper, error) { + ctxLogger := logger.ContextLogger{Logger: logger.DefaultLogger} + ctxLogger.Info("setting up Viper to load configuration", zap.String("config-path", path)) + + v := viper.New() + v.SetConfigFile(path) + v.AutomaticEnv() + + if err := v.ReadInConfig(); err != nil { + return nil, err + } + + return v, nil +} + +func LoadConfig(viper *viper.Viper) (*Config, error) { + if viper == nil { + return nil, errors.New("failed to load config") + } + + c := Config{} + if err := viper.Unmarshal(&c); err != nil { + return nil, errors.New("failed to read configuration file") + } + return &c, nil +} + +func NewConfigProviderFromViper(v *viper.Viper) ConfigProvider { + return &configProvider{v: v} +} + +func (cp *configProvider) Get(path string, cfg any) error { + if !cp.Exists(path) { + return fmt.Errorf("path %s is not found in configuration", path) + } + + if err := cp.v.UnmarshalKey(path, cfg, func(setting *mapstructure.DecoderConfig) { + setting.ErrorUnused = true + setting.ZeroFields = true + }); err != nil { + return err + } + + if u, ok := cfg.(interface{ Validate() error }); ok { + if err := u.Validate(); err != nil { + return err + } + } + return nil +} + +func (cp *configProvider) Exists(path string) bool { + return cp.v.Get(path) != nil +} diff --git a/internal/config/path.go b/internal/config/path.go new file mode 100644 index 0000000..f06f311 --- /dev/null +++ b/internal/config/path.go @@ -0,0 +1,41 @@ +package config + +import ( + "fmt" + + "github.com/bazelbuild/rules_go/go/tools/bazel" +) + +const ( + _template = "config/config.%s.yml" +) + +type ConfigFilePathResolver interface { + Resolve() (string, error) +} + +type Resolver struct { + Environment Environment + Template string +} + +func ProvideConfigPathResolver(e Environment) ConfigFilePathResolver { + return &Resolver{Environment: e, Template: _template} +} + +var _ ConfigFilePathResolver = (*Resolver)(nil) + +func (r Resolver) Resolve() (string, error) { + configurationFileName := configurationFileName(r.Environment) + location := fmt.Sprintf(r.Template, configurationFileName) + + path, err := bazel.Runfile(location) + if err != nil { + return "", fmt.Errorf(location) + } + return path, nil +} + +func configurationFileName(e Environment) string { + return fmt.Sprintf("%s.%s.%s", e.Configuration, e.Stage, e.Provider) +} diff --git a/internal/gateway/fx.go b/internal/gateway/fx.go new file mode 100644 index 0000000..a063b0c --- /dev/null +++ b/internal/gateway/fx.go @@ -0,0 +1,215 @@ +package gateway + +import ( + "context" + "database/sql" + "fmt" + "log" + "net" + "time" + + "github.com/allegro/bigcache/v3" + "github.com/casbin/casbin/v2" + db "github.com/coinbase/baseca/db/sqlc" + apiv1 "github.com/coinbase/baseca/gen/go/baseca/v1" + "github.com/coinbase/baseca/internal/client/secretsmanager" + "github.com/coinbase/baseca/internal/config" + lib "github.com/coinbase/baseca/internal/lib/authentication" + "github.com/coinbase/baseca/internal/logger" + "github.com/coinbase/baseca/internal/v1/accounts" + "github.com/coinbase/baseca/internal/v1/certificate" + "github.com/coinbase/baseca/internal/v1/health" + "github.com/coinbase/baseca/internal/v1/middleware" + "github.com/coinbase/baseca/internal/v1/users" + grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware" + "go.uber.org/fx" + "go.uber.org/zap" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + healthpb "google.golang.org/grpc/health/grpc_health_v1" + "google.golang.org/grpc/reflection" +) + +const ( + _authorization_path = "config/permissions" + _default_cleanup = 10 * time.Minute +) + +type Server struct { + apiv1.CertificateServer + Store db.DatabaseEndpoints + Auth lib.Auth + Service *accounts.Service + Certificate *certificate.Certificate + User *users.User + Middleware *middleware.Middleware +} + +var Module = fx.Options( + fx.Invoke(StartRPC), +) + +func StartRPC(lc fx.Lifecycle, cfg *config.Config) error { + client, err := secretsmanager.NewSecretsManagerClient(cfg) + if err != nil { + log.Fatalf("error instantiating secrets manager client") + } + + credentials, err := client.GetSecretValue(cfg.SecretsManager.SecretId, secretsmanager.DATABASE_CREDENTIALS) + if err != nil { + log.Fatalf("error getting database credentials: %s", err) + } + + caError := certificate.ValidateSubordinateParameters(cfg.SubordinateMetadata) + if caError != nil { + log.Fatalf("error in subordinate ca configuration: %s", caError) + } + + database_endpoint, err := GetPgConn(cfg.Database, cfg.Database.Endpoint, *credentials) + if err != nil { + log.Fatalf("error building database writer endpoint: %s", err.Error()) + } + database_reader_endpoint, err := GetPgConn(cfg.Database, cfg.Database.ReaderEndpoint, *credentials) + if err != nil { + log.Fatalf("error building database reader endpoint: %s", err.Error()) + } + + if err != nil { + log.Fatalf("cannot connect to the database: %s", err) + } + + authorization_model := fmt.Sprintf("%s/model.conf", _authorization_path) + authorization_policy := fmt.Sprintf("%s/policy.csv", _authorization_path) + enforcer, _ := casbin.NewEnforcer(authorization_model, authorization_policy) + + writer_endpoint := db.BuildDatastore(database_endpoint) + reader_endpoint := db.BuildDatastore(database_reader_endpoint) + db := db.DatabaseEndpoints{Writer: writer_endpoint, Reader: reader_endpoint} + + // RPC Server + server, err := BuildServer(db, cfg, enforcer) + if err != nil { + log.Fatal("cannot Start grpc server", err) + } + + extractor := func(resp any, err error, code codes.Code) string { + if err != nil { + if customErr, ok := err.(*logger.Error); ok && customErr.InternalError != nil { + return customErr.InternalError.Error() + } + return err.Error() + } + return "success" + } + + term := make(chan error) + var grpcServer *grpc.Server + + lc.Append(fx.Hook{ + OnStart: func(ctx context.Context) error { + + // RPC Middleware Logger + logInterceptor := logger.RpcLogger(extractor) + interceptors := grpc_middleware.ChainUnaryServer(server.Middleware.ServerAuthenticationInterceptor, logInterceptor) + grpcServer = grpc.NewServer(grpc.UnaryInterceptor(interceptors)) + + // Service Registration + apiv1.RegisterAccountServer(grpcServer, server.User) + apiv1.RegisterServiceServer(grpcServer, server.Service) + apiv1.RegisterCertificateServer(grpcServer, server.Certificate) + + hs := health.NewHealthServer() + healthpb.RegisterHealthServer(grpcServer, hs) + reflection.Register(grpcServer) + + listener, err := net.Listen("tcp", config.Configuration.GRPCServerAddress) + if err != nil { + log.Fatal("cannot create rpc listener") + } + + go func() { + term <- grpcServer.Serve(listener) + }() + + return nil + }, + OnStop: func(ctx context.Context) error { + if grpcServer == nil { + return nil + } + + grpcServer.Stop() + var err error + select { + case err = <-term: + case <-ctx.Done(): + err = fmt.Errorf("context deadline: %w", err) + } + + logger.DefaultLogger.Info("server exited:", zap.Error(err)) + return nil + }, + }) + + return nil +} + +func BuildServer(store db.DatabaseEndpoints, cfg *config.Config, enforcer *casbin.Enforcer) (*Server, error) { + signer, err := lib.BuildSigningClient(cfg) + if err != nil { + return nil, err + } + + auth, err := lib.NewAuthSigningMetadata(signer) + if err != nil { + return nil, err + } + + cache, err := bigcache.New(context.Background(), bigcache.DefaultConfig(_default_cleanup)) + if err != nil { + return nil, fmt.Errorf("error instantiating memory cache") + } + + service := accounts.New(cfg, store) + user := users.New(cfg, store, auth) + middleware := middleware.New(auth, store, enforcer, cache) + certificate, err := certificate.New(cfg, store) + if err != nil { + return nil, fmt.Errorf("issue instantiating certificate client [%s]", err) + } + + server := &Server{ + Store: store, + Auth: auth, + Service: service, + Certificate: certificate, + User: user, + Middleware: middleware, + } + + return server, nil +} + +func GetPgConn(conf config.DatabaseConfig, endpoint, credentials string) (*sql.DB, error) { + dataSource := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s", endpoint, conf.Port, conf.User, credentials, conf.Table) + + if conf.SSLMode == "disable" { + dataSource = fmt.Sprintf("%s sslmode=disable", dataSource) + } else { + dataSource = fmt.Sprintf("%s sslmode=verify-full sslrootcert=config/certificate_authority/rds.global.bundle.pem", dataSource) + } + + // Open Database Connection + sqlClient, err := sql.Open("postgres", dataSource) + if err != nil { + return nil, fmt.Errorf("error: The data source arguments are not valid: %v", err) + } + + // Validate Connection + err = sqlClient.Ping() + if err != nil { + return nil, fmt.Errorf("error: Could not establish a connection with the database: %v", err) + } + + return sqlClient, nil +} diff --git a/internal/lib/authentication/credentials.go b/internal/lib/authentication/credentials.go new file mode 100644 index 0000000..4d80d5c --- /dev/null +++ b/internal/lib/authentication/credentials.go @@ -0,0 +1,35 @@ +package lib + +import ( + "crypto/rand" + "fmt" + "math/big" + + "golang.org/x/crypto/bcrypt" +) + +func HashPassword(password string) (string, error) { + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), 15) + if err != nil { + return "", fmt.Errorf("failed to hash password %w", err) + } + return string(hashedPassword), nil +} + +func CheckPassword(password string, hashedPassword string) error { + return bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password)) +} + +func GenerateClientToken(n int) (string, error) { + const letters = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!#$%^*()-_=+{}[]:;,.?/~" + ret := make([]byte, n) + for i := 0; i < n; i++ { + num, err := rand.Int(rand.Reader, big.NewInt(int64(len(letters)))) + if err != nil { + return "", err + } + ret[i] = letters[num.Int64()] + } + + return string(ret), nil +} diff --git a/internal/lib/authentication/issuer.go b/internal/lib/authentication/issuer.go new file mode 100644 index 0000000..d5075ee --- /dev/null +++ b/internal/lib/authentication/issuer.go @@ -0,0 +1,231 @@ +package lib + +import ( + "context" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "strings" + "time" + + config_v2 "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/kms" + "github.com/aws/aws-sdk-go-v2/service/kms/types" + "github.com/coinbase/baseca/internal/config" + "github.com/google/uuid" +) + +type KmsClientIface interface { + Sign(ctx context.Context, params *kms.SignInput, optFns ...func(*kms.Options)) (*kms.SignOutput, error) + Verify(ctx context.Context, params *kms.VerifyInput, optFns ...func(*kms.Options)) (*kms.VerifyOutput, error) +} + +type signer struct { + sign func(ctx context.Context, message string) ([]byte, error) + verifySignature func(ctx context.Context, message string, signature string) (bool, error) + algorithm func(ctx context.Context) string + keyId string + time func() time.Time +} + +type ClaimProps struct { + Subject uuid.UUID + Permission string + ValidForMinutes int64 +} + +type Client struct { + KmsClient KmsClientIface + KeyId string + SigningAlgorithm string +} + +type Header struct { + Algorithm string + Type string + KeyId string +} + +type Claims struct { + Permission string `json:"permission"` + Subject uuid.UUID `json:"sub"` + IssuedAt time.Time `json:"iss"` + ExpiresAt time.Time `json:"exp"` + NotBefore time.Time `json:"not_before"` +} + +type Auth interface { + Issue(context.Context, ClaimProps) (*string, error) + Verify(context.Context, string) (*Claims, error) +} + +var signingAlgorithms = map[string]types.SigningAlgorithmSpec{ + "RSASSA_PSS_SHA_256": types.SigningAlgorithmSpecRsassaPssSha256, + "RSASSA_PSS_SHA_384": types.SigningAlgorithmSpecRsassaPssSha384, + "RSASSA_PSS_SHA_512": types.SigningAlgorithmSpecRsassaPssSha512, + "RSASSA_PKCS1_V1_5_SHA_256": types.SigningAlgorithmSpecRsassaPkcs1V15Sha256, + "RSASSA_PKCS1_V1_5_SHA_384": types.SigningAlgorithmSpecRsassaPkcs1V15Sha384, + "RSASSA_PKCS1_V1_5_SHA_512": types.SigningAlgorithmSpecEcdsaSha512, +} + +func BuildSigningClient(config *config.Config) (*Client, error) { + cfg, err := config_v2.LoadDefaultConfig(context.TODO(), + config_v2.WithRegion(config.KMS.Region), + ) + if err != nil { + return nil, fmt.Errorf("could not load kms configuration: %s", err) + } + + return &Client{ + KmsClient: kms.NewFromConfig(cfg), + KeyId: config.KMS.KeyId, + SigningAlgorithm: config.KMS.SigningAlgorithm, + }, nil +} + +func NewAuthSigningMetadata(c *Client) (Auth, error) { + s := &signer{ + sign: func(ctx context.Context, message string) ([]byte, error) { + algorithm, ok := signingAlgorithms[c.SigningAlgorithm] + if !ok { + return nil, fmt.Errorf("signing algorithm mapping not supported: %s", c.SigningAlgorithm) + } + signInput := kms.SignInput{ + KeyId: &c.KeyId, + Message: []byte(message), + SigningAlgorithm: algorithm, + } + + signOutput, err := c.KmsClient.Sign(ctx, &signInput) + if err != nil { + return nil, err + } + return signOutput.Signature, nil + }, + verifySignature: func(ctx context.Context, b64Message string, b64Signature string) (bool, error) { + decodedSignature, err := base64.RawURLEncoding.DecodeString(b64Signature) + if err != nil { + return false, fmt.Errorf("error decoding base64 signature: %s, error: %w", b64Signature, err) + } + + algorithm, ok := signingAlgorithms[c.SigningAlgorithm] + if !ok { + return false, fmt.Errorf("signing algorithm mapping not supported: %s", c.SigningAlgorithm) + } + verifyInput := &kms.VerifyInput{ + KeyId: &c.KeyId, + Message: []byte(b64Message), + Signature: decodedSignature, + SigningAlgorithm: algorithm, + } + + verifyOutput, err := c.KmsClient.Verify(ctx, verifyInput) + if err != nil { + return false, err + } + return verifyOutput.SignatureValid, nil + }, + algorithm: func(ctx context.Context) string { + return c.SigningAlgorithm + }, + keyId: c.KeyId, + time: time.Now().UTC, + } + return s, nil +} + +func (s *signer) Issue(ctx context.Context, p ClaimProps) (*string, error) { + header := Header{ + Algorithm: s.algorithm(ctx), + Type: "JWT", + KeyId: s.keyId, + } + + tokenJson, err := json.Marshal(header) + if err != nil { + return nil, fmt.Errorf("could not marshal token to json: %s", err) + } + + headerStr := base64.RawURLEncoding.EncodeToString([]byte(tokenJson)) + claims := Claims{ + Permission: p.Permission, + Subject: p.Subject, + IssuedAt: time.Now().UTC(), + NotBefore: time.Now().UTC(), + ExpiresAt: time.Now().Add(time.Duration(p.ValidForMinutes * int64(time.Minute))).UTC(), + } + claimsJson, err := json.Marshal(claims) + if err != nil { + return nil, fmt.Errorf("could not marshal token claims to json: %s", err) + } + claimsStr := base64.RawURLEncoding.EncodeToString([]byte(claimsJson)) + + message := fmt.Sprintf("%s.%s", headerStr, claimsStr) + signatureBytes, err := s.sign(ctx, message) + if err != nil { + return nil, fmt.Errorf("token signing error: %s", err) + } + + signatureStr := base64.RawURLEncoding.EncodeToString(signatureBytes) + tokenStr := fmt.Sprintf("%s.%s.%s", headerStr, claimsStr, signatureStr) + return &tokenStr, nil +} + +func (s *signer) Verify(ctx context.Context, jwt string) (*Claims, error) { + x := strings.Split(jwt, ".") + if len(x) != 3 { + return nil, fmt.Errorf("invalid jwt format") + } + + headerB64 := x[0] + claimsB64 := x[1] + signatureB64 := x[2] + + headerJson, err := base64.RawURLEncoding.DecodeString(headerB64) + if err != nil { + return nil, fmt.Errorf("base64 decoding header failed: %w", err) + } + + header := &Header{} + err = json.Unmarshal(headerJson, header) + if err != nil { + return nil, fmt.Errorf("json unmarshalling header failed: %v", err) + } + + claimsJson, err := base64.RawURLEncoding.DecodeString(claimsB64) + if err != nil { + return nil, fmt.Errorf("base64 decoding claims failed: %v", err) + } + + claims := &Claims{} + err = json.Unmarshal(claimsJson, claims) + if err != nil { + return nil, fmt.Errorf("json unmarshalling claims failed: %v", err) + } + + err = claims.Valid() + if err != nil { + return nil, err + } + isVerified, err := s.verifySignature(ctx, fmt.Sprintf("%s.%s", x[0], x[1]), signatureB64) + if err != nil { + return nil, fmt.Errorf("error verifying signature: %v", err) + } + + if !isVerified { + return nil, err + } + return claims, nil +} + +func (c *Claims) Valid() error { + if time.Now().UTC().After(c.ExpiresAt) { + return errors.New("token has expired") + } + + if time.Now().UTC().Before(c.NotBefore) { + return errors.New("token is invalid") + } + return nil +} diff --git a/internal/lib/crypto/chain.go b/internal/lib/crypto/chain.go new file mode 100644 index 0000000..b6f2f10 --- /dev/null +++ b/internal/lib/crypto/chain.go @@ -0,0 +1,145 @@ +package crypto + +import ( + "bytes" + "crypto/x509" + "encoding/pem" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/coinbase/baseca/internal/types" +) + +const ( + _subordinatePrivateKey = "/ca-subordinate.key" + _subordinateCertificate = "/ca-subordinate.crt" + _subordinateSerialNumber = "/serial.txt" + _intermediateCertificate = "/ca-intermediate.crt" + _certificateAuthorityArn = "/acm-pca.txt" + _rootCertificate = "/ca-root.crt" +) + +func BuildCertificateChain(ca_path string, certificate []byte, subordinate_ca []byte) (*bytes.Buffer, *bytes.Buffer, *bytes.Buffer, error) { + var err error + leaf_certificate := new(bytes.Buffer) + intermediate_chained_certificate := new(bytes.Buffer) + root_chained_certificate := new(bytes.Buffer) + + intermediate_ca, root_ca, err := retrieveCertificateAuthority(ca_path) + if err != nil { + return nil, nil, nil, err + } + + // End Entity Certificate + err = pem.Encode(leaf_certificate, &pem.Block{Type: "CERTIFICATE", Bytes: certificate}) + if err != nil { + return nil, nil, nil, err + } + + // Build Intermediate Certificate Chain + err = pem.Encode(intermediate_chained_certificate, &pem.Block{Type: "CERTIFICATE", Bytes: certificate}) + if err != nil { + return nil, nil, nil, err + } + + var intermediate_chain [][]byte + if intermediate_ca != nil { + intermediate_chain = [][]byte{subordinate_ca, intermediate_ca} + } + + for _, crt := range intermediate_chain { + err = pem.Encode(intermediate_chained_certificate, &pem.Block{Type: "CERTIFICATE", Bytes: crt}) + if err != nil { + return nil, nil, nil, err + } + } + + // Build Root Certificate Chains Depending on Existence of Intermediate CA + err = pem.Encode(root_chained_certificate, &pem.Block{Type: "CERTIFICATE", Bytes: certificate}) + if err != nil { + return nil, nil, nil, err + } + + var root_chain [][]byte + if intermediate_ca != nil { + root_chain = [][]byte{subordinate_ca, intermediate_ca, root_ca} + } else { + root_chain = [][]byte{subordinate_ca, root_ca} + } + + for _, crt := range root_chain { + err = pem.Encode(root_chained_certificate, &pem.Block{Type: "CERTIFICATE", Bytes: crt}) + if err != nil { + return nil, nil, nil, err + } + } + + return leaf_certificate, intermediate_chained_certificate, root_chained_certificate, nil +} + +func GetSubordinateCaPath(service string) (*string, *string, error) { + directoryPath := filepath.Join(types.SubordinatePath, service) + + caPath := filepath.Join(directoryPath, _subordinateCertificate) + if !strings.HasPrefix(caPath, types.SubordinatePath) { + return nil, nil, fmt.Errorf("unsafe file input, read ca subordinate certificate") + } + + keyPath := filepath.Join(directoryPath, _subordinatePrivateKey) + if !strings.HasPrefix(caPath, types.SubordinatePath) { + return nil, nil, fmt.Errorf("unsafe file input, read ca subordinate private key") + } + + return &caPath, &keyPath, nil +} + +func retrieveCertificateAuthority(service string) ([]byte, []byte, error) { + intermediatePath := filepath.Join(types.SubordinatePath, service+_intermediateCertificate) + rootPath := filepath.Join(types.SubordinatePath, service+_rootCertificate) + + if !strings.HasPrefix(intermediatePath, types.SubordinatePath) || !strings.HasPrefix(rootPath, types.SubordinatePath) { + return nil, nil, fmt.Errorf("unsafe file input") + } + + var x509_intermediate_ca *x509.Certificate + if _, err := os.Stat(intermediatePath); !os.IsNotExist(err) { + intermediate_ca, err := os.ReadFile(filepath.Clean(intermediatePath)) + if err != nil { + return nil, nil, err + } + x509_intermediate_ca, err = parseCertificate(intermediate_ca) + if err != nil { + return nil, nil, err + } + } + + root_ca, err := os.ReadFile(filepath.Clean(rootPath)) + if err != nil { + return nil, nil, err + } + + x509_root_ca, err := parseCertificate(root_ca) + if err != nil { + return nil, nil, err + } + + var intermediateRaw []byte + if x509_intermediate_ca != nil { + intermediateRaw = x509_intermediate_ca.Raw + } + return intermediateRaw, x509_root_ca.Raw, nil +} + +func parseCertificate(ca []byte) (*x509.Certificate, error) { + pemBlock, _ := pem.Decode(ca) + if pemBlock == nil { + return nil, fmt.Errorf("failed to parse certificate PEM") + } + cert, err := x509.ParseCertificate(pemBlock.Bytes) + if err != nil { + return nil, err + } + return cert, nil +} diff --git a/internal/lib/crypto/csr.go b/internal/lib/crypto/csr.go new file mode 100644 index 0000000..a81fe61 --- /dev/null +++ b/internal/lib/crypto/csr.go @@ -0,0 +1,99 @@ +package crypto + +import ( + "bytes" + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "fmt" + + "github.com/coinbase/baseca/internal/types" +) + +func GenerateCSR(csr types.CertificateRequest) (*types.SigningRequest, error) { + var generator CSRGenerator + + switch csr.PublicKeyAlgorithm { + case x509.RSA: + if _, ok := types.PublicKeyAlgorithms["RSA"].KeySize[csr.KeySize]; !ok { + return nil, fmt.Errorf("rsa invalid key size %d", csr.KeySize) + } + if _, ok := types.PublicKeyAlgorithms["RSA"].SigningAlgorithm[csr.SigningAlgorithm]; !ok { + return nil, fmt.Errorf("rsa invalid signing algorithm %s", csr.SigningAlgorithm) + } + generator = &SigningRequestGeneratorRSA{Size: csr.KeySize} + case x509.ECDSA: + if _, ok := types.PublicKeyAlgorithms["ECDSA"].KeySize[csr.KeySize]; !ok { + return nil, fmt.Errorf("ecdsa invalid key size %d", csr.KeySize) + } + if _, ok := types.PublicKeyAlgorithms["ECDSA"].SigningAlgorithm[csr.SigningAlgorithm]; !ok { + return nil, fmt.Errorf("ecdsa invalid signing algorithm %s", csr.SigningAlgorithm) + } + generator = &SigningRequestGeneratorECDSA{Curve: csr.KeySize} + default: + return nil, fmt.Errorf("unsupported public key algorithm") + } + + pk, err := generator.Generate() + if err != nil { + return nil, fmt.Errorf("error generating private key [%s]: %w", generator.KeyType(), err) + } + + subject := pkix.Name{ + CommonName: csr.CommonName, + Country: csr.DistinguishedName.Country, + Province: csr.DistinguishedName.Province, + Locality: csr.DistinguishedName.Locality, + Organization: csr.DistinguishedName.Organization, + OrganizationalUnit: csr.DistinguishedName.OrganizationalUnit, + } + + template := x509.CertificateRequest{ + Subject: subject, + SignatureAlgorithm: csr.SigningAlgorithm, + DNSNames: csr.SubjectAlternateNames, + } + + csrBytes, err := x509.CreateCertificateRequest(rand.Reader, &template, pk) + if err != nil { + return nil, fmt.Errorf("error creating certificate request: %w", err) + } + + certificatePem := new(bytes.Buffer) + err = pem.Encode(certificatePem, &pem.Block{ + Type: "CERTIFICATE REQUEST", + Bytes: csrBytes, + }) + + if err != nil { + return nil, fmt.Errorf("error encoding certificate request (csr): %w", err) + } + + if len(csr.Output.CertificateSigningRequest) != 0 { + if err := writeFileToSystem(csr.Output.CertificateSigningRequest, certificatePem.Bytes()); err != nil { + return nil, fmt.Errorf("error writing certificate signing request (csr) to [%s]: %w", csr.Output.CertificateSigningRequest, err) + } + } + + pkBytes, err := generator.MarshalPrivateKey(pk) + if err != nil { + return nil, fmt.Errorf("error marshaling private key: %w", err) + } + + pkBlock := &pem.Block{ + Type: generator.KeyType(), + Bytes: pkBytes, + } + + if len(csr.Output.PrivateKey) != 0 { + if err := writeFileToSystem(csr.Output.PrivateKey, pem.EncodeToMemory(pkBlock)); err != nil { + return nil, fmt.Errorf("error writing private key to [%s]: %w", csr.Output.PrivateKey, err) + } + } + + return &types.SigningRequest{ + CSR: certificatePem, + PrivateKey: pkBlock, + }, nil +} diff --git a/internal/lib/crypto/csr_test.go b/internal/lib/crypto/csr_test.go new file mode 100644 index 0000000..9673887 --- /dev/null +++ b/internal/lib/crypto/csr_test.go @@ -0,0 +1,61 @@ +package crypto + +import ( + "crypto/x509" + "encoding/pem" + "testing" + + "github.com/coinbase/baseca/internal/types" + "github.com/stretchr/testify/assert" +) + +func TestGenerateCSR(t *testing.T) { + csr := types.CertificateRequest{ + PublicKeyAlgorithm: x509.RSA, + KeySize: 2048, + SigningAlgorithm: x509.SHA256WithRSA, + CommonName: "example.com", + DistinguishedName: types.DistinguishedName{ + Country: []string{"US"}, + Province: []string{"CA"}, + }, + SubjectAlternateNames: []string{"www.example.com", "sub.example.com"}, + Output: types.Output{ + CertificateSigningRequest: "/tmp/unit_test_csr.pem", + PrivateKey: "/tmp/unit_test_pk.pem", + }, + } + + // Generate CSR with RSA Key Pair + rsaSigningRequest, err := GenerateCSR(csr) + assert.NoError(t, err) + assert.NotNil(t, rsaSigningRequest) + + assert.Contains(t, string(rsaSigningRequest.CSR.String()), "CERTIFICATE REQUEST") + assert.Contains(t, string(pem.EncodeToMemory(rsaSigningRequest.PrivateKey)), "RSA PRIVATE KEY") + + // Create an ECDSA CertificateRequest + ecdsaCsr := types.CertificateRequest{ + PublicKeyAlgorithm: x509.ECDSA, + KeySize: 256, + SigningAlgorithm: x509.ECDSAWithSHA512, + CommonName: "example.com", + DistinguishedName: types.DistinguishedName{ + Country: []string{"US"}, + Province: []string{"CA"}, + }, + SubjectAlternateNames: []string{"www.example.com", "sub.example.com"}, + Output: types.Output{ + CertificateSigningRequest: "/tmp/unit_test_csr.pem", + PrivateKey: "/tmp/unit_test_pk.pem", + }, + } + + // Generate the CSR with ECDSA Key Pair + ecdsaSigningRequest, err := GenerateCSR(ecdsaCsr) + assert.NoError(t, err) + assert.NotNil(t, ecdsaSigningRequest) + + assert.Contains(t, string(ecdsaSigningRequest.CSR.String()), "CERTIFICATE REQUEST") + assert.Contains(t, string(pem.EncodeToMemory(ecdsaSigningRequest.PrivateKey)), "EC PRIVATE KEY") +} diff --git a/internal/lib/crypto/generate.go b/internal/lib/crypto/generate.go new file mode 100644 index 0000000..f6c4d79 --- /dev/null +++ b/internal/lib/crypto/generate.go @@ -0,0 +1,95 @@ +package crypto + +import ( + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "fmt" + + "github.com/coinbase/baseca/internal/types" +) + +type CSRGenerator interface { + Generate() (crypto.PrivateKey, error) + KeyType() string + MarshalPrivateKey(key crypto.PrivateKey) ([]byte, error) + SupportsPublicKeyAlgorithm(algorithm x509.PublicKeyAlgorithm) bool + SupportsSigningAlgorithm(algorithm x509.SignatureAlgorithm) bool + SupportsKeySize(size int) bool +} + +type SigningRequestGeneratorRSA struct { + Size int +} + +type SigningRequestGeneratorECDSA struct { + Curve int +} + +// RSA Interface +func (r *SigningRequestGeneratorRSA) Generate() (crypto.PrivateKey, error) { + return rsa.GenerateKey(rand.Reader, r.Size) +} + +func (r *SigningRequestGeneratorRSA) KeyType() string { + return "RSA PRIVATE KEY" +} + +func (r *SigningRequestGeneratorRSA) MarshalPrivateKey(key crypto.PrivateKey) ([]byte, error) { + return x509.MarshalPKCS1PrivateKey(key.(*rsa.PrivateKey)), nil +} + +func (r *SigningRequestGeneratorRSA) SupportsPublicKeyAlgorithm(algorithm x509.PublicKeyAlgorithm) bool { + return algorithm == x509.RSA +} + +func (r *SigningRequestGeneratorRSA) SupportsSigningAlgorithm(algorithm x509.SignatureAlgorithm) bool { + _, ok := types.PublicKeyAlgorithms["RSA"].SigningAlgorithm[algorithm] + return ok +} + +func (r *SigningRequestGeneratorRSA) SupportsKeySize(size int) bool { + _, ok := types.PublicKeyAlgorithms["RSA"].KeySize[size] + return ok +} + +// ECDSA Interface +func (e *SigningRequestGeneratorECDSA) Generate() (crypto.PrivateKey, error) { + c, ok := types.PublicKeyAlgorithms["ECDSA"].KeySize[e.Curve] + + if !ok { + return nil, fmt.Errorf("ecdsa curve [%d] not supported", e.Curve) + } + + curve, ok := c.(elliptic.Curve) + if !ok { + return nil, fmt.Errorf("invalid elliptic.Curve type") + } + + return ecdsa.GenerateKey(curve, rand.Reader) +} + +func (e *SigningRequestGeneratorECDSA) KeyType() string { + return "EC PRIVATE KEY" +} + +func (e *SigningRequestGeneratorECDSA) MarshalPrivateKey(key crypto.PrivateKey) ([]byte, error) { + return x509.MarshalECPrivateKey(key.(*ecdsa.PrivateKey)) +} + +func (e *SigningRequestGeneratorECDSA) SupportsPublicKeyAlgorithm(algorithm x509.PublicKeyAlgorithm) bool { + return algorithm == x509.ECDSA +} + +func (e *SigningRequestGeneratorECDSA) SupportsSigningAlgorithm(algorithm x509.SignatureAlgorithm) bool { + _, ok := types.PublicKeyAlgorithms["ECDSA"].SigningAlgorithm[algorithm] + return ok +} + +func (e *SigningRequestGeneratorECDSA) SupportsKeySize(size int) bool { + _, ok := types.PublicKeyAlgorithms["ECDSA"].KeySize[size] + return ok +} diff --git a/internal/lib/crypto/generate_test.go b/internal/lib/crypto/generate_test.go new file mode 100644 index 0000000..d89bfd0 --- /dev/null +++ b/internal/lib/crypto/generate_test.go @@ -0,0 +1,71 @@ +package crypto + +import ( + "crypto/x509" + "testing" +) + +func TestSigningRequestGeneratorRSA(t *testing.T) { + r := &SigningRequestGeneratorRSA{ + Size: 2048, + } + + key, err := r.Generate() + if err != nil { + t.Fatalf("error generating rsa private key: %v", err) + } + + if keyType := r.KeyType(); keyType != "RSA PRIVATE KEY" { + t.Errorf("RSA PRIVATE KEY does not exist within private key") + + } + + if !r.SupportsPublicKeyAlgorithm(x509.RSA) { + t.Errorf("rsa public key algorithm not supported") + } + + if !r.SupportsSigningAlgorithm(x509.SHA256WithRSA) { + t.Errorf("SHA256WithRSA signing algorithm not supported") + } + + if !r.SupportsKeySize(2048) { + t.Errorf("rsa key size not supported") + } + + _, err = r.MarshalPrivateKey(key) + if err != nil { + t.Errorf("error marshaling rsa private key: %v", err) + } +} + +func TestSigningRequestGeneratorECDSA(t *testing.T) { + e := &SigningRequestGeneratorECDSA{ + Curve: 256, + } + + key, err := e.Generate() + if err != nil { + t.Fatalf("error generating ecdsa private key: %v", err) + } + + if keyType := e.KeyType(); keyType != "EC PRIVATE KEY" { + t.Errorf("EC PRIVATE KEY does not exist within private key") + } + + if !e.SupportsPublicKeyAlgorithm(x509.ECDSA) { + t.Errorf("ecdsa public key algorithm not supported") + } + + if !e.SupportsSigningAlgorithm(x509.ECDSAWithSHA256) { + t.Errorf("ECDSAWithSHA256 signing algorithm not supported") + } + + if !e.SupportsKeySize(256) { + t.Errorf("ecdsa curve size not supported") + } + + _, err = e.MarshalPrivateKey(key) + if err != nil { + t.Errorf("error marshaling ecdsa private key: %v", err) + } +} diff --git a/internal/lib/crypto/pk.go b/internal/lib/crypto/pk.go new file mode 100644 index 0000000..5c2f4f9 --- /dev/null +++ b/internal/lib/crypto/pk.go @@ -0,0 +1,184 @@ +package crypto + +import ( + "crypto" + "crypto/ecdsa" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "errors" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/coinbase/baseca/internal/types" +) + +type RSA struct { + PublicKey *rsa.PublicKey + PrivateKey *rsa.PrivateKey +} + +type ECDSA struct { + PublicKey *ecdsa.PublicKey + PrivateKey *ecdsa.PrivateKey +} + +func (key *RSA) KeyPair() any { + return key +} + +func (key *RSA) Sign(data []byte) ([]byte, error) { + h := crypto.SHA256.New() + h.Write(data) + hashed := h.Sum(nil) + return rsa.SignPKCS1v15(rand.Reader, key.PrivateKey, crypto.SHA256, hashed) +} + +func (key *ECDSA) KeyPair() any { + return key +} + +func (key *ECDSA) Sign(data []byte) ([]byte, error) { + h := crypto.SHA256.New() + h.Write(data) + hashed := h.Sum(nil) + r, s, err := ecdsa.Sign(rand.Reader, key.PrivateKey, hashed) + if err != nil { + return nil, err + } + signature := append(r.Bytes(), s.Bytes()...) + return signature, nil +} + +func ReturnPrivateKey(key types.AsymmetricKey) (any, error) { + switch k := key.KeyPair().(type) { + case *RSA: + return k.PrivateKey, nil + case *ECDSA: + return k.PrivateKey, nil + default: + return nil, fmt.Errorf("unsupported key type") + } +} + +func GetSubordinateCaParameters(service string) (*types.CertificateAuthority, error) { + subordinatePath := filepath.Join(types.SubordinatePath, service+_subordinateCertificate) + subordinate, err := readFileFromSystem(subordinatePath) + if err != nil { + return nil, fmt.Errorf("error reading subordinate ca from %s: %w", subordinatePath, err) + } + + subordinatePem, _ := pem.Decode(*subordinate) + if subordinatePem == nil { + return nil, fmt.Errorf("error decoding subordinate PEM") + } + + subordinateCertificate, err := x509.ParseCertificate(subordinatePem.Bytes) + if err != nil { + return nil, fmt.Errorf("error parsing subordinate certificate: %w", err) + } + + privateKeyPath := filepath.Join(types.SubordinatePath, service+_subordinatePrivateKey) + pk, err := readFileFromSystem(privateKeyPath) + if err != nil { + return nil, fmt.Errorf("error reading subordinate ca key from %s: %w", privateKeyPath, err) + } + + pkPem, _ := pem.Decode(*pk) + if pkPem == nil { + return nil, fmt.Errorf("error decoding private key") + } + + subordinatePrivateKey, err := formatAsymmetricKey(pkPem) + if err != nil { + return nil, fmt.Errorf("error formatting private key: %w", err) + } + + serialNumberPath := filepath.Join(types.SubordinatePath, service+_subordinateSerialNumber) + caSerialNumber, err := readFileFromSystem(serialNumberPath) + if err != nil { + return nil, fmt.Errorf("error reading subordinate ca serial number from %s: %w", serialNumberPath, err) + } + + caArnPath := filepath.Join(types.SubordinatePath, service+_certificateAuthorityArn) + caArn, err := readFileFromSystem(caArnPath) + if err != nil { + return nil, fmt.Errorf("error reading subordinate ca arn from %s: %w", caArnPath, err) + } + + return &types.CertificateAuthority{ + Certificate: subordinateCertificate, + AsymmetricKey: &subordinatePrivateKey, + SerialNumber: string(*caSerialNumber), + CertificateAuthorityArn: string(*caArn), + }, nil +} + +func readFileFromSystem(path string) (*[]byte, error) { + if !strings.HasPrefix(path, types.SubordinatePath) { + return nil, fmt.Errorf("unsafe file input path") + } + + file, err := os.ReadFile(filepath.Clean(path)) + if err != nil { + return nil, err + } + return &file, err +} + +func writeFileToSystem(path string, data []byte) error { + if !strings.HasPrefix(path, types.SubordinatePath) { + return fmt.Errorf("unsafe file input, write private key") + } + + if err := os.WriteFile(path, data, os.ModePerm); err != nil { + return err + } + return nil +} + +func formatAsymmetricKey(block *pem.Block) (types.AsymmetricKey, error) { + switch block.Type { + case "RSA PRIVATE KEY": + rsaKey, err := parseRSAPrivateKey(block.Bytes) + if err != nil { + return nil, err + } + return rsaKey, nil + case "EC PRIVATE KEY": + ecdsaKey, err := parseECDSAPrivateKey(block.Bytes) + if err != nil { + return nil, err + } + return ecdsaKey, nil + default: + return nil, errors.New("unsupported key type") + } +} + +func parseRSAPrivateKey(keyBytes []byte) (*RSA, error) { + key, err := x509.ParsePKCS1PrivateKey(keyBytes) + if err != nil { + return nil, err + } + rsaPrivateKey := &RSA{ + PublicKey: &key.PublicKey, + PrivateKey: key, + } + return rsaPrivateKey, nil +} + +func parseECDSAPrivateKey(keyBytes []byte) (*ECDSA, error) { + key, err := x509.ParseECPrivateKey(keyBytes) + if err != nil { + return nil, err + } + ecdsaPrivateKey := &ECDSA{ + PublicKey: &key.PublicKey, + PrivateKey: key, + } + return ecdsaPrivateKey, nil +} diff --git a/internal/lib/crypto/store.go b/internal/lib/crypto/store.go new file mode 100644 index 0000000..6881a69 --- /dev/null +++ b/internal/lib/crypto/store.go @@ -0,0 +1,133 @@ +package crypto + +import ( + "crypto/x509" + "encoding/pem" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/aws/aws-sdk-go-v2/service/acmpca" + "github.com/coinbase/baseca/internal/types" +) + +func WriteKeyToFile(service string, privateKey types.AsymmetricKey) error { + var pemBlock *pem.Block + directoryPath := filepath.Join(types.SubordinatePath, service) + filePath := filepath.Join(directoryPath, _subordinatePrivateKey) + + if !strings.HasPrefix(filePath, types.SubordinatePath) { + return fmt.Errorf("unsafe file input, write private key") + } + + switch k := privateKey.KeyPair().(type) { + case *RSA: + pkBytes := x509.MarshalPKCS1PrivateKey(k.PrivateKey) + pemBlock = &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: pkBytes, + } + case *ECDSA: + pkBytes, err := x509.MarshalECPrivateKey(k.PrivateKey) + if err != nil { + return err + } + pemBlock = &pem.Block{ + Type: "EC PRIVATE KEY", + Bytes: pkBytes, + } + default: + return fmt.Errorf("private key format not supported") + } + + if err := os.WriteFile(filePath, pem.EncodeToMemory(pemBlock), os.ModePerm); err != nil { + return err + } + + return nil +} + +func WriteSubordinateCaParameters(service string, caCertificate *x509.Certificate, ca types.CertificateParameters, pca *acmpca.GetCertificateAuthorityCertificateOutput) error { + var err error + + directoryPath := filepath.Join(types.SubordinatePath, service) + + // Subordinate CA + filePath := filepath.Join(directoryPath, _subordinateCertificate) + pemBlock := encodeCertificateFromx509(caCertificate) + if err := os.WriteFile(filePath, *pemBlock, os.ModePerm); err != nil { + return err + } + + if !ca.RootCa { + filePath = filepath.Join(directoryPath, _intermediateCertificate) + pemBlock, err = encodeCertificateFromString(pca.Certificate) + if err != nil { + return fmt.Errorf("error encoding intermediate ca") + } + if err := os.WriteFile(filePath, *pemBlock, os.ModePerm); err != nil { + return fmt.Errorf("error writing intermediate ca to filesystem") + } + + filePath = filepath.Join(directoryPath, _rootCertificate) + pemBlock, err = encodeCertificateFromString(pca.CertificateChain) + if err != nil { + return fmt.Errorf("error encoding root ca") + } + if err := os.WriteFile(filePath, *pemBlock, os.ModePerm); err != nil { + return fmt.Errorf("error writing root ca to filesystem") + } + } else { + filePath = filepath.Join(directoryPath, _rootCertificate) + pemBlock, err = encodeCertificateFromString(pca.Certificate) + if err != nil { + return fmt.Errorf("error encoding root ca") + } + if err := os.WriteFile(filePath, *pemBlock, os.ModePerm); err != nil { + return fmt.Errorf("error writing root ca to filesystem") + } + } + + // Certificate Authority Serial Number + ca_serial_number := fmt.Sprintf("%x", caCertificate.SerialNumber) + filePath = filepath.Join(directoryPath, _subordinateSerialNumber) + err = writeFileToSystem(filePath, []byte(ca_serial_number)) + if err != nil { + return fmt.Errorf("error writing serial number to filesystem") + } + + // Intermediate ACM Private CA ARN + filePath = filepath.Join(directoryPath, _certificateAuthorityArn) + err = writeFileToSystem(filePath, []byte(ca.CaArn)) + if err != nil { + return fmt.Errorf("error writing ca arn to filesystem") + } + return nil +} + +func encodeCertificateFromString(certificate *string) (*[]byte, error) { + c := []byte(*certificate) + block, _ := pem.Decode(c) + x509Certificate, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return nil, fmt.Errorf("invalid x509 certificate format") + } + pemBlock := pem.EncodeToMemory( + &pem.Block{ + Type: "CERTIFICATE", + Bytes: x509Certificate.Raw, + }, + ) + return &pemBlock, nil +} + +func encodeCertificateFromx509(certificate *x509.Certificate) *[]byte { + pemBlock := pem.EncodeToMemory( + &pem.Block{ + Type: "CERTIFICATE", + Bytes: certificate.Raw, + }, + ) + return &pemBlock +} diff --git a/internal/lib/util/backoff.go b/internal/lib/util/backoff.go new file mode 100644 index 0000000..c257a5c --- /dev/null +++ b/internal/lib/util/backoff.go @@ -0,0 +1,54 @@ +package util + +import ( + "errors" + "fmt" + "os" + "path/filepath" + "time" + + "github.com/coinbase/baseca/internal/types" +) + +var BackoffSchedule = []time.Duration{ + 1 * time.Second, + 2 * time.Second, + 4 * time.Second, + 8 * time.Second, + 16 * time.Second, +} + +func LockfileBackoff(lockfilePath string) error { + _, err := os.OpenFile(filepath.Clean(lockfilePath), os.O_RDONLY, 0400) + if err == nil { + // Backoff Until Lock File Removed + for _, backoff := range BackoffSchedule { + _, err = os.OpenFile(filepath.Clean(lockfilePath), os.O_RDONLY, 0400) + if errors.Is(err, os.ErrNotExist) { + return nil + } + time.Sleep(backoff) + } + return errors.New("subordinate ca lockfile present") + } + return nil +} + +func GenerateLockfile(service string) error { + // Lock Subordinate CA SSL + lockfilePath := fmt.Sprintf("%s/%s/%s.lock", types.SubordinatePath, service, service) + _, err := os.Create(filepath.Clean(lockfilePath)) + if err != nil { + return fmt.Errorf("error generating lockfile [%s]", service) + } + return nil +} + +func RemoveLockfile(service string) error { + lockfilePath := fmt.Sprintf("%s/%s/%s.lock", types.SubordinatePath, service, service) + err := os.Remove(filepath.Clean(lockfilePath)) + if err != nil { + return fmt.Errorf("error removing lock file [%s]", service) + } + return nil +} diff --git a/internal/lib/util/random.go b/internal/lib/util/random.go new file mode 100644 index 0000000..2f4509c --- /dev/null +++ b/internal/lib/util/random.go @@ -0,0 +1,98 @@ +package util + +import ( + "crypto/rand" + "encoding/base64" + "encoding/hex" + "fmt" + "testing" + "time" + + db "github.com/coinbase/baseca/db/sqlc" + lib "github.com/coinbase/baseca/internal/lib/authentication" + "github.com/google/uuid" +) + +func GenerateTestUser(t *testing.T, permissions string, length int) (db.User, string) { + client_id, _ := uuid.NewRandom() + credentials := generateRandomCredentials(length) + hashed_credentials, _ := lib.HashPassword(credentials) + email := generateRandomEmail() + username := generateRandomUsername() + full_name := generateRandomName() + + return db.User{ + Uuid: client_id, + Username: username, + HashedCredential: hashed_credentials, + FullName: full_name, + Email: email, + Permissions: permissions, + CredentialChangedAt: time.Now().UTC(), + CreatedAt: time.Now().UTC(), + }, credentials +} + +func generateRandomEmail() string { + randBytes := make([]byte, 8) + _, err := rand.Read(randBytes) + if err != nil { + panic(err) + } + + // Encode the random bytes using base64 encoding to get an ASCII string + randStr := base64.URLEncoding.EncodeToString(randBytes) + + // Use the first 10 characters of the base64-encoded string as the email username + return fmt.Sprintf("%s@coinbase.com", randStr[:10]) +} + +func generateRandomName() string { + // Generate random bytes for the first and last name + firstNameBytes := make([]byte, 6) + _, err := rand.Read(firstNameBytes) + if err != nil { + panic(err) + } + lastNameBytes := make([]byte, 6) + _, err = rand.Read(lastNameBytes) + if err != nil { + panic(err) + } + + // Convert the random bytes to hexadecimal strings + firstNameHex := hex.EncodeToString(firstNameBytes)[:10] + lastNameHex := hex.EncodeToString(lastNameBytes)[:10] + + return fmt.Sprintf("%s %s", firstNameHex, lastNameHex) +} + +func generateRandomUsername() string { + // Generate random bytes for the username + usernameBytes := make([]byte, 8) + _, err := rand.Read(usernameBytes) + if err != nil { + panic(err) + } + + // Encode the random bytes using base64 encoding to get an ASCII string + usernameStr := base64.URLEncoding.EncodeToString(usernameBytes) + + // Use the first 10 characters of the base64-encoded string as the username + return usernameStr[:10] +} + +func generateRandomCredentials(length int) string { + // Generate random bytes for the credentials + credentialsBytes := make([]byte, length) + _, err := rand.Read(credentialsBytes) + if err != nil { + panic(err) + } + + // Encode the random bytes using base64 encoding to get an ASCII string + credentialsStr := base64.URLEncoding.EncodeToString(credentialsBytes) + + // Return the first `length` characters of the base64-encoded string + return credentialsStr[:length] +} diff --git a/internal/lib/util/validator/domain.go b/internal/lib/util/validator/domain.go new file mode 100644 index 0000000..e0b93ef --- /dev/null +++ b/internal/lib/util/validator/domain.go @@ -0,0 +1,58 @@ +package validator + +import ( + "net" + "regexp" + "strings" + + "github.com/coinbase/baseca/internal/config" +) + +const ( + _dns_regular_expression = `^[a-zA-Z0-9*._-]+$` +) + +var valid_domains []string +var valid_certificate_authorities []string + +func IsValidDomain(fully_qualified_domain_name string) bool { + + arr := strings.Split(fully_qualified_domain_name, ".") + + if len(arr) < 2 { + return false + } + + domain_slice := arr[len(arr)-2:] + domain := strings.Join(domain_slice, ".") + pattern, _ := regexp.Compile(_dns_regular_expression) + + for _, valid_domain := range valid_domains { + if domain == valid_domain { + // DNS Wildcard Check + if pattern.MatchString(fully_qualified_domain_name) { + return true + } + } + } + + // Fallback Check IP Address for CN/SAN + return net.ParseIP(fully_qualified_domain_name) != nil +} + +func IsSupportedCertificateAuthority(certificate_authority string) bool { + for _, ca := range valid_certificate_authorities { + if ca == certificate_authority { + return true + } + } + return false +} + +func SupportedConfig(cfg *config.Config) { + valid_domains = cfg.Domains + + for certificate_authority := range cfg.ACMPCA { + valid_certificate_authorities = append(valid_certificate_authorities, certificate_authority) + } +} diff --git a/internal/lib/util/validator/domain_test.go b/internal/lib/util/validator/domain_test.go new file mode 100644 index 0000000..1263ebf --- /dev/null +++ b/internal/lib/util/validator/domain_test.go @@ -0,0 +1,67 @@ +package validator + +import ( + "testing" + + "github.com/coinbase/baseca/internal/config" +) + +func TestIsValidateDomain(t *testing.T) { + valid_domains = []string{"coinbase.com"} + + tests := []struct { + name string + domain string + want bool + }{ + {"Valid Domain", "www.coinbase.com", true}, + {"Invalid Domain", "www.invalid.com", false}, + {"Valid IP Address", "192.168.1.1", true}, + {"Invalid String", "coinbase", false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := IsValidDomain(tt.domain); got != tt.want { + t.Errorf("IsValidateDomain() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestIsSupportedCertificateAuthority(t *testing.T) { + // Setup + valid_certificate_authorities = []string{"certificate_authority_1", "certificate_authority_2"} + + if !IsSupportedCertificateAuthority("certificate_authority_1") { + t.Error("Expected certificate_authority_1 to be supported") + } + + if IsSupportedCertificateAuthority("certificate_authority_3") { + t.Error("Expected certificate_authority_3 not to be supported") + } +} + +func TestSupportedConfig(t *testing.T) { + cfg := &config.Config{ + Domains: []string{"domain1.com", "domain2.com"}, + ACMPCA: map[string]config.SubordinateCertificate{ + "certificate_authority_1": { + Region: "us-west-1", + }, + "certificate_authority_2": { + Region: "us-east-1", + }, + }, + } + + SupportedConfig(cfg) + + if !Contains(valid_domains, "domain1.com") { + t.Error("Expected domain1.com to be in valid_domains") + } + + if !Contains(valid_certificate_authorities, "certificate_authority_1") { + t.Error("Expected certificate_authority_1 to be in valid_certificate_authorities") + } +} diff --git a/internal/lib/util/validator/environment.go b/internal/lib/util/validator/environment.go new file mode 100644 index 0000000..8972890 --- /dev/null +++ b/internal/lib/util/validator/environment.go @@ -0,0 +1,32 @@ +package validator + +import ( + "github.com/coinbase/baseca/internal/config" + "github.com/coinbase/baseca/internal/types" +) + +const ( + BaseDirectory = "/tmp/baseca/ssl" +) + +var CertificateAuthorityEnvironments map[string][]string + +func SupportedEnvironments(cfg *config.Config) { + CertificateAuthorityEnvironments = map[string][]string{ + "local": cfg.Environment.Local, + "sandbox": cfg.Environment.Sandbox, + "development": cfg.Environment.Development, + "staging": cfg.Environment.Staging, + "pre_production": cfg.Environment.PreProduction, + "production": cfg.Environment.Production, + "corporate": cfg.Environment.Corporate, + } +} + +func SetBaseDirectory(cfg *config.Config) { + if len(cfg.SubordinateMetadata.BaseDirectory) != 0 { + types.SubordinatePath = cfg.SubordinateMetadata.BaseDirectory + } else { + types.SubordinatePath = BaseDirectory + } +} diff --git a/internal/lib/util/validator/environment_test.go b/internal/lib/util/validator/environment_test.go new file mode 100644 index 0000000..f04f93f --- /dev/null +++ b/internal/lib/util/validator/environment_test.go @@ -0,0 +1,45 @@ +package validator + +import ( + "testing" + + "github.com/coinbase/baseca/internal/config" + "github.com/coinbase/baseca/internal/types" +) + +func TestSupportedEnvironments(t *testing.T) { + cfg := &config.Config{ + Environment: config.Stage{ + Local: []string{"localhost"}, + }, + } + + SupportedEnvironments(cfg) + + if len(CertificateAuthorityEnvironments["local"]) == 0 { + t.Errorf("Expected non-empty local environments, got none") + } +} + +func TestSetBaseDirectory(t *testing.T) { + // When BaseDirectory is provided + cfg := &config.Config{ + SubordinateMetadata: config.SubordinateCertificateAuthority{ + BaseDirectory: "/some/dir", + }, + } + + SetBaseDirectory(cfg) + + if types.SubordinatePath != "/some/dir" { + t.Errorf("Expected SubordinatePath to be set to '/some/dir', got: %s", types.SubordinatePath) + } + + cfg.SubordinateMetadata.BaseDirectory = "" + + SetBaseDirectory(cfg) + + if types.SubordinatePath != BaseDirectory { + t.Errorf("Expected SubordinatePath to be set to BaseDirectory, got: %s", types.SubordinatePath) + } +} diff --git a/internal/lib/util/validator/fx.go b/internal/lib/util/validator/fx.go new file mode 100644 index 0000000..e37829c --- /dev/null +++ b/internal/lib/util/validator/fx.go @@ -0,0 +1,11 @@ +package validator + +import "go.uber.org/fx" + +var Module = fx.Options( + fx.Invoke( + SupportedConfig, + SupportedEnvironments, + SetBaseDirectory, + ), +) diff --git a/internal/lib/util/validator/permission.go b/internal/lib/util/validator/permission.go new file mode 100644 index 0000000..7f6e3bc --- /dev/null +++ b/internal/lib/util/validator/permission.go @@ -0,0 +1,12 @@ +package validator + +import "github.com/coinbase/baseca/internal/types" + +func IsSupportedPermission(permission string) bool { + switch permission { + case types.ADMIN, types.PRIVILEGED, types.READ: + return true + } + + return false +} diff --git a/internal/lib/util/validator/validate.go b/internal/lib/util/validator/validate.go new file mode 100644 index 0000000..1d5d18a --- /dev/null +++ b/internal/lib/util/validator/validate.go @@ -0,0 +1,167 @@ +package validator + +import ( + "database/sql" + "encoding/json" + "fmt" + "regexp" + "testing" + "unicode" + + "github.com/coinbase/baseca/internal/config" + "github.com/sqlc-dev/pqtype" +) + +type NullString sql.NullString + +func ValidateCertificateAuthorityEnvironment(config config.Stage, environment string, certificate_authorities []string) bool { + if len(certificate_authorities) == 0 { + return false + } + + for _, certificate_authority := range certificate_authorities { + if output := Contains(CertificateAuthorityEnvironments[environment], certificate_authority); !output { + return false + } + } + return true +} + +func ValidateSubjectAlternateNames(request_san []string, valid_san []string, regular_expression string) error { + // Convert Subject Alternative Name to Regular Expression + patterns := make([]*regexp.Regexp, 0, len(valid_san)) + for _, subject_alternative_name := range valid_san { + pattern, err := regexp.Compile(subject_alternative_name) + if err != nil { + return fmt.Errorf("regular expression compile error: %s", err) + } + patterns = append(patterns, pattern) + } + + // Compile Custom Regular Expression if Provided + if len(regular_expression) > 0 { + compiled, err := regexp.Compile(regular_expression) + if err != nil { + return fmt.Errorf("regular expression compile error: %s", err) + } + patterns = append(patterns, compiled) + } + + // Check Each Subject Alternative Name Against Regular Expression + for _, subject_alternative_name := range request_san { + valid_pattern := false + for _, pattern := range patterns { + if pattern.MatchString(subject_alternative_name) { + valid_pattern = true + } + } + if !valid_pattern { + return fmt.Errorf("invalid subject alternative name [%s]", subject_alternative_name) + } + } + return nil +} + +func ValidateEmail(email string) bool { + pattern := `^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$` + regex := regexp.MustCompile(pattern) + return regex.MatchString(email) +} + +func NullStringToString(x *sql.NullString) string { + if x.Valid { + return x.String + } + return "" +} + +func MapToNullRawMessage(m map[string]string) (pqtype.NullRawMessage, error) { + jsonBytes, err := json.Marshal(m) + if err != nil { + return pqtype.NullRawMessage{}, err + } + + nullRawMessage := pqtype.NullRawMessage{} + err = nullRawMessage.Scan(jsonBytes) + if err != nil { + return pqtype.NullRawMessage{}, err + } + + return nullRawMessage, nil +} + +func ConvertNullRawMessageToMap(nrm pqtype.NullRawMessage) (map[string]string, error) { + if !nrm.Valid { + return nil, nil + } + + var m map[string]any + err := json.Unmarshal(nrm.RawMessage, &m) + if err != nil { + return nil, err + } + + result := make(map[string]string, len(m)) + for k, v := range m { + if s, ok := v.(string); ok { + result[k] = s + } + } + return result, nil +} + +// Validate if input only contain alphanumeric +func ValidateInput(s string) bool { + for _, c := range s { + if !unicode.IsLetter(c) && !unicode.IsNumber(c) { + return false + } + } + + return true +} + +func Contains(s []string, str string) bool { + for _, v := range s { + if v == str { + return true + } + } + return false +} + +func SanitizeInput(input []string) []string { + allKeys := make(map[string]bool) + list := []string{} + for _, item := range input { + if _, value := allKeys[item]; !value { + allKeys[item] = true + list = append(list, item) + } + } + return list +} + +func TestValidateInput(t *testing.T) { + tests := []struct { + input string + expected bool + }{ + {"HelloWorld", true}, // Only Letters + {"123456", true}, // Only Numbers + {"Hello123", true}, // Mix of Letters and Numbers + {"Hello World!", false}, // Contains a Space and Exclamation Mark + {"", true}, // Empty String + {"Hello@World", false}, // Contains Special Character + {"123#456", false}, // Contains Special Character + } + + for _, tt := range tests { + t.Run(tt.input, func(t *testing.T) { + result := ValidateInput(tt.input) + if result != tt.expected { + t.Errorf("got %v, want %v", result, tt.expected) + } + }) + } +} diff --git a/internal/lib/util/validator/validate_test.go b/internal/lib/util/validator/validate_test.go new file mode 100644 index 0000000..7c744de --- /dev/null +++ b/internal/lib/util/validator/validate_test.go @@ -0,0 +1,87 @@ +package validator + +import ( + "reflect" + "testing" + + "github.com/stretchr/testify/assert" +) + +type TestValidate struct { + RegularExpression string + ValidSAN []string + RequestSAN []string +} + +func Test_ValidateSubjectAlternateNames_Regex_Error(t *testing.T) { + test_validator := []TestValidate{ + { + RegularExpression: `^[a-zA-Z]+((!coinbase.com)\w)*$`, + ValidSAN: []string{}, + RequestSAN: []string{"example.com"}, + }, + { + RegularExpression: `^[a-zA-Z]+((!coinbase.com)\w)*$`, + ValidSAN: []string{"*.coinbase.com"}, + RequestSAN: []string{"12345"}, + }, + } + + for _, elem := range test_validator { + err := ValidateSubjectAlternateNames(elem.RequestSAN, elem.ValidSAN, elem.RegularExpression) + assert.Error(t, err) + } +} + +func Test_ValidateSubjectAlternateNames_Success(t *testing.T) { + test_validator := []TestValidate{ + { + RegularExpression: `^[a-zA-Z]+((!coinbase.com)\w)*$`, + ValidSAN: []string{}, + RequestSAN: []string{"baseca"}, + }, + } + + for _, elem := range test_validator { + err := ValidateSubjectAlternateNames(elem.RequestSAN, elem.ValidSAN, elem.RegularExpression) + assert.NoError(t, err) + } +} + +func Test_ValidateSubjectAlternateNames_ValidSAN_Regex_Success(t *testing.T) { + test_validator := []TestValidate{ + { + RegularExpression: `^[a-zA-Z]+((!coinbase.com)\w)*$`, + ValidSAN: []string{"10.0.0.1"}, + RequestSAN: []string{"10.0.0.1"}, + }, + } + + for _, elem := range test_validator { + err := ValidateSubjectAlternateNames(elem.RequestSAN, elem.ValidSAN, elem.RegularExpression) + assert.NoError(t, err) + } +} + +func Test_Contains(t *testing.T) { + s := []string{"a", "b", "c"} + + if !Contains(s, "a") { + t.Error("Expected slice to contain 'a'") + } + + if Contains(s, "d") { + t.Error("Did not expect slice to contain 'd'") + } +} + +func Test_SanitizeInput(t *testing.T) { + input := []string{"a", "b", "a", "c", "c"} + expected := []string{"a", "b", "c"} + + result := SanitizeInput(input) + + if !reflect.DeepEqual(result, expected) { + t.Errorf("Expected %v, got %v", expected, result) + } +} diff --git a/internal/logger/logger.go b/internal/logger/logger.go new file mode 100644 index 0000000..4faca0d --- /dev/null +++ b/internal/logger/logger.go @@ -0,0 +1,208 @@ +package logger + +import ( + "context" + "time" + + "github.com/gogo/status" + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/peer" + + "github.com/coinbase/baseca/internal/types" +) + +type Extractor func(resp any, err error, code codes.Code) string + +type Error struct { + UserError error + InternalError error +} + +func (e *Error) Error() string { + return e.UserError.Error() +} + +func RpcError(user, internal error) *Error { + return &Error{ + UserError: user, + InternalError: internal, + } +} + +func RpcLogger(extractor Extractor) grpc.UnaryServerInterceptor { + return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp any, err error) { + currentTime := time.Now().UTC() + result, err := handler(ctx, req) + duration := time.Since(currentTime) + + var event *zerolog.Event + if err != nil { + event = log.Error().Err(err) + } else { + event = log.Info() + } + + statusCode := extractStatusCode(err) + clientIP := extractClientIP(ctx) + + event.Str("protocol", "grpc"). + Str("method", info.FullMethod). + Int("status_code", int(statusCode)). + Str("ip_address", clientIP). + Dur("duration", duration) + + provisioner, ok := ctx.Value(types.ProvisionerAuthenticationContextKey).(*types.ProvisionerAccountPayload) + if ok { + event.Str("provisioner_account_uuid", provisioner.ClientId.String()) + } + + service, ok := ctx.Value(types.ServiceAuthenticationContextKey).(*types.ServiceAccountPayload) + if ok { + event.Str("service_account_uuid", service.ServiceID.String()) + } + + event.Msg(extractor(result, err, statusCode)) + return result, err + } +} + +func extractClientIP(ctx context.Context) string { + if p, ok := peer.FromContext(ctx); ok { + return p.Addr.String() + } + return "" +} + +func extractStatusCode(err error) codes.Code { + if st, ok := status.FromError(err); ok { + return st.Code() + } else if customErr, ok := err.(*Error); ok { + return status.Code(customErr.UserError) + } + return codes.Unknown +} + +type Logger interface { + AddFields(fields ...zap.Field) + + Panic(msg string, fields ...zap.Field) + Fatal(msg string, fields ...zap.Field) + Error(msg string, fields ...zap.Field) + Warn(msg string, fields ...zap.Field) + Info(msg string, fields ...zap.Field) + Debug(msg string, fields ...zap.Field) +} + +var DefaultLogger = NewLogger( + zap.NewExample().WithOptions(zap.Development()).With(zap.Bool("default", true)), +) + +func NewLogger(logger *zap.Logger, logFields ...zap.Field) *ContextLogger { + logger = logger.WithOptions( + zap.AddCallerSkip(1), + zap.AddCaller(), + zap.Fields(append(logFields, + zap.String("logger", "observability.ContextLogger"), + )..., + ), + ) + return NewContextLogger(logWrapper{logger}) +} + +type logKey struct{} + +type ContextLogger struct { + Logger Logger + Fields []zap.Field +} + +func NewContextLogger(logger Logger, fields ...zap.Field) *ContextLogger { + if ctxLogger, ok := logger.(*ContextLogger); ok { + return NewContextLogger(ctxLogger.Logger, append(fields, ctxLogger.Fields...)...) + } + return &ContextLogger{ + Logger: logger, + Fields: fields, + } +} + +func WithLogger(ctx context.Context, logger Logger) context.Context { + return context.WithValue(ctx, logKey{}, NewContextLogger(logger)) +} + +func Log(ctx context.Context) Logger { + if l := extractLogger(ctx); l != nil { + return l + } + l := NewContextLogger(DefaultLogger) + l.AddFields(zap.Any("context", ctx)) + l.Warn("Log called but context not set up.") + return l +} + +func AddLogFields(ctx context.Context, fields ...zap.Field) { + if logger := extractLogger(ctx); logger != nil { + logger.AddFields(fields...) + } else { + Log(ctx).Warn("AddLogFields called but context not set up.", fields...) + } +} + +func extractLogger(ctx context.Context) Logger { + logger := ctx.Value(logKey{}) + if logger == nil { + return nil + } + return logger.(Logger) +} + +func (ctxLogger *ContextLogger) AddFields(fields ...zap.Field) { + ctxLogger.Fields = append(ctxLogger.Fields, fields...) +} + +func (ctxLogger *ContextLogger) fields(fields []zap.Field) []zap.Field { + return append(fields, ctxLogger.Fields...) +} + +func (ctxLogger *ContextLogger) stackFields(fields []zap.Field) []zap.Field { + return ctxLogger.fields(fields) +} + +func (ctxLogger *ContextLogger) Panic(msg string, fields ...zap.Field) { + ctxLogger.Logger.Panic(msg, ctxLogger.stackFields(fields)...) +} +func (ctxLogger *ContextLogger) Fatal(msg string, fields ...zap.Field) { + ctxLogger.Logger.Fatal(msg, ctxLogger.stackFields(fields)...) +} +func (ctxLogger *ContextLogger) Error(msg string, fields ...zap.Field) { + ctxLogger.Logger.Error(msg, ctxLogger.stackFields(fields)...) +} +func (ctxLogger *ContextLogger) Warn(msg string, fields ...zap.Field) { + ctxLogger.Logger.Warn(msg, ctxLogger.fields(fields)...) +} +func (ctxLogger *ContextLogger) Info(msg string, fields ...zap.Field) { + ctxLogger.Logger.Info(msg, ctxLogger.fields(fields)...) +} +func (ctxLogger *ContextLogger) Debug(msg string, fields ...zap.Field) { + ctxLogger.Logger.Debug(msg, ctxLogger.fields(fields)...) +} + +// logWrapper adds a nop AddFields to a zap Logger client to implement the Logger interface +type logWrapper struct { + *zap.Logger +} + +func (logWrapper) AddFields(_ ...zap.Field) {} + +func AppendField(zapFields []zapcore.Field, key string, value string) []zapcore.Field { + return append(zapFields, zap.String(key, value)) +} + +func AppendZapField(zapFields []zapcore.Field, field zapcore.Field) []zapcore.Field { + return append(zapFields, field) +} diff --git a/internal/types/account.go b/internal/types/account.go new file mode 100644 index 0000000..57a31c8 --- /dev/null +++ b/internal/types/account.go @@ -0,0 +1,27 @@ +package types + +import ( + "github.com/google/uuid" +) + +type ServiceAccountPayload struct { + ServiceID uuid.UUID `json:"service_id"` + ServiceAccount string `json:"service_account"` + Environment string `json:"environment"` + ValidSubjectAlternateName []string `json:"subject_alternate_name"` + ValidCertificateAuthorities []string `json:"certificate_authorities"` + CertificateValidity int16 `json:"certificate_validity"` + SubordinateCa string `json:"subordinate_ca"` + ExtendedKey string `json:"certificate_request_extension"` + SANRegularExpression string `json:"regular_expression"` +} + +type ProvisionerAccountPayload struct { + ClientId uuid.UUID `json:"client_id"` + ProvisionerAccount string `json:"provisioner_account"` + Environments []string `json:"environments"` + ValidSubjectAlternateNames []string `json:"subject_alternate_names"` + MaxCertificateValidity uint32 `json:"max_certificate_validity"` + ExtendedKeys []string `json:"certificate_request_extension"` + RegularExpression string `json:"regular_expression"` +} diff --git a/internal/types/attestation.go b/internal/types/attestation.go new file mode 100644 index 0000000..93b9cc1 --- /dev/null +++ b/internal/types/attestation.go @@ -0,0 +1,23 @@ +package types + +type NodeAttestation struct { + AWSInstanceIdentityDocument AWSInstanceIdentityDocument `json:"aws_iid"` +} + +type AWSInstanceIdentityDocument struct { + RoleArn string `json:"instance_profile_arn,omitempty"` + AssumeRole string `json:"assume_role,omitempty"` + SecurityGroups []string `json:"security_groups,omitempty"` + Region string `json:"region,omitempty"` + InstanceID string `json:"instance_id,omitempty"` + ImageID string `json:"image_id,omitempty"` + InstanceTags map[string]string `json:"instance_tags,omitempty"` +} + +type Node struct { + AWS_IID string +} + +var Attestation = Node{ + AWS_IID: "AWS_IID", +} diff --git a/internal/types/certificate.go b/internal/types/certificate.go new file mode 100644 index 0000000..64431d3 --- /dev/null +++ b/internal/types/certificate.go @@ -0,0 +1,202 @@ +package types + +import ( + "bytes" + "crypto/elliptic" + "crypto/x509" + "encoding/pem" + "time" + + "github.com/aws/aws-sdk-go-v2/service/acmpca/types" +) + +var SubordinatePath string + +type CertificateParameters struct { + Region string + CaArn string + AssumeRole bool + RoleArn string + Validity int + RootCa bool +} + +type Extensions struct { + KeyUsage x509.KeyUsage + ExtendedKeyUsage []x509.ExtKeyUsage + TemplateArn string +} + +type Algorithm struct { + Algorithm x509.PublicKeyAlgorithm + KeySize map[int]any + Signature map[string]bool + SigningAlgorithm map[x509.SignatureAlgorithm]bool +} + +type SignatureAlgorithm struct { + Common x509.SignatureAlgorithm + PCA types.SigningAlgorithm +} + +type SigningRequest struct { + CSR *bytes.Buffer + PrivateKey *pem.Block +} + +type SignedCertificate struct { + CertificatePath string + IntermediateCertificateChainPath string + RootCertificateChainPath string +} + +type CertificateMetadata struct { + SerialNumber string + CommonName string + SubjectAlternativeName []string + ExpirationDate time.Time + IssuedDate time.Time + CaSerialNumber string + CertificateAuthorityArn string + Revoked bool + RevokedBy string + RevokeDate time.Time +} + +type CertificateRequest struct { + CommonName string + SubjectAlternateNames []string + DistinguishedName DistinguishedName + SigningAlgorithm x509.SignatureAlgorithm + PublicKeyAlgorithm x509.PublicKeyAlgorithm + KeySize int + Output Output +} + +type Output struct { + CertificateSigningRequest string + Certificate string + CertificateChain string + PrivateKey string +} + +type DistinguishedName struct { + Country []string + Province []string + Locality []string + Organization []string + OrganizationalUnit []string +} + +type EC2InstanceMetadata struct { + InstanceIdentityDocument []byte `json:"instance_identity_document"` + InstanceIdentitySignature []byte `json:"instance_identity_signature"` +} + +type CertificateAuthority struct { + Certificate *x509.Certificate + AsymmetricKey *AsymmetricKey + SerialNumber string + CertificateAuthorityArn string +} + +type AsymmetricKey interface { + KeyPair() any + Sign(data []byte) ([]byte, error) +} + +var ValidSignatures = map[string]SignatureAlgorithm{ + "SHA256WITHECDSA": { + Common: x509.ECDSAWithSHA256, + PCA: types.SigningAlgorithmSha256withecdsa, + }, + "SHA384WITHECDSA": { + Common: x509.ECDSAWithSHA384, + PCA: types.SigningAlgorithmSha384withecdsa, + }, + "SHA512WITHECDSA": { + Common: x509.ECDSAWithSHA512, + PCA: types.SigningAlgorithmSha512withecdsa, + }, + "SHA256WITHRSA": { + Common: x509.SHA256WithRSA, + PCA: types.SigningAlgorithmSha256withrsa, + }, + "SHA384WITHRSA": { + Common: x509.SHA384WithRSA, + PCA: types.SigningAlgorithmSha384withrsa, + }, + "SHA512WITHRSA": { + Common: x509.SHA512WithRSA, + PCA: types.SigningAlgorithmSha512withrsa, + }, + // TODO: Support Probabilistic Element to the Signature Scheme [SHA256WithRSAPSS] +} + +var PublicKeyAlgorithms = map[string]Algorithm{ + "RSA": { + Algorithm: x509.RSA, + KeySize: map[int]any{ + 2048: true, + 4096: true, + }, + Signature: map[string]bool{ + "SHA256WITHRSA": true, + "SHA384WITHRSA": true, + "SHA512WITHRSA": true, + }, + SigningAlgorithm: map[x509.SignatureAlgorithm]bool{ + x509.SHA256WithRSA: true, + x509.SHA384WithRSA: true, + x509.SHA512WithRSA: true, + }, + }, + "ECDSA": { + Algorithm: x509.ECDSA, + KeySize: map[int]any{ + 256: elliptic.P256(), + 384: elliptic.P384(), + 521: elliptic.P521(), + }, + Signature: map[string]bool{ + "SHA256WITHECDSA": true, + "SHA384WITHECDSA": true, + "SHA512WITHECDSA": true, + }, + SigningAlgorithm: map[x509.SignatureAlgorithm]bool{ + x509.ECDSAWithSHA256: true, + x509.ECDSAWithSHA384: true, + x509.ECDSAWithSHA512: true, + }, + }, + // TODO: Support Ed25519 + "Ed25519": { + Algorithm: x509.Ed25519, + KeySize: map[int]any{ + 256: true, + }, + }, +} + +var CertificateRequestExtension = map[string]Extensions{ + "EndEntityClientAuthCertificate": { + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, + ExtendedKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, + TemplateArn: "arn:aws:acm-pca:::template/EndEntityClientAuthCertificate/V1", + }, + "EndEntityServerAuthCertificate": { + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, + ExtendedKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + TemplateArn: "arn:aws:acm-pca:::template/EndEntityServerAuthCertificate/V1", + }, + "CodeSigningCertificate": { + KeyUsage: x509.KeyUsageDigitalSignature, + ExtendedKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageCodeSigning}, + TemplateArn: "arn:aws:acm-pca:::template/CodeSigningCertificate/V1", + }, +} + +var ValidNodeAttestation = map[string]bool{ + "None": false, + "AWS": true, +} diff --git a/internal/types/constants.go b/internal/types/constants.go new file mode 100644 index 0000000..3372b65 --- /dev/null +++ b/internal/types/constants.go @@ -0,0 +1,16 @@ +package types + +type ContextKey int + +const ( + // Context Metadata + ServiceAuthenticationContextKey ContextKey = iota + ProvisionerAuthenticationContextKey ContextKey = iota + UserAuthenticationContextKey ContextKey = iota + EnrollmentAuthenticationContextKey ContextKey = iota + + // User Permissions + ADMIN = "ADMIN" + PRIVILEGED = "PRIVILEGED" + READ = "READ" +) diff --git a/internal/types/enrollment.go b/internal/types/enrollment.go new file mode 100644 index 0000000..6d1eec2 --- /dev/null +++ b/internal/types/enrollment.go @@ -0,0 +1,14 @@ +package types + +type DeviceEnrollmentRequest struct { + SerialNumber string `json:"serial_number" binding:"required"` + Environment string `json:"environment" binding:"required,ca_environment"` +} + +type DeviceEnrollmentResponse struct { + SerialNumber string `json:"serial_number"` + Credentials string `json:"credentials"` +} + +type EndpointCertificateIssueRequest struct { +} diff --git a/internal/v1/accounts/accounts.go b/internal/v1/accounts/accounts.go new file mode 100644 index 0000000..18ee07f --- /dev/null +++ b/internal/v1/accounts/accounts.go @@ -0,0 +1,22 @@ +package accounts + +import ( + db "github.com/coinbase/baseca/db/sqlc" + apiv1 "github.com/coinbase/baseca/gen/go/baseca/v1" + "github.com/coinbase/baseca/internal/config" +) + +type Service struct { + apiv1.ServiceServer + store db.DatabaseEndpoints + acmConfig map[string]config.SubordinateCertificate + environment config.Stage +} + +func New(cfg *config.Config, endpoints db.DatabaseEndpoints) *Service { + return &Service{ + store: endpoints, + acmConfig: cfg.ACMPCA, + environment: cfg.Environment, + } +} diff --git a/internal/v1/accounts/accounts_test.go b/internal/v1/accounts/accounts_test.go new file mode 100644 index 0000000..01b0f44 --- /dev/null +++ b/internal/v1/accounts/accounts_test.go @@ -0,0 +1,25 @@ +package accounts + +import ( + mock_store "github.com/coinbase/baseca/db/mock" + db "github.com/coinbase/baseca/db/sqlc" + "github.com/coinbase/baseca/internal/lib/util/validator" + "github.com/coinbase/baseca/test" +) + +func buildAccountsConfig(store *mock_store.MockStore) (*Service, error) { + config, err := test.GetTestConfigurationPath() + if err != nil { + return nil, err + } + + endpoints := db.DatabaseEndpoints{Writer: store, Reader: store} + validator.SupportedConfig(config) + validator.SupportedEnvironments(config) + + return &Service{ + store: endpoints, + acmConfig: config.ACMPCA, + environment: config.Environment, + }, nil +} diff --git a/internal/v1/accounts/operation_test.go b/internal/v1/accounts/operation_test.go new file mode 100644 index 0000000..8e21461 --- /dev/null +++ b/internal/v1/accounts/operation_test.go @@ -0,0 +1,150 @@ +package accounts + +import ( + "context" + "fmt" + "testing" + + "github.com/coinbase/baseca/db/mock" + apiv1 "github.com/coinbase/baseca/gen/go/baseca/v1" + "github.com/google/uuid" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" + "google.golang.org/protobuf/types/known/emptypb" +) + +func TestDeleteServiceAccount(t *testing.T) { + service_account_id := "030984ac-e8b3-4f6e-83b2-03ecc81c0477" + client_id, err := uuid.Parse(service_account_id) + require.NoError(t, err) + + cases := []struct { + name string + req *apiv1.AccountId + build func(store *mock.MockStore) + check func(t *testing.T, res *emptypb.Empty, err error) + }{ + { + name: "OK", + req: &apiv1.AccountId{ + Uuid: service_account_id, + }, + build: func(store *mock.MockStore) { + store.EXPECT().TxDeleteServiceAccount(gomock.Any(), client_id).Times(1).Return(nil) + }, + check: func(t *testing.T, res *emptypb.Empty, err error) { + require.NoError(t, err) + }, + }, + { + name: "INVALID_UUID", + req: &apiv1.AccountId{ + Uuid: "random_string", + }, + build: func(store *mock.MockStore) {}, + check: func(t *testing.T, res *emptypb.Empty, err error) { + require.Error(t, err) + require.EqualError(t, err, "rpc error: code = InvalidArgument desc = invalid uuid parameter") + }, + }, + { + name: "DB_ERROR", + req: &apiv1.AccountId{ + Uuid: service_account_id, + }, + build: func(store *mock.MockStore) { + store.EXPECT().TxDeleteServiceAccount(gomock.Any(), client_id).Times(1).Return(fmt.Errorf("internal server error")) + }, + check: func(t *testing.T, res *emptypb.Empty, err error) { + require.Error(t, err) + require.EqualError(t, err, "rpc error: code = Internal desc = internal server error") + }, + }, + } + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + store := mock.NewMockStore(ctrl) + + for elem := range cases { + tc := cases[elem] + + t.Run(tc.name, func(t *testing.T) { + tc.build(store) + + c, err := buildAccountsConfig(store) + require.NoError(t, err) + + res, err := c.DeleteServiceAccount(context.Background(), tc.req) + tc.check(t, res, err) + }) + } +} + +func TestDeleteProvisionerAccount(t *testing.T) { + provisioner_account_id := "030984ac-e8b3-4f6e-83b2-03ecc81c0477" + client_id, _ := uuid.Parse(provisioner_account_id) + + cases := []struct { + name string + req *apiv1.AccountId + build func(store *mock.MockStore) + check func(t *testing.T, res *emptypb.Empty, err error) + }{ + { + name: "OK", + req: &apiv1.AccountId{ + Uuid: provisioner_account_id, + }, + build: func(store *mock.MockStore) { + store.EXPECT().TxDeleteProvisionerAccount(gomock.Any(), client_id).Times(1).Return(nil) + }, + check: func(t *testing.T, res *emptypb.Empty, err error) { + require.NoError(t, err) + }, + }, + { + name: "INVALID_UUID", + req: &apiv1.AccountId{ + Uuid: "random_string", + }, + build: func(store *mock.MockStore) {}, + check: func(t *testing.T, res *emptypb.Empty, err error) { + require.Error(t, err) + require.EqualError(t, err, "rpc error: code = InvalidArgument desc = invalid uuid parameter") + }, + }, + { + name: "DB_ERROR", + req: &apiv1.AccountId{ + Uuid: provisioner_account_id, + }, + build: func(store *mock.MockStore) { + store.EXPECT().TxDeleteProvisionerAccount(gomock.Any(), client_id).Times(1).Return(fmt.Errorf("internal server error")) + }, + check: func(t *testing.T, res *emptypb.Empty, err error) { + require.Error(t, err) + require.EqualError(t, err, "rpc error: code = Internal desc = internal server error") + }, + }, + } + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + store := mock.NewMockStore(ctrl) + + for elem := range cases { + tc := cases[elem] + + t.Run(tc.name, func(t *testing.T) { + tc.build(store) + + c, err := buildAccountsConfig(store) + require.NoError(t, err) + + res, err := c.DeleteProvisionerAccount(context.Background(), tc.req) + tc.check(t, res, err) + }) + } +} diff --git a/internal/v1/accounts/operations.go b/internal/v1/accounts/operations.go new file mode 100644 index 0000000..7c85906 --- /dev/null +++ b/internal/v1/accounts/operations.go @@ -0,0 +1,42 @@ +package accounts + +import ( + "context" + "fmt" + + apiv1 "github.com/coinbase/baseca/gen/go/baseca/v1" + "github.com/coinbase/baseca/internal/logger" + "github.com/gogo/status" + "github.com/google/uuid" + "google.golang.org/grpc/codes" + "google.golang.org/protobuf/types/known/emptypb" +) + +func (s *Service) DeleteServiceAccount(ctx context.Context, req *apiv1.AccountId) (*emptypb.Empty, error) { + client_id, err := uuid.Parse(req.Uuid) + if err != nil { + return &emptypb.Empty{}, logger.RpcError(status.Error(codes.InvalidArgument, "invalid uuid parameter"), fmt.Errorf("[DeleteServiceAccount] invalid UUID %s", req.Uuid)) + } + + err = s.store.Writer.TxDeleteServiceAccount(ctx, client_id) + if err != nil { + return &emptypb.Empty{}, logger.RpcError(status.Error(codes.Internal, "internal server error"), fmt.Errorf("[DeleteServiceAccount] deletion transaction failed %s", err)) + } + + return &emptypb.Empty{}, nil +} + +func (s *Service) DeleteProvisionerAccount(ctx context.Context, req *apiv1.AccountId) (*emptypb.Empty, error) { + client_id, err := uuid.Parse(req.Uuid) + if err != nil { + return &emptypb.Empty{}, logger.RpcError(status.Error(codes.InvalidArgument, "invalid uuid parameter"), fmt.Errorf("[DeleteProvisionerAccount] invalid UUID %s", req.Uuid)) + } + + err = s.store.Writer.TxDeleteProvisionerAccount(ctx, client_id) + + if err != nil { + return &emptypb.Empty{}, logger.RpcError(status.Error(codes.Internal, "internal server error"), fmt.Errorf("[DeleteProvisionerAccount] deletion transaction failed %s", err)) + } + + return &emptypb.Empty{}, nil +} diff --git a/internal/v1/accounts/provision.go b/internal/v1/accounts/provision.go new file mode 100644 index 0000000..fa09d21 --- /dev/null +++ b/internal/v1/accounts/provision.go @@ -0,0 +1,451 @@ +package accounts + +import ( + "context" + "database/sql" + "fmt" + "regexp" + "time" + + db "github.com/coinbase/baseca/db/sqlc" + apiv1 "github.com/coinbase/baseca/gen/go/baseca/v1" + "github.com/coinbase/baseca/internal/attestation/aws_iid" + lib "github.com/coinbase/baseca/internal/lib/authentication" + "github.com/coinbase/baseca/internal/lib/util/validator" + "github.com/coinbase/baseca/internal/logger" + "github.com/coinbase/baseca/internal/types" + "github.com/gogo/status" + "github.com/google/uuid" + "google.golang.org/grpc/codes" + "google.golang.org/protobuf/types/known/emptypb" + "google.golang.org/protobuf/types/known/timestamppb" +) + +var ( + _production = "production" +) + +func (s *Service) CreateProvisionerAccount(ctx context.Context, req *apiv1.CreateProvisionerAccountRequest) (*apiv1.CreateProvisionerAccountResponse, error) { + var service *db.Provisioner + nodeAttestation := []string{} + + subject_alternative_names := validator.SanitizeInput(req.SubjectAlternativeNames) + + err := s.validateSanInputProvisionerAccount(ctx, req.ProvisionerAccount, req.Environments, req.SubjectAlternativeNames, req.RegularExpression) + if err != nil { + return nil, logger.RpcError(status.Error(codes.InvalidArgument, "provisioner subject alternative name (san) validation error"), fmt.Errorf("provisioner account subject alternative name validation error [%s]", err)) + } + + if req.MaxCertificateValidity <= 0 { + return nil, logger.RpcError(status.Error(codes.InvalidArgument, "invalid max_certificate_validity parameter"), fmt.Errorf("invalid max_certificate_validity parameter [%d]", req.MaxCertificateValidity)) + } + + if len(req.Environments) == 0 { + return nil, logger.RpcError(status.Error(codes.InvalidArgument, "invalid environments parameter"), fmt.Errorf("invalid environments parameter")) + } + + for _, environment := range req.Environments { + if _, ok := validator.CertificateAuthorityEnvironments[environment]; !ok { + return nil, logger.RpcError(status.Error(codes.InvalidArgument, "invalid certificate authority environment"), fmt.Errorf("invalid certificate authority environment [%s]", environment)) + } + } + + if len(req.ExtendedKeys) == 0 { + return nil, logger.RpcError(status.Error(codes.InvalidArgument, "invalid extended keys parameter"), fmt.Errorf("invalid extended keys parameter")) + } + + for _, extendedKey := range req.ExtendedKeys { + if _, ok := types.CertificateRequestExtension[extendedKey]; !ok { + return nil, logger.RpcError(status.Error(codes.InvalidArgument, "invalid extended key"), fmt.Errorf("invalid key extension [%s]", extendedKey)) + } + } + + if ok := validator.ValidateEmail(req.Email); !ok { + return nil, logger.RpcError(status.Error(codes.InvalidArgument, "invalid email"), fmt.Errorf("invalid email [%s]", req.Email)) + } + + if len(req.Team) == 0 { + return nil, logger.RpcError(status.Error(codes.InvalidArgument, "invalid team parameter"), fmt.Errorf("invalid team [%s]", req.Team)) + } + + client_id, err := uuid.NewRandom() + if err != nil { + return nil, logger.RpcError(status.Error(codes.Internal, "internal server error"), err) + } + + clientToken, err := lib.GenerateClientToken(32) + if err != nil { + return nil, logger.RpcError(status.Error(codes.Internal, "internal server error"), err) + } + + hashedClientToken, err := lib.HashPassword(clientToken) + if err != nil { + return nil, logger.RpcError(status.Error(codes.Internal, "internal server error"), err) + } + + payload, ok := ctx.Value(types.UserAuthenticationContextKey).(*lib.Claims) + if !ok { + return nil, status.Error(codes.InvalidArgument, "service auth context missing") + } + + if validator.Contains(req.Environments, _production) || req.NodeAttestation != nil { + if err = validateNodeAttestation(req.NodeAttestation); err != nil { + return nil, logger.RpcError(status.Error(codes.InvalidArgument, err.Error()), err) + } + } + + if req.NodeAttestation != nil { + nodeAttestation = aws_iid.GetNodeAttestation(req.NodeAttestation) + + account_arg := db.CreateProvisionerAccountParams{ + ClientID: client_id, + ApiToken: hashedClientToken, + ProvisionerAccount: req.ProvisionerAccount, + Environments: req.Environments, + NodeAttestation: nodeAttestation, + Team: req.Team, + Email: req.Email, + ValidSubjectAlternateNames: subject_alternative_names, + ExtendedKeys: req.ExtendedKeys, + MaxCertificateValidity: int16(req.MaxCertificateValidity), + CreatedBy: payload.Subject, + CreatedAt: time.Now().UTC(), + } + + if len(req.RegularExpression) != 0 { + account_arg.RegularExpression = sql.NullString{String: req.RegularExpression, Valid: len(req.RegularExpression) != 0} + } + + raw_message, err := validator.MapToNullRawMessage(req.NodeAttestation.AwsIid.InstanceTags) + if err != nil { + return nil, logger.RpcError(status.Error(codes.Internal, "internal server error"), err) + } + + iid_arg := db.StoreInstanceIdentityDocumentParams{ + ClientID: client_id, + RoleArn: sql.NullString{String: req.NodeAttestation.AwsIid.RoleArn, Valid: len(req.NodeAttestation.AwsIid.RoleArn) != 0}, + AssumeRole: sql.NullString{String: req.NodeAttestation.AwsIid.AssumeRole, Valid: len(req.NodeAttestation.AwsIid.AssumeRole) != 0}, + Region: sql.NullString{String: req.NodeAttestation.AwsIid.Region, Valid: len(req.NodeAttestation.AwsIid.Region) != 0}, + InstanceID: sql.NullString{String: req.NodeAttestation.AwsIid.InstanceId, Valid: len(req.NodeAttestation.AwsIid.InstanceId) != 0}, + ImageID: sql.NullString{String: req.NodeAttestation.AwsIid.ImageId, Valid: len(req.NodeAttestation.AwsIid.ImageId) != 0}, + SecurityGroupID: req.NodeAttestation.AwsIid.SecurityGroups, + InstanceTags: raw_message, + } + + service, err = s.store.Writer.TxCreateProvisionerAccount(ctx, account_arg, iid_arg) + if err != nil { + return nil, logger.RpcError(status.Error(codes.Internal, "error creating account"), err) + } + } else { + account_arg := db.CreateProvisionerAccountParams{ + ClientID: client_id, + ApiToken: hashedClientToken, + ProvisionerAccount: req.ProvisionerAccount, + Environments: req.Environments, + NodeAttestation: nodeAttestation, + Team: req.Team, + Email: req.Email, + ValidSubjectAlternateNames: subject_alternative_names, + ExtendedKeys: req.ExtendedKeys, + MaxCertificateValidity: int16(req.MaxCertificateValidity), + CreatedBy: payload.Subject, + CreatedAt: time.Now().UTC(), + } + + if len(req.RegularExpression) != 0 { + account_arg.RegularExpression = sql.NullString{String: req.RegularExpression, Valid: len(req.RegularExpression) != 0} + } + + service, err = s.store.Writer.CreateProvisionerAccount(ctx, account_arg) + if err != nil { + return nil, logger.RpcError(status.Error(codes.Internal, "error creating account"), err) + } + } + + account := apiv1.CreateProvisionerAccountResponse{ + ClientId: service.ClientID.String(), + ClientToken: clientToken, + ProvisionerAccount: service.ProvisionerAccount, + Environments: service.Environments, + NodeAttestation: req.NodeAttestation, + SubjectAlternativeNames: service.ValidSubjectAlternateNames, + ExtendedKeys: service.ExtendedKeys, + MaxCertificateValidity: uint32(service.MaxCertificateValidity), + Team: service.Team, + Email: service.Email, + CreatedAt: timestamppb.New(service.CreatedAt), + CreatedBy: service.CreatedBy.String(), + } + + if service.RegularExpression.Valid { + account.RegularExpression = service.RegularExpression.String + } + + return &account, nil +} + +func (s *Service) DeleteProvisionAccount(ctx context.Context, req *apiv1.AccountId) (*emptypb.Empty, error) { + client_id, err := uuid.Parse(req.Uuid) + if err != nil { + return &emptypb.Empty{}, logger.RpcError(status.Error(codes.InvalidArgument, "invalid uuid format"), err) + } + + err = s.store.Writer.TxDeleteProvisionerAccount(ctx, client_id) + if err != nil { + return &emptypb.Empty{}, logger.RpcError(status.Error(codes.Internal, "internal server error"), err) + } + + return &emptypb.Empty{}, nil +} + +func (s *Service) ProvisionServiceAccount(ctx context.Context, req *apiv1.ProvisionServiceAccountRequest) (*apiv1.ProvisionServiceAccountResponse, error) { + var service *db.Account + + nodeAttestation := []string{} + certAuth := []string{} + + subject_alternative_names := validator.SanitizeInput(req.SubjectAlternativeNames) + certificate_authorities := validator.SanitizeInput(req.CertificateAuthorities) + + payload, ok := ctx.Value(types.ProvisionerAuthenticationContextKey).(*types.ProvisionerAccountPayload) + if !ok { + return nil, logger.RpcError(status.Error(codes.InvalidArgument, "service auth context missing"), fmt.Errorf("service auth context missing")) + } + + if !validator.Contains(payload.Environments, req.Environment) { + return nil, logger.RpcError(status.Error(codes.InvalidArgument, "invalid environment"), fmt.Errorf("invalid environment [%s]", req.Environment)) + } + + err := s.validateSanInputServiceAccount(ctx, req.ServiceAccount, req.Environment, req.SubjectAlternativeNames, &req.RegularExpression) + if err != nil { + return nil, logger.RpcError(status.Error(codes.InvalidArgument, "provisioner subject alternative name (san) validation error"), fmt.Errorf("provisioner subject alternative name validation error [%s]", err)) + } + + err = s.CheckSubordinateCaRegion(req.SubordinateCa, req.Region, req.Environment, req.CertificateAuthorities) + if err != nil { + return nil, logger.RpcError(status.Error(codes.InvalidArgument, "subordinate ca does not support region"), fmt.Errorf("subordinate ca does not support region [%s] for service account [%s] %s", *req.Region, req.ServiceAccount, err)) + } + + regex, err := regexp.Compile(payload.RegularExpression) + if err != nil { + return nil, logger.RpcError(status.Error(codes.Internal, "internal server error"), err) + } + + for _, reqSan := range req.SubjectAlternativeNames { + if !validator.Contains(payload.ValidSubjectAlternateNames, reqSan) && !regex.Match([]byte(reqSan)) { + return nil, logger.RpcError(status.Error(codes.InvalidArgument, "internal server error"), fmt.Errorf("subject alternative name (san) %s exists in another provisioner account", reqSan)) + } + } + + if len(certificate_authorities) == 0 { + environment := req.Environment + certificate_authorities := validator.CertificateAuthorityEnvironments[environment] + + // Include Default Certificate Authorities + for _, ca := range certificate_authorities { + ca_metadata := s.acmConfig[ca] + if ca_metadata.Default { + // Add Default Certificate Authorities for Region + if req.Region != nil && ca_metadata.Region == *req.Region { + certAuth = append(certAuth, ca) + } else if req.Region == nil { + // Add All Default Certificate Authorities + certAuth = append(certAuth, ca) + } + } + } + if len(certAuth) == 0 { + return nil, logger.RpcError(status.Error(codes.InvalidArgument, "invalid default certificate authority"), fmt.Errorf("invalid default certificate authority for environment [%s]", req.Environment)) + } + } else { + certAuth = req.CertificateAuthorities + } + + if !validator.Contains(payload.Environments, req.Environment) { + return nil, logger.RpcError(status.Error(codes.InvalidArgument, "invalid extended key"), fmt.Errorf("invalid environment [%s]", req.Environment)) + } + + err = s.validateCertificateParameters(certAuth, req.Environment, int16(req.CertificateValidity), req.SubordinateCa) + if err != nil { + return nil, logger.RpcError(status.Error(codes.InvalidArgument, err.Error()), err) + } + + if _, ok := types.CertificateRequestExtension[req.ExtendedKey]; !ok { + return nil, logger.RpcError(status.Error(codes.InvalidArgument, "invalid extended key"), fmt.Errorf("invalid key extension [%s]", req.ExtendedKey)) + } + + if !validator.Contains(payload.ExtendedKeys, req.ExtendedKey) && !validator.Contains(payload.ExtendedKeys, "*") { + return nil, logger.RpcError(status.Error(codes.InvalidArgument, "invalid extended key"), fmt.Errorf("invalid key extension [%s]", req.ExtendedKey)) + } + + if ok := validator.ValidateEmail(req.Email); !ok { + return nil, logger.RpcError(status.Error(codes.InvalidArgument, "invalid email"), fmt.Errorf("invalid email [%s]", req.Email)) + } + + if len(req.Team) == 0 { + return nil, logger.RpcError(status.Error(codes.InvalidArgument, "invalid team parameter"), fmt.Errorf("invalid team [%s]", req.Team)) + } + + if req.Environment == _production || req.NodeAttestation != nil { + if err = validateNodeAttestation(req.NodeAttestation); err != nil { + return nil, logger.RpcError(status.Error(codes.InvalidArgument, err.Error()), err) + } + } + + client_id, err := uuid.NewRandom() + if err != nil { + return nil, logger.RpcError(status.Error(codes.Internal, "internal server error"), err) + } + + clientToken, err := lib.GenerateClientToken(32) + if err != nil { + return nil, logger.RpcError(status.Error(codes.Internal, "internal server error"), err) + } + + hashedClientToken, err := lib.HashPassword(clientToken) + if err != nil { + return nil, logger.RpcError(status.Error(codes.Internal, "internal server error"), err) + } + + if req.NodeAttestation != nil { + nodeAttestation = aws_iid.GetNodeAttestation(req.NodeAttestation) + + account_arg := db.CreateServiceAccountParams{ + ClientID: client_id, + ApiToken: hashedClientToken, + ServiceAccount: req.ServiceAccount, + Environment: req.Environment, + NodeAttestation: nodeAttestation, + Team: req.Team, + Email: req.Email, + ValidSubjectAlternateName: subject_alternative_names, + ValidCertificateAuthorities: certAuth, + ExtendedKey: req.ExtendedKey, + CertificateValidity: int16(req.CertificateValidity), + SubordinateCa: req.SubordinateCa, + Provisioned: true, + CreatedBy: payload.ClientId, + CreatedAt: time.Now().UTC(), + } + + if len(req.RegularExpression) != 0 { + account_arg.RegularExpression = sql.NullString{String: req.RegularExpression, Valid: len(req.RegularExpression) != 0} + } + + raw_message, err := validator.MapToNullRawMessage(req.NodeAttestation.AwsIid.InstanceTags) + if err != nil { + return nil, logger.RpcError(status.Error(codes.Internal, "internal server error"), err) + } + + iid_arg := db.StoreInstanceIdentityDocumentParams{ + ClientID: client_id, + RoleArn: sql.NullString{String: req.NodeAttestation.AwsIid.RoleArn, Valid: len(req.NodeAttestation.AwsIid.RoleArn) != 0}, + AssumeRole: sql.NullString{String: req.NodeAttestation.AwsIid.AssumeRole, Valid: len(req.NodeAttestation.AwsIid.AssumeRole) != 0}, + Region: sql.NullString{String: req.NodeAttestation.AwsIid.Region, Valid: len(req.NodeAttestation.AwsIid.Region) != 0}, + InstanceID: sql.NullString{String: req.NodeAttestation.AwsIid.InstanceId, Valid: len(req.NodeAttestation.AwsIid.InstanceId) != 0}, + ImageID: sql.NullString{String: req.NodeAttestation.AwsIid.ImageId, Valid: len(req.NodeAttestation.AwsIid.ImageId) != 0}, + SecurityGroupID: req.NodeAttestation.AwsIid.SecurityGroups, + InstanceTags: raw_message, + } + + service, err = s.store.Writer.TxCreateServiceAccount(ctx, account_arg, iid_arg) + if err != nil { + return nil, logger.RpcError(status.Error(codes.Internal, "error creating account"), err) + } + } else { + account_arg := db.CreateServiceAccountParams{ + ClientID: client_id, + ApiToken: hashedClientToken, + ServiceAccount: req.ServiceAccount, + Environment: req.Environment, + NodeAttestation: nodeAttestation, + Team: req.Team, + Email: req.Email, + ValidSubjectAlternateName: subject_alternative_names, + ValidCertificateAuthorities: certAuth, + ExtendedKey: req.ExtendedKey, + CertificateValidity: int16(req.CertificateValidity), + SubordinateCa: req.SubordinateCa, + Provisioned: true, + CreatedBy: payload.ClientId, + CreatedAt: time.Now().UTC(), + } + + if len(req.RegularExpression) != 0 { + account_arg.RegularExpression = sql.NullString{String: req.RegularExpression, Valid: len(req.RegularExpression) != 0} + } + + service, err = s.store.Writer.CreateServiceAccount(ctx, account_arg) + if err != nil { + return nil, logger.RpcError(status.Error(codes.Internal, "error creating account"), err) + } + } + + response := apiv1.ProvisionServiceAccountResponse{ + ClientId: service.ClientID.String(), + ClientToken: clientToken, + ServiceAccount: service.ServiceAccount, + Environment: service.Environment, + NodeAttestation: req.NodeAttestation, + SubjectAlternativeNames: service.ValidSubjectAlternateName, + ExtendedKey: service.ExtendedKey, + CertificateAuthorities: service.ValidCertificateAuthorities, + CertificateValidity: int32(service.CertificateValidity), + SubordinateCa: service.SubordinateCa, + Team: service.Team, + Email: service.Email, + CreatedAt: timestamppb.New(service.CreatedAt), + CreatedBy: service.CreatedBy.String(), + } + + if service.RegularExpression.Valid { + response.RegularExpression = service.RegularExpression.String + } + + return &response, nil +} + +func (s *Service) CheckSubordinateCaRegion(subordinate_ca string, region *string, environment string, valid_certificate_authorities []string) error { + // Service Account Does Not Contain Region Requirements for CA + if region == nil { + return nil + } + + // Get Valid Certificate Authorities for Subordinate CA + arg := db.ListValidCertificateAuthorityFromSubordinateCAParams{ + SubordinateCa: subordinate_ca, + Environment: environment, + } + certificate_authorities, err := s.store.Reader.ListValidCertificateAuthorityFromSubordinateCA(context.Background(), arg) + if err != nil { + return fmt.Errorf("error listing valid certificate authority from subordinate ca [%s] %s", subordinate_ca, err) + } + + // Generate Map for Certificate Authority Regions for Subordinate CA + subordinate_ca_regions := make(map[string]bool) + for _, ca := range certificate_authorities { + ca_str, ok := ca.(string) + if !ok { + return fmt.Errorf("certificate authority [%s] is not a string in database", ca) + } + region := s.acmConfig[ca_str].Region + subordinate_ca_regions[region] = true + } + + // Subordinate CA Across All Service Accounts Must Be In The Same Region + if len(subordinate_ca_regions) != 1 { + return fmt.Errorf("multiple regions present for subordinate ca [%s], cannot support region [%s]", subordinate_ca, *region) + } + + if !subordinate_ca_regions[*region] { + return fmt.Errorf("invalid region [%s] for subordinate ca [%s]", *region, subordinate_ca) + } + + // Check Valid Certificate Authorities in Correct Region + for _, ca := range valid_certificate_authorities { + if s.acmConfig[ca].Region != *region { + return fmt.Errorf("invalid region [%s] for certificate authority [%s]", *region, ca) + } + } + return nil +} diff --git a/internal/v1/accounts/provision_test.go b/internal/v1/accounts/provision_test.go new file mode 100644 index 0000000..80d329c --- /dev/null +++ b/internal/v1/accounts/provision_test.go @@ -0,0 +1,449 @@ +package accounts + +import ( + "context" + "database/sql" + "fmt" + "reflect" + "testing" + "time" + + "github.com/coinbase/baseca/db/mock" + db "github.com/coinbase/baseca/db/sqlc" + apiv1 "github.com/coinbase/baseca/gen/go/baseca/v1" + lib "github.com/coinbase/baseca/internal/lib/authentication" + "github.com/coinbase/baseca/internal/types" + "github.com/google/uuid" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" +) + +func TestCreateProvisionerAccount(t *testing.T) { + id, err := uuid.NewRandom() + require.NoError(t, err) + + authClaim := &lib.Claims{ + Permission: "ADMIN", + Subject: id, + IssuedAt: time.Now().UTC(), + ExpiresAt: time.Now().UTC().AddDate(0, 0, 1), + NotBefore: time.Now().UTC(), + } + + attestation := apiv1.AWSInstanceIdentityDocument{ + RoleArn: "arn:aws:iam::123456789012:instance-profile/role", + AssumeRole: "arn:aws:iam::123456789012:role/assumed-role", + SecurityGroups: []string{"sg-0123456789abcdef0"}, + Region: "us-east-1", + } + + request := &apiv1.CreateProvisionerAccountRequest{ + ProvisionerAccount: "example", + Environments: []string{"development"}, + SubjectAlternativeNames: []string{"development.example.com"}, + ExtendedKeys: []string{"EndEntityServerAuthCertificate"}, + MaxCertificateValidity: 30, + Team: "Team", + Email: "example@coinbase.com", + } + + requestOK := &apiv1.CreateProvisionerAccountRequest{ + ProvisionerAccount: "example", + Environments: []string{"development"}, + SubjectAlternativeNames: []string{"development.example.com"}, + ExtendedKeys: []string{"EndEntityServerAuthCertificate"}, + MaxCertificateValidity: 30, + NodeAttestation: &apiv1.NodeAttestation{ + AwsIid: &attestation, + }, + Team: "Team", + Email: "example@coinbase.com", + } + + cases := []struct { + name string + req *apiv1.CreateProvisionerAccountRequest + ctx context.Context + build func(store *mock.MockStore) + check func(t *testing.T, res *apiv1.CreateProvisionerAccountResponse, err error) + }{ + { + name: "OK_NO_ATTESTATION", + req: request, + ctx: context.WithValue(context.Background(), types.UserAuthenticationContextKey, authClaim), + build: func(store *mock.MockStore) { + account_arg := db.CreateProvisionerAccountParams{ + ProvisionerAccount: "example", + Environments: []string{"development"}, + ValidSubjectAlternateNames: []string{"development.example.com"}, + ExtendedKeys: []string{"EndEntityServerAuthCertificate"}, + MaxCertificateValidity: 30, + Team: "Team", + Email: "example@coinbase.com", + CreatedBy: authClaim.Subject, + NodeAttestation: []string{}, + } + store.EXPECT().ListProvisionerAccounts(gomock.Any(), gomock.Any()).Times(1).Return([]*db.Provisioner{}, nil) + store.EXPECT().CreateProvisionerAccount(gomock.Any(), + EqCreateProvisionerAccountParams(account_arg, "provisioner arg matcher")).Times(1).Return(&db.Provisioner{}, nil) + }, + check: func(t *testing.T, res *apiv1.CreateProvisionerAccountResponse, err error) { + require.NoError(t, err) + }, + }, + { + name: "OK", + req: requestOK, + ctx: context.WithValue(context.Background(), types.UserAuthenticationContextKey, authClaim), + build: func(store *mock.MockStore) { + account_arg := db.CreateProvisionerAccountParams{ + ProvisionerAccount: "example", + Environments: []string{"development"}, + ValidSubjectAlternateNames: []string{"development.example.com"}, + ExtendedKeys: []string{"EndEntityServerAuthCertificate"}, + MaxCertificateValidity: 30, + Team: "Team", + Email: "example@coinbase.com", + CreatedBy: authClaim.Subject, + NodeAttestation: []string{"AWS_IID"}, + } + + attestation_arg := db.StoreInstanceIdentityDocumentParams{ + RoleArn: sql.NullString{String: attestation.RoleArn, Valid: true}, + AssumeRole: sql.NullString{String: attestation.AssumeRole, Valid: true}, + SecurityGroupID: attestation.SecurityGroups, + Region: sql.NullString{String: attestation.Region, Valid: true}, + } + + store.EXPECT().ListProvisionerAccounts(gomock.Any(), gomock.Any()).Times(1).Return([]*db.Provisioner{}, nil) + store.EXPECT().TxCreateProvisionerAccount(gomock.Any(), + EqCreateProvisionerAccountParams(account_arg, "provisioner arg matcher"), + EqStoreInstanceIdentityDocumentParams(attestation_arg, "iid arg matcher"), + ).Times(1).Return(&db.Provisioner{}, nil) + }, + check: func(t *testing.T, res *apiv1.CreateProvisionerAccountResponse, err error) { + require.NoError(t, err) + }, + }, + } + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + store := mock.NewMockStore(ctrl) + + for elem := range cases { + tc := cases[elem] + + t.Run(tc.name, func(t *testing.T) { + tc.build(store) + + c, err := buildAccountsConfig(store) + require.NoError(t, err) + + res, err := c.CreateProvisionerAccount(tc.ctx, tc.req) + tc.check(t, res, err) + }) + } +} + +func TestProvisionServiceAccount(t *testing.T) { + region := "us-east-1" + id, err := uuid.NewRandom() + require.NoError(t, err) + + authClaim := &types.ProvisionerAccountPayload{ + ClientId: id, + ProvisionerAccount: "provisioner", + Environments: []string{"sandbox"}, + ValidSubjectAlternateNames: []string{"*.example.com"}, + MaxCertificateValidity: uint32(30), + ExtendedKeys: []string{"EndEntityServerAuthCertificate"}, + RegularExpression: "^.{0,250}$", + } + + cases := []struct { + name string + req *apiv1.ProvisionServiceAccountRequest + ctx context.Context + build func(store *mock.MockStore) + check func(t *testing.T, res *apiv1.ProvisionServiceAccountResponse, err error) + }{ + { + name: "OK_NO_ATTESTATION", + req: &apiv1.ProvisionServiceAccountRequest{ + ServiceAccount: "example", + Environment: "sandbox", + SubjectAlternativeNames: []string{"sandbox.example.com"}, + CertificateAuthorities: []string{"sandbox_use1"}, + SubordinateCa: "infrastructure", + CertificateValidity: 30, + ExtendedKey: "EndEntityServerAuthCertificate", + Team: "Infrastructure Security", + Email: "security@coinbase.com", + }, + build: func(store *mock.MockStore) { + account_arg := db.CreateServiceAccountParams{ + ServiceAccount: "example", + Environment: "sandbox", + ValidSubjectAlternateName: []string{"sandbox.example.com"}, + ExtendedKey: "EndEntityServerAuthCertificate", + CertificateValidity: 30, + ValidCertificateAuthorities: []string{"sandbox_use1"}, + SubordinateCa: "infrastructure", + Team: "Infrastructure Security", + Email: "security@coinbase.com", + CreatedBy: authClaim.ClientId, + NodeAttestation: []string{}, + Provisioned: true, + } + store.EXPECT().ListServiceAccounts(gomock.Any(), gomock.Any()).Times(1).Return([]*db.Account{}, nil) + store.EXPECT().CreateServiceAccount( + gomock.Any(), + EqProvisionServiceAccountParams(account_arg), + ).Times(1).Return(&db.Account{}, nil) + }, + ctx: context.WithValue(context.Background(), types.ProvisionerAuthenticationContextKey, authClaim), + check: func(t *testing.T, res *apiv1.ProvisionServiceAccountResponse, err error) { + require.NoError(t, err) + }, + }, + { + name: "OK_DEFAULT_CERTIFICATE_AUTHORITIES", + req: &apiv1.ProvisionServiceAccountRequest{ + ServiceAccount: "example", + Environment: "sandbox", + SubjectAlternativeNames: []string{"sandbox.example.com"}, + SubordinateCa: "infrastructure", + CertificateValidity: 30, + ExtendedKey: "EndEntityServerAuthCertificate", + Team: "Infrastructure Security", + Email: "security@coinbase.com", + }, + build: func(store *mock.MockStore) { + account_arg := db.CreateServiceAccountParams{ + ServiceAccount: "example", + Environment: "sandbox", + ValidSubjectAlternateName: []string{"sandbox.example.com"}, + ExtendedKey: "EndEntityServerAuthCertificate", + CertificateValidity: 30, + ValidCertificateAuthorities: []string{"sandbox_use1", "sandbox_use2"}, + SubordinateCa: "infrastructure", + Team: "Infrastructure Security", + Email: "security@coinbase.com", + CreatedBy: authClaim.ClientId, + NodeAttestation: []string{}, + Provisioned: true, + } + store.EXPECT().ListServiceAccounts(gomock.Any(), gomock.Any()).Times(1).Return([]*db.Account{}, nil) + store.EXPECT().CreateServiceAccount( + gomock.Any(), + EqProvisionServiceAccountParams(account_arg), + ).Times(1).Return(&db.Account{}, nil) + }, + ctx: context.WithValue(context.Background(), types.ProvisionerAuthenticationContextKey, authClaim), + check: func(t *testing.T, res *apiv1.ProvisionServiceAccountResponse, err error) { + require.NoError(t, err) + }, + }, + { + name: "OK_REGION_REQUIRED_AND_VALID_CA", + req: &apiv1.ProvisionServiceAccountRequest{ + ServiceAccount: "example", + Environment: "sandbox", + SubjectAlternativeNames: []string{"sandbox.example.com"}, + SubordinateCa: "infrastructure", + CertificateValidity: 30, + Region: ®ion, + ExtendedKey: "EndEntityServerAuthCertificate", + Team: "Infrastructure Security", + Email: "security@coinbase.com", + }, + build: func(store *mock.MockStore) { + account_arg := db.CreateServiceAccountParams{ + ServiceAccount: "example", + Environment: "sandbox", + ValidSubjectAlternateName: []string{"sandbox.example.com"}, + ExtendedKey: "EndEntityServerAuthCertificate", + CertificateValidity: 30, + ValidCertificateAuthorities: []string{"sandbox_use1"}, + SubordinateCa: "infrastructure", + Team: "Infrastructure Security", + Email: "security@coinbase.com", + CreatedBy: authClaim.ClientId, + NodeAttestation: []string{}, + Provisioned: true, + } + + arg := db.ListValidCertificateAuthorityFromSubordinateCAParams{ + SubordinateCa: "infrastructure", + Environment: "sandbox", + } + store.EXPECT().ListValidCertificateAuthorityFromSubordinateCA(gomock.Any(), arg).Times(1).Return([]any{"sandbox_use1"}, nil) + store.EXPECT().ListServiceAccounts(gomock.Any(), gomock.Any()).Times(1).Return([]*db.Account{}, nil) + store.EXPECT().CreateServiceAccount( + gomock.Any(), + EqProvisionServiceAccountParams(account_arg), + ).Times(1).Return(&db.Account{}, nil) + }, + ctx: context.WithValue(context.Background(), types.ProvisionerAuthenticationContextKey, authClaim), + check: func(t *testing.T, res *apiv1.ProvisionServiceAccountResponse, err error) { + require.NoError(t, err) + }, + }, + { + name: "ERROR_REGION_REQUIRED_AND_CA_WITH_CONFLICTING_REGION", + req: &apiv1.ProvisionServiceAccountRequest{ + ServiceAccount: "example", + Environment: "sandbox", + SubjectAlternativeNames: []string{"sandbox.example.com"}, + SubordinateCa: "infrastructure", + CertificateValidity: 30, + Region: ®ion, + ExtendedKey: "EndEntityServerAuthCertificate", + Team: "Infrastructure Security", + Email: "security@coinbase.com", + }, + build: func(store *mock.MockStore) { + arg := db.ListValidCertificateAuthorityFromSubordinateCAParams{ + SubordinateCa: "infrastructure", + Environment: "sandbox", + } + store.EXPECT().ListValidCertificateAuthorityFromSubordinateCA(gomock.Any(), arg).Times(1).Return([]any{"sandbox_use2"}, nil) + store.EXPECT().ListServiceAccounts(gomock.Any(), gomock.Any()).Times(1).Return([]*db.Account{}, nil) + }, + ctx: context.WithValue(context.Background(), types.ProvisionerAuthenticationContextKey, authClaim), + check: func(t *testing.T, res *apiv1.ProvisionServiceAccountResponse, err error) { + require.Error(t, err) + require.Empty(t, res) + require.EqualError(t, err, "rpc error: code = InvalidArgument desc = subordinate ca does not support region") + }, + }, + { + name: "ERROR_REGION_REQUIRED_AND_CA_WITH_MULTIPLE_REGIONS", + req: &apiv1.ProvisionServiceAccountRequest{ + ServiceAccount: "example", + Environment: "sandbox", + SubjectAlternativeNames: []string{"sandbox.example.com"}, + SubordinateCa: "infrastructure", + CertificateValidity: 30, + Region: ®ion, + ExtendedKey: "EndEntityServerAuthCertificate", + Team: "Infrastructure Security", + Email: "security@coinbase.com", + }, + build: func(store *mock.MockStore) { + arg := db.ListValidCertificateAuthorityFromSubordinateCAParams{ + SubordinateCa: "infrastructure", + Environment: "sandbox", + } + store.EXPECT().ListValidCertificateAuthorityFromSubordinateCA(gomock.Any(), arg).Times(1).Return([]any{"sandbox_use2", "sandbox_use1"}, nil) + store.EXPECT().ListServiceAccounts(gomock.Any(), gomock.Any()).Times(1).Return([]*db.Account{}, nil) + }, + ctx: context.WithValue(context.Background(), types.ProvisionerAuthenticationContextKey, authClaim), + check: func(t *testing.T, res *apiv1.ProvisionServiceAccountResponse, err error) { + require.Error(t, err) + require.Empty(t, res) + require.EqualError(t, err, "rpc error: code = InvalidArgument desc = subordinate ca does not support region") + }, + }, + { + name: "ERROR_CERTIFICATE_AUTHORITY_DIFFERENT_REGION", + req: &apiv1.ProvisionServiceAccountRequest{ + ServiceAccount: "example", + Environment: "sandbox", + SubjectAlternativeNames: []string{"sandbox.example.com"}, + SubordinateCa: "infrastructure", + CertificateValidity: 30, + Region: ®ion, + CertificateAuthorities: []string{"sandbox_use2"}, // Different Region than Region Field (us-east-1) + ExtendedKey: "EndEntityServerAuthCertificate", + Team: "Infrastructure Security", + Email: "security@coinbase.com", + }, + build: func(store *mock.MockStore) { + arg := db.ListValidCertificateAuthorityFromSubordinateCAParams{ + SubordinateCa: "infrastructure", + Environment: "sandbox", + } + store.EXPECT().ListValidCertificateAuthorityFromSubordinateCA(gomock.Any(), arg).Times(1).Return([]any{"sandbox_use2", "sandbox_use1"}, nil) + store.EXPECT().ListServiceAccounts(gomock.Any(), gomock.Any()).Times(1).Return([]*db.Account{}, nil) + }, + ctx: context.WithValue(context.Background(), types.ProvisionerAuthenticationContextKey, authClaim), + check: func(t *testing.T, res *apiv1.ProvisionServiceAccountResponse, err error) { + require.Error(t, err) + require.Empty(t, res) + require.EqualError(t, err, "rpc error: code = InvalidArgument desc = subordinate ca does not support region") + }, + }, + } + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + store := mock.NewMockStore(ctrl) + + for elem := range cases { + tc := cases[elem] + + t.Run(tc.name, func(t *testing.T) { + tc.build(store) + + c, err := buildAccountsConfig(store) + require.NoError(t, err) + + res, err := c.ProvisionServiceAccount(tc.ctx, tc.req) + tc.check(t, res, err) + }) + } +} + +type eqCreateProvisionerProvisionerParamsMatcher struct { + arg db.CreateProvisionerAccountParams + password string +} + +func (e eqCreateProvisionerProvisionerParamsMatcher) Matches(x any) bool { + arg, ok := x.(db.CreateProvisionerAccountParams) + if !ok { + return false + } + + e.arg.ClientID = arg.ClientID + e.arg.ApiToken = arg.ApiToken + e.arg.CreatedAt = arg.CreatedAt + return reflect.DeepEqual(e.arg, arg) +} + +func (e eqCreateProvisionerProvisionerParamsMatcher) String() string { + return fmt.Sprintf("%v", e.arg) +} + +func EqCreateProvisionerAccountParams(arg db.CreateProvisionerAccountParams, password string) gomock.Matcher { + return eqCreateProvisionerProvisionerParamsMatcher{arg, password} +} + +type eqProvisionServiceAccountParams struct { + arg db.CreateServiceAccountParams +} + +func EqProvisionServiceAccountParams(arg db.CreateServiceAccountParams) gomock.Matcher { + return eqProvisionServiceAccountParams{arg} +} + +func (e eqProvisionServiceAccountParams) Matches(x any) bool { + arg, ok := x.(db.CreateServiceAccountParams) + if !ok { + return false + } + + e.arg.ClientID = arg.ClientID + e.arg.ApiToken = arg.ApiToken + e.arg.CreatedAt = arg.CreatedAt + + return reflect.DeepEqual(e.arg, arg) +} + +func (e eqProvisionServiceAccountParams) String() string { + return fmt.Sprintf("%v", e.arg) +} diff --git a/internal/v1/accounts/query.go b/internal/v1/accounts/query.go new file mode 100644 index 0000000..f2b7408 --- /dev/null +++ b/internal/v1/accounts/query.go @@ -0,0 +1,279 @@ +package accounts + +import ( + "context" + "database/sql" + "fmt" + + db "github.com/coinbase/baseca/db/sqlc" + apiv1 "github.com/coinbase/baseca/gen/go/baseca/v1" + "github.com/coinbase/baseca/internal/lib/util/validator" + "github.com/coinbase/baseca/internal/logger" + "github.com/coinbase/baseca/internal/types" + "github.com/gogo/status" + "github.com/google/uuid" + "google.golang.org/grpc/codes" + "google.golang.org/protobuf/types/known/timestamppb" +) + +func (s *Service) ListServiceAccounts(ctx context.Context, req *apiv1.QueryParameter) (*apiv1.ServiceAccounts, error) { + var accounts apiv1.ServiceAccounts + + if req.PageId <= 0 || req.PageSize <= 0 { + return nil, logger.RpcError(status.Error(codes.InvalidArgument, "invalid request parameters"), fmt.Errorf("invalid page_id or page_size")) + } + + arg := db.ListServiceAccountsParams{ + Limit: req.PageSize, + Offset: (req.PageId - 1) * req.PageSize, + } + + services, err := s.store.Reader.ListServiceAccounts(ctx, arg) + if err != nil { + return nil, logger.RpcError(status.Error(codes.Internal, "internal server error"), err) + } + + for _, service := range services { + account, err := s.transformServiceAccount(ctx, service) + if err != nil { + return nil, logger.RpcError(status.Error(codes.Internal, "internal server error"), err) + } + accounts.ServiceAccounts = append(accounts.ServiceAccounts, account) + } + + return &accounts, nil +} + +func (s *Service) ListProvisionerAccounts(ctx context.Context, req *apiv1.QueryParameter) (*apiv1.ProvisionerAccounts, error) { + var accounts apiv1.ProvisionerAccounts + + if req.PageId <= 0 || req.PageSize <= 0 { + return nil, logger.RpcError(status.Error(codes.InvalidArgument, "invalid request parameters"), fmt.Errorf("invalid page_id or page_size")) + } + + arg := db.ListProvisionerAccountsParams{ + Limit: req.PageSize, + Offset: (req.PageId - 1) * req.PageSize, + } + + provisioners, err := s.store.Reader.ListProvisionerAccounts(ctx, arg) + if err != nil { + return nil, logger.RpcError(status.Error(codes.Internal, "internal server error"), err) + } + + for _, provisioner := range provisioners { + account, err := s.transformProvisionerAccount(ctx, provisioner) + if err != nil { + return nil, logger.RpcError(status.Error(codes.Internal, "internal server error"), err) + } + accounts.ProvisionerAccounts = append(accounts.ProvisionerAccounts, account) + } + return &accounts, nil +} + +func (s *Service) GetServiceAccount(ctx context.Context, req *apiv1.AccountId) (*apiv1.ServiceAccount, error) { + id, err := uuid.Parse(req.Uuid) + if err != nil { + return nil, logger.RpcError(status.Error(codes.InvalidArgument, "invalid uuid parameter"), err) + } + + service, err := s.store.Reader.GetServiceUUID(ctx, id) + if err != nil { + if err == sql.ErrNoRows { + return nil, logger.RpcError(status.Error(codes.Internal, "internal server error"), fmt.Errorf("service account uuid %s does not exist", id)) + } + return nil, logger.RpcError(status.Error(codes.Internal, "internal server error"), err) + } + + account, err := s.transformServiceAccount(ctx, service) + if err != nil { + return nil, logger.RpcError(status.Error(codes.Internal, "internal server error"), err) + } + + return account, nil +} + +func (s *Service) GetProvisionerAccount(ctx context.Context, req *apiv1.AccountId) (*apiv1.ProvisionerAccount, error) { + id, err := uuid.Parse(req.Uuid) + if err != nil { + return nil, logger.RpcError(status.Error(codes.InvalidArgument, "invalid uuid parameter"), err) + } + + provisioner, err := s.store.Reader.GetProvisionerUUID(ctx, id) + if err != nil { + if err == sql.ErrNoRows { + return nil, logger.RpcError(status.Error(codes.Internal, "internal server error"), fmt.Errorf("provisioner account uuid %s does not exist", id)) + } + return nil, logger.RpcError(status.Error(codes.Internal, "internal server error"), err) + } + + account, err := s.transformProvisionerAccount(ctx, provisioner) + if err != nil { + return nil, logger.RpcError(status.Error(codes.Internal, "internal server error"), err) + } + + return account, nil +} + +func (s *Service) transformServiceAccount(ctx context.Context, account *db.Account) (*apiv1.ServiceAccount, error) { + var attestation types.NodeAttestation + + if validator.Contains(account.NodeAttestation, types.Attestation.AWS_IID) { + iid, err := s.store.Reader.GetInstanceIdentityDocument(ctx, account.ClientID) + if err != nil { + return nil, err + } + + instance_tag_map, err := validator.ConvertNullRawMessageToMap(iid.InstanceTags) + if err != nil { + return nil, err + } + + // TODO: Update awsIid {} Response + attestation = types.NodeAttestation{ + AWSInstanceIdentityDocument: types.AWSInstanceIdentityDocument{ + RoleArn: iid.RoleArn.String, + AssumeRole: iid.AssumeRole.String, + SecurityGroups: iid.SecurityGroupID, + Region: iid.Region.String, + InstanceID: iid.InstanceID.String, + ImageID: iid.ImageID.String, + InstanceTags: instance_tag_map, + }, + } + } + + return &apiv1.ServiceAccount{ + ClientId: account.ClientID.String(), + ServiceAccount: account.ServiceAccount, + Environment: account.Environment, + RegularExpression: account.RegularExpression.String, + SubjectAlternativeNames: account.ValidSubjectAlternateName, + CertificateAuthorities: account.ValidCertificateAuthorities, + ExtendedKey: account.ExtendedKey, + CertificateValidity: int32(account.CertificateValidity), + SubordinateCa: account.SubordinateCa, + Provisioned: account.Provisioned, + Team: account.Team, + Email: account.Email, + CreatedAt: timestamppb.New(account.CreatedAt), + CreatedBy: account.CreatedBy.String(), + NodeAttestation: &apiv1.NodeAttestation{ + AwsIid: &apiv1.AWSInstanceIdentityDocument{ + RoleArn: attestation.AWSInstanceIdentityDocument.RoleArn, + AssumeRole: attestation.AWSInstanceIdentityDocument.AssumeRole, + SecurityGroups: attestation.AWSInstanceIdentityDocument.SecurityGroups, + Region: attestation.AWSInstanceIdentityDocument.Region, + InstanceId: attestation.AWSInstanceIdentityDocument.InstanceID, + InstanceTags: attestation.AWSInstanceIdentityDocument.InstanceTags, + }, + }, + }, nil +} + +func (s *Service) transformProvisionerAccount(ctx context.Context, account *db.Provisioner) (*apiv1.ProvisionerAccount, error) { + var attestation types.NodeAttestation + + if validator.Contains(account.NodeAttestation, types.Attestation.AWS_IID) { + iid, err := s.store.Reader.GetInstanceIdentityDocument(ctx, account.ClientID) + if err != nil { + return nil, err + } + + instance_tag_map, err := validator.ConvertNullRawMessageToMap(iid.InstanceTags) + if err != nil { + return nil, err + } + + // TODO: Update awsIid {} Response + attestation = types.NodeAttestation{ + AWSInstanceIdentityDocument: types.AWSInstanceIdentityDocument{ + RoleArn: iid.RoleArn.String, + AssumeRole: iid.AssumeRole.String, + SecurityGroups: iid.SecurityGroupID, + Region: iid.Region.String, + InstanceID: iid.InstanceID.String, + ImageID: iid.ImageID.String, + InstanceTags: instance_tag_map, + }, + } + } + + return &apiv1.ProvisionerAccount{ + ClientId: account.ClientID.String(), + ProvisionerAccount: account.ProvisionerAccount, + Environments: account.Environments, + RegularExpression: account.RegularExpression.String, + SubjectAlternativeNames: account.ValidSubjectAlternateNames, + ExtendedKeys: account.ExtendedKeys, + MaxCertificateValidity: uint32(account.MaxCertificateValidity), + Team: account.Team, + Email: account.Email, + CreatedAt: timestamppb.New(account.CreatedAt), + CreatedBy: account.CreatedBy.String(), + NodeAttestation: &apiv1.NodeAttestation{ + AwsIid: &apiv1.AWSInstanceIdentityDocument{ + RoleArn: attestation.AWSInstanceIdentityDocument.RoleArn, + AssumeRole: attestation.AWSInstanceIdentityDocument.AssumeRole, + SecurityGroups: attestation.AWSInstanceIdentityDocument.SecurityGroups, + Region: attestation.AWSInstanceIdentityDocument.Region, + InstanceId: attestation.AWSInstanceIdentityDocument.InstanceID, + InstanceTags: attestation.AWSInstanceIdentityDocument.InstanceTags, + }, + }, + }, nil +} + +func (s *Service) GetServiceAccountMetadata(ctx context.Context, req *apiv1.GetServiceAccountMetadataRequest) (*apiv1.ServiceAccounts, error) { + var accounts apiv1.ServiceAccounts + var serviceAccount, environment, extendedKey string + + if len(req.ServiceAccount) == 0 && len(req.Environment) == 0 && len(req.ExtendedKey) == 0 { + return nil, logger.RpcError(status.Error(codes.InvalidArgument, "invalid parameters"), fmt.Errorf("invalid parameters nil or empty")) + } + + if len(req.ServiceAccount) == 0 { + serviceAccount = "%" + } else { + serviceAccount = req.ServiceAccount + } + + if len(req.Environment) == 0 { + environment = "%" + } else { + if !validator.ValidateInput(req.Environment) { + return nil, logger.RpcError(status.Error(codes.InvalidArgument, "invalid parameters"), fmt.Errorf("invalid environment: %s", req.ServiceAccount)) + } + environment = req.Environment + } + + if len(req.ExtendedKey) == 0 { + extendedKey = "%" + } else { + if !validator.ValidateInput(req.ExtendedKey) { + return nil, logger.RpcError(status.Error(codes.InvalidArgument, "invalid parameters"), fmt.Errorf("invalid extended key: %s", req.ExtendedKey)) + } + extendedKey = req.ExtendedKey + } + + arg := db.GetServiceAccountByMetadataParams{ + ServiceAccount: serviceAccount, + Environment: environment, + ExtendedKey: extendedKey, + } + + services, err := s.store.Reader.GetServiceAccountByMetadata(ctx, arg) + if err != nil { + return nil, logger.RpcError(status.Error(codes.Internal, "internal server error"), err) + } + + for _, service := range services { + account, err := s.transformServiceAccount(ctx, service) + if err != nil { + return nil, logger.RpcError(status.Error(codes.Internal, "internal server error"), err) + } + accounts.ServiceAccounts = append(accounts.ServiceAccounts, account) + } + + return &accounts, nil +} diff --git a/internal/v1/accounts/query_test.go b/internal/v1/accounts/query_test.go new file mode 100644 index 0000000..0b95edc --- /dev/null +++ b/internal/v1/accounts/query_test.go @@ -0,0 +1,573 @@ +package accounts + +import ( + "context" + "fmt" + "testing" + + "github.com/coinbase/baseca/db/mock" + db "github.com/coinbase/baseca/db/sqlc" + apiv1 "github.com/coinbase/baseca/gen/go/baseca/v1" + "github.com/google/uuid" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" +) + +func TestListServiceAccounts(t *testing.T) { + const ( + pageId = 1 + pageSize = 5 + ) + + param := db.ListServiceAccountsParams{ + Limit: pageSize, + Offset: (pageId - 1) * pageSize, + } + + clientId, err := uuid.NewRandom() + require.NoError(t, err) + + cases := []struct { + name string + req *apiv1.QueryParameter + build func(store *mock.MockStore) + check func(t *testing.T, res *apiv1.ServiceAccounts, err error) + }{ + { + name: "OK_NO_SERVICE_ACCOUNT", + req: &apiv1.QueryParameter{ + PageId: 1, + PageSize: 5, + }, + build: func(store *mock.MockStore) { + store.EXPECT().ListServiceAccounts(gomock.Any(), param).Times(1).Return([]*db.Account{}, nil) + }, + check: func(t *testing.T, res *apiv1.ServiceAccounts, err error) { + require.NoError(t, err) + }, + }, + { + name: "OK_NO_NODE_ATTESTATION", + req: &apiv1.QueryParameter{ + PageId: 1, + PageSize: 5, + }, + build: func(store *mock.MockStore) { + account := db.Account{ + ClientID: clientId, + NodeAttestation: []string{}, + } + store.EXPECT().ListServiceAccounts(gomock.Any(), param).Times(1).Return([]*db.Account{&account}, nil) + }, + check: func(t *testing.T, res *apiv1.ServiceAccounts, err error) { + require.NoError(t, err) + }, + }, + { + name: "OK", + req: &apiv1.QueryParameter{ + PageId: 1, + PageSize: 5, + }, + build: func(store *mock.MockStore) { + account := db.Account{ + ClientID: clientId, + NodeAttestation: []string{"AWS_IID"}, + } + store.EXPECT().ListServiceAccounts(gomock.Any(), param).Times(1).Return([]*db.Account{&account}, nil) + store.EXPECT().GetInstanceIdentityDocument(gomock.Any(), clientId).Times(1).Return(&db.AwsAttestation{}, nil) + }, + check: func(t *testing.T, res *apiv1.ServiceAccounts, err error) { + require.NoError(t, err) + }, + }, + { + name: "DB_ERROR_LIST_SERVICE_ACCOUNT", + req: &apiv1.QueryParameter{ + PageId: 1, + PageSize: 5, + }, + build: func(store *mock.MockStore) { + store.EXPECT().ListServiceAccounts(gomock.Any(), param).Times(1).Return(nil, fmt.Errorf("list service account error")) + }, + check: func(t *testing.T, res *apiv1.ServiceAccounts, err error) { + require.Error(t, err) + require.Empty(t, res) + require.EqualError(t, err, "rpc error: code = Internal desc = internal server error") + }, + }, + { + name: "DB_ERROR_GET_INSTANCE_IDENTITY_DOCUMENT", + req: &apiv1.QueryParameter{ + PageId: 1, + PageSize: 5, + }, + build: func(store *mock.MockStore) { + account := db.Account{ + ClientID: clientId, + NodeAttestation: []string{"AWS_IID"}, + } + store.EXPECT().ListServiceAccounts(gomock.Any(), param).Times(1).Return([]*db.Account{&account}, nil) + store.EXPECT().GetInstanceIdentityDocument(gomock.Any(), clientId).Times(1).Return(nil, fmt.Errorf("get instance identity document error")) + }, + check: func(t *testing.T, res *apiv1.ServiceAccounts, err error) { + require.Error(t, err) + require.Empty(t, res) + require.EqualError(t, err, "rpc error: code = Internal desc = internal server error") + }, + }, + } + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + store := mock.NewMockStore(ctrl) + + for elem := range cases { + tc := cases[elem] + + t.Run(tc.name, func(t *testing.T) { + tc.build(store) + + c, err := buildAccountsConfig(store) + require.NoError(t, err) + + res, err := c.ListServiceAccounts(context.Background(), tc.req) + tc.check(t, res, err) + }) + } +} + +func TestGetServiceAccount(t *testing.T) { + clientId, err := uuid.NewRandom() + require.NoError(t, err) + + cases := []struct { + name string + req *apiv1.AccountId + build func(store *mock.MockStore) + check func(t *testing.T, res *apiv1.ServiceAccount, err error) + }{ + { + name: "OK_NO_ATTESTATION", + req: &apiv1.AccountId{ + Uuid: clientId.String(), + }, + build: func(store *mock.MockStore) { + store.EXPECT().GetServiceUUID(gomock.Any(), clientId).Times(1).Return(&db.Account{}, nil) + }, + check: func(t *testing.T, res *apiv1.ServiceAccount, err error) { + require.NoError(t, err) + }, + }, + { + name: "OK", + req: &apiv1.AccountId{ + Uuid: clientId.String(), + }, + build: func(store *mock.MockStore) { + account := &db.Account{ + ClientID: clientId, + NodeAttestation: []string{"AWS_IID"}, + } + + store.EXPECT().GetServiceUUID(gomock.Any(), clientId).Times(1).Return(account, nil) + store.EXPECT().GetInstanceIdentityDocument(gomock.Any(), clientId).Times(1).Return(&db.AwsAttestation{}, nil) + }, + check: func(t *testing.T, res *apiv1.ServiceAccount, err error) { + require.NoError(t, err) + require.NotEmpty(t, res) + }, + }, + { + name: "DB_ERROR_GET_SERVICE_UUID", + req: &apiv1.AccountId{ + Uuid: clientId.String(), + }, + build: func(store *mock.MockStore) { + store.EXPECT().GetServiceUUID(gomock.Any(), clientId).Times(1).Return(nil, fmt.Errorf("get service uuid error")) + }, + check: func(t *testing.T, res *apiv1.ServiceAccount, err error) { + require.Error(t, err) + require.EqualError(t, err, "rpc error: code = Internal desc = internal server error") + require.Empty(t, res) + }, + }, + { + name: "DB_ERROR_GET_INSTANCE_IDENTITY_DOCUMENT", + req: &apiv1.AccountId{ + Uuid: clientId.String(), + }, + build: func(store *mock.MockStore) { + account := &db.Account{ + ClientID: clientId, + NodeAttestation: []string{"AWS_IID"}, + } + store.EXPECT().GetServiceUUID(gomock.Any(), clientId).Times(1).Return(account, nil) + store.EXPECT().GetInstanceIdentityDocument(gomock.Any(), clientId).Times(1).Return(nil, fmt.Errorf("get instance identity document")) + }, + check: func(t *testing.T, res *apiv1.ServiceAccount, err error) { + require.Error(t, err) + require.EqualError(t, err, "rpc error: code = Internal desc = internal server error") + require.Empty(t, res) + }, + }, + } + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + store := mock.NewMockStore(ctrl) + + for elem := range cases { + tc := cases[elem] + + t.Run(tc.name, func(t *testing.T) { + tc.build(store) + + c, err := buildAccountsConfig(store) + require.NoError(t, err) + + res, err := c.GetServiceAccount(context.Background(), tc.req) + tc.check(t, res, err) + }) + } +} + +func TestGetServiceAccountByMetadata(t *testing.T) { + account_name := "example" + environment := "development" + extended_key := "EndEntityServerAuthCertificate" + + request := &apiv1.GetServiceAccountMetadataRequest{ + ServiceAccount: account_name, + Environment: environment, + ExtendedKey: extended_key, + } + + arg := db.GetServiceAccountByMetadataParams{ + ServiceAccount: account_name, + Environment: environment, + ExtendedKey: extended_key, + } + + clientId, err := uuid.NewRandom() + require.NoError(t, err) + + cases := []struct { + name string + req *apiv1.GetServiceAccountMetadataRequest + build func(store *mock.MockStore) + check func(t *testing.T, res *apiv1.ServiceAccounts, err error) + }{ + { + name: "OK_NO_SERVICE_ACCOUNT", + req: request, + build: func(store *mock.MockStore) { + store.EXPECT().GetServiceAccountByMetadata(gomock.Any(), arg).Times(1).Return([]*db.Account{}, nil) + }, + check: func(t *testing.T, res *apiv1.ServiceAccounts, err error) { + require.NoError(t, err) + }, + }, + { + name: "OK_NO_ATTESTATION", + req: request, + build: func(store *mock.MockStore) { + account := db.Account{ + ServiceAccount: account_name, + ClientID: clientId, + } + store.EXPECT().GetServiceAccountByMetadata(gomock.Any(), arg).Times(1).Return([]*db.Account{&account}, nil) + }, + check: func(t *testing.T, res *apiv1.ServiceAccounts, err error) { + require.NoError(t, err) + }, + }, + { + name: "OK", + req: request, + build: func(store *mock.MockStore) { + account := db.Account{ + ServiceAccount: account_name, + ClientID: clientId, + NodeAttestation: []string{"AWS_IID"}, + } + store.EXPECT().GetServiceAccountByMetadata(gomock.Any(), arg).Times(1).Return([]*db.Account{&account}, nil) + store.EXPECT().GetInstanceIdentityDocument(gomock.Any(), clientId).Times(1).Return(&db.AwsAttestation{}, nil) + }, + check: func(t *testing.T, res *apiv1.ServiceAccounts, err error) { + require.NoError(t, err) + }, + }, + { + name: "DB_ERROR_GET_SERVICE_ACCOUNTS", + req: request, + build: func(store *mock.MockStore) { + store.EXPECT().GetServiceAccountByMetadata(gomock.Any(), arg).Times(1).Return(nil, fmt.Errorf("get service accounts error")) + }, + check: func(t *testing.T, res *apiv1.ServiceAccounts, err error) { + require.Error(t, err) + require.EqualError(t, err, "rpc error: code = Internal desc = internal server error") + require.Empty(t, res) + }, + }, + { + name: "DB_ERROR_GET_INSTANCE_IDENTITY_DOCUMENT", + req: request, + build: func(store *mock.MockStore) { + account := db.Account{ + ServiceAccount: account_name, + ClientID: clientId, + NodeAttestation: []string{"AWS_IID"}, + } + store.EXPECT().GetServiceAccountByMetadata(gomock.Any(), arg).Times(1).Return([]*db.Account{&account}, nil) + store.EXPECT().GetInstanceIdentityDocument(gomock.Any(), clientId).Times(1).Return(nil, fmt.Errorf("get instance identity document err")) + }, + check: func(t *testing.T, res *apiv1.ServiceAccounts, err error) { + require.Error(t, err) + require.EqualError(t, err, "rpc error: code = Internal desc = internal server error") + }, + }, + } + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + store := mock.NewMockStore(ctrl) + + for elem := range cases { + tc := cases[elem] + + t.Run(tc.name, func(t *testing.T) { + tc.build(store) + + c, err := buildAccountsConfig(store) + require.NoError(t, err) + + res, err := c.GetServiceAccountMetadata(context.Background(), tc.req) + tc.check(t, res, err) + }) + } +} + +func TestListProvisionerAccounts(t *testing.T) { + const ( + pageId = 1 + pageSize = 5 + ) + + param := db.ListProvisionerAccountsParams{ + Limit: pageSize, + Offset: (pageId - 1) * pageSize, + } + + clientId, err := uuid.NewRandom() + require.NoError(t, err) + + cases := []struct { + name string + req *apiv1.QueryParameter + build func(store *mock.MockStore) + check func(t *testing.T, res *apiv1.ProvisionerAccounts, err error) + }{ + { + name: "OK_NO_PROVISIONER_ACCOUNT", + req: &apiv1.QueryParameter{ + PageId: 1, + PageSize: 5, + }, + build: func(store *mock.MockStore) { + store.EXPECT().ListProvisionerAccounts(gomock.Any(), param).Times(1).Return([]*db.Provisioner{}, nil) + }, + check: func(t *testing.T, res *apiv1.ProvisionerAccounts, err error) { + require.NoError(t, err) + }, + }, + { + name: "OK_NO_NODE_ATTESTATION", + req: &apiv1.QueryParameter{ + PageId: 1, + PageSize: 5, + }, + build: func(store *mock.MockStore) { + account := db.Provisioner{ + ClientID: clientId, + NodeAttestation: []string{}, + } + store.EXPECT().ListProvisionerAccounts(gomock.Any(), param).Times(1).Return([]*db.Provisioner{&account}, nil) + }, + check: func(t *testing.T, res *apiv1.ProvisionerAccounts, err error) { + require.NoError(t, err) + }, + }, + { + name: "OK", + req: &apiv1.QueryParameter{ + PageId: 1, + PageSize: 5, + }, + build: func(store *mock.MockStore) { + account := db.Provisioner{ + ClientID: clientId, + NodeAttestation: []string{"AWS_IID"}, + } + store.EXPECT().ListProvisionerAccounts(gomock.Any(), param).Times(1).Return([]*db.Provisioner{&account}, nil) + store.EXPECT().GetInstanceIdentityDocument(gomock.Any(), clientId).Times(1).Return(&db.AwsAttestation{}, nil) + }, + check: func(t *testing.T, res *apiv1.ProvisionerAccounts, err error) { + require.NoError(t, err) + }, + }, + { + name: "DB_ERROR_LIST_PROVISIONER_ACCOUNT", + req: &apiv1.QueryParameter{ + PageId: 1, + PageSize: 5, + }, + build: func(store *mock.MockStore) { + store.EXPECT().ListProvisionerAccounts(gomock.Any(), param).Times(1).Return(nil, fmt.Errorf("list provisioner account error")) + }, + check: func(t *testing.T, res *apiv1.ProvisionerAccounts, err error) { + require.Error(t, err) + require.Empty(t, res) + require.EqualError(t, err, "rpc error: code = Internal desc = internal server error") + }, + }, + { + name: "DB_ERROR_GET_INSTANCE_IDENTITY_DOCUMENT", + req: &apiv1.QueryParameter{ + PageId: 1, + PageSize: 5, + }, + build: func(store *mock.MockStore) { + account := db.Provisioner{ + ClientID: clientId, + NodeAttestation: []string{"AWS_IID"}, + } + store.EXPECT().ListProvisionerAccounts(gomock.Any(), param).Times(1).Return([]*db.Provisioner{&account}, nil) + store.EXPECT().GetInstanceIdentityDocument(gomock.Any(), clientId).Times(1).Return(nil, fmt.Errorf("get instance identity document error")) + }, + check: func(t *testing.T, res *apiv1.ProvisionerAccounts, err error) { + require.Error(t, err) + require.Empty(t, res) + require.EqualError(t, err, "rpc error: code = Internal desc = internal server error") + }, + }, + } + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + store := mock.NewMockStore(ctrl) + + for elem := range cases { + tc := cases[elem] + + t.Run(tc.name, func(t *testing.T) { + tc.build(store) + + c, err := buildAccountsConfig(store) + require.NoError(t, err) + + res, err := c.ListProvisionerAccounts(context.Background(), tc.req) + tc.check(t, res, err) + }) + } +} + +func TestGetProvisionerAccount(t *testing.T) { + clientId, err := uuid.NewRandom() + require.NoError(t, err) + + cases := []struct { + name string + req *apiv1.AccountId + build func(store *mock.MockStore) + check func(t *testing.T, res *apiv1.ProvisionerAccount, err error) + }{ + { + name: "OK_NO_ATTESTATION", + req: &apiv1.AccountId{ + Uuid: clientId.String(), + }, + build: func(store *mock.MockStore) { + store.EXPECT().GetProvisionerUUID(gomock.Any(), clientId).Times(1).Return(&db.Provisioner{}, nil) + }, + check: func(t *testing.T, res *apiv1.ProvisionerAccount, err error) { + require.NoError(t, err) + }, + }, + { + name: "OK", + req: &apiv1.AccountId{ + Uuid: clientId.String(), + }, + build: func(store *mock.MockStore) { + account := &db.Provisioner{ + ClientID: clientId, + NodeAttestation: []string{"AWS_IID"}, + } + + store.EXPECT().GetProvisionerUUID(gomock.Any(), clientId).Times(1).Return(account, nil) + store.EXPECT().GetInstanceIdentityDocument(gomock.Any(), clientId).Times(1).Return(&db.AwsAttestation{}, nil) + }, + check: func(t *testing.T, res *apiv1.ProvisionerAccount, err error) { + require.NoError(t, err) + require.NotEmpty(t, res) + }, + }, + { + name: "DB_ERROR_GET_SERVICE_UUID", + req: &apiv1.AccountId{ + Uuid: clientId.String(), + }, + build: func(store *mock.MockStore) { + store.EXPECT().GetProvisionerUUID(gomock.Any(), clientId).Times(1).Return(nil, fmt.Errorf("get service uuid error")) + }, + check: func(t *testing.T, res *apiv1.ProvisionerAccount, err error) { + require.Error(t, err) + require.EqualError(t, err, "rpc error: code = Internal desc = internal server error") + require.Empty(t, res) + }, + }, + { + name: "DB_ERROR_GET_INSTANCE_IDENTITY_DOCUMENT", + req: &apiv1.AccountId{ + Uuid: clientId.String(), + }, + build: func(store *mock.MockStore) { + account := &db.Provisioner{ + ClientID: clientId, + NodeAttestation: []string{"AWS_IID"}, + } + store.EXPECT().GetProvisionerUUID(gomock.Any(), clientId).Times(1).Return(account, nil) + store.EXPECT().GetInstanceIdentityDocument(gomock.Any(), clientId).Times(1).Return(nil, fmt.Errorf("get instance identity document")) + }, + check: func(t *testing.T, res *apiv1.ProvisionerAccount, err error) { + require.Error(t, err) + require.EqualError(t, err, "rpc error: code = Internal desc = internal server error") + require.Empty(t, res) + }, + }, + } + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + store := mock.NewMockStore(ctrl) + + for elem := range cases { + tc := cases[elem] + + t.Run(tc.name, func(t *testing.T) { + tc.build(store) + + c, err := buildAccountsConfig(store) + require.NoError(t, err) + + res, err := c.GetProvisionerAccount(context.Background(), tc.req) + tc.check(t, res, err) + }) + } +} diff --git a/internal/v1/accounts/service.go b/internal/v1/accounts/service.go new file mode 100644 index 0000000..ab782d0 --- /dev/null +++ b/internal/v1/accounts/service.go @@ -0,0 +1,174 @@ +package accounts + +import ( + "context" + "database/sql" + "fmt" + "time" + + db "github.com/coinbase/baseca/db/sqlc" + apiv1 "github.com/coinbase/baseca/gen/go/baseca/v1" + "github.com/coinbase/baseca/internal/attestation/aws_iid" + lib "github.com/coinbase/baseca/internal/lib/authentication" + "github.com/coinbase/baseca/internal/lib/util/validator" + "github.com/coinbase/baseca/internal/logger" + "github.com/coinbase/baseca/internal/types" + "github.com/gogo/status" + "github.com/google/uuid" + "google.golang.org/grpc/codes" + "google.golang.org/protobuf/types/known/timestamppb" +) + +func (s *Service) CreateServiceAccount(ctx context.Context, req *apiv1.CreateServiceAccountRequest) (*apiv1.CreateServiceAccountResponse, error) { + var service *db.Account + nodeAttestation := []string{} + + subject_alternative_names := validator.SanitizeInput(req.SubjectAlternativeNames) + certificate_authorities := validator.SanitizeInput(req.CertificateAuthorities) + + err := s.validateCertificateParameters(req.CertificateAuthorities, req.Environment, int16(req.CertificateValidity), req.SubordinateCa) + if err != nil { + return nil, logger.RpcError(status.Error(codes.InvalidArgument, err.Error()), err) + } + + // Validate Subject Alternate Name and Regular Expression + err = s.validateSanInputServiceAccount(ctx, req.ServiceAccount, req.Environment, req.SubjectAlternativeNames, req.RegularExpression) + if err != nil { + return nil, logger.RpcError(status.Error(codes.InvalidArgument, err.Error()), err) + } + + if _, ok := types.CertificateRequestExtension[req.ExtendedKey]; !ok { + return nil, logger.RpcError(status.Error(codes.InvalidArgument, "invalid extended key"), fmt.Errorf("invalid key extension [%s]", req.ExtendedKey)) + } + + if ok := validator.ValidateEmail(req.Email); !ok { + return nil, logger.RpcError(status.Error(codes.InvalidArgument, "invalid email"), fmt.Errorf("invalid email [%s]", req.Email)) + } + + if len(req.Team) == 0 { + return nil, logger.RpcError(status.Error(codes.InvalidArgument, "invalid team parameter"), fmt.Errorf("invalid team [%s]", req.Team)) + } + + client_id, err := uuid.NewRandom() + if err != nil { + return nil, logger.RpcError(status.Error(codes.Internal, "internal server error"), err) + } + + clientToken, err := lib.GenerateClientToken(32) + if err != nil { + return nil, logger.RpcError(status.Error(codes.Internal, "internal server error"), err) + } + + hashedClientToken, err := lib.HashPassword(clientToken) + if err != nil { + return nil, logger.RpcError(status.Error(codes.Internal, "internal server error"), err) + } + + payload, ok := ctx.Value(types.UserAuthenticationContextKey).(*lib.Claims) + if !ok { + return nil, logger.RpcError(status.Error(codes.Internal, "internal server error"), fmt.Errorf("service auth context missing")) + } + + // Production Service Accounts Require Attestation + if req.Environment == _production || req.NodeAttestation != nil { + if err = validateNodeAttestation(req.NodeAttestation); err != nil { + return nil, logger.RpcError(status.Error(codes.InvalidArgument, err.Error()), err) + } + } + + if req.NodeAttestation != nil { + nodeAttestation = aws_iid.GetNodeAttestation(req.NodeAttestation) + + account_arg := db.CreateServiceAccountParams{ + ClientID: client_id, + ApiToken: hashedClientToken, + ServiceAccount: req.ServiceAccount, + Environment: req.Environment, + NodeAttestation: nodeAttestation, + Team: req.Team, + Email: req.Email, + ValidSubjectAlternateName: subject_alternative_names, + ValidCertificateAuthorities: certificate_authorities, + ExtendedKey: req.ExtendedKey, + CertificateValidity: int16(req.CertificateValidity), + SubordinateCa: req.SubordinateCa, + Provisioned: false, + CreatedBy: payload.Subject, + CreatedAt: time.Now().UTC(), + } + + if req.RegularExpression != nil { + account_arg.RegularExpression = sql.NullString{String: *req.RegularExpression, Valid: req.RegularExpression != nil} + } + + raw_message, err := validator.MapToNullRawMessage(req.NodeAttestation.AwsIid.InstanceTags) + if err != nil { + return nil, logger.RpcError(status.Error(codes.Internal, "internal server error"), err) + } + + iid_arg := db.StoreInstanceIdentityDocumentParams{ + ClientID: client_id, + RoleArn: sql.NullString{String: req.NodeAttestation.AwsIid.RoleArn, Valid: len(req.NodeAttestation.AwsIid.RoleArn) != 0}, + AssumeRole: sql.NullString{String: req.NodeAttestation.AwsIid.AssumeRole, Valid: len(req.NodeAttestation.AwsIid.AssumeRole) != 0}, + Region: sql.NullString{String: req.NodeAttestation.AwsIid.Region, Valid: len(req.NodeAttestation.AwsIid.Region) != 0}, + InstanceID: sql.NullString{String: req.NodeAttestation.AwsIid.InstanceId, Valid: len(req.NodeAttestation.AwsIid.InstanceId) != 0}, + ImageID: sql.NullString{String: req.NodeAttestation.AwsIid.ImageId, Valid: len(req.NodeAttestation.AwsIid.ImageId) != 0}, + SecurityGroupID: req.NodeAttestation.AwsIid.SecurityGroups, + InstanceTags: raw_message, + } + + service, err = s.store.Writer.TxCreateServiceAccount(ctx, account_arg, iid_arg) + if err != nil { + return nil, logger.RpcError(status.Error(codes.Internal, "error creating account"), err) + } + } else { + account_arg := db.CreateServiceAccountParams{ + ClientID: client_id, + ApiToken: hashedClientToken, + ServiceAccount: req.ServiceAccount, + Environment: req.Environment, + NodeAttestation: nodeAttestation, + Team: req.Team, + Email: req.Email, + ValidSubjectAlternateName: subject_alternative_names, + ValidCertificateAuthorities: certificate_authorities, + ExtendedKey: req.ExtendedKey, + CertificateValidity: int16(req.CertificateValidity), + SubordinateCa: req.SubordinateCa, + CreatedBy: payload.Subject, + CreatedAt: time.Now().UTC(), + } + + if req.RegularExpression != nil { + account_arg.RegularExpression = sql.NullString{String: *req.RegularExpression, Valid: req.RegularExpression != nil} + } + + service, err = s.store.Writer.CreateServiceAccount(ctx, account_arg) + if err != nil { + return nil, logger.RpcError(status.Error(codes.Internal, "error creating account"), err) + } + } + + account := apiv1.CreateServiceAccountResponse{ + ClientId: service.ClientID.String(), + ClientToken: clientToken, + ServiceAccount: service.ServiceAccount, + Environment: service.Environment, + NodeAttestation: req.NodeAttestation, + SubjectAlternativeNames: service.ValidSubjectAlternateName, + ExtendedKey: service.ExtendedKey, + CertificateAuthorities: service.ValidCertificateAuthorities, + CertificateValidity: int32(service.CertificateValidity), + SubordinateCa: service.SubordinateCa, + Team: service.Team, + Email: service.Email, + CreatedAt: timestamppb.New(service.CreatedAt), + CreatedBy: service.CreatedBy.String(), + } + + if service.RegularExpression.Valid { + account.RegularExpression = service.RegularExpression.String + } + + return &account, nil +} diff --git a/internal/v1/accounts/service_test.go b/internal/v1/accounts/service_test.go new file mode 100644 index 0000000..69c8039 --- /dev/null +++ b/internal/v1/accounts/service_test.go @@ -0,0 +1,265 @@ +package accounts + +import ( + "context" + "database/sql" + "fmt" + "reflect" + "testing" + "time" + + "github.com/coinbase/baseca/db/mock" + db "github.com/coinbase/baseca/db/sqlc" + apiv1 "github.com/coinbase/baseca/gen/go/baseca/v1" + lib "github.com/coinbase/baseca/internal/lib/authentication" + "github.com/coinbase/baseca/internal/types" + "github.com/google/uuid" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" +) + +func TestCreateServiceAccount(t *testing.T) { + id, err := uuid.NewRandom() + require.NoError(t, err) + + authClaim := &lib.Claims{ + Permission: "ADMIN", + Subject: id, + IssuedAt: time.Now().UTC(), + ExpiresAt: time.Now().UTC().AddDate(0, 0, 1), + NotBefore: time.Now().UTC(), + } + + attestation := apiv1.AWSInstanceIdentityDocument{ + RoleArn: "arn:aws:iam::123456789012:instance-profile/role", + AssumeRole: "arn:aws:iam::123456789012:role/assumed-role", + SecurityGroups: []string{"sg-0123456789abcdef0"}, + Region: "us-east-1", + } + + accountParam := db.CreateServiceAccountParams{ + ServiceAccount: "example", + Environment: "sandbox", + ValidSubjectAlternateName: []string{"sandbox.example.com"}, + ExtendedKey: "EndEntityServerAuthCertificate", + NodeAttestation: []string{"AWS_IID"}, + CertificateValidity: 30, + ValidCertificateAuthorities: []string{"sandbox_use1"}, + SubordinateCa: "infrastructure", + Provisioned: false, + Team: "Infrastructure Security", + Email: "security@coinbase.com", + CreatedBy: id, + } + + attestationParam := db.StoreInstanceIdentityDocumentParams{ + RoleArn: sql.NullString{String: attestation.RoleArn, Valid: true}, + AssumeRole: sql.NullString{String: attestation.AssumeRole, Valid: true}, + SecurityGroupID: attestation.SecurityGroups, + Region: sql.NullString{String: attestation.Region, Valid: true}, + } + + request := apiv1.CreateServiceAccountRequest{ + ServiceAccount: "example", + Environment: "sandbox", + SubjectAlternativeNames: []string{"sandbox.example.com"}, + ExtendedKey: "EndEntityServerAuthCertificate", + NodeAttestation: &apiv1.NodeAttestation{ + AwsIid: &attestation, + }, + CertificateAuthorities: []string{"sandbox_use1"}, + SubordinateCa: "infrastructure", + CertificateValidity: 30, + Team: "Infrastructure Security", + Email: "security@coinbase.com", + } + + cases := []struct { + name string + req *apiv1.CreateServiceAccountRequest + build func(store *mock.MockStore) + ctx context.Context + check func(t *testing.T, res *apiv1.CreateServiceAccountResponse, err error) + }{ + { + name: "OK", + req: &request, + build: func(store *mock.MockStore) { + store.EXPECT().ListServiceAccounts(gomock.Any(), gomock.Any()).Times(1).Return([]*db.Account{}, nil) + store.EXPECT().TxCreateServiceAccount( + gomock.Any(), + EqCreateServiceAccountParams(accountParam, "account arg matcher"), + EqStoreInstanceIdentityDocumentParams(attestationParam, "iid arg matcher"), + ).Times(1).Return(&db.Account{}, nil) + }, + ctx: context.WithValue(context.Background(), types.UserAuthenticationContextKey, authClaim), + check: func(t *testing.T, res *apiv1.CreateServiceAccountResponse, err error) { + require.NoError(t, err) + }, + }, + { + name: "OK_NO_ATTESTATION", + req: &apiv1.CreateServiceAccountRequest{ + ServiceAccount: "example", + Environment: "sandbox", + SubjectAlternativeNames: []string{"sandbox.example.com"}, + ExtendedKey: "EndEntityServerAuthCertificate", + CertificateAuthorities: []string{"sandbox_use1"}, + SubordinateCa: "infrastructure", + CertificateValidity: 30, + Team: "Infrastructure Security", + Email: "security@coinbase.com", + }, + build: func(store *mock.MockStore) { + account_arg := db.CreateServiceAccountParams{ + ServiceAccount: "example", + Environment: "sandbox", + ValidSubjectAlternateName: []string{"sandbox.example.com"}, + ExtendedKey: "EndEntityServerAuthCertificate", + CertificateValidity: 30, + ValidCertificateAuthorities: []string{"sandbox_use1"}, + SubordinateCa: "infrastructure", + Team: "Infrastructure Security", + Email: "security@coinbase.com", + CreatedBy: authClaim.Subject, + NodeAttestation: []string{}, + } + store.EXPECT().ListServiceAccounts(gomock.Any(), gomock.Any()).Times(1).Return([]*db.Account{}, nil) + store.EXPECT().CreateServiceAccount( + gomock.Any(), + EqCreateServiceAccountParams(account_arg, "account arg matcher"), + ).Times(1).Return(&db.Account{}, nil) + }, + ctx: context.WithValue(context.Background(), types.UserAuthenticationContextKey, authClaim), + check: func(t *testing.T, res *apiv1.CreateServiceAccountResponse, err error) { + require.NoError(t, err) + }, + }, + { + name: "OK_WILDCARD", + req: &apiv1.CreateServiceAccountRequest{ + ServiceAccount: "example", + Environment: "sandbox", + SubjectAlternativeNames: []string{"*.example.com"}, + ExtendedKey: "EndEntityServerAuthCertificate", + CertificateAuthorities: []string{"sandbox_use1"}, + SubordinateCa: "infrastructure", + CertificateValidity: 30, + Team: "Infrastructure Security", + Email: "security@coinbase.com", + }, + build: func(store *mock.MockStore) { + account_arg := db.CreateServiceAccountParams{ + ServiceAccount: "example", + Environment: "sandbox", + ValidSubjectAlternateName: []string{"*.example.com"}, + ExtendedKey: "EndEntityServerAuthCertificate", + CertificateValidity: 30, + ValidCertificateAuthorities: []string{"sandbox_use1"}, + SubordinateCa: "infrastructure", + Team: "Infrastructure Security", + Email: "security@coinbase.com", + CreatedBy: authClaim.Subject, + NodeAttestation: []string{}, + } + store.EXPECT().ListServiceAccounts(gomock.Any(), gomock.Any()).Times(1).Return([]*db.Account{}, nil) + store.EXPECT().CreateServiceAccount( + gomock.Any(), + EqCreateServiceAccountParams(account_arg, "account arg matcher"), + ).Times(1).Return(&db.Account{}, nil) + }, + ctx: context.WithValue(context.Background(), types.UserAuthenticationContextKey, authClaim), + check: func(t *testing.T, res *apiv1.CreateServiceAccountResponse, err error) { + require.NoError(t, err) + }, + }, + { + name: "ERROR_INVALID_SUBJECT_ALTERNATIVE_NAME", + req: &apiv1.CreateServiceAccountRequest{ + ServiceAccount: "example", + Environment: "sandbox", + SubjectAlternativeNames: []string{"{}.example.com"}, + ExtendedKey: "EndEntityServerAuthCertificate", + CertificateAuthorities: []string{"sandbox_use1"}, + SubordinateCa: "infrastructure", + CertificateValidity: 30, + Team: "Infrastructure Security", + Email: "security@coinbase.com", + }, + build: func(store *mock.MockStore) {}, + ctx: context.WithValue(context.Background(), types.UserAuthenticationContextKey, authClaim), + check: func(t *testing.T, res *apiv1.CreateServiceAccountResponse, err error) { + require.Error(t, err) + }, + }, + } + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + store := mock.NewMockStore(ctrl) + + for elem := range cases { + tc := cases[elem] + + t.Run(tc.name, func(t *testing.T) { + tc.build(store) + c, err := buildAccountsConfig(store) + require.NoError(t, err) + + res, err := c.CreateServiceAccount(tc.ctx, tc.req) + tc.check(t, res, err) + }) + } +} + +type eqCreateServiceAccountParamsMatcher struct { + arg db.CreateServiceAccountParams + password string +} + +func (e eqCreateServiceAccountParamsMatcher) Matches(x any) bool { + arg, ok := x.(db.CreateServiceAccountParams) + if !ok { + return false + } + + e.arg.ClientID = arg.ClientID + e.arg.ApiToken = arg.ApiToken + e.arg.CreatedAt = arg.CreatedAt + return reflect.DeepEqual(e.arg, arg) +} + +func (e eqCreateServiceAccountParamsMatcher) String() string { + return fmt.Sprintf("%v", e.arg) +} + +func EqCreateServiceAccountParams(arg db.CreateServiceAccountParams, password string) gomock.Matcher { + return eqCreateServiceAccountParamsMatcher{arg, password} +} + +type eqStoreInstanceIdentityDocumentParamsMatcher struct { + arg db.StoreInstanceIdentityDocumentParams + password string +} + +func (e eqStoreInstanceIdentityDocumentParamsMatcher) Matches(x any) bool { + arg, ok := x.(db.StoreInstanceIdentityDocumentParams) + if !ok { + return false + } + + e.arg.ClientID = arg.ClientID + e.arg.InstanceID = arg.InstanceID + e.arg.ImageID = arg.ImageID + e.arg.InstanceTags = arg.InstanceTags + return reflect.DeepEqual(e.arg, arg) +} + +func (e eqStoreInstanceIdentityDocumentParamsMatcher) String() string { + return fmt.Sprintf("%v", e.arg) +} + +func EqStoreInstanceIdentityDocumentParams(arg db.StoreInstanceIdentityDocumentParams, password string) gomock.Matcher { + return eqStoreInstanceIdentityDocumentParamsMatcher{arg, password} +} diff --git a/internal/v1/accounts/validate.go b/internal/v1/accounts/validate.go new file mode 100644 index 0000000..1b20f44 --- /dev/null +++ b/internal/v1/accounts/validate.go @@ -0,0 +1,275 @@ +package accounts + +import ( + "context" + "fmt" + "regexp" + + db "github.com/coinbase/baseca/db/sqlc" + apiv1 "github.com/coinbase/baseca/gen/go/baseca/v1" + "github.com/coinbase/baseca/internal/lib/util/validator" +) + +const ( + _role_regex_pattern = `^arn:aws:iam::\d{12}:role\/[a-zA-Z_0-9+=,.@-]+$` + _instance_profile_regex_pattern = `^arn:aws:iam::\d{12}:instance-profile/[a-zA-Z0-9+=,.@\-_/]+` + _sg_regex_pattern = `^sg-[a-zA-Z0-9]+$` + _instance_id_pattern = `^i-[a-fA-F0-9]{8,17}$` +) + +func (s *Service) validateSanInputServiceAccount(ctx context.Context, service_account string, environment string, request_san []string, pattern *string) error { + var page_id = int32(1) + var page_size = int32(25) + var sans [][]string + + if len(service_account) == 0 { + return fmt.Errorf("invalid service_account parameter") + } + + if pattern == nil && len(request_san) == 0 { + return fmt.Errorf("subject_alternative_names and regular_expression cannot both be empty") + } + + if pattern != nil { + _, err := regexp.Compile(*pattern) + if err != nil { + return fmt.Errorf("invalid regular_expressing pattern") + } + } + + for _, fqdn := range request_san { + if !validator.IsValidDomain(fqdn) { + return fmt.Errorf("invalid domain [%s]", fqdn) + } + } + + for { + arg := db.ListServiceAccountsParams{ + Limit: page_size, + Offset: (page_id - 1) * page_size, + } + services, err := s.store.Reader.ListServiceAccounts(ctx, arg) + if err != nil { + return fmt.Errorf("error listing service accounts during san validation: %s", err) + } + if len(services) == 0 { + break + } + for _, elem := range services { + // Do Not Include Service Account with Same Name and Environment + if service_account == elem.ServiceAccount && environment == elem.Environment { + continue + } + + sans = append(sans, elem.ValidSubjectAlternateName) + } + page_id += 1 + } + + for _, san := range sans { + for _, elem := range san { + if validator.Contains(request_san, elem) { + return fmt.Errorf("subject alternative name (san) %s exists in another service account", request_san) + } + } + } + return nil +} + +func (s *Service) validateSanInputProvisionerAccount(ctx context.Context, provisioner_account string, environments []string, request_san []string, pattern string) error { + var page_id = int32(1) + var page_size = int32(25) + var sans [][]string + + if len(provisioner_account) == 0 { + return fmt.Errorf("invalid provisioner_account parameter") + } + + if len(pattern) == 0 && len(request_san) == 0 { + return fmt.Errorf("subject_alternative_names and regular_expression cannot both be empty") + } + + if len(pattern) != 0 { + _, err := regexp.Compile(pattern) + if err != nil { + return fmt.Errorf("invalid regular_expressing pattern") + } + } + + for _, fqdn := range request_san { + if !validator.IsValidDomain(fqdn) { + return fmt.Errorf("invalid domain [%s]", fqdn) + } + } + + for { + arg := db.ListProvisionerAccountsParams{ + Limit: page_size, + Offset: (page_id - 1) * page_size, + } + services, err := s.store.Reader.ListProvisionerAccounts(ctx, arg) + if err != nil { + return fmt.Errorf("error listing provisioner accounts during san validation") + } + if len(services) == 0 { + break + } + for _, elem := range services { + // Do Not Include Provisioner Account with Same Name and Environment + if provisioner_account == elem.ProvisionerAccount { + continue + } + + sans = append(sans, elem.ValidSubjectAlternateNames) + } + page_id += 1 + } + + for _, san := range sans { + for _, elem := range san { + if validator.Contains(request_san, elem) { + return fmt.Errorf("subject alternative name (san) %s exists in another provisioner account", request_san) + } + } + } + return nil +} + +func (s *Service) validateCertificateParameters(certificateAuthorities []string, environment string, certificateValidity int16, subordinateCa string) error { + if _, ok := validator.CertificateAuthorityEnvironments[environment]; !ok { + return fmt.Errorf("invalid environment [%s]", environment) + } + + // Determine Allowed CA within Environment (Service Account Cannot Access CA from Multiple Environments) + if output := validator.ValidateCertificateAuthorityEnvironment(s.environment, environment, certificateAuthorities); !output { + return fmt.Errorf("invalid certificate authorities input %s", certificateAuthorities) + } + + if len(subordinateCa) == 0 { + return fmt.Errorf("invalid subordinate_ca parameter") + } + + // Determine Certificate Validity Greater than CA Validity + if certificateValidity <= 0 { + return fmt.Errorf("invalid certificate_validity parameter") + } + + // Determine Certificate Validity Greater than CA Validity + for _, certificateAuthority := range certificateAuthorities { + caValidity := s.acmConfig[certificateAuthority].CaActiveDay + if caValidity <= int(certificateValidity) { + return fmt.Errorf("certificate expiration [%d] exceeds certificate authority [%s] validity [%d]", certificateValidity, certificateAuthority, caValidity) + } + } + + return nil +} + +func validateNodeAttestation(attestation *apiv1.NodeAttestation) error { + if attestation == nil { + return fmt.Errorf("node_attestation cannot be empty") + } + + if attestation.AwsIid == nil { + return fmt.Errorf("aws_iid cannot be empty") + } + + if err := validateAwsIidMetadata(attestation.AwsIid); err != nil { + return err + } + + return nil +} + +func validateAwsIidMetadata(iid *apiv1.AWSInstanceIdentityDocument) error { + var err bool + var validAttestation = false + + if iid.RoleArn != "" { + err = validateRegularExpression(iid.RoleArn, _instance_profile_regex_pattern) + if err { + return fmt.Errorf("invalid aws_iid instance role arn [%s]", iid.RoleArn) + } + validAttestation = true + } + + if iid.AssumeRole != "" { + err = validateRegularExpression(iid.AssumeRole, _role_regex_pattern) + if err { + return fmt.Errorf("invalid aws_iid assume role arn [%s]", iid.AssumeRole) + } + validAttestation = true + } + + if len(iid.SecurityGroups) != 0 { + for _, sg := range iid.SecurityGroups { + err = validateRegularExpression(sg, _sg_regex_pattern) + if err { + return fmt.Errorf("invalid aws_iid security group id [%s]", sg) + } + } + validAttestation = true + } + + if iid.Region != "" { + validRegion := validAWSRegion(iid.Region) + if !validRegion { + return fmt.Errorf("invalid aws_iid region [%s]", iid.Region) + } + validAttestation = true + } + + if iid.InstanceId != "" { + err = validateRegularExpression(iid.InstanceId, _instance_id_pattern) + if err { + return fmt.Errorf("invalid aws_iid instance id [%s]", iid.InstanceId) + } + validAttestation = true + } + + if !validAttestation { + return fmt.Errorf("aws_iid attestation empty") + } + + return nil +} + +func validateRegularExpression(input string, pattern string) bool { + regex, err := regexp.Compile(pattern) + if err != nil { + return true + } + + if regex.MatchString(input) { + return false + } + return true +} + +func validAWSRegion(region string) bool { + validRegions := map[string]bool{ + "us-east-1": true, + "us-east-2": true, + "us-west-1": true, + "us-west-2": true, + "af-south-1": true, + "ap-east-1": true, + "ap-south-1": true, + "ap-northeast-3": true, + "ap-northeast-2": true, + "ap-southeast-1": true, + "ap-southeast-2": true, + "ap-northeast-1": true, + "ca-central-1": true, + "eu-central-1": true, + "eu-west-1": true, + "eu-west-2": true, + "eu-south-1": true, + "eu-west-3": true, + "eu-north-1": true, + "me-south-1": true, + "sa-east-1": true, + } + _, isValid := validRegions[region] + return isValid +} diff --git a/internal/v1/certificate/certificate.go b/internal/v1/certificate/certificate.go new file mode 100644 index 0000000..6e6a4bd --- /dev/null +++ b/internal/v1/certificate/certificate.go @@ -0,0 +1,45 @@ +package certificate + +import ( + "fmt" + + db "github.com/coinbase/baseca/db/sqlc" + apiv1 "github.com/coinbase/baseca/gen/go/baseca/v1" + "github.com/coinbase/baseca/internal/client/acmpca" + "github.com/coinbase/baseca/internal/client/firehose" + "github.com/coinbase/baseca/internal/client/redis" + "github.com/coinbase/baseca/internal/config" +) + +type Certificate struct { + apiv1.CertificateServer + store db.DatabaseEndpoints + acmConfig map[string]config.SubordinateCertificate + ca config.SubordinateCertificateAuthority + ocsp []string + environment config.Stage + redis *redis.RedisClient + firehose *firehose.FirehoseClient + pca *acmpca.PrivateCaClient +} + +func New(cfg *config.Config, endpoints db.DatabaseEndpoints) (*Certificate, error) { + redisClient, err := redis.NewRedisClient(cfg) + if err != nil { + return nil, err + } + firehoseClient, err := firehose.NewFirehoseClient(cfg) + if err != nil { + return nil, fmt.Errorf("error instantiating firehose client [%s]", err) + } + + return &Certificate{ + store: endpoints, + acmConfig: cfg.ACMPCA, + ca: cfg.SubordinateMetadata, + ocsp: cfg.OCSPServer, + environment: cfg.Environment, + redis: redisClient, + firehose: firehoseClient, + }, nil +} diff --git a/internal/v1/certificate/certificate_test.go b/internal/v1/certificate/certificate_test.go new file mode 100644 index 0000000..9bab8c5 --- /dev/null +++ b/internal/v1/certificate/certificate_test.go @@ -0,0 +1,206 @@ +package certificate + +import ( + "context" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "math/big" + "time" + + "github.com/aws/aws-sdk-go-v2/service/acmpca" + firehose_v2 "github.com/aws/aws-sdk-go-v2/service/firehose" + "github.com/aws/aws-sdk-go/aws" + mock_store "github.com/coinbase/baseca/db/mock" + db "github.com/coinbase/baseca/db/sqlc" + acm_pca "github.com/coinbase/baseca/internal/client/acmpca" + "github.com/coinbase/baseca/internal/client/firehose" + redis_client "github.com/coinbase/baseca/internal/client/redis" + "github.com/coinbase/baseca/internal/lib/util/validator" + "github.com/coinbase/baseca/test" + "github.com/go-redis/redis/v8" + "github.com/stretchr/testify/mock" +) + +var csr *acmpca.IssueCertificateInput +var rootCrt string + +var pk *rsa.PrivateKey +var root []byte +var template *x509.Certificate + +type mockedRedisClient struct { + mock.Mock +} + +func (m *mockedRedisClient) HIncrBy(ctx context.Context, key, field string, incr int64) *redis.IntCmd { + ret := m.Called(ctx, key, field, incr) + return ret.Get(0).(*redis.IntCmd) +} + +func (m *mockedRedisClient) HGetAll(ctx context.Context, key string) *redis.StringStringMapCmd { + ret := m.Called(ctx, key) + return ret.Get(0).(*redis.StringStringMapCmd) +} + +func (m *mockedRedisClient) HDel(ctx context.Context, key string, fields ...string) *redis.IntCmd { + ret := m.Called(ctx, key, fields) + return ret.Get(0).(*redis.IntCmd) +} + +func (m *mockedRedisClient) Expire(ctx context.Context, key string, expiration time.Duration) *redis.BoolCmd { + ret := m.Called(ctx, key, expiration) + return ret.Get(0).(*redis.BoolCmd) +} + +type mockedFirehoseClient struct { + mock.Mock +} + +func (m *mockedFirehoseClient) PutRecord(ctx context.Context, params *firehose_v2.PutRecordInput, optFns ...func(*firehose_v2.Options)) (*firehose_v2.PutRecordOutput, error) { + ret := m.Called(ctx, params) + return ret.Get(0).(*firehose_v2.PutRecordOutput), ret.Error(1) +} + +type mockedPrivateCaClient struct { + mock.Mock +} + +func (m *mockedPrivateCaClient) IssueCertificate(ctx context.Context, params *acmpca.IssueCertificateInput, optFns ...func(*acmpca.Options)) (*acmpca.IssueCertificateOutput, error) { + ret := m.Called(ctx, params, optFns) + return ret.Get(0).(*acmpca.IssueCertificateOutput), ret.Error(1) +} + +func (m *mockedPrivateCaClient) GetCertificate(ctx context.Context, params *acmpca.GetCertificateInput, optFns ...func(*acmpca.Options)) (*acmpca.GetCertificateOutput, error) { + certificate, _ := convertX509toString(root) + rootCrt = certificate.String() + + block, _ := pem.Decode(csr.Csr) + req, _ := x509.ParseCertificateRequest(block.Bytes) + + certTemplate := x509.Certificate{ + SerialNumber: big.NewInt(2), + Subject: req.Subject, + NotBefore: time.Now().UTC(), + NotAfter: time.Now().UTC().Add(60 * 24 * time.Hour), + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, + } + + certDER, _ := x509.CreateCertificate(rand.Reader, &certTemplate, template, req.PublicKey, pk) + cert, _ := convertX509toString(certDER) + c := cert.String() + crtOutput := &c + + certOutput := &acmpca.GetCertificateOutput{ + Certificate: crtOutput, + CertificateChain: crtOutput, + } + + return certOutput, nil +} + +func (m *mockedPrivateCaClient) RevokeCertificate(ctx context.Context, params *acmpca.RevokeCertificateInput, optFns ...func(*acmpca.Options)) (*acmpca.RevokeCertificateOutput, error) { + ret := m.Called(ctx, params, optFns) + return ret.Get(0).(*acmpca.RevokeCertificateOutput), ret.Error(1) +} + +func (m *mockedPrivateCaClient) GetCertificateAuthorityCertificate(ctx context.Context, params *acmpca.GetCertificateAuthorityCertificateInput, optFns ...func(*acmpca.Options)) (*acmpca.GetCertificateAuthorityCertificateOutput, error) { + ret := m.Called(ctx, params, optFns) + return ret.Get(0).(*acmpca.GetCertificateAuthorityCertificateOutput), ret.Error(1) +} + +func buildCertificateConfig(store *mock_store.MockStore) (*Certificate, error) { + config, err := test.GetTestConfigurationPath() + if err != nil { + return nil, err + } + + validator.SupportedConfig(config) + validator.SetBaseDirectory(config) + validator.SupportedEnvironments(config) + + endpoints := db.DatabaseEndpoints{Writer: store, Reader: store} + redisConfig := &config.Redis + mockRedis := &mockedRedisClient{} + + mockRedis.On("HIncrBy", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(redis.NewIntCmd(context.Background(), "example.com", time.Now().UTC(), int64(10))) + + mockRedis.On("HGetAll", mock.Anything, mock.Anything). + Return(redis.NewStringStringMapCmd(context.Background(), map[string]string{"example.com": time.Now().UTC().String()})) + + mockRedis.On("Expire") + mockRedis.On("HDel") + + redisClient := redis_client.RedisClient{ + Client: mockRedis, + Config: redisConfig, + Limit: redisConfig.RateLimit, + Period: time.Duration(redisConfig.Period) * time.Minute, + Window: time.Duration(redisConfig.Duration) * time.Minute, + } + + mockFirehose := &mockedFirehoseClient{} + mockFirehose.On("PutRecord", mock.Anything, mock.Anything, mock.Anything). + Return(&firehose_v2.PutRecordOutput{ + RecordId: aws.String(mock.Anything), + Encrypted: aws.Bool(true), + }, nil) + + firehoseClient := firehose.FirehoseClient{ + DataStream: config.Firehose.Stream, + Service: mockFirehose, + } + + mockPrivateCa := &mockedPrivateCaClient{} + mockPrivateCa.On("IssueCertificate", mock.Anything, mock.Anything, mock.Anything).Run(func(args mock.Arguments) { + certificateInput := args.Get(1) + csr, _ = certificateInput.(*acmpca.IssueCertificateInput) + }).Return(&acmpca.IssueCertificateOutput{ + CertificateArn: aws.String(mock.Anything), + }, nil) + + mockPrivateCa.On("GetCertificate", mock.Anything, mock.Anything, mock.Anything).Return(nil) + + mockPrivateCa.On("GetCertificateAuthorityCertificate", mock.Anything, mock.Anything, mock.Anything). + Return(&acmpca.GetCertificateAuthorityCertificateOutput{ + Certificate: &rootCrt, + CertificateChain: &rootCrt, + }, nil) + + privateCaClient := acm_pca.PrivateCaClient{ + Client: mockPrivateCa, + } + + pk, template, root = mockRootCertificateAuthority() + + return &Certificate{ + store: endpoints, + acmConfig: config.ACMPCA, + ca: config.SubordinateMetadata, + environment: config.Environment, + redis: &redisClient, + firehose: &firehoseClient, + pca: &privateCaClient, + }, nil +} + +func mockRootCertificateAuthority() (*rsa.PrivateKey, *x509.Certificate, []byte) { + rootKey, _ := rsa.GenerateKey(rand.Reader, 2048) + rootTemplate := x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{ + Organization: []string{"Unit Test Root CA"}, + }, + NotBefore: time.Now(), + NotAfter: time.Now().Add(365 * 24 * time.Hour), + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, + BasicConstraintsValid: true, + IsCA: true, + } + + rootCertDER, _ := x509.CreateCertificate(rand.Reader, &rootTemplate, &rootTemplate, &rootKey.PublicKey, rootKey) + return rootKey, &rootTemplate, rootCertDER +} diff --git a/internal/v1/certificate/operations.go b/internal/v1/certificate/operations.go new file mode 100644 index 0000000..c31a5e8 --- /dev/null +++ b/internal/v1/certificate/operations.go @@ -0,0 +1,215 @@ +package certificate + +import ( + "context" + "database/sql" + "errors" + "fmt" + "time" + + pca_types "github.com/aws/aws-sdk-go-v2/service/acmpca/types" + db "github.com/coinbase/baseca/db/sqlc" + apiv1 "github.com/coinbase/baseca/gen/go/baseca/v1" + "github.com/coinbase/baseca/internal/client/acmpca" + lib "github.com/coinbase/baseca/internal/lib/authentication" + "github.com/coinbase/baseca/internal/lib/util/validator" + "github.com/coinbase/baseca/internal/logger" + "github.com/coinbase/baseca/internal/types" + "github.com/gogo/status" + "google.golang.org/grpc/codes" + "google.golang.org/protobuf/types/known/timestamppb" +) + +var ( + _revocationReason = []string{ + "AFFILIATION_CHANGED", + "CESSATION_OF_OPERATION", + "A_A_COMPROMISE", + "PRIVILEGE_WITHDRAWN", + "SUPERSEDED", + "UNSPECIFIED", + "KEY_COMPROMISE", + "CERTIFICATE_AUTHORITY_COMPROMISE", + } + + _default_signing_algorithm = "SHA512WITHRSA" + _default_validity = 365 +) + +func (c *Certificate) RevokeCertificate(ctx context.Context, req *apiv1.RevokeCertificateRequest) (*apiv1.RevokeCertificateResponse, error) { + var parameters types.CertificateParameters + var processed *pca_types.RequestAlreadyProcessedException + + if _, ok := ctx.Value(types.UserAuthenticationContextKey).(*lib.Claims); !ok { + return nil, status.Error(codes.InvalidArgument, "authentication error") + } + uuid := ctx.Value(types.UserAuthenticationContextKey).(*lib.Claims).Subject + + certificate, err := c.store.Reader.GetCertificate(ctx, req.SerialNumber) + if err != nil { + return nil, logger.RpcError(status.Error(codes.Internal, "internal server error"), err) + } + + for _, ca := range c.acmConfig { + if ca.CaArn == certificate.CertificateAuthorityArn.String { + parameters = types.CertificateParameters{ + Region: ca.Region, + CaArn: ca.CaArn, + AssumeRole: ca.AssumeRole, + RoleArn: ca.RoleArn, + Validity: ca.CaActiveDay, + } + } + } + + // c.pca Mock Private CA + if c.pca == nil { + client, err := acmpca.NewPrivateCaClient(parameters) + if err != nil { + return nil, logger.RpcError(status.Error(codes.Internal, "acm pca client error"), err) + } + c.pca = client + } + + if !validator.Contains(_revocationReason, req.RevocationReason) { + return nil, logger.RpcError(status.Error(codes.InvalidArgument, "invalid revocation"), fmt.Errorf("%s invalid revocation parameter", req.RevocationReason)) + } + _, err = c.pca.RevokeCertificate(certificate.CertificateAuthorityArn.String, certificate.SerialNumber, req.RevocationReason) + if err != nil { + if errors.As(err, &processed) { + return nil, logger.RpcError(status.Error(codes.InvalidArgument, "certificate already revoked"), err) + } + return nil, logger.RpcError(status.Error(codes.Internal, "internal server error"), err) + } + + arg := db.RevokeIssuedCertificateSerialNumberParams{ + SerialNumber: req.SerialNumber, + RevokeDate: sql.NullTime{Time: time.Now().UTC(), Valid: true}, + RevokedBy: sql.NullString{String: uuid.String(), Valid: true}, + } + + err = c.store.Writer.RevokeIssuedCertificateSerialNumber(ctx, arg) + if err != nil { + return nil, logger.RpcError(status.Error(codes.Internal, "internal server error"), err) + } + + return &apiv1.RevokeCertificateResponse{ + SerialNumber: req.SerialNumber, + RevocationDate: timestamppb.New(arg.RevokeDate.Time), + Status: req.RevocationReason, + }, nil +} + +func (c *Certificate) OperationsSignCSR(ctx context.Context, req *apiv1.OperationsSignRequest) (*apiv1.SignedCertificate, error) { + var certificateAuthorityParameters *apiv1.CertificateAuthorityParameter + + if err := c.validateCsrParameters(req); err != nil { + return nil, logger.RpcError(status.Error(codes.InvalidArgument, "invalid certificate signing request (csr)"), err) + } + + if _, ok := types.CertificateRequestExtension[req.ExtendedKey]; !ok { + return nil, logger.RpcError(status.Error(codes.InvalidArgument, "invalid certificate extended key"), fmt.Errorf("invalid certificate extended key")) + } + + // Query Account Metadata to Discover Upstream Private CA + if req.CertificateAuthority == nil { + arg := db.GetServiceAccountByMetadataParams{ + ServiceAccount: req.ServiceAccount, + Environment: req.Environment, + ExtendedKey: req.ExtendedKey, + } + + serviceAccount, err := c.store.Reader.GetServiceAccountByMetadata(ctx, arg) + if err != nil { + return nil, logger.RpcError(status.Error(codes.Internal, "error issuing certificate [OperationsSignCSR]"), fmt.Errorf("error querying service account metadata [%s]: %s", req.ServiceAccount, err)) + } + + if len(serviceAccount) != 1 { + return nil, logger.RpcError(status.Error(codes.Internal, "error querying service account(s)"), fmt.Errorf("[%d] service accounts found for [%s] in environment [%s], cleanup before issuing long-lived certificate", len(serviceAccount), req.ServiceAccount, req.Environment)) + } + + accountMetadata := serviceAccount[0] + certificateAuthority := accountMetadata.ValidCertificateAuthorities[0] + certificateAuthorityParameters = &apiv1.CertificateAuthorityParameter{ + Region: c.acmConfig[certificateAuthority].Region, + CaArn: c.acmConfig[certificateAuthority].CaArn, + AssumeRole: c.acmConfig[certificateAuthority].AssumeRole, + RoleArn: c.acmConfig[certificateAuthority].RoleArn, + Validity: int32(_default_validity), + SignAlgorithm: _default_signing_algorithm, + } + } else { + expirationDate := time.Now().UTC().AddDate(0, 0, int(req.CertificateAuthority.Validity)).UTC() + if expirationDate.Before(time.Now().UTC().Add(time.Minute).UTC()) { + return nil, logger.RpcError(status.Error(codes.Internal, "invalid certificate validity"), fmt.Errorf("invalid certificate validity [%s]", req.ServiceAccount)) + } + + certificateAuthorityParameters = &apiv1.CertificateAuthorityParameter{ + Region: req.CertificateAuthority.Region, + CaArn: req.CertificateAuthority.CaArn, + AssumeRole: req.CertificateAuthority.AssumeRole, + RoleArn: req.CertificateAuthority.RoleArn, + Validity: int32(req.CertificateAuthority.Validity), + SignAlgorithm: req.CertificateAuthority.SignAlgorithm, + } + } + // c.pca Mock Private CA + if c.pca == nil { + client, err := acmpca.NewPrivateCaClient(types.CertificateParameters{ + AssumeRole: certificateAuthorityParameters.AssumeRole, + RoleArn: certificateAuthorityParameters.RoleArn, + Region: certificateAuthorityParameters.Region, + }) + if err != nil { + return nil, logger.RpcError(status.Error(codes.InvalidArgument, "invalid parameters for acm pca client"), err) + } + c.pca = client + } + + certificate, err := c.pca.IssueCertificateFromTemplate(certificateAuthorityParameters, []byte(req.CertificateSigningRequest), types.CertificateRequestExtension[req.ExtendedKey].TemplateArn) + if err != nil { + return nil, logger.RpcError(status.Error(codes.Internal, "error issuing certificate"), err) + } + + // Only Common Name (CN) Exists in CSR + if len(certificate.DNSNames) == 0 { + certificate.DNSNames = append(certificate.DNSNames, certificate.Subject.CommonName) + } + + arg := db.LogCertificateParams{ + SerialNumber: certificate.SerialNumber.Text(16), + Account: req.ServiceAccount, + Environment: req.Environment, + ExtendedKey: req.ExtendedKey, + CommonName: certificate.Subject.CommonName, + SubjectAlternativeName: certificate.DNSNames, + ExpirationDate: certificate.NotAfter.UTC(), + IssuedDate: certificate.NotBefore.UTC(), + CertificateAuthorityArn: sql.NullString{String: certificateAuthorityParameters.CaArn, Valid: len(certificateAuthorityParameters.CaArn) != 0}, + } + + metadata, err := c.store.Writer.LogCertificate(ctx, arg) + if err != nil { + return nil, logger.RpcError(status.Error(codes.Internal, "internal server error"), err) + } + + signedCertificate, err := convertX509toString(certificate.Raw) + if err != nil { + return nil, logger.RpcError(status.Error(codes.Internal, "internal server error"), err) + } + + return &apiv1.SignedCertificate{ + Certificate: signedCertificate.String(), + Metadata: &apiv1.CertificateParameter{ + SerialNumber: metadata.SerialNumber, + Account: metadata.Account, + Environment: metadata.Environment, + ExtendedKey: metadata.ExtendedKey, + CommonName: metadata.CommonName, + SubjectAlternativeName: metadata.SubjectAlternativeName, + ExpirationDate: timestamppb.New(metadata.ExpirationDate), + IssuedDate: timestamppb.New(metadata.IssuedDate), + CertificateAuthorityArn: metadata.CertificateAuthorityArn.String, + }, + }, nil +} diff --git a/internal/v1/certificate/operations_test.go b/internal/v1/certificate/operations_test.go new file mode 100644 index 0000000..56a37fa --- /dev/null +++ b/internal/v1/certificate/operations_test.go @@ -0,0 +1,230 @@ +package certificate + +import ( + "context" + "crypto/x509" + "database/sql" + "fmt" + "testing" + "time" + + "github.com/coinbase/baseca/db/mock" + db "github.com/coinbase/baseca/db/sqlc" + "github.com/coinbase/baseca/internal/lib/crypto" + "github.com/coinbase/baseca/internal/types" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" + + apiv1 "github.com/coinbase/baseca/gen/go/baseca/v1" +) + +func TestGetCertificate(t *testing.T) { + serial_number := "4ff0501e-55b2-41e0-b5b2-1f1ff6224998" + cases := []struct { + name string + req *apiv1.CertificateSerialNumber + build func(store *mock.MockStore) + check func(t *testing.T, res *apiv1.CertificateParameter, err error) + }{ + { + name: "OK", + req: &apiv1.CertificateSerialNumber{ + SerialNumber: serial_number, + }, + build: func(store *mock.MockStore) { + resp := db.Certificate{ + SerialNumber: serial_number, + CommonName: "development.example.com", + Account: "example", + Environment: "development", + ExtendedKey: "EndEntityServerAuthCertificate", + SubjectAlternativeName: []string{"development.example.com"}, + ExpirationDate: time.Now().UTC(), + IssuedDate: time.Now().UTC().Add(24 * time.Hour), + Revoked: false, + CertificateAuthorityArn: sql.NullString{String: "arn:aws:acm-pca:us-east-1:123456789012:certificate-authority/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", Valid: true}, + } + + store.EXPECT().GetCertificate(gomock.Any(), serial_number).Times(1).Return(&resp, nil) + }, + check: func(t *testing.T, res *apiv1.CertificateParameter, err error) { + require.NoError(t, err) + }, + }, + } + + for elem := range cases { + tc := cases[elem] + + t.Run(tc.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + store := mock.NewMockStore(ctrl) + tc.build(store) + + c, err := buildCertificateConfig(store) + require.NoError(t, err) + + res, err := c.GetCertificate(context.Background(), tc.req) + tc.check(t, res, err) + }) + } +} + +func TestOperationsSignCSR(t *testing.T) { + cases := []struct { + name string + req func() *apiv1.OperationsSignRequest + build func(store *mock.MockStore) + check func(t *testing.T, res *apiv1.SignedCertificate, err error) + }{ + { + name: "OK_NO_CERTIFICATE_AUTHORITY_INPUT", + req: func() *apiv1.OperationsSignRequest { + req := types.CertificateRequest{ + CommonName: "development.example.com", + SubjectAlternateNames: []string{"development.example.com"}, + SigningAlgorithm: x509.SHA512WithRSA, + PublicKeyAlgorithm: x509.RSA, + KeySize: 2048, + } + csr, _ := crypto.GenerateCSR(req) + + return &apiv1.OperationsSignRequest{ + CertificateSigningRequest: csr.CSR.String(), + ServiceAccount: "example", + Environment: "development", + ExtendedKey: "EndEntityClientAuthCertificate", + } + }, + build: func(store *mock.MockStore) { + arg := db.GetServiceAccountByMetadataParams{ + ServiceAccount: "example", + Environment: "development", + ExtendedKey: "EndEntityClientAuthCertificate", + } + + resp := []*db.Account{ + { + ServiceAccount: "example", + Environment: "development", + ExtendedKey: "EndEntityClientAuthCertificate", + ValidCertificateAuthorities: []string{"sandbox_use1"}, + }, + } + + logArg := db.LogCertificateParams{ + Account: "example", + Environment: "development", + ExtendedKey: "EndEntityClientAuthCertificate", + } + + logResp := &db.Certificate{ + Account: "example", + Environment: "development", + ExtendedKey: "EndEntityClientAuthCertificate", + } + + store.EXPECT().GetServiceAccountByMetadata(gomock.Any(), arg).Times(1).Return(resp, nil) + store.EXPECT().LogCertificate(gomock.Any(), &certificateLogArgMatcher{ + Account: logArg.Account, + Environment: logArg.Environment, + ExtendedKey: logArg.ExtendedKey, + }).Times(1).Return(logResp, nil) + }, + check: func(t *testing.T, res *apiv1.SignedCertificate, err error) { + require.NoError(t, err) + }, + }, + { + name: "OK_CERTIFICATE_AUTHORITY_INPUT", + req: func() *apiv1.OperationsSignRequest { + req := types.CertificateRequest{ + CommonName: "development.example.com", + SubjectAlternateNames: []string{"development.example.com"}, + SigningAlgorithm: x509.SHA512WithRSA, + PublicKeyAlgorithm: x509.RSA, + KeySize: 2048, + } + csr, _ := crypto.GenerateCSR(req) + + caParameter := &apiv1.CertificateAuthorityParameter{ + Region: "us-east-1", + CaArn: "arn:aws:acm-pca:us-east-1:123456789012:certificate-authority/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", + SignAlgorithm: "SHA512WITHRSA", + AssumeRole: false, + Validity: 30, + } + + return &apiv1.OperationsSignRequest{ + CertificateSigningRequest: csr.CSR.String(), + CertificateAuthority: caParameter, + ServiceAccount: "example", + Environment: "development", + ExtendedKey: "EndEntityClientAuthCertificate", + } + }, + build: func(store *mock.MockStore) { + logArg := db.LogCertificateParams{ + Account: "example", + Environment: "development", + ExtendedKey: "EndEntityClientAuthCertificate", + } + + logResp := &db.Certificate{ + Account: "example", + Environment: "development", + ExtendedKey: "EndEntityClientAuthCertificate", + } + + store.EXPECT().LogCertificate(gomock.Any(), &certificateLogArgMatcher{ + Account: logArg.Account, + Environment: logArg.Environment, + ExtendedKey: logArg.ExtendedKey, + }).Times(1).Return(logResp, nil) + }, + check: func(t *testing.T, res *apiv1.SignedCertificate, err error) { + require.NoError(t, err) + }, + }, + } + + for elem := range cases { + tc := cases[elem] + + t.Run(tc.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + store := mock.NewMockStore(ctrl) + tc.build(store) + + c, err := buildCertificateConfig(store) + require.NoError(t, err) + + req := tc.req() + res, err := c.OperationsSignCSR(context.Background(), req) + tc.check(t, res, err) + }) + } +} + +type certificateLogArgMatcher struct { + Account string + Environment string + ExtendedKey string +} + +func (m *certificateLogArgMatcher) Matches(x any) bool { + if arg, ok := x.(db.LogCertificateParams); ok { + return arg.Account == m.Account && + arg.Environment == m.Environment + } + return false +} + +func (m *certificateLogArgMatcher) String() string { + return fmt.Sprintf("Account = %v Environment = %v ExtendedKey = %v", + m.Account, m.Environment, m.ExtendedKey) +} diff --git a/internal/v1/certificate/pca.go b/internal/v1/certificate/pca.go new file mode 100644 index 0000000..99f5647 --- /dev/null +++ b/internal/v1/certificate/pca.go @@ -0,0 +1,117 @@ +package certificate + +import ( + "context" + "crypto/rand" + "crypto/x509" + "fmt" + "math/big" + "time" + + db "github.com/coinbase/baseca/db/sqlc" + "github.com/coinbase/baseca/internal/client/firehose" + "github.com/coinbase/baseca/internal/lib/crypto" + "github.com/coinbase/baseca/internal/types" +) + +func (c *Certificate) buildCertificateAuthorityParameters(certificate_authority string) types.CertificateParameters { + return types.CertificateParameters{ + Region: c.acmConfig[certificate_authority].Region, + CaArn: c.acmConfig[certificate_authority].CaArn, + AssumeRole: c.acmConfig[certificate_authority].AssumeRole, + RoleArn: c.acmConfig[certificate_authority].RoleArn, + Validity: c.acmConfig[certificate_authority].CaActiveDay, + RootCa: c.acmConfig[certificate_authority].RootCa, + } +} + +func (c *Certificate) issueEndEntityCertificate(auth *types.ServiceAccountPayload, ca_certificate *types.CertificateAuthority, request_csr *x509.CertificateRequest) (*db.CertificateResponseData, error) { + block := make([]byte, 20) + _, err := rand.Read(block[:]) + if err != nil { + return nil, err + } + + output := big.NewInt(0).SetBytes(block) + issuedDate := time.Now().UTC() + + expirationDate := time.Now().UTC().AddDate(0, 0, int(auth.CertificateValidity)).UTC() + if expirationDate.Before(time.Now().UTC().Add(time.Minute).UTC()) { + return nil, err + } + + certificateTemplate := x509.Certificate{ + DNSNames: request_csr.DNSNames, + Signature: request_csr.Signature, + SignatureAlgorithm: ca_certificate.Certificate.SignatureAlgorithm, + PublicKeyAlgorithm: request_csr.PublicKeyAlgorithm, + PublicKey: request_csr.PublicKey, + SerialNumber: output, + Issuer: ca_certificate.Certificate.Subject, + Subject: request_csr.Subject, + NotBefore: issuedDate, + NotAfter: expirationDate, + KeyUsage: types.CertificateRequestExtension[auth.ExtendedKey].KeyUsage, + ExtKeyUsage: types.CertificateRequestExtension[auth.ExtendedKey].ExtendedKeyUsage, + } + + if len(c.ocsp) != 0 { + certificateTemplate.OCSPServer = c.ocsp + } + + certificateAuthorityRaw := ca_certificate.Certificate.Raw + pk, err := crypto.ReturnPrivateKey(*ca_certificate.AsymmetricKey) + if err != nil { + return nil, err + } + certificateRaw, err := x509.CreateCertificate(rand.Reader, &certificateTemplate, ca_certificate.Certificate, request_csr.PublicKey, pk) + if err != nil { + return nil, err + } + + leafCertificate, err := x509.ParseCertificate(certificateRaw) + if err != nil { + return nil, fmt.Errorf("error parsing leaf certificate data") + } + + caPath := fmt.Sprintf("%s_%s", auth.SubordinateCa, auth.Environment) + certificate, intermediate_chain, root_chain, err := crypto.BuildCertificateChain(caPath, certificateRaw, certificateAuthorityRaw) + if err != nil { + return nil, err + } + + certificate_data := types.CertificateMetadata{ + // Serial Number: Convert Base 10 *bit.Int output to Base 16: + SerialNumber: leafCertificate.SerialNumber.Text(16), + CommonName: request_csr.Subject.CommonName, + SubjectAlternativeName: request_csr.DNSNames, + ExpirationDate: expirationDate, + IssuedDate: issuedDate, + CaSerialNumber: ca_certificate.SerialNumber, + CertificateAuthorityArn: ca_certificate.CertificateAuthorityArn, + } + + event := firehose.ForwardedEventUploadEvent{ + SerialNumber: leafCertificate.SerialNumber.Text(16), + Metadata: firehose.Metadata{ + CommonName: certificate_data.CommonName, + SubjectAlternateName: certificate_data.SubjectAlternativeName, + CertificateExpiration: certificate_data.ExpirationDate, + IssuedDate: certificate_data.IssuedDate, + CaSerialNumber: certificate_data.CaSerialNumber, + CertificateAuthorityArn: certificate_data.CertificateAuthorityArn, + }, + } + + _, err = c.firehose.Stream(context.Background(), event) + if err != nil { + return nil, err + } + + return &db.CertificateResponseData{ + Certificate: certificate.String(), + IntermediateCertificateChain: intermediate_chain.String(), + RootCertificateChain: root_chain.String(), + Metadata: certificate_data, + }, nil +} diff --git a/internal/v1/certificate/query.go b/internal/v1/certificate/query.go new file mode 100644 index 0000000..0f94528 --- /dev/null +++ b/internal/v1/certificate/query.go @@ -0,0 +1,184 @@ +package certificate + +import ( + "context" + "database/sql" + "fmt" + + db "github.com/coinbase/baseca/db/sqlc" + apiv1 "github.com/coinbase/baseca/gen/go/baseca/v1" + "github.com/coinbase/baseca/internal/lib/util/validator" + "github.com/coinbase/baseca/internal/logger" + "github.com/coinbase/baseca/internal/types" + "github.com/gogo/status" + "google.golang.org/grpc/codes" + "google.golang.org/protobuf/types/known/timestamppb" +) + +func (c *Certificate) GetCertificate(ctx context.Context, req *apiv1.CertificateSerialNumber) (*apiv1.CertificateParameter, error) { + certificate, err := c.store.Reader.GetCertificate(ctx, req.SerialNumber) + if err != nil { + return nil, logger.RpcError(status.Error(codes.InvalidArgument, "certificate not found"), err) + } + return &apiv1.CertificateParameter{ + SerialNumber: certificate.SerialNumber, + Account: certificate.Account, + Environment: certificate.Environment, + ExtendedKey: certificate.ExtendedKey, + CommonName: certificate.CommonName, + SubjectAlternativeName: certificate.SubjectAlternativeName, + ExpirationDate: timestamppb.New(certificate.ExpirationDate), + IssuedDate: timestamppb.New(certificate.IssuedDate), + Revoked: certificate.Revoked, + RevokedBy: certificate.RevokedBy.String, + RevokeDate: timestamppb.New(certificate.RevokeDate.Time), + CertificateAuthorityArn: certificate.CertificateAuthorityArn.String, + }, nil +} + +func (c *Certificate) ListCertificates(ctx context.Context, req *apiv1.ListCertificatesRequest) (*apiv1.CertificatesParameter, error) { + var pbCertificates apiv1.CertificatesParameter + + if len(req.CommonName) == 0 { + return nil, logger.RpcError(status.Error(codes.InvalidArgument, "missing common name (cn)"), fmt.Errorf("missing common name (cn)")) + } + + arg := db.ListCertificateSubjectAlternativeNameParams{ + CommonName: req.CommonName, + Limit: req.PageSize, + Offset: (req.PageId - 1) * req.PageSize, + } + + certificates, err := c.store.Reader.ListCertificateSubjectAlternativeName(ctx, arg) + if err != nil { + return nil, logger.RpcError(status.Error(codes.Internal, "internal server error"), err) + } + + for _, certificate := range certificates { + pbCertificates.Certificates = append(pbCertificates.Certificates, &apiv1.CertificateParameter{ + SerialNumber: certificate.SerialNumber, + Account: certificate.Account, + Environment: certificate.Environment, + ExtendedKey: certificate.ExtendedKey, + CommonName: certificate.CommonName, + SubjectAlternativeName: certificate.SubjectAlternativeName, + ExpirationDate: timestamppb.New(certificate.ExpirationDate), + IssuedDate: timestamppb.New(certificate.IssuedDate), + Revoked: certificate.Revoked, + RevokedBy: certificate.RevokedBy.String, + RevokeDate: timestamppb.New(certificate.RevokeDate.Time), + CertificateAuthorityArn: certificate.CertificateAuthorityArn.String, + }) + } + return &pbCertificates, nil +} + +func (c *Certificate) QueryCertificateMetadata(ctx context.Context, req *apiv1.QueryCertificateMetadataRequest) (*apiv1.CertificatesParameter, error) { + var serialNumber, account, environment, extendedKey string + var san []string + var pbCertificates apiv1.CertificatesParameter + + if len(req.Account) == 0 && len(req.SerialNumber) == 0 && len(req.Environment) == 0 && len(req.ExtendedKey) == 0 && len(req.SubjectAlternativeName) == 0 { + return nil, logger.RpcError(status.Error(codes.InvalidArgument, "invalid parameters"), fmt.Errorf("invalid parameters nil or empty")) + } + + if len(req.SerialNumber) != 0 { + if !validator.ValidateInput(req.SerialNumber) { + return nil, logger.RpcError(status.Error(codes.InvalidArgument, "invalid serial number format"), fmt.Errorf("invalid serial number: %s", req.SerialNumber)) + } + serialNumber = req.SerialNumber + } else { + serialNumber = "%" + } + + if len(req.Account) != 0 { + account = req.Account + } else { + account = "%" + } + + if len(req.Environment) != 0 { + if _, ok := validator.CertificateAuthorityEnvironments[req.Environment]; !ok { + return nil, logger.RpcError(status.Error(codes.InvalidArgument, "invalid environment"), fmt.Errorf("invalid environment: %s", req.Environment)) + } + + environment = req.Environment + } else { + environment = "%" + } + + if len(req.ExtendedKey) != 0 { + if _, ok := types.CertificateRequestExtension[req.ExtendedKey]; !ok { + return nil, logger.RpcError(status.Error(codes.InvalidArgument, "invalid extended key"), fmt.Errorf("invalid extended key: %s", req.ExtendedKey)) + + } + extendedKey = req.ExtendedKey + } else { + extendedKey = "%" + } + + if len(req.SubjectAlternativeName) > 1 { + san = validator.SanitizeInput(req.SubjectAlternativeName) + } + + arg := db.GetSignedCertificateByMetadataParams{ + SerialNumber: serialNumber, + Account: account, + Environment: environment, + ExtendedKey: extendedKey, + } + + certificates, err := c.store.Reader.GetSignedCertificateByMetadata(ctx, arg) + if err != nil { + if err == sql.ErrNoRows { + return nil, logger.RpcError(status.Error(codes.NotFound, "certificate not found"), err) + } + return nil, logger.RpcError(status.Error(codes.Internal, "internal server error"), err) + } + + for _, certificate := range certificates { + if len(san) == 0 { + pbCertificates.Certificates = append(pbCertificates.Certificates, &apiv1.CertificateParameter{ + SerialNumber: certificate.SerialNumber, + Account: certificate.Account, + Environment: certificate.Environment, + ExtendedKey: certificate.ExtendedKey, + CommonName: certificate.CommonName, + SubjectAlternativeName: certificate.SubjectAlternativeName, + ExpirationDate: timestamppb.New(certificate.ExpirationDate), + IssuedDate: timestamppb.New(certificate.IssuedDate), + Revoked: certificate.Revoked, + RevokedBy: certificate.RevokedBy.String, + RevokeDate: timestamppb.New(certificate.RevokeDate.Time), + CertificateAuthorityArn: certificate.CertificateAuthorityArn.String, + }) + } else { + check := true + for _, a := range san { + if !validator.Contains(certificate.SubjectAlternativeName, a) { + check = false + break + } + } + + if check { + pbCertificates.Certificates = append(pbCertificates.Certificates, &apiv1.CertificateParameter{ + SerialNumber: certificate.SerialNumber, + Account: certificate.Account, + Environment: certificate.Environment, + ExtendedKey: certificate.ExtendedKey, + CommonName: certificate.CommonName, + SubjectAlternativeName: certificate.SubjectAlternativeName, + ExpirationDate: timestamppb.New(certificate.ExpirationDate), + IssuedDate: timestamppb.New(certificate.IssuedDate), + Revoked: certificate.Revoked, + RevokedBy: certificate.RevokedBy.String, + RevokeDate: timestamppb.New(certificate.RevokeDate.Time), + CertificateAuthorityArn: certificate.CertificateAuthorityArn.String, + }) + } + } + } + + return &pbCertificates, nil +} diff --git a/internal/v1/certificate/query_test.go b/internal/v1/certificate/query_test.go new file mode 100644 index 0000000..d6a573e --- /dev/null +++ b/internal/v1/certificate/query_test.go @@ -0,0 +1,68 @@ +package certificate + +import ( + "context" + "testing" + + "github.com/coinbase/baseca/db/mock" + db "github.com/coinbase/baseca/db/sqlc" + apiv1 "github.com/coinbase/baseca/gen/go/baseca/v1" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" +) + +func TestQueryCertificateMetadata(t *testing.T) { + + cases := []struct { + name string + req *apiv1.QueryCertificateMetadataRequest + build func(store *mock.MockStore) + check func(t *testing.T, res *apiv1.CertificatesParameter, err error) + }{ + { + name: "OK", + req: &apiv1.QueryCertificateMetadataRequest{ + Account: "example", + Environment: "sandbox", + }, + build: func(store *mock.MockStore) { + arg := db.GetSignedCertificateByMetadataParams{ + SerialNumber: "%", + Account: "example", + Environment: "sandbox", + ExtendedKey: "%", + } + + resp := []*db.Certificate{ + { + Account: "example", + Environment: "sandbox", + }, + } + store.EXPECT().GetSignedCertificateByMetadata(gomock.Any(), arg).Times(1).Return(resp, nil) + }, + check: func(t *testing.T, res *apiv1.CertificatesParameter, err error) { + require.NoError(t, err) + }, + }, + } + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + store := mock.NewMockStore(ctrl) + + for elem := range cases { + tc := cases[elem] + + t.Run(tc.name, func(t *testing.T) { + tc.build(store) + + c, err := buildCertificateConfig(store) + require.NoError(t, err) + + res, err := c.QueryCertificateMetadata(context.Background(), tc.req) + tc.check(t, res, err) + }) + } +} diff --git a/internal/v1/certificate/sign.go b/internal/v1/certificate/sign.go new file mode 100644 index 0000000..aedde1d --- /dev/null +++ b/internal/v1/certificate/sign.go @@ -0,0 +1,252 @@ +package certificate + +import ( + "bytes" + "context" + "crypto/x509" + "encoding/pem" + "errors" + "fmt" + + db "github.com/coinbase/baseca/db/sqlc" + apiv1 "github.com/coinbase/baseca/gen/go/baseca/v1" + "github.com/coinbase/baseca/internal/client/acmpca" + "github.com/coinbase/baseca/internal/config" + "github.com/coinbase/baseca/internal/lib/crypto" + "github.com/coinbase/baseca/internal/lib/util" + "github.com/coinbase/baseca/internal/lib/util/validator" + "github.com/coinbase/baseca/internal/logger" + "github.com/coinbase/baseca/internal/types" + "github.com/gogo/status" + "google.golang.org/grpc/codes" + "google.golang.org/protobuf/types/known/timestamppb" +) + +func (c *Certificate) SignCSR(ctx context.Context, req *apiv1.CertificateSigningRequest) (*apiv1.SignedCertificate, error) { + var service *types.ServiceAccountPayload + var ok bool + + if service, ok = ctx.Value(types.ServiceAuthenticationContextKey).(*types.ServiceAccountPayload); !ok { + return nil, logger.RpcError(status.Error(codes.Internal, "internal server error"), fmt.Errorf("service payload malformatted: %+v", service)) + } + + csrPem, _ := pem.Decode([]byte(req.CertificateSigningRequest)) + if csrPem == nil { + return nil, logger.RpcError(status.Error(codes.InvalidArgument, "certificate signing request (csr) invalid format"), fmt.Errorf("error decoding certificate signing request (csr): %+v", service)) + } + + csr, err := x509.ParseCertificateRequest(csrPem.Bytes) + if err != nil { + return nil, logger.RpcError(status.Error(codes.InvalidArgument, "certificate signing request (csr) invalid format"), err) + } + + if c.redis.Limit != 0 { + if !validator.Contains(c.redis.Excluded, service.ServiceAccount) { + err = c.rateLimit(ctx, csr) + if err != nil { + return nil, logger.RpcError(status.Error(codes.ResourceExhausted, "rate limit exceeded"), err) + } + } + } + + err = c.validateCertificateRequestParameters(csr, *service) + if err != nil { + return nil, err + } + + certificate, err := c.requestCertificate(ctx, service, csr) + if err != nil { + return nil, logger.RpcError(status.Error(codes.Internal, fmt.Sprintf("error signing certificate request %s", err.Error())), err) + } + + return &apiv1.SignedCertificate{ + Certificate: certificate.Certificate, + IntermediateCertificateChain: certificate.IntermediateCertificateChain, + CertificateChain: certificate.RootCertificateChain, + Metadata: &apiv1.CertificateParameter{ + SerialNumber: certificate.Metadata.SerialNumber, + CommonName: certificate.Metadata.CommonName, + SubjectAlternativeName: certificate.Metadata.SubjectAlternativeName, + ExpirationDate: timestamppb.New(certificate.Metadata.ExpirationDate), + IssuedDate: timestamppb.New(certificate.Metadata.IssuedDate), + Revoked: certificate.Metadata.Revoked, + RevokedBy: certificate.Metadata.RevokedBy, + RevokeDate: timestamppb.New(certificate.Metadata.RevokeDate), + CertificateAuthorityArn: certificate.Metadata.CertificateAuthorityArn, + }, + }, nil + +} + +func (c *Certificate) requestCertificate(ctx context.Context, authPayload *types.ServiceAccountPayload, certificateRequest *x509.CertificateRequest) (*db.CertificateResponseData, error) { + var subordinate *types.CertificateAuthority + var parameters types.CertificateRequest + var csr *bytes.Buffer + var err error + + intermediateCa := fmt.Sprintf("%s_%s", authPayload.SubordinateCa, authPayload.Environment) + + err = checkLockfile(intermediateCa) + if err != nil { + return nil, err + } + + subordinate, err = loadSubordinateCaParameters(intermediateCa, authPayload) + if err != nil { + err = createServiceDirectory(intermediateCa) + if err != nil { + return nil, err + } + + err = util.GenerateLockfile(intermediateCa) + if err != nil { + return nil, err + } + + signingAlgorithm, ok := types.ValidSignatures[c.ca.SigningAlgorithm] + if !ok { + return nil, fmt.Errorf("invalid signing algorithm: %s", c.ca.SigningAlgorithm) + } + + parameters = types.CertificateRequest{ + CommonName: intermediateCa, + SubjectAlternateNames: []string{intermediateCa}, + SigningAlgorithm: signingAlgorithm.Common, + PublicKeyAlgorithm: types.PublicKeyAlgorithms[c.ca.KeyAlgorithm].Algorithm, + KeySize: c.ca.KeySize, + DistinguishedName: types.DistinguishedName{ + Country: []string{c.ca.Country}, + Province: []string{c.ca.Province}, + Locality: []string{c.ca.Locality}, + Organization: []string{c.ca.Organization}, + OrganizationalUnit: []string{c.ca.OrganizationUnit}, + }, + } + + signingRequest, err := crypto.GenerateCSR(parameters) + if err != nil { + return nil, err + } + + key, err := x509.ParsePKCS1PrivateKey(signingRequest.PrivateKey.Bytes) + if err != nil { + return nil, errors.New("error parsing pkcs1 rsa private key") + } + + pk := crypto.RSA{PrivateKey: key, PublicKey: &key.PublicKey} + err = crypto.WriteKeyToFile(intermediateCa, &pk) + if err != nil { + return nil, err + } + + csr = signingRequest.CSR + + // Issue Subordinate CA + err = c.issueSubordinate(authPayload, csr, intermediateCa) + if err != nil { + return nil, fmt.Errorf("error generating subordinate ca [%s]: %s", intermediateCa, err) + } + err = util.RemoveLockfile(intermediateCa) + if err != nil { + return nil, err + } + + subordinate, err = loadSubordinateCaParameters(intermediateCa, authPayload) + if err != nil { + return nil, err + } + + certificate, err := c.issueEndEntityCertificate(authPayload, subordinate, certificateRequest) + if err != nil { + return nil, fmt.Errorf("error issuing end entity certificate: %s", err) + } else { + return certificate, nil + } + } else { + // Issue End Entity Certificate + certificate, err := c.issueEndEntityCertificate(authPayload, subordinate, certificateRequest) + if err != nil { + return nil, fmt.Errorf("error issuing end entity certificate: %s", err) + } else { + return certificate, nil + } + } +} + +func (c *Certificate) validateCertificateRequestParameters(csr *x509.CertificateRequest, auth types.ServiceAccountPayload) error { + if err := csr.CheckSignature(); err != nil { + return logger.RpcError(status.Error(codes.InvalidArgument, "invalid signature for certificate signing request (csr)"), err) + } + + if len(csr.Subject.CommonName) == 0 { + return logger.RpcError(status.Error(codes.InvalidArgument, "common name (cn) missing from certificate signing request (csr)"), fmt.Errorf("%s", csr.Subject)) + } + + subjectAlternativeNames := append(csr.DNSNames, csr.Subject.CommonName) + err := validator.ValidateSubjectAlternateNames(subjectAlternativeNames, auth.ValidSubjectAlternateName, auth.SANRegularExpression) + if err != nil { + return logger.RpcError(status.Error(codes.InvalidArgument, "invalid subject alternative name(s) in certificate signing request (csr)"), err) + } + + for _, certificate_authority := range auth.ValidCertificateAuthorities { + if (c.acmConfig[certificate_authority] == config.SubordinateCertificate{}) { + return logger.RpcError(status.Error(codes.InvalidArgument, "invalid certificate authority (ca)"), fmt.Errorf("certificate authority: %s", certificate_authority)) + } + } + return nil +} + +func (c *Certificate) rateLimit(ctx context.Context, certificateRequest *x509.CertificateRequest) error { + mapping := make(map[string]bool) + mapping[certificateRequest.Subject.CommonName] = true + for _, fqdn := range certificateRequest.DNSNames { + mapping[fqdn] = true + } + + for domain := range mapping { + err := c.redis.Increment(ctx, domain, 1) + if err != nil { + return err + } + } + return nil +} + +func (c *Certificate) issueSubordinate(auth *types.ServiceAccountPayload, certificate_signing_request *bytes.Buffer, service_name string) error { + var ca_certificate *x509.Certificate + var err error + + // Handle Multi-Region Failover + for _, ca := range auth.ValidCertificateAuthorities { + ca_parameters := c.buildCertificateAuthorityParameters(ca) + + // c.pca Mock Private CA + if c.pca == nil { + client, err := acmpca.NewPrivateCaClient(ca_parameters) + if err != nil { + return err + } + c.pca = client + } + + ca_certificate, err = c.pca.IssueSubordinateCertificate(ca_parameters, c.ca.SigningAlgorithm, certificate_signing_request.Bytes()) + if err != nil { + logger.DefaultLogger.Error(err.Error()) + + // Build ACM Client for Other Regions + continue + } + + certificate_chain, err := c.pca.GetSubordinateCAChain(ca_parameters.CaArn) + if err != nil { + return err + } + + err = crypto.WriteSubordinateCaParameters(service_name, ca_certificate, ca_parameters, certificate_chain) + if err != nil { + return err + } + return nil + } + return nil +} diff --git a/internal/v1/certificate/sign_test.go b/internal/v1/certificate/sign_test.go new file mode 100644 index 0000000..e2dd171 --- /dev/null +++ b/internal/v1/certificate/sign_test.go @@ -0,0 +1,75 @@ +package certificate + +import ( + "context" + "crypto/x509" + "testing" + + "github.com/coinbase/baseca/db/mock" + apiv1 "github.com/coinbase/baseca/gen/go/baseca/v1" + "github.com/coinbase/baseca/internal/types" + baseca "github.com/coinbase/baseca/pkg/client" + "github.com/google/uuid" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" +) + +func TestSignCSR(t *testing.T) { + cases := []struct { + name string + req func() *apiv1.CertificateSigningRequest + build func(store *mock.MockStore) + check func(t *testing.T, res *apiv1.SignedCertificate, err error) + }{ + { + name: "OK", + req: func() *apiv1.CertificateSigningRequest { + req := baseca.CertificateRequest{ + CommonName: "example.com", + SubjectAlternateNames: []string{"example.com"}, + SigningAlgorithm: x509.SHA512WithRSA, + PublicKeyAlgorithm: x509.RSA, + KeySize: 4096, + } + + csr, _ := baseca.GenerateCSR(req) + return &apiv1.CertificateSigningRequest{ + CertificateSigningRequest: csr.CSR.String(), + } + }, + build: func(store *mock.MockStore) {}, + check: func(t *testing.T, res *apiv1.SignedCertificate, err error) { + require.NoError(t, err) + }}, + } + + for elem := range cases { + tc := cases[elem] + + t.Run(tc.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + store := mock.NewMockStore(ctrl) + tc.build(store) + + c, err := buildCertificateConfig(store) + require.NoError(t, err) + + ctx := context.WithValue(context.Background(), types.ServiceAuthenticationContextKey, &types.ServiceAccountPayload{ + ServiceID: uuid.New(), + ServiceAccount: "example", + Environment: "development", + ValidSubjectAlternateName: []string{"example.com"}, + ValidCertificateAuthorities: []string{"sandbox_use1"}, + CertificateValidity: int16(30), + ExtendedKey: "EndEntityServerAuthCertificate", + SubordinateCa: "infrastructure", + }) + + req := tc.req() + res, err := c.SignCSR(ctx, req) + tc.check(t, res, err) + }) + } +} diff --git a/internal/v1/certificate/validate.go b/internal/v1/certificate/validate.go new file mode 100644 index 0000000..4da86cf --- /dev/null +++ b/internal/v1/certificate/validate.go @@ -0,0 +1,140 @@ +package certificate + +import ( + "bytes" + "crypto/tls" + "crypto/x509" + "encoding/pem" + "errors" + "fmt" + "os" + "path/filepath" + "time" + + apiv1 "github.com/coinbase/baseca/gen/go/baseca/v1" + "github.com/coinbase/baseca/internal/config" + "github.com/coinbase/baseca/internal/lib/crypto" + "github.com/coinbase/baseca/internal/lib/util" + "github.com/coinbase/baseca/internal/lib/util/validator" + "github.com/coinbase/baseca/internal/types" +) + +const ( + _lockfile_duration = 30 * time.Second +) + +func createServiceDirectory(serviceId string) error { + directoryPath := filepath.Join(types.SubordinatePath, serviceId) + + if _, err := os.Stat(directoryPath); os.IsNotExist(err) { + err := os.MkdirAll(directoryPath, os.ModePerm) + if err != nil { + return fmt.Errorf("error creating service directory [%s], %s", serviceId, err) + } + } + return nil +} + +func checkLockfile(serviceId string) error { + lockfilePath := fmt.Sprintf("%s/%s/%s.lock", types.SubordinatePath, serviceId, serviceId) + + // Clean Stuck Lockfile + if _, err := os.OpenFile(filepath.Clean(lockfilePath), os.O_RDONLY, 0400); err == nil { + lockfile_stats, _ := os.Stat(lockfilePath) + lockfile_creation := lockfile_stats.ModTime().UTC() + if lockfile_creation.Add(_lockfile_duration).UTC().Before(time.Now().UTC()) { + err = os.Remove(filepath.Clean(lockfilePath)) + if err != nil { + return fmt.Errorf("error during lockfile creation %s", err) + } + } + } + + err := util.LockfileBackoff(lockfilePath) + if err != nil { + return fmt.Errorf("error during lockfile backoff %s", err) + } + return nil +} + +// Check if Subordinate Certificate Exists for Service +func loadSubordinateCaParameters(service string, auth *types.ServiceAccountPayload) (*types.CertificateAuthority, error) { + x509_metadata, err := crypto.GetSubordinateCaParameters(service) + if err != nil { + return nil, err + } + + if !validateSubordinateExpiration(auth.CertificateValidity, x509_metadata) { + return nil, errors.New("certificate expiration exceeds subordinate ca expiration") + } + + certificate_path, key_path, err := crypto.GetSubordinateCaPath(service) + if err != nil { + return nil, err + } + + // Validate Non-Mismatch Between Private Key and Certificate + _, err = tls.LoadX509KeyPair(*certificate_path, *key_path) + if err != nil { + return nil, err + } + return x509_metadata, nil +} + +func validateSubordinateExpiration(certificate_validity int16, x509_metadata *types.CertificateAuthority) bool { + certificate_expiration := x509_metadata.Certificate.NotAfter + // Subordinate CA Expires Before End-Entity Certificate (Requires Re-Issuance of Subordinate) + return !certificate_expiration.Before(time.Now().UTC().AddDate(0, 0, int(certificate_validity)).UTC()) +} + +func (c *Certificate) validateCsrParameters(parameters *apiv1.OperationsSignRequest) error { + csrBlock, _ := pem.Decode([]byte(parameters.CertificateSigningRequest)) + if csrBlock == nil { + return fmt.Errorf("certificate signing request (csr) could not be decoded") + } + + request, err := x509.ParseCertificateRequest(csrBlock.Bytes) + if err != nil { + return fmt.Errorf("certificate signing request (csr) invalid format") + } + + dns := request.DNSNames + for _, domain := range dns { + if !validator.IsValidDomain(domain) { + return fmt.Errorf("%s is not a supported domain", domain) + } + } + + return nil +} + +func convertX509toString(certificate []byte) (*bytes.Buffer, error) { + buffer := new(bytes.Buffer) + err := pem.Encode(buffer, &pem.Block{Type: "CERTIFICATE", Bytes: certificate}) + if err != nil { + return nil, fmt.Errorf("error encoding x509 certificate: %s", err) + } + return buffer, nil +} + +func ValidateSubordinateParameters(parameter config.SubordinateCertificateAuthority) error { + switch parameter.KeyAlgorithm { + case "RSA": + if _, ok := types.PublicKeyAlgorithms[parameter.KeyAlgorithm].KeySize[parameter.KeySize]; !ok { + return fmt.Errorf("invalid rsa key size: %d", parameter.KeySize) + } + if _, ok := types.PublicKeyAlgorithms[parameter.KeyAlgorithm].Signature[parameter.SigningAlgorithm]; !ok { + return fmt.Errorf("invalid rsa signing algorithm: %s", parameter.SigningAlgorithm) + } + case "ECDSA": + if _, ok := types.PublicKeyAlgorithms[parameter.KeyAlgorithm].KeySize[parameter.KeySize]; !ok { + return fmt.Errorf("invalid ecdsa key size: %d", parameter.KeySize) + } + if _, ok := types.PublicKeyAlgorithms[parameter.KeyAlgorithm].Signature[parameter.SigningAlgorithm]; !ok { + return fmt.Errorf("invalid ecdsa signing algorithm: %s", parameter.SigningAlgorithm) + } + default: + return fmt.Errorf("public key algorithm not supported: %s", parameter.KeyAlgorithm) + } + return nil +} diff --git a/internal/v1/health/health.go b/internal/v1/health/health.go new file mode 100644 index 0000000..677b356 --- /dev/null +++ b/internal/v1/health/health.go @@ -0,0 +1,48 @@ +package health + +import ( + "sync" + + "github.com/gogo/status" + "golang.org/x/net/context" + "google.golang.org/grpc/codes" + healthpb "google.golang.org/grpc/health/grpc_health_v1" +) + +type HealthServer struct { + healthpb.HealthServer + mu sync.Mutex + // statusMap stores the serving status of the services this HealthServer monitors. + statusMap map[string]healthpb.HealthCheckResponse_ServingStatus +} + +func NewHealthServer() *HealthServer { + return &HealthServer{ + statusMap: make(map[string]healthpb.HealthCheckResponse_ServingStatus), + } +} + +func (s *HealthServer) Check(ctx context.Context, in *healthpb.HealthCheckRequest) (*healthpb.HealthCheckResponse, error) { + s.mu.Lock() + defer s.mu.Unlock() + if in.Service == "" { + // check the server overall health status. + return &healthpb.HealthCheckResponse{ + Status: healthpb.HealthCheckResponse_SERVING, + }, nil + } + if status, ok := s.statusMap[in.Service]; ok { + return &healthpb.HealthCheckResponse{ + Status: status, + }, nil + } + return nil, status.Errorf(codes.NotFound, "unknown service") +} + +// SetServingStatus is called when need to reset the serving status of a service +// or insert a new service entry into the statusMap. +func (s *HealthServer) SetServingStatus(service string, status healthpb.HealthCheckResponse_ServingStatus) { + s.mu.Lock() + s.statusMap[service] = status + s.mu.Unlock() +} diff --git a/internal/v1/middleware/authentication.go b/internal/v1/middleware/authentication.go new file mode 100644 index 0000000..c201259 --- /dev/null +++ b/internal/v1/middleware/authentication.go @@ -0,0 +1,306 @@ +package middleware + +import ( + "context" + "encoding/json" + "fmt" + "strings" + + db "github.com/coinbase/baseca/db/sqlc" + "github.com/coinbase/baseca/internal/attestation/aws_iid" + lib "github.com/coinbase/baseca/internal/lib/authentication" + "github.com/coinbase/baseca/internal/lib/util/validator" + "github.com/coinbase/baseca/internal/logger" + "github.com/coinbase/baseca/internal/types" + "github.com/gogo/status" + "github.com/google/uuid" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/metadata" +) + +const ( + _pass_auth = "pass_authentication" + _service_auth = "service_authentication" + _provisioner_auth = "provisioner_authentication" +) + +func (m *Middleware) ServerAuthenticationInterceptor(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { + var auth string + var ok bool + + methods := map[string]string{ + "/grpc.health.v1.Health/Check": _pass_auth, + "/baseca.v1.Account/LoginUser": _pass_auth, + "/baseca.v1.Account/UpdateUserCredentials": _pass_auth, + "/baseca.v1.Certificate/SignCSR": _service_auth, + "/baseca.v1.Certificate/OperationsSignCSR": _provisioner_auth, + "/baseca.v1.Certificate/QueryCertificateMetadata": _provisioner_auth, + "/baseca.v1.Service/ProvisionServiceAccount": _provisioner_auth, + "/baseca.v1.Service/GetServiceAccountByMetadata": _provisioner_auth, + "/baseca.v1.Service/DeleteProvisionedServiceAccount": _provisioner_auth, + } + + if auth, ok = methods[info.FullMethod]; !ok { + md, ok := metadata.FromIncomingContext(ctx) + if !ok { + return nil, status.Errorf(codes.Internal, "failed to retrieve metadata from context") + } + + authorizationHeader, ok := md[authorizationHeaderKey] + if !ok || len(authorizationHeader) == 0 { + return nil, logger.RpcError(status.Error(codes.Unauthenticated, "authentication failed"), fmt.Errorf("request header empty: %s", authorizationHeaderKey)) + } + + if len(authorizationHeader) != 0 { + fields := strings.Fields(authorizationHeader[0]) + if len(fields) < 2 { + return nil, logger.RpcError(status.Error(codes.Unauthenticated, "authentication failed"), fmt.Errorf("authorization header not provided")) + } + + authorizationType := strings.ToLower(fields[0]) + if authorizationType != authorizationTypeBearer { + return nil, logger.RpcError(status.Error(codes.Unauthenticated, "authentication failed"), fmt.Errorf("authorization header not provided")) + } + + accessToken := fields[1] + payload, err := m.auth.Verify(ctx, accessToken) + if err != nil { + return nil, logger.RpcError(status.Error(codes.Unauthenticated, "authentication failed"), err) + } + + userPermission := payload.Permission + ok, err := m.enforcer.Enforce(userPermission, info.FullMethod) + if err != nil { + return nil, logger.RpcError(status.Error(codes.Unauthenticated, "authentication failed"), err) + } + if !ok { + return nil, logger.RpcError(status.Error(codes.Unauthenticated, "authentication failed"), fmt.Errorf("invalid permission error %s", userPermission)) + } + ctx = context.WithValue(ctx, types.UserAuthenticationContextKey, payload) + } + } else if auth == _service_auth { + service, err := m.AuthenticateServiceAccount(ctx) + if err != nil { + return nil, err + } + + ctx = context.WithValue(ctx, types.ServiceAuthenticationContextKey, service) + } else if auth == _provisioner_auth { + service, err := m.AuthenticateProvisionerAccount(ctx) + if err != nil { + return nil, err + } + + ctx = context.WithValue(ctx, types.ProvisionerAuthenticationContextKey, service) + } + return handler(ctx, req) +} + +func (m *Middleware) AuthenticateServiceAccount(ctx context.Context) (*types.ServiceAccountPayload, error) { + md, ok := metadata.FromIncomingContext(ctx) + if !ok { + return nil, status.Errorf(codes.Internal, "failed to retrieve metadata from context") + } + + clientIdAuthorizationHeader, ok := md[clientIdAuthorizationHeaderKey] + if !ok { + return nil, status.Errorf(codes.InvalidArgument, "authorization header not provided") + } + + clientTokenAuthorizationHeader, ok := md[clientTokenAuthorizationHeaderKey] + if !ok { + return nil, status.Errorf(codes.InvalidArgument, "authorization header not provided") + } + + client_uuid, err := uuid.Parse(clientIdAuthorizationHeader[0]) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid authorization header") + } + + cachedServiceAccount, err := m.authenticationCacheServiceAccount(ctx, client_uuid) + if err != nil { + return nil, logger.RpcError(status.Error(codes.Internal, "internal server error"), err) + } + + account := cachedServiceAccount.ServiceAccount + if err := lib.CheckPassword(clientTokenAuthorizationHeader[0], account.ApiToken); err != nil { + return nil, logger.RpcError(status.Error(codes.Unauthenticated, "authentication error"), err) + } + + for _, node_attestation := range account.NodeAttestation { + clientIdentityDocumentHeader, ok := md[clientIdentityDocumentHeaderKey] + if !ok { + return nil, status.Errorf(codes.InvalidArgument, "authorization header not provided") + } + + switch node_attestation { + case "AWS_IID": + // Compare Signed Node Data with Attestation Table in Database + attestation_err := aws_iid.AWSIidNodeAttestation(client_uuid, clientIdentityDocumentHeader[0], cachedServiceAccount.AwsIid, m.cache) + if attestation_err != nil { + return nil, logger.RpcError(status.Error(codes.Unauthenticated, "aws_iid attestation error"), attestation_err) + } + } + } + + service := &types.ServiceAccountPayload{ + ServiceID: account.ClientID, + ServiceAccount: account.ServiceAccount, + Environment: account.Environment, + ValidSubjectAlternateName: account.ValidSubjectAlternateName, + ValidCertificateAuthorities: account.ValidCertificateAuthorities, + CertificateValidity: account.CertificateValidity, + SubordinateCa: account.SubordinateCa, + ExtendedKey: account.ExtendedKey, + SANRegularExpression: validator.NullStringToString(&account.RegularExpression), + } + + return service, nil +} + +func (m *Middleware) AuthenticateProvisionerAccount(ctx context.Context) (*types.ProvisionerAccountPayload, error) { + md, ok := metadata.FromIncomingContext(ctx) + if !ok { + return nil, status.Errorf(codes.Internal, "failed to retrieve metadata from context") + } + + clientIdAuthorizationHeader, ok := md[clientIdAuthorizationHeaderKey] + if !ok { + return nil, status.Errorf(codes.InvalidArgument, "authorization header not provided") + } + + clientTokenAuthorizationHeader, ok := md[clientTokenAuthorizationHeaderKey] + if !ok { + return nil, status.Errorf(codes.InvalidArgument, "authorization header not provided") + } + + client_uuid, err := uuid.Parse(clientIdAuthorizationHeader[0]) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid authorization header") + } + + cachedProvisionerAccount, err := m.authenticationCacheProvisionerAccount(ctx, client_uuid) + if err != nil { + return nil, logger.RpcError(status.Error(codes.Internal, "internal server error"), err) + } + + account := cachedProvisionerAccount.ProvisionerAccount + if err := lib.CheckPassword(clientTokenAuthorizationHeader[0], account.ApiToken); err != nil { + return nil, logger.RpcError(status.Error(codes.Unauthenticated, "authentication error"), err) + } + + for _, node_attestation := range account.NodeAttestation { + clientIdentityDocumentHeader, ok := md[clientIdentityDocumentHeaderKey] + if !ok { + return nil, status.Errorf(codes.InvalidArgument, "authorization header not provided") + } + + switch node_attestation { + case "AWS_IID": + // Compare Signed Node Data with Attestation Table in Database + attestation_err := aws_iid.AWSIidNodeAttestation(client_uuid, clientIdentityDocumentHeader[0], cachedProvisionerAccount.AwsIid, m.cache) + if attestation_err != nil { + return nil, logger.RpcError(status.Error(codes.Unauthenticated, "aws_iid attestation error"), attestation_err) + } + } + } + + service := &types.ProvisionerAccountPayload{ + ClientId: account.ClientID, + ProvisionerAccount: account.ProvisionerAccount, + Environments: account.Environments, + ValidSubjectAlternateNames: account.ValidSubjectAlternateNames, + MaxCertificateValidity: uint32(account.MaxCertificateValidity), + ExtendedKeys: account.ExtendedKeys, + RegularExpression: validator.NullStringToString(&account.RegularExpression), + } + + return service, nil +} + +func (m *Middleware) authenticationCacheServiceAccount(ctx context.Context, client_uuid uuid.UUID) (*db.CachedServiceAccount, error) { + var service_account *db.Account + var instance_identity_document *db.AwsAttestation + var cached_service_account db.CachedServiceAccount + var err error + + db_reader := m.store.Reader + uuid := client_uuid.String() + if value, cached := m.cache.Get(uuid); cached == nil { + err = json.Unmarshal(value, &cached_service_account) + if err != nil { + return &cached_service_account, fmt.Errorf("error unmarshal cached service account account, %s", err) + } + } else { + service_account, err = db_reader.GetServiceUUID(ctx, client_uuid) + if err != nil { + return &cached_service_account, fmt.Errorf("service authentication failed: %s", err) + } + + cached_service_account.ServiceAccount = *service_account + for _, node_attestation := range service_account.NodeAttestation { + switch node_attestation { + case types.Attestation.AWS_IID: + instance_identity_document, err = aws_iid.GetInstanceIdentityDocument(ctx, db_reader, client_uuid) + if err != nil { + return &cached_service_account, fmt.Errorf("aws_iid node attestation failed: %s", err) + } + cached_service_account.AwsIid = *instance_identity_document + } + } + + data, err := json.Marshal(cached_service_account) + if err != nil { + return &cached_service_account, fmt.Errorf("error marshalling cached_service_account, %s", err) + } + err = m.cache.Set(uuid, data) + if err != nil { + return &cached_service_account, fmt.Errorf("error setting middleware cache, %s", err) + } + } + return &cached_service_account, nil +} + +func (m *Middleware) authenticationCacheProvisionerAccount(ctx context.Context, client_uuid uuid.UUID) (*db.CachedProvisionerAccount, error) { + var provisioner_account *db.Provisioner + var instance_identity_document *db.AwsAttestation + var cached_provisioner_account db.CachedProvisionerAccount + var err error + + db_reader := m.store.Reader + uuid := client_uuid.String() + if value, cached := m.cache.Get(uuid); cached == nil { + err = json.Unmarshal(value, &cached_provisioner_account) + if err != nil { + return &cached_provisioner_account, fmt.Errorf("error unmarshal cached service account account, %s", err) + } + } else { + provisioner_account, err = db_reader.GetProvisionerUUID(ctx, client_uuid) + if err != nil { + return &cached_provisioner_account, fmt.Errorf("service authentication failed: %s", err) + } + + cached_provisioner_account.ProvisionerAccount = *provisioner_account + for _, node_attestation := range provisioner_account.NodeAttestation { + switch node_attestation { + case types.Attestation.AWS_IID: + instance_identity_document, err = aws_iid.GetInstanceIdentityDocument(ctx, db_reader, client_uuid) + if err != nil { + return &cached_provisioner_account, fmt.Errorf("aws_iid node attestation failed: %s", err) + } + cached_provisioner_account.AwsIid = *instance_identity_document + } + } + + data, err := json.Marshal(cached_provisioner_account) + if err != nil { + return &cached_provisioner_account, fmt.Errorf("error marshalling cached_service_account, %s", err) + } + err = m.cache.Set(uuid, data) + if err != nil { + return &cached_provisioner_account, fmt.Errorf("error setting middleware cache, %s", err) + } + } + return &cached_provisioner_account, nil +} diff --git a/internal/v1/middleware/middleware.go b/internal/v1/middleware/middleware.go new file mode 100644 index 0000000..bd972b8 --- /dev/null +++ b/internal/v1/middleware/middleware.go @@ -0,0 +1,35 @@ +package middleware + +import ( + "github.com/allegro/bigcache/v3" + "github.com/casbin/casbin/v2" + db "github.com/coinbase/baseca/db/sqlc" + lib "github.com/coinbase/baseca/internal/lib/authentication" +) + +const ( + // User Authorization + authorizationHeaderKey = "authorization" + authorizationTypeBearer = "bearer" + + // Service Authorization + clientIdAuthorizationHeaderKey = "x-baseca-client-id" // #nosec G101 False Positive + clientTokenAuthorizationHeaderKey = "x-baseca-client-token" // #nosec G101 False Positive + clientIdentityDocumentHeaderKey = "x-baseca-instance-metadata" +) + +type Middleware struct { + auth lib.Auth + store db.DatabaseEndpoints + enforcer *casbin.Enforcer + cache *bigcache.BigCache +} + +func New(auth lib.Auth, endpoints db.DatabaseEndpoints, enforcer *casbin.Enforcer, cache *bigcache.BigCache) *Middleware { + return &Middleware{ + auth: auth, + store: endpoints, + enforcer: enforcer, + cache: cache, + } +} diff --git a/internal/v1/users/operations.go b/internal/v1/users/operations.go new file mode 100644 index 0000000..acce65b --- /dev/null +++ b/internal/v1/users/operations.go @@ -0,0 +1,251 @@ +package users + +import ( + "context" + "database/sql" + "fmt" + + db "github.com/coinbase/baseca/db/sqlc" + apiv1 "github.com/coinbase/baseca/gen/go/baseca/v1" + lib "github.com/coinbase/baseca/internal/lib/authentication" + "github.com/coinbase/baseca/internal/lib/util/validator" + "github.com/coinbase/baseca/internal/logger" + "github.com/gogo/status" + "github.com/google/uuid" + passwordvalidator "github.com/wagslane/go-password-validator" + "google.golang.org/grpc/codes" + "google.golang.org/protobuf/types/known/emptypb" + "google.golang.org/protobuf/types/known/timestamppb" +) + +const ( + MIN_ENTROPY_BITS = 60 + _default_authentication_duration = 15 +) + +func (u *User) LoginUser(ctx context.Context, req *apiv1.LoginUserRequest) (*apiv1.LoginUserResponse, error) { + user, err := u.store.Reader.GetUser(ctx, req.Username) + if err != nil { + if err == sql.ErrNoRows { + return nil, logger.RpcError(status.Error(codes.NotFound, "user not found"), err) + } + return nil, logger.RpcError(status.Error(codes.Unauthenticated, "authentication failed"), err) + } + + if err := lib.CheckPassword(req.Password, user.HashedCredential); err != nil { + return nil, logger.RpcError(status.Error(codes.Unauthenticated, "authentication failed"), err) + } + + validity := func() int64 { + if u.validity == 0 { + return _default_authentication_duration + } + return int64(u.validity) + }() + + accessToken, err := u.auth.Issue(ctx, lib.ClaimProps{ + Subject: user.Uuid, + Permission: user.Permissions, + ValidForMinutes: validity, + }) + if err != nil { + return nil, logger.RpcError(status.Error(codes.Unauthenticated, "authentication failed"), err) + } + + response := apiv1.LoginUserResponse{ + AccessToken: *accessToken, + User: &apiv1.User{ + Uuid: user.Uuid.String(), + Username: user.Username, + FullName: user.FullName, + Email: user.Email, + Permissions: user.Permissions, + CredentialChangedAt: timestamppb.New(user.CredentialChangedAt), + CreatedAt: timestamppb.New(user.CreatedAt)}, + } + return &response, nil +} + +func (u *User) CreateUser(ctx context.Context, req *apiv1.CreateUserRequest) (*apiv1.User, error) { + if !validator.IsSupportedPermission(req.Permissions) { + return nil, logger.RpcError(status.Error(codes.InvalidArgument, "invalid permission field"), fmt.Errorf("invalid permission %s", req.Permissions)) + } + + err := passwordvalidator.Validate(req.Password, MIN_ENTROPY_BITS) + if err != nil { + return nil, logger.RpcError(status.Error(codes.InvalidArgument, "minimum password strength requirement not met"), err) + } + + hashedCredential, err := lib.HashPassword(req.Password) + if err != nil { + return nil, logger.RpcError(status.Error(codes.Unauthenticated, "authentication error"), err) + } + + if !validator.ValidateEmail(req.Email) { + return nil, logger.RpcError(status.Error(codes.InvalidArgument, "invalid email"), fmt.Errorf("invalid email %s", req.Email)) + } + + if len(req.Username) == 0 { + return nil, logger.RpcError(status.Error(codes.InvalidArgument, "invalid username"), fmt.Errorf("invalid username [%s]", req.Username)) + } + + if _, err = u.store.Reader.GetUser(ctx, req.Username); err != sql.ErrNoRows { + return nil, logger.RpcError(status.Error(codes.InvalidArgument, "invalid username"), fmt.Errorf("user exists %s", req.Username)) + } + + user_uuid, err := uuid.NewRandom() + if err != nil { + return nil, logger.RpcError(status.Error(codes.Internal, "internal server error"), err) + } + + arg := db.CreateUserParams{ + Uuid: user_uuid, + Username: req.Username, + HashedCredential: hashedCredential, + FullName: req.FullName, + Email: req.Email, + Permissions: req.Permissions, + } + + user, err := u.store.Writer.CreateUser(ctx, arg) + if err != nil { + return nil, logger.RpcError(status.Error(codes.Internal, "internal server error"), err) + } + + return &apiv1.User{ + Uuid: user.Uuid.String(), + Username: user.Username, + FullName: user.FullName, + Email: user.Email, + Permissions: user.Permissions, + CredentialChangedAt: timestamppb.New(user.CredentialChangedAt), + CreatedAt: timestamppb.New(user.CreatedAt)}, nil +} + +func (u *User) DeleteUser(ctx context.Context, req *apiv1.UsernameRequest) (*emptypb.Empty, error) { + if len(req.Username) == 0 { + return &emptypb.Empty{}, logger.RpcError(status.Error(codes.InvalidArgument, "invalid argument"), fmt.Errorf("v1.Accounts/DeleteUser invalid argument")) + } + + err := u.store.Writer.DeleteUser(ctx, req.Username) + if err != nil { + return &emptypb.Empty{}, logger.RpcError(status.Error(codes.Internal, "internal server error"), err) + } + return &emptypb.Empty{}, nil +} + +func (u *User) GetUser(ctx context.Context, req *apiv1.UsernameRequest) (*apiv1.User, error) { + if len(req.Username) == 0 { + return nil, logger.RpcError(status.Error(codes.Internal, "internal server error"), fmt.Errorf("v1.Accounts/GetUser invalid argument")) + } + + user, err := u.store.Reader.GetUser(ctx, req.Username) + if err != nil { + if err == sql.ErrNoRows { + return nil, logger.RpcError(status.Error(codes.NotFound, "user not found"), err) + } + return nil, logger.RpcError(status.Error(codes.Internal, "internal server error"), err) + } + + return &apiv1.User{ + Uuid: user.Uuid.String(), + Username: user.Username, + FullName: user.FullName, + Email: user.Email, + Permissions: user.Permissions, + CredentialChangedAt: timestamppb.New(user.CredentialChangedAt), + CreatedAt: timestamppb.New(user.CreatedAt)}, nil +} + +func (u *User) ListUsers(ctx context.Context, req *apiv1.QueryParameter) (*apiv1.Users, error) { + var userData apiv1.Users + + if req.PageId <= 0 || req.PageSize <= 0 { + return nil, logger.RpcError(status.Error(codes.InvalidArgument, "invalid request parameters"), fmt.Errorf("invalid page_id or page_size")) + } + + arg := db.ListUsersParams{ + Limit: req.PageSize, + Offset: (req.PageId - 1) * req.PageSize, + } + users, err := u.store.Reader.ListUsers(ctx, arg) + if err != nil { + return nil, logger.RpcError(status.Error(codes.InvalidArgument, "invalid request"), err) + } + + for _, user := range users { + userData.Users = append(userData.Users, &apiv1.User{ + Uuid: user.Uuid.String(), + Username: user.Username, + FullName: user.FullName, + Email: user.Email, + Permissions: user.Permissions, + CredentialChangedAt: timestamppb.New(user.CredentialChangedAt), + CreatedAt: timestamppb.New(user.CreatedAt)}) + } + return &userData, nil +} + +func (u *User) UpdateUserPermissions(ctx context.Context, req *apiv1.UpdatePermissionsRequest) (*apiv1.User, error) { + if !validator.IsSupportedPermission(req.Permissions) { + return nil, logger.RpcError(status.Error(codes.InvalidArgument, "invalid permission field"), fmt.Errorf("invalid permission %s", req.Permissions)) + } + + arg := db.UpdateUserPermissionParams{ + Username: req.Username, + Permissions: req.Permissions, + } + + user, err := u.store.Writer.UpdateUserPermission(ctx, arg) + if err != nil { + return nil, logger.RpcError(status.Error(codes.Internal, "internal server error"), err) + } + + return &apiv1.User{ + Uuid: user.Uuid.String(), + Username: user.Username, + FullName: user.FullName, + Email: user.Email, + Permissions: user.Permissions, + CredentialChangedAt: timestamppb.New(user.CredentialChangedAt), + CreatedAt: timestamppb.New(user.CreatedAt)}, nil +} + +func (u *User) UpdateUserCredentials(ctx context.Context, req *apiv1.UpdateCredentialsRequest) (*apiv1.User, error) { + user, err := u.store.Reader.GetUser(ctx, req.Username) + if err != nil { + return nil, logger.RpcError(status.Error(codes.Internal, "internal server error"), err) + } + + if err := lib.CheckPassword(req.Password, user.HashedCredential); err != nil { + return nil, logger.RpcError(status.Error(codes.PermissionDenied, "authentication failed"), err) + } + + err = passwordvalidator.Validate(req.UpdatedPassword, MIN_ENTROPY_BITS) + if err != nil { + return nil, logger.RpcError(status.Error(codes.InvalidArgument, "minimum password strength requirement not met"), err) + } + + hashedCredential, err := lib.HashPassword(req.UpdatedPassword) + if err != nil { + return nil, logger.RpcError(status.Error(codes.Internal, "internal server error"), err) + } + + arg := db.UpdateUserAuthenticationParams{ + Username: req.Username, + HashedCredential: hashedCredential, + } + + user, err = u.store.Writer.UpdateUserAuthentication(ctx, arg) + if err != nil { + return nil, logger.RpcError(status.Error(codes.Internal, "internal server error"), err) + } + + return &apiv1.User{ + Username: user.Username, + FullName: user.FullName, + Email: user.Email, + Permissions: user.Permissions, + CredentialChangedAt: timestamppb.New(user.CredentialChangedAt), + CreatedAt: timestamppb.New(user.CreatedAt)}, nil +} diff --git a/internal/v1/users/operations_test.go b/internal/v1/users/operations_test.go new file mode 100644 index 0000000..58f4a2c --- /dev/null +++ b/internal/v1/users/operations_test.go @@ -0,0 +1,147 @@ +package users + +import ( + "context" + "database/sql" + "fmt" + "reflect" + "testing" + + "github.com/coinbase/baseca/db/mock" + db "github.com/coinbase/baseca/db/sqlc" + apiv1 "github.com/coinbase/baseca/gen/go/baseca/v1" + lib "github.com/coinbase/baseca/internal/lib/authentication" + "github.com/coinbase/baseca/internal/lib/util" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" +) + +const ( + _read = "READ" +) + +func TestCreateUser(t *testing.T) { + user, user_credentials := util.GenerateTestUser(t, _read, 20) + + cases := []struct { + name string + req *apiv1.CreateUserRequest + build func(store *mock.MockStore) + check func(t *testing.T, res *apiv1.User, err error) + }{ + { + name: "OK", + req: &apiv1.CreateUserRequest{ + Username: user.Username, + Password: user_credentials, + FullName: user.FullName, + Email: user.Email, + Permissions: user.Permissions, + }, + build: func(store *mock.MockStore) { + arg := db.CreateUserParams{ + Uuid: user.Uuid, + Username: user.Username, + FullName: user.FullName, + Email: user.Email, + Permissions: user.Permissions, + } + store.EXPECT().GetUser(gomock.Any(), arg.Username).Times(1).Return(nil, sql.ErrNoRows) + store.EXPECT().CreateUser(gomock.Any(), EqCreateUserParams(arg, user_credentials)).Times(1).Return(&user, nil) + }, + check: func(t *testing.T, res *apiv1.User, err error) { + require.NoError(t, err) + }, + }, + } + + for elem := range cases { + tc := cases[elem] + + t.Run(tc.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + store := mock.NewMockStore(ctrl) + tc.build(store) + + user, err := buildUsersConfig(store) + require.NoError(t, err) + + res, err := user.CreateUser(context.Background(), tc.req) + tc.check(t, res, err) + }) + } +} + +func TestLoginUser(t *testing.T) { + user, user_credentials := util.GenerateTestUser(t, _read, 20) + + cases := []struct { + name string + req *apiv1.LoginUserRequest + build func(store *mock.MockStore) + check func(t *testing.T, res *apiv1.LoginUserResponse, err error) + }{ + { + name: "OK", + req: &apiv1.LoginUserRequest{ + Username: user.Username, + Password: user_credentials, + }, + build: func(store *mock.MockStore) { + store.EXPECT().GetUser(gomock.Any(), user.Username).Times(1).Return(&user, nil) + }, + check: func(t *testing.T, res *apiv1.LoginUserResponse, err error) { + require.NoError(t, err) + }, + }, + } + + for elem := range cases { + tc := cases[elem] + + t.Run(tc.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + store := mock.NewMockStore(ctrl) + tc.build(store) + + user, err := buildUsersConfig(store) + require.NoError(t, err) + + res, err := user.LoginUser(context.Background(), tc.req) + tc.check(t, res, err) + }) + } +} + +type eqCreateUserParamsMatcher struct { + arg db.CreateUserParams + password string +} + +func (e eqCreateUserParamsMatcher) Matches(x any) bool { + arg, ok := x.(db.CreateUserParams) + if !ok { + return false + } + + err := lib.CheckPassword(e.password, arg.HashedCredential) + if err != nil { + return false + } + + e.arg.Uuid = arg.Uuid + e.arg.HashedCredential = arg.HashedCredential + return reflect.DeepEqual(e.arg, arg) +} + +func (e eqCreateUserParamsMatcher) String() string { + return fmt.Sprintf("%v", e.arg) +} + +func EqCreateUserParams(arg db.CreateUserParams, password string) gomock.Matcher { + return eqCreateUserParamsMatcher{arg, password} +} diff --git a/internal/v1/users/users.go b/internal/v1/users/users.go new file mode 100644 index 0000000..ede8c9b --- /dev/null +++ b/internal/v1/users/users.go @@ -0,0 +1,23 @@ +package users + +import ( + db "github.com/coinbase/baseca/db/sqlc" + apiv1 "github.com/coinbase/baseca/gen/go/baseca/v1" + "github.com/coinbase/baseca/internal/config" + lib "github.com/coinbase/baseca/internal/lib/authentication" +) + +type User struct { + apiv1.AccountServer + store db.DatabaseEndpoints + auth lib.Auth + validity int +} + +func New(cfg *config.Config, endpoints db.DatabaseEndpoints, auth lib.Auth) *User { + return &User{ + store: endpoints, + auth: auth, + validity: cfg.KMS.AuthValidity, + } +} diff --git a/internal/v1/users/users_test.go b/internal/v1/users/users_test.go new file mode 100644 index 0000000..0c0f300 --- /dev/null +++ b/internal/v1/users/users_test.go @@ -0,0 +1,59 @@ +package users + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/service/kms" + mock_store "github.com/coinbase/baseca/db/mock" + db "github.com/coinbase/baseca/db/sqlc" + lib "github.com/coinbase/baseca/internal/lib/authentication" + "github.com/coinbase/baseca/test" + "github.com/stretchr/testify/mock" +) + +type mockedKmsClient struct { + mock.Mock +} + +func (m *mockedKmsClient) Sign(ctx context.Context, signInput *kms.SignInput, opts ...func(*kms.Options)) (*kms.SignOutput, error) { + ret := m.Called(ctx, signInput, opts) + return ret.Get(0).(*kms.SignOutput), ret.Error(1) +} + +func (m *mockedKmsClient) Verify(ctx context.Context, params *kms.VerifyInput, optFns ...func(*kms.Options)) (*kms.VerifyOutput, error) { + ret := m.Called(ctx, params, optFns) + return ret.Get(0).(*kms.VerifyOutput), ret.Error(1) +} + +func buildUsersConfig(store *mock_store.MockStore) (*User, error) { + config, err := test.GetTestConfigurationPath() + if err != nil { + return nil, err + } + + mockKms := &mockedKmsClient{} + mockKms.On("Sign", mock.Anything, mock.Anything, mock.Anything).Return( + &kms.SignOutput{ + Signature: []byte("signature"), + }, nil, + ) + + signer := &lib.Client{ + KmsClient: mockKms, + KeyId: config.KMS.KeyId, + SigningAlgorithm: config.KMS.SigningAlgorithm, + } + + auth, err := lib.NewAuthSigningMetadata(signer) + if err != nil { + return nil, err + } + + endpoints := db.DatabaseEndpoints{Writer: store, Reader: store} + + return &User{ + store: endpoints, + auth: auth, + validity: config.KMS.AuthValidity, + }, nil +} diff --git a/pkg/attestor/aws_iid/iid.go b/pkg/attestor/aws_iid/iid.go new file mode 100644 index 0000000..6990435 --- /dev/null +++ b/pkg/attestor/aws_iid/iid.go @@ -0,0 +1,57 @@ +package aws_iid + +import ( + "bytes" + "encoding/json" + "io" + "net/http" +) + +const ( + instance_identity_metadata = "http://169.254.169.254/latest/dynamic/instance-identity/document" + instance_identity_signature = "http://169.254.169.254/latest/dynamic/instance-identity/signature" +) + +type EC2InstanceMetadata struct { + InstanceIdentityDocument []byte `json:"instance_identity_document"` + InstanceIdentitySignature []byte `json:"instance_identity_signature"` +} + +func BuildInstanceMetadata() (*string, error) { + instance_metadata, err := httpGetRequest(instance_identity_metadata) + if err != nil { + return nil, err + } + + rsa_signature, err := httpGetRequest(instance_identity_signature) + if err != nil { + return nil, err + } + + metadata := EC2InstanceMetadata{ + InstanceIdentityDocument: instance_metadata, + InstanceIdentitySignature: rsa_signature, + } + + metadata_json, err := json.Marshal(metadata) + if err != nil { + return nil, err + } + + output := bytes.NewBuffer(metadata_json).String() + return &output, err +} + +func httpGetRequest(uri string) ([]byte, error) { + response, err := http.Get(uri) // #nosec G107 False Positive + if err != nil { + return nil, err + } else { + defer response.Body.Close() // #nosec G307 False Positive + response, err := io.ReadAll(response.Body) + if err != nil { + return nil, err + } + return response, nil + } +} diff --git a/pkg/client/certificate.go b/pkg/client/certificate.go new file mode 100644 index 0000000..a5e6ea4 --- /dev/null +++ b/pkg/client/certificate.go @@ -0,0 +1,37 @@ +package baseca + +import ( + "context" + + apiv1 "github.com/coinbase/baseca/gen/go/baseca/v1" + "github.com/coinbase/baseca/pkg/types" + "github.com/coinbase/baseca/pkg/util" +) + +func (c *Client) IssueCertificate(certificateRequest CertificateRequest) (*apiv1.SignedCertificate, error) { + signingRequest, err := GenerateCSR(certificateRequest) + if err != nil { + return nil, err + } + + req := apiv1.CertificateSigningRequest{ + CertificateSigningRequest: signingRequest.CSR.String(), + } + + signedCertificate, err := c.Certificate.SignCSR(context.Background(), &req) + if err != nil { + return nil, err + } + + err = util.ParseCertificateFormat(signedCertificate, types.SignedCertificate{ + CertificatePath: certificateRequest.Output.Certificate, + IntermediateCertificateChainPath: certificateRequest.Output.IntermediateCertificateChain, + RootCertificateChainPath: certificateRequest.Output.RootCertificateChain, + }) + + if err != nil { + return nil, err + } + + return signedCertificate, nil +} diff --git a/pkg/client/client.go b/pkg/client/client.go new file mode 100644 index 0000000..a19e74f --- /dev/null +++ b/pkg/client/client.go @@ -0,0 +1,162 @@ +package baseca + +import ( + "context" + "crypto/tls" + "fmt" + "strings" + "sync" + + apiv1 "github.com/coinbase/baseca/gen/go/baseca/v1" + "github.com/coinbase/baseca/pkg/attestor/aws_iid" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/metadata" + "google.golang.org/protobuf/types/known/emptypb" +) + +const ( + _client_id_header = "X-BASECA-CLIENT-ID" + _client_token_header = "X-BASECA-CLIENT-TOKEN" // #nosec G101 False Positive + _aws_iid_metadata = "X-BASECA-INSTANCE-METADATA" + _account_auth_header = "AUTHORIZATION" +) + +type Client struct { + Endpoint string + Authentication Authentication + Attestation string + Certificate apiv1.CertificateClient + Service apiv1.ServiceClient +} + +type AccountClient interface { + LoginUser(ctx context.Context, in *apiv1.LoginUserRequest, opts ...grpc.CallOption) (*apiv1.LoginUserResponse, error) + DeleteUser(ctx context.Context, in *apiv1.UsernameRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) + GetUser(ctx context.Context, in *apiv1.UsernameRequest, opts ...grpc.CallOption) (*apiv1.User, error) + ListUsers(ctx context.Context, in *apiv1.QueryParameter, opts ...grpc.CallOption) (*apiv1.Users, error) + CreateUser(ctx context.Context, in *apiv1.CreateUserRequest, opts ...grpc.CallOption) (*apiv1.User, error) + UpdateUserCredentials(ctx context.Context, in *apiv1.UpdateCredentialsRequest, opts ...grpc.CallOption) (*apiv1.User, error) + UpdateUserPermissions(ctx context.Context, in *apiv1.UpdatePermissionsRequest, opts ...grpc.CallOption) (*apiv1.User, error) +} + +type CertificateClient interface { + SignCSR(ctx context.Context, in *apiv1.CertificateSigningRequest, opts ...grpc.CallOption) (*apiv1.SignedCertificate, error) + GetCertificate(ctx context.Context, in *apiv1.CertificateSerialNumber, opts ...grpc.CallOption) (*apiv1.CertificateParameter, error) + ListCertificates(ctx context.Context, in *apiv1.ListCertificatesRequest, opts ...grpc.CallOption) (*apiv1.CertificatesParameter, error) + RevokeCertificate(ctx context.Context, in *apiv1.RevokeCertificateRequest, opts ...grpc.CallOption) (*apiv1.RevokeCertificateResponse, error) + OperationsSignCSR(ctx context.Context, in *apiv1.OperationsSignRequest, opts ...grpc.CallOption) (*apiv1.SignedCertificate, error) + QueryCertificateMetadata(ctx context.Context, in *apiv1.QueryCertificateMetadataRequest, opts ...grpc.CallOption) (*apiv1.CertificatesParameter, error) +} + +type ServiceClient interface { + CreateServiceAccount(ctx context.Context, in *apiv1.CreateServiceAccountRequest, opts ...grpc.CallOption) (*apiv1.CreateServiceAccountResponse, error) + CreateProvisionerAccount(ctx context.Context, in *apiv1.CreateProvisionerAccountRequest, opts ...grpc.CallOption) (*apiv1.CreateProvisionerAccountResponse, error) + GetProvisionerAccount(ctx context.Context, in *apiv1.AccountId, opts ...grpc.CallOption) (*apiv1.ProvisionerAccount, error) + ListProvisionerAccounts(ctx context.Context, in *apiv1.QueryParameter, opts ...grpc.CallOption) (*apiv1.ProvisionerAccounts, error) + ProvisionServiceAccount(ctx context.Context, in *apiv1.ProvisionServiceAccountRequest, opts ...grpc.CallOption) (*apiv1.ProvisionServiceAccountResponse, error) + ListServiceAccounts(ctx context.Context, in *apiv1.QueryParameter, opts ...grpc.CallOption) (*apiv1.ServiceAccounts, error) + GetServiceAccount(ctx context.Context, in *apiv1.AccountId, opts ...grpc.CallOption) (*apiv1.ServiceAccount, error) + GetServiceAccountMetadata(ctx context.Context, in *apiv1.GetServiceAccountMetadataRequest, opts ...grpc.CallOption) (*apiv1.ServiceAccounts, error) + DeleteServiceAccount(ctx context.Context, in *apiv1.AccountId, opts ...grpc.CallOption) (*emptypb.Empty, error) + DeleteProvisionerAccount(ctx context.Context, in *apiv1.AccountId, opts ...grpc.CallOption) (*emptypb.Empty, error) + DeleteProvisionedServiceAccount(ctx context.Context, in *apiv1.AccountId, opts ...grpc.CallOption) (*emptypb.Empty, error) +} + +func LoadDefaultConfiguration(configuration Configuration, attestation string, authentication Authentication) (*Client, error) { + c := Client{ + Endpoint: configuration.URL, + Authentication: authentication, + Attestation: attestation, + } + + if configuration.Environment == Env.Local { + conn, err := grpc.Dial(configuration.URL, grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithUnaryInterceptor(c.methodInterceptor())) + if err != nil { + return nil, fmt.Errorf("failed to initialize grpc client") + } + c.Certificate = apiv1.NewCertificateClient(conn) + return &c, nil + } else { + conn, err := grpc.Dial(configuration.URL, grpc.WithTransportCredentials( + credentials.NewTLS(&tls.Config{ + MinVersion: tls.VersionTLS12, + }), + ), grpc.WithUnaryInterceptor(c.methodInterceptor())) + if err != nil { + return nil, fmt.Errorf("failed to initialize grpc client") + } + c.Certificate = apiv1.NewCertificateClient(conn) + return &c, nil + } +} + +func (c *Client) methodInterceptor() grpc.UnaryClientInterceptor { + methodOptions := map[string]grpc.UnaryClientInterceptor{ + // Certificate Interface + "/baseca.v1.Certificate/SignCSR": c.clientAuthUnaryInterceptor, + "/baseca.v1.Certificate/OperationsSignCSR": c.clientAuthUnaryInterceptor, + "/baseca.v1.Certificate/QueryCertificateMetadata": c.clientAuthUnaryInterceptor, + + // Service Interface + "/baseca.v1.Service/ProvisionServiceAccount": c.clientAuthUnaryInterceptor, + "/baseca.v1.Service/GetServiceAccountByMetadata": c.clientAuthUnaryInterceptor, + "/baseca.v1.Service/DeleteProvisionedServiceAccount": c.clientAuthUnaryInterceptor, + + // Account Interface + "/baseca.v1.Account/LoginUser": c.accountAuthUnaryInterceptor, + // TODO: Add Additional RPC Methods + } + return mapMethodInterceptor(methodOptions) +} + +func mapMethodInterceptor(chain map[string]grpc.UnaryClientInterceptor) grpc.UnaryClientInterceptor { + var chainMap sync.Map + for k, v := range chain { + chainMap.Store(k, v) + } + return func(parentCtx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { + if next, ok := returnMethodInterceptor(chainMap, method); ok { + return next(parentCtx, method, req, reply, cc, invoker, opts...) + } + return invoker(parentCtx, method, req, reply, cc, opts...) + } +} + +func returnMethodInterceptor(chainMap sync.Map, method string) (grpc.UnaryClientInterceptor, bool) { + if m, ok := chainMap.Load(method); ok { + return m.(grpc.UnaryClientInterceptor), true + } + i := strings.LastIndex(method, "/") + if m, ok := chainMap.Load(method[:i+1]); ok { + return m.(grpc.UnaryClientInterceptor), true + } + if m, ok := chainMap.Load(""); ok { + return m.(grpc.UnaryClientInterceptor), true + } + return nil, false +} + +func (c *Client) clientAuthUnaryInterceptor(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { + ctx = metadata.AppendToOutgoingContext(ctx, _client_id_header, c.Authentication.ClientId) + ctx = metadata.AppendToOutgoingContext(ctx, _client_token_header, c.Authentication.ClientToken) + + if c.Attestation == Attestation.AWS { + instance_metadata, err := aws_iid.BuildInstanceMetadata() + if err != nil { + return fmt.Errorf("error generating aws_iid node attestation") + } + ctx = metadata.AppendToOutgoingContext(ctx, _aws_iid_metadata, *instance_metadata) + } + + err := invoker(ctx, method, req, reply, cc, opts...) + return err +} + +func (c *Client) accountAuthUnaryInterceptor(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { + ctx = metadata.AppendToOutgoingContext(ctx, _account_auth_header, fmt.Sprintf("Bearer %s", c.Authentication.AuthToken)) + + err := invoker(ctx, method, req, reply, cc, opts...) + return err +} diff --git a/pkg/client/csr.go b/pkg/client/csr.go new file mode 100644 index 0000000..f469571 --- /dev/null +++ b/pkg/client/csr.go @@ -0,0 +1,101 @@ +package baseca + +import ( + "bytes" + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "fmt" + "os" + + "github.com/coinbase/baseca/pkg/crypto" + "github.com/coinbase/baseca/pkg/types" +) + +func GenerateCSR(csr CertificateRequest) (*types.SigningRequest, error) { + var generator crypto.CSRGenerator + + switch csr.PublicKeyAlgorithm { + case x509.RSA: + if _, ok := types.PublicKeyAlgorithms["RSA"].KeySize[csr.KeySize]; !ok { + return nil, fmt.Errorf("rsa invalid key size %d", csr.KeySize) + } + if _, ok := types.PublicKeyAlgorithms["RSA"].SigningAlgorithm[csr.SigningAlgorithm]; !ok { + return nil, fmt.Errorf("rsa invalid signing algorithm %s", csr.SigningAlgorithm) + } + generator = &crypto.SigningRequestGeneratorRSA{Size: csr.KeySize} + case x509.ECDSA: + if _, ok := types.PublicKeyAlgorithms["ECDSA"].KeySize[csr.KeySize]; !ok { + return nil, fmt.Errorf("ecdsa invalid key size %d", csr.KeySize) + } + if _, ok := types.PublicKeyAlgorithms["ECDSA"].SigningAlgorithm[csr.SigningAlgorithm]; !ok { + return nil, fmt.Errorf("ecdsa invalid signing algorithm %s", csr.SigningAlgorithm) + } + generator = &crypto.SigningRequestGeneratorECDSA{Curve: csr.KeySize} + default: + return nil, fmt.Errorf("unsupported public key algorithm") + } + + pk, err := generator.Generate() + if err != nil { + return nil, fmt.Errorf("error generating private key [%s]: %w", generator.KeyType(), err) + } + + subject := pkix.Name{ + CommonName: csr.CommonName, + Country: csr.DistinguishedName.Country, + Province: csr.DistinguishedName.Province, + Locality: csr.DistinguishedName.Locality, + Organization: csr.DistinguishedName.Organization, + OrganizationalUnit: csr.DistinguishedName.OrganizationalUnit, + } + + template := x509.CertificateRequest{ + Subject: subject, + SignatureAlgorithm: csr.SigningAlgorithm, + DNSNames: csr.SubjectAlternateNames, + } + + csrBytes, err := x509.CreateCertificateRequest(rand.Reader, &template, pk) + if err != nil { + return nil, fmt.Errorf("error creating certificate request: %w", err) + } + + certificatePem := new(bytes.Buffer) + err = pem.Encode(certificatePem, &pem.Block{ + Type: "CERTIFICATE REQUEST", + Bytes: csrBytes, + }) + + if err != nil { + return nil, fmt.Errorf("error encoding certificate request (csr): %w", err) + } + + if len(csr.Output.CertificateSigningRequest) != 0 { + if err := os.WriteFile(csr.Output.CertificateSigningRequest, certificatePem.Bytes(), os.ModePerm); err != nil { + return nil, fmt.Errorf("error writing certificate signing request (csr) to [%s]", csr.Output.CertificateSigningRequest) + } + } + + pkBytes, err := generator.MarshalPrivateKey(pk) + if err != nil { + return nil, fmt.Errorf("error marshaling private key: %w", err) + } + + pkBlock := &pem.Block{ + Type: generator.KeyType(), + Bytes: pkBytes, + } + + if len(csr.Output.PrivateKey) != 0 { + if err := os.WriteFile(csr.Output.PrivateKey, pem.EncodeToMemory(pkBlock), os.ModePerm); err != nil { + return nil, fmt.Errorf("error writing private key to [%s]", csr.Output.PrivateKey) + } + } + + return &types.SigningRequest{ + CSR: certificatePem, + PrivateKey: pkBlock, + }, nil +} diff --git a/pkg/client/provisioner.go b/pkg/client/provisioner.go new file mode 100644 index 0000000..dc12940 --- /dev/null +++ b/pkg/client/provisioner.go @@ -0,0 +1,41 @@ +package baseca + +import ( + "context" + + apiv1 "github.com/coinbase/baseca/gen/go/baseca/v1" + "github.com/coinbase/baseca/pkg/types" + "github.com/coinbase/baseca/pkg/util" +) + +func (c *Client) ProvisionIssueCertificate(certificateRequest CertificateRequest, ca *apiv1.CertificateAuthorityParameter, service, environment, extendedKey string) (*apiv1.SignedCertificate, error) { + signingRequest, err := GenerateCSR(certificateRequest) + if err != nil { + return nil, err + } + + req := apiv1.OperationsSignRequest{ + CertificateSigningRequest: signingRequest.CSR.String(), + CertificateAuthority: ca, + ServiceAccount: service, + Environment: environment, + ExtendedKey: extendedKey, + } + + signedCertificate, err := c.Certificate.OperationsSignCSR(context.Background(), &req) + if err != nil { + return nil, err + } + + err = util.ParseCertificateFormat(signedCertificate, types.SignedCertificate{ + CertificatePath: certificateRequest.Output.Certificate, + IntermediateCertificateChainPath: certificateRequest.Output.IntermediateCertificateChain, + RootCertificateChainPath: certificateRequest.Output.RootCertificateChain, + }) + + if err != nil { + return nil, err + } + + return signedCertificate, nil +} diff --git a/pkg/client/sign.go b/pkg/client/sign.go new file mode 100644 index 0000000..b35c6a0 --- /dev/null +++ b/pkg/client/sign.go @@ -0,0 +1,132 @@ +package baseca + +import ( + "context" + "crypto" + "crypto/rand" + "crypto/rsa" + "crypto/sha256" + "crypto/x509" + "encoding/pem" + "errors" + "fmt" + "os" + "path/filepath" + + apiv1 "github.com/coinbase/baseca/gen/go/baseca/v1" + "github.com/coinbase/baseca/pkg/types" + "github.com/coinbase/baseca/pkg/util" +) + +func (c *Client) GenerateSignature(csr CertificateRequest, element []byte) (*[]byte, []*x509.Certificate, error) { + var certificatePem []*pem.Block + var certificateChain []*x509.Certificate + + signingRequest, err := GenerateCSR(csr) + if err != nil { + return nil, nil, err + } + + req := apiv1.CertificateSigningRequest{ + CertificateSigningRequest: signingRequest.CSR.String(), + } + + signedCertificate, err := c.Certificate.SignCSR(context.Background(), &req) + if err != nil { + return nil, nil, err + } + + err = util.ParseCertificateFormat(signedCertificate, types.SignedCertificate{ + CertificatePath: csr.Output.Certificate, + IntermediateCertificateChainPath: csr.Output.IntermediateCertificateChain, + RootCertificateChainPath: csr.Output.RootCertificateChain, + }) + + if err != nil { + return nil, nil, err + } + + hashedOutput := sha256.Sum256(element) + pk, err := x509.ParsePKCS1PrivateKey(signingRequest.PrivateKey.Bytes) + if err != nil { + return nil, nil, errors.New("error parsing pkcs1 private key") + } + + signature, err := rsa.SignPKCS1v15(rand.Reader, pk, crypto.SHA256, hashedOutput[:]) + if err != nil { + return nil, nil, fmt.Errorf("error calculating signature of hash using pkcs1: %s", err) + } + + fullChain, err := os.ReadFile(filepath.Clean(csr.Output.RootCertificateChain)) + if err != nil { + return nil, nil, fmt.Errorf("error retrieving full chain certificate: %s", err) + } + + // Build *pem.Block for Each Certificate in Chain + for { + block, remainder := pem.Decode(fullChain) + if block == nil { + break + } + certificatePem = append(certificatePem, block) + fullChain = remainder + } + + // Build *x509.Certificate for Each Certificate in Chain + for _, block := range certificatePem { + certificate, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return nil, nil, fmt.Errorf("error parsing code signing certificate: %s", err) + } + certificateChain = append(certificateChain, certificate) + } + + return &signature, certificateChain, nil +} + +func (c *Client) ValidateSignature(tc types.TrustChain, manifest types.Manifest) error { + err := manifest.CertificateChain[0].CheckSignature(manifest.SigningAlgorithm, manifest.Data, manifest.Signature) + if err != nil { + return fmt.Errorf("signature verification failed: %s", err) + } + + // Validate Entire Certificate Chain Does Not Break + for i := range manifest.CertificateChain[:len(manifest.CertificateChain)-1] { + err = manifest.CertificateChain[i].CheckSignatureFrom(manifest.CertificateChain[i+1]) + if err != nil { + return fmt.Errorf("certificate chain invalid: %s", err) + } + } + + if manifest.CertificateChain[0].Subject.CommonName != tc.CommonName { + return fmt.Errorf("invalid common name (cn) from code signing certificate") + } + + validSubjectAlternativeName := false + if len(manifest.CertificateChain[0].DNSNames) > 0 { + for _, san := range manifest.CertificateChain[0].DNSNames { + if san == tc.CommonName { + validSubjectAlternativeName = true + } + } + } + + if !validSubjectAlternativeName { + return fmt.Errorf("invalid subject alternative name (san) from code signing certificate") + } + + rootCertificatePool, err := util.GenerateCertificatePool(tc) + if err != nil { + return err + } + + opts := x509.VerifyOptions{ + Roots: rootCertificatePool, + KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageCodeSigning}, + } + _, err = manifest.CertificateChain[1].Verify(opts) + if err != nil { + return fmt.Errorf("error validating code signing certificate validity: %s", err) + } + return nil +} diff --git a/pkg/client/types.go b/pkg/client/types.go new file mode 100644 index 0000000..fee1b8e --- /dev/null +++ b/pkg/client/types.go @@ -0,0 +1,68 @@ +package baseca + +import "crypto/x509" + +var Attestation Provider = Provider{ + Local: "NONE", + AWS: "AWS", +} + +var Env = Environment{ + Local: "Local", + Sandbox: "Sandbox", + Development: "Development", + Staging: "Staging", + PreProduction: "PreProduction", + Production: "Production", +} + +type Environment struct { + Local string + Sandbox string + Development string + Staging string + PreProduction string + Production string +} + +type Configuration struct { + URL string + Environment string +} + +type Provider struct { + Local string + AWS string +} + +type Authentication struct { + ClientId string + ClientToken string + AuthToken string +} + +type CertificateRequest struct { + CommonName string + SubjectAlternateNames []string + DistinguishedName DistinguishedName + SigningAlgorithm x509.SignatureAlgorithm + PublicKeyAlgorithm x509.PublicKeyAlgorithm + KeySize int + Output Output +} + +type DistinguishedName struct { + Country []string + Province []string + Locality []string + Organization []string + OrganizationalUnit []string +} + +type Output struct { + CertificateSigningRequest string + Certificate string + IntermediateCertificateChain string + RootCertificateChain string + PrivateKey string +} diff --git a/pkg/crypto/generate.go b/pkg/crypto/generate.go new file mode 100644 index 0000000..2134e7d --- /dev/null +++ b/pkg/crypto/generate.go @@ -0,0 +1,95 @@ +package crypto + +import ( + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "fmt" + + "github.com/coinbase/baseca/pkg/types" +) + +type CSRGenerator interface { + Generate() (crypto.PrivateKey, error) + KeyType() string + MarshalPrivateKey(key crypto.PrivateKey) ([]byte, error) + SupportsPublicKeyAlgorithm(algorithm x509.PublicKeyAlgorithm) bool + SupportsSigningAlgorithm(algorithm x509.SignatureAlgorithm) bool + SupportsKeySize(size int) bool +} + +type SigningRequestGeneratorRSA struct { + Size int +} + +type SigningRequestGeneratorECDSA struct { + Curve int +} + +// RSA Interface +func (r *SigningRequestGeneratorRSA) Generate() (crypto.PrivateKey, error) { + return rsa.GenerateKey(rand.Reader, r.Size) +} + +func (r *SigningRequestGeneratorRSA) KeyType() string { + return "RSA PRIVATE KEY" +} + +func (r *SigningRequestGeneratorRSA) MarshalPrivateKey(key crypto.PrivateKey) ([]byte, error) { + return x509.MarshalPKCS1PrivateKey(key.(*rsa.PrivateKey)), nil +} + +func (r *SigningRequestGeneratorRSA) SupportsPublicKeyAlgorithm(algorithm x509.PublicKeyAlgorithm) bool { + return algorithm == x509.RSA +} + +func (r *SigningRequestGeneratorRSA) SupportsSigningAlgorithm(algorithm x509.SignatureAlgorithm) bool { + _, ok := types.PublicKeyAlgorithms["RSA"].SigningAlgorithm[algorithm] + return ok +} + +func (r *SigningRequestGeneratorRSA) SupportsKeySize(size int) bool { + _, ok := types.PublicKeyAlgorithms["RSA"].KeySize[size] + return ok +} + +// ECDSA Interface +func (e *SigningRequestGeneratorECDSA) Generate() (crypto.PrivateKey, error) { + c, ok := types.PublicKeyAlgorithms["ECDSA"].KeySize[e.Curve] + + if !ok { + return nil, fmt.Errorf("ecdsa curve [%d] not supported", e.Curve) + } + + curve, ok := c.(elliptic.Curve) + if !ok { + return nil, fmt.Errorf("invalid elliptic.Curve type") + } + + return ecdsa.GenerateKey(curve, rand.Reader) +} + +func (e *SigningRequestGeneratorECDSA) KeyType() string { + return "EC PRIVATE KEY" +} + +func (e *SigningRequestGeneratorECDSA) MarshalPrivateKey(key crypto.PrivateKey) ([]byte, error) { + return x509.MarshalECPrivateKey(key.(*ecdsa.PrivateKey)) +} + +func (e *SigningRequestGeneratorECDSA) SupportsPublicKeyAlgorithm(algorithm x509.PublicKeyAlgorithm) bool { + return algorithm == x509.ECDSA +} + +func (e *SigningRequestGeneratorECDSA) SupportsSigningAlgorithm(algorithm x509.SignatureAlgorithm) bool { + _, ok := types.PublicKeyAlgorithms["ECDSA"].SigningAlgorithm[algorithm] + return ok +} + +func (e *SigningRequestGeneratorECDSA) SupportsKeySize(size int) bool { + _, ok := types.PublicKeyAlgorithms["ECDSA"].KeySize[size] + return ok +} diff --git a/pkg/crypto/generate_test.go b/pkg/crypto/generate_test.go new file mode 100644 index 0000000..d89bfd0 --- /dev/null +++ b/pkg/crypto/generate_test.go @@ -0,0 +1,71 @@ +package crypto + +import ( + "crypto/x509" + "testing" +) + +func TestSigningRequestGeneratorRSA(t *testing.T) { + r := &SigningRequestGeneratorRSA{ + Size: 2048, + } + + key, err := r.Generate() + if err != nil { + t.Fatalf("error generating rsa private key: %v", err) + } + + if keyType := r.KeyType(); keyType != "RSA PRIVATE KEY" { + t.Errorf("RSA PRIVATE KEY does not exist within private key") + + } + + if !r.SupportsPublicKeyAlgorithm(x509.RSA) { + t.Errorf("rsa public key algorithm not supported") + } + + if !r.SupportsSigningAlgorithm(x509.SHA256WithRSA) { + t.Errorf("SHA256WithRSA signing algorithm not supported") + } + + if !r.SupportsKeySize(2048) { + t.Errorf("rsa key size not supported") + } + + _, err = r.MarshalPrivateKey(key) + if err != nil { + t.Errorf("error marshaling rsa private key: %v", err) + } +} + +func TestSigningRequestGeneratorECDSA(t *testing.T) { + e := &SigningRequestGeneratorECDSA{ + Curve: 256, + } + + key, err := e.Generate() + if err != nil { + t.Fatalf("error generating ecdsa private key: %v", err) + } + + if keyType := e.KeyType(); keyType != "EC PRIVATE KEY" { + t.Errorf("EC PRIVATE KEY does not exist within private key") + } + + if !e.SupportsPublicKeyAlgorithm(x509.ECDSA) { + t.Errorf("ecdsa public key algorithm not supported") + } + + if !e.SupportsSigningAlgorithm(x509.ECDSAWithSHA256) { + t.Errorf("ECDSAWithSHA256 signing algorithm not supported") + } + + if !e.SupportsKeySize(256) { + t.Errorf("ecdsa curve size not supported") + } + + _, err = e.MarshalPrivateKey(key) + if err != nil { + t.Errorf("error marshaling ecdsa private key: %v", err) + } +} diff --git a/pkg/crypto/pk.go b/pkg/crypto/pk.go new file mode 100644 index 0000000..3c01a74 --- /dev/null +++ b/pkg/crypto/pk.go @@ -0,0 +1,74 @@ +package crypto + +import ( + "crypto" + "crypto/ecdsa" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "fmt" +) + +type CertificateAuthority struct { + Certificate *x509.Certificate + AsymmetricKey *AsymmetricKey + SerialNumber string + CertificateAuthorityArn string +} + +type AsymmetricKey interface { + KeyPair() interface{} + Sign(data []byte) ([]byte, error) +} + +type RSA struct { + PublicKey *rsa.PublicKey + PrivateKey *rsa.PrivateKey +} + +type ECDSA struct { + PublicKey *ecdsa.PublicKey + PrivateKey *ecdsa.PrivateKey +} + +func (key *RSA) KeyPair() interface{} { + return key +} + +func (key *RSA) Sign(data []byte) ([]byte, error) { + h := crypto.SHA256.New() + h.Write(data) + hashed := h.Sum(nil) + return rsa.SignPKCS1v15(rand.Reader, key.PrivateKey, crypto.SHA256, hashed) +} + +func (key *ECDSA) KeyPair() interface{} { + return key +} + +func (key *ECDSA) Sign(data []byte) ([]byte, error) { + h := crypto.SHA256.New() + h.Write(data) + hashed := h.Sum(nil) + r, s, err := ecdsa.Sign(rand.Reader, key.PrivateKey, hashed) + if err != nil { + return nil, err + } + signature := append(r.Bytes(), s.Bytes()...) + return signature, nil +} + +func ReturnPrivateKey(key AsymmetricKey) (interface{}, error) { + if key == nil { + return nil, fmt.Errorf("asymmetric key is nil") + } + + switch k := key.KeyPair().(type) { + case *RSA: + return k.PrivateKey, nil + case *ECDSA: + return k.PrivateKey, nil + default: + return nil, fmt.Errorf("unsupported key type") + } +} diff --git a/pkg/crypto/pk_test.go b/pkg/crypto/pk_test.go new file mode 100644 index 0000000..2badf9e --- /dev/null +++ b/pkg/crypto/pk_test.go @@ -0,0 +1,88 @@ +package crypto + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" + "reflect" + "testing" +) + +func TestRSASign(t *testing.T) { + privateKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + t.Fatalf("failed to generate private key: %v", err) + } + rsaKey := &RSA{ + PublicKey: &privateKey.PublicKey, + PrivateKey: privateKey, + } + data := []byte("_example") + signature, err := rsaKey.Sign(data) + if err != nil { + t.Fatalf("failed to sign data: %v", err) + } + if len(signature) == 0 { + t.Fatalf("expected non-empty signature") + } +} + +func TestECDSASign(t *testing.T) { + privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatalf("failed to generate private key: %v", err) + } + ecdsaKey := &ECDSA{ + PublicKey: &privateKey.PublicKey, + PrivateKey: privateKey, + } + data := []byte("_example") + signature, err := ecdsaKey.Sign(data) + if err != nil { + t.Fatalf("failed to sign data: %v", err) + } + if len(signature) == 0 { + t.Fatalf("expected non-empty signature") + } +} + +func TestReturnPrivateKey(t *testing.T) { + rsaPrivateKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + t.Fatalf("failed to generate rsa private key: %v", err) + } + + ecdsaPrivateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatalf("failed to generate ecdsa private key: %v", err) + } + + tests := []struct { + key AsymmetricKey + expected interface{} + }{ + {&RSA{PrivateKey: rsaPrivateKey}, rsaPrivateKey}, + {&ECDSA{PrivateKey: ecdsaPrivateKey}, ecdsaPrivateKey}, + {nil, nil}, + } + + for _, test := range tests { + got, err := ReturnPrivateKey(test.key) + if err != nil && test.key != nil { + t.Fatalf("unexpected error: %v", err) + } + if !reflect.DeepEqual(got, test.expected) { + t.Errorf("expected %v, but got %v", test.expected, got) + } + } +} + +func TestCertificateAuthorityInitialization(t *testing.T) { + ca := &CertificateAuthority{ + SerialNumber: "0000000000", + } + if ca.SerialNumber != "0000000000" { + t.Errorf("expected serial number to be '0000000000', but got '%s'", ca.SerialNumber) + } +} diff --git a/pkg/types/certificate.go b/pkg/types/certificate.go new file mode 100644 index 0000000..ff5a5ee --- /dev/null +++ b/pkg/types/certificate.go @@ -0,0 +1,17 @@ +package types + +import ( + "bytes" + "encoding/pem" +) + +type SigningRequest struct { + CSR *bytes.Buffer + PrivateKey *pem.Block +} + +type SignedCertificate struct { + CertificatePath string + IntermediateCertificateChainPath string + RootCertificateChainPath string +} diff --git a/pkg/types/pk.go b/pkg/types/pk.go new file mode 100644 index 0000000..b0252d3 --- /dev/null +++ b/pkg/types/pk.go @@ -0,0 +1,58 @@ +package types + +import ( + "crypto/elliptic" + "crypto/x509" +) + +type PublicKeyAlgorithm struct { + Algorithm x509.PublicKeyAlgorithm + KeySize map[int]any + Signature map[string]bool + SigningAlgorithm map[x509.SignatureAlgorithm]bool +} + +var PublicKeyAlgorithms = map[string]PublicKeyAlgorithm{ + "RSA": { + Algorithm: x509.RSA, + KeySize: map[int]interface{}{ + 2048: true, + 4096: true, + }, + Signature: map[string]bool{ + "SHA256WITHRSA": true, + "SHA384WITHRSA": true, + "SHA512WITHRSA": true, + }, + SigningAlgorithm: map[x509.SignatureAlgorithm]bool{ + x509.SHA256WithRSA: true, + x509.SHA384WithRSA: true, + x509.SHA512WithRSA: true, + }, + }, + "ECDSA": { + Algorithm: x509.ECDSA, + KeySize: map[int]interface{}{ + 256: elliptic.P256(), + 384: elliptic.P384(), + 521: elliptic.P521(), + }, + Signature: map[string]bool{ + "SHA256WITHECDSA": true, + "SHA384WITHECDSA": true, + "SHA512WITHECDSA": true, + }, + SigningAlgorithm: map[x509.SignatureAlgorithm]bool{ + x509.ECDSAWithSHA256: true, + x509.ECDSAWithSHA384: true, + x509.ECDSAWithSHA512: true, + }, + }, + // TODO: Support Ed25519 + "Ed25519": { + Algorithm: x509.Ed25519, + KeySize: map[int]interface{}{ + 256: true, + }, + }, +} diff --git a/pkg/types/sign.go b/pkg/types/sign.go new file mode 100644 index 0000000..c08d88e --- /dev/null +++ b/pkg/types/sign.go @@ -0,0 +1,16 @@ +package types + +import "crypto/x509" + +type TrustChain struct { + CommonName string + CertificateAuthorityDirectory []string + CertificateAuthorityFiles []string +} + +type Manifest struct { + CertificateChain []*x509.Certificate + Signature []byte + Data []byte + SigningAlgorithm x509.SignatureAlgorithm +} diff --git a/pkg/util/x509.go b/pkg/util/x509.go new file mode 100644 index 0000000..210e531 --- /dev/null +++ b/pkg/util/x509.go @@ -0,0 +1,84 @@ +package util + +import ( + "crypto/x509" + "encoding/pem" + "errors" + "fmt" + "os" + "path/filepath" + + apiv1 "github.com/coinbase/baseca/gen/go/baseca/v1" + "github.com/coinbase/baseca/pkg/types" +) + +func ParseCertificateFormat(certificate *apiv1.SignedCertificate, parameter types.SignedCertificate) error { + // Leaf Certificate Path + if len(parameter.CertificatePath) != 0 { + certificate := []byte(certificate.Certificate) + if err := os.WriteFile(parameter.CertificatePath, certificate, os.ModePerm); err != nil { + return fmt.Errorf("error writing certificate to [%s]", parameter.CertificatePath) + } + } + + // Intermediate Certificate Chain Path + if len(parameter.IntermediateCertificateChainPath) != 0 { + certificate := []byte(certificate.IntermediateCertificateChain) + if err := os.WriteFile(parameter.IntermediateCertificateChainPath, certificate, os.ModePerm); err != nil { + return fmt.Errorf("error writing certificate to [%s]", parameter.IntermediateCertificateChainPath) + } + } + + // Root Certificate Chain Path + if len(parameter.RootCertificateChainPath) != 0 { + certificate := []byte(certificate.CertificateChain) + if err := os.WriteFile(parameter.RootCertificateChainPath, certificate, os.ModePerm); err != nil { + return fmt.Errorf("error writing certificate chain to [%s]", parameter.RootCertificateChainPath) + } + } + return nil +} + +func GenerateCertificatePool(tc types.TrustChain) (*x509.CertPool, error) { + certPool := x509.NewCertPool() + + for _, dir := range tc.CertificateAuthorityDirectory { + files, err := os.ReadDir(dir) + if err != nil { + return nil, errors.New("invalid certificate authority directory") + } + + for _, certFile := range files { // #nosec G304 User Only Has Predefined Environment Parameters + data, err := os.ReadFile(filepath.Join(dir, certFile.Name())) + if err != nil { + return nil, errors.New("invalid certificate file") + } + pemBlock, _ := pem.Decode(data) + if pemBlock == nil || pemBlock.Type != "CERTIFICATE" { + return nil, errors.New("invalid input file") + } + cert, err := x509.ParseCertificate(pemBlock.Bytes) + if err != nil { + return nil, errors.New("error parsing x.509 certificate") + } + certPool.AddCert(cert) + } + } + + for _, ca := range tc.CertificateAuthorityFiles { + data, err := os.ReadFile(filepath.Clean(ca)) + if err != nil { + return nil, errors.New("invalid certificate authority file") + } + pemBlock, _ := pem.Decode(data) + if pemBlock == nil || pemBlock.Type != "CERTIFICATE" { + return nil, errors.New("invalid input file") + } + cert, err := x509.ParseCertificate(pemBlock.Bytes) + if err != nil { + return nil, errors.New("error parsing x.509 certificate") + } + certPool.AddCert(cert) + } + return certPool, nil +} diff --git a/pkg/util/x509_test.go b/pkg/util/x509_test.go new file mode 100644 index 0000000..6f169e4 --- /dev/null +++ b/pkg/util/x509_test.go @@ -0,0 +1,45 @@ +package util + +import ( + "os" + "path/filepath" + "testing" + + apiv1 "github.com/coinbase/baseca/gen/go/baseca/v1" + "github.com/coinbase/baseca/pkg/types" +) + +func TestParseCertificateFormat(t *testing.T) { + tempDir, err := os.MkdirTemp("", "certificate") + if err != nil { + t.Fatalf("Failed to create temp directory: %v", err) + } + defer os.RemoveAll(tempDir) + + certificatePath := filepath.Join(tempDir, "certificate.pem") + intermediateCertificateChainPath := filepath.Join(tempDir, "intermediate.pem") + rootCertificateChainPath := filepath.Join(tempDir, "root.pem") + + certificate := &apiv1.SignedCertificate{ + Certificate: "-----BEGIN CERTIFICATE-----", + IntermediateCertificateChain: "-----BEGIN CERTIFICATE-----", + CertificateChain: "-----BEGIN CERTIFICATE-----", + } + parameters := types.SignedCertificate{ + CertificatePath: certificatePath, + IntermediateCertificateChainPath: intermediateCertificateChainPath, + RootCertificateChainPath: rootCertificateChainPath, + } + + err = ParseCertificateFormat(certificate, parameters) + if err != nil { + t.Fatalf("failed to parse certificate format: %v", err) + } + + for _, path := range []string{certificatePath, intermediateCertificateChainPath, rootCertificateChainPath} { + _, err := os.ReadFile(path) + if err != nil { + t.Fatalf("failed to read file %s: %v", path, err) + } + } +} diff --git a/protos/baseca/v1/api.proto b/protos/baseca/v1/api.proto new file mode 100644 index 0000000..b043213 --- /dev/null +++ b/protos/baseca/v1/api.proto @@ -0,0 +1,349 @@ +syntax = "proto3"; + +package baseca.v1; + +import "google/protobuf/timestamp.proto"; +import "google/protobuf/empty.proto"; + +option go_package = "github.com/coinbase/baseca/protos/baseca/v1;apiv1"; + +message CertificateParameter { + string serial_number = 1; + string common_name = 2; + repeated string subject_alternative_name = 3; + google.protobuf.Timestamp expiration_date = 4; + google.protobuf.Timestamp issued_date = 5; + bool revoked = 6; + string revoked_by = 7; + google.protobuf.Timestamp revoke_date = 8; + string certificate_authority_arn = 9; + string account = 10; + string environment = 11; + string extended_key = 12; +} + +message CertificatesParameter { + repeated CertificateParameter certificates = 1; +} + +message CertificateAuthorityParameter { + string region = 1; + string ca_arn = 2; + string sign_algorithm = 3; + bool assume_role = 4; + string role_arn = 5; + int32 validity = 6; +} + +message CertificateSigningRequest { + string certificate_signing_request = 1; +} + +message SignedCertificate { + string certificate = 1; + string certificate_chain = 2; + CertificateParameter metadata = 3; + string intermediate_certificate_chain = 4; +} + +message CertificateSerialNumber { + string serial_number = 1; +} + +message ListCertificatesRequest { + string common_name = 1; + int32 page_id = 3; + int32 page_size = 4; +} + +message RevokeCertificateRequest { + string serial_number = 1; + string revocation_reason = 2; +} + +message RevokeCertificateResponse { + string serial_number = 1; + google.protobuf.Timestamp revocation_date = 2; + string status = 3; +} + +message OperationsSignRequest { + string certificate_signing_request = 1; + optional CertificateAuthorityParameter certificate_authority = 2; + string service_account = 3; + string environment = 4; + string extended_key = 5; +} + +message Environment { + string environment = 1; +} + +service Certificate { + rpc SignCSR (CertificateSigningRequest) returns (SignedCertificate); + rpc GetCertificate (CertificateSerialNumber) returns (CertificateParameter); + rpc ListCertificates (ListCertificatesRequest) returns (CertificatesParameter); + rpc RevokeCertificate (RevokeCertificateRequest) returns (RevokeCertificateResponse); + rpc OperationsSignCSR (OperationsSignRequest) returns (SignedCertificate); + rpc QueryCertificateMetadata (QueryCertificateMetadataRequest) returns (CertificatesParameter); +} + +message QueryCertificateMetadataRequest { + string serial_number = 1; + string account = 2; + string environment = 3; + string extended_key = 4; + repeated string subject_alternative_name = 5; +} + +message User { + string uuid = 1; + string username = 2; + string full_name = 3; + string email = 4; + string permissions = 5; + google.protobuf.Timestamp credential_changed_at = 6; + google.protobuf.Timestamp created_at = 7; +} + +message Users { + repeated User users = 1; +} + +message LoginUserRequest { + string username = 1; + string password = 2; +} + +message LoginUserResponse { + string access_token = 1; + User user = 2; +} + +message UsernameRequest { + string username = 1; +} + +message QueryParameter { + int32 page_id = 2; + int32 page_size = 3; +} + +message CreateUserRequest { + string username = 1; + string password = 2; + string full_name = 3; + string email = 4; + string permissions = 5; +} + +message UpdateCredentialsRequest { + string username = 1; + string password = 2; + string updated_password = 3; +} + +message UpdatePermissionsRequest { + string username = 1; + string permissions = 2; +} + +service Account { + rpc LoginUser (LoginUserRequest) returns (LoginUserResponse); + rpc DeleteUser (UsernameRequest) returns (google.protobuf.Empty); + rpc GetUser (UsernameRequest) returns (User); + rpc ListUsers (QueryParameter) returns (Users); + rpc CreateUser (CreateUserRequest) returns (User); + rpc UpdateUserCredentials (UpdateCredentialsRequest) returns (User); + rpc UpdateUserPermissions (UpdatePermissionsRequest) returns (User); +} + +message CreateServiceAccountRequest { + string service_account = 1; + string environment = 2; + optional string regular_expression = 3; + repeated string subject_alternative_names = 4; + repeated string certificate_authorities = 5; + string extended_key = 6; + int32 certificate_validity = 7; + string subordinate_ca = 8; + optional NodeAttestation node_attestation = 9; + string team = 10; + string email = 11; +} + +message CreateServiceAccountResponse { + string client_id = 1; + string client_token = 2; + string service_account = 3; + string environment = 4; + string regular_expression = 5; + repeated string subject_alternative_names = 6; + repeated string certificate_authorities = 7; + string extended_key = 8; + NodeAttestation node_attestation = 9; + int32 certificate_validity = 10; + string subordinate_ca = 11; + string team = 12; + string email = 13; + google.protobuf.Timestamp created_at = 14; + string created_by = 15; +} + +message ServiceAccount { + string client_id = 1; + string service_account = 2; + string environment = 3; + string regular_expression = 4; + repeated string subject_alternative_names = 5; + repeated string certificate_authorities = 6; + string extended_key = 7; + NodeAttestation node_attestation = 8; + int32 certificate_validity = 9; + string subordinate_ca = 10; + bool provisioned = 11; + string team = 12; + string email = 13; + google.protobuf.Timestamp created_at = 14; + string created_by = 15; +} + +message ServiceAccounts { + repeated ServiceAccount service_accounts = 1; +} + +message CreateProvisionerAccountRequest { + string provisioner_account = 1; + repeated string environments = 2; + string regular_expression = 3; + repeated string subject_alternative_names = 4; + repeated string extended_keys = 5; + uint32 max_certificate_validity = 6; + optional NodeAttestation node_attestation = 7; + string team = 8; + string email = 9; +} + +message CreateProvisionerAccountResponse { + string client_id = 1; + string client_token = 2; + string provisioner_account = 3; + repeated string environments = 4; + string regular_expression = 5; + repeated string subject_alternative_names = 6; + repeated string extended_keys = 8; + NodeAttestation node_attestation = 9; + uint32 max_certificate_validity = 10; + string team = 11; + string email = 12; + google.protobuf.Timestamp created_at = 13; + string created_by = 14; +} + +message ProvisionerAccounts { + repeated ProvisionerAccount provisioner_accounts = 1; +} + +message ProvisionerAccount { + string client_id = 1; + string provisioner_account = 2; + repeated string environments = 3; + string regular_expression = 4; + repeated string subject_alternative_names = 5; + repeated string extended_keys = 7; + NodeAttestation node_attestation = 8; + uint32 max_certificate_validity = 9; + string team = 10; + string email = 11; + google.protobuf.Timestamp created_at = 12; + string created_by = 13; +} + +message NodeAttestation { + AWSInstanceIdentityDocument aws_iid = 1; +} + +message AWSInstanceIdentityDocument { + string role_arn = 1; + string assume_role = 2; + repeated string security_groups = 3; + string region = 4; + string instance_id = 5; + string image_id = 6; + map instance_tags = 7; +} + +message AccountId { + string uuid = 1; +} + +message GetServiceAccountMetadataRequest{ + string service_account = 1; + string environment = 2; + string extended_key = 3; +} + +message ProvisionServiceAccountRequest { + string service_account = 1; + string environment = 2; + string regular_expression = 3; + repeated string subject_alternative_names = 4; + repeated string certificate_authorities = 5; + string extended_key = 6; + int32 certificate_validity = 7; + string subordinate_ca = 8; + NodeAttestation node_attestation = 9; + string team = 10; + string email = 11; + optional string region = 12; +} + +message ProvisionServiceAccountResponse { + string client_id = 1; + string client_token = 2; + string service_account = 3; + string environment = 4; + string regular_expression = 5; + repeated string subject_alternative_names = 6; + repeated string certificate_authorities = 7; + string extended_key = 8; + NodeAttestation node_attestation = 9; + int32 certificate_validity = 10; + string subordinate_ca = 11; + string team = 12; + string email = 13; + google.protobuf.Timestamp created_at = 14; + string created_by = 15; +} + + +service Service { + rpc CreateServiceAccount (CreateServiceAccountRequest) returns (CreateServiceAccountResponse); + rpc CreateProvisionerAccount(CreateProvisionerAccountRequest) returns (CreateProvisionerAccountResponse); + rpc GetProvisionerAccount (AccountId) returns (ProvisionerAccount); + rpc GetServiceAccount (AccountId) returns (ServiceAccount); + rpc GetServiceAccountMetadata (GetServiceAccountMetadataRequest) returns (ServiceAccounts); + rpc DeleteServiceAccount (AccountId) returns (google.protobuf.Empty); + rpc DeleteProvisionedServiceAccount (AccountId) returns (google.protobuf.Empty); + rpc DeleteProvisionerAccount (AccountId) returns (google.protobuf.Empty); + rpc ProvisionServiceAccount (ProvisionServiceAccountRequest) returns (ProvisionServiceAccountResponse); + rpc ListServiceAccounts (QueryParameter) returns (ServiceAccounts); + rpc ListProvisionerAccounts (QueryParameter) returns (ProvisionerAccounts); +} + +message HealthCheckRequest { + string service = 1; +} + +message HealthCheckResponse { + enum ServingStatus { + UNKNOWN = 0; + SERVING = 1; + NOT_SERVING = 2; + } + ServingStatus status = 1; +} + +service Health{ + rpc Check(HealthCheckRequest) returns (HealthCheckResponse); +} \ No newline at end of file diff --git a/protos/buf.gen.yaml b/protos/buf.gen.yaml new file mode 100644 index 0000000..44df641 --- /dev/null +++ b/protos/buf.gen.yaml @@ -0,0 +1,10 @@ +version: v1 +plugins: + - name: go + out: ./gen/go + opt: + - paths=source_relative + - name: go-grpc + out: ./gen/go + opt: + - paths=source_relative \ No newline at end of file diff --git a/terraform/README.md b/terraform/README.md new file mode 100644 index 0000000..e4d7b63 --- /dev/null +++ b/terraform/README.md @@ -0,0 +1,117 @@ +# `baseca` Infrastructure + +## Local Deployment Resources + +| Variable | Description | Type | Optional | Default | Example | +| ------------- | -------------------------------------- | ------ | -------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------ | +| `service` | Terraform Resource Prefix | string | true | baseca | coinbase | +| `environment` | Service Environment | string | false | | development | +| `region` | AWS Region to Deploy | string | false | | us-east-1 | +| `key_spec` | KMS Customer Managed Key Spec | string | true | RSA_4096 | [`customer_master_key_spec`](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/kms_key#customer_master_key_spec) | +| `bucket` | S3 Bucket Name (Kinesis Data Firehose) | string | false | | baseca-firehose-development | + +Example Local Deployment for `development/baseca` Module + +```sh +# baseca/terraform/development/baseca.tf + +module "baseca" { + source = "./baseca" + service = "baseca" + environment = "development" + region = "us-east-1" + key_spec = "RSA_4096" + bucket = "baseca-firehose-example" +} +``` + +## Production Deployment Resources + +| Variable | Description | Type | Optional | Default | Example | +| ----------------- | ---------------------------------------------------------------------------------------------------------------- | ------------ | -------------------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `service` | Terraform Resource Prefix | string | true | baseca | coinbase | +| `environment` | Service Environment | string | false | | production | +| `region` | AWS Region to Deploy | string | false | | us-east-1 | +| `key_spec` | KMS Customer Managed Key Spec | string | true | RSA_4096 | [`customer_master_key_spec`](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/kms_key#customer_master_key_spec) | +| `bucket` | S3 Bucket Name | string | false | | baseca-firehose-production | +| `acm_pca_arns` | List of Private CA ARNs in Account | list(string) | false | | ["arn:aws:acm-pca:us-east-1:123456789012:certificate-authority/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"]

_**NOTE:** These are upstream Intermediate CA(s) from AWS Private CA `baseca` will be issuing Subordinate CAs from._ | +| `db_ingress_cidr` | IP CIDR Blocks for Ingress to RDS and Redis | list(string) | Requires `db_ingress_sg` if Not Used | | ["10.0.0.1/24", "10.0.0.2/24"] | +| `db_ingress_sg` | Security Groups for Ingress to RDS and Redis | list(string) | Requires `db_ingress_cidr` if Not Used | | ["sg-0123abcd1234abcd1", "sg-5678efgh5678efgh2"] | +| `db_subnet_ids` | Private Subnets to Deploy RDS Cluster

_**NOTE:** Minimum of two subnets in different availability zones._ | list(string) | false | | ["subnet-01234567", "subnet-09876543"] | +| `vpc_id` | VPC ID for Security Groups

_**NOTE:** `db_subnet_ids` must be within this VPC._ | string | false | | vpc-12345678 | + +Example Production Deployment for `production/baseca` Resource Module + +```sh +# baseca/terraform/production/baseca.tf + +module "baseca" { + source = "./baseca" + service = "baseca" + environment = "production" + region = "us-east-1" + key_spec = "RSA_4096" + bucket = "baseca-firehose-example" + + vpc_id = "vpc-xxxxxxxx" + db_ingress_cidr = ["10.0.0.0/8"] + db_subnet_ids = [ + "subnet-09876543", + "subnet-12345678" + ] + + acm_pca_arns = [ + "arn:aws:acm-pca:us-east-1:123456789012:certificate-authority/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", + "arn:aws:acm-pca:us-east-1:987654321098:certificate-authority/yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy" + ] +} +``` + +## Production Compute Deployment + +**DISCLAIMER:** The compute deployment is a sample of how `baseca` can run within an environment; this is not intended to be run as is and will require your organization to design the deployment pipeline for your use case. + +| Variable | Description | Type | Optional | Default | Example | +| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------ | -------- | ------- | ------------------------------------------------------------------------------------------------ | +| `service` | Terraform Resource Prefix | string | true | baseca | coinbase | +| `environment` | Service Environment | string | false | | production | +| `configuration` | Determines which Configuration File `baseca` Reads

_**NOTE:** For example, a value of `production` will read the `config.primary.production.aws.yml` file._ | string | false | | production | +| `region` | AWS Region to Deploy | string | false | | us-east-1 | +| `vpc_id` | VPC ID to Deploy ECS Service

_**NOTE:** `subnet_ids` and `db_subnet_ids` must be within this VPC._ | string | false | | vpc-12345678 | +| `subnet_ids` | VPC Subnets to Deploy ECS Service

_**NOTE:** Multiple subnets must be in different availability zones. Subnets also must be routable to Private Subnets from `db_subnet_ids`._ | list(string) | false | | ["subnet-0123456789abcdef", "subnet-9876543210fedcba"] | +| `public_ip` | Assign Public IP for ECS Service

_**NOTE:** If `public_ip` is set to `true` the `subnet_ids` must be Public Subnets._ | bool | true | false | false | +| `network_ingress` | IP CIDR Blocks to Ingress to `baseca` ECS Service Service | list(string) | false | | ["10.0.0.1/24", "10.0.0.0.24"] | +| `host_port` | Port for `baseca` ECS Service Service | int | true | 9090 | 9090 | +| `min_instance_count` | Minimum Instances for ECS Services | int | true | 1 | 2 | +| `max_instance_count` | Maximum Instances for ECS Services | int | true | 2 | 4 | +| `cpu` | Maximum CPU for ECS Task | int | true | 2048 | [cpu](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-cpu-memory-error.html) | +| `memory` | Maximum Memory for ECS Task | int | true | 4096 | [memory](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-cpu-memory-error.html) | +| `baseca_iam_role` | Task Role for `baseca` ECS Service | string | false | | `module.baseca.baseca_iam_role` | +| `ecr_repository` | `baseca` ECR Repository URL | string | false | | `module.baseca.ecr_repository` | + +Example Production Deployment for `production/baseca` Compute Module + +```sh +# baseca/terraform/production/baseca.tf + +module "compute" { + source = "./compute" + service = "baseca" + region = "us-east-1" + environment = "production" + configuration = "production" + + vpc_id = "vpc-xxxxxxxx" + subnet_ids = ["subnet-0123456789abcdef", "subnet-9876543210fedcba"] + + host_port = 9090 + network_ingress = ["10.0.0.0/8"] + public_ip = false + + baseca_iam_role = module.baseca.baseca_iam_role + ecr_repository = module.baseca.ecr_repository + depends_on = [ + module.baseca + ] +} +``` diff --git a/terraform/development/baseca/firehose.tf b/terraform/development/baseca/firehose.tf new file mode 100644 index 0000000..f171476 --- /dev/null +++ b/terraform/development/baseca/firehose.tf @@ -0,0 +1,64 @@ +resource "aws_kinesis_firehose_delivery_stream" "certificate_stream" { + name = "${var.service}-${var.environment}" + destination = "extended_s3" + + extended_s3_configuration { + role_arn = aws_iam_role.certificate_stream.arn + bucket_arn = aws_s3_bucket.certificate_metadata.arn + } +} + +resource "aws_s3_bucket" "certificate_metadata" { + bucket = var.bucket +} + +resource "aws_s3_bucket_policy" "certificate_stream" { + bucket = aws_s3_bucket.certificate_metadata.id + + policy = jsonencode({ + Version = "2012-10-17", + Statement = [ + { + Effect = "Allow", + Principal = { + AWS = aws_iam_role.certificate_stream.arn + }, + Action = [ + "s3:AbortMultipartUpload", + "s3:GetBucketLocation", + "s3:GetObject", + "s3:ListBucket", + "s3:ListBucketMultipartUploads", + "s3:PutObject" + ], + Resource = [ + "${aws_s3_bucket.certificate_metadata.arn}", + "${aws_s3_bucket.certificate_metadata.arn}/*" + ], + Condition = { + "Bool": { + "aws:SecureTransport": "true" + } + } + } + ] + }) +} + +data "aws_iam_policy_document" "certificate_stream" { + statement { + effect = "Allow" + + principals { + type = "Service" + identifiers = ["firehose.amazonaws.com"] + } + + actions = ["sts:AssumeRole"] + } +} + +resource "aws_iam_role" "certificate_stream" { + name = "${var.service}-${var.environment}-stream" + assume_role_policy = data.aws_iam_policy_document.certificate_stream.json +} diff --git a/terraform/development/baseca/kms.tf b/terraform/development/baseca/kms.tf new file mode 100644 index 0000000..a33ba1f --- /dev/null +++ b/terraform/development/baseca/kms.tf @@ -0,0 +1,31 @@ +resource "aws_kms_key" "signing" { + customer_master_key_spec = var.key_spec + description = "Authentication Signing" + key_usage = "SIGN_VERIFY" + deletion_window_in_days = 10 + + policy = jsonencode({ + Version = "2012-10-17", + Statement = [ + { + Effect = "Allow", + Principal = { + AWS = "arn:aws:iam::${data.aws_caller_identity.account.account_id}:root" + }, + Action = "kms:*" + Resource = "*" + } + ] + }) + + tags = { + Name = "${var.service}-${var.environment}" + } +} + +resource "aws_kms_alias" "baseca" { + name = "alias/${var.service}-${var.environment}" + target_key_id = aws_kms_key.signing.key_id +} + +data "aws_caller_identity" "account" {} diff --git a/terraform/development/baseca/output.tf b/terraform/development/baseca/output.tf new file mode 100644 index 0000000..828e760 --- /dev/null +++ b/terraform/development/baseca/output.tf @@ -0,0 +1,9 @@ +output "kinesis_firehose_stream" { + description = "baseca Kinesis Firehose Delivery Stream" + value = aws_kinesis_firehose_delivery_stream.certificate_stream.name +} + +output "kms_key_id" { + description = "baseca KMS Key for Signing User Auth Token" + value = aws_kms_key.signing.key_id +} \ No newline at end of file diff --git a/terraform/development/baseca/variables.tf b/terraform/development/baseca/variables.tf new file mode 100644 index 0000000..cca4bdd --- /dev/null +++ b/terraform/development/baseca/variables.tf @@ -0,0 +1,26 @@ +variable "service" { + description = "Resource Prefix" + type = string + default = "baseca" +} + +variable "environment" { + description = "Service Environment" + type = string +} + +variable "region" { + description = "AWS Region" + type = string +} + +variable "key_spec" { + description = "KMS Key Spec: [SYMMETRIC_DEFAULT, RSA_2048, RSA_3072, RSA_4096, HMAC_256, ECC_NIST_P256, ECC_NIST_P384, ECC_NIST_P521, ECC_SECG_P256K1]" + type = string + default = "RSA_4096" +} + +variable "bucket" { + description = "Bucket Name to Store Certificate Streaming Data from Firehose" + type = string +} diff --git a/terraform/development/output.tf b/terraform/development/output.tf new file mode 100644 index 0000000..632355c --- /dev/null +++ b/terraform/development/output.tf @@ -0,0 +1,9 @@ +output "kinesis_firehose_stream" { + description = "baseca Kinesis Firehose Delivery Stream" + value = module.baseca.kinesis_firehose_stream +} + +output "kms_key_id" { + description = "baseca KMS Key for Signing User Auth Token" + value = module.baseca.kms_key_id +} diff --git a/terraform/development/provider.tf b/terraform/development/provider.tf new file mode 100644 index 0000000..808dcf3 --- /dev/null +++ b/terraform/development/provider.tf @@ -0,0 +1,12 @@ +terraform { + required_providers { + random = { + source = "hashicorp/random" + version = "3.4.3" + } + aws = { + source = "hashicorp/aws" + version = "5.1.0" + } + } +} diff --git a/terraform/production/baseca/ecr.tf b/terraform/production/baseca/ecr.tf new file mode 100644 index 0000000..afb0b98 --- /dev/null +++ b/terraform/production/baseca/ecr.tf @@ -0,0 +1,8 @@ +resource "aws_ecr_repository" "baseca" { + name = "baseca" + image_tag_mutability = "MUTABLE" + + image_scanning_configuration { + scan_on_push = true + } +} \ No newline at end of file diff --git a/terraform/production/baseca/elasticache.tf b/terraform/production/baseca/elasticache.tf new file mode 100644 index 0000000..918cf66 --- /dev/null +++ b/terraform/production/baseca/elasticache.tf @@ -0,0 +1,47 @@ +resource "aws_elasticache_cluster" "baseca" { + cluster_id = "${var.service}-${var.environment}" + engine = "redis" + node_type = "cache.t3.small" + num_cache_nodes = 1 + parameter_group_name = aws_elasticache_parameter_group.baseca.name + engine_version = "6.2" + port = 6379 + security_group_ids = [aws_security_group.redis.id] + + snapshot_retention_limit = 7 + snapshot_window = "05:00-09:00" +} + +resource "aws_elasticache_parameter_group" "baseca" { + name = "${var.service}-${var.environment}-pg" + family = "redis6.x" +} + +resource "aws_security_group" "redis" { + name = "${var.service}-${var.environment}-redis" + description = "baseca Redis Security Group" + + dynamic "ingress" { + for_each = length(var.db_ingress_cidr) > 0 ? [1] : [] + content { + from_port = 6379 + to_port = 6379 + protocol = "tcp" + cidr_blocks = var.db_ingress_cidr + } + } + + dynamic "ingress" { + for_each = length(var.db_ingress_sg) > 0 ? [1] : [] + content { + from_port = 6379 + to_port = 6379 + protocol = "tcp" + security_groups = var.db_ingress_sg + } + } + + tags = { + Name = "${var.service}-${var.environment}-redis" + } +} \ No newline at end of file diff --git a/terraform/production/baseca/firehose.tf b/terraform/production/baseca/firehose.tf new file mode 100644 index 0000000..f171476 --- /dev/null +++ b/terraform/production/baseca/firehose.tf @@ -0,0 +1,64 @@ +resource "aws_kinesis_firehose_delivery_stream" "certificate_stream" { + name = "${var.service}-${var.environment}" + destination = "extended_s3" + + extended_s3_configuration { + role_arn = aws_iam_role.certificate_stream.arn + bucket_arn = aws_s3_bucket.certificate_metadata.arn + } +} + +resource "aws_s3_bucket" "certificate_metadata" { + bucket = var.bucket +} + +resource "aws_s3_bucket_policy" "certificate_stream" { + bucket = aws_s3_bucket.certificate_metadata.id + + policy = jsonencode({ + Version = "2012-10-17", + Statement = [ + { + Effect = "Allow", + Principal = { + AWS = aws_iam_role.certificate_stream.arn + }, + Action = [ + "s3:AbortMultipartUpload", + "s3:GetBucketLocation", + "s3:GetObject", + "s3:ListBucket", + "s3:ListBucketMultipartUploads", + "s3:PutObject" + ], + Resource = [ + "${aws_s3_bucket.certificate_metadata.arn}", + "${aws_s3_bucket.certificate_metadata.arn}/*" + ], + Condition = { + "Bool": { + "aws:SecureTransport": "true" + } + } + } + ] + }) +} + +data "aws_iam_policy_document" "certificate_stream" { + statement { + effect = "Allow" + + principals { + type = "Service" + identifiers = ["firehose.amazonaws.com"] + } + + actions = ["sts:AssumeRole"] + } +} + +resource "aws_iam_role" "certificate_stream" { + name = "${var.service}-${var.environment}-stream" + assume_role_policy = data.aws_iam_policy_document.certificate_stream.json +} diff --git a/terraform/production/baseca/iam.tf b/terraform/production/baseca/iam.tf new file mode 100644 index 0000000..e3f9cab --- /dev/null +++ b/terraform/production/baseca/iam.tf @@ -0,0 +1,85 @@ +resource "aws_iam_role" "compute" { + name = "${var.service}-${var.environment}" + + assume_role_policy = jsonencode({ + Version = "2012-10-17", + Statement = [ + { + Action = "sts:AssumeRole", + Effect = "Allow", + Principal = { + Service = ["ec2.amazonaws.com", "ecs-tasks.amazonaws.com"] + } + } + ] + }) +} + +resource "aws_iam_policy" "compute" { + name = "${var.service}-${var.environment}" + description = "baseca Control Plane Permissions" + + policy = jsonencode({ + Version = "2012-10-17", + Statement = [ + { + Effect = "Allow", + Action = [ + "acm-pca:WaitUntilCertificateIssued", + "acm-pca:GetCertificate", + "acm-pca:GetCertificateAuthorityCertificate", + "acm-pca:RevokeCertificate" + ] + Resource = var.acm_pca_arns + }, + { + Effect = "Allow", + Action = "acm-pca:IssueCertificate", + Resource = var.acm_pca_arns, + Condition = { + StringEquals = { + "acm-pca:TemplateArn": [ + "arn:aws:acm-pca:::template/SubordinateCACertificate_PathLen0/V1", + "arn:aws:acm-pca:::template/EndEntityClientAuthCertificate/V1", + "arn:aws:acm-pca:::template/EndEntityServerAuthCertificate/V1", + "arn:aws:acm-pca:::template/CodeSigningCertificate/V1" + ] + } + } + }, + { + Effect = "Allow", + Action = "sts:AssumeRole" + Resource = "arn:aws:iam::*:role/baseca-node-attestation" + }, + { + Effect = "Allow", + Action = [ + "firehose:PutRecord", + "firehose:PutRecordBatch" + ] + Resource = aws_kinesis_firehose_delivery_stream.certificate_stream.arn + }, + { + Effect = "Allow", + Action = [ + "secretsmanager:GetSecretValue" + ] + Resource = aws_rds_cluster.aurora_cluster.master_user_secret[0].secret_arn + }, + { + Effect = "Allow", + Action = [ + "ec2:DescribeInstances", + "iam:GetInstanceProfile" + ] + Resource = "*" + } + ] + }) +} + +resource "aws_iam_role_policy_attachment" "compute_policy_attachment" { + policy_arn = aws_iam_policy.compute.arn + role = aws_iam_role.compute.name +} diff --git a/terraform/production/baseca/kms.tf b/terraform/production/baseca/kms.tf new file mode 100644 index 0000000..1b2c40e --- /dev/null +++ b/terraform/production/baseca/kms.tf @@ -0,0 +1,44 @@ +resource "aws_kms_key" "signing" { + customer_master_key_spec = var.key_spec + description = "Authentication Signing" + key_usage = "SIGN_VERIFY" + deletion_window_in_days = 10 + + policy = jsonencode({ + Version = "2012-10-17", + Statement = [ + { + Effect = "Allow", + Principal = { + AWS = aws_iam_role.compute.arn + }, + Action = [ + "kms:DescribeKey", + "kms:GetPublicKey", + "kms:Verify", + "kms:Sign" + ], + Resource = "*" + }, + { + Effect = "Allow", + Principal = { + AWS = "arn:aws:iam::${data.aws_caller_identity.account.account_id}:root" + }, + Action = "kms:*" + Resource = "*" + } + ] + }) + + tags = { + Name = "${var.service}-${var.environment}" + } +} + +resource "aws_kms_alias" "baseca" { + name = "alias/${var.service}-${var.environment}" + target_key_id = aws_kms_key.signing.key_id +} + +data "aws_caller_identity" "account" {} diff --git a/terraform/production/baseca/output.tf b/terraform/production/baseca/output.tf new file mode 100644 index 0000000..42571dd --- /dev/null +++ b/terraform/production/baseca/output.tf @@ -0,0 +1,34 @@ +output "kinesis_firehose_stream" { + description = "baseca Kinesis Firehose Delivery Stream" + value = aws_kinesis_firehose_delivery_stream.certificate_stream.name +} + +output "kms_key_id" { + description = "baseca KMS Key for Signing User Auth Token" + value = aws_kms_key.signing.key_id +} + +output "rds_writer_endpoint" { + description = "baseca RDS Writer Endpoint DNS Address" + value = aws_rds_cluster.aurora_cluster.endpoint +} + +output "rds_reader_endpoint" { + description = "baseca RDS Reader Endpoint DNS Address" + value = aws_rds_cluster.aurora_cluster.reader_endpoint +} + +output "redis_endpoint" { + description = "baseca Elasticache Redis DNS Address" + value = aws_elasticache_cluster.baseca.cache_nodes.0.address +} + +output "baseca_iam_role" { + description = "baseca Control Plane IAM Role ARN" + value = aws_iam_role.compute.arn +} + +output "ecr_repository" { + description = "baseca Elastic Container Registry (ECR) ARN" + value = aws_ecr_repository.baseca.repository_url +} diff --git a/terraform/production/baseca/rds.tf b/terraform/production/baseca/rds.tf new file mode 100644 index 0000000..ea62eab --- /dev/null +++ b/terraform/production/baseca/rds.tf @@ -0,0 +1,65 @@ +resource "aws_rds_cluster" "aurora_cluster" { + cluster_identifier = "${var.service}-${var.environment}-cluster" + engine = "aurora-postgresql" + engine_version = "14" + backup_retention_period = 3 + skip_final_snapshot = true + apply_immediately = true + storage_encrypted = true + vpc_security_group_ids = [aws_security_group.baseca.id] + db_subnet_group_name = aws_db_subnet_group.baseca.name + + database_name = "baseca" + master_username = "baseca" + manage_master_user_password = true +} + +resource "aws_rds_cluster_instance" "cluster_instances" { + count = 2 + identifier = "aurora-cluster-instance-${var.service}-${count.index}" + cluster_identifier = aws_rds_cluster.aurora_cluster.id + instance_class = "db.t4g.medium" + engine = "aurora-postgresql" + engine_version = "14" + auto_minor_version_upgrade = true + publicly_accessible = false +} + +resource "aws_security_group" "baseca" { + name = "${var.service}-${var.environment}-db" + description = "baseca Database Security Group" + vpc_id = var.vpc_id + + dynamic "ingress" { + for_each = length(var.db_ingress_cidr) > 0 ? [1] : [] + content { + from_port = 5432 + to_port = 5432 + protocol = "tcp" + cidr_blocks = var.db_ingress_cidr + } + } + + dynamic "ingress" { + for_each = length(var.db_ingress_sg) > 0 ? [1] : [] + content { + from_port = 5432 + to_port = 5432 + protocol = "tcp" + security_groups = var.db_ingress_sg + } + } + + tags = { + Name = "${var.service}-${var.environment}-db" + } +} + +resource "aws_db_subnet_group" "baseca" { + name = "${var.service}-${var.environment}" + subnet_ids = var.db_subnet_ids + + tags = { + Name = "${var.service}-${var.environment}" + } +} \ No newline at end of file diff --git a/terraform/production/baseca/secretsmanager.tf b/terraform/production/baseca/secretsmanager.tf new file mode 100644 index 0000000..0b50939 --- /dev/null +++ b/terraform/production/baseca/secretsmanager.tf @@ -0,0 +1,4 @@ +resource "random_password" "temporary"{ + length = 16 + special = true +} \ No newline at end of file diff --git a/terraform/production/baseca/variables.tf b/terraform/production/baseca/variables.tf new file mode 100644 index 0000000..3ad5254 --- /dev/null +++ b/terraform/production/baseca/variables.tf @@ -0,0 +1,53 @@ +variable "service" { + description = "Resource Prefix" + type = string + default = "baseca" +} + +variable "environment" { + description = "Service Environment" + type = string +} + +variable "db_ingress_cidr" { + description = "List of IP CIDR Blocks for Ingress to Database" + type = list(string) +} + +variable "db_ingress_sg" { + description = "List of Security Groups for Ingress to Database" + type = list(string) + default = [] +} + +variable "db_subnet_ids" { + description = "List of Subnets for RDS Database" + type = list(string) + default = [] +} + +variable "region" { + description = "AWS Region" + type = string +} + +variable "key_spec" { + description = "KMS Key Spec: [SYMMETRIC_DEFAULT, RSA_2048, RSA_3072, RSA_4096, HMAC_256, ECC_NIST_P256, ECC_NIST_P384, ECC_NIST_P521, ECC_SECG_P256K1]" + type = string + default = "RSA_4096" +} + +variable "bucket" { + description = "Bucket Name to Store Certificate Streaming Data from Firehose" + type = string +} + +variable "acm_pca_arns" { + description = "List of Supported ACM Private CA ARNs" + type = list(string) +} + +variable "vpc_id" { + description = "AWS VPC ID" + type = string +} \ No newline at end of file diff --git a/terraform/production/compute/ecs.tf b/terraform/production/compute/ecs.tf new file mode 100644 index 0000000..6da02f8 --- /dev/null +++ b/terraform/production/compute/ecs.tf @@ -0,0 +1,142 @@ +resource "aws_ecs_cluster" "baseca" { + name = "${var.service}-${var.environment}" +} + +resource "aws_ecs_service" "baseca" { + name = "${var.service}-${var.environment}" + cluster = aws_ecs_cluster.baseca.id + task_definition = aws_ecs_task_definition.baseca.arn + desired_count = var.min_instance_count + launch_type = "FARGATE" + + network_configuration { + subnets = var.subnet_ids + security_groups = [aws_security_group.baseca.id] + assign_public_ip = var.public_ip ? true : false + } + + lifecycle { + ignore_changes = [ + desired_count + ] + } +} + +resource "aws_ecs_task_definition" "baseca" { + family = "${var.service}-${var.environment}" + network_mode = "awsvpc" + requires_compatibilities = ["FARGATE"] + cpu = var.cpu + memory = var.memory + execution_role_arn = aws_iam_role.ecs_task_execution_role.arn + task_role_arn = var.baseca_iam_role + + container_definitions = jsonencode([{ + name = "baseca" + image = "${var.ecr_repository}:latest" + essential = true + portMappings = [{ + containerPort = var.host_port + hostPort = var.host_port + protocol = "tcp" + }] + + environment = [ + { + "name": "ENVIRONMENT", + "value": var.environment + }, + { + "name": "CONFIGURATION", + "value": var.configuration + } + ] + + mountPoints = [{ + sourceVolume = "efs" + containerPath = "/tmp/baseca/ssl" + }] + + logConfiguration = { + logDriver = "awslogs" + options = { + awslogs-group = var.service + awslogs-region = var.region + awslogs-stream-prefix = var.environment + } + } + }]) + + volume { + name = "efs" + + efs_volume_configuration { + file_system_id = aws_efs_file_system.efs.id + root_directory = "/" + transit_encryption = "ENABLED" + transit_encryption_port = 2049 + authorization_config { + access_point_id = aws_efs_access_point.access_point.id + iam = "ENABLED" + } + } + } +} + +resource "aws_security_group" "baseca" { + name = "${var.service}-${var.environment}-compute" + description = "baseca Instance Control Plane" + vpc_id = var.vpc_id + + ingress { + from_port = var.host_port + to_port = var.host_port + protocol = "tcp" + cidr_blocks = var.network_ingress + } + + ingress { + from_port = 22 + to_port = 22 + protocol = "tcp" + cidr_blocks = var.network_ingress + } + + egress { + from_port = 443 + to_port = 443 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + } + + egress { + from_port = 2049 + to_port = 2049 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + } + + egress { + from_port = 5432 + to_port = 5432 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + } + + egress { + from_port = 6379 + to_port = 6379 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + } + + tags = { + Name = var.service + Environment = var.environment + } +} + +resource "aws_cloudwatch_log_group" "baseca" { + name = var.service + retention_in_days = 14 +} diff --git a/terraform/production/compute/efs.tf b/terraform/production/compute/efs.tf new file mode 100644 index 0000000..d67e578 --- /dev/null +++ b/terraform/production/compute/efs.tf @@ -0,0 +1,121 @@ +resource "aws_efs_file_system" "efs" { + creation_token = "${var.service}-${var.environment}" + + encrypted = true + kms_key_id = aws_kms_key.baseca_efs.arn + + tags = { + Name = var.service + Environment = var.environment + } +} + +resource "aws_efs_mount_target" "efs_mount_target" { + count = length(var.subnet_ids) + file_system_id = aws_efs_file_system.efs.id + subnet_id = var.subnet_ids[count.index] + security_groups = [aws_security_group.efs.id] +} + +resource "aws_efs_access_point" "access_point" { + file_system_id = aws_efs_file_system.efs.id + + posix_user { + gid = 1000 + uid = 1000 + } + + root_directory { + path = "/baseca" + creation_info { + owner_gid = 1000 + owner_uid = 1000 + permissions = "755" + } + } +} + +resource "aws_efs_file_system_policy" "efs_policy" { + file_system_id = aws_efs_file_system.efs.id + + policy = jsonencode({ + Version = "2012-10-17", + Statement = [ + { + Effect = "Allow", + Principal = { + AWS = var.baseca_iam_role + }, + Action = [ + "elasticfilesystem:ClientMount", + "elasticfilesystem:ClientWrite" + ], + Resource = [ + "arn:aws:elasticfilesystem:${var.region}:${data.aws_caller_identity.account.account_id}:file-system/${aws_efs_file_system.efs.id}" + ], + Condition = { + "Bool": { + "aws:SecureTransport": "true" + } + } + } + ] + }) +} + +resource "aws_iam_policy" "efs_write_access" { + name = "${var.service}-${var.environment}-efs" + description = "baseca Write Access to EFS" + + policy = jsonencode({ + Version = "2012-10-17", + Statement = [ + { + Effect = "Allow", + Action = [ + "elasticfilesystem:ClientWrite", + "elasticfilesystem:ClientMount" + ], + Resource = [ + "arn:aws:elasticfilesystem:${var.region}:${data.aws_caller_identity.account.account_id}:file-system/${aws_efs_file_system.efs.id}" + ], + Condition = { + "Bool": { + "aws:SecureTransport": "true" + } + } + } + ] + }) +} + +resource "aws_iam_role_policy_attachment" "baseca_efs" { + role = "${var.service}-${var.environment}" + policy_arn = aws_iam_policy.efs_write_access.arn +} + +resource "aws_security_group" "efs" { + name = "${var.service}-${var.environment}-efs" + description = "baseca EFS Volume Security Group" + vpc_id = var.vpc_id + + ingress { + from_port = 2049 + to_port = 2049 + protocol = "tcp" + security_groups = [aws_security_group.baseca.id] + } + + egress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } +} + +resource "aws_kms_key" "baseca_efs" { + description = "baseca EFS Encryption Key" +} + +data "aws_caller_identity" "account" {} diff --git a/terraform/production/compute/iam.tf b/terraform/production/compute/iam.tf new file mode 100644 index 0000000..729ddc8 --- /dev/null +++ b/terraform/production/compute/iam.tf @@ -0,0 +1,21 @@ +resource "aws_iam_role" "ecs_task_execution_role" { + name = "${var.service}-${var.environment}-ecs" + + assume_role_policy = jsonencode({ + Version = "2012-10-17", + Statement = [ + { + Action = "sts:AssumeRole", + Effect = "Allow", + Principal = { + Service = ["ecs-tasks.amazonaws.com"] + } + } + ] + }) +} + +resource "aws_iam_role_policy_attachment" "ecs_task_execution_role_policy_attachment" { + role = aws_iam_role.ecs_task_execution_role.name + policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy" +} diff --git a/terraform/production/compute/variables.tf b/terraform/production/compute/variables.tf new file mode 100644 index 0000000..ffa2b0f --- /dev/null +++ b/terraform/production/compute/variables.tf @@ -0,0 +1,86 @@ +variable "service" { + description = "Resource Prefix" + type = string + default = "baseca" +} + +variable "environment" { + description = "Service Environment" + type = string +} + +variable "region" { + description = "AWS Region" + type = string +} + +variable "configuration" { + description = "baseca Deployment Configuration" + type = string +} + +variable "vpc_id" { + description = "AWS VPC ID" + type = string +} + +variable "public_ip" { + description = "ECS Assign Public IP" + type = bool + default = false +} + +variable "subnet_ids" { + type = list(string) +} + +variable "network_ingress" { + description = "List of IP CIDR Blocks for Ingress to Service" + type = list(string) +} + +variable "host_port" { + description = "EC2 Instance Service" + type = number + default = 9090 +} + +variable "lb_port" { + description = "Application Load Balancer Port" + type = number + default = 9090 +} + +variable "min_instance_count" { + description = "Minimum Instances in Target Group" + type = number + default = 1 +} + +variable "max_instance_count" { + description = "Maximum Instance in Target Group" + type = number + default = 2 +} + +variable "cpu" { + description = "CPU Allocated for ECS Service" + type = number + default = 2048 +} + +variable "memory" { + description = "RAM Allocated for ECS Service" + type = number + default = 4096 +} + +variable "baseca_iam_role" { + description = "baseca Control Plane IAM Role ARN" + type = string +} + +variable "ecr_repository" { + description = "baseca ECR Registry ARN" + type = string +} \ No newline at end of file diff --git a/terraform/production/output.tf b/terraform/production/output.tf new file mode 100644 index 0000000..65d0eab --- /dev/null +++ b/terraform/production/output.tf @@ -0,0 +1,29 @@ +output "kinesis_firehose_stream" { + description = "baseca Kinesis Firehose Delivery Stream" + value = module.baseca.kinesis_firehose_stream +} + +output "kms_key_id" { + description = "baseca KMS Key for Signing User Auth Token" + value = module.baseca.kms_key_id +} + +output "rds_writer_endpoint" { + description = "baseca RDS Writer Endpoint DNS Address" + value = module.baseca.rds_writer_endpoint +} + +output "rds_reader_endpoint" { + description = "baseca RDS Reader Endpoint DNS Address" + value = module.baseca.rds_reader_endpoint +} + +output "redis_endpoint" { + description = "baseca Elasticache Redis DNS Address" + value = module.baseca.redis_endpoint +} + +output "ecr_repository" { + description = "baseca Elastic Container Registry (ECR) ARN" + value = module.baseca.ecr_repository +} \ No newline at end of file diff --git a/terraform/production/provider.tf b/terraform/production/provider.tf new file mode 100644 index 0000000..808dcf3 --- /dev/null +++ b/terraform/production/provider.tf @@ -0,0 +1,12 @@ +terraform { + required_providers { + random = { + source = "hashicorp/random" + version = "3.4.3" + } + aws = { + source = "hashicorp/aws" + version = "5.1.0" + } + } +} diff --git a/test/config.go b/test/config.go new file mode 100644 index 0000000..e767b22 --- /dev/null +++ b/test/config.go @@ -0,0 +1,62 @@ +package test + +import ( + "fmt" + "os" + "path/filepath" + "runtime" + + "github.com/coinbase/baseca/internal/config" + "github.com/coinbase/baseca/internal/logger" +) + +const ( + configuration = "config.test.local.sandbox.yml" +) + +func GetTestConfigurationPath() (*config.Config, error) { + _, filename, _, ok := runtime.Caller(0) + if !ok { + fmt.Println("Error: Unable to get current file path") + } + + baseDir := filepath.Dir(filename) + for { + if _, err := os.Stat(filepath.Join(baseDir, "go.mod")); err == nil { + break + } + + parentDir := filepath.Dir(baseDir) + if parentDir == baseDir { + fmt.Println("Error: Unable to find base directory") + break + } + + baseDir = parentDir + } + + path := fmt.Sprintf("%s/test/config/%s", baseDir, configuration) + config, err := provideConfig(path) + if err != nil { + return nil, err + } + return config, nil +} + +func provideConfig(path string) (*config.Config, error) { + ctxLogger := logger.ContextLogger{Logger: logger.DefaultLogger} + + v, err := config.BuildViper(path) + if err != nil { + ctxLogger.Error(err.Error()) + } + + config, err := config.LoadConfig(v) + if err != nil { + return nil, err + } + + return config, err +} + +// diff --git a/test/config/config.test.local.sandbox.yml b/test/config/config.test.local.sandbox.yml new file mode 100644 index 0000000..53f372b --- /dev/null +++ b/test/config/config.test.local.sandbox.yml @@ -0,0 +1,69 @@ +grpc_server_address: 0.0.0.0:9090 + +ocsp_server: + - sandbox.ocsp.example.com + +database: + database_driver: postgres + database_table: baseca + database_endpoint: localhost + database_reader_endpoint: localhost + database_user: root + database_port: 5432 + region: us-east-1 + ssl_mode: disable + +redis: + cluster_endpoint: xxxxxx.xxxxxx.000000.0001.use1.cache.amazonaws.com + port: 6379 + rate_limit: 20 + exclude_rate_limit: ["example_service"] + +domains: + - example.com + +acm_pca: + sandbox_use1: + region: us-east-1 + ca_arn: arn:aws:acm-pca:us-east-1:123456789012:certificate-authority/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + ca_active_day: 90 + assume_role: false + root_ca: false + default: true + sandbox_use2: + region: us-east-2 + ca_arn: arn:aws:acm-pca:us-east-1:123456789012:certificate-authority/yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy + ca_active_day: 90 + assume_role: false + root_ca: false + default: true + +firehose: + stream: baseca-test + region: us-east-1 + +kms: + key_id: 12345678-1234-1234-1234-123456789012 + signing_algorithm: RSASSA_PSS_SHA_512 + region: us-east-1 + auth_validity: 720 + +secrets_manager: + secret_id: baseca-xxxxxxxxxxxx + region: us-east-1 + +subordinate_ca_metadata: + country: "US" + province: "CA" + locality: "San Francisco" + organization: "Example" + organization_unit: "Security" + email: "example@example.com" + signing_algorithm: SHA512WITHRSA + key_algorithm: "RSA" + key_size: 4096 + +certificate_authority: + sandbox: + - sandbox_use1 + - sandbox_use2 \ No newline at end of file