mirror of
https://github.com/SSLMate/certspotter.git
synced 2026-01-31 09:44:21 +01:00
22
README.md
22
README.md
@@ -54,8 +54,30 @@ The following instructions require you to have [Go version 1.21 or higher](https
|
||||
|
||||
* Command line options and operational details: [certspotter(8) man page](man/certspotter.md)
|
||||
* The script interface: [certspotter-script(8) man page](man/certspotter-script.md)
|
||||
* Authorizing known certificates: [certspotter-authorize(8) man page](man/certspotter-authorize.md)
|
||||
* [Change Log](CHANGELOG.md)
|
||||
|
||||
## Authorizing Known Certificates to Prevent False Alarms
|
||||
|
||||
You can use the **certspotter-authorize** command to tell certspotter
|
||||
about legitimate certificates issued by your certificate authority.
|
||||
certspotter won't notify you when it discovers an authorized certificate
|
||||
(or its corresponding precertificate) in Certificate Transparency logs.
|
||||
|
||||
To install certspotter-authorize, run:
|
||||
|
||||
```
|
||||
go install software.sslmate.com/src/certspotter/cmd/certspotter-authorize@latest
|
||||
```
|
||||
|
||||
To authorize a certificate, run:
|
||||
|
||||
```
|
||||
certspotter-authorize -cert /path/to/cert.pem
|
||||
```
|
||||
|
||||
For more details, see the [certspotter-authorize(8) man page](man/certspotter-authorize.md).
|
||||
|
||||
## What certificates are detected by Cert Spotter?
|
||||
|
||||
In the default configuration, any certificate that is logged to a
|
||||
|
||||
1
cmd/certspotter-authorize/.gitignore
vendored
Normal file
1
cmd/certspotter-authorize/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/certspotter-authorize
|
||||
182
cmd/certspotter-authorize/main.go
Normal file
182
cmd/certspotter-authorize/main.go
Normal file
@@ -0,0 +1,182 @@
|
||||
// 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 {
|
||||
cert string
|
||||
stateDir string
|
||||
version bool
|
||||
}
|
||||
|
||||
flag.StringVar(&flags.cert, "cert", "", "Path to a PEM-encoded certificate (- to read from stdin)")
|
||||
flag.StringVar(&flags.stateDir, "state_dir", defaultStateDir(), "State directory used by certspotter")
|
||||
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)
|
||||
}
|
||||
|
||||
if flags.cert == "" {
|
||||
fmt.Fprintf(os.Stderr, "Usage: %s -cert PATH [-state_dir PATH]\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)
|
||||
}
|
||||
|
||||
certBytes, err := readCertFile(flags.cert)
|
||||
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\n", programName, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
tbsHash, err := computeTBSHash(certDER)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "%s: %s\n", programName, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
_, err = createNotifiedMarker(flags.stateDir, tbsHash)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "%s: %s\n", programName, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
os.Exit(0)
|
||||
}
|
||||
190
cmd/certspotter-authorize/main_test.go
Normal file
190
cmd/certspotter-authorize/main_test.go
Normal file
@@ -0,0 +1,190 @@
|
||||
// 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 (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const testCertPEM = `
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIGuzCCBSOgAwIBAgIRANubk4g/6c+TF8jITzhFX44wDQYJKoZIhvcNAQELBQAw
|
||||
YDELMAkGA1UEBhMCR0IxGDAWBgNVBAoTD1NlY3RpZ28gTGltaXRlZDE3MDUGA1UE
|
||||
AxMuU2VjdGlnbyBQdWJsaWMgU2VydmVyIEF1dGhlbnRpY2F0aW9uIENBIERWIFIz
|
||||
NjAeFw0yNTEyMDIwMDAwMDBaFw0yNjExMjEyMzU5NTlaMBYxFDASBgNVBAMTC3Nz
|
||||
bG1hdGUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA235/3Y/E
|
||||
4yPAHPa37C7Fgp7KPVjjuTB5vKV9nYIJzfp7NgvDBlf7k5bZFCSsSIj2txhL0hzX
|
||||
Bwvmy7u7CYR7CApr2Rx2UPOl7Gmlt/DmtfyKac8Iunn2ozuGZDtxq19Go4NL9jl9
|
||||
e9O3H/lcL/ZFqzbUNlKIOfkOYkOxM3qpQXHTXuhkeI2MJO/S4wX8y8/8uhArWQ9e
|
||||
h/YrtJlO9fla60kLUlQF7mtJTc+0oB3+N4eF5t2a8Pav00T6lVvH8hMhbY0nZ/tB
|
||||
CD6/I6yelh8cP094VRJEGWs+zcEuXpz4FsZggkhF/l+AhQ+DfgxZhno4M60kBKC8
|
||||
Un1BTGX5TjfjJQIDAQABo4IDODCCAzQwHwYDVR0jBBgwFoAUaMASFhgOr872h6Yy
|
||||
V6NGUV3LBycwHQYDVR0OBBYEFKg2kl8xIzdSxjOEAzlNpdpigAazMA4GA1UdDwEB
|
||||
/wQEAwIFoDAMBgNVHRMBAf8EAjAAMBMGA1UdJQQMMAoGCCsGAQUFBwMBMEkGA1Ud
|
||||
IARCMEAwNAYLKwYBBAGyMQECAgcwJTAjBggrBgEFBQcCARYXaHR0cHM6Ly9zZWN0
|
||||
aWdvLmNvbS9DUFMwCAYGZ4EMAQIBMIGEBggrBgEFBQcBAQR4MHYwTwYIKwYBBQUH
|
||||
MAKGQ2h0dHA6Ly9jcnQuc2VjdGlnby5jb20vU2VjdGlnb1B1YmxpY1NlcnZlckF1
|
||||
dGhlbnRpY2F0aW9uQ0FEVlIzNi5jcnQwIwYIKwYBBQUHMAGGF2h0dHA6Ly9vY3Nw
|
||||
LnNlY3RpZ28uY29tMIIBfQYKKwYBBAHWeQIEAgSCAW0EggFpAWcAdQDXbX0Q0af1
|
||||
d8LH6V/XAL/5gskzWmXh0LMBcxfAyMVpdwAAAZrg2UujAAAEAwBGMEQCIApVmNqo
|
||||
gzJBNbcVXezO5sSvOFE5FaVZVz/eaqnCbG+2AiAT/A7XPtOYHwsE0wmTUBCTV/0l
|
||||
bF7lk573b3rNtBvP3QB2AK9niDtXsE7dj6bZfvYuqOuBCsdxYPAkXlXWDC/nhYc6
|
||||
AAABmuDZTFoAAAQDAEcwRQIgODruJKtbjW1QJcQP7ARZAw5FgChfI599pJBA2bbQ
|
||||
suICIQCskIUnzQCD6taycnQCN3zpu+rsz3Vd4AsMeFDM/cDJ5AB2AKyrMHBs6+yE
|
||||
MfQT0vSRXxEeQiRDsfKmjE88KzunHgLDAAABmuDZS5kAAAQDAEcwRQIgD9IQCPTc
|
||||
N88jbz5DUILwmDruTo411Ep5M2ZryNjBkywCIQCFwIyqGZEd+PiFv4l+5LOV3yDW
|
||||
/zUuimFUoAJH5OIiNDBsBgNVHREEZTBjggtzc2xtYXRlLmNvbYIbKi5odHRwLWFw
|
||||
cHJvdmFsLnNzbG1hdGUuY29tghFjZXJ0cy5zc2xtYXRlLmNvbYITY29uc29sZS5z
|
||||
c2xtYXRlLmNvbYIPd3d3LnNzbG1hdGUuY29tMA0GCSqGSIb3DQEBCwUAA4IBgQBR
|
||||
Pjx14qo9PiYYEE1695CHdctA6up8L+n0MRapZcxALN/cetfGeoR00ZEH+7b1X7Ma
|
||||
F9GGv1OtJXDoCySlAsdwFKHYtKhrUYRuQXdKGkTjdMzKO/+5kXeZIqgsCR10j8nr
|
||||
Zq0Zcg2ply4j03/0y7+8ZNC1Erp4DB1Tq7ybgXnyURaNQTHsSkDoxMT/bWIrhGD0
|
||||
C8kN/ExkFvOBQlzdbuwo2d3v0zSM4mYmnqUhUYHprZllOziYgxIqjM/7mfnDkVAi
|
||||
ov8yNJtn6EPt1wt6Oo3fC+Ft1T/kbSxeZbqWf3Zgbon5ijmNz+xqkb8br2+JdzM+
|
||||
8gEIqO6mNoMl0tayzb4a5KDaHxhczMGB3ggBwpVcdLtYBBa41thrgRP0VARqFTFG
|
||||
IIkC9gPMjScf+uv9CQPsNk3kFI8vN4T3x4/g54N8Mc3M4JxvLaOsBj8dMeyq7v2p
|
||||
1zE9WRngMUWuPgx0O94c0Pteumg/+pSGVeRapIuYZxXvkmLJ5wmwgYepix+cw1w=
|
||||
-----END CERTIFICATE-----
|
||||
`
|
||||
|
||||
func TestComputeTBSHash(t *testing.T) {
|
||||
certDER, err := parseCertificate([]byte(testCertPEM))
|
||||
if err != nil {
|
||||
t.Fatalf("parseCertificate failed: %v", err)
|
||||
}
|
||||
tbsHash, err := computeTBSHash(certDER)
|
||||
if err != nil {
|
||||
t.Fatalf("computeTBSHash failed: %v", err)
|
||||
}
|
||||
if expected := [...]byte{0x3c, 0xf6, 0xb2, 0x44, 0xc2, 0x95, 0x85, 0xdb, 0xfb, 0xfd, 0x42, 0x0a, 0x6a, 0x4c, 0x62, 0xf7, 0x96, 0x8f, 0xa9, 0x05, 0xb4, 0xd6, 0xa4, 0xf5, 0x9d, 0x4d, 0x3b, 0xc9, 0xfa, 0xcb, 0x0c, 0xc8}; expected != tbsHash {
|
||||
t.Fatalf("computeTBSHash returned %x; expected %x", tbsHash, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateNotifiedMarker(t *testing.T) {
|
||||
stateDir := t.TempDir()
|
||||
|
||||
certDER, err := parseCertificate([]byte(testCertPEM))
|
||||
if err != nil {
|
||||
t.Fatalf("parseCertificate failed: %v", err)
|
||||
}
|
||||
|
||||
tbsHash, err := computeTBSHash(certDER)
|
||||
if err != nil {
|
||||
t.Fatalf("computeTBSHash failed: %v", err)
|
||||
}
|
||||
|
||||
// First call should create the marker
|
||||
notifiedPath, err := createNotifiedMarker(stateDir, tbsHash)
|
||||
if err != nil {
|
||||
t.Fatalf("createNotifiedMarker failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify marker file exists
|
||||
if !fileExists(notifiedPath) {
|
||||
t.Fatalf("marker file does not exist: %s", notifiedPath)
|
||||
}
|
||||
|
||||
// Verify path structure is correct
|
||||
tbsHex := hex.EncodeToString(tbsHash[:])
|
||||
expectedPath := filepath.Join(stateDir, "certs", tbsHex[0:2], "."+tbsHex+".notified")
|
||||
if notifiedPath != expectedPath {
|
||||
t.Fatalf("unexpected marker path: got %s, expected %s", notifiedPath, expectedPath)
|
||||
}
|
||||
|
||||
// Second call should succeed (idempotency)
|
||||
notifiedPath2, err := createNotifiedMarker(stateDir, tbsHash)
|
||||
if err != nil {
|
||||
t.Fatalf("createNotifiedMarker second call failed: %v", err)
|
||||
}
|
||||
if notifiedPath != notifiedPath2 {
|
||||
t.Fatalf("second call returned different path: got %s, expected %s", notifiedPath2, notifiedPath)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadCertFile(t *testing.T) {
|
||||
// Test reading from a file
|
||||
tmpDir := t.TempDir()
|
||||
certPath := filepath.Join(tmpDir, "cert.pem")
|
||||
if err := os.WriteFile(certPath, []byte(testCertPEM), 0644); err != nil {
|
||||
t.Fatalf("failed to write test cert: %v", err)
|
||||
}
|
||||
|
||||
certBytes, err := readCertFile(certPath)
|
||||
if err != nil {
|
||||
t.Fatalf("readCertFile failed: %v", err)
|
||||
}
|
||||
if !bytes.Equal(certBytes, []byte(testCertPEM)) {
|
||||
t.Fatal("readCertFile returned different content")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileExists(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
// Test with non-existent file
|
||||
if fileExists(filepath.Join(tmpDir, "nonexistent")) {
|
||||
t.Fatal("fileExists returned true for non-existent file")
|
||||
}
|
||||
|
||||
// Test with existing file
|
||||
existingFile := filepath.Join(tmpDir, "existing")
|
||||
if err := os.WriteFile(existingFile, []byte("test"), 0644); err != nil {
|
||||
t.Fatalf("failed to write test file: %v", err)
|
||||
}
|
||||
if !fileExists(existingFile) {
|
||||
t.Fatal("fileExists returned false for existing file")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEndToEnd(t *testing.T) {
|
||||
stateDir := t.TempDir()
|
||||
|
||||
certDER, err := parseCertificate([]byte(testCertPEM))
|
||||
if err != nil {
|
||||
t.Fatalf("parseCertificate failed: %v", err)
|
||||
}
|
||||
|
||||
tbsHash, err := computeTBSHash(certDER)
|
||||
if err != nil {
|
||||
t.Fatalf("computeTBSHash failed: %v", err)
|
||||
}
|
||||
|
||||
notifiedPath, err := createNotifiedMarker(stateDir, tbsHash)
|
||||
if err != nil {
|
||||
t.Fatalf("createNotifiedMarker failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify the marker file structure matches what monitor/fsstate.go expects
|
||||
tbsHex := hex.EncodeToString(tbsHash[:])
|
||||
expectedDir := filepath.Join(stateDir, "certs", tbsHex[0:2])
|
||||
expectedFile := filepath.Join(expectedDir, "."+tbsHex+".notified")
|
||||
|
||||
if notifiedPath != expectedFile {
|
||||
t.Fatalf("unexpected marker path: got %s, expected %s", notifiedPath, expectedFile)
|
||||
}
|
||||
|
||||
if !fileExists(expectedFile) {
|
||||
t.Fatalf("marker file does not exist: %s", expectedFile)
|
||||
}
|
||||
|
||||
// Verify file is empty (as expected by certspotter)
|
||||
stat, err := os.Stat(expectedFile)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to stat marker file: %v", err)
|
||||
}
|
||||
if stat.Size() != 0 {
|
||||
t.Fatalf("marker file should be empty, but has size %d", stat.Size())
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
all: certspotter-script.8 certspotter.8
|
||||
all: certspotter-script.8 certspotter.8 certspotter-authorize.8
|
||||
|
||||
%.8: %.md
|
||||
lowdown -s -Tman \
|
||||
|
||||
90
man/certspotter-authorize.md
Normal file
90
man/certspotter-authorize.md
Normal file
@@ -0,0 +1,90 @@
|
||||
# NAME
|
||||
|
||||
**certspotter-authorize** - Authorize certificates to suppress certspotter notifications
|
||||
|
||||
# SYNOPSIS
|
||||
|
||||
**certspotter-authorize** `-cert` *PATH* [`-state_dir` *PATH*]
|
||||
|
||||
# DESCRIPTION
|
||||
|
||||
**certspotter-authorize** is a utility for preemptively authorizing certificates so
|
||||
that **certspotter(8)** will not send notifications when those certificates are
|
||||
discovered in Certificate Transparency logs.
|
||||
|
||||
This is useful for preventing false alarms when you know in advance that a
|
||||
certificate will be issued. For example, you might run **certspotter-authorize**
|
||||
immediately after receiving a certificate from your certificate authority, as
|
||||
part of your certificate issuance pipeline.
|
||||
|
||||
**certspotter-authorize** uses the TBSCertificate hash as defined by RFC
|
||||
6962 Section 3.2 to identify certificates. This hash is the same for a
|
||||
certificate and its corresponding precertificate. This means authorizing
|
||||
the certificate will suppress notifications for the precertificate
|
||||
as well. Certificates with different serial numbers, validity periods,
|
||||
or other changes to the TBSCertificate will not be covered by the authorization
|
||||
and will trigger notifications.
|
||||
|
||||
# OPTIONS
|
||||
|
||||
-cert *PATH*
|
||||
|
||||
: Path to a PEM-encoded certificate. Use `-` to read from stdin.
|
||||
This option is required.
|
||||
|
||||
-state\_dir *PATH*
|
||||
|
||||
: Directory where certspotter stores state. Defaults to
|
||||
`$CERTSPOTTER_STATE_DIR` if set, or `~/.certspotter` otherwise.
|
||||
This should be the same directory used by **certspotter(8)**.
|
||||
|
||||
-version
|
||||
|
||||
: Print version information and exit.
|
||||
|
||||
# EXAMPLES
|
||||
|
||||
Authorize a certificate from a file:
|
||||
|
||||
$ certspotter-authorize -cert /path/to/cert.pem
|
||||
|
||||
Authorize a certificate from stdin:
|
||||
|
||||
$ cat cert.pem | certspotter-authorize -cert -
|
||||
|
||||
Authorize a certificate in a custom state directory:
|
||||
|
||||
$ certspotter-authorize -cert cert.pem -state_dir /var/lib/certspotter
|
||||
|
||||
# OPERATION
|
||||
|
||||
When **certspotter-authorize** is run with a certificate, it computes
|
||||
the SHA-256 hash of the certificate's TBSCertificate as defined by RFC
|
||||
6962 Section 3.2 creates a `.notified` marker file in the certspotter
|
||||
state directory. When certspotter later discovers a certificate with
|
||||
the same TBSCertificate in a CT log, it will skip sending notifications
|
||||
because the marker file is present.
|
||||
|
||||
# ENVIRONMENT
|
||||
|
||||
`CERTSPOTTER_STATE_DIR`
|
||||
|
||||
: Directory for storing state. Overridden by `-state_dir`. Defaults to
|
||||
`~/.certspotter`. This should be the same directory used by **certspotter(8)**.
|
||||
|
||||
# FILES
|
||||
|
||||
`$CERTSPOTTER_STATE_DIR/certs/XX/.HASH.notified`
|
||||
|
||||
: Marker files indicating that a certificate with TBS hash `HASH`
|
||||
has been authorized. `XX` is the first two hex digits of the hash.
|
||||
The file is empty; only its existence is checked.
|
||||
|
||||
# EXIT STATUS
|
||||
|
||||
**certspotter-authorize** exits with status 0 on success, 1 on error, or 2 on
|
||||
invalid usage.
|
||||
|
||||
# SEE ALSO
|
||||
|
||||
certspotter(8), certspotter-script(8)
|
||||
Reference in New Issue
Block a user