Search code examples
rubyopensslcsr

How to get an attribute from OpenSSL::X509::Request?


There are tutorials out there showing, how to add an attribute (such as the subjectAltName-extension) to a Certificate Sign Request (CSR). For example, this is how I enumerate valid aliases, when creating a CSR:

aliases.each do |a|
    alist << ("DNS:#{a}")
    alist << ("IP:#{a}") if IPAddress.valid? a
end

extension = OpenSSL::X509::ExtensionFactory.new.create_extension(
    'subjectAltName',
    alist.join(', '),
    false
)
csr.add_attribute OpenSSL::X509::Attribute.new(
    'extReq',
    OpenSSL::ASN1::Set.new(
        [OpenSSL::ASN1::Sequence.new([extension])]
    )
)

But, suppose I want to read any such attribute from an already existing CSR (such as something read from disk)? There is no get_attribute method... Is there a straightforward way of obtaining the original list (such as DNS:meow, DNS:127.0.0.1, IP:127.0.0.1) from the Request-object?


Solution

  • Ok, this is, how I do it for now -- after finding some code in Ruby's openssl/ssl.rb:

    def getAliases(csr) 
        attributes = csr.attributes
        return nil if not attributes
    
        seq = nil
        values = nil
    
        attributes.each do |a|
            if a.oid == 'extReq'
                seq = a.value
                break
            end
        end
        return nil if not seq
    
        seq.value.each do |v|
            v.each do |v|
                if v.value[0].value == 'subjectAltName'
                    values = v.value[1].value
                    break
                end
                break if values
            end
        end
        return nil if not values
    
        values = OpenSSL::ASN1.decode(values).value
    
        result = []
        values.each do |v|
            case v.tag
            when 2
                result << "DNS:#{v.value}"
            when 7
                case v.value.size
                when 4
                    ip = v.value.unpack('C*').join('.')
                when 16
                    ip = v.value.unpack('n*').map { |o| sprintf("%X", o) }.join(':')
                else
                    STDERR.print "The encountered IP-address is neither IPv4 nor IPv6\n"
                    next
                end
                result << "IP:#{ip}"
            else
                STDERR.print "Uknown tag #{v.tag} -- I only know 2 (DNS) and 7 (IP)\n"
            end
        end
        return result
    end
    

    However, I do not like it because it should be possible to obtain the entire ready-made extension (by name) from the csr and add it to cert verbatim -- without the decoding (even if I ever make it perfect) and re-encoding.