certViewer/cmd/web/certificates.go (537 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/. */
/* Some of the following code is adapted 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/slog"
"net"
"os"
"reflect"
"strconv"
"strings"
"time"
)
type Certificate struct {
Serial string
Version int
SignatureAlgorithm string
Issuer string
Validity Validity
Subject string
Key SubjectPublicKeyInfo
X509v3Extensions Extensions
X509v3BasicConstraints string
CA bool
Hashes Hashes
Raw string
}
type Hashes struct {
SHA1 string
SHA256 string
SPKISHA256 string
SubjectSPKISHA256 string
PKPSHA256 string
}
type Validity struct {
NotBefore string
NotAfter string
}
type Subject struct {
Country []string
Organization []string
OrganizationalUnit []string
CommonName string
Locality []string
StateOrProvince []string
StreetAddress []string
PostalCode []string
SerialNumber string
EmailAddress any
UID any
DomainComponent []string
Name any
Surname any
GivenName any
Initials any
GenerationQualifier any
Title any
Pseudonym any
BusinessCategory any
JurisdictionLocality any
JurisdictionStateOrProvince any
JurisdictionCountry any
OrganizationIdentifier any
DNQualifier any
}
type SubjectPublicKeyInfo struct {
Alg string
Size float64
Exponent float64
X string
Y string
P string
Q string
G string
Curve string
}
// Extensions that are already decoded in the x509 Certificate structure
type Extensions struct {
AuthorityKeyId string
SubjectKeyId string
KeyUsage string
ExtendedKeyUsage string
ExtendedKeyUsageOID string
SubjectAlternativeName []string
CRLDistributionPoints string
PolicyIdentifiers string
PermittedDNSDomains string
PermittedIPAddresses string
ExcludedDNSDomains string
ExcludedIPAddresses string
InhibitAnyPolicy *int
}
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
}
// 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 ""
}
}
// 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: strings.Join(getKeyUsages(cert), ", "),
ExtendedKeyUsage: strings.Join(getExtKeyUsages(cert), ", "),
ExtendedKeyUsageOID: strings.Join(getExtKeyUsageOIDs(cert), ", "),
PolicyIdentifiers: strings.Join(getPolicyIdentifiers(cert), ", "),
SubjectAlternativeName: san,
CRLDistributionPoints: strings.Join(crld, ", "),
PermittedDNSDomains: strings.Join(constraints.PermittedDNSDomains, ", "),
ExcludedDNSDomains: strings.Join(constraints.ExcludedDNSDomains, ", "),
PermittedIPAddresses: strings.Join(permittedIPAddresses, ", "),
ExcludedIPAddresses: strings.Join(excludedIPAddresses, ", "),
}
for _, v := range cert.Extensions {
if OIDFieldName(v.Id) == "InhibitAnyPolicy" {
value, _ := strconv.Atoi(string(v.Value))
ext.InhibitAnyPolicy = &value
}
}
return ext
}
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
}
// GetOIDAttributes retrieves field names for OIDs that Go does not natively support
func GetOIDAttributes(attributes []pkix.AttributeTypeAndValue) Subject {
var (
subjectAttributes Subject
domainComponents []string
)
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
case "0.9.2342.19200300.100.1.25":
domainComponents = append(domainComponents, v.Value.(string))
}
}
subjectAttributes.DomainComponent = domainComponents
return subjectAttributes
}
// GetAttributes takes a Subject struct and returns all fields in a comma-delimited string
func GetAttributes(attributes Subject) string {
var attr []string
if len(attributes.Country) > 0 {
attr = append(attr, "C="+strings.Join(attributes.Country, ", C="))
}
if len(attributes.Organization) > 0 {
attr = append(attr, "O="+strings.Join(attributes.Organization, ", O="))
}
if len(attributes.OrganizationalUnit) > 0 {
attr = append(attr, "OU="+strings.Join(attributes.OrganizationalUnit, ", OU="))
}
if len(attributes.Locality) > 0 {
attr = append(attr, "L="+strings.Join(attributes.Locality, ", L="))
}
if len(attributes.StateOrProvince) > 0 {
attr = append(attr, "ST="+strings.Join(attributes.StateOrProvince, ", ST="))
}
if len(attributes.StreetAddress) > 0 {
attr = append(attr, "streetAddress="+strings.Join(attributes.StreetAddress, ", streetAddress="))
}
if len(attributes.PostalCode) > 0 {
attr = append(attr, "postalCode="+strings.Join(attributes.PostalCode, ", postalCode="))
}
if len(attributes.SerialNumber) > 0 {
attr = append(attr, "SN="+attributes.SerialNumber)
}
if len(attributes.CommonName) > 0 {
attr = append(attr, "CN="+attributes.CommonName)
}
if attributes.EmailAddress != nil && len(attributes.EmailAddress.(string)) > 0 {
attr = append(attr, "emailAddress="+attributes.EmailAddress.(string))
}
if attributes.UID != nil && len(attributes.UID.(string)) > 0 {
attr = append(attr, "UID="+attributes.UID.(string))
}
if attributes.DomainComponent != nil && len(attributes.DomainComponent) > 0 {
attr = append(attr, "DC="+strings.Join(attributes.DomainComponent, ", DC="))
}
if attributes.Name != nil && len(attributes.Name.(string)) > 0 {
attr = append(attr, "name="+attributes.Name.(string))
}
if attributes.Surname != nil && len(attributes.Surname.(string)) > 0 {
attr = append(attr, "surname="+attributes.Surname.(string))
}
if attributes.GivenName != nil && len(attributes.GivenName.(string)) > 0 {
attr = append(attr, "givenName="+attributes.GivenName.(string))
}
if attributes.Initials != nil && len(attributes.Initials.(string)) > 0 {
attr = append(attr, "initials="+attributes.Initials.(string))
}
if attributes.GenerationQualifier != nil && len(attributes.GenerationQualifier.(string)) > 0 {
attr = append(attr, "generationQualifier="+attributes.GenerationQualifier.(string))
}
if attributes.Title != nil && len(attributes.Title.(string)) > 0 {
attr = append(attr, "title="+attributes.Title.(string))
}
if attributes.Pseudonym != nil && len(attributes.Pseudonym.(string)) > 0 {
attr = append(attr, "pseudonym="+attributes.Pseudonym.(string))
}
if attributes.BusinessCategory != nil && len(attributes.BusinessCategory.(string)) > 0 {
attr = append(attr, "businessCategory="+attributes.BusinessCategory.(string))
}
if attributes.JurisdictionLocality != nil && len(attributes.JurisdictionLocality.(string)) > 0 {
attr = append(attr, "jurisdictionLocality="+attributes.JurisdictionLocality.(string))
}
if attributes.JurisdictionStateOrProvince != nil && len(attributes.JurisdictionStateOrProvince.(string)) > 0 {
attr = append(attr, "jurisdictionStateOrProvince="+attributes.JurisdictionStateOrProvince.(string))
}
if attributes.JurisdictionCountry != nil && len(attributes.JurisdictionCountry.(string)) > 0 {
attr = append(attr, "jurisdictionCountry="+attributes.JurisdictionCountry.(string))
}
if attributes.OrganizationIdentifier != nil && len(attributes.OrganizationIdentifier.(string)) > 0 {
attr = append(attr, "organizationIdentifier="+attributes.OrganizationIdentifier.(string))
}
if attributes.DNQualifier != nil && len(attributes.DNQualifier.(string)) > 0 {
attr = append(attr, "dnQualifier="+attributes.DNQualifier.(string))
}
return strings.Join(attr, ", ")
}
// CertInfo returns a Certificate struct created from a X509.Certificate
func certInfo(cert *x509.Certificate) Certificate {
serial, err := GetHexASN1Serial(cert)
if err != nil {
slog.Error("Unable to retrieve ASN1 serial", "error", err.Error())
}
certRead := Certificate{
Version: cert.Version,
Serial: serial,
Validity: Validity{
NotBefore: cert.NotBefore.UTC().Format(time.RFC3339),
NotAfter: cert.NotAfter.UTC().Format(time.RFC3339),
},
SignatureAlgorithm: SignatureAlgorithm[cert.SignatureAlgorithm],
Hashes: Hashes{
SHA1: SHA1Hash(cert.Raw),
SHA256: SHA256Hash(cert.Raw),
SPKISHA256: SPKISHA256(cert),
SubjectSPKISHA256: SubjectSPKISHA256(cert),
PKPSHA256: PKPSHA256Hash(cert),
},
CA: cert.IsCA,
Raw: base64.StdEncoding.EncodeToString(cert.Raw),
}
certRead.Key, err = getPublicKeyInfo(cert)
if err != nil {
slog.Error("Failed to retrieve public key information", "error", err.Error())
}
// Handle common attributes for Issuer
var commonIssuerAttributes Subject
commonIssuerAttributes.Country = cert.Issuer.Country
commonIssuerAttributes.Organization = cert.Issuer.Organization
commonIssuerAttributes.OrganizationalUnit = cert.Issuer.OrganizationalUnit
commonIssuerAttributes.Locality = cert.Issuer.Locality
commonIssuerAttributes.StateOrProvince = cert.Issuer.Province
commonIssuerAttributes.StreetAddress = cert.Issuer.StreetAddress
commonIssuerAttributes.PostalCode = cert.Issuer.PostalCode
commonIssuerAttributes.SerialNumber = cert.Issuer.SerialNumber
commonIssuerAttributes.CommonName = cert.Issuer.CommonName
// Handle uncommon attributes for Issuer
uncommonIssuerAttributes := GetOIDAttributes(cert.Issuer.Names)
// Format all Issuer attributes into one string
// If uncommon attributes are empty, only return common... otherwise we get a trailing comma
if reflect.DeepEqual(uncommonIssuerAttributes, Subject{}) {
certRead.Issuer = GetAttributes(commonIssuerAttributes)
} else {
certRead.Issuer = strings.Join([]string{GetAttributes(commonIssuerAttributes), GetAttributes(uncommonIssuerAttributes)}, ", ")
}
// Handle common attributes for Subject
var commonSubjectAttributes Subject
commonSubjectAttributes.Country = cert.Subject.Country
commonSubjectAttributes.Organization = cert.Subject.Organization
commonSubjectAttributes.OrganizationalUnit = cert.Subject.OrganizationalUnit
commonSubjectAttributes.Locality = cert.Subject.Locality
commonSubjectAttributes.StateOrProvince = cert.Subject.Province
commonSubjectAttributes.StreetAddress = cert.Subject.StreetAddress
commonSubjectAttributes.PostalCode = cert.Subject.PostalCode
commonSubjectAttributes.SerialNumber = cert.Subject.SerialNumber
commonSubjectAttributes.CommonName = cert.Subject.CommonName
// Handle uncommon attributes for Subject
uncommonSubjectAttributes := GetOIDAttributes(cert.Subject.Names)
// Format all Subject attributes into one string
// If uncommon attributes are empty, only return common... otherwise we get a trailing comma
if reflect.DeepEqual(uncommonSubjectAttributes, Subject{}) {
certRead.Subject = GetAttributes(commonSubjectAttributes)
} else {
certRead.Subject = strings.Join([]string{GetAttributes(commonSubjectAttributes), GetAttributes(uncommonSubjectAttributes)}, ", ")
}
certRead.X509v3Extensions = getCertExtensions(cert)
return certRead
}
// certCleanup removes the cert submitted after ev-checker runs to keep things tidy
func (app *application) certCleanup(pemFile string) {
err := os.RemoveAll(pemFile)
if err != nil {
app.logger.Error("Unable to delete PEM files or directories", "Error", err.Error())
} else {
app.logger.Info("Removed unused PEM file", "File", pemFile)
}
}