diff --git a/internal/darwin/security/security_darwin.go b/internal/darwin/security/security_darwin.go index e1a37fe8..7ce8a7d3 100644 --- a/internal/darwin/security/security_darwin.go +++ b/internal/darwin/security/security_darwin.go @@ -100,6 +100,7 @@ var ( KSecKeyAlgorithmRSASignatureDigestPSSSHA256 = C.kSecKeyAlgorithmRSASignatureDigestPSSSHA256 KSecKeyAlgorithmRSASignatureDigestPSSSHA384 = C.kSecKeyAlgorithmRSASignatureDigestPSSSHA384 KSecKeyAlgorithmRSASignatureDigestPSSSHA512 = C.kSecKeyAlgorithmRSASignatureDigestPSSSHA512 + KSecKeyAlgorithmECDHKeyExchangeStandard = C.kSecKeyAlgorithmECDHKeyExchangeStandard ) type SecAccessControlCreateFlags = C.SecAccessControlCreateFlags @@ -271,6 +272,28 @@ func SecCertificateCreateWithData(certData *cf.DataRef) (*SecCertificateRef, err }, nil } +func SecKeyCreateWithData(keyData *cf.DataRef, attributes *cf.DictionaryRef) (*SecKeyRef, error) { + var cerr C.CFErrorRef + keyRef := C.SecKeyCreateWithData(C.CFDataRef(keyData.Value), C.CFDictionaryRef(attributes.Value), &cerr) + if err := goCFErrorRef(cerr); err != nil { + return nil, err + } + return &SecKeyRef{ + Value: keyRef, + }, nil +} + +func SecKeyCopyKeyExchangeResult(privateKey *SecKeyRef, algorithm SecKeyAlgorithm, publicKey *SecKeyRef, parameters *cf.DictionaryRef) (*cf.DataRef, error) { + var cerr C.CFErrorRef + dataRef := C.SecKeyCopyKeyExchangeResult(privateKey.Value, algorithm, publicKey.Value, C.CFDictionaryRef(parameters.Value), &cerr) + if err := goCFErrorRef(cerr); err != nil { + return nil, err + } + return &cf.DataRef{ + Value: cf.CFDataRef(dataRef), + }, nil +} + func SecCopyErrorMessageString(status C.OSStatus) *cf.StringRef { s := C.SecCopyErrorMessageString(status, nil) return &cf.StringRef{ diff --git a/kms/mackms/signer.go b/kms/mackms/signer.go index 4d88a4c6..69acb772 100644 --- a/kms/mackms/signer.go +++ b/kms/mackms/signer.go @@ -22,7 +22,9 @@ package mackms import ( "crypto" + "crypto/ecdh" "crypto/ecdsa" + "crypto/elliptic" "crypto/rsa" "fmt" "io" @@ -110,3 +112,104 @@ func getSecKeyAlgorithm(pub crypto.PublicKey, opts crypto.SignerOpts) (security. return 0, fmt.Errorf("unsupported key type %T", pub) } } + +// ECDH extends [Signer] with ECDH exchange method. +// +// # Experimental +// +// Notice: This API is EXPERIMENTAL and may be changed or removed in a later +// release. +type ECDH struct { + *Signer +} + +// ECDH performs an ECDH exchange and returns the shared secret. The private key +// and public key must use the same curve. +// +// For NIST curves, this performs ECDH as specified in SEC 1, Version 2.0, +// Section 3.3.1, and returns the x-coordinate encoded according to SEC 1, +// Version 2.0, Section 2.3.5. The result is never the point at infinity. +// +// # Experimental +// +// Notice: This API is EXPERIMENTAL and may be changed or removed in a later +// release. +func (e *ECDH) ECDH(pub *ecdh.PublicKey) ([]byte, error) { + key, err := getPrivateKey(e.Signer.keyAttributes) + if err != nil { + return nil, fmt.Errorf("mackms ECDH failed: %w", err) + } + defer key.Release() + + pubData, err := cf.NewData(pub.Bytes()) + if err != nil { + return nil, fmt.Errorf("mackms ECDH failed: %w", err) + } + defer pubData.Release() + + pubDict, err := cf.NewDictionary(cf.Dictionary{ + security.KSecAttrKeyType: security.KSecAttrKeyTypeECSECPrimeRandom, + security.KSecAttrKeyClass: security.KSecAttrKeyClassPublic, + }) + if err != nil { + return nil, fmt.Errorf("mackms ECDH failed: %w", err) + } + defer pubDict.Release() + + pubRef, err := security.SecKeyCreateWithData(pubData, pubDict) + if err != nil { + return nil, fmt.Errorf("macOS SecKeyCreateWithData failed: %w", err) + } + defer pubRef.Release() + + sharedSecret, err := security.SecKeyCopyKeyExchangeResult(key, security.KSecKeyAlgorithmECDHKeyExchangeStandard, pubRef, &cf.DictionaryRef{}) + if err != nil { + return nil, fmt.Errorf("macOS SecKeyCopyKeyExchangeResult failed: %w", err) + } + defer sharedSecret.Release() + + return sharedSecret.Bytes(), nil +} + +// Curve returns the [ecdh.Curve] of the key. If the key is not an ECDSA key it +// will return nil. +// +// # Experimental +// +// Notice: This API is EXPERIMENTAL and may be changed or removed in a later +// release. +func (e *ECDH) Curve() ecdh.Curve { + pub, ok := e.Signer.pub.(*ecdsa.PublicKey) + if !ok { + return nil + } + switch pub.Curve { + case elliptic.P256(): + return ecdh.P256() + case elliptic.P384(): + return ecdh.P384() + case elliptic.P521(): + return ecdh.P521() + default: + return nil + } +} + +// PublicKey returns the [ecdh.PublicKey] representation of the key. If the key +// is not an ECDSA or it cannot be converted it will return nil. +// +// # Experimental +// +// Notice: This API is EXPERIMENTAL and may be changed or removed in a later +// release. +func (e *ECDH) PublicKey() *ecdh.PublicKey { + pub, ok := e.Signer.pub.(*ecdsa.PublicKey) + if !ok { + return nil + } + ecdhPub, err := pub.ECDH() + if err != nil { + return nil + } + return ecdhPub +} diff --git a/kms/mackms/signer_test.go b/kms/mackms/signer_test.go new file mode 100644 index 00000000..863ba346 --- /dev/null +++ b/kms/mackms/signer_test.go @@ -0,0 +1,230 @@ +//go:build darwin && cgo && !nomackms + +// Copyright (c) Smallstep Labs, Inc. +// Copyright (c) Meta Platforms, Inc. and affiliates. +// +// 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. +// +// Part of this code is based on +// https://github.com/facebookincubator/sks/blob/183e7561ecedc71992f23b2d37983d2948391f4c/macos/macos.go + +package mackms + +import ( + "crypto" + "crypto/ecdh" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.step.sm/crypto/kms/apiv1" +) + +func createKey(t *testing.T, name string, sa apiv1.SignatureAlgorithm) *apiv1.CreateKeyResponse { + t.Helper() + + kms := &MacKMS{} + resp, err := kms.CreateKey(&apiv1.CreateKeyRequest{ + Name: "mackms:label=" + name, + SignatureAlgorithm: sa, + }) + require.NoError(t, err) + t.Cleanup(func() { + assert.NoError(t, kms.DeleteKey(&apiv1.DeleteKeyRequest{ + Name: resp.Name, + })) + }) + return resp +} + +func TestECDH_ECDH(t *testing.T) { + goP256, err := ecdh.P256().GenerateKey(rand.Reader) + require.NoError(t, err) + goP384, err := ecdh.P384().GenerateKey(rand.Reader) + require.NoError(t, err) + goP521, err := ecdh.P521().GenerateKey(rand.Reader) + require.NoError(t, err) + goX25519, err := ecdh.X25519().GenerateKey(rand.Reader) + require.NoError(t, err) + + kms := &MacKMS{} + p256 := createKey(t, t.Name()+"-p256", apiv1.ECDSAWithSHA256) + s256, err := kms.CreateSigner(&p256.CreateSignerRequest) + require.NoError(t, err) + p384 := createKey(t, t.Name()+"-p384", apiv1.ECDSAWithSHA384) + s384, err := kms.CreateSigner(&p384.CreateSignerRequest) + require.NoError(t, err) + p521 := createKey(t, t.Name()+"-p521", apiv1.ECDSAWithSHA512) + s521, err := kms.CreateSigner(&p521.CreateSignerRequest) + require.NoError(t, err) + + type fields struct { + Signer *Signer + } + type args struct { + pub *ecdh.PublicKey + } + tests := []struct { + name string + fields fields + args args + wantFunc func(t *testing.T, got []byte) + assertion assert.ErrorAssertionFunc + }{ + {"ok P256", fields{s256.(*Signer)}, args{goP256.PublicKey()}, func(t *testing.T, got []byte) { + pub, ok := s256.Public().(*ecdsa.PublicKey) + require.True(t, ok) + ecdhPub, err := pub.ECDH() + require.NoError(t, err) + sharedSecret, err := goP256.ECDH(ecdhPub) + require.NoError(t, err) + assert.Equal(t, sharedSecret, got) + }, assert.NoError}, + {"ok P384", fields{s384.(*Signer)}, args{goP384.PublicKey()}, func(t *testing.T, got []byte) { + pub, ok := s384.Public().(*ecdsa.PublicKey) + require.True(t, ok) + ecdhPub, err := pub.ECDH() + require.NoError(t, err) + sharedSecret, err := goP384.ECDH(ecdhPub) + require.NoError(t, err) + assert.Equal(t, sharedSecret, got) + }, assert.NoError}, + {"ok P521", fields{s521.(*Signer)}, args{goP521.PublicKey()}, func(t *testing.T, got []byte) { + pub, ok := s521.Public().(*ecdsa.PublicKey) + require.True(t, ok) + ecdhPub, err := pub.ECDH() + require.NoError(t, err) + sharedSecret, err := goP521.ECDH(ecdhPub) + require.NoError(t, err) + assert.Equal(t, sharedSecret, got) + }, assert.NoError}, + {"fail missing", fields{&Signer{ + keyAttributes: &keyAttributes{tag: DefaultTag, label: t.Name() + "-missing"}, + }}, args{goP256.PublicKey()}, func(t *testing.T, got []byte) { + assert.Nil(t, got) + }, assert.Error}, + {"fail SecKeyCreateWithData", fields{s256.(*Signer)}, args{goX25519.PublicKey()}, func(t *testing.T, got []byte) { + assert.Nil(t, got) + }, assert.Error}, + {"fail SecKeyCopyKeyExchangeResult", fields{s256.(*Signer)}, args{goP384.PublicKey()}, func(t *testing.T, got []byte) { + assert.Nil(t, got) + }, assert.Error}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + e := &ECDH{ + Signer: tt.fields.Signer, + } + got, err := e.ECDH(tt.args.pub) + tt.assertion(t, err) + tt.wantFunc(t, got) + }) + } +} + +func TestECDH_Curve(t *testing.T) { + kms := &MacKMS{} + p256 := createKey(t, t.Name()+"-p256", apiv1.ECDSAWithSHA256) + s256, err := kms.CreateSigner(&p256.CreateSignerRequest) + require.NoError(t, err) + p384 := createKey(t, t.Name()+"-p384", apiv1.ECDSAWithSHA384) + s384, err := kms.CreateSigner(&p384.CreateSignerRequest) + require.NoError(t, err) + p521 := createKey(t, t.Name()+"-p521", apiv1.ECDSAWithSHA512) + s521, err := kms.CreateSigner(&p521.CreateSignerRequest) + require.NoError(t, err) + + rsaKey := createKey(t, t.Name()+"-rsa", apiv1.SHA256WithRSA) + rsaSigmer, err := kms.CreateSigner(&rsaKey.CreateSignerRequest) + require.NoError(t, err) + + p224, err := ecdsa.GenerateKey(elliptic.P224(), rand.Reader) + require.NoError(t, err) + + type fields struct { + Signer *Signer + } + tests := []struct { + name string + fields fields + want ecdh.Curve + }{ + {"P256", fields{s256.(*Signer)}, ecdh.P256()}, + {"P384", fields{s384.(*Signer)}, ecdh.P384()}, + {"P521", fields{s521.(*Signer)}, ecdh.P521()}, + {"P224", fields{&Signer{pub: p224.Public()}}, nil}, + {"RSA", fields{rsaSigmer.(*Signer)}, nil}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + e := &ECDH{ + Signer: tt.fields.Signer, + } + assert.Equal(t, tt.want, e.Curve()) + }) + } +} + +func TestECDH_PublicKey(t *testing.T) { + kms := &MacKMS{} + p256 := createKey(t, t.Name()+"-p256", apiv1.ECDSAWithSHA256) + s256, err := kms.CreateSigner(&p256.CreateSignerRequest) + require.NoError(t, err) + p384 := createKey(t, t.Name()+"-p384", apiv1.ECDSAWithSHA384) + s384, err := kms.CreateSigner(&p384.CreateSignerRequest) + require.NoError(t, err) + p521 := createKey(t, t.Name()+"-p521", apiv1.ECDSAWithSHA512) + s521, err := kms.CreateSigner(&p521.CreateSignerRequest) + require.NoError(t, err) + + rsaKey := createKey(t, t.Name()+"-rsa", apiv1.SHA256WithRSA) + rsaSigmer, err := kms.CreateSigner(&rsaKey.CreateSignerRequest) + require.NoError(t, err) + + p224, err := ecdsa.GenerateKey(elliptic.P224(), rand.Reader) + require.NoError(t, err) + + mustPublicKey := func(k crypto.PublicKey) *ecdh.PublicKey { + pub, ok := k.(*ecdsa.PublicKey) + require.True(t, ok) + ecdhPub, err := pub.ECDH() + require.NoError(t, err) + return ecdhPub + } + + type fields struct { + Signer *Signer + } + tests := []struct { + name string + fields fields + want *ecdh.PublicKey + }{ + {"P256", fields{s256.(*Signer)}, mustPublicKey(p256.PublicKey)}, + {"P384", fields{s384.(*Signer)}, mustPublicKey(p384.PublicKey)}, + {"P521", fields{s521.(*Signer)}, mustPublicKey(p521.PublicKey)}, + {"P224", fields{&Signer{pub: p224.Public()}}, nil}, + {"RSA", fields{rsaSigmer.(*Signer)}, nil}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + e := &ECDH{ + Signer: tt.fields.Signer, + } + assert.Equal(t, tt.want, e.PublicKey()) + }) + } +}