Skip to content

Commit a9c30b7

Browse files
committed
SDK-442: Add anchors to new profile class
1 parent fd4c657 commit a9c30b7

16 files changed

+607
-391
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ pip install yoti
8181
You can reference the project URL by adding the following import:
8282

8383
```python
84-
import "yoti_python_sdk"
84+
import yoti_python_sdk
8585
```
8686

8787
## Configuration

requirements.in

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1+
asn1==2.2.0
12
click==6.6
2-
cryptography>=1.5.2
3+
cryptography>=2.2.1
34
Django==1.11
45
flask==0.11.1
56
itsdangerous==0.24
@@ -8,8 +9,9 @@ MarkupSafe==0.23
89
mock==2.0.0
910
pbr==1.10.0
1011
protobuf==3.1.0.post1
12+
pyopenssl==18.0.0
1113
pytest==3.3.2
12-
requests==2.11.1
14+
requests>=2.11.1
1315
six==1.10.0
1416
tox>=1.7.2
1517
virtualenv==13.1.2

requirements.txt

Lines changed: 35 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,35 @@
1-
#
2-
# This file is autogenerated by pip-compile
3-
# To update, run:
4-
#
5-
# pip-compile --output-file requirements.txt requirements.in
6-
#
7-
attrs==18.1.0 # via pytest
8-
cffi==1.11.5 # via cryptography
9-
click==6.6
10-
colorama==0.3.9 # via pytest
11-
cryptography==1.5.2
12-
django==1.11
13-
flask==0.11.1
14-
future==0.15.2
15-
idna==2.7 # via cryptography
16-
itsdangerous==0.24
17-
jinja2==2.8
18-
markupsafe==0.23
19-
mock==2.0.0
20-
pbr==1.10.0
21-
pluggy==0.6.0 # via pytest, tox
22-
protobuf==3.1.0.post1
23-
py==1.5.3 # via pytest, tox
24-
pyasn1==0.4.3 # via cryptography
25-
pycparser==2.18 # via cffi
26-
pytest==3.3.2
27-
pytz==2018.4 # via django
28-
requests==2.11.1
29-
six==1.10.0
30-
tox==3.0.0
31-
virtualenv==13.1.2
32-
werkzeug==0.11.15
33-
wheel==0.24.0
1+
#
2+
# This file is autogenerated by pip-compile
3+
# To update, run:
4+
#
5+
# pip-compile --output-file requirements.txt requirements.in
6+
#
7+
asn1==2.2.0
8+
asn1crypto==0.24.0 # via cryptography
9+
attrs==18.1.0 # via pytest
10+
cffi==1.11.5 # via cryptography
11+
click==6.6
12+
colorama==0.3.9 # via pytest
13+
cryptography==2.2.2
14+
django==1.11
15+
flask==0.11.1
16+
future==0.15.2
17+
idna==2.7 # via cryptography
18+
itsdangerous==0.24
19+
jinja2==2.8
20+
markupsafe==0.23
21+
mock==2.0.0
22+
pbr==1.10.0
23+
pluggy==0.6.0 # via pytest, tox
24+
protobuf==3.1.0.post1
25+
py==1.5.3 # via pytest, tox
26+
pycparser==2.18 # via cffi
27+
pyopenssl==18.0.0
28+
pytest==3.3.2
29+
pytz==2018.4 # via django
30+
requests==2.11.1
31+
six==1.10.0
32+
tox==3.0.0
33+
virtualenv==13.1.2
34+
werkzeug==0.11.15
35+
wheel==0.24.0

setup.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,10 @@
1717
url='https://github.com/getyoti/yoti-python-sdk',
1818
author='Yoti',
1919
author_email='[email protected]',
20-
install_requires=['cryptography>=1.4', 'protobuf>=3.0.0',
21-
'requests>=2.0.0', 'future>=0.11.0'],
20+
install_requires=['cryptography>=2.2.1', 'protobuf>=3.1.0',
21+
'requests>=2.11.1', 'future>=0.11.0', 'asn1==2.2.0', 'pyopenssl>=18.0.0'],
2222
extras_require={
23-
'examples': ['Django>=1.8', 'Flask>=0.10', 'python-dotenv>=0.7.1', 'django-sslserver>=0.2',
24-
'pyopenssl>=18.0.0'],
23+
'examples': ['Django>=1.8', 'Flask>=0.10', 'python-dotenv>=0.7.1', 'django-sslserver>=0.2', 'Werkzeug==0.11.15'],
2524
},
2625
classifiers=[
2726
'Development Status :: 5 - Production/Stable',
Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
# -*- coding: utf-8 -*-
2-
import json
3-
42
import collections
3+
import json
54

6-
from yoti_python_sdk import config
5+
from yoti_python_sdk.anchor import Anchor
6+
from yoti_python_sdk import config, attribute
77
from yoti_python_sdk.protobuf.v1.protobuf import Protobuf
88

99

1010
class ActivityDetails:
1111
def __init__(self, receipt, decrypted_profile=None):
1212
self.decrypted_profile = decrypted_profile
13-
self.user_profile = {}
13+
self.user_profile = {} # will be deprecated in v3.0.0
14+
self.profile = {}
1415
self.base64_selfie_uri = None
1516

1617
if decrypted_profile and hasattr(decrypted_profile, 'attributes'):
@@ -19,17 +20,21 @@ def __init__(self, receipt, decrypted_profile=None):
1920
field.value,
2021
field.content_type
2122
)
22-
self.user_profile[field.name] = value
23+
24+
anchors = Anchor().parse_anchors(field.anchors)
25+
26+
self.profile[field.name] = attribute.attribute(field.name, value, anchors)
27+
self.user_profile[field.name] = value # will be deprecated in v3.0.0
2328

2429
if field.name == 'selfie':
2530
self.try_parse_selfie_field(field)
2631

2732
if field.name.startswith(config.ATTRIBUTE_AGE_OVER) or field.name.startswith(
2833
config.ATTRIBUTE_AGE_UNDER):
29-
self.try_parse_age_verified_field(field)
34+
self.try_parse_age_verified_field(field, anchors)
3035

3136
if field.name == config.ATTRIBUTE_STRUCTURED_POSTAL_ADDRESS:
32-
self.try_convert_structured_postal_address_to_dict(field)
37+
self.try_convert_structured_postal_address_to_dict(field, anchors)
3338

3439
self.set_address_to_be_formatted_address_if_null()
3540

@@ -42,33 +47,45 @@ def try_parse_selfie_field(self, field):
4247
field.content_type
4348
)
4449

45-
def try_parse_age_verified_field(self, field):
50+
def try_parse_age_verified_field(self, field, anchors):
4651
if field is not None:
4752
is_age_verified = Protobuf().value_based_on_content_type(
4853
field.value,
4954
field.content_type
5055
)
5156
if is_age_verified == 'true':
5257
self.user_profile['is_age_verified'] = True
58+
self.profile['is_age_verified'] = attribute.attribute(is_age_verified, True, anchors)
5359
return
5460
if is_age_verified == 'false':
5561
self.user_profile['is_age_verified'] = False
62+
self.profile['is_age_verified'] = attribute.attribute(is_age_verified, False, anchors)
5663
return
5764

5865
raise TypeError("age_verified_field unable to be parsed")
5966

60-
def try_convert_structured_postal_address_to_dict(self, field):
67+
def try_convert_structured_postal_address_to_dict(self, field, anchors):
6168
decoder = json.JSONDecoder(object_pairs_hook=collections.OrderedDict)
6269
self.user_profile[config.ATTRIBUTE_STRUCTURED_POSTAL_ADDRESS] = decoder.decode(field.value)
70+
self.profile[config.ATTRIBUTE_STRUCTURED_POSTAL_ADDRESS] = attribute.attribute(
71+
"structured_postal_address",
72+
decoder.decode(field.value),
73+
anchors)
6374

6475
def set_address_to_be_formatted_address_if_null(self):
6576
if 'postal_address' not in self.user_profile and 'structured_postal_address' in self.user_profile:
6677
if 'formatted_address' in self.user_profile['structured_postal_address']:
6778
self.user_profile['postal_address'] = self.user_profile['structured_postal_address'][
6879
'formatted_address']
6980

81+
if 'postal_address' not in self.profile and 'structured_postal_address' in self.profile:
82+
if 'formatted_address' in self.profile['structured_postal_address'].get_value():
83+
self.profile['postal_address'] = self.profile['structured_postal_address'].get_value()[
84+
'formatted_address']
85+
7086
def __iter__(self):
7187
yield 'user_id', self.user_id
7288
yield 'outcome', self.outcome
7389
yield 'user_profile', self.user_profile
90+
yield 'profile', self.profile
7491
yield 'base64_selfie_uri', self.base64_selfie_uri

yoti_python_sdk/anchor.py

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import asn1
2+
import datetime
3+
import OpenSSL
4+
import yoti_python_sdk.protobuf.v1.common_public_api.signed_timestamp_pb2 as compubapi
5+
6+
from OpenSSL import crypto
7+
from yoti_python_sdk import config
8+
9+
UNKNOWN_EXTENSION = ""
10+
SOURCE_EXTENSION = "1.3.6.1.4.1.47127.1.1.1"
11+
VERIFIER_EXTENSION = "1.3.6.1.4.1.47127.1.1.2"
12+
13+
14+
class Anchor:
15+
anchor_type = "Unknown"
16+
sub_type = ""
17+
value = ""
18+
signed_timestamp = compubapi.SignedTimestamp()
19+
20+
def __init__(self, anchor_type=None, sub_type=None, value=None, signed_timestamp=None):
21+
self.anchor_type = anchor_type
22+
self.sub_type = sub_type
23+
self.value = value
24+
self.signed_timestamp = signed_timestamp
25+
26+
def __iter__(self):
27+
return self
28+
29+
def __next__(self):
30+
self.idx += 1
31+
try:
32+
return self.data[self.idx - 1]
33+
except IndexError:
34+
self.idx = 0
35+
raise StopIteration
36+
37+
next = __next__ # python2.x compatibility.
38+
39+
@staticmethod
40+
def parse_anchors(anchors):
41+
for anc in anchors:
42+
parsed_anchors = []
43+
44+
if hasattr(anc, 'origin_server_certs'):
45+
origin_server_certs_list = list(anc.origin_server_certs)
46+
origin_server_certs_item = origin_server_certs_list[0]
47+
48+
cert = crypto.load_certificate(OpenSSL.crypto.FILETYPE_ASN1, origin_server_certs_item).to_cryptography()
49+
50+
for i in range(len(cert.extensions)):
51+
extensions = cert.extensions[i]
52+
if hasattr(extensions, 'oid'):
53+
oid = extensions.oid
54+
if hasattr(oid, 'dotted_string'):
55+
if oid.dotted_string == SOURCE_EXTENSION:
56+
anchor_type = config.ANCHOR_SOURCE
57+
elif oid.dotted_string == VERIFIER_EXTENSION:
58+
anchor_type = config.ANCHOR_VERIFIER
59+
else:
60+
continue
61+
62+
if hasattr(extensions, 'value'):
63+
extension_value = extensions.value
64+
if hasattr(extension_value, 'value'):
65+
parsed_anchors.append(Anchor(
66+
anchor_type,
67+
Anchor.get_sub_type(anc),
68+
Anchor.decode_asn1_value(extension_value.value),
69+
Anchor.get_signed_timestamp(anc)))
70+
71+
return parsed_anchors
72+
73+
@staticmethod
74+
def decode_asn1_value(value_to_decode):
75+
extension_value_asn1 = value_to_decode
76+
decoder = asn1.Decoder()
77+
78+
decoder.start(extension_value_asn1)
79+
tag, once_decoded_value = decoder.read()
80+
81+
decoder.start(once_decoded_value)
82+
tag, twice_decoded_value = decoder.read()
83+
84+
utf8_value = twice_decoded_value.decode('utf-8')
85+
return utf8_value
86+
87+
@staticmethod
88+
def get_sub_type(anchor):
89+
if hasattr(anchor, 'sub_type'):
90+
return anchor.sub_type
91+
else:
92+
return ""
93+
94+
@staticmethod
95+
def get_signed_timestamp(anchor):
96+
if hasattr(anchor, 'signed_time_stamp'):
97+
signed_timestamp_object = compubapi.SignedTimestamp()
98+
signed_timestamp_object.MergeFromString(anchor.signed_time_stamp)
99+
100+
try:
101+
signed_timestamp_parsed = datetime.datetime.fromtimestamp(signed_timestamp_object.timestamp / float(1000000))
102+
except OSError:
103+
print("Unable to parse timestamp from integer: '{0}'".format(signed_timestamp_object.timestamp))
104+
return ""
105+
106+
return signed_timestamp_parsed
107+
else:
108+
return ""

yoti_python_sdk/attribute.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
class attribute:
2+
name = ""
3+
value = ""
4+
anchors = {}
5+
6+
def __init__(self, name, value, anchors):
7+
self.name = name
8+
self.value = value
9+
self.anchors = anchors
10+
11+
def get_name(self):
12+
return self.name
13+
14+
def get_value(self):
15+
return self.value
16+
17+
def get_anchors(self):
18+
return self.anchors

yoti_python_sdk/config.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
# -*- coding: utf-8 -*-
2-
SDK_IDENTIFIER = "Python"
3-
ATTRIBUTE_AGE_OVER = "age_over:"
4-
ATTRIBUTE_AGE_UNDER = "age_under:"
5-
ATTRIBUTE_STRUCTURED_POSTAL_ADDRESS = "structured_postal_address"
1+
# -*- coding: utf-8 -*-
2+
SDK_IDENTIFIER = "Python"
3+
ATTRIBUTE_AGE_OVER = "age_over:"
4+
ATTRIBUTE_AGE_UNDER = "age_under:"
5+
ATTRIBUTE_STRUCTURED_POSTAL_ADDRESS = "structured_postal_address"
6+
ANCHOR_SOURCE = "SOURCE"
7+
ANCHOR_VERIFIER = "VERIFIER"

0 commit comments

Comments
 (0)