Skip to content

Commit 81fac9c

Browse files
authored
Merge pull request #13 from isometry/feature/issue-mode
feat!: support and default to ephemeral Vault-issued keypairs
2 parents 3565ff5 + 762ed35 commit 81fac9c

File tree

8 files changed

+383
-113
lines changed

8 files changed

+383
-113
lines changed

README.md

Lines changed: 39 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ An enhanced implementation of [`vault ssh`](https://www.vaultproject.io/docs/com
1818
* A [HashiCorp Vault](https://www.vaultproject.io/) instance configured for [SSH Client Key Signing](https://www.vaultproject.io/docs/secrets/ssh/signed-ssh-certificates.html#client-key-signing), access to an appropriate role, and an SSH server configured to trust the Vault CA.
1919
* An active Vault token (either in the `VAULT_TOKEN` environment variable, or – if the standard `vault` binary is available within `$PATH` – available from a Vault Token Helper). The `VAULT_ADDR` environment variable must also be set.
2020
* OpenSSH 7.2 or newer `ssh` client binary.
21-
* A standard SSH private key (stored anywhere supported by `ssh`), and the associated *unsigned* public key (default: `~/.ssh/id_rsa.pub`). `vssh` does *not* require access to the private key.
2221

2322
## Usage
2423

@@ -30,44 +29,63 @@ Usage:
3029
vssh [options] destination [command]
3130

3231
Application Options:
33-
--version Show version
34-
35-
Vault SSH key signing Options:
36-
--path= Vault SSH Path (default: ssh) [$VAULT_SSH_PATH]
37-
--role= Vault SSH Role (default: default) [$VAULT_SSH_ROLE]
38-
--ttl= Vault SSH Certificate TTL (default: 300) [$VAULT_SSH_TTL]
39-
-P, --public-key= OpenSSH Public RSA Key to sign (default: ~/.ssh/id_rsa.pub) [$VAULT_SSH_PUBLIC_KEY]
32+
--mode=[sign|issue] Mode (default: issue) [$VAULT_SSH_MODE]
33+
--type=[rsa|ec|ed25519] Preferred key type (default: ed25519) [$VAULT_SSH_KEY_TYPE]
34+
--bits=[0|2048|3072|4096|256|384|521] Key bits for 'issue' mode (default: 0) [$VAULT_SSH_KEY_BITS]
35+
--path= Vault SSH mountpoint (default: ssh) [$VAULT_SSH_PATH]
36+
--role= Vault SSH role (default: <ssh-username>) [$VAULT_SSH_ROLE]
37+
--ttl= Vault SSH certificate TTL (default: 300) [$VAULT_SSH_TTL]
38+
-P, --public-key= Path to preferred public key for 'sign' mode [$VAULT_SSH_PUBLIC_KEY]
39+
--version Show version
4040

4141
Certificate Extensions:
42-
--default-extensions Disable automatic extension calculation and request signer-default extensions [$VAULT_SSH_DEFAULT_EXTENSIONS]
43-
--agent-forwarding Force permit-agent-forwarding extension [$VAULT_SSH_AGENT_FORWARDING]
44-
--port-forwarding Force permit-port-forwarding extension [$VAULT_SSH_PORT_FORWARDING]
45-
--no-pty Force disable permit-pty extension [$VAULT_SSH_NO_PTY]
46-
--user-rc Enable permit-user-rc extension [$VAULT_SSH_USER_RC]
47-
--x11-forwarding Force permit-X11-forwarding extension [$VAULT_SSH_X11_FORWARDING]
42+
--default-extensions Disable automatic extension calculation and request signer-default extensions [$VAULT_SSH_DEFAULT_EXTENSIONS]
43+
--agent-forwarding Force permit-agent-forwarding extension [$VAULT_SSH_AGENT_FORWARDING]
44+
--port-forwarding Force permit-port-forwarding extension [$VAULT_SSH_PORT_FORWARDING]
45+
--no-pty Force disable permit-pty extension [$VAULT_SSH_NO_PTY]
46+
--user-rc Enable permit-user-rc extension [$VAULT_SSH_USER_RC]
47+
--x11-forwarding Force permit-X11-forwarding extension [$VAULT_SSH_X11_FORWARDING]
4848

4949
Help Options:
50-
-h, --help Show this help message
50+
-h, --help Show this help message
5151
```
5252

5353
If you need to override the [SSH Client Key Signing](https://www.vaultproject.io/docs/secrets/ssh/signed-ssh-certificates.html#client-key-signing) mountpoint or role, this is most easily achieved by setting the `VAULT_SSH_PATH` and `VAULT_SSH_ROLE` environment variables in your shell rc.
54+
If your Vault SSH mountpoint isn't configured with a role matching the target SSH username, you *will* need to specify the Vault SSH role to use (e.g. `export VAULT_SSH_ROLE=self` or `vssh --role=self host` if you're using a role named `self` configured with templated `allowed_users`).
55+
56+
In `issue` mode (the default), the client will retrieve an ephemeral keypair from Vault, exposed to `ssh(1)` via an internal SSH agent.
5457

55-
Similarly, if you prefer an `ed25519` or `ecdsa` key, override with `VAULT_SSH_PUBLIC_KEY`.
58+
In `sign` mode, the client will sign the public key specified, defaulting to the first key added into `ssh-agent(1)` (preferring the first of type matching `VAULT_SSH_KEY_TYPE`).
5659

57-
By default, the certificate will be requested with only those extensions required for the current command (default `permit-pty` unless `-N` is specified). Additional extensions may be requested (e.g. to support expected future multiplexed connections) with the "Certificate Extensions" arguments, or the Vault role default extensions may be forced with `--default-extensions`.
60+
The certificate will be requested with only those extensions required for the current command (default `permit-pty` unless `-N` is specified). Additional extensions may be requested (e.g. to support expected future multiplexed connections) with the "Certificate Extensions" arguments, or the Vault role default extensions may be forced with `--default-extensions`.
5861

59-
### Example
62+
### Examples
6063

61-
The following will request that the ed25519 public key be signed by the Vault signed at `https://vault.example.com:8200/v1/ssh/sign/ssh-client-signer`, with `permit-pty` and `permit-port-forwarding` extensions to support the connection to `host.example.com`
64+
The following will request that an existing ed25519 public key be signed by the Vault signer at `https://vault.example.com:8200/v1/ssh-client-signer/sign/default`, with (automatic) `permit-pty` and `permit-port-forwarding` extensions to support the connection to `host.example.com`:
6265

6366
```console
64-
$ export VAULT_ADDR=https://vault.example.com:8200 VAULT_SSH_PATH=ssh-client-signer VAULT_SSH_PUBLIC_KEY=~/.ssh/id_ed25519.pub
65-
$ vault login -method=oidc
67+
$ ssh-add ~/.ssh/id_ed25519
68+
$ export VAULT_ADDR=https://vault.example.com:8200
69+
$ export VAULT_SSH_PATH=ssh-client-signer
70+
$ export VAULT_SSH_ROLE=default
71+
$ export VAULT_SSH_MODE=sign
72+
$ vault login
6673
...
6774
$ vssh -L8080:localhost:80 host.example.com
6875
...
6976
```
7077

78+
The following will request that an ephemeral ecdsa keypair with a (default) 256-bit private key be generated by the Vault issuer at `https://vault.example.com/v1/ssh/issue/root`, and used to run the `id` command on `host2.example.com` as `root`:
79+
80+
```console
81+
$ export VAULT_ADDR=https://vault.example.com
82+
$ export VAULT_SSH_KEY_TYPE=ec
83+
$ vault login
84+
...
85+
86+
uid=0(root) gid=0(wheel) groups=0(wheel),5(operator)
87+
```
88+
7189
## Installation
7290

7391
### Manual

agent/external.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package agent
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"net"
7+
"os"
8+
"os/user"
9+
10+
log "github.com/sirupsen/logrus"
11+
12+
"golang.org/x/crypto/ssh"
13+
"golang.org/x/crypto/ssh/agent"
14+
"golang.org/x/exp/slices"
15+
)
16+
17+
var sshKeyTypeMap = map[string][]string{
18+
"rsa": {ssh.KeyAlgoRSA},
19+
"ec": {ssh.KeyAlgoECDSA256, ssh.KeyAlgoSKECDSA256, ssh.KeyAlgoECDSA384, ssh.KeyAlgoECDSA521},
20+
"ed25519": {ssh.KeyAlgoED25519, ssh.KeyAlgoSKED25519},
21+
"sk": {ssh.KeyAlgoSKECDSA256, ssh.KeyAlgoSKED25519},
22+
}
23+
24+
var supportedKeyTypes = []string{
25+
ssh.KeyAlgoRSA,
26+
ssh.KeyAlgoECDSA256,
27+
ssh.KeyAlgoSKECDSA256,
28+
ssh.KeyAlgoECDSA384,
29+
ssh.KeyAlgoECDSA521,
30+
ssh.KeyAlgoED25519,
31+
ssh.KeyAlgoSKED25519,
32+
}
33+
34+
func GetBestPublicKey(preferredType string) (publicKey []byte, err error) {
35+
prefKeyTypes, ok := sshKeyTypeMap[preferredType]
36+
if !ok {
37+
return nil, fmt.Errorf("invalid key type: %q", preferredType)
38+
}
39+
40+
sock := os.Getenv("SSH_AUTH_SOCK")
41+
if sock == "" {
42+
currentUser, err := user.Current()
43+
if err != nil {
44+
return nil, err
45+
}
46+
sock = fmt.Sprintf("%s/.ssh/agent.sock", currentUser.HomeDir)
47+
}
48+
conn, err := net.Dial("unix", sock)
49+
if err != nil {
50+
return
51+
}
52+
defer conn.Close()
53+
54+
keyring := agent.NewClient(conn)
55+
56+
keys, err := keyring.List()
57+
if err != nil {
58+
return
59+
}
60+
61+
var bestKey *agent.Key
62+
for _, key := range keys {
63+
keyType := key.Type()
64+
if bestKey == nil && slices.Contains(supportedKeyTypes, keyType) {
65+
bestKey = key
66+
}
67+
if slices.Contains(prefKeyTypes, keyType) {
68+
bestKey = key
69+
break
70+
}
71+
}
72+
if bestKey == nil {
73+
return nil, errors.New("no viable key found in external agent")
74+
}
75+
if !slices.Contains(prefKeyTypes, bestKey.Type()) {
76+
log.Infof("no key of type %q found, falling back to first key of type %q", preferredType, bestKey.Type())
77+
}
78+
79+
return []byte(bestKey.String()), nil
80+
}

agent/internal.go

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
package agent
2+
3+
import (
4+
"io"
5+
"net"
6+
"os"
7+
"path/filepath"
8+
9+
log "github.com/sirupsen/logrus"
10+
11+
"github.com/pkg/errors"
12+
"golang.org/x/crypto/ssh"
13+
"golang.org/x/crypto/ssh/agent"
14+
)
15+
16+
type InternalAgent struct {
17+
keyring agent.Agent
18+
socketDir string
19+
socketFile string
20+
listener net.Listener
21+
stop chan bool
22+
stopped chan bool
23+
}
24+
25+
func NewInternalAgent() (ia *InternalAgent, err error) {
26+
socketDir, err := os.MkdirTemp("", "vssh-agent.*")
27+
if err != nil {
28+
return
29+
}
30+
31+
ia = &InternalAgent{
32+
keyring: agent.NewKeyring(),
33+
socketDir: socketDir,
34+
socketFile: filepath.Join(socketDir, "agent.sock"),
35+
stop: make(chan bool),
36+
stopped: make(chan bool),
37+
}
38+
39+
ia.listener, err = net.Listen("unix", ia.socketFile)
40+
if err != nil {
41+
return nil, err
42+
}
43+
44+
go ia.run()
45+
return ia, nil
46+
}
47+
48+
func (ia *InternalAgent) run() {
49+
defer close(ia.stopped)
50+
for {
51+
select {
52+
case <-ia.stop:
53+
return
54+
default:
55+
conn, err := ia.listener.Accept()
56+
if err != nil {
57+
select {
58+
case <-ia.stop:
59+
return
60+
default:
61+
log.Fatalf("could not accept connection to agent %v", err)
62+
continue
63+
}
64+
}
65+
defer conn.Close()
66+
go func(c io.ReadWriter) {
67+
err := agent.ServeAgent(ia.keyring, c)
68+
if err != nil && !errors.Is(err, io.EOF) {
69+
log.Printf("could not serve ssh agent %v", err)
70+
}
71+
}(conn)
72+
}
73+
}
74+
}
75+
76+
func (ia *InternalAgent) AddSignedKeyPair(privateKeyStr string, signedKeyStr string) (err error) {
77+
privateKey, err := ssh.ParseRawPrivateKey([]byte(privateKeyStr))
78+
if err != nil {
79+
return errors.Wrap(err, "failed to parse private key")
80+
}
81+
82+
signedKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(signedKeyStr))
83+
if err != nil {
84+
return errors.Wrap(err, "failed to parse signed public key")
85+
}
86+
87+
return ia.keyring.Add(agent.AddedKey{
88+
PrivateKey: privateKey,
89+
Certificate: signedKey.(*ssh.Certificate),
90+
})
91+
}
92+
93+
func (ia *InternalAgent) Stop() {
94+
close(ia.stop)
95+
ia.listener.Close()
96+
<-ia.stopped
97+
os.RemoveAll(ia.socketDir)
98+
}
99+
100+
func (ia *InternalAgent) SocketFile() string {
101+
return ia.socketFile
102+
}

go.mod

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ require (
77
github.com/jessevdk/go-flags v1.5.0
88
github.com/pkg/errors v0.9.1
99
github.com/sirupsen/logrus v1.9.0
10-
golang.org/x/crypto v0.8.0
10+
golang.org/x/crypto v0.9.0
11+
golang.org/x/exp v0.0.0-20230510235704-dd950f8aeaea
1112
)
1213

1314
require (
@@ -16,7 +17,7 @@ require (
1617
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
1718
github.com/hashicorp/go-hclog v1.4.0 // indirect
1819
github.com/hashicorp/go-multierror v1.1.1 // indirect
19-
github.com/hashicorp/go-retryablehttp v0.7.1 // indirect
20+
github.com/hashicorp/go-retryablehttp v0.7.2 // indirect
2021
github.com/hashicorp/go-rootcerts v1.0.2 // indirect
2122
github.com/hashicorp/go-secure-stdlib/parseutil v0.1.7 // indirect
2223
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect
@@ -28,8 +29,9 @@ require (
2829
github.com/mitchellh/mapstructure v1.5.0 // indirect
2930
github.com/ryanuber/go-glob v1.0.0 // indirect
3031
github.com/stretchr/testify v1.7.2 // indirect
31-
golang.org/x/net v0.9.0 // indirect
32-
golang.org/x/sys v0.7.0 // indirect
32+
golang.org/x/net v0.10.0 // indirect
33+
golang.org/x/sys v0.8.0 // indirect
34+
golang.org/x/term v0.8.0 // indirect
3335
golang.org/x/text v0.9.0 // indirect
3436
golang.org/x/time v0.3.0 // indirect
3537
gopkg.in/square/go-jose.v2 v2.6.0 // indirect

go.sum

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,10 @@ github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5Kwzbycv
99
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
1010
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
1111
github.com/go-test/deep v1.0.2 h1:onZX1rnHT3Wv6cqNgYyFOOlgVKJrksuCMCRvJStbMYw=
12-
github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o=
12+
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
1313
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
1414
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
1515
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
16-
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
1716
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
1817
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
1918
github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
@@ -22,8 +21,8 @@ github.com/hashicorp/go-hclog v1.4.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVH
2221
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
2322
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
2423
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
25-
github.com/hashicorp/go-retryablehttp v0.7.1 h1:sUiuQAnLlbvmExtFQs72iFW/HXeUn8Z1aJLQ4LJJbTQ=
26-
github.com/hashicorp/go-retryablehttp v0.7.1/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY=
24+
github.com/hashicorp/go-retryablehttp v0.7.2 h1:AcYqCvkpalPnPF2pn0KamgwamS42TqUDDYFRKq/RAd0=
25+
github.com/hashicorp/go-retryablehttp v0.7.2/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8=
2726
github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc=
2827
github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
2928
github.com/hashicorp/go-secure-stdlib/parseutil v0.1.7 h1:UpiO20jno/eV1eVZcxqWnUohyKRe1g8FPV/xH1s/2qs=
@@ -71,10 +70,12 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
7170
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
7271
github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s=
7372
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
74-
golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ=
75-
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
76-
golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM=
77-
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
73+
golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
74+
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
75+
golang.org/x/exp v0.0.0-20230510235704-dd950f8aeaea h1:vLCWI/yYrdEHyN2JzIzPO3aaQJHQdp89IZBA/+azVC4=
76+
golang.org/x/exp v0.0.0-20230510235704-dd950f8aeaea/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
77+
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
78+
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
7879
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
7980
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
8081
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -84,9 +85,10 @@ golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBc
8485
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
8586
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
8687
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
87-
golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
88-
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
89-
golang.org/x/term v0.7.0 h1:BEvjmm5fURWqcfbSKTdpkDXYBrUS1c0m8agp14W48vQ=
88+
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
89+
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
90+
golang.org/x/term v0.8.0 h1:n5xxQn2i3PC0yLAbjTpNT85q/Kgzcr2gIoX9OrJUols=
91+
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
9092
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
9193
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
9294
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=

0 commit comments

Comments
 (0)