README child-mnemonic vector and host-key note; ssh install refuses a ~/.ssh it cannot enter (closes #51)
check / check (push) Successful in 2m4s
check / check (push) Successful in 2m4s
The README now gives the child mnemonic keyfunc prints for the abandon ... about test mnemonic at index 0, checked by the README vectors test; the BIP-85 specification vector stays, marked as starting from a master key keyfunc cannot take. It also says ssh install needs the host key in known_hosts already, and how to get round that. ssh install now also lists ~/.ssh/. on its first connection and refuses, before any upload, a ~/.ssh it can read but not enter, which sftp shows as empty; a file where ~/.ssh belongs is refused the same way. Model: opus-5-5 Co-authored-by: clawbot <sneak+clawbot@sneak.cloud>
This commit was merged in pull request #55.
This commit is contained in:
+37
-21
@@ -4,6 +4,7 @@ import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
@@ -32,6 +33,12 @@ const (
|
||||
localMode = 0o600
|
||||
)
|
||||
|
||||
// ErrCannotEnter is the refusal of a host whose .ssh is there but
|
||||
// cannot be entered, so that nothing in it can be read or written.
|
||||
var ErrCannotEnter = errors.New(
|
||||
"~/.ssh is there on the host but cannot be entered",
|
||||
)
|
||||
|
||||
// install returns the command that adds the public key to a host.
|
||||
func install() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
@@ -199,30 +206,39 @@ func merge(content, line string) (string, bool) {
|
||||
|
||||
// fetch brings the host's authorized_keys into the given path and
|
||||
// returns what is in it, and whether the .ssh directory was already
|
||||
// there. The one session lists .ssh and then gets the file, so the
|
||||
// listing settles the state of the directory before the get is read.
|
||||
// there. The one session lists .ssh, then .ssh/., and then gets the
|
||||
// file, so the listings settle the state of the directory before the
|
||||
// get is read.
|
||||
//
|
||||
// The file reads as empty in just two cases: sftp reported .ssh itself
|
||||
// as not there, or the listing succeeded and the get then reported the
|
||||
// file as not there. Anything else — the listing refused, the file
|
||||
// as not there, or both listings succeeded and the get then reported
|
||||
// the file as not there. Anything else — a listing refused, the file
|
||||
// there but unreadable, the connection down — fails the run and writes
|
||||
// nothing, because writing back over what was not read would leave the
|
||||
// host with the new key and nothing else. sftp cannot tell a missing
|
||||
// file from one in a directory it cannot enter, so the listing does:
|
||||
// a directory that is there but cannot be read is a failure, not an
|
||||
// empty file.
|
||||
// file from one in a directory it cannot enter, so the listings do: a
|
||||
// directory that is there but cannot be read fails the first, and one
|
||||
// that can be read but not entered fails the second, because nothing in
|
||||
// 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.
|
||||
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 -1 " + directory + "/.",
|
||||
"get " + authorized + " " + quoted(into),
|
||||
})
|
||||
if err != nil {
|
||||
if directoryAbsent(said) {
|
||||
if listingNotFound(said, directory) {
|
||||
return "", false, nil
|
||||
}
|
||||
|
||||
if listingNotFound(said, directory+"/.") {
|
||||
return "", false, ErrCannotEnter
|
||||
}
|
||||
|
||||
if absent(said) {
|
||||
return "", true, nil
|
||||
}
|
||||
@@ -239,18 +255,18 @@ func fetch(
|
||||
return string(content), true, nil
|
||||
}
|
||||
|
||||
// directoryAbsent says whether sftp reported .ssh itself as not being
|
||||
// there, which is the one listing failure read as a host that has no
|
||||
// authorized_keys yet. The reading is taken only from the line in which
|
||||
// sftp reports on that directory: any other failure of the listing, in
|
||||
// particular a directory that is there but cannot be entered, is left
|
||||
// as a failure, so that no key is written to a host whose keys were
|
||||
// never read.
|
||||
func directoryAbsent(said string) bool {
|
||||
// listingNotFound says whether sftp reported the path it was asked to
|
||||
// list as not being there. For .ssh that is the one listing failure
|
||||
// read as a host that has no authorized_keys yet; for .ssh/., once .ssh
|
||||
// itself has been listed, it is a .ssh that is there but cannot be
|
||||
// entered. The reading is taken only from the line in which sftp
|
||||
// reports on that path: any other failure of a listing, in particular a
|
||||
// directory that is there but cannot be read, is left as a failure, so
|
||||
// that no key is written to a host whose keys were never read.
|
||||
func listingNotFound(said, path string) bool {
|
||||
for line := range strings.Lines(said) {
|
||||
named, is := reportedCannotList(strings.TrimSpace(line))
|
||||
if is && (named == directory ||
|
||||
strings.HasSuffix(named, "/"+directory)) {
|
||||
if is && (named == path || strings.HasSuffix(named, "/"+path)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -259,9 +275,9 @@ func directoryAbsent(said string) bool {
|
||||
}
|
||||
|
||||
// reportedCannotList returns the path an sftp line reports it cannot
|
||||
// list for want of the directory, and whether the line is such a
|
||||
// report. The client writes this one wording when the directory a
|
||||
// listing names is not there, giving the path the server expanded.
|
||||
// list for want of it, and whether the line is such a report. The
|
||||
// client writes this one wording when it cannot look up the path a
|
||||
// listing names, giving the path the server expanded.
|
||||
func reportedCannotList(line string) (string, bool) {
|
||||
const (
|
||||
before = `Can't ls: "`
|
||||
|
||||
Reference in New Issue
Block a user