diff --git a/ssh/api_test.go b/ssh/api_test.go index 24f4518..cf471f0 100644 --- a/ssh/api_test.go +++ b/ssh/api_test.go @@ -50,14 +50,17 @@ func (s *sshSuite) Test_access_AllKeys_ReturnsAKeyEntryListOfPrivateKeysIfSSHDir r := rand.Int() privateKeyFile1 := "private-key" privateKeyFile2 := fmt.Sprintf("is-a-private-key-%d", r) + privateKeyFile3 := "ed25519-key" s.createFileWithContent(sshDirectory, privateKeyFile1, correctRSASSHPrivateKey) s.createFileWithContent(sshDirectory, privateKeyFile2, correctRSASSHPrivateKeyOther) + s.createFileWithContent(sshDirectory, privateKeyFile3, correctEd25519PrivateKey) s.createEmptyFile(sshDirectory, "empty-file") a, _ := accessWithTestLogging() s.ElementsMatch([]api.KeyEntry{ createPrivateKeyRepresentation(path.Join(sshDirectory, privateKeyFile1)), createPrivateKeyRepresentation(path.Join(sshDirectory, privateKeyFile2)), + createPrivateKeyRepresentation(path.Join(sshDirectory, privateKeyFile3)), }, a.AllKeys()) } @@ -69,14 +72,17 @@ func (s *sshSuite) Test_access_AllKeys_ReturnsAKeyEntryListOfPublicKeysIfSSHDire r := rand.Int() publicKeyFile1 := "ssh-rsa.pub" publicKeyFile2 := fmt.Sprintf("ssh-rsa-%d.pub", r) + publicKeyFile3 := "ed25519.pub" s.createFileWithContent(sshDirectory, publicKeyFile1, "ssh-rsa BBBB batman@debian") s.createFileWithContent(sshDirectory, publicKeyFile2, "ssh-rsa AAAA robin@debian") + s.createFileWithContent(sshDirectory, publicKeyFile3, "ssh-ed25519 CCC alfred@debian") s.createEmptyFile(sshDirectory, "empty-file") a, _ := accessWithTestLogging() s.ElementsMatch([]api.KeyEntry{ createPublicKeyRepresentation(path.Join(sshDirectory, publicKeyFile1)), createPublicKeyRepresentation(path.Join(sshDirectory, publicKeyFile2)), + createPublicKeyRepresentation(path.Join(sshDirectory, publicKeyFile3)), }, a.AllKeys()) } @@ -88,18 +94,23 @@ func (s *sshSuite) Test_access_AllKeys_ReturnsAKeyEntryListOfKeypairsIfSSHDirect r := rand.Int() matchingPrivateKeyFile1 := "match-key" matchingPrivateKeyFile2 := fmt.Sprintf("is-a-match-key-%d", r) + matchingPrivateKeyFile3 := "match-ed25519-key" s.createFileWithContent(sshDirectory, matchingPrivateKeyFile1, correctRSASSHPrivateKey) s.createFileWithContent(sshDirectory, matchingPrivateKeyFile2, correctRSASSHPrivateKeyOther) + s.createFileWithContent(sshDirectory, matchingPrivateKeyFile3, correctEd25519PrivateKey) matchingPublicKeyFile1 := "match-key.pub" matchingPublicKeyFile2 := fmt.Sprintf("is-a-match-key-%d.pub", r) + matchingPublicKeyFile3 := "match-ed25519-key.pub" s.createFileWithContent(sshDirectory, matchingPublicKeyFile1, "ssh-rsa BBBB batman@debian") s.createFileWithContent(sshDirectory, matchingPublicKeyFile2, "ssh-rsa AAAA robin@debian") + s.createFileWithContent(sshDirectory, matchingPublicKeyFile3, "ssh-ed25519 CCCC alfred@debian") s.createEmptyFile(sshDirectory, "empty-file") a, _ := accessWithTestLogging() s.ElementsMatch([]api.KeyEntry{ createKeypairRepresentation(createPrivateKeyRepresentation(path.Join(sshDirectory, matchingPrivateKeyFile1)), createPublicKeyRepresentation(path.Join(sshDirectory, matchingPublicKeyFile1))), createKeypairRepresentation(createPrivateKeyRepresentation(path.Join(sshDirectory, matchingPrivateKeyFile2)), createPublicKeyRepresentation(path.Join(sshDirectory, matchingPublicKeyFile2))), + createKeypairRepresentation(createPrivateKeyRepresentation(path.Join(sshDirectory, matchingPrivateKeyFile3)), createPublicKeyRepresentation(path.Join(sshDirectory, matchingPublicKeyFile3))), }, a.AllKeys()) } diff --git a/ssh/ed25519_file_reader.go b/ssh/ed25519_file_reader.go new file mode 100644 index 0000000..b61c3a0 --- /dev/null +++ b/ssh/ed25519_file_reader.go @@ -0,0 +1,18 @@ +package ssh + +func (a *access) checkIfFileContainsAPrivateEd25519Key(fileName string) (bool, error) { + return fileContentMatches(fileName, a.isEd25519PrivateKey) +} + +func (a *access) filesContainingEd25519PrivateKeys(fileNameList []string) []string { + result := filter(fileNameList, ignoringErrors(a.checkIfFileContainsAPrivateEd25519Key)) + return result +} + +func checkIfFileContainsAPublicEd25519Key(fileName string) (bool, error) { + return fileContentMatches(fileName, isEd25519PublicKey) +} + +func filesContainingEd25519PublicKeys(fileNameList []string) []string { + return filter(fileNameList, ignoringErrors(checkIfFileContainsAPublicEd25519Key)) +} diff --git a/ssh/ed25519_file_reader_test.go b/ssh/ed25519_file_reader_test.go new file mode 100644 index 0000000..82b01e3 --- /dev/null +++ b/ssh/ed25519_file_reader_test.go @@ -0,0 +1,91 @@ +package ssh + +import "path/filepath" + +func (s *sshSuite) Test_filesContainingEd25519PrivateKeys_ReturnsAnEmptyListIfAnEmptyListIsProvided() { + fileNameList := []string{} + + a, _ := accessWithTestLogging() + selected := a.filesContainingEd25519PrivateKeys(fileNameList) + + s.Empty(selected) +} + +func (s *sshSuite) Test_filesContainingEd25519PrivateKeys_ReturnsAnEmptyListIfAListWithANonExistingFileIsProvided() { + fileNameList := []string{"File that doesn't exist"} + + a, _ := accessWithTestLogging() + selected := a.filesContainingEd25519PrivateKeys(fileNameList) + + s.Empty(selected) +} + +func (s *sshSuite) Test_filesContainingEd25519PrivateKeys_ReturnsAnEmptyListIfAListWithAnEmptyFileIsProvided() { + // Given + fileName := "Empty file" + s.createEmptyFile(s.tdir, fileName) + fileNameList := []string{filepath.Join(s.tdir, fileName)} + + // When + a, _ := accessWithTestLogging() + selected := a.filesContainingEd25519PrivateKeys(fileNameList) + + // Then + s.Empty(selected) +} + +func (s *sshSuite) Test_filesContainingEd25519PrivateKeys_ReturnsAnEmptyListIfAListWithAFileThatDoesntContainAnEd25519PrivateKeyIsProvided() { + // Given + fileName := "Empty file" + s.createFileWithContent(s.tdir, fileName, "not a Ed25519 private key") + fileNameList := []string{filepath.Join(s.tdir, fileName)} + + // When + a, _ := accessWithTestLogging() + selected := a.filesContainingEd25519PrivateKeys(fileNameList) + + // Then + s.Empty(selected) +} + +func (s *sshSuite) Test_filesContainingEd25519PrivateKeys_ReturnsAListWithOneFileNameIfAListWithAFileThatContainsAnEd25519PrivateKeyIsProvided() { + // Given + fileName := "File-with-content" + s.createFileWithContent(s.tdir, fileName, correctEd25519PrivateKey) + fileNameList := []string{filepath.Join(s.tdir, fileName)} + + // When + a, _ := accessWithTestLogging() + selected := a.filesContainingEd25519PrivateKeys(fileNameList) + + // Then + s.Equal(selected, []string{filepath.Join(s.tdir, fileName)}) +} + +const correctEd25519PrivateKeyOther = ` +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW +QyNTUxOQAAACCRcAFuKgCAnlEuMGswxk18tn2JVXH+7OkVSDbBC2WQ2gAAAJCs0I2+rNCN +vgAAAAtzc2gtZWQyNTUxOQAAACCRcAFuKgCAnlEuMGswxk18tn2JVXH+7OkVSDbBC2WQ2g +AAAED9p1K4JP1ykaLj705pfax2AVvTXryKkJxEXkp3eIuLm5FwAW4qAICeUS4wazDGTXy2 +fYlVcf7s6RVINsELZZDaAAAACmZhdXN0b0BDQUQBAgM= +-----END OPENSSH PRIVATE KEY----- +` + +func (s *sshSuite) Test_filesContainingEd25519PrivateKeys_ReturnsAListWithSeveralFileNamesThatContainsEd25519Key() { + // Given + s.createFileWithContent(s.tdir, "key_file1", correctECDSASSHPrivateKey) + s.createFileWithContent(s.tdir, "key_file2", correctEd25519PrivateKey) + s.createEmptyFile(s.tdir, "key_file3") + s.createFileWithContent(s.tdir, "key_file4", correctEd25519PrivateKeyOther) + + fileList := s.withDirectory("key_file1", "key_file2", "key_file3", "key_file4", "key_file5") + + // When + a, _ := accessWithTestLogging() + selected := a.filesContainingEd25519PrivateKeys(fileList) + + // Then + expected := s.withDirectory("key_file2", "key_file4") + s.Equal(expected, selected) +} diff --git a/ssh/ed25519_key_reader.go b/ssh/ed25519_key_reader.go new file mode 100644 index 0000000..e13d357 --- /dev/null +++ b/ssh/ed25519_key_reader.go @@ -0,0 +1,23 @@ +package ssh + +const ed25519Algorithm = "ssh-ed25519" + +func (a *access) isEd25519PrivateKey(pk string) bool { + priv, ok := a.parsePrivateKey(pk) + if !ok { + return false + } + return priv.isEd25519() +} + +func (k *publicKey) isEd25519() bool { + return k.isAlgorithm(ed25519Algorithm) +} + +func isEd25519PublicKey(k string) bool { + pub, ok := parsePublicKey(k) + if !ok { + return false + } + return pub.isEd25519() +} diff --git a/ssh/ed25519_private_key_parser.go b/ssh/ed25519_private_key_parser.go new file mode 100644 index 0000000..15df133 --- /dev/null +++ b/ssh/ed25519_private_key_parser.go @@ -0,0 +1,5 @@ +package ssh + +func (k *privateKey) isEd25519() bool { + return k.isAlgorithm(ed25519Algorithm) +} diff --git a/ssh/ed25519_private_key_parser_test.go b/ssh/ed25519_private_key_parser_test.go new file mode 100644 index 0000000..40b3b97 --- /dev/null +++ b/ssh/ed25519_private_key_parser_test.go @@ -0,0 +1,21 @@ +package ssh + +const correctEd25519PrivateKey = ` +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW +QyNTUxOQAAACC7eNn1eQ/DPPtfZUAie2p9I1TAuj91YucOlbHyxV7hygAAAJC+YoKmvmKC +pgAAAAtzc2gtZWQyNTUxOQAAACC7eNn1eQ/DPPtfZUAie2p9I1TAuj91YucOlbHyxV7hyg +AAAEBVI12MKVSate/Pvx/nqIe2B4/J3Y8qURPhFGcUZyEtgbt42fV5D8M8+19lQCJ7an0j +VMC6P3Vi5w6VsfLFXuHKAAAACmZhdXN0b0BDQUQBAgM= +-----END OPENSSH PRIVATE KEY----- +` + +func (s *sshSuite) Test_parsePrivateKey_AStringContainingACorrectEd25519KeyShouldBeConsideredAPrivateKey() { + pk := correctEd25519PrivateKey + + a, _ := accessWithTestLogging() + priv, ok := a.parsePrivateKey(pk) + + s.True(ok) + s.Equal("ssh-ed25519", priv.algorithm) +} diff --git a/ssh/file_reader.go b/ssh/file_reader.go index 9aec2d8..c35a42b 100644 --- a/ssh/file_reader.go +++ b/ssh/file_reader.go @@ -42,9 +42,18 @@ func createPrivateKeyRepresentationsFrom(input []string) []*privateKeyRepresenta } func (a *access) privateKeyRepresentationsFrom(input []string) []*privateKeyRepresentation { - return createPrivateKeyRepresentationsFrom(a.filesContainingRSAPrivateKeys(input)) + privateKeyFiles := concat( + a.filesContainingRSAPrivateKeys(input), + a.filesContainingEd25519PrivateKeys(input), + ) + + return createPrivateKeyRepresentationsFrom(privateKeyFiles) } func publicKeyRepresentationsFrom(input []string) []*publicKeyRepresentation { - return createPublicKeyRepresentationsFrom(filesContainingRSAPublicKeys(input)) + publicKeyFiles := concat( + filesContainingRSAPublicKeys(input), + filesContainingEd25519PublicKeys(input), + ) + return createPublicKeyRepresentationsFrom(publicKeyFiles) } diff --git a/ssh/file_reader_test.go b/ssh/file_reader_test.go index e1de11d..6bb5068 100644 --- a/ssh/file_reader_test.go +++ b/ssh/file_reader_test.go @@ -136,7 +136,7 @@ func (s *sshSuite) Test_selectFilesContainingRSAPublicKeys_ReturnsAListWithSever s.Equal(expected, selected) } -func (s *sshSuite) Test_selectFilesContainingRSAPrivateKeys_ReturnsAnEmptyListIfAnEmptyListIsProvided() { +func (s *sshSuite) Test_filesContainingRSAPrivateKeys_ReturnsAnEmptyListIfAnEmptyListIsProvided() { fileNameList := []string{} a, _ := accessWithTestLogging() @@ -145,7 +145,7 @@ func (s *sshSuite) Test_selectFilesContainingRSAPrivateKeys_ReturnsAnEmptyListIf s.Empty(selected) } -func (s *sshSuite) Test_selectFilesContainingRSAPrivateKeys_ReturnsAnEmptyListIfAListWithANonExistingFileIsProvided() { +func (s *sshSuite) Test_filesContainingRSAPrivateKeys_ReturnsAnEmptyListIfAListWithANonExistingFileIsProvided() { fileNameList := []string{"File that doesn't exist"} a, _ := accessWithTestLogging() @@ -154,7 +154,7 @@ func (s *sshSuite) Test_selectFilesContainingRSAPrivateKeys_ReturnsAnEmptyListIf s.Empty(selected) } -func (s *sshSuite) Test_selectFilesContainingRSAPrivateKeys_ReturnsAnEmptyListIfAListWithAnEmptyFileIsProvided() { +func (s *sshSuite) Test_filesContainingRSAPrivateKeys_ReturnsAnEmptyListIfAListWithAnEmptyFileIsProvided() { // Given fileName := "Empty file" s.createEmptyFile(s.tdir, fileName) @@ -168,10 +168,10 @@ func (s *sshSuite) Test_selectFilesContainingRSAPrivateKeys_ReturnsAnEmptyListIf s.Empty(selected) } -func (s *sshSuite) Test_selectFilesContainingRSAPrivateKeys_ReturnsAnEmptyListIfAListWithAFileThatDoesntContainAnRSAPublicKeyIsProvided() { +func (s *sshSuite) Test_filesContainingRSAPrivateKeys_ReturnsAnEmptyListIfAListWithAFileThatDoesntContainAnRSAPrivateKeyIsProvided() { // Given fileName := "Empty file" - s.createFileWithContent(s.tdir, fileName, "not a RSA public key") + s.createFileWithContent(s.tdir, fileName, "not a RSA private key") fileNameList := []string{filepath.Join(s.tdir, fileName)} // When @@ -182,7 +182,7 @@ func (s *sshSuite) Test_selectFilesContainingRSAPrivateKeys_ReturnsAnEmptyListIf s.Empty(selected) } -func (s *sshSuite) Test_selectFilesContainingRSAPrivateKeys_ReturnsAListWithOneFileNameIfAListWithAFileThatContainsAnRSAPublicKeyIsProvided() { +func (s *sshSuite) Test_filesContainingRSAPrivateKeys_ReturnsAListWithOneFileNameIfAListWithAFileThatContainsAnRSAPrivateKeyIsProvided() { // Given fileName := "File-with-content" s.createFileWithContent(s.tdir, fileName, correctRSASSHPrivateKey) @@ -196,7 +196,7 @@ func (s *sshSuite) Test_selectFilesContainingRSAPrivateKeys_ReturnsAListWithOneF s.Equal(selected, []string{filepath.Join(s.tdir, fileName)}) } -func (s *sshSuite) Test_selectFilesContainingRSAPrivateKeys_ReturnsAListWithSeveralFileNamesThatContainsRSAKey() { +func (s *sshSuite) Test_filesContainingRSAPrivateKeys_ReturnsAListWithSeveralFileNamesThatContainsRSAKey() { // Given s.createFileWithContent(s.tdir, "key_file1", correctECDSASSHPrivateKey) s.createFileWithContent(s.tdir, "key_file2", correctRSASSHPrivateKey) diff --git a/ssh/slices.go b/ssh/slices.go index 0944df9..7a30429 100644 --- a/ssh/slices.go +++ b/ssh/slices.go @@ -109,3 +109,11 @@ func foreach[T any](values []T, f func(T)) { f(v) } } + +func concat[T any](s ...[]T) []T { + if len(s) == 0 { + return nil + } + + return append(s[0], concat(s[1:]...)...) +}