Skip to content

Commit

Permalink
0.4 (#2)
Browse files Browse the repository at this point in the history
_Breaking Changes:_

* rename `include_kid_thumbprint()` to `with_kid_thumbprint()`
* rename AES CBC HMAC classes from Aes???CbcHmacSha??? to A???CBC_HS???
* SignedJwt.audiences will always be a list (empty if no aud claim in token)

_Other changes and improvements:_

* support for signed then encrypted JWTs
* add `RSAJwk.key_size` property
* added tests for  `RSAJwk.key_size` property
* `Jwk.include_kid_thumbprint()` only returns a new instance when it modifies the kid
* add `to_pem()` in `Jwk` base class
* add `Jwk.is_symmetric` property
* add `use` and `key_ops` properties
* add methods to add or remove key usage parameters
* RSAJwk can compute the optional parameters if not present in the JWK. Add methods to add or remove those optional parameters.
* make sure key_ops are kept when using `public_jwk()` only when the param existed in the private JWK.
* add methods to get signature verification and encryption keys from JwkSet
* warn when generating EC keys with an implicit curve
* misc fixes and improvements
  • Loading branch information
guillp authored Sep 30, 2022
1 parent f5c13af commit be9ac80
Show file tree
Hide file tree
Showing 55 changed files with 2,733 additions and 809 deletions.
1 change: 1 addition & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ exclude_lines =
if 0:
if __name__ == .__main__.:
def main
\.\.\.
4 changes: 2 additions & 2 deletions .github/workflows/dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ jobs:
strategy:
matrix:
python-versions: [3.7, 3.8, 3.9, '3.10']
os: [ubuntu-18.04, macos-latest, windows-latest]
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}

# Steps represent a sequence of tasks that will be executed as part of the job
Expand Down Expand Up @@ -78,7 +78,7 @@ jobs:
poetry build
- name: publish to Test PyPI
uses: pypa/gh-action-pypi-publish@master
uses: pypa/gh-action-pypi-publish@release/v1
with:
user: __token__
password: ${{ secrets.TEST_PYPI_API_TOKEN}}
Expand Down
8 changes: 4 additions & 4 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/Lucas-C/pre-commit-hooks
rev: v1.3.0
rev: v1.3.1
hooks:
- id: forbid-crlf
- id: remove-crlf
Expand All @@ -15,7 +15,7 @@ repos:
- id: check-yaml
args: [--unsafe]
- repo: https://github.com/executablebooks/mdformat
rev: 0.7.14
rev: 0.7.16
hooks:
- id: mdformat
args:
Expand All @@ -32,7 +32,7 @@ repos:
hooks:
- id: isort
- repo: https://github.com/psf/black
rev: 22.6.0
rev: 22.8.0
hooks:
- id: black
- repo: https://gitlab.com/pycqa/flake8
Expand All @@ -53,7 +53,7 @@ repos:
args:
- --add-ignore=D107
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v0.971
rev: v0.981
hooks:
- id: mypy
args: [--strict]
Expand Down
243 changes: 146 additions & 97 deletions README.md

Large diffs are not rendered by default.

189 changes: 189 additions & 0 deletions docs/recipes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
This is a collection of recipes related to `jwskate` usage.

# JWK

## Generate private/public key pairs

```python
from jwskate import Jwk

private_jwk = (
Jwk.generate_for_alg("ES256") # select the signature or encryption alg here
.with_kid_thumbprint() # optionally, include a RFC7638 compliant thumbprint as kid
.with_usage_parameters() # optionally, include 'use' and 'key_ops'
)

print(private_jwk)
# {
# "kty": "EC",
# "crv": "P-256",
# "x": "fYI3VbV5MYEu3TNGU4fgEr5re_Pq_PfexDYvDomK3SY",
# "y": "BEe3LhDVW_MsFFwPeRxW_cnGLakXdE6cvLfSXwLe6Gk",
# "d": "Lce_08inNOEe6Q9xEGrR9T0CJNQa1o4EhGtDQYAI0N8",
# "alg": "ES256",
# "kid": "CzCOqostujy4iT3B55dkYYrSusaFvYjbCotGvo-e2gA",
# "use": "sig",
# "key_ops": [
# "sign"
# ]
# }

public_jwk = private_jwk.public_jwk()
print(public_jwk.to_json(indent=2))
# {
# "kty": "EC",
# "kid": "CzCOqostujy4iT3B55dkYYrSusaFvYjbCotGvo-e2gA",
# "alg": "ES256",
# "use": "sig",
# "key_ops": [
# "verify"
# ],
# "crv": "P-256",
# "x": "fYI3VbV5MYEu3TNGU4fgEr5re_Pq_PfexDYvDomK3SY",
# "y": "BEe3LhDVW_MsFFwPeRxW_cnGLakXdE6cvLfSXwLe6Gk"
# }

# let's expose this public key as a JWKS:
print(public_jwk.as_jwks().to_json(indent=2))
# {
# "keys": [
# {
# "kty": "EC",
# "kid": "CzCOqostujy4iT3B55dkYYrSusaFvYjbCotGvo-e2gA",
# "alg": "ES256",
# "use": "sig",
# "key_ops": [
# "verify"
# ],
# "crv": "P-256",
# "x": "fYI3VbV5MYEu3TNGU4fgEr5re_Pq_PfexDYvDomK3SY",
# "y": "BEe3LhDVW_MsFFwPeRxW_cnGLakXdE6cvLfSXwLe6Gk"
# }
# ]
# }
```

## Fetching a JWKS from a remote endpoint

```python
from jwskate import JwkSet
import requests

raw_jwks = requests.get("https://www.googleapis.com/oauth2/v3/certs").json()
jwkset = JwkSet(raw_jwks)

print(jwkset)
# {
# "keys": [
# ...
# ]
# }

# compared to a raw dict, a JwkSet offers convenience methods like:
if jwkset.is_private: # returns True if the jwks contains at least one private key
raise ValueError(
"JWKS contains private keys!"
) # an exposed JWKS should only contain public keys

my_jwk = jwkset.get_jwk_by_kid("my_key_id") # gets a key by key id (kid)
# select keys that is suitable for signature verification
verification_jwks = jwkset.verification_keys()
# select keys that are suitable for encryption
encryption_jwks = jwkset.encryption_keys()
```

## Converting between PEM key, JWK and `cryptography` keys

```python
from jwskate import Jwk

# generate a sample JWK, any asymmetric type will do:
private_jwk = (
Jwk.generate_for_alg("ES256") # generates the key
.with_usage_parameters() # adds use and key_ops
.with_kid_thumbprint() # adds the key thumbprint as kid
)
print(private_jwk.to_json(indent=2))
# {'kty': 'EC',
# 'crv': 'P-256',
# 'x': '8xX1CEhDNNjEySUKLw88YeiVwEOW34BWm0hBkAxqlVU',
# 'y': 'UfZ0JKT7MxdNMyqMKzKcAcYTcuqoXeplcJ3jNfnj3tM',
# 'd': 'T45KDokOKyuhEA92ri5a951c5kjmQfGyh1SrEkonb4s',
# 'alg': 'ES256',
# 'use': 'sig',
# 'key_ops': ['sign'],
# 'kid': '_E8_LoT4QEwctEkGNbiP9dogVDz6Lq9i8G_fj9nnEo0'}

# get the cryptography key that is wrapped in the Jwk:
cryptography_private_key = private_jwk.cryptography_key
print(type(cryptography_private_key))
# <class 'cryptography.hazmat.backends.openssl.ec._EllipticCurvePrivateKey'>

# create the PEM for the private key (encrypted with a password)
private_pem = private_jwk.to_pem("Th1s_P@ssW0rD_iS_5o_5tr0nG!")
print(private_pem.decode())
# -----BEGIN ENCRYPTED PRIVATE KEY-----
# MIHsMFcGCSqGSIb3DQEFDTBKMCkGCSqGSIb3DQEFDDAcBAhFd4nINf0/8QICCAAw
# DAYIKoZIhvcNAgkFADAdBglghkgBZQMEASoEEJNdsyMjSx3d6RqBBTuI5LoEgZD4
# qdPHcTZhKAuzQ9mkM1SlaZfiydWM2KFqPCYPLwoX+3kuCHPanMLlDxwOGN9XMRYl
# hG3eO0Gu4eWdc/2QEcXIyBCbyKnSwhaHUSSfkhyK9eh8diHQw+blOIImIldLPxnp
# +ABOhO6pCjQxM7I5op7RZuxLNWGLyAlfOOvawLfnM/wKLW6GXmlywu7PZ5qk9Bk=
# -----END ENCRYPTED PRIVATE KEY-----

# write this private PEM to a file:
with open("my_private_key.pem", "wb") as foutput:
foutput.write(private_pem)

# create the PEM for the public key (unencrypted)
public_jwk = private_jwk.public_jwk()
print(public_jwk)
# {
# "kty": "EC",
# "kid": "m-oFw9zA2YPFyqm265jbHnzXRa3SQ1ESdCE1AtAqO1U",
# "alg": "ES256",
# "use": "sig",
# "key_ops": [
# "verify"
# ],
# "crv": "P-256",
# "x": "VVbLOXwIgIFsYQSpnbLm5hr-ibfnIK0EeWYj2HXWvks",
# "y": "7f24WIqwHGr-jU9dH8GHpPEHMtAuXiwsedFnS6xayhk"
# }

# get the cryptography public key
cryptography_public_key = public_jwk.cryptography_key
print(type(cryptography_public_key))
# <class 'cryptography.hazmat.backends.openssl.ec._EllipticCurvePublicKey'>

# get the public PEM
public_pem = public_jwk.to_pem()
print(public_pem.decode())
# -----BEGIN PUBLIC KEY-----
# MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE8xX1CEhDNNjEySUKLw88YeiVwEOW
# 34BWm0hBkAxqlVVR9nQkpPszF00zKowrMpwBxhNy6qhd6mVwneM1+ePe0w==
# -----END PUBLIC KEY-----

# write this public PEM to a file:
with open("my_public_key.pem", "wb") as foutput:
foutput.write(public_pem)

# read the private PEM from file and load it as a Jwk:
with open("my_private_key.pem", "rb") as finput:
private_pem_from_file = finput.read()
private_jwk_from_file = (
Jwk.from_pem_key(private_pem_from_file, password="Th1s_P@ssW0rD_iS_5o_5tr0nG!")
.with_usage_parameters(alg="ES256") # adds back the alg, use and key_ops parameters
.with_kid_thumbprint() # adds back the thumbprint as kid
)
assert private_jwk_from_file == private_jwk

# read the public PEM from file and load it as a Jwk:
with open("my_public_key.pem", "rb") as finput:
public_pem_from_file = finput.read()
public_jwk_from_file = (
Jwk.from_pem_key(public_pem_from_file)
.with_usage_parameters(alg="ES256") # adds back the alg, use and key_ops parameters
.with_kid_thumbprint() # adds back the thumbprint as kid
)
assert public_jwk_from_file == public_jwk
```
19 changes: 18 additions & 1 deletion docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,9 @@ The available special attributes vary depending on the key type.

## Generating keys

You can generate a `Jwk` with the class method `Jwk.generate_for_kty()`. It needs the key type as parameter, and
### Based on a Key Type

You can generate a `Jwk` of a specific type (RSA, EC, etc.) using the class method `Jwk.generate_for_kty()`. It needs the key type as parameter, and
type-specific parameters:

```python
Expand All @@ -155,6 +157,21 @@ jwk = Jwk.generate_for_kty("EC", crv="P-256", use="sig")
assert jwk.use == "sig"
```

### Based on intended algorithm

You can generate a private key of the appropriate type for a given signature, key management or encryption algorithm
by using the method `Jwk.generate_for_alg()` this way:

```python
from jwskate import Jwk

ec_jwk = Jwk.generate_for_alg("ES512") # key of type EC, with crv=P-521
rsa_jwk = Jwk.generate_for_alg("RSA-OAEP-256", key_size=4096) # RSA
okp_jwk = Jwk.generate_for_alg("EdDSA") # EC, default to crv=P-256
okp_jwk = Jwk.generate_for_alg("EdDSA", crv="P-521") # EC, default to crv=P-256
okp_jwk = Jwk.generate_for_alg("HS512")
```

## Private and Public Keys

You can check if a key is public or private with the `is_private` property:
Expand Down
16 changes: 8 additions & 8 deletions jwskate/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,15 @@

from .enums import EncryptionAlgs, KeyManagementAlgs, SignatureAlgs
from .jwa import (
A128CBC_HS256,
A128GCM,
A128GCMKW,
A128KW,
A192CBC_HS384,
A192GCM,
A192GCMKW,
A192KW,
A256CBC_HS512,
A256GCM,
A256GCMKW,
A256KW,
Expand All @@ -41,9 +44,6 @@
RS512,
X448,
X25519,
Aes128CbcHmacSha256,
Aes192CbcHmacSha384,
Aes256CbcHmacSha512,
BaseAESEncryptionAlg,
BaseAlg,
BaseAsymmetricAlg,
Expand All @@ -62,6 +62,7 @@
Ed25519,
EdDsa,
EllipticCurve,
MismatchingAuthTag,
OKPCurve,
Pbes2_HS256_A128KW,
Pbes2_HS384_A192KW,
Expand Down Expand Up @@ -91,7 +92,6 @@
)
from .jws import InvalidJws, JwsCompact
from .jwt import (
EncryptedJwt,
ExpiredJwt,
InvalidClaim,
InvalidJwt,
Expand Down Expand Up @@ -132,9 +132,9 @@
"RS512",
"X448",
"X25519",
"Aes128CbcHmacSha256",
"Aes192CbcHmacSha384",
"Aes256CbcHmacSha512",
"A128CBC_HS256",
"A192CBC_HS384",
"A256CBC_HS512",
"BaseAESEncryptionAlg",
"BaseAlg",
"BaseAsymmetricAlg",
Expand All @@ -153,6 +153,7 @@
"Ed25519",
"EdDsa",
"EllipticCurve",
"MismatchingAuthTag",
"OKPCurve",
"Pbes2_HS256_A128KW",
"Pbes2_HS384_A192KW",
Expand Down Expand Up @@ -180,7 +181,6 @@
"UnsupportedOKPCurve",
"InvalidJws",
"JwsCompact",
"EncryptedJwt",
"ExpiredJwt",
"InvalidClaim",
"InvalidJwt",
Expand Down
11 changes: 8 additions & 3 deletions jwskate/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ class SignatureAlgs:
PS512,
EdDSA,
]
ALL = ALL_ASYMMETRIC + ALL_SYMMETRIC


class EncryptionAlgs:
Expand Down Expand Up @@ -83,9 +84,6 @@ class KeyManagementAlgs:
A192GCMKW,
A256GCMKW,
dir,
PBES2_HS256_A128KW,
PBES2_HS384_A192KW,
PBES2_HS512_A256KW,
]
ALL_ASYMMETRIC = [
RSA1_5,
Expand All @@ -98,3 +96,10 @@ class KeyManagementAlgs:
ECDH_ES_A192KW,
ECDH_ES_A256KW,
]
ALL_PASSWORD_BASED = [
PBES2_HS256_A128KW,
PBES2_HS384_A192KW,
PBES2_HS512_A256KW,
]
ALL_KEY_BASED = ALL_ASYMMETRIC + ALL_SYMMETRIC
ALL = ALL_ASYMMETRIC + ALL_SYMMETRIC + ALL_PASSWORD_BASED
Loading

0 comments on commit be9ac80

Please sign in to comment.