mirror of
https://github.com/slackhq/nebula.git
synced 2025-11-22 08:24:25 +01:00
Add ability to encrypt CA private key at rest (#386)
Fixes #8. `nebula-cert ca` now supports encrypting the CA's private key with a passphrase. Pass `-encrypt` in order to be prompted for a passphrase. Encryption is performed using AES-256-GCM and Argon2id for KDF. KDF parameters default to RFC recommendations, but can be overridden via CLI flags `-argon-memory`, `-argon-parallelism`, and `-argon-iterations`.
This commit is contained in:
151
cert/cert.go
151
cert/cert.go
@@ -9,7 +9,9 @@ import (
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
@@ -21,11 +23,12 @@ import (
|
||||
const publicKeyLen = 32
|
||||
|
||||
const (
|
||||
CertBanner = "NEBULA CERTIFICATE"
|
||||
X25519PrivateKeyBanner = "NEBULA X25519 PRIVATE KEY"
|
||||
X25519PublicKeyBanner = "NEBULA X25519 PUBLIC KEY"
|
||||
Ed25519PrivateKeyBanner = "NEBULA ED25519 PRIVATE KEY"
|
||||
Ed25519PublicKeyBanner = "NEBULA ED25519 PUBLIC KEY"
|
||||
CertBanner = "NEBULA CERTIFICATE"
|
||||
X25519PrivateKeyBanner = "NEBULA X25519 PRIVATE KEY"
|
||||
X25519PublicKeyBanner = "NEBULA X25519 PUBLIC KEY"
|
||||
EncryptedEd25519PrivateKeyBanner = "NEBULA ED25519 ENCRYPTED PRIVATE KEY"
|
||||
Ed25519PrivateKeyBanner = "NEBULA ED25519 PRIVATE KEY"
|
||||
Ed25519PublicKeyBanner = "NEBULA ED25519 PUBLIC KEY"
|
||||
)
|
||||
|
||||
type NebulaCertificate struct {
|
||||
@@ -48,8 +51,21 @@ type NebulaCertificateDetails struct {
|
||||
InvertedGroups map[string]struct{}
|
||||
}
|
||||
|
||||
type NebulaEncryptedData struct {
|
||||
EncryptionMetadata NebulaEncryptionMetadata
|
||||
Ciphertext []byte
|
||||
}
|
||||
|
||||
type NebulaEncryptionMetadata struct {
|
||||
EncryptionAlgorithm string
|
||||
Argon2Parameters Argon2Parameters
|
||||
}
|
||||
|
||||
type m map[string]interface{}
|
||||
|
||||
// Returned if we try to unmarshal an encrypted private key without a passphrase
|
||||
var ErrPrivateKeyEncrypted = errors.New("private key must be decrypted")
|
||||
|
||||
// UnmarshalNebulaCertificate will unmarshal a protobuf byte representation of a nebula cert
|
||||
func UnmarshalNebulaCertificate(b []byte) (*NebulaCertificate, error) {
|
||||
if len(b) == 0 {
|
||||
@@ -144,6 +160,30 @@ func MarshalEd25519PrivateKey(key ed25519.PrivateKey) []byte {
|
||||
return pem.EncodeToMemory(&pem.Block{Type: Ed25519PrivateKeyBanner, Bytes: key})
|
||||
}
|
||||
|
||||
// EncryptAndMarshalX25519PrivateKey is a simple helper to encrypt and PEM encode an X25519 private key
|
||||
func EncryptAndMarshalEd25519PrivateKey(b []byte, passphrase []byte, kdfParams *Argon2Parameters) ([]byte, error) {
|
||||
ciphertext, err := aes256Encrypt(passphrase, kdfParams, b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
b, err = proto.Marshal(&RawNebulaEncryptedData{
|
||||
EncryptionMetadata: &RawNebulaEncryptionMetadata{
|
||||
EncryptionAlgorithm: "AES-256-GCM",
|
||||
Argon2Parameters: &RawNebulaArgon2Parameters{
|
||||
Version: kdfParams.version,
|
||||
Memory: kdfParams.Memory,
|
||||
Parallelism: uint32(kdfParams.Parallelism),
|
||||
Iterations: kdfParams.Iterations,
|
||||
Salt: kdfParams.salt,
|
||||
},
|
||||
},
|
||||
Ciphertext: ciphertext,
|
||||
})
|
||||
|
||||
return pem.EncodeToMemory(&pem.Block{Type: EncryptedEd25519PrivateKeyBanner, Bytes: b}), nil
|
||||
}
|
||||
|
||||
// UnmarshalX25519PrivateKey will try to pem decode an X25519 private key, returning any other bytes b
|
||||
// or an error on failure
|
||||
func UnmarshalX25519PrivateKey(b []byte) ([]byte, []byte, error) {
|
||||
@@ -168,9 +208,13 @@ func UnmarshalEd25519PrivateKey(b []byte) (ed25519.PrivateKey, []byte, error) {
|
||||
if k == nil {
|
||||
return nil, r, fmt.Errorf("input did not contain a valid PEM encoded block")
|
||||
}
|
||||
if k.Type != Ed25519PrivateKeyBanner {
|
||||
|
||||
if k.Type == EncryptedEd25519PrivateKeyBanner {
|
||||
return nil, r, ErrPrivateKeyEncrypted
|
||||
} else if k.Type != Ed25519PrivateKeyBanner {
|
||||
return nil, r, fmt.Errorf("bytes did not contain a proper nebula Ed25519 private key banner")
|
||||
}
|
||||
|
||||
if len(k.Bytes) != ed25519.PrivateKeySize {
|
||||
return nil, r, fmt.Errorf("key was not 64 bytes, is invalid ed25519 private key")
|
||||
}
|
||||
@@ -178,6 +222,101 @@ func UnmarshalEd25519PrivateKey(b []byte) (ed25519.PrivateKey, []byte, error) {
|
||||
return k.Bytes, r, nil
|
||||
}
|
||||
|
||||
// UnmarshalNebulaCertificate will unmarshal a protobuf byte representation of a nebula cert into its
|
||||
// protobuf-generated struct.
|
||||
func UnmarshalNebulaEncryptedData(b []byte) (*NebulaEncryptedData, error) {
|
||||
if len(b) == 0 {
|
||||
return nil, fmt.Errorf("nil byte array")
|
||||
}
|
||||
var rned RawNebulaEncryptedData
|
||||
err := proto.Unmarshal(b, &rned)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if rned.EncryptionMetadata == nil {
|
||||
return nil, fmt.Errorf("encoded EncryptionMetadata was nil")
|
||||
}
|
||||
|
||||
if rned.EncryptionMetadata.Argon2Parameters == nil {
|
||||
return nil, fmt.Errorf("encoded Argon2Parameters was nil")
|
||||
}
|
||||
|
||||
params, err := unmarshalArgon2Parameters(rned.EncryptionMetadata.Argon2Parameters)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ned := NebulaEncryptedData{
|
||||
EncryptionMetadata: NebulaEncryptionMetadata{
|
||||
EncryptionAlgorithm: rned.EncryptionMetadata.EncryptionAlgorithm,
|
||||
Argon2Parameters: *params,
|
||||
},
|
||||
Ciphertext: rned.Ciphertext,
|
||||
}
|
||||
|
||||
return &ned, nil
|
||||
}
|
||||
|
||||
func unmarshalArgon2Parameters(params *RawNebulaArgon2Parameters) (*Argon2Parameters, error) {
|
||||
if params.Version < math.MinInt32 || params.Version > math.MaxInt32 {
|
||||
return nil, fmt.Errorf("Argon2Parameters Version must be at least %d and no more than %d", math.MinInt32, math.MaxInt32)
|
||||
}
|
||||
if params.Memory <= 0 || params.Memory > math.MaxUint32 {
|
||||
return nil, fmt.Errorf("Argon2Parameters Memory must be be greater than 0 and no more than %d KiB", uint32(math.MaxUint32))
|
||||
}
|
||||
if params.Parallelism <= 0 || params.Parallelism > math.MaxUint8 {
|
||||
return nil, fmt.Errorf("Argon2Parameters Parallelism must be be greater than 0 and no more than %d", math.MaxUint8)
|
||||
}
|
||||
if params.Iterations <= 0 || params.Iterations > math.MaxUint32 {
|
||||
return nil, fmt.Errorf("-argon-iterations must be be greater than 0 and no more than %d", uint32(math.MaxUint32))
|
||||
}
|
||||
|
||||
return &Argon2Parameters{
|
||||
version: rune(params.Version),
|
||||
Memory: uint32(params.Memory),
|
||||
Parallelism: uint8(params.Parallelism),
|
||||
Iterations: uint32(params.Iterations),
|
||||
salt: params.Salt,
|
||||
}, nil
|
||||
|
||||
}
|
||||
|
||||
// DecryptAndUnmarshalEd25519PrivateKey will try to pem decode and decrypt an Ed25519 private key with
|
||||
// the given passphrase, returning any other bytes b or an error on failure
|
||||
func DecryptAndUnmarshalEd25519PrivateKey(passphrase, b []byte) (ed25519.PrivateKey, []byte, error) {
|
||||
k, r := pem.Decode(b)
|
||||
if k == nil {
|
||||
return nil, r, fmt.Errorf("input did not contain a valid PEM encoded block")
|
||||
}
|
||||
|
||||
if k.Type != EncryptedEd25519PrivateKeyBanner {
|
||||
return nil, r, fmt.Errorf("bytes did not contain a proper nebula encrypted Ed25519 private key banner")
|
||||
}
|
||||
|
||||
ned, err := UnmarshalNebulaEncryptedData(k.Bytes)
|
||||
if err != nil {
|
||||
return nil, r, err
|
||||
}
|
||||
|
||||
var bytes []byte
|
||||
switch ned.EncryptionMetadata.EncryptionAlgorithm {
|
||||
case "AES-256-GCM":
|
||||
bytes, err = aes256Decrypt(passphrase, &ned.EncryptionMetadata.Argon2Parameters, ned.Ciphertext)
|
||||
if err != nil {
|
||||
return nil, r, err
|
||||
}
|
||||
default:
|
||||
return nil, r, fmt.Errorf("unsupported encryption algorithm: %s", ned.EncryptionMetadata.EncryptionAlgorithm)
|
||||
}
|
||||
|
||||
if len(bytes) != ed25519.PrivateKeySize {
|
||||
return nil, r, fmt.Errorf("key was not 64 bytes, is invalid ed25519 private key")
|
||||
}
|
||||
|
||||
return bytes, r, nil
|
||||
}
|
||||
|
||||
// MarshalX25519PublicKey is a simple helper to PEM encode an X25519 public key
|
||||
func MarshalX25519PublicKey(b []byte) []byte {
|
||||
return pem.EncodeToMemory(&pem.Block{Type: X25519PublicKeyBanner, Bytes: b})
|
||||
|
||||
Reference in New Issue
Block a user