From b225f00f4883bb3e128afecf6945511d431342c8 Mon Sep 17 00:00:00 2001
From: ytkhs <506771+ytkhs@users.noreply.github.com>
Date: Tue, 7 Jul 2026 15:42:22 +0900
Subject: [PATCH] Add certificate validation type output
---
README.md | 119 ++++++++++++++++++----------------
cert.go | 149 +++++++++++++++++++++++++++++++++----------
cert_example_test.go | 25 ++++----
cert_test.go | 120 ++++++++++++++++++++++++++++++----
4 files changed, 301 insertions(+), 112 deletions(-)
diff --git a/README.md b/README.md
index 552a52e..271bf0e 100644
--- a/README.md
+++ b/README.md
@@ -28,24 +28,27 @@ Give domain names as arguments.
```sh
$ cert github.com google.co.jp
-DomainName: github.com
-IP: 192.30.255.113
-Port: 443
-Issuer: DigiCert SHA2 Extended Validation Server CA
-NotBefore: 2016-03-10 09:00:00 +0900 JST
-NotAfter: 2018-05-17 21:00:00 +0900 JST
-CommonName: github.com
-SANs: [github.com www.github.com]
+DomainName: github.com
+IP: 192.30.255.113
+Port: 443
+Issuer: DigiCert SHA2 Extended Validation Server CA
+Validation: EV
+Organization: GitHub, Inc.
+NotBefore: 2016-03-10 09:00:00 +0900 JST
+NotAfter: 2018-05-17 21:00:00 +0900 JST
+CommonName: github.com
+SANs: [github.com www.github.com]
Error:
-DomainName: google.co.jp
-IP: 216.58.196.227
-Port: 443
-Issuer: Google Internet Authority G3
-NotBefore: 2017-10-17 19:59:51 +0900 JST
-NotAfter: 2018-01-09 19:00:00 +0900 JST
-CommonName: *.google.co.jp
-SANs: [*.google.co.jp google.co.jp]
+DomainName: google.co.jp
+IP: 216.58.196.227
+Port: 443
+Issuer: Google Internet Authority G3
+Validation: DV
+NotBefore: 2017-10-17 19:59:51 +0900 JST
+NotAfter: 2018-01-09 19:00:00 +0900 JST
+CommonName: *.google.co.jp
+SANs: [*.google.co.jp google.co.jp]
Error:
```
@@ -55,34 +58,38 @@ So you can get server certificate information of not only web server but also *m
```sh
$ cert github.com google.co.jp:443 imap.gmail.com:993
-DomainName: github.com
-IP: 192.30.255.113
-Port: 443
-Issuer: DigiCert SHA2 Extended Validation Server CA
-NotBefore: 2016-03-10 09:00:00 +0900 JST
-NotAfter: 2018-05-17 21:00:00 +0900 JST
-CommonName: github.com
-SANs: [github.com www.github.com]
+DomainName: github.com
+IP: 192.30.255.113
+Port: 443
+Issuer: DigiCert SHA2 Extended Validation Server CA
+Validation: EV
+Organization: GitHub, Inc.
+NotBefore: 2016-03-10 09:00:00 +0900 JST
+NotAfter: 2018-05-17 21:00:00 +0900 JST
+CommonName: github.com
+SANs: [github.com www.github.com]
Error:
-DomainName: google.co.jp
-IP: 172.217.27.163
-Port: 443
-Issuer: Google Internet Authority G3
-NotBefore: 2017-10-17 19:59:51 +0900 JST
-NotAfter: 2018-01-09 19:00:00 +0900 JST
-CommonName: *.google.co.jp
-SANs: [*.google.co.jp google.co.jp]
+DomainName: google.co.jp
+IP: 172.217.27.163
+Port: 443
+Issuer: Google Internet Authority G3
+Validation: DV
+NotBefore: 2017-10-17 19:59:51 +0900 JST
+NotAfter: 2018-01-09 19:00:00 +0900 JST
+CommonName: *.google.co.jp
+SANs: [*.google.co.jp google.co.jp]
Error:
-DomainName: imap.gmail.com
-IP: 64.233.188.108
-Port: 993
-Issuer: Google Internet Authority G2
-NotBefore: 2017-10-17 19:10:29 +0900 JST
-NotAfter: 2017-12-29 09:00:00 +0900 JST
-CommonName: imap.gmail.com
-SANs: [imap.gmail.com]
+DomainName: imap.gmail.com
+IP: 64.233.188.108
+Port: 993
+Issuer: Google Internet Authority G2
+Validation: DV
+NotBefore: 2017-10-17 19:10:29 +0900 JST
+NotAfter: 2017-12-29 09:00:00 +0900 JST
+CommonName: imap.gmail.com
+SANs: [imap.gmail.com]
Error:
```
@@ -127,18 +134,20 @@ Use `cert -f json`.
$ cert -f json github.com | jq .
[
{
- "DomainName": "github.com",
- "IP": "192.30.255.112",
- "Port": "443",
- "Issuer": "DigiCert SHA2 Extended Validation Server CA",
- "CommonName": "github.com",
- "SANs": [
+ "domainName": "github.com",
+ "ip": "192.30.255.112",
+ "port": "443",
+ "issuer": "DigiCert SHA2 Extended Validation Server CA",
+ "validationType": "EV",
+ "organization": "GitHub, Inc.",
+ "commonName": "github.com",
+ "sans": [
"github.com",
"www.github.com"
],
- "NotBefore": "2016-03-10 09:00:00 +0900 JST",
- "NotAfter": "2018-05-17 21:00:00 +0900 JST",
- "Error": ""
+ "notBefore": "2016-03-10 09:00:00 +0900 JST",
+ "notAfter": "2018-05-17 21:00:00 +0900 JST",
+ "error": ""
}
]
```
@@ -149,14 +158,14 @@ Use `cert -f md`.
```sh
$ cert -f md github.com
-DomainName | IP | Port | Issuer | NotBefore | NotAfter | CN | SANs | Error
---- | --- | --- | --- | --- | --- | --- | --- | ---
-github.com | 192.30.255.113 | 443 | DigiCert SHA2 Extended Validation Server CA | 2016-03-10 09:00:00 +0900 JST | 2018-05-17 21:00:00 +0900 JST | github.com | github.com
www.github.com
|
+DomainName | IP | Port | Issuer | Validation | Organization | NotBefore | NotAfter | CN | SANs | Error
+--- | --- | --- | --- | --- | --- | --- | --- | --- | --- | ---
+github.com | 192.30.255.113 | 443 | DigiCert SHA2 Extended Validation Server CA | EV | GitHub, Inc. | 2016-03-10 09:00:00 +0900 JST | 2018-05-17 21:00:00 +0900 JST | github.com | github.com
www.github.com
|
```
-DomainName | IP | Port | Issuer | NotBefore | NotAfter | CN | SANs | Error
---- | --- | --- | --- | --- | --- | --- | --- | ---
-github.com | 192.30.255.113 | 443 | DigiCert SHA2 Extended Validation Server CA | 2016-03-10 09:00:00 +0900 JST | 2018-05-17 21:00:00 +0900 JST | github.com | github.com
www.github.com
|
+DomainName | IP | Port | Issuer | Validation | Organization | NotBefore | NotAfter | CN | SANs | Error
+--- | --- | --- | --- | --- | --- | --- | --- | --- | --- | ---
+github.com | 192.30.255.113 | 443 | DigiCert SHA2 Extended Validation Server CA | EV | GitHub, Inc. | 2016-03-10 09:00:00 +0900 JST | 2018-05-17 21:00:00 +0900 JST | github.com | github.com
www.github.com
|
### Specify output format by Go template
diff --git a/cert.go b/cert.go
index e0cd47a..75a88da 100644
--- a/cert.go
+++ b/cert.go
@@ -4,6 +4,7 @@ import (
"bytes"
"crypto/tls"
"crypto/x509"
+ "encoding/asn1"
"encoding/json"
"fmt"
"io/ioutil"
@@ -102,18 +103,38 @@ func SplitHostPort(hostport string) (string, string, error) {
}
type Cert struct {
- DomainName string `json:"domainName"`
- IP string `json:"ip"`
- Port string `json:"port"`
- Issuer string `json:"issuer"`
- CommonName string `json:"commonName"`
- SANs []string `json:"sans"`
- NotBefore string `json:"notBefore"`
- NotAfter string `json:"notAfter"`
- Error string `json:"error"`
- certChain []*x509.Certificate
+ DomainName string `json:"domainName"`
+ IP string `json:"ip"`
+ Port string `json:"port"`
+ Issuer string `json:"issuer"`
+ ValidationType string `json:"validationType"`
+ Organization string `json:"organization,omitempty"`
+ CommonName string `json:"commonName"`
+ SANs []string `json:"sans"`
+ NotBefore string `json:"notBefore"`
+ NotAfter string `json:"notAfter"`
+ Error string `json:"error"`
+ certChain []*x509.Certificate
}
+const (
+ ValidationTypeDV = "DV"
+ ValidationTypeOV = "OV"
+ ValidationTypeEV = "EV"
+)
+
+var (
+ cabfDVPolicy = asn1.ObjectIdentifier{2, 23, 140, 1, 2, 1}
+ cabfOVPolicy = asn1.ObjectIdentifier{2, 23, 140, 1, 2, 2}
+ cabfEVPolicy = asn1.ObjectIdentifier{2, 23, 140, 1, 1}
+
+ oidBusinessCategory = asn1.ObjectIdentifier{2, 5, 4, 15}
+ oidJurisdictionLocality = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 311, 60, 2, 1, 1}
+ oidJurisdictionState = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 311, 60, 2, 1, 2}
+ oidJurisdictionCountry = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 311, 60, 2, 1, 3}
+ oidSubjectSerialNumber = asn1.ObjectIdentifier{2, 5, 4, 5}
+)
+
func cipherSuite() ([]uint16, error) {
if CipherSuite == "" {
return nil, nil
@@ -135,6 +156,65 @@ func tlsVersion() uint16 {
return 0
}
+func hasPolicy(cert *x509.Certificate, oid asn1.ObjectIdentifier) bool {
+ for _, policy := range cert.PolicyIdentifiers {
+ if policy.String() == oid.String() {
+ return true
+ }
+ }
+ return false
+}
+
+func hasSubjectAttribute(cert *x509.Certificate, oid asn1.ObjectIdentifier) bool {
+ for _, name := range cert.Subject.Names {
+ if name.Type.String() == oid.String() {
+ return true
+ }
+ }
+ return false
+}
+
+func isEV(cert *x509.Certificate) bool {
+ if hasPolicy(cert, cabfEVPolicy) {
+ return true
+ }
+ if len(cert.Subject.Organization) == 0 {
+ return false
+ }
+ if !hasSubjectAttribute(cert, oidBusinessCategory) {
+ return false
+ }
+ if !hasSubjectAttribute(cert, oidSubjectSerialNumber) {
+ return false
+ }
+ return hasSubjectAttribute(cert, oidJurisdictionCountry) ||
+ hasSubjectAttribute(cert, oidJurisdictionState) ||
+ hasSubjectAttribute(cert, oidJurisdictionLocality)
+}
+
+func validationType(cert *x509.Certificate) string {
+ if isEV(cert) {
+ return ValidationTypeEV
+ }
+ if hasPolicy(cert, cabfOVPolicy) {
+ return ValidationTypeOV
+ }
+ if hasPolicy(cert, cabfDVPolicy) {
+ return ValidationTypeDV
+ }
+ if len(cert.Subject.Organization) > 0 {
+ return ValidationTypeOV
+ }
+ return ValidationTypeDV
+}
+
+func organization(cert *x509.Certificate, validationType string) string {
+ if validationType == ValidationTypeDV || len(cert.Subject.Organization) == 0 {
+ return ""
+ }
+ return strings.Join(cert.Subject.Organization, ", ")
+}
+
var serverCert = func(host, port string) ([]*x509.Certificate, string, error) {
d := &net.Dialer{
Timeout: time.Duration(TimeoutSeconds) * time.Second,
@@ -178,18 +258,21 @@ func NewCert(hostport string) *Cert {
if UTC {
loc = time.UTC
}
+ vt := validationType(cert)
return &Cert{
- DomainName: host,
- IP: ip,
- Port: port,
- Issuer: cert.Issuer.CommonName,
- CommonName: cert.Subject.CommonName,
- SANs: cert.DNSNames,
- NotBefore: cert.NotBefore.In(loc).String(),
- NotAfter: cert.NotAfter.In(loc).String(),
- Error: "",
- certChain: certChain,
+ DomainName: host,
+ IP: ip,
+ Port: port,
+ Issuer: cert.Issuer.CommonName,
+ ValidationType: vt,
+ Organization: organization(cert, vt),
+ CommonName: cert.Subject.CommonName,
+ SANs: cert.DNSNames,
+ NotBefore: cert.NotBefore.In(loc).String(),
+ NotAfter: cert.NotAfter.In(loc).String(),
+ Error: "",
+ certChain: certChain,
}
}
@@ -239,15 +322,17 @@ func NewCerts(s []string) (Certs, error) {
return certs, nil
}
-const defaultTempl = `{{range .}}DomainName: {{.DomainName}}
-IP: {{.IP}}
-Port: {{.Port}}
-Issuer: {{.Issuer}}
-NotBefore: {{.NotBefore}}
-NotAfter: {{.NotAfter}}
-CommonName: {{.CommonName}}
-SANs: {{.SANs}}
-Error: {{.Error}}
+const defaultTempl = `{{range .}}DomainName: {{.DomainName}}
+IP: {{.IP}}
+Port: {{.Port}}
+Issuer: {{.Issuer}}
+Validation: {{.ValidationType}}
+{{if .Organization}}Organization: {{.Organization}}
+{{end}}NotBefore: {{.NotBefore}}
+NotAfter: {{.NotAfter}}
+CommonName: {{.CommonName}}
+SANs: {{.SANs}}
+Error: {{.Error}}
{{end}}
`
@@ -267,9 +352,9 @@ func (certs Certs) String() string {
return b.String()
}
-const markdownTempl = `DomainName | IP | Port | Issuer | NotBefore | NotAfter | CN | SANs | Error
---- | --- | --- | --- | --- | --- | --- | --- | ---
-{{range .}}{{.DomainName}} | {{.IP}} | {{.Port}} | {{.Issuer}} | {{.NotBefore}} | {{.NotAfter}} | {{.CommonName}} | {{range .SANs}}{{.}}
{{end}} | {{.Error}}
+const markdownTempl = `DomainName | IP | Port | Issuer | Validation | Organization | NotBefore | NotAfter | CN | SANs | Error
+--- | --- | --- | --- | --- | --- | --- | --- | --- | --- | ---
+{{range .}}{{.DomainName}} | {{.IP}} | {{.Port}} | {{.Issuer}} | {{.ValidationType}} | {{.Organization}} | {{.NotBefore}} | {{.NotAfter}} | {{.CommonName}} | {{range .SANs}}{{.}}
{{end}} | {{.Error}}
{{end}}
`
diff --git a/cert_example_test.go b/cert_example_test.go
index e6824b5..71fe824 100644
--- a/cert_example_test.go
+++ b/cert_example_test.go
@@ -9,14 +9,15 @@ func ExampleCerts_String() {
fmt.Printf("%s", certs)
// Output:
- // DomainName: example.com
- // IP: 127.0.0.1
- // Port: 443
- // Issuer: CA for test
- // NotBefore: 2017-01-01 00:00:00 +0000 UTC
- // NotAfter: 2018-01-01 00:00:00 +0000 UTC
- // CommonName: example.com
- // SANs: [example.com www.example.com]
+ // DomainName: example.com
+ // IP: 127.0.0.1
+ // Port: 443
+ // Issuer: CA for test
+ // Validation: DV
+ // NotBefore: 2017-01-01 00:00:00 +0000 UTC
+ // NotAfter: 2018-01-01 00:00:00 +0000 UTC
+ // CommonName: example.com
+ // SANs: [example.com www.example.com]
// Error:
}
@@ -25,9 +26,9 @@ func ExampleCerts_Markdown() {
fmt.Printf("%s", certs.Markdown())
// Output:
- // DomainName | IP | Port | Issuer | NotBefore | NotAfter | CN | SANs | Error
- // --- | --- | --- | --- | --- | --- | --- | --- | ---
- // example.com | 127.0.0.1 | 443 | CA for test | 2017-01-01 00:00:00 +0000 UTC | 2018-01-01 00:00:00 +0000 UTC | example.com | example.com
www.example.com
|
+ // DomainName | IP | Port | Issuer | Validation | Organization | NotBefore | NotAfter | CN | SANs | Error
+ // --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | ---
+ // example.com | 127.0.0.1 | 443 | CA for test | DV | | 2017-01-01 00:00:00 +0000 UTC | 2018-01-01 00:00:00 +0000 UTC | example.com | example.com
www.example.com
|
}
func ExampleCerts_JSON() {
@@ -35,5 +36,5 @@ func ExampleCerts_JSON() {
fmt.Printf("%s", certs.JSON())
// Output:
- // [{"domainName":"example.com","ip":"127.0.0.1","port":"443","issuer":"CA for test","commonName":"example.com","sans":["example.com","www.example.com"],"notBefore":"2017-01-01 00:00:00 +0000 UTC","notAfter":"2018-01-01 00:00:00 +0000 UTC","error":""}]
+ // [{"domainName":"example.com","ip":"127.0.0.1","port":"443","issuer":"CA for test","validationType":"DV","commonName":"example.com","sans":["example.com","www.example.com"],"notBefore":"2017-01-01 00:00:00 +0000 UTC","notAfter":"2018-01-01 00:00:00 +0000 UTC","error":""}]
}
diff --git a/cert_test.go b/cert_test.go
index 28f69ce..4bf97e3 100644
--- a/cert_test.go
+++ b/cert_test.go
@@ -4,6 +4,7 @@ import (
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
+ "encoding/asn1"
"fmt"
"os"
"testing"
@@ -114,6 +115,12 @@ func TestNewCert(t *testing.T) {
if c.Issuer != "CA for test" {
t.Errorf(`unexpected Cert.Issuer %q, want %q`, c.Issuer, "CA for test")
}
+ if c.ValidationType != ValidationTypeDV {
+ t.Errorf(`unexpected Cert.ValidationType %q, want %q`, c.ValidationType, ValidationTypeDV)
+ }
+ if c.Organization != "" {
+ t.Errorf(`unexpected Cert.Organization %q, want empty`, c.Organization)
+ }
if c.CommonName != "example.com" {
t.Errorf(`unexpected Cert.CommonName %q, want %q`, c.CommonName, "example.com")
}
@@ -151,15 +158,16 @@ func TestCertsAsString(t *testing.T) {
certChain, _, _ := serverCert("example.com", defaultPort)
origCert := certChain[0]
- expected := fmt.Sprintf(`DomainName: example.com
-IP: 127.0.0.1
-Port: 443
-Issuer: CA for test
-NotBefore: %s
-NotAfter: %s
-CommonName: example.com
-SANs: [example.com www.example.com]
-Error:
+ expected := fmt.Sprintf(`DomainName: example.com
+IP: 127.0.0.1
+Port: 443
+Issuer: CA for test
+Validation: DV
+NotBefore: %s
+NotAfter: %s
+CommonName: example.com
+SANs: [example.com www.example.com]
+Error:
`, origCert.NotBefore.String(), origCert.NotAfter.String())
@@ -175,9 +183,9 @@ func TestCertsAsMarkdown(t *testing.T) {
certChain, _, _ := serverCert("example.com", defaultPort)
origCert := certChain[0]
- expected := fmt.Sprintf(`DomainName | IP | Port | Issuer | NotBefore | NotAfter | CN | SANs | Error
---- | --- | --- | --- | --- | --- | --- | --- | ---
-example.com | 127.0.0.1 | 443 | CA for test | %s | %s | example.com | example.com
www.example.com
|
+ expected := fmt.Sprintf(`DomainName | IP | Port | Issuer | Validation | Organization | NotBefore | NotAfter | CN | SANs | Error
+--- | --- | --- | --- | --- | --- | --- | --- | --- | --- | ---
+example.com | 127.0.0.1 | 443 | CA for test | DV | | %s | %s | example.com | example.com
www.example.com
|
`, origCert.NotBefore.String(), origCert.NotAfter.String())
@@ -192,7 +200,7 @@ func TestCertsAsJSON(t *testing.T) {
certChain, _, _ := serverCert("example.com", defaultPort)
origCert := certChain[0]
- expected := fmt.Sprintf("[{\"domainName\":\"example.com\",\"ip\":\"127.0.0.1\",\"port\":\"443\",\"issuer\":\"CA for test\",\"commonName\":\"example.com\",\"sans\":[\"example.com\",\"www.example.com\"],\"notBefore\":%q,\"notAfter\":%q,\"error\":\"\"}]", origCert.NotBefore.String(), origCert.NotAfter.String())
+ expected := fmt.Sprintf("[{\"domainName\":\"example.com\",\"ip\":\"127.0.0.1\",\"port\":\"443\",\"issuer\":\"CA for test\",\"validationType\":\"DV\",\"commonName\":\"example.com\",\"sans\":[\"example.com\",\"www.example.com\"],\"notBefore\":%q,\"notAfter\":%q,\"error\":\"\"}]", origCert.NotBefore.String(), origCert.NotAfter.String())
certs, _ := NewCerts([]string{"example.com"})
@@ -215,6 +223,92 @@ func TestCertsEscapeStarInSANs(t *testing.T) {
}
}
+func TestValidationTypeByPolicy(t *testing.T) {
+ tests := []struct {
+ name string
+ cert *x509.Certificate
+ want string
+ }{
+ {
+ name: "dv",
+ cert: &x509.Certificate{
+ PolicyIdentifiers: []asn1.ObjectIdentifier{cabfDVPolicy},
+ },
+ want: ValidationTypeDV,
+ },
+ {
+ name: "ov",
+ cert: &x509.Certificate{
+ Subject: pkix.Name{
+ Organization: []string{"Example Inc."},
+ },
+ PolicyIdentifiers: []asn1.ObjectIdentifier{cabfOVPolicy},
+ },
+ want: ValidationTypeOV,
+ },
+ {
+ name: "ev",
+ cert: &x509.Certificate{
+ Subject: pkix.Name{
+ Organization: []string{"Example Inc."},
+ },
+ PolicyIdentifiers: []asn1.ObjectIdentifier{cabfEVPolicy},
+ },
+ want: ValidationTypeEV,
+ },
+ }
+
+ for _, test := range tests {
+ got := validationType(test.cert)
+ if got != test.want {
+ t.Errorf("%s: validationType() = %q, want %q", test.name, got, test.want)
+ }
+ }
+}
+
+func TestValidationTypeBySubjectFallback(t *testing.T) {
+ ovCert := &x509.Certificate{
+ Subject: pkix.Name{
+ Organization: []string{"Example Inc."},
+ },
+ }
+ if got := validationType(ovCert); got != ValidationTypeOV {
+ t.Errorf("validationType() = %q, want %q", got, ValidationTypeOV)
+ }
+
+ evCert := &x509.Certificate{
+ Subject: pkix.Name{
+ Organization: []string{"Example Inc."},
+ Names: []pkix.AttributeTypeAndValue{
+ {Type: oidBusinessCategory, Value: "Private Organization"},
+ {Type: oidSubjectSerialNumber, Value: "1234567"},
+ {Type: oidJurisdictionCountry, Value: "JP"},
+ },
+ },
+ }
+ if got := validationType(evCert); got != ValidationTypeEV {
+ t.Errorf("validationType() = %q, want %q", got, ValidationTypeEV)
+ }
+}
+
+func TestOrganization(t *testing.T) {
+ cert := &x509.Certificate{
+ Subject: pkix.Name{
+ Organization: []string{"Example Inc.", "Example Co."},
+ },
+ }
+
+ if got := organization(cert, ValidationTypeDV); got != "" {
+ t.Errorf("organization() = %q, want empty", got)
+ }
+ if got := organization(cert, ValidationTypeOV); got != "Example Inc., Example Co." {
+ t.Errorf("organization() = %q, want %q", got, "Example Inc., Example Co.")
+ }
+ if got := organization(cert, ValidationTypeEV); got != "Example Inc., Example Co." {
+ t.Errorf("organization() = %q, want %q", got, "Example Inc., Example Co.")
+ }
+}
+
func TestSetUserTempl(t *testing.T) {
_ = SetUserTempl("{{range .}}Issuer: {{.Issuer}}{{end}}")
expected := "Issuer: CA for test"