Module: GlobiGuard::Entitlements
- Defined in:
- lib/globiguard.rb
Constant Summary collapse
- MANIFEST_TYPE =
"globiguard.entitlement.v1"
Class Method Summary collapse
- .base64url_decode(value) ⇒ Object
- .decode_object(encoded, label) ⇒ Object
- .expect(expected, actual, label) ⇒ Object
- .nullable_counter(parent, field) ⇒ Object
- .required_hash(parent, field) ⇒ Object
- .required_string(parent, field) ⇒ Object
- .timestamp(parent, field) ⇒ Object
- .validate_payload(payload) ⇒ Object
- .verify_ed25519!(raw_public_key, signing_input, signature) ⇒ Object
- .verify_signed_manifest(compact_jws, public_keys_by_id:, expected_issuer: nil, expected_org_id: nil, expected_project_id: nil, expected_environment: nil, expected_deployment_mode: nil, now: Time.now) ⇒ Object
Class Method Details
.base64url_decode(value) ⇒ Object
493 494 495 |
# File 'lib/globiguard.rb', line 493 def base64url_decode(value) Base64.urlsafe_decode64(value + ("=" * ((4 - value.length % 4) % 4))) end |
.decode_object(encoded, label) ⇒ Object
439 440 441 442 443 444 445 446 |
# File 'lib/globiguard.rb', line 439 def decode_object(encoded, label) value = JSON.parse(base64url_decode(encoded)) raise ArgumentError, "Entitlement manifest #{label} must be a JSON object." unless value.is_a?(Hash) value rescue JSON::ParserError, ArgumentError => e raise e if e..start_with?("Entitlement manifest") raise ArgumentError, "Invalid entitlement manifest #{label}." end |
.expect(expected, actual, label) ⇒ Object
476 477 478 479 480 481 482 |
# File 'lib/globiguard.rb', line 476 def expect(expected, actual, label) return if expected.nil? expected_digest = OpenSSL::Digest::SHA256.digest(expected.to_s) actual_digest = OpenSSL::Digest::SHA256.digest(actual.to_s) return if OpenSSL.fixed_length_secure_compare(expected_digest, actual_digest) raise ArgumentError, "Entitlement manifest #{label} does not match the expected value." end |
.nullable_counter(parent, field) ⇒ Object
469 470 471 472 473 474 |
# File 'lib/globiguard.rb', line 469 def nullable_counter(parent, field) raise ArgumentError, "Entitlement manifest field #{field} is required." unless parent.key?(field) value = parent[field] return if value.nil? || (value.is_a?(Integer) && value >= 0) raise ArgumentError, "Entitlement manifest field #{field} must be nil or a non-negative integer." end |
.required_hash(parent, field) ⇒ Object
448 449 450 451 452 |
# File 'lib/globiguard.rb', line 448 def required_hash(parent, field) value = parent[field] raise ArgumentError, "Entitlement manifest field #{field} must be an object." unless value.is_a?(Hash) value end |
.required_string(parent, field) ⇒ Object
454 455 456 457 458 459 460 |
# File 'lib/globiguard.rb', line 454 def required_string(parent, field) value = parent[field] unless value.is_a?(String) && !value.strip.empty? raise ArgumentError, "Entitlement manifest field #{field} must be a non-empty string." end value end |
.timestamp(parent, field) ⇒ Object
462 463 464 465 466 467 |
# File 'lib/globiguard.rb', line 462 def (parent, field) Time.iso8601(required_string(parent, field)) rescue ArgumentError => e raise e if e..start_with?("Entitlement manifest field") raise ArgumentError, "Entitlement manifest field #{field} must be an ISO timestamp." end |
.validate_payload(payload) ⇒ Object
405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 |
# File 'lib/globiguard.rb', line 405 def validate_payload(payload) unless payload["manifestType"] == MANIFEST_TYPE && payload["manifestVersion"] == 1 raise ArgumentError, "Unsupported entitlement manifest payload." end %w[manifestId issuer issuedAt notBefore expiresAt].each { |field| required_string(payload, field) } subject = required_hash(payload, "subject") %w[orgId workspaceName orgSlug projectId projectSlug].each { |field| required_string(subject, field) } unless %w[sandbox live].include?(required_string(subject, "environment")) raise ArgumentError, "Entitlement manifest subject environment is invalid." end unless %w[self_hosted sovereign].include?(required_string(subject, "deploymentMode")) raise ArgumentError, "Entitlement manifest subject deployment mode is invalid." end commercial = required_hash(payload, "commercial") unless %w[FREE STARTER GROWTH SCALE ENTERPRISE].include?(required_string(commercial, "commercialPlan")) raise ArgumentError, "Entitlement manifest commercial plan is invalid." end unless %w[FREE PILOT ACTIVE GRACE PAST_DUE SUSPENDED CANCELED].include?(required_string(commercial, "billingStatus")) raise ArgumentError, "Entitlement manifest billing status is invalid." end unless [true, false].include?(commercial["pilotActive"]) raise ArgumentError, "Entitlement manifest pilotActive must be boolean." end entitlements = required_hash(payload, "entitlements") nullable_counter(entitlements, "includedQueriesPerMonth") nullable_counter(entitlements, "frameworkSlots") unless %w[NONE METERED CONTRACT].include?(required_string(entitlements, "overageMode")) raise ArgumentError, "Entitlement manifest overage mode is invalid." end end |
.verify_ed25519!(raw_public_key, signing_input, signature) ⇒ Object
484 485 486 487 488 489 490 491 |
# File 'lib/globiguard.rb', line 484 def verify_ed25519!(raw_public_key, signing_input, signature) prefix = [48, 42, 48, 5, 6, 3, 43, 101, 112, 3, 33, 0].pack("C*") key = OpenSSL::PKey.read(prefix + raw_public_key) verified = key.verify(nil, signature, signing_input) raise OpenSSL::PKey::PKeyError, "Invalid entitlement manifest signature." unless verified rescue NoMethodError, OpenSSL::PKey::PKeyError => e raise OpenSSL::PKey::PKeyError, "Ruby/OpenSSL Ed25519 verification is unavailable or failed: #{e.}" end |
.verify_signed_manifest(compact_jws, public_keys_by_id:, expected_issuer: nil, expected_org_id: nil, expected_project_id: nil, expected_environment: nil, expected_deployment_mode: nil, now: Time.now) ⇒ Object
360 361 362 363 364 365 366 367 368 369 370 371 372 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 401 402 403 |
# File 'lib/globiguard.rb', line 360 def verify_signed_manifest( compact_jws, public_keys_by_id:, expected_issuer: nil, expected_org_id: nil, expected_project_id: nil, expected_environment: nil, expected_deployment_mode: nil, now: Time.now ) parts = compact_jws.split(".") raise ArgumentError, "Entitlement manifest must be compact JWS." unless parts.length == 3 header = decode_object(parts[0], "protected header") unless header["alg"] == "EdDSA" && header["typ"] == MANIFEST_TYPE raise ArgumentError, "Unsupported entitlement manifest protected header." end key_id = required_string(header, "kid") public_key = public_keys_by_id.fetch(key_id) do raise ArgumentError, "Unknown entitlement signing key." end signing_input = "#{parts[0]}.#{parts[1]}" signature = base64url_decode(parts[2]) verify_ed25519!(base64url_decode(public_key), signing_input, signature) payload = decode_object(parts[1], "payload") validate_payload(payload) issued_at = (payload, "issuedAt") not_before = (payload, "notBefore") expires_at = (payload, "expiresAt") verification_time = now.is_a?(Time) ? now : Time.iso8601(now.to_s) raise ArgumentError, "Entitlement manifest timestamps are inconsistent." if issued_at > expires_at || not_before >= expires_at raise ArgumentError, "Entitlement manifest is not active yet." if not_before > verification_time raise ArgumentError, "Entitlement manifest is expired." if expires_at <= verification_time subject = payload.fetch("subject") expect(expected_issuer, payload.fetch("issuer"), "issuer") expect(expected_org_id, subject.fetch("orgId"), "organization") expect(expected_project_id, subject.fetch("projectId"), "project") expect(expected_environment, subject.fetch("environment"), "environment") expect(expected_deployment_mode, subject.fetch("deploymentMode"), "deployment mode") payload rescue Date::Error raise ArgumentError, "Entitlement verification now must be an ISO timestamp." end |