selfsdk 0.0.124
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/lib/acl.rb +52 -0
- data/lib/authenticated.rb +26 -0
- data/lib/client.rb +95 -0
- data/lib/jwt_service.rb +91 -0
- data/lib/log.rb +15 -0
- data/lib/messages/attestation.rb +53 -0
- data/lib/messages/authentication_message.rb +25 -0
- data/lib/messages/authentication_req.rb +52 -0
- data/lib/messages/authentication_resp.rb +23 -0
- data/lib/messages/base.rb +89 -0
- data/lib/messages/fact.rb +55 -0
- data/lib/messages/fact_request.rb +112 -0
- data/lib/messages/fact_response.rb +91 -0
- data/lib/messages/message.rb +39 -0
- data/lib/messaging.rb +441 -0
- data/lib/ntptime.rb +51 -0
- data/lib/proto/acl_pb.rb +19 -0
- data/lib/proto/aclcommand_pb.rb +16 -0
- data/lib/proto/auth_pb.rb +19 -0
- data/lib/proto/errtype_pb.rb +19 -0
- data/lib/proto/header_pb.rb +16 -0
- data/lib/proto/message_pb.rb +22 -0
- data/lib/proto/msgtype_pb.rb +18 -0
- data/lib/proto/notification_pb.rb +19 -0
- data/lib/selfsdk.rb +109 -0
- data/lib/services/auth.rb +163 -0
- data/lib/services/facts.rb +138 -0
- data/lib/services/identity.rb +44 -0
- data/lib/services/messaging.rb +100 -0
- data/lib/sources.rb +78 -0
- metadata +354 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: df6729e3320be7cc2abd4ea22a94ff757b8e8d72ee5e1f2b2ddbafed509483c4
|
4
|
+
data.tar.gz: c7202790ad7ac8c0fcb984a3c494983b08549e9e516b238cc247d869a630d2a2
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 860606a05e95e553e6c1e36c63b15a1e3b93922d56e0a58a773d911da0ea29d95f5395406b13124b005e05c468d1c2740308e55908decffc77eb79d6c569ac14
|
7
|
+
data.tar.gz: 7a3ba36d29276730966cc01ce4d2730c0613142ce6d844deb61380a3b3055596e724bfe78160061436cf43f9f510b49b7407a3a9eb107e0e057c094f9d8e7f32
|
data/lib/acl.rb
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'date'
|
4
|
+
|
5
|
+
# Namespace for classes and modules that handle Self interactions.
|
6
|
+
module SelfSDK
|
7
|
+
# Access control list
|
8
|
+
class ACL
|
9
|
+
def initialize(messaging)
|
10
|
+
@messaging = messaging
|
11
|
+
@jwt = @messaging.jwt
|
12
|
+
end
|
13
|
+
|
14
|
+
# Lists allowed connections.
|
15
|
+
def list
|
16
|
+
SelfSDK.logger.info "Listing allowed connections"
|
17
|
+
rules = {}
|
18
|
+
@messaging.list_acl_rules.each do |c|
|
19
|
+
rules[c['acl_source']] = DateTime.parse(c['acl_exp'])
|
20
|
+
end
|
21
|
+
rules
|
22
|
+
end
|
23
|
+
|
24
|
+
# Allows incomming messages from the given identity.
|
25
|
+
def allow(id)
|
26
|
+
SelfSDK.logger.info "Allowing connections from #{id}"
|
27
|
+
@messaging.add_acl_rule(@jwt.prepare(jti: SecureRandom.uuid,
|
28
|
+
cid: SecureRandom.uuid,
|
29
|
+
typ: 'acl.permit',
|
30
|
+
iss: @jwt.id,
|
31
|
+
sub: @jwt.id,
|
32
|
+
iat: (SelfSDK::Time.now - 5).strftime('%FT%TZ'),
|
33
|
+
exp: (SelfSDK::Time.now + 60).strftime('%FT%TZ'),
|
34
|
+
acl_source: id,
|
35
|
+
acl_exp: (SelfSDK::Time.now + 360_000).to_datetime.rfc3339))
|
36
|
+
end
|
37
|
+
|
38
|
+
# Deny incomming messages from the given identity.
|
39
|
+
def deny(id)
|
40
|
+
SelfSDK.logger.info "Denying connections from #{id}"
|
41
|
+
@messaging.remove_acl_rule(@jwt.prepare(jti: SecureRandom.uuid,
|
42
|
+
cid: SecureRandom.uuid,
|
43
|
+
typ: 'acl.revoke',
|
44
|
+
iss: @jwt.id,
|
45
|
+
sub: @jwt.id,
|
46
|
+
iat: (SelfSDK::Time.now - 5).strftime('%FT%TZ'),
|
47
|
+
exp: (SelfSDK::Time.now + 60).strftime('%FT%TZ'),
|
48
|
+
acl_source: id,
|
49
|
+
acl_exp: (SelfSDK::Time.now + 360_000).to_datetime.rfc3339))
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module SelfSDK
|
2
|
+
class Authenticated
|
3
|
+
attr_accessor :payload, :uuid, :selfsdk, :status
|
4
|
+
|
5
|
+
def initialize(payload)
|
6
|
+
return if payload.nil?
|
7
|
+
|
8
|
+
@payload = payload
|
9
|
+
@uuid = payload[:cid]
|
10
|
+
@selfsdk = payload[:sub]
|
11
|
+
end
|
12
|
+
|
13
|
+
def accepted?
|
14
|
+
return false if @payload.nil?
|
15
|
+
|
16
|
+
@payload[:status] == "accepted"
|
17
|
+
end
|
18
|
+
|
19
|
+
def to_hash
|
20
|
+
{ uuid: @uuid,
|
21
|
+
selfsdk: @selfsdk,
|
22
|
+
accepted: accepted? }
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
data/lib/client.rb
ADDED
@@ -0,0 +1,95 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'httparty'
|
4
|
+
|
5
|
+
module SelfSDK
|
6
|
+
class RestClient
|
7
|
+
attr_reader :self_url, :jwt, :env
|
8
|
+
|
9
|
+
# RestClient initializer
|
10
|
+
#
|
11
|
+
# @param url [string] self-messaging url
|
12
|
+
# @param token [string] jwt token identifying the authenticated user
|
13
|
+
def initialize(url, app_id, app_key, env)
|
14
|
+
SelfSDK.logger.info "client setup with #{url}"
|
15
|
+
@self_url = url
|
16
|
+
@env = env
|
17
|
+
@jwt = SelfSDK::JwtService.new(app_id, app_key)
|
18
|
+
end
|
19
|
+
|
20
|
+
# Get identity details
|
21
|
+
#
|
22
|
+
# @param id [string] identity self_id.
|
23
|
+
def identity(id)
|
24
|
+
get_identity "/v1/identities/#{id}"
|
25
|
+
end
|
26
|
+
|
27
|
+
# Get app details
|
28
|
+
#
|
29
|
+
# @param id [string] app self_id.
|
30
|
+
def app(id)
|
31
|
+
get_identity "/v1/apps/#{id}"
|
32
|
+
end
|
33
|
+
|
34
|
+
# Get app/identity details
|
35
|
+
#
|
36
|
+
# @param id [string] app/identity self_id.
|
37
|
+
def entity(id)
|
38
|
+
#TODO : Consider a better check for this conditional
|
39
|
+
if id.length == 11
|
40
|
+
return identity(id)
|
41
|
+
else
|
42
|
+
return app(id)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Lists all devices assigned to the given identity
|
47
|
+
#
|
48
|
+
# @param id [string] identity id
|
49
|
+
def devices(id)
|
50
|
+
res = get "/v1/identities/#{id}/devices"
|
51
|
+
body = JSON.parse(res.body, symbolize_names: true)
|
52
|
+
if res.code != 200
|
53
|
+
SelfSDK.logger.error "identity response : #{body[:message]}"
|
54
|
+
raise "you need connection permissions"
|
55
|
+
end
|
56
|
+
body
|
57
|
+
end
|
58
|
+
|
59
|
+
# Lists all public keys stored on self for the given ID
|
60
|
+
#
|
61
|
+
# @param id [string] identity id
|
62
|
+
def public_keys(id)
|
63
|
+
i = entity(id)
|
64
|
+
i[:public_keys]
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
def get_identity(endpoint)
|
70
|
+
res = get endpoint
|
71
|
+
body = JSON.parse(res.body, symbolize_names: true)
|
72
|
+
if res.code != 200
|
73
|
+
SelfSDK.logger.error "app response : #{body[:message]}"
|
74
|
+
raise body[:message]
|
75
|
+
end
|
76
|
+
body
|
77
|
+
end
|
78
|
+
|
79
|
+
def get(endpoint)
|
80
|
+
HTTParty.get("#{@self_url}#{endpoint}", headers: {
|
81
|
+
'Content-Type' => 'application/json',
|
82
|
+
'Authorization' => "Bearer #{@jwt.auth_token}"
|
83
|
+
})
|
84
|
+
end
|
85
|
+
|
86
|
+
def post(endpoint, body)
|
87
|
+
HTTParty.post("#{@self_url}#{endpoint}",
|
88
|
+
headers: {
|
89
|
+
'Content-Type' => 'application/json',
|
90
|
+
'Authorization' => "Bearer #{@jwt.auth_token}"
|
91
|
+
},
|
92
|
+
body: body)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
data/lib/jwt_service.rb
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'base64'
|
4
|
+
require 'json'
|
5
|
+
|
6
|
+
module SelfSDK
|
7
|
+
class JwtService
|
8
|
+
attr_reader :id, :key
|
9
|
+
|
10
|
+
# Jwt initializer
|
11
|
+
#
|
12
|
+
# @param app_id [string] the app id.
|
13
|
+
# @param app_key [string] the app api key provided by developer portal.
|
14
|
+
def initialize(app_id, app_key)
|
15
|
+
@id = app_id
|
16
|
+
@key = app_key
|
17
|
+
end
|
18
|
+
|
19
|
+
# Prepares a jwt object based on an input
|
20
|
+
#
|
21
|
+
# @param input [string] input to be prepared
|
22
|
+
def prepare(input)
|
23
|
+
signed(input).to_json
|
24
|
+
end
|
25
|
+
|
26
|
+
def signed(input)
|
27
|
+
payload = encode(input.to_json)
|
28
|
+
{
|
29
|
+
payload: payload,
|
30
|
+
protected: header,
|
31
|
+
signature: sign("#{header}.#{payload}")
|
32
|
+
}
|
33
|
+
end
|
34
|
+
|
35
|
+
def parse(input)
|
36
|
+
JSON.parse(input, symbolize_names: true)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Encodes the input with base64
|
40
|
+
#
|
41
|
+
# @param input [string] the string to be encoded.
|
42
|
+
def encode(input)
|
43
|
+
Base64.urlsafe_encode64(input, padding: false)
|
44
|
+
end
|
45
|
+
|
46
|
+
# Base64 decodes the input string
|
47
|
+
#
|
48
|
+
# @param input [string] the string to be decoded.
|
49
|
+
def decode(input)
|
50
|
+
Base64.urlsafe_decode64(input)
|
51
|
+
end
|
52
|
+
|
53
|
+
# Signs the given input with the configured Ed25519 key.
|
54
|
+
#
|
55
|
+
# @param input [string] the string to be signed.
|
56
|
+
def sign(input)
|
57
|
+
signing_key = Ed25519::SigningKey.new(decode(@key))
|
58
|
+
signature = signing_key.sign(input)
|
59
|
+
encode(signature)
|
60
|
+
end
|
61
|
+
|
62
|
+
def verify(payload, key)
|
63
|
+
verify_key = Ed25519::VerifyKey.new(decode(key))
|
64
|
+
if verify_key.verify(decode(payload[:signature]), "#{payload[:protected]}.#{payload[:payload]}")
|
65
|
+
return true
|
66
|
+
end
|
67
|
+
rescue StandardError
|
68
|
+
false
|
69
|
+
end
|
70
|
+
|
71
|
+
# Generates the auth_token based on the app's private key.
|
72
|
+
def auth_token
|
73
|
+
payload = header + "." + encode({
|
74
|
+
jti: SecureRandom.uuid,
|
75
|
+
cid: SecureRandom.uuid,
|
76
|
+
typ: 'auth.token',
|
77
|
+
iat: (SelfSDK::Time.now - 5).to_i,
|
78
|
+
exp: (SelfSDK::Time.now + 60).to_i,
|
79
|
+
iss: @id,
|
80
|
+
sub: @id}.to_json)
|
81
|
+
signature = sign(payload)
|
82
|
+
"#{payload}.#{signature}"
|
83
|
+
end
|
84
|
+
|
85
|
+
private
|
86
|
+
|
87
|
+
def header
|
88
|
+
encode({ alg: "EdDSA", typ: "JWT" }.to_json)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
data/lib/log.rb
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SelfSDK
|
4
|
+
module Messages
|
5
|
+
class Attestation
|
6
|
+
attr_accessor :verified, :origin, :source, :value, :operator, :expected_value, :fact_name, :to, :audience
|
7
|
+
|
8
|
+
def initialize(messaging)
|
9
|
+
@messaging = messaging
|
10
|
+
end
|
11
|
+
|
12
|
+
def parse(name, attestation)
|
13
|
+
payload = JSON.parse(@messaging.jwt.decode(attestation[:payload]), symbolize_names: true)
|
14
|
+
@origin = payload[:iss]
|
15
|
+
@to = payload[:sub]
|
16
|
+
@audience = payload[:aud]
|
17
|
+
@source = payload[:source]
|
18
|
+
@verified = valid_signature?(attestation)
|
19
|
+
@expected_value = payload[:expected_value]
|
20
|
+
@operator = payload[:operator]
|
21
|
+
@fact_name = name.to_s
|
22
|
+
unless payload[name].nil?
|
23
|
+
@value = payload[name]
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def valid_signature?(body)
|
28
|
+
k = @messaging.client.public_keys(@origin).first[:key]
|
29
|
+
raise ::StandardError.new("invalid signature") unless @messaging.jwt.verify(body, k)
|
30
|
+
|
31
|
+
true
|
32
|
+
end
|
33
|
+
|
34
|
+
def validate!(original)
|
35
|
+
raise ::StandardError.new("invalid origin") if @to != original.to
|
36
|
+
end
|
37
|
+
|
38
|
+
def signed
|
39
|
+
o = {
|
40
|
+
sub: @to,
|
41
|
+
iss: @origin,
|
42
|
+
source: @source,
|
43
|
+
fact: @fact_name,
|
44
|
+
expected_value: @expected_value,
|
45
|
+
operator: @operator,
|
46
|
+
}
|
47
|
+
o[:aud] = @audience unless @audience.nil?
|
48
|
+
o[@fact_name.to_sym] = @value
|
49
|
+
@messaging.jwt.signed(o)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'base'
|
4
|
+
require_relative '../ntptime'
|
5
|
+
|
6
|
+
module SelfSDK
|
7
|
+
module Messages
|
8
|
+
class AuthenticationMessage < Base
|
9
|
+
|
10
|
+
def parse(input, original=nil)
|
11
|
+
@input = input
|
12
|
+
@typ = @typ
|
13
|
+
@payload = get_payload input
|
14
|
+
@id = payload[:cid]
|
15
|
+
@from = payload[:iss]
|
16
|
+
@to = payload[:sub]
|
17
|
+
@audience = payload[:aud]
|
18
|
+
@from_device = payload[:device_id]
|
19
|
+
@expires = ::Time.parse(payload[:exp])
|
20
|
+
@issued = ::Time.parse(payload[:iat])
|
21
|
+
@status = payload[:status]
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'base'
|
4
|
+
require_relative '../ntptime'
|
5
|
+
require_relative 'authentication_message'
|
6
|
+
|
7
|
+
module SelfSDK
|
8
|
+
module Messages
|
9
|
+
class AuthenticationReq < AuthenticationMessage
|
10
|
+
MSG_TYPE = "identities.authenticate.req"
|
11
|
+
DEFAULT_EXP_TIMEOUT = 300
|
12
|
+
|
13
|
+
def initialize(messaging)
|
14
|
+
@typ = MSG_TYPE
|
15
|
+
super
|
16
|
+
end
|
17
|
+
|
18
|
+
def populate(selfid, opts)
|
19
|
+
@id = SecureRandom.uuid
|
20
|
+
@from = @client.jwt.id
|
21
|
+
@to = selfid
|
22
|
+
|
23
|
+
@id = opts[:cid] if opts.include?(:cid)
|
24
|
+
@description = opts.include?(:description) ? opts[:description] : nil
|
25
|
+
@exp_timeout = opts.fetch(:exp_timeout, DEFAULT_EXP_TIMEOUT)
|
26
|
+
end
|
27
|
+
|
28
|
+
def body
|
29
|
+
{ typ: MSG_TYPE,
|
30
|
+
iss: @jwt.id,
|
31
|
+
sub: @to,
|
32
|
+
aud: @to,
|
33
|
+
iat: SelfSDK::Time.now.strftime('%FT%TZ'),
|
34
|
+
exp: (SelfSDK::Time.now + @exp_timeout).strftime('%FT%TZ'),
|
35
|
+
cid: @id,
|
36
|
+
jti: SecureRandom.uuid }
|
37
|
+
end
|
38
|
+
|
39
|
+
protected
|
40
|
+
|
41
|
+
def proto
|
42
|
+
@to_device = @client.devices(@to).first
|
43
|
+
Msgproto::Message.new(type: Msgproto::MsgType::MSG,
|
44
|
+
sender: "#{@jwt.id}:#{@messaging.device_id}",
|
45
|
+
id: @id,
|
46
|
+
recipient: "#{@to}:#{@to_device}",
|
47
|
+
ciphertext: @jwt.prepare(body))
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'base'
|
4
|
+
require_relative '../ntptime'
|
5
|
+
require_relative 'authentication_message'
|
6
|
+
|
7
|
+
module SelfSDK
|
8
|
+
module Messages
|
9
|
+
class AuthenticationResp < AuthenticationMessage
|
10
|
+
MSG_TYPE = "identities.authenticate.resp"
|
11
|
+
def initialize(messaging)
|
12
|
+
@typ = MSG_TYPE
|
13
|
+
super
|
14
|
+
end
|
15
|
+
|
16
|
+
protected
|
17
|
+
|
18
|
+
def proto
|
19
|
+
nil
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|