Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions api/proto/teleport/legacy/types/types.proto
Original file line number Diff line number Diff line change
Expand Up @@ -1249,6 +1249,19 @@ message JWTKeyPair {
PrivateKeyType PrivateKeyType = 3 [(gogoproto.jsontag) = "private_key_type,omitempty"];
}

// EncryptionKeyPair is a PEM encoded keypair used for encrypting and decrypting data.
message EncryptionKeyPair {
// PublicKey is a PEM encoded public key.
bytes public_key = 1 [(gogoproto.jsontag) = "public_key,omitempty"];
// PrivateKey is a PEM encoded private key.
bytes private_key = 2 [(gogoproto.jsontag) = "private_key,omitempty"];
// PrivateKeyType is the type of the PrivateKey.
PrivateKeyType private_key_type = 3 [(gogoproto.jsontag) = "private_key_type,omitempty"];
// Hash function used during OAEP encryption/decryption. It maps directly to the possible
// values of [crypto.Hash] in the go crypto package.
uint32 hash = 4 [(gogoproto.jsontag) = "hash,omitempty"];
}

// CertAuthorityV2 is version 2 resource spec for Cert Authority
message CertAuthorityV2 {
option (gogoproto.goproto_stringer) = false;
Expand Down
46 changes: 46 additions & 0 deletions api/types/keys.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Copyright 2025 Gravitational, Inc
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package types

import (
"crypto"
"crypto/rand"
"crypto/rsa"

"github.com/gravitational/trace"

"github.com/gravitational/teleport/api/utils/keys"
)

// EncryptOAEP encrypts data using OAEP with the public key and hash present
// in the EncryptionKey receiver.
func (k EncryptionKeyPair) EncryptOAEP(plaintext []byte) ([]byte, error) {
pub, err := keys.ParsePublicKey(k.PublicKey)
if err != nil {
return nil, trace.Wrap(err)
}

hash := crypto.SHA256
if k.Hash > 0 {
hash = crypto.Hash(k.Hash)
}
switch pubKey := pub.(type) {
case *rsa.PublicKey:
ciphertext, err := rsa.EncryptOAEP(hash.New(), rand.Reader, pubKey, plaintext, nil)
return ciphertext, trace.Wrap(err)
default:
return nil, trace.BadParameter("unsupported encryption public key type %T", pub)
}
}
5,127 changes: 2,707 additions & 2,420 deletions api/types/types.pb.go

Large diffs are not rendered by default.

16 changes: 16 additions & 0 deletions api/utils/keys/privatekey.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ type cryptoPublicKeyI interface {
// custom implementation for a non-standard private key, such as a hardware key.
type PrivateKey struct {
crypto.Signer

// sshPub is the public key in ssh.PublicKey form.
sshPub ssh.PublicKey
// keyPEM is PEM-encoded private key data which can be parsed with ParsePrivateKey.
Expand Down Expand Up @@ -395,6 +396,21 @@ func MarshalPrivateKey(key crypto.Signer) ([]byte, error) {
}
}

// MarshalDecrypter will return a PEM encoded crypto.Decrypter.
// [key] must be an *rsa.PrivateKey
func MarshalDecrypter(key crypto.Decrypter) ([]byte, error) {
switch privateKey := key.(type) {
case *rsa.PrivateKey:
privPEM := pem.EncodeToMemory(&pem.Block{
Type: PKCS1PrivateKeyType,
Bytes: x509.MarshalPKCS1PrivateKey(privateKey),
})
return privPEM, nil
default:
return nil, trace.BadParameter("unsupported private key type %T", key)
}
}

// LoadKeyPair returns the PrivateKey for the given private and public key files.
func LoadKeyPair(privFile, sshPubFile string, opts ...ParsePrivateKeyOpt) (*PrivateKey, error) {
privPEM, err := os.ReadFile(privFile)
Expand Down
19 changes: 14 additions & 5 deletions lib/auth/keystore/aws_kms.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,9 +163,9 @@ func (a *awsKMSKeystore) keyTypeDescription() string {
return fmt.Sprintf("AWS KMS keys in account %s and region %s", a.awsAccount, a.awsRegion)
}

// generateKey creates a new private key and returns its identifier and a crypto.Signer. The returned
// generateSigner creates a new private key and returns its identifier and a crypto.Signer. The returned
// identifier can be passed to getSigner later to get an equivalent crypto.Signer.
func (a *awsKMSKeystore) generateKey(ctx context.Context, algorithm cryptosuites.Algorithm) ([]byte, crypto.Signer, error) {
func (a *awsKMSKeystore) generateSigner(ctx context.Context, algorithm cryptosuites.Algorithm) ([]byte, crypto.Signer, error) {
alg, err := awsAlgorithm(algorithm)
if err != nil {
return nil, nil, trace.Wrap(err)
Expand Down Expand Up @@ -212,6 +212,10 @@ func (a *awsKMSKeystore) generateKey(ctx context.Context, algorithm cryptosuites
return keyID, signer, nil
}

func (a *awsKMSKeystore) generateDecrypter(ctx context.Context, alg cryptosuites.Algorithm) (keyID []byte, decrypter crypto.Decrypter, hash crypto.Hash, err error) {
return nil, nil, crypto.SHA256, trace.NotImplemented("decryption not yet supported for AWS KMS key store")
}

func awsAlgorithm(alg cryptosuites.Algorithm) (kmstypes.KeySpec, error) {
switch alg {
case cryptosuites.RSA2048:
Expand All @@ -231,6 +235,11 @@ func (a *awsKMSKeystore) getSigner(ctx context.Context, rawKey []byte, publicKey
return a.newSignerWithPublicKey(ctx, key, publicKey)
}

// getDecrypter returns a crypto.Signer for the given key identifier, if it is found.
func (a *awsKMSKeystore) getDecrypter(ctx context.Context, rawKey []byte, publicKey crypto.PublicKey, hash crypto.Hash) (crypto.Decrypter, error) {
return nil, trace.NotImplemented("decryption not yet supported for AWS KMS key store")
}

type awsKMSSigner struct {
key awsKMSKeyID
pub crypto.PublicKey
Expand Down Expand Up @@ -367,9 +376,9 @@ func (a *awsKMSKeystore) deleteKey(ctx context.Context, rawKey []byte) error {
return trace.Wrap(err, "error deleting AWS KMS key")
}

// canSignWithKey returns true if this KeyStore is able to sign with the given
// canUseKey returns true if this KeyStore is able to sign with the given
// key.
func (a *awsKMSKeystore) canSignWithKey(ctx context.Context, raw []byte, keyType types.PrivateKeyType) (bool, error) {
func (a *awsKMSKeystore) canUseKey(ctx context.Context, raw []byte, keyType types.PrivateKeyType) (bool, error) {
if keyType != types.PrivateKeyType_AWS_KMS {
return false, nil
}
Expand Down Expand Up @@ -397,7 +406,7 @@ func (a *awsKMSKeystore) canSignWithKey(ctx context.Context, raw []byte, keyType
func (a *awsKMSKeystore) deleteUnusedKeys(ctx context.Context, activeKeys [][]byte) error {
activeAWSKMSKeys := make(map[string]int)
for _, activeKey := range activeKeys {
keyIsRelevent, err := a.canSignWithKey(ctx, activeKey, keyType(activeKey))
keyIsRelevent, err := a.canUseKey(ctx, activeKey, keyType(activeKey))
if err != nil {
// Don't expect this error to ever hit, safer to return if it does.
return trace.Wrap(err)
Expand Down
39 changes: 39 additions & 0 deletions lib/auth/keystore/aws_kms_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"context"
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"fmt"
"slices"
Expand Down Expand Up @@ -355,6 +356,7 @@ func newFakeAWSKMSService(t *testing.T, clock clockwork.Clock, account string, r
type fakeAWSKMSKey struct {
arn arn.ARN
privKeyPEM []byte
keyUsage kmstypes.KeyUsageType
tags []kmstypes.Tag
creationDate time.Time
state kmstypes.KeyState
Expand Down Expand Up @@ -408,6 +410,7 @@ func (f *fakeAWSKMSService) CreateKey(_ context.Context, input *kms.CreateKeyInp
f.keys = append(f.keys, &fakeAWSKMSKey{
arn: a,
privKeyPEM: privKeyPEM,
keyUsage: input.KeyUsage,
tags: input.Tags,
creationDate: f.clock.Now(),
region: f.region,
Expand Down Expand Up @@ -450,6 +453,9 @@ func (f *fakeAWSKMSService) Sign(_ context.Context, input *kms.SignInput, _ ...f
if key.state != kmstypes.KeyStateEnabled {
return nil, trace.NotFound("key %q is not enabled", aws.ToString(input.KeyId))
}
if key.keyUsage != kmstypes.KeyUsageTypeSignVerify {
return nil, trace.BadParameter("key %q is not a signing key", aws.ToString(input.KeyId))
}
signer, err := keys.ParsePrivateKey(key.privKeyPEM)
if err != nil {
return nil, trace.Wrap(err)
Expand All @@ -472,6 +478,39 @@ func (f *fakeAWSKMSService) Sign(_ context.Context, input *kms.SignInput, _ ...f
}, nil
}

func (f *fakeAWSKMSService) Decrypt(_ context.Context, input *kms.DecryptInput, _ ...func(*kms.Options)) (*kms.DecryptOutput, error) {
Comment thread
eriktate marked this conversation as resolved.
key, err := f.findKey(aws.ToString(input.KeyId))
if err != nil {
return nil, trace.Wrap(err)
}
if key.state != kmstypes.KeyStateEnabled {
return nil, trace.NotFound("key %q is not enabled", aws.ToString(input.KeyId))
}
if key.keyUsage != kmstypes.KeyUsageTypeEncryptDecrypt {
return nil, trace.BadParameter("key %q is not a decryption key", aws.ToString(input.KeyId))
}
signer, err := keys.ParsePrivateKey(key.privKeyPEM)
if err != nil {
return nil, trace.Wrap(err)
}
decrypter, ok := signer.Signer.(crypto.Decrypter)
if !ok {
return nil, trace.Errorf("private key is not a decrypter")
}
switch input.EncryptionAlgorithm {
case kmstypes.EncryptionAlgorithmSpecRsaesOaepSha256:
default:
return nil, trace.BadParameter("unsupported EncryptionAlgorithm %q", input.EncryptionAlgorithm)
}
plaintext, err := decrypter.Decrypt(rand.Reader, input.CiphertextBlob, &rsa.OAEPOptions{Hash: crypto.SHA256})
if err != nil {
return nil, trace.Wrap(err)
}
return &kms.DecryptOutput{
Plaintext: plaintext,
}, nil
}

func (f *fakeAWSKMSService) ScheduleKeyDeletion(_ context.Context, input *kms.ScheduleKeyDeletionInput, _ ...func(*kms.Options)) (*kms.ScheduleKeyDeletionOutput, error) {
key, err := f.findKey(aws.ToString(input.KeyId))
if err != nil {
Expand Down
Loading
Loading