Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cosmetics #18

Merged
merged 14 commits into from
Jul 24, 2023
22 changes: 16 additions & 6 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,15 @@ repos:
rev: v4.4.0
hooks:
- id: trailing-whitespace
args: [--markdown-linebreak-ext=md]
- id: end-of-file-fixer
- id: check-merge-conflict
- id: check-yaml
args: [--unsafe]
- id: debug-statements
- id: mixed-line-ending
args: [--fix=lf]
- id: no-commit-to-branch
- repo: https://github.com/pre-commit/pygrep-hooks
rev: v1.10.0
hooks:
Expand All @@ -22,16 +27,21 @@ repos:
- id: python-no-eval
- id: python-use-type-annotations
- id: text-unicode-replacement-char
- repo: https://github.com/asottile/pyupgrade
rev: v3.9.0
hooks:
- id: pyupgrade
args: [--py38-plus]
- repo: https://github.com/myint/docformatter
rev: v1.7.2
rev: v1.7.5
hooks:
- id: docformatter
args:
- --in-place
- --wrap-summaries=100
- --wrap-descriptions=100
- repo: https://github.com/hadialqattan/pycln
rev: v2.1.5
rev: v2.1.6
hooks:
- id: pycln
args: [--config=pyproject.toml]
Expand All @@ -40,7 +50,7 @@ repos:
hooks:
- id: isort
- repo: https://github.com/psf/black
rev: 23.3.0
rev: 23.7.0
hooks:
- id: black
- repo: https://github.com/pycqa/flake8
Expand All @@ -50,7 +60,7 @@ repos:
additional_dependencies:
- flake8-typing-imports==1.14.0
- repo: https://github.com/asottile/blacken-docs
rev: 1.14.0
rev: 1.15.0
hooks:
- id: blacken-docs
- repo: https://github.com/pycqa/pydocstyle
Expand All @@ -62,7 +72,7 @@ repos:
args:
- --add-ignore=D107
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.4.0
rev: v1.4.1
hooks:
- id: mypy
args:
Expand All @@ -74,5 +84,5 @@ repos:
additional_dependencies:
- types-cryptography==3.3.23.2
- pytest-mypy==0.10.3
- binapy==0.6.0
- binapy==0.7.0
- freezegun==1.2.2
7 changes: 7 additions & 0 deletions HISTORY.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# History

## 0.9.0 (2023-07-24)
- Include `"typ": "JWT"` header by default when signing JWT tokens
- Added `RSAJwk.from_prime_numbers()` to generate a RSA private key from 2 prime numbers
- Code cleanups, packaging fixes & docs review

## 0.8.0 (2023-06-21)

- BREAKING CHANGE: all method parameters `jwk`, `sig_jwk`, `enc_jwk`, or `jwk_or_password`, accepting a `Jwk` instance
have been renamed to `key` or `sig_key`,`enc_key` or `key_or_password` respectively.
They now accept either a `Jwk` instance, or a dict containing a JWK, or a `cryptography` key instance directly.
Expand Down
168 changes: 91 additions & 77 deletions README.md

Large diffs are not rendered by default.

72 changes: 40 additions & 32 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,46 +10,53 @@ from jwskate import *

## Generating new keys


| Usage | Method |
|--------------------------|-----------------------------|
| ------------------------ | --------------------------- |
| For a specific algorithm | `Jwk.generate(alg='ES256')` |
| For a specific key type | `Jwk.generate(kty='RSA')` |

## Loading keys

| From | Method |
|--------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| A JWK in Python dict | `jwk = Jwk({'kty': 'RSA', 'n': '...', ...})` |
| A JWK in JSON formatted string | `jwk = Jwk('{"kty": "RSA", "n": "...", ...}')` |
| A `cryptography` key | `jwk = Jwk(cryptography_key, password='mypassword')` |
| A PEM formatted string | `jwk = Jwk.from_pem(`<br/>`'''-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAp0VYc2zc/6yNzQUSFprv`<br/>`... 3QIDAQAB`<br/>`-----END PUBLIC KEY----- '''`<br/>`)` |

| From | Method |
|-----------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| A JWK in Python dict | `jwk = Jwk({'kty': 'RSA', 'n': '...', ...})` |
| A JWK in JSON formatted string | `jwk = Jwk('{"kty": "RSA", "n": "...", ...}')` |
| A`cryptography` key | `jwk = Jwk(cryptography_key)` |
| A public key in a PEM formatted string | `jwk = Jwk.from_pem(`<br/>`'''-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAp0VYc2zc/6yNzQUSFprv`<br/>`... 3QIDAQAB`<br/>`-----END PUBLIC KEY----- '''`<br/>`)` |
| A private key in a PEM formatted string | `jwk = Jwk.from_pem(`<br/>`'''-----BEGIN PRIVATE KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAp0VYc2zc/6yNzQUSFprv`<br/>`... 3QIDAQAB`<br/>`-----END PRIVATE KEY----- ''',`<br/> `password=b'password_if_any'`<br/> `)` |
| A private key in a DER binary | `jwk = Jwk.from_der(b'der_formatted_binary')` |


## Saving keys

From an instance of a `Jwk` named `jwk`:

| To | Method | Note |
|--------------------------------|-------------------------------------|----------------------------------|
| A JWK in Python dict | `jwk` # Jwk is a dict subclass | you may also do `dict(jwk)` |
| A JWK in JSON formatted string | `jwk.to_json()` | |
| A cryptography key | `jwk.cryptography_key` | |
| A PEM formatted string | `jwk.to_pem(password="mypassword")` | password is optional |
| A symmetric key, as bytes | `jwk.key` | only works with kty=oct |
| A JWKS | `jwk.as_jwks()` | will contain `jwk` as single key |

| To | Method | Note |
| ------------------------------ | ----------------------------------- | ------------------------------- |
| A JWK in Python dict | `jwk` # Jwk is a dict subclass | you may also do`dict(jwk)` |
| A JWK in JSON formatted string | `jwk.to_json()` | |
| A cryptography key | `jwk.cryptography_key` | |
| A PEM formatted string | `jwk.to_pem(password="mypassword")` | password is optional |
| A symmetric key, as bytes | `jwk.key` | only works with kty=oct |
| A JWKS | `jwk.as_jwks()` | will contain`jwk` as single key |

## Inspecting keys

| Usage | Method | Returns |
|--------------------------|------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------|
| Check privateness | `jwk.is_private()` | `bool` |
| Check symmetricness | `jwk.is_symmetric()` | `bool` |
| Get Key Type | `jwk.kty` | key type, as `str` |
| Get Alg (if present) | `jwk.alg` | intended algorithm identifier, as `str` |
| Get Use | `jwk.use` | intended key use, if present, or deduced from `alg` |
| Get Key Ops | `jwk.key_ops` | intended key operations, if present,<br>or deduced from `use`, `kty`, privateness and symmetricness |
| Get attributes | `jwk['attribute']`<br>`jwk.attribute` | attribute value |
| Get thumbprint | `jwk.thumbprint()`<br>`jwk.thumbprint_uri()` | Computed thumbprint or thumbprint URI |
| Get supported algorithms | `jwk.supported_signing_algorithms()`<br>`jwk.supported_key_management_algorithms()`<br>`jwk.supported_encryption_algorithms()` | List of supported algorithms identifiers, as `str`. |

| Usage | Method | Returns |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------- |
| Check privateness | `jwk.is_private()` | `bool` |
| Check symmetricness | `jwk.is_symmetric()` | `bool` |
| Get Key Type | `jwk.kty` | key type, as`str` |
| Get Alg (if present) | `jwk.alg` | intended algorithm identifier, as`str` |
| Get Use | `jwk.use` | intended key use, if present, or deduced from`alg` |
| Get Key Ops | `jwk.key_ops` | intended key operations, if present,<br>or deduced from `use`, `kty`, privateness and symmetricness |
| Get attributes | `jwk['attribute']`<br>`jwk.attribute` | attribute value |
| Get thumbprint | `jwk.thumbprint()`<br>`jwk.thumbprint_uri()` | Computed thumbprint or thumbprint URI |
| Get supported algorithms | `jwk.supported_signing_algorithms()`<br>`jwk.supported_key_management_algorithms()`<br>`jwk.supported_encryption_algorithms()` | List of supported algorithms identifiers, as`str`. |

# JWK

Expand Down Expand Up @@ -125,7 +132,7 @@ daBAqhoDEr4SoKju8pagw6lqm65XeARyWkxqFqAZbb2K3bWY3x9qZT6oubLrCDGD

## Getting key parameters

Once you have a `Jwk` instance, you can get its parameters either with subscription or attribute access:
Once you have a `Jwk` instance, you can access its parameters either by using subscription or attributes:

```python
from jwskate import Jwk
Expand All @@ -145,8 +152,8 @@ assert jwk.x == "WtjnvHG9b_IKBLn4QYTHz-AdoAiO_ork5LH1BL_5tyI"
assert jwk["x"] == jwk.x
```

Those will return the exact (usually base64url-encoded) value exactly as expressed in the JWK. You can also get the
real, decoded parameters with some special attributes:
Those will return the exact (usually base64url-encoded) value, exactly as expressed in the JWK. You can also get the
real, decoded parameters with some special attributes, which depend on the key type (thus on the `Jwk` subclass):

```python
from jwskate import Jwk
Expand Down Expand Up @@ -180,8 +187,8 @@ The available special attributes vary depending on the key type.

`jwskate` can generate private keys from any of it supported key types. To generate a key, use `Jwk.generate()`. It just
needs some hints to know what kind of key to generate, either an identifier for the algorithm that will be used with
that key (`alg`), or a key type (`kty`). An `alg` is preferred, since it gives more hints, like the Elliptic Curve to
use, or the key size to generate. The specified `alg` will be part of the generated key, and will avoid having to
that key (`alg`), or a key type (`kty`). An `alg` is preferred, since it gives more hints to generate a key that is suitable
for its purpose. Those hints include the Elliptic Curve to use, or the key size to generate. The specified `alg` will be part of the generated key, and will avoid having to
specify the alg for every cryptographic operation you will perform with that key.

```python
Expand All @@ -201,8 +208,9 @@ oct_jwk = Jwk.generate(kty="oct")

# you may combine both if needed:
rsa_jwk = Jwk.generate(kty="RSA", alg="RS256")
# alg takes precedence if it is inconsistent with kty:
# a ValueError is raised if kty and alg are inconsistent:
rsa_jwk = Jwk.generate(kty="EC", alg="RS256")
# ValueError: Incompatible `alg='RS256'` and `kty='EC'` parameters. `alg='RS256'` points to `kty='RSA'`.
```

You can include additional parameters such as "use" or "key_ops", or custom parameters which will be included in the
Expand Down
2 changes: 2 additions & 0 deletions jwskate/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@

`jwskate` doesn't implement any actual cryptographic operation, it just
provides a set of convenient wrappers around the `cryptography` module.

"""
from __future__ import annotations

__author__ = """Guillaume Pujol"""
__email__ = "[email protected]"
Expand Down
1 change: 1 addition & 0 deletions jwskate/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
See [IANA JOSE](https://www.iana.org/assignments/jose/jose.xhtml).

"""
from __future__ import annotations


class SignatureAlgs:
Expand Down
1 change: 1 addition & 0 deletions jwskate/jwa/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
[RFC7518]: https://www.rfc-editor.org/rfc/rfc7518

"""
from __future__ import annotations

from .base import (
BaseAESEncryptionAlg,
Expand Down
32 changes: 16 additions & 16 deletions jwskate/jwa/base.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
"""This module implement base classes for the algorithms defined in JWA."""

from __future__ import annotations

from contextlib import contextmanager
from typing import Generic, Iterator, SupportsBytes, Tuple, Type, TypeVar, Union
from typing import Generic, Iterator, SupportsBytes, TypeVar

import cryptography.exceptions
from binapy import BinaPy
Expand Down Expand Up @@ -88,6 +87,7 @@ def supports_key(cls, key: bytes) -> bool:

Returns:
`True` if the key is suitable for this alg class, `False` otherwise

"""
try:
cls.check_key(key)
Expand All @@ -111,15 +111,15 @@ class BaseAsymmetricAlg(Generic[Kpriv, Kpub], BaseAlg):

"""

private_key_class: Union[Type[Kpriv], Tuple[Type[Kpriv], ...]]
public_key_class: Union[Type[Kpub], Tuple[Type[Kpub], ...]]
private_key_class: type[Kpriv] | tuple[type[Kpriv], ...]
public_key_class: type[Kpub] | tuple[type[Kpub], ...]

def __init__(self, key: Union[Kpriv, Kpub]):
def __init__(self, key: Kpriv | Kpub):
self.check_key(key)
self.key = key

@classmethod
def check_key(cls, key: Union[Kpriv, Kpub]) -> None:
def check_key(cls, key: Kpriv | Kpub) -> None:
"""Check that a given key is suitable for this alg class.

This must be implemented by subclasses as required.
Expand Down Expand Up @@ -183,7 +183,7 @@ class BaseSignatureAlg(BaseAlg):
use = "sig"
hashing_alg: hashes.HashAlgorithm

def sign(self, data: Union[bytes, SupportsBytes]) -> BinaPy:
def sign(self, data: bytes | SupportsBytes) -> BinaPy:
"""Sign arbitrary data, return the signature.

Args:
Expand All @@ -196,7 +196,7 @@ def sign(self, data: Union[bytes, SupportsBytes]) -> BinaPy:
raise NotImplementedError

def verify(
self, data: Union[bytes, SupportsBytes], signature: Union[bytes, SupportsBytes]
self, data: bytes | SupportsBytes, signature: bytes | SupportsBytes
) -> bool:
"""Verify a signature against some data.

Expand Down Expand Up @@ -258,11 +258,11 @@ def generate_iv(cls) -> BinaPy:

def encrypt(
self,
plaintext: Union[bytes, SupportsBytes],
plaintext: bytes | SupportsBytes,
*,
iv: Union[bytes, SupportsBytes],
aad: Union[bytes, SupportsBytes, None] = None,
) -> Tuple[BinaPy, BinaPy]:
iv: bytes | SupportsBytes,
aad: bytes | SupportsBytes | None = None,
) -> tuple[BinaPy, BinaPy]:
"""Encrypt arbitrary data, with optional Authenticated Encryption.

This needs as parameters:
Expand All @@ -286,11 +286,11 @@ def encrypt(

def decrypt(
self,
ciphertext: Union[bytes, SupportsBytes],
ciphertext: bytes | SupportsBytes,
*,
iv: Union[bytes, SupportsBytes],
auth_tag: Union[bytes, SupportsBytes],
aad: Union[bytes, SupportsBytes, None] = None,
iv: bytes | SupportsBytes,
auth_tag: bytes | SupportsBytes,
aad: bytes | SupportsBytes | None = None,
) -> BinaPy:
"""Decrypt and verify a ciphertext with Authenticated Encryption.

Expand Down
Loading