Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f463eeed88 | |||
| 289c9d2343 | |||
| e375963243 |
82
certlib/hosts/hosts.go
Normal file
82
certlib/hosts/hosts.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package hosts
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Target struct {
|
||||
Host string
|
||||
Port int
|
||||
}
|
||||
|
||||
func (t *Target) String() string {
|
||||
return fmt.Sprintf("%s:%d", t.Host, t.Port)
|
||||
}
|
||||
|
||||
func parseURL(host string) (string, int, error) {
|
||||
url, err := url.Parse(host)
|
||||
if err != nil {
|
||||
return "", 0, fmt.Errorf("certlib/hosts: invalid host: %s", host)
|
||||
}
|
||||
|
||||
if strings.ToLower(url.Scheme) != "https" {
|
||||
return "", 0, errors.New("certlib/hosts: only https scheme supported")
|
||||
}
|
||||
|
||||
if url.Port() == "" {
|
||||
return url.Hostname(), 443, nil
|
||||
}
|
||||
|
||||
port, err := strconv.ParseInt(url.Port(), 10, 16)
|
||||
if err != nil {
|
||||
return "", 0, fmt.Errorf("certlib/hosts: invalid port: %s", url.Port())
|
||||
}
|
||||
|
||||
return url.Hostname(), int(port), nil
|
||||
}
|
||||
|
||||
func parseHostPort(host string) (string, int, error) {
|
||||
host, sport, err := net.SplitHostPort(host)
|
||||
if err == nil {
|
||||
port, err := strconv.ParseInt(sport, 10, 16)
|
||||
if err != nil {
|
||||
return "", 0, fmt.Errorf("certlib/hosts: invalid port: %s", sport)
|
||||
}
|
||||
|
||||
return host, int(port), nil
|
||||
}
|
||||
|
||||
return host, 443, nil
|
||||
}
|
||||
|
||||
func ParseHost(host string) (*Target, error) {
|
||||
host, port, err := parseURL(host)
|
||||
if err == nil {
|
||||
return &Target{Host: host, Port: port}, nil
|
||||
}
|
||||
|
||||
host, port, err = parseHostPort(host)
|
||||
if err == nil {
|
||||
return &Target{Host: host, Port: port}, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("certlib/hosts: invalid host: %s", host)
|
||||
}
|
||||
|
||||
func ParseHosts(hosts ...string) ([]*Target, error) {
|
||||
targets := make([]*Target, 0, len(hosts))
|
||||
for _, host := range hosts {
|
||||
target, err := ParseHost(host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
targets = append(targets, target)
|
||||
}
|
||||
|
||||
return targets, nil
|
||||
}
|
||||
148
cmd/cert-bundler/README.txt
Normal file
148
cmd/cert-bundler/README.txt
Normal file
@@ -0,0 +1,148 @@
|
||||
cert-bundler: create certificate chain archives
|
||||
------------------------------------------------
|
||||
|
||||
Description
|
||||
cert-bundler creates archives of certificate chains from a YAML configuration
|
||||
file. It validates certificates, checks expiration dates, and generates
|
||||
archives in multiple formats (zip, tar.gz) with optional manifest files
|
||||
containing SHA256 checksums.
|
||||
|
||||
Usage
|
||||
cert-bundler [options]
|
||||
|
||||
Options:
|
||||
-c <file> Path to YAML configuration file (default: bundle.yaml)
|
||||
-o <dir> Output directory for archives (default: pkg)
|
||||
|
||||
YAML Configuration Format
|
||||
|
||||
The configuration file uses the following structure:
|
||||
|
||||
config:
|
||||
hashes: <filename>
|
||||
expiry: <duration>
|
||||
chains:
|
||||
<group_name>:
|
||||
certs:
|
||||
- root: <path>
|
||||
intermediates:
|
||||
- <path>
|
||||
- <path>
|
||||
- root: <path>
|
||||
intermediates:
|
||||
- <path>
|
||||
outputs:
|
||||
include_single: <bool>
|
||||
include_individual: <bool>
|
||||
manifest: <bool>
|
||||
encoding: <encoding>
|
||||
formats:
|
||||
- <format>
|
||||
- <format>
|
||||
|
||||
Configuration Fields
|
||||
|
||||
config:
|
||||
hashes: (optional) Name of the file to write SHA256 checksums of all
|
||||
generated archives. If omitted, no hash file is created.
|
||||
expiry: (optional) Expiration warning threshold. Certificates expiring
|
||||
within this period will trigger a warning. Supports formats like
|
||||
"1y" (year), "6m" (month), "30d" (day). Default: 1y
|
||||
|
||||
chains:
|
||||
Each key under "chains" defines a named certificate group. All certificates
|
||||
in a group are bundled together into archives with the group name.
|
||||
|
||||
certs:
|
||||
List of certificate chains. Each chain has:
|
||||
root: Path to root CA certificate (PEM or DER format)
|
||||
intermediates: List of paths to intermediate certificates
|
||||
|
||||
All intermediates are validated against their root CA. An error is
|
||||
reported if signature verification fails.
|
||||
|
||||
outputs:
|
||||
Defines output formats and content for the group's archives:
|
||||
|
||||
include_single: (bool) If true, all certificates in the group are
|
||||
concatenated into a single file named "bundle.pem"
|
||||
(or "bundle.crt" for DER encoding).
|
||||
|
||||
include_individual: (bool) If true, each certificate is included as
|
||||
a separate file in the archive, named after the
|
||||
original file (e.g., "int/cca2.pem" becomes
|
||||
"cca2.pem").
|
||||
|
||||
manifest: (bool) If true, a MANIFEST file is included containing
|
||||
SHA256 checksums of all files in the archive.
|
||||
|
||||
encoding: Specifies certificate encoding in the archive:
|
||||
- "pem": PEM format with .pem extension (default)
|
||||
- "der": DER format with .crt extension
|
||||
- "both": Both PEM and DER versions are included
|
||||
|
||||
formats: List of archive formats to generate:
|
||||
- "zip": Creates a .zip archive
|
||||
- "tgz": Creates a .tar.gz archive
|
||||
|
||||
Output Files
|
||||
|
||||
For each group and format combination, an archive is created:
|
||||
<group_name>.zip or <group_name>.tar.gz
|
||||
|
||||
If config.hashes is specified, a hash file is created in the output directory
|
||||
containing SHA256 checksums of all generated archives.
|
||||
|
||||
Example Configuration
|
||||
|
||||
config:
|
||||
hashes: bundle.sha256
|
||||
expiry: 1y
|
||||
chains:
|
||||
core_certs:
|
||||
certs:
|
||||
- root: roots/core-ca.pem
|
||||
intermediates:
|
||||
- int/cca1.pem
|
||||
- int/cca2.pem
|
||||
- int/cca3.pem
|
||||
- root: roots/ssh-ca.pem
|
||||
intermediates:
|
||||
- ssh/ssh_dmz1.pem
|
||||
- ssh/ssh_internal.pem
|
||||
outputs:
|
||||
include_single: true
|
||||
include_individual: true
|
||||
manifest: true
|
||||
encoding: pem
|
||||
formats:
|
||||
- zip
|
||||
- tgz
|
||||
|
||||
This configuration:
|
||||
- Creates core_certs.zip and core_certs.tar.gz in the output directory
|
||||
- Each archive contains bundle.pem (all certificates concatenated)
|
||||
- Each archive contains individual certificates (core-ca.pem, cca1.pem, etc.)
|
||||
- Each archive includes a MANIFEST file with SHA256 checksums
|
||||
- Creates bundle.sha256 with checksums of the two archives
|
||||
- Warns if any certificate expires within 1 year
|
||||
|
||||
Examples
|
||||
|
||||
# Create bundles using default configuration (bundle.yaml -> pkg/)
|
||||
cert-bundler
|
||||
|
||||
# Use custom configuration and output directory
|
||||
cert-bundler -c myconfig.yaml -o output
|
||||
|
||||
# Create bundles from testdata configuration
|
||||
cert-bundler -c testdata/bundle.yaml -o testdata/pkg
|
||||
|
||||
Notes
|
||||
- Certificate paths in the YAML are relative to the current working directory
|
||||
- All intermediates must be properly signed by their specified root CA
|
||||
- Certificates are checked for expiration; warnings are printed to stderr
|
||||
- Expired certificates do not prevent archive creation but generate warnings
|
||||
- Both PEM and DER certificate formats are supported as input
|
||||
- Archive filenames use the group name, not individual chain names
|
||||
- If both include_single and include_individual are true, archives contain both
|
||||
489
cmd/cert-bundler/main.go
Normal file
489
cmd/cert-bundler/main.go
Normal file
@@ -0,0 +1,489 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"archive/zip"
|
||||
"compress/gzip"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
_ "embed"
|
||||
"encoding/pem"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.wntrmute.dev/kyle/goutils/certlib"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
// Config represents the top-level YAML configuration
|
||||
type Config struct {
|
||||
Config struct {
|
||||
Hashes string `yaml:"hashes"`
|
||||
Expiry string `yaml:"expiry"`
|
||||
} `yaml:"config"`
|
||||
Chains map[string]ChainGroup `yaml:"chains"`
|
||||
}
|
||||
|
||||
// ChainGroup represents a named group of certificate chains
|
||||
type ChainGroup struct {
|
||||
Certs []CertChain `yaml:"certs"`
|
||||
Outputs Outputs `yaml:"outputs"`
|
||||
}
|
||||
|
||||
// CertChain represents a root certificate and its intermediates
|
||||
type CertChain struct {
|
||||
Root string `yaml:"root"`
|
||||
Intermediates []string `yaml:"intermediates"`
|
||||
}
|
||||
|
||||
// Outputs defines output format options
|
||||
type Outputs struct {
|
||||
IncludeSingle bool `yaml:"include_single"`
|
||||
IncludeIndividual bool `yaml:"include_individual"`
|
||||
Manifest bool `yaml:"manifest"`
|
||||
Formats []string `yaml:"formats"`
|
||||
Encoding string `yaml:"encoding"`
|
||||
}
|
||||
|
||||
var (
|
||||
configFile string
|
||||
outputDir string
|
||||
)
|
||||
|
||||
var formatExtensions = map[string]string{
|
||||
"zip": ".zip",
|
||||
"tgz": ".tar.gz",
|
||||
}
|
||||
|
||||
//go:embed README.txt
|
||||
var readmeContent string
|
||||
|
||||
func usage() {
|
||||
fmt.Fprint(os.Stderr, readmeContent)
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Usage = usage
|
||||
flag.StringVar(&configFile, "c", "bundle.yaml", "path to YAML configuration file")
|
||||
flag.StringVar(&outputDir, "o", "pkg", "output directory for archives")
|
||||
flag.Parse()
|
||||
|
||||
if configFile == "" {
|
||||
fmt.Fprintf(os.Stderr, "Error: configuration file required (-c flag)\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Load and parse configuration
|
||||
cfg, err := loadConfig(configFile)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error loading config: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Parse expiry duration (default 1 year)
|
||||
expiryDuration := 365 * 24 * time.Hour
|
||||
if cfg.Config.Expiry != "" {
|
||||
expiryDuration, err = parseDuration(cfg.Config.Expiry)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error parsing expiry: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// Create output directory if it doesn't exist
|
||||
if err := os.MkdirAll(outputDir, 0755); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error creating output directory: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Process each chain group
|
||||
createdFiles := []string{}
|
||||
for groupName, group := range cfg.Chains {
|
||||
files, err := processChainGroup(groupName, group, expiryDuration)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error processing chain group %s: %v\n", groupName, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
createdFiles = append(createdFiles, files...)
|
||||
}
|
||||
|
||||
// Generate hash file for all created archives
|
||||
if cfg.Config.Hashes != "" {
|
||||
hashFile := filepath.Join(outputDir, cfg.Config.Hashes)
|
||||
if err := generateHashFile(hashFile, createdFiles); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error generating hash file: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println("Certificate bundling completed successfully")
|
||||
}
|
||||
|
||||
func loadConfig(path string) (*Config, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var cfg Config
|
||||
if err := yaml.Unmarshal(data, &cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &cfg, nil
|
||||
}
|
||||
|
||||
func parseDuration(s string) (time.Duration, error) {
|
||||
// Support simple formats like "1y", "6m", "30d"
|
||||
if len(s) < 2 {
|
||||
return 0, fmt.Errorf("invalid duration format: %s", s)
|
||||
}
|
||||
|
||||
unit := s[len(s)-1]
|
||||
value := s[:len(s)-1]
|
||||
|
||||
var multiplier time.Duration
|
||||
switch unit {
|
||||
case 'y', 'Y':
|
||||
multiplier = 365 * 24 * time.Hour
|
||||
case 'm', 'M':
|
||||
multiplier = 30 * 24 * time.Hour
|
||||
case 'd', 'D':
|
||||
multiplier = 24 * time.Hour
|
||||
default:
|
||||
return time.ParseDuration(s)
|
||||
}
|
||||
|
||||
var num int
|
||||
_, err := fmt.Sscanf(value, "%d", &num)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("invalid duration value: %s", s)
|
||||
}
|
||||
|
||||
return time.Duration(num) * multiplier, nil
|
||||
}
|
||||
|
||||
func processChainGroup(groupName string, group ChainGroup, expiryDuration time.Duration) ([]string, error) {
|
||||
var createdFiles []string
|
||||
|
||||
// Default encoding to "pem" if not specified
|
||||
encoding := group.Outputs.Encoding
|
||||
if encoding == "" {
|
||||
encoding = "pem"
|
||||
}
|
||||
|
||||
// Collect data from all chains in the group
|
||||
var singleFileCerts []*x509.Certificate
|
||||
var individualCerts []certWithPath
|
||||
|
||||
for _, chain := range group.Certs {
|
||||
// Step 1: Load all certificates for this chain
|
||||
allCerts := make(map[string]*x509.Certificate)
|
||||
|
||||
// Load root certificate
|
||||
rootCert, err := certlib.LoadCertificate(chain.Root)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load root certificate %s: %v", chain.Root, err)
|
||||
}
|
||||
allCerts[chain.Root] = rootCert
|
||||
|
||||
// Check expiry for root
|
||||
checkExpiry(chain.Root, rootCert, expiryDuration)
|
||||
|
||||
// Add root to single file if needed
|
||||
if group.Outputs.IncludeSingle {
|
||||
singleFileCerts = append(singleFileCerts, rootCert)
|
||||
}
|
||||
|
||||
// Add root to individual files if needed
|
||||
if group.Outputs.IncludeIndividual {
|
||||
individualCerts = append(individualCerts, certWithPath{
|
||||
cert: rootCert,
|
||||
path: chain.Root,
|
||||
})
|
||||
}
|
||||
|
||||
// Step 2: Load and validate intermediates
|
||||
for _, intPath := range chain.Intermediates {
|
||||
intCert, err := certlib.LoadCertificate(intPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load intermediate certificate %s: %v", intPath, err)
|
||||
}
|
||||
allCerts[intPath] = intCert
|
||||
|
||||
// Validate that intermediate is signed by root
|
||||
if err := intCert.CheckSignatureFrom(rootCert); err != nil {
|
||||
return nil, fmt.Errorf("intermediate %s is not properly signed by root %s: %v", intPath, chain.Root, err)
|
||||
}
|
||||
|
||||
// Check expiry for intermediate
|
||||
checkExpiry(intPath, intCert, expiryDuration)
|
||||
|
||||
// Add intermediate to a single file if needed
|
||||
if group.Outputs.IncludeSingle {
|
||||
singleFileCerts = append(singleFileCerts, intCert)
|
||||
}
|
||||
|
||||
// Add intermediate to individual files if needed
|
||||
if group.Outputs.IncludeIndividual {
|
||||
individualCerts = append(individualCerts, certWithPath{
|
||||
cert: intCert,
|
||||
path: intPath,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare files for inclusion in archives for the entire group.
|
||||
var archiveFiles []fileEntry
|
||||
|
||||
// Handle a single bundle file.
|
||||
if group.Outputs.IncludeSingle && len(singleFileCerts) > 0 {
|
||||
files, err := encodeCertsToFiles(singleFileCerts, "bundle", encoding, true)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to encode single bundle: %v", err)
|
||||
}
|
||||
archiveFiles = append(archiveFiles, files...)
|
||||
}
|
||||
|
||||
// Handle individual files
|
||||
if group.Outputs.IncludeIndividual {
|
||||
for _, cp := range individualCerts {
|
||||
baseName := strings.TrimSuffix(filepath.Base(cp.path), filepath.Ext(cp.path))
|
||||
files, err := encodeCertsToFiles([]*x509.Certificate{cp.cert}, baseName, encoding, false)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to encode individual cert %s: %v", cp.path, err)
|
||||
}
|
||||
archiveFiles = append(archiveFiles, files...)
|
||||
}
|
||||
}
|
||||
|
||||
// Generate manifest if requested
|
||||
if group.Outputs.Manifest {
|
||||
manifestContent := generateManifest(archiveFiles)
|
||||
archiveFiles = append(archiveFiles, fileEntry{
|
||||
name: "MANIFEST",
|
||||
content: manifestContent,
|
||||
})
|
||||
}
|
||||
|
||||
// Create archives for the entire group
|
||||
for _, format := range group.Outputs.Formats {
|
||||
ext, ok := formatExtensions[format]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unsupported format: %s", format)
|
||||
}
|
||||
archivePath := filepath.Join(outputDir, groupName+ext)
|
||||
switch format {
|
||||
case "zip":
|
||||
if err := createZipArchive(archivePath, archiveFiles); err != nil {
|
||||
return nil, fmt.Errorf("failed to create zip archive: %v", err)
|
||||
}
|
||||
case "tgz":
|
||||
if err := createTarGzArchive(archivePath, archiveFiles); err != nil {
|
||||
return nil, fmt.Errorf("failed to create tar.gz archive: %v", err)
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported format: %s", format)
|
||||
}
|
||||
createdFiles = append(createdFiles, archivePath)
|
||||
}
|
||||
|
||||
return createdFiles, nil
|
||||
}
|
||||
|
||||
func checkExpiry(path string, cert *x509.Certificate, expiryDuration time.Duration) {
|
||||
now := time.Now()
|
||||
expiryThreshold := now.Add(expiryDuration)
|
||||
|
||||
if cert.NotAfter.Before(expiryThreshold) {
|
||||
daysUntilExpiry := int(cert.NotAfter.Sub(now).Hours() / 24)
|
||||
if daysUntilExpiry < 0 {
|
||||
fmt.Fprintf(os.Stderr, "WARNING: Certificate %s has EXPIRED (expired %d days ago)\n", path, -daysUntilExpiry)
|
||||
} else {
|
||||
fmt.Fprintf(os.Stderr, "WARNING: Certificate %s will expire in %d days (on %s)\n", path, daysUntilExpiry, cert.NotAfter.Format("2006-01-02"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type fileEntry struct {
|
||||
name string
|
||||
content []byte
|
||||
}
|
||||
|
||||
type certWithPath struct {
|
||||
cert *x509.Certificate
|
||||
path string
|
||||
}
|
||||
|
||||
// encodeCertsToFiles converts certificates to file entries based on encoding type
|
||||
// If isSingle is true, certs are concatenated into a single file; otherwise one cert per file
|
||||
func encodeCertsToFiles(certs []*x509.Certificate, baseName string, encoding string, isSingle bool) ([]fileEntry, error) {
|
||||
var files []fileEntry
|
||||
|
||||
switch encoding {
|
||||
case "pem":
|
||||
pemContent, err := encodeCertsToPEM(certs, isSingle)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
files = append(files, fileEntry{
|
||||
name: baseName + ".pem",
|
||||
content: pemContent,
|
||||
})
|
||||
case "der":
|
||||
if isSingle {
|
||||
// For single file in DER, concatenate all cert DER bytes
|
||||
var derContent []byte
|
||||
for _, cert := range certs {
|
||||
derContent = append(derContent, cert.Raw...)
|
||||
}
|
||||
files = append(files, fileEntry{
|
||||
name: baseName + ".crt",
|
||||
content: derContent,
|
||||
})
|
||||
} else {
|
||||
// Individual DER file (should only have one cert)
|
||||
if len(certs) > 0 {
|
||||
files = append(files, fileEntry{
|
||||
name: baseName + ".crt",
|
||||
content: certs[0].Raw,
|
||||
})
|
||||
}
|
||||
}
|
||||
case "both":
|
||||
// Add PEM version
|
||||
pemContent, err := encodeCertsToPEM(certs, isSingle)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
files = append(files, fileEntry{
|
||||
name: baseName + ".pem",
|
||||
content: pemContent,
|
||||
})
|
||||
// Add DER version
|
||||
if isSingle {
|
||||
var derContent []byte
|
||||
for _, cert := range certs {
|
||||
derContent = append(derContent, cert.Raw...)
|
||||
}
|
||||
files = append(files, fileEntry{
|
||||
name: baseName + ".crt",
|
||||
content: derContent,
|
||||
})
|
||||
} else {
|
||||
if len(certs) > 0 {
|
||||
files = append(files, fileEntry{
|
||||
name: baseName + ".crt",
|
||||
content: certs[0].Raw,
|
||||
})
|
||||
}
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported encoding: %s (must be 'pem', 'der', or 'both')", encoding)
|
||||
}
|
||||
|
||||
return files, nil
|
||||
}
|
||||
|
||||
// encodeCertsToPEM encodes certificates to PEM format
|
||||
func encodeCertsToPEM(certs []*x509.Certificate, concatenate bool) ([]byte, error) {
|
||||
var pemContent []byte
|
||||
for _, cert := range certs {
|
||||
pemBlock := &pem.Block{
|
||||
Type: "CERTIFICATE",
|
||||
Bytes: cert.Raw,
|
||||
}
|
||||
pemContent = append(pemContent, pem.EncodeToMemory(pemBlock)...)
|
||||
}
|
||||
return pemContent, nil
|
||||
}
|
||||
|
||||
func generateManifest(files []fileEntry) []byte {
|
||||
var manifest strings.Builder
|
||||
for _, file := range files {
|
||||
if file.name == "MANIFEST" {
|
||||
continue
|
||||
}
|
||||
hash := sha256.Sum256(file.content)
|
||||
manifest.WriteString(fmt.Sprintf("%x %s\n", hash, file.name))
|
||||
}
|
||||
return []byte(manifest.String())
|
||||
}
|
||||
|
||||
func createZipArchive(path string, files []fileEntry) error {
|
||||
f, err := os.Create(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
w := zip.NewWriter(f)
|
||||
defer w.Close()
|
||||
|
||||
for _, file := range files {
|
||||
fw, err := w.Create(file.name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := fw.Write(file.content); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func createTarGzArchive(path string, files []fileEntry) error {
|
||||
f, err := os.Create(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
gw := gzip.NewWriter(f)
|
||||
defer gw.Close()
|
||||
|
||||
tw := tar.NewWriter(gw)
|
||||
defer tw.Close()
|
||||
|
||||
for _, file := range files {
|
||||
hdr := &tar.Header{
|
||||
Name: file.name,
|
||||
Mode: 0644,
|
||||
Size: int64(len(file.content)),
|
||||
}
|
||||
if err := tw.WriteHeader(hdr); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := tw.Write(file.content); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func generateHashFile(path string, files []string) error {
|
||||
f, err := os.Create(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
for _, file := range files {
|
||||
data, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
hash := sha256.Sum256(data)
|
||||
fmt.Fprintf(f, "%x %s\n", hash, filepath.Base(file))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
189
cmd/cert-bundler/prompt.txt
Normal file
189
cmd/cert-bundler/prompt.txt
Normal file
@@ -0,0 +1,189 @@
|
||||
Task: build a certificate bundling tool in cmd/cert-bundler. It
|
||||
creates archives of certificates chains.
|
||||
|
||||
A YAML file for this looks something like:
|
||||
|
||||
``` yaml
|
||||
config:
|
||||
hashes: bundle.sha256
|
||||
expiry: 1y
|
||||
chains:
|
||||
core_certs:
|
||||
certs:
|
||||
- root: roots/core-ca.pem
|
||||
intermediates:
|
||||
- int/cca1.pem
|
||||
- int/cca2.pem
|
||||
- int/cca3.pem
|
||||
- root: roots/ssh-ca.pem
|
||||
intermediates:
|
||||
- ssh/ssh_dmz1.pem
|
||||
- ssh/ssh_internal.pem
|
||||
outputs:
|
||||
include_single: true
|
||||
include_individual: true
|
||||
manifest: true
|
||||
formats:
|
||||
- zip
|
||||
- tgz
|
||||
```
|
||||
|
||||
Some requirements:
|
||||
|
||||
1. First, all the certificates should be loaded.
|
||||
2. For each root, each of the indivudal intermediates should be
|
||||
checked to make sure they are properly signed by the root CA.
|
||||
3. The program should optionally take an expiration period (defaulting
|
||||
to one year), specified in config.expiration, and if any certificate
|
||||
is within that expiration period, a warning should be printed.
|
||||
4. If outputs.include_single is true, all certificates under chains
|
||||
should be concatenated into a single file.
|
||||
5. If outputs.include_individual is true, all certificates under
|
||||
chains should be included at the root level (e.g. int/cca2.pem
|
||||
would be cca2.pem in the archive).
|
||||
6. If bundle.manifest is true, a "MANIFEST" file is created with
|
||||
SHA256 sums of each file included in the archive.
|
||||
7. For each of the formats, create an archive file in the output
|
||||
directory (specified with `-o`) with that format.
|
||||
- If zip is included, create a .zip file.
|
||||
- If tgz is included, create a .tar.gz file with default compression
|
||||
levels.
|
||||
- All archive files should include any generated files (single
|
||||
and/or individual) in the top-level directory.
|
||||
8. In the output directory, create a file with the same name as
|
||||
config.hashes that contains the SHA256 sum of all files created.
|
||||
|
||||
-----
|
||||
|
||||
The outputs.include_single and outputs.include_individual describe
|
||||
what should go in the final archive. If both are specified, the output
|
||||
archive should include both a single bundle.pem and each individual
|
||||
certificate, for example.
|
||||
|
||||
-----
|
||||
|
||||
As it stands, given the following `bundle.yaml`:
|
||||
|
||||
``` yaml
|
||||
config:
|
||||
hashes: bundle.sha256
|
||||
expiry: 1y
|
||||
chains:
|
||||
core_certs:
|
||||
certs:
|
||||
- root: pems/gts-r1.pem
|
||||
intermediates:
|
||||
- pems/goog-wr2.pem
|
||||
outputs:
|
||||
include_single: true
|
||||
include_individual: true
|
||||
manifest: true
|
||||
formats:
|
||||
- zip
|
||||
- tgz
|
||||
- root: pems/isrg-root-x1.pem
|
||||
intermediates:
|
||||
- pems/le-e7.pem
|
||||
outputs:
|
||||
include_single: true
|
||||
include_individual: false
|
||||
manifest: true
|
||||
formats:
|
||||
- zip
|
||||
- tgz
|
||||
google_certs:
|
||||
certs:
|
||||
- root: pems/gts-r1.pem
|
||||
intermediates:
|
||||
- pems/goog-wr2.pem
|
||||
outputs:
|
||||
include_single: true
|
||||
include_individual: false
|
||||
manifest: true
|
||||
formats:
|
||||
- tgz
|
||||
lets_encrypt:
|
||||
certs:
|
||||
- root: pems/isrg-root-x1.pem
|
||||
intermediates:
|
||||
- pems/le-e7.pem
|
||||
outputs:
|
||||
include_single: false
|
||||
include_individual: true
|
||||
manifest: false
|
||||
formats:
|
||||
- zip
|
||||
```
|
||||
|
||||
The program outputs the following files:
|
||||
|
||||
- bundle.sha256
|
||||
- core_certs_0.tgz (contains individual certs)
|
||||
- core_certs_0.zip (contains individual certs)
|
||||
- core_certs_1.tgz (contains core_certs.pem)
|
||||
- core_certs_1.zip (contains core_certs.pem)
|
||||
- google_certs_0.tgz
|
||||
- lets_encrypt_0.zip
|
||||
|
||||
It should output
|
||||
|
||||
- bundle.sha256
|
||||
- core_certs.tgz
|
||||
- core_certs.zip
|
||||
- google_certs.tgz
|
||||
- lets_encrypt.zip
|
||||
|
||||
core_certs.* should contain `bundle.pem` and all the individual
|
||||
certs. There should be no _$n$ variants of archives.
|
||||
|
||||
-----
|
||||
|
||||
Add an additional field to outputs: encoding. It should accept one of
|
||||
`der`, `pem`, or `both`. If `der`, certificates should be output as a
|
||||
`.crt` file containing a DER-encoded certificate. If `pem`, certificates
|
||||
should be output as a `.pem` file containing a PEM-encoded certificate.
|
||||
If both, both the `.crt` and `.pem` certificate should be included.
|
||||
|
||||
For example, given the previous config, if `encoding` is der, the
|
||||
google_certs.tgz archive should contain
|
||||
|
||||
- bundle.crt
|
||||
- MANIFEST
|
||||
|
||||
Or with lets_encrypt.zip:
|
||||
|
||||
- isrg-root-x1.crt
|
||||
- le-e7.crt
|
||||
|
||||
However, if `encoding` is pem, the lets_encrypt.zip archive should contain:
|
||||
|
||||
- isrg-root-x1.pem
|
||||
- le-e7.pem
|
||||
|
||||
And if it `encoding` is both, the lets_encrypt.zip archive should contain:
|
||||
|
||||
- isrg-root-x1.crt
|
||||
- isrg-root-x1.pem
|
||||
- le-e7.crt
|
||||
- le-e7.pem
|
||||
|
||||
-----
|
||||
|
||||
The tgz format should output a `.tar.gz` file instead of a `.tgz` file.
|
||||
|
||||
-----
|
||||
|
||||
Move the format extensions to a global variable.
|
||||
|
||||
-----
|
||||
|
||||
Write a README.txt with a description of the bundle.yaml format.
|
||||
|
||||
Additionally, update the help text for the program (e.g. with `-h`)
|
||||
to provide the same detailed information.
|
||||
|
||||
-----
|
||||
|
||||
It may be easier to embed the README.txt in the program on build.
|
||||
|
||||
|
||||
43
cmd/cert-bundler/testdata/bundle.yaml
vendored
Normal file
43
cmd/cert-bundler/testdata/bundle.yaml
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
config:
|
||||
hashes: bundle.sha256
|
||||
expiry: 1y
|
||||
chains:
|
||||
core_certs:
|
||||
certs:
|
||||
- root: pems/gts-r1.pem
|
||||
intermediates:
|
||||
- pems/goog-wr2.pem
|
||||
- root: pems/isrg-root-x1.pem
|
||||
intermediates:
|
||||
- pems/le-e7.pem
|
||||
outputs:
|
||||
include_single: true
|
||||
include_individual: true
|
||||
manifest: true
|
||||
formats:
|
||||
- zip
|
||||
- tgz
|
||||
google_certs:
|
||||
certs:
|
||||
- root: pems/gts-r1.pem
|
||||
intermediates:
|
||||
- pems/goog-wr2.pem
|
||||
outputs:
|
||||
include_single: true
|
||||
include_individual: false
|
||||
manifest: true
|
||||
encoding: der
|
||||
formats:
|
||||
- tgz
|
||||
lets_encrypt:
|
||||
certs:
|
||||
- root: pems/isrg-root-x1.pem
|
||||
intermediates:
|
||||
- pems/le-e7.pem
|
||||
outputs:
|
||||
include_single: false
|
||||
include_individual: true
|
||||
manifest: false
|
||||
encoding: both
|
||||
formats:
|
||||
- zip
|
||||
29
cmd/cert-bundler/testdata/pems/goog-wr2.pem
vendored
Normal file
29
cmd/cert-bundler/testdata/pems/goog-wr2.pem
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIFCzCCAvOgAwIBAgIQf/AFoHxM3tEArZ1mpRB7mDANBgkqhkiG9w0BAQsFADBH
|
||||
MQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExM
|
||||
QzEUMBIGA1UEAxMLR1RTIFJvb3QgUjEwHhcNMjMxMjEzMDkwMDAwWhcNMjkwMjIw
|
||||
MTQwMDAwWjA7MQswCQYDVQQGEwJVUzEeMBwGA1UEChMVR29vZ2xlIFRydXN0IFNl
|
||||
cnZpY2VzMQwwCgYDVQQDEwNXUjIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK
|
||||
AoIBAQCp/5x/RR5wqFOfytnlDd5GV1d9vI+aWqxG8YSau5HbyfsvAfuSCQAWXqAc
|
||||
+MGr+XgvSszYhaLYWTwO0xj7sfUkDSbutltkdnwUxy96zqhMt/TZCPzfhyM1IKji
|
||||
aeKMTj+xWfpgoh6zySBTGYLKNlNtYE3pAJH8do1cCA8Kwtzxc2vFE24KT3rC8gIc
|
||||
LrRjg9ox9i11MLL7q8Ju26nADrn5Z9TDJVd06wW06Y613ijNzHoU5HEDy01hLmFX
|
||||
xRmpC5iEGuh5KdmyjS//V2pm4M6rlagplmNwEmceOuHbsCFx13ye/aoXbv4r+zgX
|
||||
FNFmp6+atXDMyGOBOozAKql2N87jAgMBAAGjgf4wgfswDgYDVR0PAQH/BAQDAgGG
|
||||
MB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjASBgNVHRMBAf8ECDAGAQH/
|
||||
AgEAMB0GA1UdDgQWBBTeGx7teRXUPjckwyG77DQ5bUKyMDAfBgNVHSMEGDAWgBTk
|
||||
rysmcRorSCeFL1JmLO/wiRNxPjA0BggrBgEFBQcBAQQoMCYwJAYIKwYBBQUHMAKG
|
||||
GGh0dHA6Ly9pLnBraS5nb29nL3IxLmNydDArBgNVHR8EJDAiMCCgHqAchhpodHRw
|
||||
Oi8vYy5wa2kuZ29vZy9yL3IxLmNybDATBgNVHSAEDDAKMAgGBmeBDAECATANBgkq
|
||||
hkiG9w0BAQsFAAOCAgEARXWL5R87RBOWGqtY8TXJbz3S0DNKhjO6V1FP7sQ02hYS
|
||||
TL8Tnw3UVOlIecAwPJQl8hr0ujKUtjNyC4XuCRElNJThb0Lbgpt7fyqaqf9/qdLe
|
||||
SiDLs/sDA7j4BwXaWZIvGEaYzq9yviQmsR4ATb0IrZNBRAq7x9UBhb+TV+PfdBJT
|
||||
DhEl05vc3ssnbrPCuTNiOcLgNeFbpwkuGcuRKnZc8d/KI4RApW//mkHgte8y0YWu
|
||||
ryUJ8GLFbsLIbjL9uNrizkqRSvOFVU6xddZIMy9vhNkSXJ/UcZhjJY1pXAprffJB
|
||||
vei7j+Qi151lRehMCofa6WBmiA4fx+FOVsV2/7R6V2nyAiIJJkEd2nSi5SnzxJrl
|
||||
Xdaqev3htytmOPvoKWa676ATL/hzfvDaQBEcXd2Ppvy+275W+DKcH0FBbX62xevG
|
||||
iza3F4ydzxl6NJ8hk8R+dDXSqv1MbRT1ybB5W0k8878XSOjvmiYTDIfyc9acxVJr
|
||||
Y/cykHipa+te1pOhv7wYPYtZ9orGBV5SGOJm4NrB3K1aJar0RfzxC3ikr7Dyc6Qw
|
||||
qDTBU39CluVIQeuQRgwG3MuSxl7zRERDRilGoKb8uY45JzmxWuKxrfwT/478JuHU
|
||||
/oTxUFqOl2stKnn7QGTq8z29W+GgBLCXSBxC9epaHM0myFH/FJlniXJfHeytWt0=
|
||||
-----END CERTIFICATE-----
|
||||
31
cmd/cert-bundler/testdata/pems/gts-r1.pem
vendored
Normal file
31
cmd/cert-bundler/testdata/pems/gts-r1.pem
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
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
|
||||
bP6MvPJwNQzcmRk13NfIRmPVNnGuV/u3gm3c
|
||||
-----END CERTIFICATE-----
|
||||
31
cmd/cert-bundler/testdata/pems/isrg-root-x1.pem
vendored
Normal file
31
cmd/cert-bundler/testdata/pems/isrg-root-x1.pem
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
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
|
||||
-----END CERTIFICATE-----
|
||||
26
cmd/cert-bundler/testdata/pems/le-e7.pem
vendored
Normal file
26
cmd/cert-bundler/testdata/pems/le-e7.pem
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIEVzCCAj+gAwIBAgIRAKp18eYrjwoiCWbTi7/UuqEwDQYJKoZIhvcNAQELBQAw
|
||||
TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
|
||||
cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMjQwMzEzMDAwMDAw
|
||||
WhcNMjcwMzEyMjM1OTU5WjAyMQswCQYDVQQGEwJVUzEWMBQGA1UEChMNTGV0J3Mg
|
||||
RW5jcnlwdDELMAkGA1UEAxMCRTcwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAARB6AST
|
||||
CFh/vjcwDMCgQer+VtqEkz7JANurZxLP+U9TCeioL6sp5Z8VRvRbYk4P1INBmbef
|
||||
QHJFHCxcSjKmwtvGBWpl/9ra8HW0QDsUaJW2qOJqceJ0ZVFT3hbUHifBM/2jgfgw
|
||||
gfUwDgYDVR0PAQH/BAQDAgGGMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcD
|
||||
ATASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBSuSJ7chx1EoG/aouVgdAR4
|
||||
wpwAgDAfBgNVHSMEGDAWgBR5tFnme7bl5AFzgAiIyBpY9umbbjAyBggrBgEFBQcB
|
||||
AQQmMCQwIgYIKwYBBQUHMAKGFmh0dHA6Ly94MS5pLmxlbmNyLm9yZy8wEwYDVR0g
|
||||
BAwwCjAIBgZngQwBAgEwJwYDVR0fBCAwHjAcoBqgGIYWaHR0cDovL3gxLmMubGVu
|
||||
Y3Iub3JnLzANBgkqhkiG9w0BAQsFAAOCAgEAjx66fDdLk5ywFn3CzA1w1qfylHUD
|
||||
aEf0QZpXcJseddJGSfbUUOvbNR9N/QQ16K1lXl4VFyhmGXDT5Kdfcr0RvIIVrNxF
|
||||
h4lqHtRRCP6RBRstqbZ2zURgqakn/Xip0iaQL0IdfHBZr396FgknniRYFckKORPG
|
||||
yM3QKnd66gtMst8I5nkRQlAg/Jb+Gc3egIvuGKWboE1G89NTsN9LTDD3PLj0dUMr
|
||||
OIuqVjLB8pEC6yk9enrlrqjXQgkLEYhXzq7dLafv5Vkig6Gl0nuuqjqfp0Q1bi1o
|
||||
yVNAlXe6aUXw92CcghC9bNsKEO1+M52YY5+ofIXlS/SEQbvVYYBLZ5yeiglV6t3S
|
||||
M6H+vTG0aP9YHzLn/KVOHzGQfXDP7qM5tkf+7diZe7o2fw6O7IvN6fsQXEQQj8TJ
|
||||
UXJxv2/uJhcuy/tSDgXwHM8Uk34WNbRT7zGTGkQRX0gsbjAea/jYAoWv0ZvQRwpq
|
||||
Pe79D/i7Cep8qWnA+7AE/3B3S/3dEEYmc0lpe1366A/6GEgk3ktr9PEoQrLChs6I
|
||||
tu3wnNLB2euC8IKGLQFpGtOO/2/hiAKjyajaBP25w1jF0Wl8Bbqne3uZ2q1GyPFJ
|
||||
YRmT7/OXpmOH/FVLtwS+8ng1cAmpCujPwteJZNcDG0sF2n/sc0+SQf49fdyUK0ty
|
||||
+VUwFj9tmWxyR/M=
|
||||
-----END CERTIFICATE-----
|
||||
4
cmd/cert-bundler/testdata/pkg/bundle.sha256
vendored
Normal file
4
cmd/cert-bundler/testdata/pkg/bundle.sha256
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
5ed8bf9ed693045faa8a5cb0edc4a870052e56aef6291ce8b1604565affbc2a4 core_certs.zip
|
||||
e59eddc590d2f7b790a87c5b56e81697088ab54be382c0e2c51b82034006d308 core_certs.tgz
|
||||
51b9b63b1335118079e90700a3a5b847c363808e9116e576ca84f301bc433289 google_certs.tgz
|
||||
3d1910ca8835c3ded1755a8c7d6c48083c2f3ff68b2bfbf932aaf27e29d0a232 lets_encrypt.zip
|
||||
BIN
cmd/cert-bundler/testdata/pkg/core_certs.tgz
vendored
Normal file
BIN
cmd/cert-bundler/testdata/pkg/core_certs.tgz
vendored
Normal file
Binary file not shown.
BIN
cmd/cert-bundler/testdata/pkg/core_certs.zip
vendored
Normal file
BIN
cmd/cert-bundler/testdata/pkg/core_certs.zip
vendored
Normal file
Binary file not shown.
BIN
cmd/cert-bundler/testdata/pkg/google_certs.tgz
vendored
Normal file
BIN
cmd/cert-bundler/testdata/pkg/google_certs.tgz
vendored
Normal file
Binary file not shown.
BIN
cmd/cert-bundler/testdata/pkg/lets_encrypt.zip
vendored
Normal file
BIN
cmd/cert-bundler/testdata/pkg/lets_encrypt.zip
vendored
Normal file
Binary file not shown.
34
cmd/tlsinfo/README.txt
Normal file
34
cmd/tlsinfo/README.txt
Normal file
@@ -0,0 +1,34 @@
|
||||
tlsinfo: show TLS version, cipher, and peer certificates
|
||||
---------------------------------------------------------
|
||||
|
||||
Description
|
||||
tlsinfo connects to a TLS server and prints the negotiated TLS version and
|
||||
cipher suite, followed by details for each certificate in the server’s
|
||||
presented chain (as provided by the server).
|
||||
|
||||
Usage
|
||||
tlsinfo <hostname:port>
|
||||
|
||||
Output
|
||||
The program prints the negotiated protocol and cipher, then one section per
|
||||
certificate in the order received from the server. Example fields:
|
||||
TLS Version: TLS 1.3
|
||||
Cipher Suite: TLS_AES_128_GCM_SHA256
|
||||
Certificate 1
|
||||
Subject: CN=example.com, O=Example Corp, C=US
|
||||
Issuer: CN=Example Root CA, O=Example Corp, C=US
|
||||
DNS Names: [example.com www.example.com]
|
||||
Not Before: 2025-01-01 00:00:00 +0000 UTC
|
||||
Not After: 2026-01-01 23:59:59 +0000 UTC
|
||||
|
||||
Examples
|
||||
# Inspect a public HTTPS endpoint
|
||||
tlsinfo example.com:443
|
||||
|
||||
Notes
|
||||
- Verification is intentionally disabled (InsecureSkipVerify=true). The tool
|
||||
does not validate the server certificate or hostname; it is for inspection
|
||||
only.
|
||||
- The SNI/ServerName is inferred from <hostname> when applicable.
|
||||
- You must specify a port (e.g., 443 for HTTPS).
|
||||
- The entire certificate chain is printed exactly as presented by the server.
|
||||
64
cmd/tlsinfo/main.go
Normal file
64
cmd/tlsinfo/main.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if len(os.Args) != 2 {
|
||||
fmt.Printf("Usage: %s ‹hostname:port>\n", os.Args[0])
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
hostPort := os.Args[1]
|
||||
conn, err := tls.Dial("tcp", hostPort, &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to connect to the TLS server: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer conn.Close()
|
||||
state := conn.ConnectionState()
|
||||
printConnectionDetails(state)
|
||||
}
|
||||
|
||||
func printConnectionDetails(state tls.ConnectionState) {
|
||||
version := tlsVersion(state.Version)
|
||||
cipherSuite := tls.CipherSuiteName(state.CipherSuite)
|
||||
fmt.Printf("TLS Version: %s\n", version)
|
||||
fmt.Printf("Cipher Suite: %s\n", cipherSuite)
|
||||
printPeerCertificates(state.PeerCertificates)
|
||||
}
|
||||
|
||||
func tlsVersion(version uint16) string {
|
||||
switch version {
|
||||
|
||||
case tls.VersionTLS13:
|
||||
return "TLS 1.3"
|
||||
case tls.VersionTLS12:
|
||||
|
||||
return "TLS 1.2"
|
||||
case tls.VersionTLS11:
|
||||
return "TLS 1.1"
|
||||
case tls.VersionTLS10:
|
||||
return "TLS 1.0"
|
||||
default:
|
||||
return "Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
func printPeerCertificates(certificates []*x509.Certificate) {
|
||||
for i, cert := range certificates {
|
||||
fmt.Printf("Certificate %d\n", i+1)
|
||||
fmt.Printf("\tSubject: %s\n", cert.Subject)
|
||||
fmt.Printf("\tIssuer: %s\n", cert.Issuer)
|
||||
fmt.Printf("\tDNS Names: %v\n", cert.DNSNames)
|
||||
fmt.Printf("\tNot Before: %s\n:", cert.NotBefore)
|
||||
fmt.Printf("\tNot After: %s\n", cert.NotAfter)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user