Skip to content

Instantly share code, notes, and snippets.

@zmajstor
Last active August 29, 2015 14:05
Show Gist options
  • Save zmajstor/8fe69dd2f92e749268d9 to your computer and use it in GitHub Desktop.
Save zmajstor/8fe69dd2f92e749268d9 to your computer and use it in GitHub Desktop.
improved extraction and validation of the subjectAltName X509 extension; ASN1 decode for subjectAltName extension containing funny msUPN from Microsoft PKI (not decoded by out-of-the-box ruby OpenSSL library)
-----BEGIN CERTIFICATE-----
MIIExDCCA6ygAwIBAgITUgAACqTdV8IeZ2qIFgAAAAAKpDANBgkqhkiG9w0BAQUF
ADBJMRMwEQYKCZImiZPyLGQBGRYDbmV0MRYwFAYKCZImiZPyLGQBGRYGcHJvbWRt
MRowGAYDVQQDExFQcm9tZG1ORVRSb290Q0F2MTAeFw0xNDA4MTUxODIwNDBaFw0x
NTA4MTUxODIwNDBaMB0xGzAZBgNVBAMMElpvcmFuIE1hanN0b3JvdmnEhzCCASIw
DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAM2z4QYHjTHNbSkl4UFF1E1X5zgG
TdVX5b6gpYjwlTpBNbFDNwAIePjJHZ/IMwpFpJzHU/G9chGx67D+RsfCkd1LHdnR
P0WOLKXsCImZIneAfvMt8gXDdeHzcXtNapgoIfjnAAZs4UN9Lxvd4KN2UpsSYKu+
jhRrmDE3vtedMLK7l1pnKSK6HCjch6zdnF9JytsdApGjcwKm4ubr/0y17XQvcrzl
21C0NRE77am/nR/3Jd3/qZjA7OT99KCjTx3OYXqZvU0s/4IEEBIyAWWFkxFQHlsE
oTid2CquLycK5Nmi1mqV4IbLnA60Jb1jXteX03WM96dk98NobRrT5jCgOvsCAwEA
AaOCAc8wggHLMA4GA1UdDwEB/wQEAwIFoDA3BgNVHREEMDAugQ16bUBwcm9tZG0u
Y29toB0GCisGAQQBgjcUAgOgDwwNem1AcHJvbWRtLm5ldDAdBgNVHQ4EFgQUhz4n
3F4HHGwNekWp9j9hbMwdiM4wHwYDVR0jBBgwFoAUOkBhIRg6S6cooasjqbukaO4M
ErkwTAYDVR0fBEUwQzBBoD+gPYY7aHR0cDovL2Nsb3VkcGtpLnByb21kbS5uZXQv
Q2VydEVucm9sbC9Qcm9tZG1ORVRSb290Q0F2MS5jcmwwawYIKwYBBQUHAQEEXzBd
MFsGCCsGAQUFBzAChk9odHRwOi8vY2xvdWRwa2kucHJvbWRtLm5ldC9DZXJ0RW5y
b2xsL2Nsb3VkcGtpLnByb21kbS5uZXRfUHJvbWRtTkVUUm9vdENBdjEuY3J0MD0G
CSsGAQQBgjcVBwQwMC4GJisGAQQBgjcVCIaUvg2Bi6h/hL2LJ4e21l+C9aIfCIbq
iRmHpuIVAgFkAgEMMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcDATAnBgkr
BgEEAYI3FQoEGjAYMAoGCCsGAQUFBwMCMAoGCCsGAQUFBwMBMA0GCSqGSIb3DQEB
BQUAA4IBAQA9WtQnIZiI0Ysra4USEnqcC/29fFJlU7TCFCHV2NjiqWoQeff28O7x
Wq+i32/HzAObCUUZXZbOyz0AypMEiIctllrj0Ckhj66yrmc4h+SQJP9i4K0p60M5
Yi3LD4fxweVjqv+lMPTmr4s9tc+WOqR5Egjx2ith8mkSolRB2OI307AuLUsBt9ll
km/XM1zptOADm8euzQ3fiaSt1kvj8r0FnqkJMYUzwbTcGovNUz9af9u3gZckQin9
73JcW91VKLG8gCL0y7xo+Byv4/UoDiI17biEzkCtgKetk4d4hH68FcmK1DURS8E2
i2+daiavtmzeCXjH/4EbwJVUGcYE5PuR
-----END CERTIFICATE-----
require 'openssl'
# improved extraction and validation of the subjectAltName X509 extension
# Universal Principal Name (UPN)
# Widely deployed Microsoft OtherName name form (OID 1.3.6.1.4.1.311.20.2.3)
# Format: user@domain
# Encoding: UTF-8
# Matching: case ignore
# OtherName, unsupported in current implementation:
# san = cert.extensions.find { |ext| ext.oid == 'subjectAltName' }
# san.to_h.inspect # =>
# before:
# {"oid"=>"subjectAltName", "value"=>"email:[email protected], othername:<unsupported>", "critical"=>false}
# after:
# {"oid"=>"subjectAltName", "value"=>"email:[email protected], othername:<unsupported>", "critical"=>false, "Email"=>"[email protected]", "OtherName"=>"[email protected]"}
module OpenSSL
module X509
UPN_OID = '1.3.6.1.4.1.311.20.2.3'
UPN_TAG, EMAIL_TAG = 0, 1
class Certificate
def verify_san_upn(upn)
/\A#{san_hash['OtherName']}\z/i =~ upn
end
def verify_san_email(email)
/\A#{san_hash['Email']}\z/i =~ email
end
def san_hash
subject_alt_name.to_h if subject_alt_name
end
def subject_alt_name
@subject_alt_name ||= self.extensions.find { |e| 'subjectAltName' == e.oid }
end
end
class Extension
def to_h # {"oid"=>sn|ln, "value"=>value, "critical"=>true|false}
# {"oid"=>self.oid,"value"=>self.value,"critical"=>self.critical?}
san_values = {}
if self.oid == "subjectAltName"
ostr = OpenSSL::ASN1.decode(self.to_der).value.last
sequence = OpenSSL::ASN1.decode(ostr.value)
sequence.value.each do |san|
case san.tag
when 0 # UPN_TAG
san_values["OtherName"] = san.value.last.value.first.value if san.value.first.oid == UPN_OID
when 1 # EMAIL_TAG
san_values["Email"] = san.value
when 2 # dNSName in GeneralName (RFC5280)
san_values["dNSName"] = san.value # hostname
when 7 # iPAddress in GeneralName (RFC5280)
# follows GENERAL_NAME_print() in x509v3/v3_alt.c
if san.value.size == 4
san_values["iPAddress"] = san.value.unpack('C*').join('.')
elsif san.value.size == 16
san_values["iPAddress"] = san.value.unpack('n*').map { |e| sprintf("%X", e) }.join(':')
end
end
end
end
{ "oid" => self.oid, "value" => self.value, "critical" => self.critical? }.merge(san_values)
end
end
end
end
require 'test/unit'
class TestSubjectAltName < Test::Unit::TestCase
def setup
@cert = OpenSSL::X509::Certificate.new File.read('san_cert.pem')
end
def test_verify_san_upn
assert @cert.verify_san_upn('[email protected]')
refute @cert.verify_san_upn('[email protected]')
end
def test_verify_san_email
assert @cert.verify_san_email('[email protected]')
refute @cert.verify_san_email('[email protected]')
refute @cert.verify_san_email('[email protected]')
end
def test_san_to_h
san = @cert.extensions.find { |ex| ex.oid == 'subjectAltName' }
if san && (san_hash = san.to_h)
assert_equal san_hash['Email'], '[email protected]'
assert_equal san_hash['OtherName'], '[email protected]'
assert_not_equal san_hash['Email'], '[email protected]'
assert_not_equal san_hash['OtherName'], '[email protected]'
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment