diff --git a/config/crypt.go b/config/crypt.go index ab1095d76..5f860a6c9 100644 --- a/config/crypt.go +++ b/config/crypt.go @@ -22,5 +22,5 @@ func (o *Options) GetAuditKey() (*cryptutil.PublicKeyEncryptionKey, error) { if err != nil { return nil, err } - return cryptutil.NewPublicKeyEncryptionKey(o.AuditKey.ID, raw) + return cryptutil.NewPublicKeyEncryptionKeyWithID(o.AuditKey.ID, raw) } diff --git a/pkg/cryptutil/kek.go b/pkg/cryptutil/kek.go index 1fa85b54b..3c5d12156 100644 --- a/pkg/cryptutil/kek.go +++ b/pkg/cryptutil/kek.go @@ -36,20 +36,18 @@ const KeyEncryptionKeySize = curve25519.ScalarSize // PrivateKeyEncryptionKey is a Curve25519 asymmetric private encryption key used to decrypt data encryption keys. type PrivateKeyEncryptionKey struct { - id string data [KeyEncryptionKeySize]byte } func (*PrivateKeyEncryptionKey) isKeyEncryptionKey() {} // NewPrivateKeyEncryptionKey creates a new encryption key from existing bytes. -func NewPrivateKeyEncryptionKey(id string, raw []byte) (*PrivateKeyEncryptionKey, error) { +func NewPrivateKeyEncryptionKey(raw []byte) (*PrivateKeyEncryptionKey, error) { if len(raw) != KeyEncryptionKeySize { return nil, fmt.Errorf("cryptutil: invalid key encryption key, expected %d bytes, got %d", KeyEncryptionKeySize, len(raw)) } kek := new(PrivateKeyEncryptionKey) - kek.id = id copy(kek.data[:], raw) return kek, nil } @@ -57,8 +55,7 @@ func NewPrivateKeyEncryptionKey(id string, raw []byte) (*PrivateKeyEncryptionKey // GenerateKeyEncryptionKey generates a new random key encryption key. func GenerateKeyEncryptionKey() (*PrivateKeyEncryptionKey, error) { raw := randomBytes(KeyEncryptionKeySize) - id := GetKeyEncryptionKeyID(raw) - return NewPrivateKeyEncryptionKey(id, raw) + return NewPrivateKeyEncryptionKey(raw) } // GetKeyEncryptionKeyID derives an id from the key encryption key data itself. @@ -89,7 +86,7 @@ func (kek *PrivateKeyEncryptionKey) DecryptDataEncryptionKey(ciphertext []byte) // ID returns the private key's id. func (kek *PrivateKeyEncryptionKey) ID() string { - return kek.id + return kek.Public().id } // KeyBytes returns the private key encryption key's raw bytes. @@ -104,7 +101,10 @@ func (kek *PrivateKeyEncryptionKey) Public() *PublicKeyEncryptionKey { // taken from NACL box.GenerateKey var publicKey [32]byte curve25519.ScalarBaseMult(&publicKey, &kek.data) - return &PublicKeyEncryptionKey{id: kek.id, data: publicKey} + return &PublicKeyEncryptionKey{ + id: GetKeyEncryptionKeyID(kek.data[:]), + data: publicKey, + } } // PublicKeyEncryptionKey is a Curve25519 asymmetric public encryption key used to encrypt data encryption keys. @@ -116,7 +116,12 @@ type PublicKeyEncryptionKey struct { func (*PublicKeyEncryptionKey) isKeyEncryptionKey() {} // NewPublicKeyEncryptionKey creates a new encryption key from existing bytes. -func NewPublicKeyEncryptionKey(id string, raw []byte) (*PublicKeyEncryptionKey, error) { +func NewPublicKeyEncryptionKey(raw []byte) (*PublicKeyEncryptionKey, error) { + return NewPublicKeyEncryptionKeyWithID(GetKeyEncryptionKeyID(raw), raw) +} + +// NewPublicKeyEncryptionKeyWithID creates a new encryption key from an existing id and bytes. +func NewPublicKeyEncryptionKeyWithID(id string, raw []byte) (*PublicKeyEncryptionKey, error) { if len(raw) != KeyEncryptionKeySize { return nil, fmt.Errorf("cryptutil: invalid key encryption key, expected %d bytes, got %d", KeyEncryptionKeySize, len(raw)) diff --git a/pkg/cryptutil/kek_test.go b/pkg/cryptutil/kek_test.go index 57e47ed7e..3e380bb4a 100644 --- a/pkg/cryptutil/kek_test.go +++ b/pkg/cryptutil/kek_test.go @@ -21,7 +21,7 @@ func TestKeyEncryptionKey(t *testing.T) { t.Run("anonymous", func(t *testing.T) { kek, err := GenerateKeyEncryptionKey() require.NoError(t, err) - kekPublic, err := NewPublicKeyEncryptionKey(kek.ID(), kek.Public().KeyBytes()) + kekPublic, err := NewPublicKeyEncryptionKey(kek.Public().KeyBytes()) require.NoError(t, err) ciphertext, err := kekPublic.Encrypt([]byte("HELLO WORLD")) require.NoError(t, err) @@ -43,7 +43,7 @@ func TestKeyEncryptionKey(t *testing.T) { t.Run("ID", func(t *testing.T) { kek, err := GenerateKeyEncryptionKey() require.NoError(t, err) - assert.Equal(t, kek.id, kek.ID()) + assert.Equal(t, kek.Public().id, kek.ID()) }) t.Run("KeyBytes", func(t *testing.T) { private, err := GenerateKeyEncryptionKey() @@ -60,12 +60,12 @@ func TestKeyEncryptionKey(t *testing.T) { }) t.Run("invalid key", func(t *testing.T) { t.Run("private", func(t *testing.T) { - kek, err := NewPrivateKeyEncryptionKey("TEST", []byte("NOT BIG ENOUGH")) + kek, err := NewPrivateKeyEncryptionKey([]byte("NOT BIG ENOUGH")) require.Nil(t, kek) require.Error(t, err) }) t.Run("public", func(t *testing.T) { - kek, err := NewPublicKeyEncryptionKey("TEST", []byte("NOT BIG ENOUGH")) + kek, err := NewPublicKeyEncryptionKey([]byte("NOT BIG ENOUGH")) require.Nil(t, kek) require.Error(t, err) }) diff --git a/pkg/cryptutil/x509.go b/pkg/cryptutil/x509.go new file mode 100644 index 000000000..e1d8500b0 --- /dev/null +++ b/pkg/cryptutil/x509.go @@ -0,0 +1,123 @@ +package cryptutil + +import ( + "crypto/x509" + "crypto/x509/pkix" + "encoding/asn1" + "errors" + "fmt" +) + +// https://tools.ietf.org/id/draft-ietf-curdle-pkix-05.html#rfc.section.3 +var oidPublicKeyX25519 = asn1.ObjectIdentifier{1, 3, 101, 110} + +// from x509, used for ASN.1 +type ( + pkcs8 struct { + Version int + Algo pkix.AlgorithmIdentifier + PrivateKey []byte + } + pkixPublicKey struct { + Algo pkix.AlgorithmIdentifier + BitString asn1.BitString + } + publicKeyInfo struct { + Raw asn1.RawContent + Algorithm pkix.AlgorithmIdentifier + PublicKey asn1.BitString + } +) + +// MarshalPKCS8PrivateKey wraps x509.MarshalPKCS8PrivateKey with added support for KeyEncryptionKeys. +func MarshalPKCS8PrivateKey(key interface{}) ([]byte, error) { + // also support a pointer to a private key encryption key + if kek, ok := key.(*PrivateKeyEncryptionKey); ok { + key = *kek + } + if kek, ok := key.(PrivateKeyEncryptionKey); ok { + var privKey pkcs8 + privKey.Algo = pkix.AlgorithmIdentifier{ + Algorithm: oidPublicKeyX25519, + } + curvePrivateKey, err := asn1.Marshal(kek.KeyBytes()) + if err != nil { + return nil, fmt.Errorf("cryptutil: failed to marshal private key: %v", err) + } + privKey.PrivateKey = curvePrivateKey + return asn1.Marshal(privKey) + } + + // fall back to the original MarshalPKCS8PrivateKey + return x509.MarshalPKCS8PrivateKey(key) +} + +// MarshalPKIXPublicKey wraps x509.MarshalPKIXPublicKey with added support for KeyEncryptionKeys. +func MarshalPKIXPublicKey(pub interface{}) ([]byte, error) { + if kek, ok := pub.(*PublicKeyEncryptionKey); ok { + pub = *kek + } + if kek, ok := pub.(PublicKeyEncryptionKey); ok { + val := pkixPublicKey{ + Algo: pkix.AlgorithmIdentifier{ + Algorithm: oidPublicKeyX25519, + }, + BitString: asn1.BitString{ + Bytes: kek.KeyBytes(), + BitLength: 8 * len(kek.KeyBytes()), + }, + } + ret, _ := asn1.Marshal(val) + return ret, nil + } + + // fall back to the original MarshalPKIXPublicKey + return x509.MarshalPKIXPublicKey(pub) +} + +// ParsePKCS8PrivateKey wraps x509.ParsePKCS8PrivateKey with added support for KeyEncryptionKeys. +func ParsePKCS8PrivateKey(der []byte) (interface{}, error) { + var privKey pkcs8 + _, err := asn1.Unmarshal(der, &privKey) + if err != nil { + return x509.ParsePKCS8PrivateKey(der) + } + + if privKey.Algo.Algorithm.Equal(oidPublicKeyX25519) { + var bs []byte + if _, err := asn1.Unmarshal(privKey.PrivateKey, &bs); err != nil { + return nil, fmt.Errorf("cryptutil: invalid X25519 private key: %v", err) + } + return NewPrivateKeyEncryptionKey(bs) + } + + // fallback to the original ParsePKCS8PrivateKey + return x509.ParsePKCS8PrivateKey(der) +} + +// ParsePKIXPublicKey wraps x509.ParsePKIXPublicKey with added support for KeyEncryptionKeys. +func ParsePKIXPublicKey(derBytes []byte) (pub interface{}, err error) { + var pki publicKeyInfo + rest, err := asn1.Unmarshal(derBytes, &pki) + if err != nil || len(rest) > 0 { + return x509.ParsePKIXPublicKey(derBytes) + } + + if pki.Algorithm.Algorithm.Equal(oidPublicKeyX25519) { + asn1Data := pki.PublicKey.RightAlign() + // RFC 8410, Section 3 + // > For all of the OIDs, the parameters MUST be absent. + if len(pki.Algorithm.Parameters.FullBytes) != 0 { + return nil, errors.New("cryptutil: x25519 key encoded with illegal parameters") + } + if len(asn1Data) != KeyEncryptionKeySize { + return nil, errors.New("cryptutil: wrong x25519 public key size") + } + pub := make([]byte, KeyEncryptionKeySize) + copy(pub, asn1Data) + return NewPublicKeyEncryptionKey(pub) + } + + // fall back to the original ParsePKIXPublicKey + return x509.ParsePKIXPublicKey(derBytes) +} diff --git a/pkg/cryptutil/x509_test.go b/pkg/cryptutil/x509_test.go new file mode 100644 index 000000000..b24968ed7 --- /dev/null +++ b/pkg/cryptutil/x509_test.go @@ -0,0 +1,59 @@ +package cryptutil + +import ( + "encoding/pem" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// generated using: +// openssl genpkey -algorithm x25519 -out priv.pem +// openssl pkey -in priv.pem -out pub.pem -pubout +var ( + rawPrivateX25519Key = []byte(`-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VuBCIEIKALoNgzCksH0v0Bc7Ghl8vGin4MAIKpmtZSmaMN0Vtb +-----END PRIVATE KEY----- +`) + rawPublicX25519Key = []byte(`-----BEGIN PUBLIC KEY----- +MCowBQYDK2VuAyEAk63g8PY1JJTkrranWTxGSd/yA5kAgJlPk4/srMKg9mg= +-----END PUBLIC KEY----- +`) +) + +func TestPKCS8PrivateKey(t *testing.T) { + block, _ := pem.Decode(rawPrivateX25519Key) + + kek, err := ParsePKCS8PrivateKey(block.Bytes) + assert.NoError(t, err) + assert.IsType(t, &PrivateKeyEncryptionKey{}, kek) + + t.Run("marshal", func(t *testing.T) { + der, err := MarshalPKCS8PrivateKey(kek) + require.NoError(t, err) + actual := pem.EncodeToMemory(&pem.Block{ + Type: "PRIVATE KEY", + Bytes: der, + }) + assert.Equal(t, rawPrivateX25519Key, actual) + }) +} + +func TestPKIXPublicKey(t *testing.T) { + block, _ := pem.Decode(rawPublicX25519Key) + + kek, err := ParsePKIXPublicKey(block.Bytes) + assert.NoError(t, err) + assert.IsType(t, &PublicKeyEncryptionKey{}, kek) + + t.Run("marshal", func(t *testing.T) { + der, err := MarshalPKIXPublicKey(kek) + require.NoError(t, err) + actual := pem.EncodeToMemory(&pem.Block{ + Type: "PUBLIC KEY", + Bytes: der, + }) + assert.Equal(t, rawPublicX25519Key, actual) + }) +}