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"