├── .devcontainer ├── devcontainer.json └── postCreate.sh ├── .github └── workflows │ └── go.yml ├── .gitignore ├── .goreleaser.yml ├── .vscode └── launch.json ├── FIXME.txt ├── LICENSE ├── README.md ├── action ├── env.go ├── issue │ ├── config.go │ ├── config_test.go │ ├── issue.go │ └── issue_test.go ├── serve │ ├── authprofile │ │ └── authprofile.go │ ├── cert.go │ ├── certificateservice │ │ └── service.go │ ├── certshandler │ │ └── handler.go │ ├── config.go │ ├── hello_service.go │ ├── httpzaplog │ │ └── handler.go │ ├── issuehandler │ │ └── handler.go │ ├── serve.go │ ├── tokenauth.go │ ├── tokenauth_test.go │ └── version_service.go └── setup │ ├── config.go │ ├── config_test.go │ ├── nameconstraints.go │ ├── nameconstraints_test.go │ ├── setup.go │ └── setup_test.go ├── buf.gen.yaml ├── buf.lock ├── buf.yaml ├── cmd └── kmgm │ ├── app │ ├── app.go │ └── appflags │ │ └── appflags.go │ ├── batch │ └── batch.go │ ├── issue │ └── issue.go │ ├── list │ └── list.go │ ├── main.go │ ├── main_test.go │ ├── remote │ ├── bootstrap │ │ └── bootstrap.go │ ├── issue │ │ └── issue.go │ ├── show │ │ └── show.go │ ├── subcommand.go │ └── version │ │ └── version.go │ ├── remote_test.go │ ├── serve │ ├── serve.go │ └── testserver │ │ └── testserver.go │ ├── setup │ └── setup.go │ ├── show │ ├── format.go │ ├── ifchanged.go │ └── show.go │ ├── testkmgm │ ├── gen.go │ ├── generate_testkeys.go │ ├── testkeys.go │ ├── testkeys_util.go │ └── testkmgm.go │ └── tool │ ├── dump │ └── dump.go │ ├── pubkeyhash │ ├── format.go │ └── pubkeyhash.go │ └── tool.go ├── consts └── consts.go ├── dname ├── config.go └── protoconv.go ├── docs ├── demo.svg └── tutorials │ └── nginx │ ├── .gitignore │ ├── README.md │ ├── chrome-warning.png │ ├── conf │ └── nginx.conf │ ├── docker.sh │ ├── pub │ └── index.html │ └── tls │ └── .placeholder ├── domainname ├── domainname.go ├── domainname_posix.go └── domainname_test.go ├── exporter └── exporter.go ├── frontend ├── editstruct.go ├── editstruct_test.go ├── frontend.go ├── noninteractive.go ├── promptuife │ └── frontend.go └── validate │ ├── path.go │ └── pkixelement.go ├── gen.go ├── go.mod ├── go.sum ├── httperr └── error.go ├── ipapi ├── ipapi.go └── ipapi_test.go ├── keyusage ├── keyusage.go ├── keyusage_test.go ├── x509.go └── x509_test.go ├── pb ├── apiversion.go ├── kmgm.pb.go ├── kmgm.proto └── kmgm_grpc.pb.go ├── pemparser ├── consts.go ├── marshaller.go ├── parser.go └── parser_test.go ├── period ├── days.go ├── days_test.go ├── validityperiod.go └── validityperiod_test.go ├── remote ├── conn.go ├── conn_test.go ├── hello │ └── hello.go ├── issue │ └── issue.go ├── transportcredentials.go └── user │ ├── authorization.go │ ├── context.go │ └── user.go ├── renovate.json ├── san ├── protoconv.go ├── san.go └── san_test.go ├── storage ├── config.go ├── issuedb │ ├── issuedb.go │ ├── issuedb_test.go │ └── state.go ├── k8ssecret.go ├── storage.go └── storage_test.go ├── structflags ├── flags.go └── tag.go ├── testutils └── testutils.go ├── tools └── demoenv │ ├── bashrc │ ├── demoenv.sh │ └── sh_wrap.sh ├── version └── version.go └── wcrypto ├── cert.go ├── cert_test.go ├── key.go ├── keytype.go └── token.go /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kmgm", 3 | "image": "mcr.microsoft.com/devcontainers/go", 4 | "customizations": { 5 | "vscode": { 6 | "settings": { 7 | "terminal.integrated.defaultProfile.linux": "zsh", 8 | "terminal.integrated.profiles.linux": { 9 | "zsh": { 10 | "path": "/usr/bin/zsh", 11 | }, 12 | }, 13 | }, 14 | "extensions": [ 15 | "golang.Go" 16 | ] 17 | } 18 | }, 19 | "postCreateCommand": "./.devcontainer/postCreate.sh", 20 | "remoteUser": "vscode" 21 | } 22 | -------------------------------------------------------------------------------- /.devcontainer/postCreate.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | PREFIX="/usr/local" && \ 3 | VERSION="1.46.0" && \ 4 | curl -sSL \ 5 | "https://github.com/bufbuild/buf/releases/download/v${VERSION}/buf-$(uname -s)-$(uname -m).tar.gz" | \ 6 | sudo tar -xvzf - -C "${PREFIX}" --strip-components 1 7 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | name: Test and Release 3 | jobs: 4 | unit_tests: 5 | strategy: 6 | matrix: 7 | platform: [ubuntu-latest, macos-latest] 8 | runs-on: ${{ matrix.platform }} 9 | steps: 10 | - name: Install Go 11 | uses: actions/setup-go@v5 12 | with: 13 | go-version: '>= 1.21' 14 | - name: Checkout code 15 | uses: actions/checkout@v4 16 | - name: Test 17 | run: go test ./... 18 | release: 19 | needs: [unit_tests] 20 | if: contains(github.ref, 'tags/v') 21 | runs-on: ubuntu-latest 22 | steps: 23 | - name: Set up Go 24 | uses: actions/setup-go@v5 25 | with: 26 | go-version: '>= 1.21' 27 | - name: Checkout 28 | uses: actions/checkout@v4 29 | - name: Run GoReleaser 30 | uses: goreleaser/goreleaser-action@v6 31 | with: 32 | version: latest 33 | args: release --clean 34 | env: 35 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | kmgm 2 | !kmgm/ 3 | *.pem 4 | dist/ 5 | cover.out 6 | trace.out 7 | *.pprof 8 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | before: 2 | hooks: 3 | # - go mod tidy 4 | # - go generate ./... 5 | builds: 6 | - main: ./cmd/kmgm/main.go 7 | 8 | env: 9 | - CGO_ENABLED=0 10 | 11 | goarch: 12 | - amd64 13 | - arm64 14 | 15 | goos: 16 | - linux 17 | - darwin 18 | 19 | ldflags: 20 | - -s -w 21 | - -X github.com/IPA-CyberLab/kmgm/version.Version={{.Version}} 22 | - -X github.com/IPA-CyberLab/kmgm/version.Commit={{.Commit}} 23 | # - -X main.date={{.Date}} -X main.builtBy=goreleaser 24 | 25 | archives: 26 | - name_template: >- 27 | {{- .ProjectName }}_ 28 | {{- title .Os }}_ 29 | {{- if eq .Arch "amd64" }}x86_64 30 | {{- else if eq .Arch "386" }}i386 31 | {{- else }}{{ .Arch }}{{ end }} 32 | {{- if .Arm }}v{{ .Arm }}{{ end -}} 33 | 34 | checksum: 35 | name_template: 'checksums.txt' 36 | 37 | snapshot: 38 | name_template: "{{ .Tag }}-next" 39 | 40 | changelog: 41 | sort: asc 42 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Launch", 9 | "type": "go", 10 | "request": "launch", 11 | "mode": "test", 12 | "program": "${fileDirname}", 13 | "env": {}, 14 | "args": [] 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /FIXME.txt: -------------------------------------------------------------------------------- 1 | // FIXME[P1]: make profile configurable via config file. 2 | 3 | // FIXME[P0]: output cert chain, not just leaf 4 | 5 | // FIXME[P1]: remote issue test: check perm 6 | 7 | // FIXME[P2]: consistency: Certificate -> Cert 8 | // FIXME[P2]: zip / PKCS #12 support 9 | 10 | // FIXME[P3]: prometheus support 11 | 12 | // FIXME[PX]: 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # kmgm 2 | **:closed_lock_with_key::link: Generate certs for your cluster, easy way** 3 | 4 | [![Build Status][gh-actions-badge]][gh-actions] 5 | [![go report][go-report-badge]][go-report] 6 | 7 | kmgm is a [certificate authority](https://en.wikipedia.org/wiki/Certificate_authority) with focus on its ease of use. Setup certificates and deploy to your cluster in minutes! 8 | 9 | ![demo session][demo-session-svg] 10 | 11 | ## Installation 12 | 13 | Linux, macOS: 14 | 15 | Install a pre-built binary of the latest version: 16 | 17 | ```sh 18 | curl -L https://github.com/IPA-CyberLab/kmgm/releases/latest/download/kmgm_$(uname)_$(uname -m).tar.gz | sudo tar zx -C /usr/local/bin kmgm 19 | ``` 20 | 21 | Install a pre-built binary of a specific version: 22 | 23 | ```sh 24 | VER=0.3.0; curl -L https://github.com/IPA-CyberLab/kmgm/releases/download/v${VER}/kmgm_$(uname)_$(uname -m).tar.gz | sudo tar zx -C /usr/local/bin kmgm 25 | ``` 26 | 27 | or, to build it yourself: 28 | 29 | ```sh 30 | go get -v -u github.com/IPA-CyberLab/kmgm/cmd/... 31 | ``` 32 | 33 | ## Quick start 34 | 35 | Setup a new CA: 36 | ```sh 37 | kmgm setup 38 | ``` 39 | 40 | Issue a new certificate: 41 | ```sh 42 | kmgm issue 43 | ``` 44 | 45 | ## Tutorials 46 | 47 | - [Setup nginx with kmgm issued certificate](https://github.com/IPA-CyberLab/kmgm/blob/master/docs/tutorials/nginx/README.md) 48 | 49 | ## License 50 | 51 | kmgm is licensed under Apache license version 2.0. See [LICENSE](https://github.com/IPA-CyberLab/kmgm/blob/master/LICENSE) for more information. 52 | 53 | 54 | [go-report-badge]: https://goreportcard.com/badge/github.com/IPA-CyberLab/kmgm 55 | [go-report]: https://goreportcard.com/report/github.com/IPA-CyberLab/kmgm 56 | [gh-actions-badge]: https://github.com/IPA-CyberLab/kmgm/workflows/Test%20and%20Release/badge.svg 57 | [gh-actions]: https://github.com/IPA-CyberLab/kmgm/actions 58 | [demo-session-svg]: https://raw.githubusercontent.com/IPA-CyberLab/kmgm/master/docs/demo.svg 59 | -------------------------------------------------------------------------------- /action/env.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "context" 5 | "crypto" 6 | "crypto/rand" 7 | "errors" 8 | "fmt" 9 | "io" 10 | "io/ioutil" 11 | "os" 12 | "time" 13 | 14 | "go.uber.org/zap" 15 | "google.golang.org/grpc" 16 | "gopkg.in/yaml.v3" 17 | 18 | "github.com/IPA-CyberLab/kmgm/frontend" 19 | "github.com/IPA-CyberLab/kmgm/remote" 20 | "github.com/IPA-CyberLab/kmgm/remote/hello" 21 | "github.com/IPA-CyberLab/kmgm/storage" 22 | "github.com/IPA-CyberLab/kmgm/wcrypto" 23 | ) 24 | 25 | type Environment struct { 26 | Storage *storage.Storage 27 | Randr io.Reader 28 | Frontend frontend.Frontend 29 | Logger *zap.Logger 30 | NowImpl func() time.Time 31 | PregenKeySupplier func(wcrypto.KeyType) crypto.PrivateKey 32 | 33 | ProfileName string 34 | 35 | ConnectionInfo remote.ConnectionInfo 36 | ClientConn *grpc.ClientConn 37 | } 38 | 39 | func NewEnvironment(fe frontend.Frontend, stor *storage.Storage) (*Environment, error) { 40 | l := zap.L() 41 | 42 | cfg := &Environment{ 43 | Storage: stor, 44 | Randr: rand.Reader, 45 | Frontend: fe, 46 | Logger: l, 47 | NowImpl: time.Now, 48 | 49 | ProfileName: storage.DefaultProfileName, 50 | } 51 | return cfg, nil 52 | } 53 | 54 | func (env *Environment) Clone() *Environment { 55 | return &Environment{ 56 | Storage: env.Storage, 57 | Randr: env.Randr, 58 | Frontend: env.Frontend, 59 | Logger: env.Logger, 60 | NowImpl: env.NowImpl, 61 | 62 | ProfileName: env.ProfileName, 63 | 64 | ConnectionInfo: env.ConnectionInfo, 65 | ClientConn: nil, // Don't clone 66 | } 67 | } 68 | 69 | func (env *Environment) SaveConnectionInfo() error { 70 | path := env.Storage.ConnectionInfoPath() 71 | 72 | bs, err := yaml.Marshal(env.ConnectionInfo) 73 | if err != nil { 74 | return fmt.Errorf("Failed to marshal ConnectionInfo to yaml: %w", err) 75 | } 76 | 77 | if err := ioutil.WriteFile(path, bs, 0644); err != nil { 78 | return fmt.Errorf("Failed to write server connection info to %q: %w", path, err) 79 | } 80 | env.Logger.Sugar().Infof("Wrote server connection info to file %q.", path) 81 | 82 | return nil 83 | } 84 | 85 | func (env *Environment) LoadConnectionInfo() error { 86 | slog := env.Logger.Sugar() 87 | path := env.Storage.ConnectionInfoPath() 88 | 89 | bs, err := os.ReadFile(path) 90 | if err != nil { 91 | if errors.Is(err, os.ErrNotExist) { 92 | slog.Warnf("Could not find server connection info file %q. Ignoring.", path) 93 | return nil 94 | } 95 | return err 96 | } 97 | 98 | if err := yaml.Unmarshal(bs, &env.ConnectionInfo); err != nil { 99 | return fmt.Errorf("Failed to unmarshal server connection info: %w", err) 100 | } 101 | 102 | return nil 103 | } 104 | 105 | func (env *Environment) EnsureClientConn(ctx context.Context) error { 106 | if env.ClientConn != nil { 107 | return nil 108 | } 109 | 110 | conn, err := env.ConnectionInfo.Dial(ctx, env.Logger) 111 | if err != nil { 112 | return err 113 | } 114 | if hello.Hello(ctx, conn, env.Logger); err != nil { 115 | return err 116 | } 117 | 118 | env.ClientConn = conn 119 | return nil 120 | } 121 | 122 | func (env *Environment) Profile() (*storage.Profile, error) { 123 | return env.Storage.Profile(env.ProfileName) 124 | } 125 | 126 | var GlobalEnvironment *Environment 127 | -------------------------------------------------------------------------------- /action/issue/config.go: -------------------------------------------------------------------------------- 1 | package issue 2 | 3 | import ( 4 | "crypto/x509" 5 | "errors" 6 | "fmt" 7 | "log" 8 | "os" 9 | "time" 10 | 11 | "go.uber.org/multierr" 12 | 13 | "github.com/IPA-CyberLab/kmgm/dname" 14 | "github.com/IPA-CyberLab/kmgm/keyusage" 15 | "github.com/IPA-CyberLab/kmgm/period" 16 | "github.com/IPA-CyberLab/kmgm/san" 17 | "github.com/IPA-CyberLab/kmgm/wcrypto" 18 | ) 19 | 20 | type Config struct { 21 | Subject *dname.Config `yaml:"subject" flags:""` 22 | Names san.Names `yaml:"subjectAltNames" flags:"subject-alt-name,set cert subjectAltNames,san"` 23 | KeyUsage keyusage.KeyUsage `yaml:"keyUsage" flags:"key-usage,what the key/cert is used for (tlsServer, tlsClient, tlsClientServer),ku"` 24 | Validity period.ValidityPeriod `yaml:"validity" flags:"validity,time duration/timestamp where the cert is valid to (examples: 30d, 1y, 20220530)"` 25 | KeyType wcrypto.KeyType `yaml:"keyType" flags:"key-type,private key type (rsa, ecdsa),t"` 26 | 27 | // Don't create issuedb entry. 28 | NoIssueDBEntry bool 29 | } 30 | 31 | func DefaultConfig(baseSubject *dname.Config) *Config { 32 | var ns san.Names 33 | 34 | override := os.Getenv("KMGM_DEFAULT_NAMES") 35 | if override != "" { 36 | var err error 37 | ns, err = san.Parse(override) 38 | if err != nil { 39 | log.Panicf("Failed to parse KMGM_DEFAULT_NAMES %q: %v", override, err) 40 | } 41 | } else { 42 | ns = san.ForThisHost() 43 | } 44 | 45 | return &Config{ 46 | Subject: dname.DefaultConfig("", baseSubject), 47 | Names: ns, 48 | KeyUsage: keyusage.KeyUsageTLSClientServer.Clone(), 49 | Validity: period.ValidityPeriod{Days: 820}, 50 | KeyType: wcrypto.KeyAny, 51 | } 52 | } 53 | 54 | func EmptyConfig() *Config { 55 | return &Config{ 56 | Subject: &dname.Config{}, 57 | } 58 | } 59 | 60 | func (o *Config) Clone() *Config { 61 | return &Config{ 62 | Subject: o.Subject.Clone(), 63 | Names: o.Names.Clone(), 64 | KeyUsage: o.KeyUsage, 65 | Validity: o.Validity, 66 | KeyType: o.KeyType, 67 | NoIssueDBEntry: o.NoIssueDBEntry, 68 | } 69 | } 70 | 71 | func ConfigFromCert(cert *x509.Certificate) (*Config, error) { 72 | kt, err := wcrypto.KeyTypeOfPub(cert.PublicKey) 73 | if err != nil { 74 | return nil, err 75 | } 76 | 77 | return &Config{ 78 | Subject: dname.FromPkixName(cert.Subject), 79 | Names: san.FromCertificate(cert), 80 | KeyUsage: keyusage.FromCertificate(cert), 81 | Validity: period.ValidityPeriod{NotAfter: cert.NotAfter}, 82 | KeyType: kt, 83 | }, nil 84 | } 85 | 86 | func (a *Config) CompatibleWith(b *Config) error { 87 | if err := a.Subject.CompatibleWith(b.Subject); err != nil { 88 | return err 89 | } 90 | if err := a.Names.CompatibleWith(b.Names); err != nil { 91 | return err 92 | } 93 | if !a.KeyUsage.Equals(b.KeyUsage) { 94 | return fmt.Errorf("KeyUsage mismatch: %v != %v", a.KeyUsage, b.KeyUsage) 95 | } 96 | if err := a.KeyType.CompatibleWith(b.KeyType); err != nil { 97 | return err 98 | } 99 | return nil 100 | } 101 | 102 | const expireThreshold = 30 * time.Second 103 | 104 | var ErrValidityPeriodExpired = errors.New("Declining to issue certificate which expires within 30 seconds.") 105 | 106 | func (cfg *Config) Verify(now time.Time) error { 107 | var merr error 108 | if err := cfg.Subject.Verify(); err != nil { 109 | merr = fmt.Errorf("Subject.%w", err) 110 | } 111 | if err := cfg.Names.Verify(); err != nil { 112 | merr = multierr.Append(merr, fmt.Errorf("Names.%w", err)) 113 | } 114 | if err := cfg.KeyUsage.Verify(); err != nil { 115 | merr = multierr.Append(merr, fmt.Errorf("KeyUsage.%w", err)) 116 | } 117 | if cfg.Validity.GetNotAfter(now).Before(now.Add(expireThreshold)) { 118 | merr = multierr.Append(merr, ErrValidityPeriodExpired) 119 | } 120 | 121 | return merr 122 | } 123 | -------------------------------------------------------------------------------- /action/issue/config_test.go: -------------------------------------------------------------------------------- 1 | package issue_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/IPA-CyberLab/kmgm/action/issue" 7 | "github.com/IPA-CyberLab/kmgm/dname" 8 | "github.com/IPA-CyberLab/kmgm/keyusage" 9 | "github.com/IPA-CyberLab/kmgm/pemparser" 10 | "github.com/IPA-CyberLab/kmgm/san" 11 | "github.com/IPA-CyberLab/kmgm/wcrypto" 12 | ) 13 | 14 | const testCertPem = `-----BEGIN CERTIFICATE----- 15 | MIIBmjCCAUGgAwIBAgIIYw/GUWnaxD4wCgYIKoZIzj0EAwIwEjEQMA4GA1UEAxMH 16 | dGVzdCBDQTAeFw0xOTEyMjkxMjUxMDNaFw0yMjAzMjgxMjUyMDNaMBIxEDAOBgNV 17 | BAMMB3Rlc3RfY24wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASJNqgEYpQNmZO9 18 | fDPoqs84G4vl++6gJyumdRny+OX/lqLlb6VdYFmfd5S7XhCHUUp0jGulQO7WxDsn 19 | cXLxHu9do4GAMH4wDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMC 20 | BggrBgEFBQcDATAMBgNVHRMBAf8EAjAAMB8GA1UdIwQYMBaAFEMkkSwCNkQq7Z6N 21 | MWGhLNpHv5z6MB4GA1UdEQQXMBWCE3Rlc3QtY24uZXhhbXBsZS5jb20wCgYIKoZI 22 | zj0EAwIDRwAwRAIgRtlJshmnNsQKwvMYMTiF4a8XXu1aytz3cmVi8NMy4ykCIAVn 23 | cXdZYB/A9OZlZbB1J6dhVN8z9owmfdznO7ln3Iol 24 | -----END CERTIFICATE-----` 25 | 26 | func TestConfigFromCert(t *testing.T) { 27 | certs, err := pemparser.ParseCertificates([]byte(testCertPem)) 28 | if err != nil { 29 | t.Fatalf("%v", err) 30 | } 31 | if len(certs) != 1 { 32 | t.Fatalf("len(certs)=%d", len(certs)) 33 | } 34 | 35 | cert := certs[0] 36 | cfg, err := issue.ConfigFromCert(cert) 37 | if err != nil { 38 | t.Fatalf("%v", err) 39 | } 40 | if cfg.KeyType != wcrypto.KeySECP256R1 { 41 | t.Errorf("KeyType") 42 | } 43 | expected := &issue.Config{ 44 | Subject: &dname.Config{ 45 | CommonName: "test_cn", 46 | }, 47 | Names: san.MustParse("test-cn.example.com"), 48 | KeyUsage: keyusage.KeyUsageTLSClientServer.Clone(), 49 | KeyType: wcrypto.KeySECP256R1, 50 | } 51 | if err := cfg.CompatibleWith(expected); err != nil { 52 | t.Fatalf("err: %v", err) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /action/issue/issue_test.go: -------------------------------------------------------------------------------- 1 | package issue_test 2 | 3 | import ( 4 | "crypto/x509" 5 | "errors" 6 | "io/ioutil" 7 | "os" 8 | "testing" 9 | "time" 10 | 11 | "github.com/IPA-CyberLab/kmgm/action" 12 | "github.com/IPA-CyberLab/kmgm/action/issue" 13 | "github.com/IPA-CyberLab/kmgm/action/setup" 14 | "github.com/IPA-CyberLab/kmgm/dname" 15 | "github.com/IPA-CyberLab/kmgm/frontend" 16 | "github.com/IPA-CyberLab/kmgm/keyusage" 17 | "github.com/IPA-CyberLab/kmgm/period" 18 | "github.com/IPA-CyberLab/kmgm/storage" 19 | "github.com/IPA-CyberLab/kmgm/wcrypto" 20 | "go.uber.org/zap" 21 | ) 22 | 23 | func init() { 24 | logger, err := zap.NewDevelopment() 25 | if err != nil { 26 | panic(err) 27 | } 28 | zap.ReplaceGlobals(logger) 29 | } 30 | 31 | func testEnv(t *testing.T) (*action.Environment, func()) { 32 | t.Helper() 33 | 34 | basedir, err := ioutil.TempDir("", "kmgm_issue_test") 35 | if err != nil { 36 | t.Fatalf("ioutil.TempDir: %v", err) 37 | } 38 | 39 | stor, err := storage.New(basedir) 40 | if err != nil { 41 | t.Fatalf("storage.New: %v", err) 42 | } 43 | 44 | fe := &frontend.NonInteractive{ 45 | Logger: zap.L(), 46 | } 47 | 48 | env, err := action.NewEnvironment(fe, stor) 49 | if err != nil { 50 | t.Fatalf("action.NewEnvironment: %v", err) 51 | } 52 | env.Frontend = &frontend.NonInteractive{Logger: zap.L()} 53 | 54 | cfg := setup.DefaultConfig(nil) 55 | if err := setup.Run(env, cfg); err != nil { 56 | t.Fatalf("setup.Run: %v", err) 57 | } 58 | 59 | return env, func() { 60 | os.RemoveAll(basedir) 61 | } 62 | } 63 | 64 | func TestIssue(t *testing.T) { 65 | env, teardown := testEnv(t) 66 | t.Cleanup(teardown) 67 | 68 | profile, err := env.Profile() 69 | if err != nil { 70 | t.Fatalf("%v", err) 71 | } 72 | cacert, err := profile.ReadCACertificate() 73 | if err != nil { 74 | t.Fatalf("%v", err) 75 | } 76 | 77 | priv, err := wcrypto.GenerateKey(env.Randr, wcrypto.KeySECP256R1, "", env.Logger) 78 | if err != nil { 79 | t.Fatalf("%v", err) 80 | } 81 | pub, err := wcrypto.ExtractPublicKey(priv) 82 | if err != nil { 83 | t.Fatalf("%v", err) 84 | } 85 | 86 | t.Run("Default", func(t *testing.T) { 87 | cfg := issue.DefaultConfig(nil) 88 | 89 | certDer, err := issue.Run(env, pub, cfg) 90 | if err != nil { 91 | t.Fatalf("issue.Run: %v", err) 92 | } 93 | 94 | cert, err := x509.ParseCertificate(certDer) 95 | if err != nil { 96 | t.Fatalf("x509.ParseCertificate: %v", err) 97 | } 98 | 99 | certpool := x509.NewCertPool() 100 | certpool.AddCert(cacert) 101 | 102 | if _, err := cert.Verify(x509.VerifyOptions{ 103 | Roots: certpool, 104 | KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, 105 | CurrentTime: time.Now(), 106 | }); err != nil { 107 | t.Errorf("%v", err) 108 | } 109 | if _, err := cert.Verify(x509.VerifyOptions{ 110 | Roots: certpool, 111 | KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, 112 | CurrentTime: time.Now().Add(800 * 24 * time.Hour), 113 | }); err != nil { 114 | t.Errorf("%v", err) 115 | } 116 | if _, err := cert.Verify(x509.VerifyOptions{ 117 | Roots: certpool, 118 | KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, 119 | CurrentTime: time.Now().Add(900 * 24 * time.Hour), 120 | }); err == nil { 121 | t.Errorf("should have expired") 122 | } 123 | }) 124 | 125 | t.Run("WrongKeyType", func(t *testing.T) { 126 | cfg := &issue.Config{ 127 | Subject: &dname.Config{CommonName: "test_cn"}, 128 | KeyUsage: keyusage.KeyUsageTLSClientServer.Clone(), 129 | Validity: period.ValidityPeriod{Days: 30}, 130 | KeyType: wcrypto.KeyRSA4096, 131 | } 132 | 133 | _, err := issue.Run(env, pub, cfg) 134 | if errors.Is(err, &wcrypto.UnexpectedKeyTypeErr{}) { 135 | t.Fatalf("issue.Run: %v", err) 136 | } 137 | }) 138 | } 139 | -------------------------------------------------------------------------------- /action/serve/authprofile/authprofile.go: -------------------------------------------------------------------------------- 1 | package authprofile 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/IPA-CyberLab/kmgm/action" 8 | "github.com/IPA-CyberLab/kmgm/action/setup" 9 | "github.com/IPA-CyberLab/kmgm/consts" 10 | "github.com/IPA-CyberLab/kmgm/dname" 11 | "github.com/IPA-CyberLab/kmgm/storage" 12 | "github.com/IPA-CyberLab/kmgm/wcrypto" 13 | ) 14 | 15 | // The name of internal storage profile to serve as kmgm HTTPS/gRPC server client auth. 16 | const ProfileName = consts.AuthProfileName 17 | 18 | func Ensure(env *action.Environment) (*storage.Profile, error) { 19 | slog := env.Logger.Sugar() 20 | 21 | profile, err := env.Storage.Profile(ProfileName) 22 | if err != nil { 23 | return nil, err 24 | } 25 | 26 | st := profile.Status(env.NowImpl()) 27 | switch st.Code { 28 | case storage.ValidCA: 29 | return profile, nil 30 | case storage.NotCA: 31 | slog.Infof("Setting up CA for kmgm HTTPS/gRPC server.") 32 | start := time.Now() 33 | defer func() { 34 | slog.Infow("Setting up CA for kmgm HTTPS/gRPC server... Done.", "took", time.Now().Sub(start)) 35 | }() 36 | 37 | // Create CA 38 | envS := env.Clone() 39 | envS.ProfileName = ProfileName 40 | 41 | cfg := setup.DefaultConfig(nil) 42 | cfg.Subject = &dname.Config{ 43 | CommonName: "kmgm serverauth CA", 44 | } 45 | cfg.KeyType = wcrypto.ServerKeyType 46 | 47 | if err := setup.Run(envS, cfg); err != nil { 48 | return nil, fmt.Errorf("Failed to setup serverauth CA: %v", err) 49 | } 50 | 51 | if st := profile.Status(env.NowImpl()); st.Code != storage.ValidCA { 52 | return nil, st 53 | } 54 | return profile, nil 55 | default: 56 | return nil, st 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /action/serve/cert.go: -------------------------------------------------------------------------------- 1 | package serve 2 | 3 | import ( 4 | "crypto" 5 | "crypto/tls" 6 | "crypto/x509" 7 | "errors" 8 | "fmt" 9 | "os" 10 | "time" 11 | 12 | "github.com/IPA-CyberLab/kmgm/action" 13 | "github.com/IPA-CyberLab/kmgm/action/issue" 14 | "github.com/IPA-CyberLab/kmgm/action/serve/authprofile" 15 | "github.com/IPA-CyberLab/kmgm/dname" 16 | "github.com/IPA-CyberLab/kmgm/keyusage" 17 | "github.com/IPA-CyberLab/kmgm/period" 18 | "github.com/IPA-CyberLab/kmgm/san" 19 | "github.com/IPA-CyberLab/kmgm/storage" 20 | "github.com/IPA-CyberLab/kmgm/wcrypto" 21 | ) 22 | 23 | func ensurePrivateKey(env *action.Environment, authp *storage.Profile) (crypto.PrivateKey, error) { 24 | priv, err := authp.ReadServerPrivateKey() 25 | if err != nil { 26 | if !errors.Is(err, os.ErrNotExist) { 27 | return nil, err 28 | } 29 | 30 | priv, err := wcrypto.GenerateKey(env.Randr, wcrypto.ServerKeyType, "server ", env.Logger) 31 | if err != nil { 32 | return nil, err 33 | } 34 | if err := authp.WriteServerPrivateKey(priv); err != nil { 35 | return nil, err 36 | } 37 | 38 | return priv, nil 39 | } 40 | 41 | return priv, nil 42 | } 43 | 44 | func ensureServerCert(env *action.Environment, authp *storage.Profile, ns san.Names) (*tls.Certificate, string, error) { 45 | l := env.Logger.Sugar().Named("ensureServerCert") 46 | 47 | priv, err := ensurePrivateKey(env, authp) 48 | if err != nil { 49 | return nil, "", err 50 | } 51 | pub, err := wcrypto.ExtractPublicKey(priv) 52 | if err != nil { 53 | return nil, "", err 54 | } 55 | 56 | now := time.Now() 57 | 58 | cacert, err := authp.ReadCACertificate() 59 | if err != nil { 60 | return nil, "", err 61 | } 62 | if err := wcrypto.VerifyCACert(cacert, now); err != nil { 63 | return nil, "", err 64 | } 65 | 66 | cert, err := authp.ReadServerCertificate() 67 | if err == nil { 68 | // FIXME[P2]: also check if ns matches 69 | err = wcrypto.VerifyServerCert(cert, cacert, now) 70 | if err != nil { 71 | l.Infof("Successfully read the server certificate, but it was not valid: %v", err) 72 | cert = nil 73 | } 74 | } 75 | if err != nil { 76 | var srvEnv action.Environment 77 | srvEnv = *env 78 | srvEnv.ProfileName = authprofile.ProfileName 79 | issueCfg := issue.Config{ 80 | Subject: &dname.Config{ 81 | CommonName: "kmgm server", 82 | }, 83 | Names: ns, 84 | KeyUsage: keyusage.KeyUsageTLSServer.Clone(), 85 | Validity: period.FarFuture, 86 | KeyType: wcrypto.ServerKeyType, 87 | 88 | NoIssueDBEntry: true, 89 | } 90 | certDer, err := issue.Run(&srvEnv, pub, &issueCfg) 91 | if err != nil { 92 | return nil, "", fmt.Errorf("Failed to issue server cert: %w", err) 93 | } 94 | cert, err = x509.ParseCertificate(certDer) 95 | if err != nil { 96 | return nil, "", fmt.Errorf("Failed to parse server cert: %w", err) 97 | } 98 | 99 | if err := authp.WriteServerCertificate(cert); err != nil { 100 | return nil, "", err 101 | } 102 | } 103 | 104 | pub, ok := cert.PublicKey.(crypto.PublicKey) 105 | if !ok { 106 | return nil, "", errors.New("Failed to extract cert's public key as crypto.PublicKey") 107 | } 108 | tlscert := &tls.Certificate{ 109 | Certificate: [][]byte{cert.Raw, cacert.Raw}, 110 | PrivateKey: priv, 111 | } 112 | pubkeyhash, err := wcrypto.PubKeyPinString(pub) 113 | if err != nil { 114 | return nil, "", err 115 | } 116 | return tlscert, pubkeyhash, nil 117 | } 118 | -------------------------------------------------------------------------------- /action/serve/certificateservice/service.go: -------------------------------------------------------------------------------- 1 | package certificateservice 2 | 3 | import ( 4 | "context" 5 | "crypto/x509" 6 | "errors" 7 | "fmt" 8 | "time" 9 | 10 | "google.golang.org/grpc" 11 | "google.golang.org/grpc/codes" 12 | "google.golang.org/grpc/status" 13 | 14 | "github.com/IPA-CyberLab/kmgm/action" 15 | "github.com/IPA-CyberLab/kmgm/action/issue" 16 | "github.com/IPA-CyberLab/kmgm/action/setup" 17 | "github.com/IPA-CyberLab/kmgm/dname" 18 | "github.com/IPA-CyberLab/kmgm/keyusage" 19 | "github.com/IPA-CyberLab/kmgm/pb" 20 | "github.com/IPA-CyberLab/kmgm/pemparser" 21 | "github.com/IPA-CyberLab/kmgm/period" 22 | "github.com/IPA-CyberLab/kmgm/remote/user" 23 | "github.com/IPA-CyberLab/kmgm/san" 24 | "github.com/IPA-CyberLab/kmgm/storage" 25 | "github.com/IPA-CyberLab/kmgm/storage/issuedb" 26 | "github.com/IPA-CyberLab/kmgm/wcrypto" 27 | ) 28 | 29 | type Service struct { 30 | pb.UnimplementedCertificateServiceServer 31 | 32 | env *action.Environment 33 | } 34 | 35 | var _ = pb.CertificateServiceServer(&Service{}) 36 | 37 | func New(env *action.Environment) (*Service, error) { 38 | return &Service{env: env}, nil 39 | } 40 | 41 | func PbKeyTypeToKeyType(pkt pb.KeyType) (wcrypto.KeyType, error) { 42 | switch pkt { 43 | case pb.KeyType_KEYTYPE_UNSPECIFIED: 44 | return wcrypto.KeyAny, nil 45 | case pb.KeyType_KEYTYPE_RSA4096: 46 | return wcrypto.KeyRSA4096, nil 47 | case pb.KeyType_KEYTYPE_SECP256R1: 48 | return wcrypto.KeySECP256R1, nil 49 | case pb.KeyType_KEYTYPE_RSA2048: 50 | return wcrypto.KeyRSA2048, nil 51 | default: 52 | return wcrypto.KeyAny, fmt.Errorf("Don't know how to convert pb.KeyType(%d)", pkt) 53 | } 54 | } 55 | 56 | func (svc *Service) SetupCA(ctx context.Context, req *pb.SetupCARequest) (*pb.SetupCAResponse, error) { 57 | slog := svc.env.Logger.Sugar() 58 | _ = slog 59 | 60 | u := user.FromContext(ctx) 61 | if !u.IsAllowedToSetupCA(req.Profile) { 62 | return nil, status.Errorf(codes.Unauthenticated, "%v is not allowed to issue certificate.", u) 63 | } 64 | 65 | kt, err := PbKeyTypeToKeyType(req.KeyType) 66 | if err != nil { 67 | return nil, status.Errorf(codes.InvalidArgument, "%v", err) 68 | } 69 | 70 | cfg := &setup.Config{ 71 | Subject: dname.FromProtoStruct(req.Subject), 72 | Validity: period.FarFuture, 73 | KeyType: kt, 74 | } 75 | if req.NotAfterUnixtime != 0 { 76 | cfg.Validity = period.ValidityPeriod{ 77 | NotAfter: time.Unix(req.NotAfterUnixtime, 0).UTC(), 78 | } 79 | } 80 | 81 | envP := svc.env.Clone() 82 | envP.ProfileName = req.Profile 83 | if err := setup.Run(envP, cfg); err != nil { 84 | if errors.Is(err, setup.ErrValidCAExist) { 85 | return nil, status.Errorf(codes.AlreadyExists, "%v", err) 86 | } 87 | 88 | return nil, status.Errorf(codes.Internal, "%v", err) 89 | } 90 | 91 | return &pb.SetupCAResponse{}, nil 92 | } 93 | 94 | func (svc *Service) IssuePreflight(ctx context.Context, req *pb.IssuePreflightRequest) (*pb.IssuePreflightResponse, error) { 95 | slog := svc.env.Logger.Sugar() 96 | 97 | u := user.FromContext(ctx) 98 | if !u.IsAllowedToIssueCertificate(req.Profile) { 99 | return nil, grpc.Errorf(codes.Unauthenticated, "%v is not allowed to issue certificate.", u) 100 | } 101 | 102 | if err := storage.VerifyProfileName(req.Profile); err != nil { 103 | return nil, grpc.Errorf(codes.InvalidArgument, "%v", err) 104 | } 105 | 106 | profile, err := svc.env.Storage.Profile(req.Profile) 107 | if err != nil { 108 | slog.Infof("IssuePreflight: Storage.Profile(%q) returned err: %v", req.Profile, err) 109 | return nil, grpc.Errorf(codes.NotFound, "Failed to access specified profile.") 110 | } 111 | if st := profile.Status(time.Now()); st.Code != storage.ValidCA { 112 | return nil, grpc.Errorf(codes.Internal, "Can't issue certificate from CA profile %q: %v", req.Profile, st) 113 | } 114 | 115 | return &pb.IssuePreflightResponse{}, nil 116 | } 117 | 118 | func (svc *Service) IssueCertificate(ctx context.Context, req *pb.IssueCertificateRequest) (*pb.IssueCertificateResponse, error) { 119 | // FIXME[P1]: log while issue -> rely on grpc middleware logging? 120 | u := user.FromContext(ctx) 121 | if !u.IsAllowedToIssueCertificate(req.Profile) { 122 | return nil, grpc.Errorf(codes.Unauthenticated, "%v is not allowed to issue certificate.", u) 123 | } 124 | 125 | pub, err := x509.ParsePKIXPublicKey(req.PublicKey) 126 | if err != nil { 127 | return nil, grpc.Errorf(codes.InvalidArgument, "Failed to parse PublicKey.") 128 | } 129 | 130 | ns, err := san.FromProtoStruct(req.Names) 131 | if err != nil { 132 | return nil, grpc.Errorf(codes.InvalidArgument, "Failed to parse Names.") 133 | } 134 | 135 | cfg := &issue.Config{ 136 | Subject: dname.FromProtoStruct(req.Subject), 137 | Names: ns, 138 | KeyUsage: keyusage.FromProtoStruct(req.KeyUsage), 139 | Validity: period.ValidityPeriod{ 140 | NotAfter: time.Unix(req.NotAfterUnixtime, 0).UTC(), 141 | }, 142 | } 143 | envP := svc.env.Clone() 144 | envP.ProfileName = req.Profile 145 | certDer, err := issue.Run(envP, pub, cfg) 146 | if err != nil { 147 | return nil, grpc.Errorf(codes.Internal, "%v", err) 148 | } 149 | 150 | return &pb.IssueCertificateResponse{ 151 | Certificate: certDer, 152 | }, nil 153 | } 154 | 155 | func (svc *Service) GetCertificate(ctx context.Context, req *pb.GetCertificateRequest) (*pb.GetCertificateResponse, error) { 156 | u := user.FromContext(ctx) 157 | if !u.IsAllowedToGetCertificate() { 158 | return nil, grpc.Errorf(codes.Unauthenticated, "%v is not allowed to get certificate.", u) 159 | } 160 | env := svc.env 161 | 162 | profile, err := env.Storage.Profile(req.Profile) 163 | if err != nil { 164 | return nil, grpc.Errorf(codes.Internal, "%v", err) 165 | } 166 | 167 | if req.SerialNumber == 0 { 168 | cacert, err := profile.ReadCACertificate() 169 | if err != nil { 170 | return nil, grpc.Errorf(codes.Internal, "%v", err) 171 | } 172 | 173 | return &pb.GetCertificateResponse{Certificate: cacert.Raw}, nil 174 | } 175 | 176 | db, err := issuedb.New(profile.IssueDBPath()) 177 | if err != nil { 178 | return nil, grpc.Errorf(codes.Internal, "%v", err) 179 | } 180 | 181 | e, err := db.Query(req.SerialNumber) 182 | if err != nil { 183 | return nil, grpc.Errorf(codes.Internal, "%v", err) 184 | } 185 | 186 | cs, err := pemparser.ParseCertificates([]byte(e.CertificatePEM)) 187 | if err != nil { 188 | return nil, grpc.Errorf(codes.Internal, "%v", err) 189 | } 190 | if len(cs) != 1 { 191 | return nil, grpc.Errorf(codes.Internal, "multiple certificates unexpected: %v", err) 192 | } 193 | c := cs[0] 194 | 195 | return &pb.GetCertificateResponse{Certificate: c.Raw}, nil 196 | } 197 | -------------------------------------------------------------------------------- /action/serve/certshandler/handler.go: -------------------------------------------------------------------------------- 1 | package certshandler 2 | 3 | import ( 4 | "crypto/x509" 5 | "errors" 6 | "fmt" 7 | "net/http" 8 | "net/url" 9 | "strings" 10 | 11 | "github.com/IPA-CyberLab/kmgm/action" 12 | "github.com/IPA-CyberLab/kmgm/httperr" 13 | "github.com/IPA-CyberLab/kmgm/pemparser" 14 | "github.com/IPA-CyberLab/kmgm/storage/issuedb" 15 | ) 16 | 17 | type Handler struct { 18 | env *action.Environment 19 | } 20 | 21 | func New(env *action.Environment) http.Handler { 22 | return &Handler{env} 23 | } 24 | 25 | func serveCertificate(w http.ResponseWriter, cert *x509.Certificate, filename string) error { 26 | w.Header().Set("X-Content-Type-Options", "nosniff") 27 | w.Header().Set("Content-Disposition", "attachment; filename*=UTF-8''"+url.QueryEscape(filename)) 28 | w.Header().Set("Content-Type", "application/x-pem-file") 29 | certPem := pemparser.MarshalCertificateDer(cert.Raw) 30 | if _, err := w.Write(certPem); err != nil { 31 | return fmt.Errorf("Failed to output PEM: %w", err) 32 | } 33 | return nil 34 | } 35 | 36 | func (h *Handler) serveHTTPIfPossible(w http.ResponseWriter, r *http.Request) error { 37 | if r.Method != "GET" { 38 | return httperr.ErrorWithStatusCode{ 39 | StatusCode: http.StatusBadRequest, 40 | Err: errors.New("Unsupported method")} 41 | } 42 | 43 | env := h.env.Clone() 44 | slog := env.Logger.Sugar() 45 | 46 | var queryStr string 47 | 48 | args := strings.Split(strings.TrimPrefix(r.URL.Path, "/"), "/") 49 | switch len(args) { 50 | case 1: 51 | queryStr = args[0] 52 | case 2: 53 | env.ProfileName = args[0] 54 | queryStr = args[1] 55 | default: 56 | return httperr.ErrorWithStatusCode{ 57 | StatusCode: http.StatusNotFound, 58 | Err: errors.New("bad path")} 59 | } 60 | queryStr = strings.TrimSuffix(queryStr, ".pem") 61 | 62 | slog.Debugf("certshandler GET profile: %s, queryStr: %s", env.ProfileName, queryStr) 63 | 64 | profile, err := env.Profile() 65 | if err != nil { 66 | return err 67 | } 68 | 69 | switch queryStr { 70 | case "": 71 | w.Header().Set("Content-Type", "text/plain") 72 | w.Write([]byte("/issue/[profileName]/[query].pem\n")) 73 | return nil 74 | case "cacert": 75 | cacert, err := profile.ReadCACertificate() 76 | if err != nil { 77 | return fmt.Errorf("Failed to query cacert: %w", err) 78 | } 79 | 80 | return serveCertificate(w, cacert, "cacert.pem") 81 | } 82 | 83 | db, err := issuedb.New(profile.IssueDBPath()) 84 | if err != nil { 85 | return err 86 | } 87 | 88 | db.Query(123) 89 | 90 | return nil 91 | } 92 | 93 | func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 94 | slog := h.env.Logger.Sugar() 95 | 96 | if err := h.serveHTTPIfPossible(w, r); err != nil { 97 | slog.Warnw("Failed to process request", "err", err) 98 | http.Error(w, fmt.Sprintf("Failed to process request: %v", err), httperr.StatusCodeFromError(err)) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /action/serve/config.go: -------------------------------------------------------------------------------- 1 | package serve 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/IPA-CyberLab/kmgm/san" 7 | ) 8 | 9 | type Config struct { 10 | ListenAddr string `flags:"listen-addr,server listen host:addr (:34680)"` 11 | ReusePort bool `flags:"reuse-port,set SO_REUSEPORT on listener socket"` 12 | 13 | Names san.Names `flags:"subject-alt-name,set subjectAltNames to use on server certificate,san"` 14 | 15 | IssueHttp int `flags:"issue-http,enable certificate issue via HTTP API"` 16 | 17 | ExposeMetrics bool `flags:"expose-metrics,expose /metrics HTTP endpoint for prometheus to crawl"` 18 | 19 | AutoShutdown time.Duration `flags:"auto-shutdown,auto shutdown server after specified time,,duration"` 20 | 21 | // Enable node bootstrapping with the given auth provider. 22 | Bootstrap TokenAuthProvider 23 | } 24 | 25 | func DefaultConfig() (*Config, error) { 26 | cfg := &Config{ 27 | ListenAddr: ":34680", 28 | ReusePort: false, 29 | IssueHttp: 0, 30 | } 31 | return cfg, nil 32 | } 33 | -------------------------------------------------------------------------------- /action/serve/hello_service.go: -------------------------------------------------------------------------------- 1 | package serve 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/IPA-CyberLab/kmgm/pb" 7 | "github.com/IPA-CyberLab/kmgm/remote/user" 8 | ) 9 | 10 | type helloService struct { 11 | pb.UnimplementedHelloServiceServer 12 | } 13 | 14 | var _ = pb.HelloServiceServer(&helloService{}) 15 | 16 | func (helloService) Hello(ctx context.Context, req *pb.HelloRequest) (*pb.HelloResponse, error) { 17 | u := user.FromContext(ctx) 18 | return &pb.HelloResponse{ 19 | ApiVersion: pb.ApiVersion, 20 | AuthenticationType: u.Type, 21 | AuthenticatedUser: u.Name, 22 | }, nil 23 | } 24 | -------------------------------------------------------------------------------- /action/serve/httpzaplog/handler.go: -------------------------------------------------------------------------------- 1 | package httpzaplog 2 | 3 | import ( 4 | "net/http" 5 | 6 | "go.uber.org/zap" 7 | ) 8 | 9 | type Handler struct { 10 | Upstream http.Handler 11 | *zap.Logger 12 | } 13 | 14 | func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 15 | var remoteCN string 16 | if r.TLS != nil && len(r.TLS.PeerCertificates) > 0 { 17 | peerCert := r.TLS.PeerCertificates[0] 18 | remoteCN = peerCert.Subject.CommonName 19 | } 20 | 21 | h.Logger.Info("ServeHTTP", 22 | zap.String("url", r.URL.String()), 23 | zap.String("remote_addr", r.RemoteAddr), 24 | zap.String("remote_common_name", remoteCN), 25 | ) 26 | 27 | h.Upstream.ServeHTTP(w, r) 28 | } 29 | -------------------------------------------------------------------------------- /action/serve/tokenauth.go: -------------------------------------------------------------------------------- 1 | package serve 2 | 3 | import ( 4 | "errors" 5 | "io/ioutil" 6 | "log" 7 | "net" 8 | "os" 9 | "path/filepath" 10 | "sort" 11 | "strings" 12 | "time" 13 | 14 | "go.uber.org/zap" 15 | ) 16 | 17 | type TokenAuthProvider interface { 18 | Authenticate(token string, now time.Time) error 19 | LogHelpMessage(listenAddr, pubkeyhash string) 20 | } 21 | 22 | type FixedTokenAuthProvider struct { 23 | Token string 24 | NotAfter time.Time 25 | Logger *zap.Logger 26 | } 27 | 28 | var _ = TokenAuthProvider(&FixedTokenAuthProvider{}) 29 | 30 | var ErrBadToken = errors.New("Bad token.") 31 | var ErrTokenExpired = errors.New("Token expired.") 32 | 33 | func (ta *FixedTokenAuthProvider) Authenticate(t string, now time.Time) error { 34 | slog := ta.Logger.Sugar() 35 | 36 | if t != ta.Token { 37 | slog.Infof("Bad token %q provided.") 38 | return ErrBadToken 39 | } 40 | if now.After(ta.NotAfter) { 41 | slog.Infof("Token provided has expired. It was valid until %v.", ta.NotAfter) 42 | return ErrTokenExpired 43 | } 44 | slog.Debugf("Token auth succeeded.") 45 | return nil 46 | } 47 | 48 | func (ta *FixedTokenAuthProvider) LogHelpMessage(listenAddr, pubkeyhash string) { 49 | slog := ta.Logger.Sugar() 50 | 51 | // FIXME[P3]: make NotAfter configurable. 52 | slog.Infof("Node bootstrap enabled for 15 minutes using token: %s", ta.Token) 53 | slog.Infof("For your convenience, bootstrap command-line to be executed on your clients would look like: kmgm client --server %s --pinnedpubkey %s --token %s bootstrap", FormatListenAddr(listenAddr), pubkeyhash, ta.Token) 54 | } 55 | 56 | type tokenFileAuthProvider struct { 57 | path string 58 | logger *zap.Logger 59 | } 60 | 61 | var _ = TokenAuthProvider(&tokenFileAuthProvider{}) 62 | 63 | func NewTokenFileAuthProvider(path string, logger *zap.Logger) (TokenAuthProvider, error) { 64 | slog := logger.Sugar() 65 | 66 | apath, err := filepath.Abs(path) 67 | if err != nil { 68 | return nil, err 69 | } 70 | 71 | ta := &tokenFileAuthProvider{path: apath, logger: logger} 72 | slog.Debugf("Initialized tokenFileAuthProvider{%q}", apath) 73 | 74 | if _, err := ioutil.ReadFile(ta.path); err != nil { 75 | slog.Infof("Failed to read token file: %v", err) 76 | } 77 | 78 | return ta, nil 79 | } 80 | 81 | func (ta *tokenFileAuthProvider) Authenticate(t string, now time.Time) error { 82 | slog := ta.logger.Sugar() 83 | 84 | st, err := os.Stat(ta.path) 85 | if err != nil { 86 | slog.Warnf("Failed to stat token file: %v", err) 87 | return ErrBadToken 88 | } 89 | modt := st.ModTime() 90 | // FIXME[P3]: make NotAfter configurable. 91 | if now.Sub(modt) > 15*time.Minute { 92 | slog.Warnf("The token file is too old. It must be modified w/in 15 min to be valid") 93 | return ErrBadToken 94 | } 95 | 96 | bs, err := ioutil.ReadFile(ta.path) 97 | if err != nil { 98 | slog.Warnf("Failed to read token file: %v", err) 99 | return ErrBadToken 100 | } 101 | token := strings.TrimSpace(string(bs)) 102 | if t != token { 103 | slog.Infof("Expected token %q, but got %q.", token, t) 104 | return ErrBadToken 105 | } 106 | 107 | return nil 108 | } 109 | 110 | func (ta *tokenFileAuthProvider) LogHelpMessage(listenAddr, pubkeyhash string) { 111 | slog := ta.logger.Sugar() 112 | 113 | slog.Infof("Node bootstrap enabled. Token is read from file %q.", ta.path) 114 | slog.Infof("For your convenience, bootstrap command-line to be executed on your clients would look like: kmgm client --server %s --pinnedpubkey %s --token [token] bootstrap", FormatListenAddr(listenAddr), pubkeyhash) 115 | } 116 | 117 | var ipDocker = net.IPv4(172, 17, 0, 1) 118 | 119 | // FormatListenAddr takes a hostport str, and appends an interface ip addr as 120 | // a host if the original host was empty or 0.0.0.0. 121 | func FormatListenAddr(listenAddr string) string { 122 | host, port, err := net.SplitHostPort(listenAddr) 123 | if err != nil { 124 | log.Panicf("Failed to net.SplitHostPort(%q): %v", listenAddr, err) 125 | } 126 | if host == "" || host == "0.0.0.0" { 127 | if addrs, err := net.InterfaceAddrs(); err == nil { 128 | type candidate struct { 129 | Host string 130 | Score int 131 | } 132 | cs := make([]candidate, 0, len(addrs)) 133 | for _, addr := range addrs { 134 | if ipaddr, ok := addr.(*net.IPNet); ok { 135 | ip := ipaddr.IP 136 | c := candidate{Host: ip.String(), Score: 100} 137 | if ip4 := ip.To4(); len(ip4) == net.IPv4len { 138 | c.Score += 50 139 | } 140 | if ip.IsLoopback() { 141 | c.Score -= 10 142 | } 143 | if ip.Equal(ipDocker) { 144 | c.Score -= 20 145 | } 146 | if ip.IsLinkLocalUnicast() { 147 | c.Score -= 30 148 | } 149 | cs = append(cs, c) 150 | } 151 | } 152 | 153 | // sort by .Score desc 154 | sort.Slice(cs, func(i, j int) bool { return cs[i].Score > cs[j].Score }) 155 | host = cs[0].Host 156 | } 157 | } 158 | 159 | return net.JoinHostPort(host, port) 160 | } 161 | -------------------------------------------------------------------------------- /action/serve/tokenauth_test.go: -------------------------------------------------------------------------------- 1 | package serve_test 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "testing" 7 | "time" 8 | 9 | "go.uber.org/zap" 10 | 11 | "github.com/IPA-CyberLab/kmgm/action/serve" 12 | ) 13 | 14 | var TestLogger *zap.Logger 15 | 16 | func init() { 17 | logger, err := zap.NewDevelopment() 18 | if err != nil { 19 | panic(err) 20 | } 21 | zap.ReplaceGlobals(logger) 22 | TestLogger = logger 23 | } 24 | 25 | func TestTokenFileAuthProvider(t *testing.T) { 26 | tmpfile, err := ioutil.TempFile("", "tokenfile") 27 | if err != nil { 28 | t.Fatal(err) 29 | } 30 | defer os.Remove(tmpfile.Name()) 31 | 32 | now := time.Now() 33 | 34 | ta, err := serve.NewTokenFileAuthProvider(tmpfile.Name(), TestLogger) 35 | if err != nil { 36 | t.Fatal(err) 37 | } 38 | if err := ta.Authenticate("foobar", now); err == nil { 39 | t.Fatal(err) 40 | } 41 | 42 | if _, err := tmpfile.Write([]byte(" validtoken\t\t \n")); err != nil { 43 | t.Fatal(err) 44 | } 45 | // We need Close() here since we read the tokenfile in the following ta.Authenticate calls. 46 | if err := tmpfile.Close(); err != nil { 47 | t.Fatal(err) 48 | } 49 | 50 | if err := ta.Authenticate("validtoken", now); err != nil { 51 | t.Fatal(err) 52 | } 53 | if err := ta.Authenticate("invalidtoken", now); err == nil { 54 | t.Fatal(err) 55 | } 56 | if err := ta.Authenticate("validtoken", now.Add(20*time.Minute)); err == nil { 57 | t.Fatal(err) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /action/serve/version_service.go: -------------------------------------------------------------------------------- 1 | package serve 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/IPA-CyberLab/kmgm/pb" 7 | "github.com/IPA-CyberLab/kmgm/version" 8 | ) 9 | 10 | type versionService struct { 11 | pb.UnimplementedVersionServiceServer 12 | } 13 | 14 | var _ = pb.VersionServiceServer(&versionService{}) 15 | 16 | func (versionService) GetVersion(ctx context.Context, req *pb.GetVersionRequest) (*pb.GetVersionResponse, error) { 17 | return &pb.GetVersionResponse{ 18 | Version: version.Version, 19 | Commit: version.Commit, 20 | }, nil 21 | } 22 | -------------------------------------------------------------------------------- /action/setup/config.go: -------------------------------------------------------------------------------- 1 | package setup 2 | 3 | import ( 4 | "crypto/x509" 5 | "errors" 6 | "fmt" 7 | "time" 8 | 9 | "github.com/IPA-CyberLab/kmgm/dname" 10 | "github.com/IPA-CyberLab/kmgm/period" 11 | "github.com/IPA-CyberLab/kmgm/wcrypto" 12 | ) 13 | 14 | var ( 15 | ErrSubjectEmpty = errors.New("CA Subject must not be empty") 16 | ErrValidityPeriodExpired = errors.New("Declining to setup CA which expires within 30 seconds") 17 | ErrKeyTypeAny = errors.New("KeyType cannot be KeyAny, please specify a specific key algorithm such as KeyRSA4096") 18 | ) 19 | 20 | type Config struct { 21 | Subject *dname.Config `yaml:"subject" flags:""` 22 | Validity period.ValidityPeriod `yaml:"validity" flags:"validity,time duration/timestamp where the cert is valid to (examples: 30d, 1y, 20220530)"` 23 | KeyType wcrypto.KeyType `yaml:"keyType" flags:"key-type,private key type (rsa, ecdsa),t"` 24 | 25 | NameConstraints NameConstraints `yaml:"nameConstraints"` 26 | } 27 | 28 | func DefaultConfig(baseSubject *dname.Config) *Config { 29 | return &Config{ 30 | Subject: dname.DefaultConfig(" CA", baseSubject), 31 | KeyType: wcrypto.KeyRSA4096, 32 | Validity: period.FarFuture, 33 | } 34 | } 35 | 36 | func EmptyConfig() *Config { 37 | return &Config{ 38 | Subject: &dname.Config{}, 39 | } 40 | } 41 | 42 | func ConfigFromCert(cert *x509.Certificate) (*Config, error) { 43 | kt, err := wcrypto.KeyTypeOfPub(cert.PublicKey) 44 | if err != nil { 45 | return nil, err 46 | } 47 | 48 | return &Config{ 49 | Subject: dname.FromPkixName(cert.Subject), 50 | KeyType: kt, 51 | Validity: period.ValidityPeriod{NotAfter: cert.NotAfter}, 52 | }, nil 53 | } 54 | 55 | func (a *Config) CompatibleWith(b *Config) error { 56 | if err := a.Subject.CompatibleWith(b.Subject); err != nil { 57 | return err 58 | } 59 | if a.KeyType != b.KeyType { 60 | return fmt.Errorf("KeyType mismatch: %v != %v", a.KeyType, b.KeyType) 61 | } 62 | return nil 63 | } 64 | 65 | const expireThreshold = 30 * time.Second 66 | 67 | func (cfg *Config) Verify(now time.Time) error { 68 | if err := cfg.Subject.Verify(); err != nil { 69 | return fmt.Errorf("Subject.%w", err) 70 | } 71 | if cfg.Subject.IsEmpty() { 72 | return ErrSubjectEmpty 73 | } 74 | if cfg.Validity.GetNotAfter(now).Before(now.Add(expireThreshold)) { 75 | return ErrValidityPeriodExpired 76 | } 77 | if cfg.KeyType == wcrypto.KeyAny { 78 | return ErrKeyTypeAny 79 | } 80 | 81 | return nil 82 | } 83 | -------------------------------------------------------------------------------- /action/setup/config_test.go: -------------------------------------------------------------------------------- 1 | package setup_test 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | "time" 7 | 8 | "github.com/IPA-CyberLab/kmgm/action/setup" 9 | "github.com/IPA-CyberLab/kmgm/dname" 10 | "github.com/IPA-CyberLab/kmgm/period" 11 | "github.com/IPA-CyberLab/kmgm/wcrypto" 12 | ) 13 | 14 | func TestConfigVerify_SubjectEmpty(t *testing.T) { 15 | cfg := &setup.Config{ 16 | Subject: &dname.Config{}, 17 | KeyType: wcrypto.KeyRSA4096, 18 | Validity: period.FarFuture, 19 | } 20 | 21 | if err := cfg.Verify(time.Now()); !errors.Is(err, setup.ErrSubjectEmpty) { 22 | t.Errorf("Unexpected: %v", err) 23 | } 24 | } 25 | 26 | func TestConfigVerify_Expired(t *testing.T) { 27 | now := time.Date(2020, time.March, 1, 0, 0, 0, 0, time.UTC) 28 | 29 | cfg := &setup.Config{ 30 | Subject: &dname.Config{CommonName: "foo"}, 31 | KeyType: wcrypto.KeyAny, 32 | Validity: period.ValidityPeriod{NotAfter: now.Add(-1 * time.Hour)}, 33 | } 34 | 35 | if err := cfg.Verify(now); !errors.Is(err, setup.ErrValidityPeriodExpired) { 36 | t.Errorf("Unexpected: %v", err) 37 | } 38 | } 39 | 40 | func TestConfigVerify_KeyTypeAny(t *testing.T) { 41 | cfg := &setup.Config{ 42 | Subject: &dname.Config{CommonName: "foo"}, 43 | KeyType: wcrypto.KeyAny, 44 | Validity: period.FarFuture, 45 | } 46 | 47 | if err := cfg.Verify(time.Now()); !errors.Is(err, setup.ErrKeyTypeAny) { 48 | t.Errorf("Unexpected: %v", err) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /action/setup/nameconstraints.go: -------------------------------------------------------------------------------- 1 | package setup 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | ) 7 | 8 | type NameConstraints struct { 9 | PermittedDNSDomains []string 10 | ExcludedDNSDomains []string 11 | 12 | PermittedIPRanges []*net.IPNet 13 | ExcludedIPRanges []*net.IPNet 14 | } 15 | 16 | func (p *NameConstraints) UnmarshalYAML(unmarshal func(interface{}) error) error { 17 | var ss []string 18 | if err := unmarshal(&ss); err != nil { 19 | return err 20 | } 21 | 22 | for _, s := range ss { 23 | excludeRule := false 24 | if s[0] == '-' { 25 | excludeRule = true 26 | s = s[1:] 27 | } else if s[0] == '+' { 28 | s = s[1:] 29 | } 30 | 31 | _, net, err := net.ParseCIDR(s) 32 | if err == nil { 33 | if excludeRule { 34 | p.ExcludedIPRanges = append(p.ExcludedIPRanges, net) 35 | } else { 36 | p.PermittedIPRanges = append(p.PermittedIPRanges, net) 37 | } 38 | } else { 39 | if excludeRule { 40 | p.ExcludedDNSDomains = append(p.ExcludedDNSDomains, s) 41 | } else { 42 | p.PermittedDNSDomains = append(p.PermittedDNSDomains, s) 43 | } 44 | } 45 | } 46 | return nil 47 | } 48 | 49 | func (nc *NameConstraints) IsEmpty() bool { 50 | if len(nc.PermittedDNSDomains) > 0 { 51 | return false 52 | } 53 | if len(nc.ExcludedDNSDomains) > 0 { 54 | return false 55 | } 56 | if len(nc.PermittedIPRanges) > 0 { 57 | return false 58 | } 59 | if len(nc.ExcludedIPRanges) > 0 { 60 | return false 61 | } 62 | 63 | return true 64 | } 65 | 66 | func (nc *NameConstraints) Strings() []string { 67 | var ss []string 68 | for _, e := range nc.PermittedDNSDomains { 69 | ss = append(ss, fmt.Sprintf("+%s", e)) 70 | } 71 | for _, e := range nc.ExcludedDNSDomains { 72 | ss = append(ss, fmt.Sprintf("-%s", e)) 73 | } 74 | for _, e := range nc.PermittedIPRanges { 75 | ss = append(ss, fmt.Sprintf("+%v", e)) 76 | } 77 | for _, e := range nc.ExcludedIPRanges { 78 | ss = append(ss, fmt.Sprintf("-%v", e)) 79 | } 80 | 81 | return ss 82 | } 83 | -------------------------------------------------------------------------------- /action/setup/nameconstraints_test.go: -------------------------------------------------------------------------------- 1 | package setup_test 2 | 3 | import ( 4 | "reflect" 5 | "strings" 6 | "testing" 7 | 8 | "gopkg.in/yaml.v3" 9 | 10 | "github.com/IPA-CyberLab/kmgm/action/setup" 11 | ) 12 | 13 | func TestNameConstraints_UnmarshalYAML(t *testing.T) { 14 | src := ` 15 | - example.org 16 | - 192.0.2.0/24 17 | - 2001:db8:1::/48 18 | - -10.0.0.0/8 19 | - "-bad.example" 20 | - "+ipa.go.jp" 21 | ` 22 | 23 | var nc setup.NameConstraints 24 | if err := yaml.Unmarshal([]byte(src), &nc); err != nil { 25 | t.Fatalf("unmarshal failed: %v", err) 26 | } 27 | 28 | actual := nc.Strings() 29 | expected := strings.Split("+example.org +ipa.go.jp -bad.example +192.0.2.0/24 +2001:db8:1::/48 -10.0.0.0/8", " ") 30 | if !reflect.DeepEqual(actual, expected) { 31 | t.Errorf("unexp: %s", actual) 32 | } 33 | 34 | remarshaled, err := yaml.Marshal(actual) 35 | if err != nil { 36 | t.Fatalf("marshal failed: %v", err) 37 | } 38 | 39 | var nc2 setup.NameConstraints 40 | if err := yaml.Unmarshal(remarshaled, &nc2); err != nil { 41 | t.Fatalf("unmarshal failed: %v", err) 42 | } 43 | 44 | if !reflect.DeepEqual(nc, nc2) { 45 | t.Errorf("noneq") 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /action/setup/setup.go: -------------------------------------------------------------------------------- 1 | package setup 2 | 3 | import ( 4 | "crypto" 5 | "crypto/x509" 6 | "errors" 7 | "fmt" 8 | "math/big" 9 | "time" 10 | 11 | // zx509 "github.com/zmap/zcrypto/x509" 12 | // "github.com/zmap/zlint" 13 | 14 | "github.com/IPA-CyberLab/kmgm/action" 15 | "github.com/IPA-CyberLab/kmgm/consts" 16 | "github.com/IPA-CyberLab/kmgm/frontend/validate" 17 | "github.com/IPA-CyberLab/kmgm/storage" 18 | "github.com/IPA-CyberLab/kmgm/storage/issuedb" 19 | "github.com/IPA-CyberLab/kmgm/wcrypto" 20 | ) 21 | 22 | func createCertificate(env *action.Environment, cfg *Config, priv crypto.PrivateKey) ([]byte, error) { 23 | slog := env.Logger.Sugar().With( 24 | "profile", env.ProfileName, 25 | "subject", cfg.Subject.ToPkixName().String(), 26 | "notafter", cfg.Validity, 27 | ) 28 | 29 | start := time.Now() 30 | slog.Infow("Generating self-signed CA certificate...") 31 | 32 | pub, err := wcrypto.ExtractPublicKey(priv) 33 | if err != nil { 34 | return nil, err 35 | } 36 | pubkeyhash, err := wcrypto.PubKeyPinString(pub) 37 | if err != nil { 38 | pubkeyhash = fmt.Sprintf("Error: %v", err) 39 | } 40 | slog = slog.With("pubkeyhash", pubkeyhash) 41 | 42 | kt, err := wcrypto.KeyTypeOfPub(pub) 43 | if err != nil { 44 | return nil, err 45 | } 46 | if err := cfg.KeyType.CompatibleWith(kt); err != nil { 47 | return nil, err 48 | } 49 | 50 | ski, err := wcrypto.SubjectKeyIdFromPubkey(pub) 51 | if err != nil { 52 | return nil, err 53 | } 54 | 55 | now := env.NowImpl() 56 | t := &x509.Certificate{ 57 | // AuthorityKeyId meaningless for self-signed 58 | BasicConstraintsValid: true, 59 | 60 | // Subject Alternate Name Values should be empty for CA: 61 | // - DNSNames 62 | // - EmailAddresses 63 | // - IPAddresses 64 | // - URIs 65 | 66 | // https://tools.ietf.org/html/rfc5280#section-4.2.2.1 67 | // OCSPServer: []string{}, 68 | // IssuingCertificateURL: []string{}, 69 | 70 | // https://tools.ietf.org/html/rfc3647 71 | // PolicyIdentifiers: []asn1.ObjectIdentifier{}, 72 | 73 | IsCA: true, 74 | KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, 75 | // ExtKeyUsage: []ExtKeyUsage{}, 76 | // UnknownExtKeyUsage: []asn1.ObjectIdentifier{}, 77 | 78 | // ExtraExtensions: []pkix.Extension, 79 | 80 | // pathlen of 0 : Sign end-user certs only 81 | MaxPathLen: 0, 82 | MaxPathLenZero: true, 83 | 84 | NotAfter: cfg.Validity.GetNotAfter(now).UTC(), 85 | NotBefore: now.Add(-consts.NodesOutOfSyncThreshold).UTC(), 86 | 87 | // FIXME[P2]: https://crypto.stackexchange.com/questions/257/unpredictability-of-x-509-serial-numbers 88 | SerialNumber: new(big.Int).SetInt64(1), 89 | 90 | // SignatureAlgorithm will be auto-specified by x509 package 91 | 92 | Subject: cfg.Subject.ToPkixName(), 93 | 94 | // https://security.stackexchange.com/questions/200295/the-difference-between-subject-key-identifier-and-sha1fingerprint-in-x509-certif 95 | // https://security.stackexchange.com/questions/27797/what-damage-could-be-done-if-a-malicious-certificate-had-an-identical-subject-k?rq=1 96 | SubjectKeyId: ski, 97 | 98 | // CRLDistributionPoints: FIXME 99 | 100 | PermittedDNSDomainsCritical: !cfg.NameConstraints.IsEmpty(), 101 | PermittedDNSDomains: cfg.NameConstraints.PermittedDNSDomains, 102 | ExcludedDNSDomains: cfg.NameConstraints.ExcludedDNSDomains, 103 | PermittedIPRanges: cfg.NameConstraints.PermittedIPRanges, 104 | ExcludedIPRanges: cfg.NameConstraints.ExcludedIPRanges, 105 | 106 | // FIXME[P2]: Support more name constraints: 107 | // https://tools.ietf.org/html/rfc5280#section-4.2.1.10 108 | // PermittedEmailAddresses 109 | // ExcludedEmailAddresses 110 | // PermittedURIDomains 111 | // ExcludedURIDomains 112 | } 113 | 114 | parent := t // self signed cert 115 | 116 | certDer, err := x509.CreateCertificate(env.Randr, t, parent, pub, priv) 117 | if err != nil { 118 | return nil, fmt.Errorf("Create a self-signed CA cert failed: %w", err) 119 | } 120 | slog.Infow("Generating a self-signed CA certificate... Done.", "took", time.Since(start)) 121 | return certDer, nil 122 | } 123 | 124 | var ErrValidCAExist = errors.New("Valid CA already exists.") 125 | 126 | func Run(env *action.Environment, cfg *Config) error { 127 | slog := env.Logger.Sugar() 128 | 129 | profile, err := env.Profile() 130 | if err != nil { 131 | return err 132 | } 133 | 134 | st := profile.Status(env.NowImpl()) 135 | switch st.Code { 136 | case storage.NotCA: 137 | break 138 | case storage.ValidCA: 139 | return ErrValidCAExist 140 | case storage.Broken: 141 | return fmt.Errorf("Broken CA already exists.") 142 | case storage.Expired: 143 | return fmt.Errorf("Expired CA already exists.") 144 | } 145 | 146 | idb, err := issuedb.New(profile.IssueDBPath()) 147 | if err != nil { 148 | return err 149 | } 150 | 151 | if err := validate.MkdirAndCheckWritable(profile.CAPrivateKeyPath()); err != nil { 152 | return fmt.Errorf("Prepare private key destination: %w", err) 153 | } 154 | if err := validate.MkdirAndCheckWritable(profile.CACertPath()); err != nil { 155 | return fmt.Errorf("Prepare CA cert destination: %w", err) 156 | } 157 | if err := idb.Initialize(); err != nil { 158 | return err 159 | } 160 | 161 | var priv crypto.PrivateKey 162 | ktype := cfg.KeyType 163 | if env.PregenKeySupplier != nil { 164 | slog.Errorf("!!!DANGEROUS - FOR TEST ONLY!!! Using unsafe, pregenerated key of type %v", ktype) 165 | priv = env.PregenKeySupplier(ktype) 166 | } else { 167 | priv, err = wcrypto.GenerateKey(env.Randr, ktype, "CA", env.Logger) 168 | if err != nil { 169 | return err 170 | } 171 | } 172 | if err := profile.WriteCAPrivateKey(priv); err != nil { 173 | return err 174 | } 175 | slog.Infof("The CA private key saved to file: %s", profile.CAPrivateKeyPath()) 176 | 177 | certDer, err := createCertificate(env, cfg, priv) 178 | if err != nil { 179 | return err 180 | } 181 | 182 | if err := profile.WriteCACertificateDer(certDer); err != nil { 183 | return err 184 | } 185 | slog.Infof("The CA certificate saved to file: %s", profile.CACertPath()) 186 | 187 | return nil 188 | } 189 | -------------------------------------------------------------------------------- /action/setup/setup_test.go: -------------------------------------------------------------------------------- 1 | package setup_test 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "go.uber.org/zap" 8 | 9 | "github.com/IPA-CyberLab/kmgm/action" 10 | "github.com/IPA-CyberLab/kmgm/action/setup" 11 | "github.com/IPA-CyberLab/kmgm/frontend" 12 | "github.com/IPA-CyberLab/kmgm/storage" 13 | ) 14 | 15 | func init() { 16 | logger, err := zap.NewDevelopment() 17 | if err != nil { 18 | panic(err) 19 | } 20 | zap.ReplaceGlobals(logger) 21 | } 22 | 23 | func testEnv(t *testing.T) (*action.Environment, func()) { 24 | t.Helper() 25 | 26 | basedir, err := os.MkdirTemp("", "kmgm_issue_test") 27 | if err != nil { 28 | t.Fatalf("ioutil.TempDir: %v", err) 29 | } 30 | 31 | stor, err := storage.New(basedir) 32 | if err != nil { 33 | t.Fatalf("storage.New: %v", err) 34 | } 35 | 36 | fe := &frontend.NonInteractive{ 37 | Logger: zap.L(), 38 | } 39 | 40 | env, err := action.NewEnvironment(fe, stor) 41 | if err != nil { 42 | t.Fatalf("action.NewEnvironment: %v", err) 43 | } 44 | env.Frontend = &frontend.NonInteractive{Logger: zap.L()} 45 | 46 | return env, func() { 47 | os.RemoveAll(basedir) 48 | } 49 | } 50 | 51 | func TestSetup_Default(t *testing.T) { 52 | env, teardown := testEnv(t) 53 | t.Cleanup(teardown) 54 | 55 | cfg := setup.DefaultConfig(nil) 56 | if err := setup.Run(env, cfg); err != nil { 57 | t.Fatalf("setup.Run: %v", err) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /buf.gen.yaml: -------------------------------------------------------------------------------- 1 | version: v2 2 | plugins: 3 | - remote: buf.build/grpc/go:v1.5.1 4 | out: . 5 | opt: paths=source_relative 6 | - remote: buf.build/protocolbuffers/go:v1.35.1 7 | out: . 8 | opt: paths=source_relative 9 | -------------------------------------------------------------------------------- /buf.lock: -------------------------------------------------------------------------------- 1 | # Generated by buf. DO NOT EDIT. 2 | version: v1 3 | -------------------------------------------------------------------------------- /buf.yaml: -------------------------------------------------------------------------------- 1 | version: v1 2 | breaking: 3 | use: 4 | - FILE 5 | lint: 6 | use: 7 | - DEFAULT 8 | -------------------------------------------------------------------------------- /cmd/kmgm/app/appflags/appflags.go: -------------------------------------------------------------------------------- 1 | package appflags 2 | 3 | type AppFlags struct { 4 | BaseDir string `yaml:"baseDir" flags:"basedir,The root directory storing all kmgm data,"` 5 | Profile string `yaml:"profile" flags:"profile,Name of the profile to operate against"` 6 | Config string `flags:"config,Read the specified YAML config file instead of interactive prompt.,c,path"` 7 | LogJson bool `flags:"log-json,Format logs in json"` 8 | NoGeoIp bool `flags:"no-geo-ip,Disable querying ip-api.com for geolocation data"` 9 | LogLocation bool `flags:"log-location,Annotate logs with code location where the log was output"` 10 | NoDefault bool `yaml:"noDefault" flags:"no-default,Disable populating default values on non-interactive mode,"` 11 | Verbose bool `flags:"verbose,Enable verbose output"` 12 | NonInteractive bool `flags:"non-interactive,Use non-interactive frontend, which auto proceeds with default answers."` 13 | } 14 | -------------------------------------------------------------------------------- /cmd/kmgm/batch/batch.go: -------------------------------------------------------------------------------- 1 | package batch 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | 8 | "github.com/urfave/cli/v2" 9 | "go.uber.org/multierr" 10 | "gopkg.in/yaml.v3" 11 | 12 | "github.com/IPA-CyberLab/kmgm/action" 13 | "github.com/IPA-CyberLab/kmgm/action/issue" 14 | "github.com/IPA-CyberLab/kmgm/cmd/kmgm/app/appflags" 15 | issuecmd "github.com/IPA-CyberLab/kmgm/cmd/kmgm/issue" 16 | setupcmd "github.com/IPA-CyberLab/kmgm/cmd/kmgm/setup" 17 | "github.com/IPA-CyberLab/kmgm/dname" 18 | "github.com/IPA-CyberLab/kmgm/frontend" 19 | "github.com/IPA-CyberLab/kmgm/period" 20 | "github.com/IPA-CyberLab/kmgm/storage" 21 | "github.com/IPA-CyberLab/kmgm/structflags" 22 | ) 23 | 24 | const ConfigTemplateText = ` 25 | --- 26 | # kmgm pki batch config 27 | baseDir: /my/pki/dir 28 | 29 | setup: 30 | subject: 31 | commonName: my CA 32 | validity: farfuture 33 | 34 | copyCACertPath: my-ca.crt 35 | 36 | issues: 37 | - certPath: leaf1.cert.pem 38 | privateKeyPath: leaf1.key.pem 39 | subject: 40 | commonName: leaf1 41 | subjectAltNames: 42 | - leaf1.example 43 | keyType: ecdsa 44 | validity: farfuture 45 | - certPath: leaf2.cert.pem 46 | privateKeyPath: leaf2.key.pem 47 | subject: 48 | commonName: leaf2 49 | validity: 90d 50 | renewBefore: 7d 51 | 52 | ` 53 | 54 | type Config struct { 55 | Setup *setupcmd.Config `yaml:"setup"` 56 | 57 | Issues []*issuecmd.Config `yaml:"issues"` 58 | 59 | // This is here to avoid yaml.v3 Decoder with KnownFields(true) throwing error for valid AppFlags fields 60 | XXX_AppFlags appflags.AppFlags `yaml:",inline"` 61 | } 62 | 63 | var ErrYamlMustBeProvided = errors.New("batch: yaml config must be provided. Try `kmgm -c [config.yaml] batch`") 64 | 65 | func Action(c *cli.Context) error { 66 | af := c.App.Metadata["AppFlags"].(*appflags.AppFlags) 67 | 68 | env := action.GlobalEnvironment 69 | slog := env.Logger.Sugar() 70 | 71 | var cfg *Config 72 | if c.Bool("dump-template") || !af.NoDefault { 73 | slog.Infof("Constructing default config.") 74 | 75 | cfg = &Config{ 76 | Setup: setupcmd.DefaultConfig(env), 77 | } 78 | } else { 79 | slog.Infof("Config is from scratch.") 80 | 81 | cfg = &Config{ 82 | Setup: setupcmd.EmptyConfig(), 83 | } 84 | } 85 | 86 | if c.Bool("dump-template") { 87 | if err := frontend.DumpTemplate(ConfigTemplateText, cfg); err != nil { 88 | return err 89 | } 90 | return nil 91 | } 92 | 93 | cfgbs, ok := c.App.Metadata["config"] 94 | if !ok { 95 | return ErrYamlMustBeProvided 96 | } 97 | 98 | decodeConfig := func() error { 99 | r := bytes.NewBuffer(cfgbs.([]byte)) 100 | 101 | d := yaml.NewDecoder(r) 102 | d.KnownFields(true) 103 | 104 | return d.Decode(cfg) 105 | } 106 | if err := decodeConfig(); err != nil { 107 | return err 108 | } 109 | 110 | profile, err := env.Profile() 111 | if err != nil { 112 | return err 113 | } 114 | 115 | if err := setupcmd.EnsureCA(env, cfg.Setup, profile, setupcmd.AllowNonInteractiveSetupAndRequireCompatibleConfig); err != nil { 116 | return err 117 | } 118 | if err := setupcmd.CopyCACert(env, cfg.Setup, profile); err != nil { 119 | return err 120 | } 121 | 122 | if !af.NoDefault { 123 | slog.Infof("Rereading config with to process cert issue with updated defaults.") 124 | 125 | origTemplate := issuecmd.UnmarshalConfigTemplate 126 | defer func() { 127 | issuecmd.UnmarshalConfigTemplate = origTemplate 128 | }() 129 | 130 | now := env.NowImpl() 131 | st := profile.Status(now) 132 | if st.Code != storage.ValidCA { 133 | return fmt.Errorf("BUG: CA profile %q is not valid: %v", env.ProfileName, st) 134 | } 135 | baseSubject := dname.FromPkixName(st.CACert.Subject) 136 | issuecmd.UnmarshalConfigTemplate = &issuecmd.Config{ 137 | Issue: issue.DefaultConfig(baseSubject), 138 | RenewBefore: period.DaysAuto, 139 | } 140 | 141 | cfg = &Config{Setup: cfg.Setup} 142 | if err := decodeConfig(); err != nil { 143 | return err 144 | } 145 | } 146 | 147 | processIssue := func(issueCfg *issuecmd.Config) error { 148 | if issueCfg.PrivateKeyPath == "" { 149 | return fmt.Errorf("privateKeyPath must be specified") 150 | } 151 | if err := issuecmd.PrepareKeyTypePath(env, &issueCfg.Issue.KeyType, &issueCfg.PrivateKeyPath); err != nil { 152 | return err 153 | } 154 | 155 | if issueCfg.CertPath == "" { 156 | return fmt.Errorf("certPath must be specified") 157 | } 158 | newCertPath, err := issuecmd.PromptCertPath(env, issueCfg.PrivateKeyPath, issueCfg.CertPath) 159 | if err != nil { 160 | return err 161 | } 162 | issueCfg.CertPath = newCertPath 163 | 164 | if err := issueCfg.Verify(env, af.NoDefault); err != nil { 165 | return err 166 | } 167 | if err := issuecmd.IssuePrivateKeyAndCertificateFile(c.Context, env, issuecmd.Local{}, issueCfg); err != nil { 168 | return err 169 | } 170 | return nil 171 | } 172 | 173 | var merr error 174 | for i, issueCfg := range cfg.Issues { 175 | slog.Infof("batch: processing issue[%d]: %v", i, issueCfg.Issue.Subject) 176 | err := processIssue(issueCfg) 177 | if err != nil { 178 | slog.Errorf("batch: issue[%d]: %v", i, err) 179 | merr = multierr.Append(merr, fmt.Errorf("batch: issue[%d]: %w", i, err)) 180 | } 181 | } 182 | 183 | return merr 184 | } 185 | 186 | var Command = &cli.Command{ 187 | Name: "batch", 188 | Usage: "Processes a set of kmgm commands.", 189 | Flags: append(structflags.MustPopulateFlagsFromStruct(Config{}), 190 | &cli.BoolFlag{ 191 | Name: "dump-template", 192 | Usage: "dump configuration template yaml without making actual changes", 193 | }, 194 | ), 195 | Action: Action, 196 | } 197 | -------------------------------------------------------------------------------- /cmd/kmgm/list/list.go: -------------------------------------------------------------------------------- 1 | package list 2 | 3 | import ( 4 | "crypto/x509" 5 | "fmt" 6 | 7 | "github.com/urfave/cli/v2" 8 | 9 | "github.com/IPA-CyberLab/kmgm/action" 10 | "github.com/IPA-CyberLab/kmgm/storage" 11 | "github.com/IPA-CyberLab/kmgm/storage/issuedb" 12 | ) 13 | 14 | // FIXME[PX]: [DB op] list all issued certs 15 | // FIXME[PX]: [DB op] check that issued certs expires > threshold 16 | 17 | const dateFormat = "06/01/02" 18 | 19 | func CertInfo(c *x509.Certificate) string { 20 | return fmt.Sprintf("%s %s %v", 21 | c.NotBefore.Format(dateFormat), 22 | c.NotAfter.Format(dateFormat), 23 | c.Subject) 24 | } 25 | 26 | func LsProfile(env *action.Environment) error { 27 | now := env.NowImpl() 28 | 29 | ps, err := env.Storage.Profiles() 30 | if err != nil { 31 | return fmt.Errorf("Failed to list profiles: %w", err) 32 | } 33 | 34 | for _, p := range ps { 35 | fmt.Printf("%s %s\n", p.Name(), p.Status(now)) 36 | } 37 | 38 | return nil 39 | } 40 | 41 | var Command = &cli.Command{ 42 | Name: "list", 43 | Usage: "List certificates issued", 44 | Aliases: []string{"ls"}, 45 | Flags: []cli.Flag{ 46 | &cli.BoolFlag{ 47 | Name: "profile", 48 | Aliases: []string{"p", "profiles"}, 49 | Usage: "Print list of profiles", 50 | }, 51 | }, 52 | Action: func(c *cli.Context) error { 53 | env := action.GlobalEnvironment 54 | slog := env.Logger.Sugar() 55 | 56 | // FIXME[P3]: Unless verbose, omit time/level logging as well 57 | 58 | if c.Bool("profile") { 59 | if err := LsProfile(env); err != nil { 60 | return err 61 | } 62 | return nil 63 | } 64 | 65 | profile, err := env.Profile() 66 | if err != nil { 67 | return err 68 | } 69 | 70 | now := env.NowImpl() 71 | st := profile.Status(now) 72 | if st.Code != storage.ValidCA { 73 | if st.Code == storage.Expired { 74 | slog.Warnf("Expired %s") 75 | } else { 76 | slog.Infof("Could not find a valid CA profile %q: %v", env.ProfileName, st) 77 | return nil 78 | } 79 | } 80 | 81 | db, err := issuedb.New(profile.IssueDBPath()) 82 | if err != nil { 83 | return err 84 | } 85 | 86 | es, err := db.Entries() 87 | if err != nil { 88 | return err 89 | } 90 | 91 | // 1 2 3 4 5 6 7 92 | // 01234567890123456789012345678901234567890123456789012345678901234567890123456789 93 | // 01234567 0123456789012345678 94 | fmt.Printf(" YY/MM/DD YY/MM/DD\n") 95 | fmt.Printf("Status SerialNumber NotBefor NotAfter Subject\n") 96 | for _, e := range es { 97 | cert, err := e.ParseCertificate() 98 | var infotxt string 99 | if err != nil { 100 | infotxt = fmt.Sprintf("error: Failed to parse PEM: %v", err) 101 | } else { 102 | infotxt = CertInfo(cert) 103 | } 104 | 105 | switch e.State { 106 | case issuedb.IssueInProgress: 107 | fmt.Printf("issueing %19d\n", e.SerialNumber) 108 | 109 | case issuedb.ActiveCertificate: 110 | fmt.Printf("active %19d %s\n", e.SerialNumber, infotxt) 111 | } 112 | } 113 | 114 | return nil 115 | }, 116 | } 117 | -------------------------------------------------------------------------------- /cmd/kmgm/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | 7 | "go.uber.org/zap" 8 | 9 | "github.com/IPA-CyberLab/kmgm/cmd/kmgm/app" 10 | ) 11 | 12 | type ExitCoder interface { 13 | ExitCode() int 14 | } 15 | 16 | func ExitCodeOfError(err error) int { 17 | for { 18 | if ec, ok := err.(ExitCoder); ok { 19 | return ec.ExitCode() 20 | } 21 | 22 | if err = errors.Unwrap(err); err == nil { 23 | break 24 | } 25 | } 26 | 27 | return 1 28 | } 29 | 30 | func main() { 31 | if err := app.New().Run(os.Args); err != nil { 32 | // omit stacktrace 33 | zap.L().WithOptions(zap.AddStacktrace(zap.FatalLevel)).Error(err.Error()) 34 | os.Exit(ExitCodeOfError(err)) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /cmd/kmgm/remote/bootstrap/bootstrap.go: -------------------------------------------------------------------------------- 1 | package bootstrap 2 | 3 | import ( 4 | "context" 5 | "crypto" 6 | "errors" 7 | "fmt" 8 | "os" 9 | 10 | "github.com/urfave/cli/v2" 11 | 12 | action "github.com/IPA-CyberLab/kmgm/action" 13 | localissue "github.com/IPA-CyberLab/kmgm/action/issue" 14 | "github.com/IPA-CyberLab/kmgm/action/serve/authprofile" 15 | "github.com/IPA-CyberLab/kmgm/dname" 16 | "github.com/IPA-CyberLab/kmgm/frontend/validate" 17 | "github.com/IPA-CyberLab/kmgm/keyusage" 18 | "github.com/IPA-CyberLab/kmgm/remote/issue" 19 | "github.com/IPA-CyberLab/kmgm/storage" 20 | "github.com/IPA-CyberLab/kmgm/period" 21 | "github.com/IPA-CyberLab/kmgm/wcrypto" 22 | ) 23 | 24 | func EnsureKey(env *action.Environment) (crypto.PrivateKey, error) { 25 | slog := env.Logger.Sugar() 26 | privPath := env.Storage.ClientPrivateKeyPath() 27 | 28 | priv, err := storage.ReadPrivateKeyFile(privPath) 29 | if err == nil { 30 | slog.Infof("Using existing client private key %q.", privPath) 31 | return priv, nil 32 | } 33 | if !errors.Is(err, os.ErrNotExist) { 34 | return nil, err 35 | } 36 | 37 | if err := validate.MkdirAndCheckWritable(privPath); err != nil { 38 | return nil, err 39 | } 40 | priv, err = wcrypto.GenerateKey(env.Randr, wcrypto.ServerKeyType, "clientauth", env.Logger) 41 | if err != nil { 42 | return nil, err 43 | } 44 | if err := storage.WritePrivateKeyFile(privPath, priv); err != nil { 45 | return nil, err 46 | } 47 | slog.Infof("Wrote client private key to %q.", privPath) 48 | return priv, nil 49 | } 50 | 51 | func IssueCertPair(ctx context.Context, env *action.Environment) error { 52 | slog := env.Logger.Sugar() 53 | 54 | priv, err := EnsureKey(env) 55 | if err != nil { 56 | return err 57 | } 58 | 59 | certPath := env.Storage.ClientCertPath() 60 | _, err = storage.ReadCertificateFile(certPath) 61 | if err == nil { 62 | slog.Warnf("Existing kmgm client cert found. Not requesting new cert.") 63 | slog.Warnf("To force re-bootstrapping, delete file %q and try again.", certPath) 64 | 65 | return nil 66 | } 67 | if !errors.Is(err, os.ErrNotExist) { 68 | return fmt.Errorf("Error when reading existing client auth cert %q. Try removing the file and retry: %w", certPath, err) 69 | } 70 | if err := validate.MkdirAndCheckWritable(certPath); err != nil { 71 | return err 72 | } 73 | 74 | pub, err := wcrypto.ExtractPublicKey(priv) 75 | if err != nil { 76 | return err 77 | } 78 | 79 | hostname, err := os.Hostname() 80 | if err != nil { 81 | return err 82 | } 83 | cfg := &localissue.Config{ 84 | Subject: &dname.Config{ 85 | CommonName: hostname, 86 | }, 87 | KeyUsage: keyusage.KeyUsageTLSClient, 88 | Validity: period.FarFuture, 89 | KeyType: wcrypto.ServerKeyType, 90 | } 91 | certDer, err := issue.IssueCertificate(ctx, env, pub, cfg) 92 | if err != nil { 93 | return err 94 | } 95 | 96 | if err := storage.WriteCertificateDerFile(certPath, certDer); err != nil { 97 | return err 98 | } 99 | slog.Infof("Wrote issued client cert to %q.", certPath) 100 | return nil 101 | } 102 | 103 | var Command = &cli.Command{ 104 | Name: "bootstrap", 105 | Usage: "Register this client to the kmgm HTTPS/gRPC server", 106 | Flags: []cli.Flag{}, 107 | Action: func(c *cli.Context) error { 108 | env := action.GlobalEnvironment 109 | slog := env.Logger.Sugar() 110 | 111 | if env.ProfileName != storage.DefaultProfileName && 112 | env.ProfileName != authprofile.ProfileName { 113 | slog.Warnf("Specified --profile %q setting is ignored in bootstrap cmd.", env.ProfileName) 114 | } 115 | env.ProfileName = authprofile.ProfileName 116 | 117 | if err := env.EnsureClientConn(c.Context); err != nil { 118 | return err 119 | } 120 | if err := IssueCertPair(c.Context, env); err != nil { 121 | return err 122 | } 123 | 124 | // Rewrite ConnectionInfo to use the issued client certificate instead of bootstrap token. 125 | env.ConnectionInfo.AccessToken = "" 126 | env.ConnectionInfo.ClientCertificateFile = env.Storage.ClientCertPath() 127 | env.ConnectionInfo.ClientPrivateKeyFile = env.Storage.ClientPrivateKeyPath() 128 | 129 | if err := env.SaveConnectionInfo(); err != nil { 130 | return err 131 | } 132 | 133 | return nil 134 | }, 135 | } 136 | -------------------------------------------------------------------------------- /cmd/kmgm/remote/issue/issue.go: -------------------------------------------------------------------------------- 1 | package issue 2 | 3 | import ( 4 | "context" 5 | "crypto" 6 | "crypto/x509" 7 | 8 | "github.com/urfave/cli/v2" 9 | 10 | "github.com/IPA-CyberLab/kmgm/action" 11 | localissue "github.com/IPA-CyberLab/kmgm/cmd/kmgm/issue" 12 | "github.com/IPA-CyberLab/kmgm/pb" 13 | "github.com/IPA-CyberLab/kmgm/remote/issue" 14 | "github.com/IPA-CyberLab/kmgm/structflags" 15 | ) 16 | 17 | type Remote struct { 18 | } 19 | 20 | var _ = localissue.Strategy(Remote{}) 21 | 22 | func (Remote) EnsureCA(ctx context.Context, env *action.Environment) error { 23 | if err := env.EnsureClientConn(ctx); err != nil { 24 | return err 25 | } 26 | 27 | sc := pb.NewCertificateServiceClient(env.ClientConn) 28 | resp, err := sc.IssuePreflight(ctx, &pb.IssuePreflightRequest{ 29 | Profile: env.ProfileName, 30 | }) 31 | if err != nil { 32 | return err 33 | } 34 | _ = resp 35 | 36 | return nil 37 | } 38 | 39 | func (Remote) CACert(ctx context.Context, env *action.Environment) *x509.Certificate { 40 | slog := env.Logger.Sugar() 41 | 42 | if err := env.EnsureClientConn(ctx); err != nil { 43 | slog.Debugf("CASubject: EnsureClientConn: %v", err) 44 | return nil 45 | } 46 | 47 | sc := pb.NewCertificateServiceClient(env.ClientConn) 48 | resp, err := sc.GetCertificate(ctx, &pb.GetCertificateRequest{ 49 | Profile: env.ProfileName, 50 | SerialNumber: 0, 51 | }) 52 | if err != nil { 53 | slog.Debugf("CASubject: GetCertificate: %v", err) 54 | return nil 55 | } 56 | cert, err := x509.ParseCertificate(resp.Certificate) 57 | if err != nil { 58 | slog.Debugf("CASubject: ParseCertificate: %v", err) 59 | return nil 60 | } 61 | 62 | return cert 63 | } 64 | 65 | func (Remote) Issue(ctx context.Context, env *action.Environment, pub crypto.PublicKey, cfg *issue.Config) ([]byte, error) { 66 | return issue.IssueCertificate(ctx, env, pub, cfg) 67 | } 68 | 69 | var Command = &cli.Command{ 70 | Name: "issue", 71 | Usage: "Issue a new certificate or renew an existing certificate. Generates private key if needed.", 72 | Flags: structflags.MustPopulateFlagsFromStruct(localissue.Config{}), 73 | Action: func(c *cli.Context) error { 74 | return localissue.ActionImpl(Remote{}, c) 75 | }, 76 | } 77 | -------------------------------------------------------------------------------- /cmd/kmgm/remote/show/show.go: -------------------------------------------------------------------------------- 1 | package show 2 | 3 | import ( 4 | "context" 5 | "crypto/x509" 6 | "strconv" 7 | 8 | "github.com/IPA-CyberLab/kmgm/action" 9 | localshow "github.com/IPA-CyberLab/kmgm/cmd/kmgm/show" 10 | "github.com/IPA-CyberLab/kmgm/pb" 11 | "github.com/urfave/cli/v2" 12 | ) 13 | 14 | func FindCertificateWithPrefix(ctx context.Context, env *action.Environment, prefix string) (*x509.Certificate, error) { 15 | if err := env.EnsureClientConn(ctx); err != nil { 16 | return nil, err 17 | } 18 | 19 | sc := pb.NewCertificateServiceClient(env.ClientConn) 20 | 21 | var err error 22 | sn := int64(0) 23 | 24 | if prefix != "" && prefix != "ca" { 25 | sn, err = strconv.ParseInt(prefix, 10, 64) 26 | if err != nil { 27 | return nil, err 28 | } 29 | } 30 | // FIXME[P1] actually perform prefix match 31 | 32 | resp, err := sc.GetCertificate(ctx, &pb.GetCertificateRequest{ 33 | Profile: env.ProfileName, 34 | SerialNumber: sn, 35 | }) 36 | if err != nil { 37 | return nil, err 38 | } 39 | 40 | cert, err := x509.ParseCertificate(resp.Certificate) 41 | if err != nil { 42 | return nil, err 43 | } 44 | return cert, nil 45 | } 46 | 47 | var Command = &cli.Command{ 48 | Name: "show", 49 | Usage: "Show CA status, an existing certificate and/or its key.", 50 | UsageText: localshow.Command.UsageText, 51 | Flags: localshow.Command.Flags, 52 | Action: localshow.ActionImpl(FindCertificateWithPrefix), 53 | } 54 | -------------------------------------------------------------------------------- /cmd/kmgm/remote/subcommand.go: -------------------------------------------------------------------------------- 1 | package remote 2 | 3 | import ( 4 | "github.com/urfave/cli/v2" 5 | 6 | action "github.com/IPA-CyberLab/kmgm/action" 7 | "github.com/IPA-CyberLab/kmgm/cmd/kmgm/remote/bootstrap" 8 | "github.com/IPA-CyberLab/kmgm/cmd/kmgm/remote/issue" 9 | "github.com/IPA-CyberLab/kmgm/cmd/kmgm/remote/show" 10 | "github.com/IPA-CyberLab/kmgm/cmd/kmgm/remote/version" 11 | "github.com/IPA-CyberLab/kmgm/remote" 12 | "github.com/IPA-CyberLab/kmgm/structflags" 13 | ) 14 | 15 | var Command = &cli.Command{ 16 | Name: "client", 17 | Aliases: []string{"c", "cli"}, 18 | Usage: "Interact with remote CA", 19 | Flags: structflags.MustPopulateFlagsFromStruct(remote.ConnectionInfo{}), 20 | Before: func(c *cli.Context) error { 21 | env := action.GlobalEnvironment 22 | if err := env.LoadConnectionInfo(); err != nil { 23 | return err 24 | } 25 | if err := structflags.PopulateStructFromCliContext(&env.ConnectionInfo, c); err != nil { 26 | return err 27 | } 28 | 29 | return nil 30 | }, 31 | Subcommands: []*cli.Command{ 32 | bootstrap.Command, 33 | issue.Command, 34 | version.Command, 35 | show.Command, 36 | }, 37 | } 38 | -------------------------------------------------------------------------------- /cmd/kmgm/remote/version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/urfave/cli/v2" 7 | 8 | action "github.com/IPA-CyberLab/kmgm/action" 9 | "github.com/IPA-CyberLab/kmgm/pb" 10 | ) 11 | 12 | func queryVersion(ctx context.Context, env *action.Environment) error { 13 | slog := env.Logger.Sugar() 14 | 15 | sc := pb.NewVersionServiceClient(env.ClientConn) 16 | resp, err := sc.GetVersion(ctx, &pb.GetVersionRequest{}) 17 | if err != nil { 18 | return err 19 | } 20 | slog.Infof("Version: %s", resp.Version) 21 | slog.Infof("Commit: %s", resp.Commit) 22 | 23 | return nil 24 | } 25 | 26 | var Command = &cli.Command{ 27 | Name: "version", 28 | Usage: "Query remote CA kmgm version", 29 | Action: func(c *cli.Context) error { 30 | env := action.GlobalEnvironment 31 | if err := env.EnsureClientConn(c.Context); err != nil { 32 | return err 33 | } 34 | 35 | return queryVersion(c.Context, env) 36 | }, 37 | } 38 | -------------------------------------------------------------------------------- /cmd/kmgm/remote_test.go: -------------------------------------------------------------------------------- 1 | package main_test 2 | 3 | import ( 4 | "context" 5 | "path/filepath" 6 | "testing" 7 | 8 | "github.com/IPA-CyberLab/kmgm/cmd/kmgm/serve/testserver" 9 | "github.com/IPA-CyberLab/kmgm/cmd/kmgm/testkmgm" 10 | "github.com/IPA-CyberLab/kmgm/storage" 11 | "github.com/IPA-CyberLab/kmgm/testutils" 12 | ) 13 | 14 | func TestServe_Noop(t *testing.T) { 15 | _ = testserver.Run(t) 16 | } 17 | 18 | func TestServe_Issue(t *testing.T) { 19 | ts := testserver.Run(t) 20 | basedir := testutils.PrepareBasedir(t) 21 | 22 | t.Run("bootstrap", func(t *testing.T) { 23 | logs, err := testkmgm.Run(t, context.Background(), basedir, nil, []string{"client", "--server", ts.AddrPort, "--cacert", ts.CACertPath, "--token", testserver.BootstrapToken, "bootstrap"}, testkmgm.NowDefault) 24 | testutils.ExpectErr(t, err, nil) 25 | testutils.ExpectLogMessage(t, logs, "Wrote server connection info to file ") 26 | }) 27 | 28 | t.Run("nonexistent profile", func(t *testing.T) { 29 | _, err := testkmgm.Run(t, context.Background(), basedir, nil, []string{"--profile", "noexist", "client", "issue"}, testkmgm.NowDefault) 30 | testutils.ExpectErrMessage(t, err, `Can't issue certificate from CA profile "noexist"`) 31 | }) 32 | 33 | t.Run("specify profile", func(t *testing.T) { 34 | yaml := []byte(` 35 | noDefault: true 36 | 37 | subject: 38 | commonName: myCA 39 | validity: farfuture 40 | keyType: ecdsa 41 | `) 42 | logs, err := testkmgm.Run(t, context.Background(), ts.Basedir, yaml, []string{"--profile", "myprofile", "setup"}, testkmgm.NowDefault) 43 | testutils.ExpectErr(t, err, nil) 44 | testutils.ExpectLogMessage(t, logs, "CA setup successfully completed") 45 | 46 | certPath := filepath.Join(basedir, "issue.cert.pem") 47 | logs, err = testkmgm.Run(t, context.Background(), basedir, nil, []string{"--profile", "myprofile", "client", "issue", "--cert", certPath}, testkmgm.NowDefault) 48 | testutils.ExpectErr(t, err, nil) 49 | testutils.ExpectLogMessage(t, logs, `Generating certificate... Done.`) 50 | 51 | cert, err := storage.ReadCertificateFile(certPath) 52 | if err != nil { 53 | t.Fatalf("cert read: %v", err) 54 | } 55 | 56 | if cert.Issuer.String() != "CN=myCA" { 57 | t.Fatalf("unexpected cert issuer: %v", cert.Issuer) 58 | } 59 | }) 60 | 61 | } 62 | -------------------------------------------------------------------------------- /cmd/kmgm/serve/serve.go: -------------------------------------------------------------------------------- 1 | package serve 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/urfave/cli/v2" 9 | 10 | action "github.com/IPA-CyberLab/kmgm/action" 11 | "github.com/IPA-CyberLab/kmgm/action/serve" 12 | "github.com/IPA-CyberLab/kmgm/structflags" 13 | "github.com/IPA-CyberLab/kmgm/wcrypto" 14 | ) 15 | 16 | var IssueHttpDefaultShutdown = 5 * time.Minute 17 | 18 | var Command = &cli.Command{ 19 | Name: "serve", 20 | Usage: "Start Komagome PKI HTTPS/gRPC API Server", 21 | Aliases: []string{"s", "srv"}, 22 | Flags: append(structflags.MustPopulateFlagsFromStruct(serve.Config{}), 23 | &cli.BoolFlag{ 24 | Name: "bootstrap", 25 | Usage: "enable node bootstrapping via (generated) token.", 26 | }, 27 | &cli.StringFlag{ 28 | Name: "bootstrap-token", 29 | Usage: "enable node bootstrapping using the specified token.", 30 | }, 31 | &cli.PathFlag{ 32 | Name: "bootstrap-token-file", 33 | Usage: "enable node bootstrapping using the specified token file.", 34 | }, 35 | ), 36 | Action: func(c *cli.Context) error { 37 | env := action.GlobalEnvironment 38 | 39 | cfg, err := serve.DefaultConfig() 40 | if err != nil { 41 | return err 42 | } 43 | if err := structflags.PopulateStructFromCliContext(cfg, c); err != nil { 44 | return err 45 | } 46 | 47 | if !c.IsSet("auto-shutdown") && cfg.IssueHttp > 0 { 48 | cfg.AutoShutdown = IssueHttpDefaultShutdown 49 | } 50 | 51 | if c.IsSet("bootstrap") || c.IsSet("bootstrap-token") { 52 | if c.IsSet("bootstrap-token-file") { 53 | return errors.New("--bootstrap-token-file can't be specified when --bootstrap,--bootstrap-token is specified.") 54 | } 55 | 56 | token := c.String("bootstrap-token") 57 | if token == "" { 58 | token, err = wcrypto.GenBase64Token(env.Randr, env.Logger) 59 | if err != nil { 60 | return err 61 | } 62 | } 63 | cfg.Bootstrap = &serve.FixedTokenAuthProvider{ 64 | Token: token, 65 | NotAfter: time.Now().Add(15 * time.Minute), 66 | Logger: env.Logger, 67 | } 68 | } else if c.IsSet("bootstrap-token-file") { 69 | ta, err := serve.NewTokenFileAuthProvider( 70 | c.String("bootstrap-token-file"), env.Logger) 71 | if err != nil { 72 | return fmt.Errorf("Failed to initialize tokenFileAuthProvider: %w", err) 73 | } 74 | cfg.Bootstrap = ta 75 | } 76 | 77 | return serve.Run(c.Context, env, cfg) 78 | }, 79 | } 80 | -------------------------------------------------------------------------------- /cmd/kmgm/serve/testserver/testserver.go: -------------------------------------------------------------------------------- 1 | package testserver 2 | 3 | import ( 4 | "context" 5 | "crypto/x509" 6 | "fmt" 7 | "net" 8 | "path/filepath" 9 | "testing" 10 | "time" 11 | 12 | "github.com/IPA-CyberLab/kmgm/cmd/kmgm/testkmgm" 13 | "github.com/IPA-CyberLab/kmgm/storage" 14 | "github.com/IPA-CyberLab/kmgm/testutils" 15 | "github.com/IPA-CyberLab/kmgm/wcrypto" 16 | "github.com/prometheus/client_golang/prometheus" 17 | ) 18 | 19 | const BootstrapToken = "testtoken" 20 | 21 | type TestServer struct { 22 | AddrPort string 23 | CACertPath string 24 | CACert *x509.Certificate 25 | PubKeyHash string 26 | Basedir string 27 | } 28 | 29 | type option struct { 30 | RunSetup bool 31 | } 32 | 33 | type Option func(*option) 34 | 35 | func RunSetup(o *option) { o.RunSetup = true } 36 | 37 | func Run(t *testing.T, tsos ...Option) *TestServer { 38 | t.Helper() 39 | 40 | o := &option{} 41 | for _, tso := range tsos { 42 | tso(o) 43 | } 44 | 45 | r := prometheus.NewRegistry() 46 | prometheus.DefaultRegisterer = r 47 | prometheus.DefaultGatherer = r 48 | 49 | basedir := testutils.PrepareBasedir(t) 50 | 51 | if o.RunSetup { 52 | logs, err := testkmgm.Run(t, context.Background(), basedir, nil, []string{"setup"}, testkmgm.NowDefault) 53 | if err != nil { 54 | t.Fatalf("Failed to run setup: %v", err) 55 | } 56 | testutils.ExpectLogMessage(t, logs, "CA setup successfully completed") 57 | } 58 | 59 | testPort := 34000 60 | addrPort := fmt.Sprintf("127.0.0.1:%d", testPort) 61 | cacertPath := filepath.Join(basedir, ".kmgm_server/cacert.pem") 62 | 63 | ctx, cancel := context.WithCancel(context.Background()) 64 | 65 | joinC := make(chan struct{}) 66 | go func() { 67 | logs, err := testkmgm.Run(t, ctx, basedir, nil, []string{"serve", "--reuse-port", "--listen-addr", addrPort, "--bootstrap-token", BootstrapToken}, testkmgm.NowDefault) 68 | _ = err // expectErr(t, err, context.Canceled) // not always reliable 69 | testutils.ExpectLogMessage(t, logs, "Started listening") 70 | close(joinC) 71 | }() 72 | 73 | for i := 0; i < 10; i++ { 74 | conn, err := net.Dial("tcp", addrPort) 75 | if err != nil { 76 | t.Logf("net.Dial(%s) error: %v", addrPort, err) 77 | 78 | time.Sleep(100 * time.Millisecond) 79 | continue 80 | } 81 | conn.Close() 82 | t.Logf("net.Dial(%s) success", addrPort) 83 | break 84 | } 85 | 86 | cacert, err := storage.ReadCertificateFile(cacertPath) 87 | if err != nil { 88 | t.Fatalf("Failed to read cacert: %v", err) 89 | } 90 | pubkeyhash, err := wcrypto.PubKeyPinString(cacert.PublicKey) 91 | if err != nil { 92 | t.Fatalf("Failed to compute pubkeyhash: %v", err) 93 | } 94 | 95 | t.Cleanup(func() { 96 | cancel() 97 | <-joinC 98 | }) 99 | return &TestServer{ 100 | AddrPort: addrPort, 101 | CACertPath: cacertPath, 102 | CACert: cacert, 103 | PubKeyHash: pubkeyhash, 104 | Basedir: basedir, 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /cmd/kmgm/show/format.go: -------------------------------------------------------------------------------- 1 | package show 2 | 3 | import "fmt" 4 | 5 | type FormatType int 6 | 7 | const ( 8 | FormatFull FormatType = iota 9 | FormatPEM 10 | ) 11 | 12 | func (t FormatType) String() string { 13 | switch t { 14 | case FormatFull: 15 | return "full" 16 | case FormatPEM: 17 | return "pem" 18 | default: 19 | return fmt.Sprintf("unknown_formattype_%d", int(t)) 20 | } 21 | } 22 | 23 | func FormatTypeFromString(s string) (FormatType, error) { 24 | switch s { 25 | case "full": 26 | return FormatFull, nil 27 | case "pem": 28 | return FormatPEM, nil 29 | default: 30 | return FormatFull, fmt.Errorf("Unknown format %q.", s) 31 | } 32 | } 33 | 34 | func (t FormatType) ShouldOutputInfo() bool { 35 | switch t { 36 | case FormatFull: 37 | return true 38 | default: 39 | return false 40 | } 41 | } 42 | 43 | func (t FormatType) ShouldOutputPEM() bool { 44 | switch t { 45 | case FormatFull, FormatPEM: 46 | return true 47 | default: 48 | return false 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /cmd/kmgm/show/ifchanged.go: -------------------------------------------------------------------------------- 1 | package show 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "io/ioutil" 7 | "os" 8 | ) 9 | 10 | type IfChangedWriteFile struct { 11 | f *os.File 12 | originalContent []byte 13 | buf bytes.Buffer 14 | } 15 | 16 | var _ io.Writer = &IfChangedWriteFile{} 17 | 18 | func NewIfChangedWriteFile(path string) (*IfChangedWriteFile, error) { 19 | f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0666) 20 | if err != nil { 21 | f.Close() 22 | return nil, err 23 | } 24 | originalContent, err := ioutil.ReadAll(f) 25 | if err != nil { 26 | f.Close() 27 | return nil, err 28 | } 29 | 30 | return &IfChangedWriteFile{ 31 | f: f, 32 | originalContent: originalContent, 33 | }, nil 34 | } 35 | 36 | func (wf *IfChangedWriteFile) Write(bs []byte) (int, error) { 37 | return wf.buf.Write(bs) 38 | } 39 | 40 | func (wf *IfChangedWriteFile) Close() error { 41 | bs := wf.buf.Bytes() 42 | if !bytes.Equal(wf.originalContent, bs) { 43 | if _, err := wf.f.Seek(0, os.SEEK_SET); err != nil { 44 | wf.f.Close() 45 | return err 46 | } 47 | if _, err := wf.f.Write(bs); err != nil { 48 | wf.f.Close() 49 | return err 50 | } 51 | } 52 | 53 | if err := wf.f.Close(); err != nil { 54 | return err 55 | } 56 | return nil 57 | } 58 | -------------------------------------------------------------------------------- /cmd/kmgm/testkmgm/gen.go: -------------------------------------------------------------------------------- 1 | package testkmgm 2 | 3 | //go:generate go run generate_testkeys.go 4 | -------------------------------------------------------------------------------- /cmd/kmgm/testkmgm/generate_testkeys.go: -------------------------------------------------------------------------------- 1 | //go:build ignore 2 | 3 | package main 4 | 5 | import ( 6 | "crypto" 7 | "crypto/rand" 8 | "fmt" 9 | "os" 10 | 11 | "go.uber.org/zap" 12 | 13 | "github.com/IPA-CyberLab/kmgm/pemparser" 14 | "github.com/IPA-CyberLab/kmgm/wcrypto" 15 | ) 16 | 17 | func main() { 18 | logger := zap.NewExample().Sugar() 19 | 20 | rsa2048keys := []crypto.PrivateKey{} 21 | for i := 0; i < 10; i++ { 22 | logger.Infof("Generating rsa2048key[%d]:", i) 23 | pk, err := wcrypto.GenerateKey(rand.Reader, wcrypto.KeyRSA2048, "testkeys", logger.Desugar()) 24 | if err != nil { 25 | panic(err) 26 | } 27 | rsa2048keys = append(rsa2048keys, pk) 28 | } 29 | 30 | rsa4096keys := []crypto.PrivateKey{} 31 | for i := 0; i < 10; i++ { 32 | logger.Infof("Generating rsa4096key[%d]:", i) 33 | pk, err := wcrypto.GenerateKey(rand.Reader, wcrypto.KeyRSA4096, "testkeys", logger.Desugar()) 34 | if err != nil { 35 | panic(err) 36 | } 37 | rsa4096keys = append(rsa4096keys, pk) 38 | } 39 | 40 | eckeys := []crypto.PrivateKey{} 41 | for i := 0; i < 10; i++ { 42 | logger.Infof("Generating eckey[%d]:", i) 43 | pk, err := wcrypto.GenerateKey(rand.Reader, wcrypto.KeySECP256R1, "testkeys", logger.Desugar()) 44 | if err != nil { 45 | panic(err) 46 | } 47 | eckeys = append(eckeys, pk) 48 | } 49 | 50 | f, err := os.Create("testkeys.go") 51 | if err != nil { 52 | panic(err) 53 | } 54 | defer f.Close() 55 | 56 | fmt.Fprintln(f, "package testkmgm") 57 | fmt.Fprintln(f, "// This file is autogenerated from generate_testkeys.go. Do not modify manually.") 58 | 59 | fmt.Fprintf(f, "var RSA2048Keys = []string{\n") 60 | for _, pk := range rsa2048keys { 61 | pemData, err := pemparser.MarshalPrivateKey(pk) 62 | if err != nil { 63 | panic(err) 64 | } 65 | fmt.Fprintf(f, " `%s`,\n", pemData) 66 | } 67 | fmt.Fprintln(f, "}") 68 | 69 | fmt.Fprintf(f, "var RSA4096Keys = []string{\n") 70 | for _, pk := range rsa4096keys { 71 | pemData, err := pemparser.MarshalPrivateKey(pk) 72 | if err != nil { 73 | panic(err) 74 | } 75 | fmt.Fprintf(f, " `%s`,\n", pemData) 76 | } 77 | fmt.Fprintln(f, "}") 78 | 79 | fmt.Fprintf(f, "var ECKeys = []string{\n") 80 | for _, pk := range eckeys { 81 | pemData, err := pemparser.MarshalPrivateKey(pk) 82 | if err != nil { 83 | panic(err) 84 | } 85 | fmt.Fprintf(f, " `%s`,\n", pemData) 86 | } 87 | fmt.Fprintln(f, "}") 88 | } 89 | -------------------------------------------------------------------------------- /cmd/kmgm/testkmgm/testkeys_util.go: -------------------------------------------------------------------------------- 1 | package testkmgm 2 | 3 | import ( 4 | "crypto" 5 | "os" 6 | 7 | "github.com/IPA-CyberLab/kmgm/pemparser" 8 | "github.com/IPA-CyberLab/kmgm/wcrypto" 9 | ) 10 | 11 | var rsa2048KeyIndex int = 0 12 | var rsa4096KeyIndex int = 0 13 | var ecKeyIndex int = 0 14 | 15 | func ResetPreGenKeyIndex() { 16 | rsa2048KeyIndex = 0 17 | rsa4096KeyIndex = 0 18 | ecKeyIndex = 0 19 | } 20 | 21 | func GetPregenKeyPEM(ktype wcrypto.KeyType) []byte { 22 | switch ktype { 23 | case wcrypto.KeyRSA2048: 24 | pemstr := RSA2048Keys[rsa2048KeyIndex%len(RSA2048Keys)] 25 | rsa2048KeyIndex++ 26 | return []byte(pemstr) 27 | case wcrypto.KeyRSA4096: 28 | pemstr := RSA4096Keys[rsa4096KeyIndex%len(RSA4096Keys)] 29 | rsa4096KeyIndex++ 30 | return []byte(pemstr) 31 | case wcrypto.KeySECP256R1: 32 | pemstr := ECKeys[ecKeyIndex%len(ECKeys)] 33 | ecKeyIndex++ 34 | return []byte(pemstr) 35 | default: 36 | panic("not available") 37 | } 38 | } 39 | 40 | func WritePregenKeyPEMToFile(ktype wcrypto.KeyType, path string) crypto.PrivateKey { 41 | pemData := GetPregenKeyPEM(ktype) 42 | pk, err := pemparser.ParsePrivateKey(pemData) 43 | if err != nil { 44 | panic(err) 45 | } 46 | if err := os.WriteFile(path, pemData, 0644); err != nil { 47 | panic(err) 48 | } 49 | return pk 50 | } 51 | 52 | func GetPregenKey(ktype wcrypto.KeyType) crypto.PrivateKey { 53 | pemData := GetPregenKeyPEM(ktype) 54 | pk, err := pemparser.ParsePrivateKey(pemData) 55 | if err != nil { 56 | panic(err) 57 | } 58 | return pk 59 | } 60 | -------------------------------------------------------------------------------- /cmd/kmgm/testkmgm/testkmgm.go: -------------------------------------------------------------------------------- 1 | package testkmgm 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "io/ioutil" 7 | mrand "math/rand" 8 | "os" 9 | "testing" 10 | "time" 11 | 12 | "github.com/urfave/cli/v2" 13 | "go.uber.org/zap" 14 | "go.uber.org/zap/zapcore" 15 | "go.uber.org/zap/zaptest/observer" 16 | 17 | "github.com/IPA-CyberLab/kmgm/action" 18 | "github.com/IPA-CyberLab/kmgm/cmd/kmgm/app" 19 | "github.com/IPA-CyberLab/kmgm/frontend" 20 | "github.com/IPA-CyberLab/kmgm/storage" 21 | ) 22 | 23 | var NowDefault = time.Date(2020, time.March, 1, 0, 0, 0, 0, time.UTC) 24 | 25 | func mockNowImpl(t time.Time) func() time.Time { 26 | return func() time.Time { 27 | return t 28 | } 29 | } 30 | 31 | type mockClock struct { 32 | t time.Time 33 | } 34 | 35 | func (c mockClock) Now() time.Time { 36 | return c.t 37 | } 38 | 39 | func (c mockClock) NewTicker(duration time.Duration) *time.Ticker { 40 | panic("not implemented") 41 | return nil 42 | } 43 | 44 | func Env(t *testing.T, basedir string, mockNow time.Time) *action.Environment { 45 | stor, err := storage.New(basedir) 46 | if err != nil { 47 | t.Fatalf("storage.New: %v", err) 48 | } 49 | 50 | fe := &frontend.NonInteractive{ 51 | Logger: zap.L(), 52 | } 53 | 54 | env, err := action.NewEnvironment(fe, stor) 55 | if err != nil { 56 | panic(err) 57 | } 58 | env.Frontend = &frontend.NonInteractive{Logger: zap.L()} 59 | env.NowImpl = mockNowImpl(mockNow) 60 | env.Randr = MrandReader{} 61 | env.PregenKeySupplier = GetPregenKey 62 | 63 | return env 64 | } 65 | 66 | type MrandReader struct{} 67 | 68 | func (MrandReader) Read(p []byte) (int, error) { 69 | return mrand.Read(p) 70 | } 71 | 72 | func Run(t *testing.T, ctx context.Context, basedir string, configYaml []byte, args []string, mockNow time.Time) (*observer.ObservedLogs, error) { 73 | t.Helper() 74 | 75 | a := app.New() 76 | 77 | zobs, logs := observer.New(zapcore.DebugLevel) 78 | 79 | logger := zap.New(zobs, zap.WithClock(mockClock{mockNow})) 80 | a.Metadata["Logger"] = logger 81 | a.Metadata["NowImpl"] = mockNowImpl(mockNow) 82 | a.Metadata["Randr"] = MrandReader{} 83 | a.Metadata["PregenKeySupplier"] = GetPregenKey 84 | 85 | var stdoutBuf bytes.Buffer 86 | var stderrBuf bytes.Buffer 87 | a.Writer = &stdoutBuf 88 | a.ErrWriter = &stderrBuf 89 | 90 | // replace `ExitErrHandler` to avoid exiting the test process. 91 | a.ExitErrHandler = func(cCtx *cli.Context, err error) {} 92 | 93 | var tmpfile *os.File 94 | if configYaml != nil { 95 | var err error 96 | tmpfile, err = ioutil.TempFile("", "kmgm-testconfig-*.yml") 97 | if err != nil { 98 | t.Fatal(err) 99 | } 100 | defer os.Remove(tmpfile.Name()) 101 | 102 | if _, err := tmpfile.Write(configYaml); err != nil { 103 | t.Fatal(err) 104 | } 105 | // We need Close() here since we read the tmpfile later in the test. 106 | if err := tmpfile.Close(); err != nil { 107 | t.Fatal(err) 108 | } 109 | } 110 | 111 | as := []string{"kmgm", "--non-interactive", "--verbose", "--basedir", basedir} 112 | if configYaml != nil { 113 | as = append(as, "--config", tmpfile.Name()) 114 | } 115 | as = append(as, args...) 116 | err := a.RunContext(ctx, as) 117 | 118 | t.Logf("stdout: %s", stdoutBuf.String()) 119 | t.Logf("stderr: %s", stderrBuf.String()) 120 | for _, l := range logs.All() { 121 | t.Logf("🔒 %s", l.Message) 122 | } 123 | return logs, err 124 | } 125 | -------------------------------------------------------------------------------- /cmd/kmgm/tool/dump/dump.go: -------------------------------------------------------------------------------- 1 | package dump 2 | 3 | import ( 4 | "io" 5 | "io/ioutil" 6 | "os" 7 | 8 | "github.com/urfave/cli/v2" 9 | 10 | "github.com/IPA-CyberLab/kmgm/cmd/kmgm/show" 11 | "github.com/IPA-CyberLab/kmgm/pemparser" 12 | ) 13 | 14 | var Command = &cli.Command{ 15 | Name: "dump", 16 | Usage: "dump details of the input x509 certificate", 17 | UsageText: `kmgm tool dump`, 18 | Flags: []cli.Flag{ 19 | &cli.StringFlag{ 20 | Name: "output", 21 | Aliases: []string{"o"}, 22 | Usage: "Output format. (full, pem)", 23 | Value: "full", 24 | }, 25 | &cli.StringFlag{ 26 | Name: "input", 27 | Aliases: []string{"i"}, 28 | Usage: "The certificate file.", 29 | Value: "-", 30 | }, 31 | }, 32 | Action: func(c *cli.Context) error { 33 | ftstr := c.String("output") 34 | ft, err := show.FormatTypeFromString(ftstr) 35 | if err != nil { 36 | return err 37 | } 38 | 39 | var r io.Reader 40 | inpath := c.String("input") 41 | if inpath == "-" { 42 | r = os.Stdin 43 | } else { 44 | f, err := os.Open(inpath) 45 | if err != nil { 46 | return err 47 | } 48 | r = f 49 | defer f.Close() 50 | } 51 | 52 | bs, err := ioutil.ReadAll(r) 53 | if err != nil { 54 | return err 55 | } 56 | 57 | certs, err := pemparser.ParseCertificates(bs) 58 | if err != nil { 59 | return err 60 | } 61 | 62 | w := os.Stdout 63 | for _, cert := range certs { 64 | show.PrintCertInfo(w, cert, ft) 65 | } 66 | 67 | return nil 68 | }, 69 | } 70 | -------------------------------------------------------------------------------- /cmd/kmgm/tool/pubkeyhash/format.go: -------------------------------------------------------------------------------- 1 | package pubkeyhash 2 | 3 | import "fmt" 4 | 5 | type FormatType int 6 | 7 | const ( 8 | FormatFull FormatType = iota 9 | FormatHashOnly 10 | ) 11 | 12 | func FormatTypeFromString(s string) (FormatType, error) { 13 | switch s { 14 | case "full": 15 | return FormatFull, nil 16 | case "hashonly": 17 | return FormatHashOnly, nil 18 | default: 19 | return FormatFull, fmt.Errorf("Unknown format %q.", s) 20 | } 21 | } 22 | 23 | func (ft FormatType) ShouldOutputLabel() bool { 24 | switch ft { 25 | case FormatFull: 26 | return true 27 | default: 28 | return false 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /cmd/kmgm/tool/pubkeyhash/pubkeyhash.go: -------------------------------------------------------------------------------- 1 | package pubkeyhash 2 | 3 | import ( 4 | "crypto" 5 | "crypto/x509" 6 | "encoding/pem" 7 | "fmt" 8 | "io" 9 | "io/ioutil" 10 | "os" 11 | "reflect" 12 | "strings" 13 | 14 | "github.com/urfave/cli/v2" 15 | "go.uber.org/zap" 16 | 17 | "github.com/IPA-CyberLab/kmgm/action" 18 | "github.com/IPA-CyberLab/kmgm/pemparser" 19 | "github.com/IPA-CyberLab/kmgm/wcrypto" 20 | ) 21 | 22 | type KeyHash struct { 23 | Label string 24 | Hash string 25 | } 26 | 27 | func ExtractPublicKeyHashesFromPem(bs []byte, logger *zap.Logger) ([]KeyHash, error) { 28 | slog := logger.Sugar() 29 | 30 | khs := make([]KeyHash, 0) 31 | err := pemparser.ForeachPemBlock(bs, func(b *pem.Block) error { 32 | label := "" 33 | var pub crypto.PublicKey 34 | 35 | if priv, err := pemparser.ParseSinglePrivateKeyBlock(b); err == nil { 36 | label = fmt.Sprintf("PrivateKey Type=%q", b.Type) 37 | pub, err = wcrypto.ExtractPublicKey(priv) 38 | if err != nil { 39 | return err 40 | } 41 | } else { 42 | switch b.Type { 43 | case pemparser.CertificatePemType: 44 | cert, err := x509.ParseCertificate(b.Bytes) 45 | if err != nil { 46 | slog.Errorf("Failed to parse a %s block: %v", b.Type, err) 47 | return nil // ignore err to continue parsing to next pem block 48 | } 49 | 50 | label = fmt.Sprintf("Certificate %v", cert.Subject) 51 | pub = cert.PublicKey 52 | case pemparser.PublicKeyPemType: 53 | pubi, err := x509.ParsePKIXPublicKey(b.Bytes) 54 | if err != nil { 55 | slog.Errorf("Failed to parse a %s block: %v", b.Type, err) 56 | return nil // ignore err to continue parsing to next pem block 57 | } 58 | 59 | var ok bool 60 | pub, ok = pubi.(crypto.PublicKey) 61 | if !ok { 62 | slog.Errorf("Unknown public key type: %v", reflect.TypeOf(pubi)) 63 | return nil // ignore err to continue parsing to next pem block 64 | } 65 | label = fmt.Sprintf("PublicKey Type=%v", b.Type) 66 | } 67 | } 68 | if pub == nil { 69 | slog.Errorf("Could not extract a public key from block: Type: %q.", b.Type) 70 | return nil 71 | } 72 | 73 | hash, err := wcrypto.PubKeyPinString(pub) 74 | if err != nil { 75 | slog.Errorf("Failed to generate pubkeyhash: %v", err) 76 | return nil // ignore err to continue parsing other pem blocks 77 | } 78 | kh := KeyHash{Label: label, Hash: hash} 79 | 80 | for i, e := range khs { 81 | if kh.Hash == e.Hash { 82 | // overwrite label if Certificate label 83 | if strings.HasPrefix(kh.Label, "Certificate") { 84 | khs[i].Label = kh.Label 85 | } 86 | 87 | // continue parsing to next pem block 88 | return nil 89 | } 90 | } 91 | khs = append(khs, kh) 92 | return nil 93 | }) 94 | if err != nil { 95 | return nil, err 96 | } 97 | return khs, nil 98 | } 99 | 100 | var Command = &cli.Command{ 101 | Name: "pubkeyhash", 102 | Usage: "Dump pubkeyhash of the specified public key/certificate", 103 | Aliases: []string{"hash"}, 104 | Flags: []cli.Flag{ 105 | &cli.StringFlag{ 106 | Name: "output", 107 | Aliases: []string{"o"}, 108 | Usage: "Output format. (full, hashonly)", 109 | Value: "full", 110 | }, 111 | &cli.PathFlag{ 112 | Name: "file", 113 | Aliases: []string{"f"}, 114 | Usage: "PEM file containing public key/certificates", 115 | Value: "-", 116 | }, 117 | }, 118 | Action: func(c *cli.Context) error { 119 | env := action.GlobalEnvironment 120 | 121 | ftstr := c.String("output") 122 | ft, err := FormatTypeFromString(ftstr) 123 | if err != nil { 124 | return err 125 | } 126 | 127 | var r io.Reader 128 | 129 | path := c.String("file") 130 | if path == "-" { 131 | r = os.Stdin 132 | } else { 133 | f, err := os.Open(path) 134 | if err != nil { 135 | return err 136 | } 137 | defer f.Close() 138 | 139 | r = f 140 | } 141 | 142 | bs, err := ioutil.ReadAll(r) 143 | if err != nil { 144 | return err 145 | } 146 | 147 | khs, err := ExtractPublicKeyHashesFromPem(bs, env.Logger) 148 | if err != nil { 149 | return err 150 | } 151 | 152 | for _, e := range khs { 153 | if ft.ShouldOutputLabel() { 154 | fmt.Printf("# %s\n", e.Label) 155 | } 156 | fmt.Printf("%s\n", e.Hash) 157 | } 158 | 159 | return nil 160 | }, 161 | } 162 | -------------------------------------------------------------------------------- /cmd/kmgm/tool/tool.go: -------------------------------------------------------------------------------- 1 | package tool 2 | 3 | import ( 4 | "github.com/urfave/cli/v2" 5 | 6 | "github.com/IPA-CyberLab/kmgm/cmd/kmgm/tool/dump" 7 | "github.com/IPA-CyberLab/kmgm/cmd/kmgm/tool/pubkeyhash" 8 | ) 9 | 10 | var Command = &cli.Command{ 11 | Name: "tool", 12 | Usage: "misc tools", 13 | Aliases: []string{"t"}, 14 | Subcommands: []*cli.Command{ 15 | pubkeyhash.Command, 16 | dump.Command, 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /consts/consts.go: -------------------------------------------------------------------------------- 1 | package consts 2 | 3 | import ( 4 | "encoding/asn1" 5 | "time" 6 | ) 7 | 8 | const NodesOutOfSyncThreshold = 1 * time.Minute 9 | const AuthProfileName = ".kmgm_server" 10 | const PrometheusNamespace = "kmgm" 11 | 12 | var ( 13 | OIDExtensionKeyUsage = asn1.ObjectIdentifier{2, 5, 29, 15} 14 | OIDExtensionExtendedKeyUsage = asn1.ObjectIdentifier{2, 5, 29, 37} 15 | OIDExtKeyUsageServerAuth = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 1} 16 | OIDExtKeyUsageClientAuth = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 2} 17 | ) 18 | -------------------------------------------------------------------------------- /dname/protoconv.go: -------------------------------------------------------------------------------- 1 | package dname 2 | 3 | import "github.com/IPA-CyberLab/kmgm/pb" 4 | 5 | func FromProtoStruct(s *pb.DistinguishedName) *Config { 6 | if s == nil { 7 | return &Config{} 8 | } 9 | 10 | return &Config{ 11 | CommonName: s.CommonName, 12 | Organization: s.Organization, 13 | OrganizationalUnit: s.OrganizationalUnit, 14 | Country: s.Country, 15 | Locality: s.Locality, 16 | Province: s.Province, 17 | StreetAddress: s.StreetAddress, 18 | PostalCode: s.PostalCode, 19 | } 20 | } 21 | 22 | func (c *Config) ToProtoStruct() *pb.DistinguishedName { 23 | return &pb.DistinguishedName{ 24 | CommonName: c.CommonName, 25 | Organization: c.Organization, 26 | OrganizationalUnit: c.OrganizationalUnit, 27 | Country: c.Country, 28 | Locality: c.Locality, 29 | Province: c.Province, 30 | StreetAddress: c.StreetAddress, 31 | PostalCode: c.PostalCode, 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /docs/tutorials/nginx/.gitignore: -------------------------------------------------------------------------------- 1 | tls/*.pem 2 | -------------------------------------------------------------------------------- /docs/tutorials/nginx/README.md: -------------------------------------------------------------------------------- 1 | # nginx tutorial 2 | In this tutorial, we setup a [nginx](https://www.nginx.com/) serving HTTPS using a certificate issued by [kmgm](https://github.com/IPA-CyberLab/kmgm). 3 | 4 | ## Prerequisites 5 | - kmgm commandline tool installed. To install kmgm, please refer to the instructions [here](https://github.com/IPA-CyberLab/kmgm/blob/master/README.md#installation). 6 | - kmgm git repository checkout on `/home/example/kmgm/` (Please replace the path with your actual checkout path in the instructions below) 7 | - To focus on kmgm usage, we use a pre-prepared nginx.conf and directory structure in this tutorial. 8 | - Checkout the repository via: `git clone https://github.com/IPA-CyberLab/kmgm` 9 | - docker installation. Instructions [here](https://docs.docker.com/get-docker/). 10 | - We use docker in this tutorial to run nginx instance in a disposable manner. kmgm itself doesn't have any dependency to docker, so you can also run nginx directly on your machine. 11 | 12 | ## Setup a CA 13 | Setup a new CA with the following command: 14 | 15 | ```bash 16 | $ kmgm setup 17 | ``` 18 | 19 | kmgm launches `$EDITOR` to customize the CA settings. In this tutorial, the default settings are enough, so just close the editor. 20 | 21 | ## Generate a private key and issue a certificate 22 | Let's change the working directory so we can save some typing later. 23 | ```bash 24 | $ cd docs/tutorials/nginx/tls 25 | ``` 26 | 27 | Generate a new private key and issue a certificate using the CA setup in the previous section: 28 | 29 | ```bash 30 | $ kmgm issue 31 | ``` 32 | 33 | kmgm will prompt you for the private key file path. Press the return key to proceed with the default, which is a `key.pem` right under the current working directory. kmgm will generate and write a new private key if it doesn't exist. 34 | ``` 35 | ✔ Private key file: /home/example/kmgm/docs/tutorials/nginx/tls/key.pem 36 | ``` 37 | 38 | Next, kmgm will prompt you for the certificate file path. Again, press the return key to proceed with the default. kmgm will issue a fresh new certificate on the specified path, or renew an existing certificate if the file already exists: 39 | ``` 40 | ✔ Certificate pem file: /home/example/kmgm/docs/tutorials/nginx/tls/key.pem 41 | ``` 42 | 43 | Finally, kmgm launches `$EDITOR` to customize the certificate details. In this tutorial, the default settings are enough, so just close the editor, and kmgm will handle the rest: 44 | 45 | ``` 46 | INFO Generating key... {"usage": "", "type": "rsa"} 47 | INFO Generating key... Done. {"usage": "", "type": "rsa", "took": 0.838800879} 48 | INFO Allocated sn: 6362397607487909327 49 | INFO Generating certificate... 50 | INFO Generating certificate... Done. {"took": 0.006699632} 51 | ``` 52 | 53 | After the kmgm command finishes, you should have the pem files populated: 54 | ``` 55 | $ ls -l 56 | total 8 57 | -rw-r--r-- 1 example example 1939 May 25 23:30 cert.pem 58 | -r-------- 1 example example 3243 May 25 23:30 key.pem 59 | ``` 60 | 61 | ## Run nginx 62 | Now that you have a private key and a certificate, let's configure a `nginx.conf` to use them: 63 | ```nginx 64 | events {} 65 | 66 | http { 67 | server { 68 | listen 443 ssl http2; 69 | 70 | ssl_certificate /etc/tls/cert.pem; # replace with the certificate file path 71 | ssl_certificate_key /etc/tls/key.pem; # replace with the key file path 72 | ssl_session_timeout 1d; 73 | ssl_session_cache shared:SSL:10m; 74 | ssl_session_tickets off; 75 | 76 | ssl_protocols TLSv1.3; 77 | ssl_prefer_server_ciphers off; 78 | 79 | root /pub; # replace with the directory which you wish to publish 80 | } 81 | } 82 | ``` 83 | 84 | If you have docker installed, we can run nginx like below: 85 | ```bash 86 | $ cd .. # cd to [checkout]/docs/tutorials/nginx 87 | $ docker run --rm -v `pwd`/tls:/etc/tls:ro -v `pwd`/conf:/etc/nginx:ro -v `pwd`/pub:/pub:ro -p 8443:443 nginx 88 | ``` 89 | 90 | You should be able to test the nginx instance by navigating your web browser to https://[your hostname]:8443/. 91 | 92 | ## CA root 93 | When you navigate to the nginx instance, your browser would warn you that the CA is invalid. 94 | ![Chrome warning][chrome-warning] 95 | 96 | To avoid the warning, we need to import the CA we just setup as a trusted root. To show the CA certificate info, use: 97 | ```bash 98 | $ kmgm show ca 99 | ``` 100 | 101 | To output only pem part, do 102 | ```bash 103 | $ kmgm show -o pem ca 104 | ``` 105 | or, to save the result to a pem file, 106 | ```bash 107 | $ kmgm show -o pem -f ca.pem 108 | ``` 109 | 110 | If you are using Chrome and would like to import the CA certificate, type `chrome://settings/certificates` into the omnibox (URL bar), and click on `Import`. 111 | 112 | To import the certificate on Windows, you might want to name the file with `.crt` extension to be recognized as certificate file. 113 | 114 | 115 | [chrome-warning]: https://raw.githubusercontent.com/IPA-CyberLab/kmgm/master/docs/tutorials/nginx/chrome-warning.png 116 | -------------------------------------------------------------------------------- /docs/tutorials/nginx/chrome-warning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IPA-CyberLab/kmgm/27dbe309ec3d4b83fde1f020762164cf4a8581d0/docs/tutorials/nginx/chrome-warning.png -------------------------------------------------------------------------------- /docs/tutorials/nginx/conf/nginx.conf: -------------------------------------------------------------------------------- 1 | events {} 2 | 3 | http { 4 | server { 5 | listen 443 ssl http2; 6 | 7 | ssl_certificate /etc/tls/cert.pem; 8 | ssl_certificate_key /etc/tls/key.pem; 9 | ssl_session_timeout 1d; 10 | ssl_session_cache shared:SSL:10m; 11 | ssl_session_tickets off; 12 | 13 | ssl_protocols TLSv1.3; 14 | ssl_prefer_server_ciphers off; 15 | 16 | root /pub; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /docs/tutorials/nginx/docker.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | cd $(dirname $0) 3 | docker run --rm -v `pwd`/tls:/etc/tls:ro -v `pwd`/conf:/etc/nginx:ro -v `pwd`/pub:/pub:ro -p 443:443 nginx 4 | -------------------------------------------------------------------------------- /docs/tutorials/nginx/pub/index.html: -------------------------------------------------------------------------------- 1 | 2 | hello 3 | 4 | hello! 5 | -------------------------------------------------------------------------------- /docs/tutorials/nginx/tls/.placeholder: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IPA-CyberLab/kmgm/27dbe309ec3d4b83fde1f020762164cf4a8581d0/docs/tutorials/nginx/tls/.placeholder -------------------------------------------------------------------------------- /domainname/domainname.go: -------------------------------------------------------------------------------- 1 | package domainname 2 | 3 | var MockResult string 4 | 5 | func DNSDomainname() (string, error) { 6 | if MockResult != "" { 7 | return MockResult, nil 8 | } 9 | 10 | return dnsdomainname() 11 | } 12 | -------------------------------------------------------------------------------- /domainname/domainname_posix.go: -------------------------------------------------------------------------------- 1 | // +build linux darwin 2 | 3 | package domainname 4 | 5 | import ( 6 | "fmt" 7 | "net" 8 | "os" 9 | "strings" 10 | ) 11 | 12 | func dnsdomainname() (string, error) { 13 | hostname, err := os.Hostname() 14 | if err != nil { 15 | return "", fmt.Errorf("os.Hostname: %w", err) 16 | } 17 | 18 | addrs, err := net.LookupHost(hostname) 19 | if err != nil { 20 | return "", fmt.Errorf("net.LookupHost(%q): %w", hostname, err) 21 | } 22 | 23 | names, err := net.LookupAddr(addrs[0]) 24 | if err != nil { 25 | return "", fmt.Errorf("net.LookupAddr: %w", err) 26 | } 27 | 28 | for _, e := range names { 29 | // trim trailing '.'s 30 | e := strings.TrimRight(e, ".") 31 | 32 | // trim hostname+'.' 33 | i := strings.Index(e, ".") 34 | if i < 0 { 35 | continue 36 | } 37 | e = e[i+1:] 38 | 39 | return e, nil 40 | } 41 | 42 | return "", fmt.Errorf("FQDN not found from name list %v", names) 43 | } 44 | -------------------------------------------------------------------------------- /domainname/domainname_test.go: -------------------------------------------------------------------------------- 1 | package domainname_test 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/IPA-CyberLab/kmgm/domainname" 8 | ) 9 | 10 | func TestDNSDomainname(t *testing.T) { 11 | dn, err := domainname.DNSDomainname() 12 | if err != nil { 13 | // FIXME[P2]: Some hosts simply doesn't have domainname configured... 14 | // t.Errorf("%v", err) 15 | } 16 | 17 | hn, _ := os.Hostname() 18 | t.Logf("os.Hostname: %s", hn) 19 | t.Logf("dn: %s", dn) 20 | } 21 | -------------------------------------------------------------------------------- /exporter/exporter.go: -------------------------------------------------------------------------------- 1 | package exporter 2 | 3 | import ( 4 | "crypto/x509" 5 | "time" 6 | 7 | "github.com/prometheus/client_golang/prometheus" 8 | "go.uber.org/zap" 9 | 10 | "github.com/IPA-CyberLab/kmgm/consts" 11 | "github.com/IPA-CyberLab/kmgm/storage" 12 | "github.com/IPA-CyberLab/kmgm/storage/issuedb" 13 | ) 14 | 15 | const promSubsystemDB = "issuedb" 16 | 17 | type collector struct { 18 | storage *storage.Storage 19 | logger *zap.Logger 20 | 21 | caStatusDesc *prometheus.Desc 22 | entriesTotalDesc *prometheus.Desc 23 | certExpiresDaysDesc *prometheus.Desc 24 | } 25 | 26 | func NewCollector(storage *storage.Storage, logger *zap.Logger) prometheus.Collector { 27 | return &collector{ 28 | storage: storage, 29 | logger: logger, 30 | 31 | caStatusDesc: prometheus.NewDesc( 32 | prometheus.BuildFQName( 33 | consts.PrometheusNamespace, 34 | "ca", 35 | "status", 36 | ), 37 | "CA status", 38 | []string{"profile", "status"}, 39 | nil, 40 | ), 41 | entriesTotalDesc: prometheus.NewDesc( 42 | prometheus.BuildFQName( 43 | consts.PrometheusNamespace, 44 | promSubsystemDB, 45 | "entries_total", 46 | ), 47 | "Number of entries in the CA issue db with the status", 48 | []string{"profile", "status"}, 49 | nil, 50 | ), 51 | certExpiresDaysDesc: prometheus.NewDesc( 52 | prometheus.BuildFQName( 53 | consts.PrometheusNamespace, 54 | promSubsystemDB, 55 | "expires_days", 56 | ), 57 | "The certificate expires after given days", 58 | []string{"profile", "serialNumber", "subject"}, 59 | nil, 60 | ), 61 | } 62 | } 63 | 64 | var _ = prometheus.Collector(&collector{}) 65 | 66 | // Describe returns all descriptions of the collector. 67 | func (c *collector) Describe(ch chan<- *prometheus.Desc) { 68 | ch <- c.caStatusDesc 69 | ch <- c.entriesTotalDesc 70 | ch <- c.certExpiresDaysDesc 71 | } 72 | 73 | // Collect returns the current state of all metrics of the collector. 74 | func (c *collector) Collect(ch chan<- prometheus.Metric) { 75 | slog := c.logger.Sugar() 76 | now := time.Now() 77 | 78 | ps, err := c.storage.Profiles() 79 | if err != nil { 80 | slog.Warnf("collector: storage.Profiles() failed: %v", err) 81 | return 82 | } 83 | 84 | for _, p := range ps { 85 | profileName := p.Name() 86 | 87 | st := p.Status(now) 88 | for code := storage.CAStatusCode(0); code < storage.MaxCAStatusCode+1; code++ { 89 | eq := float64(0) 90 | if code == st.Code { 91 | eq = 1 92 | } 93 | ch <- prometheus.MustNewConstMetric(c.caStatusDesc, prometheus.GaugeValue, eq, profileName, code.String()) 94 | } 95 | 96 | db, err := issuedb.New(p.IssueDBPath()) 97 | if err != nil { 98 | slog.Warnf("Failed to open issuedb %q: %v", p.IssueDBPath(), err) 99 | continue 100 | } 101 | 102 | entries, err := db.Entries() 103 | if err != nil { 104 | slog.Warnf("collector: storage.Profiles() failed: %v", err) 105 | } 106 | 107 | stateCount := make(map[issuedb.State]int) 108 | farthestNotAfter := make(map[string]*x509.Certificate) 109 | for _, e := range entries { 110 | stateCount[e.State]++ 111 | 112 | if e.State != issuedb.ActiveCertificate { 113 | continue 114 | } 115 | cert, err := e.ParseCertificate() 116 | if err != nil { 117 | continue 118 | } 119 | 120 | subj := cert.Subject.String() 121 | 122 | farthest, ok := farthestNotAfter[subj] 123 | if !ok || farthest.NotAfter.Before(cert.NotAfter) { 124 | farthestNotAfter[subj] = cert 125 | } 126 | } 127 | for state := issuedb.State(0); state < issuedb.MaxState+1; state++ { 128 | count := stateCount[state] 129 | ch <- prometheus.MustNewConstMetric(c.entriesTotalDesc, prometheus.GaugeValue, float64(count), profileName, state.String()) 130 | } 131 | for _, cert := range farthestNotAfter { 132 | expiresDays := cert.NotAfter.Sub(now).Hours() / 24 133 | ch <- prometheus.MustNewConstMetric(c.certExpiresDaysDesc, prometheus.GaugeValue, expiresDays, profileName, cert.SerialNumber.String(), cert.Subject.String()) 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /frontend/editstruct.go: -------------------------------------------------------------------------------- 1 | package frontend 2 | 3 | import ( 4 | "bytes" 5 | "crypto/x509" 6 | "fmt" 7 | "log" 8 | "strings" 9 | "text/template" 10 | 11 | "github.com/IPA-CyberLab/kmgm/keyusage" 12 | "gopkg.in/yaml.v3" 13 | ) 14 | 15 | type templateContext struct { 16 | ErrorString string `yaml:"-"` 17 | Config interface{} `yaml:"config"` 18 | } 19 | 20 | const stripBeforeLine = "# *** LINES ABOVE WILL BE AUTOMATICALLY DELETED ***" 21 | 22 | const configTemplateTextPrologue = ` 23 | {{- define "subject" -}} 24 | # The subject explains name, affiliation, and location of the target computer, 25 | # user, or service the cert is issued against. 26 | subject: 27 | commonName: {{ .CommonName | YamlEscapeString }} 28 | organization: {{ .Organization | YamlEscapeString }} 29 | organizationalUnit: {{ .OrganizationalUnit | YamlEscapeString }} 30 | country: {{ .Country | YamlEscapeString }} 31 | locality: {{ .Locality | YamlEscapeString }} 32 | province: {{ .Province | YamlEscapeString }} 33 | streetAddress: {{ .StreetAddress | YamlEscapeString }} 34 | postalCode: {{ .PostalCode | YamlEscapeString }} 35 | {{- end -}} 36 | {{- with .ErrorString -}} 37 | # Please address the following error: 38 | {{ PrependYamlCommentLiteral . -}} 39 | {{ StripBeforeLine }} 40 | {{- end }} 41 | {{- with .Config -}} 42 | ` 43 | 44 | const configTemplateTextEpilogue = `{{- end }}` 45 | 46 | func PrependYamlCommentLiteral(s string) string { 47 | var b strings.Builder 48 | 49 | for { 50 | ss := strings.SplitN(s, "\n", 2) 51 | b.WriteString("# ") 52 | b.WriteString(ss[0]) 53 | b.WriteRune('\n') 54 | 55 | if len(ss) < 2 { 56 | break 57 | } 58 | s = ss[1] 59 | } 60 | return b.String() 61 | } 62 | 63 | func StripErrorText(s string) string { 64 | lines := strings.Split(s, "\n") 65 | 66 | for i, l := range lines { 67 | if l == stripBeforeLine { 68 | return strings.Join(lines[i+1:], "\n") 69 | } 70 | } 71 | return s 72 | } 73 | 74 | func YamlEscapeString(s string) string { 75 | bs, err := yaml.Marshal(s) 76 | if err != nil { 77 | log.Panicf("Failed to yaml.Marshal string %q: %v", s, err) 78 | } 79 | 80 | // omit last \n 81 | bs = bs[:len(bs)-1] 82 | 83 | return string(bs) 84 | } 85 | 86 | func CallVerifyMethod(cfgI interface{}) error { 87 | cfg := cfgI.(interface { 88 | Verify() error 89 | }) 90 | if err := cfg.Verify(); err != nil { 91 | return err 92 | } 93 | return nil 94 | } 95 | 96 | func makeTemplate(tmplstr string) (*template.Template, error) { 97 | tmplstrFull := configTemplateTextPrologue + tmplstr + configTemplateTextEpilogue 98 | tmpl, err := 99 | template.New("setupconfig"). 100 | Funcs(template.FuncMap{ 101 | "PrependYamlCommentLiteral": PrependYamlCommentLiteral, 102 | "YamlEscapeString": YamlEscapeString, 103 | "StripBeforeLine": func() string { return stripBeforeLine }, 104 | "CommentOutIfFalse": func(e bool) string { 105 | if e { 106 | return "" 107 | } 108 | return "# " 109 | }, 110 | "TestKeyUsageBit": func(bitName string, ku x509.KeyUsage) bool { 111 | bit, err := keyusage.KeyUsageFromString(bitName) 112 | if err != nil { 113 | panic(err) 114 | } 115 | 116 | return (ku & bit) != 0 117 | }, 118 | "HasExtKeyUsage": func(ekuName string, ekus []x509.ExtKeyUsage) bool { 119 | eku, err := keyusage.ExtKeyUsageFromString(ekuName) 120 | if err != nil { 121 | panic(err) 122 | } 123 | for _, e := range ekus { 124 | if e == eku { 125 | return true 126 | } 127 | } 128 | 129 | return false 130 | }, 131 | }). 132 | Parse(tmplstrFull) 133 | if err != nil { 134 | return nil, err 135 | } 136 | 137 | return tmpl, nil 138 | } 139 | 140 | func DumpTemplate(tmplstr string, cfg interface{}) error { 141 | tmpl, err := makeTemplate(tmplstr) 142 | if err != nil { 143 | return err 144 | } 145 | 146 | var buf bytes.Buffer 147 | if err := tmpl.Execute(&buf, templateContext{Config: cfg}); err != nil { 148 | return err 149 | } 150 | buf.WriteString(` 151 | # noDefault prevents kmgm from assigning default values to unspecified fields. 152 | # Setting "noDefault: true" is recommended for non-interactive invocations to 153 | # avoid unintended behavior. 154 | noDefault: true 155 | `) 156 | 157 | if _, err := fmt.Print(buf.String()); err != nil { 158 | return err 159 | } 160 | return nil 161 | } 162 | 163 | func EditStructWithVerifier(fe Frontend, tmplstr string, cfg interface{}, VerifyCfg func(cfg interface{}) error) error { 164 | tmpl, err := makeTemplate(tmplstr) 165 | if err != nil { 166 | return err 167 | } 168 | 169 | tctx := templateContext{Config: cfg} 170 | 171 | var buf bytes.Buffer 172 | if err := tmpl.Execute(&buf, tctx); err != nil { 173 | return err 174 | } 175 | cfgtxt := buf.String() 176 | 177 | VerifyText := func(src string) (string, error) { 178 | r := bytes.NewBuffer([]byte(src)) 179 | 180 | d := yaml.NewDecoder(r) 181 | d.KnownFields(true) 182 | 183 | if err := d.Decode(tctx.Config); err != nil { 184 | // yaml error means that we can't use the template (we will lose the full data) 185 | txtwerr := strings.Join([]string{ 186 | "# Please correct syntax error:\n", 187 | PrependYamlCommentLiteral(err.Error()), 188 | stripBeforeLine, "\n", 189 | StripErrorText(src)}, 190 | "") 191 | return txtwerr, err 192 | } 193 | 194 | if err := VerifyCfg(tctx.Config); err != nil { 195 | tctx.ErrorString = err.Error() 196 | 197 | var buf bytes.Buffer 198 | if err := tmpl.Execute(&buf, tctx); err != nil { 199 | // FIXME[P3]: How should we handle template exec error? 200 | panic(err) 201 | } 202 | return buf.String(), err 203 | } 204 | 205 | return src, nil 206 | } 207 | 208 | if _, err := fe.EditText(cfgtxt, VerifyText); err != nil { 209 | return err 210 | } 211 | 212 | return nil 213 | } 214 | -------------------------------------------------------------------------------- /frontend/editstruct_test.go: -------------------------------------------------------------------------------- 1 | package frontend_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/IPA-CyberLab/kmgm/frontend" 7 | ) 8 | 9 | func TestStripErrorText(t *testing.T) { 10 | testcases := []struct { 11 | Input string 12 | Expected string 13 | }{ 14 | {"abcd", "abcd"}, 15 | {"", ""}, 16 | {`before 17 | # *** LINES ABOVE WILL BE AUTOMATICALLY DELETED *** 18 | after`, 19 | "after"}, 20 | {`before 21 | before2 22 | # *** LINES ABOVE WILL BE AUTOMATICALLY DELETED *** 23 | after 24 | after2`, 25 | "after\nafter2"}, 26 | {`before 27 | # *** LINES ABOVE WILL BE AUTOMATICALLY DELETED ***`, 28 | ""}, 29 | } 30 | for _, tc := range testcases { 31 | actual := frontend.StripErrorText(tc.Input) 32 | if actual != tc.Expected { 33 | t.Errorf("Expected: %q Actual: %q", tc.Expected, actual) 34 | } 35 | } 36 | } 37 | 38 | func TestYamlEscapeString(t *testing.T) { 39 | testcases := []struct { 40 | Input string 41 | Expected string 42 | }{ 43 | {"abcd", "abcd"}, 44 | {"こんにちは", "こんにちは"}, 45 | {"💯", `"\U0001F4AF"`}, 46 | {"", `""`}, 47 | {"\t ", `"\t "`}, 48 | } 49 | for _, tc := range testcases { 50 | actual := frontend.YamlEscapeString(tc.Input) 51 | if actual != tc.Expected { 52 | t.Errorf("Expected: %q Actual: %q", tc.Expected, actual) 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /frontend/frontend.go: -------------------------------------------------------------------------------- 1 | package frontend 2 | 3 | type ConfigItem struct { 4 | Label string 5 | Validate func(string) error 6 | Value *string 7 | } 8 | 9 | type Frontend interface { 10 | Confirm(question string) error 11 | IsInteractive() bool 12 | EditText(template string, validator func(string) (string, error)) (edited string, err error) 13 | Configure([]ConfigItem) error 14 | } 15 | -------------------------------------------------------------------------------- /frontend/noninteractive.go: -------------------------------------------------------------------------------- 1 | package frontend 2 | 3 | import ( 4 | "fmt" 5 | 6 | "go.uber.org/zap" 7 | ) 8 | 9 | type NonInteractive struct { 10 | Logger *zap.Logger 11 | } 12 | 13 | var _ = Frontend(&NonInteractive{}) 14 | 15 | func (fe *NonInteractive) Confirm(q string) error { 16 | slog := fe.Logger.Sugar() 17 | slog.Infof("%q -> yes [noninteractive]", q) 18 | return nil 19 | } 20 | 21 | func (fe *NonInteractive) IsInteractive() bool { return false } 22 | 23 | func (fe *NonInteractive) EditText(beforeEdit string, validator func(string) (string, error)) (string, error) { 24 | slog := fe.Logger.Sugar() 25 | 26 | txt, err := validator(beforeEdit) 27 | if err != nil { 28 | slog.Debugf("[noninteractive] Parsed input was:\n%s", txt) 29 | return txt, fmt.Errorf("Validate input failed: %w", err) 30 | } 31 | 32 | slog.Infof("[noninteractive] proceeding with:\n%s", txt) 33 | 34 | return txt, nil 35 | } 36 | 37 | func (fe *NonInteractive) Configure(items []ConfigItem) error { 38 | slog := fe.Logger.Sugar() 39 | for _, i := range items { 40 | slog.Infof("[noninteractive] %s = %q.", i.Label, *i.Value) 41 | } 42 | 43 | return nil 44 | } 45 | -------------------------------------------------------------------------------- /frontend/promptuife/frontend.go: -------------------------------------------------------------------------------- 1 | package promptuife 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io/ioutil" 7 | "os" 8 | "os/exec" 9 | "os/user" 10 | "path/filepath" 11 | "strings" 12 | 13 | "github.com/manifoldco/promptui" 14 | 15 | "github.com/IPA-CyberLab/kmgm/frontend" 16 | ) 17 | 18 | var PromptTemplate = &promptui.PromptTemplates{ 19 | Success: `{{ . | bold }}{{ ":" | bold }} `, 20 | } 21 | 22 | type Frontend struct{} 23 | 24 | var _ = frontend.Frontend(Frontend{}) 25 | 26 | func (Frontend) Confirm(q string) error { 27 | // promptui automatically append '?' at the end of |IsConfirm| |Label|. 28 | // Avoid printing double '?'s at the end. 29 | q = strings.TrimRight(q, "?") 30 | 31 | p := promptui.Prompt{ 32 | Label: q, 33 | IsConfirm: true, 34 | Default: "y", 35 | Templates: PromptTemplate, 36 | } 37 | if _, err := p.Run(); err != nil { 38 | return err 39 | } 40 | return nil 41 | } 42 | 43 | var ErrAbortEdit = errors.New("frontend: User declined to correct error") 44 | 45 | func stripTrailingWhitespace(s string) string { 46 | return strings.TrimRight(s, " \n") 47 | } 48 | 49 | func (fe Frontend) IsInteractive() bool { return true } 50 | 51 | func (fe Frontend) EditText(beforeEdit string, validator func(string) (string, error)) (string, error) { 52 | u, err := user.Current() 53 | if err != nil { 54 | return "", fmt.Errorf("user.Current: %w", err) 55 | } 56 | 57 | // Use /tmp if root? 58 | tmppath := filepath.Join(u.HomeDir, ".kmgm_input.yaml") 59 | 60 | editor := os.Getenv("EDITOR") 61 | if editor == "" { 62 | editor = "nano" 63 | } 64 | 65 | for { 66 | if err := ioutil.WriteFile(tmppath, []byte(beforeEdit), 0600); err != nil { 67 | return "", fmt.Errorf("Failed to write to tmpfile %q: %w", tmppath, err) 68 | } 69 | 70 | c := exec.Command(editor, tmppath) 71 | c.Stdin = os.Stdin 72 | c.Stdout = os.Stdout 73 | c.Stderr = os.Stderr 74 | if err := c.Run(); err != nil { 75 | return "", fmt.Errorf("Failed to run editor %q: %w", editor, err) 76 | } 77 | 78 | bs, err := ioutil.ReadFile(tmppath) 79 | if err != nil { 80 | return "", fmt.Errorf("Failed to read afterEdit result %q: %w", tmppath, err) 81 | } 82 | afterEdit := string(bs) 83 | 84 | suggestion, err := validator(afterEdit) 85 | if err == nil { 86 | break 87 | } 88 | fmt.Printf("Invalid entry: %v\n", err) 89 | if stripTrailingWhitespace(afterEdit) == stripTrailingWhitespace(beforeEdit) { 90 | if err := fe.Confirm("Continue edit"); err != nil { 91 | return "", ErrAbortEdit 92 | } 93 | } 94 | 95 | beforeEdit = suggestion 96 | } 97 | 98 | fmt.Println("Validator passed :)") 99 | return beforeEdit, nil 100 | } 101 | 102 | func myBlockCursor(input []rune) []rune { 103 | return []rune(fmt.Sprintf("\033[7m%s\033[0m", string(input))) 104 | } 105 | 106 | func (fe Frontend) Configure(items []frontend.ConfigItem) error { 107 | for _, i := range items { 108 | var err error 109 | p := promptui.Prompt{ 110 | Label: i.Label, 111 | Validate: i.Validate, 112 | Default: *i.Value, 113 | Templates: PromptTemplate, 114 | AllowEdit: true, 115 | Pointer: myBlockCursor, 116 | } 117 | *i.Value, err = p.Run() 118 | if err != nil { 119 | return fmt.Errorf("prompt %s failed: %w", i.Label, err) 120 | } 121 | } 122 | return nil 123 | } 124 | -------------------------------------------------------------------------------- /frontend/validate/path.go: -------------------------------------------------------------------------------- 1 | package validate 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "path/filepath" 8 | ) 9 | 10 | func Dir(s string) error { 11 | s = filepath.Clean(s) 12 | for s != "." { 13 | fi, err := os.Stat(s) 14 | if err != nil { 15 | if os.IsNotExist(err) { 16 | s = filepath.Dir(s) 17 | continue 18 | } 19 | return fmt.Errorf("os.Stat(%q): %w", s, err) 20 | } 21 | if !fi.IsDir() { 22 | return fmt.Errorf("%q is not a dir.", s) 23 | } 24 | 25 | return nil 26 | } 27 | 28 | return nil 29 | } 30 | 31 | func File(s string) error { 32 | s = filepath.Clean(s) 33 | 34 | fi, err := os.Stat(s) 35 | if err != nil && !os.IsNotExist(err) { 36 | return fmt.Errorf("os.Stat(%q): %w", s, err) 37 | } 38 | if err == nil { 39 | if fi.IsDir() { 40 | return fmt.Errorf("New file dest %q is a dir.", s) 41 | } 42 | 43 | // File exists. 44 | return nil 45 | } 46 | 47 | // New File. Check that the parent dir is potentially valid. 48 | if err := Dir(filepath.Dir(s)); err != nil { 49 | return err 50 | } 51 | 52 | return nil 53 | } 54 | 55 | // NewFile checks that a file doesn't exist at the specified path and is not a dir. 56 | func NewFile(s string) error { 57 | s = filepath.Clean(s) 58 | 59 | fi, err := os.Stat(s) 60 | if err != nil && !os.IsNotExist(err) { 61 | return fmt.Errorf("os.Stat(%q): %w", s, err) 62 | } 63 | if err == nil { 64 | if fi.IsDir() { 65 | return fmt.Errorf("New file dest %q is a dir.", s) 66 | } 67 | return fmt.Errorf("File %q already exists.", s) 68 | } 69 | 70 | if err := Dir(s); err != nil { 71 | return err 72 | } 73 | 74 | return nil 75 | } 76 | 77 | var placeholderBytes = []byte("placeholder for checking if the file is writable\n") 78 | 79 | func MkdirAndCheckWritable(p string) error { 80 | _, err := os.Stat(p) 81 | if err == nil { 82 | return fmt.Errorf("%w: %q", os.ErrExist, p) 83 | } 84 | if err != nil && !os.IsNotExist(err) { 85 | return fmt.Errorf("os.Stat(%q): %w", p, err) 86 | } 87 | 88 | dirp := filepath.Dir(p) 89 | if err := os.MkdirAll(dirp, 0755); err != nil { 90 | return fmt.Errorf("os.MkdirAll(%q): %w", dirp, err) 91 | } 92 | if err := ioutil.WriteFile(p, placeholderBytes, 0400); err != nil { 93 | return fmt.Errorf("Failed to write to file %q: %w", p, err) 94 | } 95 | if err := os.Remove(p); err != nil { 96 | return fmt.Errorf("Failed to remove placeholder file %q: %w", p, err) 97 | } 98 | 99 | return nil 100 | } 101 | -------------------------------------------------------------------------------- /frontend/validate/pkixelement.go: -------------------------------------------------------------------------------- 1 | package validate 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | /* 8 | [X.520] ITU-T Recommendation X.520 (2005) | ISO/IEC 9594-6:2005, 9 | Information technology - Open Systems Interconnection - 10 | The Directory: Selected attribute types. 11 | */ 12 | 13 | func PKIXElement(ub int) func(s string) error { 14 | return func(s string) error { 15 | if len(s) > ub { 16 | return fmt.Errorf("string longer than its allowed length %d.", ub) 17 | } 18 | for _, r := range s { 19 | // FIXME[P4]: make more strict 20 | if r > 0x7f { 21 | return fmt.Errorf("Rune '%c' is not allowed.", r) 22 | } 23 | } 24 | return nil 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /gen.go: -------------------------------------------------------------------------------- 1 | //go:generate buf generate 2 | package pb 3 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/IPA-CyberLab/kmgm 2 | 3 | go 1.21.0 4 | 5 | toolchain go1.23.1 6 | 7 | require ( 8 | github.com/gofrs/flock v0.12.1 9 | github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 10 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 11 | github.com/manifoldco/promptui v0.9.0 12 | github.com/mattn/go-isatty v0.0.20 13 | github.com/prometheus/client_golang v1.20.5 14 | github.com/urfave/cli/v2 v2.27.5 15 | go.uber.org/multierr v1.11.0 16 | go.uber.org/zap v1.27.0 17 | golang.org/x/net v0.30.0 18 | golang.org/x/oauth2 v0.23.0 19 | golang.org/x/sys v0.26.0 20 | google.golang.org/grpc v1.67.1 21 | google.golang.org/protobuf v1.35.1 22 | gopkg.in/yaml.v3 v3.0.1 23 | ) 24 | 25 | require ( 26 | cloud.google.com/go/compute v1.28.1 // indirect 27 | cloud.google.com/go/compute/metadata v0.5.2 // indirect 28 | github.com/beorn7/perks v1.0.1 // indirect 29 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 30 | github.com/chzyer/readline v1.5.1 // indirect 31 | github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect 32 | github.com/golang/protobuf v1.5.4 // indirect 33 | github.com/klauspost/compress v1.17.11 // indirect 34 | github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect 35 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 36 | github.com/prometheus/client_model v0.6.1 // indirect 37 | github.com/prometheus/common v0.60.1 // indirect 38 | github.com/prometheus/procfs v0.15.1 // indirect 39 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 40 | github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect 41 | golang.org/x/text v0.19.0 // indirect 42 | google.golang.org/appengine v1.6.8 // indirect 43 | google.golang.org/genproto/googleapis/rpc v0.0.0-20241021214115-324edc3d5d38 // indirect 44 | ) 45 | -------------------------------------------------------------------------------- /httperr/error.go: -------------------------------------------------------------------------------- 1 | package httperr 2 | 3 | type ErrorWithStatusCode struct { 4 | StatusCode int 5 | Err error 6 | } 7 | 8 | func (e ErrorWithStatusCode) Error() string { 9 | return e.Err.Error() 10 | } 11 | 12 | func (e ErrorWithStatusCode) GetStatusCode() int { 13 | return e.StatusCode 14 | } 15 | 16 | func (e ErrorWithStatusCode) Unwrap() error { 17 | return e.Err 18 | } 19 | 20 | func StatusCodeFromError(e interface{}) int { 21 | if statusCoder, ok := e.(interface { 22 | GetStatusCode() int 23 | }); ok { 24 | return statusCoder.GetStatusCode() 25 | } 26 | 27 | return 500 28 | } 29 | -------------------------------------------------------------------------------- /ipapi/ipapi.go: -------------------------------------------------------------------------------- 1 | package ipapi 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "io/ioutil" 8 | "net/http" 9 | 10 | "go.uber.org/zap" 11 | ) 12 | 13 | type Result struct { 14 | As string `json:"as"` 15 | City string `json:"city"` 16 | Country string `json:"country"` 17 | CountryCode string `json:"countryCode"` 18 | Isp string `json:"isp"` 19 | Lat float64 `json:"lat"` 20 | Lon float64 `json:"lon"` 21 | Org string `json:"org"` 22 | Query string `json:"query"` 23 | Region string `json:"region"` 24 | RegionName string `json:"regionName"` 25 | TimeZone string `json:"timezone"` 26 | Zip string `json:"zip"` 27 | } 28 | 29 | const Endpoint = "http://ip-api.com/json" 30 | 31 | var EnableQuery bool = true 32 | var ErrQueryDisabled = errors.New("ipapi: Query disabled by user.") 33 | var MockResult *Result = nil 34 | 35 | func Query() (*Result, error) { 36 | if !EnableQuery { 37 | return nil, ErrQueryDisabled 38 | } 39 | 40 | if MockResult != nil { 41 | return MockResult, nil 42 | } 43 | 44 | resp, err := http.Get(Endpoint) 45 | if err != nil { 46 | return nil, fmt.Errorf("http.Get: %w", err) 47 | } 48 | defer resp.Body.Close() 49 | 50 | if resp.StatusCode != http.StatusOK { 51 | return nil, fmt.Errorf("non 200 response status: %s", resp.Status) 52 | } 53 | 54 | bs, err := ioutil.ReadAll(resp.Body) 55 | if err != nil { 56 | return nil, fmt.Errorf("ioutil.ReadAll(resp.Body): %w", err) 57 | } 58 | 59 | var result Result 60 | if err := json.Unmarshal(bs, &result); err != nil { 61 | return nil, fmt.Errorf("json.Unmarshal: %w", err) 62 | } 63 | return &result, nil 64 | } 65 | 66 | func tryReadCache(cachePath string) (*Result, error) { 67 | bs, err := ioutil.ReadFile(cachePath) 68 | if err != nil { 69 | return nil, err 70 | } 71 | 72 | var result Result 73 | if err := json.Unmarshal(bs, &result); err != nil { 74 | return nil, fmt.Errorf("json.Unmarshal: %w", err) 75 | } 76 | return &result, nil 77 | } 78 | 79 | func QueryCached(cachePath string, l *zap.Logger) (*Result, error) { 80 | s := l.Sugar() 81 | 82 | if !EnableQuery { 83 | return nil, ErrQueryDisabled 84 | } 85 | 86 | result, err := tryReadCache(cachePath) 87 | if err == nil { 88 | s.Debugf("Using cached geoip query result read from %q", cachePath) 89 | return result, nil 90 | } 91 | 92 | result, err = Query() 93 | if err != nil { 94 | return result, err 95 | } 96 | 97 | bs, err := json.Marshal(result) 98 | if err != nil { 99 | s.Infof("Failed to marshal geoip cache content: %v", err) 100 | return result, err 101 | } 102 | 103 | if err := ioutil.WriteFile(cachePath, bs, 0644); err != nil { 104 | s.Infof("Failed to write geoip cache file %q: %v", cachePath, err) 105 | return result, err 106 | } 107 | 108 | return result, nil 109 | } 110 | -------------------------------------------------------------------------------- /ipapi/ipapi_test.go: -------------------------------------------------------------------------------- 1 | package ipapi_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/IPA-CyberLab/kmgm/ipapi" 7 | ) 8 | 9 | func TestQuery(t *testing.T) { 10 | r, err := ipapi.Query() 11 | if err != nil { 12 | t.Fatalf("ipapi.Query: %v", err) 13 | } 14 | 15 | t.Logf("%+v", r) 16 | } 17 | -------------------------------------------------------------------------------- /keyusage/keyusage.go: -------------------------------------------------------------------------------- 1 | package keyusage 2 | 3 | import ( 4 | "crypto/x509" 5 | "errors" 6 | "fmt" 7 | "sort" 8 | 9 | "github.com/IPA-CyberLab/kmgm/pb" 10 | ) 11 | 12 | type KeyUsage struct { 13 | KeyUsage x509.KeyUsage 14 | ExtKeyUsages []x509.ExtKeyUsage 15 | } 16 | 17 | var KeyUsageCA = KeyUsage{ 18 | KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, 19 | // https://tools.ietf.org/html/rfc5280#section-4.2.1.12 20 | // "In general, this extension will appear only in end entity certificates." 21 | ExtKeyUsages: nil, 22 | } 23 | 24 | var KeyUsageTLSServer = KeyUsage{ 25 | KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, 26 | ExtKeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, 27 | } 28 | 29 | var KeyUsageTLSClient = KeyUsage{ 30 | KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, 31 | ExtKeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, 32 | } 33 | 34 | var KeyUsageTLSClientServer = KeyUsage{ 35 | KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, 36 | ExtKeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, 37 | } 38 | 39 | func (u KeyUsage) Clone() KeyUsage { 40 | return KeyUsage{ 41 | KeyUsage: u.KeyUsage, 42 | ExtKeyUsages: append([]x509.ExtKeyUsage{}, u.ExtKeyUsages...), 43 | } 44 | } 45 | 46 | func (u KeyUsage) Verify() error { 47 | if int(u.KeyUsage) == 0 { 48 | return errors.New("KeyUsage is empty.") 49 | } 50 | 51 | // FIXME[P2]: Implement https://tools.ietf.org/html/rfc5280#section-4.2.1.3 52 | 53 | return nil 54 | } 55 | 56 | type yamlKeyUsage struct { 57 | KeyUsage []string `yaml:"keyUsage"` 58 | ExtKeyUsage []string `yaml:"extKeyUsage"` 59 | Preset string `yaml:"preset"` 60 | } 61 | 62 | func PresetFromString(s string) (KeyUsage, error) { 63 | if s == "tlsServer" { 64 | return KeyUsageTLSServer.Clone(), nil 65 | } else if s == "tlsClient" { 66 | return KeyUsageTLSClient.Clone(), nil 67 | } else if s == "tlsClientServer" { 68 | return KeyUsageTLSClientServer.Clone(), nil 69 | } else { 70 | return KeyUsage{}, fmt.Errorf("Unknown preset %q specified", s) 71 | } 72 | } 73 | 74 | func KeyUsageFromString(bitName string) (x509.KeyUsage, error) { 75 | // FIXME[P2]: Support more 76 | switch bitName { 77 | case "keyEncipherment": 78 | return x509.KeyUsageKeyEncipherment, nil 79 | case "digitalSignature": 80 | return x509.KeyUsageDigitalSignature, nil 81 | default: 82 | return x509.KeyUsage(0), fmt.Errorf("unknown bitName %q", bitName) 83 | } 84 | } 85 | 86 | func ExtKeyUsageFromString(ekuName string) (x509.ExtKeyUsage, error) { 87 | // FIXME[P2]: Support more 88 | switch ekuName { 89 | case "any": 90 | return x509.ExtKeyUsageAny, nil 91 | case "clientAuth": 92 | return x509.ExtKeyUsageClientAuth, nil 93 | case "serverAuth": 94 | return x509.ExtKeyUsageServerAuth, nil 95 | default: 96 | return x509.ExtKeyUsage(0), fmt.Errorf("unknown ekuName %q", ekuName) 97 | } 98 | } 99 | 100 | func (u *KeyUsage) UnmarshalYAML(unmarshal func(interface{}) error) error { 101 | var yku yamlKeyUsage 102 | if err := unmarshal(&yku); err != nil { 103 | return err 104 | } 105 | 106 | if yku.Preset != "" { 107 | if len(yku.KeyUsage) != 0 { 108 | return errors.New("preset and keyUsage is not allowed to be specified at once.") 109 | } 110 | if len(yku.ExtKeyUsage) != 0 { 111 | return errors.New("preset and extKeyUsage is not allowed to be specified at once.") 112 | } 113 | 114 | var err error 115 | *u, err = PresetFromString(yku.Preset) 116 | if err != nil { 117 | return err 118 | } 119 | return nil 120 | } 121 | 122 | u.KeyUsage = x509.KeyUsage(0) 123 | for _, ku := range yku.KeyUsage { 124 | bit, err := KeyUsageFromString(ku) 125 | if err != nil { 126 | return err 127 | } 128 | u.KeyUsage |= bit 129 | } 130 | 131 | foundAny := false 132 | u.ExtKeyUsages = []x509.ExtKeyUsage{} 133 | for _, ekustr := range yku.ExtKeyUsage { 134 | eku, err := ExtKeyUsageFromString(ekustr) 135 | if err != nil { 136 | return err 137 | } 138 | 139 | u.ExtKeyUsages = append(u.ExtKeyUsages, eku) 140 | if eku == x509.ExtKeyUsageAny { 141 | foundAny = true 142 | } 143 | } 144 | if foundAny && len(u.ExtKeyUsages) > 1 { 145 | return fmt.Errorf("extKeyUsage \"any\" and other extKeyUsages cannot be specified at once.") 146 | } 147 | 148 | return nil 149 | } 150 | 151 | func FromProtoStruct(s *pb.KeyUsage) KeyUsage { 152 | if s == nil { 153 | return KeyUsage{} 154 | } 155 | 156 | ekus := make([]x509.ExtKeyUsage, 0, len(s.ExtKeyUsages)) 157 | for _, ekuint := range s.ExtKeyUsages { 158 | ekus = append(ekus, x509.ExtKeyUsage(ekuint)) 159 | } 160 | 161 | return KeyUsage{ 162 | KeyUsage: x509.KeyUsage(s.KeyUsage), 163 | ExtKeyUsages: ekus, 164 | } 165 | } 166 | 167 | func (u KeyUsage) ToProtoStruct() *pb.KeyUsage { 168 | ekuints := make([]uint32, 0, len(u.ExtKeyUsages)) 169 | for _, eku := range u.ExtKeyUsages { 170 | ekuints = append(ekuints, uint32(eku)) 171 | } 172 | 173 | return &pb.KeyUsage{ 174 | KeyUsage: uint32(u.KeyUsage), 175 | ExtKeyUsages: ekuints, 176 | } 177 | } 178 | 179 | func FromCertificate(cert *x509.Certificate) KeyUsage { 180 | return KeyUsage{ 181 | KeyUsage: cert.KeyUsage, 182 | ExtKeyUsages: cert.ExtKeyUsage, 183 | } 184 | } 185 | 186 | func (p *KeyUsage) UnmarshalFlag(s string) error { 187 | ku, err := PresetFromString(s) 188 | if err != nil { 189 | return err 190 | } 191 | 192 | *p = ku 193 | return nil 194 | } 195 | 196 | func (a KeyUsage) Equals(b KeyUsage) bool { 197 | if a.KeyUsage != b.KeyUsage { 198 | return false 199 | } 200 | if len(a.ExtKeyUsages) != len(b.ExtKeyUsages) { 201 | return false 202 | } 203 | 204 | ekua := append([]x509.ExtKeyUsage{}, a.ExtKeyUsages...) 205 | ekub := append([]x509.ExtKeyUsage{}, b.ExtKeyUsages...) 206 | sort.Slice(ekua, func(i, j int) bool { return ekua[i] < ekua[j] }) 207 | sort.Slice(ekub, func(i, j int) bool { return ekub[i] < ekub[j] }) 208 | for i := range ekua { 209 | if ekua[i] != ekub[i] { 210 | return false 211 | } 212 | } 213 | 214 | return true 215 | } 216 | 217 | func (ku KeyUsage) Preset() string { 218 | if ku.Equals(KeyUsageTLSServer) { 219 | return "tlsServer" 220 | } 221 | if ku.Equals(KeyUsageTLSClient) { 222 | return "tlsClient" 223 | } 224 | if ku.Equals(KeyUsageTLSClientServer) { 225 | return "tlsClientServer" 226 | } 227 | return "custom" 228 | } 229 | -------------------------------------------------------------------------------- /keyusage/keyusage_test.go: -------------------------------------------------------------------------------- 1 | package keyusage_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/IPA-CyberLab/kmgm/keyusage" 7 | ) 8 | 9 | func TestKeyUsage_Equals(t *testing.T) { 10 | if keyusage.KeyUsageCA.Equals(keyusage.KeyUsageTLSServer) { 11 | t.Errorf("Unexpected: CA == TlsServer") 12 | } 13 | if keyusage.KeyUsageTLSClientServer.Equals(keyusage.KeyUsageTLSServer) { 14 | t.Errorf("Unexpected: cs == s") 15 | } 16 | if keyusage.KeyUsageTLSClient.Equals(keyusage.KeyUsageTLSServer) { 17 | t.Errorf("Unexpected: c == s") 18 | } 19 | if !keyusage.KeyUsageCA.Equals(keyusage.KeyUsageCA) { 20 | t.Errorf("Unexpected: CA != CA") 21 | } 22 | if !keyusage.KeyUsageTLSClientServer.Equals(keyusage.KeyUsageTLSClientServer) { 23 | t.Errorf("Unexpected: cs != cs") 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /keyusage/x509.go: -------------------------------------------------------------------------------- 1 | package keyusage 2 | 3 | import ( 4 | "crypto/x509" 5 | "encoding/asn1" 6 | "fmt" 7 | 8 | "github.com/IPA-CyberLab/kmgm/consts" 9 | ) 10 | 11 | func FromCSR(req *x509.CertificateRequest) (KeyUsage, error) { 12 | var ku x509.KeyUsage 13 | var ekus []x509.ExtKeyUsage 14 | 15 | for _, e := range req.Extensions { 16 | if e.Id.Equal(consts.OIDExtensionKeyUsage) { 17 | var bstr asn1.BitString 18 | if _, err := asn1.Unmarshal(e.Value, &bstr); err != nil { 19 | return KeyUsage{}, fmt.Errorf("Failed to ans1.Unmarshal keyUsage extension value: %w", err) 20 | } 21 | 22 | x := 0 23 | for i := bstr.BitLength - 1; i > -1; i-- { 24 | x = x << 1 25 | x = x | bstr.At(i) 26 | } 27 | ku = x509.KeyUsage(x) 28 | } else if e.Id.Equal(consts.OIDExtensionExtendedKeyUsage) { 29 | var oids []asn1.ObjectIdentifier 30 | if _, err := asn1.Unmarshal(e.Value, &oids); err != nil { 31 | return KeyUsage{}, fmt.Errorf("Failed to ans1.Unmarshal extendedKeyUsage extension value: %w", err) 32 | } 33 | 34 | for _, oid := range oids { 35 | if oid.Equal(consts.OIDExtKeyUsageClientAuth) { 36 | ekus = append(ekus, x509.ExtKeyUsageClientAuth) 37 | } else if oid.Equal(consts.OIDExtKeyUsageServerAuth) { 38 | ekus = append(ekus, x509.ExtKeyUsageServerAuth) 39 | } else { 40 | return KeyUsage{}, fmt.Errorf("Unsupported extendedKeyUsage oid: %v", oid) 41 | } 42 | } 43 | } 44 | } 45 | 46 | return KeyUsage{ 47 | KeyUsage: ku, 48 | ExtKeyUsages: ekus, 49 | }, nil 50 | } 51 | -------------------------------------------------------------------------------- /keyusage/x509_test.go: -------------------------------------------------------------------------------- 1 | package keyusage_test 2 | 3 | import ( 4 | "crypto/x509" 5 | "testing" 6 | 7 | "github.com/IPA-CyberLab/kmgm/keyusage" 8 | "github.com/IPA-CyberLab/kmgm/pemparser" 9 | ) 10 | 11 | func TestFromCSR(t *testing.T) { 12 | testcases := []struct { 13 | Comment string 14 | PEM string 15 | Expected keyusage.KeyUsage 16 | }{ 17 | {"no usage", 18 | /* 19 | apiVersion: cert-manager.io/v1 20 | kind: Certificate 21 | metadata: 22 | name: kmgm-test-cert 23 | spec: 24 | secretName: kmgm-test-cert 25 | dnsNames: 26 | - bar.example 27 | issuerRef: 28 | name: kmgm-test-issuer 29 | kind: Issuer 30 | group: kmgm-issuer.coe.ad.jp 31 | privateKey: 32 | size: 4096 33 | */ 34 | `-----BEGIN CERTIFICATE REQUEST----- 35 | MIIEezCCAmMCAQAwADCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMLw 36 | yO/DJ4tl9ly8BjhSg86nAAfQX/B1YXStwLlkw1phF05VY/dyYfKQOpPsaqprQ3m7 37 | jeCxUS0FFCPrY6d8NCu9WQBzWKBTn0RNVtOCw6PN+C1tRBQbu4nbAJO/WmhrA2T6 38 | xPuNglBI+8Z3Bn5RnlQJeh5cxUdDRL3mURR4/5iK62d4wQF46KfAWN0BIsl2DxMt 39 | 0Ea5t4n+GC4YRu9U/ITc7PvLaip5fhmb+YeYyC+24CF55sjoYUL3+W6f6kbcjGx1 40 | gKP4qZFJ8OiaIuqaKJRqSKmahFLbOZrY+Dq/yKXjK4DICoLzs/lWY04xhvdNTTdX 41 | 9FohVf1F2kXmk6zmCMiFUW8qZcP0igMmrgDceTycJjmsP8G7d1YP8QqWUT5/vZC9 42 | huzz6G1qJlUyIRtFJOoC10Hd9Nf/oS6ngg1YJcqNR2Nj9/fIpk+Vbv0XeDJJ27mx 43 | fNmmR0EzCda8R+CAtMkA9jir2vh5vv0sVZ41AeV989ruqXGHmhupScbHl6/7J+n/ 44 | M118qxUfQHZzBHZw12G2uVpSw4QWTPCppAhr9LWEhuLjGedG5hqhlaqCfjl2NQ8e 45 | FrrhwJ/n6LuXr/+sCYanuNXUOSVtSmfyqengYmWtwrZW+CS8Jbn5KTK9Vcf8+bCR 46 | ydotBXCj8ry/Nj/WzX2teQUpMxZATvDKq2xNm69XAgMBAAGgNjA0BgkqhkiG9w0B 47 | CQ4xJzAlMBYGA1UdEQQPMA2CC2Jhci5leGFtcGxlMAsGA1UdDwQEAwIFoDANBgkq 48 | hkiG9w0BAQsFAAOCAgEAO2YDXWc+/uZeRAd5CLr3PTfjGK4zEH2H+glYH15UGt2Y 49 | G4KvhhoFd+OJlaMeF8mIIx+aouDU053lLNlD3LYppvNbkexaJFOjCH3rvrRrTKs3 50 | 1ZT8YSTdwjhc+iGW+3Pf4LdAp3+zAK7EH1PgkWm/8Ie+8uvoMEmV3JZt6vACP7LO 51 | LT3uDWISJNkXHnzYYN5nsaqyJIGw0HNtKbShp1xWPLxcP5YBCMhvk4S4j3pt1qwC 52 | KO+vxm4rYpOpN1Vv6sdQvjUnmPBVauxDI+Jy63F1lvXVQ8NoT9OpwgPF8nRK9Ca4 53 | ULgC5KJkhVaNjUFHJ/O35xEpA3qzmCySsVfvTNAvoTK2uR6mCgHTn0B5Ss3k2ohI 54 | Ne0I7zXOPo5JfVOZX8CWXBpMO/atRtzL+4A3SNRBA3jee1jIMwwD4/VXg6/4252t 55 | UU+KaHoD7lHnQDyBhLKgvn3V/sEwCW3ZW2EKpDnIMyfF025mA3oretsjtgOKJdj6 56 | /JF0/OrU4Sxh8C5tLUCQCfRet65t9h7VrNVN7Qz5W29GkIPdYhJlcxgdDToo7T0a 57 | I+VZoQwXdTl4hmntl6rodGApIIjq28kX2WwJwQZfbQlWzuDDC7U+UFHe2lKHwYOd 58 | xz0DPRAT+f4no6b7q7rp0b132YDS6OPcr84ZBQ1Ro+KRMQUkTKIc5U0Un165kbs= 59 | -----END CERTIFICATE REQUEST-----`, 60 | keyusage.KeyUsage{ 61 | KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, 62 | }, 63 | }, 64 | {"custom usage", 65 | /* 66 | apiVersion: cert-manager.io/v1 67 | kind: Certificate 68 | metadata: 69 | name: kmgm-test-cert3 70 | spec: 71 | secretName: kmgm-test-cert3 72 | dnsNames: 73 | - example.foobar 74 | issuerRef: 75 | name: kmgm-test-issuer 76 | kind: Issuer 77 | group: kmgm-issuer.coe.ad.jp 78 | privateKey: 79 | size: 4096 80 | usages: 81 | - "digital signature" 82 | - "key encipherment" 83 | - "client auth" 84 | */ 85 | `-----BEGIN CERTIFICATE REQUEST----- 86 | MIIEkzCCAnsCAQAwADCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALJK 87 | /XrJTK0/b5lN5Mr3fFJDa6yZR2nLQA5831MP0knOySL0WIfNljHamBnnSgOTsDxG 88 | iyRabwfgFZ4uSMeZCrcSHfk3+VcseZSbxXdFZjF8bDyY3zTTSNDce4ZiAX83hwhA 89 | Qh1X9CzHkBta7k2PN5fAhpLR1a/xtCp6b64GHXJKWR6AnMQfrwV64JCEcx6LS15x 90 | MrvmeM+kgfi0j0EYfgpIyYVJ22tmNlUJhlaEaM9KcmPPTY8aZw54oiNRSWeDPfl+ 91 | ZFiXIekwib+3qaAngCy7r8+/8gpNmzwT+CZlyx/61jiKDqdwEJAD7Ce8bKP5IQJL 92 | LK91FycHgOGEPs3v6DjfkvI7hmyuhdhKihCK24ovBNcJDgYP/FZPUNkF5plHxUUx 93 | bKFV7nSsom8jbCYuGYaxWkf3fjViFoktrgbhKlG0z5sJrUu622FwUWm4oL3o0CqA 94 | qq9NMMIJmjJSTaxlooWFEqdBTwaKP3lLHQGd4oXwrMQdrNfyQdRx+txQV9/viQ+V 95 | ldARjEu2+0F00IawpFhRvy45WOfk2LKSwsU1+U5EiAbdJE7/4/UH4KxASAQ5QMCW 96 | f8DTboqdt6V/AI5Zt3Zo3BS42ca5Khtm+bg70370xGzKYS3slqmC7cf4bJubTyD5 97 | qO7/rBdkE8SgmY3qdHKxRNzPrpmAdslWJyuTvKT9AgMBAAGgTjBMBgkqhkiG9w0B 98 | CQ4xPzA9MBkGA1UdEQQSMBCCDmV4YW1wbGUuZm9vYmFyMAsGA1UdDwQEAwIFoDAT 99 | BgNVHSUEDDAKBggrBgEFBQcDAjANBgkqhkiG9w0BAQsFAAOCAgEAJusoZEMMPmOT 100 | UDjhRcEwvRYNrsCudZwIJ5Bhv+2evBsZ0MS2pOHN8heQ/acT9H7U+8RLavqz9zw5 101 | uE0b3EkrLGtWGRMwqXwL0Ls4usUaO6HE7BaHcchIzSCI5ShmJ8QznMdOftmcOfwB 102 | Vqydt2zarlrh8Pj8VyKC6lC5iijfrWNUVCKBFQu9mPZJRdkIYNU0ksyJ/zCAWknE 103 | C/8KFZUr9m4k0+k8dgEkSINong0ianWvzwwMHwRVbzxd74PUHh2jXNFocBNfBSbD 104 | Iu1173BX7bospFmq2Ap3fn3T42HdMU/kDim2b+6AdLfgmNpml2em3gmZIBjzrSks 105 | /Y9jV+KZLdoacKdLsDnpVmlXBr86zVezoR9PbFlsrdvhT5YKOBG7vFwTYRbmWhZ0 106 | ei9tTx44weltYRV6wrNhcNP7Wus5jGsDLMHXGY9bg6yoOls5uBUgaUVFzcfrstVX 107 | aTQ032yfKu2GhofzHMv2I059sOF/yQxGZ46bRpOow06zJLPOFz0rhs4zIk5J+KN9 108 | +kxv1HrA3kaFuWPNlItmiduDUcQrt/ceQDWE2Y7U/hU1h49IQxg1VFUp2+xVI825 109 | Ko8pTMssA+JTdIMFErzAvhd6vKAE/KEBJW8AugfbFI2AGI0h92NtmVV5NCh5vKwR 110 | Gq3o+TTUlnWc7zEPT12zxGYXUf1SP4g= 111 | -----END CERTIFICATE REQUEST-----`, 112 | keyusage.KeyUsageTLSClient, 113 | }, 114 | } 115 | 116 | for _, tc := range testcases { 117 | req, err := pemparser.ParseCertificateRequest([]byte(tc.PEM)) 118 | if err != nil { 119 | t.Fatalf("%q Failed to parse test case creq: %v", tc.Comment, err) 120 | } 121 | 122 | ku, err := keyusage.FromCSR(req) 123 | if err != nil { 124 | t.Errorf("%q Unexpected KeyUsageFromCSR error: %v", tc.Comment, err) 125 | } 126 | 127 | if !ku.Equals(tc.Expected) { 128 | t.Errorf("%q expected: %v, actual: %v", tc.Comment, tc.Expected, ku) 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /pb/apiversion.go: -------------------------------------------------------------------------------- 1 | package pb 2 | 3 | const ApiVersion = 3 4 | -------------------------------------------------------------------------------- /pb/kmgm.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package kmgm; 3 | option go_package = "./;pb"; 4 | 5 | enum AuthenticationType { 6 | UNSPECIFIED = 0; 7 | ANONYMOUS = 1; 8 | BOOTSTRAP_TOKEN = 2; 9 | CLIENT_CERT = 3; 10 | } 11 | 12 | // Keep this in sync with wcrypto.KeyType 13 | enum KeyType { 14 | KEYTYPE_UNSPECIFIED = 0; 15 | KEYTYPE_RSA4096 = 1; 16 | KEYTYPE_SECP256R1 = 2; 17 | KEYTYPE_RSA2048 = 3; 18 | } 19 | 20 | message HelloRequest { 21 | } 22 | 23 | message HelloResponse { 24 | int32 api_version = 1; 25 | AuthenticationType authentication_type = 2; 26 | string authenticated_user = 3; 27 | } 28 | 29 | service HelloService { 30 | rpc Hello(HelloRequest) returns (HelloResponse); 31 | } 32 | 33 | message GetVersionRequest { 34 | } 35 | 36 | message GetVersionResponse { 37 | string version = 1; 38 | string commit = 2; 39 | } 40 | 41 | service VersionService { 42 | rpc GetVersion(GetVersionRequest) returns (GetVersionResponse); 43 | } 44 | 45 | message DistinguishedName { 46 | string common_name = 1; 47 | string organization = 2; 48 | string organizational_unit = 3; 49 | string country = 4; 50 | string locality = 5; 51 | string province = 6; 52 | string street_address = 7; 53 | string postal_code = 8; 54 | } 55 | 56 | message Names { 57 | repeated string dnsnames = 1; 58 | repeated string ipaddrs = 2; 59 | } 60 | 61 | message KeyUsage { 62 | uint32 key_usage = 1; 63 | repeated uint32 ext_key_usages = 2; 64 | } 65 | 66 | message SetupCARequest { 67 | string profile = 1; 68 | DistinguishedName subject = 2; 69 | KeyType key_type = 3; 70 | int64 not_after_unixtime = 4; 71 | } 72 | 73 | message SetupCAResponse { 74 | } 75 | 76 | message IssuePreflightRequest { 77 | string profile = 1; 78 | } 79 | 80 | message IssuePreflightResponse { 81 | } 82 | 83 | message IssueCertificateRequest { 84 | bytes public_key = 1; 85 | DistinguishedName subject = 2; 86 | Names names = 3; 87 | int64 not_after_unixtime = 4; 88 | 89 | KeyUsage key_usage = 5; 90 | string profile = 6; 91 | } 92 | 93 | message IssueCertificateResponse { 94 | bytes certificate = 1; 95 | } 96 | 97 | message GetCertificateRequest { 98 | string profile = 1; 99 | int64 serial_number = 2; 100 | } 101 | 102 | message GetCertificateResponse { 103 | bytes certificate = 1; 104 | } 105 | 106 | service CertificateService { 107 | rpc SetupCA(SetupCARequest) returns (SetupCAResponse); 108 | rpc IssuePreflight(IssuePreflightRequest) returns (IssuePreflightResponse); 109 | rpc IssueCertificate(IssueCertificateRequest) returns (IssueCertificateResponse); 110 | rpc GetCertificate(GetCertificateRequest) returns (GetCertificateResponse); 111 | } 112 | -------------------------------------------------------------------------------- /pemparser/consts.go: -------------------------------------------------------------------------------- 1 | package pemparser 2 | 3 | const ( 4 | RSAPrivateKeyPemType = "RSA PRIVATE KEY" 5 | ECPrivateKeyPemType = "EC PRIVATE KEY" 6 | CertificatePemType = "CERTIFICATE" 7 | CertificateRequestPemType = "CERTIFICATE REQUEST" 8 | PublicKeyPemType = "PUBLIC KEY" 9 | ) 10 | -------------------------------------------------------------------------------- /pemparser/marshaller.go: -------------------------------------------------------------------------------- 1 | package pemparser 2 | 3 | import ( 4 | "crypto" 5 | "crypto/ecdsa" 6 | "crypto/rsa" 7 | "crypto/x509" 8 | "encoding/pem" 9 | "fmt" 10 | "reflect" 11 | ) 12 | 13 | func MarshalPrivateKey(priv crypto.PrivateKey) ([]byte, error) { 14 | switch impl := priv.(type) { 15 | case (*rsa.PrivateKey): 16 | bs := pem.EncodeToMemory(&pem.Block{ 17 | Type: RSAPrivateKeyPemType, 18 | Bytes: x509.MarshalPKCS1PrivateKey(impl), 19 | }) 20 | return bs, nil 21 | case (*ecdsa.PrivateKey): 22 | kbs, err := x509.MarshalECPrivateKey(impl) 23 | if err != nil { 24 | return nil, err 25 | } 26 | bs := pem.EncodeToMemory(&pem.Block{ 27 | Type: ECPrivateKeyPemType, 28 | Bytes: kbs, 29 | }) 30 | return bs, nil 31 | 32 | default: 33 | return nil, fmt.Errorf("Couldn't marshal unknown private key type: %v", reflect.TypeOf(priv)) 34 | } 35 | } 36 | 37 | func MarshalCertificateDer(certDer []byte) []byte { 38 | return pem.EncodeToMemory(&pem.Block{ 39 | Type: CertificatePemType, 40 | Bytes: certDer, 41 | }) 42 | } 43 | 44 | func MarshalCertificateRequestDer(der []byte) []byte { 45 | return pem.EncodeToMemory(&pem.Block{ 46 | Type: CertificateRequestPemType, 47 | Bytes: der, 48 | }) 49 | } 50 | -------------------------------------------------------------------------------- /pemparser/parser.go: -------------------------------------------------------------------------------- 1 | package pemparser 2 | 3 | import ( 4 | "crypto" 5 | "crypto/x509" 6 | "encoding/pem" 7 | "errors" 8 | "fmt" 9 | ) 10 | 11 | func ForeachPemBlock(pemText []byte, f func(*pem.Block) error) error { 12 | for len(pemText) > 0 { 13 | var block *pem.Block 14 | block, pemText = pem.Decode(pemText) 15 | if block == nil { 16 | break 17 | } 18 | 19 | if err := f(block); err != nil { 20 | return err 21 | } 22 | } 23 | 24 | return nil 25 | } 26 | 27 | var ErrMultipleCertificateRequestBlocks = errors.New("Found more than one CERTIFICATE REQUEST block") 28 | 29 | func ParseCertificateRequest(pemText []byte) (req *x509.CertificateRequest, err error) { 30 | err = ForeachPemBlock(pemText, func(block *pem.Block) error { 31 | if block.Type != CertificateRequestPemType { 32 | return nil 33 | } 34 | 35 | if req != nil { 36 | err = ErrMultipleCertificateRequestBlocks 37 | return err 38 | } 39 | 40 | req, err = x509.ParseCertificateRequest(block.Bytes) 41 | return err 42 | }) 43 | if err == nil && req == nil { 44 | err = fmt.Errorf("Target pem block %q not found.", CertificateRequestPemType) 45 | return 46 | } 47 | return 48 | } 49 | 50 | func ParseCertificates(pemText []byte) ([]*x509.Certificate, error) { 51 | var certs []*x509.Certificate 52 | 53 | if err := ForeachPemBlock(pemText, func(block *pem.Block) error { 54 | cert, err := x509.ParseCertificate(block.Bytes) 55 | if err != nil { 56 | return err 57 | } 58 | 59 | certs = append(certs, cert) 60 | return nil 61 | }); err != nil { 62 | return nil, err 63 | } 64 | if len(certs) == 0 { 65 | return nil, errors.New("No certificate was found.") 66 | } 67 | 68 | return certs, nil 69 | } 70 | 71 | func ParseSinglePrivateKeyBlock(block *pem.Block) (crypto.PrivateKey, error) { 72 | der := block.Bytes 73 | if k, err := x509.ParsePKCS1PrivateKey(der); err == nil { 74 | return k, nil 75 | } 76 | if k, err := x509.ParsePKCS8PrivateKey(der); err == nil { 77 | return k, nil 78 | } 79 | if k, err := x509.ParseECPrivateKey(der); err == nil { 80 | return k, nil 81 | } 82 | return nil, errors.New("Failed to parse private key.") 83 | } 84 | 85 | func ParsePrivateKey(pemText []byte) (crypto.PrivateKey, error) { 86 | var priv crypto.PrivateKey 87 | 88 | if err := ForeachPemBlock(pemText, func(block *pem.Block) error { 89 | newpriv, err := ParseSinglePrivateKeyBlock(block) 90 | if err == nil { 91 | if priv != nil { 92 | return errors.New("More than one private key found in given pemText.") 93 | } 94 | priv = newpriv 95 | } 96 | 97 | return nil 98 | }); err != nil { 99 | return nil, err 100 | } 101 | if priv == nil { 102 | return nil, errors.New("Failed to parse private key.") 103 | } 104 | 105 | return priv, nil 106 | } 107 | -------------------------------------------------------------------------------- /pemparser/parser_test.go: -------------------------------------------------------------------------------- 1 | package pemparser_test 2 | 3 | import ( 4 | "encoding/asn1" 5 | "errors" 6 | "testing" 7 | 8 | "github.com/IPA-CyberLab/kmgm/pemparser" 9 | ) 10 | 11 | var TestCSR = []byte(`-----BEGIN CERTIFICATE REQUEST----- 12 | MIICmDCCAYACAQAwUzELMAkGA1UEBhMCSlAxDjAMBgNVBAgMBVRva3lvMSEwHwYD 13 | VQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxETAPBgNVBAMMCGhvZ2VmdWdh 14 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0f7/xMDop3WNJAYuWFaJ 15 | G5dcbzXRlWk4PHAtZjxQPCKP/lN0pHGyCFs58v4rg7OFba+5WFg9DznvzYukhE7Z 16 | rIej/E4Xpl1LUQcaSm6IdzzBUUF6+rOuufLZMV1v1eaa3KIT96u+65k9+eM7CmkK 17 | cke2dIQs7/OTz+viq/8dFZnSRWCyH0HPE61wF79VHJgAt6Cdi4muWgcBgxg+8nRv 18 | vy0XO70Z2EYtD01ncsoNb+Xd9v6eXLsMBWbMzljN/5rKlybodwnXgMcz2RzQdeuY 19 | PA4MYh5dwieZ23UaKB5IX2IvieCOz5KYT8hsS54HUXQX+DBPnj4uqwVMDAG+xbK8 20 | uwIDAQABoAAwDQYJKoZIhvcNAQELBQADggEBAJXCcsaRXvx1T+AdOvF5aFElJ9tn 21 | 00gK8gWf4uyOOypOv3XUzdmuk93m2zkuCTvdC1lyj7KogJR6oHe+Y4UhJBqISh1J 22 | +8ZKSBlusicJftHhxR3s63Zy7cKHu57CdrLW8eYY+Wrt53s/EzN8Rv0s5kQTWtjI 23 | 2v7IFUJe81tf5NDW8f4vqcilqM4pA4IqzPJCoulXTlCMiJhhJGFP76YpDOfZX7eA 24 | X/8dzdW3bJ6aBNkt+mMFIk32veY0NKaflVo57FauPyD6/9d1PajYXsTMXL4O/c5j 25 | Lv7aCvdGIifcy7qV0Slxjg6YbDtai0MGogOvsxSFsSzUmwGnfDGb9Q9nhog= 26 | -----END CERTIFICATE REQUEST----- 27 | `) 28 | 29 | var IncompleteCSR = []byte(`-----BEGIN CERTIFICATE REQUEST----- 30 | MIHzMIGaAgEAMAAwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQzH2cBP3lcXHwm 31 | 451JhOmDRq1Y/ZbNgw00r5mSf5r9hqR/Xd+30QhiHOrCA7LfE0vKCNuidndDTH8Q 32 | 95VrjM8ooDgwNgYJKoZIhvcNAQkOMSkwJzAYBgNVHREEETAPgg1lY2RzYS5leGFt 33 | cGxlMAsGA1UdDwQEAwIFoDAKBggqhkjOPQQDAgNIADBFAiEAsEvJuhNtieOyEmqN 34 | lXabDvu2IoDqCshBpwyjsvy+rTUCIAW/Dn80lqxR2YQiMYujLxP84EOPZfwY1e7p 35 | -----END CERTIFICATE REQUEST----- 36 | `) 37 | 38 | func Test_ParseCertificateRequest(t *testing.T) { 39 | _, err := pemparser.ParseCertificateRequest([]byte{}) 40 | if err == nil { 41 | t.Errorf("Expected err for parsing empty input") 42 | } 43 | 44 | req, err := pemparser.ParseCertificateRequest(TestCSR) 45 | if err != nil { 46 | t.Errorf("Unexpected error: %v", err) 47 | } 48 | 49 | if req.Subject.CommonName != "hogefuga" { 50 | t.Errorf("Unexpected CN: %s", req.Subject.CommonName) 51 | } 52 | 53 | multipleCSRs := append([]byte(TestCSR), []byte(TestCSR)...) 54 | _, err = pemparser.ParseCertificateRequest(multipleCSRs) 55 | if err != pemparser.ErrMultipleCertificateRequestBlocks { 56 | t.Errorf("Unexpected error: %v", err) 57 | } 58 | 59 | _, err = pemparser.ParseCertificateRequest(IncompleteCSR) 60 | var asnsynerr asn1.SyntaxError 61 | if !errors.As(err, &asnsynerr) { 62 | t.Errorf("Unexpected err when parsing incomplete csr pem: %v", err) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /period/days.go: -------------------------------------------------------------------------------- 1 | package period 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "strconv" 7 | "time" 8 | ) 9 | 10 | const immediatelyToken = "immediately" 11 | const autoToken = "auto" 12 | 13 | type Days int 14 | 15 | const ( 16 | DaysAuto Days = -1 17 | DaysImmediately Days = 0 18 | ) 19 | 20 | func (d Days) String() string { 21 | switch d { 22 | case DaysAuto: 23 | return autoToken 24 | case 0: 25 | return immediatelyToken 26 | default: 27 | } 28 | 29 | left := d 30 | 31 | years := left / 365 32 | left -= years * 365 33 | if years > 0 { 34 | return fmt.Sprintf("%dy%dd", years, left) 35 | } else { 36 | return fmt.Sprintf("%dd", left) 37 | } 38 | } 39 | 40 | var ( 41 | reYears = regexp.MustCompile(`^(\d+)y(.*)`) 42 | reDays = regexp.MustCompile(`^(\d+)d$`) 43 | ) 44 | 45 | func (d *Days) UnmarshalFlag(s string) error { 46 | switch s { 47 | case "", "auto": 48 | *d = DaysAuto 49 | return nil 50 | case immediatelyToken: 51 | *d = Days(0) 52 | return nil 53 | default: 54 | } 55 | 56 | *d = Days(0) 57 | left := s 58 | 59 | if ms := reYears.FindStringSubmatch(left); len(ms) > 0 { 60 | u, err := strconv.ParseUint(ms[1], 10, 32) 61 | if err != nil { 62 | return fmt.Errorf("Failed to parse years uint %q.", ms[1]) 63 | } 64 | *d += Days(uint(u) * 365) 65 | left = ms[2] 66 | if left == "" { 67 | return nil 68 | } 69 | } 70 | 71 | if ms := reDays.FindStringSubmatch(left); len(ms) > 0 { 72 | u, err := strconv.ParseUint(ms[1], 10, 32) 73 | if err != nil { 74 | return fmt.Errorf("Failed to parse days uint %q.", ms[1]) 75 | } 76 | *d += Days(uint(u)) 77 | 78 | return nil 79 | } 80 | return fmt.Errorf("Failed to parse Days %q. Try something like 30d, 1y.", s) 81 | } 82 | 83 | func (d *Days) UnmarshalYAML(unmarshal func(interface{}) error) error { 84 | var s string 85 | if err := unmarshal(&s); err != nil { 86 | return err 87 | } 88 | if err := d.UnmarshalFlag(s); err != nil { 89 | return err 90 | } 91 | return nil 92 | } 93 | 94 | func (d Days) ToDuration() time.Duration { 95 | return time.Duration(d) * 24 * time.Hour 96 | } 97 | -------------------------------------------------------------------------------- /period/days_test.go: -------------------------------------------------------------------------------- 1 | package period 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestDays_UnmarshalFlag(t *testing.T) { 8 | tests := []struct { 9 | days Days 10 | s string 11 | wantErr bool 12 | }{ 13 | {DaysAuto, "", false}, 14 | {Days(0), "immediately", false}, 15 | {Days(1), "1d", false}, 16 | {Days(365), "1y", false}, 17 | {Days(366), "1y1d", false}, 18 | {Days(3*365 + 35), "3y35d", false}, 19 | } 20 | for _, tc := range tests { 21 | t.Run(tc.s, func(t *testing.T) { 22 | var d Days 23 | if err := d.UnmarshalFlag(tc.s); (err != nil) != tc.wantErr { 24 | t.Errorf("Days.UnmarshalFlag() error = %v, wantErr %v", err, tc.wantErr) 25 | } 26 | if d != tc.days { 27 | t.Errorf("Days.UnmarshalFlag() = %v, want %v", d, tc.days) 28 | } 29 | }) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /period/validityperiod.go: -------------------------------------------------------------------------------- 1 | package period 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "time" 7 | ) 8 | 9 | type ValidityPeriod struct { 10 | // Days specifies the number of days that the issued cert would be valid for. 11 | // Days count is ignored if NotAfter is specified to non-zero. 12 | Days 13 | 14 | // NotAfter specifies the timestamp where the cert is considered valid to (inclusive). 15 | NotAfter time.Time 16 | 17 | // I couldn't find a reasonable use of the user specifying NotBefore % tests. 18 | // Thus omitting. 19 | } 20 | 21 | var FarFuture = ValidityPeriod{ 22 | Days: 0, 23 | 24 | // RFC5280 4.1.2.5 says we should use 99991231235959Z, for the certs which 25 | // never expire but some implementation have issues handling the date. 26 | // We pick 2099-12-31 for the timestamp of a reasonably far future. 27 | NotAfter: time.Date(2099, 12, 31, 23, 59, 0, 0, time.UTC), 28 | } 29 | 30 | func (p ValidityPeriod) GetNotAfter(base time.Time) time.Time { 31 | if !p.NotAfter.IsZero() { 32 | return p.NotAfter 33 | } 34 | return base.Add(time.Duration(p.Days) * 24 * time.Hour) 35 | } 36 | 37 | const notAfterLayout = "20060102" 38 | 39 | func (p ValidityPeriod) IsFarFuture() bool { 40 | return p.NotAfter.Equal(FarFuture.NotAfter) 41 | } 42 | 43 | func (p ValidityPeriod) String() string { 44 | if p.NotAfter.Equal(FarFuture.NotAfter) { 45 | return "farfuture" 46 | } 47 | if !p.NotAfter.IsZero() { 48 | return p.NotAfter.Format(notAfterLayout) 49 | } 50 | return p.Days.String() 51 | } 52 | 53 | func (p *ValidityPeriod) UnmarshalFlag(s string) error { 54 | if strings.ToLower(s) == "farfuture" { 55 | *p = FarFuture 56 | return nil 57 | } 58 | if err := p.Days.UnmarshalFlag(s); err == nil { 59 | return nil 60 | } 61 | if t, err := time.ParseInLocation(notAfterLayout, s, time.Local); err == nil { 62 | p.NotAfter = t 63 | return nil 64 | } 65 | 66 | return fmt.Errorf("Failed to parse ValidityPeriod %q. Try something like 30d, 1y, or 20220530.", s) 67 | } 68 | 69 | func (p *ValidityPeriod) UnmarshalYAML(unmarshal func(interface{}) error) error { 70 | var s string 71 | if err := unmarshal(&s); err != nil { 72 | return err 73 | } 74 | if err := p.UnmarshalFlag(s); err != nil { 75 | return err 76 | } 77 | return nil 78 | } 79 | -------------------------------------------------------------------------------- /period/validityperiod_test.go: -------------------------------------------------------------------------------- 1 | package period_test 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | "time" 7 | 8 | "github.com/IPA-CyberLab/kmgm/period" 9 | ) 10 | 11 | var testcases = []struct { 12 | Validity period.ValidityPeriod 13 | String string 14 | }{ 15 | {period.ValidityPeriod{Days: period.DaysAuto}, "auto"}, 16 | {period.ValidityPeriod{Days: 0}, "immediately"}, 17 | {period.ValidityPeriod{Days: 123}, "123d"}, 18 | {period.ValidityPeriod{Days: 245}, "245d"}, 19 | {period.ValidityPeriod{Days: 3650}, "10y0d"}, 20 | {period.ValidityPeriod{NotAfter: time.Date(2019, 6, 1, 0, 0, 0, 0, time.Local)}, "20190601"}, 21 | {period.FarFuture, "farfuture"}, 22 | } 23 | 24 | func TestValidityPeriod_UnmarshalFlag(t *testing.T) { 25 | for _, tc := range testcases { 26 | var p period.ValidityPeriod 27 | if err := p.UnmarshalFlag(tc.String); err != nil { 28 | t.Errorf("Failed to parse %q: %v", tc.String, err) 29 | } 30 | if !reflect.DeepEqual(tc.Validity, p) { 31 | t.Errorf("Parse %q failed. Expected %+v, Actual %+v", tc.String, tc.Validity, p) 32 | } 33 | } 34 | } 35 | 36 | func TestValidityPeriod_String(t *testing.T) { 37 | for _, tc := range testcases { 38 | s := tc.Validity.String() 39 | if s != tc.String { 40 | t.Errorf("Expected %q but got %q", tc.String, s) 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /remote/conn.go: -------------------------------------------------------------------------------- 1 | package remote 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "crypto/x509" 7 | "errors" 8 | "fmt" 9 | 10 | "go.uber.org/zap" 11 | "golang.org/x/oauth2" 12 | "google.golang.org/grpc" 13 | "google.golang.org/grpc/credentials/oauth" 14 | 15 | "github.com/IPA-CyberLab/kmgm/storage" 16 | ) 17 | 18 | type ConnectionInfo struct { 19 | HostPort string `yaml:"hostPort" flags:"server,host:port of kmgm server to connect to"` 20 | 21 | CACertificateFile string `yaml:"caCertificateFile" flags:"cacert,Path to a CA certificate to verify the kmgm server,,path"` 22 | PinnedPubKey string `yaml:"pinnedPubKey" flags:"pinnedpubkey,SHA256 hash of the kmgm server publickey"` 23 | AllowInsecure bool `yaml:"allowInsecure,omitempty" flags:"insecure,skip kmgm server certificate verification (hidden),,hidden"` 24 | 25 | ClientCertificateFile string `yaml:"clientCertificateFile" flags:"client-cert,Path to a client certificate to present to the kmgm server,,path"` 26 | ClientPrivateKeyFile string `yaml:"clientPrivateKeyFile" flags:"client-priv,Path to the private key corresponding to the client certificate,,path"` 27 | 28 | AccessToken string `yaml:"accessToken,omitempty" flags:"token,Token string to use for server authentication when bootstrapping"` 29 | } 30 | 31 | func (cinfo ConnectionInfo) TransportCredentials(l *zap.Logger) (*TransportCredentials, error) { 32 | slog := l.Sugar() 33 | 34 | var tc *tls.Config 35 | if cinfo.CACertificateFile != "" { 36 | cacert, err := storage.ReadCertificateFile(cinfo.CACertificateFile) 37 | if err != nil { 38 | return nil, err 39 | } 40 | 41 | cp := x509.NewCertPool() 42 | cp.AddCert(cacert) 43 | 44 | tc = &tls.Config{RootCAs: cp} 45 | } else { 46 | if cinfo.PinnedPubKey == "" && !cinfo.AllowInsecure { 47 | return nil, errors.New("Neither CA cert or public key pin hash was supplied to authenticate server.") 48 | } 49 | 50 | tc = &tls.Config{InsecureSkipVerify: true} 51 | } 52 | if cinfo.AccessToken != "" { 53 | if cinfo.ClientCertificateFile != "" { 54 | slog.Debugf("Ignoring ClientCertificateFile since AccessToken was provided.") 55 | } 56 | } else if cinfo.ClientCertificateFile != "" { 57 | if cinfo.ClientPrivateKeyFile == "" { 58 | return nil, errors.New("Client auth privateKey was supplied without a client certificate.") 59 | } 60 | 61 | priv, err := storage.ReadPrivateKeyFile(cinfo.ClientPrivateKeyFile) 62 | if err != nil { 63 | return nil, err 64 | } 65 | 66 | cert, err := storage.ReadCertificateFile(cinfo.ClientCertificateFile) 67 | if err != nil { 68 | return nil, err 69 | } 70 | 71 | tc.Certificates = []tls.Certificate{{ 72 | Certificate: [][]byte{cert.Raw}, 73 | PrivateKey: priv, 74 | }} 75 | } else { 76 | if cinfo.ClientPrivateKeyFile != "" { 77 | return nil, errors.New("Client auth certificate was supplied without a privateKey.") 78 | } 79 | } 80 | 81 | tcred := NewTransportCredentials(tc, cinfo.PinnedPubKey) 82 | 83 | return tcred, nil 84 | } 85 | 86 | func (cinfo ConnectionInfo) DialPubKeys(ctx context.Context, l *zap.Logger) (*grpc.ClientConn, map[string]struct{}, error) { 87 | tcred, err := cinfo.TransportCredentials(l) 88 | if err != nil { 89 | return nil, nil, err 90 | } 91 | 92 | opts := []grpc.DialOption{ 93 | grpc.WithTransportCredentials(tcred), 94 | } 95 | if cinfo.AccessToken != "" { 96 | ts := oauth2.StaticTokenSource( 97 | &oauth2.Token{AccessToken: cinfo.AccessToken}) 98 | opts = append(opts, 99 | grpc.WithPerRPCCredentials(oauth.TokenSource{TokenSource: ts})) 100 | } 101 | conn, err := grpc.DialContext(ctx, cinfo.HostPort, opts...) 102 | if err != nil { 103 | return nil, nil, fmt.Errorf("Failed to grpc.Dial(%q). err: %v", cinfo.HostPort, err) 104 | } 105 | return conn, tcred.PeerPubKeys, nil 106 | } 107 | 108 | func (cinfo ConnectionInfo) Dial(ctx context.Context, l *zap.Logger) (*grpc.ClientConn, error) { 109 | conn, _, err := cinfo.DialPubKeys(ctx, l) 110 | return conn, err 111 | } 112 | -------------------------------------------------------------------------------- /remote/hello/hello.go: -------------------------------------------------------------------------------- 1 | package hello 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "go.uber.org/zap" 8 | "google.golang.org/grpc" 9 | 10 | "github.com/IPA-CyberLab/kmgm/pb" 11 | "github.com/IPA-CyberLab/kmgm/remote/user" 12 | ) 13 | 14 | func Hello(ctx context.Context, conn *grpc.ClientConn, l *zap.Logger) (user.User, error) { 15 | slog := l.Sugar() 16 | 17 | sc := pb.NewHelloServiceClient(conn) 18 | resp, err := sc.Hello(ctx, &pb.HelloRequest{}) 19 | if err != nil { 20 | return user.User{}, err 21 | } 22 | 23 | if resp.ApiVersion != pb.ApiVersion { 24 | err := fmt.Errorf( 25 | "Server version %d and client version %d mismatch.", 26 | resp.ApiVersion, pb.ApiVersion) 27 | return user.User{}, err 28 | } 29 | 30 | slog.Debugf("Server recognizes me as the user %q", resp.AuthenticatedUser) 31 | au := user.User{ 32 | Type: resp.AuthenticationType, 33 | Name: resp.AuthenticatedUser, 34 | } 35 | return au, nil 36 | } 37 | -------------------------------------------------------------------------------- /remote/issue/issue.go: -------------------------------------------------------------------------------- 1 | package issue 2 | 3 | import ( 4 | "context" 5 | "crypto" 6 | "crypto/x509" 7 | "time" 8 | 9 | "github.com/IPA-CyberLab/kmgm/action" 10 | localissue "github.com/IPA-CyberLab/kmgm/action/issue" 11 | "github.com/IPA-CyberLab/kmgm/pb" 12 | ) 13 | 14 | type Config = localissue.Config 15 | 16 | var DefaultConfig = localissue.DefaultConfig 17 | 18 | func IssueCertificate(ctx context.Context, env *action.Environment, pub crypto.PublicKey, cfg *Config) ([]byte, error) { 19 | slog := env.Logger.Sugar() 20 | 21 | if err := cfg.Names.Verify(); err != nil { 22 | return nil, err 23 | } 24 | 25 | pkixpub, err := x509.MarshalPKIXPublicKey(pub) 26 | if err != nil { 27 | return nil, err 28 | } 29 | 30 | if err := env.EnsureClientConn(ctx); err != nil { 31 | return nil, err 32 | } 33 | 34 | sc := pb.NewCertificateServiceClient(env.ClientConn) 35 | 36 | slog.Info("Requesting certificate...") 37 | start := time.Now() 38 | resp, err := sc.IssueCertificate(ctx, &pb.IssueCertificateRequest{ 39 | PublicKey: pkixpub, 40 | Subject: cfg.Subject.ToProtoStruct(), 41 | Names: cfg.Names.ToProtoStruct(), 42 | NotAfterUnixtime: cfg.Validity.GetNotAfter(start).Unix(), 43 | KeyUsage: cfg.KeyUsage.ToProtoStruct(), 44 | Profile: env.ProfileName, 45 | }) 46 | slog.Infow("Generating certificate... Done.", "took", time.Now().Sub(start)) 47 | if err != nil { 48 | return nil, err 49 | } 50 | return resp.Certificate, nil 51 | } 52 | -------------------------------------------------------------------------------- /remote/transportcredentials.go: -------------------------------------------------------------------------------- 1 | package remote 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "errors" 7 | "fmt" 8 | "net" 9 | 10 | "github.com/IPA-CyberLab/kmgm/wcrypto" 11 | "google.golang.org/grpc/credentials" 12 | ) 13 | 14 | // TransportCredentials is grpc.tlsCreds + pubkey pinning support. 15 | type TransportCredentials struct { 16 | tlscreds credentials.TransportCredentials 17 | PinnedPubKey string 18 | PeerPubKeys map[string]struct{} 19 | } 20 | 21 | var _ = credentials.TransportCredentials(&TransportCredentials{}) 22 | 23 | func NewTransportCredentials(c *tls.Config, pinnedpubkey string) *TransportCredentials { 24 | return &TransportCredentials{ 25 | tlscreds: credentials.NewTLS(c), 26 | PinnedPubKey: pinnedpubkey, 27 | PeerPubKeys: make(map[string]struct{}), 28 | } 29 | } 30 | 31 | func (c *TransportCredentials) Info() credentials.ProtocolInfo { 32 | return c.tlscreds.Info() 33 | } 34 | 35 | func (c *TransportCredentials) ClientHandshake(ctx context.Context, authority string, rawConn net.Conn) (net.Conn, credentials.AuthInfo, error) { 36 | conn, authinfo, err := c.tlscreds.ClientHandshake(ctx, authority, rawConn) 37 | if err != nil { 38 | return nil, nil, err 39 | } 40 | 41 | ti := authinfo.(credentials.TLSInfo) 42 | 43 | found := false 44 | 45 | pcerts := ti.State.PeerCertificates 46 | for _, pcert := range pcerts { 47 | pubkeyhash, err := wcrypto.PubKeyPinString(pcert.PublicKey) 48 | if err != nil { 49 | // FIXME[P2]: how should we handle this? at least log? 50 | continue 51 | } 52 | 53 | c.PeerPubKeys[pubkeyhash] = struct{}{} 54 | 55 | if pubkeyhash == c.PinnedPubKey { 56 | found = true 57 | break 58 | } 59 | } 60 | 61 | if c.PinnedPubKey != "" && !found { 62 | _ = conn.Close() 63 | 64 | return nil, nil, fmt.Errorf("Server certificate did not match pinnedpubkey %q.", c.PinnedPubKey) 65 | } 66 | 67 | return conn, authinfo, nil 68 | } 69 | 70 | func (c *TransportCredentials) ServerHandshake(rawConn net.Conn) (net.Conn, credentials.AuthInfo, error) { 71 | return nil, nil, errors.New("Not implemented.") 72 | } 73 | 74 | func (c *TransportCredentials) Clone() credentials.TransportCredentials { 75 | return &TransportCredentials{ 76 | tlscreds: c.tlscreds.Clone(), 77 | PinnedPubKey: c.PinnedPubKey, 78 | } 79 | } 80 | 81 | func (c *TransportCredentials) OverrideServerName(serverNameOverride string) error { 82 | return c.tlscreds.OverrideServerName(serverNameOverride) 83 | } 84 | -------------------------------------------------------------------------------- /remote/user/authorization.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "github.com/IPA-CyberLab/kmgm/consts" 5 | "github.com/IPA-CyberLab/kmgm/pb" 6 | ) 7 | 8 | // FIXME: cover other internal profiles too. For instance, reserve `kmgm serve` CA profile. 9 | 10 | func (u User) IsAllowedToSetupCA(profileName string) bool { 11 | if profileName == consts.AuthProfileName { 12 | return false 13 | } 14 | return u.Type == pb.AuthenticationType_CLIENT_CERT 15 | } 16 | 17 | func (u User) IsAllowedToIssueCertificate(profileName string) bool { 18 | if profileName == consts.AuthProfileName && 19 | u.Type == pb.AuthenticationType_BOOTSTRAP_TOKEN { 20 | return true 21 | } 22 | return u.Type == pb.AuthenticationType_CLIENT_CERT 23 | } 24 | 25 | func (u User) IsAllowedToGetCertificate() bool { 26 | return u.Type == pb.AuthenticationType_CLIENT_CERT 27 | } 28 | -------------------------------------------------------------------------------- /remote/user/context.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import "context" 4 | 5 | type userKey struct{} 6 | 7 | // NewContext creates a new context with user information attached. 8 | func NewContext(ctx context.Context, u User) context.Context { 9 | return context.WithValue(ctx, userKey{}, u) 10 | } 11 | 12 | // FromContext returns the user information in ctx if it exists. 13 | func FromContext(ctx context.Context) User { 14 | if u, ok := ctx.Value(userKey{}).(User); ok { 15 | return u 16 | } 17 | return Anonymous 18 | } 19 | -------------------------------------------------------------------------------- /remote/user/user.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/IPA-CyberLab/kmgm/pb" 7 | ) 8 | 9 | // User contains the information of an authenticated user to kmgm HTTPS/gRPC server. 10 | type User struct { 11 | Type pb.AuthenticationType 12 | Name string 13 | } 14 | 15 | var Anonymous = User{ 16 | Type: pb.AuthenticationType_ANONYMOUS, 17 | Name: "anonymous", 18 | } 19 | 20 | var BootstrapToken = User{ 21 | Type: pb.AuthenticationType_BOOTSTRAP_TOKEN, 22 | Name: "bootstrapToken", 23 | } 24 | 25 | func ClientCert(commonName string) User { 26 | return User{ 27 | Type: pb.AuthenticationType_CLIENT_CERT, 28 | Name: fmt.Sprintf("clientcert:%s", commonName), 29 | } 30 | } 31 | 32 | func (u User) String() string { 33 | return fmt.Sprintf("User[%s]", u.Name) 34 | } 35 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /san/protoconv.go: -------------------------------------------------------------------------------- 1 | package san 2 | 3 | import ( 4 | "net" 5 | 6 | "github.com/IPA-CyberLab/kmgm/pb" 7 | ) 8 | 9 | func FromProtoStruct(s *pb.Names) (Names, error) { 10 | var ns Names 11 | if s == nil { 12 | return ns, nil 13 | } 14 | 15 | ns.DNSNames = append(ns.DNSNames, s.Dnsnames...) 16 | for _, e := range s.Ipaddrs { 17 | if ipaddr := net.ParseIP(e); ipaddr != nil { 18 | ns.IPAddrs = append(ns.IPAddrs, ipaddr) 19 | } 20 | } 21 | if err := ns.Verify(); err != nil { 22 | return Names{}, err 23 | } 24 | 25 | return ns, nil 26 | } 27 | 28 | func (ns Names) ToProtoStruct() *pb.Names { 29 | ss := make([]string, 0, len(ns.IPAddrs)) 30 | for _, ip := range ns.IPAddrs { 31 | ss = append(ss, ip.String()) 32 | } 33 | 34 | return &pb.Names{ 35 | Dnsnames: append([]string{}, ns.DNSNames...), 36 | Ipaddrs: ss, 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /san/san_test.go: -------------------------------------------------------------------------------- 1 | package san_test 2 | 3 | import ( 4 | "net" 5 | "testing" 6 | 7 | "github.com/IPA-CyberLab/kmgm/san" 8 | ) 9 | 10 | func TestAdd(t *testing.T) { 11 | var ns san.Names 12 | 13 | if err := ns.Add("192.168.0.1"); err != nil { 14 | t.Fatalf("unexpected err: %v", err) 15 | } 16 | if len(ns.IPAddrs) != 1 { 17 | t.Fatalf("unexpected") 18 | } 19 | 20 | if err := ns.Add("example.com"); err != nil { 21 | t.Fatalf("unexpected err: %v", err) 22 | } 23 | if len(ns.IPAddrs) != 1 { 24 | t.Fatalf("unexpected") 25 | } 26 | if len(ns.DNSNames) != 1 { 27 | t.Fatalf("unexpected") 28 | } 29 | 30 | if err := ns.Add("example.com"); err != nil { 31 | t.Fatalf("unexpected err: %v", err) 32 | } 33 | if len(ns.DNSNames) != 1 { 34 | t.Fatalf("unexpected") 35 | } 36 | } 37 | 38 | func TestPunycode(t *testing.T) { 39 | var ns san.Names 40 | 41 | if err := ns.Add("日本語.example"); err != nil { 42 | t.Fatalf("unexpected: %v", err) 43 | } 44 | 45 | if len(ns.DNSNames) != 1 { 46 | t.Fatalf("unexpected") 47 | } 48 | if ns.DNSNames[0] != "xn--wgv71a119e.example" { 49 | t.Fatalf("unexpected: %q", ns.DNSNames[0]) 50 | } 51 | } 52 | 53 | func TestForThisHost_IPAddr(t *testing.T) { 54 | ns, err := san.ForListenAddr("192.168.0.100:12345") 55 | if err != nil { 56 | t.Fatalf("%v", err) 57 | } 58 | if len(ns.IPAddrs) != 1 { 59 | t.Fatalf("unexpected len: %d", len(ns.IPAddrs)) 60 | } 61 | exp := net.ParseIP("192.168.0.100") 62 | if !ns.IPAddrs[0].Equal(exp) { 63 | t.Fatalf("unexpected ip: %v", ns.IPAddrs[0]) 64 | } 65 | } 66 | 67 | func TestForThisHost_0000(t *testing.T) { 68 | ns, err := san.ForListenAddr("0.0.0.0:12345") 69 | if err != nil { 70 | t.Fatalf("%v", err) 71 | } 72 | if len(ns.IPAddrs) != 0 { 73 | t.Fatalf("unexpected len: %d", len(ns.IPAddrs)) 74 | } 75 | } 76 | 77 | func TestForThisHost_Empty(t *testing.T) { 78 | ns, err := san.ForListenAddr(":12345") 79 | if err != nil { 80 | t.Fatalf("%v", err) 81 | } 82 | if len(ns.IPAddrs) != 0 { 83 | t.Fatalf("unexpected len: %d", len(ns.IPAddrs)) 84 | } 85 | } 86 | 87 | func TestCompatibleWith(t *testing.T) { 88 | a := san.MustParse("a,b.example,c.example.net") 89 | if err := a.CompatibleWith(a); err != nil { 90 | t.Errorf("%v", err) 91 | } 92 | 93 | b := san.MustParse("a,b.example,c.example") 94 | if err := a.CompatibleWith(b); err == nil { 95 | t.Errorf("should have errored") 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /storage/config.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "fmt" 5 | "os/user" 6 | "path/filepath" 7 | ) 8 | 9 | func DefaultStoragePath() (string, error) { 10 | u, err := user.Current() 11 | if err != nil { 12 | return "", fmt.Errorf("user.Current: %w", err) 13 | } 14 | if u.Uid == "0" { 15 | return "/var/lib/kmgm", nil 16 | } 17 | return filepath.Join(u.HomeDir, ".config", "kmgm"), nil 18 | } 19 | -------------------------------------------------------------------------------- /storage/issuedb/issuedb.go: -------------------------------------------------------------------------------- 1 | package issuedb 2 | 3 | import ( 4 | "crypto/x509" 5 | "encoding/binary" 6 | "encoding/json" 7 | "errors" 8 | "fmt" 9 | "io" 10 | "io/ioutil" 11 | "os" 12 | 13 | "github.com/IPA-CyberLab/kmgm/pemparser" 14 | "github.com/gofrs/flock" 15 | ) 16 | 17 | var ErrNotExist = errors.New("issuedb: Entry does not exist.") 18 | 19 | type Entry struct { 20 | SerialNumber int64 `json:"sn"` 21 | State State `json:"state"` 22 | CertificatePEM string `json:"certPem"` 23 | } 24 | 25 | func (e *Entry) ParseCertificate() (*x509.Certificate, error) { 26 | pem := []byte(e.CertificatePEM) 27 | certs, err := pemparser.ParseCertificates(pem) 28 | if err != nil { 29 | return nil, err 30 | } 31 | if len(certs) != 1 { 32 | return nil, fmt.Errorf("issuedb: Expected 1 cert pem, found %d certs.", len(certs)) 33 | } 34 | return certs[0], nil 35 | } 36 | 37 | func RandInt63(randr io.Reader) (n int64) { 38 | if err := binary.Read(randr, binary.LittleEndian, &n); err != nil { 39 | panic(err) 40 | } 41 | if n < 0 { 42 | n = -n 43 | } 44 | return 45 | } 46 | 47 | type IssueDB struct { 48 | jsonFilePath string 49 | } 50 | 51 | func New(jsonFilePath string) (*IssueDB, error) { 52 | return &IssueDB{ 53 | jsonFilePath: jsonFilePath, 54 | }, nil 55 | } 56 | 57 | func (db *IssueDB) Initialize() error { 58 | fl := flock.New(db.jsonFilePath) 59 | if err := fl.Lock(); err != nil { 60 | return fmt.Errorf("Failed to acquire issuedb flock: %w", err) 61 | } 62 | defer fl.Unlock() 63 | 64 | es, err := db.entriesWithLock() 65 | if err != nil { 66 | return err 67 | } 68 | 69 | if len(es) > 0 { 70 | return fmt.Errorf("Tried to initialize issuedb, but found %d existing entries", len(es)) 71 | } 72 | if err := ioutil.WriteFile(db.jsonFilePath, []byte("[]"), 0644); err != nil { 73 | return err 74 | } 75 | 76 | return nil 77 | } 78 | 79 | func (db *IssueDB) entriesWithLock() ([]Entry, error) { 80 | bs, err := ioutil.ReadFile(db.jsonFilePath) 81 | if err != nil { 82 | if errors.Is(err, os.ErrNotExist) { 83 | return nil, ErrNotExist 84 | } 85 | return nil, fmt.Errorf("Failed to open issuedb json: %w", err) 86 | } 87 | 88 | var es []Entry 89 | if len(bs) != 0 { 90 | if err := json.Unmarshal(bs, &es); err != nil { 91 | return nil, fmt.Errorf("Failed to unmarshal issuedb json: %w", err) 92 | } 93 | } 94 | 95 | return es, nil 96 | } 97 | 98 | func (db *IssueDB) Entries() ([]Entry, error) { 99 | fl := flock.New(db.jsonFilePath) 100 | if err := fl.Lock(); err != nil { 101 | return nil, fmt.Errorf("Failed to acquire issuedb flock: %w", err) 102 | } 103 | defer fl.Unlock() 104 | 105 | return db.entriesWithLock() 106 | } 107 | 108 | func (db *IssueDB) setEntry(ne Entry) error { 109 | fl := flock.New(db.jsonFilePath) 110 | if err := fl.Lock(); err != nil { 111 | return fmt.Errorf("Failed to acquire issuedb flock: %w", err) 112 | } 113 | defer fl.Unlock() 114 | 115 | es, err := db.entriesWithLock() 116 | if err != nil { 117 | return err 118 | } 119 | 120 | found := false 121 | for i, e := range es { 122 | if e.SerialNumber == ne.SerialNumber { 123 | found = true 124 | 125 | if err := validateStateTransfer(e.State, ne.State); err != nil { 126 | return err 127 | } 128 | 129 | es[i] = ne 130 | break 131 | } 132 | } 133 | if !found { 134 | es = append(es, ne) 135 | } 136 | 137 | bs, err := json.MarshalIndent(es, "", " ") 138 | if err != nil { 139 | return err 140 | } 141 | 142 | if err := ioutil.WriteFile(db.jsonFilePath, bs, 0644); err != nil { 143 | return err 144 | } 145 | return nil 146 | } 147 | 148 | func (db *IssueDB) Query(n int64) (Entry, error) { 149 | es, err := db.Entries() 150 | if err != nil { 151 | return Entry{}, err 152 | } 153 | 154 | for _, e := range es { 155 | if e.SerialNumber == n { 156 | return e, nil 157 | } 158 | } 159 | 160 | return Entry{}, ErrNotExist 161 | } 162 | 163 | func (db *IssueDB) AllocateSerialNumber(randr io.Reader) (int64, error) { 164 | // Serial number must be unique and unpredictable. We use a random 63-bit int here. 165 | // (see https://crypto.stackexchange.com/questions/257/unpredictability-of-x-509-serial-number) 166 | 167 | var n int64 168 | for { 169 | n = RandInt63(randr) 170 | 171 | _, err := db.Query(n) 172 | if err != nil { 173 | if errors.Is(err, ErrNotExist) { 174 | break 175 | } 176 | return -1, err 177 | } 178 | // if err == nil { 179 | // there's already an entry, so continue search for next num. 180 | // } 181 | } 182 | 183 | if err := db.setEntry(Entry{SerialNumber: n, State: IssueInProgress}); err != nil { 184 | return -1, err 185 | } 186 | return n, nil 187 | } 188 | 189 | func (db *IssueDB) IssueCertificate(n int64, pem string) error { 190 | if err := db.setEntry(Entry{ 191 | SerialNumber: n, 192 | State: ActiveCertificate, 193 | CertificatePEM: pem, 194 | }); err != nil { 195 | return err 196 | } 197 | return nil 198 | } 199 | -------------------------------------------------------------------------------- /storage/issuedb/issuedb_test.go: -------------------------------------------------------------------------------- 1 | package issuedb_test 2 | 3 | import ( 4 | "crypto/rand" 5 | "io/ioutil" 6 | "os" 7 | "testing" 8 | 9 | "github.com/IPA-CyberLab/kmgm/storage/issuedb" 10 | ) 11 | 12 | func issueDBForTest(t *testing.T) *issuedb.IssueDB { 13 | t.Helper() 14 | 15 | tmpfile, err := ioutil.TempFile("", "issuedb.json") 16 | defer os.Remove(tmpfile.Name()) 17 | if err != nil { 18 | t.Fatalf("ioutil.TempFile: %v", err) 19 | } 20 | 21 | db, err := issuedb.New(tmpfile.Name()) 22 | if err != nil { 23 | t.Fatalf("issuedb.New: %v", err) 24 | } 25 | 26 | return db 27 | } 28 | 29 | func TestAllocateSerialNumber(t *testing.T) { 30 | db := issueDBForTest(t) 31 | 32 | var ns []int64 33 | for i := 0; i < 16; i++ { 34 | n, err := db.AllocateSerialNumber(rand.Reader) 35 | if err != nil { 36 | t.Fatalf("AllocateSerialNumber failed: %v", err) 37 | } 38 | t.Logf("Alloc %d", n) 39 | 40 | ns = append(ns, n) 41 | } 42 | 43 | for i, n := range ns { 44 | e, err := db.Query(n) 45 | if err != nil { 46 | t.Fatalf("%d: Query(%d) failed: %v", i, n, err) 47 | } 48 | if e.State != issuedb.IssueInProgress { 49 | t.Fatalf("%d: Query(%d) entry unexpected: %+v", i, n, e) 50 | } 51 | } 52 | } 53 | 54 | func TestIssueCert(t *testing.T) { 55 | db := issueDBForTest(t) 56 | 57 | n, err := db.AllocateSerialNumber(rand.Reader) 58 | if err != nil { 59 | t.Fatalf("AllocateSerialNumber failed: %v", err) 60 | } 61 | t.Logf("Alloc %d", n) 62 | 63 | pemDummy := "-----BEGIN CERTIFICATE-----\ndummy\n-----END CERTIFICATE-----\n" 64 | 65 | if err := db.IssueCertificate(n, pemDummy); err != nil { 66 | t.Fatalf("IssueCertificate failed: %v", err) 67 | } 68 | 69 | e, err := db.Query(n) 70 | if err != nil { 71 | t.Fatalf("Query(%d) failed: %v", n, err) 72 | } 73 | if e.CertificatePEM != pemDummy { 74 | t.Fatalf("pemdiffer: %v", e.CertificatePEM) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /storage/issuedb/state.go: -------------------------------------------------------------------------------- 1 | package issuedb 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | ) 8 | 9 | type State int 10 | 11 | const ( 12 | IssueInProgress State = iota 13 | ActiveCertificate 14 | MaxState = ActiveCertificate 15 | ) 16 | 17 | func (s State) String() string { 18 | switch s { 19 | case IssueInProgress: 20 | return "issue_in_progress" 21 | case ActiveCertificate: 22 | return "active" 23 | } 24 | return fmt.Sprintf("unknown_state_%d", s) 25 | } 26 | 27 | func (s State) MarshalJSON() ([]byte, error) { 28 | return json.Marshal(s.String()) 29 | } 30 | 31 | func (s *State) UnmarshalJSON(bs []byte) error { 32 | var str string 33 | if err := json.Unmarshal(bs, &str); err != nil { 34 | return fmt.Errorf("Unmarshal issuedb.State string: %w", err) 35 | } 36 | 37 | switch str { 38 | case "issue_in_progress": 39 | *s = IssueInProgress 40 | return nil 41 | case "active": 42 | *s = ActiveCertificate 43 | return nil 44 | default: 45 | return fmt.Errorf("Unknown State %q", str) 46 | } 47 | } 48 | 49 | func validateStateTransfer(current, next State) error { 50 | switch current { 51 | case IssueInProgress: 52 | if next == ActiveCertificate { 53 | return nil 54 | } 55 | return fmt.Errorf("Invalid state transfer issue_in_progress -> %v.", next) 56 | 57 | case ActiveCertificate: 58 | return errors.New("No valid state transfer from ActiveCertificate state.") 59 | 60 | default: 61 | return fmt.Errorf("Unknown current state: %v", current) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /storage/k8ssecret.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "bytes" 5 | "encoding/base64" 6 | "text/template" 7 | ) 8 | 9 | const secretTemplateSource = ` 10 | apiVersion: v1 11 | kind: Secret 12 | type: kubernetes.io/tls │ 13 | metadata: 14 | {{- if .Namespace }} 15 | namespace: {{ .Namespace }} 16 | {{- end }} 17 | name: {{ .Name }} 18 | data: 19 | ca.crt: {{ .CACertBase64 }} 20 | tls.crt: {{ .CertBase64 }} 21 | tls.key: {{ .KeyBase64 }} 22 | ` 23 | 24 | var secretTemplate = template.Must(template.New("secret").Parse(secretTemplateSource)) 25 | 26 | // KubernetesSecretFromCertAndKey creates a k8s secret yaml from given cert and key. 27 | func KubernetesSecretFromCertAndKey(name, namespace string, cacertPem, certPem, keyPem []byte) []byte { 28 | var buf bytes.Buffer 29 | 30 | if err := secretTemplate.Execute(&buf, map[string]interface{}{ 31 | "Name": name, 32 | "Namespace": namespace, 33 | "CACertBase64": base64.StdEncoding.EncodeToString(cacertPem), 34 | "CertBase64": base64.StdEncoding.EncodeToString(certPem), 35 | "KeyBase64": base64.StdEncoding.EncodeToString(keyPem), 36 | }); err != nil { 37 | panic(err) 38 | } 39 | 40 | return buf.Bytes() 41 | } 42 | -------------------------------------------------------------------------------- /storage/storage_test.go: -------------------------------------------------------------------------------- 1 | package storage_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/IPA-CyberLab/kmgm/storage" 8 | ) 9 | 10 | func TestVerifyProfileName(t *testing.T) { 11 | testcases := []struct { 12 | Name string 13 | Valid bool 14 | }{ 15 | {"default", true}, 16 | {"foobar", true}, 17 | {"a", true}, 18 | {"a_b_c", true}, 19 | {"a-b-c", true}, 20 | {".kmgm_server", true}, 21 | {"", false}, 22 | {".", false}, 23 | {"..", false}, 24 | {"./..", false}, 25 | {"日本語", false}, 26 | } 27 | 28 | for _, tc := range testcases { 29 | err := storage.VerifyProfileName(tc.Name) 30 | if tc.Valid { 31 | if err != nil { 32 | t.Errorf("%q expected to be a valid profile name, got %v", tc.Name, err) 33 | } 34 | } else { 35 | if err == nil { 36 | t.Errorf("%q expected to be a invalid profile name, got valid", tc.Name) 37 | } 38 | } 39 | } 40 | } 41 | 42 | const TestCertPem = `-----BEGIN CERTIFICATE----- 43 | MIIE7jCCAtagAwIBAgIBATANBgkqhkiG9w0BAQsFADAXMRUwEwYDVQQDEwx3Y3J5 44 | cHRvIHRlc3QwHhcNMTkxMDE3MTMxMDUxWhcNMjkxMDE0MTMxMTUxWjAXMRUwEwYD 45 | VQQDEwx3Y3J5cHRvIHRlc3QwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC 46 | AQDkVilip3w0ITSUEDsz3KpEgphf8cmCRei9hBScX+kdHVScrlPUMbT0ER+Kh37n 47 | xKumANt2UKapX8OpFC+NTufmD4qxXJ5G9y9imG0adGe09BD3TXPf8BWV+HCkrLUb 48 | il6s2simMkZ4KY/ddTVJawR1eMugNor+UWw344x3kD67uR1PmPOZqj+mbwfmfz0U 49 | hkjGciS3Q+f4hr26XcJyNPGMcufQBOZEn1MQeSnsERlzFItrhV8mUb9CLhwBMREb 50 | c3/fyPFE1oyH/ctfAyjRhBvUn9X+AE09yyMeymhfnr3SeFfujXnvA9cJVhNHDzty 51 | hGoDNICXJWHMIWYbGgRclTjpCVr+Qpe7a/5Da5rtIxF/CacISjDMknyIVKhcWMrx 52 | pcfScvyR3Enrdl9e3GrfmKLmYoGqD6ck1sCXE2BrdMOli+cDmGndpWOOLFAw12YM 53 | 4BuKvliMC0a7Q3zUW3r8elgyDk6mTqrsZquiEjPQ32fnDjQxBOum4yuy9D5fEibE 54 | CUwkHyxpWieI9DBnEqE9BiZymzk6Qry/mvuPbF5i5UNwn6bObDLu0KHJOYf1UAwY 55 | rqFIWJe99nUUg+w0fHrPln5vrDkW8a+sSOgvY9CNVjTIHSbZRF18fML1tOXiLzr7 56 | T6oIjB9G6liVMlncwdiYotU79uwR+S9mtDcIWy7YC3VTKwIDAQABo0UwQzAOBgNV 57 | HQ8BAf8EBAMCAqQwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUR47TuhD5 58 | DSubRMt/80T1kV4KKz4wDQYJKoZIhvcNAQELBQADggIBAHNFc31lTXtyRr6B2IZE 59 | qMDztLZ2BTHiNZ+bXdhiwCu5C79RVuZoyRM18k3b6/B+1g10jFoNwVGNzzok+h3h 60 | OORojGWJA0HjN1drnl2cyWQ/MrdAsjvGcJ5X4jcTnm2mAfrg13OylF+iAqwT4CrJ 61 | 2oakymcQrXW8pGr2YOEVZEicGIzAAAWFFvWxESkGb2mztriEadLCg1t43EAeIFtM 62 | kQD4+scWUsb/6WOfZ3j15Z7+gWfty7h5nkeEItJIpxWwDhVxlWug0GRdJzmZv1UA 63 | wf66DkA6ulNCDiS/n28NFMRJqEuHwa5QV16YsK4W1kXaYOwa7q7qlThhJu9aznrN 64 | HbXUXT4/6+lp2L8zzuWyllrRjAPqFQ/ZEqKw5sIlgwFaU9YP82Ly9IHIPigm0mtL 65 | XthlWat7zkwxt1+QkwC3ADzrY/DfmX3VrYZBUTjJslTlEgz4OhBr+E5UpmOfLYRm 66 | 6NtzcUo9xEYlGuxxLexOXV/tRMZYKuhAPZzQmkuXJarDskewyA4GqdNGIlvFvWbz 67 | HBvlRgSPzKUNromy7v0YihTdYayrJ2gZjokwjaYBeTWVl0+NawtcjsUr9vhVGp94 68 | JsLTgS5vva9pGOWs5PHNiDfx0lIshklgTgr9sNSbIclLMAP0wK8t/U+Ub59EJJBK 69 | GYgjLXpNp4gpO4bQ8ZN2O8Wa 70 | -----END CERTIFICATE-----` 71 | 72 | func TestReadCertificateFile_Inline(t *testing.T) { 73 | inl := fmt.Sprintf("%s%s", storage.InlinePrefix, TestCertPem) 74 | cert, err := storage.ReadCertificateFile(inl) 75 | if err != nil { 76 | t.Errorf("Unexpected err: %v", err) 77 | } 78 | 79 | if cert.Subject.CommonName != "wcrypto test" { 80 | t.Errorf("Unexpected CN: %s", cert.Subject.CommonName) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /structflags/flags.go: -------------------------------------------------------------------------------- 1 | package structflags 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | 7 | "github.com/urfave/cli/v2" 8 | ) 9 | 10 | type Unmarshaler interface { 11 | UnmarshalFlag(string) error 12 | } 13 | 14 | var UnmarshalerType = reflect.TypeOf((*Unmarshaler)(nil)).Elem() 15 | 16 | func populateValueFromCliContext(v reflect.Value, c *cli.Context, parsed *ParsedTag) error { 17 | for v.Kind() == reflect.Ptr { 18 | v = v.Elem() 19 | } 20 | 21 | if v.CanAddr() { 22 | if u, ok := v.Addr().Interface().(Unmarshaler); ok { 23 | if c.IsSet(parsed.Name) { 24 | flagVal := c.String(parsed.Name) 25 | if err := u.UnmarshalFlag(flagVal); err != nil { 26 | return fmt.Errorf("Failed to parse flag %s=%q: %w", parsed.Name, flagVal, err) 27 | } 28 | } 29 | return nil 30 | } 31 | } 32 | if DurationType.AssignableTo(v.Type()) { 33 | if c.IsSet(parsed.Name) { 34 | flagVal := c.Duration(parsed.Name) 35 | v.Set(reflect.ValueOf(flagVal)) 36 | } 37 | return nil 38 | } 39 | 40 | switch v.Kind() { 41 | case reflect.Bool: 42 | if c.IsSet(parsed.Name) { 43 | v.SetBool(true) 44 | } 45 | 46 | case reflect.Int: 47 | if c.IsSet(parsed.Name) { 48 | v.SetInt(int64(c.Int(parsed.Name))) 49 | } 50 | 51 | case reflect.String: 52 | if c.IsSet(parsed.Name) { 53 | v.SetString(c.String(parsed.Name)) 54 | } 55 | 56 | case reflect.Struct: 57 | for i := 0; i < v.NumField(); i++ { 58 | vf := v.Field(i) 59 | tf := v.Type().Field(i) 60 | parsedField := Parse(tf.Tag, parsed) 61 | if parsedField == nil { 62 | continue 63 | } 64 | 65 | if err := populateValueFromCliContext(vf, c, parsedField); err != nil { 66 | return err 67 | } 68 | } 69 | 70 | default: 71 | return fmt.Errorf("Don't know how to populate type %v from commandline flag.", v.Type()) 72 | } 73 | 74 | return nil 75 | } 76 | 77 | func PopulateStructFromCliContext(cfg interface{}, c *cli.Context) error { 78 | return populateValueFromCliContext(reflect.ValueOf(cfg), c, nil) 79 | } 80 | 81 | func populateFlagsFromValue(v reflect.Value, parsed *ParsedTag, fs *[]cli.Flag) error { 82 | for v.Kind() == reflect.Ptr { 83 | v = v.Elem() 84 | } 85 | 86 | if v.Type().Implements(UnmarshalerType) || 87 | v.CanAddr() && v.Addr().Type().Implements(UnmarshalerType) { 88 | // make it a single flag if unmarshallable 89 | *fs = append(*fs, parsed.ToCliFlag(v)) 90 | return nil 91 | } 92 | 93 | if v.Kind() == reflect.Struct { 94 | // recurse if not unmarshallable struct 95 | for i := 0; i < v.NumField(); i++ { 96 | parsedField := Parse(v.Type().Field(i).Tag, parsed) 97 | 98 | // only process fields if tagged 99 | if parsedField == nil { 100 | continue 101 | } 102 | 103 | fv := v.Field(i) 104 | 105 | // if the field value is nil ptr, zero construct a struct 106 | if fv.Type().Kind() == reflect.Ptr && fv.IsNil() { 107 | ft := v.Type().Field(i).Type.Elem() 108 | fv = reflect.New(ft) 109 | } 110 | 111 | if err := populateFlagsFromValue(fv, parsedField, fs); err != nil { 112 | return err 113 | } 114 | } 115 | 116 | return nil 117 | } 118 | 119 | *fs = append(*fs, parsed.ToCliFlag(v)) 120 | return nil 121 | } 122 | 123 | func PopulateFlagsFromStruct(s interface{}) ([]cli.Flag, error) { 124 | var fs []cli.Flag 125 | 126 | if err := populateFlagsFromValue(reflect.ValueOf(s), nil, &fs); err != nil { 127 | return nil, err 128 | } 129 | 130 | return fs, nil 131 | } 132 | 133 | func MustPopulateFlagsFromStruct(s interface{}) []cli.Flag { 134 | flags, err := PopulateFlagsFromStruct(s) 135 | if err != nil { 136 | panic(err) 137 | } 138 | return flags 139 | } 140 | -------------------------------------------------------------------------------- /structflags/tag.go: -------------------------------------------------------------------------------- 1 | package structflags 2 | 3 | import ( 4 | "fmt" 5 | "html" 6 | "log" 7 | "reflect" 8 | "strings" 9 | "time" 10 | 11 | "github.com/urfave/cli/v2" 12 | ) 13 | 14 | type ParsedTag struct { 15 | Name string 16 | Usage string 17 | Aliases []string 18 | Opts map[string]struct{} 19 | } 20 | 21 | func Parse(tag reflect.StructTag, parent *ParsedTag) *ParsedTag { 22 | tagstr, ok := tag.Lookup("flags") 23 | if !ok { 24 | return nil 25 | } 26 | 27 | parsed := &ParsedTag{} 28 | 29 | ss := strings.Split(tagstr, ",") 30 | if len(ss) > 0 { 31 | parsed.Name = ss[0] 32 | if parent != nil && parent.Name != "" { 33 | parsed.Name = fmt.Sprintf("%s.%s", parent.Name, parsed.Name) 34 | } 35 | } 36 | if len(ss) > 1 { 37 | parsed.Usage = html.UnescapeString(ss[1]) 38 | } 39 | if len(ss) > 2 { 40 | aliases := strings.Split(ss[2], ";") 41 | for _, e := range aliases { 42 | if e == "" { 43 | continue 44 | } 45 | parsed.Aliases = append(parsed.Aliases, e) 46 | } 47 | } 48 | if len(ss) > 3 { 49 | parsed.Opts = make(map[string]struct{}) 50 | for _, e := range ss[3:] { 51 | parsed.Opts[e] = struct{}{} 52 | } 53 | } 54 | 55 | return parsed 56 | } 57 | 58 | var DurationType = reflect.TypeOf(time.Duration(0)) 59 | 60 | func (parsed *ParsedTag) ToCliFlag(v reflect.Value) cli.Flag { 61 | _, required := parsed.Opts["required"] 62 | _, hidden := parsed.Opts["hidden"] 63 | 64 | if _, ok := parsed.Opts["duration"]; ok { 65 | return &cli.DurationFlag{ 66 | Name: parsed.Name, 67 | Usage: parsed.Usage, 68 | Aliases: parsed.Aliases, 69 | Required: required, 70 | Hidden: hidden, 71 | // FIXME: default value 72 | } 73 | } 74 | 75 | var defaultValue string 76 | stringLike := false 77 | if v.Kind() == reflect.String { 78 | stringLike = true 79 | defaultValue = v.String() 80 | } else if v.Type().Implements(UnmarshalerType) { 81 | stringLike = true 82 | defaultValue = "" // FIXME 83 | } else if v.CanAddr() && v.Addr().Type().Implements(UnmarshalerType) { 84 | stringLike = true 85 | defaultValue = "" // FIXME 86 | } 87 | if stringLike { 88 | if _, ok := parsed.Opts["path"]; ok { 89 | return &cli.PathFlag{ 90 | Name: parsed.Name, 91 | Usage: parsed.Usage, 92 | Aliases: parsed.Aliases, 93 | Required: required, 94 | Hidden: hidden, 95 | Value: defaultValue, 96 | DefaultText: defaultValue, 97 | } 98 | } else { 99 | return &cli.StringFlag{ 100 | Name: parsed.Name, 101 | Usage: parsed.Usage, 102 | Aliases: parsed.Aliases, 103 | Required: required, 104 | Hidden: hidden, 105 | Value: defaultValue, 106 | DefaultText: defaultValue, 107 | } 108 | } 109 | } 110 | 111 | switch v.Kind() { 112 | case reflect.Bool: 113 | return &cli.BoolFlag{ 114 | Name: parsed.Name, 115 | Usage: parsed.Usage, 116 | Aliases: parsed.Aliases, 117 | Required: required, 118 | Hidden: hidden, 119 | Value: v.Bool(), 120 | } 121 | 122 | case reflect.Int: 123 | return &cli.IntFlag{ 124 | Name: parsed.Name, 125 | Usage: parsed.Usage, 126 | Aliases: parsed.Aliases, 127 | Required: required, 128 | Hidden: hidden, 129 | Value: int(v.Int()), 130 | } 131 | 132 | default: 133 | log.Panicf("ToCliFlag: unknown kind %v", v.Kind()) 134 | return nil 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /testutils/testutils.go: -------------------------------------------------------------------------------- 1 | package testutils 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | "os/exec" 7 | "path" 8 | "regexp" 9 | "testing" 10 | 11 | "github.com/IPA-CyberLab/kmgm/domainname" 12 | "github.com/IPA-CyberLab/kmgm/ipapi" 13 | "go.uber.org/zap/zaptest/observer" 14 | ) 15 | 16 | func init() { 17 | ipapi.MockResult = &ipapi.Result{ 18 | RegionName: "California", 19 | CountryCode: "US", 20 | } 21 | 22 | domainname.MockResult = "host.example" 23 | } 24 | 25 | func PrepareBasedir(t *testing.T) string { 26 | t.Helper() 27 | 28 | basedir, err := os.MkdirTemp("", "kmgm-testdir-*") 29 | if err != nil { 30 | t.Fatal(err) 31 | } 32 | t.Cleanup(func() { os.RemoveAll(basedir) }) 33 | 34 | return basedir 35 | } 36 | 37 | func ExpectEmptyDir(t *testing.T, basedirDummy string) { 38 | t.Helper() 39 | 40 | f, err := os.Open(basedirDummy) 41 | if err != nil { 42 | t.Fatal(err) 43 | } 44 | des, err := f.ReadDir(-1) 45 | if err != nil { 46 | t.Fatal(err) 47 | } 48 | for _, de := range des { 49 | t.Error("Found unexpected file:", de.Name()) 50 | } 51 | } 52 | 53 | func ExpectFile(t *testing.T, basedir string, relpath string) { 54 | t.Helper() 55 | 56 | filepath := path.Join(basedir, relpath) 57 | 58 | if _, err := os.Stat(filepath); err != nil { 59 | if !errors.Is(err, os.ErrNotExist) { 60 | t.Errorf("Unexpected error when Stat(%q): %v", filepath, err) 61 | } 62 | t.Errorf("File %s does not exist.", filepath) 63 | 64 | cmd := exec.Command("tree", basedir) 65 | out, err := cmd.CombinedOutput() 66 | if err != nil { 67 | t.Errorf("Error running tree command: %v", err) 68 | } 69 | t.Logf("Directory structure of %s:\n%s", basedir, out) 70 | return 71 | } 72 | } 73 | 74 | func ExpectFileNotExist(t *testing.T, basedir, relpath string) { 75 | t.Helper() 76 | 77 | filepath := path.Join(basedir, relpath) 78 | 79 | if _, err := os.Stat(filepath); err != nil { 80 | if errors.Is(err, os.ErrNotExist) { 81 | return 82 | } 83 | t.Errorf("Unexpected error when Stat(%q): %v", filepath, err) 84 | return 85 | } 86 | t.Errorf("File %s exists while it shouldn't.", filepath) 87 | } 88 | 89 | func ExpectLogMessage(t *testing.T, logs *observer.ObservedLogs, expectedRE string) { 90 | t.Helper() 91 | 92 | re := regexp.MustCompile(expectedRE) 93 | for _, l := range logs.All() { 94 | if re.MatchString(l.Message) { 95 | return 96 | } 97 | } 98 | 99 | t.Errorf("Could not find a log line that matches %s", expectedRE) 100 | } 101 | 102 | func ExpectErrMessage(t *testing.T, err error, expectedRE string) { 103 | t.Helper() 104 | 105 | if err == nil { 106 | t.Errorf("No error occured while expecting error message that match %s", expectedRE) 107 | return 108 | } 109 | 110 | re := regexp.MustCompile(expectedRE) 111 | 112 | msg := err.Error() 113 | if re.MatchString(msg) { 114 | return 115 | } 116 | 117 | t.Errorf("Error message %q doesn't match %s", msg, expectedRE) 118 | } 119 | 120 | func ExpectErr(t *testing.T, actual, expected error) { 121 | t.Helper() 122 | 123 | if errors.Is(actual, expected) { 124 | return 125 | } 126 | if expected == nil { 127 | t.Errorf("Expected no error but got error: %v", actual) 128 | return 129 | } 130 | t.Errorf("Expected err %v, but got err: %v", expected, actual) 131 | } 132 | 133 | func RemoveExistingFile(t *testing.T, fpath string) { 134 | if err := os.Remove(fpath); err != nil { 135 | t.Errorf("os.Remove(%q) failed: %v", fpath, err) 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /tools/demoenv/bashrc: -------------------------------------------------------------------------------- 1 | export LANG=en_US.UTF-8 2 | export HOSTNAME=demohost 3 | 4 | PS1="$ " 5 | 6 | export PATH="$(pwd)/../..:$PATH" 7 | 8 | rm -rf /tmp/kmgmdemo 9 | export KMGMDIR=/tmp/kmgmdemo/.config/kmgm 10 | mkdir -p $KMGMDIR 11 | echo '{"city":"Bunkyo","country":"Japan","countryCode":"JP","regionName":"Tokyo","timezone":"Asia/Tokyo"}' > $KMGMDIR/geoip_cache.json 12 | 13 | export KMGM_DEFAULT_NAMES="demohost,demohost.example,192.168.0.10" 14 | cd /tmp/kmgmdemo 15 | -------------------------------------------------------------------------------- /tools/demoenv/demoenv.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | cd $(dirname $0) 3 | mkdir -p /tmp/kmgmdemo/.config/kmgm 4 | 5 | termtosvg -t window_frame -g 120x30 -c ./sh_wrap.sh 6 | -------------------------------------------------------------------------------- /tools/demoenv/sh_wrap.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | exec bash --rcfile bashrc 3 | -------------------------------------------------------------------------------- /version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | var ( 4 | Version = "0.0.0" 5 | Commit = "gitcommit-unavailable" 6 | ) 7 | -------------------------------------------------------------------------------- /wcrypto/cert.go: -------------------------------------------------------------------------------- 1 | package wcrypto 2 | 3 | import ( 4 | "crypto" 5 | "crypto/x509" 6 | "fmt" 7 | "time" 8 | ) 9 | 10 | func VerifyCACert(cert *x509.Certificate, t time.Time) error { 11 | certpool := x509.NewCertPool() 12 | certpool.AddCert(cert) 13 | 14 | if _, err := cert.Verify(x509.VerifyOptions{ 15 | Roots: certpool, 16 | KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny}, 17 | CurrentTime: t, 18 | }); err != nil { 19 | return err 20 | } 21 | 22 | // FIXME[P1]: check BasicConstraints 23 | // FIXME[P1]: check KeyUsage 24 | // FIXME[P3]: check pathlen? 25 | 26 | return nil 27 | } 28 | 29 | func VerifyServerCert(cert *x509.Certificate, cacert *x509.Certificate, t time.Time) error { 30 | certpool := x509.NewCertPool() 31 | certpool.AddCert(cacert) 32 | 33 | if _, err := cert.Verify(x509.VerifyOptions{ 34 | Roots: certpool, 35 | KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, 36 | CurrentTime: t, 37 | }); err != nil { 38 | return err 39 | } 40 | 41 | // FIXME[P1]: check BasicConstraints 42 | // FIXME[P1]: check KeyUsage 43 | // FIXME[P3]: check pathlen? 44 | 45 | return nil 46 | } 47 | 48 | func VerifyCACertAndKey(priv crypto.PrivateKey, cert *x509.Certificate, t time.Time) error { 49 | if err := VerifyCACert(cert, t); err != nil { 50 | return err 51 | } 52 | 53 | if privv, ok := priv.(interface { 54 | Validate() error 55 | }); ok { 56 | if err := privv.Validate(); err != nil { 57 | return fmt.Errorf("private key: %w", err) 58 | } 59 | } 60 | 61 | priv2pub, err := ExtractPublicKey(priv) 62 | if err != nil { 63 | return err 64 | } 65 | 66 | if err := VerifyPublicKeyMatch(cert.PublicKey, priv2pub); err != nil { 67 | return fmt.Errorf("The given private key is not for the given CA cert: %w", err) 68 | } 69 | 70 | return nil 71 | } 72 | -------------------------------------------------------------------------------- /wcrypto/key.go: -------------------------------------------------------------------------------- 1 | package wcrypto 2 | 3 | import ( 4 | "crypto" 5 | "crypto/ecdsa" 6 | "crypto/elliptic" 7 | "crypto/rsa" 8 | "crypto/sha1" 9 | "crypto/sha256" 10 | "crypto/x509" 11 | "encoding/asn1" 12 | "encoding/base64" 13 | "errors" 14 | "fmt" 15 | "io" 16 | "reflect" 17 | "time" 18 | 19 | "go.uber.org/zap" 20 | ) 21 | 22 | var ErrKeyAnyForGenerateKey = errors.New("KeyAny is not a valid keytype for wcrypto.GenerateKey") //nolint 23 | 24 | func GenerateKey(randr io.Reader, ktype KeyType, usage string, logger *zap.Logger) (crypto.PrivateKey, error) { 25 | slog := logger.Sugar() 26 | start := time.Now() 27 | 28 | slog.Infow("Generating key...", "usage", usage, "type", ktype) 29 | defer func() { 30 | slog.Infow("Generating key... Done.", "usage", usage, "type", ktype, "took", time.Since(start)) 31 | }() 32 | 33 | switch ktype { 34 | case KeyRSA2048: 35 | priv, err := rsa.GenerateKey(randr, 2048) 36 | if err != nil { 37 | return nil, fmt.Errorf("rsa.GenerateKey: %w", err) 38 | } 39 | return priv, nil 40 | 41 | case KeyRSA4096: 42 | priv, err := rsa.GenerateKey(randr, 4096) 43 | if err != nil { 44 | return nil, fmt.Errorf("rsa.GenerateKey: %w", err) 45 | } 46 | return priv, nil 47 | 48 | case KeySECP256R1: 49 | priv, err := ecdsa.GenerateKey(elliptic.P256(), randr) 50 | if err != nil { 51 | return nil, fmt.Errorf("ecdsa.GenerateKey: %w", err) 52 | } 53 | return priv, nil 54 | 55 | case KeyAny: 56 | return nil, ErrKeyAnyForGenerateKey 57 | 58 | default: 59 | return nil, fmt.Errorf("unknown key type: %v", ktype) 60 | } 61 | } 62 | 63 | func ExtractPublicKey(priv crypto.PrivateKey) (crypto.PublicKey, error) { 64 | privp, ok := priv.(interface { 65 | Public() crypto.PublicKey 66 | }) 67 | if !ok { 68 | return nil, errors.New("could not extract public key from private key") 69 | } 70 | pub := privp.Public() 71 | return pub, nil 72 | } 73 | 74 | func errTypeMismatch(a, b interface{}) error { 75 | return fmt.Errorf("Type mismatch: %v and %v", reflect.TypeOf(a), reflect.TypeOf(b)) 76 | } 77 | 78 | var ErrPublicKeyMismatch = errors.New("public keys do not match") 79 | 80 | func VerifyPublicKeyMatch(a, b crypto.PublicKey) error { 81 | switch at := a.(type) { 82 | case *rsa.PublicKey: 83 | bt, ok := b.(*rsa.PublicKey) 84 | if !ok { 85 | return errTypeMismatch(a, b) 86 | } 87 | if at.N.Cmp(bt.N) != 0 || at.E != bt.E { 88 | return ErrPublicKeyMismatch 89 | } 90 | case *ecdsa.PublicKey: 91 | bt, ok := b.(*ecdsa.PublicKey) 92 | if !ok { 93 | return errTypeMismatch(a, b) 94 | } 95 | if at.X.Cmp(bt.X) != 0 || at.Y.Cmp(bt.Y) != 0 { 96 | return ErrPublicKeyMismatch 97 | } 98 | default: 99 | return fmt.Errorf("Unknown public key type: %v", reflect.TypeOf(a)) 100 | } 101 | return nil 102 | } 103 | 104 | // PubKeyPinString extracts the SHA256 hash for use of curl`s --pinnedpubkey commandline option. 105 | func PubKeyPinString(pub crypto.PublicKey) (string, error) { 106 | pkix, err := x509.MarshalPKIXPublicKey(pub) 107 | if err != nil { 108 | return "", err 109 | } 110 | 111 | hash := sha256.Sum256(pkix) 112 | str := base64.StdEncoding.EncodeToString(hash[:]) 113 | return str, nil 114 | } 115 | 116 | func SubjectKeyIdFromPubkey(pub crypto.PublicKey) ([]byte, error) { 117 | pkix, err := x509.MarshalPKIXPublicKey(pub) 118 | if err != nil { 119 | return nil, err 120 | } 121 | 122 | var target struct { 123 | Algorithm asn1.RawValue 124 | SubjectPublicKey asn1.BitString 125 | } 126 | if _, err := asn1.Unmarshal(pkix, &target); err != nil { 127 | return nil, err 128 | } 129 | 130 | ski := sha1.Sum(target.SubjectPublicKey.Bytes) 131 | return ski[:], nil 132 | } 133 | -------------------------------------------------------------------------------- /wcrypto/keytype.go: -------------------------------------------------------------------------------- 1 | package wcrypto 2 | 3 | import ( 4 | "crypto" 5 | "crypto/ecdsa" 6 | "crypto/rsa" 7 | "fmt" 8 | "reflect" 9 | ) 10 | 11 | type KeyType int 12 | 13 | // Keep this in sync with pb.KeyType 14 | const ( 15 | KeyAny KeyType = iota 16 | KeyRSA4096 17 | KeySECP256R1 18 | KeyRSA2048 19 | ) 20 | 21 | var DefaultKeyType = KeyRSA4096 22 | var ServerKeyType = KeySECP256R1 23 | 24 | func (kt KeyType) String() string { 25 | switch kt { 26 | case KeyAny: 27 | return "any" 28 | case KeyRSA4096: 29 | return "rsa" 30 | case KeySECP256R1: 31 | return "ecdsa" 32 | case KeyRSA2048: 33 | return "rsa2048" 34 | default: 35 | return fmt.Sprintf("unknown_keytype%d", int(kt)) 36 | } 37 | } 38 | 39 | func KeyTypeFromString(s string) (KeyType, error) { 40 | switch s { 41 | case "any": 42 | return KeyAny, nil 43 | case "rsa": 44 | return KeyRSA4096, nil 45 | case "rsa2048": 46 | return KeyRSA2048, nil 47 | case "secp256r1", "ecdsa": 48 | return KeySECP256R1, nil 49 | default: 50 | return KeyRSA4096, fmt.Errorf("Unknown key type %q.", s) 51 | } 52 | } 53 | 54 | func KeyTypeOfPub(pub crypto.PublicKey) (KeyType, error) { 55 | switch p := pub.(type) { 56 | case *rsa.PublicKey: 57 | bitlen := p.N.BitLen() 58 | switch bitlen { 59 | case 2048: 60 | return KeyRSA2048, nil 61 | case 4096: 62 | return KeyRSA4096, nil 63 | default: 64 | return KeyAny, fmt.Errorf("rsa.PublicKey with unsupported key size of %d", bitlen) 65 | } 66 | 67 | case *ecdsa.PublicKey: 68 | curven := p.Curve.Params().Name 69 | switch curven { 70 | case "P-256": 71 | return KeySECP256R1, nil 72 | default: 73 | return KeyAny, fmt.Errorf("ecdsa.PublicKey with unsupported curve %q", curven) 74 | } 75 | 76 | default: 77 | return KeyAny, fmt.Errorf("Unknown public key type: %v", reflect.TypeOf(pub)) 78 | } 79 | } 80 | 81 | type UnexpectedKeyTypeErr struct { 82 | Expected KeyType 83 | Actual KeyType 84 | } 85 | 86 | func (e UnexpectedKeyTypeErr) Error() string { 87 | return fmt.Sprintf("Expected key type of %s but specified key %s", e.Expected, e.Actual) 88 | } 89 | 90 | func (UnexpectedKeyTypeErr) Is(target error) bool { 91 | _, ok := target.(UnexpectedKeyTypeErr) 92 | return ok 93 | } 94 | 95 | func (expected KeyType) CompatibleWith(actual KeyType) error { 96 | if expected == KeyAny { 97 | return nil 98 | } 99 | if expected != actual { 100 | return UnexpectedKeyTypeErr{Expected: expected, Actual: actual} 101 | } 102 | return nil 103 | } 104 | 105 | func (p *KeyType) UnmarshalFlag(s string) error { 106 | kt, err := KeyTypeFromString(s) 107 | if err != nil { 108 | return err 109 | } 110 | 111 | *p = kt 112 | return nil 113 | } 114 | 115 | func (p *KeyType) UnmarshalYAML(unmarshal func(interface{}) error) error { 116 | var s string 117 | if err := unmarshal(&s); err != nil { 118 | return err 119 | } 120 | 121 | kt, err := KeyTypeFromString(s) 122 | if err != nil { 123 | return err 124 | } 125 | 126 | *p = kt 127 | return nil 128 | } 129 | -------------------------------------------------------------------------------- /wcrypto/token.go: -------------------------------------------------------------------------------- 1 | package wcrypto 2 | 3 | import ( 4 | "encoding/base64" 5 | "fmt" 6 | "io" 7 | "time" 8 | 9 | "go.uber.org/zap" 10 | ) 11 | 12 | const TokenBitsLength = 6 /* bits / Base64 chr */ * 4 /* base64 block size */ * 3 13 | 14 | func GenBase64Token(randr io.Reader, logger *zap.Logger) (string, error) { 15 | slog := logger.Sugar() 16 | 17 | start := time.Now() 18 | slog.Infow("Generating token...") 19 | defer slog.Infow("Generating token... Done.", "took", time.Since(start)) 20 | 21 | buf := make([]byte, TokenBitsLength/8) 22 | if _, err := io.ReadFull(randr, buf); err != nil { 23 | return "", fmt.Errorf("Failed to generate token bits: %w", err) 24 | } 25 | 26 | token := base64.StdEncoding.EncodeToString(buf) 27 | return token, nil 28 | } 29 | --------------------------------------------------------------------------------