├── protos ├── BUILD └── external │ ├── BUILD │ └── goval │ ├── BUILD │ └── api │ ├── BUILD.bazel │ ├── client.proto │ ├── signing.proto │ └── client.pb.go ├── examples ├── Makefile ├── BUILD.bazel └── extract.go ├── codecov.yml ├── paserk ├── BUILD.bazel └── paserk.go ├── .gitignore ├── Makefile ├── encoding.go ├── scripts ├── install_codecov.sh └── codecov.sh ├── go.mod ├── README.md ├── .semaphore └── semaphore.yml ├── LICENSE ├── keys.go ├── keys_test.go ├── BUILD.bazel ├── box_test.go ├── WORKSPACE ├── box.go ├── auth.go ├── identity_test.go ├── util.go ├── go.sum ├── sign.go ├── deps.bzl ├── verify.go └── sign_test.go /protos/BUILD: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /protos/external/BUILD: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /protos/external/goval/BUILD: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/Makefile: -------------------------------------------------------------------------------- 1 | 2 | all: *.go 3 | go build . 4 | 5 | .PHONY: all 6 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: 4 | default: 5 | target: auto 6 | # Allow coverage to drop by at most 2%. 7 | threshold: 2% -------------------------------------------------------------------------------- /examples/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library") 2 | 3 | go_library( 4 | name = "examples_lib", 5 | srcs = ["extract.go"], 6 | importpath = "github.com/replit/go-replidentity/examples", 7 | visibility = ["//visibility:private"], 8 | deps = ["//:go-replidentity"], 9 | ) 10 | 11 | go_binary( 12 | name = "examples", 13 | embed = [":examples_lib"], 14 | visibility = ["//visibility:public"], 15 | ) 16 | -------------------------------------------------------------------------------- /paserk/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_library") 2 | 3 | go_library( 4 | name = "paserk", 5 | srcs = ["paserk.go"], 6 | importpath = "github.com/replit/go-replidentity/paserk", 7 | visibility = ["//visibility:public"], 8 | deps = [ 9 | "//protos/external/goval/api", 10 | "@org_golang_google_protobuf//proto", 11 | "@org_golang_x_crypto//blake2b", 12 | "@org_golang_x_crypto//ed25519", 13 | ], 14 | ) 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | 17 | *.stamp 18 | 19 | # Ignore all bazel-* symlinks. There is no full list since this can change 20 | # based on the name of the directory bazel is cloned into. 21 | /bazel-* 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | SUBDIRS := examples 3 | 4 | %.pb.go: %.proto 5 | protoc \ 6 | -I. \ 7 | --go_out=paths=source_relative:. \ 8 | $< 9 | 10 | ./protos/external/goval/api/.proto.stamp: protos/external/goval/api/client.pb.go protos/external/goval/api/signing.pb.go 11 | touch $@ 12 | 13 | main: ./protos/external/goval/api/.proto.stamp 14 | go build . 15 | 16 | test: ./protos/external/goval/api/.proto.stamp 17 | go test . 18 | 19 | all: *.go ./protos/external/goval/api/.proto.stamp examples 20 | 21 | $(SUBDIRS): 22 | $(MAKE) -C $@ 23 | 24 | .PHONY: all $(SUBDIRS) 25 | -------------------------------------------------------------------------------- /encoding.go: -------------------------------------------------------------------------------- 1 | package replidentity 2 | 3 | import ( 4 | "crypto/ed25519" 5 | "crypto/x509" 6 | "encoding/pem" 7 | "fmt" 8 | ) 9 | 10 | func pemToPubkey(key string) (ed25519.PublicKey, error) { 11 | block, _ := pem.Decode([]byte(key)) 12 | if block == nil { 13 | return nil, fmt.Errorf("failed to decode public key: %s", key) 14 | } 15 | 16 | pub, err := x509.ParsePKIXPublicKey(block.Bytes) 17 | if err != nil { 18 | return nil, fmt.Errorf("failed to decode public key: %w", err) 19 | } 20 | 21 | pubkey, ok := pub.(ed25519.PublicKey) 22 | if !ok { 23 | return nil, fmt.Errorf("unknown pubkey type: %T", pub) 24 | } 25 | 26 | return pubkey, nil 27 | } 28 | -------------------------------------------------------------------------------- /scripts/install_codecov.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Get the codecov uploader, verify integrity 4 | curl --max-time 30 https://keybase.io/codecovsecurity/pgp_keys.asc | gpg --no-default-keyring --keyring trustedkeys.gpg --import # One-time step 5 | 6 | curl -Ov https://uploader.codecov.io/latest/linux/codecov 7 | curl -Ov https://uploader.codecov.io/latest/linux/codecov.SHA256SUM 8 | curl -Ov https://uploader.codecov.io/latest/linux/codecov.SHA256SUM.sig 9 | gpgv codecov.SHA256SUM.sig codecov.SHA256SUM 10 | shasum -a 256 -c codecov.SHA256SUM 11 | 12 | # Make sure codecov doesn't turn this into an unclean checkout 13 | rm -f codecov.SHA256SUM.sig codecov.SHA256SUM 14 | chmod +x codecov 15 | sudo mv codecov /usr/bin/ -------------------------------------------------------------------------------- /scripts/codecov.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | SHA="-C $SEMAPHORE_GIT_SHA" 4 | BRANCH="-B $SEMAPHORE_GIT_BRANCH" 5 | PR= 6 | if [ "x$SEMAPHORE_GIT_PR_NUMBER" != "x" ]; then 7 | PR="-P $SEMAPHORE_GIT_PR_NUMBER" 8 | SHA="-C $SEMAPHORE_GIT_PR_SHA" 9 | BRANCH="-B $SEMAPHORE_GIT_PR_BRANCH" 10 | fi 11 | BUILD="-b $SEMAPHORE_JOB_ID" 12 | NAME="-n '$SEMAPHORE_JOB_NAME'" 13 | 14 | # Are we in a PR context? The variables are wrong if so. 15 | if [[ "$SEMAPHORE_GIT_REF_TYPE" == "pull-request" ]]; then 16 | echo "codecov wrapper: this appears to be a PR named '$SEMAPHORE_GIT_PR_NAME', setting params accordingly..." 17 | fi 18 | 19 | codecov $SHA $BRANCH $PR $BUILD -r $SEMAPHORE_GIT_REPO_SLUG -f 'coverage.out' "$@" || echo 'Failed to upload coverage data.' 20 | rm -f coverage.out -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/replit/go-replidentity 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.24.7 6 | 7 | require ( 8 | filippo.io/edwards25519 v1.1.0 9 | github.com/o1egl/paseto v1.0.0 10 | github.com/stretchr/testify v1.8.0 11 | golang.org/x/crypto v0.35.0 12 | google.golang.org/protobuf v1.33.0 13 | ) 14 | 15 | require ( 16 | github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect 17 | github.com/aead/chacha20poly1305 v0.0.0-20170617001512-233f39982aeb // indirect 18 | github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635 // indirect 19 | github.com/davecgh/go-spew v1.1.1 // indirect 20 | github.com/pkg/errors v0.8.0 // indirect 21 | github.com/pmezard/go-difflib v1.0.0 // indirect 22 | golang.org/x/sys v0.30.0 // indirect 23 | gopkg.in/yaml.v3 v3.0.1 // indirect 24 | ) 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Repl Identity 2 | 3 | [![Go Reference](https://pkg.go.dev/badge/github.com/replit/go-replidentity.svg)](https://pkg.go.dev/github.com/replit/go-replidentity) 4 | 5 | Blog post: https://blog.replit.com/repl-identity 6 | 7 | Repl Identity stores a `REPL_IDENTITY` token in every Repl automatically. This 8 | token is a signed [PASETO](https://paseto.io) that includes verifiable repl 9 | identity data (such as the user in the repl, and the repl ID). 10 | 11 | This package provides the necessary code to verify these tokens. 12 | 13 | Check the example at `examples/extract.go` for an example usage. You can also 14 | see this in action at https://replit.com/@mattiselin/repl-identity. If you are 15 | logged in to Replit, you'll see your username when you click "Run" on the Cover 16 | Page - that's Repl Identity at work. 17 | -------------------------------------------------------------------------------- /.semaphore/semaphore.yml: -------------------------------------------------------------------------------- 1 | version: v1.0 2 | name: go-replidentity 3 | 4 | agent: 5 | machine: 6 | type: e1-standard-4 7 | os_image: ubuntu2004 8 | 9 | fail_fast: 10 | stop: 11 | when: "true" 12 | 13 | auto_cancel: 14 | running: 15 | when: branch != 'main' 16 | 17 | global_job_config: 18 | secrets: 19 | - name: codecov-go-replidentity 20 | epilogue: 21 | always: 22 | commands: 23 | - '[[ -e scripts/codecov.sh ]] && ./scripts/codecov.sh' 24 | 25 | blocks: 26 | - name: test 27 | task: 28 | prologue: 29 | commands: 30 | - checkout 31 | - ./scripts/install_codecov.sh 32 | - sem-version go 1.22 33 | - go mod download 34 | jobs: 35 | - name: run tests 36 | commands: 37 | - go test -cover -covermode=atomic -coverprofile coverage.out 38 | dependencies: [] 39 | -------------------------------------------------------------------------------- /protos/external/goval/api/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@rules_proto//proto:defs.bzl", "proto_library") 2 | load("@io_bazel_rules_go//go:def.bzl", "go_library") 3 | load("@io_bazel_rules_go//proto:def.bzl", "go_proto_library") 4 | 5 | proto_library( 6 | name = "api_proto", 7 | srcs = [ 8 | "client.proto", 9 | "signing.proto", 10 | ], 11 | visibility = ["//visibility:public"], 12 | deps = ["@com_google_protobuf//:timestamp_proto"], 13 | ) 14 | 15 | go_proto_library( 16 | name = "api_go_proto", 17 | importpath = "github.com/replit/go-replidentity/protos/external/goval/api", 18 | proto = ":api_proto", 19 | visibility = ["//visibility:public"], 20 | ) 21 | 22 | go_library( 23 | name = "api", 24 | embed = [":api_go_proto"], 25 | importpath = "github.com/replit/go-replidentity/protos/external/goval/api", 26 | visibility = ["//visibility:public"], 27 | ) 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Replit 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /keys.go: -------------------------------------------------------------------------------- 1 | package replidentity 2 | 3 | import ( 4 | "crypto/ed25519" 5 | "crypto/sha512" 6 | 7 | "filippo.io/edwards25519" 8 | "golang.org/x/crypto/curve25519" 9 | ) 10 | 11 | // ed25519PrivateKeyToCurve25519 converts a ed25519 private key in X25519 equivalent 12 | // source: https://github.com/FiloSottile/age/blob/980763a16e30ea5c285c271344d2202fcb18c33b/agessh/agessh.go#L287 13 | func Ed25519PrivateKeyToCurve25519(pk ed25519.PrivateKey) [32]byte { 14 | h := sha512.New() 15 | h.Write(pk.Seed()) 16 | out := h.Sum(nil) 17 | var res [curve25519.ScalarSize]byte 18 | copy(res[:], out[:curve25519.ScalarSize]) 19 | return res 20 | } 21 | 22 | // ed25519PublicKeyToCurve25519 converts a ed25519 public key in X25519 equivalent 23 | // source: https://github.com/FiloSottile/age/blob/main/agessh/agessh.go#L190 24 | func Ed25519PublicKeyToCurve25519(pk ed25519.PublicKey) ([32]byte, error) { 25 | // See https://blog.filippo.io/using-ed25519-keys-for-encryption and 26 | // https://pkg.go.dev/filippo.io/edwards25519#Point.BytesMontgomery. 27 | p, err := new(edwards25519.Point).SetBytes(pk) 28 | var res [curve25519.ScalarSize]byte 29 | if err != nil { 30 | return res, err 31 | } 32 | copy(res[:], p.BytesMontgomery()) 33 | return res, nil 34 | } 35 | -------------------------------------------------------------------------------- /examples/extract.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/replit/go-replidentity" 7 | ) 8 | 9 | func main() { 10 | // To prevent security problems, every time we prove our identity 11 | // to another Repl, it needs to be addressed to it, so that the 12 | // other Repl cannot grab that identity token and spoof you. 13 | // In order to do that, we need to get that other Repl's `$REPL_ID`. 14 | audience := "another-cool-repl-id" 15 | 16 | identityToken, err := replidentity.CreateIdentityTokenAddressedTo(audience) 17 | if err != nil { 18 | panic(err) 19 | } 20 | 21 | // The other Repl can now be sent the identityToken and can verify 22 | // the authenticity of it! 23 | // In this case, we'll just immediately verify it for demo purposes. 24 | 25 | // audience := os.Getenv("REPL_ID") // uncomment this on the other Repl. 26 | replIdentity, err := replidentity.VerifyIdentity( 27 | identityToken, 28 | []string{audience}, 29 | replidentity.ReadPublicKeyFromEnv, 30 | ) 31 | if err != nil { 32 | panic(err) 33 | } 34 | 35 | fmt.Println() 36 | fmt.Printf("The identity token (%d bytes) is:\n", len(identityToken)) 37 | fmt.Printf("repl id: %s\n user: %s\n slug: %s\n audience: %s\n ephemeral: %v\n origin: %v\n", replIdentity.Replid, replIdentity.User, replIdentity.Slug, replIdentity.Aud, replIdentity.Ephemeral, replIdentity.OriginReplid) 38 | } 39 | -------------------------------------------------------------------------------- /keys_test.go: -------------------------------------------------------------------------------- 1 | package replidentity 2 | 3 | import ( 4 | "crypto/ed25519" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | var ( 12 | // https://github.com/jedisct1/libsodium/blob/09e995c0c85a0026510704b8ce7f5867a09a3841/test/default/ed25519_convert.c#L5 13 | seed = []byte{ 14 | 0x42, 0x11, 0x51, 0xa4, 0x59, 0xfa, 0xea, 0xde, 0x3d, 0x24, 0x71, 0x15, 0xf9, 0x4a, 0xed, 0xae, 15 | 0x42, 0x31, 0x81, 0x24, 0x09, 0x5a, 0xfa, 0xbe, 0x4d, 0x14, 0x51, 0xa5, 0x59, 0xfa, 0xed, 0xee, 16 | } 17 | ) 18 | 19 | func TestEd25519PrivateKeyToCurve25519(t *testing.T) { 20 | privateKey := ed25519.NewKeyFromSeed(seed) 21 | 22 | privateCurve := Ed25519PrivateKeyToCurve25519(privateKey) 23 | // https://github.com/jedisct1/libsodium/blob/09e995c0c85a0026510704b8ce7f5867a09a3841/test/default/ed25519_convert.c#L36 24 | assert.Equal(t, privateCurve, [32]byte{ 25 | 0x82, 0x52, 0x03, 0x03, 0x76, 0xd4, 0x71, 0x12, 0xbe, 0x7f, 0x73, 0xed, 0x7a, 0x01, 0x92, 0x93, 26 | 0xdd, 0x12, 0xad, 0x91, 0x0b, 0x65, 0x44, 0x55, 0x79, 0x8b, 0x46, 0x67, 0xd7, 0x3d, 0xe1, 0xe6, 27 | }) 28 | publicCurve, err := Ed25519PublicKeyToCurve25519(privateKey.Public().(ed25519.PublicKey)) 29 | require.NoError(t, err) 30 | // https://github.com/jedisct1/libsodium/blob/09e995c0c85a0026510704b8ce7f5867a09a3841/test/default/ed25519_convert.c#L35 31 | assert.Equal(t, publicCurve, [32]byte{ 32 | 0xf1, 0x81, 0x4f, 0x0e, 0x8f, 0xf1, 0x04, 0x3d, 0x8a, 0x44, 0xd2, 0x5b, 0xab, 0xff, 0x3c, 0xed, 33 | 0xca, 0xe6, 0xc2, 0x2c, 0x3e, 0xda, 0xa4, 0x8f, 0x85, 0x7a, 0xe7, 0x0d, 0xe2, 0xba, 0xae, 0x50, 34 | }) 35 | } 36 | -------------------------------------------------------------------------------- /BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") 2 | load("@bazel_gazelle//:def.bzl", "gazelle") 3 | 4 | # gazelle:prefix github.com/replit/go-replidentity 5 | gazelle(name = "gazelle") 6 | 7 | gazelle( 8 | name = "gazelle-update-repos", 9 | args = [ 10 | "-from_file=go.mod", 11 | "-prune", 12 | "-to_macro=deps.bzl%go_dependencies", 13 | ], 14 | command = "update-repos", 15 | ) 16 | 17 | go_library( 18 | name = "go-replidentity", 19 | srcs = [ 20 | "auth.go", 21 | "encoding.go", 22 | "sign.go", 23 | "util.go", 24 | "verify.go", 25 | ], 26 | importpath = "github.com/replit/go-replidentity", 27 | visibility = ["//visibility:public"], 28 | deps = [ 29 | "//paserk", 30 | "//protos/external/goval/api", 31 | "@com_github_o1egl_paseto//:paseto", 32 | "@org_golang_google_protobuf//encoding/protojson", 33 | "@org_golang_google_protobuf//proto", 34 | "@org_golang_x_crypto//ed25519", 35 | ], 36 | ) 37 | 38 | go_test( 39 | name = "go-replidentity_test", 40 | srcs = [ 41 | "identity_test.go", 42 | "sign_test.go", 43 | ], 44 | embed = [":go-replidentity"], 45 | deps = [ 46 | "//paserk", 47 | "//protos/external/goval/api", 48 | "@com_github_o1egl_paseto//:paseto", 49 | "@com_github_stretchr_testify//assert", 50 | "@com_github_stretchr_testify//require", 51 | "@org_golang_google_protobuf//proto", 52 | "@org_golang_google_protobuf//types/known/timestamppb", 53 | "@org_golang_x_crypto//ed25519", 54 | ], 55 | ) 56 | -------------------------------------------------------------------------------- /box_test.go: -------------------------------------------------------------------------------- 1 | package replidentity 2 | 3 | import ( 4 | "crypto/ed25519" 5 | "crypto/rand" 6 | "encoding/base64" 7 | "fmt" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | "github.com/stretchr/testify/require" 12 | 13 | "github.com/replit/go-replidentity/paserk" 14 | "github.com/replit/go-replidentity/protos/external/goval/api" 15 | ) 16 | 17 | func TestBoxAnonymous(t *testing.T) { 18 | privkey, identity, err := identityToken("repl", "user", 1, "slug") 19 | require.NoError(t, err) 20 | 21 | getPubKey := func(keyid, issuer string) (ed25519.PublicKey, error) { 22 | if keyid != developmentKeyID { 23 | return nil, nil 24 | } 25 | keyBytes, err := base64.StdEncoding.DecodeString(developmentPublicKey) 26 | if err != nil { 27 | return nil, fmt.Errorf("failed to parse public key as base64: %w", err) 28 | } 29 | 30 | return ed25519.PublicKey(keyBytes), nil 31 | } 32 | 33 | signingAuthority, err := NewSigningAuthority( 34 | string(paserk.PrivateKeyToPASERKSecret(privkey)), 35 | identity, 36 | "repl", 37 | getPubKey, 38 | ) 39 | require.NoError(t, err) 40 | forwarded, err := signingAuthority.Sign("testing") 41 | require.NoError(t, err) 42 | 43 | verifiedToken, err := VerifyToken(VerifyTokenOpts{ 44 | Message: forwarded, 45 | Audience: []string{"testing"}, 46 | GetPubKey: getPubKey, 47 | Flags: []api.FlagClaim{api.FlagClaim_IDENTITY}, 48 | }) 49 | require.NoError(t, err) 50 | 51 | secret := "secret message" 52 | 53 | sealedBox, err := verifiedToken.SealAnonymousBox([]byte(secret), rand.Reader) 54 | require.NoError(t, err) 55 | 56 | unsealedBox, err := signingAuthority.OpenAnonymousBox(sealedBox) 57 | require.NoError(t, err) 58 | 59 | assert.Equal(t, secret, string(unsealedBox)) 60 | } 61 | -------------------------------------------------------------------------------- /WORKSPACE: -------------------------------------------------------------------------------- 1 | load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") 2 | 3 | # ---------- Go ---------- 4 | 5 | http_archive( 6 | name = "io_bazel_rules_go", 7 | sha256 = "56d8c5a5c91e1af73eca71a6fab2ced959b67c86d12ba37feedb0a2dfea441a6", 8 | urls = [ 9 | "https://mirror.bazel.build/github.com/bazelbuild/rules_go/releases/download/v0.37.0/rules_go-v0.37.0.zip", 10 | "https://github.com/bazelbuild/rules_go/releases/download/v0.37.0/rules_go-v0.37.0.zip", 11 | ], 12 | ) 13 | 14 | http_archive( 15 | name = "bazel_gazelle", 16 | sha256 = "448e37e0dbf61d6fa8f00aaa12d191745e14f07c31cabfa731f0c8e8a4f41b97", 17 | urls = [ 18 | "https://mirror.bazel.build/github.com/bazelbuild/bazel-gazelle/releases/download/v0.28.0/bazel-gazelle-v0.28.0.tar.gz", 19 | "https://github.com/bazelbuild/bazel-gazelle/releases/download/v0.28.0/bazel-gazelle-v0.28.0.tar.gz", 20 | ], 21 | ) 22 | 23 | load("@io_bazel_rules_go//go:deps.bzl", "go_register_toolchains", "go_rules_dependencies") 24 | load("@bazel_gazelle//:deps.bzl", "gazelle_dependencies") 25 | load("//:deps.bzl", "go_dependencies") 26 | 27 | # gazelle:repository_macro deps.bzl%go_dependencies 28 | go_dependencies() 29 | 30 | go_rules_dependencies() 31 | 32 | go_register_toolchains(version = "1.19.3") 33 | 34 | gazelle_dependencies() 35 | 36 | # ---------- Protobuf ---------- 37 | 38 | http_archive( 39 | name = "com_google_protobuf", 40 | sha256 = "d0f5f605d0d656007ce6c8b5a82df3037e1d8fe8b121ed42e536f569dec16113", 41 | strip_prefix = "protobuf-3.14.0", 42 | urls = [ 43 | "https://mirror.bazel.build/github.com/protocolbuffers/protobuf/archive/v3.14.0.tar.gz", 44 | "https://github.com/protocolbuffers/protobuf/archive/v3.14.0.tar.gz", 45 | ], 46 | ) 47 | 48 | load("@com_google_protobuf//:protobuf_deps.bzl", "protobuf_deps") 49 | 50 | protobuf_deps() -------------------------------------------------------------------------------- /box.go: -------------------------------------------------------------------------------- 1 | package replidentity 2 | 3 | import ( 4 | "crypto/ed25519" 5 | "fmt" 6 | "io" 7 | 8 | "golang.org/x/crypto/nacl/box" 9 | 10 | "github.com/replit/go-replidentity/paserk" 11 | ) 12 | 13 | // SealAnonymousBox encrypts a message using the public key of the certificate. 14 | // Only the private key can decrypt the message. 15 | // 16 | // This uses 17 | // https://pkg.go.dev/golang.org/x/crypto@v0.42.0/nacl/box#SealAnonymous, and 18 | // uses the ed25519 public key embedded in the certificate (converted to 19 | // curve25519 public key). 20 | func (v *VerifiedToken) SealAnonymousBox(message []byte, rand io.Reader) ([]byte, error) { 21 | pubkey, err := paserk.PASERKPublicToPublicKey(paserk.PASERKPublic(v.Certificate.GetPublicKey())) 22 | if err != nil { 23 | return nil, fmt.Errorf("paserk public key to ed25519 public key: %w", err) 24 | } 25 | 26 | curve25519Pubkey, err := Ed25519PublicKeyToCurve25519(pubkey) 27 | if err != nil { 28 | return nil, fmt.Errorf("ed25519 public key to curve25519 public key: %w", err) 29 | } 30 | 31 | result, err := box.SealAnonymous( 32 | nil, 33 | message, 34 | &curve25519Pubkey, 35 | rand, 36 | ) 37 | if err != nil { 38 | return nil, fmt.Errorf("box.SealAnonymous: %w", err) 39 | } 40 | 41 | return result, nil 42 | } 43 | 44 | // OpenAnonymousBox decrypts a message encrypted with [SealAnonymousBox] using 45 | // the private key of the signature authority. 46 | // 47 | // This uses 48 | // https://pkg.go.dev/golang.org/x/crypto@v0.42.0/nacl/box#OpenAnonymous, and 49 | // uses the ed25519 private key (converted to curve25519 private key). 50 | func (s *SigningAuthority) OpenAnonymousBox(sealedBox []byte) ([]byte, error) { 51 | curve25519Privkey := Ed25519PrivateKeyToCurve25519(s.privateKey) 52 | curve25519Pubkey, err := Ed25519PublicKeyToCurve25519(s.privateKey.Public().(ed25519.PublicKey)) 53 | if err != nil { 54 | return nil, fmt.Errorf("ed25519 private key to curve25519 private key: %w", err) 55 | } 56 | 57 | message, ok := box.OpenAnonymous(nil, sealedBox, &curve25519Pubkey, &curve25519Privkey) 58 | if !ok { 59 | return nil, fmt.Errorf("box.OpenAnonymous") 60 | } 61 | 62 | return message, nil 63 | } 64 | -------------------------------------------------------------------------------- /auth.go: -------------------------------------------------------------------------------- 1 | // Package replidentity provides verification utilities for Repl Identity tokens. 2 | package replidentity 3 | 4 | import ( 5 | "encoding/base64" 6 | "fmt" 7 | 8 | "github.com/o1egl/paseto" 9 | "golang.org/x/crypto/ed25519" 10 | "google.golang.org/protobuf/proto" 11 | 12 | "github.com/replit/go-replidentity/protos/external/goval/api" 13 | ) 14 | 15 | // PubKeySource provides an interface for looking up an [ed25519.PublicKey] from some external source. 16 | type PubKeySource func(keyid, issuer string) (ed25519.PublicKey, error) 17 | 18 | // MessageClaims is a collection of indexable claims that are made by a certificate. 19 | type MessageClaims struct { 20 | Repls map[string]struct{} 21 | Users map[string]struct{} 22 | UserIDs map[int64]struct{} 23 | Orgs map[OrgKey]struct{} 24 | Clusters map[string]struct{} 25 | Subclusters map[string]struct{} 26 | Flags map[api.FlagClaim]struct{} 27 | } 28 | 29 | type OrgKey struct { 30 | Id string 31 | Typ api.Org_OrgType 32 | } 33 | 34 | func parseClaims(cert *api.GovalCert) *MessageClaims { 35 | if cert == nil { 36 | return nil 37 | } 38 | 39 | claims := MessageClaims{ 40 | Repls: map[string]struct{}{}, 41 | Users: map[string]struct{}{}, 42 | UserIDs: map[int64]struct{}{}, 43 | Orgs: map[OrgKey]struct{}{}, 44 | Clusters: map[string]struct{}{}, 45 | Subclusters: map[string]struct{}{}, 46 | Flags: map[api.FlagClaim]struct{}{}, 47 | } 48 | 49 | for _, claim := range cert.Claims { 50 | switch typedClaim := claim.Claim.(type) { 51 | case *api.CertificateClaim_Replid: 52 | claims.Repls[typedClaim.Replid] = struct{}{} 53 | 54 | case *api.CertificateClaim_User: 55 | claims.Users[typedClaim.User] = struct{}{} 56 | 57 | case *api.CertificateClaim_UserId: 58 | claims.UserIDs[typedClaim.UserId] = struct{}{} 59 | 60 | case *api.CertificateClaim_Org: 61 | orgKey := OrgKey{ 62 | Id: typedClaim.Org.Id, 63 | Typ: typedClaim.Org.Type, 64 | } 65 | claims.Orgs[orgKey] = struct{}{} 66 | 67 | case *api.CertificateClaim_Cluster: 68 | claims.Clusters[typedClaim.Cluster] = struct{}{} 69 | 70 | case *api.CertificateClaim_Subcluster: 71 | claims.Subclusters[typedClaim.Subcluster] = struct{}{} 72 | 73 | case *api.CertificateClaim_Flag: 74 | claims.Flags[typedClaim.Flag] = struct{}{} 75 | } 76 | } 77 | 78 | return &claims 79 | } 80 | 81 | func getSigningAuthority(message string) (*api.GovalSigningAuthority, error) { 82 | var encodedFooter string 83 | err := paseto.ParseFooter(message, &encodedFooter) 84 | if err != nil { 85 | return nil, err 86 | } 87 | 88 | if len(encodedFooter) == 0 { 89 | return nil, fmt.Errorf("footer is empty") 90 | } 91 | 92 | footerBytes, err := base64.StdEncoding.DecodeString(encodedFooter) 93 | if err != nil { 94 | return nil, fmt.Errorf("failed to decode footer: %w", err) 95 | } 96 | 97 | var signingAuthority api.GovalSigningAuthority 98 | err = proto.Unmarshal(footerBytes, &signingAuthority) 99 | if err != nil { 100 | return nil, fmt.Errorf("failed to unmarshal footer: %w", err) 101 | } 102 | 103 | return &signingAuthority, nil 104 | } 105 | -------------------------------------------------------------------------------- /identity_test.go: -------------------------------------------------------------------------------- 1 | package replidentity_test 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/replit/go-replidentity" 8 | ) 9 | 10 | func Example() { 11 | identity := os.Getenv("REPL_IDENTITY") 12 | if identity == "" { 13 | fmt.Println("Sorry, this repl does not yet have an identity (anonymous run?).") 14 | return 15 | } 16 | identityKey := os.Getenv("REPL_IDENTITY_KEY") 17 | if identity == "" { 18 | fmt.Println("Sorry, this repl does not yet have an identity (anonymous run?).") 19 | return 20 | } 21 | 22 | // This should be set to the Repl ID of the repl you want to prove your 23 | // identity to. 24 | targetRepl := "target_repl" 25 | 26 | // Create a signing authority that is authorized to emit tokens for the 27 | // current repl. 28 | signingAuthority, err := replidentity.NewSigningAuthority( 29 | string(identityKey), 30 | identity, 31 | os.Getenv("REPL_ID"), 32 | replidentity.ReadPublicKeyFromEnv, 33 | ) 34 | if err != nil { 35 | panic(err) 36 | } 37 | 38 | signedToken, err := signingAuthority.Sign(targetRepl) 39 | if err != nil { 40 | panic(err) 41 | } 42 | 43 | // Verify the signed token, pretending we are the target repl. 44 | replIdentity, err := replidentity.VerifyIdentity( 45 | signedToken, 46 | []string{targetRepl}, 47 | replidentity.ReadPublicKeyFromEnv, 48 | ) 49 | if err != nil { 50 | panic(err) 51 | } 52 | 53 | fmt.Println() 54 | fmt.Printf("The identity in the repl's token (%d bytes) is:\n", len(identity)) 55 | fmt.Printf( 56 | "repl id: %s\n user: %s\n slug: %s audience: %s\n", 57 | replIdentity.Replid, 58 | replIdentity.User, 59 | replIdentity.Slug, 60 | replIdentity.Aud, 61 | ) 62 | } 63 | 64 | func ExampleVerifyRenewIdentity() { 65 | identity := os.Getenv("REPL_RENEWAL") 66 | if identity == "" { 67 | fmt.Println("Sorry, this repl does not yet have an identity (anonymous run?).") 68 | return 69 | } 70 | identityKey := os.Getenv("REPL_RENEWAL_KEY") 71 | if identity == "" { 72 | fmt.Println("Sorry, this repl does not yet have an identity (anonymous run?).") 73 | return 74 | } 75 | 76 | // This should be set to the Repl ID of the repl you want to prove your 77 | // identity to. 78 | targetRepl := "target_repl" 79 | 80 | // Create a signing authority that is authorized to emit tokens for the 81 | // current repl. 82 | signingAuthority, err := replidentity.NewSigningAuthority( 83 | string(identityKey), 84 | identity, 85 | os.Getenv("REPL_ID"), 86 | replidentity.ReadPublicKeyFromEnv, 87 | ) 88 | if err != nil { 89 | panic(err) 90 | } 91 | 92 | signedToken, err := signingAuthority.Sign(targetRepl) 93 | if err != nil { 94 | panic(err) 95 | } 96 | 97 | // Verify the signed token, pretending we are the target repl. 98 | replIdentity, err := replidentity.VerifyRenewIdentity( 99 | signedToken, 100 | []string{targetRepl}, 101 | replidentity.ReadPublicKeyFromEnv, 102 | ) 103 | if err != nil { 104 | panic(err) 105 | } 106 | 107 | fmt.Println() 108 | fmt.Printf("The identity in the repl's token (%d bytes) is:\n", len(identity)) 109 | fmt.Printf( 110 | "repl id: %s\n user: %s\n slug: %s audience: %s\n", 111 | replIdentity.Replid, 112 | replIdentity.User, 113 | replIdentity.Slug, 114 | replIdentity.Aud, 115 | ) 116 | } 117 | -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | package replidentity 2 | 3 | import ( 4 | "encoding/base64" 5 | "encoding/json" 6 | "fmt" 7 | "os" 8 | 9 | "golang.org/x/crypto/ed25519" 10 | ) 11 | 12 | // ReadPublicKeyFromEnv provides a [PubKeySource] that reads public keys from the `REPL_PUBKEYS` 13 | // environment variable that is present in all repls. 14 | func ReadPublicKeyFromEnv(keyid, issuer string) (ed25519.PublicKey, error) { 15 | var pubkeys map[string]json.RawMessage 16 | err := json.Unmarshal([]byte(os.Getenv("REPL_PUBKEYS")), &pubkeys) 17 | if err != nil { 18 | return nil, fmt.Errorf("failed to unmarshal REPL_PUBKEYS: %w", err) 19 | } 20 | 21 | pubkey, ok := pubkeys[keyid] 22 | if !ok { 23 | // no key 24 | return nil, nil 25 | } 26 | 27 | var keyBase64 string 28 | err = json.Unmarshal(pubkey, &keyBase64) 29 | if err != nil { 30 | return nil, fmt.Errorf("failed to unmarshal pubkey value: %w", err) 31 | } 32 | 33 | keyBytes, err := base64.StdEncoding.DecodeString(keyBase64) 34 | if err != nil { 35 | return nil, fmt.Errorf("failed to parse public key as base64: %w", err) 36 | } 37 | 38 | return ed25519.PublicKey(keyBytes), nil 39 | } 40 | 41 | // CreateIdentityTokenSigningAuthority creates a signing authority with this repl's identity key. 42 | func CreateIdentityTokenSigningAuthority() (*SigningAuthority, error) { 43 | if os.Getenv("REPL_OWNER") == "five-nine" { 44 | return nil, fmt.Errorf("not logged into Replit, no identity present") 45 | } 46 | 47 | identitySigningAuthorityToken, identitySigningAuthorityKey := readIdentity() 48 | 49 | if identitySigningAuthorityToken == "" { 50 | return nil, fmt.Errorf("could not read token from /tmp/replidentity or REPL_IDENTITY env var") 51 | } 52 | 53 | if identitySigningAuthorityKey == "" { 54 | return nil, fmt.Errorf("could not read key from /tmp/replidentity.key or REPL_IDENTITY_KEY env var") 55 | } 56 | 57 | return NewSigningAuthority( 58 | identitySigningAuthorityKey, 59 | identitySigningAuthorityToken, 60 | os.Getenv("REPL_ID"), 61 | ReadPublicKeyFromEnv, 62 | ) 63 | } 64 | 65 | // CreateIdentityTokenAddressedTo returns a Replit identity token that proves this Repl's identity 66 | // that includes an audience claim to restrict forwarding. It creates a new signing authority each 67 | // time, which can be slow. If you plan on signing multiple tokens, use 68 | // CreateIdentityTokenSigningAuthority() to create an authority to sign with. 69 | func CreateIdentityTokenAddressedTo(audience string) (string, error) { 70 | signingAuthority, err := CreateIdentityTokenSigningAuthority() 71 | if err != nil { 72 | return "", err 73 | } 74 | 75 | if signingAuthority == nil { 76 | return "", fmt.Errorf("no signing authority could be created") 77 | } 78 | 79 | identityToken, err := signingAuthority.Sign(audience) 80 | if err != nil { 81 | return "", err 82 | } 83 | 84 | return identityToken, nil 85 | } 86 | 87 | // Try to read from /tmp/replidentity and /tmp/replidentity.key, 88 | // falling back to the environment variables. 89 | func readIdentity() (string, string) { 90 | identity, err := os.ReadFile("/tmp/replidentity") 91 | if err != nil { 92 | identity = []byte(os.Getenv("REPL_IDENTITY")) 93 | } 94 | identityKey, err := os.ReadFile("/tmp/replidentity.key") 95 | if err != nil { 96 | identityKey = []byte(os.Getenv("REPL_IDENTITY_KEY")) 97 | } 98 | 99 | return string(identity), string(identityKey) 100 | } 101 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= 2 | filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= 3 | github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY= 4 | github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA= 5 | github.com/aead/chacha20poly1305 v0.0.0-20170617001512-233f39982aeb h1:6Z/wqhPFZ7y5ksCEV/V5MXOazLaeu/EW97CU5rz8NWk= 6 | github.com/aead/chacha20poly1305 v0.0.0-20170617001512-233f39982aeb/go.mod h1:UzH9IX1MMqOcwhoNOIjmTQeAxrFgzs50j4golQtXXxU= 7 | github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635 h1:52m0LGchQBBVqJRyYYufQuIbVqRawmubW3OFGqK1ekw= 8 | github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635/go.mod h1:lmLxL+FV291OopO93Bwf9fQLQeLyt33VJRUg5VJ30us= 9 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 11 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 12 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= 13 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 14 | github.com/o1egl/paseto v1.0.0 h1:bwpvPu2au176w4IBlhbyUv/S5VPptERIA99Oap5qUd0= 15 | github.com/o1egl/paseto v1.0.0/go.mod h1:5HxsZPmw/3RI2pAwGo1HhOOwSdvBpcuVzO7uDkm+CLU= 16 | github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= 17 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 18 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 19 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 20 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 21 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 22 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 23 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 24 | github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= 25 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 26 | golang.org/x/crypto v0.0.0-20181025213731-e84da0312774/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 27 | golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs= 28 | golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= 29 | golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 30 | golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= 31 | golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 32 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 33 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 34 | google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= 35 | google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 36 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 37 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 38 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 39 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 40 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 41 | -------------------------------------------------------------------------------- /sign.go: -------------------------------------------------------------------------------- 1 | package replidentity 2 | 3 | import ( 4 | "crypto/ed25519" 5 | "encoding/base64" 6 | "errors" 7 | "fmt" 8 | 9 | "github.com/o1egl/paseto" 10 | "github.com/replit/go-replidentity/paserk" 11 | "github.com/replit/go-replidentity/protos/external/goval/api" 12 | "google.golang.org/protobuf/proto" 13 | ) 14 | 15 | // SigningAuthority can generate tokens that prove the identity of one repl 16 | // (your own) against another repl (the audience). Use this to prevent the 17 | // target repl from spoofing your own identity by forwarding the token. 18 | type SigningAuthority struct { 19 | privateKey ed25519.PrivateKey 20 | signingAuthority *api.GovalSigningAuthority 21 | identity *api.GovalReplIdentity 22 | } 23 | 24 | // NewSigningAuthority returns a new SigningAuthority given the marshaled 25 | // private key (obtained from the `REPL_IDENTITY_KEY` environment variable or /tmp/replidentity.key), 26 | // the identity token (obtained from the `REPL_IDENTITY` environment variable or /tmp/replidentity), 27 | // the current Repl ID (obtained from the `REPL_ID` environment varaible), and 28 | // the source of public keys (typically [ReadPublicKeyFromEnv]). 29 | func NewSigningAuthority( 30 | marshaledPrivateKey, 31 | marshaledIdentity string, 32 | replid string, 33 | getPubKey PubKeySource, 34 | ) (*SigningAuthority, error) { 35 | v, bytes, _, err := verifyChain(marshaledIdentity, getPubKey) 36 | if err != nil { 37 | return nil, fmt.Errorf("failed verify message: %w", err) 38 | } 39 | signingAuthority, err := getSigningAuthority(marshaledIdentity) 40 | if err != nil { 41 | return nil, fmt.Errorf("failed to read body type: %w", err) 42 | } 43 | privateKey, err := paserk.PASERKSecretToPrivateKey(paserk.PASERKSecret(marshaledPrivateKey)) 44 | if err != nil { 45 | return nil, fmt.Errorf("failed to read private key: %w", err) 46 | } 47 | 48 | var identity api.GovalReplIdentity 49 | 50 | switch signingAuthority.GetVersion() { 51 | case api.TokenVersion_BARE_REPL_TOKEN: 52 | return nil, errors.New("wrong type of token provided") 53 | case api.TokenVersion_TYPE_AWARE_TOKEN: 54 | err = proto.Unmarshal(bytes, &identity) 55 | if err != nil { 56 | return nil, fmt.Errorf("failed to decode body: %w", err) 57 | } 58 | } 59 | 60 | err = v.checkClaimsAgainstToken(&identity) 61 | if err != nil { 62 | return nil, fmt.Errorf("claim mismatch: %w", err) 63 | } 64 | 65 | if replid != identity.Replid { 66 | return nil, fmt.Errorf("message replid mismatch. expected %q, got %q", replid, identity.Replid) 67 | } 68 | if replid != identity.Aud { 69 | return nil, fmt.Errorf("message audience mismatch. expected %q, got %q", replid, identity.Aud) 70 | } 71 | 72 | return &SigningAuthority{ 73 | privateKey: privateKey, 74 | signingAuthority: signingAuthority, 75 | identity: &identity, 76 | }, nil 77 | } 78 | 79 | func (s *SigningAuthority) String() string { 80 | return fmt.Sprintf("SigningAuthority{signingAuthority: %s, identity: %s}", s.signingAuthority, s.identity) 81 | } 82 | 83 | // Sign generates a new token that can be given to the provided audience, and 84 | // is resistant against forwarding, so that the recipient cannot forward this 85 | // token to another repl and claim it came directly from you. 86 | func (a *SigningAuthority) Sign(audience string) (string, error) { 87 | replIdentity := api.GovalReplIdentity{ 88 | Replid: a.identity.Replid, 89 | User: a.identity.User, 90 | Slug: a.identity.Slug, 91 | Aud: audience, 92 | OriginReplid: a.identity.OriginReplid, 93 | UserId: a.identity.UserId, 94 | Org: a.identity.Org, 95 | BuildInfo: a.identity.BuildInfo, 96 | IsTeam: a.identity.IsTeam, 97 | Roles: a.identity.Roles, 98 | Runtime: a.identity.Runtime, 99 | } 100 | 101 | token, err := signIdentity(a.privateKey, a.signingAuthority, &replIdentity) 102 | if err != nil { 103 | return "", fmt.Errorf("sign identity: %w", err) 104 | } 105 | 106 | return token, nil 107 | } 108 | 109 | func signIdentity( 110 | parentPrivateKey ed25519.PrivateKey, 111 | parentAuthority *api.GovalSigningAuthority, 112 | identity *api.GovalReplIdentity, 113 | ) (string, error) { 114 | encodedIdentity, err := proto.Marshal(identity) 115 | if err != nil { 116 | return "", fmt.Errorf("failed to serialize the identity: %w", err) 117 | } 118 | 119 | serializedCert, err := proto.Marshal(parentAuthority) 120 | 121 | if err != nil { 122 | return "", fmt.Errorf("failed to serialize the cert: %w", err) 123 | } 124 | 125 | return paseto.NewV2().Sign( 126 | parentPrivateKey, 127 | base64.StdEncoding.EncodeToString(encodedIdentity), 128 | base64.StdEncoding.EncodeToString(serializedCert), 129 | ) 130 | } 131 | -------------------------------------------------------------------------------- /deps.bzl: -------------------------------------------------------------------------------- 1 | load("@bazel_gazelle//:deps.bzl", "go_repository") 2 | 3 | def go_dependencies(): 4 | go_repository( 5 | name = "com_github_aead_chacha20", 6 | importpath = "github.com/aead/chacha20", 7 | sum = "h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY=", 8 | version = "v0.0.0-20180709150244-8b13a72661da", 9 | ) 10 | go_repository( 11 | name = "com_github_aead_chacha20poly1305", 12 | importpath = "github.com/aead/chacha20poly1305", 13 | sum = "h1:6Z/wqhPFZ7y5ksCEV/V5MXOazLaeu/EW97CU5rz8NWk=", 14 | version = "v0.0.0-20170617001512-233f39982aeb", 15 | ) 16 | go_repository( 17 | name = "com_github_aead_poly1305", 18 | importpath = "github.com/aead/poly1305", 19 | sum = "h1:52m0LGchQBBVqJRyYYufQuIbVqRawmubW3OFGqK1ekw=", 20 | version = "v0.0.0-20180717145839-3fee0db0b635", 21 | ) 22 | go_repository( 23 | name = "com_github_davecgh_go_spew", 24 | importpath = "github.com/davecgh/go-spew", 25 | sum = "h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=", 26 | version = "v1.1.1", 27 | ) 28 | go_repository( 29 | name = "com_github_golang_protobuf", 30 | importpath = "github.com/golang/protobuf", 31 | sum = "h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=", 32 | version = "v1.5.2", 33 | ) 34 | go_repository( 35 | name = "com_github_google_go_cmp", 36 | importpath = "github.com/google/go-cmp", 37 | sum = "h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=", 38 | version = "v0.5.5", 39 | ) 40 | go_repository( 41 | name = "com_github_o1egl_paseto", 42 | importpath = "github.com/o1egl/paseto", 43 | sum = "h1:bwpvPu2au176w4IBlhbyUv/S5VPptERIA99Oap5qUd0=", 44 | version = "v1.0.0", 45 | ) 46 | go_repository( 47 | name = "com_github_pkg_errors", 48 | importpath = "github.com/pkg/errors", 49 | sum = "h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=", 50 | version = "v0.8.0", 51 | ) 52 | go_repository( 53 | name = "com_github_pmezard_go_difflib", 54 | importpath = "github.com/pmezard/go-difflib", 55 | sum = "h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=", 56 | version = "v1.0.0", 57 | ) 58 | go_repository( 59 | name = "com_github_stretchr_objx", 60 | importpath = "github.com/stretchr/objx", 61 | sum = "h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4=", 62 | version = "v0.4.0", 63 | ) 64 | go_repository( 65 | name = "com_github_stretchr_testify", 66 | importpath = "github.com/stretchr/testify", 67 | sum = "h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=", 68 | version = "v1.8.0", 69 | ) 70 | go_repository( 71 | name = "in_gopkg_check_v1", 72 | importpath = "gopkg.in/check.v1", 73 | sum = "h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=", 74 | version = "v0.0.0-20161208181325-20d25e280405", 75 | ) 76 | go_repository( 77 | name = "in_gopkg_yaml_v3", 78 | importpath = "gopkg.in/yaml.v3", 79 | sum = "h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=", 80 | version = "v3.0.1", 81 | ) 82 | go_repository( 83 | name = "org_golang_google_protobuf", 84 | importpath = "google.golang.org/protobuf", 85 | sum = "h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=", 86 | version = "v1.28.0", 87 | ) 88 | go_repository( 89 | name = "org_golang_x_crypto", 90 | importpath = "golang.org/x/crypto", 91 | sum = "h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY=", 92 | version = "v0.0.0-20220622213112-05595931fe9d", 93 | ) 94 | go_repository( 95 | name = "org_golang_x_net", 96 | importpath = "golang.org/x/net", 97 | sum = "h1:CIJ76btIcR3eFI5EgSo6k1qKw9KJexJuRLI9G7Hp5wE=", 98 | version = "v0.0.0-20211112202133-69e39bad7dc2", 99 | ) 100 | go_repository( 101 | name = "org_golang_x_sys", 102 | importpath = "golang.org/x/sys", 103 | sum = "h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4=", 104 | version = "v0.0.0-20210615035016-665e8c7367d1", 105 | ) 106 | go_repository( 107 | name = "org_golang_x_term", 108 | importpath = "golang.org/x/term", 109 | sum = "h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=", 110 | version = "v0.0.0-20201126162022-7de9c90e9dd1", 111 | ) 112 | go_repository( 113 | name = "org_golang_x_text", 114 | importpath = "golang.org/x/text", 115 | sum = "h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=", 116 | version = "v0.3.6", 117 | ) 118 | go_repository( 119 | name = "org_golang_x_xerrors", 120 | importpath = "golang.org/x/xerrors", 121 | sum = "h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=", 122 | version = "v0.0.0-20191204190536-9bdfabe68543", 123 | ) 124 | -------------------------------------------------------------------------------- /paserk/paserk.go: -------------------------------------------------------------------------------- 1 | // Package paserk contains implementations of 2 | // [PASERK](https://github.com/paseto-standard/paserk), an extension to PASETO 3 | // that allows for key sharing. These are not critical security-sensitive, so 4 | // it's fine-ish to implement ourselves to avoid having to add one more 5 | // dependency. 6 | package paserk 7 | 8 | import ( 9 | "encoding/base64" 10 | "fmt" 11 | "strings" 12 | 13 | "golang.org/x/crypto/blake2b" 14 | "golang.org/x/crypto/ed25519" 15 | "google.golang.org/protobuf/proto" 16 | 17 | "github.com/replit/go-replidentity/protos/external/goval/api" 18 | ) 19 | 20 | const ( 21 | // PaserkPublicHeader is the header of a PASERK public key: 22 | // https://github.com/paseto-standard/paserk/blob/master/types/public.md 23 | PaserkPublicHeader = "k2.public." 24 | 25 | // PaserkSecretHeader is the header of a PASERK secret key: 26 | // https://github.com/paseto-standard/paserk/blob/master/types/secret.md 27 | PaserkSecretHeader = "k2.secret." 28 | 29 | // PaserkSIDHeader is the header of a PASERK sid: 30 | // https://github.com/paseto-standard/paserk/blob/master/types/sid.md 31 | PaserkSIDHeader = "k2.sid." 32 | 33 | // PaserkPIDHeader is the header of a PASERK pid: 34 | // https://github.com/paseto-standard/paserk/blob/master/types/sid.md 35 | PaserkPIDHeader = "k2.pid." 36 | 37 | // PaserkGSAIDHeader is the header of a PASERK [api.GovalSigningAuthority] id. This 38 | // is a replit extension to PASERK. 39 | PaserkGSAIDHeader = "k2.gsaid." 40 | 41 | // paserkPublicLength is the expected length of a PASERK Public. 42 | paserkPublicLength = 53 43 | 44 | // paserkSecretLength is the expected length of a PASERK Secret. 45 | paserkSecretLength = 96 46 | ) 47 | 48 | // PASERKPublic is the serialized version of an [ed25519.PublicKey]: 49 | // https://github.com/paseto-standard/paserk/blob/master/types/public.md 50 | type PASERKPublic string 51 | 52 | // PASERKSecret is the serialized version of an [ed25519.PrivateKey]: 53 | // https://github.com/paseto-standard/paserk/blob/master/types/secret.md 54 | type PASERKSecret string 55 | 56 | // PublicKeyToPASERKPublic wraps an [ed25519.PublicKey] into its PASERK representation. 57 | func PublicKeyToPASERKPublic(pubkey ed25519.PublicKey) PASERKPublic { 58 | return PASERKPublic(PaserkPublicHeader + base64.RawURLEncoding.EncodeToString(pubkey)) 59 | } 60 | 61 | // PASERKPublicToPublicKey unwraps an [ed25519.PublicKey] from its PASERK representation. 62 | func PASERKPublicToPublicKey(encoded PASERKPublic) (ed25519.PublicKey, error) { 63 | if !strings.HasPrefix(string(encoded), PaserkPublicHeader) { 64 | return nil, fmt.Errorf("%q does not have the %q header", encoded, PaserkPublicHeader) 65 | } 66 | if len(encoded) != paserkPublicLength { 67 | return nil, fmt.Errorf("%q is not the expected length of %d", encoded, paserkPublicLength) 68 | } 69 | rawKeyData, err := base64.RawURLEncoding.DecodeString(strings.TrimPrefix(string(encoded), PaserkPublicHeader)) 70 | if err != nil { 71 | return nil, err 72 | } 73 | return ed25519.PublicKey(rawKeyData), nil 74 | } 75 | 76 | // PrivateKeyToPASERKSecret wraps an [ed25519.PrivateKey] into its PASERK representation. 77 | func PrivateKeyToPASERKSecret(privkey ed25519.PrivateKey) PASERKSecret { 78 | return PASERKSecret(PaserkSecretHeader + base64.RawURLEncoding.EncodeToString(privkey)) 79 | } 80 | 81 | // PASERKSecretToPrivateKey unwraps an [ed25519.PrivateKey] from its PASERK representation. 82 | func PASERKSecretToPrivateKey(encoded PASERKSecret) (ed25519.PrivateKey, error) { 83 | if !strings.HasPrefix(string(encoded), PaserkSecretHeader) { 84 | return nil, fmt.Errorf("%q does not have the %q header", encoded, PaserkSecretHeader) 85 | } 86 | if len(encoded) != paserkSecretLength { 87 | return nil, fmt.Errorf("%q is not the expected length of %d", encoded, paserkSecretLength) 88 | } 89 | rawKeyData, err := base64.RawURLEncoding.DecodeString(strings.TrimPrefix(string(encoded), PaserkSecretHeader)) 90 | if err != nil { 91 | return nil, err 92 | } 93 | return ed25519.PrivateKey(rawKeyData), nil 94 | } 95 | 96 | // paserkID implements the PASERK ID operation: 97 | // https://github.com/paseto-standard/paserk/blob/master/operations/ID.md 98 | func paserkID(header, data string) string { 99 | h, err := blake2b.New(33, nil) 100 | if err != nil { 101 | panic(err) 102 | } 103 | h.Write([]byte(header)) 104 | h.Write([]byte(data)) 105 | return header + base64.RawURLEncoding.EncodeToString(h.Sum(nil)) 106 | } 107 | 108 | // PaserkPID returns the PASERK ID of an [ed25519.PublicKey]: 109 | // https://github.com/paseto-standard/paserk/blob/master/types/pid.md 110 | func PaserkPID(pubkey ed25519.PublicKey) string { 111 | return paserkID(PaserkPIDHeader, string(PublicKeyToPASERKPublic(pubkey))) 112 | } 113 | 114 | // PaserkSID returns the PASERK ID of an [ed25519.PrivateKey]: 115 | // https://github.com/paseto-standard/paserk/blob/master/types/sid.md 116 | func PaserkSID(privkey ed25519.PrivateKey) string { 117 | return paserkID(PaserkSIDHeader, string(PrivateKeyToPASERKSecret(privkey))) 118 | } 119 | 120 | // PaserkGSAID returns the PASERK ID of a [api.GovalSigningAuthority]. This is a Replit 121 | // extension to PASERK. 122 | func PaserkGSAID(authority *api.GovalSigningAuthority) string { 123 | serializedCertProto, err := proto.Marshal(authority) 124 | if err != nil { 125 | return "" 126 | } 127 | certSerialized := base64.StdEncoding.EncodeToString(serializedCertProto) 128 | return paserkID(PaserkGSAIDHeader, certSerialized) 129 | } 130 | -------------------------------------------------------------------------------- /protos/external/goval/api/client.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | import "google/protobuf/timestamp.proto"; 4 | 5 | package api; 6 | option go_package = "github.com/replit/go-replidentity/protos/external/goval/api"; 7 | 8 | // This message constitutes the repl metadata and define the repl we're 9 | // connecting to. All fields are required unless otherwise stated. 10 | message Repl { 11 | string id = 1; 12 | string language = 2; 13 | string bucket = 3; 14 | string slug = 4; 15 | string user = 5; 16 | 17 | // (Optional) The replID of a repl to be used as the source filesystem. All 18 | // writes will still go to the actual repl. This is intended to be a 19 | // replacement for guest repls, giving us cheap COW semantics so all 20 | // connections can have a real repl. 21 | // 22 | // One exception: 23 | // 24 | // It's important to note that data is not implicitly copied from src to 25 | // dest. Only what is explicitly written when talking to pid1 (either 26 | // gcsfiles or snapshots) will persist. This makes it slightly different 27 | // than just forking. 28 | // 29 | // It's unclear what the behaviour should be if: 30 | // - the dest and src repl both exist 31 | // - the dest and src are the same 32 | // - we have an src but no dest 33 | // consider these unsupported/undefined for now. 34 | string sourceRepl = 6; 35 | } 36 | 37 | // The resource limits that should be applied to the Repl's container. 38 | message ResourceLimits { 39 | // Whether the repl has network access. 40 | bool net = 1; 41 | 42 | // The amount of RAM in bytes that this repl will have. 43 | int64 memory = 2; 44 | 45 | // The number of cores that the container will be allowed to have. 46 | double threads = 3; 47 | 48 | // The Docker container weight factor for the scheduler. Similar to the 49 | // `--cpu-shares` commandline flag. 50 | double shares = 4; 51 | 52 | // The size of the disk in bytes. 53 | int64 disk = 5; 54 | 55 | // Whether these limits are cachable, and if they are, by what facet of the token. 56 | enum Cachability { 57 | // Do not cache these limits. 58 | NONE = 0; 59 | 60 | // These limits can be cached and applied to this and any of the user's 61 | // other repls. 62 | USER = 1; 63 | 64 | // These limits can be cached and applied only to this repl. 65 | REPL = 2; 66 | } 67 | 68 | Cachability cache = 6; 69 | 70 | // If set, apply a restrictive allowlist-based network policy to the container 71 | // The container will only be able to communicate with the minimum domains 72 | // necessary to make Replit work, such as package managers. 73 | bool restrictNetwork = 7; 74 | } 75 | 76 | // Permissions allow tokens to perform certain actions. 77 | message Permissions { 78 | // This token has permission to toggle the always on state of a container. 79 | // For a connection to send the AlwaysOn message, it must have this permission. 80 | bool toggleAlwaysOn = 1; 81 | } 82 | 83 | // ReplToken is the expected client options during the handshake. This is encoded 84 | // into the token that is used to connect using WebSocket. 85 | message ReplToken { 86 | // Issue timestamp. Equivalent to JWT's "iat" (Issued At) claim. Tokens with 87 | // no `iat` field will be treated as if they had been issed at the UNIX epoch 88 | // (1970-01-01T00:00:00Z). 89 | google.protobuf.Timestamp iat = 1; 90 | 91 | // Expiration timestamp. Equivalent to JWT's "exp" (Expiration Time) Claim. 92 | // If unset, will default to one hour after `iat`. 93 | google.protobuf.Timestamp exp = 2; 94 | 95 | // An arbitrary string that helps prevent replay attacks by ensuring that all 96 | // tokens are distinct. 97 | string salt = 3; 98 | 99 | // The cluster that a repl is located in. This prevents replay attacks in 100 | // which a user is given a token for one cluster and then presents that same 101 | // token to a conman instance in another token, which could lead to a case 102 | // where multiple containers are associated with a repl. 103 | // 104 | // Conman therefore needs to validate that this parameter matches the 105 | // `-cluster` flag it was started with. 106 | string cluster = 4; 107 | 108 | // Whether to persist filesystem, metadata, or both. 109 | enum Persistence { 110 | // This is the usual mode of operation: both filesystem and metadata will be 111 | // persisted. 112 | PERSISTENT = 0; 113 | 114 | // The ephemeral flag indicates the repl being connected to will have a time 115 | // restriction on stored metadata. This has the consequence that repl will 116 | // be unable to wakeup or serve static traffic once the metadata has timed 117 | // out. This option does NOT affect filesystem and other data persistence. 118 | // 119 | // For context, this value is used on the client when repls are created for: 120 | // - replrun 121 | // - guests 122 | // - anon users 123 | // - temp vnc repls 124 | // - users with non-verified emails 125 | EPHEMERAL = 1; 126 | 127 | // This indicates that the repl being connected does not have the ability to 128 | // persist files or be woken up after the lifetime of this repl expires. 129 | // 130 | // For context, this value is used on the client when repls are created for: 131 | // - replrun 132 | // - guests 133 | // - language pages 134 | NONE = 2; 135 | } 136 | // Whether to persist filesystem, metadata, or both. When connecting to an 137 | // already running/existing repl, its settings will be updated to match this 138 | // mode. 139 | Persistence persistence = 6; 140 | 141 | // Metadata for the classroom. This is deprecated and should be removed 142 | // hopefully soon. 143 | message ClassroomMetadata { 144 | string id = 1; 145 | string language = 2; 146 | } 147 | 148 | // Metadata for a repl that is only identified by its id. 149 | message ReplID { 150 | string id = 1; 151 | 152 | // (Optional) See the comment for Repl.sourceRepl. 153 | string sourceRepl = 2; 154 | } 155 | 156 | // One of the three ways to identify a repl in goval. 157 | oneof metadata { 158 | // This is the standard connection behavior. If the repl doesn't exist it 159 | // will be created. Any future connections with a matching ID will go to 160 | // the same container. If other metadata mismatches besides ID it will be 161 | // rectified (typically by recreating the container to make it match the 162 | // provided value). 163 | Repl repl = 7; 164 | 165 | // The repl must already be known to goval, the connection will proceed 166 | // with the Repl metadata from a previous connection's metadata with the 167 | // same ID. 168 | ReplID id = 8; 169 | 170 | // This is DEPRECATED and only used by the classroom. This will never share 171 | // a container between connections. Please don't use this even for tests, 172 | // we intend to remove it soon. 173 | ClassroomMetadata classroom = 9 [deprecated=true]; 174 | } 175 | 176 | // The resource limits for the container. 177 | ResourceLimits resourceLimits = 10; 178 | 179 | // allows the client to choose a wire format. 180 | enum WireFormat { 181 | // The default wire format: Protobuf-over-WebSocket. 182 | PROTOBUF = 0; 183 | 184 | // Legacy protocol. 185 | JSON = 1 [deprecated=true]; 186 | } 187 | WireFormat format = 12; 188 | 189 | message Presenced { 190 | uint32 bearerID = 1; 191 | string bearerName = 2; 192 | } 193 | Presenced presenced = 13; 194 | 195 | // Flags are handy for passing arbitrary configs along. Mostly used so 196 | // the client can try out new features 197 | repeated string flags = 14; 198 | 199 | Permissions permissions = 15; 200 | } 201 | 202 | // TLSCertificate is a SSL/TLS certificate for a specific domain. This is used to transfer 203 | // certificates between clusters when a repl is transferred so we do not need to request 204 | // a new certificate in the new cluster. 205 | message TLSCertificate { 206 | string domain = 1; 207 | bytes cert = 2; 208 | } 209 | 210 | // ReplTransfer includes all the data needed to transfer a repl between clusters. 211 | message ReplTransfer { 212 | Repl repl = 1; 213 | ResourceLimits replLimits = 2; 214 | ResourceLimits userLimits = 3; 215 | repeated string customDomains = 4; 216 | repeated TLSCertificate certificates = 5; 217 | repeated string flags = 6; 218 | } 219 | 220 | // AllowReplRequest represents a request to allow a repl into a cluster. 221 | message AllowReplRequest { 222 | ReplTransfer replTransfer = 1; 223 | } 224 | 225 | // ClusterMetadata represents all the metadata Lore knows about a cluster. This 226 | // includes all endpoints needed to communicate with the cluster. 227 | message ClusterMetadata { 228 | reserved 4, 6; 229 | 230 | string id = 1; 231 | string conmanURL = 2; 232 | string gurl = 3; 233 | string proxy = 5; 234 | } 235 | 236 | // EvictReplRequest represents a request to evict a repl from a cluster. 237 | // Includes the metadata about the repl that will be evicted and a token in 238 | // case conman needs to forward this request to another instance. 239 | message EvictReplRequest { 240 | ClusterMetadata clusterMetadata = 1; 241 | string token = 2; 242 | 243 | // User and slug are sent so that a repl route can be added even if the cluster 244 | // doesn't have metadata for this repl. 245 | string user = 3; 246 | string slug = 4; 247 | } 248 | 249 | // EvictReplResponse represents a response after evicting a repl from a cluster and includes 250 | // metadata about the repl that was evicted. 251 | message EvictReplResponse { 252 | ReplTransfer replTransfer = 1; 253 | } 254 | -------------------------------------------------------------------------------- /protos/external/goval/api/signing.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | import "google/protobuf/timestamp.proto"; 4 | 5 | 6 | import "protos/external/goval/api/client.proto"; 7 | 8 | package api; 9 | option go_package = "github.com/replit/go-replidentity/protos/external/goval/api"; 10 | 11 | enum TokenVersion { 12 | // Body contains are bare ReplToken and must be decoded explicitly 13 | BARE_REPL_TOKEN = 0; 14 | // Body contains a GovalToken and can be interrogated about the type of its 15 | // own message 16 | TYPE_AWARE_TOKEN = 1; 17 | } 18 | 19 | // GovalSigningAuthority is information about a goval token, that can be used to 20 | // validate it. It is stored in the footer of the PASETO. 21 | message GovalSigningAuthority { 22 | oneof cert { 23 | // The ID of the root public key that was used to sign the token. 24 | string key_id = 1; 25 | 26 | // A signed PASETO with a GovalCert in the body and the 27 | // GovalSigningAuthority used to sign the body in the footer. 28 | string signed_cert = 2; 29 | } 30 | 31 | // An enum detailing how the body of the PASETO this is a footer of should 32 | // be decoded 33 | TokenVersion version = 3; 34 | 35 | // A string containing the issuer of a token. This is used to track who is 36 | // sending tokens with a particular key id, so that we can rotate safely. 37 | string issuer = 4; 38 | } 39 | 40 | enum FlagClaim { 41 | // Cert has the authority to sign ReplToken messages that can be validated 42 | // by goval 43 | MINT_GOVAL_TOKEN = 0; 44 | 45 | // Cert has the authority to sign additional intermediate certs. (The claims 46 | // on intermediate certs signed by this cert are still enforced.) 47 | SIGN_INTERMEDIATE_CERT = 1; 48 | 49 | // Cert has the authority to sign GovalToken messages that can prove identity. 50 | IDENTITY = 5; 51 | 52 | // Cert has the authority to sign GovalToken messages that authorizes the 53 | // bearer to use Ghostwriter. 54 | GHOSTWRITER = 6; 55 | 56 | // Cert has ability to mint Repl Identity tokens 57 | RENEW_IDENTITY = 7; 58 | // Cert has abilit to mint Repl KV tokens 59 | RENEW_KV = 8; 60 | 61 | // Cert has the authority to sign ReplToken messages that claim to come from 62 | // Deployments. If this claim is not set, the cert will only be able to emit 63 | // tokens only for interactive Repls. 64 | DEPLOYMENTS = 10; 65 | 66 | // Cert has the authority to sign ReplToken messages for any ReplID. If this 67 | // claim is not set, the cert will only be able to emit tokens only for the 68 | // list explicitly enumerated by the other claims. If that list is empty, the 69 | // cert has no ability to sign any tokens. 70 | ANY_REPLID = 2; 71 | 72 | // Cert has the authority to sign ReplToken messages for any user. If this 73 | // claim is not set, the cert will only be able to emit tokens only for the 74 | // list explicitly enumerated by the other claims. If that list is empty, the 75 | // cert has no ability to sign any tokens. 76 | ANY_USER = 3; 77 | 78 | // Cert has the authority to sign ReplToken messages for any user id. If this 79 | // claim is not set, the cert will only be able to emit tokens only for the 80 | // list explicitly enumerated by the other claims. If that list is empty, the 81 | // cert has no ability to sign any tokens that have a user id. 82 | ANY_USER_ID = 11; 83 | 84 | // Cert has the authority to sign ReplToken messages for any org. If this 85 | // claim is not set, the cert will only be able to emit tokens only for the 86 | // list explicitly enumerated by the other claims. If that list is empty, the 87 | // cert has no ability to sign any tokens that have an org. 88 | ANY_ORG = 12; 89 | 90 | // Cert has the authority to sign ReplToken messages for any cluster. If this 91 | // claim is not set, the cert will only be able to emit tokens only for the 92 | // list explicitly enumerated by the other claims. If that list is empty, the 93 | // cert has no ability to sign any tokens. 94 | ANY_CLUSTER = 4; 95 | 96 | // Cert has the authority to sign ReplToken messages for any subcluster. If 97 | // this claim is not set, the cert will only be able to emit tokens only for 98 | // the list explicitly enumerated by the other claims. If that list is empty, 99 | // the cert has no ability to sign any tokens that have a subcluster. 100 | ANY_SUBCLUSTER = 9; 101 | } 102 | 103 | // Claims are actions that a cert is allowed to do. Claims can be repeated (e.g. 104 | // to allow a cert to apply to multiple replids or users). 105 | // 106 | // Claims should be enforced on certificates by ensuring that certificates 107 | // are signed by a certificate that has a superset of claims. 108 | // 109 | // When a cert is used to sign a message, it is the responsibility of the 110 | // service validating the message to ensure that any requests in the message are 111 | // backed up by claims in the certificate. Claims in a single certificate should 112 | // be interpreted as a union (e.g. if replid and user is set, the token may 113 | // apply to any repls owned by the user, or any repls in replid, regardless of 114 | // the owner). 115 | message CertificateClaim { 116 | oneof claim { 117 | // This cert has the authority to sign messages on behalf of a replid 118 | string replid = 1; 119 | // This cert has the authority to sign messages on behalf of a user 120 | string user = 2; 121 | // This cert has the authority to sign messages on behalf of a user id 122 | int64 user_id = 7; 123 | // This cert has the authority to sign messages on behalf of an org 124 | Org org = 8; 125 | // This cert has the authority to sign messages in a certain cluster 126 | string cluster = 4; 127 | // This cert has the authority to sign messages in a certain subcluster 128 | string subcluster = 5; 129 | // This cert has the authority to sign messages that claim to come from a 130 | // deployment. 131 | bool deployment = 6; 132 | // This cert has the authority to perform an action as described in 133 | // FlagClaim 134 | FlagClaim flag = 3; 135 | } 136 | } 137 | 138 | // GovalCert provides a mechanism of establishing a chain of trust without 139 | // requiring a single private key to be duplciated to all services that send 140 | // messages. The processes of generating intermediate certs is as follows: 141 | // - A PASETO `v2.public` root keypair is generated and added to GSM with an 142 | // arbitrary key id. 143 | // - The root public key id is encoded in a GovalSigningAuthority 144 | // - An intermediate PASETO `v2.public` keypair is generated 145 | // - The intermediate public key is encoded in a GovalCert, along with 146 | // information about the lifetime and claims of that cert. 147 | // - The GovalCert is encoded in the body of a PASETO and signed with the root 148 | // private key. The root signing authority is inserted into the footer of the 149 | // PASETO to use for validation. 150 | // - This signed PASETO is encoded in another GovalSigningAuthority and appended 151 | // as the footer of PASETOs signed by the intermediate private key. 152 | // Additional intermediate certs can be generated and signed by private key and 153 | // signing authority of the previous cert. 154 | // 155 | // When validating a chain of certs, the footer of each wrapped PASETO is 156 | // recursed until reaching a root key id. The body of that PASETO is 157 | // validated with the root public key. The body is decoded into a GovalCert, 158 | // its lifetime is checked, and the public key is pulled out and used to 159 | // validate the next PASETO, continuing back up the chain. At each step along 160 | // the chain (except for the root), the claims of a certificate must be verified 161 | // to be a subset of the claims of the certificate signing it. 162 | message GovalCert { 163 | // Issue timestamp. Equivalent to JWT's "iat" (Issued At) claim. Tokens with 164 | // no `iat` field will be treated as if they had been issed at the UNIX epoch 165 | // (1970-01-01T00:00:00Z). 166 | google.protobuf.Timestamp iat = 1; 167 | 168 | // Expiration timestamp. Equivalent to JWT's "exp" (Expiration Time) Claim. 169 | // If unset, will default to one hour after `iat`. 170 | google.protobuf.Timestamp exp = 2; 171 | 172 | // A list of claims this cert can authorize 173 | repeated CertificateClaim claims = 3; 174 | 175 | // The PASETO `v2.public` (Ed25519) public key authorized to sign requests in 176 | // this scope. Must be encoded in either PASERK SID or a PEM PUBLIC KEY 177 | // block. (This key is usally generated in nodejs, and nodejs does not 178 | // provide an interface to get the raw key bytes) 179 | string publicKey = 4; 180 | } 181 | 182 | // A GovalToken should be the body of any PASETO we send 183 | message GovalToken { 184 | // Issue timestamp. Equivalent to JWT's "iat" (Issued At) claim. Tokens with 185 | // no `iat` field will be treated as if they had been issed at the UNIX epoch 186 | // (1970-01-01T00:00:00Z). 187 | google.protobuf.Timestamp iat = 1; 188 | 189 | // Expiration timestamp. Equivalent to JWT's "exp" (Expiration Time) Claim. 190 | // If unset, will default to one hour after `iat`. 191 | google.protobuf.Timestamp exp = 2; 192 | 193 | // Tokens are only allowed to act for a single repl, replid is the repl that 194 | // this token is authorized for. The validator must check that the replid of 195 | // this token agrees with the claims in any of the certs signing it. 196 | string replid = 3; 197 | 198 | // The token body, all future tokens should rely on the information in 199 | // GovalToken to establish basic validity, and should only add additional 200 | // fields. ReplToken has its own iat, exp, and replid for legacy reasons. 201 | oneof Token { 202 | // This token is used to authorize a request to create a repl in goval 203 | ReplToken repl_token = 4; 204 | 205 | // This token is used to prove a Repl's identity. 206 | GovalReplIdentity repl_identity = 5; 207 | } 208 | } 209 | 210 | // A GovalReplIdentity is used in identity PASETO tokens which are used for 211 | // authentication between repls. 212 | message GovalReplIdentity { 213 | // This identity has this Repl ID 214 | string replid = 1; 215 | // This identity is in the context of this user 216 | string user = 2; 217 | // This repl has this slug 218 | string slug = 3; 219 | // If set, this token can only be consumed by this a Repl with this Repl ID. 220 | // Equivalent to JWT's "aud" (Audience) claim. 221 | string aud = 4; 222 | // If true, this token is generated in an ephemeral environment (such as 223 | // a guest fork). Systems can use this to potentially reject ephemeral tokens 224 | // if that makes sense for their API. 225 | bool ephemeral = 5; 226 | // This identity is forked from this Repl ID. 227 | // This is set for "guest forks", where server(s) might need to know the 228 | // original repl's ID despite the running environment being a fork. 229 | string originReplid = 6; 230 | // same as the `user` field, but it's the ID instead of the username 231 | int64 user_id = 7; 232 | // If this is a build repl for a hosting deployment, include extra 233 | // information about the specs of the build 234 | BuildInfo build_info = 8; 235 | // A boolean indicating if the owner of the repl is a team. 236 | bool is_team = 9; 237 | // A list of roles for the user who owns the repl. 238 | repeated string roles = 10; 239 | // Runtime information about the Repl. 240 | oneof runtime { 241 | // This is set if the Repl is running interactively. This is not set when 242 | // the Repl is running in hosting. 243 | ReplRuntimeInteractive interactive = 11; 244 | // This is set if the Repl is running in a hosting subcluster. 245 | ReplRuntimeHosting hosting = 13; 246 | // This is set if the Repl is running in a Deployment. 247 | ReplRuntimeDeployment deployment = 12; 248 | } 249 | // The organization that owns the Repl 250 | Org org = 14; 251 | } 252 | 253 | // BuildInfo includes information about which deployment this repl is allowed to 254 | // create or update. 255 | message BuildInfo { 256 | // ID is a unique identitifier for the deployment that this builder repl is 257 | // allowed to push to. 258 | string deployment_id = 1; 259 | 260 | // URL is the replit.app URL that will be used for the deployment. 261 | string url = 2; 262 | 263 | // Build ID is a unique identifier for this particular deployment build 264 | string build_id = 3; 265 | 266 | // Tier refers to the GCE machine tier that will be used for the build 267 | string machine_tier = 4; 268 | } 269 | 270 | message ReplRuntimeInteractive { 271 | // The cluster in which this Repl is running. 272 | string cluster = 1; 273 | // The subcluster in which this Repl is running. 274 | string subcluster = 2; 275 | } 276 | 277 | message ReplRuntimeHosting { 278 | // The cluster in which this Repl is running. 279 | string cluster = 1; 280 | // The subcluster in which this Repl is running. 281 | string subcluster = 2; 282 | } 283 | 284 | message ReplRuntimeDeployment {} 285 | 286 | // Org contains information about the org to which a Repl belongs. 287 | message Org { 288 | // Organization type. There are legacy types, but we are not 289 | // supporting them, and they should not be getting passed. 290 | enum OrgType { 291 | TYPE_UNSPECIFIED = 0; 292 | PERSONAL = 1; 293 | TEAM = 2; 294 | } 295 | 296 | string id = 1; 297 | OrgType type = 2; 298 | } 299 | -------------------------------------------------------------------------------- /verify.go: -------------------------------------------------------------------------------- 1 | package replidentity 2 | 3 | import ( 4 | "encoding/base64" 5 | "errors" 6 | "fmt" 7 | "strings" 8 | "time" 9 | 10 | "github.com/o1egl/paseto" 11 | "golang.org/x/crypto/ed25519" 12 | "google.golang.org/protobuf/encoding/protojson" 13 | "google.golang.org/protobuf/proto" 14 | 15 | "github.com/replit/go-replidentity/paserk" 16 | "github.com/replit/go-replidentity/protos/external/goval/api" 17 | ) 18 | 19 | type verifier struct { 20 | claims *MessageClaims 21 | 22 | // signing certs can allow "any *" variants 23 | anyReplid bool 24 | anyUser bool 25 | anyUserID bool 26 | anyOrg bool 27 | anyCluster bool 28 | anySubcluster bool 29 | deployments bool 30 | } 31 | 32 | func (v *verifier) verifyToken(token string, pubkey ed25519.PublicKey) ([]byte, error) { 33 | if len(pubkey) == 0 { 34 | return nil, errors.New("pubkey is empty") 35 | } 36 | 37 | var meta string 38 | 39 | err := paseto.NewV2().Verify(token, pubkey, &meta, nil) 40 | if err != nil { 41 | return nil, fmt.Errorf("failed to verify token with public key: %w", err) 42 | } 43 | 44 | bytes, err := base64.StdEncoding.DecodeString(meta) 45 | if err != nil { 46 | return nil, fmt.Errorf("failed to decode token: %w", err) 47 | } 48 | 49 | return bytes, nil 50 | } 51 | 52 | func (v *verifier) verifyTokenWithKeyID(token string, keyid string, issuer string, getPubKey PubKeySource) ([]byte, error) { 53 | pubkey, err := getPubKey(keyid, issuer) 54 | if err != nil { 55 | return nil, fmt.Errorf("failed to get pubkey %s: %w", keyid, err) 56 | } 57 | 58 | return v.verifyToken(token, pubkey) 59 | } 60 | 61 | func (v *verifier) verifyTokenWithCert(token string, cert *api.GovalCert) ([]byte, error) { 62 | var pubkey ed25519.PublicKey 63 | var err error 64 | 65 | if strings.HasPrefix(cert.PublicKey, paserk.PaserkPublicHeader) { 66 | pubkey, err = paserk.PASERKPublicToPublicKey(paserk.PASERKPublic(cert.PublicKey)) 67 | } else { 68 | pubkey, err = pemToPubkey(cert.PublicKey) 69 | } 70 | if err != nil { 71 | return nil, fmt.Errorf("failed to decode public key: %w", err) 72 | } 73 | 74 | return v.verifyToken(token, pubkey) 75 | } 76 | 77 | func (v *verifier) verifyCert(certBytes []byte, signingCert *api.GovalCert) (*api.GovalCert, error) { 78 | var cert api.GovalCert 79 | err := proto.Unmarshal(certBytes, &cert) 80 | if err != nil { 81 | return nil, fmt.Errorf("failed to unmarshal cert: %w", err) 82 | } 83 | 84 | // Verify that the cert is valid 85 | err = verifyClaims(cert.Iat.AsTime(), cert.Exp.AsTime(), "", "", "", "", "", 0, false, nil) 86 | if err != nil { 87 | return nil, fmt.Errorf("cert is not valid: %w", err) 88 | } 89 | 90 | // If the parent cert is not the root cert 91 | if signingCert != nil { 92 | claims := parseClaims(signingCert) 93 | if _, ok := claims.Flags[api.FlagClaim_SIGN_INTERMEDIATE_CERT]; !ok { 94 | return nil, fmt.Errorf("signing cert doesn't have authority to sign intermediate certs") 95 | 96 | } 97 | 98 | // Verify the cert claims agrees with its signer 99 | authorizedClaims := map[string]struct{}{} 100 | var anyReplid, anyUser, anyUserID, anyOrg, anyCluster, anySubcluster, deployments bool 101 | for _, claim := range signingCert.Claims { 102 | authorizedClaims[claim.String()] = struct{}{} 103 | switch tc := claim.Claim.(type) { 104 | case *api.CertificateClaim_Flag: 105 | if tc.Flag == api.FlagClaim_ANY_REPLID { 106 | anyReplid = true 107 | } 108 | if tc.Flag == api.FlagClaim_ANY_USER { 109 | anyUser = true 110 | } 111 | if tc.Flag == api.FlagClaim_ANY_USER_ID { 112 | anyUserID = true 113 | } 114 | if tc.Flag == api.FlagClaim_ANY_ORG { 115 | anyOrg = true 116 | } 117 | if tc.Flag == api.FlagClaim_ANY_CLUSTER { 118 | anyCluster = true 119 | } 120 | if tc.Flag == api.FlagClaim_ANY_SUBCLUSTER { 121 | anySubcluster = true 122 | } 123 | if tc.Flag == api.FlagClaim_DEPLOYMENTS { 124 | deployments = true 125 | } 126 | } 127 | } 128 | 129 | for _, claim := range cert.Claims { 130 | switch tc := claim.Claim.(type) { 131 | case *api.CertificateClaim_Flag: 132 | if tc.Flag == api.FlagClaim_ANY_REPLID { 133 | v.anyReplid = true 134 | } 135 | if tc.Flag == api.FlagClaim_ANY_USER { 136 | v.anyUser = true 137 | } 138 | if tc.Flag == api.FlagClaim_ANY_USER_ID { 139 | v.anyUserID = true 140 | } 141 | if tc.Flag == api.FlagClaim_ANY_ORG { 142 | v.anyOrg = true 143 | } 144 | if tc.Flag == api.FlagClaim_ANY_CLUSTER { 145 | v.anyCluster = true 146 | } 147 | if tc.Flag == api.FlagClaim_ANY_SUBCLUSTER { 148 | v.anySubcluster = true 149 | } 150 | if tc.Flag == api.FlagClaim_DEPLOYMENTS { 151 | v.deployments = true 152 | } 153 | case *api.CertificateClaim_Replid: 154 | if anyReplid { 155 | continue 156 | } 157 | case *api.CertificateClaim_User: 158 | if anyUser { 159 | continue 160 | } 161 | case *api.CertificateClaim_UserId: 162 | if anyUserID { 163 | continue 164 | } 165 | case *api.CertificateClaim_Org: 166 | if anyOrg { 167 | continue 168 | } 169 | case *api.CertificateClaim_Cluster: 170 | if anyCluster { 171 | continue 172 | } 173 | case *api.CertificateClaim_Subcluster: 174 | if anySubcluster { 175 | continue 176 | } 177 | case *api.CertificateClaim_Deployment: 178 | if deployments || !tc.Deployment { 179 | continue 180 | } 181 | } 182 | if _, ok := authorizedClaims[claim.String()]; !ok { 183 | return nil, fmt.Errorf("signing cert {%+v} does not authorize claim in {%+v}: %s", signingCert, cert, claim) 184 | } 185 | } 186 | } 187 | 188 | // Store this cert's claims so we can validate tokens later. 189 | certClaims := parseClaims(&cert) 190 | if certClaims != nil { 191 | v.claims = certClaims 192 | } 193 | 194 | return &cert, nil 195 | } 196 | 197 | func (v *verifier) verifyChain(token string, getPubKey PubKeySource) ([]byte, *api.GovalCert, error) { 198 | signingAuthority, err := getSigningAuthority(token) 199 | if err != nil { 200 | return nil, nil, fmt.Errorf("failed to get authority: %w", err) 201 | } 202 | 203 | switch signingAuth := signingAuthority.Cert.(type) { 204 | case *api.GovalSigningAuthority_KeyId: 205 | // If it's signed directly with a root key, grab the pubkey and verify it 206 | verifiedBytes, err := v.verifyTokenWithKeyID(token, signingAuth.KeyId, signingAuthority.Issuer, getPubKey) 207 | if err != nil { 208 | return nil, nil, fmt.Errorf("failed to verify root signiture: %w", err) 209 | } 210 | 211 | return verifiedBytes, nil, nil 212 | 213 | case *api.GovalSigningAuthority_SignedCert: 214 | // If its signed by another token, verify the other token first 215 | signingBytes, skipLevelCert, err := v.verifyChain(signingAuth.SignedCert, getPubKey) 216 | if err != nil { 217 | return nil, nil, fmt.Errorf("failed to verify signing token: %w", err) 218 | } 219 | 220 | // Make sure the two parent certs agree 221 | signingCert, err := v.verifyCert(signingBytes, skipLevelCert) 222 | if err != nil { 223 | return nil, nil, fmt.Errorf("signing cert invalid: %w", err) 224 | } 225 | 226 | // Now verify this token using the parent cert 227 | verifiedBytes, err := v.verifyTokenWithCert(token, signingCert) 228 | if err != nil { 229 | return nil, nil, fmt.Errorf("failed to verify token: %w", err) 230 | } 231 | 232 | return verifiedBytes, signingCert, nil 233 | 234 | default: 235 | return nil, nil, fmt.Errorf("unknown token authority: %s", signingAuth) 236 | } 237 | } 238 | 239 | // easy entry-point so you don't need to create a verifier yourself 240 | func verifyChain(token string, getPubKey PubKeySource) (*verifier, []byte, *api.GovalCert, error) { 241 | v := verifier{} 242 | bytes, cert, err := v.verifyChain(token, getPubKey) 243 | if err != nil { 244 | return nil, nil, nil, err 245 | } 246 | 247 | return &v, bytes, cert, err 248 | } 249 | 250 | // checkClaimsAgainstToken ensures the claims match up with the token. 251 | // This ensures that the final token in the chain is not spoofed via the forwarding protection private key. 252 | func (v *verifier) checkClaimsAgainstToken(token *api.GovalReplIdentity) error { 253 | // if the claims are nil, it means that the token was signed by the root privkey, 254 | // which implicitly has all claims. 255 | if v.claims == nil { 256 | return nil 257 | } 258 | 259 | var cluster, subcluster string 260 | var deployment bool 261 | switch v := token.Runtime.(type) { 262 | case *api.GovalReplIdentity_Deployment: 263 | deployment = true 264 | case *api.GovalReplIdentity_Interactive: 265 | cluster = v.Interactive.Cluster 266 | subcluster = v.Interactive.Subcluster 267 | case *api.GovalReplIdentity_Hosting: 268 | cluster = v.Hosting.Cluster 269 | subcluster = v.Hosting.Subcluster 270 | } 271 | 272 | opts := verifyRawClaimsOpts{ 273 | replid: token.Replid, 274 | user: token.User, 275 | cluster: cluster, 276 | subcluster: subcluster, 277 | deployment: deployment, 278 | claims: v.claims, 279 | anyReplid: v.anyReplid, 280 | anyUser: v.anyUser, 281 | anyCluster: v.anyCluster, 282 | anyOrg: v.anyOrg, 283 | anySubcluster: v.anySubcluster, 284 | allowsDeployment: v.deployments, 285 | } 286 | 287 | if token.Org != nil { 288 | opts.orgId = token.GetOrg().GetId() 289 | opts.orgType = token.GetOrg().GetType() 290 | } 291 | 292 | return verifyRawClaims(opts) 293 | } 294 | 295 | // VerifyOption specifies an additional verification step to be performed on an identity. 296 | type VerifyOption interface { 297 | verify(*api.GovalReplIdentity) error 298 | } 299 | 300 | type funcVerifyOption struct { 301 | f func(identity *api.GovalReplIdentity) error 302 | } 303 | 304 | func (o *funcVerifyOption) verify(identity *api.GovalReplIdentity) error { 305 | return o.f(identity) 306 | } 307 | 308 | // WithVerify allows the caller to specify an arbitrary function to perform 309 | // verification on the identity prior to it being returned. 310 | func WithVerify(f func(identity *api.GovalReplIdentity) error) VerifyOption { 311 | return &funcVerifyOption{ 312 | f: f, 313 | } 314 | } 315 | 316 | // WithSource verifies that the identity's origin replID matches the given 317 | // source, if present. This can be used to enforce specific clients in servers 318 | // when verifying identities. 319 | func WithSource(sourceReplid string) VerifyOption { 320 | return WithVerify(func(identity *api.GovalReplIdentity) error { 321 | if identity.OriginReplid != "" && identity.OriginReplid != sourceReplid { 322 | return fmt.Errorf("identity origin replid does not match. expected %q; got %q", sourceReplid, identity.OriginReplid) 323 | } 324 | return nil 325 | }) 326 | } 327 | 328 | // VerifyIdentity verifies that the given `REPL_IDENTITY` value is in fact 329 | // signed by Goval's chain of authority, and addressed to the provided audience 330 | // (the `REPL_ID` of the recipient). 331 | // 332 | // The optional options allow specifying additional verifications on the identity. 333 | func VerifyIdentity(message string, audience []string, getPubKey PubKeySource, options ...VerifyOption) (*api.GovalReplIdentity, error) { 334 | opts := VerifyTokenOpts{ 335 | Message: message, 336 | Audience: audience, 337 | GetPubKey: getPubKey, 338 | Options: options, 339 | Flags: []api.FlagClaim{api.FlagClaim_IDENTITY}, 340 | } 341 | verified, err := VerifyToken(opts) 342 | if err != nil { 343 | return nil, err 344 | } 345 | return verified.Identity, nil 346 | } 347 | 348 | // VerifyRenewIdentity verifies that the given `REPL_RENEWAL` value is in fact 349 | // signed by Goval's chain of authority, addressed to the provided audience 350 | // (the `REPL_ID` of the recipient), and has the capability to renew the 351 | // identity. 352 | // 353 | // The optional options allow specifying additional verifications on the identity. 354 | func VerifyRenewIdentity(message string, audience []string, getPubKey PubKeySource, options ...VerifyOption) (*api.GovalReplIdentity, error) { 355 | opts := VerifyTokenOpts{ 356 | Message: message, 357 | Audience: audience, 358 | GetPubKey: getPubKey, 359 | Options: options, 360 | Flags: []api.FlagClaim{api.FlagClaim_RENEW_IDENTITY}, 361 | } 362 | verified, err := VerifyToken(opts) 363 | if err != nil { 364 | return nil, err 365 | } 366 | return verified.Identity, nil 367 | } 368 | 369 | type VerifyTokenOpts struct { 370 | Message string 371 | Audience []string 372 | GetPubKey PubKeySource 373 | Options []VerifyOption 374 | Flags []api.FlagClaim 375 | } 376 | 377 | // VerifiedToken is the result of verifying a token. 378 | type VerifiedToken struct { 379 | Identity *api.GovalReplIdentity 380 | SigningAuthority *api.GovalSigningAuthority 381 | Certificate *api.GovalCert 382 | } 383 | 384 | // VerifyToken verifies that the given `REPL_IDENTITY` value is in fact signed 385 | // by Goval's chain of authority, and addressed to the provided audience (the 386 | // `REPL_ID` of the recipient). This is the preferred way of verifying tokens. 387 | // 388 | // The optional options allow specifying additional verifications on the identity. 389 | func VerifyToken(opts VerifyTokenOpts) (*VerifiedToken, error) { 390 | v, bytes, cert, err := verifyChain(opts.Message, opts.GetPubKey) 391 | if err != nil { 392 | return nil, fmt.Errorf("failed verify message: %w", err) 393 | } 394 | 395 | signingAuthority, err := getSigningAuthority(opts.Message) 396 | if err != nil { 397 | return nil, fmt.Errorf("failed to read body type: %w", err) 398 | } 399 | 400 | var identity api.GovalReplIdentity 401 | 402 | switch signingAuthority.GetVersion() { 403 | case api.TokenVersion_BARE_REPL_TOKEN: 404 | return nil, errors.New("wrong type of token provided") 405 | case api.TokenVersion_TYPE_AWARE_TOKEN: 406 | err = proto.Unmarshal(bytes, &identity) 407 | if err != nil { 408 | return nil, fmt.Errorf("failed to decode body: %w", err) 409 | } 410 | } 411 | 412 | var validAudience bool 413 | for _, aud := range opts.Audience { 414 | if aud == identity.Aud { 415 | validAudience = true 416 | break 417 | } 418 | } 419 | if !validAudience { 420 | return nil, fmt.Errorf("message identity mismatch. expected %q, got %q", opts.Audience, identity.Aud) 421 | } 422 | 423 | err = v.checkClaimsAgainstToken(&identity) 424 | if err != nil { 425 | return nil, fmt.Errorf("claim mismatch: %w", err) 426 | } 427 | 428 | if v.claims != nil { 429 | for _, flag := range opts.Flags { 430 | if _, ok := v.claims.Flags[flag]; !ok { 431 | return nil, fmt.Errorf("token not authorized for flag %s", flag) 432 | } 433 | } 434 | } else if len(opts.Flags) > 0 { 435 | return nil, fmt.Errorf("token not authorized for flags") 436 | } 437 | 438 | for _, option := range opts.Options { 439 | err = option.verify(&identity) 440 | if err != nil { 441 | return nil, err 442 | } 443 | } 444 | 445 | return &VerifiedToken{ 446 | Identity: &identity, 447 | SigningAuthority: signingAuthority, 448 | Certificate: cert, 449 | }, nil 450 | } 451 | 452 | type verifyRawClaimsOpts struct { 453 | replid string 454 | user string 455 | cluster string 456 | subcluster string 457 | deployment bool 458 | claims *MessageClaims 459 | anyReplid bool 460 | anyUser bool 461 | anyCluster bool 462 | anySubcluster bool 463 | anyOrg bool 464 | allowsDeployment bool 465 | orgId string 466 | orgType api.Org_OrgType 467 | } 468 | 469 | func verifyRawClaims( 470 | opts verifyRawClaimsOpts, 471 | ) error { 472 | if opts.claims != nil { 473 | if opts.replid != "" && !opts.anyReplid { 474 | if _, ok := opts.claims.Repls[opts.replid]; !ok { 475 | return errors.New("not authorized (replid)") 476 | } 477 | } 478 | 479 | if opts.user != "" && !opts.anyUser { 480 | if _, ok := opts.claims.Users[opts.user]; !ok { 481 | return errors.New("not authorized (user)") 482 | } 483 | } 484 | 485 | if opts.orgId != "" && !opts.anyOrg { 486 | orgKey := OrgKey{ 487 | Id: opts.orgId, 488 | Typ: opts.orgType, 489 | } 490 | if _, ok := opts.claims.Orgs[orgKey]; !ok { 491 | return errors.New("not authorized (orgId)") 492 | } 493 | } 494 | 495 | if opts.cluster != "" && !opts.anyCluster { 496 | if _, ok := opts.claims.Clusters[opts.cluster]; !ok { 497 | return errors.New("not authorized (cluster)") 498 | } 499 | } 500 | 501 | if opts.subcluster != "" && !opts.anySubcluster { 502 | if _, ok := opts.claims.Subclusters[opts.subcluster]; !ok { 503 | return errors.New("not authorized (subcluster)") 504 | } 505 | } 506 | 507 | if opts.deployment && !opts.allowsDeployment { 508 | return errors.New("not authorized (deployment)") 509 | } 510 | } 511 | 512 | return nil 513 | } 514 | 515 | func verifyClaims(iat time.Time, exp time.Time, replid, user, cluster, subcluster, orgId string, orgType api.Org_OrgType, deployment bool, claims *MessageClaims) error { 516 | if iat.After(time.Now()) { 517 | return fmt.Errorf("not valid for %s", time.Until(iat)) 518 | } 519 | 520 | if exp.Before(time.Now()) { 521 | return fmt.Errorf("expired %s ago", time.Since(exp)) 522 | } 523 | 524 | opts := verifyRawClaimsOpts{ 525 | replid: replid, 526 | user: user, 527 | orgId: orgId, 528 | orgType: orgType, 529 | cluster: cluster, 530 | subcluster: subcluster, 531 | deployment: deployment, 532 | claims: claims, 533 | } 534 | 535 | return verifyRawClaims(opts) 536 | } 537 | 538 | func decodeUnsafePASETO(token string) ([]byte, error) { 539 | parts := strings.Split(token, ".") 540 | if len(parts) != 4 { 541 | return nil, fmt.Errorf("token not in PASETO format") 542 | } 543 | if parts[0] != "v2" || parts[1] != "public" { 544 | return nil, fmt.Errorf("token does not start with v2.public.") 545 | } 546 | bytes, err := base64.RawURLEncoding.DecodeString(parts[2]) 547 | if err != nil { 548 | return nil, fmt.Errorf("invalid token body payload: %w", err) 549 | } 550 | // v2.public tokens have a 64-byte signature after the main body. 551 | bytes, err = base64.StdEncoding.DecodeString(string(bytes[:len(bytes)-64])) 552 | if err != nil { 553 | return nil, fmt.Errorf("invalid token body internal payload: %w", err) 554 | } 555 | return bytes, nil 556 | } 557 | 558 | func decodeUnsafeReplIdentity(token string) (*api.GovalReplIdentity, error) { 559 | bytes, err := decodeUnsafePASETO(token) 560 | if err != nil { 561 | return nil, err 562 | } 563 | var replIdentity api.GovalReplIdentity 564 | err = proto.Unmarshal(bytes, &replIdentity) 565 | if err != nil { 566 | return nil, fmt.Errorf("token body not an api.GovalReplIdentity: %w", err) 567 | } 568 | return &replIdentity, nil 569 | } 570 | 571 | func decodeUnsafeGovalCert(token string) (*api.GovalCert, error) { 572 | bytes, err := decodeUnsafePASETO(token) 573 | if err != nil { 574 | return nil, err 575 | } 576 | var decodedCert api.GovalCert 577 | err = proto.Unmarshal(bytes, &decodedCert) 578 | if err != nil { 579 | return nil, fmt.Errorf("token body not an api.GovalReplIdentity: %w", err) 580 | } 581 | return &decodedCert, nil 582 | } 583 | 584 | // DebugTokenAsString returns a string representation explaining a token. It does not perform any 585 | // validation of the token, and should be used only for debugging. 586 | func DebugTokenAsString(token string) string { 587 | lines := []string{ 588 | "raw token:", 589 | fmt.Sprintf(" %s", token), 590 | } 591 | marshalOptions := protojson.MarshalOptions{ 592 | Indent: " ", 593 | Multiline: true, 594 | } 595 | 596 | // First dump the token contents. 597 | lines = append(lines, "decoded token:") 598 | replIdentity, err := decodeUnsafeReplIdentity(token) 599 | if err != nil { 600 | lines = append(lines, fmt.Sprintf(" token decode error: %v", err)) 601 | return strings.Join(lines, "\n") 602 | } 603 | for _, line := range strings.Split(marshalOptions.Format(replIdentity), "\n") { 604 | lines = append(lines, fmt.Sprintf(" %s", line)) 605 | } 606 | lines = append(lines, "signing authority chain:") 607 | 608 | // Now dump the signing authority chain. 609 | for { 610 | signingAuthority, err := getSigningAuthority(token) 611 | lines = append(lines, " signing authority:") 612 | if err != nil { 613 | lines = append(lines, fmt.Sprintf(" signing authority unmarshal error: %v", err)) 614 | return strings.Join(lines, "\n") 615 | } 616 | for _, line := range strings.Split(marshalOptions.Format(signingAuthority), "\n") { 617 | lines = append(lines, fmt.Sprintf(" %s", line)) 618 | } 619 | if signingAuthority.GetKeyId() != "" { 620 | break 621 | } 622 | lines = append(lines, " certificate:") 623 | token = signingAuthority.GetSignedCert() 624 | cert, err := decodeUnsafeGovalCert(token) 625 | if err != nil { 626 | lines = append(lines, fmt.Sprintf(" cert unmarshal error: %v", err)) 627 | return strings.Join(lines, "\n") 628 | } 629 | for _, line := range strings.Split(marshalOptions.Format(cert), "\n") { 630 | lines = append(lines, fmt.Sprintf(" %s", line)) 631 | } 632 | lines = append(lines, "") 633 | } 634 | 635 | // This text is not supposed to be machine-readable, so let's make 636 | // it extra hard for machines to parse this by word-wrapping (also makes 637 | // it nice to print on a Repl). 638 | const wordWrapCols = 60 639 | var wrappedLines []string 640 | for _, line := range lines { 641 | indent := " " 642 | for i := 0; i < len(line) && line[i] == ' '; i++ { 643 | indent += " " 644 | } 645 | for len(line) > wordWrapCols { 646 | wrappedLines = append(wrappedLines, line[:wordWrapCols]) 647 | line = indent + line[wordWrapCols:] 648 | } 649 | wrappedLines = append(wrappedLines, line) 650 | } 651 | lines = wrappedLines 652 | return strings.Join(lines, "\n") 653 | } 654 | -------------------------------------------------------------------------------- /sign_test.go: -------------------------------------------------------------------------------- 1 | package replidentity 2 | 3 | import ( 4 | "crypto/rand" 5 | "encoding/base64" 6 | "fmt" 7 | "testing" 8 | "time" 9 | 10 | "github.com/o1egl/paseto" 11 | "github.com/stretchr/testify/assert" 12 | "github.com/stretchr/testify/require" 13 | "golang.org/x/crypto/ed25519" 14 | "google.golang.org/protobuf/proto" 15 | "google.golang.org/protobuf/types/known/timestamppb" 16 | 17 | "github.com/replit/go-replidentity/paserk" 18 | "github.com/replit/go-replidentity/protos/external/goval/api" 19 | ) 20 | 21 | const ( 22 | developmentKeyID = "dev:1" 23 | developmentPublicKey = "on0FkSmEC+ce40V9Vc4QABXSx6TXo+lhp99b6Ka0gro=" 24 | conmanPrivateKey = "mRe4Bu9PG4Tq52M6LXp2oRcljhOjhJ43+x4AjPsPHaOkImeb6EduKRzVok/pADoVeNa8XEWAbly+Wipo7qPM4Q==" 25 | conmanCertificate = "GAEiBmNvbm1hbhLrAnYyLnB1YmxpYy5RMmR6U1d0TGRqZHpRVmxSTlhJMk0xWkNTVXhEU2k5dU1qVkJVMFZKVEVSME1WRmhRV2huUWtkblNWbENVbTlEUjBGallVRm9aMHRIWjBsM1FWSnZRMGRCU1dGQmFHZEVSMmRKV1VONGIwTkhRWGRoUkZOSlRGcEhWakphVjNoMlkwY3hiR0p1VVdsT1YzTjVURzVDTVZsdGVIQlplVFYzVVRCd2RXSlRNVzlUUjBwd1lUSk5lRmxWY0ZGT2JFWkNUbXRhV1dGc1pESlNibWhIV2pCak1Wa3pXbk5pTTBab1ZIcGFjV1ZyT1VaZ29sMmlPZkRIQ3pNSEF1SFJBM2F5MlRRZVV0SU1ySzN5VHNzVC1HcUM0ekl4a3NFRmVTc0hWbFlTSVlOOTkyd0diY1pyQW0zM1RrOHJ0UFZvWll3Ry5SMEZGYVVKdFRuWmliVEZvWW1kdlJscEhWakpQYWtVOQ==" 26 | ) 27 | 28 | func generateIntermediateCert( 29 | parentPrivateKey ed25519.PrivateKey, 30 | parentAuthority *api.GovalSigningAuthority, 31 | claims []*api.CertificateClaim, 32 | issuer string, 33 | duration time.Duration, 34 | ) (ed25519.PrivateKey, *api.GovalSigningAuthority, error) { 35 | // Generate a new keypair for this cert 36 | publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader) 37 | if err != nil { 38 | return nil, nil, fmt.Errorf("failed to generate certificate key pair: %w", err) 39 | } 40 | 41 | encodedKey := paserk.PublicKeyToPASERKPublic(publicKey) 42 | 43 | cert := &api.GovalCert{ 44 | // Issue this token 15s into the past to accomodate for clock drift. 45 | Iat: timestamppb.New(time.Now().Add(-15 * time.Second)), 46 | Exp: timestamppb.New(time.Now().Add(duration)), 47 | Claims: claims, 48 | PublicKey: string(encodedKey), 49 | } 50 | 51 | serializedCert, err := proto.Marshal(cert) 52 | if err != nil { 53 | return nil, nil, fmt.Errorf("failed to serialize the cert: %w", err) 54 | } 55 | 56 | serializedSigningAuth, err := proto.Marshal(parentAuthority) 57 | if err != nil { 58 | return nil, nil, fmt.Errorf("failed to serialize the cert: %w", err) 59 | } 60 | 61 | // Sign the intermediate cert with the parent's cert. 62 | signedCert, err := paseto.NewV2().Sign( 63 | parentPrivateKey, 64 | base64.StdEncoding.EncodeToString(serializedCert), 65 | base64.StdEncoding.EncodeToString(serializedSigningAuth), 66 | ) 67 | if err != nil { 68 | return nil, nil, fmt.Errorf("failed to sign the cert: %w", err) 69 | } 70 | 71 | return privateKey, &api.GovalSigningAuthority{ 72 | Cert: &api.GovalSigningAuthority_SignedCert{ 73 | SignedCert: signedCert, 74 | }, 75 | Issuer: issuer, 76 | Version: api.TokenVersion_TYPE_AWARE_TOKEN, 77 | }, nil 78 | } 79 | 80 | // identityToken generates and returns a signed identity (plus a private key) 81 | // for the given repl metadata, both can then be used to sign further identity 82 | // tokens. Other repls can verify this identity to verify a client is a 83 | // particular user or repl. 84 | func identityToken( 85 | replID string, 86 | user string, 87 | userID int64, 88 | slug string, 89 | ) (ed25519.PrivateKey, string, error) { 90 | return tokenWithClaims( 91 | replID, 92 | user, 93 | userID, 94 | slug, 95 | "", // orgId 96 | 0, // orgType 97 | []*api.CertificateClaim{ 98 | {Claim: &api.CertificateClaim_Flag{Flag: api.FlagClaim_IDENTITY}}, 99 | {Claim: &api.CertificateClaim_Replid{Replid: replID}}, 100 | {Claim: &api.CertificateClaim_User{User: user}}, 101 | {Claim: &api.CertificateClaim_UserId{UserId: userID}}, 102 | }, 103 | ) 104 | } 105 | 106 | func identityTokenWithOrg( 107 | replID string, 108 | user string, 109 | userID int64, 110 | slug string, 111 | orgID string, 112 | orgType api.Org_OrgType, 113 | ) (ed25519.PrivateKey, string, error) { 114 | return tokenWithClaims( 115 | replID, 116 | user, 117 | userID, 118 | slug, 119 | orgID, 120 | orgType, 121 | []*api.CertificateClaim{ 122 | {Claim: &api.CertificateClaim_Flag{Flag: api.FlagClaim_IDENTITY}}, 123 | {Claim: &api.CertificateClaim_Replid{Replid: replID}}, 124 | {Claim: &api.CertificateClaim_User{User: user}}, 125 | {Claim: &api.CertificateClaim_UserId{UserId: userID}}, 126 | {Claim: &api.CertificateClaim_Org{ 127 | Org: &api.Org{ 128 | Id: orgID, 129 | Type: orgType, 130 | }, 131 | }}, 132 | }, 133 | ) 134 | } 135 | 136 | func renewalToken( 137 | replID string, 138 | user string, 139 | userID int64, 140 | slug string, 141 | ) (ed25519.PrivateKey, string, error) { 142 | return tokenWithClaims( 143 | replID, 144 | user, 145 | userID, 146 | slug, 147 | "", // orgId 148 | 0, // orgType 149 | []*api.CertificateClaim{ 150 | {Claim: &api.CertificateClaim_Flag{Flag: api.FlagClaim_RENEW_IDENTITY}}, 151 | {Claim: &api.CertificateClaim_Replid{Replid: replID}}, 152 | {Claim: &api.CertificateClaim_User{User: user}}, 153 | {Claim: &api.CertificateClaim_UserId{UserId: userID}}, 154 | }, 155 | ) 156 | } 157 | 158 | func tokenWithClaims( 159 | replID string, 160 | user string, 161 | userID int64, 162 | slug string, 163 | orgId string, 164 | orgType api.Org_OrgType, 165 | claims []*api.CertificateClaim, 166 | ) (ed25519.PrivateKey, string, error) { 167 | replIdentity := api.GovalReplIdentity{ 168 | Replid: replID, 169 | User: user, 170 | UserId: userID, 171 | Slug: slug, 172 | Aud: replID, 173 | } 174 | 175 | if orgId != "" { 176 | replIdentity.Org = &api.Org{ 177 | Id: orgId, 178 | Type: orgType, 179 | } 180 | } 181 | 182 | var conmanAuthority api.GovalSigningAuthority 183 | conmanDecodedCertificate, err := base64.StdEncoding.DecodeString(conmanCertificate) 184 | if err != nil { 185 | return nil, "", fmt.Errorf("decode base64 identity: %w", err) 186 | } 187 | err = proto.Unmarshal(conmanDecodedCertificate, &conmanAuthority) 188 | if err != nil { 189 | return nil, "", fmt.Errorf("unmarshal identity: %w", err) 190 | } 191 | conmanDecodedPrivateKey, err := base64.StdEncoding.DecodeString(conmanPrivateKey) 192 | if err != nil { 193 | return nil, "", fmt.Errorf("decode base64 private key: %w", err) 194 | } 195 | conmanPrivateKey := ed25519.PrivateKey(conmanDecodedPrivateKey) 196 | 197 | intermediatePrivateKey, intermediateAuthority, err := generateIntermediateCert( 198 | conmanPrivateKey, 199 | &conmanAuthority, 200 | claims, 201 | "conman", 202 | 36*time.Hour, // Repls can not live for more than 20-ish hours at the moment. 203 | ) 204 | if err != nil { 205 | return nil, "", fmt.Errorf("generate intermediate identity cert: %w", err) 206 | } 207 | 208 | token, err := signIdentity(intermediatePrivateKey, intermediateAuthority, &replIdentity) 209 | if err != nil { 210 | return nil, "", fmt.Errorf("sign identity: %w", err) 211 | } 212 | 213 | return intermediatePrivateKey, token, nil 214 | } 215 | 216 | // identityToken generates and returns a signed identity (plus a private key) 217 | // for the given repl metadata with a specific origin ID. 218 | func identityTokenWithOrigin( 219 | replID string, 220 | user string, 221 | userID int64, 222 | slug string, 223 | originID string, 224 | ) (ed25519.PrivateKey, string, error) { 225 | replIdentity := api.GovalReplIdentity{ 226 | Replid: replID, 227 | User: user, 228 | UserId: userID, 229 | Slug: slug, 230 | Aud: replID, 231 | OriginReplid: originID, 232 | } 233 | 234 | var conmanAuthority api.GovalSigningAuthority 235 | conmanDecodedCertificate, err := base64.StdEncoding.DecodeString(conmanCertificate) 236 | if err != nil { 237 | return nil, "", fmt.Errorf("decode base64 identity: %w", err) 238 | } 239 | err = proto.Unmarshal(conmanDecodedCertificate, &conmanAuthority) 240 | if err != nil { 241 | return nil, "", fmt.Errorf("unmarshal identity: %w", err) 242 | } 243 | conmanDecodedPrivateKey, err := base64.StdEncoding.DecodeString(conmanPrivateKey) 244 | if err != nil { 245 | return nil, "", fmt.Errorf("decode base64 private key: %w", err) 246 | } 247 | conmanPrivateKey := ed25519.PrivateKey(conmanDecodedPrivateKey) 248 | 249 | intermediatePrivateKey, intermediateAuthority, err := generateIntermediateCert( 250 | conmanPrivateKey, 251 | &conmanAuthority, 252 | []*api.CertificateClaim{ 253 | {Claim: &api.CertificateClaim_Flag{Flag: api.FlagClaim_IDENTITY}}, 254 | {Claim: &api.CertificateClaim_Replid{Replid: replIdentity.Replid}}, 255 | {Claim: &api.CertificateClaim_User{User: replIdentity.User}}, 256 | {Claim: &api.CertificateClaim_UserId{UserId: replIdentity.UserId}}, 257 | }, 258 | "conman", 259 | 36*time.Hour, // Repls can not live for more than 20-ish hours at the moment. 260 | ) 261 | if err != nil { 262 | return nil, "", fmt.Errorf("generate intermediate identity cert: %w", err) 263 | } 264 | 265 | token, err := signIdentity(intermediatePrivateKey, intermediateAuthority, &replIdentity) 266 | if err != nil { 267 | return nil, "", fmt.Errorf("sign identity: %w", err) 268 | } 269 | 270 | return intermediatePrivateKey, token, nil 271 | } 272 | 273 | // identityTokenAnyRepl creates an identity token that allows for any replid 274 | func identityTokenAnyRepl( 275 | replID string, 276 | user string, 277 | userID int64, 278 | slug string, 279 | ) (ed25519.PrivateKey, string, error) { 280 | replIdentity := api.GovalReplIdentity{ 281 | Replid: replID, 282 | User: user, 283 | UserId: userID, 284 | Slug: slug, 285 | Aud: replID, 286 | } 287 | 288 | var conmanAuthority api.GovalSigningAuthority 289 | conmanDecodedCertificate, err := base64.StdEncoding.DecodeString(conmanCertificate) 290 | if err != nil { 291 | return nil, "", fmt.Errorf("decode base64 identity: %w", err) 292 | } 293 | err = proto.Unmarshal(conmanDecodedCertificate, &conmanAuthority) 294 | if err != nil { 295 | return nil, "", fmt.Errorf("unmarshal identity: %w", err) 296 | } 297 | conmanDecodedPrivateKey, err := base64.StdEncoding.DecodeString(conmanPrivateKey) 298 | if err != nil { 299 | return nil, "", fmt.Errorf("decode base64 private key: %w", err) 300 | } 301 | conmanPrivateKey := ed25519.PrivateKey(conmanDecodedPrivateKey) 302 | 303 | intermediatePrivateKey, intermediateAuthority, err := generateIntermediateCert( 304 | conmanPrivateKey, 305 | &conmanAuthority, 306 | []*api.CertificateClaim{ 307 | {Claim: &api.CertificateClaim_Flag{Flag: api.FlagClaim_IDENTITY}}, 308 | {Claim: &api.CertificateClaim_Flag{Flag: api.FlagClaim_RENEW_IDENTITY}}, 309 | {Claim: &api.CertificateClaim_Flag{Flag: api.FlagClaim_ANY_REPLID}}, 310 | {Claim: &api.CertificateClaim_Cluster{Cluster: "development"}}, 311 | {Claim: &api.CertificateClaim_User{User: replIdentity.User}}, 312 | {Claim: &api.CertificateClaim_UserId{UserId: replIdentity.UserId}}, 313 | }, 314 | "conman", 315 | 36*time.Hour, // Repls can not live for more than 20-ish hours at the moment. 316 | ) 317 | if err != nil { 318 | return nil, "", fmt.Errorf("generate intermediate identity cert: %w", err) 319 | } 320 | 321 | token, err := signIdentity(intermediatePrivateKey, intermediateAuthority, &replIdentity) 322 | if err != nil { 323 | return nil, "", fmt.Errorf("sign identity: %w", err) 324 | } 325 | 326 | return intermediatePrivateKey, token, nil 327 | } 328 | 329 | // multiTierIdentityToken generates and returns a broken identity token that includes 330 | // intermediate certs with differing repl IDs. 331 | func multiTierIdentityToken( 332 | replID string, 333 | user string, 334 | userID int64, 335 | slug string, 336 | ) (ed25519.PrivateKey, string, error) { 337 | replIdentity := api.GovalReplIdentity{ 338 | Replid: replID, 339 | User: user, 340 | UserId: userID, 341 | Slug: slug, 342 | Aud: replID, 343 | } 344 | 345 | var conmanAuthority api.GovalSigningAuthority 346 | conmanDecodedCertificate, err := base64.StdEncoding.DecodeString(conmanCertificate) 347 | if err != nil { 348 | return nil, "", fmt.Errorf("decode base64 identity: %w", err) 349 | } 350 | err = proto.Unmarshal(conmanDecodedCertificate, &conmanAuthority) 351 | if err != nil { 352 | return nil, "", fmt.Errorf("unmarshal identity: %w", err) 353 | } 354 | conmanDecodedPrivateKey, err := base64.StdEncoding.DecodeString(conmanPrivateKey) 355 | if err != nil { 356 | return nil, "", fmt.Errorf("decode base64 private key: %w", err) 357 | } 358 | conmanPrivateKey := ed25519.PrivateKey(conmanDecodedPrivateKey) 359 | 360 | intermediatePrivateKey, intermediateAuthority, err := generateIntermediateCert( 361 | conmanPrivateKey, 362 | &conmanAuthority, 363 | []*api.CertificateClaim{ 364 | {Claim: &api.CertificateClaim_Flag{Flag: api.FlagClaim_IDENTITY}}, 365 | {Claim: &api.CertificateClaim_Replid{Replid: replIdentity.Replid}}, 366 | {Claim: &api.CertificateClaim_User{User: replIdentity.User}}, 367 | {Claim: &api.CertificateClaim_UserId{UserId: replIdentity.UserId}}, 368 | }, 369 | "conman", 370 | 36*time.Hour, // Repls can not live for more than 20-ish hours at the moment. 371 | ) 372 | if err != nil { 373 | return nil, "", fmt.Errorf("generate intermediate identity cert: %w", err) 374 | } 375 | 376 | finalPrivateKey, finalAuthority, err := generateIntermediateCert( 377 | intermediatePrivateKey, 378 | intermediateAuthority, 379 | []*api.CertificateClaim{ 380 | {Claim: &api.CertificateClaim_Flag{Flag: api.FlagClaim_IDENTITY}}, 381 | {Claim: &api.CertificateClaim_Replid{Replid: replIdentity.Replid + "-spoofed"}}, 382 | {Claim: &api.CertificateClaim_User{User: replIdentity.User}}, 383 | {Claim: &api.CertificateClaim_UserId{UserId: replIdentity.UserId}}, 384 | }, 385 | "conman", 386 | 36*time.Hour, // Repls can not live for more than 20-ish hours at the moment. 387 | ) 388 | if err != nil { 389 | return nil, "", fmt.Errorf("generate intermediate identity cert: %w", err) 390 | } 391 | 392 | token, err := signIdentity(finalPrivateKey, finalAuthority, &replIdentity) 393 | if err != nil { 394 | return nil, "", fmt.Errorf("sign identity: %w", err) 395 | } 396 | 397 | return finalPrivateKey, token, nil 398 | } 399 | 400 | func TestIdentity(t *testing.T) { 401 | privkey, identity, err := identityToken("repl", "user", 1, "slug") 402 | require.NoError(t, err) 403 | 404 | getPubKey := func(keyid, issuer string) (ed25519.PublicKey, error) { 405 | if keyid != developmentKeyID { 406 | return nil, nil 407 | } 408 | keyBytes, err := base64.StdEncoding.DecodeString(developmentPublicKey) 409 | if err != nil { 410 | return nil, fmt.Errorf("failed to parse public key as base64: %w", err) 411 | } 412 | 413 | return ed25519.PublicKey(keyBytes), nil 414 | } 415 | 416 | signingAuthority, err := NewSigningAuthority( 417 | string(paserk.PrivateKeyToPASERKSecret(privkey)), 418 | identity, 419 | "repl", 420 | getPubKey, 421 | ) 422 | require.NoError(t, err) 423 | forwarded, err := signingAuthority.Sign("testing") 424 | require.NoError(t, err) 425 | 426 | replIdentity, err := VerifyIdentity( 427 | forwarded, 428 | []string{"testing"}, 429 | getPubKey, 430 | ) 431 | require.NoError(t, err) 432 | 433 | // identities without origin repl IDs are accepted by default 434 | // (they're not guest forks, replID can be used) 435 | _, err = VerifyIdentity( 436 | forwarded, 437 | []string{"testing"}, 438 | getPubKey, 439 | WithSource("origin"), 440 | ) 441 | require.NoError(t, err) 442 | 443 | assert.Equal(t, "repl", replIdentity.Replid) 444 | assert.Equal(t, "user", replIdentity.User) 445 | assert.Equal(t, int64(1), replIdentity.UserId) 446 | assert.Equal(t, "slug", replIdentity.Slug) 447 | } 448 | 449 | func TestNoIdentityClaim(t *testing.T) { 450 | replID := "repl" 451 | user := "user" 452 | privkey, identity, err := tokenWithClaims( 453 | replID, 454 | user, 455 | 1, 456 | "slug", 457 | "", 458 | 0, 459 | // We're leaving out the IDENTITY claim 460 | []*api.CertificateClaim{ 461 | {Claim: &api.CertificateClaim_User{User: user}}, 462 | {Claim: &api.CertificateClaim_UserId{UserId: 1}}, 463 | {Claim: &api.CertificateClaim_Replid{Replid: replID}}, 464 | }) 465 | require.NoError(t, err) 466 | 467 | getPubKey := func(keyid, issuer string) (ed25519.PublicKey, error) { 468 | if keyid != developmentKeyID { 469 | return nil, nil 470 | } 471 | keyBytes, err := base64.StdEncoding.DecodeString(developmentPublicKey) 472 | if err != nil { 473 | return nil, fmt.Errorf("failed to parse public key as base64: %w", err) 474 | } 475 | 476 | return ed25519.PublicKey(keyBytes), nil 477 | } 478 | 479 | signingAuthority, err := NewSigningAuthority( 480 | string(paserk.PrivateKeyToPASERKSecret(privkey)), 481 | identity, 482 | "repl", 483 | getPubKey, 484 | ) 485 | require.NoError(t, err) 486 | forwarded, err := signingAuthority.Sign("testing") 487 | require.NoError(t, err) 488 | 489 | _, err = VerifyIdentity( 490 | forwarded, 491 | []string{"testing"}, 492 | getPubKey, 493 | ) 494 | // Check that we got a 'token not authorized for flag IDENTITY' error 495 | require.Error(t, err) 496 | assert.Equal(t, "token not authorized for flag IDENTITY", err.Error()) 497 | } 498 | 499 | func TestOriginIdentity(t *testing.T) { 500 | privkey, identity, err := identityTokenWithOrigin("repl", "user", 1, "slug", "origin") 501 | require.NoError(t, err) 502 | 503 | getPubKey := func(keyid, issuer string) (ed25519.PublicKey, error) { 504 | if keyid != developmentKeyID { 505 | return nil, nil 506 | } 507 | keyBytes, err := base64.StdEncoding.DecodeString(developmentPublicKey) 508 | if err != nil { 509 | return nil, fmt.Errorf("failed to parse public key as base64: %w", err) 510 | } 511 | 512 | return ed25519.PublicKey(keyBytes), nil 513 | } 514 | 515 | signingAuthority, err := NewSigningAuthority( 516 | string(paserk.PrivateKeyToPASERKSecret(privkey)), 517 | identity, 518 | "repl", 519 | getPubKey, 520 | ) 521 | require.NoError(t, err) 522 | forwarded, err := signingAuthority.Sign("testing") 523 | require.NoError(t, err) 524 | 525 | replIdentity, err := VerifyIdentity( 526 | forwarded, 527 | []string{"testing"}, 528 | getPubKey, 529 | WithSource("origin"), 530 | ) 531 | require.NoError(t, err) 532 | 533 | _, err = VerifyIdentity( 534 | forwarded, 535 | []string{"testing"}, 536 | getPubKey, 537 | WithSource("another-origin"), 538 | ) 539 | require.Error(t, err) 540 | 541 | assert.Equal(t, "repl", replIdentity.Replid) 542 | assert.Equal(t, "user", replIdentity.User) 543 | assert.Equal(t, int64(1), replIdentity.UserId) 544 | assert.Equal(t, "slug", replIdentity.Slug) 545 | assert.Equal(t, "origin", replIdentity.OriginReplid) 546 | } 547 | 548 | func TestLayeredIdentity(t *testing.T) { 549 | layeredReplIdentity := api.GovalReplIdentity{ 550 | Replid: "a-b-c-d", 551 | User: "spoof", 552 | UserId: 2, 553 | Slug: "spoofed", 554 | Aud: "another-audience", 555 | } 556 | 557 | privkey, identity, err := identityToken("repl", "user", 1, "slug") 558 | require.NoError(t, err) 559 | 560 | getPubKey := func(keyid, issuer string) (ed25519.PublicKey, error) { 561 | if keyid != developmentKeyID { 562 | return nil, nil 563 | } 564 | keyBytes, err := base64.StdEncoding.DecodeString(developmentPublicKey) 565 | if err != nil { 566 | return nil, fmt.Errorf("failed to parse public key as base64: %w", err) 567 | } 568 | 569 | return ed25519.PublicKey(keyBytes), nil 570 | } 571 | 572 | signingAuthority, err := NewSigningAuthority( 573 | string(paserk.PrivateKeyToPASERKSecret(privkey)), 574 | identity, 575 | "repl", 576 | getPubKey, 577 | ) 578 | require.NoError(t, err) 579 | 580 | // generate yet another layer using our key 581 | token, err := signIdentity(privkey, signingAuthority.signingAuthority, &layeredReplIdentity) 582 | require.NoError(t, err) 583 | 584 | _, err = VerifyIdentity( 585 | token, 586 | // the audience claim mismatch fails too early. we need to make sure we don't trust 587 | // the wrong level of replid/user/slug, because another repl could use its private 588 | // key to sign a spoofed identity with a "valid" audience. 589 | []string{"another-audience"}, 590 | getPubKey, 591 | ) 592 | require.Error(t, err) 593 | } 594 | 595 | func TestLayeredIdentityWithSpoofedCert(t *testing.T) { 596 | privkey, identity, err := multiTierIdentityToken("repl", "user", 1, "slug") 597 | require.NoError(t, err) 598 | 599 | getPubKey := func(keyid, issuer string) (ed25519.PublicKey, error) { 600 | if keyid != developmentKeyID { 601 | return nil, nil 602 | } 603 | keyBytes, err := base64.StdEncoding.DecodeString(developmentPublicKey) 604 | if err != nil { 605 | return nil, fmt.Errorf("failed to parse public key as base64: %w", err) 606 | } 607 | 608 | return ed25519.PublicKey(keyBytes), nil 609 | } 610 | 611 | // This will fail (extra intermediate cert is not permitted) 612 | _, err = NewSigningAuthority( 613 | string(paserk.PrivateKeyToPASERKSecret(privkey)), 614 | identity, 615 | "repl", 616 | getPubKey, 617 | ) 618 | require.Error(t, err) 619 | } 620 | 621 | func TestAnyReplIDIdentity(t *testing.T) { 622 | layeredReplIdentity := api.GovalReplIdentity{ 623 | Replid: "a-b-c-d", 624 | User: "user", 625 | UserId: 1, 626 | Slug: "slug", 627 | Aud: "another-audience", 628 | Runtime: &api.GovalReplIdentity_Interactive{ 629 | Interactive: &api.ReplRuntimeInteractive{ 630 | Cluster: "development", 631 | Subcluster: "", 632 | }, 633 | }, 634 | } 635 | 636 | privkey, identity, err := identityTokenAnyRepl("repl", "user", 1, "slug") 637 | require.NoError(t, err) 638 | 639 | getPubKey := func(keyid, issuer string) (ed25519.PublicKey, error) { 640 | if keyid != developmentKeyID { 641 | return nil, nil 642 | } 643 | keyBytes, err := base64.StdEncoding.DecodeString(developmentPublicKey) 644 | if err != nil { 645 | return nil, fmt.Errorf("failed to parse public key as base64: %w", err) 646 | } 647 | 648 | return ed25519.PublicKey(keyBytes), nil 649 | } 650 | 651 | signingAuthority, err := NewSigningAuthority( 652 | string(paserk.PrivateKeyToPASERKSecret(privkey)), 653 | identity, 654 | "repl", 655 | getPubKey, 656 | ) 657 | require.NoError(t, err) 658 | 659 | // generate yet another layer using our key 660 | token, err := signIdentity(privkey, signingAuthority.signingAuthority, &layeredReplIdentity) 661 | require.NoError(t, err) 662 | 663 | replIdentity, err := VerifyIdentity( 664 | token, 665 | []string{"another-audience"}, 666 | getPubKey, 667 | ) 668 | require.NoError(t, err) 669 | 670 | assert.Equal(t, "a-b-c-d", replIdentity.Replid) 671 | assert.Equal(t, "user", replIdentity.User) 672 | assert.Equal(t, int64(1), replIdentity.UserId) 673 | assert.Equal(t, "slug", replIdentity.Slug) 674 | } 675 | 676 | func TestSpoofedRuntimeIdentity(t *testing.T) { 677 | for i, layeredReplIdentity := range []*api.GovalReplIdentity{ 678 | { 679 | Replid: "a-b-c-d", 680 | User: "user", 681 | UserId: 1, 682 | Slug: "slug", 683 | Aud: "another-audience", 684 | Runtime: &api.GovalReplIdentity_Interactive{ 685 | Interactive: &api.ReplRuntimeInteractive{ 686 | Cluster: "development", 687 | Subcluster: "foo", 688 | }, 689 | }, 690 | }, 691 | { 692 | Replid: "a-b-c-d", 693 | User: "user", 694 | UserId: 1, 695 | Slug: "slug", 696 | Aud: "another-audience", 697 | Runtime: &api.GovalReplIdentity_Hosting{ 698 | Hosting: &api.ReplRuntimeHosting{ 699 | Cluster: "development", 700 | Subcluster: "foo", 701 | }, 702 | }, 703 | }, 704 | { 705 | Replid: "a-b-c-d", 706 | User: "user", 707 | UserId: 1, 708 | Slug: "slug", 709 | Aud: "another-audience", 710 | Runtime: &api.GovalReplIdentity_Deployment{ 711 | Deployment: &api.ReplRuntimeDeployment{}, 712 | }, 713 | }, 714 | } { 715 | layeredReplIdentity := layeredReplIdentity 716 | t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { 717 | privkey, identity, err := identityTokenAnyRepl("repl", "user", 1, "slug") 718 | require.NoError(t, err) 719 | 720 | getPubKey := func(keyid, issuer string) (ed25519.PublicKey, error) { 721 | if keyid != developmentKeyID { 722 | return nil, nil 723 | } 724 | keyBytes, err := base64.StdEncoding.DecodeString(developmentPublicKey) 725 | if err != nil { 726 | return nil, fmt.Errorf("failed to parse public key as base64: %w", err) 727 | } 728 | 729 | return ed25519.PublicKey(keyBytes), nil 730 | } 731 | 732 | signingAuthority, err := NewSigningAuthority( 733 | string(paserk.PrivateKeyToPASERKSecret(privkey)), 734 | identity, 735 | "repl", 736 | getPubKey, 737 | ) 738 | require.NoError(t, err) 739 | 740 | // generate yet another layer using our key 741 | token, err := signIdentity(privkey, signingAuthority.signingAuthority, layeredReplIdentity) 742 | require.NoError(t, err) 743 | 744 | _, err = VerifyIdentity( 745 | token, 746 | []string{"another-audience"}, 747 | getPubKey, 748 | ) 749 | assert.Error(t, err) 750 | }) 751 | } 752 | } 753 | 754 | func TestRenew(t *testing.T) { 755 | privkey, identity, err := renewalToken("repl", "user", 1, "slug") 756 | require.NoError(t, err) 757 | 758 | getPubKey := func(keyid, issuer string) (ed25519.PublicKey, error) { 759 | if keyid != developmentKeyID { 760 | return nil, nil 761 | } 762 | keyBytes, err := base64.StdEncoding.DecodeString(developmentPublicKey) 763 | if err != nil { 764 | return nil, fmt.Errorf("failed to parse public key as base64: %w", err) 765 | } 766 | 767 | return ed25519.PublicKey(keyBytes), nil 768 | } 769 | 770 | signingAuthority, err := NewSigningAuthority( 771 | string(paserk.PrivateKeyToPASERKSecret(privkey)), 772 | identity, 773 | "repl", 774 | getPubKey, 775 | ) 776 | require.NoError(t, err) 777 | forwarded, err := signingAuthority.Sign("testing") 778 | require.NoError(t, err) 779 | 780 | replIdentity, err := VerifyRenewIdentity( 781 | forwarded, 782 | []string{"testing"}, 783 | getPubKey, 784 | ) 785 | require.NoError(t, err) 786 | 787 | assert.Equal(t, "repl", replIdentity.Replid) 788 | assert.Equal(t, "user", replIdentity.User) 789 | assert.Equal(t, int64(1), replIdentity.UserId) 790 | assert.Equal(t, "slug", replIdentity.Slug) 791 | } 792 | 793 | func TestRenewNoClaim(t *testing.T) { 794 | privkey, identity, err := tokenWithClaims( 795 | "replid", 796 | "user", 797 | 1, 798 | "slug", 799 | "", // org Id 800 | 0, // org type 801 | []*api.CertificateClaim{ 802 | {Claim: &api.CertificateClaim_Replid{Replid: "replid"}}, 803 | {Claim: &api.CertificateClaim_User{User: "user"}}, 804 | }, 805 | ) 806 | require.NoError(t, err) 807 | 808 | getPubKey := func(keyid, issuer string) (ed25519.PublicKey, error) { 809 | if keyid != developmentKeyID { 810 | return nil, nil 811 | } 812 | keyBytes, err := base64.StdEncoding.DecodeString(developmentPublicKey) 813 | if err != nil { 814 | return nil, fmt.Errorf("failed to parse public key as base64: %w", err) 815 | } 816 | 817 | return ed25519.PublicKey(keyBytes), nil 818 | } 819 | 820 | signingAuthority, err := NewSigningAuthority( 821 | string(paserk.PrivateKeyToPASERKSecret(privkey)), 822 | identity, 823 | "replid", 824 | getPubKey, 825 | ) 826 | require.NoError(t, err) 827 | forwarded, err := signingAuthority.Sign("testing") 828 | require.NoError(t, err) 829 | 830 | _, err = VerifyRenewIdentity( 831 | forwarded, 832 | []string{"testing"}, 833 | getPubKey, 834 | ) 835 | require.Error(t, err) 836 | } 837 | 838 | func TestIdentityWithOrgID(t *testing.T) { 839 | privkey, identity, err := identityTokenWithOrg("repl", "user", 1, "slug", "acmecorp", api.Org_TEAM) 840 | require.NoError(t, err) 841 | 842 | getPubKey := func(keyid, issuer string) (ed25519.PublicKey, error) { 843 | if keyid != developmentKeyID { 844 | return nil, nil 845 | } 846 | keyBytes, err := base64.StdEncoding.DecodeString(developmentPublicKey) 847 | if err != nil { 848 | return nil, fmt.Errorf("failed to parse public key as base64: %w", err) 849 | } 850 | 851 | return ed25519.PublicKey(keyBytes), nil 852 | } 853 | 854 | signingAuthority, err := NewSigningAuthority( 855 | string(paserk.PrivateKeyToPASERKSecret(privkey)), 856 | identity, 857 | "repl", 858 | getPubKey, 859 | ) 860 | require.NoError(t, err) 861 | forwarded, err := signingAuthority.Sign("testing") 862 | require.NoError(t, err) 863 | 864 | replIdentity, err := VerifyIdentity( 865 | forwarded, 866 | []string{"testing"}, 867 | getPubKey, 868 | ) 869 | require.NoError(t, err) 870 | 871 | // identities without origin repl IDs are accepted by default 872 | // (they're not guest forks, replID can be used) 873 | _, err = VerifyIdentity( 874 | forwarded, 875 | []string{"testing"}, 876 | getPubKey, 877 | WithSource("origin"), 878 | ) 879 | require.NoError(t, err) 880 | 881 | assert.Equal(t, "repl", replIdentity.Replid) 882 | assert.Equal(t, "user", replIdentity.User) 883 | assert.Equal(t, int64(1), replIdentity.UserId) 884 | assert.Equal(t, "slug", replIdentity.Slug) 885 | assert.Equal(t, "acmecorp", replIdentity.Org.Id) 886 | } 887 | 888 | func TestIdentityWithOrgIDFail(t *testing.T) { 889 | replID := "repl" 890 | user := "user" 891 | var userID int64 = 1 892 | slug := "slug" 893 | orgID := "acmecorp" 894 | orgType := api.Org_PERSONAL 895 | 896 | tcs := []struct { 897 | name string 898 | claims []*api.CertificateClaim 899 | }{ 900 | { 901 | name: "missing org claim", 902 | claims: []*api.CertificateClaim{ 903 | {Claim: &api.CertificateClaim_Flag{Flag: api.FlagClaim_IDENTITY}}, 904 | {Claim: &api.CertificateClaim_Replid{Replid: "repl"}}, 905 | {Claim: &api.CertificateClaim_User{User: "user"}}, 906 | {Claim: &api.CertificateClaim_UserId{UserId: 1}}, 907 | }, 908 | }, 909 | { 910 | name: "org id mismatch", 911 | claims: []*api.CertificateClaim{ 912 | {Claim: &api.CertificateClaim_Flag{Flag: api.FlagClaim_IDENTITY}}, 913 | {Claim: &api.CertificateClaim_Replid{Replid: "repl"}}, 914 | {Claim: &api.CertificateClaim_User{User: "user"}}, 915 | {Claim: &api.CertificateClaim_UserId{UserId: 1}}, 916 | {Claim: &api.CertificateClaim_Org{ 917 | Org: &api.Org{ 918 | Id: "wrong-org-id", 919 | Type: orgType, 920 | }}, 921 | }, 922 | }, 923 | }, 924 | { 925 | name: "org type mismatch", 926 | claims: []*api.CertificateClaim{ 927 | {Claim: &api.CertificateClaim_Flag{Flag: api.FlagClaim_IDENTITY}}, 928 | {Claim: &api.CertificateClaim_Replid{Replid: "repl"}}, 929 | {Claim: &api.CertificateClaim_User{User: "user"}}, 930 | {Claim: &api.CertificateClaim_UserId{UserId: 1}}, 931 | {Claim: &api.CertificateClaim_Org{ 932 | Org: &api.Org{ 933 | Id: orgID, 934 | Type: orgType + 1, 935 | }}, 936 | }, 937 | }, 938 | }, 939 | } 940 | 941 | for _, tc := range tcs { 942 | t.Run(tc.name, func(t *testing.T) { 943 | identity := api.GovalReplIdentity{ 944 | Replid: replID, 945 | User: user, 946 | UserId: userID, 947 | Slug: slug, 948 | Aud: replID, 949 | Org: &api.Org{ 950 | Id: orgID, 951 | Type: orgType, 952 | }, 953 | } 954 | 955 | privkey, marshaledIdentity, err := tokenWithClaims( 956 | replID, 957 | user, 958 | userID, 959 | slug, 960 | orgID, 961 | orgType, 962 | tc.claims, 963 | ) 964 | require.NoError(t, err) 965 | 966 | getPubKey := func(keyid, issuer string) (ed25519.PublicKey, error) { 967 | if keyid != developmentKeyID { 968 | return nil, nil 969 | } 970 | keyBytes, err := base64.StdEncoding.DecodeString(developmentPublicKey) 971 | if err != nil { 972 | return nil, fmt.Errorf("failed to parse public key as base64: %w", err) 973 | } 974 | 975 | return ed25519.PublicKey(keyBytes), nil 976 | } 977 | 978 | _, err = NewSigningAuthority( 979 | string(paserk.PrivateKeyToPASERKSecret(privkey)), 980 | marshaledIdentity, 981 | "repl", 982 | getPubKey, 983 | ) 984 | require.Error(t, err) 985 | assert.Equal(t, "claim mismatch: not authorized (orgId)", err.Error()) 986 | 987 | // check that, if we were to sign the token, the 988 | // receiving party would also not be able to verify it 989 | sa, err := getSigningAuthority(marshaledIdentity) 990 | require.NoError(t, err) 991 | 992 | signingAuthority := &SigningAuthority{ 993 | privateKey: privkey, 994 | signingAuthority: sa, 995 | identity: &identity, 996 | } 997 | 998 | forwarded, err := signingAuthority.Sign("testing") 999 | require.NoError(t, err) 1000 | 1001 | _, err = VerifyIdentity( 1002 | forwarded, 1003 | []string{"testing"}, 1004 | getPubKey, 1005 | ) 1006 | require.Error(t, err) 1007 | assert.Equal(t, "claim mismatch: not authorized (orgId)", err.Error()) 1008 | }) 1009 | } 1010 | } 1011 | -------------------------------------------------------------------------------- /protos/external/goval/api/client.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // versions: 3 | // protoc-gen-go v1.30.0 4 | // protoc v3.21.12 5 | // source: protos/external/goval/api/client.proto 6 | 7 | package api 8 | 9 | import ( 10 | protoreflect "google.golang.org/protobuf/reflect/protoreflect" 11 | protoimpl "google.golang.org/protobuf/runtime/protoimpl" 12 | timestamppb "google.golang.org/protobuf/types/known/timestamppb" 13 | reflect "reflect" 14 | sync "sync" 15 | ) 16 | 17 | const ( 18 | // Verify that this generated code is sufficiently up-to-date. 19 | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) 20 | // Verify that runtime/protoimpl is sufficiently up-to-date. 21 | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) 22 | ) 23 | 24 | // Whether these limits are cachable, and if they are, by what facet of the token. 25 | type ResourceLimits_Cachability int32 26 | 27 | const ( 28 | // Do not cache these limits. 29 | ResourceLimits_NONE ResourceLimits_Cachability = 0 30 | // These limits can be cached and applied to this and any of the user's 31 | // other repls. 32 | ResourceLimits_USER ResourceLimits_Cachability = 1 33 | // These limits can be cached and applied only to this repl. 34 | ResourceLimits_REPL ResourceLimits_Cachability = 2 35 | ) 36 | 37 | // Enum value maps for ResourceLimits_Cachability. 38 | var ( 39 | ResourceLimits_Cachability_name = map[int32]string{ 40 | 0: "NONE", 41 | 1: "USER", 42 | 2: "REPL", 43 | } 44 | ResourceLimits_Cachability_value = map[string]int32{ 45 | "NONE": 0, 46 | "USER": 1, 47 | "REPL": 2, 48 | } 49 | ) 50 | 51 | func (x ResourceLimits_Cachability) Enum() *ResourceLimits_Cachability { 52 | p := new(ResourceLimits_Cachability) 53 | *p = x 54 | return p 55 | } 56 | 57 | func (x ResourceLimits_Cachability) String() string { 58 | return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) 59 | } 60 | 61 | func (ResourceLimits_Cachability) Descriptor() protoreflect.EnumDescriptor { 62 | return file_protos_external_goval_api_client_proto_enumTypes[0].Descriptor() 63 | } 64 | 65 | func (ResourceLimits_Cachability) Type() protoreflect.EnumType { 66 | return &file_protos_external_goval_api_client_proto_enumTypes[0] 67 | } 68 | 69 | func (x ResourceLimits_Cachability) Number() protoreflect.EnumNumber { 70 | return protoreflect.EnumNumber(x) 71 | } 72 | 73 | // Deprecated: Use ResourceLimits_Cachability.Descriptor instead. 74 | func (ResourceLimits_Cachability) EnumDescriptor() ([]byte, []int) { 75 | return file_protos_external_goval_api_client_proto_rawDescGZIP(), []int{1, 0} 76 | } 77 | 78 | // Whether to persist filesystem, metadata, or both. 79 | type ReplToken_Persistence int32 80 | 81 | const ( 82 | // This is the usual mode of operation: both filesystem and metadata will be 83 | // persisted. 84 | ReplToken_PERSISTENT ReplToken_Persistence = 0 85 | // The ephemeral flag indicates the repl being connected to will have a time 86 | // restriction on stored metadata. This has the consequence that repl will 87 | // be unable to wakeup or serve static traffic once the metadata has timed 88 | // out. This option does NOT affect filesystem and other data persistence. 89 | // 90 | // For context, this value is used on the client when repls are created for: 91 | // - replrun 92 | // - guests 93 | // - anon users 94 | // - temp vnc repls 95 | // - users with non-verified emails 96 | ReplToken_EPHEMERAL ReplToken_Persistence = 1 97 | // This indicates that the repl being connected does not have the ability to 98 | // persist files or be woken up after the lifetime of this repl expires. 99 | // 100 | // For context, this value is used on the client when repls are created for: 101 | // - replrun 102 | // - guests 103 | // - language pages 104 | ReplToken_NONE ReplToken_Persistence = 2 105 | ) 106 | 107 | // Enum value maps for ReplToken_Persistence. 108 | var ( 109 | ReplToken_Persistence_name = map[int32]string{ 110 | 0: "PERSISTENT", 111 | 1: "EPHEMERAL", 112 | 2: "NONE", 113 | } 114 | ReplToken_Persistence_value = map[string]int32{ 115 | "PERSISTENT": 0, 116 | "EPHEMERAL": 1, 117 | "NONE": 2, 118 | } 119 | ) 120 | 121 | func (x ReplToken_Persistence) Enum() *ReplToken_Persistence { 122 | p := new(ReplToken_Persistence) 123 | *p = x 124 | return p 125 | } 126 | 127 | func (x ReplToken_Persistence) String() string { 128 | return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) 129 | } 130 | 131 | func (ReplToken_Persistence) Descriptor() protoreflect.EnumDescriptor { 132 | return file_protos_external_goval_api_client_proto_enumTypes[1].Descriptor() 133 | } 134 | 135 | func (ReplToken_Persistence) Type() protoreflect.EnumType { 136 | return &file_protos_external_goval_api_client_proto_enumTypes[1] 137 | } 138 | 139 | func (x ReplToken_Persistence) Number() protoreflect.EnumNumber { 140 | return protoreflect.EnumNumber(x) 141 | } 142 | 143 | // Deprecated: Use ReplToken_Persistence.Descriptor instead. 144 | func (ReplToken_Persistence) EnumDescriptor() ([]byte, []int) { 145 | return file_protos_external_goval_api_client_proto_rawDescGZIP(), []int{3, 0} 146 | } 147 | 148 | // allows the client to choose a wire format. 149 | type ReplToken_WireFormat int32 150 | 151 | const ( 152 | // The default wire format: Protobuf-over-WebSocket. 153 | ReplToken_PROTOBUF ReplToken_WireFormat = 0 154 | // Legacy protocol. 155 | // 156 | // Deprecated: Marked as deprecated in protos/external/goval/api/client.proto. 157 | ReplToken_JSON ReplToken_WireFormat = 1 158 | ) 159 | 160 | // Enum value maps for ReplToken_WireFormat. 161 | var ( 162 | ReplToken_WireFormat_name = map[int32]string{ 163 | 0: "PROTOBUF", 164 | 1: "JSON", 165 | } 166 | ReplToken_WireFormat_value = map[string]int32{ 167 | "PROTOBUF": 0, 168 | "JSON": 1, 169 | } 170 | ) 171 | 172 | func (x ReplToken_WireFormat) Enum() *ReplToken_WireFormat { 173 | p := new(ReplToken_WireFormat) 174 | *p = x 175 | return p 176 | } 177 | 178 | func (x ReplToken_WireFormat) String() string { 179 | return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) 180 | } 181 | 182 | func (ReplToken_WireFormat) Descriptor() protoreflect.EnumDescriptor { 183 | return file_protos_external_goval_api_client_proto_enumTypes[2].Descriptor() 184 | } 185 | 186 | func (ReplToken_WireFormat) Type() protoreflect.EnumType { 187 | return &file_protos_external_goval_api_client_proto_enumTypes[2] 188 | } 189 | 190 | func (x ReplToken_WireFormat) Number() protoreflect.EnumNumber { 191 | return protoreflect.EnumNumber(x) 192 | } 193 | 194 | // Deprecated: Use ReplToken_WireFormat.Descriptor instead. 195 | func (ReplToken_WireFormat) EnumDescriptor() ([]byte, []int) { 196 | return file_protos_external_goval_api_client_proto_rawDescGZIP(), []int{3, 1} 197 | } 198 | 199 | // This message constitutes the repl metadata and define the repl we're 200 | // connecting to. All fields are required unless otherwise stated. 201 | type Repl struct { 202 | state protoimpl.MessageState 203 | sizeCache protoimpl.SizeCache 204 | unknownFields protoimpl.UnknownFields 205 | 206 | Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` 207 | Language string `protobuf:"bytes,2,opt,name=language,proto3" json:"language,omitempty"` 208 | Bucket string `protobuf:"bytes,3,opt,name=bucket,proto3" json:"bucket,omitempty"` 209 | Slug string `protobuf:"bytes,4,opt,name=slug,proto3" json:"slug,omitempty"` 210 | User string `protobuf:"bytes,5,opt,name=user,proto3" json:"user,omitempty"` 211 | // (Optional) The replID of a repl to be used as the source filesystem. All 212 | // writes will still go to the actual repl. This is intended to be a 213 | // replacement for guest repls, giving us cheap COW semantics so all 214 | // connections can have a real repl. 215 | // 216 | // One exception: 217 | // 218 | // It's important to note that data is not implicitly copied from src to 219 | // dest. Only what is explicitly written when talking to pid1 (either 220 | // gcsfiles or snapshots) will persist. This makes it slightly different 221 | // than just forking. 222 | // 223 | // It's unclear what the behaviour should be if: 224 | // - the dest and src repl both exist 225 | // - the dest and src are the same 226 | // - we have an src but no dest 227 | // consider these unsupported/undefined for now. 228 | SourceRepl string `protobuf:"bytes,6,opt,name=sourceRepl,proto3" json:"sourceRepl,omitempty"` 229 | } 230 | 231 | func (x *Repl) Reset() { 232 | *x = Repl{} 233 | if protoimpl.UnsafeEnabled { 234 | mi := &file_protos_external_goval_api_client_proto_msgTypes[0] 235 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 236 | ms.StoreMessageInfo(mi) 237 | } 238 | } 239 | 240 | func (x *Repl) String() string { 241 | return protoimpl.X.MessageStringOf(x) 242 | } 243 | 244 | func (*Repl) ProtoMessage() {} 245 | 246 | func (x *Repl) ProtoReflect() protoreflect.Message { 247 | mi := &file_protos_external_goval_api_client_proto_msgTypes[0] 248 | if protoimpl.UnsafeEnabled && x != nil { 249 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 250 | if ms.LoadMessageInfo() == nil { 251 | ms.StoreMessageInfo(mi) 252 | } 253 | return ms 254 | } 255 | return mi.MessageOf(x) 256 | } 257 | 258 | // Deprecated: Use Repl.ProtoReflect.Descriptor instead. 259 | func (*Repl) Descriptor() ([]byte, []int) { 260 | return file_protos_external_goval_api_client_proto_rawDescGZIP(), []int{0} 261 | } 262 | 263 | func (x *Repl) GetId() string { 264 | if x != nil { 265 | return x.Id 266 | } 267 | return "" 268 | } 269 | 270 | func (x *Repl) GetLanguage() string { 271 | if x != nil { 272 | return x.Language 273 | } 274 | return "" 275 | } 276 | 277 | func (x *Repl) GetBucket() string { 278 | if x != nil { 279 | return x.Bucket 280 | } 281 | return "" 282 | } 283 | 284 | func (x *Repl) GetSlug() string { 285 | if x != nil { 286 | return x.Slug 287 | } 288 | return "" 289 | } 290 | 291 | func (x *Repl) GetUser() string { 292 | if x != nil { 293 | return x.User 294 | } 295 | return "" 296 | } 297 | 298 | func (x *Repl) GetSourceRepl() string { 299 | if x != nil { 300 | return x.SourceRepl 301 | } 302 | return "" 303 | } 304 | 305 | // The resource limits that should be applied to the Repl's container. 306 | type ResourceLimits struct { 307 | state protoimpl.MessageState 308 | sizeCache protoimpl.SizeCache 309 | unknownFields protoimpl.UnknownFields 310 | 311 | // Whether the repl has network access. 312 | Net bool `protobuf:"varint,1,opt,name=net,proto3" json:"net,omitempty"` 313 | // The amount of RAM in bytes that this repl will have. 314 | Memory int64 `protobuf:"varint,2,opt,name=memory,proto3" json:"memory,omitempty"` 315 | // The number of cores that the container will be allowed to have. 316 | Threads float64 `protobuf:"fixed64,3,opt,name=threads,proto3" json:"threads,omitempty"` 317 | // The Docker container weight factor for the scheduler. Similar to the 318 | // `--cpu-shares` commandline flag. 319 | Shares float64 `protobuf:"fixed64,4,opt,name=shares,proto3" json:"shares,omitempty"` 320 | // The size of the disk in bytes. 321 | Disk int64 `protobuf:"varint,5,opt,name=disk,proto3" json:"disk,omitempty"` 322 | Cache ResourceLimits_Cachability `protobuf:"varint,6,opt,name=cache,proto3,enum=api.ResourceLimits_Cachability" json:"cache,omitempty"` 323 | // If set, apply a restrictive allowlist-based network policy to the container 324 | // The container will only be able to communicate with the minimum domains 325 | // necessary to make Replit work, such as package managers. 326 | RestrictNetwork bool `protobuf:"varint,7,opt,name=restrictNetwork,proto3" json:"restrictNetwork,omitempty"` 327 | } 328 | 329 | func (x *ResourceLimits) Reset() { 330 | *x = ResourceLimits{} 331 | if protoimpl.UnsafeEnabled { 332 | mi := &file_protos_external_goval_api_client_proto_msgTypes[1] 333 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 334 | ms.StoreMessageInfo(mi) 335 | } 336 | } 337 | 338 | func (x *ResourceLimits) String() string { 339 | return protoimpl.X.MessageStringOf(x) 340 | } 341 | 342 | func (*ResourceLimits) ProtoMessage() {} 343 | 344 | func (x *ResourceLimits) ProtoReflect() protoreflect.Message { 345 | mi := &file_protos_external_goval_api_client_proto_msgTypes[1] 346 | if protoimpl.UnsafeEnabled && x != nil { 347 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 348 | if ms.LoadMessageInfo() == nil { 349 | ms.StoreMessageInfo(mi) 350 | } 351 | return ms 352 | } 353 | return mi.MessageOf(x) 354 | } 355 | 356 | // Deprecated: Use ResourceLimits.ProtoReflect.Descriptor instead. 357 | func (*ResourceLimits) Descriptor() ([]byte, []int) { 358 | return file_protos_external_goval_api_client_proto_rawDescGZIP(), []int{1} 359 | } 360 | 361 | func (x *ResourceLimits) GetNet() bool { 362 | if x != nil { 363 | return x.Net 364 | } 365 | return false 366 | } 367 | 368 | func (x *ResourceLimits) GetMemory() int64 { 369 | if x != nil { 370 | return x.Memory 371 | } 372 | return 0 373 | } 374 | 375 | func (x *ResourceLimits) GetThreads() float64 { 376 | if x != nil { 377 | return x.Threads 378 | } 379 | return 0 380 | } 381 | 382 | func (x *ResourceLimits) GetShares() float64 { 383 | if x != nil { 384 | return x.Shares 385 | } 386 | return 0 387 | } 388 | 389 | func (x *ResourceLimits) GetDisk() int64 { 390 | if x != nil { 391 | return x.Disk 392 | } 393 | return 0 394 | } 395 | 396 | func (x *ResourceLimits) GetCache() ResourceLimits_Cachability { 397 | if x != nil { 398 | return x.Cache 399 | } 400 | return ResourceLimits_NONE 401 | } 402 | 403 | func (x *ResourceLimits) GetRestrictNetwork() bool { 404 | if x != nil { 405 | return x.RestrictNetwork 406 | } 407 | return false 408 | } 409 | 410 | // Permissions allow tokens to perform certain actions. 411 | type Permissions struct { 412 | state protoimpl.MessageState 413 | sizeCache protoimpl.SizeCache 414 | unknownFields protoimpl.UnknownFields 415 | 416 | // This token has permission to toggle the always on state of a container. 417 | // For a connection to send the AlwaysOn message, it must have this permission. 418 | ToggleAlwaysOn bool `protobuf:"varint,1,opt,name=toggleAlwaysOn,proto3" json:"toggleAlwaysOn,omitempty"` 419 | } 420 | 421 | func (x *Permissions) Reset() { 422 | *x = Permissions{} 423 | if protoimpl.UnsafeEnabled { 424 | mi := &file_protos_external_goval_api_client_proto_msgTypes[2] 425 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 426 | ms.StoreMessageInfo(mi) 427 | } 428 | } 429 | 430 | func (x *Permissions) String() string { 431 | return protoimpl.X.MessageStringOf(x) 432 | } 433 | 434 | func (*Permissions) ProtoMessage() {} 435 | 436 | func (x *Permissions) ProtoReflect() protoreflect.Message { 437 | mi := &file_protos_external_goval_api_client_proto_msgTypes[2] 438 | if protoimpl.UnsafeEnabled && x != nil { 439 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 440 | if ms.LoadMessageInfo() == nil { 441 | ms.StoreMessageInfo(mi) 442 | } 443 | return ms 444 | } 445 | return mi.MessageOf(x) 446 | } 447 | 448 | // Deprecated: Use Permissions.ProtoReflect.Descriptor instead. 449 | func (*Permissions) Descriptor() ([]byte, []int) { 450 | return file_protos_external_goval_api_client_proto_rawDescGZIP(), []int{2} 451 | } 452 | 453 | func (x *Permissions) GetToggleAlwaysOn() bool { 454 | if x != nil { 455 | return x.ToggleAlwaysOn 456 | } 457 | return false 458 | } 459 | 460 | // ReplToken is the expected client options during the handshake. This is encoded 461 | // into the token that is used to connect using WebSocket. 462 | type ReplToken struct { 463 | state protoimpl.MessageState 464 | sizeCache protoimpl.SizeCache 465 | unknownFields protoimpl.UnknownFields 466 | 467 | // Issue timestamp. Equivalent to JWT's "iat" (Issued At) claim. Tokens with 468 | // no `iat` field will be treated as if they had been issed at the UNIX epoch 469 | // (1970-01-01T00:00:00Z). 470 | Iat *timestamppb.Timestamp `protobuf:"bytes,1,opt,name=iat,proto3" json:"iat,omitempty"` 471 | // Expiration timestamp. Equivalent to JWT's "exp" (Expiration Time) Claim. 472 | // If unset, will default to one hour after `iat`. 473 | Exp *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=exp,proto3" json:"exp,omitempty"` 474 | // An arbitrary string that helps prevent replay attacks by ensuring that all 475 | // tokens are distinct. 476 | Salt string `protobuf:"bytes,3,opt,name=salt,proto3" json:"salt,omitempty"` 477 | // The cluster that a repl is located in. This prevents replay attacks in 478 | // which a user is given a token for one cluster and then presents that same 479 | // token to a conman instance in another token, which could lead to a case 480 | // where multiple containers are associated with a repl. 481 | // 482 | // Conman therefore needs to validate that this parameter matches the 483 | // `-cluster` flag it was started with. 484 | Cluster string `protobuf:"bytes,4,opt,name=cluster,proto3" json:"cluster,omitempty"` 485 | // Whether to persist filesystem, metadata, or both. When connecting to an 486 | // already running/existing repl, its settings will be updated to match this 487 | // mode. 488 | Persistence ReplToken_Persistence `protobuf:"varint,6,opt,name=persistence,proto3,enum=api.ReplToken_Persistence" json:"persistence,omitempty"` 489 | // One of the three ways to identify a repl in goval. 490 | // 491 | // Types that are assignable to Metadata: 492 | // 493 | // *ReplToken_Repl 494 | // *ReplToken_Id 495 | // *ReplToken_Classroom 496 | Metadata isReplToken_Metadata `protobuf_oneof:"metadata"` 497 | // The resource limits for the container. 498 | ResourceLimits *ResourceLimits `protobuf:"bytes,10,opt,name=resourceLimits,proto3" json:"resourceLimits,omitempty"` 499 | Format ReplToken_WireFormat `protobuf:"varint,12,opt,name=format,proto3,enum=api.ReplToken_WireFormat" json:"format,omitempty"` 500 | Presenced *ReplToken_Presenced `protobuf:"bytes,13,opt,name=presenced,proto3" json:"presenced,omitempty"` 501 | // Flags are handy for passing arbitrary configs along. Mostly used so 502 | // the client can try out new features 503 | Flags []string `protobuf:"bytes,14,rep,name=flags,proto3" json:"flags,omitempty"` 504 | Permissions *Permissions `protobuf:"bytes,15,opt,name=permissions,proto3" json:"permissions,omitempty"` 505 | } 506 | 507 | func (x *ReplToken) Reset() { 508 | *x = ReplToken{} 509 | if protoimpl.UnsafeEnabled { 510 | mi := &file_protos_external_goval_api_client_proto_msgTypes[3] 511 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 512 | ms.StoreMessageInfo(mi) 513 | } 514 | } 515 | 516 | func (x *ReplToken) String() string { 517 | return protoimpl.X.MessageStringOf(x) 518 | } 519 | 520 | func (*ReplToken) ProtoMessage() {} 521 | 522 | func (x *ReplToken) ProtoReflect() protoreflect.Message { 523 | mi := &file_protos_external_goval_api_client_proto_msgTypes[3] 524 | if protoimpl.UnsafeEnabled && x != nil { 525 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 526 | if ms.LoadMessageInfo() == nil { 527 | ms.StoreMessageInfo(mi) 528 | } 529 | return ms 530 | } 531 | return mi.MessageOf(x) 532 | } 533 | 534 | // Deprecated: Use ReplToken.ProtoReflect.Descriptor instead. 535 | func (*ReplToken) Descriptor() ([]byte, []int) { 536 | return file_protos_external_goval_api_client_proto_rawDescGZIP(), []int{3} 537 | } 538 | 539 | func (x *ReplToken) GetIat() *timestamppb.Timestamp { 540 | if x != nil { 541 | return x.Iat 542 | } 543 | return nil 544 | } 545 | 546 | func (x *ReplToken) GetExp() *timestamppb.Timestamp { 547 | if x != nil { 548 | return x.Exp 549 | } 550 | return nil 551 | } 552 | 553 | func (x *ReplToken) GetSalt() string { 554 | if x != nil { 555 | return x.Salt 556 | } 557 | return "" 558 | } 559 | 560 | func (x *ReplToken) GetCluster() string { 561 | if x != nil { 562 | return x.Cluster 563 | } 564 | return "" 565 | } 566 | 567 | func (x *ReplToken) GetPersistence() ReplToken_Persistence { 568 | if x != nil { 569 | return x.Persistence 570 | } 571 | return ReplToken_PERSISTENT 572 | } 573 | 574 | func (m *ReplToken) GetMetadata() isReplToken_Metadata { 575 | if m != nil { 576 | return m.Metadata 577 | } 578 | return nil 579 | } 580 | 581 | func (x *ReplToken) GetRepl() *Repl { 582 | if x, ok := x.GetMetadata().(*ReplToken_Repl); ok { 583 | return x.Repl 584 | } 585 | return nil 586 | } 587 | 588 | func (x *ReplToken) GetId() *ReplToken_ReplID { 589 | if x, ok := x.GetMetadata().(*ReplToken_Id); ok { 590 | return x.Id 591 | } 592 | return nil 593 | } 594 | 595 | // Deprecated: Marked as deprecated in protos/external/goval/api/client.proto. 596 | func (x *ReplToken) GetClassroom() *ReplToken_ClassroomMetadata { 597 | if x, ok := x.GetMetadata().(*ReplToken_Classroom); ok { 598 | return x.Classroom 599 | } 600 | return nil 601 | } 602 | 603 | func (x *ReplToken) GetResourceLimits() *ResourceLimits { 604 | if x != nil { 605 | return x.ResourceLimits 606 | } 607 | return nil 608 | } 609 | 610 | func (x *ReplToken) GetFormat() ReplToken_WireFormat { 611 | if x != nil { 612 | return x.Format 613 | } 614 | return ReplToken_PROTOBUF 615 | } 616 | 617 | func (x *ReplToken) GetPresenced() *ReplToken_Presenced { 618 | if x != nil { 619 | return x.Presenced 620 | } 621 | return nil 622 | } 623 | 624 | func (x *ReplToken) GetFlags() []string { 625 | if x != nil { 626 | return x.Flags 627 | } 628 | return nil 629 | } 630 | 631 | func (x *ReplToken) GetPermissions() *Permissions { 632 | if x != nil { 633 | return x.Permissions 634 | } 635 | return nil 636 | } 637 | 638 | type isReplToken_Metadata interface { 639 | isReplToken_Metadata() 640 | } 641 | 642 | type ReplToken_Repl struct { 643 | // This is the standard connection behavior. If the repl doesn't exist it 644 | // will be created. Any future connections with a matching ID will go to 645 | // the same container. If other metadata mismatches besides ID it will be 646 | // rectified (typically by recreating the container to make it match the 647 | // provided value). 648 | Repl *Repl `protobuf:"bytes,7,opt,name=repl,proto3,oneof"` 649 | } 650 | 651 | type ReplToken_Id struct { 652 | // The repl must already be known to goval, the connection will proceed 653 | // with the Repl metadata from a previous connection's metadata with the 654 | // same ID. 655 | Id *ReplToken_ReplID `protobuf:"bytes,8,opt,name=id,proto3,oneof"` 656 | } 657 | 658 | type ReplToken_Classroom struct { 659 | // This is DEPRECATED and only used by the classroom. This will never share 660 | // a container between connections. Please don't use this even for tests, 661 | // we intend to remove it soon. 662 | // 663 | // Deprecated: Marked as deprecated in protos/external/goval/api/client.proto. 664 | Classroom *ReplToken_ClassroomMetadata `protobuf:"bytes,9,opt,name=classroom,proto3,oneof"` 665 | } 666 | 667 | func (*ReplToken_Repl) isReplToken_Metadata() {} 668 | 669 | func (*ReplToken_Id) isReplToken_Metadata() {} 670 | 671 | func (*ReplToken_Classroom) isReplToken_Metadata() {} 672 | 673 | // TLSCertificate is a SSL/TLS certificate for a specific domain. This is used to transfer 674 | // certificates between clusters when a repl is transferred so we do not need to request 675 | // a new certificate in the new cluster. 676 | type TLSCertificate struct { 677 | state protoimpl.MessageState 678 | sizeCache protoimpl.SizeCache 679 | unknownFields protoimpl.UnknownFields 680 | 681 | Domain string `protobuf:"bytes,1,opt,name=domain,proto3" json:"domain,omitempty"` 682 | Cert []byte `protobuf:"bytes,2,opt,name=cert,proto3" json:"cert,omitempty"` 683 | } 684 | 685 | func (x *TLSCertificate) Reset() { 686 | *x = TLSCertificate{} 687 | if protoimpl.UnsafeEnabled { 688 | mi := &file_protos_external_goval_api_client_proto_msgTypes[4] 689 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 690 | ms.StoreMessageInfo(mi) 691 | } 692 | } 693 | 694 | func (x *TLSCertificate) String() string { 695 | return protoimpl.X.MessageStringOf(x) 696 | } 697 | 698 | func (*TLSCertificate) ProtoMessage() {} 699 | 700 | func (x *TLSCertificate) ProtoReflect() protoreflect.Message { 701 | mi := &file_protos_external_goval_api_client_proto_msgTypes[4] 702 | if protoimpl.UnsafeEnabled && x != nil { 703 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 704 | if ms.LoadMessageInfo() == nil { 705 | ms.StoreMessageInfo(mi) 706 | } 707 | return ms 708 | } 709 | return mi.MessageOf(x) 710 | } 711 | 712 | // Deprecated: Use TLSCertificate.ProtoReflect.Descriptor instead. 713 | func (*TLSCertificate) Descriptor() ([]byte, []int) { 714 | return file_protos_external_goval_api_client_proto_rawDescGZIP(), []int{4} 715 | } 716 | 717 | func (x *TLSCertificate) GetDomain() string { 718 | if x != nil { 719 | return x.Domain 720 | } 721 | return "" 722 | } 723 | 724 | func (x *TLSCertificate) GetCert() []byte { 725 | if x != nil { 726 | return x.Cert 727 | } 728 | return nil 729 | } 730 | 731 | // ReplTransfer includes all the data needed to transfer a repl between clusters. 732 | type ReplTransfer struct { 733 | state protoimpl.MessageState 734 | sizeCache protoimpl.SizeCache 735 | unknownFields protoimpl.UnknownFields 736 | 737 | Repl *Repl `protobuf:"bytes,1,opt,name=repl,proto3" json:"repl,omitempty"` 738 | ReplLimits *ResourceLimits `protobuf:"bytes,2,opt,name=replLimits,proto3" json:"replLimits,omitempty"` 739 | UserLimits *ResourceLimits `protobuf:"bytes,3,opt,name=userLimits,proto3" json:"userLimits,omitempty"` 740 | CustomDomains []string `protobuf:"bytes,4,rep,name=customDomains,proto3" json:"customDomains,omitempty"` 741 | Certificates []*TLSCertificate `protobuf:"bytes,5,rep,name=certificates,proto3" json:"certificates,omitempty"` 742 | Flags []string `protobuf:"bytes,6,rep,name=flags,proto3" json:"flags,omitempty"` 743 | } 744 | 745 | func (x *ReplTransfer) Reset() { 746 | *x = ReplTransfer{} 747 | if protoimpl.UnsafeEnabled { 748 | mi := &file_protos_external_goval_api_client_proto_msgTypes[5] 749 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 750 | ms.StoreMessageInfo(mi) 751 | } 752 | } 753 | 754 | func (x *ReplTransfer) String() string { 755 | return protoimpl.X.MessageStringOf(x) 756 | } 757 | 758 | func (*ReplTransfer) ProtoMessage() {} 759 | 760 | func (x *ReplTransfer) ProtoReflect() protoreflect.Message { 761 | mi := &file_protos_external_goval_api_client_proto_msgTypes[5] 762 | if protoimpl.UnsafeEnabled && x != nil { 763 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 764 | if ms.LoadMessageInfo() == nil { 765 | ms.StoreMessageInfo(mi) 766 | } 767 | return ms 768 | } 769 | return mi.MessageOf(x) 770 | } 771 | 772 | // Deprecated: Use ReplTransfer.ProtoReflect.Descriptor instead. 773 | func (*ReplTransfer) Descriptor() ([]byte, []int) { 774 | return file_protos_external_goval_api_client_proto_rawDescGZIP(), []int{5} 775 | } 776 | 777 | func (x *ReplTransfer) GetRepl() *Repl { 778 | if x != nil { 779 | return x.Repl 780 | } 781 | return nil 782 | } 783 | 784 | func (x *ReplTransfer) GetReplLimits() *ResourceLimits { 785 | if x != nil { 786 | return x.ReplLimits 787 | } 788 | return nil 789 | } 790 | 791 | func (x *ReplTransfer) GetUserLimits() *ResourceLimits { 792 | if x != nil { 793 | return x.UserLimits 794 | } 795 | return nil 796 | } 797 | 798 | func (x *ReplTransfer) GetCustomDomains() []string { 799 | if x != nil { 800 | return x.CustomDomains 801 | } 802 | return nil 803 | } 804 | 805 | func (x *ReplTransfer) GetCertificates() []*TLSCertificate { 806 | if x != nil { 807 | return x.Certificates 808 | } 809 | return nil 810 | } 811 | 812 | func (x *ReplTransfer) GetFlags() []string { 813 | if x != nil { 814 | return x.Flags 815 | } 816 | return nil 817 | } 818 | 819 | // AllowReplRequest represents a request to allow a repl into a cluster. 820 | type AllowReplRequest struct { 821 | state protoimpl.MessageState 822 | sizeCache protoimpl.SizeCache 823 | unknownFields protoimpl.UnknownFields 824 | 825 | ReplTransfer *ReplTransfer `protobuf:"bytes,1,opt,name=replTransfer,proto3" json:"replTransfer,omitempty"` 826 | } 827 | 828 | func (x *AllowReplRequest) Reset() { 829 | *x = AllowReplRequest{} 830 | if protoimpl.UnsafeEnabled { 831 | mi := &file_protos_external_goval_api_client_proto_msgTypes[6] 832 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 833 | ms.StoreMessageInfo(mi) 834 | } 835 | } 836 | 837 | func (x *AllowReplRequest) String() string { 838 | return protoimpl.X.MessageStringOf(x) 839 | } 840 | 841 | func (*AllowReplRequest) ProtoMessage() {} 842 | 843 | func (x *AllowReplRequest) ProtoReflect() protoreflect.Message { 844 | mi := &file_protos_external_goval_api_client_proto_msgTypes[6] 845 | if protoimpl.UnsafeEnabled && x != nil { 846 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 847 | if ms.LoadMessageInfo() == nil { 848 | ms.StoreMessageInfo(mi) 849 | } 850 | return ms 851 | } 852 | return mi.MessageOf(x) 853 | } 854 | 855 | // Deprecated: Use AllowReplRequest.ProtoReflect.Descriptor instead. 856 | func (*AllowReplRequest) Descriptor() ([]byte, []int) { 857 | return file_protos_external_goval_api_client_proto_rawDescGZIP(), []int{6} 858 | } 859 | 860 | func (x *AllowReplRequest) GetReplTransfer() *ReplTransfer { 861 | if x != nil { 862 | return x.ReplTransfer 863 | } 864 | return nil 865 | } 866 | 867 | // ClusterMetadata represents all the metadata Lore knows about a cluster. This 868 | // includes all endpoints needed to communicate with the cluster. 869 | type ClusterMetadata struct { 870 | state protoimpl.MessageState 871 | sizeCache protoimpl.SizeCache 872 | unknownFields protoimpl.UnknownFields 873 | 874 | Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` 875 | ConmanURL string `protobuf:"bytes,2,opt,name=conmanURL,proto3" json:"conmanURL,omitempty"` 876 | Gurl string `protobuf:"bytes,3,opt,name=gurl,proto3" json:"gurl,omitempty"` 877 | Proxy string `protobuf:"bytes,5,opt,name=proxy,proto3" json:"proxy,omitempty"` 878 | } 879 | 880 | func (x *ClusterMetadata) Reset() { 881 | *x = ClusterMetadata{} 882 | if protoimpl.UnsafeEnabled { 883 | mi := &file_protos_external_goval_api_client_proto_msgTypes[7] 884 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 885 | ms.StoreMessageInfo(mi) 886 | } 887 | } 888 | 889 | func (x *ClusterMetadata) String() string { 890 | return protoimpl.X.MessageStringOf(x) 891 | } 892 | 893 | func (*ClusterMetadata) ProtoMessage() {} 894 | 895 | func (x *ClusterMetadata) ProtoReflect() protoreflect.Message { 896 | mi := &file_protos_external_goval_api_client_proto_msgTypes[7] 897 | if protoimpl.UnsafeEnabled && x != nil { 898 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 899 | if ms.LoadMessageInfo() == nil { 900 | ms.StoreMessageInfo(mi) 901 | } 902 | return ms 903 | } 904 | return mi.MessageOf(x) 905 | } 906 | 907 | // Deprecated: Use ClusterMetadata.ProtoReflect.Descriptor instead. 908 | func (*ClusterMetadata) Descriptor() ([]byte, []int) { 909 | return file_protos_external_goval_api_client_proto_rawDescGZIP(), []int{7} 910 | } 911 | 912 | func (x *ClusterMetadata) GetId() string { 913 | if x != nil { 914 | return x.Id 915 | } 916 | return "" 917 | } 918 | 919 | func (x *ClusterMetadata) GetConmanURL() string { 920 | if x != nil { 921 | return x.ConmanURL 922 | } 923 | return "" 924 | } 925 | 926 | func (x *ClusterMetadata) GetGurl() string { 927 | if x != nil { 928 | return x.Gurl 929 | } 930 | return "" 931 | } 932 | 933 | func (x *ClusterMetadata) GetProxy() string { 934 | if x != nil { 935 | return x.Proxy 936 | } 937 | return "" 938 | } 939 | 940 | // EvictReplRequest represents a request to evict a repl from a cluster. 941 | // Includes the metadata about the repl that will be evicted and a token in 942 | // case conman needs to forward this request to another instance. 943 | type EvictReplRequest struct { 944 | state protoimpl.MessageState 945 | sizeCache protoimpl.SizeCache 946 | unknownFields protoimpl.UnknownFields 947 | 948 | ClusterMetadata *ClusterMetadata `protobuf:"bytes,1,opt,name=clusterMetadata,proto3" json:"clusterMetadata,omitempty"` 949 | Token string `protobuf:"bytes,2,opt,name=token,proto3" json:"token,omitempty"` 950 | // User and slug are sent so that a repl route can be added even if the cluster 951 | // doesn't have metadata for this repl. 952 | User string `protobuf:"bytes,3,opt,name=user,proto3" json:"user,omitempty"` 953 | Slug string `protobuf:"bytes,4,opt,name=slug,proto3" json:"slug,omitempty"` 954 | } 955 | 956 | func (x *EvictReplRequest) Reset() { 957 | *x = EvictReplRequest{} 958 | if protoimpl.UnsafeEnabled { 959 | mi := &file_protos_external_goval_api_client_proto_msgTypes[8] 960 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 961 | ms.StoreMessageInfo(mi) 962 | } 963 | } 964 | 965 | func (x *EvictReplRequest) String() string { 966 | return protoimpl.X.MessageStringOf(x) 967 | } 968 | 969 | func (*EvictReplRequest) ProtoMessage() {} 970 | 971 | func (x *EvictReplRequest) ProtoReflect() protoreflect.Message { 972 | mi := &file_protos_external_goval_api_client_proto_msgTypes[8] 973 | if protoimpl.UnsafeEnabled && x != nil { 974 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 975 | if ms.LoadMessageInfo() == nil { 976 | ms.StoreMessageInfo(mi) 977 | } 978 | return ms 979 | } 980 | return mi.MessageOf(x) 981 | } 982 | 983 | // Deprecated: Use EvictReplRequest.ProtoReflect.Descriptor instead. 984 | func (*EvictReplRequest) Descriptor() ([]byte, []int) { 985 | return file_protos_external_goval_api_client_proto_rawDescGZIP(), []int{8} 986 | } 987 | 988 | func (x *EvictReplRequest) GetClusterMetadata() *ClusterMetadata { 989 | if x != nil { 990 | return x.ClusterMetadata 991 | } 992 | return nil 993 | } 994 | 995 | func (x *EvictReplRequest) GetToken() string { 996 | if x != nil { 997 | return x.Token 998 | } 999 | return "" 1000 | } 1001 | 1002 | func (x *EvictReplRequest) GetUser() string { 1003 | if x != nil { 1004 | return x.User 1005 | } 1006 | return "" 1007 | } 1008 | 1009 | func (x *EvictReplRequest) GetSlug() string { 1010 | if x != nil { 1011 | return x.Slug 1012 | } 1013 | return "" 1014 | } 1015 | 1016 | // EvictReplResponse represents a response after evicting a repl from a cluster and includes 1017 | // metadata about the repl that was evicted. 1018 | type EvictReplResponse struct { 1019 | state protoimpl.MessageState 1020 | sizeCache protoimpl.SizeCache 1021 | unknownFields protoimpl.UnknownFields 1022 | 1023 | ReplTransfer *ReplTransfer `protobuf:"bytes,1,opt,name=replTransfer,proto3" json:"replTransfer,omitempty"` 1024 | } 1025 | 1026 | func (x *EvictReplResponse) Reset() { 1027 | *x = EvictReplResponse{} 1028 | if protoimpl.UnsafeEnabled { 1029 | mi := &file_protos_external_goval_api_client_proto_msgTypes[9] 1030 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 1031 | ms.StoreMessageInfo(mi) 1032 | } 1033 | } 1034 | 1035 | func (x *EvictReplResponse) String() string { 1036 | return protoimpl.X.MessageStringOf(x) 1037 | } 1038 | 1039 | func (*EvictReplResponse) ProtoMessage() {} 1040 | 1041 | func (x *EvictReplResponse) ProtoReflect() protoreflect.Message { 1042 | mi := &file_protos_external_goval_api_client_proto_msgTypes[9] 1043 | if protoimpl.UnsafeEnabled && x != nil { 1044 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 1045 | if ms.LoadMessageInfo() == nil { 1046 | ms.StoreMessageInfo(mi) 1047 | } 1048 | return ms 1049 | } 1050 | return mi.MessageOf(x) 1051 | } 1052 | 1053 | // Deprecated: Use EvictReplResponse.ProtoReflect.Descriptor instead. 1054 | func (*EvictReplResponse) Descriptor() ([]byte, []int) { 1055 | return file_protos_external_goval_api_client_proto_rawDescGZIP(), []int{9} 1056 | } 1057 | 1058 | func (x *EvictReplResponse) GetReplTransfer() *ReplTransfer { 1059 | if x != nil { 1060 | return x.ReplTransfer 1061 | } 1062 | return nil 1063 | } 1064 | 1065 | // Metadata for the classroom. This is deprecated and should be removed 1066 | // hopefully soon. 1067 | type ReplToken_ClassroomMetadata struct { 1068 | state protoimpl.MessageState 1069 | sizeCache protoimpl.SizeCache 1070 | unknownFields protoimpl.UnknownFields 1071 | 1072 | Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` 1073 | Language string `protobuf:"bytes,2,opt,name=language,proto3" json:"language,omitempty"` 1074 | } 1075 | 1076 | func (x *ReplToken_ClassroomMetadata) Reset() { 1077 | *x = ReplToken_ClassroomMetadata{} 1078 | if protoimpl.UnsafeEnabled { 1079 | mi := &file_protos_external_goval_api_client_proto_msgTypes[10] 1080 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 1081 | ms.StoreMessageInfo(mi) 1082 | } 1083 | } 1084 | 1085 | func (x *ReplToken_ClassroomMetadata) String() string { 1086 | return protoimpl.X.MessageStringOf(x) 1087 | } 1088 | 1089 | func (*ReplToken_ClassroomMetadata) ProtoMessage() {} 1090 | 1091 | func (x *ReplToken_ClassroomMetadata) ProtoReflect() protoreflect.Message { 1092 | mi := &file_protos_external_goval_api_client_proto_msgTypes[10] 1093 | if protoimpl.UnsafeEnabled && x != nil { 1094 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 1095 | if ms.LoadMessageInfo() == nil { 1096 | ms.StoreMessageInfo(mi) 1097 | } 1098 | return ms 1099 | } 1100 | return mi.MessageOf(x) 1101 | } 1102 | 1103 | // Deprecated: Use ReplToken_ClassroomMetadata.ProtoReflect.Descriptor instead. 1104 | func (*ReplToken_ClassroomMetadata) Descriptor() ([]byte, []int) { 1105 | return file_protos_external_goval_api_client_proto_rawDescGZIP(), []int{3, 0} 1106 | } 1107 | 1108 | func (x *ReplToken_ClassroomMetadata) GetId() string { 1109 | if x != nil { 1110 | return x.Id 1111 | } 1112 | return "" 1113 | } 1114 | 1115 | func (x *ReplToken_ClassroomMetadata) GetLanguage() string { 1116 | if x != nil { 1117 | return x.Language 1118 | } 1119 | return "" 1120 | } 1121 | 1122 | // Metadata for a repl that is only identified by its id. 1123 | type ReplToken_ReplID struct { 1124 | state protoimpl.MessageState 1125 | sizeCache protoimpl.SizeCache 1126 | unknownFields protoimpl.UnknownFields 1127 | 1128 | Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` 1129 | // (Optional) See the comment for Repl.sourceRepl. 1130 | SourceRepl string `protobuf:"bytes,2,opt,name=sourceRepl,proto3" json:"sourceRepl,omitempty"` 1131 | } 1132 | 1133 | func (x *ReplToken_ReplID) Reset() { 1134 | *x = ReplToken_ReplID{} 1135 | if protoimpl.UnsafeEnabled { 1136 | mi := &file_protos_external_goval_api_client_proto_msgTypes[11] 1137 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 1138 | ms.StoreMessageInfo(mi) 1139 | } 1140 | } 1141 | 1142 | func (x *ReplToken_ReplID) String() string { 1143 | return protoimpl.X.MessageStringOf(x) 1144 | } 1145 | 1146 | func (*ReplToken_ReplID) ProtoMessage() {} 1147 | 1148 | func (x *ReplToken_ReplID) ProtoReflect() protoreflect.Message { 1149 | mi := &file_protos_external_goval_api_client_proto_msgTypes[11] 1150 | if protoimpl.UnsafeEnabled && x != nil { 1151 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 1152 | if ms.LoadMessageInfo() == nil { 1153 | ms.StoreMessageInfo(mi) 1154 | } 1155 | return ms 1156 | } 1157 | return mi.MessageOf(x) 1158 | } 1159 | 1160 | // Deprecated: Use ReplToken_ReplID.ProtoReflect.Descriptor instead. 1161 | func (*ReplToken_ReplID) Descriptor() ([]byte, []int) { 1162 | return file_protos_external_goval_api_client_proto_rawDescGZIP(), []int{3, 1} 1163 | } 1164 | 1165 | func (x *ReplToken_ReplID) GetId() string { 1166 | if x != nil { 1167 | return x.Id 1168 | } 1169 | return "" 1170 | } 1171 | 1172 | func (x *ReplToken_ReplID) GetSourceRepl() string { 1173 | if x != nil { 1174 | return x.SourceRepl 1175 | } 1176 | return "" 1177 | } 1178 | 1179 | type ReplToken_Presenced struct { 1180 | state protoimpl.MessageState 1181 | sizeCache protoimpl.SizeCache 1182 | unknownFields protoimpl.UnknownFields 1183 | 1184 | BearerID uint32 `protobuf:"varint,1,opt,name=bearerID,proto3" json:"bearerID,omitempty"` 1185 | BearerName string `protobuf:"bytes,2,opt,name=bearerName,proto3" json:"bearerName,omitempty"` 1186 | } 1187 | 1188 | func (x *ReplToken_Presenced) Reset() { 1189 | *x = ReplToken_Presenced{} 1190 | if protoimpl.UnsafeEnabled { 1191 | mi := &file_protos_external_goval_api_client_proto_msgTypes[12] 1192 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 1193 | ms.StoreMessageInfo(mi) 1194 | } 1195 | } 1196 | 1197 | func (x *ReplToken_Presenced) String() string { 1198 | return protoimpl.X.MessageStringOf(x) 1199 | } 1200 | 1201 | func (*ReplToken_Presenced) ProtoMessage() {} 1202 | 1203 | func (x *ReplToken_Presenced) ProtoReflect() protoreflect.Message { 1204 | mi := &file_protos_external_goval_api_client_proto_msgTypes[12] 1205 | if protoimpl.UnsafeEnabled && x != nil { 1206 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 1207 | if ms.LoadMessageInfo() == nil { 1208 | ms.StoreMessageInfo(mi) 1209 | } 1210 | return ms 1211 | } 1212 | return mi.MessageOf(x) 1213 | } 1214 | 1215 | // Deprecated: Use ReplToken_Presenced.ProtoReflect.Descriptor instead. 1216 | func (*ReplToken_Presenced) Descriptor() ([]byte, []int) { 1217 | return file_protos_external_goval_api_client_proto_rawDescGZIP(), []int{3, 2} 1218 | } 1219 | 1220 | func (x *ReplToken_Presenced) GetBearerID() uint32 { 1221 | if x != nil { 1222 | return x.BearerID 1223 | } 1224 | return 0 1225 | } 1226 | 1227 | func (x *ReplToken_Presenced) GetBearerName() string { 1228 | if x != nil { 1229 | return x.BearerName 1230 | } 1231 | return "" 1232 | } 1233 | 1234 | var File_protos_external_goval_api_client_proto protoreflect.FileDescriptor 1235 | 1236 | var file_protos_external_goval_api_client_proto_rawDesc = []byte{ 1237 | 0x0a, 0x26, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x73, 0x2f, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 1238 | 0x6c, 0x2f, 0x67, 0x6f, 0x76, 0x61, 0x6c, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x63, 0x6c, 0x69, 0x65, 1239 | 0x6e, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x03, 0x61, 0x70, 0x69, 0x1a, 0x1f, 0x67, 1240 | 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 1241 | 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x92, 1242 | 0x01, 0x0a, 0x04, 0x52, 0x65, 0x70, 0x6c, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 1243 | 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x6c, 0x61, 0x6e, 0x67, 0x75, 1244 | 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6c, 0x61, 0x6e, 0x67, 0x75, 1245 | 0x61, 0x67, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x62, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x18, 0x03, 0x20, 1246 | 0x01, 0x28, 0x09, 0x52, 0x06, 0x62, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x73, 1247 | 0x6c, 0x75, 0x67, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x73, 0x6c, 0x75, 0x67, 0x12, 1248 | 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x75, 1249 | 0x73, 0x65, 0x72, 0x12, 0x1e, 0x0a, 0x0a, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x70, 1250 | 0x6c, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 1251 | 0x65, 0x70, 0x6c, 0x22, 0x8e, 0x02, 0x0a, 0x0e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 1252 | 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x6e, 0x65, 0x74, 0x18, 0x01, 0x20, 1253 | 0x01, 0x28, 0x08, 0x52, 0x03, 0x6e, 0x65, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x6d, 0x65, 0x6d, 0x6f, 1254 | 0x72, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x6d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 1255 | 0x12, 0x18, 0x0a, 0x07, 0x74, 0x68, 0x72, 0x65, 0x61, 0x64, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 1256 | 0x01, 0x52, 0x07, 0x74, 0x68, 0x72, 0x65, 0x61, 0x64, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x68, 1257 | 0x61, 0x72, 0x65, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x01, 0x52, 0x06, 0x73, 0x68, 0x61, 0x72, 1258 | 0x65, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x69, 0x73, 0x6b, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 1259 | 0x52, 0x04, 0x64, 0x69, 0x73, 0x6b, 0x12, 0x35, 0x0a, 0x05, 0x63, 0x61, 0x63, 0x68, 0x65, 0x18, 1260 | 0x06, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1f, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x52, 0x65, 0x73, 0x6f, 1261 | 0x75, 0x72, 0x63, 0x65, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x73, 0x2e, 0x43, 0x61, 0x63, 0x68, 0x61, 1262 | 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x52, 0x05, 0x63, 0x61, 0x63, 0x68, 0x65, 0x12, 0x28, 0x0a, 1263 | 0x0f, 0x72, 0x65, 0x73, 0x74, 0x72, 0x69, 0x63, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 1264 | 0x18, 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, 0x72, 0x65, 0x73, 0x74, 0x72, 0x69, 0x63, 0x74, 1265 | 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x22, 0x2b, 0x0a, 0x0b, 0x43, 0x61, 0x63, 0x68, 0x61, 1266 | 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x12, 0x08, 0x0a, 0x04, 0x4e, 0x4f, 0x4e, 0x45, 0x10, 0x00, 1267 | 0x12, 0x08, 0x0a, 0x04, 0x55, 0x53, 0x45, 0x52, 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x52, 0x45, 1268 | 0x50, 0x4c, 0x10, 0x02, 0x22, 0x35, 0x0a, 0x0b, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 1269 | 0x6f, 0x6e, 0x73, 0x12, 0x26, 0x0a, 0x0e, 0x74, 0x6f, 0x67, 0x67, 0x6c, 0x65, 0x41, 0x6c, 0x77, 1270 | 0x61, 0x79, 0x73, 0x4f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0e, 0x74, 0x6f, 0x67, 1271 | 0x67, 0x6c, 0x65, 0x41, 0x6c, 0x77, 0x61, 0x79, 0x73, 0x4f, 0x6e, 0x22, 0x87, 0x07, 0x0a, 0x09, 1272 | 0x52, 0x65, 0x70, 0x6c, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x2c, 0x0a, 0x03, 0x69, 0x61, 0x74, 1273 | 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 1274 | 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 1275 | 0x6d, 0x70, 0x52, 0x03, 0x69, 0x61, 0x74, 0x12, 0x2c, 0x0a, 0x03, 0x65, 0x78, 0x70, 0x18, 0x02, 1276 | 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 1277 | 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 1278 | 0x52, 0x03, 0x65, 0x78, 0x70, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x61, 0x6c, 0x74, 0x18, 0x03, 0x20, 1279 | 0x01, 0x28, 0x09, 0x52, 0x04, 0x73, 0x61, 0x6c, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6c, 0x75, 1280 | 0x73, 0x74, 0x65, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x6c, 0x75, 0x73, 1281 | 0x74, 0x65, 0x72, 0x12, 0x3c, 0x0a, 0x0b, 0x70, 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 1282 | 0x63, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1a, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x52, 1283 | 0x65, 0x70, 0x6c, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x2e, 0x50, 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 1284 | 0x65, 0x6e, 0x63, 0x65, 0x52, 0x0b, 0x70, 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 1285 | 0x65, 0x12, 0x1f, 0x0a, 0x04, 0x72, 0x65, 0x70, 0x6c, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 1286 | 0x09, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x52, 0x65, 0x70, 0x6c, 0x48, 0x00, 0x52, 0x04, 0x72, 0x65, 1287 | 0x70, 0x6c, 0x12, 0x27, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 1288 | 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x52, 0x65, 0x70, 0x6c, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x2e, 0x52, 1289 | 0x65, 0x70, 0x6c, 0x49, 0x44, 0x48, 0x00, 0x52, 0x02, 0x69, 0x64, 0x12, 0x44, 0x0a, 0x09, 0x63, 1290 | 0x6c, 0x61, 0x73, 0x73, 0x72, 0x6f, 0x6f, 0x6d, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x20, 1291 | 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x52, 0x65, 0x70, 0x6c, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x2e, 0x43, 1292 | 0x6c, 0x61, 0x73, 0x73, 0x72, 0x6f, 0x6f, 0x6d, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 1293 | 0x42, 0x02, 0x18, 0x01, 0x48, 0x00, 0x52, 0x09, 0x63, 0x6c, 0x61, 0x73, 0x73, 0x72, 0x6f, 0x6f, 1294 | 0x6d, 0x12, 0x3b, 0x0a, 0x0e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4c, 0x69, 0x6d, 1295 | 0x69, 0x74, 0x73, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x61, 0x70, 0x69, 0x2e, 1296 | 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x73, 0x52, 0x0e, 1297 | 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x73, 0x12, 0x31, 1298 | 0x0a, 0x06, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x19, 1299 | 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x52, 0x65, 0x70, 0x6c, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x2e, 0x57, 1300 | 0x69, 0x72, 0x65, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x52, 0x06, 0x66, 0x6f, 0x72, 0x6d, 0x61, 1301 | 0x74, 0x12, 0x36, 0x0a, 0x09, 0x70, 0x72, 0x65, 0x73, 0x65, 0x6e, 0x63, 0x65, 0x64, 0x18, 0x0d, 1302 | 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x52, 0x65, 0x70, 0x6c, 0x54, 1303 | 0x6f, 0x6b, 0x65, 0x6e, 0x2e, 0x50, 0x72, 0x65, 0x73, 0x65, 0x6e, 0x63, 0x65, 0x64, 0x52, 0x09, 1304 | 0x70, 0x72, 0x65, 0x73, 0x65, 0x6e, 0x63, 0x65, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x66, 0x6c, 0x61, 1305 | 0x67, 0x73, 0x18, 0x0e, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x66, 0x6c, 0x61, 0x67, 0x73, 0x12, 1306 | 0x32, 0x0a, 0x0b, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x0f, 1307 | 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x50, 0x65, 0x72, 0x6d, 0x69, 1308 | 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x0b, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 1309 | 0x6f, 0x6e, 0x73, 0x1a, 0x3f, 0x0a, 0x11, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x72, 0x6f, 0x6f, 0x6d, 1310 | 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 1311 | 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x6c, 0x61, 0x6e, 0x67, 1312 | 0x75, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6c, 0x61, 0x6e, 0x67, 1313 | 0x75, 0x61, 0x67, 0x65, 0x1a, 0x38, 0x0a, 0x06, 0x52, 0x65, 0x70, 0x6c, 0x49, 0x44, 0x12, 0x0e, 1314 | 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x1e, 1315 | 0x0a, 0x0a, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x70, 0x6c, 0x18, 0x02, 0x20, 0x01, 1316 | 0x28, 0x09, 0x52, 0x0a, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x70, 0x6c, 0x1a, 0x47, 1317 | 0x0a, 0x09, 0x50, 0x72, 0x65, 0x73, 0x65, 0x6e, 0x63, 0x65, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x62, 1318 | 0x65, 0x61, 0x72, 0x65, 0x72, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x08, 0x62, 1319 | 0x65, 0x61, 0x72, 0x65, 0x72, 0x49, 0x44, 0x12, 0x1e, 0x0a, 0x0a, 0x62, 0x65, 0x61, 0x72, 0x65, 1320 | 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x62, 0x65, 0x61, 1321 | 0x72, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x22, 0x36, 0x0a, 0x0b, 0x50, 0x65, 0x72, 0x73, 0x69, 1322 | 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x0e, 0x0a, 0x0a, 0x50, 0x45, 0x52, 0x53, 0x49, 0x53, 1323 | 0x54, 0x45, 0x4e, 0x54, 0x10, 0x00, 0x12, 0x0d, 0x0a, 0x09, 0x45, 0x50, 0x48, 0x45, 0x4d, 0x45, 1324 | 0x52, 0x41, 0x4c, 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x4e, 0x4f, 0x4e, 0x45, 0x10, 0x02, 0x22, 1325 | 0x28, 0x0a, 0x0a, 0x57, 0x69, 0x72, 0x65, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x12, 0x0c, 0x0a, 1326 | 0x08, 0x50, 0x52, 0x4f, 0x54, 0x4f, 0x42, 0x55, 0x46, 0x10, 0x00, 0x12, 0x0c, 0x0a, 0x04, 0x4a, 1327 | 0x53, 0x4f, 0x4e, 0x10, 0x01, 0x1a, 0x02, 0x08, 0x01, 0x42, 0x0a, 0x0a, 0x08, 0x6d, 0x65, 0x74, 1328 | 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, 0x3c, 0x0a, 0x0e, 0x54, 0x4c, 0x53, 0x43, 0x65, 0x72, 0x74, 1329 | 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 1330 | 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 1331 | 0x12, 0x0a, 0x04, 0x63, 0x65, 0x72, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x63, 1332 | 0x65, 0x72, 0x74, 0x22, 0x8c, 0x02, 0x0a, 0x0c, 0x52, 0x65, 0x70, 0x6c, 0x54, 0x72, 0x61, 0x6e, 1333 | 0x73, 0x66, 0x65, 0x72, 0x12, 0x1d, 0x0a, 0x04, 0x72, 0x65, 0x70, 0x6c, 0x18, 0x01, 0x20, 0x01, 1334 | 0x28, 0x0b, 0x32, 0x09, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x52, 0x65, 0x70, 0x6c, 0x52, 0x04, 0x72, 1335 | 0x65, 0x70, 0x6c, 0x12, 0x33, 0x0a, 0x0a, 0x72, 0x65, 0x70, 0x6c, 0x4c, 0x69, 0x6d, 0x69, 0x74, 1336 | 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x52, 0x65, 1337 | 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x73, 0x52, 0x0a, 0x72, 0x65, 1338 | 0x70, 0x6c, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x73, 0x12, 0x33, 0x0a, 0x0a, 0x75, 0x73, 0x65, 0x72, 1339 | 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x61, 1340 | 0x70, 0x69, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4c, 0x69, 0x6d, 0x69, 0x74, 1341 | 0x73, 0x52, 0x0a, 0x75, 0x73, 0x65, 0x72, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x73, 0x12, 0x24, 0x0a, 1342 | 0x0d, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x04, 1343 | 0x20, 0x03, 0x28, 0x09, 0x52, 0x0d, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x44, 0x6f, 0x6d, 0x61, 1344 | 0x69, 0x6e, 0x73, 0x12, 0x37, 0x0a, 0x0c, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 1345 | 0x74, 0x65, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x61, 0x70, 0x69, 0x2e, 1346 | 0x54, 0x4c, 0x53, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x52, 0x0c, 1347 | 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x73, 0x12, 0x14, 0x0a, 0x05, 1348 | 0x66, 0x6c, 0x61, 0x67, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x66, 0x6c, 0x61, 1349 | 0x67, 0x73, 0x22, 0x49, 0x0a, 0x10, 0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x52, 0x65, 0x70, 0x6c, 0x52, 1350 | 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x35, 0x0a, 0x0c, 0x72, 0x65, 0x70, 0x6c, 0x54, 0x72, 1351 | 0x61, 0x6e, 0x73, 0x66, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x61, 1352 | 0x70, 0x69, 0x2e, 0x52, 0x65, 0x70, 0x6c, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x66, 0x65, 0x72, 0x52, 1353 | 0x0c, 0x72, 0x65, 0x70, 0x6c, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x66, 0x65, 0x72, 0x22, 0x75, 0x0a, 1354 | 0x0f, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 1355 | 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 1356 | 0x12, 0x1c, 0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x6d, 0x61, 0x6e, 0x55, 0x52, 0x4c, 0x18, 0x02, 0x20, 1357 | 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x6d, 0x61, 0x6e, 0x55, 0x52, 0x4c, 0x12, 0x12, 1358 | 0x0a, 0x04, 0x67, 0x75, 0x72, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x67, 0x75, 1359 | 0x72, 0x6c, 0x12, 0x14, 0x0a, 0x05, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x18, 0x05, 0x20, 0x01, 0x28, 1360 | 0x09, 0x52, 0x05, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x4a, 0x04, 0x08, 0x04, 0x10, 0x05, 0x4a, 0x04, 1361 | 0x08, 0x06, 0x10, 0x07, 0x22, 0x90, 0x01, 0x0a, 0x10, 0x45, 0x76, 0x69, 0x63, 0x74, 0x52, 0x65, 1362 | 0x70, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x3e, 0x0a, 0x0f, 0x63, 0x6c, 0x75, 1363 | 0x73, 0x74, 0x65, 0x72, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 1364 | 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 1365 | 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x0f, 0x63, 0x6c, 0x75, 0x73, 0x74, 0x65, 1366 | 0x72, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 1367 | 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 1368 | 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x75, 1369 | 0x73, 0x65, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x6c, 0x75, 0x67, 0x18, 0x04, 0x20, 0x01, 0x28, 1370 | 0x09, 0x52, 0x04, 0x73, 0x6c, 0x75, 0x67, 0x22, 0x4a, 0x0a, 0x11, 0x45, 0x76, 0x69, 0x63, 0x74, 1371 | 0x52, 0x65, 0x70, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x35, 0x0a, 0x0c, 1372 | 0x72, 0x65, 0x70, 0x6c, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x66, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 1373 | 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x52, 0x65, 0x70, 0x6c, 0x54, 0x72, 0x61, 1374 | 0x6e, 0x73, 0x66, 0x65, 0x72, 0x52, 0x0c, 0x72, 0x65, 0x70, 0x6c, 0x54, 0x72, 0x61, 0x6e, 0x73, 1375 | 0x66, 0x65, 0x72, 0x42, 0x3d, 0x5a, 0x3b, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 1376 | 0x6d, 0x2f, 0x72, 0x65, 0x70, 0x6c, 0x69, 0x74, 0x2f, 0x67, 0x6f, 0x2d, 0x72, 0x65, 0x70, 0x6c, 1377 | 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x73, 0x2f, 1378 | 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x67, 0x6f, 0x76, 0x61, 0x6c, 0x2f, 0x61, 1379 | 0x70, 0x69, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, 1380 | } 1381 | 1382 | var ( 1383 | file_protos_external_goval_api_client_proto_rawDescOnce sync.Once 1384 | file_protos_external_goval_api_client_proto_rawDescData = file_protos_external_goval_api_client_proto_rawDesc 1385 | ) 1386 | 1387 | func file_protos_external_goval_api_client_proto_rawDescGZIP() []byte { 1388 | file_protos_external_goval_api_client_proto_rawDescOnce.Do(func() { 1389 | file_protos_external_goval_api_client_proto_rawDescData = protoimpl.X.CompressGZIP(file_protos_external_goval_api_client_proto_rawDescData) 1390 | }) 1391 | return file_protos_external_goval_api_client_proto_rawDescData 1392 | } 1393 | 1394 | var file_protos_external_goval_api_client_proto_enumTypes = make([]protoimpl.EnumInfo, 3) 1395 | var file_protos_external_goval_api_client_proto_msgTypes = make([]protoimpl.MessageInfo, 13) 1396 | var file_protos_external_goval_api_client_proto_goTypes = []interface{}{ 1397 | (ResourceLimits_Cachability)(0), // 0: api.ResourceLimits.Cachability 1398 | (ReplToken_Persistence)(0), // 1: api.ReplToken.Persistence 1399 | (ReplToken_WireFormat)(0), // 2: api.ReplToken.WireFormat 1400 | (*Repl)(nil), // 3: api.Repl 1401 | (*ResourceLimits)(nil), // 4: api.ResourceLimits 1402 | (*Permissions)(nil), // 5: api.Permissions 1403 | (*ReplToken)(nil), // 6: api.ReplToken 1404 | (*TLSCertificate)(nil), // 7: api.TLSCertificate 1405 | (*ReplTransfer)(nil), // 8: api.ReplTransfer 1406 | (*AllowReplRequest)(nil), // 9: api.AllowReplRequest 1407 | (*ClusterMetadata)(nil), // 10: api.ClusterMetadata 1408 | (*EvictReplRequest)(nil), // 11: api.EvictReplRequest 1409 | (*EvictReplResponse)(nil), // 12: api.EvictReplResponse 1410 | (*ReplToken_ClassroomMetadata)(nil), // 13: api.ReplToken.ClassroomMetadata 1411 | (*ReplToken_ReplID)(nil), // 14: api.ReplToken.ReplID 1412 | (*ReplToken_Presenced)(nil), // 15: api.ReplToken.Presenced 1413 | (*timestamppb.Timestamp)(nil), // 16: google.protobuf.Timestamp 1414 | } 1415 | var file_protos_external_goval_api_client_proto_depIdxs = []int32{ 1416 | 0, // 0: api.ResourceLimits.cache:type_name -> api.ResourceLimits.Cachability 1417 | 16, // 1: api.ReplToken.iat:type_name -> google.protobuf.Timestamp 1418 | 16, // 2: api.ReplToken.exp:type_name -> google.protobuf.Timestamp 1419 | 1, // 3: api.ReplToken.persistence:type_name -> api.ReplToken.Persistence 1420 | 3, // 4: api.ReplToken.repl:type_name -> api.Repl 1421 | 14, // 5: api.ReplToken.id:type_name -> api.ReplToken.ReplID 1422 | 13, // 6: api.ReplToken.classroom:type_name -> api.ReplToken.ClassroomMetadata 1423 | 4, // 7: api.ReplToken.resourceLimits:type_name -> api.ResourceLimits 1424 | 2, // 8: api.ReplToken.format:type_name -> api.ReplToken.WireFormat 1425 | 15, // 9: api.ReplToken.presenced:type_name -> api.ReplToken.Presenced 1426 | 5, // 10: api.ReplToken.permissions:type_name -> api.Permissions 1427 | 3, // 11: api.ReplTransfer.repl:type_name -> api.Repl 1428 | 4, // 12: api.ReplTransfer.replLimits:type_name -> api.ResourceLimits 1429 | 4, // 13: api.ReplTransfer.userLimits:type_name -> api.ResourceLimits 1430 | 7, // 14: api.ReplTransfer.certificates:type_name -> api.TLSCertificate 1431 | 8, // 15: api.AllowReplRequest.replTransfer:type_name -> api.ReplTransfer 1432 | 10, // 16: api.EvictReplRequest.clusterMetadata:type_name -> api.ClusterMetadata 1433 | 8, // 17: api.EvictReplResponse.replTransfer:type_name -> api.ReplTransfer 1434 | 18, // [18:18] is the sub-list for method output_type 1435 | 18, // [18:18] is the sub-list for method input_type 1436 | 18, // [18:18] is the sub-list for extension type_name 1437 | 18, // [18:18] is the sub-list for extension extendee 1438 | 0, // [0:18] is the sub-list for field type_name 1439 | } 1440 | 1441 | func init() { file_protos_external_goval_api_client_proto_init() } 1442 | func file_protos_external_goval_api_client_proto_init() { 1443 | if File_protos_external_goval_api_client_proto != nil { 1444 | return 1445 | } 1446 | if !protoimpl.UnsafeEnabled { 1447 | file_protos_external_goval_api_client_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { 1448 | switch v := v.(*Repl); i { 1449 | case 0: 1450 | return &v.state 1451 | case 1: 1452 | return &v.sizeCache 1453 | case 2: 1454 | return &v.unknownFields 1455 | default: 1456 | return nil 1457 | } 1458 | } 1459 | file_protos_external_goval_api_client_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { 1460 | switch v := v.(*ResourceLimits); i { 1461 | case 0: 1462 | return &v.state 1463 | case 1: 1464 | return &v.sizeCache 1465 | case 2: 1466 | return &v.unknownFields 1467 | default: 1468 | return nil 1469 | } 1470 | } 1471 | file_protos_external_goval_api_client_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { 1472 | switch v := v.(*Permissions); i { 1473 | case 0: 1474 | return &v.state 1475 | case 1: 1476 | return &v.sizeCache 1477 | case 2: 1478 | return &v.unknownFields 1479 | default: 1480 | return nil 1481 | } 1482 | } 1483 | file_protos_external_goval_api_client_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { 1484 | switch v := v.(*ReplToken); i { 1485 | case 0: 1486 | return &v.state 1487 | case 1: 1488 | return &v.sizeCache 1489 | case 2: 1490 | return &v.unknownFields 1491 | default: 1492 | return nil 1493 | } 1494 | } 1495 | file_protos_external_goval_api_client_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { 1496 | switch v := v.(*TLSCertificate); i { 1497 | case 0: 1498 | return &v.state 1499 | case 1: 1500 | return &v.sizeCache 1501 | case 2: 1502 | return &v.unknownFields 1503 | default: 1504 | return nil 1505 | } 1506 | } 1507 | file_protos_external_goval_api_client_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { 1508 | switch v := v.(*ReplTransfer); i { 1509 | case 0: 1510 | return &v.state 1511 | case 1: 1512 | return &v.sizeCache 1513 | case 2: 1514 | return &v.unknownFields 1515 | default: 1516 | return nil 1517 | } 1518 | } 1519 | file_protos_external_goval_api_client_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { 1520 | switch v := v.(*AllowReplRequest); i { 1521 | case 0: 1522 | return &v.state 1523 | case 1: 1524 | return &v.sizeCache 1525 | case 2: 1526 | return &v.unknownFields 1527 | default: 1528 | return nil 1529 | } 1530 | } 1531 | file_protos_external_goval_api_client_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { 1532 | switch v := v.(*ClusterMetadata); i { 1533 | case 0: 1534 | return &v.state 1535 | case 1: 1536 | return &v.sizeCache 1537 | case 2: 1538 | return &v.unknownFields 1539 | default: 1540 | return nil 1541 | } 1542 | } 1543 | file_protos_external_goval_api_client_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { 1544 | switch v := v.(*EvictReplRequest); i { 1545 | case 0: 1546 | return &v.state 1547 | case 1: 1548 | return &v.sizeCache 1549 | case 2: 1550 | return &v.unknownFields 1551 | default: 1552 | return nil 1553 | } 1554 | } 1555 | file_protos_external_goval_api_client_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { 1556 | switch v := v.(*EvictReplResponse); i { 1557 | case 0: 1558 | return &v.state 1559 | case 1: 1560 | return &v.sizeCache 1561 | case 2: 1562 | return &v.unknownFields 1563 | default: 1564 | return nil 1565 | } 1566 | } 1567 | file_protos_external_goval_api_client_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { 1568 | switch v := v.(*ReplToken_ClassroomMetadata); i { 1569 | case 0: 1570 | return &v.state 1571 | case 1: 1572 | return &v.sizeCache 1573 | case 2: 1574 | return &v.unknownFields 1575 | default: 1576 | return nil 1577 | } 1578 | } 1579 | file_protos_external_goval_api_client_proto_msgTypes[11].Exporter = func(v interface{}, i int) interface{} { 1580 | switch v := v.(*ReplToken_ReplID); i { 1581 | case 0: 1582 | return &v.state 1583 | case 1: 1584 | return &v.sizeCache 1585 | case 2: 1586 | return &v.unknownFields 1587 | default: 1588 | return nil 1589 | } 1590 | } 1591 | file_protos_external_goval_api_client_proto_msgTypes[12].Exporter = func(v interface{}, i int) interface{} { 1592 | switch v := v.(*ReplToken_Presenced); i { 1593 | case 0: 1594 | return &v.state 1595 | case 1: 1596 | return &v.sizeCache 1597 | case 2: 1598 | return &v.unknownFields 1599 | default: 1600 | return nil 1601 | } 1602 | } 1603 | } 1604 | file_protos_external_goval_api_client_proto_msgTypes[3].OneofWrappers = []interface{}{ 1605 | (*ReplToken_Repl)(nil), 1606 | (*ReplToken_Id)(nil), 1607 | (*ReplToken_Classroom)(nil), 1608 | } 1609 | type x struct{} 1610 | out := protoimpl.TypeBuilder{ 1611 | File: protoimpl.DescBuilder{ 1612 | GoPackagePath: reflect.TypeOf(x{}).PkgPath(), 1613 | RawDescriptor: file_protos_external_goval_api_client_proto_rawDesc, 1614 | NumEnums: 3, 1615 | NumMessages: 13, 1616 | NumExtensions: 0, 1617 | NumServices: 0, 1618 | }, 1619 | GoTypes: file_protos_external_goval_api_client_proto_goTypes, 1620 | DependencyIndexes: file_protos_external_goval_api_client_proto_depIdxs, 1621 | EnumInfos: file_protos_external_goval_api_client_proto_enumTypes, 1622 | MessageInfos: file_protos_external_goval_api_client_proto_msgTypes, 1623 | }.Build() 1624 | File_protos_external_goval_api_client_proto = out.File 1625 | file_protos_external_goval_api_client_proto_rawDesc = nil 1626 | file_protos_external_goval_api_client_proto_goTypes = nil 1627 | file_protos_external_goval_api_client_proto_depIdxs = nil 1628 | } 1629 | --------------------------------------------------------------------------------