module SecureHeaders class PublicKeyPinsConfigError < StandardError; end class PublicKeyPins HEADER_NAME = "Public-Key-Pins".freeze REPORT_ONLY = "Public-Key-Pins-Report-Only".freeze HASH_ALGORITHMS = [:sha256].freeze CONFIG_KEY = :hpkp class << self # Public: make an hpkp header name, value pair # # Returns nil if not configured, returns header name and value if configured. def make_header(config) return if config.nil? header = new(config) [header.name, header.value] end def validate_config!(config) return if config.nil? || config == OPT_OUT raise PublicKeyPinsConfigError.new("config must be a hash.") unless config.is_a? Hash if !config[:max_age] raise PublicKeyPinsConfigError.new("max-age is a required directive.") elsif config[:max_age].to_s !~ /\A\d+\z/ raise PublicKeyPinsConfigError.new("max-age must be a number. #{config[:max_age]} was supplied.") elsif config[:pins] && config[:pins].length < 2 raise PublicKeyPinsConfigError.new("A minimum of 2 pins are required.") end end end def initialize(config) @max_age = config.fetch(:max_age, nil) @pins = config.fetch(:pins, nil) @report_uri = config.fetch(:report_uri, nil) @report_only = !!config.fetch(:report_only, nil) @include_subdomains = !!config.fetch(:include_subdomains, nil) end def name if @report_only REPORT_ONLY else HEADER_NAME end end def value [ max_age_directive, pin_directives, report_uri_directive, subdomain_directive ].compact.join('; ').strip end def pin_directives return nil if @pins.nil? @pins.collect do |pin| pin.map do |token, hash| "pin-#{token}=\"#{hash}\"" if HASH_ALGORITHMS.include?(token) end end.join('; ') end def max_age_directive "max-age=#{@max_age}" if @max_age end def report_uri_directive "report-uri=\"#{@report_uri}\"" if @report_uri end def subdomain_directive @include_subdomains ? 'includeSubDomains' : nil end end end