globiguard 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +7 -0
- data/CONTRIBUTING.md +14 -0
- data/LICENSE +106 -0
- data/README.md +46 -0
- data/SECURITY.md +15 -0
- data/lib/globiguard.rb +245 -0
- metadata +51 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: dfe6ca1d82d5c27cd8b501fbf9306911785035cd09384c41e081e1c286cac576
|
|
4
|
+
data.tar.gz: ae191efdeca8f3ca701fdfd999878bdb46e60e1ca473e87db8af0d997a678957
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 698b401af1ea2603753d2d99093bfc559a87b5c0b02ca30a5e59c0cb071f3a088d10ab2849a86d976fffcb35fc2049fb96d7896b88d72aaf96fd12492d1d0cd9
|
|
7
|
+
data.tar.gz: c0dd8138ca55a3575ab55df1e829a0660caf014ef09ff42e15639dc9c98d5f47b3c9072d015b2cdf054d501c40a706852b64db6de751d7bcfa1d0db8a4e07694
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
- Initial dependency-minimal Ruby SDK foundation.
|
|
6
|
+
- Added credential helpers, safe request transport, resource clients, governed action helpers, trust webhook verification, bootstrap helpers, and offline entitlement manifest verification through Ruby OpenSSL Ed25519 support.
|
|
7
|
+
|
data/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
GlobiGuard Ruby SDK changes should avoid gem runtime dependencies unless a security review accepts a specific exception.
|
|
4
|
+
|
|
5
|
+
## Validate locally
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
ruby -c lib/globiguard.rb
|
|
9
|
+
ruby test/smoke_test.rb
|
|
10
|
+
gem build globiguard.gemspec
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Examples must use placeholder secrets only and webhook handlers must pass raw request body bytes into verification.
|
|
14
|
+
|
data/LICENSE
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
+
|
|
7
|
+
1. Definitions.
|
|
8
|
+
|
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
10
|
+
and distribution as defined in Sections 1 through 9 of this document.
|
|
11
|
+
|
|
12
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
13
|
+
the copyright owner that is granting the License.
|
|
14
|
+
|
|
15
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
16
|
+
other entities that control, are controlled by, or are under common
|
|
17
|
+
control with that entity.
|
|
18
|
+
|
|
19
|
+
"You" (or "Your") shall mean an individual or Legal Entity exercising
|
|
20
|
+
permissions granted by this License.
|
|
21
|
+
|
|
22
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
23
|
+
including but not limited to software source code, documentation
|
|
24
|
+
source, and configuration files.
|
|
25
|
+
|
|
26
|
+
"Object" form shall mean any form resulting from mechanical
|
|
27
|
+
transformation or translation of a Source form.
|
|
28
|
+
|
|
29
|
+
"Work" shall mean the work of authorship, whether in Source or Object
|
|
30
|
+
form, made available under the License.
|
|
31
|
+
|
|
32
|
+
"Derivative Works" shall mean any work that is based on (or derived
|
|
33
|
+
from) the Work and for which the editorial revisions represent an
|
|
34
|
+
original work of authorship.
|
|
35
|
+
|
|
36
|
+
"Contribution" shall mean any work of authorship that is submitted
|
|
37
|
+
to, included in, or in any way used by Licensor, or incorporated
|
|
38
|
+
into, the Work.
|
|
39
|
+
|
|
40
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
41
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
42
|
+
subsequently incorporated within the Work.
|
|
43
|
+
|
|
44
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
45
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
46
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
47
|
+
copyright license to reproduce, prepare Derivative Works of, publicly
|
|
48
|
+
display, publicly perform, sublicense, and distribute the Work and
|
|
49
|
+
such Derivative Works in Source or Object form.
|
|
50
|
+
|
|
51
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
52
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
53
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
54
|
+
(except as stated in this section) patent license to make, have made,
|
|
55
|
+
use, offer to sell, sell, import, and otherwise transfer the Work.
|
|
56
|
+
|
|
57
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
58
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
59
|
+
modifications, and in Source or Object form, provided that You
|
|
60
|
+
meet the following conditions:
|
|
61
|
+
|
|
62
|
+
(a) You must give any other recipients of the Work or
|
|
63
|
+
Derivative Works a copy of this License; and
|
|
64
|
+
|
|
65
|
+
(b) You must cause any modified files to carry prominent notices
|
|
66
|
+
stating that You changed the files; and
|
|
67
|
+
|
|
68
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
69
|
+
that You distribute, all copyright, patent, trademark, and
|
|
70
|
+
attribution notices from the Source form of the Work.
|
|
71
|
+
|
|
72
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
73
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
74
|
+
by You to Licensor shall be under the terms and conditions of
|
|
75
|
+
this License, without any limitation.
|
|
76
|
+
|
|
77
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
78
|
+
names, trademarks, service marks, or product names of the Licensor
|
|
79
|
+
or its Subsidiaries or Licensors, except as required for reasonable
|
|
80
|
+
and customary use in describing the origin of the Work and
|
|
81
|
+
reproducing the content of the NOTICE file.
|
|
82
|
+
|
|
83
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
84
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
85
|
+
Contributor provides its Contributions) on an "AS-IS" BASIS,
|
|
86
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
|
|
87
|
+
or implied, including, without limitation, any warranties or
|
|
88
|
+
conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS
|
|
89
|
+
FOR A PARTICULAR PURPOSE.
|
|
90
|
+
|
|
91
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
92
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
93
|
+
unless required by applicable law (such as deliberate and grossly
|
|
94
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
95
|
+
liable to You for damages, including any direct, indirect, special,
|
|
96
|
+
incidental, or consequential damages of any character arising as a
|
|
97
|
+
result of this License or out of the use or inability to use the
|
|
98
|
+
Work.
|
|
99
|
+
|
|
100
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
101
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
102
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
103
|
+
or other liability obligations and/or rights consistent with this
|
|
104
|
+
License.
|
|
105
|
+
|
|
106
|
+
END OF TERMS AND CONDITIONS
|
data/README.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# globiguard-ruby
|
|
2
|
+
|
|
3
|
+
Official dependency-minimal Ruby SDK for GlobiGuard.
|
|
4
|
+
|
|
5
|
+
The package has no gem runtime dependencies. It uses Ruby stdlib for HTTP, HMAC, JSON, URL handling, and OpenSSL-backed Ed25519 entitlement verification where the installed Ruby/OpenSSL build supports Ed25519.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
gem install globiguard
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Server client
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
require "globiguard"
|
|
17
|
+
|
|
18
|
+
client = GlobiGuard::Client.server(
|
|
19
|
+
environment: "sandbox",
|
|
20
|
+
services: { "controlPlane" => "https://api.globiguard.com" },
|
|
21
|
+
credential: GlobiGuard::Credential.secret("proj_example", "ggsk_example_replace_me", "sandbox")
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
decision = client.governed_actions.authorize_action_or_throw(
|
|
25
|
+
actionType: "refund",
|
|
26
|
+
actor: { id: "user_123" },
|
|
27
|
+
target: { id: "order_456" }
|
|
28
|
+
)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Webhooks
|
|
32
|
+
|
|
33
|
+
Pass the exact raw request body string. Do not parse and re-serialize JSON before verification.
|
|
34
|
+
|
|
35
|
+
```ruby
|
|
36
|
+
result = GlobiGuard::TrustWebhook.verify(headers, raw_body, "whsec_example_replace_me")
|
|
37
|
+
raise result[:error] unless result[:ok]
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Development
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
ruby -c lib/globiguard.rb
|
|
44
|
+
ruby test/smoke_test.rb
|
|
45
|
+
gem build globiguard.gemspec
|
|
46
|
+
```
|
data/SECURITY.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
Report suspected vulnerabilities privately through GitHub security advisories for `globiguard/globiguard-ruby`.
|
|
4
|
+
|
|
5
|
+
## Supported versions
|
|
6
|
+
|
|
7
|
+
The `0.x` series receives security fixes while the SDK surface is stabilizing.
|
|
8
|
+
|
|
9
|
+
## Security expectations
|
|
10
|
+
|
|
11
|
+
- Do not log secret keys, webhook secrets, raw entitlement signing keys, or raw webhook bodies that may contain sensitive data.
|
|
12
|
+
- Always verify trust webhooks against the exact raw request body bytes.
|
|
13
|
+
- Use `sandbox` for tests and `live` only for production credentials issued by the GlobiGuard app.
|
|
14
|
+
- Keep gem runtime dependencies at zero unless a reviewed security need outweighs the supply-chain cost.
|
|
15
|
+
|
data/lib/globiguard.rb
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "base64"
|
|
4
|
+
require "json"
|
|
5
|
+
require "net/http"
|
|
6
|
+
require "openssl"
|
|
7
|
+
require "time"
|
|
8
|
+
require "uri"
|
|
9
|
+
|
|
10
|
+
module GlobiGuard
|
|
11
|
+
VERSION = "0.1.0"
|
|
12
|
+
ENVIRONMENTS = %w[local sandbox live].freeze
|
|
13
|
+
|
|
14
|
+
Credential = Struct.new(:kind, :project_id, :token, :environment, keyword_init: true) do
|
|
15
|
+
def self.secret(project_id, token, environment)
|
|
16
|
+
new(kind: "secret", project_id: project_id, token: token, environment: environment)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.publishable(project_id, token, environment)
|
|
20
|
+
new(kind: "publishable", project_id: project_id, token: token, environment: environment)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.local(token = nil)
|
|
24
|
+
new(kind: "local", project_id: nil, token: token, environment: "local")
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
class Client
|
|
29
|
+
attr_reader :transport
|
|
30
|
+
|
|
31
|
+
def self.server(environment:, services:, credential:)
|
|
32
|
+
raise ArgumentError, "Server clients require secret or local credentials." if credential.kind == "publishable"
|
|
33
|
+
new(environment: environment, services: services, credential: credential)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def self.browser(environment:, services:, credential:)
|
|
37
|
+
raise ArgumentError, "Browser clients cannot use secret credentials." if credential.kind == "secret"
|
|
38
|
+
new(environment: environment, services: services, credential: credential)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def initialize(environment:, services:, credential:)
|
|
42
|
+
@transport = Transport.new(environment: environment, services: services, credential: credential)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def actions = ResourceClient.new(@transport, "/v1/actions")
|
|
46
|
+
def audit = ResourceClient.new(@transport, "/v1/audit")
|
|
47
|
+
def installs = ResourceClient.new(@transport, "/v1/installs")
|
|
48
|
+
def orgs = ResourceClient.new(@transport, "/v1/orgs")
|
|
49
|
+
def policies = ResourceClient.new(@transport, "/v1/policies")
|
|
50
|
+
def queue = ResourceClient.new(@transport, "/v1/queue")
|
|
51
|
+
def workflows = ResourceClient.new(@transport, "/v1/workflows")
|
|
52
|
+
def governed_actions = GovernedActions.new(@transport)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
class Transport
|
|
56
|
+
RESERVED_HEADERS = %w[
|
|
57
|
+
x-globiguard-project-id
|
|
58
|
+
x-globiguard-secret-key
|
|
59
|
+
x-globiguard-publishable-key
|
|
60
|
+
x-globiguard-local-mode
|
|
61
|
+
x-globiguard-local-token
|
|
62
|
+
x-globiguard-client
|
|
63
|
+
x-globiguard-environment
|
|
64
|
+
].freeze
|
|
65
|
+
|
|
66
|
+
def initialize(environment:, services:, credential:)
|
|
67
|
+
raise ArgumentError, "Environment must be local, sandbox, or live." unless ENVIRONMENTS.include?(environment)
|
|
68
|
+
raise ArgumentError, "Credential environment must match client environment." unless credential.environment == environment
|
|
69
|
+
|
|
70
|
+
@environment = environment
|
|
71
|
+
@services = services
|
|
72
|
+
@credential = credential
|
|
73
|
+
@base_uri = URI(services.fetch("controlPlane"))
|
|
74
|
+
raise ArgumentError, "HTTPS is required outside local." if environment != "local" && @base_uri.scheme != "https"
|
|
75
|
+
if credential.kind == "local" && !%w[localhost 127.0.0.1 ::1].include?(@base_uri.host)
|
|
76
|
+
raise ArgumentError, "Local credentials require localhost or loopback URLs."
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def request(method, path, body: nil, headers: {})
|
|
81
|
+
self.class.validate_path(path)
|
|
82
|
+
headers.each_key do |name|
|
|
83
|
+
raise ArgumentError, "Reserved GlobiGuard header cannot be overridden: #{name}" if RESERVED_HEADERS.include?(name.downcase)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
uri = URI(@base_uri.to_s.sub(%r{/+\z}, "") + path)
|
|
87
|
+
request = Net::HTTP.const_get(method.capitalize).new(uri)
|
|
88
|
+
auth_headers.merge(headers).each { |name, value| request[name] = value }
|
|
89
|
+
request["content-type"] = "application/json" if body
|
|
90
|
+
request.body = JSON.generate(body) if body
|
|
91
|
+
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == "https", read_timeout: 30, open_timeout: 30) do |http|
|
|
92
|
+
http.request(request)
|
|
93
|
+
end
|
|
94
|
+
raise "GlobiGuard request failed with #{response.code}: #{response.body}" unless response.is_a?(Net::HTTPSuccess)
|
|
95
|
+
|
|
96
|
+
response.body.nil? || response.body.empty? ? {} : JSON.parse(response.body)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def auth_headers
|
|
100
|
+
headers = {
|
|
101
|
+
"x-globiguard-client" => "globiguard-ruby/#{VERSION}",
|
|
102
|
+
"x-globiguard-environment" => @environment
|
|
103
|
+
}
|
|
104
|
+
if @credential.kind == "local"
|
|
105
|
+
headers["x-globiguard-local-mode"] = "true"
|
|
106
|
+
headers["x-globiguard-local-token"] = @credential.token if @credential.token && !@credential.token.empty?
|
|
107
|
+
else
|
|
108
|
+
headers["x-globiguard-project-id"] = require_value(@credential.project_id, "project id")
|
|
109
|
+
headers[@credential.kind == "secret" ? "x-globiguard-secret-key" : "x-globiguard-publishable-key"] = require_value(@credential.token, "credential token")
|
|
110
|
+
end
|
|
111
|
+
headers
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def self.validate_path(path)
|
|
115
|
+
raise ArgumentError, "Request path must start with /." unless path.start_with?("/")
|
|
116
|
+
if path.start_with?("//") || path.include?("\\") || path.include?("?") || path.include?("#") || path.match?(/%(?![0-9A-Fa-f]{2})/)
|
|
117
|
+
raise ArgumentError, "Unsafe request path."
|
|
118
|
+
end
|
|
119
|
+
raise ArgumentError, "Absolute request paths are not allowed." if URI(path).absolute?
|
|
120
|
+
raise ArgumentError, "Dot segments are not allowed." if path.split("/").any? { |segment| segment == "." || segment == ".." }
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
private
|
|
124
|
+
|
|
125
|
+
def require_value(value, label)
|
|
126
|
+
raise ArgumentError, "Missing #{label}." if value.nil? || value.empty?
|
|
127
|
+
value
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
class ResourceClient
|
|
132
|
+
def initialize(transport, base_path)
|
|
133
|
+
@transport = transport
|
|
134
|
+
@base_path = base_path
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def list = @transport.request("get", @base_path)
|
|
138
|
+
def get(id) = @transport.request("get", "#{@base_path}/#{URI.encode_www_form_component(id)}")
|
|
139
|
+
def create(body) = @transport.request("post", @base_path, body: body)
|
|
140
|
+
def post(suffix, body) = @transport.request("post", "#{@base_path}/#{URI.encode_www_form_component(suffix.sub(%r{\A/+}, ""))}", body: body)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
class GovernedActions
|
|
144
|
+
def initialize(transport)
|
|
145
|
+
@transport = transport
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def authorize_action_or_throw(body, idempotency_key: nil, correlation_id: nil)
|
|
149
|
+
headers = {}
|
|
150
|
+
headers["Idempotency-Key"] = idempotency_key if idempotency_key
|
|
151
|
+
headers["x-correlation-id"] = correlation_id if correlation_id
|
|
152
|
+
result = @transport.request("post", "/v1/actions/authorize", body: body, headers: headers)
|
|
153
|
+
raise "GlobiGuard blocked the governed action." if result["decision"] == "BLOCK"
|
|
154
|
+
result
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
module TrustWebhook
|
|
159
|
+
module_function
|
|
160
|
+
|
|
161
|
+
def verify(headers, raw_body, signing_secret, tolerance_seconds: 300)
|
|
162
|
+
normalized = headers.transform_keys { |key| key.to_s.downcase }
|
|
163
|
+
delivery = normalized["x-globiguard-delivery-id"]
|
|
164
|
+
timestamp = normalized["x-globiguard-timestamp"]
|
|
165
|
+
event_type = normalized["x-globiguard-event-type"]
|
|
166
|
+
signature = normalized["x-globiguard-signature"]
|
|
167
|
+
return { ok: false, error: "Missing required webhook headers." } unless delivery && timestamp && event_type && signature
|
|
168
|
+
return { ok: false, error: "Webhook timestamp is outside the replay window." } if (Time.now.to_i - timestamp.to_i).abs > tolerance_seconds
|
|
169
|
+
|
|
170
|
+
signed = "globiguard-hmac-sha256-v1.#{delivery}.#{timestamp}.#{event_type}.#{raw_body}"
|
|
171
|
+
expected = "v1=#{OpenSSL::HMAC.hexdigest("SHA256", signing_secret, signed)}"
|
|
172
|
+
return { ok: false, error: "Invalid webhook signature." } unless secure_compare(expected, signature)
|
|
173
|
+
|
|
174
|
+
{ ok: true, envelope: JSON.parse(raw_body) }
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def secure_compare(left, right)
|
|
178
|
+
return false unless left.bytesize == right.bytesize
|
|
179
|
+
left.bytes.zip(right.bytes).reduce(0) { |memo, pair| memo | (pair[0] ^ pair[1]) }.zero?
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
module Bootstrap
|
|
184
|
+
module_function
|
|
185
|
+
|
|
186
|
+
def install_registration(profile, package_name:, package_version:, integration_kind:, runtime_kind:)
|
|
187
|
+
validate_profile(profile)
|
|
188
|
+
{
|
|
189
|
+
environment: profile.fetch(:environment),
|
|
190
|
+
deploymentMode: profile.fetch(:deploymentMode),
|
|
191
|
+
issuerMode: profile.fetch(:issuerMode),
|
|
192
|
+
installReporting: profile.fetch(:installReporting),
|
|
193
|
+
installLabel: profile[:installLabel],
|
|
194
|
+
package: { name: package_name, version: package_version },
|
|
195
|
+
integration: { kind: integration_kind, runtime: runtime_kind }
|
|
196
|
+
}
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def validate_profile(profile)
|
|
200
|
+
raise ArgumentError, "Invalid environment." unless ENVIRONMENTS.include?(profile[:environment])
|
|
201
|
+
if profile[:deploymentMode] == "hosted" && profile[:issuerMode] != "globiguard_issued"
|
|
202
|
+
raise ArgumentError, "Hosted deployments require globiguard_issued issuer mode."
|
|
203
|
+
end
|
|
204
|
+
return unless %w[self_hosted sovereign].include?(profile[:deploymentMode])
|
|
205
|
+
|
|
206
|
+
raise ArgumentError, "Self-hosted and sovereign deployments require customer_issued issuer mode." unless profile[:issuerMode] == "customer_issued"
|
|
207
|
+
raise ArgumentError, "Self-hosted and sovereign install reporting must be opt_in or disabled." unless %w[opt_in disabled].include?(profile[:installReporting])
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
module Entitlements
|
|
212
|
+
module_function
|
|
213
|
+
|
|
214
|
+
def verify_signed_manifest(compact_jws, public_keys_by_id:)
|
|
215
|
+
parts = compact_jws.split(".")
|
|
216
|
+
raise ArgumentError, "Entitlement manifest must be compact JWS." unless parts.length == 3
|
|
217
|
+
header = JSON.parse(base64url_decode(parts[0]))
|
|
218
|
+
raise ArgumentError, "Entitlement manifest must use EdDSA." unless header["alg"] == "EdDSA"
|
|
219
|
+
public_key = public_keys_by_id.fetch(header.fetch("kid"))
|
|
220
|
+
signing_input = "#{parts[0]}.#{parts[1]}"
|
|
221
|
+
signature = base64url_decode(parts[2])
|
|
222
|
+
verify_ed25519!(base64url_decode(public_key), signing_input, signature)
|
|
223
|
+
payload = JSON.parse(base64url_decode(parts[1]))
|
|
224
|
+
raise ArgumentError, "Unsupported entitlement manifest schema." unless payload["schema"] == "globiguard.entitlement_manifest.v1"
|
|
225
|
+
now = Time.now.to_i
|
|
226
|
+
raise ArgumentError, "Entitlement manifest is not active yet." if payload["nbf"] && payload["nbf"].to_i > now
|
|
227
|
+
raise ArgumentError, "Entitlement manifest is expired." if payload["exp"] && payload["exp"].to_i <= now
|
|
228
|
+
payload
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def verify_ed25519!(raw_public_key, signing_input, signature)
|
|
232
|
+
prefix = [48, 42, 48, 5, 6, 3, 43, 101, 112, 3, 33, 0].pack("C*")
|
|
233
|
+
key = OpenSSL::PKey.read(prefix + raw_public_key)
|
|
234
|
+
verified = key.verify(nil, signature, signing_input)
|
|
235
|
+
raise OpenSSL::PKey::PKeyError, "Invalid entitlement manifest signature." unless verified
|
|
236
|
+
rescue NoMethodError, OpenSSL::PKey::PKeyError => e
|
|
237
|
+
raise OpenSSL::PKey::PKeyError, "Ruby/OpenSSL Ed25519 verification is unavailable or failed: #{e.message}"
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def base64url_decode(value)
|
|
241
|
+
Base64.urlsafe_decode64(value + ("=" * ((4 - value.length % 4) % 4)))
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
|
metadata
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: globiguard
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- GlobiGuard
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-05-29 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description: Dependency-minimal Ruby SDK for GlobiGuard trusted access workflows.
|
|
14
|
+
email:
|
|
15
|
+
executables: []
|
|
16
|
+
extensions: []
|
|
17
|
+
extra_rdoc_files: []
|
|
18
|
+
files:
|
|
19
|
+
- CHANGELOG.md
|
|
20
|
+
- CONTRIBUTING.md
|
|
21
|
+
- LICENSE
|
|
22
|
+
- README.md
|
|
23
|
+
- SECURITY.md
|
|
24
|
+
- lib/globiguard.rb
|
|
25
|
+
homepage: https://globiguard.com
|
|
26
|
+
licenses:
|
|
27
|
+
- Apache-2.0
|
|
28
|
+
metadata:
|
|
29
|
+
homepage_uri: https://globiguard.com
|
|
30
|
+
source_code_uri: https://github.com/globiguard/globiguard-ruby
|
|
31
|
+
bug_tracker_uri: https://github.com/globiguard/globiguard-ruby/issues
|
|
32
|
+
post_install_message:
|
|
33
|
+
rdoc_options: []
|
|
34
|
+
require_paths:
|
|
35
|
+
- lib
|
|
36
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - ">="
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '3.1'
|
|
41
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
42
|
+
requirements:
|
|
43
|
+
- - ">="
|
|
44
|
+
- !ruby/object:Gem::Version
|
|
45
|
+
version: '0'
|
|
46
|
+
requirements: []
|
|
47
|
+
rubygems_version: 3.4.19
|
|
48
|
+
signing_key:
|
|
49
|
+
specification_version: 4
|
|
50
|
+
summary: Official dependency-minimal Ruby SDK for GlobiGuard.
|
|
51
|
+
test_files: []
|