New Upstream Release - golang-github-charmbracelet-keygen
Ready changes
Summary
Merged new upstream version: 0.4.2 (was: 0.3.0).
Diff
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 067ee56..c9fcf3f 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -1,28 +1,12 @@
name: build
-on: [push, pull_request]
-jobs:
- test:
- strategy:
- matrix:
- go-version: [~1.17]
- os: [ubuntu-latest, macos-latest, windows-latest]
- runs-on: ${{ matrix.os }}
- env:
- GO111MODULE: "on"
- steps:
- - name: Install Go
- uses: actions/setup-go@v2
- with:
- go-version: ${{ matrix.go-version }}
-
- - name: Checkout code
- uses: actions/checkout@v2
- - name: Download Go modules
- run: go mod download
+on: [push, pull_request]
- - name: Build
- run: go build -v ./...
+jobs:
+ build:
+ uses: charmbracelet/meta/.github/workflows/build.yml@main
- - name: Test
- run: go test ./...
+ snapshot:
+ uses: charmbracelet/meta/.github/workflows/snapshot.yml@main
+ secrets:
+ goreleaser_key: ${{ secrets.GORELEASER_KEY }}
\ No newline at end of file
diff --git a/.github/workflows/goreleaser.yml b/.github/workflows/goreleaser.yml
new file mode 100644
index 0000000..55b3662
--- /dev/null
+++ b/.github/workflows/goreleaser.yml
@@ -0,0 +1,19 @@
+name: goreleaser
+
+on:
+ push:
+ tags:
+ - v*.*.*
+
+concurrency:
+ group: goreleaser
+ cancel-in-progress: true
+
+jobs:
+ goreleaser:
+ uses: charmbracelet/meta/.github/workflows/goreleaser.yml@main
+ secrets:
+ docker_username: ${{ secrets.DOCKERHUB_USERNAME }}
+ docker_token: ${{ secrets.DOCKERHUB_TOKEN }}
+ gh_pat: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
+ goreleaser_key: ${{ secrets.GORELEASER_KEY }}
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 8f5e197..6249b5b 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -1,5 +1,7 @@
name: lint
-on: [push, pull_request]
+on:
+ push:
+ pull_request:
jobs:
golangci:
@@ -12,7 +14,5 @@ jobs:
with:
# Optional: golangci-lint command line arguments.
args: --issues-exit-code=0
- # Optional: working directory, useful for monorepos
- # working-directory: somedir
# Optional: show only new issues if it's a pull request. The default value is `false`.
only-new-issues: true
diff --git a/.github/workflows/soft-serve.yml b/.github/workflows/soft-serve.yml
new file mode 100644
index 0000000..b10a5c9
--- /dev/null
+++ b/.github/workflows/soft-serve.yml
@@ -0,0 +1,12 @@
+name: soft-serve
+
+on:
+ push:
+ branches:
+ - main
+
+jobs:
+ soft-serve:
+ uses: charmbracelet/meta/.github/workflows/soft-serve.yml@main
+ secrets:
+ ssh-key: "${{ secrets.CHARM_SOFT_SERVE_KEY }}"
\ No newline at end of file
diff --git a/.goreleaser.yml b/.goreleaser.yml
new file mode 100644
index 0000000..eea5c74
--- /dev/null
+++ b/.goreleaser.yml
@@ -0,0 +1,3 @@
+includes:
+ - from_url:
+ url: charmbracelet/meta/main/goreleaser-lib.yaml
\ No newline at end of file
diff --git a/README.md b/README.md
index da00d4f..656b621 100644
--- a/README.md
+++ b/README.md
@@ -10,23 +10,30 @@ An SSH key pair generator with password protected keys support. Supports generat
## Example
```go
-filepath := filepath.Join(".ssh", "my_awesome_key")
-passphrase := []byte("awesome_secret")
-k, err := NewWithWrite(filepath, passphrase, key.Ed25519)
+kp, err := keygen.New("awesome", keygen.WithPassphrase("awesome_secret"),
+ keygen.WithKeyType(keygen.Ed25519))
if err != nil {
- fmt.Printf("error creating SSH key pair: %v", err)
- os.Exit(1)
+ log.Fatalf("error creating SSH key pair: %v", err)
}
+fmt.Printf("Your authorized key: %s\n", kp.AuthorizedKey())
```
+## Feedback
+
+We’d love to hear your thoughts on this project. Feel free to drop us a note!
+
+- [Twitter](https://twitter.com/charmcli)
+- [The Fediverse](https://mastodon.social/@charmcli)
+- [Discord](https://charm.sh/chat)
+
## License
[MIT](https://github.com/charmbracelet/keygen/raw/master/LICENSE)
-***
+---
Part of [Charm](https://charm.sh).
-<a href="https://charm.sh/"><img alt="the Charm logo" src="https://stuff.charm.sh/charm-badge-unrounded.jpg" width="400"></a>
+<a href="https://charm.sh/"><img alt="The Charm logo" src="https://stuff.charm.sh/charm-badge.jpg" width="400"></a>
Charm热爱开源 • Charm loves open source
diff --git a/debian/changelog b/debian/changelog
index 3ed1594..6e6a6df 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,8 +1,12 @@
-golang-github-charmbracelet-keygen (0.3.0-1) UNRELEASED; urgency=medium
+golang-github-charmbracelet-keygen (0.4.2-1) UNRELEASED; urgency=medium
+ [ Martin Dosch ]
* New upstream release.
- -- Martin Dosch <martin@mdosch.de> Sun, 30 Oct 2022 09:40:45 +0000
+ [ Debian Janitor ]
+ * New upstream release.
+
+ -- Martin Dosch <martin@mdosch.de> Mon, 05 Jun 2023 11:30:39 -0000
golang-github-charmbracelet-keygen (0.1.2-2) unstable; urgency=medium
diff --git a/go.mod b/go.mod
index 955be9b..5eb5dd5 100644
--- a/go.mod
+++ b/go.mod
@@ -3,9 +3,8 @@ module github.com/charmbracelet/keygen
go 1.17
require (
- github.com/caarlos0/sshmarshal v0.0.0-20220308164159-9ddb9f83c6b3
- github.com/mitchellh/go-homedir v1.1.0
- golang.org/x/crypto v0.0.0-20220307211146-efcb8507fb70
+ github.com/caarlos0/sshmarshal v0.1.0
+ golang.org/x/crypto v0.8.0
)
-require golang.org/x/sys v0.0.0-20210616094352-59db8d763f22 // indirect
+require golang.org/x/sys v0.7.0 // indirect
diff --git a/go.sum b/go.sum
index 6ba6dc9..f10a613 100644
--- a/go.sum
+++ b/go.sum
@@ -1,16 +1,44 @@
-github.com/caarlos0/sshmarshal v0.0.0-20220308164159-9ddb9f83c6b3 h1:w2ANoiT4ubmh4Nssa3/QW1M7lj3FZkma8f8V5aBDxXM=
-github.com/caarlos0/sshmarshal v0.0.0-20220308164159-9ddb9f83c6b3/go.mod h1:7Pd/0mmq9x/JCzKauogNjSQEhivBclCQHfr9dlpDIyA=
-github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
-github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
-golang.org/x/crypto v0.0.0-20220307211146-efcb8507fb70 h1:syTAU9FwmvzEoIYMqcPHOcVm4H3U5u90WsvuYgwpETU=
+github.com/caarlos0/sshmarshal v0.1.0 h1:zTCZrDORFfWh526Tsb7vCm3+Yg/SfW/Ub8aQDeosk0I=
+github.com/caarlos0/sshmarshal v0.1.0/go.mod h1:7Pd/0mmq9x/JCzKauogNjSQEhivBclCQHfr9dlpDIyA=
+github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220307211146-efcb8507fb70/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ=
+golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
+golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210616094352-59db8d763f22 h1:RqytpXGR1iVNX7psjB3ff8y7sNFinVFvkx1c8SjBkio=
-golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
+golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
+golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
+golang.org/x/term v0.7.0 h1:BEvjmm5fURWqcfbSKTdpkDXYBrUS1c0m8agp14W48vQ=
+golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
diff --git a/keygen.go b/keygen.go
index f35f590..37b5ce3 100644
--- a/keygen.go
+++ b/keygen.go
@@ -2,7 +2,6 @@
package keygen
import (
- "bytes"
"crypto"
"crypto/ecdsa"
"crypto/ed25519"
@@ -16,9 +15,9 @@ import (
"os"
"os/user"
"path/filepath"
+ "strings"
"github.com/caarlos0/sshmarshal"
- "github.com/mitchellh/go-homedir"
"golang.org/x/crypto/ssh"
)
@@ -32,6 +31,11 @@ const (
ECDSA KeyType = "ecdsa"
)
+// String implements the Stringer interface for KeyType.
+func (k KeyType) String() string {
+ return string(k)
+}
+
const rsaDefaultBits = 4096
// ErrMissingSSHKeys indicates we're missing some keys that we expected to
@@ -41,14 +45,18 @@ var ErrMissingSSHKeys = errors.New("missing one or more keys; did something happ
// ErrUnsupportedKeyType indicates an unsupported key type.
type ErrUnsupportedKeyType struct {
keyType string
+ ecName string
}
-// Error implements the error interface for ErrUnsupportedKeyType
+// Error implements the error interface for ErrUnsupportedKeyType.
func (e ErrUnsupportedKeyType) Error() string {
err := "unsupported key type"
if e.keyType != "" {
err += fmt.Sprintf(": %s", e.keyType)
}
+ if e.ecName != "" {
+ err += fmt.Sprintf(" (ECDSA curve: %s)", e.ecName)
+ }
return err
}
@@ -79,81 +87,147 @@ type SSHKeysAlreadyExistErr struct {
// SSHKeyPair holds a pair of SSH keys and associated methods.
type SSHKeyPair struct {
path string // private key filename path; public key will have .pub appended
+ writeKeys bool
passphrase []byte
+ rsaBitSize int
+ ec elliptic.Curve
keyType KeyType
privateKey crypto.PrivateKey
}
func (s SSHKeyPair) privateKeyPath() string {
- p := fmt.Sprintf("%s_%s", s.path, s.keyType)
- return p
+ return s.path
}
func (s SSHKeyPair) publicKeyPath() string {
return s.privateKeyPath() + ".pub"
}
+// Option is a functional option for SSHKeyPair.
+type Option func(*SSHKeyPair)
+
+// WithPassphrase sets the passphrase for the private key.
+func WithPassphrase(passphrase string) Option {
+ return func(s *SSHKeyPair) {
+ s.passphrase = []byte(passphrase)
+ }
+}
+
+// WithKeyType sets the key type for the key pair.
+// Available key types are RSA, Ed25519, and ECDSA.
+func WithKeyType(keyType KeyType) Option {
+ return func(s *SSHKeyPair) {
+ s.keyType = keyType
+ }
+}
+
+// WithBitSize sets the key size for the RSA key pair.
+// This option is ignored for other key types.
+func WithBitSize(bits int) Option {
+ return func(s *SSHKeyPair) {
+ s.rsaBitSize = bits
+ }
+}
+
+// WithWrite writes the key pair to disk if it doesn't exist.
+func WithWrite() Option {
+ return func(s *SSHKeyPair) {
+ s.writeKeys = true
+ }
+}
+
+// WithEllipticCurve sets the elliptic curve for the ECDSA key pair.
+// Supported curves are P-256, P-384, and P-521.
+// The default curve is P-384.
+// This option is ignored for other key types.
+func WithEllipticCurve(curve elliptic.Curve) Option {
+ return func(s *SSHKeyPair) {
+ s.ec = curve
+ }
+}
+
// New generates an SSHKeyPair, which contains a pair of SSH keys.
-func New(path string, passphrase []byte, keyType KeyType) (*SSHKeyPair, error) {
+//
+// If the key pair already exists, it will be loaded from disk, otherwise, a
+// new SSH key pair is generated.
+// If no key type is specified, Ed25519 will be used.
+func New(path string, opts ...Option) (*SSHKeyPair, error) {
var err error
s := &SSHKeyPair{
- path: path,
- keyType: keyType,
- passphrase: passphrase,
+ path: expandPath(path),
+ rsaBitSize: rsaDefaultBits,
+ ec: elliptic.P384(),
+ keyType: Ed25519,
+ }
+
+ for _, opt := range opts {
+ opt(s)
}
+
+ ecName := s.ec.Params().Name
+ switch ecName {
+ case "P-256", "P-384", "P-521":
+ default:
+ return nil, ErrUnsupportedKeyType{keyType: ecName, ecName: ecName}
+ }
+
if s.KeyPairExists() {
privData, err := ioutil.ReadFile(s.privateKeyPath())
if err != nil {
return nil, err
}
+
var k interface{}
- if len(passphrase) > 0 {
- k, err = ssh.ParseRawPrivateKeyWithPassphrase(privData, passphrase)
+ if len(s.passphrase) > 0 {
+ k, err = ssh.ParseRawPrivateKeyWithPassphrase(privData, s.passphrase)
} else {
k, err = ssh.ParseRawPrivateKey(privData)
}
+
if err != nil {
return nil, err
}
+
switch k := k.(type) {
- case *rsa.PrivateKey, *ecdsa.PrivateKey, *ed25519.PrivateKey:
+ case *rsa.PrivateKey:
+ s.keyType = RSA
+ s.privateKey = k
+ case *ecdsa.PrivateKey:
+ s.keyType = ECDSA
+ s.privateKey = k
+ case *ed25519.PrivateKey:
+ s.keyType = Ed25519
s.privateKey = k
default:
- return nil, ErrUnsupportedKeyType{fmt.Sprintf("%T", k)}
+ return nil, ErrUnsupportedKeyType{keyType: fmt.Sprintf("%T", k)}
}
+
return s, nil
}
- switch keyType {
+
+ switch s.keyType {
case Ed25519:
err = s.generateEd25519Keys()
case RSA:
- err = s.generateRSAKeys(rsaDefaultBits)
+ err = s.generateRSAKeys(s.rsaBitSize)
case ECDSA:
- err = s.generateECDSAKeys(elliptic.P384())
+ err = s.generateECDSAKeys(s.ec)
default:
- return nil, ErrUnsupportedKeyType{string(keyType)}
+ return nil, ErrUnsupportedKeyType{keyType: string(s.keyType)}
}
- if err != nil {
- return nil, err
- }
- return s, nil
-}
-// NewWithWrite generates an SSHKeyPair and writes it to disk if not exist.
-func NewWithWrite(path string, passphrase []byte, keyType KeyType) (*SSHKeyPair, error) {
- s, err := New(path, passphrase, keyType)
if err != nil {
return nil, err
}
- if !s.KeyPairExists() {
- if err = s.WriteKeys(); err != nil {
- return nil, err
- }
+
+ if s.writeKeys {
+ return s, s.WriteKeys()
}
+
return s, nil
}
-// PrivateKey returns the unencrypted private key.
+// PrivateKey returns the unencrypted crypto.PrivateKey.
func (s *SSHKeyPair) PrivateKey() crypto.PrivateKey {
switch s.keyType {
case RSA, Ed25519, ECDSA:
@@ -163,49 +237,115 @@ func (s *SSHKeyPair) PrivateKey() crypto.PrivateKey {
}
}
-// PrivateKeyPEM returns the unencrypted private key in OPENSSH PEM format.
-func (s *SSHKeyPair) PrivateKeyPEM() []byte {
- block, err := s.pemBlock(nil)
- if err != nil {
- return nil
- }
- return pem.EncodeToMemory(block)
+// Ensure that SSHKeyPair implements crypto.Signer.
+// This is used to ensure that the private key is a valid crypto.Signer to be
+// passed to ssh.NewSignerFromKey.
+var (
+ _ crypto.Signer = (*rsa.PrivateKey)(nil)
+ _ crypto.Signer = (*ecdsa.PrivateKey)(nil)
+ _ crypto.Signer = (*ed25519.PrivateKey)(nil)
+)
+
+// Signer returns an ssh.Signer for the key pair.
+func (s *SSHKeyPair) Signer() ssh.Signer {
+ sk, _ := ssh.NewSignerFromKey(s.PrivateKey())
+ return sk
+}
+
+// PublicKey returns the ssh.PublicKey for the key pair.
+func (s *SSHKeyPair) PublicKey() ssh.PublicKey {
+ p, _ := ssh.NewPublicKey(s.cryptoPublicKey())
+ return p
}
-// PublicKey returns the SSH public key (RFC 4253). Ready to be used in an
-// OpenSSH authorized_keys file.
-func (s *SSHKeyPair) PublicKey() []byte {
- var pk crypto.PublicKey
- // Prepare public key
+func (s *SSHKeyPair) cryptoPublicKey() crypto.PublicKey {
switch s.keyType {
case RSA:
key, ok := s.privateKey.(*rsa.PrivateKey)
if !ok {
return nil
}
- pk = key.Public()
+ return key.Public()
case Ed25519:
key, ok := s.privateKey.(*ed25519.PrivateKey)
if !ok {
return nil
}
- pk = key.Public()
+ return key.Public()
case ECDSA:
key, ok := s.privateKey.(*ecdsa.PrivateKey)
if !ok {
return nil
}
- pk = key.Public()
+ return key.Public()
default:
return nil
}
- p, err := ssh.NewPublicKey(pk)
+}
+
+// CryptoPublicKey returns the crypto.PublicKey of the SSH key pair.
+func (s *SSHKeyPair) CryptoPublicKey() crypto.PublicKey {
+ return s.cryptoPublicKey()
+}
+
+// RawAuthorizedKey returns the underlying SSH public key (RFC 4253) in OpenSSH
+// authorized_keys format.
+func (s *SSHKeyPair) RawAuthorizedKey() []byte {
+ bts, err := os.ReadFile(s.publicKeyPath())
+ if err != nil {
+ return []byte(s.AuthorizedKey())
+ }
+
+ _, c, opts, _, err := ssh.ParseAuthorizedKey(bts)
+ if err != nil {
+ return []byte(s.AuthorizedKey())
+ }
+
+ ak := s.authorizedKey(s.PublicKey())
+ if len(opts) > 0 {
+ ak = fmt.Sprintf("%s %s", strings.Join(opts, ","), ak)
+ }
+
+ if c != "" {
+ ak = fmt.Sprintf("%s %s", ak, c)
+ }
+
+ return []byte(ak)
+}
+
+func (s *SSHKeyPair) authorizedKey(pk ssh.PublicKey) string {
+ if pk == nil {
+ return ""
+ }
+
+ // serialize authorized key
+ return strings.TrimSpace(string(ssh.MarshalAuthorizedKey(pk)))
+}
+
+// AuthorizedKey returns the SSH public key (RFC 4253) in OpenSSH authorized_keys
+// format. The returned string is trimmed of sshd options and comments.
+func (s *SSHKeyPair) AuthorizedKey() string {
+ return s.authorizedKey(s.PublicKey())
+}
+
+// RawPrivateKey returns the raw unencrypted private key bytes in PEM format.
+func (s *SSHKeyPair) RawPrivateKey() []byte {
+ return s.rawPrivateKey(nil)
+}
+
+// RawProtectedPrivateKey returns the raw password protected private key bytes
+// in PEM format.
+func (s *SSHKeyPair) RawProtectedPrivateKey() []byte {
+ return s.rawPrivateKey(s.passphrase)
+}
+
+func (s *SSHKeyPair) rawPrivateKey(pass []byte) []byte {
+ block, err := s.pemBlock(pass)
if err != nil {
return nil
}
- // serialize public key
- ak := ssh.MarshalAuthorizedKey(p)
- return pubKeyWithMemo(ak)
+
+ return pem.EncodeToMemory(block)
}
func (s *SSHKeyPair) pemBlock(passphrase []byte) (*pem.Block, error) {
@@ -220,7 +360,7 @@ func (s *SSHKeyPair) pemBlock(passphrase []byte) (*pem.Block, error) {
}
return sshmarshal.MarshalPrivateKey(key, "")
default:
- return nil, ErrUnsupportedKeyType{string(s.keyType)}
+ return nil, ErrUnsupportedKeyType{keyType: s.keyType.String()}
}
}
@@ -273,7 +413,7 @@ func (s *SSHKeyPair) prepFilesystem() error {
keyDir := filepath.Dir(s.path)
if keyDir != "" {
- keyDir, err = homedir.Expand(keyDir)
+ keyDir, err = filepath.Abs(keyDir)
if err != nil {
return err
}
@@ -314,20 +454,11 @@ func (s *SSHKeyPair) prepFilesystem() error {
// WriteKeys writes the SSH key pair to disk.
func (s *SSHKeyPair) WriteKeys() error {
var err error
- priv := s.PrivateKeyPEM()
- pub := s.PublicKey()
- if priv == nil || pub == nil {
+ priv := s.RawProtectedPrivateKey()
+ if priv == nil {
return ErrMissingSSHKeys
}
- // Encrypt private key with passphrase
- if len(s.passphrase) > 0 {
- block, err := s.pemBlock(s.passphrase)
- if err != nil {
- return err
- }
- priv = pem.EncodeToMemory(block)
- }
if err = s.prepFilesystem(); err != nil {
return err
}
@@ -335,7 +466,12 @@ func (s *SSHKeyPair) WriteKeys() error {
if err := writeKeyToFile(priv, s.privateKeyPath()); err != nil {
return err
}
- if err := writeKeyToFile(pub, s.publicKeyPath()); err != nil {
+
+ ak := s.AuthorizedKey()
+ if memo := pubKeyMemo(); memo != "" {
+ ak = fmt.Sprintf("%s %s", ak, memo)
+ }
+ if err := writeKeyToFile([]byte(ak), s.publicKeyPath()); err != nil {
return err
}
@@ -365,19 +501,33 @@ func fileExists(path string) bool {
return true
}
+// expandPath resolves the tilde `~` and any environment variables in path.
+func expandPath(path string) string {
+ if len(path) > 0 && path[0] == '~' {
+ userdir, err := os.UserHomeDir()
+ if err != nil {
+ return path
+ }
+
+ path = filepath.Join(userdir, path[1:])
+ }
+
+ return os.ExpandEnv(path)
+}
+
// attaches a user@host suffix to a serialized public key. returns the original
// pubkey if we can't get the username or host.
-func pubKeyWithMemo(pubKey []byte) []byte {
+func pubKeyMemo() string {
u, err := user.Current()
if err != nil {
- return pubKey
+ return ""
}
hostname, err := os.Hostname()
if err != nil {
- return pubKey
+ return ""
}
- return append(bytes.TrimRight(pubKey, "\n"), []byte(fmt.Sprintf(" %s@%s\n", u.Username, hostname))...)
+ return fmt.Sprintf("%s@%s\n", u.Username, hostname)
}
// Error returns the a human-readable error message for SSHKeysAlreadyExistErr.
diff --git a/keygen_test.go b/keygen_test.go
index c5c5b35..8188a95 100644
--- a/keygen_test.go
+++ b/keygen_test.go
@@ -1,6 +1,7 @@
package keygen
import (
+ "bytes"
"crypto/elliptic"
"fmt"
"io/ioutil"
@@ -10,11 +11,85 @@ import (
)
func TestNewSSHKeyPair(t *testing.T) {
- p := filepath.Join(t.TempDir(), "test")
- _, err := NewWithWrite(p, []byte(""), RSA)
+ kp, err := New("")
if err != nil {
t.Errorf("error creating SSH key pair: %v", err)
}
+ if kp.keyType != Ed25519 {
+ t.Errorf("expected default key type to be Ed25519, got %s", kp.keyType)
+ }
+}
+
+func nilTest(t testing.TB, kp *SSHKeyPair) {
+ t.Helper()
+ if kp == nil {
+ t.Error("expected key pair to be non-nil")
+ }
+ if kp.PrivateKey() == nil {
+ t.Error("expected private key to be non-nil")
+ }
+ if kp.PublicKey() == nil {
+ t.Error("expected public key to be non-nil")
+ }
+ if kp.RawPrivateKey() == nil {
+ t.Error("expected raw private key to be non-nil")
+ }
+ if kp.RawProtectedPrivateKey() == nil {
+ t.Error("expected raw protected private key to be non-nil")
+ }
+ if kp.AuthorizedKey() == "" {
+ t.Error("expected authorized key to be non-nil")
+ }
+ if kp.Signer() == nil {
+ t.Error("expected signer to be non-nil")
+ }
+}
+
+func TestNilSSHKeyPair(t *testing.T) {
+ for _, kt := range []KeyType{RSA, Ed25519, ECDSA} {
+ t.Run(fmt.Sprintf("test nil key pair for %s", kt), func(t *testing.T) {
+ kp, err := New("", WithKeyType(kt))
+ if err != nil {
+ t.Errorf("error creating SSH key pair: %v", err)
+ }
+ nilTest(t, kp)
+ })
+ }
+}
+
+func TestNilSSHKeyPairWithPassphrase(t *testing.T) {
+ for _, kt := range []KeyType{RSA, Ed25519, ECDSA} {
+ t.Run(fmt.Sprintf("test nil key pair for %s", kt), func(t *testing.T) {
+ kp, err := New("", WithKeyType(kt), WithPassphrase("test"))
+ if err != nil {
+ t.Errorf("error creating SSH key pair: %v", err)
+ }
+ nilTest(t, kp)
+ })
+ }
+}
+
+func TestNilSSHKeyPairTestdata(t *testing.T) {
+ for _, kt := range []KeyType{RSA, Ed25519, ECDSA} {
+ t.Run(fmt.Sprintf("test nil key pair for %s", kt), func(t *testing.T) {
+ kp, err := New(filepath.Join("testdata", "test_"+kt.String()), WithPassphrase("test"), WithKeyType(kt))
+ if err != nil {
+ t.Errorf("error creating SSH key pair: %v", err)
+ }
+ nilTest(t, kp)
+ })
+ }
+}
+
+func TestUnsupportedCurve(t *testing.T) {
+ _, err := New("", WithKeyType(ECDSA), WithEllipticCurve(elliptic.P224()))
+ if err == nil {
+ t.Error("expected error for unsupported curve")
+ }
+ _, err = New("", WithKeyType(ECDSA), WithEllipticCurve(elliptic.P256()))
+ if err != nil {
+ t.Errorf("expected no error for supported curve, got %v", err)
+ }
}
func TestGenerateEd25519Keys(t *testing.T) {
@@ -35,11 +110,11 @@ func TestGenerateEd25519Keys(t *testing.T) {
// TODO: is there a good way to validate these? Lengths seem to vary a bit,
// so far now we're just asserting that the keys indeed exist.
- if len(k.PrivateKeyPEM()) == 0 {
+ if len(k.RawPrivateKey()) == 0 {
t.Error("error creating SSH private key PEM; key is 0 bytes")
}
- if len(k.PublicKey()) == 0 {
- t.Error("error creating SSH public key; key is 0 bytes")
+ if len(k.AuthorizedKey()) == 0 {
+ t.Error("error creating SSH authorized key; key is 0 bytes")
}
})
@@ -91,20 +166,21 @@ func TestGenerateECDSAKeys(t *testing.T) {
k := &SSHKeyPair{
path: filepath.Join(dir, filename),
keyType: ECDSA,
+ ec: elliptic.P384(),
}
t.Run("test generate SSH keys", func(t *testing.T) {
- err := k.generateECDSAKeys(elliptic.P384())
+ err := k.generateECDSAKeys(k.ec)
if err != nil {
t.Errorf("error creating SSH key pair: %v", err)
}
// TODO: is there a good way to validate these? Lengths seem to vary a bit,
// so far now we're just asserting that the keys indeed exist.
- if len(k.PrivateKeyPEM()) == 0 {
+ if len(k.RawPrivateKey()) == 0 {
t.Error("error creating SSH private key PEM; key is 0 bytes")
}
- if len(k.PublicKey()) == 0 {
+ if len(k.AuthorizedKey()) == 0 {
t.Error("error creating SSH public key; key is 0 bytes")
}
})
@@ -173,13 +249,13 @@ func createEmptyFile(t *testing.T, path string) (ok bool) {
func TestGeneratePublicKeyWithEmptyDir(t *testing.T) {
for _, keyType := range []KeyType{RSA, ECDSA, Ed25519} {
- func(t *testing.T) {
- k, err := NewWithWrite("testkey", nil, keyType)
+ t.Run("test generate public key with empty dir", func(t *testing.T) {
+ fp := filepath.Join(t.TempDir(), "testkey")
+ k, err := New(fp, WithKeyType(keyType), WithWrite())
if err != nil {
t.Fatalf("error creating SSH key pair: %v", err)
}
- fn := fmt.Sprintf("testkey_%s", keyType)
- f, err := os.Open(fn + ".pub")
+ f, err := os.Open(fp + ".pub")
if err != nil {
t.Fatalf("error opening SSH key file: %v", err)
}
@@ -188,25 +264,27 @@ func TestGeneratePublicKeyWithEmptyDir(t *testing.T) {
if err != nil {
t.Fatalf("error reading SSH key file: %v", err)
}
- defer os.Remove(fn)
- defer os.Remove(fn + ".pub")
- if string(k.PublicKey()) != string(fc) {
+ if bytes.Equal(k.RawAuthorizedKey(), fc) {
t.Errorf("error key mismatch\nprivate key:\n%s\n\nactual file:\n%s", k.PrivateKey(), string(fc))
}
- }(t)
+ t.Cleanup(func() {
+ os.Remove(fp)
+ os.Remove(fp + ".pub")
+ })
+ })
}
}
func TestGenerateKeyWithPassphrase(t *testing.T) {
+ ph := "testpass"
for _, keyType := range []KeyType{RSA, ECDSA, Ed25519} {
- ph := "testpass"
- func(t *testing.T) {
- _, err := NewWithWrite("testph", []byte(ph), keyType)
+ t.Run("test generate key with passphrase", func(t *testing.T) {
+ fp := filepath.Join(t.TempDir(), "testph")
+ _, err := New(fp, WithKeyType(keyType), WithPassphrase(ph), WithWrite())
if err != nil {
t.Fatalf("error creating SSH key pair: %v", err)
}
- fn := fmt.Sprintf("testph_%s", keyType)
- f, err := os.Open(fn)
+ f, err := os.Open(fp)
if err != nil {
t.Fatalf("error opening SSH key file: %v", err)
}
@@ -215,25 +293,67 @@ func TestGenerateKeyWithPassphrase(t *testing.T) {
if err != nil {
t.Fatalf("error reading SSH key file: %v", err)
}
- defer os.Remove(fn)
- defer os.Remove(fn + ".pub")
- k, err := New("testph", []byte(ph), keyType)
+ k, err := New(fp, WithKeyType(keyType), WithPassphrase(ph))
if err != nil {
t.Fatalf("error reading SSH key pair: %v", err)
}
- if string(k.PrivateKeyPEM()) == string(fc) {
+ if bytes.Equal(k.RawPrivateKey(), fc) {
t.Errorf("encrypted private key matches file contents")
}
- }(t)
+ t.Cleanup(func() {
+ os.Remove(fp)
+ os.Remove(fp + ".pub")
+ })
+ })
}
}
func TestReadingKeyWithPassphrase(t *testing.T) {
for _, keyType := range []KeyType{RSA, ECDSA, Ed25519} {
kp := filepath.Join("testdata", "test")
- _, err := New(kp, []byte("test"), keyType)
+ _, err := New(kp, WithKeyType(keyType), WithPassphrase("test"))
if err != nil {
t.Fatalf("error reading SSH key pair: %v", err)
}
}
}
+
+func TestKeynameSuffix(t *testing.T) {
+ for _, keyType := range []KeyType{RSA, ECDSA, Ed25519} {
+ t.Run("test keyname suffix", func(t *testing.T) {
+ fp := filepath.Join(t.TempDir(), "testkey_"+keyType.String())
+ _, err := New(fp, WithKeyType(keyType), WithWrite())
+ if err != nil {
+ t.Fatalf("error creating SSH key pair: %v", err)
+ }
+ if _, err := os.Stat(fp); os.IsNotExist(err) {
+ t.Errorf("private key file %s does not exist", fp)
+ }
+ t.Cleanup(func() {
+ os.Remove(fp)
+ os.Remove(fp + ".pub")
+ })
+ })
+ }
+}
+
+func TestExpandPath(t *testing.T) {
+ tmpdir := t.TempDir()
+ os.Setenv("TEMP", tmpdir)
+ defer os.Unsetenv("TEMP")
+
+ // Test environment variable expansion
+ if fp := expandPath(filepath.Join("$TEMP", "testkey")); fp != filepath.Join(tmpdir, "testkey") {
+ t.Errorf("error expanding path: %s", fp)
+ }
+
+ // Test tilde expansion
+ homedir, err := os.UserHomeDir()
+ if err != nil {
+ t.Fatalf("error getting user home directory: %v", err)
+ }
+
+ if fp := expandPath(filepath.Join("~", "testkey")); fp != filepath.Join(homedir, "testkey") {
+ t.Errorf("error expanding path: %s", fp)
+ }
+}
More details
Historical runs
- unsatisfied-apt-dependencies: Unsatisfied APT dependencies: golang-github-caarlos0-sshmarshal-dev:amd64
- missing-go-package: Missing Go package: github.com/caarlos0/sshmarshal
- worker-timeout: No keepalives received in 1:00:14.876187.
- run-disappeared: Worker started processing new run rather than d2d69450-ff6e-4cf3-969e-f73d37192f5d
- missing-go-package: Missing Go package: github.com/caarlos0/sshmarshal
- aborted: Killed by signal
- worker-timeout: No keepalives received in 10:00:10.332165.
- worker-timeout: No keepalives received in 10:00:10.991026.
- success: Merged new upstream version 0.2.1
- worker-timeout: No keepalives received in 10:06:07.338235.
- worker-failure: TypeError: cannot use a bytes pattern on a string-like object