mirror of
https://github.com/Mic92/sops-nix.git
synced 2024-12-14 11:57:52 +00:00
add tests + ssh key support
This commit is contained in:
parent
bb21643ce1
commit
b75e51c423
29 changed files with 706 additions and 102 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -16,4 +16,5 @@
|
|||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
|
||||
/pkgs/sops-install-secrets/sops-install-secrets
|
||||
/pkgs/sops-install-secrets/sops-install-secrets
|
||||
/pkgs/ssh-to-pgp/ssh-to-pgp
|
10
go.mod
Normal file
10
go.mod
Normal file
|
@ -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
|
||||
)
|
|
@ -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=
|
|
@ -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 <literal>config.sops.sshKeyPaths</literal>.
|
||||
'';
|
||||
};
|
||||
};
|
||||
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}
|
||||
'';
|
||||
};
|
||||
}
|
||||
|
|
|
@ -72,4 +72,6 @@ gpg --export --armor
|
|||
echo 'EOF'
|
||||
|
||||
rm "${GNUPGHOME}/key-template"
|
||||
parent=$(dirname "$FINAL_GNUPGHOME")
|
||||
mkdir -p "$parent"
|
||||
mv "$GNUPGHOME" "$FINAL_GNUPGHOME"
|
||||
|
|
|
@ -5,10 +5,9 @@ buildGoModule {
|
|||
|
||||
hardeningDisable = [ "all" ];
|
||||
|
||||
src = ./.;
|
||||
src = ../..;
|
||||
|
||||
vendorSha256 = "1ky7xzsx12d8m4kvqkayqzybkf3s0w21d6m8qlhvrm00fmyidkxj";
|
||||
shellHook = ''
|
||||
unset GOFLAGS
|
||||
'';
|
||||
subPackages = [ "pkgs/sops-install-secrets" ];
|
||||
|
||||
vendorSha256 = "sha256-O0z+oEffOOZa/bn2gV9onLVbPBHsNDH2yq1CZPi8w58=";
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
47
pkgs/sops-install-secrets/nixos-test.nix
Normal file
47
pkgs/sops-install-secrets/nixos-test.nix
Normal file
|
@ -0,0 +1,47 @@
|
|||
let
|
||||
makeTest = import <nixpkgs/nixos/tests/make-test-python.nix>;
|
||||
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")
|
||||
'';
|
||||
};
|
||||
}
|
4
pkgs/sops-install-secrets/shell.nix
Normal file
4
pkgs/sops-install-secrets/shell.nix
Normal file
|
@ -0,0 +1,4 @@
|
|||
{ pkgs ? import <nixpkgs> {} }:
|
||||
pkgs.mkShell {
|
||||
nativeBuildInputs = with pkgs; [ go delve utillinux gnupg ];
|
||||
}
|
Binary file not shown.
BIN
pkgs/sops-install-secrets/test-assets/gnupghome/pubring.kbx
Normal file
BIN
pkgs/sops-install-secrets/test-assets/gnupghome/pubring.kbx
Normal file
Binary file not shown.
BIN
pkgs/sops-install-secrets/test-assets/gnupghome/trustdb.gpg
Normal file
BIN
pkgs/sops-install-secrets/test-assets/gnupghome/trustdb.gpg
Normal file
Binary file not shown.
43
pkgs/sops-install-secrets/test-assets/secrets.yaml
Normal file
43
pkgs/sops-install-secrets/test-assets/secrets.yaml
Normal file
|
@ -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
|
|
@ -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" "$@"
|
38
pkgs/sops-install-secrets/test-assets/ssh-key
Normal file
38
pkgs/sops-install-secrets/test-assets/ssh-key
Normal file
|
@ -0,0 +1,38 @@
|
|||
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
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
|
||||
-----END OPENSSH PRIVATE KEY-----
|
42
pkgs/sops-install-secrets/test-assets/ssh-key.asc
Normal file
42
pkgs/sops-install-secrets/test-assets/ssh-key.asc
Normal file
|
@ -0,0 +1,42 @@
|
|||
-----BEGIN PGP PRIVATE KEY BLOCK-----
|
||||
|
||||
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
|
||||
=WFkZ
|
||||
-----END PGP PRIVATE KEY BLOCK-----
|
1
pkgs/sops-install-secrets/test-assets/ssh-key.pub
Normal file
1
pkgs/sops-install-secrets/test-assets/ssh-key.pub
Normal file
|
@ -0,0 +1 @@
|
|||
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDtwQhY3uyYF0EtACthbkam7TA672VqWH6epfX/m6KqVTbqozHstCp6CRuBhGxTtkHQNbf38iQkwePPaqXQWMD1ahyKmgUg1LNek1At6X+Q4kOHTKyS8ygbavKy7MhGT8TE3fG/M1PZoz58tw7Coe0tLlQh+wPnIFEhBeR15ZxZAaJWbxYXnvijbXwkxDV/KJ9itElmH3SpkOyhUhZCThWsORNF9ipbZVwiVRwOjoOdHISZUrUvcamhxxt8TM8sKWzao2GXnQTiMvf8xMfyAtHLGBdPpiVcSym2FvjPqRSmKoVFDFwsNQMfcDv1eSH1/c5art14KyKwuJpkiJUPWwfdppdjHbBV26moE+4YvMi04Z7tw2RMi0x8tXaiYxwhNvyDVqBdSD56AjckTmHo5dZCjbcsV00gzDiVZlWgP8RCzr55BJlMVjKLM6YkzVZl+c5kJorsfL3NyohxC/hrIwItQoqJW9XtA/rSd9/1AdkrE+iPahsCQQ1ZhH0KiUr00Pk= joerg@turingmachine
|
|
@ -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
|
112
pkgs/ssh-to-pgp/main.go
Normal file
112
pkgs/ssh-to-pgp/main.go
Normal file
|
@ -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)
|
||||
}
|
||||
}
|
47
pkgs/ssh-to-pgp/main_test.go
Normal file
47
pkgs/ssh-to-pgp/main_test.go
Normal file
|
@ -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())
|
||||
}
|
||||
}
|
10
pkgs/ssh-to-pgp/shell.nix
Normal file
10
pkgs/ssh-to-pgp/shell.nix
Normal file
|
@ -0,0 +1,10 @@
|
|||
with import <nixpkgs> {};
|
||||
mkShell {
|
||||
nativeBuildInputs = [
|
||||
bashInteractive
|
||||
go
|
||||
delve
|
||||
gnupg
|
||||
];
|
||||
hardeningDisable = [ "all" ];
|
||||
}
|
38
pkgs/ssh-to-pgp/test-assets/id_rsa
Normal file
38
pkgs/ssh-to-pgp/test-assets/id_rsa
Normal file
|
@ -0,0 +1,38 @@
|
|||
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
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
|
||||
-----END OPENSSH PRIVATE KEY-----
|
1
pkgs/ssh-to-pgp/test-assets/id_rsa.pub
Normal file
1
pkgs/ssh-to-pgp/test-assets/id_rsa.pub
Normal file
|
@ -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
|
94
pkgs/sshkeys/convert.go
Normal file
94
pkgs/sshkeys/convert.go
Normal file
|
@ -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
|
||||
}
|
Loading…
Reference in a new issue