Skip to content

Commit aaf6c18

Browse files
authored
Add basic configuration validation (#4)
This adds a check that the key types are valid, that an issuer CN is present, and that domains aren't duplicated.
1 parent de1dcc2 commit aaf6c18

File tree

3 files changed

+87
-0
lines changed

3 files changed

+87
-0
lines changed

config/config.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,19 @@ package config
33

44
import (
55
"encoding/json"
6+
"errors"
67
"fmt"
78
"os"
89
)
910

11+
const (
12+
// KeyTypeP256 is one of the valid key types in configuration.
13+
KeyTypeP256 = "p256"
14+
15+
// KeyTypeRSA2048 is one of the valid key types in configuration.
16+
KeyTypeRSA2048 = "rsa2048"
17+
)
18+
1019
// Load a configuration file from cfgPath.
1120
func Load(cfgPath string) (*Config, error) {
1221
cfgBytes, err := os.ReadFile(cfgPath) //nolint:gosec // Reading arbitrary config file is expected
@@ -21,9 +30,43 @@ func Load(cfgPath string) (*Config, error) {
2130
return nil, fmt.Errorf("parsing %s: %w", cfgPath, err)
2231
}
2332

33+
err = validate(&cfg)
34+
if err != nil {
35+
return nil, fmt.Errorf("validating %s: %w", cfgPath, err)
36+
}
37+
2438
return &cfg, nil
2539
}
2640

41+
// validate the loaded configuration.
42+
// This checks domains are unique, key types are valid, and that an issuer CN is set.
43+
func validate(cfg *Config) error {
44+
domains := make(map[string]struct{}, 0)
45+
var errs []error
46+
for i, site := range cfg.Sites {
47+
switch site.KeyType {
48+
case KeyTypeP256, KeyTypeRSA2048:
49+
// Valid key types
50+
default:
51+
errs = append(errs, fmt.Errorf("site %d unsupported key type: %s", i, site.KeyType))
52+
}
53+
54+
for _, d := range []string{site.Domains.Valid, site.Domains.Revoked, site.Domains.Expired} {
55+
_, seen := domains[d]
56+
if seen {
57+
errs = append(errs, fmt.Errorf("site %d duplicate domain: %s", i, d))
58+
}
59+
domains[d] = struct{}{}
60+
}
61+
62+
if site.IssuerCN == "" {
63+
errs = append(errs, fmt.Errorf("site %d missing issuer CN", i))
64+
}
65+
}
66+
67+
return errors.Join(errs...)
68+
}
69+
2770
// Config is the structure of the JSON configuration file.
2871
type Config struct {
2972
// Sites is a list of sites to host.
@@ -47,6 +90,7 @@ type Site struct {
4790
KeyType string
4891

4992
// Profile selects the ACME profile to use for this certificate.
93+
// Optional.
5094
Profile string
5195

5296
// Domain names to use.

config/config_test.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package config_test
22

33
import (
44
"reflect"
5+
"strings"
56
"testing"
67

78
"github.com/letsencrypt/test-certs-site/config"
@@ -56,3 +57,25 @@ func TestLoadConfig(t *testing.T) {
5657
t.Fatalf("got:\n%+q\nwant:\n%+q", cfg, &expected)
5758
}
5859
}
60+
61+
func TestInvalidConfig(t *testing.T) {
62+
t.Parallel()
63+
_, err := config.Load("invalid.json")
64+
if err == nil {
65+
t.Fatal("LoadConfig should have returned an error on invalid json")
66+
}
67+
68+
errStr := err.Error()
69+
70+
for _, expected := range []string{
71+
"site 0 missing issuer CN",
72+
"site 0 duplicate domain: duplicate.domain",
73+
"site 1 duplicate domain: valid.salad",
74+
"site 0 unsupported key type: 3des",
75+
"site 1 unsupported key type: ",
76+
} {
77+
if !strings.Contains(errStr, expected) {
78+
t.Errorf("got error %q, want error containing %q", errStr, expected)
79+
}
80+
}
81+
}

config/invalid.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"sites": [
3+
{
4+
"keyType": "3des",
5+
"domains": {
6+
"valid": "valid.salad",
7+
"expired": "duplicate.domain",
8+
"revoked": "duplicate.domain"
9+
}
10+
},
11+
{
12+
"issuerCN": "root",
13+
"domains": {
14+
"valid": "valid.salad",
15+
"expired": "expired.salad",
16+
"revoked": "revoked.salad"
17+
}
18+
}
19+
]
20+
}

0 commit comments

Comments
 (0)