Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dda9fd9f07 | |||
| c251c1e1b5 | |||
| 6eb533f79b | |||
| ea5ffa4828 | |||
| aa96e47112 | |||
| d34a417dce | |||
| d11e0cf9f9 | |||
| aad7d68599 | |||
| 4560868688 | |||
| 8d5406256f | |||
| 9280e846fa |
35
.github/workflows/release.yml
vendored
Normal file
35
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
workflow_dispatch: {}
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
goreleaser:
|
||||
name: GoReleaser
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
cache: true
|
||||
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v6
|
||||
with:
|
||||
distribution: goreleaser
|
||||
version: latest
|
||||
args: release --clean
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,2 +1,5 @@
|
||||
.idea
|
||||
cmd/cert-bundler/testdata/pkg/*
|
||||
# Added by goreleaser init:
|
||||
dist/
|
||||
cmd/cert-bundler/testdata/bundle/
|
||||
|
||||
456
.goreleaser.yaml
Normal file
456
.goreleaser.yaml
Normal file
@@ -0,0 +1,456 @@
|
||||
# This is an example .goreleaser.yml file with some sensible defaults.
|
||||
# Make sure to check the documentation at https://goreleaser.com
|
||||
|
||||
# The lines below are called `modelines`. See `:help modeline`
|
||||
# Feel free to remove those if you don't want/need to use them.
|
||||
# yaml-language-server: $schema=https://goreleaser.com/static/schema.json
|
||||
# vim: set ts=2 sw=2 tw=0 fo=cnqoj
|
||||
|
||||
version: 2
|
||||
|
||||
before:
|
||||
hooks:
|
||||
# You may remove this if you don't use go modules.
|
||||
- go mod tidy
|
||||
# you may remove this if you don't need go generate
|
||||
- go generate ./...
|
||||
|
||||
builds:
|
||||
- id: atping
|
||||
main: ./cmd/atping/main.go
|
||||
binary: atping
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos: [linux, darwin]
|
||||
goarch: [amd64, arm64]
|
||||
ignore:
|
||||
- goos: darwin
|
||||
goarch: amd64
|
||||
- id: ca-signed
|
||||
main: ./cmd/ca-signed/main.go
|
||||
binary: ca-signed
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos: [linux, darwin]
|
||||
goarch: [amd64, arm64]
|
||||
ignore:
|
||||
- goos: darwin
|
||||
goarch: amd64
|
||||
- id: cert-bundler
|
||||
main: ./cmd/cert-bundler/main.go
|
||||
binary: cert-bundler
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos: [linux, darwin]
|
||||
goarch: [amd64, arm64]
|
||||
ignore:
|
||||
- goos: darwin
|
||||
goarch: amd64
|
||||
- id: cert-revcheck
|
||||
main: ./cmd/cert-revcheck/main.go
|
||||
binary: cert-revcheck
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos: [linux, darwin]
|
||||
goarch: [amd64, arm64]
|
||||
ignore:
|
||||
- goos: darwin
|
||||
goarch: amd64
|
||||
- id: certchain
|
||||
main: ./cmd/certchain/main.go
|
||||
binary: certchain
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos: [linux, darwin]
|
||||
goarch: [amd64, arm64]
|
||||
ignore:
|
||||
- goos: darwin
|
||||
goarch: amd64
|
||||
- id: certdump
|
||||
main: ./cmd/certdump/main.go
|
||||
binary: certdump
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos: [linux, darwin]
|
||||
goarch: [amd64, arm64]
|
||||
ignore:
|
||||
- goos: darwin
|
||||
goarch: amd64
|
||||
- id: certexpiry
|
||||
main: ./cmd/certexpiry/main.go
|
||||
binary: certexpiry
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos: [linux, darwin]
|
||||
goarch: [amd64, arm64]
|
||||
ignore:
|
||||
- goos: darwin
|
||||
goarch: amd64
|
||||
- id: certser
|
||||
main: ./cmd/certser/main.go
|
||||
binary: certser
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos: [linux, darwin]
|
||||
goarch: [amd64, arm64]
|
||||
ignore:
|
||||
- goos: darwin
|
||||
goarch: amd64
|
||||
- id: certverify
|
||||
main: ./cmd/certverify/main.go
|
||||
binary: certverify
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos: [linux, darwin]
|
||||
goarch: [amd64, arm64]
|
||||
ignore:
|
||||
- goos: darwin
|
||||
goarch: amd64
|
||||
- id: clustersh
|
||||
main: ./cmd/clustersh/main.go
|
||||
binary: clustersh
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos: [linux, darwin]
|
||||
goarch: [amd64, arm64]
|
||||
ignore:
|
||||
- goos: darwin
|
||||
goarch: amd64
|
||||
- id: cruntar
|
||||
main: ./cmd/cruntar/main.go
|
||||
binary: cruntar
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos: [linux, darwin]
|
||||
goarch: [amd64, arm64]
|
||||
ignore:
|
||||
- goos: darwin
|
||||
goarch: amd64
|
||||
- id: csrpubdump
|
||||
main: ./cmd/csrpubdump/main.go
|
||||
binary: csrpubdump
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos: [linux, darwin]
|
||||
goarch: [amd64, arm64]
|
||||
ignore:
|
||||
- goos: darwin
|
||||
goarch: amd64
|
||||
- id: data_sync
|
||||
main: ./cmd/data_sync/main.go
|
||||
binary: data_sync
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos: [linux, darwin]
|
||||
goarch: [amd64, arm64]
|
||||
ignore:
|
||||
- goos: darwin
|
||||
goarch: amd64
|
||||
- id: diskimg
|
||||
main: ./cmd/diskimg/main.go
|
||||
binary: diskimg
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos: [linux, darwin]
|
||||
goarch: [amd64, arm64]
|
||||
ignore:
|
||||
- goos: darwin
|
||||
goarch: amd64
|
||||
- id: dumpbytes
|
||||
main: ./cmd/dumpbytes/main.go
|
||||
binary: dumpbytes
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos: [linux, darwin]
|
||||
goarch: [amd64, arm64]
|
||||
ignore:
|
||||
- goos: darwin
|
||||
goarch: amd64
|
||||
- id: eig
|
||||
main: ./cmd/eig/main.go
|
||||
binary: eig
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos: [linux, darwin]
|
||||
goarch: [amd64, arm64]
|
||||
ignore:
|
||||
- goos: darwin
|
||||
goarch: amd64
|
||||
- id: fragment
|
||||
main: ./cmd/fragment/main.go
|
||||
binary: fragment
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos: [linux, darwin]
|
||||
goarch: [amd64, arm64]
|
||||
ignore:
|
||||
- goos: darwin
|
||||
goarch: amd64
|
||||
- id: host
|
||||
main: ./cmd/host/main.go
|
||||
binary: host
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos: [linux, darwin]
|
||||
goarch: [amd64, arm64]
|
||||
ignore:
|
||||
- goos: darwin
|
||||
goarch: amd64
|
||||
- id: jlp
|
||||
main: ./cmd/jlp/main.go
|
||||
binary: jlp
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos: [linux, darwin]
|
||||
goarch: [amd64, arm64]
|
||||
ignore:
|
||||
- goos: darwin
|
||||
goarch: amd64
|
||||
- id: kgz
|
||||
main: ./cmd/kgz/main.go
|
||||
binary: kgz
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos: [linux, darwin]
|
||||
goarch: [amd64, arm64]
|
||||
ignore:
|
||||
- goos: darwin
|
||||
goarch: amd64
|
||||
- id: minmax
|
||||
main: ./cmd/minmax/main.go
|
||||
binary: minmax
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos: [linux, darwin]
|
||||
goarch: [amd64, arm64]
|
||||
ignore:
|
||||
- goos: darwin
|
||||
goarch: amd64
|
||||
- id: parts
|
||||
main: ./cmd/parts/main.go
|
||||
binary: parts
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos: [linux, darwin]
|
||||
goarch: [amd64, arm64]
|
||||
ignore:
|
||||
- goos: darwin
|
||||
goarch: amd64
|
||||
- id: pem2bin
|
||||
main: ./cmd/pem2bin/main.go
|
||||
binary: pem2bin
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos: [linux, darwin]
|
||||
goarch: [amd64, arm64]
|
||||
ignore:
|
||||
- goos: darwin
|
||||
goarch: amd64
|
||||
- id: pembody
|
||||
main: ./cmd/pembody/main.go
|
||||
binary: pembody
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos: [linux, darwin]
|
||||
goarch: [amd64, arm64]
|
||||
ignore:
|
||||
- goos: darwin
|
||||
goarch: amd64
|
||||
- id: pemit
|
||||
main: ./cmd/pemit/main.go
|
||||
binary: pemit
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos: [linux, darwin]
|
||||
goarch: [amd64, arm64]
|
||||
ignore:
|
||||
- goos: darwin
|
||||
goarch: amd64
|
||||
- id: readchain
|
||||
main: ./cmd/readchain/main.go
|
||||
binary: readchain
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos: [linux, darwin]
|
||||
goarch: [amd64, arm64]
|
||||
ignore:
|
||||
- goos: darwin
|
||||
goarch: amd64
|
||||
- id: renfnv
|
||||
main: ./cmd/renfnv/main.go
|
||||
binary: renfnv
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos: [linux, darwin]
|
||||
goarch: [amd64, arm64]
|
||||
ignore:
|
||||
- goos: darwin
|
||||
goarch: amd64
|
||||
- id: rhash
|
||||
main: ./cmd/rhash/main.go
|
||||
binary: rhash
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos: [linux, darwin]
|
||||
goarch: [amd64, arm64]
|
||||
ignore:
|
||||
- goos: darwin
|
||||
goarch: amd64
|
||||
- id: rolldie
|
||||
main: ./cmd/rolldie/main.go
|
||||
binary: rolldie
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos: [linux, darwin]
|
||||
goarch: [amd64, arm64]
|
||||
ignore:
|
||||
- goos: darwin
|
||||
goarch: amd64
|
||||
- id: showimp
|
||||
main: ./cmd/showimp/main.go
|
||||
binary: showimp
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos: [linux, darwin]
|
||||
goarch: [amd64, arm64]
|
||||
ignore:
|
||||
- goos: darwin
|
||||
goarch: amd64
|
||||
- id: ski
|
||||
main: ./cmd/ski/main.go
|
||||
binary: ski
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos: [linux, darwin]
|
||||
goarch: [amd64, arm64]
|
||||
ignore:
|
||||
- goos: darwin
|
||||
goarch: amd64
|
||||
- id: sprox
|
||||
main: ./cmd/sprox/main.go
|
||||
binary: sprox
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos: [linux, darwin]
|
||||
goarch: [amd64, arm64]
|
||||
ignore:
|
||||
- goos: darwin
|
||||
goarch: amd64
|
||||
- id: stealchain
|
||||
main: ./cmd/stealchain/main.go
|
||||
binary: stealchain
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos: [linux, darwin]
|
||||
goarch: [amd64, arm64]
|
||||
ignore:
|
||||
- goos: darwin
|
||||
goarch: amd64
|
||||
- id: stealchain-server
|
||||
main: ./cmd/stealchain-server/main.go
|
||||
binary: stealchain-server
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos: [linux, darwin]
|
||||
goarch: [amd64, arm64]
|
||||
ignore:
|
||||
- goos: darwin
|
||||
goarch: amd64
|
||||
- id: subjhash
|
||||
main: ./cmd/subjhash/main.go
|
||||
binary: subjhash
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos: [linux, darwin]
|
||||
goarch: [amd64, arm64]
|
||||
ignore:
|
||||
- goos: darwin
|
||||
goarch: amd64
|
||||
- id: tlsinfo
|
||||
main: ./cmd/tlsinfo/main.go
|
||||
binary: tlsinfo
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos: [linux, darwin]
|
||||
goarch: [amd64, arm64]
|
||||
ignore:
|
||||
- goos: darwin
|
||||
goarch: amd64
|
||||
- id: tlskeypair
|
||||
main: ./cmd/tlskeypair/main.go
|
||||
binary: tlskeypair
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos: [linux, darwin]
|
||||
goarch: [amd64, arm64]
|
||||
ignore:
|
||||
- goos: darwin
|
||||
goarch: amd64
|
||||
- id: utc
|
||||
main: ./cmd/utc/main.go
|
||||
binary: utc
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos: [linux, darwin]
|
||||
goarch: [amd64, arm64]
|
||||
ignore:
|
||||
- goos: darwin
|
||||
goarch: amd64
|
||||
- id: yamll
|
||||
main: ./cmd/yamll/main.go
|
||||
binary: yamll
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos: [linux, darwin]
|
||||
goarch: [amd64, arm64]
|
||||
ignore:
|
||||
- goos: darwin
|
||||
goarch: amd64
|
||||
- id: zsearch
|
||||
main: ./cmd/zsearch/main.go
|
||||
binary: zsearch
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos: [linux, darwin]
|
||||
goarch: [amd64, arm64]
|
||||
ignore:
|
||||
- goos: darwin
|
||||
goarch: amd64
|
||||
|
||||
archives:
|
||||
- formats: [tar.gz]
|
||||
# this name template makes the OS and Arch compatible with the results of `uname`.
|
||||
name_template: >-
|
||||
{{ .ProjectName }}_
|
||||
{{- title .Os }}_
|
||||
{{- if eq .Arch "amd64" }}x86_64
|
||||
{{- else if eq .Arch "386" }}i386
|
||||
{{- else }}{{ .Arch }}{{ end }}
|
||||
{{- if .Arm }}v{{ .Arm }}{{ end }}
|
||||
# use zip for windows archives
|
||||
format_overrides:
|
||||
- goos: windows
|
||||
formats: [zip]
|
||||
|
||||
changelog:
|
||||
sort: asc
|
||||
filters:
|
||||
exclude:
|
||||
- "^docs:"
|
||||
- "^test:"
|
||||
|
||||
gitea_urls:
|
||||
api: https://git.wntrmute.dev/api/v1
|
||||
download: https://git.wntrmute.dev/
|
||||
# set to true if you use a self-signed certificate
|
||||
skip_tls_verify: false
|
||||
|
||||
release:
|
||||
github:
|
||||
owner: kisom
|
||||
name: goutils
|
||||
footer: >-
|
||||
|
||||
---
|
||||
|
||||
Released by [GoReleaser](https://github.com/goreleaser/goreleaser).
|
||||
20
CHANGELOG
20
CHANGELOG
@@ -1,5 +1,25 @@
|
||||
CHANGELOG
|
||||
|
||||
v1.13.4 - 2025-11-18
|
||||
|
||||
Changed:
|
||||
- build: updating goreleaser config.
|
||||
|
||||
v1.13.3 - 2025-11-18
|
||||
|
||||
Added:
|
||||
- certlib: introduce `Fetcher` for retrieving certificates.
|
||||
- lib: `HexEncode` gains a byte-slice output variant.
|
||||
- build: add GoReleaser configuration.
|
||||
|
||||
Changed:
|
||||
- cmd: migrate programs to use `certlib.Fetcher` for certificate retrieval
|
||||
(includes `certdump`, `ski`, and others).
|
||||
- cmd/ski: update display mode.
|
||||
|
||||
Misc:
|
||||
- repository fixups and small cleanups.
|
||||
|
||||
v1.13.2 - 2025-11-17
|
||||
|
||||
Add:
|
||||
|
||||
@@ -91,7 +91,7 @@ func TestReset(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
const decay = 25 * time.Millisecond
|
||||
const decay = time.Second
|
||||
const maxDuration = 10 * time.Millisecond
|
||||
const interval = time.Millisecond
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -483,11 +484,19 @@ func encodeCertsToPEM(certs []*x509.Certificate) []byte {
|
||||
}
|
||||
|
||||
func generateManifest(files []fileEntry) []byte {
|
||||
var manifest strings.Builder
|
||||
for _, file := range files {
|
||||
if file.name == "MANIFEST" {
|
||||
// Build a sorted list of files by filename to ensure deterministic manifest ordering
|
||||
sorted := make([]fileEntry, 0, len(files))
|
||||
for _, f := range files {
|
||||
// Defensive: skip any existing manifest entry
|
||||
if f.name == "MANIFEST" {
|
||||
continue
|
||||
}
|
||||
sorted = append(sorted, f)
|
||||
}
|
||||
sort.Slice(sorted, func(i, j int) bool { return sorted[i].name < sorted[j].name })
|
||||
|
||||
var manifest strings.Builder
|
||||
for _, file := range sorted {
|
||||
hash := sha256.Sum256(file.content)
|
||||
manifest.WriteString(fmt.Sprintf("%x %s\n", hash, file.name))
|
||||
}
|
||||
|
||||
175
certlib/fetch.go
Normal file
175
certlib/fetch.go
Normal file
@@ -0,0 +1,175 @@
|
||||
package certlib
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
|
||||
"git.wntrmute.dev/kyle/goutils/certlib/hosts"
|
||||
"git.wntrmute.dev/kyle/goutils/fileutil"
|
||||
"git.wntrmute.dev/kyle/goutils/lib"
|
||||
)
|
||||
|
||||
// FetcherOpts are options for fetching certificates. They are only applicable to ServerFetcher.
|
||||
type FetcherOpts struct {
|
||||
SkipVerify bool
|
||||
Roots *x509.CertPool
|
||||
}
|
||||
|
||||
// Fetcher is an interface for fetching certificates from a remote source. It
|
||||
// currently supports fetching from a server or a file.
|
||||
type Fetcher interface {
|
||||
Get() (*x509.Certificate, error)
|
||||
GetChain() ([]*x509.Certificate, error)
|
||||
String() string
|
||||
}
|
||||
|
||||
type ServerFetcher struct {
|
||||
host string
|
||||
port int
|
||||
insecure bool
|
||||
roots *x509.CertPool
|
||||
}
|
||||
|
||||
// WithRoots sets the roots for the ServerFetcher.
|
||||
func WithRoots(roots *x509.CertPool) func(*ServerFetcher) {
|
||||
return func(sf *ServerFetcher) {
|
||||
sf.roots = roots
|
||||
}
|
||||
}
|
||||
|
||||
// WithSkipVerify sets the insecure flag for the ServerFetcher.
|
||||
func WithSkipVerify() func(*ServerFetcher) {
|
||||
return func(sf *ServerFetcher) {
|
||||
sf.insecure = true
|
||||
}
|
||||
}
|
||||
|
||||
// ParseServer parses a server string into a ServerFetcher. It can be a URL or a
|
||||
// a host:port pair.
|
||||
func ParseServer(host string) (*ServerFetcher, error) {
|
||||
target, err := hosts.ParseHost(host)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse server: %w", err)
|
||||
}
|
||||
|
||||
return &ServerFetcher{
|
||||
host: target.Host,
|
||||
port: target.Port,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (sf *ServerFetcher) String() string {
|
||||
return fmt.Sprintf("tls://%s", net.JoinHostPort(sf.host, lib.Itoa(sf.port, -1)))
|
||||
}
|
||||
|
||||
func (sf *ServerFetcher) GetChain() ([]*x509.Certificate, error) {
|
||||
config := &tls.Config{
|
||||
InsecureSkipVerify: sf.insecure, // #nosec G402 - no shit sherlock
|
||||
RootCAs: sf.roots,
|
||||
}
|
||||
|
||||
dialer := &tls.Dialer{
|
||||
Config: config,
|
||||
}
|
||||
|
||||
hostSpec := net.JoinHostPort(sf.host, lib.Itoa(sf.port, -1))
|
||||
|
||||
netConn, err := dialer.DialContext(context.Background(), "tcp", hostSpec)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("dialing server: %w", err)
|
||||
}
|
||||
|
||||
conn, ok := netConn.(*tls.Conn)
|
||||
if !ok {
|
||||
return nil, errors.New("connection is not TLS")
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
state := conn.ConnectionState()
|
||||
return state.PeerCertificates, nil
|
||||
}
|
||||
|
||||
func (sf *ServerFetcher) Get() (*x509.Certificate, error) {
|
||||
certs, err := sf.GetChain()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return certs[0], nil
|
||||
}
|
||||
|
||||
type FileFetcher struct {
|
||||
path string
|
||||
}
|
||||
|
||||
func NewFileFetcher(path string) *FileFetcher {
|
||||
return &FileFetcher{
|
||||
path: path,
|
||||
}
|
||||
}
|
||||
|
||||
func (ff *FileFetcher) String() string {
|
||||
return ff.path
|
||||
}
|
||||
|
||||
func (ff *FileFetcher) GetChain() ([]*x509.Certificate, error) {
|
||||
if ff.path == "-" {
|
||||
certData, err := io.ReadAll(os.Stdin)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read from stdin: %w", err)
|
||||
}
|
||||
|
||||
return ParseCertificatesPEM(certData)
|
||||
}
|
||||
|
||||
certs, err := LoadCertificates(ff.path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load chain: %w", err)
|
||||
}
|
||||
|
||||
return certs, nil
|
||||
}
|
||||
|
||||
func (ff *FileFetcher) Get() (*x509.Certificate, error) {
|
||||
certs, err := ff.GetChain()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return certs[0], nil
|
||||
}
|
||||
|
||||
// GetCertificateChain fetches a certificate chain from a remote source.
|
||||
func GetCertificateChain(spec string, opts *FetcherOpts) ([]*x509.Certificate, error) {
|
||||
if fileutil.FileDoesExist(spec) {
|
||||
return NewFileFetcher(spec).GetChain()
|
||||
}
|
||||
|
||||
fetcher, err := ParseServer(spec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if opts != nil {
|
||||
fetcher.insecure = opts.SkipVerify
|
||||
fetcher.roots = opts.Roots
|
||||
}
|
||||
|
||||
return fetcher.GetChain()
|
||||
}
|
||||
|
||||
// GetCertificate fetches the first certificate from a certificate chain.
|
||||
func GetCertificate(spec string, opts *FetcherOpts) (*x509.Certificate, error) {
|
||||
certs, err := GetCertificateChain(spec, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return certs[0], nil
|
||||
}
|
||||
@@ -396,6 +396,45 @@ func ParseOneCertificateFromPEM(certsPEM []byte) ([]*x509.Certificate, []byte, e
|
||||
return certs, rest, nil
|
||||
}
|
||||
|
||||
// LoadFullCertPool returns a certificate pool with roots and intermediates
|
||||
// from disk. If no roots are provided, the system root pool will be used.
|
||||
func LoadFullCertPool(roots, intermediates string) (*x509.CertPool, error) {
|
||||
var err error
|
||||
|
||||
pool := x509.NewCertPool()
|
||||
|
||||
if roots == "" {
|
||||
pool, err = x509.SystemCertPool()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("loading system cert pool: %w", err)
|
||||
}
|
||||
} else {
|
||||
var rootCerts []*x509.Certificate
|
||||
rootCerts, err = LoadCertificates(roots)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("loading roots: %w", err)
|
||||
}
|
||||
|
||||
for _, cert := range rootCerts {
|
||||
pool.AddCert(cert)
|
||||
}
|
||||
}
|
||||
|
||||
if intermediates != "" {
|
||||
var intCerts []*x509.Certificate
|
||||
intCerts, err = LoadCertificates(intermediates)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("loading intermediates: %w", err)
|
||||
}
|
||||
|
||||
for _, cert := range intCerts {
|
||||
pool.AddCert(cert)
|
||||
}
|
||||
}
|
||||
|
||||
return pool, nil
|
||||
}
|
||||
|
||||
// LoadPEMCertPool loads a pool of PEM certificates from file.
|
||||
func LoadPEMCertPool(certsFile string) (*x509.CertPool, error) {
|
||||
if certsFile == "" {
|
||||
|
||||
@@ -26,7 +26,12 @@ func parseURL(host string) (string, int, error) {
|
||||
return "", 0, fmt.Errorf("certlib/hosts: invalid host: %s", host)
|
||||
}
|
||||
|
||||
if strings.ToLower(url.Scheme) != "https" {
|
||||
switch strings.ToLower(url.Scheme) {
|
||||
case "https":
|
||||
// OK
|
||||
case "tls":
|
||||
// OK
|
||||
default:
|
||||
return "", 0, errors.New("certlib/hosts: only https scheme supported")
|
||||
}
|
||||
|
||||
@@ -43,28 +48,28 @@ func parseURL(host string) (string, int, error) {
|
||||
}
|
||||
|
||||
func parseHostPort(host string) (string, int, error) {
|
||||
host, sport, err := net.SplitHostPort(host)
|
||||
shost, sport, err := net.SplitHostPort(host)
|
||||
if err == nil {
|
||||
portInt, err2 := strconv.ParseInt(sport, 10, 16)
|
||||
if err2 != nil {
|
||||
return "", 0, fmt.Errorf("certlib/hosts: invalid port: %s", sport)
|
||||
}
|
||||
|
||||
return host, int(portInt), nil
|
||||
return shost, int(portInt), nil
|
||||
}
|
||||
|
||||
return host, defaultHTTPSPort, nil
|
||||
}
|
||||
|
||||
func ParseHost(host string) (*Target, error) {
|
||||
host, port, err := parseURL(host)
|
||||
uhost, port, err := parseURL(host)
|
||||
if err == nil {
|
||||
return &Target{Host: host, Port: port}, nil
|
||||
return &Target{Host: uhost, Port: port}, nil
|
||||
}
|
||||
|
||||
host, port, err = parseHostPort(host)
|
||||
shost, port, err := parseHostPort(host)
|
||||
if err == nil {
|
||||
return &Target{Host: host, Port: port}, nil
|
||||
return &Target{Host: shost, Port: port}, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("certlib/hosts: invalid host: %s", host)
|
||||
|
||||
35
certlib/hosts/hosts_test.go
Normal file
35
certlib/hosts/hosts_test.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package hosts_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.wntrmute.dev/kyle/goutils/certlib/hosts"
|
||||
)
|
||||
|
||||
type testCase struct {
|
||||
Host string
|
||||
Target hosts.Target
|
||||
}
|
||||
|
||||
var testCases = []testCase{
|
||||
{Host: "server-name", Target: hosts.Target{Host: "server-name", Port: 443}},
|
||||
{Host: "server-name:8443", Target: hosts.Target{Host: "server-name", Port: 8443}},
|
||||
{Host: "tls://server-name", Target: hosts.Target{Host: "server-name", Port: 443}},
|
||||
{Host: "https://server-name", Target: hosts.Target{Host: "server-name", Port: 443}},
|
||||
{Host: "https://server-name:8443", Target: hosts.Target{Host: "server-name", Port: 8443}},
|
||||
{Host: "tls://server-name:8443", Target: hosts.Target{Host: "server-name", Port: 8443}},
|
||||
{Host: "https://server-name/something/else", Target: hosts.Target{Host: "server-name", Port: 443}},
|
||||
}
|
||||
|
||||
func TestParseHost(t *testing.T) {
|
||||
for i, tc := range testCases {
|
||||
target, err := hosts.ParseHost(tc.Host)
|
||||
if err != nil {
|
||||
t.Fatalf("test case %d: %s", i+1, err)
|
||||
}
|
||||
|
||||
if target.Host != tc.Target.Host {
|
||||
t.Fatalf("test case %d: got host '%s', want host '%s'", i+1, target.Host, tc.Target.Host)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,27 +2,151 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/dsa"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rsa"
|
||||
"crypto/sha256"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/kr/text"
|
||||
|
||||
"git.wntrmute.dev/kyle/goutils/certlib"
|
||||
"git.wntrmute.dev/kyle/goutils/lib"
|
||||
)
|
||||
|
||||
// following two lifted from CFSSL, (replace-regexp "\(.+\): \(.+\),"
|
||||
// "\2: \1,")
|
||||
|
||||
const (
|
||||
sSHA256 = "SHA256"
|
||||
sSHA512 = "SHA512"
|
||||
)
|
||||
|
||||
var keyUsage = map[x509.KeyUsage]string{
|
||||
x509.KeyUsageDigitalSignature: "digital signature",
|
||||
x509.KeyUsageContentCommitment: "content committment",
|
||||
x509.KeyUsageKeyEncipherment: "key encipherment",
|
||||
x509.KeyUsageKeyAgreement: "key agreement",
|
||||
x509.KeyUsageDataEncipherment: "data encipherment",
|
||||
x509.KeyUsageCertSign: "cert sign",
|
||||
x509.KeyUsageCRLSign: "crl sign",
|
||||
x509.KeyUsageEncipherOnly: "encipher only",
|
||||
x509.KeyUsageDecipherOnly: "decipher only",
|
||||
}
|
||||
|
||||
var extKeyUsages = map[x509.ExtKeyUsage]string{
|
||||
x509.ExtKeyUsageAny: "any",
|
||||
x509.ExtKeyUsageServerAuth: "server auth",
|
||||
x509.ExtKeyUsageClientAuth: "client auth",
|
||||
x509.ExtKeyUsageCodeSigning: "code signing",
|
||||
x509.ExtKeyUsageEmailProtection: "s/mime",
|
||||
x509.ExtKeyUsageIPSECEndSystem: "ipsec end system",
|
||||
x509.ExtKeyUsageIPSECTunnel: "ipsec tunnel",
|
||||
x509.ExtKeyUsageIPSECUser: "ipsec user",
|
||||
x509.ExtKeyUsageTimeStamping: "timestamping",
|
||||
x509.ExtKeyUsageOCSPSigning: "ocsp signing",
|
||||
x509.ExtKeyUsageMicrosoftServerGatedCrypto: "microsoft sgc",
|
||||
x509.ExtKeyUsageNetscapeServerGatedCrypto: "netscape sgc",
|
||||
x509.ExtKeyUsageMicrosoftCommercialCodeSigning: "microsoft commercial code signing",
|
||||
x509.ExtKeyUsageMicrosoftKernelCodeSigning: "microsoft kernel code signing",
|
||||
}
|
||||
|
||||
func sigAlgoPK(a x509.SignatureAlgorithm) string {
|
||||
switch a {
|
||||
case x509.MD2WithRSA, x509.MD5WithRSA, x509.SHA1WithRSA, x509.SHA256WithRSA, x509.SHA384WithRSA, x509.SHA512WithRSA:
|
||||
return "RSA"
|
||||
case x509.SHA256WithRSAPSS, x509.SHA384WithRSAPSS, x509.SHA512WithRSAPSS:
|
||||
return "RSA-PSS"
|
||||
case x509.ECDSAWithSHA1, x509.ECDSAWithSHA256, x509.ECDSAWithSHA384, x509.ECDSAWithSHA512:
|
||||
return "ECDSA"
|
||||
case x509.DSAWithSHA1, x509.DSAWithSHA256:
|
||||
return "DSA"
|
||||
case x509.PureEd25519:
|
||||
return "Ed25519"
|
||||
case x509.UnknownSignatureAlgorithm:
|
||||
return "unknown public key algorithm"
|
||||
default:
|
||||
return "unknown public key algorithm"
|
||||
}
|
||||
}
|
||||
|
||||
func sigAlgoHash(a x509.SignatureAlgorithm) string {
|
||||
switch a {
|
||||
case x509.MD2WithRSA:
|
||||
return "MD2"
|
||||
case x509.MD5WithRSA:
|
||||
return "MD5"
|
||||
case x509.SHA1WithRSA, x509.ECDSAWithSHA1, x509.DSAWithSHA1:
|
||||
return "SHA1"
|
||||
case x509.SHA256WithRSA, x509.ECDSAWithSHA256, x509.DSAWithSHA256:
|
||||
return sSHA256
|
||||
case x509.SHA256WithRSAPSS:
|
||||
return sSHA256
|
||||
case x509.SHA384WithRSA, x509.ECDSAWithSHA384:
|
||||
return "SHA384"
|
||||
case x509.SHA384WithRSAPSS:
|
||||
return "SHA384"
|
||||
case x509.SHA512WithRSA, x509.ECDSAWithSHA512:
|
||||
return sSHA512
|
||||
case x509.SHA512WithRSAPSS:
|
||||
return sSHA512
|
||||
case x509.PureEd25519:
|
||||
return sSHA512
|
||||
case x509.UnknownSignatureAlgorithm:
|
||||
return "unknown hash algorithm"
|
||||
default:
|
||||
return "unknown hash algorithm"
|
||||
}
|
||||
}
|
||||
|
||||
const maxLine = 78
|
||||
|
||||
func makeIndent(n int) string {
|
||||
s := " "
|
||||
var sSb97 strings.Builder
|
||||
for range n {
|
||||
sSb97.WriteString(" ")
|
||||
}
|
||||
s += sSb97.String()
|
||||
return s
|
||||
}
|
||||
|
||||
func indentLen(n int) int {
|
||||
return 4 + (8 * n)
|
||||
}
|
||||
|
||||
// this isn't real efficient, but that's not a problem here.
|
||||
func wrap(s string, indent int) string {
|
||||
if indent > 3 {
|
||||
indent = 3
|
||||
}
|
||||
|
||||
wrapped := text.Wrap(s, maxLine)
|
||||
lines := strings.SplitN(wrapped, "\n", 2)
|
||||
if len(lines) == 1 {
|
||||
return lines[0]
|
||||
}
|
||||
|
||||
if (maxLine - indentLen(indent)) <= 0 {
|
||||
panic("too much indentation")
|
||||
}
|
||||
|
||||
rest := strings.Join(lines[1:], " ")
|
||||
wrapped = text.Wrap(rest, maxLine-indentLen(indent))
|
||||
return lines[0] + "\n" + text.Indent(wrapped, makeIndent(indent))
|
||||
}
|
||||
|
||||
func dumpHex(in []byte) string {
|
||||
return lib.HexEncode(in, lib.HexEncodeUpperColon)
|
||||
}
|
||||
|
||||
func certPublic(cert *x509.Certificate) string {
|
||||
switch pub := cert.PublicKey.(type) {
|
||||
case *rsa.PublicKey:
|
||||
@@ -116,7 +240,7 @@ func showBasicConstraints(cert *x509.Certificate) {
|
||||
fmt.Fprint(os.Stdout, " (basic constraint failure)")
|
||||
}
|
||||
} else {
|
||||
fmt.Fprint(os.Stdout, "is not a CA certificate")
|
||||
fmt.Fprint(os.Stdout, ", is not a CA certificate")
|
||||
if cert.KeyUsage&x509.KeyUsageKeyEncipherment != 0 {
|
||||
fmt.Fprint(os.Stdout, " (key encipherment usage enabled!)")
|
||||
}
|
||||
@@ -220,122 +344,6 @@ func displayCert(cert *x509.Certificate) {
|
||||
}
|
||||
}
|
||||
|
||||
func displayAllCerts(in []byte, leafOnly bool) {
|
||||
certs, err := certlib.ParseCertificatesPEM(in)
|
||||
if err != nil {
|
||||
certs, _, err = certlib.ParseCertificatesDER(in, "")
|
||||
if err != nil {
|
||||
_, _ = lib.Warn(err, "failed to parse certificates")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if len(certs) == 0 {
|
||||
_, _ = lib.Warnx("no certificates found")
|
||||
return
|
||||
}
|
||||
|
||||
if leafOnly {
|
||||
displayCert(certs[0])
|
||||
return
|
||||
}
|
||||
|
||||
for i := range certs {
|
||||
displayCert(certs[i])
|
||||
}
|
||||
}
|
||||
|
||||
func displayAllCertsWeb(uri string, leafOnly bool) {
|
||||
ci := getConnInfo(uri)
|
||||
d := &tls.Dialer{Config: permissiveConfig()}
|
||||
nc, err := d.DialContext(context.Background(), "tcp", ci.Addr)
|
||||
if err != nil {
|
||||
_, _ = lib.Warn(err, "couldn't connect to %s", ci.Addr)
|
||||
return
|
||||
}
|
||||
|
||||
conn, ok := nc.(*tls.Conn)
|
||||
if !ok {
|
||||
_, _ = lib.Warnx("invalid TLS connection (not a *tls.Conn)")
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
state := conn.ConnectionState()
|
||||
if err = conn.Close(); err != nil {
|
||||
_, _ = lib.Warn(err, "couldn't close TLS connection")
|
||||
}
|
||||
|
||||
d = &tls.Dialer{Config: verifyConfig(ci.Host)}
|
||||
nc, err = d.DialContext(context.Background(), "tcp", ci.Addr)
|
||||
if err == nil {
|
||||
conn, ok = nc.(*tls.Conn)
|
||||
if !ok {
|
||||
_, _ = lib.Warnx("invalid TLS connection (not a *tls.Conn)")
|
||||
return
|
||||
}
|
||||
|
||||
err = conn.VerifyHostname(ci.Host)
|
||||
if err == nil {
|
||||
state = conn.ConnectionState()
|
||||
}
|
||||
conn.Close()
|
||||
} else {
|
||||
_, _ = lib.Warn(err, "TLS verification error with server name %s", ci.Host)
|
||||
}
|
||||
|
||||
if len(state.PeerCertificates) == 0 {
|
||||
_, _ = lib.Warnx("no certificates found")
|
||||
return
|
||||
}
|
||||
|
||||
if leafOnly {
|
||||
displayCert(state.PeerCertificates[0])
|
||||
return
|
||||
}
|
||||
|
||||
if len(state.VerifiedChains) == 0 {
|
||||
_, _ = lib.Warnx("no verified chains found; using peer chain")
|
||||
for i := range state.PeerCertificates {
|
||||
displayCert(state.PeerCertificates[i])
|
||||
}
|
||||
} else {
|
||||
fmt.Fprintln(os.Stdout, "TLS chain verified successfully.")
|
||||
for i := range state.VerifiedChains {
|
||||
fmt.Fprintf(os.Stdout, "--- Verified certificate chain %d ---%s", i+1, "\n")
|
||||
for j := range state.VerifiedChains[i] {
|
||||
displayCert(state.VerifiedChains[i][j])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func shouldReadStdin(argc int, argv []string) bool {
|
||||
if argc == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
if argc == 1 && argv[0] == "-" {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func readStdin(leafOnly bool) {
|
||||
certs, err := io.ReadAll(os.Stdin)
|
||||
if err != nil {
|
||||
_, _ = lib.Warn(err, "couldn't read certificates from standard input")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// This is needed for getting certs from JSON/jq.
|
||||
certs = bytes.TrimSpace(certs)
|
||||
certs = bytes.ReplaceAll(certs, []byte(`\n`), []byte{0xa})
|
||||
certs = bytes.Trim(certs, `"`)
|
||||
displayAllCerts(certs, leafOnly)
|
||||
}
|
||||
|
||||
func main() {
|
||||
var leafOnly bool
|
||||
flag.BoolVar(&showHash, "d", false, "show hashes of raw DER contents")
|
||||
@@ -343,23 +351,26 @@ func main() {
|
||||
flag.BoolVar(&leafOnly, "l", false, "only show the leaf certificate")
|
||||
flag.Parse()
|
||||
|
||||
if shouldReadStdin(flag.NArg(), flag.Args()) {
|
||||
readStdin(leafOnly)
|
||||
return
|
||||
opts := &certlib.FetcherOpts{
|
||||
SkipVerify: true,
|
||||
Roots: nil,
|
||||
}
|
||||
|
||||
for _, filename := range flag.Args() {
|
||||
fmt.Fprintf(os.Stdout, "--%s ---%s", filename, "\n")
|
||||
if strings.HasPrefix(filename, "https://") {
|
||||
displayAllCertsWeb(filename, leafOnly)
|
||||
} else {
|
||||
in, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
_, _ = lib.Warn(err, "couldn't read certificate")
|
||||
continue
|
||||
}
|
||||
certs, err := certlib.GetCertificateChain(filename, opts)
|
||||
if err != nil {
|
||||
_, _ = lib.Warn(err, "couldn't read certificate")
|
||||
continue
|
||||
}
|
||||
|
||||
displayAllCerts(in, leafOnly)
|
||||
if leafOnly {
|
||||
displayCert(certs[0])
|
||||
continue
|
||||
}
|
||||
|
||||
for i := range certs {
|
||||
displayCert(certs[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,189 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/kr/text"
|
||||
)
|
||||
|
||||
// following two lifted from CFSSL, (replace-regexp "\(.+\): \(.+\),"
|
||||
// "\2: \1,")
|
||||
|
||||
const (
|
||||
sSHA256 = "SHA256"
|
||||
sSHA512 = "SHA512"
|
||||
)
|
||||
|
||||
var keyUsage = map[x509.KeyUsage]string{
|
||||
x509.KeyUsageDigitalSignature: "digital signature",
|
||||
x509.KeyUsageContentCommitment: "content committment",
|
||||
x509.KeyUsageKeyEncipherment: "key encipherment",
|
||||
x509.KeyUsageKeyAgreement: "key agreement",
|
||||
x509.KeyUsageDataEncipherment: "data encipherment",
|
||||
x509.KeyUsageCertSign: "cert sign",
|
||||
x509.KeyUsageCRLSign: "crl sign",
|
||||
x509.KeyUsageEncipherOnly: "encipher only",
|
||||
x509.KeyUsageDecipherOnly: "decipher only",
|
||||
}
|
||||
|
||||
var extKeyUsages = map[x509.ExtKeyUsage]string{
|
||||
x509.ExtKeyUsageAny: "any",
|
||||
x509.ExtKeyUsageServerAuth: "server auth",
|
||||
x509.ExtKeyUsageClientAuth: "client auth",
|
||||
x509.ExtKeyUsageCodeSigning: "code signing",
|
||||
x509.ExtKeyUsageEmailProtection: "s/mime",
|
||||
x509.ExtKeyUsageIPSECEndSystem: "ipsec end system",
|
||||
x509.ExtKeyUsageIPSECTunnel: "ipsec tunnel",
|
||||
x509.ExtKeyUsageIPSECUser: "ipsec user",
|
||||
x509.ExtKeyUsageTimeStamping: "timestamping",
|
||||
x509.ExtKeyUsageOCSPSigning: "ocsp signing",
|
||||
x509.ExtKeyUsageMicrosoftServerGatedCrypto: "microsoft sgc",
|
||||
x509.ExtKeyUsageNetscapeServerGatedCrypto: "netscape sgc",
|
||||
x509.ExtKeyUsageMicrosoftCommercialCodeSigning: "microsoft commercial code signing",
|
||||
x509.ExtKeyUsageMicrosoftKernelCodeSigning: "microsoft kernel code signing",
|
||||
}
|
||||
|
||||
func sigAlgoPK(a x509.SignatureAlgorithm) string {
|
||||
switch a {
|
||||
case x509.MD2WithRSA, x509.MD5WithRSA, x509.SHA1WithRSA, x509.SHA256WithRSA, x509.SHA384WithRSA, x509.SHA512WithRSA:
|
||||
return "RSA"
|
||||
case x509.SHA256WithRSAPSS, x509.SHA384WithRSAPSS, x509.SHA512WithRSAPSS:
|
||||
return "RSA-PSS"
|
||||
case x509.ECDSAWithSHA1, x509.ECDSAWithSHA256, x509.ECDSAWithSHA384, x509.ECDSAWithSHA512:
|
||||
return "ECDSA"
|
||||
case x509.DSAWithSHA1, x509.DSAWithSHA256:
|
||||
return "DSA"
|
||||
case x509.PureEd25519:
|
||||
return "Ed25519"
|
||||
case x509.UnknownSignatureAlgorithm:
|
||||
return "unknown public key algorithm"
|
||||
default:
|
||||
return "unknown public key algorithm"
|
||||
}
|
||||
}
|
||||
|
||||
func sigAlgoHash(a x509.SignatureAlgorithm) string {
|
||||
switch a {
|
||||
case x509.MD2WithRSA:
|
||||
return "MD2"
|
||||
case x509.MD5WithRSA:
|
||||
return "MD5"
|
||||
case x509.SHA1WithRSA, x509.ECDSAWithSHA1, x509.DSAWithSHA1:
|
||||
return "SHA1"
|
||||
case x509.SHA256WithRSA, x509.ECDSAWithSHA256, x509.DSAWithSHA256:
|
||||
return sSHA256
|
||||
case x509.SHA256WithRSAPSS:
|
||||
return sSHA256
|
||||
case x509.SHA384WithRSA, x509.ECDSAWithSHA384:
|
||||
return "SHA384"
|
||||
case x509.SHA384WithRSAPSS:
|
||||
return "SHA384"
|
||||
case x509.SHA512WithRSA, x509.ECDSAWithSHA512:
|
||||
return sSHA512
|
||||
case x509.SHA512WithRSAPSS:
|
||||
return sSHA512
|
||||
case x509.PureEd25519:
|
||||
return sSHA512
|
||||
case x509.UnknownSignatureAlgorithm:
|
||||
return "unknown hash algorithm"
|
||||
default:
|
||||
return "unknown hash algorithm"
|
||||
}
|
||||
}
|
||||
|
||||
const maxLine = 78
|
||||
|
||||
func makeIndent(n int) string {
|
||||
s := " "
|
||||
var sSb97 strings.Builder
|
||||
for range n {
|
||||
sSb97.WriteString(" ")
|
||||
}
|
||||
s += sSb97.String()
|
||||
return s
|
||||
}
|
||||
|
||||
func indentLen(n int) int {
|
||||
return 4 + (8 * n)
|
||||
}
|
||||
|
||||
// this isn't real efficient, but that's not a problem here.
|
||||
func wrap(s string, indent int) string {
|
||||
if indent > 3 {
|
||||
indent = 3
|
||||
}
|
||||
|
||||
wrapped := text.Wrap(s, maxLine)
|
||||
lines := strings.SplitN(wrapped, "\n", 2)
|
||||
if len(lines) == 1 {
|
||||
return lines[0]
|
||||
}
|
||||
|
||||
if (maxLine - indentLen(indent)) <= 0 {
|
||||
panic("too much indentation")
|
||||
}
|
||||
|
||||
rest := strings.Join(lines[1:], " ")
|
||||
wrapped = text.Wrap(rest, maxLine-indentLen(indent))
|
||||
return lines[0] + "\n" + text.Indent(wrapped, makeIndent(indent))
|
||||
}
|
||||
|
||||
func dumpHex(in []byte) string {
|
||||
var s string
|
||||
var sSb130 strings.Builder
|
||||
for i := range in {
|
||||
sSb130.WriteString(fmt.Sprintf("%02X:", in[i]))
|
||||
}
|
||||
s += sSb130.String()
|
||||
|
||||
return strings.Trim(s, ":")
|
||||
}
|
||||
|
||||
// permissiveConfig returns a maximally-accepting TLS configuration;
|
||||
// the purpose is to look at the cert, not verify the security properties
|
||||
// of the connection.
|
||||
func permissiveConfig() *tls.Config {
|
||||
return &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
} // #nosec G402
|
||||
}
|
||||
|
||||
// verifyConfig returns a config that will verify the connection.
|
||||
func verifyConfig(hostname string) *tls.Config {
|
||||
return &tls.Config{
|
||||
ServerName: hostname,
|
||||
} // #nosec G402
|
||||
}
|
||||
|
||||
type connInfo struct {
|
||||
// The original URI provided.
|
||||
URI string
|
||||
|
||||
// The hostname of the server.
|
||||
Host string
|
||||
|
||||
// The port to connect on.
|
||||
Port string
|
||||
|
||||
// The address to connect to.
|
||||
Addr string
|
||||
}
|
||||
|
||||
func getConnInfo(uri string) *connInfo {
|
||||
ci := &connInfo{URI: uri}
|
||||
ci.Host = uri[len("https://"):]
|
||||
|
||||
host, port, err := net.SplitHostPort(ci.Host)
|
||||
if err != nil {
|
||||
ci.Port = "443"
|
||||
} else {
|
||||
ci.Host = host
|
||||
ci.Port = port
|
||||
}
|
||||
ci.Addr = net.JoinHostPort(ci.Host, ci.Port)
|
||||
return ci
|
||||
}
|
||||
@@ -75,18 +75,15 @@ func checkCert(cert *x509.Certificate) {
|
||||
}
|
||||
|
||||
func main() {
|
||||
opts := &certlib.FetcherOpts{}
|
||||
|
||||
flag.BoolVar(&opts.SkipVerify, "k", false, "skip server verification")
|
||||
flag.BoolVar(&warnOnly, "q", false, "only warn about expiring certs")
|
||||
flag.DurationVar(&leeway, "t", leeway, "warn if certificates are closer than this to expiring")
|
||||
flag.Parse()
|
||||
|
||||
for _, file := range flag.Args() {
|
||||
in, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
_, _ = lib.Warn(err, "failed to read file")
|
||||
continue
|
||||
}
|
||||
|
||||
certs, err := certlib.ParseCertificatesPEM(in)
|
||||
certs, err := certlib.GetCertificateChain(file, opts)
|
||||
if err != nil {
|
||||
_, _ = lib.Warn(err, "while parsing certificates")
|
||||
continue
|
||||
|
||||
@@ -32,14 +32,16 @@ func serialString(cert *x509.Certificate, mode lib.HexEncodeMode) string {
|
||||
}
|
||||
|
||||
func main() {
|
||||
opts := &certlib.FetcherOpts{}
|
||||
displayAs := flag.String("d", "int", "display mode (int, hex, uhex)")
|
||||
showExpiry := flag.Bool("e", false, "show expiry date")
|
||||
flag.BoolVar(&opts.SkipVerify, "k", false, "skip server verification")
|
||||
flag.Parse()
|
||||
|
||||
displayMode := parseDisplayMode(*displayAs)
|
||||
|
||||
for _, arg := range flag.Args() {
|
||||
cert, err := certlib.LoadCertificate(arg)
|
||||
cert, err := certlib.GetCertificate(arg, opts)
|
||||
die.If(err)
|
||||
|
||||
fmt.Printf("%s: %s", arg, serialString(cert, displayMode))
|
||||
|
||||
@@ -29,9 +29,9 @@ func printRevocation(cert *x509.Certificate) {
|
||||
}
|
||||
|
||||
type appConfig struct {
|
||||
caFile, intFile string
|
||||
forceIntermediateBundle bool
|
||||
revexp, verbose bool
|
||||
caFile, intFile string
|
||||
forceIntermediateBundle bool
|
||||
revexp, skipVerify, verbose bool
|
||||
}
|
||||
|
||||
func parseFlags() appConfig {
|
||||
@@ -40,6 +40,7 @@ func parseFlags() appConfig {
|
||||
flag.StringVar(&cfg.intFile, "i", "", "intermediate `bundle`")
|
||||
flag.BoolVar(&cfg.forceIntermediateBundle, "f", false,
|
||||
"force the use of the intermediate bundle, ignoring any intermediates bundled with certificate")
|
||||
flag.BoolVar(&cfg.skipVerify, "k", false, "skip CA verification")
|
||||
flag.BoolVar(&cfg.revexp, "r", false, "print revocation and expiry information")
|
||||
flag.BoolVar(&cfg.verbose, "v", false, "verbose")
|
||||
flag.Parse()
|
||||
@@ -102,12 +103,17 @@ func run(cfg appConfig) error {
|
||||
fmt.Fprintf(os.Stderr, "Usage: %s [-ca bundle] [-i bundle] cert", lib.ProgName())
|
||||
}
|
||||
|
||||
fileData, err := os.ReadFile(flag.Arg(0))
|
||||
combinedPool, err := certlib.LoadFullCertPool(cfg.caFile, cfg.intFile)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("failed to build combined pool: %w", err)
|
||||
}
|
||||
|
||||
chain, err := certlib.ParseCertificatesPEM(fileData)
|
||||
opts := &certlib.FetcherOpts{
|
||||
Roots: combinedPool,
|
||||
SkipVerify: cfg.skipVerify,
|
||||
}
|
||||
|
||||
chain, err := certlib.GetCertificateChain(flag.Arg(0), opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"git.wntrmute.dev/kyle/goutils/certlib"
|
||||
"git.wntrmute.dev/kyle/goutils/die"
|
||||
@@ -32,10 +31,10 @@ Usage:
|
||||
ski [-hm] files...
|
||||
|
||||
Flags:
|
||||
-d Hex encoding mode.
|
||||
-h Print this help message.
|
||||
-m All SKIs should match; as soon as an SKI mismatch is found,
|
||||
it is reported.
|
||||
|
||||
`)
|
||||
}
|
||||
|
||||
@@ -145,15 +144,8 @@ func parseCSR(data []byte) ([]byte, string) {
|
||||
return public, kt
|
||||
}
|
||||
|
||||
func dumpHex(in []byte) string {
|
||||
var s string
|
||||
var sSb153 strings.Builder
|
||||
for i := range in {
|
||||
sSb153.WriteString(fmt.Sprintf("%02X:", in[i]))
|
||||
}
|
||||
s += sSb153.String()
|
||||
|
||||
return strings.Trim(s, ":")
|
||||
func dumpHex(in []byte, mode lib.HexEncodeMode) string {
|
||||
return lib.HexEncode(in, mode)
|
||||
}
|
||||
|
||||
type subjectPublicKeyInfo struct {
|
||||
@@ -163,10 +155,14 @@ type subjectPublicKeyInfo struct {
|
||||
|
||||
func main() {
|
||||
var help, shouldMatch bool
|
||||
var displayModeString string
|
||||
flag.StringVar(&displayModeString, "d", "lower", "hex encoding mode")
|
||||
flag.BoolVar(&help, "h", false, "print a help message and exit")
|
||||
flag.BoolVar(&shouldMatch, "m", false, "all SKIs should match")
|
||||
flag.Parse()
|
||||
|
||||
displayMode := lib.ParseHexEncodeMode(displayModeString)
|
||||
|
||||
if help {
|
||||
usage(os.Stdout)
|
||||
os.Exit(0)
|
||||
@@ -184,7 +180,7 @@ func main() {
|
||||
}
|
||||
|
||||
pubHash := sha1.Sum(subPKI.SubjectPublicKey.Bytes) // #nosec G401 this is the standard
|
||||
pubHashString := dumpHex(pubHash[:])
|
||||
pubHashString := dumpHex(pubHash[:], displayMode)
|
||||
if ski == "" {
|
||||
ski = pubHashString
|
||||
}
|
||||
|
||||
2
go.mod
2
go.mod
@@ -15,6 +15,7 @@ require (
|
||||
github.com/benbjohnson/clock v1.3.5
|
||||
github.com/davecgh/go-spew v1.1.1
|
||||
github.com/google/certificate-transparency-go v1.0.21
|
||||
rsc.io/qr v0.2.0
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -22,5 +23,4 @@ require (
|
||||
github.com/kr/pretty v0.1.0 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
|
||||
rsc.io/qr v0.2.0 // indirect
|
||||
)
|
||||
|
||||
19
lib/lib.go
19
lib/lib.go
@@ -125,6 +125,8 @@ const (
|
||||
// HexEncodeUpperColon prints the bytes as uppercase hexadecimal
|
||||
// with colons between each pair of bytes.
|
||||
HexEncodeUpperColon
|
||||
// HexEncodeBytes prints the string as a sequence of []byte.
|
||||
HexEncodeBytes
|
||||
)
|
||||
|
||||
func (m HexEncodeMode) String() string {
|
||||
@@ -137,6 +139,8 @@ func (m HexEncodeMode) String() string {
|
||||
return "lcolon"
|
||||
case HexEncodeUpperColon:
|
||||
return "ucolon"
|
||||
case HexEncodeBytes:
|
||||
return "bytes"
|
||||
default:
|
||||
panic("invalid hex encode mode")
|
||||
}
|
||||
@@ -152,6 +156,8 @@ func ParseHexEncodeMode(s string) HexEncodeMode {
|
||||
return HexEncodeLowerColon
|
||||
case "ucolon":
|
||||
return HexEncodeUpperColon
|
||||
case "bytes":
|
||||
return HexEncodeBytes
|
||||
}
|
||||
|
||||
panic("invalid hex encode mode")
|
||||
@@ -202,6 +208,17 @@ func hexEncode(b []byte) string {
|
||||
return s
|
||||
}
|
||||
|
||||
func bytesAsByteSliceString(buf []byte) string {
|
||||
sb := &strings.Builder{}
|
||||
sb.WriteString("[]byte{")
|
||||
for i := range buf {
|
||||
fmt.Fprintf(sb, "0x%02x, ", buf[i])
|
||||
}
|
||||
sb.WriteString("}")
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// HexEncode encodes the given bytes as a hexadecimal string.
|
||||
func HexEncode(b []byte, mode HexEncodeMode) string {
|
||||
str := hexEncode(b)
|
||||
@@ -215,6 +232,8 @@ func HexEncode(b []byte, mode HexEncodeMode) string {
|
||||
return hexColons(str)
|
||||
case HexEncodeUpperColon:
|
||||
return strings.ToUpper(hexColons(str))
|
||||
case HexEncodeBytes:
|
||||
return bytesAsByteSliceString(b)
|
||||
default:
|
||||
panic("invalid hex encode mode")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user