├── .github ├── labeler.yml └── workflows │ ├── greetings.yml │ ├── stale.yml │ ├── label.yml │ ├── licensed.yml │ ├── workflow.yaml │ └── release-new-action-version.yml ├── go.mod ├── .licensed.yml ├── .gitignore ├── cmd ├── xca.go ├── version.go ├── root.go ├── createca.go ├── sign.go └── info.go ├── examples └── default.conf ├── SECURITY.md ├── go.sum ├── ca ├── common_test.go ├── ca.go ├── common.go ├── base.go └── tls.go ├── version.go ├── README.md ├── Makefile └── LICENSE /.github/labeler.yml: -------------------------------------------------------------------------------- 1 | # https://github.com/actions/labeler/blob/master/README.md 2 | 3 | code: 4 | - ./gsync/* 5 | 6 | license: 7 | - ./LICENSE 8 | 9 | actions: 10 | - .github/* 11 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module go.xiexianbin.cn/xca 2 | 3 | go 1.21 4 | 5 | require github.com/spf13/cobra v1.8.0 6 | 7 | require ( 8 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 9 | github.com/spf13/pflag v1.0.5 // indirect 10 | ) 11 | -------------------------------------------------------------------------------- /.licensed.yml: -------------------------------------------------------------------------------- 1 | sources: 2 | bundler: true 3 | 4 | allowed: 5 | - apache-2.0 6 | - bsd-2-clause 7 | - bsd-3-clause 8 | - isc 9 | - mit 10 | - cc0-1.0 11 | - unlicense 12 | 13 | reviewed: 14 | bundler: 15 | - pathname-common_prefix 16 | - racc 17 | - reverse_markdown 18 | - ruby2_keywords 19 | -------------------------------------------------------------------------------- /.github/workflows/greetings.yml: -------------------------------------------------------------------------------- 1 | name: Greetings 2 | 3 | on: [pull_request, issues] 4 | 5 | jobs: 6 | greeting: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/first-interaction@v1 10 | continue-on-error: true 11 | with: 12 | repo-token: "${{ secrets.GITHUB_TOKEN }}" 13 | issue-message: "# Welcome.\nThanks for your issue." 14 | pr-message: "Thanks for you pr." 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | .DS_Store 17 | .history/ 18 | .idea/ 19 | .lh/ 20 | bin/ 21 | vendor 22 | x-ca 23 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: Mark stale issues and pull requests 2 | 3 | on: 4 | schedule: 5 | - cron: "0 0 * * *" 6 | 7 | jobs: 8 | stale: 9 | 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/stale@v1 14 | with: 15 | repo-token: ${{ secrets.GITHUB_TOKEN }} 16 | stale-issue-message: 'Stale issue message' 17 | stale-pr-message: 'Stale pull request message' 18 | stale-issue-label: 'no-issue-activity' 19 | stale-pr-label: 'no-pr-activity' 20 | -------------------------------------------------------------------------------- /.github/workflows/label.yml: -------------------------------------------------------------------------------- 1 | # This workflow will triage pull requests and apply a label based on the 2 | # paths that are modified in the pull request. 3 | # 4 | # To use this workflow, you will need to set up a .github/labeler.yml 5 | # file with configuration. For more information, see: 6 | # https://github.com/actions/labeler/blob/master/README.md 7 | 8 | name: Labeler 9 | on: [pull_request] 10 | 11 | jobs: 12 | label: 13 | 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/labeler@v4 18 | with: 19 | repo-token: "${{ secrets.GITHUB_TOKEN }}" 20 | -------------------------------------------------------------------------------- /cmd/xca.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2025 xiexianbin.cn 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package main 15 | 16 | func main() { 17 | initCommands() 18 | Execute() 19 | } 20 | -------------------------------------------------------------------------------- /examples/default.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 443 ssl http2 default_server; 3 | listen [::]:443 ssl http2 default_server; 4 | server_name _; 5 | root /usr/share/nginx/html; 6 | 7 | ssl_certificate "/etc/pki/nginx/server.crt"; 8 | ssl_certificate_key "/etc/pki/nginx/private/server.key"; 9 | ssl_session_cache shared:SSL:1m; 10 | ssl_session_timeout 10m; 11 | ssl_ciphers HIGH:!aNULL:!MD5; 12 | ssl_prefer_server_ciphers on; 13 | 14 | location / { 15 | } 16 | 17 | error_page 404 /404.html; 18 | location = /40x.html { 19 | } 20 | 21 | error_page 500 502 503 504 /50x.html; 22 | location = /50x.html { 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | If you have discovered a security vulnerability in this project, please report it 4 | privately. **Do not disclose it as a public issue.** This gives us time to work with you 5 | to fix the issue before public exposure, reducing the chance that the exploit will be 6 | used before a patch is released. 7 | 8 | You may submit the report in the following ways: 9 | 10 | - send an email to me@xiexianbin.cn 11 | - send us a [private vulnerability report](https://github.com/x-ca/go-ca/security/advisories/new) 12 | 13 | Please provide the following information in your report: 14 | 15 | - A description of the vulnerability and its impact 16 | - How to reproduce the issue 17 | 18 | We ask that you give us 90 days to work on a fix before public exposure. 19 | -------------------------------------------------------------------------------- /.github/workflows/licensed.yml: -------------------------------------------------------------------------------- 1 | name: Licensed 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | reason: 7 | description: 'run license action reason' 8 | required: false 9 | type: string 10 | default: 'manually test' 11 | push: 12 | branches: 13 | - main 14 | pull_request: 15 | branches: 16 | - main 17 | 18 | jobs: 19 | test: 20 | runs-on: ubuntu-latest 21 | name: Check licenses 22 | steps: 23 | - uses: actions/checkout@v4 24 | - name: Install licensed 25 | run: | 26 | cd $RUNNER_TEMP 27 | curl -Lfs -o licensed.tar.gz https://github.com/github/licensed/releases/download/3.4.4/licensed-3.4.4-linux-x64.tar.gz 28 | sudo tar -xzf licensed.tar.gz 29 | sudo mv licensed /usr/local/bin/licensed 30 | - run: licensed status 31 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 2 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 3 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 4 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 5 | github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= 6 | github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= 7 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 8 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 9 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 10 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 11 | -------------------------------------------------------------------------------- /ca/common_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022 xiexianbin.cn 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package ca 15 | 16 | import "testing" 17 | 18 | func TestParseDomains(t *testing.T) { 19 | _, err := ParseDomains([]string{"www.xiexianbin.cn", "*.xiexianbin.cn"}) 20 | if err != nil { 21 | t.Errorf(err.Error()) 22 | } 23 | } 24 | 25 | func TestParseIPs(t *testing.T) { 26 | _, err := ParseIPs([]string{"1.1.1.1", "8.8.8.8"}) 27 | if err != nil { 28 | t.Errorf(err.Error()) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /cmd/version.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2025 xiexianbin.cn 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package main 15 | 16 | import ( 17 | "github.com/spf13/cobra" 18 | xca "go.xiexianbin.cn/xca" 19 | ) 20 | 21 | // versionCmd represents the version command 22 | var versionCmd = &cobra.Command{ 23 | Use: "version", 24 | Short: "Show version information", 25 | Long: `Show version information for XCA including build date, git commit, and platform details.`, 26 | Run: func(cmd *cobra.Command, args []string) { 27 | v := xca.GetVersion() 28 | xca.PrintVersion("xca", v, false) 29 | }, 30 | } 31 | -------------------------------------------------------------------------------- /.github/workflows/workflow.yaml: -------------------------------------------------------------------------------- 1 | name: workflow 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | reason: 6 | description: 'run action reason' 7 | required: false 8 | type: string 9 | default: 'manually test' 10 | push: 11 | branches: 12 | - main 13 | - dev 14 | - bug/** 15 | - fix/** 16 | - bugfix/** 17 | - feature/** 18 | - release/** 19 | paths-ignore: 20 | - '**.md' 21 | pull_request: 22 | paths-ignore: 23 | - '**.md' 24 | 25 | jobs: 26 | build: 27 | runs-on: ubuntu-latest 28 | strategy: 29 | matrix: 30 | go: ['1.21.13', '1.22.12', '1.23.11', '1.24.5'] 31 | name: Go ${{ matrix.go }} test 32 | steps: 33 | - name: Checkout 34 | uses: actions/checkout@v4 35 | 36 | - name: Setup Go 37 | uses: actions/setup-go@v5 38 | with: 39 | go-version: ${{ matrix.go }} 40 | 41 | - name: Check Go Version and Install Go Dep 42 | run: | 43 | go version 44 | go mod vendor 45 | 46 | - name: Test 47 | run: make test && make build 48 | -------------------------------------------------------------------------------- /.github/workflows/release-new-action-version.yml: -------------------------------------------------------------------------------- 1 | name: Release new action version 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | reason: 6 | description: 'run release action reason' 7 | required: false 8 | type: string 9 | default: 'manually test' 10 | push: 11 | tags: 12 | - 'v*.*.*' 13 | 14 | permissions: 15 | contents: write 16 | 17 | jobs: 18 | release: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@v4 23 | 24 | - name: Setup Go 25 | uses: actions/setup-go@v5 26 | 27 | - name: Check Go Version and Install Go Dep 28 | run: | 29 | go version 30 | go mod vendor 31 | 32 | - name: Build 33 | run: make all 34 | 35 | - name: Generate Release.txt 36 | run: | 37 | echo ${{ github.sha }} > Release.txt 38 | cat Release.txt 39 | 40 | - name: Release 41 | uses: softprops/action-gh-release@v1 42 | if: startsWith(github.ref, 'refs/tags/') 43 | with: 44 | files: | 45 | bin/xca-linux-amd64 46 | bin/xca-linux-arm64 47 | bin/xca-linux-ppc64le 48 | bin/xca-linux-s390x 49 | bin/xca-darwin-amd64 50 | bin/xca-darwin-arm64 51 | bin/xca-windows-amd64.exe 52 | Release.txt 53 | LICENSE 54 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022 xiexianbin.cn 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package main 15 | 16 | import ( 17 | "fmt" 18 | "os" 19 | 20 | "github.com/spf13/cobra" 21 | ) 22 | 23 | // rootCmd represents the base command 24 | var rootCmd = &cobra.Command{ 25 | Use: "xca", 26 | Short: "X Certificate Authority management tool", 27 | Long: `XCA is a command-line tool for creating and managing Root/Second-Level Certificate Authorities (CAs) 28 | and signing certificates for domains and IP addresses. 29 | 30 | Available Commands: 31 | create-ca Create root and TLS CA certificates 32 | info Display information about Certificates 33 | sign Sign a certificate for domains and/or IPs 34 | version Show version information 35 | 36 | Environment: 37 | XCA_ROOT_PATH Which path to store Root/Second-Level/TLS cert, default is "$(pwd)/x-ca" 38 | 39 | Examples: 40 | xca create-ca --key-type ec --curve P256 41 | xca sign example.com --domains "example.com,www.example.com" 42 | xca sign 192.168.1.1 --ips "192.168.1.1" 43 | 44 | Source Code: 45 | https://github.com/x-ca/go-ca `, 46 | Run: func(cmd *cobra.Command, args []string) { 47 | // If no subcommand, show help 48 | cmd.Help() 49 | }, 50 | } 51 | 52 | func init() { 53 | // Add subcommands 54 | rootCmd.AddCommand(createCaCmd) 55 | rootCmd.AddCommand(infoCmd) 56 | rootCmd.AddCommand(signCmd) 57 | rootCmd.AddCommand(versionCmd) 58 | } 59 | 60 | func initCommands() { 61 | initCreateCACmd() 62 | initSignCmd() 63 | } 64 | 65 | func Execute() { 66 | if err := rootCmd.Execute(); err != nil { 67 | fmt.Println(err) 68 | os.Exit(1) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /version.go: -------------------------------------------------------------------------------- 1 | package xca 2 | 3 | import ( 4 | "fmt" 5 | "runtime" 6 | ) 7 | 8 | var ( 9 | version = "v0.0.0" // value from VERSION file 10 | buildDate = "1970-01-01T00:00:00Z" // output from `TZ=UTC-8 date +'%Y-%m-%dT%H:%M:%SZ+08:00'` 11 | gitCommit = "" // output from `git rev-parse HEAD` 12 | gitTag = "" // output from `git describe --exact-match --tags HEAD` (if clean tree state) 13 | gitTreeState = "" // determined from `git status --porcelain`. either 'clean' or 'dirty' 14 | ) 15 | 16 | type Version struct { 17 | Version string `json:"version"` 18 | BuildDate string `json:"buildDate"` 19 | GitCommit string `json:"gitCommit"` 20 | GitTag string `json:"gitTag"` 21 | GitTreeState string `json:"gitTreeState"` 22 | GoVersion string `json:"goVersion"` 23 | Compiler string `json:"compiler"` 24 | Platform string `json:"platform"` 25 | } 26 | 27 | // GetVersion returns the version information 28 | func GetVersion() Version { 29 | var versionStr string 30 | if gitCommit != "" && gitTag != "" && gitTreeState == "clean" { 31 | // if we have a clean tree state and the current commit is tagged, 32 | // this is an official release. 33 | versionStr = gitTag 34 | } else { 35 | // otherwise formulate a version string based on as much metadata 36 | // information we have available. 37 | versionStr = version 38 | if len(gitCommit) >= 7 { 39 | versionStr += "+" + gitCommit[0:7] 40 | if gitTreeState != "clean" { 41 | versionStr += ".dirty" 42 | } 43 | } else { 44 | versionStr += "+unknown" 45 | } 46 | } 47 | return Version{ 48 | Version: versionStr, 49 | BuildDate: buildDate, 50 | GitCommit: gitCommit, 51 | GitTag: gitTag, 52 | GitTreeState: gitTreeState, 53 | GoVersion: runtime.Version(), 54 | Compiler: runtime.Compiler, 55 | Platform: fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH), 56 | } 57 | } 58 | 59 | func PrintVersion(cliName string, version Version, short bool) { 60 | fmt.Printf("%s(https://github.com/x-ca/go-ca): %s\n", cliName, version.Version) 61 | if short { 62 | return 63 | } 64 | fmt.Printf(" BuildDate: %s\n", version.BuildDate) 65 | fmt.Printf(" GitCommit: %s\n", version.GitCommit) 66 | fmt.Printf(" GitTreeState: %s\n", version.GitTreeState) 67 | if version.GitTag != "" { 68 | fmt.Printf(" GitTag: %s\n", version.GitTag) 69 | } 70 | fmt.Printf(" GoVersion: %s\n", version.GoVersion) 71 | fmt.Printf(" Compiler: %s\n", version.Compiler) 72 | fmt.Printf(" Platform: %s\n", version.Platform) 73 | } 74 | -------------------------------------------------------------------------------- /cmd/createca.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022 xiexianbin.cn 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package main 15 | 16 | import ( 17 | "fmt" 18 | "os" 19 | 20 | "github.com/spf13/cobra" 21 | 22 | "go.xiexianbin.cn/xca/ca" 23 | ) 24 | 25 | var ( 26 | createCaRootCert string 27 | createCaRootKey string 28 | createCaTlsCert string 29 | createCaTlsKey string 30 | createCaTlsChain string 31 | createCaKeyType string 32 | createCaKeyBits int 33 | createCaCurve string 34 | ) 35 | 36 | // createCaCmd represents the create-ca command 37 | var createCaCmd = &cobra.Command{ 38 | Use: "create-ca", 39 | Short: "Create root and TLS CA certificates", 40 | Long: `Create a new root CA and TLS CA with the specified parameters. 41 | 42 | Examples: 43 | xca create-ca --key-type ec --curve P256 44 | xca create-ca --root-cert custom-root.crt --root-key custom-root.key 45 | xca create-ca --key-type rsa --key-bits 4096`, 46 | Run: func(cmd *cobra.Command, args []string) { 47 | if err := runCreateCa(); err != nil { 48 | fmt.Printf("Error: %v\n", err) 49 | os.Exit(1) 50 | } 51 | }, 52 | } 53 | 54 | func initCreateCACmd() { 55 | xcarootpath := ca.GetEnvDefault(ca.XCARootPath, "x-ca") 56 | createCaCmd.Flags().StringVar(&createCaRootCert, "root-cert", xcarootpath+"/ca/root-ca.crt", "Root certificate file path") 57 | createCaCmd.Flags().StringVar(&createCaRootKey, "root-key", xcarootpath+"/ca/root-ca/private/root-ca.key", "Root private key file path") 58 | createCaCmd.Flags().StringVar(&createCaTlsCert, "tls-cert", xcarootpath+"/ca/tls-ca.crt", "TLS certificate file path") 59 | createCaCmd.Flags().StringVar(&createCaTlsKey, "tls-key", xcarootpath+"/ca/tls-ca/private/tls-ca.key", "TLS private key file path") 60 | createCaCmd.Flags().StringVar(&createCaTlsChain, "tls-chain", xcarootpath+"/ca/tls-ca-chain.pem", "TLS CA chain file path") 61 | createCaCmd.Flags().StringVar(&createCaKeyType, "key-type", ca.DefaultKeyType, "Key type (rsa or ec)") 62 | createCaCmd.Flags().IntVar(&createCaKeyBits, "key-bits", ca.DefaultKeyBits, "RSA key bits") 63 | createCaCmd.Flags().StringVar(&createCaCurve, "curve", ca.DefaultCurve, "EC curve (P224, P256, P384, P521)") 64 | } 65 | 66 | func runCreateCa() error { 67 | // Check if files already exist 68 | files := []string{createCaRootKey, createCaRootCert, createCaTlsKey, createCaTlsCert} 69 | for _, file := range files { 70 | if exists, _ := ca.CheckFileExists(file); exists { 71 | return fmt.Errorf("%s already exists", file) 72 | } 73 | } 74 | 75 | // Create root CA 76 | rootCA, err := ca.NewRootCA(createCaKeyType, createCaKeyBits, createCaCurve) 77 | if err != nil { 78 | return fmt.Errorf("failed to create root CA: %w", err) 79 | } 80 | 81 | // Write root CA 82 | if err := rootCA.Write(createCaRootKey, createCaRootCert, ""); err != nil { 83 | return fmt.Errorf("failed to write root CA: %w", err) 84 | } 85 | 86 | // Create TLS CA 87 | tlsCA, err := ca.NewTLSCA(createCaKeyType, createCaKeyBits, createCaCurve, rootCA.Cert, rootCA.Key) 88 | if err != nil { 89 | return fmt.Errorf("failed to create TLS CA: %w", err) 90 | } 91 | 92 | // Write TLS CA 93 | if err := tlsCA.Write(createCaTlsKey, createCaTlsCert, createCaTlsChain); err != nil { 94 | return fmt.Errorf("failed to write TLS CA: %w", err) 95 | } 96 | 97 | fmt.Println("Successfully created root and TLS CA certificates") 98 | return nil 99 | } 100 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-ca 2 | 3 | [![build](https://github.com/x-ca/go-ca/actions/workflows/workflow.yaml/badge.svg)](https://github.com/x-ca/go-ca/actions/workflows/workflow.yaml) 4 | [![GoDoc](https://godoc.org/github.com/x-ca/go-ca?status.svg)](https://pkg.go.dev/github.com/x-ca/go-ca) 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/x-ca/go-ca)](https://goreportcard.com/report/github.com/x-ca/go-ca) 6 | 7 | golang x-ca client, which can simple Sign Self Root/Second-Level CA, and sign for Domains and IPs. 8 | 9 | - shell implement at [x-ca/x-ca](https://github.com/x-ca/x-ca) 10 | - [import Self Sign CA To System](https://www.xiexianbin.cn/http/ssl/2017-02-15-openssl-self-sign-ca/#导出导入自签名证书) `x-ca/ca/root-ca.crt` and `x-ca/ca/tls-ca.crt` to trust Your CA. 11 | 12 | ## install 13 | 14 | - binary 15 | 16 | ``` 17 | curl -Lfs -o xca https://github.com/x-ca/go-ca/releases/latest/download/xca-{linux|darwin|windows}-{amd64|arm64|s390x|ppc64le}-{amd64|arm64} 18 | chmod +x xca 19 | mv xca /usr/local/bin/ 20 | ``` 21 | 22 | - source 23 | 24 | ``` 25 | go install go.xiexianbin.cn/xca/cmd@latest 26 | ``` 27 | 28 | ## Help 29 | 30 | ``` 31 | xca --help 32 | xca create-ca --help 33 | xca sign --help 34 | ``` 35 | 36 | ``` 37 | $ xca --help 38 | XCA is a command-line tool for creating and managing Root/Second-Level Certificate Authorities (CAs) 39 | and signing certificates for domains and IP addresses. 40 | 41 | Available Commands: 42 | create-ca Create root and TLS CA certificates 43 | info Display information about Certificates 44 | sign Sign a certificate for domains and/or IPs 45 | version Show version information 46 | 47 | Environment: 48 | XCA_ROOT_PATH Which path to store Root/Second-Level/TLS cert, default is "$(pwd)/x-ca" 49 | 50 | Examples: 51 | xca create-ca --key-type ec --curve P256 52 | xca sign example.com --domains "example.com,www.example.com" 53 | xca sign 192.168.1.1 --ips "192.168.1.1" 54 | 55 | Source Code: 56 | https://github.com/x-ca/go-ca 57 | ``` 58 | 59 | ## Usage Demo 60 | 61 | You can specify the key type (`-key-type`) and curve (`-curve`) to create an EC root CA and TLS CA: 62 | 63 | ``` 64 | # Create EC CA 65 | $ xca create-ca --key-type ec --curve P256 66 | 67 | # default out `x-ca/...` 68 | $ tree x-ca 69 | x-ca 70 | └── ca 71 | ├── root-ca 72 | │ └── private 73 | │ └── root-ca.key 74 | ├── root-ca.crt 75 | ├── tls-ca 76 | │ └── private 77 | │ └── tls-ca.key 78 | ├── tls-ca-chain.pem 79 | └── tls-ca.crt 80 | 81 | 6 directories, 5 files 82 | 83 | # Show CA info 84 | $ xca info ./x-ca/ca/root-ca.crt 85 | $ xca info ./x-ca/ca/tls-ca.crt 86 | 87 | # Sign Domains certificate 88 | xca sign example.com --domains "example.com,www.example.com" 89 | 90 | # Sign Domains and IPs certificate 91 | $ xca sign xiexianbin.cn --ips "192.168.1.1,*.xiexianbin.cn,*.dev.xiexianbin.cn" 92 | 93 | # Show TLS cert info 94 | $ xca info ./x-ca/certs/xiexianbin.cn/xiexianbin.cn.crt 95 | ``` 96 | 97 | - test cert 98 | 99 | ``` 100 | docker run -it -d \ 101 | -p 8443:443 \ 102 | -v $(pwd)/examples/default.conf:/etc/nginx/conf.d/default.conf \ 103 | -v $(pwd)/x-ca/certs/xiexianbin.cn/xiexianbin.cn.bundle.crt:/etc/pki/nginx/server.crt \ 104 | -v $(pwd)/x-ca/certs/xiexianbin.cn/xiexianbin.cn.key:/etc/pki/nginx/private/server.key \ 105 | nginx 106 | ``` 107 | 108 | - to verify, visit https://dev.xiexianbin.cn:8443/ in brower or run command: 109 | 110 | ``` 111 | curl -i -v -k https://dev.xiexianbin.cn:8443/ --resolve dev.xiexianbin.cn:8443:127.0.0.1 112 | ``` 113 | 114 | ## Dev 115 | 116 | - core file 117 | 118 | ``` 119 | go.mod - Added cobra dependency 120 | ca/baseca.go - Common CA functionality 121 | ca/common.go - Shared utilities 122 | cmd/create.go - create-ca command 123 | cmd/sign.go - sign command 124 | cmd/root.go - root cobra command 125 | cmd/xca.go - main entry point (refactored) 126 | ``` 127 | 128 | ## FaQ 129 | 130 | if CA Cert begin with `BEGIN ENCRYPTED PRIVATE KEY`(raise `Error: fromPEMBytes: x509: no DEK-Info header in block`), 131 | Use `openssl rsa -in root-ca.key -des3` change cipher 132 | 133 | ## Ref 134 | 135 | - [基于OpenSSL签署根CA、二级CA](https://www.xiexianbin.cn/s/ca/) 136 | -------------------------------------------------------------------------------- /cmd/sign.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022 xiexianbin.cn 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package main 15 | 16 | import ( 17 | "fmt" 18 | "os" 19 | "strings" 20 | 21 | "github.com/spf13/cobra" 22 | 23 | "go.xiexianbin.cn/xca/ca" 24 | ) 25 | 26 | var ( 27 | signCommonName string 28 | signDomains string 29 | signIPs string 30 | signTlsKey string 31 | signTlsCert string 32 | signTlsChain string 33 | signKeyType string 34 | signKeyBits int 35 | signCurve string 36 | signDays int 37 | signKeyPassword string 38 | ) 39 | 40 | // signCmd represents the sign command 41 | var signCmd = &cobra.Command{ 42 | Use: "sign [common-name]", 43 | Short: "Sign a certificate for domains and/or IPs", 44 | Long: `Sign a new certificate using the TLS CA for the specified common name and domains/IPs. 45 | 46 | Examples: 47 | xca sign example.com --domains "example.com" 48 | xca sign api.example.com --domains "api.example.com,*.example.com" 49 | xca sign 192.168.1.1 --ips "192.168.1.1" 50 | xca sign multi.example.com --domains "example.com,www.example.com" --ips "10.0.0.1"`, 51 | Args: cobra.ExactArgs(1), 52 | Run: func(cmd *cobra.Command, args []string) { 53 | signCommonName = args[0] 54 | if err := runSign(); err != nil { 55 | fmt.Printf("Error: %v\n", err) 56 | os.Exit(1) 57 | } 58 | }, 59 | } 60 | 61 | func initSignCmd() { 62 | xcarootpath := ca.GetEnvDefault(ca.XCARootPath, "x-ca") 63 | signCmd.Flags().StringVar(&signDomains, "domains", "", "Comma-separated domain names") 64 | signCmd.Flags().StringVar(&signIPs, "ips", "", "Comma-separated IP addresses") 65 | signCmd.Flags().StringVar(&signTlsKey, "tls-key", xcarootpath+"/ca/tls-ca/private/tls-ca.key", "TLS CA private key file path") 66 | signCmd.Flags().StringVar(&signTlsCert, "tls-cert", xcarootpath+"/ca/tls-ca.crt", "TLS CA certificate file path") 67 | signCmd.Flags().StringVar(&signTlsChain, "tls-chain", xcarootpath+"/ca/tls-ca-chain.pem", "TLS CA chain file path") 68 | signCmd.Flags().StringVar(&signKeyType, "key-type", ca.DefaultKeyType, "Key type (rsa or ec)") 69 | signCmd.Flags().IntVar(&signKeyBits, "key-bits", ca.DefaultKeyBits, "RSA key bits") 70 | signCmd.Flags().StringVar(&signCurve, "curve", ca.DefaultCurve, "EC curve (P224, P256, P384, P521)") 71 | signCmd.Flags().IntVar(&signDays, "days", 825, "Certificate validity in days") 72 | signCmd.Flags().StringVar(&signKeyPassword, "tls-key-password", "", "TLS key password (if encrypted)") 73 | } 74 | 75 | func runSign() error { 76 | // Parse domains and IPs 77 | domainList, err := ca.ParseDomains(strings.Split(signDomains, ",")) 78 | if err != nil { 79 | return fmt.Errorf("invalid domains: %w", err) 80 | } 81 | 82 | ipList, err := ca.ParseIPs(strings.Split(signIPs, ",")) 83 | if err != nil { 84 | return fmt.Errorf("invalid IPs: %w", err) 85 | } 86 | 87 | if len(domainList) == 0 && len(ipList) == 0 { 88 | return fmt.Errorf("at least one domain or IP must be specified") 89 | } 90 | 91 | // Load TLS CA 92 | tlsCA, err := ca.LoadTLSCA(signTlsKey, signTlsCert, signKeyPassword) 93 | if err != nil { 94 | return fmt.Errorf("failed to load TLS CA: %w", err) 95 | } 96 | 97 | // Sign certificate 98 | key, cert, err := tlsCA.Sign(signCommonName, domainList, ipList, signDays, signKeyType, signKeyBits, signCurve) 99 | if err != nil { 100 | return fmt.Errorf("failed to sign certificate: %w", err) 101 | } 102 | 103 | // Write certificate 104 | if err := tlsCA.WriteCert(signCommonName, key, cert, signTlsChain); err != nil { 105 | return fmt.Errorf("failed to write certificate: %w", err) 106 | } 107 | 108 | fmt.Printf("Successfully signed certificate for %s\n", signCommonName) 109 | return nil 110 | } 111 | -------------------------------------------------------------------------------- /ca/ca.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022 xiexianbin.cn 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package ca 15 | 16 | import ( 17 | "crypto/rand" 18 | "crypto/x509" 19 | "crypto/x509/pkix" 20 | "fmt" 21 | "time" 22 | ) 23 | 24 | var ( 25 | // sort.StringsAreSorted(supportPemType) == true 26 | supportPemType = []string{"EC PRIVATE KEY", "ECDSA PRIVATE KEY", "RSA PRIVATE KEY"} 27 | ) 28 | 29 | // RootCA represents a root certificate authority 30 | type RootCA struct { 31 | BaseCA 32 | } 33 | 34 | // NewRootCA creates a new root CA 35 | func NewRootCA(keyType string, keyBits int, curve string) (*RootCA, error) { 36 | rootCA := &RootCA{ 37 | BaseCA: BaseCA{ 38 | KeyBits: keyBits, 39 | Curve: curve, 40 | }, 41 | } 42 | 43 | if err := rootCA.GenerateKey(keyType); err != nil { 44 | return nil, fmt.Errorf("failed to generate key: %w", err) 45 | } 46 | 47 | if err := rootCA.CreateCert(); err != nil { 48 | return nil, fmt.Errorf("failed to create certificate: %w", err) 49 | } 50 | 51 | return rootCA, nil 52 | } 53 | 54 | // LoadRootCA loads an existing root CA from files 55 | func LoadRootCA(keyPath, certPath, password string) (*RootCA, error) { 56 | rootCA := &RootCA{ 57 | BaseCA: BaseCA{}, 58 | } 59 | 60 | if err := rootCA.LoadKey(keyPath); err != nil { 61 | return nil, fmt.Errorf("failed to load key: %w", err) 62 | } 63 | 64 | if err := rootCA.LoadCert(certPath); err != nil { 65 | return nil, fmt.Errorf("failed to load certificate: %w", err) 66 | } 67 | 68 | // Validate key and certificate match 69 | if err := ValidateKeyCertMatch(rootCA.Key, rootCA.Cert); err != nil { 70 | return nil, fmt.Errorf("key and certificate don't match: %w", err) 71 | } 72 | 73 | return rootCA, nil 74 | } 75 | 76 | // CreateCert creates the root CA certificate 77 | func (c *RootCA) CreateCert() error { 78 | pubKey, err := c.GetPublicKey() 79 | if err != nil { 80 | return fmt.Errorf("failed to get public key: %w", err) 81 | } 82 | 83 | rootKeyID, err := calculateKeyID(pubKey) 84 | if err != nil { 85 | return fmt.Errorf("failed to calculate key ID: %w", err) 86 | } 87 | 88 | rootCSR := &x509.Certificate{ 89 | Version: 3, 90 | SerialNumber: randSerial(1), // default root serial number is 1 91 | Subject: pkix.Name{ 92 | Country: []string{RootCertCountry}, 93 | Organization: []string{RootCertOrganization}, 94 | OrganizationalUnit: []string{RootCertOrganizationalUnit}, 95 | CommonName: RootCertCN, 96 | }, 97 | NotBefore: time.Now(), 98 | NotAfter: time.Now().AddDate(RootCertYears, 0, 0), 99 | SubjectKeyId: rootKeyID, 100 | AuthorityKeyId: rootKeyID, 101 | BasicConstraintsValid: true, 102 | IsCA: true, 103 | MaxPathLen: 1, 104 | MaxPathLenZero: false, 105 | KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign | x509.KeyUsageDigitalSignature, 106 | ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}, 107 | } 108 | 109 | der, err := x509.CreateCertificate(rand.Reader, rootCSR, rootCSR, pubKey, c.Key) 110 | if err != nil { 111 | return fmt.Errorf("failed to create certificate: %w", err) 112 | } 113 | 114 | cert, err := x509.ParseCertificate(der) 115 | if err != nil { 116 | return fmt.Errorf("failed to parse certificate: %w", err) 117 | } 118 | 119 | c.Cert = cert 120 | return nil 121 | } 122 | 123 | // Write writes the root CA key and certificate to files 124 | func (c *RootCA) Write(rootKeyPath, rootCertPath, chainPath string) error { 125 | if err := c.WriteKey(rootKeyPath); err != nil { 126 | return fmt.Errorf("failed to write key: %w", err) 127 | } 128 | 129 | if err := c.WriteCert(rootCertPath); err != nil { 130 | return fmt.Errorf("failed to write certificate: %w", err) 131 | } 132 | 133 | return nil 134 | } 135 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # https://www.xiexianbin.cn/program/tools/2016-01-09-makefile/index.html 2 | export SHELL:=bash 3 | export SHELLOPTS:=$(if $(SHELLOPTS),$(SHELLOPTS):)pipefail:errexit 4 | 5 | # https://stackoverflow.com/questions/4122831/disable-make-builtin-rules-and-variables-from-inside-the-make-file 6 | MAKEFLAGS += --no-builtin-rules 7 | .SUFFIXES: 8 | 9 | VERSION := latest 10 | BUILD_DATE := $(shell TZ=UTC-8 date +'%Y-%m-%dT%H:%M:%SZ+08:00') 11 | GIT_COMMIT := $(shell git rev-parse HEAD || echo unknown) 12 | GIT_BRANCH := $(shell git rev-parse --symbolic-full-name --verify --quiet --abbrev-ref HEAD) 13 | GIT_TAG := $(shell git describe --exact-match --tags --abbrev=0 2> /dev/null || echo untagged) 14 | GIT_TREE_STATE := $(shell if [ -z "`git status --porcelain`" ]; then echo "clean" ; else echo "dirty"; fi) 15 | RELEASE_TAG := $(shell if [[ "$(GIT_TAG)" =~ ^v[0-9]+\.[0-9]+\.[0-9]+.*$$ ]]; then echo "true"; else echo "false"; fi) 16 | DEV_BRANCH := $(shell [ "$(GIT_BRANCH)" = master ] || [ `echo $(GIT_BRANCH) | cut -c -8` = release- ] || [ `echo $(GIT_BRANCH) | cut -c -4` = dev- ] || [ $(RELEASE_TAG) = true ] && echo false || echo true) 17 | 18 | GOCMD ?= go 19 | GOBUILD ?= $(GOCMD) build -v 20 | GOCLEAN ?= $(GOCMD) clean 21 | GOTEST ?= $(GOCMD) test -v -p 20 22 | 23 | linux-amd64: GOARGS = GOOS=linux GOARCH=amd64 24 | linux-arm64: GOARGS = GOOS=linux GOARCH=arm64 25 | linux-ppc64le: GOARGS = GOOS=linux GOARCH=ppc64le 26 | linux-s390x: GOARGS = GOOS=linux GOARCH=s390x 27 | darwin-amd64: GOARGS = GOOS=darwin GOARCH=amd64 28 | darwin-arm64: GOARGS = GOOS=darwin GOARCH=arm64 29 | windows-amd64: GOARGS = GOOS=windows GOARCH=amd64 30 | 31 | BINARY_NAME ?= xca 32 | IMG ?= xiexianbin/go-actions-demo:latest 33 | 34 | ifeq ($(RELEASE_TAG),true) 35 | VERSION := $(GIT_TAG) 36 | endif 37 | 38 | # $(info GIT_COMMIT=$(GIT_COMMIT) GIT_BRANCH=$(GIT_BRANCH) GIT_TAG=$(GIT_TAG) GIT_TREE_STATE=$(GIT_TREE_STATE) RELEASE_TAG=$(RELEASE_TAG) DEV_BRANCH=$(DEV_BRANCH) VERSION=$(VERSION)) 39 | # $(info MAKEFILE_LIST=${MAKEFILE_LIST}) 40 | 41 | # -X github.com/xiexianbin/go-actions-demo.version=$(VERSION) 42 | override LDFLAGS += \ 43 | -X go.xiexianbin.cn/xca.version=$(VERSION) \ 44 | -X go.xiexianbin.cn/xca.buildDate=$(BUILD_DATE) \ 45 | -X go.xiexianbin.cn/xca.gitCommit=$(GIT_COMMIT) \ 46 | -X go.xiexianbin.cn/xca.gitTreeState=$(GIT_TREE_STATE) 47 | 48 | ifneq ($(GIT_TAG),) 49 | override LDFLAGS += -X go.xiexianbin.cn/xca.gitTag=${GIT_TAG} 50 | endif 51 | 52 | SUB_BUILD_CMD ?= $(GOBUILD) -gcflags '${GCFLAGS}' -ldflags '${LDFLAGS} -extldflags -static' 53 | 54 | .PHONY: help 55 | help: ## Show this help 56 | @awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z0-9_-]+:.*?## / {sub("\\\\n",sprintf("\n%22c"," "), $$2);printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST) 57 | 58 | .PHONY: all 59 | all: clean test build linux-amd64 linux-arm64 linux-ppc64le linux-s390x darwin-amd64 darwin-arm64 windows-amd64 ## Build all 60 | 61 | .PHONY: test 62 | test: ## Run test 63 | $(GOTEST) -v ./... 64 | 65 | .PHONY: clean 66 | clean: ## Run clean bin files 67 | $(GOCLEAN) 68 | rm -f bin/* 69 | 70 | .PHONY: build 71 | build: ## Build for current os 72 | ${SUB_BUILD_CMD} -o bin/$(BINARY_NAME) ./cmd/... 73 | 74 | .PHONY: linux-amd64 75 | linux-amd64: ## Build linux amd64 76 | CGO_ENABLED=0 ${GOARGS} ${SUB_BUILD_CMD} -o bin/${BINARY_NAME}-$@ ./cmd/... 77 | 78 | .PHONY: linux-arm64 79 | linux-arm64: ## Build linux arm64 80 | CGO_ENABLED=0 ${GOARGS} ${SUB_BUILD_CMD} -o bin/${BINARY_NAME}-$@ ./cmd/... 81 | 82 | .PHONY: linux-ppc64le 83 | linux-ppc64le: ## Build linux ppc64le 84 | CGO_ENABLED=0 ${GOARGS} ${SUB_BUILD_CMD} -o bin/${BINARY_NAME}-$@ ./cmd/... 85 | 86 | .PHONY: linux-s390x 87 | linux-s390x: ## Build linux s390x 88 | CGO_ENABLED=0 ${GOARGS} ${SUB_BUILD_CMD} -o bin/${BINARY_NAME}-$@ ./cmd/... 89 | 90 | .PHONY: darwin-amd64 91 | darwin-amd64: ## Build darwin amd64 92 | CGO_ENABLED=0 ${GOARGS} ${SUB_BUILD_CMD} -o bin/${BINARY_NAME}-$@ ./cmd/... 93 | 94 | .PHONY: darwin-arm64 95 | darwin-arm64: ## Build darwin arm64 96 | CGO_ENABLED=0 ${GOARGS} ${SUB_BUILD_CMD} -o bin/${BINARY_NAME}-$@ ./cmd/... 97 | 98 | .PHONY: windows-amd64 99 | windows-amd64: ## Build windows amd64 100 | CGO_ENABLED=0 ${GOARGS} ${SUB_BUILD_CMD} -o bin/${BINARY_NAME}-$@.exe ./cmd/... 101 | 102 | # .PHONY: docker-build 103 | # docker-build: test ## Build docker image 104 | # docker build -t ${IMG} . 105 | 106 | # .PHONY: docker-push 107 | # docker-push: ## Push docker image 108 | # docker push ${IMG} 109 | -------------------------------------------------------------------------------- /ca/common.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022 xiexianbin.cn 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package ca 15 | 16 | import ( 17 | "bytes" 18 | "crypto" 19 | "crypto/ecdsa" 20 | "crypto/rand" 21 | "crypto/rsa" 22 | "crypto/sha1" 23 | "crypto/x509" 24 | "crypto/x509/pkix" 25 | "encoding/asn1" 26 | "encoding/pem" 27 | "fmt" 28 | "math" 29 | "math/big" 30 | "net" 31 | "os" 32 | "path" 33 | "path/filepath" 34 | "regexp" 35 | ) 36 | 37 | // Common CA constants 38 | const ( 39 | DefaultKeyType = "ec" 40 | DefaultKeyBits = 2048 41 | DefaultCurve = "P256" 42 | 43 | RootCertCountry = "CN" 44 | RootCertOrganization = "X CA" 45 | RootCertOrganizationalUnit = "www.xiexianbin.cn" 46 | RootCertCN = "X Root CA - R1" 47 | RootCertYears = 60 48 | 49 | XCARootPath = "XCA_ROOT_PATH" 50 | ) 51 | 52 | // CreateCertificateChain writes a certificate chain to file 53 | func CreateCertificateChain(chainPath string, certs ...*x509.Certificate) error { 54 | if len(certs) == 0 { 55 | return fmt.Errorf("no certificates provided") 56 | } 57 | 58 | // Create directory if it doesn't exist 59 | err := os.MkdirAll(path.Dir(chainPath), 0700) 60 | if err != nil && !os.IsExist(err) { 61 | return err 62 | } 63 | 64 | chainFile, err := os.OpenFile(chainPath, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0600) 65 | if err != nil { 66 | return err 67 | } 68 | defer chainFile.Close() 69 | 70 | for _, cert := range certs { 71 | err = pem.Encode(chainFile, &pem.Block{ 72 | Type: "CERTIFICATE", 73 | Bytes: cert.Raw, 74 | }) 75 | if err != nil { 76 | return err 77 | } 78 | } 79 | 80 | return nil 81 | } 82 | 83 | // ValidateKeyCertMatch validates that a private key matches a certificate 84 | func ValidateKeyCertMatch(privateKey any, cert *x509.Certificate) error { 85 | var pubKey crypto.PublicKey 86 | switch k := privateKey.(type) { 87 | case *rsa.PrivateKey: 88 | pubKey = k.Public() 89 | case *ecdsa.PrivateKey: 90 | pubKey = k.Public() 91 | default: 92 | return fmt.Errorf("unsupported key type") 93 | } 94 | 95 | keyPKBytes, err := x509.MarshalPKIXPublicKey(pubKey) 96 | if err != nil { 97 | return err 98 | } 99 | 100 | certPKBytes, err := x509.MarshalPKIXPublicKey(cert.PublicKey) 101 | if err != nil { 102 | return err 103 | } 104 | 105 | if !bytes.Equal(keyPKBytes, certPKBytes) { 106 | return fmt.Errorf("public key in certificate doesn't match private key") 107 | } 108 | 109 | return nil 110 | } 111 | 112 | // CheckFileExists checks if a file exists 113 | func CheckFileExists(filePath string) (bool, error) { 114 | _, err := os.ReadFile(filePath) 115 | if err == nil { 116 | return true, nil 117 | } 118 | if os.IsNotExist(err) { 119 | return false, nil 120 | } 121 | return false, err 122 | } 123 | 124 | // EnsureDirectory creates a directory if it doesn't exist 125 | func EnsureDirectory(dirPath string) error { 126 | return os.MkdirAll(dirPath, 0700) 127 | } 128 | 129 | // CreateFile creates a file with exclusive creation mode 130 | func CreateFile(filePath string) (*os.File, error) { 131 | return os.OpenFile(filePath, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0600) 132 | } 133 | 134 | func randSerial(x int64) *big.Int { 135 | if x > 0 { 136 | return big.NewInt(x) 137 | } 138 | 139 | b, err := rand.Int(rand.Reader, big.NewInt(math.MaxInt64)) 140 | if err != nil { 141 | return big.NewInt(1) 142 | } 143 | return b 144 | } 145 | 146 | func calculateKeyID(pubKey crypto.PublicKey) ([]byte, error) { 147 | pkixByte, err := x509.MarshalPKIXPublicKey(pubKey) 148 | if err != nil { 149 | return nil, err 150 | } 151 | 152 | var pkiInfo struct { 153 | Algorithm pkix.AlgorithmIdentifier 154 | SubjectPublicKey asn1.BitString 155 | } 156 | _, err = asn1.Unmarshal(pkixByte, &pkiInfo) 157 | if err != nil { 158 | return nil, err 159 | } 160 | skid := sha1.Sum(pkiInfo.SubjectPublicKey.Bytes) 161 | return skid[:], nil 162 | } 163 | 164 | func ParseDomains(domainStr []string) ([]string, error) { 165 | var domainSlice []string 166 | re := regexp.MustCompile("^[A-Za-z0-9-.*]+$") 167 | for _, s := range domainStr { 168 | if re.MatchString(s) { 169 | domainSlice = append(domainSlice, s) 170 | } 171 | } 172 | 173 | return domainSlice, nil 174 | } 175 | 176 | func ParseIPs(ipStr []string) (ipSlice []net.IP, err error) { 177 | for _, s := range ipStr { 178 | if len(s) == 0 { 179 | continue 180 | } 181 | p := net.ParseIP(s) 182 | if p == nil { 183 | continue 184 | } 185 | ipSlice = append(ipSlice, p) 186 | } 187 | return ipSlice, nil 188 | } 189 | 190 | func GetEnvDefault(key, defVal string) string { 191 | val, ex := os.LookupEnv(key) 192 | if !ex { 193 | return defVal 194 | } 195 | return val 196 | } 197 | 198 | func ExecPath() (string, error) { 199 | ex, err := os.Executable() 200 | if err != nil { 201 | return "", err 202 | } 203 | return filepath.Dir(ex), nil 204 | } 205 | -------------------------------------------------------------------------------- /ca/base.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022 xiexianbin.cn 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package ca 15 | 16 | import ( 17 | "crypto/ecdsa" 18 | "crypto/elliptic" 19 | "crypto/rand" 20 | "crypto/rsa" 21 | "crypto/x509" 22 | "encoding/pem" 23 | "fmt" 24 | "os" 25 | "path" 26 | "strings" 27 | ) 28 | 29 | type CA interface { 30 | GenerateKey() error 31 | CreateCert() error 32 | Write(keyPath, certPath, chainPath string) error 33 | //Load(keyPath, certPath string) (any, error) 34 | } 35 | 36 | // BaseCA represents common functionality for all CA types 37 | type BaseCA struct { 38 | Key any // *rsa.PrivateKey or *ecdsa.PrivateKey 39 | Cert *x509.Certificate 40 | KeyBits int 41 | Curve string 42 | } 43 | 44 | // GenerateKey generates a new private key based on key type 45 | func (b *BaseCA) GenerateKey(keyType string) error { 46 | switch strings.ToLower(keyType) { 47 | case "ec", "ecdsa": 48 | var curve elliptic.Curve 49 | switch b.Curve { 50 | case "P224": 51 | curve = elliptic.P224() 52 | case "P256": 53 | curve = elliptic.P256() 54 | case "P384": 55 | curve = elliptic.P384() 56 | case "P521": 57 | curve = elliptic.P521() 58 | default: 59 | return fmt.Errorf("unsupported curve %s", b.Curve) 60 | } 61 | key, err := ecdsa.GenerateKey(curve, rand.Reader) 62 | if err != nil { 63 | return err 64 | } 65 | b.Key = key 66 | case "rsa": 67 | key, err := rsa.GenerateKey(rand.Reader, b.KeyBits) 68 | if err != nil { 69 | return err 70 | } 71 | b.Key = key 72 | default: 73 | return fmt.Errorf("unsupported key type %s", keyType) 74 | } 75 | return nil 76 | } 77 | 78 | // GetPublicKey extracts the public key from the private key 79 | func (b *BaseCA) GetPublicKey() (any, error) { 80 | switch k := b.Key.(type) { 81 | case *rsa.PrivateKey: 82 | return k.Public(), nil 83 | case *ecdsa.PrivateKey: 84 | return k.Public(), nil 85 | default: 86 | return nil, fmt.Errorf("unsupported key type") 87 | } 88 | } 89 | 90 | // WriteKey writes the private key to a PEM file 91 | func (b *BaseCA) WriteKey(keyPath string) error { 92 | // Create directory if it doesn't exist 93 | err := os.MkdirAll(path.Dir(keyPath), 0700) 94 | if err != nil && !os.IsExist(err) { 95 | return err 96 | } 97 | 98 | keyFile, err := os.OpenFile(keyPath, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0600) 99 | if err != nil { 100 | return err 101 | } 102 | defer keyFile.Close() 103 | 104 | var keyType string 105 | var keyBytes []byte 106 | switch k := b.Key.(type) { 107 | case *rsa.PrivateKey: 108 | keyType = "RSA PRIVATE KEY" 109 | keyBytes = x509.MarshalPKCS1PrivateKey(k) 110 | case *ecdsa.PrivateKey: 111 | keyType = "EC PRIVATE KEY" 112 | var err error 113 | keyBytes, err = x509.MarshalECPrivateKey(k) 114 | if err != nil { 115 | return err 116 | } 117 | default: 118 | return fmt.Errorf("unsupported key type") 119 | } 120 | 121 | return pem.Encode(keyFile, &pem.Block{ 122 | Type: keyType, 123 | Bytes: keyBytes, 124 | }) 125 | } 126 | 127 | // WriteCert writes the certificate to a PEM file 128 | func (b *BaseCA) WriteCert(certPath string) error { 129 | // Create directory if it doesn't exist 130 | err := os.MkdirAll(path.Dir(certPath), 0700) 131 | if err != nil && !os.IsExist(err) { 132 | return err 133 | } 134 | 135 | certFile, err := os.OpenFile(certPath, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0600) 136 | if err != nil { 137 | return err 138 | } 139 | defer certFile.Close() 140 | 141 | return pem.Encode(certFile, &pem.Block{ 142 | Type: "CERTIFICATE", 143 | Bytes: b.Cert.Raw, 144 | }) 145 | } 146 | 147 | // LoadKey loads a private key from a PEM file 148 | func (b *BaseCA) LoadKey(keyPath string) error { 149 | keyBytes, err := os.ReadFile(keyPath) 150 | if err != nil { 151 | return err 152 | } 153 | 154 | keyBlock, _ := pem.Decode(keyBytes) 155 | if keyBlock == nil { 156 | return fmt.Errorf("decode key is nil") 157 | } 158 | 159 | // Check if encrypted 160 | isEncrypted := len(keyBlock.Headers) > 0 && keyBlock.Headers["Proc-Type"] == "4,ENCRYPTED" 161 | if isEncrypted { 162 | return fmt.Errorf("encrypted PEM blocks are not supported - please decrypt your key first, using: openssl rsa -in encrypted.key -out decrypted.key") 163 | } 164 | 165 | switch keyBlock.Type { 166 | case "RSA PRIVATE KEY": 167 | b.Key, err = x509.ParsePKCS1PrivateKey(keyBlock.Bytes) 168 | case "EC PRIVATE KEY": 169 | b.Key, err = x509.ParseECPrivateKey(keyBlock.Bytes) 170 | default: 171 | return fmt.Errorf("unsupported PEM type %s", keyBlock.Type) 172 | } 173 | return err 174 | } 175 | 176 | // LoadCert loads a certificate from a PEM file 177 | func (b *BaseCA) LoadCert(certPath string) error { 178 | certBytes, err := os.ReadFile(certPath) 179 | if err != nil { 180 | return err 181 | } 182 | 183 | certBlock, _ := pem.Decode(certBytes) 184 | if certBlock == nil { 185 | return fmt.Errorf("decode cert is nil") 186 | } else if certBlock.Type != "CERTIFICATE" { 187 | return fmt.Errorf("unsupported PEM type %s", certBlock.Type) 188 | } 189 | 190 | b.Cert, err = x509.ParseCertificate(certBlock.Bytes) 191 | return err 192 | } 193 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /cmd/info.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2025 xiexianbin.cn 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package main 15 | 16 | import ( 17 | "crypto/ecdsa" 18 | "crypto/rsa" 19 | "crypto/x509" 20 | "crypto/x509/pkix" 21 | "encoding/asn1" 22 | "encoding/pem" 23 | "fmt" 24 | "math/big" 25 | "os" 26 | "strings" 27 | 28 | "github.com/spf13/cobra" 29 | ) 30 | 31 | var ( 32 | certfilePath string 33 | ) 34 | 35 | // OIDs for various X.509 extensions. 36 | var ( 37 | oidExtensionSubjectKeyId = asn1.ObjectIdentifier{2, 5, 29, 14} 38 | oidExtensionKeyUsage = asn1.ObjectIdentifier{2, 5, 29, 15} 39 | oidExtensionExtendedKeyUsage = asn1.ObjectIdentifier{2, 5, 29, 37} 40 | oidExtensionAuthorityKeyId = asn1.ObjectIdentifier{2, 5, 29, 35} 41 | oidExtensionSubjectAltName = asn1.ObjectIdentifier{2, 5, 29, 17} 42 | oidExtensionBasicConstraints = asn1.ObjectIdentifier{2, 5, 29, 19} 43 | oidExtensionCRLDistributionPoints = asn1.ObjectIdentifier{2, 5, 29, 31} 44 | oidExtensionCertificatePolicies = asn1.ObjectIdentifier{2, 5, 29, 32} 45 | oidAuthorityInfoAccess = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 1} 46 | ) 47 | 48 | // Maps OIDs to their string representations for better readability. 49 | var oidToStringMap = map[string]string{ 50 | "2.5.29.14": "X509v3 Subject Key Identifier", 51 | "2.5.29.15": "X509v3 Key Usage", 52 | "2.5.29.19": "X509v3 Basic Constraints", 53 | "2.5.29.31": "X509v3 CRL Distribution Points", 54 | "2.5.29.32": "X509v3 Certificate Policies", 55 | "2.5.29.35": "X509v3 Authority Key Identifier", 56 | "2.5.29.37": "X509v3 Extended Key Usage", 57 | "2.5.29.17": "X509v3 Subject Alternative Name", 58 | "1.3.6.1.5.5.7.1.1": "Authority Information Access", 59 | } 60 | 61 | func printCertificateInfo(cert *x509.Certificate) { 62 | fmt.Println("Certificate:") 63 | fmt.Println(" Data:") 64 | // Version 65 | fmt.Printf(" Version: %d (0x%x)\n", cert.Version, cert.Version-1) 66 | 67 | // Serial Number 68 | fmt.Printf(" Serial Number:\n %s\n", formatSerialNumber(cert.SerialNumber)) 69 | 70 | // Signature Algorithm 71 | fmt.Printf(" Signature Algorithm: %s\n", cert.SignatureAlgorithm.String()) 72 | 73 | // Issuer 74 | fmt.Printf(" Issuer: %s\n", formatName(cert.Issuer)) 75 | 76 | // Validity 77 | fmt.Println(" Validity") 78 | fmt.Printf(" Not Before: %s\n", cert.NotBefore.UTC().Format("Jan 2 15:04:05 2006 GMT")) 79 | fmt.Printf(" Not After : %s\n", cert.NotAfter.UTC().Format("Jan 2 15:04:05 2006 GMT")) 80 | 81 | // Subject 82 | fmt.Printf(" Subject: %s\n", formatName(cert.Subject)) 83 | 84 | // Public Key 85 | fmt.Println(" Subject Public Key Info:") 86 | printPublicKeyInfo(cert.PublicKey) 87 | 88 | // Extensions 89 | if len(cert.Extensions) > 0 { 90 | fmt.Println(" X509v3 extensions:") 91 | printExtensions(cert) 92 | } 93 | 94 | // Signature 95 | fmt.Printf(" Signature Algorithm: %s\n", cert.SignatureAlgorithm.String()) 96 | printHexBlock(" ", cert.Signature, 18) 97 | } 98 | 99 | func formatName(name pkix.Name) string { 100 | var parts []string 101 | if len(name.Country) > 0 { 102 | parts = append(parts, "C="+strings.Join(name.Country, ",")) 103 | } 104 | if len(name.Province) > 0 { 105 | parts = append(parts, "ST="+strings.Join(name.Province, ",")) 106 | } 107 | if len(name.Locality) > 0 { 108 | parts = append(parts, "L="+strings.Join(name.Locality, ",")) 109 | } 110 | if len(name.Organization) > 0 { 111 | parts = append(parts, "O="+strings.Join(name.Organization, ",")) 112 | } 113 | if len(name.OrganizationalUnit) > 0 { 114 | parts = append(parts, "OU="+strings.Join(name.OrganizationalUnit, ",")) 115 | } 116 | if name.CommonName != "" { 117 | parts = append(parts, "CN="+name.CommonName) 118 | } 119 | return strings.Join(parts, ", ") 120 | } 121 | 122 | func formatSerialNumber(serial *big.Int) string { 123 | hex := fmt.Sprintf("%x", serial) 124 | if len(hex)%2 != 0 { 125 | hex = "0" + hex 126 | } 127 | var parts []string 128 | for i := 0; i < len(hex); i += 2 { 129 | parts = append(parts, hex[i:i+2]) 130 | } 131 | return strings.Join(parts, ":") 132 | } 133 | 134 | func printPublicKeyInfo(pub interface{}) { 135 | switch pub := pub.(type) { 136 | case *rsa.PublicKey: 137 | fmt.Printf(" Public Key Algorithm: %s\n", x509.RSA.String()) 138 | fmt.Printf(" RSA Public-Key: (%d bit)\n", pub.N.BitLen()) 139 | fmt.Println(" Modulus:") 140 | printHexBlock(" ", pub.N.Bytes(), 15) 141 | fmt.Printf(" Exponent: %d (0x%x)\n", pub.E, pub.E) 142 | 143 | case *ecdsa.PublicKey: 144 | fmt.Printf(" Public Key Algorithm: %s\n", x509.ECDSA.String()) 145 | fmt.Printf(" Public-Key: (%d bit)\n", pub.Curve.Params().BitSize) 146 | printHexBlock(" ", pub.X.Bytes(), 15) // Just showing the X coordinate for brevity, similar to some tools 147 | fmt.Printf(" Curve: %s\n", pub.Curve.Params().Name) 148 | default: 149 | fmt.Println(" Public Key Algorithm: Unknown") 150 | } 151 | } 152 | 153 | func printExtensions(cert *x509.Certificate) { 154 | for _, ext := range cert.Extensions { 155 | oidStr := ext.Id.String() 156 | extName, ok := oidToStringMap[oidStr] 157 | if !ok { 158 | extName = oidStr // Fallback to OID if not in map 159 | } 160 | 161 | criticalStr := "" 162 | if ext.Critical { 163 | criticalStr = "critical" 164 | } 165 | fmt.Printf(" %s: %s\n", extName, criticalStr) 166 | 167 | // Parse and print specific extension details 168 | printExtensionValue(ext, cert) 169 | } 170 | } 171 | 172 | func printExtensionValue(ext pkix.Extension, cert *x509.Certificate) { 173 | indent := " " 174 | switch { 175 | case ext.Id.Equal(oidExtensionKeyUsage): 176 | printKeyUsage(cert.KeyUsage, indent) 177 | case ext.Id.Equal(oidExtensionExtendedKeyUsage): 178 | printExtendedKeyUsage(cert.ExtKeyUsage, indent) 179 | case ext.Id.Equal(oidExtensionBasicConstraints): 180 | fmt.Printf("%sCA:%t\n", indent, cert.IsCA) 181 | case ext.Id.Equal(oidExtensionSubjectKeyId): 182 | fmt.Printf("%s%s\n", indent, formatHex(cert.SubjectKeyId)) 183 | case ext.Id.Equal(oidExtensionAuthorityKeyId): 184 | fmt.Printf("%skeyid:%s\n", indent, formatHex(cert.AuthorityKeyId)) 185 | case ext.Id.Equal(oidExtensionSubjectAltName): 186 | printSAN(cert, indent) 187 | case ext.Id.Equal(oidAuthorityInfoAccess): 188 | printAIA(cert, indent) 189 | case ext.Id.Equal(oidExtensionCRLDistributionPoints): 190 | for _, point := range cert.CRLDistributionPoints { 191 | fmt.Printf("%sFull Name:\n%s URI:%s\n", indent, indent, point) 192 | } 193 | case ext.Id.Equal(oidExtensionCertificatePolicies): 194 | for _, policy := range cert.PolicyIdentifiers { 195 | fmt.Printf("%sPolicy: %s\n", indent, policy.String()) 196 | } 197 | } 198 | } 199 | 200 | func printKeyUsage(ku x509.KeyUsage, indent string) { 201 | var usages []string 202 | if ku&x509.KeyUsageDigitalSignature != 0 { 203 | usages = append(usages, "Digital Signature") 204 | } 205 | if ku&x509.KeyUsageContentCommitment != 0 { 206 | usages = append(usages, "Content Commitment") 207 | } 208 | if ku&x509.KeyUsageKeyEncipherment != 0 { 209 | usages = append(usages, "Key Encipherment") 210 | } 211 | if ku&x509.KeyUsageDataEncipherment != 0 { 212 | usages = append(usages, "Data Encipherment") 213 | } 214 | if ku&x509.KeyUsageKeyAgreement != 0 { 215 | usages = append(usages, "Key Agreement") 216 | } 217 | if ku&x509.KeyUsageCertSign != 0 { 218 | usages = append(usages, "Certificate Sign") 219 | } 220 | if ku&x509.KeyUsageCRLSign != 0 { 221 | usages = append(usages, "CRL Sign") 222 | } 223 | if ku&x509.KeyUsageEncipherOnly != 0 { 224 | usages = append(usages, "Encipher Only") 225 | } 226 | if ku&x509.KeyUsageDecipherOnly != 0 { 227 | usages = append(usages, "Decipher Only") 228 | } 229 | fmt.Printf("%s%s\n", indent, strings.Join(usages, ", ")) 230 | } 231 | 232 | func printExtendedKeyUsage(ekus []x509.ExtKeyUsage, indent string) { 233 | var usages []string 234 | for _, eku := range ekus { 235 | switch eku { 236 | case x509.ExtKeyUsageAny: 237 | usages = append(usages, "Any") 238 | case x509.ExtKeyUsageServerAuth: 239 | usages = append(usages, "TLS Web Server Authentication") 240 | case x509.ExtKeyUsageClientAuth: 241 | usages = append(usages, "TLS Web Client Authentication") 242 | case x509.ExtKeyUsageCodeSigning: 243 | usages = append(usages, "Code Signing") 244 | case x509.ExtKeyUsageEmailProtection: 245 | usages = append(usages, "E-mail Protection") 246 | case x509.ExtKeyUsageIPSECEndSystem: 247 | usages = append(usages, "IPSEC End System") 248 | case x509.ExtKeyUsageIPSECTunnel: 249 | usages = append(usages, "IPSEC Tunnel") 250 | case x509.ExtKeyUsageIPSECUser: 251 | usages = append(usages, "IPSEC User") 252 | case x509.ExtKeyUsageTimeStamping: 253 | usages = append(usages, "Time Stamping") 254 | case x509.ExtKeyUsageOCSPSigning: 255 | usages = append(usages, "OCSP Signing") 256 | case x509.ExtKeyUsageMicrosoftServerGatedCrypto: 257 | usages = append(usages, "Microsoft Server Gated Crypto") 258 | case x509.ExtKeyUsageNetscapeServerGatedCrypto: 259 | usages = append(usages, "Netscape Server Gated Crypto") 260 | default: 261 | usages = append(usages, "Unknown") 262 | } 263 | } 264 | fmt.Printf("%s%s\n", indent, strings.Join(usages, ", ")) 265 | } 266 | 267 | func printSAN(cert *x509.Certificate, indent string) { 268 | var san []string 269 | for _, name := range cert.DNSNames { 270 | san = append(san, "DNS:"+name) 271 | } 272 | for _, email := range cert.EmailAddresses { 273 | san = append(san, "email:"+email) 274 | } 275 | for _, ip := range cert.IPAddresses { 276 | san = append(san, "IP Address:"+ip.String()) 277 | } 278 | for _, uri := range cert.URIs { 279 | san = append(san, "URI:"+uri.String()) 280 | } 281 | fmt.Printf("%s%s\n", indent, strings.Join(san, ", ")) 282 | } 283 | 284 | func printAIA(cert *x509.Certificate, indent string) { 285 | if len(cert.OCSPServer) > 0 { 286 | fmt.Printf("%sOCSP - URI:%s\n", indent, strings.Join(cert.OCSPServer, ", ")) 287 | } 288 | if len(cert.IssuingCertificateURL) > 0 { 289 | fmt.Printf("%sCA Issuers - URI:%s\n", indent, strings.Join(cert.IssuingCertificateURL, ", ")) 290 | } 291 | } 292 | 293 | func formatHex(data []byte) string { 294 | var parts []string 295 | for i, b := range data { 296 | if i > 0 { 297 | parts = append(parts, ":") 298 | } 299 | parts = append(parts, fmt.Sprintf("%02X", b)) 300 | } 301 | return strings.Join(parts, "") 302 | } 303 | 304 | func printHexBlock(prefix string, data []byte, wrap int) { 305 | var parts []string 306 | for i, b := range data { 307 | if i > 0 && i%wrap == 0 { 308 | parts = append(parts, "\n"+prefix) 309 | } 310 | parts = append(parts, fmt.Sprintf("%02x:", b)) 311 | } 312 | // remove last ":" 313 | if len(parts) > 0 { 314 | str := strings.Join(parts, "") 315 | fmt.Printf("%s%s\n", prefix, str[:len(str)-1]) 316 | } 317 | } 318 | 319 | // infoCmd represents the info command 320 | var infoCmd = &cobra.Command{ 321 | Use: "info", 322 | Short: "Display information about Certificates", 323 | Long: `Display information about Certificate, like 'openssl x509 -noout -text -in xxx.crt' including version, and basic information. 324 | 325 | Examples: 326 | xca info /root-ca.crt`, 327 | Args: cobra.ExactArgs(1), 328 | Run: func(cmd *cobra.Command, args []string) { 329 | certfilePath = args[0] 330 | 331 | // Read the certificate file 332 | certPEM, err := os.ReadFile(certfilePath) 333 | if err != nil { 334 | fmt.Fprintf(os.Stderr, "Error reading certificate file: %v\n", err) 335 | os.Exit(1) 336 | } 337 | 338 | // Decode the PEM-encoded certificate 339 | block, _ := pem.Decode(certPEM) 340 | if block == nil || block.Type != "CERTIFICATE" { 341 | fmt.Fprintf(os.Stderr, "Failed to decode PEM block containing certificate\n") 342 | os.Exit(1) 343 | } 344 | 345 | // Parse the certificate 346 | cert, err := x509.ParseCertificate(block.Bytes) 347 | if err != nil { 348 | fmt.Fprintf(os.Stderr, "Error parsing certificate: %v\n", err) 349 | os.Exit(1) 350 | } 351 | 352 | // Print certificate details 353 | printCertificateInfo(cert) 354 | }, 355 | } 356 | -------------------------------------------------------------------------------- /ca/tls.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022 xiexianbin.cn 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | package ca 15 | 16 | import ( 17 | "bytes" 18 | "crypto/ecdsa" 19 | "crypto/elliptic" 20 | "crypto/rand" 21 | "crypto/rsa" 22 | "crypto/x509" 23 | "crypto/x509/pkix" 24 | "encoding/pem" 25 | "fmt" 26 | "net" 27 | "os" 28 | "path" 29 | "sort" 30 | "strings" 31 | "time" 32 | ) 33 | 34 | const ( 35 | tlsCertCountry = "CN" 36 | tlsCertOrganization = "X CA" 37 | tlsCertOrganizationalUnit = "www.xiexianbin.cn" 38 | tlsCertCN = "X TLS CA 1C1" 39 | tlsCertYears = 20 40 | MaxTLSDays = 825 41 | ) 42 | 43 | type TLSCA struct { 44 | Key any // *rsa.PrivateKey or *ecdsa.PrivateKey 45 | Cert *x509.Certificate 46 | KeyBits int // 1024 * 2^x 47 | Curve string // P224, P256, P384, P521 48 | 49 | RootCert *x509.Certificate 50 | RootKey any // *rsa.PrivateKey or *ecdsa.PrivateKey 51 | } 52 | 53 | // NewTLSCA create new tls CA 54 | func NewTLSCA(keyType string, keyBits int, curve string, rootCert *x509.Certificate, rootKey any) (*TLSCA, error) { 55 | tlsCA := &TLSCA{ 56 | KeyBits: keyBits, 57 | Curve: curve, 58 | } 59 | if rootCert != nil { 60 | tlsCA.RootCert = rootCert 61 | } 62 | if rootKey != nil { 63 | tlsCA.RootKey = rootKey 64 | } 65 | 66 | if err := tlsCA.CreateKey(keyType); err != nil { 67 | return nil, err 68 | } 69 | 70 | if err := tlsCA.CreateCert(); err != nil { 71 | return nil, err 72 | } 73 | 74 | return tlsCA, nil 75 | } 76 | 77 | // LoadTLSCA create new tls CA 78 | func LoadTLSCA(keyPath, certPath, password string) (*TLSCA, error) { 79 | keyBytes, kErr := os.ReadFile(keyPath) 80 | certBytes, cErr := os.ReadFile(certPath) 81 | if kErr != nil { 82 | return nil, kErr 83 | } else if cErr != nil { 84 | return nil, cErr 85 | } 86 | 87 | // parse key 88 | keyBlock, _ := pem.Decode(keyBytes) 89 | if keyBlock == nil { 90 | return nil, fmt.Errorf("decode key is nil") 91 | } 92 | 93 | // Check if PEM type is supported 94 | index := sort.SearchStrings(supportPemType, keyBlock.Type) 95 | if index >= len(supportPemType) || supportPemType[index] != keyBlock.Type { 96 | return nil, fmt.Errorf("unsupport PEM type %s", keyBlock.Type) 97 | } 98 | 99 | /* Fix x-ca/ca root/tls key Problem 100 | * https://github.com/x-ca/ca/blob/f82f6cc529662d5a751b79d87698a13c65f342ec/etc/root-ca.conf#L15 101 | * https://security.stackexchange.com/questions/93417/what-encryption-is-applied-on-a-key-generated-by-openssl-req 102 | * https://rfc-editor.org/rfc/rfc1423.html 103 | * openssl asn1parse -in root-ca.key -i | cut -c-90 104 | * 105 | * Note: The deprecated x509.IsEncryptedPEMBlock and x509.DecryptPEMBlock functions 106 | * have been replaced with direct PEM header checking 107 | */ 108 | var key any 109 | var err error 110 | 111 | // Check if the PEM block is encrypted by checking for headers 112 | isEncrypted := len(keyBlock.Headers) > 0 && keyBlock.Headers["Proc-Type"] == "4,ENCRYPTED" 113 | if isEncrypted { 114 | // Since x509.DecryptPEMBlock is deprecated, we recommend users decrypt their keys first 115 | return nil, fmt.Errorf("encrypted PEM blocks are not supported - please decrypt your key first using: openssl rsa -in encrypted.key -out decrypted.key") 116 | } 117 | 118 | // Parse the unencrypted key 119 | switch keyBlock.Type { 120 | case "RSA PRIVATE KEY": 121 | key, err = x509.ParsePKCS1PrivateKey(keyBlock.Bytes) 122 | case "EC PRIVATE KEY": 123 | key, err = x509.ParseECPrivateKey(keyBlock.Bytes) 124 | default: 125 | return nil, fmt.Errorf("unsupport PEM type %s", keyBlock.Type) 126 | } 127 | if err != nil { 128 | return nil, fmt.Errorf("load private key %s, error %s", keyPath, err) 129 | } 130 | 131 | // parse cert 132 | certBlock, _ := pem.Decode(certBytes) 133 | if certBlock == nil { 134 | return nil, fmt.Errorf("decode cert is nil") 135 | } else if certBlock.Type != "CERTIFICATE" { 136 | return nil, fmt.Errorf("unsupport PEM type %s", certBlock.Type) 137 | } 138 | cert, err := x509.ParseCertificate(certBlock.Bytes) 139 | if err != nil { 140 | return nil, fmt.Errorf("parse CA certificate %s, error %s", certPath, err) 141 | } 142 | 143 | // compare key and cert match? 144 | var pubKey any 145 | switch k := key.(type) { 146 | case *rsa.PrivateKey: 147 | pubKey = k.Public() 148 | case *ecdsa.PrivateKey: 149 | pubKey = k.Public() 150 | } 151 | keyPKBytes, keyPKErr := x509.MarshalPKIXPublicKey(pubKey) 152 | if keyPKErr != nil { 153 | return nil, keyPKErr 154 | } 155 | certPKBytes, certPKErr := x509.MarshalPKIXPublicKey(cert.PublicKey) 156 | if certPKErr != nil { 157 | return nil, certPKErr 158 | } 159 | if !bytes.Equal(keyPKBytes, certPKBytes) { 160 | return nil, fmt.Errorf("public key in CA certificate %s don't match private key in %s", certPath, keyPath) 161 | } 162 | 163 | tlsCA := &TLSCA{ 164 | Key: key, 165 | Cert: cert, 166 | } 167 | 168 | return tlsCA, nil 169 | } 170 | 171 | // CreateKey create tls key 172 | func (c *TLSCA) CreateKey(keyType string) error { 173 | switch strings.ToLower(keyType) { 174 | case "ec", "ecdsa": 175 | var curve elliptic.Curve 176 | switch c.Curve { 177 | case "P224": 178 | curve = elliptic.P224() 179 | case "P256": 180 | curve = elliptic.P256() 181 | case "P384": 182 | curve = elliptic.P384() 183 | case "P521": 184 | curve = elliptic.P521() 185 | default: 186 | return fmt.Errorf("unsupport curve %s", c.Curve) 187 | } 188 | tlsKey, err := ecdsa.GenerateKey(curve, rand.Reader) 189 | if err != nil { 190 | return err 191 | } 192 | c.Key = tlsKey 193 | case "rsa": 194 | tlsKey, err := rsa.GenerateKey(rand.Reader, c.KeyBits) 195 | if err != nil { 196 | return err 197 | } 198 | c.Key = tlsKey 199 | default: 200 | return fmt.Errorf("unsupport key type %s", keyType) 201 | } 202 | return nil 203 | } 204 | 205 | // CreateCert create tls cert 206 | func (c *TLSCA) CreateCert() error { 207 | tlsCSR := &x509.Certificate{ 208 | Version: 3, 209 | SerialNumber: randSerial(2), // default tls serial number is 2 210 | Subject: pkix.Name{ 211 | Country: []string{tlsCertCountry}, 212 | Organization: []string{tlsCertOrganization}, 213 | OrganizationalUnit: []string{tlsCertOrganizationalUnit}, 214 | CommonName: tlsCertCN, 215 | }, 216 | 217 | NotBefore: time.Now(), 218 | NotAfter: time.Now().AddDate(tlsCertYears, 0, 0), 219 | 220 | BasicConstraintsValid: true, 221 | IsCA: true, 222 | MaxPathLen: 0, 223 | MaxPathLenZero: true, 224 | KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, 225 | } 226 | 227 | var pubKey any 228 | switch k := c.Key.(type) { 229 | case *rsa.PrivateKey: 230 | pubKey = k.Public() 231 | case *ecdsa.PrivateKey: 232 | pubKey = k.Public() 233 | } 234 | 235 | der, err := x509.CreateCertificate(rand.Reader, tlsCSR, c.RootCert, pubKey, c.RootKey) 236 | if err != nil { 237 | return err 238 | } 239 | 240 | certificate, err := x509.ParseCertificate(der) 241 | if err != nil { 242 | return err 243 | } 244 | c.Cert = certificate 245 | return nil 246 | } 247 | 248 | // Write root key/cert to file 249 | func (c *TLSCA) Write(keyPath, certPath, chainPath string) error { 250 | var err error 251 | // mkdir 252 | err = os.MkdirAll(path.Dir(keyPath), 0700) 253 | if err != nil && !os.IsExist(err) { 254 | return err 255 | } 256 | 257 | err = os.MkdirAll(path.Dir(certPath), 0700) 258 | if err != nil && !os.IsExist(err) { 259 | return err 260 | } 261 | 262 | // write key 263 | keyFile, err := os.OpenFile(keyPath, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0600) 264 | if err != nil { 265 | return err 266 | } 267 | defer keyFile.Close() 268 | 269 | var keyType string 270 | var keyBytes []byte 271 | switch k := c.Key.(type) { 272 | case *rsa.PrivateKey: 273 | keyType = "RSA PRIVATE KEY" 274 | keyBytes = x509.MarshalPKCS1PrivateKey(k) 275 | case *ecdsa.PrivateKey: 276 | keyType = "EC PRIVATE KEY" 277 | keyBytes, err = x509.MarshalECPrivateKey(k) 278 | if err != nil { 279 | return err 280 | } 281 | } 282 | 283 | err = pem.Encode(keyFile, &pem.Block{ 284 | Type: keyType, 285 | Bytes: keyBytes, 286 | }) 287 | if err != nil { 288 | return err 289 | } 290 | 291 | // write cert 292 | certFile, err := os.OpenFile(certPath, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0600) 293 | if err != nil { 294 | return err 295 | } 296 | defer certFile.Close() 297 | 298 | err = pem.Encode(certFile, &pem.Block{ 299 | Type: "CERTIFICATE", 300 | Bytes: c.Cert.Raw, 301 | }) 302 | if err != nil { 303 | return err 304 | } 305 | 306 | // write chain 307 | chainFile, err := os.OpenFile(chainPath, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0600) 308 | if err != nil { 309 | return err 310 | } 311 | defer chainFile.Close() 312 | 313 | // root cert 314 | err = pem.Encode(chainFile, &pem.Block{ 315 | Type: "CERTIFICATE", 316 | Bytes: c.RootCert.Raw, 317 | }) 318 | if err != nil { 319 | return err 320 | } 321 | 322 | // tsl cert 323 | err = pem.Encode(chainFile, &pem.Block{ 324 | Type: "CERTIFICATE", 325 | Bytes: c.Cert.Raw, 326 | }) 327 | if err != nil { 328 | return err 329 | } 330 | 331 | return nil 332 | } 333 | 334 | func (c *TLSCA) Sign(commonName string, domains []string, ips []net.IP, days int, keyType string, keyBits int, curve string) (any, *x509.Certificate, error) { 335 | if keyBits%1024 != 0 { 336 | keyBits = 1024 * 4 337 | } 338 | if days/365 > tlsCertYears || days > MaxTLSDays { 339 | days = MaxTLSDays 340 | } 341 | 342 | // generate key 343 | var key any 344 | var err error 345 | switch strings.ToLower(keyType) { 346 | case "ec", "ecdsa": 347 | var ecCurve elliptic.Curve 348 | switch curve { 349 | case "P224": 350 | ecCurve = elliptic.P224() 351 | case "P256": 352 | ecCurve = elliptic.P256() 353 | case "P384": 354 | ecCurve = elliptic.P384() 355 | case "P521": 356 | ecCurve = elliptic.P521() 357 | default: 358 | return nil, nil, fmt.Errorf("unsupport curve %s", curve) 359 | } 360 | key, err = ecdsa.GenerateKey(ecCurve, rand.Reader) 361 | if err != nil { 362 | return nil, nil, err 363 | } 364 | case "rsa": 365 | key, err = rsa.GenerateKey(rand.Reader, keyBits) 366 | if err != nil { 367 | return nil, nil, err 368 | } 369 | default: 370 | return nil, nil, fmt.Errorf("unsupport key type %s", keyType) 371 | } 372 | 373 | // create csr 374 | csr := &x509.Certificate{ 375 | Version: 3, 376 | SerialNumber: randSerial(0), 377 | Subject: pkix.Name{ 378 | CommonName: commonName, 379 | }, 380 | 381 | NotBefore: time.Now(), 382 | NotAfter: time.Now().AddDate(0, 0, days), 383 | 384 | BasicConstraintsValid: true, 385 | IsCA: false, 386 | KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, 387 | ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}, 388 | } 389 | if len(domains) > 0 { 390 | csr.DNSNames = domains 391 | } 392 | if len(ips) > 0 { 393 | csr.IPAddresses = ips 394 | } 395 | 396 | var pubKey any 397 | switch k := key.(type) { 398 | case *rsa.PrivateKey: 399 | pubKey = k.Public() 400 | case *ecdsa.PrivateKey: 401 | pubKey = k.Public() 402 | } 403 | 404 | // create cert 405 | der, err := x509.CreateCertificate(rand.Reader, csr, c.Cert, pubKey, c.Key) 406 | if err != nil { 407 | return nil, nil, err 408 | } 409 | 410 | cert, err := x509.ParseCertificate(der) 411 | if err != nil { 412 | return nil, nil, err 413 | } 414 | 415 | return key, cert, nil 416 | } 417 | 418 | func (c *TLSCA) WriteCert(commonName string, key any, cert *x509.Certificate, tlsChainPath string) error { 419 | // mkdir 420 | var dir = strings.Replace(commonName, "*.", "", -1) 421 | err := os.MkdirAll(fmt.Sprintf("x-ca/certs/%s", dir), 0700) 422 | if err != nil && !os.IsExist(err) { 423 | return err 424 | } 425 | 426 | // write key 427 | keyPath := fmt.Sprintf("x-ca/certs/%s/%s.key", dir, commonName) 428 | keyFile, err := os.OpenFile(keyPath, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0600) 429 | if err != nil { 430 | return err 431 | } 432 | defer keyFile.Close() 433 | 434 | var keyType string 435 | var keyBytes []byte 436 | switch k := key.(type) { 437 | case *rsa.PrivateKey: 438 | keyType = "RSA PRIVATE KEY" 439 | keyBytes = x509.MarshalPKCS1PrivateKey(k) 440 | case *ecdsa.PrivateKey: 441 | keyType = "EC PRIVATE KEY" 442 | keyBytes, err = x509.MarshalECPrivateKey(k) 443 | if err != nil { 444 | return err 445 | } 446 | } 447 | 448 | err = pem.Encode(keyFile, &pem.Block{ 449 | Type: keyType, 450 | Bytes: keyBytes, 451 | }) 452 | if err != nil { 453 | return err 454 | } 455 | 456 | // write cert 457 | certPath := fmt.Sprintf("x-ca/certs/%s/%s.crt", dir, commonName) 458 | certFile, err := os.OpenFile(certPath, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0600) 459 | if err != nil { 460 | return err 461 | } 462 | defer certFile.Close() 463 | 464 | err = pem.Encode(certFile, &pem.Block{ 465 | Type: "CERTIFICATE", 466 | Bytes: cert.Raw, 467 | }) 468 | if err != nil { 469 | return err 470 | } 471 | 472 | // write cert chain 473 | certChainPath := fmt.Sprintf("x-ca/certs/%s/%s.bundle.crt", dir, commonName) 474 | certChainFile, err := os.OpenFile(certChainPath, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0600) 475 | if err != nil { 476 | return err 477 | } 478 | defer certChainFile.Close() 479 | 480 | certBytes, err := os.ReadFile(certPath) 481 | if err == nil { 482 | _, err := certChainFile.Write(certBytes) 483 | if err != nil { 484 | return err 485 | } 486 | } 487 | chainBytes, err := os.ReadFile(tlsChainPath) 488 | if err == nil { 489 | _, err := certChainFile.Write(chainBytes) 490 | if err != nil { 491 | return err 492 | } 493 | } 494 | 495 | // print 496 | fmt.Println("write cert to", fmt.Sprintf("./x-ca/certs/%s/{%s.key,%s.crt,%s.bundle.crt}", commonName, commonName, commonName, commonName)) 497 | 498 | return nil 499 | } 500 | --------------------------------------------------------------------------------