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

feat: add Always Encrypted support #637

Open
wants to merge 4 commits into
base: master
Choose a base branch
from

Conversation

denysvitali
Copy link

This commit adds partial support for the Microsoft SQL "Always Encrypted" feature (basically, E2E encryption).

The current implementation is to be consider a preview since it might not be perfectly implemented.

Supported features:

  • PFX "keystore"
  • Seamless encryption

Missing features:

  • Support for Private Keys that are not RSA
  • Encryption support (only Decryption is possible at the moment)

The most probably needs to be improved a bit, but so far it's working for some of the use cases that I needed it for.

Feel free to test the feature and open an issue if you find any problem: my goal is to have enough testers to spot eventual bugs.

Fixes #265

This commit adds partial support for the Microsoft SQL
"Always Encrypted" feature (basically, E2E encryption).

The current implementation is to be consider a __preview__
since it might not be perfectly implemented.

Supported features:
- PFX "keystore"
- Seamless encryption

Missing features:
- Support for Private Keys that are not RSA
- Encryption support (only Decryption is possible at the moment)

The most probably needs to be improved a bit, but so far it's
working for some of the use cases that I needed it for.
Feel free to test the feature and open an issue if you find any
problem: my goal is to have enough testers to spot eventual bugs.

fix: lint issues
Denys Vitali added 3 commits February 11, 2021 14:33
fix(alwaysEncrypted): fix NBCRow

feat(go.mod): update mssql-always-encrypted to v0.1.3
Copy link
Collaborator

@kardianos kardianos left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is unlikely to go further unless you bring the encryption into this package. I would recommend opening a new PR for that if you are interested in that.

I would also want to start with encryption parameters in the Connector.


db, err := sql.Open("sqlserver", u.String())
if err != nil {
logrus.Fatalf("unable to open db: %v", err)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this example, use return fmt.Errorf("unable to open db: %w", err) throughout.

}
rows, err := db.Query("SELECT id, ssn FROM [dbo].[cid]")
if err != nil {
logrus.Fatalf("unable to perform query: %v", err)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above.

if err != nil {
logrus.Fatalf("unable to open db: %v", err)
}
rows, err := db.Query("SELECT id, ssn FROM [dbo].[cid]")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove [ and ].

logrus.Fatalf("unable to perform query: %v", err)
}

for ; rows.Next(); {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In Go, there are three for loop variants: 0 argument (loop forever), 1 argument (test condition), and 3 argument.

You want the 1 argument version here: for rows.Next() {

}
err = rows.Scan(&dest.Id, &dest.SSN)
if err != nil {
logrus.Fatalf("unable to scan into struct: %v", err)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above.

defer rows.Close()

if err != nil {
t.Fatalf("unable to query db: %s", err)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The verb should be %w, not %s.

import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove. We don't need an assert package.

@@ -0,0 +1,29 @@
package mssql

type cekTable struct {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is cek mean? A comment or full name would be good.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Column Encryption Key, this is used everywhere in Microsoft's documentation. Not sure if it's a good idea to make it longer here since cek is mentioned everywhere in the codebase now

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, then above type cekTable struct {, put

// Column Encryption Key table that ...


type cekTableEntry struct {
databaseID int
keyId int
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

KeyID

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why should this be exported?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh nevermind, you were referring to the ID part 😅

github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe
github.com/stretchr/testify v1.7.0
github.com/swisscom/mssql-always-encrypted v0.1.3
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a non-starter.

If you want to bring this into this repo for review, we can do that. But we won't be adding an external repo like this.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense, but I wanted to keep the two things separated to not clog too much this repo, especially since one package is feature complete (encryption /+ decryption) but here we only support encryption for now.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would belong in an internal/ package, but not a different repo.

@denysvitali
Copy link
Author

Thanks for the review! I'll try to find the time to improve the PR

@lokmannicholas
Copy link

any idea how to Insert or update the encrypted data?

@shueybubbles
Copy link
Contributor

@denysvitali are you interested in re-homing this PR in our fork? https://github.com/microsoft/go-mssqldb

@denysvitali
Copy link
Author

Sure!

}

// TODO: Support other private keys
rootKey, err := cekv.Decrypt(s.alwaysEncryptedSettings.pKey.(*rsa.PrivateKey))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as we consider porting this over to the microsoft fork - how can we factor out the key providers into separate packages and make the core agnostic to which key provider is chosen?

IE, we want to have an app to be able to import whichever key provider package they want, and we would implement the AKV provider package and perhaps the local PFX file provider.
Then the connection string can specify the name of the key provider along with its authentication parameters. Each provider would be responsible for parsing its auth parameters from the connection string.

@shueybubbles
Copy link
Contributor

I've started a more extensive AE implementation in the Microsoft fork and welcome feedback. We're starting with decryption using local certs or Azure Key Vault then expanding to encryption. microsoft#116

panic(err)
}

pk, cert, err := pkcs12.Decode(pfxBytes, s.alwaysEncryptedSettings.ksSecret)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cs12.Decode(pfxByte

@denysvitali Hi Denys. I am experimenting with supporting the Windows certificate store to try to get certificate bytes and private key bytes to feed into your existing decryption code.
Unfortunately the encoded cert bytes referenced in a windows CERT_CONTEXT structure are not the same as the contents of the pfx file so pkcs12.Decode fails:

panic: pkcs12: error reading P12 data: asn1: structure error: tags don't match (2 vs {class:0 tag:16 length:559 isCompound:true}) {optional:false explicit:false application:false private:false defaultValue:<nil> tag:<nil> stringType:0 timeType:0 set:false omitEmpty:false} int @4 [recovered]

Do you happen to have some insight on the best way to get the raw bytes of the private key from the Windows cryptapi ?

I can get the x509Certificate thusly:

func certContextToX509(certHandle *windows.CertContext) (*x509.Certificate, error) {
 var der []byte
 slice := (*reflect.SliceHeader)(unsafe.Pointer(&der))
 slice.Data = uintptr(unsafe.Pointer(certHandle.EncodedCert))
 slice.Len = int(certHandle.Length)
 slice.Cap = int(certHandle.Length)
 return x509.ParseCertificate(der)
}

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not a Windows user, but I recall Microsoft actually preventing in some cases to export the private key (I think there is a flag somewhere).

Maybe this is the case for you (or you get an encrypted version of it) and thus you cannot parse it :(

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh ncryptexportkey looks promising...

@shueybubbles
Copy link
Contributor

@denysvitali @kardianos Does go-mssqldb have an equivalent function for converting the []namedValue slice to a comma delimited string like .Net at https://github.com/dotnet/SqlClient/blob/2b31810ce69b88d707450e2059ee8fbde63f774f/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.cs#L6150 ?
For encrypting parameters I need to build a query to sp_describe_parameter_encryption which has the list of named parameters and their types like this.

exec sp_describe_parameter_encryption N'DECLARE @c1 AS NCHAR (10) = @p3ee82b8b457c44498ce61053237ba60e;
DECLARE @c2 AS INT = @p643017f69684494b9143f6de7c9078b1;
INSERT  INTO Table_1
VALUES (@c1, @c2);
',N'@p3ee82b8b457c44498ce61053237ba60e nchar(10),@p643017f69684494b9143f6de7c9078b1 int'

@shueybubbles
Copy link
Contributor

@denysvitali I've got a PR for both encryption and decryption in the Microsoft fork at microsoft#116.
It handles both pfx files and the windows certificate store, though I haven't yet written test automation for the E2E of the pfx file-based operations.
I welcome folks like you to grab that branch and try out your scenarios and to give input on the PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

feature: enable column encryption at the TDS level
4 participants