mirror of
https://github.com/Mic92/sops-nix.git
synced 2025-03-13 20:29:17 +00:00
Add support for ssh-generated age keys
This commit is contained in:
parent
b21c0ce3a8
commit
db8fcb50a3
13 changed files with 488 additions and 43 deletions
|
@ -1,5 +1,5 @@
|
|||
{ pkgs ? import <nixpkgs> {} }: let
|
||||
vendorSha256 = "sha256-YFy0eIIwrOvAiA+CJNVqY1AgswnPOzxq+GsA82XrT3M=";
|
||||
vendorSha256 = "sha256:1bizqlj56lka37gbvm37p3yifn7w2z9kfhv486gv40wknzqclq12";
|
||||
|
||||
sops-install-secrets = pkgs.callPackage ./pkgs/sops-install-secrets {
|
||||
inherit vendorSha256;
|
||||
|
|
1
go.mod
1
go.mod
|
@ -3,6 +3,7 @@ module github.com/Mic92/sops-nix
|
|||
go 1.14
|
||||
|
||||
require (
|
||||
filippo.io/age v1.0.0-rc.3
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20210707164159-52430bf6b52c
|
||||
github.com/mozilla-services/yaml v0.0.0-20191106225358-5c216288813c
|
||||
go.mozilla.org/sops/v3 v3.7.1
|
||||
|
|
6
go.sum
6
go.sum
|
@ -4,9 +4,11 @@ cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSR
|
|||
cloud.google.com/go v0.43.0 h1:banaiRPAM8kUVYneOSkhgcDsLzEvL25FinuiSZaH/2w=
|
||||
cloud.google.com/go v0.43.0/go.mod h1:BOSR3VbTLkk6FDC/TcffxP4NF/FFBGA5ku+jvKOP7pg=
|
||||
contrib.go.opencensus.io/exporter/ocagent v0.4.12/go.mod h1:450APlNTSR6FrvC3CTRqYosuDstRB9un7SOx2k/9ckA=
|
||||
filippo.io/age v1.0.0-beta7 h1:RZiSK+N3KL2UwT82xiCavjYw8jJHzWMEUYePAukTpk0=
|
||||
filippo.io/age v1.0.0-beta7/go.mod h1:chAuTrTb0FTTmKtvs6fQTGhYTvH9AigjN1uEUsvLdZ0=
|
||||
filippo.io/age v1.0.0-rc.3 h1:8JjuJ5ffGKDmC4SS0zoyQxZROZX75so768b7AjulKLw=
|
||||
filippo.io/age v1.0.0-rc.3/go.mod h1:UjINLBMeA60aGZkHCGsmDzKcaXoTTzpvrqQM+Vo3YHU=
|
||||
filippo.io/edwards25519 v1.0.0-alpha.2/go.mod h1:X+pm78QAUPtFLi1z9PYIlS/bdDnvbCOGKtZ+ACWEf7o=
|
||||
filippo.io/edwards25519 v1.0.0-beta.3/go.mod h1:X+pm78QAUPtFLi1z9PYIlS/bdDnvbCOGKtZ+ACWEf7o=
|
||||
github.com/Azure/azure-sdk-for-go v31.2.0+incompatible h1:kZFnTLmdQYNGfakatSivKHUfUnDZhqNdchHD4oIhp5k=
|
||||
github.com/Azure/azure-sdk-for-go v31.2.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8=
|
||||
|
@ -242,7 +244,6 @@ github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6So
|
|||
github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||
github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk=
|
||||
github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
|
||||
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
|
||||
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
|
||||
|
@ -273,6 +274,7 @@ golang.org/x/crypto v0.0.0-20190418165655-df01cb2cc480/go.mod h1:WFFai1msRO1wXaE
|
|||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 h1:It14KIkyBFYkHkwZ7k45minvA9aorojkyjGk9KJ5B/w=
|
||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
|
|
|
@ -88,6 +88,7 @@ let
|
|||
gnupgHome = cfg.gnupg.home;
|
||||
sshKeyPaths = cfg.gnupg.sshKeyPaths;
|
||||
ageKeyFile = cfg.age.keyFile;
|
||||
ageSshKeyPaths = cfg.age.sshKeyPaths;
|
||||
});
|
||||
|
||||
checkedManifest = let
|
||||
|
@ -152,6 +153,17 @@ in {
|
|||
present at the specified location.
|
||||
'';
|
||||
};
|
||||
|
||||
sshKeyPaths = mkOption {
|
||||
type = types.listOf types.path;
|
||||
default = []; # If we set this like the gnupg option, we would use age by default
|
||||
description = ''
|
||||
Path to ssh keys added as age keys during sops description.
|
||||
This option must be explicitly unset if <literal>config.sops.age.keyFile</literal> is set.
|
||||
|
||||
Setting this to a non-empty list causes age to be used instead of gnupg.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
gnupg = {
|
||||
|
@ -182,10 +194,10 @@ in {
|
|||
];
|
||||
config = mkIf (cfg.secrets != {}) {
|
||||
assertions = [{
|
||||
assertion = cfg.age.keyFile == null -> (cfg.gnupg.home == null) != (cfg.gnupg.sshKeyPaths == []);
|
||||
assertion = (cfg.age.keyFile == null && cfg.age.sshKeyPaths == []) -> (cfg.gnupg.home == null) != (cfg.gnupg.sshKeyPaths == []);
|
||||
message = "Exactly one of sops.gnupg.home and sops.gnupg.sshKeyPaths must be set for gnupg mode";
|
||||
} {
|
||||
assertion = cfg.age.keyFile != null -> (cfg.age.sshKeyPaths != []) != (cfg.age.keyFile != null);
|
||||
assertion = (cfg.age.keyFile != null || cfg.age.sshKeyPaths != []) -> (cfg.age.sshKeyPaths != []) != (cfg.age.keyFile != null);
|
||||
message = "sops.age.keyFile is mutually exclusive with sops.age.sshKeyPaths";
|
||||
}] ++ optionals cfg.validateSopsFiles (
|
||||
concatLists (mapAttrsToList (name: secret: [{
|
||||
|
|
179
pkgs/bech32/bech32.go
Normal file
179
pkgs/bech32/bech32.go
Normal file
|
@ -0,0 +1,179 @@
|
|||
// Copyright (c) 2017 Takatoshi Nakagawa
|
||||
// Copyright (c) 2019 Google LLC
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
// Package bech32 is a modified version of the reference implementation of BIP173.
|
||||
package bech32
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var charset = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
|
||||
|
||||
var generator = []uint32{0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3}
|
||||
|
||||
func polymod(values []byte) uint32 {
|
||||
chk := uint32(1)
|
||||
for _, v := range values {
|
||||
top := chk >> 25
|
||||
chk = (chk & 0x1ffffff) << 5
|
||||
chk = chk ^ uint32(v)
|
||||
for i := 0; i < 5; i++ {
|
||||
bit := top >> i & 1
|
||||
if bit == 1 {
|
||||
chk ^= generator[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
return chk
|
||||
}
|
||||
|
||||
func hrpExpand(hrp string) []byte {
|
||||
h := []byte(strings.ToLower(hrp))
|
||||
var ret []byte
|
||||
for _, c := range h {
|
||||
ret = append(ret, c>>5)
|
||||
}
|
||||
ret = append(ret, 0)
|
||||
for _, c := range h {
|
||||
ret = append(ret, c&31)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func verifyChecksum(hrp string, data []byte) bool {
|
||||
return polymod(append(hrpExpand(hrp), data...)) == 1
|
||||
}
|
||||
|
||||
func createChecksum(hrp string, data []byte) []byte {
|
||||
values := append(hrpExpand(hrp), data...)
|
||||
values = append(values, []byte{0, 0, 0, 0, 0, 0}...)
|
||||
mod := polymod(values) ^ 1
|
||||
ret := make([]byte, 6)
|
||||
for p := range ret {
|
||||
shift := 5 * (5 - p)
|
||||
ret[p] = byte(mod>>shift) & 31
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func convertBits(data []byte, frombits, tobits byte, pad bool) ([]byte, error) {
|
||||
var ret []byte
|
||||
acc := uint32(0)
|
||||
bits := byte(0)
|
||||
maxv := byte(1<<tobits - 1)
|
||||
for idx, value := range data {
|
||||
if value>>frombits != 0 {
|
||||
return nil, fmt.Errorf("invalid data range: data[%d]=%d (frombits=%d)", idx, value, frombits)
|
||||
}
|
||||
acc = acc<<frombits | uint32(value)
|
||||
bits += frombits
|
||||
for bits >= tobits {
|
||||
bits -= tobits
|
||||
ret = append(ret, byte(acc>>bits)&maxv)
|
||||
}
|
||||
}
|
||||
if pad {
|
||||
if bits > 0 {
|
||||
ret = append(ret, byte(acc<<(tobits-bits))&maxv)
|
||||
}
|
||||
} else if bits >= frombits {
|
||||
return nil, fmt.Errorf("illegal zero padding")
|
||||
} else if byte(acc<<(tobits-bits))&maxv != 0 {
|
||||
return nil, fmt.Errorf("non-zero padding")
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// Encode encodes the HRP and a bytes slice to Bech32. If the HRP is uppercase,
|
||||
// the output will be uppercase.
|
||||
func Encode(hrp string, data []byte) (string, error) {
|
||||
values, err := convertBits(data, 8, 5, true)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(hrp)+len(values)+7 > 90 {
|
||||
return "", fmt.Errorf("too long: hrp length=%d, data length=%d", len(hrp), len(values))
|
||||
}
|
||||
if len(hrp) < 1 {
|
||||
return "", fmt.Errorf("invalid HRP: %q", hrp)
|
||||
}
|
||||
for p, c := range hrp {
|
||||
if c < 33 || c > 126 {
|
||||
return "", fmt.Errorf("invalid HRP character: hrp[%d]=%d", p, c)
|
||||
}
|
||||
}
|
||||
if strings.ToUpper(hrp) != hrp && strings.ToLower(hrp) != hrp {
|
||||
return "", fmt.Errorf("mixed case HRP: %q", hrp)
|
||||
}
|
||||
lower := strings.ToLower(hrp) == hrp
|
||||
hrp = strings.ToLower(hrp)
|
||||
var ret strings.Builder
|
||||
ret.WriteString(hrp)
|
||||
ret.WriteString("1")
|
||||
for _, p := range values {
|
||||
ret.WriteByte(charset[p])
|
||||
}
|
||||
for _, p := range createChecksum(hrp, values) {
|
||||
ret.WriteByte(charset[p])
|
||||
}
|
||||
if lower {
|
||||
return ret.String(), nil
|
||||
}
|
||||
return strings.ToUpper(ret.String()), nil
|
||||
}
|
||||
|
||||
// Decode decodes a Bech32 string. If the string is uppercase, the HRP will be uppercase.
|
||||
func Decode(s string) (hrp string, data []byte, err error) {
|
||||
if len(s) > 90 {
|
||||
return "", nil, fmt.Errorf("too long: len=%d", len(s))
|
||||
}
|
||||
if strings.ToLower(s) != s && strings.ToUpper(s) != s {
|
||||
return "", nil, fmt.Errorf("mixed case")
|
||||
}
|
||||
pos := strings.LastIndex(s, "1")
|
||||
if pos < 1 || pos+7 > len(s) {
|
||||
return "", nil, fmt.Errorf("separator '1' at invalid position: pos=%d, len=%d", pos, len(s))
|
||||
}
|
||||
hrp = s[:pos]
|
||||
for p, c := range hrp {
|
||||
if c < 33 || c > 126 {
|
||||
return "", nil, fmt.Errorf("invalid character human-readable part: s[%d]=%d", p, c)
|
||||
}
|
||||
}
|
||||
s = strings.ToLower(s)
|
||||
for p, c := range s[pos+1:] {
|
||||
d := strings.IndexRune(charset, c)
|
||||
if d == -1 {
|
||||
return "", nil, fmt.Errorf("invalid character data part: s[%d]=%v", p, c)
|
||||
}
|
||||
data = append(data, byte(d))
|
||||
}
|
||||
if !verifyChecksum(hrp, data) {
|
||||
return "", nil, fmt.Errorf("invalid checksum")
|
||||
}
|
||||
data, err = convertBits(data[:len(data)-6], 5, 8, false)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
return hrp, data, nil
|
||||
}
|
94
pkgs/bech32/bech32_test.go
Normal file
94
pkgs/bech32/bech32_test.go
Normal file
|
@ -0,0 +1,94 @@
|
|||
// Copyright (c) 2013-2017 The btcsuite developers
|
||||
// Copyright (c) 2016-2017 The Lightning Network Developers
|
||||
// Copyright (c) 2019 Google LLC
|
||||
//
|
||||
// Permission to use, copy, modify, and distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
package bech32_test
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"filippo.io/age/internal/bech32"
|
||||
)
|
||||
|
||||
func TestBech32(t *testing.T) {
|
||||
tests := []struct {
|
||||
str string
|
||||
valid bool
|
||||
}{
|
||||
{"A12UEL5L", true},
|
||||
{"a12uel5l", true},
|
||||
{"an83characterlonghumanreadablepartthatcontainsthenumber1andtheexcludedcharactersbio1tt5tgs", true},
|
||||
{"abcdef1qpzry9x8gf2tvdw0s3jn54khce6mua7lmqqqxw", true},
|
||||
{"11qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqc8247j", true},
|
||||
{"split1checkupstagehandshakeupstreamerranterredcaperred2y9e3w", true},
|
||||
|
||||
// invalid checksum
|
||||
{"split1checkupstagehandshakeupstreamerranterredcaperred2y9e2w", false},
|
||||
// invalid character (space) in hrp
|
||||
{"s lit1checkupstagehandshakeupstreamerranterredcaperredp8hs2p", false},
|
||||
{"split1cheo2y9e2w", false}, // invalid character (o) in data part
|
||||
{"split1a2y9w", false}, // too short data part
|
||||
{"1checkupstagehandshakeupstreamerranterredcaperred2y9e3w", false}, // empty hrp
|
||||
// invalid character (DEL) in hrp
|
||||
{"spl" + string(rune(127)) + "t1checkupstagehandshakeupstreamerranterredcaperred2y9e3w", false},
|
||||
// too long
|
||||
{"11qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqsqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqc8247j", false},
|
||||
|
||||
// BIP 173 invalid vectors.
|
||||
{"an84characterslonghumanreadablepartthatcontainsthenumber1andtheexcludedcharactersbio1569pvx", false},
|
||||
{"pzry9x0s0muk", false},
|
||||
{"1pzry9x0s0muk", false},
|
||||
{"x1b4n0q5v", false},
|
||||
{"li1dgmt3", false},
|
||||
{"de1lg7wt\xff", false},
|
||||
{"A1G7SGD8", false},
|
||||
{"10a06t8", false},
|
||||
{"1qzzfhee", false},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
str := test.str
|
||||
hrp, decoded, err := bech32.Decode(str)
|
||||
if !test.valid {
|
||||
// Invalid string decoding should result in error.
|
||||
if err == nil {
|
||||
t.Errorf("expected decoding to fail for invalid string %v", test.str)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Valid string decoding should result in no error.
|
||||
if err != nil {
|
||||
t.Errorf("expected string to be valid bech32: %v", err)
|
||||
}
|
||||
|
||||
// Check that it encodes to the same string.
|
||||
encoded, err := bech32.Encode(hrp, decoded)
|
||||
if err != nil {
|
||||
t.Errorf("encoding failed: %v", err)
|
||||
}
|
||||
if encoded != str {
|
||||
t.Errorf("expected data to encode to %v, but got %v", str, encoded)
|
||||
}
|
||||
|
||||
// Flip a bit in the string an make sure it is caught.
|
||||
pos := strings.LastIndexAny(str, "1")
|
||||
flipped := str[:pos+1] + string((str[pos+1] ^ 1)) + str[pos+2:]
|
||||
if _, _, err = bech32.Decode(flipped); err == nil {
|
||||
t.Error("expected decoding to fail")
|
||||
}
|
||||
}
|
||||
}
|
45
pkgs/sops-install-secrets/agessh/convert.go
Normal file
45
pkgs/sops-install-secrets/agessh/convert.go
Normal file
|
@ -0,0 +1,45 @@
|
|||
package agessh
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"crypto/sha512"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/Mic92/sops-nix/pkgs/bech32"
|
||||
"golang.org/x/crypto/curve25519"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
func ed25519PrivateKeyToCurve25519(pk ed25519.PrivateKey) ([]byte, error) {
|
||||
h := sha512.New()
|
||||
_, err := h.Write(pk.Seed())
|
||||
if err != nil {
|
||||
return []byte{}, err
|
||||
}
|
||||
out := h.Sum(nil)
|
||||
return out[:curve25519.ScalarSize], nil
|
||||
}
|
||||
|
||||
func SSHPrivateKeyToBech32(sshPrivateKey []byte) (string, error) {
|
||||
privateKey, err := ssh.ParseRawPrivateKey(sshPrivateKey)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
ed25519Key, ok := privateKey.(*ed25519.PrivateKey)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("Only ED25519 keys are supported, got: %s", reflect.TypeOf(privateKey))
|
||||
}
|
||||
bytes, err := ed25519PrivateKeyToCurve25519(*ed25519Key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
s, err := bech32.Encode("AGE-SECRET-KEY-", bytes)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return strings.ToUpper(s), nil
|
||||
}
|
|
@ -15,7 +15,9 @@ import (
|
|||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/Mic92/sops-nix/pkgs/sops-install-secrets/agessh"
|
||||
"github.com/Mic92/sops-nix/pkgs/sops-install-secrets/sshkeys"
|
||||
|
||||
"github.com/mozilla-services/yaml"
|
||||
|
@ -47,6 +49,7 @@ type manifest struct {
|
|||
SSHKeyPaths []string `json:"sshKeyPaths"`
|
||||
GnupgHome string `json:"gnupgHome"`
|
||||
AgeKeyFile string `json:"ageKeyFile"`
|
||||
AgeSshKeyPaths []string `json:"ageSshKeyPaths"`
|
||||
}
|
||||
|
||||
type secretFile struct {
|
||||
|
@ -516,6 +519,35 @@ func importSSHKeys(keyPaths []string, gpgHome string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func importAgeSSHKeys(keyPaths []string, ageFilePath string) error {
|
||||
ageFile, err := os.OpenFile(ageFilePath, os.O_WRONLY|os.O_CREATE, 0600)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Cannot create '%s': %w", ageFilePath, err)
|
||||
}
|
||||
defer ageFile.Close()
|
||||
fmt.Fprintf(ageFile, "# generated by sops-nix at %s\n", time.Now().Format(time.RFC3339))
|
||||
|
||||
for _, p := range keyPaths {
|
||||
// Read the key
|
||||
sshKey, err := ioutil.ReadFile(p)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Cannot read ssh key '%s': %w", p, err)
|
||||
}
|
||||
// Convert the key to bech32
|
||||
bech32, err := agessh.SSHPrivateKeyToBech32(sshKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Cannot convert ssh key '%s': %w", p, err)
|
||||
}
|
||||
// Append it to the file
|
||||
_, err = ageFile.WriteString(bech32 + "\n")
|
||||
if err != nil {
|
||||
return fmt.Errorf("Cannot write key to age file: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type keyring struct {
|
||||
path string
|
||||
}
|
||||
|
@ -611,8 +643,18 @@ func installSecrets(args []string) error {
|
|||
defer keyring.Remove()
|
||||
} else if manifest.GnupgHome != "" {
|
||||
os.Setenv("GNUPGHOME", manifest.GnupgHome)
|
||||
} else if manifest.AgeKeyFile != "" {
|
||||
os.Setenv("SOPS_AGE_KEY_FILE", manifest.AgeKeyFile)
|
||||
} else if manifest.AgeKeyFile != "" || len(manifest.AgeSshKeyPaths) != 0 {
|
||||
if len(manifest.AgeSshKeyPaths) == 0 {
|
||||
os.Setenv("SOPS_AGE_KEY_FILE", manifest.AgeKeyFile)
|
||||
} else {
|
||||
keyfile := filepath.Join(manifest.SecretsMountPoint, "age-keys.txt")
|
||||
err = importAgeSSHKeys(manifest.AgeSshKeyPaths, keyfile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("Wrote keys to %s\n", keyfile)
|
||||
os.Setenv("SOPS_AGE_KEY_FILE", keyfile)
|
||||
}
|
||||
}
|
||||
|
||||
if err := decryptSecrets(manifest.Secrets); err != nil {
|
||||
|
|
|
@ -219,7 +219,7 @@ func testSSHKey(t *testing.T) {
|
|||
testInstallSecret(t, testdir, &m)
|
||||
}
|
||||
|
||||
func testAge(t *testing.T) {
|
||||
func TestAge(t *testing.T) {
|
||||
assets := testAssetPath()
|
||||
|
||||
testdir := newTestDir(t)
|
||||
|
@ -252,11 +252,43 @@ func testAge(t *testing.T) {
|
|||
testInstallSecret(t, testdir, &m)
|
||||
}
|
||||
|
||||
func TestAgeWithSSH(t *testing.T) {
|
||||
assets := testAssetPath()
|
||||
|
||||
testdir := newTestDir(t)
|
||||
defer testdir.Remove()
|
||||
|
||||
target := path.Join(testdir.path, "existing-target")
|
||||
file, err := os.Create(target)
|
||||
ok(t, err)
|
||||
file.Close()
|
||||
|
||||
s := secret{
|
||||
Name: "test",
|
||||
Key: "test_key",
|
||||
Owner: "nobody",
|
||||
Group: "nogroup",
|
||||
SopsFile: path.Join(assets, "secrets.yaml"),
|
||||
Path: target,
|
||||
Mode: "0400",
|
||||
RestartServices: []string{"affected-service"},
|
||||
ReloadServices: make([]string, 0),
|
||||
}
|
||||
|
||||
m := manifest{
|
||||
Secrets: []secret{s},
|
||||
SecretsMountPoint: testdir.secretsPath,
|
||||
SymlinkPath: testdir.symlinkPath,
|
||||
AgeSshKeyPaths: []string{path.Join(assets, "ssh-ed25519-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)
|
||||
testAge(t)
|
||||
}
|
||||
|
||||
func TestValidateManifest(t *testing.T) {
|
||||
|
|
|
@ -38,10 +38,30 @@
|
|||
start_all()
|
||||
machine.succeed("cat /run/secrets/test_key | grep -q test_value")
|
||||
'';
|
||||
} {
|
||||
inherit pkgs;
|
||||
inherit (pkgs) system;
|
||||
};
|
||||
} {
|
||||
inherit pkgs;
|
||||
inherit (pkgs) system;
|
||||
};
|
||||
|
||||
age-ssh-keys = makeTest {
|
||||
name = "sops-age-ssh-keys";
|
||||
machine = {
|
||||
imports = [ ../../modules/sops ];
|
||||
sops = {
|
||||
age.sshKeyPaths = [ ./test-assets/ssh-ed25519-key ];
|
||||
defaultSopsFile = ./test-assets/secrets.yaml;
|
||||
secrets.test_key = {};
|
||||
};
|
||||
};
|
||||
|
||||
testScript = ''
|
||||
start_all()
|
||||
machine.succeed("cat /run/secrets/test_key | grep -q test_value")
|
||||
'';
|
||||
} {
|
||||
inherit pkgs;
|
||||
inherit (pkgs) system;
|
||||
};
|
||||
|
||||
pgp-keys = makeTest {
|
||||
name = "sops-pgp-keys";
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
test_key: ENC[AES256_GCM,data:6aaSGYcvIY1+lQ==,iv:voX4IQemcgt0O97oLExy5r2V85nn687cIyWmHNDhUag=,tag:/FSMgXuX8TnbxRxpcuwGEA==,type:str]
|
||||
test_key: ENC[AES256_GCM,data:2mP+IAdczoEr0g==,iv:voX4IQemcgt0O97oLExy5r2V85nn687cIyWmHNDhUag=,tag:R97qy4fKneU7D9UFhXNvgA==,type:str]
|
||||
a_list:
|
||||
- ENC[AES256_GCM,data:IBI=,iv:5P+1UQyIYOW8xXgsvTXC17msGcA6IGB3N8n+pstfqjo=,tag:7A4l/SgzgxK9sqi+/15A6w==,type:str]
|
||||
- ENC[AES256_GCM,data:tLE=,iv:LbGS8DjM6Vnr2nU7QokzQlg0gL+XMWhqbN+ypP7ZIZo=,tag:cmhMaddcY2bhydWrPDWNlQ==,type:str]
|
||||
- ENC[AES256_GCM,data:oOQ=,iv:5P+1UQyIYOW8xXgsvTXC17msGcA6IGB3N8n+pstfqjo=,tag:ox4rgjbb8c0vYZ2XmwRgpg==,type:str]
|
||||
- ENC[AES256_GCM,data:mYU=,iv:LbGS8DjM6Vnr2nU7QokzQlg0gL+XMWhqbN+ypP7ZIZo=,tag:CFrhnZv6lYGJOVso+2YBFg==,type:str]
|
||||
sops:
|
||||
kms: []
|
||||
gcp_kms: []
|
||||
|
@ -11,45 +11,55 @@ sops:
|
|||
- recipient: age1yt3tfqlfrwdwx0z0ynwplcr6qxcxfaqycuprpmy89nr83ltx74tqdpszlw
|
||||
enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBuY2RtdnhrMEtOeEUvRmFr
|
||||
SU5uRm1xaU12R3F5TnJvbzI5YlNYMDFycGxvCnRBQVNwY2cxMDVicUNZWlpFdStp
|
||||
ZTYxcUJkNEFHVkxTbGdrdkIxN3ZMNkUKLS0tIHd2K2ZhaFVoTmlhNDBKeXZyVHBh
|
||||
ZXFnRy9hUTFNQm9rWVA3YnJXaWV0S2cKBpGxzhth36fab8KDKFBoweQO9L0juys4
|
||||
cMjz2X/hXVMLvDeCLVBTZTj3K/lXAo4v2qMUGZsR2Idpw3FOPxfSGg==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBOcUlPTGtUY2R1Si9SUnpN
|
||||
dGVMVzMzTXd2Z09UeGdxQlNqeVg3WTFadWtNCkpzaVJqMkZ0b1JUamJsbysrdGll
|
||||
QVlLNU9xMnZqandMdGxKNlZ2amJFbncKLS0tIFlrbGtyMkZKNUthd0Z5VW5MVjBN
|
||||
bWFhWGlJaXMzWUJoZGpnMjNoSnlMYTgK2hM/Cc6xN1xkluL69jDaaoaEijAJk+l8
|
||||
TwhUG7Qlggod2xCWTC4cpjb+THip2u31tFoSPQZKEG8gDcGNIz2HOw==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
lastmodified: "2021-08-26T22:38:49Z"
|
||||
mac: ENC[AES256_GCM,data:1TNMF0HIfIOetCF4F268N5k2DFQ28JBYOdjPxfOuw03udJ1eKcTZrlBAGGFEgMdu1FuW+ZC+gLHW/b0GpfZWAtkiLuWP9SUof01rrFPYse6LvqWvlY1mLK6unbMveGeD9My3QTkZmt962I9tEHQW+ph35j80egGnN1f6stiJdlY=,iv:KEa/Q6q+B0F2Dv+/Km+2WeztYij0Of1pCSygGXM2fG4=,tag:TzuiTyYwTor36eGPKvTcwQ==,type:str]
|
||||
- recipient: age1a8pk4akrdamj7nvqy3zywgtny8dxz7t5xzu7u8v9mhrayp9freqsqatyrs
|
||||
enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBCS2NvekFqa2dVeXUzRzZY
|
||||
emR6NDJqMnptOHpQUDBjTjdaMGp4SGZ1bTJVCk16Tnd3ZFl0dFUzQ1RNRzYyclV3
|
||||
d24xeDZMaXFvbEhZWXRObTM4RWRQMk0KLS0tIGs4bjFweEFjblFEalBOLzMyYyt6
|
||||
WHJmNkhFVStxRG5PZTZUWnRFTmtzemsKLXKJN3GSJKDI4MYPxDU5HbTzoSAt0jK9
|
||||
T9sJbd++By2OC9rl+GJoJcy4aM0uTYy83EDfqBV02Y1CfepRjHLRWQ==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
lastmodified: "2021-08-27T17:58:39Z"
|
||||
mac: ENC[AES256_GCM,data:V9QGBTrofAza2LK1hA5cQmuT37BsfRJZZtSmvc5CDnIeWYTLUOkCFRzY+wk3uZNj0aM3BUAjvIM6LPNJ+rR8p0vXwKN37UH/oRmGADuLcu5Ec7hmArCGjKMo0gyaarlvuGmFhjb1gcW2PAbo84lDykxbTyf7Pp7APJhIvGbwBu4=,iv:ufG8sG5NAtVO25kZXrWnSQ/kkbDlwmOWhO4T9UpRvOg=,tag:YidkKTBqH48N7bjBabrAgQ==,type:str]
|
||||
pgp:
|
||||
- created_at: "2020-07-12T08:03:51Z"
|
||||
enc: |
|
||||
-----BEGIN PGP MESSAGE-----
|
||||
|
||||
hQEMA/m6nevQP1fAAQf/Zih3JJl/L/ApqkwdhIv7iUQsbebXV+WRwlslwOz6by/t
|
||||
Sf6CH3b5FHLaBQtPEUh6QNLpQSqm52Rs4MoJxML0Tan8eu6Q313lS09XP5mQP1yc
|
||||
d1FN4Tsg8V/xaGMGAtJHM/bj+6vfimGtnnQvNZ7N7PW2U+fxpq51NmqIXMmG+jOw
|
||||
2Tw+04oNoK6lrG89y0dzJrTvP9ph0wM+hwOgBthfhr0X6/UF7vS9fgEod+HFEqGn
|
||||
MfAW4CiijXDvFYI5unwseeUE8IosOaq2VTW57eBSteLWIyJqmLUxvVSoMP0emitN
|
||||
4T0DkzTxywq5KPUMIEISEZrxYEskatqlTdQBmcjEAdJcAQl/u1OGCn7YRIJbXjMP
|
||||
8/UvlFVfaT3L19sgEg8rSVpo3GiwlwbP0KTcb4jVMu0mHhd70VciNKojtDVlZg9n
|
||||
0V8Z2LwPHY0+IzcTJu5IS7sO/fuyYJohWQ4ZV7Y=
|
||||
=1olg
|
||||
hQEMA/m6nevQP1fAAQgApvcEy9FBr6kag0PkFBabiEhqtKG6CcN/ewbxfDGXbOPI
|
||||
hyndS7Poc6a7VeYo6cDQwxNqbUbjjn6BRZBFGHxuVInjvtDVm2phh/HOd25IH68s
|
||||
RGh93wyW637rqJGp8+X3of7b+XBxq0fg0hLqKxR8iMaVF3WnyAMfS/r1tAOuHRGF
|
||||
geMSQftnWkv1OIl2DPDcv02lqHSKqVZpidzxEdeAqAH/Ml9SoTOEyC8uNz0LIdvP
|
||||
SQUp5JFp5CEyXaAzeTiypodIjCKOmCNTLuR9VC8O5+P+E62xVmxoFVVfozg2ZBdk
|
||||
CJrEGR5jxTxAI1IB+ywWOde+cVzQtPXds1d3at2uFtJeAZuS3VYfvL1f4rXNrSBV
|
||||
3x7+rDknN8PsFAmmnLdxtbPJAij9eERpoAOsJOy6Ka4OSvOj4sCCU09wb2i/PugU
|
||||
a7y4M55KzV/8J5aQ4iMVym/9Gkb98XK2Ff5na1jQGQ==
|
||||
=MU7I
|
||||
-----END PGP MESSAGE-----
|
||||
fp: 7FB89715AADA920D65D25E63F9BA9DEBD03F57C0
|
||||
- created_at: "2020-07-12T08:03:51Z"
|
||||
enc: |
|
||||
-----BEGIN PGP MESSAGE-----
|
||||
|
||||
hQGMA3ulPRkZxd/UAQv+LSFG7a6XqqW7GYdYLb1H0bv3NkxJeBYGohsMrEC4AXnA
|
||||
RdhJ2B55WqiNyDmVmylalUKAPFpD7RJ4c6HDZcRLx6doy5wgA+oFvKCe8FcYy5YS
|
||||
abSUhNoT0TwWlDQ4Bc4Q/QSu9kMCica56Bc10tz5Pvb5JEYApoLfL0kTO7mBI+w4
|
||||
hKovAxlp9aoT9cky/7WFhmFn1+mYz//ejDKPuyVSYhM/2eIosDE9maPootLQMrIX
|
||||
cqnsCDCOgMDwIOXt5/W4Cab799Pop4SQcFDSDa4FU4kN12vmNwJu7HPxpR+W80bL
|
||||
GOUu35gVyykig9XCaTA/UCc85URtOMYmXPBsdTVoMVAgfxkji3ovJCEFBcI7wYgd
|
||||
XudLQw5tJ9Pwz+5EeNw771ISaTBO7bWazSq+q3HrKW9kKLFJV4kVt7zLr+yZ1kNu
|
||||
jEjTFmaKYl3p9AYh6yxb6n9PuTosk1ZJE8gv8ME+b4z21BVxfZsDfYWVhT9thZz5
|
||||
q2LHw+E/fmf0EXz9tviS0k4BimrDWSnYNyuDqTImB+v7baY0rEoYhQY2ZLAbUsew
|
||||
wg+KC2RjkapaZ7CFbCMs7xaWzZ5p/nfWOtsb1GvmvCUZcHK5MYeVM5S+Xf0GnYg=
|
||||
=7cg6
|
||||
hQGMA3ulPRkZxd/UAQwAidXXZa5HVHCuI9pULCMVfX25pjYk3CpGdo1jLt20teRu
|
||||
QVe5Ner7Z3QF8BMk4YRDDaJWlWLbHQE4KYM5/ER/iJyrSIp9wcIx7bQvoCO44KLh
|
||||
5wXbmxRnscUaW67+qdnjZBFSIHxtaeRYSBGCk3CGODnVamvGXdv733eG/O2IBHqt
|
||||
sIE3+cOk6N+gQxYcz5IxJlRJlF6NagD4RxdMzjx6QJ43pZp8tKupDFZ1Teh1c4mY
|
||||
8XtVekaWz9ToKiQD3uQoCIwSW/YszuviYf/ar4Bi7j2xTH9vzSxxoRsSjo0JXKyB
|
||||
EDj2Y1M5KzZAb3OWNINmNt2jqwKF8HS06TrbP6bdmRgHWRnwJLaSHSpxiclT/YpC
|
||||
En4/ZvjqJdxyJc0nmEyDpEgelpTzm19jzFvsEvj43GnnWjh6/aAb0TF2Ms1E7I5E
|
||||
VpJFI7l/I1JDacdDlvx1jFMhsya9n356GhZaiJky89hURsHhH5ek8E3f0PpC20dp
|
||||
J7o8e7N0zXV39iIw6kdT0lABPe8KVRzQOsIKqNGaVwZVQuX4i/C1vbz6yTb+cHc6
|
||||
yCYEoi674QYg7ofZV4VkY318XOQz7P5sVlASjADvKF9SzjENadp8Y0SHvuYkXU4W
|
||||
yQ==
|
||||
=DqFU
|
||||
-----END PGP MESSAGE-----
|
||||
fp: 2504791468B153B8A3963CC97BA53D1919C5DFD4
|
||||
unencrypted_suffix: _unencrypted
|
||||
|
|
7
pkgs/sops-install-secrets/test-assets/ssh-ed25519-key
Normal file
7
pkgs/sops-install-secrets/test-assets/ssh-ed25519-key
Normal file
|
@ -0,0 +1,7 @@
|
|||
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
|
||||
QyNTUxOQAAACASNfkPp7cwDXm7Z4nAogGNvbqixljmjhixGvG1KjlZkgAAAJjHY9ZUx2PW
|
||||
VAAAAAtzc2gtZWQyNTUxOQAAACASNfkPp7cwDXm7Z4nAogGNvbqixljmjhixGvG1KjlZkg
|
||||
AAAEC5eNs176OO7IO8ap33TVXlOxhhQYcYtv3VW+/5Ft8UohI1+Q+ntzANebtnicCiAY29
|
||||
uqLGWOaOGLEa8bUqOVmSAAAAEHNhcmF0QHNhcmF0LWRlbGwBAgMEBQ==
|
||||
-----END OPENSSH PRIVATE KEY-----
|
|
@ -0,0 +1 @@
|
|||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBI1+Q+ntzANebtnicCiAY29uqLGWOaOGLEa8bUqOVmS
|
Loading…
Add table
Reference in a new issue