Skip to content

Commit 3d56169

Browse files
committed
refactor: adds certLife to replace before/after timestamps.
Signed-off-by: ianhundere <138915+ianhundere@users.noreply.github.com>
1 parent dc2a6c5 commit 3d56169

File tree

10 files changed

+2576
-1214
lines changed

10 files changed

+2576
-1214
lines changed

cmd/certificate_maker/certificate_maker.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -174,14 +174,18 @@ func runCreate(_ *cobra.Command, _ []string) error {
174174
}
175175

176176
// Validate template paths
177-
if err := certmaker.ValidateTemplatePath(viper.GetString("root-template")); err != nil {
177+
rootTemplate := viper.GetString("root-template")
178+
leafTemplate := viper.GetString("leaf-template")
179+
intermediateTemplate := viper.GetString("intermediate-template")
180+
181+
if err := certmaker.ValidateTemplatePath(rootTemplate); err != nil {
178182
return fmt.Errorf("root template error: %w", err)
179183
}
180-
if err := certmaker.ValidateTemplatePath(viper.GetString("leaf-template")); err != nil {
184+
if err := certmaker.ValidateTemplatePath(leafTemplate); err != nil {
181185
return fmt.Errorf("leaf template error: %w", err)
182186
}
183187
if viper.GetString("intermediate-key-id") != "" {
184-
if err := certmaker.ValidateTemplatePath(viper.GetString("intermediate-template")); err != nil {
188+
if err := certmaker.ValidateTemplatePath(intermediateTemplate); err != nil {
185189
return fmt.Errorf("intermediate template error: %w", err)
186190
}
187191
}

cmd/certificate_maker/certificate_maker_test.go

Lines changed: 248 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -166,8 +166,7 @@ func TestRunCreate(t *testing.T) {
166166
"issuer": {
167167
"commonName": "Test TSA Root CA"
168168
},
169-
"notBefore": "2024-01-01T00:00:00Z",
170-
"notAfter": "2025-01-01T00:00:00Z",
169+
"certLife": "8760h",
171170
"keyUsage": ["certSign", "crlSign"],
172171
"basicConstraints": {
173172
"isCA": true,
@@ -179,8 +178,10 @@ func TestRunCreate(t *testing.T) {
179178
"subject": {
180179
"commonName": "Test TSA"
181180
},
182-
"notBefore": "2024-01-01T00:00:00Z",
183-
"notAfter": "2025-01-01T00:00:00Z",
181+
"issuer": {
182+
"commonName": "Test TSA Root CA"
183+
},
184+
"certLife": "8760h",
184185
"keyUsage": ["digitalSignature"],
185186
"extKeyUsage": ["TimeStamping"],
186187
"basicConstraints": {
@@ -324,7 +325,7 @@ func TestRunCreate(t *testing.T) {
324325
viper.Reset()
325326
cmd := &cobra.Command{}
326327
for i := 0; i < len(tt.args); i += 2 {
327-
flag := tt.args[i][2:] // Remove "--" prefix
328+
flag := tt.args[i][2:]
328329
value := tt.args[i+1]
329330
viper.Set(flag, value)
330331
}
@@ -352,8 +353,7 @@ func TestCreateCommand(t *testing.T) {
352353
"issuer": {
353354
"commonName": "Test TSA Root CA"
354355
},
355-
"notBefore": "2024-01-01T00:00:00Z",
356-
"notAfter": "2025-01-01T00:00:00Z",
356+
"certLife": "8760h",
357357
"keyUsage": ["certSign", "crlSign"],
358358
"basicConstraints": {
359359
"isCA": true,
@@ -365,8 +365,10 @@ func TestCreateCommand(t *testing.T) {
365365
"subject": {
366366
"commonName": "Test TSA"
367367
},
368-
"notBefore": "2024-01-01T00:00:00Z",
369-
"notAfter": "2025-01-01T00:00:00Z",
368+
"issuer": {
369+
"commonName": "Test TSA Root CA"
370+
},
371+
"certLife": "8760h",
370372
"keyUsage": ["digitalSignature"],
371373
"extKeyUsage": ["TimeStamping"],
372374
"basicConstraints": {
@@ -432,7 +434,7 @@ func TestCreateCommand(t *testing.T) {
432434
viper.Reset()
433435
cmd := &cobra.Command{}
434436
for i := 0; i < len(tt.args); i += 2 {
435-
flag := tt.args[i][2:] // Remove "--" prefix
437+
flag := tt.args[i][2:]
436438
value := tt.args[i+1]
437439
viper.Set(flag, value)
438440
}
@@ -479,3 +481,239 @@ func TestRootCommand(t *testing.T) {
479481
})
480482
}
481483
}
484+
485+
func TestEnvironmentVariableHandling(t *testing.T) {
486+
tests := []struct {
487+
name string
488+
envVars map[string]string
489+
args []string
490+
wantError bool
491+
errorString string
492+
}{
493+
{
494+
name: "AWS KMS from environment",
495+
envVars: map[string]string{
496+
"KMS_TYPE": "awskms",
497+
"AWS_REGION": "us-west-2",
498+
"KMS_ROOT_KEY_ID": "alias/test-root",
499+
"KMS_LEAF_KEY_ID": "alias/test-leaf",
500+
},
501+
args: []string{"create"},
502+
wantError: true,
503+
},
504+
{
505+
name: "GCP KMS from environment",
506+
envVars: map[string]string{
507+
"KMS_TYPE": "gcpkms",
508+
"GCP_CREDENTIALS_FILE": "/path/to/creds.json",
509+
"KMS_ROOT_KEY_ID": "projects/test/locations/global/keyRings/test/cryptoKeys/root/cryptoKeyVersions/1",
510+
"KMS_LEAF_KEY_ID": "projects/test/locations/global/keyRings/test/cryptoKeys/leaf/cryptoKeyVersions/1",
511+
},
512+
args: []string{"create"},
513+
wantError: true,
514+
errorString: "credentials file not found",
515+
},
516+
{
517+
name: "Azure KMS from environment",
518+
envVars: map[string]string{
519+
"KMS_TYPE": "azurekms",
520+
"AZURE_TENANT_ID": "test-tenant",
521+
"KMS_ROOT_KEY_ID": "azurekms:name=test-key;vault=test-vault",
522+
"KMS_LEAF_KEY_ID": "azurekms:name=test-key;vault=test-vault",
523+
},
524+
args: []string{"create"},
525+
wantError: true,
526+
},
527+
{
528+
name: "HashiVault KMS from environment",
529+
envVars: map[string]string{
530+
"KMS_TYPE": "hashivault",
531+
"VAULT_TOKEN": "test-token",
532+
"VAULT_ADDR": "http://vault:8200",
533+
"KMS_ROOT_KEY_ID": "transit/keys/test-root",
534+
"KMS_LEAF_KEY_ID": "transit/keys/test-leaf",
535+
},
536+
args: []string{"create"},
537+
wantError: true,
538+
},
539+
}
540+
541+
for _, tt := range tests {
542+
t.Run(tt.name, func(t *testing.T) {
543+
oldEnv := map[string]string{}
544+
for k := range tt.envVars {
545+
if v, ok := os.LookupEnv(k); ok {
546+
oldEnv[k] = v
547+
}
548+
}
549+
550+
for k, v := range tt.envVars {
551+
os.Setenv(k, v)
552+
}
553+
554+
viper.Reset()
555+
556+
viper.BindEnv("kms-type", "KMS_TYPE")
557+
viper.BindEnv("aws-region", "AWS_REGION")
558+
viper.BindEnv("azure-tenant-id", "AZURE_TENANT_ID")
559+
viper.BindEnv("gcp-credentials-file", "GCP_CREDENTIALS_FILE")
560+
viper.BindEnv("vault-token", "VAULT_TOKEN")
561+
viper.BindEnv("vault-address", "VAULT_ADDR")
562+
viper.BindEnv("root-key-id", "KMS_ROOT_KEY_ID")
563+
viper.BindEnv("leaf-key-id", "KMS_LEAF_KEY_ID")
564+
565+
defer func() {
566+
for k := range tt.envVars {
567+
if v, ok := oldEnv[k]; ok {
568+
os.Setenv(k, v)
569+
} else {
570+
os.Unsetenv(k)
571+
}
572+
}
573+
}()
574+
575+
cmd := &cobra.Command{
576+
Use: "test",
577+
RunE: runCreate,
578+
}
579+
580+
cmd.Flags().String("kms-type", "", "KMS type")
581+
cmd.Flags().String("aws-region", "", "AWS region")
582+
cmd.Flags().String("azure-tenant-id", "", "Azure tenant ID")
583+
cmd.Flags().String("gcp-credentials-file", "", "GCP credentials file")
584+
cmd.Flags().String("vault-token", "", "HashiVault token")
585+
cmd.Flags().String("vault-address", "", "HashiVault address")
586+
cmd.Flags().String("root-key-id", "", "Root key ID")
587+
cmd.Flags().String("leaf-key-id", "", "Leaf key ID")
588+
cmd.Flags().String("root-template", "templates/root-template.json", "Root template")
589+
cmd.Flags().String("leaf-template", "templates/leaf-template.json", "Leaf template")
590+
591+
viper.BindPFlag("kms-type", cmd.Flags().Lookup("kms-type"))
592+
viper.BindPFlag("aws-region", cmd.Flags().Lookup("aws-region"))
593+
viper.BindPFlag("azure-tenant-id", cmd.Flags().Lookup("azure-tenant-id"))
594+
viper.BindPFlag("gcp-credentials-file", cmd.Flags().Lookup("gcp-credentials-file"))
595+
viper.BindPFlag("vault-token", cmd.Flags().Lookup("vault-token"))
596+
viper.BindPFlag("vault-address", cmd.Flags().Lookup("vault-address"))
597+
viper.BindPFlag("root-key-id", cmd.Flags().Lookup("root-key-id"))
598+
viper.BindPFlag("leaf-key-id", cmd.Flags().Lookup("leaf-key-id"))
599+
viper.BindPFlag("root-template", cmd.Flags().Lookup("root-template"))
600+
viper.BindPFlag("leaf-template", cmd.Flags().Lookup("leaf-template"))
601+
602+
cmd.SetArgs(tt.args)
603+
err := cmd.Execute()
604+
605+
if tt.wantError {
606+
require.Error(t, err)
607+
if tt.errorString != "" {
608+
assert.Contains(t, err.Error(), tt.errorString)
609+
}
610+
} else {
611+
require.NoError(t, err)
612+
}
613+
614+
assert.Equal(t, tt.envVars["KMS_TYPE"], viper.GetString("kms-type"))
615+
assert.Equal(t, tt.envVars["KMS_ROOT_KEY_ID"], viper.GetString("root-key-id"))
616+
assert.Equal(t, tt.envVars["KMS_LEAF_KEY_ID"], viper.GetString("leaf-key-id"))
617+
})
618+
}
619+
}
620+
621+
func TestKMSProviderConfigurationValidation(t *testing.T) {
622+
tests := []struct {
623+
name string
624+
args []string
625+
wantError bool
626+
errorString string
627+
}{
628+
{
629+
name: "AWS KMS invalid key format",
630+
args: []string{
631+
"create",
632+
"--kms-type", "awskms",
633+
"--aws-region", "us-west-2",
634+
"--root-key-id", "invalid-format",
635+
"--leaf-key-id", "invalid-format",
636+
},
637+
wantError: true,
638+
errorString: "must start with 'arn:aws:kms:' or 'alias/'",
639+
},
640+
{
641+
name: "GCP KMS missing key version",
642+
args: []string{
643+
"create",
644+
"--kms-type", "gcpkms",
645+
"--root-key-id", "projects/test/locations/global/keyRings/test/cryptoKeys/test",
646+
"--leaf-key-id", "projects/test/locations/global/keyRings/test/cryptoKeys/test",
647+
},
648+
wantError: true,
649+
errorString: "must contain '/cryptoKeyVersions/'",
650+
},
651+
{
652+
name: "Azure KMS invalid key format",
653+
args: []string{
654+
"create",
655+
"--kms-type", "azurekms",
656+
"--azure-tenant-id", "test-tenant",
657+
"--root-key-id", "invalid-format",
658+
"--leaf-key-id", "invalid-format",
659+
},
660+
wantError: true,
661+
errorString: "must start with 'azurekms:name='",
662+
},
663+
{
664+
name: "HashiVault KMS invalid key path",
665+
args: []string{
666+
"create",
667+
"--kms-type", "hashivault",
668+
"--vault-token", "test-token",
669+
"--vault-address", "http://vault:8200",
670+
"--root-key-id", "invalid/path",
671+
"--leaf-key-id", "invalid/path",
672+
},
673+
wantError: true,
674+
errorString: "must be in format: transit/keys/keyname",
675+
},
676+
}
677+
678+
for _, tt := range tests {
679+
t.Run(tt.name, func(t *testing.T) {
680+
viper.Reset()
681+
cmd := &cobra.Command{
682+
Use: "test",
683+
RunE: runCreate,
684+
}
685+
686+
cmd.Flags().String("kms-type", "", "KMS type")
687+
cmd.Flags().String("aws-region", "", "AWS region")
688+
cmd.Flags().String("azure-tenant-id", "", "Azure tenant ID")
689+
cmd.Flags().String("vault-token", "", "HashiVault token")
690+
cmd.Flags().String("vault-address", "", "HashiVault address")
691+
cmd.Flags().String("root-key-id", "", "Root key ID")
692+
cmd.Flags().String("leaf-key-id", "", "Leaf key ID")
693+
cmd.Flags().String("root-template", "templates/root-template.json", "Root template")
694+
cmd.Flags().String("leaf-template", "templates/leaf-template.json", "Leaf template")
695+
696+
viper.BindPFlag("kms-type", cmd.Flags().Lookup("kms-type"))
697+
viper.BindPFlag("aws-region", cmd.Flags().Lookup("aws-region"))
698+
viper.BindPFlag("azure-tenant-id", cmd.Flags().Lookup("azure-tenant-id"))
699+
viper.BindPFlag("vault-token", cmd.Flags().Lookup("vault-token"))
700+
viper.BindPFlag("vault-address", cmd.Flags().Lookup("vault-address"))
701+
viper.BindPFlag("root-key-id", cmd.Flags().Lookup("root-key-id"))
702+
viper.BindPFlag("leaf-key-id", cmd.Flags().Lookup("leaf-key-id"))
703+
viper.BindPFlag("root-template", cmd.Flags().Lookup("root-template"))
704+
viper.BindPFlag("leaf-template", cmd.Flags().Lookup("leaf-template"))
705+
706+
cmd.SetArgs(tt.args)
707+
err := cmd.Execute()
708+
709+
if tt.wantError {
710+
require.Error(t, err)
711+
if tt.errorString != "" {
712+
assert.Contains(t, err.Error(), tt.errorString)
713+
}
714+
} else {
715+
require.NoError(t, err)
716+
}
717+
})
718+
}
719+
}

docs/certificate-maker.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ This tool creates root, intermediate (optional), and leaf certificates for Times
55
- Two-level chain (root -> leaf)
66
- Three-level chain (root -> intermediate -> leaf)
77

8+
Relies on [x509util](https://pkg.go.dev/go.step.sm/crypto/x509util) which builds X.509 certificates from JSON templates.
9+
810
## Requirements
911

1012
- Access to one of the supported KMS providers (AWS, Google Cloud, Azure)

pkg/certmaker/certmaker.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -432,10 +432,14 @@ func ValidateKMSConfig(config KMSConfig) error {
432432
if keyID == "" {
433433
return nil
434434
}
435-
if strings.Contains(keyID, "/") {
436-
return fmt.Errorf("hashivault %s must be a simple key name without path separators", keyType)
435+
parts := strings.Split(keyID, "/")
436+
if len(parts) < 3 {
437+
return fmt.Errorf("hashivault %s must be in format: transit/keys/keyname", keyType)
437438
}
438-
if strings.TrimSpace(keyID) == "" {
439+
if parts[0] != "transit" || parts[1] != "keys" {
440+
return fmt.Errorf("hashivault %s must start with 'transit/keys/'", keyType)
441+
}
442+
if parts[2] == "" {
439443
return fmt.Errorf("key name cannot be empty for %s", keyType)
440444
}
441445
return nil

0 commit comments

Comments
 (0)