...

Source file src/go.mongodb.org/mongo-driver/x/mongo/driver/mongocrypt/mongocrypt_test.go

Documentation: go.mongodb.org/mongo-driver/x/mongo/driver/mongocrypt

     1  // Copyright (C) MongoDB, Inc. 2017-present.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License"); you may
     4  // not use this file except in compliance with the License. You may obtain
     5  // a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
     6  
     7  //go:build cse
     8  // +build cse
     9  
    10  package mongocrypt
    11  
    12  import (
    13  	"bufio"
    14  	"bytes"
    15  	"io/ioutil"
    16  	"os"
    17  	"path"
    18  	"strings"
    19  	"testing"
    20  
    21  	"go.mongodb.org/mongo-driver/bson"
    22  	"go.mongodb.org/mongo-driver/bson/primitive"
    23  	"go.mongodb.org/mongo-driver/x/bsonx/bsoncore"
    24  	"go.mongodb.org/mongo-driver/x/mongo/driver/mongocrypt/options"
    25  )
    26  
    27  const resourcesDir = "../../../../testdata/mongocrypt"
    28  
    29  func noerr(t *testing.T, err error) {
    30  	if err != nil {
    31  		t.Helper()
    32  		t.Errorf("Unexpected error: (%T)%v", err, err)
    33  		t.FailNow()
    34  	}
    35  }
    36  
    37  func compareStates(t *testing.T, expected, actual State) {
    38  	t.Helper()
    39  	if expected != actual {
    40  		t.Fatalf("state mismatch; expected %s, got %s", expected, actual)
    41  	}
    42  }
    43  
    44  func compareResources(t *testing.T, expected, actual bsoncore.Document) {
    45  	t.Helper()
    46  	if !bytes.Equal(expected, actual) {
    47  		t.Fatalf("resource mismatch; expected %v, got %v", expected, actual)
    48  	}
    49  }
    50  
    51  func createMongoCrypt(t *testing.T) *MongoCrypt {
    52  	t.Helper()
    53  
    54  	awsProvider := bsoncore.NewDocumentBuilder().
    55  		AppendString("accessKeyId", "example").
    56  		AppendString("secretAccessKey", "example").
    57  		Build()
    58  	localProvider := bsoncore.NewDocumentBuilder().
    59  		AppendBinary("key", 0, make([]byte, 96)).
    60  		Build()
    61  	kmsProviders := bsoncore.NewDocumentBuilder().
    62  		AppendDocument("aws", awsProvider).
    63  		AppendDocument("local", localProvider).
    64  		Build()
    65  
    66  	cryptOpts := options.MongoCrypt().SetKmsProviders(kmsProviders)
    67  	crypt, err := NewMongoCrypt(cryptOpts)
    68  	noerr(t, err)
    69  	if crypt == nil {
    70  		t.Fatalf("expected MongoCrypt instance but got nil")
    71  	}
    72  	return crypt
    73  }
    74  
    75  func resourceToDocument(t *testing.T, filename string) bsoncore.Document {
    76  	t.Helper()
    77  	filepath := path.Join(resourcesDir, filename)
    78  	content, err := ioutil.ReadFile(filepath)
    79  	noerr(t, err)
    80  
    81  	var doc bsoncore.Document
    82  	noerr(t, bson.UnmarshalExtJSON(content, false, &doc))
    83  	return doc
    84  }
    85  
    86  func httpResponseToBytes(t *testing.T, filename string) []byte {
    87  	t.Helper()
    88  	file, err := os.Open(path.Join(resourcesDir, filename))
    89  	noerr(t, err)
    90  	defer func() {
    91  		_ = file.Close()
    92  	}()
    93  
    94  	firstLine := true
    95  	var fileStr string
    96  	scanner := bufio.NewScanner(file)
    97  	for scanner.Scan() {
    98  		if !firstLine {
    99  			fileStr += "\r\n"
   100  		}
   101  		firstLine = false
   102  		fileStr += scanner.Text()
   103  	}
   104  	noerr(t, scanner.Err())
   105  
   106  	return []byte(fileStr)
   107  }
   108  
   109  // iterate a context in the NeedKms state
   110  func testKmsCtx(t *testing.T, ctx *Context, keyAltName bool) {
   111  	// get op to send to key vault
   112  	keyFilter, err := ctx.NextOperation()
   113  	noerr(t, err)
   114  	filterFile := "key-filter.json"
   115  	if keyAltName {
   116  		filterFile = "key-filter-keyAltName.json"
   117  	}
   118  	compareResources(t, resourceToDocument(t, filterFile), keyFilter)
   119  
   120  	// feed result and finish op
   121  	noerr(t, ctx.AddOperationResult(resourceToDocument(t, "key-document.json")))
   122  	noerr(t, ctx.CompleteOperation())
   123  	compareStates(t, NeedKms, ctx.State())
   124  
   125  	// verify KMS hostname
   126  	kmsCtx := ctx.NextKmsContext()
   127  	hostname, err := kmsCtx.HostName()
   128  	noerr(t, err)
   129  
   130  	// TODO GODRIVER-2217: Simply check if hostname != expectedHost once all OSes build the latest libmongocrypt
   131  	// TODO versions.
   132  	//
   133  	// Only check for the hostname. libmongocrypt versions that do not include MONGOCRYPT-352 will not
   134  	// include the default port "443".
   135  	expectedHost := "kms.us-east-1.amazonaws.com"
   136  	if !strings.Contains(hostname, expectedHost) {
   137  		t.Fatalf("hostname mismatch; expected %s to contain %s", hostname, expectedHost)
   138  	}
   139  
   140  	// get message to send to KMS
   141  	kmsMsg, err := kmsCtx.Message()
   142  	noerr(t, err)
   143  	if len(kmsMsg) != 790 {
   144  		t.Fatalf("message length mismatch; expected 790, got %d", len(kmsMsg))
   145  	}
   146  
   147  	// feed mock KMS response
   148  	bytesNeeded := kmsCtx.BytesNeeded()
   149  	if bytesNeeded != 1024 {
   150  		t.Fatalf("number of bytes mismatch; expected 1024, got %d", bytesNeeded)
   151  	}
   152  	noerr(t, kmsCtx.FeedResponse(httpResponseToBytes(t, "kms-reply.txt")))
   153  	bytesNeeded = kmsCtx.BytesNeeded()
   154  	if bytesNeeded != 0 {
   155  		t.Fatalf("number of bytes mismatch; expected 0, got %d", bytesNeeded)
   156  	}
   157  
   158  	// verify that there are no more KMS contexts left
   159  	kmsCtx = ctx.NextKmsContext()
   160  	if kmsCtx != nil {
   161  		t.Fatalf("expected nil but got a KmsContext")
   162  	}
   163  	noerr(t, ctx.FinishKmsContexts())
   164  }
   165  
   166  func TestMongoCrypt(t *testing.T) {
   167  	t.Run("encrypt", func(t *testing.T) {
   168  		t.Run("remote schema", func(t *testing.T) {
   169  			crypt := createMongoCrypt(t)
   170  			defer crypt.Close()
   171  
   172  			// create encryption context and check initial state
   173  			cmdDoc := resourceToDocument(t, "command.json")
   174  			encryptCtx, err := crypt.CreateEncryptionContext("test", cmdDoc)
   175  			noerr(t, err)
   176  			defer encryptCtx.Close()
   177  			compareStates(t, NeedMongoCollInfo, encryptCtx.State())
   178  
   179  			// get listCollections op
   180  			listCollFilter, err := encryptCtx.NextOperation()
   181  			noerr(t, err)
   182  			compareResources(t, resourceToDocument(t, "list-collections-filter.json"), listCollFilter)
   183  
   184  			// feed result and finish op
   185  			noerr(t, encryptCtx.AddOperationResult(resourceToDocument(t, "collection-info.json")))
   186  			noerr(t, encryptCtx.CompleteOperation())
   187  			compareStates(t, NeedMongoMarkings, encryptCtx.State())
   188  
   189  			// get mongocryptd op
   190  			mongocryptdCmd, err := encryptCtx.NextOperation()
   191  			noerr(t, err)
   192  			compareResources(t, resourceToDocument(t, "mongocryptd-command-remote.json"), mongocryptdCmd)
   193  
   194  			// feed result and finish op
   195  			noerr(t, encryptCtx.AddOperationResult(resourceToDocument(t, "mongocryptd-reply.json")))
   196  			noerr(t, encryptCtx.CompleteOperation())
   197  			compareStates(t, NeedMongoKeys, encryptCtx.State())
   198  
   199  			// mock KMS communication and iterate encryptCtx
   200  			testKmsCtx(t, encryptCtx, false)
   201  			compareStates(t, Ready, encryptCtx.State())
   202  
   203  			// perform final encryption
   204  			encryptedDoc, err := encryptCtx.Finish()
   205  			noerr(t, err)
   206  			compareResources(t, resourceToDocument(t, "encrypted-command.json"), encryptedDoc)
   207  		})
   208  		t.Run("local schema", func(t *testing.T) {
   209  			// take schema from collection info and create MongoCrypt instance
   210  			collInfo := resourceToDocument(t, "collection-info.json")
   211  			schema := collInfo.Lookup("options", "validator", "$jsonSchema").Document()
   212  			schemaMap := map[string]bsoncore.Document{
   213  				"test.test": schema,
   214  			}
   215  			kmsProviders := bsoncore.NewDocumentBuilder().
   216  				StartDocument("aws").
   217  				AppendString("accessKeyId", "example").
   218  				AppendString("secretAccessKey", "example").
   219  				FinishDocument().
   220  				Build()
   221  			cryptOpts := options.MongoCrypt().SetKmsProviders(kmsProviders).SetLocalSchemaMap(schemaMap)
   222  			crypt, err := NewMongoCrypt(cryptOpts)
   223  			noerr(t, err)
   224  			defer crypt.Close()
   225  
   226  			// create encryption context and check initial state
   227  			encryptCtx, err := crypt.CreateEncryptionContext("test", resourceToDocument(t, "command.json"))
   228  			noerr(t, err)
   229  			defer encryptCtx.Close()
   230  			compareStates(t, NeedMongoMarkings, encryptCtx.State())
   231  
   232  			// get mongocryptd op
   233  			mongocryptdCmd, err := encryptCtx.NextOperation()
   234  			noerr(t, err)
   235  			compareResources(t, resourceToDocument(t, "mongocryptd-command-local.json"), mongocryptdCmd)
   236  
   237  			// feed result and finish op
   238  			noerr(t, encryptCtx.AddOperationResult(resourceToDocument(t, "mongocryptd-reply.json")))
   239  			noerr(t, encryptCtx.CompleteOperation())
   240  			compareStates(t, NeedMongoKeys, encryptCtx.State())
   241  
   242  			// mock KMS communication and iterate encryptCtx
   243  			testKmsCtx(t, encryptCtx, false)
   244  			compareStates(t, Ready, encryptCtx.State())
   245  
   246  			// perform final encryption
   247  			encryptedDoc, err := encryptCtx.Finish()
   248  			noerr(t, err)
   249  			compareResources(t, resourceToDocument(t, "encrypted-command.json"), encryptedDoc)
   250  		})
   251  		t.Run("invalid bson", func(t *testing.T) {
   252  			crypt := createMongoCrypt(t)
   253  			defer crypt.Close()
   254  
   255  			_, err := crypt.CreateEncryptionContext("test", []byte{0x1, 0x2, 0x3})
   256  			if err == nil {
   257  				t.Fatalf("expected error creating encryption context for invalid BSON but got nil")
   258  			}
   259  			if _, ok := err.(Error); !ok {
   260  				t.Fatalf("error type mismatch; expected Error, got %v", err)
   261  			}
   262  		})
   263  	})
   264  	t.Run("decrypt", func(t *testing.T) {
   265  		crypt := createMongoCrypt(t)
   266  		defer crypt.Close()
   267  
   268  		// create decryption context and check initial state
   269  		decryptCtx, err := crypt.CreateDecryptionContext(resourceToDocument(t, "encrypted-command-reply.json"))
   270  		noerr(t, err)
   271  		defer decryptCtx.Close()
   272  		compareStates(t, NeedMongoKeys, decryptCtx.State())
   273  
   274  		// mock KMS communication and iterate decryptCtx
   275  		testKmsCtx(t, decryptCtx, false)
   276  		compareStates(t, Ready, decryptCtx.State())
   277  
   278  		// perform final decryption
   279  		decryptedDoc, err := decryptCtx.Finish()
   280  		noerr(t, err)
   281  		compareResources(t, resourceToDocument(t, "command-reply.json"), decryptedDoc)
   282  	})
   283  	t.Run("data key creation", func(t *testing.T) {
   284  		crypt := createMongoCrypt(t)
   285  		defer crypt.Close()
   286  
   287  		// create master key document
   288  		var midx int32
   289  		var masterKey bsoncore.Document
   290  		midx, masterKey = bsoncore.AppendDocumentStart(nil)
   291  		masterKey, _ = bsoncore.AppendDocumentEnd(masterKey, midx)
   292  
   293  		// create data key context and check initial state
   294  		dataKeyOpts := options.DataKey().SetMasterKey(masterKey)
   295  		dataKeyCtx, err := crypt.CreateDataKeyContext("local", dataKeyOpts)
   296  		noerr(t, err)
   297  		defer dataKeyCtx.Close()
   298  		compareStates(t, Ready, dataKeyCtx.State())
   299  
   300  		// create data key
   301  		dataKeyDoc, err := dataKeyCtx.Finish()
   302  		noerr(t, err)
   303  		if len(dataKeyDoc) == 0 {
   304  			t.Fatalf("expected data key document but got empty doc")
   305  		}
   306  		compareStates(t, Done, dataKeyCtx.State())
   307  	})
   308  	t.Run("explicit roundtrip", func(t *testing.T) {
   309  		// algorithm to use for encryption/decryption
   310  		algorithm := "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic"
   311  		// doc to encrypt
   312  		idx, originalDoc := bsoncore.AppendDocumentStart(nil)
   313  		originalDoc = bsoncore.AppendStringElement(originalDoc, "v", "hello")
   314  		originalDoc, _ = bsoncore.AppendDocumentEnd(originalDoc, idx)
   315  
   316  		t.Run("no keyAltName", func(t *testing.T) {
   317  			crypt := createMongoCrypt(t)
   318  			defer crypt.Close()
   319  
   320  			// create explicit encryption context and check initial state
   321  			keyID := primitive.Binary{
   322  				Subtype: 0x04, // 0x04 is UUID subtype
   323  				Data:    []byte("aaaaaaaaaaaaaaaa"),
   324  			}
   325  			opts := options.ExplicitEncryption().SetKeyID(keyID).SetAlgorithm(algorithm)
   326  			encryptCtx, err := crypt.CreateExplicitEncryptionContext(originalDoc, opts)
   327  			noerr(t, err)
   328  			defer encryptCtx.Close()
   329  			compareStates(t, NeedMongoKeys, encryptCtx.State())
   330  
   331  			// mock KMS communication and iterate encryptCtx
   332  			testKmsCtx(t, encryptCtx, false)
   333  			compareStates(t, Ready, encryptCtx.State())
   334  
   335  			// perform final encryption
   336  			encryptedDoc, err := encryptCtx.Finish()
   337  			noerr(t, err)
   338  			compareStates(t, Done, encryptCtx.State())
   339  			compareResources(t, resourceToDocument(t, "encrypted-value.json"), encryptedDoc)
   340  
   341  			// create explicit decryption context and check initial state
   342  			decryptCtx, err := crypt.CreateDecryptionContext(encryptedDoc)
   343  			noerr(t, err)
   344  			defer decryptCtx.Close()
   345  			compareStates(t, Ready, decryptCtx.State())
   346  
   347  			// perform final decryption
   348  			decryptedDoc, err := decryptCtx.Finish()
   349  			noerr(t, err)
   350  			compareStates(t, Done, decryptCtx.State())
   351  			compareResources(t, originalDoc, decryptedDoc)
   352  		})
   353  		t.Run("keyAltName", func(t *testing.T) {
   354  			crypt := createMongoCrypt(t)
   355  			defer crypt.Close()
   356  
   357  			// create explicit encryption context and check initial state
   358  			opts := options.ExplicitEncryption().SetKeyAltName("altKeyName").SetAlgorithm(algorithm)
   359  			encryptCtx, err := crypt.CreateExplicitEncryptionContext(originalDoc, opts)
   360  			noerr(t, err)
   361  			defer encryptCtx.Close()
   362  			compareStates(t, NeedMongoKeys, encryptCtx.State())
   363  
   364  			// mock KMS communication and iterate encryptCtx
   365  			testKmsCtx(t, encryptCtx, true)
   366  			compareStates(t, Ready, encryptCtx.State())
   367  
   368  			// perform final encryption
   369  			encryptedDoc, err := encryptCtx.Finish()
   370  			noerr(t, err)
   371  			compareStates(t, Done, encryptCtx.State())
   372  			compareResources(t, resourceToDocument(t, "encrypted-value.json"), encryptedDoc)
   373  
   374  			// create explicit decryption context and check initial state
   375  			// the cryptCtx should automatically be in the Ready state because the key should be cached from the
   376  			// encryption process.
   377  			decryptCtx, err := crypt.CreateExplicitDecryptionContext(encryptedDoc)
   378  			noerr(t, err)
   379  			defer decryptCtx.Close()
   380  			compareStates(t, Ready, decryptCtx.State())
   381  
   382  			// perform final decryption
   383  			decryptedDoc, err := decryptCtx.Finish()
   384  			noerr(t, err)
   385  			compareStates(t, Done, decryptCtx.State())
   386  			compareResources(t, originalDoc, decryptedDoc)
   387  		})
   388  	})
   389  }
   390  

View as plain text