17 Commits

Author SHA1 Message Date
Timmy Welch
8989dc25ac Retrieve CA Key for display on startup 2026-01-10 13:12:50 -08:00
Timmy Welch
899aad07b2 modernize 2026-01-10 12:56:08 -08:00
Timmy Welch
2bc73596d3 Updates
Better forking
Simplify socket checking
Use logger properly
Fix errors/panic's killing the agent
Ensure a user-agent is used for http requests
2026-01-10 12:55:27 -08:00
Timmy Welch
00de399557 Remove cloud stuff 2026-01-10 10:54:34 -08:00
Timmy Welch
4de02548c1 Add pre-commit 2023-05-24 17:40:52 -07:00
Timmy Welch
fbca16fc9a Improve username extraction 2023-05-24 17:40:23 -07:00
Timmy Welch
d504f22925 Return error if connection to OIDC server fails 2023-01-16 19:46:34 -08:00
Timmy Welch
3e77c30ed3 goimports 2023-01-16 19:46:04 -08:00
Timmy Welch
9573438f75 Open the socket before opening the log 2023-01-16 19:45:32 -08:00
Timmy Welch
d9b0b2e21a go mod tidy 2023-01-14 19:54:06 -08:00
Timmy Welch
78559f0bc7 Remove replace directive 2023-01-14 19:35:47 -08:00
Timmy Welch
0276f52b49 Show current CA key on login 2023-01-14 18:30:28 -08:00
Timmy Welch
5d7886db40 go get -u && go mod tidy 2023-01-14 10:57:24 -08:00
Timmy Welch
a9a40622ca Add compatibility with Zitadel
Expand ~ to HOME in Agent.Socket
Add url override for gcloud functions v2
Add logging for parsing the principals
go fmt
2023-01-14 10:49:39 -08:00
Timmy Welch
bcb5789044 Make it "better" 2022-10-07 17:43:18 -07:00
lordwelch
da6ba96063 Bump aws-oidc version
Ignore SIGHUP on unix systems
2020-12-06 12:11:18 -08:00
lordwelch
20c5cf00ea Bump aws-oidc version 2020-12-06 11:15:51 -08:00
29 changed files with 1003 additions and 2074 deletions

28
.pre-commit-config.yaml Normal file
View File

@@ -0,0 +1,28 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
hooks:
- id: trailing-whitespace
args: [--markdown-linebreak-ext=.gitignore]
- id: end-of-file-fixer
- id: check-yaml
- repo: local
hooks:
- id: go-imports
name: goimports
entry: goimports
args: [-w, -l]
language: golang
types: [go]
additional_dependencies: [golang.org/x/tools/cmd/goimports@latest]
- id: go-mod-tidy
name: go mod tidy
entry: go mod tidy
language: golang
types: [go-mod]
always_run: true
pass_filenames: false
- repo: https://github.com/golangci/golangci-lint
rev: v1.52.2
hooks:
- id: golangci-lint

View File

@@ -4,4 +4,4 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -0,0 +1,25 @@
package main
import (
"fmt"
"os"
"path/filepath"
"strings"
)
func logRotate(path string, count int) {
if _, err := os.Stat(path); err == nil {
ext := filepath.Ext(path)
base := strings.TrimSuffix(path, ext)
for i := count - 1; i >= 1; i-- {
source := fmt.Sprintf("%s.%d%s", base, i, ext)
dest := fmt.Sprintf("%s.%d%s", base, i+1, ext)
_ = os.Remove(dest)
_ = os.Rename(source, dest)
}
dest := fmt.Sprintf("%s.%d%s", base, 1, ext)
_ = os.Remove(dest)
_ = os.Rename(path, dest)
}
}

View File

@@ -2,8 +2,8 @@ package main
import (
"crypto"
"crypto/ed25519"
"crypto/rand"
"crypto/rsa"
"errors"
"flag"
"fmt"
@@ -11,116 +11,352 @@ import (
"net"
"os"
"os/signal"
"path/filepath"
"runtime"
"strings"
"syscall"
"git.narnian.us/lordwelch/sshrimp/internal/config"
"git.narnian.us/lordwelch/sshrimp/internal/signer"
"git.narnian.us/lordwelch/sshrimp/internal/sshrimpagent"
"gitea.narnian.us/lordwelch/sshrimp/internal/config"
"gitea.narnian.us/lordwelch/sshrimp/internal/signer"
"gitea.narnian.us/lordwelch/sshrimp/internal/sshrimpagent"
"github.com/prometheus/procfs"
"github.com/sirupsen/logrus"
"github.com/sirupsen/logrus/hooks/writer"
"golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/agent"
"inet.af/peercred"
)
var sigs = []os.Signal{os.Kill, os.Interrupt}
var logger = logrus.New()
var log *logrus.Entry
var (
sigExit = []os.Signal{os.Kill, os.Interrupt}
sigIgnore []os.Signal
logger = logrus.New()
log *logrus.Entry
appname = "sshrimp"
)
var cli struct {
Config string `kong:"arg,type='string',help='sshrimp config file (default: ${config_file} or ${env_var_name} environment variable)',default='${config_file}',env='SSHRIMP_CONFIG'"`
type cfg struct {
Config string
LogDirectory string
Verbose bool
Foreground bool
}
func main() {
flag.StringVar(&cli.Config, "config", config.DefaultPath, "sshrimp config file")
v := flag.Bool("v", false, "enable verbose logging")
func getLogDir() string {
logdir := ""
switch runtime.GOOS {
case "plan9":
if dir, err := os.UserConfigDir(); err == nil {
logdir = filepath.Join(dir, "logs", appname)
}
case "darwin", "ios":
if dir, err := os.UserHomeDir(); err == nil {
logdir = filepath.Join(dir, "Library/Logs", appname)
}
default:
if dir, err := os.UserCacheDir(); err == nil {
logdir = filepath.Join(dir, appname, "logs")
}
}
if logdir == "" {
if dir, err := os.UserHomeDir(); err == nil {
logdir = filepath.Join(dir, ".ssh/sshrimp_logs")
}
}
return logdir
}
func setupLoging(config cfg) error {
levels := []logrus.Level{
logrus.PanicLevel,
logrus.FatalLevel,
logrus.ErrorLevel,
logrus.WarnLevel,
}
err := os.MkdirAll(config.LogDirectory, 0o750)
if err != nil {
log.Fatal(err)
}
logName := filepath.Join(config.LogDirectory, appname+".log")
logRotate(logName, 10)
logger.SetLevel(logrus.TraceLevel)
if config.Verbose {
levels = logrus.AllLevels
}
logger.SetFormatter(&logrus.TextFormatter{
FullTimestamp: true,
})
logger.AddHook(&writer.Hook{ // Send logs with level higher than warning to stderr
Writer: os.Stderr,
LogLevels: levels,
})
logger.Out = io.Discard
file, err := os.Create(logName)
if err != nil {
return err
}
// defer file.Close()
logger.AddHook(&writer.Hook{ // Send all logs to file
Writer: file,
LogLevels: logrus.AllLevels,
})
log = logger.WithFields(logrus.Fields{
"pid": os.Getpid(),
})
sshrimpagent.Log = log
signer.Log = log
return nil
}
flag.Parse()
if *v {
logger.SetLevel(logrus.TraceLevel)
func ExpandPath(path string) string {
home, err := os.UserHomeDir()
if err != nil {
return path
}
c := config.NewSSHrimpWithDefaults()
err := c.Read(cli.Config)
if err != nil {
panic(err)
if path[0] == '~' {
path = filepath.Join(home, path[1:])
}
err = launchAgent(c)
return path
}
func main2(cli cfg, c *config.SSHrimp) {
err := setupLoging(cli)
if err != nil {
panic(err)
log.Warnf("Error setting up logging: %v", err)
}
listener := openSocket(ExpandPath(c.Agent.Socket))
if listener == nil {
log.Errorln("Failed to open socket")
return
}
err = launchAgent(c, listener)
if err != nil {
log.Panic("Failed to launch agent", err)
}
}
func launchAgent(c *config.SSHrimp) error {
func main() {
defaultConfigPath := "~/.ssh/sshrimp.toml"
if configPathFromEnv, ok := os.LookupEnv("SSHRIMP_CONFIG"); ok && configPathFromEnv != "" {
defaultConfigPath = configPathFromEnv
}
var (
cli cfg
err error
)
flag.StringVar(&cli.Config, "config", defaultConfigPath, "sshrimp config file")
flag.StringVar(&cli.LogDirectory, "log", getLogDir(), "sshrimp log directory")
flag.BoolVar(&cli.Verbose, "v", false, "enable verbose logging")
flag.BoolVar(&cli.Foreground, "f", false, "Run in the foreground")
flag.Parse()
sshCommand := flag.Args()
cfgFile := ExpandPath(cli.Config)
cfgFile, err = filepath.Abs(cfgFile)
if err != nil {
log.Errorln("config must be an absolute path")
os.Exit(1)
}
c := config.NewSSHrimpWithDefaults()
err = c.Read(cfgFile)
if err != nil {
panic(err)
}
if os.Getenv("SSHRIMP_DAEMON") == "true" {
cli.Foreground = true
}
if cli.Foreground {
logger.Println("Launching agent")
main2(cli, c)
} else {
logger.Println("Attempting to start daemon")
var nullFile *os.File
nullFile, err = os.Open(os.DevNull)
if err != nil {
panic(err)
}
env := os.Environ()
env = append(env, "SSHRIMP_DAEMON=true")
executable, err := os.Executable()
if err != nil {
panic(err)
}
_, err = os.StartProcess(executable, os.Args, &os.ProcAttr{
Dir: filepath.Dir(cfgFile),
Env: env,
Files: []*os.File{nullFile, nullFile, nullFile},
Sys: &syscall.SysProcAttr{
// Chroot: d.Chroot,
Setsid: true,
},
})
if err != nil {
panic(err)
}
nullFile.Close()
}
if len(sshCommand) > 1 && filepath.Base(sshCommand[0]) == "ssh" {
syscall.Exec(sshCommand[0], sshCommand, os.Environ())
}
}
func socketWorks(path string) bool {
var (
pid int
cred *peercred.Creds
)
conn, sockErr := net.Dial("unix", path)
if sockErr != nil {
return false
}
if conn == nil {
return false
}
defer conn.Close()
cred, sockErr = peercred.Get(conn)
if sockErr != nil {
return false
}
var (
ok bool
process *os.Process
)
pid, ok = cred.PID()
if !ok {
return false
}
process, sockErr = os.FindProcess(pid)
if sockErr != nil {
return false
}
defer process.Release()
return process.Signal(syscall.SIGHUP) == nil
}
func openSocket(socketPath string) net.Listener {
var (
err error
listener net.Listener
privateKey crypto.Signer
signer ssh.Signer
err error
logMessage string
)
log.Traceln("Creating socket")
if _, err = os.Stat(c.Agent.Socket); err == nil {
log.Tracef("File already exists at %s", c.Agent.Socket)
conn, sockErr := net.Dial("unix", c.Agent.Socket)
if conn == nil {
logMessage = "conn is nil"
}
if sockErr == nil { // socket is accepting connections
logMessage += "err reports successful connection"
conn.Close()
log.Errorf("Socket connected successfully %s", logMessage)
return fmt.Errorf("socket %s already exists", c.Agent.Socket)
}
log.Tracef("Socket is not connected %s", logMessage)
if os.Remove(c.Agent.Socket) == nil { // socket is not accepting connections, assuming safe to remove
log.Traceln("Deleting socket: success")
} else {
log.Errorf("Deleting socket: fail")
}
if socketWorks(socketPath) { // socket is accepting connections
log.Printf("socket %s already exists\n", socketPath)
return nil
}
log.Printf("Socket is not connected %s\n", logMessage)
err = os.Remove(socketPath)
if err == nil { // socket is not accepting connections, assuming safe to remove
log.Println("Deleting socket: success")
} else if !errors.Is(err, os.ErrNotExist) {
log.Println("Deleting socket: fail", err)
return nil
}
// This affects all files created for the process. Since this is a sensitive
// socket, only allow the current user to write to the socket.
syscall.Umask(0077)
listener, err = net.Listen("unix", c.Agent.Socket)
syscall.Umask(0o077)
listener, err = net.Listen("unix", socketPath)
if err != nil {
log.Println("Error opening socket:", err)
return nil
}
log.Println("Opened socket", socketPath)
return listener
}
func getConnectedProcess(conn net.Conn) string {
var (
cred *peercred.Creds
err error
)
cred, err = peercred.Get(conn)
if err != nil {
return ""
}
pid, ok := cred.PID()
if !ok {
return ""
}
var (
proc procfs.Proc
name string
)
proc, err = procfs.NewProc(pid)
if err != nil {
return fmt.Sprintf("pid %d", pid)
}
name, err = proc.Executable()
if err == nil {
return fmt.Sprintf("pid %d", pid)
}
return name
}
func handle(sshrimpAgent agent.Agent, conn net.Conn) (err error) {
defer func() {
panicErr := recover()
if panicErr != nil {
if err != nil {
err = fmt.Errorf("something panicked: %w: %v", err, panicErr)
return
}
err, _ = panicErr.(error)
return
}
}()
log.Infof("Serving agent to %s", getConnectedProcess(conn))
if err = agent.ServeAgent(sshrimpAgent, conn); err != nil && !errors.Is(err, io.EOF) {
log.Errorf("Error serving agent: %v", err)
return err
}
return err
}
func launchAgent(c *config.SSHrimp, listener net.Listener) error {
var (
err error
privateKey crypto.Signer
sshSigner ssh.Signer
)
defer listener.Close()
fmt.Printf("listening on %s\n", c.Agent.Socket)
log.Printf("listening on %s\n", c.Agent.Socket)
// Generate a new SSH private/public key pair
log.Tracef("Generating RSA %d ssh keys", 2048)
privateKey, err = rsa.GenerateKey(rand.Reader, 2048)
log.Tracef("Generating ed25519 ssh keys")
_, privateKey, err = ed25519.GenerateKey(rand.Reader)
if err != nil {
return err
}
log.Traceln("Creating new signer from key")
signer, err = ssh.NewSignerFromKey(privateKey)
log.Traceln("Creating new sshSigner from key")
sshSigner, err = ssh.NewSignerFromKey(privateKey)
if err != nil {
return err
}
// Create the sshrimp agent with our configuration and the private key signer
log.Traceln("Creating new sshrimp agent from signer and config")
sshrimpAgent := sshrimpagent.NewSSHrimpAgent(c, signer)
// Create the sshrimp agent with our configuration and the private key sshSigner
log.Traceln("Creating new sshrimp agent from sshSigner and config")
sshrimpAgent, err := sshrimpagent.NewSSHrimpAgent(c, sshSigner)
if err != nil {
log.Errorf("Failed to create sshrimpAgent: %v", err)
}
// Listen for signals so that we can close the listener and exit nicely
log.Debugf("Exiting on signals: %v", sigs)
osSignals := make(chan os.Signal)
signal.Notify(osSignals, sigs...)
log.Debugf("Ignoring signals: %v", sigIgnore)
signal.Ignore(sigIgnore...)
log.Debugf("Exiting on signals: %v", sigExit)
osSignals := make(chan os.Signal, 10)
signal.Notify(osSignals, sigExit...)
go func() {
<-osSignals
listener.Close()
@@ -135,14 +371,10 @@ func launchAgent(c *config.SSHrimp) error {
log.Errorf("Error accepting connection: %v", err)
if strings.Contains(err.Error(), "use of closed network connection") {
// Occurs if the user interrupts the agent with a ctrl-c signal
return nil
continue
}
return err
}
log.Traceln("Serving agent")
if err = agent.ServeAgent(sshrimpAgent, conn); err != nil && !errors.Is(err, io.EOF) {
log.Errorf("Error serving agent: %v", err)
return err
log.Errorf("Error accepting connection: %v", err)
}
go handle(sshrimpAgent, conn)
}
}

View File

@@ -1,4 +1,4 @@
// +build darwin linux
//go:build darwin || linux
package main
@@ -7,5 +7,6 @@ import (
)
func init() {
sigs = append(sigs, syscall.SIGTERM)
sigExit = append(sigExit, syscall.SIGTERM)
sigIgnore = append(sigIgnore, syscall.SIGHUP)
}

View File

@@ -1,64 +1,182 @@
package main
import (
"context"
"bytes"
"crypto/ed25519"
"crypto/rand"
"crypto/x509"
"encoding/json"
"encoding/pem"
"errors"
"flag"
"fmt"
"io"
"log"
"net/http"
"os"
"strings"
"git.narnian.us/lordwelch/sshrimp/internal/config"
"git.narnian.us/lordwelch/sshrimp/internal/signer"
"github.com/aws/aws-lambda-go/lambda"
"github.com/aws/aws-lambda-go/lambdacontext"
"gitea.narnian.us/lordwelch/sshrimp/internal/config"
"gitea.narnian.us/lordwelch/sshrimp/internal/signer"
"github.com/BurntSushi/toml"
gonanoid "github.com/matoous/go-nanoid/v2"
"github.com/sirupsen/logrus"
"golang.org/x/crypto/ssh"
)
// HandleRequest handles a request to sign an SSH public key verified by an OpenIDConnect id_token
func HandleRequest(ctx context.Context, event signer.SSHrimpEvent) (*signer.SSHrimpResult, error) {
func httpError(w http.ResponseWriter, v any, statusCode int) {
var b bytes.Buffer
e := json.NewEncoder(&b)
_ = e.Encode(v)
http.Error(w, b.String(), statusCode)
}
// Make sure we are running in a lambda context, to get the requestid and ARN
lambdaContext, ok := lambdacontext.FromContext(ctx)
if !ok {
return nil, fmt.Errorf("lambdacontext not in ctx")
type Server struct {
config *config.SSHrimp
Key ssh.Signer
Log *logrus.Logger
}
func NewServer(cfg *config.SSHrimp) (*Server, error) {
server := &Server{
config: cfg,
Log: logrus.New(),
}
server.Log.SetLevel(logrus.DebugLevel)
return server, server.LoadKey()
}
func (s *Server) LoadKey() error {
if s.config.CertificateAuthority.KeyPath == "" {
return fmt.Errorf("key path missing")
}
b, err := os.ReadFile(s.config.CertificateAuthority.KeyPath)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return s.GenerateKey()
}
return err
}
s.Key, err = ssh.ParsePrivateKey(b)
return err
}
func (s *Server) GenerateKey() error {
_, key, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
return err
}
file, err := os.OpenFile(s.config.CertificateAuthority.KeyPath, os.O_RDWR|os.O_CREATE, 0o400)
if err == nil {
defer file.Close()
var pkcs8 []byte
if pkcs8, err = x509.MarshalPKCS8PrivateKey(key); err == nil {
err = pem.Encode(file, &pem.Block{
Type: "PRIVATE KEY",
Bytes: pkcs8,
})
}
}
if err != nil {
log.Printf("could not save generated CertificateAuthority key: %v", err)
}
s.Key, err = ssh.NewSignerFromKey(key)
return err
}
// ServeHTTP handles a request to sign an SSH public key verified by an OpenIDConnect id_token
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
txid := gonanoid.Must()
log := s.Log.WithField("X-Request-ID", txid)
w.Header().Add("X-Request-ID", txid)
defer r.Body.Close()
if strings.HasPrefix(r.URL.Path, "/config") {
io.Copy(io.Discard, r.Body)
newConfig := *s.config
newConfig.CertificateAuthority = config.CertificateAuthority{}
w.Header().Add("Content-Type", "application/toml")
w.Header().Add("Content-Disposition", `attachment; filename="sshrimp.toml"`)
t := toml.NewEncoder(w)
_ = t.Encode(newConfig)
return
}
if r.Header.Get("Content-Type") != "application/json" {
io.Copy(io.Discard, r.Body)
w.Header().Add("Content-Type", "text/x-ssh-public-key")
w.Write(ssh.MarshalAuthorizedKey(s.Key.PublicKey()))
return
}
w.Header().Add("Content-Type", "application/json")
// Load the configuration file, if not exsits, exit.
c := config.NewSSHrimp()
if err := c.Read(config.GetPath()); err != nil {
return nil, err
var event signer.SSHrimpEvent
if err := json.NewDecoder(r.Body).Decode(&event); err != nil {
httpError(w, signer.SSHrimpResult{Certificate: "", ErrorMessage: err.Error(), ErrorType: http.StatusText(http.StatusBadRequest)}, http.StatusBadRequest)
return
}
// Create the certificate struct with all our configured values
certificate, err := signer.ValidateRequest(event, c, lambdaContext.AwsRequestID, lambdaContext.InvokedFunctionArn)
certificate, err := signer.ValidateRequest(log, event, s.config, txid, s.Key.PublicKey())
if err != nil {
return nil, err
httpError(w, signer.SSHrimpResult{Certificate: "", ErrorMessage: err.Error(), ErrorType: http.StatusText(http.StatusBadRequest)}, http.StatusBadRequest)
return
}
// Setup our Certificate Authority signer backed by KMS
kmsSigner := signer.NewAWSSigner(c.CertificateAuthority.KeyAlias)
sshAlgorithmSigner, err := signer.NewAlgorithmSignerFromSigner(kmsSigner, ssh.SigAlgoRSASHA2256)
sshAlgorithmSigner, err := ssh.NewSignerWithAlgorithms(s.Key.(ssh.AlgorithmSigner), []string{ssh.KeyAlgoED25519})
if err != nil {
return nil, err
httpError(w, signer.SSHrimpResult{Certificate: "", ErrorMessage: err.Error(), ErrorType: http.StatusText(http.StatusBadRequest)}, http.StatusBadRequest)
return
}
// Sign the certificate!!
if err := certificate.SignCert(rand.Reader, sshAlgorithmSigner); err != nil {
return nil, err
httpError(w, signer.SSHrimpResult{Certificate: "", ErrorMessage: err.Error(), ErrorType: http.StatusText(http.StatusInternalServerError)}, http.StatusInternalServerError)
return
}
// Extract the public key (certificate) to return to the user
// If you want to validate that the generated cert is correct
// i, _ := identity.NewIdentity(s.config)
// username, _ := i.Validate(event.Token)
// cc := ssh.CertChecker{}
// err = cc.CheckCert(username, &certificate)
// if err != nil {
// httpError(w, signer.SSHrimpResult{Certificate: "", ErrorMessage: err.Error(), ErrorType: http.StatusText(http.StatusInternalServerError)}, http.StatusBadRequest)
// return
// }
// Return the certificate to the user
pubkey, err := ssh.ParsePublicKey(certificate.Marshal())
if err != nil {
return nil, err
httpError(w, signer.SSHrimpResult{Certificate: "", ErrorMessage: err.Error(), ErrorType: http.StatusText(http.StatusInternalServerError)}, http.StatusInternalServerError)
return
}
// Success!
return &signer.SSHrimpResult{
res := &signer.SSHrimpResult{
Certificate: string(ssh.MarshalAuthorizedKey(pubkey)),
ErrorMessage: "",
ErrorType: "",
}, nil
}
e := json.NewEncoder(w)
_ = e.Encode(res)
}
func main() {
lambda.Start(HandleRequest)
cfgFile := flag.String("config", "/etc/sshrimp.toml", "Path to sshrimp.toml")
addr := flag.String("addr", "127.0.0.1:8080", "Address to listen on")
flag.Parse()
cfg := config.NewSSHrimp()
if err := cfg.Read(*cfgFile); err != nil {
log.Printf("Unable to read config file %s: %v", *cfgFile, err)
os.Exit(1)
}
server, err := NewServer(cfg)
if err != nil {
log.Printf("Unable to start server: %v", err)
os.Exit(2)
}
if err = http.ListenAndServe(*addr, server); err != nil {
log.Printf("Error serving http: %v", err)
os.Exit(99)
}
}

View File

@@ -1,83 +0,0 @@
package gcp
import (
"bytes"
"crypto/rand"
"encoding/json"
"fmt"
"net/http"
"os"
"git.narnian.us/lordwelch/sshrimp/internal/config"
"git.narnian.us/lordwelch/sshrimp/internal/identity"
"git.narnian.us/lordwelch/sshrimp/internal/signer"
"golang.org/x/crypto/ssh"
)
func httpError(w http.ResponseWriter, v interface{}, statusCode int) {
var b bytes.Buffer
e := json.NewEncoder(&b)
_ = e.Encode(v)
http.Error(w, b.String(), statusCode)
}
// SSHrimp handles a request to sign an SSH public key verified by an OpenIDConnect id_token
func SSHrimp(w http.ResponseWriter, r *http.Request) {
// Load the configuration file, if not exsits, exit.
c := config.NewSSHrimp()
if err := c.Read("./serverless_function_source_code/sshrimp.toml"); err != nil {
httpError(w, signer.SSHrimpResult{Certificate: "", ErrorMessage: err.Error(), ErrorType: http.StatusText(http.StatusInternalServerError)}, http.StatusInternalServerError)
return
}
var event signer.SSHrimpEvent
if err := json.NewDecoder(r.Body).Decode(&event); err != nil {
httpError(w, signer.SSHrimpResult{Certificate: "", ErrorMessage: err.Error(), ErrorType: http.StatusText(http.StatusBadRequest)}, http.StatusBadRequest)
return
}
certificate, err := signer.ValidateRequest(event, c, r.Header.Get("Function-Execution-Id"), fmt.Sprintf("%s/%s/%s", os.Getenv("GCP_PROJECT"), os.Getenv("FUNCTION_REGION"), os.Getenv("FUNCTION_NAME")))
if err != nil {
httpError(w, signer.SSHrimpResult{Certificate: "", ErrorMessage: err.Error(), ErrorType: http.StatusText(http.StatusBadRequest)}, http.StatusBadRequest)
return
}
// Setup our Certificate Authority signer backed by KMS
kmsSigner := signer.NewGCPSSigner(c.CertificateAuthority.KeyAlias)
sshAlgorithmSigner, err := signer.NewAlgorithmSignerFromSigner(kmsSigner, ssh.SigAlgoRSASHA2256)
if err != nil {
httpError(w, signer.SSHrimpResult{Certificate: "", ErrorMessage: err.Error(), ErrorType: http.StatusText(http.StatusBadRequest)}, http.StatusBadRequest)
return
}
// Sign the certificate!!
if err := certificate.SignCert(rand.Reader, sshAlgorithmSigner); err != nil {
httpError(w, signer.SSHrimpResult{Certificate: "", ErrorMessage: err.Error(), ErrorType: http.StatusText(http.StatusInternalServerError)}, http.StatusInternalServerError)
return
}
i, _ := identity.NewIdentity(c)
username, _ := i.Validate(event.Token)
cc := ssh.CertChecker{}
err = cc.CheckCert(username, &certificate)
if err != nil {
httpError(w, signer.SSHrimpResult{Certificate: "", ErrorMessage: err.Error(), ErrorType: http.StatusText(http.StatusInternalServerError)}, http.StatusBadRequest)
return
}
// Extract the public key (certificate) to return to the user
pubkey, err := ssh.ParsePublicKey(certificate.Marshal())
if err != nil {
httpError(w, signer.SSHrimpResult{Certificate: "", ErrorMessage: err.Error(), ErrorType: http.StatusText(http.StatusInternalServerError)}, http.StatusInternalServerError)
return
}
// Success!
res := &signer.SSHrimpResult{
Certificate: string(ssh.MarshalAuthorizedKey(pubkey)),
ErrorMessage: "",
ErrorType: "",
}
e := json.NewEncoder(w)
_ = e.Encode(res)
}

46
go.mod
View File

@@ -1,22 +1,34 @@
module git.narnian.us/lordwelch/sshrimp
module gitea.narnian.us/lordwelch/sshrimp
go 1.13
go 1.24.0
replace github.com/b-b3rn4rd/gocfn => github.com/stoggi/gocfn v0.0.0-20200214083946-6202cea979b9
toolchain go1.24.6
require (
cloud.google.com/go v0.73.0
git.narnian.us/lordwelch/aws-oidc v0.0.2
github.com/AlecAivazis/survey/v2 v2.2.4
github.com/BurntSushi/toml v0.3.1
github.com/aws/aws-lambda-go v1.19.0
github.com/aws/aws-sdk-go v1.36.2
github.com/awslabs/goformation/v4 v4.14.0
github.com/coreos/go-oidc v2.2.1+incompatible
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
github.com/magefile/mage v1.10.0
github.com/pkg/errors v0.9.1
github.com/sirupsen/logrus v1.7.0
golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de
google.golang.org/genproto v0.0.0-20201203001206-6486ece9c497
github.com/BurntSushi/toml v1.6.0
github.com/coreos/go-oidc/v3 v3.17.0
github.com/google/uuid v1.6.0
github.com/matoous/go-nanoid/v2 v2.1.0
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c
github.com/prometheus/procfs v0.19.2
github.com/sirupsen/logrus v1.9.3
github.com/zitadel/oidc v1.13.5
golang.org/x/crypto v0.46.0
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93
inet.af/peercred v0.0.0-20210906144145-0893ea02156a
)
require (
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
github.com/gorilla/schema v1.4.1 // indirect
github.com/gorilla/securecookie v1.1.2 // indirect
github.com/jeremija/gosubmit v0.2.8 // indirect
github.com/rs/cors v1.11.1 // indirect
github.com/stretchr/testify v1.11.1 // indirect
github.com/zitadel/logging v0.6.2 // indirect
golang.org/x/net v0.48.0 // indirect
golang.org/x/oauth2 v0.34.0 // indirect
golang.org/x/sys v0.40.0 // indirect
golang.org/x/text v0.32.0 // indirect
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
)

603
go.sum
View File

@@ -1,555 +1,72 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0 h1:eOI3/cP2VTU6uZLDYAoic+eyzzB9YyGmJ7eIjl8rOPg=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0 h1:ROfEUZz+Gh5pa62DJWXSaonyu3StP6EA6lPEXPI6mCo=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
cloud.google.com/go v0.73.0 h1:sGvc4e0Cmm4+DKQR76a9VwNukpacQK8TOl5pDl0Pcn0=
cloud.google.com/go v0.73.0/go.mod h1:BkDh9dFvGjCitVw03TNjKbBxXNKULXXIq6orU6HrJ4Q=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
git.narnian.us/lordwelch/aws-oidc v0.0.2 h1:75AOGD8IYvKpg772n4/4vku84kHOQETmQqp0YVLS/xw=
git.narnian.us/lordwelch/aws-oidc v0.0.2/go.mod h1:lfAaTDbI5Ip4TjBDAvJnPNmSLCMAS9hNRWgqwmm4bTk=
github.com/99designs/keyring v0.0.0-20190110203331-82da6802f65f h1:WXiWWJrYCaOaYimBAXlRdRJ7qOisrYyMLYnCvvhHVms=
github.com/99designs/keyring v0.0.0-20190110203331-82da6802f65f/go.mod h1:aKt8W/yd91/xHY6ixZAJZ2vYbhr3pP8DcrvuGSGNPJk=
github.com/AlecAivazis/survey/v2 v2.2.4 h1:OAh6g17JmXsjVVHTnfQFEi6K+YZX6mrC+pT8IPkUlpk=
github.com/AlecAivazis/survey/v2 v2.2.4/go.mod h1:9FJRdMdDm8rnT+zHVbvQT2RTSTLq0Ttd6q3Vl2fahjk=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8 h1:xzYJEypr/85nBpB11F9br+3HUrpgb+fcm5iADzXXYEw=
github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8/go.mod h1:oX5x61PbNXchhh0oikYAH+4Pcfw5LKv21+Jnpr6r6Pc=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc h1:cAKDfWh5VpdgMhJosfJnn5/FoN2SRZ4p7fJNX58YPaU=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf h1:qet1QNfXsQxTZqLG4oE62mJzwPIB8+Tee4RNCL9ulrY=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/aulanov/go.dbus v0.0.0-20150729231527-25c3068a42a0 h1:EEDvbomAQ+MFWqJ9FM6RXyJTkc4lckyWsbc5CGQkG1Y=
github.com/aulanov/go.dbus v0.0.0-20150729231527-25c3068a42a0/go.mod h1:VHvUx+4lTCaJ8zUnEXF4cWEc9c8lnDt4PGLwlZ+3yaM=
github.com/aws/aws-lambda-go v1.19.0 h1:Cn28zA8Mic4NpR7p4IlaEW2srI+U3+I7tRqjFMpt/fs=
github.com/aws/aws-lambda-go v1.19.0/go.mod h1:jJmlefzPfGnckuHdXX7/80O3BvUUi12XOkbv4w9SGLU=
github.com/aws/aws-sdk-go v1.19.11/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/aws/aws-sdk-go v1.36.2 h1:UAeFPct+jHqWM+tgiqDrC9/sfbWj6wkcvpsJ+zdcsvA=
github.com/aws/aws-sdk-go v1.36.2/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
github.com/awslabs/goformation/v4 v4.14.0 h1:E2Pet9eIqA4qzt3dzzzE4YN83V4Kyfbcio0VokBC9TA=
github.com/awslabs/goformation/v4 v4.14.0/go.mod h1:GcJULxCJfloT+3pbqCluXftdEK2AD/UqpS3hkaaBntg=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/coreos/go-oidc v2.0.0+incompatible h1:+RStIopZ8wooMx+Vs5Bt8zMXxV1ABl5LbakNExNmZIg=
github.com/coreos/go-oidc v2.0.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=
github.com/coreos/go-oidc v2.2.1+incompatible h1:mh48q/BqXqgjVHpy2ZY7WnWAbenxRjsz9N1i1YxjHAk=
github.com/coreos/go-oidc v2.2.1+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/danieljoos/wincred v1.0.1 h1:fcRTaj17zzROVqni2FiToKUVg3MmJ4NtMSGCySPIr/g=
github.com/danieljoos/wincred v1.0.1/go.mod h1:SnuYRW9lp1oJrZX/dXJqr0cPK5gYXqx3EJbmjhLdK9U=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc=
github.com/coreos/go-oidc/v3 v3.17.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dvsekhvalnov/jose2go v0.0.0-20180829124132-7f401d37b68a h1:mq+R6XEM6lJX5VlLyZIrUSP8tSuJp82xTK89hvBwJbU=
github.com/dvsekhvalnov/jose2go v0.0.0-20180829124132-7f401d37b68a/go.mod h1:7BvyPhdbLxMXIYTFPLsyJRFMsKmOZnQmzh6Gb+uquuM=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/godbus/dbus v4.1.0+incompatible h1:WqqLRTsQic3apZUK9qC5sGNfXthmPXzUZ7nQPrNITa4=
github.com/godbus/dbus v4.1.0+incompatible/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1 h1:JFrFEBb2xKufg6XkJsJr+WbKb4FQlURi5RUcBveYu9k=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201117184057-ae444373da19/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c h1:6rhixN/i8ZofjG1Y75iExal34USq5p+wiN1tpie8IrU=
github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c/go.mod h1:NMPJylDgVpX0MLRlPy15sqSwOFv/U1GZ2m21JhFfek0=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174 h1:WlZsjVhE8Af9IcZDGgJGQpNflI3+MJSBhsgT5PCtzBQ=
github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174/go.mod h1:DqJ97dSdRW1W22yXSB90986pcOyQ7r45iio1KN2ez1A=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28=
github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM=
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1 h1:6QPYqodiu3GuPL+7mfx+NwDdp2eTkp9IfEUpgAwUN0o=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/keybase/go-keychain v0.0.0-20190408194155-7f2ef9fddce6 h1:hfM5TYph19rQBp3oOg4SVckf4ZmYrycciBJCWmxOcIE=
github.com/keybase/go-keychain v0.0.0-20190408194155-7f2ef9fddce6/go.mod h1:JJNrCn9otv/2QP4D7SMJBgaleKpOf66PnW6F5WGNRIc=
github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.4 h1:5Myjjh3JY/NaAi4IsUbHADytDyl1VE1Y9PXDlL+P/VQ=
github.com/kr/pty v1.1.4/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/magefile/mage v1.10.0 h1:3HiXzCUY12kh9bIuyXShaVe529fJfyqoVM42o/uom2g=
github.com/magefile/mage v1.10.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/onsi/ginkgo v1.5.0 h1:uZr+v/TFDdYkdA+j02sPO1kA5owrfjBGCJAogfIyThE=
github.com/onsi/ginkgo v1.5.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.2.0 h1:tQjc4uvqBp0z424R9V/S2L18penoUiwZftoY0t48IZ4=
github.com/onsi/gomega v1.2.0/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/schema v1.4.1 h1:jUg5hUjCSDZpNGLuXQOgIWGdlgrIdYvgQ0wZtdK1M3E=
github.com/gorilla/schema v1.4.1/go.mod h1:Dg5SSm5PV60mhF2NFaTV1xuYYj8tV8NOPRo4FggUMnM=
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
github.com/jeremija/gosubmit v0.2.8 h1:mmSITBz9JxVtu8eqbN+zmmwX7Ij2RidQxhcwRVI4wqA=
github.com/jeremija/gosubmit v0.2.8/go.mod h1:Ui+HS073lCFREXBbdfrJzMB57OI/bdxTiLtrDHHhFPI=
github.com/matoous/go-nanoid/v2 v2.1.0 h1:P64+dmq21hhWdtvZfEAofnvJULaRR1Yib0+PnU669bE=
github.com/matoous/go-nanoid/v2 v2.1.0/go.mod h1:KlbGNQ+FhrUNIHUxZdL63t7tl4LaPkZNpUULS8H4uVM=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 h1:J9b7z+QKAmPf4YLrFg6oQUotqHQeUNWwkvo7jZp1GLU=
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sanathkr/go-yaml v0.0.0-20170819195128-ed9d249f429b h1:jUK33OXuZP/l6babJtnLo1qsGvq6G9so9KMflGAm4YA=
github.com/sanathkr/go-yaml v0.0.0-20170819195128-ed9d249f429b/go.mod h1:8458kAagoME2+LN5//WxE71ysZ3B7r22fdgb7qVmXSY=
github.com/sanathkr/yaml v0.0.0-20170819201035-0056894fa522 h1:fOCp11H0yuyAt2wqlbJtbyPzSgaxHTv8uN1pMpkG1t8=
github.com/sanathkr/yaml v0.0.0-20170819201035-0056894fa522/go.mod h1:tQTYKOQgxoH3v6dEmdHiz4JG+nbxWwM5fgPQUpSZqVQ=
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/urfave/cli/v2 v2.2.0/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/xeipuuv/gojsonschema v0.0.0-20181112162635-ac52e6811b56 h1:yhqBHs09SmmUoNOHc9jgK4a60T3XFRtPAkYxVnqgY50=
github.com/xeipuuv/gojsonschema v0.0.0-20181112162635-ac52e6811b56/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.opencensus.io v0.21.0 h1:mU6zScU4U1YAFPHEHYk+3JC4SY7JxgkqS10ZOSyksNg=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4 h1:LYy1Hy3MJdrCdMwwzxA/dRok4ejH+RwNGbuoD9fCjto=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.5 h1:dntmOdLpSpHlVqbW5Eay97DelsZHe+55D+xC6i0dDS0=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/zitadel/logging v0.6.2 h1:MW2kDDR0ieQynPZ0KIZPrh9ote2WkxfBif5QoARDQcU=
github.com/zitadel/logging v0.6.2/go.mod h1:z6VWLWUkJpnNVDSLzrPSQSQyttysKZ6bCRongw0ROK4=
github.com/zitadel/oidc v1.13.5 h1:7jhh68NGZitLqwLiVU9Dtwa4IraJPFF1vS+4UupO93U=
github.com/zitadel/oidc v1.13.5/go.mod h1:rHs1DhU3Sv3tnI6bQRVlFa3u0lCwtR7S21WHY+yXgPA=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de h1:ikNHVSjEfnvz6sxdSPCaPt572qowuyMDMJLLm3Db3ig=
golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b h1:Wh+f8QHJXR411sJR8/vRBTZ7YapZaRvUcLFFJhusH0k=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0=
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381 h1:VXak5I6aEWmAXeQjA+QSZzlgNrpq9mjcfDemuexIKsU=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b h1:uwuIcX0g4Yl1NC5XAz37xsr2lTtcqevgzYNVt49waME=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58 h1:Mj83v+wSRNEar42a/MQgxk9X42TdEmrOl9i+y8WbxLo=
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 h1:qwRHBd0NqMbJxfbotnDhm2ByMI1Shq4Y6oRJo21SGJA=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190530182044-ad28b68e88f1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642 h1:B6caxRw+hozq68X2MY7jEpZh/cr4/aHLv9xU8Kkadrw=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3 h1:kzM6+9dur93BcC2kVlYl34cHU+TYZLanmpSJHVMmL64=
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/sys v0.0.0-20210301091718-77cc2087c03b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201202200335-bef1c476418a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
google.golang.org/api v0.30.0 h1:yfrXXP61wVuLb0vBcG6qaOoIoqYEzOQS8jum51jkv2w=
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
google.golang.org/api v0.36.0 h1:l2Nfbl2GPXdWorv+dT2XfinX2jOOw4zv1VhLstx+6rE=
google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0 h1:KxkO13IPW4Lslp2bz+KHP2E3gtFlrIGNThxkZQ3g+4c=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201203001206-6486ece9c497 h1:jDYzwXmX9tLnuG4sL85HPmE1ruErXOopALp2i/0AHnI=
google.golang.org/genproto v0.0.0-20201203001206-6486ece9c497/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0 h1:T7P4R73V3SSDPhH7WW7ATbfViLtmamH0DKrP3f9AuDI=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.33.2 h1:EQyQC3sa8M+p6Ulc8yy9SWSS2GVwyRc83gAbG8lrl4o=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/square/go-jose.v2 v2.3.0 h1:nLzhkFyl5bkblqYBoiWJUt5JkWOzmiaBtCxdJAqJd3U=
gopkg.in/square/go-jose.v2 v2.3.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI=
gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
inet.af/peercred v0.0.0-20210906144145-0893ea02156a h1:qdkS8Q5/i10xU2ArJMKYhVa1DORzBfYS/qA2UK2jheg=
inet.af/peercred v0.0.0-20210906144145-0893ea02156a/go.mod h1:FjawnflS/udxX+SvpsMgZfdqx2aykOlkISeAsADi5IU=

View File

@@ -10,31 +10,28 @@ import (
"strings"
"time"
"github.com/AlecAivazis/survey/v2"
"github.com/BurntSushi/toml"
"github.com/kballard/go-shellquote"
)
// Agent config for the sshrimp-agent agent
type Agent struct {
ProviderURL string
ClientID string
ClientSecret string
BrowserCommand []string
Socket string
ProviderURL string
ClientID string
ClientSecret string
Socket string
Scopes []string
KeyPath string
Port int
CAUrls []string
}
// CertificateAuthority config for the sshrimp-ca lambda
type CertificateAuthority struct {
Project string
AccountID int
Regions []string
FunctionName string
KeyAlias string
KeyPath string
ForceCommandRegex string
SourceAddressRegex string
UsernameRegex string
UsernameClaim string
UsernameRegexs []string
UsernameClaims []string
ValidAfterOffset string
ValidBeforeOffset string
Extensions []string
@@ -46,39 +43,6 @@ type SSHrimp struct {
CertificateAuthority CertificateAuthority
}
// List of supported regions for the config wizard
var SupportedAwsRegions = []string{
"ap-east-1",
"ap-northeast-1",
"ap-northeast-2",
"ap-south-1",
"ap-southeast-1",
"ap-southeast-2",
"ca-central-1",
"eu-central-1",
"eu-north-1",
"eu-west-1",
"eu-west-2",
"eu-west-3",
"me-south-1",
"sa-east-1",
"us-east-1",
"us-east-2",
"us-west-1",
"us-west-2",
}
var SupportedGcpRegions = []string{
"europe-west1",
"europe-west2",
"europe-west3",
"us-central1",
"us-east1",
"us-east4",
"asia-northeast1",
"asia-east2",
}
var supportedExtensions = []string{
"no-agent-forwarding",
"no-port-forwarding",
@@ -99,19 +63,17 @@ func NewSSHrimp() *SSHrimp {
// NewSSHrimpWithDefaults returns SSHrimp with defaults already set
func NewSSHrimpWithDefaults() *SSHrimp {
sshrimp := SSHrimp{
Agent{
ProviderURL: "https://accounts.google.com",
Socket: "/tmp/sshrimp.sock",
Socket: "~/.ssh/sshrimp.sock",
Scopes: []string{"openid", "email", "profile"},
},
CertificateAuthority{
FunctionName: "sshrimp",
KeyAlias: "alias/sshrimp",
ForceCommandRegex: "^$",
SourceAddressRegex: "^$",
UsernameRegex: `^(.*)@example\.com$`,
UsernameClaim: "email",
UsernameRegexs: []string{`^(.*)@example\.com$`},
UsernameClaims: []string{"email"},
ValidAfterOffset: "-5m",
ValidBeforeOffset: "+12h",
Extensions: []string{
@@ -126,21 +88,7 @@ func NewSSHrimpWithDefaults() *SSHrimp {
return &sshrimp
}
// DefaultPath of the sshrimp config file
var DefaultPath = "./sshrimp.toml"
// EnvVarName is the optional environment variable that if set overrides DefaultPath
var EnvVarName = "SSHRIMP_CONFIG"
// GetPath returns the default sshrimp config file path taking into account EnvVarName
func GetPath() string {
if configPathFromEnv, ok := os.LookupEnv(EnvVarName); ok && configPathFromEnv != "" {
return configPathFromEnv
}
return DefaultPath
}
func validateInt(val interface{}) error {
func validateInt(val any) error {
if str, ok := val.(string); ok {
if _, err := strconv.Atoi(str); err != nil {
return err
@@ -152,7 +100,7 @@ func validateInt(val interface{}) error {
return nil
}
func validateURL(val interface{}) error {
func validateURL(val any) error {
if str, ok := val.(string); ok {
if _, err := url.ParseRequestURI(str); err != nil {
return err
@@ -164,7 +112,7 @@ func validateURL(val interface{}) error {
return nil
}
func validateDuration(val interface{}) error {
func validateDuration(val any) error {
if str, ok := val.(string); ok {
if _, err := time.ParseDuration(str); err != nil {
return err
@@ -176,7 +124,7 @@ func validateDuration(val interface{}) error {
return nil
}
func validateAlias(val interface{}) error {
func validateAlias(val any) error {
if str, ok := val.(string); ok {
if !strings.HasPrefix(str, "alias/") {
return errors.New("KMS alias must begin with alias/")
@@ -188,200 +136,6 @@ func validateAlias(val interface{}) error {
return nil
}
func certificateAuthorityQuestions(config *SSHrimp) []*survey.Question {
defaultAccountID := ""
if config.CertificateAuthority.AccountID > 0 {
defaultAccountID = strconv.Itoa(config.CertificateAuthority.AccountID)
}
return []*survey.Question{
{
Name: "AccountID",
Prompt: &survey.Input{
Message: "AWS Account ID:",
Default: defaultAccountID,
Help: "12 Digit account ID. You could get this by running `aws sts get-caller-identity`",
},
Validate: survey.ComposeValidators(
survey.Required,
validateInt,
survey.MaxLength(12),
survey.MinLength(12),
),
},
{
Name: "Regions",
Prompt: &survey.MultiSelect{
Message: "AWS Region:",
Default: config.CertificateAuthority.Regions,
Help: "Select multiple regions for high availability. Each region gets it's own Lambda function and KMS key.",
Options: SupportedAwsRegions,
PageSize: 10,
},
Validate: survey.Required,
},
{
Name: "FunctionName",
Prompt: &survey.Input{
Message: "Lambda Function Name:",
Help: "The sshrimp certificate authority lambda will have this name.",
Default: config.CertificateAuthority.FunctionName,
},
Validate: survey.Required,
},
{
Name: "KeyAlias",
Prompt: &survey.Input{
Message: "KMS Key Alias:",
Help: "A name beginning with 'alias/' to easily refer to KMS keys in IAM policies and configuration files.",
Default: config.CertificateAuthority.KeyAlias,
},
Validate: survey.ComposeValidators(
survey.Required,
validateAlias,
),
},
{
Name: "UsernameClaim",
Prompt: &survey.Input{
Message: "Username claim in JWT",
Help: "Which claim in the JWT should be used as the username. See https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims",
Default: config.CertificateAuthority.UsernameClaim,
},
Validate: survey.Required,
},
{
Name: "UsernameRegex",
Prompt: &survey.Input{
Message: "Username regular expression",
Help: "A regular expression to validate the username present in the identity token. The first matching group will be used as the username enforced in the certificate.",
Default: config.CertificateAuthority.UsernameRegex,
},
Validate: survey.Required,
},
{
Name: "ForceCommandRegex",
Prompt: &survey.Input{
Message: "ForceCommand regular expression:",
Help: "A regular expression to validate the force command supplied by the user, but enforced in the certificate. See https://man.openbsd.org/sshd_config#ForceCommand",
Default: config.CertificateAuthority.ForceCommandRegex,
},
Validate: survey.Required,
},
{
Name: "SourceAddressRegex",
Prompt: &survey.Input{
Message: "Source IP address regular expression",
Help: "A regular expression to validate the source IP address supplied by the user, but enforced in the certificate.",
Default: config.CertificateAuthority.SourceAddressRegex,
},
Validate: survey.Required,
},
{
Name: "ValidAfterOffset",
Prompt: &survey.Input{
Message: "A time.now() offset for valid_after",
Help: "The amount to add to time.now() that the certificate will be valid FROM.",
Default: config.CertificateAuthority.ValidAfterOffset,
},
Validate: survey.ComposeValidators(
survey.Required,
validateDuration,
),
},
{
Name: "ValidBeforeOffset",
Prompt: &survey.Input{
Message: "A time.now() offset for valid_before",
Help: "The amount to add to time.now() that the certificate will be valid TO.",
Default: config.CertificateAuthority.ValidBeforeOffset,
},
Validate: survey.ComposeValidators(
survey.Required,
validateDuration,
),
},
{
Name: "Extensions",
Prompt: &survey.MultiSelect{
Message: "Certificate extensions",
Help: "Extensions to be added to the certificate, see https://man.openbsd.org/ssh-keygen#CERTIFICATES",
Default: config.CertificateAuthority.Extensions,
Options: supportedExtensions,
PageSize: 10,
},
Validate: survey.Required,
},
}
}
func agentQuestions(config *SSHrimp) []*survey.Question {
return []*survey.Question{
{
Name: "ProviderURL",
Prompt: &survey.Input{
Message: "OpenIDConnect Provider URL:",
Default: config.Agent.ProviderURL,
Help: "Get this from your OIDC provider. For example Google's is https://accounts.google.com.",
},
Validate: survey.ComposeValidators(survey.Required, validateURL),
},
{
Name: "ClientID",
Prompt: &survey.Input{
Message: "OpenIDConnect Client ID:",
Default: config.Agent.ClientID,
Help: "Get this from your OIDC provider. For example Google uses the format 1234-0a1b2bc3.apps.googleusercontent.com",
},
Validate: survey.Required,
},
{
Name: "ClientSecret",
Prompt: &survey.Input{
Message: "OpenIDConnect Client Secret (only if required):",
Default: config.Agent.ClientSecret,
Help: "Google requires the Client Secret even when using PKCE. Most OpenIDConnect provdiders don't. Read more about PKCE: https://tools.ietf.org/html/rfc7636",
},
},
{
Name: "Socket",
Prompt: &survey.Input{
Message: "sshrimp-agent socket:",
Default: config.Agent.Socket,
Help: "Path of the socket for the sshrimp-agent to listen on. Create a unique one for each instance of sshrimp-agent.",
},
Validate: survey.Required,
},
}
}
func browserCommandQuestions(config *SSHrimp) []*survey.Question {
return []*survey.Question{
{
Name: "BrowserCommand",
Prompt: &survey.Input{
Message: "Command to open a browser:",
Default: shellquote.Join(config.Agent.BrowserCommand...),
Help: "Optionally {} will be substituted with the URL to open.",
},
Validate: survey.Required,
},
}
}
func configFileQuestions(configPath string) []*survey.Question {
return []*survey.Question{
{
Name: "ConfigPath",
Prompt: &survey.Input{
Message: "File path to write the new config:",
Default: configPath,
Help: "Set environment variable SSHRIMP_CONFIG to this path if different from ./sshrimp.toml",
},
Validate: survey.Required,
},
}
}
func (c *SSHrimp) Read(configPath string) error {
_, err := toml.DecodeFile(configPath, c)
return err
@@ -403,56 +157,3 @@ func (c *SSHrimp) Write(configPath string) error {
return nil
}
// Wizard launches a interactive question/answer terminal prompt to create a config file
func Wizard(configPath string, config *SSHrimp) (string, error) {
// Create a new config that doesn't have any default values, otherwise survey appends to the defaults.
newConfig := NewSSHrimp()
if err := survey.Ask(certificateAuthorityQuestions(config), &newConfig.CertificateAuthority); err != nil {
return "", err
}
if err := survey.Ask(agentQuestions(config), &newConfig.Agent); err != nil {
return "", err
}
// Ask BrowserCommand separately so we can store it as a []string, currently not supported by survey.
var browserCommand string
if err := survey.Ask(browserCommandQuestions(config), &browserCommand); err != nil {
return "", err
}
// Split the command by sh rules using shellquote. The command is stored as a slice of arguments.
words, err := shellquote.Split(browserCommand)
if err != nil {
return "", err
}
newConfig.Agent.BrowserCommand = words
// Confirm config file path, and keep prompting if exists and user chooses not to overwrite
var overwriteIfExists = false
for !overwriteIfExists {
if err := survey.Ask(configFileQuestions(configPath), &configPath); err != nil {
return "", err
}
if _, err := os.Stat(configPath); err == nil {
// File exists, confirm to be overwritten
if err := survey.AskOne(&survey.Confirm{
Message: "File exists, overwrite?",
Default: false,
}, &overwriteIfExists); err != nil {
return "", err
}
} else {
// File doesn't exist, break and save the file
break
}
}
// Write the new configuration to a file
newConfig.Write(configPath)
return configPath, nil
}

39
internal/http/client.go Normal file
View File

@@ -0,0 +1,39 @@
package http
import (
"net"
"net/http"
"time"
)
type Transport struct {
http.RoundTripper
UserAgent string
}
func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) {
if req.Header.Get("User-Agent") == "" {
req.Header.Set("User-Agent", t.UserAgent)
}
if t.RoundTripper == nil {
d := &net.Dialer{
Timeout: 2 * time.Second,
KeepAlive: 30 * time.Second,
}
t.RoundTripper = &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: d.DialContext,
ForceAttemptHTTP2: false,
MaxIdleConns: 10,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 2 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}
}
return t.RoundTripper.RoundTrip(req)
}
var Client = &http.Client{
Transport: &Transport{UserAgent: "sshrimp-agent"},
Timeout: 10 * time.Second,
}

View File

@@ -2,23 +2,28 @@ package identity
import (
"context"
"encoding/base64"
"errors"
"regexp"
"strings"
"unicode/utf8"
"git.narnian.us/lordwelch/sshrimp/internal/config"
"github.com/coreos/go-oidc"
"gitea.narnian.us/lordwelch/sshrimp/internal/config"
"github.com/coreos/go-oidc/v3/oidc"
"github.com/sirupsen/logrus"
)
// Identity holds information required to verify an OIDC identity token
type Identity struct {
ctx context.Context
verifier *oidc.IDTokenVerifier
usernameRE *regexp.Regexp
usernameClaim string
ctx context.Context
verifier *oidc.IDTokenVerifier
usernameREs []*regexp.Regexp
usernameClaims []string
log *logrus.Entry
}
// NewIdentity return a new Identity, with default values and oidc proivder information populated
func NewIdentity(c *config.SSHrimp) (*Identity, error) {
func NewIdentity(log *logrus.Entry, c *config.SSHrimp) (*Identity, error) {
ctx := context.Background()
provider, err := oidc.NewProvider(ctx, c.Agent.ProviderURL)
if err != nil {
@@ -30,38 +35,107 @@ func NewIdentity(c *config.SSHrimp) (*Identity, error) {
SupportedSigningAlgs: []string{"RS256"},
}
regexes := make([]*regexp.Regexp, 0, len(c.CertificateAuthority.UsernameRegexs))
for _, regex := range c.CertificateAuthority.UsernameRegexs {
regexes = append(regexes, regexp.MustCompile(regex))
}
return &Identity{
ctx: ctx,
verifier: provider.Verifier(oidcConfig),
usernameRE: regexp.MustCompile(c.CertificateAuthority.UsernameRegex),
usernameClaim: c.CertificateAuthority.UsernameClaim,
ctx: ctx,
verifier: provider.Verifier(oidcConfig),
usernameREs: regexes,
usernameClaims: c.CertificateAuthority.UsernameClaims,
log: log,
}, nil
}
// Validate an identity token
func (i *Identity) Validate(token string) (string, error) {
func (i *Identity) Validate(token string) ([]string, error) {
idToken, err := i.verifier.Verify(i.ctx, token)
if err != nil {
return "", errors.New("failed to verify identity token: " + err.Error())
return nil, errors.New("failed to verify identity token: " + err.Error())
}
return i.getUsernames(idToken)
}
var claims map[string]interface{}
func (i *Identity) getUsernames(idToken *oidc.IDToken) ([]string, error) {
var claims map[string]any
if err := idToken.Claims(&claims); err != nil {
return "", errors.New("failed to parse claims: " + err.Error())
return nil, errors.New("failed to parse claims: " + err.Error())
}
usernames := make([]string, 0, len(i.usernameClaims))
for idx, claim := range i.usernameClaims {
claimedUsername, ok := claims[i.usernameClaim].(string)
if !ok {
return "", errors.New("configured username claim not in identity token")
claimedUsernames := i.getClaim(claim, claims)
if len(claimedUsernames) == 0 {
i.log.Errorf("Did not find a username using: getClaim(%#v, %#v)", claim, claims)
}
if idx < len(i.usernameREs) {
for _, name := range claimedUsernames {
usernames = append(usernames, parseUsername(name, i.usernameREs[idx]))
}
} else {
usernames = append(usernames, claimedUsernames...)
}
}
return i.parseUsername(claimedUsername)
i.log.Infof("Adding usernames: %v", usernames)
if len(usernames) < 1 {
return nil, errors.New("configured username claim not in identity token")
}
return usernames, nil
}
func (i *Identity) parseUsername(username string) (string, error) {
if match := i.usernameRE.FindStringSubmatch(username); match != nil {
return match[1], nil
func parseUsername(username string, re *regexp.Regexp) string {
if match := re.FindStringSubmatch(username); match != nil {
return match[1]
}
return "", errors.New("unable to parse username from claim")
return ""
}
func (i *Identity) getClaim(claim string, claims map[string]any) []string {
usernames := make([]string, 0, 2)
parts := strings.Split(claim, ".")
f:
for idx, part := range parts {
switch v := claims[part].(type) {
case map[string]any:
claims = v
case []map[string]string:
for _, claimItem := range v {
name, ok := claimItem[parts[idx+1]]
if ok {
usernames = append(usernames, name)
}
}
break f
case []any:
for _, value := range v {
if name, ok := value.(string); ok {
usernames = append(usernames, name)
}
}
break f
case string:
usernames = append(usernames, v)
default:
break f
}
}
return i.base64Decode(usernames)
}
func (i *Identity) base64Decode(names []string) []string {
for idx, name := range names {
i.log.Debugf("Attempting to decode %q as base64\n", name)
decoded, err := base64.RawURLEncoding.DecodeString(name)
if err == nil && utf8.Valid(decoded) {
names[idx] = string(decoded)
i.log.Debugf("Successfully decoded %q as base64\n", names[idx])
}
}
return names
}

View File

@@ -1,42 +0,0 @@
package signer
import (
"crypto"
"errors"
"io"
"golang.org/x/crypto/ssh"
)
type sshAlgorithmSigner struct {
algorithm string
signer ssh.AlgorithmSigner
}
// PublicKey returns the wrapped signers public key
func (s *sshAlgorithmSigner) PublicKey() ssh.PublicKey {
return s.signer.PublicKey()
}
// Sign uses the correct algorithm to sign the certificate
func (s *sshAlgorithmSigner) Sign(rand io.Reader, data []byte) (*ssh.Signature, error) {
return s.signer.SignWithAlgorithm(rand, data, s.algorithm)
}
// NewAlgorithmSignerFromSigner returns a ssh.Signer with a different default algorithm.
// Waiting for upstream changes to x/crypto/ssh, see: https://github.com/golang/go/issues/36261
func NewAlgorithmSignerFromSigner(signer crypto.Signer, algorithm string) (ssh.Signer, error) {
sshSigner, err := ssh.NewSignerFromSigner(signer)
if err != nil {
return nil, err
}
algorithmSigner, ok := sshSigner.(ssh.AlgorithmSigner)
if !ok {
return nil, errors.New("unable to cast to ssh.AlgorithmSigner")
}
s := sshAlgorithmSigner{
signer: algorithmSigner,
algorithm: algorithm,
}
return &s, nil
}

View File

@@ -1,67 +0,0 @@
package signer
import (
"crypto"
"crypto/x509"
"fmt"
"io"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/kms"
"github.com/aws/aws-sdk-go/service/kms/kmsiface"
)
// KMSSigner an AWS asymetric crypto signer
type AWSSigner struct {
crypto.Signer
client kmsiface.KMSAPI
key string
}
// NewKMSSigner return a new instsance of AWSSigner
func NewAWSSigner(key string) *AWSSigner {
sess := session.Must(session.NewSession())
return &AWSSigner{
key: key,
client: kms.New(sess),
}
}
// Public returns the public key from KMS
func (s *AWSSigner) Public() crypto.PublicKey {
response, err := s.client.GetPublicKey(&kms.GetPublicKeyInput{
KeyId: &s.key,
})
if err != nil {
fmt.Printf(err.Error())
return nil
}
publicKey, err := x509.ParsePKIXPublicKey(response.PublicKey)
if err != nil {
fmt.Printf(err.Error())
return nil
}
return publicKey
}
// Sign a digest with the private key in KMS
func (s *AWSSigner) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) {
response, err := s.client.Sign(&kms.SignInput{
KeyId: &s.key,
Message: digest,
MessageType: aws.String(kms.MessageTypeDigest),
SigningAlgorithm: aws.String(kms.SigningAlgorithmSpecRsassaPkcs1V15Sha256),
})
if err != nil {
return nil, err
}
return response.Signature, nil
}

View File

@@ -1,105 +0,0 @@
package signer
import (
"context"
"crypto"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"fmt"
"io"
kms "cloud.google.com/go/kms/apiv1"
kmspb "google.golang.org/genproto/googleapis/cloud/kms/v1"
)
// GCPSigner a GCP asymetric crypto signer
type GCPSigner struct {
crypto.Signer
ctx context.Context
client *kms.KeyManagementClient
key string
}
// NewGCPSSigner return a new instsance of NewGCPSSigner
func NewGCPSSigner(key string) *GCPSigner {
ctx := context.Background()
c, err := kms.NewKeyManagementClient(ctx)
if err != nil {
panic(err)
}
return &GCPSigner{
ctx: ctx,
client: c,
key: key,
}
}
// Public returns the public key from KMS
func (s *GCPSigner) Public() crypto.PublicKey {
response, err := s.client.GetPublicKey(s.ctx, &kmspb.GetPublicKeyRequest{
Name: s.key,
})
if err != nil {
fmt.Println(err.Error())
return nil
}
switch response.GetAlgorithm() {
case kmspb.CryptoKeyVersion_RSA_SIGN_PKCS1_2048_SHA256,
kmspb.CryptoKeyVersion_RSA_SIGN_PKCS1_3072_SHA256,
kmspb.CryptoKeyVersion_RSA_SIGN_PKCS1_4096_SHA256,
kmspb.CryptoKeyVersion_RSA_SIGN_PKCS1_4096_SHA512:
// awesome
default:
fmt.Println("crypto key has the wrong algorithm, must be rsa with PKCS1 padding")
return nil
}
pubPem := response.GetPem()
// pubAlg := response.GetAlgorithm()
pemBlock, _ := pem.Decode([]byte(pubPem))
publicKey, err := x509.ParsePKIXPublicKey(pemBlock.Bytes)
if err != nil {
fmt.Println(err.Error())
return nil
}
return publicKey
}
// Sign a digest with the private key in KMS
func (s *GCPSigner) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) {
var dig *kmspb.Digest = &kmspb.Digest{}
switch opts {
case crypto.SHA256:
dig.Digest = &kmspb.Digest_Sha256{
Sha256: digest,
}
case crypto.SHA384:
dig.Digest = &kmspb.Digest_Sha384{
Sha384: digest,
}
case crypto.SHA512:
dig.Digest = &kmspb.Digest_Sha512{
Sha512: digest,
}
}
response, err := s.client.AsymmetricSign(s.ctx, &kmspb.AsymmetricSignRequest{
Name: s.key,
Digest: dig,
})
if err != nil {
return nil, err
}
sig := response.GetSignature()
pubKey := s.Public()
rKey := pubKey.(*rsa.PublicKey)
err = rsa.VerifyPKCS1v15(rKey, crypto.SHA256, digest, sig)
if err != nil {
return nil, err
}
return sig, nil
}

View File

@@ -5,21 +5,18 @@ import (
"crypto/rand"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"io"
"math"
"math/big"
"net/http"
"regexp"
"sort"
"strings"
"time"
"git.narnian.us/lordwelch/sshrimp/internal/config"
"git.narnian.us/lordwelch/sshrimp/internal/identity"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/lambda"
"github.com/pkg/errors"
"gitea.narnian.us/lordwelch/sshrimp/internal/config"
"gitea.narnian.us/lordwelch/sshrimp/internal/http"
"gitea.narnian.us/lordwelch/sshrimp/internal/identity"
"github.com/sirupsen/logrus"
"golang.org/x/crypto/ssh"
@@ -42,21 +39,17 @@ type SSHrimpEvent struct {
ForceCommand string `json:"forcecommand"`
}
// SignCertificateAllRegions iterate through each configured region if there is an error signing the certificate
func SignCertificateAllRegions(publicKey ssh.PublicKey, token string, forceCommand string, c *config.SSHrimp) (*ssh.Certificate, error) {
// SignCertificateAllURLs iterate through each configured url if there is an error signing the certificate
func SignCertificateAllURLs(publicKey ssh.PublicKey, token string, forceCommand string, urls []string) (*ssh.Certificate, error) {
var (
err error
err = fmt.Errorf("no urls found to sign certificate")
cert *ssh.Certificate
)
// Try each configured region before exiting if there is an error
for _, region := range c.CertificateAuthority.Regions {
if i := sort.SearchStrings(config.SupportedAwsRegions, region); i < len(config.SupportedAwsRegions) && config.SupportedAwsRegions[i] == region {
cert, err = SignCertificateAWS(publicKey, token, forceCommand, region, c)
} else if i := sort.SearchStrings(config.SupportedGcpRegions, region); i < len(config.SupportedGcpRegions) && config.SupportedGcpRegions[i] == region {
cert, err = SignCertificateGCP(publicKey, token, forceCommand, region, c)
}
// Try each configured url before exiting if there is an error
Log.Logger.Tracef("Attempting to sign cert with urls %v", urls)
for _, url := range urls {
cert, err = SignCertificate(publicKey, token, forceCommand, url)
if err == nil {
return cert, nil
}
@@ -64,8 +57,8 @@ func SignCertificateAllRegions(publicKey ssh.PublicKey, token string, forceComma
return nil, err
}
// SignCertificateGCP given a public key, identity token and forceCommand, invoke the sshrimp-ca GCP function
func SignCertificateGCP(publicKey ssh.PublicKey, token string, forceCommand string, region string, c *config.SSHrimp) (*ssh.Certificate, error) {
// SignCertificate given a public key, identity token and forceCommand, invoke the sshrimp-ca GCP function
func SignCertificate(publicKey ssh.PublicKey, token string, forceCommand string, url string) (*ssh.Certificate, error) {
// Setup the JSON payload for the SSHrimp CA
payload, err := json.Marshal(SSHrimpEvent{
PublicKey: string(ssh.MarshalAuthorizedKey(publicKey)),
@@ -76,20 +69,23 @@ func SignCertificateGCP(publicKey ssh.PublicKey, token string, forceCommand stri
return nil, err
}
result, err := http.Post(fmt.Sprintf("https://%s-%s.cloudfunctions.net/%s", region, c.CertificateAuthority.Project, c.CertificateAuthority.FunctionName), "application/json", bytes.NewReader(payload))
Log.Logger.Tracef("Posting to url %s", url)
result, err := http.Client.Post(url, "application/json", bytes.NewReader(payload))
if err != nil {
return nil, errors.Wrap(err, "http post failed: "+err.Error())
return nil, fmt.Errorf("http post failed: %w", err)
}
resbody, err := ioutil.ReadAll(result.Body)
Log.Logger.Tracef("Reading body length: %d", result.ContentLength)
resbody, err := io.ReadAll(result.Body)
if err != nil {
return nil, errors.Wrap(err, "failed to retrieve the response from sshrimp-ca")
return nil, fmt.Errorf("failed to retrieve the response from sshrimp-ca: %w", err)
}
// Parse the result form the lambda to extract the certificate
sshrimpResult := SSHrimpResult{}
Log.Logger.Tracef("parsing result: %v", string(resbody))
err = json.Unmarshal(resbody, &sshrimpResult)
if err != nil {
return nil, errors.Wrap(err, "failed to parse json response from sshrimp-ca.: "+string(resbody))
return nil, fmt.Errorf("failed to parse json response from sshrimp-ca: %w: %v", err, string(resbody))
}
if result.StatusCode != 200 {
@@ -103,63 +99,14 @@ func SignCertificateGCP(publicKey ssh.PublicKey, token string, forceCommand stri
// Parse the certificate received by sshrimp-ca
cert, _, _, _, err := ssh.ParseAuthorizedKey([]byte(sshrimpResult.Certificate))
Log.Logger.Tracef("parsing cert: %v", err)
if err != nil {
return nil, err
}
return cert.(*ssh.Certificate), nil
}
// SignCertificateAWS given a public key, identity token and forceCommand, invoke the sshrimp-ca lambda function
func SignCertificateAWS(publicKey ssh.PublicKey, token string, forceCommand string, region string, c *config.SSHrimp) (*ssh.Certificate, error) {
// Create a lambdaService using the new temporary credentials for the role
session := session.Must(session.NewSession(&aws.Config{
Region: aws.String(region),
}))
lambdaService := lambda.New(session)
// Setup the JSON payload for the SSHrimp CA
payload, err := json.Marshal(SSHrimpEvent{
PublicKey: string(ssh.MarshalAuthorizedKey(publicKey)),
Token: token,
ForceCommand: forceCommand,
})
if err != nil {
return nil, err
}
// Invoke the SSHrimp lambda
result, err := lambdaService.Invoke(&lambda.InvokeInput{
FunctionName: aws.String(c.CertificateAuthority.FunctionName),
Payload: payload,
})
if err != nil {
return nil, err
}
if *result.StatusCode != 200 {
return nil, fmt.Errorf("sshrimp returned status code %d", *result.StatusCode)
}
// Parse the result form the lambda to extract the certificate
sshrimpResult := SSHrimpResult{}
err = json.Unmarshal(result.Payload, &sshrimpResult)
if err != nil {
return nil, errors.Wrap(err, "failed to parse json response from sshrimp-ca")
}
// These error types and messages can also come from the aws-sdk-go lambda handler
if sshrimpResult.ErrorType != "" || sshrimpResult.ErrorMessage != "" {
return nil, fmt.Errorf("%s: %s", sshrimpResult.ErrorType, sshrimpResult.ErrorMessage)
}
// Parse the certificate received by sshrimp-ca
cert, _, _, _, err := ssh.ParseAuthorizedKey([]byte(sshrimpResult.Certificate))
if err != nil {
return nil, err
}
return cert.(*ssh.Certificate), nil
}
func ValidateRequest(event SSHrimpEvent, c *config.SSHrimp, requestID string, functionID string) (ssh.Certificate, error) {
func ValidateRequest(log *logrus.Entry, event SSHrimpEvent, c *config.SSHrimp, requestID string, ca ssh.PublicKey) (ssh.Certificate, error) {
// Validate the user supplied public key
publicKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(event.PublicKey))
if err != nil {
@@ -167,8 +114,11 @@ func ValidateRequest(event SSHrimpEvent, c *config.SSHrimp, requestID string, fu
}
// Validate the user supplied identity token with the loaded configuration
i, _ := identity.NewIdentity(c)
username, err := i.Validate(event.Token)
i, err := identity.NewIdentity(log, c)
if err != nil {
return ssh.Certificate{}, err
}
usernames, err := i.Validate(event.Token)
if err != nil {
return ssh.Certificate{}, err
}
@@ -191,12 +141,12 @@ func ValidateRequest(event SSHrimpEvent, c *config.SSHrimp, requestID string, fu
}
// Generate a random nonce for the certificate
bytes := make([]byte, 32)
nonce := make([]byte, len(bytes)*2)
if _, err := rand.Read(bytes); err != nil {
nonceHex := make([]byte, 32)
nonce := make([]byte, len(nonceHex)*2)
if _, err := rand.Read(nonceHex); err != nil {
return ssh.Certificate{}, err
}
hex.Encode(nonce, bytes)
hex.Encode(nonce, nonceHex)
// Generate a random serial number
serial, err := rand.Int(rand.Reader, big.NewInt(math.MaxInt64))
@@ -227,24 +177,22 @@ func ValidateRequest(event SSHrimpEvent, c *config.SSHrimp, requestID string, fu
// https://github.com/Netflix/bless
keyID := fmt.Sprintf("request[%s] for[%s] from[%s] command[%s] ssh_key[%s] ca[%s] valid_to[%s]",
requestID,
username,
strings.Join(usernames, ", "),
event.SourceAddress,
event.ForceCommand,
ssh.FingerprintSHA256(publicKey),
functionID,
ssh.FingerprintSHA256(ca),
validBefore.Format("2006/01/02 15:04:05"),
)
// Create the certificate struct with all our configured alues
certificate := ssh.Certificate{
Nonce: nonce,
Key: publicKey,
Serial: serial.Uint64(),
CertType: ssh.UserCert,
KeyId: keyID,
ValidPrincipals: []string{
username,
},
Nonce: nonce,
Key: publicKey,
Serial: serial.Uint64(),
CertType: ssh.UserCert,
KeyId: keyID,
ValidPrincipals: usernames,
Permissions: ssh.Permissions{
CriticalOptions: criticalOptions,
Extensions: extensions,

View File

@@ -0,0 +1,161 @@
package sshrimpagent
import (
"fmt"
"io"
"net"
"net/http"
"net/url"
"time"
"golang.org/x/crypto/ssh"
"gitea.narnian.us/lordwelch/sshrimp/internal/config"
sshrimp_http "gitea.narnian.us/lordwelch/sshrimp/internal/http"
"github.com/google/uuid"
"github.com/zitadel/oidc/pkg/client/rp"
httphelper "github.com/zitadel/oidc/pkg/http"
"github.com/zitadel/oidc/pkg/oidc"
"golang.org/x/exp/slices"
)
var hashKey = []byte(uuid.New().String())[:16]
type OidcClient struct {
ListenAddress string
*http.Server
oidcMux *http.ServeMux
OIDCToken chan *oidc.Tokens
Certificate *ssh.Certificate
*config.SSHrimp
}
func newOIDCClient(c *config.SSHrimp) (*OidcClient, error) {
if len(c.Agent.Scopes) < 1 {
c.Agent.Scopes = []string{"openid", "email", "profile"}
}
if !slices.Contains(c.Agent.Scopes, "openid") {
c.Agent.Scopes = append([]string{"scopes"}, c.Agent.Scopes...)
}
token := make(chan *oidc.Tokens)
oidcMux := http.NewServeMux()
return &OidcClient{
oidcMux: oidcMux,
Server: &http.Server{
Addr: fmt.Sprintf("localhost:%d", c.Agent.Port),
Handler: oidcMux,
ReadTimeout: time.Minute / 2,
ReadHeaderTimeout: time.Minute / 2,
WriteTimeout: time.Minute / 2,
IdleTimeout: time.Minute / 2,
},
OIDCToken: token,
Certificate: &ssh.Certificate{},
SSHrimp: c,
}, nil
}
func (o *OidcClient) baseURI() url.URL {
return url.URL{
Scheme: "http",
Host: o.Addr,
}
}
func (o *OidcClient) ListenAndServe() error {
ln, err := net.Listen("tcp", o.Addr)
if err != nil {
return err
}
o.Addr = ln.Addr().String()
if err = o.setupHandlers(); err != nil {
return err
}
return o.Serve(ln)
}
func (o *OidcClient) setupHandlers() error {
redirectURI := o.baseURI()
redirectURI.Path = "/auth/callback"
successURI := o.baseURI()
successURI.Path = "/success"
var CAKey []byte
resp, err := sshrimp_http.Client.Get(o.Agent.CAUrls[0])
if err == nil && resp.Header.Get("Content-Type") == "text/x-ssh-public-key" {
CAKey, err = io.ReadAll(resp.Body)
if err != nil {
CAKey = []byte{}
}
}
cookieHandler := httphelper.NewCookieHandler(hashKey, nil)
options := []rp.Option{
rp.WithCookieHandler(cookieHandler),
rp.WithVerifierOpts(rp.WithIssuedAtOffset(5 * time.Second)),
rp.WithHTTPClient(sshrimp_http.Client),
}
options = append(options, rp.WithPKCE(cookieHandler))
if o.Agent.KeyPath != "" {
options = append(options, rp.WithJWTProfile(rp.SignerFromKeyPath(o.Agent.KeyPath)))
}
provider, err := rp.NewRelyingPartyOIDC(o.Agent.ProviderURL, o.Agent.ClientID, o.Agent.ClientSecret, redirectURI.String(), o.Agent.Scopes, options...)
if err != nil {
return fmt.Errorf("error creating provider: %w", err)
}
// generate some state (representing the state of the user in your application,
// e.g. the page where he was before sending him to login
state := func() string {
return uuid.New().String()
}
// register the AuthURLHandler at your preferred path
// the AuthURLHandler creates the auth request and redirects the user to the auth server
// including state handling with secure cookie and the possibility to use PKCE
o.oidcMux.Handle("/login", rp.AuthURLHandler(state, provider))
o.oidcMux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if o.Certificate != nil && o.Certificate.SignatureKey != nil {
key := ssh.MarshalAuthorizedKey(o.Certificate.SignatureKey)
if len(CAKey) < 3 {
CAKey = key
}
if !slices.Equal(key, CAKey) {
Log.Errorf("Certificate Authority key has changed from %#v to %#v", string(CAKey), string(key))
fmt.Fprintf(w, "\n\nCertificate Authority key has changed from \n%#v\nto \n%#v", string(CAKey), string(key))
}
}
fmt.Fprintf(w, "The SSH CA currently in use is:\n%s", CAKey)
Log.Printf("The SSH CA currently in use is:\n%s", CAKey)
}))
o.oidcMux.Handle(successURI.Path, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Return to the CLI.")
if o.Certificate != nil && o.Certificate.SignatureKey != nil {
key := ssh.MarshalAuthorizedKey(o.Certificate.SignatureKey)
if len(CAKey) < 3 {
CAKey = key
}
if !slices.Equal(key, CAKey) {
Log.Errorf("Certificate Authority key has changed from %#v to %#v", string(CAKey), string(key))
fmt.Fprintf(w, "\n\nCertificate Authority key has changed from \n%#v\nto \n%#v", string(CAKey), string(key))
}
}
fmt.Fprintf(w, "The SSH CA currently in use is: %s", CAKey)
Log.Printf("The SSH CA currently in use is:\n%s", CAKey)
}))
marshalUserinfo := func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens, state string, rp rp.RelyingParty) {
o.OIDCToken <- tokens
w.Header().Add("location", successURI.String())
w.WriteHeader(301)
}
// register the CodeExchangeHandler at the callbackPath
// the CodeExchangeHandler handles the auth response, creates the token request and calls the callback function
// with the returned tokens from the token endpoint
o.oidcMux.Handle(redirectURI.Path, rp.CodeExchangeHandler(marshalUserinfo, provider))
return nil
}

View File

@@ -3,12 +3,14 @@ package sshrimpagent
import (
"crypto/rand"
"errors"
"net/http"
"time"
"git.narnian.us/lordwelch/aws-oidc/provider"
"git.narnian.us/lordwelch/sshrimp/internal/config"
"git.narnian.us/lordwelch/sshrimp/internal/signer"
"gitea.narnian.us/lordwelch/sshrimp/internal/config"
"gitea.narnian.us/lordwelch/sshrimp/internal/signer"
"github.com/pkg/browser"
"github.com/sirupsen/logrus"
"github.com/zitadel/oidc/pkg/oidc"
"golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/agent"
)
@@ -16,39 +18,64 @@ import (
var Log *logrus.Entry
type sshrimpAgent struct {
providerConfig provider.ProviderConfig
signer ssh.Signer
certificate *ssh.Certificate
token *provider.OAuth2Token
config *config.SSHrimp
oidcClient *OidcClient
signer ssh.Signer
certificate *ssh.Certificate
token *oidc.Tokens
config *config.SSHrimp
}
// NewSSHrimpAgent returns an agent.Agent capable of signing certificates with a SSHrimp Certificate Authority
func NewSSHrimpAgent(c *config.SSHrimp, signer ssh.Signer) agent.Agent {
providerConfig := provider.ProviderConfig{
ClientID: c.Agent.ClientID,
ClientSecret: c.Agent.ClientSecret,
ProviderURL: c.Agent.ProviderURL,
PKCE: true,
Nonce: true,
AgentCommand: c.Agent.BrowserCommand,
func NewSSHrimpAgent(c *config.SSHrimp, signer ssh.Signer) (agent.Agent, error) {
oidcClient, err := newOIDCClient(c)
if err != nil {
return nil, err
}
go func() {
for {
if err = oidcClient.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
Log.Logger.Errorf("Server failed: %v", err)
}
}
}()
return &sshrimpAgent{
providerConfig: providerConfig,
signer: signer,
certificate: &ssh.Certificate{},
token: &provider.OAuth2Token{},
config: c,
oidcClient: oidcClient,
signer: signer,
certificate: &ssh.Certificate{},
token: nil,
config: c,
}, nil
}
// authenticate authenticates a oidc token
func (r *sshrimpAgent) authenticate() error {
var err error
if r.token != nil {
err = oidc.CheckExpiration(r.token.IDTokenClaims, 0)
} else {
err = errors.New("no token provided")
}
if err != nil {
Log.Debugln("Token is expired re-authenticating http://" + r.oidcClient.Addr + "/login")
_ = browser.OpenURL("http://" + r.oidcClient.Addr + "/login")
select {
case r.token = <-r.oidcClient.OIDCToken:
return nil
case <-time.After(30 * time.Second):
return errors.New("Timeout")
}
}
return err
}
// RemoveAll clears the current certificate and identity token (including refresh token)
func (r *sshrimpAgent) RemoveAll() error {
Log.Debugln("Removing identity token and certificate")
r.certificate = &ssh.Certificate{}
r.token = &provider.OAuth2Token{}
r.oidcClient.Certificate = r.certificate
r.token = nil
return nil
}
@@ -75,19 +102,20 @@ func (r *sshrimpAgent) List() ([]*agent.Key, error) {
if r.certificate.ValidBefore != uint64(ssh.CertTimeInfinity) && (time.Now().After(validEndDate) || validEndDate.Unix() < 0) {
Log.Traceln("Certificate has expired")
Log.Traceln("authenticating token")
err := r.providerConfig.Authenticate(r.token)
err := r.authenticate()
if err != nil {
Log.Errorf("authenticating the token failed: %v", err)
return nil, err
}
Log.Traceln("signing certificate")
cert, err := signer.SignCertificateAllRegions(r.signer.PublicKey(), r.token.IDToken, "", r.config)
cert, err := signer.SignCertificateAllURLs(r.signer.PublicKey(), r.token.IDToken, "", r.config.Agent.CAUrls)
if err != nil {
Log.Errorf("signing certificate failed: %v", err)
return nil, err
}
r.certificate = cert
r.oidcClient.Certificate = r.certificate
}
var ids []*agent.Key
@@ -123,7 +151,7 @@ func (r *sshrimpAgent) SignWithFlags(key ssh.PublicKey, data []byte, flags agent
if ok {
if flags&agent.SignatureFlagRsaSha512 == agent.SignatureFlagRsaSha512 {
Log.Traceln("sha 512 requested")
s, err := sign.SignWithAlgorithm(rand.Reader, data, ssh.SigAlgoRSASHA2512)
s, err := sign.SignWithAlgorithm(rand.Reader, data, ssh.KeyAlgoRSASHA512)
if err == nil {
Log.Debugln("sha 512 available")
return s, nil
@@ -131,7 +159,7 @@ func (r *sshrimpAgent) SignWithFlags(key ssh.PublicKey, data []byte, flags agent
}
if flags&agent.SignatureFlagRsaSha256 == agent.SignatureFlagRsaSha256 {
Log.Traceln("sha 256 requested")
s, err := sign.SignWithAlgorithm(rand.Reader, data, ssh.SigAlgoRSASHA2256)
s, err := sign.SignWithAlgorithm(rand.Reader, data, ssh.KeyAlgoRSASHA256)
if err == nil {
Log.Debugln("sha 256 available")
return s, nil
@@ -141,6 +169,7 @@ func (r *sshrimpAgent) SignWithFlags(key ssh.PublicKey, data []byte, flags agent
Log.Traceln("signing data")
return r.Sign(key, data)
}
func (r *sshrimpAgent) Extension(extensionType string, contents []byte) ([]byte, error) {
return nil, agent.ErrExtensionUnsupported
}

View File

@@ -1,38 +0,0 @@
//+build mage
package main
import (
"fmt"
"os"
"github.com/magefile/mage/mg"
// mage:import ca
"git.narnian.us/lordwelch/sshrimp/tools/mage/ca"
// mage:import agent
"git.narnian.us/lordwelch/sshrimp/tools/mage/agent"
)
var Default = All
// Builds all the targets
func Build() {
mg.Deps(ca.Build, agent.Build)
}
// Remove all build output (except generated configuration files)
func Clean() {
mg.Deps(ca.Clean, agent.Clean)
}
// Build and deploy the ca and agent
func All() {
mg.Deps(agent.Build, ca.Package, ca.Generate)
if _, err := os.Stat("./terraform"); os.IsNotExist(err) {
fmt.Println("All done. Run `terraform init` then `terraform apply` to deploy.")
} else {
fmt.Println("All done. Run `terraform apply` to deploy.")
}
}

View File

@@ -1,47 +0,0 @@
data "aws_iam_policy_document" "sshrimp_ca_assume_role" {
statement {
actions = ["sts:AssumeRole"]
principals {
type = "Service"
identifiers = ["lambda.amazonaws.com"]
}
}
}
data "aws_iam_policy_document" "sshrimp_ca" {
statement {
actions = [
"kms:Sign",
"kms:GetPublicKey"
]
resources = [
"${aws_kms_key.sshrimp_ca_private_key.arn}",
]
}
statement {
actions = [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents",
]
resources = [
"*",
]
}
}
resource "aws_iam_role_policy" "sshrimp_ca" {
name = "sshrimp-ca-${data.aws_region.current.name}"
role = aws_iam_role.sshrimp_ca.id
policy = data.aws_iam_policy_document.sshrimp_ca.json
}
resource "aws_iam_role" "sshrimp_ca" {
name = "sshrimp-ca-${data.aws_region.current.name}"
assume_role_policy = data.aws_iam_policy_document.sshrimp_ca_assume_role.json
}

View File

@@ -1,64 +0,0 @@
data "aws_iam_policy_document" "sshrimp_ca_private_key" {
// Allow the root account to administer the key, but not encrypt/decrypt/sign
statement {
effect = "Allow"
principals {
type = "AWS"
identifiers = ["arn:aws:iam::${data.aws_caller_identity.current.account_id}:root"]
}
actions = [
"kms:CancelKeyDeletion",
"kms:Create*",
"kms:Delete*",
"kms:Describe*",
"kms:Disable*",
"kms:Enable*",
"kms:Get*",
"kms:List*",
"kms:Put*",
"kms:Revoke*",
"kms:ScheduleKeyDeletion",
"kms:TagResource",
"kms:UntagResource",
"kms:Update*",
]
resources = ["*"]
}
// Allow the SSHrimp lambda to sign and get the public key
statement {
effect = "Allow"
principals {
type = "AWS"
identifiers = ["${aws_iam_role.sshrimp_ca.arn}"]
}
actions = [
"kms:Sign",
"kms:GetPublicKey",
]
resources = ["*"]
}
}
resource "aws_kms_key" "sshrimp_ca_private_key" {
description = "KMS key used to sign SSH certificates for the SSHrimp Certificate Authority"
deletion_window_in_days = 10
customer_master_key_spec = "RSA_4096"
key_usage = "SIGN_VERIFY"
policy = data.aws_iam_policy_document.sshrimp_ca_private_key.json
depends_on = [
aws_iam_role.sshrimp_ca,
]
}
resource "aws_kms_alias" "sshrimp_ca_private_key" {
name = "alias/${var.key_alias}"
target_key_id = aws_kms_key.sshrimp_ca_private_key.key_id
}

View File

@@ -1,11 +0,0 @@
resource "aws_lambda_function" "sshrimp_ca" {
function_name = var.lambda_name
filename = "sshrimp-ca.zip"
role = aws_iam_role.sshrimp_ca.arn
timeout = 120
memory_size = 512
description = "SSHrimp Certificate Authority"
handler = "sshrimp-ca"
runtime = "go1.x"
}

View File

@@ -1,3 +0,0 @@
data "aws_caller_identity" "current" {}
data "aws_region" "current" {}

View File

@@ -1,9 +0,0 @@
variable "lambda_name" {
type = string
default = "sshrimp"
}
variable "key_alias" {
type = string
default = "sshrimp"
}

View File

@@ -1,20 +0,0 @@
package agent
import (
"github.com/magefile/mage/sh"
)
// Build Builds the local ssh agent
func Build() error {
return sh.Run("go", "build", "./cmd/sshrimp-agent")
}
// Clean Cleans the output files for sshrimp-agent
func Clean() error {
return sh.Rm("sshrimp-agent")
}
// Install Installs the sshrimp-agent
func Install() error {
return sh.Run("go", "install", "./cmd/sshrimp-agent")
}

View File

@@ -1,186 +0,0 @@
package ca
import (
"crypto/x509"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
"git.narnian.us/lordwelch/sshrimp/internal/config"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/kms"
"github.com/magefile/mage/mg"
"github.com/magefile/mage/sh"
"github.com/magefile/mage/target"
"golang.org/x/crypto/ssh"
)
// Config Generate a sshrimp configuration file if it doesn't exsit
func Config() error {
c := config.NewSSHrimpWithDefaults()
// Read the existing config if it doesn't exist, generate a new one
if err := c.Read(config.GetPath()); err != nil {
configPath, err := config.Wizard(config.GetPath(), c)
if err != nil {
return err
}
// If a different config path is chosen, use it for the rest of the build
os.Setenv("SSHRIMP_CONFIG", configPath)
}
return nil
}
// Build Builds the certificate authority
func Build() error {
env := map[string]string{
"GOOS": "linux",
}
return sh.RunWith(env, "go", "build", "./cmd/sshrimp-ca")
}
// Build Builds the GCP certificate authority
// It produces gcp.a for checking timestamps
func BuildGCP() error {
env := map[string]string{
"GOOS": "linux",
}
return sh.RunWith(env, "go", "build", "-o", "gcp.a", "./gcp")
}
// Package Packages the certificate authority files into a zip archive
func Package() error {
if modified, err := target.Path("sshrimp-ca", config.GetPath()); err == nil && !modified {
return nil
}
mg.Deps(Build, Config)
zipFile, err := os.Create("sshrimp-ca.zip")
if err != nil {
return err
}
defer zipFile.Close()
if err := lambdaCreateArchive(zipFile, "sshrimp-ca", config.GetPath()); err != nil {
return err
}
return nil
}
// PackageGCP Packages the certificate authority files into a zip archive
func PackageGCP() error {
if modified, err := target.Path("gcp.a", config.GetPath()); err == nil && !modified {
return nil
}
mg.Deps(BuildGCP, Config)
zipFile, err := os.Create("sshrimp-ca-gcp.zip")
if err != nil {
return err
}
defer zipFile.Close()
err = gcpCreateArchive(zipFile, []ZipFiles{
ZipFiles{Filename: "go.mod"},
{"gcp/gcp.go", "gcp.go"},
ZipFiles{Filename: "internal"},
ZipFiles{config.GetPath(), filepath.Base(config.GetPath())},
}...)
if err != nil {
return err
}
return nil
}
// Generate Generates a CloudFormation template used to deploy the certificate authority
func Generate() error {
if modified, err := target.Path("sshrimp-ca.tf.json", config.GetPath()); err == nil && !modified {
return nil
}
mg.Deps(Config)
c := config.NewSSHrimp()
if err := c.Read(config.GetPath()); err != nil {
return err
}
template, err := generateTerraform(c)
if err != nil {
return err
}
ioutil.WriteFile("sshrimp-ca.tf.json", template, 0644)
return nil
}
// Keys Get the public keys of all configured KMS keys in OpenSSH format
func Keys() error {
c := config.NewSSHrimp()
if err := c.Read(config.GetPath()); err != nil {
return err
}
// For each configured region, get the public key from KMS and format it in an OpenSSH authorized_keys format
for _, region := range c.CertificateAuthority.Regions {
// Create a new session in the correct region
session := session.Must(session.NewSession(&aws.Config{
Region: aws.String(region),
}))
svc := kms.New(session)
// Get the public key from KMS
response, err := svc.GetPublicKey(&kms.GetPublicKeyInput{
KeyId: aws.String(c.CertificateAuthority.KeyAlias),
})
if err != nil {
return err
}
// Parse the public key from KMS
publicKey, err := x509.ParsePKIXPublicKey(response.PublicKey)
if err != nil {
return err
}
// Convert the public key into an SSH public key
sshPublicKey, err := ssh.NewPublicKey(publicKey)
if err != nil {
return err
}
// Generate the final string to output on stdout
authorizedKey := strings.TrimSuffix(string(ssh.MarshalAuthorizedKey(sshPublicKey)), "\n")
fmt.Printf("%s sshrimp-ca@%s\n", authorizedKey, region)
}
return nil
}
// Clean Cleans the output files for sshrimp-ca
func Clean() error {
if err := sh.Rm("sshrimp-ca"); err != nil {
return err
}
if err := sh.Rm("sshrimp-ca.tf.json"); err != nil {
return err
}
if err := sh.Rm("sshrimp-ca.zip"); err != nil {
return err
}
if err := sh.Rm("sshrimp-ca-gcp.zip"); err != nil {
return err
}
if err := sh.Rm("gcp.a"); err != nil {
return err
}
return nil
}

View File

@@ -1,136 +0,0 @@
package ca
import (
"archive/zip"
"io"
"os"
"path/filepath"
"strings"
)
type ZipFiles struct {
Filename, ZipPath string
}
// Packages the certificate authority lambda into a zip archive on writer
func lambdaCreateArchive(wr io.Writer, filename ...string) error {
archive := zip.NewWriter(wr)
defer archive.Close()
for _, path := range filename {
info, err := os.Stat(path)
if err != nil {
return err
}
header, err := zip.FileInfoHeader(info)
if err != nil {
return err
}
writer, err := archive.CreateHeader(header)
if err != nil {
return err
}
file, err := os.Open(path)
if err != nil {
return err
}
defer file.Close()
if _, err := io.Copy(writer, file); err != nil {
return err
}
}
return nil
}
// Packages the certificate authority function into a GCP compatible zip archive on writer
func gcpCreateArchive(wr io.Writer, files ...ZipFiles) error {
archive := zip.NewWriter(wr)
defer archive.Close()
for _, fileinfo := range files {
info, err := os.Stat(fileinfo.Filename)
if err != nil {
return err
}
if info.IsDir() {
err = zipDirectory(archive, fileinfo.Filename, fileinfo.ZipPath)
if err != nil {
return err
}
continue
}
if strings.TrimSpace(fileinfo.ZipPath) == "" {
fileinfo.ZipPath = fileinfo.Filename
}
header, err := zip.FileInfoHeader(info)
if err != nil {
return err
}
header.Name = fileinfo.ZipPath
writer, err := archive.CreateHeader(header)
if err != nil {
return err
}
file, err := os.Open(fileinfo.Filename)
if err != nil {
return err
}
defer file.Close()
if _, err := io.Copy(writer, file); err != nil {
return err
}
}
return nil
}
func zipDirectory(archive *zip.Writer, dir, zipDir string) error {
return filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
var zipPath string
if strings.TrimSpace(zipDir) == "" {
zipPath = path
} else {
zipPath = strings.Replace(path, dir, zipDir, 1)
}
if info.IsDir() {
return nil
}
header, err := zip.FileInfoHeader(info)
if err != nil {
return err
}
header.Name = zipPath
writer, err := archive.CreateHeader(header)
if err != nil {
return err
}
file, err := os.Open(path)
if err != nil {
return err
}
defer file.Close()
if _, err := io.Copy(writer, file); err != nil {
return err
}
return nil
})
}

View File

@@ -1,88 +0,0 @@
package ca
import (
"git.narnian.us/lordwelch/sshrimp/internal/config"
"github.com/awslabs/goformation/v4/cloudformation"
"github.com/awslabs/goformation/v4/cloudformation/iam"
"github.com/awslabs/goformation/v4/cloudformation/kms"
"github.com/awslabs/goformation/v4/cloudformation/lambda"
)
func makePolicyDocument(statement map[string]interface{}) map[string]interface{} {
return map[string]interface{}{
"Version": "2012-10-17",
"Statement": []interface{}{
statement,
},
}
}
func makeAssumeRolePolicyDocument(service string) map[string]interface{} {
return makePolicyDocument(map[string]interface{}{
"Effect": "Allow",
"Principal": map[string][]string{
"Service": []string{service},
},
"Action": []string{"sts:AssumeRole"},
})
}
func generateTemplate(c *config.SSHrimp) ([]byte, error) {
// Create a new CloudFormation template
template := cloudformation.NewTemplate()
template.Resources["SSHrimpPrivateKey"] = &kms.Key{
Description: "SSHrimp Certificate Authority Private Key",
PendingWindowInDays: 7,
KeyUsage: "SIGN_VERIFY",
KeyPolicy: makePolicyDocument(map[string]interface{}{
"Effect": "Allow",
"Principal": map[string][]string{
"AWS": []string{
cloudformation.GetAtt("SSHrimpLambdaExecutionRole", "Arn"),
},
},
"Action": []string{
"kms:GetPublicKey",
"kms:Sign",
},
"Resource": cloudformation.GetAtt("SSHrimpLambda", "Arn"),
}),
}
template.Resources["SSHrimpLambdaExecutionRole"] = &iam.Role{
AssumeRolePolicyDocument: makeAssumeRolePolicyDocument("lambda.amazonaws.com"),
RoleName: "sshrimp-ca",
Policies: []iam.Role_Policy{
{
PolicyDocument: makePolicyDocument(map[string]interface{}{
"Effect": "Allow",
"Action": "kms:Sign",
"Resource": "*",
}),
PolicyName: "sshrimp-ca-lambda",
},
},
}
template.Resources["SSHrimpLambda"] = &lambda.Function{
FunctionName: c.CertificateAuthority.FunctionName,
Description: "SSHrimp Certificate Authority",
Role: cloudformation.GetAtt("SSHrimpLambdaExecutionRole", "Arn"),
Handler: "sshrimp-ca",
MemorySize: 512,
Runtime: "python3.7",
Code: &lambda.Function_Code{
ZipFile: "sshrimp-ca.zip",
},
}
// Generate the YAML AWS CloudFormation template
y, err := template.YAML()
if err != nil {
return nil, err
}
return y, nil
}

View File

@@ -1,57 +0,0 @@
package ca
import (
"encoding/json"
"strconv"
"git.narnian.us/lordwelch/sshrimp/internal/config"
)
// Provider describes an AWS provider
type Provider struct {
Version string `json:"version"`
Alias string `json:"alias"`
Region string `json:"region"`
AllowedAccountIDs []string `json:"allowed_account_ids"`
}
// Module describes a terraform module
type Module struct {
Source string `json:"source"`
Providers map[string]string `json:"providers"`
}
// TerraformOutput represents the main.tf.json struct
type TerraformOutput struct {
Provider map[string][]Provider `json:"provider"`
Module map[string]Module `json:"module"`
}
func generateTerraform(c *config.SSHrimp) ([]byte, error) {
providers := make([]Provider, len(c.CertificateAuthority.Regions))
modules := make(map[string]Module, len(c.CertificateAuthority.Regions))
for index, region := range c.CertificateAuthority.Regions {
providers[index].Version = "~> 2.49"
providers[index].Alias = region
providers[index].Region = region
providers[index].AllowedAccountIDs = []string{
strconv.Itoa(c.CertificateAuthority.AccountID),
}
modules["sshrimp-"+region] = Module{
Source: "./terraform",
Providers: map[string]string{
"aws": "aws." + region,
},
}
}
output := TerraformOutput{
Provider: map[string][]Provider{
"aws": providers,
},
Module: modules,
}
return json.MarshalIndent(output, "", " ")
}