openbadgeslib 1.1.1__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.
- openbadgeslib/__init__.py +40 -0
- openbadgeslib/_jws/__init__.py +102 -0
- openbadgeslib/_jws/exceptions.py +18 -0
- openbadgeslib/_jws/utils.py +30 -0
- openbadgeslib/badge.py +12 -0
- openbadgeslib/baking.py +166 -0
- openbadgeslib/config.ini.example +67 -0
- openbadgeslib/confparser.py +73 -0
- openbadgeslib/errors.py +101 -0
- openbadgeslib/keys.py +175 -0
- openbadgeslib/logs.py +67 -0
- openbadgeslib/mail.py +112 -0
- openbadgeslib/ob2/__init__.py +36 -0
- openbadgeslib/ob2/badge.py +329 -0
- openbadgeslib/ob2/signer.py +152 -0
- openbadgeslib/ob2/verifier.py +178 -0
- openbadgeslib/ob3/__init__.py +34 -0
- openbadgeslib/ob3/credential.py +198 -0
- openbadgeslib/ob3/signer.py +79 -0
- openbadgeslib/ob3/verifier.py +196 -0
- openbadgeslib/openbadges_init.py +59 -0
- openbadgeslib/openbadges_keygenerator.py +112 -0
- openbadgeslib/openbadges_publish.py +142 -0
- openbadgeslib/openbadges_signer.py +204 -0
- openbadgeslib/openbadges_verifier.py +184 -0
- openbadgeslib/signer.py +4 -0
- openbadgeslib/util.py +111 -0
- openbadgeslib/verifier.py +4 -0
- openbadgeslib-1.1.1.dist-info/METADATA +316 -0
- openbadgeslib-1.1.1.dist-info/RECORD +34 -0
- openbadgeslib-1.1.1.dist-info/WHEEL +5 -0
- openbadgeslib-1.1.1.dist-info/entry_points.txt +6 -0
- openbadgeslib-1.1.1.dist-info/licenses/LICENSE.txt +165 -0
- openbadgeslib-1.1.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
OpenBadges Library
|
|
4
|
+
|
|
5
|
+
Copyright (c) 2014-2026, Luis González Fernández, luisgf@luisgf.es
|
|
6
|
+
Copyright (c) 2014-2026, Jesús Cea Avión, jcea@jcea.es
|
|
7
|
+
|
|
8
|
+
All rights reserved.
|
|
9
|
+
|
|
10
|
+
This library is free software; you can redistribute it and/or
|
|
11
|
+
modify it under the terms of the GNU Lesser General Public
|
|
12
|
+
License as published by the Free Software Foundation; either
|
|
13
|
+
version 3.0 of the License, or (at your option) any later version.
|
|
14
|
+
|
|
15
|
+
This library is distributed in the hope that it will be useful,
|
|
16
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
17
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
18
|
+
Lesser General Public License for more details.
|
|
19
|
+
|
|
20
|
+
You should have received a copy of the GNU Lesser General Public
|
|
21
|
+
License along with this library.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
# ── OpenBadges 2.0 ─────────────────────────────────────────────────────────────
|
|
25
|
+
from .ob2 import ( # noqa: F401
|
|
26
|
+
Signer, Verifier, VerifyInfo,
|
|
27
|
+
Badge, BadgeSigned, Assertion,
|
|
28
|
+
BadgeStatus, BadgeImgType, BadgeType,
|
|
29
|
+
extract_svg_assertion, extract_png_assertion,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
# ── OpenBadges 3.0 ─────────────────────────────────────────────────────────────
|
|
33
|
+
from .ob3 import ( # noqa: F401
|
|
34
|
+
OB3Signer, OB3Verifier, OB3VerificationError,
|
|
35
|
+
OpenBadgeCredential, Achievement, Issuer,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
# ── Shared utilities ────────────────────────────────────────────────────────────
|
|
39
|
+
from .keys import KeyFactory, KeyRSA, KeyECC # noqa: F401
|
|
40
|
+
from .util import __version__ # noqa: F401
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""JWS sign/verify backed by PyJWT algorithm implementations (RS256/384/512, ES256/384/512)."""
|
|
2
|
+
|
|
3
|
+
from . import utils
|
|
4
|
+
from .exceptions import SignatureError, MissingKey, MissingSigner, MissingVerifier, RouteMissingError
|
|
5
|
+
|
|
6
|
+
from jwt.algorithms import RSAAlgorithm, ECAlgorithm
|
|
7
|
+
from jwt.exceptions import InvalidKeyError
|
|
8
|
+
|
|
9
|
+
from ..keys import KeyType, detect_key_type, key_to_pem
|
|
10
|
+
from ..errors import UnknownKeyType
|
|
11
|
+
|
|
12
|
+
_ALGORITHMS = {
|
|
13
|
+
'RS256': (RSAAlgorithm, RSAAlgorithm.SHA256),
|
|
14
|
+
'RS384': (RSAAlgorithm, RSAAlgorithm.SHA384),
|
|
15
|
+
'RS512': (RSAAlgorithm, RSAAlgorithm.SHA512),
|
|
16
|
+
'ES256': (ECAlgorithm, ECAlgorithm.SHA256),
|
|
17
|
+
'ES384': (ECAlgorithm, ECAlgorithm.SHA384),
|
|
18
|
+
'ES512': (ECAlgorithm, ECAlgorithm.SHA512),
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _algo_for(alg_name):
|
|
23
|
+
entry = _ALGORITHMS.get(alg_name)
|
|
24
|
+
if entry is None:
|
|
25
|
+
raise RouteMissingError(f"Algorithm {alg_name!r} is not supported")
|
|
26
|
+
cls, hash_id = entry
|
|
27
|
+
return cls(hash_id)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _allowed_algs_for_key(key):
|
|
31
|
+
"""Signature algorithms permitted for a verification key, bound to its type.
|
|
32
|
+
|
|
33
|
+
Binding the accepted algorithm to the key type stops a forged JWS header
|
|
34
|
+
from dictating the algorithm (cross-type confusion, and—were a symmetric
|
|
35
|
+
entry ever added to _ALGORITHMS—the classic RS256->HS256 downgrade).
|
|
36
|
+
"""
|
|
37
|
+
try:
|
|
38
|
+
key_type = detect_key_type(key_to_pem(key))
|
|
39
|
+
except UnknownKeyType:
|
|
40
|
+
return set()
|
|
41
|
+
if key_type is KeyType.RSA:
|
|
42
|
+
return {'RS256', 'RS384', 'RS512'}
|
|
43
|
+
if key_type is KeyType.ECC:
|
|
44
|
+
return {'ES256', 'ES384', 'ES512'}
|
|
45
|
+
return set()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def sign(header_dict, payload_dict, key):
|
|
49
|
+
"""Sign header+payload dicts and return raw signature bytes."""
|
|
50
|
+
if key is None:
|
|
51
|
+
raise MissingKey("No signing key provided")
|
|
52
|
+
alg_name = header_dict.get('alg')
|
|
53
|
+
if not alg_name:
|
|
54
|
+
raise MissingSigner("Header is missing 'alg'")
|
|
55
|
+
|
|
56
|
+
signing_input = utils.encode(header_dict) + b'.' + utils.encode(payload_dict)
|
|
57
|
+
algo = _algo_for(alg_name)
|
|
58
|
+
try:
|
|
59
|
+
prepared = algo.prepare_key(key_to_pem(key))
|
|
60
|
+
return algo.sign(signing_input, prepared)
|
|
61
|
+
except (InvalidKeyError, ValueError) as exc:
|
|
62
|
+
raise SignatureError(str(exc)) from exc
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def verify_block(msg, key=None):
|
|
66
|
+
"""Verify a JWS compact serialization (bytes or str). Returns True or raises SignatureError."""
|
|
67
|
+
if isinstance(msg, str):
|
|
68
|
+
msg = msg.encode('utf-8')
|
|
69
|
+
|
|
70
|
+
try:
|
|
71
|
+
head_b64, payload_b64, sig_b64 = msg.split(b'.')
|
|
72
|
+
except ValueError:
|
|
73
|
+
raise SignatureError("Malformed JWS: expected header.payload.signature")
|
|
74
|
+
|
|
75
|
+
if key is None:
|
|
76
|
+
raise MissingKey("No verification key provided")
|
|
77
|
+
|
|
78
|
+
header = utils.decode(head_b64)
|
|
79
|
+
alg_name = header.get('alg')
|
|
80
|
+
if not alg_name:
|
|
81
|
+
raise MissingVerifier("JWS header is missing 'alg'")
|
|
82
|
+
|
|
83
|
+
allowed = _allowed_algs_for_key(key)
|
|
84
|
+
if allowed and alg_name not in allowed:
|
|
85
|
+
raise SignatureError(
|
|
86
|
+
"Algorithm %r in JWS header is not allowed for this key type"
|
|
87
|
+
% alg_name)
|
|
88
|
+
|
|
89
|
+
signing_input = head_b64 + b'.' + payload_b64
|
|
90
|
+
raw_sig = utils.from_base64(sig_b64)
|
|
91
|
+
|
|
92
|
+
algo = _algo_for(alg_name)
|
|
93
|
+
try:
|
|
94
|
+
prepared = algo.prepare_key(key_to_pem(key))
|
|
95
|
+
valid = algo.verify(signing_input, prepared, raw_sig)
|
|
96
|
+
except (InvalidKeyError, ValueError) as exc:
|
|
97
|
+
raise SignatureError(str(exc)) from exc
|
|
98
|
+
|
|
99
|
+
if not valid:
|
|
100
|
+
raise SignatureError("Signature verification failed")
|
|
101
|
+
|
|
102
|
+
return True
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import json
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def base64url_decode(data):
|
|
6
|
+
# JWS/JWT base64url strips the '=' padding; restore it before decoding.
|
|
7
|
+
# The required pad count is (-len) % 4, which is 0 when the length is
|
|
8
|
+
# already a multiple of 4 (the old `4 - len % 4` wrongly added 4 there).
|
|
9
|
+
data += b'=' * (-len(data) % 4)
|
|
10
|
+
return base64.urlsafe_b64decode(data)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def base64url_encode(input):
|
|
14
|
+
return base64.urlsafe_b64encode(input).replace(b'=', b'')
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def to_json(a):
|
|
18
|
+
return json.dumps(a).encode('utf-8')
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def from_json(a):
|
|
22
|
+
if isinstance(a, (bytes, bytearray)):
|
|
23
|
+
a = a.decode('utf-8')
|
|
24
|
+
return json.loads(a)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def to_base64(a): return base64url_encode(a)
|
|
28
|
+
def from_base64(a): return base64url_decode(a)
|
|
29
|
+
def encode(a): return to_base64(to_json(a))
|
|
30
|
+
def decode(a): return from_json(from_base64(a))
|
openbadgeslib/badge.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""OpenBadges 2.0 badge objects — compatibility shim, re-exports from openbadgeslib.ob2."""
|
|
2
|
+
from .ob2.badge import (
|
|
3
|
+
BadgeStatus, BadgeImgType, BadgeType,
|
|
4
|
+
Assertion, Badge, BadgeSigned,
|
|
5
|
+
extract_svg_assertion, extract_png_assertion,
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
'BadgeStatus', 'BadgeImgType', 'BadgeType',
|
|
10
|
+
'Assertion', 'Badge', 'BadgeSigned',
|
|
11
|
+
'extract_svg_assertion', 'extract_png_assertion',
|
|
12
|
+
]
|
openbadgeslib/baking.py
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OpenBadges Library
|
|
3
|
+
|
|
4
|
+
Copyright (c) 2014-2026, Luis González Fernández, luisgf@luisgf.es
|
|
5
|
+
Copyright (c) 2014-2026, Jesús Cea Avión, jcea@jcea.es
|
|
6
|
+
|
|
7
|
+
All rights reserved.
|
|
8
|
+
|
|
9
|
+
This library is free software; you can redistribute it and/or
|
|
10
|
+
modify it under the terms of the GNU Lesser General Public
|
|
11
|
+
License as published by the Free Software Foundation; either
|
|
12
|
+
version 3.0 of the License, or (at your option) any later version.
|
|
13
|
+
|
|
14
|
+
This library is distributed in the hope that it will be useful,
|
|
15
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
16
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
17
|
+
Lesser General Public License for more details.
|
|
18
|
+
|
|
19
|
+
You should have received a copy of the GNU Lesser General Public
|
|
20
|
+
License along with this library.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
# Version-agnostic carrier format for embedding an OpenBadges token (an OB 2.0
|
|
24
|
+
# JWS or an OB 3.0 JWT-VC) into an SVG or PNG badge image, and extracting it
|
|
25
|
+
# back out. OB2 and OB3 share the exact on-disk format; keeping a single
|
|
26
|
+
# implementation here stops the two paths from drifting (e.g. two PNG readers,
|
|
27
|
+
# one fixed-offset and one structured).
|
|
28
|
+
|
|
29
|
+
from struct import pack
|
|
30
|
+
from zlib import crc32
|
|
31
|
+
|
|
32
|
+
from defusedxml.minidom import parseString
|
|
33
|
+
from png import Reader, signature as _png_signature
|
|
34
|
+
|
|
35
|
+
ITXT_KEYWORD = b'openbadges'
|
|
36
|
+
SVG_ELEMENT = 'openbadges:assertion'
|
|
37
|
+
|
|
38
|
+
# Maximum bytes a compressed iTXt token is allowed to inflate to. A JWS/JWT-VC
|
|
39
|
+
# is a few KB; this cap stops a crafted zlib bomb from exhausting memory during
|
|
40
|
+
# extraction (which runs on untrusted input, before any signature check).
|
|
41
|
+
MAX_ITXT_DECOMPRESSED = 256 * 1024
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class DecompressionLimitExceeded(Exception):
|
|
45
|
+
"""Raised when a compressed iTXt token inflates beyond the allowed size."""
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _bounded_inflate(data, limit=MAX_ITXT_DECOMPRESSED):
|
|
49
|
+
import zlib
|
|
50
|
+
inflator = zlib.decompressobj()
|
|
51
|
+
out = inflator.decompress(data, limit)
|
|
52
|
+
if inflator.unconsumed_tail:
|
|
53
|
+
raise DecompressionLimitExceeded(
|
|
54
|
+
"Compressed token exceeds the %d-byte limit" % limit)
|
|
55
|
+
return out
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# ── SVG ─────────────────────────────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
def bake_svg(image_bytes, token, comment=None):
|
|
61
|
+
"""Return *image_bytes* with an ``<openbadges:assertion verify=token>``
|
|
62
|
+
element (and an optional XML comment) appended to the root ``<svg>``."""
|
|
63
|
+
svg_doc = parseString(image_bytes)
|
|
64
|
+
try:
|
|
65
|
+
svg_tag = svg_doc.getElementsByTagName('svg').item(0)
|
|
66
|
+
node = svg_doc.createElement(SVG_ELEMENT)
|
|
67
|
+
node.attributes['xmlns:openbadges'] = 'http://openbadges.org'
|
|
68
|
+
node.attributes['verify'] = token
|
|
69
|
+
svg_tag.appendChild(node)
|
|
70
|
+
if comment:
|
|
71
|
+
svg_tag.appendChild(svg_doc.createComment(comment))
|
|
72
|
+
return svg_doc.toxml().encode('utf-8')
|
|
73
|
+
finally:
|
|
74
|
+
svg_doc.unlink()
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def has_svg(image_bytes):
|
|
78
|
+
"""Return True if *image_bytes* already carries an OpenBadges assertion."""
|
|
79
|
+
svg_doc = parseString(image_bytes)
|
|
80
|
+
try:
|
|
81
|
+
return bool(svg_doc.getElementsByTagName(SVG_ELEMENT))
|
|
82
|
+
finally:
|
|
83
|
+
svg_doc.unlink()
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def extract_svg(image_bytes):
|
|
87
|
+
"""Return the embedded token string, or None if there is no assertion node.
|
|
88
|
+
|
|
89
|
+
Raises on malformed XML (left to the caller to map to its own error type).
|
|
90
|
+
"""
|
|
91
|
+
svg_doc = None
|
|
92
|
+
try:
|
|
93
|
+
svg_doc = parseString(image_bytes)
|
|
94
|
+
nodes = svg_doc.getElementsByTagName(SVG_ELEMENT)
|
|
95
|
+
if not nodes:
|
|
96
|
+
return None
|
|
97
|
+
return nodes[0].attributes['verify'].nodeValue
|
|
98
|
+
finally:
|
|
99
|
+
if svg_doc is not None:
|
|
100
|
+
svg_doc.unlink()
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# ── PNG ─────────────────────────────────────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
def _serialize_png(chunks):
|
|
106
|
+
out = _png_signature
|
|
107
|
+
for tag, data in chunks:
|
|
108
|
+
out += pack("!I", len(data))
|
|
109
|
+
if isinstance(tag, str):
|
|
110
|
+
tag = tag.encode('iso8859-1')
|
|
111
|
+
out += tag + data
|
|
112
|
+
checksum = crc32(tag)
|
|
113
|
+
checksum = crc32(data, checksum) & 0xFFFFFFFF
|
|
114
|
+
out += pack("!I", checksum)
|
|
115
|
+
return out
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def bake_png(image_bytes, token, text_comment=None):
|
|
119
|
+
"""Return *image_bytes* with the token stored in an ``openbadges`` iTXt
|
|
120
|
+
chunk (and an optional ``tEXt`` comment chunk) inserted before IEND."""
|
|
121
|
+
chunks = list(Reader(bytes=image_bytes).chunks())
|
|
122
|
+
itxt_data = ITXT_KEYWORD + pack('BBBBB', 0, 0, 0, 0, 0) + token.encode('utf-8')
|
|
123
|
+
chunks.insert(len(chunks) - 1, ('iTXt', itxt_data))
|
|
124
|
+
if text_comment:
|
|
125
|
+
chunks.insert(len(chunks) - 1, ('tEXt', text_comment.encode('utf-8')))
|
|
126
|
+
return _serialize_png(chunks)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def has_png(image_bytes):
|
|
130
|
+
"""Return True if *image_bytes* already carries an OpenBadges iTXt chunk."""
|
|
131
|
+
for tag, data in Reader(bytes=image_bytes).chunks():
|
|
132
|
+
tag_str = tag.decode('ascii') if isinstance(tag, bytes) else tag
|
|
133
|
+
if tag_str == 'iTXt' and data.startswith(ITXT_KEYWORD):
|
|
134
|
+
return True
|
|
135
|
+
return False
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def extract_png(image_bytes, max_decompressed=MAX_ITXT_DECOMPRESSED):
|
|
139
|
+
"""Return the embedded token string, or None if there is no openbadges
|
|
140
|
+
iTXt chunk.
|
|
141
|
+
|
|
142
|
+
Parses the iTXt structure (keyword, compression flag/method, language tag,
|
|
143
|
+
translated keyword, then text) rather than a fixed byte offset, so tokens
|
|
144
|
+
baked by any conformant tool — including compressed ones — are recovered.
|
|
145
|
+
Raises :class:`DecompressionLimitExceeded` if a compressed token inflates
|
|
146
|
+
beyond *max_decompressed*.
|
|
147
|
+
"""
|
|
148
|
+
for tag, data in Reader(bytes=image_bytes).chunks():
|
|
149
|
+
tag_str = tag.decode('ascii') if isinstance(tag, bytes) else tag
|
|
150
|
+
if tag_str != 'iTXt':
|
|
151
|
+
continue
|
|
152
|
+
|
|
153
|
+
# iTXt layout: keyword \0 comp_flag comp_method lang \0 trans \0 text
|
|
154
|
+
keyword, sep, rest = data.partition(b'\x00')
|
|
155
|
+
if sep != b'\x00' or keyword != ITXT_KEYWORD or len(rest) < 2:
|
|
156
|
+
continue
|
|
157
|
+
compression_flag = rest[0]
|
|
158
|
+
_, sep_lang, rest = rest[2:].partition(b'\x00') # drop language tag
|
|
159
|
+
_, sep_trans, text = rest.partition(b'\x00') # drop translated keyword
|
|
160
|
+
if sep_lang != b'\x00' or sep_trans != b'\x00':
|
|
161
|
+
continue
|
|
162
|
+
if compression_flag:
|
|
163
|
+
text = _bounded_inflate(text, max_decompressed)
|
|
164
|
+
return text.decode('utf-8')
|
|
165
|
+
|
|
166
|
+
return None
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
;
|
|
2
|
+
; OpenBadges Lib configuration example for RSA keys.
|
|
3
|
+
;
|
|
4
|
+
|
|
5
|
+
; Paths to the keys and log
|
|
6
|
+
[paths]
|
|
7
|
+
base = .
|
|
8
|
+
base_key = ${base}/keys
|
|
9
|
+
base_log = ${base}/log
|
|
10
|
+
base_image = ${base}/images
|
|
11
|
+
|
|
12
|
+
; Log configuration. Stored in ${base_log}
|
|
13
|
+
[logs]
|
|
14
|
+
general = general.log
|
|
15
|
+
signer = signer.log
|
|
16
|
+
|
|
17
|
+
; SMTP Configuration
|
|
18
|
+
[smtp]
|
|
19
|
+
smtp_server = localhost
|
|
20
|
+
smtp_port = 25
|
|
21
|
+
use_ssl = False
|
|
22
|
+
mail_from = no-reply@issuer.badge
|
|
23
|
+
; Uncomment this if your SMTP server needs authentication
|
|
24
|
+
;username =
|
|
25
|
+
;password =
|
|
26
|
+
|
|
27
|
+
; Configuration of the OpenBadges issuer.
|
|
28
|
+
[issuer]
|
|
29
|
+
name = OpenBadge issuer
|
|
30
|
+
url = https://www.issuer.badge
|
|
31
|
+
image = issuer_logo.png
|
|
32
|
+
email = issuer_mail@issuer.badge
|
|
33
|
+
publish_url = https://openbadges.issuer.badge/issuer/
|
|
34
|
+
revocationList = revoked.json
|
|
35
|
+
|
|
36
|
+
;Badge configuration sections.
|
|
37
|
+
[badge_1]
|
|
38
|
+
name = Badge 1
|
|
39
|
+
description = Given to any user that install this library
|
|
40
|
+
local_image = image_badge1.svg
|
|
41
|
+
image = https://www.issuer.badge/issuer/badge_1/badge1.svg
|
|
42
|
+
criteria = https://www.issuer.badge/issuer/badge_1/criteria.html
|
|
43
|
+
verify_key = https://www.issuer.badge/issuer/badge_1/verify_rsa_key.pem
|
|
44
|
+
badge = https://www.issuer.badge/issuer/badge_1/badge.json
|
|
45
|
+
private_key = ${paths:base_key}/sign_rsa_key_1.pem
|
|
46
|
+
public_key = ${paths:base_key}/verify_rsa_key_1.pem
|
|
47
|
+
; key_type selects the algorithm for openbadges-keygenerator: RSA (default) or ECC
|
|
48
|
+
key_type = RSA
|
|
49
|
+
;alignement =
|
|
50
|
+
;tags =
|
|
51
|
+
;mail = ${paths:base}/badge_1_mail.txt
|
|
52
|
+
|
|
53
|
+
[badge_2]
|
|
54
|
+
name = Badge 2
|
|
55
|
+
description = Given to any user that promote the library
|
|
56
|
+
local_image = image_badge2.svg
|
|
57
|
+
image = https://www.issuer.badge/issuer/badge_2/badge2.svg
|
|
58
|
+
criteria = https://www.issuer.badge/issuer/badge_2/criteria.html
|
|
59
|
+
verify_key = https://www.issuer.badge/issuer/badge_2/verify_rsa_key.pem
|
|
60
|
+
badge = https://www.issuer.badge/issuer/badge_2/badge.json
|
|
61
|
+
private_key = ${paths:base_key}/sign_rsa_key_2.pem
|
|
62
|
+
public_key = ${paths:base_key}/verify_rsa_key_2.pem
|
|
63
|
+
key_type = RSA
|
|
64
|
+
;alignement =
|
|
65
|
+
;tags =
|
|
66
|
+
;mail = ${paths:base}/badge_2_mail.txt
|
|
67
|
+
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
OpenBadges Library
|
|
4
|
+
|
|
5
|
+
Copyright (c) 2014-2026, Luis González Fernández, luisgf@luisgf.es
|
|
6
|
+
Copyright (c) 2014-2026, Jesús Cea Avión, jcea@jcea.es
|
|
7
|
+
|
|
8
|
+
All rights reserved.
|
|
9
|
+
|
|
10
|
+
This library is free software; you can redistribute it and/or
|
|
11
|
+
modify it under the terms of the GNU Lesser General Public
|
|
12
|
+
License as published by the Free Software Foundation; either
|
|
13
|
+
version 3.0 of the License, or (at your option) any later version.
|
|
14
|
+
|
|
15
|
+
This library is distributed in the hope that it will be useful,
|
|
16
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
17
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
18
|
+
Lesser General Public License for more details.
|
|
19
|
+
|
|
20
|
+
You should have received a copy of the GNU Lesser General Public
|
|
21
|
+
License along with this library.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from configparser import ConfigParser, ExtendedInterpolation
|
|
25
|
+
import os
|
|
26
|
+
import sys
|
|
27
|
+
import logging
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def read_config_or_exit(config_file):
|
|
32
|
+
"""Read a config file for a CLI tool, exiting with a clear message if it is
|
|
33
|
+
missing or empty. Shared by all the console-script entrypoints."""
|
|
34
|
+
conf = ConfParser(config_file).read_conf()
|
|
35
|
+
if not conf:
|
|
36
|
+
print('[!] The config file %s does not exist or is empty' % config_file)
|
|
37
|
+
sys.exit(-1)
|
|
38
|
+
return conf
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def resolve_badge_section(conf, name):
|
|
42
|
+
"""Return the ``badge_<name>`` section name, exiting if it is not defined."""
|
|
43
|
+
section = 'badge_' + name
|
|
44
|
+
if section not in conf:
|
|
45
|
+
sys.exit('There is no "%s" badge in the configuration' % name)
|
|
46
|
+
return section
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class ConfParser():
|
|
50
|
+
def __init__(self, config_file='config.ini'):
|
|
51
|
+
self.config_file = config_file
|
|
52
|
+
|
|
53
|
+
def read_conf(self):
|
|
54
|
+
if not os.path.isfile(self.config_file):
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
self.parser = ConfigParser(interpolation=ExtendedInterpolation())
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
self.parser.read(self.config_file)
|
|
61
|
+
except UnicodeDecodeError:
|
|
62
|
+
# We should raise an UnicodeDecodeError, but the error message is too cryptic.#
|
|
63
|
+
raise ValueError("The encoding of the configuration file and the default encoding of "
|
|
64
|
+
"the operating system mismatch") from None
|
|
65
|
+
if self.parser['paths']['base'][0] == '.':
|
|
66
|
+
abs_path = os.path.dirname(self.config_file)
|
|
67
|
+
full_path = os.path.abspath(abs_path)
|
|
68
|
+
self.parser['paths']['base'] = full_path
|
|
69
|
+
return self.parser
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
if __name__ == '__main__':
|
|
73
|
+
pass
|
openbadgeslib/errors.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
OpenBadges Library
|
|
4
|
+
|
|
5
|
+
Copyright (c) 2014-2026, Luis González Fernández, luisgf@luisgf.es
|
|
6
|
+
Copyright (c) 2014-2026, Jesús Cea Avión, jcea@jcea.es
|
|
7
|
+
|
|
8
|
+
All rights reserved.
|
|
9
|
+
|
|
10
|
+
This library is free software; you can redistribute it and/or
|
|
11
|
+
modify it under the terms of the GNU Lesser General Public
|
|
12
|
+
License as published by the Free Software Foundation; either
|
|
13
|
+
version 3.0 of the License, or (at your option) any later version.
|
|
14
|
+
|
|
15
|
+
This library is distributed in the hope that it will be useful,
|
|
16
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
17
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
18
|
+
Lesser General Public License for more details.
|
|
19
|
+
|
|
20
|
+
You should have received a copy of the GNU Lesser General Public
|
|
21
|
+
License along with this library.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class LibOpenBadgesException(Exception):
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
""" Exception base classes """
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class KeyGenExceptions(LibOpenBadgesException):
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class SignerExceptions(LibOpenBadgesException):
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class VerifierExceptions(LibOpenBadgesException):
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
""" User-defined Exceptions """
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class GenPrivateKeyError(KeyGenExceptions):
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class GenPublicKeyError(KeyGenExceptions):
|
|
52
|
+
pass
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class PrivateKeySaveError(KeyGenExceptions):
|
|
56
|
+
pass
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class PublicKeySaveError(KeyGenExceptions):
|
|
60
|
+
pass
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class PrivateKeyReadError(KeyGenExceptions):
|
|
64
|
+
pass
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class PublicKeyReadError(KeyGenExceptions):
|
|
68
|
+
pass
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class UnknownKeyType(KeyGenExceptions):
|
|
72
|
+
pass
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
""" Signer Exceptions """
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class ErrorSigningFile(SignerExceptions):
|
|
79
|
+
pass
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
""" Verifier Exceptions """
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class AssertionFormatIncorrect(VerifierExceptions):
|
|
86
|
+
pass
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class NotIdentityInAssertion(VerifierExceptions):
|
|
90
|
+
pass
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class ErrorParsingFile(VerifierExceptions):
|
|
94
|
+
pass
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
""" Badge Object Exceptions """
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class BadgeImgFormatUnsupported(LibOpenBadgesException):
|
|
101
|
+
pass
|