├── MINOR ├── VERSION ├── .gitignore ├── libpkcs11-aws-kms.darwin-arm64.so ├── version.sh ├── libpkcs11-aws-kms.darwin-arm64.so.md ├── SECURITY.md ├── usage.go ├── Makefile ├── .github ├── dependabot.yml └── workflows │ └── main.yml ├── go.mod ├── .dockerignore ├── config.json.sample ├── Dockerfile ├── LICENSE ├── release.sh ├── logger.go ├── go.sum ├── pkcs11.go ├── usage.txt ├── db.go ├── awsauth.go ├── cloudformation └── certsquirt.yaml ├── README.md ├── x509.go └── main.go /MINOR: -------------------------------------------------------------------------------- 1 | 73 2 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 1.0.73 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pem 2 | *.crt 3 | *.csr 4 | *.key 5 | certsquirt* -------------------------------------------------------------------------------- /libpkcs11-aws-kms.darwin-arm64.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PortSwigger/certsquirt/main/libpkcs11-aws-kms.darwin-arm64.so -------------------------------------------------------------------------------- /version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | MINOR=`cat MINOR` 3 | NEWMINOR=`expr ${MINOR} + 1` 4 | export VERSION=1.0.${NEWMINOR} 5 | echo $VERSION > VERSION 6 | echo $NEWMINOR > MINOR 7 | -------------------------------------------------------------------------------- /libpkcs11-aws-kms.darwin-arm64.so.md: -------------------------------------------------------------------------------- 1 | This is a pre-built macos lib, from https://github.com/JackOfMostTrades/aws-kms-pkcs11 2 | 3 | I built this from source. 4 | 5 | S. 6 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Current. 6 | 7 | ## Reporting a Vulnerability 8 | 9 | Raise a PR please and assign security tag. 10 | -------------------------------------------------------------------------------- /usage.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | _ "embed" 7 | ) 8 | 9 | //go:embed usage.txt 10 | var instructions string 11 | 12 | func usage() { 13 | fmt.Printf("%v", instructions) 14 | } 15 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: 2 | go build -v -ldflags "-X main.buildstamp=`date -u '+%Y-%m-%d_%I:%M:%S%p'` -X main.githash=`git rev-parse HEAD`" || exit 3 | 4 | clean: 5 | rm -- certsquirt 6 | 7 | release: 8 | ./version.sh 9 | ./release.sh 10 | 11 | realclean: 12 | rm -f *.crt *.pem *.key 13 | rm -f *.tar.gz 14 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "gomod" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/PortSwigger/certsquirt 2 | 3 | go 1.24.0 4 | 5 | toolchain go1.24.1 6 | 7 | require ( 8 | github.com/ThalesGroup/crypto11 v1.4.1 9 | github.com/aws/aws-sdk-go v1.55.8 10 | github.com/pkg/errors v0.9.1 11 | github.com/pquerna/otp v1.5.0 12 | github.com/sethvargo/go-password v0.3.1 13 | golang.org/x/term v0.38.0 14 | ) 15 | 16 | require ( 17 | github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect 18 | github.com/jmespath/go-jmespath v0.4.0 // indirect 19 | github.com/miekg/pkcs11 v1.1.1 // indirect 20 | github.com/thales-e-security/pool v0.0.2 // indirect 21 | golang.org/x/sys v0.39.0 // indirect 22 | ) 23 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # Git and version control 2 | .git/ 3 | .gitignore 4 | .github/ 5 | 6 | # Development and build artifacts 7 | *.key 8 | *.pem 9 | *.crt 10 | *.csr 11 | *.p12 12 | *.pkcs12 13 | *.tar.gz 14 | certsquirt-* 15 | !certsquirt 16 | 17 | # Documentation and examples 18 | README.md 19 | SECURITY.md 20 | *.md 21 | usage.txt 22 | 23 | # Test and development files 24 | test-config.json 25 | **/test* 26 | 27 | # IDE and editor files 28 | .vscode/ 29 | .idea/ 30 | *.swp 31 | *.swo 32 | *~ 33 | 34 | # OS generated files 35 | .DS_Store 36 | .DS_Store? 37 | ._* 38 | .Spotlight-V100 39 | .Trashes 40 | ehthumbs.db 41 | Thumbs.db 42 | 43 | # Build directories and caches 44 | node_modules/ 45 | vendor/ 46 | target/ 47 | build/ 48 | dist/ 49 | 50 | # Temporary files 51 | tmp/ 52 | temp/ 53 | *.tmp 54 | *.temp 55 | 56 | # Config files that shouldn't be in container 57 | config.json 58 | !config.json.sample -------------------------------------------------------------------------------- /config.json.sample: -------------------------------------------------------------------------------- 1 | { 2 | "Organisation": "PortSwigger", 3 | "Country": "UK", 4 | "CaName": "PortSwigger CA", 5 | "CaVersion": "2023", 6 | "CaAiaUrl": "", 7 | "CaAiaRootUrl": "", 8 | "CaAiaIssuerUrl": "", 9 | "OrgUnit": "SecEng", 10 | "City": "Knutsford", 11 | "County": "Cheshire", 12 | "SigningCert": "your_root_ca_cert.pem", 13 | "OCSPServer": "", 14 | "AwsRoleARN": "arn:aws:iam::{AWS_ACCOUNT_ID}:role/{SOMEROLE}", 15 | "AwsMfaSerial": "arn:aws:iam::{AWS_ACCOUNT_ID}:mfa/{SOMEMFA}", 16 | "AwsDbTableName": "{SOME_DYNAMO_DB_TABLE}", 17 | "AwsRegion": "eu-west-1", 18 | "P11Path": "/usr/lib64/pkcs11/aws_kms_pkcs11.so", 19 | "P11TokenLabel": "your-testing-rsa-key-as-in-kms-console", 20 | "P11Pin": "", 21 | "P11Slot": 0, 22 | "LogLevel": "INFO", 23 | "LogFormat": "text", 24 | "AuditLogFile": "/var/log/certsquirt/audit.log" 25 | } 26 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:12-slim 2 | 3 | # Noninteractive to keep apt quiet in CI 4 | ENV DEBIAN_FRONTEND=noninteractive 5 | 6 | # Install runtime dependencies only (no -dev packages) 7 | # Use BuildKit cache mounts for speedy rebuilds 8 | RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ 9 | --mount=type=cache,target=/var/lib/apt,sharing=locked \ 10 | set -eux; \ 11 | apt-get update; \ 12 | apt-get install --no-install-recommends -y \ 13 | ca-certificates \ 14 | libc6 \ 15 | libc-bin \ 16 | libgcc-s1 \ 17 | libstdc++6 \ 18 | libjson-c5 \ 19 | libp11-kit0 \ 20 | libcurl4 \ 21 | libssl3 \ 22 | jq \ 23 | file \ 24 | ; \ 25 | rm -rf /var/lib/apt/lists/* 26 | 27 | # Add required libs for pkcs11 provider in separate layer for better caching 28 | # ignore symlinks. 29 | # Note: The build process will copy the appropriate architecture libs 30 | COPY vcpkg/installed/*/lib/ /usr/local/lib/ 31 | 32 | # Install aws kms (depends on aws-sdk-cpp libs) 33 | COPY aws-kms-pkcs11/aws_kms_pkcs11.so /usr/local/lib/ 34 | 35 | # Create required symlinks for above libs 36 | RUN /usr/sbin/ldconfig 37 | 38 | # Bootstrap the aws-kms provider (separate layer for config) 39 | RUN mkdir -p /etc/aws-kms-pkcs11/ && ln -s /depot/aws-kms-config.json /etc/aws-kms-pkcs11/config.json 40 | 41 | # Copy application binary (this should be the most frequently changing layer) 42 | COPY certsquirt/certsquirt /usr/local/bin/certsquirt 43 | 44 | # Set up working directory 45 | WORKDIR /depot 46 | 47 | ENTRYPOINT ["/usr/local/bin/certsquirt"] 48 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2023, PortSwigger 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | 3. Neither the name of the copyright holder nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | export VERSION=`cat VERSION` 2 | git archive --prefix=certsquirt-${VERSION}/ --format=tar.gz --output=certsquirt-${VERSION}.tar.gz main 3 | 4 | # crypto11/pkcs11 has issues cross compiling. This is expected to fail. 5 | CGO_ENABLED=1 GOOS=darwin GOARCH=arm64 go build -a -ldflags "-X main.buildstamp=`date -u '+%Y-%m-%d_%I:%M:%S%p'` -X main.githash=`git rev-parse HEAD`" -o certsquirt-${VERSION}-macos-arm64 6 | CGO_ENABLED=1 GOOS=darwin GOARCH=amd64 go build -a -ldflags "-X main.buildstamp=`date -u '+%Y-%m-%d_%I:%M:%S%p'` -X main.githash=`git rev-parse HEAD`" -o certsquirt-${VERSION}-macos-amd64 7 | CGO_ENABLED=1 GOOS=windows GOARCH=amd64 go build -a -ldflags "-X main.buildstamp=`date -u '+%Y-%m-%d_%I:%M:%S%p'` -X main.githash=`git rev-parse HEAD`" -o certsquirt-${VERSION}-windows-amd64.exe 8 | CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -a -ldflags "-X main.buildstamp=`date -u '+%Y-%m-%d_%I:%M:%S%p'` -X main.githash=`git rev-parse HEAD`" -o certsquirt-${VERSION}-linux-amd64 9 | CGO_ENABLED=1 GOOS=linux GOARCH=arm64 go build -a -ldflags "-X main.buildstamp=`date -u '+%Y-%m-%d_%I:%M:%S%p'` -X main.githash=`git rev-parse HEAD`" -o certsquirt-${VERSION}-linux-arm64 10 | CGO_ENABLED=1 GOOS=freebsd GOARCH=amd64 go build -a -ldflags "-X main.buildstamp=`date -u '+%Y-%m-%d_%I:%M:%S%p'` -X main.githash=`git rev-parse HEAD`" -o certsquirt-${VERSION}-freebsd-amd64 11 | CGO_ENABLED=1 GOOS=freebsd GOARCH=arm64 go build -a -ldflags "-X main.buildstamp=`date -u '+%Y-%m-%d_%I:%M:%S%p'` -X main.githash=`git rev-parse HEAD`" -o certsquirt-${VERSION}-freebsd-arm64 12 | CGO_ENABLED=1 GOOS=openbsd GOARCH=amd64 go build -a -ldflags "-X main.buildstamp=`date -u '+%Y-%m-%d_%I:%M:%S%p'` -X main.githash=`git rev-parse HEAD`" -o certsquirt-${VERSION}-openbsd-amd64 13 | CGO_ENABLED=1 GOOS=openbsd GOARCH=arm64 go build -a -ldflags "-X main.buildstamp=`date -u '+%Y-%m-%d_%I:%M:%S%p'` -X main.githash=`git rev-parse HEAD`" -o certsquirt-${VERSION}-openbsd-arm64 14 | -------------------------------------------------------------------------------- /logger.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log/slog" 5 | "os" 6 | ) 7 | 8 | var ( 9 | logger *slog.Logger 10 | auditLogger *slog.Logger 11 | ) 12 | 13 | // LogConfig holds logging configuration 14 | type LogConfig struct { 15 | Level string // "DEBUG", "INFO", "WARN", "ERROR" 16 | Format string // "json" or "text" 17 | AuditFile string // path to audit log file, empty for stdout 18 | } 19 | 20 | // InitLogging initializes the structured logger based on configuration 21 | func InitLogging(cfg LogConfig, debug bool) { 22 | var level slog.Level 23 | switch cfg.Level { 24 | case "DEBUG": 25 | level = slog.LevelDebug 26 | case "INFO": 27 | level = slog.LevelInfo 28 | case "WARN": 29 | level = slog.LevelWarn 30 | case "ERROR": 31 | level = slog.LevelError 32 | default: 33 | if debug { 34 | level = slog.LevelDebug 35 | } else { 36 | level = slog.LevelInfo 37 | } 38 | } 39 | 40 | var handler slog.Handler 41 | if cfg.Format == "json" { 42 | handler = slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ 43 | Level: level, 44 | AddSource: true, 45 | }) 46 | } else { 47 | handler = slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ 48 | Level: level, 49 | AddSource: true, 50 | }) 51 | } 52 | 53 | logger = slog.New(handler) 54 | slog.SetDefault(logger) 55 | 56 | // Set up audit logger 57 | var auditHandler slog.Handler 58 | var auditOutput *os.File = os.Stdout 59 | 60 | if cfg.AuditFile != "" { 61 | var err error 62 | auditOutput, err = os.OpenFile(cfg.AuditFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600) 63 | if err != nil { 64 | logger.Error("Failed to open audit log file, using stdout", "error", err, "file", cfg.AuditFile) 65 | auditOutput = os.Stdout 66 | } 67 | } 68 | 69 | auditHandler = slog.NewJSONHandler(auditOutput, &slog.HandlerOptions{ 70 | Level: slog.LevelInfo, 71 | AddSource: true, 72 | }) 73 | auditLogger = slog.New(auditHandler) 74 | } 75 | 76 | // GetLogger returns the main application logger 77 | func GetLogger() *slog.Logger { 78 | if logger == nil { 79 | // Fallback initialization with default settings 80 | InitLogging(LogConfig{Level: "INFO", Format: "text"}, false) 81 | } 82 | return logger 83 | } 84 | 85 | // GetAuditLogger returns the audit logger for security events 86 | func GetAuditLogger() *slog.Logger { 87 | if auditLogger == nil { 88 | // Fallback initialization 89 | InitLogging(LogConfig{Level: "INFO", Format: "text"}, false) 90 | } 91 | return auditLogger 92 | } 93 | 94 | // AuditEvent logs a security audit event with consistent structure 95 | func AuditEvent(operation string, success bool, details ...any) { 96 | auditLogger.Info("AUDIT", 97 | append([]any{ 98 | "operation", operation, 99 | "success", success, 100 | "timestamp", "auto", // slog adds timestamp automatically 101 | }, details...)...) 102 | } -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/ThalesGroup/crypto11 v1.4.1 h1:6YR6aVL8LI8akReXKTEgxf+k0+b8wlV8Ra7tZnCG9y4= 2 | github.com/ThalesGroup/crypto11 v1.4.1/go.mod h1:vggvBwlVrqePDrooq/B32dMXlfEsdsFY+6YlSD7VOy0= 3 | github.com/aws/aws-sdk-go v1.55.8 h1:JRmEUbU52aJQZ2AjX4q4Wu7t4uZjOu71uyNmaWlUkJQ= 4 | github.com/aws/aws-sdk-go v1.55.8/go.mod h1:ZkViS9AqA6otK+JBBNH2++sx1sgxrPKcSzPPvQkUtXk= 5 | github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI= 6 | github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= 7 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 8 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= 10 | github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= 11 | github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= 12 | github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= 13 | github.com/miekg/pkcs11 v1.1.1 h1:Ugu9pdy6vAYku5DEpVWVFPYnzV+bxB+iRdbuFSu7TvU= 14 | github.com/miekg/pkcs11 v1.1.1/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= 15 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 16 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 17 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 18 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 19 | github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs= 20 | github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg= 21 | github.com/sethvargo/go-password v0.3.1 h1:WqrLTjo7X6AcVYfC6R7GtSyuUQR9hGyAj/f1PYQZCJU= 22 | github.com/sethvargo/go-password v0.3.1/go.mod h1:rXofC1zT54N7R8K/h1WDUdkf9BOx5OptoxrMBcrXzvs= 23 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 24 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 25 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 26 | github.com/thales-e-security/pool v0.0.2 h1:RAPs4q2EbWsTit6tpzuvTFlgFRJ3S8Evf5gtvVDbmPg= 27 | github.com/thales-e-security/pool v0.0.2/go.mod h1:qtpMm2+thHtqhLzTwgDBj/OuNnMpupY8mv0Phz0gjhU= 28 | golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= 29 | golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 30 | golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= 31 | golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= 32 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 33 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= 34 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 35 | -------------------------------------------------------------------------------- /pkcs11.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/rsa" 5 | "errors" 6 | "fmt" 7 | 8 | "github.com/ThalesGroup/crypto11" 9 | ) 10 | 11 | func initPkcs11(pubkey *rsa.PublicKey) (signer crypto11.Signer, err error) { 12 | logger := GetLogger() 13 | if flDebug { 14 | logger.Debug("PKCS11 initialization", "pubkey_type", fmt.Sprintf("%T", pubkey)) 15 | } 16 | // depending on the pkcs11 provider we need to pass these in differently. 17 | // For KMS and some others , we need to find the key via the Token Label, 18 | // but the YubiKey and others, we need to locate the token via the slot. 19 | // 20 | // crypto11.Config 21 | var p11Config crypto11.Config 22 | if flDebug { 23 | logger.Debug("PKCS11 provider configuration", 24 | "library_path", config.Path, 25 | "pin_configured", config.Pin != "", 26 | "token_label", config.TokenLabel, 27 | "slot_number", config.SlotNumber) 28 | } 29 | if config.TokenLabel != "" { 30 | p11Config = crypto11.Config{ 31 | Path: config.Path, 32 | Pin: config.Pin, 33 | //SlotNumber: &config.SlotNumber, 34 | TokenLabel: config.TokenLabel, 35 | } 36 | } else { 37 | p11Config = crypto11.Config{ 38 | Path: config.Path, 39 | Pin: config.Pin, 40 | SlotNumber: &config.SlotNumber, 41 | //TokenLabel: config.TokenLabel, 42 | } 43 | } 44 | 45 | if flDebug { 46 | logger.Debug("PKCS11 configuration created", "config", p11Config) 47 | } 48 | ctx, err := crypto11.Configure(&p11Config) 49 | if err != nil { 50 | logger.Error("PKCS11 context configuration failed", "error", err) 51 | return signer, err 52 | } 53 | if flDebug { 54 | logger.Debug("PKCS11 context created successfully") 55 | } 56 | signers, err := ctx.FindAllKeyPairs() 57 | if err != nil { 58 | logger.Error("Failed to find key pairs in PKCS11 context", "error", err) 59 | return signer, err 60 | } 61 | if flDebug { 62 | logger.Debug("Found PKCS11 key pairs", "count", len(signers)) 63 | } 64 | for x, y := range signers { 65 | if flDebug { 66 | logger.Debug("Examining signer", 67 | "index", x, 68 | "public_key_type", fmt.Sprintf("%T", y.Public())) 69 | } 70 | switch y.Public().(type) { 71 | case *rsa.PublicKey: 72 | if pubkey.Equal(y.Public()) { 73 | logger.Info("Found matching PKCS11 key pair", "index", x) 74 | AuditEvent("pkcs11_key_found", true, 75 | "key_index", x, 76 | "key_type", "RSA") 77 | return signers[x], nil 78 | } else { 79 | if flDebug { 80 | logger.Debug("Public key mismatch, checking next key", "index", x) 81 | } 82 | } 83 | default: 84 | if flDebug { 85 | logger.Debug("Skipping non-RSA key", 86 | "index", x, 87 | "key_type", fmt.Sprintf("%T", y.Public())) 88 | } 89 | } 90 | } 91 | logger.Error("No matching PKCS11 key pair found", 92 | "searched_keys", len(signers), 93 | "suggestion", "verify the public key file matches a key in the PKCS11 provider") 94 | AuditEvent("pkcs11_key_found", false, 95 | "searched_keys", len(signers), 96 | "reason", "no_matching_key") 97 | return signer, errors.New("no matching key pair found in PKCS11 provider") 98 | } 99 | -------------------------------------------------------------------------------- /usage.txt: -------------------------------------------------------------------------------- 1 | ### Edit the config file 2 | 3 | You will need to have read the INSTALL instructions, in order to have set up 4 | AWS KMS, the AWS C++ SDK, and the aws-kms-pkcs11 provider. You will then need 5 | to edit or create the config.json file, which either needs to be in the same 6 | directory as you are running this from, or stored in 7 | $XDG_CONFIG_HOME/.certsquirt/config.json - XDG_CONFIG_HOME is usually set to 8 | ~/.config on Linux systems. 9 | 10 | You can leave the root ca file definition blank, and populate it after you have 11 | created the initial CA. Use -debug to output a little more information if you 12 | want see whats going on. 13 | 14 | ### Create the initial Root CA 15 | 16 | $ ./certsquirt -ca -bootstrap 17 | 18 | This will emit a new .pem and .crt formatted certificate. At this point you 19 | should edit config.json and add the location of the pem file to point to it, 20 | e.g. "SigningCert": "/home/certsquirt/PortSwigger CA - 2023.pem". 21 | 22 | We will next use this certificate as part of the signing process for the 23 | sub-ca's we will create next. The expectation is that you will have *another* 24 | RSA key in KMS for operating the sub-ca. Within KMS, head to the console for 25 | KMS and find the target key, then click on 'Public Key' and copy/download the 26 | key somewhere for the next step. 27 | 28 | This will be issued with a 10 year lifetime. 29 | 30 | ### Create a Sub CA using an existing RSA Public key (in pem format only) 31 | 32 | Using the public key from the new target sub-ca key, run something similar to 33 | the following to generate the intermediate/sub-ca certificate. 34 | 35 | This will be issues with a 5 year lifetime. 36 | 37 | $ ./certsquirt -subca -pubkey externalpublic.pem -subcaname 'AppleStuff' 38 | 39 | ### Create a Sub CA and generate an RSA private key at the same time 40 | 41 | ./certsquirt -subca -genkey -subcaname "WindowsStuff" 42 | 43 | Note, this is intended *solely* for import into systems which require it (e.g. 44 | an intercepting proxy, for example.) You should not have keys on disk (whether 45 | protected or not...). 46 | 47 | Keys are wrapped with a passphrase which is output to screen - this is not 48 | stored anyway, so take note of it. Keys are wrapped using TripleDES to support 49 | older systems - you may wish to change this. 50 | 51 | ### Switch to using the Intermediate CA with AWS KMS Keys (or similar) 52 | 53 | At this point, change the pkcs11 provider to target the sub-CA key, to sign 54 | csrs. 55 | 56 | To start with, inspect the csr that a user has given you, or you have generated 57 | to ensure it is sensible: 58 | 59 | $ ./certsquirt -csr test/04/04.csr 60 | 61 | This will output some information, and tell you what the request is for. 62 | Assuming you are happy to sign it, you should be able to run: 63 | 64 | ./certsquirt -csr test/04/04.csr -sign 65 | 66 | This will sign the cert, and save both the pem and der/crt to files named after 67 | the 'Subject' name in the CSR. 68 | 69 | Then, inspect the certificate with OpenSSL or similar: 70 | openssl x509 -in foo.bar.com.pem -text -noout 71 | 72 | Take note of the following in this certificate: 73 | 74 | * X509v3 Subject Key Identifier is unique to this certificate. 75 | 76 | * X509v3 Authority Key Identifier should match X509v3 Subject Key Identifier in 77 | the SubCA cert. 78 | 79 | * In the SubCA cert the Subject Key should match the Authority Key in the 80 | end-user certificate. 81 | 82 | * The Authority Key Identifier should match the Subject Key in the root CA 83 | certificate, to complete the chain. 84 | 85 | * The root CA certificate should not have an Authority Key Identifier, making 86 | it effectively self-signed (and thus untrusted till imported to other systems). 87 | -------------------------------------------------------------------------------- /db.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "crypto/ecdsa" 6 | "crypto/elliptic" 7 | "crypto/rsa" 8 | "crypto/x509" 9 | "encoding/asn1" 10 | "fmt" 11 | "net" 12 | "net/url" 13 | "os" 14 | "strings" 15 | "time" 16 | 17 | "github.com/aws/aws-sdk-go/aws" 18 | "github.com/aws/aws-sdk-go/aws/session" 19 | "github.com/aws/aws-sdk-go/service/dynamodb" 20 | "github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute" 21 | "github.com/pkg/errors" 22 | ) 23 | 24 | var dyndb *dynamodb.DynamoDB 25 | 26 | type x509Record struct { 27 | Status string 28 | Requester string 29 | SerialNumber string 30 | Issuer string 31 | Subject string 32 | NotBefore time.Time 33 | NotAfter time.Time 34 | PublicKeyAlgorithm x509.PublicKeyAlgorithm 35 | SignatureAlgorithm x509.SignatureAlgorithm 36 | DNSNames []string 37 | EmailAddresses []string 38 | IPAddresses []net.IP 39 | URIs []*url.URL 40 | PubKey []byte 41 | DerCert []byte 42 | } 43 | 44 | func addDbRecord(crtBytes []byte) error { 45 | logger := GetLogger() 46 | // now parse the cert back and add it to the DB. 47 | crt, err := x509.ParseCertificate(crtBytes) 48 | if err != nil { 49 | logger.Error("Cannot parse certificate for database record", "error", err) 50 | return err 51 | } 52 | // chomp out the pub key bytes 53 | var pubBytes []byte 54 | switch pub := crt.PublicKey.(type) { 55 | case *rsa.PublicKey: 56 | pubBytes, err = asn1.Marshal(*pub) 57 | if err != nil { 58 | return err 59 | } 60 | case *ecdsa.PublicKey: 61 | pubBytes = elliptic.Marshal(pub.Curve, pub.X, pub.Y) 62 | default: 63 | return errors.New("only ECDSA and RSA public keys are supported") 64 | } 65 | reader := bufio.NewReader(os.Stdin) 66 | fmt.Print("Enter Requester in the format of \"Joe Blogs \" -> ") 67 | requester, _ := reader.ReadString('\n') 68 | // marshal the crt to a pem byte array 69 | record := x509Record{ 70 | Status: "V", // Valid 71 | Requester: requester, 72 | SerialNumber: crt.SerialNumber.String(), // serial number should be unique (as in cryptographically) so we can use this as the key 73 | Issuer: crt.Issuer.String(), 74 | Subject: crt.Subject.String(), 75 | NotBefore: crt.NotBefore, 76 | NotAfter: crt.NotAfter, 77 | PublicKeyAlgorithm: crt.PublicKeyAlgorithm, 78 | SignatureAlgorithm: crt.SignatureAlgorithm, 79 | DNSNames: crt.DNSNames, 80 | EmailAddresses: crt.EmailAddresses, 81 | IPAddresses: crt.IPAddresses, 82 | URIs: crt.URIs, 83 | PubKey: pubBytes, 84 | DerCert: crtBytes, 85 | } 86 | 87 | // we should be running under the role given to us by the sts tokens. 88 | // We'll just use this role to create a new session. 89 | sess, err := session.NewSessionWithOptions(session.Options{ 90 | Config: aws.Config{Region: aws.String(config.AwsRegion)}, 91 | }) 92 | if err != nil { 93 | logger.Error("Cannot create AWS session for database", 94 | "error", err, 95 | "region", config.AwsRegion) 96 | return err 97 | } 98 | dyndb = dynamodb.New(sess) 99 | av, err := dynamodbattribute.MarshalMap(record) 100 | if err != nil { 101 | logger.Error("Cannot marshal certificate record for database", 102 | "error", err, 103 | "subject", crt.Subject.CommonName) 104 | return err 105 | } 106 | 107 | input := &dynamodb.PutItemInput{ 108 | Item: av, 109 | TableName: aws.String(config.AwsDbTableName), 110 | } 111 | 112 | logger.Debug("Adding certificate to database", 113 | "table", config.AwsDbTableName, 114 | "subject", crt.Subject.CommonName, 115 | "serial", crt.SerialNumber.String()) 116 | 117 | _, err = dyndb.PutItem(input) 118 | if err != nil { 119 | logger.Error("Cannot add certificate to database", 120 | "error", err, 121 | "table", config.AwsDbTableName, 122 | "subject", crt.Subject.CommonName, 123 | "serial", crt.SerialNumber.String()) 124 | return err 125 | } 126 | 127 | logger.Info("Certificate successfully added to database", 128 | "table", config.AwsDbTableName, 129 | "subject", crt.Subject.CommonName, 130 | "serial", crt.SerialNumber.String(), 131 | "requester", strings.TrimSpace(record.Requester)) 132 | 133 | AuditEvent("certificate_database_add", true, 134 | "subject", crt.Subject.CommonName, 135 | "serial", crt.SerialNumber.String(), 136 | "table", config.AwsDbTableName, 137 | "requester", strings.TrimSpace(record.Requester)) 138 | 139 | return nil 140 | } 141 | -------------------------------------------------------------------------------- /awsauth.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "os" 7 | "os/user" 8 | "strings" 9 | "time" 10 | 11 | "github.com/aws/aws-sdk-go/aws/session" 12 | "github.com/aws/aws-sdk-go/service/sts" 13 | "github.com/aws/aws-sdk-go/service/sts/stsiface" 14 | "github.com/pquerna/otp" 15 | "github.com/pquerna/otp/totp" 16 | 17 | "golang.org/x/term" 18 | ) 19 | 20 | // TakeRole gets temporary security credentials to access resources 21 | // Inputs: 22 | // 23 | // svc is an AWS STS service client 24 | // roleARN is the Amazon Resource Name (ARN) of the role to assume 25 | // sessionName is a unique identifier for the session 26 | // mfaserial is the ARN of the MFA token you are using 27 | // mfacode is the code produced by the seed 28 | // 29 | // Output: 30 | // 31 | // If success, information about the assumed role and nil 32 | // Otherwise, nil and an error from the call to AssumeRole 33 | func TakeRole(svc stsiface.STSAPI, roleARN, sessionName *string, mfaserial *string, mfacode *string) (*sts.AssumeRoleOutput, error) { 34 | // snippet-start:[sts.go.take_role.call] 35 | 36 | // 🙁, this leaves an attacker a small window of opportunity. 37 | // Would be nice if AWS exposed an InvalidateCredentials() call. 38 | var duration int64 = 900 // minimum accepted by aws api 39 | result, err := svc.AssumeRole(&sts.AssumeRoleInput{ 40 | RoleArn: roleARN, 41 | RoleSessionName: sessionName, 42 | SerialNumber: mfaserial, 43 | TokenCode: mfacode, 44 | DurationSeconds: &duration, 45 | }) 46 | // snippet-end:[sts.go.take_role.call] 47 | 48 | return result, err 49 | } 50 | 51 | // nice wrapper to deal with command line copy pasta of credentials 52 | func credentials() (string, string, string) { 53 | logger := GetLogger() 54 | reader := bufio.NewReader(os.Stdin) 55 | 56 | fmt.Print("Enter Access Key ID: ") 57 | username, _ := reader.ReadString('\n') 58 | 59 | fmt.Print("Enter Access Key Secret: ") 60 | bytePassword, err := term.ReadPassword(0) 61 | if err != nil { 62 | logger.Error("Failed to read access key secret", "error", err) 63 | os.Exit(1) 64 | } 65 | fmt.Println() 66 | fmt.Printf("Enter MFA Code: ") 67 | byteMfaCode, err := term.ReadPassword(0) 68 | if err != nil { 69 | logger.Error("Failed to read MFA code", "error", err) 70 | os.Exit(1) 71 | } 72 | password := string(bytePassword) 73 | mfacode := string(byteMfaCode) 74 | 75 | return strings.TrimSpace(username), strings.TrimSpace(password), strings.TrimSpace(mfacode) 76 | } 77 | 78 | func assumeRole() (creds sts.Credentials) { 79 | logger := GetLogger() 80 | var mfacode string 81 | if flDebug { 82 | logger.Debug("Authentication credentials configuration", 83 | "has_access_key", config.AwsAccessKey != "", 84 | "has_secret_key", config.AwsSecretKey != "", 85 | "has_totp_secret", config.AwsTotpSecret != "") 86 | } 87 | // if any of the creds are unset, then force all to be entered. 88 | if config.AwsAccessKey == "" || config.AwsSecretKey == "" || config.AwsTotpSecret == "" { 89 | logger.Info("Interactive credential input required") 90 | config.AwsAccessKey, config.AwsSecretKey, mfacode = credentials() 91 | } else { 92 | // User wants to use kms, perhaps we should Fatalf here. 93 | logger.Warn("HARDCODED CREDENTIALS DETECTED - THIS IS EXTREMELY DANGEROUS FOR PRODUCTION USE") 94 | AuditEvent("hardcoded_credentials_used", true, 95 | "warning", "hardcoded AWS credentials in configuration") 96 | } 97 | // set them in our env so that they are used below, overwritting anything that already 98 | // exists 99 | os.Setenv("AWS_ACCESS_KEY", config.AwsAccessKey) 100 | os.Setenv("AWS_SECRET_KEY", config.AwsSecretKey) 101 | os.Setenv("AWS_DEFAULT_REGION", config.AwsRegion) 102 | // defined role we want to assume 103 | // get the current user to popilate the sessionName with 104 | user, err := user.Current() 105 | if err != nil { 106 | logger.Error("Cannot determine current username for session", "error", err) 107 | os.Exit(1) 108 | } 109 | // now figure out the hostname to add to the sessionName to track via cloudtrail 110 | hostname, err := os.Hostname() 111 | if err != nil { 112 | logger.Error("Cannot determine hostname for session", "error", err) 113 | os.Exit(1) 114 | } 115 | sessionName := user.Username + "@" + hostname 116 | 117 | // perhaps don't do this for production.... 118 | if config.AwsTotpSecret != "" { 119 | logger.Warn("HARDCODED TOTP SECRET DETECTED - THIS IS EXTREMELY DANGEROUS FOR PRODUCTION USE") 120 | mfacode, err = totp.GenerateCodeCustom(config.AwsTotpSecret, time.Now(), totp.ValidateOpts{ 121 | Period: 30, 122 | Skew: 1, 123 | Digits: otp.DigitsSix, 124 | Algorithm: otp.AlgorithmSHA1, // yep, sha1. 125 | }) 126 | if err != nil { 127 | logger.Error("Cannot generate TOTP code from configuration secret", "error", err) 128 | os.Exit(1) 129 | } 130 | AuditEvent("hardcoded_totp_used", true, 131 | "warning", "hardcoded TOTP secret used for authentication") 132 | } 133 | 134 | // snippet-start:[sts.go.take_role.session] 135 | sess := session.Must(session.NewSessionWithOptions(session.Options{ 136 | SharedConfigState: session.SharedConfigEnable, 137 | })) 138 | 139 | svc := sts.New(sess) 140 | // snippet-end:[sts.go.take_role.session] 141 | 142 | logger.Info("Assuming AWS role", 143 | "role_arn", config.AwsRoleARN, 144 | "session_name", sessionName, 145 | "mfa_serial", config.AwsMfaSerial) 146 | 147 | result, err := TakeRole(svc, &config.AwsRoleARN, &sessionName, &config.AwsMfaSerial, &mfacode) 148 | if err != nil { 149 | logger.Error("Failed to assume AWS role", 150 | "error", err, 151 | "role_arn", config.AwsRoleARN, 152 | "session_name", sessionName, 153 | "suggestion", "check MFA timing or try again") 154 | AuditEvent("aws_role_assumption", false, 155 | "role_arn", config.AwsRoleARN, 156 | "session_name", sessionName, 157 | "error", err.Error()) 158 | os.Exit(1) 159 | } 160 | 161 | logger.Info("Successfully assumed AWS role", 162 | "role_arn", config.AwsRoleARN, 163 | "session_name", sessionName) 164 | AuditEvent("aws_role_assumption", true, 165 | "role_arn", config.AwsRoleARN, 166 | "session_name", sessionName) 167 | 168 | return *result.Credentials 169 | } 170 | -------------------------------------------------------------------------------- /cloudformation/certsquirt.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | AWSTemplateFormatVersion: "2010-09-09" 3 | Description: | 4 | This cfn-template creates any required infrastructure for the CertSquirt service. 5 | Comment out the KMS keys if you do not need them, you will need to edit any other 6 | affected resources as well. 7 | 8 | Note that once this has been run, you will still need to create the users access 9 | keys and create a soft MFA token. 10 | 11 | Parameters: 12 | BuildTag: 13 | Type: String 14 | Description: Who or what built this thing 15 | Default: CertSquirt 16 | 17 | EnvironmentTag: 18 | Type: String 19 | Description: The Environment 20 | Default: Production 21 | 22 | RootKeyAliasName: 23 | Type: String 24 | Description: Alias (friendly) name for the root CA KMS key (keep alias/ prefix!) 25 | Default: alias/CertSquirt-Root-CA-Key 26 | 27 | SubKeyAliasName: 28 | Type: String 29 | Description: Alias (friendly) name for the Sub CA KMS key (keep alias/ prefix!) 30 | Default: alias/CertSquirt-Sub-CA-Key 31 | 32 | Resources: 33 | # Comment all this block if not needed 34 | CertSquirtRootCaKey: 35 | Type: AWS::KMS::Key 36 | Properties: 37 | Description: "This is the root CA RSA key used to support the certsquirt service" 38 | KeySpec: RSA_4096 39 | KeyUsage: SIGN_VERIFY 40 | MultiRegion: false 41 | KeyPolicy: 42 | Version: 2012-10-17 43 | Id: CertSquirt-1 44 | Statement: 45 | - Sid: Enable IAM User Permissions 46 | Effect: Allow 47 | Principal: 48 | AWS: !Sub arn:${AWS::Partition}:iam::${AWS::AccountId}:root 49 | Action: 'kms:*' 50 | Resource: '*' 51 | Tags: 52 | - Key: Name 53 | Value: CA-Key-CertSquirt 54 | - Key: Environment 55 | Value: !Ref EnvironmentTag 56 | - Key: BuildTag 57 | Value: !Ref BuildTag 58 | # Comment all this block if not needed 59 | CertSquirtRootCaKeyAlias: 60 | Type: AWS::KMS::Alias 61 | Properties: 62 | AliasName: !Ref RootKeyAliasName 63 | TargetKeyId: !Ref CertSquirtRootCaKey 64 | 65 | # Comment all this block if not needed 66 | CertSquirtSubCaKey: 67 | Type: AWS::KMS::Key 68 | Properties: 69 | Description: "This is the Sub CA RSA key used to support the certsquirt service" 70 | KeySpec: RSA_4096 71 | KeyUsage: SIGN_VERIFY 72 | MultiRegion: false 73 | KeyPolicy: 74 | Version: 2012-10-17 75 | Id: CertSquirt-1 76 | Statement: 77 | - Sid: Enable IAM User Permissions 78 | Effect: Allow 79 | Principal: 80 | AWS: !Sub arn:${AWS::Partition}:iam::${AWS::AccountId}:root 81 | Action: 'kms:*' 82 | Resource: '*' 83 | Tags: 84 | - Key: Name 85 | Value: Sub-CA-Key-CertSquirt 86 | - Key: Environment 87 | Value: !Ref EnvironmentTag 88 | - Key: BuildTag 89 | Value: !Ref BuildTag 90 | # Comment all this block if not needed 91 | CertSquirtSubCaKeyAlias: 92 | Type: AWS::KMS::Alias 93 | Properties: 94 | AliasName: !Ref SubKeyAliasName 95 | TargetKeyId: !Ref CertSquirtSubCaKey 96 | 97 | CertSquirtTable: 98 | Type: AWS::DynamoDB::Table 99 | DeletionPolicy: Retain 100 | UpdateReplacePolicy: Retain 101 | Properties: 102 | BillingMode: PAY_PER_REQUEST 103 | AttributeDefinitions: 104 | - 105 | AttributeName: SerialNumber 106 | AttributeType: S 107 | KeySchema: 108 | - 109 | AttributeName: SerialNumber 110 | KeyType: HASH 111 | DeletionProtectionEnabled: true 112 | Tags: 113 | - Key: Name 114 | Value: CertSquirtDB 115 | - Key: Environment 116 | Value: !Ref EnvironmentTag 117 | - Key: BuildTag 118 | Value: !Ref BuildTag 119 | 120 | CertSquirtAccessPolicy: 121 | Type: AWS::IAM::ManagedPolicy 122 | Properties: 123 | Path: / 124 | PolicyDocument: 125 | Version: "2012-10-17" 126 | Statement: 127 | - 128 | Action: 129 | - sts:AssumeRole 130 | Effect: Allow 131 | Resource: !GetAtt CertSquirtRole.Arn 132 | 133 | CertSquirtRole: 134 | Type: AWS::IAM::Role 135 | Properties: 136 | AssumeRolePolicyDocument: 137 | Version: "2012-10-17" 138 | Statement: 139 | - Effect: Allow 140 | Principal: 141 | AWS: !Sub arn:${AWS::Partition}:iam::${AWS::AccountId}:root 142 | Action: 143 | - sts:AssumeRole 144 | Condition: 145 | Bool: 146 | aws:MultiFactorAuthPresent: true 147 | Path: / 148 | Policies: 149 | - PolicyName: CertSquirtAccessPolicy 150 | PolicyDocument: 151 | Version: "2012-10-17" 152 | Statement: 153 | - Effect: Allow 154 | Action: 'dynamodb:*' 155 | Resource: !GetAtt CertSquirtTable.Arn 156 | # Not strictly needed but allows aws-kms-pkcs11 to list keys for debugging 157 | - Effect: Allow 158 | Action: 159 | - 'kms:ListKeys' 160 | Resource: '*' 161 | # Comment below if not using KMS or are using only one of the keys accordingly 162 | - Effect: Allow 163 | Action: 164 | - 'kms:DescribeKey' 165 | - 'kms:Sign' 166 | - 'kms:GetPublicKey' 167 | - 'kms:GenerateRandom' 168 | Resource: 169 | - !GetAtt CertSquirtRootCaKey.Arn 170 | - !GetAtt CertSquirtSubCaKey.Arn 171 | 172 | CertSquirtUser: 173 | Type: AWS::IAM::User 174 | Properties: 175 | ManagedPolicyArns: 176 | - !Ref CertSquirtAccessPolicy 177 | Path: / 178 | Tags: 179 | - Key: Name 180 | Value: CertSquirt User 181 | - Key: Environment 182 | Value: !Ref EnvironmentTag 183 | - Key: BuildTag 184 | Value: !Ref BuildTag 185 | 186 | Outputs: 187 | CertSquirtTable: 188 | Value: !Ref CertSquirtTable 189 | Export: 190 | Name: CertSquirtTable 191 | CertSquirtTableArn: 192 | Value: !GetAtt CertSquirtTable.Arn 193 | Export: 194 | Name: CertSquirtTableArn 195 | CertSquirtUser: 196 | Value: !Ref CertSquirtUser 197 | Export: 198 | Name: CertSquirtUser 199 | CertSquirtRootCaKey: 200 | Value: !Ref CertSquirtRootCaKey 201 | Export: 202 | Name: CertSquirtRootCaKey 203 | CertSquirtRootCaKeyAlias: 204 | Value: CertSquirtRootCaKeyAlias 205 | Export: 206 | Name: CertSquirtRootCaKeyAlias 207 | CertSquirtRootCaKeyArn: 208 | Value: !GetAtt CertSquirtRootCaKey.Arn 209 | Export: 210 | Name: CertSquirtRootCaKeyArn 211 | CertSquirtSubCaKey: 212 | Value: !Ref CertSquirtSubCaKey 213 | Export: 214 | Name: CertSquirtSubCaKey 215 | CertSquirtSubCaKeyAlias: 216 | Value: CertSquirtSubCaKeyAlias 217 | Export: 218 | Name: CertSquirtSubCaKeyAlias 219 | CertSquirtSubCaKeyArn: 220 | Value: !GetAtt CertSquirtSubCaKey.Arn 221 | Export: 222 | Name: CertSquirtSubCaKeyArn -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Build & Release Workflow 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | pull_request: 7 | branches: ["main"] 8 | 9 | env: 10 | REGISTRY: ghcr.io 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-22.04 15 | permissions: 16 | contents: write 17 | packages: write 18 | id-token: write 19 | attestations: write 20 | 21 | strategy: 22 | matrix: 23 | platform: [linux/amd64, linux/arm64] 24 | fail-fast: false 25 | 26 | steps: 27 | - name: Set lowercase repo info and sanitized platform tag 28 | id: vars 29 | run: | 30 | repo="${{ github.repository }}" 31 | platform="${{ matrix.platform }}" 32 | echo "repo_lower=$(echo "$repo" | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV 33 | echo "PLATFORM_TAG=$(echo "$platform" | sed 's|/|-|')" >> $GITHUB_ENV 34 | echo "SHORT_SHA=$(echo '${{ github.sha }}' | cut -c1-7)" >> $GITHUB_ENV 35 | 36 | # - name: Harden Runner 37 | # uses: step-security/harden-runner@v2.10.2 38 | # with: 39 | # egress-policy: audit 40 | 41 | - name: Harden Runner 42 | uses: step-security/harden-runner@446798f8213ac2e75931c1b0769676d927801858 # v2.10.0 43 | with: 44 | egress-policy: block 45 | allowed-endpoints: > 46 | api.github.com:443 47 | archive.ubuntu.com:80 48 | auth.docker.io:443 49 | azure.archive.ubuntu.com:80 50 | dc.services.visualstudio.com:443 51 | esm.ubuntu.com:443 52 | ghcr.io:443 53 | github.com:443 54 | objects.githubusercontent.com:443 55 | packages.microsoft.com:443 56 | production.cloudflare.docker.com:443 57 | proxy.golang.org:443 58 | registry-1.docker.io:443 59 | motd.ubuntu.com:80 60 | security.ubuntu.com:80 61 | ports.ubuntu.com:80 62 | storage.googleapis.com:443 63 | uploads.github.com:443 64 | fulcio.sigstore.dev:443 65 | rekor.sigstore.dev:443 66 | 67 | - name: Cache Go modules 68 | uses: actions/cache@v4 69 | with: 70 | path: | 71 | ~/.cache/go-build 72 | ~/go/pkg/mod 73 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 74 | restore-keys: | 75 | ${{ runner.os }}-go- 76 | 77 | - name: Cache APT packages 78 | uses: actions/cache@v4 79 | with: 80 | path: | 81 | /var/cache/apt/archives 82 | /var/lib/apt/lists 83 | key: ${{ runner.os }}-apt-${{ hashFiles('**/Dockerfile') }} 84 | restore-keys: | 85 | ${{ runner.os }}-apt- 86 | 87 | - name: Checkout certsquirt 88 | uses: actions/checkout@v4 89 | with: 90 | fetch-depth: 0 91 | path: certsquirt 92 | 93 | - name: Read version from file 94 | id: get_version 95 | run: echo "VERSION=$(cat certsquirt/VERSION)" >> $GITHUB_ENV 96 | 97 | - name: Setup Go 98 | uses: actions/setup-go@v5 99 | with: 100 | go-version: 'stable' 101 | 102 | - name: Set architecture variables 103 | id: arch-vars 104 | run: | 105 | case "${{ matrix.platform }}" in 106 | "linux/amd64") 107 | echo "GOOS=linux" >> $GITHUB_ENV 108 | echo "GOARCH=amd64" >> $GITHUB_ENV 109 | echo "VCPKG_TRIPLET=x64-linux-dynamic" >> $GITHUB_ENV 110 | echo "CC=gcc" >> $GITHUB_ENV 111 | echo "CXX=g++" >> $GITHUB_ENV 112 | ;; 113 | "linux/arm64") 114 | echo "GOOS=linux" >> $GITHUB_ENV 115 | echo "GOARCH=arm64" >> $GITHUB_ENV 116 | echo "VCPKG_TRIPLET=arm64-linux-dynamic" >> $GITHUB_ENV 117 | echo "CC=aarch64-linux-gnu-gcc" >> $GITHUB_ENV 118 | echo "CXX=aarch64-linux-gnu-g++" >> $GITHUB_ENV 119 | ;; 120 | esac 121 | 122 | - name: Install cross-compilation tools for ARM64 123 | if: matrix.platform == 'linux/arm64' 124 | run: | 125 | sudo apt-get update 126 | sudo apt-get install -y gcc-aarch64-linux-gnu g++-aarch64-linux-gnu 127 | 128 | - name: Install aws-sdk-cpp 129 | id: vcpkg 130 | uses: johnwason/vcpkg-action@v6 131 | with: 132 | pkgs: aws-sdk-cpp[kms] aws-sdk-cpp[acm-pca] 133 | triplet: ${{ env.VCPKG_TRIPLET }} 134 | token: ${{ github.token }} 135 | 136 | - name: Build CertSquirt with version info 137 | run: | 138 | cd certsquirt 139 | echo "Building for GOOS=$GOOS GOARCH=$GOARCH with CC=$CC CXX=$CXX" 140 | CGO_ENABLED=1 go build -v -ldflags "-X main.buildstamp=$(date -u '+%Y-%m-%d_%I:%M:%S%p') -X main.githash=$(git rev-parse HEAD)" -o ../certsquirt-${{ env.PLATFORM_TAG }} 141 | cp ../certsquirt-${{ env.PLATFORM_TAG }} ./certsquirt 142 | 143 | - name: Checkout aws-kms-pkcs11 144 | uses: actions/checkout@v4 145 | with: 146 | repository: "JackOfMostTrades/aws-kms-pkcs11" 147 | path: aws-kms-pkcs11 148 | fetch-depth: 1 149 | 150 | - name: Get aws-kms-pkcs11 commit hash 151 | id: aws-kms-commit 152 | run: | 153 | cd aws-kms-pkcs11 154 | echo "hash=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT 155 | 156 | - name: Cache aws-kms-pkcs11 build 157 | id: cache-aws-kms 158 | uses: actions/cache@v4 159 | with: 160 | path: aws-kms-pkcs11/aws_kms_pkcs11.so 161 | key: ${{ runner.os }}-${{ matrix.platform }}-aws-kms-pkcs11-${{ steps.aws-kms-commit.outputs.hash }}-${{ hashFiles('**/vcpkg.json') }} 162 | restore-keys: | 163 | ${{ runner.os }}-${{ matrix.platform }}-aws-kms-pkcs11-${{ steps.aws-kms-commit.outputs.hash }}- 164 | ${{ runner.os }}-${{ matrix.platform }}-aws-kms-pkcs11- 165 | 166 | - name: Install build dependencies 167 | if: steps.cache-aws-kms.outputs.cache-hit != 'true' 168 | run: | 169 | sudo apt-get update 170 | if [ "${{ matrix.platform }}" = "linux/arm64" ]; then 171 | # Enable cross-architecture packages 172 | sudo dpkg --add-architecture arm64 173 | # Restrict existing repositories to amd64 only 174 | sudo sed -i 's/^deb /deb [arch=amd64] /' /etc/apt/sources.list 175 | sudo sed -i 's/^deb-src /deb-src [arch=amd64] /' /etc/apt/sources.list 176 | # Add ARM64 repository for packages 177 | echo "deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ $(lsb_release -cs) main universe" | sudo tee /etc/apt/sources.list.d/arm64.list 178 | echo "deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ $(lsb_release -cs)-updates main universe" | sudo tee -a /etc/apt/sources.list.d/arm64.list 179 | echo "deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ $(lsb_release -cs)-security main universe" | sudo tee -a /etc/apt/sources.list.d/arm64.list 180 | sudo apt-get update 181 | sudo apt-get install -y build-essential libjson-c-dev:arm64 libp11-kit-dev:arm64 libcurl4-openssl-dev:arm64 libssl-dev:arm64 zlib1g-dev:arm64 182 | else 183 | sudo apt-get install -y build-essential libjson-c-dev libp11-kit-dev libcurl4-openssl-dev 184 | fi 185 | 186 | - name: Set vcpkg architecture path 187 | run: | 188 | case "${{ matrix.platform }}" in 189 | "linux/amd64") 190 | echo "VCPKG_INSTALLED_PATH=${{ github.workspace }}/vcpkg/installed/x64-linux-dynamic" >> $GITHUB_ENV 191 | ;; 192 | "linux/arm64") 193 | echo "VCPKG_INSTALLED_PATH=${{ github.workspace }}/vcpkg/installed/arm64-linux-dynamic" >> $GITHUB_ENV 194 | ;; 195 | esac 196 | 197 | - name: Build aws-kms-pkcs11 198 | if: steps.cache-aws-kms.outputs.cache-hit != 'true' 199 | run: | 200 | cd aws-kms-pkcs11 201 | if [ "${{ matrix.platform }}" = "linux/arm64" ]; then 202 | # Debug: Check the architecture of vcpkg libraries and toolchain 203 | echo "Checking vcpkg library architectures:" 204 | file ${{ env.VCPKG_INSTALLED_PATH }}/lib/libaws-cpp-sdk-core.so || echo "File not found" 205 | echo "Checking compiler and linker:" 206 | which ${{ env.CC }} && ${{ env.CC }} --version 207 | which ${{ env.CXX }} && ${{ env.CXX }} --version 208 | echo "Testing simple compilation:" 209 | echo 'int main(){return 0;}' > test.c 210 | ${{ env.CC }} -c test.c -o test.o 211 | file test.o 212 | rm -f test.c test.o 213 | # For ARM64 cross-compilation, specify all required library paths with correct -I flags 214 | # pkcs11.h is in /usr/include/p11-kit-1/p11-kit/pkcs11.h 215 | # Add proper library path and cross-compilation flags 216 | export PKG_CONFIG_PATH="${{ env.VCPKG_INSTALLED_PATH }}/lib/pkgconfig:$PKG_CONFIG_PATH" 217 | export PKG_CONFIG_SYSROOT_DIR="/" 218 | # Fix the hardcoded g++ in Makefile by replacing it with our cross-compiler 219 | sed -i 's/g++ -shared/$(CXX) -shared/' Makefile 220 | sed -i '1i CXX ?= g++' Makefile 221 | AWS_SDK_PATH="${{ env.VCPKG_INSTALLED_PATH }}" make CC="${{ env.CC }}" CXX="${{ env.CXX }}" \ 222 | PKCS11_INC="-I/usr/include/p11-kit-1/p11-kit" \ 223 | PKCS11_MOD_PATH="/usr/lib/aarch64-linux-gnu/pkcs11" \ 224 | JSON_C_INC="-I/usr/include/json-c" \ 225 | LDFLAGS="-L${{ env.VCPKG_INSTALLED_PATH }}/lib -L/usr/lib/aarch64-linux-gnu" 226 | else 227 | AWS_SDK_PATH="${{ env.VCPKG_INSTALLED_PATH }}" make CC="${{ env.CC }}" CXX="${{ env.CXX }}" 228 | fi 229 | 230 | - name: Set up QEMU 231 | uses: docker/setup-qemu-action@v3.6.0 232 | 233 | - name: Set up Docker Buildx 234 | uses: docker/setup-buildx-action@v3 235 | 236 | - name: Log in to the container registry 237 | uses: docker/login-action@v3 238 | with: 239 | registry: ${{ env.REGISTRY }} 240 | username: ${{ github.actor }} 241 | password: ${{ secrets.GITHUB_TOKEN }} 242 | 243 | - name: Extract metadata (tags, labels) for docker 244 | id: meta 245 | uses: docker/metadata-action@v5 246 | with: 247 | images: ${{ env.REGISTRY }}/${{ env.repo_lower }} 248 | 249 | - name: Build and push Docker image (${{ matrix.platform }}) 250 | uses: docker/build-push-action@v6 251 | id: push 252 | with: 253 | context: ./ 254 | platforms: ${{ matrix.platform }} 255 | push: true 256 | file: certsquirt/Dockerfile 257 | tags: | 258 | ${{ env.REGISTRY }}/${{ env.repo_lower }}:${{ env.VERSION }} 259 | ${{ env.REGISTRY }}/${{ env.repo_lower }}:latest 260 | ${{ env.REGISTRY }}/${{ env.repo_lower }}:${{ env.PLATFORM_TAG }} 261 | labels: ${{ steps.meta.outputs.labels }} 262 | build-args: | 263 | BUILDKIT_INLINE_CACHE=1 264 | PLATFORM=${{ matrix.platform }} 265 | cache-from: | 266 | type=gha 267 | type=registry,ref=${{ env.REGISTRY }}/${{ env.repo_lower }}:buildcache-${{ env.PLATFORM_TAG }} 268 | type=registry,ref=${{ env.REGISTRY }}/${{ env.repo_lower }}:latest 269 | cache-to: | 270 | type=gha,mode=max 271 | type=registry,ref=${{ env.REGISTRY }}/${{ env.repo_lower }}:buildcache-${{ env.PLATFORM_TAG }},mode=max 272 | provenance: false 273 | sbom: false 274 | 275 | - name: Attest 276 | uses: actions/attest-build-provenance@v1 277 | id: attest 278 | with: 279 | subject-name: ${{ env.REGISTRY }}/${{ env.repo_lower }} 280 | subject-digest: ${{ steps.push.outputs.digest }} 281 | push-to-registry: true 282 | 283 | - name: Debug image digest 284 | run: echo "Digest ${{ steps.push.outputs.digest }}" 285 | 286 | - name: Upload binary artifact 287 | uses: actions/upload-artifact@v4 288 | with: 289 | name: certsquirt-${{ env.PLATFORM_TAG }} 290 | path: certsquirt-${{ env.PLATFORM_TAG }} 291 | outputs: 292 | version: ${{ env.VERSION }} 293 | 294 | release: 295 | needs: build 296 | runs-on: ubuntu-latest 297 | permissions: 298 | contents: write 299 | packages: write 300 | id-token: write 301 | attestations: write 302 | if: github.ref == 'refs/heads/main' 303 | env: 304 | VERSION: ${{ needs.build.outputs.version }} 305 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 306 | 307 | steps: 308 | - name: Checkout full repo for GH CLI 309 | uses: actions/checkout@v4 310 | with: 311 | fetch-depth: 0 312 | 313 | - name: Download binary artifacts 314 | uses: actions/download-artifact@v4 315 | with: 316 | path: ./artifacts 317 | pattern: certsquirt-* 318 | merge-multiple: false 319 | 320 | - name: Move artifacts to root 321 | run: | 322 | mv ./artifacts/certsquirt-linux-amd64/certsquirt-linux-amd64 ./ 323 | mv ./artifacts/certsquirt-linux-arm64/certsquirt-linux-arm64 ./ 324 | 325 | - name: Upload release assets 326 | run: | 327 | export VERSION=`cat VERSION` 328 | gh release delete v$VERSION --cleanup-tag --yes || echo "No existing release to delete" 329 | gh release create v$VERSION \ 330 | --title "Release v$VERSION" \ 331 | --generate-notes \ 332 | certsquirt-linux-amd64 \ 333 | certsquirt-linux-arm64 334 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A golang PKI in just over a 1000 lines of code. 2 | 3 | ## pokebadges 4 | [![Build Status](https://github.com/PortSwigger/certsquirt/actions/workflows/main.yml/badge.svg)](https://github.com/PortSwigger/certsquirt/actions/workflows/main.yml) 5 | [![GHCR](https://img.shields.io/badge/GHCR-certsquirt-blue?logo=docker)](https://github.com/orgs/portswigger/packages/container/package/certsquirt) 6 | [![Latest Release](https://img.shields.io/github/v/release/portswigger/certsquirt)](https://github.com/portswigger/certsquirt/releases) 7 | [![License](https://img.shields.io/github/license/portswigger/certsquirt)](LICENSE) 8 | [![Go Report Card](https://goreportcard.com/badge/github.com/portswigger/certsquirt)](https://goreportcard.com/report/github.com/portswigger/certsquirt) 9 | [![CodeQL](https://github.com/PortSwigger/certsquirt/actions/workflows/github-code-scanning/codeql/badge.svg)](https://github.com/PortSwigger/certsquirt/actions/workflows/github-code-scanning/codeql/) 10 | [![Hardened](https://img.shields.io/badge/Secured%20By-StepSecurity-success?logo=shield)](https://github.com/step-security/harden-runner) 11 | [![Security Status](https://img.shields.io/badge/Security-Enabled-brightgreen?logo=github)](https://github.com/PortSwigger/certsquirt/security) 12 | [![Go Reference](https://pkg.go.dev/badge/github.com/PortSwigger/certsquirt.svg)](https://pkg.go.dev/github.com/PortSwigger/certsquirt) 13 | 14 | # Introduction 15 | 16 | This repository contains a solution to create and manage a small-scale PKI for a small or medium sized enterprise, which is managed securely with keys being managed by a PKCS11 provider. This is an easier solution to drive than something like easy-rsa, is more secure, and is far simpler to configure than other PKI software solutions. 17 | 18 | While there are native API's to talk to crypto providers, for example KMS, directly one of the goals of this project was to make things configurable, so that you are not tied to a single crypto provider. To that end, we chose to use [pkcs11](https://en.wikipedia.org/wiki/PKCS_11) as a crypto provider. This means you should theoretically be able to talk to ['real world' HSM's](https://github.com/ThalesGroup/crypto11#testing-with-thales-luna-hsm), [AWS CloudHSM](https://docs.aws.amazon.com/cloudhsm/latest/userguide/pkcs11-library.html), [YubiKeys](https://developers.yubico.com/yubico-piv-tool/YKCS11/) , et al. 19 | 20 | This effectively gives you an excellent starting point to expand your crypto world as you progress - you should be able to start with Yubikeys or AWS KMS and then as requirements dictate, migrate upwards to more robust crypto providers. You could mix and match, for example the root in KMS and a YubiKey for issuing certificates, or a Yubikey for root and KMS for the subca issuing, or just 2 Yubikeys, though for testing all you need is 1. 21 | 22 | # Caveats 23 | 24 | Although not an issue with this solution per se, if you intend to use the `aws-kms-pkcs11` provider, this does not build easily on MacOS. You are advised to use a Linux machine/VM to run this solution if you wish to use KMS. 25 | 26 | ## Docker 27 | There is also a docker image which you can use, based on ubuntu, on the 'releases' page of the github. 28 | 29 | You can run it like this - note you will need to map `somedir` to `depot`, with somedir containing the config and artifacts you need, e.g. 30 | **FIXME** 31 | ``` 32 | docker run -v somedir:/depot/ --platform linux/x86_64 -w /depot -it ghcr.io/portswigger/certsquirt:main -config /depot/config.json.prod -ca -bootstrap -pubkey /depot/some_kms_root_ca_pubkey.pub 33 | ``` 34 | 35 | ``` 36 | docker pull ghcr.io/portswigger/certsquirt:linux-arm64 37 | ``` 38 | 39 | # 40 | 41 | If you wish to use Yubikeys as the provider, then this works rather well with MacOS. 42 | 43 | As is usual, mostly everything works with Linux. 44 | 45 | # Configuration 46 | 47 | Configuration is performed via a JSON config file. By default, we will attempt to load a file name config.json in the current directory. 48 | 49 | A sample is available [here](https://raw.githubusercontent.com/PortSwigger/certsquirt/main/config.json.sample). 50 | 51 | You will need to edit the config file to (at a bare minimum) configure the following: 52 | 53 | * AwsRoleARN 54 | * AwsMfaSerial 55 | * AwsDbTableName 56 | * AwsRegion 57 | * P11Path 58 | * P11TokenLabel (Use "" for a Yubikey, or try "" if having problems) 59 | * P11Pin (Add the pin for a Yubikey, leave as "" for AWS KMS) 60 | * P11Slot (Leave as 0 for both KMS and Yubikeys) 61 | 62 | If using KMS, you will need to install and configure [aws-kms-pkcs11](#jack-of-most-trades---aws-kms-pkcs11). The P11TokenLabel corresponds to the label defined in the aws-kms-pkcs11 [configuration file](https://github.com/JackOfMostTrades/aws-kms-pkcs11#configuration) . You will need to have this configured and working before you can proceed. You can ignore the part around 'AWS Credentials' - we'll handle that for you. 63 | 64 | # Installation 65 | 66 | Either clone this repository with git and run `make`, or you can install it via 67 | 68 | `go install github.com/PortSwigger/certsquirt@latest` 69 | 70 | At a minimum, you will need to create a DynamoDB table within AWS. The [cloudformation](https://github.com/PortSwigger/certsquirt/blob/main/cloudformation/certsquirt.yaml) template can be editted to remove references to KMS keys if you are going to use something else as a crypto provider, otherwise the default will create everything you need to host the keys and DB in AWS. See [here](#cloudformation) for more details. 71 | 72 | # Execution 73 | 74 | ## Yubikey 75 | 76 | Yubikeys are awesome devices, and have a massive scope for usage in crypto problem solving. Here's a guide to how to set up using a Yubikey for one of the crypto providers. 77 | 78 | *note: you will still need to use AWS DynamoDB as the backend database! Simply comment out the KMS key generations in the cloudformation template provided* 79 | 80 | To use the yubikey, you will need to install the `yubico-piv-tool` via your package manager (it's in homebrew on MacOS), or by following the instructions at [Yubico PIV Guide](https://developers.yubico.com/yubico-piv-tool/). 81 | 82 | The Yubikey has multiple key slots available for use, described fully [here](https://developers.yubico.com/PIV/Introduction/Certificate_slots.html). There are exposed slots which are now 'retired' by Yubico, living in slots 82-95. In the following example we are using slot 88 to generate an RSA2048 key. Please be careful - the tool doesn't prompt you if you are about to overwrite an existing key! With that said, it's extremely unlikely anything is in slot 88 unless you've done this before, in which case you should already be well aware of this! 83 | 84 | With the `yubico-piv-tool` installed, we can run the following to generate the key, and save the public key to a pem formatted file: 85 | 86 | `yubico-piv-tool -s 88 -a generate -o new_root_ca_pubkey.pem` 87 | 88 | You can now create the root CA using this key, edit the config.json file to populate the following (this example is based on MacOS). For testing you may wish to populate the AWS credentials. For production, not. 89 | 90 | ``` 91 | { 92 | "Organisation": "PortSwigger", 93 | "Country": "UK", 94 | "CaName": "PortSwigger CA", 95 | "CaVersion": "2023", 96 | "OrgUnit": "SecEng", 97 | "City": "Knutsford", 98 | "County": "Cheshire", 99 | "SigningCert": "your_root_ca_cert.pem", 100 | "OCSPServer": "", 101 | "AwsRoleARN": "SOME_ARN_OF_ROLE_FROM_CLOUDFORMATION_OUTPUTS", 102 | "AwsMfaSerial": "SOME_AWS_ARN_OF_CREATED_MFA_TOKEN", 103 | "AwsDbTableName": "SOME_AWS_ARN_OF_CREATED_DYNAMODB_TABLE", 104 | "AwsRegion": "eu-west-1", 105 | "AwsAccessKey": "", 106 | "AwsSecretKey": "", 107 | "AwsTotpSecret": "", 108 | "P11Path": "/opt/homebrew/opt/yubico-piv-tool/lib/libykcs11.dylib", 109 | "P11TokenLabel": "", 110 | "P11Pin": "123456", 111 | "P11Slot": 0, 112 | } 113 | ``` 114 | 115 | You should now be able to create an x509 certificate for the root CA, using something like: 116 | 117 | Non Debug mode: 118 | `./certsquirt -ca -bootstrap -pubkey new_root_ca_pubkey.pem` 119 | 120 | Debug mode : 121 | 122 | `YKCS11_DBG=1 ./certsquirt -ca -bootstrap -pubkey new_root_ca_pubkey.pem -debug` 123 | 124 | Assuming this all went well, you should see the last few lines say something like `INFO: Successfully wrote out pem cert to `. Edit the config.json file, changing the `SigningCert` entry to point to the new pem certificate file. 125 | 126 | You may wish to continue this experiment, by creating a second key on the Yubikey. For example, to create another key in slot 89, simply run something like: 127 | 128 | `yubico-piv-tool -s 89 -a generate -o new_sub_ca_pubkey.pem` 129 | 130 | You should now be able to create the SubCA using this new key, by running something like: 131 | 132 | `./certsquirt -subca -subcaname "Yubikey Testing" -pubkey new_sub_ca_pubkey.pem` 133 | 134 | This will result in another sub-ca pem file being written. You can now swap out the `SigningCert` entry to point to this pem certificate file, resulting in any further signed certificates being signed by this subca/intermediate. 135 | 136 | To continue the demo, create a new csr using openssl or similar. To save you the effort of having to google this, something like the following will create a sub-standard csr (no SAN values), but will work for this example: 137 | 138 | `openssl req -new -newkey rsa:2048 -nodes -out sdfasdf.csr -keyout sdfasdf.key -subj "/C=GB/ST=asdfsdfsadf/L=asdfasdf/O=asdfasdf/OU=sdfasdfasdf/CN=sdfasdf"` 139 | 140 | With the new CSR, we should be able to issue our first certificate. 141 | 142 | If you run the following it will inspect the certificate request, and print out some details to the screen: 143 | 144 | `./certsquirt -csr sdfasdf.csr` 145 | 146 | and you should be able to sign it with: 147 | 148 | `./certsquirt -csr sdfasdf.csr -sign` 149 | 150 | You can then inspect the certificate using openssl or whatever you prefer, e.g. 151 | 152 | `openssl x509 -in sdfasdf.pem -text -noout | less` 153 | 154 | # 155 | ## AWS 156 | 157 | It would be far superior from a rigour perspective if this was a new AWS account, with nothing in it other than secured root user access, using hardware MFA (yubikey, etc). 158 | 159 | AWS KMS is very cheap - $1 per month per key. In the below deployment , you will need 2 keys ($1/per key/per month), versus the cost of AWS Private CA ($0.75 per certificate) and, *mostly*, with all the security features that provides. 160 | 161 | ### Cloudformation. 162 | 163 | In the cloudformation subdirectory is a [template](https://github.com/PortSwigger/certsquirt/blob/main/cloudformation/certsquirt.yaml) which will create the following: 164 | 165 | * A CA root key in KMS (RSA 4096) 166 | * A Sub CA key in KMS (RSA 4096) 167 | * A DynamoDB Table for the service 168 | * An IAM access role to be assumed via the app which has access to keys and DB 169 | * A user which is allowed to assume the role when authentication uses MFA. 170 | 171 | **Once executed, you need to find the user and then create** 172 | * an *access key*, and 173 | * a *software based* MFA token. 174 | 175 | The cloudformation script outputs the other variables you will need when creating the config.json file, to be copy/pasted into the config.json. 176 | 177 | * *Tip*: You can hardcode these credentials into the config.json, in case you are testing as generating mfa codes can be annoying. To do this, set the following variables within config.json: 178 | ``` 179 | "AwsAccessKey": "AKIAXXXXXXXXXXXXXXXX", 180 | "AwsSecretKey": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", 181 | "AwsTotpSecret": "BASE32SECRET", 182 | * If these are set, we will then generate TOTP codes for you at runtime. You may see errors when doing this though, as you can only use an MFA code precisely once every 30 seconds. 183 | 184 | 185 | ### Configuring AWS-KMS Mode Operations 186 | 187 | You'll need to then install and configure the `aws-kms-pkcs11` layer - a link is at the bottom of this page. 188 | 189 | You will need to create a config.json for aws-kms-pkcs11. If you kept the defaults in the cloudformation the label will be correct, and you just need to replace the kms_key_id in the following config, which should be created in either /etc/aws-kms-pkcs11/config.json or $XDG_CONFIG_HOME/aws-kms-pkcs11/config.json (note that XDG_CONFIG_HOME=$HOME/.config by default). 190 | 191 | ``` 192 | { 193 | "slots": [ 194 | { 195 | "label": "CertSquirt-Root-CA-Key", 196 | "kms_key_id": "d52cbc45-eeb9-4dc5-bc53-487b20e8ae7e", 197 | "aws_region": "eu-west-1" 198 | } 199 | ] 200 | } 201 | ``` 202 | 203 | Once you've configured everything... copy the root ca KMS key's public key to a file, and then you should now be able to create an x509 certificate for the root CA, using something like: 204 | 205 | Non Debug mode: 206 | 207 | `./certsquirt -ca -bootstrap -pubkey new_root_ca_pubkey.pem` 208 | 209 | Debug mode : 210 | 211 | `AWS_KMS_PKCS11_DEBUG=1 ./certsquirt -ca -bootstrap -pubkey new_root_ca_pubkey.pem -debug` 212 | 213 | Assuming this all went well, you should see the last few lines say something like `INFO: Successfully wrote out pem cert to `. Edit the config.json file, changing the `SigningCert` entry to point to the new pem certificate file. 214 | 215 | Once this has worked, you should then copy the second sub-ca public key from the AWS KMS console to somewhere, and be able to run the following to sign the sub ca key, which you wil: 216 | 217 | `./certsquirt -subca -subcaname "AWS SubCA Testing" -pubkey new_sub_ca_pubkey.pem` 218 | 219 | Change the config file once again, to change the `SigningCert` entry to point to the new pem certificate file. You should then be able to sign a CSR using something like: 220 | 221 | `./certsquirt -csr sdfasdf.csr -sign` 222 | 223 | You can then inspect the certificate using openssl or whatever you prefer, e.g. 224 | 225 | `openssl x509 -in sdfasdf.pem -text -noout | less` 226 | 227 | Once all this is working, create any more sub ca's you need, and once you've finished with this remove access to the root ca key by commenting out the [line](https://github.com/PortSwigger/certsquirt/blob/8c78d995856e4bbb3bb1a72d5c5dbf7561be2808/cloudformation/certsquirt.yaml#L165) in the cloudformation, and running it again. 228 | 229 | # 230 | # Software Prerequisites 231 | ## AWS C++ SDK 232 | 233 | You should follow the instructions at https://github.com/aws/aws-sdk-cpp in order to install the SDK. Assuming that all went ok, move onto the next section. 234 | 235 | ## Jack of Most Trades - aws-kms-pkcs11 236 | 237 | Next up, we will need to install https://github.com/JackOfMostTrades/aws-kms-pkcs11. Again, follow the instructions on this page. 238 | 239 | Pay close attention to https://github.com/JackOfMostTrades/aws-kms-pkcs11#configuration to ensure you have got things configured correctly, and that you can talk to your keys in KMS. 240 | 241 | ## Final Steps 242 | 243 | With these in place, you should be good to go. Checkout usage.txt which explains some of the first things you may wish to do... 244 | -------------------------------------------------------------------------------- /x509.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto" 5 | "crypto/ecdsa" 6 | "crypto/elliptic" 7 | "crypto/rand" 8 | "crypto/rsa" 9 | "crypto/sha256" 10 | "crypto/x509" 11 | "crypto/x509/pkix" 12 | "encoding/asn1" 13 | "encoding/pem" 14 | "fmt" 15 | "math/big" 16 | "os" 17 | "time" 18 | 19 | "github.com/ThalesGroup/crypto11" 20 | "github.com/pkg/errors" 21 | "github.com/sethvargo/go-password/password" 22 | ) 23 | 24 | // TODO - we should implement a CRL function 25 | func revokeCRT(crt *x509.Certificate) { 26 | // func CreateRevocationList(rand io.Reader, template *RevocationList, issuer *Certificate, priv crypto.Signer) ([]byte, error) 27 | // https://pkg.go.dev/crypto/x509#CreateRevocationList 28 | } 29 | 30 | // GenerateSubjectKeyID generates Subject Key Identifier (SKI) using SHA-256 31 | // hash of the public key bytes according to RFC 7093 section 2. 32 | func GenerateSubjectKeyID(pub crypto.PublicKey) ([]byte, error) { 33 | var pubBytes []byte 34 | var err error 35 | switch pub := pub.(type) { 36 | case *rsa.PublicKey: 37 | pubBytes, err = asn1.Marshal(*pub) 38 | if err != nil { 39 | return nil, err 40 | } 41 | case *ecdsa.PublicKey: 42 | pubBytes = elliptic.Marshal(pub.Curve, pub.X, pub.Y) 43 | default: 44 | return nil, errors.New("only ECDSA and RSA public keys are supported") 45 | } 46 | 47 | hash := sha256.Sum256(pubBytes) 48 | 49 | // According to RFC 7093, The keyIdentifier is composed of the leftmost 50 | // 160-bits of the SHA-256 hash of the value of the BIT STRING 51 | // subjectPublicKey (excluding the tag, length, and number of unused bits). 52 | return hash[:20], nil 53 | } 54 | 55 | func loadPemCert(filename string) (crt *x509.Certificate, err error) { 56 | bytes, err := os.ReadFile(filename) 57 | if err != nil { 58 | return crt, err 59 | } 60 | block, _ := pem.Decode(bytes) 61 | return x509.ParseCertificate(block.Bytes) 62 | 63 | } 64 | 65 | func loadCSR(filename string) (csr *x509.CertificateRequest, err error) { 66 | bytes, err := os.ReadFile(filename) 67 | if err != nil { 68 | return csr, err 69 | } 70 | block, _ := pem.Decode(bytes) 71 | return x509.ParseCertificateRequest(block.Bytes) 72 | } 73 | 74 | func loadPubKey(filename string) (key crypto.PublicKey, err error) { 75 | pemkey, err := os.ReadFile(filename) 76 | if err != nil { 77 | return key, err 78 | } 79 | block, _ := pem.Decode(pemkey) 80 | return x509.ParsePKIXPublicKey(block.Bytes) 81 | } 82 | 83 | func certToPem(crt []byte, fname string) error { 84 | block := &pem.Block{ 85 | Type: "CERTIFICATE", 86 | /* 87 | // this appears to break openssl. which says quite a bit about openssl pem parsing... 88 | Headers: map[string]string{ 89 | "Issued By": user.Username, 90 | }, 91 | */ 92 | Bytes: []byte(crt), 93 | } 94 | return os.WriteFile(fname, pem.EncodeToMemory(block), 0644) 95 | } 96 | 97 | func newSerialNumber() (*big.Int, error) { 98 | // ensure we have a decent serial instead of '1' or '42' 99 | serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) 100 | return rand.Int(rand.Reader, serialNumberLimit) 101 | } 102 | 103 | // This function call should be executed precisely once, to generate the root CA. 104 | // This is effectively creating a 'self-signed' cert. 105 | func createRootCA(signer crypto11.Signer) bool { 106 | logger := GetLogger() 107 | id, err := GenerateSubjectKeyID(signer.Public()) 108 | if err != nil { 109 | logger.Error("Cannot generate subject key ID for root CA", "error", err) 110 | return false 111 | } 112 | // this should only ever be used once, well maybe twice. 113 | // but definately we should have another solution by then. 114 | name := &pkix.Name{} 115 | name.Country = []string{config.Country} 116 | name.Organization = []string{config.Organisation} 117 | name.OrganizationalUnit = []string{config.OrgUnit} 118 | name.CommonName = config.CaName + " - " + config.CaVersion 119 | 120 | tmpl := &x509.Certificate{ 121 | IsCA: true, 122 | BasicConstraintsValid: true, 123 | SerialNumber: big.NewInt(31337), 124 | Subject: *name, 125 | NotBefore: time.Now().Add(time.Second * -600).UTC(), 126 | NotAfter: time.Now().AddDate(0, 0, 3650).UTC(), 127 | SubjectKeyId: id, 128 | KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign | x509.KeyUsageCRLSign, 129 | // according to https://cabforum.org/wp-content/uploads/CA-Browser-Forum-BR-1.8.7.pdf 130 | // we shouldn't set this. See 7.1.2.1.b Root CA Certificate for details 131 | //ExtKeyUsage: []x509.ExtKeyUsage{ 132 | // x509.ExtKeyUsageOCSPSigning, 133 | //}, 134 | SignatureAlgorithm: x509.SHA512WithRSA, 135 | // as above, should not be set 7.1.2.1.a for details 136 | //MaxPathLen: 1, 137 | // TODO? 138 | //OCSPServer: []string{"https://ocsp.security.portswigger.internal"}, 139 | } 140 | if config.CaAiaRootURL != "" { 141 | tmpl.IssuingCertificateURL = append(tmpl.IssuingCertificateURL, config.CaAiaRootURL) 142 | } 143 | if config.OCSPServer != "" { 144 | tmpl.OCSPServer = append(tmpl.OCSPServer, config.OCSPServer) 145 | } 146 | // chomp chomp. 147 | crtBytes, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, signer.Public().(*rsa.PublicKey), signer) 148 | if err != nil { 149 | logger.Error("Certificate signing failed for root CA", "error", err, "subject", name.CommonName) 150 | return false 151 | } 152 | pemBlock := &pem.Block{ 153 | Type: "CERTIFICATE", 154 | Bytes: crtBytes, 155 | } 156 | file, err := os.OpenFile(name.CommonName+".pem", os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600) 157 | if err != nil { 158 | logger.Error("Cannot create PEM certificate file", 159 | "error", err, 160 | "filename", name.CommonName+".pem", 161 | "subject", name.CommonName) 162 | return false 163 | } 164 | defer file.Close() 165 | out := pem.EncodeToMemory(pemBlock) 166 | if _, err := file.Write(out); err != nil { 167 | file.Close() 168 | os.Remove(name.CommonName + ".pem") 169 | logger.Error("Cannot write PEM certificate file", 170 | "error", err, 171 | "filename", name.CommonName+".pem", 172 | "subject", name.CommonName) 173 | return false 174 | } 175 | logger.Info("Successfully wrote PEM certificate file", 176 | "filename", name.CommonName+".pem", 177 | "subject", name.CommonName) 178 | // now write out the DER bytes to a crt file 179 | err = os.WriteFile(name.CommonName+".crt", crtBytes, 0644) 180 | if err != nil { 181 | logger.Error("Cannot write CRT certificate file", 182 | "error", err, 183 | "filename", name.CommonName+".crt", 184 | "subject", name.CommonName) 185 | return false 186 | } 187 | logger.Info("Successfully wrote CRT certificate file", 188 | "filename", name.CommonName+".crt", 189 | "subject", name.CommonName) 190 | err = addDbRecord(crtBytes) 191 | if err != nil { 192 | logger.Warn("Failed to add certificate to database - please investigate", 193 | "error", err, 194 | "subject", name.CommonName) 195 | } 196 | 197 | AuditEvent("root_ca_creation", true, 198 | "subject", name.CommonName, 199 | "serial", tmpl.SerialNumber.String(), 200 | "not_after", tmpl.NotAfter, 201 | "pem_file", name.CommonName+".pem", 202 | "crt_file", name.CommonName+".crt") 203 | 204 | return true 205 | } 206 | 207 | func createIntermediateCert(signer crypto11.Signer, intpubkey crypto.PublicKey, subjectname string) (caName string, ok bool) { 208 | logger := GetLogger() 209 | // need to read in pubkey 210 | id, err := GenerateSubjectKeyID(intpubkey) 211 | if err != nil { 212 | logger.Error("Cannot generate subject key ID for intermediate CA", "error", err) 213 | return "", false 214 | } 215 | 216 | // generate a unique identifier for the CA to go into the Subject name: 217 | uid, err := password.Generate(6, 2, 0, false, false) 218 | if err != nil { 219 | logger.Error("Cannot generate unique identifier for intermediate CA", "error", err) 220 | return "", false 221 | } 222 | name := &pkix.Name{} 223 | name.Country = []string{config.Country} 224 | name.Organization = []string{config.Organisation} 225 | name.OrganizationalUnit = []string{config.OrgUnit} 226 | name.CommonName = config.Organisation + " - Sub CA - " + subjectname + " - " + uid 227 | 228 | serialNumber, err := newSerialNumber() 229 | if err != nil { 230 | logger.Error("Cannot generate serial number for intermediate CA", "error", err) 231 | return "", false 232 | } 233 | 234 | tmpl := &x509.Certificate{ 235 | IsCA: true, 236 | BasicConstraintsValid: true, 237 | SerialNumber: serialNumber, 238 | Subject: *name, 239 | NotBefore: time.Now().Add(time.Second * -600).UTC(), 240 | NotAfter: time.Now().AddDate(0, 0, 1825).UTC(), 241 | SubjectKeyId: id, 242 | KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign | x509.KeyUsageCRLSign, 243 | 244 | // you may want to undefine this - if you do any end user certs signed by this cert will be scoped 245 | // to only those usages below. Of course, this may be what you wish to achieve. 246 | // More details here: https://cabforum.org/wp-content/uploads/CA-Browser-Forum-BR-1.8.7.pdf 247 | // See section 7.1.2.2 Subordinate CA Certificate item g. 248 | //ExtKeyUsage: []x509.ExtKeyUsage{ 249 | // x509.ExtKeyUsageOCSPSigning, x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth, 250 | //}, 251 | SignatureAlgorithm: x509.SHA512WithRSA, 252 | // define this here. 253 | MaxPathLen: 0, 254 | // TODO? 255 | //OCSPServer: []string{"https://ocsp.security.portswigger.internal"}, 256 | } 257 | if config.CaAiaRootURL != "" { 258 | tmpl.IssuingCertificateURL = append(tmpl.IssuingCertificateURL, config.CaAiaRootURL) 259 | } 260 | if config.OCSPServer != "" { 261 | tmpl.OCSPServer = append(tmpl.OCSPServer, config.OCSPServer) 262 | } 263 | // we need the upstream cert 264 | if _, err := os.Stat(flCaCertFile); os.IsNotExist(err) { 265 | // path/to/whatever does not exist 266 | logger.Error("Parent CA certificate file not found", 267 | "error", err, 268 | "ca_cert_file", flCaCertFile) 269 | return "", false 270 | } 271 | 272 | cacert, err := loadPemCert(flCaCertFile) 273 | if err != nil { 274 | logger.Error("Cannot read parent CA certificate", 275 | "error", err, 276 | "ca_cert_file", flCaCertFile) 277 | return "", false 278 | } 279 | 280 | logger.Info("Signing intermediate certificate", 281 | "signing_ca", cacert.Subject.CommonName, 282 | "intermediate_subject", name.CommonName) 283 | 284 | crtBytes, err := x509.CreateCertificate(rand.Reader, tmpl, cacert, intpubkey, signer) 285 | if err != nil { 286 | logger.Error("Certificate signing failed for intermediate CA", 287 | "error", err, 288 | "intermediate_subject", name.CommonName, 289 | "signing_ca", cacert.Subject.CommonName) 290 | return "", false 291 | } 292 | pemBlock := &pem.Block{ 293 | Type: "CERTIFICATE", 294 | Bytes: crtBytes, 295 | } 296 | file, err := os.OpenFile(name.CommonName+".pem", os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600) 297 | if err != nil { 298 | logger.Error("Cannot create PEM certificate file for intermediate CA", 299 | "error", err, 300 | "filename", name.CommonName+".pem", 301 | "subject", name.CommonName) 302 | return "", false 303 | } 304 | defer file.Close() 305 | out := pem.EncodeToMemory(pemBlock) 306 | if _, err := file.Write(out); err != nil { 307 | file.Close() 308 | os.Remove(name.CommonName + ".pem") 309 | logger.Error("Cannot write PEM certificate file for intermediate CA", 310 | "error", err, 311 | "filename", name.CommonName+".pem", 312 | "subject", name.CommonName) 313 | return "", false 314 | } 315 | logger.Info("Successfully wrote PEM certificate file for intermediate CA", 316 | "filename", name.CommonName+".pem", 317 | "subject", name.CommonName) 318 | // now write out the DER bytes to a crt file 319 | err = os.WriteFile(name.CommonName+".crt", crtBytes, 0644) 320 | if err != nil { 321 | logger.Error("Cannot write CRT certificate file for intermediate CA", 322 | "error", err, 323 | "filename", name.CommonName+".crt", 324 | "subject", name.CommonName) 325 | return "", false 326 | } 327 | logger.Info("Successfully wrote CRT certificate file for intermediate CA", 328 | "filename", name.CommonName+".crt", 329 | "subject", name.CommonName) 330 | // now add the cert to the db 331 | err = addDbRecord(crtBytes) 332 | if err != nil { 333 | logger.Warn("Failed to add intermediate certificate to database", 334 | "error", err, 335 | "subject", name.CommonName) 336 | } 337 | 338 | AuditEvent("intermediate_ca_creation", true, 339 | "subject", name.CommonName, 340 | "signing_ca", cacert.Subject.CommonName, 341 | "serial", serialNumber.String(), 342 | "not_after", tmpl.NotAfter, 343 | "pem_file", name.CommonName+".pem", 344 | "crt_file", name.CommonName+".crt") 345 | 346 | return name.CommonName, true 347 | } 348 | 349 | func signCSR(signer crypto11.Signer, csr *x509.CertificateRequest) (crtBytes []byte, err error) { 350 | logger := GetLogger() 351 | serialNumber, err := newSerialNumber() 352 | if err != nil { 353 | logger.Error("Cannot generate serial number for CSR certificate", "error", err) 354 | return nil, err 355 | } 356 | id, err := GenerateSubjectKeyID(csr.PublicKey) 357 | if err != nil { 358 | logger.Error("Cannot generate subject key ID for CSR certificate", "error", err) 359 | return nil, err 360 | } 361 | // overwrite some of the values in the cert. 362 | var newSubject pkix.Name 363 | newSubject.Country = append(newSubject.Country, config.Country) 364 | newSubject.Organization = append(newSubject.Organization, config.Organisation) 365 | newSubject.Locality = append(newSubject.Locality, config.City) 366 | newSubject.Province = append(newSubject.Province, config.County) 367 | 368 | // copy *just* this from the original request 369 | newSubject.CommonName = csr.Subject.CommonName 370 | // pay no attention to the man on the mountain. 371 | xx, _ := asn1.Marshal("WC1GYWNlOiAkP2omdGtsMGhydVBmTnJuQVFPQUFnJ2V1YFxkYCZVQT02NFN1WVZTTU9NUFYsfCdNKD9seEV4Rno4cFpRXFFOaHU7YDB9fQogOkw5Qkx5QX1mfi1yVUN+Q1VDcCQtPiVBcUpRa15CJHZUMmoxbkhsO2ByOlgiNjddVXRGVWxqMXElZF1adW42cGteS24kXSwvLSFAPkVpCiAyci0idScoIVVaNndLSSR4cWBLUS55VTRHZCRWIy16el0/V1U0cUcvSDI7J09WJVJcUTJmQjdUMj5eVDtjWTZXbU1FCg==") 372 | foo := pkix.Extension{ 373 | Id: asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 13, 37}, 374 | Critical: false, 375 | Value: xx, 376 | } 377 | yy, _ := asn1.Marshal("aHR0cHM6Ly93d3cuY3MuY211LmVkdS9+cmRyaWxleS80ODcvcGFwZXJzL1Rob21wc29uXzE5ODRfUmVmbGVjdGlvbnNvblRydXN0aW5nVHJ1c3QucGRmCg==") 378 | bar := pkix.Extension{ 379 | Id: asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 13, 38}, 380 | Critical: false, 381 | Value: yy, 382 | } 383 | 384 | tmpl := &x509.Certificate{ 385 | SerialNumber: serialNumber, 386 | Subject: newSubject, 387 | NotBefore: time.Now().Add(time.Second * -600).UTC(), 388 | NotAfter: time.Now().AddDate(0, 0, flTtl), 389 | SubjectKeyId: id, 390 | KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyAgreement | x509.KeyUsageKeyEncipherment | x509.KeyUsageDataEncipherment, 391 | ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}, 392 | SignatureAlgorithm: csr.SignatureAlgorithm, 393 | DNSNames: csr.DNSNames, 394 | EmailAddresses: csr.EmailAddresses, 395 | IPAddresses: csr.IPAddresses, 396 | URIs: csr.URIs, 397 | } 398 | if flOcspSigner { 399 | tmpl.ExtKeyUsage = append(tmpl.ExtKeyUsage, x509.ExtKeyUsageOCSPSigning) 400 | } 401 | tmpl.ExtraExtensions = []pkix.Extension{bar, foo} 402 | // if config.CaAiaIssuerURL != "" { 403 | // tmpl.IssuingCertificateURL = append(tmpl.IssuingCertificateURL, config.CaAiaIssuerURL) 404 | // } 405 | if config.CaAiaRootURL != "" { 406 | tmpl.IssuingCertificateURL = append(tmpl.IssuingCertificateURL, config.CaAiaRootURL) 407 | } 408 | if config.OCSPServer != "" { 409 | tmpl.OCSPServer = append(tmpl.OCSPServer, config.OCSPServer) 410 | } 411 | 412 | // we need the upstream cert 413 | if _, err := os.Stat(flCaCertFile); os.IsNotExist(err) { 414 | // path/to/whatever does not exist 415 | logger.Error("Parent CA certificate file not found for CSR signing", 416 | "error", err, 417 | "ca_cert_file", flCaCertFile) 418 | return nil, err 419 | } 420 | 421 | if flDebug { 422 | logger.Debug("Loading CA certificate for CSR signing", "ca_cert_file", flCaCertFile) 423 | } 424 | cacert, err := loadPemCert(flCaCertFile) 425 | if err != nil { 426 | logger.Error("Cannot read CA certificate for CSR signing", 427 | "error", err, 428 | "ca_cert_file", flCaCertFile) 429 | return nil, err 430 | } 431 | 432 | logger.Info("Signing certificate request", 433 | "signing_ca", cacert.Subject.CommonName, 434 | "csr_subject", csr.Subject.CommonName) 435 | 436 | if flDebug { 437 | logger.Debug("Certificate signing details", 438 | "signer_type", fmt.Sprintf("%T", signer), 439 | "target_key_type", fmt.Sprintf("%T", csr.PublicKey), 440 | "target_key_id", fmt.Sprintf("%x", id)) 441 | signerid, err := GenerateSubjectKeyID(signer.Public()) 442 | if err != nil { 443 | logger.Error("Cannot generate subject key ID for signing key", "error", err) 444 | } else { 445 | logger.Debug("Signing key details", "signer_key_id", fmt.Sprintf("%x", signerid)) 446 | } 447 | } 448 | 449 | crtBytes, err = x509.CreateCertificate(rand.Reader, tmpl, cacert, csr.PublicKey, signer) 450 | if err != nil { 451 | logger.Error("Certificate creation/signing failed", 452 | "error", err, 453 | "csr_subject", csr.Subject.CommonName, 454 | "ca_subject", cacert.Subject.CommonName, 455 | "suggestion", "verify config file points to correct CA certificate") 456 | return nil, err 457 | } 458 | 459 | // Add certificate to database 460 | err = addDbRecord(crtBytes) 461 | if err != nil { 462 | logger.Warn("Failed to add certificate to database", 463 | "error", err, 464 | "csr_subject", csr.Subject.CommonName) 465 | // Continue anyway, don't fail the signing operation 466 | } 467 | 468 | logger.Info("Certificate signing completed successfully", 469 | "csr_subject", csr.Subject.CommonName, 470 | "ca_subject", cacert.Subject.CommonName, 471 | "serial", serialNumber.String()) 472 | 473 | AuditEvent("certificate_signed", true, 474 | "csr_subject", csr.Subject.CommonName, 475 | "ca_subject", cacert.Subject.CommonName, 476 | "serial", serialNumber.String(), 477 | "not_after", tmpl.NotAfter) 478 | 479 | return crtBytes, nil 480 | } 481 | 482 | func prettyPrintCSR(csr *x509.CertificateRequest) { 483 | logger := GetLogger() 484 | logger.Info("******************************************************************") 485 | logger.Info("*** CERTIFICATE SIGNING REQUEST INFORMATION ***") 486 | logger.Info("******************************************************************") 487 | 488 | switch csr.SignatureAlgorithm { 489 | case x509.SHA256WithRSA: 490 | case x509.SHA384WithRSA: 491 | case x509.SHA512WithRSA: 492 | case x509.ECDSAWithSHA256: 493 | case x509.ECDSAWithSHA384: 494 | case x509.ECDSAWithSHA512: 495 | case x509.SHA256WithRSAPSS: 496 | case x509.SHA384WithRSAPSS: 497 | case x509.SHA512WithRSAPSS: 498 | case x509.PureEd25519: 499 | default: 500 | logger.Error("Signature algorithm not supported by this CA", 501 | "algorithm", csr.SignatureAlgorithm, 502 | "subject", csr.Subject.CommonName) 503 | os.Exit(1) 504 | } 505 | 506 | logger.Info("Certificate signing request details", 507 | "signature_algorithm", csr.SignatureAlgorithm.String(), 508 | "public_key_algorithm", csr.PublicKeyAlgorithm.String(), 509 | "subject", csr.Subject.CommonName, 510 | "dns_names", csr.DNSNames, 511 | "email_addresses", csr.EmailAddresses, 512 | "ip_addresses", csr.IPAddresses, 513 | "uris", csr.URIs) 514 | } 515 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/rand" 5 | "crypto/rsa" 6 | "crypto/x509" 7 | "encoding/json" 8 | "encoding/pem" 9 | "flag" 10 | "fmt" 11 | "os" 12 | "strings" 13 | "time" 14 | 15 | _ "embed" 16 | 17 | "github.com/sethvargo/go-password/password" 18 | ) 19 | 20 | type Config struct { 21 | Organisation string `json:""` 22 | Country string `json:""` 23 | CaName string `json:""` 24 | CaVersion string `json:""` 25 | //CaAiaIssuerURL string `json:""` 26 | CaAiaRootURL string `json:""` 27 | OrgUnit string `json:""` 28 | City string `json:""` 29 | County string `json:""` 30 | OCSPServer string `json:""` 31 | AwsRoleARN string `json:""` 32 | AwsMfaSerial string `json:""` 33 | AwsDbTableName string `json:""` 34 | AwsAccessKey string `json:""` 35 | AwsSecretKey string `json:""` 36 | AwsTotpSecret string `json:""` 37 | AwsRegion string `json:""` 38 | Path string `json:"P11Path"` 39 | TokenLabel string `json:"P11TokenLabel"` 40 | Pin string `json:"P11Pin"` 41 | SlotNumber int `json:"P11Slot"` 42 | SigningCert string `json:""` 43 | // Logging configuration 44 | LogLevel string `json:"LogLevel,omitempty"` // DEBUG, INFO, WARN, ERROR 45 | LogFormat string `json:"LogFormat,omitempty"` // json or text 46 | AuditLogFile string `json:"AuditLogFile,omitempty"` // path to audit log file 47 | } 48 | 49 | var config Config 50 | 51 | // command line flags/arguments 52 | var flShowVersion, flCA, flSign, flSubCa, flBootstrap, flUsage, flDebug, flGenPrivKey, flOcspSigner bool 53 | var flCSR, flPubKey, flCaCertFile, flInterName, flConfig string 54 | var flTtl int 55 | 56 | var keypassword string 57 | var buildstamp, githash string // For versioning, via go build -v -x -a -ldflags "-X main.buildstamp=`date -u '+%Y-%m-%d_%I:%M:%S%p'` -X main.githash=`git rev-parse HEAD`" || exit'` 58 | 59 | //go:embed VERSION 60 | var version string 61 | 62 | func main() { 63 | var err error 64 | // Root CA Based operations 65 | flag.BoolVar(&flCA, "ca", false, "create a ca. also requires -bootstrap to confirm.") 66 | flag.BoolVar(&flBootstrap, "bootstrap", false, "required to confirm you really want to bootstrap the ca. you almost certainly don't want to do this.") 67 | 68 | // CSR Based operations 69 | flag.StringVar(&flCSR, "csr", "", "read from filename and sign/inspect a csr from another system.") 70 | flag.BoolVar(&flSign, "sign", false, "actually issue a certificate from a csr.") 71 | flag.BoolVar(&flOcspSigner, "ocspsigner", false, "*dangerous* - indicate this certificate can be used for OCSP signing. Be sure you need this!") 72 | 73 | // Intermediate based operations 74 | flag.BoolVar(&flSubCa, "subca", false, "indicates you want to sign another CA's CSR (ca:true) to authorize it's operation as a sub CA. Use with 'bootstrap' flag to create one automatically.") 75 | flag.StringVar(&flInterName, "subcaname", "", "extend the SubjectName, to identify an intermediate certificate (e.g. Fortinet Edge FW-A)") 76 | flag.StringVar(&flPubKey, "pubkey", "", "filename to pass in a pem formatted rsa key from aws kms or similar") 77 | 78 | // Other options which may be needed in the above 79 | flag.StringVar(&flCaCertFile, "cacert", "", "when signing a cert, you must provide the *pem* formatted parent certificate filename. this can also be defined in config.json.") 80 | flag.BoolVar(&flGenPrivKey, "genkey", false, "generate an rsa private key to be used in the operation, e.g. for import into another system.") 81 | flag.BoolVar(&flUsage, "usage", false, "show more detailed usage and examples.") 82 | flag.BoolVar(&flDebug, "debug", false, "show more stuff about what's happening.") 83 | // config 84 | flag.StringVar(&flConfig, "config", "config.json", "override the default config file to be used.") 85 | flag.IntVar(&flTtl, "ttl", 365, "override the default 365 days for an issued cert.") 86 | flag.BoolVar(&flShowVersion, "version", false, "Show version information, and quit.") 87 | flag.Parse() 88 | if flShowVersion { 89 | // Initialize basic logging for version output 90 | InitLogging(LogConfig{Level: "INFO", Format: "text"}, flDebug) 91 | logger := GetLogger() 92 | if buildstamp != "" && githash != "" { 93 | logger.Info("Version information", 94 | "version", version, 95 | "buildstamp", buildstamp, 96 | "githash", githash) 97 | } else { 98 | logger.Info("Version information", 99 | "version", version, 100 | "type", "release") 101 | } 102 | os.Exit(0) 103 | } 104 | if flUsage { 105 | usage() 106 | os.Exit(0) 107 | } 108 | // XDG_CONFIG_HOME=$HOME/.config by default. 109 | // try and load our config file from some locations.... 110 | var confFile string 111 | if flConfig != "config.json" { // user has overridden default 112 | confFile = flConfig 113 | // Will log after logger is initialized 114 | } else if _, err := os.Stat(os.Getenv("XDG_CONFIG_HOME") + "/.certsquirt/config.json"); err == nil { 115 | confFile = os.Getenv("XDG_CONFIG_HOME") + "/.certsquirt/config.json" 116 | } else if _, err := os.Stat("config.json"); err == nil { 117 | confFile = "config.json" 118 | } else { 119 | // Initialize basic logging for error output before config is loaded 120 | InitLogging(LogConfig{Level: "ERROR", Format: "text"}, false) 121 | logger := GetLogger() 122 | logger.Error("Cannot find configuration file", 123 | "default_config", flConfig, 124 | "suggestion", "specify config file with -config argument") 125 | os.Exit(1) 126 | } 127 | confJson, err := os.ReadFile(confFile) 128 | if err != nil { 129 | // Initialize basic logging for error output before config is loaded 130 | InitLogging(LogConfig{Level: "ERROR", Format: "text"}, false) 131 | logger := GetLogger() 132 | logger.Error("Cannot open configuration file", 133 | "error", err, 134 | "config_file", confFile) 135 | os.Exit(1) 136 | } 137 | err = json.Unmarshal(confJson, &config) 138 | if err != nil { 139 | // Initialize basic logging for error output before config is loaded 140 | InitLogging(LogConfig{Level: "ERROR", Format: "text"}, false) 141 | logger := GetLogger() 142 | logger.Error("Malformed configuration file", 143 | "error", err, 144 | "config_file", confFile) 145 | os.Exit(1) 146 | } 147 | 148 | // Initialize structured logging 149 | logConfig := LogConfig{ 150 | Level: config.LogLevel, 151 | Format: config.LogFormat, 152 | AuditFile: config.AuditLogFile, 153 | } 154 | InitLogging(logConfig, flDebug) 155 | 156 | logger := GetLogger() 157 | 158 | // Log which config file is being used 159 | if flConfig != "config.json" { 160 | logger.Info("Using config file specified on command line", 161 | "config_file", confFile) 162 | } else if confFile == os.Getenv("XDG_CONFIG_HOME")+"/.certsquirt/config.json" { 163 | logger.Info("Using XDG config directory", 164 | "config_file", confFile) 165 | } else { 166 | logger.Info("Using config file from current directory", 167 | "config_file", confFile) 168 | } 169 | 170 | if flDebug { 171 | logger.Debug("Configuration loaded successfully", 172 | "config_file", confFile, 173 | "config", config) 174 | } 175 | if flCaCertFile == "" && config.SigningCert != "" { 176 | // set flCaCertFile to use the defined file in the json config 177 | flCaCertFile = config.SigningCert 178 | } 179 | 180 | // right logically go through what the user might want to do... 181 | if flCSR != "" { 182 | logger.Info("Inspecting certificate signing request", "csr_file", flCSR) 183 | csr, err := loadCSR(flCSR) 184 | if err != nil { 185 | logger.Error("Certificate signing request is invalid", "error", err, "csr_file", flCSR) 186 | os.Exit(1) 187 | } 188 | // right. don;t make a fatal mistake. 189 | err = csr.CheckSignature() 190 | if err != nil { 191 | logger.Error("SIGNATURE MISMATCH ON CSR - REFUSING TO CONTINUE", 192 | "csr_file", flCSR, 193 | "error", err) 194 | AuditEvent("csr_validation", false, 195 | "csr_file", flCSR, 196 | "reason", "signature_mismatch") 197 | os.Exit(1) 198 | } 199 | AuditEvent("csr_validation", true, 200 | "csr_file", flCSR, 201 | "subject", csr.Subject.CommonName) 202 | prettyPrintCSR(csr) 203 | if !flSubCa && !flSign { 204 | os.Exit(0) 205 | } 206 | } 207 | if !flCA && !flSubCa && !flSign && flCSR == "" { 208 | logger.Info("No operation specified - use -help for available options") 209 | usage() 210 | os.Exit(0) 211 | } 212 | 213 | // As we use DynamoDB, we will always need to auth to AWS to pop records there. 214 | creds := assumeRole() 215 | 216 | // the pkcs11 provider knows nothing about our credentials, indeed it is just a 217 | // library on disk - as a result we need to set our environment variables here, 218 | // so that future calls will use our new creds. 219 | // see: https://github.com/JackOfMostTrades/aws-kms-pkcs11#configuration 220 | os.Setenv("AWS_ACCESS_KEY_ID", *creds.AccessKeyId) 221 | os.Setenv("AWS_SECRET_ACCESS_KEY", *creds.SecretAccessKey) 222 | os.Setenv("AWS_SESSION_TOKEN", *creds.SessionToken) 223 | os.Setenv("AWS_DEFAULT_REGION", "eu-west-1") 224 | // TODO AWS_ROLE_SESSION_NAME? 225 | 226 | // most likely we're being used to simply sign a csr, so start there... 227 | if flSign && flCSR != "" { 228 | logger.Info("Starting certificate signing operation", 229 | "csr_file", flCSR, 230 | "ca_cert", flCaCertFile) 231 | 232 | csr, err := loadCSR(flCSR) 233 | if err != nil { 234 | logger.Error("Cannot load certificate signing request", 235 | "error", err, 236 | "csr_file", flCSR) 237 | os.Exit(1) 238 | } 239 | if flCaCertFile == "" { 240 | logger.Error("No signing CA certificate provided - use -cacert flag") 241 | os.Exit(1) 242 | } 243 | 244 | signingCert, err := loadPemCert(flCaCertFile) 245 | if err != nil { 246 | logger.Error("Cannot load CA certificate", 247 | "error", err, 248 | "ca_cert_file", flCaCertFile) 249 | os.Exit(1) 250 | } 251 | signer, err := initPkcs11(signingCert.PublicKey.(*rsa.PublicKey)) 252 | if err != nil { 253 | logger.Error("PKCS11 configuration failed", 254 | "error", err, 255 | "library_path", config.Path, 256 | "suggestions", []string{ 257 | "Verify PKCS11 configuration file exists", 258 | "Check if hardware token is connected", 259 | "Confirm PKCS11 library path is accessible", 260 | }) 261 | AuditEvent("pkcs11_init", false, 262 | "error", err.Error(), 263 | "library_path", config.Path) 264 | os.Exit(1) 265 | } 266 | 267 | AuditEvent("certificate_signing_started", true, 268 | "csr_subject", csr.Subject.CommonName, 269 | "ca_subject", signingCert.Subject.CommonName) 270 | 271 | crtBytes, err := signCSR(signer, csr) 272 | if err != nil { 273 | logger.Error("Certificate signing failed", 274 | "error", err, 275 | "csr_subject", csr.Subject.CommonName) 276 | AuditEvent("certificate_signing", false, 277 | "csr_subject", csr.Subject.CommonName, 278 | "error", err.Error()) 279 | os.Exit(1) 280 | } 281 | 282 | certFilename := csr.Subject.CommonName 283 | logger.Info("Writing certificate files", 284 | "subject", csr.Subject.CommonName, 285 | "pem_file", certFilename+".pem", 286 | "crt_file", certFilename+".crt") 287 | 288 | err = certToPem(crtBytes, certFilename+".pem") 289 | if err != nil { 290 | logger.Error("Cannot save PEM certificate file", 291 | "error", err, 292 | "filename", certFilename+".pem") 293 | os.Exit(1) 294 | } 295 | err = os.WriteFile(certFilename+".crt", crtBytes, 0644) 296 | if err != nil { 297 | logger.Error("Cannot save CRT certificate file", 298 | "error", err, 299 | "filename", certFilename+".crt") 300 | os.Exit(1) 301 | } 302 | 303 | AuditEvent("certificate_signing", true, 304 | "csr_subject", csr.Subject.CommonName, 305 | "ca_subject", signingCert.Subject.CommonName, 306 | "pem_file", certFilename+".pem", 307 | "crt_file", certFilename+".crt") 308 | 309 | logger.Info("Certificate signed successfully", 310 | "subject", csr.Subject.CommonName, 311 | "pem_file", certFilename+".pem", 312 | "crt_file", certFilename+".crt") 313 | os.Exit(0) 314 | } 315 | // catch a missing public key 316 | if flCA && flBootstrap && flPubKey == "" { 317 | logger.Error("Public key required for root CA bootstrap operation - use -pubkey flag") 318 | os.Exit(1) 319 | } 320 | if flCA && flBootstrap && flPubKey != "" { 321 | logger.Warn("ROOT CA BOOTSTRAP OPERATION REQUESTED") 322 | logger.Warn("DO NOT PROCEED UNLESS YOU ARE ABSOLUTELY SURE") 323 | logger.Warn("YOU WANT TO CREATE A NEW ROOT CA") 324 | logger.Warn("Hit Ctrl+C to cancel this operation") 325 | logger.Info("Waiting 5 seconds before proceeding...") 326 | 327 | time.Sleep(5 * time.Second) 328 | // stop this being used in a script via `expect' or similar 329 | challengeString, err := password.Generate(20, 2, 0, false, false) 330 | if err != nil { 331 | logger.Error("Cannot generate challenge string", "error", err) 332 | os.Exit(1) 333 | } 334 | var challengeResponse string 335 | 336 | logger.Info("Authentication challenge required") 337 | logger.Info("Enter the following text *exactly* as it is shown:") 338 | fmt.Printf("\t\t%v\n", challengeString) 339 | fmt.Printf("\t\tRESPONSE: -> ") 340 | fmt.Scanln(&challengeResponse) 341 | if strings.Compare(challengeResponse, challengeString) != 0 { 342 | logger.Error("INCORRECT CHALLENGE RESPONSE - OPERATION CANCELLED") 343 | AuditEvent("root_ca_bootstrap", false, 344 | "reason", "challenge_failed", 345 | "pubkey_file", flPubKey) 346 | os.Exit(10) 347 | } 348 | // flPubKey should point to a file on disk we can consume to grab out the rsa public key, to allow us to find the 349 | // corresponding signer... 350 | pubkey, err := loadPubKey(flPubKey) 351 | if err != nil { 352 | logger.Error("Cannot load public key", "error", err, "pubkey_file", flPubKey) 353 | os.Exit(1) 354 | } 355 | if flDebug { 356 | logger.Debug("Loading matching private key", 357 | "pubkey_type", fmt.Sprintf("%T", pubkey), 358 | "pubkey_file", flPubKey) 359 | } 360 | 361 | signer, err := initPkcs11(pubkey.(*rsa.PublicKey)) 362 | if err != nil { 363 | logger.Error("PKCS11 initialization failed for root CA bootstrap", 364 | "error", err, 365 | "pubkey_file", flPubKey, 366 | "troubleshooting", []string{ 367 | "Check PIN is correct", 368 | "Verify slot number is correct", 369 | "Confirm PKCS11 library path", 370 | "For KMS: set AWS_KMS_PKCS11_DEBUG=1", 371 | "For YubiKey: set YKCS11_DBG=9 or YKCS11_DBG=1", 372 | }) 373 | AuditEvent("root_ca_bootstrap", false, 374 | "reason", "pkcs11_init_failed", 375 | "pubkey_file", flPubKey, 376 | "error", err.Error()) 377 | os.Exit(1) 378 | } 379 | 380 | AuditEvent("root_ca_bootstrap", true, 381 | "operation", "started", 382 | "pubkey_file", flPubKey) 383 | 384 | ok := createRootCA(signer) 385 | if ok { 386 | logger.Info("Root CA created successfully") 387 | AuditEvent("root_ca_bootstrap", true, 388 | "operation", "completed", 389 | "pubkey_file", flPubKey) 390 | os.Exit(0) 391 | } else { 392 | logger.Error("Root CA creation failed unexpectedly") 393 | AuditEvent("root_ca_bootstrap", false, 394 | "reason", "creation_failed", 395 | "pubkey_file", flPubKey) 396 | os.Exit(1) 397 | } 398 | } 399 | if flSubCa && flPubKey != "" && !flGenPrivKey { 400 | if flInterName == "" { 401 | logger.Error("SubCA friendly name required - use -subcaname flag (e.g. -subcaname 'Apple TV Devices')") 402 | os.Exit(1) 403 | } 404 | if flCaCertFile == "" { 405 | logger.Error("Signing CA certificate required - use -cacert flag") 406 | os.Exit(1) 407 | } 408 | 409 | logger.Info("Creating intermediate CA certificate", 410 | "subca_name", flInterName, 411 | "pubkey_file", flPubKey, 412 | "ca_cert_file", flCaCertFile) 413 | 414 | pubkey, err := loadPubKey(flPubKey) 415 | if err != nil { 416 | logger.Error("Cannot load public key for SubCA", 417 | "error", err, 418 | "pubkey_file", flPubKey) 419 | os.Exit(1) 420 | } 421 | 422 | signingCert, err := loadPemCert(flCaCertFile) 423 | if err != nil { 424 | logger.Error("Cannot load signing CA certificate", 425 | "error", err, 426 | "ca_cert_file", flCaCertFile) 427 | os.Exit(1) 428 | } 429 | signer, err := initPkcs11(signingCert.PublicKey.(*rsa.PublicKey)) 430 | if err != nil { 431 | logger.Error("PKCS11 initialization failed for SubCA creation", 432 | "error", err) 433 | AuditEvent("subca_creation", false, 434 | "subca_name", flInterName, 435 | "reason", "pkcs11_init_failed", 436 | "error", err.Error()) 437 | os.Exit(1) 438 | } 439 | 440 | AuditEvent("subca_creation", true, 441 | "operation", "started", 442 | "subca_name", flInterName, 443 | "ca_subject", signingCert.Subject.CommonName) 444 | 445 | _, ok := createIntermediateCert(signer, pubkey, flInterName) 446 | if !ok { 447 | logger.Error("SubCA certificate creation failed", 448 | "subca_name", flInterName) 449 | AuditEvent("subca_creation", false, 450 | "subca_name", flInterName, 451 | "reason", "creation_failed") 452 | os.Exit(1) 453 | } 454 | } 455 | 456 | // perhaps we've been asked to create a new subca and generate a privkey 457 | if flSubCa && flGenPrivKey && flPubKey == "" { 458 | logger.Error("Signing key required for SubCA with generated private key - use -pubkey flag") 459 | os.Exit(1) 460 | } 461 | if flSubCa && flGenPrivKey && flPubKey != "" { 462 | if flInterName == "" { 463 | logger.Error("SubCA friendly name required - use -subcaname flag (e.g. -subcaname 'Apple TV Devices')") 464 | os.Exit(1) 465 | } 466 | 467 | logger.Info("Creating SubCA with generated private key", 468 | "subca_name", flInterName, 469 | "signing_key_file", flPubKey) 470 | // Generate a new RSA key. 471 | key, err := rsa.GenerateKey(rand.Reader, 4096) 472 | if err != nil { 473 | logger.Error("Cannot generate RSA private key", "error", err) 474 | os.Exit(1) 475 | } 476 | // this is our newly generated public key 477 | newpubkey := key.Public() 478 | // this is our signing public key 479 | pubkey, err := loadPubKey(flPubKey) 480 | if err != nil { 481 | logger.Error("Cannot load signing public key", "error", err, "pubkey_file", flPubKey) 482 | os.Exit(1) 483 | } 484 | 485 | signer, err := initPkcs11(pubkey.(*rsa.PublicKey)) 486 | if err != nil { 487 | logger.Error("PKCS11 initialization failed", "error", err) 488 | AuditEvent("subca_with_genkey", false, 489 | "subca_name", flInterName, 490 | "reason", "pkcs11_init_failed", 491 | "error", err.Error()) 492 | os.Exit(1) 493 | } 494 | 495 | AuditEvent("subca_with_genkey", true, 496 | "operation", "started", 497 | "subca_name", flInterName) 498 | 499 | caName, ok := createIntermediateCert(signer, newpubkey, flInterName) 500 | if !ok { 501 | logger.Error("SubCA certificate creation failed", "subca_name", flInterName) 502 | AuditEvent("subca_with_genkey", false, 503 | "subca_name", flInterName, 504 | "reason", "cert_creation_failed") 505 | os.Exit(1) 506 | } 507 | 508 | keypassword, err = password.Generate(20, 2, 0, false, false) 509 | if err != nil { 510 | logger.Error("Cannot generate key password", "error", err) 511 | os.Exit(1) 512 | } 513 | file, err := os.OpenFile(caName+".key", os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0400) 514 | if err != nil { 515 | logger.Error("Cannot create private key file", 516 | "error", err, 517 | "filename", caName+".key") 518 | os.Exit(1) 519 | } 520 | // write out PEM wrapped key 521 | privPEMBlock, err := x509.EncryptPEMBlock( 522 | rand.Reader, 523 | "PRIVATE KEY", 524 | x509.MarshalPKCS1PrivateKey(key), 525 | []byte(keypassword), 526 | x509.PEMCipher3DES, // PEMCipherAES256 prefereable but interopability reigns supreme... 527 | ) 528 | if err != nil { 529 | logger.Error("Cannot encrypt private key", "error", err) 530 | os.Exit(1) 531 | } 532 | err = pem.Encode(file, privPEMBlock) 533 | if err != nil { 534 | logger.Error("Cannot write private key file", "error", err) 535 | os.Exit(1) 536 | } 537 | file.Close() 538 | 539 | logger.Info("SubCA with generated private key created successfully", 540 | "ca_name", caName, 541 | "key_file", caName+".key", 542 | "cert_file", caName+".pem") 543 | logger.Warn("PRIVATE KEY PASSPHRASE (store securely)", "passphrase", keypassword) 544 | logger.Info("To convert to PKCS12 format, use:", 545 | "command", fmt.Sprintf("openssl pkcs12 -export -out '%v.pkcs12' -inkey '%v.key' -in '%v.pem'", caName, caName, caName)) 546 | 547 | AuditEvent("subca_with_genkey", true, 548 | "operation", "completed", 549 | "subca_name", flInterName, 550 | "ca_name", caName, 551 | "key_file", caName+".key") 552 | 553 | } else if flSubCa && !flGenPrivKey && flPubKey == "" { 554 | logger.Error("PEM formatted public key required for SubCA, or use -genkey flag to generate private key") 555 | } 556 | logger.Info("Operation completed successfully") 557 | } 558 | --------------------------------------------------------------------------------