github.com/greenpau/go-authcrunch@v1.1.4/pkg/kms/key_test.go (about)

     1  // Copyright 2022 Paul Greenberg greenpau@outlook.com
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package kms
    16  
    17  import (
    18  	"fmt"
    19  	"github.com/google/go-cmp/cmp"
    20  	"github.com/greenpau/go-authcrunch/internal/tests"
    21  	"github.com/greenpau/go-authcrunch/pkg/errors"
    22  	"github.com/greenpau/go-authcrunch/pkg/requests"
    23  	"github.com/greenpau/go-authcrunch/pkg/user"
    24  	"os"
    25  	"strings"
    26  	"testing"
    27  	"time"
    28  )
    29  
    30  func newTestUser() *user.User {
    31  	cfg := `{
    32          "exp": ` + fmt.Sprintf("%d", time.Now().Add(10*time.Minute).Unix()) + `,
    33          "iat": ` + fmt.Sprintf("%d", time.Now().Add(10*time.Minute*-1).Unix()) + `,
    34          "nbf": ` + fmt.Sprintf("%d", time.Date(2015, 10, 10, 12, 0, 0, 0, time.UTC).Unix()) + `,
    35          "name":   "Smith, John",
    36          "email":  "smithj@outlook.com",
    37          "origin": "localhost",
    38          "sub":    "smithj@outlook.com",
    39          "roles": "anonymous guest"
    40      }`
    41  	usr, err := user.NewUser(cfg)
    42  	if err != nil {
    43  		panic(err)
    44  	}
    45  	return usr
    46  }
    47  
    48  func TestGetKeysFromConfig(t *testing.T) {
    49  	var testcases = []struct {
    50  		name   string
    51  		config string
    52  		env    map[string]string
    53  		want   map[string]interface{}
    54  		log    bool
    55  		// keyPair indicates which keys are being used for sign/verification.
    56  		keyPair   []int
    57  		shouldErr bool
    58  		err       error
    59  	}{
    60  		{
    61  			name: "default shared key in default context",
    62  			config: `
    63                  crypto key token name "foobar token"
    64                  crypto key sign-verify foobar
    65              `,
    66  			keyPair: []int{0, 0},
    67  			want: map[string]interface{}{
    68  				"config_count": 1,
    69  				"key_count":    1,
    70  				"keys": []string{
    71  					"0: sign   0: []uint8",
    72  					"0: verify 0: []uint8",
    73  				},
    74  			},
    75  		},
    76  		{
    77  			name: "shared secret embedded in environment variable",
    78  			config: `
    79                  crypto key cb315f43c868 sign-verify from env JWT_SHARED_SECRET
    80              `,
    81  			env: map[string]string{
    82  				"JWT_TOKEN_LIFETIME": "3600",
    83  				"JWT_SHARED_SECRET":  "foobar",
    84  			},
    85  			keyPair: []int{0, 0},
    86  			want: map[string]interface{}{
    87  				"config_count": 1,
    88  				"key_count":    1,
    89  				"keys": []string{
    90  					"0: sign   cb315f43c868: []uint8",
    91  					"0: verify cb315f43c868: []uint8",
    92  				},
    93  			},
    94  		},
    95  		{
    96  			name: "rsa key embedded in environment variable",
    97  			config: `
    98                  crypto key cb315f43c868 sign-verify from env JWT_SHARED_SECRET
    99              `,
   100  			env: map[string]string{
   101  				"JWT_TOKEN_LIFETIME": "3600",
   102  				"JWT_SHARED_SECRET":  "file:./../../testdata/rskeys/test_2_pri.pem",
   103  			},
   104  			keyPair: []int{0, 0},
   105  			want: map[string]interface{}{
   106  				"config_count": 1,
   107  				"key_count":    1,
   108  				"keys": []string{
   109  					"0: sign   cb315f43c868: *rsa.PrivateKey",
   110  					"0: verify cb315f43c868: *rsa.PublicKey",
   111  				},
   112  			},
   113  		},
   114  		{
   115  			name: "bad rsa key embedded in environment variable",
   116  			// log:  true,
   117  			config: `
   118                  crypto key cb315f43c868 sign-verify from env JWT_SHARED_SECRET
   119              `,
   120  			env: map[string]string{
   121  				"JWT_SHARED_SECRET": "-----BEGIN PRIVATE",
   122  			},
   123  			shouldErr: true,
   124  			err:       errors.ErrNotPEMEncodedKey,
   125  		},
   126  		{
   127  			name: "bad rsa key embedded in environment variable",
   128  			// log:  true,
   129  			config: `
   130                  crypto key cb315f43c868 sign-verify from env JWT_SHARED_SECRET
   131              `,
   132  			env: map[string]string{
   133  				"JWT_SHARED_SECRET": "-----BEGIN PRIVATE ---END PRIVATE",
   134  			},
   135  			shouldErr: true,
   136  			err:       errors.ErrNotPEMEncodedKey,
   137  		},
   138  		{
   139  			name: "load private and public rsa keys from file path",
   140  			config: `
   141                  crypto key k9738a405e99 sign from file ./../../testdata/rskeys/test_2_pri.pem
   142                  crypto key k9738a405e99 verify from file ./../../testdata/rskeys/test_2_pub.pem
   143              `,
   144  			keyPair: []int{0, 1},
   145  			want: map[string]interface{}{
   146  				"config_count": 2,
   147  				"key_count":    2,
   148  				"keys": []string{
   149  					"0: sign   k9738a405e99: *rsa.PrivateKey",
   150  					"1: verify k9738a405e99: *rsa.PublicKey",
   151  				},
   152  			},
   153  		},
   154  		{
   155  			name: "load private and public rsa and ecdsa keys from file path",
   156  			config: `
   157                  crypto key k9738a405e99 sign-verify from file ./../../testdata/misckeys/rsa_test_2_pri.pem
   158                  crypto key k9738a405e11 sign-verify from file ./../../testdata/misckeys/ecdsa_test_2_pri.pem
   159              `,
   160  			keyPair: []int{0, 0},
   161  			want: map[string]interface{}{
   162  				"config_count": 2,
   163  				"key_count":    2,
   164  				"keys": []string{
   165  					"0: sign   k9738a405e99: *rsa.PrivateKey",
   166  					"0: verify k9738a405e99: *rsa.PublicKey",
   167  					"1: sign   k9738a405e11: *ecdsa.PrivateKey",
   168  					"1: verify k9738a405e11: *ecdsa.PublicKey",
   169  				},
   170  			},
   171  		},
   172  		{
   173  			name: "load private rsa key from file path for both sign and verify",
   174  			config: `
   175                  crypto key k9738a405e99 sign-verify from file ./../../testdata/rskeys/test_1_pri.pem
   176              `,
   177  			keyPair: []int{0, 0},
   178  			want: map[string]interface{}{
   179  				"config_count": 1,
   180  				"key_count":    1,
   181  				"keys": []string{
   182  					"0: sign   k9738a405e99: *rsa.PrivateKey",
   183  					"0: verify k9738a405e99: *rsa.PublicKey",
   184  				},
   185  			},
   186  		},
   187  		{
   188  			name: "load private and public ecdsa keys from file path",
   189  			config: `
   190                  crypto key k9738a405e99 sign from file ./../../testdata/ecdsakeys/test_2_pri.pem
   191                  crypto key k9738a405e99 verify from file ./../../testdata/ecdsakeys/test_2_pub.pem
   192              `,
   193  			keyPair: []int{0, 1},
   194  			want: map[string]interface{}{
   195  				"config_count": 2,
   196  				"key_count":    2,
   197  				"keys": []string{
   198  					"0: sign   k9738a405e99: *ecdsa.PrivateKey",
   199  					"1: verify k9738a405e99: *ecdsa.PublicKey",
   200  				},
   201  			},
   202  		},
   203  		{
   204  			name: "load private ecdsa key from file path for both sign and verify",
   205  			config: `
   206                  crypto key k9738a405e99 sign-verify from file ./../../testdata/ecdsakeys/test_1_pri.pem
   207              `,
   208  			keyPair: []int{0, 0},
   209  			want: map[string]interface{}{
   210  				"config_count": 1,
   211  				"key_count":    1,
   212  				"keys": []string{
   213  					"0: sign   k9738a405e99: *ecdsa.PrivateKey",
   214  					"0: verify k9738a405e99: *ecdsa.PublicKey",
   215  				},
   216  			},
   217  		},
   218  		{
   219  			name: "load private ecdsa key from environment variable with file path for both sign and verify",
   220  			config: `
   221                  crypto key cb315f43c868 sign-verify from env JWT_SECRET_FILE as file
   222              `,
   223  			env: map[string]string{
   224  				"JWT_SECRET_FILE": "./../../testdata/rskeys/test_1_pri.pem",
   225  			},
   226  			keyPair: []int{0, 0},
   227  			want: map[string]interface{}{
   228  				"config_count": 1,
   229  				"key_count":    1,
   230  				"keys": []string{
   231  					"0: sign   cb315f43c868: *rsa.PrivateKey",
   232  					"0: verify cb315f43c868: *rsa.PublicKey",
   233  				},
   234  			},
   235  		},
   236  		{
   237  			name: "load keys from rsa directory path",
   238  			config: `
   239                  crypto key k9738a405e99 verify from directory ./../../testdata/rskeys
   240              `,
   241  			want: map[string]interface{}{
   242  				"config_count": 1,
   243  				"key_count":    4,
   244  				"keys": []string{
   245  					"0: sign   test_1_pri: *rsa.PrivateKey",
   246  					"0: verify test_1_pri: *rsa.PublicKey",
   247  					"1: sign   test_2_pri: *rsa.PrivateKey",
   248  					"1: verify test_2_pri: *rsa.PublicKey",
   249  					"2: verify test_2_pub: *rsa.PublicKey",
   250  					"3: sign   private: *rsa.PrivateKey",
   251  					"3: verify private: *rsa.PublicKey",
   252  				},
   253  			},
   254  		},
   255  
   256  		{
   257  			name: "load keys from rsa directory path via env vars",
   258  			config: `
   259                  crypto key cb315f43c868 sign-verify from env JWT_SECRET_DIR as directory
   260              `,
   261  			env: map[string]string{
   262  				"JWT_SECRET_DIR": "./../../testdata/rskeys",
   263  			},
   264  			want: map[string]interface{}{
   265  				"config_count": 1,
   266  				"key_count":    4,
   267  				"keys": []string{
   268  					"0: sign   test_1_pri: *rsa.PrivateKey",
   269  					"0: verify test_1_pri: *rsa.PublicKey",
   270  					"1: sign   test_2_pri: *rsa.PrivateKey",
   271  					"1: verify test_2_pri: *rsa.PublicKey",
   272  					"2: verify test_2_pub: *rsa.PublicKey",
   273  					"3: sign   private: *rsa.PrivateKey",
   274  					"3: verify private: *rsa.PublicKey",
   275  				},
   276  			},
   277  		},
   278  		{
   279  			name: "load keys from rsa directory path",
   280  			config: `
   281                  crypto key k9738a405e99 verify from directory ./../../testdata/nokeys/docs
   282              `,
   283  			shouldErr: true,
   284  			err:       errors.ErrWalkDir.WithArgs("no crypto keys found"),
   285  		},
   286  		{
   287  			name: "load keys from rsa directory path",
   288  			config: `
   289                  crypto key k9738a405e99 verify from directory ./../../testdata/nokeys/bad
   290              `,
   291  			shouldErr: true,
   292  			err: errors.ErrWalkDir.WithArgs(
   293  				errors.ErrCryptoKeyConfigReadFile.WithArgs(
   294  					"../../testdata/nokeys/bad/bad_begin_only.key",
   295  					errors.ErrNotPEMEncodedKey,
   296  				),
   297  			),
   298  		},
   299  		{
   300  			name: "load keys from ecdsa directory path",
   301  			config: `
   302                  crypto key k9738a405e99 verify from directory ./../../testdata/ecdsakeys
   303              `,
   304  			want: map[string]interface{}{
   305  				"config_count": 1,
   306  				"key_count":    6,
   307  				"keys": []string{
   308  					"0: sign   test_1_pri: *ecdsa.PrivateKey",
   309  					"0: verify test_1_pri: *ecdsa.PublicKey",
   310  					"1: sign   test_2_pri: *ecdsa.PrivateKey",
   311  					"1: verify test_2_pri: *ecdsa.PublicKey",
   312  					"2: verify test_2_pub: *ecdsa.PublicKey",
   313  					"3: sign   test_3_pri: *ecdsa.PrivateKey",
   314  					"3: verify test_3_pri: *ecdsa.PublicKey",
   315  					"4: sign   test_4_pri: *ecdsa.PrivateKey",
   316  					"4: verify test_4_pri: *ecdsa.PublicKey",
   317  					"5: sign   private: *ecdsa.PrivateKey",
   318  					"5: verify private: *ecdsa.PublicKey",
   319  				},
   320  			},
   321  		},
   322  		{
   323  			name: "private rsa key wrapped in ec header",
   324  			config: `
   325                  crypto key k9738a405e99 sign-verify from file ./../../testdata/malformed/ec_header_rsa_pri.pem
   326              `,
   327  			shouldErr: true,
   328  			err: errors.ErrCryptoKeyConfigReadFile.WithArgs(
   329  				"./../../testdata/malformed/ec_header_rsa_pri.pem",
   330  				`x509: failed to parse private key (use ParsePKCS1PrivateKey instead for this key format)`,
   331  			),
   332  		},
   333  		{
   334  			name: "private ec key wrapped in rsa header",
   335  			config: `
   336                  crypto key k9738a405e99 sign-verify from file ./../../testdata/malformed/rsa_header_ec_pri.pem
   337              `,
   338  			shouldErr: true,
   339  			err: errors.ErrCryptoKeyConfigReadFile.WithArgs(
   340  				"./../../testdata/malformed/rsa_header_ec_pri.pem",
   341  				`x509: failed to parse private key (use ParseECPrivateKey instead for this key format)`,
   342  			),
   343  		},
   344  		{
   345  			name: "public key passed as private",
   346  			config: `
   347                  crypto key k9738a405e99 sign-verify from file ./../../testdata/malformed/rsa_pub_as_pri.pem
   348              `,
   349  			shouldErr: true,
   350  
   351  			err: errors.ErrCryptoKeyConfigReadFile.WithArgs(
   352  				"./../../testdata/malformed/rsa_pub_as_pri.pem",
   353  				`asn1: structure error: tags don't match (2 vs {class:0 tag:16 length:19 isCompound:true}) `+
   354  					`{optional:false explicit:false application:false private:false defaultValue:<nil> `+
   355  					`tag:<nil> stringType:0 timeType:0 set:false omitEmpty:false} int @2`,
   356  			),
   357  		},
   358  		{
   359  			name: "private key passed as public",
   360  			config: `
   361                  crypto key k9738a405e99 sign-verify from file ./../../testdata/malformed/rsa_pri_as_pub.pem
   362              `,
   363  			shouldErr: true,
   364  			err: errors.ErrCryptoKeyConfigReadFile.WithArgs(
   365  				"./../../testdata/malformed/rsa_pri_as_pub.pem",
   366  				`asn1: structure error: tags don't match (16 vs {class:0 tag:2 length:1 isCompound:false}) `+
   367  					`{optional:false explicit:false application:false private:false defaultValue:<nil> `+
   368  					`tag:<nil> stringType:0 timeType:0 set:false omitEmpty:false} AlgorithmIdentifier @2`,
   369  			),
   370  		},
   371  		{
   372  			name: "cert passed as private key",
   373  			config: `
   374                  crypto key k9738a405e99 sign-verify from file ./../../testdata/malformed/cert.pem
   375              `,
   376  			shouldErr: true,
   377  			err: errors.ErrCryptoKeyConfigReadFile.WithArgs(
   378  				"./../../testdata/malformed/cert.pem",
   379  				errors.ErrNotPEMEncodedKey,
   380  			),
   381  		},
   382  	}
   383  	for _, tc := range testcases {
   384  		t.Run(tc.name, func(t *testing.T) {
   385  			msgs := []string{fmt.Sprintf("test name: %s", tc.name)}
   386  			msgs = append(msgs, fmt.Sprintf("config: %s", tc.config))
   387  			for k, v := range tc.env {
   388  				if strings.HasPrefix(v, "file:") {
   389  					b, err := extractBytesFromFile(strings.TrimPrefix(v, "file:"))
   390  					if err != nil {
   391  						t.Fatal(err)
   392  					}
   393  					v = string(b)
   394  				}
   395  				msgs = append(msgs, fmt.Sprintf("env: %s = %s", k, v))
   396  				os.Setenv(k, v)
   397  				defer os.Unsetenv(k)
   398  			}
   399  
   400  			configs, err := ParseCryptoKeyConfigs(tc.config)
   401  			if err != nil {
   402  				t.Fatal(err)
   403  			}
   404  
   405  			if tc.log {
   406  				for _, c := range configs {
   407  					t.Logf("%s", c.ToString())
   408  				}
   409  			}
   410  
   411  			keys, err := GetKeysFromConfigs(configs)
   412  			if tests.EvalErrWithLog(t, err, "keys", tc.shouldErr, tc.err, msgs) {
   413  				return
   414  			}
   415  
   416  			got := make(map[string]interface{})
   417  			got["config_count"] = len(configs)
   418  			got["key_count"] = len(keys)
   419  
   420  			var km []string
   421  			for i, k := range keys {
   422  				if k.Sign.Token.Capable {
   423  					km = append(km, fmt.Sprintf("%d: sign   %s: %T", i, k.Sign.Token.ID, k.Sign.Secret))
   424  				}
   425  				if k.Verify.Token.Capable {
   426  					km = append(km, fmt.Sprintf("%d: verify %s: %T", i, k.Verify.Token.ID, k.Verify.Secret))
   427  				}
   428  			}
   429  			got["keys"] = km
   430  
   431  			if tc.log {
   432  				t.Logf("crypto configs:\n%s", cmp.Diff(nil, configs))
   433  				for i, key := range keys {
   434  					t.Logf("crypto key %d:\n%s", i, cmp.Diff(nil, key))
   435  				}
   436  			}
   437  
   438  			if diff := cmp.Diff(tc.want, got, cmp.AllowUnexported(CryptoKeyConfig{})); diff != "" {
   439  				tests.WriteLog(t, msgs)
   440  				t.Fatalf("output mismatch (-want +got):\n%s", diff)
   441  			}
   442  
   443  			if len(tc.keyPair) != 2 {
   444  				return
   445  			}
   446  
   447  			var privKey, pubKey *CryptoKey
   448  			for i, j := range tc.keyPair {
   449  				if j >= len(keys) {
   450  					break
   451  				}
   452  				if i == 0 {
   453  					privKey = keys[j]
   454  					continue
   455  				}
   456  				pubKey = keys[j]
   457  			}
   458  
   459  			ks := NewCryptoKeyStore()
   460  			if err := ks.AddKeys([]*CryptoKey{privKey, pubKey}); err != nil {
   461  				t.Fatal(err)
   462  			}
   463  			usr := newTestUser()
   464  			if tc.log {
   465  				t.Logf("%v", usr)
   466  			}
   467  
   468  			if err := ks.SignToken(privKey.Sign.Token.Name, privKey.Sign.Token.DefaultMethod, usr); err != nil {
   469  				t.Fatal(err)
   470  			}
   471  
   472  			if tc.log {
   473  				t.Logf("token %v: %s", privKey.Sign.Token.Name, usr.Token)
   474  			}
   475  
   476  			ar := requests.NewAuthorizationRequest()
   477  			ar.ID = "TEST_REQUEST_ID"
   478  			ar.SessionID = "TEST_SESSION_ID"
   479  			ar.Token.Name = pubKey.Verify.Token.Name
   480  			ar.Token.Payload = usr.Token
   481  			tokenUser, err := ks.ParseToken(ar)
   482  			if err != nil {
   483  				t.Fatal(err)
   484  			}
   485  			if tc.log {
   486  				t.Logf("user:\n%s", cmp.Diff(nil, tokenUser))
   487  			}
   488  		})
   489  	}
   490  }
   491  
   492  func TestGetKeysFromCryptoKeyConfigs(t *testing.T) {
   493  	var testcases = []struct {
   494  		name      string
   495  		config    *CryptoKeyConfig
   496  		shouldErr bool
   497  		err       error
   498  	}{
   499  		{
   500  			name: "bad config file path",
   501  			config: &CryptoKeyConfig{
   502  				Source:   "config",
   503  				FilePath: "foo",
   504  			},
   505  			shouldErr: true,
   506  			err:       fmt.Errorf(`kms: file "foo" is not supported due to extension type`),
   507  		},
   508  		{
   509  			name: "bad config dir path",
   510  			config: &CryptoKeyConfig{
   511  				Source:  "config",
   512  				DirPath: "foo",
   513  			},
   514  			shouldErr: true,
   515  			err:       fmt.Errorf(`walking directory: lstat foo: no such file or directory`),
   516  		},
   517  		{
   518  			name: "bad config without file dir path",
   519  			config: &CryptoKeyConfig{
   520  				Source: "config",
   521  			},
   522  			shouldErr: true,
   523  			err:       fmt.Errorf(`unsupported config`),
   524  		},
   525  		{
   526  			name: "bad env file path",
   527  			config: &CryptoKeyConfig{
   528  				Source:      "env",
   529  				EnvVarType:  "file",
   530  				EnvVarValue: "foo",
   531  			},
   532  			shouldErr: true,
   533  			err:       fmt.Errorf(`kms: file "foo" is not supported due to extension type`),
   534  		},
   535  		{
   536  			name: "bad env dir path",
   537  			config: &CryptoKeyConfig{
   538  				Source:      "env",
   539  				EnvVarType:  "directory",
   540  				EnvVarValue: "foo",
   541  			},
   542  			shouldErr: true,
   543  			err:       fmt.Errorf(`walking directory: lstat foo: no such file or directory`),
   544  		},
   545  		{
   546  			name: "bad env without file dir path",
   547  			config: &CryptoKeyConfig{
   548  				Source:      "env",
   549  				EnvVarType:  "foo",
   550  				EnvVarValue: "foo",
   551  			},
   552  			shouldErr: true,
   553  			err:       fmt.Errorf(`unsupported env config type foo`),
   554  		},
   555  	}
   556  	for _, tc := range testcases {
   557  		t.Run(tc.name, func(t *testing.T) {
   558  			msgs := []string{fmt.Sprintf("test name: %s", tc.name)}
   559  			msgs = append(msgs, fmt.Sprintf("config: %v", tc.config))
   560  			_, err := GetKeysFromConfigs([]*CryptoKeyConfig{tc.config})
   561  			if tests.EvalErrWithLog(t, err, nil, tc.shouldErr, tc.err, msgs) {
   562  				return
   563  			}
   564  		})
   565  	}
   566  }