square/rails-auth

View on GitHub
lib/rails/auth/x509/certificate.rb

Summary

Maintainability
A
0 mins
Test Coverage
# frozen_string_literal: true

module Rails
  module Auth
    module X509
      # X.509 client certificates obtained from HTTP requests
      class Certificate
        attr_reader :certificate

        def initialize(certificate)
          unless certificate.is_a?(OpenSSL::X509::Certificate)
            raise TypeError, "expecting OpenSSL::X509::Certificate, got #{certificate.class}"
          end

          @certificate = certificate.freeze
          @subject = {}

          @certificate.subject.to_a.each do |name, data, _type|
            @subject[name.freeze] = data.freeze
          end
          @subject_alt_names = SubjectAltNameExtension.new(certificate)
          @subject_alt_names.freeze
          @subject.freeze
        end

        def [](component)
          @subject[component.to_s.upcase]
        end

        def cn
          @subject["CN"]
        end
        alias common_name cn

        def dns_names
          @subject_alt_names.dns_names
        end

        def ips
          @subject_alt_names.ips
        end

        def ou
          @subject["OU"]
        end
        alias organizational_unit ou

        def uris
          @subject_alt_names.uris
        end

        # According to the SPIFFE standard only one SPIFFE ID can exist in the URI
        # SAN:
        # (https://github.com/spiffe/spiffe/blob/master/standards/X509-SVID.md#2-spiffe-id)
        #
        # @return [String, nil] string containing SPIFFE ID if one is present
        #         in the certificate
        def spiffe_id
          uris.detect { |uri| uri.start_with?("spiffe://") }
        end

        # Generates inspectable attributes for debugging
        #
        # @return [Hash] hash containing parts of the certificate subject (cn, ou)
        #         and subject alternative name extension (uris, dns_names) as well
        #         as SPIFFE ID (spiffe_id), which is just a convenience since those
        #         are already included in the uris
        def attributes
          {
            cn: cn,
            dns_names: dns_names,
            ips: ips,
            ou: ou,
            spiffe_id: spiffe_id,
            uris: uris
          }.reject { |_, v| v.nil? || v.empty? }
        end

        # Compare ourself to another object by ensuring that it has the same type
        # and that its certificate pem is the same as ours
        def ==(other)
          other.is_a?(self.class) && other.certificate.to_der == certificate.to_der
        end

        alias eql? ==
      end
    end
  end
end