Skip to content

Instantly share code, notes, and snippets.

@zmajstor
Last active August 29, 2015 14:05
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save zmajstor/a907fac1705e66411163 to your computer and use it in GitHub Desktop.
Save zmajstor/a907fac1705e66411163 to your computer and use it in GitHub Desktop.
require 'openssl' unless defined? OpenSSL
# improved extraction and validation of the subjectAltName X509 extension for
# 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
# e.g.
# otherName=1.3.6.1.4.1.311.20.2.3;UTF8:bobOtherAltName@example.com
# email=bobRFC822AltName@example.com
# DNS.1=example1.com
# DNS.2=example2.com
# URI=http://example.com/
# IP.1=13::17
# IP.2=192.168.7.1
# OtherName, unsupported in current implementation:
# ex = cert.extensions.find { |ext| ext.oid == 'subjectAltName' }
#
# before:
# ex.value #=> "email:bobRFC822AltName@example.com, othername:<unsupported>"
#
# after:
# ex.san_value_hash.inspect # => {"email"=>"bobRFC822AltName@example.com", "otherName"=>{ "upn"=>"bobOtherAltName@example.com"}}
# cert.san_value_hash # => {"email"=>"bobRFC822AltName@example.com", "otherName"=>{ "upn"=>"bobOtherAltName@example.com"}}
# cert.verify_san_other_name_upn("bobOtherAltName@example.com") #=> true
class OpenSSL::X509::CertificateHelper < Struct.new(:cert)
UPN_OID = '1.3.6.1.4.1.311.20.2.3'
def verify_san_other_name_upn(upn)
!!(/\A#{san_value_hash['otherName']['upn']}\z/i =~ upn)
end
def verify_san_email(email)
!!(/\A#{san_value_hash['email']}\z/i =~ email)
end
def common_name_value
cert.subject.to_a.each do |oid, value|
return value if oid == "CN"
end
end
def key_usage_extension
@key_usage_extension ||= cert.extensions.detect { |ex| ex.oid == 'keyUsage' }
end
def key_usage_value
key_usage_extension.value if key_usage_extension
end
def san_extension
# return first subjectAltName, or nil if there is no subjectAltName
@san_extension ||= cert.extensions.detect { |ex| ex.oid == 'subjectAltName' }
end
def san_value_hash
return {} unless san_extension && (san_extension.oid == 'subjectAltName')
octet_str = OpenSSL::ASN1.decode(san_extension.to_der).value.last
sequence = OpenSSL::ASN1.decode(octet_str.value)
Hash.new.tap do |san_values|
sequence.value.each do |san|
case san.tag
when 0 # otherName
if san.value.first.oid == UPN_OID
# san_values['otherNameUPN'] = san.value.last.value.first.value
san_values['otherName'] = { 'upn' => san.value.last.value.first.value }
end
when 1 # email
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
end
end
require 'test_helper'
class TestCertificateHelper < ActiveSupport::TestCase
def setup
@cert = OpenSSL::X509::Certificate.new File.read(Rails.root.join('test/fixtures/san_cert.pem'))
@cert_helper = OpenSSL::X509::CertificateHelper.new @cert
end
def test_verify_san_other_name_upn
assert @cert_helper.verify_san_other_name_upn('zm@promdm.net')
refute @cert_helper.verify_san_other_name_upn('zm1@promdm.net')
end
def test_verify_san_email
assert @cert_helper.verify_san_email('zm@promdm.com')
refute @cert_helper.verify_san_email('zm@promdm.net')
refute @cert_helper.verify_san_email('zm1@promdm.net')
end
def test_san_value_hash
# san_ex = @cert.extensions.find { |ex| ex.oid == 'subjectAltName' }
san = @cert_helper.san_value_hash
assert_equal 'zm@promdm.com', san['email']
assert_equal 'zm@promdm.net', san['otherName']['upn']
assert_not_equal 'zm@promdm.net', san['email']
assert_not_equal 'zm@promdm.com', san['otherName']['upn']
end
def test_key_usage_value
assert_equal "Digital Signature, Key Encipherment", @cert_helper.key_usage_value
end
end
-----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-----
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment