# frozen_string_literal: true # # ronin-db-activerecord - ActiveRecord backend for the Ronin Database. # # Copyright (c) 2022-2024 Hal Brodigan (postmodern.mod3 at gmail.com) # # ronin-db-activerecord is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published # by the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # ronin-db-activerecord is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with ronin-db-activerecord. If not, see . # require 'ronin/db/model' require 'ronin/db/model/importable' module Ronin module DB # # Represents a SSL/TLS certificate. # # @since 0.2.0 # class Cert < ActiveRecord::Base include Model include Model::Importable # @!attribute [rw] id # The primary ID of the certificate. # # @return [Integer] attribute :id, :integer # @!attribute [rw] serial # The certificate's serial number. # # @return [String] attribute :serial, :string validates :serial, presence: true # @!attribute [rw] version # The certificate's version number. # # @return [Integer] attribute :version, :integer validates :version, presence: true # @!attribute [rw] not_before # When the certificate starts being valid. # # @return [Time] attribute :not_before, :datetime validates :not_before, presence: true # @!attribute [rw] not_after # When the certificate expires. # # @return [Time] attribute :not_after, :datetime validates :not_after, presence: true # @!attribute [rw] issuer # The certificate issuer information. # # @return [CertIssuer, nil] # # @note # When the certificate is self-signed, {#issuer} will not be set. belongs_to :issuer, class_name: 'CertIssuer', optional: true # @!attribute [rw] subject # The certificate subject information. # # @return [CertSubject] belongs_to :subject, class_name: 'CertSubject', required: true # @!attribute [rw] public_key_algorithm # The public key algorithm. # # @return ["rsa", "dsa", "dh", "ec"] enum :public_key_algorithm, {rsa: 'RSA', dsa: 'DSA', dh: 'DH', ec: 'EC'} validates :public_key_algorithm, presence: true # @!attribute [rw] public_key_size # The public key size in bits. # # @return [Integer] attribute :public_key_size, :integer validates :public_key_size, presence: true # @!attribute [rw] signing_algorithm # The algorithm used to sign the certificate. # # @return [String] attribute :signing_algorithm, :string validates :signing_algorithm, presence: true # @!attribute [rw] sha1_fingerprint # The SHA1 fingerprint of the certificate. # # @return [String] attribute :sha1_fingerprint # @!attribute [rw] sha256_fingerprint # The SHA256 fingerprint of the certificate. # # @return [String] attribute :sha256_fingerprint # @!attribute [rw] pem # The PEM encoded version of the certificate. # # @return [String] attribute :pem, :text validates :pem, presence: true # @!attribute [rw] created_at # When the certificate was created. # # @return [Time] attribute :created_at, :datetime # @!attribute [rw] subject_alt_names # The `subjectAltName`s of the certificate. # # @return [Array] has_many :subject_alt_names, class_name: 'CertSubjectAltName', dependent: :destroy # @!attribute [rw] open_ports # The open ports that use this certificate. # # @return [Array] has_many :open_ports, dependent: :nullify # @!attribute [rw] ip_addresses # The IP addresses that use this certificate. # # @return [Array] has_many :ip_addresses, through: :open_ports # @!attribute [rw] notes # The associated notes. # # @return [Array] # # @since 0.2.0 has_many :notes # # Queries all active certificates. # # @return [Array] # def self.active now = DateTime.now where(not_before: ..now, not_after: now...) end # # Queries all expired certificates. # # @return [Array] # def self.expired where(not_after: ...Time.now) end # # Queries all certificates with the issuer common name (`CN`). # # @param [String] name # The issuer common name to search for. # # @return [Array] # def self.with_issuer_common_name(name) joins(:issuer).where(issuer: {common_name: name}) end # # Queries all certificates with the issuer common name (`O`). # # @param [String] name # The issuer organization to search for. # # @return [Array] # def self.with_issuer_organization(name) joins(:issuer).where(issuer: {organization: name}) end # # Queries all certificates with the issuer common name (`OU`). # # @param [String] unit # The issuer organizational unit name to search for. # # @return [Array] # def self.with_issuer_organizational_unit(unit) joins(:issuer).where(issuer: {organizational_unit: unit}) end # # Queries all certificates with the issuer common name (`L`). # # @param [String] locality # The issuer locality to search for. # # @return [Array] # def self.with_issuer_locality(locality) joins(:issuer).where(issuer: {locality: locality}) end # # Queries all certificates with the issuer common name (`ST`). # # @param [String] state # The issuer state name to search for. # # @return [Array] # def self.with_issuer_state(state) joins(:issuer).where(issuer: {state: state}) end # # Queries all certificates with the issuer common name (`C`). # # @param [String] country # The issuer's two-letter country code to search for. # # @return [Array] # def self.with_issuer_country(country) joins(:issuer).where(issuer: {country: country}) end # # Queries all certificates with the subject state (`O`). # # @param [String] name # The organization name to search for. # # @return [Array] # def self.with_organization(name) joins(:subject).where(subject: {organization: name}) end # # Queries all certificates with the subject state (`OU`). # # @param [String] unit # The organizational unit name to search for. # # @return [Array] # def self.with_organizational_unit(unit) joins(:subject).where(subject: {organizational_unit: unit}) end # # Queries all certificates with the subject state (`L`). # # @param [String] locality # The locality to search for. # # @return [Array] # def self.with_locality(locality) joins(:subject).where(subject: {locality: locality}) end # # Queries all certificates with the subject state (`ST`). # # @param [String] state # The state name to search for. # # @return [Array] # def self.with_state(state) joins(:subject).where(subject: {state: state}) end # # Queries all certificates with the subject country (`C`). # # @param [String] country # The two-letter country code to search for. # # @return [Array] # def self.with_country(country) joins(:subject).where(subject: {country: country}) end # # Queries all certificates with the common name (`CN`). # # @param [String] name # The common name to search for. # # @return [Array] # def self.with_common_name(name) joins(subject: [:common_name]).where( subject: { ronin_cert_names: { name: name } } ) end # # Queries all certificates with the `subjectAltName` value. # # @param [String] name # The host name or IP address to query. # # @return [Array] # def self.with_subject_alt_name(name) joins(subject_alt_names: [:name]).where( subject_alt_names: { ronin_cert_names: { name: name } } ) end # # Looks up the certificate. # # @param [OpenSSL::X509::Certificate] cert # The X509 certificate object or PEM string. # # @return [Cert, nil] # The matching certificate. # def self.lookup(cert) find_by(sha256_fingerprint: Digest::SHA256.hexdigest(cert.to_der)) end # # Imports an SSL/TLS X509 certificate into the database. # # @param [OpenSSL::X509::Certificate] cert # The certificate object to import. # # @return [Cert] # The imported certificate. # def self.import(cert) case (public_key = cert.public_key) when OpenSSL::PKey::RSA public_key_algorithm = :rsa public_key_size = public_key.n.num_bits when OpenSSL::PKey::DSA public_key_algorithm = :dsa public_key_size = public_key.p.num_bits when OpenSSL::PKey::DH public_key_algorithm = :dh public_key_size = public_key.p.num_bits when OpenSSL::PKey::EC public_key_algorithm = :ec public_key_text = public_key.to_text public_key_size = if (match = public_key_text.match(/\((\d+) bit\)/)) match[1].to_i end else raise(NotImplementedError,"unsupported public key type: #{public_key.inspect}") end der = cert.to_der create( serial: cert.serial.to_s(16), version: cert.version, not_before: cert.not_before, not_after: cert.not_after, # NOTE: set #issuer to nil if the cert is self-signed issuer: unless cert.issuer == cert.subject CertIssuer.import(cert.issuer) end, subject: CertSubject.import(cert.subject), public_key_algorithm: public_key_algorithm, public_key_size: public_key_size, signing_algorithm: cert.signature_algorithm, sha1_fingerprint: Digest::SHA1.hexdigest(der), sha256_fingerprint: Digest::SHA256.hexdigest(der), pem: cert.to_pem ) do |new_cert| if (subject_alt_name = cert.find_extension('subjectAltName')) CertSubjectAltName.parse(subject_alt_name.value).each do |name| new_cert.subject_alt_names.new( name: CertName.find_or_import(name) ) end end end end # # The subject's common name (`CN`). # # @return [String] # def common_name subject.common_name end # # The subject's organization (`O`). # # @return [String] # def organization subject.organization end # # The subject's organizational unit (`OU`). # # @return [String] # def organizational_unit subject.organizational_unit end # # The subject's locality (`L`). # # @return [String] # def locality subject.locality end # # The subject's state (`ST`). # # @return [String] # def state subject.state end # # The subject's country (`C`). # # @return [String] # def country subject.country end # # Converts the certificate back into PEM format. # # @return [String] # def to_pem pem end alias to_s to_pem end end end require 'ronin/db/cert_issuer' require 'ronin/db/cert_subject' require 'ronin/db/cert_subject_alt_name' require 'ronin/db/open_port' require 'ronin/db/note'