Class: MCollective::Security::Choria
- Defined in:
- lib/mcollective/security/choria.rb
Instance Attribute Summary
Attributes inherited from Base
Instance Method Summary collapse
-
#cache_client_pubcert(envelope, pubcert) ⇒ Boolean
Caches the public certificate of a sender.
-
#callerid ⇒ String
The callerid based on the certificate name.
-
#certname ⇒ String
The certname of the current context.
-
#certname_from_callerid(id) ⇒ String
Parses our callerids and return the certname.
-
#certname_whitelist_regex ⇒ Regexp
Calculate a Regex that will match the entire cert whitelist.
- #choria ⇒ Object
-
#client_cache_mutex ⇒ Mutex
Mutex used for locking write access to the pubcert cache.
-
#client_private_key ⇒ String
The path to a client private key.
-
#client_pubcert_metadata(envelope, pubcert) ⇒ Hash
Metadata about a pubcert based on the envelope.
-
#client_public_cert ⇒ String
The path to a client public certificate.
-
#comma_sep_list_to_regex(list, default) ⇒ Regexp
Parse a comma seperated list into a Regex spanning the list.
-
#current_timestamp ⇒ Fixnum
Retrieves the current time in UTC.
-
#decode_reply(secure_payload) ⇒ Hash
Validates a received reply is in the correct format and passes security checks.
-
#decode_request(message, secure_payload) ⇒ Hash
Validates a received request is in the correct format and passes security checks.
-
#decodemsg(message) ⇒ void
Decodes a message and validates it’s security.
-
#default_serializer ⇒ Symbol
Determines the default serializer.
-
#deserialize(string, format = :json) ⇒ Class
Deserialize a string.
-
#empty_reply ⇒ Hash
Creates a empty choria:reply:1.
-
#empty_request ⇒ Hash
Creates a empty choria:request:1.
-
#encodereply(sender_agent, msg, requestid, requestcallerid = nil) ⇒ String
Encodes a reply to a earlier received message.
-
#encoderequest(sender, msg, requestid, filter, target_agent, target_collective, ttl = 60) ⇒ String
Encodes a request on behalf of the MCollective Client code.
- #env_fetch(key, default = nil) ⇒ Object
-
#has_client_private_key? ⇒ Boolean
Determines if the client_private_key exist.
-
#has_client_public_cert? ⇒ Boolean
Determines if teh client_public_cert exist.
-
#hash(string) ⇒ String
Produce a Base64 encoded SHA256 digest of a string.
-
#initialize ⇒ Choria
constructor
A new instance of Choria.
-
#legacy_processed!(requestid) ⇒ Object
Mark a request as processed and mark it for removal from the cache.
-
#legacy_request?(requestid) ⇒ Boolean
Determines if a specific requestid was a previously seen legacy request.
-
#privilegeduser_certs ⇒ Array<String>
Search the cache directory for certificates matching the privileged user list.
-
#privilegeduser_regex ⇒ Regexp
Calculate a Regex that will match the entire privileged user list.
- #public_cert_metadatafile(callerid) ⇒ Object
-
#public_certfile(callerid) ⇒ String
Determines the path to a cached certificate for a caller.
-
#record_legacy_request(request) ⇒ Object
Records the fact that a request is from a legacy client.
-
#request_signer ⇒ Object
The class the implements signing the requests.
-
#serialize(obj, format = :json) ⇒ String
Serialize a object.
-
#server_public_cert_dir ⇒ String
The path where a server caches client certificates.
-
#should_cache_certname?(pubcert, callerid) ⇒ Boolean
Determines if a certificate should be cached.
-
#sign(string, id = nil) ⇒ String
Signs a string using the private key.
-
#sign_secure_request!(secure_request) ⇒ Object
Signs a secure request.
-
#ssl_dir ⇒ String
The directory where SSL related files live.
-
#to_legacy_filter(filter) ⇒ Hash
Converts a choria filter into a legacy format.
-
#to_legacy_reply(body) ⇒ Hash
Converts a choria:reply:1 to a legacy format.
-
#to_legacy_request(body) ⇒ Hash
Converts a choria:request:1 to a legacy format.
-
#valid_protocol?(body, protocol, template) ⇒ Boolean
Checks the structure of a message is well formed.
-
#validrequest?(secure_payload, request) ⇒ Boolean
Verifies the request by checking it’s been signed with the cached certificate of the claimed callerid.
-
#verify_signature(string, signature, callerid, allow_privileged = false) ⇒ Object
Verifies a signature of a string using a certificate.
Methods inherited from Base
#create_reply, #create_request, inherited, #should_process_msg?, #valid_callerid?, #validate_filter?
Constructor Details
#initialize ⇒ Choria
Returns a new instance of Choria.
11 12 13 14 15 16 17 18 19 20 21 22 23 |
# File 'lib/mcollective/security/choria.rb', line 11 def initialize super # Stores lists of requests that came from legacy choria clients so they # can be encoded appropriately for them on reply # # This has to be an expiring entity since not all requests make # replies # # See issue 288 for background on this, this can be removed once we hit # 1.0.0 along with the calls to the methods using this Cache.setup(:choria_security, 3600) end |
Instance Method Details
#cache_client_pubcert(envelope, pubcert) ⇒ Boolean
Caches the public certificate of a sender
If there is not yet a cached certificate for the callerid a new one is saved after first checking it against our CA
442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 |
# File 'lib/mcollective/security/choria.rb', line 442 def cache_client_pubcert(envelope, pubcert) return false if $choria_unsafe_disable_protocol_security # rubocop:disable Style/GlobalVars client_cache_mutex.synchronize do callerid = envelope["callerid"] certfile = public_certfile(callerid) = (callerid) if File.exist?(certfile) Log.debug("Already have a cert from %s in %s" % [callerid, certfile]) false else raise("Received an invalid certificate for %s" % callerid) unless should_cache_certname?(pubcert, callerid) Log.info("Saving verified pubcert for %s in %s" % [callerid, certfile]) File.open(certfile, "w") do |f| f.print(pubcert) end File.open(, "w") do |f| f.print((envelope, pubcert).to_json) end true end end end |
#callerid ⇒ String
The callerid based on the certificate name
Caller ids are in the form ‘choria=certname`
590 591 592 593 594 |
# File 'lib/mcollective/security/choria.rb', line 590 def callerid return request_signer.callerid if choria.anon_tls? "choria=%s" % certname end |
#certname ⇒ String
The certname of the current context
In the case of root that would be the configured ‘identity` for non root it would a string made up of the current username as determined by the USER environment variable or the configured `identity`
At present windows clients are probably not supported automatically as they will default to the certificate based on identity. Same as root. Windows will have to rely on the environment override until we can figure out what the best behaviour is
In all cases the certname can be overridden using the ‘MCOLLECTIVE_CERTNAME` environment variable
556 557 558 |
# File 'lib/mcollective/security/choria.rb', line 556 def certname choria.certname end |
#certname_from_callerid(id) ⇒ String
Parses our callerids and return the certname
489 490 491 492 493 494 495 |
# File 'lib/mcollective/security/choria.rb', line 489 def certname_from_callerid(id) if id =~ /^choria=([\w.\-]+)/ $1 else raise("Received a callerid in an unexpected format: %s" % id) end end |
#certname_whitelist_regex ⇒ Regexp
Calculate a Regex that will match the entire cert whitelist
Defaults to match /.mcollective$/ othwerwise whatever is specified, in the comma seperated config item ‘choria.security.cert_whitelist`
359 360 361 362 363 |
# File 'lib/mcollective/security/choria.rb', line 359 def certname_whitelist_regex whitelist = @config.pluginconf.fetch("choria.security.certname_whitelist", "") comma_sep_list_to_regex(whitelist, /\.mcollective$/) end |
#choria ⇒ Object
25 26 27 |
# File 'lib/mcollective/security/choria.rb', line 25 def choria @_choria ||= Util::Choria.new(false) end |
#client_cache_mutex ⇒ Mutex
Mutex used for locking write access to the pubcert cache
429 430 431 |
# File 'lib/mcollective/security/choria.rb', line 429 def client_cache_mutex @_client_cache_mutex ||= Mutex.new end |
#client_private_key ⇒ String
paths determined by Puppet AIO packages
The path to a client private key
575 576 577 |
# File 'lib/mcollective/security/choria.rb', line 575 def client_private_key choria.client_private_key end |
#client_pubcert_metadata(envelope, pubcert) ⇒ Hash
Metadata about a pubcert based on the envelope
407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 |
# File 'lib/mcollective/security/choria.rb', line 407 def (envelope, pubcert) cert = choria.parse_pubcert(pubcert).first { "create_time" => , "senderid" => envelope["senderid"], "requestid" => envelope["requestid"], "certinfo" => { "issuer" => cert.issuer.to_s, "not_after" => Integer(cert.not_after), "not_before" => Integer(cert.not_before), "serial" => cert.serial.to_s, "subject" => cert.subject.to_s, "version" => cert.version, "signature_algorithm" => cert.signature_algorithm } } end |
#client_public_cert ⇒ String
paths determined by Puppet AIO packages
The path to a client public certificate
565 566 567 |
# File 'lib/mcollective/security/choria.rb', line 565 def client_public_cert choria.client_public_cert end |
#comma_sep_list_to_regex(list, default) ⇒ Regexp
Parse a comma seperated list into a Regex spanning the list
301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 |
# File 'lib/mcollective/security/choria.rb', line 301 def comma_sep_list_to_regex(list, default) matchlist = list.split(",").map do |item| item.strip! if item =~ /^\/(.+)\/$/ Regexp.new($1) else item end end.compact matchlist << default if matchlist.empty? Regexp.union(matchlist.compact.uniq) end |
#current_timestamp ⇒ Fixnum
Retrieves the current time in UTC
662 663 664 |
# File 'lib/mcollective/security/choria.rb', line 662 def Integer(Time.now.utc) end |
#decode_reply(secure_payload) ⇒ Hash
right now no actual security checks are done on replies
Validates a received reply is in the correct format and passes security checks
During this the YAML encoded ‘message` held will be deserialized
206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 |
# File 'lib/mcollective/security/choria.rb', line 206 def decode_reply(secure_payload) reply = deserialize(secure_payload["message"], default_serializer) if reply["message"].is_a?(String) # non json based things like 'mco ping' that just sends 'ping' will fail on JSON serialize # while yaml would not fail and just return the string # # So we ensure the message is left as it was should json deserialize fail, tbh this a train wreck # but it's how the original mcollective was designed, definitely need a bit of a rethink there as # at core its not compatible with this JSON stuff as is begin reply["message"] = deserialize(reply["message"], default_serializer) rescue # rubocop:disable Lint/SuppressedException end end unless valid_protocol?(reply, "choria:reply:1", empty_reply) || valid_protocol?(reply, "mcollective:reply:3", empty_reply) raise(SecurityValidationFailed, "Unknown reply body format received. Expected choria:reply:1 or mcollective:reply:3, cannot continue") end to_legacy_reply(reply) end |
#decode_request(message, secure_payload) ⇒ Hash
Validates a received request is in the correct format and passes security checks
During this the YAML encoded ‘message` held will be deserialized
143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 |
# File 'lib/mcollective/security/choria.rb', line 143 def decode_request(, secure_payload) request = deserialize(secure_payload["message"], default_serializer) unless valid_protocol?(request, "choria:request:1", empty_request) || valid_protocol?(request, "mcollective:request:3", empty_request) raise(SecurityValidationFailed, "Unknown request body format received. Expected choria:request:1 or mcollective:request:3, cannot continue") end cache_client_pubcert(request["envelope"], secure_payload["pubcert"]) if @initiated_by == :node validrequest?(secure_payload, request) should_process_msg?(, request["envelope"]["requestid"]) if request["message"].is_a?(String) # non json based things like 'mco ping' that just sends 'ping' will fail on JSON serialize # while yaml would not fail and just return the string # # So we ensure the message is left as it was should json deserialize fail, tbh this a train wreck # but it's how the original mcollective was designed, definitely need a bit of a rethink there as # at core its not compatible with this JSON stuff as is begin request["message"] = deserialize(request["message"], default_serializer) rescue # rubocop:disable Lint/SuppressedException end else record_legacy_request(request) end to_legacy_request(request) end |
#decodemsg(message) ⇒ void
This method returns an undefined value.
Decodes a message and validates it’s security
This will delegate the actual checking of messages to #decode_request and #decode_reply.
119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 |
# File 'lib/mcollective/security/choria.rb', line 119 def decodemsg() secure_payload = deserialize(.payload) case secure_payload["protocol"] when "choria:secure:request:1" decode_request(, secure_payload) when "choria:secure:reply:1" decode_reply(secure_payload) else Log.debug("Unknown protocol in message:\n%s" % secure_payload.pretty_inspect) raise(SecurityValidationFailed, "Received an unknown protocol '%s' message, ignoring" % secure_payload["protocol"]) end end |
#default_serializer ⇒ Symbol
Determines the default serializer
As of MCollective 2.11.0 it will translate “package” into :package to faciliate JSON requests and other programming languages. This is a super experimental feature but will allow us to ditch YAML for now.
By setting ‘choria.security.serializer` to JSON this new behaviour can be tested
533 534 535 |
# File 'lib/mcollective/security/choria.rb', line 533 def default_serializer @config.pluginconf.fetch("choria.security.serializer", "json").downcase.intern end |
#deserialize(string, format = :json) ⇒ Class
Deserialize a string
515 516 517 518 519 520 521 |
# File 'lib/mcollective/security/choria.rb', line 515 def deserialize(string, format=:json) if format == :yaml YAML.load(string) else JSON.parse(string, :object_class => Util::IndifferentHash) end end |
#empty_reply ⇒ Hash
Creates a empty choria:reply:1
Some envelope fields like time are set to sane defautls
693 694 695 696 697 698 699 700 701 702 703 704 |
# File 'lib/mcollective/security/choria.rb', line 693 def empty_reply { "protocol" => "choria:reply:1", "message" => nil, "envelope" => { "senderid" => @config.identity, "requestid" => nil, "agent" => nil, "time" => } } end |
#empty_request ⇒ Hash
Creates a empty choria:request:1
Some envelope fields like time are set to sane defautls
671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 |
# File 'lib/mcollective/security/choria.rb', line 671 def empty_request { "protocol" => "choria:request:1", "message" => nil, "envelope" => { "requestid" => nil, "senderid" => @config.identity, "callerid" => nil, "filter" => {}, "collective" => @config.main_collective, "agent" => nil, "ttl" => @config.ttl, "time" => } } end |
#encodereply(sender_agent, msg, requestid, requestcallerid = nil) ⇒ String
Encodes a reply to a earlier received message
The reply is turned into a ‘choria:reply:1` and then encoded in a `choria:secure:reply:1` before being serialized
90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 |
# File 'lib/mcollective/security/choria.rb', line 90 def encodereply(sender_agent, msg, requestid, requestcallerid=nil) reply = empty_reply reply["envelope"]["requestid"] = requestid reply["envelope"]["agent"] = sender_agent if legacy_request?(requestid) reply["message"] = msg legacy_processed!(requestid) else reply["message"] = serialize(msg, default_serializer) end serialized_reply = serialize(reply, default_serializer) serialize( "protocol" => "choria:secure:reply:1", "message" => serialized_reply, "hash" => hash(serialized_reply) ) end |
#encoderequest(sender, msg, requestid, filter, target_agent, target_collective, ttl = 60) ⇒ String
Encodes a request on behalf of the MCollective Client code
The request is turned into a ‘choria:request:1` message and then encoded in a `choria:secure:request:1` message prior to being serialized
42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 |
# File 'lib/mcollective/security/choria.rb', line 42 def encoderequest(sender, msg, requestid, filter, target_agent, target_collective, ttl=60) request = empty_request request["message"] = serialize(msg, default_serializer) request["envelope"]["requestid"] = requestid request["envelope"]["filter"] = filter request["envelope"]["agent"] = target_agent request["envelope"]["collective"] = target_collective request["envelope"]["ttl"] = ttl request["envelope"]["callerid"] = callerid serialized_request = serialize(request, default_serializer) secure_request = { "protocol" => "choria:secure:request:1", "message" => serialized_request, "signature" => "insecure", "pubcert" => "insecure" } sign_secure_request!(secure_request) serialize(secure_request) end |
#env_fetch(key, default = nil) ⇒ Object
560 561 562 |
# File 'lib/mcollective/security/choria.rb', line 560 def env_fetch(key, default=nil) choria.env_fetch(key, default) end |
#has_client_private_key? ⇒ Boolean
Determines if the client_private_key exist
580 581 582 |
# File 'lib/mcollective/security/choria.rb', line 580 def has_client_private_key? choria.has_client_private_key? end |
#has_client_public_cert? ⇒ Boolean
Determines if teh client_public_cert exist
570 571 572 |
# File 'lib/mcollective/security/choria.rb', line 570 def has_client_public_cert? choria.has_client_public_cert? end |
#hash(string) ⇒ String
Produce a Base64 encoded SHA256 digest of a string
655 656 657 |
# File 'lib/mcollective/security/choria.rb', line 655 def hash(string) OpenSSL::Digest.new("sha256", string).base64digest end |
#legacy_processed!(requestid) ⇒ Object
Mark a request as processed and mark it for removal from the cache
194 195 196 |
# File 'lib/mcollective/security/choria.rb', line 194 def legacy_processed!(requestid) Cache.invalidate!(:choria_security, requestid) end |
#legacy_request?(requestid) ⇒ Boolean
Determines if a specific requestid was a previously seen legacy request
185 186 187 188 189 |
# File 'lib/mcollective/security/choria.rb', line 185 def legacy_request?(requestid) !!Cache.read(:choria_security, requestid) rescue false end |
#privilegeduser_certs ⇒ Array<String>
Search the cache directory for certificates matching the privileged user list
336 337 338 339 340 341 342 343 344 345 346 347 |
# File 'lib/mcollective/security/choria.rb', line 336 def privilegeduser_certs match = privilegeduser_regex dir = server_public_cert_dir certs = Dir.entries(dir).grep(/pem$/).select do |cert| File.basename(cert, ".pem").match(match) end certs.map {|cert| File.join(dir, cert) } rescue Errno::ENOENT [] end |
#privilegeduser_regex ⇒ Regexp
Calculate a Regex that will match the entire privileged user list
Defaults to match /.privileged.mcollective$/ othwerwise whatever is specified, in the comma seperated config item ‘choria.security.privileged_users`
327 328 329 330 331 |
# File 'lib/mcollective/security/choria.rb', line 327 def privilegeduser_regex users = @config.pluginconf.fetch("choria.security.privileged_users", "") comma_sep_list_to_regex(users, /\.privileged\.mcollective$/) end |
#public_cert_metadatafile(callerid) ⇒ Object
480 481 482 |
# File 'lib/mcollective/security/choria.rb', line 480 def (callerid) public_certfile(callerid).gsub(/\.pem$/, ".json") end |
#public_certfile(callerid) ⇒ String
Determines the path to a cached certificate for a caller
476 477 478 |
# File 'lib/mcollective/security/choria.rb', line 476 def public_certfile(callerid) "%s/%s.pem" % [server_public_cert_dir, certname_from_callerid(callerid)] end |
#record_legacy_request(request) ⇒ Object
Records the fact that a request is from a legacy client
177 178 179 |
# File 'lib/mcollective/security/choria.rb', line 177 def record_legacy_request(request) Cache.write(:choria_security, request["envelope"]["requestid"], true) if request["envelope"] && request["envelope"]["requestid"] end |
#request_signer ⇒ Object
The class the implements signing the requests
75 76 77 78 |
# File 'lib/mcollective/security/choria.rb', line 75 def request_signer PluginManager.loadclass("MCollective::Signer::%s" % @config.pluginconf.fetch("choria.security.request_signer.plugin", "choria").capitalize) PluginManager["choria_signer_plugin"] end |
#serialize(obj, format = :json) ⇒ String
Serialize a object
502 503 504 505 506 507 508 |
# File 'lib/mcollective/security/choria.rb', line 502 def serialize(obj, format=:json) if format == :yaml YAML.dump(obj) else JSON.dump(obj) end end |
#server_public_cert_dir ⇒ String
when the path does not exist it will attempt to make it
The path where a server caches client certificates
542 543 544 545 546 547 548 |
# File 'lib/mcollective/security/choria.rb', line 542 def server_public_cert_dir dir = File.join(ssl_dir, "choria_security", "public_certs") FileUtils.mkdir_p(dir) unless File.directory?(dir) dir end |
#should_cache_certname?(pubcert, callerid) ⇒ Boolean
support white/black lists
Determines if a certificate should be cached
This checks the cert is valid against our CA, it’s name etc
373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 |
# File 'lib/mcollective/security/choria.rb', line 373 def should_cache_certname?(pubcert, callerid) callerid_certname = certname_from_callerid(callerid) certname = choria.valid_certificate?(pubcert, callerid_certname) valid_regex = certname_whitelist_regex unless certname Log.warn("Received a certificate for '%s' that is not signed by a known CA, discarding" % callerid_certname) return false end # this cert is allowed to set callerids != certname, so check it here and log callerid if certname =~ privilegeduser_regex Log.warn("Allowing cache of privileged user certname %s from callerid %s" % [certname, callerid]) return true end unless certname == callerid_certname Log.warn("Received a certificate called '%s' that does not match the received callerid of '%s'" % [certname, callerid_certname]) return false end unless certname =~ valid_regex Log.warn("Received certificate name '%s' does not match %s" % [certname, valid_regex]) return false end true end |
#sign(string, id = nil) ⇒ String
Signs a string using the private key
602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 |
# File 'lib/mcollective/security/choria.rb', line 602 def sign(string, id=nil) key = client_private_key @_keys ||= {} if @_keys[key].nil? if has_client_private_key? Log.debug("Signing request using client private key %s" % key) else raise("Cannot find private key %s, cannot sign message" % key) end @_keys[key] ||= OpenSSL::PKey::RSA.new(File.read(key)) end signed = @_keys[key].sign(OpenSSL::Digest.new("SHA256"), string) Base64.encode64(signed).chomp end |
#sign_secure_request!(secure_request) ⇒ Object
Signs a secure request
69 70 71 |
# File 'lib/mcollective/security/choria.rb', line 69 def sign_secure_request!(secure_request) request_signer.sign_secure_request!(secure_request) end |
#ssl_dir ⇒ String
The directory where SSL related files live
This is configurable using choria.ssldir which should be a path expandable using File.expand_path
On Windows or when running as root Puppet settings will be consulted but when running as a normal user it will default to the AIO path when not configured
551 552 553 |
# File 'lib/mcollective/security/choria.rb', line 551 def ssl_dir choria.ssl_dir end |
#to_legacy_filter(filter) ⇒ Hash
Converts a choria filter into a legacy format
Choria filters have strings for fact filter keys, mcollective expect symbols
712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 |
# File 'lib/mcollective/security/choria.rb', line 712 def to_legacy_filter(filter) return filter unless filter.include?("fact") new = {} filter.each do |key, value| new[key] = value next unless key == "fact" new["fact"] = value.map do |ff| { :fact => ff.fetch(:fact, ff["fact"]), :operator => ff.fetch(:operator, ff["operator"]), :value => ff.fetch(:value, ff["value"]) } end end new end |
#to_legacy_reply(body) ⇒ Hash
Converts a choria:reply:1 to a legacy format
754 755 756 757 758 759 760 761 762 |
# File 'lib/mcollective/security/choria.rb', line 754 def to_legacy_reply(body) { :senderid => body["envelope"]["senderid"], :requestid => body["envelope"]["requestid"], :senderagent => body["envelope"]["agent"], :msgtime => body["envelope"]["time"], :body => body["message"] } end |
#to_legacy_request(body) ⇒ Hash
Converts a choria:request:1 to a legacy format
737 738 739 740 741 742 743 744 745 746 747 748 749 |
# File 'lib/mcollective/security/choria.rb', line 737 def to_legacy_request(body) { :body => body["message"], :senderid => body["envelope"]["senderid"], :requestid => body["envelope"]["requestid"], :filter => to_legacy_filter(body["envelope"]["filter"]), :collective => body["envelope"]["collective"], :agent => body["envelope"]["agent"], :callerid => body["envelope"]["callerid"], :ttl => body["envelope"]["ttl"], :msgtime => body["envelope"]["time"] } end |
#valid_protocol?(body, protocol, template) ⇒ Boolean
this really should be json schema or even better protobufs
Checks the structure of a message is well formed
258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 |
# File 'lib/mcollective/security/choria.rb', line 258 def valid_protocol?(body, protocol, template) unless body.is_a?(Hash) Log.warn("Body from the message should be a Hash") return false end unless body["protocol"] == protocol Log.warn("Unknown message protocol, should be %s" % protocol) return false end unless body.include?("envelope") Log.warn("No envelope found in the message") return false end envelope = body["envelope"] unless envelope.is_a?(Hash) Log.warn("Envelope in message is not a hash") return false end valid_envelope = template["envelope"].keys unless (envelope.keys - valid_envelope).empty? Log.warn("Envelope does not have the correct keys, only %s allowed" % valid_envelope.join(", ")) return false end unless body.include?("message") Log.warn("Body has no message") return false end true end |
#validrequest?(secure_payload, request) ⇒ Boolean
Verifies the request by checking it’s been signed with the cached certificate of the claimed callerid
235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 |
# File 'lib/mcollective/security/choria.rb', line 235 def validrequest?(secure_payload, request) return true if $choria_unsafe_disable_protocol_security # rubocop:disable Style/GlobalVars callerid = request["envelope"]["callerid"] if verify_signature(secure_payload["message"], secure_payload["signature"], callerid, true) Log.info("Received valid request %s from %s" % [request["envelope"]["requestid"], callerid]) @stats.validated else @stats.unvalidated raise(SecurityValidationFailed, "Received an invalid signature in message from %s" % callerid) end true end |
#verify_signature(string, signature, callerid, allow_privileged = false) ⇒ Object
Verifies a signature of a string using a certificate
Optionally should the signature validation fail - or the specified cert does not exist - the list of privileged user certs will be tried to validate the message and any of those can validate it
631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 |
# File 'lib/mcollective/security/choria.rb', line 631 def verify_signature(string, signature, callerid, allow_privileged=false) candidate_keys = [public_certfile(callerid)] candidate_keys.concat(privilegeduser_certs) if allow_privileged candidate_keys.each do |certname| next unless File.exist?(certname) key = OpenSSL::X509::Certificate.new(File.read(certname)).public_key result = key.verify(OpenSSL::Digest.new("SHA256"), Base64.decode64(signature), string) if result Log.debug("Message validated using certificate in %s (allow_privileged=%s)" % [certname, allow_privileged]) return true end end false end |