pax_global_header00006660000000000000000000000064141560243070014514gustar00rootroot0000000000000052 comment=c3327de968695ca12c0f9f5f195b246bcdf7e4dd certinfo-1.0.6+ds1/000077500000000000000000000000001415602430700140145ustar00rootroot00000000000000certinfo-1.0.6+ds1/.github/000077500000000000000000000000001415602430700153545ustar00rootroot00000000000000certinfo-1.0.6+ds1/.github/dependabot.yml000066400000000000000000000001771415602430700202110ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: gomod directory: "/" schedule: interval: daily open-pull-requests-limit: 10 certinfo-1.0.6+ds1/.github/workflows/000077500000000000000000000000001415602430700174115ustar00rootroot00000000000000certinfo-1.0.6+ds1/.github/workflows/release.yml000066400000000000000000000012371415602430700215570ustar00rootroot00000000000000name: goreleaser on: push: tags: - '*' permissions: contents: write jobs: goreleaser: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v2 with: fetch-depth: 0 - name: Set up Go uses: actions/setup-go@v2 with: go-version: 1.17 - name: Run tests run: make test - name: Run GoReleaser uses: goreleaser/goreleaser-action@v2 with: distribution: goreleaser version: latest args: release --rm-dist env: GITHUB_TOKEN: ${{ secrets.PUBLIC_REPO_TOKEN }} certinfo-1.0.6+ds1/.gitignore000066400000000000000000000000351415602430700160020ustar00rootroot00000000000000/certinfo /releases/* dist/ certinfo-1.0.6+ds1/.goreleaser.yml000066400000000000000000000014171415602430700167500ustar00rootroot00000000000000builds: - goos: - linux - darwin - windows goarch: - amd64 - arm64 ldflags: - -X main.Version={{.Version}} checksum: name_template: 'checksums.txt' dist: releases archives: - replacements: format_overrides: - goos: windows format: zip changelog: sort: asc filters: exclude: - '^docs:' - '^test:' release: github: owner: pete911 name: certinfo brews: - tap: owner: pete911 name: homebrew-tap token: "{{ .Env.GITHUB_TOKEN }}" name: certinfo homepage: "https://github.com/pete911/certinfo" description: "Print x509 certificate info." folder: Formula install: | bin.install "certinfo" test: | assert_match /Usage/, shell_output("#{bin}/certinfo -h", 0) certinfo-1.0.6+ds1/LICENSE000066400000000000000000000020601415602430700150170ustar00rootroot00000000000000MIT License Copyright (c) 2021 Peter Reisinger Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. certinfo-1.0.6+ds1/Makefile000066400000000000000000000004651415602430700154610ustar00rootroot00000000000000VERSION ?= dev .DEFAULT_GOAL := build test: go fmt ./... go vet ./... go clean -testcache && go test -cover ./... .PHONY:test build: test go build -ldflags "-X main.Version=${VERSION}" -mod vendor .PHONY:build install: test go install -ldflags "-X main.Version=${VERSION}" -mod vendor .PHONY:install certinfo-1.0.6+ds1/README.md000066400000000000000000000103351415602430700152750ustar00rootroot00000000000000# print x509 certificate info Similar to `openssl x509 -in -text` command, but handles chains, multiple files and TCP addresses. TLS/SSL version prints as well when using TCP address argument. ## usage ```shell script certinfo [flags] [| ...] ``` **file** argument can be: - **local file path** `certinfo ` - **TCP network address** `certinfo ` e.g. `certinfo google.com:443` - **stdin** `echo "" | certinfo` ``` +---------------------------------------------------------------------------------------------------------------+ | optional flags | +-----------+---------------------------------------------------------------------------------------------------+ | -chains | whether to print verified chains as well (only applicable for host) | | -expiry | print expiry of certificates | | -insecure | whether a client verifies the server's certificate chain and host name (only applicable for host) | | -pem | whether to print pem as well | | -pem-only | whether to print only pem (useful for downloading certs from host) | | -version | certinfo version | | -help | help | +-----------+---------------------------------------------------------------------------------------------------+ ``` Flags can be set as env. variable as well (`CERTINFO_=true` e.g. `CERTINFO_INSECURE=true`) and can be then overridden with a flag. ## download - [binary](https://github.com/pete911/certinfo/releases) ## build/install ### brew - add tap `brew tap pete911/tap` - install `brew install certinfo` ### go [go](https://golang.org/dl/) has to be installed. - build `make build` - install `make install` ## release Releases are published when the new tag is created e.g. `git tag -m "add super cool feature" v1.0.0 && git push --follow-tags` ## examples ### info/verbose `certinfo vault.com:443` ``` --- [vault.com:443 TLS 1.2] --- Version: 3 Serial Number: 15424177460318123999 Signature Algorithm: SHA256-RSA Type: end-entity Issuer: CN=Go Daddy Secure Certificate Authority - G2,OU=http://certs.godaddy.com/repository/,O=GoDaddy.com\, Inc.,L=Scottsdale,ST=Arizona,C=US Validity Not Before: Apr 8 05:28:12 2020 UTC Not After : Apr 17 02:03:38 2022 UTC Subject: CN=*.vault.com,OU=Domain Control Validated DNS Names: *.vault.com, vault.com IP Addresses: Key Usage: Digital Signature, Key Encipherment Ext Key Usage: Server Auth, Client Auth CA: false Version: 3 Serial Number: 7 Signature Algorithm: SHA256-RSA Type: intermediate Issuer: CN=Go Daddy Root Certificate Authority - G2,O=GoDaddy.com\, Inc.,L=Scottsdale,ST=Arizona,C=US Validity Not Before: May 3 07:00:00 2011 UTC Not After : May 3 07:00:00 2031 UTC Subject: CN=Go Daddy Secure Certificate Authority - G2,OU=http://certs.godaddy.com/repository/,O=GoDaddy.com\, Inc.,L=Scottsdale,ST=Arizona,C=US DNS Names: IP Addresses: Key Usage: Cert Sign, CRL Sign Ext Key Usage: CA: true Version: 3 Serial Number: 1828629 Signature Algorithm: SHA256-RSA Type: intermediate Issuer: OU=Go Daddy Class 2 Certification Authority,O=The Go Daddy Group\, Inc.,C=US Validity Not Before: Jan 1 07:00:00 2014 UTC Not After : May 30 07:00:00 2031 UTC Subject: CN=Go Daddy Root Certificate Authority - G2,O=GoDaddy.com\, Inc.,L=Scottsdale,ST=Arizona,C=US DNS Names: IP Addresses: Key Usage: Cert Sign, CRL Sign Ext Key Usage: CA: true ``` ### info/expiry `certinfo -expiry google.com:443` ``` --- [google.com:443 TLS 1.3] --- Subject: CN=*.google.com,O=Google LLC,L=Mountain View,ST=California,C=US Expiry: 2 months 5 days 6 hours 56 minutes Subject: CN=GTS CA 1O1,O=Google Trust Services,C=US Expiry: 1 years 1 months 7 days 12 hours 54 minutes ``` ### local root certs - linux `ls -d /etc/ssl/certs/* | grep '.pem' | xargs certinfo -expiry` - mac `cat /etc/ssl/cert.pem | certinfo -expiry` certinfo-1.0.6+ds1/flag.go000066400000000000000000000032131415602430700152530ustar00rootroot00000000000000package main import ( "flag" "fmt" "os" "strconv" ) type Flags struct { Usage func() Expiry bool Insecure bool Chains bool Pem bool PemOnly bool Version bool Args []string } func ParseFlags() (Flags, error) { var flags Flags flagSet := flag.NewFlagSet(os.Args[0], flag.ContinueOnError) flagSet.BoolVar(&flags.Expiry, "expiry", getBoolEnv("CERTINFO_EXPIRY", false), "print expiry of certificates") flagSet.BoolVar(&flags.Insecure, "insecure", getBoolEnv("CERTINFO_INSECURE", false), "whether a client verifies the server's certificate chain and host name (only applicable for host)") flagSet.BoolVar(&flags.Chains, "chains", getBoolEnv("CERTINFO_CHAINS", false), "whether to print verified chains as well (only applicable for host)") flagSet.BoolVar(&flags.Pem, "pem", getBoolEnv("CERTINFO_PEM", false), "whether to print pem as well") flagSet.BoolVar(&flags.PemOnly, "pem-only", getBoolEnv("CERTINFO_PEM_ONLY", false), "whether to print only pem (useful for downloading certs from host)") flagSet.BoolVar(&flags.Version, "version", getBoolEnv("CERTINFO_VERSION", false), "certinfo version") flagSet.Usage = func() { fmt.Fprint(flagSet.Output(), "Usage: certinfo [flags] [| ...]\n") flagSet.PrintDefaults() } flags.Usage = flagSet.Usage if err := flagSet.Parse(os.Args[1:]); err != nil { return Flags{}, err } flags.Args = flagSet.Args() return flags, nil } func getBoolEnv(envName string, defaultValue bool) bool { env, ok := os.LookupEnv(envName) if !ok { return defaultValue } if intValue, err := strconv.ParseBool(env); err == nil { return intValue } return defaultValue } certinfo-1.0.6+ds1/go.mod000066400000000000000000000001541415602430700151220ustar00rootroot00000000000000module github.com/pete911/certinfo go 1.17 require github.com/icza/gox v0.0.0-20200525134802-370c390b446f certinfo-1.0.6+ds1/go.sum000066400000000000000000000003251415602430700151470ustar00rootroot00000000000000github.com/icza/gox v0.0.0-20200525134802-370c390b446f h1:SeGpofetb5f2dpH+mc8XshCNd2Bpbh5qL4vtLW4P99w= github.com/icza/gox v0.0.0-20200525134802-370c390b446f/go.mod h1:VbcN86fRkkUMPX2ufM85Um8zFndLZswoIW1eYtpAcVk= certinfo-1.0.6+ds1/main.go000066400000000000000000000040101415602430700152620ustar00rootroot00000000000000package main import ( "fmt" "os" "strconv" "strings" "github.com/pete911/certinfo/pkg/cert" ) var Version = "dev" func main() { flags, err := ParseFlags() if err != nil { fmt.Println(err.Error()) os.Exit(1) } if flags.Version { fmt.Println(Version) os.Exit(0) } certificatesFiles := LoadCertificatesLocations(flags) if flags.Expiry { PrintCertificatesExpiry(certificatesFiles) return } if flags.PemOnly { PrintPemOnly(certificatesFiles, flags.Chains) return } PrintCertificatesLocations(certificatesFiles, flags.Chains, flags.Pem) } func LoadCertificatesLocations(flags Flags) []cert.CertificateLocation { if len(flags.Args) > 0 { var certificateLocations []cert.CertificateLocation for _, arg := range flags.Args { var certificateLocation cert.CertificateLocation var err error if isTCPNetworkAddress(arg) { certificateLocation, err = cert.LoadCertificatesFromNetwork(arg, flags.Insecure) } else { certificateLocation, err = cert.LoadCertificatesFromFile(arg) } if err != nil { printCertFileError(arg, err) continue } certificateLocations = append(certificateLocations, certificateLocation) } return certificateLocations } if isStdin() { certificateLocation, err := cert.LoadCertificateFromStdin() if err != nil { printCertFileError("stdin", err) return nil } return []cert.CertificateLocation{certificateLocation} } // no stdin and not args flags.Usage() os.Exit(0) return nil } func printCertFileError(fileName string, err error) { fmt.Printf("--- [%s] ---\n", nameFormat(fileName, 0)) fmt.Println(err) fmt.Println() } func isTCPNetworkAddress(arg string) bool { parts := strings.Split(arg, ":") if len(parts) != 2 { return false } if _, err := strconv.Atoi(parts[1]); err != nil { return false } return true } func isStdin() bool { info, err := os.Stdin.Stat() if err != nil { fmt.Printf("checking stdin: %v\n", err) return false } if (info.Mode() & os.ModeCharDevice) == 0 { return true } return false } certinfo-1.0.6+ds1/pkg/000077500000000000000000000000001415602430700145755ustar00rootroot00000000000000certinfo-1.0.6+ds1/pkg/cert/000077500000000000000000000000001415602430700155325ustar00rootroot00000000000000certinfo-1.0.6+ds1/pkg/cert/cert.go000066400000000000000000000034171415602430700170230ustar00rootroot00000000000000package cert import ( "crypto/x509" "encoding/pem" "fmt" "strings" "time" ) type Certificate struct { // position of certificate in the chain, starts with 0 Index int X509Certificate *x509.Certificate } func (c Certificate) IsExpired() bool { return time.Now().After(c.X509Certificate.NotAfter) } func (c Certificate) IsExpiredAt(t time.Time) bool { return t.After(c.X509Certificate.NotAfter) } func (c Certificate) ToPEM() []byte { return pem.EncodeToMemory(&pem.Block{ Type: certificateBlockType, Bytes: c.X509Certificate.Raw, }) } func (c Certificate) String() string { dnsNames := strings.Join(c.X509Certificate.DNSNames, ", ") var ips []string for _, ip := range c.X509Certificate.IPAddresses { ips = append(ips, fmt.Sprintf("%s", ip)) } ipAddresses := strings.Join(ips, ", ") keyUsage := KeyUsageToString(c.X509Certificate.KeyUsage) extKeyUsage := ExtKeyUsageToString(c.X509Certificate.ExtKeyUsage) return strings.Join([]string{ fmt.Sprintf("Version: %d", c.X509Certificate.Version), fmt.Sprintf("Serial Number: %d", c.X509Certificate.SerialNumber), fmt.Sprintf("Signature Algorithm: %s", c.X509Certificate.SignatureAlgorithm), fmt.Sprintf("Type: %s", CertificateType(c.X509Certificate)), fmt.Sprintf("Issuer: %s", c.X509Certificate.Issuer), fmt.Sprintf("Validity\n Not Before: %s\n Not After : %s", ValidityFormat(c.X509Certificate.NotBefore), ValidityFormat(c.X509Certificate.NotAfter)), fmt.Sprintf("Subject: %s", c.X509Certificate.Subject), fmt.Sprintf("DNS Names: %s", dnsNames), fmt.Sprintf("IP Addresses: %s", ipAddresses), fmt.Sprintf("Key Usage: %s", strings.Join(keyUsage, ", ")), fmt.Sprintf("Ext Key Usage: %s", strings.Join(extKeyUsage, ", ")), fmt.Sprintf("CA: %t", c.X509Certificate.IsCA), }, "\n") } certinfo-1.0.6+ds1/pkg/cert/certs.go000066400000000000000000000022621415602430700172030ustar00rootroot00000000000000package cert import ( "crypto/x509" "encoding/pem" "errors" ) const certificateBlockType = "CERTIFICATE" type Certificates []Certificate // FromBytes converts raw certificate bytes to certificate, if the supplied data is cert bundle (or chain) // all the certificates will be returned func FromBytes(data []byte) (Certificates, error) { cs, err := DecodeCertificatesPEM(data) if err != nil { return nil, err } return FromX509Certificates(cs), nil } func FromX509Certificates(cs []*x509.Certificate) Certificates { var certificates Certificates for i, c := range cs { certificate := Certificate{ Index: i, X509Certificate: c, } certificates = append(certificates, certificate) } return certificates } func DecodeCertificatesPEM(data []byte) ([]*x509.Certificate, error) { var block *pem.Block var decodedCerts []byte for { block, data = pem.Decode(data) if block == nil { return nil, errors.New("failed to parse certificate PEM") } // append only certificates if block.Type == certificateBlockType { decodedCerts = append(decodedCerts, block.Bytes...) } if len(data) == 0 { break } } return x509.ParseCertificates(decodedCerts) } certinfo-1.0.6+ds1/pkg/cert/location.go000066400000000000000000000036461415602430700177020ustar00rootroot00000000000000package cert import ( "crypto/tls" "fmt" "io/ioutil" "net" "os" "time" ) const tlsDialTimeout = 5 * time.Second type CertificateLocation struct { TLSVersion uint16 // only applicable for network certificates Path string Certificates Certificates VerifiedChains []Certificates // only applicable for network certificates } func LoadCertificatesFromNetwork(addr string, tlsSkipVerify bool) (CertificateLocation, error) { conn, err := tls.DialWithDialer(&net.Dialer{Timeout: tlsDialTimeout}, "tcp", addr, &tls.Config{InsecureSkipVerify: tlsSkipVerify}) if err != nil { return CertificateLocation{}, fmt.Errorf("tcp connection failed: %w", err) } connectionState := conn.ConnectionState() x509Certificates := connectionState.PeerCertificates var verifiedChains []Certificates for _, chain := range connectionState.VerifiedChains { verifiedChains = append(verifiedChains, FromX509Certificates(chain)) } return CertificateLocation{ TLSVersion: conn.ConnectionState().Version, Path: addr, Certificates: FromX509Certificates(x509Certificates), VerifiedChains: verifiedChains, }, nil } func LoadCertificatesFromFile(fileName string) (CertificateLocation, error) { b, err := ioutil.ReadFile(fileName) if err != nil { return CertificateLocation{}, fmt.Errorf("skipping %s file: %w", fileName, err) } return loadCertificate(fileName, b) } func LoadCertificateFromStdin() (CertificateLocation, error) { content, err := ioutil.ReadAll(os.Stdin) if err != nil { return CertificateLocation{}, fmt.Errorf("reading stdin: %w", err) } return loadCertificate("stdin", content) } func loadCertificate(fileName string, data []byte) (CertificateLocation, error) { certificates, err := FromBytes(data) if err != nil { return CertificateLocation{}, fmt.Errorf("file %s: %w", fileName, err) } return CertificateLocation{ Path: fileName, Certificates: certificates, }, nil } certinfo-1.0.6+ds1/pkg/cert/util.go000066400000000000000000000034031415602430700170360ustar00rootroot00000000000000package cert import ( "bytes" "crypto/x509" "time" ) var ( // format for NotBefore and NotAfter fields to make output similar to openssl validityFormat = "Jan _2 15:04:05 2006 MST" // order is important! keyUsages = []string{ "Digital Signature", "Content Commitment", "Key Encipherment", "Data Encipherment", "Key Agreement", "Cert Sign", "CRL Sign", "Encipher Only", "Decipher Only", } // order is important! extKeyUsages = []string{ "Any", "Server Auth", "Client Auth", "Code Signing", "Email Protection", "IPSEC End System", "IPSEC Tunnel", "IPSEC User", "Time Stamping", "OCSP Signing", "Microsoft Server Gated Crypto", "Netscape Server Gated Crypto", "Microsoft Commercial Code Signing", "Microsoft Kernel Code Signing", } ) func ValidityFormat(t time.Time) string { return t.Format(validityFormat) } func CertificateType(cert *x509.Certificate) string { if IsRoot(cert) { return "root" } if cert.IsCA { return "intermediate" } return "end-entity" } func IsRoot(cert *x509.Certificate) bool { return bytes.Equal(cert.RawIssuer, cert.RawSubject) && cert.IsCA } // ExtKeyUsageToString converts extended key usage integer values to strings func ExtKeyUsageToString(extKeyUsage []x509.ExtKeyUsage) []string { var extendedKeyUsageString []string for _, v := range extKeyUsage { extendedKeyUsageString = append(extendedKeyUsageString, extKeyUsages[v]) } return extendedKeyUsageString } // KeyUsageToString converts key usage bit values to strings func KeyUsageToString(keyUsage x509.KeyUsage) []string { var keyUsageString []string for i, v := range keyUsages { bitmask := 1 << i if (int(keyUsage) & bitmask) == 0 { continue } keyUsageString = append(keyUsageString, v) } return keyUsageString } certinfo-1.0.6+ds1/print.go000066400000000000000000000061221415602430700155000ustar00rootroot00000000000000package main import ( "crypto/tls" "fmt" "github.com/icza/gox/timex" "github.com/pete911/certinfo/pkg/cert" "time" ) func PrintCertificatesLocations(certificateLocations []cert.CertificateLocation, printChains, printPem bool) { for _, certificateLocation := range certificateLocations { fmt.Printf("--- [%s] ---\n", nameFormat(certificateLocation.Path, certificateLocation.TLSVersion)) printCertificates(certificateLocation.Certificates, printPem) if certificateLocation.VerifiedChains != nil { fmt.Printf("--- %d verified chains ---\n", len(certificateLocation.VerifiedChains)) } if printChains { for i, chain := range certificateLocation.VerifiedChains { fmt.Printf("--- chain %d ---\n", i+1) printCertificates(chain, printPem) } } } } func printCertificates(certificates cert.Certificates, printPem bool) { for _, certificate := range certificates { fmt.Println(certificate) fmt.Println() if printPem { fmt.Println(string(certificate.ToPEM())) } } } func PrintPemOnly(certificateLocations []cert.CertificateLocation, printChains bool) { for _, certificateLocation := range certificateLocations { for _, certificate := range certificateLocation.Certificates { fmt.Print(string(certificate.ToPEM())) } if printChains { for _, chains := range certificateLocation.VerifiedChains { fmt.Println() for _, chain := range chains { fmt.Print(string(chain.ToPEM())) } } } } } func PrintCertificatesExpiry(certificateLocations []cert.CertificateLocation) { for _, certificateLocation := range certificateLocations { fmt.Printf("--- [%s] ---\n", nameFormat(certificateLocation.Path, certificateLocation.TLSVersion)) for _, certificate := range certificateLocation.Certificates { expiry := expiryFormat(certificate.X509Certificate.NotAfter) if certificate.IsExpired() { expiry = fmt.Sprintf("EXPIRED %s ago", expiry) } fmt.Printf("Subject: %s\n", certificate.X509Certificate.Subject) fmt.Printf("Expiry: %s\n", expiry) fmt.Println() } } } func nameFormat(name string, tlsVersion uint16) string { if tlsVersion == 0 { return name } return fmt.Sprintf("%s %s", name, tlsFormat(tlsVersion)) } func tlsFormat(tlsVersion uint16) string { switch tlsVersion { case 0: return "" case tls.VersionSSL30: return "SSLv3 - Deprecated!" case tls.VersionTLS10: return "TLS 1.0 - Deprecated!" case tls.VersionTLS11: return "TLS 1.1 - Deprecated!" case tls.VersionTLS12: return "TLS 1.2" case tls.VersionTLS13: return "TLS 1.3" default: return "TLS Version %d (unknown)" } } func expiryFormat(t time.Time) string { year, month, day, hour, minute, _ := timex.Diff(time.Now(), t) if year != 0 { return fmt.Sprintf("%d years %d months %d days %d hours %d minutes", year, month, day, hour, minute) } if month != 0 { return fmt.Sprintf("%d months %d days %d hours %d minutes", month, day, hour, minute) } if day != 0 { return fmt.Sprintf("%d days %d hours %d minutes", day, hour, minute) } if hour != 0 { return fmt.Sprintf("%d hours %d minutes", hour, minute) } return fmt.Sprintf("%d minutes", minute) }