diff --git a/go.mod b/go.mod index 83475e76e2..81e2b0b8f9 100644 --- a/go.mod +++ b/go.mod @@ -35,6 +35,7 @@ require ( github.com/BurntSushi/toml v1.3.2 github.com/aws/aws-sdk-go-v2/config v1.27.0 github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.15.0 + github.com/aws/aws-sdk-go-v2/service/kms v1.28.1 github.com/aws/smithy-go v1.20.0 github.com/beevik/ntp v1.3.1 github.com/benbjohnson/clock v1.3.5 // project archived on 2023-05-18 diff --git a/go.sum b/go.sum index 017b23dc42..5127eca486 100644 --- a/go.sum +++ b/go.sum @@ -107,6 +107,8 @@ github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.0 h1:a33HuFl github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.0/go.mod h1:SxIkWpByiGbhbHYTo9CMTUnx2G4p4ZQMrDPcRRy//1c= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.0 h1:SHN/umDLTmFTmYfI+gkanz6da3vK8Kvj/5wkqnTHbuA= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.0/go.mod h1:l8gPU5RYGOFHJqWEpPMoRTP0VoaWQSkJdKo+hwWnnDA= +github.com/aws/aws-sdk-go-v2/service/kms v1.28.1 h1:+KE6+fDNH9gwg/t6DRddIZW7MJVqf3/IdZqeNTFehuA= +github.com/aws/aws-sdk-go-v2/service/kms v1.28.1/go.mod h1:Y/mkxhbaWCswchbBBLRwet6uYKl/026DZXS87c0DmuU= github.com/aws/aws-sdk-go-v2/service/sso v1.19.0 h1:u6OkVDxtBPnxPkZ9/63ynEe+8kHbtS5IfaC4PzVxzWM= github.com/aws/aws-sdk-go-v2/service/sso v1.19.0/go.mod h1:YqbU3RS/pkDVu+v+Nwxvn0i1WB0HkNWEePWbmODEbbs= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.22.0 h1:6DL0qu5+315wbsAEEmzK+P9leRwNbkp+lGjPC+CEvb8= diff --git a/pkg/imager/profile/input.go b/pkg/imager/profile/input.go index 67f84ee9f8..3b1b62f771 100644 --- a/pkg/imager/profile/input.go +++ b/pkg/imager/profile/input.go @@ -21,6 +21,7 @@ import ( "github.com/siderolabs/talos/internal/pkg/secureboot/measure" "github.com/siderolabs/talos/internal/pkg/secureboot/pesign" "github.com/siderolabs/talos/pkg/archiver" + "github.com/siderolabs/talos/pkg/imager/profile/internal/signer/aws" "github.com/siderolabs/talos/pkg/imager/profile/internal/signer/azure" "github.com/siderolabs/talos/pkg/imager/profile/internal/signer/file" "github.com/siderolabs/talos/pkg/images" @@ -92,24 +93,43 @@ type SecureBootAssets struct { // SigningKeyAndCertificate describes a signing key & certificate. type SigningKeyAndCertificate struct { - // File-based: + // File-based. + // + // Static key and certificate paths. KeyPath string `yaml:"keyPath,omitempty"` CertPath string `yaml:"certPath,omitempty"` - // Azure: + // Azure. + // + // Azure Vault URL and certificate ID, key will be found from the certificate. AzureVaultURL string `yaml:"azureVaultURL,omitempty"` AzureCertificateID string `yaml:"azureCertificateID,omitempty"` + // AWS. + // + // AWS KMS Key ID and region. + // AWS doesn't have a good way to store a certificate, so it's expected to be a file. + AwsKMSKeyID string `yaml:"awsKMSKeyID,omitempty"` + AwsRegion string `yaml:"awsRegion,omitempty"` + AwsCertPath string `yaml:"awsCertPath,omitempty"` } // SigningKey describes a signing key. type SigningKey struct { - // File-based: + // File-based. + // + // Static key path. KeyPath string `yaml:"keyPath,omitempty"` - // Azure: + // Azure. // + // Azure Vault URL and key ID. // AzureKeyVersion might be left empty to use the latest key version. AzureVaultURL string `yaml:"azureVaultURL,omitempty"` AzureKeyID string `yaml:"azureKeyID,omitempty"` AzureKeyVersion string `yaml:"azureKeyVersion,omitempty"` + // AWS. + // + // AWS KMS Key ID and region. + AwsKMSKeyID string `yaml:"awsKMSKeyID,omitempty"` + AwsRegion string `yaml:"awsRegion,omitempty"` } // GetSigner returns the signer. @@ -119,6 +139,8 @@ func (key SigningKey) GetSigner(ctx context.Context) (measure.RSAKey, error) { return file.NewPCRSigner(key.KeyPath) case key.AzureVaultURL != "" && key.AzureKeyID != "": return azure.NewPCRSigner(ctx, key.AzureVaultURL, key.AzureKeyID, key.AzureKeyVersion) + case key.AwsKMSKeyID != "": + return aws.NewPCRSigner(ctx, key.AwsKMSKeyID, key.AwsRegion) default: return nil, errors.New("unsupported PCR signer") } @@ -131,6 +153,8 @@ func (keyAndCert SigningKeyAndCertificate) GetSigner(ctx context.Context) (pesig return file.NewSecureBootSigner(keyAndCert.CertPath, keyAndCert.KeyPath) case keyAndCert.AzureVaultURL != "" && keyAndCert.AzureCertificateID != "": return azure.NewSecureBootSigner(ctx, keyAndCert.AzureVaultURL, keyAndCert.AzureCertificateID, keyAndCert.AzureCertificateID) + case keyAndCert.AwsKMSKeyID != "" && keyAndCert.AwsCertPath != "": + return aws.NewSecureBootSigner(ctx, keyAndCert.AwsKMSKeyID, keyAndCert.AwsRegion, keyAndCert.AwsCertPath) default: return nil, errors.New("unsupported PCR signer") } diff --git a/pkg/imager/profile/internal/signer/aws/aws.go b/pkg/imager/profile/internal/signer/aws/aws.go new file mode 100644 index 0000000000..1ec7cb851b --- /dev/null +++ b/pkg/imager/profile/internal/signer/aws/aws.go @@ -0,0 +1,23 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +// Package aws implements SecureBoot/PCR signers via AWS Key Management Service. +package aws + +import ( + "context" + "fmt" + + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/kms" +) + +func getKmsClient(ctx context.Context, awsRegion string) (*kms.Client, error) { + awsCfg, err := config.LoadDefaultConfig(ctx, config.WithRegion(awsRegion)) + if err != nil { + return nil, fmt.Errorf("error initializing AWS default config: %w", err) + } + + return kms.NewFromConfig(awsCfg), nil +} diff --git a/pkg/imager/profile/internal/signer/aws/aws_test.go b/pkg/imager/profile/internal/signer/aws/aws_test.go new file mode 100644 index 0000000000..6be343b36a --- /dev/null +++ b/pkg/imager/profile/internal/signer/aws/aws_test.go @@ -0,0 +1,38 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package aws_test + +import ( + "context" + "crypto/sha256" + "os" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/siderolabs/talos/pkg/imager/profile/internal/signer/aws" +) + +func TestIntegration(t *testing.T) { + for _, envVar := range []string{"AWS_KMS_KEY_ID", "AWS_REGION", "AWS_CERT_PATH"} { + if os.Getenv(envVar) == "" { + t.Skipf("%s not set", envVar) + } + } + + signer, err := aws.NewPCRSigner(context.TODO(), os.Getenv("AWS_KMS_KEY_ID"), os.Getenv("AWS_REGION")) + require.NoError(t, err) + + digest := sha256.Sum256(nil) + + _, err = signer.Sign(nil, digest[:], nil) + require.NoError(t, err) + + sbSigner, err := aws.NewSecureBootSigner(context.TODO(), os.Getenv("AWS_KMS_KEY_ID"), os.Getenv("AWS_REGION"), os.Getenv("AWS_CERT_PATH")) + require.NoError(t, err) + + _, err = sbSigner.Signer().Sign(nil, digest[:], nil) + require.NoError(t, err) +} diff --git a/pkg/imager/profile/internal/signer/aws/pcr.go b/pkg/imager/profile/internal/signer/aws/pcr.go new file mode 100644 index 0000000000..117e7b02a3 --- /dev/null +++ b/pkg/imager/profile/internal/signer/aws/pcr.go @@ -0,0 +1,151 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package aws + +import ( + "context" + "crypto" + "crypto/rsa" + "crypto/x509" + "fmt" + "io" + "math/big" + + "github.com/aws/aws-sdk-go-v2/service/kms" + "github.com/aws/aws-sdk-go-v2/service/kms/types" + + "github.com/siderolabs/talos/internal/pkg/secureboot/measure" +) + +// KeySigner implements measure.RSAKey interface. +// +// KeySigner wraps Azure APIs to provide public key and crypto.Signer interface out of Azure Key Vault RSA key. +type KeySigner struct { + keyName string + mode mode + + client *kms.Client + publicKey *rsa.PublicKey +} + +var algMap = map[mode]map[crypto.Hash]types.SigningAlgorithmSpec{ + rsaPKCS1v15: { + crypto.SHA256: types.SigningAlgorithmSpecRsassaPkcs1V15Sha256, + crypto.SHA384: types.SigningAlgorithmSpecRsassaPkcs1V15Sha384, + crypto.SHA512: types.SigningAlgorithmSpecRsassaPkcs1V15Sha512, + }, + rsaPSS: { + crypto.SHA256: types.SigningAlgorithmSpecRsassaPssSha256, + crypto.SHA384: types.SigningAlgorithmSpecRsassaPssSha384, + crypto.SHA512: types.SigningAlgorithmSpecRsassaPssSha512, + }, + ecdsa: { + crypto.SHA256: types.SigningAlgorithmSpecEcdsaSha256, + crypto.SHA384: types.SigningAlgorithmSpecEcdsaSha384, + crypto.SHA512: types.SigningAlgorithmSpecEcdsaSha512, + }, +} + +type mode string + +const ( + rsaPKCS1v15 mode = "pkcs1v15" + rsaPSS mode = "pss" + ecdsa mode = "ecdsa" +) + +// PublicRSAKey returns the public key. +func (s *KeySigner) PublicRSAKey() *rsa.PublicKey { + return s.publicKey +} + +// Public returns the public key. +func (s *KeySigner) Public() crypto.PublicKey { + return s.PublicRSAKey() +} + +// Sign implements the crypto.Signer interface. +func (s *KeySigner) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) { + mode := s.mode + + inner := algMap[mode] + if inner == nil { + return nil, fmt.Errorf("mode not supported") + } + + hf := crypto.SHA256 + + if opts != nil { + hf = opts.HashFunc() + } + + algorithm := inner[hf] + if algorithm == "" { + return nil, fmt.Errorf("algorithm not supported") + } + + resp, err := s.client.Sign(context.Background(), &kms.SignInput{ + KeyId: &s.keyName, + Message: digest, + MessageType: types.MessageTypeDigest, + SigningAlgorithm: algorithm, + }) + if err != nil { + return nil, err + } + + return resp.Signature, nil +} + +// Verify interface. +var _ measure.RSAKey = (*KeySigner)(nil) + +// NewPCRSigner creates a new PCR signer from AWS settings. +func NewPCRSigner(ctx context.Context, kmsKeyID, awsRegion string) (*KeySigner, error) { + client, err := getKmsClient(ctx, awsRegion) + if err != nil { + return nil, fmt.Errorf("failed to build AWS kms client: %w", err) + } + + keyResponse, err := client.GetPublicKey(ctx, &kms.GetPublicKeyInput{ + KeyId: &kmsKeyID, + }) + if err != nil { + return nil, fmt.Errorf("failed to get key: %w", err) + } + + if keyResponse.KeyUsage != "SIGN_VERIFY" { + return nil, fmt.Errorf("key usage is not SIGN_VERIFY") + } + + switch keyResponse.KeySpec { //nolint:exhaustive + case types.KeySpecRsa2048, types.KeySpecRsa3072, types.KeySpecRsa4096: + // expected, continue + default: + return nil, fmt.Errorf("key type is not RSA") + } + + parsedKey, err := x509.ParsePKIXPublicKey(keyResponse.PublicKey) + if err != nil { + return nil, fmt.Errorf("Public key is not valid: %w", err) + } + + rsaKey := parsedKey.(*rsa.PublicKey) //nolint:errcheck + if rsaKey.E == 0 { + return nil, fmt.Errorf("property e is empty") + } + + if rsaKey.N.Cmp(big.NewInt(0)) == 0 { + return nil, fmt.Errorf("property N is empty") + } + + return &KeySigner{ + keyName: kmsKeyID, + mode: rsaPKCS1v15, // TODO: make this configurable + + publicKey: rsaKey, + client: client, + }, nil +} diff --git a/pkg/imager/profile/internal/signer/aws/secureboot.go b/pkg/imager/profile/internal/signer/aws/secureboot.go new file mode 100644 index 0000000000..8c17d48d08 --- /dev/null +++ b/pkg/imager/profile/internal/signer/aws/secureboot.go @@ -0,0 +1,63 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package aws + +import ( + "context" + "crypto" + "crypto/x509" + "encoding/pem" + "fmt" + "os" + + "github.com/siderolabs/talos/internal/pkg/secureboot/pesign" +) + +// SecureBootSigner implements pesign.CertificateSigner interface. +type SecureBootSigner struct { + keySigner *KeySigner + cert *x509.Certificate +} + +// Verify interface. +var _ pesign.CertificateSigner = (*SecureBootSigner)(nil) + +// Signer returns the signer. +func (s *SecureBootSigner) Signer() crypto.Signer { + return s.keySigner +} + +// Certificate returns the certificate. +func (s *SecureBootSigner) Certificate() *x509.Certificate { + return s.cert +} + +// NewSecureBootSigner creates a new SecureBootSigner. +func NewSecureBootSigner(ctx context.Context, kmsKeyID, awsRegion, certPath string) (*SecureBootSigner, error) { + keySigner, err := NewPCRSigner(ctx, kmsKeyID, awsRegion) + if err != nil { + return nil, fmt.Errorf("failed to initialize certificate key signer (kms): %w", err) + } + + certData, err := os.ReadFile(certPath) + if err != nil { + return nil, err + } + + certBlock, _ := pem.Decode(certData) + if certBlock == nil { + return nil, fmt.Errorf("failed to decode certificate") + } + + cert, err := x509.ParseCertificate(certBlock.Bytes) + if err != nil { + return nil, fmt.Errorf("failed to parse certificate: %w", err) + } + + return &SecureBootSigner{ + keySigner: keySigner, + cert: cert, + }, nil +}