Skip to content

API documentation of security

In this module we define important classes for signing, encryption etc. Please be aware that this module has not yet undergone a security audit and is still in an early version. Any suggestions for improvements will be very welcome.

JWK

Bases: BaseModel

The JSON Web Key (JWK) for Ed25519 as standardized in

https://datatracker.ietf.org/doc/html/rfc8037

Source code in src/sqooler/security.py
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
class JWK(BaseModel):
    """
    The JSON Web Key (JWK) for Ed25519 as standardized in

    https://datatracker.ietf.org/doc/html/rfc8037
    """

    x: Base64UrlBytes = Field(
        description="Contain the public key encoded using the base64url encoding"
    )
    key_ops: Literal["sign", "verify"] = Field(
        description="Identifies the operation for which the key is intended to be used"
    )
    kid: str = Field(description="The key id of the key")
    d: Optional[Base64UrlBytes] = Field(
        default=None,
        description="Contains the private key encoded using the base64url encoding.",
    )
    kty: Literal["OKP"] = Field(
        default="OKP",
        description="Identifies the cryptographic algorithm family used with the key",
    )
    alg: Literal["EdDSA"] = Field(
        default="EdDSA", description="The algorithm used for signing"
    )
    crv: Literal["Ed25519"] = Field(
        default="Ed25519", description="Identifies the cryptographic curve used"
    )

    def to_config_str(self) -> str:
        """
        Convert the JWK to a string that can be stored in a config file.
        """
        # now it would be nice to have the whole thing as a string
        jwk_string = self.model_dump_json()

        # create a byte string
        jwk_bytes = jwk_string.encode("utf-8")

        # and now we can base64 encode it
        jwk_base64 = base64.urlsafe_b64encode(jwk_bytes)

        # and for storing it in a file we would like to decode it
        jwk_base64_str = jwk_base64.decode("utf-8")
        return jwk_base64_str

to_config_str()

Convert the JWK to a string that can be stored in a config file.

Source code in src/sqooler/security.py
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
def to_config_str(self) -> str:
    """
    Convert the JWK to a string that can be stored in a config file.
    """
    # now it would be nice to have the whole thing as a string
    jwk_string = self.model_dump_json()

    # create a byte string
    jwk_bytes = jwk_string.encode("utf-8")

    # and now we can base64 encode it
    jwk_base64 = base64.urlsafe_b64encode(jwk_bytes)

    # and for storing it in a file we would like to decode it
    jwk_base64_str = jwk_base64.decode("utf-8")
    return jwk_base64_str

JWSDict

Bases: BaseModel

A JSON Web Signature in a dictionary form. We follow the JWS standard as defined in RFC 7515.

https://datatracker.ietf.org/doc/html/rfc7515

Source code in src/sqooler/security.py
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
class JWSDict(BaseModel):
    """
    A JSON Web Signature in a dictionary form. We follow the JWS standard as defined in RFC 7515.

    https://datatracker.ietf.org/doc/html/rfc7515
    """

    header: Annotated[JWSHeader, Field(description="The header of the JWS object")]
    payload: Annotated[dict, Field(description="The payload of the JWS object")]
    signature: Annotated[
        Base64UrlBytes,
        Field(description="The signature of the JWS object."),
    ]

    def verify_signature(self, public_jwk: JWK) -> bool:
        """
        Verify the integraty of JWS object.

        Args:
            public_jwk: The public key to use for verification

        Returns:
            if the signature can be verified
        """

        if not public_jwk.key_ops == "verify":
            raise ValueError("The key is not intended for verification")

        public_key = Ed25519PublicKey.from_public_bytes(public_jwk.x)

        header_base64 = self.header.to_base64url()  # pylint: disable=no-member
        payload_base64 = payload_to_base64url(self.payload)
        full_message = header_base64 + b"." + payload_base64

        try:
            public_key.verify(self.signature, full_message)
            return True
        except InvalidSignature:
            return False

verify_signature(public_jwk)

Verify the integraty of JWS object.

Parameters:

Name Type Description Default
public_jwk JWK

The public key to use for verification

required

Returns:

Type Description
bool

if the signature can be verified

Source code in src/sqooler/security.py
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
def verify_signature(self, public_jwk: JWK) -> bool:
    """
    Verify the integraty of JWS object.

    Args:
        public_jwk: The public key to use for verification

    Returns:
        if the signature can be verified
    """

    if not public_jwk.key_ops == "verify":
        raise ValueError("The key is not intended for verification")

    public_key = Ed25519PublicKey.from_public_bytes(public_jwk.x)

    header_base64 = self.header.to_base64url()  # pylint: disable=no-member
    payload_base64 = payload_to_base64url(self.payload)
    full_message = header_base64 + b"." + payload_base64

    try:
        public_key.verify(self.signature, full_message)
        return True
    except InvalidSignature:
        return False

JWSFlat

Bases: BaseModel

A serialization of a JSON Web Signature in its flat JSON form. We follow the form described in section 3 and exemplified in Annex 7 of RFC 7515. Quite importantly we have no need of the unprotected header.

https://datatracker.ietf.org/doc/html/rfc7515

Source code in src/sqooler/security.py
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
class JWSFlat(BaseModel):
    """
    A serialization of a JSON Web Signature in its flat JSON form. We follow the
    form described in section 3 and exemplified in Annex 7 of RFC 7515. Quite
    importantly we have no need of the unprotected header.

    https://datatracker.ietf.org/doc/html/rfc7515
    """

    protected: Annotated[
        Base64UrlStr, Field(description="The header of the JWS object")
    ]
    payload: Annotated[Base64UrlStr, Field(description="The payload of the JWS object")]
    signature: Annotated[
        Base64UrlBytes, Field(description="The signature of the JWS object.")
    ]

JWSHeader

Bases: BaseModel

The header of a JWS object

Source code in src/sqooler/security.py
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
class JWSHeader(BaseModel):
    """
    The header of a JWS object
    """

    alg: str = Field(default="EdDSA", description="The algorithm used for signing")
    kid: str = Field(description="The key id of the key used for signing")
    typ: str = Field(default="JWS", description="The type of the signature")
    version: str = Field(
        default="0.1", description="The base64 encoded version of the signature"
    )

    def to_base64url(self) -> bytes:
        """
        Convert the header to a base64url encoded string.

        Returns:
            bytes : The base64url encoded header
        """

        # transform into a json string
        header_json = self.model_dump_json()

        # binary encode the json string
        binary_string = header_json.encode()

        # base64 encode the binary string
        base64_encoded = base64.urlsafe_b64encode(binary_string)
        return base64_encoded

to_base64url()

Convert the header to a base64url encoded string.

Returns:

Name Type Description
bytes bytes

The base64url encoded header

Source code in src/sqooler/security.py
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
def to_base64url(self) -> bytes:
    """
    Convert the header to a base64url encoded string.

    Returns:
        bytes : The base64url encoded header
    """

    # transform into a json string
    header_json = self.model_dump_json()

    # binary encode the json string
    binary_string = header_json.encode()

    # base64 encode the binary string
    base64_encoded = base64.urlsafe_b64encode(binary_string)
    return base64_encoded

create_jwk_pair(kid)

Create a pair of JWKs designed for signing and verification.

Parameters:

Name Type Description Default
kid

The key id of the key

required

Returns:

Name Type Description
JWK tuple[JWK, JWK]

The JWK object

Source code in src/sqooler/security.py
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
def create_jwk_pair(kid: str) -> tuple[JWK, JWK]:
    """
    Create a pair of JWKs designed for signing and verification.

    Args:
        kid : The key id of the key

    Returns:
        JWK : The JWK object
    """

    # create a new key pair
    private_key = Ed25519PrivateKey.generate()
    public_key = private_key.public_key()

    # transform the keys into base64url encoded strings
    private_base64 = base64.urlsafe_b64encode(private_key.private_bytes_raw())
    public_base64 = base64.urlsafe_b64encode(public_key.public_bytes_raw())

    # create the JWK
    private_jwk = JWK(key_ops="sign", kid=kid, d=private_base64, x=public_base64)
    public_jwk = JWK(key_ops="verify", kid=kid, x=public_base64)
    return private_jwk, public_jwk

datetime_handler(in_var)

Convert a datetime object to a string.

Parameters:

Name Type Description Default
in_var

The object to convert

required

Returns:

Name Type Description
str str

The string representation of the object

Source code in src/sqooler/security.py
50
51
52
53
54
55
56
57
58
59
60
61
62
def datetime_handler(in_var: Any) -> str:
    """
    Convert a datetime object to a string.

    Args:
        in_var : The object to convert

    Returns:
        str : The string representation of the object
    """
    if isinstance(in_var, datetime.datetime):
        return in_var.isoformat()
    raise TypeError("Unknown type")

jwk_from_config_str(jwk_base64_str)

Create a JWK from a string that was stored in a config file.

Parameters:

Name Type Description Default
jwk_base64_str

The base64 encoded JWK

required

Returns:

Name Type Description
JWK JWK

The JWK object

Source code in src/sqooler/security.py
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
def jwk_from_config_str(jwk_base64_str: str) -> JWK:
    """
    Create a JWK from a string that was stored in a config file.

    Args:
        jwk_base64_str : The base64 encoded JWK

    Returns:
        JWK : The JWK object
    """
    jwk_base64 = jwk_base64_str.encode("utf-8")
    jwk_bytes = base64.urlsafe_b64decode(jwk_base64)

    jwk_json_str = jwk_bytes.decode("utf-8")
    jwk_dict = json.loads(jwk_json_str)
    jwk = JWK(**jwk_dict)
    return jwk

payload_to_base64url(payload)

Convert an arbitrary payload to a base64url encoded string.

Parameters:

Name Type Description Default
payload

The dictionary to encode

required

Returns:

Name Type Description
bytes bytes

The base64url encoded header

Source code in src/sqooler/security.py
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
def payload_to_base64url(payload: dict) -> bytes:
    """
    Convert an arbitrary payload to a base64url encoded string.

    Args:
        payload : The dictionary to encode

    Returns:
        bytes : The base64url encoded header
    """

    # transform into a json string
    payload_string = json.dumps(payload, default=datetime_handler)

    # binary encode the json string
    binary_string = payload_string.encode()

    # base64 encode the binary string
    base64_encoded = base64.urlsafe_b64encode(binary_string)
    return base64_encoded

public_from_private_jwk(private_jwk)

Create a public JWK from a private JWK.

Parameters:

Name Type Description Default
private_jwk

The private JWK

required

Returns:

Name Type Description
JWK JWK

The public JWK

Raises:

Type Description
ValueError

If the private key is not intended for signing

Source code in src/sqooler/security.py
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
def public_from_private_jwk(private_jwk: JWK) -> JWK:
    """
    Create a public JWK from a private JWK.

    Args:
        private_jwk : The private JWK

    Returns:
        JWK : The public JWK

    Raises:
        ValueError : If the private key is not intended for signing
    """

    # is the key intended for signing?
    if not private_jwk.key_ops == "sign" or private_jwk.d is None:
        raise ValueError(
            "The private key is not intended for signing. Might not be a private key."
        )
    b64_public_key = base64.urlsafe_b64encode(private_jwk.x)
    public_jwk = JWK(
        key_ops="verify",
        kid=private_jwk.kid,
        x=b64_public_key,
    )
    return public_jwk

sign_payload(payload, jwk)

Convert a payload to a JWS object.

Parameters:

Name Type Description Default
payload

The payload to convert

required
jwk JWK

The private JWK to use for signing

required

Returns:

Name Type Description
JWSDict JWSDict

The JWS object

Source code in src/sqooler/security.py
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
def sign_payload(payload: dict, jwk: JWK) -> JWSDict:
    """
    Convert a payload to a JWS object.

    Args:
        payload : The payload to convert
        jwk: The private JWK to use for signing

    Returns:
        JWSDict : The JWS object
    """

    header = JWSHeader(kid=jwk.kid)
    header_base64 = header.to_base64url()
    payload_base64 = payload_to_base64url(payload)
    full_message = header_base64 + b"." + payload_base64
    # create the private key from the JWK
    # make sure that the key is intended for signing and contains the private key
    if not jwk.key_ops == "sign":
        raise ValueError("The key is not intended for signing")
    if jwk.d is None:
        raise ValueError("The private key is missing from the JWK")

    private_key = Ed25519PrivateKey.from_private_bytes(jwk.d)

    signature = private_key.sign(full_message)
    signature_base64 = base64.urlsafe_b64encode(signature)
    return JWSDict(header=header, payload=payload, signature=signature_base64)

Comments