README child-mnemonic vector and host-key note; ssh install refuses a ~/.ssh it cannot enter (closes #51)
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:
2026-10-04 07:25:49 +02:00
committed by clawbot
parent 897b43a206
commit d4fbcbc83d
5 changed files with 142 additions and 63 deletions
+52 -18
View File
@@ -52,7 +52,7 @@ const (
)
// notADirectory is what a test puts where the .ssh directory belongs
// to make a step of the write session fail.
// to make a .ssh that is listed but cannot be entered.
const notADirectory = "a file where the directory belongs\n"
// missingIdentity is a path with no file at it, handed to sftp after
@@ -106,9 +106,11 @@ const marker = "KEYFUNC_TEST_MARKER"
// another for a file that is there and cannot be read, which is a
// failure. A directory shut to the user is stood in for by mode 000,
// which the listing reads off the mode itself so that the test does not
// turn on the user it runs as. An -i naming a file that is not here
// draws the warning ssh writes for it, which carries the wording of a
// missing file into a session that goes on to authenticate.
// turn on the user it runs as, and one the user can enter but not write
// to by mode 500, which the put reads off the same way. An -i naming a
// file that is not here draws the warning ssh writes for it, which
// carries the wording of a missing file into a session that goes on to
// authenticate.
const installer = `
[ -n "$KEYFUNC_TEST_ENVIRONMENT" ] && env > "$KEYFUNC_TEST_ENVIRONMENT"
previous=
@@ -160,7 +162,13 @@ while IFS= read -r line; do
printf 'remote open "%s": Permission denied\n' "$home/$2" >&2
fi
;;
put) cp "$2" "$home/$3" 2>/dev/null || worked=no ;;
put)
if [ "$(stat -c '%a' "$(dirname "$home/$3")")" = 500 ]; then
worked=no
else
cp "$2" "$home/$3" 2>/dev/null || worked=no
fi
;;
mkdir) mkdir "$home/$2" 2>/dev/null || worked=no ;;
chmod) chmod "$2" "$home/$3" 2>/dev/null || worked=no ;;
rename) mv "$home/$2" "$home/$3" 2>/dev/null || worked=no ;;
@@ -329,6 +337,29 @@ func TestAnUnreadableDirectoryIsNotWrittenInto(t *testing.T) {
require.Equal(t, 1, connections(t, pretend))
}
func TestADirectoryThatCannotBeEnteredIsRefusedBeforeAnyUpload(t *testing.T) {
t.Setenv(mnemonic.Variable, example())
pretend := pretendHost(t)
// A file where .ssh belongs is listed and cannot be entered, which is
// how sftp sees a directory that can be read but not entered: the
// listing of .ssh comes up and the listing of .ssh/. finds nothing.
inTheWay := filepath.Join(pretend.home, keptUnder)
require.NoError(t,
os.WriteFile(inTheWay, []byte(notADirectory), fileMode),
)
printed, _, err := attempt(t, host)
require.ErrorIs(t, err, ssh.ErrCannotEnter)
require.Empty(t, printed)
// The read and nothing after it: no upload was tried, and what was
// on the host is still what is on the host.
require.Equal(t, 1, connections(t, pretend))
require.Equal(t, notADirectory, read(t, inTheWay))
}
func TestAnExistingDirectoryKeepsItsModeAndIsNotRemade(t *testing.T) {
t.Setenv(mnemonic.Variable, example())
@@ -392,27 +423,30 @@ func TestAFailedStepNamesTheUploadedFileAndChangesNothing(t *testing.T) {
pretend := pretendHost(t)
// A file where the .ssh directory belongs: the listing shows it and
// so the directory reads as already there, but then the put has
// nowhere to put anything, so the write session ends at the put.
inTheWay := filepath.Join(pretend.home, keptUnder)
require.NoError(t,
os.WriteFile(inTheWay, []byte(notADirectory), fileMode),
)
// A .ssh that can be listed and entered but not written to: the
// fetch finds no file in it, and then the put has nowhere to put
// anything, so the write session ends at the put.
const unwritable = 0o500
directory := filepath.Join(pretend.home, keptUnder)
require.NoError(t, os.Mkdir(directory, unwritable))
printed, said, err := attempt(t, host)
require.Error(t, err)
require.Empty(t, printed)
require.Contains(t, said, "put failed")
// The put is the first and last command the write session got to,
// and the file it was uploading is the one the message names.
// The put, after the three commands of the fetch, is the first and
// last command the write session got to, and the file it was
// uploading is the one the message names.
sent := recorded(t, pretend.batch)
require.Len(t, sent, 3)
require.Equal(t, "put", strings.Fields(sent[2])[0])
require.Contains(t, err.Error(), strings.Fields(sent[2])[2])
require.Len(t, sent, 4)
require.Equal(t, "put", strings.Fields(sent[3])[0])
require.Contains(t, err.Error(), strings.Fields(sent[3])[2])
require.Equal(t, notADirectory, read(t, inTheWay))
left, err := os.ReadDir(directory)
require.NoError(t, err)
require.Empty(t, left)
// The same run again, this way for the status it ends with.
given := os.Args