sci-protocol 0.2.0__py3-none-any.whl
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.
- pysci/__init__.py +77 -0
- pysci/__main__.py +8 -0
- pysci/_cli/__init__.py +5 -0
- pysci/_cli/checks.py +173 -0
- pysci/_cli/clients/__init__.py +0 -0
- pysci/_cli/clients/archive.py +456 -0
- pysci/_cli/clients/credentials.py +54 -0
- pysci/_cli/clients/github.py +734 -0
- pysci/_cli/clients/hosting.py +238 -0
- pysci/_cli/clients/http.py +481 -0
- pysci/_cli/clients/huggingface.py +492 -0
- pysci/_cli/clients/index.py +435 -0
- pysci/_cli/clients/tsa.py +223 -0
- pysci/_cli/commands/__init__.py +44 -0
- pysci/_cli/commands/check.py +115 -0
- pysci/_cli/commands/claim.py +179 -0
- pysci/_cli/commands/common.py +91 -0
- pysci/_cli/commands/folders.py +83 -0
- pysci/_cli/commands/index.py +51 -0
- pysci/_cli/commands/keygen.py +58 -0
- pysci/_cli/commands/license.py +75 -0
- pysci/_cli/commands/protocol.py +72 -0
- pysci/_cli/commands/publish.py +282 -0
- pysci/_cli/commands/pull.py +337 -0
- pysci/_cli/commands/read.py +364 -0
- pysci/_cli/commands/revoke.py +207 -0
- pysci/_cli/commands/search.py +400 -0
- pysci/_cli/commands/sign.py +284 -0
- pysci/_cli/commands/submit.py +172 -0
- pysci/_cli/commands/timestamp.py +196 -0
- pysci/_cli/commands/verify.py +163 -0
- pysci/_cli/config.py +233 -0
- pysci/_cli/context.py +79 -0
- pysci/_cli/destinations.py +163 -0
- pysci/_cli/errors.py +168 -0
- pysci/_cli/main.py +229 -0
- pysci/_cli/output.py +80 -0
- pysci/_cli/retrieval.py +51 -0
- pysci/_cli/spec.py +113 -0
- pysci/_cli/storage/__init__.py +0 -0
- pysci/_cli/storage/catalog.py +371 -0
- pysci/_cli/storage/files.py +252 -0
- pysci/_cli/storage/inputs.py +205 -0
- pysci/_cli/storage/keys.py +132 -0
- pysci/_cli/storage/local_publish.py +88 -0
- pysci/accounts.py +135 -0
- pysci/baseline.py +142 -0
- pysci/bundle.py +878 -0
- pysci/citations.py +233 -0
- pysci/data/freetsa-cacert.pem +45 -0
- pysci/data/sci-baseline-LICENSE.txt +457 -0
- pysci/data/sci-protocol.md +613 -0
- pysci/der.py +146 -0
- pysci/discovery.py +461 -0
- pysci/draft.py +374 -0
- pysci/ed25519.py +153 -0
- pysci/errors.py +204 -0
- pysci/ids.py +61 -0
- pysci/jsonstrict.py +225 -0
- pysci/keys.py +247 -0
- pysci/manifest.py +302 -0
- pysci/metadata.py +406 -0
- pysci/paths.py +136 -0
- pysci/png.py +217 -0
- pysci/py.typed +0 -0
- pysci/records.py +384 -0
- pysci/rfc3339.py +66 -0
- pysci/schemas.py +75 -0
- pysci/source.py +166 -0
- pysci/specification.py +58 -0
- pysci/submission.py +225 -0
- pysci/templates.py +129 -0
- pysci/text.py +226 -0
- pysci/timestamps.py +867 -0
- pysci/zipformat.py +400 -0
- sci_protocol-0.2.0.dist-info/METADATA +112 -0
- sci_protocol-0.2.0.dist-info/RECORD +80 -0
- sci_protocol-0.2.0.dist-info/WHEEL +4 -0
- sci_protocol-0.2.0.dist-info/entry_points.txt +2 -0
- sci_protocol-0.2.0.dist-info/licenses/LICENSE +202 -0
pysci/citations.py
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"""Citation targets (protocol §4.4): validation and comparison keys.
|
|
2
|
+
|
|
3
|
+
Three target forms are permitted:
|
|
4
|
+
|
|
5
|
+
* ``sci:sha256:<digest>`` -- an SCI publication;
|
|
6
|
+
* ``doi:<name>`` -- a DOI URI (DOI names compare case-insensitively for ASCII
|
|
7
|
+
after percent-decoding);
|
|
8
|
+
* an ``http``/``https`` URI (RFC 9110 §4.2) with an optional fragment.
|
|
9
|
+
|
|
10
|
+
:func:`citation_target_key` derives a normalized key so that equivalent
|
|
11
|
+
spellings of the same DOI or URL can be grouped, e.g. by an indexer.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import ipaddress
|
|
17
|
+
import re
|
|
18
|
+
from dataclasses import dataclass
|
|
19
|
+
from enum import StrEnum
|
|
20
|
+
from typing import Final
|
|
21
|
+
|
|
22
|
+
from .ids import is_digest
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"CITATION_RELATIONS",
|
|
26
|
+
"CitationTarget",
|
|
27
|
+
"TargetKind",
|
|
28
|
+
"citation_target_key",
|
|
29
|
+
"is_http_uri",
|
|
30
|
+
"parse_citation_target",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
CITATION_RELATIONS: Final = frozenset({"uses", "reviews", "supports", "challenges", "supersedes"})
|
|
34
|
+
|
|
35
|
+
_UNRESERVED: Final = frozenset("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~")
|
|
36
|
+
_SUB_DELIMS: Final = frozenset("!$&'()*+,;=")
|
|
37
|
+
_PCHAR: Final = _UNRESERVED | _SUB_DELIMS | frozenset(":@")
|
|
38
|
+
_PATH_CHARS: Final = _PCHAR | frozenset("/")
|
|
39
|
+
_QUERY_CHARS: Final = _PCHAR | frozenset("/?")
|
|
40
|
+
_REG_NAME_CHARS: Final = _UNRESERVED | _SUB_DELIMS
|
|
41
|
+
_HEX: Final = frozenset("0123456789abcdefABCDEF")
|
|
42
|
+
|
|
43
|
+
_HTTP_URI: Final = re.compile(
|
|
44
|
+
r"(?P<scheme>[A-Za-z][A-Za-z0-9+.-]*)://(?P<authority>[^/?#]*)(?P<path>[^?#]*)"
|
|
45
|
+
r"(?:\?(?P<query>[^#]*))?(?:#(?P<fragment>.*))?",
|
|
46
|
+
re.DOTALL,
|
|
47
|
+
)
|
|
48
|
+
_DOI: Final = re.compile(r"doi:(?P<prefix>10\.[0-9]+(?:\.[0-9]+)*)/(?P<suffix>.+)", re.DOTALL)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class TargetKind(StrEnum):
|
|
52
|
+
SCI = "sci"
|
|
53
|
+
DOI = "doi"
|
|
54
|
+
URL = "url"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass(frozen=True, slots=True)
|
|
58
|
+
class CitationTarget:
|
|
59
|
+
"""A validated citation target and its comparison key."""
|
|
60
|
+
|
|
61
|
+
kind: TargetKind
|
|
62
|
+
target: str
|
|
63
|
+
key: str
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _valid_chars(value: str, allowed: frozenset[str]) -> bool:
|
|
67
|
+
"""True if ``value`` uses only ``allowed`` characters and valid %HH escapes."""
|
|
68
|
+
index = 0
|
|
69
|
+
length = len(value)
|
|
70
|
+
while index < length:
|
|
71
|
+
ch = value[index]
|
|
72
|
+
if ch == "%":
|
|
73
|
+
if index + 2 >= length:
|
|
74
|
+
return False
|
|
75
|
+
if value[index + 1] not in _HEX or value[index + 2] not in _HEX:
|
|
76
|
+
return False
|
|
77
|
+
index += 3
|
|
78
|
+
continue
|
|
79
|
+
if ch not in allowed:
|
|
80
|
+
return False
|
|
81
|
+
index += 1
|
|
82
|
+
return True
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _valid_authority(authority: str) -> bool:
|
|
86
|
+
if not authority or "@" in authority:
|
|
87
|
+
# RFC 9110 §4.2.1: an empty host is invalid; userinfo is deprecated
|
|
88
|
+
# and treated as an error by recipients (§4.2.4).
|
|
89
|
+
return False
|
|
90
|
+
if authority.startswith("["):
|
|
91
|
+
end = authority.find("]")
|
|
92
|
+
if end < 0:
|
|
93
|
+
return False
|
|
94
|
+
literal, rest = authority[1:end], authority[end + 1 :]
|
|
95
|
+
try:
|
|
96
|
+
ipaddress.IPv6Address(literal)
|
|
97
|
+
except ValueError:
|
|
98
|
+
return False
|
|
99
|
+
port = rest[1:] if rest.startswith(":") else None
|
|
100
|
+
if rest and port is None:
|
|
101
|
+
return False
|
|
102
|
+
else:
|
|
103
|
+
host, sep, port_text = authority.partition(":")
|
|
104
|
+
port = port_text if sep else None
|
|
105
|
+
if not host or not _valid_chars(host, _REG_NAME_CHARS):
|
|
106
|
+
return False
|
|
107
|
+
return port is None or port.isdigit() or port == ""
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def is_http_uri(value: str) -> bool:
|
|
111
|
+
"""True for an absolute ``http``/``https`` URI with an optional fragment."""
|
|
112
|
+
match = _HTTP_URI.fullmatch(value)
|
|
113
|
+
if match is None or match["scheme"].lower() not in ("http", "https"):
|
|
114
|
+
return False
|
|
115
|
+
if not value.isascii():
|
|
116
|
+
return False
|
|
117
|
+
if not _valid_authority(match["authority"]):
|
|
118
|
+
return False
|
|
119
|
+
if not _valid_chars(match["path"], _PATH_CHARS):
|
|
120
|
+
return False
|
|
121
|
+
query = match["query"]
|
|
122
|
+
if query is not None and not _valid_chars(query, _QUERY_CHARS):
|
|
123
|
+
return False
|
|
124
|
+
fragment = match["fragment"]
|
|
125
|
+
return fragment is None or _valid_chars(fragment, _QUERY_CHARS)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _percent_normalize(value: str, keep: frozenset[str], *, fold_case: bool = False) -> str:
|
|
129
|
+
"""Decode %HH into bytes, optionally ASCII-lowercase, and re-encode canonically.
|
|
130
|
+
|
|
131
|
+
Characters in ``keep`` (and unreserved characters) are written literally;
|
|
132
|
+
every other byte becomes an uppercase %HH escape.
|
|
133
|
+
"""
|
|
134
|
+
raw = bytearray()
|
|
135
|
+
index = 0
|
|
136
|
+
while index < len(value):
|
|
137
|
+
ch = value[index]
|
|
138
|
+
if ch == "%":
|
|
139
|
+
raw.append(int(value[index + 1 : index + 3], 16))
|
|
140
|
+
index += 3
|
|
141
|
+
else:
|
|
142
|
+
raw.extend(ch.encode("utf-8"))
|
|
143
|
+
index += 1
|
|
144
|
+
if fold_case:
|
|
145
|
+
raw = bytearray(raw.lower()) # bytes.lower() affects ASCII letters only
|
|
146
|
+
out = []
|
|
147
|
+
literal = keep | _UNRESERVED
|
|
148
|
+
for byte in raw:
|
|
149
|
+
ch = chr(byte)
|
|
150
|
+
out.append(ch if byte < 0x80 and ch in literal else f"%{byte:02X}")
|
|
151
|
+
return "".join(out)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _normalize_escapes(value: str, *, lower: bool = False) -> str:
|
|
155
|
+
"""RFC 3986 §6.2.2: uppercase %HH, decode escaped unreserved characters only."""
|
|
156
|
+
out: list[str] = []
|
|
157
|
+
index = 0
|
|
158
|
+
while index < len(value):
|
|
159
|
+
ch = value[index]
|
|
160
|
+
if ch == "%":
|
|
161
|
+
byte = int(value[index + 1 : index + 3], 16)
|
|
162
|
+
decoded = chr(byte)
|
|
163
|
+
if decoded in _UNRESERVED:
|
|
164
|
+
out.append(decoded.lower() if lower else decoded)
|
|
165
|
+
else:
|
|
166
|
+
out.append(f"%{byte:02X}")
|
|
167
|
+
index += 3
|
|
168
|
+
else:
|
|
169
|
+
out.append(ch.lower() if lower else ch)
|
|
170
|
+
index += 1
|
|
171
|
+
return "".join(out)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _url_key(value: str) -> str:
|
|
175
|
+
match = _HTTP_URI.fullmatch(value)
|
|
176
|
+
if match is None: # pragma: no cover - validated before
|
|
177
|
+
raise ValueError("not an HTTP URI")
|
|
178
|
+
scheme = match["scheme"].lower()
|
|
179
|
+
authority = match["authority"]
|
|
180
|
+
if authority.startswith("["):
|
|
181
|
+
end = authority.index("]")
|
|
182
|
+
host = "[" + str(ipaddress.IPv6Address(authority[1:end])) + "]"
|
|
183
|
+
port = authority[end + 2 :] if authority[end + 1 :].startswith(":") else ""
|
|
184
|
+
else:
|
|
185
|
+
host_text, _, port = authority.partition(":")
|
|
186
|
+
host = _normalize_escapes(host_text, lower=True)
|
|
187
|
+
if port and port.lstrip("0") == ("80" if scheme == "http" else "443"):
|
|
188
|
+
port = ""
|
|
189
|
+
port = port.lstrip("0") or ("0" if port else "")
|
|
190
|
+
path = _normalize_escapes(match["path"]) or "/"
|
|
191
|
+
key = f"{scheme}://{host}{':' + port if port else ''}{path}"
|
|
192
|
+
if match["query"] is not None:
|
|
193
|
+
key += "?" + _normalize_escapes(match["query"])
|
|
194
|
+
if match["fragment"] is not None:
|
|
195
|
+
key += "#" + _normalize_escapes(match["fragment"])
|
|
196
|
+
return key
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def parse_citation_target(target: str) -> CitationTarget | None:
|
|
200
|
+
"""Validate a §4.4 target; None if it uses none of the permitted forms."""
|
|
201
|
+
if target.startswith("sci:"):
|
|
202
|
+
digest = target[len("sci:sha256:") :] if target.startswith("sci:sha256:") else ""
|
|
203
|
+
if not is_digest(digest):
|
|
204
|
+
return None
|
|
205
|
+
return CitationTarget(TargetKind.SCI, target, target)
|
|
206
|
+
if target.startswith("doi:"):
|
|
207
|
+
match = _DOI.fullmatch(target)
|
|
208
|
+
if match is None or not target.isascii() or not _valid_chars(match["suffix"], _PATH_CHARS):
|
|
209
|
+
return None
|
|
210
|
+
suffix = _percent_normalize(match["suffix"], _PATH_CHARS - _UNRESERVED, fold_case=True)
|
|
211
|
+
return CitationTarget(TargetKind.DOI, target, f"doi:{match['prefix']}/{suffix}")
|
|
212
|
+
if is_http_uri(target):
|
|
213
|
+
return CitationTarget(TargetKind.URL, target, _url_key(target))
|
|
214
|
+
return None
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def citation_target_key(target: str) -> str:
|
|
218
|
+
"""Normalized comparison key for a citation target.
|
|
219
|
+
|
|
220
|
+
* SCI targets are returned unchanged (they are already canonical).
|
|
221
|
+
* DOI targets are percent-decoded, ASCII-lowercased, and re-encoded with
|
|
222
|
+
uppercase escapes for every byte outside RFC 3986 ``pchar``/``/``.
|
|
223
|
+
* URLs get RFC 3986 §6.2.2-6.2.3 normalization: lowercase scheme and host,
|
|
224
|
+
default port removed, empty path as ``/``, percent-encoding normalized.
|
|
225
|
+
The fragment is retained.
|
|
226
|
+
|
|
227
|
+
Raises:
|
|
228
|
+
ValueError: ``target`` is not a valid citation target.
|
|
229
|
+
"""
|
|
230
|
+
parsed = parse_citation_target(target)
|
|
231
|
+
if parsed is None:
|
|
232
|
+
raise ValueError("not a valid SCI, DOI, or HTTP(S) citation target")
|
|
233
|
+
return parsed.key
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
-----BEGIN CERTIFICATE-----
|
|
2
|
+
MIIH/zCCBeegAwIBAgIJAMHphhYNqOmAMA0GCSqGSIb3DQEBDQUAMIGVMREwDwYD
|
|
3
|
+
VQQKEwhGcmVlIFRTQTEQMA4GA1UECxMHUm9vdCBDQTEYMBYGA1UEAxMPd3d3LmZy
|
|
4
|
+
ZWV0c2Eub3JnMSIwIAYJKoZIhvcNAQkBFhNidXNpbGV6YXNAZ21haWwuY29tMRIw
|
|
5
|
+
EAYDVQQHEwlXdWVyemJ1cmcxDzANBgNVBAgTBkJheWVybjELMAkGA1UEBhMCREUw
|
|
6
|
+
HhcNMTYwMzEzMDE1MjEzWhcNNDEwMzA3MDE1MjEzWjCBlTERMA8GA1UEChMIRnJl
|
|
7
|
+
ZSBUU0ExEDAOBgNVBAsTB1Jvb3QgQ0ExGDAWBgNVBAMTD3d3dy5mcmVldHNhLm9y
|
|
8
|
+
ZzEiMCAGCSqGSIb3DQEJARYTYnVzaWxlemFzQGdtYWlsLmNvbTESMBAGA1UEBxMJ
|
|
9
|
+
V3VlcnpidXJnMQ8wDQYDVQQIEwZCYXllcm4xCzAJBgNVBAYTAkRFMIICIjANBgkq
|
|
10
|
+
hkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAtgKODjAy8REQ2WTNqUudAnjhlCrpE6ql
|
|
11
|
+
mQfNppeTmVvZrH4zutn+NwTaHAGpjSGv4/WRpZ1wZ3BRZ5mPUBZyLgq0YrIfQ5Fx
|
|
12
|
+
0s/MRZPzc1r3lKWrMR9sAQx4mN4z11xFEO529L0dFJjPF9MD8Gpd2feWzGyptlel
|
|
13
|
+
b+PqT+++fOa2oY0+NaMM7l/xcNHPOaMz0/2olk0i22hbKeVhvokPCqhFhzsuhKsm
|
|
14
|
+
q4Of/o+t6dI7sx5h0nPMm4gGSRhfq+z6BTRgCrqQG2FOLoVFgt6iIm/BnNffUr7V
|
|
15
|
+
DYd3zZmIwFOj/H3DKHoGik/xK3E82YA2ZulVOFRW/zj4ApjPa5OFbpIkd0pmzxzd
|
|
16
|
+
EcL479hSA9dFiyVmSxPtY5ze1P+BE9bMU1PScpRzw8MHFXxyKqW13Qv7LWw4sbk3
|
|
17
|
+
SciB7GACbQiVGzgkvXG6y85HOuvWNvC5GLSiyP9GlPB0V68tbxz4JVTRdw/Xn/XT
|
|
18
|
+
FNzRBM3cq8lBOAVt/PAX5+uFcv1S9wFE8YjaBfWCP1jdBil+c4e+0tdywT2oJmYB
|
|
19
|
+
BF/kEt1wmGwMmHunNEuQNzh1FtJY54hbUfiWi38mASE7xMtMhfj/C4SvapiDN837
|
|
20
|
+
gYaPfs8x3KZxbX7C3YAsFnJinlwAUss1fdKar8Q/YVs7H/nU4c4Ixxxz4f67fcVq
|
|
21
|
+
M2ITKentbCMCAwEAAaOCAk4wggJKMAwGA1UdEwQFMAMBAf8wDgYDVR0PAQH/BAQD
|
|
22
|
+
AgHGMB0GA1UdDgQWBBT6VQ2MNGZRQ0z357OnbJWveuaklzCBygYDVR0jBIHCMIG/
|
|
23
|
+
gBT6VQ2MNGZRQ0z357OnbJWveuakl6GBm6SBmDCBlTERMA8GA1UEChMIRnJlZSBU
|
|
24
|
+
U0ExEDAOBgNVBAsTB1Jvb3QgQ0ExGDAWBgNVBAMTD3d3dy5mcmVldHNhLm9yZzEi
|
|
25
|
+
MCAGCSqGSIb3DQEJARYTYnVzaWxlemFzQGdtYWlsLmNvbTESMBAGA1UEBxMJV3Vl
|
|
26
|
+
cnpidXJnMQ8wDQYDVQQIEwZCYXllcm4xCzAJBgNVBAYTAkRFggkAwemGFg2o6YAw
|
|
27
|
+
MwYDVR0fBCwwKjAooCagJIYiaHR0cDovL3d3dy5mcmVldHNhLm9yZy9yb290X2Nh
|
|
28
|
+
LmNybDCBzwYDVR0gBIHHMIHEMIHBBgorBgEEAYHyJAEBMIGyMDMGCCsGAQUFBwIB
|
|
29
|
+
FidodHRwOi8vd3d3LmZyZWV0c2Eub3JnL2ZyZWV0c2FfY3BzLmh0bWwwMgYIKwYB
|
|
30
|
+
BQUHAgEWJmh0dHA6Ly93d3cuZnJlZXRzYS5vcmcvZnJlZXRzYV9jcHMucGRmMEcG
|
|
31
|
+
CCsGAQUFBwICMDsaOUZyZWVUU0EgdHJ1c3RlZCB0aW1lc3RhbXBpbmcgU29mdHdh
|
|
32
|
+
cmUgYXMgYSBTZXJ2aWNlIChTYWFTKTA3BggrBgEFBQcBAQQrMCkwJwYIKwYBBQUH
|
|
33
|
+
MAGGG2h0dHA6Ly93d3cuZnJlZXRzYS5vcmc6MjU2MDANBgkqhkiG9w0BAQ0FAAOC
|
|
34
|
+
AgEAaK9+v5OFYu9M6ztYC+L69sw1omdyli89lZAfpWMMh9CRmJhM6KBqM/ipwoLt
|
|
35
|
+
nxyxGsbCPhcQjuTvzm+ylN6VwTMmIlVyVSLKYZcdSjt/eCUN+41K7sD7GVmxZBAF
|
|
36
|
+
ILnBDmTGJmLkrU0KuuIpj8lI/E6Z6NnmuP2+RAQSHsfBQi6sssnXMo4HOW5gtPO7
|
|
37
|
+
gDrUpVXID++1P4XndkoKn7Svw5n0zS9fv1hxBcYIHPPQUze2u30bAQt0n0iIyRLz
|
|
38
|
+
aWuhtpAtd7ffwEbASgzB7E+NGF4tpV37e8KiA2xiGSRqT5ndu28fgpOY87gD3ArZ
|
|
39
|
+
DctZvvTCfHdAS5kEO3gnGGeZEVLDmfEsv8TGJa3AljVa5E40IQDsUXpQLi8G+UC4
|
|
40
|
+
1DWZu8EVT4rnYaCw1VX7ShOR1PNCCvjb8S8tfdudd9zhU3gEB0rxdeTy1tVbNLXW
|
|
41
|
+
99y90xcwr1ZIDUwM/xQ/noO8FRhm0LoPC73Ef+J4ZBdrvWwauF3zJe33d4ibxEcb
|
|
42
|
+
8/pz5WzFkeixYM2nsHhqHsBKw7JPouKNXRnl5IAE1eFmqDyC7G/VT7OF669xM6hb
|
|
43
|
+
Ut5G21JE4cNK6NNucS+fzg1JPX0+3VhsYZjj7D5uljRvQXrJ8iHgr/M6j2oLHvTA
|
|
44
|
+
I2MLdq2qjZFDOCXsxBxJpbmLGBx9ow6ZerlUxzws2AWv2pk=
|
|
45
|
+
-----END CERTIFICATE-----
|