certificate/certificate.go (474 lines of code) (raw):
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/* The following code is adapted from code from:
* https://github.com/mozilla/tls-observatory/blob/7bc42856d2e5594614b56c2f55baf42bb9751b3d/certificate/certificate.go */
package main
import (
"crypto/dsa"
"crypto/ecdsa"
"crypto/rsa"
"crypto/sha1"
"crypto/sha256"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"encoding/base64"
"fmt"
"log"
"net"
"strconv"
"time"
)
type Certificate struct {
Serial string `json:"serialNumber"`
ScanTarget string `json:"scanTarget,omitempty"`
IPs []string `json:"ips,omitempty"`
Version int `json:"version"`
SignatureAlgorithm string `json:"signatureAlgorithm"`
Issuer Subject `json:"issuer"`
Validity Validity `json:"validity"`
Subject Subject `json:"subject"`
Key SubjectPublicKeyInfo `json:"key"`
X509v3Extensions Extensions `json:"x509v3Extensions"`
X509v3BasicConstraints string `json:"x509v3BasicConstraints"`
CA bool `json:"ca"`
Analysis interface{} `json:"analysis,omitempty"` //for future use...
FirstSeenTimestamp time.Time `json:"firstSeenTimestamp"`
LastSeenTimestamp time.Time `json:"lastSeenTimestamp"`
Hashes Hashes `json:"hashes"`
Raw string `json:"Raw"`
Anomalies string `json:"anomalies,omitempty"`
MozillaPolicyV25 MozillaPolicy `json:"mozillaPolicyV2_5"`
MozillaPolicyV29 MozillaPolicy `json:"mozillaPolicyV2_9"`
}
type MozillaPolicy struct {
IsTechnicallyConstrained bool
}
type Hashes struct {
SHA1 string `json:"sha1,omitempty"`
SHA256 string `json:"sha256,omitempty"`
SPKISHA256 string `json:"spki-sha256,omitempty"`
SubjectSPKISHA256 string `json:"subject-spki-sha256,omitempty"`
PKPSHA256 string `json:"pin-sha256,omitempty"`
}
type Validity struct {
NotBefore time.Time `json:"notBefore"`
NotAfter time.Time `json:"notAfter"`
}
type Subject struct {
Country []string `json:"c,omitempty"`
Organization []string `json:"o,omitempty"`
OrganizationalUnit []string `json:"ou,omitempty"`
CommonName string `json:"cn,omitempty"`
Locality []string `json:"l,omitempty"`
StateOrProvince []string `json:"st,omitempty"`
StreetAddress []string `json:"streetAddress,omitempty"`
PostalCode []string `json:"postalCode,omitempty"`
SerialNumber string `json:"serialNumber,omitempty"`
EmailAddress any `json:"emailAddress,omitempty"`
UID any `json:"uid,omitempty"`
DomainComponent []string `json:"dc,omitempty"`
Name any `json:"name,omitempty"`
Surname any `json:"surname,omitempty"`
GivenName any `json:"givenName,omitempty"`
Initials any `json:"initials,omitempty"`
GenerationQualifier any `json:"generationQualifier,omitempty"`
Title any `json:"title,omitempty"`
Pseudonym any `json:"pseudonym,omitempty"`
BusinessCategory any `json:"businessCategory,omitempty"`
JurisdictionLocality any `json:"jurisdictionLocality,omitempty"`
JurisdictionStateOrProvince any `json:"jurisdictionStateOrProvince,omitempty"`
JurisdictionCountry any `json:"jurisdictionCountry,omitempty"`
OrganizationIdentifier any `json:"organizationIdentifier,omitempty"`
DNQualifier any `json:"dnQualifier,omitempty"`
}
type SubjectPublicKeyInfo struct {
Alg string `json:"alg,omitempty"`
Size float64 `json:"size,omitempty"`
Exponent float64 `json:"exponent,omitempty"`
X string `json:"x,omitempty"`
Y string `json:"y,omitempty"`
P string `json:"p,omitempty"`
Q string `json:"q,omitempty"`
G string `json:"g,omitempty"`
Curve string `json:"curve,omitempty"`
}
// Extensions that are already decoded in the x509 Certificate structure
type Extensions struct {
AuthorityKeyId string `json:"authorityKeyId"`
SubjectKeyId string `json:"subjectKeyId"`
KeyUsage []string `json:"keyUsage"`
ExtendedKeyUsage []string `json:"extendedKeyUsage"`
ExtendedKeyUsageOID []string `json:"extendedKeyUsageOID"`
SubjectAlternativeName []string `json:"subjectAlternativeName"`
CRLDistributionPoints []string `json:"crlDistributionPoint"`
PolicyIdentifiers []string `json:"policyIdentifiers,omitempty"`
PermittedDNSDomains []string `json:"permittedDNSNames,omitempty"`
PermittedIPAddresses []string `json:"permittedIPAddresses,omitempty"`
ExcludedDNSDomains []string `json:"excludedDNSNames,omitempty"`
ExcludedIPAddresses []string `json:"excludedIPAddresses,omitempty"`
InhibitAnyPolicy *int `json:"inhibitAnyPolicy,omitempty"`
}
var SignatureAlgorithm = [...]string{
"UnknownSignatureAlgorithm",
"MD2WithRSA",
"MD5WithRSA",
"SHA1WithRSA",
"SHA256WithRSA",
"SHA384WithRSA",
"SHA512WithRSA",
"DSAWithSHA1",
"DSAWithSHA256",
"ECDSAWithSHA1",
"ECDSAWithSHA256",
"ECDSAWithSHA384",
"ECDSAWithSHA512",
"SHA256WithRSAPSS",
"SHA384WithRSAPSS",
"SHA512WithRSAPSS",
"PureEd25519",
}
var ExtKeyUsage = [...]string{
"ExtKeyUsageAny",
"ExtKeyUsageServerAuth",
"ExtKeyUsageClientAuth",
"ExtKeyUsageCodeSigning",
"ExtKeyUsageEmailProtection",
"ExtKeyUsageIPSECEndSystem",
"ExtKeyUsageIPSECTunnel",
"ExtKeyUsageIPSECUser",
"ExtKeyUsageTimeStamping",
"ExtKeyUsageOCSPSigning",
"ExtKeyUsageMicrosoftServerGatedCrypto",
"ExtKeyUsageNetscapeServerGatedCrypto",
"ExtKeyUsageMicrosoftCommercialCodeSigning",
"ExtKeyUsageMicrosoftKernelCodeSigning",
}
var ExtKeyUsageOID = [...]string{
asn1.ObjectIdentifier{2, 5, 29, 37, 0}.String(), // ExtKeyUsageAny
asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 1}.String(), // ExtKeyUsageServerAuth
asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 2}.String(), // ExtKeyUsageClientAuth
asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 3}.String(), // ExtKeyUsageCodeSigning
asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 4}.String(), // ExtKeyUsageEmailProtection
asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 5}.String(), // ExtKeyUsageIPSECEndSystem
asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 6}.String(), // ExtKeyUsageIPSECTunnel
asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 7}.String(), // ExtKeyUsageIPSECUser
asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 8}.String(), // ExtKeyUsageTimeStamping
asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 9}.String(), // ExtKeyUsageOCSPSigning
asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 311, 10, 3, 3}.String(), // ExtKeyUsageMicrosoftServerGatedCrypto
asn1.ObjectIdentifier{2, 16, 840, 1, 113730, 4, 1}.String(), // ExtKeyUsageNetscapeServerGatedCrypto
asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 311, 2, 1, 22}.String(), // ExtKeyUsageMicrosoftCommercialCodeSigning
asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 311, 61, 1, 1}.String(), // ExtKeyUsageMicrosoftKernelCodeSigning
}
var PublicKeyAlgorithm = [...]string{
"UnknownPublicKeyAlgorithm",
"RSA",
"DSA",
"ECDSA",
}
func SubjectSPKISHA256(cert *x509.Certificate) string {
h := sha256.New()
h.Write(cert.RawSubject)
h.Write(cert.RawSubjectPublicKeyInfo)
return fmt.Sprintf("%X", h.Sum(nil))
}
func SPKISHA256(cert *x509.Certificate) string {
h := sha256.New()
h.Write(cert.RawSubjectPublicKeyInfo)
return fmt.Sprintf("%X", h.Sum(nil))
}
func PKPSHA256Hash(cert *x509.Certificate) string {
h := sha256.New()
switch pub := cert.PublicKey.(type) {
case *rsa.PublicKey, *dsa.PublicKey, *ecdsa.PublicKey:
der, _ := x509.MarshalPKIXPublicKey(pub)
h.Write(der)
default:
return ""
}
return base64.StdEncoding.EncodeToString(h.Sum(nil))
}
func SHA256Hash(data []byte) string {
h := sha256.Sum256(data)
return fmt.Sprintf("%X", h[:])
}
func SHA1Hash(data []byte) string {
h := sha1.Sum(data)
return fmt.Sprintf("%X", h[:])
}
func getExtKeyUsages(cert *x509.Certificate) []string {
usage := make([]string, 0)
for _, eku := range cert.ExtKeyUsage {
usage = append(usage, ExtKeyUsage[eku])
}
for _, unknownEku := range cert.UnknownExtKeyUsage {
usage = append(usage, unknownEku.String())
}
return usage
}
func getExtKeyUsageOIDs(cert *x509.Certificate) []string {
usage := make([]string, 0)
for _, eku := range cert.ExtKeyUsage {
usage = append(usage, ExtKeyUsageOID[eku])
}
for _, unknownEku := range cert.UnknownExtKeyUsage {
usage = append(usage, unknownEku.String())
}
return usage
}
func getPolicyIdentifiers(cert *x509.Certificate) []string {
identifiers := make([]string, 0)
for _, pi := range cert.PolicyIdentifiers {
identifiers = append(identifiers, pi.String())
}
return identifiers
}
func getKeyUsages(cert *x509.Certificate) []string {
usage := make([]string, 0)
keyUsage := cert.KeyUsage
//calculate included keyUsage from bitmap
//String values taken from OpenSSL
if keyUsage&x509.KeyUsageDigitalSignature != 0 {
usage = append(usage, "Digital Signature")
}
if keyUsage&x509.KeyUsageContentCommitment != 0 {
usage = append(usage, "Non Repudiation")
}
if keyUsage&x509.KeyUsageKeyEncipherment != 0 {
usage = append(usage, "Key Encipherment")
}
if keyUsage&x509.KeyUsageDataEncipherment != 0 {
usage = append(usage, "Data Encipherment")
}
if keyUsage&x509.KeyUsageKeyAgreement != 0 {
usage = append(usage, "Key Agreement")
}
if keyUsage&x509.KeyUsageCertSign != 0 {
usage = append(usage, "Certificate Sign")
}
if keyUsage&x509.KeyUsageCRLSign != 0 {
usage = append(usage, "CRL Sign")
}
if keyUsage&x509.KeyUsageEncipherOnly != 0 {
usage = append(usage, "Encipher Only")
}
if keyUsage&x509.KeyUsageDecipherOnly != 0 {
usage = append(usage, "Decipher Only")
}
return usage
}
// getCertExtensions currently stores only the extensions that are already exported by GoLang
// (in the x509 Certificate Struct)
func getCertExtensions(cert *x509.Certificate) Extensions {
// initialize []string to store them as `[]` instead of null
san := make([]string, 0)
san = append(san, cert.DNSNames...)
crld := make([]string, 0)
crld = append(crld, cert.CRLDistributionPoints...)
constraints, _ := GetConstraints(cert)
ipNetSliceToStringSlice := func(in []*net.IPNet) []string {
out := make([]string, 0)
for _, ipnet := range in {
out = append(out, ipnet.String())
}
return out
}
permittedIPAddresses := ipNetSliceToStringSlice(constraints.PermittedIPRanges)
excludedIPAddresses := ipNetSliceToStringSlice(constraints.ExcludedIPRanges)
ext := Extensions{
AuthorityKeyId: base64.StdEncoding.EncodeToString(cert.AuthorityKeyId),
SubjectKeyId: base64.StdEncoding.EncodeToString(cert.SubjectKeyId),
KeyUsage: getKeyUsages(cert),
ExtendedKeyUsage: getExtKeyUsages(cert),
ExtendedKeyUsageOID: getExtKeyUsageOIDs(cert),
PolicyIdentifiers: getPolicyIdentifiers(cert),
SubjectAlternativeName: san,
CRLDistributionPoints: crld,
PermittedDNSDomains: constraints.PermittedDNSDomains,
ExcludedDNSDomains: constraints.ExcludedDNSDomains,
PermittedIPAddresses: permittedIPAddresses,
ExcludedIPAddresses: excludedIPAddresses,
}
for _, v := range cert.Extensions {
if OIDFieldName(v.Id) == "InhibitAnyPolicy" {
value, _ := strconv.Atoi(string(v.Value))
ext.InhibitAnyPolicy = &value
}
}
return ext
}
func getMozillaPolicyV25(cert *x509.Certificate) MozillaPolicy {
return MozillaPolicy{IsTechnicallyConstrained: IsTechnicallyConstrainedMozPolicyV25(cert)}
}
func getMozillaPolicyV29(cert *x509.Certificate) MozillaPolicy {
return MozillaPolicy{IsTechnicallyConstrained: IsTechnicallyConstrainedMozPolicyV29(cert)}
}
func getPublicKeyInfo(cert *x509.Certificate) (SubjectPublicKeyInfo, error) {
pubInfo := SubjectPublicKeyInfo{
Alg: PublicKeyAlgorithm[cert.PublicKeyAlgorithm],
}
switch pub := cert.PublicKey.(type) {
case *rsa.PublicKey:
pubInfo.Size = float64(pub.N.BitLen())
pubInfo.Exponent = float64(pub.E)
case *dsa.PublicKey:
pubInfo.Size = float64(pub.Y.BitLen())
textInt, err := pub.G.MarshalText()
if err == nil {
pubInfo.G = string(textInt)
} else {
return pubInfo, err
}
textInt, err = pub.P.MarshalText()
if err == nil {
pubInfo.P = string(textInt)
} else {
return pubInfo, err
}
textInt, err = pub.Q.MarshalText()
if err == nil {
pubInfo.Q = string(textInt)
} else {
return pubInfo, err
}
textInt, err = pub.Y.MarshalText()
if err == nil {
pubInfo.Y = string(textInt)
} else {
return pubInfo, err
}
case *ecdsa.PublicKey:
pubInfo.Size = float64(pub.Curve.Params().BitSize)
pubInfo.Curve = pub.Curve.Params().Name
pubInfo.Y = pub.Y.String()
pubInfo.X = pub.X.String()
}
return pubInfo, nil
}
func GetHexASN1Serial(cert *x509.Certificate) (serial string, err error) {
m, err := asn1.Marshal(cert.SerialNumber)
if err != nil {
return
}
var rawValue asn1.RawValue
_, err = asn1.Unmarshal(m, &rawValue)
if err != nil {
return
}
serial = fmt.Sprintf("%X", rawValue.Bytes)
return
}
// OIDFieldName takes in an OID asn1.ObjectIdentifier and returns the field name
// This is where we can add additional OIDs and fields that Go doesn't support natively
func OIDFieldName(oid asn1.ObjectIdentifier) string {
switch oid.String() {
case "2.5.29.54":
return "InhibitAnyPolicy"
default:
return ""
}
}
// GetDomainComponent converts the Domain Component field for Subject/Issuer to an array of strings
// for multiple DCs. Eg - dc=com,dc=example
func GetDomainComponent(attributes []pkix.AttributeTypeAndValue) []string {
var domainComponents []string
for _, v := range attributes {
if v.Type.String() == "0.9.2342.19200300.100.1.25" {
domainComponents = append(domainComponents, v.Value.(string))
}
}
return domainComponents
}
// GetSubjectAttributes retrieves field names for OIDs that Go does not natively support
func GetSubjectAttributes(attributes []pkix.AttributeTypeAndValue) Subject {
var subjectAttributes Subject
for _, v := range attributes {
switch v.Type.String() {
case "1.2.840.113549.1.9.1":
subjectAttributes.EmailAddress = v.Value
case "0.9.2342.19200300.100.1.1":
subjectAttributes.UID = v.Value
case "2.5.4.41":
subjectAttributes.Name = v.Value
case "2.5.4.4":
subjectAttributes.Surname = v.Value
case "2.5.4.42":
subjectAttributes.GivenName = v.Value
case "2.5.4.43":
subjectAttributes.Initials = v.Value
case "2.5.4.44":
subjectAttributes.GenerationQualifier = v.Value
case "2.5.4.12":
subjectAttributes.Title = v.Value
case "2.5.4.65":
subjectAttributes.Pseudonym = v.Value
case "2.5.4.15":
subjectAttributes.BusinessCategory = v.Value
case "1.3.6.1.4.1.311.60.2.1.1":
subjectAttributes.JurisdictionLocality = v.Value
case "1.3.6.1.4.1.311.60.2.1.2":
subjectAttributes.JurisdictionStateOrProvince = v.Value
case "1.3.6.1.4.1.311.60.2.1.3":
subjectAttributes.JurisdictionCountry = v.Value
case "2.5.4.97":
subjectAttributes.OrganizationIdentifier = v.Value
case "2.5.4.46":
subjectAttributes.DNQualifier = v.Value
}
}
return subjectAttributes
}
// CertToJSON returns a Certificate struct created from a X509.Certificate
func CertToJSON(cert *x509.Certificate) Certificate {
var (
domain string
ip string
err error
certJson = Certificate{}
)
// initialize []string to never store them as null
certJson.IPs = make([]string, 0)
certJson.Version = cert.Version
// If there's an error, we just store the zero value ("")
serial, _ := GetHexASN1Serial(cert)
certJson.Serial = serial
certJson.SignatureAlgorithm = SignatureAlgorithm[cert.SignatureAlgorithm]
certJson.Key, err = getPublicKeyInfo(cert)
if err != nil {
log.Printf("Failed to retrieve public key information: %v. Continuing anyway.", err)
}
// Handle uncommon attributes for Issuer
certJson.Issuer = GetSubjectAttributes(cert.Issuer.Names)
// Handle Domain Components properly
certJson.Issuer.DomainComponent = GetDomainComponent(cert.Issuer.Names)
// Handle common attributes for Issuer
certJson.Issuer.Country = cert.Issuer.Country
certJson.Issuer.Organization = cert.Issuer.Organization
certJson.Issuer.OrganizationalUnit = cert.Issuer.OrganizationalUnit
certJson.Issuer.Locality = cert.Issuer.Locality
certJson.Issuer.StateOrProvince = cert.Issuer.Province
certJson.Issuer.StreetAddress = cert.Issuer.StreetAddress
certJson.Issuer.PostalCode = cert.Issuer.PostalCode
certJson.Issuer.SerialNumber = cert.Issuer.SerialNumber
certJson.Issuer.CommonName = cert.Issuer.CommonName
// Handle uncommon attributes for Subject
certJson.Subject = GetSubjectAttributes(cert.Subject.Names)
// Handle Domain Components properly
certJson.Subject.DomainComponent = GetDomainComponent(cert.Subject.Names)
// Handle common attributes for Subject
certJson.Subject.Country = cert.Subject.Country
certJson.Subject.Organization = cert.Subject.Organization
certJson.Subject.OrganizationalUnit = cert.Subject.OrganizationalUnit
certJson.Subject.Locality = cert.Subject.Locality
certJson.Subject.StateOrProvince = cert.Subject.Province
certJson.Subject.StreetAddress = cert.Subject.StreetAddress
certJson.Subject.PostalCode = cert.Subject.PostalCode
certJson.Subject.SerialNumber = cert.Subject.SerialNumber
certJson.Subject.CommonName = cert.Subject.CommonName
certJson.Validity.NotBefore = cert.NotBefore.UTC()
certJson.Validity.NotAfter = cert.NotAfter.UTC()
certJson.X509v3Extensions = getCertExtensions(cert)
certJson.MozillaPolicyV25 = getMozillaPolicyV25(cert)
certJson.MozillaPolicyV29 = getMozillaPolicyV29(cert)
//below check tries to hack around the basic constraints extension
//not being available in versions < 3.
//Only the IsCa variable is set, as setting X509v3BasicConstraints
//messes up the validation procedure.
if cert.Version < 3 {
certJson.CA = cert.IsCA
} else {
if cert.BasicConstraintsValid {
certJson.X509v3BasicConstraints = "Critical"
certJson.CA = cert.IsCA
} else {
certJson.X509v3BasicConstraints = ""
certJson.CA = false
}
}
t := time.Now().UTC()
certJson.FirstSeenTimestamp = t
certJson.LastSeenTimestamp = t
if !cert.IsCA {
certJson.ScanTarget = domain
certJson.IPs = append(certJson.IPs, ip)
}
certJson.Hashes.SHA1 = SHA1Hash(cert.Raw)
certJson.Hashes.SHA256 = SHA256Hash(cert.Raw)
certJson.Hashes.SPKISHA256 = SPKISHA256(cert)
certJson.Hashes.SubjectSPKISHA256 = SubjectSPKISHA256(cert)
certJson.Hashes.PKPSHA256 = PKPSHA256Hash(cert)
certJson.Raw = base64.StdEncoding.EncodeToString(cert.Raw)
return certJson
}