mirror of
https://github.com/SSLMate/certspotter.git
synced 2026-01-31 09:44:21 +01:00
203 lines
5.2 KiB
Go
203 lines
5.2 KiB
Go
// Copyright (C) 2026 Opsmate, Inc.
|
|
//
|
|
// This Source Code Form is subject to the terms of the Mozilla
|
|
// Public License, v. 2.0. If a copy of the MPL was not distributed
|
|
// with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
//
|
|
// This software is distributed WITHOUT A WARRANTY OF ANY KIND.
|
|
// See the Mozilla Public License for details.
|
|
|
|
package main
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/pem"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime/debug"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"software.sslmate.com/src/certspotter"
|
|
)
|
|
|
|
var programName = os.Args[0]
|
|
var Version = "unknown"
|
|
var Source = "unknown"
|
|
|
|
func certspotterVersion() (string, string) {
|
|
if buildinfo, ok := debug.ReadBuildInfo(); ok && strings.HasPrefix(buildinfo.Main.Version, "v") {
|
|
return strings.TrimPrefix(buildinfo.Main.Version, "v"), buildinfo.Main.Path
|
|
} else {
|
|
return Version, Source
|
|
}
|
|
}
|
|
|
|
func homedir() string {
|
|
homedir, err := os.UserHomeDir()
|
|
if err != nil {
|
|
panic(fmt.Errorf("unable to determine home directory: %w", err))
|
|
}
|
|
return homedir
|
|
}
|
|
|
|
func startedBySupervisor() bool {
|
|
return os.Getenv("SYSTEMD_EXEC_PID") == strconv.Itoa(os.Getpid())
|
|
}
|
|
|
|
func defaultStateDir() string {
|
|
if envVar := os.Getenv("CERTSPOTTER_STATE_DIR"); envVar != "" {
|
|
return envVar
|
|
} else if envVar := os.Getenv("STATE_DIRECTORY"); envVar != "" && startedBySupervisor() {
|
|
return envVar
|
|
} else {
|
|
return filepath.Join(homedir(), ".certspotter")
|
|
}
|
|
}
|
|
|
|
func fileExists(filename string) bool {
|
|
_, err := os.Lstat(filename)
|
|
return err == nil
|
|
}
|
|
|
|
func readCertFile(path string) ([]byte, error) {
|
|
if path == "-" {
|
|
return io.ReadAll(os.Stdin)
|
|
} else {
|
|
return os.ReadFile(path)
|
|
}
|
|
}
|
|
|
|
func parseCertificate(certBytes []byte) ([]byte, error) {
|
|
block, _ := pem.Decode(certBytes)
|
|
if block != nil {
|
|
if block.Type == "CERTIFICATE" {
|
|
return block.Bytes, nil
|
|
}
|
|
return nil, fmt.Errorf("PEM block type is %q, expected CERTIFICATE", block.Type)
|
|
}
|
|
return nil, fmt.Errorf("no PEM data found")
|
|
}
|
|
|
|
func computeTBSHash(certDER []byte) ([32]byte, error) {
|
|
certInfo, err := certspotter.MakeCertInfoFromRawCert(certDER)
|
|
if err != nil {
|
|
return [32]byte{}, fmt.Errorf("error parsing certificate: %w", err)
|
|
}
|
|
precertTBS, err := certspotter.ReconstructPrecertTBS(certInfo.TBS)
|
|
if err != nil {
|
|
return [32]byte{}, fmt.Errorf("error reconstructing precertificate TBSCertificate: %w", err)
|
|
}
|
|
return sha256.Sum256(precertTBS.Raw), nil
|
|
}
|
|
|
|
func createNotifiedMarker(stateDir string, tbsHash [32]byte) (string, error) {
|
|
tbsHex := hex.EncodeToString(tbsHash[:])
|
|
|
|
certsDir := filepath.Join(stateDir, "certs")
|
|
tbsDir := filepath.Join(certsDir, tbsHex[0:2])
|
|
notifiedPath := filepath.Join(tbsDir, "."+tbsHex+".notified")
|
|
|
|
// Check if already notified
|
|
if fileExists(notifiedPath) {
|
|
return notifiedPath, nil
|
|
}
|
|
|
|
// Create certs directory if needed
|
|
if err := os.Mkdir(certsDir, 0777); err != nil && !errors.Is(err, fs.ErrExist) {
|
|
return "", fmt.Errorf("error creating certs directory: %w", err)
|
|
}
|
|
|
|
// Create TBS-specific subdirectory if needed
|
|
if err := os.Mkdir(tbsDir, 0777); err != nil && !errors.Is(err, fs.ErrExist) {
|
|
return "", fmt.Errorf("error creating directory: %w", err)
|
|
}
|
|
|
|
// Create marker file
|
|
if err := os.WriteFile(notifiedPath, nil, 0666); err != nil {
|
|
return "", fmt.Errorf("error creating marker file: %w", err)
|
|
}
|
|
|
|
return notifiedPath, nil
|
|
}
|
|
|
|
func main() {
|
|
version, source := certspotterVersion()
|
|
|
|
var flags struct {
|
|
stateDir string
|
|
printhash bool
|
|
version bool
|
|
}
|
|
|
|
flag.StringVar(&flags.stateDir, "state_dir", defaultStateDir(), "State directory used by certspotter")
|
|
flag.BoolVar(&flags.printhash, "printhash", false, "Instead of authorizing certificate, print its TBS hash")
|
|
flag.BoolVar(&flags.version, "version", false, "Print version and exit")
|
|
flag.Parse()
|
|
|
|
if flags.version {
|
|
fmt.Fprintf(os.Stdout, "certspotter-authorize version %s (%s)\n", version, source)
|
|
os.Exit(0)
|
|
}
|
|
|
|
args := flag.Args()
|
|
if len(args) == 0 {
|
|
fmt.Fprintf(os.Stderr, "Usage: %s [options] -|CERTFILE...\n", programName)
|
|
fmt.Fprintf(os.Stderr, "Purpose: suppress future certspotter notifications for a certificate and its corresponding precertificate.\n")
|
|
fmt.Fprintf(os.Stderr, "Options:\n")
|
|
flag.PrintDefaults()
|
|
os.Exit(2)
|
|
}
|
|
|
|
if len(args) > 1 {
|
|
for _, arg := range args {
|
|
if arg == "-" {
|
|
fmt.Fprintf(os.Stderr, "%s: '-' must be the only argument when used\n", programName)
|
|
os.Exit(2)
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, certPath := range args {
|
|
certBytes, err := readCertFile(certPath)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "%s: error reading certificate: %s\n", programName, err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
certDER, err := parseCertificate(certBytes)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "%s: %s: %s\n", programName, certPath, err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
tbsHash, err := computeTBSHash(certDER)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "%s: %s: %s\n", programName, certPath, err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
if flags.printhash {
|
|
if certPath == "-" {
|
|
fmt.Println(hex.EncodeToString(tbsHash[:]))
|
|
} else {
|
|
fmt.Printf("%s %s\n", hex.EncodeToString(tbsHash[:]), certPath)
|
|
}
|
|
} else {
|
|
_, err = createNotifiedMarker(flags.stateDir, tbsHash)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "%s: %s\n", programName, err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
}
|
|
|
|
os.Exit(0)
|
|
}
|