Updated 2008-07-29 23:27:32 by skrebs

An small viewer for X509v3 Certificates as they are used by SSL. It is an example how to use the ASN.1 BER/DER decoder package from Tcllib. The code is far from complete and the OID to name maps are especially incomplete.

This code parses only a subset of the Certificates specified in RFC 3280.
    #
    # X.509v3 Certificate Viewer
    #
    # (c) 2004 Michael Schlenker <mic42@users.sourcerforge.net>
    #
    # License: like tcllib
    #
    #
    # This implements an incomplete certificate viewer for
    # X.509v3 Certificates stored in the PEM Format used by openssl.
    #
    #
    #

    package require base64
    package require asn
    package require textutil

    #
    # Some (incomplete) OID -> Name mappings from RFC 3279, 3280 and the X.500
    #
    #
    # attribute names under OID 2.5.4.x
    set attributeType {
		  objectClass
		  aliasedEntryName
		  knowledgeInformation
		  commonName
		  surname
		  serialNumber
		  countryName
		  localityName
		  stateOrProvinceName
		  streetAddress
		  organizationName
		  organizationUnitName
		  title
		  description
		  searchGuide
		  businessCategory
		  postalAddress
		  postalCode
		  postOfficeBox
		  physicalDeliveryOfficeName
		  telephoneNumber
		  telexNumber
		  teletexTerminalIdentifier
		  facsimileTelephoneNumber
		  x121Address
		  internationalISDNNumber
		  registeredAddress
		  destinationIndicator
		  preferredDeliveryMethod
		  presentationAddress
		  supportedApplicationContext
		  member
		  owner
		  roleOccupant
		  seeAlso
		  userPassword
		  userCertificate
		  cAcertificate
		  authorityRevocationList
		  certificateRevocationList
		  crossCertificatePair
		  name
		  givenName
		  initials
		  generationQualifier
		  uniqueIdentifier
		  dnQualifier
		  enhancedSearchGuide
		  protocolInformation
		  distinguishedName
		  uniqueMember
		  houseIdentifier
		  supportedAlgorithms
		  deltaRevocationList
		  dmdName
		  clearance
		  defaultDirQop
		  attributeIntegrityInfo
		  attributeCertificate
		  attributeCertificateRevocationList
		  confKeyInfo
		  aACertificate
		  attributeDescriptorCertificate
		  attributeAuthorityRevocationList
		  family-information
		  pseudonym
		  communicationsService
		  communicationsNetwork
		  certificationPracticeStmt
		  certificatePolicy
		  pkiPath
		  privPolicy
		  role
    }

    array set algorithms {
	{1 2 840 113549 2} md2
	{1 2 840 113549 5} md5
	{1 3 14 3 2 26} sha1
	{1 2 840 10040 4 1} dsa
	{1 2 840 10040 4 3} dsa-with-sha1
	{1 2 840 113549 1 1} "pkcs-1 oid arc"
	{1 2 840 113549 1 1 1} rsa
	{1 2 840 113549 1 1 2} md2-with-rsa
	{1 2 840 113549 1 1 4} md5-with-rsa
	{1 2 840 113549 1 1 5} sha1-with-rsa
	{1 2 840 10046 2 1} Diffie-Hellman
    }

    #####################################################################
    #
    # Actual Parsing Code starts here
    #
    #####################################################################

    proc load_cert {filename} {

	set fd [open $filename]
	set data [read $fd]
	close $fd

	set lines [split $data \n]
	set hlines 0
	set total 0
	set headers ""
	set head_banner ""
	set foot_banner ""
	set first 0
	foreach line $lines {
	    incr total
	    if {[regexp {^-----(.*?)-----$} $line -> banner]} {
		if {$first} {
		    set foot_banner $banner
		    incr total -1
		    break
		} else {
		    set first 1
		    set head_banner $banner
		    incr hlines
		}
	    }
	    if {[regexp {^(.*):(.*)$} $line -> tag value]} {
		lappend headers [list $tag $value]
		incr hlines
	    }
	}
	set block [join [lrange $lines $hlines [expr {$total-1}]]]
	set asnblock [base64::decode $block]
	return [list $head_banner $headers $asnblock $foot_banner]
    }

    proc parse_cert {asnblock} {
	puts "Parsing Certificate..."
	asn::asnGetSequence asnblock certificate
	asn::asnGetSequence certificate TBSCertificate
	parse_TBSCertificate $TBSCertificate

	asn::asnGetSequence certificate AlgorithmIdentifier
	parse_AlgorithmIdentifier $AlgorithmIdentifier
	asn::asnGetBitString certificate Signature
	set len [string length $Signature]
	if {($len % 8)==0} {
	    binary scan [binary format B* $Signature] H* sig
	    puts "Signature ($len bits):"
	    puts [textutil::adjust $sig -strictlength 1 -length 67]"
	} else {
	    puts "Signature ($len bits):"
	    puts [textutil::adjust $Signature -strictlength 1 -length 67]
	}
    }

    proc parse_AlgorithmIdentifier {asnblock} {
	asn::asnGetObjectIdentifier asnblock oid
	set name [map_oid_to_name $oid]
	# skipping the parameter for now
	return [list $name $oid]
    }

    proc parse_TBSCertificate {asnblock} {
	puts [string repeat - 72 ]
	set check [string index $asnblock 0]
	if {![catch {asn::asnGetContext check tag}]} {
	    asn::asnGetContext asnblock context
	    asn::asnGetInteger asnblock version
	} else {
	    set version 0
	}
	set versionID [list v1 v2 v3]
	puts [format "Certificate Version	    :\t%s" [lindex $versionID $version]]
	# homegrown getInteger for the 20octet ints in serial number
	asn::asnGetByte asnblock tag
	asn::asnGetLength asnblock len
	asn::asnGetBytes asnblock $len ser
	binary scan $ser H* serialNumber
	puts [format "Certificate Serial Number      :\t%s" $serialNumber]
	asn::asnGetSequence asnblock Algorithm
	foreach {name oid} [parse_AlgorithmIdentifier $Algorithm] {break}
	puts [format "Certificate Signature Algorithm:\t%s <urn:oid:%s>" $name [join $oid .]]
	startbanner Issuer
	asn::asnGetSequence asnblock Issuer
	parse_RelativeDistinguishedName $Issuer
	endbanner
	asn::asnGetSequence asnblock Validity
	parse_Validity $Validity
	startbanner Subject
	asn::asnGetSequence asnblock Subject
	parse_RelativeDistinguishedName $Subject
	endbanner
	asn::asnGetSequence asnblock SubjectPublicKeyInfo
	parse_SubjectPublicKeyInfo $SubjectPublicKeyInfo
	while {[string length $asnblock]} {
	    asn::asnGetContext asnblock context
	    switch -exact -- $context {
		0 { error "Context Tag for Version Number found in wrong place"}
		1 {
		    puts "Issuer Unique Identifier:"
		    asn::asnGetBitstring asnblock IssuerUniqueIdentifier
		    puts $IssuerUniqueIdentifier
		}
		2 {
		    puts "Subject Unique Identifier:"
		    asn::asnGetBitstring asnblock SubjectUniqueIdentifier
		    puts $SubjectUniqueIdentifier
		}
		3 {
		    asn::asnGetSequence asnblock Extension
		    parse_Extension $Extension

		}
	    }
	}

    }

    proc parse_Extension {asnblock} {
	startbanner Extension
	asn::asnGetSequence asnblock extension
	asn::asnGetObjectIdentifier extension oid
	puts "Extension OID: $oid"
	binary scan [string index $extension 0] c c
	if {$c == 0x01} {
	    asn::asnGetBoolean extension critical
	    puts "Is Critical: [expr {$critical ? "yes" : "no"}]"
	}
	asn::asnGetOctetString extension value
	puts "Extension Value:"
	binary scan $value H* bin
	puts $bin
	endbanner
    }

    proc parse_SubjectPublicKeyInfo {asnblock} {
	startbanner "SubjectPublicKey"
	asn::asnGetSequence asnblock Algorithm
	foreach {name oid} [parse_AlgorithmIdentifier $Algorithm] {break}
	puts [format "PublicKey Algorithm:\t%s <urn:oid:%s>" $name [join $oid .]]

	asn::asnGetBitString asnblock publicKey
	set len [string length $publicKey]
	if {($len % 8)==0} {
	    binary scan [binary format B* $publicKey] H* key
	    puts "Key ($len bits):"
	    puts [textutil::adjust $key -strictlength 1 -length 67 ]
	} else {
	    puts "Key ($len bits):"
	    puts [textutil::adjust $publicKey -strictlength 1 -length 67]
	}
	endbanner

    }

    proc parse_Validity {asnblock} {
	startbanner Validity
	asn::asnGetUTCTime asnblock notBefore
	puts "Valid not before: $notBefore"
	asn::asnGetUTCTime asnblock notAfter
	puts "Valid not after:  $notAfter"
	endbanner
    }

    proc parse_RelativeDistinguishedName {asnblock} {
	while {[string length $asnblock]} {
	    asn::asnGetSet asnblock AttributeValueAssertion
	    asn::asnGetSequence AttributeValueAssertion valblock
	    asn::asnGetObjectIdentifier valblock oid
	    set name [map_oid_to_name $oid]
	    set poid [join $oid .]
	    set tagbyte [string index $valblock 0]
	    binary scan $tagbyte c* dectag
	    set value ""
	    if {$dectag == 19} {
		asn::asnGetPrintableString valblock value
	    }
	    puts "$name <urn:oid:$poid> => $value"
	}
    }

    #################################################################################
    #
    # Extra ASN.1 BER/DER Decoders not yet supported in tcllib asn v0.1
    #
    #################################################################################

    proc asn::asnGetBoolean {data_var bool_var} {
	upvar $data_var data $bool_var bool

	asnGetByte data tag
	if {$tag != 0x01} {
	    binary scan $tag H2 tag_hex
	    return -code error "Expected Boolean (0x01), but got $tag_hex"
	}
	asnGetLength data length
	asnGetByte data byte
	set bool [expr {$byte == 0 ? 0 : 1}]
    }

    proc asn::asnGetUTCTime {data_var utc_var} {
	upvar $data_var data $utc_var utc

	asnGetByte data tag
	if {$tag != 0x17} {
	    binary scan $tag H2 tag_hex
	    return -code error "Expected UTCTime (0x17), but got $tag_hex"
	}
	asnGetLength data length
	asnGetBytes data $length bytes
	set bytes [encoding convertfrom ascii $bytes]
	binary scan $bytes a* utc

    }

    proc asn::asnGetBitString {data_var bitstring_var} {
	upvar $data_var data $bitstring_var bitstring

	asnGetByte data tag
	if {$tag != 0x03} {
	    binary scan $tag H2 tag_hex
	    return -code error "Expected Bit String (0x03), but got $tag_hex"
	}
	asnGetLength data length
	asnGetByte data offset
	incr length -1
	asnGetBytes data $length bytes
	binary scan $bytes B* bits
	set bits [string range $bits 0 end-$offset]
	set bitstring $bits
    }

    proc asn::asnGetObjectIdentifier {data_var oid_var} {
	  upvar $data_var data $oid_var oid

	  asnGetByte data tag
	  if {$tag != 0x06} {
	    binary scan $tag H2 tag_hex
	    return -code error "Expected Object Identifier (0x06), but got $tag_hex"
	  }
	  asnGetLength data length
	  asnGetByte data val
	  set oid [expr {$val / 40}]
	  lappend oid [expr {$val % 40}]
	  incr length -1
	  set bytes [list]
	  set incomplete 0
	  while {$length} {
	    asnGetByte data octet
	    incr length -1
	    if {$octet < 128} {
		set oidval $octet
		set mult 128
		foreach byte $bytes {
		    if {$byte != {}} {
		    incr oidval [expr {$mult*$byte}]
		    set mult [expr {$mult*128}]
		    }
		}
		lappend oid $oidval
		set bytes [list]
		set incomplete 0
	    } else {
		set byte [expr {$octet-128}]
		set bytes [concat [list $byte] $bytes]
		set incomplete 1
	    }
	  }
	  if {$incomplete} {
	    error "OID Data is incomplete, not enough octets."
	  }
    }

    proc ::asn::asnGetContext {data_var contextNumber_var} {
	upvar $data_var data $contextNumber_var contextNumber

	asnGetByte   data tag
	asnGetLength data length

	if {($tag & 0xE0) != 0x0A0} {
	    binary scan $tag H2 tag_hex
	    return -code error "Expected Context (0xa0), but got $tag_hex"
	}
	set contextNumber [expr {$tag & 0x1F}]
	return
    }

    proc ::asn::asnGetPrintableString {data_var print_var} {
	upvar $data_var data $print_var print

	asnGetByte data tag
	if {$tag != 0x13} {
	    binary scan $tag H2 tag_hex
	    return -code error "Expected Printable String (0x13), but got $tag_hex"
	}
	asnGetLength data length
	asnGetBytes data $length string
	set print [encoding convertfrom ascii $string]
    }

    ###########################################################################
    #
    # Some OID support routines
    #
    ###########################################################################

    proc is_oid_prefix {oid1 oid2} {
	foreach key1 $oid1 key2 [lrange $oid2 0 [expr {[llength $oid1]-1}]] {
	    if {$key1 != $key2} {return 0}
	    if {$key1 == ""} {return 1}
	}
	return 1
    }

    proc map_oid_to_name {oid} {
	global attributeType algorithms
	set name "unknown OID"
	if {[is_oid_prefix {2 5 4} $oid]} {
	    set key [lindex $oid 3]
	    set name [lindex $attributeType $key]
	}
	if {[is_oid_prefix {2 5 29 14} $oid]} {
	}
	if {[is_oid_prefix {1 2} $oid]} {
	    if {[info exists algorithms($oid)]} {
		set name $algorithms($oid)
	    }
	}
	return $name
    }

    proc startbanner {text} {
	set len [expr {(70-[string length $text])/2}]
	puts "[string repeat = $len] $text \
	      [string repeat = [expr {69-$len-[string length $text]}]]"
    }

    proc endbanner {} {
	puts [string repeat = 72]
    }

    # Main code
    #
    #
    #
    #
    set file [lindex $argv 0]
    puts "Loading and parsing $file"
    foreach {header headers asndata footer} [load_cert $file] {break}
    puts "Header: $header"
    foreach h $headers {
	puts "Header lines: $h"
    }
    puts "[string length $asndata] octets of ASN.1 Data"
    puts "Footer: $footer"
    puts "[string repeat - 72]"
    parse_cert $asndata

Usage:
 cert.tcl example.pem

(an example .pem file can be found in the tcllib examples dir, with the smtpd examples)

SV (2006-01-19) Produces an error with 8.4.5 on WinXP (ASN 0.1 or ASN 0.5)
	Loading and parsing G:\cvsroot\tcllib\examples\smtpd\server-public.pem
	Header: BEGIN CERTIFICATE
	942 octets of ASN.1 Data
	Footer: END CERTIFICATE
	----------------------------------------------------------------------
	Parsing Certificate...
	----------------------------------------------------------------------
	Certificate Version	    :	v1
	Certificate Serial Number      :	020102
	Expected Sequence (0x30), but got 02
	  while executing
	"asn::asnGetSequence asnblock Algorithm"
	  (procedure "parse_TBSCertificate" line 18)
	  invoked from within
	"parse_TBSCertificate $TBSCertificate"
	  (procedure "parse_cert" line 5)
	  invoked from within
	"parse_cert $asndata"
	  (file "H:\tcl_sources\X509v3_Cert_Viewer.tcl" line 493)

schlenk (2006-01-19) I don't have an 8.4.5 installed currently, but i will take a look. I'm currently updating and expanding the asn module, to make examples like this much shorter and easier.

anonymous (2006-08-30) same problem under 8.4.13 (activestate win32 tcllib 1.8)

CL observes the same symptom with Tcl 8.4.4, tcllib 1.8-1, under Debian Linux.

schlenk I hope i find the time to rewrite the example for the new tcllib release this fall, so it can be included with in the example dir.

This section breaks it.
	if {![catch {asn::asnGetContext check tag}]} {
	    asn::asnGetContext asnblock context
	    asn::asnGetInteger asnblock version
	} else {
	    set version 0
	}

Just use
	asn::asnGetContext asnblock context
	asn::asnGetInteger asnblock version