ssh install refuses stray arguments before -- and a symlinked authorized_keys (closes #61)
check / check (push) Failing after 3s
check / check (push) Failing after 3s
ssh install now takes the host alone before --: any other word there, or a second argument without --, is refused before the mnemonic is read or sftp runs, so keyfunc ssh install alice@host frank@host no longer installs the key for frank@host. The first listing of ~/.ssh is now ls -n, which shows the file type, so a symlinked authorized_keys is refused before any upload instead of being replaced by a regular file; the README says so. Judgement calls: install -- host is refused; a symlinked authorized_keys is refused even when its target already holds the key. Model: opus-5-5 Co-authored-by: clawbot <sneak+clawbot@sneak.cloud>
This commit was merged in pull request #62.
This commit is contained in:
@@ -49,6 +49,20 @@ var ErrCannotEnter = errors.New(
|
||||
"~/.ssh is there on the host but cannot be entered",
|
||||
)
|
||||
|
||||
// ErrSymlink is the refusal of a host whose authorized_keys is a
|
||||
// symlink: the rename that puts the new file in place would replace the
|
||||
// link itself, and the file it points at would never get the key.
|
||||
var ErrSymlink = errors.New(
|
||||
"~/.ssh/authorized_keys on the host is a symlink, which the tool " +
|
||||
"leaves alone",
|
||||
)
|
||||
|
||||
// ErrStrayArgument is the refusal of anything but the host before --,
|
||||
// which would otherwise be handed to sftp in front of the host.
|
||||
var ErrStrayArgument = errors.New(
|
||||
"only the host goes before --; options for sftp go after --",
|
||||
)
|
||||
|
||||
// install returns the command that adds the public key to a host.
|
||||
func install() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
@@ -60,7 +74,22 @@ func install() *cobra.Command {
|
||||
"beside it which is then renamed over it. Nothing is run " +
|
||||
"on the host. Anything after -- is given to sftp " +
|
||||
"unchanged, which is where the port goes (-P).",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
Args: cobra.MatchAll(
|
||||
cobra.MinimumNArgs(1),
|
||||
func(cmd *cobra.Command, args []string) error {
|
||||
// ArgsLenAtDash is -1 when there is no --.
|
||||
before := cmd.ArgsLenAtDash()
|
||||
if before == -1 {
|
||||
before = len(args)
|
||||
}
|
||||
|
||||
if before != 1 {
|
||||
return ErrStrayArgument
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
key, comment, err := derived(cmd)
|
||||
if err != nil {
|
||||
@@ -256,14 +285,23 @@ func merge(content, line string) (string, bool) {
|
||||
// it can be looked up, not even ".". The first listing of such a
|
||||
// directory comes up empty, as the server leaves out every name it
|
||||
// cannot look up.
|
||||
//
|
||||
// The first listing is a long one, which shows an authorized_keys that
|
||||
// is a symlink as one. That is refused before anything else sftp said
|
||||
// is read, so a link the get could not follow is refused in the same
|
||||
// words.
|
||||
func fetch(
|
||||
cmd *cobra.Command, host string, options []string, into string,
|
||||
) (string, bool, error) {
|
||||
said, err := session(cmd, host, options, []string{
|
||||
"ls -1 " + directory,
|
||||
"ls -n " + directory,
|
||||
"ls -1 " + directory + "/.",
|
||||
"get " + authorized + " " + quoted(into),
|
||||
})
|
||||
if symlinked(said) {
|
||||
return "", false, ErrSymlink
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if listingNotFound(said, directory) {
|
||||
return "", false, nil
|
||||
@@ -326,6 +364,22 @@ func reportedCannotList(line string) (string, bool) {
|
||||
return strings.TrimSuffix(strings.TrimPrefix(line, before), after), true
|
||||
}
|
||||
|
||||
// symlinked says whether the long listing of .ssh shows authorized_keys
|
||||
// as a symlink. With -n the client writes each line itself, as ls -l
|
||||
// does, whatever the server: the type comes first, "l" for a symlink,
|
||||
// and the path as the listing named it comes last.
|
||||
func symlinked(said string) bool {
|
||||
for line := range strings.Lines(said) {
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) > 0 && strings.HasPrefix(fields[0], "l") &&
|
||||
fields[len(fields)-1] == authorized {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// absent says whether sftp reported the file that was asked for as
|
||||
// not being there, which is the one failure of the fetch that is read
|
||||
// as an empty authorized_keys. The reading is taken only from the
|
||||
|
||||
Reference in New Issue
Block a user