Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Start Always Encrypted feature branch #116

Merged
merged 50 commits into from
Aug 10, 2023
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
6ce6c4f
add core CEK parameters and types
shueybubbles Jun 7, 2023
1ba7c9a
add column encryption featureext
shueybubbles Jun 7, 2023
67d93fa
Add parsing of always encrypted tokens
shueybubbles Jun 9, 2023
f10e1c3
Merge branch 'main' of https://github.com/microsoft/go-mssqldb into f…
shueybubbles Jun 9, 2023
0cc1a7e
Add skeleton for AE test
shueybubbles Jun 12, 2023
282072d
implement local cert key provider
shueybubbles Jun 14, 2023
bcd9e1c
build fixes
shueybubbles Jun 14, 2023
1c7a2e3
use key providers for decrypt
shueybubbles Jun 14, 2023
fb7a081
refactor packages to avoid cycle
shueybubbles Jun 15, 2023
3083f56
initial code for AE result set query
shueybubbles Jun 22, 2023
fc53c14
skeleton of parameter encryption
shueybubbles Jun 28, 2023
ff797ce
implement EncryptColumnEncryptionKey for local cert
shueybubbles Jun 30, 2023
2e3ec3f
fix query for param encryption data
shueybubbles Jun 30, 2023
0fcb7ea
add cipher data to parameters
shueybubbles Jul 3, 2023
f936d90
Merge branch 'main' of https://github.com/microsoft/go-mssqldb into f…
shueybubbles Jul 5, 2023
2e75557
copy swisscom code locally
shueybubbles Jul 5, 2023
a98b1fd
implement Encrypt
shueybubbles Jul 6, 2023
0dcb602
don't claim to support enclaves
shueybubbles Jul 6, 2023
2346b5d
fix encrypt
shueybubbles Jul 10, 2023
c4bd2b1
close Rows when done
shueybubbles Jul 10, 2023
0d97e9e
fix bulk copy
shueybubbles Jul 10, 2023
954472a
fix return value
shueybubbles Jul 11, 2023
fc4e1d8
fix unnamed params to sprocs
shueybubbles Jul 11, 2023
9c6c679
update readme
shueybubbles Jul 11, 2023
45896d3
Remove allocations from unmarshalRSA
shueybubbles Jul 11, 2023
0dc231b
remove go-winio dependency
shueybubbles Jul 11, 2023
83e98f1
fix Scan to use correct data types
shueybubbles Jul 12, 2023
039dc28
fix encryption of more types
shueybubbles Jul 14, 2023
83edee4
try to fix appveyor build
shueybubbles Jul 17, 2023
0ae9b2d
mute test
shueybubbles Jul 17, 2023
689434d
make cert store provider go1.17+
shueybubbles Jul 17, 2023
396a5dd
fix build directives
shueybubbles Jul 17, 2023
b4ab997
rename files for clarity
shueybubbles Jul 17, 2023
25a5ebf
fix typo
shueybubbles Jul 17, 2023
643b7f1
fix test file directive
shueybubbles Jul 17, 2023
2c946e4
skip windows package get in pipeline
shueybubbles Jul 17, 2023
522314e
fix build
shueybubbles Jul 18, 2023
5c0dfc9
fix build breaks
shueybubbles Jul 18, 2023
44af0a5
update dependencies and min Go version
shueybubbles Jul 19, 2023
3b17cd6
update appveyor
shueybubbles Jul 19, 2023
64a993f
try older appveyor image
shueybubbles Jul 19, 2023
4c42fbf
no race on go 1.20
shueybubbles Jul 19, 2023
e2b1af1
Merge branch 'main' of https://github.com/microsoft/go-mssqldb into f…
shueybubbles Jul 19, 2023
8e171eb
update reviewdog
shueybubbles Jul 19, 2023
e2e907a
fix linter warnings
shueybubbles Jul 19, 2023
a66a566
more linter fixes
shueybubbles Jul 19, 2023
cdaec23
check err in test
shueybubbles Jul 19, 2023
9e0b61e
remove old SQL versions from PR build
shueybubbles Jul 19, 2023
2a41c82
check err in test
shueybubbles Jul 24, 2023
c53676e
fix unit tests
shueybubbles Jul 25, 2023
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
73 changes: 73 additions & 0 deletions aecmk/keyprovider.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package aecmk

import (
"fmt"
"time"
)

const (
CertificateStoreKeyProvider = "MSSQL_CERTIFICATE_STORE"
CspKeyProvider = "MSSQL_CSP_PROVIDER"
CngKeyProvider = "MSSQL_CNG_STORE"
AzureKeyVaultKeyProvider = "AZURE_KEY_VAULT"
JavaKeyProvider = "MSSQL_JAVA_KEYSTORE"
)

// ColumnEncryptionKeyLifetime is the default lifetime of decrypted Column Encryption Keys in the global cache.
// The default is 2 hours
var ColumnEncryptionKeyLifetime time.Duration = 2 * time.Hour

type CekCacheEntry struct {
Expiry time.Time
Key []byte
}

type CekCache map[string]CekCacheEntry

type CekProvider struct {
Provider ColumnEncryptionKeyProvider
DecryptedKeys CekCache
}

// no synchronization on this map. Providers register during init.
type ColumnEncryptionKeyProviderMap map[string]*CekProvider

var globalCekProviderFactoryMap = ColumnEncryptionKeyProviderMap{}

// ColumnEncryptionKeyProvider is the interface for decrypting and encrypting column encryption keys.
// It is similar to .Net https://learn.microsoft.com/dotnet/api/microsoft.data.sqlclient.sqlcolumnencryptionkeystoreprovider.
type ColumnEncryptionKeyProvider interface {
// DecryptColumnEncryptionKey decrypts the specified encrypted value of a column encryption key.
// The encrypted value is expected to be encrypted using the column master key with the specified key path and using the specified algorithm.
DecryptColumnEncryptionKey(masterKeyPath string, encryptionAlgorithm string, encryptedCek []byte) []byte
// EncryptColumnEncryptionKey encrypts a column encryption key using the column master key with the specified key path and using the specified algorithm.
EncryptColumnEncryptionKey(masterKeyPath string, encryptionAlgorithm string, cek []byte) []byte
// SignColumnMasterKeyMetadata digitally signs the column master key metadata with the column master key
// referenced by the masterKeyPath parameter. The input values used to generate the signature should be the
// specified values of the masterKeyPath and allowEnclaveComputations parameters. May return an empty slice if not supported.
SignColumnMasterKeyMetadata(masterKeyPath string, allowEnclaveComputations bool) []byte
// VerifyColumnMasterKeyMetadata verifies the specified signature is valid for the column master key
// with the specified key path and the specified enclave behavior. Return nil if not supported.
VerifyColumnMasterKeyMetadata(masterKeyPath string, allowEnclaveComputations bool) *bool
// KeyLifetime is an optional Duration. Keys fetched by this provider will be discarded after their lifetime expires.
// If it returns nil, the keys will expire based on the value of ColumnEncryptionKeyLifetime.
// If it returns zero, the keys will not be cached.
KeyLifetime() *time.Duration
}

func RegisterCekProvider(name string, provider ColumnEncryptionKeyProvider) error {
_, ok := globalCekProviderFactoryMap[name]
if ok {
return fmt.Errorf("CEK provider %s is already registered", name)
}
globalCekProviderFactoryMap[name] = &CekProvider{Provider: provider, DecryptedKeys: CekCache{}}
return nil
}

func GetGlobalCekProviders() (providers ColumnEncryptionKeyProviderMap) {
providers = make(ColumnEncryptionKeyProviderMap)
for i, p := range globalCekProviderFactoryMap {
providers[i] = p
}
return
}
169 changes: 169 additions & 0 deletions aecmk/localcert/keyprovider.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
package localcert

import (
"crypto/rsa"
"crypto/sha1"
"crypto/x509"
"fmt"
"io/ioutil"
"os"
"strconv"
"time"

"github.com/microsoft/go-mssqldb/aecmk"
ae "github.com/swisscom/mssql-always-encrypted/pkg"
pkcs "software.sslmate.com/src/go-pkcs12"
)

const (
PfxKeyProviderName = "pfx"
wildcard = "*"
)

// LocalCertProvider uses local certificates to decrypt CEKs
// It supports both 'MSSQL_CERTIFICATE_STORE' and 'pfx' key stores.
// MSSQL_CERTIFICATE_STORE key paths are of the form `storename/storepath/thumbprint` and only supported on Windows clients.
// pfx key paths are absolute file system paths that are operating system dependent.
type LocalCertProvider struct {
// Name identifies which key store the provider supports.
name string
// AllowedLocations constrains which locations the provider will use to find certificates. If empty, all locations are allowed.
// When presented with a key store path not in the allowed list, the data will be returned still encrypted.
AllowedLocations []string
passwords map[string]string
}

// SetCertificatePassword stores the password associated with the certificate at the given location.
// If location is empty the given password applies to all certificates that have not been explicitly assigned a value.
func (p LocalCertProvider) SetCertificatePassword(location string, password string) {
if location == "" {
location = wildcard
}
p.passwords[location] = password
}

var PfxKeyProvider = LocalCertProvider{name: PfxKeyProviderName, passwords: make(map[string]string), AllowedLocations: make([]string, 0)}

func init() {
aecmk.RegisterCekProvider("pfx", &PfxKeyProvider)
}

// DecryptColumnEncryptionKey decrypts the specified encrypted value of a column encryption key.
// The encrypted value is expected to be encrypted using the column master key with the specified key path and using the specified algorithm.
func (p *LocalCertProvider) DecryptColumnEncryptionKey(masterKeyPath string, encryptionAlgorithm string, encryptedCek []byte) (decryptedKey []byte) {
decryptedKey = nil
allowed := len(p.AllowedLocations) == 0
if !allowed {
loop:
for _, l := range p.AllowedLocations {
if l == masterKeyPath {
allowed = true
break loop
}
}
}
if !allowed {
return
}
var cert *x509.Certificate
var pk interface{}
switch p.name {
case PfxKeyProviderName:
pk, cert = p.loadLocalCertificate(masterKeyPath)
case aecmk.CertificateStoreKeyProvider:
pk, cert = p.loadWindowsCertStoreCertificate(masterKeyPath)
default:
return
}
cekv := ae.LoadCEKV(encryptedCek)
if !cekv.Verify(cert) {
panic(fmt.Errorf("Invalid certificate provided for decryption. Key Store Path: %s. <%s>-<%v>", masterKeyPath, cekv.KeyPath, fmt.Sprintf("%02x", sha1.Sum(cert.Raw))))
}

decryptedKey, err := cekv.Decrypt(pk.(*rsa.PrivateKey))
if err != nil {
panic(err)
}
return
}

func (p *LocalCertProvider) loadLocalCertificate(path string) (privateKey interface{}, cert *x509.Certificate) {
if f, err := os.Open(path); err == nil {
pfxBytes, err := ioutil.ReadAll(f)
if err != nil {
panic(invalidCertificatePath(path, err))
}
pwd, ok := p.passwords[path]
if !ok {
pwd, ok = p.passwords[wildcard]
if !ok {
pwd = ""
}
}
privateKey, cert, err = pkcs.Decode(pfxBytes, pwd)
if err != nil {
panic(err)
}
} else {
panic(invalidCertificatePath(path, err))
}
return
}

// EncryptColumnEncryptionKey encrypts a column encryption key using the column master key with the specified key path and using the specified algorithm.
func (p *LocalCertProvider) EncryptColumnEncryptionKey(masterKeyPath string, encryptionAlgorithm string, cek []byte) []byte {
return nil
}

// SignColumnMasterKeyMetadata digitally signs the column master key metadata with the column master key
// referenced by the masterKeyPath parameter. The input values used to generate the signature should be the
// specified values of the masterKeyPath and allowEnclaveComputations parameters. May return an empty slice if not supported.
func (p *LocalCertProvider) SignColumnMasterKeyMetadata(masterKeyPath string, allowEnclaveComputations bool) []byte {
return nil
}

// VerifyColumnMasterKeyMetadata verifies the specified signature is valid for the column master key
// with the specified key path and the specified enclave behavior. Return nil if not supported.
func (p *LocalCertProvider) VerifyColumnMasterKeyMetadata(masterKeyPath string, allowEnclaveComputations bool) *bool {
return nil
}

// KeyLifetime is an optional Duration. Keys fetched by this provider will be discarded after their lifetime expires.
// If it returns nil, the keys will expire based on the value of ColumnEncryptionKeyLifetime.
// If it returns zero, the keys will not be cached.
func (p *LocalCertProvider) KeyLifetime() *time.Duration {
return nil
}

// InvalidCertificatePathError indicates the provided path could not be used to load a certificate
type InvalidCertificatePathError struct {
path string
innerErr error
}

func (i *InvalidCertificatePathError) Error() string {
return fmt.Sprintf("Invalid certificate path: %s", i.path)
}

func (i *InvalidCertificatePathError) Unwrap() error {
return i.innerErr
}

func invalidCertificatePath(path string, err error) error {
return &InvalidCertificatePathError{path: path, innerErr: err}
}

func thumbprintToByteArray(thumbprint string) []byte {
if len(thumbprint)%2 != 0 {
panic(fmt.Errorf("Thumbprint must have even length %s", thumbprint))
}
bytes := make([]byte, len(thumbprint)/2)
for i := range bytes {
b, err := strconv.ParseInt(thumbprint[i*2:(i*2)+2], 16, 32)
if err != nil {
panic(err)
}
bytes[i] = byte(b)
}
return bytes
}
11 changes: 11 additions & 0 deletions aecmk/localcert/keyprovider_darwin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package localcert

import (
"crypto/x509"
"fmt"
)

func (p *LocalCertProvider) loadWindowsCertStoreCertificate(path string) (privateKey interface{}, cert *x509.Certificate) {
panic(fmt.Errorf("Windows cert store not supported on this OS"))
return
}
11 changes: 11 additions & 0 deletions aecmk/localcert/keyprovider_linux.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package localcert

import (
"crypto/x509"
"fmt"
)

func (p *LocalCertProvider) loadWindowsCertStoreCertificate(path string) (privateKey interface{}, cert *x509.Certificate) {
panic(fmt.Errorf("Windows cert store not supported on this OS"))
return
}
15 changes: 15 additions & 0 deletions aecmk/localcert/keyprovider_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package localcert

import (
"bytes"
"encoding/hex"
"testing"
)

func TestThumbPrintToSignature(t *testing.T) {
thumbprint := "5e89a107f0ade0aed5f753ecc60378b1bbae3598"
signature := thumbprintToByteArray(thumbprint)
if !bytes.Equal(signature, []byte{0x5e, 0x89, 0xa1, 0x07, 0xf0, 0xad, 0xe0, 0xae, 0xd5, 0xf7, 0x53, 0xec, 0xc6, 0x03, 0x78, 0xb1, 0xbb, 0xae, 0x35, 0x98}) {
t.Fatalf("Incorrect signature bytes for %s. Got: %s", thumbprint, hex.Dump(signature))
}
}
51 changes: 51 additions & 0 deletions aecmk/localcert/keyprovider_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package localcert

import (
"crypto/x509"
"fmt"
"strings"
"unsafe"

"github.com/microsoft/go-mssqldb/aecmk"
"github.com/microsoft/go-mssqldb/internal/certs"
"golang.org/x/sys/windows"
)

var WindowsCertificateStoreKeyProvider = LocalCertProvider{name: aecmk.CertificateStoreKeyProvider, passwords: make(map[string]string)}

func init() {
aecmk.RegisterCekProvider(aecmk.CertificateStoreKeyProvider, &WindowsCertificateStoreKeyProvider)
}

func (p *LocalCertProvider) loadWindowsCertStoreCertificate(path string) (privateKey interface{}, cert *x509.Certificate) {
privateKey = nil
cert = nil
pathParts := strings.Split(path, `/`)
if len(pathParts) != 3 {
panic(invalidCertificatePath(path, fmt.Errorf("key store path requires 3 segments")))
}

var storeId uint32
switch strings.ToLower(pathParts[0]) {
case "localmachine":
storeId = windows.CERT_SYSTEM_STORE_LOCAL_MACHINE
case "currentuser":
storeId = windows.CERT_SYSTEM_STORE_CURRENT_USER
default:
panic(invalidCertificatePath(path, fmt.Errorf("Unknown certificate store")))
}
system, err := windows.UTF16PtrFromString(pathParts[1])
if err != nil {
panic(err)
}
h, err := windows.CertOpenStore(windows.CERT_STORE_PROV_SYSTEM,
windows.PKCS_7_ASN_ENCODING|windows.X509_ASN_ENCODING,
0,
storeId, uintptr(unsafe.Pointer(system)))
if err != nil {
panic(err)
}
defer windows.CertCloseStore(h, 0)
signature := thumbprintToByteArray(pathParts[2])
return certs.FindCertBySignatureHash(h, signature)
}
30 changes: 30 additions & 0 deletions aecmk/localcert/keyprovider_windows_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package localcert

import (
"crypto/rsa"
"strings"
"testing"

"github.com/microsoft/go-mssqldb/aecmk"
"github.com/microsoft/go-mssqldb/internal/certs"
)

func TestLoadWindowsCertStoreCertificate(t *testing.T) {
thumbprint, err := certs.ProvisionMasterKeyInCertStore()
if err != nil {
t.Fatal(err)
}
defer certs.DeleteMasterKeyCert(thumbprint)
provider := aecmk.GetGlobalCekProviders()[aecmk.CertificateStoreKeyProvider].Provider.(*LocalCertProvider)
pk, cert := provider.loadWindowsCertStoreCertificate("CurrentUser/My/" + thumbprint)
switch z := pk.(type) {
case *rsa.PrivateKey:

t.Logf("Got an rsa.PrivateKey with size %d", z.Size())
default:
t.Fatalf("Unexpected private key type: %v", z)
}
if !strings.HasPrefix(cert.Subject.String(), `CN=gomssqltest-`) {
t.Fatalf("Wrong cert loaded: %s", cert.Subject.String())
}
}
Loading