├── CODEOWNERS ├── depot ├── perms.go ├── test_tags.go ├── test_tags_windows.go ├── perms_windows.go ├── depot.go ├── depot_test.go └── pkix.go ├── .github ├── dependabot.yml └── workflows │ ├── go.yml │ └── release.yaml ├── NOTICE ├── .gitignore ├── go.mod ├── CONTRIBUTING.md ├── Dockerfile ├── cmd ├── curve.go ├── expiry.go ├── revoke_test.go ├── revoke.go ├── util.go ├── expiry_test.go ├── sign.go ├── init.go └── request_cert.go ├── certstrap.go ├── pkix ├── cert_info_test.go ├── cert_info.go ├── cert_host_test.go ├── crl_test.go ├── crl.go ├── cert_host.go ├── cert_auth_test.go ├── cert.go ├── csr_test.go ├── cert_auth.go ├── key.go ├── csr.go ├── cert_test.go └── key_test.go ├── tests ├── basic_test.go ├── ip_test.go ├── uri_test.go ├── not_ca_test.go └── workflow_test.go ├── go.sum ├── README.md └── LICENSE /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @square/idinfra-staff 2 | -------------------------------------------------------------------------------- /depot/perms.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | package depot 5 | 6 | const ( 7 | BranchPerm = 0440 8 | LeafPerm = 0444 9 | ) 10 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: gomod 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2015 Square, Inc. 2 | 3 | CoreOS Project 4 | Copyright 2014 CoreOS, Inc 5 | 6 | This product includes software developed at CoreOS, Inc. 7 | (http://www.coreos.com/). 8 | -------------------------------------------------------------------------------- /depot/test_tags.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | package depot 5 | 6 | var ( 7 | tag = &Tag{"host.pem", 0600} 8 | tag2 = &Tag{"host2.pem", 0600} 9 | ) 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | gopath 2 | bin 3 | out 4 | certstrap 5 | # Intellij Idea Generates following file 6 | # Nice to have in .gitignore 7 | .idea/ 8 | # .DS_Store file sometimes generated by Mac computers 9 | .DS_Store -------------------------------------------------------------------------------- /depot/test_tags_windows.go: -------------------------------------------------------------------------------- 1 | package depot 2 | 3 | // windows does not allow 0600 permissions so we need to set test tags to be 0666 4 | var ( 5 | tag = &Tag{"host.pem", 0666} 6 | tag2 = &Tag{"host2.pem", 0666} 7 | ) 8 | -------------------------------------------------------------------------------- /depot/perms_windows.go: -------------------------------------------------------------------------------- 1 | package depot 2 | 3 | const ( 4 | // 0440 is not supported on Windows (which only allows for all-read or all-write permissions) 5 | // Because our permissions checking requires permissions to meet a minimum criteria, 6 | // requiring 0440 for the leaf perm (key files) in windows will cause the permissions check to fail, 7 | // resulting permission denied errors for Windows users. 8 | BranchPerm = 0444 9 | LeafPerm = 0444 10 | ) 11 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/square/certstrap 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/howeyc/gopass v0.0.0-20170109162249-bf9dde6d0d2c 7 | github.com/urfave/cli v1.22.13 8 | go.step.sm/crypto v0.25.1 9 | ) 10 | 11 | require ( 12 | filippo.io/edwards25519 v1.0.0 // indirect 13 | github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect 14 | github.com/pkg/errors v0.9.1 // indirect 15 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 16 | golang.org/x/crypto v0.6.0 // indirect 17 | golang.org/x/sys v0.5.0 // indirect 18 | golang.org/x/term v0.5.0 // indirect 19 | ) 20 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | We appreciate your desire to contribute code to this repo. You may do so 4 | through GitHub by forking the repository and sending a pull request. 5 | 6 | When submitting code, please make every effort to follow existing conventions 7 | and style in order to keep the code as readable as possible. Please also make 8 | sure all tests pass by running `./test`, and format your code with `go fmt`. 9 | We also recommend using `golint`. 10 | 11 | Before your code can be accepted into the project you must also sign the 12 | Individual Contributor License Agreement. We use cla-assistant.io and you 13 | will be prompted to sign once a pull request is opened. 14 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Dockerfile for squareup/certstrap 2 | # 3 | # To build this image: 4 | # docker build -t squareup/certstrap . 5 | # 6 | # To run certstrap from the image (for example): 7 | # docker run --rm squareup/certstrap --version 8 | 9 | FROM golang:1.19-alpine as build 10 | 11 | WORKDIR /app 12 | 13 | COPY go.mod . 14 | COPY go.sum . 15 | 16 | # Download dependencies 17 | RUN go mod download 18 | 19 | # Copy source 20 | COPY . . 21 | 22 | # Build 23 | RUN CGO_ENABLED=0 go build -buildvcs=false -o /usr/bin/certstrap github.com/square/certstrap 24 | 25 | # Create a multi-stage build with the binary 26 | FROM gcr.io/distroless/static 27 | 28 | COPY --from=build /usr/bin/certstrap /usr/bin/certstrap 29 | 30 | ENTRYPOINT ["/usr/bin/certstrap"] 31 | -------------------------------------------------------------------------------- /cmd/curve.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "crypto/elliptic" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/square/certstrap/pkix" 9 | ) 10 | 11 | // curves is a map of canonical curve name (as specified by the -curve flag) 12 | // to function that creates a new key on that curve. 13 | var curves = map[string]func() (*pkix.Key, error){ 14 | "P-224": func() (*pkix.Key, error) { 15 | return pkix.CreateECDSAKey(elliptic.P224()) 16 | }, 17 | "P-256": func() (*pkix.Key, error) { 18 | return pkix.CreateECDSAKey(elliptic.P256()) 19 | }, 20 | "P-384": func() (*pkix.Key, error) { 21 | return pkix.CreateECDSAKey(elliptic.P384()) 22 | }, 23 | "P-521": func() (*pkix.Key, error) { 24 | return pkix.CreateECDSAKey(elliptic.P521()) 25 | }, 26 | "Ed25519": func() (*pkix.Key, error) { 27 | return pkix.CreateEd25519Key() 28 | }, 29 | } 30 | 31 | // supportedCurves returns the list of supported curve names as a comma separated 32 | // string for use in help text and error messages. 33 | func supportedCurves() string { 34 | result := make([]string, 0, len(curves)) 35 | for name := range curves { 36 | result = append(result, name) 37 | } 38 | return strings.Join(result, ", ") 39 | } 40 | 41 | func createKeyOnCurve(name string) (*pkix.Key, error) { 42 | create, ok := curves[name] 43 | if !ok { 44 | return nil, fmt.Errorf("unknown curve %q, curve must be one of %s", name, supportedCurves()) 45 | } 46 | return create() 47 | } 48 | -------------------------------------------------------------------------------- /certstrap.go: -------------------------------------------------------------------------------- 1 | /*- 2 | * Copyright 2015 Square Inc. 3 | * Copyright 2014 CoreOS 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package main 19 | 20 | import ( 21 | "os" 22 | 23 | "github.com/square/certstrap/cmd" 24 | "github.com/square/certstrap/depot" 25 | "github.com/urfave/cli" 26 | ) 27 | 28 | var release = "1.3.0" 29 | 30 | func main() { 31 | app := cli.NewApp() 32 | app.Name = "certstrap" 33 | app.Version = release 34 | app.Usage = "A simple certificate manager written in Go, to bootstrap your own certificate authority and public key infrastructure." 35 | app.Flags = []cli.Flag{ 36 | cli.StringFlag{ 37 | Name: "depot-path", 38 | Value: depot.DefaultFileDepotDir, 39 | Usage: "Location to store certificates, keys and other files.", 40 | EnvVar: "", 41 | }, 42 | } 43 | app.Author = "Square Inc., CoreOS" 44 | app.Email = "" 45 | app.Commands = []cli.Command{ 46 | cmd.NewInitCommand(), 47 | cmd.NewCertRequestCommand(), 48 | cmd.NewSignCommand(), 49 | cmd.NewRevokeCommand(), 50 | } 51 | app.Before = func(c *cli.Context) error { 52 | return cmd.InitDepot(c.String("depot-path")) 53 | } 54 | 55 | if err := app.Run(os.Args); err != nil { 56 | os.Exit(1) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /pkix/cert_info_test.go: -------------------------------------------------------------------------------- 1 | /*- 2 | * Copyright 2015 Square Inc. 3 | * Copyright 2014 CoreOS 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package pkix 19 | 20 | import ( 21 | "encoding/base64" 22 | "testing" 23 | ) 24 | 25 | const ( 26 | serialNumber = 10 27 | infoBASE64 = "MTA=" 28 | ) 29 | 30 | func TestCertificateAuthorityInfo(t *testing.T) { 31 | i := NewCertificateAuthorityInfo(serialNumber) 32 | 33 | i.IncSerialNumber() 34 | if i.SerialNumber.Uint64() != serialNumber+1 { 35 | t.Fatal("Failed incrementing serial number") 36 | } 37 | } 38 | 39 | func TestCertificateAuthorityInfoFromJSON(t *testing.T) { 40 | data, err := base64.StdEncoding.DecodeString(infoBASE64) 41 | if err != nil { 42 | t.Fatal("Failed decoding base64 string:", err) 43 | } 44 | 45 | i, err := NewCertificateAuthorityInfoFromJSON(data) 46 | if err != nil { 47 | t.Fatal("Failed init CertificateAuthorityInfo:", err) 48 | } 49 | 50 | if i.SerialNumber.Uint64() != serialNumber { 51 | t.Fatal("Failed getting correct serial number") 52 | } 53 | 54 | b, err := i.Export() 55 | if err != nil { 56 | t.Fatal("Failed exporting info:", err) 57 | } 58 | if base64.StdEncoding.EncodeToString(b) != infoBASE64 { 59 | t.Fatal("Failed exporting correct info") 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /cmd/expiry.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "strconv" 7 | "time" 8 | ) 9 | 10 | var nowFunc = time.Now 11 | 12 | func parseExpiry(fromNow string) (time.Time, error) { 13 | now := nowFunc().UTC() 14 | re := regexp.MustCompile(`\s*(\d+)\s*(day|month|year|hour|minute|second)s?`) 15 | matches := re.FindAllStringSubmatch(fromNow, -1) 16 | addDate := map[string]int{ 17 | "day": 0, 18 | "month": 0, 19 | "year": 0, 20 | "hour": 0, 21 | "minute": 0, 22 | "second": 0, 23 | } 24 | for _, r := range matches { 25 | number, err := strconv.ParseInt(r[1], 10, 32) 26 | if err != nil { 27 | return now, err 28 | } 29 | addDate[r[2]] = int(number) 30 | } 31 | 32 | // Ensure that we do not overflow time.Duration. 33 | // Doing so is silent and causes signed integer overflow like issues. 34 | if _, err := time.ParseDuration(fmt.Sprintf("%dh", addDate["hour"])); err != nil { 35 | return now, fmt.Errorf("hour unit too large to process") 36 | } else if _, err = time.ParseDuration(fmt.Sprintf("%dm", addDate["minute"])); err != nil { 37 | return now, fmt.Errorf("minute unit too large to process") 38 | } else if _, err = time.ParseDuration(fmt.Sprintf("%ds", addDate["second"])); err != nil { 39 | return now, fmt.Errorf("second unit too large to process") 40 | } 41 | 42 | result := now. 43 | AddDate(addDate["year"], addDate["month"], addDate["day"]). 44 | Add(time.Duration(addDate["hour"]) * time.Hour). 45 | Add(time.Duration(addDate["minute"]) * time.Minute). 46 | Add(time.Duration(addDate["second"]) * time.Second) 47 | 48 | if now == result { 49 | return now, fmt.Errorf("invalid or empty format") 50 | } 51 | 52 | // ASN.1 (encoding format used by SSL) only supports up to year 9999 53 | // https://www.openssl.org/docs/man1.1.0/crypto/ASN1_TIME_check.html 54 | if result.Year() > 9999 { 55 | return now, fmt.Errorf("proposed date too far in to the future: %s. Expiry year must be less than or equal to 9999", result) 56 | } 57 | 58 | return result, nil 59 | } 60 | -------------------------------------------------------------------------------- /pkix/cert_info.go: -------------------------------------------------------------------------------- 1 | /*- 2 | * Copyright 2015 Square Inc. 3 | * Copyright 2014 CoreOS 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package pkix 19 | 20 | import ( 21 | "math/big" 22 | ) 23 | 24 | // CertificateAuthorityInfo includes extra information required for CA 25 | type CertificateAuthorityInfo struct { 26 | // SerialNumber that has been used so far 27 | // Recorded to ensure all serial numbers issued by the CA are different 28 | SerialNumber *big.Int 29 | } 30 | 31 | // NewCertificateAuthorityInfo creates a new CertifaceAuthorityInfo with the given serial number 32 | func NewCertificateAuthorityInfo(serialNumber int64) *CertificateAuthorityInfo { 33 | return &CertificateAuthorityInfo{big.NewInt(serialNumber)} 34 | } 35 | 36 | // NewCertificateAuthorityInfoFromJSON creates a new CertifaceAuthorityInfo with the given JSON information 37 | func NewCertificateAuthorityInfoFromJSON(data []byte) (*CertificateAuthorityInfo, error) { 38 | i := big.NewInt(0) 39 | 40 | if err := i.UnmarshalJSON(data); err != nil { 41 | return nil, err 42 | } 43 | 44 | return &CertificateAuthorityInfo{i}, nil 45 | } 46 | 47 | // IncSerialNumber increments the given CA Info's serial number 48 | func (n *CertificateAuthorityInfo) IncSerialNumber() { 49 | n.SerialNumber.Add(n.SerialNumber, big.NewInt(1)) 50 | } 51 | 52 | // Export transfers the serial number to a JSON format 53 | func (n *CertificateAuthorityInfo) Export() ([]byte, error) { 54 | return n.SerialNumber.MarshalJSON() 55 | } 56 | -------------------------------------------------------------------------------- /tests/basic_test.go: -------------------------------------------------------------------------------- 1 | //go:build integration 2 | // +build integration 3 | 4 | /*- 5 | * Copyright 2015 Square Inc. 6 | * Copyright 2014 CoreOS 7 | * 8 | * Licensed under the Apache License, Version 2.0 (the "License"); 9 | * you may not use this file except in compliance with the License. 10 | * You may obtain a copy of the License at 11 | * 12 | * http://www.apache.org/licenses/LICENSE-2.0 13 | * 14 | * Unless required by applicable law or agreed to in writing, software 15 | * distributed under the License is distributed on an "AS IS" BASIS, 16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | * See the License for the specific language governing permissions and 18 | * limitations under the License. 19 | */ 20 | 21 | package tests 22 | 23 | import ( 24 | "bytes" 25 | "io" 26 | "os/exec" 27 | "strings" 28 | "testing" 29 | ) 30 | 31 | const ( 32 | depotDir = ".certstrap-test" 33 | hostname = "host1" 34 | passphrase = "123456" 35 | ) 36 | 37 | var binPath = "../certstrap" 38 | 39 | func run(command string, args ...string) (string, string, error) { 40 | var stdoutBytes, stderrBytes bytes.Buffer 41 | args = append([]string{"--depot-path", depotDir}, args...) 42 | cmd := exec.Command(command, args...) 43 | cmd.Stdout = &stdoutBytes 44 | cmd.Stderr = &stderrBytes 45 | err := cmd.Run() 46 | return stdoutBytes.String(), stderrBytes.String(), err 47 | } 48 | 49 | func runWithStdin(stdin io.Reader, command string, args ...string) (string, string, error) { 50 | var stdoutBytes, stderrBytes bytes.Buffer 51 | args = append([]string{"--depot-path", depotDir}, args...) 52 | cmd := exec.Command(command, args...) 53 | cmd.Stdin = stdin 54 | cmd.Stdout = &stdoutBytes 55 | cmd.Stderr = &stderrBytes 56 | err := cmd.Run() 57 | return stdoutBytes.String(), stderrBytes.String(), err 58 | } 59 | 60 | func TestVersion(t *testing.T) { 61 | stdout, stderr, err := run(binPath, "--version") 62 | if stderr != "" || err != nil { 63 | t.Fatalf("Received unexpected error: %v, %v", stderr, err) 64 | } 65 | if !strings.Contains(stdout, "version") { 66 | t.Fatalf("Received unexpected stdout: %v", stdout) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | 7 | build: 8 | name: Build 9 | runs-on: ubuntu-latest 10 | steps: 11 | 12 | - name: Set up Go 13 | uses: actions/setup-go@0caeaed6fd66a828038c2da3c0f662a42862658f # ratchet:actions/setup-go@v1 14 | with: 15 | go-version: 1.19 16 | id: go 17 | 18 | - name: Check out code into the Go module directory 19 | uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 # ratchet:actions/checkout@v2 20 | 21 | - name: Get dependencies 22 | run: go mod download 23 | 24 | - name: Run tests 25 | run: go test -v ./... 26 | 27 | - name: Build binaries 28 | run: go build -o . ./... 29 | 30 | - name: Run integration tests 31 | run: go test -v -tags=integration ./... 32 | 33 | lint: 34 | name: Lint 35 | runs-on: ubuntu-latest 36 | steps: 37 | 38 | - name: Check out code into the Go module directory 39 | uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 # ratchet:actions/checkout@v2 40 | 41 | - name: Run golangci-lint 42 | uses: golangci/golangci-lint-action@5c56cd6c9dc07901af25baab6f2b0d9f3b7c3018 # ratchet:golangci/golangci-lint-action@v2 43 | 44 | with: 45 | # Exclude deprecated PEM functions from the linter until 46 | # https://github.com/square/certstrap/issues/124 is resolved 47 | args: --exclude '(De|En)cryptPEMBlock' 48 | 49 | build-windows: 50 | name: Build Windows 51 | runs-on: windows-latest 52 | steps: 53 | 54 | - name: Set up Go 55 | uses: actions/setup-go@0caeaed6fd66a828038c2da3c0f662a42862658f # ratchet:actions/setup-go@v1 56 | with: 57 | go-version: 1.19 58 | id: go 59 | 60 | - name: Check out code into the Go module directory 61 | uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 # ratchet:actions/checkout@v2 62 | 63 | - name: Get dependencies 64 | run: go mod download 65 | 66 | - name: Run tests 67 | run: | 68 | go test -v ./... 69 | 70 | - name: Build binaries 71 | run: go build -o . ./... 72 | 73 | - name: Run integration tests 74 | run: | 75 | go test -v -tags=integration ./... 76 | -------------------------------------------------------------------------------- /pkix/cert_host_test.go: -------------------------------------------------------------------------------- 1 | /*- 2 | * Copyright 2015 Square Inc. 3 | * Copyright 2014 CoreOS 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package pkix 19 | 20 | import ( 21 | "bytes" 22 | "testing" 23 | "time" 24 | ) 25 | 26 | func TestCreateCertificateHost(t *testing.T) { 27 | crtAuth, err := NewCertificateFromPEM([]byte(certAuthPEM)) 28 | if err != nil { 29 | t.Fatal("Failed to parse certificate from PEM:", err) 30 | } 31 | 32 | key, err := NewKeyFromPrivateKeyPEM([]byte(rsaPrivKeyAuthPEM)) 33 | if err != nil { 34 | t.Fatal("Failed parsing RSA private key:", err) 35 | } 36 | 37 | csr, err := NewCertificateSigningRequestFromPEM([]byte(csrPEM)) 38 | if err != nil { 39 | t.Fatal("Failed parsing certificate request from PEM:", err) 40 | } 41 | 42 | crt, err := CreateCertificateHost(crtAuth, key, csr, time.Now().AddDate(5000, 0, 0)) 43 | if err != nil { 44 | t.Fatal("Failed creating certificate for host:", err) 45 | } 46 | if crt.GetExpirationDuration() > crtAuth.GetExpirationDuration() { 47 | t.Fatal("Cert expires after issuer") 48 | } 49 | rawCrt, err := crt.GetRawCertificate() 50 | if err != nil { 51 | t.Fatal("Failed to get x509.Certificate:", err) 52 | } 53 | 54 | rawCsr, err := csr.GetRawCertificateSigningRequest() 55 | if err != nil { 56 | t.Fatal("Failed to get x509.Certificate:", err) 57 | } 58 | if !bytes.Equal(rawCrt.RawSubject, rawCsr.RawSubject) { 59 | t.Fatalf("Failed to preserve subject: %s %s", rawCrt.RawSubject, rawCsr.RawSubject) 60 | } 61 | 62 | rawCrtAuth, err := crtAuth.GetRawCertificate() 63 | if err != nil { 64 | t.Fatal("Failed to get x509.Certificate:", err) 65 | } 66 | if err = rawCrt.CheckSignatureFrom(rawCrtAuth); err != nil { 67 | t.Fatal("Failed to check signature:", err) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /tests/ip_test.go: -------------------------------------------------------------------------------- 1 | // +build integration 2 | 3 | /*- 4 | * Copyright 2015 Square Inc. 5 | * Copyright 2014 CoreOS 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | package tests 21 | 22 | import ( 23 | "os" 24 | "strings" 25 | "testing" 26 | ) 27 | 28 | // Tests IP validation 29 | func TestIp(t *testing.T) { 30 | os.RemoveAll(depotDir) 31 | defer os.RemoveAll(depotDir) 32 | 33 | stdout, stderr, err := run(binPath, "request-cert", "--passphrase", passphrase, "--common-name", "CA", "--ip", "192.168.0.1") 34 | if stderr != "" || err != nil { 35 | t.Fatalf("Received unexpected error: %v, %v", stderr, err) 36 | } 37 | if strings.Count(stdout, "Created") != 2 { 38 | t.Fatalf("Received incorrect create: %v", stdout) 39 | } 40 | 41 | os.RemoveAll(depotDir) 42 | defer os.RemoveAll(depotDir) 43 | 44 | stdout, stderr, err = run(binPath, "request-cert", "--passphrase", passphrase, "--common-name", "CA", "--ip", "192.168.0.1,8.8.8.8") 45 | if stderr != "" || err != nil { 46 | t.Fatalf("Received unexpected error: %v, %v", stderr, err) 47 | } 48 | if strings.Count(stdout, "Created") != 2 { 49 | t.Fatalf("Received incorrect create: %v", stdout) 50 | } 51 | 52 | os.RemoveAll(depotDir) 53 | defer os.RemoveAll(depotDir) 54 | 55 | stdout, stderr, err = run(binPath, "request-cert", "--passphrase", passphrase, "--common-name", "CA", "--ip", "foo") 56 | if !strings.Contains(stderr, "Invalid IP address: foo") { 57 | t.Fatalf("Received unexpected : %v, %v", stderr, err) 58 | } 59 | 60 | stdout, stderr, err = run(binPath, "request-cert", "--passphrase", passphrase, "--common-name", "CA", "--ip", "192.168.0.1,8.8.8.8,bar,1.2.3.4") 61 | if !strings.Contains(stderr, "Invalid IP address: bar") { 62 | t.Fatalf("Received unexpected : %v, %v", stderr, err) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /tests/uri_test.go: -------------------------------------------------------------------------------- 1 | // +build integration 2 | 3 | /*- 4 | * Copyright 2015 Square Inc. 5 | * Copyright 2014 CoreOS 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | package tests 21 | 22 | import ( 23 | "os" 24 | "strings" 25 | "testing" 26 | ) 27 | 28 | // TODO: needs improvement 29 | func TestURI(t *testing.T) { 30 | os.RemoveAll(depotDir) 31 | defer os.RemoveAll(depotDir) 32 | 33 | stdout, stderr, err := run(binPath, "request-cert", "--passphrase", passphrase, "--common-name", "CA", "--uri", "http://test.test/test") 34 | if stderr != "" || err != nil { 35 | t.Fatalf("Received unexpected error: %v, %v", stderr, err) 36 | } 37 | if strings.Count(stdout, "Created") != 2 { 38 | t.Fatalf("Received incorrect create: %v", stdout) 39 | } 40 | 41 | os.RemoveAll(depotDir) 42 | defer os.RemoveAll(depotDir) 43 | 44 | stdout, stderr, err = run(binPath, "request-cert", "--passphrase", passphrase, "--common-name", "CA", "--uri", "test://192.168.0.1,test2://8.8.8.8") 45 | if stderr != "" || err != nil { 46 | t.Fatalf("Received unexpected error: %v, %v", stderr, err) 47 | } 48 | if strings.Count(stdout, "Created") != 2 { 49 | t.Fatalf("Received incorrect create: %v", stdout) 50 | } 51 | 52 | os.RemoveAll(depotDir) 53 | defer os.RemoveAll(depotDir) 54 | 55 | // Disallow URIs as "/" 56 | stdout, stderr, err = run(binPath, "request-cert", "--passphrase", passphrase, "--common-name", "CA", "--uri", "/") 57 | if !strings.Contains(stderr, "Invalid URI: /") { 58 | t.Fatalf("Received unexpected : %v, %v", stderr, err) 59 | } 60 | 61 | os.RemoveAll(depotDir) 62 | defer os.RemoveAll(depotDir) 63 | 64 | // Disallow URIs as "/test" 65 | stdout, stderr, err = run(binPath, "request-cert", "--passphrase", passphrase, "--common-name", "CA", "--uri", "/test") 66 | if !strings.Contains(stderr, "Invalid URI: /test") { 67 | t.Fatalf("Received unexpected : %v, %v", stderr, err) 68 | } 69 | os.RemoveAll(depotDir) 70 | defer os.RemoveAll(depotDir) 71 | } 72 | -------------------------------------------------------------------------------- /tests/not_ca_test.go: -------------------------------------------------------------------------------- 1 | // +build integration 2 | 3 | /*- 4 | * Copyright 2015 Square Inc. 5 | * Copyright 2014 CoreOS 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | package tests 21 | 22 | import ( 23 | "os" 24 | "strings" 25 | "testing" 26 | ) 27 | 28 | // Ensures certificates which aren't a CA can't sign other certificates. 29 | func TestNotCA(t *testing.T) { 30 | os.RemoveAll(depotDir) 31 | defer os.RemoveAll(depotDir) 32 | 33 | stdout, stderr, err := run(binPath, "init", "--passphrase", passphrase, "--common-name", "cert1") 34 | if stderr != "" || err != nil { 35 | t.Fatalf("Received unexpected error: %v, %v", stderr, err) 36 | } 37 | if strings.Count(stdout, "Created") != 3 { 38 | t.Fatalf("Received incorrect create: %v", stdout) 39 | } 40 | 41 | stdout, stderr, err = run(binPath, "request-cert", "--passphrase", passphrase, "--common-name", "cert2") 42 | if stderr != "" || err != nil { 43 | t.Fatalf("Received unexpected error: %v, %v", stderr, err) 44 | } 45 | if strings.Count(stdout, "Created") != 2 { 46 | t.Fatalf("Received incorrect create: %v", stdout) 47 | } 48 | 49 | stdout, stderr, err = run(binPath, "request-cert", "--passphrase", passphrase, "--common-name", "cert3") 50 | if stderr != "" || err != nil { 51 | t.Fatalf("Received unexpected error: %v, %v", stderr, err) 52 | } 53 | if strings.Count(stdout, "Created") != 2 { 54 | t.Fatalf("Received incorrect create: %v", stdout) 55 | } 56 | 57 | stdout, stderr, err = run(binPath, "sign", "--passphrase", passphrase, "--CA", "cert1", "cert2") 58 | if stderr != "" || err != nil { 59 | t.Fatalf("Received unexpected error: %v, %v", stderr, err) 60 | } 61 | if strings.Count(stdout, "Created") != 1 { 62 | t.Fatalf("Received incorrect create: %v", stdout) 63 | } 64 | 65 | stdout, stderr, err = run(binPath, "sign", "--passphrase", passphrase, "--CA", "cert2", "cert3") 66 | if stderr != "Selected CA certificate is not allowed to sign certificates.\n" { 67 | t.Fatalf("Failed to receive expected error.") 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /pkix/crl_test.go: -------------------------------------------------------------------------------- 1 | /*- 2 | * Copyright 2016 Square Inc. 3 | * Copyright 2014 CoreOS 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package pkix 19 | 20 | import ( 21 | "bytes" 22 | "testing" 23 | "time" 24 | ) 25 | 26 | const ( 27 | crlPEM = `-----BEGIN X509 CRL----- 28 | MIICfzBpMA0GCSqGSIb3DQEBCwUAMBMxETAPBgNVBAMTCENlcnRBdXRoFw0xNjAy 29 | MDQyMjAwMTdaFw0yNjAyMDQyMjAwMTdaMACgIzAhMB8GA1UdIwQYMBaAFIM33UgM 30 | CnTVX7cuOFiPIdvMsrzmMA0GCSqGSIb3DQEBCwUAA4ICAQBcrKZml+1XEb7iXiRX 31 | 3zkSlXqYmhW3WK5N2uF8+xpJWukkJNmQyM6FzeMWs0hZWTuN84lOBU4CmDjCglrt 32 | Bn6VtmdAQHf42ZTAMUkFDI8+DsfXHxEYrDp1//1Ljz7ybNhuanmXkVcsyNVN6Rn3 33 | LRV2g4tHSAtxMJBHg/CAQWI7vOzD6fDX+1JPMcmrAufglxPEc6r/I0N/CduIJMzO 34 | Ivb6A6Nx/fZmYJEMuvb9Mt9uwnPhC7iiktq0QiAixOG3yPBduQNl73vsuRoROGDn 35 | AYg+cIQ16jIqpaXYXj//QyfWWqqRl29TmXY1kRFZuH+hyAay30lcU+uUrAYqhG+N 36 | ZbrwE1vLtaUGTko36ZY6omqz/Do2dU5bxDbKWskkSLqFleLXtoJqZsKfE1ZdFW0+ 37 | iAPDJcl3jCKrs2lN3RinJj76LtLxmIiaK2AsDg/iLaplaqbjtx4xWDzvfiNAeo8k 38 | zEST4Zo0VXTJ/cxzx7Roe0kPFlCt/YNsOKLTOCfvjyFjMRcbcBlut7Fk7/VGWxsj 39 | XkF1bcyI7WPSM8Taq6lWhjHtRUDT3q1gPpUY1CBWJKQrKUwjBzVk81wCS6LJfdTG 40 | /5z7+UfcUAHh7Afm90hyk3nh+fPgSCQrRx9OC5kAJSLMKMvV9ikDZvr9rSkacowD 41 | lrpOuuKFsK22BhjvCNY2fLWn0A== 42 | -----END X509 CRL----- 43 | ` 44 | ) 45 | 46 | func TestCreateCertificateRevocationList(t *testing.T) { 47 | key, err := CreateRSAKey(rsaBits) 48 | if err != nil { 49 | t.Fatal("Failed creating rsa key:", err) 50 | } 51 | 52 | crt, err := CreateCertificateAuthority(key, "OU", time.Now().AddDate(5, 0, 0), "test", "US", "California", "San Francisco", "CA Name", nil) 53 | if err != nil { 54 | t.Fatal("Failed creating certificate authority:", err) 55 | } 56 | _, err = CreateCertificateRevocationList(key, crt, time.Now().AddDate(5, 0, 0)) 57 | if err != nil { 58 | t.Fatal("Failed creating crl:", err) 59 | } 60 | } 61 | 62 | func TestCertificateRevocationList(t *testing.T) { 63 | csr, err := NewCertificateRevocationListFromPEM([]byte(crlPEM)) 64 | if err != nil { 65 | t.Fatal("Failed parsing CRL from PEM:", err) 66 | } 67 | 68 | pemBytes, err := csr.Export() 69 | if err != nil { 70 | t.Fatal("Failed exporting PEM-format bytes:", err) 71 | } 72 | if !bytes.Equal(pemBytes, []byte(crlPEM)) { 73 | t.Fatal("Failed exporting the same PEM-format bytes") 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /pkix/crl.go: -------------------------------------------------------------------------------- 1 | /*- 2 | * Copyright 2016 Square Inc. 3 | * Copyright 2014 CoreOS 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package pkix 19 | 20 | import ( 21 | "bytes" 22 | "crypto/rand" 23 | "crypto/x509/pkix" 24 | "encoding/pem" 25 | "errors" 26 | "time" 27 | ) 28 | 29 | const ( 30 | crlPEMBlockType = "X509 CRL" 31 | ) 32 | 33 | func CreateCertificateRevocationList(key *Key, ca *Certificate, expiry time.Time) (*CertificateRevocationList, error) { 34 | rawCrt, err := ca.GetRawCertificate() 35 | if err != nil { 36 | return nil, err 37 | } 38 | 39 | crlBytes, err := rawCrt.CreateCRL(rand.Reader, key.Private, []pkix.RevokedCertificate{}, time.Now(), expiry) 40 | if err != nil { 41 | return nil, err 42 | } 43 | return NewCertificateRevocationListFromDER(crlBytes), nil 44 | } 45 | 46 | // CertificateSigningRequest is a wrapper around a x509 CertificateRequest and its DER-formatted bytes 47 | type CertificateRevocationList struct { 48 | derBytes []byte 49 | } 50 | 51 | //DERBytes returns DER-formatted bytes of the CRL. 52 | func (c *CertificateRevocationList) DERBytes() []byte { 53 | return c.derBytes 54 | } 55 | 56 | // NewCertificateRevocationListFromDER inits CertificateRevocationList from DER-format bytes 57 | func NewCertificateRevocationListFromDER(derBytes []byte) *CertificateRevocationList { 58 | return &CertificateRevocationList{derBytes: derBytes} 59 | } 60 | 61 | // NewCertificateRevocationListFromPEM inits CertificateRevocationList from PEM-format bytes 62 | func NewCertificateRevocationListFromPEM(data []byte) (*CertificateRevocationList, error) { 63 | pemBlock, _ := pem.Decode(data) 64 | if pemBlock == nil { 65 | return nil, errors.New("cannot find the next PEM formatted block") 66 | } 67 | if pemBlock.Type != crlPEMBlockType || len(pemBlock.Headers) != 0 { 68 | return nil, errors.New("unmatched type or headers") 69 | } 70 | return &CertificateRevocationList{derBytes: pemBlock.Bytes}, nil 71 | } 72 | 73 | // Export returns PEM-format bytes 74 | func (c *CertificateRevocationList) Export() ([]byte, error) { 75 | pemBlock := &pem.Block{ 76 | Type: crlPEMBlockType, 77 | Headers: nil, 78 | Bytes: c.derBytes, 79 | } 80 | 81 | buf := new(bytes.Buffer) 82 | if err := pem.Encode(buf, pemBlock); err != nil { 83 | return nil, err 84 | } 85 | return buf.Bytes(), nil 86 | } 87 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Release 3 | 4 | on: 5 | push: 6 | tags: [ "*" ] 7 | 8 | jobs: 9 | build: 10 | name: Build 11 | strategy: 12 | matrix: 13 | version: [1.19.x] 14 | target: 15 | - { os: 'darwin', platform: 'macos-latest', arch: 'amd64' } 16 | - { os: 'darwin', platform: 'macos-latest', arch: 'arm64' } 17 | - { os: 'linux', platform: 'ubuntu-latest', arch: 'amd64' } 18 | - { os: 'linux', platform: 'ubuntu-latest', arch: 'arm64' } 19 | - { os: 'windows', platform: 'windows-latest', arch: 'amd64' } 20 | runs-on: ${{ matrix.target.platform }} 21 | steps: 22 | - name: Set up toolchain 23 | uses: actions/setup-go@v2 24 | with: 25 | go-version: ${{ matrix.version }} 26 | id: go 27 | - name: Check out code 28 | uses: actions/checkout@v2 29 | - name: Build binary 30 | run: go build -o certstrap . 31 | - name: Upload artifact 32 | uses: actions/upload-artifact@v2 33 | with: 34 | name: certstrap-${{ matrix.target.os }}-${{ matrix.target.arch }} 35 | path: certstrap 36 | 37 | release: 38 | name: Create release 39 | runs-on: ubuntu-latest 40 | needs: [ build ] 41 | outputs: 42 | upload_url: ${{ steps.create_release.outputs.upload_url }} 43 | steps: 44 | - uses: actions/checkout@v2 45 | - name: Create release 46 | id: create_release 47 | uses: actions/create-release@v1 48 | env: 49 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 50 | with: 51 | tag_name: ${{ github.ref }} 52 | release_name: "Release Build (Draft)" 53 | body: "Release Build (from ${{ github.ref }}/${{ github.sha }})" 54 | draft: true 55 | prerelease: true 56 | 57 | add-assets: 58 | name: Add assets 59 | runs-on: ubuntu-latest 60 | needs: [ build, release ] 61 | strategy: 62 | matrix: 63 | target: 64 | - { os: 'darwin', arch: 'amd64' } 65 | - { os: 'darwin', arch: 'arm64' } 66 | - { os: 'linux', arch: 'amd64' } 67 | - { os: 'linux', arch: 'arm64' } 68 | - { os: 'windows', arch: 'amd64' } 69 | steps: 70 | - uses: actions/checkout@v2 71 | - name: Download artifact 72 | uses: actions/download-artifact@v2 73 | with: 74 | name: certstrap-${{ matrix.target.os }}-${{ matrix.target.arch }} 75 | path: dist 76 | - name: Upload artifact to release 77 | uses: actions/upload-release-asset@v1 78 | env: 79 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 80 | with: 81 | upload_url: ${{ needs.release.outputs.upload_url }} 82 | asset_path: ./dist/certstrap 83 | asset_name: certstrap-${{ matrix.target.os }}-${{ matrix.target.arch }} 84 | asset_content_type: application/octet-stream 85 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | filippo.io/edwards25519 v1.0.0 h1:0wAIcmJUqRdI8IJ/3eGi5/HwXZWPujYXXlkrQogz0Ek= 2 | filippo.io/edwards25519 v1.0.0/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns= 3 | github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= 4 | github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= 5 | github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 6 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 8 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/howeyc/gopass v0.0.0-20170109162249-bf9dde6d0d2c h1:kQWxfPIHVLbgLzphqk3QUflDy9QdksZR4ygR807bpy0= 10 | github.com/howeyc/gopass v0.0.0-20170109162249-bf9dde6d0d2c/go.mod h1:lADxMC39cJJqL93Duh1xhAs4I2Zs8mKS89XWXFGp9cs= 11 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 12 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 13 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 14 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 15 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 16 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 17 | github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262 h1:unQFBIznI+VYD1/1fApl1A+9VcBk+9dcqGfnePY87LY= 18 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 19 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 20 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 21 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 22 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 23 | github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= 24 | github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 25 | github.com/urfave/cli v1.22.13 h1:wsLILXG8qCJNse/qAgLNf23737Cx05GflHg/PJGe1Ok= 26 | github.com/urfave/cli v1.22.13/go.mod h1:VufqObjsMTF2BBwKawpx9R8eAneNEWhoO0yx8Vd+FkE= 27 | go.step.sm/crypto v0.25.1 h1:e08ioZBiZoHrWG0tJOUDPwqoF3PTRiFebINDEw3yPpo= 28 | go.step.sm/crypto v0.25.1/go.mod h1:4pUEuZ+4OAf2f70RgW5oRv/rJudibcAAWQg5prC3DT8= 29 | golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc= 30 | golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= 31 | golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= 32 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 33 | golang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY= 34 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 35 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 36 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 37 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 38 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 39 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 40 | -------------------------------------------------------------------------------- /pkix/cert_host.go: -------------------------------------------------------------------------------- 1 | /*- 2 | * Copyright 2015 Square Inc. 3 | * Copyright 2014 CoreOS 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package pkix 19 | 20 | import ( 21 | "crypto/rand" 22 | "crypto/x509" 23 | "crypto/x509/pkix" 24 | "math/big" 25 | "time" 26 | ) 27 | 28 | // CreateCertificateHost creates certificate for host. 29 | // The arguments include CA certificate, CA key, certificate request. 30 | func CreateCertificateHost(crtAuth *Certificate, keyAuth *Key, csr *CertificateSigningRequest, proposedExpiry time.Time) (*Certificate, error) { 31 | // Build CA based on RFC5280 32 | hostTemplate := x509.Certificate{ 33 | // **SHOULD** be filled in a unique number 34 | SerialNumber: big.NewInt(0), 35 | // **SHOULD** be filled in host info 36 | Subject: pkix.Name{}, 37 | // NotBefore is set to be 10min earlier to fix gap on time difference in cluster 38 | NotBefore: time.Now().Add(-10*time.Minute).UTC(), 39 | // 10-year lease 40 | NotAfter: time.Time{}, 41 | // Used for certificate signing only 42 | KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDataEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageKeyAgreement, 43 | 44 | ExtKeyUsage: []x509.ExtKeyUsage{ 45 | x509.ExtKeyUsageServerAuth, 46 | x509.ExtKeyUsageClientAuth, 47 | }, 48 | UnknownExtKeyUsage: nil, 49 | 50 | BasicConstraintsValid: false, 51 | 52 | // 160-bit SHA-1 hash of the value of the BIT STRING subjectPublicKey 53 | // (excluding the tag, length, and number of unused bits) 54 | // **SHOULD** be filled in later 55 | SubjectKeyId: nil, 56 | 57 | // Subject Alternative Name 58 | DNSNames: nil, 59 | 60 | PermittedDNSDomainsCritical: false, 61 | PermittedDNSDomains: nil, 62 | } 63 | 64 | serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) 65 | serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) 66 | if err != nil { 67 | return nil, err 68 | } 69 | hostTemplate.SerialNumber.Set(serialNumber) 70 | 71 | rawCsr, err := csr.GetRawCertificateSigningRequest() 72 | if err != nil { 73 | return nil, err 74 | } 75 | 76 | // pkix.Name{} doesn't take ordering into account. 77 | // RawSubject works because CreateCertificate() first checks if 78 | // RawSubject has a value. 79 | hostTemplate.RawSubject = rawCsr.RawSubject 80 | 81 | caExpiry := time.Now().Add(crtAuth.GetExpirationDuration()) 82 | // ensure cert doesn't expire after issuer 83 | if caExpiry.Before(proposedExpiry) { 84 | hostTemplate.NotAfter = caExpiry 85 | } else { 86 | hostTemplate.NotAfter = proposedExpiry 87 | } 88 | 89 | hostTemplate.SubjectKeyId, err = GenerateSubjectKeyID(rawCsr.PublicKey) 90 | if err != nil { 91 | return nil, err 92 | } 93 | 94 | hostTemplate.IPAddresses = rawCsr.IPAddresses 95 | hostTemplate.DNSNames = rawCsr.DNSNames 96 | hostTemplate.URIs = rawCsr.URIs 97 | 98 | rawCrtAuth, err := crtAuth.GetRawCertificate() 99 | if err != nil { 100 | return nil, err 101 | } 102 | 103 | crtHostBytes, err := x509.CreateCertificate(rand.Reader, &hostTemplate, rawCrtAuth, rawCsr.PublicKey, keyAuth.Private) 104 | if err != nil { 105 | return nil, err 106 | } 107 | 108 | return NewCertificateFromDER(crtHostBytes), nil 109 | } 110 | -------------------------------------------------------------------------------- /cmd/revoke_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "crypto/x509" 5 | "flag" 6 | "os" 7 | "testing" 8 | "time" 9 | 10 | "github.com/square/certstrap/depot" 11 | "github.com/square/certstrap/pkix" 12 | "github.com/urfave/cli" 13 | ) 14 | 15 | const ( 16 | caName = "ca" 17 | cnName = "cn" 18 | ) 19 | 20 | func TestRevokeCmd(t *testing.T) { 21 | tmp, err := os.MkdirTemp("", "certstrap-revoke") 22 | if err != nil { 23 | t.Fatalf("could not create tmp dir: %v", err) 24 | } 25 | defer os.RemoveAll(tmp) 26 | 27 | d, err = depot.NewFileDepot(tmp) 28 | if err != nil { 29 | t.Fatalf("could not create file depot: %v", err) 30 | } 31 | 32 | setupCA(t, d) 33 | setupCN(t, d) 34 | 35 | fs := flag.NewFlagSet("test", flag.ContinueOnError) 36 | fs.String("CA", "", "") 37 | fs.String("CN", "", "") 38 | if err := fs.Parse([]string{"-CA", "ca", "-CN", "cn"}); err != nil { 39 | t.Fatal("could not parse flags") 40 | } 41 | 42 | new(revokeCommand).run(cli.NewContext(nil, fs, nil)) 43 | 44 | list, err := depot.GetCertificateRevocationList(d, caName) 45 | if err != nil { 46 | t.Fatalf("could not get crl: %v", err) 47 | } 48 | 49 | certList, err := x509.ParseDERCRL(list.DERBytes()) 50 | if err != nil { 51 | t.Fatalf("could not parse crl: %v", err) 52 | } 53 | 54 | if len(certList.TBSCertList.RevokedCertificates) != 1 { 55 | t.Fatalf("unexpected number of revoked certs: want = 1, got = %d", len(certList.TBSCertList.RevokedCertificates)) 56 | } 57 | 58 | cnCert, _ := depot.GetCertificate(d, cnName) 59 | cnX509, _ := cnCert.GetRawCertificate() 60 | 61 | if cnX509.SerialNumber.Cmp(certList.TBSCertList.RevokedCertificates[0].SerialNumber) != 0 { 62 | t.Fatalf("certificates serial numbers are not equal") 63 | } 64 | } 65 | 66 | func setupCA(t *testing.T, dt depot.Depot) { 67 | // create private key 68 | key, err := pkix.CreateRSAKey(2048) 69 | if err != nil { 70 | t.Fatalf("could not create RSA key: %v", err) 71 | } 72 | if err = depot.PutPrivateKey(dt, caName, key); err != nil { 73 | t.Fatalf("could not put private key: %v", err) 74 | } 75 | 76 | // create certificate authority 77 | caCert, err := pkix.CreateCertificateAuthority(key, caName, time.Now().Add(1*time.Minute), "", "", "", "", caName, nil) 78 | if err != nil { 79 | t.Fatalf("could not create authority cert: %v", err) 80 | } 81 | if err = depot.PutCertificate(dt, caName, caCert); err != nil { 82 | t.Fatalf("could not put certificate: %v", err) 83 | } 84 | 85 | // create an empty certificate revocation list 86 | crl, err := pkix.CreateCertificateRevocationList(key, caCert, time.Now().Add(1*time.Minute)) 87 | if err != nil { 88 | t.Fatalf("could not create crl: %v", err) 89 | } 90 | if err = depot.PutCertificateRevocationList(dt, caName, crl); err != nil { 91 | t.Fatalf("could not put crl: %v", err) 92 | } 93 | } 94 | 95 | func setupCN(t *testing.T, dt depot.Depot) { 96 | // create private key 97 | key, err := pkix.CreateRSAKey(2048) 98 | if err != nil { 99 | t.Fatalf("could not create RSA key: %v", err) 100 | } 101 | if err = depot.PutPrivateKey(dt, cnName, key); err != nil { 102 | t.Fatalf("could not put private key: %v", err) 103 | } 104 | 105 | csr, err := pkix.CreateCertificateSigningRequest(key, cnName, nil, []string{"example.com"}, nil, "", "", "", "", cnName) 106 | if err != nil { 107 | t.Fatalf("could not create csr: %v", err) 108 | } 109 | 110 | caCert, err := depot.GetCertificate(dt, caName) 111 | if err != nil { 112 | t.Fatalf("could not get cert: %v", err) 113 | } 114 | 115 | caKey, err := depot.GetPrivateKey(dt, caName) 116 | if err != nil { 117 | t.Fatalf("could not get CA key: %v", err) 118 | } 119 | 120 | cnCert, err := pkix.CreateCertificateHost(caCert, caKey, csr, time.Now().Add(1*time.Hour)) 121 | if err != nil { 122 | t.Fatalf("could not create cert host: %v", err) 123 | } 124 | if err = depot.PutCertificate(dt, "cn", cnCert); err != nil { 125 | t.Fatalf("could not put cert: %v", err) 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /cmd/revoke.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "crypto/rand" 5 | "crypto/x509" 6 | x509pkix "crypto/x509/pkix" 7 | "errors" 8 | "fmt" 9 | "os" 10 | "strings" 11 | "time" 12 | 13 | "github.com/square/certstrap/depot" 14 | "github.com/square/certstrap/pkix" 15 | "github.com/urfave/cli" 16 | ) 17 | 18 | type revokeCommand struct { 19 | ca, cn string 20 | } 21 | 22 | // NewRevokeCommand revokes the given certificate by adding it to the CA's CRL. 23 | func NewRevokeCommand() cli.Command { 24 | return cli.Command{ 25 | Name: "revoke", 26 | Usage: "Revoke certificate", 27 | Description: "Add certificate to the CA's CRL.", 28 | Flags: []cli.Flag{ 29 | cli.StringFlag{ 30 | Name: "passphrase", 31 | Usage: "Passphrase to decrypt private-key PEM block of CA", 32 | }, 33 | cli.StringFlag{ 34 | Name: "CN", 35 | Usage: "Common Name (CN) of certificate to revoke", 36 | }, 37 | cli.StringFlag{ 38 | Name: "CA", 39 | Usage: "Name of CA under which certificate was issued", 40 | }, 41 | }, 42 | Action: new(revokeCommand).run, 43 | } 44 | } 45 | 46 | func (c *revokeCommand) checkErr(err error) { 47 | if err != nil { 48 | fmt.Fprintln(os.Stderr, err.Error()) 49 | os.Exit(1) 50 | } 51 | } 52 | 53 | func (c *revokeCommand) parseArgs(ctx *cli.Context) error { 54 | if ctx.String("CA") == "" { 55 | return errors.New("CA name must be provided") 56 | } 57 | c.ca = strings.Replace(ctx.String("CA"), " ", "_", -1) 58 | 59 | if ctx.String("CN") == "" { 60 | return errors.New("CN name must be provided") 61 | } 62 | c.cn = strings.Replace(ctx.String("CN"), " ", "_", -1) 63 | 64 | return nil 65 | } 66 | 67 | func (c *revokeCommand) run(ctx *cli.Context) { 68 | c.checkErr(c.parseArgs(ctx)) 69 | 70 | caCert, err := c.CAx509Certificate() 71 | c.checkErr(err) 72 | 73 | cnCert, err := c.CNx509Certificate() 74 | c.checkErr(err) 75 | 76 | revoked, err := c.revokedCertificates() 77 | c.checkErr(err) 78 | 79 | revoked = append(revoked, x509pkix.RevokedCertificate{ 80 | SerialNumber: cnCert.SerialNumber, 81 | RevocationTime: time.Now(), 82 | }) 83 | 84 | err = c.saveRevokedCertificates(ctx, caCert, revoked) 85 | c.checkErr(err) 86 | } 87 | 88 | func (c *revokeCommand) CAx509Certificate() (*x509.Certificate, error) { 89 | cert, err := depot.GetCertificate(d, c.ca) 90 | if err != nil { 91 | return nil, err 92 | } 93 | return cert.GetRawCertificate() 94 | } 95 | 96 | func (c *revokeCommand) CNx509Certificate() (*x509.Certificate, error) { 97 | cert, err := depot.GetCertificate(d, c.cn) 98 | if err != nil { 99 | return nil, err 100 | } 101 | return cert.GetRawCertificate() 102 | } 103 | 104 | func (c *revokeCommand) revokedCertificates() ([]x509pkix.RevokedCertificate, error) { 105 | list, err := depot.GetCertificateRevocationList(d, c.ca) 106 | if err != nil { 107 | return nil, err 108 | } 109 | 110 | certList, err := x509.ParseDERCRL(list.DERBytes()) 111 | if err != nil { 112 | return nil, err 113 | } 114 | 115 | return certList.TBSCertList.RevokedCertificates, nil 116 | } 117 | 118 | func (c *revokeCommand) saveRevokedCertificates(ctx *cli.Context, cert *x509.Certificate, list []x509pkix.RevokedCertificate) error { 119 | privateKey, err := depot.GetPrivateKey(d, c.ca) 120 | if err != nil { 121 | pass, err := getPassPhrase(ctx, "CA key") 122 | if err != nil { 123 | return fmt.Errorf("error retreiving passphrase when saving revoked certificates: %v", err) 124 | } 125 | privateKey, err = depot.GetEncryptedPrivateKey(d, c.ca, pass) 126 | if err != nil { 127 | return fmt.Errorf("get CA key error when saving revoked certificates: %v", err) 128 | } 129 | } 130 | 131 | crlBytes, err := cert.CreateCRL(rand.Reader, privateKey.Private, list, time.Now(), time.Now().Add(2*8760*time.Hour)) 132 | if err != nil { 133 | return fmt.Errorf("could not create CRL: %v", err) 134 | } 135 | if err := d.Delete(depot.CrlTag(c.ca)); err != nil { 136 | return fmt.Errorf("could not delete CRL: %v", err) 137 | } 138 | if err = depot.PutCertificateRevocationList(d, c.ca, pkix.NewCertificateRevocationListFromDER(crlBytes)); err != nil { 139 | return fmt.Errorf("could not put revokation list: %v", err) 140 | } 141 | return nil 142 | } 143 | -------------------------------------------------------------------------------- /tests/workflow_test.go: -------------------------------------------------------------------------------- 1 | //go:build integration 2 | 3 | /*- 4 | * Copyright 2015 Square Inc. 5 | * Copyright 2014 CoreOS 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | package tests 21 | 22 | import ( 23 | "crypto/x509" 24 | "encoding/pem" 25 | "os" 26 | "path" 27 | "strings" 28 | "testing" 29 | ) 30 | 31 | // TestWorkflow runs certstrap in the normal workflow 32 | // and traverses all commands and all key algorithms. 33 | func TestWorkflow(t *testing.T) { 34 | tests := []struct { 35 | desc string 36 | keySpec []string 37 | expected x509.PublicKeyAlgorithm 38 | }{{ 39 | desc: "default RSA", 40 | expected: x509.RSA, 41 | }, { 42 | desc: "P-256", 43 | keySpec: []string{"--curve", "P-256"}, 44 | expected: x509.ECDSA, 45 | }, { 46 | desc: "P-521", 47 | keySpec: []string{"--curve", "P-521"}, 48 | expected: x509.ECDSA, 49 | }, { 50 | desc: "Ed25519", 51 | keySpec: []string{"--curve", "Ed25519"}, 52 | expected: x509.Ed25519, 53 | }, { 54 | desc: "RSA 2048", 55 | keySpec: []string{"--key-bits", "2048"}, 56 | expected: x509.RSA, 57 | }} 58 | 59 | for _, tc := range tests { 60 | t.Run(tc.desc, func(t *testing.T) { 61 | os.RemoveAll(depotDir) 62 | defer os.RemoveAll(depotDir) 63 | 64 | use_uri := "test://test/test" 65 | 66 | args := []string{"init", "--passphrase", passphrase, "--common-name", "CA"} 67 | args = append(args, tc.keySpec...) 68 | stdout, stderr, err := run(binPath, args...) 69 | if stderr != "" || err != nil { 70 | t.Fatalf("Received unexpected error: %v, %v", stdout, err) 71 | } 72 | if strings.Count(stdout, "Created") != 3 { 73 | t.Fatalf("Received incorrect create: %v", stdout) 74 | } 75 | 76 | args = []string{"request-cert", "--passphrase", passphrase, "--common-name", hostname, "--uri", use_uri} 77 | args = append(args, tc.keySpec...) 78 | stdout, stderr, err = run(binPath, args...) 79 | if stderr != "" || err != nil { 80 | t.Fatalf("Received unexpected error: %v, %v", stderr, err) 81 | } 82 | if strings.Count(stdout, "Created") != 2 { 83 | t.Fatalf("Received incorrect create: %v", stdout) 84 | } 85 | 86 | stdout, stderr, err = run(binPath, "request-cert", "--passphrase", passphrase, "--ip", "127.0.0.1,8.8.8.8", "--common-name", "127.0.0.1") 87 | if stderr != "" || err != nil { 88 | t.Fatalf("Received unexpected error: %v, %v", stderr, err) 89 | } 90 | if strings.Count(stdout, "Created") != 2 { 91 | t.Fatalf("Received incorrect create: %v", stdout) 92 | } 93 | 94 | stdout, stderr, err = run(binPath, "sign", "--passphrase", passphrase, "--CA", "CA", hostname) 95 | if stderr != "" || err != nil { 96 | t.Fatalf("Received unexpected error: %v, %v", stderr, err) 97 | } 98 | if strings.Count(stdout, "Created") != 1 { 99 | t.Fatalf("Received incorrect create: %v", stdout) 100 | } 101 | 102 | fcontents, err := os.ReadFile(path.Join(depotDir, strings.Join([]string{hostname, ".crt"}, ""))) 103 | if err != nil { 104 | t.Fatalf("Reading cert failed: %v", err) 105 | } 106 | der, _ := pem.Decode(fcontents) 107 | cert, err := x509.ParseCertificate(der.Bytes) 108 | if !(len(cert.URIs) == 1 && cert.URIs[0].String() == use_uri) { 109 | t.Fatalf("URI not reflected in cert") 110 | } 111 | if cert.PublicKeyAlgorithm != tc.expected { 112 | t.Fatalf("Public key algorithm = %d, want %d", cert.PublicKeyAlgorithm, tc.expected) 113 | } 114 | 115 | stdout, stderr, err = run(binPath, "revoke", "--passphrase", passphrase, "--CN", hostname, "--CA", "CA") 116 | if stderr != "" || err != nil { 117 | t.Fatalf("Received unexpected error: %v, %v", stderr, err) 118 | } 119 | if strings.Count(stdout, hostname) != 0 { 120 | t.Fatalf("Received incorrect create: %v", stdout) 121 | } 122 | }) 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /cmd/util.go: -------------------------------------------------------------------------------- 1 | /*- 2 | * Copyright 2015 Square Inc. 3 | * Copyright 2014 CoreOS 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package cmd 19 | 20 | import ( 21 | "bytes" 22 | "errors" 23 | "fmt" 24 | "os" 25 | 26 | "github.com/howeyc/gopass" 27 | "github.com/square/certstrap/depot" 28 | "github.com/square/certstrap/pkix" 29 | "github.com/urfave/cli" 30 | ) 31 | 32 | var ( 33 | d *depot.FileDepot 34 | depotDir string 35 | ) 36 | 37 | // InitDepot creates the depot directory, which stores key/csr/crt files 38 | func InitDepot(path string) error { 39 | depotDir = path 40 | if d == nil { 41 | var err error 42 | if d, err = depot.NewFileDepot(path); err != nil { 43 | return err 44 | } 45 | } 46 | return nil 47 | } 48 | 49 | func createPassPhrase() ([]byte, error) { 50 | pass1, err := gopass.GetPasswdPrompt("Enter passphrase (empty for no passphrase): ", false, os.Stdin, os.Stdout) 51 | if err != nil { 52 | return nil, err 53 | } 54 | pass2, err := gopass.GetPasswdPrompt("Enter same passphrase again: ", false, os.Stdin, os.Stdout) 55 | if err != nil { 56 | return nil, err 57 | } 58 | 59 | if !bytes.Equal(pass1, pass2) { 60 | return nil, errors.New("Passphrases do not match.") 61 | } 62 | return pass1, nil 63 | } 64 | 65 | func askPassPhrase(name string) ([]byte, error) { 66 | pass, err := gopass.GetPasswdPrompt(fmt.Sprintf("Enter passphrase for %v (empty for no passphrase): ", name), false, os.Stdin, os.Stdout) 67 | if err != nil { 68 | return nil, err 69 | } 70 | return pass, nil 71 | } 72 | 73 | func getPassPhrase(c *cli.Context, name string) ([]byte, error) { 74 | if c.IsSet("passphrase") { 75 | return []byte(c.String("passphrase")), nil 76 | } 77 | return askPassPhrase(name) 78 | } 79 | 80 | func putCertificate(c *cli.Context, d *depot.FileDepot, name string, crt *pkix.Certificate) error { 81 | if c.IsSet("cert") { 82 | bytes, err := crt.Export() 83 | if err != nil { 84 | return err 85 | } 86 | return os.WriteFile(c.String("cert"), bytes, depot.LeafPerm) 87 | } 88 | return depot.PutCertificate(d, name, crt) 89 | } 90 | 91 | func putCertificateSigningRequest(c *cli.Context, d *depot.FileDepot, name string, csr *pkix.CertificateSigningRequest) error { 92 | if c.IsSet("csr") { 93 | bytes, err := csr.Export() 94 | if err != nil { 95 | return err 96 | } 97 | return os.WriteFile(c.String("csr"), bytes, depot.LeafPerm) 98 | } 99 | return depot.PutCertificateSigningRequest(d, name, csr) 100 | } 101 | 102 | func getCertificateSigningRequest(c *cli.Context, d *depot.FileDepot, name string) (*pkix.CertificateSigningRequest, error) { 103 | if c.IsSet("csr") { 104 | bytes, err := os.ReadFile(c.String("csr")) 105 | if err != nil { 106 | return nil, err 107 | } 108 | return pkix.NewCertificateSigningRequestFromPEM(bytes) 109 | } 110 | return depot.GetCertificateSigningRequest(d, name) 111 | } 112 | 113 | func putEncryptedPrivateKey(c *cli.Context, d *depot.FileDepot, name string, key *pkix.Key, passphrase []byte) error { 114 | if c.IsSet("key") { 115 | if fileExists(c.String("key")) { 116 | return nil 117 | } 118 | 119 | bytes, err := key.ExportEncryptedPrivate(passphrase) 120 | if err != nil { 121 | return err 122 | } 123 | return os.WriteFile(c.String("key"), bytes, depot.BranchPerm) 124 | } 125 | return depot.PutEncryptedPrivateKey(d, name, key, passphrase) 126 | } 127 | 128 | func putPrivateKey(c *cli.Context, d *depot.FileDepot, name string, key *pkix.Key) error { 129 | if c.IsSet("key") { 130 | if fileExists(c.String("key")) { 131 | return nil 132 | } 133 | 134 | bytes, err := key.ExportPrivate() 135 | if err != nil { 136 | return err 137 | } 138 | return os.WriteFile(c.String("key"), bytes, depot.BranchPerm) 139 | } 140 | return depot.PutPrivateKey(d, name, key) 141 | } 142 | 143 | func fileExists(filepath string) bool { 144 | _, err := os.Stat(filepath) 145 | return !os.IsNotExist(err) 146 | } 147 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # certstrap 2 | [![godoc](http://img.shields.io/badge/godoc-certstrap-blue.svg?style=flat)](https://godoc.org/github.com/square/certstrap) 3 | [![CI](https://github.com/square/certstrap/actions/workflows/go.yml/badge.svg)](https://github.com/square/certstrap/actions/workflows/go.yml) 4 | [![license](http://img.shields.io/badge/license-apache_2.0-red.svg?style=flat)](https://raw.githubusercontent.com/square/certstrap/master/LICENSE) 5 | 6 | A simple certificate manager written in Go, to bootstrap your own certificate authority and public key infrastructure. Adapted from etcd-ca. 7 | 8 | certstrap is a very convenient app if you don't feel like dealing with openssl, its myriad of options or config files. 9 | 10 | ## Common Uses 11 | 12 | certstrap allows you to build your own certificate system: 13 | 14 | 1. Initialize certificate authorities 15 | 2. Create identities and certificate signature requests for hosts 16 | 3. Sign and generate certificates 17 | 18 | ## Certificate architecture 19 | 20 | certstrap can init multiple certificate authorities to sign certificates with. Users can make arbitrarily long certificate chains by using signed hosts to sign later certificate requests, as well. 21 | 22 | ## Examples 23 | 24 | ## Getting Started 25 | 26 | ### Building 27 | 28 | certstrap must be built with Go 1.18+. You can build certstrap from source: 29 | 30 | ``` 31 | $ git clone https://github.com/square/certstrap 32 | $ cd certstrap 33 | $ go build 34 | ``` 35 | 36 | This will generate a binary called `certstrap` under project root folder. 37 | 38 | ### Initialize a new certificate authority: 39 | 40 | ``` 41 | $ ./certstrap init --common-name "CertAuth" 42 | Created out/CertAuth.key 43 | Created out/CertAuth.crt 44 | Created out/CertAuth.crl 45 | ``` 46 | 47 | Note that the `-common-name` flag is required, and will be used to name output files. 48 | 49 | Moreover, this will also generate a new keypair for the Certificate Authority, 50 | though you can use a pre-existing private PEM key with the `-key` flag. 51 | 52 | If the CN contains spaces, certstrap will change them to underscores in the filename for easier use. The spaces will be preserved inside the fields of the generated files: 53 | 54 | ``` 55 | $ ./certstrap init --common-name "Cert Auth" 56 | Created out/Cert_Auth.key 57 | Created out/Cert_Auth.crt 58 | Created out/Cert_Auth.crl 59 | ``` 60 | 61 | ### Request a certificate, including keypair: 62 | 63 | ``` 64 | $ ./certstrap request-cert --common-name Alice 65 | Created out/Alice.key 66 | Created out/Alice.csr 67 | ``` 68 | 69 | certstrap requires either `-common-name` or `-domain` flag to be set in order to generate a certificate signing request. The CN for the certificate will be found from these fields. 70 | 71 | If your server has mutiple ip addresses or domains, use comma seperated ip/domain/uri list. eg: `./certstrap request-cert -ip $ip1,$ip2 -domain $domain1,$domain2 -uri $uri1,$uri2` 72 | 73 | If you do not wish to generate a new keypair, you can use a pre-existing private 74 | PEM key with the `-key` flag 75 | 76 | ### Sign certificate request of host and generate the certificate: 77 | 78 | ``` 79 | $ ./certstrap sign Alice --CA CertAuth 80 | Created out/Alice.crt from out/Alice.csr signed by out/CertAuth.key 81 | ``` 82 | 83 | #### PKCS Format: 84 | If you'd like to convert your certificate and key to PKCS12 format, simply run: 85 | ``` 86 | $ openssl pkcs12 -export -out outputCert.p12 -inkey inputKey.key -in inputCert.crt -certfile CA.crt 87 | ``` 88 | `inputKey.key` and `inputCert.crt` make up the leaf private key and certificate pair of your choosing (generated by a `sign` command), with `CA.crt` being the certificate authority certificate that was used to sign it. The output PKCS12 file is `outputCert.p12` 89 | 90 | ### Key Algorithms: 91 | Certstrap supports curves P-224, P-256, P-384, P-521, and Ed25519. Curve names can be specified by name as part of the `init` and `request_cert` commands: 92 | 93 | ``` 94 | $ ./certstrap init --common-name CertAuth --curve P-256 95 | Created out/CertAuth.key 96 | Created out/CertAuth.crt 97 | Created out/CertAuth.crl 98 | 99 | $ ./certstrap request-cert --common-name Alice --curve P-256 100 | Created out/Alice.key 101 | Created out/Alice.csr 102 | ``` 103 | 104 | ### Retrieving Files 105 | 106 | Outputted key, request, and certificate files can be found in the depot directory. 107 | By default, this is in `out/` 108 | 109 | 110 | ## Project Details 111 | 112 | ### Contributing 113 | 114 | See [CONTRIBUTING](CONTRIBUTING.md) for details on submitting patches. 115 | 116 | ### License 117 | 118 | certstrap is under the Apache 2.0 license. See the [LICENSE](LICENSE) file for details. 119 | -------------------------------------------------------------------------------- /pkix/cert_auth_test.go: -------------------------------------------------------------------------------- 1 | /*- 2 | * Copyright 2015 Square Inc. 3 | * Copyright 2014 CoreOS 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package pkix 19 | 20 | import ( 21 | "testing" 22 | "time" 23 | ) 24 | 25 | func TestCreateCertificateAuthority(t *testing.T) { 26 | key, err := CreateRSAKey(rsaBits) 27 | if err != nil { 28 | t.Fatal("Failed creating rsa key:", err) 29 | } 30 | 31 | crt, err := CreateCertificateAuthority(key, "OU", time.Now().AddDate(5, 0, 0), "test", "US", "California", "San Francisco", "CA Name", []string{".example.com"}) 32 | if err != nil { 33 | t.Fatal("Failed creating certificate authority:", err) 34 | } 35 | rawCrt, err := crt.GetRawCertificate() 36 | if err != nil { 37 | t.Fatal("Failed to get x509.Certificate:", err) 38 | } 39 | 40 | if err = rawCrt.CheckSignatureFrom(rawCrt); err != nil { 41 | t.Fatal("Failed to check signature:", err) 42 | } 43 | 44 | if rawCrt.Subject.OrganizationalUnit[0] != "OU" { 45 | t.Fatal("Failed to verify hostname:", err) 46 | } 47 | 48 | if !time.Now().After(rawCrt.NotBefore) { 49 | t.Fatal("Failed to be after NotBefore") 50 | } 51 | 52 | if !time.Now().Before(rawCrt.NotAfter) { 53 | t.Fatal("Failed to be before NotAfter") 54 | } 55 | 56 | if crt.crt.PermittedDNSDomainsCritical != true { 57 | t.Fatal("Permitted DNS Domains is not set to critical") 58 | } 59 | 60 | if len(crt.crt.PermittedDNSDomains) != 1 { 61 | t.Fatal("More than one entry found in list of permitted DNS domains") 62 | } 63 | 64 | if crt.crt.PermittedDNSDomains[0] != ".example.com" { 65 | t.Fatalf("Wrong permitted DNS domain, want %q, got %q", ".example.com", crt.crt.PermittedDNSDomains[0]) 66 | } 67 | } 68 | 69 | func TestCreateCertificateAuthorityWithOptions(t *testing.T) { 70 | tests := []struct { 71 | name string 72 | pathlen int 73 | excludePathlen bool 74 | }{{ 75 | name: "pathlen: 0, excludePathlen: false", 76 | pathlen: 0, 77 | excludePathlen: false, 78 | }, { 79 | name: "pathlen: 1, excludePathlen: false", 80 | pathlen: 1, 81 | excludePathlen: false, 82 | }, { 83 | name: "pathlen: 0, excludePathlen: true", 84 | pathlen: 0, 85 | excludePathlen: true, 86 | }, { 87 | name: "pathlen: 1, excludePathlen: true", 88 | pathlen: 1, 89 | excludePathlen: true, 90 | }} 91 | 92 | for _, tc := range tests { 93 | t.Run(tc.name, func(t *testing.T) { 94 | key, err := CreateRSAKey(rsaBits) 95 | if err != nil { 96 | t.Fatal("Failed creating rsa key:", err) 97 | } 98 | 99 | crt, err := CreateCertificateAuthorityWithOptions(key, "OU", time.Now().AddDate(5, 0, 0), "test", "US", "California", "San Francisco", "CA Name", []string{".example.com"}, WithPathlenOption(tc.pathlen, tc.excludePathlen)) 100 | if err != nil { 101 | t.Fatal("Failed creating certificate authority:", err) 102 | } 103 | rawCrt, err := crt.GetRawCertificate() 104 | if err != nil { 105 | t.Fatal("Failed to get x509.Certificate:", err) 106 | } 107 | 108 | if err = rawCrt.CheckSignatureFrom(rawCrt); err != nil { 109 | t.Fatal("Failed to check signature:", err) 110 | } 111 | 112 | if rawCrt.Subject.OrganizationalUnit[0] != "OU" { 113 | t.Fatal("Failed to verify hostname:", err) 114 | } 115 | 116 | if !time.Now().After(rawCrt.NotBefore) { 117 | t.Fatal("Failed to be after NotBefore") 118 | } 119 | 120 | if !time.Now().Before(rawCrt.NotAfter) { 121 | t.Fatal("Failed to be before NotAfter") 122 | } 123 | 124 | if crt.crt.PermittedDNSDomainsCritical != true { 125 | t.Fatal("Permitted DNS Domains is not set to critical") 126 | } 127 | 128 | if len(crt.crt.PermittedDNSDomains) != 1 { 129 | t.Fatal("More than one entry found in list of permitted DNS domains") 130 | } 131 | 132 | if crt.crt.PermittedDNSDomains[0] != ".example.com" { 133 | t.Fatalf("Wrong permitted DNS domain, want %q, got %q", ".example.com", crt.crt.PermittedDNSDomains[0]) 134 | } 135 | 136 | if tc.excludePathlen { 137 | if crt.crt.MaxPathLen != -1 { 138 | t.Fatalf("Wrong MaxPathLen value, want %v, got %v", -1, crt.crt.MaxPathLen) 139 | } 140 | } else { 141 | if crt.crt.MaxPathLen != tc.pathlen { 142 | t.Fatalf("Wrong MaxPathLen value, want %v, got %v", tc.pathlen, crt.crt.MaxPathLen) 143 | } 144 | } 145 | }) 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /pkix/cert.go: -------------------------------------------------------------------------------- 1 | /*- 2 | * Copyright 2015 Square Inc. 3 | * Copyright 2014 CoreOS 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package pkix 19 | 20 | import ( 21 | "bytes" 22 | "crypto/x509" 23 | "encoding/pem" 24 | "errors" 25 | "fmt" 26 | "time" 27 | ) 28 | 29 | const ( 30 | certificatePEMBlockType = "CERTIFICATE" 31 | ) 32 | 33 | // Certificate is a wrapper around a x509 Certificate and its DER-formatted bytes 34 | type Certificate struct { 35 | // derBytes is always set for valid Certificate 36 | derBytes []byte 37 | 38 | crt *x509.Certificate 39 | } 40 | 41 | // NewCertificateFromDER inits Certificate from DER-format bytes 42 | func NewCertificateFromDER(derBytes []byte) *Certificate { 43 | return &Certificate{derBytes: derBytes} 44 | } 45 | 46 | // NewCertificateFromPEM inits Certificate from PEM-format bytes 47 | // data should contain at most one certificate 48 | func NewCertificateFromPEM(data []byte) (c *Certificate, err error) { 49 | pemBlock, _ := pem.Decode(data) 50 | if pemBlock == nil { 51 | err = errors.New("cannot find the next PEM formatted block") 52 | return 53 | } 54 | if pemBlock.Type != certificatePEMBlockType || len(pemBlock.Headers) != 0 { 55 | err = errors.New("unmatched type or headers") 56 | return 57 | } 58 | c = &Certificate{derBytes: pemBlock.Bytes} 59 | return 60 | } 61 | 62 | // build crt field if needed 63 | func (c *Certificate) buildX509Certificate() error { 64 | if c.crt != nil { 65 | return nil 66 | } 67 | 68 | crts, err := x509.ParseCertificates(c.derBytes) 69 | if err != nil { 70 | return err 71 | } 72 | if len(crts) != 1 { 73 | return errors.New("unsupported multiple certificates in a block") 74 | } 75 | c.crt = crts[0] 76 | return nil 77 | } 78 | 79 | // GetRawCertificate returns a copy of this certificate as an x509.Certificate 80 | func (c *Certificate) GetRawCertificate() (*x509.Certificate, error) { 81 | if err := c.buildX509Certificate(); err != nil { 82 | return nil, err 83 | } 84 | return c.crt, nil 85 | } 86 | 87 | // GetExpirationDuration gets time duration before expiration 88 | func (c *Certificate) GetExpirationDuration() time.Duration { 89 | if err := c.buildX509Certificate(); err != nil { 90 | return time.Until(time.Unix(0, 0)) 91 | } 92 | return time.Until(c.crt.NotAfter) 93 | } 94 | 95 | // CheckAuthority checks the authority of certificate against itself. 96 | // It only ensures that certificate is self-explanatory, and 97 | // cannot promise the validity and security. 98 | func (c *Certificate) CheckAuthority() error { 99 | if err := c.buildX509Certificate(); err != nil { 100 | return err 101 | } 102 | return c.crt.CheckSignatureFrom(c.crt) 103 | } 104 | 105 | // VerifyHost verifies the host certificate using host name. 106 | // Only certificate of authority could call this function successfully. 107 | // Current implementation allows one CA and direct hosts only, 108 | // so the organization is always this: 109 | // CA 110 | // host1 host2 host3 111 | func (c *Certificate) VerifyHost(hostCert *Certificate, name string) error { 112 | if err := c.CheckAuthority(); err != nil { 113 | return err 114 | } 115 | 116 | roots := x509.NewCertPool() 117 | roots.AddCert(c.crt) 118 | 119 | verifyOpts := x509.VerifyOptions{ 120 | DNSName: "", 121 | // no intermediates are allowed for now 122 | Intermediates: nil, 123 | Roots: roots, 124 | // if zero, the current time is used 125 | CurrentTime: time.Now(), 126 | // An empty list means ExtKeyUsageServerAuth. 127 | KeyUsages: nil, 128 | } 129 | 130 | rawHostCrt, err := hostCert.GetRawCertificate() 131 | if err != nil { 132 | return err 133 | } 134 | 135 | units := rawHostCrt.Subject.OrganizationalUnit 136 | if len(units) != 1 || units[0] != name { 137 | return fmt.Errorf("unmatched hostname between %v and %v", units, name) 138 | } 139 | 140 | chains, err := rawHostCrt.Verify(verifyOpts) 141 | if err != nil { 142 | return err 143 | } 144 | if len(chains) != 1 { 145 | return errors.New("internal error: verify chain number != 1") 146 | } 147 | return nil 148 | } 149 | 150 | // Export returns PEM-format bytes 151 | func (c *Certificate) Export() ([]byte, error) { 152 | pemBlock := &pem.Block{ 153 | Type: certificatePEMBlockType, 154 | Headers: nil, 155 | Bytes: c.derBytes, 156 | } 157 | 158 | buf := new(bytes.Buffer) 159 | if err := pem.Encode(buf, pemBlock); err != nil { 160 | return nil, err 161 | } 162 | return buf.Bytes(), nil 163 | } 164 | -------------------------------------------------------------------------------- /cmd/expiry_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | const dateFormat = "2006-01-02" 11 | const timeFormat = "2006-01-02 15:04:05" 12 | 13 | func init() { 14 | nowFunc = func() time.Time { 15 | t, _ := time.Parse(dateFormat, "2017-01-01") 16 | return t 17 | } 18 | } 19 | 20 | func TestParseExpiryWithSeconds(t *testing.T) { 21 | t1, _ := parseExpiry("1 second") 22 | t2, _ := parseExpiry("1 seconds") 23 | expected, _ := time.Parse(timeFormat, "2017-01-01 00:00:01") 24 | 25 | if t1 != expected { 26 | t.Fatalf("Parsing expiry 1 second from now (singular) did not return expected value (wanted: %s, got: %s)", expected, t1) 27 | } 28 | 29 | if t2 != expected { 30 | t.Fatalf("Parsing expiry 1 second from now (plural) did not return expected value (wanted: %s, got: %s)", expected, t2) 31 | } 32 | } 33 | 34 | func TestParseExpiryWithMinutes(t *testing.T) { 35 | t1, _ := parseExpiry("1 minute") 36 | t2, _ := parseExpiry("1 minutes") 37 | expected, _ := time.Parse(timeFormat, "2017-01-01 00:01:00") 38 | 39 | if t1 != expected { 40 | t.Fatalf("Parsing expiry 1 minute from now (singular) did not return expected value (wanted: %s, got: %s)", expected, t1) 41 | } 42 | 43 | if t2 != expected { 44 | t.Fatalf("Parsing expiry 1 minute from now (plural) did not return expected value (wanted: %s, got: %s)", expected, t2) 45 | } 46 | } 47 | 48 | func TestParseExpiryWithHours(t *testing.T) { 49 | t1, _ := parseExpiry("1 hour") 50 | t2, _ := parseExpiry("1 hours") 51 | expected, _ := time.Parse(timeFormat, "2017-01-01 01:00:00") 52 | 53 | if t1 != expected { 54 | t.Fatalf("Parsing expiry 1 hour from now (singular) did not return expected value (wanted: %s, got: %s)", expected, t1) 55 | } 56 | 57 | if t2 != expected { 58 | t.Fatalf("Parsing expiry 1 hour from now (plural) did not return expected value (wanted: %s, got: %s)", expected, t2) 59 | } 60 | } 61 | 62 | func TestParseExpiryWithDays(t *testing.T) { 63 | t1, _ := parseExpiry("1 day") 64 | t2, _ := parseExpiry("1 days") 65 | expected, _ := time.Parse(dateFormat, "2017-01-02") 66 | 67 | if t1 != expected { 68 | t.Fatalf("Parsing expiry 1 day from now (singular) did not return expected value (wanted: %s, got: %s)", expected, t1) 69 | } 70 | 71 | if t2 != expected { 72 | t.Fatalf("Parsing expiry 1 day from now (plural) did not return expected value (wanted: %s, got: %s)", expected, t2) 73 | } 74 | } 75 | 76 | func TestParseExpiryWithMonths(t *testing.T) { 77 | t1, _ := parseExpiry("1 month") 78 | t2, _ := parseExpiry("1 months") 79 | expected, _ := time.Parse(dateFormat, "2017-02-01") 80 | 81 | if t1 != expected { 82 | t.Fatalf("Parsing expiry 1 month from now (singular) did not return expected value (wanted: %s, got: %s)", expected, t1) 83 | } 84 | 85 | if t2 != expected { 86 | t.Fatalf("Parsing expiry 1 month from now (plural) did not return expected value (wanted: %s, got: %s)", expected, t2) 87 | } 88 | } 89 | 90 | func TestParseExpiryWithYears(t *testing.T) { 91 | t1, _ := parseExpiry("1 year") 92 | t2, _ := parseExpiry("1 years") 93 | expected, _ := time.Parse(dateFormat, "2018-01-01") 94 | 95 | if t1 != expected { 96 | t.Fatalf("Parsing expiry 1 year from now (singular) did not return expected value (wanted: %s, got: %s)", expected, t1) 97 | } 98 | 99 | if t2 != expected { 100 | t.Fatalf("Parsing expiry 1 year from now (plural) did not return expected value (wanted: %s, got: %s)", expected, t2) 101 | } 102 | } 103 | 104 | func TestParseExpiryWithMixed(t *testing.T) { 105 | t1, _ := parseExpiry("2 days 3 months 1 year") 106 | t2, _ := parseExpiry("5 years 5 days 6 months") 107 | expectedt1, _ := time.Parse(dateFormat, "2018-04-03") 108 | expectedt2, _ := time.Parse(dateFormat, "2022-07-06") 109 | 110 | if t1 != expectedt1 { 111 | t.Fatalf("Parsing expiry for mixed format t1 did not return expected value (wanted: %s, got: %s)", expectedt1, t1) 112 | } 113 | 114 | if t2 != expectedt2 { 115 | t.Fatalf("Parsing expiry for mixed format t2 did not return expected value (wanted: %s, got: %s)", expectedt2, t2) 116 | } 117 | } 118 | 119 | func TestParseInvalidExpiry(t *testing.T) { 120 | errorTime := onlyTime(time.Parse(dateFormat, "2017-01-01")) 121 | cases := []struct { 122 | Input string 123 | Expected time.Time 124 | ExpectedErr string 125 | }{ 126 | {"53257284647843897", errorTime, "invalid or empty format"}, 127 | {"5y", errorTime, "invalid or empty format"}, 128 | {"53257284647843897 days", errorTime, ".*value out of range"}, 129 | {"2147483647 hours", errorTime, ".*hour unit too large.*"}, 130 | {"2147483647 minutes", errorTime, ".*minute unit too large.*"}, 131 | {"2147483648 seconds", errorTime, ".*value out of range.*"}, 132 | {"2147483647 days", errorTime, ".*proposed date too far in to the future.*"}, 133 | } 134 | 135 | for _, c := range cases { 136 | result, err := parseExpiry(c.Input) 137 | if result != c.Expected { 138 | t.Fatalf("Invalid expiry '%s' did not have expected value (wanted: %s, got: %s)", c.Input, c.Expected, result) 139 | } 140 | 141 | if match, _ := regexp.MatchString(c.ExpectedErr, fmt.Sprintf("%s", err)); !match { 142 | t.Fatalf("Invalid expiry '%s' did not have expected error (wanted: %s, got: %s)", c.Input, c.ExpectedErr, err) 143 | } 144 | } 145 | } 146 | 147 | func onlyTime(a time.Time, b error) time.Time { 148 | return a 149 | } 150 | -------------------------------------------------------------------------------- /depot/depot.go: -------------------------------------------------------------------------------- 1 | /*- 2 | * Copyright 2015 Square Inc. 3 | * Copyright 2014 CoreOS 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package depot 19 | 20 | import ( 21 | "errors" 22 | "fmt" 23 | "io/fs" 24 | "os" 25 | "path/filepath" 26 | ) 27 | 28 | const ( 29 | // DefaultFileDepotDir is the default directory where .key/.csr/.crt files can be found 30 | DefaultFileDepotDir = "out" 31 | ) 32 | 33 | // Tag includes name and permission requirement 34 | // Permission requirement is used in two ways: 35 | // 1. Set the permission for data when Put 36 | // 2. Check the permission required when Get 37 | // It is set to prevent attacks from other users for FileDepot. 38 | // For example, 'evil' creates file ca.key with 0666 file perm, 39 | // 'core' reads it and uses it as ca.key. It may cause the security 40 | // problem of fake certificate and key. 41 | type Tag struct { 42 | name string 43 | perm os.FileMode 44 | } 45 | 46 | // Depot is in charge of data storage 47 | type Depot interface { 48 | Put(tag *Tag, data []byte) error 49 | Check(tag *Tag) bool 50 | Get(tag *Tag) ([]byte, error) 51 | Delete(tag *Tag) error 52 | } 53 | 54 | // FileDepot is a implementation of Depot using file system 55 | type FileDepot struct { 56 | // Absolute path of directory that holds all files 57 | dirPath string 58 | } 59 | 60 | // NewFileDepot creates a new Depot at the specified path 61 | func NewFileDepot(dir string) (*FileDepot, error) { 62 | dirpath, err := filepath.Abs(dir) 63 | if err != nil { 64 | return nil, err 65 | } 66 | 67 | return &FileDepot{dirpath}, nil 68 | } 69 | 70 | func (d *FileDepot) path(name string) string { 71 | return filepath.Join(d.dirPath, name) 72 | } 73 | 74 | // Put inserts the data into the file specified by the tag 75 | func (d *FileDepot) Put(tag *Tag, data []byte) error { 76 | if data == nil { 77 | return errors.New("data is nil") 78 | } 79 | 80 | if err := os.MkdirAll(d.dirPath, 0755); err != nil { 81 | return err 82 | } 83 | 84 | name := d.path(tag.name) 85 | perm := tag.perm 86 | 87 | file, err := os.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_EXCL, perm) 88 | if err != nil { 89 | return err 90 | } 91 | 92 | if _, err := file.Write(data); err != nil { 93 | file.Close() 94 | os.Remove(name) 95 | return err 96 | } 97 | 98 | file.Close() 99 | return nil 100 | } 101 | 102 | // Check returns whether the file at the tag location exists and has permissions at least as restrictive as the given tag. 103 | func (d *FileDepot) Check(tag *Tag) bool { 104 | name := d.path(tag.name) 105 | if fi, err := os.Stat(name); err == nil && checkPermissions(tag.perm, fi.Mode()) { 106 | return true 107 | } 108 | return false 109 | } 110 | 111 | func (d *FileDepot) check(tag *Tag) error { 112 | name := d.path(tag.name) 113 | fi, err := os.Stat(name) 114 | if err != nil { 115 | return err 116 | } 117 | if !checkPermissions(tag.perm, fi.Mode()) { 118 | return fmt.Errorf("permissions too lax for %v: required no more than %v, found %v", name, tag.perm, fi.Mode()) 119 | } 120 | return nil 121 | } 122 | 123 | // checkPermissions returns true if the mode bits in file are a subset of required. 124 | func checkPermissions(required, file fs.FileMode) bool { 125 | // Clear the bits of required from file. The check passes if there are no remaining bits set. 126 | return file&^required == 0 127 | } 128 | 129 | // Get reads the file specified by the tag 130 | func (d *FileDepot) Get(tag *Tag) ([]byte, error) { 131 | if err := d.check(tag); err != nil { 132 | return nil, err 133 | } 134 | return os.ReadFile(d.path(tag.name)) 135 | } 136 | 137 | // Delete removes the file specified by the tag 138 | func (d *FileDepot) Delete(tag *Tag) error { 139 | return os.Remove(d.path(tag.name)) 140 | } 141 | 142 | // List returns all tags in the specified depot 143 | func (d *FileDepot) List() []*Tag { 144 | var tags = make([]*Tag, 0) 145 | 146 | //nolint:errcheck 147 | filepath.Walk(d.dirPath, func(path string, info os.FileInfo, err error) error { 148 | if err != nil { 149 | return nil 150 | } 151 | if info.IsDir() { 152 | return nil 153 | } 154 | rel, err := filepath.Rel(d.dirPath, path) 155 | if err != nil { 156 | return nil 157 | } 158 | if rel != info.Name() { 159 | return nil 160 | } 161 | tags = append(tags, &Tag{info.Name(), info.Mode()}) 162 | return nil 163 | }) 164 | 165 | return tags 166 | } 167 | 168 | // File is a wrapper around a FileInfo and the files data bytes 169 | type File struct { 170 | Info os.FileInfo 171 | Data []byte 172 | } 173 | 174 | // GetFile returns the File at the specified tag in the given depot 175 | func (d *FileDepot) GetFile(tag *Tag) (*File, error) { 176 | if err := d.check(tag); err != nil { 177 | return nil, err 178 | } 179 | fi, err := os.Stat(d.path(tag.name)) 180 | if err != nil { 181 | return nil, err 182 | } 183 | b, err := os.ReadFile(d.path(tag.name)) 184 | return &File{fi, b}, err 185 | } 186 | -------------------------------------------------------------------------------- /depot/depot_test.go: -------------------------------------------------------------------------------- 1 | /*- 2 | * Copyright 2015 Square Inc. 3 | * Copyright 2014 CoreOS 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package depot 19 | 20 | import ( 21 | "bytes" 22 | "io/fs" 23 | "os" 24 | "testing" 25 | ) 26 | 27 | const ( 28 | data = "It is a trap only!" 29 | dir = ".certstrap-test" 30 | ) 31 | 32 | func getDepot(t *testing.T) *FileDepot { 33 | os.RemoveAll(dir) 34 | 35 | d, err := NewFileDepot(dir) 36 | if err != nil { 37 | t.Fatal("Failed init Depot:", err) 38 | } 39 | return d 40 | } 41 | 42 | // TestDepotCRUD tests to create, update and delete data 43 | func TestDepotCRUD(t *testing.T) { 44 | d := getDepot(t) 45 | defer os.RemoveAll(dir) 46 | 47 | if err := d.Put(tag, []byte(data)); err != nil { 48 | t.Fatal("Failed putting file into Depot:", err) 49 | } 50 | 51 | dataRead, err := d.Get(tag) 52 | if err != nil { 53 | t.Fatal("Failed getting file from Depot:", err) 54 | } 55 | if !bytes.Equal(dataRead, []byte(data)) { 56 | t.Fatal("Failed getting the previous data") 57 | } 58 | 59 | if err = d.Put(tag, []byte(data)); err == nil || !os.IsExist(err) { 60 | t.Fatal("Expect not to put file into Depot:", err) 61 | } 62 | 63 | if err := d.Delete(tag); err != nil { 64 | t.Fatal("Failed to delete a tag:", err) 65 | } 66 | 67 | if d.Check(tag) { 68 | t.Fatal("Expected the tag to be deleted") 69 | } 70 | } 71 | 72 | func TestDepotPutNil(t *testing.T) { 73 | d := getDepot(t) 74 | defer os.RemoveAll(dir) 75 | 76 | if err := d.Put(tag, nil); err == nil { 77 | t.Fatal("Expect not to put nil into Depot:", err) 78 | } 79 | 80 | if err := d.Put(tag, []byte(data)); err != nil { 81 | t.Fatal("Failed putting file into Depot:", err) 82 | } 83 | 84 | if err := d.Delete(tag); err != nil { 85 | t.Fatal("Failed to delete a tag:", err) 86 | } 87 | } 88 | 89 | func TestDepotCheckFailure(t *testing.T) { 90 | d := getDepot(t) 91 | defer os.RemoveAll(dir) 92 | 93 | if err := d.Put(&Tag{"host.pem", 0600}, []byte(data)); err != nil { 94 | t.Fatal("Failed putting file into Depot:", err) 95 | } 96 | 97 | // host.pem was created with mode 0600, so the permission check for 0400 should fail... 98 | if d.Check(&Tag{"host.pem", 0400}) { 99 | t.Fatal("Expected check permissions failure") 100 | } 101 | 102 | if d.Check(&Tag{"does-not-exist.pem", 0777}) { 103 | t.Fatal("Expect check failure for non-existent file") 104 | } 105 | 106 | if err := d.Delete(&Tag{"host.pem", 0777}); err != nil { 107 | t.Fatal("Failed to delete a tag:", err) 108 | } 109 | } 110 | 111 | func TestDepotGetFailure(t *testing.T) { 112 | d := getDepot(t) 113 | defer os.RemoveAll(dir) 114 | 115 | if err := d.Put(&Tag{"host.pem", 0600}, []byte(data)); err != nil { 116 | t.Fatal("Failed putting file into Depot:", err) 117 | } 118 | 119 | // host.pem was created with mode 0600, so the permission check for 0400 should fail... 120 | if _, err := d.Get(&Tag{"host.pem", 0400}); err == nil { 121 | t.Fatal("Expected permissions failure") 122 | } 123 | 124 | if _, err := d.Get(&Tag{"does-not-exist.pem", 0777}); err == nil { 125 | t.Fatal("Expect get failure for non-existent file") 126 | } 127 | 128 | if err := d.Delete(tag); err != nil { 129 | t.Fatal("Failed to delete a tag:", err) 130 | } 131 | } 132 | 133 | func TestDepotList(t *testing.T) { 134 | d := getDepot(t) 135 | defer os.RemoveAll(dir) 136 | 137 | if err := d.Put(tag, []byte(data)); err != nil { 138 | t.Fatal("Failed putting file into Depot:", err) 139 | } 140 | if err := d.Put(tag2, []byte(data)); err != nil { 141 | t.Fatal("Failed putting file into Depot:", err) 142 | } 143 | 144 | tags := d.List() 145 | if len(tags) != 2 { 146 | t.Fatal("Expect to list 2 instead of", len(tags)) 147 | } 148 | if tags[0].name != tag.name || tags[1].name != tag2.name { 149 | t.Fatal("Failed getting file tags back") 150 | } 151 | } 152 | 153 | func TestDepotGetFile(t *testing.T) { 154 | d := getDepot(t) 155 | defer os.RemoveAll(dir) 156 | 157 | if err := d.Put(tag, []byte(data)); err != nil { 158 | t.Fatal("Failed putting file into Depot:", err) 159 | } 160 | 161 | file, err := d.GetFile(tag) 162 | if err != nil { 163 | t.Fatal("Failed getting file from Depot:", err) 164 | } 165 | if !bytes.Equal(file.Data, []byte(data)) { 166 | t.Fatal("Failed getting the previous data") 167 | } 168 | 169 | if file.Info.Mode() != tag.perm { 170 | t.Fatal("Failed setting permission") 171 | } 172 | } 173 | 174 | func TestCheckPermissions(t *testing.T) { 175 | tests := []struct { 176 | required fs.FileMode 177 | file fs.FileMode 178 | want bool 179 | }{ 180 | // If required is 0777, any file permissions should pass... 181 | {0777, 0000, true}, 182 | {0777, 0666, true}, 183 | {0777, 0400, true}, 184 | // When required is 0400, anything with more bits set should fail... 185 | {0400, 0600, false}, 186 | {0400, 0440, false}, 187 | {0400, 0004, false}, 188 | // required == file should pass... 189 | {0000, 0000, true}, 190 | {0440, 0440, true}, 191 | {0600, 0600, true}, 192 | {0777, 0777, true}, 193 | // When required is 0660, anything with more bits set fails, and 194 | // anything with fewer bits set succeeds... 195 | {0660, 0664, false}, 196 | {0660, 0670, false}, 197 | {0660, 0760, false}, 198 | {0660, 0660, true}, 199 | {0660, 0600, true}, 200 | {0660, 0440, true}, 201 | } 202 | for _, tc := range tests { 203 | if got := checkPermissions(tc.required, tc.file); got != tc.want { 204 | t.Errorf("checkPermissions(required=%o, file=%o) = %t, want %t", tc.required, tc.file, got, tc.want) 205 | } 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /cmd/sign.go: -------------------------------------------------------------------------------- 1 | /*- 2 | * Copyright 2015 Square Inc. 3 | * Copyright 2014 CoreOS 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package cmd 19 | 20 | import ( 21 | "fmt" 22 | "os" 23 | "strings" 24 | 25 | "github.com/square/certstrap/depot" 26 | "github.com/square/certstrap/pkix" 27 | "github.com/urfave/cli" 28 | ) 29 | 30 | // NewSignCommand sets up a "sign" command to sign a CSR with a given CA for a new certificate 31 | func NewSignCommand() cli.Command { 32 | return cli.Command{ 33 | Name: "sign", 34 | Usage: "Sign certificate request", 35 | Description: "Sign certificate request with CA, and generate certificate for the host.", 36 | Flags: []cli.Flag{ 37 | cli.StringFlag{ 38 | Name: "passphrase", 39 | Usage: "Passphrase to decrypt private-key PEM block of CA", 40 | }, 41 | cli.IntFlag{ 42 | Name: "years", 43 | Hidden: true, 44 | }, 45 | cli.StringFlag{ 46 | Name: "expires", 47 | Value: "2 years", 48 | Usage: "How long until the certificate expires (example: 1 year 2 days 3 months 4 hours)", 49 | }, 50 | cli.StringFlag{ 51 | Name: "CA", 52 | Usage: "Name of CA to issue cert with", 53 | }, 54 | cli.StringFlag{ 55 | Name: "csr", 56 | Usage: "Path to certificate request PEM file (if blank, will use --depot-path and default name)", 57 | }, 58 | cli.StringFlag{ 59 | Name: "cert", 60 | Usage: "Path to certificate output PEM file (if blank, will use --depot-path and default name)", 61 | }, 62 | cli.BoolFlag{ 63 | Name: "stdout", 64 | Usage: "Print certificate to stdout in addition to saving file", 65 | }, 66 | cli.BoolFlag{ 67 | Name: "intermediate", 68 | Usage: "Whether generated certificate should be a intermediate", 69 | }, 70 | cli.IntFlag{ 71 | Name: "path-length", 72 | Value: 0, 73 | Usage: "Maximum number of non-self-issued intermediate certificates that may follow this CA certificate in a valid certification path", 74 | }, 75 | }, 76 | Action: newSignAction, 77 | } 78 | } 79 | 80 | func newSignAction(c *cli.Context) { 81 | if len(c.Args()) != 1 { 82 | fmt.Fprintln(os.Stderr, "One host name must be provided.") 83 | os.Exit(1) 84 | } 85 | 86 | formattedReqName := strings.Replace(c.Args()[0], " ", "_", -1) 87 | formattedCAName := strings.Replace(c.String("CA"), " ", "_", -1) 88 | 89 | if depot.CheckCertificate(d, formattedReqName) { 90 | fmt.Fprintf(os.Stderr, "Certificate \"%s\" already exists!\n", formattedReqName) 91 | os.Exit(1) 92 | } 93 | 94 | expires := c.String("expires") 95 | if years := c.Int("years"); years != 0 { 96 | expires = fmt.Sprintf("%s %d years", expires, years) 97 | } 98 | 99 | // Expiry parsing is a naive regex implementation 100 | // Token based parsing would provide better feedback but 101 | expiresTime, err := parseExpiry(expires) 102 | if err != nil { 103 | fmt.Fprintf(os.Stderr, "Invalid expiry: %s\n", err) 104 | os.Exit(1) 105 | } 106 | 107 | csr, err := getCertificateSigningRequest(c, d, formattedReqName) 108 | if err != nil { 109 | fmt.Fprintln(os.Stderr, "Get certificate request error:", err) 110 | os.Exit(1) 111 | } 112 | crt, err := depot.GetCertificate(d, formattedCAName) 113 | if err != nil { 114 | fmt.Fprintln(os.Stderr, "Get CA certificate error:", err) 115 | os.Exit(1) 116 | } 117 | // Validate that crt is allowed to sign certificates. 118 | raw_crt, err := crt.GetRawCertificate() 119 | if err != nil { 120 | fmt.Fprintln(os.Stderr, "GetRawCertificate failed on CA certificate:", err) 121 | os.Exit(1) 122 | } 123 | // We punt on checking BasicConstraintsValid and checking MaxPathLen. The goal 124 | // is to prevent accidentally creating invalid certificates, not protecting 125 | // against malicious input. 126 | if !raw_crt.IsCA { 127 | fmt.Fprintln(os.Stderr, "Selected CA certificate is not allowed to sign certificates.") 128 | os.Exit(1) 129 | } 130 | 131 | key, err := depot.GetPrivateKey(d, formattedCAName) 132 | if err != nil { 133 | pass, err := getPassPhrase(c, "CA key") 134 | if err != nil { 135 | fmt.Fprintln(os.Stderr, "Get CA key error: ", err) 136 | os.Exit(1) 137 | } 138 | key, err = depot.GetEncryptedPrivateKey(d, formattedCAName, pass) 139 | if err != nil { 140 | fmt.Fprintln(os.Stderr, "Get CA key error: ", err) 141 | os.Exit(1) 142 | } 143 | } 144 | 145 | var crtOut *pkix.Certificate 146 | if c.Bool("intermediate") { 147 | fmt.Fprintln(os.Stderr, "Building intermediate") 148 | 149 | opts := []pkix.Option{ 150 | pkix.WithPathlenOption(c.Int("path-length"), false), 151 | } 152 | 153 | crtOut, err = pkix.CreateIntermediateCertificateAuthorityWithOptions(crt, key, csr, expiresTime, opts...) 154 | } else { 155 | if c.IsSet("path-length") { 156 | fmt.Fprintln(os.Stderr, "The 'path-length' can only be used with 'intermediate' flag.") 157 | os.Exit(1) 158 | } 159 | 160 | crtOut, err = pkix.CreateCertificateHost(crt, key, csr, expiresTime) 161 | } 162 | 163 | if err != nil { 164 | fmt.Fprintln(os.Stderr, "Create certificate error:", err) 165 | os.Exit(1) 166 | } else { 167 | fmt.Printf("Created %s/%s.crt from %s/%s.csr signed by %s/%s.key\n", depotDir, formattedReqName, depotDir, formattedReqName, depotDir, formattedCAName) 168 | } 169 | 170 | if c.Bool("stdout") { 171 | crtBytes, err := crtOut.Export() 172 | if err != nil { 173 | fmt.Fprintln(os.Stderr, "Print certificate error:", err) 174 | os.Exit(1) 175 | } else { 176 | fmt.Println(string(crtBytes)) 177 | } 178 | } 179 | 180 | if err = putCertificate(c, d, formattedReqName, crtOut); err != nil { 181 | fmt.Fprintln(os.Stderr, "Save certificate error:", err) 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /pkix/csr_test.go: -------------------------------------------------------------------------------- 1 | /*- 2 | * Copyright 2015 Square Inc. 3 | * Copyright 2014 CoreOS 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package pkix 19 | 20 | import ( 21 | "bytes" 22 | "testing" 23 | ) 24 | 25 | const ( 26 | csrHostname = "host1" 27 | csrCN = "CN" 28 | csrPEM = `-----BEGIN CERTIFICATE REQUEST----- 29 | MIIBqDCCARECAQAwaDELMAkGA1UEBhMCVVMxDzANBgNVBAcTBmZpZWxkMjESMBAG 30 | A1UEChMJY2VydHN0cmFwMQ8wDQYDVQQLEwZmb29iYXIxEjAQBgNVBAMTCTEyNy4w 31 | LjAuMTEPMA0GA1UECBMGZmllbGQ2MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKB 32 | gQDPzdA4f6O1X81IiCIjoPfne4oYXcjl/vcC1le7G8Hk3r9AlyPG1KgZvh4YHOZo 33 | Qs9bJ9YKOEHsHIxrV7Mk7i4knlr9WNIRJydCpH+/DTv5ZbqlDbpi5K/DYS8ly5Zh 34 | DN6jyZ83+bwmpRUePZr/rXGzhGtLN8IKVww6UOi+yUH+iQIDAQABoAAwDQYJKoZI 35 | hvcNAQEFBQADgYEAd6zCGoQHwqwcCtETtmEnlry1kienYt8WgMLU89HcJpSTUR7e 36 | 1VfXfkS9MO5SUp9apPq0LIgT3ZcZwhFjgYmM9BTDUeMKT21FLnQbJ3C7xTTtSHQ6 37 | FlV5Hq5RkPqaigS6EmWl1zQrSZ4330jpt8y9J5rHGbsNwGlR+0xr34xqAYg= 38 | -----END CERTIFICATE REQUEST----- 39 | ` 40 | oldStyleCsrPEM = `-----BEGIN NEW CERTIFICATE REQUEST----- 41 | MIIBqDCCARECAQAwaDELMAkGA1UEBhMCVVMxDzANBgNVBAcTBmZpZWxkMjESMBAG 42 | A1UEChMJY2VydHN0cmFwMQ8wDQYDVQQLEwZmb29iYXIxEjAQBgNVBAMTCTEyNy4w 43 | LjAuMTEPMA0GA1UECBMGZmllbGQ2MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKB 44 | gQDPzdA4f6O1X81IiCIjoPfne4oYXcjl/vcC1le7G8Hk3r9AlyPG1KgZvh4YHOZo 45 | Qs9bJ9YKOEHsHIxrV7Mk7i4knlr9WNIRJydCpH+/DTv5ZbqlDbpi5K/DYS8ly5Zh 46 | DN6jyZ83+bwmpRUePZr/rXGzhGtLN8IKVww6UOi+yUH+iQIDAQABoAAwDQYJKoZI 47 | hvcNAQEFBQADgYEAd6zCGoQHwqwcCtETtmEnlry1kienYt8WgMLU89HcJpSTUR7e 48 | 1VfXfkS9MO5SUp9apPq0LIgT3ZcZwhFjgYmM9BTDUeMKT21FLnQbJ3C7xTTtSHQ6 49 | FlV5Hq5RkPqaigS6EmWl1zQrSZ4330jpt8y9J5rHGbsNwGlR+0xr34xqAYg= 50 | -----END NEW CERTIFICATE REQUEST----- 51 | ` 52 | wrongCSRPEM = `-----BEGIN WRONG CERTIFICATE REQUEST----- 53 | MIIBgTCB7QIBADBGMQwwCgYDVQQGEwNVU0ExEDAOBgNVBAoTB2V0Y2QtY2ExEDAO 54 | BgNVBAsTB3NlcnZlcjIxEjAQBgNVBAMTCTEyNy4wLjAuMTCBnTALBgkqhkiG9w0B 55 | AQEDgY0AMIGJAoGBAMTO2QZgrM9RXjfZTn9LWQZ0Y5B+Uh0+z4mEiXIbKno/omW3 56 | dsEdxM9Er0dAw4zBS5lr0QUymy2AZlJo078Bgz1KyEVKS48udvv404HnBc6fDhUC 57 | 3aax/V2aiX3SFPj8SLLy2h7hJBkIikwuSYo2ajuq69FgA0pd8UHtEsKhokyZAgMB 58 | AAGgADALBgkqhkiG9w0BAQUDgYEAhsgW8OvSeJN3w+0IDGLx12WYbHUD44yV5VzV 59 | Jp3vi0CaLKA4mNh6rlxhYFVX5AUlaSGKwVkn3M9br/apfP14esIRnuq+nZd7BtU1 60 | 13tL4D+UCnGHN5iYIb8stB7UVwuXNxnqUfJqiO4zoYNmrcBpssYuHVZ7to7Xvxu+ 61 | 5iyRRSg= 62 | -----END WRONG CERTIFICATE REQUEST----- 63 | ` 64 | badCSRPEM = `-----BEGIN CERTIFICATE REQUEST----- 65 | MIIBgTCB7QIBADBGMQwwCgYDVQQGEwNVU0ExEDAOBgNVBAoTB2V0Y2QtY2ExEDAO 66 | dsEdxM9Er0dAw4zBS5lr0QUymy2AZlJo078Bgz1KyEVKS48udvv404HnBc6fDhUC 67 | 3aax/V2aiX3SFPj8SLLy2h7hJBkIikwuSYo2ajuq69FgA0pd8UHtEsKhokyZAgMB 68 | AAGgADALBgkqhkiG9w0BAQUDgYEAhsgW8OvSeJN3w+0IDGLx12WYbHUD44yV5VzV 69 | Jp3vi0CaLKA4mNh6rlxhYFVX5AUlaSGKwVkn3M9br/apfP14esIRnuq+nZd7BtU1 70 | 13tL4D+UCnGHN5iYIb8stB7UVwuXNxnqUfJqiO4zoYNmrcBpssYuHVZ7to7Xvxu+ 71 | 5iyRRSg= 72 | -----END CERTIFICATE REQUEST----- 73 | ` 74 | ) 75 | 76 | func TestCreateCertificateSigningRequest(t *testing.T) { 77 | key, err := CreateRSAKey(rsaBits) 78 | if err != nil { 79 | t.Fatal("Failed creating rsa key:", err) 80 | } 81 | 82 | csr, err := CreateCertificateSigningRequest(key, csrHostname, nil, nil, nil, "example", "US", "California", "San Francisco", csrCN) 83 | if err != nil { 84 | t.Fatal("Failed creating certificate request:", err) 85 | } 86 | 87 | if err = csr.CheckSignature(); err != nil { 88 | t.Fatal("Failed cheching signature in certificate request:", err) 89 | } 90 | 91 | rawCsr, err := csr.GetRawCertificateSigningRequest() 92 | if err != nil { 93 | t.Fatal("Failed getting raw certificate request:", err) 94 | } 95 | 96 | if csrHostname != rawCsr.Subject.OrganizationalUnit[0] { 97 | t.Fatalf("Expect OrganizationalUnit to be %v instead of %v", csrHostname, rawCsr.Subject.OrganizationalUnit[0]) 98 | } 99 | if csrCN != rawCsr.Subject.CommonName { 100 | t.Fatalf("Expect CommonName to be %v instead of %v", csrCN, rawCsr.Subject.CommonName) 101 | } 102 | } 103 | 104 | func TestCertificateSigningRequest(t *testing.T) { 105 | csr, err := NewCertificateSigningRequestFromPEM([]byte(csrPEM)) 106 | if err != nil { 107 | t.Fatal("Failed parsing certificate request from PEM:", err) 108 | } 109 | 110 | if err = csr.CheckSignature(); err != nil { 111 | t.Fatal("Failed checking signature:", err) 112 | } 113 | 114 | pemBytes, err := csr.Export() 115 | if err != nil { 116 | t.Fatal("Failed exporting PEM-format bytes:", err) 117 | } 118 | if !bytes.Equal(pemBytes, []byte(csrPEM)) { 119 | t.Fatal("Failed exporting the same PEM-format bytes") 120 | } 121 | } 122 | 123 | func TestOldStyleCertificateSigningRequest(t *testing.T) { 124 | csr, err := NewCertificateSigningRequestFromPEM([]byte(oldStyleCsrPEM)) 125 | if err != nil { 126 | t.Fatal("Failed parsing certificate request from PEM:", err) 127 | } 128 | 129 | if err = csr.CheckSignature(); err != nil { 130 | t.Fatal("Failed checking signature:", err) 131 | } 132 | 133 | pemBytes, err := csr.Export() 134 | if err != nil { 135 | t.Fatal("Failed exporting PEM-format bytes:", err) 136 | } 137 | if !bytes.Equal(pemBytes, []byte(csrPEM)) { 138 | t.Fatal("Failed exporting the same PEM-format bytes") 139 | } 140 | } 141 | 142 | func TestWrongCertificateSigningRequest(t *testing.T) { 143 | if _, err := NewCertificateSigningRequestFromPEM([]byte("-")); err == nil { 144 | t.Fatal("Expect not to parse from PEM:", err) 145 | } 146 | 147 | if _, err := NewCertificateSigningRequestFromPEM([]byte(wrongCSRPEM)); err == nil { 148 | t.Fatal("Expect not to parse from PEM:", err) 149 | } 150 | } 151 | 152 | func TestBadCertificateSigningRequest(t *testing.T) { 153 | csr, err := NewCertificateSigningRequestFromPEM([]byte(badCSRPEM)) 154 | if err != nil { 155 | t.Fatal("Failed to parse from PEM:", err) 156 | } 157 | 158 | if _, err = csr.GetRawCertificateSigningRequest(); err == nil { 159 | t.Fatal("Expect not to get x509.CertificateRequest") 160 | } 161 | 162 | if err = csr.CheckSignature(); err == nil { 163 | t.Fatal("Expect not to get x509.CertificateRequest") 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /depot/pkix.go: -------------------------------------------------------------------------------- 1 | /*- 2 | * Copyright 2015 Square Inc. 3 | * Copyright 2014 CoreOS 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package depot 19 | 20 | import ( 21 | "strings" 22 | 23 | "github.com/square/certstrap/pkix" 24 | ) 25 | 26 | const ( 27 | crtSuffix = ".crt" 28 | csrSuffix = ".csr" 29 | privKeySuffix = ".key" 30 | crlSuffix = ".crl" 31 | ) 32 | 33 | // CrtTag returns a tag corresponding to a certificate 34 | func CrtTag(prefix string) *Tag { 35 | return &Tag{prefix + crtSuffix, LeafPerm} 36 | } 37 | 38 | // PrivKeyTag returns a tag corresponding to a private key 39 | func PrivKeyTag(prefix string) *Tag { 40 | return &Tag{prefix + privKeySuffix, BranchPerm} 41 | } 42 | 43 | // CsrTag returns a tag corresponding to a certificate signature request file 44 | func CsrTag(prefix string) *Tag { 45 | return &Tag{prefix + csrSuffix, LeafPerm} 46 | } 47 | 48 | // CrlTag returns a tag corresponding to a certificate revocation list 49 | func CrlTag(prefix string) *Tag { 50 | return &Tag{prefix + crlSuffix, LeafPerm} 51 | } 52 | 53 | // GetNameFromCrtTag returns the host name from a certificate file tag 54 | func GetNameFromCrtTag(tag *Tag) string { 55 | return getName(tag, crtSuffix) 56 | } 57 | 58 | // GetNameFromPrivKeyTag returns the host name from a private key file tag 59 | func GetNameFromPrivKeyTag(tag *Tag) string { 60 | return getName(tag, privKeySuffix) 61 | } 62 | 63 | // GetNameFromCsrTag returns the host name from a certificate request file tag 64 | func GetNameFromCsrTag(tag *Tag) string { 65 | return getName(tag, csrSuffix) 66 | } 67 | 68 | // GetNameFromCrlTag returns the host name from a certificate revocation list file tag 69 | func GetNameFromCrlTag(tag *Tag) string { 70 | return getName(tag, crlSuffix) 71 | } 72 | 73 | // PutCertificate creates a certificate file for a given CA name in the depot 74 | func PutCertificate(d Depot, name string, crt *pkix.Certificate) error { 75 | b, err := crt.Export() 76 | if err != nil { 77 | return err 78 | } 79 | return d.Put(CrtTag(name), b) 80 | } 81 | 82 | // CheckCertificate checks the depot for existence of a certificate file for a given CA name 83 | func CheckCertificate(d Depot, name string) bool { 84 | return d.Check(CrtTag(name)) 85 | } 86 | 87 | // GetCertificate retrieves a certificate file for a given name from the depot 88 | func GetCertificate(d Depot, name string) (crt *pkix.Certificate, err error) { 89 | b, err := d.Get(CrtTag(name)) 90 | if err != nil { 91 | return nil, err 92 | } 93 | return pkix.NewCertificateFromPEM(b) 94 | } 95 | 96 | // DeleteCertificate removes a certificate file for a given name from the depot 97 | func DeleteCertificate(d Depot, name string) error { 98 | return d.Delete(CrtTag(name)) 99 | } 100 | 101 | // PutCertificateSigningRequest creates a certificate signing request file for a given name and csr in the depot 102 | func PutCertificateSigningRequest(d Depot, name string, csr *pkix.CertificateSigningRequest) error { 103 | b, err := csr.Export() 104 | if err != nil { 105 | return err 106 | } 107 | return d.Put(CsrTag(name), b) 108 | } 109 | 110 | // CheckCertificateSigningRequest checks the depot for existence of a certificate signing request file for a given host name 111 | func CheckCertificateSigningRequest(d Depot, name string) bool { 112 | return d.Check(CsrTag(name)) 113 | } 114 | 115 | // GetCertificateSigningRequest retrieves a certificate signing request file for a given host name from the depot 116 | func GetCertificateSigningRequest(d Depot, name string) (crt *pkix.CertificateSigningRequest, err error) { 117 | b, err := d.Get(CsrTag(name)) 118 | if err != nil { 119 | return nil, err 120 | } 121 | return pkix.NewCertificateSigningRequestFromPEM(b) 122 | } 123 | 124 | // DeleteCertificateSigningRequest removes a certificate signing request file for a given host name from the depot 125 | func DeleteCertificateSigningRequest(d Depot, name string) error { 126 | return d.Delete(CsrTag(name)) 127 | } 128 | 129 | // PutPrivateKey creates a private key file for a given name in the depot 130 | func PutPrivateKey(d Depot, name string, key *pkix.Key) error { 131 | b, err := key.ExportPrivate() 132 | if err != nil { 133 | return err 134 | } 135 | return d.Put(PrivKeyTag(name), b) 136 | } 137 | 138 | // CheckPrivateKey checks the depot for existence of a private key file for a given name 139 | func CheckPrivateKey(d Depot, name string) bool { 140 | return d.Check(PrivKeyTag(name)) 141 | } 142 | 143 | // GetPrivateKey retrieves a private key file for a given name from the depot 144 | func GetPrivateKey(d Depot, name string) (key *pkix.Key, err error) { 145 | b, err := d.Get(PrivKeyTag(name)) 146 | if err != nil { 147 | return nil, err 148 | } 149 | return pkix.NewKeyFromPrivateKeyPEM(b) 150 | } 151 | 152 | // PutEncryptedPrivateKey creates an encrypted private key file for a given name in the depot 153 | func PutEncryptedPrivateKey(d Depot, name string, key *pkix.Key, passphrase []byte) error { 154 | b, err := key.ExportEncryptedPrivate(passphrase) 155 | if err != nil { 156 | return err 157 | } 158 | return d.Put(PrivKeyTag(name), b) 159 | } 160 | 161 | // GetEncryptedPrivateKey retrieves an encrypted private key file for a given name from the depot 162 | func GetEncryptedPrivateKey(d Depot, name string, passphrase []byte) (key *pkix.Key, err error) { 163 | b, err := d.Get(PrivKeyTag(name)) 164 | if err != nil { 165 | return nil, err 166 | } 167 | return pkix.NewKeyFromEncryptedPrivateKeyPEM(b, passphrase) 168 | } 169 | 170 | // PutCertificateRevocationList creates a CRL file for a given name and ca in the depot 171 | func PutCertificateRevocationList(d Depot, name string, crl *pkix.CertificateRevocationList) error { 172 | b, err := crl.Export() 173 | if err != nil { 174 | return err 175 | } 176 | return d.Put(CrlTag(name), b) 177 | } 178 | 179 | //GetCertificateRevocationList gets a CRL file for a given name and ca in the depot. 180 | func GetCertificateRevocationList(d Depot, name string) (*pkix.CertificateRevocationList, error) { 181 | b, err := d.Get(CrlTag(name)) 182 | if err != nil { 183 | return nil, err 184 | } 185 | return pkix.NewCertificateRevocationListFromPEM(b) 186 | } 187 | 188 | func getName(tag *Tag, suffix string) string { 189 | name := strings.TrimSuffix(tag.name, suffix) 190 | if name == tag.name { 191 | return "" 192 | } 193 | return name 194 | } 195 | -------------------------------------------------------------------------------- /pkix/cert_auth.go: -------------------------------------------------------------------------------- 1 | /*- 2 | * Copyright 2015 Square Inc. 3 | * Copyright 2014 CoreOS 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package pkix 19 | 20 | import ( 21 | "crypto/rand" 22 | "crypto/x509" 23 | "math/big" 24 | "time" 25 | ) 26 | 27 | type Option func(*x509.Certificate) 28 | 29 | // CreateCertificateAuthority creates Certificate Authority using existing key. 30 | // CertificateAuthorityInfo returned is the extra infomation required by Certificate Authority. 31 | func CreateCertificateAuthority(key *Key, organizationalUnit string, expiry time.Time, organization string, country string, province string, locality string, commonName string, permitDomains []string) (*Certificate, error) { 32 | // Passing all arguments to CreateCertificateAuthorityWithOptions 33 | return CreateCertificateAuthorityWithOptions(key, organizationalUnit, expiry, organization, country, province, locality, commonName, permitDomains) 34 | } 35 | 36 | // CreateCertificateAuthorityWithOptions creates Certificate Authority using existing key with options. 37 | // CertificateAuthorityInfo returned is the extra infomation required by Certificate Authority. 38 | func CreateCertificateAuthorityWithOptions(key *Key, organizationalUnit string, expiry time.Time, organization string, country string, province string, locality string, commonName string, permitDomains []string, opts ...Option) (*Certificate, error) { 39 | authTemplate := newAuthTemplate() 40 | 41 | subjectKeyID, err := GenerateSubjectKeyID(key.Public) 42 | if err != nil { 43 | return nil, err 44 | } 45 | authTemplate.SubjectKeyId = subjectKeyID 46 | authTemplate.NotAfter = expiry 47 | if len(country) > 0 { 48 | authTemplate.Subject.Country = []string{country} 49 | } 50 | if len(province) > 0 { 51 | authTemplate.Subject.Province = []string{province} 52 | } 53 | if len(locality) > 0 { 54 | authTemplate.Subject.Locality = []string{locality} 55 | } 56 | if len(organization) > 0 { 57 | authTemplate.Subject.Organization = []string{organization} 58 | } 59 | if len(organizationalUnit) > 0 { 60 | authTemplate.Subject.OrganizationalUnit = []string{organizationalUnit} 61 | } 62 | if len(commonName) > 0 { 63 | authTemplate.Subject.CommonName = commonName 64 | } 65 | 66 | if len(permitDomains) > 0 { 67 | authTemplate.PermittedDNSDomainsCritical = true 68 | authTemplate.PermittedDNSDomains = permitDomains 69 | } 70 | 71 | applyOptions(&authTemplate, opts) 72 | 73 | crtBytes, err := x509.CreateCertificate(rand.Reader, &authTemplate, &authTemplate, key.Public, key.Private) 74 | if err != nil { 75 | return nil, err 76 | } 77 | 78 | return NewCertificateFromDER(crtBytes), nil 79 | } 80 | 81 | // CreateIntermediateCertificateAuthority creates an intermediate 82 | // CA certificate signed by the given authority. 83 | func CreateIntermediateCertificateAuthority(crtAuth *Certificate, keyAuth *Key, csr *CertificateSigningRequest, proposedExpiry time.Time) (*Certificate, error) { 84 | // Passing all arguments to CreateIntermediateCertificateAuthorityWithOptions 85 | return CreateIntermediateCertificateAuthorityWithOptions(crtAuth, keyAuth, csr, proposedExpiry) 86 | } 87 | 88 | // CreateIntermediateCertificateAuthorityWithOptions creates an intermediate with options. 89 | // CA certificate signed by the given authority. 90 | func CreateIntermediateCertificateAuthorityWithOptions(crtAuth *Certificate, keyAuth *Key, csr *CertificateSigningRequest, proposedExpiry time.Time, opts ...Option) (*Certificate, error) { 91 | authTemplate := newAuthTemplate() 92 | 93 | serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) 94 | serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) 95 | if err != nil { 96 | return nil, err 97 | } 98 | authTemplate.SerialNumber.Set(serialNumber) 99 | 100 | rawCsr, err := csr.GetRawCertificateSigningRequest() 101 | if err != nil { 102 | return nil, err 103 | } 104 | 105 | authTemplate.RawSubject = rawCsr.RawSubject 106 | 107 | caExpiry := time.Now().Add(crtAuth.GetExpirationDuration()) 108 | // ensure cert doesn't expire after issuer 109 | if caExpiry.Before(proposedExpiry) { 110 | authTemplate.NotAfter = caExpiry 111 | } else { 112 | authTemplate.NotAfter = proposedExpiry 113 | } 114 | 115 | authTemplate.SubjectKeyId, err = GenerateSubjectKeyID(rawCsr.PublicKey) 116 | if err != nil { 117 | return nil, err 118 | } 119 | 120 | authTemplate.IPAddresses = rawCsr.IPAddresses 121 | authTemplate.DNSNames = rawCsr.DNSNames 122 | authTemplate.URIs = rawCsr.URIs 123 | 124 | rawCrtAuth, err := crtAuth.GetRawCertificate() 125 | if err != nil { 126 | return nil, err 127 | } 128 | 129 | applyOptions(&authTemplate, opts) 130 | 131 | crtOutBytes, err := x509.CreateCertificate(rand.Reader, &authTemplate, rawCrtAuth, rawCsr.PublicKey, keyAuth.Private) 132 | if err != nil { 133 | return nil, err 134 | } 135 | 136 | return NewCertificateFromDER(crtOutBytes), nil 137 | } 138 | 139 | // WithPathlenOption will check if the certificate should have `pathlen` or not. 140 | func WithPathlenOption(pathlen int, excludePathlen bool) func(template *x509.Certificate) { 141 | return func(template *x509.Certificate) { 142 | template.MaxPathLen = pathlen 143 | 144 | if excludePathlen { 145 | template.MaxPathLen = -1 146 | } 147 | } 148 | } 149 | 150 | func applyOptions(template *x509.Certificate, opts []Option) { 151 | for _, opt := range opts { 152 | opt(template) 153 | } 154 | } 155 | 156 | func newAuthTemplate() x509.Certificate { 157 | // Build CA based on RFC5280 158 | return x509.Certificate{ 159 | SerialNumber: big.NewInt(1), 160 | // NotBefore is set to be 10min earlier to fix gap on time difference in cluster 161 | NotBefore: time.Now().Add(-10 * time.Minute).UTC(), 162 | NotAfter: time.Time{}, 163 | // Used for certificate signing only 164 | KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, 165 | 166 | ExtKeyUsage: nil, 167 | UnknownExtKeyUsage: nil, 168 | 169 | // activate CA 170 | BasicConstraintsValid: true, 171 | IsCA: true, 172 | // Not allow any non-self-issued intermediate CA, sets MaxPathLen=0 173 | MaxPathLenZero: true, 174 | 175 | // 160-bit SHA-1 hash of the value of the BIT STRING subjectPublicKey 176 | // (excluding the tag, length, and number of unused bits) 177 | // **SHOULD** be filled in later 178 | SubjectKeyId: nil, 179 | 180 | // Subject Alternative Name 181 | DNSNames: nil, 182 | 183 | PermittedDNSDomainsCritical: false, 184 | PermittedDNSDomains: nil, 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /pkix/key.go: -------------------------------------------------------------------------------- 1 | /*- 2 | * Copyright 2015 Square Inc. 3 | * Copyright 2014 CoreOS 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package pkix 19 | 20 | import ( 21 | "crypto" 22 | "crypto/ecdsa" 23 | "crypto/ed25519" 24 | "crypto/elliptic" 25 | "crypto/rand" 26 | "crypto/rsa" 27 | "crypto/sha1" 28 | "crypto/x509" 29 | "encoding/asn1" 30 | "encoding/pem" 31 | "errors" 32 | "fmt" 33 | "math/big" 34 | 35 | "go.step.sm/crypto/pemutil" 36 | ) 37 | 38 | const ( 39 | rsaPrivateKeyPEMBlockType = "RSA PRIVATE KEY" 40 | pkcs8PrivateKeyPEMBlockType = "PRIVATE KEY" 41 | encryptedPKCS8PrivateKeyPEMBLockType = "ENCRYPTED PRIVATE KEY" 42 | ) 43 | 44 | // CreateRSAKey creates a new Key using RSA algorithm 45 | func CreateRSAKey(rsaBits int) (*Key, error) { 46 | priv, err := rsa.GenerateKey(rand.Reader, rsaBits) 47 | if err != nil { 48 | return nil, err 49 | } 50 | 51 | return NewKey(&priv.PublicKey, priv), nil 52 | } 53 | 54 | // CreateECDSAKey creates a new ECDSA key on the given curve 55 | func CreateECDSAKey(c elliptic.Curve) (*Key, error) { 56 | priv, err := ecdsa.GenerateKey(c, rand.Reader) 57 | if err != nil { 58 | return nil, err 59 | } 60 | 61 | return NewKey(&priv.PublicKey, priv), nil 62 | } 63 | 64 | // CreateEd25519Key creates a new Ed25519 key 65 | func CreateEd25519Key() (*Key, error) { 66 | _, priv, err := ed25519.GenerateKey(rand.Reader) 67 | if err != nil { 68 | return nil, err 69 | } 70 | 71 | return NewKey(priv.Public(), priv), nil 72 | } 73 | 74 | // Key contains a public-private keypair 75 | type Key struct { 76 | Public crypto.PublicKey 77 | Private crypto.PrivateKey 78 | } 79 | 80 | func NewKeyFromSigner(signer crypto.Signer) *Key { 81 | return &Key{Public: signer.Public(), Private: signer} 82 | } 83 | 84 | // NewKey returns a new public-private keypair Key type 85 | func NewKey(pub crypto.PublicKey, priv crypto.PrivateKey) *Key { 86 | return &Key{Public: pub, Private: priv} 87 | } 88 | 89 | // NewKeyFromPrivateKeyPEM inits Key from PEM-format rsa private key bytes 90 | func NewKeyFromPrivateKeyPEM(data []byte) (*Key, error) { 91 | pemBlock, _ := pem.Decode(data) 92 | if pemBlock == nil { 93 | return nil, errors.New("cannot find the next PEM formatted block") 94 | } 95 | var signer crypto.Signer 96 | switch pemBlock.Type { 97 | case rsaPrivateKeyPEMBlockType: 98 | priv, err := x509.ParsePKCS1PrivateKey(pemBlock.Bytes) 99 | if err != nil { 100 | return nil, err 101 | } 102 | signer = priv 103 | case pkcs8PrivateKeyPEMBlockType: 104 | priv, err := x509.ParsePKCS8PrivateKey(pemBlock.Bytes) 105 | if err != nil { 106 | return nil, err 107 | } 108 | signer = priv.(crypto.Signer) 109 | default: 110 | return nil, fmt.Errorf("unknown PEM block type %q", pemBlock.Type) 111 | } 112 | return NewKeyFromSigner(signer), nil 113 | } 114 | 115 | // NewKeyFromEncryptedPrivateKeyPEM inits Key from encrypted PEM-format private key bytes 116 | func NewKeyFromEncryptedPrivateKeyPEM(data []byte, password []byte) (*Key, error) { 117 | pemBlock, _ := pem.Decode(data) 118 | if pemBlock == nil { 119 | return nil, errors.New("cannot find the next PEM formatted block") 120 | } 121 | var signer crypto.Signer 122 | switch pemBlock.Type { 123 | case rsaPrivateKeyPEMBlockType: 124 | b, err := x509.DecryptPEMBlock(pemBlock, password) 125 | if err != nil { 126 | return nil, err 127 | } 128 | priv, err := x509.ParsePKCS1PrivateKey(b) 129 | if err != nil { 130 | return nil, err 131 | } 132 | signer = priv 133 | case encryptedPKCS8PrivateKeyPEMBLockType: 134 | b, err := pemutil.DecryptPKCS8PrivateKey(pemBlock.Bytes, password) 135 | if err != nil { 136 | return nil, err 137 | } 138 | priv, err := x509.ParsePKCS8PrivateKey(b) 139 | if err != nil { 140 | return nil, err 141 | } 142 | signer = priv.(crypto.Signer) 143 | default: 144 | return nil, fmt.Errorf("unsupported PEM block type %q", pemBlock.Type) 145 | } 146 | 147 | return NewKeyFromSigner(signer), nil 148 | } 149 | 150 | // ExportPrivate exports PEM-format private key. RSA keys are exported 151 | // as PKCS#1, ECDSA and Ed25519 keys are exported as PKCS#8. 152 | func (k *Key) ExportPrivate() ([]byte, error) { 153 | var privPEMBlock *pem.Block 154 | switch priv := k.Private.(type) { 155 | case *rsa.PrivateKey: 156 | privBytes := x509.MarshalPKCS1PrivateKey(priv) 157 | privPEMBlock = &pem.Block{ 158 | Type: rsaPrivateKeyPEMBlockType, 159 | Bytes: privBytes, 160 | } 161 | case *ecdsa.PrivateKey, ed25519.PrivateKey: 162 | privBytes, err := x509.MarshalPKCS8PrivateKey(priv) 163 | if err != nil { 164 | return nil, err 165 | } 166 | privPEMBlock = &pem.Block{ 167 | Type: pkcs8PrivateKeyPEMBlockType, 168 | Bytes: privBytes, 169 | } 170 | default: 171 | return nil, fmt.Errorf("unsupported key type %T", k.Private) 172 | } 173 | 174 | return pem.EncodeToMemory(privPEMBlock), nil 175 | } 176 | 177 | // ExportEncryptedPrivate exports encrypted PEM-format private key 178 | func (k *Key) ExportEncryptedPrivate(password []byte) ([]byte, error) { 179 | var privPEMBlock *pem.Block 180 | switch priv := k.Private.(type) { 181 | case *rsa.PrivateKey: 182 | privBytes := x509.MarshalPKCS1PrivateKey(priv) 183 | block, err := x509.EncryptPEMBlock(rand.Reader, rsaPrivateKeyPEMBlockType, privBytes, password, x509.PEMCipher3DES) 184 | if err != nil { 185 | return nil, err 186 | } 187 | privPEMBlock = block 188 | case *ecdsa.PrivateKey, ed25519.PrivateKey: 189 | privBytes, err := x509.MarshalPKCS8PrivateKey(priv) 190 | if err != nil { 191 | return nil, err 192 | } 193 | block, err := pemutil.EncryptPKCS8PrivateKey(rand.Reader, privBytes, password, x509.PEMCipherAES256) 194 | if err != nil { 195 | return nil, err 196 | } 197 | privPEMBlock = block 198 | default: 199 | return nil, fmt.Errorf("unsupported key type %T", k.Private) 200 | } 201 | 202 | return pem.EncodeToMemory(privPEMBlock), nil 203 | } 204 | 205 | // rsaPublicKey reflects the ASN.1 structure of a PKCS#1 public key. 206 | type rsaPublicKey struct { 207 | N *big.Int 208 | E int 209 | } 210 | 211 | // GenerateSubjectKeyID generates SubjectKeyId used in Certificate 212 | // Id is 160-bit SHA-1 hash of the value of the BIT STRING subjectPublicKey 213 | func GenerateSubjectKeyID(pub crypto.PublicKey) ([]byte, error) { 214 | var pubBytes []byte 215 | var err error 216 | switch pub := pub.(type) { 217 | case *rsa.PublicKey: 218 | pubBytes, err = asn1.Marshal(rsaPublicKey{ 219 | N: pub.N, 220 | E: pub.E, 221 | }) 222 | if err != nil { 223 | return nil, err 224 | } 225 | case *ecdsa.PublicKey: 226 | pubBytes = elliptic.Marshal(pub.Curve, pub.X, pub.Y) 227 | case ed25519.PublicKey: 228 | pubBytes = pub 229 | default: 230 | return nil, fmt.Errorf("unsupported key type %T", pub) 231 | } 232 | 233 | hash := sha1.Sum(pubBytes) 234 | 235 | return hash[:], nil 236 | } 237 | -------------------------------------------------------------------------------- /pkix/csr.go: -------------------------------------------------------------------------------- 1 | /*- 2 | * Copyright 2015 Square Inc. 3 | * Copyright 2014 CoreOS 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package pkix 19 | 20 | import ( 21 | "bytes" 22 | "crypto" 23 | "crypto/ecdsa" 24 | "crypto/rand" 25 | "crypto/rsa" 26 | "crypto/x509" 27 | "crypto/x509/pkix" 28 | "encoding/asn1" 29 | "encoding/pem" 30 | "errors" 31 | "fmt" 32 | "math/big" 33 | "net" 34 | "net/url" 35 | "strings" 36 | ) 37 | 38 | const ( 39 | csrPEMBlockType = "CERTIFICATE REQUEST" 40 | oldCsrPEMBlockType = "NEW CERTIFICATE REQUEST" 41 | ) 42 | 43 | // ParseAndValidateIPs parses a comma-delimited list of IP addresses into an array of IP addresses 44 | func ParseAndValidateIPs(ipList string) (res []net.IP, err error) { 45 | // IP list can potentially be a blank string, "" 46 | if len(ipList) > 0 { 47 | ips := strings.Split(ipList, ",") 48 | for _, ip := range ips { 49 | parsedIP := net.ParseIP(ip) 50 | if parsedIP == nil { 51 | return nil, fmt.Errorf("Invalid IP address: %s", ip) 52 | } 53 | res = append(res, parsedIP) 54 | } 55 | } 56 | return 57 | } 58 | 59 | // ParseAndValidateURIs parses a comma-delimited list of URIs into an array of url.URLs 60 | func ParseAndValidateURIs(uriList string) (res []*url.URL, err error) { 61 | if len(uriList) > 0 { 62 | uris := strings.Split(uriList, ",") 63 | for _, uri := range uris { 64 | parsedURI, err := url.Parse(uri) 65 | if err != nil { 66 | parsedURI = nil 67 | } 68 | if parsedURI == nil { 69 | return nil, fmt.Errorf("Invalid URI: %s", uri) 70 | } 71 | if !parsedURI.IsAbs() { 72 | return nil, fmt.Errorf("Invalid URI: %s", uri) 73 | } 74 | res = append(res, parsedURI) 75 | } 76 | } 77 | return 78 | } 79 | 80 | // CreateCertificateSigningRequest sets up a request to create a csr file with the given parameters 81 | func CreateCertificateSigningRequest(key *Key, organizationalUnit string, ipList []net.IP, domainList []string, uriList []*url.URL, organization string, country string, province string, locality string, commonName string) (*CertificateSigningRequest, error) { 82 | csrPkixName := pkix.Name{CommonName: commonName} 83 | 84 | if len(organizationalUnit) > 0 { 85 | csrPkixName.OrganizationalUnit = []string{organizationalUnit} 86 | } 87 | if len(organization) > 0 { 88 | csrPkixName.Organization = []string{organization} 89 | } 90 | if len(country) > 0 { 91 | csrPkixName.Country = []string{country} 92 | } 93 | if len(province) > 0 { 94 | csrPkixName.Province = []string{province} 95 | } 96 | if len(locality) > 0 { 97 | csrPkixName.Locality = []string{locality} 98 | } 99 | csrTemplate := &x509.CertificateRequest{ 100 | Subject: csrPkixName, 101 | IPAddresses: ipList, 102 | DNSNames: domainList, 103 | URIs: uriList, 104 | } 105 | 106 | csrBytes, err := x509.CreateCertificateRequest(rand.Reader, csrTemplate, key.Private) 107 | if err != nil { 108 | return nil, err 109 | } 110 | return NewCertificateSigningRequestFromDER(csrBytes), nil 111 | } 112 | 113 | // CertificateSigningRequest is a wrapper around a x509 CertificateRequest and its DER-formatted bytes 114 | type CertificateSigningRequest struct { 115 | // derBytes is always set for valid Certificate 116 | derBytes []byte 117 | 118 | cr *x509.CertificateRequest 119 | } 120 | 121 | // NewCertificateSigningRequestFromDER inits CertificateSigningRequest from DER-format bytes 122 | func NewCertificateSigningRequestFromDER(derBytes []byte) *CertificateSigningRequest { 123 | return &CertificateSigningRequest{derBytes: derBytes} 124 | } 125 | 126 | // NewCertificateSigningRequestFromPEM inits CertificateSigningRequest from PEM-format bytes 127 | // data should contain at most one certificate 128 | func NewCertificateSigningRequestFromPEM(data []byte) (*CertificateSigningRequest, error) { 129 | pemBlock, _ := pem.Decode(data) 130 | if pemBlock == nil { 131 | return nil, errors.New("cannot find the next PEM formatted block") 132 | } 133 | if (pemBlock.Type != csrPEMBlockType && pemBlock.Type != oldCsrPEMBlockType) || len(pemBlock.Headers) != 0 { 134 | return nil, errors.New("unmatched type or headers") 135 | } 136 | return &CertificateSigningRequest{derBytes: pemBlock.Bytes}, nil 137 | } 138 | 139 | // build cr field if needed 140 | func (c *CertificateSigningRequest) buildPKCS10CertificateSigningRequest() error { 141 | if c.cr != nil { 142 | return nil 143 | } 144 | 145 | var err error 146 | c.cr, err = x509.ParseCertificateRequest(c.derBytes) 147 | if err != nil { 148 | return err 149 | } 150 | return nil 151 | } 152 | 153 | // GetRawCertificateSigningRequest returns a copy of this certificate request as an x509.CertificateRequest. 154 | func (c *CertificateSigningRequest) GetRawCertificateSigningRequest() (*x509.CertificateRequest, error) { 155 | if err := c.buildPKCS10CertificateSigningRequest(); err != nil { 156 | return nil, err 157 | } 158 | return c.cr, nil 159 | } 160 | 161 | // CheckSignature verifies that the signature is a valid signature 162 | // using the public key in CertificateSigningRequest. 163 | func (c *CertificateSigningRequest) CheckSignature() error { 164 | if err := c.buildPKCS10CertificateSigningRequest(); err != nil { 165 | return err 166 | } 167 | return checkSignature(c.cr, c.cr.SignatureAlgorithm, c.cr.RawTBSCertificateRequest, c.cr.Signature) 168 | } 169 | 170 | // checkSignature verifies a signature made by the key on a CSR, such 171 | // as on the CSR itself. 172 | func checkSignature(csr *x509.CertificateRequest, algo x509.SignatureAlgorithm, signed, signature []byte) error { 173 | var hashType crypto.Hash 174 | switch algo { 175 | case x509.SHA1WithRSA, x509.ECDSAWithSHA1: 176 | hashType = crypto.SHA1 177 | case x509.SHA256WithRSA, x509.ECDSAWithSHA256: 178 | hashType = crypto.SHA256 179 | case x509.SHA384WithRSA, x509.ECDSAWithSHA384: 180 | hashType = crypto.SHA384 181 | case x509.SHA512WithRSA, x509.ECDSAWithSHA512: 182 | hashType = crypto.SHA512 183 | default: 184 | return x509.ErrUnsupportedAlgorithm 185 | } 186 | if !hashType.Available() { 187 | return x509.ErrUnsupportedAlgorithm 188 | } 189 | h := hashType.New() 190 | 191 | // nolint:errcheck 192 | h.Write(signed) 193 | digest := h.Sum(nil) 194 | switch pub := csr.PublicKey.(type) { 195 | case *rsa.PublicKey: 196 | return rsa.VerifyPKCS1v15(pub, hashType, digest, signature) 197 | case *ecdsa.PublicKey: 198 | ecdsaSig := new(struct{ R, S *big.Int }) 199 | if _, err := asn1.Unmarshal(signature, ecdsaSig); err != nil { 200 | return err 201 | } 202 | if ecdsaSig.R.Sign() <= 0 || ecdsaSig.S.Sign() <= 0 { 203 | return errors.New("x509: ECDSA signature contained zero or negative values") 204 | } 205 | if !ecdsa.Verify(pub, digest, ecdsaSig.R, ecdsaSig.S) { 206 | return errors.New("x509: ECDSA verification failure") 207 | } 208 | return nil 209 | } 210 | return x509.ErrUnsupportedAlgorithm 211 | } 212 | 213 | // Export returns PEM-format bytes 214 | func (c *CertificateSigningRequest) Export() ([]byte, error) { 215 | pemBlock := &pem.Block{ 216 | Type: csrPEMBlockType, 217 | Headers: nil, 218 | Bytes: c.derBytes, 219 | } 220 | 221 | buf := new(bytes.Buffer) 222 | if err := pem.Encode(buf, pemBlock); err != nil { 223 | return nil, err 224 | } 225 | return buf.Bytes(), nil 226 | } 227 | -------------------------------------------------------------------------------- /cmd/init.go: -------------------------------------------------------------------------------- 1 | /*- 2 | * Copyright 2015 Square Inc. 3 | * Copyright 2014 CoreOS 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package cmd 19 | 20 | import ( 21 | "fmt" 22 | "os" 23 | "strings" 24 | 25 | "github.com/square/certstrap/depot" 26 | "github.com/square/certstrap/pkix" 27 | "github.com/urfave/cli" 28 | ) 29 | 30 | // NewInitCommand sets up an "init" command to initialize a new CA 31 | func NewInitCommand() cli.Command { 32 | return cli.Command{ 33 | Name: "init", 34 | Usage: "Create Certificate Authority", 35 | Description: "Create Certificate Authority, including certificate, key and extra information file.", 36 | Flags: []cli.Flag{ 37 | cli.StringFlag{ 38 | Name: "passphrase", 39 | Usage: "Passphrase to encrypt private key PEM block", 40 | }, 41 | cli.IntFlag{ 42 | Name: "key-bits", 43 | Value: 4096, 44 | Usage: "Size (in bits) of RSA keypair to generate (example: 4096)", 45 | }, 46 | cli.StringFlag{ 47 | Name: "curve", 48 | Usage: fmt.Sprintf("Elliptic curve name. Must be one of %s.", supportedCurves()), 49 | }, 50 | cli.IntFlag{ 51 | Name: "years", 52 | Hidden: true, 53 | }, 54 | cli.StringFlag{ 55 | Name: "expires", 56 | Value: "18 months", 57 | Usage: "How long until the certificate expires (example: 1 year 2 days 3 months 4 hours)", 58 | }, 59 | cli.StringFlag{ 60 | Name: "organization, o", 61 | Usage: "Sets the Organization (O) field of the certificate", 62 | }, 63 | cli.StringFlag{ 64 | Name: "organizational-unit, ou", 65 | Usage: "Sets the Organizational Unit (OU) field of the certificate", 66 | }, 67 | cli.StringFlag{ 68 | Name: "country, c", 69 | Usage: "Sets the Country (C) field of the certificate", 70 | }, 71 | cli.StringFlag{ 72 | Name: "common-name, cn", 73 | Usage: "Sets the Common Name (CN) field of the certificate", 74 | }, 75 | cli.StringFlag{ 76 | Name: "province, st", 77 | Usage: "Sets the State/Province (ST) field of the certificate", 78 | }, 79 | cli.StringFlag{ 80 | Name: "locality, l", 81 | Usage: "Sets the Locality (L) field of the certificate", 82 | }, 83 | cli.StringFlag{ 84 | Name: "key", 85 | Usage: "Path to private key PEM file (if blank, will generate new key pair)", 86 | }, 87 | cli.BoolFlag{ 88 | Name: "stdout", 89 | Usage: "Print certificate to stdout in addition to saving file", 90 | }, 91 | cli.StringSliceFlag{ 92 | Name: "permit-domain", 93 | Usage: "Create a CA restricted to subdomains of this domain (can be specified multiple times)", 94 | }, 95 | cli.IntFlag{ 96 | Name: "path-length", 97 | Value: 0, 98 | Usage: "Maximum number of non-self-issued intermediate certificates that may follow this CA certificate in a valid certification path", 99 | }, 100 | cli.BoolFlag{ 101 | Name: "exclude-path-length", 102 | Usage: "Exclude 'Path Length Constraint' from this CA certificate", 103 | }, 104 | }, 105 | Action: initAction, 106 | } 107 | } 108 | 109 | func initAction(c *cli.Context) { 110 | if !c.IsSet("common-name") { 111 | fmt.Println("Must supply Common Name for CA") 112 | os.Exit(1) 113 | } 114 | 115 | formattedName := strings.Replace(c.String("common-name"), " ", "_", -1) 116 | 117 | if depot.CheckCertificate(d, formattedName) || depot.CheckPrivateKey(d, formattedName) { 118 | fmt.Fprintf(os.Stderr, "CA with specified name \"%s\" already exists!\n", formattedName) 119 | os.Exit(1) 120 | } 121 | 122 | if c.IsSet("path-length") && c.IsSet("exclude-path-length") { 123 | fmt.Fprintf(os.Stderr, "The \"path-length\" and \"exclude-path-length\" flags cannot be used together!\n") 124 | os.Exit(1) 125 | } 126 | 127 | var err error 128 | expires := c.String("expires") 129 | if years := c.Int("years"); years != 0 { 130 | expires = fmt.Sprintf("%s %d years", expires, years) 131 | } 132 | 133 | // Expiry parsing is a naive regex implementation 134 | // Token based parsing would provide better feedback but 135 | expiresTime, err := parseExpiry(expires) 136 | if err != nil { 137 | fmt.Fprintf(os.Stderr, "Invalid expiry: %s\n", err) 138 | os.Exit(1) 139 | } 140 | 141 | var passphrase []byte 142 | if c.IsSet("passphrase") { 143 | passphrase = []byte(c.String("passphrase")) 144 | } else { 145 | passphrase, err = createPassPhrase() 146 | if err != nil { 147 | fmt.Fprintln(os.Stderr, err) 148 | os.Exit(1) 149 | } 150 | } 151 | 152 | var key *pkix.Key 153 | switch { 154 | case c.IsSet("key"): 155 | keyBytes, err := os.ReadFile(c.String("key")) 156 | if err != nil { 157 | fmt.Fprintln(os.Stderr, "Read Key error:", err) 158 | os.Exit(1) 159 | } 160 | 161 | key, err = pkix.NewKeyFromPrivateKeyPEM(keyBytes) 162 | if err != nil { 163 | fmt.Fprintln(os.Stderr, "Read Key error:", err) 164 | os.Exit(1) 165 | } 166 | fmt.Printf("Read %s\n", c.String("key")) 167 | case c.IsSet("curve"): 168 | curve := c.String("curve") 169 | key, err = createKeyOnCurve(curve) 170 | if err != nil { 171 | fmt.Fprintf(os.Stderr, "Create %s Key error: %v\n", curve, err) 172 | os.Exit(1) 173 | } 174 | if len(passphrase) > 0 { 175 | fmt.Printf("Created %s/%s.key (encrypted by passphrase)\n", depotDir, formattedName) 176 | } else { 177 | fmt.Printf("Created %s/%s.key\n", depotDir, formattedName) 178 | } 179 | default: 180 | key, err = pkix.CreateRSAKey(c.Int("key-bits")) 181 | if err != nil { 182 | fmt.Fprintln(os.Stderr, "Create RSA Key error:", err) 183 | os.Exit(1) 184 | } 185 | if len(passphrase) > 0 { 186 | fmt.Printf("Created %s/%s.key (encrypted by passphrase)\n", depotDir, formattedName) 187 | } else { 188 | fmt.Printf("Created %s/%s.key\n", depotDir, formattedName) 189 | } 190 | } 191 | 192 | opts := []pkix.Option{ 193 | pkix.WithPathlenOption(c.Int("path-length"), c.Bool("exclude-path-length")), 194 | } 195 | 196 | crt, err := pkix.CreateCertificateAuthorityWithOptions(key, c.String("organizational-unit"), expiresTime, c.String("organization"), c.String("country"), c.String("province"), c.String("locality"), c.String("common-name"), c.StringSlice("permit-domain"), opts...) 197 | 198 | if err != nil { 199 | fmt.Fprintln(os.Stderr, "Create certificate error:", err) 200 | os.Exit(1) 201 | } 202 | fmt.Printf("Created %s/%s.crt\n", depotDir, formattedName) 203 | 204 | if c.Bool("stdout") { 205 | crtBytes, err := crt.Export() 206 | if err != nil { 207 | fmt.Fprintln(os.Stderr, "Print CA certificate error:", err) 208 | os.Exit(1) 209 | } else { 210 | fmt.Println(string(crtBytes)) 211 | } 212 | } 213 | 214 | if err = depot.PutCertificate(d, formattedName, crt); err != nil { 215 | fmt.Fprintln(os.Stderr, "Save certificate error:", err) 216 | } 217 | if len(passphrase) > 0 { 218 | if err = depot.PutEncryptedPrivateKey(d, formattedName, key, passphrase); err != nil { 219 | fmt.Fprintln(os.Stderr, "Save encrypted private key error:", err) 220 | } 221 | } else { 222 | if err = depot.PutPrivateKey(d, formattedName, key); err != nil { 223 | fmt.Fprintln(os.Stderr, "Save private key error:", err) 224 | } 225 | } 226 | 227 | // Create an empty CRL, this is useful for Java apps which mandate a CRL. 228 | crl, err := pkix.CreateCertificateRevocationList(key, crt, expiresTime) 229 | if err != nil { 230 | fmt.Fprintln(os.Stderr, "Create CRL error:", err) 231 | os.Exit(1) 232 | } 233 | if err = depot.PutCertificateRevocationList(d, formattedName, crl); err != nil { 234 | fmt.Fprintln(os.Stderr, "Save CRL error:", err) 235 | os.Exit(1) 236 | } 237 | fmt.Printf("Created %s/%s.crl\n", depotDir, formattedName) 238 | } 239 | -------------------------------------------------------------------------------- /cmd/request_cert.go: -------------------------------------------------------------------------------- 1 | /*- 2 | * Copyright 2015 Square Inc. 3 | * Copyright 2014 CoreOS 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package cmd 19 | 20 | import ( 21 | "fmt" 22 | "os" 23 | "regexp" 24 | "strings" 25 | 26 | "github.com/square/certstrap/depot" 27 | "github.com/square/certstrap/pkix" 28 | "github.com/urfave/cli" 29 | ) 30 | 31 | // NewCertRequestCommand sets up a "request-cert" command to create a request for a new certificate (CSR) 32 | func NewCertRequestCommand() cli.Command { 33 | return cli.Command{ 34 | Name: "request-cert", 35 | Usage: "Create certificate request for host", 36 | Description: "Create certificate for host, including certificate signing request and key. Must sign the request in order to generate a certificate.", 37 | Flags: []cli.Flag{ 38 | cli.StringFlag{ 39 | Name: "passphrase", 40 | Usage: "Passphrase to encrypt private-key PEM block", 41 | }, 42 | cli.IntFlag{ 43 | Name: "key-bits", 44 | Value: 2048, 45 | Usage: "Size (in bits) of RSA keypair to generate (example: 4096)", 46 | }, 47 | cli.StringFlag{ 48 | Name: "curve", 49 | Usage: fmt.Sprintf("Elliptic curve name. Must be one of %s.", supportedCurves()), 50 | }, 51 | cli.StringFlag{ 52 | Name: "organization, o", 53 | Usage: "Sets the Organization (O) field of the certificate", 54 | }, 55 | cli.StringFlag{ 56 | Name: "country, c", 57 | Usage: "Sets the Country (C) field of the certificate", 58 | }, 59 | cli.StringFlag{ 60 | Name: "locality, l", 61 | Usage: "Sets the Locality (L) field of the certificate", 62 | }, 63 | cli.StringFlag{ 64 | Name: "common-name, cn", 65 | Usage: "Sets the Common Name (CN) field of the certificate", 66 | }, 67 | cli.StringFlag{ 68 | Name: "organizational-unit, ou", 69 | Usage: "Sets the Organizational Unit (OU) field of the certificate", 70 | }, 71 | cli.StringFlag{ 72 | Name: "province, st", 73 | Usage: "Sets the State/Province (ST) field of the certificate", 74 | }, 75 | cli.StringFlag{ 76 | Name: "ip", 77 | Usage: "IP addresses to add as subject alt name (comma separated)", 78 | }, 79 | cli.StringFlag{ 80 | Name: "domain", 81 | Usage: "DNS entries to add as subject alt name (comma separated)", 82 | }, 83 | cli.StringFlag{ 84 | Name: "uri", 85 | Usage: "URI values to add as subject alt name (comma separated)", 86 | }, 87 | cli.StringFlag{ 88 | Name: "key", 89 | Usage: "Path to private key PEM file (if blank or if file doesn't exist, will generate new keypair)", 90 | }, 91 | cli.StringFlag{ 92 | Name: "csr", 93 | Usage: "Path to CSR output PEM file (if blank, will use --depot-path and default name)", 94 | }, 95 | cli.BoolFlag{ 96 | Name: "stdout", 97 | Usage: "Print signing request to stdout in addition to saving file", 98 | }, 99 | }, 100 | Action: newCertAction, 101 | } 102 | } 103 | 104 | func newCertAction(c *cli.Context) { 105 | var name = "" 106 | var err error 107 | 108 | // The CLI Context returns an empty string ("") if no value is available 109 | ips, err := pkix.ParseAndValidateIPs(c.String("ip")) 110 | 111 | if err != nil { 112 | fmt.Fprintln(os.Stderr, err) 113 | os.Exit(1) 114 | } 115 | 116 | // The CLI Context returns an empty string ("") if no value is available 117 | uris, err := pkix.ParseAndValidateURIs(c.String("uri")) 118 | 119 | if err != nil { 120 | fmt.Fprintln(os.Stderr, err) 121 | os.Exit(1) 122 | } 123 | 124 | domains := strings.Split(c.String("domain"), ",") 125 | if c.String("domain") == "" { 126 | domains = nil 127 | } 128 | 129 | switch { 130 | case len(c.String("common-name")) != 0: 131 | name = c.String("common-name") 132 | case len(domains) != 0: 133 | name = domains[0] 134 | case len(uris) != 0: 135 | name = uris[0].String() 136 | default: 137 | fmt.Fprintln(os.Stderr, "Must provide Common Name, domain, or URI") 138 | os.Exit(1) 139 | } 140 | 141 | var formattedName = formatName(name) 142 | 143 | // skip the check if the --csr option is specified 144 | if !c.IsSet("csr") && (depot.CheckCertificateSigningRequest(d, formattedName) || depot.CheckPrivateKey(d, formattedName)) { 145 | fmt.Fprintf(os.Stderr, "Certificate request \"%s\" already exists!\n", formattedName) 146 | os.Exit(1) 147 | } 148 | 149 | var passphrase []byte 150 | if c.IsSet("passphrase") { 151 | passphrase = []byte(c.String("passphrase")) 152 | } else { 153 | passphrase, err = createPassPhrase() 154 | if err != nil { 155 | fmt.Fprintln(os.Stderr, err) 156 | os.Exit(1) 157 | } 158 | } 159 | 160 | // generate new key if one doesn't exist already 161 | var key *pkix.Key 162 | keyFilepath := fileName(c, "key", depotDir, formattedName, "key") 163 | switch { 164 | case c.IsSet("key") && fileExists(c.String("key")): 165 | keyBytes, err := os.ReadFile(c.String("key")) 166 | if err != nil { 167 | fmt.Fprintln(os.Stderr, "Read Key error:", err) 168 | os.Exit(1) 169 | } 170 | 171 | key, err = pkix.NewKeyFromPrivateKeyPEM(keyBytes) 172 | if err != nil { 173 | fmt.Fprintln(os.Stderr, "Read Key error:", err) 174 | os.Exit(1) 175 | } 176 | fmt.Printf("Read %s\n", keyFilepath) 177 | case c.IsSet("curve"): 178 | curve := c.String("curve") 179 | key, err = createKeyOnCurve(curve) 180 | if err != nil { 181 | fmt.Fprintf(os.Stderr, "Create %s Key error: %v", curve, err) 182 | os.Exit(1) 183 | } 184 | if len(passphrase) > 0 { 185 | fmt.Printf("Created %s (encrypted by passphrase)\n", keyFilepath) 186 | } else { 187 | fmt.Printf("Created %s\n", keyFilepath) 188 | } 189 | default: 190 | key, err = pkix.CreateRSAKey(c.Int("key-bits")) 191 | if err != nil { 192 | fmt.Fprintln(os.Stderr, "Create RSA Key error:", err) 193 | os.Exit(1) 194 | } 195 | if len(passphrase) > 0 { 196 | fmt.Printf("Created %s (encrypted by passphrase)\n", keyFilepath) 197 | } else { 198 | fmt.Printf("Created %s\n", keyFilepath) 199 | } 200 | } 201 | 202 | csr, err := pkix.CreateCertificateSigningRequest(key, c.String("organizational-unit"), ips, domains, uris, c.String("organization"), c.String("country"), c.String("province"), c.String("locality"), name) 203 | if err != nil { 204 | fmt.Fprintln(os.Stderr, "Create certificate request error:", err) 205 | os.Exit(1) 206 | } else { 207 | fmt.Printf("Created %s\n", fileName(c, "csr", depotDir, formattedName, "csr")) 208 | } 209 | 210 | if c.Bool("stdout") { 211 | csrBytes, err := csr.Export() 212 | if err != nil { 213 | fmt.Fprintln(os.Stderr, "Print certificate request error:", err) 214 | os.Exit(1) 215 | } else { 216 | fmt.Println(string(csrBytes)) 217 | } 218 | } 219 | 220 | if err = putCertificateSigningRequest(c, d, formattedName, csr); err != nil { 221 | fmt.Fprintln(os.Stderr, "Save certificate request error:", err) 222 | } 223 | if len(passphrase) > 0 { 224 | if err = putEncryptedPrivateKey(c, d, formattedName, key, passphrase); err != nil { 225 | fmt.Fprintln(os.Stderr, "Save encrypted private key error:", err) 226 | } 227 | } else { 228 | if err = putPrivateKey(c, d, formattedName, key); err != nil { 229 | fmt.Fprintln(os.Stderr, "Save private key error:", err) 230 | } 231 | } 232 | } 233 | 234 | func formatName(name string) string { 235 | var filenameAcceptable, err = regexp.Compile("[^a-zA-Z0-9._-]+") 236 | if err != nil { 237 | fmt.Fprintln(os.Stderr, "Error compiling regex:", err) 238 | os.Exit(1) 239 | } 240 | return string(filenameAcceptable.ReplaceAll([]byte(name), []byte("_"))) 241 | } 242 | 243 | func fileName(c *cli.Context, flagName, depotDir, name, ext string) string { 244 | if c.IsSet(flagName) { 245 | return c.String(flagName) 246 | } 247 | return fmt.Sprintf("%s/%s.%s", depotDir, name, ext) 248 | } 249 | -------------------------------------------------------------------------------- /pkix/cert_test.go: -------------------------------------------------------------------------------- 1 | /*- 2 | * Copyright 2015 Square Inc. 3 | * Copyright 2014 CoreOS 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package pkix 19 | 20 | import ( 21 | "bytes" 22 | "testing" 23 | "time" 24 | ) 25 | 26 | const ( 27 | // hostname used by CA certificate 28 | authHostname = "CA" 29 | 30 | // ./certstrap init --c "USA" -o "etcd-ca" --ou "CA" --cn "CA" 31 | certAuthPEM = `-----BEGIN CERTIFICATE----- 32 | MIIFNDCCAxygAwIBAgIBATANBgkqhkiG9w0BAQsFADA6MQwwCgYDVQQGEwNVU0Ex 33 | EDAOBgNVBAoTB2V0Y2QtY2ExCzAJBgNVBAsTAkNBMQswCQYDVQQDEwJDQTAeFw0y 34 | NTA3MDIyMzA5MzVaFw0yNzAxMDIyMzE5MzNaMDoxDDAKBgNVBAYTA1VTQTEQMA4G 35 | A1UEChMHZXRjZC1jYTELMAkGA1UECxMCQ0ExCzAJBgNVBAMTAkNBMIICIjANBgkq 36 | hkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA0yNmBdiP1mI6qpUhIs7iN4kGVIOG36cF 37 | ez53lsjDNUn1L/nqcOlRNRacpgJkhBvMb/SEVv0muLC0b+bHJLGThIKJQyi1/CcG 38 | LiXFLKjuyx8VSsKCuROhp4LZLMLpUhcftXB+NMUUkJOCQPeytvFvpYeOD7rKVK3L 39 | Xf2PtZpIYQrBkmAcdwkqoVhTuvPqw5apOdURC+9rb9utJauaEkEhVrH1rrW9OBUN 40 | be0Xu5isMzEHohSfeeAqIdR1PPrMRmc8eEfEbWKXf8Jzwri0F7Phrfxtxz9eT3yA 41 | TpNr8b5G1XZIc9sKUG7q+8BV4rgLpAjd/n3+7vaF7JGyd7ohip3tIzdf/N7jwW+8 42 | G7JQWRDJwf52EGmM3miEyBMef/B9lprjSO+bHRyjEAjWU7G+Muy29mKi7l3h+3XR 43 | dBler2mLfsaqYjFmSVkX3Uf4ENUau26v3jiVJBzLBHarg4FTcABX3S96UiQivc50 44 | zp1wSkTEcnZjYsGcqgZKnLDK6X4ThwAGfPlkJbK7uEh4fLtk9mVfjDPu4fgp5b52 45 | MsCdXp/FGbnOpDGAEGTgsTq6KKPCRIxU5bTH38v/j2MI3MSNyfsb73PnMnqqApNQ 46 | 3Mrkwb7UmdVK9dadlDn6X47GKKmg7MqKeJ4Xxo8j32LWCdpGT09myWrh06hsrqAH 47 | E8Zm1vmrsOsCAwEAAaNFMEMwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYB 48 | Af8CAQAwHQYDVR0OBBYEFIMTOTmZTYIKOYoj2Y17xmyVKi0FMA0GCSqGSIb3DQEB 49 | CwUAA4ICAQAq1FtkMNR+4SMf1ojaNxvEIYbp759XDOUS0IuWPTrWYxgdjoJoc9Wp 50 | 0JRSDqADI5SdwSuNUbJ+H+YydZ4Dq9PTfgKxN0kHrK40e13F1G/qZ1V00+mNI5Zr 51 | vGKK5rzIfWyv1elnwcoh04k8l/EO9azhF+bUY4VKQEIjc26zHZwdQfcPRGSlfh2m 52 | DsjTUcGCa6J4vty98Z29rRrmO1KConAc1aI/GWCDDogCzHTeBJG4MAvzwtPe4oCs 53 | h8awwy7HqQRyKWxVtfh+3yRrx4fE4IDHXlaGj1Y2eiR31r3DjA2VdOepebHY/rzu 54 | FmrlNWAlu5EpucfquI5bPgxANVGKVXTxsVztclsoKeqm7w1mLJXbEiuJihKwmfyA 55 | 0KjSnKzoFP4l72o15bGUMWr7jvSRJYGVCTuUEMcpAWWvohS4d4m0tBjfEimDkDwZ 56 | TyrpFueAWCw7XI19IAa79ileVJvxN2Pnp3dEdaYpKEPsxfjXQyofG6m9eG7GVdgh 57 | KbtjRZJ1xKUeBH0u0yHIhSAvjYeOHcWF/mhQSO9gwanciI5FnsG5zYjFcSj5qcY+ 58 | PWK1J5ZtavbbdYJCAVoKsMQKSzNemzqATIQmoCeRt5ztyt8Za/mN1KFnCgbwSFTn 59 | GYUrZo1+6jMH+1oBLLex8YV6uJxoM9SoFqRMq4poMlSlLmk5sX4JiA== 60 | -----END CERTIFICATE----- 61 | ` 62 | badCertAuthPEM = `-----BEGIN CERTIFICATE----- 63 | MIIB9zCCAWKgAwIBAgIBATALBgkqhkiG9w0BAQUwMTEMMAoGA1UEBhMDVVNBMRQw 64 | EgYAVQQKEwtDb3JlT1MgSW5jLjELMAkGA1UEAxMCQ0EwHhcNMTQwMzA5MjE1NDI5 65 | WhcNMjQwMzA5MjI1NDI5WjAxMQwwCgYDVQQGEwNVU0ExFDASBgNVBAoTC0NvcmVP 66 | UyBJbmMuMQswCQYDVQQDEwJDQTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA 67 | xLZYiSaYRWC90r/W+3cVFI6NnWfEo9Wrbn/PsJRz+Nn1NURuLpYWrMSZa1ihipVr 68 | bPY9Xi8Xo5YCll2z9RcWoVp0ASU1VxctXKWbsk/lqnAKDX+/lTW4iKERUF67NOlR 69 | GFtBzq7iVPQT7qNYCMu3CRG/4cTuOcCglH/xE9HdgdcCAwEAAaMjMCEwDgYDVR0P 70 | AQH/BAQDAgAEMA8GA1UdEwEB/wQFMAMBAf8wCwYJKoZIhvcNAQEFA4GBAL129Vc3 71 | lcfYfSfI2fMgkG3hc2Yhtu/SJ7wRFqlrNBM9lnNJnYMF+fAWv6u8xix8OWfYs38U 72 | BB6sTriDpe5oo2H0o7Pf5ACE3IIy2Cf2+HAmNClYrdlwNYfP7aUazbEhuzPcvJYA 73 | zPNy61oRnsETV77BH+JQ7j4E+pAJ5MHpKUcq 74 | -----END CERTIFICATE----- 75 | ` 76 | wrongCertAuthPEM = `-----BEGIN WRONG CERTIFICATE----- 77 | MIIB9zCCAWKgAwIBAgIBATALBgkqhkiG9w0BAQUwMTEMMAoGA1UEBhMDVVNBMRQw 78 | EgYDVQQKEwtDb3JlT1MgSW5jLjELMAkGA1UEAxMCQ0EwHhcNMTQwMzA5MTgzMzQx 79 | WhcNMjQwMzA5MTkzMzQxWjAxMQwwCgYDVQQGEwNVU0ExFDASBgNVBAoTC0NvcmVP 80 | UyBJbmMuMQswCQYDVQQDEwJDQTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA 81 | ptSfk77PDDWYiNholqgPyQwtnf7hmoFGEqiA4Cu0u+LW7vLqkysaXHUVjQH/ditJ 82 | FPlvwsllgPbgCF9bUzrCbXbrV2xjIhairyOGFSrLGBZMIB91xHXPlFhy2U+4Piio 83 | bisrv2InHvPTyyZqVbqLDhF8DmVMIZI/UCOKtCMSrN8CAwEAAaMjMCEwDgYDVR0P 84 | AQH/BAQDAgAEMA8GA1UdEwEB/wQFMAMBAf8wCwYJKoZIhvcNAQEFA4GBAHKzf9iH 85 | fKUdWUz5Ue8a1yRRTu5EuGK3pz22x6udcIYH6KFBPVfj5lSbbE3NirE7TKWvF2on 86 | SCP/620bWJMxqNAYdwpiyGibsiUlueWB/3aavoq10MIHA6MBxw/wrsoLPns9f7dP 87 | +ddM40NjuI1tvX6SnUwuahONdvUJDxqVR+AM 88 | -----END WRONG CERTIFICATE----- 89 | ` 90 | // ./certstrap request-cert --c "USA" -o "etcd-ca" --ou "host1" --cn "host1" 91 | // ./certstrap sign host1 --CA CA 92 | certHostPEM = `-----BEGIN CERTIFICATE----- 93 | MIIEdTCCAl2gAwIBAgIQD9oO6SirviOMj1tTCD2FEDANBgkqhkiG9w0BAQsFADA6 94 | MQwwCgYDVQQGEwNVU0ExEDAOBgNVBAoTB2V0Y2QtY2ExCzAJBgNVBAsTAkNBMQsw 95 | CQYDVQQDEwJDQTAeFw0yNTA3MDIyMzE0MjZaFw0yNzAxMDIyMzE5MzNaMEAxDDAK 96 | BgNVBAYTA1VTQTEQMA4GA1UEChMHZXRjZC1jYTEOMAwGA1UECxMFaG9zdDExDjAM 97 | BgNVBAMTBWhvc3QxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAucbb 98 | gMKFM0HiYdYwqLHgAsA/0dytfFYObnPNPLFSMpKKEYxRSHVRKBFfm8ce6ivVDBEN 99 | AN75VqQdi0qPh2tYDs1XFcJM6AU9TPiWPFoMq00e+SyFqwR7s0SRJOfIt4VA0ggG 100 | rVdtjrWTlCv5EjpS1lTapMelZfoiNomebrKmfCMo9+VTjK32Hx8ncM1NNzVgq6rM 101 | uts0Yc27AeiTm7lZbUjm6TDAElzUREnT90i/IPEm4VHX8eWxtpaBU+Aals7lSftf 102 | aRESyTlu+b/lvH8WHfRqrCFpSNgBKimIP0+eSsaSs+K8tQ1QgEQtt7CLjlLCVYU6 103 | H5k5oF+jA3VRM96aQQIDAQABo3EwbzAOBgNVHQ8BAf8EBAMCA7gwHQYDVR0lBBYw 104 | FAYIKwYBBQUHAwEGCCsGAQUFBwMCMB0GA1UdDgQWBBQ3KvRJ3MhCjTittRUR5erx 105 | /DldSzAfBgNVHSMEGDAWgBSDEzk5mU2CCjmKI9mNe8ZslSotBTANBgkqhkiG9w0B 106 | AQsFAAOCAgEAO6VLumsg5D3INYK/B0ITHPf5StdeQRGEIfZZmdKmZfkcdfIedRjB 107 | S9GxJ44twdKTLVQhje4IAEMxcy7o5vWCScVtV8c+jq+KKBY8wpUgtBFKULdB86sO 108 | Iiaf1rDgFEKmu+ksfX8DY8BQJJpZ4bqUQPqkyD7bfTN7FHBIMHQVsQAvnr0SDnd1 109 | ei0+CO4J1BZq+jFQA8LLdvpXrdMW+AzbalE/HaW2A2HZD0fBHmWYuHWqO8RFPEmX 110 | If12xgHw6c9uskVu+SqXkaFoYf4Wg5MgYM9C/QKZgV7uooy2dHIJH+n9U3Js3dCs 111 | 2RIFinyw2MpQ0ULAduqN9R5Oppzzcw3P21bYFBf2kEwy5OLYoV2JkPVUoIZDBphk 112 | Olm5oHoz99qG4ufzo7nPUV5WbebInfY2Ql4Fua8nR4FEkc8MXyPEmEphqXwyJsS+ 113 | OH93trtyP7TMTDH43euAzVzz4E0pcwNXL4g/U6pElLIvtR8FCt/TOliWxIOtJlhU 114 | JZgrj2oQd85Ppp5AxcFBaDvXU5qFd5DlwDEQOIcV716OCvnP8X5Y9JO7C6gjvyGQ 115 | bIk5R1WNw9pUy5QOETnWIEJOj06Q70dFEO1LGSt1bg4PDZ8lwJ/BWDd05NvioXPR 116 | fRlokuzJaHNz3Y12aeaiypLowGhjfDISujswNl5be3vDDjKcwCwa/yE= 117 | -----END CERTIFICATE----- 118 | ` 119 | ) 120 | 121 | func TestCertificateAuthority(t *testing.T) { 122 | crt, err := NewCertificateFromPEM([]byte(certAuthPEM)) 123 | if err != nil { 124 | t.Fatal("Failed to parse certificate from PEM:", err) 125 | } 126 | 127 | if err = crt.CheckAuthority(); err != nil { 128 | t.Fatal("Failed to check self-sign:", err) 129 | } 130 | 131 | if err = crt.VerifyHost(crt, authHostname); err != nil { 132 | t.Fatal("Failed to verify CA:", err) 133 | } 134 | 135 | duration := crt.GetExpirationDuration() 136 | expireDate, _ := time.Parse("2006-Jan-02", "2024-Feb-03") 137 | if !time.Now().Add(duration).After(expireDate) { 138 | t.Fatal("Failed to get correct expiration") 139 | } 140 | 141 | pemBytes, err := crt.Export() 142 | if err != nil { 143 | t.Fatal("Failed exporting PEM-format bytes:", err) 144 | } 145 | if !bytes.Equal(pemBytes, []byte(certAuthPEM)) { 146 | t.Fatal("Failed exporting the same PEM-format bytes") 147 | } 148 | } 149 | 150 | func TestWrongCertificate(t *testing.T) { 151 | if _, err := NewCertificateFromPEM([]byte("-")); err == nil { 152 | t.Fatal("Expect not to parse certificate from PEM:", err) 153 | } 154 | 155 | if _, err := NewCertificateFromPEM([]byte(wrongCertAuthPEM)); err == nil { 156 | t.Fatal("Expect not to parse certificate from PEM:", err) 157 | } 158 | } 159 | 160 | func TestBadCertificate(t *testing.T) { 161 | crt, err := NewCertificateFromPEM([]byte(badCertAuthPEM)) 162 | if err != nil { 163 | t.Fatal("Failed to parse certificate from PEM:", err) 164 | } 165 | 166 | if _, err = crt.GetRawCertificate(); err == nil { 167 | t.Fatal("Expect not to get x509.Certificate") 168 | } 169 | 170 | if err = crt.CheckAuthority(); err == nil { 171 | t.Fatal("Expect not to get x509.Certificate") 172 | } 173 | 174 | if err = crt.VerifyHost(crt, authHostname); err == nil { 175 | t.Fatal("Expect not to get x509.Certificate") 176 | } 177 | 178 | if duration := crt.GetExpirationDuration(); duration.Hours() >= 0 { 179 | t.Fatal("Expect not to get positive duration") 180 | } 181 | 182 | pemBytes, err := crt.Export() 183 | if err != nil { 184 | t.Fatal("Failed exporting PEM-format bytes:", err) 185 | } 186 | if !bytes.Equal(pemBytes, []byte(badCertAuthPEM)) { 187 | t.Fatal("Failed exporting the same PEM-format bytes") 188 | } 189 | } 190 | 191 | func TestCertificateVerify(t *testing.T) { 192 | crtAuth, err := NewCertificateFromPEM([]byte(certAuthPEM)) 193 | if err != nil { 194 | t.Fatal("Failed to parse certificate from PEM:", err) 195 | } 196 | 197 | crtHost, err := NewCertificateFromPEM([]byte(certHostPEM)) 198 | if err != nil { 199 | t.Fatal("Failed to parse certificate from PEM:", err) 200 | } 201 | 202 | if err = crtAuth.VerifyHost(crtHost, csrHostname); err != nil { 203 | t.Fatal("Verify certificate host from CA:", err) 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /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 | 203 | -------------------------------------------------------------------------------- /pkix/key_test.go: -------------------------------------------------------------------------------- 1 | /*- 2 | * Copyright 2015 Square Inc. 3 | * Copyright 2014 CoreOS 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package pkix 19 | 20 | import ( 21 | "bytes" 22 | "crypto/ecdsa" 23 | "crypto/ed25519" 24 | "crypto/elliptic" 25 | "crypto/rsa" 26 | "encoding/base64" 27 | "encoding/hex" 28 | "testing" 29 | ) 30 | 31 | const ( 32 | // ./certstrap init --c "USA" -o "etcd-ca" --ou "CA" --cn "CA" 33 | rsaPrivKeyAuthPEM = `-----BEGIN RSA PRIVATE KEY----- 34 | MIIJKQIBAAKCAgEA0yNmBdiP1mI6qpUhIs7iN4kGVIOG36cFez53lsjDNUn1L/nq 35 | cOlRNRacpgJkhBvMb/SEVv0muLC0b+bHJLGThIKJQyi1/CcGLiXFLKjuyx8VSsKC 36 | uROhp4LZLMLpUhcftXB+NMUUkJOCQPeytvFvpYeOD7rKVK3LXf2PtZpIYQrBkmAc 37 | dwkqoVhTuvPqw5apOdURC+9rb9utJauaEkEhVrH1rrW9OBUNbe0Xu5isMzEHohSf 38 | eeAqIdR1PPrMRmc8eEfEbWKXf8Jzwri0F7Phrfxtxz9eT3yATpNr8b5G1XZIc9sK 39 | UG7q+8BV4rgLpAjd/n3+7vaF7JGyd7ohip3tIzdf/N7jwW+8G7JQWRDJwf52EGmM 40 | 3miEyBMef/B9lprjSO+bHRyjEAjWU7G+Muy29mKi7l3h+3XRdBler2mLfsaqYjFm 41 | SVkX3Uf4ENUau26v3jiVJBzLBHarg4FTcABX3S96UiQivc50zp1wSkTEcnZjYsGc 42 | qgZKnLDK6X4ThwAGfPlkJbK7uEh4fLtk9mVfjDPu4fgp5b52MsCdXp/FGbnOpDGA 43 | EGTgsTq6KKPCRIxU5bTH38v/j2MI3MSNyfsb73PnMnqqApNQ3Mrkwb7UmdVK9dad 44 | lDn6X47GKKmg7MqKeJ4Xxo8j32LWCdpGT09myWrh06hsrqAHE8Zm1vmrsOsCAwEA 45 | AQKCAgBBZlmXvfjv4wVhCUh2S7bulNcNHqCMbmPYRQUuA4nT29DCx5rC1sJ8u0BS 46 | e7M+6I1usEK93zQ7SSDa+JT+3LJg/T4fO2EDdeMIMFLe/oTZDgu+WHm9ckNEa9dx 47 | cf5rmxYLUYkGN3WjQs256f/FgwueLlrmrGk3yY2Q05XMHroEtRw4huTKSmCWEZH9 48 | +sfhRa2taD4bgFG7GESNwpW6ycnV3NHJCCpQUNUUE7iiNyw/vxQqNFEhoznpuLGH 49 | 7feQZzHn3/MMHtnmjQjma+f8348sIWCvswU3gc0MicWJ3/J49GaE3HhZacIHsQ/p 50 | ZjDU4ppA1i49PsdE++xYAaOaGEj3a2Mo3QSb3lc2fBnxiOHyWOA8ICaF5iCRWFYM 51 | MIS+moBIXaLONY9LXw3FBmwYw5+fRRX5SAsznbQ+mA18PBP4mziteeqAlVFQzuel 52 | JCiCRlcP+j/xT/uExI0YLLAQfqNGv+LDxTslEv3B3IjErOYgcCy8/d9HKoFcMmj2 53 | SYFWUdkckPzasMtuFofHsqE84TL74p3vUy75SDWni9PlKdqxl+O9FkhMF0SjUsE9 54 | t2RQjjE8dpaZZko1z41E7fQ2jPZmH8UEbwCg15csm21kFSbLhCwp966ccUd3LWIY 55 | XjxSNq/88Vn4Z1JfU0c94HB8cOL7zDrvvwx0stlXir733NVFIQKCAQEA3cUNegIw 56 | fEPyBAkwM321qvp3yqRCek1FbU+kzruNBrycqQhFHwA6Ck8fNA1eiHH9GHqULxEO 57 | G5MfEosRMqCX+354mDgMCk2JHO2uUICZs9gHT5pj0GEojXpC7HFz7BbWUhj4TXwr 58 | 8IN3Qh4+K72LzxTu/11bTNtArGp+bSxjXrlEbBIlqoBZ9vmZjqGALlsvW/CdnzqC 59 | ERIXXajb81ss3SO/MJhrx6B68sU80Gqm5g5rfXivtN9X8+beKrVrWjEx1kNNS0V5 60 | 035+rQVos8EoHX6iz9VmMjiMf0S06tAUvSzOPkgR4gXp2jckS9dZk0RwUwBCC02D 61 | r3RyfSxxR1XKdQKCAQEA87pB3eBLQGg2WcerLKandEnygYRnG6+Fj99x+b6eS5O+ 62 | 6l+lSmRTKj+GKAYW1D+w8eqhVT9z7Ifs236q9PxLva/dDkKJYO0OtHFJ53NCe2tB 63 | lMphqu8SCSZl1jQxQ0UlSuyUM5S79dEytLkwS4kt5qYB0yR1ZFSXI7zfQaLKZ/Rp 64 | AGoD5T936zkycQBnTar2oBQ8XP32LNMI9L5SYIx8ZCQ/ucMkfl7MaHxOIbSiTyMV 65 | R/erNhpdJL2ORf5ysLGZkmb1BirHS9ovEhgNZYj4AtykF0UGiNfC9VwnBFFrG2ls 66 | abZCm4cZoVsjABpa9ReLZS0iG+IibG+XmY1+uBJh3wKCAQBa7M3ntjoW2OzDRtki 67 | Y2o2nda7mLlA16mddcgGktLxbid1DlT4rukdDO+oMcsOel3gyXE0EvQLzjgxLB9y 68 | +HEXxfS/xEr7dmq/F5wemXtrRylIM+60owEzcGs78hArPfnFU0OK0Vxakiw1SZ0H 69 | 5gEKeHS88pPaYRKVHlyTel2Lmr446P/UdidsoU2aMxEQ8IXsVizp+d0WDqrR1cfI 70 | cRtl16At1nBqOpvuKXwTn4aqUEM2AGNZ7zBqab+xFwzav8zFInbwY53dXsGlQtB4 71 | 0rsVzLQILmBmOtUv4QWkOIgoP9SXqIjceLw2oeEZz0OEo8zB2xs48yEIsN+3/p67 72 | Nqt5AoIBAQDlB8U3i7sLRiK00VXAesbnF0okfVgrAxCed1nyVzcHTEpekgyQUKB6 73 | FgGqgLZZM5TCcDq1EhCMV9qzFF/wIVnHYYh4CvxvsbRcygypy3zQ36Rb/qYy679m 74 | C8gstxUH4uU9d/14Ty8luzVL8K46fSk+Egeq8xrBcmAovCaL1j8f2uQE+Jq6hZ7Z 75 | 0wDcgYWRzbM+EGX8+MWpr5I98s8UXU/TBuE/XepgOhMZqJ3/PHA9r3kjDNC94Z5f 76 | lSUqDwaVlf77PXbJGc/4LoqHFUUZgdGVVuN33mxakW5qBPPBMgVVWAcBe70xy43B 77 | PBQy15FbuYlLRVNFIoY4odCzAezvao6/AoIBAQDD5U+ghaGdp44N/zOd0AthVjAx 78 | oi4VMx6gLI78pNUT7IZHVUMn00878qv4VITscT3zY7EgdK+rNbg2R4+ZBRPO0tsC 79 | kGHl6bXiB+XwINVtoFs/PZdx+xTNcoZ/SULkcRC/XQmwTgJe23Nr5IpQBcKHX9co 80 | MWB0OY48H3Ur5vFnHSxjC3QwNMgOd7aLOyNu4Q7rbfM7GRBCQcbcKm58t0PkZKDO 81 | ZNyf3gSt6If1fqnN5dUukPYSpP1e1975BYmBxOZuY+jVwqPoGfwWSmo4ZcGrPWK9 82 | J4GydnVtTsMmp/zTN1cDG+1a6TourRQXclSVS9zR3NALJJnCgzCrKvm9L70/ 83 | -----END RSA PRIVATE KEY----- 84 | ` 85 | wrongRSAPrivKeyAuthPEM = `-----BEGIN WRONG RSA PRIVATE KEY----- 86 | MIICWwIBAAKBgQCm1J+Tvs8MNZiI2GiWqA/JDC2d/uGagUYSqIDgK7S74tbu8uqT 87 | KxpcdRWNAf92K0kU+W/CyWWA9uAIX1tTOsJtdutXbGMiFqKvI4YVKssYFkwgH3XE 88 | dc+UWHLZT7g+KKhuKyu/Yice89PLJmpVuosOEXwOZUwhkj9QI4q0IxKs3wIDAQAB 89 | AoGAHCjLfq64WAE76+1LShK4B2Fs2bxJ7EBhyYhzqGL4MLaLPO33tjuSSYThzFlH 90 | +3Q287leqexAm9IP4pnl2liStI2X0eQqZAfX6gd/QQ4Rr7zI9URcd8UPKykKO8Lm 91 | ghpDW+tuEV2A89/NUlcFKteLDYp1wCxCHNTAbY1R4QXVdYECQQDNlw4I/6RcSodX 92 | veAYIQy9eSeAzgAwchtpzz+/7xWG95OUaadyZsPDQp2dmbJPKSGJ0v1cetieQ3ji 93 | fb/qr7q/AkEAz7yfwW6v/M9vCqcxkik9I2VGiE5Xg11f7wX7eT0rwtfSPUpWPtgp 94 | L1YF3FLi58xCxPUkzDlyQ+NZaYQ4roo14QJAQHC+h3eJzxvVPF1ZpnaFhcY56Zeo 95 | W4cIrKu3cbPA7aMgcP6E68jmR4fT25hXWZSs3IRzwc8HouPHOkbsJuWaBQJAf2Yu 96 | k3JOe7y7XM0smXaxCAQUPYPOJ8IcE3qXvsLFE7lINk5glin7GAypi3VJst6SFDhD 97 | WPviF8BWFWABYwlgAQJAViX52BxO/KzLm+/QuTzVqKoqEZW+dqJx984TJug9Vy7h 98 | IEzY0Lcuq3pwJlQyyaNQxXF4orPp5Rzi5pNabuGJ8Q== 99 | -----END WRONG RSA PRIVATE KEY----- 100 | ` 101 | badRSAPrivKeyAuthPEM = `-----BEGIN RSA PRIVATE KEY----- 102 | MIICWwIBAAKBgQCm1J+Tvs8MNZiI2GiWqA/JDC2d/uGagUYSqIDgK7S74tbu8uqT 103 | dc+UWHLZT7g+KKhuKyu/Yice89PLJmpVuosOEXwOZUwhkj9QI4q0IxKs3wIDAQAB 104 | ghpDW+tuEV2A89/NUlcFKteLDYp1wCxCHNTAbY1R4QXVdYECQQDNlw4I/6RcSodX 105 | veAYIQy9eSeAzgAwchtpzz+/7xWG95OUaadyZsPDQp2dmbJPKSGJ0v1cetieQ3ji 106 | fb/qr7q/AkEAz7yfwW6v/M9vCqcxkik9I2VGiE5Xg11f7wX7eT0rwtfSPUpWPtgp 107 | L1YF3FLi58xCxPUkzDlyQ+NZaYQ4roo14QJAQHC+h3eJzxvVPF1ZpnaFhcY56Zeo 108 | W4cIrKu3cbPA7aMgcP6E68jmR4fT25hXWZSs3IRzwc8HouPHOkbsJuWaBQJAf2Yu 109 | k3JOe7y7XM0smXaxCAQUPYPOJ8IcE3qXvsLFE7lINk5glin7GAypi3VJst6SFDhD 110 | IEzY0Lcuq3pwJlQyyaNQxXF4orPp5Rzi5pNabuGJ8Q== 111 | -----END RSA PRIVATE KEY----- 112 | ` 113 | // openssl pkey -in out/CA.key -out out/CA.key.encrypted -des3 -traditional 114 | rsaEncryptedPrivKeyAuthPEM = `-----BEGIN RSA PRIVATE KEY----- 115 | Proc-Type: 4,ENCRYPTED 116 | DEK-Info: DES-EDE3-CBC,F1EF634443F5EE07 117 | 118 | wkmIgevYnThGVSDD0eCLti3FXziditA5xxg1TnA10XGpk0wTA+bZv9aQ70v0r8e+ 119 | TsX+6HGLSkifu0GMcDqZsgY1Hq060Sy1JuqizQnk4MGnP4vyHrRAOqKFj+AC03BR 120 | HK1S6P6MwjDVt9A+D5A5Jo0XZtt8NsxAitE4DdxU5lF4D31Ae6SEQD/ceXLuky6o 121 | 2S+IZjwWzVlLGY+jaWosz1I8asCXNDGBN0IzFVJds6J8G7iViSly+RmA+VgPfSyW 122 | 9lK36zHbnI3OmTlBeb3xkcmJGw+i4a7qJADv268Xo6mB3WELCBf+yFo1wB7C/Lqx 123 | UDhANn8n2N/EynsbqST/lUe9mDCqipW+kABjaZIEsP3er9xAfnaO3BZY+jpMGETK 124 | 9NAHy4iEirQGzYBZHCetK/gJ10fsBSHrzMs7rBmnpaiolINlPNfhv2T53AzJ+l6p 125 | WpTWxuJSR6EirmLuW5n/9JraTDX9knkAG+SqYW3MzUWhvAu496FdjMt0rQnCHoH7 126 | HVN8HBxW5PGtSDvYDkUZvot23XABlhBQjwCK/7biO3FeO6lBK8UoZjOYVfdNwgDC 127 | JfUwoZQqQ/4PK34UyW0aCKPNP4AXqZzGyc4oYkdqbHoF+Dv0c+i354WSZ75Hauf7 128 | NzF50pJKhUnXP9qMC5dlpMrVedQrQoR59/P3Al8tKDxijTvktk2vACzon33v0D82 129 | 7kby9R/gl9GGPEJHrbqje68kWsWutaVWCoIvc3lVwXUxNhvXJPJ1ONAuz2l8AEN0 130 | L9amcAco2ERdPc1L4kwL/MlT7yY7HVTIOxx+vEHxlvfR6IwDgG6OpnVvJaUtaQ8e 131 | oAUtWZVtA05SLA5mqwMpkC4LxljtlYgmSv2eWJOwFdpiz3Em/qGJ/vtyLT8aqzgY 132 | zpN5oVczR3ocDdId+wtI6Gq/l2Fy7Ao4sW8SdthSmhmTDf6UHwO4g8kASUi5f5ze 133 | AL2oTJFGIdQQc0x/X8sdGYW9Tb/x6jwtFiyvNOlgDzMGJN50d9phdUTauB2ME2MS 134 | kbJYVFj995HQUucJnKSeOaBv6b6bYzypSn9YRCd6HIUYidpTI2sHhgUw3cLt461E 135 | K2yuj10DdEJcmFYP94ZzFXgUAp+xpcNn+Z87rZ4Z0M4p0WR63/zpoTOYFHo+2HKs 136 | GKuYMa68o8dUJyr/Koq7mTwO/CkJhLR7xAwSOqSZgyzEgSuzxUjkcImOYdOiDJav 137 | xf+cL13W2Tb5Y6UPIqSy9XkmMwzQSS344/V5vMn4vnMFEvz9lRTE8JKZNcijOLww 138 | T5HiGlYCjDVU42DgvISllUrIVVs+JCSIoscNzEUN8mgSYFlIRfhfhBTAssuER5EP 139 | f91Enrl4TZXcgTF/+iZjoxo9fQzF0vA01Vfrm9Z8Jv6VRW6xO3LvP3xMfnIvyMea 140 | 6aMT/Q2NyPAEeFyWflH+sCAa9vTO3OPbx+9yIIvB90wOol28wcfCvxuBg5+afMJV 141 | aF5K0vh4Clg5T7Ux38NwSHQdQuMWVIxswN0pMEUM1wKvZzG1RRa1QTL/2IfAiYp/ 142 | RmT3ad3v3YevspfAWYRTdELaruA0NWrWtVPN4yW40pY9PND/qDggxmcyVcaSkFMp 143 | p/6O/izYP0U10L170Cj8FQQP9AFf9O/Nff/LZaj/wJV9UoN2wyoE17xZDRRAS+ET 144 | 2ljuKxTECTjzgS12Zg4Rl6ZVniuGRQCpjfFoOpX+LKbBuoWzqOj/iLOW1iysd2AX 145 | hLpBZP9d7PUG5LvkKRLXdA4C4sE18uFmqMG0yOHctOyOmbVINuSAUwHrDGO2Okld 146 | MysgfPBg3xf5BGoTFV+SNWARrOq0YCPT/nLXxnkWjpireMciqdFGDDUAHKLk9CKk 147 | +5A4986tahkK39sfB0Ce8wD1dTAimQdhLBu1Yf8iIN6VNptk3QD0RE7Bs7+TCFZF 148 | MM/ranfmXUc0ijO4VkFjQEnlv/pxgWaldWtBopMi5CeibaBj3snBUSuTnUGsQbsb 149 | j72hd9zX0NJwQDHeL3RW99YHYlRZ34UgIoxPyUVR+SbMEEZi/dDN1tvsk72cxgzm 150 | J3EveTH/1oCIakIPwwC3nT2GCGsITvqTHwDL9fki019zTz34SsfsVkJmF+wOZZvr 151 | SeQJbC+lu4kA373CAsUvcn8wl+njNHL1o9eG48HxiCGc5EGXpcQKbU9a7LEGlejS 152 | j069od1fD6wihh/OKy1Fvc8Ow4qkQnxlSk1ALaGwB14SMLFVZGd9KINftougHiWG 153 | nAo86PGZSz6pvSz2OLm9U/aI3Qzzpt7R9h6iuvqejYSf9zoNW3Eixa9zbbDM4gZc 154 | xcZUMAsAWAYQGgXq8HuMzQdpHTsjILWvAIRgXVNXuMA9vP2FxvkQNsOBUaiHfblg 155 | SpEw51UZmlxSO1miwG8lcn3aPJBrSFDZu0Yk6s915OaqrVBY0HGR+POtam0Z5CQc 156 | WhCTD9RJqfgkSReVDDuPu+Td+M2ENLm7OYHVhveBDlX7igawZsXDlHDKN+7drC2b 157 | QiP2fm1PA3AgeNkz38terNYaDuz1oHovRjgD/0BIH50B9TsptaaRjDP+7sUrmBgi 158 | O66I/Dzi3WiDyB33spcMYZJvyA68W/QLY390SRKGjHYq1wyLwaZtIwbxULC7617q 159 | cvdZAu7MCSvuUKWrfJ5OmSU03mCRMJjsATYOpqTv5Aqyx+l29yIksosumk9CEv0u 160 | HRCD4lRT9mzDeAEaUaV4g6O6OUbDiLvCnIOfEWgQWqCFPyYIydGWQsywUCEUOybD 161 | iAgLA4IPsGHV2/TUB+hM8iTwlIPtnALTvqTsF1j8k8T2LvzoNT3PBGEZ3An36f3i 162 | H7z0G0QoiJDMXEEcetnxc6+r6U2z+v+4AuV3pYheE/oaV3FHlC+nRnJmbtASIWNs 163 | cM9wR3LDU6bdH6tLujlABT3S6xkFOtZwsCwIaTIySyfmt0N5XubcvjU9wbzFlq96 164 | 2JtNO20dCoQwNGeA4iDrFRgaBOPjNdYQ7EolEcovUpQyCVwGI2SWLXMICof65E65 165 | nct8oJmlkOaehgin/oevxheLYsq+FCnFKYRDqOZiUUHIeEflAS57D8p8sd/cQAK+ 166 | u/j6/AWFBqWYtt/fNtabA2FtOCaXWSznZ+xYuXC8gZyhn+Z12D/fEHoFDbwfEWhc 167 | -----END RSA PRIVATE KEY----- 168 | ` 169 | p256PrivKeyPEM = `-----BEGIN PRIVATE KEY----- 170 | MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgxMg/54VIbzxz212o 171 | g9ud2XLplNaPqi1jqadVuFXPxMOhRANCAARiNZ2xp1llazZyUjvjnsvRknFbUI9r 172 | f8I43034ustdwkvgh+7TfZGeEHNmOwQi9RInxIAReFx2gUARYn2f1qnj 173 | -----END PRIVATE KEY----- 174 | ` 175 | // Encrypted PKCS8 generated with openssl to ensure comaptibility. 176 | p256EncryptedPrivKeyPEM = `-----BEGIN ENCRYPTED PRIVATE KEY----- 177 | MIHeMEkGCSqGSIb3DQEFDTA8MBsGCSqGSIb3DQEFDDAOBAgQo5EVNe/KkgICCAAw 178 | HQYJYIZIAWUDBAEqBBBYCWWlK2TXfGNAf/dNVsUdBIGQP5aHz3sKokKkrLLKrxNv 179 | t/HFn59QJPaagQm1IlK0LfwV6RMrKuJjAJ+bSFZit9/2iCLQw9yekyNbzcAuhihz 180 | 4CCMvvHCigtATIby3Bv/OhnHnpcsF4HVkXd1h0rKaV0xDK6xWYZ6d7KS2NJtaJAF 181 | DBi43A9dlURMcw3/CYJ3jgzJGAP8nCm4omyNckloBaAa 182 | -----END ENCRYPTED PRIVATE KEY----- 183 | ` 184 | ed25519PrivKeyPEM = `-----BEGIN PRIVATE KEY----- 185 | MC4CAQAwBQYDK2VwBCIEINhpLppsFdDXk0P4mMq5kBHntPJGaaG27C1ZZYJCoWL3 186 | -----END PRIVATE KEY----- 187 | ` 188 | // Encrypted PKCS8 generated with openssl to ensure comaptibility. 189 | ed25519EncryptedPrivKeyPEM = `-----BEGIN ENCRYPTED PRIVATE KEY----- 190 | MIGbMFcGCSqGSIb3DQEFDTBKMCkGCSqGSIb3DQEFDDAcBAgaJGiyG0Hd8wICCAAw 191 | DAYIKoZIhvcNAgkFADAdBglghkgBZQMEASoEEAKxHGl8qfK3DM9FKMWQAHUEQKu6 192 | TL8589WqvDu8nE8ZrFwibbR9eMloekr89lqs1vJd7KJngUtXbW3XL2dtdSXzCGYF 193 | T4rZH6EqxXGdvvmo1pw= 194 | -----END ENCRYPTED PRIVATE KEY----- 195 | ` 196 | password = "123456" 197 | wrongPassword = "654321" 198 | rsaBits = 1024 199 | 200 | // openssl rsa -in out/CA.key -pubout | openssl asn1parse -strparse 19 -noout -out - | openssl dgst -binary -sha1 | openssl base64 201 | subjectKeyIDOfRSAPubKeyAuthBASE64 = "gxM5OZlNggo5iiPZjXvGbJUqLQU=" 202 | ) 203 | 204 | func TestCreateRSAKey(t *testing.T) { 205 | key, err := CreateRSAKey(rsaBits) 206 | if err != nil { 207 | t.Fatal("Failed creating rsa key:", err) 208 | } 209 | 210 | if err = key.Private.(*rsa.PrivateKey).Validate(); err != nil { 211 | t.Fatal("Failed to validate private key") 212 | } 213 | } 214 | 215 | func TestRSAKey(t *testing.T) { 216 | key, err := NewKeyFromPrivateKeyPEM([]byte(rsaPrivKeyAuthPEM)) 217 | if err != nil { 218 | t.Fatal("Failed parsing RSA private key:", err) 219 | } 220 | 221 | if err = key.Private.(*rsa.PrivateKey).Validate(); err != nil { 222 | t.Fatal("Failed validating RSA private key:", err) 223 | } 224 | } 225 | 226 | func TestWrongRSAKey(t *testing.T) { 227 | key, err := NewKeyFromPrivateKeyPEM([]byte("..")) 228 | if key != nil || err == nil { 229 | t.Fatal("Expect not to parse RSA private key:", err) 230 | } 231 | 232 | key, err = NewKeyFromPrivateKeyPEM([]byte(wrongRSAPrivKeyAuthPEM)) 233 | if key != nil || err == nil { 234 | t.Fatal("Expect not to parse RSA private key:", err) 235 | } 236 | } 237 | 238 | func TestBadRSAKey(t *testing.T) { 239 | key, err := NewKeyFromPrivateKeyPEM([]byte(badRSAPrivKeyAuthPEM)) 240 | if key != nil || err == nil { 241 | t.Fatal("Expect not to parse bad RSA private key:", err) 242 | } 243 | } 244 | 245 | // TestRSAKeyExport tests the ability to convert rsa key into PEM bytes 246 | func TestRSAKeyExport(t *testing.T) { 247 | key, err := NewKeyFromPrivateKeyPEM([]byte(rsaPrivKeyAuthPEM)) 248 | if err != nil { 249 | t.Fatal("Failed to parse certificate from PEM:", err) 250 | } 251 | 252 | pemBytes, err := key.ExportPrivate() 253 | if err != nil { 254 | t.Fatal("Failed exporting PEM-format bytes:", err) 255 | } 256 | if !bytes.Equal(pemBytes, []byte(rsaPrivKeyAuthPEM)) { 257 | t.Fatal("Failed exporting the same PEM-format bytes") 258 | } 259 | } 260 | 261 | // TestRSAKeyExportEncrypted tests the ability to convert rsa key into encrypted PEM bytes 262 | func TestRSAKeyExportEncrypted(t *testing.T) { 263 | key, err := NewKeyFromEncryptedPrivateKeyPEM([]byte(rsaEncryptedPrivKeyAuthPEM), []byte(password)) 264 | if err != nil { 265 | t.Fatal("Failed to parse certificate from PEM:", err) 266 | } 267 | 268 | pemBytes, err := key.ExportPrivate() 269 | if err != nil { 270 | t.Fatal("Failed exporting PEM-format bytes:", err) 271 | } 272 | if !bytes.Equal(pemBytes, []byte(rsaPrivKeyAuthPEM)) { 273 | t.Fatal("Failed exporting the same PEM-format bytes") 274 | } 275 | 276 | pemBytes, err = key.ExportEncryptedPrivate([]byte(password)) 277 | if err != nil { 278 | t.Fatal("Failed exporting PEM-format bytes:", err) 279 | } 280 | 281 | if _, err := NewKeyFromEncryptedPrivateKeyPEM(pemBytes, []byte(wrongPassword)); err == nil { 282 | t.Fatal("Expect not parsing certificate from PEM:", err) 283 | } 284 | } 285 | 286 | func TestRSAKeyGenerateSubjectKeyID(t *testing.T) { 287 | key, err := NewKeyFromPrivateKeyPEM([]byte(rsaPrivKeyAuthPEM)) 288 | if err != nil { 289 | t.Fatal("Failed parsing RSA private key:", err) 290 | } 291 | 292 | id, err := GenerateSubjectKeyID(key.Public) 293 | if err != nil { 294 | t.Fatal("Failed generating SubjectKeyId:", err) 295 | } 296 | correctID, _ := base64.StdEncoding.DecodeString(subjectKeyIDOfRSAPubKeyAuthBASE64) 297 | if !bytes.Equal(id, correctID) { 298 | t.Fatal("Failed generating correct SubjectKeyId") 299 | } 300 | } 301 | 302 | func TestCreateECDSAKey(t *testing.T) { 303 | key, err := CreateECDSAKey(elliptic.P256()) 304 | if err != nil { 305 | t.Fatalf("CreateECDSAKey(P256) failed: %v", err) 306 | } 307 | if _, ok := key.Private.(*ecdsa.PrivateKey); !ok { 308 | t.Fatal("CreateECDSAKey did not contain an ecdsa.PrivateKey") 309 | } 310 | } 311 | 312 | func TestCreateEd25519Key(t *testing.T) { 313 | key, err := CreateEd25519Key() 314 | if err != nil { 315 | t.Fatalf("CreateEd25519Key failed: %v", err) 316 | } 317 | if _, ok := key.Private.(ed25519.PrivateKey); !ok { 318 | t.Fatal("CreateEd25519Key did not contain an ed25519.PrivateKey") 319 | } 320 | } 321 | 322 | func TestECCExportImport(t *testing.T) { 323 | tests := []struct { 324 | desc string 325 | key *Key 326 | }{{ 327 | desc: "ECDSA P256", 328 | key: func() *Key { 329 | key, err := CreateECDSAKey(elliptic.P256()) 330 | if err != nil { 331 | t.Fatalf("CreateECDSAKey(P256) failed: %v", err) 332 | } 333 | return key 334 | }(), 335 | }, { 336 | desc: "Ed25519", 337 | key: func() *Key { 338 | key, err := CreateEd25519Key() 339 | if err != nil { 340 | t.Fatalf("CreateEd25519Key() failed: %v", err) 341 | } 342 | return key 343 | }(), 344 | }} 345 | for _, tc := range tests { 346 | t.Run(tc.desc, func(t *testing.T) { 347 | // Use the SKID to be sure that the key is the same post-import. 348 | before, err := GenerateSubjectKeyID(tc.key.Public) 349 | if err != nil { 350 | t.Fatalf("GenerateSubjectKeyID failed: %v", err) 351 | } 352 | pem, err := tc.key.ExportPrivate() 353 | if err != nil { 354 | t.Fatalf("ExportPrivate failed: %v", err) 355 | } 356 | key, err := NewKeyFromPrivateKeyPEM(pem) 357 | if err != nil { 358 | t.Fatalf("NewKeyFromPrivateKeyPEM failed: %v", err) 359 | } 360 | after, err := GenerateSubjectKeyID(key.Public) 361 | if err != nil { 362 | t.Fatalf("GenerateSubjectKeyID failed: %v", err) 363 | } 364 | if !bytes.Equal(before, after) { 365 | t.Fatalf("SKID before export (%s) does not match SKID after export/import (%s)", hex.EncodeToString(before), hex.EncodeToString(after)) 366 | } 367 | }) 368 | } 369 | } 370 | 371 | func TestEncryptedECCImportExport(t *testing.T) { 372 | tests := []struct { 373 | name string 374 | pem string 375 | encryptedPEM string 376 | }{{ 377 | name: "ECDSA P256", 378 | pem: p256PrivKeyPEM, 379 | encryptedPEM: p256EncryptedPrivKeyPEM, 380 | }, { 381 | name: "Ed25519", 382 | pem: ed25519PrivKeyPEM, 383 | encryptedPEM: ed25519EncryptedPrivKeyPEM, 384 | }} 385 | for _, tc := range tests { 386 | t.Run(tc.name, func(t *testing.T) { 387 | // 1. Decrypt the openssl-encrypted PKCS8. 388 | priv, err := NewKeyFromEncryptedPrivateKeyPEM([]byte(tc.encryptedPEM), []byte(password)) 389 | if err != nil { 390 | t.Fatalf("NewKeyFromEncryptedPrivateKeyPEM failed: %v", err) 391 | } 392 | // 2. Export the decrypted PEM to compare with the expected PEM. 393 | pem, err := priv.ExportPrivate() 394 | if err != nil { 395 | t.Fatalf("ExportPrivate failed: %v", err) 396 | } 397 | if tc.pem != string(pem) { 398 | t.Fatalf("Want pem:\n%sgot:\n%s", tc.pem, pem) 399 | } 400 | // 3. Ensure that Decrypt(Encrypt(pem)) == pem. 401 | encryptedPEM, err := priv.ExportEncryptedPrivate([]byte(password)) 402 | if err != nil { 403 | t.Fatalf("ExportEncryptedPrivate failed: %v", err) 404 | } 405 | priv, err = NewKeyFromEncryptedPrivateKeyPEM([]byte(encryptedPEM), []byte(password)) 406 | if err != nil { 407 | t.Fatalf("NewKeyFromEncryptedPrivateKeyPEM failed: %v", err) 408 | } 409 | decryptedPEM, err := priv.ExportPrivate() 410 | if err != nil { 411 | t.Fatalf("ExportPrivate failed: %v", err) 412 | } 413 | if !bytes.Equal(pem, decryptedPEM) { 414 | t.Fatalf("Want pem:\n%sgot:\n%s", pem, decryptedPEM) 415 | } 416 | // 4. Sanity check to ensure that the wrong password fails to decrypt... 417 | if _, err := NewKeyFromEncryptedPrivateKeyPEM([]byte(encryptedPEM), []byte(wrongPassword)); err == nil { 418 | t.Fatalf("NewKeyFromEncryptedPrivateKeyPEM(wrongPassword) succeeeded, expected failure") 419 | } 420 | }) 421 | } 422 | } 423 | --------------------------------------------------------------------------------