feat(pki): add trust store wrapper and iOS mobileconfig generation
- commit
- 1c010968359878613bacf019f676403ce064091a
- author
- brice · · 2026-03-29 16:41 UTC
- parent
- b939cf1
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
5 files · +111 -0
M go.mod +2 -0
| @@ -15,6 +15,7 @@ require ( | |||
| 15 | 15 | github.com/mattn/go-runewidth v0.0.21 | |
| 16 | 16 | github.com/muesli/termenv v0.16.0 | |
| 17 | 17 | github.com/rivo/uniseg v0.4.7 | |
| 18 | + | github.com/smallstep/truststore v0.13.0 | |
| 18 | 19 | github.com/spf13/cobra v1.10.2 | |
| 19 | 20 | github.com/spf13/viper v1.21.0 | |
| 20 | 21 | github.com/stretchr/testify v1.11.1 | |
| @@ -67,6 +68,7 @@ require ( | |||
| 67 | 68 | google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d // indirect | |
| 68 | 69 | google.golang.org/grpc v1.79.3 // indirect | |
| 69 | 70 | google.golang.org/protobuf v1.36.11 // indirect | |
| 71 | + | howett.net/plist v1.0.0 // indirect | |
| 70 | 72 | ) | |
| 71 | 73 | ||
| 72 | 74 | require ( | |
| @@ -15,6 +15,7 @@ require ( | |||||
| 15 | github.com/mattn/go-runewidth v0.0.21 | 15 | github.com/mattn/go-runewidth v0.0.21 | ||
| 16 | github.com/muesli/termenv v0.16.0 | 16 | github.com/muesli/termenv v0.16.0 | ||
| 17 | github.com/rivo/uniseg v0.4.7 | 17 | github.com/rivo/uniseg v0.4.7 | ||
| 18 | + | github.com/smallstep/truststore v0.13.0 | |||
| 18 | github.com/spf13/cobra v1.10.2 | 19 | github.com/spf13/cobra v1.10.2 | ||
| 19 | github.com/spf13/viper v1.21.0 | 20 | github.com/spf13/viper v1.21.0 | ||
| 20 | github.com/stretchr/testify v1.11.1 | 21 | github.com/stretchr/testify v1.11.1 | ||
| @@ -67,6 +68,7 @@ require ( | |||||
| 67 | google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d // indirect | 68 | google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d // indirect | ||
| 68 | google.golang.org/grpc v1.79.3 // indirect | 69 | google.golang.org/grpc v1.79.3 // indirect | ||
| 69 | google.golang.org/protobuf v1.36.11 // indirect | 70 | google.golang.org/protobuf v1.36.11 // indirect | ||
| 71 | + | howett.net/plist v1.0.0 // indirect | |||
| 70 | ) | 72 | ) | ||
| 71 | 73 | ||||
| 72 | require ( | 74 | require ( | ||
M go.sum +6 -0 generated file: collapsed
A internal/adapters/out/pki/mobileconfig.go +58 -0
| @@ -0,0 +1,58 @@ | |||
| 1 | + | package pki | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "encoding/base64" | |
| 5 | + | "fmt" | |
| 6 | + | ||
| 7 | + | "github.com/google/uuid" | |
| 8 | + | ) | |
| 9 | + | ||
| 10 | + | // GenerateMobileconfig generates an iOS configuration profile containing | |
| 11 | + | // the root CA certificate for one-tap trust installation. | |
| 12 | + | func GenerateMobileconfig(rootDER []byte, rootCN string) []byte { | |
| 13 | + | b64Cert := base64.StdEncoding.EncodeToString(rootDER) | |
| 14 | + | profileUUID := uuid.New().String() | |
| 15 | + | certUUID := uuid.New().String() | |
| 16 | + | ||
| 17 | + | profile := fmt.Sprintf(`<?xml version="1.0" encoding="UTF-8"?> | |
| 18 | + | <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | |
| 19 | + | <plist version="1.0"> | |
| 20 | + | <dict> | |
| 21 | + | <key>PayloadContent</key> | |
| 22 | + | <array> | |
| 23 | + | <dict> | |
| 24 | + | <key>PayloadCertificateFileName</key> | |
| 25 | + | <string>gordon-ca.crt</string> | |
| 26 | + | <key>PayloadContent</key> | |
| 27 | + | <data>%s</data> | |
| 28 | + | <key>PayloadDescription</key> | |
| 29 | + | <string>Installs the Gordon internal CA root certificate</string> | |
| 30 | + | <key>PayloadDisplayName</key> | |
| 31 | + | <string>%s</string> | |
| 32 | + | <key>PayloadIdentifier</key> | |
| 33 | + | <string>dev.gordon.ca.%s</string> | |
| 34 | + | <key>PayloadType</key> | |
| 35 | + | <string>com.apple.security.root</string> | |
| 36 | + | <key>PayloadUUID</key> | |
| 37 | + | <string>%s</string> | |
| 38 | + | <key>PayloadVersion</key> | |
| 39 | + | <integer>1</integer> | |
| 40 | + | </dict> | |
| 41 | + | </array> | |
| 42 | + | <key>PayloadDescription</key> | |
| 43 | + | <string>Trust Gordon's internal Certificate Authority for HTTPS access</string> | |
| 44 | + | <key>PayloadDisplayName</key> | |
| 45 | + | <string>Gordon CA Trust</string> | |
| 46 | + | <key>PayloadIdentifier</key> | |
| 47 | + | <string>dev.gordon.ca-profile</string> | |
| 48 | + | <key>PayloadType</key> | |
| 49 | + | <string>Configuration</string> | |
| 50 | + | <key>PayloadUUID</key> | |
| 51 | + | <string>%s</string> | |
| 52 | + | <key>PayloadVersion</key> | |
| 53 | + | <integer>1</integer> | |
| 54 | + | </dict> | |
| 55 | + | </plist>`, b64Cert, rootCN, certUUID, certUUID, profileUUID) | |
| 56 | + | ||
| 57 | + | return []byte(profile) | |
| 58 | + | } | |
| @@ -0,0 +1,58 @@ | |||||
| 1 | + | package pki | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "encoding/base64" | |||
| 5 | + | "fmt" | |||
| 6 | + | ||||
| 7 | + | "github.com/google/uuid" | |||
| 8 | + | ) | |||
| 9 | + | ||||
| 10 | + | // GenerateMobileconfig generates an iOS configuration profile containing | |||
| 11 | + | // the root CA certificate for one-tap trust installation. | |||
| 12 | + | func GenerateMobileconfig(rootDER []byte, rootCN string) []byte { | |||
| 13 | + | b64Cert := base64.StdEncoding.EncodeToString(rootDER) | |||
| 14 | + | profileUUID := uuid.New().String() | |||
| 15 | + | certUUID := uuid.New().String() | |||
| 16 | + | ||||
| 17 | + | profile := fmt.Sprintf(`<?xml version="1.0" encoding="UTF-8"?> | |||
| 18 | + | <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | |||
| 19 | + | <plist version="1.0"> | |||
| 20 | + | <dict> | |||
| 21 | + | <key>PayloadContent</key> | |||
| 22 | + | <array> | |||
| 23 | + | <dict> | |||
| 24 | + | <key>PayloadCertificateFileName</key> | |||
| 25 | + | <string>gordon-ca.crt</string> | |||
| 26 | + | <key>PayloadContent</key> | |||
| 27 | + | <data>%s</data> | |||
| 28 | + | <key>PayloadDescription</key> | |||
| 29 | + | <string>Installs the Gordon internal CA root certificate</string> | |||
| 30 | + | <key>PayloadDisplayName</key> | |||
| 31 | + | <string>%s</string> | |||
| 32 | + | <key>PayloadIdentifier</key> | |||
| 33 | + | <string>dev.gordon.ca.%s</string> | |||
| 34 | + | <key>PayloadType</key> | |||
| 35 | + | <string>com.apple.security.root</string> | |||
| 36 | + | <key>PayloadUUID</key> | |||
| 37 | + | <string>%s</string> | |||
| 38 | + | <key>PayloadVersion</key> | |||
| 39 | + | <integer>1</integer> | |||
| 40 | + | </dict> | |||
| 41 | + | </array> | |||
| 42 | + | <key>PayloadDescription</key> | |||
| 43 | + | <string>Trust Gordon's internal Certificate Authority for HTTPS access</string> | |||
| 44 | + | <key>PayloadDisplayName</key> | |||
| 45 | + | <string>Gordon CA Trust</string> | |||
| 46 | + | <key>PayloadIdentifier</key> | |||
| 47 | + | <string>dev.gordon.ca-profile</string> | |||
| 48 | + | <key>PayloadType</key> | |||
| 49 | + | <string>Configuration</string> | |||
| 50 | + | <key>PayloadUUID</key> | |||
| 51 | + | <string>%s</string> | |||
| 52 | + | <key>PayloadVersion</key> | |||
| 53 | + | <integer>1</integer> | |||
| 54 | + | </dict> | |||
| 55 | + | </plist>`, b64Cert, rootCN, certUUID, certUUID, profileUUID) | |||
| 56 | + | ||||
| 57 | + | return []byte(profile) | |||
| 58 | + | } | |||
A internal/adapters/out/pki/mobileconfig_test.go +22 -0
| @@ -0,0 +1,22 @@ | |||
| 1 | + | package pki_test | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "strings" | |
| 5 | + | "testing" | |
| 6 | + | ||
| 7 | + | "github.com/bnema/gordon/internal/adapters/out/pki" | |
| 8 | + | "github.com/stretchr/testify/assert" | |
| 9 | + | "github.com/stretchr/testify/require" | |
| 10 | + | ) | |
| 11 | + | ||
| 12 | + | func TestGenerateMobileconfig(t *testing.T) { | |
| 13 | + | dir := t.TempDir() | |
| 14 | + | ca, err := pki.NewCA(dir, testLogger()) | |
| 15 | + | require.NoError(t, err) | |
| 16 | + | ||
| 17 | + | mc := pki.GenerateMobileconfig(ca.RootCertificateDER(), ca.RootCommonName()) | |
| 18 | + | assert.True(t, strings.HasPrefix(string(mc), "<?xml")) | |
| 19 | + | assert.Contains(t, string(mc), "Gordon Internal CA") | |
| 20 | + | assert.Contains(t, string(mc), "<key>PayloadType</key>") | |
| 21 | + | assert.Contains(t, string(mc), "com.apple.security.root") | |
| 22 | + | } | |
| @@ -0,0 +1,22 @@ | |||||
| 1 | + | package pki_test | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "strings" | |||
| 5 | + | "testing" | |||
| 6 | + | ||||
| 7 | + | "github.com/bnema/gordon/internal/adapters/out/pki" | |||
| 8 | + | "github.com/stretchr/testify/assert" | |||
| 9 | + | "github.com/stretchr/testify/require" | |||
| 10 | + | ) | |||
| 11 | + | ||||
| 12 | + | func TestGenerateMobileconfig(t *testing.T) { | |||
| 13 | + | dir := t.TempDir() | |||
| 14 | + | ca, err := pki.NewCA(dir, testLogger()) | |||
| 15 | + | require.NoError(t, err) | |||
| 16 | + | ||||
| 17 | + | mc := pki.GenerateMobileconfig(ca.RootCertificateDER(), ca.RootCommonName()) | |||
| 18 | + | assert.True(t, strings.HasPrefix(string(mc), "<?xml")) | |||
| 19 | + | assert.Contains(t, string(mc), "Gordon Internal CA") | |||
| 20 | + | assert.Contains(t, string(mc), "<key>PayloadType</key>") | |||
| 21 | + | assert.Contains(t, string(mc), "com.apple.security.root") | |||
| 22 | + | } | |||
A internal/adapters/out/pki/trust.go +23 -0
| @@ -0,0 +1,23 @@ | |||
| 1 | + | package pki | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "crypto/x509" | |
| 5 | + | ||
| 6 | + | "github.com/smallstep/truststore" | |
| 7 | + | ) | |
| 8 | + | ||
| 9 | + | // InstallRoot installs the root CA certificate into the system trust store. | |
| 10 | + | func InstallRoot(cert *x509.Certificate) error { | |
| 11 | + | return truststore.Install(cert, | |
| 12 | + | truststore.WithFirefox(), | |
| 13 | + | truststore.WithJava(), | |
| 14 | + | ) | |
| 15 | + | } | |
| 16 | + | ||
| 17 | + | // UninstallRoot removes the root CA certificate from the system trust store. | |
| 18 | + | func UninstallRoot(cert *x509.Certificate) error { | |
| 19 | + | return truststore.Uninstall(cert, | |
| 20 | + | truststore.WithFirefox(), | |
| 21 | + | truststore.WithJava(), | |
| 22 | + | ) | |
| 23 | + | } | |
| @@ -0,0 +1,23 @@ | |||||
| 1 | + | package pki | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "crypto/x509" | |||
| 5 | + | ||||
| 6 | + | "github.com/smallstep/truststore" | |||
| 7 | + | ) | |||
| 8 | + | ||||
| 9 | + | // InstallRoot installs the root CA certificate into the system trust store. | |||
| 10 | + | func InstallRoot(cert *x509.Certificate) error { | |||
| 11 | + | return truststore.Install(cert, | |||
| 12 | + | truststore.WithFirefox(), | |||
| 13 | + | truststore.WithJava(), | |||
| 14 | + | ) | |||
| 15 | + | } | |||
| 16 | + | ||||
| 17 | + | // UninstallRoot removes the root CA certificate from the system trust store. | |||
| 18 | + | func UninstallRoot(cert *x509.Certificate) error { | |||
| 19 | + | return truststore.Uninstall(cert, | |||
| 20 | + | truststore.WithFirefox(), | |||
| 21 | + | truststore.WithJava(), | |||
| 22 | + | ) | |||
| 23 | + | } | |||