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 0000000..e0ed157
Binary files /dev/null and b/pkgs/sops-install-secrets/test-assets/gnupghome/private-keys-v1.d/289B6199193DE765808658DA7A0B892D914345FC.key differ
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 0000000..903b0c3
Binary files /dev/null and b/pkgs/sops-install-secrets/test-assets/gnupghome/pubring.kbx differ
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 0000000..bbe17a0
Binary files /dev/null and b/pkgs/sops-install-secrets/test-assets/gnupghome/trustdb.gpg differ
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
+}