ib1-provenance 0.1__tar.gz
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.
- ib1_provenance-0.1/LICENSE +21 -0
- ib1_provenance-0.1/PKG-INFO +38 -0
- ib1_provenance-0.1/README.md +22 -0
- ib1_provenance-0.1/pyproject.toml +25 -0
- ib1_provenance-0.1/src/ib1/provenance/__init__.py +2 -0
- ib1_provenance-0.1/src/ib1/provenance/certificates.py +107 -0
- ib1_provenance-0.1/src/ib1/provenance/identifier.py +5 -0
- ib1_provenance-0.1/src/ib1/provenance/record.py +290 -0
- ib1_provenance-0.1/src/ib1/provenance/signing.py +32 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Icebreaker One
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: ib1-provenance
|
|
3
|
+
Version: 0.1
|
|
4
|
+
Summary: Tools to encode, decode, and sign provenance data
|
|
5
|
+
Author: Ben Summers
|
|
6
|
+
Author-email: ben.summers@icebreakerone.org
|
|
7
|
+
Requires-Python: >=3.10,<4.0
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Requires-Dist: asn1crypto (>=1.5.1,<2.0.0)
|
|
13
|
+
Requires-Dist: cryptography (>=43.0.1,<44.0.0)
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# Provenance
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
python3 -m venv .venv
|
|
20
|
+
. .venv/bin/activate
|
|
21
|
+
pip install -r requirements.txt
|
|
22
|
+
|
|
23
|
+
pipx install poetry
|
|
24
|
+
poetry install
|
|
25
|
+
|
|
26
|
+
mkdir certs
|
|
27
|
+
cd certs
|
|
28
|
+
sh ../scripts/certmaker.sh
|
|
29
|
+
|
|
30
|
+
python3 main.py
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Provenance record decoder
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
python3 decode-self-contained-provenance.py path/to/signing-CA-root.pem < record.json
|
|
37
|
+
```
|
|
38
|
+
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Provenance
|
|
2
|
+
|
|
3
|
+
```
|
|
4
|
+
python3 -m venv .venv
|
|
5
|
+
. .venv/bin/activate
|
|
6
|
+
pip install -r requirements.txt
|
|
7
|
+
|
|
8
|
+
pipx install poetry
|
|
9
|
+
poetry install
|
|
10
|
+
|
|
11
|
+
mkdir certs
|
|
12
|
+
cd certs
|
|
13
|
+
sh ../scripts/certmaker.sh
|
|
14
|
+
|
|
15
|
+
python3 main.py
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Provenance record decoder
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
python3 decode-self-contained-provenance.py path/to/signing-CA-root.pem < record.json
|
|
22
|
+
```
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "ib1-provenance"
|
|
3
|
+
version = "0.1"
|
|
4
|
+
description = "Tools to encode, decode, and sign provenance data"
|
|
5
|
+
authors = ["Ben Summers <ben.summers@icebreakerone.org>"]
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
packages = [{include = "ib1", from = "src"}]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
[tool.poetry.dependencies]
|
|
11
|
+
python = "^3.10"
|
|
12
|
+
cryptography = "^43.0.1"
|
|
13
|
+
asn1crypto = "^1.5.1"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
[tool.poetry.group.dev.dependencies]
|
|
17
|
+
pytest = "^8.3.3"
|
|
18
|
+
mypy = "^1.12.0"
|
|
19
|
+
ruff = "^0.6.9"
|
|
20
|
+
pytest-cov = "^5.0.0"
|
|
21
|
+
tox = "^4.23.0"
|
|
22
|
+
|
|
23
|
+
[build-system]
|
|
24
|
+
requires = ["poetry-core"]
|
|
25
|
+
build-backend = "poetry.core.masonry.api"
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
|
|
2
|
+
import json
|
|
3
|
+
import base64
|
|
4
|
+
import datetime
|
|
5
|
+
|
|
6
|
+
from cryptography import x509
|
|
7
|
+
from cryptography.x509.oid import NameOID, ExtensionOID
|
|
8
|
+
from cryptography.x509.verification import PolicyBuilder, Store
|
|
9
|
+
from cryptography.hazmat.primitives import hashes
|
|
10
|
+
from cryptography.hazmat.primitives.asymmetric import ec
|
|
11
|
+
import asn1crypto.core as asn1
|
|
12
|
+
|
|
13
|
+
# ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
OID_IB1_ROLES = x509.ObjectIdentifier("1.3.6.1.4.1.62329.1.1")
|
|
16
|
+
OID_IB1_APPLICATION = x509.ObjectIdentifier("1.3.6.1.4.1.62329.1.2")
|
|
17
|
+
|
|
18
|
+
class CertExtUTF8Sequence(asn1.SequenceOf):
|
|
19
|
+
_child_spec = asn1.UTF8String
|
|
20
|
+
|
|
21
|
+
class SigningCertificate:
|
|
22
|
+
def __init__(self, x509_certificate: x509.Certificate):
|
|
23
|
+
self._x509_certificate = x509_certificate
|
|
24
|
+
|
|
25
|
+
def subject(self):
|
|
26
|
+
san_extension = self._x509_certificate.extensions.get_extension_for_oid(ExtensionOID.SUBJECT_ALTERNATIVE_NAME)
|
|
27
|
+
uri = san_extension.value.get_values_for_type(x509.UniformResourceIdentifier)
|
|
28
|
+
if len(uri) != 1:
|
|
29
|
+
raise Exception("Certificate doesn't contain exactly one URI subject alternative name")
|
|
30
|
+
return uri[0]
|
|
31
|
+
|
|
32
|
+
def organisation_name(self):
|
|
33
|
+
return self._x509_certificate.subject.get_attributes_for_oid(NameOID.ORGANIZATION_NAME)[0].value
|
|
34
|
+
|
|
35
|
+
def application(self):
|
|
36
|
+
value = self._x509_certificate.extensions.get_extension_for_oid(OID_IB1_APPLICATION).value.value # type: ignore [attr-defined]
|
|
37
|
+
return str(asn1.UTF8String.load(value))
|
|
38
|
+
|
|
39
|
+
def roles(self):
|
|
40
|
+
value = self._x509_certificate.extensions.get_extension_for_oid(OID_IB1_ROLES).value.value # type: ignore [attr-defined]
|
|
41
|
+
return list(map(str, CertExtUTF8Sequence.load(value)))
|
|
42
|
+
|
|
43
|
+
# ---------------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
class CertificateProviderBase:
|
|
46
|
+
def __init__(self, root_ca_certificate):
|
|
47
|
+
with open(root_ca_certificate, "rb") as f:
|
|
48
|
+
self._ca_store = Store(x509.load_pem_x509_certificates(f.read()))
|
|
49
|
+
|
|
50
|
+
def policy_include_certificates_in_record(self):
|
|
51
|
+
return False
|
|
52
|
+
|
|
53
|
+
def _verify(self, certificates_from_record, serial, sign_timestamp, data, signature):
|
|
54
|
+
certs = self.certificates_for_serial(certificates_from_record, serial)
|
|
55
|
+
# first certificate in file is signing certificate
|
|
56
|
+
signing_cert, *issuer_chain = certs
|
|
57
|
+
# 1) check certificate chain validity at the time of signature
|
|
58
|
+
verification_time = datetime.datetime.fromisoformat(sign_timestamp)
|
|
59
|
+
verifier = (PolicyBuilder().
|
|
60
|
+
store(self._ca_store).
|
|
61
|
+
time(verification_time).
|
|
62
|
+
build_client_verifier())
|
|
63
|
+
verifier.verify(signing_cert, issuer_chain)
|
|
64
|
+
# 2) check signature on data
|
|
65
|
+
pubkey = signing_cert.public_key()
|
|
66
|
+
pubkey.verify(signature, data, ec.ECDSA(hashes.SHA256()))
|
|
67
|
+
# Return information about the signer
|
|
68
|
+
cert_info = SigningCertificate(signing_cert)
|
|
69
|
+
return {
|
|
70
|
+
"member": cert_info.subject(),
|
|
71
|
+
"name": cert_info.organisation_name(),
|
|
72
|
+
"application": cert_info.application(),
|
|
73
|
+
"roles": cert_info.roles()
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
# ---------------------------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
class CertificatesProviderLocal(CertificateProviderBase):
|
|
79
|
+
def __init__(self, root_ca_certificate, directory):
|
|
80
|
+
CertificateProviderBase.__init__(self, root_ca_certificate)
|
|
81
|
+
self._directory = directory
|
|
82
|
+
|
|
83
|
+
def certificates_for_serial(self, certificates_from_record, serial):
|
|
84
|
+
certificate_filename = self._directory + '/' + str(int(serial)) + '-bundle.pem'
|
|
85
|
+
with open(certificate_filename, "rb") as f:
|
|
86
|
+
return x509.load_pem_x509_certificates(f.read())
|
|
87
|
+
|
|
88
|
+
# ---------------------------------------------------------------------------
|
|
89
|
+
|
|
90
|
+
class CertificatesProviderSelfContainedRecord(CertificateProviderBase):
|
|
91
|
+
def policy_include_certificates_in_record(self):
|
|
92
|
+
return True
|
|
93
|
+
|
|
94
|
+
def certificates_for_serial(self, certificates_from_record, serial):
|
|
95
|
+
certs = certificates_from_record.get(serial)
|
|
96
|
+
if certs is None:
|
|
97
|
+
raise Exception("Certificate serial "+serial+" is not present in record")
|
|
98
|
+
signing_cert, *path_serials = certs
|
|
99
|
+
cert_chain = [signing_cert]
|
|
100
|
+
cert_chain.extend(list(map(
|
|
101
|
+
lambda s: certificates_from_record[s][0],
|
|
102
|
+
path_serials
|
|
103
|
+
)))
|
|
104
|
+
return list(map(
|
|
105
|
+
lambda c: x509.load_pem_x509_certificate(c.encode("utf-8")),
|
|
106
|
+
cert_chain
|
|
107
|
+
))
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import json
|
|
3
|
+
import copy
|
|
4
|
+
import datetime
|
|
5
|
+
|
|
6
|
+
from cryptography.hazmat.primitives import serialization
|
|
7
|
+
|
|
8
|
+
from .identifier import globally_unique_step_identifier
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
CURRENT_CONTAINER_FORMAT_VERSION = 0
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Record:
|
|
15
|
+
|
|
16
|
+
# data attributes:
|
|
17
|
+
# _record -- signed record as output from JSON decoding
|
|
18
|
+
# _additional_records -- array of signed records to add to the record
|
|
19
|
+
# _additional_steps -- array of steps to add to the record
|
|
20
|
+
# _signed -- whether the record is completely signed
|
|
21
|
+
# _verified -- whether the record has had signatures verified
|
|
22
|
+
|
|
23
|
+
def __init__(self, trust_framework, record=None):
|
|
24
|
+
if record is not None:
|
|
25
|
+
if not isinstance(record.get("steps"), list):
|
|
26
|
+
raise Exception("Not an encoded Provenance record")
|
|
27
|
+
if trust_framework != record["ib1:provenance"]:
|
|
28
|
+
raise Exception("Unexpected trust framework when creating Record from encoded form")
|
|
29
|
+
self.trust_framework = trust_framework
|
|
30
|
+
self._record = record
|
|
31
|
+
self._additional_records = []
|
|
32
|
+
self._additional_steps = []
|
|
33
|
+
self._signed = True
|
|
34
|
+
self._verified = None
|
|
35
|
+
|
|
36
|
+
def verify(self, certificate_provider):
|
|
37
|
+
self._require_signed()
|
|
38
|
+
# Recursively verify record
|
|
39
|
+
steps = []
|
|
40
|
+
origins = []
|
|
41
|
+
signer_stack = []
|
|
42
|
+
certificates_from_record = self._record.get("certificates")
|
|
43
|
+
if certificates_from_record is None:
|
|
44
|
+
certificates_from_record = {}
|
|
45
|
+
self._verify_record_container(self._record["steps"], certificates_from_record, certificate_provider, steps, origins, signer_stack)
|
|
46
|
+
if self._record["origins"] != origins:
|
|
47
|
+
raise Exception("origins property does not match origin steps in record")
|
|
48
|
+
self._verified = steps
|
|
49
|
+
|
|
50
|
+
def _verify_record_container(self, container, certificates_from_record, certificate_provider, steps, origins, signer_stack):
|
|
51
|
+
*data, sig_block = container
|
|
52
|
+
container_format_version, serial, sign_timestamp, signature = sig_block
|
|
53
|
+
# Check it's an understood format (multiple versions of formats may be included in a single record)
|
|
54
|
+
if container_format_version != CURRENT_CONTAINER_FORMAT_VERSION:
|
|
55
|
+
raise Exception("Cannot decode container format version: "+str(container_format_version))
|
|
56
|
+
# Serial number must only be a number
|
|
57
|
+
if str(int(serial)) != serial:
|
|
58
|
+
raise Exception("Bad certificate serial number in record: "+serial)
|
|
59
|
+
# Check signatures at this level and get signer information
|
|
60
|
+
data_for_signing = self._data_for_signing(data, [str(container_format_version), serial, sign_timestamp])
|
|
61
|
+
signer_info = certificate_provider._verify(certificates_from_record, serial, sign_timestamp, data_for_signing.encode("utf-8"), base64.urlsafe_b64decode(signature))
|
|
62
|
+
# Recurse into signed data, collecting decoded steps and adding signer info
|
|
63
|
+
for e in data:
|
|
64
|
+
if not isinstance(e, str):
|
|
65
|
+
signer_stack.append(signer_info)
|
|
66
|
+
self._verify_record_container(e, certificates_from_record, certificate_provider, steps, origins, signer_stack)
|
|
67
|
+
del signer_stack[-1]
|
|
68
|
+
else:
|
|
69
|
+
decoded_step = json.loads(base64.urlsafe_b64decode(e))
|
|
70
|
+
if decoded_step["type"] == "origin":
|
|
71
|
+
origins.append(decoded_step["id"])
|
|
72
|
+
decoded_step["_signature"] = {
|
|
73
|
+
"signed": signer_info,
|
|
74
|
+
"includedBy": copy.copy(signer_stack)
|
|
75
|
+
}
|
|
76
|
+
steps.append(decoded_step)
|
|
77
|
+
|
|
78
|
+
def add_record(self, record):
|
|
79
|
+
self._signed = False
|
|
80
|
+
self._verified = None
|
|
81
|
+
if not isinstance(record, Record):
|
|
82
|
+
raise Exception("Not a Record object")
|
|
83
|
+
if self.trust_framework != record.trust_framework:
|
|
84
|
+
raise Exception("Incompatible trust frameworks in added Record")
|
|
85
|
+
self._additional_records.append(record.encoded())
|
|
86
|
+
|
|
87
|
+
def add_step(self, step_in):
|
|
88
|
+
self._signed = False
|
|
89
|
+
self._verified = None
|
|
90
|
+
step = copy.deepcopy(step_in)
|
|
91
|
+
# Step identifier
|
|
92
|
+
if "id" in step:
|
|
93
|
+
raise Exception("Step may not contain an id key. Identifiers are allocated automatically and returned by this function.")
|
|
94
|
+
id = globally_unique_step_identifier()
|
|
95
|
+
# Step timestamp
|
|
96
|
+
# TODO: Verify timestamp is in the right format (but signing cert does not need to be valid at that time?)
|
|
97
|
+
timestamp = step.pop("timestamp", None)
|
|
98
|
+
if not timestamp:
|
|
99
|
+
timestamp = self._timestamp_now_iso8601()
|
|
100
|
+
# Step type
|
|
101
|
+
step_type = step.pop("type")
|
|
102
|
+
# No keys with _ prefix allowed
|
|
103
|
+
prohibited_keys_present = list(filter(
|
|
104
|
+
lambda k: k.startswith('_'),
|
|
105
|
+
step.keys()
|
|
106
|
+
))
|
|
107
|
+
if prohibited_keys_present:
|
|
108
|
+
raise Exception("Step may not contain keys beginning with an underscore. Prohibited keys present: "+(", ".join(prohibited_keys_present)))
|
|
109
|
+
# Add to list of steps pending addition
|
|
110
|
+
self._additional_steps.append({
|
|
111
|
+
"id": id,
|
|
112
|
+
"timestamp": timestamp,
|
|
113
|
+
"type": step_type,
|
|
114
|
+
**step
|
|
115
|
+
})
|
|
116
|
+
return id
|
|
117
|
+
|
|
118
|
+
def find_step(self, required_values):
|
|
119
|
+
steps = self.filter_steps(required_values)
|
|
120
|
+
if len(steps) == 0:
|
|
121
|
+
raise Exception("No step matches required values")
|
|
122
|
+
if len(steps) > 1:
|
|
123
|
+
raise Exception("More than one step matches required values")
|
|
124
|
+
return steps[0]
|
|
125
|
+
|
|
126
|
+
def filter_steps(self, required_values):
|
|
127
|
+
self._require_verified()
|
|
128
|
+
return list(filter(
|
|
129
|
+
lambda s: self._filter_step_contains(s, required_values),
|
|
130
|
+
self._verified
|
|
131
|
+
))
|
|
132
|
+
|
|
133
|
+
def _filter_step_contains(self, step, required_values):
|
|
134
|
+
if isinstance(required_values, dict):
|
|
135
|
+
# If required_values is a dict, ensure step is a dict and contains all keys/values of required_values
|
|
136
|
+
if not isinstance(step, dict):
|
|
137
|
+
return False
|
|
138
|
+
for key, value in required_values.items():
|
|
139
|
+
if key not in step or not self._filter_step_contains(step[key], value):
|
|
140
|
+
return False
|
|
141
|
+
return True
|
|
142
|
+
elif isinstance(required_values, list):
|
|
143
|
+
# If required_values is a list, ensure step is a list and contains all elements of required_values
|
|
144
|
+
if not isinstance(step, list):
|
|
145
|
+
return False
|
|
146
|
+
for item in required_values:
|
|
147
|
+
if not any(self._filter_step_contains(x, item) for x in step):
|
|
148
|
+
return False
|
|
149
|
+
return True
|
|
150
|
+
else:
|
|
151
|
+
# If required_values is a primitive, ensure it matches the value in step
|
|
152
|
+
return step == required_values
|
|
153
|
+
|
|
154
|
+
def sign(self, signer):
|
|
155
|
+
output = []
|
|
156
|
+
certificates = {}
|
|
157
|
+
if self._record is not None:
|
|
158
|
+
if "certificates" in self._record:
|
|
159
|
+
certificates.update(self._record["certificates"])
|
|
160
|
+
output.append(self._record["steps"]) # signed and encoded
|
|
161
|
+
for r in self._additional_records:
|
|
162
|
+
if "certificates" in r:
|
|
163
|
+
certificates.update(r["certificates"])
|
|
164
|
+
output.append(r["steps"]) # signed and encoded
|
|
165
|
+
for s in self._additional_steps:
|
|
166
|
+
output.append(self._encode_step(s)) # unencoded, not signed
|
|
167
|
+
serial = signer.serial()
|
|
168
|
+
sign_timestamp = self._timestamp_now_iso8601()
|
|
169
|
+
data_for_signing = self._data_for_signing(output, [str(CURRENT_CONTAINER_FORMAT_VERSION), serial, sign_timestamp])
|
|
170
|
+
signature = signer.sign(data_for_signing.encode("utf-8"))
|
|
171
|
+
output.append([
|
|
172
|
+
CURRENT_CONTAINER_FORMAT_VERSION,
|
|
173
|
+
serial,
|
|
174
|
+
sign_timestamp,
|
|
175
|
+
base64.urlsafe_b64encode(signature).decode('utf-8')
|
|
176
|
+
])
|
|
177
|
+
if not serial in certificates:
|
|
178
|
+
certs_for_record = signer.certificates_for_record()
|
|
179
|
+
if certs_for_record is not None:
|
|
180
|
+
# Represent path as [pem encoded cert, serials of issuer chain ...]
|
|
181
|
+
first_cert, *other_certs = certs_for_record
|
|
182
|
+
cert_path = [first_cert.public_bytes(serialization.Encoding.PEM).decode("utf-8")]
|
|
183
|
+
cert_path.extend(list(map(
|
|
184
|
+
lambda c: str(c.serial_number), other_certs
|
|
185
|
+
)))
|
|
186
|
+
certificates[serial] = cert_path
|
|
187
|
+
for c in other_certs:
|
|
188
|
+
certificates[str(c.serial_number)] = [c.public_bytes(serialization.Encoding.PEM).decode("utf-8")]
|
|
189
|
+
origins = []
|
|
190
|
+
self._gather_origins(output, origins)
|
|
191
|
+
encoded = {
|
|
192
|
+
"ib1:provenance": self.trust_framework,
|
|
193
|
+
"origins": origins,
|
|
194
|
+
"steps": copy.deepcopy(output)
|
|
195
|
+
}
|
|
196
|
+
if certificates:
|
|
197
|
+
encoded["certificates"] = certificates
|
|
198
|
+
return Record(self.trust_framework, encoded)
|
|
199
|
+
|
|
200
|
+
def _gather_origins(self, container, origins):
|
|
201
|
+
*data, sig_block = container
|
|
202
|
+
for e in data:
|
|
203
|
+
if not isinstance(e, str):
|
|
204
|
+
self._gather_origins(e, origins)
|
|
205
|
+
else:
|
|
206
|
+
decoded_step = json.loads(base64.urlsafe_b64decode(e))
|
|
207
|
+
if decoded_step["type"] == "origin":
|
|
208
|
+
origins.append(decoded_step["id"])
|
|
209
|
+
|
|
210
|
+
def _encode_step(self, step):
|
|
211
|
+
return base64.urlsafe_b64encode(json.dumps(step, separators=(",", ":")).encode("utf-8")).decode('utf-8')
|
|
212
|
+
|
|
213
|
+
def _data_for_signing(self, data, additional=None, is_root=True):
|
|
214
|
+
gather = []
|
|
215
|
+
if is_root:
|
|
216
|
+
gather.append(self.trust_framework)
|
|
217
|
+
for e in data:
|
|
218
|
+
if isinstance(e, str):
|
|
219
|
+
gather.append(e)
|
|
220
|
+
elif isinstance(e, int):
|
|
221
|
+
gather.append(str(e))
|
|
222
|
+
else:
|
|
223
|
+
gather.append("%")
|
|
224
|
+
gather.append(self._data_for_signing(e, None, False))
|
|
225
|
+
gather.append("&")
|
|
226
|
+
if additional is not None:
|
|
227
|
+
gather.extend(additional)
|
|
228
|
+
return ".".join(gather)
|
|
229
|
+
|
|
230
|
+
def encoded(self): # TODO name
|
|
231
|
+
self._require_signed()
|
|
232
|
+
return self._record
|
|
233
|
+
|
|
234
|
+
def decoded(self): # TODO name
|
|
235
|
+
self._require_verified()
|
|
236
|
+
return copy.deepcopy(self._verified)
|
|
237
|
+
|
|
238
|
+
def _require_signed(self):
|
|
239
|
+
if not self._signed:
|
|
240
|
+
raise Exception("Record is not signed, call sign() and use returned object")
|
|
241
|
+
|
|
242
|
+
def _require_verified(self):
|
|
243
|
+
if not (self._signed and (self._verified != None)):
|
|
244
|
+
raise Exception("Record is not verified, call verify() first")
|
|
245
|
+
|
|
246
|
+
def _timestamp_now_iso8601(self):
|
|
247
|
+
return (datetime.datetime.now(datetime.UTC).
|
|
248
|
+
replace(microsecond=0).
|
|
249
|
+
isoformat().
|
|
250
|
+
replace("+00:00", "Z")
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
def to_graphviz(self):
|
|
254
|
+
self._require_verified()
|
|
255
|
+
signers = {}
|
|
256
|
+
dot = ["digraph ProvenanceRecord {"]
|
|
257
|
+
for s in self._verified:
|
|
258
|
+
id = s["id"]
|
|
259
|
+
shape = "box"
|
|
260
|
+
match s["type"]:
|
|
261
|
+
case "permission":
|
|
262
|
+
shape = "invhouse"
|
|
263
|
+
case "origin":
|
|
264
|
+
shape = "diamond"
|
|
265
|
+
case "transfer":
|
|
266
|
+
dot.append(" \""+s["of"]+"\" -> \""+id+"\" [label=of];")
|
|
267
|
+
case "receipt":
|
|
268
|
+
shape = "box3d"
|
|
269
|
+
dot.append(" \""+s["transfer"]+"\" -> \""+id+"\" [label=transfer];")
|
|
270
|
+
case "process":
|
|
271
|
+
shape = "parallelogram"
|
|
272
|
+
for key in ["inputs", "permissions"]:
|
|
273
|
+
for i in s.get(key, []):
|
|
274
|
+
dot.append(" \""+i+"\" -> \""+id+"\" [label="+key+"];")
|
|
275
|
+
dot.append(" \""+id+"\" [shape="+shape+",label=\""+s["type"]+"\\n"+id+"\"];")
|
|
276
|
+
signer = s["_signature"]["signed"]["name"]
|
|
277
|
+
if signer not in signers:
|
|
278
|
+
signers[signer] = []
|
|
279
|
+
signers[signer].append(id)
|
|
280
|
+
index = 0
|
|
281
|
+
for signer in signers:
|
|
282
|
+
dot.append(" subgraph cluster_"+str(index)+" {")
|
|
283
|
+
index += 1
|
|
284
|
+
dot.append(" graph[style=dashed];")
|
|
285
|
+
dot.append(" label = \""+signer+"\";")
|
|
286
|
+
for i in signers[signer]:
|
|
287
|
+
dot.append(" \""+i+"\";")
|
|
288
|
+
dot.append(" }")
|
|
289
|
+
dot.append("}")
|
|
290
|
+
return "\n".join(dot)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
|
|
2
|
+
from cryptography import x509
|
|
3
|
+
from cryptography.hazmat.primitives import serialization
|
|
4
|
+
from cryptography.hazmat.primitives import hashes
|
|
5
|
+
from cryptography.hazmat.primitives.asymmetric import ec
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SignerInMemory:
|
|
9
|
+
def __init__(self, certificate_provider, certificates, private_key):
|
|
10
|
+
self._certificate_provider = certificate_provider
|
|
11
|
+
self._certificates = certificates
|
|
12
|
+
self._private_key = private_key
|
|
13
|
+
|
|
14
|
+
def serial(self):
|
|
15
|
+
return str(self._certificates[0].serial_number) # String, as JSON rounds large integers
|
|
16
|
+
|
|
17
|
+
def certificates_for_record(self):
|
|
18
|
+
if not self._certificate_provider.policy_include_certificates_in_record():
|
|
19
|
+
return None
|
|
20
|
+
return self._certificates.copy();
|
|
21
|
+
|
|
22
|
+
def sign(self, data):
|
|
23
|
+
# TODO: Use correct algorithm for type of key in certificate, assuming EC crypto
|
|
24
|
+
return self._private_key.sign(data, ec.ECDSA(hashes.SHA256()))
|
|
25
|
+
|
|
26
|
+
class SignerFiles(SignerInMemory):
|
|
27
|
+
def __init__(self, certificate_provider, certificate_file, key_file):
|
|
28
|
+
with open(certificate_file, "rb") as certs:
|
|
29
|
+
certificates = x509.load_pem_x509_certificates(certs.read())
|
|
30
|
+
with open(key_file, "rb") as key:
|
|
31
|
+
private_key = serialization.load_pem_private_key(key.read(), password=None)
|
|
32
|
+
super().__init__(certificate_provider, certificates, private_key)
|