go

How to parse IDP extension information in a CRL using Go


I wrote some code in Go to parse the IDP extension information in a CRL, but I’m encountering an error while parsing the DistributionPoint. I’ve tried multiple times, but I can’t get it to work. Can you help me figure out what is wrong with this code?

package main

import (
    "crypto/x509"
    "crypto/x509/pkix"
    "encoding/asn1"
    "encoding/hex"
    "flag"
    "fmt"
    "os"
    "strings"
)

type IssuingDistributionPoint struct {
    DistributionPoint          asn1.RawValue  `asn1:"optional,tag:0,explicit"`
    OnlyContainsUserCerts      bool           `asn1:"optional,tag:1"`
    OnlyContainsCACerts        bool           `asn1:"optional,tag:2"`
    OnlySomeReasons            asn1.BitString `asn1:"optional,tag:3"`
    IndirectCRL                bool           `asn1:"optional,tag:4"`
    OnlyContainsAttributeCerts bool           `asn1:"optional,tag:5"`
}

var oidMap = map[string]string{
    "2.5.4.3":  "CN",
    "2.5.4.6":  "C",
    "2.5.4.10": "O",
    "2.5.4.11": "OU",
    "1.2.840.113549.1.9.1":       "E",
    "0.9.2342.19200300.100.1.25": "DC",
    "0.9.2342.19200300.100.1.1":  "UID",
}

func parseRDN(data []byte) (string, error) {
    var builder strings.Builder
    var raw asn1.RawValue
    if _, err := asn1.Unmarshal(data, &raw); err != nil {
        return "", fmt.Errorf("initial parsing failed: %v", err)
    }
    if raw.Tag == asn1.TagSequence {
        type rdnSequence []pkix.AttributeTypeAndValue
        var rdns rdnSequence
        if _, err := asn1.Unmarshal(raw.FullBytes, &rdns); err != nil {
            return "", fmt.Errorf("failed to parse rdnSequence: %v", err)
        }

        for i, rdn := range rdns {
            builder.WriteString(fmt.Sprintf("RDN[%d]:\n", i+1))
            oid := rdn.Type.String()
            name := oidMap[oid]
            if name == "" {
                name = oid
            }

            value, err := decodeAttributeValue(rdn.Value)
            if err != nil {
                return "", fmt.Errorf("failed to decode attribute value: %v", err)
            }

            if name == "DC" {
                if decoded, err := decodeDomainComponent(value); err == nil {
                    value = decoded
                }
            }

            builder.WriteString(fmt.Sprintf("  Type:%-8s Value:%s\n", name, value))
        }
        return builder.String(), nil
    }

    return "", fmt.Errorf("unexpected tag type: %d", raw.Tag)
}

func decodeAttributeValue(raw interface{}) (string, error) {
    switch v := raw.(type) {
    case string:
        return v, nil
    case []byte:
        if isPrintable(string(v)) {
            return string(v), nil
        }
        return fmt.Sprintf("#%X", v), nil
    default:
        return fmt.Sprintf("%v", v), nil
    }
}

func decodeDomainComponent(value string) (string, error) {
    if strings.HasPrefix(value, "#") {
        decoded, err := hex.DecodeString(value[1:])
        if err != nil {
            return "", fmt.Errorf("HEX decoding failed: %v", err)
        }
        return string(decoded), nil
    }
    return value, nil
}

func isPrintable(s string) bool {
    for _, r := range s {
        if r < 32 || r > 126 {
            return false
        }
    }
    return true
}

func parseGeneralName(gn asn1.RawValue) (interface{}, error) {
    if gn.Class == asn1.ClassContextSpecific && gn.Tag == 1 {
        return parseRDN(gn.FullBytes)
    }

    if gn.Tag == asn1.TagSequence {
        var names []asn1.RawValue
        if _, err := asn1.Unmarshal(gn.Bytes, &names); err != nil {
            return nil, fmt.Errorf("failed to decode GeneralNames: %v", err)
        }

        var results []string
        for _, name := range names {
            res, err := parseGeneralName(name)
            if err != nil {
                return nil, err
            }
            results = append(results, fmt.Sprintf("%v", res))
        }
        return strings.Join(results, ", "), nil
    }

    if gn.Class == asn1.ClassContextSpecific {
        switch gn.Tag {
        case 0, 2, 6: 
            return string(gn.Bytes), nil
        }
    }

    return nil, fmt.Errorf("unsupported GeneralName type: tag=%d class=%d", gn.Tag, gn.Class)
}

func main() {
    crlFilePath := flag.String("crl", "", "Path to the CRL file")
    flag.Parse()

    if *crlFilePath == "" {
        fmt.Println("CRL file path must be provided")
        os.Exit(1)
    }

    derBytes, err := os.ReadFile(*crlFilePath)
    if err != nil {
        fmt.Printf("Failed to read file: %v\n", err)
        os.Exit(1)
    }

    crl, err := x509.ParseRevocationList(derBytes)
    if err != nil {
        fmt.Printf("Failed to parse CRL: %v\n", err)
        os.Exit(1)
    }

    oidIssuingDistributionPoint := asn1.ObjectIdentifier{2, 5, 29, 28}

    for _, ext := range crl.Extensions {
        if ext.Id.Equal(oidIssuingDistributionPoint) {
            var idp IssuingDistributionPoint
            if _, err := asn1.Unmarshal(ext.Value, &idp); err != nil {
                fmt.Printf("Failed to decode IDP extension: %v\n", err)
                continue
            }

            fmt.Printf("IDP Extension Flags:\n")
            fmt.Printf(" Only Contains User Certs: %t\n", idp.OnlyContainsUserCerts)
            fmt.Printf(" Only Contains CA Certs: %t\n", idp.OnlyContainsCACerts)
            fmt.Printf(" Indirect CRL: %t\n", idp.IndirectCRL)

            if len(idp.DistributionPoint.Bytes) > 0 {
                var dpName asn1.RawValue
                if _, err := asn1.Unmarshal(idp.DistributionPoint.Bytes, &dpName); err != nil {
                    fmt.Printf("Failed to unpack DistributionPointName: %v\n", err)
                    continue
                }

                if dpName.Class == asn1.ClassContextSpecific {
                    switch dpName.Tag {
                    case 0: // fullName
                        fmt.Println("Distribution Point Type: fullName")
                        var generalNames []asn1.RawValue
                        if _, err := asn1.Unmarshal(dpName.Bytes, &generalNames); err != nil {
                            fmt.Printf("Failed to parse GeneralNames: %v\n", err)
                            continue
                        }

                        for i, gn := range generalNames {
                            result, err := parseGeneralName(gn)
                            if err != nil {
                                fmt.Printf("[Entry %d] Parsing error: %v\n", i+1, err)
                                continue
                            }
                            fmt.Printf("[Entry %d] Distribution Point: %s\n", i+1, result)
                        }

                    case 1: // nameRelativeToCRLIssuer
                        fmt.Println("Distribution Point Type: nameRelativeToCRLIssuer")
                        result, err := parseRDN(dpName.Bytes)
                        if err != nil {
                            fmt.Printf("Failed to parse RDN: %v\n", err)
                            continue
                        }
                        fmt.Println(result)

                    default:
                        fmt.Printf("Unknown distribution point tag: %d\n", dpName.Tag)
                    }
                }
            }
        }
    }
}

error:

   IDP Extension Flags:
   Only Contains User Certs: false
   Only Contains CA Certs: false
   Indirect CRL: false
   Distribution Point Type: nameRelativeToCRLIssuer
   Failed to parse RDN: failed to parse rdnSequence: asn1: structure error: sequence tag 
   mismatch

PEM:

-----BEGIN X509 CRL-----
MIICnTCCAYUCAQEwDQYJKoZIhvcNAQELBQAwTjELMAkGA1UEBhMCVVMxCzAJBgNV
BAgMAlVTMQswCQYDVQQHDAJVUzELMAkGA1UECgwCVVMxCzAJBgNVBAMMAlVTMQsw
CQYDVQQLDAJVUxcNMjUwMTAxMDAwMDAwWhcNMjUxMjAxMDAwMDAwWjA1MDMCFByA
Ai74HyQF7pamEty2H+CscB5eFw0yNTAzMjcwMjUxMTBaMAwwCgYDVR0VBAMKAQag
gcswgcgwgasGA1UdHAEB/wSBoDCBnaCBmqGBlzAJBgNVBAYTAkNOMAkGA1UEChMC
Q0EwCgYDVQQDEwNDUkwwEQYKCZImiZPyLGQBGQwDY29tMBQGA1UECxMNSVQgRGVw
YXJ0bWVudDAUBgoJkiaJk/IsZAEBDAZib2IxMjMwFQYKCZImiZPyLGQBGQwHZXhh
bXBsZTAdBgkqhkiG9w0BCQEWEHVzZXJAZXhhbXBsZS5jb20wGAYDVR0UBBECDxnP
/97adO3y9qRGDM7hQDANBgkqhkiG9w0BAQsFAAOCAQEApPcq43Py08J9wMWTXIQT
Q3E30ACBzEW2E+3HZ5818Z/FK7+YYV4umPZ5JQVINqYoTbpRoBrdh8VJyJ2U/B1u
9NDgtMRv7gVHad+uy3ciRG+nvOa9JP4a0a3GMN5nhWIykghH9LBYOL48WCP6r3pO
t8inbw7bSB25HSDIuHHeuChchDOgv926MFmYTphaFY7h6sFRDjHVSSFJEicSRx/t
0OJ8mtfwBqWLw9725u4A5b08FGAfSV0UBE25QqqpE/W5Vt4tDmDfR8idEsQyRbCf
qETKAFd0a7J5MiI4MNj3CaUWUbsq1kZsFfSRFqPJyoiXlUm8jz6n/8eLrV3rDLiS
bg==
-----END X509 CRL-----

Solution

  • Reference: RFC 5280 https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.13

    Conforming CAs SHOULD NOT use nameRelativeToCRLIssuer to specify distribution point names.

    The DistributionPointName MUST NOT use the nameRelativeToCRLIssuer alternative when cRLIssuer contains more than one distinguished name.

    My feeling is that your crl has : DistributionPointName ::= nameRelativeToCRLIssuer: "Name"

    ASN.1 encoding needs

     SEQUENCE {
          type = OID (e.g. commonName)
          value = UTF8String (e.g. "CRL")
        }
    

    If using OpenSSL config use fullName instead of nameRelativeToCRLIssuer