From b75e51c423aa20ba5bc16963429d5bb8aa362bee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Sun, 12 Jul 2020 13:50:55 +0100 Subject: [PATCH] add tests + ssh key support --- .gitignore | 3 +- go.mod | 10 ++ pkgs/sops-install-secrets/go.sum => go.sum | 6 +- modules/sops/default.nix | 30 ++++- pkgs/sops-init-gpg-key/sops-init-gpg-key | 2 + pkgs/sops-install-secrets/default.nix | 9 +- pkgs/sops-install-secrets/go.mod | 9 -- pkgs/sops-install-secrets/main.go | 122 ++++++++++++++---- pkgs/sops-install-secrets/main_test.go | 110 +++++++++++----- pkgs/sops-install-secrets/nixos-test.nix | 47 +++++++ pkgs/sops-install-secrets/shell.nix | 4 + ...9B6199193DE765808658DA7A0B892D914345FC.key | Bin 0 -> 977 bytes .../test-assets/gnupghome/pubring.kbx | Bin 0 -> 821 bytes .../test-assets/gnupghome/trustdb.gpg | Bin 0 -> 1200 bytes .../{test-secrets => test-assets}/key.asc | 0 .../{test-secrets => test-assets}/secrets.bin | 0 .../secrets.json | 0 .../test-assets/secrets.yaml | 43 ++++++ .../{test-secrets => test-assets}/sops-edit | 5 +- pkgs/sops-install-secrets/test-assets/ssh-key | 38 ++++++ .../test-assets/ssh-key.asc | 42 ++++++ .../test-assets/ssh-key.pub | 1 + .../test-secrets/secrets.yaml | 25 ---- pkgs/ssh-to-pgp/main.go | 112 ++++++++++++++++ pkgs/ssh-to-pgp/main_test.go | 47 +++++++ pkgs/ssh-to-pgp/shell.nix | 10 ++ pkgs/ssh-to-pgp/test-assets/id_rsa | 38 ++++++ pkgs/ssh-to-pgp/test-assets/id_rsa.pub | 1 + pkgs/sshkeys/convert.go | 94 ++++++++++++++ 29 files changed, 706 insertions(+), 102 deletions(-) create mode 100644 go.mod rename pkgs/sops-install-secrets/go.sum => go.sum (99%) delete mode 100644 pkgs/sops-install-secrets/go.mod create mode 100644 pkgs/sops-install-secrets/nixos-test.nix create mode 100644 pkgs/sops-install-secrets/shell.nix create mode 100644 pkgs/sops-install-secrets/test-assets/gnupghome/private-keys-v1.d/289B6199193DE765808658DA7A0B892D914345FC.key create mode 100644 pkgs/sops-install-secrets/test-assets/gnupghome/pubring.kbx create mode 100644 pkgs/sops-install-secrets/test-assets/gnupghome/trustdb.gpg rename pkgs/sops-install-secrets/{test-secrets => test-assets}/key.asc (100%) rename pkgs/sops-install-secrets/{test-secrets => test-assets}/secrets.bin (100%) rename pkgs/sops-install-secrets/{test-secrets => test-assets}/secrets.json (100%) create mode 100644 pkgs/sops-install-secrets/test-assets/secrets.yaml rename pkgs/sops-install-secrets/{test-secrets => test-assets}/sops-edit (63%) create mode 100644 pkgs/sops-install-secrets/test-assets/ssh-key create mode 100644 pkgs/sops-install-secrets/test-assets/ssh-key.asc create mode 100644 pkgs/sops-install-secrets/test-assets/ssh-key.pub delete mode 100644 pkgs/sops-install-secrets/test-secrets/secrets.yaml create mode 100644 pkgs/ssh-to-pgp/main.go create mode 100644 pkgs/ssh-to-pgp/main_test.go create mode 100644 pkgs/ssh-to-pgp/shell.nix create mode 100644 pkgs/ssh-to-pgp/test-assets/id_rsa create mode 100644 pkgs/ssh-to-pgp/test-assets/id_rsa.pub create mode 100644 pkgs/sshkeys/convert.go diff --git a/.gitignore b/.gitignore index 5f64cce..a70a7d9 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,5 @@ # Dependency directories (remove the comment below to include it) # vendor/ -/pkgs/sops-install-secrets/sops-install-secrets \ No newline at end of file +/pkgs/sops-install-secrets/sops-install-secrets +/pkgs/ssh-to-pgp/ssh-to-pgp \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..aa560aa --- /dev/null +++ b/go.mod @@ -0,0 +1,10 @@ +module github.com/Mic92/sops-nix + +go 1.14 + +require ( + github.com/mozilla-services/yaml v0.0.0-20191106225358-5c216288813c + go.mozilla.org/sops/v3 v3.5.0 + golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899 + golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0 +) diff --git a/pkgs/sops-install-secrets/go.sum b/go.sum similarity index 99% rename from pkgs/sops-install-secrets/go.sum rename to go.sum index 5ce03b9..9c467ee 100644 --- a/pkgs/sops-install-secrets/go.sum +++ b/go.sum @@ -208,8 +208,9 @@ golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnf golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190418165655-df01cb2cc480/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc= golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899 h1:DZhuSZLsGlFL4CmhA8BcRA0mnthyA/nZ00AqCUo7vHg= +golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= @@ -260,9 +261,8 @@ golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0 h1:HyfiK1WMnHj5FXFXatD+Qs1A/xC2Run6RzeW1SyHxpc= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae h1:Ih9Yo4hSPImZOpfGuA4bR/ORKTAbhZo2AbWNRCnevdo= -golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= diff --git a/modules/sops/default.nix b/modules/sops/default.nix index 7aadd03..bfb44d3 100644 --- a/modules/sops/default.nix +++ b/modules/sops/default.nix @@ -78,6 +78,7 @@ let # Does this need to be configurable? secretsMountPoint = "/run/secrets.d"; symlinkPath = "/run/secrets"; + inherit (cfg) gnupgHome sshKeyPaths; }); in { options.sops = { @@ -97,17 +98,38 @@ in { }; gnupgHome = mkOption { - type = types.str; - default = "/root/.gnupg"; + type = types.nullOr types.str; + default = null; + example = "/root/.gnupg"; description = '' - Path to gnupg database directory containing the key for decrypting sops file + Path to gnupg database directory containing the key for decrypting sops file. + ''; + }; + + sshKeyPaths = mkOption { + type = types.listOf types.path; + default = if config.services.openssh.enable then + map (e: e.path) (lib.filter (e: e.type == "rsa") config.services.openssh.hostKeys) + else []; + description = '' + Path to ssh keys added as GPG keys during sops description. + This option must be explicitly unset if config.sops.sshKeyPaths. ''; }; }; config = mkIf (cfg.secrets != {}) { + + assertions = [{ + assertion = cfg.gnupgHome != null -> cfg.sshKeyPaths == []; + message = "config.sops.gnupgHome and config.sops.sshKeyPaths are mutual exclusive"; + } { + assertion = cfg.gnupgHome == null -> cfg.sshKeyPaths != []; + message = "Either config.sops.sshKeyPaths and config.sops.gnupgHome must be set"; + }]; + system.activationScripts.setup-secrets = stringAfter [ "users" "groups" ] '' echo setting up secrets... - SOPS_GPG_EXEC=${pkgs.gnupg}/bin/gpg GNUPGHOME=${cfg.gnupgHome} ${sops-install-secrets}/bin/sops-install-secrets ${manifest} + ${optionalString (cfg.gnupgHome != null) "SOPS_GPG_EXEC=${pkgs.gnupg}/bin/gpg"} ${sops-install-secrets}/bin/sops-install-secrets ${manifest} ''; }; } diff --git a/pkgs/sops-init-gpg-key/sops-init-gpg-key b/pkgs/sops-init-gpg-key/sops-init-gpg-key index a394750..c6ce3c1 100755 --- a/pkgs/sops-init-gpg-key/sops-init-gpg-key +++ b/pkgs/sops-init-gpg-key/sops-init-gpg-key @@ -72,4 +72,6 @@ gpg --export --armor echo 'EOF' rm "${GNUPGHOME}/key-template" +parent=$(dirname "$FINAL_GNUPGHOME") +mkdir -p "$parent" mv "$GNUPGHOME" "$FINAL_GNUPGHOME" diff --git a/pkgs/sops-install-secrets/default.nix b/pkgs/sops-install-secrets/default.nix index 8e34bc0..0b3e0b8 100644 --- a/pkgs/sops-install-secrets/default.nix +++ b/pkgs/sops-install-secrets/default.nix @@ -5,10 +5,9 @@ buildGoModule { hardeningDisable = [ "all" ]; - src = ./.; + src = ../..; - vendorSha256 = "1ky7xzsx12d8m4kvqkayqzybkf3s0w21d6m8qlhvrm00fmyidkxj"; - shellHook = '' - unset GOFLAGS - ''; + subPackages = [ "pkgs/sops-install-secrets" ]; + + vendorSha256 = "sha256-O0z+oEffOOZa/bn2gV9onLVbPBHsNDH2yq1CZPi8w58="; } diff --git a/pkgs/sops-install-secrets/go.mod b/pkgs/sops-install-secrets/go.mod deleted file mode 100644 index f7f3107..0000000 --- a/pkgs/sops-install-secrets/go.mod +++ /dev/null @@ -1,9 +0,0 @@ -module github.com/Mic92/sops-install-secrets - -go 1.14 - -require ( - github.com/mozilla-services/yaml v0.0.0-20191106225358-5c216288813c - go.mozilla.org/sops/v3 v3.5.0 - golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae -) diff --git a/pkgs/sops-install-secrets/main.go b/pkgs/sops-install-secrets/main.go index 8d4886e..7443c68 100644 --- a/pkgs/sops-install-secrets/main.go +++ b/pkgs/sops-install-secrets/main.go @@ -2,6 +2,7 @@ package main import ( "encoding/json" + "errors" "fmt" "io/ioutil" "os" @@ -10,6 +11,8 @@ import ( "strconv" "strings" + "github.com/Mic92/sops-nix/pkgs/sshkeys" + "github.com/mozilla-services/yaml" "go.mozilla.org/sops/v3/decrypt" "golang.org/x/sys/unix" @@ -36,6 +39,8 @@ type manifest struct { Secrets []secret `json:"secrets"` SecretsMountPoint string `json:"secretsMountpoint"` SymlinkPath string `json:"symlinkPath"` + SSHKeyPaths []string `json:"sshKeyPaths"` + GnupgHome string `json:"gnupgHome` } func readManifest(path string) (*manifest, error) { @@ -52,7 +57,26 @@ func readManifest(path string) (*manifest, error) { return &m, nil } -func prepareSymlinks(targetDir string, secrets []secret) error { +func symlinkSecret(targetFile string, secret *secret) error { + for { + currentLinkTarget, err := os.Readlink(secret.Path) + if os.IsNotExist(err) { + if err := os.Symlink(targetFile, secret.Path); err != nil { + return fmt.Errorf("Cannot create symlink '%s': %s", secret.Path, err) + } + return nil + } else if err != nil { + return fmt.Errorf("Cannot read symlink: '%s'", err) + } else if currentLinkTarget == targetFile { + return nil + } + if err := os.Remove(secret.Path); err != nil { + return fmt.Errorf("Cannot override %s", secret.Path) + } + } +} + +func symlinkSecrets(targetDir string, secrets []secret) error { for _, secret := range secrets { targetFile := filepath.Join(targetDir, secret.Name) if targetFile == secret.Path { @@ -62,21 +86,8 @@ func prepareSymlinks(targetDir string, secrets []secret) error { if err := os.MkdirAll(parent, os.ModePerm); err != nil { return fmt.Errorf("Cannot create parent directory of '%s': %s", secret.Path, err) } - for { - currentLinkTarget, err := os.Readlink(secret.Path) - if os.IsNotExist(err) { - if err := os.Symlink(targetFile, secret.Path); err != nil { - return fmt.Errorf("Cannot create symlink '%s': %s", secret.Path, err) - } - return nil - } else if err != nil { - return fmt.Errorf("Cannot read symlink: '%s'", err) - } else if currentLinkTarget == targetFile { - return nil - } - if err := os.Remove(secret.Path); err != nil { - return fmt.Errorf("Cannot override %s", secret.Path) - } + if err := symlinkSecret(targetFile, &secret); err != nil { + return err } } return nil @@ -131,7 +142,7 @@ func decryptSecrets(secrets []secret) error { return nil } -func prepareSecretFs(mountpoint string, keysGid int) error { +func mountSecretFs(mountpoint string, keysGid int) error { if err := os.MkdirAll(mountpoint, 0750); err != nil { return fmt.Errorf("Cannot create directory '%s': %s", mountpoint, err) } @@ -247,6 +258,10 @@ func validateManifest(m *manifest) error { if m.SymlinkPath == "" { m.SymlinkPath = "/run/secrets" } + if len(m.SSHKeyPaths) > 0 && m.GnupgHome != "" { + return errors.New("gnupgHome and sshKeyPaths were specified in the manifest. " + + "Both options are mutual exclusive.") + } for i := range m.Secrets { if err := validateSecret(&m.Secrets[i]); err != nil { return err @@ -288,13 +303,61 @@ func atomicSymlink(oldname, newname string) error { return os.RemoveAll(d) } +func importSSHKeys(keyPaths []string, gpgHome string) error { + secringPath := filepath.Join(gpgHome, "secring.gpg") + secring, err := os.Create(secringPath) + if err != nil { + return fmt.Errorf("Cannot create %s: %s", secringPath, err) + } + for _, path := range keyPaths { + sshKey, err := ioutil.ReadFile(path) + if err != nil { + return fmt.Errorf("Cannot read ssh key '%s': %s", path, err) + } + gpgKey, err := sshkeys.SSHPrivateKeyToPGP(sshKey) + if err != nil { + return err + } + if err := gpgKey.SerializePrivate(secring, nil); err != nil { + return fmt.Errorf("Cannot write secring: %s", err) + } + } + + return nil +} + +type keyring struct { + path string +} + +func (k *keyring) Remove() { + os.RemoveAll(k.path) + os.Unsetenv("GNUPGHOME") +} + +func setupGPGKeyring(sshKeys []string, parentDir string) (*keyring, error) { + dir, err := ioutil.TempDir(parentDir, "gpg") + if err != nil { + return nil, fmt.Errorf("Cannot create gpg home in '%s': %s", parentDir, err) + } + k := keyring{dir} + + if err := importSSHKeys(sshKeys, dir); err != nil { + os.RemoveAll(dir) + return nil, err + } + os.Setenv("GNUPGHOME", dir) + + return &k, nil +} + func installSecrets(args []string) error { if len(args) <= 1 { return fmt.Errorf("USAGE: %s manifest.json", args) } manifest, err := readManifest(args[1]) if err != nil { - return fmt.Errorf("%s", err) + return err } if err := validateManifest(manifest); err != nil { @@ -306,16 +369,24 @@ func installSecrets(args []string) error { return err } + if err := mountSecretFs(manifest.SecretsMountPoint, keysGid); err != nil { + return fmt.Errorf("Failed to mount filesystem for secrets: %s", err) + } + + if len(manifest.SSHKeyPaths) != 0 { + keyring, err := setupGPGKeyring(manifest.SSHKeyPaths, manifest.SecretsMountPoint) + if err != nil { + return fmt.Errorf("Error setting up gpg keyring: %s", err) + } + defer keyring.Remove() + } else if manifest.GnupgHome != "" { + os.Setenv("GNUPGHOME", manifest.GnupgHome) + } + if err := decryptSecrets(manifest.Secrets); err != nil { return err } - if err := prepareSecretFs(manifest.SecretsMountPoint, keysGid); err != nil { - return fmt.Errorf("Failed to mount filesystem for secrets: %s", err) - } - if err := prepareSymlinks(manifest.SymlinkPath, manifest.Secrets); err != nil { - return fmt.Errorf("Failed to prepare symlinks to secret store: %s", err) - } secretDir, err := prepareSecretsDir(manifest.SecretsMountPoint, manifest.SymlinkPath, keysGid) if err != nil { return fmt.Errorf("Failed to prepare new secrets directory: %s", err) @@ -323,6 +394,9 @@ func installSecrets(args []string) error { if err := writeSecrets(*secretDir, manifest.Secrets); err != nil { return fmt.Errorf("Cannot write secrets: %s", err) } + if err := symlinkSecrets(manifest.SymlinkPath, manifest.Secrets); err != nil { + return fmt.Errorf("Failed to prepare symlinks to secret store: %s", err) + } if err := atomicSymlink(*secretDir, manifest.SymlinkPath); err != nil { return fmt.Errorf("Cannot update secrets symlink: %s", err) } diff --git a/pkgs/sops-install-secrets/main_test.go b/pkgs/sops-install-secrets/main_test.go index 909f1d4..23dd522 100644 --- a/pkgs/sops-install-secrets/main_test.go +++ b/pkgs/sops-install-secrets/main_test.go @@ -43,24 +43,48 @@ func writeManifest(t *testing.T, dir string, m *manifest) string { return filename } -func TestCliArgs(t *testing.T) { +func testAssetPath() string { _, filename, _, _ := runtime.Caller(0) - var testSecrets = path.Join(path.Dir(filename), "test-secrets") + return path.Join(path.Dir(filename), "test-assets") +} +type testDir struct { + path, secretsPath, symlinkPath string +} + +func (dir testDir) Remove() { + os.RemoveAll(dir.path) +} + +func newTestDir(t *testing.T) testDir { tempdir, err := ioutil.TempDir("", "symlinkDir") ok(t, err) - defer os.RemoveAll(tempdir) - secretsPath := path.Join(tempdir, "secrets.d") - symlinkPath := path.Join(tempdir, "secrets") - gpgHome := path.Join(tempdir, "gpg-home") + return testDir{tempdir, path.Join(tempdir, "secrets.d"), path.Join(tempdir, "secrets")} +} + +func testInstallSecret(t *testing.T, testdir testDir, m *manifest) { + path := writeManifest(t, testdir.path, m) + ok(t, installSecrets([]string{"sops-install-secrets", path})) +} + +func testGPG(t *testing.T) { + assets := testAssetPath() + + testdir := newTestDir(t) + defer testdir.Remove() + gpgHome := path.Join(testdir.path, "gpg-home") + gpgEnv := append(os.Environ(), fmt.Sprintf("GNUPGHOME=%s", gpgHome)) ok(t, os.Mkdir(gpgHome, os.FileMode(0700))) - os.Setenv("GNUPGHOME", gpgHome) - cmd := exec.Command("gpg", "--import", path.Join(testSecrets, "key.asc")) + cmd := exec.Command("gpg", "--import", path.Join(assets, "key.asc")) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr + cmd.Env = gpgEnv ok(t, cmd.Run()) stopGpgCmd := exec.Command("gpgconf", "--kill", "gpg-agent") + stopGpgCmd.Stdout = os.Stdout + stopGpgCmd.Stderr = os.Stderr + stopGpgCmd.Env = gpgEnv defer func() { if err := stopGpgCmd.Run(); err != nil { fmt.Printf("failed to stop gpg-agent: %s\n", err) @@ -73,43 +97,40 @@ func TestCliArgs(t *testing.T) { Key: "test_key", Owner: "nobody", Group: "nogroup", - SourceFile: path.Join(testSecrets, "secrets.yaml"), - Path: path.Join(tempdir, "test-target"), + SopsFile: path.Join(assets, "secrets.yaml"), + Path: path.Join(testdir.path, "test-target"), Mode: "0400", RestartServices: []string{"affected-service"}, ReloadServices: make([]string, 0), } - var jsonSecret secret + var jsonSecret, binarySecret secret // should not create a symlink jsonSecret = yamlSecret jsonSecret.Name = "test2" jsonSecret.Owner = "root" jsonSecret.Format = "json" jsonSecret.Group = "root" - jsonSecret.SourceFile = path.Join(testSecrets, "secrets.json") - jsonSecret.Path = path.Join(symlinkPath, "test2") + jsonSecret.SopsFile = path.Join(assets, "secrets.json") + jsonSecret.Path = path.Join(testdir.secretsPath, "test2") jsonSecret.Mode = "0700" - var binarySecret secret binarySecret = yamlSecret binarySecret.Name = "test3" binarySecret.Format = "binary" - binarySecret.SourceFile = path.Join(testSecrets, "secrets.bin") - binarySecret.Path = path.Join(symlinkPath, "test3") + binarySecret.SopsFile = path.Join(assets, "secrets.bin") + binarySecret.Path = path.Join(testdir.secretsPath, "test3") manifest := manifest{ Secrets: []secret{yamlSecret, jsonSecret, binarySecret}, - SecretsMountPoint: secretsPath, - SymlinkPath: symlinkPath, + SecretsMountPoint: testdir.secretsPath, + SymlinkPath: testdir.symlinkPath, + GnupgHome: gpgHome, } - manifestPath := writeManifest(t, tempdir, &manifest) + testInstallSecret(t, testdir, &manifest) - err = installSecrets([]string{"sops-install-secrets", manifestPath}) - ok(t, err) - - _, err = os.Stat(manifest.SecretsMountPoint) + _, err := os.Stat(manifest.SecretsMountPoint) ok(t, err) _, err = os.Stat(manifest.SymlinkPath) @@ -150,13 +171,44 @@ func TestCliArgs(t *testing.T) { content, err = ioutil.ReadFile(binarySecret.Path) ok(t, err) - equals(t, "binary_value\n", string(content)) - manifestPath = writeManifest(t, symlinkPath, &manifest) + testInstallSecret(t, testdir, &manifest) - err = installSecrets([]string{"sops-install-secrets", manifestPath}) + target, err := os.Readlink(testdir.symlinkPath) ok(t, err) - - target, err := os.Readlink(symlinkPath) - equals(t, path.Join(secretsPath, "2"), target) + equals(t, path.Join(testdir.secretsPath, "2"), target) +} + +func testSSHKey(t *testing.T) { + assets := testAssetPath() + + testdir := newTestDir(t) + defer testdir.Remove() + + s := secret{ + Name: "test", + Key: "test_key", + Owner: "nobody", + Group: "nogroup", + SopsFile: path.Join(assets, "secrets.yaml"), + Path: path.Join(testdir.path, "test-target"), + Mode: "0400", + RestartServices: []string{"affected-service"}, + ReloadServices: make([]string, 0), + } + + m := manifest{ + Secrets: []secret{s}, + SecretsMountPoint: testdir.secretsPath, + SymlinkPath: testdir.symlinkPath, + SSHKeyPaths: []string{path.Join(assets, "ssh-key")}, + } + + testInstallSecret(t, testdir, &m) +} + +func TestAll(t *testing.T) { + // we can't test in parallel because we rely on GNUPGHOME environment variable + testGPG(t) + testSSHKey(t) } diff --git a/pkgs/sops-install-secrets/nixos-test.nix b/pkgs/sops-install-secrets/nixos-test.nix new file mode 100644 index 0000000..fb0988a --- /dev/null +++ b/pkgs/sops-install-secrets/nixos-test.nix @@ -0,0 +1,47 @@ +let + makeTest = import ; +in { + ssh-keys = makeTest { + nodes.server = { ... }: { + imports = [ ../../modules/sops ]; + services.openssh.enable = true; + services.openssh.hostKeys = [{ + type = "rsa"; + bits = 4096; + path = ./test-assets/ssh-key; + }]; + sops.defaultSopsFile = ./test-assets/secrets.yaml; + sops.secrets.test_key = {}; + }; + + testScript = '' + start_all() + server.succeed("cat /run/secrets/test_key | grep -q test_value") + ''; + }; + + gpg-keys = makeTest { + nodes.server = { pkgs, lib, ... }: { + imports = [ ../../modules/sops ]; + sops.gnupgHome = "/run/gpghome"; + sops.defaultSopsFile = ./test-assets/secrets.yaml; + sops.secrets.test_key = {}; + # must run before sops + system.activationScripts.gnupghome = lib.stringAfter [ "etc" ] '' + cp -r ${./test-assets/gnupghome} /run/gpghome + chmod -R 700 /run/gpghome + ''; + # Useful for debugging + #environment.systemPackages = [ pkgs.gnupg pkgs.sops ]; + #environment.variables = { + # GNUPGHOME = "/run/gpghome"; + # SOPS_GPG_EXEC="${pkgs.gnupg}/bin/gpg"; + # SOPSFILE = "${./test-assets/secrets.yaml}"; + #}; + }; + testScript = '' + start_all() + server.succeed("cat /run/secrets/test_key | grep -q test_value") + ''; + }; +} diff --git a/pkgs/sops-install-secrets/shell.nix b/pkgs/sops-install-secrets/shell.nix new file mode 100644 index 0000000..fbe3e5b --- /dev/null +++ b/pkgs/sops-install-secrets/shell.nix @@ -0,0 +1,4 @@ +{ pkgs ? import {} }: +pkgs.mkShell { + nativeBuildInputs = with pkgs; [ go delve utillinux gnupg ]; +} diff --git a/pkgs/sops-install-secrets/test-assets/gnupghome/private-keys-v1.d/289B6199193DE765808658DA7A0B892D914345FC.key b/pkgs/sops-install-secrets/test-assets/gnupghome/private-keys-v1.d/289B6199193DE765808658DA7A0B892D914345FC.key new file mode 100644 index 0000000000000000000000000000000000000000..e0ed157751e2e5d4e4bfdf02eb2b0ed9d525b462 GIT binary patch literal 977 zcmdNeG_)!x$}CGPN!86xt<*5KDk@IYFto}uGBvkiST&9JP6Fej=&m1jH=Un*rcUEl zntEF&SGFPftA>Xoh`c3_5Xa|s)SDQKn_Nc`AaYSx}ZEua9aj%#{swXTwK59 zW`4c@<-^7Y-{kz+*K*F0Dsn&b-`#aj_0?+;$+`YLul}tlyKw27a@|q;=Ka#VTlB6a z&41A47~np60jhFZJlmSiWFDHE$@%ZxoZH{l8n-d(wS;O& z{m~c6-jpuUUHvRJqA00Au|RV6pG=_?*Q+O|O<0@!^xB(K2ANWuV$Is(M2mN5%zwG= zsO8Sf0*cpUlh}1ne!jWnTVN80>4kHQq0Sm>F1ze0X6t*z$rG@9leVUYp;fA}6(a*9 zh?QbwYG%d!H?DMk{(SC@cmFi(h%a8j^mo$3#;Hau@8&alE{VUf?On^=eb3eAYX$P_ z-JjpDZ?3}oF@N1cr?ZWok*=%0?b=Wqy(IblqRo2?3R*L+UgkW~S~vG+$U0r+=O$bq zv(FvP`m|0{;NCM+|KJ?<#*FRj1NzIK=tlB>&$#=qvmmmHUHf0wdkJR4;v+wlUwu2E z&8D(F%5>?`{t4GJGTKT$J>I&s)?J_P!4B`O8-9pCa8a7B;=(KT_Cf6fnf=%N_8Qu5 zl@;XPYoT5B@xWTnP>GJ_K&gGAXVtl@+#XoY`90s;L|?3S2dJTc75hZ$FF8Hc8|OvocMQYd0u7`C$X|W?Si} zqT6n+UfH_w*^~Ltu3RqTd~0#@`lISusd2uudy6ma?VaVEH#d}5$lhnO`9)W*_#RQ; z(|oC^AeR(^U2^G$vX$P8aF;pSYWt1VysJgyQ;qBwbQ|}4D+};TQt~e09@K_VTjUwHY@nyvjRse>V8vo5b+NGP3_b^4BYMejffHmy{YBSy&bB-V>s| zH+@64+~I?-7JLkIVtX^C_mat~WmzbR%bqJX1+ajTFl}=IJZ{BrLbK7XDk` huPv$5b9wdQRbfBu%$JpK|Fo)PF;lW!WSFL=CIEh{$^HNU literal 0 HcmV?d00001 diff --git a/pkgs/sops-install-secrets/test-assets/gnupghome/pubring.kbx b/pkgs/sops-install-secrets/test-assets/gnupghome/pubring.kbx new file mode 100644 index 0000000000000000000000000000000000000000..903b0c3cf9846b560a757b4a270871593d206f04 GIT binary patch literal 821 zcmZQzU{GLWWMJ}kib!K%U|@*n`ZoiFL2L+S7G+`tv*H*Sm=-fIGRV~Lm@c~N)+FB4 zOL56Rcg=l$!9M%|$Q-aL1_nk39tH-+ItB&?8L$W=0}Gf&B49SmWaMRu`*$~vk%M8? zG~PQ2jE|zbe%Re~e(sq%jazBzZJk`%hUB+ObC;~h{JM6w=t|fB^L?ulI>iGy7)9nU zz3}UT@+`q^8N3|_+}3k({g#{g_5PO+8y|d=^JibnIY+9<{mg%N*FDu&uSF#1`uDv0 zx1#LArEki0N9~*UOY?5gyOuQnL6>9PQMT9>rLB#{HKH+0od2`W_4Ng)%4zXzYdVv8 zXl^FwzjJeLe_LzZ#;Dg4sv-49UnF}|x z?-3_Y!0t`j41$adj2n0u7}5*UA>qxiMJBr_zo1yRB(=Ci!KNrbzr+C{U8*)PQG{n@IG%hfZjR)6=MqO*yME8*>C<@0$L wQuG-Gm+k!WC%NLs63Y#249w{T=?n*pzpQKW{}8gxHMii3%wqf13rto400kyTWdHyG literal 0 HcmV?d00001 diff --git a/pkgs/sops-install-secrets/test-assets/gnupghome/trustdb.gpg b/pkgs/sops-install-secrets/test-assets/gnupghome/trustdb.gpg new file mode 100644 index 0000000000000000000000000000000000000000..bbe17a0bcb0d4441d116b6d7296bd1d4f9415e78 GIT binary patch literal 1200 zcmZQfFGy!*W@Ke#Vql2p`Zt3CJ7DC(E{-8OstzMazyhP;0uBZYVl-SZf&?ru8ZO{q Gz#sq&O9XKM literal 0 HcmV?d00001 diff --git a/pkgs/sops-install-secrets/test-secrets/key.asc b/pkgs/sops-install-secrets/test-assets/key.asc similarity index 100% rename from pkgs/sops-install-secrets/test-secrets/key.asc rename to pkgs/sops-install-secrets/test-assets/key.asc diff --git a/pkgs/sops-install-secrets/test-secrets/secrets.bin b/pkgs/sops-install-secrets/test-assets/secrets.bin similarity index 100% rename from pkgs/sops-install-secrets/test-secrets/secrets.bin rename to pkgs/sops-install-secrets/test-assets/secrets.bin diff --git a/pkgs/sops-install-secrets/test-secrets/secrets.json b/pkgs/sops-install-secrets/test-assets/secrets.json similarity index 100% rename from pkgs/sops-install-secrets/test-secrets/secrets.json rename to pkgs/sops-install-secrets/test-assets/secrets.json diff --git a/pkgs/sops-install-secrets/test-assets/secrets.yaml b/pkgs/sops-install-secrets/test-assets/secrets.yaml new file mode 100644 index 0000000..a721aac --- /dev/null +++ b/pkgs/sops-install-secrets/test-assets/secrets.yaml @@ -0,0 +1,43 @@ +test_key: ENC[AES256_GCM,data:4cC2PTi7xVPZPA==,iv:voX4IQemcgt0O97oLExy5r2V85nn687cIyWmHNDhUag=,tag:ZaKi9m6ziFKNV+gx7XedTw==,type:str] +sops: + kms: [] + gcp_kms: [] + azure_kv: [] + lastmodified: '2020-07-12T08:04:25Z' + mac: ENC[AES256_GCM,data:Sw+u03EAxagKQ9qd4Vwr5BRrnAdtPlUC660fpaVb62W481YVmcUo/CW+SBwdZhn1oSAqGDFE0exqWp4+FRhBPNnxcatI2kjnJ/m9INZhrjgTGVcSVC+pLfXYrmtqCxJCS1clREuQ89QG3inDQvgJ2M+A8S6qhlwPfXlIhuyHMI8=,iv:zpdbtNjicBx74MnYqLwMkY0atPFe7BDJI8o4VDhGlb0=,tag:yGnUUSrs+6uFZGuX33yVJg==,type:str] + pgp: + - created_at: '2020-07-12T08:03:51Z' + enc: | + -----BEGIN PGP MESSAGE----- + + hQEMA/m6nevQP1fAAQgAnGLEGUnWgYTLGGZN7sEETu14MJjr4TY7JDhs8VOrA9ng + x9ivSF1dPw6arewal70OgxMLnS55HY6L81vRCbu8d0rdfXkdzO6oeMAo79/udKc8 + F5CDSkdrBImK5xpEQ0stpt1zmmKMAPGuPSbSc48YEt4OYUdpoIO+PesYUUgGerL7 + 4UvXtbhYjIhZL7Sgst/coOUNUWjpQgVRSeza6qcuVfS3gVBvxNCed51n+Cr3/rnD + 5PXjeWQ8aayLDDV32CjGf+s3+LG0gvJ7A6eq0THjsvgkY4qGHNSMobkN7npFI4lX + KZG68Scu5d11IY2N3y0ijRQQxYlPPPpukWvw3CK8htJeAZltLDHzh0bpISpc7gqU + xzATRG/LmJk7yS9Vej/B0NGmkVlbB+lFnLZG3lVmDiaQUsgePp2Ho1/j6ysit3/C + i87IM8B71y1brldk2mcr1oAoNSVE4dg+/C2HrDRKxA== + =Qyuq + -----END PGP MESSAGE----- + fp: 7FB89715AADA920D65D25E63F9BA9DEBD03F57C0 + - created_at: '2020-07-12T08:03:51Z' + enc: | + -----BEGIN PGP MESSAGE----- + + hQGMA3ulPRkZxd/UAQv/d1ExCl7gVIe0GeH8bc0xl8gt4hvR2ujH0BmmHpQ7SDLa + tS7lH3ennlLlN+owN3DkCKqm/BNEQ7Wy5y0oTvmQSo2BdSMYOvHrLKoPK3OsDLPH + vtXUKZ+NLKSV2xCSiqrcu4GRki8klLgDNEgna8T21erCXZM8W/ehcOd7gLSUiatd + wQTGEhNAA0RO04kf0NoPcNrPak7e0bGbwEc88RdM2QyxSXi6SVrKVHvYVPE3pfFB + VkUcWZU2edLUEKIAFRAtmb9s3DSjECMMuoVXR6lrPx4c6OMRKvxtlZh0uOOcRpoG + M5KL8O4ZXNxUVNRzPisgqe0hkh2AsnMLIQeWrVLfOLXUdDjr9x42vga/P8WU7AzT + /Lxmlu9cY0KKsXpv7yXibZfFJ/K/4Mo9WsNk6tGAmiSUg1hr/NJPha3PDHyX0ER7 + 6dPFeMrvs4VngC0I27v6rlEjU+HiRv3/uLnn04z9DRQEk6a+YpLZTpHvAbzfb1Dg + EBpKCQePm2rIzywLE8d60lABXSQKG4/LSH6WgxfRFibORME2z5DD2G7ibiDbVg8j + YbrH0E0KeRxYJMJ28FVDlLgoUL+JbNr70uHkdvz8S0hqCYwo+K4KqUOIsiPc6iIL + tA== + =XoKf + -----END PGP MESSAGE----- + fp: 2504791468B153B8A3963CC97BA53D1919C5DFD4 + unencrypted_suffix: _unencrypted + version: 3.5.0 diff --git a/pkgs/sops-install-secrets/test-secrets/sops-edit b/pkgs/sops-install-secrets/test-assets/sops-edit similarity index 63% rename from pkgs/sops-install-secrets/test-secrets/sops-edit rename to pkgs/sops-install-secrets/test-assets/sops-edit index 5e45d80..381861d 100755 --- a/pkgs/sops-install-secrets/test-secrets/sops-edit +++ b/pkgs/sops-install-secrets/test-assets/sops-edit @@ -11,8 +11,9 @@ export GNUPGHOME=$(mktemp -d) trap "gpgconf --kill gpg-agent && rm -rf $GNUPGHOME" EXIT DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" -gpg --import "$DIR/key.asc" +gpg --import "$DIR/key.asc" "$DIR/ssh-key.asc" gpg --fingerprint --list-keys -fpr=$(gpg --with-fingerprint --with-colons --show-key ./key.asc | awk -F: '$1 == "fpr" { print $10;}') + +fpr=$(gpg --with-fingerprint --with-colons --show-key "$DIR/key.asc" "$DIR/ssh-key.asc" | awk -F: '$1 == "fpr" { print $10;}' | xargs | sed -e 's/ /,/g') sops --pgp "$fpr" "$@" \ No newline at end of file diff --git a/pkgs/sops-install-secrets/test-assets/ssh-key b/pkgs/sops-install-secrets/test-assets/ssh-key new file mode 100644 index 0000000..4ba1332 --- /dev/null +++ b/pkgs/sops-install-secrets/test-assets/ssh-key @@ -0,0 +1,38 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +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 +-----END OPENSSH PRIVATE KEY----- diff --git a/pkgs/sops-install-secrets/test-assets/ssh-key.asc b/pkgs/sops-install-secrets/test-assets/ssh-key.asc new file mode 100644 index 0000000..997260c --- /dev/null +++ b/pkgs/sops-install-secrets/test-assets/ssh-key.asc @@ -0,0 +1,42 @@ +-----BEGIN PGP PRIVATE KEY BLOCK----- + +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 +=WFkZ +-----END PGP PRIVATE KEY BLOCK----- \ No newline at end of file diff --git a/pkgs/sops-install-secrets/test-assets/ssh-key.pub b/pkgs/sops-install-secrets/test-assets/ssh-key.pub new file mode 100644 index 0000000..9966a38 --- /dev/null +++ b/pkgs/sops-install-secrets/test-assets/ssh-key.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDtwQhY3uyYF0EtACthbkam7TA672VqWH6epfX/m6KqVTbqozHstCp6CRuBhGxTtkHQNbf38iQkwePPaqXQWMD1ahyKmgUg1LNek1At6X+Q4kOHTKyS8ygbavKy7MhGT8TE3fG/M1PZoz58tw7Coe0tLlQh+wPnIFEhBeR15ZxZAaJWbxYXnvijbXwkxDV/KJ9itElmH3SpkOyhUhZCThWsORNF9ipbZVwiVRwOjoOdHISZUrUvcamhxxt8TM8sKWzao2GXnQTiMvf8xMfyAtHLGBdPpiVcSym2FvjPqRSmKoVFDFwsNQMfcDv1eSH1/c5art14KyKwuJpkiJUPWwfdppdjHbBV26moE+4YvMi04Z7tw2RMi0x8tXaiYxwhNvyDVqBdSD56AjckTmHo5dZCjbcsV00gzDiVZlWgP8RCzr55BJlMVjKLM6YkzVZl+c5kJorsfL3NyohxC/hrIwItQoqJW9XtA/rSd9/1AdkrE+iPahsCQQ1ZhH0KiUr00Pk= joerg@turingmachine diff --git a/pkgs/sops-install-secrets/test-secrets/secrets.yaml b/pkgs/sops-install-secrets/test-secrets/secrets.yaml deleted file mode 100644 index 786ab27..0000000 --- a/pkgs/sops-install-secrets/test-secrets/secrets.yaml +++ /dev/null @@ -1,25 +0,0 @@ -test_key: ENC[AES256_GCM,data:bjCOFe3ngTjbBA==,iv:v5M/Ws9d4yBAMFZ13qJ6hkLPceT7a2wcxiJjVp8mvSQ=,tag:zAWPg4H7E2Eavq+iE3yXpg==,type:str] -sops: - kms: [] - gcp_kms: [] - azure_kv: [] - lastmodified: '2020-07-05T19:01:53Z' - mac: ENC[AES256_GCM,data:CrYKSxkKZynq9wwD11GE280VIwv0FqFRNgm5EF588fTN4RwVs8s51DoN4Qx9kPoabYH/lwv+OdeVRq9mbYxtclhJh6ind04pmz5UZps/V5PgJDFd0ckLc9SCeqnEmvBFsp9WM+Tr6M/v9QgeamK+GGovw0+ePwgU1VADPnoyTAo=,iv:kgtewrlBa2N3qyhd7ipTZSxMQndvTy2p8PIM3lynrNY=,tag:aUzPq+u2fi5zziYXVfdaYQ==,type:str] - pgp: - - created_at: '2020-07-05T19:01:34Z' - enc: | - -----BEGIN PGP MESSAGE----- - - hQEMA/m6nevQP1fAAQgAnUb/ShUMr5vkDLG+LWGD8DT6etv3kPoJIhq7C0d7y/BX - 0UHTcxR4coZ55jB6+juxGmh9e9WhawoxHToUs/edxJHxOlC19HunWv9EvTFkKYVf - MAFu+Gfx6cWgDPOrrtvFVZ3YUqPizoOUmP/Op5y/gxhKEYBtD1/5YuSGNnL1VYkv - ZEfgPYtAq5Jmx+jF2Smkb/T6cWFV79WUJTZbYRQs466qQHtZGU9Ms9amxQ73Xf/Y - KhysQCu1yuVxGzBuPVrT5jbZdgaHm5KlmSIGwk47m+PH4ienkhE76WTUCr4xx9R8 - n8/+eVga9cYWbwHYoY84GNUi7fbx5LTD3x19LnVk9dJeAahWrgzT3azwJRPH7WdW - U8+La2v36cucQ/u7+KraVCofOuNXBsjZOJokte97DtKwqtPXRSbyL8owSA90O3Vh - ++zsV3ufM+8TO+9LDAjMqmF0lpQuGexNPafdkuAGdQ== - =5GUN - -----END PGP MESSAGE----- - fp: 7FB89715AADA920D65D25E63F9BA9DEBD03F57C0 - unencrypted_suffix: _unencrypted - version: 3.5.0 diff --git a/pkgs/ssh-to-pgp/main.go b/pkgs/ssh-to-pgp/main.go new file mode 100644 index 0000000..f046c8f --- /dev/null +++ b/pkgs/ssh-to-pgp/main.go @@ -0,0 +1,112 @@ +package main + +import ( + "flag" + "fmt" + "io" + "io/ioutil" + "os" + "syscall" + + "github.com/Mic92/sops-nix/pkgs/sshkeys" + "golang.org/x/crypto/openpgp" + "golang.org/x/crypto/openpgp/armor" + "golang.org/x/crypto/ssh/terminal" +) + +type options struct { + publicKey, privateKey, format, out string +} + +func parseFlags(args []string) options { + var opts options + f := flag.NewFlagSet(args[0], flag.ExitOnError) + f.StringVar(&opts.publicKey, "pubkey", "", "Path to public key. Reads from standard input if equal to '-'") + f.StringVar(&opts.privateKey, "privkey", "", "Path to private key. Reads from standard input if equal to '-'") + f.StringVar(&opts.format, "format", "auto", "GPG format encoding (auto|binary|armor)") + f.StringVar(&opts.out, "o", "-", "Output path. Prints by default to standard output") + f.Parse(args[1:]) + + if opts.format == "auto" { + if opts.out == "-" && terminal.IsTerminal(syscall.Stdout) { + opts.format = "armor" + } else { + opts.format = "binary" + } + } + if opts.publicKey != "" && opts.privateKey != "" { + fmt.Fprintln(os.Stderr, "-pubkey and -privkey are mutual exclusive") + os.Exit(1) + } + + if opts.publicKey == "" && opts.privateKey == "" { + fmt.Fprintln(os.Stderr, "Either -pubkey and -privkey must be specified") + os.Exit(1) + } + + return opts +} + +func convertKeys(args []string) error { + opts := parseFlags(args) + var err error + var sshKey []byte + keyPath := opts.privateKey + if opts.publicKey != "" { + keyPath = opts.publicKey + } + if keyPath == "-" { + sshKey, _ = ioutil.ReadAll(os.Stdin) + if err != nil { + return fmt.Errorf("error reading stdin: %s", err) + } + } else { + sshKey, err = ioutil.ReadFile(keyPath) + if err != nil { + return fmt.Errorf("error reading %s: %s", opts.privateKey, err) + } + } + + writer := io.WriteCloser(os.Stdout) + if opts.out != "-" { + writer, err = os.Create(opts.out) + if err != nil { + return fmt.Errorf("failed to create %s: %s", opts.out, err) + } + defer writer.Close() + } + + if opts.format == "armor" { + keyType := openpgp.PrivateKeyType + if opts.publicKey != "" { + keyType = openpgp.PublicKeyType + } + writer, err = armor.Encode(writer, keyType, make(map[string]string)) + if err != nil { + return fmt.Errorf("failed to encode armor writer") + } + defer writer.Close() + } + + if opts.publicKey != "" { + gpgKey, err := sshkeys.SSHPublicKeyToPGP(sshKey) + if err != nil { + return err + } + gpgKey.Serialize(writer) + } else { + gpgKey, err := sshkeys.SSHPrivateKeyToPGP(sshKey) + if err != nil { + return err + } + gpgKey.SerializePrivate(writer, nil) + } + return err +} + +func main() { + if err := convertKeys(os.Args); err != nil { + fmt.Fprintf(os.Stderr, "%s: %s", os.Args[0], err) + os.Exit(1) + } +} diff --git a/pkgs/ssh-to-pgp/main_test.go b/pkgs/ssh-to-pgp/main_test.go new file mode 100644 index 0000000..14e7296 --- /dev/null +++ b/pkgs/ssh-to-pgp/main_test.go @@ -0,0 +1,47 @@ +package main + +import ( + "fmt" + "io/ioutil" + "os" + "os/exec" + "path" + "path/filepath" + "runtime" + "testing" +) + +// ok fails the test if an err is not nil. +func ok(tb testing.TB, err error) { + if err != nil { + _, file, line, _ := runtime.Caller(1) + fmt.Printf("\033[31m%s:%d: unexpected error: %s\033[39m\n\n", filepath.Base(file), line, err.Error()) + tb.FailNow() + } +} + +func TestCli(t *testing.T) { + _, filename, _, _ := runtime.Caller(0) + assets := path.Join(path.Dir(filename), "test-assets") + tempdir, err := ioutil.TempDir("", "testdir") + ok(t, err) + defer os.RemoveAll(tempdir) + + out := path.Join(tempdir, "out") + pubKey := path.Join(assets, "id_rsa.pub") + privKey := path.Join(assets, "id_rsa") + cmds := [][]string{ + {"ssh-to-pgp", "-pubkey", pubKey, "-o", out}, + {"ssh-to-pgp", "-format=armor", "-pubkey", pubKey, "-o", out}, + {"ssh-to-pgp", "-privkey", privKey, "-o", out}, + {"ssh-to-pgp", "-format=armor", "-privkey", privKey, "-o", out}, + } + for _, cmd := range cmds { + err = convertKeys(cmd) + ok(t, err) + cmd := exec.Command("gpg", "--with-fingerprint", "--show-key", out) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + ok(t, cmd.Run()) + } +} diff --git a/pkgs/ssh-to-pgp/shell.nix b/pkgs/ssh-to-pgp/shell.nix new file mode 100644 index 0000000..c88d83f --- /dev/null +++ b/pkgs/ssh-to-pgp/shell.nix @@ -0,0 +1,10 @@ +with import {}; +mkShell { + nativeBuildInputs = [ + bashInteractive + go + delve + gnupg + ]; + hardeningDisable = [ "all" ]; +} diff --git a/pkgs/ssh-to-pgp/test-assets/id_rsa b/pkgs/ssh-to-pgp/test-assets/id_rsa new file mode 100644 index 0000000..7b631e7 --- /dev/null +++ b/pkgs/ssh-to-pgp/test-assets/id_rsa @@ -0,0 +1,38 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +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 +-----END OPENSSH PRIVATE KEY----- diff --git a/pkgs/ssh-to-pgp/test-assets/id_rsa.pub b/pkgs/ssh-to-pgp/test-assets/id_rsa.pub new file mode 100644 index 0000000..330534f --- /dev/null +++ b/pkgs/ssh-to-pgp/test-assets/id_rsa.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDSeS6SEputIOi2mQhLeIEJnMAink+KcUv38HnaLak3nnLmUsYJnXYB5KZGxaxVtIjr59J8TndmwniZ+wc0rql6Dkif9CsTXgAjxrPiknZNQ7JQbgWUr0pk4jx/K3zLD6i/XAS8QWySNJmY5aJWySbF/K687kUMJ5ql0BX4Tt0RiWL4pIwzZZlLzH4rRySy4z1kbiuOZf8htVRtlGoDGqGViJRpuybSKrmXbevRI7aWjiml2BVTMktPekAPx+MA3t/8EM/uJxtWp7g3BsneHQdKIjR0WEKAITTmuDLEEtIXXEUbgBW0WjbD62nRft/A6/iyWykPJmkLA4WnSLS03caeUxCKoEthZ1xfBPCRNw7xbysQF8CHJz8cAMjZGgBGlOin8EKDhmlma6FZ94cAB5Tr4G3R0h4ky77bPk2/6vvZtyU/AFnDP2HfGaRCDNF+Q7+fR9YmKwcW/vCa2ItIEXgMmBjS+yl0p+4fVaY6Q7bCTbrd6znb6gTGo7nD9Kj/CGU= joerg@turingmachine diff --git a/pkgs/sshkeys/convert.go b/pkgs/sshkeys/convert.go new file mode 100644 index 0000000..ca51f6b --- /dev/null +++ b/pkgs/sshkeys/convert.go @@ -0,0 +1,94 @@ +package sshkeys + +import ( + "crypto" + "crypto/rsa" + "fmt" + "reflect" + "time" + + "golang.org/x/crypto/openpgp" + "golang.org/x/crypto/openpgp/packet" + "golang.org/x/crypto/ssh" +) + +func parsePublicKey(publicKey []byte) (*rsa.PublicKey, error) { + key, _, _, _, err := ssh.ParseAuthorizedKey(publicKey) + if err != nil { + return nil, fmt.Errorf("failed to parse public ssh key: %s", err) + } + + cryptoPublicKey, ok := key.(ssh.CryptoPublicKey) + + if !ok { + return nil, fmt.Errorf("Unsupported public key algo: %s", key.Type()) + } + + rsaKey, ok := cryptoPublicKey.CryptoPublicKey().(*rsa.PublicKey) + + if !ok { + return nil, fmt.Errorf("Unsupported public key algo: %s", key.Type()) + } + + return rsaKey, nil +} + +func SSHPublicKeyToPGP(sshPublicKey []byte) (*packet.PublicKey, error) { + rsaKey, err := parsePublicKey(sshPublicKey) + if err != nil { + return nil, err + } + return packet.NewRSAPublicKey(time.Unix(0, 0), rsaKey), nil +} + +func parsePrivateKey(sshPrivateKey []byte) (*rsa.PrivateKey, error) { + privateKey, err := ssh.ParseRawPrivateKey(sshPrivateKey) + if err != nil { + panic(err) + } + + rsaKey, ok := privateKey.(*rsa.PrivateKey) + + if !ok { + return nil, fmt.Errorf("Only RSA keys are supported right now, got: %s", reflect.TypeOf(privateKey)) + } + + return rsaKey, nil +} + +func SSHPrivateKeyToPGP(sshPrivateKey []byte) (*openpgp.Entity, error) { + key, err := parsePrivateKey(sshPrivateKey) + if err != nil { + return nil, fmt.Errorf("failed to parse private ssh key: %s", err) + } + + // Let's make keys reproducible + timeNull := time.Unix(0, 0) + + gpgKey := &openpgp.Entity{ + PrimaryKey: packet.NewRSAPublicKey(timeNull, &key.PublicKey), + PrivateKey: packet.NewRSAPrivateKey(timeNull, key), + Identities: make(map[string]*openpgp.Identity), + } + uid := packet.NewUserId("root", "", "root@localhost") + isPrimaryID := true + gpgKey.Identities[uid.Id] = &openpgp.Identity{ + Name: uid.Id, + UserId: uid, + SelfSignature: &packet.Signature{ + CreationTime: timeNull, + SigType: packet.SigTypePositiveCert, + PubKeyAlgo: packet.PubKeyAlgoRSA, + Hash: crypto.SHA256, + IsPrimaryId: &isPrimaryID, + FlagsValid: true, + FlagSign: true, + FlagCertify: true, + FlagEncryptStorage: true, + FlagEncryptCommunications: true, + IssuerKeyId: &gpgKey.PrimaryKey.KeyId, + }, + } + + return gpgKey, nil +}